Drei Helden mit Nähe-/Teilnahme-XP, Talenten und drei Itemplätzen, drei Angriffschampions mit Buchung n -> n+1, Bossgrotte mit Streitfall, ruhendem Boss, idempotenter Eroberung, einmaliger Entsendung für das Siegerteam, hohem Kopfgeld und Sperre bis Sieg+4. HUD-Reiter, Porträt aus echtem Modell, Grotten-Asset, Bots für alle Systeme. Regeln M2 73/73, M1 91/91, App M2 15/15 und M1 38/38, Bossszenarien dokumentiert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
484 lines
22 KiB
GDScript
484 lines
22 KiB
GDScript
extends Control
|
||
## Champions of Luneraria HUD. Presentation only: it reads LuneMatch and sends the human
|
||
## player's commands through the owning LuneGame; UI state never creates ownership.
|
||
|
||
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 menu_requested
|
||
signal rematch_requested
|
||
signal settings_requested
|
||
|
||
const Minimap = preload("res://scripts/luneraria/ui/lune_minimap.gd")
|
||
const HeroPanel = preload("res://scripts/luneraria/ui/lune_hero_panel.gd")
|
||
const ReinforcePanel = preload("res://scripts/luneraria/ui/lune_reinforce_panel.gd")
|
||
const TEAM_COLORS: Array[Color] = [Color("5aa8ff"), Color("ff6a55")]
|
||
const TEAM_NAMES: Array[String] = ["Blau", "Rot"]
|
||
const ROLE_NAMES: Dictionary = {"front": "Front", "melee": "Nahkampf", "ranged": "Fernkampf", "splash": "Fläche", "support": "Unterstützung"}
|
||
|
||
var match_state: LuneMatch
|
||
var human_slot: int = 1
|
||
var minimap: Control
|
||
var _phase: Label
|
||
var _round: Label
|
||
var _core_bars: Array[ProgressBar] = []
|
||
var _core_labels: Array[Label] = []
|
||
var _gold: Label
|
||
var _income: Label
|
||
var _identity: Label
|
||
var _pause: Button
|
||
var _speed: Button
|
||
var _lane_rows: Dictionary = {}
|
||
var _team_list: Label
|
||
var _plan_panel: PanelContainer
|
||
var _shop: GridContainer
|
||
var _shop_buttons: Dictionary = {}
|
||
var _roster: VBoxContainer
|
||
var _roster_title: Label
|
||
var _ready_button: Button
|
||
var _starter: Button
|
||
var _undo: Button
|
||
var _combat_panel: PanelContainer
|
||
var _combat_info: Label
|
||
var _report: Label
|
||
var _toast: Label
|
||
var _toast_left: float = 0.0
|
||
var _over: Control
|
||
var _over_title: Label
|
||
var _over_body: Label
|
||
var _roster_signature: String = "@"
|
||
var hero_panel: VBoxContainer
|
||
var reinforce_panel: VBoxContainer
|
||
var _pages: Array[Control] = []
|
||
var _tabs: ChoiceGroup
|
||
var _boss_status: Label
|
||
|
||
func _ready() -> void:
|
||
theme = SaumwachtTheme.shared()
|
||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_top_bar()
|
||
_lane_panel()
|
||
_plan_side()
|
||
_bottom()
|
||
_game_over()
|
||
|
||
func _panel(variation: String = "CaptionPanel") -> PanelContainer:
|
||
var panel := PanelContainer.new()
|
||
panel.theme_type_variation = variation
|
||
panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
add_child(panel)
|
||
return panel
|
||
|
||
func _label(text_value: String, variation: String, parent: Node, size: int = 0) -> Label:
|
||
var label := Label.new()
|
||
label.text = text_value
|
||
label.theme_type_variation = variation
|
||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
if size > 0:
|
||
label.add_theme_font_size_override("font_size", size)
|
||
parent.add_child(label)
|
||
return label
|
||
|
||
func _button(text_value: String, parent: Node, action: Callable, variation: String = "Button") -> Button:
|
||
var button := Button.new()
|
||
button.text = text_value
|
||
button.theme_type_variation = variation
|
||
button.focus_mode = Control.FOCUS_ALL
|
||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
button.pressed.connect(action)
|
||
parent.add_child(button)
|
||
return button
|
||
|
||
func _bar(color: Color, parent: Node) -> ProgressBar:
|
||
var bar := ProgressBar.new()
|
||
bar.show_percentage = false
|
||
bar.custom_minimum_size = Vector2(190, 10)
|
||
var back := StyleBoxFlat.new()
|
||
back.bg_color = Color(0, 0, 0, .55)
|
||
back.border_color = Color("5c4a33")
|
||
back.set_border_width_all(1)
|
||
var fill := StyleBoxFlat.new()
|
||
fill.bg_color = color
|
||
bar.add_theme_stylebox_override("background", back)
|
||
bar.add_theme_stylebox_override("fill", fill)
|
||
parent.add_child(bar)
|
||
return bar
|
||
|
||
func _top_bar() -> void:
|
||
var top := _panel()
|
||
top.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
||
top.offset_left = 16
|
||
top.offset_right = -16
|
||
top.offset_top = 12
|
||
var row := HBoxContainer.new()
|
||
row.add_theme_constant_override("separation", 22)
|
||
top.add_child(row)
|
||
var title := VBoxContainer.new()
|
||
title.add_theme_constant_override("separation", 0)
|
||
row.add_child(title)
|
||
_label("CHAMPIONS OF LUNERARIA", "EyebrowLabel", title)
|
||
_phase = _label("PLANUNG", "SectionLabel", title, 20)
|
||
_round = _label("Welle 1", "MutedLabel", title)
|
||
for team: int in range(2):
|
||
var box := VBoxContainer.new()
|
||
box.add_theme_constant_override("separation", 3)
|
||
row.add_child(box)
|
||
_core_labels.append(_label("Ancient %s" % TEAM_NAMES[team], "MutedLabel", box, 15))
|
||
_core_bars.append(_bar(TEAM_COLORS[team], box))
|
||
var economy := VBoxContainer.new()
|
||
economy.add_theme_constant_override("separation", 0)
|
||
row.add_child(economy)
|
||
_gold = _label("0 Gold", "CardTitleLabel", economy, 24)
|
||
_income = _label("", "MutedLabel", economy, 15)
|
||
_identity = _label("", "MutedLabel", row, 15)
|
||
_identity.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_identity.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
_pause = _button("Pause · P", row, func() -> void: pause_requested.emit())
|
||
_speed = _button("1×", row, func() -> void: speed_requested.emit(0))
|
||
_button("Optionen · F1", row, func() -> void: settings_requested.emit())
|
||
_button("Menü", row, func() -> void: menu_requested.emit(), "QuietButton")
|
||
|
||
func _lane_panel() -> void:
|
||
var panel := _panel()
|
||
panel.position = Vector2(16, 112)
|
||
panel.custom_minimum_size = Vector2(300, 0)
|
||
var column := VBoxContainer.new()
|
||
column.add_theme_constant_override("separation", 6)
|
||
panel.add_child(column)
|
||
_label("LANES · TÜRME", "SectionLabel", column)
|
||
for lane: String in LuneMap.LANES:
|
||
var button := _button("", column, func() -> void: lane_focus_requested.emit(lane), "ChoiceButton")
|
||
button.custom_minimum_size = Vector2(0, 52)
|
||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
button.tooltip_text = "Kamera auf %s" % LuneMap.LANE_NAMES[lane]
|
||
_lane_rows[lane] = button
|
||
_team_list = _label("", "MutedLabel", column, 14)
|
||
_boss_status = _label("", "Label", column, 15)
|
||
_boss_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_boss_status.custom_minimum_size.x = 264
|
||
|
||
func _plan_side() -> void:
|
||
_plan_panel = _panel("MenuPanel")
|
||
_plan_panel.anchor_left = 1.0
|
||
_plan_panel.anchor_right = 1.0
|
||
_plan_panel.anchor_bottom = 1.0
|
||
_plan_panel.offset_left = -500
|
||
_plan_panel.offset_right = -16
|
||
_plan_panel.offset_top = 112
|
||
_plan_panel.offset_bottom = -270
|
||
var outer := VBoxContainer.new()
|
||
outer.add_theme_constant_override("separation", 8)
|
||
_plan_panel.add_child(outer)
|
||
_tabs = ChoiceGroup.new(true)
|
||
_tabs.name = "PlanTabs"
|
||
_tabs.add_option("Truppen")
|
||
_tabs.add_option("Held")
|
||
_tabs.add_option("Verstärkung")
|
||
outer.add_child(_tabs)
|
||
_tabs.item_selected.connect(show_page)
|
||
var column := VBoxContainer.new()
|
||
column.add_theme_constant_override("separation", 8)
|
||
column.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
outer.add_child(column)
|
||
_pages.append(column)
|
||
_label("TRUPPEN KAUFEN · 1–5", "SectionLabel", column)
|
||
_shop = GridContainer.new()
|
||
_shop.columns = 5
|
||
_shop.add_theme_constant_override("h_separation", 6)
|
||
column.add_child(_shop)
|
||
_roster_title = _label("DEINE ARMEE", "SectionLabel", column)
|
||
var scroll := ScrollContainer.new()
|
||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
column.add_child(scroll)
|
||
_roster = VBoxContainer.new()
|
||
_roster.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_roster.add_theme_constant_override("separation", 4)
|
||
scroll.add_child(_roster)
|
||
for index: int in range(2):
|
||
var page := ScrollContainer.new()
|
||
page.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
page.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
page.visible = false
|
||
outer.add_child(page)
|
||
_pages.append(page)
|
||
var panel: VBoxContainer = HeroPanel.new() if index == 0 else ReinforcePanel.new()
|
||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
page.add_child(panel)
|
||
panel.command_requested.connect(func(command: Dictionary) -> void: command_requested.emit(command))
|
||
if index == 0:
|
||
hero_panel = panel
|
||
else:
|
||
reinforce_panel = panel
|
||
_tabs.select(0)
|
||
var actions := HBoxContainer.new()
|
||
outer.add_child(actions)
|
||
_starter = _button("Startaufstellung", actions, func() -> void: starter_requested.emit())
|
||
_starter.tooltip_text = "Kauft Front, Nahkampf und Fernkampf deines Bundes für deine Lane."
|
||
_undo = _button("Rückgängig", actions, func() -> void: undo_requested.emit())
|
||
_ready_button = _button("BEREIT · KAMPF ›", outer, func() -> void: ready_requested.emit(), "PrimaryButton")
|
||
_ready_button.custom_minimum_size.y = 58
|
||
_ready_button.tooltip_text = "Leertaste · Befehle werden jetzt verbindlich übernommen."
|
||
_combat_panel = _panel()
|
||
_combat_panel.anchor_left = 1.0
|
||
_combat_panel.anchor_right = 1.0
|
||
_combat_panel.offset_left = -500
|
||
_combat_panel.offset_right = -16
|
||
_combat_panel.offset_top = 112
|
||
_combat_info = _label("", "Label", _combat_panel, 16)
|
||
|
||
func _bottom() -> void:
|
||
var report_panel := _panel()
|
||
report_panel.anchor_top = 1.0
|
||
report_panel.anchor_bottom = 1.0
|
||
report_panel.offset_left = 16
|
||
report_panel.offset_top = -120
|
||
report_panel.offset_bottom = -16
|
||
report_panel.offset_right = 760
|
||
var column := VBoxContainer.new()
|
||
report_panel.add_child(column)
|
||
_report = _label("Planung: Kaufe Truppen für deine Lane und drücke Bereit.", "Label", column, 16)
|
||
_report.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_toast = _label("", "MutedLabel", column, 15)
|
||
var map_panel := _panel()
|
||
map_panel.anchor_left = 1.0
|
||
map_panel.anchor_right = 1.0
|
||
map_panel.anchor_top = 1.0
|
||
map_panel.anchor_bottom = 1.0
|
||
map_panel.offset_left = -270
|
||
map_panel.offset_right = -16
|
||
map_panel.offset_top = -258
|
||
map_panel.offset_bottom = -16
|
||
minimap = Minimap.new()
|
||
map_panel.add_child(minimap)
|
||
minimap.focus_requested.connect(func(point: Vector3) -> void: map_focus_requested.emit(point))
|
||
|
||
func _game_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.new()
|
||
column.add_theme_constant_override("separation", 14)
|
||
panel.add_child(column)
|
||
_over_title = _label("", "TitleLabel", column, 60)
|
||
_over_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_over_body = _label("", "BodyLabel", column)
|
||
_over_body.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_over_body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
var row := HBoxContainer.new()
|
||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
row.add_theme_constant_override("separation", 12)
|
||
column.add_child(row)
|
||
_button("NEUE PARTIE", row, func() -> void: rematch_requested.emit(), "PrimaryButton")
|
||
_button("Hauptmenü", row, func() -> void: menu_requested.emit())
|
||
|
||
# ---------------------------------------------------------------- refresh
|
||
func notify(message: String, error: bool = false) -> void:
|
||
_toast.text = message
|
||
_toast.add_theme_color_override("font_color", Color("f0a888") if error else Color("9fe0c8"))
|
||
_toast_left = 5.0
|
||
|
||
func _process(delta: float) -> void:
|
||
_toast_left -= delta
|
||
if _toast_left <= 0.0:
|
||
_toast.text = "WASD / mittlere Maustaste: Kamera · Mausrad: Zoom · Tab: Lanes · Minimap klicken"
|
||
_toast.remove_theme_color_override("font_color")
|
||
if match_state != null:
|
||
refresh()
|
||
|
||
func show_page(index: int) -> void:
|
||
_tabs.select(index)
|
||
for page_index: int in range(_pages.size()):
|
||
_pages[page_index].visible = page_index == index
|
||
|
||
func refresh() -> void:
|
||
var player: Dictionary = match_state.players[human_slot]
|
||
if hero_panel.match_state != match_state:
|
||
hero_panel.match_state = match_state
|
||
hero_panel.slot = human_slot
|
||
reinforce_panel.match_state = match_state
|
||
reinforce_panel.slot = human_slot
|
||
_boss_status.text = _grotto_text()
|
||
var planning: bool = match_state.phase == LuneMatch.Phase.PLANNING
|
||
var combat: bool = match_state.phase == LuneMatch.Phase.COMBAT
|
||
var phase_name: String = "PLANUNG" if planning else ("KAMPF" if combat else "PARTIE BEENDET")
|
||
_phase.text = phase_name
|
||
_round.text = "Welle %d%s" % [match_state.round_number, (" · %02d:%02d" % [int(match_state.combat.elapsed) / 60, int(match_state.combat.elapsed) % 60]) if combat else ""]
|
||
for team: int in range(2):
|
||
var hp: float = match_state.core_hp(team)
|
||
if combat and match_state.combat.buildings.has("%s_ancient" % ("blau" if team == 0 else "rot")):
|
||
hp = (match_state.combat.buildings["%s_ancient" % ("blau" if team == 0 else "rot")] as LuneUnit).hp
|
||
_core_bars[team].max_value = match_state.balance.core_hp
|
||
_core_bars[team].value = hp
|
||
_core_labels[team].text = "Ancient %s · %d" % [TEAM_NAMES[team], ceili(hp)]
|
||
_gold.text = "%d Gold" % match_state.gold_of(human_slot)
|
||
_income.text = "+%d pro Runde · Armee %d / %d" % [match_state.balance.round_income(match_state.round_number), (player.draft.roster as Array).size() if planning else (player.roster as Array).size(), match_state.balance.supply_cap]
|
||
_identity.text = "%s · %s-Lane · %s" % [TEAM_NAMES[int(player.team)], LuneMap.LANE_NAMES[player.lane], DataCatalog.FACTION_NAMES[player.bund]]
|
||
_pause.visible = combat
|
||
_speed.visible = combat
|
||
for lane: String in LuneMap.LANES:
|
||
var button: Button = _lane_rows[lane]
|
||
var own: String = "●".repeat(match_state.towers_alive(0, lane, true)) + "○".repeat(2 - match_state.towers_alive(0, lane, true))
|
||
var rival: String = "●".repeat(match_state.towers_alive(1, lane, true)) + "○".repeat(2 - match_state.towers_alive(1, lane, true))
|
||
var blue_owner: String = match_state.players[match_state.slot_of(0, lane)].name
|
||
var red_owner: String = match_state.players[match_state.slot_of(1, lane)].name
|
||
button.text = "%s Blau %s Rot %s\n%s · %s" % [LuneMap.LANE_NAMES[lane].to_upper(), own, rival, blue_owner, red_owner]
|
||
var team_lines: PackedStringArray = []
|
||
for team: int in range(2):
|
||
var names: PackedStringArray = []
|
||
for slot: int in range(team * 3, team * 3 + 3):
|
||
names.append(match_state.players[slot].name)
|
||
team_lines.append("%s: %s · Basistürme %d/2" % [TEAM_NAMES[team], ", ".join(names), int(match_state.building_alive_now("%s_base_1" % ("blau" if team == 0 else "rot"))) + int(match_state.building_alive_now("%s_base_2" % ("blau" if team == 0 else "rot")))])
|
||
_team_list.text = "\n".join(team_lines)
|
||
_plan_panel.visible = planning
|
||
_combat_panel.visible = combat
|
||
if planning:
|
||
_refresh_plan(player)
|
||
elif combat:
|
||
var lines: PackedStringArray = ["KAMPF LÄUFT · automatisch"]
|
||
for lane: String in LuneMap.LANES:
|
||
var counts: Array[int] = [0, 0]
|
||
for unit: LuneUnit in match_state.combat.units:
|
||
if unit.alive and unit.is_mobile() and unit.lane == lane:
|
||
counts[unit.team] += 1
|
||
lines.append("%s Blau %d · Rot %d Einheiten" % [LuneMap.LANE_NAMES[lane], counts[0], counts[1]])
|
||
lines.append("Leertaste/P: Pause · Tempo 1×/2×/4×")
|
||
_combat_info.text = "\n".join(lines)
|
||
if combat:
|
||
_report.text = "Welle %d läuft. Armeen und Monsterwellen rücken über alle drei Lanes vor; Türme fallen in fester Reihenfolge." % match_state.round_number
|
||
elif planning and match_state.last_settlement.is_empty():
|
||
_report.text = "Planung: Kaufe Truppen für deine Lane, teile sie auf Top, Mid oder Bottom ein und drücke Bereit."
|
||
if not match_state.last_settlement.is_empty() and planning:
|
||
var s: Dictionary = match_state.last_settlement
|
||
var lost: PackedStringArray = []
|
||
for id: String in s.buildings_lost:
|
||
lost.append(_building_name(id))
|
||
_report.text = "Welle %d abgeschlossen (%s, %ds) · dein Ertrag +%d Gold (Kills %d, Einkommen %d)%s" % [s.round, _reason(s.end_reason), int(s.combat_time), int(s.gains[human_slot]) + int(s.income), int(s.gains[human_slot]), int(s.income), ("\nGefallen: " + ", ".join(lost)) if not lost.is_empty() else ""]
|
||
_over.visible = match_state.phase == LuneMatch.Phase.OVER
|
||
if _over.visible:
|
||
var own_team: int = int(player.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]
|
||
minimap.match_state = match_state
|
||
minimap.queue_redraw()
|
||
|
||
func _grotto_text() -> String:
|
||
var claim: Dictionary = match_state.boss.claim
|
||
if not claim.is_empty():
|
||
var team: String = TEAM_NAMES[int(claim.team)]
|
||
return "BOSSGROTTE · von %s erobert (Runde %d)\nDer Boss greift in Runde %d für %s über %s an · Grotte wieder ab Runde %d" % [team, int(claim.won_round), int(claim.deploy_round), team, LuneMap.LANE_NAMES[claim.lane], int(match_state.boss.next_available_round)]
|
||
if match_state.grotto_available():
|
||
return "BOSSGROTTE · verfügbar\nEinheiten mit „G“ entsenden; sie fehlen ihrer Lane die ganze Kampfphase."
|
||
return "BOSSGROTTE · gesperrt bis Runde %d" % int(match_state.boss.next_available_round)
|
||
|
||
func _reason(reason: String) -> String:
|
||
return {"cleared": "alle Einheiten gefallen", "timeout": "Zeitfenster abgelaufen", "core": "Ancient zerstört"}.get(reason, reason)
|
||
|
||
func _building_name(id: String) -> String:
|
||
var parts: PackedStringArray = id.split("_")
|
||
var team: String = "Blau" if parts[0] == "blau" else "Rot"
|
||
if parts[1] == "ancient":
|
||
return "Ancient " + team
|
||
if parts[1] == "base":
|
||
return "Basisturm %s %s" % [team, parts[2]]
|
||
return "%s %s-%s" % [team, LuneMap.LANE_NAMES[parts[1]], "Außenturm" if parts[2] == "outer" else "Innenturm"]
|
||
|
||
func _refresh_plan(player: Dictionary) -> void:
|
||
var draft: Dictionary = player.draft
|
||
var ids: Array[String] = match_state.base_ids(player.bund)
|
||
if _shop_buttons.is_empty() or not _shop_buttons.has(ids[0]):
|
||
for child: Node in _shop.get_children():
|
||
child.free()
|
||
_shop_buttons.clear()
|
||
for index: int in range(ids.size()):
|
||
var id: String = ids[index]
|
||
var data: Dictionary = DataCatalog.get_unit(id)
|
||
var button := Button.new()
|
||
button.icon = AssetLibrary.icon(id)
|
||
button.expand_icon = true
|
||
button.custom_minimum_size = Vector2(76, 96)
|
||
button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
button.vertical_icon_alignment = VERTICAL_ALIGNMENT_TOP
|
||
button.text = "%d G" % int(data.cost)
|
||
button.focus_mode = Control.FOCUS_ALL
|
||
button.tooltip_text = "%s [%d]\n%s · %d HP · %d Schaden/s · %.1f m\n%s" % [data.display_name, index + 1, ROLE_NAMES[match_state.role_of(id)], data.max_hp, data.damage, data.attack_range, data.ability_description]
|
||
button.pressed.connect(func() -> void: command_requested.emit({"type": "buy", "type_id": id}))
|
||
_shop.add_child(button)
|
||
_shop_buttons[id] = button
|
||
for id: String in _shop_buttons:
|
||
var cost: int = int(DataCatalog.get_unit(id).cost)
|
||
(_shop_buttons[id] as Button).disabled = int(draft.gold) < cost or (draft.roster as Array).size() >= match_state.balance.supply_cap
|
||
_undo.disabled = (player.undo as Array).is_empty()
|
||
_starter.disabled = not (draft.roster as Array).is_empty()
|
||
_ready_button.disabled = bool(player.ready)
|
||
_ready_button.text = "BEREIT · KAMPF ›" if not player.ready else "WARTE AUF ANDERE …"
|
||
var signature: String = JSON.stringify(draft)
|
||
if signature == _roster_signature:
|
||
return
|
||
_roster_signature = signature
|
||
for child: Node in _roster.get_children():
|
||
child.free()
|
||
_roster_title.text = "DEINE ARMEE · %d / %d" % [(draft.roster as Array).size(), match_state.balance.supply_cap]
|
||
if (draft.roster as Array).is_empty():
|
||
_label("Noch keine Truppen. Kaufe oben oder nutze die Startaufstellung.", "MutedLabel", _roster, 15).autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
for entry: Dictionary in draft.roster:
|
||
var data: Dictionary = DataCatalog.get_unit(entry.type_id)
|
||
var row := HBoxContainer.new()
|
||
row.add_theme_constant_override("separation", 6)
|
||
_roster.add_child(row)
|
||
var icon := TextureRect.new()
|
||
icon.texture = AssetLibrary.icon(entry.type_id)
|
||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
icon.custom_minimum_size = Vector2(40, 40)
|
||
row.add_child(icon)
|
||
var name_label := _label(str(data.display_name), "Label", row, 15)
|
||
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
name_label.clip_text = true
|
||
for lane: String in LuneMap.LANES + [LuneMatch.GROTTO]:
|
||
var lane_button := Button.new()
|
||
var grotto: bool = lane == LuneMatch.GROTTO
|
||
lane_button.text = "G" if grotto else LuneMap.LANE_NAMES[lane].left(1)
|
||
lane_button.toggle_mode = true
|
||
lane_button.button_pressed = entry.lane == lane
|
||
lane_button.theme_type_variation = "ChoiceButton"
|
||
lane_button.custom_minimum_size = Vector2(34, 34)
|
||
lane_button.disabled = grotto and not match_state.grotto_available()
|
||
lane_button.tooltip_text = "In die Bossgrotte entsenden (fehlt der Lane die ganze Kampfphase)" if grotto else "Auf %s einteilen" % LuneMap.LANE_NAMES[lane]
|
||
var uid: String = str(entry.uid)
|
||
lane_button.pressed.connect(func() -> void: command_requested.emit({"type": "assign", "uid": uid, "lane": lane}))
|
||
row.add_child(lane_button)
|
||
var upgrade_id: String = str(data.get("upgrade_id", ""))
|
||
var uid_value: String = str(entry.uid)
|
||
var up := Button.new()
|
||
up.text = "↑ %d" % int(DataCatalog.get_unit(upgrade_id).cost) if not upgrade_id.is_empty() else "max"
|
||
up.disabled = upgrade_id.is_empty() or int(draft.gold) < int(DataCatalog.get_unit(upgrade_id).get("cost", 0))
|
||
up.tooltip_text = ("Aufwerten zu %s" % DataCatalog.get_unit(upgrade_id).display_name) if not upgrade_id.is_empty() else "Vollständig aufgewertet"
|
||
up.pressed.connect(func() -> void: command_requested.emit({"type": "upgrade", "uid": uid_value}))
|
||
row.add_child(up)
|
||
var sell := Button.new()
|
||
var refund: int = int(entry.invested) if bool(entry.get("fresh", false)) else floori(float(entry.invested) * match_state.balance.sell_refund)
|
||
sell.text = "×"
|
||
sell.tooltip_text = "Zurückgeben für %d Gold" % refund
|
||
sell.pressed.connect(func() -> void: command_requested.emit({"type": "sell", "uid": uid_value}))
|
||
row.add_child(sell)
|
||
|
||
func set_speed_label(value: int, paused: bool) -> void:
|
||
_speed.text = "%d×" % value
|
||
_pause.text = "Weiter · P" if paused else "Pause · P"
|