saumwacht-godot/scripts/luneraria/ui/theme/lune_ui_theme.gd
Martin Stickelbroeck 1b540f2a55 G3 (Zwischenstand): neues Ingame-HUD; Projekt wechselt nach Unity
Nicht abgenommen: Nach dem ersten Spieltest wurde entschieden, Champions of
Luneraria nach Unity 6 zu portieren. Dieser Stand ist die Referenz für das
HUD im Unity-Projekt (../luneraria-unity).

- HUD neu aus Komponenten mit eigenem Theme, Porträts aus den echten Modellen
  (einmal gerendert, zwischengespeichert), Lane-Übersicht mit deterministischer
  Bedrohung aus öffentlichem Zustand, Bossgrotte, nächste Welle, Ereignisse,
  Minimap mit zusammengefassten Armeen, Held/Items/Talente, Einheitendetails,
  Angriffschampions, Bereit-Sperre, UI-Größe, Team-Formen statt nur Farben
- Privatsphäre über LuneHudModel: fremdes Gold und fremde Entwürfe erscheinen nie
- Keine Regeländerung: Regeln 91/91, M2 73/73, Netz 43/43; verify_g3 715/715;
  verify_m1 38/38 und verify_m2 14/14 mit angepassten UI-Treibern
- Spieltest bis Welle 8 über die echte Oberfläche, Befunde in G3-HUD.md
- HELDEN-KONZEPT.md: vom Projektinhaber vorgegebene Helden (nicht umgesetzt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:55 +02:00

282 lines
14 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name LuneUiTheme
extends RefCounted
## Champions of Luneraria UI theme. It extends the Saumwacht menu theme (fonts, buttons,
## focus rings, ForgedStyle plates) with the in-match look: dark blue-anthracite plates,
## bronze and gold only on what the player can act on, quiet team accents. HUD components
## pick their look through theme_type_variation; shared styles are built once and cached,
## never copied per control.
const SLATE_TOP := Color(.075, .1, .135, .9)
const SLATE_BOTTOM := Color(.05, .068, .094, .93)
const EDGE := Color("2b3848")
const EDGE_LIGHT := Color("455870")
const TEXT := Color("ebe5d8")
const BRIGHT := Color("f6eedc")
const MUTED := Color("9aa4b0")
const FAINT := Color("6a7480")
const GOLD := Color("f2c96b")
const BRONZE := SaumwachtTheme.BRONZE
const BRONZE_LIGHT := SaumwachtTheme.BRONZE_LIGHT
const BRONZE_DEEP := SaumwachtTheme.BRONZE_DEEP
## Team accents: Blau (cool, crescent moon), Rot (warm, sun disc), neutral grotto (violet).
const TEAM: Array[Color] = [Color("6cb4ff"), Color("ff7b5e"), Color("b78cff")]
const TEAM_DARK: Array[Color] = [Color("1c3f6a"), Color("722a1f"), Color("45305f")]
const TEAM_NAMES: Array[String] = ["Blau", "Rot", "Grotte"]
const TEAM_EMBLEMS: Array[String] = ["Mondsichel", "Sonnenscheibe", "Grotte"]
## Threat levels RUHIG, UMKÄMPFT, UNTER DRUCK, KRITISCH.
const THREAT: Array[Color] = [Color("7cc9a6"), Color("e5c35e"), Color("ff9448"), Color("ff584a")]
const CHAMPION := Color("e2a15e")
const HERO := Color("f2c96b")
const GOOD := Color("8fdcb4")
const WARN := Color("f0c060")
const DANGER := Color("ff6a55")
const INFO := Color("a9c6e8")
## Lower bound for any HUD text: 14 logical px stay above 10 px at 1280×720.
const MIN_FONT: int = 14
static var _theme: Theme
static var _styles: Dictionary = {}
static func shared() -> Theme:
if _theme == null:
_theme = build()
return _theme
static func font_ui(weight: float = 560.0, spacing: int = 0) -> FontVariation:
return SaumwachtTheme.font(SaumwachtTheme.CANTARELL, weight, spacing)
static func font_display(weight: float = 700.0, spacing: int = 1) -> FontVariation:
return SaumwachtTheme.font(SaumwachtTheme.CINZEL, weight, spacing)
static func plate(top: Color, bottom: Color, edge: Color, edge_width: float = 1.0, cut: float = 6.0, horizontal: float = 12.0, vertical: float = 10.0) -> StyleBox:
var style: StyleBox = SaumwachtTheme.plate(top, bottom, edge, edge_width, cut)
(style as ForgedStyle).inner = Color(1, 1, 1, .035)
(style as ForgedStyle).inner_inset = 3.0
return SaumwachtTheme.margins(style, horizontal, vertical)
static func build() -> Theme:
var theme: Theme = SaumwachtTheme.build()
var ui := font_ui()
var ui_bold := font_ui(720.0)
var caps := font_ui(700.0, 2)
var display := font_display(700.0, 1)
_label(theme, "HudText", ui, 15, TEXT)
_label(theme, "HudMuted", ui, 14, MUTED)
_label(theme, "HudCaps", caps, 14, BRONZE_LIGHT)
_label(theme, "HudBadge", font_ui(720.0, 1), 14, TEXT)
_label(theme, "HudLane", font_display(700.0, 1), 16, Color("f4e6c4"))
_label(theme, "HudValue", ui_bold, 17, BRIGHT)
_label(theme, "HudGold", ui_bold, 24, GOLD)
_label(theme, "HudName", display, 18, Color("f4e6c4"))
_label(theme, "HudTitle", display, 24, Color("f4e6c4"))
_label(theme, "HudWave", font_display(700.0, 3), 30, Color("f4e6c4"))
for type: String in ["HudWave", "HudTitle", "HudGold"]:
theme.set_color("font_outline_color", type, Color(0, 0, 0, .55))
theme.set_constant("outline_size", type, 4)
# Panels: one quiet plate for all regions, a heavier one for modals and the tooltip.
theme.set_stylebox("panel", "HudPanel", plate(SLATE_TOP, SLATE_BOTTOM, EDGE, 1.0, 6.0, 12, 10))
theme.set_type_variation("HudPanel", "PanelContainer")
theme.set_stylebox("panel", "HudPanelTight", plate(SLATE_TOP, SLATE_BOTTOM, EDGE, 1.0, 5.0, 8, 6))
theme.set_type_variation("HudPanelTight", "PanelContainer")
var inset := StyleBoxFlat.new()
inset.bg_color = Color(0, 0, 0, .28)
inset.set_corner_radius_all(4)
SaumwachtTheme.margins(inset, 8, 6)
theme.set_stylebox("panel", "HudInset", inset)
theme.set_type_variation("HudInset", "PanelContainer")
var modal := plate(Color(.07, .09, .125, .98), Color(.045, .06, .085, .98), Color("5c4a33"), 1.5, 10.0, 22, 18)
(modal as ForgedStyle).shadow = Color(0, 0, 0, .55)
(modal as ForgedStyle).shadow_size = 16.0
theme.set_stylebox("panel", "HudModal", modal)
theme.set_type_variation("HudModal", "PanelContainer")
var banner := plate(Color(.16, .075, .05, .94), Color(.11, .05, .035, .95), Color("a0523a"), 1.5, 6.0, 16, 8)
theme.set_stylebox("panel", "HudBannerDanger", banner)
theme.set_type_variation("HudBannerDanger", "PanelContainer")
var banner_own := plate(Color(.07, .1, .16, .94), Color(.05, .07, .11, .95), Color("5f86b8"), 1.5, 6.0, 16, 8)
theme.set_stylebox("panel", "HudBannerOwn", banner_own)
theme.set_type_variation("HudBannerOwn", "PanelContainer")
var tooltip := plate(Color(.06, .08, .11, .98), Color(.045, .06, .085, .98), Color("6b5a44"), 1.0, 6.0, 12, 9)
(tooltip as ForgedStyle).shadow = Color(0, 0, 0, .5)
(tooltip as ForgedStyle).shadow_size = 8.0
theme.set_stylebox("panel", "HudTooltip", tooltip)
theme.set_type_variation("HudTooltip", "PanelContainer")
# Buttons. Secondary HUD buttons are compact; the gold PrimaryButton stays for Bereit.
var focus: StyleBox = SaumwachtTheme.focus_ring(6.0)
_button(theme, "HudButton",
plate(Color("1b232d"), Color("131920"), Color("4a5a6c"), 1.0, 5.0, 12, 7),
plate(Color("26303c"), Color("1a212a"), BRONZE_LIGHT, 1.0, 5.0, 12, 7),
plate(Color("10151b"), Color("171d25"), BRONZE, 1.5, 5.0, 12, 7),
plate(Color("141a20"), Color("10141a"), Color("2c333c"), 1.0, 5.0, 12, 7), focus)
theme.set_font_size("font_size", "HudButton", 15)
theme.set_color("font_disabled_color", "HudButton", FAINT)
_button(theme, "HudIconButton",
plate(Color("1b232d"), Color("131920"), Color("3c4a5a"), 1.0, 5.0, 9, 6),
plate(Color("26303c"), Color("1a212a"), BRONZE_LIGHT, 1.0, 5.0, 9, 6),
plate(Color("10151b"), Color("171d25"), BRONZE, 1.5, 5.0, 9, 6),
plate(Color("141a20"), Color("10141a"), Color("2c333c"), 1.0, 5.0, 9, 6), focus)
theme.set_font_size("font_size", "HudIconButton", 15)
var primary_small: StyleBox = SaumwachtTheme.margins(SaumwachtTheme.plate(Color("e8c47e"), Color("b98a4a"), Color("fff0cc"), 1.5, 7.0), 16, 8)
var primary_small_hover: StyleBox = SaumwachtTheme.margins(SaumwachtTheme.plate(Color("f6d898"), Color("cc9c56"), Color.WHITE, 1.5, 7.0), 16, 8)
var primary_small_pressed: StyleBox = SaumwachtTheme.margins(SaumwachtTheme.plate(Color("b98a4a"), Color("d5aa66"), Color("fff0cc"), 2.0, 7.0), 16, 8)
var primary_small_disabled: StyleBox = SaumwachtTheme.margins(SaumwachtTheme.plate(Color("4a4234"), Color("3a3328"), Color("6a5c46"), 1.0, 7.0), 16, 8)
_button(theme, "HudPrimary", primary_small, primary_small_hover, primary_small_pressed, primary_small_disabled, focus)
theme.set_font("font", "HudPrimary", font_ui(760.0, 1))
theme.set_font_size("font_size", "HudPrimary", 16)
for state: String in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color", "font_hover_pressed_color"]:
theme.set_color(state, "HudPrimary", SaumwachtTheme.INK)
theme.set_color("font_disabled_color", "HudPrimary", Color("9c8f78"))
# Cards (units, heroes, items) and the champion variant with its own copper language.
var card_focus: StyleBox = SaumwachtTheme.focus_ring(7.0)
_button(theme, "HudCard",
plate(Color("19222c"), Color("10161d"), Color("34424f"), 1.0, 7.0, 6, 6),
plate(Color("212c38"), Color("141b23"), BRONZE, 1.5, 7.0, 6, 6),
plate(Color("2a2820"), Color("17171a"), BRONZE_LIGHT, 2.0, 7.0, 6, 6),
plate(Color("12171d"), Color("0d1116"), Color("252c34"), 1.0, 7.0, 6, 6), card_focus)
theme.set_stylebox("hover_pressed", "HudCard", theme.get_stylebox("pressed", "HudCard"))
_button(theme, "HudChampionCard",
plate(Color("261c14"), Color("15100c"), Color("6e4a2c"), 1.0, 7.0, 6, 6),
plate(Color("302216"), Color("1b140e"), CHAMPION, 1.5, 7.0, 6, 6),
plate(Color("3a2818"), Color("1e150e"), Color("ffc88c"), 2.0, 7.0, 6, 6),
plate(Color("16110d"), Color("0f0c0a"), Color("3a2c20"), 1.0, 7.0, 6, 6), card_focus)
_button(theme, "HudChip",
plate(Color("1a232d"), Color("11171e"), Color("33404d"), 1.0, 4.0, 2, 2),
plate(Color("222d39"), Color("151c24"), BRONZE, 1.0, 4.0, 2, 2),
plate(Color("2c2a22"), Color("17171a"), BRONZE_LIGHT, 2.0, 4.0, 2, 2),
plate(Color("12171d"), Color("0d1116"), Color("252c34"), 1.0, 4.0, 2, 2), SaumwachtTheme.focus_ring(4.0))
theme.set_stylebox("hover_pressed", "HudChip", theme.get_stylebox("pressed", "HudChip"))
_button(theme, "HudRow",
plate(Color(.09, .12, .16, .55), Color(.06, .08, .11, .55), Color("2a3645"), 1.0, 5.0, 10, 7),
plate(Color(.13, .17, .22, .75), Color(.08, .11, .15, .75), BRONZE, 1.0, 5.0, 10, 7),
plate(Color(.16, .15, .12, .8), Color(.09, .09, .1, .8), BRONZE_LIGHT, 1.5, 5.0, 10, 7),
plate(Color(.09, .12, .16, .55), Color(.06, .08, .11, .55), Color("2a3645"), 1.0, 5.0, 10, 7), focus)
# Toggles for lane assignment and target lanes: selected = bronze fill, never colour alone.
var toggle_on := plate(Color("3a3222"), Color("262117"), BRONZE_LIGHT, 1.5, 5.0, 10, 6)
(toggle_on as ForgedStyle).inner = Color(BRONZE_LIGHT, .35)
_button(theme, "HudToggle",
plate(Color("171e27"), Color("11161d"), Color("3a4756"), 1.0, 5.0, 10, 6),
plate(Color("212a35"), Color("171d25"), BRONZE, 1.0, 5.0, 10, 6),
toggle_on,
plate(Color("11151a"), Color("0e1115"), Color("242a31"), 1.0, 5.0, 10, 6), focus)
theme.set_stylebox("hover_pressed", "HudToggle", toggle_on)
theme.set_font("font", "HudToggle", font_ui(700.0, 1))
theme.set_font_size("font_size", "HudToggle", 14)
theme.set_color("font_color", "HudToggle", Color("cfd5dc"))
theme.set_color("font_pressed_color", "HudToggle", Color("fff4dc"))
theme.set_color("font_hover_pressed_color", "HudToggle", Color("fff4dc"))
theme.set_color("font_disabled_color", "HudToggle", Color("59616b"))
# Tabs: flat with a bronze underline for the open tab.
var tab := StyleBoxFlat.new()
tab.bg_color = Color(0, 0, 0, 0)
tab.border_color = Color(0, 0, 0, 0)
tab.border_width_bottom = 2
SaumwachtTheme.margins(tab, 12, 6)
var tab_hover := tab.duplicate() as StyleBoxFlat
tab_hover.bg_color = Color(1, 1, 1, .04)
tab_hover.border_color = Color(BRONZE, .5)
var tab_on := tab.duplicate() as StyleBoxFlat
tab_on.bg_color = Color(BRONZE_LIGHT, .08)
tab_on.border_color = BRONZE_LIGHT
_button(theme, "HudTab", tab, tab_hover, tab_on, tab, SaumwachtTheme.focus_ring(3.0))
theme.set_stylebox("hover_pressed", "HudTab", tab_on)
theme.set_font("font", "HudTab", caps)
theme.set_font_size("font_size", "HudTab", 14)
theme.set_color("font_color", "HudTab", MUTED)
theme.set_color("font_hover_color", "HudTab", TEXT)
theme.set_color("font_pressed_color", "HudTab", BRONZE_LIGHT)
theme.set_color("font_hover_pressed_color", "HudTab", BRONZE_LIGHT)
theme.set_color("font_disabled_color", "HudTab", FAINT)
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, maxi(MIN_FONT, 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_type_variation(type, "Button")
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)
theme.set_color("font_color", type, TEXT)
theme.set_color("font_hover_color", type, Color.WHITE)
theme.set_color("font_focus_color", type, Color.WHITE)
## Rounded pill for StatusBadge; one cached style per colour and fill mode.
static func badge_style(color: Color, filled: bool) -> StyleBox:
var key := "badge:%s:%s" % [color.to_html(), filled]
if not _styles.has(key):
var style := StyleBoxFlat.new()
style.bg_color = Color(color, .92) if filled else Color(color.darkened(.75), .85)
style.border_color = color if not filled else color.lightened(.2)
style.set_border_width_all(1)
style.set_corner_radius_all(4)
style.content_margin_left = 7
style.content_margin_right = 7
style.content_margin_top = 1
style.content_margin_bottom = 1
style.anti_aliasing = true
_styles[key] = style
return _styles[key]
## Small recessed square for item slots in cards (shared, 4 px padding).
static func slot_style(active: bool = false) -> StyleBox:
var key := "slot:%s" % active
if not _styles.has(key):
var style := StyleBoxFlat.new()
style.bg_color = Color(0, 0, 0, .35)
style.border_color = BRONZE if active else Color("33404d")
style.set_border_width_all(1)
style.set_corner_radius_all(3)
style.set_content_margin_all(4)
_styles[key] = style
return _styles[key]
## Label factory used by every component so sizes and variations stay consistent.
static func label(text_value: String, variation: String = "HudText", parent: Node = null, size: int = 0) -> Label:
var result := Label.new()
result.text = text_value
result.theme_type_variation = variation
result.mouse_filter = Control.MOUSE_FILTER_IGNORE
if size > 0:
result.add_theme_font_size_override("font_size", maxi(MIN_FONT, size))
if parent != null:
parent.add_child(result)
return result
static func button(text_value: String, variation: String = "HudButton", parent: Node = null) -> Button:
var result := Button.new()
result.text = text_value
result.theme_type_variation = variation
result.focus_mode = Control.FOCUS_ALL
result.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
if parent != null:
parent.add_child(result)
return result
static func team_color(team: int) -> Color:
return TEAM[clampi(team, 0, 2)]
static func team_name(team: int) -> String:
return TEAM_NAMES[clampi(team, 0, 2)]
## Whole numbers with German thousands separators (12.000).
static func number(value: float) -> String:
var whole: int = int(round(value))
var negative: bool = whole < 0
var digits: String = str(absi(whole))
var result: String = ""
while digits.length() > 3:
result = "." + digits.right(3) + result
digits = digits.left(digits.length() - 3)
return ("-" if negative else "") + digits + result
static func decimal(value: float, places: int = 1) -> String:
return (("%." + str(places) + "f") % value).replace(".", ",")