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>
580 lines
34 KiB
C#
580 lines
34 KiB
C#
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;
|
||
/// I (U3 addition) the field: formation areas, grotto entrance, resting boss, name tags, owner ring, Ancient bar.
|
||
/// 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");
|
||
}
|
||
|
||
// ---------------------------------------------------------------- I field
|
||
static GameObject SceneObject(string name)
|
||
{
|
||
return Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None).Select(t => t.gameObject).FirstOrDefault(g => g.name == name);
|
||
}
|
||
|
||
/// <summary>
|
||
/// U3 addition (from the G3 motif comparison): the field around the HUD. Formation areas and
|
||
/// the own grotto entrance in planning, the resting Schlundkrone while the grotto is open,
|
||
/// name tags of heroes and of a deployed boss, its owner ring, the Ancients' health bar.
|
||
/// </summary>
|
||
[UnityTest]
|
||
[Timeout(900000)]
|
||
public IEnumerator I_Field()
|
||
{
|
||
var s = new HudSession();
|
||
yield return s.Open();
|
||
yield return HudSession.Frames(2);
|
||
List<Renderer> pads = Object.FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None).Where(r => r.gameObject.name.StartsWith("Deployment_")).ToList();
|
||
Check(pads.Count == 6, "six formation areas in the scene: " + pads.Count);
|
||
Check(pads.Where(p => p.gameObject.name.StartsWith("Deployment_0_")).All(p => p.gameObject.activeInHierarchy), "own formation areas shown in planning");
|
||
Check(pads.Where(p => p.gameObject.name.StartsWith("Deployment_1_")).All(p => !p.gameObject.activeInHierarchy), "enemy formation areas hidden");
|
||
GameObject ownEntry = SceneObject("GrottoEntry_0");
|
||
GameObject enemyEntry = SceneObject("GrottoEntry_1");
|
||
Check(ownEntry != null && ownEntry.activeInHierarchy, "own grotto entrance shown while the grotto is open");
|
||
Check(enemyEntry != null && !enemyEntry.activeInHierarchy, "enemy grotto entrance hidden");
|
||
Check(s.View.RestingBoss != null && s.View.RestingBoss.activeInHierarchy, "the Schlundkrone rests in the open grotto");
|
||
s.Match.Boss.NextAvailableRound = 5;
|
||
yield return HudSession.Frames(2);
|
||
Check(ownEntry != null && !ownEntry.activeInHierarchy, "grotto entrance hidden while the grotto is locked");
|
||
Check(s.View.RestingBoss != null && !s.View.RestingBoss.activeInHierarchy, "no resting boss while the grotto is locked");
|
||
s.Match.Boss.NextAvailableRound = 1;
|
||
// The starter army stands on Mid: that area lights up.
|
||
yield return s.Starter();
|
||
s.Leave();
|
||
yield return HudSession.Frames(2);
|
||
var block = new MaterialPropertyBlock();
|
||
Renderer mid = pads.First(p => p.gameObject.name == "Deployment_0_mid");
|
||
mid.GetPropertyBlock(block);
|
||
Check(block.GetFloat("_Highlight") > .5f, "the area of a lane with own units lights up");
|
||
Renderer top = pads.First(p => p.gameObject.name == "Deployment_0_top");
|
||
top.GetPropertyBlock(block);
|
||
Check(block.GetFloat("_Highlight") < .5f, "an empty lane's area stays plain");
|
||
// A recruited hero carries its name and level in the planning preview.
|
||
s.Me.Draft.Gold += 2000;
|
||
s.Hud.OpenHero();
|
||
string heroId = LuneContent.Heroes.Keys[0];
|
||
yield return s.Tap(() => s.Hud.HeroDetail.RecruitButtons[heroId]);
|
||
s.Hud.CloseModal();
|
||
s.Leave();
|
||
yield return HudSession.Frames(3);
|
||
string heroTag = LuneContent.Heroes[heroId].Name + " · Stufe 1";
|
||
Check(s.Hud.FieldTags.Texts.Contains(heroTag), "hero tag in the planning preview: " + string.Join(", ", s.Hud.FieldTags.Texts));
|
||
// An Ancient below full health shows its bar.
|
||
double core = s.Match.Map.BuildingDef("blau_ancient").MaxHp;
|
||
s.Match.BuildingHp["blau_ancient"] = core * .6;
|
||
yield return HudSession.Frames(2);
|
||
Transform bar = s.View.Building("blau_ancient").transform.Find("AncientBar");
|
||
Check(bar != null && bar.gameObject.activeInHierarchy, "damaged Ancient shows its health bar");
|
||
Transform enemyBar = s.View.Building("rot_ancient").transform.Find("AncientBar");
|
||
Check(enemyBar == null || !enemyBar.gameObject.activeInHierarchy, "an Ancient at full health shows none");
|
||
// Combat with the own team's boss deployed on Mid: areas hidden, boss tag and owner ring.
|
||
s.Match.Boss.Claim = new BossClaim { Team = 0, WonRound = 0, DeployRound = s.Match.RoundNumber, ChooserSlot = HudSession.Human, Lane = "mid" };
|
||
s.Game.Key(KeyCode.Space);
|
||
yield return s.Fight(1.0);
|
||
yield return HudSession.Frames(2);
|
||
Check(pads.All(p => !p.gameObject.activeInHierarchy), "formation areas hidden in combat");
|
||
Check(ownEntry != null && !ownEntry.activeInHierarchy, "grotto entrance hidden in combat");
|
||
Check(s.Hud.FieldTags.Texts.Contains("SCHLUNDKRONE · für Blau"), "deployed boss says whom it fights for: " + string.Join(", ", s.Hud.FieldTags.Texts));
|
||
Check(s.Hud.FieldTags.Texts.Contains(heroTag), "hero tag in combat");
|
||
LuneUnitView boss = s.View.Units.FirstOrDefault(v => v.Unit.Kind == "boss" && v.Unit.Team == 0);
|
||
Transform ring = boss != null ? boss.transform.Find("OwnerRing") : null;
|
||
Check(ring != null && ring.gameObject.activeInHierarchy, "deployed boss wears the owner ring of its team");
|
||
Check(s.View.RestingBoss == null || !s.View.RestingBoss.activeInHierarchy, "no resting boss while it is deployed");
|
||
s.Close();
|
||
Done("I");
|
||
}
|
||
}
|
||
}
|