ddc8f27c82
Three small UX improvements bundled because they share ui_theme token
edits.
Tooltip-delay slider in Settings → Gameplay
- Settings.tooltip_delay_secs (f32, #[serde(default)] = 0.5) tunable
via "−" / "+" icon buttons next to a value readout. Range
[TOOLTIP_DELAY_MIN_SECS, TOOLTIP_DELAY_MAX_SECS] = [0.0, 1.5] in
TOOLTIP_DELAY_STEP_SECS (0.1) increments. "Instant" label when
value is 0; "{n:.1} s" otherwise.
- ui_tooltip's hover-delay comparison reads from SettingsResource
with MOTION_TOOLTIP_DELAY_SECS as the fallback when the resource
is absent (test path). New tooltip_should_show(elapsed, delay)
pure helper covers the boundary cases.
- adjust_tooltip_delay clamps; sanitized() carries the clamp through
load. Five round-trip / default / legacy-deserialise tests.
Win-streak milestone fire animation
- New WinStreakMilestoneEvent { streak: u32 } fired from stats_plugin
when win_streak_current crosses any of [3, 5, 10] (only the
threshold crossing — not every subsequent win). HUD streak readout
scale-pulses 1.0 → 1.20 → 1.0 over MOTION_STREAK_FLOURISH_SECS
(0.6 s) on receipt; mirrors the foundation-flourish curve shape.
- Three threshold-crossing tests pin the firing contract.
Score-breakdown reveal on the win modal
- Win modal body replaces the single "Score: N" line with a
per-component reveal: Base score, Time bonus (m:ss), No-undo
bonus, Mode multiplier, separator, Total. Rows fade in over
MOTION_SCORE_BREAKDOWN_FADE_SECS (0.12 s) staggered by
MOTION_SCORE_BREAKDOWN_STAGGER_SECS (0.15 s) so the math reads as
it animates. Skipped rows: zero time bonus, undo-tainted no-undo
bonus, multiplier == 1.0.
- Honours AnimSpeed::Instant: rows spawn fully visible, no stagger.
- New ScoreBreakdown::compute helper sources base from
GameWonEvent.score, time bonus from
solitaire_core::scoring::compute_time_bonus, no-undo from a +25
constant when undo_count == 0, mode multiplier from GameMode (Zen
zeros the total). 9 new tests cover the math and the reveal
cadence.
Test count net: +25 across the workspace (1007 → 1031).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
256 lines
11 KiB
Rust
256 lines
11 KiB
Rust
//! Cross-system events used by the engine's plugins.
|
|
|
|
use bevy::prelude::Message;
|
|
use solitaire_core::card::Suit;
|
|
use solitaire_core::game_state::GameMode;
|
|
use solitaire_core::pile::PileType;
|
|
use solitaire_data::AchievementRecord;
|
|
use solitaire_sync::SyncResponse;
|
|
|
|
/// Request to move `count` cards from `from` to `to`. Fired by input systems,
|
|
/// consumed by `GamePlugin`.
|
|
#[derive(Message, Debug, Clone)]
|
|
pub struct MoveRequestEvent {
|
|
pub from: PileType,
|
|
pub to: PileType,
|
|
pub count: usize,
|
|
}
|
|
|
|
/// Request to draw from the stock (or recycle waste when stock is empty).
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct DrawRequestEvent;
|
|
|
|
/// Request to undo the most recent state change.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct UndoRequestEvent;
|
|
|
|
/// Request to start a new game. `seed = None` uses a system-time seed.
|
|
/// `mode = None` reuses the current game's `GameMode`.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct NewGameRequestEvent {
|
|
pub seed: Option<u64>,
|
|
pub mode: Option<GameMode>,
|
|
/// `true` when this request originated from the user confirming the
|
|
/// abandon-current-game modal (Y / Enter on `ConfirmNewGameScreen`).
|
|
/// `handle_new_game` skips spawning the dialog when this is set,
|
|
/// otherwise it would respawn the modal in the frame after the player
|
|
/// presses Y (the despawn-on-Y has flushed by then) and the new game
|
|
/// would never actually start.
|
|
pub confirmed: bool,
|
|
}
|
|
|
|
/// Fired by `GamePlugin` after any successful state mutation. Rendering and
|
|
/// score-display systems listen for this to refresh.
|
|
#[derive(Message, 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(Message, Debug, Clone)]
|
|
pub struct MoveRejectedEvent {
|
|
pub from: PileType,
|
|
pub to: PileType,
|
|
pub count: usize,
|
|
}
|
|
|
|
/// Fired once when the active game transitions to won.
|
|
#[derive(Message, Debug, Clone, Copy)]
|
|
pub struct GameWonEvent {
|
|
pub score: i32,
|
|
pub time_seconds: u64,
|
|
}
|
|
|
|
/// Fired by `GamePlugin` whenever a successful move lands a card on a
|
|
/// foundation pile that, after the move, contains all 13 cards of its
|
|
/// suit (Ace → King). Drives the per-suit completion flourish — a brief
|
|
/// scale pulse on the King card and a golden tint on the foundation
|
|
/// pile marker — plus a short audio ping.
|
|
///
|
|
/// Fired once per per-suit completion. The fourth completion will
|
|
/// co-occur with `GameWonEvent` and the win cascade — they layer
|
|
/// cleanly because the flourish is purely decorative and lives on a
|
|
/// dedicated marker component.
|
|
///
|
|
/// This event is a UI/audio cue only. It does **not** cross
|
|
/// `solitaire_sync` and is not persisted.
|
|
#[derive(Message, Debug, Clone, Copy)]
|
|
pub struct FoundationCompletedEvent {
|
|
/// Foundation pile slot (0..=3) that just reached 13 cards.
|
|
pub slot: u8,
|
|
/// The suit of the completed foundation, taken from the bottom card
|
|
/// (always an Ace by construction).
|
|
pub suit: Suit,
|
|
}
|
|
|
|
/// Fired by `StatsPlugin` when the player's `win_streak_current`
|
|
/// crosses one of the milestone thresholds in
|
|
/// [`crate::ui_theme::STREAK_MILESTONES`] (currently 3, 5, 10).
|
|
///
|
|
/// Fires only on the threshold crossing — i.e. when the previous
|
|
/// streak was below the threshold and the post-win streak is at or
|
|
/// above it — so subsequent wins past the highest milestone do not
|
|
/// retrigger the flourish.
|
|
///
|
|
/// Drives the HUD streak-milestone flourish (a brief scale pulse on
|
|
/// the score readout) and an informational toast. UI/audio cue only;
|
|
/// not persisted, not synchronised.
|
|
#[derive(Message, Debug, Clone, Copy)]
|
|
pub struct WinStreakMilestoneEvent {
|
|
/// The new `win_streak_current` value at the moment the
|
|
/// threshold was crossed. Always equal to a value in
|
|
/// [`crate::ui_theme::STREAK_MILESTONES`].
|
|
pub streak: u32,
|
|
}
|
|
|
|
/// Fired when a card's face-up state changes during gameplay.
|
|
#[derive(Message, Debug, Clone, Copy)]
|
|
pub struct CardFlippedEvent(pub u32);
|
|
|
|
/// Fired by the flip animation at its midpoint — the instant the card face
|
|
/// becomes visible (scale.x crosses zero and the phase switches to ScalingUp).
|
|
///
|
|
/// Audio systems should listen to this event rather than `CardFlippedEvent`
|
|
/// so the flip sound is synchronised with the visual reveal, not the move
|
|
/// that triggered the animation.
|
|
#[derive(Message, Debug, Clone, Copy)]
|
|
pub struct CardFaceRevealedEvent(pub u32);
|
|
|
|
/// Achievement unlocked notification carrying the full `AchievementRecord` for
|
|
/// the newly unlocked achievement. Consumed by the toast renderer and any
|
|
/// persistence/UI systems that need unlock metadata.
|
|
#[derive(Message, Debug, Clone)]
|
|
pub struct AchievementUnlockedEvent(pub AchievementRecord);
|
|
|
|
/// Request to manually trigger a sync pull from the active backend.
|
|
///
|
|
/// Fired by the Settings panel "Sync Now" button. `SyncPlugin` responds by
|
|
/// starting a new pull task if one is not already in flight.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct ManualSyncRequestEvent;
|
|
|
|
/// Request to toggle the pause overlay. Fired by the HUD "Pause" button so
|
|
/// the same toggle path runs whether the player presses `Esc` or clicks.
|
|
/// Consumed by `pause_plugin::toggle_pause`, which honours the same drag /
|
|
/// game-over / selection guards either way.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct PauseRequestEvent;
|
|
|
|
/// Request to toggle the help / controls overlay. Fired by the HUD "Help"
|
|
/// button alongside the existing `F1` accelerator so the overlay is
|
|
/// reachable without a keyboard. Consumed by `help_plugin::toggle_help_screen`.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct HelpRequestEvent;
|
|
|
|
/// Request to start a Zen-mode game. Fired by the HUD Modes-popover "Zen"
|
|
/// row alongside the existing `Z` accelerator. The handler in
|
|
/// `input_plugin` enforces the level gate (Zen unlocks at level 5) and
|
|
/// shows an informational toast when locked.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct StartZenRequestEvent;
|
|
|
|
/// Request to start the next Challenge-mode game. Fired by the HUD
|
|
/// Modes-popover "Challenge" row alongside the existing `X` accelerator.
|
|
/// The handler in `challenge_plugin` enforces the level gate, picks the
|
|
/// next seed from `progress.challenge_index`, and writes the
|
|
/// corresponding `NewGameRequestEvent`.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct StartChallengeRequestEvent;
|
|
|
|
/// Request to start a Time Attack session. Fired by the HUD
|
|
/// Modes-popover "Time Attack" row alongside the existing `T`
|
|
/// accelerator. The handler in `time_attack_plugin` enforces the level
|
|
/// gate, initialises `TimeAttackResource`, and writes the corresponding
|
|
/// `NewGameRequestEvent`.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct StartTimeAttackRequestEvent;
|
|
|
|
/// Request to start today's Daily Challenge. Fired by the HUD
|
|
/// Modes-popover "Daily Challenge" row alongside the existing `C`
|
|
/// accelerator. The handler in `daily_challenge_plugin` reads
|
|
/// `DailyChallengeResource::seed` and writes a `NewGameRequestEvent`.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct StartDailyChallengeRequestEvent;
|
|
|
|
/// Request to toggle the Stats overlay. Fired by the HUD Menu-popover
|
|
/// "Stats" row alongside the existing `S` accelerator.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct ToggleStatsRequestEvent;
|
|
|
|
/// Request to toggle the Achievements overlay. Fired by the HUD
|
|
/// Menu-popover "Achievements" row alongside the existing `A` accelerator.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct ToggleAchievementsRequestEvent;
|
|
|
|
/// Request to toggle the Profile overlay. Fired by the HUD Menu-popover
|
|
/// "Profile" row alongside the existing `P` accelerator.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct ToggleProfileRequestEvent;
|
|
|
|
/// Request to toggle the Settings overlay. Fired by the HUD Menu-popover
|
|
/// "Settings" row alongside the existing `O` accelerator.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct ToggleSettingsRequestEvent;
|
|
|
|
/// Request to toggle the Leaderboard overlay. Fired by the HUD
|
|
/// Menu-popover "Leaderboard" row alongside the existing `L` accelerator.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct ToggleLeaderboardRequestEvent;
|
|
|
|
/// Fired by `SyncPlugin` after a pull task resolves and the merged result has
|
|
/// been persisted to disk. `Ok(SyncResponse)` carries the merged payload plus
|
|
/// any `ConflictReport`s the merge produced. `Err(String)` carries a
|
|
/// human-readable failure message (network, auth, serialization, etc.).
|
|
///
|
|
/// UI systems listen for this to refresh views without polling
|
|
/// `SyncStatusResource`. See [ARCHITECTURE.md §4](../../ARCHITECTURE.md).
|
|
#[derive(Message, Debug, Clone)]
|
|
pub struct SyncCompleteEvent(pub Result<SyncResponse, String>);
|
|
|
|
/// Fired by `InputPlugin` when N is pressed while a game is in progress
|
|
/// but confirmation has not yet been received. The animation plugin shows
|
|
/// a "Press N again to confirm" toast. A second N press within the
|
|
/// confirmation window sends `NewGameRequestEvent`.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct NewGameConfirmEvent;
|
|
|
|
/// Generic informational toast message. Any system can fire this to display
|
|
/// a short string to the player, e.g. "Locked — reach level 5".
|
|
#[derive(Message, Debug, Clone)]
|
|
pub struct InfoToastEvent(pub String);
|
|
|
|
/// Fired by `ProgressPlugin` immediately after awarding XP for a win so the
|
|
/// animation layer can display a "+N XP" toast alongside the win cascade.
|
|
#[derive(Message, Debug, Clone, Copy)]
|
|
pub struct XpAwardedEvent {
|
|
pub amount: u64,
|
|
}
|
|
|
|
/// Fired by `InputPlugin` when the player presses G to forfeit the current
|
|
/// game. Consumed by `StatsPlugin` which records the abandoned game,
|
|
/// persists stats, and starts a fresh deal.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct ForfeitEvent;
|
|
|
|
/// Request to open the forfeit-confirm modal. Fired by the `G` accelerator
|
|
/// and by the Pause modal's "Forfeit" button so the same modal opens
|
|
/// either way. Consumed by `PausePlugin`, which spawns
|
|
/// `ForfeitConfirmScreen` after checking that a game is in progress and
|
|
/// no forfeit modal is already showing. Confirmation inside that modal
|
|
/// then fires `ForfeitEvent` for `StatsPlugin` to consume.
|
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
|
pub struct ForfeitRequestEvent;
|
|
|
|
/// Fired when the player requests a hint (H key). Carries the source card ID
|
|
/// and destination pile for visual highlighting.
|
|
///
|
|
/// Consumed by `CardPlugin` (to apply `HintHighlight` on the card entity) and
|
|
/// `TablePlugin` (to tint the destination `PileMarker` gold for 2 s).
|
|
#[derive(Message, Debug, Clone)]
|
|
pub struct HintVisualEvent {
|
|
/// The `Card::id` of the source card to be highlighted.
|
|
pub source_card_id: u32,
|
|
/// The destination pile whose `PileMarker` should be tinted gold.
|
|
pub dest_pile: solitaire_core::pile::PileType,
|
|
}
|