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))]