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();
}
}
}