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

1058 lines
45 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends Control
## Champions of Luneraria in-match HUD (G3). Presentation only: it reads the match through
## LuneHudModel (the privacy filter) and sends the human player's commands through LuneGame;
## no UI state creates ownership or changes a rule.
##
## Layout in logical pixels (1706×960 at 16:9, UI scale applied on top):
## top match status: own Ancient, WELLE n and phase, enemy Ancient; options and menu
## left lanes (threat, towers, players, announced dangers), own team, event feed
## bottom gold, unit cards or attack champions, own army, hero
## right context only: boss grotto, next wave, the selected unit
## corner Bereit (planning) or pause and tempo (combat) above the minimap
## Updates follow changes: every component compares a signature and redraws only when its
## data changed; the HUD polls at 10 Hz in planning and 5 Hz in combat, the minimap at 10 Hz.
signal command_requested(command: Dictionary)
signal undo_requested
signal ready_requested
signal starter_requested
signal speed_requested(value: int)
signal pause_requested
signal lane_focus_requested(lane: String)
signal map_focus_requested(point: Vector3)
signal camera_requested(target: String)
signal menu_requested
signal rematch_requested
signal settings_requested
const Minimap = preload("res://scripts/luneraria/ui/lune_minimap.gd")
const MARGIN: float = 12.0
const LEFT_WIDTH: float = 312.0
const RIGHT_WIDTH: float = 322.0
const MAP_EDGE: float = 224.0
const READY_HEIGHT: float = 84.0
const READY_COMBAT: float = 54.0
const BAR_PLANNING: float = 176.0
const BAR_COMBAT: float = 128.0
## Smallest logical area the layout is built for; the UI scale never shrinks below it.
const MIN_AREA := Vector2(1560, 880)
const LANE_KEYS: Dictionary = {"top": "F2", "mid": "F3", "bot": "F4"}
const PLANNING_RATE: float = .1
const COMBAT_RATE: float = .2
const MAP_RATE: float = .1
const FEED_RATE: float = .25
var match_state: LuneMatch
var human_slot: int = 1
## Optional LuneSession (network display only: seats, connection).
var session: Node
var model: LuneHudModel
var minimap: Control
var tooltip: LuneTooltip
var ui_scale: float = 1.0
var hero_card: LuneHeroCard
var hero_detail: LuneHeroDetail
var champion_panel: LuneChampionPanel
var boss_card: LuneBossIndicator
var next_wave: LuneNextWave
var unit_detail: LuneUnitDetail
var feed: LuneEventFeed
var lanes: Dictionary = {}
var selected_uid: String = ""
## Counters for tests and the performance log.
var syncs: int = 0
var sync_usec: float = 0.0
var _top: PanelContainer
var _ancients: Array[Dictionary] = []
var _wave: Label
var _phase_badge: LuneStatusBadge
var _system: HBoxContainer
var _left: VBoxContainer
var _team: LuneTeamPanel
var _right: VBoxContainer
var _bottom: PanelContainer
var _gold: LuneResourceDisplay
var _undo: Button
var _starter: Button
var _purchase: VBoxContainer
var _purchase_tabs: LuneTabBar
var _troop_row: HBoxContainer
var _shop_buttons: Dictionary = {}
var _army: VBoxContainer
var _army_title: Label
var _army_lanes: Label
var _strip: HFlowContainer
var _chips: Dictionary = {}
var _chip_signature: String = "@"
var _ready_box: PanelContainer
var _ready_button: Button
var _ready_note: Label
var _ready_check: LuneGlyph
var _combat_row: HBoxContainer
var _clock: Label
var _auto: Label
## Bereit ignores clicks and Space for a moment after a combat ended (stale clicks).
var _ready_block_until: int = 0
var _pause: Button
var _speed: Button
var _map_panel: PanelContainer
var _banners: VBoxContainer
var _banner_signature: String = "@"
var _status: PanelContainer
var _report: Label
var _toast: Label
var _toast_left: float = 0.0
var _modal: LuneModalPanel
var _over: Control
var _over_title: Label
var _over_body: Label
var _tick: float = 0.0
var _map_tick: float = 0.0
var _feed_tick: float = 0.0
var _dirty: bool = true
var _last_phase: int = -1
var _last_round: int = -1
var _layout_phase: int = -1
var _known_alive: Dictionary = {}
var _ancient_level: Array[int] = [0, 0]
var _boss_seen: Dictionary = {}
var _hero_level: int = 0
# ---------------------------------------------------------------- build
func _ready() -> void:
theme = LuneUiTheme.shared()
mouse_filter = Control.MOUSE_FILTER_IGNORE
model = LuneHudModel.new(match_state, human_slot, session)
_build_top()
_build_system()
_build_left()
_build_right()
_build_bottom()
_build_ready()
_build_map()
_build_banner()
_build_status()
_build_modal()
_build_over()
tooltip = LuneTooltip.new()
add_child(tooltip)
# Mouse wheel over any HUD panel must not zoom the world (Godot passes scroll events on by default).
for node: Node in find_children("*", "Control", true, false):
var control := node as Control
if control.mouse_filter == Control.MOUSE_FILTER_STOP:
control.mouse_force_pass_scroll_events = false
get_viewport().size_changed.connect(_fit)
Preferences.changed.connect(_fit)
_fit()
_remember_buildings()
_hero_level = _own_hero_level()
func _panel(variation: String = "HudPanel") -> PanelContainer:
var panel := PanelContainer.new()
panel.theme_type_variation = variation
panel.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(panel)
return panel
func _box(vertical: bool, separation: int, parent: Node) -> BoxContainer:
var box: BoxContainer = VBoxContainer.new() if vertical else HBoxContainer.new()
box.add_theme_constant_override("separation", separation)
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(box)
return box
func _spacer(parent: Node) -> Control:
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(spacer)
return spacer
func _build_top() -> void:
_top = _panel()
var row: HBoxContainer = _box(false, 20, _top)
for side: int in range(2):
var block: Dictionary = _ancient_block(side == 0)
_ancients.append(block)
if side == 0:
row.add_child(block.box)
var center: VBoxContainer = _box(true, 2, row)
center.custom_minimum_size.x = 190
center.alignment = BoxContainer.ALIGNMENT_CENTER
_wave = LuneUiTheme.label("WELLE 1", "HudWave", center)
_wave.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_phase_badge = LuneStatusBadge.new()
_phase_badge.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(_phase_badge)
LuneTooltip.attach(_top, _wave_tip)
else:
row.add_child(block.box)
_top.minimum_size_changed.connect(_layout)
## Ancient block: team emblem, name, HP bar and value, the two base towers, critical badge.
func _ancient_block(own: bool) -> Dictionary:
var box := HBoxContainer.new()
box.add_theme_constant_override("separation", 10)
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
box.custom_minimum_size.x = 300
var emblem := LuneGlyph.make("emblem_0", LuneUiTheme.TEAM[0], 36.0)
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 3)
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
column.mouse_filter = Control.MOUSE_FILTER_IGNORE
if own:
box.add_child(emblem)
box.add_child(column)
else:
box.add_child(column)
box.add_child(emblem)
var head := HBoxContainer.new()
head.add_theme_constant_override("separation", 6)
head.mouse_filter = Control.MOUSE_FILTER_IGNORE
column.add_child(head)
var caption := LuneUiTheme.label("", "HudCaps")
var badge := LuneStatusBadge.new()
badge.visible = false
if own:
head.add_child(caption)
_spacer(head)
head.add_child(badge)
else:
head.add_child(badge)
_spacer(head)
head.add_child(caption)
var bar := LuneHpBar.new()
bar.custom_minimum_size = Vector2(240, 10)
bar.ticks = 4
column.add_child(bar)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 5)
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
column.add_child(row)
var value := LuneUiTheme.label("", "HudValue")
var base_label := LuneUiTheme.label("BASIS", "HudMuted")
var towers: Array[LuneTowerIndicator] = []
for index: int in range(2):
var tower := LuneTowerIndicator.new()
tower.custom_minimum_size = Vector2(18, 22)
tower.tier = "base"
towers.append(tower)
if own:
row.add_child(value)
_spacer(row)
row.add_child(base_label)
for tower: LuneTowerIndicator in towers:
row.add_child(tower)
else:
for tower: LuneTowerIndicator in towers:
row.add_child(tower)
row.add_child(base_label)
_spacer(row)
row.add_child(value)
var block: Dictionary = {"box": box, "emblem": emblem, "caption": caption, "badge": badge, "bar": bar, "value": value,
"towers": towers, "pulse": 0.0, "own": own, "data": {}}
LuneTooltip.attach(box, _ancient_tip.bind(block))
for index: int in range(2):
LuneTooltip.attach(towers[index], _base_tip.bind(block, index))
return block
func _build_system() -> void:
_system = HBoxContainer.new()
_system.add_theme_constant_override("separation", 6)
_system.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_system)
# Pause and tempo live up here, far from Bereit: a late click at the end of a combat must
# never land on Bereit (found in the G3 playtest).
_pause = _icon_button("pause", "P", _system)
_pause.visible = false
_pause.pressed.connect(func() -> void: pause_requested.emit())
LuneTooltip.attach(_pause, func() -> Dictionary: return {"title": "Pause", "text": "Hält die Simulation an (offline). Im Netzspiel bestimmt der Host.", "note": "P oder Leertaste"})
_speed = LuneUiTheme.button("1×", "HudIconButton", _system)
_speed.custom_minimum_size = Vector2(46, 36)
_speed.visible = false
_speed.pressed.connect(func() -> void: speed_requested.emit(0))
LuneTooltip.attach(_speed, func() -> Dictionary: return {"title": "Tempo", "text": "Wechselt 1× · 2× · 4×; die Regeln bleiben gleich."})
var options := _icon_button("gear", "F1", _system)
options.pressed.connect(func() -> void: settings_requested.emit())
LuneTooltip.attach(options, func() -> Dictionary: return {"title": "Optionen", "text": "Lautstärke, Vollbild, Bewegung reduzieren, UI-Größe und alle Tasten.", "note": "F1"})
var menu := _icon_button("menu", "Menü", _system)
menu.pressed.connect(func() -> void: menu_requested.emit())
LuneTooltip.attach(menu, func() -> Dictionary: return {"title": "Hauptmenü", "text": "Verlässt die Partie."})
_system.minimum_size_changed.connect(_layout)
func _icon_button(glyph: String, text_value: String, parent: Node) -> Button:
var button := LuneUiTheme.button("", "HudIconButton", parent)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 5)
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
row.alignment = BoxContainer.ALIGNMENT_CENTER
button.add_child(row)
LuneGlyph.make(glyph, LuneUiTheme.TEXT, 15.0, row)
var label := LuneUiTheme.label(text_value, "HudText", row)
button.custom_minimum_size = Vector2(maxf(40.0, 30.0 + text_value.length() * 8.5), 36)
button.set_meta("label", label)
return button
func _build_left() -> void:
_left = _box(true, 8, self) as VBoxContainer
var lane_panel := PanelContainer.new()
lane_panel.theme_type_variation = "HudPanel"
_left.add_child(lane_panel)
var column: VBoxContainer = _box(true, 5, lane_panel)
var head: HBoxContainer = _box(false, 6, column)
LuneUiTheme.label("LANES", "HudCaps", head)
_spacer(head)
LuneUiTheme.label("Klick oder F2–F4", "HudMuted", head)
for lane: String in LuneMap.LANES:
var indicator := LuneLaneIndicator.new(lane, LANE_KEYS[lane])
column.add_child(indicator)
indicator.pressed.connect(func() -> void: lane_focus_requested.emit(lane))
lanes[lane] = indicator
var team_panel := PanelContainer.new()
team_panel.theme_type_variation = "HudPanel"
_left.add_child(team_panel)
_team = LuneTeamPanel.new()
team_panel.add_child(_team)
feed = LuneEventFeed.new()
_left.add_child(feed)
feed.focus_requested.connect(_focus_target)
func _build_right() -> void:
_right = _box(true, 8, self) as VBoxContainer
boss_card = LuneBossIndicator.new()
_right.add_child(boss_card)
boss_card.focus_requested.connect(func() -> void: camera_requested.emit("grotto"))
boss_card.lane_chosen.connect(func(lane: String) -> void: command_requested.emit({"type": "boss_lane", "lane": lane}))
next_wave = LuneNextWave.new()
_right.add_child(next_wave)
unit_detail = LuneUnitDetail.new()
unit_detail.visible = false
_right.add_child(unit_detail)
unit_detail.command_requested.connect(func(command: Dictionary) -> void: command_requested.emit(command))
unit_detail.closed.connect(clear_selection)
func _build_bottom() -> void:
_bottom = _panel()
var row: HBoxContainer = _box(false, 14, _bottom)
# Gold and undo.
var gold_column: VBoxContainer = _box(true, 4, row)
gold_column.custom_minimum_size.x = 150
LuneUiTheme.label("GOLD", "HudCaps", gold_column)
_gold = LuneResourceDisplay.new()
gold_column.add_child(_gold)
LuneTooltip.attach(_gold, _gold_tip)
_undo = LuneUiTheme.button("Rückgängig", "HudButton", gold_column)
_undo.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_undo.pressed.connect(func() -> void: undo_requested.emit())
LuneTooltip.attach(_undo, func() -> Dictionary: return {"title": "Rückgängig", "text": "Nimmt den letzten Befehl dieser Planung zurück (Kauf, Einsatz, Item, Buchung)."})
# Purchases: troops (1–5) or attack champions.
_purchase = _box(true, 4, row) as VBoxContainer
_purchase_tabs = LuneTabBar.new()
_purchase.add_child(_purchase_tabs)
_purchase_tabs.add_tab("TRUPPEN · 1–5", "Dauerhafte Truppen", "Bleiben in deiner Armee, kämpfen jede Welle, lassen sich aufwerten.")
_purchase_tabs.add_tab("ANGRIFFSCHAMPIONS", "Angriffschampions", "Einmalige Söldner: jetzt kaufen, eine Welle später auf der Ziel-Lane angreifen, danach weg.")
_purchase_tabs.tab_changed.connect(_show_purchase)
_troop_row = _box(false, 6, _purchase) as HBoxContainer
for index: int in range(5):
var card := LuneUnitCard.new()
_troop_row.add_child(card)
champion_panel = LuneChampionPanel.new()
champion_panel.visible = false
_purchase.add_child(champion_panel)
champion_panel.command_requested.connect(func(command: Dictionary) -> void: command_requested.emit(command))
# Own army, ordered by lane.
_army = _box(true, 4, row) as VBoxContainer
_army.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_army.resized.connect(mark_dirty)
var head: HBoxContainer = _box(false, 8, _army)
_army_title = LuneUiTheme.label("ARMEE", "HudCaps", head)
_army_lanes = LuneUiTheme.label("", "HudMuted", head)
_army_lanes.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_army_lanes.clip_text = true
_strip = HFlowContainer.new()
_strip.add_theme_constant_override("h_separation", 4)
_strip.add_theme_constant_override("v_separation", 4)
_strip.mouse_filter = Control.MOUSE_FILTER_IGNORE
_army.add_child(_strip)
_starter = LuneUiTheme.button("Startaufstellung", "HudButton", _army)
_starter.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_starter.pressed.connect(func() -> void: starter_requested.emit())
LuneTooltip.attach(_starter, _starter_tip)
hero_card = LuneHeroCard.new()
row.add_child(hero_card)
hero_card.pressed.connect(func() -> void: open_hero())
func _build_ready() -> void:
_ready_box = _panel("HudPanelTight")
var column: VBoxContainer = _box(true, 2, _ready_box)
_ready_button = LuneUiTheme.button("BEREIT", "PrimaryButton", column)
_ready_button.custom_minimum_size.y = 44
_ready_button.pressed.connect(func() -> void:
if not ready_blocked():
ready_requested.emit())
LuneTooltip.attach(_ready_button, _ready_tip)
_ready_check = LuneGlyph.make("check", SaumwachtTheme.INK, 22.0, _ready_button)
_ready_check.set_anchors_and_offsets_preset(Control.PRESET_CENTER_LEFT)
_ready_check.position.x = 16
_ready_check.visible = false
_ready_note = LuneUiTheme.label("Leertaste", "HudMuted", column)
_ready_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_combat_row = _box(false, 8, column) as HBoxContainer
_combat_row.visible = false
_combat_row.alignment = BoxContainer.ALIGNMENT_CENTER
LuneGlyph.make("melee", LuneUiTheme.DANGER.lightened(.15), 16.0, _combat_row)
_clock = LuneUiTheme.label("", "HudValue", _combat_row)
_auto = LuneUiTheme.label("", "HudMuted", _combat_row)
_auto.clip_text = true
_auto.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
func _build_map() -> void:
_map_panel = _panel("HudPanelTight")
minimap = Minimap.new()
minimap.match_state = match_state
_map_panel.add_child(minimap)
minimap.focus_requested.connect(func(point: Vector3) -> void: map_focus_requested.emit(point))
LuneTooltip.attach(minimap, func() -> Dictionary: return {"title": "Minimap", "text": "Klicken oder ziehen bewegt die Kamera. Runde Marker: Blau · eckige Marker: Rot. Krone: Boss, Pfeil: Angriffschampion, Goldring: Held.", "note": "Die Minimap löst nie Käufe oder Befehle aus."})
func _build_banner() -> void:
_banners = _box(true, 6, self) as VBoxContainer
func _build_status() -> void:
_status = _panel("HudPanelTight")
var column: VBoxContainer = _box(true, 0, _status)
_report = LuneUiTheme.label("", "HudText", column)
_report.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_toast = LuneUiTheme.label("", "HudMuted", column)
_toast.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_status.visible = false
_status.minimum_size_changed.connect(_layout)
func _build_modal() -> void:
_modal = LuneModalPanel.new(Vector2(1040, 660))
add_child(_modal)
hero_detail = LuneHeroDetail.new()
_modal.body.add_child(hero_detail)
hero_detail.command_requested.connect(func(command: Dictionary) -> void: command_requested.emit(command))
func _build_over() -> void:
_over = ColorRect.new()
(_over as ColorRect).color = Color(.02, .03, .04, .82)
_over.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_over.mouse_filter = Control.MOUSE_FILTER_STOP
_over.visible = false
add_child(_over)
var center := CenterContainer.new()
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_over.add_child(center)
var panel := PanelContainer.new()
panel.theme_type_variation = "MenuPanel"
panel.custom_minimum_size = Vector2(620, 0)
center.add_child(panel)
var column: VBoxContainer = _box(true, 14, panel)
_over_title = LuneUiTheme.label("", "TitleLabel", column, 60)
_over_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_over_body = LuneUiTheme.label("", "BodyLabel", column)
_over_body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_over_body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
var row: HBoxContainer = _box(false, 12, column)
row.alignment = BoxContainer.ALIGNMENT_CENTER
LuneUiTheme.button("NEUE PARTIE", "PrimaryButton", row).pressed.connect(func() -> void: rematch_requested.emit())
LuneUiTheme.button("Menü", "Button", row).pressed.connect(func() -> void: menu_requested.emit())
# ---------------------------------------------------------------- layout
## UI scale: the preference, capped so the layout always has at least MIN_AREA logical pixels.
func _fit() -> void:
var area: Vector2 = get_viewport().get_visible_rect().size
var wanted: float = clampf(float(Preferences.get("ui_scale") if Preferences.get("ui_scale") != null else 1.0), .75, 1.5)
var fits: float = minf(area.x / MIN_AREA.x, area.y / MIN_AREA.y)
ui_scale = clampf(minf(wanted, fits), .6, 1.5)
position = Vector2.ZERO
scale = Vector2(ui_scale, ui_scale)
size = area / ui_scale
_layout()
func _layout() -> void:
if _over == null:
return
var w: float = size.x
var h: float = size.y
_top.size = _top.get_combined_minimum_size()
_top.position = Vector2(floorf((w - _top.size.x) * .5), MARGIN)
_system.size = _system.get_combined_minimum_size()
_system.position = Vector2(w - MARGIN - _system.size.x, MARGIN)
_left.position = Vector2(MARGIN, MARGIN)
_left.size = Vector2(LEFT_WIDTH, 0)
var box: float = MAP_EDGE + 16.0
_map_panel.position = Vector2(w - MARGIN - box, h - MARGIN - box)
_map_panel.size = Vector2(box, box)
var planning: bool = match_state == null or match_state.phase == LuneMatch.Phase.PLANNING
var ready_height: float = READY_HEIGHT if planning else READY_COMBAT
_ready_box.position = Vector2(_map_panel.position.x, _map_panel.position.y - 8.0 - ready_height)
_ready_box.size = Vector2(box, ready_height)
var bar: float = maxf(BAR_PLANNING if planning else BAR_COMBAT, _bottom.get_combined_minimum_size().y)
_bottom.position = Vector2(MARGIN, h - MARGIN - bar)
_bottom.size = Vector2(_map_panel.position.x - 10.0 - MARGIN, bar)
_right.position = Vector2(w - MARGIN - RIGHT_WIDTH, MARGIN + _system.size.y + 8.0)
_right.size = Vector2(RIGHT_WIDTH, 0)
_banners.size = Vector2(minf(700.0, w - 2.0 * (LEFT_WIDTH + 2.0 * MARGIN)), 0)
_banners.position = Vector2(floorf((w - _banners.size.x) * .5), _top.position.y + _top.size.y + 8.0)
_status.size = _status.get_combined_minimum_size()
_status.position = Vector2(floorf(_bottom.position.x + (_bottom.size.x - _status.size.x) * .5), _bottom.position.y - 8.0 - _status.size.y)
_layout_phase = match_state.phase if match_state != null else -1
## Screen rectangles of the main regions (layout tests).
func regions() -> Dictionary:
var result: Dictionary = {"top": _top, "system": _system, "left": _left, "right": _right, "bottom": _bottom, "ready": _ready_box, "minimap": _map_panel}
var rects: Dictionary = {}
for key: String in result:
var control: Control = result[key]
if control.visible:
rects[key] = control.get_global_rect()
return rects
# ---------------------------------------------------------------- per frame
func _process(delta: float) -> void:
if match_state == null:
return
_toast_left -= delta
if _toast_left <= 0.0 and not _toast.text.is_empty():
_toast.text = ""
_status_visibility()
for block: Dictionary in _ancients:
if float(block.pulse) > 0.0:
block.pulse = maxf(0.0, float(block.pulse) - delta)
(block.bar as Control).modulate = Color(1, 1, 1, .45 + .55 * absf(cos(float(block.pulse) * PI * 2.0)))
if float(block.pulse) <= 0.0:
(block.bar as Control).modulate = Color.WHITE
_tick += delta
_map_tick += delta
_feed_tick += delta
var fighting: bool = match_state.phase == LuneMatch.Phase.COMBAT
if fighting and _map_tick >= MAP_RATE:
_map_tick = 0.0
minimap.refresh(match_state.announcements())
if _feed_tick >= FEED_RATE:
_track_events()
_feed_tick = 0.0
if _dirty or match_state.phase != _last_phase or _tick >= (COMBAT_RATE if fighting else PLANNING_RATE):
var started: int = Time.get_ticks_usec()
_sync(_tick)
_tick = 0.0
_dirty = false
syncs += 1
sync_usec = lerpf(sync_usec, float(Time.get_ticks_usec() - started), .2)
func mark_dirty() -> void:
_dirty = true
## True for one second after a combat ended: Bereit (click or Space) is ignored meanwhile.
func ready_blocked() -> bool:
return Time.get_ticks_msec() < _ready_block_until
func _sync(delta: float) -> void:
var phase: int = match_state.phase
if phase != _last_phase:
_phase_changed(_last_phase, phase)
_last_phase = phase
if _layout_phase != phase:
_layout()
var planning: bool = model.planning()
var fighting: bool = model.fighting()
_sync_top()
var reduced: bool = Preferences.reduced_motion
for lane: String in lanes:
(lanes[lane] as LuneLaneIndicator).apply(model.lane(lane), model.team(), delta, reduced)
_team.apply(model.members(model.team()), model.team(), planning, model.networked())
_sync_right(planning, fighting)
_sync_bottom(planning, fighting)
_sync_ready(planning, fighting)
_sync_banners()
if not fighting:
minimap.viewer_team = model.team()
minimap.human_slot = human_slot
minimap.refresh(match_state.announcements())
if _modal.visible:
hero_detail.apply(model)
_modal.title.text = hero_detail.title_text(model)
_over.visible = model.over()
if _over.visible:
var own_team: int = model.team()
_over_title.text = "UNENTSCHIEDEN" if match_state.winner == LuneMatch.DRAW else ("SIEG" if match_state.winner == own_team else "NIEDERLAGE")
_over_body.text = "%s\nNach %d Wellen." % [match_state.end_note, match_state.round_number]
func _sync_top() -> void:
for side: int in range(2):
var team: int = model.team() if side == 0 else model.enemy()
var block: Dictionary = _ancients[side]
var ancient: Dictionary = model.ancient(team)
block.data = ancient
var tint: Color = LuneUiTheme.team_color(team)
(block.emblem as LuneGlyph).glyph = "emblem_%d" % team
(block.emblem as LuneGlyph).color = tint
(block.caption as Label).text = ("DEIN ANCIENT · %s" if side == 0 else "GEGNER · %s") % LuneUiTheme.team_name(team).to_upper()
var bar: LuneHpBar = block.bar
bar.fraction = float(ancient.fraction)
bar.fill = LuneHpBar.health_color(bar.fraction, tint)
(block.value as Label).text = "%s / %s" % [LuneUiTheme.number(ancient.hp), LuneUiTheme.number(ancient.max_hp)]
var towers: Array = block.towers
for index: int in range(2):
var base: Dictionary = ancient.base[index]
(towers[index] as LuneTowerIndicator).team = team
(towers[index] as LuneTowerIndicator).set_state(bool(base.alive), float(base.fraction), bool(base.attackable))
var level: int = 2 if bar.fraction < .3 else (1 if bar.fraction < .5 else 0)
var badge: LuneStatusBadge = block.badge
badge.visible = level > 0 or not ancient.alive
if not ancient.alive:
badge.set_state("ZERSTÖRT", LuneUiTheme.DANGER, "close", true)
elif level == 2:
badge.set_state("KRITISCH", LuneUiTheme.DANGER, "warning", true)
elif level == 1:
badge.set_state("BESCHÄDIGT", LuneUiTheme.WARN)
elif bool(ancient.attackable):
badge.visible = true
badge.set_state("ANGREIFBAR", LuneUiTheme.WARN)
_wave.text = "WELLE %d" % match_state.round_number
if model.over():
_phase_badge.set_state("PARTIE BEENDET", LuneUiTheme.MUTED)
elif model.fighting():
var elapsed: int = int(match_state.combat.elapsed)
_phase_badge.set_state("KAMPF · %02d:%02d" % [elapsed / 60, elapsed % 60], LuneUiTheme.DANGER.lightened(.1), "melee")
elif model.locked():
_phase_badge.set_state("PLANUNG · BEREIT", LuneUiTheme.GOOD, "check")
else:
_phase_badge.set_state("PLANUNG", LuneUiTheme.BRONZE_LIGHT, "clock")
func _sync_right(planning: bool, fighting: bool) -> void:
var boss: Dictionary = model.boss()
boss_card.apply(boss, model.team(), match_state.round_number, planning)
if boss.has("hp"):
boss_card.set_boss_hp(float(boss.hp) / maxf(1.0, float(boss.max_hp)))
var entry: Dictionary = _selected_entry()
if entry.is_empty() and not selected_uid.is_empty():
selected_uid = ""
unit_detail.visible = not entry.is_empty()
boss_card.set_compact(unit_detail.visible)
if unit_detail.visible:
unit_detail.apply(entry, model, model.live_unit(selected_uid))
next_wave.visible = not unit_detail.visible and not model.over()
if next_wave.visible:
next_wave.apply(model.next_wave(), model.team(), fighting)
func _selected_entry() -> Dictionary:
if selected_uid.is_empty():
return {}
for entry: Dictionary in model.own_roster():
if str(entry.uid) == selected_uid:
return entry
return {}
func _sync_bottom(planning: bool, fighting: bool) -> void:
_gold.set_values(model.own_gold(), model.income(), not model.over())
var roster: Array = model.own_roster()
var locked: bool = model.locked()
var can_act: bool = planning and not locked
_purchase.visible = planning
_army.visible = not planning or not champion_panel.visible
_undo.visible = planning
_undo.disabled = not can_act or (model.me().undo as Array).is_empty()
_starter.visible = planning and roster.is_empty()
_starter.disabled = not can_act
if planning:
var ids: Array[String] = match_state.base_ids(model.me().bund)
var gold: int = model.own_gold()
var full: bool = roster.size() >= model.supply_cap()
if not _shop_buttons.has(ids[0]):
_shop_buttons.clear()
for index: int in range(ids.size()):
var card: LuneUnitCard = _troop_row.get_child(index)
var id: String = ids[index]
var data: Dictionary = DataCatalog.get_unit(id)
card.setup(id, id, str(data.display_name), model.role_of(id), int(data.cost), str(index + 1))
for connection: Dictionary in card.pressed.get_connections():
card.pressed.disconnect(connection.callable)
card.pressed.connect(func() -> void: command_requested.emit({"type": "buy", "type_id": id}))
if not card.has_meta("tip"):
card.set_meta("tip", true)
LuneTooltip.attach(card, func() -> Dictionary: return LuneStats.troop_tip(card.type_id, model.role_of(card.type_id), card.reason))
_shop_buttons[id] = card
for id: String in _shop_buttons:
var card: LuneUnitCard = _shop_buttons[id]
var why: String = ""
if locked:
why = "Bereit gemeldet – die Planung ist verbindlich."
elif full:
why = "Armee voll (%d/%d)." % [roster.size(), model.supply_cap()]
elif gold < card.cost:
why = "Es fehlen %d Gold." % (card.cost - gold)
card.set_available(why.is_empty(), why, locked, gold < card.cost)
if champion_panel.visible:
champion_panel.apply(model)
_army_title.text = "ARMEE %d/%d" % [roster.size(), model.supply_cap()]
var per_lane: Dictionary = {}
var alive_lane: Dictionary = {}
for entry: Dictionary in roster:
var lane: String = str(entry.lane)
per_lane[lane] = int(per_lane.get(lane, 0)) + 1
if fighting:
var unit: LuneUnit = model.live_unit(str(entry.uid))
if unit != null and unit.alive:
alive_lane[lane] = int(alive_lane.get(lane, 0)) + 1
var parts: PackedStringArray = []
for lane: String in LuneMap.LANES + [LuneMatch.GROTTO]:
if per_lane.has(lane):
var short: String = "GROTTE" if lane == LuneMatch.GROTTO else LuneMap.LANE_NAMES[lane].to_upper()
parts.append(("%s %d/%d" % [short, int(alive_lane.get(lane, 0)), int(per_lane[lane])]) if fighting else ("%s %d" % [short, int(per_lane[lane])]))
_army_lanes.text = " · ".join(parts)
_sync_chips(roster, fighting)
var hero: Dictionary = model.own_hero()
hero_card.apply(hero, model, model.live_unit(str(hero.get("uid", ""))) if not hero.is_empty() else null)
func _sync_chips(roster: Array, fighting: bool) -> void:
var ordered: Array = roster.duplicate()
var order: Array = LuneMap.LANES + [LuneMatch.GROTTO]
ordered.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var la: int = order.find(str(a.lane))
var lb: int = order.find(str(b.lane))
return int(a.seq) < int(b.seq) if la == lb else la < lb)
var edge: float = _chip_edge(ordered.size(), fighting)
var signature: String = "%s|%d" % [str(ordered.map(func(e: Dictionary) -> String: return "%s:%s:%s" % [e.uid, e.type_id, e.lane])), int(edge)]
if signature != _chip_signature:
_chip_signature = signature
for child: Node in _strip.get_children():
child.free()
_chips.clear()
for entry: Dictionary in ordered:
_chips[str(entry.uid)] = _make_chip(entry, edge)
for uid: String in _chips:
var chip: LuneArmyChip = _chips[uid]
chip.set_pressed_no_signal(uid == selected_uid)
if fighting:
var unit: LuneUnit = model.live_unit(uid)
chip.fallen = unit == null or not unit.alive
else:
chip.fallen = false
## Largest chip edge (46 … 30 px) at which the whole army fits into the strip.
func _chip_edge(count: int, fighting: bool) -> float:
var width: float = _army.size.x if _army.size.x > 60.0 else 300.0
var height: float = (BAR_COMBAT if fighting else BAR_PLANNING) - 20.0 - 26.0
for edge: float in [46.0, 42.0, 38.0, 34.0, 30.0]:
var per_row: int = maxi(1, floori((width + 4.0) / (edge + 4.0)))
var rows: int = ceili(float(count) / float(per_row))
if float(rows) * (edge + LuneArmyChip.BAND + 4.0) <= height:
return edge
return 30.0
func _make_chip(entry: Dictionary, edge: float) -> LuneArmyChip:
var chip := LuneArmyChip.new()
_strip.add_child(chip)
chip.setup(entry, edge)
var uid: String = str(entry.uid)
chip.pressed.connect(func() -> void:
if selected_uid == uid:
clear_selection()
else:
select_unit(uid))
var type_id: String = str(entry.type_id)
var lane: String = str(entry.lane)
LuneTooltip.attach(chip, func() -> Dictionary:
var tip: Dictionary = LuneStats.troop_tip(type_id, model.role_of(type_id))
tip.note = "Einsatz: %s · Klick: Details, Aufwerten, Einsatz" % ("Grotte" if lane == LuneMatch.GROTTO else LuneMap.LANE_NAMES[lane])
if chip.fallen:
tip.warn = "In diesem Kampf gefallen; kehrt zur nächsten Planung zurück."
return tip)
return chip
func _sync_ready(planning: bool, fighting: bool) -> void:
_ready_box.visible = not model.over()
_ready_button.visible = planning
_ready_note.visible = planning
_combat_row.visible = fighting
_pause.visible = fighting
_speed.visible = fighting
if planning:
var locked: bool = model.locked()
_ready_button.disabled = locked or ready_blocked()
_ready_button.text = "BEREIT"
_ready_check.visible = locked
if locked:
var count: Vector2i = model.ready_count()
_ready_note.text = "Warte auf %d Mitspieler" % maxi(0, count.y - count.x) if count.y > count.x else "Kampf beginnt …"
else:
_ready_note.text = "Leertaste · startet Welle %d" % match_state.round_number
if fighting:
var elapsed: int = int(match_state.combat.elapsed)
_clock.text = "KAMPF %02d:%02d" % [elapsed / 60, elapsed % 60]
_auto.text = "Tempo %s" % _speed.text
var guest: bool = model.is_guest()
_pause.disabled = guest
_speed.disabled = guest
func _sync_banners() -> void:
var lines: Array = []
var boss: Dictionary = model.boss()
if str(boss.state) in ["deploy_next", "deploying"]:
var own: bool = bool(boss.own)
var lane: String = LuneMap.LANE_NAMES.get(str(boss.lane), "?").to_upper()
var text: String
if own:
text = "Deine SCHLUNDKRONE greift in Welle %d auf %s an · Kopfgeld für den Gegner %s Gold" % [int(boss.deploy_round), lane, LuneUiTheme.number(boss.bounty)]
else:
text = "SCHLUNDKRONE (%s) greift in Welle %d auf %s an · Kopfgeld für euch %s Gold" % [LuneUiTheme.team_name(int(boss.team)), int(boss.deploy_round), lane, LuneUiTheme.number(boss.bounty)]
lines.append([text, "crown", own, {"lane": str(boss.lane)}])
var core: Dictionary = model.ancient(model.team())
if bool(core.alive) and float(core.fraction) < .3:
lines.append(["Dein Ancient ist kritisch: %d %% Leben" % int(round(float(core.fraction) * 100.0)), "warning", false, {"camera": "base"}])
var signature: String = str(lines)
if signature == _banner_signature:
return
_banner_signature = signature
for child: Node in _banners.get_children():
child.free()
for line: Array in lines:
var panel := PanelContainer.new()
panel.theme_type_variation = "HudBannerOwn" if line[2] else "HudBannerDanger"
panel.mouse_filter = Control.MOUSE_FILTER_STOP
panel.mouse_force_pass_scroll_events = false
panel.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
_banners.add_child(panel)
var row: HBoxContainer = _box(false, 10, panel)
row.alignment = BoxContainer.ALIGNMENT_CENTER
LuneGlyph.make(line[1], LuneUiTheme.team_color(model.team()) if line[2] else LuneUiTheme.DANGER, 20.0, row)
var label := LuneUiTheme.label(line[0], "HudValue", row)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var target: Dictionary = line[3]
panel.gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
_focus_target(target))
func _status_visibility() -> void:
_status.visible = not _report.text.is_empty() or not _toast.text.is_empty()
_report.visible = not _report.text.is_empty()
_toast.visible = not _toast.text.is_empty()
# ---------------------------------------------------------------- phase changes and events
func _phase_changed(previous: int, phase: int) -> void:
if phase == LuneMatch.Phase.COMBAT:
close_modal()
_report.text = ""
_status_visibility()
for entry: Dictionary in match_state.announcements():
var own: bool = int(entry.team) == model.team()
var lane: String = LuneMap.LANE_NAMES[str(entry.lane)].to_upper()
if entry.kind == "boss":
feed.push("%s – Schlundkrone (%s) greift an" % [lane, LuneUiTheme.team_name(int(entry.team))], "good" if own else "danger", "crown", {"lane": str(entry.lane)})
else:
feed.push("%s – Angriffschampion %s (%s) erschienen" % [lane, LuneContent.CHAMPIONS[entry.champion_id].name, LuneUiTheme.team_name(int(entry.team))], "info" if own else "warn", "champion", {"lane": str(entry.lane)})
elif phase == LuneMatch.Phase.PLANNING and previous == LuneMatch.Phase.COMBAT:
_ready_block_until = Time.get_ticks_msec() + 1000
_report_settlement()
elif phase == LuneMatch.Phase.PLANNING and previous < 0 and not match_state.last_settlement.is_empty():
_report_settlement()
_boss_seen.clear()
_layout()
func _report_settlement() -> void:
var s: Dictionary = match_state.last_settlement
if s.is_empty():
return
var gained: int = int((s.gains as Array)[human_slot]) + int(s.income)
_report.text = "Welle %d abgeschlossen (%s, %ds) · dein Ertrag +%d Gold (Kills %d, Einkommen %d)" % [int(s.round), _reason(str(s.end_reason)), int(s.combat_time), gained, int((s.gains as Array)[human_slot]), int(s.income)]
_status_visibility()
feed.push("Welle %d abgeschlossen · +%d Gold" % [int(s.round), gained], "info", "coin")
var claim: Dictionary = match_state.boss.claim
if str(s.get("boss", "")) == "won" and not claim.is_empty():
var own: bool = int(claim.team) == model.team()
feed.push("Boss in Welle %d auf %s (%s)" % [int(claim.deploy_round), LuneMap.LANE_NAMES[claim.lane].to_upper(), LuneUiTheme.team_name(int(claim.team))], "good" if own else "danger", "crown", {"lane": str(claim.lane)})
var level: int = _own_hero_level()
if level > _hero_level and _hero_level > 0:
var hero: Dictionary = model.own_hero()
feed.push("%s erreicht Stufe %d" % [LuneContent.HEROES[hero.hero_id].name, level], "good", "star", {"camera": "hero"})
_hero_level = level
func _own_hero_level() -> int:
var hero: Dictionary = model.own_hero()
return 0 if hero.is_empty() else LuneContent.level_for(int(hero.xp))
func _remember_buildings() -> void:
_known_alive.clear()
for definition: Dictionary in match_state.map.buildings:
_known_alive[definition.id] = model.building(definition.id).alive
## Diffs public state for the event feed: fallen buildings, Ancient thresholds, the boss.
func _track_events() -> void:
if model.over() and _known_alive.is_empty():
return
for definition: Dictionary in match_state.map.buildings:
var id: String = definition.id
var alive: bool = model.building(id).alive
if bool(_known_alive.get(id, alive)) and not alive:
var own: bool = int(definition.team) == model.team()
var place: String = "BASIS" if str(definition.lane) == "base" else LuneMap.LANE_NAMES[str(definition.lane)].to_upper()
var title: String = LuneHudModel.building_title(id)
feed.push("%s – %s (%s) zerstört" % [place, title, LuneUiTheme.team_name(int(definition.team))], "danger" if own else "good", "tower", {"point": definition.position})
_known_alive[id] = alive
for side: int in range(2):
var team: int = model.team() if side == 0 else model.enemy()
var core: Dictionary = model.ancient(team)
var level: int = 2 if float(core.fraction) < .3 else (1 if float(core.fraction) < .5 else 0)
if level > _ancient_level[side] and bool(core.alive):
if level == 2:
feed.push("Dein Ancient kritisch (%d %%)" % int(round(float(core.fraction) * 100.0)) if side == 0 else "Gegnerischer Ancient kritisch (%d %%)" % int(round(float(core.fraction) * 100.0)),
"danger" if side == 0 else "good", "warning", {"camera": "base" if side == 0 else "enemy_base"})
if not Preferences.reduced_motion:
_ancients[side].pulse = 1.5
_ancient_level[side] = level
if model.fighting():
for unit: LuneUnit in match_state.combat.units:
if unit.kind != "boss":
continue
var key: String = "%s-%d" % [unit.zone, unit.id]
var was: bool = bool(_boss_seen.get(key, unit.alive))
if was and not unit.alive:
if unit.zone == "grotto":
var boss: Dictionary = model.boss()
if int(boss.team) >= 0:
feed.push("Bossgrotte erobert – %s" % LuneUiTheme.team_name(int(boss.team)), "good" if bool(boss.own) else "warn", "crown", {"camera": "grotto"})
else:
feed.push("SCHLUNDKRONE besiegt (%s)" % LuneMap.LANE_NAMES.get(unit.lane, "").to_upper(), "good" if unit.team != model.team() else "warn", "crown", {"lane": unit.lane})
_boss_seen[key] = unit.alive
func _focus_target(target: Dictionary) -> void:
if target.has("lane") and LuneMap.LANES.has(str(target.lane)):
lane_focus_requested.emit(str(target.lane))
elif target.has("point"):
var point: Vector2 = target.point
map_focus_requested.emit(Vector3(point.x, 0, point.y))
elif target.has("camera"):
camera_requested.emit(str(target.camera))
func _reason(reason: String) -> String:
return {"cleared": "alle Einheiten gefallen", "timeout": "Zeitfenster abgelaufen", "core": "Ancient zerstört"}.get(reason, reason)
# ---------------------------------------------------------------- public API
func notify(message: String, error: bool = false) -> void:
_toast.text = message
_toast.add_theme_color_override("font_color", LuneUiTheme.DANGER if error else LuneUiTheme.GOOD)
_toast_left = 5.0
_status_visibility()
_dirty = true
func set_speed_label(value: int, paused: bool) -> void:
_speed.text = "%d×" % value
var label: Label = _pause.get_meta("label")
label.text = "P"
var glyph: LuneGlyph = _pause.get_child(0).get_child(0)
glyph.glyph = "play" if paused else "pause"
## G2 compatibility: 0 = troops, 1 = hero, 2 = attack champions.
func show_page(index: int) -> void:
match index:
1:
open_hero()
2:
close_modal()
open_champions()
_:
close_modal()
open_troops()
func open_hero(tab: int = LuneHeroDetail.TAB_VALUES) -> void:
hero_detail.tab = tab
hero_detail.show_tab(tab)
hero_detail.apply(model)
_modal.open(hero_detail.title_text(model))
func open_items() -> void:
open_hero(LuneHeroDetail.TAB_ITEMS)
func open_talents() -> void:
open_hero(LuneHeroDetail.TAB_TALENTS)
func close_modal() -> void:
_modal.close()
func modal_open() -> bool:
return _modal.visible
func open_champions() -> void:
_purchase_tabs.select(1)
_show_purchase(1)
func open_troops() -> void:
_purchase_tabs.select(0)
_show_purchase(0)
func _show_purchase(index: int) -> void:
_troop_row.visible = index == 0
champion_panel.visible = index == 1
_army.visible = index == 0
_purchase.size_flags_horizontal = Control.SIZE_EXPAND_FILL if index == 1 else Control.SIZE_FILL
_dirty = true
func select_unit(uid: String) -> void:
selected_uid = uid
_dirty = true
func clear_selection() -> void:
selected_uid = ""
_dirty = true
func public_digest() -> Dictionary:
return model.public_digest()
func _unhandled_key_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not selected_uid.is_empty() and not _modal.visible:
clear_selection()
get_viewport().set_input_as_handled()
# ---------------------------------------------------------------- tooltips
func _wave_tip() -> Dictionary:
var wave: Dictionary = model.next_wave()
return {"title": "Welle %d" % match_state.round_number, "glyph": "clock",
"text": "Wellen sind unbegrenzt: Die Partie endet nur, wenn ein Ancient fällt. Monster werden jede Runde stärker, alle drei Runden kommt eines je Lane hinzu (bis 7).",
"note": "Kommende Welle %d: %d Monster je Lane." % [int(wave.round), int(wave.count)]}
func _ancient_tip(block: Dictionary) -> Dictionary:
var ancient: Dictionary = block.data
if ancient.is_empty():
return {}
var team: int = int(ancient.team)
var standing: int = int(ancient.base[0].alive) + int(ancient.base[1].alive)
return {"title": "Ancient %s · %s" % [LuneUiTheme.team_name(team), LuneUiTheme.TEAM_EMBLEMS[team]], "glyph": "emblem_%d" % team, "glyph_color": LuneUiTheme.team_color(team),
"stats": [["heart", "Leben", "%s / %s" % [LuneUiTheme.number(ancient.hp), LuneUiTheme.number(ancient.max_hp)]], ["tower", "Basistürme", "%d von 2 stehen" % standing]],
"text": "Fällt ein Ancient, ist die Partie vorbei. Er ist erst angreifbar, wenn beide Basistürme (Kernwacht) gefallen sind; die fallen erst, wenn eine Lane ganz offen ist.",
"note": "B: Kamera zum eigenen Ancient" if bool(block.own) else ""}
func _base_tip(block: Dictionary, index: int) -> Dictionary:
var ancient: Dictionary = block.data
if ancient.is_empty():
return {}
var base: Dictionary = ancient.base[index]
return {"title": "Kernwacht %d · %s" % [index + 1, LuneUiTheme.team_name(int(base.team))], "glyph": "tower", "glyph_color": LuneUiTheme.team_color(int(base.team)),
"stats": [["heart", "Leben", LuneLaneIndicator._tower_value(base)]],
"text": "Basisturm vor dem Ancient. Angreifbar, sobald auf einer Lane Grenzwacht und Bastion gefallen sind." if base.alive else "Zerstört."}
func _gold_tip() -> Dictionary:
return {"title": "Dein Gold", "glyph": "coin", "glyph_color": LuneUiTheme.GOLD,
"stats": [["coin", "Jetzt", LuneUiTheme.number(model.own_gold())], ["clock", "Einkommen", "+%s nach dieser Welle" % LuneUiTheme.number(model.income())]],
"text": "Dazu kommt Gold für besiegte Monster, Champions, Gebäude und den Boss auf der eigenen Seite (Teile gehen ans Team).",
"note": "Das Gold der anderen Spieler bleibt privat."}
func _starter_tip() -> Dictionary:
var ids: Array[String] = match_state.base_ids(model.me().bund)
var cost: int = 0
var names: PackedStringArray = []
for index: int in [1, 0, 2]:
cost += int(DataCatalog.get_unit(ids[index]).cost)
names.append(str(DataCatalog.get_unit(ids[index]).display_name))
return {"title": "Startaufstellung · %d Gold" % cost, "text": "Kauft %s für deine Lane." % ", ".join(names)}
func _ready_tip() -> Dictionary:
if model.locked():
return {"title": "Bereit gemeldet", "text": "Deine Planung ist verbindlich. Der Kampf beginnt, sobald alle bereit sind."}
return {"title": "Bereit", "text": "Übernimmt deine Planung verbindlich. Der Kampf läuft automatisch über alle drei Lanes.", "note": "Leertaste"}