199 lines
9.3 KiB
GDScript
199 lines
9.3 KiB
GDScript
# 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 SERIF_ITALIC := "res://assets/theme/fonts/EBGaramond-Italic-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)
|
|
_title_type(theme)
|
|
_rich_text(theme)
|
|
_primary_cta(theme)
|
|
_tabs(theme)
|
|
_dock_button(theme)
|
|
_parchment_button(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 _title_type(theme: Theme) -> void:
|
|
# Title-screen display type (mock Title Screen): the big serif logo + the mono
|
|
# kicker above it. Label font-role variations (font/size/colour), the same
|
|
# shape as Heading/Accent/Mono.
|
|
theme.set_type_variation(ThemeKeys.TITLE_LOGO, "Label")
|
|
theme.set_font(&"font", ThemeKeys.TITLE_LOGO, load(SERIF))
|
|
theme.set_font_size(&"font_size", ThemeKeys.TITLE_LOGO, 116)
|
|
theme.set_color(&"font_color", ThemeKeys.TITLE_LOGO, Palette.CREAM)
|
|
theme.set_type_variation(ThemeKeys.TITLE_KICKER, "Label")
|
|
theme.set_font(&"font", ThemeKeys.TITLE_KICKER, load(MONO))
|
|
theme.set_font_size(&"font_size", ThemeKeys.TITLE_KICKER, 13)
|
|
theme.set_color(&"font_color", ThemeKeys.TITLE_KICKER, Palette.GOLD)
|
|
|
|
|
|
static func _rich_text(theme: Theme) -> void:
|
|
# Base RichTextLabel styling — the narrative "prose" voice (mock README §Typography:
|
|
# Georgia/serif for DM/flavor body, italic for emphasis). RichTextLabel does NOT
|
|
# inherit default_font/default_color, so without this its text renders in the engine
|
|
# default (near-white) and is unreadable on parchment. Every parchment prose surface
|
|
# (the narration book, later quest briefs/dialogue) reads dark serif ink from here.
|
|
theme.set_font(&"normal_font", "RichTextLabel", load(SERIF))
|
|
theme.set_font(&"italics_font", "RichTextLabel", load(SERIF_ITALIC))
|
|
theme.set_font_size(&"normal_font_size", "RichTextLabel", 20)
|
|
theme.set_color(&"default_color", "RichTextLabel", Palette.INK_BODY)
|
|
|
|
|
|
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 _dock_button(theme: Theme) -> void:
|
|
# Dark nav button for the slide-in system dock (mock 2a). Button-base so
|
|
# normal/hover/pressed styleboxes actually apply (a PanelContainer variation's
|
|
# lone `panel` stylebox does not skin a Button). Colours are dock-dark.
|
|
var k := ThemeKeys.DOCK_BUTTON
|
|
theme.set_type_variation(k, "Button")
|
|
theme.set_stylebox(&"normal", k, _flat(Palette.DARK_PANEL_70, Palette.BORDER_1, 1, 8))
|
|
theme.set_stylebox(&"hover", k, _flat(Palette.DARK_PANEL_85, Palette.GOLD, 1, 8))
|
|
theme.set_stylebox(&"pressed", k, _flat(Palette.DARK_PANEL_90, Palette.GOLD, 1, 8))
|
|
theme.set_stylebox(&"focus", k, StyleBoxEmpty.new())
|
|
theme.set_color(&"font_color", k, Palette.CREAM_SECONDARY)
|
|
|
|
|
|
static func _parchment_button(theme: Theme) -> void:
|
|
# Clickable parchment row/card for the narration book (mock 2a response
|
|
# choices + the refire affordance). Button-base so normal/hover/pressed/
|
|
# disabled styleboxes actually apply (a PanelContainer variation's lone
|
|
# `panel` stylebox does not skin a Button). Colours are the mock's choice-card.
|
|
var k := ThemeKeys.PARCHMENT_BUTTON
|
|
theme.set_type_variation(k, "Button")
|
|
theme.set_stylebox(&"normal", k, _flat(Palette.PARCHMENT_CARD, Palette.PARCHMENT_BORDER_3, 1, 8))
|
|
theme.set_stylebox(&"hover", k, _flat(Palette.PARCHMENT_INSET, Palette.INK_LABEL, 1, 8))
|
|
theme.set_stylebox(&"pressed", k, _flat(Palette.PARCHMENT_INSET_2, Palette.INK_LABEL, 1, 8))
|
|
theme.set_stylebox(&"disabled", k, _flat(Palette.PARCHMENT_INSET_2, Palette.PARCHMENT_BORDER_3, 1, 8))
|
|
theme.set_stylebox(&"focus", k, StyleBoxEmpty.new())
|
|
theme.set_color(&"font_color", k, Palette.INK_BODY)
|
|
theme.set_color(&"font_disabled_color", k, Palette.INK_MUTED)
|
|
|
|
|
|
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))
|