using System; using System.Collections.Generic; using Luneraria.Core; using UnityEngine; using UnityEngine.UIElements; namespace Luneraria.UI { /// /// 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. /// public sealed class LuneMinimap : VisualElement { public const float Cell = 6f; /// Radius of the grotto arena (lune_terrain.gd ARENA_RADIUS). public const float ArenaRadius = 7.8f; const float Extent = (float)LuneMap.HalfExtent + 4f; const float HeroMarker = 18f; public event Action FocusRequested; public LuneMatch Match; public int ViewerTeam; public int HumanSlot; /// Counts for tests and the performance log. 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 _specials = new List(); readonly List _dangers = new List(); readonly VisualElement _heroLayer; readonly List _heroMarkers = new List(); 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(OnDown); RegisterCallback(OnMove); RegisterCallback(OnUp); RegisterCallback(_ => { 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(); } /// Camera focus, view span and the ground outline of the view (empty = rectangle around the focus). 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(); } /// Recomputes markers from the match (called by the HUD at its update rate). public void Refresh(List 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(); } /// Hero markers are portraits in rings: child elements placed over the map. 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(); } } }