//! Routes game-request events to `solitaire_core::GameState` and emits //! state-change notifications. //! //! Game state persistence: on startup the plugin attempts to restore an //! in-progress game from `game_state.json`. On app exit the current state is //! written back (unless the game is won). On a win or new-game request the //! file is deleted so the next launch starts fresh. use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; use bevy::prelude::*; use solitaire_core::game_state::{DrawMode, GameState}; use solitaire_data::{delete_game_state_at, game_state_file_path, load_game_state_from, save_game_state_to}; use crate::events::{ CardFlippedEvent, DrawRequestEvent, FoundationCompletedEvent, GameWonEvent, InfoToastEvent, MoveRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent, }; use crate::font_plugin::FontResource; use crate::resources::{DragState, GameStateResource, SyncStatusResource}; use crate::ui_modal::{ spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button, spawn_modal_header, ButtonVariant, }; use crate::ui_theme; // --------------------------------------------------------------------------- // Task #57 — Confirm-new-game dialog // --------------------------------------------------------------------------- /// Marker on the confirm-new-game modal root node. #[derive(Component, Debug)] pub struct ConfirmNewGameScreen; // --------------------------------------------------------------------------- // Task #58 — Game-over overlay // --------------------------------------------------------------------------- /// Marker on the game-over overlay root node. #[derive(Component, Debug)] pub struct GameOverScreen; /// System set for `GamePlugin`'s state-mutating systems. Downstream plugins /// that read the resulting `StateChangedEvent` should schedule themselves /// `.after(GameMutation)` so updates propagate within a single frame. #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] pub struct GameMutation; /// Persistence path for the in-progress game state file. `None` disables I/O. #[derive(Resource, Debug, Clone)] pub struct GameStatePath(pub Option); /// Registers game resources, events, and the systems that route user intent /// (events) into mutations on `GameState`. pub struct GamePlugin; impl GamePlugin { /// Plugin with no persistence. Use in headless tests to avoid touching the /// real `game_state.json` on disk. pub fn headless() -> Self { Self } } impl Plugin for GamePlugin { fn build(&self, app: &mut App) { let path = game_state_file_path(); // Restore any saved in-progress game, falling back to a fresh deal. let initial_state = path .as_deref() .and_then(load_game_state_from) .unwrap_or_else(|| GameState::new(seed_from_system_time(), DrawMode::DrawOne)); app.insert_resource(GameStateResource(initial_state)) .insert_resource(GameStatePath(path)) .init_resource::() .init_resource::() .add_message::() .add_message::() .add_message::() .add_message::() .add_message::() .add_message::() .add_message::() .add_message::() .add_message::() .add_message::() .add_message::() .add_systems( Update, ( handle_new_game, handle_draw, handle_move, handle_undo, ) .chain() .in_set(GameMutation), ) .add_systems(Update, check_no_moves.after(GameMutation)) .add_systems(Update, handle_confirm_input.after(GameMutation)) .add_systems(Update, handle_confirm_button_input.after(GameMutation)) .add_systems(Update, handle_game_over_input.after(GameMutation)) .add_systems(Update, handle_game_over_button_input.after(GameMutation)) .init_resource::() .add_systems(Update, tick_elapsed_time) .add_systems(Update, auto_save_game_state) .add_systems(Last, save_game_state_on_exit); } } /// Pure, testable helper. Updates `elapsed_seconds` and drains the /// fractional accumulator into whole-second ticks. No-op when `is_won`. pub fn advance_elapsed( elapsed_seconds: &mut u64, accumulator: &mut f32, delta_secs: f32, is_won: bool, ) { if is_won { return; } *accumulator += delta_secs; while *accumulator >= 1.0 { *elapsed_seconds = elapsed_seconds.saturating_add(1); *accumulator -= 1.0; } } /// Increment `GameState.elapsed_seconds` once per real-world second while /// the game is in progress (not won) and not paused. Stops counting on /// win so the final time reflects how long the player took to solve the /// deal; stops while the pause overlay is open. fn tick_elapsed_time( time: Res