diff --git a/src/engine_constants/mod.rs b/src/engine_constants/mod.rs index 0253898..517c04b 100644 --- a/src/engine_constants/mod.rs +++ b/src/engine_constants/mod.rs @@ -337,6 +337,7 @@ pub struct EngineConstants { pub music_table: Vec, pub organya_paths: Vec, pub credit_illustration_paths: Vec, + pub player_skin_paths: Vec, pub animated_face_table: Vec, pub string_table: HashMap, pub missile_flags: Vec, @@ -369,6 +370,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(), + player_skin_paths: self.player_skin_paths.clone(), animated_face_table: self.animated_face_table.clone(), string_table: self.string_table.clone(), missile_flags: self.missile_flags.clone(), @@ -1433,6 +1435,7 @@ impl EngineConstants { "ItemImage" => (256, 128), "Loading" => (64, 8), "MyChar" => (200, 64), + "mychar_p2" => (200, 384), // switch "Npc/Npc0" => (32, 32), "Npc/NpcAlmo1" => (320, 240), "Npc/NpcAlmo2" => (320, 240), @@ -1665,6 +1668,7 @@ impl EngineConstants { "Resource/BITMAP/".to_owned(), // CSE2E "endpic/".to_owned(), // NXEngine ], + player_skin_paths: vec!["MyChar".to_owned()], animated_face_table: vec![AnimatedFace { face_id: 0, anim_id: 0, anim_frames: vec![(0, 0)] }], string_table: HashMap::new(), missile_flags: vec![200, 201, 202, 218, 550, 766, 880, 920, 1551], @@ -1769,6 +1773,7 @@ impl EngineConstants { self.textscript.fade_ticks = 21; self.game.tile_offset_x = 3; self.game.new_game_player_pos = (13, 8); + self.player_skin_paths.push("mychar_p2".to_owned()); } pub fn apply_csdemo_patches(&mut self) { diff --git a/src/framework/backend_sdl2.rs b/src/framework/backend_sdl2.rs index d470dac..ab22187 100644 --- a/src/framework/backend_sdl2.rs +++ b/src/framework/backend_sdl2.rs @@ -497,14 +497,15 @@ struct SDL2Gamepad { } impl SDL2Gamepad { - fn new(inner: GameController) -> Box { + pub fn new(inner: GameController) -> Box { Box::new(SDL2Gamepad { inner }) } } impl BackendGamepad for SDL2Gamepad { fn set_rumble(&mut self, low_freq: u16, high_freq: u16, duration_ms: u32) -> GameResult { - self.inner.set_rumble(low_freq, high_freq, duration_ms).map_err(|e| GameError::GamepadError(e.to_string())) + let _ = self.inner.set_rumble(low_freq, high_freq, duration_ms); + Ok(()) } fn instance_id(&self) -> u32 { diff --git a/src/game/player/mod.rs b/src/game/player/mod.rs index f3953d4..218e8a2 100644 --- a/src/game/player/mod.rs +++ b/src/game/player/mod.rs @@ -3,7 +3,7 @@ use std::clone::Clone; use num_derive::FromPrimitive; use num_traits::clamp; -use crate::common::{Condition, Direction, Equipment, Flag, interpolate_fix9_scale, Rect}; +use crate::common::{interpolate_fix9_scale, Condition, Direction, Equipment, Flag, Rect}; use crate::components::number_popup::NumberPopup; use crate::entity::GameEntity; use crate::framework::context::Context; @@ -12,8 +12,8 @@ use crate::game::caret::CaretType; use crate::game::frame::Frame; use crate::game::npc::list::NPCList; use crate::game::npc::NPC; -use crate::game::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSkin}; use crate::game::player::skin::basic::BasicPlayerSkin; +use crate::game::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSkin}; use crate::game::shared_game_state::SharedGameState; use crate::input::dummy_player_controller::DummyPlayerController; use crate::input::player_controller::PlayerController; @@ -189,6 +189,12 @@ impl Player { } } + pub fn load_skin(&mut self, texture_name: String, state: &mut SharedGameState, ctx: &mut Context) { + self.skin = Box::new(BasicPlayerSkin::new(texture_name, state, ctx)); + self.display_bounds = self.skin.get_display_bounds(); + self.hit_bounds = self.skin.get_hit_bounds(); + } + fn tick_normal(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult { if !state.control_flags.interactions_disabled() && state.control_flags.control_enabled() { if self.equip.has_air_tank() { @@ -402,10 +408,10 @@ impl Player { // stop interacting when moved if state.control_flags.control_enabled() && (self.controller.move_left() - || self.controller.move_right() - || self.controller.move_up() - || self.controller.jump() - || self.controller.shoot()) + || self.controller.move_right() + || self.controller.move_up() + || self.controller.jump() + || self.controller.shoot()) { self.cond.set_interacted(false); } @@ -524,8 +530,8 @@ impl Player { if (self.flags.hit_bottom_wall() && self.flags.hit_right_higher_half() && self.vel_x < 0) || (self.flags.hit_bottom_wall() && self.flags.hit_left_higher_half() && self.vel_x > 0) || (self.flags.hit_bottom_wall() - && self.flags.hit_left_lower_half() - && self.flags.hit_right_lower_half()) + && self.flags.hit_left_lower_half() + && self.flags.hit_right_lower_half()) { self.vel_y = 0x400; // 2.0fix9 } @@ -533,9 +539,9 @@ impl Player { let max_move = if self.flags.in_water() && !(self.flags.force_left() - || self.flags.force_up() - || self.flags.force_right() - || self.flags.force_down()) + || self.flags.force_up() + || self.flags.force_right() + || self.flags.force_down()) { state.constants.player.water_physics.max_move } else { diff --git a/src/game/shared_game_state.rs b/src/game/shared_game_state.rs index 4a8fe35..b8f326b 100644 --- a/src/game/shared_game_state.rs +++ b/src/game/shared_game_state.rs @@ -251,6 +251,24 @@ impl TileSize { } } +#[derive(PartialEq, Eq, Copy, Clone)] +pub struct PlayerSkinLocation { + pub texture_index: u16, + pub offset: u16, +} + +impl PlayerSkinLocation { + pub const fn new(texture_index: u16, offset: u16) -> PlayerSkinLocation { + PlayerSkinLocation { texture_index, offset } + } +} + +impl Default for PlayerSkinLocation { + fn default() -> PlayerSkinLocation { + PlayerSkinLocation::new(0, 0) + } +} + pub struct SharedGameState { pub control_flags: ControlFlags, pub game_flags: BitVec, @@ -301,7 +319,7 @@ pub struct SharedGameState { pub difficulty: GameDifficulty, pub player_count: PlayerCount, pub player_count_modified_in_game: bool, - pub player2_skin: u16, + pub player2_skin_location: PlayerSkinLocation, pub replay_state: ReplayState, pub mod_requirements: ModRequirements, pub loc: Locale, @@ -455,7 +473,7 @@ impl SharedGameState { difficulty: GameDifficulty::Normal, player_count: PlayerCount::One, player_count_modified_in_game: false, - player2_skin: 0, + player2_skin_location: PlayerSkinLocation::default(), replay_state: ReplayState::None, mod_requirements, loc: locale, diff --git a/src/live_debugger/mod.rs b/src/live_debugger/mod.rs index fcc9268..b6c7595 100644 --- a/src/live_debugger/mod.rs +++ b/src/live_debugger/mod.rs @@ -3,9 +3,9 @@ use itertools::Itertools; use crate::framework::context::Context; use crate::framework::error::GameResult; +use crate::game::scripting::tsc::text_script::TextScriptExecutionState; use crate::game::shared_game_state::SharedGameState; use crate::scene::game_scene::GameScene; -use crate::game::scripting::tsc::text_script::TextScriptExecutionState; use self::command_line::CommandLineParser; @@ -187,22 +187,22 @@ impl LiveDebugger { } #[cfg(feature = "scripting-lua")] - { - ui.same_line(); - if ui.button("Reload Lua Scripts") { - if let Err(err) = state.lua.reload_scripts(ctx) { - log::error!("Error reloading scripts: {:?}", err); - self.error = Some(ImString::new(err.to_string())); - } + { + ui.same_line(); + if ui.button("Reload Lua Scripts") { + if let Err(err) = state.lua.reload_scripts(ctx) { + log::error!("Error reloading scripts: {:?}", err); + self.error = Some(ImString::new(err.to_string())); } } + } if game_scene.player2.cond.alive() { if ui.button("Drop Player 2") { game_scene.drop_player2(); } } else if ui.button("Add Player 2") { - game_scene.add_player2(state); + game_scene.add_player2(state, ctx); } ui.same_line(); @@ -216,7 +216,8 @@ impl LiveDebugger { let _ = state.save_game(game_scene, ctx); state.sound_manager.play_sfx(18); } - } else if ui.button("Busy") {} + } else if ui.button("Busy") { + } ui.same_line(); if ui.button("Hotkey List") { diff --git a/src/menu/coop_menu.rs b/src/menu/coop_menu.rs index 6643edd..cbe9f2c 100644 --- a/src/menu/coop_menu.rs +++ b/src/menu/coop_menu.rs @@ -2,8 +2,8 @@ use crate::framework::context::Context; use crate::framework::error::GameResult; use crate::game::shared_game_state::{PlayerCount, SharedGameState}; use crate::input::combined_menu_controller::CombinedMenuController; -use crate::menu::{Menu, MenuSelectionResult}; use crate::menu::MenuEntry; +use crate::menu::{Menu, MenuSelectionResult}; pub enum CurrentMenu { CoopMenu, @@ -59,20 +59,26 @@ impl PlayerCountMenu { self.coop_menu = Menu::new(0, 0, 130, 0); self.skin_menu = Menu::new(0, 0, 130, 0); - self.coop_menu.push_entry(CoopMenuEntry::Title, MenuEntry::Disabled(state.loc.t("menus.coop_menu.title").to_owned())); + self.coop_menu + .push_entry(CoopMenuEntry::Title, MenuEntry::Disabled(state.loc.t("menus.coop_menu.title").to_owned())); self.coop_menu.push_entry(CoopMenuEntry::One, MenuEntry::Active(state.loc.t("menus.coop_menu.one").to_owned())); self.coop_menu.push_entry(CoopMenuEntry::Two, MenuEntry::Active(state.loc.t("menus.coop_menu.two").to_owned())); self.coop_menu.push_entry(CoopMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned())); self.coop_menu.selected = CoopMenuEntry::One; - self.skin_menu.push_entry(SkinMenuEntry::Title, MenuEntry::Disabled(state.loc.t("menus.skin_menu.title").to_owned())); + self.skin_menu + .push_entry(SkinMenuEntry::Title, MenuEntry::Disabled(state.loc.t("menus.skin_menu.title").to_owned())); self.skin_menu.push_entry(SkinMenuEntry::Skin, MenuEntry::PlayerSkin); if self.on_title { - self.skin_menu.push_entry(SkinMenuEntry::Start, MenuEntry::Active(state.loc.t("menus.main_menu.start").to_owned())); + self.skin_menu + .push_entry(SkinMenuEntry::Start, MenuEntry::Active(state.loc.t("menus.main_menu.start").to_owned())); } else { - self.skin_menu.push_entry(SkinMenuEntry::Add, MenuEntry::Active(state.loc.t("menus.pause_menu.add_player2").to_owned())); + self.skin_menu.push_entry( + SkinMenuEntry::Add, + MenuEntry::Active(state.loc.t("menus.pause_menu.add_player2").to_owned()), + ); } self.skin_menu.push_entry(SkinMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned())); @@ -136,7 +142,24 @@ impl PlayerCountMenu { } } MenuSelectionResult::Selected(SkinMenuEntry::Skin, _) => { - state.player2_skin += 2; + state.player2_skin_location.offset += 2; + + let current_skin_spritesheet_name = + state.constants.player_skin_paths[state.player2_skin_location.texture_index as usize].as_str(); + + if let Some(tex_size) = state.constants.tex_sizes.get(current_skin_spritesheet_name) { + // TODO: should probably have a way to figure out the height from the spritesheet ahead of time + + if state.player2_skin_location.offset * 2 * 16 >= tex_size.1 { + state.player2_skin_location.offset = 0; + + if state.player2_skin_location.texture_index == 1 { + state.player2_skin_location.texture_index = 0; + } else { + state.player2_skin_location.texture_index = 1; + } + } + } } MenuSelectionResult::Selected(SkinMenuEntry::Start, _) => { state.player_count = PlayerCount::Two; diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 14d0948..18264e4 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -375,62 +375,90 @@ impl Menu { for (_, entry) in &self.entries { match entry { MenuEntry::Active(name) | MenuEntry::DisabledWhite(name) => { - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(name, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + name, + ctx, + &state.constants, + &mut state.texture_set, + )?; } MenuEntry::Disabled(name) => { - state.font.builder() - .position(self.x as f32 + 20.0, y) - .color((0xa0, 0xa0, 0xff, 0xff)) - .draw(name, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).color((0xa0, 0xa0, 0xff, 0xff)).draw( + name, + ctx, + &state.constants, + &mut state.texture_set, + )?; } MenuEntry::Toggle(name, value) => { let value_text = if *value { "ON" } else { "OFF" }; let name_text_len = state.font.builder().compute_width(name); - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(name, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + name, + ctx, + &state.constants, + &mut state.texture_set, + )?; - state.font.builder() - .position(self.x as f32 + 25.0 + name_text_len, y) - .draw(value_text, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 25.0 + name_text_len, y).draw( + value_text, + ctx, + &state.constants, + &mut state.texture_set, + )?; } MenuEntry::Options(name, index, value) => { let value_text = if let Some(text) = value.get(*index) { text } else { "???" }; let name_text_len = state.font.builder().compute_width(name); - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(name, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + name, + ctx, + &state.constants, + &mut state.texture_set, + )?; - state.font.builder() - .position(self.x as f32 + 25.0 + name_text_len, y) - .draw(value_text, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 25.0 + name_text_len, y).draw( + value_text, + ctx, + &state.constants, + &mut state.texture_set, + )?; } MenuEntry::DescriptiveOptions(name, index, value, description) => { let value_text = if let Some(text) = value.get(*index) { text } else { "???" }; let description_text = if let Some(text) = description.get(*index) { text } else { "???" }; let name_text_len = state.font.builder().compute_width(name); - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(name, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + name, + ctx, + &state.constants, + &mut state.texture_set, + )?; - state.font.builder() - .position(self.x as f32 + 25.0 + name_text_len, y) - .draw(value_text, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 25.0 + name_text_len, y).draw( + value_text, + ctx, + &state.constants, + &mut state.texture_set, + )?; - state.font.builder() + state + .font + .builder() .position(self.x as f32 + 20.0, y + 16.0) .color((0xc0, 0xc0, 0xff, 0xff)) .draw(description_text, ctx, &state.constants, &mut state.texture_set)?; } MenuEntry::OptionsBar(name, percent) => { - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(name, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + name, + ctx, + &state.constants, + &mut state.texture_set, + )?; if state.constants.is_switch || state.constants.is_cs_plus { let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "ui")?; @@ -469,20 +497,29 @@ impl Menu { } } MenuEntry::NewSave => { - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(state.loc.t("menus.save_menu.new"), ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + state.loc.t("menus.save_menu.new"), + ctx, + &state.constants, + &mut state.texture_set, + )?; } MenuEntry::PlayerSkin => { - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(state.loc.t("menus.skin_menu.label"), ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + state.loc.t("menus.skin_menu.label"), + ctx, + &state.constants, + &mut state.texture_set, + )?; - let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "MyChar")?; + let spritesheet_name = + state.constants.player_skin_paths[state.player2_skin_location.texture_index as usize].as_str(); + + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, spritesheet_name)?; batch.add_rect( self.x as f32 + 88.0, y - 4.0, - &Rect::new_size(0, (state.player2_skin).saturating_mul(2 * 16), 16, 16), + &Rect::new_size(0, (state.player2_skin_location.offset).saturating_mul(2 * 16), 16, 16), ); batch.draw(ctx)?; } @@ -496,9 +533,12 @@ impl Menu { let bar_width = (save.life as f32 / save.max_life as f32 * 39.0) as u16; let right_edge = self.x as f32 + self.width as f32 - 4.0; - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(name, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + name, + ctx, + &state.constants, + &mut state.texture_set, + )?; if valid_save { // Lifebar @@ -529,9 +569,12 @@ impl Menu { _ => difficulty_name.push_str("(unknown)"), } - state.font.builder() - .position(self.x as f32 + 20.0, y + 10.0) - .draw(difficulty_name.as_str(), ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y + 10.0).draw( + difficulty_name.as_str(), + ctx, + &state.constants, + &mut state.texture_set, + )?; } // Weapons @@ -554,15 +597,20 @@ impl Menu { } } MenuEntry::Control(name, data) => { - state.font.builder() - .position(self.x as f32 + 20.0, y) - .draw(name, ctx, &state.constants, &mut state.texture_set)?; + state.font.builder().position(self.x as f32 + 20.0, y).draw( + name, + ctx, + &state.constants, + &mut state.texture_set, + )?; match data { ControlMenuData::String(value) => { let text_width = state.font.builder().compute_width(value); - state.font.builder() + state + .font + .builder() .position(self.x as f32 + self.width as f32 - 5.0 - text_width, y) .draw(value, ctx, &state.constants, &mut state.texture_set)?; } diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 4cd5bdf..2d0cbf6 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -138,13 +138,21 @@ impl GameScene { Rc::new(RefCell::new(textures)) }; + let mut player2 = Player::new(state, ctx); + + if state.player2_skin_location.texture_index != 0 { + let skinsheet_name = + state.constants.player_skin_paths[state.player2_skin_location.texture_index as usize].as_str(); + player2.load_skin(skinsheet_name.to_owned(), state, ctx); + } + Ok(Self { tick: 0, stage, water_params, water_renderer, player1: Player::new(state, ctx), - player2: Player::new(state, ctx), + player2: player2, inventory_player1: Inventory::new(), inventory_player2: Inventory::new(), boss_life_bar: BossLifeBar::new(), @@ -181,10 +189,16 @@ impl GameScene { pub fn display_map_name(&mut self, ticks: u16) { self.map_name_counter = ticks; } - pub fn add_player2(&mut self, state: &mut SharedGameState) { + + pub fn add_player2(&mut self, state: &mut SharedGameState, ctx: &mut Context) { self.player2.cond.set_alive(true); self.player2.cond.set_hidden(self.player1.cond.hidden()); - self.player2.skin.set_skinsheet_offset(state.player2_skin); + + let skinsheet_name = + state.constants.player_skin_paths[state.player2_skin_location.texture_index as usize].as_str(); + self.player2.load_skin(skinsheet_name.to_owned(), state, ctx); + self.player2.skin.set_skinsheet_offset(state.player2_skin_location.offset); + self.player2.x = self.player1.x; self.player2.y = self.player1.y; self.player2.vel_x = self.player1.vel_x; @@ -1631,7 +1645,7 @@ impl Scene for GameScene { self.replay.initialize_recording(state); } if state.player_count == PlayerCount::Two { - self.add_player2(state); + self.add_player2(state, ctx); } else { self.drop_player2(); } @@ -1737,7 +1751,7 @@ impl Scene for GameScene { if state.player_count_modified_in_game { if state.player_count == PlayerCount::Two { - self.add_player2(state); + self.add_player2(state, ctx); } else { self.drop_player2(); }