//! 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 crate::audio_plugin::{AudioState, SoundLibrary}; use crate::events::{MoveRequestEvent, StateChangedEvent}; use crate::game_plugin::GameMutation; 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. const AUTO_COMPLETE_CHIME_VOLUME: f64 = 0.5; /// Seconds between consecutive auto-complete moves. const STEP_INTERVAL: f32 = 0.12; /// 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_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: EventReader, ) { // 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 = 0.0; // fire first move immediately } else if !game.0.is_auto_completable { 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. 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); } /// Fires one `MoveRequestEvent` per `STEP_INTERVAL` while auto-complete is active. fn drive_auto_complete( mut state: ResMut, game: Res, time: Res