diff --git a/src/menu/mod.rs b/src/menu/mod.rs index f82bdfd..2aafaca 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -1,17 +1,18 @@ use std::cell::Cell; use crate::common::{Color, Rect}; +use crate::components::draw_common::{draw_number, Alignment}; use crate::framework::context::Context; use crate::framework::error::GameResult; use crate::framework::graphics; use crate::input::combined_menu_controller::CombinedMenuController; +use crate::menu::save_select_menu::MenuSaveInfo; use crate::shared_game_state::{MenuCharacter, SharedGameState}; pub mod pause_menu; +pub mod save_select_menu; pub mod settings_menu; -pub struct MenuSaveInfo {} - pub enum MenuEntry { Hidden, Active(String), @@ -402,6 +403,55 @@ impl Menu { graphics::draw_rect(ctx, bar_rect, Color::new(1.0, 1.0, 1.0, 1.0))?; } } + MenuEntry::NewSave => { + state.font.draw_text( + "New Save".chars(), + self.x as f32 + 20.0, + y, + &state.constants, + &mut state.texture_set, + ctx, + )?; + } + MenuEntry::SaveData(save) => { + let name = &state.stages[save.current_map as usize].name; + 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; + + // Lifebar + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?; + + batch.add_rect(right_edge - 60.0, y, &Rect::new_size(0, 40, 24, 8)); + batch.add_rect(right_edge - 36.0, y, &Rect::new_size(24, 40, 40, 8)); + batch.add_rect(right_edge - 36.0, y, &Rect::new_size(0, 24, bar_width, 8)); + + state.font.draw_text( + name.chars(), + self.x as f32 + 20.0, + y, + &state.constants, + &mut state.texture_set, + ctx, + )?; + + // Weapons + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "ArmsImage")?; + + for weapon_slot in 0..save.weapon_count { + let wtype = save.weapon_id[weapon_slot]; + let pos_x = weapon_slot as f32 * 16.0 - (16 * save.weapon_count.saturating_sub(4)) as f32; + let mut rect = Rect::new(0, 0, 0, 16); + if wtype != 0 { + rect.left = wtype as u16 * 16; + rect.right = rect.left + 16; + batch.add_rect(right_edge + pos_x - 60.0, y + 8.0, &rect); + } + } + + batch.draw(ctx)?; + + draw_number(right_edge - 36.0, y, save.life as usize, Alignment::Right, state, ctx)?; + } _ => {} } @@ -453,6 +503,8 @@ impl Menu { let mut y = self.y as f32 + 8.0; for (idx, entry) in self.entries.iter_mut().enumerate() { let entry_bounds = Rect::new_size(self.x, y as isize, self.width as isize, entry.height() as isize); + let right_entry_bounds = + Rect::new_size(self.x + self.width as isize, y as isize, self.width as isize, entry.height() as isize); y += entry.height() as f32; match entry { @@ -460,6 +512,8 @@ impl Menu { | MenuEntry::Toggle(_, _) | MenuEntry::Options(_, _, _) | MenuEntry::DescriptiveOptions(_, _, _, _) + | MenuEntry::SaveData(_) + | MenuEntry::NewSave if (self.selected == idx && controller.trigger_ok()) || state.touch_controls.consume_click_in(entry_bounds) => { @@ -478,11 +532,16 @@ impl Menu { state.sound_manager.play_sfx(1); return MenuSelectionResult::Right(self.selected, entry, 1); } - MenuEntry::DescriptiveOptions(_, _, _, _) if self.selected == idx && controller.trigger_left() => { + MenuEntry::DescriptiveOptions(_, _, _, _) + if (self.selected == idx && controller.trigger_left()) + || state.touch_controls.consume_click_in(right_entry_bounds) => + { state.sound_manager.play_sfx(1); return MenuSelectionResult::Left(self.selected, entry, -1); } - MenuEntry::DescriptiveOptions(_, _, _, _) if self.selected == idx && controller.trigger_right() => { + MenuEntry::DescriptiveOptions(_, _, _, _) | MenuEntry::SaveData(_) + if self.selected == idx && controller.trigger_right() => + { state.sound_manager.play_sfx(1); return MenuSelectionResult::Right(self.selected, entry, 1); } diff --git a/src/menu/save_select_menu.rs b/src/menu/save_select_menu.rs new file mode 100644 index 0000000..a1e4fd4 --- /dev/null +++ b/src/menu/save_select_menu.rs @@ -0,0 +1,144 @@ +use crate::framework::context::Context; +use crate::framework::error::GameResult; +use crate::framework::filesystem; +use crate::input::combined_menu_controller::CombinedMenuController; +use crate::menu::MenuEntry; +use crate::menu::{Menu, MenuSelectionResult}; +use crate::profile::GameProfile; +use crate::shared_game_state::SharedGameState; + +#[derive(Clone, Copy)] +pub struct MenuSaveInfo { + pub current_map: u32, + pub max_life: u16, + pub life: u16, + pub weapon_count: usize, + pub weapon_id: [u32; 8], +} + +impl Default for MenuSaveInfo { + fn default() -> Self { + MenuSaveInfo { current_map: 0, max_life: 0, life: 0, weapon_count: 0, weapon_id: [0; 8] } + } +} +pub enum CurrentMenu { + SaveMenu, + DeleteConfirm, +} +pub struct SaveSelectMenu { + pub saves: [MenuSaveInfo; 3], + current_menu: CurrentMenu, + save_menu: Menu, + delete_confirm: Menu, +} + +impl SaveSelectMenu { + pub fn new() -> SaveSelectMenu { + SaveSelectMenu { + saves: [MenuSaveInfo::default(); 3], + current_menu: CurrentMenu::SaveMenu, + save_menu: Menu::new(0, 0, 200, 0), + delete_confirm: Menu::new(0, 0, 75, 0), + } + } + + pub fn init(&mut self, state: &mut SharedGameState, ctx: &Context) -> GameResult { + for (iter, save) in self.saves.iter_mut().enumerate() { + if let Ok(data) = filesystem::user_open(ctx, state.get_save_filename(iter + 1)) { + let loaded_save = GameProfile::load_from_save(data)?; + + save.current_map = loaded_save.current_map; + save.max_life = loaded_save.max_life; + save.life = loaded_save.life; + save.weapon_count = loaded_save.weapon_data.iter().filter(|weapon| weapon.weapon_id != 0).count(); + save.weapon_id = loaded_save.weapon_data.map(|weapon| weapon.weapon_id); + + self.save_menu.push_entry(MenuEntry::SaveData(*save)); + } else { + self.save_menu.push_entry(MenuEntry::NewSave); + } + } + + self.save_menu.push_entry(MenuEntry::Active("< Back".to_owned())); + self.save_menu.push_entry(MenuEntry::Disabled("Press Right to Delete".to_owned())); + + self.delete_confirm.push_entry(MenuEntry::Disabled("Delete?".to_owned())); + self.delete_confirm.push_entry(MenuEntry::Active("Yes".to_owned())); + self.delete_confirm.push_entry(MenuEntry::Active("No".to_owned())); + + self.delete_confirm.selected = 2; + + self.update_sizes(state); + + Ok(()) + } + + fn update_sizes(&mut self, state: &SharedGameState) { + self.save_menu.update_height(); + self.save_menu.x = ((state.canvas_size.0 - self.save_menu.width as f32) / 2.0).floor() as isize; + self.save_menu.y = 30 + ((state.canvas_size.1 - self.save_menu.height as f32) / 2.0).floor() as isize; + self.delete_confirm.update_height(); + self.delete_confirm.x = ((state.canvas_size.0 - self.delete_confirm.width as f32) / 2.0).floor() as isize; + self.delete_confirm.y = 30 + ((state.canvas_size.1 - self.delete_confirm.height as f32) / 2.0).floor() as isize + } + + pub fn tick( + &mut self, + exit_action: &mut dyn FnMut(), + controller: &mut CombinedMenuController, + state: &mut SharedGameState, + ctx: &mut Context, + ) -> GameResult { + self.update_sizes(state); + match self.current_menu { + CurrentMenu::SaveMenu => match self.save_menu.tick(controller, state) { + MenuSelectionResult::Selected(0, _) => { + state.save_slot = 1; + state.load_or_start_game(ctx)?; + } + MenuSelectionResult::Selected(1, _) => { + state.save_slot = 2; + state.load_or_start_game(ctx)?; + } + MenuSelectionResult::Selected(2, _) => { + state.save_slot = 3; + state.load_or_start_game(ctx)?; + } + MenuSelectionResult::Selected(3, _) | MenuSelectionResult::Canceled => exit_action(), + MenuSelectionResult::Right(slot, _, _) => { + if slot <= 2 { + self.current_menu = CurrentMenu::DeleteConfirm; + self.delete_confirm.selected = 2; + } + } + _ => (), + }, + CurrentMenu::DeleteConfirm => match self.delete_confirm.tick(controller, state) { + MenuSelectionResult::Selected(1, _) => { + state.sound_manager.play_sfx(17); // Player Death sfx + filesystem::user_delete(ctx, state.get_save_filename(self.save_menu.selected + 1))?; + self.save_menu.entries[self.save_menu.selected] = MenuEntry::NewSave; + self.current_menu = CurrentMenu::SaveMenu; + } + MenuSelectionResult::Selected(2, _) | MenuSelectionResult::Canceled => { + self.current_menu = CurrentMenu::SaveMenu; + } + _ => (), + }, + } + + Ok(()) + } + + pub fn draw(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + match self.current_menu { + CurrentMenu::SaveMenu => { + self.save_menu.draw(state, ctx)?; + } + CurrentMenu::DeleteConfirm => { + self.delete_confirm.draw(state, ctx)?; + } + } + Ok(()) + } +} diff --git a/src/scene/title_scene.rs b/src/scene/title_scene.rs index d585653..61072ce 100644 --- a/src/scene/title_scene.rs +++ b/src/scene/title_scene.rs @@ -9,6 +9,7 @@ use crate::framework::graphics; use crate::input::combined_menu_controller::CombinedMenuController; use crate::input::touch_controls::TouchControlType; use crate::map::Map; +use crate::menu::save_select_menu::SaveSelectMenu; use crate::menu::settings_menu::SettingsMenu; use crate::menu::{Menu, MenuEntry, MenuSelectionResult}; use crate::scene::Scene; @@ -33,7 +34,7 @@ pub struct TitleScene { current_menu: CurrentMenu, main_menu: Menu, option_menu: SettingsMenu, - save_select_menu: Menu, + save_select_menu: SaveSelectMenu, background: Background, frame: Frame, nikumaru_rec: NikumaruCounter, @@ -67,7 +68,7 @@ impl TitleScene { current_menu: CurrentMenu::MainMenu, main_menu: Menu::new(0, 0, 100, 0), option_menu: SettingsMenu::new(), - save_select_menu: Menu::new(0, 0, 200, 0), + save_select_menu: SaveSelectMenu::new(), background: Background::new(), frame: Frame::new(), nikumaru_rec: NikumaruCounter::new(), @@ -126,8 +127,7 @@ impl Scene for TitleScene { self.controller.add(state.settings.create_player1_controller()); self.controller.add(state.settings.create_player2_controller()); - self.main_menu.push_entry(MenuEntry::Active("New game".to_string())); - self.main_menu.push_entry(MenuEntry::Active("Load game".to_string())); + self.main_menu.push_entry(MenuEntry::Active("Start Game".to_string())); self.main_menu.push_entry(MenuEntry::Active("Options".to_string())); if cfg!(feature = "editor") { self.main_menu.push_entry(MenuEntry::Active("Editor".to_string())); @@ -138,11 +138,7 @@ impl Scene for TitleScene { self.option_menu.init(state, ctx)?; - self.save_select_menu.push_entry(MenuEntry::NewSave); - self.save_select_menu.push_entry(MenuEntry::NewSave); - self.save_select_menu.push_entry(MenuEntry::NewSave); - self.save_select_menu.push_entry(MenuEntry::Active("Delete a save".to_string())); - self.save_select_menu.push_entry(MenuEntry::Active("Back".to_string())); + self.save_select_menu.init(state, ctx)?; self.controller.update(state, ctx)?; self.controller.update_trigger(); @@ -166,20 +162,12 @@ impl Scene for TitleScene { match self.current_menu { CurrentMenu::MainMenu => match self.main_menu.tick(&mut self.controller, state) { MenuSelectionResult::Selected(0, _) => { - state.reset(); - state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?; - self.tick = 1; - self.current_menu = CurrentMenu::StartGame; + self.current_menu = CurrentMenu::SaveSelectMenu; } MenuSelectionResult::Selected(1, _) => { - state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?; - self.tick = 1; - self.current_menu = CurrentMenu::LoadGame; - } - MenuSelectionResult::Selected(2, _) => { self.current_menu = CurrentMenu::OptionMenu; } - MenuSelectionResult::Selected(3, _) => { + MenuSelectionResult::Selected(2, _) => { // this comment is just there because rustfmt removes parenthesis around the match case and breaks compilation #[cfg(feature = "editor")] { @@ -187,7 +175,7 @@ impl Scene for TitleScene { state.next_scene = Some(Box::new(EditorScene::new())); } } - MenuSelectionResult::Selected(4, _) => { + MenuSelectionResult::Selected(3, _) => { state.shutdown(); } _ => {} @@ -207,6 +195,17 @@ impl Scene for TitleScene { self.update_menu_cursor(state, ctx)?; } } + CurrentMenu::SaveSelectMenu => { + let cm = &mut self.current_menu; + self.save_select_menu.tick( + &mut || { + *cm = CurrentMenu::MainMenu; + }, + &mut self.controller, + state, + ctx, + )?; + } CurrentMenu::StartGame => { if self.tick == 10 { state.reset_skip_flags(); @@ -253,6 +252,7 @@ impl Scene for TitleScene { match self.current_menu { CurrentMenu::MainMenu => self.main_menu.draw(state, ctx)?, CurrentMenu::OptionMenu => self.option_menu.draw(state, ctx)?, + CurrentMenu::SaveSelectMenu => self.save_select_menu.draw(state, ctx)?, _ => {} } diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 37d1dff..0f2edd6 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -184,6 +184,7 @@ pub struct SharedGameState { pub lua: LuaScriptingState, pub sound_manager: SoundManager, pub settings: Settings, + pub save_slot: usize, pub shutdown: bool, } @@ -293,6 +294,7 @@ impl SharedGameState { lua: LuaScriptingState::new(), sound_manager, settings, + save_slot: 1, shutdown: false, }) } @@ -384,7 +386,11 @@ impl SharedGameState { } pub fn save_game(&mut self, game_scene: &mut GameScene, ctx: &mut Context) -> GameResult { - if let Ok(data) = filesystem::open_options(ctx, "/Profile.dat", OpenOptions::new().write(true).create(true)) { + if let Ok(data) = filesystem::open_options( + ctx, + self.get_save_filename(self.save_slot), + OpenOptions::new().write(true).create(true), + ) { let profile = GameProfile::dump(self, game_scene); profile.write_save(data)?; } else { @@ -395,7 +401,7 @@ impl SharedGameState { } pub fn load_or_start_game(&mut self, ctx: &mut Context) -> GameResult { - if let Ok(data) = filesystem::user_open(ctx, "/Profile.dat") { + if let Ok(data) = filesystem::user_open(ctx, self.get_save_filename(self.save_slot)) { match GameProfile::load_from_save(data) { Ok(profile) => { self.reset(); @@ -529,4 +535,12 @@ impl SharedGameState { false } } + + pub fn get_save_filename(&self, slot: usize) -> String { + if slot == 1 { + "/Profile.dat".to_owned() + } else { + format!("/Profile{}.dat", slot) + } + } }