champions-of-luneraria/Assets/Luneraria/UI/Theme/LunePlates.cs
Martin Stickelbroeck 4dee9b18d2 U3A HUD-Grundlagen: UI Toolkit, Sichtmodell, menschlicher Sitz
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>
2026-10-01 23:57:45 +02:00

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