# 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 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 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))