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

197 lines
9.3 KiB
GDScript

class_name LuneUnitDetail
extends PanelContainer
## The selected normal unit: portrait, name, stage, role, HP, damage, tempo, range, armour,
## ability, upgrade with cost and stat change, return value, and the lane order with TOP,
## MID, BOTTOM and GROTTE (only allowed options active). Normal units carry no items, so this
## panel has no item slots. In combat or after Ready it only informs.
signal command_requested(command: Dictionary)
signal closed
const GROTTO_WARNING := "Diese Einheit fehlt deiner Lane in der nächsten Kampfphase."
var uid: String = ""
var _portrait: LunePortraitRect
var _name: Label
var _sub: Label
var _live: LuneHpBar
var _stats: GridContainer
var _ability: Label
var _types: Label
var _upgrade_box: VBoxContainer
var _upgrade_title: Label
var _upgrade_delta: Label
var _upgrade: Button
var _sell: Button
var _sell_note: Label
var _lanes: Dictionary = {}
var _lane_row: HBoxContainer
var _warning: Label
var _locked: Label
var _signature: String = ""
func _init() -> void:
theme_type_variation = "HudPanel"
mouse_filter = Control.MOUSE_FILTER_STOP
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 5)
add_child(column)
var head := HBoxContainer.new()
head.add_theme_constant_override("separation", 10)
column.add_child(head)
_portrait = LunePortraitRect.new()
_portrait.custom_minimum_size = Vector2(76, 76)
head.add_child(_portrait)
var titles := VBoxContainer.new()
titles.size_flags_horizontal = Control.SIZE_EXPAND_FILL
titles.add_theme_constant_override("separation", 2)
head.add_child(titles)
_name = LuneUiTheme.label("", "HudName", titles)
_name.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_sub = LuneUiTheme.label("", "HudMuted", titles)
_live = LuneHpBar.new()
_live.custom_minimum_size = Vector2(0, 6)
titles.add_child(_live)
var close := LuneUiTheme.button("", "HudIconButton", head)
close.custom_minimum_size = Vector2(32, 30)
close.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
var cross := LuneGlyph.make("close", LuneUiTheme.MUTED, 12.0, close)
cross.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
close.pressed.connect(func() -> void: closed.emit())
LuneTooltip.attach(close, func() -> Dictionary: return {"title": "Auswahl aufheben", "text": "Esc"})
_stats = GridContainer.new()
_stats.columns = 4
_stats.add_theme_constant_override("h_separation", 6)
_stats.add_theme_constant_override("v_separation", 2)
column.add_child(_stats)
LuneTooltip.attach(_stats, _stats_tip)
_types = LuneUiTheme.label("", "HudMuted", column)
_types.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_ability = LuneUiTheme.label("", "HudText", column)
_ability.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_upgrade_box = VBoxContainer.new()
_upgrade_box.add_theme_constant_override("separation", 2)
column.add_child(_upgrade_box)
var upgrade_row := HBoxContainer.new()
upgrade_row.add_theme_constant_override("separation", 8)
_upgrade_box.add_child(upgrade_row)
_upgrade_title = LuneUiTheme.label("", "HudText", upgrade_row)
_upgrade_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_upgrade_title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_upgrade = LuneUiTheme.button("", "HudPrimary", upgrade_row)
_upgrade.size_flags_vertical = Control.SIZE_SHRINK_CENTER
_upgrade.pressed.connect(func() -> void: command_requested.emit({"type": "upgrade", "uid": uid}))
_upgrade_delta = LuneUiTheme.label("", "HudMuted", _upgrade_box)
_upgrade_delta.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_lane_row = HBoxContainer.new()
_lane_row.add_theme_constant_override("separation", 4)
column.add_child(_lane_row)
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", _lane_row)
button.toggle_mode = true
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.pressed.connect(func() -> void:
command_requested.emit({"type": "assign", "uid": uid, "lane": lane}))
_lanes[lane] = button
LuneTooltip.attach(button, func() -> Dictionary: return {"title": "Einsatz: %s" % title,
"text": GROTTO_WARNING if lane == LuneMatch.GROTTO else "Kämpft in der nächsten Welle auf der %s-Lane." % LuneMap.LANE_NAMES[lane]})
_warning = LuneUiTheme.label(GROTTO_WARNING, "HudText", column)
_warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_warning.add_theme_color_override("font_color", LuneUiTheme.WARN)
var sell_row := HBoxContainer.new()
sell_row.add_theme_constant_override("separation", 8)
column.add_child(sell_row)
_sell = LuneUiTheme.button("", "HudButton", sell_row)
_sell.pressed.connect(func() -> void: command_requested.emit({"type": "sell", "uid": uid}))
_sell_note = LuneUiTheme.label("", "HudMuted", sell_row)
_sell_note.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_sell_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_locked = LuneUiTheme.label("", "HudMuted", column)
_locked.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
## entry: own roster entry (uid, type_id, lane, invested, fresh); live: LuneUnit in combat or null.
func apply(entry: Dictionary, model: LuneHudModel, live: LuneUnit) -> void:
uid = str(entry.uid)
var type_id: String = str(entry.type_id)
var data: Dictionary = model.troop(type_id)
var planning: bool = model.planning()
var locked: bool = model.locked()
var gold: int = model.own_gold()
var signature: String = "%s|%s|%s|%d|%s|%s|%d" % [str(entry), planning, locked, gold, model.state.grotto_available(), live != null, model.state.round_number]
if live != null:
_live.fraction = live.hp / maxf(1.0, live.max_hp) if live.alive else 0.0
_live.fill = LuneHpBar.health_color(_live.fraction, LuneUiTheme.team_color(model.team()))
_live.visible = live != null
if signature == _signature:
return
_signature = signature
_portrait.show_model(type_id)
_name.text = str(data.display_name)
var lane_text: String = "Grotte" if entry.lane == LuneMatch.GROTTO else LuneMap.LANE_NAMES[entry.lane]
_sub.text = "%s · Stufe %s · %s" % [LuneStats.role_name(data.role), "II" if int(data.stage) == 2 else "I", lane_text]
if live != null and not live.alive:
_sub.text += " · gefallen"
for child: Node in _stats.get_children():
child.free()
var rows: Array = LuneStats.short_rows(data)
for row: Array in rows.slice(0, 4):
LuneGlyph.make(str(row[0]), LuneUiTheme.MUTED, 14.0, _stats)
var cell := LuneUiTheme.label(str(row[1]), "HudText", _stats)
cell.size_flags_horizontal = Control.SIZE_EXPAND_FILL
cell.clip_text = true
cell.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
var attack_type: int = int(data.attack_type)
_types.text = "Rüstung %s · Angriff %s, stark gegen %s" % [DataCatalog.ARMOR_NAMES[int(data.armor_type)], DataCatalog.DAMAGE_NAMES[attack_type], LuneStats.strong_against(attack_type)]
_ability.text = str(data.get("ability_description", ""))
var upgrade_id: String = str(data.get("upgrade_id", ""))
var can_act: bool = planning and not locked
if upgrade_id.is_empty():
_upgrade_title.text = "Höchste Stufe erreicht."
_upgrade.visible = false
_upgrade_delta.text = ""
else:
var next: Dictionary = DataCatalog.get_unit(upgrade_id)
var cost: int = int(next.cost)
_upgrade_title.text = "Aufwerten → %s" % next.display_name
_upgrade.visible = true
_upgrade.text = "↑ %d" % cost
_upgrade.disabled = not can_act or gold < cost
var delta: String = "Leben %s → %s · Schaden %s → %s" % [LuneUiTheme.number(data.max_hp), LuneUiTheme.number(next.max_hp), LuneUiTheme.decimal(data.damage), LuneUiTheme.decimal(next.damage)]
if not is_equal_approx(float(data.attack_range), float(next.attack_range)):
delta += " · Reichweite %s → %s m" % [LuneUiTheme.decimal(data.attack_range), LuneUiTheme.decimal(next.attack_range)]
if can_act and gold < cost:
delta += " · es fehlen %d Gold" % (cost - gold)
_upgrade_delta.text = delta
var fresh: bool = bool(entry.get("fresh", false))
var refund: int = int(entry.invested) if fresh else floori(float(entry.invested) * model.state.balance.sell_refund)
_sell.text = "Zurückgeben +%d" % refund
_sell.disabled = not can_act
_sell.visible = planning
_sell_note.text = "voller Preis" if fresh else "%d %% von %d" % [int(round(model.state.balance.sell_refund * 100.0)), int(entry.invested)]
_sell_note.visible = planning
var grotto_open: bool = model.state.grotto_available()
for lane: String in _lanes:
var button: Button = _lanes[lane]
button.set_pressed_no_signal(str(entry.lane) == lane)
button.disabled = not can_act or (lane == LuneMatch.GROTTO and not grotto_open)
_warning.visible = str(entry.lane) == LuneMatch.GROTTO
if not planning:
_locked.text = "Im Kampf gibt es keine Befehle; Aufwertung, Rückgabe und Einsatz wieder in der Planung."
elif locked:
_locked.text = "Bereit gemeldet: Die Planung ist verbindlich."
elif not grotto_open:
_locked.text = "Grotte gesperrt bis Welle %d." % int(model.state.boss.next_available_round)
else:
_locked.text = ""
_locked.visible = not _locked.text.is_empty()
func _stats_tip() -> Dictionary:
var entry_type: String = ""
if _portrait != null:
entry_type = _portrait.model_id
if entry_type.is_empty():
return {}
var data: Dictionary = DataCatalog.get_unit(entry_type)
return {"title": str(data.display_name), "stats": LuneStats.rows(data, true),
"note": "Ein Angriff pro Sekunde mal Tempo; Schaden je Treffer vor Rüstung. Stark: 130 %, schwach: 75 % (Rüstungsmatrix)."}