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>
202 lines
9 KiB
GDScript
202 lines
9 KiB
GDScript
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))]
|