champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneTooltip.cs
Martin Stickelbroeck 2b86c0d3b5 U3B/U3C Zwischenstand: HUD-Bereiche, Eingabe und Befehle
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>
2026-10-02 01:45:02 +02:00

209 lines
7.8 KiB
C#

using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
/// <summary>
/// The one tooltip of the HUD (port of components/lune_tooltip.gd). A single panel is reused
/// for every hover: it appears after a short delay next to the hovered element, stays inside
/// the window, never takes mouse input and hides as soon as the element is left, hidden,
/// detached or clicked. Content is a LuneTip (title, subtitle, stat rows, text, note, warn).
/// Elements register with LuneTooltip.Attach(element, builder); the HUD calls Tick each frame.
/// </summary>
public sealed class LuneTooltip : LunePanel
{
public const float Delay = .28f;
public const float Quick = .05f;
public const float Width = 300f;
public static LuneTooltip Current;
VisualElement _owner;
Func<LuneTip> _builder;
float _wait;
float _lastHide = -10f;
float _clock;
bool _placed;
Vector2 _placedSize;
readonly VisualElement _column;
/// <summary>Last pointer position in panel coordinates (real and synthetic events).</summary>
public Vector2 Pointer = new Vector2(-1f, -1f);
/// <summary>Text of the visible tooltip (tests).</summary>
public string Shown { get; private set; } = "";
public LuneTooltip() : base(LunePanels.Tooltip, false)
{
style.position = Position.Absolute;
style.maxWidth = Width;
pickingMode = PickingMode.Ignore;
_column = LuneUi.Box(true, 4f, this);
LuneUi.Show(this, false);
RegisterCallback<GeometryChangedEvent>(_ => Place());
Current = this;
}
public bool Open { get { return style.display != DisplayStyle.None; } }
public VisualElement Owner { get { return _owner; } }
public static void Attach(VisualElement element, Func<LuneTip> builder)
{
if (element.pickingMode == PickingMode.Ignore)
element.pickingMode = PickingMode.Position;
element.RegisterCallback<PointerEnterEvent>(evt =>
{
if (Current != null)
Current.Hover(element, builder, evt.position);
});
element.RegisterCallback<PointerLeaveEvent>(_ => Current?.Leave(element));
element.RegisterCallback<PointerDownEvent>(_ => Current?.Leave(element));
}
public void Hover(VisualElement element, Func<LuneTip> builder, Vector2 at)
{
if (Pointer.x < 0f)
Pointer = at;
_owner = element;
_builder = builder;
_wait = _clock - _lastHide < .6f ? Quick : Delay;
Hide(false);
}
public void Leave(VisualElement element)
{
if (element == _owner)
{
Hide(true);
_owner = null;
}
}
void Hide(bool remember)
{
if (Open && remember)
_lastHide = _clock;
LuneUi.Show(this, false);
Shown = "";
}
/// <summary>Refreshes an open tooltip (values changed while hovering).</summary>
public void Refresh(VisualElement element)
{
if (element == _owner && Open)
ShowNow();
}
public void Tick(float delta)
{
_clock += delta;
if (_owner == null)
return;
if (_owner.panel == null || !Showing(_owner) || !_owner.worldBound.Contains(Pointer))
{
Hide(true);
_owner = null;
return;
}
if (!Open)
{
_wait -= delta;
if (_wait <= 0f)
ShowNow();
}
}
/// <summary>Visible in the hierarchy (Godot is_visible_in_tree).</summary>
static bool Showing(VisualElement element)
{
for (VisualElement node = element; node != null; node = node.parent)
{
if (node.style.display == DisplayStyle.None || node.resolvedStyle.display == DisplayStyle.None)
return false;
}
return true;
}
void ShowNow()
{
LuneTip tip = _builder != null ? _builder() : null;
if (tip == null || tip.IsEmpty)
{
Hide(false);
return;
}
_column.Clear();
if (tip.Title.Length > 0)
{
VisualElement head = LuneUi.Box(false, 6f, _column);
if (tip.Glyph.Length > 0)
LuneUi.Glyph(tip.Glyph, tip.GlyphColor ?? LuneUiTheme.BronzeLight, 18f, head);
LuneUi.Wrap(LuneUi.Label(tip.Title, "HudValue", head));
}
if (tip.Subtitle.Length > 0)
LuneUi.Wrap(LuneUi.Label(tip.Subtitle, "HudMuted", _column));
if (tip.Stats.Count > 0)
{
VisualElement grid = LuneUi.Box(true, 2f, _column);
foreach (string[] row in tip.Stats)
{
VisualElement line = LuneUi.Box(false, 8f, grid);
LuneUi.Glyph(row[0], LuneUiTheme.Muted, 14f, line);
Label name = LuneUi.Label(row[1], "HudMuted", line);
name.style.minWidth = 104f;
LuneUi.Wrap(LuneUi.Label(row[2], "HudText", line)).style.flexGrow = 1f;
}
}
AddLine(tip.Text, "HudText", null);
AddLine(tip.Note, "HudMuted", null);
AddLine(tip.Warn, "HudMuted", LuneUiTheme.Danger);
Shown = tip.ToString();
// Measure at the top left with the full width available, then place at that size.
_placed = false;
style.left = 0f;
style.top = 0f;
style.width = StyleKeyword.Auto;
LuneUi.Show(this, true);
BringToFront();
}
void AddLine(string text, string variation, Color? color)
{
if (string.IsNullOrEmpty(text))
return;
Label line = LuneUi.Wrap(LuneUi.Label(text, variation, _column));
line.style.width = Width - 24f;
if (color.HasValue)
LuneUi.SetColor(line, color.Value);
}
/// <summary>
/// Above the owner, centred; below when there is no room; always inside the HUD. Placed once
/// per content: measured at the top left with the full width, then the measured width is
/// fixed and the tooltip moves to whole pixels. Later geometry changes are ignored; a
/// re-placement could change the pixel snapping, the wrapping and the size again (a layout
/// loop found in the U3 tests).
/// </summary>
void Place()
{
if (_placed || !Open || _owner == null || parent == null)
return;
float width = layout.width;
float height = layout.height;
if (float.IsNaN(width) || width <= 0f || float.IsNaN(height))
return;
Rect area = parent.contentRect;
Rect anchor = parent.WorldToLocal(_owner.worldBound);
float x = anchor.center.x - width * .5f;
float y = anchor.y - height - 8f;
if (y < area.y + 6f)
y = anchor.yMax + 8f;
x = Mathf.Clamp(x, area.x + 6f, Mathf.Max(area.x + 6f, area.xMax - width - 6f));
y = Mathf.Clamp(y, area.y + 6f, Mathf.Max(area.y + 6f, area.yMax - height - 6f));
_placed = true;
_placedSize = new Vector2(width, height);
style.width = width;
style.left = Mathf.Round(x);
style.top = Mathf.Round(y);
}
}
}