using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace Luneraria.UI { /// /// Vector symbols of the HUD (port of scripts/luneraria/ui/theme/lune_glyphs.gd), drawn from a /// unit square so they stay sharp at every UI scale. Team emblems follow the G2 world: Blau = /// Mondsichel (round shapes), Rot = Sonnenscheibe (rayed, angular), the grotto = hexagon. /// public static class LuneGlyphs { public static readonly string[] Names = { "coin", "emblem_0", "emblem_1", "emblem_2", "shape_0", "shape_1", "shape_2", "tank", "melee", "ranged", "splash", "support", "siege", "crown", "champion", "hourglass", "tower", "ancient", "warning", "check", "close", "bot", "offline", "clock", "heart", "tempo", "range", "armor", "move", "star", "lock", "up", "lane_top", "lane_mid", "lane_bot", "grotto", "pause", "play", "menu", "gear", "human", "sword", "basaltschild", "glutklinge", "rankenmantel", "funkenfokus", "quellamulett", "kristallsplitter", "hero", "skull", "bollwerk", "glutsturm", "quellherz", "splitterkrone" }; const float Tau = Mathf.PI * 2f; static readonly Dictionary Cache = new Dictionary(); /// Draws one symbol into rect (unit square centred, aspect kept). `accent` is an optional second colour for inner details. public static void Draw(Painter2D painter, string glyph, Rect rect, Color color, Color accent = default) { if (string.IsNullOrEmpty(glyph)) return; var f = new Frame(rect); Color dark = accent.a > 0f ? accent : new Color(color.r * .45f, color.g * .45f, color.b * .45f, color.a); // S2 recipes: the first part's symbol inside a forged ring with a small spark. Luneraria.Core.ItemSpec recipe; if (Luneraria.Core.ContentData.Items.TryGetValue(glyph, out recipe) && recipe.IsRecipe) { float inset = rect.width * .16f; Draw(painter, recipe.Parts[0], new Rect(rect.x + inset, rect.y + inset, rect.width - 2f * inset, rect.height - 2f * inset), color, accent); ArcLine(painter, f.P(.5f, .5f), f.S * .47f, 0f, Tau, color, Mathf.Max(1f, f.S * .06f)); Shard(painter, f, V(.82f, .18f), 0f, .14f, LuneUiTheme.Hex("fff2c0")); return; } switch (glyph) { case "coin": { Vector2 c = f.P(.5f, .5f); float r = f.S * .42f; Disc(painter, c, r, color); ArcLine(painter, c, r, 0f, Tau, LuneUiTheme.Darkened(color, .35f), Mathf.Max(1f, r * .12f)); ArcLine(painter, c, r * .62f, 0f, Tau, LuneUiTheme.Darkened(color, .28f), Mathf.Max(1f, r * .1f)); ArcLine(painter, c, r * .8f, Mathf.PI * 1.1f, Mathf.PI * 1.45f, new Color(1f, 1f, 1f, .55f * color.a), Mathf.Max(1f, r * .1f)); break; } case "emblem_0": Fill(painter, f, Crescent(), color); break; case "emblem_1": Disc(painter, f.P(.5f, .5f), f.S * .2f, color); for (int index = 0; index < 8; index++) { float angle = Tau * index / 8f - Mathf.PI * .5f; Vector2 tip = new Vector2(.5f, .5f) + Dir(angle) * .47f; Vector2 left = new Vector2(.5f, .5f) + Dir(angle - .22f) * .27f; Vector2 right = new Vector2(.5f, .5f) + Dir(angle + .22f) * .27f; Fill(painter, f, new[] { left, tip, right }, color); } break; case "emblem_2": { List hexagon = Circle(6, new Vector2(.5f, .5f), .42f, -Mathf.PI * .5f); hexagon.Add(hexagon[0]); Line(painter, f, hexagon.ToArray(), color, .08f); Fill(painter, f, new[] { V(.5f, .3f), V(.64f, .5f), V(.5f, .7f), V(.36f, .5f) }, color); break; } case "shape_0": Disc(painter, f.P(.5f, .5f), f.S * .4f, color); break; case "shape_1": Fill(painter, f, new[] { V(.5f, .06f), V(.94f, .5f), V(.5f, .94f), V(.06f, .5f) }, color); break; case "shape_2": Fill(painter, f, Circle(6, new Vector2(.5f, .5f), .44f, -Mathf.PI * .5f).ToArray(), color); break; case "tank": case "armor": { var shield = new List { V(.2f, .16f), V(.8f, .16f), V(.8f, .48f), V(.72f, .66f), V(.5f, .87f), V(.28f, .66f), V(.2f, .48f) }; if (glyph == "tank") { Fill(painter, f, shield.ToArray(), color); Line(painter, f, new[] { V(.5f, .24f), V(.5f, .74f) }, dark, .07f); } else { shield.Add(shield[0]); Line(painter, f, shield.ToArray(), color, .09f); } break; } case "melee": case "sword": { Vector2[] blade = { V(.5f, .06f), V(.56f, .14f), V(.56f, .62f), V(.44f, .62f), V(.44f, .14f) }; Fill(painter, f, Rot(blade, Mathf.PI * .25f), color); Fill(painter, f, Rot(new[] { V(.3f, .62f), V(.7f, .62f), V(.7f, .69f), V(.3f, .69f) }, Mathf.PI * .25f), color); Fill(painter, f, Rot(new[] { V(.46f, .69f), V(.54f, .69f), V(.54f, .88f), V(.46f, .88f) }, Mathf.PI * .25f), color); break; } case "ranged": { var bow = new Vector2[13]; for (int index = 0; index < 13; index++) { float angle = Mathf.PI * .62f + Mathf.PI * .76f * index / 12f; bow[index] = new Vector2(.78f, .5f) + Dir(angle) * .5f; } Line(painter, f, Rot(bow, -Mathf.PI * .25f), color, .08f); Line(painter, f, Rot(new[] { bow[0], bow[12] }, -Mathf.PI * .25f), color, .03f); Line(painter, f, Rot(new[] { V(.14f, .5f), V(.82f, .5f) }, -Mathf.PI * .25f), color, .06f); Fill(painter, f, Rot(new[] { V(.9f, .5f), V(.76f, .41f), V(.76f, .59f) }, -Mathf.PI * .25f), color); break; } case "splash": Fill(painter, f, Star(8, .46f, .2f), color); Disc(painter, f.P(.5f, .5f), f.S * .1f, dark); break; case "support": Fill(painter, f, new[] { V(.4f, .14f), V(.6f, .14f), V(.6f, .4f), V(.86f, .4f), V(.86f, .6f), V(.6f, .6f), V(.6f, .86f), V(.4f, .86f), V(.4f, .6f), V(.14f, .6f), V(.14f, .4f), V(.4f, .4f) }, color); break; case "siege": Fill(painter, f, Rot(new[] { V(.18f, .18f), V(.74f, .18f), V(.74f, .42f), V(.18f, .42f) }, -Mathf.PI * .2f), color); Fill(painter, f, Rot(new[] { V(.42f, .4f), V(.52f, .4f), V(.52f, .92f), V(.42f, .92f) }, -Mathf.PI * .2f), color); break; case "crown": case "hero": Fill(painter, f, new[] { V(.12f, .74f), V(.12f, .3f), V(.32f, .5f), V(.5f, .2f), V(.68f, .5f), V(.88f, .3f), V(.88f, .74f) }, color); Fill(painter, f, new[] { V(.12f, .78f), V(.88f, .78f), V(.88f, .88f), V(.12f, .88f) }, color); if (glyph == "crown") Disc(painter, f.P(.5f, .6f), f.S * .06f, dark); break; case "skull": Fill(painter, f, Circle(20, new Vector2(.5f, .44f), .34f).ToArray(), color); Fill(painter, f, new[] { V(.32f, .6f), V(.68f, .6f), V(.64f, .86f), V(.36f, .86f) }, color); Disc(painter, f.P(.37f, .45f), f.S * .09f, dark); Disc(painter, f.P(.63f, .45f), f.S * .09f, dark); break; case "champion": Fill(painter, f, new[] { V(.5f, .06f), V(.82f, .46f), V(.61f, .46f), V(.61f, .9f), V(.39f, .9f), V(.39f, .46f), V(.18f, .46f) }, color); break; case "hourglass": Fill(painter, f, new[] { V(.24f, .12f), V(.76f, .12f), V(.56f, .5f), V(.44f, .5f) }, color); Fill(painter, f, new[] { V(.44f, .5f), V(.56f, .5f), V(.76f, .88f), V(.24f, .88f) }, color); Line(painter, f, new[] { V(.18f, .1f), V(.82f, .1f) }, color, .06f); Line(painter, f, new[] { V(.18f, .9f), V(.82f, .9f) }, color, .06f); break; case "tower": Fill(painter, f, new[] { V(.28f, .92f), V(.28f, .36f), V(.22f, .36f), V(.22f, .14f), V(.33f, .14f), V(.33f, .23f), V(.44f, .23f), V(.44f, .14f), V(.56f, .14f), V(.56f, .23f), V(.67f, .23f), V(.67f, .14f), V(.78f, .14f), V(.78f, .36f), V(.72f, .36f), V(.72f, .92f) }, color); Fill(painter, f, new[] { V(.44f, .92f), V(.44f, .72f), V(.5f, .66f), V(.56f, .72f), V(.56f, .92f) }, dark); break; case "ancient": Fill(painter, f, new[] { V(.5f, .05f), V(.76f, .4f), V(.6f, .95f), V(.4f, .95f), V(.24f, .4f) }, color); Line(painter, f, new[] { V(.5f, .05f), V(.5f, .95f) }, dark, .04f); Line(painter, f, new[] { V(.24f, .4f), V(.5f, .52f), V(.76f, .4f) }, dark, .04f); break; case "warning": Fill(painter, f, new[] { V(.5f, .08f), V(.94f, .88f), V(.06f, .88f) }, color); Fill(painter, f, new[] { V(.46f, .34f), V(.54f, .34f), V(.53f, .64f), V(.47f, .64f) }, dark); Disc(painter, f.P(.5f, .75f), f.S * .05f, dark); break; case "check": Line(painter, f, new[] { V(.16f, .52f), V(.4f, .76f), V(.86f, .24f) }, color, .13f); break; case "close": Line(painter, f, new[] { V(.2f, .2f), V(.8f, .8f) }, color, .12f); Line(painter, f, new[] { V(.8f, .2f), V(.2f, .8f) }, color, .12f); break; case "bot": case "gear": { Vector2 c = f.P(.5f, .5f); for (int index = 0; index < 8; index++) Fill(painter, f, Rot(new[] { V(.43f, .06f), V(.57f, .06f), V(.57f, .24f), V(.43f, .24f) }, Tau * index / 8f), color); Disc(painter, c, f.S * .3f, color); Disc(painter, c, f.S * .12f, dark); break; } case "human": Disc(painter, f.P(.5f, .3f), f.S * .17f, color); Fill(painter, f, new[] { V(.2f, .9f), V(.24f, .62f), V(.38f, .52f), V(.62f, .52f), V(.76f, .62f), V(.8f, .9f) }, color); break; case "offline": ArcLine(painter, f.P(.5f, .5f), f.S * .38f, 0f, Tau, color, Mathf.Max(1f, f.S * .1f)); Line(painter, f, new[] { V(.22f, .78f), V(.78f, .22f) }, color, .1f); break; case "clock": ArcLine(painter, f.P(.5f, .5f), f.S * .4f, 0f, Tau, color, Mathf.Max(1f, f.S * .09f)); Line(painter, f, new[] { V(.5f, .24f), V(.5f, .5f), V(.68f, .62f) }, color, .08f); break; case "heart": Fill(painter, f, Heart(), color); break; case "tempo": Fill(painter, f, new[] { V(.6f, .04f), V(.22f, .56f), V(.46f, .56f), V(.38f, .96f), V(.78f, .42f), V(.54f, .42f), V(.66f, .04f) }, color); break; case "range": { Vector2 c = f.P(.5f, .5f); ArcLine(painter, c, f.S * .38f, 0f, Tau, color, Mathf.Max(1f, f.S * .08f)); ArcLine(painter, c, f.S * .18f, 0f, Tau, color, Mathf.Max(1f, f.S * .08f)); Disc(painter, c, f.S * .06f, color); Line(painter, f, new[] { V(.5f, .02f), V(.5f, .16f) }, color, .07f); Line(painter, f, new[] { V(.5f, .84f), V(.5f, .98f) }, color, .07f); Line(painter, f, new[] { V(.02f, .5f), V(.16f, .5f) }, color, .07f); Line(painter, f, new[] { V(.84f, .5f), V(.98f, .5f) }, color, .07f); break; } case "move": Line(painter, f, new[] { V(.18f, .2f), V(.46f, .5f), V(.18f, .8f) }, color, .11f); Line(painter, f, new[] { V(.52f, .2f), V(.8f, .5f), V(.52f, .8f) }, color, .11f); break; case "star": Fill(painter, f, Star(5, .47f, .2f), color); break; case "lock": Fill(painter, f, new[] { V(.22f, .46f), V(.78f, .46f), V(.78f, .9f), V(.22f, .9f) }, color); ArcLine(painter, f.P(.5f, .46f), f.S * .19f, Mathf.PI, Tau, color, Mathf.Max(1f, f.S * .09f)); Disc(painter, f.P(.5f, .64f), f.S * .06f, dark); break; case "up": Fill(painter, f, new[] { V(.5f, .08f), V(.86f, .48f), V(.62f, .48f), V(.62f, .9f), V(.38f, .9f), V(.38f, .48f), V(.14f, .48f) }, color); break; case "lane_top": case "lane_mid": case "lane_bot": { Line(painter, f, new[] { V(.12f, .12f), V(.88f, .12f), V(.88f, .88f), V(.12f, .88f), V(.12f, .12f) }, LuneUiTheme.WithAlpha(color, color.a * .3f), .05f); Vector2[] path = glyph == "lane_top" ? new[] { V(.2f, .8f), V(.2f, .2f), V(.8f, .2f) } : glyph == "lane_mid" ? new[] { V(.2f, .8f), V(.8f, .2f) } : new[] { V(.2f, .8f), V(.8f, .8f), V(.8f, .2f) }; Line(painter, f, path, color, .12f); break; } case "grotto": { var arch = new List { V(.08f, .9f) }; for (int index = 0; index < 13; index++) { float angle = Mathf.PI + Mathf.PI * index / 12f; arch.Add(new Vector2(.5f, .62f) + new Vector2(Mathf.Cos(angle) * .42f, Mathf.Sin(angle) * .5f)); } arch.Add(V(.92f, .9f)); Fill(painter, f, arch.ToArray(), color); var mouth = new List { V(.3f, .9f) }; for (int index = 0; index < 9; index++) { float angle = Mathf.PI + Mathf.PI * index / 8f; mouth.Add(new Vector2(.5f, .72f) + new Vector2(Mathf.Cos(angle) * .2f, Mathf.Sin(angle) * .26f)); } mouth.Add(V(.7f, .9f)); Fill(painter, f, mouth.ToArray(), dark); break; } case "pause": Fill(painter, f, new[] { V(.24f, .16f), V(.42f, .16f), V(.42f, .84f), V(.24f, .84f) }, color); Fill(painter, f, new[] { V(.58f, .16f), V(.76f, .16f), V(.76f, .84f), V(.58f, .84f) }, color); break; case "play": Fill(painter, f, new[] { V(.26f, .14f), V(.84f, .5f), V(.26f, .86f) }, color); break; case "menu": foreach (float y in new[] { .24f, .5f, .76f }) Line(painter, f, new[] { V(.16f, y), V(.84f, y) }, color, .1f); break; case "basaltschild": { Fill(painter, f, Circle(6, new Vector2(.5f, .5f), .44f, -Mathf.PI * .5f).ToArray(), color); List inner = Circle(6, new Vector2(.5f, .5f), .27f, -Mathf.PI * .5f); inner.Add(inner[0]); Line(painter, f, inner.ToArray(), dark, .07f); break; } case "glutklinge": { Vector2[] blade = { V(.5f, .06f), V(.57f, .15f), V(.57f, .66f), V(.43f, .66f), V(.43f, .15f) }; Fill(painter, f, Rot(blade, Mathf.PI * .25f), color); Fill(painter, f, Rot(new[] { V(.3f, .66f), V(.7f, .66f), V(.7f, .73f), V(.3f, .73f) }, Mathf.PI * .25f), color); Fill(painter, f, new[] { V(.2f, .5f), V(.3f, .3f), V(.36f, .42f), V(.42f, .22f), V(.5f, .44f), V(.4f, .6f) }, accent.a > 0f ? dark : LuneUiTheme.Hex("ffb070")); break; } case "rankenmantel": Fill(painter, f, new[] { V(.5f, .1f), V(.66f, .18f), V(.8f, .9f), V(.5f, .8f), V(.2f, .9f), V(.34f, .18f) }, color); Line(painter, f, new[] { V(.5f, .22f), V(.42f, .4f), V(.56f, .56f), V(.46f, .74f) }, dark, .05f); break; case "funkenfokus": ArcLine(painter, f.P(.5f, .5f), f.S * .42f, 0f, Tau, color, Mathf.Max(1f, f.S * .08f)); Fill(painter, f, new[] { V(.56f, .16f), V(.3f, .54f), V(.47f, .54f), V(.42f, .84f), V(.7f, .44f), V(.53f, .44f), V(.62f, .16f) }, color); break; case "quellamulett": ArcLine(painter, f.P(.5f, .2f), f.S * .14f, Mathf.PI * .9f, Mathf.PI * 2.1f, color, Mathf.Max(1f, f.S * .06f)); Fill(painter, f, Drop(), color); Disc(painter, f.P(.44f, .66f), f.S * .06f, new Color(1f, 1f, 1f, .45f * color.a)); break; case "kristallsplitter": Shard(painter, f, V(.5f, .5f), 0f, .44f, color); Shard(painter, f, V(.3f, .62f), -.55f, .3f, color); Shard(painter, f, V(.7f, .62f), .55f, .3f, color); break; default: Disc(painter, f.P(.5f, .5f), f.S * .3f, color); break; } } static void Shard(Painter2D painter, Frame f, Vector2 center, float angle, float length, Color color) { Vector2[] shard = { center + new Vector2(0f, -length), center + new Vector2(.08f, 0f), center + new Vector2(0f, length * .45f), center + new Vector2(-.08f, 0f) }; Fill(painter, f, Rot(shard, angle, center), color); } /// The unit square fitted into a rect (centred, aspect kept). struct Frame { public readonly float S; public readonly Vector2 Origin; public Frame(Rect rect) { S = Mathf.Min(rect.width, rect.height); Origin = rect.position + (rect.size - new Vector2(S, S)) * .5f; } public Vector2 P(float x, float y) { return Origin + new Vector2(x, y) * S; } public Vector2 P(Vector2 p) { return Origin + p * S; } } static Vector2 V(float x, float y) { return new Vector2(x, y); } static Vector2 Dir(float angle) { return new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); } static Vector2[] Rot(Vector2[] points, float angle) { return Rot(points, angle, new Vector2(.5f, .5f)); } static Vector2[] Rot(Vector2[] points, float angle, Vector2 pivot) { var result = new Vector2[points.Length]; float c = Mathf.Cos(angle); float s = Mathf.Sin(angle); for (int index = 0; index < points.Length; index++) { Vector2 d = points[index] - pivot; result[index] = pivot + new Vector2(d.x * c - d.y * s, d.x * s + d.y * c); } return result; } static void Fill(Painter2D painter, Frame f, Vector2[] points, Color color) { if (points.Length < 3) return; painter.fillColor = color; painter.BeginPath(); painter.MoveTo(f.P(points[0])); for (int index = 1; index < points.Length; index++) painter.LineTo(f.P(points[index])); painter.ClosePath(); painter.Fill(FillRule.NonZero); } static void Line(Painter2D painter, Frame f, Vector2[] points, Color color, float width) { painter.strokeColor = color; painter.lineWidth = Mathf.Max(1f, width * f.S); painter.lineJoin = LineJoin.Miter; painter.lineCap = LineCap.Butt; painter.BeginPath(); painter.MoveTo(f.P(points[0])); for (int index = 1; index < points.Length; index++) painter.LineTo(f.P(points[index])); painter.Stroke(); } static void Disc(Painter2D painter, Vector2 center, float radius, Color color) { painter.fillColor = color; painter.BeginPath(); painter.Arc(center, radius, Angle.Degrees(0f), Angle.Degrees(360f)); painter.ClosePath(); painter.Fill(); } /// Godot draw_arc: start to end angle (radians, clockwise on screen). static void ArcLine(Painter2D painter, Vector2 center, float radius, float start, float end, Color color, float width) { painter.strokeColor = color; painter.lineWidth = width; painter.lineCap = LineCap.Butt; painter.BeginPath(); painter.Arc(center, radius, Angle.Radians(start), Angle.Radians(end)); if (end - start >= Tau - .001f) painter.ClosePath(); painter.Stroke(); } static List Circle(int points, Vector2 center, float radius, float start = 0f) { var result = new List(points); for (int index = 0; index < points; index++) result.Add(center + Dir(start + Tau * index / points) * radius); return result; } static Vector2[] Star(int points, float outer, float inner, float start = -Mathf.PI * .5f) { var result = new Vector2[points * 2]; for (int index = 0; index < points * 2; index++) result[index] = new Vector2(.5f, .5f) + Dir(start + Mathf.PI * index / points) * (index % 2 == 0 ? outer : inner); return result; } /// Crescent (Blau): the outer disc minus an offset disc, as Geometry2D.clip_polygons gave in Godot. static Vector2[] Crescent() { if (Cache.TryGetValue("crescent", out Vector2[] cached)) return cached; var outerCenter = new Vector2(.5f, .5f); var innerCenter = new Vector2(.66f, .4f); const float outerRadius = .42f; const float innerRadius = .33f; List outer = Circle(48, outerCenter, outerRadius); List inner = Circle(48, innerCenter, innerRadius); // The outer arc outside the inner disc, then the inner arc inside the outer disc, backwards. int startOuter = 0; while (!(Inside(outer[startOuter], innerCenter, innerRadius) && !Inside(outer[(startOuter + 1) % 48], innerCenter, innerRadius))) startOuter++; var result = new List(); for (int step = 1; step <= 48; step++) { Vector2 point = outer[(startOuter + step) % 48]; if (Inside(point, innerCenter, innerRadius)) break; result.Add(point); } int startInner = 0; while (!(Inside(inner[startInner], outerCenter, outerRadius) && !Inside(inner[(startInner + 1) % 48], outerCenter, outerRadius))) startInner++; for (int step = 0; step < 48; step++) { Vector2 point = inner[((startInner - step) % 48 + 48) % 48]; if (!Inside(point, outerCenter, outerRadius)) break; result.Add(point); } cached = result.ToArray(); Cache["crescent"] = cached; return cached; } static bool Inside(Vector2 point, Vector2 center, float radius) { return (point - center).sqrMagnitude < radius * radius; } static Vector2[] Heart() { if (Cache.TryGetValue("heart", out Vector2[] cached)) return cached; cached = new Vector2[40]; for (int index = 0; index < 40; index++) { float t = Tau * index / 40f; float x = 16f * Mathf.Pow(Mathf.Sin(t), 3); float y = 13f * Mathf.Cos(t) - 5f * Mathf.Cos(2f * t) - 2f * Mathf.Cos(3f * t) - Mathf.Cos(4f * t); cached[index] = new Vector2(.5f + x / 38f, .47f - y / 38f); } Cache["heart"] = cached; return cached; } static Vector2[] Drop() { var result = new Vector2[18]; result[0] = new Vector2(.5f, .16f); for (int index = 0; index < 17; index++) { float angle = -Mathf.PI * .15f + Mathf.PI * 1.3f * index / 16f; result[1 + index] = new Vector2(.5f, .64f) + Dir(angle) * .22f; } return result; } } }