Files
pomomon-garden/src/features/savegame/mod.rs

221 lines
7.7 KiB
Rust

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::<SavegameDumpMessage>();
app.add_message::<SavegameLoadMessage>();
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<Vec<TileState>>,
current_phase: CurrentPhase,
session_tracker: SessionTracker,
achievement_progress: AchievementProgress,
timer_settings: TimerSettings,
pom_position: GridPosition,
inventory: Vec<ItemStack>,
}
/// Serializes game state and writes it to a file.
fn dump_savegame(
mut messages: MessageReader<SavegameDumpMessage>,
save_path: Res<SavegamePath>,
grid: Res<Grid>,
tile_query: Query<&TileState>,
phase: Res<CurrentPhase>,
tracker: Res<SessionTracker>,
achievement_progress: Res<AchievementProgress>,
settings: Res<TimerSettings>,
pom_query: Query<&GridPosition, With<Pom>>,
inventory: Res<Inventory>,
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<ItemStack> = 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<SavegameLoadMessage>,
save_path: Res<SavegamePath>,
grid: Res<Grid>,
mut tile_query: Query<&mut TileState>,
mut phase: ResMut<CurrentPhase>,
mut tracker: ResMut<SessionTracker>,
mut achievement_progress: ResMut<AchievementProgress>,
mut settings: ResMut<TimerSettings>,
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
mut inventory: ResMut<Inventory>,
) {
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::<SaveData>(&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<Entity> = 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<Grid>,
mut tile_query: Query<&mut TileState>,
mut phase: ResMut<CurrentPhase>,
mut tracker: ResMut<SessionTracker>,
mut achievement_progress: ResMut<AchievementProgress>,
mut settings: ResMut<TimerSettings>,
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
mut inventory: ResMut<Inventory>,
) {
*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());
}