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:
@@ -4,7 +4,25 @@
|
||||
[ext_resource type="FontFile" path="res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf" id="2_605w4"]
|
||||
[ext_resource type="FontFile" path="res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf" id="3_4lwr2"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_eckwq"]
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_eckwq"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i0b23"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.6039216, 0.56078434, 0.47058824, 0.22)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cqbvn"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -20,7 +38,23 @@ corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i0b23"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q5gpk"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.6039216, 0.56078434, 0.47058824, 0.32)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bbpmc"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -36,7 +70,7 @@ corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cqbvn"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_73lme"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -52,7 +86,7 @@ corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q5gpk"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_js3tr"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -69,7 +103,7 @@ corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bbpmc"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ube3r"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -85,7 +119,7 @@ corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_73lme"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2tooq"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -101,7 +135,23 @@ corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_js3tr"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4kten"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.47843137, 0.4392157, 0.3764706, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.47843137, 0.4392157, 0.3764706, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p6wtm"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -117,7 +167,7 @@ corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ube3r"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0fjdt"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -133,7 +183,7 @@ corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2tooq"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ns3kg"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -149,7 +199,25 @@ corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4kten"]
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jly1r"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lw8lk"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.94509804, 0.9098039, 0.8235294, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.79607844, 0.7137255, 0.5176471, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nh0e7"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -165,7 +233,41 @@ corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p6wtm"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dtr07"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.9019608, 0.8509804, 0.72156864, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.7490196, 0.65882355, 0.47058824, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_nmowx"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oi067"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.72156864, 0.32156864, 0.2901961, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.72156864, 0.32156864, 0.2901961, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ukblj"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
@@ -181,6 +283,22 @@ corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_utt73"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.47843137, 0.18431373, 0.16078432, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[resource]
|
||||
default_font = ExtResource("3_4lwr2")
|
||||
default_font_size = 18
|
||||
@@ -192,33 +310,44 @@ Chip/base_type = &"Button"
|
||||
Chip/colors/font_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
Chip/font_sizes/font_size = 11
|
||||
Chip/fonts/font = ExtResource("2_605w4")
|
||||
Chip/styles/normal = SubResource("StyleBoxFlat_eckwq")
|
||||
Chip/styles/focus = SubResource("StyleBoxEmpty_eckwq")
|
||||
Chip/styles/hover = SubResource("StyleBoxFlat_i0b23")
|
||||
Chip/styles/normal = SubResource("StyleBoxFlat_cqbvn")
|
||||
Chip/styles/pressed = SubResource("StyleBoxFlat_q5gpk")
|
||||
DarkPanel/base_type = &"PanelContainer"
|
||||
DarkPanel/styles/panel = SubResource("StyleBoxFlat_i0b23")
|
||||
DarkPanel/styles/panel = SubResource("StyleBoxFlat_bbpmc")
|
||||
Heading/base_type = &"Label"
|
||||
Heading/colors/font_color = Color(0.22745098, 0.18431373, 0.10980392, 1)
|
||||
Heading/font_sizes/font_size = 34
|
||||
Heading/fonts/font = ExtResource("3_4lwr2")
|
||||
ItemTile/base_type = &"PanelContainer"
|
||||
ItemTile/styles/panel = SubResource("StyleBoxFlat_cqbvn")
|
||||
ItemTile/styles/panel = SubResource("StyleBoxFlat_73lme")
|
||||
ItemTileEmpty/base_type = &"PanelContainer"
|
||||
ItemTileEmpty/styles/panel = SubResource("StyleBoxFlat_q5gpk")
|
||||
ItemTileEmpty/styles/panel = SubResource("StyleBoxFlat_js3tr")
|
||||
Mono/base_type = &"Label"
|
||||
Mono/colors/font_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
Mono/font_sizes/font_size = 13
|
||||
Mono/fonts/font = ExtResource("2_605w4")
|
||||
ParchmentCard/base_type = &"PanelContainer"
|
||||
ParchmentCard/styles/panel = SubResource("StyleBoxFlat_bbpmc")
|
||||
ParchmentCard/styles/panel = SubResource("StyleBoxFlat_ube3r")
|
||||
ParchmentInset/base_type = &"PanelContainer"
|
||||
ParchmentInset/styles/panel = SubResource("StyleBoxFlat_73lme")
|
||||
ParchmentInset/styles/panel = SubResource("StyleBoxFlat_2tooq")
|
||||
PrimaryCTA/base_type = &"Button"
|
||||
PrimaryCTA/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
|
||||
PrimaryCTA/styles/hover = SubResource("StyleBoxFlat_js3tr")
|
||||
PrimaryCTA/styles/normal = SubResource("StyleBoxFlat_ube3r")
|
||||
PrimaryCTA/styles/pressed = SubResource("StyleBoxFlat_2tooq")
|
||||
PrimaryCTA/colors/font_disabled_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
PrimaryCTA/styles/disabled = SubResource("StyleBoxFlat_4kten")
|
||||
PrimaryCTA/styles/hover = SubResource("StyleBoxFlat_p6wtm")
|
||||
PrimaryCTA/styles/normal = SubResource("StyleBoxFlat_0fjdt")
|
||||
PrimaryCTA/styles/pressed = SubResource("StyleBoxFlat_ns3kg")
|
||||
Tab/base_type = &"Button"
|
||||
Tab/colors/font_color = Color(0.41568628, 0.3529412, 0.22745098, 1)
|
||||
Tab/styles/normal = SubResource("StyleBoxFlat_4kten")
|
||||
Tab/styles/focus = SubResource("StyleBoxEmpty_jly1r")
|
||||
Tab/styles/hover = SubResource("StyleBoxFlat_lw8lk")
|
||||
Tab/styles/normal = SubResource("StyleBoxFlat_nh0e7")
|
||||
Tab/styles/pressed = SubResource("StyleBoxFlat_dtr07")
|
||||
TabActive/base_type = &"Button"
|
||||
TabActive/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
|
||||
TabActive/styles/normal = SubResource("StyleBoxFlat_p6wtm")
|
||||
TabActive/styles/focus = SubResource("StyleBoxEmpty_nmowx")
|
||||
TabActive/styles/hover = SubResource("StyleBoxFlat_oi067")
|
||||
TabActive/styles/normal = SubResource("StyleBoxFlat_ukblj")
|
||||
TabActive/styles/pressed = SubResource("StyleBoxFlat_utt73")
|
||||
|
||||
@@ -4,6 +4,11 @@ uniform vec4 stripe_b : source_color;
|
||||
uniform float band_px = 16.0; // 16px bands per README
|
||||
uniform float vignette = 0.55; // edge darkening strength
|
||||
void fragment() {
|
||||
// Coordinate spaces differ on purpose: the stripe uses FRAGCOORD (screen-space,
|
||||
// not node-local), while the vignette below uses UV (node-local, 0..1 across
|
||||
// this node's rect). An offset DarkBay instance therefore keeps its stripes
|
||||
// anchored to the screen rather than restarting at the node's own origin —
|
||||
// intended for full-bleed use.
|
||||
// 135deg diagonal stripe: sum of pixel coords, banded.
|
||||
vec2 px = FRAGCOORD.xy;
|
||||
float diag = px.x + px.y;
|
||||
|
||||
@@ -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")
|
||||
|
||||
1
client/scripts/theme/palette.gd.uid
Normal file
1
client/scripts/theme/palette.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d4bext4g7reql
|
||||
@@ -1,6 +1,9 @@
|
||||
class_name DarkBay
|
||||
extends Panel
|
||||
## Drop-in dark stripe+vignette bay backdrop. §2: presentation.
|
||||
## Uniforms are set at runtime from Palette (deliberate deviation from spec §5.4's
|
||||
## "baked into the .tscn/material" approach) — keeps Palette the single source of
|
||||
## truth at the cost of an editor preview; the surface renders once the scene runs.
|
||||
const Palette = preload("res://scripts/theme/palette.gd")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
class_name ParchmentPanel
|
||||
extends Panel
|
||||
## Drop-in parchment backdrop (mockups "Dark Bay + Parchment" motif). §2: presentation.
|
||||
## Uniforms are set at runtime from Palette (deliberate deviation from spec §5.4's
|
||||
## "baked into the .tscn/material" approach) — keeps Palette the single source of
|
||||
## truth at the cost of an editor preview; the surface renders once the scene runs.
|
||||
const Palette = preload("res://scripts/theme/palette.gd")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
class_name VignetteOverlay
|
||||
extends ColorRect
|
||||
## Drop-in edge-dim overlay for pause/dialogue dim. §2: presentation.
|
||||
## The ShaderMaterial (and, for the other two surfaces, its Palette-sourced
|
||||
## uniforms) is built at runtime in _ready() rather than baked into the .tscn
|
||||
## (deliberate deviation from spec §5.4's "baked" approach) — keeps Palette the
|
||||
## single source of truth at the cost of an editor preview; the surface renders
|
||||
## once the scene runs.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
||||
@@ -15,6 +15,12 @@ const PARCHMENT_CARD := &"ParchmentCard"
|
||||
const PARCHMENT_INSET := &"ParchmentInset"
|
||||
const DARK_PANEL := &"DarkPanel"
|
||||
|
||||
## Font roles (Label type-variations carrying a font, not a stylebox). Kept
|
||||
## separate from ALL below, which is stylebox variations + their base type only.
|
||||
const HEADING := &"Heading"
|
||||
const ACCENT := &"Accent"
|
||||
const MONO := &"Mono"
|
||||
|
||||
## Every variation name + the base Control type it decorates. The builder and the
|
||||
## test both iterate this so they can never drift apart.
|
||||
const ALL := {
|
||||
|
||||
@@ -5,6 +5,7 @@ extends Control
|
||||
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:
|
||||
@@ -22,6 +23,7 @@ func _ready() -> void:
|
||||
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())
|
||||
@@ -30,7 +32,7 @@ func _ready() -> void:
|
||||
func _heading(text: String) -> Label:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.theme_type_variation = &"Heading"
|
||||
l.theme_type_variation = ThemeKeys.HEADING
|
||||
return l
|
||||
|
||||
|
||||
@@ -52,7 +54,7 @@ func _swatches() -> Control:
|
||||
col.add_child(r)
|
||||
var caption := Label.new()
|
||||
caption.text = entry[0]
|
||||
caption.theme_type_variation = &"Mono"
|
||||
caption.theme_type_variation = ThemeKeys.MONO
|
||||
col.add_child(caption)
|
||||
grid.add_child(col)
|
||||
return grid
|
||||
@@ -64,8 +66,12 @@ func _variations() -> Control:
|
||||
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
|
||||
var chip := Button.new(); chip.text = "INTIMIDATE · STR"; chip.theme_type_variation = ThemeKeys.CHIP
|
||||
for b in [cta, tab, tab_a, chip]:
|
||||
# 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)
|
||||
@@ -94,6 +100,40 @@ func _variations() -> Control:
|
||||
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).
|
||||
@@ -106,12 +146,12 @@ func _fonts() -> Control:
|
||||
|
||||
var accent := Label.new()
|
||||
accent.text = "Vexcca of the Ashmarch"
|
||||
accent.theme_type_variation = &"Accent"
|
||||
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 = &"Mono"
|
||||
mono.theme_type_variation = ThemeKeys.MONO
|
||||
col.add_child(mono)
|
||||
|
||||
return col
|
||||
|
||||
1
client/tests/unit/test_fonts.gd.uid
Normal file
1
client/tests/unit/test_fonts.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ck81t1k5xlvgu
|
||||
1
client/tests/unit/test_palette.gd.uid
Normal file
1
client/tests/unit/test_palette.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://w5blbshxn630
|
||||
@@ -2,6 +2,7 @@
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const ThemeKeys = preload("res://scripts/theme/theme_keys.gd")
|
||||
const Builder = preload("res://scripts/theme/build_game_theme.gd")
|
||||
const THEME_PATH := "res://assets/theme/game_theme.tres"
|
||||
|
||||
|
||||
@@ -27,3 +28,24 @@ func test_cta_has_normal_stylebox():
|
||||
func test_default_font_is_set():
|
||||
var t: Theme = load(THEME_PATH)
|
||||
assert_true(t.default_font is FontFile)
|
||||
|
||||
|
||||
func test_committed_tres_matches_builder():
|
||||
# Catches "palette/builder changed but nobody regenerated game_theme.tres."
|
||||
# Preloading the builder script and calling its static build_theme() does
|
||||
# NOT run _init() (that only fires on .new()), so this is side-effect-free.
|
||||
var fresh: Theme = Builder.build_theme()
|
||||
var committed: Theme = load(THEME_PATH)
|
||||
|
||||
var checks := [
|
||||
[ThemeKeys.PRIMARY_CTA, &"normal"],
|
||||
[ThemeKeys.PARCHMENT_CARD, &"panel"],
|
||||
[ThemeKeys.DARK_PANEL, &"panel"],
|
||||
]
|
||||
for pair in checks:
|
||||
var variation: StringName = pair[0]
|
||||
var state: StringName = pair[1]
|
||||
var fresh_box: StyleBoxFlat = fresh.get_stylebox(state, variation)
|
||||
var committed_box: StyleBoxFlat = committed.get_stylebox(state, variation)
|
||||
assert_eq(fresh_box.bg_color, committed_box.bg_color,
|
||||
"stale game_theme.tres — %s/%s bg_color drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
||||
|
||||
Reference in New Issue
Block a user