champions-of-luneraria/Assets/Luneraria/Tests/UI/PlayMode/HudSession.cs
Martin Stickelbroeck 8e4b52f4ab S1 Strategie-Regeln nach Legion TD 2, Dota 2 und League of Legends
Hinter MatchOptions.Extended (Standard im Spiel, Schalter im Startbildschirm):
Söldner-Einkommen (Angriffschampion +Preis/12 Einkommen dauerhaft), Kasernen
(gefallener innerer Turm: Gegnerwellen +25 % und ein Monster mehr), Erschöpfung
und Rückkauf (60 % Leben, Rückkauf 60 + 20 x Stufe), Mondsegen (Grottensieg
+4 % je Stapel, Mondseele bei 4). Bots wählen mit erweiterten Regeln auch
Sylvara und kaufen Helden zurück. Netz überträgt Regeln und neue Zustände.
HUD: Einkommen, Kasernen-Symbol, Rückkauf, Segen im Teamkopf.
Kern 531/531 (s1 31), EditMode 31/31, PlayMode 16/16, Verify 718/0,
Selbsttest 30/30, Golden 0 Abweichungen, Fingerabdruck unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:18:47 +02:00

260 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, bool extended = false)
{
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.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;
}
/// <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);
}
}
}