saumwacht-godot/scripts/luneraria/ui/lune_hero_panel.gd
Martin Stickelbroeck f53859a298 M2: Helden, Items, Talente, Angriffschampions und Bossgrotte
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>
2026-09-27 22:33:11 +02:00

186 lines
8.3 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 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