champions-of-luneraria/Assets/Luneraria/Tests/UI/PlayMode/HudVerifyTests.cs
Martin Stickelbroeck 2b86c0d3b5 U3B/U3C Zwischenstand: HUD-Bereiche, Eingabe und Befehle
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>
2026-10-02 01:45:02 +02:00

501 lines
28 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.Linq;
using Luneraria.Core;
using Luneraria.View;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.UIElements;
namespace Luneraria.UI.Tests
{
/// <summary>
/// U3 acceptance of the HUD in the real game (port of tools/luneraria/verify_g3.gd, parts A–H):
/// A layout at 1152×720 … 2560×1440 and ultrawide, at several UI scales;
/// B match status (Ancients, towers, lanes, waves with two and three digits, own gold only);
/// C hero, items, talents and normal units (no item slots), lane orders with the grotto warning;
/// D attack champions (temporary), the boss grotto in every state, the chooser's lane, banners;
/// E Ready lock, bots, privacy, a dropped human seat;
/// F input: minimap, lane clicks, hotkeys, field clicks, no click-through, tooltips;
/// G events, critical alerts and reduced motion;
/// H update rates and the portrait cache.
/// Every check is counted; the totals go to tools/out/hud-verify-*.txt.
/// </summary>
public sealed class HudVerifyTests
{
readonly List<string> _failures = new List<string>();
int _checks;
[SetUp]
public void Reset()
{
_failures.Clear();
_checks = 0;
}
void Check(bool ok, string label)
{
_checks++;
if (!ok)
_failures.Add(label);
}
void Done(string part)
{
Debug.Log(string.Format("LUNE_VERIFY {0}: {1} checks, {2} failed{3}", part, _checks, _failures.Count, string.Concat(_failures.Select(f => "\n FAIL " + f))));
System.IO.Directory.CreateDirectory(System.IO.Path.Combine(HudSession.Root, "tools/out"));
System.IO.File.WriteAllText(System.IO.Path.Combine(HudSession.Root, "tools/out/hud-verify-" + part + ".txt"), part + " " + _checks + " " + _failures.Count + "\n" + string.Join("\n", _failures) + "\n");
Assert.IsEmpty(_failures, string.Join("\n", _failures));
}
static bool Overlap(Rect a, Rect b)
{
return a.xMin < b.xMax - .5f && b.xMin < a.xMax - .5f && a.yMin < b.yMax - .5f && b.yMin < a.yMax - .5f;
}
// ---------------------------------------------------------------- A layout
static readonly (int w, int h, float[] scales)[] Sizes =
{
(1152, 720, new[] { 1f }), (1280, 720, new[] { .9f, 1f, 1.3f }), (1920, 1080, new[] { .9f, 1f, 1.15f, 1.3f }),
(2560, 1440, new[] { .9f, 1f, 1.3f }), (2560, 1080, new[] { 1f, 1.3f }),
};
void Layout(HudSession s, string label)
{
Dictionary<string, Rect> regions = s.Hud.Regions();
Rect area = s.Hud.Root.Q("lune-hud").layout;
foreach (KeyValuePair<string, Rect> region in regions)
Check(region.Value.xMin >= -.5f && region.Value.yMin >= -.5f && region.Value.xMax <= area.width + .5f && region.Value.yMax <= area.height + .5f,
label + ": " + region.Key + " inside the window " + region.Value + " in " + area.size);
string[][] pairs =
{
new[] { "top", "system" }, new[] { "top", "left" }, new[] { "top", "right" }, new[] { "left", "bottom" }, new[] { "left", "right" },
new[] { "right", "ready" }, new[] { "right", "minimap" }, new[] { "bottom", "ready" }, new[] { "bottom", "minimap" }, new[] { "ready", "minimap" },
new[] { "system", "right" }, new[] { "left", "minimap" },
};
foreach (string[] pair in pairs)
{
if (regions.TryGetValue(pair[0], out Rect a) && regions.TryGetValue(pair[1], out Rect b))
Check(!Overlap(a, b), label + ": " + pair[0] + " and " + pair[1] + " overlap " + a + " " + b);
}
Check(s.Hud.LogicalSize.x >= LuneHud.MinArea.x - 1f || s.Hud.UiScale <= .6f + 1e-4f, label + ": logical width " + s.Hud.LogicalSize);
Check(s.Hud.LogicalSize.y >= LuneHud.MinArea.y - 1f || s.Hud.UiScale <= .6f + 1e-4f, label + ": logical height " + s.Hud.LogicalSize);
}
[UnityTest]
[Timeout(1800000)]
public IEnumerator A_Layout()
{
foreach (var size in Sizes)
{
foreach (float scale in size.scales)
{
var s = new HudSession();
yield return s.Open(size.w, size.h, uiScale: scale);
string label = size.w + "x" + size.h + "@" + Mathf.RoundToInt(scale * 100f) + "%";
float expected = LuneHud.ScaleFor(new Vector2(size.w, size.h), scale, out _);
Check(Mathf.Abs(s.Hud.UiScale - expected) < 1e-4f, label + ": UI scale " + s.Hud.UiScale + " expected " + expected);
Check(s.Hud.UiScale <= scale + 1e-4f, label + ": scale never above the preference");
yield return s.Starter();
yield return HudSession.Frames(3);
Layout(s, label + " planning");
s.Hud.OpenHero();
yield return HudSession.Frames(2);
VisualElement modal = s.Hud.Root.Query<LuneModalPanel>().First().Panel;
Rect area = s.Hud.Root.Q("lune-hud").layout;
Rect box = s.Hud.Root.Q("lune-hud").WorldToLocal(modal.worldBound);
Check(box.xMin >= 0f && box.yMin >= 0f && box.xMax <= area.width + .5f && box.yMax <= area.height + .5f, label + ": hero window inside " + box);
s.Hud.CloseModal();
s.Runner.Ready();
yield return s.Fight(8.0);
yield return HudSession.Frames(2);
Layout(s, label + " combat");
if (Mathf.Approximately(scale, 1f) && (size.w != 1920 || size.h != 1080))
yield return s.Shot("docs/port/screenshots/u3/aufloesungen/kampf-" + size.w + "x" + size.h + ".png");
s.Close();
}
}
Done("A");
}
// ---------------------------------------------------------------- B status
[UnityTest]
[Timeout(900000)]
public IEnumerator B_Status()
{
var s = new HudSession();
yield return s.Open();
string text = s.Texts();
Check(text.Contains("DEIN ANCIENT · BLAU"), "own Ancient caption");
Check(text.Contains("GEGNER · ROT"), "enemy Ancient caption");
Check(text.Contains("12.000 / 12.000"), "Ancient HP with thousands separator");
Check(text.Contains("WELLE 1"), "wave 1");
Check(text.Contains("PLANUNG"), "phase badge");
foreach (int round in new[] { 21, 99, 120 })
{
s.Match.RoundNumber = round;
s.Sync();
yield return HudSession.Frames(2);
Check(s.Texts().Contains("WELLE " + round), "unlimited waves: WELLE " + round);
Rect top = s.Hud.Regions()["top"];
Check(top.width < 1100f, "top panel stays compact at wave " + round + ": " + top.width);
}
s.Match.RoundNumber = 1;
double core = s.Match.Map.BuildingDef("blau_ancient").MaxHp;
s.Match.BuildingHp["blau_ancient"] = core * .45;
s.Sync();
Check(s.Texts().Contains("BESCHÄDIGT"), "own Ancient below 50 %: BESCHÄDIGT");
s.Match.BuildingHp["blau_ancient"] = core * .25;
s.Sync();
s.Tick(.3f);
text = s.Texts();
Check(text.Contains("KRITISCH"), "own Ancient below 30 %: KRITISCH");
Check(text.Contains("Dein Ancient ist kritisch: 25 % Leben"), "banner for the own critical Ancient");
Check(s.Hud.Feed.Texts.Any(t => t.StartsWith("Dein Ancient kritisch")), "event for the own critical Ancient");
s.Match.BuildingHp["rot_ancient"] = core * .2;
s.Sync();
s.Tick(.3f);
Check(s.Hud.Feed.Texts.Any(t => t.StartsWith("Gegnerischer Ancient kritisch")), "event for the enemy's critical Ancient");
// A lane tower falls: indicator, threat and event.
s.Match.BuildingHp["blau_mid_outer"] = 0.0;
s.Sync();
s.Tick(.3f);
LuneLaneIndicator mid = s.Hud.Lanes["mid"];
Check(!mid.Towers[1].Alive, "fallen Grenzwacht shown as ruin");
Check(mid.Level >= LuneLaneThreat.Pressure, "Mid threat after the Grenzwacht fell: " + mid.Level);
Check(s.Hud.Feed.Texts.Any(t => t.Contains("MID – Grenzwacht (Blau) zerstört")), "event for the fallen tower");
// Own gold only: another seat's gold never reaches the HUD.
s.Match.Players[4].Draft.Gold = 98765;
s.Match.Players[0].Draft.Gold = 87654;
s.Sync();
s.Tick(.3f);
text = s.Texts();
Check(!text.Contains("98.765") && !text.Contains("87.654"), "other players' gold is never shown");
Check(text.Contains(LuneUiTheme.Number(s.Match.GoldOf(HudSession.Human))), "own gold shown");
yield return s.Shot("docs/port/screenshots/u3/pruefung/b-status.png");
s.Close();
Done("B");
}
// ---------------------------------------------------------------- C hero, items, units
[UnityTest]
[Timeout(900000)]
public IEnumerator C_HeroItemsUnits()
{
var s = new HudSession();
yield return s.Open();
yield return s.Tap(() => s.Hud.HeroCard);
Check(s.Hud.ModalOpen, "hero card opens the hero window");
Check(s.Hud.HeroDetail.RecruitButtons.Count == 3, "three heroes to recruit");
int heroCost = LuneContent.Heroes.Values[0].Cost;
bool unaffordable = s.Match.GoldOf(HudSession.Human) < heroCost;
Check(!unaffordable || s.Hud.HeroDetail.RecruitButtons.Values.All(b => b.Disabled), "recruiting blocked without gold");
Check(!unaffordable || s.Texts().Contains("Es fehlen " + (heroCost - s.Match.GoldOf(HudSession.Human)) + " Gold."), "missing gold explained");
s.Me.Draft.Gold += 2000;
s.Sync();
string heroId = LuneContent.Heroes.Keys[0];
yield return s.Tap(() => s.Hud.HeroDetail.RecruitButtons[heroId]);
Check(s.Model.OwnHero != null && s.Model.OwnHero.HeroId == heroId, "hero recruited through the window");
Check(s.Hud.HeroDetail.Tabs != null, "hero window shows the hero tabs");
Check(s.Hud.ToastText.Length > 0, "feedback after recruiting");
yield return s.Shot("docs/port/screenshots/u3/pruefung/c-held.png");
// Items: buy one, the card turns unique; slots fill up; higher items need levels.
yield return s.Tap(() => s.Hud.HeroDetail.Tabs.Tabs[LuneHeroDetail.TabItems]);
Dictionary<string, LuneItemSlot> cards = s.Hud.HeroDetail.ItemCards;
Check(cards.Count == LuneContent.Items.Count, "item shop lists every item");
string first = cards.First(c => c.Value.Current == LuneItemSlot.State.Available).Key;
yield return s.Tap(() => cards[first]);
Check(s.Model.OwnHero.Items.Contains(first), "item bought by click");
Check(s.Hud.HeroDetail.ItemCards[first].Current == LuneItemSlot.State.Unique, "carried item is marked GETRAGEN");
ItemSpec gated = LuneContent.Items.Values.FirstOrDefault(i => i.MinLevel > 1);
if (gated != null)
Check(s.Hud.HeroDetail.ItemCards[gated.Id].Current == LuneItemSlot.State.Incompatible, "item above the hero level: STUFE FEHLT");
foreach (string id in s.Hud.HeroDetail.ItemCards.Where(c => c.Value.Current == LuneItemSlot.State.Available).Select(c => c.Key).ToList())
{
if (s.Model.OwnHero.Items.Count >= LuneContent.ItemSlots)
break;
yield return s.Tap(() => s.Hud.HeroDetail.ItemCards[id]);
}
if (s.Model.OwnHero.Items.Count >= LuneContent.ItemSlots)
Check(s.Hud.HeroDetail.ItemCards.Values.Where(c => !s.Model.OwnHero.Items.Contains(c.ItemId)).All(c => c.Current == LuneItemSlot.State.Full || c.Current == LuneItemSlot.State.Incompatible),
"all slots used: other items PLÄTZE VOLL");
yield return s.Shot("docs/port/screenshots/u3/pruefung/c-items.png");
// Talents: locked at level 1, choosable at level 3.
yield return s.Tap(() => s.Hud.HeroDetail.Tabs.Tabs[LuneHeroDetail.TabTalents]);
Check(s.Hud.HeroDetail.TalentCards.All(c => c.Disabled), "no talent at level 1");
s.Me.Draft.Hero.Xp = LuneContent.LevelXp[LuneContent.TalentLevels[0] - 1];
s.Sync();
LuneButton option = s.Hud.HeroDetail.TalentCards.First(c => !c.Disabled);
yield return s.Tap(() => option);
Check(s.Model.OwnHero.Talents.Count == 1, "talent chosen by click");
s.Hud.CloseModal();
Check(!s.Hud.ModalOpen, "window closes");
// Normal units: details, lane order with the grotto warning, no item slots, return.
yield return s.Starter();
s.Sync();
Check(s.Model.OwnRoster.Count == 3, "starter lineup bought");
LuneArmyChip chip = s.Hud.Chips.Values.First();
yield return s.Tap(() => chip);
Check(s.Hud.SelectedUid == chip.Uid, "chip selects the unit");
Check(HudSession.Visible(s.Hud.UnitDetail), "unit details shown");
Check(s.Hud.UnitDetail.Query<LuneItemSlot>().ToList().Count == 0, "normal units have no item slots");
yield return s.Tap(() => s.Hud.UnitDetail.LaneButtons[LuneMatch.Grotto]);
Check(s.Model.OwnRoster.First(e => e.Uid == chip.Uid).Lane == LuneMatch.Grotto, "lane order GROTTE by click");
Check(s.Texts().Contains(LuneUnitDetail.GrottoWarning), "grotto warning shown");
yield return s.Shot("docs/port/screenshots/u3/pruefung/c-einheit.png");
int before = s.Model.OwnRoster.Count;
yield return s.Tap(() => s.Hud.UnitDetail.SellButton);
Check(s.Model.OwnRoster.Count == before - 1, "return by click");
Check(!HudSession.Visible(s.Hud.UnitDetail), "details close when the unit is gone");
s.Close();
Done("C");
}
// ---------------------------------------------------------------- D champions and boss
[UnityTest]
[Timeout(900000)]
public IEnumerator D_ChampionsAndBoss()
{
var s = new HudSession();
yield return s.Open();
s.Me.Draft.Gold += 3000;
s.Hud.OpenChampions();
s.Sync();
LuneChampionPanel panel = s.Hud.ChampionPanel;
Check(HudSession.Visible(panel), "champion tab");
yield return s.Tap(() => panel.LaneButtons["top"]);
Check(panel.TargetLane == "top", "target lane TOP");
string championId = panel.Cards.Keys.First();
yield return s.Tap(() => panel.Cards[championId]);
Check(s.Me.Draft.NewBookings.Count == 1 && s.Me.Draft.NewBookings[0].Lane == "top", "champion booked on TOP by click");
Check(s.Texts().Contains("Einsatz in Welle 2 – TOP"), "booking listed with wave and lane");
yield return s.Tap(() => panel.Cards[championId]);
Check(panel.Cards.Values.All(c => c.Disabled), "at most " + LuneContent.MaxBookings + " bookings per round");
Check(panel.Cards.Values.First().Reason.Contains("Höchstens"), "the limit is explained");
yield return s.Shot("docs/port/screenshots/u3/pruefung/d-champions.png");
LuneButton cancel = s.ButtonWithText("×", panel);
yield return s.Tap(() => cancel);
Check(s.Me.Draft.NewBookings.Count == 1, "booking cancelled by click");
s.Hud.OpenTroops();
s.Sync();
// Boss states from public state.
LuneBossIndicator boss = s.Hud.BossCard;
Check(boss.BadgeText == "BOSS VERFÜGBAR", "available: " + boss.BadgeText);
s.Match.RoundNumber = 3;
s.Match.Boss.NextAvailableRound = 6;
s.Sync();
Check(boss.BadgeText == "COOLDOWN – 3 RUNDEN", "cooldown: " + boss.BadgeText);
// The grotto was won in round 2; the boss attacks in round 3, its chooser picks the lane now.
s.Match.Boss.Claim = new BossClaim { Team = 0, WonRound = 2, DeployRound = 3, ChooserSlot = HudSession.Human, Lane = "mid" };
s.Sync();
Check(boss.BadgeText == "EINSATZ NÄCHSTE WELLE", "own boss next wave: " + boss.BadgeText);
Check(HudSession.Visible(boss.LaneButtons["bot"]), "the chooser picks the lane in the card");
yield return s.Tap(() => boss.LaneButtons["bot"]);
Check(s.Me.Draft.BossLane == "bot", "boss lane chosen by click (private until combat)");
Check(s.Texts().Contains("Deine SCHLUNDKRONE greift in Welle 3 auf BOTTOM an"), "banner for the own boss");
yield return s.Shot("docs/port/screenshots/u3/pruefung/d-boss-eigen.png");
s.Match.Boss.Claim = new BossClaim { Team = 1, WonRound = 2, DeployRound = 3, ChooserSlot = 4, Lane = "top" };
s.Sync();
string text = s.Texts();
Check(text.Contains("SCHLUNDKRONE (Rot) greift in Welle 3 auf TOP an"), "banner for the enemy boss");
Check(text.Contains("ANGEKÜNDIGT") && text.Contains("GEGEN EUCH"), "announced in the wave preview");
Check(s.Hud.Lanes["top"].Level >= LuneLaneThreat.Pressure, "enemy boss raises the lane threat");
s.Close();
Done("D");
}
// ---------------------------------------------------------------- E ready, bots, privacy
[UnityTest]
[Timeout(900000)]
public IEnumerator E_ReadyBotsPrivacy()
{
var s = new HudSession();
yield return s.Open();
string team = s.Texts();
Check(team.Contains("BOT") && team.Contains("PLANT"), "team panel: bots and the planning human");
yield return s.Starter();
s.Game.Key(KeyCode.Space);
s.Sync();
yield return HudSession.Frames(1);
Check(s.Runner.Current == LuneMatchRunner.Stage.Combat, "Space reports ready; with every bot ready the combat starts");
yield return s.FinishCombat();
Check(s.Runner.Current == LuneMatchRunner.Stage.Planning, "back in planning after the wave");
s.Game.Key(KeyCode.Space);
Check(!s.Me.Ready, "Bereit is blocked for a second after a combat (stale clicks)");
yield return new WaitForSecondsRealtime(1.05f);
s.Sync();
int roster = s.Model.OwnRoster.Count;
s.Match.SetReady(HudSession.Human, true);
s.Sync();
Check(s.Model.Locked, "plan locked");
Check(s.Hud.ShopCards.Values.All(c => c.Disabled), "cards locked after Ready");
s.Game.Key(KeyCode.Alpha1);
Check(s.Model.OwnRoster.Count == roster, "hotkey buys nothing after Ready");
Check(s.Hud.ToastText == "Bereit gemeldet – die Planung ist verbindlich.", "hotkey explains the lock");
Check(s.Texts().Contains("PLANUNG · BEREIT"), "phase badge PLANUNG · BEREIT");
// A human seat of the own team drops: a bot plays it on, marked as such.
s.Match.Players[0].IsHuman = true;
s.Sync();
s.Match.Players[0].IsHuman = false;
s.Sync();
Check(s.Texts().Contains("BOT ÜBERNIMMT"), "dropped human seat: BOT ÜBERNIMMT");
// Privacy: the same public picture for every seat, other drafts invisible.
string digest = s.Hud.PublicDigest();
for (int seat = 0; seat < 6; seat++)
Check(new LuneHudModel(s.Match, seat).PublicDigest() == digest, "seat " + seat + " sees the same public state");
s.Match.Players[4].Draft.Gold = 77777;
s.Sync();
Check(!s.Texts().Contains("77.777"), "an enemy draft's gold never appears");
s.Close();
Done("E");
}
// ---------------------------------------------------------------- F input
[UnityTest]
[Timeout(900000)]
public IEnumerator F_Input()
{
var s = new HudSession();
yield return s.Open();
// Minimap: a click moves the camera to that map point.
LuneMinimap map = s.Hud.Minimap;
Vector2 local = new Vector2(map.layout.width * .75f, map.layout.height * .25f);
Vec2 expected = map.ToWorld(local);
s.ClickAt(map.LocalToWorld(local));
yield return HudSession.Frames(1);
yield return new WaitForSecondsRealtime(.6f);
Check(s.Rig.Focus.DistanceTo(expected) < 3.0, "minimap click moves the camera to " + expected + ", focus " + s.Rig.Focus);
// Lane click and hotkeys.
yield return s.Tap(() => s.Hud.Lanes["bot"]);
yield return new WaitForSecondsRealtime(.6f);
LanePath path = s.Match.Map.LanePaths["bot"];
Vec2 lanePoint = s.Match.Map.LanePoint(0, "bot", path.Length * .2);
Check(s.Rig.Focus.DistanceTo(lanePoint) < 3.0, "lane click focuses the own BOTTOM deployment " + s.Rig.Focus + " vs " + lanePoint);
s.Game.Key(KeyCode.F3);
yield return new WaitForSecondsRealtime(.6f);
Check(s.Rig.Focus.DistanceTo(s.Match.Map.LanePoint(0, "mid", s.Match.Map.LanePaths["mid"].Length * .2)) < 3.0, "F3 focuses Mid");
s.Game.Key(KeyCode.G);
yield return new WaitForSecondsRealtime(.6f);
Check(s.Rig.Focus.DistanceTo(s.Match.Map.GrottoCenter()) < 3.0, "G focuses the grotto");
s.Game.Key(KeyCode.B);
yield return new WaitForSecondsRealtime(.6f);
Check(s.Rig.Focus.DistanceTo(s.Match.Map.CorePosition(0)) < 3.0, "B focuses the own Ancient");
s.Game.Key(KeyCode.H);
Check(s.Hud.ToastText.StartsWith("Kein Held im Feld"), "H without a hero explains why");
// Hotkeys 1–5 buy.
int roster = s.Model.OwnRoster.Count;
s.Game.Key(KeyCode.Alpha2);
Check(s.Model.OwnRoster.Count == roster + 1, "hotkey 2 buys the second card's troop");
// No click-through: HUD panels take the pointer, the open field does not.
Vector2 topPixel = PanelToPixel(s, HudSession.Center(s.Hud.Root.Q("lune-hud").Query<LunePanel>().First()));
Check(s.Hud.IsPointerOverHud(topPixel), "the HUD takes clicks on its panels");
Check(!s.Hud.IsPointerOverHud(new Vector2(s.Width * .5f, s.Height * .5f)), "the open field is free");
// Field click on the own preview unit selects it.
s.Game.FocusLane("mid", true);
yield return HudSession.Frames(4);
LuneUnitView figure = s.View.Units.FirstOrDefault(v => v.Unit != null && v.Unit.OwnerSlot == HudSession.Human && v.Unit.Kind == "troop");
Check(figure != null, "planning shows the own army in formation");
if (figure != null)
{
Vector3 screen = s.Camera.WorldToScreenPoint(figure.AimPoint);
s.Game.ClickField(new Vector2(screen.x, screen.y));
Check(s.Hud.SelectedUid == figure.Unit.PersistentUid, "field click selects the own unit");
s.Game.Key(KeyCode.Escape);
Check(s.Hud.SelectedUid.Length == 0, "Esc clears the selection");
}
// Tooltips: delay, content, gone on leave.
LuneUnitCard card = s.Hud.ShopCards.Values.First();
LuneTooltip tip = s.Hover(card);
Check(tip.Open && tip.Shown.Contains(DataCatalog.GetUnit(card.TypeId).DisplayName), "card tooltip with the unit: " + tip.Shown);
s.Leave();
Check(!tip.Open, "tooltip hides when the pointer leaves");
tip = s.Hover(s.Hud.Lanes["mid"]);
Check(tip.Open && tip.Shown.Contains("MID-LANE"), "lane tooltip");
yield return s.Shot("docs/port/screenshots/u3/pruefung/f-tooltip.png");
s.Leave();
// Options: F1 opens, a running combat pauses, Esc closes.
s.Runner.Ready();
yield return s.Fight(2.0);
s.Game.Key(KeyCode.F1);
Check(s.Hud.Settings.Open, "F1 opens the options");
Check(s.Driver.Paused, "options pause the combat offline");
s.Game.Key(KeyCode.Escape);
Check(!s.Hud.Settings.Open && !s.Driver.Paused, "Esc closes the options and resumes");
s.Close();
Done("F");
}
static Vector2 PanelToPixel(HudSession s, Vector2 panel)
{
float scale = Mathf.Min(s.Width / (float)LuneHud.Reference.x, s.Height / (float)LuneHud.Reference.y) * s.Hud.UiScale;
Vector2 pixel = panel * scale;
return new Vector2(pixel.x, s.Height - pixel.y);
}
// ---------------------------------------------------------------- G events and reduced motion
[UnityTest]
[Timeout(900000)]
public IEnumerator G_EventsAndMotion()
{
foreach (bool reduced in new[] { false, true })
{
var s = new HudSession();
yield return s.Open(reducedMotion: reduced);
for (int index = 0; index < 7; index++)
s.Hud.Feed.Push("Ereignis " + index);
Check(s.Hud.Feed.Texts.Count == LuneEventFeed.Limit, "event feed holds at most five entries");
Check(s.Hud.Feed.Texts[0] == "Ereignis 6", "newest entry on top");
s.Tick(LuneEventFeed.Lifetime + .5f, .1f);
Check(s.Hud.Feed.Texts.Count == 0, "entries fade after their lifetime");
double max = s.Match.Map.BuildingDef("rot_ancient").MaxHp;
s.Match.BuildingHp["rot_ancient"] = max * .2;
s.Sync();
s.Tick(.3f);
var lanes = s.Hud.Lanes["mid"];
s.Match.BuildingHp["blau_mid_outer"] = 0.0;
s.Match.BuildingHp["blau_mid_inner"] = 0.0;
s.Sync();
s.Tick(.3f);
Check(lanes.Level == LuneLaneThreat.Critical, "both Mid towers fell: KRITISCH");
Check(s.Hud.Feed.Texts.Count >= 2, "tower falls reported");
Check(lanes.Pulsing == !reduced, (reduced ? "reduced motion: no pulse" : "critical lane pulses") + " on the threat badge");
Check(s.Hud.AncientPulsing(1) == !reduced, (reduced ? "reduced motion: no pulse" : "critical Ancient pulses") + " on the Ancient bar");
s.Close();
}
Done("G");
}
// ---------------------------------------------------------------- H rates and portraits
[UnityTest]
[Timeout(900000)]
public IEnumerator H_RatesAndPortraits()
{
var s = new HudSession();
yield return s.Open();
int before = s.Hud.Syncs;
s.Tick(2f, .01f);
int planning = s.Hud.Syncs - before;
Check(planning >= 18 && planning <= 22, "planning: 10 syncs per second (" + planning + " in 2 s)");
s.Runner.Ready();
s.Hud.Tick(.01f);
before = s.Hud.Syncs;
s.Tick(2f, .01f);
int combat = s.Hud.Syncs - before;
Check(combat >= 9 && combat <= 11, "combat: 5 syncs per second (" + combat + " in 2 s)");
Check(s.Hud.SyncMs < 4.0, "one HUD sync costs " + s.Hud.SyncMs.ToString("0.000") + " ms");
LunePortraits cache = LunePortraits.Shared;
cache.Portrait("g01");
cache.Flush();
int rendered = cache.Rendered;
Texture first = cache.Portrait("g01");
cache.Flush();
Texture again = cache.Portrait("g01");
Check(first != null && first == again, "portrait cached");
Check(cache.Rendered == rendered, "each portrait is rendered once");
Debug.Log("LUNE_VERIFY rates planning " + planning + "/2s combat " + combat + "/2s sync " + s.Hud.SyncMs.ToString("0.000") + " ms portraits " + cache.Rendered);
s.Close();
Done("H");
}
}
}