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>
257 lines
9.7 KiB
C#
257 lines
9.7 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)
|
||
{
|
||
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);
|
||
_sceneTarget = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32);
|
||
Camera.targetTexture = _sceneTarget;
|
||
Game.Target = Target;
|
||
Driver.HumanSlot = Human;
|
||
Driver.Seed = seed;
|
||
Driver.StartRound = startRound;
|
||
Driver.ExtraGold = extraGold;
|
||
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.ResetCamera(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);
|
||
}
|
||
}
|
||
}
|