feat(engine): "Won before" HUD indicator on rematched seeds
When the current deal's (seed, draw_mode, mode) triple matches an entry in the rolling ReplayHistory, the HUD's tier-2 context row now shows "✓ Won before" in the success-green colour. Cleared when the active game itself is won (the on-screen victory cue is enough) and on fresh deals the player hasn't beaten before. The indicator answers a question the rolling-history feature implicitly raised: when a new game starts on a seed the player has already conquered, surface that fact so they know they can try for a faster / higher-scoring win on the same layout. Seed re-rolls in "Winnable deals only" + system-time seeds make this a natural pace for the indicator to fire — usually empty, occasionally lit. Implementation: new `HudWonPreviously` marker spawned in tier-2 alongside Mode / Challenge / DrawCycle. Driven by a separate `update_won_previously` system rather than threading the marker through `update_hud`'s ten-way query disambiguation. Reads the existing `ReplayHistoryResource` from `stats_plugin`; gracefully no-ops in headless tests that don't load StatsPlugin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,18 @@ pub struct HudMode;
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct HudChallenge;
|
pub struct HudChallenge;
|
||||||
|
|
||||||
|
/// Marker on the "won this deal before" indicator text node.
|
||||||
|
///
|
||||||
|
/// Displays `"✓ Won before"` when the current deal's seed + draw_mode +
|
||||||
|
/// mode triple matches one of the entries in `ReplayHistoryResource`.
|
||||||
|
/// Empty string otherwise (including won games — the score readout
|
||||||
|
/// already conveys the win on the active deal). Only meaningful for
|
||||||
|
/// Classic / Zen / Challenge — daily-challenge and time-attack seeds
|
||||||
|
/// are filtered out implicitly because their replay entries always
|
||||||
|
/// carry a different mode tag.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct HudWonPreviously;
|
||||||
|
|
||||||
/// Marker on the undo-count text node.
|
/// Marker on the undo-count text node.
|
||||||
///
|
///
|
||||||
/// Shows how many undos have been used this game. Displayed in amber when
|
/// Shows how many undos have been used this game. Displayed in amber when
|
||||||
@@ -302,6 +314,7 @@ impl Plugin for HudPlugin {
|
|||||||
.init_resource::<HudActionFade>()
|
.init_resource::<HudActionFade>()
|
||||||
.add_systems(Startup, (spawn_hud_band, spawn_hud, spawn_action_buttons))
|
.add_systems(Startup, (spawn_hud_band, spawn_hud, spawn_action_buttons))
|
||||||
.add_systems(Update, update_hud.after(GameMutation))
|
.add_systems(Update, update_hud.after(GameMutation))
|
||||||
|
.add_systems(Update, update_won_previously.after(GameMutation))
|
||||||
.add_systems(Update, announce_auto_complete.after(GameMutation))
|
.add_systems(Update, announce_auto_complete.after(GameMutation))
|
||||||
.add_systems(Update, update_selection_hud)
|
.add_systems(Update, update_selection_hud)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
@@ -481,6 +494,15 @@ fn spawn_hud(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
|||||||
font_body.clone(),
|
font_body.clone(),
|
||||||
TextColor(STATE_INFO),
|
TextColor(STATE_INFO),
|
||||||
));
|
));
|
||||||
|
t2.spawn((
|
||||||
|
HudWonPreviously,
|
||||||
|
Tooltip::new(
|
||||||
|
"You've won this deal before. Same seed in your replay history.",
|
||||||
|
),
|
||||||
|
Text::new(""),
|
||||||
|
font_body.clone(),
|
||||||
|
TextColor(STATE_SUCCESS),
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tier 3 — penalty / bonus. Undos and Recycles share the
|
// Tier 3 — penalty / bonus. Undos and Recycles share the
|
||||||
@@ -1480,6 +1502,42 @@ fn lerp_text_color(from: Color, to: Color, t: f32) -> Color {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the [`HudWonPreviously`] text to "✓ Won before" whenever the
|
||||||
|
/// current deal's seed + draw_mode + mode triple matches an entry in
|
||||||
|
/// the rolling [`ReplayHistory`]. Cleared while the active game is won
|
||||||
|
/// (the on-screen "Game won!" cue already conveys victory) and on
|
||||||
|
/// fresh deals the player hasn't won before.
|
||||||
|
///
|
||||||
|
/// Lives in its own system rather than `update_hud` to keep this
|
||||||
|
/// orthogonal: `update_hud`'s query disambiguation is already busy
|
||||||
|
/// enough; threading another marker through every Without filter
|
||||||
|
/// would touch ~10 unrelated queries for no benefit.
|
||||||
|
fn update_won_previously(
|
||||||
|
game: Res<GameStateResource>,
|
||||||
|
// Optional because the HUD plugin's headless tests run without
|
||||||
|
// `StatsPlugin` and therefore without this resource. With the
|
||||||
|
// resource absent there's no history to compare against; the
|
||||||
|
// indicator just stays empty.
|
||||||
|
history: Option<Res<crate::stats_plugin::ReplayHistoryResource>>,
|
||||||
|
mut q: Query<&mut Text, With<HudWonPreviously>>,
|
||||||
|
) {
|
||||||
|
let Ok(mut text) = q.single_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let won_before = !game.0.is_won
|
||||||
|
&& history.as_ref().is_some_and(|h| {
|
||||||
|
h.0.replays.iter().any(|r| {
|
||||||
|
r.seed == game.0.seed
|
||||||
|
&& r.draw_mode == game.0.draw_mode
|
||||||
|
&& r.mode == game.0.mode
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let next = if won_before { "\u{2713} Won before" } else { "" };
|
||||||
|
if text.0 != next {
|
||||||
|
text.0 = next.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||||
fn update_hud(
|
fn update_hud(
|
||||||
game: Res<GameStateResource>,
|
game: Res<GameStateResource>,
|
||||||
|
|||||||
Reference in New Issue
Block a user