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>
347 lines
17 KiB
GDScript
347 lines
17 KiB
GDScript
class_name LuneHeroDetail
|
||
extends VBoxContainer
|
||
## Hero window content. Without a hero: the three heroes with portrait, role, real stats and
|
||
## price to recruit one. With a hero: portrait, level and experience, lane order, and tabs for
|
||
## values and ability (from the rules' definitions, including level, items and talents), the
|
||
## three item slots with the item shop, and the two talent tiers. Only shows data; every
|
||
## change is a command through the game.
|
||
|
||
signal command_requested(command: Dictionary)
|
||
|
||
const TAB_VALUES: int = 0
|
||
const TAB_ITEMS: int = 1
|
||
const TAB_TALENTS: int = 2
|
||
|
||
var tab: int = TAB_VALUES
|
||
var _model: LuneHudModel
|
||
var _signature: String = ""
|
||
var item_cards: Dictionary = {}
|
||
var recruit_buttons: Dictionary = {}
|
||
var lane_buttons: Dictionary = {}
|
||
var tabs: LuneTabBar
|
||
|
||
func _init() -> void:
|
||
add_theme_constant_override("separation", 10)
|
||
size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
|
||
func show_tab(index: int) -> void:
|
||
tab = index
|
||
_signature = ""
|
||
if _model != null:
|
||
apply(_model)
|
||
|
||
func title_text(model: LuneHudModel) -> String:
|
||
var hero: Dictionary = model.own_hero()
|
||
if hero.is_empty():
|
||
return "HELD ANWERBEN"
|
||
return "HELD · %s" % str(LuneContent.HEROES[hero.hero_id].name).to_upper()
|
||
|
||
func apply(model: LuneHudModel) -> void:
|
||
_model = model
|
||
var hero: Dictionary = model.own_hero()
|
||
var signature: String = "%s|%d|%s|%s|%d|%s|%d" % [str(hero), model.own_gold(), model.planning(), model.locked(), tab, model.state.grotto_available(), model.state.round_number]
|
||
if signature == _signature:
|
||
return
|
||
_signature = signature
|
||
for child: Node in get_children():
|
||
child.free()
|
||
item_cards.clear()
|
||
recruit_buttons.clear()
|
||
lane_buttons.clear()
|
||
if hero.is_empty():
|
||
_recruit(model)
|
||
else:
|
||
_hero(model, hero)
|
||
|
||
func _can_act(model: LuneHudModel) -> bool:
|
||
return model.planning() and not model.locked()
|
||
|
||
func _why_not(model: LuneHudModel) -> String:
|
||
if not model.planning():
|
||
return "Nur in der Planung."
|
||
if model.locked():
|
||
return "Bereit gemeldet – die Planung ist verbindlich."
|
||
return ""
|
||
|
||
# ---------------------------------------------------------------- recruiting
|
||
func _recruit(model: LuneHudModel) -> void:
|
||
LuneUiTheme.label("Jeder Spieler führt einen Helden. Er kämpft auf seiner Lane mit, sammelt Erfahrung aus Gefechten in seiner Nähe und aus jeder Runde, trägt drei Items und wählt ab Stufe 3 und 6 je ein Talent.", "HudMuted", self).autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
var row := HBoxContainer.new()
|
||
row.add_theme_constant_override("separation", 12)
|
||
row.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
add_child(row)
|
||
var gold: int = model.own_gold()
|
||
for hero_id: String in LuneContent.HEROES:
|
||
var spec: Dictionary = LuneContent.HEROES[hero_id]
|
||
var panel := PanelContainer.new()
|
||
panel.theme_type_variation = "HudPanelTight"
|
||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
row.add_child(panel)
|
||
var column := VBoxContainer.new()
|
||
column.add_theme_constant_override("separation", 5)
|
||
panel.add_child(column)
|
||
var portrait := LunePortraitRect.new()
|
||
portrait.custom_minimum_size = Vector2(0, 170)
|
||
column.add_child(portrait)
|
||
portrait.show_model(str(spec.model), true)
|
||
LuneUiTheme.label(str(spec.name), "HudTitle", column)
|
||
LuneUiTheme.label(str(spec.title), "HudMuted", column)
|
||
var role := HBoxContainer.new()
|
||
role.add_theme_constant_override("separation", 5)
|
||
column.add_child(role)
|
||
var definition: Dictionary = model.hero_definition({"hero_id": hero_id, "xp": 0, "items": [], "talents": []})
|
||
LuneGlyph.make(LuneStats.role_glyph(str(definition.role)), LuneUiTheme.BRONZE_LIGHT, 15.0, role)
|
||
LuneUiTheme.label(str(spec.role), "HudText", role)
|
||
var grid := GridContainer.new()
|
||
grid.columns = 3
|
||
grid.add_theme_constant_override("h_separation", 6)
|
||
column.add_child(grid)
|
||
var rows: Array = LuneStats.rows(definition)
|
||
for entry: Array in rows.slice(0, 5):
|
||
LuneGlyph.make(str(entry[0]), LuneUiTheme.MUTED, 13.0, grid)
|
||
LuneUiTheme.label(str(entry[1]), "HudMuted", grid)
|
||
LuneUiTheme.label(str(entry[2]), "HudText", grid)
|
||
var summary := LuneUiTheme.label(str(spec.summary), "HudText", column)
|
||
summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
summary.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
var cost: int = int(spec.cost)
|
||
var buy := LuneUiTheme.button("ANWERBEN · %d G" % cost, "HudPrimary", column)
|
||
var why: String = _why_not(model)
|
||
if why.is_empty() and gold < cost:
|
||
why = "Es fehlen %d Gold." % (cost - gold)
|
||
buy.disabled = not why.is_empty()
|
||
buy.pressed.connect(func() -> void: command_requested.emit({"type": "hero_buy", "hero_id": hero_id}))
|
||
recruit_buttons[hero_id] = buy
|
||
if not why.is_empty():
|
||
LuneUiTheme.label(why, "HudMuted", column).add_theme_color_override("font_color", LuneUiTheme.DANGER if gold < cost else LuneUiTheme.MUTED)
|
||
LuneUiTheme.label("Vorläufige Rolle des vorhandenen Modells.", "HudMuted", column)
|
||
|
||
# ---------------------------------------------------------------- hero
|
||
func _hero(model: LuneHudModel, hero: Dictionary) -> void:
|
||
var spec: Dictionary = LuneContent.HEROES[hero.hero_id]
|
||
var definition: Dictionary = model.hero_definition(hero)
|
||
var can_act: bool = _can_act(model)
|
||
var body := HBoxContainer.new()
|
||
body.add_theme_constant_override("separation", 18)
|
||
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
add_child(body)
|
||
var left := VBoxContainer.new()
|
||
left.add_theme_constant_override("separation", 6)
|
||
left.custom_minimum_size.x = 290
|
||
body.add_child(left)
|
||
var portrait := LunePortraitRect.new()
|
||
portrait.custom_minimum_size = Vector2(290, 230)
|
||
left.add_child(portrait)
|
||
portrait.show_model(str(spec.model), true)
|
||
LuneUiTheme.label(str(spec.title), "HudMuted", left)
|
||
var role := HBoxContainer.new()
|
||
role.add_theme_constant_override("separation", 6)
|
||
left.add_child(role)
|
||
LuneGlyph.make(LuneStats.role_glyph(str(definition.role)), LuneUiTheme.BRONZE_LIGHT, 16.0, role)
|
||
LuneUiTheme.label(str(spec.role), "HudText", role)
|
||
var xp: int = int(hero.xp)
|
||
var level: int = LuneContent.level_for(xp)
|
||
var level_row := HBoxContainer.new()
|
||
level_row.add_theme_constant_override("separation", 6)
|
||
left.add_child(level_row)
|
||
LuneGlyph.make("star", LuneUiTheme.HERO, 16.0, level_row)
|
||
LuneUiTheme.label("STUFE %d / %d" % [level, LuneContent.LEVEL_XP.size()], "HudValue", level_row)
|
||
var bar := LuneHpBar.new()
|
||
bar.custom_minimum_size = Vector2(0, 8)
|
||
bar.fill = LuneUiTheme.BRONZE_LIGHT
|
||
left.add_child(bar)
|
||
var top: bool = level >= LuneContent.LEVEL_XP.size()
|
||
var low: int = LuneContent.LEVEL_XP[level - 1]
|
||
var high: int = LuneContent.LEVEL_XP[mini(level, LuneContent.LEVEL_XP.size() - 1)]
|
||
bar.fraction = 1.0 if top else float(xp - low) / maxf(1.0, float(high - low))
|
||
LuneUiTheme.label(("%d / %d EP · noch %d bis Stufe %d" % [xp, high, high - xp, level + 1]) if not top else "%d EP · Höchststufe" % xp, "HudMuted", left)
|
||
LuneUiTheme.label("EINSATZ", "HudCaps", left)
|
||
var lanes := HBoxContainer.new()
|
||
lanes.add_theme_constant_override("separation", 4)
|
||
left.add_child(lanes)
|
||
for lane: String in LuneMap.LANES + [LuneMatch.GROTTO]:
|
||
var title: String = "GROTTE" if lane == LuneMatch.GROTTO else LuneMap.LANE_NAMES[lane].to_upper()
|
||
var button := LuneUiTheme.button(title, "HudToggle", lanes)
|
||
button.toggle_mode = true
|
||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
button.set_pressed_no_signal(str(hero.lane) == lane)
|
||
button.disabled = not can_act or (lane == LuneMatch.GROTTO and not model.state.grotto_available())
|
||
var uid: String = str(hero.uid)
|
||
button.pressed.connect(func() -> void: command_requested.emit({"type": "assign", "uid": uid, "lane": lane}))
|
||
lane_buttons[lane] = button
|
||
if str(hero.lane) == LuneMatch.GROTTO:
|
||
var warning := LuneUiTheme.label("Der Held fehlt seiner Lane in der nächsten Kampfphase.", "HudText", left)
|
||
warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
warning.add_theme_color_override("font_color", LuneUiTheme.WARN)
|
||
if bool(hero.get("fresh", false)) and model.planning():
|
||
var refund: int = int(hero.invested)
|
||
for item_id: String in hero.items:
|
||
refund += int(LuneContent.ITEMS[item_id].cost)
|
||
var cancel := LuneUiTheme.button("Held zurückgeben +%d" % refund, "HudButton", left)
|
||
cancel.disabled = not can_act
|
||
cancel.pressed.connect(func() -> void: command_requested.emit({"type": "hero_cancel"}))
|
||
var right := VBoxContainer.new()
|
||
right.add_theme_constant_override("separation", 8)
|
||
right.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
body.add_child(right)
|
||
tabs = LuneTabBar.new()
|
||
right.add_child(tabs)
|
||
tabs.add_tab("WERTE & FÄHIGKEIT")
|
||
tabs.add_tab("ITEMS %d/%d" % [(hero.items as Array).size(), LuneContent.ITEM_SLOTS])
|
||
var open_tier: int = (hero.talents as Array).size()
|
||
var talent_ready: bool = open_tier < LuneContent.TALENT_LEVELS.size() and level >= LuneContent.TALENT_LEVELS[open_tier]
|
||
tabs.add_tab("TALENTE" + (" ★" if talent_ready else ""))
|
||
tabs.select(tab)
|
||
tabs.tab_changed.connect(show_tab)
|
||
var page := VBoxContainer.new()
|
||
page.add_theme_constant_override("separation", 8)
|
||
page.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
right.add_child(page)
|
||
match tab:
|
||
TAB_ITEMS:
|
||
_items(model, hero, level, page)
|
||
TAB_TALENTS:
|
||
_talents(model, hero, level, page)
|
||
_:
|
||
_values(hero, definition, spec, page)
|
||
|
||
func _values(hero: Dictionary, definition: Dictionary, spec: Dictionary, page: VBoxContainer) -> void:
|
||
var grid := GridContainer.new()
|
||
grid.columns = 3
|
||
grid.add_theme_constant_override("h_separation", 10)
|
||
grid.add_theme_constant_override("v_separation", 3)
|
||
page.add_child(grid)
|
||
var rows: Array = LuneStats.rows(definition, true)
|
||
rows.append(["sword", "Schaden/Sekunde", "≈ %s (ohne Fähigkeit)" % LuneUiTheme.decimal(LuneStats.dps(definition))])
|
||
for entry: Array in rows:
|
||
LuneGlyph.make(str(entry[0]), LuneUiTheme.MUTED, 15.0, grid)
|
||
LuneUiTheme.label(str(entry[1]), "HudMuted", grid)
|
||
LuneUiTheme.label(str(entry[2]), "HudText", grid)
|
||
var level: int = int(definition.level)
|
||
var base: Dictionary = spec.def
|
||
var growth := LuneUiTheme.label("Grundwerte %s Leben, %s Schaden; je Stufe +%d %% Leben und +%d %% Schaden (jetzt Stufe %d)." % [
|
||
LuneUiTheme.number(base.max_hp), LuneUiTheme.decimal(base.damage), int(LuneContent.HP_PER_LEVEL * 100.0), int(LuneContent.DAMAGE_PER_LEVEL * 100.0), level], "HudMuted", page)
|
||
growth.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
LuneUiTheme.label("FÄHIGKEIT", "HudCaps", page)
|
||
var ability := LuneUiTheme.label(str(DataCatalog.get_unit(str(spec.model)).get("ability_description", "")), "HudText", page)
|
||
ability.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
var chosen: PackedStringArray = []
|
||
for tier: int in range((hero.talents as Array).size()):
|
||
for option: Dictionary in LuneContent.TALENTS[hero.hero_id][tier]:
|
||
if option.id == hero.talents[tier]:
|
||
chosen.append("%s: %s" % [option.name, option.text])
|
||
if not chosen.is_empty():
|
||
LuneUiTheme.label("TALENTE", "HudCaps", page)
|
||
var talents := LuneUiTheme.label("\n".join(chosen), "HudText", page)
|
||
talents.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
var xp := LuneUiTheme.label("Erfahrung: Tode von Gegnern in %d m (Monster 12 + Welle, Champion 30 + 2 × Welle, Gebäude %d, Boss %d) und je Kampf %d + %d × Welle." % [
|
||
int(LuneContent.XP_RADIUS), LuneContent.BUILDING_XP, LuneContent.BOSS_XP, LuneContent.XP_PARTICIPATION, LuneContent.XP_PER_ROUND], "HudMuted", page)
|
||
xp.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
|
||
func _items(model: LuneHudModel, hero: Dictionary, level: int, page: VBoxContainer) -> void:
|
||
var can_act: bool = _can_act(model)
|
||
var items: Array = hero.items
|
||
var slots := HBoxContainer.new()
|
||
slots.add_theme_constant_override("separation", 10)
|
||
page.add_child(slots)
|
||
for index: int in range(LuneContent.ITEM_SLOTS):
|
||
var box := VBoxContainer.new()
|
||
box.add_theme_constant_override("separation", 3)
|
||
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
slots.add_child(box)
|
||
var slot := LuneItemSlot.new(true)
|
||
slot.custom_minimum_size = Vector2(46, 46)
|
||
slot.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||
box.add_child(slot)
|
||
if index < items.size():
|
||
var item_id: String = items[index]
|
||
var item: Dictionary = LuneContent.ITEMS[item_id]
|
||
slot.set_item(item_id, LuneItemSlot.State.EQUIPPED)
|
||
LuneTooltip.attach(slot, func() -> Dictionary: return {"title": str(item.name), "glyph": item_id, "glyph_color": LuneItemSlot.color_of(item_id), "text": str(item.text)})
|
||
LuneUiTheme.label(str(item.name), "HudText", box)
|
||
var fresh: bool = (hero.get("fresh_items", []) as Array).has(item_id)
|
||
var refund: int = int(item.cost) if fresh else floori(float(item.cost) * LuneContent.ITEM_REFUND)
|
||
var sell := LuneUiTheme.button("Verkaufen +%d" % refund, "HudButton", box)
|
||
sell.disabled = not can_act
|
||
sell.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||
sell.pressed.connect(func() -> void: command_requested.emit({"type": "item_sell", "item_id": item_id}))
|
||
else:
|
||
slot.set_item("", LuneItemSlot.State.EMPTY)
|
||
LuneUiTheme.label("Platz %d frei" % (index + 1), "HudMuted", box)
|
||
LuneUiTheme.label("ITEMS · einzigartig je Held, bleiben dauerhaft, Rückgabe %d %%" % int(LuneContent.ITEM_REFUND * 100.0), "HudCaps", page)
|
||
var grid := GridContainer.new()
|
||
grid.columns = 2
|
||
grid.add_theme_constant_override("h_separation", 8)
|
||
grid.add_theme_constant_override("v_separation", 8)
|
||
page.add_child(grid)
|
||
var gold: int = model.own_gold()
|
||
for item_id: String in LuneContent.ITEMS:
|
||
var item: Dictionary = LuneContent.ITEMS[item_id]
|
||
var card := LuneItemSlot.new(false)
|
||
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
grid.add_child(card)
|
||
var state: int = LuneItemSlot.State.AVAILABLE
|
||
if items.has(item_id):
|
||
state = LuneItemSlot.State.UNIQUE
|
||
elif not can_act:
|
||
state = LuneItemSlot.State.LOCKED
|
||
elif level < int(item.get("min_level", 1)):
|
||
state = LuneItemSlot.State.INCOMPATIBLE
|
||
elif items.size() >= LuneContent.ITEM_SLOTS:
|
||
state = LuneItemSlot.State.FULL
|
||
elif gold < int(item.cost):
|
||
state = LuneItemSlot.State.UNAFFORDABLE
|
||
card.set_item(item_id, state)
|
||
item_cards[item_id] = card
|
||
var uid: String = str(hero.uid)
|
||
card.pressed.connect(func() -> void:
|
||
if card.state == LuneItemSlot.State.AVAILABLE:
|
||
command_requested.emit({"type": "item_buy", "item_id": item_id, "uid": uid}))
|
||
var reasons: Dictionary = {LuneItemSlot.State.UNIQUE: "Dein Held trägt es bereits; Items sind einzigartig.",
|
||
LuneItemSlot.State.LOCKED: _why_not(model), LuneItemSlot.State.INCOMPATIBLE: "Erfordert Heldenstufe %d." % int(item.get("min_level", 1)),
|
||
LuneItemSlot.State.FULL: "Alle %d Plätze belegt; verkaufe zuerst ein Item." % LuneContent.ITEM_SLOTS,
|
||
LuneItemSlot.State.UNAFFORDABLE: "Es fehlen %d Gold." % (int(item.cost) - gold), LuneItemSlot.State.AVAILABLE: "Klick: kaufen und ausrüsten."}
|
||
var why: String = str(reasons.get(state, ""))
|
||
LuneTooltip.attach(card, func() -> Dictionary: return {"title": "%s · %d Gold" % [item.name, int(item.cost)], "glyph": item_id,
|
||
"glyph_color": LuneItemSlot.color_of(item_id), "text": str(item.text), "note": why})
|
||
|
||
func _talents(model: LuneHudModel, hero: Dictionary, level: int, page: VBoxContainer) -> void:
|
||
var can_act: bool = _can_act(model)
|
||
var talents: Array = hero.talents
|
||
for tier: int in range(LuneContent.TALENT_LEVELS.size()):
|
||
var required: int = LuneContent.TALENT_LEVELS[tier]
|
||
var state_text: String = "gewählt" if tier < talents.size() else ("jetzt wählbar" if level >= required and tier == talents.size() else "ab Stufe %d" % required)
|
||
LuneUiTheme.label("TALENT %d · STUFE %d · %s" % [tier + 1, required, state_text.to_upper()], "HudCaps", page)
|
||
var row := HBoxContainer.new()
|
||
row.add_theme_constant_override("separation", 8)
|
||
page.add_child(row)
|
||
var options: Array = LuneContent.TALENTS[hero.hero_id][tier]
|
||
for choice: int in range(options.size()):
|
||
var option: Dictionary = options[choice]
|
||
var card := Button.new()
|
||
card.theme_type_variation = "HudCard"
|
||
card.toggle_mode = true
|
||
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
card.custom_minimum_size = Vector2(0, 96)
|
||
card.focus_mode = Control.FOCUS_ALL
|
||
row.add_child(card)
|
||
var column := VBoxContainer.new()
|
||
column.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
column.offset_left = 10
|
||
column.offset_right = -10
|
||
column.offset_top = 8
|
||
column.offset_bottom = -8
|
||
column.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
card.add_child(column)
|
||
LuneUiTheme.label(str(option.name), "HudValue", column)
|
||
var text := LuneUiTheme.label(str(option.text), "HudMuted", column)
|
||
text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
var chosen: bool = tier < talents.size() and talents[tier] == option.id
|
||
card.set_pressed_no_signal(chosen)
|
||
card.disabled = not (can_act and tier == talents.size() and level >= required) and not chosen
|
||
var command: Dictionary = {"type": "talent", "tier": tier, "choice": choice}
|
||
card.pressed.connect(func() -> void:
|
||
card.set_pressed_no_signal(chosen)
|
||
if not chosen:
|
||
command_requested.emit(command))
|
||
LuneUiTheme.label("Die Wahl ist endgültig; jede Stufe bietet zwei Richtungen (Einzelziel oder Fläche, Schutz oder Druck).", "HudMuted", page).autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|