From 90917075351336aac963b0019a0995e6840904ef Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 10 Jul 2026 20:29:22 -0500 Subject: [PATCH] feat(theme): game_theme.tres (fonts + stylebox variations) via builder --- client/assets/theme/game_theme.tres | 224 +++++++++++++++++++ client/scripts/theme/build_game_theme.gd | 115 ++++++++++ client/scripts/theme/build_game_theme.gd.uid | 1 + client/scripts/theme/theme_keys.gd | 30 +++ client/scripts/theme/theme_keys.gd.uid | 1 + client/tests/unit/test_theme_resource.gd | 29 +++ client/tests/unit/test_theme_resource.gd.uid | 1 + 7 files changed, 401 insertions(+) create mode 100644 client/assets/theme/game_theme.tres create mode 100644 client/scripts/theme/build_game_theme.gd create mode 100644 client/scripts/theme/build_game_theme.gd.uid create mode 100644 client/scripts/theme/theme_keys.gd create mode 100644 client/scripts/theme/theme_keys.gd.uid create mode 100644 client/tests/unit/test_theme_resource.gd create mode 100644 client/tests/unit/test_theme_resource.gd.uid diff --git a/client/assets/theme/game_theme.tres b/client/assets/theme/game_theme.tres new file mode 100644 index 0000000..9a1dfef --- /dev/null +++ b/client/assets/theme/game_theme.tres @@ -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") diff --git a/client/scripts/theme/build_game_theme.gd b/client/scripts/theme/build_game_theme.gd new file mode 100644 index 0000000..ec27bdf --- /dev/null +++ b/client/scripts/theme/build_game_theme.gd @@ -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)) diff --git a/client/scripts/theme/build_game_theme.gd.uid b/client/scripts/theme/build_game_theme.gd.uid new file mode 100644 index 0000000..c01e0a5 --- /dev/null +++ b/client/scripts/theme/build_game_theme.gd.uid @@ -0,0 +1 @@ +uid://drualc64s2x3p diff --git a/client/scripts/theme/theme_keys.gd b/client/scripts/theme/theme_keys.gd new file mode 100644 index 0000000..3ef0431 --- /dev/null +++ b/client/scripts/theme/theme_keys.gd @@ -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", +} diff --git a/client/scripts/theme/theme_keys.gd.uid b/client/scripts/theme/theme_keys.gd.uid new file mode 100644 index 0000000..e0f33e1 --- /dev/null +++ b/client/scripts/theme/theme_keys.gd.uid @@ -0,0 +1 @@ +uid://upoh5yt3dd4i diff --git a/client/tests/unit/test_theme_resource.gd b/client/tests/unit/test_theme_resource.gd new file mode 100644 index 0000000..525ab9a --- /dev/null +++ b/client/tests/unit/test_theme_resource.gd @@ -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) diff --git a/client/tests/unit/test_theme_resource.gd.uid b/client/tests/unit/test_theme_resource.gd.uid new file mode 100644 index 0000000..7a8e942 --- /dev/null +++ b/client/tests/unit/test_theme_resource.gd.uid @@ -0,0 +1 @@ +uid://bjwiotv7oid2t