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>
216 lines
9.9 KiB
GDScript
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)
|