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>
282 lines
14 KiB
GDScript
282 lines
14 KiB
GDScript
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(".", ",")
|