221 lines
7.7 KiB
Rust
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());
|
|
}
|