feat(engine): reserve top band for HUD so it stops crowding the cards

Player report: the action button bar (Menu / Undo / Pause / Help /
Modes / New Game) and Score / Moves / Timer text were sharing the
same vertical band as the stock + foundation row, with no visual
separation. The HUD read as part of the play surface.

Two-part fix:

1. layout.rs reserves HUD_BAND_HEIGHT (64 px) at the top of the
   window. Card-grid math takes that off the available vertical
   budget so cards still fit; top_y shifts down by the same amount.
   New layout test pins the reservation. Existing
   worst_case_tableau_fits_vertically tests verify the height-budget
   arithmetic still holds.

2. hud_plugin.rs spawns a translucent purple band (BG_HUD_BAND, new
   token in ui_theme.rs at the BG_BASE hue with 0.70 alpha) filling
   that reserved zone. Z-index sits one rung below Z_HUD so action
   buttons paint on top while the band reads as their container. The
   band's bottom edge lines up with the top edge of the highest
   playable card, so the buttons feel anchored to a "tools strip"
   rather than floating in the play area.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-01 18:57:51 +00:00
parent efa063fb8f
commit 2c72e1fc87
3 changed files with 69 additions and 7 deletions
+31 -2
View File
@@ -16,9 +16,10 @@ use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
use crate::daily_challenge_plugin::DailyChallengeResource;
use crate::progress_plugin::ProgressResource;
use crate::settings_plugin::SettingsResource;
use crate::layout::HUD_BAND_HEIGHT;
use crate::ui_theme::{
scaled_duration, ACCENT_PRIMARY, ACCENT_SECONDARY, BG_ELEVATED, BG_ELEVATED_HI,
BG_ELEVATED_PRESSED, BORDER_SUBTLE, MOTION_SCORE_PULSE_SECS, RADIUS_MD, RADIUS_SM,
BG_ELEVATED_PRESSED, BG_HUD_BAND, BORDER_SUBTLE, MOTION_SCORE_PULSE_SECS, RADIUS_MD, RADIUS_SM,
STATE_DANGER, STATE_INFO, STATE_SUCCESS, STATE_WARNING, TEXT_PRIMARY, TEXT_SECONDARY,
TYPE_BODY, TYPE_BODY_LG, TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3,
};
@@ -251,7 +252,7 @@ impl Plugin for HudPlugin {
.add_message::<ToggleSettingsRequestEvent>()
.add_message::<ToggleLeaderboardRequestEvent>()
.init_resource::<PreviousScore>()
.add_systems(Startup, (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, announce_auto_complete.after(GameMutation))
.add_systems(Update, update_selection_hud)
@@ -282,6 +283,34 @@ impl Plugin for HudPlugin {
}
}
/// Spawns the translucent HUD band that anchors the action buttons
/// and primary readouts visually. Sits behind every other HUD element
/// (one z-rung below `Z_HUD`) so it reads as the band's "container"
/// without intercepting clicks from the buttons it sits under.
///
/// Width is full-window, height matches `layout::HUD_BAND_HEIGHT` (the
/// same constant the card layout reserves at the top), so the band's
/// bottom edge lines up exactly with the top edge of the highest
/// playable card. The fill is `BG_HUD_BAND` — midnight purple at 0.70
/// alpha, so the green felt reads through subtly.
fn spawn_hud_band(mut commands: Commands) {
commands.spawn((
Node {
position_type: PositionType::Absolute,
top: Val::Px(0.0),
left: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Px(HUD_BAND_HEIGHT),
..default()
},
BackgroundColor(BG_HUD_BAND),
// Sit one z-rung below the HUD content so the buttons and text
// paint on top, but above the card sprites (which are 2D-world
// entities and rendered behind UI regardless).
ZIndex(Z_HUD - 1),
));
}
/// Spawns the in-game HUD as a 4-tier vertical column anchored to the
/// top-left of the play area.
///