Das G3-HUD steht im Spiel; die Abnahme (U3D) folgt. - U3B: alle Bereiche und Komponenten aus Godot übertragen (LuneHud, Lanes, Bossgrotte, nächste Welle, Kaufkarten, Armee, Held mit Items und Talenten, Angriffschampions, Team, Ereignisse, Minimap, Tooltip, Optionen, Partieende). Porträts aus den echten Modellen, einmal gerendert. - Offene G3-Punkte umgesetzt: „1 WELLE“ als Band am Porträtrand, Schadens- und Rüstungssymbole auf den Karten, Armeewert, Heldenporträts auf der Minimap. Befehlsplatz für die Ultimates der geplanten Helden reserviert (verborgen). - U3C: LuneGame verdrahtet Befehle (gleiche Schnittstelle wie die Bots), Bereit mit 1-s-Sperre nach dem Kampf, Tempo und Pause, Tasten wie in G3, Kamera-Ziele, Auswahl im Feld, Planungsvorschau und Aufstellungsflächen. Ersetzt die U2-Debug-Anzeige. Bot-Partien und Benchmark unverändert. - Szenen- und Bibliotheksbauer idempotent: Animator-Controller und Post-Processing-Profil werden an Ort und Stelle aktualisiert. Die Szene bekommt bei jedem Bau neue Objekt-IDs, der Inhalt ist gleich. - Prüfung (Port von verify_g3, Teile A–H): 699 Prüfungen ohne Fehler. EditMode 23/23, PlayMode 12/12, Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
211 lines
9.7 KiB
C#
211 lines
9.7 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>Small rivet diamonds in the middle of the long edges (menu plates).</summary>
|
|
public bool Ornaments;
|
|
public Color OrnamentColor = LuneUiTheme.Hex("c99a58");
|
|
/// <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);
|
|
if (style.Ornaments)
|
|
{
|
|
// Small rivet diamonds on the long edges mark forged panels without adding frames.
|
|
float x = r.x + r.width * .5f;
|
|
foreach (float y in new[] { r.y, r.yMax })
|
|
{
|
|
const float size = 3.5f;
|
|
Polygon(painter, new[] { new Vector2(x, y - size), new Vector2(x + size, y), new Vector2(x, y + size), new Vector2(x - size, y) }, style.OrnamentColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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();
|
|
}
|
|
}
|
|
}
|