use crate::features::achievement::components::AchievementProgress; use crate::features::phase::components::{SessionTracker, TimerSettings}; use crate::features::savegame::ui::load_popup_handler; use crate::prelude::*; use components::*; use messages::*; use std::fs::File; use std::io::{Read, Write}; pub mod components; pub mod messages; pub mod ui; /// Plugin dealing with savegame loading and saving. pub struct SavegamePlugin; impl Plugin for SavegamePlugin { fn build(&self, app: &mut App) { app.add_message::(); app.add_message::(); app.add_systems(Update, dump_savegame.run_if(in_state(AppState::GameScreen))); app.add_systems(Update, load_savegame.run_if(in_state(AppState::GameScreen))); app.add_systems(OnExit(AppState::GameScreen), reset_savegame); app.add_systems(Update, load_popup_handler); } } /// The structure of a save file. #[derive(Serialize, Deserialize)] struct SaveData { grid_width: u32, grid_height: u32, tiles: Vec>, current_phase: CurrentPhase, session_tracker: SessionTracker, achievement_progress: AchievementProgress, timer_settings: TimerSettings, pom_position: GridPosition, inventory: Vec, } /// Serializes game state and writes it to a file. fn dump_savegame( mut messages: MessageReader, save_path: Res, grid: Res, tile_query: Query<&TileState>, phase: Res, tracker: Res, achievement_progress: Res, settings: Res, pom_query: Query<&GridPosition, With>, inventory: Res, item_query: Query<&ItemStack>, ) { for _ in messages.read() { let mut tile_states = Vec::new(); for x in 0..grid.width { let mut col = Vec::new(); for y in 0..grid.height { if let Ok(entity) = grid.get_tile((x, y)) { if let Ok(state) = tile_query.get(entity) { col.push(state.clone()); } else { col.push(TileState::Unclaimed); } } else { col.push(TileState::Unclaimed); } } tile_states.push(col); } let pom_pos = pom_query.single().unwrap(); let item_stacks: Vec = inventory .items .iter() .filter_map(|entity| item_query.get(*entity).ok().cloned()) .collect(); let save_data = SaveData { grid_width: grid.width, grid_height: grid.height, tiles: tile_states, current_phase: phase.clone(), session_tracker: tracker.clone(), achievement_progress: achievement_progress.clone(), timer_settings: settings.clone(), pom_position: *pom_pos, inventory: item_stacks, }; match serde_json::to_string_pretty(&save_data) { Ok(serialized) => { if let Ok(mut file) = File::create(&save_path.0) { if let Err(e) = file.write_all(serialized.as_bytes()) { panic!("Failed to write save file: {}", e); } else { println!("Game saved to {}", save_path.0.display()); } } else { panic!("Failed to create save file at {}", save_path.0.display()); } } Err(e) => { panic!("Failed to serialize save data: {}", e); } } } } /// Reads a save file and restores game state. fn load_savegame( mut commands: Commands, mut messages: MessageReader, save_path: Res, grid: Res, mut tile_query: Query<&mut TileState>, mut phase: ResMut, mut tracker: ResMut, mut achievement_progress: ResMut, mut settings: ResMut, mut pom_query: Query<(&mut GridPosition, &mut Transform), With>, mut inventory: ResMut, ) { for _ in messages.read() { if let Ok(mut file) = File::open(&save_path.0) { let mut content = String::new(); if let Err(e) = file.read_to_string(&mut content) { eprintln!("Failed to read save file: {}", e); continue; } match serde_json::from_str::(&content) { Ok(save_data) => { *phase = save_data.current_phase; *tracker = save_data.session_tracker; *achievement_progress = save_data.achievement_progress; *settings = save_data.timer_settings; if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() { *pom_pos = save_data.pom_position; pom_transform.translation = grid_to_world_coords( save_data.pom_position.x, save_data.pom_position.y, Some(1.0), save_data.grid_width, save_data.grid_height, ) } for x in 0..save_data.grid_width { for y in 0..save_data.grid_height { if x < grid.width && y < grid.height { if let Ok(entity) = grid.get_tile((x, y)) { if let Ok(mut state) = tile_query.get_mut(entity) { *state = save_data.tiles[x as usize][y as usize].clone(); } } } } } let stack_entities: Vec = save_data .inventory .iter() .map(|stack| commands.spawn(stack.clone()).id()) .collect(); inventory.items = stack_entities; println!("Game loaded from {}", save_path.0.display()); } Err(e) => { eprintln!("Failed to parse save data: {}", e); } } } else { eprintln!("Failed to open save file at {}", save_path.0.display()); } } } /// Resets all components/resources loaded by `load_savegame`. fn reset_savegame( mut commands: Commands, grid: Res, mut tile_query: Query<&mut TileState>, mut phase: ResMut, mut tracker: ResMut, mut achievement_progress: ResMut, mut settings: ResMut, mut pom_query: Query<(&mut GridPosition, &mut Transform), With>, mut inventory: ResMut, ) { *tracker = SessionTracker::default(); *achievement_progress = AchievementProgress::default(); *settings = TimerSettings::default(); *phase = CurrentPhase(Phase::Focus { duration: settings.focus_duration as f32, }); inventory .items .iter() .for_each(|entity| commands.entity(*entity).despawn()); inventory.items.clear(); if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() { *pom_pos = GridPosition::default(); pom_transform.translation = grid_to_world_coords(0, 0, Some(1.0), grid.width, grid.height); } tile_query .iter_mut() .for_each(|mut state| *state = TileState::default()); }