From b72058868798118a888d97cfdce5698d386b8b17 Mon Sep 17 00:00:00 2001 From: funman300 Date: Sat, 25 Apr 2026 22:56:35 -0700 Subject: [PATCH] feat(engine): MoveRejectedEvent + PausePlugin (Esc) - New MoveRejectedEvent fires from end_drag when the cursor is over a real pile but the placement is illegal. AudioPlugin plays card_invalid.wav on it. - New PausePlugin + PausedResource: Esc toggles a full-window overlay and the flag. tick_elapsed_time and advance_time_attack skip work while paused. Help cheat sheet updated. Co-Authored-By: Claude Opus 4.7 --- docs/SESSION_HANDOFF.md | 17 ++- solitaire_app/src/main.rs | 5 +- solitaire_engine/src/audio_plugin.rs | 23 +++- solitaire_engine/src/events.rs | 10 ++ solitaire_engine/src/game_plugin.rs | 10 +- solitaire_engine/src/help_plugin.rs | 2 +- solitaire_engine/src/input_plugin.rs | 23 ++-- solitaire_engine/src/lib.rs | 6 +- solitaire_engine/src/pause_plugin.rs | 139 +++++++++++++++++++++ solitaire_engine/src/time_attack_plugin.rs | 4 + 10 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 solitaire_engine/src/pause_plugin.rs diff --git a/docs/SESSION_HANDOFF.md b/docs/SESSION_HANDOFF.md index 7575c6f..5f3986b 100644 --- a/docs/SESSION_HANDOFF.md +++ b/docs/SESSION_HANDOFF.md @@ -2,7 +2,7 @@ > Last updated: 2026-04-25 > Branch: `master` — pushed to https://git.aleshym.co/funman300/Rusty_Solitare.git -> Test count: **226 passing** (83 core + 54 data + 89 engine), `cargo clippy --workspace -- -D warnings` clean +> Test count: **228 passing** (83 core + 54 data + 91 engine), `cargo clippy --workspace -- -D warnings` clean --- @@ -156,19 +156,24 @@ All sub-phases (3A–3F) done. Plugins: `GamePlugin`, `TablePlugin`, `CardPlugin ### Phase 7 (part 2) — Synthesized SFX + AudioPlugin ✅ COMPLETE - New workspace crate `solitaire_assetgen` with bin `gen_sfx`. Synthesizes five 44.1kHz mono 16-bit PCM WAVs from a deterministic LCG noise source + sine/square synths into `assets/audio/`. Run with `cargo run -p solitaire_assetgen --bin gen_sfx`. Output is committed; end users never run the generator. -- `AudioPlugin` (`solitaire_engine`): embeds the WAVs via `include_bytes!()`, decodes once via `kira::StaticSoundData::from_cursor`, plays on `DrawRequestEvent` (flip), `MoveRequestEvent` (place), `NewGameRequestEvent` (deal), `GameWonEvent` (fanfare). `card_invalid.wav` is loaded but unused — wiring it needs a `MoveRejectedEvent` (no such event today). +- `AudioPlugin` (`solitaire_engine`): embeds the WAVs via `include_bytes!()`, decodes once via `kira::StaticSoundData::from_cursor`, plays on `DrawRequestEvent` (flip), `MoveRequestEvent` (place), `NewGameRequestEvent` (deal), `GameWonEvent` (fanfare). - Backend handle stored as `NonSend` (cpal stream is `!Send` on some platforms). Plugin degrades gracefully if no audio device is available — logs a warning, gameplay continues silently. - Single decode unit test (`embedded_wavs_decode_successfully`) keeps the loader and generator in sync. +### Phase 7 (part 3) — MoveRejectedEvent + Pause Menu ✅ COMPLETE + +- New `MoveRejectedEvent { from, to, count }`. `end_drag` fires it when the cursor is over a real pile but `can_place_*` rejects the placement. `AudioPlugin` plays `card_invalid.wav` on it. +- New `PausePlugin` + `PausedResource(bool)`. **Esc** toggles a full-window pause overlay (ZIndex 220) and flips the resource. `tick_elapsed_time` and `advance_time_attack` skip work while paused. Input is deliberately not blocked — pause is a "stop the clock" screen, nothing more. +- `HelpPlugin` cheat sheet updated to reflect the new Esc behaviour. + ## What Is Next -### Phase 7 (part 3+) — Pause Menu + Polish +### Phase 7 (part 4+) — Polish -- **Pause menu**: Esc currently logs a placeholder. Likely a small overlay similar to `HelpPlugin` with a `Paused` resource that gates `Time::delta_secs` in `tick_elapsed_time` / `advance_time_attack`. -- **`MoveRejectedEvent`** (small): emit from `end_drag` when a drop is on a real pile but validation fails, so `card_invalid.wav` finally has something to fire on. -- **Volume controls**: Settings overlay with `sfx_volume` slider; persist via `solitaire_data::Settings` (already defined). Apply to kira's main-track gain. +- **Volume controls**: Settings overlay with `sfx_volume` slider; persist via `solitaire_data::Settings`. Apply to kira's main-track gain. - **Ambient loop**: optional sixth WAV — needs taste, deferred. - **Onboarding**: first-run banner pointing at the **H**/`?` cheat sheet (single-shot via `Settings.first_run_complete`). +- **Optional**: block input while paused (drag, hotkeys) for stricter pause semantics. ### Phase 8 — Sync diff --git a/solitaire_app/src/main.rs b/solitaire_app/src/main.rs index fe7885d..2b2351b 100644 --- a/solitaire_app/src/main.rs +++ b/solitaire_app/src/main.rs @@ -1,8 +1,8 @@ use bevy::prelude::*; use solitaire_engine::{ AchievementPlugin, AnimationPlugin, AudioPlugin, CardPlugin, ChallengePlugin, - DailyChallengePlugin, GamePlugin, HelpPlugin, InputPlugin, ProgressPlugin, StatsPlugin, - TablePlugin, TimeAttackPlugin, WeeklyGoalsPlugin, + DailyChallengePlugin, GamePlugin, HelpPlugin, InputPlugin, PausePlugin, ProgressPlugin, + StatsPlugin, TablePlugin, TimeAttackPlugin, WeeklyGoalsPlugin, }; fn main() { @@ -30,6 +30,7 @@ fn main() { .add_plugins(ChallengePlugin) .add_plugins(TimeAttackPlugin) .add_plugins(HelpPlugin) + .add_plugins(PausePlugin) .add_plugins(AudioPlugin) .run(); } diff --git a/solitaire_engine/src/audio_plugin.rs b/solitaire_engine/src/audio_plugin.rs index 98b384f..48f1a17 100644 --- a/solitaire_engine/src/audio_plugin.rs +++ b/solitaire_engine/src/audio_plugin.rs @@ -7,12 +7,10 @@ //! |---|---| //! | `DrawRequestEvent` | `card_flip.wav` | //! | `MoveRequestEvent` | `card_place.wav` | +//! | `MoveRejectedEvent` | `card_invalid.wav` | //! | `NewGameRequestEvent` | `card_deal.wav` | //! | `GameWonEvent` | `win_fanfare.wav` | //! -//! `card_invalid.wav` is loaded but not yet wired — there is no -//! "rejected move" event today; adding one is a follow-up. -//! //! If the audio device cannot be opened (e.g. a headless CI machine or a //! Linux box without a running PulseAudio/Pipewire session), the plugin //! logs a warning and degrades gracefully — gameplay continues, just @@ -25,7 +23,9 @@ use kira::manager::backend::DefaultBackend; use kira::manager::{AudioManager, AudioManagerSettings}; use kira::sound::static_sound::StaticSoundData; -use crate::events::{DrawRequestEvent, GameWonEvent, MoveRequestEvent, NewGameRequestEvent}; +use crate::events::{ + DrawRequestEvent, GameWonEvent, MoveRejectedEvent, MoveRequestEvent, NewGameRequestEvent, +}; /// Pre-decoded sound effects. Cheap to clone (frames are an `Arc<[Frame]>`), /// so we hand a fresh handle to `manager.play()` on every event. @@ -63,6 +63,7 @@ impl Plugin for AudioPlugin { app.add_event::() .add_event::() + .add_event::() .add_event::() .add_event::() .add_systems( @@ -70,6 +71,7 @@ impl Plugin for AudioPlugin { ( play_on_draw, play_on_move, + play_on_rejected, play_on_new_game, play_on_win, ), @@ -137,6 +139,19 @@ fn play_on_move( } } +fn play_on_rejected( + mut events: EventReader, + mut audio: NonSendMut, + lib: Option>, +) { + let Some(lib) = lib else { + return; + }; + for _ in events.read() { + play(&mut audio, &lib.invalid); + } +} + fn play_on_new_game( mut events: EventReader, mut audio: NonSendMut, diff --git a/solitaire_engine/src/events.rs b/solitaire_engine/src/events.rs index 96b9026..cf50b0e 100644 --- a/solitaire_engine/src/events.rs +++ b/solitaire_engine/src/events.rs @@ -34,6 +34,16 @@ pub struct NewGameRequestEvent { #[derive(Event, Debug, Clone, Copy, Default)] pub struct StateChangedEvent; +/// Fired by input/UI systems when a player attempts to drop dragged cards +/// on a real pile but the move violates the rules. Drives the +/// `card_invalid.wav` SFX. Not fired for drops in empty space. +#[derive(Event, Debug, Clone)] +pub struct MoveRejectedEvent { + pub from: PileType, + pub to: PileType, + pub count: usize, +} + /// Fired once when the active game transitions to won. #[derive(Event, Debug, Clone, Copy)] pub struct GameWonEvent { diff --git a/solitaire_engine/src/game_plugin.rs b/solitaire_engine/src/game_plugin.rs index 1b22d11..46da46b 100644 --- a/solitaire_engine/src/game_plugin.rs +++ b/solitaire_engine/src/game_plugin.rs @@ -35,6 +35,7 @@ impl Plugin for GamePlugin { .add_event::() .add_event::() .add_event::() + .add_event::() .add_event::() .add_event::() .add_event::() @@ -72,13 +73,18 @@ pub fn advance_elapsed( } /// Increment `GameState.elapsed_seconds` once per real-world second while -/// the game is in progress (not won). Stops counting on win so the final -/// time reflects how long the player took to solve the deal. +/// 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