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>
255 lines
11 KiB
C#
255 lines
11 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.TextCore.Text;
|
||
using UnityEngine.UIElements;
|
||
using TextElement = UnityEngine.UIElements.TextElement;
|
||
|
||
namespace Luneraria.UI
|
||
{
|
||
/// <summary>Weights of the HUD fonts (Godot FontVariation weights of lune_ui_theme.gd).</summary>
|
||
public enum LuneFace
|
||
{
|
||
/// <summary>Cantarell 560: Regular with a thin outline in the text colour.</summary>
|
||
Text,
|
||
/// <summary>Cantarell 700–760.</summary>
|
||
Bold,
|
||
/// <summary>Cinzel 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) },
|
||
{ "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) },
|
||
{ "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>Outline that thickens Cantarell Regular to the G3 text weight 560.</summary>
|
||
public const float MediumOutline = .25f;
|
||
|
||
public static FontAsset Font(LuneFace face)
|
||
{
|
||
if (Assets == null)
|
||
return null;
|
||
return face == LuneFace.Display ? Assets.Display : face == LuneFace.Bold ? Assets.TextBold : Assets.Text;
|
||
}
|
||
|
||
/// <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);
|
||
element.style.fontSize = size > 0f ? Mathf.Max(LuneUiTheme.MinFont, size) : style.Size;
|
||
element.style.letterSpacing = style.Spacing;
|
||
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; keeps the weight outline (560) or the dark title outline in step.</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;
|
||
if (style.Outline > 0f)
|
||
{
|
||
element.style.unityTextOutlineWidth = style.Outline * .25f;
|
||
element.style.unityTextOutlineColor = new Color(0f, 0f, 0f, .55f);
|
||
}
|
||
else if (style.Face == LuneFace.Text)
|
||
{
|
||
element.style.unityTextOutlineWidth = MediumOutline;
|
||
element.style.unityTextOutlineColor = color;
|
||
}
|
||
else
|
||
{
|
||
element.style.unityTextOutlineWidth = 0f;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
}
|