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