diff --git a/src/components/text_boxes.rs b/src/components/text_boxes.rs index 63ea38b..bf2bceb 100644 --- a/src/components/text_boxes.rs +++ b/src/components/text_boxes.rs @@ -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, diff --git a/src/engine_constants/mod.rs b/src/engine_constants/mod.rs index ed8fc86..aba5341 100644 --- a/src/engine_constants/mod.rs +++ b/src/engine_constants/mod.rs @@ -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(()) + } } diff --git a/src/scripting/tsc/text_script.rs b/src/scripting/tsc/text_script.rs index 0defeeb..4092bd4 100644 --- a/src/scripting/tsc/text_script.rs +++ b/src/scripting/tsc/text_script.rs @@ -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; diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 0f2edd6..5750220 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -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.");