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 { /// /// 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. /// 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); } } /// Loads the scene and starts a match with the human seat, HUD into a width×height texture. 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, bool extended = false) { LunePreferences.ResetSession(uiScale, reducedMotion); yield return HudCaptureTests.LoadScene(); Width = width; Height = height; Game = Object.FindAnyObjectByType(); 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.Extended = extended; 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; } /// The HUD rebuilds now (Godot sync()). public void Sync() { Hud.MarkDirty(); Hud.Tick(0f); } /// HUD time passes (tooltips, feed, rates) without simulating the match. 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); } /// A real click on an element (pointer move, down and up at its centre). 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(); } /// /// 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. /// public IEnumerator Tap(System.Func 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; } /// All visible text of the HUD (Godot texts(root)). public string Texts(VisualElement root = null) { var parts = new List(); root = root ?? Hud.Root; root.Query().ForEach(text => { if (Visible(text) && !string.IsNullOrEmpty(text.text)) parts.Add(text.text); }); return string.Join("\n", parts); } public T First(System.Func where = null) where T : VisualElement { return Hud.Root.Query().ToList().FirstOrDefault(e => Visible(e) && (where == null || where(e))); } public LuneButton ButtonWithText(string text, VisualElement root = null) { return (root ?? Hud.Root).Query().ToList().FirstOrDefault(b => Visible(b) && b.Text == text); } // ---------------------------------------------------------------- match helpers (public state, like g3_views.gd) public IEnumerator Starter() { yield return Tap(() => ButtonWithText("Startaufstellung")); } /// Plays the running combat to its end through the host loop (no HUD shortcuts). 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(); } /// Steps the combat for some simulated seconds. 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); } /// Waits until portraits and the HUD have caught up, then captures. public IEnumerator Shot(string relative) { Sync(); LunePortraits.Shared?.Flush(); yield return Frames(3); Capture(relative); } } }