champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneUi.cs
Martin Stickelbroeck 66867f3332 U3D Abnahme: Prüfung, Feld um das HUD, Bilder, Leistung
Das G3-HUD ist abgenommen; der Meilenstein U3 ist damit fertig.

- Prüfung (Port von verify_g3, Teile A–H plus I): 718 Prüfungen ohne
  Fehler. Teil I prüft die Feld-Darstellung um das HUD (U3-Zugabe aus
  dem Motivvergleich): Aufstellungsflächen, Grotteneingang, ruhender
  Boss, Namensschilder, Eigentümer-Ring, Ancient-Balken.
- Feld um das HUD: Namensschilder für Helden (mit Stufe), Angriffs-
  champions und den eingesetzten Boss („für Blau“), Eigentümer-Ring,
  Ancient-Balken bei Schaden, ruhende Schlundkrone auf dem Podest,
  eigener Grotteneingang, Aufstellungsflächen als eigene Objekte.
- Schriften korrigiert: Die Referenz deklariert Cantarell 560/700–760
  und Cinzel 700, wendet die Gewichte aber nie an (variation_opentype
  mit dem String-Schlüssel „wght“ wird von Godot ignoriert). G3 zeigt
  Regular 400, der Port ebenso — Cinzel-Regular statt Bold, keine
  Kontur zur Aufdickung. Laufweite aus spacing_glyph umgerechnet;
  dunkler Saum als Schatten statt TextCore-Kontur.
- Werkzeuge: HudViewsTests (14 G3-Motive), HudPlaytest (Spieltest über
  die echte Oberfläche per Befehlsdateien), ui_ms_per_frame im
  LuneBenchmark. Layout folgt den echten Panelhöhen, Kamera startet wie
  Godot _ready auf der eigenen Lane.
- Tests: EditMode 23/23, PlayMode 13/13, Kerntests 402/402, Verify
  718/0. Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A,
  7B8E43AEFC0D60C5).
- Bilder: 14 Motive bei 1920×1080, Auflösungen 1152×720 bis 2560×1440,
  Prüf-Schlüsselbilder, Spieltest-Screenshots.
- Leistung (macOS-Player mit HUD, 116 Einheiten): 169 fps Übersicht,
  218 fps Mid nah; HUD-Skripte 0,090 bzw. 0,078 ms je Bild.
- Dokumentation: U3-UI (U3D, Schriftbefund), PORT-QA (U3), PORT-STATUS
  (U3 fertig, U4 nächster Schritt), KNOWN-BEHAVIOR-DIFFERENCES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:55:06 +02:00

264 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements;
using TextElement = UnityEngine.UIElements.TextElement;
namespace Luneraria.UI
{
/// <summary>
/// Font roles of the HUD as lune_ui_theme.gd declares them: Cantarell 560, Cantarell 700–760
/// and Cinzel 700. Godot never applied those weights: its FontVariation used the String key
/// "wght", which Godot ignores (measured in 4.7.2: the same advances at 400, 560 and 700; only
/// the integer tag changes them). G3 therefore shows both fonts in their default instance,
/// Regular 400, and so does the port. The roles stay apart so that U5 can give them weights.
/// </summary>
public enum LuneFace
{
/// <summary>Cantarell (declared 560).</summary>
Text,
/// <summary>Cantarell (declared 700–760).</summary>
Bold,
/// <summary>Cinzel (declared 700).</summary>
Display,
}
/// <summary>A Godot label variation: face, size, colour, glyph spacing, optional outline.</summary>
public sealed class LuneTextStyle
{
public LuneFace Face;
public float Size;
public Color Color;
public float Spacing;
/// <summary>Godot outline_size (black, alpha .55) for titles, the wave and gold; 0 = none.</summary>
public float Outline;
public LuneTextStyle(LuneFace face, float size, Color color, float spacing = 0f, float outline = 0f)
{
Face = face;
Size = Mathf.Max(LuneUiTheme.MinFont, size);
Color = color;
Spacing = spacing;
Outline = outline;
}
}
/// <summary>
/// Factory and text styles of the HUD (Godot LuneUiTheme.label/button and the label variations
/// of lune_ui_theme.gd). Components build their elements here, so sizes, faces and colours
/// stay consistent. Fonts come from Settings/LuneUi.asset (LuneUi.Assets, set by the HUD).
/// </summary>
public static class LuneUi
{
public static LuneUiAssets Assets;
static readonly Color Title = LuneUiTheme.Hex("f4e6c4");
public static readonly Dictionary<string, LuneTextStyle> Styles = new Dictionary<string, LuneTextStyle>
{
{ "HudText", new LuneTextStyle(LuneFace.Text, 15, LuneUiTheme.Text) },
{ "HudMuted", new LuneTextStyle(LuneFace.Text, 14, LuneUiTheme.Muted) },
{ "HudCaps", new LuneTextStyle(LuneFace.Bold, 14, LuneUiTheme.BronzeLight, 2f) },
{ "HudBadge", new LuneTextStyle(LuneFace.Bold, 14, LuneUiTheme.Text, 1f) },
{ "HudLane", new LuneTextStyle(LuneFace.Display, 16, Title, 1f) },
{ "HudValue", new LuneTextStyle(LuneFace.Bold, 17, LuneUiTheme.Bright) },
{ "HudPrimary", new LuneTextStyle(LuneFace.Bold, 16, LuneUiTheme.Ink, 1f) },
{ "HudGold", new LuneTextStyle(LuneFace.Bold, 24, LuneUiTheme.Gold, 0f, 4f) },
{ "HudName", new LuneTextStyle(LuneFace.Display, 18, Title, 1f) },
{ "HudTitle", new LuneTextStyle(LuneFace.Display, 24, Title, 1f, 4f) },
{ "HudWave", new LuneTextStyle(LuneFace.Display, 30, Title, 3f, 4f) },
// Menu theme (game-over panel, options).
{ "TitleLabel", new LuneTextStyle(LuneFace.Display, 78, Title, 2f, 6f) },
{ "PrimaryButton", new LuneTextStyle(LuneFace.Display, 22, LuneUiTheme.Ink, 2f) },
{ "HeadingLabel", new LuneTextStyle(LuneFace.Display, 34, Title, 1f) },
{ "SectionLabel", new LuneTextStyle(LuneFace.Display, 16, LuneUiTheme.BronzeLight, 3f) },
{ "BodyLabel", new LuneTextStyle(LuneFace.Text, 20, LuneUiTheme.Parchment) },
{ "MutedLabel", new LuneTextStyle(LuneFace.Text, 16, LuneUiTheme.Hex("a9a293")) },
{ "EyebrowLabel", new LuneTextStyle(LuneFace.Bold, 14, LuneUiTheme.BronzeLight, 2f) },
{ "CardFactLabel", new LuneTextStyle(LuneFace.Text, 17, LuneUiTheme.Hex("d8cfbc")) },
};
/// <summary>The atlas of a role: Cinzel Regular for Display, Cantarell Regular otherwise (see LuneFace).</summary>
public static FontAsset Font(LuneFace face)
{
if (Assets == null)
return null;
return face == LuneFace.Display ? Assets.Display : Assets.Text;
}
/// <summary>
/// Godot's spacing_glyph (pixels per glyph) as UI Toolkit letter spacing, which counts in
/// hundredths of the font size (measured: 10 widens each glyph by 0.1 em).
/// </summary>
public static float LetterSpacing(float pixels, float fontSize)
{
return fontSize > 0f ? pixels * 100f / fontSize : 0f;
}
/// <summary>Applies a text style; size 0 keeps the style's size.</summary>
public static void Apply(TextElement element, string variation, float size = 0f)
{
if (!Styles.TryGetValue(variation, out LuneTextStyle style))
style = Styles["HudText"];
FontAsset font = Font(style.Face);
if (font != null)
element.style.unityFontDefinition = FontDefinition.FromSDFFont(font);
float fontSize = size > 0f ? Mathf.Max(LuneUiTheme.MinFont, size) : style.Size;
element.style.fontSize = fontSize;
element.style.letterSpacing = LetterSpacing(style.Spacing, fontSize);
element.style.textShadow = new TextShadow { offset = new Vector2(0f, 1f), blurRadius = 1f, color = new Color(0f, 0f, 0f, .55f) };
SetColor(element, style.Color, style);
}
/// <summary>
/// Text colour. Titles, the wave and gold get Godot's dark rim (outline_size, black .55) as
/// a soft shadow: a TextCore outline straddles the glyph edge and eats the thin strokes of
/// the regular faces, Godot's outline only grows outwards.
/// </summary>
public static void SetColor(TextElement element, Color color, LuneTextStyle style = null)
{
element.style.color = color;
if (style == null)
style = element.userData as LuneTextStyle;
if (style == null)
return;
element.userData = style;
element.style.unityTextOutlineWidth = 0f;
if (style.Outline > 0f)
element.style.textShadow = new TextShadow { offset = new Vector2(0f, 1f), blurRadius = 1f + style.Outline * .5f, color = new Color(0f, 0f, 0f, .7f) };
}
public static Label Label(string text, string variation = "HudText", VisualElement parent = null, float size = 0f)
{
var label = new Label(text ?? "");
label.pickingMode = PickingMode.Ignore;
Zero(label);
label.style.whiteSpace = WhiteSpace.NoWrap;
Apply(label, variation, size);
parent?.Add(label);
return label;
}
/// <summary>Godot clip_text with OVERRUN_TRIM_ELLIPSIS: one line, cut with "…".</summary>
public static Label Clip(Label label)
{
label.style.overflow = Overflow.Hidden;
label.style.textOverflow = TextOverflow.Ellipsis;
label.style.whiteSpace = WhiteSpace.NoWrap;
label.style.flexShrink = 1f;
label.style.minWidth = 0f;
return label;
}
/// <summary>Godot AUTOWRAP_WORD_SMART.</summary>
public static Label Wrap(Label label)
{
label.style.whiteSpace = WhiteSpace.Normal;
label.style.flexShrink = 1f;
label.style.minWidth = 0f;
return label;
}
public static void Zero(VisualElement element)
{
element.style.marginLeft = 0f;
element.style.marginRight = 0f;
element.style.marginTop = 0f;
element.style.marginBottom = 0f;
element.style.paddingLeft = 0f;
element.style.paddingRight = 0f;
element.style.paddingTop = 0f;
element.style.paddingBottom = 0f;
}
/// <summary>Godot HBox/VBoxContainer with a separation; ignores the mouse like Godot's boxes in the HUD.</summary>
public static VisualElement Box(bool vertical, float separation, VisualElement parent = null)
{
var box = new LuneBox(vertical, separation);
parent?.Add(box);
return box;
}
public static VisualElement Spacer(VisualElement parent)
{
var spacer = new VisualElement { pickingMode = PickingMode.Ignore };
spacer.style.flexGrow = 1f;
parent.Add(spacer);
return spacer;
}
public static LuneGlyphElement Glyph(string glyph, Color color, float edge = 18f, VisualElement parent = null)
{
var element = new LuneGlyphElement(glyph, color, edge);
parent?.Add(element);
return element;
}
/// <summary>Godot `visible`: hidden elements take no space; the parent box closes the gap.</summary>
public static void Show(VisualElement element, bool visible)
{
DisplayStyle display = visible ? DisplayStyle.Flex : DisplayStyle.None;
if (element.style.display == display)
return;
element.style.display = display;
(element.parent as LuneBox)?.Space();
}
public static bool Visible(VisualElement element)
{
return element.resolvedStyle.display != DisplayStyle.None && element.style.display != DisplayStyle.None;
}
public static string Upper(string text)
{
return (text ?? "").ToUpperInvariant();
}
}
/// <summary>
/// Box container with Godot's separation: the gap goes between children, not around them.
/// Children added later get the gap too; hidden children (display none) leave no gap.
/// </summary>
public sealed class LuneBox : VisualElement
{
public readonly bool Vertical;
public float Separation;
public LuneBox(bool vertical, float separation)
{
Vertical = vertical;
Separation = separation;
pickingMode = PickingMode.Ignore;
style.flexDirection = vertical ? FlexDirection.Column : FlexDirection.Row;
if (!vertical)
style.alignItems = Align.Center;
RegisterCallback<GeometryChangedEvent>(_ => Space());
RegisterCallback<AttachToPanelEvent>(_ => Space());
}
/// <summary>Applies the gap before every visible child except the first.</summary>
public void Space()
{
bool first = true;
foreach (VisualElement child in Children())
{
float gap = first ? 0f : Separation;
if (child.style.display == DisplayStyle.None)
continue;
// Only real changes: setting a margin dirties the layout even with the same value.
if (Vertical)
{
StyleLength current = child.style.marginTop;
if (current.keyword != StyleKeyword.Undefined || Mathf.Abs(current.value.value - gap) > .01f)
child.style.marginTop = gap;
}
else
{
StyleLength current = child.style.marginLeft;
if (current.keyword != StyleKeyword.Undefined || Mathf.Abs(current.value.value - gap) > .01f)
child.style.marginLeft = gap;
}
first = false;
}
}
}
}