feat(engine): playability improvements — rounds 7–9 (#40–#64)
Round 7 — Input & feedback
- H key cycles hints; F1 opens help (conflict resolved)
- N key cancels active Time Attack session
- Hint text distinguishes "draw from stock" vs "recycle waste"
- Forfeit (G) clears AutoCompleteState so chime does not bleed into new deal
- Zen mode timer clears immediately on Z press
- HUD shows recycle count in both draw modes
- Settings scroll position persisted across open/close
Round 8 — Polish & clarity
- Undo unavailable fires "Nothing to undo" toast
- Streak-break toast on forfeit/abandon when streak > 1
- F11 fullscreen toggle with toast; documented in help and home screens
- H-after-win, new-game countdown expiry, Tab-no-cards toasts
- Win cascade duration/stagger scales with animation speed setting
- Draw-Three cycle counter HUD ("Cycle: N/3")
- Forfeit requires G×2 confirmation within 3 s (mirrors N key)
Round 9 — Game feel & information
- Escape dismisses game-over/stuck overlay (PausePlugin skips Escape when overlay visible)
- Shake animation on rejected drag before snap-back
- Forfeit countdown cancels when any other key is pressed (U/H/D/Z/Space)
- Tab wrap-around fires "Back to first card" toast
- HUD selection indicator shows active Tab-selected pile in yellow
- Challenge time-limit HUD turns orange < 60s, red < 30s
- Win summary shows XP breakdown (+50 base, +25 no-undo, +N speed)
- Game-over overlay: "No more moves available" with clear N/Escape/G instructions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,17 @@
|
||||
|
||||
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;
|
||||
|
||||
@@ -34,7 +41,11 @@ impl Plugin for AutoCompletePlugin {
|
||||
app.init_resource::<AutoCompleteState>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(detect_auto_complete, drive_auto_complete)
|
||||
(
|
||||
detect_auto_complete,
|
||||
on_auto_complete_start,
|
||||
drive_auto_complete,
|
||||
)
|
||||
.chain()
|
||||
.after(GameMutation),
|
||||
);
|
||||
@@ -66,6 +77,30 @@ fn detect_auto_complete(
|
||||
}
|
||||
}
|
||||
|
||||
/// Plays a distinct chime the moment auto-complete first activates.
|
||||
///
|
||||
/// Uses a `Local<bool>` 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<AutoCompleteState>,
|
||||
mut was_active: Local<bool>,
|
||||
mut audio: Option<NonSendMut<AudioState>>,
|
||||
lib: Option<Res<SoundLibrary>>,
|
||||
) {
|
||||
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<AutoCompleteState>,
|
||||
|
||||
Reference in New Issue
Block a user