saumwacht-godot/scripts/luneraria/ui/lune_minimap.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

216 lines
9.9 KiB
GDScript

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)