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>
197 lines
9.3 KiB
GDScript
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)."}
|