Merge feature/shared-theme into dev

M3-a shared Theme: palette tokens (single colour source), bundled OFL fonts,
builder-generated game_theme.tres, three shader surfaces, and the showcase
eyeball proof. Whole-branch review passed; human eyeball confirmed; 125/125.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 23:15:17 -05:00
44 changed files with 1236 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://dgftorjwjkcx6"
path="res://.godot/imported/ArchitectsDaughter-Regular.ttf-39fe9a11a65cddcd9ceecf51a839f0c5.fontdata"
[deps]
source_file="res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf"
dest_files=["res://.godot/imported/ArchitectsDaughter-Regular.ttf-39fe9a11a65cddcd9ceecf51a839f0c5.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://bosx65t7inqbs"
path="res://.godot/imported/EBGaramond-Italic-VariableFont_wght.ttf-8a9f89c45d1261f74ac5e087dfe5ec85.fontdata"
[deps]
source_file="res://assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf"
dest_files=["res://.godot/imported/EBGaramond-Italic-VariableFont_wght.ttf-8a9f89c45d1261f74ac5e087dfe5ec85.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://b00hfuuyxctc6"
path="res://.godot/imported/EBGaramond-VariableFont_wght.ttf-32cc8b7204b14062fe5657f6ee3ae93b.fontdata"
[deps]
source_file="res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf"
dest_files=["res://.godot/imported/EBGaramond-VariableFont_wght.ttf-32cc8b7204b14062fe5657f6ee3ae93b.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://1ge2menmepj8"
path="res://.godot/imported/JetBrainsMono-VariableFont_wght.ttf-9b8836092a0b124cd7b65994f8da4002.fontdata"
[deps]
source_file="res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf"
dest_files=["res://.godot/imported/JetBrainsMono-VariableFont_wght.ttf-9b8836092a0b124cd7b65994f8da4002.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,353 @@
[gd_resource type="Theme" format=3]
[ext_resource type="FontFile" path="res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf" id="1_cu4xe"]
[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="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
content_margin_bottom = 6.0
bg_color = Color(0.6039216, 0.56078434, 0.47058824, 0.12)
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_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
content_margin_bottom = 6.0
bg_color = Color(0.07058824, 0.05882353, 0.043137256, 0.7)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.22745098, 0.20784314, 0.18039216, 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_73lme"]
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 = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.7490196, 0.65882355, 0.47058824, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_js3tr"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
content_margin_bottom = 6.0
bg_color = Color(0.8509804, 0.7882353, 0.63529414, 1)
draw_center = false
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.6627451, 0.5803922, 0.39215687, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ube3r"]
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.7490196, 0.65882355, 0.47058824, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2tooq"]
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.6627451, 0.5803922, 0.39215687, 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_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
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.56078434, 0.22745098, 0.20392157, 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_0fjdt"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
content_margin_bottom = 6.0
bg_color = Color(0.56078434, 0.22745098, 0.20392157, 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 = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ns3kg"]
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 = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[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
content_margin_bottom = 6.0
bg_color = Color(0.9372549, 0.9019608, 0.8117647, 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_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
content_margin_bottom = 6.0
bg_color = Color(0.56078434, 0.22745098, 0.20392157, 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
[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
Accent/base_type = &"Label"
Accent/colors/font_color = Color(0.56078434, 0.41568628, 0.16470589, 1)
Accent/font_sizes/font_size = 24
Accent/fonts/font = ExtResource("1_cu4xe")
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/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_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_73lme")
ItemTileEmpty/base_type = &"PanelContainer"
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_ube3r")
ParchmentInset/base_type = &"PanelContainer"
ParchmentInset/styles/panel = SubResource("StyleBoxFlat_2tooq")
PrimaryCTA/base_type = &"Button"
PrimaryCTA/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
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/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/focus = SubResource("StyleBoxEmpty_nmowx")
TabActive/styles/hover = SubResource("StyleBoxFlat_oi067")
TabActive/styles/normal = SubResource("StyleBoxFlat_ukblj")
TabActive/styles/pressed = SubResource("StyleBoxFlat_utt73")

View File

@@ -0,0 +1,21 @@
shader_type canvas_item;
uniform vec4 stripe_a : source_color;
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;
float band = mod(floor(diag / band_px), 2.0);
vec4 base = mix(stripe_a, stripe_b, band);
// radial vignette centred slightly high (README: 50% 38%).
float d = distance(UV, vec2(0.5, 0.38));
base.rgb *= 1.0 - clamp(d * vignette, 0.0, 1.0);
COLOR = base;
}

View File

@@ -0,0 +1 @@
uid://jb8clyworwr1

View File

@@ -0,0 +1,6 @@
shader_type canvas_item;
uniform vec4 top : source_color;
uniform vec4 bottom : source_color;
void fragment() {
COLOR = mix(top, bottom, UV.y);
}

View File

@@ -0,0 +1 @@
uid://b2heb6jhvwxxl

View File

@@ -0,0 +1,6 @@
shader_type canvas_item;
uniform float strength = 0.62;
void fragment() {
float d = distance(UV, vec2(0.5));
COLOR = vec4(0.0, 0.0, 0.0, clamp(d * strength, 0.0, 1.0));
}

View File

@@ -0,0 +1 @@
uid://b4tami4peyxaw

View File

@@ -13,3 +13,15 @@ config_version=5
config/name="coc-rpg"
config/description="AI-driven single-player party RPG. Code owns state. AI owns text."
config/features=PackedStringArray("4.7")
[display]
; Design/logical canvas stays 1920x1080 (charter §16, mockups authored here).
window/size/viewport_width=1920
window/size/viewport_height=1080
; Open a smaller 1600x900 window that scales the 1920x1080 canvas down, so it
; doesn't fill the whole screen. canvas_items + keep aspect does the scaling.
window/size/window_width_override=1600
window/size/window_height_override=900
window/stretch/mode="canvas_items"
window/stretch/aspect="keep"

View File

@@ -0,0 +1,5 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/theme/surfaces/dark_bay.gd" id="1"]
[node name="DarkBay" type="Panel"]
anchors_preset = 15
script = ExtResource("1")

View File

@@ -0,0 +1,5 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/theme/surfaces/parchment_panel.gd" id="1"]
[node name="ParchmentPanel" type="Panel"]
anchors_preset = 15
script = ExtResource("1")

View File

@@ -0,0 +1,5 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/theme/surfaces/vignette_overlay.gd" id="1"]
[node name="VignetteOverlay" type="ColorRect"]
anchors_preset = 15
script = ExtResource("1")

View File

@@ -0,0 +1,5 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/theme/theme_showcase.gd" id="1"]
[node name="ThemeShowcase" type="Control"]
anchors_preset = 15
script = ExtResource("1")

View File

@@ -0,0 +1,138 @@
# client/scripts/theme/build_game_theme.gd
@tool
extends SceneTree
## One-shot Theme builder. Run headless:
## 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 OUT := "res://assets/theme/game_theme.tres"
const SERIF := "res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf"
const ACCENT := "res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf"
const MONO := "res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf"
func _init() -> void:
var t := build_theme()
var err := ResourceSaver.save(t, OUT)
if err != OK:
push_error("theme save failed: %d" % err)
else:
print("wrote ", OUT)
quit()
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
s.set_border_width_all(width)
s.set_corner_radius_all(radius)
s.content_margin_left = 12
s.content_margin_right = 12
s.content_margin_top = 6
s.content_margin_bottom = 6
return s
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(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(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(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)
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)
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)
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")
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)
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)
var empty := _flat(Palette.PARCHMENT_INSET_2, Palette.PARCHMENT_BORDER_3, 1, 6)
empty.draw_center = false
theme.set_type_variation(ThemeKeys.ITEM_TILE_EMPTY, "PanelContainer")
theme.set_stylebox(&"panel", ThemeKeys.ITEM_TILE_EMPTY, empty)
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")
theme.set_stylebox(&"panel", ThemeKeys.PARCHMENT_INSET, _flat(Palette.PARCHMENT_INSET, Palette.PARCHMENT_BORDER_3, 1, 4))
theme.set_type_variation(ThemeKeys.DARK_PANEL, "PanelContainer")
theme.set_stylebox(&"panel", ThemeKeys.DARK_PANEL, _flat(Palette.DARK_PANEL_70, Palette.BORDER_4, 1, 4))

View File

@@ -0,0 +1 @@
uid://drualc64s2x3p

View File

@@ -0,0 +1,89 @@
class_name Palette
extends RefCounted
## The single source of truth for every colour token (mockups README, "Color
## tokens"). §2: presentation — owns no state. No hex literal may live anywhere
## else in the client; every script, shader, and the Theme builder reads here.
# --- Dark backgrounds ---
const STAGE_1 := Color("0a0806")
const STAGE_2 := Color("0f0d0b")
const BAY_STRIPE_A := Color("26221c")
const BAY_STRIPE_B := Color("221e18")
const DARK_PANEL_70 := Color("120f0b", 0.7)
const DARK_PANEL_85 := Color("0e0b09", 0.85)
const DARK_PANEL_90 := Color("0a0806", 0.9)
# border ladder, light -> dark
const BORDER_1 := Color("6b6152")
const BORDER_2 := Color("5a5348")
const BORDER_3 := Color("4a4238")
const BORDER_4 := Color("3a352e")
const BORDER_5 := Color("1a1512")
# --- Parchment ---
const SHEET_TOP := Color("ece2ca")
const SHEET_BOTTOM := Color("e4d8bd")
const LOG_TOP := Color("e7dcc4")
const LOG_BOTTOM := Color("ddd0b2")
const PARCHMENT_CARD := Color("f1e8d2")
const PARCHMENT_INSET := Color("e6d9b8")
const PARCHMENT_INSET_2 := Color("d9c9a2")
const PARCHMENT_BORDER := Color("bfa878")
const PARCHMENT_BORDER_2 := Color("cbb684")
const PARCHMENT_BORDER_3 := Color("a99464")
const TAB_INACTIVE_BG := Color("efe6cf")
# --- Ink (text on parchment) ---
const INK_HEADING := Color("3a2f1c")
const INK_HEADING_2 := Color("4a3520")
const INK_BODY := Color("4a3f2c")
const INK_MUTED := Color("6a5a3a")
const INK_LABEL := Color("5a4326")
const INK_GOLD := Color("8f6a2a")
# --- Text on dark ---
const CREAM := Color("e8ddc8")
const CREAM_BRIGHT := Color("f0e6d0")
const CREAM_SECONDARY := Color("c9bda3")
const MUTED_MONO := Color("9a8f78")
const MUTED_MONO_DIM := Color("7a7060")
# --- Accents (muted for grit) ---
const BLOOD := Color("8f3a34") # core CTA / danger / selection / enemies
const BLOOD_BRIGHT := Color("b8524a") # hover
const BLOOD_DARK := Color("7a2f29") # CTA gradient bottom
const GOLD := Color("a9843f") # highlight / currency / current-turn
const GOLD_BRIGHT := Color("c8963f")
const STEEL := Color("5f8a9a") # allies / mana
const STEEL_BLUE := Color("4f7ab8")
const STEEL_DEEP := Color("243f6f")
const GREEN := Color("5f8a4f") # stamina / success / cleared
# --- Stat bars ---
const HP_A := Color("b8524a")
const HP_B := Color("8f3a34")
const MP_A := Color("4f7ab8")
const MP_B := Color("243f6f")
const STAMINA := Color("8f9a4f")
const BAR_TRACK := Color("2a251f")
const BAR_BORDER := Color("4a4238")
# --- Rarity (ordered common..legendary) ---
const RARITY: Array[Color] = [
Color("8a8378"), # 0 common
Color("5f8a4f"), # 1 uncommon
Color("4f7ab8"), # 2 rare
Color("8a5fb8"), # 3 epic
Color("c8963f"), # 4 legendary
]
static func rarity_color(rarity: int) -> Color:
return RARITY[clampi(rarity, 0, RARITY.size() - 1)]
static func stat_bar(kind: StringName) -> Array:
match kind:
&"hp": return [HP_A, HP_B]
&"mp": return [MP_A, MP_B]
&"stamina": return [STAMINA, STAMINA]
_: return [MUTED_MONO, MUTED_MONO_DIM]

View File

@@ -0,0 +1 @@
uid://d4bext4g7reql

View File

@@ -0,0 +1,14 @@
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.
func _ready() -> void:
var m := ShaderMaterial.new()
m.shader = load("res://assets/theme/shaders/dark_bay.gdshader")
m.set_shader_parameter("stripe_a", Palette.BAY_STRIPE_A)
m.set_shader_parameter("stripe_b", Palette.BAY_STRIPE_B)
material = m

View File

@@ -0,0 +1 @@
uid://24c8pl0yx8wu

View File

@@ -0,0 +1,14 @@
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.
func _ready() -> void:
var m := ShaderMaterial.new()
m.shader = load("res://assets/theme/shaders/parchment.gdshader")
m.set_shader_parameter("top", Palette.SHEET_TOP)
m.set_shader_parameter("bottom", Palette.SHEET_BOTTOM)
material = m

View File

@@ -0,0 +1 @@
uid://cnv1r00ane2la

View File

@@ -0,0 +1,16 @@
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:
color = Color(0, 0, 0, 0) # base transparent; shader draws the dim
mouse_filter = Control.MOUSE_FILTER_IGNORE
var m := ShaderMaterial.new()
m.shader = load("res://assets/theme/shaders/vignette.gdshader")
material = m

View File

@@ -0,0 +1 @@
uid://7bc7agrl6oi0

View File

@@ -0,0 +1,36 @@
# client/scripts/theme/theme_keys.gd
class_name ThemeKeys
extends RefCounted
## Type-variation names for game_theme.tres. Scripts set `theme_type_variation`
## to one of these so a typo is a compile-time symbol error, not a silent miss.
## §2: presentation.
const PRIMARY_CTA := &"PrimaryCTA"
const TAB := &"Tab"
const TAB_ACTIVE := &"TabActive"
const CHIP := &"Chip"
const ITEM_TILE := &"ItemTile"
const ITEM_TILE_EMPTY := &"ItemTileEmpty"
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 := {
PRIMARY_CTA: "Button",
TAB: "Button",
TAB_ACTIVE: "Button",
CHIP: "Button",
ITEM_TILE: "PanelContainer",
ITEM_TILE_EMPTY: "PanelContainer",
PARCHMENT_CARD: "PanelContainer",
PARCHMENT_INSET: "PanelContainer",
DARK_PANEL: "PanelContainer",
}

View File

@@ -0,0 +1 @@
uid://upoh5yt3dd4i

View File

@@ -0,0 +1,214 @@
# 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 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)
# A root Control run directly (F6) is not reliably sized to the viewport by
# full-rect anchors — it collapses to (0,0), clipping the ScrollContainer and
# hiding everything. Size to the viewport explicitly (no full-rect anchors here:
# they conflict with setting `size` directly) and track resizes.
_fit_to_viewport()
get_viewport().size_changed.connect(_fit_to_viewport)
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 _fit_to_viewport() -> void:
size = get_viewport_rect().size
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

View File

@@ -0,0 +1 @@
uid://ciqf5caopnsh3

View File

@@ -0,0 +1,17 @@
# client/tests/unit/test_fonts.gd
extends "res://addons/gut/test.gd"
const FONTS := {
"serif": "res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf",
"serif_italic": "res://assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf",
"accent": "res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf",
"mono": "res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf",
}
func test_all_fonts_exist_and_load_as_fontfile():
for key in FONTS:
var path: String = FONTS[key]
assert_true(FileAccess.file_exists(path), "missing font: %s" % path)
var f = load(path)
assert_true(f is FontFile, "%s did not load as FontFile" % key)

View File

@@ -0,0 +1 @@
uid://ck81t1k5xlvgu

View File

@@ -0,0 +1,43 @@
extends "res://addons/gut/test.gd"
func test_core_tokens_are_colors():
assert_true(Palette.BLOOD is Color)
assert_true(Palette.GOLD is Color)
assert_true(Palette.PARCHMENT_CARD is Color)
assert_true(Palette.CREAM is Color)
func test_blood_core_matches_readme_hex():
# #8f3a34 — the primary CTA / danger / selection red.
assert_eq(Palette.BLOOD, Color("8f3a34"))
func test_dark_panel_is_translucent():
assert_almost_eq(Palette.DARK_PANEL_70.a, 0.7, 0.01)
func test_rarity_ladder_has_five_ordered_colors():
assert_eq(Palette.RARITY.size(), 5)
assert_eq(Palette.rarity_color(0), Color("8a8378")) # common
assert_eq(Palette.rarity_color(4), Color("c8963f")) # legendary
func test_rarity_color_clamps_out_of_range():
# Out-of-range must NOT index-error (would emit an engine error -> fail).
assert_eq(Palette.rarity_color(-3), Palette.RARITY[0])
assert_eq(Palette.rarity_color(99), Palette.RARITY[4])
func test_stat_bar_returns_from_to_pair():
var hp = Palette.stat_bar(&"hp")
assert_eq(hp.size(), 2)
assert_eq(hp[0], Color("b8524a"))
assert_eq(hp[1], Color("8f3a34"))
func test_stat_bar_unknown_kind_is_safe():
# Unknown kind returns a grey pair, never null / error.
var pair = Palette.stat_bar(&"nonsense")
assert_eq(pair.size(), 2)
assert_true(pair[0] is Color)

View File

@@ -0,0 +1 @@
uid://w5blbshxn630

View File

@@ -0,0 +1,17 @@
extends "res://addons/gut/test.gd"
const SCENES := [
"res://scenes/theme/surfaces/ParchmentPanel.tscn",
"res://scenes/theme/surfaces/DarkBay.tscn",
"res://scenes/theme/surfaces/VignetteOverlay.tscn",
]
func test_surfaces_instance_with_shader_material():
for path in SCENES:
var packed = load(path)
assert_true(packed is PackedScene, "not a scene: %s" % path)
var node = packed.instantiate()
add_child_autofree(node) # triggers _ready()
assert_true(node.material is ShaderMaterial, "%s has no ShaderMaterial" % path)
assert_true(node.material.shader is Shader, "%s shader missing" % path)

View File

@@ -0,0 +1 @@
uid://csc3r7lqoe83d

View File

@@ -0,0 +1,50 @@
# client/tests/unit/test_theme_resource.gd
extends "res://addons/gut/test.gd"
const Builder = preload("res://scripts/theme/build_game_theme.gd")
const THEME_PATH := "res://assets/theme/game_theme.tres"
func test_theme_loads():
assert_true(FileAccess.file_exists(THEME_PATH), "theme not built — run build_game_theme.gd")
var t = load(THEME_PATH)
assert_true(t is Theme)
func test_every_variation_resolves():
var t: Theme = load(THEME_PATH)
for key in ThemeKeys.ALL:
var base: String = ThemeKeys.ALL[key]
assert_eq(t.get_type_variation_base(key), StringName(base),
"variation %s missing or wrong base" % key)
func test_cta_has_normal_stylebox():
var t: Theme = load(THEME_PATH)
assert_true(t.has_stylebox(&"normal", ThemeKeys.PRIMARY_CTA))
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])

View File

@@ -0,0 +1 @@
uid://bjwiotv7oid2t

View File

@@ -0,0 +1,11 @@
# client/tests/unit/test_theme_showcase.gd
extends "res://addons/gut/test.gd"
func test_showcase_instances_without_error():
var packed = load("res://scenes/theme/theme_showcase.tscn")
assert_true(packed is PackedScene)
var node = packed.instantiate()
add_child_autofree(node) # runs _ready(); any engine error here fails the suite
assert_true(node is Control)
assert_true(node.theme is Theme, "showcase did not apply game_theme")

View File

@@ -0,0 +1 @@
uid://e4p6b65e14mw