Beginn des Ports des G3-HUD (Referenz luneraria/g3, 1b540f2). - Entscheidung UI Toolkit statt uGUI nach kurzer Bewertung und einer Probe: Panel rendert im Batchmodus in eine 1920x1080-Textur (Skalierung wie Godot canvas_items/expand, 1600x960), gefaste Verlaufsplatten, alle 50 Symbole, Schriften aus den variablen Fonts der Referenz. - Sichtmodell portiert (LuneHudModel, LuneLaneThreat, LuneStats) mit den Privatsphäre-Regeln der Referenz: Gold, Entwurf, Rückgängig und neue Buchungen nur für den eigenen Sitz; Lanes, Gebäude, Boss und Wellen aus öffentlichem Zustand. - Menschlicher Sitz in LuneMatchRunner/LuneMatchDriver: Bots planen die übrigen fünf Sitze, die Planung wartet auf Bereit; Tempo 1x/2x/4x und Pause nur im Kampf. Bot-Partien unverändert. - Theme-Grundlagen: Farben und Zahlenformate, Platten (forged_style.gd), Vektorsymbole (lune_glyphs.gd) mit Painter2D, statische SDF-Atlanten aus den benannten Instanzen von Cantarell und Cinzel (OFL). - Tests: EditMode 23/23 (12 neue: gleiches öffentliches Bild für alle Sitze, fremde Entwürfe privat, private Boss-Lane-Wahl, Lane-Bedrohung, Bosszustände, nächste Welle, Bereit-Sperre, Formate, menschlicher Sitz); PlayMode 3/3, Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). - Dokumentation: U3-UI (Bewertung, U3A), U5-VISUAL (Vorgaben des Projektinhabers: stilisierter Realismus für die ganze Welt, erst in U5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
9 KiB
C#
198 lines
9 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace Luneraria.UI
|
|
{
|
|
/// <summary>
|
|
/// A vector-drawn plate (port of scripts/ui/theme/forged_style.gd): chamfered corners, a
|
|
/// vertical fill gradient, an edge line, an inset line, an optional top highlight and a soft
|
|
/// shadow. Drawn from the element's size, so it stays crisp at every UI scale.
|
|
/// </summary>
|
|
public sealed class LunePlateStyle
|
|
{
|
|
public Color FillTop = LuneUiTheme.Hex("1b2027");
|
|
public Color FillBottom = LuneUiTheme.Hex("12161b");
|
|
public Color Edge = LuneUiTheme.Hex("8a6a3c");
|
|
public float EdgeWidth = 1.5f;
|
|
public Color Inner = new Color(1f, 1f, 1f, .05f);
|
|
public float InnerInset = 4f;
|
|
public float Chamfer = 7f;
|
|
public Color Highlight = Color.clear;
|
|
public Color Shadow = Color.clear;
|
|
public float ShadowSize;
|
|
/// <summary>Grows the drawing outside the element (focus rings).</summary>
|
|
public float Grow;
|
|
public bool DrawFill = true;
|
|
/// <summary>Content margins (Godot content_margin_*), applied as padding.</summary>
|
|
public float PadX = 12f;
|
|
public float PadY = 12f;
|
|
|
|
/// <summary>SaumwachtTheme.plate (inner .045) plus LuneUiTheme.plate (inner .035, inset 3, margins).</summary>
|
|
public static LunePlateStyle Hud(Color top, Color bottom, Color edge, float edgeWidth = 1f, float cut = 6f, float padX = 12f, float padY = 10f)
|
|
{
|
|
return new LunePlateStyle { FillTop = top, FillBottom = bottom, Edge = edge, EdgeWidth = edgeWidth, Chamfer = cut, Inner = new Color(1f, 1f, 1f, .035f), InnerInset = 3f, PadX = padX, PadY = padY };
|
|
}
|
|
|
|
/// <summary>SaumwachtTheme.plate with margins: menu plates (inner .045, inset 4).</summary>
|
|
public static LunePlateStyle Menu(Color top, Color bottom, Color edge, float edgeWidth = 1.5f, float cut = 7f, float padX = 12f, float padY = 12f)
|
|
{
|
|
return new LunePlateStyle { FillTop = top, FillBottom = bottom, Edge = edge, EdgeWidth = edgeWidth, Chamfer = cut, Inner = new Color(1f, 1f, 1f, .045f), InnerInset = 4f, PadX = padX, PadY = padY };
|
|
}
|
|
|
|
/// <summary>Keyboard focus ring outside the element (SaumwachtTheme.focus_ring).</summary>
|
|
public static LunePlateStyle FocusRing(float cut = 7f)
|
|
{
|
|
return new LunePlateStyle { DrawFill = false, Edge = LuneUiTheme.Focus, EdgeWidth = 2.5f, Inner = LuneUiTheme.WithAlpha(LuneUiTheme.Focus, .55f), InnerInset = 4.5f,
|
|
Chamfer = cut + 4f, Grow = 4f, Shadow = LuneUiTheme.WithAlpha(LuneUiTheme.Focus, .22f), ShadowSize = 5f, PadX = 0f, PadY = 0f };
|
|
}
|
|
|
|
public void ApplyPadding(VisualElement element)
|
|
{
|
|
element.style.paddingLeft = PadX;
|
|
element.style.paddingRight = PadX;
|
|
element.style.paddingTop = PadY;
|
|
element.style.paddingBottom = PadY;
|
|
}
|
|
|
|
public LunePlateStyle WithShadow(Color shadow, float size)
|
|
{
|
|
var copy = (LunePlateStyle)MemberwiseClone();
|
|
copy.Shadow = shadow;
|
|
copy.ShadowSize = size;
|
|
return copy;
|
|
}
|
|
|
|
public LunePlateStyle WithInner(Color inner)
|
|
{
|
|
var copy = (LunePlateStyle)MemberwiseClone();
|
|
copy.Inner = inner;
|
|
return copy;
|
|
}
|
|
}
|
|
|
|
public static class LunePlates
|
|
{
|
|
static readonly Vector2[] Points = new Vector2[8];
|
|
|
|
/// <summary>The eight corners of a chamfered rectangle, clockwise from the top left cut.</summary>
|
|
public static Vector2[] PlatePoints(Rect rect, float cut, Vector2[] into = null)
|
|
{
|
|
Vector2[] result = into ?? new Vector2[8];
|
|
float c = Mathf.Min(cut, Mathf.Min(rect.width, rect.height) * .5f);
|
|
Vector2 p = rect.min;
|
|
Vector2 e = rect.max;
|
|
result[0] = new Vector2(p.x + c, p.y);
|
|
result[1] = new Vector2(e.x - c, p.y);
|
|
result[2] = new Vector2(e.x, p.y + c);
|
|
result[3] = new Vector2(e.x, e.y - c);
|
|
result[4] = new Vector2(e.x - c, e.y);
|
|
result[5] = new Vector2(p.x + c, e.y);
|
|
result[6] = new Vector2(p.x, e.y - c);
|
|
result[7] = new Vector2(p.x, p.y + c);
|
|
return result;
|
|
}
|
|
|
|
static Rect Grow(Rect rect, float amount)
|
|
{
|
|
return new Rect(rect.x - amount, rect.y - amount, rect.width + amount * 2f, rect.height + amount * 2f);
|
|
}
|
|
|
|
public static void Draw(MeshGenerationContext context, Rect rect, LunePlateStyle style)
|
|
{
|
|
Rect r = Grow(rect, style.Grow);
|
|
if (r.width < 2f || r.height < 2f)
|
|
return;
|
|
Painter2D painter = context.painter2D;
|
|
if (style.Shadow.a > 0f && style.ShadowSize > 0f)
|
|
{
|
|
for (int layer = 0; layer < 3; layer++)
|
|
{
|
|
float spread = style.ShadowSize * (layer + 1) / 3f;
|
|
var soft = new Color(style.Shadow.r, style.Shadow.g, style.Shadow.b, style.Shadow.a / 3f);
|
|
if (style.DrawFill)
|
|
{
|
|
Rect offset = new Rect(r.x, r.y + spread * .5f, r.width, r.height);
|
|
Polygon(painter, PlatePoints(Grow(offset, spread), style.Chamfer + spread, Points), soft);
|
|
}
|
|
else
|
|
{
|
|
// Rings only glow outwards; the element underneath stays untinted.
|
|
Outline(painter, PlatePoints(Grow(r, spread), style.Chamfer + spread, Points), soft, style.ShadowSize / 3f + 1f);
|
|
}
|
|
}
|
|
}
|
|
if (style.DrawFill)
|
|
Gradient(context, PlatePoints(r, style.Chamfer, Points), r, style.FillTop, style.FillBottom);
|
|
if (style.Highlight.a > 0f)
|
|
{
|
|
float y = r.y + style.EdgeWidth + 1f;
|
|
Line(painter, new Vector2(r.x + style.Chamfer, y), new Vector2(r.xMax - style.Chamfer, y), style.Highlight, 1f);
|
|
}
|
|
if (style.Inner.a > 0f && style.InnerInset > 0f)
|
|
Outline(painter, PlatePoints(Grow(r, -style.InnerInset), Mathf.Max(1f, style.Chamfer - style.InnerInset * .45f), Points), style.Inner, 1f);
|
|
if (style.Edge.a > 0f && style.EdgeWidth > 0f)
|
|
Outline(painter, PlatePoints(Grow(r, -style.EdgeWidth * .5f), style.Chamfer, Points), style.Edge, style.EdgeWidth);
|
|
}
|
|
|
|
/// <summary>Convex polygon with a vertical colour gradient (Godot: polygon with per-vertex colours).</summary>
|
|
public static void Gradient(MeshGenerationContext context, Vector2[] points, Rect bounds, Color top, Color bottom)
|
|
{
|
|
int count = points.Length;
|
|
MeshWriteData mesh = context.Allocate(count + 1, count * 3);
|
|
Vector2 center = Vector2.zero;
|
|
foreach (Vector2 point in points)
|
|
center += point;
|
|
center /= count;
|
|
mesh.SetNextVertex(new Vertex { position = new Vector3(center.x, center.y, Vertex.nearZ), tint = Color.Lerp(top, bottom, Fraction(center.y, bounds)) });
|
|
foreach (Vector2 point in points)
|
|
mesh.SetNextVertex(new Vertex { position = new Vector3(point.x, point.y, Vertex.nearZ), tint = Color.Lerp(top, bottom, Fraction(point.y, bounds)) });
|
|
// Clockwise triangles in screen space (y down).
|
|
for (int index = 0; index < count; index++)
|
|
{
|
|
mesh.SetNextIndex(0);
|
|
mesh.SetNextIndex((ushort)(1 + index));
|
|
mesh.SetNextIndex((ushort)(1 + (index + 1) % count));
|
|
}
|
|
}
|
|
|
|
static float Fraction(float y, Rect bounds)
|
|
{
|
|
return Mathf.Clamp01((y - bounds.y) / Mathf.Max(1f, bounds.height));
|
|
}
|
|
|
|
public static void Polygon(Painter2D painter, Vector2[] points, Color color)
|
|
{
|
|
painter.fillColor = color;
|
|
painter.BeginPath();
|
|
painter.MoveTo(points[0]);
|
|
for (int index = 1; index < points.Length; index++)
|
|
painter.LineTo(points[index]);
|
|
painter.ClosePath();
|
|
painter.Fill();
|
|
}
|
|
|
|
public static void Outline(Painter2D painter, Vector2[] points, Color color, float width)
|
|
{
|
|
painter.strokeColor = color;
|
|
painter.lineWidth = width;
|
|
painter.lineJoin = LineJoin.Miter;
|
|
painter.BeginPath();
|
|
painter.MoveTo(points[0]);
|
|
for (int index = 1; index < points.Length; index++)
|
|
painter.LineTo(points[index]);
|
|
painter.ClosePath();
|
|
painter.Stroke();
|
|
}
|
|
|
|
public static void Line(Painter2D painter, Vector2 from, Vector2 to, Color color, float width)
|
|
{
|
|
painter.strokeColor = color;
|
|
painter.lineWidth = width;
|
|
painter.lineCap = LineCap.Butt;
|
|
painter.BeginPath();
|
|
painter.MoveTo(from);
|
|
painter.LineTo(to);
|
|
painter.Stroke();
|
|
}
|
|
}
|
|
}
|