extends Control ## Minimap from the public match state. Lanes, river, grotto and both bases as the map; every ## building in its team's shape language (round = Blau, angular = Rot; Ancients as crescent ## and sun disc with an HP ring); heroes, attack champions and the boss as their own markers; ## troops and monsters merged per map cell into one marker whose size grows with the count, ## so there is no cloud of pixels. Click or drag moves the camera; the minimap never buys, ## orders or changes anything in the world. It redraws only when the HUD refreshes it (about ## ten times a second in combat) and the camera frame lives on a separate overlay. signal focus_requested(point: Vector3) const CELL: float = 6.0 const Overlay = preload("res://scripts/luneraria/ui/lune_minimap_overlay.gd") var match_state: LuneMatch var viewer_team: int = 0 var human_slot: int = 0 var camera_focus := Vector3.ZERO var camera_span: float = 34.0 ## Ground outline of the game camera's view (perspective trapezoid); empty = rectangle. var camera_footprint := PackedVector2Array(): set(value): if value.size() != camera_footprint.size() or _moved(value): camera_footprint = value if _overlay != null: _overlay.queue_redraw() ## Counts for tests and the performance log. var markers: Dictionary = {} var redraws: int = 0 var _clusters: Array = [] var _specials: Array = [] var _overlay: Control var _dangers: Array = [] func _ready() -> void: custom_minimum_size = Vector2(224, 224) mouse_filter = Control.MOUSE_FILTER_STOP focus_mode = Control.FOCUS_NONE mouse_default_cursor_shape = Control.CURSOR_CROSS clip_contents = true _overlay = Overlay.new() _overlay.map = self _overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(_overlay) resized.connect(queue_redraw) func _moved(value: PackedVector2Array) -> bool: for index: int in range(value.size()): if value[index].distance_squared_to(camera_footprint[index]) > .04: return true return false func to_map(v: Vector2) -> Vector2: var extent: float = LuneMap.HALF_EXTENT + 4.0 return (v + Vector2(extent, extent)) / (extent * 2.0) * size func to_world(p: Vector2) -> Vector3: var extent: float = LuneMap.HALF_EXTENT + 4.0 var v: Vector2 = p / size * extent * 2.0 - Vector2(extent, extent) return Vector3(v.x, 0, v.y) func _gui_input(event: InputEvent) -> void: if event is InputEventMouseButton: if event.pressed and event.button_index == MOUSE_BUTTON_LEFT: focus_requested.emit(to_world(event.position)) accept_event() elif event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_LEFT: focus_requested.emit(to_world(event.position)) accept_event() ## Recomputes markers from the match (called by the HUD at its update rate). func refresh(announcements: Array = []) -> void: _clusters.clear() _specials.clear() _dangers = announcements var counts: Dictionary = {"clusters": 0, "heroes": 0, "champions": 0, "boss": 0, "units": 0} if match_state != null and match_state.phase == LuneMatch.Phase.COMBAT and match_state.combat != null: var cells: Dictionary = {} for unit: LuneUnit in match_state.combat.units: if not unit.alive or not unit.is_mobile(): continue counts.units += 1 if unit.kind in ["hero", "champion", "boss"]: _specials.append([unit.kind, unit.team, unit.position, unit.owner_slot == human_slot, unit.hp / maxf(1.0, unit.max_hp)]) counts[{"hero": "heroes", "champion": "champions", "boss": "boss"}[unit.kind]] += 1 continue var key := Vector3i(unit.team, floori(unit.position.x / CELL), floori(unit.position.y / CELL)) if not cells.has(key): cells[key] = [Vector2.ZERO, 0] cells[key][0] += unit.position cells[key][1] += 1 for key: Vector3i in cells: var entry: Array = cells[key] _clusters.append([key.x, entry[0] / float(entry[1]), int(entry[1])]) counts.clusters = _clusters.size() markers = counts redraws += 1 queue_redraw() func _draw() -> void: var rect := Rect2(Vector2.ZERO, size) draw_rect(rect, Color(.035, .05, .06, .95)) if match_state == null: return var scale_factor: float = size.x / 224.0 # Ground: the Kluft from the grotto spring into the south-eastern delta, lanes, plazas. var spring: Vector2 = LuneMap.GROTTO + LuneMap.AXIS * LuneTerrain.ARENA_RADIUS draw_line(to_map(spring), to_map(Vector2(40, 40)), Color(.1, .3, .34, .9), 6.0 * scale_factor) draw_circle(to_map(Vector2(33, 33)), 10.0 * scale_factor, Color(.1, .3, .34, .9)) for team: int in range(2): draw_circle(to_map(match_state.map.core_position(team)), 17.0 * scale_factor, Color(LuneUiTheme.TEAM_DARK[team], .55)) for lane: String in LuneMap.LANES: var path: LuneMap.LanePath = match_state.map.lanes[lane] var points := PackedVector2Array() for p: Vector2 in path.points: points.append(to_map(p)) draw_polyline(points, Color(.2, .19, .16), 7.0 * scale_factor, true) draw_polyline(points, Color(.55, .5, .4), 3.2 * scale_factor, true) _draw_grotto(scale_factor) # Buildings. for definition: Dictionary in match_state.map.buildings: _draw_building(definition, scale_factor) # Announced dangers: a warning mark at the own end of the lane. for entry: Dictionary in _dangers: if int(entry.team) == viewer_team: continue var at: Vector2 = to_map(match_state.map.lane_point(viewer_team, str(entry.lane), match_state.map.lanes[str(entry.lane)].length * .5)) var edge: float = 13.0 * scale_factor draw_circle(at, edge * .62, Color(0, 0, 0, .6)) LuneGlyphs.draw(self, "crown" if entry.kind == "boss" else "warning", Rect2(at - Vector2(edge, edge) * .5, Vector2(edge, edge)), LuneUiTheme.DANGER) # Armies merged per cell. for cluster: Array in _clusters: var team: int = cluster[0] var at: Vector2 = to_map(cluster[1]) var radius: float = (2.0 + 1.15 * sqrt(float(cluster[2]))) * scale_factor var tint: Color = LuneUiTheme.team_color(team) if team <= 1 else LuneUiTheme.TEAM[2] _shape(team, at, radius + 1.2 * scale_factor, Color(0, 0, 0, .7)) _shape(team, at, radius, Color(tint, .9)) # Heroes, champions and the boss on top. for special: Array in _specials: var kind: String = special[0] var team: int = special[1] var at: Vector2 = to_map(special[2]) var tint: Color = LuneUiTheme.team_color(team) match kind: "hero": var radius: float = 4.6 * scale_factor draw_circle(at, radius + 2.4 * scale_factor, Color(0, 0, 0, .75)) draw_arc(at, radius + 1.2 * scale_factor, 0, TAU, 20, LuneUiTheme.HERO, 1.8 * scale_factor, true) _shape(team, at, radius, tint) if special[3]: draw_arc(at, radius + 3.6 * scale_factor, 0, TAU, 24, Color.WHITE, 1.2 * scale_factor, true) "champion": var edge: float = 12.0 * scale_factor draw_circle(at, edge * .55, Color(0, 0, 0, .75)) LuneGlyphs.draw(self, "champion", Rect2(at - Vector2(edge, edge) * .5, Vector2(edge, edge)), tint.lerp(LuneUiTheme.CHAMPION, .35)) "boss": var edge: float = 17.0 * scale_factor draw_circle(at, edge * .6, Color(0, 0, 0, .8)) draw_arc(at, edge * .6, 0, TAU, 24, tint, 1.6 * scale_factor, true) LuneGlyphs.draw(self, "crown", Rect2(at - Vector2(edge, edge) * .45, Vector2(edge, edge) * .9), tint.lightened(.2)) draw_rect(rect, Color("3a4656"), false, 1.0) func _shape(team: int, at: Vector2, radius: float, color: Color) -> void: if team == 1: draw_colored_polygon(PackedVector2Array([at + Vector2(0, -radius * 1.2), at + Vector2(radius * 1.2, 0), at + Vector2(0, radius * 1.2), at + Vector2(-radius * 1.2, 0)]), color) else: draw_circle(at, radius, color) func _draw_building(definition: Dictionary, scale_factor: float) -> void: var id: String = definition.id var team: int = int(definition.team) var alive: bool = match_state.building_alive(id) var hp: float = float(match_state.building_hp.get(id, 0.0)) if match_state.phase == LuneMatch.Phase.COMBAT and match_state.combat != null and match_state.combat.buildings.has(id): var unit: LuneUnit = match_state.combat.buildings[id] alive = unit.alive hp = unit.hp var fraction: float = clampf(hp / maxf(1.0, float(definition.max_hp)), 0.0, 1.0) if alive else 0.0 var at: Vector2 = to_map(definition.position) var tint: Color = LuneUiTheme.team_color(team) if definition.tier == "core": var edge: float = 18.0 * scale_factor var radius: float = 10.5 * scale_factor draw_circle(at, radius, Color(0, 0, 0, .7)) draw_arc(at, radius, 0, TAU, 32, Color(tint, .25), 2.4 * scale_factor, true) if fraction > 0.0: draw_arc(at, radius, -PI * .5, -PI * .5 + TAU * fraction, 32, LuneHpBar.health_color(fraction, tint), 2.4 * scale_factor, true) LuneGlyphs.draw(self, "emblem_%d" % team, Rect2(at - Vector2(edge, edge) * .38, Vector2(edge, edge) * .76), tint if alive else Color(.4, .4, .4)) return var radius: float = (4.4 if definition.tier == "base" else 3.8) * scale_factor if alive: _shape(team, at, radius + 1.3 * scale_factor, Color(0, 0, 0, .75)) _shape(team, at, radius, tint) if fraction < .99: var bar := Rect2(at + Vector2(-radius - 1.0, radius + 2.5 * scale_factor), Vector2(radius * 2.0 + 2.0, 2.0 * scale_factor)) draw_rect(bar, Color(0, 0, 0, .8)) draw_rect(Rect2(bar.position, Vector2(bar.size.x * fraction, bar.size.y)), LuneHpBar.health_color(fraction, tint)) else: _shape(team, at, radius, Color(0, 0, 0, .55)) var cross: float = radius * .8 draw_line(at - Vector2(cross, cross), at + Vector2(cross, cross), Color(.55, .55, .58), 1.4 * scale_factor, true) draw_line(at + Vector2(-cross, cross), at + Vector2(cross, -cross), Color(.55, .55, .58), 1.4 * scale_factor, true) func _draw_grotto(scale_factor: float) -> void: var at: Vector2 = to_map(LuneMap.GROTTO) var claim: Dictionary = match_state.boss.claim var tint: Color = LuneUiTheme.TEAM[2] if not claim.is_empty(): tint = LuneUiTheme.team_color(int(claim.team)) elif not match_state.grotto_available(): tint = Color(.42, .4, .46) var edge: float = 16.0 * scale_factor draw_circle(at, edge * .72, Color(0, 0, 0, .65)) draw_arc(at, edge * .72, 0, TAU, 24, Color(tint, .9), 1.4 * scale_factor, true) LuneGlyphs.draw(self, "grotto", Rect2(at - Vector2(edge, edge) * .42, Vector2(edge, edge) * .84), tint)