Files
code_of_conquest_dnd/client/scripts/theme/theme_showcase.gd
Phillip Tarrant 6530625f4d 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
2026-07-10 20:59:05 -05:00

208 lines
7.1 KiB
GDScript

# client/scripts/theme/theme_showcase.gd
extends Control
## The eyeball proof for the shared Theme (spec §7). Builds every token, stylebox,
## font, surface, and bar on one scrollable screen. No AI, no state. §2: presentation.
const Palette = preload("res://scripts/theme/palette.gd")
const ThemeKeys = preload("res://scripts/theme/theme_keys.gd")
const GAME_THEME := "res://assets/theme/game_theme.tres"
const SERIF_ITALIC := "res://assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf"
func _ready() -> void:
theme = load(GAME_THEME)
set_anchors_preset(Control.PRESET_FULL_RECT)
var scroll := ScrollContainer.new()
scroll.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(scroll)
var col := VBoxContainer.new()
col.custom_minimum_size = Vector2(1900, 0)
col.add_theme_constant_override("separation", 16)
scroll.add_child(col)
col.add_child(_heading("SHARED THEME — SHOWCASE"))
col.add_child(_swatches())
col.add_child(_variations())
col.add_child(_fonts())
col.add_child(_prose())
col.add_child(_rarity())
col.add_child(_bars())
col.add_child(_surfaces())
func _heading(text: String) -> Label:
var l := Label.new()
l.text = text
l.theme_type_variation = ThemeKeys.HEADING
return l
func _swatches() -> Control:
var grid := GridContainer.new()
grid.columns = 8
var tokens := [
["STAGE_1", Palette.STAGE_1], ["BAY_STRIPE_A", Palette.BAY_STRIPE_A],
["DARK_PANEL_70", Palette.DARK_PANEL_70], ["SHEET_TOP", Palette.SHEET_TOP],
["PARCHMENT_CARD", Palette.PARCHMENT_CARD], ["BLOOD", Palette.BLOOD],
["GOLD", Palette.GOLD], ["STEEL", Palette.STEEL], ["GREEN", Palette.GREEN],
["INK_BODY", Palette.INK_BODY], ["CREAM", Palette.CREAM],
]
for entry in tokens:
var col := VBoxContainer.new()
var r := ColorRect.new()
r.color = entry[1]
r.custom_minimum_size = Vector2(120, 60)
col.add_child(r)
var caption := Label.new()
caption.text = entry[0]
caption.theme_type_variation = ThemeKeys.MONO
col.add_child(caption)
grid.add_child(col)
return grid
func _variations() -> Control:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
var cta := Button.new(); cta.text = "ENTER THE WORLD"; cta.theme_type_variation = ThemeKeys.PRIMARY_CTA
var tab := Button.new(); tab.text = "INACTIVE"; tab.theme_type_variation = ThemeKeys.TAB
var tab_a := Button.new(); tab_a.text = "ACTIVE"; tab_a.theme_type_variation = ThemeKeys.TAB_ACTIVE
# Neutral chip (grey) plus the semantic colours a script applies per-use
# (spec §5.3): red for aggressive/combat actions, gold for pay/faith.
var chip := Button.new(); chip.text = "STEALTH · DEX"; chip.theme_type_variation = ThemeKeys.CHIP
var chip_red := _semantic_chip("INTIMIDATE · STR", Palette.BLOOD)
var chip_gold := _semantic_chip("PAY ◈40", Palette.GOLD)
for b in [cta, tab, tab_a, chip, chip_red, chip_gold]:
row.add_child(b)
var card := PanelContainer.new(); card.theme_type_variation = ThemeKeys.PARCHMENT_CARD
card.custom_minimum_size = Vector2(160, 80)
row.add_child(card)
# Filled vs empty item tile, side by side for comparison.
var tile_filled := PanelContainer.new()
tile_filled.theme_type_variation = ThemeKeys.ITEM_TILE
tile_filled.custom_minimum_size = Vector2(64, 64)
row.add_child(tile_filled)
var tile_empty := PanelContainer.new()
tile_empty.theme_type_variation = ThemeKeys.ITEM_TILE_EMPTY
tile_empty.custom_minimum_size = Vector2(64, 64)
row.add_child(tile_empty)
var inset := PanelContainer.new()
inset.theme_type_variation = ThemeKeys.PARCHMENT_INSET
inset.custom_minimum_size = Vector2(160, 80)
row.add_child(inset)
var dark := PanelContainer.new()
dark.theme_type_variation = ThemeKeys.DARK_PANEL
dark.custom_minimum_size = Vector2(160, 80)
row.add_child(dark)
return row
func _semantic_chip(text: String, color: Color) -> Button:
# Chip base is neutral (ThemeKeys.CHIP); the semantic colour (§5.3) is set
# by the consuming script, here via font-colour + border overrides.
var chip := Button.new()
chip.text = text
chip.theme_type_variation = ThemeKeys.CHIP
chip.add_theme_color_override("font_color", color)
var tinted := StyleBoxFlat.new()
tinted.bg_color = Color(color, 0.15)
tinted.border_color = color
tinted.set_border_width_all(1)
tinted.set_corner_radius_all(3)
tinted.content_margin_left = 12
tinted.content_margin_right = 12
tinted.content_margin_top = 6
tinted.content_margin_bottom = 6
chip.add_theme_stylebox_override("normal", tinted)
return chip
func _prose() -> Control:
# Narrative italic (spec §6) — the bundled EB Garamond italic face, shown
# nowhere else on this screen. RichTextLabel is required for bbcode italics;
# a plain Label cannot render [i].
var rtl := RichTextLabel.new()
rtl.bbcode_enabled = true
rtl.fit_content = true
rtl.custom_minimum_size = Vector2(900, 0)
rtl.add_theme_font_override("normal_font", load(GAME_THEME).default_font)
rtl.add_theme_font_override("italics_font", load(SERIF_ITALIC))
rtl.text = "[i]The chamber is cold. Something waits in the dark.[/i]"
return rtl
func _fonts() -> Control:
# All three bundled font families, at their real Theme sizes, plus the
# default serif body face (not otherwise shown elsewhere on this screen).
var col := VBoxContainer.new()
col.add_theme_constant_override("separation", 6)
var body := Label.new()
body.text = "The road to the Ashmarch is longer than the maps admit."
col.add_child(body)
var accent := Label.new()
accent.text = "Vexcca of the Ashmarch"
accent.theme_type_variation = ThemeKeys.ACCENT
col.add_child(accent)
var mono := Label.new()
mono.text = "STR 14 · INITIATIVE +2 · AC 16"
mono.theme_type_variation = ThemeKeys.MONO
col.add_child(mono)
return col
func _rarity() -> Control:
var row := HBoxContainer.new()
for i in range(5):
var tile := PanelContainer.new()
tile.theme_type_variation = ThemeKeys.ITEM_TILE
tile.custom_minimum_size = Vector2(72, 72)
var bar := ColorRect.new()
bar.color = Palette.rarity_color(i)
bar.custom_minimum_size = Vector2(60, 4)
tile.add_child(bar)
row.add_child(tile)
return row
func _bars() -> Control:
var row := HBoxContainer.new()
for kind in [&"hp", &"mp", &"stamina"]:
var pair = Palette.stat_bar(kind)
var bar := ColorRect.new()
bar.color = pair[0]
bar.custom_minimum_size = Vector2(200, 18)
row.add_child(bar)
return row
func _surfaces() -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 240)
for path in ["res://scenes/theme/surfaces/ParchmentPanel.tscn",
"res://scenes/theme/surfaces/DarkBay.tscn"]:
var s = load(path).instantiate()
s.custom_minimum_size = Vector2(600, 220)
row.add_child(s)
# VignetteOverlay draws a black-with-alpha dim; give it a mid-tone backing
# so the dim is visibly rendering rather than reading as empty space.
var vignette_slot := Control.new()
vignette_slot.custom_minimum_size = Vector2(600, 220)
var backing := ColorRect.new()
backing.color = Palette.STEEL
backing.set_anchors_preset(Control.PRESET_FULL_RECT)
vignette_slot.add_child(backing)
var vignette = load("res://scenes/theme/surfaces/VignetteOverlay.tscn").instantiate()
vignette.set_anchors_preset(Control.PRESET_FULL_RECT)
vignette_slot.add_child(vignette)
row.add_child(vignette_slot)
return row