fix(theme): drift-guard test, interactive stylebox states, ThemeKeys font consts, richer showcase (whole-branch review)

- build_game_theme.gd: extract build_theme() as a static func returning the
  fully-built Theme; _init() is now a thin save+quit wrapper. Enables a test
  to build a fresh Theme in memory without running the SceneTree main loop.
- test_theme_resource.gd: add test_committed_tres_matches_builder() — builds
  a fresh Theme via the preloaded builder and compares bg_color against the
  committed game_theme.tres for PrimaryCTA/normal, ParchmentCard/panel, and
  DarkPanel/panel. Catches "palette/builder changed, .tres not regenerated."
- build_game_theme.gd: give Tab, TabActive, and Chip explicit hover/pressed/
  focus styleboxes (focus is StyleBoxEmpty to suppress the default ring) so
  they no longer fall back to Godot's default gray button on interaction.
  Add a disabled stylebox + font_disabled_color to PrimaryCTA for the
  unaffordable-CTA state the 2a shell will need.
- theme_keys.gd: add HEADING/ACCENT/MONO font-role consts (kept out of ALL,
  which is stylebox-variation-only); build_game_theme.gd and
  theme_showcase.gd now reference them instead of raw StringName literals.
- theme_showcase.gd: add an italic DM-voice RichTextLabel (EB Garamond
  italic face, spec §6) and two semantic-coloured chips (BLOOD for
  aggressive actions, GOLD for pay/faith) alongside the existing neutral
  chip, demonstrating the script-applied colouring from spec §5.3.
- dark_bay.gdshader: comment noting the stripe uses FRAGCOORD (screen-space)
  while the vignette uses UV (node-local) — intended for full-bleed use.
- surfaces/*.gd: ## comments documenting the accepted deviation from spec
  §5.4 (uniforms set at runtime from Palette rather than baked into the
  .tscn) — no behavior change.
- Regenerated game_theme.tres via build_game_theme.gd.

Full suite: 125/125 (was 124, +1 drift-guard test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFDm1ku9WDUacK2S78m2be
This commit is contained in:
2026-07-10 20:59:05 -05:00
parent 0082765ee7
commit 6530625f4d
12 changed files with 304 additions and 63 deletions

View File

@@ -5,6 +5,12 @@ extends SceneTree
## godot --headless -s res://scripts/theme/build_game_theme.gd
## Constructs game_theme.tres from Palette + the bundled fonts and saves it.
## The builder is the source of truth; the .tres is a committed artifact.
##
## build_theme() is a static func so it is reachable without saving/quitting —
## tests preload this script and call Builder.build_theme() directly to compare
## a fresh in-memory Theme against the committed .tres (drift guard). Preloading
## a script and calling a static func does not run _init() — only `.new()` does
## — so this is safe to call from a test without side effects.
## §2: presentation.
const Palette = preload("res://scripts/theme/palette.gd")
@@ -17,14 +23,8 @@ const MONO := "res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf"
func _init() -> void:
var theme := Theme.new()
_fonts(theme)
_primary_cta(theme)
_tabs(theme)
_chip(theme)
_tiles(theme)
_cards_and_panels(theme)
var err := ResourceSaver.save(theme, OUT)
var t := build_theme()
var err := ResourceSaver.save(t, OUT)
if err != OK:
push_error("theme save failed: %d" % err)
else:
@@ -32,7 +32,18 @@ func _init() -> void:
quit()
func _flat(bg: Color, border: Color, width := 1, radius := 4) -> StyleBoxFlat:
static func build_theme() -> Theme:
var theme := Theme.new()
_fonts(theme)
_primary_cta(theme)
_tabs(theme)
_chip(theme)
_tiles(theme)
_cards_and_panels(theme)
return theme
static func _flat(bg: Color, border: Color, width := 1, radius := 4) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = bg
s.border_color = border
@@ -45,58 +56,72 @@ func _flat(bg: Color, border: Color, width := 1, radius := 4) -> StyleBoxFlat:
return s
func _fonts(theme: Theme) -> void:
static func _fonts(theme: Theme) -> void:
var serif: FontFile = load(SERIF)
# Global defaults: prose serif at body size.
theme.default_font = serif
theme.default_font_size = 18
# Heading role via the Label "Heading" variation.
theme.set_type_variation(&"Heading", "Label")
theme.set_font(&"font", &"Heading", serif)
theme.set_font_size(&"font_size", &"Heading", 34)
theme.set_color(&"font_color", &"Heading", Palette.INK_HEADING)
theme.set_type_variation(ThemeKeys.HEADING, "Label")
theme.set_font(&"font", ThemeKeys.HEADING, serif)
theme.set_font_size(&"font_size", ThemeKeys.HEADING, 34)
theme.set_color(&"font_color", ThemeKeys.HEADING, Palette.INK_HEADING)
# Accent (hand-inked) role.
theme.set_type_variation(&"Accent", "Label")
theme.set_font(&"font", &"Accent", load(ACCENT))
theme.set_font_size(&"font_size", &"Accent", 24)
theme.set_color(&"font_color", &"Accent", Palette.INK_GOLD)
theme.set_type_variation(ThemeKeys.ACCENT, "Label")
theme.set_font(&"font", ThemeKeys.ACCENT, load(ACCENT))
theme.set_font_size(&"font_size", ThemeKeys.ACCENT, 24)
theme.set_color(&"font_color", ThemeKeys.ACCENT, Palette.INK_GOLD)
# Mono chrome role.
theme.set_type_variation(&"Mono", "Label")
theme.set_font(&"font", &"Mono", load(MONO))
theme.set_font_size(&"font_size", &"Mono", 13)
theme.set_color(&"font_color", &"Mono", Palette.MUTED_MONO)
theme.set_type_variation(ThemeKeys.MONO, "Label")
theme.set_font(&"font", ThemeKeys.MONO, load(MONO))
theme.set_font_size(&"font_size", ThemeKeys.MONO, 13)
theme.set_color(&"font_color", ThemeKeys.MONO, Palette.MUTED_MONO)
func _primary_cta(theme: Theme) -> void:
static func _primary_cta(theme: Theme) -> void:
var k := ThemeKeys.PRIMARY_CTA
theme.set_type_variation(k, "Button")
theme.set_stylebox(&"normal", k, _flat(Palette.BLOOD, Palette.BLOOD, 1, 4))
theme.set_stylebox(&"hover", k, _flat(Palette.BLOOD_BRIGHT, Palette.BLOOD, 1, 4))
theme.set_stylebox(&"pressed", k, _flat(Palette.BLOOD_DARK, Palette.BLOOD, 1, 4))
# Unaffordable/disabled CTA state (2a shell needs this) — desaturated/greyed.
theme.set_stylebox(&"disabled", k, _flat(Palette.MUTED_MONO_DIM, Palette.MUTED_MONO_DIM, 1, 4))
theme.set_color(&"font_color", k, Palette.CREAM_BRIGHT)
theme.set_color(&"font_disabled_color", k, Palette.MUTED_MONO)
func _tabs(theme: Theme) -> void:
theme.set_type_variation(ThemeKeys.TAB, "Button")
theme.set_stylebox(&"normal", ThemeKeys.TAB, _flat(Palette.TAB_INACTIVE_BG, Palette.PARCHMENT_BORDER_2, 1, 14))
theme.set_color(&"font_color", ThemeKeys.TAB, Palette.INK_MUTED)
theme.set_type_variation(ThemeKeys.TAB_ACTIVE, "Button")
theme.set_stylebox(&"normal", ThemeKeys.TAB_ACTIVE, _flat(Palette.BLOOD, Palette.BLOOD, 1, 14))
theme.set_color(&"font_color", ThemeKeys.TAB_ACTIVE, Palette.CREAM_BRIGHT)
static func _tabs(theme: Theme) -> void:
var tab := ThemeKeys.TAB
theme.set_type_variation(tab, "Button")
theme.set_stylebox(&"normal", tab, _flat(Palette.TAB_INACTIVE_BG, Palette.PARCHMENT_BORDER_2, 1, 14))
theme.set_stylebox(&"hover", tab, _flat(Palette.PARCHMENT_CARD, Palette.PARCHMENT_BORDER_2, 1, 14))
theme.set_stylebox(&"pressed", tab, _flat(Palette.PARCHMENT_INSET, Palette.PARCHMENT_BORDER, 1, 14))
theme.set_stylebox(&"focus", tab, StyleBoxEmpty.new())
theme.set_color(&"font_color", tab, Palette.INK_MUTED)
var tab_active := ThemeKeys.TAB_ACTIVE
theme.set_type_variation(tab_active, "Button")
theme.set_stylebox(&"normal", tab_active, _flat(Palette.BLOOD, Palette.BLOOD, 1, 14))
theme.set_stylebox(&"hover", tab_active, _flat(Palette.BLOOD_BRIGHT, Palette.BLOOD_BRIGHT, 1, 14))
theme.set_stylebox(&"pressed", tab_active, _flat(Palette.BLOOD_DARK, Palette.BLOOD, 1, 14))
theme.set_stylebox(&"focus", tab_active, StyleBoxEmpty.new())
theme.set_color(&"font_color", tab_active, Palette.CREAM_BRIGHT)
func _chip(theme: Theme) -> void:
static func _chip(theme: Theme) -> void:
# Base chip; the semantic colour (red/gold/grey) is applied per-use by script.
var k := ThemeKeys.CHIP
theme.set_type_variation(k, "Button")
var s := _flat(Color(Palette.MUTED_MONO, 0.12), Palette.MUTED_MONO, 1, 3)
theme.set_stylebox(&"normal", k, s)
theme.set_stylebox(&"normal", k, _flat(Color(Palette.MUTED_MONO, 0.12), Palette.MUTED_MONO, 1, 3))
theme.set_stylebox(&"hover", k, _flat(Color(Palette.MUTED_MONO, 0.22), Palette.MUTED_MONO, 1, 3))
theme.set_stylebox(&"pressed", k, _flat(Color(Palette.MUTED_MONO, 0.32), Palette.MUTED_MONO, 1, 3))
theme.set_stylebox(&"focus", k, StyleBoxEmpty.new())
theme.set_font(&"font", k, load(MONO))
theme.set_font_size(&"font_size", k, 11)
theme.set_color(&"font_color", k, Palette.MUTED_MONO)
func _tiles(theme: Theme) -> void:
static func _tiles(theme: Theme) -> void:
var filled := _flat(Palette.PARCHMENT_INSET, Palette.PARCHMENT_BORDER, 2, 6)
theme.set_type_variation(ThemeKeys.ITEM_TILE, "PanelContainer")
theme.set_stylebox(&"panel", ThemeKeys.ITEM_TILE, filled)
@@ -106,7 +131,7 @@ func _tiles(theme: Theme) -> void:
theme.set_stylebox(&"panel", ThemeKeys.ITEM_TILE_EMPTY, empty)
func _cards_and_panels(theme: Theme) -> void:
static func _cards_and_panels(theme: Theme) -> void:
theme.set_type_variation(ThemeKeys.PARCHMENT_CARD, "PanelContainer")
theme.set_stylebox(&"panel", ThemeKeys.PARCHMENT_CARD, _flat(Palette.PARCHMENT_CARD, Palette.PARCHMENT_BORDER, 1, 6))
theme.set_type_variation(ThemeKeys.PARCHMENT_INSET, "PanelContainer")