saumwacht-godot/scripts/luneraria/ui/components/lane_indicator.gd
Martin Stickelbroeck 1b540f2a55 G3 (Zwischenstand): neues Ingame-HUD; Projekt wechselt nach Unity
Nicht abgenommen: Nach dem ersten Spieltest wurde entschieden, Champions of
Luneraria nach Unity 6 zu portieren. Dieser Stand ist die Referenz für das
HUD im Unity-Projekt (../luneraria-unity).

- HUD neu aus Komponenten mit eigenem Theme, Porträts aus den echten Modellen
  (einmal gerendert, zwischengespeichert), Lane-Übersicht mit deterministischer
  Bedrohung aus öffentlichem Zustand, Bossgrotte, nächste Welle, Ereignisse,
  Minimap mit zusammengefassten Armeen, Held/Items/Talente, Einheitendetails,
  Angriffschampions, Bereit-Sperre, UI-Größe, Team-Formen statt nur Farben
- Privatsphäre über LuneHudModel: fremdes Gold und fremde Entwürfe erscheinen nie
- Keine Regeländerung: Regeln 91/91, M2 73/73, Netz 43/43; verify_g3 715/715;
  verify_m1 38/38 und verify_m2 14/14 mit angepassten UI-Treibern
- Spieltest bis Welle 8 über die echte Oberfläche, Befunde in G3-HUD.md
- HELDEN-KONZEPT.md: vom Projektinhaber vorgegebene Helden (nicht umgesetzt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:55 +02:00

202 lines
9 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.

class_name LuneLaneIndicator
extends Button
## One lane in the lane overview: name and hotkey, the threat level (text, symbol and colour),
## the tower chain from the own base to the enemy base, the two lane players, announced
## champions or the boss, and in combat the unit count of both sides. A click moves the camera.
## The threat rises at once but only falls after a short hold, so it does not flicker.
const HOLD: float = 2.5
var lane: String = "top"
var hotkey: String = ""
var level: int = 0
var data: Dictionary = {}
var towers: Array[LuneTowerIndicator] = []
var _title: Label
var _badge: LuneStatusBadge
var _own: Label
var _enemy: Label
var _own_hero: LunePortraitRect
var _enemy_hero: LunePortraitRect
var _army: Label
var _dangers: HBoxContainer
var _mine: LuneGlyph
var _own_shape: LuneGlyph
var _enemy_shape: LuneGlyph
var _lower_hold: float = 0.0
var _pulse: float = 0.0
var _signature: String = ""
func _init(lane_name: String = "top", key: String = "") -> void:
lane = lane_name
hotkey = key
theme_type_variation = "HudRow"
focus_mode = Control.FOCUS_ALL
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
custom_minimum_size = Vector2(0, 92)
var column := VBoxContainer.new()
column.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
column.offset_left = 10
column.offset_right = -9
column.offset_top = 6
column.offset_bottom = -6
column.add_theme_constant_override("separation", 3)
column.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(column)
var head := HBoxContainer.new()
head.add_theme_constant_override("separation", 6)
head.mouse_filter = Control.MOUSE_FILTER_IGNORE
column.add_child(head)
LuneGlyph.make("lane_" + lane, LuneUiTheme.BRONZE_LIGHT, 17.0, head)
_title = LuneUiTheme.label(LuneMap.LANE_NAMES[lane].to_upper(), "HudLane", head)
_mine = LuneGlyph.make("star", LuneUiTheme.BRONZE_LIGHT, 12.0, head)
_mine.visible = false
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
head.add_child(spacer)
_badge = LuneStatusBadge.new()
head.add_child(_badge)
var chain := HBoxContainer.new()
chain.add_theme_constant_override("separation", 3)
chain.mouse_filter = Control.MOUSE_FILTER_IGNORE
column.add_child(chain)
_own_shape = LuneGlyph.make("emblem_0", LuneUiTheme.TEAM[0], 13.0, chain)
for index: int in range(4):
var tower := LuneTowerIndicator.new()
chain.add_child(tower)
towers.append(tower)
LuneTooltip.attach(tower, _tower_tip.bind(index))
if index == 1:
var middle := HBoxContainer.new()
middle.size_flags_horizontal = Control.SIZE_EXPAND_FILL
middle.alignment = BoxContainer.ALIGNMENT_CENTER
middle.add_theme_constant_override("separation", 4)
middle.mouse_filter = Control.MOUSE_FILTER_IGNORE
chain.add_child(middle)
_army = LuneUiTheme.label("", "HudValue", middle)
_dangers = HBoxContainer.new()
_dangers.add_theme_constant_override("separation", 2)
_dangers.mouse_filter = Control.MOUSE_FILTER_IGNORE
middle.add_child(_dangers)
_enemy_shape = LuneGlyph.make("emblem_1", LuneUiTheme.TEAM[1], 13.0, chain)
var names := HBoxContainer.new()
names.add_theme_constant_override("separation", 5)
names.mouse_filter = Control.MOUSE_FILTER_IGNORE
column.add_child(names)
# Heroes of both lane players (Dota-style "who stands where"), committed heroes only.
_own_hero = LunePortraitRect.new()
_own_hero.custom_minimum_size = Vector2(22, 22)
names.add_child(_own_hero)
_own = LuneUiTheme.label("", "HudMuted", names)
_own.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_own.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
_own.clip_text = true
_enemy = LuneUiTheme.label("", "HudMuted", names)
_enemy.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_enemy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_enemy.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
_enemy.clip_text = true
_enemy_hero = LunePortraitRect.new()
_enemy_hero.custom_minimum_size = Vector2(22, 22)
names.add_child(_enemy_hero)
LuneTooltip.attach(self, _tip)
## Applies a lane dictionary from LuneHudModel.lane(); returns true if something changed.
func apply(entry: Dictionary, own_team: int, delta: float, reduced_motion: bool) -> void:
data = entry
var threat: int = int(entry.threat.level)
if threat >= level:
if threat == LuneLaneThreat.Level.CRITICAL and level != threat and not reduced_motion:
_pulse = 1.6
level = threat
_lower_hold = HOLD
else:
_lower_hold -= delta
if _lower_hold <= 0.0:
level = threat
_lower_hold = HOLD
var tint: Color = LuneUiTheme.THREAT[level]
_badge.set_state(LuneLaneThreat.NAMES[level], tint, LuneLaneThreat.GLYPHS[level] if level >= 2 else "", level == LuneLaneThreat.Level.CRITICAL)
_own_shape.glyph = "emblem_%d" % own_team
_own_shape.color = LuneUiTheme.team_color(own_team)
_enemy_shape.glyph = "emblem_%d" % (1 - own_team)
_enemy_shape.color = LuneUiTheme.team_color(1 - own_team)
var list: Array = entry.towers
for index: int in range(4):
var tower: Dictionary = list[index]
towers[index].team = int(tower.team)
towers[index].tier = str(tower.tier)
towers[index].set_state(bool(tower.alive), float(tower.fraction), bool(tower.attackable))
_own.text = str(entry.own_player)
_own.add_theme_color_override("font_color", LuneUiTheme.BRONZE_LIGHT if entry.own_is_me else LuneUiTheme.MUTED)
_enemy.text = str(entry.enemy_player)
for pair: Array in [[_own_hero, entry.get("own_hero", {})], [_enemy_hero, entry.get("enemy_hero", {})]]:
var portrait: LunePortraitRect = pair[0]
var hero: Dictionary = pair[1]
portrait.visible = not hero.is_empty()
if not hero.is_empty():
portrait.show_model(str(hero.model))
_mine.visible = bool(entry.own_is_me)
_army.text = "%d : %d" % [int(entry.own_count), int(entry.enemy_count)] if entry.has("own_count") else ""
var signature: String = str(entry.dangers)
if signature != _signature:
_signature = signature
for child: Node in _dangers.get_children():
child.free()
for danger: Dictionary in entry.dangers:
var hostile: bool = int(danger.team) != own_team
var glyph: String = "crown" if danger.kind == "boss" else "champion"
LuneGlyph.make(glyph, LuneUiTheme.DANGER if hostile else LuneUiTheme.team_color(int(danger.team)), 16.0, _dangers)
if _pulse > 0.0:
_pulse = maxf(0.0, _pulse - delta)
_badge.modulate = Color(1, 1, 1, .55 + .45 * absf(cos(_pulse * PI * 2.0)))
if _pulse <= 0.0:
_badge.modulate = Color.WHITE
elif reduced_motion and _badge.modulate != Color.WHITE:
_badge.modulate = Color.WHITE
if LuneTooltip.current != null:
LuneTooltip.current.refresh(self)
func _tip() -> Dictionary:
if data.is_empty():
return {}
var lines: PackedStringArray = []
for reason: String in data.threat.reasons:
lines.append("• " + reason)
if lines.is_empty():
lines.append("Keine Bedrohung aus Türmen, Kampflage oder Ankündigungen.")
var stats: Array = []
var list: Array = data.towers
for index: int in [0, 1]:
var tower: Dictionary = list[index]
stats.append(["tower", "Eigene " + LuneHudModel.building_title(tower.id), _tower_value(tower)])
for index: int in [2, 3]:
var tower: Dictionary = list[index]
stats.append(["tower", "Gegner " + LuneHudModel.building_title(tower.id), _tower_value(tower)])
if data.has("own_count"):
stats.append(["melee", "Einheiten", "%d eigene · %d gegnerische" % [int(data.own_count), int(data.enemy_count)]])
var notes: PackedStringArray = []
for danger: Dictionary in data.dangers:
var side: String = LuneUiTheme.team_name(int(danger.team))
notes.append(("Schlundkrone (%s) greift an" % side) if danger.kind == "boss" else ("%s (%s) angekündigt" % [LuneContent.CHAMPIONS[danger.champion_id].name, side]))
return {"title": "%s-LANE · %s" % [data.title, LuneLaneThreat.NAMES[level]], "glyph": "lane_" + lane,
"subtitle": "%s gegen %s" % [_player_text(data.own_player, data.get("own_hero", {})), _player_text(data.enemy_player, data.get("enemy_hero", {}))], "stats": stats, "text": "\n".join(lines),
"note": ("; ".join(notes) + "\n" if not notes.is_empty() else "") + "Klick: Kamera auf die Lane" + (" (%s)" % hotkey if not hotkey.is_empty() else "")}
static func _player_text(player: String, hero: Dictionary) -> String:
return player if hero.is_empty() else "%s (%s, Stufe %d)" % [player, hero.name, int(hero.level)]
func _tower_tip(index: int) -> Dictionary:
if data.is_empty():
return {}
var tower: Dictionary = data.towers[index]
var title: String = "%s · %s" % [LuneHudModel.building_title(tower.id), LuneUiTheme.team_name(int(tower.team))]
var text: String = "Zerstört – die Ruine blockiert nicht." if not tower.alive else ("Angreifbar." if tower.attackable else "Noch geschützt: erst fällt der Turm davor.")
return {"title": title, "glyph": "tower", "glyph_color": LuneUiTheme.team_color(int(tower.team)),
"stats": [["heart", "Leben", _tower_value(tower)]], "text": text}
static func _tower_value(tower: Dictionary) -> String:
if not tower.alive:
return "zerstört"
return "%s / %s (%d %%)" % [LuneUiTheme.number(tower.hp), LuneUiTheme.number(tower.max_hp), int(round(float(tower.fraction) * 100.0))]