Das G3-HUD steht im Spiel; die Abnahme (U3D) folgt. - U3B: alle Bereiche und Komponenten aus Godot übertragen (LuneHud, Lanes, Bossgrotte, nächste Welle, Kaufkarten, Armee, Held mit Items und Talenten, Angriffschampions, Team, Ereignisse, Minimap, Tooltip, Optionen, Partieende). Porträts aus den echten Modellen, einmal gerendert. - Offene G3-Punkte umgesetzt: „1 WELLE“ als Band am Porträtrand, Schadens- und Rüstungssymbole auf den Karten, Armeewert, Heldenporträts auf der Minimap. Befehlsplatz für die Ultimates der geplanten Helden reserviert (verborgen). - U3C: LuneGame verdrahtet Befehle (gleiche Schnittstelle wie die Bots), Bereit mit 1-s-Sperre nach dem Kampf, Tempo und Pause, Tasten wie in G3, Kamera-Ziele, Auswahl im Feld, Planungsvorschau und Aufstellungsflächen. Ersetzt die U2-Debug-Anzeige. Bot-Partien und Benchmark unverändert. - Szenen- und Bibliotheksbauer idempotent: Animator-Controller und Post-Processing-Profil werden an Ort und Stelle aktualisiert. Die Szene bekommt bei jedem Bau neue Objekt-IDs, der Inhalt ist gleich. - Prüfung (Port von verify_g3, Teile A–H): 699 Prüfungen ohne Fehler. EditMode 23/23, PlayMode 12/12, Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
441 lines
20 KiB
C#
441 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Luneraria.Core;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace Luneraria.UI
|
|
{
|
|
/// <summary>
|
|
/// Minimap from the public match state (port of scripts/luneraria/ui/lune_minimap.gd). 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 as small
|
|
/// portraits (G3 open point, Dota principle), 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. It redraws only when the HUD refreshes it (about ten times
|
|
/// a second in combat); the camera frame lives on a separate overlay.
|
|
/// </summary>
|
|
public sealed class LuneMinimap : VisualElement
|
|
{
|
|
public const float Cell = 6f;
|
|
/// <summary>Radius of the grotto arena (lune_terrain.gd ARENA_RADIUS).</summary>
|
|
public const float ArenaRadius = 7.8f;
|
|
const float Extent = (float)LuneMap.HalfExtent + 4f;
|
|
const float HeroMarker = 18f;
|
|
|
|
public event Action<Vec2> FocusRequested;
|
|
public LuneMatch Match;
|
|
public int ViewerTeam;
|
|
public int HumanSlot;
|
|
/// <summary>Counts for tests and the performance log.</summary>
|
|
public int Clusters { get; private set; }
|
|
public int Heroes { get; private set; }
|
|
public int Champions { get; private set; }
|
|
public int Bosses { get; private set; }
|
|
public int Units { get; private set; }
|
|
public int Redraws { get; private set; }
|
|
|
|
readonly List<(int team, Vector2 at, int count)> _clusters = new List<(int, Vector2, int)>();
|
|
readonly List<LuneUnit> _specials = new List<LuneUnit>();
|
|
readonly List<Announcement> _dangers = new List<Announcement>();
|
|
readonly VisualElement _heroLayer;
|
|
readonly List<VisualElement> _heroMarkers = new List<VisualElement>();
|
|
readonly VisualElement _overlay;
|
|
Vec2[] _footprint = new Vec2[0];
|
|
Vec2 _focus;
|
|
float _span = 34f;
|
|
bool _dragging;
|
|
|
|
public LuneMinimap()
|
|
{
|
|
style.width = 224f;
|
|
style.height = 224f;
|
|
style.overflow = Overflow.Hidden;
|
|
pickingMode = PickingMode.Position;
|
|
generateVisualContent += Draw;
|
|
_heroLayer = new VisualElement { pickingMode = PickingMode.Ignore };
|
|
_heroLayer.style.position = Position.Absolute;
|
|
_heroLayer.style.left = 0f;
|
|
_heroLayer.style.top = 0f;
|
|
_heroLayer.style.right = 0f;
|
|
_heroLayer.style.bottom = 0f;
|
|
Add(_heroLayer);
|
|
_overlay = new VisualElement { pickingMode = PickingMode.Ignore };
|
|
_overlay.style.position = Position.Absolute;
|
|
_overlay.style.left = 0f;
|
|
_overlay.style.top = 0f;
|
|
_overlay.style.right = 0f;
|
|
_overlay.style.bottom = 0f;
|
|
_overlay.generateVisualContent += DrawFrame;
|
|
Add(_overlay);
|
|
RegisterCallback<PointerDownEvent>(OnDown);
|
|
RegisterCallback<PointerMoveEvent>(OnMove);
|
|
RegisterCallback<PointerUpEvent>(OnUp);
|
|
RegisterCallback<GeometryChangedEvent>(_ => { MarkDirtyRepaint(); _overlay.MarkDirtyRepaint(); PlaceHeroes(); });
|
|
}
|
|
|
|
Vector2 Size { get { return new Vector2(layout.width, layout.height); } }
|
|
|
|
public Vector2 ToMap(Vec2 v)
|
|
{
|
|
Vector2 size = Size;
|
|
return new Vector2(((float)v.X + Extent) / (Extent * 2f) * size.x, ((float)v.Y + Extent) / (Extent * 2f) * size.y);
|
|
}
|
|
|
|
public Vec2 ToWorld(Vector2 p)
|
|
{
|
|
Vector2 size = Size;
|
|
return new Vec2(p.x / Mathf.Max(1f, size.x) * Extent * 2f - Extent, p.y / Mathf.Max(1f, size.y) * Extent * 2f - Extent);
|
|
}
|
|
|
|
void OnDown(PointerDownEvent evt)
|
|
{
|
|
if (evt.button != 0)
|
|
return;
|
|
_dragging = true;
|
|
this.CapturePointer(evt.pointerId);
|
|
FocusRequested?.Invoke(ToWorld(evt.localPosition));
|
|
evt.StopPropagation();
|
|
}
|
|
|
|
void OnMove(PointerMoveEvent evt)
|
|
{
|
|
if (!_dragging)
|
|
return;
|
|
FocusRequested?.Invoke(ToWorld(evt.localPosition));
|
|
evt.StopPropagation();
|
|
}
|
|
|
|
void OnUp(PointerUpEvent evt)
|
|
{
|
|
if (!_dragging)
|
|
return;
|
|
_dragging = false;
|
|
this.ReleasePointer(evt.pointerId);
|
|
evt.StopPropagation();
|
|
}
|
|
|
|
/// <summary>Camera focus, view span and the ground outline of the view (empty = rectangle around the focus).</summary>
|
|
public void SetCamera(Vec2 focus, float span, Vec2[] footprint)
|
|
{
|
|
bool moved = footprint == null || footprint.Length != _footprint.Length;
|
|
if (!moved)
|
|
{
|
|
for (int index = 0; index < footprint.Length; index++)
|
|
{
|
|
if (footprint[index].DistanceSquaredTo(_footprint[index]) > .04)
|
|
moved = true;
|
|
}
|
|
}
|
|
moved |= focus.DistanceSquaredTo(_focus) > .04 || Mathf.Abs(span - _span) > .1f;
|
|
if (!moved)
|
|
return;
|
|
_footprint = footprint ?? new Vec2[0];
|
|
_focus = focus;
|
|
_span = span;
|
|
_overlay.MarkDirtyRepaint();
|
|
}
|
|
|
|
/// <summary>Recomputes markers from the match (called by the HUD at its update rate).</summary>
|
|
public void Refresh(List<Announcement> announcements)
|
|
{
|
|
_clusters.Clear();
|
|
_specials.Clear();
|
|
_dangers.Clear();
|
|
if (announcements != null)
|
|
_dangers.AddRange(announcements);
|
|
Clusters = Heroes = Champions = Bosses = Units = 0;
|
|
if (Match != null && Match.Phase == Phase.Combat && Match.Combat != null)
|
|
{
|
|
var cells = new Dictionary<(int, int, int), (Vector2 sum, int count)>();
|
|
var order = new List<(int, int, int)>();
|
|
foreach (LuneUnit unit in Match.Combat.Units)
|
|
{
|
|
if (!unit.Alive || !unit.IsMobile())
|
|
continue;
|
|
Units++;
|
|
if (unit.Kind == "hero" || unit.Kind == "champion" || unit.Kind == "boss")
|
|
{
|
|
_specials.Add(unit);
|
|
if (unit.Kind == "hero")
|
|
Heroes++;
|
|
else if (unit.Kind == "champion")
|
|
Champions++;
|
|
else
|
|
Bosses++;
|
|
continue;
|
|
}
|
|
var key = (unit.Team, (int)Math.Floor(unit.Position.X / Cell), (int)Math.Floor(unit.Position.Y / Cell));
|
|
if (!cells.TryGetValue(key, out var entry))
|
|
{
|
|
entry = (Vector2.zero, 0);
|
|
order.Add(key);
|
|
}
|
|
cells[key] = (entry.sum + new Vector2((float)unit.Position.X, (float)unit.Position.Y), entry.count + 1);
|
|
}
|
|
foreach (var key in order)
|
|
{
|
|
var entry = cells[key];
|
|
_clusters.Add((key.Item1, entry.sum / entry.count, entry.count));
|
|
}
|
|
Clusters = _clusters.Count;
|
|
}
|
|
Redraws++;
|
|
MarkDirtyRepaint();
|
|
PlaceHeroes();
|
|
}
|
|
|
|
/// <summary>Hero markers are portraits in rings: child elements placed over the map.</summary>
|
|
void PlaceHeroes()
|
|
{
|
|
int used = 0;
|
|
float scale = Size.x / 224f;
|
|
if (float.IsNaN(scale) || scale <= 0f)
|
|
return;
|
|
foreach (LuneUnit unit in _specials)
|
|
{
|
|
if (unit.Kind != "hero")
|
|
continue;
|
|
if (used >= _heroMarkers.Count)
|
|
{
|
|
var marker = new VisualElement { pickingMode = PickingMode.Ignore };
|
|
marker.style.position = Position.Absolute;
|
|
marker.style.overflow = Overflow.Hidden;
|
|
var portrait = new LunePortraitElement();
|
|
portrait.style.flexGrow = 1f;
|
|
marker.Add(portrait);
|
|
_heroLayer.Add(marker);
|
|
_heroMarkers.Add(marker);
|
|
}
|
|
VisualElement element = _heroMarkers[used++];
|
|
float edge = HeroMarker * scale;
|
|
Vector2 at = ToMap(unit.Position);
|
|
element.style.left = at.x - edge * .5f;
|
|
element.style.top = at.y - edge * .5f;
|
|
element.style.width = edge;
|
|
element.style.height = edge;
|
|
float radius = edge * .5f;
|
|
element.style.borderTopLeftRadius = radius;
|
|
element.style.borderTopRightRadius = radius;
|
|
element.style.borderBottomLeftRadius = radius;
|
|
element.style.borderBottomRightRadius = radius;
|
|
Color ring = unit.OwnerSlot == HumanSlot ? Color.white : LuneUiTheme.Hero;
|
|
float width = 1.6f * scale;
|
|
element.style.borderLeftWidth = width;
|
|
element.style.borderRightWidth = width;
|
|
element.style.borderTopWidth = width;
|
|
element.style.borderBottomWidth = width;
|
|
element.style.borderLeftColor = ring;
|
|
element.style.borderRightColor = ring;
|
|
element.style.borderTopColor = ring;
|
|
element.style.borderBottomColor = ring;
|
|
element.style.backgroundColor = LuneUiTheme.TeamColor(unit.Team);
|
|
((LunePortraitElement)element[0]).ShowModel(unit.Def.Id);
|
|
LuneUi.Show(element, true);
|
|
}
|
|
for (int index = used; index < _heroMarkers.Count; index++)
|
|
LuneUi.Show(_heroMarkers[index], false);
|
|
}
|
|
|
|
void Draw(MeshGenerationContext context)
|
|
{
|
|
Painter2D p = context.painter2D;
|
|
Vector2 size = Size;
|
|
if (size.x <= 0f)
|
|
return;
|
|
LunePlates.Polygon(p, LuneHpBar.Corners(new Rect(0f, 0f, size.x, size.y)), new Color(.035f, .05f, .06f, .95f));
|
|
if (Match == null)
|
|
return;
|
|
float s = size.x / 224f;
|
|
// Ground: the Kluft from the grotto spring into the south-eastern delta, lanes, plazas.
|
|
Vec2 spring = LuneMap.Grotto + LuneMap.Axis * ArenaRadius;
|
|
Polyline(p, new[] { ToMap(spring), ToMap(new Vec2(40, 40)) }, new Color(.1f, .3f, .34f, .9f), 6f * s);
|
|
Disc(p, ToMap(new Vec2(33, 33)), 10f * s, new Color(.1f, .3f, .34f, .9f));
|
|
for (int team = 0; team < 2; team++)
|
|
Disc(p, ToMap(Match.Map.CorePosition(team)), 17f * s, LuneUiTheme.WithAlpha(LuneUiTheme.TeamDark[team], .55f));
|
|
foreach (string lane in LuneMap.Lanes)
|
|
{
|
|
Vec2[] points = Match.Map.LanePaths[lane].Points;
|
|
var mapped = new Vector2[points.Length];
|
|
for (int index = 0; index < points.Length; index++)
|
|
mapped[index] = ToMap(points[index]);
|
|
Polyline(p, mapped, new Color(.2f, .19f, .16f), 7f * s);
|
|
Polyline(p, mapped, new Color(.55f, .5f, .4f), 3.2f * s);
|
|
}
|
|
DrawGrotto(p, s);
|
|
foreach (BuildingDef definition in Match.Map.Buildings)
|
|
DrawBuilding(p, definition, s);
|
|
// Announced dangers: a warning mark in the middle of the lane.
|
|
foreach (Announcement entry in _dangers)
|
|
{
|
|
if (entry.Team == ViewerTeam)
|
|
continue;
|
|
Vector2 at = ToMap(Match.Map.LanePoint(ViewerTeam, entry.Lane, Match.Map.LanePaths[entry.Lane].Length * .5));
|
|
float edge = 13f * s;
|
|
Disc(p, at, edge * .62f, new Color(0f, 0f, 0f, .6f));
|
|
LuneGlyphs.Draw(p, entry.Kind == "boss" ? "crown" : "warning", new Rect(at - new Vector2(edge, edge) * .5f, new Vector2(edge, edge)), LuneUiTheme.Danger);
|
|
}
|
|
// Armies merged per cell.
|
|
foreach (var cluster in _clusters)
|
|
{
|
|
Vector2 at = ToMap(new Vec2(cluster.at.x, cluster.at.y));
|
|
float radius = (2f + 1.15f * Mathf.Sqrt(cluster.count)) * s;
|
|
Color tint = cluster.team <= 1 ? LuneUiTheme.TeamColor(cluster.team) : LuneUiTheme.Team[2];
|
|
Shape(p, cluster.team, at, radius + 1.2f * s, new Color(0f, 0f, 0f, .7f));
|
|
Shape(p, cluster.team, at, radius, LuneUiTheme.WithAlpha(tint, .9f));
|
|
}
|
|
// Champions and the boss on top (heroes are portrait elements over the map).
|
|
foreach (LuneUnit unit in _specials)
|
|
{
|
|
Vector2 at = ToMap(unit.Position);
|
|
Color tint = LuneUiTheme.TeamColor(unit.Team);
|
|
if (unit.Kind == "hero")
|
|
{
|
|
Disc(p, at, (HeroMarker * .5f + 1.5f) * s, new Color(0f, 0f, 0f, .75f));
|
|
}
|
|
else if (unit.Kind == "champion")
|
|
{
|
|
float edge = 12f * s;
|
|
Disc(p, at, edge * .55f, new Color(0f, 0f, 0f, .75f));
|
|
LuneGlyphs.Draw(p, "champion", new Rect(at - new Vector2(edge, edge) * .5f, new Vector2(edge, edge)), Color.Lerp(tint, LuneUiTheme.Champion, .35f));
|
|
}
|
|
else
|
|
{
|
|
float edge = 17f * s;
|
|
Disc(p, at, edge * .6f, new Color(0f, 0f, 0f, .8f));
|
|
Ring(p, at, edge * .6f, 0f, Mathf.PI * 2f, tint, 1.6f * s);
|
|
LuneGlyphs.Draw(p, "crown", new Rect(at - new Vector2(edge, edge) * .45f, new Vector2(edge, edge) * .9f), LuneUiTheme.Lightened(tint, .2f));
|
|
}
|
|
}
|
|
LunePlates.Outline(p, LuneHpBar.Corners(new Rect(.5f, .5f, size.x - 1f, size.y - 1f)), LuneUiTheme.Hex("3a4656"), 1f);
|
|
}
|
|
|
|
void DrawFrame(MeshGenerationContext context)
|
|
{
|
|
Painter2D p = context.painter2D;
|
|
Vector2 size = Size;
|
|
if (size.x <= 0f)
|
|
return;
|
|
if (_footprint.Length == 4)
|
|
{
|
|
var outline = new Vector2[4];
|
|
for (int index = 0; index < 4; index++)
|
|
{
|
|
Vector2 corner = ToMap(_footprint[index]);
|
|
outline[index] = new Vector2(Mathf.Clamp(corner.x, 0f, size.x), Mathf.Clamp(corner.y, 0f, size.y));
|
|
}
|
|
LunePlates.Polygon(p, outline, new Color(1f, 1f, 1f, .05f));
|
|
LunePlates.Outline(p, outline, new Color(1f, 1f, 1f, .75f), 1.3f);
|
|
}
|
|
else
|
|
{
|
|
Vector2 center = ToMap(_focus);
|
|
float span = _span / (Extent * 2f) * size.x;
|
|
LunePlates.Outline(p, LuneHpBar.Corners(new Rect(center.x - span * .9f, center.y - span * .55f, span * 1.8f, span * 1.1f)), new Color(1f, 1f, 1f, .6f), 1.2f);
|
|
}
|
|
}
|
|
|
|
static void Shape(Painter2D p, int team, Vector2 at, float radius, Color color)
|
|
{
|
|
if (team == 1)
|
|
LunePlates.Polygon(p, new[] { at + new Vector2(0f, -radius * 1.2f), at + new Vector2(radius * 1.2f, 0f), at + new Vector2(0f, radius * 1.2f), at + new Vector2(-radius * 1.2f, 0f) }, color);
|
|
else
|
|
Disc(p, at, radius, color);
|
|
}
|
|
|
|
void DrawBuilding(Painter2D p, BuildingDef definition, float s)
|
|
{
|
|
string id = definition.Id;
|
|
int team = definition.Team;
|
|
bool alive = Match.BuildingAlive(id);
|
|
double hp = Match.BuildingHp.GetValueOrDefault(id, 0.0);
|
|
if (Match.Phase == Phase.Combat && Match.Combat != null && Match.Combat.Buildings.TryGetValue(id, out LuneUnit unit))
|
|
{
|
|
alive = unit.Alive;
|
|
hp = unit.Hp;
|
|
}
|
|
float fraction = alive ? Mathf.Clamp01((float)(hp / Math.Max(1.0, definition.MaxHp))) : 0f;
|
|
Vector2 at = ToMap(definition.Position);
|
|
Color tint = LuneUiTheme.TeamColor(team);
|
|
if (definition.Tier == "core")
|
|
{
|
|
float edge = 18f * s;
|
|
float radius = 10.5f * s;
|
|
Disc(p, at, radius, new Color(0f, 0f, 0f, .7f));
|
|
Ring(p, at, radius, 0f, Mathf.PI * 2f, LuneUiTheme.WithAlpha(tint, .25f), 2.4f * s);
|
|
if (fraction > 0f)
|
|
Ring(p, at, radius, -Mathf.PI * .5f, -Mathf.PI * .5f + Mathf.PI * 2f * fraction, LuneHpBar.HealthColor(fraction, tint), 2.4f * s);
|
|
LuneGlyphs.Draw(p, "emblem_" + team, new Rect(at - new Vector2(edge, edge) * .38f, new Vector2(edge, edge) * .76f), alive ? tint : new Color(.4f, .4f, .4f));
|
|
return;
|
|
}
|
|
float r = (definition.Tier == "base" ? 4.4f : 3.8f) * s;
|
|
if (alive)
|
|
{
|
|
Shape(p, team, at, r + 1.3f * s, new Color(0f, 0f, 0f, .75f));
|
|
Shape(p, team, at, r, tint);
|
|
if (fraction < .99f)
|
|
{
|
|
var bar = new Rect(at.x - r - 1f, at.y + r + 2.5f * s, r * 2f + 2f, 2f * s);
|
|
LunePlates.Polygon(p, LuneHpBar.Corners(bar), new Color(0f, 0f, 0f, .8f));
|
|
LunePlates.Polygon(p, LuneHpBar.Corners(new Rect(bar.x, bar.y, bar.width * fraction, bar.height)), LuneHpBar.HealthColor(fraction, tint));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Shape(p, team, at, r, new Color(0f, 0f, 0f, .55f));
|
|
float cross = r * .8f;
|
|
var gray = new Color(.55f, .55f, .58f);
|
|
LunePlates.Line(p, at - new Vector2(cross, cross), at + new Vector2(cross, cross), gray, 1.4f * s);
|
|
LunePlates.Line(p, at + new Vector2(-cross, cross), at + new Vector2(cross, -cross), gray, 1.4f * s);
|
|
}
|
|
}
|
|
|
|
void DrawGrotto(Painter2D p, float s)
|
|
{
|
|
Vector2 at = ToMap(LuneMap.Grotto);
|
|
BossClaim claim = Match.Boss.Claim;
|
|
Color tint = LuneUiTheme.Team[2];
|
|
if (claim != null)
|
|
tint = LuneUiTheme.TeamColor(claim.Team);
|
|
else if (!Match.GrottoAvailable())
|
|
tint = new Color(.42f, .4f, .46f);
|
|
float edge = 16f * s;
|
|
Disc(p, at, edge * .72f, new Color(0f, 0f, 0f, .65f));
|
|
Ring(p, at, edge * .72f, 0f, Mathf.PI * 2f, LuneUiTheme.WithAlpha(tint, .9f), 1.4f * s);
|
|
LuneGlyphs.Draw(p, "grotto", new Rect(at - new Vector2(edge, edge) * .42f, new Vector2(edge, edge) * .84f), tint);
|
|
}
|
|
|
|
static void Disc(Painter2D p, Vector2 center, float radius, Color color)
|
|
{
|
|
p.fillColor = color;
|
|
p.BeginPath();
|
|
p.Arc(center, radius, Angle.Degrees(0f), Angle.Degrees(360f));
|
|
p.ClosePath();
|
|
p.Fill();
|
|
}
|
|
|
|
static void Ring(Painter2D p, Vector2 center, float radius, float start, float end, Color color, float width)
|
|
{
|
|
p.strokeColor = color;
|
|
p.lineWidth = width;
|
|
p.lineCap = LineCap.Butt;
|
|
p.BeginPath();
|
|
p.Arc(center, radius, Angle.Radians(start), Angle.Radians(end));
|
|
p.Stroke();
|
|
}
|
|
|
|
static void Polyline(Painter2D p, Vector2[] points, Color color, float width)
|
|
{
|
|
p.strokeColor = color;
|
|
p.lineWidth = width;
|
|
p.lineJoin = LineJoin.Round;
|
|
p.lineCap = LineCap.Round;
|
|
p.BeginPath();
|
|
p.MoveTo(points[0]);
|
|
for (int index = 1; index < points.Length; index++)
|
|
p.LineTo(points[index]);
|
|
p.Stroke();
|
|
}
|
|
}
|
|
}
|