champions-of-luneraria/Assets/Luneraria/Tests/UI/PlayMode/HudSession.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

259 lines
9.9 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Luneraria.Core;
using Luneraria.View;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI.Tests
{
/// <summary>
/// The real game for the U3 tests (port of the helpers of tools/luneraria/verify_g3.gd): the G1
/// scene with its game controller, a human seat, the HUD and the camera rendering into textures
/// of one size, so screen points of the camera and of the HUD agree. Input goes through the
/// real paths: UI Toolkit pointer events for the HUD, LuneGame.Key and ClickField for keys and
/// the field. Nothing sets HUD state directly; public match state (round, HP, gold, claims) is
/// set like Godot's g3_views.gd did.
/// </summary>
public sealed class HudSession
{
public const int Human = 1;
public LuneGame Game;
public LuneHud Hud;
public LuneMatchDriver Driver;
public LuneMatchView View;
public LuneCameraRig Rig;
public Camera Camera;
public RenderTexture Target;
RenderTexture _sceneTarget;
public int Width;
public int Height;
public LuneMatch Match { get { return Driver.Runner.Match; } }
public LuneMatchRunner Runner { get { return Driver.Runner; } }
public LuneHudModel Model { get { return Hud.Model; } }
public Player Me { get { return Match.Players[Human]; } }
public static string Root { get { return Path.GetDirectoryName(Application.dataPath); } }
/// <summary>Loads the scene and starts a match with the human seat, HUD into a width×height texture.</summary>
public IEnumerator Open(int width = 1920, int height = 1080, int seed = 4, float uiScale = 1f, bool reducedMotion = false, int startRound = 1, int extraGold = 0, string[] bunds = null)
{
LunePreferences.ResetSession(uiScale, reducedMotion);
yield return HudCaptureTests.LoadScene();
Width = width;
Height = height;
Game = Object.FindAnyObjectByType<LuneGame>();
Assert.IsNotNull(Game, "the scene has a game controller");
Driver = Game.Driver;
View = Game.View;
Rig = Game.Rig;
Camera = View.ViewCamera != null ? View.ViewCamera : Camera.main;
Target = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
// MSAA like the player (URP asset 4×); a camera target texture sets the sample count itself.
_sceneTarget = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32) { antiAliasing = 4 };
Camera.targetTexture = _sceneTarget;
Game.Target = Target;
Driver.HumanSlot = Human;
Driver.Seed = seed;
Driver.StartRound = startRound;
Driver.ExtraGold = extraGold;
Driver.Bunds = bunds;
Driver.Speed = 1f;
Driver.Restart();
yield return Frames(3);
Hud = Game.Hud;
Assert.AreSame(Driver.Runner.Match, Hud.Match, "the HUD shows the driver's match");
Game.StartCamera(true);
yield return Frames(2);
}
public void Close()
{
if (Camera != null)
Camera.targetTexture = null;
Target?.Release();
_sceneTarget?.Release();
}
public static IEnumerator Frames(int count)
{
for (int index = 0; index < count; index++)
yield return null;
}
/// <summary>The HUD rebuilds now (Godot sync()).</summary>
public void Sync()
{
Hud.MarkDirty();
Hud.Tick(0f);
}
/// <summary>HUD time passes (tooltips, feed, rates) without simulating the match.</summary>
public void Tick(float seconds, float step = .02f)
{
for (float time = 0f; time < seconds - 1e-4f; time += step)
Hud.Tick(step);
}
// ---------------------------------------------------------------- pointer input (UI Toolkit events)
IPanel Panel { get { return Hud.Root.panel; } }
public void Pointer(Vector2 at, EventType type)
{
var source = new Event { type = type, mousePosition = at, button = 0, clickCount = 1 };
EventBase evt = type == EventType.MouseDown ? PointerDownEvent.GetPooled(source)
: type == EventType.MouseUp ? PointerUpEvent.GetPooled(source)
: (EventBase)PointerMoveEvent.GetPooled(source);
using (evt)
Panel.visualTree.SendEvent(evt);
}
public static Vector2 Center(VisualElement element)
{
return element.worldBound.center;
}
public void MoveTo(Vector2 at) { Pointer(at, EventType.MouseMove); }
public void ClickAt(Vector2 at)
{
Pointer(at, EventType.MouseMove);
Pointer(at, EventType.MouseDown);
Pointer(at, EventType.MouseUp);
}
/// <summary>A real click on an element (pointer move, down and up at its centre).</summary>
public void Click(VisualElement element)
{
Assert.IsNotNull(element);
Assert.IsTrue(Visible(element), "click on a hidden element: " + element.name + " " + element.GetType().Name);
ClickAt(Center(element));
Sync();
}
/// <summary>
/// A real click on an element that may just have been rebuilt: one layout frame first (a
/// player can only click what is drawn), then pointer move, down and up, then a sync.
/// </summary>
public IEnumerator Tap(System.Func<VisualElement> find)
{
yield return null;
VisualElement element = find();
Assert.IsNotNull(element, "nothing to tap");
Assert.IsTrue(Visible(element), "tap on a hidden element: " + element.GetType().Name);
Rect bound = element.worldBound;
Assert.IsFalse(float.IsNaN(bound.width) || bound.width <= 0f, "tap on an element without layout: " + element.GetType().Name);
ClickAt(bound.center);
Sync();
yield return null;
}
public LuneTooltip Hover(VisualElement element, float wait = .35f)
{
MoveTo(Center(element));
Tick(wait);
return Hud.Tooltip;
}
public void Leave()
{
MoveTo(new Vector2(-50f, -50f));
Tick(.05f);
}
// ---------------------------------------------------------------- queries
public static bool Visible(VisualElement element)
{
for (VisualElement node = element; node != null; node = node.parent)
{
if (node.style.display == DisplayStyle.None)
return false;
}
return element.panel != null;
}
/// <summary>All visible text of the HUD (Godot texts(root)).</summary>
public string Texts(VisualElement root = null)
{
var parts = new List<string>();
root = root ?? Hud.Root;
root.Query<TextElement>().ForEach(text =>
{
if (Visible(text) && !string.IsNullOrEmpty(text.text))
parts.Add(text.text);
});
return string.Join("\n", parts);
}
public T First<T>(System.Func<T, bool> where = null) where T : VisualElement
{
return Hud.Root.Query<T>().ToList().FirstOrDefault(e => Visible(e) && (where == null || where(e)));
}
public LuneButton ButtonWithText(string text, VisualElement root = null)
{
return (root ?? Hud.Root).Query<LuneButton>().ToList().FirstOrDefault(b => Visible(b) && b.Text == text);
}
// ---------------------------------------------------------------- match helpers (public state, like g3_views.gd)
public IEnumerator Starter()
{
yield return Tap(() => ButtonWithText("Startaufstellung"));
}
/// <summary>Plays the running combat to its end through the host loop (no HUD shortcuts).</summary>
public IEnumerator FinishCombat(int maxFrames = 20000)
{
int frames = 0;
while (Runner.Current == LuneMatchRunner.Stage.Combat && frames++ < maxFrames)
{
Runner.Advance(.25);
if (frames % 20 == 0)
yield return null;
}
Runner.Advance(.02);
yield return Frames(2);
Sync();
}
/// <summary>Steps the combat for some simulated seconds.</summary>
public IEnumerator Fight(double seconds)
{
double done = 0.0;
while (done < seconds && Runner.Current == LuneMatchRunner.Stage.Combat)
{
Runner.Advance(.05);
done += .05;
}
yield return Frames(2);
Sync();
}
// ---------------------------------------------------------------- screenshots
public void Capture(string relative)
{
LunePortraits.Shared?.Flush();
Camera.Render();
Texture2D scene = LuneHudCapture.Read(_sceneTarget);
Texture2D overlay = LuneHudCapture.Read(Target);
Texture2D image = LuneHudCapture.Compose(scene, overlay);
LuneHudCapture.Save(image, Path.Combine(Root, relative));
Object.Destroy(scene);
Object.Destroy(overlay);
Object.Destroy(image);
}
/// <summary>Waits until portraits and the HUD have caught up, then captures.</summary>
public IEnumerator Shot(string relative)
{
Sync();
LunePortraits.Shared?.Flush();
yield return Frames(3);
Capture(relative);
}
}
}