champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneTooltip.cs
Martin Stickelbroeck 527f7c5a35 U3-Nachtrag: Spieltest bis Welle 8, Tooltip-Wertzeilen, Motive
Die offenen Reste der U3-Abnahme, auf dem letzten Commit (U5.2).

- Spieltest über die echte Oberfläche (HudPlaytest): Wellen 1–8 nur mit
  Klicks und Tasten gespielt, Ende in der Planung von Welle 9. Geprüft:
  Startaufstellung, Tastenkauf, Lane- und Karten-Tooltips, Bereit, Tempo,
  Pause, Lane-Wechsel, Kamera-Reset, Einheitendetails und Einsatz auf
  Bottom, Held anwerben, Items, Talentwahl, Angriffschampion mit
  Ankündigung und Einsatz. Der erste Lauf endete in Runde 3, weil die
  Werkzeugumgebung den Unity-Prozess beendete.
- Tooltip: Wertzeilen wie Godots GridContainer in drei Spalten ohne
  Umbruch; vorher brach „7.500 / 7.500 (100 %)“ um.
- Motive bei 1280×720, 2560×1080 und 2560×1440 (JPEG), die der
  U3D-Commit nannte, aber nicht enthielt; erzeugt vom U3-Stand e2aadcb.
- Befund für U4: Der Startbildschirm ist nicht in der Szene eingebunden.
- Tests: EditMode 29/29, PlayMode 13/13, Verify 718/0, Fingerabdrücke
  der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:54:54 +02:00

229 lines
8.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;
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();
// Like Godot: title, subtitle and stat rows never wrap (a long row widens the tooltip);
// only text, note and warning wrap at the tooltip width.
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.Label(tip.Title, "HudValue", head);
}
if (tip.Subtitle.Length > 0)
LuneUi.Label(tip.Subtitle, "HudMuted", _column);
if (tip.Stats.Count > 0)
{
// Godot GridContainer with three columns (glyph, name, value; 8 apart, rows 2 apart).
// The rows are single lines, so three columns of equally high cells line up like
// the grid and the widest name sets the second column.
VisualElement grid = LuneUi.Box(false, 8f, _column);
grid.style.alignItems = Align.FlexStart;
VisualElement glyphs = LuneUi.Box(true, 2f, grid);
VisualElement names = LuneUi.Box(true, 2f, grid);
VisualElement values = LuneUi.Box(true, 2f, grid);
foreach (string[] row in tip.Stats)
{
LuneUi.Glyph(row[0], LuneUiTheme.Muted, 14f, Cell(glyphs));
LuneUi.Label(row[1], "HudMuted", Cell(names));
LuneUi.Label(row[2], "HudText", Cell(values));
}
}
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();
}
/// <summary>One grid cell: a line high, content centred vertically.</summary>
static VisualElement Cell(VisualElement column)
{
var cell = new VisualElement { pickingMode = PickingMode.Ignore };
cell.style.height = RowHeight;
cell.style.justifyContent = Justify.Center;
cell.style.alignItems = Align.FlexStart;
column.Add(cell);
return cell;
}
/// <summary>Height of a stat row (a 15 px Cantarell line).</summary>
const float RowHeight = 19f;
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);
}
}
}