diff --git a/client/assets/theme/game_theme.tres b/client/assets/theme/game_theme.tres index 9a1dfef..8eef967 100644 --- a/client/assets/theme/game_theme.tres +++ b/client/assets/theme/game_theme.tres @@ -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") diff --git a/client/assets/theme/shaders/dark_bay.gdshader b/client/assets/theme/shaders/dark_bay.gdshader index ff69961..94c36fb 100644 --- a/client/assets/theme/shaders/dark_bay.gdshader +++ b/client/assets/theme/shaders/dark_bay.gdshader @@ -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; diff --git a/client/scripts/theme/build_game_theme.gd b/client/scripts/theme/build_game_theme.gd index ec27bdf..091dbf0 100644 --- a/client/scripts/theme/build_game_theme.gd +++ b/client/scripts/theme/build_game_theme.gd @@ -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") diff --git a/client/scripts/theme/palette.gd.uid b/client/scripts/theme/palette.gd.uid new file mode 100644 index 0000000..c635ce2 --- /dev/null +++ b/client/scripts/theme/palette.gd.uid @@ -0,0 +1 @@ +uid://d4bext4g7reql diff --git a/client/scripts/theme/surfaces/dark_bay.gd b/client/scripts/theme/surfaces/dark_bay.gd index bf74224..ef08178 100644 --- a/client/scripts/theme/surfaces/dark_bay.gd +++ b/client/scripts/theme/surfaces/dark_bay.gd @@ -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") diff --git a/client/scripts/theme/surfaces/parchment_panel.gd b/client/scripts/theme/surfaces/parchment_panel.gd index 0db054e..11a07f5 100644 --- a/client/scripts/theme/surfaces/parchment_panel.gd +++ b/client/scripts/theme/surfaces/parchment_panel.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") diff --git a/client/scripts/theme/surfaces/vignette_overlay.gd b/client/scripts/theme/surfaces/vignette_overlay.gd index 9fcbcf3..ea172c8 100644 --- a/client/scripts/theme/surfaces/vignette_overlay.gd +++ b/client/scripts/theme/surfaces/vignette_overlay.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: diff --git a/client/scripts/theme/theme_keys.gd b/client/scripts/theme/theme_keys.gd index 3ef0431..0a11113 100644 --- a/client/scripts/theme/theme_keys.gd +++ b/client/scripts/theme/theme_keys.gd @@ -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 := { diff --git a/client/scripts/theme/theme_showcase.gd b/client/scripts/theme/theme_showcase.gd index fed6637..0eb0878 100644 --- a/client/scripts/theme/theme_showcase.gd +++ b/client/scripts/theme/theme_showcase.gd @@ -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 diff --git a/client/tests/unit/test_fonts.gd.uid b/client/tests/unit/test_fonts.gd.uid new file mode 100644 index 0000000..5a1e3a5 --- /dev/null +++ b/client/tests/unit/test_fonts.gd.uid @@ -0,0 +1 @@ +uid://ck81t1k5xlvgu diff --git a/client/tests/unit/test_palette.gd.uid b/client/tests/unit/test_palette.gd.uid new file mode 100644 index 0000000..484d2a2 --- /dev/null +++ b/client/tests/unit/test_palette.gd.uid @@ -0,0 +1 @@ +uid://w5blbshxn630 diff --git a/client/tests/unit/test_theme_resource.gd b/client/tests/unit/test_theme_resource.gd index 525ab9a..2d555ad 100644 --- a/client/tests/unit/test_theme_resource.gd +++ b/client/tests/unit/test_theme_resource.gd @@ -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])