1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-03-24 02:49:21 +00:00

Animated portrait support

This commit is contained in:
dawnDus 2022-02-06 21:52:19 -05:00 committed by alula
parent 693155ca6a
commit 79d28822e8
4 changed files with 94 additions and 10 deletions

View file

@ -1,4 +1,5 @@
use crate::common::{Color, Rect};
use crate::engine_constants::AnimatedFace;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
@ -9,22 +10,49 @@ use crate::scripting::tsc::text_script::{ConfirmSelection, TextScriptExecutionSt
use crate::shared_game_state::SharedGameState;
pub struct TextBoxes {
pub slide_in: u8,
pub anim_counter: usize,
animated_face: AnimatedFace,
}
const FACE_TEX: &str = "Face";
const SWITCH_FACE_TEX: [&str; 4] = ["Face1", "Face2", "Face3", "Face4"];
const SWITCH_FACE_TEX: [&str; 5] = ["Face1", "Face2", "Face3", "Face4", "Face5"];
impl TextBoxes {
pub fn new() -> TextBoxes {
TextBoxes { anim_counter: 0 }
TextBoxes {
slide_in: 7,
anim_counter: 0,
animated_face: AnimatedFace { face_id: 0, anim_id: 0, anim_frames: vec![(0, 0)] },
}
}
}
impl GameEntity<()> for TextBoxes {
fn tick(&mut self, state: &mut SharedGameState, _custom: ()) -> GameResult {
if state.textscript_vm.face != 0 {
self.slide_in = self.slide_in.saturating_sub(1);
self.anim_counter = self.anim_counter.wrapping_add(1);
let face_num = state.textscript_vm.face % 100;
let animation = state.textscript_vm.face % 1000 / 100;
if state.constants.textscript.animated_face_pics
&& (self.animated_face.anim_id != animation || self.animated_face.face_id != face_num)
{
self.animated_face = state
.constants
.animated_face_table
.clone()
.into_iter()
.find(|face| face.face_id == face_num && face.anim_id == animation)
.unwrap_or_else(|| AnimatedFace { face_id: face_num, anim_id: 0, anim_frames: vec![(0, 0)] });
}
if self.anim_counter > self.animated_face.anim_frames.first().unwrap().1 as usize {
self.animated_face.anim_frames.rotate_left(1);
self.anim_counter = 0;
}
}
Ok(())
}
@ -121,16 +149,16 @@ impl GameEntity<()> for TextBoxes {
graphics::set_clip_rect(ctx, Some(clip_rect))?;
let tex_name = if state.constants.textscript.animated_face_pics { SWITCH_FACE_TEX[0] } else { FACE_TEX };
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex_name)?;
// switch version uses 1xxx flag to show a flipped version of face
let flip = state.textscript_vm.face > 1000;
// x1xx flag shows a talking animation
let _talking = (state.textscript_vm.face % 1000) > 100;
let face_num = state.textscript_vm.face % 100;
let animation_frame = self.animated_face.anim_frames.first().unwrap().0 as usize;
let face_x = (4.0 + (self.anim_counter.min(7) - 1) as f32 * 8.0) - 52.0;
let tex_name =
if state.constants.textscript.animated_face_pics { SWITCH_FACE_TEX[animation_frame] } else { FACE_TEX };
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex_name)?;
let face_x = (4.0 + (6 - self.slide_in) as f32 * 8.0) - 52.0;
batch.add_rect_flip(
left_pos + 14.0 + face_x,

View file

@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::io::{Cursor, Read};
use std::io::{BufRead, BufReader, Cursor, Read};
use byteorder::{ReadBytesExt, LE};
use case_insensitive_hashmap::CaseInsensitiveHashMap;
@ -194,6 +194,13 @@ pub struct WorldConsts {
pub water_push_rect: Rect<u16>,
}
#[derive(Debug, Clone)]
pub struct AnimatedFace {
pub face_id: u16,
pub anim_id: u16,
pub anim_frames: Vec<(u16, u16)>,
}
#[derive(Debug, Copy, Clone)]
pub struct TextScriptConsts {
pub encoding: TextScriptEncoding,
@ -290,6 +297,7 @@ pub struct EngineConstants {
pub music_table: Vec<String>,
pub organya_paths: Vec<String>,
pub credit_illustration_paths: Vec<String>,
pub animated_face_table: Vec<AnimatedFace>,
}
impl Clone for EngineConstants {
@ -316,6 +324,7 @@ impl Clone for EngineConstants {
music_table: self.music_table.clone(),
organya_paths: self.organya_paths.clone(),
credit_illustration_paths: self.credit_illustration_paths.clone(),
animated_face_table: self.animated_face_table.clone(),
}
}
}
@ -1586,6 +1595,7 @@ impl EngineConstants {
"Resource/BITMAP/".to_owned(), // CSE2E
"endpic/".to_owned(), // NXEngine
],
animated_face_table: vec![AnimatedFace { face_id: 0, anim_id: 0, anim_frames: vec![(0, 0)] }],
}
}
@ -1650,6 +1660,7 @@ impl EngineConstants {
self.textscript.text_speed_fast = 0;
self.soundtracks.insert("Famitracks".to_owned(), "/base/ogg17/".to_owned());
self.soundtracks.insert("Ridiculon".to_owned(), "/base/ogg_ridic/".to_owned());
self.animated_face_table.push(AnimatedFace { face_id: 5, anim_id: 4, anim_frames: vec![(4, 0)] }); // Teethrog fix
self.game.tile_offset_x = 3;
}
@ -1713,4 +1724,48 @@ impl EngineConstants {
Ok(())
}
/// Load in the `faceanm.dat` file that details the Switch extensions to the <FAC command
/// It's actually a text file, go figure
pub fn load_animated_faces(&mut self, ctx: &mut Context) -> GameResult {
if filesystem::exists(ctx, "/base/faceanm.dat") {
let file = filesystem::open(ctx, "/base/faceanm.dat")?;
let buf = BufReader::new(file);
let mut face_id = 1;
let mut anim_id = 0;
for line in buf.lines() {
let line_str = line?.to_owned().replace(",", " ");
let mut anim_frames = Vec::new();
if line_str.find("\\") == None {
continue;
} else if line_str == "\\end" {
face_id += 1;
anim_id = 0;
continue;
}
for split in line_str.split_whitespace() {
// The animation labels aren't actually used
// There are also comments on some lines that we need to ignore
if split.find("\\") != None {
continue;
} else if split.find("//") != None {
break;
}
let mut parse = split.split(":");
let frame = (
parse.next().unwrap().parse::<u16>().unwrap_or(0),
parse.next().unwrap().parse::<u16>().unwrap_or(0),
);
anim_frames.push(frame);
}
self.animated_face_table.push(AnimatedFace { face_id, anim_id, anim_frames });
anim_id += 1;
}
}
Ok(())
}
}

View file

@ -991,7 +991,7 @@ impl TextScriptVM {
let face = read_cur_varint(&mut cursor)? as u16;
// Switch uses xx00 for face animation states
if face % 100 != state.textscript_vm.face % 100 {
game_scene.text_boxes.anim_counter = 0;
game_scene.text_boxes.slide_in = 7;
}
state.textscript_vm.face = face;

View file

@ -201,6 +201,7 @@ impl SharedGameState {
constants.apply_csplus_patches(&sound_manager);
constants.apply_csplus_nx_patches();
constants.load_csplus_tables(ctx)?;
constants.load_animated_faces(ctx)?;
base_path = "/base/";
} else if filesystem::exists(ctx, "/base/Nicalis.bmp") || filesystem::exists(ctx, "/base/Nicalis.png") {
info!("Cave Story+ (PC) data files detected.");