refactor(engine): migrate table_plugin chrome to Terminal tokens

- Promote `marker_colour` to module-level const PILE_MARKER_DEFAULT_COLOUR
  and re-export it. cursor_plugin::MARKER_DEFAULT now imports the const
  directly, replacing the prior duplicated literal kept in sync only by
  doc comment. Drift becomes a compile error instead of a stale claim.
- Empty-tableau "K" placeholder text now uses TEXT_PRIMARY at 0.35 alpha
  (was raw `Color::srgba(1.0, 1.0, 1.0, 0.35)`) so it picks up the
  Terminal off-white foreground.
- HINT_PILE_HIGHLIGHT_COLOUR retuned from bright `srgb(1.0, 0.85, 0.1)`
  to the design-system STATE_WARNING token (`#ddb26f`). Spelled as a
  literal because Alpha::with_alpha is not yet const on stable; a new
  test pins the RGB to STATE_WARNING so a palette swap can't drift the
  two apart silently.
- The existing "is gold" character test was hardcoded to the old bright
  palette (red ≥ 0.9). Loosened to "warmer than cool" + ranges that the
  Terminal muted gold satisfies, with exact-RGB tracking handled by the
  new STATE_WARNING test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-07 18:32:03 -07:00
parent a1376075bd
commit 651f4060e6
2 changed files with 58 additions and 20 deletions
+6 -4
View File
@@ -41,14 +41,16 @@ use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
use crate::card_plugin::{RightClickHighlight, TABLEAU_FAN_FRAC}; use crate::card_plugin::{RightClickHighlight, TABLEAU_FAN_FRAC};
use crate::layout::{Layout, LayoutResource}; use crate::layout::{Layout, LayoutResource};
use crate::resources::{DragState, GameStateResource}; use crate::resources::{DragState, GameStateResource};
use crate::table_plugin::PileMarker; use crate::table_plugin::{PileMarker, PILE_MARKER_DEFAULT_COLOUR};
use crate::ui_theme::{ use crate::ui_theme::{
DROP_TARGET_FILL, DROP_TARGET_OUTLINE, DROP_TARGET_OUTLINE_PX, Z_DROP_OVERLAY, DROP_TARGET_FILL, DROP_TARGET_OUTLINE, DROP_TARGET_OUTLINE_PX, Z_DROP_OVERLAY,
}; };
/// Semi-transparent white that `table_plugin` uses for idle pile markers. /// Idle pile-marker tint — re-exported from `table_plugin` so the
/// Kept in sync with the `marker_colour` constant there. /// "valid drop" toggle in this plugin and the marker spawn in
const MARKER_DEFAULT: Color = Color::srgba(1.0, 1.0, 1.0, 0.08); /// `table_plugin` cannot drift apart. Was previously a duplicated
/// literal kept in sync via doc comment.
const MARKER_DEFAULT: Color = PILE_MARKER_DEFAULT_COLOUR;
/// Lime tint applied to pile markers that are valid drop targets during /// Lime tint applied to pile markers that are valid drop targets during
/// a drag. Same RGB as the design-system [`STATE_SUCCESS`] token at 55% /// a drag. Same RGB as the design-system [`STATE_SUCCESS`] token at 55%
+52 -16
View File
@@ -14,9 +14,21 @@ use crate::layout::{compute_layout, Layout, LayoutResource, LayoutSystem};
#[cfg(test)] #[cfg(test)]
use crate::layout::TABLE_COLOUR; use crate::layout::TABLE_COLOUR;
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource}; use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
use crate::ui_theme::TEXT_PRIMARY;
#[cfg(test)] #[cfg(test)]
use solitaire_data::Theme; use solitaire_data::Theme;
/// Default tint applied to every empty-pile marker sprite. Pure white
/// at 8% alpha — soft enough that the marker reads as a "hint of a
/// slot" rather than a panel, but visible against every felt
/// background.
///
/// Re-exported as the source of truth for `cursor_plugin::MARKER_DEFAULT`,
/// which used to duplicate the literal alongside a "kept in sync" doc
/// comment. Pulling both call sites through this const makes drift a
/// compile error instead of a stale comment.
pub const PILE_MARKER_DEFAULT_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.08);
/// Holds pre-loaded [`Handle<Image>`]s for the 5 selectable table backgrounds. /// Holds pre-loaded [`Handle<Image>`]s for the 5 selectable table backgrounds.
/// ///
/// Loaded once at startup by [`load_background_images`]. Index 0 is the /// Loaded once at startup by [`load_background_images`]. Index 0 is the
@@ -218,7 +230,7 @@ pub fn suit_symbol(suit: &Suit) -> &'static str {
} }
fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) { fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
let marker_colour = Color::srgba(1.0, 1.0, 1.0, 0.08); let marker_colour = PILE_MARKER_DEFAULT_COLOUR;
let marker_size = layout.card_size; let marker_size = layout.card_size;
let font_size = layout.card_size.x * 0.28; let font_size = layout.card_size.x * 0.28;
@@ -254,7 +266,7 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
b.spawn(( b.spawn((
Text2d::new("K"), Text2d::new("K"),
TextFont { font_size, ..default() }, TextFont { font_size, ..default() },
TextColor(Color::srgba(1.0, 1.0, 1.0, 0.35)), TextColor(TEXT_PRIMARY.with_alpha(0.35)),
Transform::from_xyz(0.0, 0.0, 0.1), Transform::from_xyz(0.0, 0.0, 0.1),
)); ));
}); });
@@ -308,9 +320,14 @@ fn on_window_resized(
// Task #6 — Hint pile-marker highlight // Task #6 — Hint pile-marker highlight
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Gold tint applied to a `PileMarker` sprite when it is the current hint /// Gold tint applied to a `PileMarker` sprite when it is the current
/// destination. /// hint destination. Same RGB as the design-system [`STATE_WARNING`]
const HINT_PILE_HIGHLIGHT_COLOUR: Color = Color::srgb(1.0, 0.85, 0.1); /// token (`#ddb26f`) so the in-game "look here" colour is the same hue
/// as every other warning/attention signal in the UI. Spelled as a
/// literal because `Alpha::with_alpha` is not yet a `const` trait
/// method on stable; the tracking test below pins the RGB to
/// `STATE_WARNING` so a future palette swap can't drift the two apart.
const HINT_PILE_HIGHLIGHT_COLOUR: Color = Color::srgb(0.867, 0.698, 0.435);
/// Listens for `HintVisualEvent` and tints the matching `PileMarker` entity /// Listens for `HintVisualEvent` and tints the matching `PileMarker` entity
/// gold for 2 s, storing the original colour in `HintPileHighlight` so it can /// gold for 2 s, storing the original colour in `HintPileHighlight` so it can
@@ -480,19 +497,33 @@ mod tests {
/// default pile marker colour so the player can see which pile is highlighted. /// default pile marker colour so the player can see which pile is highlighted.
#[test] #[test]
fn hint_pile_highlight_colour_is_distinct_from_default() { fn hint_pile_highlight_colour_is_distinct_from_default() {
let default = Color::srgba(1.0, 1.0, 1.0, 0.08); // PILE_MARKER_DEFAULT_COLOUR
assert_ne!( assert_ne!(
HINT_PILE_HIGHLIGHT_COLOUR, default, HINT_PILE_HIGHLIGHT_COLOUR, PILE_MARKER_DEFAULT_COLOUR,
"HINT_PILE_HIGHLIGHT_COLOUR must differ from the default pile marker colour" "HINT_PILE_HIGHLIGHT_COLOUR must differ from the default pile marker colour"
); );
} }
/// `HINT_PILE_HIGHLIGHT_COLOUR` is spelled as a literal because
/// `Alpha::with_alpha` is not a `const` trait method on stable.
/// This test pins its RGB to the design-system `STATE_WARNING`
/// token so a future palette swap that updates the token but
/// forgets the hint highlight fails loudly here.
#[test]
fn hint_pile_highlight_rgb_tracks_state_warning_token() {
use crate::ui_theme::STATE_WARNING;
let hint = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba();
let warning = STATE_WARNING.to_srgba();
assert!((hint.red - warning.red).abs() < 1e-6);
assert!((hint.green - warning.green).abs() < 1e-6);
assert!((hint.blue - warning.blue).abs() < 1e-6);
}
/// A freshly-created HintPileHighlight has a positive timer countdown. /// A freshly-created HintPileHighlight has a positive timer countdown.
#[test] #[test]
fn hint_pile_highlight_timer_starts_positive() { fn hint_pile_highlight_timer_starts_positive() {
let h = HintPileHighlight { let h = HintPileHighlight {
timer: 2.0, timer: 2.0,
original_color: Color::srgba(1.0, 1.0, 1.0, 0.08), original_color: PILE_MARKER_DEFAULT_COLOUR,
}; };
assert!( assert!(
h.timer > 0.0, h.timer > 0.0,
@@ -529,17 +560,22 @@ mod tests {
); );
} }
/// The gold hint colour must have a strong yellow component (r ≥ 0.9, g ≥ 0.8, /// The hint colour must read as "gold-ish" — red dominant, green
/// b ≤ 0.3) to be clearly visible as a "destination" indicator. /// close behind, blue noticeably lower — so a player intuitively
/// associates the highlight with attention/warning. Bounds are
/// loose enough to accommodate the Terminal palette's muted gold
/// (`STATE_WARNING`, `#ddb26f`) while still rejecting a stray
/// red, green, or neutral grey if someone refactors badly.
/// Exact-RGB tracking lives in
/// `hint_pile_highlight_rgb_tracks_state_warning_token`.
#[test] #[test]
fn hint_pile_highlight_colour_is_gold() { fn hint_pile_highlight_colour_is_gold() {
// Extract linear components. srgb(1.0, 0.85, 0.1) is the expected gold.
// We test the channel values rather than exact equality so future tweaks
// to the shade do not break the test, as long as the colour remains golden.
let Srgba { red, green, blue, .. } = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba(); let Srgba { red, green, blue, .. } = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba();
assert!(red >= 0.9, "gold hint colour must have red ≥ 0.9, got {red}"); assert!(red >= 0.7, "gold hint colour must have red ≥ 0.7, got {red}");
assert!(green >= 0.7, "gold hint colour must have green ≥ 0.7, got {green}"); assert!(green >= 0.5, "gold hint colour must have green ≥ 0.5, got {green}");
assert!(blue <= 0.3, "gold hint colour must have blue ≤ 0.3, got {blue}"); assert!(blue <= 0.6, "gold hint colour must have blue ≤ 0.6, got {blue}");
assert!(red > blue, "gold hint colour must be warmer than cool, got r={red} b={blue}");
assert!(green > blue, "gold hint colour must be warmer than cool, got g={green} b={blue}");
} }
#[test] #[test]