saumwacht-godot/scripts/luneraria/ui/components/hero_card.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

207 lines
8.8 KiB
GDScript

class_name LuneHeroCard
extends Button
## The hero in the bottom bar: portrait, name, level, experience progress, HP, role, the
## three item slots and the talent state. A click opens the hero window (details, items,
## talents) or, without a hero, the recruiting window. No MOBA action bar: heroes act alone.
var hero_id: String = ""
var _portrait: LunePortraitRect
var _name: Label
var _level: Label
var _role_glyph: LuneGlyph
var _role: Label
var _xp: LuneHpBar
var _xp_text: Label
var _hp_glyph: LuneGlyph
var _hp: Label
var _hp_bar: LuneHpBar
var _slots: Array[LuneGlyph] = []
var _slot_boxes: Array[PanelContainer] = []
var _talent: LuneStatusBadge
var _empty: VBoxContainer
var _filled: VBoxContainer
var _faces: Array[LunePortraitRect] = []
var data: Dictionary = {}
var _signature: String = ""
var _empty_title: Label
var _empty_price: Label
var _empty_coin: LuneGlyph
var _faces_row: HBoxContainer
func _init() -> void:
theme_type_variation = "HudCard"
focus_mode = Control.FOCUS_ALL
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
custom_minimum_size = Vector2(320, 132)
var row := HBoxContainer.new()
row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
row.offset_left = 8
row.offset_right = -8
row.offset_top = 8
row.offset_bottom = -8
row.add_theme_constant_override("separation", 10)
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(row)
_portrait = LunePortraitRect.new()
_portrait.custom_minimum_size = Vector2(88, 0)
row.add_child(_portrait)
_filled = VBoxContainer.new()
_filled.add_theme_constant_override("separation", 2)
_filled.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_filled.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(_filled)
_name = LuneUiTheme.label("", "HudName", _filled)
_name.clip_text = true
_name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
var role_row := HBoxContainer.new()
role_row.add_theme_constant_override("separation", 5)
role_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
_filled.add_child(role_row)
LuneGlyph.make("star", LuneUiTheme.HERO, 13.0, role_row)
_level = LuneUiTheme.label("", "HudValue", role_row)
_role_glyph = LuneGlyph.make("tank", LuneUiTheme.MUTED, 14.0, role_row)
_role = LuneUiTheme.label("", "HudMuted", role_row)
var xp_row := HBoxContainer.new()
xp_row.add_theme_constant_override("separation", 6)
xp_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
_filled.add_child(xp_row)
_xp = LuneHpBar.new()
_xp.custom_minimum_size = Vector2(60, 6)
_xp.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_xp.size_flags_vertical = Control.SIZE_SHRINK_CENTER
_xp.fill = LuneUiTheme.BRONZE_LIGHT
xp_row.add_child(_xp)
_xp_text = LuneUiTheme.label("", "HudMuted", xp_row)
var hp_row := HBoxContainer.new()
hp_row.add_theme_constant_override("separation", 5)
hp_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
_filled.add_child(hp_row)
_hp_glyph = LuneGlyph.make("heart", LuneUiTheme.DANGER.lightened(.2), 13.0, hp_row)
_hp = LuneUiTheme.label("", "HudText", hp_row)
_hp_bar = LuneHpBar.new()
_hp_bar.custom_minimum_size = Vector2(50, 5)
_hp_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_hp_bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
hp_row.add_child(_hp_bar)
var item_row := HBoxContainer.new()
item_row.add_theme_constant_override("separation", 4)
item_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
_filled.add_child(item_row)
for index: int in range(LuneContent.ITEM_SLOTS):
var box := PanelContainer.new()
box.add_theme_stylebox_override("panel", LuneUiTheme.slot_style())
box.custom_minimum_size = Vector2(26, 26)
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
item_row.add_child(box)
var glyph := LuneGlyph.new()
glyph.custom_minimum_size = Vector2(18, 18)
box.add_child(glyph)
_slots.append(glyph)
_slot_boxes.append(box)
_talent = LuneStatusBadge.new()
item_row.add_child(_talent)
_empty = VBoxContainer.new()
_empty.add_theme_constant_override("separation", 3)
_empty.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_empty.alignment = BoxContainer.ALIGNMENT_CENTER
_empty.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(_empty)
_empty_title = LuneUiTheme.label("HELD ANWERBEN", "HudName", _empty)
var faces := HBoxContainer.new()
_faces_row = faces
faces.add_theme_constant_override("separation", 4)
faces.mouse_filter = Control.MOUSE_FILTER_IGNORE
_empty.add_child(faces)
for hero: String in LuneContent.HEROES:
var face := LunePortraitRect.new()
face.custom_minimum_size = Vector2(38, 38)
faces.add_child(face)
_faces.append(face)
var price := HBoxContainer.new()
price.add_theme_constant_override("separation", 4)
price.mouse_filter = Control.MOUSE_FILTER_IGNORE
_empty.add_child(price)
_empty_coin = LuneGlyph.make("coin", LuneUiTheme.GOLD, 14.0, price)
_empty_price = LuneUiTheme.label("%d · ein Held pro Spieler" % int(LuneContent.HEROES.values()[0].cost), "HudMuted", price)
LuneTooltip.attach(self, _tip)
## hero: own hero dictionary (draft in planning) or {}; live: the combat unit or null.
func apply(hero: Dictionary, model: LuneHudModel, live: LuneUnit) -> void:
var has_hero: bool = not hero.is_empty()
if live != null and has_hero:
_hp.text = ("%s / %s" % [LuneUiTheme.number(live.hp), LuneUiTheme.number(live.max_hp)]) if live.alive else "gefallen"
_hp_bar.fraction = live.hp / maxf(1.0, live.max_hp) if live.alive else 0.0
_hp_bar.fill = LuneHpBar.health_color(_hp_bar.fraction, LuneUiTheme.GOOD)
_portrait.crossed = not live.alive
var signature: String = "%s|%s|%d|%s" % [str(hero), live != null, model.own_gold(), model.planning()]
if signature == _signature:
return
_signature = signature
_filled.visible = has_hero
_empty.visible = not has_hero
if not has_hero:
hero_id = ""
data = {}
_portrait.show_model("w04")
_portrait.dimmed = true
var index: int = 0
for id: String in LuneContent.HEROES:
_faces[index].show_model(str(LuneContent.HEROES[id].model))
index += 1
# In combat nobody can recruit: say so instead of offering it.
var planning: bool = model.planning()
_empty_title.text = "HELD ANWERBEN" if planning else "KEIN HELD"
_empty_price.text = ("%d · ein Held pro Spieler" % int(LuneContent.HEROES.values()[0].cost)) if planning else "Anwerben in der Planung"
_empty_coin.visible = planning
_faces_row.visible = planning
return
hero_id = str(hero.hero_id)
var spec: Dictionary = LuneContent.HEROES[hero_id]
var definition: Dictionary = model.hero_definition(hero)
data = {"hero": hero, "definition": definition}
_portrait.dimmed = false
_portrait.crossed = live != null and not live.alive
_portrait.show_model(str(spec.model))
_name.text = str(spec.name)
var xp: int = int(hero.xp)
var level: int = LuneContent.level_for(xp)
_level.text = "STUFE %d" % level
_role_glyph.glyph = LuneStats.role_glyph(str(definition.role))
_role.text = LuneStats.role_name(str(definition.role))
var low: int = LuneContent.LEVEL_XP[level - 1]
var top: bool = level >= LuneContent.LEVEL_XP.size()
var high: int = LuneContent.LEVEL_XP[mini(level, LuneContent.LEVEL_XP.size() - 1)]
_xp.fraction = 1.0 if top else float(xp - low) / maxf(1.0, float(high - low))
_xp_text.text = "EP %d / %d" % [xp, high] if not top else "EP %d · max" % xp
_hp_bar.visible = live != null
if live == null:
_hp.text = "%s Leben" % LuneUiTheme.number(definition.max_hp)
var items: Array = hero.items
for index: int in range(_slots.size()):
var item_id: String = str(items[index]) if index < items.size() else ""
_slots[index].glyph = item_id
_slots[index].color = LuneItemSlot.color_of(item_id)
var talents: Array = hero.talents
var open_tier: int = talents.size()
if open_tier < LuneContent.TALENT_LEVELS.size() and level >= LuneContent.TALENT_LEVELS[open_tier]:
_talent.set_state("TALENT!", LuneUiTheme.HERO, "star", true)
_talent.visible = model.planning()
elif open_tier < LuneContent.TALENT_LEVELS.size():
_talent.set_state("TALENT AB %d" % LuneContent.TALENT_LEVELS[open_tier], LuneUiTheme.MUTED)
_talent.visible = true
else:
_talent.visible = false
func _tip() -> Dictionary:
if data.is_empty():
return {"title": "Held anwerben", "glyph": "crown", "glyph_color": LuneUiTheme.HERO,
"text": "Drei Helden stehen zur Wahl; jeder Spieler führt einen. Helden sammeln Erfahrung, steigen bis Stufe 10 auf, tragen drei Items und wählen zwei Talente.",
"note": "Klick: Heldenauswahl öffnen"}
var hero: Dictionary = data.hero
var spec: Dictionary = LuneContent.HEROES[hero.hero_id]
var names: PackedStringArray = []
for item_id: String in hero.items:
names.append(LuneContent.ITEMS[item_id].name)
return {"title": "%s · Stufe %d" % [spec.name, LuneContent.level_for(int(hero.xp))], "glyph": "crown", "glyph_color": LuneUiTheme.HERO,
"subtitle": str(spec.title), "stats": LuneStats.rows(data.definition),
"text": "Items: " + (", ".join(names) if not names.is_empty() else "keine"), "note": "Klick: Werte, Items und Talente · H: Kamera zum Helden"}