feat(theme): game_theme.tres (fonts + stylebox variations) via builder

This commit is contained in:
2026-07-10 20:29:22 -05:00
parent 3981047ec6
commit 9091707535
7 changed files with 401 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
[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="StyleBoxFlat" id="StyleBoxFlat_eckwq"]
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_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.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_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.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_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.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_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.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_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 = 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_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.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_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.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_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.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="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.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_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.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
[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/normal = SubResource("StyleBoxFlat_eckwq")
DarkPanel/base_type = &"PanelContainer"
DarkPanel/styles/panel = SubResource("StyleBoxFlat_i0b23")
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")
ItemTileEmpty/base_type = &"PanelContainer"
ItemTileEmpty/styles/panel = SubResource("StyleBoxFlat_q5gpk")
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")
ParchmentInset/base_type = &"PanelContainer"
ParchmentInset/styles/panel = SubResource("StyleBoxFlat_73lme")
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")
Tab/base_type = &"Button"
Tab/colors/font_color = Color(0.41568628, 0.3529412, 0.22745098, 1)
Tab/styles/normal = SubResource("StyleBoxFlat_4kten")
TabActive/base_type = &"Button"
TabActive/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
TabActive/styles/normal = SubResource("StyleBoxFlat_p6wtm")

View File

@@ -0,0 +1,115 @@
# 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.
## §2: presentation.
const Palette = preload("res://scripts/theme/palette.gd")
const ThemeKeys = preload("res://scripts/theme/theme_keys.gd")
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 theme := Theme.new()
_fonts(theme)
_primary_cta(theme)
_tabs(theme)
_chip(theme)
_tiles(theme)
_cards_and_panels(theme)
var err := ResourceSaver.save(theme, OUT)
if err != OK:
push_error("theme save failed: %d" % err)
else:
print("wrote ", OUT)
quit()
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
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)
# 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)
# 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)
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))
theme.set_color(&"font_color", k, Palette.CREAM_BRIGHT)
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)
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_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:
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)
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,30 @@
# 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"
## 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,29 @@
# client/tests/unit/test_theme_resource.gd
extends "res://addons/gut/test.gd"
const ThemeKeys = preload("res://scripts/theme/theme_keys.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)

View File

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