//! Automatic card-to-foundation sequencing once `is_auto_completable` is set. //! //! When `GameState::is_auto_completable` becomes `true`, this plugin fires //! `MoveRequestEvent` for one card per `STEP_INTERVAL` seconds until the game //! is won. A single toast announces the sequence; no player input is required. //! //! The plugin is intentionally passive: it only reads `GameStateResource` and //! fires `MoveRequestEvent`. If for some reason `next_auto_complete_move` //! returns `None` (e.g. a transient state), the plugin retries next tick. use bevy::prelude::*; use bevy::window::RequestRedraw; #[cfg(not(target_arch = "wasm32"))] use crate::audio_plugin::{AudioState, SoundLibrary}; use crate::events::{MoveRequestEvent, StateChangedEvent}; use crate::game_plugin::GameMutation; use crate::pause_plugin::PausedResource; use crate::resources::GameStateResource; /// Volume amplitude used for the auto-complete activation chime. /// /// Plays the win fanfare at half volume so it is clearly distinguishable from /// both normal card-place sounds and the full win fanfare that fires later. #[cfg(not(target_arch = "wasm32"))] const AUTO_COMPLETE_CHIME_VOLUME: f64 = 0.5; /// Seconds between consecutive auto-complete moves. const STEP_INTERVAL: f32 = 0.12; /// Seconds to wait after detection before firing the first auto-complete move. /// /// This pause gives the player a moment to register that the game is /// transitioning into auto-complete mode before cards start moving. const AUTO_COMPLETE_INITIAL_DELAY: f32 = 0.75; /// Tracks whether auto-complete is active and when the next move fires. #[derive(Resource, Default, Debug)] pub struct AutoCompleteState { /// `true` once we've detected `is_auto_completable` and started firing moves. pub active: bool, /// Countdown to the next move, in seconds. cooldown: f32, } /// Plugin that drives the auto-complete sequence. pub struct AutoCompletePlugin; impl Plugin for AutoCompletePlugin { fn build(&self, app: &mut App) { app.init_resource::() .add_message::() .add_systems( Update, ( detect_auto_complete, on_auto_complete_start, drive_auto_complete, ) .chain() .after(GameMutation), ); } } /// Activates auto-complete when `is_auto_completable` flips to `true`. /// Deactivates it on win or new game (any state where it should not be running). fn detect_auto_complete( mut state: ResMut, game: Res, mut changed: MessageReader, ) { // Only re-evaluate on state changes to avoid per-frame allocations. if changed.is_empty() && !game.is_changed() { return; } changed.clear(); if game.0.is_won { state.active = false; return; } if game.0.is_auto_completable && !state.active { state.active = true; state.cooldown = AUTO_COMPLETE_INITIAL_DELAY; } else if !game.0.is_auto_completable && state.active { // `is_auto_completable` only becomes false after an explicit undo // (which puts a card back on the tableau or re-fills the stock/waste) // or a new-game reset — never as a transient gap during a normal // auto-complete sequence. Deactivate here so `drive_auto_complete` // does not keep retrying indefinitely after the player undoes out of // the sequence. // // Note: the transient-`None` case mentioned in older versions of this // comment referred to `next_auto_complete_move()` returning `None`, not // to `is_auto_completable` being false. Those are independent fields; // `drive_auto_complete` still retries on a transient `None` return from // `next_auto_complete_move` because that check happens there, not here. state.active = false; } } /// Plays a distinct chime the moment auto-complete first activates. /// /// Uses a `Local` to remember the previous `active` state and fires /// exactly once on the `false → true` edge. The win fanfare is played at half /// volume (`AUTO_COMPLETE_CHIME_VOLUME`) so it is clearly recognisable but does /// not overwhelm the card-place sounds that follow immediately. #[cfg(not(target_arch = "wasm32"))] fn on_auto_complete_start( state: Res, mut was_active: Local, mut audio: Option>, lib: Option>, ) { let now_active = state.active; let edge = now_active && !*was_active; *was_active = now_active; if !edge { return; } let (Some(audio), Some(lib)) = (audio.as_mut(), lib) else { return; }; audio.play_sfx_at_volume(&lib.fanfare, AUTO_COMPLETE_CHIME_VOLUME); } // No audio on wasm — stub keeps the system registration unconditional. #[cfg(target_arch = "wasm32")] fn on_auto_complete_start(state: Res, mut was_active: Local) { *was_active = state.active; } /// Fires one `MoveRequestEvent` per `STEP_INTERVAL` while auto-complete is active. fn drive_auto_complete( mut state: ResMut, game: Res, time: Res