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>
223 lines
9.7 KiB
C#
223 lines
9.7 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;
|
||
|
||
namespace Luneraria.UI.Tests
|
||
{
|
||
/// <summary>
|
||
/// The fourteen G3 motifs (port of tools/luneraria/g3_views.gd) from the real game with the
|
||
/// real HUD at one window size. Like Godot, fixtures only set public match state before or
|
||
/// between phases (gold, round, building HP, boss claim, the armies of round 14), the starter
|
||
/// army comes from the HUD's own button (its toast stays, as in Godot's pictures), further
|
||
/// commands go straight to the match followed by the preview rebuild (Godot command() and
|
||
/// _rebuild_preview()), and HUD views open through the HUD's public methods. After a click
|
||
/// the pointer leaves the HUD, so no tooltip covers a picture. A capture tool, not part of the
|
||
/// suite: -testFilter "Luneraria.UI.Tests.HudViewsTests" (all sizes) or one size by its test
|
||
/// name. 1920×1080 is written as PNG (the reference set next to Godot's), the other sizes as
|
||
/// JPEG to keep the repository small.
|
||
/// </summary>
|
||
[Explicit("Bildwerkzeug: nur gezielt per -testFilter")]
|
||
public sealed class HudViewsTests
|
||
{
|
||
/// <summary>The Bünde of Godot's g3_views.gd (the human seat 1 plays the Wurzelbund).</summary>
|
||
static readonly string[] Bunds = { "wurzelbund", "wurzelbund", "glutpakt", "glutpakt", "wurzelbund", "glutpakt" };
|
||
|
||
int _width;
|
||
int _height;
|
||
string _folder;
|
||
string _extension;
|
||
readonly List<string> _shots = new List<string>();
|
||
|
||
[UnityTest, Timeout(1800000)] public IEnumerator Motifs_1920x1080() { yield return Run(1920, 1080); }
|
||
[UnityTest, Timeout(1800000)] public IEnumerator Motifs_2560x1440() { yield return Run(2560, 1440); }
|
||
[UnityTest, Timeout(1800000)] public IEnumerator Motifs_1280x720() { yield return Run(1280, 720); }
|
||
[UnityTest, Timeout(1800000)] public IEnumerator Motifs_2560x1080() { yield return Run(2560, 1080); }
|
||
|
||
IEnumerator Run(int width, int height)
|
||
{
|
||
_width = width;
|
||
_height = height;
|
||
_folder = "docs/port/screenshots/u3/motive-" + width + "x" + height + "/";
|
||
_extension = width == 1920 && height == 1080 ? ".png" : ".jpg";
|
||
_shots.Clear();
|
||
yield return PlanningViews();
|
||
yield return BossViews();
|
||
yield return CombatViews();
|
||
yield return AncientViews();
|
||
Debug.Log("LUNE_VIEWS " + width + "x" + height + ": " + _shots.Count + " " + string.Join(", ", _shots));
|
||
Assert.AreEqual(14, _shots.Count, "all fourteen motifs");
|
||
}
|
||
|
||
IEnumerator Fresh(HudSession s)
|
||
{
|
||
yield return s.Open(_width, _height, seed: 4, bunds: Bunds);
|
||
}
|
||
|
||
IEnumerator Shot(HudSession s, string label)
|
||
{
|
||
yield return s.Shot(_folder + label + _extension);
|
||
_shots.Add(label);
|
||
}
|
||
|
||
static IEnumerator Wait(float seconds)
|
||
{
|
||
yield return new WaitForSecondsRealtime(seconds);
|
||
}
|
||
|
||
/// <summary>Godot command(): a command of the human seat straight to the match, then the preview.</summary>
|
||
static void Send(HudSession s, Command command)
|
||
{
|
||
CommandResult result = s.Match.Submit(HudSession.Human, command);
|
||
Assert.IsTrue(result.Ok, command.Type + ": " + result.Message);
|
||
s.Game.RebuildPreview();
|
||
s.Sync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Godot armies(): the given seats plan round n with plenty of gold (profile "ausgewogen"),
|
||
/// grotto orders go to the own lane; the human keeps an ordinary amount afterwards.
|
||
/// </summary>
|
||
static void Armies(HudSession s, int round)
|
||
{
|
||
LuneMatch match = s.Match;
|
||
match.RoundNumber = round;
|
||
for (int slot = 0; slot < 6; slot++)
|
||
{
|
||
Draft draft = match.Players[slot].Draft;
|
||
draft.Roster.Clear();
|
||
draft.Gold = 100000;
|
||
LuneBot.Plan(match, slot, "ausgewogen");
|
||
foreach (RosterEntry entry in draft.Roster)
|
||
{
|
||
if (entry.Lane == LuneMatch.Grotto)
|
||
entry.Lane = match.Players[slot].Lane;
|
||
}
|
||
if (draft.Hero != null && draft.Hero.Lane == LuneMatch.Grotto)
|
||
draft.Hero.Lane = match.Players[slot].Lane;
|
||
}
|
||
match.Players[HudSession.Human].Draft.Gold = 185;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- planning
|
||
IEnumerator PlanningViews()
|
||
{
|
||
var s = new HudSession();
|
||
yield return Fresh(s);
|
||
// A normal planning round: round 3, starter army bought, some gold left.
|
||
s.Match.RoundNumber = 3;
|
||
s.Me.Draft.Gold = 420;
|
||
yield return s.Starter();
|
||
s.Leave();
|
||
yield return Wait(.4f);
|
||
Assert.AreEqual(3, s.Model.OwnRoster.Count, "starter army");
|
||
yield return Shot(s, "01-planung");
|
||
// Hero hired and shown.
|
||
s.Me.Draft.Gold = 900;
|
||
Send(s, Command.HeroBuy("held_ringborke"));
|
||
s.Hud.OpenHero();
|
||
yield return Wait(.4f);
|
||
yield return Shot(s, "02-held-ausgewaehlt");
|
||
// Item choice with one item equipped.
|
||
Send(s, Command.ItemBuy("glutklinge", s.Me.Draft.Hero.Uid));
|
||
s.Hud.OpenItems();
|
||
yield return Wait(.3f);
|
||
yield return Shot(s, "03-itemauswahl");
|
||
s.Hud.CloseModal();
|
||
// A normal unit selected.
|
||
s.Hud.SelectUnit(s.Model.OwnRoster.First().Uid);
|
||
yield return Wait(.3f);
|
||
yield return Shot(s, "04-einheit-ausgewaehlt");
|
||
s.Hud.ClearSelection();
|
||
// Attack champions: one booked for the next round.
|
||
Send(s, Command.ChampionBook("ch_belagerung", "mid"));
|
||
s.Hud.OpenChampions();
|
||
yield return Wait(.3f);
|
||
yield return Shot(s, "05-angriffschampion");
|
||
s.Close();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- boss
|
||
IEnumerator BossViews()
|
||
{
|
||
var s = new HudSession();
|
||
yield return Fresh(s);
|
||
s.Match.RoundNumber = 10;
|
||
s.Match.Boss.NextAvailableRound = 10;
|
||
s.Game.FocusTarget("grotto");
|
||
s.Sync();
|
||
yield return Wait(1f);
|
||
yield return Shot(s, "06-boss-verfuegbar");
|
||
// The own team conquered the boss in round 11: deployment in round 12 on Mid, planning of 12.
|
||
s.Match.RoundNumber = 12;
|
||
s.Match.Boss.Claim = new BossClaim { Team = 0, WonRound = 11, DeployRound = 12, ChooserSlot = HudSession.Human, Lane = "mid" };
|
||
s.Match.Boss.NextAvailableRound = 15;
|
||
s.Game.FocusLane("mid", true);
|
||
s.Sync();
|
||
yield return Wait(1f);
|
||
yield return Shot(s, "07-boss-angekuendigt");
|
||
s.Close();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- combat
|
||
IEnumerator CombatViews()
|
||
{
|
||
var s = new HudSession();
|
||
yield return Fresh(s);
|
||
Armies(s, 14);
|
||
s.Match.BuildingHp["rot_top_outer"] = 0.0;
|
||
s.Match.BuildingHp["blau_bot_outer"] = 0.0;
|
||
s.Game.Key(KeyCode.Space);
|
||
yield return HudSession.Frames(1);
|
||
Assert.AreEqual(LuneMatchRunner.Stage.Combat, s.Runner.Current, "combat of round 14");
|
||
// Godot advance_combat(22): the clock runs to 22 s, then holds.
|
||
yield return s.Fight(22.0);
|
||
s.Driver.Paused = true;
|
||
foreach (var spec in new[] { ("08-kampf-top", "top"), ("09-kampf-mid", "mid"), ("10-kampf-bottom", "bot") })
|
||
{
|
||
s.Game.FocusLane(spec.Item2, true);
|
||
yield return Wait(.5f);
|
||
yield return Shot(s, spec.Item1);
|
||
}
|
||
Vec2 ruin = s.Match.Map.BuildingDef("blau_bot_outer").Position;
|
||
s.Rig.LookAt(ruin, 22f, true);
|
||
yield return Wait(.5f);
|
||
yield return Shot(s, "13-turm-zerstoert");
|
||
s.Rig.LookAt(Vec2.Zero, 46f, true);
|
||
yield return Wait(.5f);
|
||
yield return Shot(s, "14-grosse-kampfansicht");
|
||
s.Close();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Ancients
|
||
IEnumerator AncientViews()
|
||
{
|
||
var s = new HudSession();
|
||
yield return Fresh(s);
|
||
s.Match.RoundNumber = 21;
|
||
foreach (string id in new[] { "blau_mid_outer", "blau_mid_inner", "blau_base_1", "blau_base_2" })
|
||
s.Match.BuildingHp[id] = 0.0;
|
||
s.Match.BuildingHp["blau_ancient"] = s.Match.Map.BuildingDef("blau_ancient").MaxHp * .22;
|
||
s.Game.FocusTarget("base");
|
||
s.Sync();
|
||
yield return Wait(1.2f);
|
||
yield return Shot(s, "11-eigener-ancient-kritisch");
|
||
s.Close();
|
||
s = new HudSession();
|
||
yield return Fresh(s);
|
||
s.Match.RoundNumber = 137;
|
||
foreach (string id in new[] { "rot_top_outer", "rot_top_inner", "rot_base_1", "rot_base_2" })
|
||
s.Match.BuildingHp[id] = 0.0;
|
||
s.Match.BuildingHp["rot_ancient"] = s.Match.Map.BuildingDef("rot_ancient").MaxHp * .18;
|
||
s.Rig.LookAt(s.Match.Map.CorePosition(1), 26f, true);
|
||
s.Sync();
|
||
yield return Wait(1.2f);
|
||
yield return Shot(s, "12-gegnerischer-ancient-kritisch");
|
||
s.Close();
|
||
}
|
||
}
|
||
}
|