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>
259 lines
9.9 KiB
C#
259 lines
9.9 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|