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:
parent
693155ca6a
commit
79d28822e8
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
|
Loading…
Reference in a new issue