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