champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneComponents.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

687 lines
30 KiB
C#
Raw Permalink 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;
using System.Collections.Generic;
using Luneraria.Core;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
/// <summary>Where a HUD click sends the camera: a lane, a map point or a named target (base, enemy_base, grotto, hero).</summary>
public sealed class LuneFocusTarget
{
public string Lane = "";
public Vec2? Point;
public string Camera = "";
public bool IsEmpty { get { return Lane.Length == 0 && Point == null && Camera.Length == 0; } }
public static LuneFocusTarget ForLane(string lane) { return new LuneFocusTarget { Lane = lane ?? "" }; }
public static LuneFocusTarget ForPoint(Vec2 point) { return new LuneFocusTarget { Point = point }; }
public static LuneFocusTarget ForCamera(string target) { return new LuneFocusTarget { Camera = target ?? "" }; }
public override string ToString() { return Lane + "|" + (Point.HasValue ? Point.Value.ToString() : "") + "|" + Camera; }
}
/// <summary>
/// Colours of the armour and damage types (G3 open point, Legion TD principle): a damage type
/// has the colour of the armour it hits for 130 % (DamageMatrix: type i is strong against armour i).
/// </summary>
public static class LuneTypeColors
{
public static readonly Color[] Types = { LuneUiTheme.Hex("e2b46b"), LuneUiTheme.Hex("7fb2e5"), LuneUiTheme.Hex("c39bf0") };
public static Color Armor(int armorType) { return Types[Mathf.Clamp(armorType, 0, 2)]; }
public static Color Damage(int attackType)
{
int row = Mathf.Clamp(attackType, 0, 2);
for (int armor = 0; armor < 3; armor++)
{
if (LuneUnit.DamageMatrix[row, armor] == 1.30)
return Types[armor];
}
return LuneUiTheme.Muted;
}
}
/// <summary>
/// The viewer's own gold with the coin symbol and the base income of the next settlement
/// (Godot LuneResourceDisplay). It is only ever fed the own seat's gold.
/// </summary>
public sealed class LuneResourceDisplay : VisualElement
{
public readonly Label Amount;
public readonly Label Detail;
int _shown = -1;
float _flash;
public LuneResourceDisplay()
{
style.flexDirection = FlexDirection.Row;
style.alignItems = Align.Center;
pickingMode = PickingMode.Position;
LuneUi.Glyph("coin", LuneUiTheme.Gold, 30f, this);
VisualElement column = LuneUi.Box(true, -2f, this);
column.style.marginLeft = 8f;
Amount = LuneUi.Label("0", "HudGold", column);
Detail = LuneUi.Label("", "HudMuted", column);
}
public void SetValues(int gold, int income, bool showIncome = true)
{
if (gold != _shown)
{
if (_shown >= 0 && !LunePreferences.ReducedMotion)
_flash = .45f;
_shown = gold;
Amount.text = LuneUiTheme.Number(gold);
}
string line = showIncome ? "+" + LuneUiTheme.Number(income) + " nächste Runde" : "";
if (Detail.text != line)
Detail.text = line;
LuneUi.Show(Detail, showIncome);
}
public void Tick(float delta)
{
if (_flash <= 0f)
return;
_flash = Mathf.Max(0f, _flash - delta);
// Godot modulate (1.35, 1.2, .8) fading back to white.
float t = _flash / .45f;
Color gold = LuneUiTheme.Gold;
var flash = new Color(Mathf.Min(1f, gold.r * 1.35f), Mathf.Min(1f, gold.g * 1.2f), gold.b * .8f, 1f);
LuneUi.SetColor(Amount, Color.Lerp(gold, flash, t));
}
}
/// <summary>
/// The own team (Godot LuneTeamPanel): per seat the name, lane, hero (committed hero of the
/// others, own draft for the viewer), readiness during planning and whether a human, a bot or –
/// after a disconnect – a bot on a human's seat plays it. Gold or plans of others never appear.
/// </summary>
public sealed class LuneTeamPanel : VisualElement
{
sealed class Row
{
public VisualElement Element;
public LuneGlyphElement Status;
public LunePortraitElement Portrait;
public Label Name;
public Label Lane;
public LuneStatusBadge Badge;
public LuneHudModel.MemberInfo Data;
}
readonly List<Row> _rows = new List<Row>();
readonly Label _header;
readonly LuneGlyphElement _emblem;
string _signature = "";
public LuneTeamPanel()
{
pickingMode = PickingMode.Ignore;
var column = (LuneBox)LuneUi.Box(true, 3f, this);
VisualElement head = LuneUi.Box(false, 6f, column);
_emblem = LuneUi.Glyph("emblem_0", LuneUiTheme.Team[0], 15f, head);
_header = LuneUi.Label("DEIN TEAM", "HudCaps", head);
for (int index = 0; index < 3; index++)
{
VisualElement element = LuneUi.Box(false, 6f, column);
element.pickingMode = PickingMode.Position;
element.style.minHeight = 28f;
var row = new Row { Element = element };
row.Status = LuneUi.Glyph("human", LuneUiTheme.Muted, 15f, element);
row.Portrait = new LunePortraitElement();
row.Portrait.style.width = 26f;
row.Portrait.style.height = 26f;
element.Add(row.Portrait);
row.Name = LuneUi.Clip(LuneUi.Label("", "HudText", element));
row.Name.style.flexGrow = 1f;
row.Lane = LuneUi.Label("", "HudMuted", element);
row.Badge = new LuneStatusBadge();
element.Add(row.Badge);
_rows.Add(row);
int captured = index;
LuneTooltip.Attach(element, () => Tip(captured));
}
}
public void Apply(List<LuneHudModel.MemberInfo> members, int team, bool planning, bool networked)
{
string signature = string.Join(",", members) + "|" + team + "|" + planning + "|" + networked;
if (signature == _signature)
return;
_signature = signature;
_emblem.Glyph = "emblem_" + team;
_emblem.Color = LuneUiTheme.TeamColor(team);
_header.text = "DEIN TEAM · " + LuneUi.Upper(LuneUiTheme.TeamName(team));
for (int index = 0; index < Mathf.Min(3, members.Count); index++)
{
LuneHudModel.MemberInfo member = members[index];
Row row = _rows[index];
row.Data = member;
string status = member.Status;
row.Status.Glyph = status == "self" ? "star" : status == "bot" ? "bot" : status == "offline" ? "offline" : "human";
row.Status.Color = status == "self" ? LuneUiTheme.BronzeLight : status == "offline" ? LuneUiTheme.Danger : LuneUiTheme.Muted;
row.Name.text = member.Name;
LuneUi.SetColor(row.Name, member.IsSelf ? LuneUiTheme.BronzeLight : LuneUiTheme.Text);
row.Lane.text = LuneUi.Upper(LuneMap.LaneNames[member.Lane]);
LuneUi.Show(row.Portrait, member.HeroId.Length > 0);
if (member.HeroId.Length > 0)
row.Portrait.ShowModel(LuneContent.Heroes[member.HeroId].Model);
if (status == "offline")
row.Badge.SetState("BOT ÜBERNIMMT", LuneUiTheme.Danger, "offline");
else if (status == "bot")
row.Badge.SetState("BOT", LuneUiTheme.Muted, "bot");
else if (planning && member.Ready)
row.Badge.SetState("BEREIT", LuneUiTheme.Good, "check");
else if (planning)
row.Badge.SetState("PLANT", LuneUiTheme.Warn, "clock");
else
row.Badge.SetState("IM KAMPF", LuneUiTheme.Info);
}
}
LuneTip Tip(int index)
{
LuneHudModel.MemberInfo member = _rows[index].Data;
if (member == null)
return null;
string kind = member.Status == "self" ? "Du" : member.Status == "human" ? "Mensch" : member.Status == "bot" ? "Bot" : "Verbindung getrennt – ein Bot spielt den Platz weiter";
string hero = "kein Held";
if (member.HeroId.Length > 0)
hero = LuneContent.Heroes[member.HeroId].Name + " · Stufe " + member.HeroLevel;
var tip = new LuneTip
{
Title = member.Name,
Subtitle = LuneMap.LaneNames[member.Lane] + "-Lane · " + ContentData.FactionNames.GetValueOrDefault(member.Bund, ""),
Note = member.IsSelf ? "" : "Gold und laufende Planung anderer Spieler bleiben privat.",
};
tip.Stat("human", "Spieler", kind).Stat("crown", "Held", hero).Stat("check", "Planung", member.Ready ? "bereit" : "plant noch");
return tip;
}
}
/// <summary>
/// Purchase card (Godot LuneUnitCard): real-model portrait, name, role, gold price, hotkey and
/// availability; the armour and damage type as coloured symbols (G3 open point). The champion
/// variant uses the copper card style, a hatched portrait and the "1 WELLE" band at the bottom
/// of the portrait, so temporary attack champions never read like permanent troops.
/// </summary>
public sealed class LuneUnitCard : LuneButton
{
static readonly Dictionary<string, string> RoleGlyphs = new Dictionary<string, string>
{ { "front", "tank" }, { "melee", "melee" }, { "ranged", "ranged" }, { "splash", "splash" }, { "support", "support" }, { "schutz", "tank" }, { "druck", "tempo" }, { "siege", "siege" } };
static readonly Dictionary<string, string> RoleText = new Dictionary<string, string>
{ { "front", "Tank" }, { "melee", "Nahkampf" }, { "ranged", "Fernkampf" }, { "splash", "Fläche" }, { "support", "Support" }, { "schutz", "Schutz" }, { "druck", "Druck" }, { "siege", "Belagerung" } };
public string TypeId = "";
public bool Champion;
public int Cost;
public string Reason = "";
public readonly LunePortraitElement Portrait;
readonly Label _name;
readonly LuneGlyphElement _roleGlyph;
readonly Label _role;
readonly Label _cost;
readonly Label _key;
readonly VisualElement _band;
readonly LuneGlyphElement _lock;
readonly LuneGlyphElement _armor;
readonly LuneGlyphElement _attack;
public LuneUnitCard() : base(null, LuneButtons.HudCard)
{
style.width = 116f;
style.height = 132f;
style.flexDirection = FlexDirection.Column;
style.alignItems = Align.Stretch;
style.justifyContent = Justify.FlexStart;
style.paddingLeft = 6f;
style.paddingRight = 6f;
style.paddingTop = 6f;
style.paddingBottom = 5f;
Portrait = new LunePortraitElement();
Portrait.style.minHeight = 62f;
Portrait.style.flexGrow = 1f;
Add(Portrait);
_key = LuneUi.Label("", "HudCaps", Portrait);
_key.style.position = Position.Absolute;
_key.style.left = 4f;
_key.style.top = 1f;
// Armour and damage type, top right (Legion TD: what it is good against).
VisualElement types = LuneUi.Box(false, 2f, Portrait);
types.style.position = Position.Absolute;
types.style.right = 3f;
types.style.top = 3f;
_attack = LuneUi.Glyph("sword", LuneUiTheme.Muted, 13f, types);
_armor = LuneUi.Glyph("armor", LuneUiTheme.Muted, 13f, types);
_band = LuneUi.Box(false, 2f, Portrait);
_band.style.position = Position.Absolute;
_band.style.left = 0f;
_band.style.right = 0f;
_band.style.bottom = 0f;
_band.style.height = 16f;
_band.style.justifyContent = Justify.Center;
_band.style.backgroundColor = new Color(0f, 0f, 0f, .62f);
LuneUi.Glyph("hourglass", LuneUiTheme.Champion, 12f, _band);
LuneUi.SetColor(LuneUi.Label("1 WELLE", "HudCaps", _band, 14f), LuneUiTheme.Champion);
LuneUi.Show(_band, false);
_lock = LuneUi.Glyph("lock", LuneUiTheme.Muted, 18f, Portrait);
_lock.style.position = Position.Absolute;
_lock.style.left = new Length(50f, LengthUnit.Percent);
_lock.style.top = new Length(50f, LengthUnit.Percent);
_lock.style.translate = new Translate(new Length(-50f, LengthUnit.Percent), new Length(-50f, LengthUnit.Percent));
LuneUi.Show(_lock, false);
_name = LuneUi.Clip(LuneUi.Label("", "HudText", this));
_name.style.unityTextAlign = TextAnchor.MiddleCenter;
_name.style.marginTop = 1f;
VisualElement roleRow = LuneUi.Box(false, 4f, this);
roleRow.style.justifyContent = Justify.Center;
roleRow.style.marginTop = 1f;
_roleGlyph = LuneUi.Glyph("", LuneUiTheme.Muted, 14f, roleRow);
_role = LuneUi.Label("", "HudMuted", roleRow);
VisualElement costRow = LuneUi.Box(false, 4f, this);
costRow.style.justifyContent = Justify.Center;
costRow.style.marginTop = 1f;
LuneUi.Glyph("coin", LuneUiTheme.Gold, 15f, costRow);
_cost = LuneUi.Label("", "HudValue", costRow);
}
public void Setup(string id, string modelId, string title, string roleKey, int price, string hotkey = "", bool champion = false, UnitDef definition = null)
{
TypeId = id;
Champion = champion;
Cost = price;
Look = champion ? LuneButtons.HudChampionCard : LuneButtons.HudCard;
Portrait.ShowModel(modelId);
_name.text = title;
_roleGlyph.Glyph = RoleGlyphs.TryGetValue(roleKey ?? "", out string glyph) ? glyph : "melee";
_role.text = RoleText.TryGetValue(roleKey ?? "", out string text) ? text : roleKey;
_cost.text = price.ToString();
_key.text = hotkey;
LuneUi.Show(_key, hotkey.Length > 0);
LuneUi.Show(_band, champion);
Portrait.Hatch = champion ? LuneUiTheme.WithAlpha(LuneUiTheme.Champion, .13f) : Color.clear;
if (definition != null)
{
_attack.Color = LuneTypeColors.Damage(definition.AttackType);
_armor.Color = LuneTypeColors.Armor(definition.ArmorType);
}
}
/// <summary>ok = purchasable now; otherwise `why` explains it (tooltip) and the card greys out.</summary>
public void SetAvailable(bool ok, string why = "", bool locked = false, bool unaffordable = false)
{
Reason = why ?? "";
Disabled = !ok;
Portrait.Dimmed = !ok;
LuneUi.Show(_lock, locked);
LuneUi.SetColor(_cost, unaffordable ? LuneUiTheme.Danger : LuneUiTheme.Bright);
LuneUi.SetColor(_name, ok ? LuneUiTheme.Text : LuneUiTheme.Muted);
}
}
/// <summary>
/// One own troop in the army strip (Godot LuneArmyChip): real-model portrait, a band with its
/// lane (T, M, B or G) and stage (II for upgraded troops). Selected = pressed style. In combat a
/// fallen troop is dimmed and crossed out.
/// </summary>
public sealed class LuneArmyChip : LuneButton
{
public const float Band = 15f;
public string Uid = "";
public string TypeId = "";
public string Lane = "mid";
public readonly LunePortraitElement Portrait;
readonly Label _lane;
readonly Label _stage;
bool _fallen;
public LuneArmyChip() : base(null, LuneButtons.HudChip)
{
style.flexDirection = FlexDirection.Column;
style.alignItems = Align.Stretch;
style.justifyContent = Justify.FlexStart;
style.paddingLeft = 2f;
style.paddingRight = 2f;
style.paddingTop = 2f;
style.paddingBottom = 1f;
Portrait = new LunePortraitElement();
Portrait.style.flexGrow = 1f;
Add(Portrait);
VisualElement band = LuneUi.Box(false, 0f, this);
band.style.height = Band;
band.style.backgroundColor = new Color(0f, 0f, 0f, .55f);
band.style.justifyContent = Justify.SpaceBetween;
band.style.paddingLeft = 3f;
band.style.paddingRight = 3f;
_lane = LuneUi.Label("", "HudCaps", band, 14f);
_stage = LuneUi.Label("II", "HudCaps", band, 14f);
LuneUi.SetColor(_stage, LuneUiTheme.Gold);
}
public void Setup(RosterEntry entry, float edge)
{
Uid = entry.Uid;
TypeId = entry.TypeId;
Lane = entry.Lane;
style.width = edge;
style.height = edge + Band;
Portrait.ShowModel(TypeId);
bool grotto = Lane == LuneMatch.Grotto;
_lane.text = grotto ? "G" : LuneMap.LaneNames.TryGetValue(Lane, out string name) ? name.Substring(0, 1) : "?";
LuneUi.SetColor(_lane, grotto ? LuneUiTheme.Team[2] : LuneUiTheme.BronzeLight);
LuneUi.Show(_stage, LuneStats.StageOf(TypeId) == 2);
}
public bool Fallen
{
get { return _fallen; }
set
{
if (value == _fallen)
return;
_fallen = value;
Portrait.Dimmed = value;
Portrait.Crossed = value;
}
}
}
/// <summary>
/// A hero item (Godot LuneItemSlot): one of the three slots (compact: symbol only) or a card in
/// the item shop (symbol, name, cost, main effect, state). States say why something cannot be
/// done: equipped, available, unaffordable, incompatible (hero level), unique (already carried),
/// slots full, locked (plan committed) or an empty slot.
/// </summary>
public sealed class LuneItemSlot : LuneButton
{
public enum State { Empty, Equipped, Available, Unaffordable, Incompatible, Unique, Full, Locked }
static readonly Dictionary<State, string> StateText = new Dictionary<State, string>
{
{ State.Equipped, "AUSGERÜSTET" }, { State.Available, "KAUFBAR" }, { State.Unaffordable, "ZU TEUER" }, { State.Incompatible, "STUFE FEHLT" },
{ State.Unique, "GETRAGEN" }, { State.Full, "PLÄTZE VOLL" }, { State.Locked, "GESPERRT" }, { State.Empty, "LEER" },
};
static readonly Dictionary<string, Color> ItemColors = new Dictionary<string, Color>
{
{ "basaltschild", LuneUiTheme.Hex("9fb2c8") }, { "glutklinge", LuneUiTheme.Hex("ff9a5a") }, { "rankenmantel", LuneUiTheme.Hex("8fd08a") },
{ "funkenfokus", LuneUiTheme.Hex("f5d76a") }, { "quellamulett", LuneUiTheme.Hex("7ad6e6") }, { "kristallsplitter", LuneUiTheme.Hex("c7a2ff") },
};
public string ItemId = "";
public State Current = State.Empty;
public readonly bool Compact;
readonly LuneGlyphElement _glyph;
readonly Label _name;
readonly Label _effect;
readonly Label _cost;
readonly LuneStatusBadge _badge;
public static Color ColorOf(string id)
{
return ItemColors.TryGetValue(id ?? "", out Color color) ? color : LuneUiTheme.Muted;
}
public LuneItemSlot(bool compact = true) : base(null, compact ? LuneButtons.HudChip : LuneButtons.HudCard)
{
Compact = compact;
if (compact)
{
style.width = 30f;
style.height = 30f;
// Godot: the glyph fills the slot with 5 px insets at any slot size (46 in the hero window).
_glyph = LuneUi.Glyph("", Color.white, 20f, this);
_glyph.style.position = Position.Absolute;
_glyph.style.left = 5f;
_glyph.style.top = 5f;
_glyph.style.right = 5f;
_glyph.style.bottom = 5f;
_glyph.style.width = StyleKeyword.Auto;
_glyph.style.height = StyleKeyword.Auto;
return;
}
style.minHeight = 74f;
style.justifyContent = Justify.FlexStart;
style.paddingLeft = 10f;
style.paddingRight = 8f;
style.paddingTop = 6f;
style.paddingBottom = 6f;
_glyph = LuneUi.Glyph("", Color.white, 34f, this);
VisualElement column = LuneUi.Box(true, 0f, this);
column.style.marginLeft = 10f;
column.style.flexGrow = 1f;
column.style.flexShrink = 1f;
column.style.minWidth = 0f;
column.style.justifyContent = Justify.Center;
VisualElement head = LuneUi.Box(false, 6f, column);
_name = LuneUi.Clip(LuneUi.Label("", "HudValue", head));
_name.style.flexGrow = 1f;
LuneUi.Glyph("coin", LuneUiTheme.Gold, 14f, head);
_cost = LuneUi.Label("", "HudValue", head);
_effect = LuneUi.Clip(LuneUi.Label("", "HudMuted", column));
_badge = new LuneStatusBadge();
_badge.style.alignSelf = Align.FlexStart;
column.Add(_badge);
}
public void SetItem(string id, State state)
{
ItemId = id ?? "";
Current = state;
Color tint = ColorOf(ItemId);
bool usable = state == State.Available || state == State.Equipped;
_glyph.Glyph = ItemId;
_glyph.Color = usable || Compact ? tint : LuneUiTheme.WithAlpha(tint, .45f);
if (Compact)
{
Disabled = ItemId.Length == 0;
return;
}
LuneContent.Items.TryGetValue(ItemId, out ItemSpec item);
_name.text = item != null ? item.Name : "";
_cost.text = item != null ? item.Cost.ToString() : "";
LuneUi.SetColor(_cost, state == State.Unaffordable ? LuneUiTheme.Danger : LuneUiTheme.Bright);
_effect.text = item != null ? item.Text : "";
Color badge = state == State.Equipped ? LuneUiTheme.Good : state == State.Available ? LuneUiTheme.BronzeLight : LuneUiTheme.Muted;
if (state == State.Unaffordable)
badge = LuneUiTheme.Danger;
string glyph = state == State.Equipped || state == State.Unique ? "check" : state == State.Incompatible || state == State.Locked ? "lock" : "";
_badge.SetState(StateText[state], badge, glyph, state == State.Equipped);
Disabled = !usable;
}
}
/// <summary>
/// Short event list (Godot LuneEventFeed): tower falls, champions, boss, critical Ancients,
/// round results. At most five entries, newest on top; each fades after a while. A click on an
/// entry with a place moves the camera there. It never lists every kill.
/// </summary>
public sealed class LuneEventFeed : VisualElement
{
public const int Limit = 5;
public const float Lifetime = 14f;
public event Action<LuneFocusTarget> FocusRequested;
sealed class Entry
{
public string Text;
public string Level;
public float Born;
public LuneButton Node;
}
readonly List<Entry> _entries = new List<Entry>();
float _clock;
public LuneEventFeed()
{
pickingMode = PickingMode.Ignore;
LuneUi.Show(this, false);
}
public IReadOnlyList<string> Texts
{
get { return _entries.ConvertAll(e => e.Text); }
}
static Color LevelColor(string level)
{
return level == "good" ? LuneUiTheme.Good : level == "warn" ? LuneUiTheme.Warn : level == "danger" ? LuneUiTheme.Danger : LuneUiTheme.Info;
}
/// <summary>level: info, good, warn, danger. target: null or a lane, point or camera target.</summary>
public void Push(string text, string level = "info", string glyph = "", LuneFocusTarget target = null)
{
foreach (Entry entry in _entries)
{
if (entry.Text == text && _clock - entry.Born < 4f)
return;
}
var button = new LuneButton(null, LuneButtons.HudRow);
button.style.minHeight = 30f;
button.style.justifyContent = Justify.FlexStart;
button.style.paddingLeft = 8f;
button.style.paddingRight = 8f;
button.style.marginBottom = 3f;
Color tint = LevelColor(level);
LuneUi.Glyph(glyph.Length > 0 ? glyph : "star", tint, 15f, button).style.marginRight = 7f;
Label label = LuneUi.Clip(LuneUi.Label(text, "HudText", button));
label.style.flexGrow = 1f;
LuneUi.SetColor(label, Color.Lerp(tint, LuneUiTheme.Text, .45f));
bool hasTarget = target != null && !target.IsEmpty;
button.Clicked += () =>
{
if (hasTarget)
FocusRequested?.Invoke(target);
};
LuneTooltip.Attach(button, () => new LuneTip { Title = text, Note = hasTarget ? "Klick: Kamera dorthin" : "" });
Insert(0, button);
_entries.Insert(0, new Entry { Text = text, Level = level, Born = _clock, Node = button });
while (_entries.Count > Limit)
{
Entry old = _entries[_entries.Count - 1];
_entries.RemoveAt(_entries.Count - 1);
old.Node.RemoveFromHierarchy();
}
LuneUi.Show(this, true);
}
public void ClearAll()
{
foreach (Entry entry in _entries)
entry.Node.RemoveFromHierarchy();
_entries.Clear();
LuneUi.Show(this, false);
}
public void Tick(float delta)
{
_clock += delta;
for (int index = _entries.Count - 1; index >= 0; index--)
{
Entry entry = _entries[index];
float age = _clock - entry.Born;
if (age > Lifetime)
{
entry.Node.RemoveFromHierarchy();
_entries.RemoveAt(index);
}
else if (age > Lifetime - 2f)
{
entry.Node.style.opacity = Mathf.Clamp01((Lifetime - age) / 2f);
}
}
LuneUi.Show(this, _entries.Count > 0);
}
}
/// <summary>
/// A window over the field for detail work (hero, recruiting; Godot LuneModalPanel). A light
/// backdrop keeps clicks away from the world and closes the window; Esc and the × close it too.
/// The window never covers the whole screen and only opens on request.
/// </summary>
public sealed class LuneModalPanel : VisualElement
{
public event Action Closed;
public readonly LunePanel Panel;
public readonly VisualElement Body;
public readonly Label Title;
readonly Vector2 _box;
public LuneModalPanel(Vector2 size)
{
_box = size;
style.position = Position.Absolute;
style.left = 0f;
style.top = 0f;
style.right = 0f;
style.bottom = 0f;
pickingMode = PickingMode.Position;
var backdrop = new VisualElement();
backdrop.style.position = Position.Absolute;
backdrop.style.left = 0f;
backdrop.style.top = 0f;
backdrop.style.right = 0f;
backdrop.style.bottom = 0f;
backdrop.style.backgroundColor = new Color(.01f, .015f, .025f, .38f);
backdrop.RegisterCallback<PointerDownEvent>(evt =>
{
if (evt.button == 0)
Close();
});
Add(backdrop);
Panel = new LunePanel(LunePanels.Modal);
Panel.style.position = Position.Absolute;
Add(Panel);
VisualElement column = LuneUi.Box(true, 12f, Panel);
column.style.flexGrow = 1f;
VisualElement head = LuneUi.Box(false, 0f, column);
Title = LuneUi.Label("", "HudTitle", head);
Title.style.flexGrow = 1f;
var close = new LuneButton(null, LuneButtons.HudIconButton, head);
close.style.width = 36f;
close.style.height = 34f;
LuneUi.Glyph("close", LuneUiTheme.Muted, 14f, close);
close.Clicked += Close;
LuneTooltip.Attach(close, () => new LuneTip { Title = "Schließen", Text = "Esc oder Klick neben das Fenster." });
Body = LuneUi.Box(true, 10f, column);
Body.style.flexGrow = 1f;
RegisterCallback<GeometryChangedEvent>(_ => Layout());
RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
LuneUi.Show(this, false);
}
public bool Open { get { return style.display != DisplayStyle.None; } }
public void OpenWith(string heading)
{
Title.text = heading;
LuneUi.Show(this, true);
BringToFront();
Layout();
}
public void Close()
{
if (!Open)
return;
LuneUi.Show(this, false);
Closed?.Invoke();
}
void Layout()
{
float width = layout.width;
float height = layout.height;
if (float.IsNaN(width) || width <= 0f)
return;
var edge = new Vector2(Mathf.Min(_box.x, width - 40f), Mathf.Min(_box.y, height - 40f));
Panel.style.width = edge.x;
Panel.style.height = edge.y;
Panel.style.left = Mathf.Floor((width - edge.x) * .5f);
Panel.style.top = Mathf.Floor((height - edge.y) * .5f);
}
}
}