feat(engine): UX iteration round — tooltip slider, streak fire, score breakdown
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>
This commit is contained in:
@@ -34,6 +34,7 @@ use bevy::prelude::*;
|
||||
use bevy::ui::{ComputedNode, UiGlobalTransform};
|
||||
|
||||
use crate::font_plugin::FontResource;
|
||||
use crate::settings_plugin::SettingsResource;
|
||||
use crate::ui_theme::{
|
||||
BG_ELEVATED_HI, BORDER_SUBTLE, MOTION_TOOLTIP_DELAY_SECS, RADIUS_SM, TEXT_PRIMARY,
|
||||
TYPE_CAPTION, VAL_SPACE_2, Z_TOOLTIP,
|
||||
@@ -137,6 +138,23 @@ struct TooltipText;
|
||||
/// target's own border.
|
||||
const TOOLTIP_GAP_PX: f32 = 4.0;
|
||||
|
||||
/// Pure helper: returns `true` once `elapsed_secs` has met or exceeded
|
||||
/// the player-configured `delay_secs`, so the tooltip should be revealed.
|
||||
///
|
||||
/// Treating "elapsed >= delay" as the show condition (rather than
|
||||
/// strictly greater than) is what makes a `delay_secs == 0.0` setting
|
||||
/// behave as advertised: on the very first tick after hover starts,
|
||||
/// `elapsed_secs` is `0.0` and the tooltip appears immediately. With a
|
||||
/// strict `>` the zero-delay case would still wait one tick.
|
||||
///
|
||||
/// Extracted so the comparison can be unit-tested without spinning up
|
||||
/// a Bevy `App` — `Time<Virtual>` clamps each tick to 250 ms under
|
||||
/// `MinimalPlugins`, which makes precise sub-second timing assertions
|
||||
/// awkward.
|
||||
pub(crate) fn tooltip_should_show(elapsed_secs: f32, delay_secs: f32) -> bool {
|
||||
elapsed_secs >= delay_secs
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Systems
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -257,6 +275,7 @@ fn track_tooltip_hover(
|
||||
fn show_or_hide_tooltip(
|
||||
time: Res<Time>,
|
||||
state: Res<TooltipState>,
|
||||
settings: Option<Res<SettingsResource>>,
|
||||
tooltips: Query<(&Tooltip, &UiGlobalTransform, &ComputedNode)>,
|
||||
tooltip_text_only: Query<&Tooltip>,
|
||||
mut overlay_q: Query<(&mut Node, &mut Visibility, &Children), With<TooltipOverlay>>,
|
||||
@@ -280,9 +299,15 @@ fn show_or_hide_tooltip(
|
||||
return;
|
||||
};
|
||||
|
||||
// Player-configurable dwell delay; falls back to the design-token
|
||||
// default when `SettingsResource` is absent (test harnesses running
|
||||
// `UiTooltipPlugin` under `MinimalPlugins` without `SettingsPlugin`).
|
||||
let delay_secs = settings
|
||||
.as_ref()
|
||||
.map(|s| s.0.tooltip_delay_secs)
|
||||
.unwrap_or(MOTION_TOOLTIP_DELAY_SECS);
|
||||
let elapsed = time.elapsed().saturating_sub(started_at);
|
||||
let delay = Duration::from_secs_f32(MOTION_TOOLTIP_DELAY_SECS);
|
||||
if elapsed < delay {
|
||||
if !tooltip_should_show(elapsed.as_secs_f32(), delay_secs) {
|
||||
hide(&mut visibility);
|
||||
return;
|
||||
}
|
||||
@@ -550,4 +575,30 @@ mod tests {
|
||||
"overlay text must update to the new hovered entity's Tooltip string"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 5: `tooltip_should_show` is the pure helper that the system
|
||||
/// uses to gate the reveal — exercising it directly avoids the
|
||||
/// `Time<Virtual>` 250 ms clamp that makes precise sub-second
|
||||
/// timing assertions in `MinimalPlugins` fiddly. The four cases
|
||||
/// below cover the boundary semantics:
|
||||
///
|
||||
/// * `delay = 0.0` ("Instant") must show on the first tick.
|
||||
/// * `elapsed < delay` must NOT show.
|
||||
/// * `elapsed == delay` must show (boundary inclusive).
|
||||
/// * `elapsed > delay` must show.
|
||||
#[test]
|
||||
fn tooltip_should_show_respects_delay() {
|
||||
// delay == 0 ("Instant"): any elapsed (including zero) shows.
|
||||
assert!(tooltip_should_show(0.0, 0.0), "instant delay must show on first tick");
|
||||
assert!(tooltip_should_show(0.5, 0.0));
|
||||
|
||||
// Standard non-zero delay.
|
||||
assert!(!tooltip_should_show(0.4, 0.5), "elapsed < delay must hide");
|
||||
assert!(tooltip_should_show(0.5, 0.5), "elapsed == delay must show (boundary)");
|
||||
assert!(tooltip_should_show(0.6, 0.5), "elapsed > delay must show");
|
||||
|
||||
// Larger delay (max-end of the slider).
|
||||
assert!(!tooltip_should_show(1.0, 1.5));
|
||||
assert!(tooltip_should_show(1.5, 1.5));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user