diff --git a/src/components/credits.rs b/src/components/credits.rs index b91f60f..7ff7e98 100644 --- a/src/components/credits.rs +++ b/src/components/credits.rs @@ -1,8 +1,10 @@ -use crate::common::Rect; +use crate::common::{Color, Rect}; use crate::entity::GameEntity; use crate::frame::Frame; use crate::framework::context::Context; use crate::framework::error::GameResult; +use crate::framework::graphics; +use crate::scripting::tsc::text_script::IllustrationState; use crate::shared_game_state::SharedGameState; pub struct Credits {} @@ -11,14 +13,48 @@ impl Credits { pub fn new() -> Credits { Credits {} } + + pub fn draw_tick(&mut self, state: &mut SharedGameState) { + match state.textscript_vm.illustration_state { + IllustrationState::FadeIn(mut x) => { + x += 40.0 * state.frame_time as f32; + + state.textscript_vm.illustration_state = + if x >= 0.0 { IllustrationState::Shown } else { IllustrationState::FadeIn(x) }; + } + IllustrationState::FadeOut(mut x) => { + x -= 40.0 * state.frame_time as f32; + + state.textscript_vm.illustration_state = + if x <= -160.0 { IllustrationState::Hidden } else { IllustrationState::FadeOut(x) }; + } + _ => (), + } + } } impl GameEntity<()> for Credits { - fn tick(&mut self, state: &mut SharedGameState, custom: ()) -> GameResult { + fn tick(&mut self, _state: &mut SharedGameState, _custom: ()) -> GameResult { Ok(()) } fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult { + if state.textscript_vm.illustration_state != IllustrationState::Hidden { + let x = match state.textscript_vm.illustration_state { + IllustrationState::FadeIn(x) | IllustrationState::FadeOut(x) => x, + _ => 0.0, + }; + + if let Some(tex) = &state.textscript_vm.current_illustration { + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex)?; + batch.add(x, 0.0); + batch.draw(ctx)?; + } else { + let rect = Rect::new_size((x * state.scale) as isize, 0, (160.0 * state.scale) as _, state.screen_size.1 as _); + graphics::draw_rect(ctx, rect, Color::from_rgb(0, 0, 32))?; + } + } + if state.creditscript_vm.lines.is_empty() { return Ok(()); } diff --git a/src/engine_constants/mod.rs b/src/engine_constants/mod.rs index 9063299..aa8d08e 100644 --- a/src/engine_constants/mod.rs +++ b/src/engine_constants/mod.rs @@ -266,6 +266,7 @@ pub struct EngineConstants { pub soundtracks: HashMap, pub music_table: Vec, pub organya_paths: Vec, + pub credit_illustration_paths: Vec, } impl Clone for EngineConstants { @@ -291,6 +292,7 @@ impl Clone for EngineConstants { soundtracks: self.soundtracks.clone(), music_table: self.music_table.clone(), organya_paths: self.organya_paths.clone(), + credit_illustration_paths: self.credit_illustration_paths.clone(), } } } @@ -1287,6 +1289,27 @@ impl EngineConstants { "Bullet" => (320, 176), "Caret" => (320, 240), "casts" => (320, 240), + "Credit01" => (160, 240), + "Credit01a" => (160, 240), + "Credit02" => (160, 240), + "Credit02a" => (160, 240), + "Credit03" => (160, 240), + "Credit03a" => (160, 240), + "Credit04" => (160, 240), + "Credit05" => (160, 240), + "Credit06" => (160, 240), + "Credit07" => (160, 240), + "Credit08" => (160, 240), + "Credit09" => (160, 240), + "Credit10" => (160, 240), + "Credit11" => (160, 240), + "Credit12" => (160, 240), + "Credit13" => (160, 240), + "Credit14" => (160, 240), + "Credit15" => (160, 240), + "Credit16" => (160, 240), + "Credit17" => (160, 240), + "Credit18" => (160, 240), "Face" => (288, 240), "Face_0" => (288, 240), // nxengine "Face_1" => (288, 240), // nxengine @@ -1487,6 +1510,11 @@ impl EngineConstants { "/base/Org/".to_owned(), // CS+ "/Resource/ORG/".to_owned(), // CSE2E ], + credit_illustration_paths: vec![ + "".to_owned(), + "Resource/BITMAP/".to_owned(), // CSE2E + "endpic/".to_owned(), // NXEngine + ], } } diff --git a/src/frame.rs b/src/frame.rs index 7dff3c1..1c1aad6 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -35,18 +35,23 @@ impl Frame { } pub fn immediate_update(&mut self, state: &mut SharedGameState, stage: &Stage) { + let mut screen_width = state.canvas_size.0; + if state.constants.is_switch { + screen_width += 10.0; // hack for scrolling + } + let tile_size = state.tile_size.as_int(); - if (stage.map.width as usize).saturating_sub(1) * (tile_size as usize) < state.canvas_size.0 as usize { - self.x = -(((state.canvas_size.0 as i32 - (stage.map.width as i32 - 1) * tile_size) * 0x200) / 2); + if (stage.map.width as usize).saturating_sub(1) * (tile_size as usize) < screen_width as usize { + self.x = -(((screen_width as i32 - (stage.map.width as i32 - 1) * tile_size) * 0x200) / 2); } else { - self.x = self.target_x - (state.canvas_size.0 as i32 * 0x200 / 2); + self.x = self.target_x - (screen_width as i32 * 0x200 / 2); if self.x < 0 { self.x = 0; } - let max_x = (((stage.map.width as i32 - 1) * tile_size) - state.canvas_size.0 as i32) * 0x200; + let max_x = (((stage.map.width as i32 - 1) * tile_size) - screen_width as i32) * 0x200; if self.x > max_x { self.x = max_x; } @@ -72,18 +77,22 @@ impl Frame { } pub fn update(&mut self, state: &mut SharedGameState, stage: &Stage) { + let mut screen_width = state.canvas_size.0; + if state.constants.is_switch { + screen_width += 10.0; + } let tile_size = state.tile_size.as_int(); - if (stage.map.width as usize).saturating_sub(1) * (tile_size as usize) < state.canvas_size.0 as usize { - self.x = -(((state.canvas_size.0 as i32 - (stage.map.width as i32 - 1) * tile_size) * 0x200) / 2); + if (stage.map.width as usize).saturating_sub(1) * (tile_size as usize) < screen_width as usize { + self.x = -(((screen_width as i32 - (stage.map.width as i32 - 1) * tile_size) * 0x200) / 2); } else { - self.x += (self.target_x - (state.canvas_size.0 as i32 * 0x200 / 2) - self.x) / self.wait; + self.x += (self.target_x - (screen_width as i32 * 0x200 / 2) - self.x) / self.wait; if self.x < 0 { self.x = 0; } - let max_x = (((stage.map.width as i32 - 1) * tile_size) - state.canvas_size.0 as i32) * 0x200; + let max_x = (((stage.map.width as i32 - 1) * tile_size) - screen_width as i32) * 0x200; if self.x > max_x { self.x = max_x; } diff --git a/src/framework/backend.rs b/src/framework/backend.rs index 486315f..bcce639 100644 --- a/src/framework/backend.rs +++ b/src/framework/backend.rs @@ -75,7 +75,7 @@ pub trait BackendTexture { } #[allow(unreachable_code)] -pub fn init_backend(headless: bool) -> GameResult> { +pub fn init_backend(headless: bool, size_hint: (u16, u16)) -> GameResult> { if headless { return crate::framework::backend_null::NullBackend::new(); } @@ -87,7 +87,7 @@ pub fn init_backend(headless: bool) -> GameResult> { #[cfg(feature = "backend-sdl")] { - return crate::framework::backend_sdl2::SDL2Backend::new(); + return crate::framework::backend_sdl2::SDL2Backend::new(size_hint); } log::warn!("No backend compiled in, using null backend instead."); diff --git a/src/framework/backend_sdl2.rs b/src/framework/backend_sdl2.rs index b185421..87b4ba3 100644 --- a/src/framework/backend_sdl2.rs +++ b/src/framework/backend_sdl2.rs @@ -31,13 +31,14 @@ use crate::GAME_SUSPENDED; pub struct SDL2Backend { context: Sdl, + size_hint: (u16, u16), } impl SDL2Backend { - pub fn new() -> GameResult> { + pub fn new(size_hint: (u16, u16)) -> GameResult> { let context = sdl2::init().map_err(|e| GameError::WindowError(e))?; - let backend = SDL2Backend { context }; + let backend = SDL2Backend { context, size_hint }; Ok(Box::new(backend)) } @@ -45,7 +46,7 @@ impl SDL2Backend { impl Backend for SDL2Backend { fn create_event_loop(&self) -> GameResult> { - SDL2EventLoop::new(&self.context) + SDL2EventLoop::new(&self.context, self.size_hint) } } @@ -64,7 +65,7 @@ struct SDL2Context { } impl SDL2EventLoop { - pub fn new(sdl: &Sdl) -> GameResult> { + pub fn new(sdl: &Sdl, size_hint: (u16, u16)) -> GameResult> { let event_pump = sdl.event_pump().map_err(|e| GameError::WindowError(e))?; let video = sdl.video().map_err(|e| GameError::WindowError(e))?; let gl_attr = video.gl_attr(); @@ -72,7 +73,7 @@ impl SDL2EventLoop { gl_attr.set_context_profile(GLProfile::Core); gl_attr.set_context_version(3, 0); - let mut window = video.window("Cave Story (doukutsu-rs)", 640, 480); + let mut window = video.window("Cave Story (doukutsu-rs)", size_hint.0 as _, size_hint.1 as _); window.position_centered(); window.resizable(); diff --git a/src/framework/context.rs b/src/framework/context.rs index 3a641c9..a12e658 100644 --- a/src/framework/context.rs +++ b/src/framework/context.rs @@ -6,6 +6,7 @@ use crate::Game; pub struct Context { pub headless: bool, + pub size_hint: (u16, u16), pub(crate) filesystem: Filesystem, pub(crate) renderer: Option>, pub(crate) keyboard_context: KeyboardContext, @@ -18,6 +19,7 @@ impl Context { pub fn new() -> Context { Context { headless: false, + size_hint: (640, 480), filesystem: Filesystem::new(), renderer: None, keyboard_context: KeyboardContext::new(), @@ -28,7 +30,7 @@ impl Context { } pub fn run(&mut self, game: &mut Game) -> GameResult { - let backend = init_backend(self.headless)?; + let backend = init_backend(self.headless, self.size_hint)?; let mut event_loop = backend.create_event_loop()?; self.renderer = Some(event_loop.new_renderer()?); diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 813b1f5..d2ee6a3 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -2018,6 +2018,7 @@ impl Scene for GameScene { }; self.inventory_dim = self.inventory_dim.clamp(0.0, 1.0); + self.credits.draw_tick(state); Ok(()) } diff --git a/src/scripting/tsc/text_script.rs b/src/scripting/tsc/text_script.rs index 3e9e390..a097918 100644 --- a/src/scripting/tsc/text_script.rs +++ b/src/scripting/tsc/text_script.rs @@ -97,6 +97,14 @@ pub enum TextScriptExecutionState { Reset, } +#[derive(PartialEq, Copy, Clone)] +pub enum IllustrationState { + Hidden, + Shown, + FadeIn(f32), + FadeOut(f32), +} + pub struct TextScriptVM { pub scripts: Rc>, pub state: TextScriptExecutionState, @@ -117,6 +125,8 @@ pub struct TextScriptVM { pub line_1: Vec, pub line_2: Vec, pub line_3: Vec, + pub current_illustration: Option, + pub illustration_state: IllustrationState, prev_char: char, } @@ -180,6 +190,8 @@ impl TextScriptVM { line_1: Vec::with_capacity(24), line_2: Vec::with_capacity(24), line_3: Vec::with_capacity(24), + current_illustration: None, + illustration_state: IllustrationState::Hidden, prev_char: '\x00', } } @@ -219,6 +231,8 @@ impl TextScriptVM { pub fn reset(&mut self) { self.state = TextScriptExecutionState::Ended; self.flags.0 = 0; + self.current_illustration = None; + self.illustration_state = IllustrationState::Hidden; self.clear_text_box(); } @@ -1520,10 +1534,33 @@ impl TextScriptVM { exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } + TSCOpCode::SIL => { + let number = read_cur_varint(&mut cursor)? as u16; + + for path in state.constants.credit_illustration_paths.iter() { + let path = format!("{}Credit{:02}", path, number); + if let Some(_) = state.texture_set.find_texture(ctx, &path) { + state.textscript_vm.current_illustration = Some(path); + break; + } + } + + state.textscript_vm.illustration_state = IllustrationState::FadeIn(-160.0); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::CIL => { + state.textscript_vm.illustration_state = if let Some(_) = state.textscript_vm.current_illustration { + IllustrationState::FadeOut(0.0) + } else { + IllustrationState::Hidden + }; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } // unimplemented opcodes // Zero operands - TSCOpCode::CIL - | TSCOpCode::CPS + TSCOpCode::CPS | TSCOpCode::KE2 | TSCOpCode::CSS | TSCOpCode::MLP @@ -1536,7 +1573,7 @@ impl TextScriptVM { exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } // One operand codes - TSCOpCode::UNJ | TSCOpCode::XX1 | TSCOpCode::SIL | TSCOpCode::SSS | TSCOpCode::ACH => { + TSCOpCode::UNJ | TSCOpCode::XX1 | TSCOpCode::SSS | TSCOpCode::ACH => { let par_a = read_cur_varint(&mut cursor)?; log::warn!("unimplemented opcode: {:?} {}", op, par_a); diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 2c76ea1..254cde2 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -164,6 +164,7 @@ impl SharedGameState { base_path = "/base/"; } else if filesystem::exists(ctx, "/base/lighting.tbl") { info!("Cave Story+ (Switch) data files detected."); + ctx.size_hint = (854, 480); constants.apply_csplus_patches(&sound_manager); constants.apply_csplus_nx_patches(); base_path = "/base/"; diff --git a/src/texture_set.rs b/src/texture_set.rs index d181b1a..1815364 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -354,18 +354,26 @@ impl TextureSet { create_texture(ctx, width as u16, height as u16, &img) } + pub fn find_texture( + &self, + ctx: &mut Context, + name: &str, + ) -> Option { + self + .paths + .iter() + .find_map(|s| { + FILE_TYPES.iter().map(|ext| [s, name, ext].join("")).find(|path| filesystem::exists(ctx, path)) + }) + } + pub fn load_texture( &self, ctx: &mut Context, constants: &EngineConstants, name: &str, ) -> GameResult> { - let path = self - .paths - .iter() - .find_map(|s| { - FILE_TYPES.iter().map(|ext| [s, name, ext].join("")).find(|path| filesystem::exists(ctx, path)) - }) + let path = self.find_texture(ctx, name) .ok_or_else(|| GameError::ResourceLoadError(format!("Texture {} does not exist.", name)))?; let has_glow_layer = self @@ -376,7 +384,7 @@ impl TextureSet { }) .is_some(); - info!("Loading texture: {}", path); + info!("Loading texture: {} -> {}", name, path); let batch = self.load_image(ctx, &path)?; let size = batch.dimensions();