extends VBoxContainer ## Hero tab: recruit one of three heroes, then level, lane, three item slots and talents. ## One shared portrait viewport renders the real game model; nothing here owns state. signal command_requested(command: Dictionary) var match_state: LuneMatch var slot: int = 0 var _signature: String = "@" var _portrait_view: SubViewport var _portrait_root: Node3D var _portrait_model: Node3D var _portrait_id: String = "" var _body: VBoxContainer var _time: float = 0.0 func _ready() -> void: add_theme_constant_override("separation", 8) var frame := SubViewportContainer.new() frame.stretch = true frame.custom_minimum_size = Vector2(0, 112) frame.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(frame) _portrait_view = SubViewport.new() _portrait_view.own_world_3d = true _portrait_view.transparent_bg = true _portrait_view.msaa_3d = Viewport.MSAA_2X _portrait_view.render_target_update_mode = SubViewport.UPDATE_WHEN_VISIBLE frame.add_child(_portrait_view) _portrait_root = Node3D.new() _portrait_view.add_child(_portrait_root) var environment := WorldEnvironment.new() environment.environment = Environment.new() environment.environment.background_mode = Environment.BG_CLEAR_COLOR environment.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR environment.environment.ambient_light_color = Color("8fa0b8") environment.environment.ambient_light_energy = .5 _portrait_root.add_child(environment) var key := DirectionalLight3D.new() key.rotation_degrees = Vector3(-35, -35, 0) key.light_energy = 1.6 key.light_color = Color("ffe2bc") _portrait_root.add_child(key) var rim := DirectionalLight3D.new() rim.rotation_degrees = Vector3(-20, 150, 0) rim.light_energy = .8 rim.light_color = Color("9fc4ff") _portrait_root.add_child(rim) var camera := Camera3D.new() camera.projection = Camera3D.PROJECTION_ORTHOGONAL camera.size = 2.3 camera.position = Vector3(1.4, 1.35, 2.6) _portrait_root.add_child(camera) camera.look_at(Vector3(0, .9, 0), Vector3.UP) _body = VBoxContainer.new() _body.add_theme_constant_override("separation", 6) add_child(_body) func _process(delta: float) -> void: _time += delta if is_instance_valid(_portrait_model) and is_visible_in_tree(): if _portrait_model.has_method("preview"): _portrait_model.preview("idle", fmod(_time, _portrait_model.clip_length("idle"))) _portrait_model.rotation.y = -.35 if match_state != null and match_state.phase == LuneMatch.Phase.PLANNING: var signature: String = JSON.stringify([match_state.players[slot].draft.hero, match_state.players[slot].draft.gold, match_state.round_number]) if signature != _signature: _signature = signature _rebuild() func _show_model(model_id: String) -> void: if model_id == _portrait_id: return _portrait_id = model_id if is_instance_valid(_portrait_model): _portrait_model.queue_free() _portrait_model = AssetLibrary.instantiate_fighter(model_id) if _portrait_model != null: _portrait_root.add_child(_portrait_model) 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.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART 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, command: Dictionary, disabled: bool = false, tooltip: String = "", variation: String = "Button") -> Button: var button := Button.new() button.text = text_value button.theme_type_variation = variation button.disabled = disabled button.tooltip_text = tooltip button.focus_mode = Control.FOCUS_ALL button.pressed.connect(func() -> void: command_requested.emit(command)) parent.add_child(button) return button func _rebuild() -> void: for child: Node in _body.get_children(): child.free() var draft: Dictionary = match_state.players[slot].draft var hero: Dictionary = draft.hero var gold: int = int(draft.gold) if hero.is_empty(): _show_model("w04") _label("HELD ANWERBEN · ein aktiver Held pro Spieler", "SectionLabel", _body) for hero_id: String in LuneContent.HEROES: var spec: Dictionary = LuneContent.HEROES[hero_id] var row := HBoxContainer.new() _body.add_child(row) var text := VBoxContainer.new() text.size_flags_horizontal = Control.SIZE_EXPAND_FILL text.add_theme_constant_override("separation", 0) row.add_child(text) _label("%s · %s" % [spec.name, spec.role], "Label", text, 16) _label(spec.summary, "MutedLabel", text, 14) var buy := _button("%d G" % int(spec.cost), row, {"type": "hero_buy", "hero_id": hero_id}, gold < int(spec.cost), "%s anwerben" % spec.name) buy.mouse_entered.connect(func() -> void: _show_model(spec.model)) buy.focus_entered.connect(func() -> void: _show_model(spec.model)) return var spec: Dictionary = LuneContent.HEROES[hero.hero_id] _show_model(spec.model) var level: int = LuneContent.level_for(int(hero.xp)) var next_xp: int = LuneContent.LEVEL_XP[mini(level, LuneContent.LEVEL_XP.size() - 1)] _label("%s · Stufe %d" % [spec.name, level], "CardTitleLabel", _body, 22) var bar := ProgressBar.new() bar.min_value = LuneContent.LEVEL_XP[level - 1] bar.max_value = maxi(next_xp, LuneContent.LEVEL_XP[level - 1] + 1) bar.value = int(hero.xp) bar.show_percentage = false bar.custom_minimum_size.y = 8 bar.tooltip_text = "%d Erfahrung · nächste Stufe bei %d" % [int(hero.xp), next_xp] _body.add_child(bar) _label("%s · Erfahrung aus Gefechten in der Nähe und aus jeder Runde" % spec.role, "MutedLabel", _body, 14) var lanes := HBoxContainer.new() _body.add_child(lanes) _label("Einsatz", "MutedLabel", lanes, 14).size_flags_horizontal = Control.SIZE_EXPAND_FILL for lane: String in LuneMap.LANES + [LuneMatch.GROTTO]: var name: String = "Grotte" if lane == LuneMatch.GROTTO else LuneMap.LANE_NAMES[lane] var button := _button(name, lanes, {"type": "assign", "uid": hero.uid, "lane": lane}, lane == LuneMatch.GROTTO and not match_state.grotto_available(), "Held auf %s" % name, "ChoiceButton") button.toggle_mode = true button.button_pressed = hero.lane == lane _label("ITEMS · %d / %d Plätze" % [(hero.items as Array).size(), LuneContent.ITEM_SLOTS], "SectionLabel", _body) var slots := HBoxContainer.new() _body.add_child(slots) for index: int in range(LuneContent.ITEM_SLOTS): if index < (hero.items as Array).size(): var item_id: String = hero.items[index] var item: Dictionary = LuneContent.ITEMS[item_id] 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) _button("%s ×" % item.name, slots, {"type": "item_sell", "item_id": item_id}, false, "%s\nVerkaufen für %d Gold" % [item.text, refund]) else: var empty := Button.new() empty.text = "leer" empty.disabled = true slots.add_child(empty) var shop := GridContainer.new() shop.columns = 2 _body.add_child(shop) for item_id: String in LuneContent.ITEMS: var item: Dictionary = LuneContent.ITEMS[item_id] var blocked: bool = (hero.items as Array).has(item_id) or (hero.items as Array).size() >= LuneContent.ITEM_SLOTS or gold < int(item.cost) or level < int(item.get("min_level", 1)) var button := _button("%s · %d G" % [item.name, int(item.cost)], shop, {"type": "item_buy", "item_id": item_id, "uid": hero.uid}, blocked, item.text) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL var talents: Array = hero.talents for tier: int in range(LuneContent.TALENT_LEVELS.size()): var options: Array = LuneContent.TALENTS[hero.hero_id][tier] if tier < talents.size(): for option: Dictionary in options: if option.id == talents[tier]: _label("TALENT %d · %s: %s" % [tier + 1, option.name, option.text], "MutedLabel", _body, 14) else: var required: int = LuneContent.TALENT_LEVELS[tier] _label("TALENT %d · %s" % [tier + 1, "jetzt wählen" if level >= required and tier == talents.size() else "ab Stufe %d" % required], "SectionLabel", _body) var row := HBoxContainer.new() _body.add_child(row) for choice: int in range(options.size()): var option: Dictionary = options[choice] var button := _button(option.name, row, {"type": "talent", "tier": tier, "choice": choice}, level < required or tier != talents.size(), option.text) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL break