saumwacht-godot/scripts/ui/theme/saumwacht_theme.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

247 lines
12 KiB
GDScript

class_name SaumwachtTheme
extends RefCounted
## Shared menu theme: basalt plates, bronze edges, Cinzel for titles, Alegreya for prose,
## Cantarell for controls. Components pick a look through theme_type_variation.
const NIGHT := Color("0b0f14")
const BASALT := Color("151a20")
const BASALT_DEEP := Color("0f1317")
const PARCHMENT := Color("efe6d2")
const MUTED := Color("a9a293")
const FAINT := Color("7d7466")
const BRONZE := Color("b7874a")
const BRONZE_LIGHT := Color("e4bd7c")
const BRONZE_DEEP := Color("6b4f2e")
const FOCUS := Color("f6e2b0")
const EMBER := Color("ff9a52")
const SPRING := Color("7fe0c4")
const INK := Color("1a120a")
const CINZEL := preload("res://assets/fonts/Cinzel-VF.ttf")
const ALEGREYA := preload("res://assets/fonts/Alegreya-VF.ttf")
const ALEGREYA_ITALIC := preload("res://assets/fonts/Alegreya-Italic-VF.ttf")
const CANTARELL := preload("res://assets/fonts/Cantarell-VF.otf")
const ForgedStyle = preload("res://scripts/ui/theme/forged_style.gd")
static var _shared: Theme
static func shared() -> Theme:
if _shared == null:
_shared = build()
return _shared
static func font(base: Font, weight: float, spacing: int = 0) -> FontVariation:
var variation := FontVariation.new()
variation.base_font = base
variation.variation_opentype = {"wght": weight}
variation.spacing_glyph = spacing
return variation
static func plate(top: Color, bottom: Color, edge: Color, edge_width: float = 1.5, cut: float = 7.0) -> StyleBox:
var style := ForgedStyle.new()
style.fill_top = top
style.fill_bottom = bottom
style.edge = edge
style.edge_width = edge_width
style.chamfer = cut
style.inner = Color(1, 1, 1, .045)
style.set_content_margin_all(12)
return style
static func focus_ring(cut: float = 7.0) -> StyleBox:
# Keyboard focus: a bright ring outside the control plus a soft halo, distinct from hover.
var style := ForgedStyle.new()
style.draw_fill = false
style.edge = FOCUS
style.edge_width = 2.5
style.inner = Color(FOCUS, .55)
style.inner_inset = 4.5
style.chamfer = cut + 4.0
style.grow = 4.0
style.shadow = Color(FOCUS, .22)
style.shadow_size = 5.0
return style
static func margins(style: StyleBox, horizontal: float, vertical: float) -> StyleBox:
style.content_margin_left = horizontal
style.content_margin_right = horizontal
style.content_margin_top = vertical
style.content_margin_bottom = vertical
return style
static func build() -> Theme:
var theme := Theme.new()
var ui := font(CANTARELL, 560.0)
theme.default_font = ui
theme.default_font_size = 17
var display := font(CINZEL, 700.0, 2)
var display_medium := font(CINZEL, 600.0, 1)
var caps := font(CANTARELL, 700.0, 2)
var prose := font(ALEGREYA, 480.0)
var prose_italic := font(ALEGREYA_ITALIC, 460.0)
for type: String in ["Label", "Button", "CheckButton", "OptionButton"]:
theme.set_color("font_color", type, PARCHMENT)
theme.set_color("font_shadow_color", "Label", Color(0, 0, 0, .55))
theme.set_constant("shadow_offset_y", "Label", 1)
theme.set_constant("shadow_outline_size", "Label", 2)
_label(theme, "TitleLabel", display, 78, Color("f4e6c4"))
theme.set_color("font_outline_color", "TitleLabel", Color(0, 0, 0, .5))
theme.set_constant("outline_size", "TitleLabel", 6)
_label(theme, "SubtitleLabel", font(CINZEL, 600.0, 7), 19, BRONZE_LIGHT)
_label(theme, "HeadingLabel", display_medium, 34, Color("f4e6c4"))
_label(theme, "SectionLabel", font(CINZEL, 700.0, 3), 16, BRONZE_LIGHT)
_label(theme, "EyebrowLabel", caps, 14, BRONZE_LIGHT)
_label(theme, "BodyLabel", prose, 20, PARCHMENT)
_label(theme, "LoreLabel", prose_italic, 21, Color("ddd3bd"))
_label(theme, "MutedLabel", ui, 16, MUTED)
_label(theme, "CardTitleLabel", display, 29, Color("f4e6c4"))
_label(theme, "CardFactLabel", ui, 17, Color("d8cfbc"))
_label(theme, "BadgeLabel", caps, 13, INK)
# Secondary buttons are the default look; primary and quiet buttons are variations.
var secondary := margins(plate(Color("1d232b"), Color("141920"), Color("6b5a44")), 20, 11)
var secondary_hover := margins(plate(Color("27303a"), Color("1a2028"), BRONZE_LIGHT), 20, 11)
var secondary_pressed := margins(plate(Color("10141a"), Color("171c23"), BRONZE, 2.0), 20, 11)
var secondary_disabled := margins(plate(Color("15191e"), Color("111418"), Color("3a352e")), 20, 11)
_button(theme, "Button", secondary, secondary_hover, secondary_pressed, secondary_disabled, focus_ring())
theme.set_color("font_hover_color", "Button", Color.WHITE)
theme.set_color("font_pressed_color", "Button", BRONZE_LIGHT)
theme.set_color("font_disabled_color", "Button", FAINT)
theme.set_color("font_focus_color", "Button", Color.WHITE)
var primary := margins(plate(Color("e8c47e"), Color("b98a4a"), Color("fff0cc"), 1.5, 9.0), 30, 15)
(primary as ForgedStyle).inner = Color(INK, .35)
(primary as ForgedStyle).highlight = Color(1, 1, 1, .45)
(primary as ForgedStyle).shadow = Color(0, 0, 0, .45)
(primary as ForgedStyle).shadow_size = 8.0
var primary_hover := margins(plate(Color("f6d898"), Color("cc9c56"), Color.WHITE, 1.5, 9.0), 30, 15)
(primary_hover as ForgedStyle).inner = Color(INK, .35)
(primary_hover as ForgedStyle).highlight = Color(1, 1, 1, .6)
(primary_hover as ForgedStyle).shadow = Color(.95, .7, .35, .35)
(primary_hover as ForgedStyle).shadow_size = 12.0
var primary_pressed := margins(plate(Color("b98a4a"), Color("d5aa66"), Color("fff0cc"), 2.0, 9.0), 30, 15)
(primary_pressed as ForgedStyle).inner = Color(INK, .45)
var primary_disabled := margins(plate(Color("5a4c38"), Color("463a2a"), Color("7d6a4c"), 1.5, 9.0), 30, 15)
_button(theme, "PrimaryButton", primary, primary_hover, primary_pressed, primary_disabled, focus_ring(9.0))
theme.set_type_variation("PrimaryButton", "Button")
theme.set_font("font", "PrimaryButton", display)
theme.set_font_size("font_size", "PrimaryButton", 22)
for state: String in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color", "font_hover_pressed_color"]:
theme.set_color(state, "PrimaryButton", INK)
theme.set_color("font_disabled_color", "PrimaryButton", Color("2a2218"))
var quiet := margins(StyleBoxEmpty.new(), 10, 7)
var quiet_hover := margins(plate(Color(1, 1, 1, .05), Color(1, 1, 1, .02), Color(0, 0, 0, 0), 0.0, 5.0), 10, 7)
_button(theme, "QuietButton", quiet, quiet_hover, quiet_hover, quiet, focus_ring(5.0))
theme.set_type_variation("QuietButton", "Button")
theme.set_color("font_color", "QuietButton", MUTED)
theme.set_color("font_hover_color", "QuietButton", PARCHMENT)
theme.set_font_size("font_size", "QuietButton", 16)
# Segmented choices: the pressed state is the selected option (toggle mode).
var choice := margins(plate(Color("171c23"), Color("12161b"), Color("463d31")), 16, 10)
var choice_hover := margins(plate(Color("212831"), Color("171c23"), BRONZE), 16, 10)
var choice_on := margins(plate(Color("2e2a22"), Color("1d1b17"), BRONZE_LIGHT, 2.0), 16, 10)
(choice_on as ForgedStyle).inner = Color(BRONZE_LIGHT, .35)
_button(theme, "ChoiceButton", choice, choice_hover, choice_on, choice, focus_ring())
theme.set_type_variation("ChoiceButton", "Button")
theme.set_stylebox("hover_pressed", "ChoiceButton", choice_on)
theme.set_color("font_color", "ChoiceButton", Color("cfc6b2"))
theme.set_color("font_pressed_color", "ChoiceButton", Color("fff4dc"))
theme.set_color("font_hover_pressed_color", "ChoiceButton", Color("fff4dc"))
theme.set_constant("h_separation", "ChoiceButton", 12)
theme.set_constant("icon_max_width", "ChoiceButton", 18)
# Faction cards: a heavier plate; selected adds a double bronze frame and rivets.
var card := margins(plate(Color("1a1f26"), Color("111419"), Color("4a4034"), 1.5, 12.0), 18, 16)
var card_hover := margins(plate(Color("222932"), Color("151a20"), BRONZE, 1.5, 12.0), 18, 16)
var card_on := margins(plate(Color("2a2720"), Color("16171a"), BRONZE_LIGHT, 2.5, 12.0), 18, 16)
(card_on as ForgedStyle).inner = Color(BRONZE_LIGHT, .55)
(card_on as ForgedStyle).inner_inset = 5.0
(card_on as ForgedStyle).ornaments = true
(card_on as ForgedStyle).ornament_color = BRONZE_LIGHT
(card_on as ForgedStyle).shadow = Color(0, 0, 0, .5)
(card_on as ForgedStyle).shadow_size = 10.0
_button(theme, "FactionCard", card, card_hover, card_on, card, focus_ring(12.0))
theme.set_type_variation("FactionCard", "Button")
theme.set_stylebox("hover_pressed", "FactionCard", card_on)
var panel := margins(plate(Color(.078, .094, .114, .95), Color(.055, .066, .08, .96), Color("5c4a33"), 1.5, 12.0), 30, 26)
(panel as ForgedStyle).ornaments = true
(panel as ForgedStyle).ornament_color = Color("8a6a3c")
(panel as ForgedStyle).shadow = Color(0, 0, 0, .45)
(panel as ForgedStyle).shadow_size = 18.0
theme.set_stylebox("panel", "MenuPanel", panel)
theme.set_type_variation("MenuPanel", "PanelContainer")
var caption := margins(plate(Color(.06, .075, .09, .82), Color(.05, .06, .075, .86), Color("4a4034"), 1.0, 6.0), 16, 9)
theme.set_stylebox("panel", "CaptionPanel", caption)
theme.set_type_variation("CaptionPanel", "PanelContainer")
var badge := margins(plate(BRONZE_LIGHT, BRONZE, Color("fff0cc"), 1.0, 4.0), 9, 3)
theme.set_stylebox("panel", "BadgePanel", badge)
theme.set_type_variation("BadgePanel", "PanelContainer")
theme.set_stylebox("panel", "PanelContainer", panel)
var on_icon: Texture2D = load("res://assets/ui/controls/toggle_on.svg")
var off_icon: Texture2D = load("res://assets/ui/controls/toggle_off.svg")
for type: String in ["CheckButton"]:
theme.set_icon("checked", type, on_icon)
theme.set_icon("unchecked", type, off_icon)
theme.set_icon("checked_disabled", type, on_icon)
theme.set_icon("unchecked_disabled", type, off_icon)
theme.set_stylebox("normal", type, margins(StyleBoxEmpty.new(), 6, 6))
theme.set_stylebox("hover", type, margins(plate(Color(1, 1, 1, .04), Color(1, 1, 1, .02), Color(0, 0, 0, 0), 0.0, 5.0), 6, 6))
theme.set_stylebox("pressed", type, margins(StyleBoxEmpty.new(), 6, 6))
theme.set_stylebox("hover_pressed", type, margins(plate(Color(1, 1, 1, .04), Color(1, 1, 1, .02), Color(0, 0, 0, 0), 0.0, 5.0), 6, 6))
theme.set_stylebox("focus", type, focus_ring(5.0))
theme.set_color("font_hover_color", type, Color.WHITE)
theme.set_color("font_pressed_color", type, PARCHMENT)
theme.set_color("font_hover_pressed_color", type, Color.WHITE)
theme.set_color("font_focus_color", type, Color.WHITE)
theme.set_constant("h_separation", type, 14)
var track := StyleBoxFlat.new()
track.bg_color = Color("0c0f13")
track.border_color = Color("5c4a33")
track.set_border_width_all(1)
track.content_margin_top = 3
track.content_margin_bottom = 3
theme.set_stylebox("slider", "HSlider", track)
var fill := StyleBoxFlat.new()
fill.bg_color = BRONZE
fill.content_margin_top = 3
fill.content_margin_bottom = 3
theme.set_stylebox("grabber_area", "HSlider", fill)
theme.set_stylebox("grabber_area_highlight", "HSlider", fill)
theme.set_icon("grabber", "HSlider", load("res://assets/ui/controls/choice_on.svg"))
theme.set_icon("grabber_highlight", "HSlider", load("res://assets/ui/controls/choice_on.svg"))
theme.set_stylebox("focus", "HSlider", focus_ring(4.0))
var rule := StyleBoxLine.new()
rule.color = Color("3a3226")
rule.thickness = 1
theme.set_stylebox("separator", "HSeparator", rule)
theme.set_constant("separation", "HSeparator", 14)
var scroll := StyleBoxFlat.new()
scroll.bg_color = Color(1, 1, 1, .03)
var grab := StyleBoxFlat.new()
grab.bg_color = Color("5c4a33")
for key: String in ["grabber", "grabber_highlight", "grabber_pressed"]:
theme.set_stylebox(key, "VScrollBar", grab)
theme.set_stylebox("scroll", "VScrollBar", scroll)
var tooltip := margins(plate(Color("1d232b"), Color("141920"), BRONZE), 12, 8)
theme.set_stylebox("panel", "TooltipPanel", tooltip)
theme.set_color("font_color", "TooltipLabel", PARCHMENT)
return theme
static func _label(theme: Theme, type: String, face: Font, size: int, color: Color) -> void:
theme.set_type_variation(type, "Label")
theme.set_font("font", type, face)
theme.set_font_size("font_size", type, size)
theme.set_color("font_color", type, color)
static func _button(theme: Theme, type: String, normal: StyleBox, hover: StyleBox, pressed: StyleBox, disabled: StyleBox, focus: StyleBox) -> void:
theme.set_stylebox("normal", type, normal)
theme.set_stylebox("hover", type, hover)
theme.set_stylebox("pressed", type, pressed)
theme.set_stylebox("disabled", type, disabled)
theme.set_stylebox("focus", type, focus)