Merge luneraria/u4-nachtrag in luneraria/u5.3: Startbildschirm in der Szene

Der U4-Nachtrag (d590fde … dafcaae, aufgesetzt auf U5.2) kommt in den
U5.3-Stand. Einziger Konflikt war G1_Map.unity, weil beide Zweige die Szene
neu gebaut hatten. Die Szene ist mit dem zusammengeführten LuneWorldBuilder
neu gebaut: U5.3-Welt (Nebel .0045, PBR-Sätze) plus LuneNetPlay. Gegenüber
der U5.3-Szene kommt genau eine Komponente hinzu; alle Material-, Mesh- und
Texturverweise sind gleich. LuneWorldBuilder und PORT-QA liefen ohne
Konflikt zusammen.

Prüfung auf dem zusammengeführten Stand: EditMode 29/29, PlayMode 14/14
(StartScreenTests 26/26, Verify 718/0), Fingerabdrücke DCE95C96556F3A1A und
7B8E43AEFC0D60C5 unverändert, macOS-Player-Selbsttest -luneSmoke 26/26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Martin Stickelbroeck 2026-10-08 14:33:22 +02:00
commit c62a1f4df5
31 changed files with 59119 additions and 58534 deletions

View file

@ -76,7 +76,8 @@ namespace Luneraria.EditorTools
rig.Ground = ground; rig.Ground = ground;
rig.Zoom = LuneCameraMath.OverviewZoom; rig.Zoom = LuneCameraMath.OverviewZoom;
// The match on the host and its views; the game controller with the HUD (U3) lets the // The match on the host and its views; the game controller with the HUD (U3) lets the
// human seat play. Tests and the benchmark start bot matches themselves. // human seat play; the start screen (U4) comes first and starts the matches. Tests and
// the benchmark skip it and start bot matches themselves.
var matchObject = new GameObject("Match"); var matchObject = new GameObject("Match");
var driver = matchObject.AddComponent<LuneMatchDriver>(); var driver = matchObject.AddComponent<LuneMatchDriver>();
var view = matchObject.AddComponent<LuneMatchView>(); var view = matchObject.AddComponent<LuneMatchView>();
@ -92,6 +93,12 @@ namespace Luneraria.EditorTools
benchmark.Driver = driver; benchmark.Driver = driver;
benchmark.View = view; benchmark.View = view;
benchmark.Rig = rig; benchmark.Rig = rig;
var netPlay = matchObject.AddComponent<Luneraria.UI.LuneNetPlay>();
netPlay.Game = game;
netPlay.Driver = driver;
netPlay.View = view;
netPlay.Rig = rig;
netPlay.UiAssets = game.UiAssets;
// Meshes of an earlier build that this build no longer produces. // Meshes of an earlier build that this build no longer produces.
foreach (string stale in AssetDatabase.FindAssets("t:Mesh", new[] { GeneratedFolder }).Select(AssetDatabase.GUIDToAssetPath).Where(p => !_written.Contains(p)).ToList()) foreach (string stale in AssetDatabase.FindAssets("t:Mesh", new[] { GeneratedFolder }).Select(AssetDatabase.GUIDToAssetPath).Where(p => !_written.Contains(p)).ToList())
AssetDatabase.DeleteAsset(stale); AssetDatabase.DeleteAsset(stale);

File diff suppressed because it is too large Load diff

View file

@ -28,8 +28,10 @@ namespace Luneraria.UI.Tests
#endif #endif
} }
/// <summary>The G1 scene with its match running at once, without the start screen.</summary>
public static IEnumerator LoadScene() public static IEnumerator LoadScene()
{ {
LuneMatchDriver.DirectStart = true;
AsyncOperation loading = SceneManager.LoadSceneAsync("G1_Map", LoadSceneMode.Single); AsyncOperation loading = SceneManager.LoadSceneAsync("G1_Map", LoadSceneMode.Single);
while (!loading.isDone) while (!loading.isDone)
yield return null; yield return null;

View file

@ -0,0 +1,44 @@
using System.Collections;
using Luneraria.View;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
namespace Luneraria.UI.Tests
{
/// <summary>
/// U4 addendum: the G1 scene starts with the start screen (LuneNetPlay from the scene
/// builder), not with a running match. Runs the player's smoke test in the editor without
/// photos: start screen, offline match into the combat, "Menü" → "Zum Menü", start screen
/// again, a second offline match.
/// </summary>
public sealed class StartScreenTests
{
[UnityTest]
[Timeout(300000)]
public IEnumerator StartScreenLeadsIntoTheMatchAndBack()
{
LunePreferences.ResetSession();
LuneMatchDriver.DirectStart = false;
AsyncOperation loading = SceneManager.LoadSceneAsync("G1_Map", LoadSceneMode.Single);
while (!loading.isDone)
yield return null;
yield return null;
var net = Object.FindAnyObjectByType<LuneNetPlay>();
Assert.IsNotNull(net, "the scene builder puts the start screen into the scene");
Assert.IsTrue(net.enabled && net.Menu != null, "without DirectStart the shell takes over");
Assert.AreSame(net, net.Game.Net, "the game controller knows its shell");
var smoke = net.gameObject.AddComponent<LuneSmoke>();
smoke.Net = net;
smoke.QuitWhenDone = false;
float start = Time.realtimeSinceStartup;
while (!smoke.Done && Time.realtimeSinceStartup - start < 180f)
yield return null;
LuneMatchDriver.DirectStart = true;
Assert.IsTrue(smoke.Done, "the smoke test finished");
Assert.IsEmpty(smoke.Failed, string.Join("\n", smoke.Failed));
Debug.Log("LUNE_START_SCREEN " + smoke.Passed.Count + " checks passed");
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2292db77945fe488c8dd7223630853fb

View file

@ -23,8 +23,10 @@ namespace Luneraria.View.Tests
return Path.Combine(Root, "docs/port", kind, string.IsNullOrEmpty(name) ? milestone : name); return Path.Combine(Root, "docs/port", kind, string.IsNullOrEmpty(name) ? milestone : name);
} }
/// <summary>The G1 scene with its match running at once, without the start screen (U4).</summary>
public static IEnumerator Load() public static IEnumerator Load()
{ {
LuneMatchDriver.DirectStart = true;
AsyncOperation loading = SceneManager.LoadSceneAsync(Scene, LoadSceneMode.Single); AsyncOperation loading = SceneManager.LoadSceneAsync(Scene, LoadSceneMode.Single);
while (!loading.isDone) while (!loading.isDone)
yield return null; yield return null;

View file

@ -31,7 +31,7 @@ namespace Luneraria.UI
public RenderTexture Target; public RenderTexture Target;
/// <summary>Network session (U4); null offline. Commands and Ready go through it.</summary> /// <summary>Network session (U4); null offline. Commands and Ready go through it.</summary>
public LuneSession Session; public LuneSession Session;
/// <summary>Menu/shell that owns the session (U4); null in tests and the benchmark.</summary> /// <summary>Menu/shell that owns the session (U4; in the scene since the U4 addendum); null in tests and the benchmark.</summary>
public LuneNetPlay Net; public LuneNetPlay Net;
public LuneHud Hud { get; private set; } public LuneHud Hud { get; private set; }
@ -75,7 +75,10 @@ namespace Luneraria.UI
} }
FindPads(); FindPads();
Hud = gameObject.GetComponent<LuneHud>() ?? gameObject.AddComponent<LuneHud>(); Hud = gameObject.GetComponent<LuneHud>() ?? gameObject.AddComponent<LuneHud>();
Hud.enabled = true;
Bind(); Bind();
// After a return to the start screen (Close) the HUD shows again.
LuneUi.Show(Hud.Root, true);
LuneSettingsPanel.Apply(); LuneSettingsPanel.Apply();
if (Rig != null) if (Rig != null)
{ {
@ -88,6 +91,27 @@ namespace Luneraria.UI
LuneBenchmark.UiMs = () => (Hud != null ? Hud.LastTickMs : 0.0) + _updateMs + _lateMs; LuneBenchmark.UiMs = () => (Hud != null ? Hud.LastTickMs : 0.0) + _updateMs + _lateMs;
} }
/// <summary>
/// Leaves the match for the start screen (LuneNetPlay.ReturnToMenu; Godot frees the game
/// node): the HUD and the deployment areas disappear and the controller stops until the
/// next Launch.
/// </summary>
public void Close()
{
if (Hud != null && Hud.Root != null)
{
LuneUi.Show(Hud.Root, false);
Hud.enabled = false;
}
foreach (var pad in _pads)
pad.renderer.gameObject.SetActive(false);
foreach (var entry in _grottoRings)
entry.ring.SetActive(false);
_deployment = "";
View.Preview = null;
enabled = false;
}
bool Networked { get { return Session != null && Session.IsNetworked(); } } bool Networked { get { return Session != null && Session.IsNetworked(); } }
bool IsClient { get { return Networked && !Session.IsAuthority(); } } bool IsClient { get { return Networked && !Session.IsAuthority(); } }
@ -635,10 +659,12 @@ namespace Luneraria.UI
_leave.Add(panel); _leave.Add(panel);
VisualElement column = LuneUi.Box(true, 14f, panel); VisualElement column = LuneUi.Box(true, 14f, panel);
column.style.alignItems = Align.Center; column.style.alignItems = Align.Center;
// Without the start screen (tests, benchmark) leaving ends the game.
bool menu = Net != null;
LuneUi.Label("Partie verlassen?", "HeadingLabel", column); LuneUi.Label("Partie verlassen?", "HeadingLabel", column);
LuneUi.Wrap(LuneUi.Label("Zurück zum Hauptmenü mit Offline, Hosten und Beitreten.", "MutedLabel", column)).style.unityTextAlign = TextAnchor.MiddleCenter; LuneUi.Wrap(LuneUi.Label(menu ? "Zurück zum Hauptmenü mit Offline, Hosten und Beitreten." : "Ohne Startbildschirm wird das Spiel beendet.", "MutedLabel", column)).style.unityTextAlign = TextAnchor.MiddleCenter;
VisualElement row = LuneUi.Box(false, 12f, column); VisualElement row = LuneUi.Box(false, 12f, column);
new LuneButton("ZUM MENÜ", LuneButtons.Primary, row).Clicked += () => new LuneButton(menu ? "ZUM MENÜ" : "BEENDEN", LuneButtons.Primary, row).Clicked += () =>
{ {
LuneUi.Show(_leave, false); LuneUi.Show(_leave, false);
if (Net != null) if (Net != null)

View file

@ -25,6 +25,8 @@ namespace Luneraria.UI
readonly TextField _address; readonly TextField _address;
readonly TextField _port; readonly TextField _port;
readonly Label _status; readonly Label _status;
LuneButton _bundGlut;
LuneButton _bundWurzel;
LuneButton _laneTop; LuneButton _laneTop;
LuneButton _laneMid; LuneButton _laneMid;
LuneButton _laneBot; LuneButton _laneBot;
@ -42,7 +44,8 @@ namespace Luneraria.UI
var panel = new LunePanel(LunePanels.Menu); var panel = new LunePanel(LunePanels.Menu);
panel.style.minWidth = 560f; panel.style.minWidth = 560f;
panel.style.maxWidth = 640f; // The four action buttons need about 650 px; at 640 they stuck out of the frame.
panel.style.maxWidth = 760f;
Add(panel); Add(panel);
VisualElement column = LuneUi.Box(true, 14f, panel); VisualElement column = LuneUi.Box(true, 14f, panel);
column.style.alignItems = Align.Center; column.style.alignItems = Align.Center;
@ -54,8 +57,13 @@ namespace Luneraria.UI
LuneUi.Label("DEIN BUND", "SectionLabel", column); LuneUi.Label("DEIN BUND", "SectionLabel", column);
VisualElement bundRow = LuneUi.Box(false, 10f, column); VisualElement bundRow = LuneUi.Box(false, 10f, column);
new LuneButton("Glutpakt", LuneButtons.HudToggle, bundRow).Clicked += () => SetBund("glutpakt"); _bundGlut = new LuneButton("Glutpakt", LuneButtons.HudToggle, bundRow);
new LuneButton("Wurzelbund", LuneButtons.HudToggle, bundRow).Clicked += () => SetBund("wurzelbund"); _bundWurzel = new LuneButton("Wurzelbund", LuneButtons.HudToggle, bundRow);
_bundGlut.ToggleMode = true;
_bundWurzel.ToggleMode = true;
_bundGlut.Clicked += () => SetBund("glutpakt");
_bundWurzel.Clicked += () => SetBund("wurzelbund");
SetBund(Bund);
LuneUi.Label("DEINE LANE", "SectionLabel", column); LuneUi.Label("DEINE LANE", "SectionLabel", column);
VisualElement laneRow = LuneUi.Box(false, 10f, column); VisualElement laneRow = LuneUi.Box(false, 10f, column);
@ -104,6 +112,8 @@ namespace Luneraria.UI
void SetBund(string bund) void SetBund(string bund)
{ {
Bund = bund; Bund = bund;
_bundGlut.On = bund == "glutpakt";
_bundWurzel.On = bund == "wurzelbund";
} }
void SetLane(string lane) void SetLane(string lane)

View file

@ -11,10 +11,16 @@ namespace Luneraria.UI
/// Owns the session and the TCP link (port of lune_main.gd): title menu, match, return, /// Owns the session and the TCP link (port of lune_main.gd): title menu, match, return,
/// rematch. Offline is a local host with bot-filled seats. Host/Join uses LuneSession over /// rematch. Offline is a local host with bot-filled seats. Host/Join uses LuneSession over
/// LuneTcp. Every setup from the host (first join, rejoin, rematch) relaunches the match. /// LuneTcp. Every setup from the host (first join, rejoin, rematch) relaunches the match.
/// The scene builder puts it next to the game controller, so the start screen comes first.
/// Tests and the benchmark keep the scene's own match (LuneMatchDriver.DirectStart,
/// LuneBenchmark.Running); then the shell stays out and LuneGame starts as before U4.
/// </summary> /// </summary>
[DefaultExecutionOrder(-50)] [DefaultExecutionOrder(-50)]
public sealed class LuneNetPlay : MonoBehaviour public sealed class LuneNetPlay : MonoBehaviour
{ {
/// <summary>The start screen draws above the HUD (sorting order 10).</summary>
public const int MenuSortingOrder = 20;
public LuneGame Game; public LuneGame Game;
public LuneMatchDriver Driver; public LuneMatchDriver Driver;
public LuneMatchView View; public LuneMatchView View;
@ -23,11 +29,16 @@ namespace Luneraria.UI
public LuneSession Session { get; private set; } public LuneSession Session { get; private set; }
public LuneMainMenu Menu { get; private set; } public LuneMainMenu Menu { get; private set; }
/// <summary>
/// The start screen's own document. The HUD takes over the UIDocument of the match object
/// and clears it on every launch, so the menu cannot live there.
/// </summary>
public UIDocument MenuDocument { get; private set; }
LuneTcp _tcp; LuneTcp _tcp;
MatchOptions _lastOptions; MatchOptions _lastOptions;
bool _relaunch; bool _relaunch;
UIDocument _document; PanelSettings _panel;
void Awake() void Awake()
{ {
@ -39,34 +50,18 @@ namespace Luneraria.UI
View = GetComponent<LuneMatchView>(); View = GetComponent<LuneMatchView>();
if (Rig == null) if (Rig == null)
Rig = GetComponent<LuneCameraRig>(); Rig = GetComponent<LuneCameraRig>();
_document = GetComponent<UIDocument>(); if (UiAssets == null && Game != null)
UiAssets = Game.UiAssets;
} }
void Start() void Start()
{ {
var root = new VisualElement(); if (LuneMatchDriver.DirectStart || LuneBenchmark.Running)
root.style.position = Position.Absolute;
root.style.left = 0f;
root.style.top = 0f;
root.style.right = 0f;
root.style.bottom = 0f;
if (_document != null && _document.rootVisualElement != null)
{ {
_document.rootVisualElement.Add(root); enabled = false;
return;
} }
else BuildMenu();
{
// Tests and captures build their own panel; a runtime fallback still shows the menu.
root.name = "lune-menu-root";
gameObject.AddComponent<UIDocument>();
}
Menu = new LuneMainMenu();
root.Add(Menu);
Menu.StartOffline += StartOffline;
Menu.HostRequested += HostMatch;
Menu.JoinRequested += JoinMatch;
Menu.QuitRequested += Quit;
Menu.Enter();
if (Game != null) if (Game != null)
{ {
Game.Net = this; Game.Net = this;
@ -74,6 +69,44 @@ namespace Luneraria.UI
} }
if (Driver != null) if (Driver != null)
Driver.enabled = false; Driver.enabled = false;
ShowMenu();
LuneSmoke.StartIfRequested(this);
}
void BuildMenu()
{
LuneUi.Assets = UiAssets;
_panel = ScriptableObject.CreateInstance<PanelSettings>();
_panel.name = "LuneMenuPanel";
_panel.themeStyleSheet = UiAssets != null ? UiAssets.Theme : null;
_panel.scaleMode = PanelScaleMode.ScaleWithScreenSize;
_panel.referenceResolution = LuneHud.Reference;
_panel.screenMatchMode = PanelScreenMatchMode.Expand;
_panel.sortingOrder = MenuSortingOrder;
var holder = new GameObject("Menu");
holder.transform.SetParent(transform, false);
MenuDocument = holder.AddComponent<UIDocument>();
MenuDocument.panelSettings = _panel;
// While a match runs the menu is hidden; its root must not take the HUD's clicks.
MenuDocument.rootVisualElement.pickingMode = PickingMode.Ignore;
Menu = new LuneMainMenu();
MenuDocument.rootVisualElement.Add(Menu);
Menu.StartOffline += StartOffline;
Menu.HostRequested += HostMatch;
Menu.JoinRequested += JoinMatch;
Menu.QuitRequested += Quit;
}
/// <summary>The start screen over the empty map; the camera rests, the fields take W, A, S, D, Q and E.</summary>
void ShowMenu()
{
Menu.Enter();
if (Rig != null)
{
Rig.Interactive = false;
Rig.SetYaw(0f);
Rig.Overview(true);
}
} }
void Update() void Update()
@ -183,6 +216,12 @@ namespace Luneraria.UI
Driver.Restart(); Driver.Restart();
} }
} }
if (Rig != null)
{
// Every match starts with an unturned camera, like Godot's new game node.
Rig.SetYaw(0f, true);
Rig.Interactive = true;
}
Game.enabled = true; Game.enabled = true;
Game.Launch(); Game.Launch();
} }
@ -203,15 +242,23 @@ namespace Luneraria.UI
_relaunch = true; _relaunch = true;
} }
/// <summary>
/// Back to the start screen. Godot frees the game node; here the HUD closes and the driver
/// holds a fresh match that never runs, so the map behind the menu is empty again.
/// </summary>
public void ReturnToMenu() public void ReturnToMenu()
{ {
if (Game != null) if (Game != null)
Game.enabled = false; Game.Close();
if (Driver != null) if (Driver != null)
{
Driver.HumanSlot = -1;
Driver.Restart();
Driver.enabled = false; Driver.enabled = false;
}
ResetSession(); ResetSession();
if (Menu != null) if (Menu != null)
Menu.Enter(); ShowMenu();
} }
void ResetSession() void ResetSession()
@ -231,6 +278,8 @@ namespace Luneraria.UI
void OnDestroy() void OnDestroy()
{ {
ResetSession(); ResetSession();
if (_panel != null)
Destroy(_panel);
} }
static void Quit() static void Quit()

View file

@ -0,0 +1,229 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Luneraria.View;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
/// <summary>
/// Smoke test of a player build through the start screen (after lune_smoke.gd, plus the way
/// back to the menu): start screen, "Allein spielen", planning, Startaufstellung and Bereit,
/// combat, "Menü" → "Zum Menü", start screen again, a second offline match. It presses the
/// real buttons (LuneButton.Click, as Godot's smoke emits `pressed`), photographs each step
/// from the screen, writes report.json and quits with 0 (pass) or 1. Nothing here changes
/// rules or presentation. Run: &lt;player&gt; -luneSmoke &lt;folder&gt;. The PlayMode test runs it
/// in the editor without photos.
/// </summary>
public sealed class LuneSmoke : MonoBehaviour
{
/// <summary>The whole run takes about 20 s; after this the smoke test gives up and reports.</summary>
const float Limit = 120f;
public LuneNetPlay Net;
/// <summary>Folder for the photos and report.json; empty: neither (tests).</summary>
public string Folder = "";
public bool QuitWhenDone = true;
public bool Done { get; private set; }
public readonly List<string> Passed = new List<string>();
public readonly List<string> Failed = new List<string>();
readonly List<string> _photos = new List<string>();
float _started;
/// <summary>Adds the smoke test when the command line asks for it (lune_main.gd: --lune-smoke).</summary>
public static void StartIfRequested(LuneNetPlay net)
{
string[] args = Environment.GetCommandLineArgs();
int at = Array.IndexOf(args, "-luneSmoke");
if (at < 0)
return;
var smoke = net.gameObject.AddComponent<LuneSmoke>();
smoke.Net = net;
smoke.Folder = at + 1 < args.Length && !args[at + 1].StartsWith("-") ? args[at + 1] : Path.Combine(Application.persistentDataPath, "smoke");
// It runs unattended; it must not pause when its window loses focus.
Application.runInBackground = true;
}
IEnumerator Start()
{
_started = Time.realtimeSinceStartup;
LuneGame game = Net.Game;
LuneMatchDriver driver = Net.Driver;
yield return new WaitForSecondsRealtime(1.5f);
// The start screen comes first; the scene's match waits.
Check("Startbildschirm sichtbar", Shown(Net.Menu));
Check("Partie wartet: Spielcontroller und Treiber aus", !game.enabled && !driver.enabled);
Check("vor dem Start kein HUD", game.Hud == null);
LuneButton alone = Button(Net.Menu, "ALLEIN SPIELEN");
Check("„Allein spielen“ bedienbar", Pressable(alone));
yield return Photo("00-startbildschirm");
// Offline starts a match in planning, the own seat on the chosen lane (Mid: seat 1).
LuneMatchRunner waiting = driver.Runner;
alone?.Click();
yield return new WaitForSecondsRealtime(1.5f);
Check("Offline: Startbildschirm verborgen", !Shown(Net.Menu));
Check("Offline: Spielcontroller und Treiber laufen", game.enabled && driver.enabled);
Check("Offline: HUD sichtbar", game.Hud != null && Shown(game.Hud.Root));
Check("Offline: neue Partie, Sitz 1 (Mid)", driver.Runner != waiting && driver.Runner.HumanSlot == 1);
Check("Offline: Planung", driver.Runner.Current == LuneMatchRunner.Stage.Planning);
Check("Offline: lokale Sitzung", Net.Session != null && !Net.Session.IsNetworked());
yield return Photo("01-planung");
// The HUD plays the match: Startaufstellung and Bereit lead into the combat.
LuneButton starter = Button(game.Hud != null ? game.Hud.Root : null, "Startaufstellung");
Check("„Startaufstellung“ bedienbar", Pressable(starter));
int roster = driver.Runner.Match.Players[1].Draft.Roster.Count;
starter?.Click();
yield return new WaitForSecondsRealtime(.5f);
Check("Startaufstellung gekauft (3 Einheiten)", driver.Runner.Match.Players[1].Draft.Roster.Count == roster + 3);
LuneButton ready = Button(game.Hud != null ? game.Hud.Root : null, "BEREIT");
Check("„Bereit“ bedienbar", Pressable(ready));
ready?.Click();
float waited = 0f;
while (driver.Runner.Current != LuneMatchRunner.Stage.Combat && waited < 10f)
{
waited += Time.unscaledDeltaTime;
yield return null;
}
Check("Bereit: Kampf beginnt", driver.Runner.Current == LuneMatchRunner.Stage.Combat);
yield return new WaitForSecondsRealtime(4f);
Check("Kampf: Figuren im Feld", Net.View.Units.Any());
yield return Photo("02-kampf");
// "Menü" asks; "Zum Menü" returns to the start screen over the empty map.
LuneButton menu = Button(game.Hud != null ? game.Hud.Root : null, "Menü");
Check("„Menü“ bedienbar", Pressable(menu));
menu?.Click();
yield return new WaitForSecondsRealtime(.5f);
LuneButton back = Button(game.Hud != null ? game.Hud.Root : null, "ZUM MENÜ");
Check("Verlassen-Dialog mit „Zum Menü“", Pressable(back));
yield return Photo("03-verlassen");
back?.Click();
yield return new WaitForSecondsRealtime(1f);
Check("Zurück: Startbildschirm sichtbar", Shown(Net.Menu));
Check("Zurück: HUD verborgen", game.Hud != null && !Shown(game.Hud.Root));
Check("Zurück: Spielcontroller und Treiber aus", !game.enabled && !driver.enabled);
Check("Zurück: Sitzung beendet", Net.Session == null);
Check("Zurück: keine Figuren im Feld", !Net.View.Units.Any());
Check("Zurück: Kamera ruht", Net.Rig != null && !Net.Rig.Interactive);
alone = Button(Net.Menu, "ALLEIN SPIELEN");
Check("Zurück: „Allein spielen“ bedienbar", Pressable(alone));
yield return Photo("04-zurueck-im-startbildschirm");
// The start screen works again: a second offline match.
waiting = driver.Runner;
alone?.Click();
yield return new WaitForSecondsRealtime(1.5f);
Check("Zweite Partie: Startbildschirm verborgen, HUD sichtbar", !Shown(Net.Menu) && game.Hud != null && Shown(game.Hud.Root));
Check("Zweite Partie: neue Partie in der Planung", driver.Runner != waiting && driver.Runner.Current == LuneMatchRunner.Stage.Planning && driver.enabled && game.enabled);
yield return Photo("05-zweite-partie");
Finish();
}
void Update()
{
// A stuck step (or an exception in the run) must still end with a report.
if (!Done && _started > 0f && Time.realtimeSinceStartup - _started > Limit)
{
Check("Lauf innerhalb von " + Limit + " s beendet", false);
Finish();
}
}
void Check(string name, bool ok)
{
(ok ? Passed : Failed).Add(name);
Debug.Log("LUNE_SMOKE " + (ok ? "ok " : "FEHLER ") + name);
}
/// <summary>Displayed in a panel, with no hidden ancestor.</summary>
static bool Shown(VisualElement element)
{
if (element == null || element.panel == null)
return false;
for (VisualElement node = element; node != null; node = node.parent)
{
if (node.style.display == DisplayStyle.None || node.resolvedStyle.display == DisplayStyle.None)
return false;
}
return true;
}
/// <summary>
/// Shown and enabled; in a player with a screen also laid out and on top at its centre,
/// so a mouse click there reaches it.
/// </summary>
static bool Pressable(LuneButton button)
{
if (button == null || button.Disabled || !Shown(button))
return false;
if (Application.isBatchMode)
return true;
Rect bound = button.worldBound;
if (float.IsNaN(bound.width) || bound.width <= 0f)
return false;
VisualElement picked = button.panel.Pick(bound.center);
return picked != null && (picked == button || button.Contains(picked));
}
/// <summary>The first shown button with this text (icon buttons carry it in a label).</summary>
static LuneButton Button(VisualElement root, string text)
{
if (root == null)
return null;
return root.Query<LuneButton>().ToList().FirstOrDefault(b => Shown(b) && (b.Text == text || b.Query<Label>().ToList().Any(l => l.text == text)));
}
IEnumerator Photo(string label)
{
if (string.IsNullOrEmpty(Folder))
yield break;
yield return new WaitForEndOfFrame();
Texture2D shot = ScreenCapture.CaptureScreenshotAsTexture();
Directory.CreateDirectory(Folder);
string path = Path.Combine(Folder, label + ".jpg");
File.WriteAllBytes(path, shot.EncodeToJPG(92));
Destroy(shot);
_photos.Add(label + ".jpg");
Debug.Log("LUNE_SMOKE_SHOT " + path);
}
void Finish()
{
if (Done)
return;
Done = true;
bool ok = Failed.Count == 0;
var text = new StringBuilder("{\n");
text.AppendFormat(" \"build\": \"{0} player, {1}, {2}\",\n", Application.platform, Application.unityVersion, Debug.isDebugBuild ? "development" : "release");
text.AppendFormat(" \"device\": \"{0}; {1}; {2}\",\n", SystemInfo.deviceModel, SystemInfo.processorType, SystemInfo.graphicsDeviceName);
text.AppendFormat(" \"graphics_api\": \"{0}\",\n \"resolution\": \"{1}x{2}\",\n", SystemInfo.graphicsDeviceType, Screen.width, Screen.height);
text.AppendFormat(CultureInfo.InvariantCulture, " \"seconds\": {0:0.0},\n", Time.realtimeSinceStartup - _started);
text.Append(" \"passed\": [" + string.Join(", ", Passed.Select(Quote)) + "],\n");
text.Append(" \"failed\": [" + string.Join(", ", Failed.Select(Quote)) + "],\n");
text.Append(" \"photos\": [" + string.Join(", ", _photos.Select(Quote)) + "],\n");
text.AppendFormat(" \"result\": \"{0}\"\n}}\n", ok ? "PASS" : "FAIL");
Debug.Log("LUNE_SMOKE_" + (ok ? "PASS" : "FAIL") + " " + Passed.Count + " ok, " + Failed.Count + " Fehler");
if (!string.IsNullOrEmpty(Folder))
{
Directory.CreateDirectory(Folder);
File.WriteAllText(Path.Combine(Folder, "report.json"), text.ToString());
}
if (QuitWhenDone)
Application.Quit(ok ? 0 : 1);
}
static string Quote(string value)
{
return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6935d76d65e3347f4a1a7e44acd9c865

View file

@ -16,6 +16,13 @@ namespace Luneraria.View
/// <summary>Tempo steps of the HUD's tempo button.</summary> /// <summary>Tempo steps of the HUD's tempo button.</summary>
public static readonly int[] SpeedSteps = { 1, 2, 4 }; public static readonly int[] SpeedSteps = { 1, 2, 4 };
/// <summary>
/// True: the scene's match starts at once, without the start screen, as before U4. Tests
/// set it before they load the scene; the benchmark (-luneBenchmark) skips the start screen
/// by itself. False (the game): the start screen (Luneraria.UI.LuneNetPlay) starts matches.
/// </summary>
public static bool DirectStart;
public int Seed = 2; public int Seed = 2;
public int StartRound = 1; public int StartRound = 1;
public int ExtraGold; public int ExtraGold;

View file

@ -36,7 +36,13 @@ Szene neu bauen (nach geänderter Welt, Materialien oder Assets):
/Applications/Unity/Hub/Editor/6000.3.20f1/Unity.app/Contents/MacOS/Unity -batchmode -projectPath "$PWD" -executeMethod Luneraria.EditorTools.LuneWorldBuilder.BuildScene -logFile - /Applications/Unity/Hub/Editor/6000.3.20f1/Unity.app/Contents/MacOS/Unity -batchmode -projectPath "$PWD" -executeMethod Luneraria.EditorTools.LuneWorldBuilder.BuildScene -logFile -
``` ```
Spielen: `Assets/Luneraria/Scenes/G1_Map.unity` im Editor öffnen und Play drücken (Bot-Partie, Kamera mit WASD, Mausrad, Q/E, R, O). Spielen: `Assets/Luneraria/Scenes/G1_Map.unity` im Editor öffnen und Play drücken. Zuerst kommt der Startbildschirm (Allein spielen, Hosten, Beitreten), dann die Partie mit HUD (Kamera mit WASD, Mausrad, Q/E, R, O). „Menü“ führt zurück zum Startbildschirm.
Selbsttest eines Players (Startbildschirm, Partie, Rückkehr; Bilder und `report.json` im Ordner):
```bash
Builds/macOS/Luneraria.app/Contents/MacOS/luneraria-unity -screen-fullscreen 0 -screen-width 1920 -screen-height 1080 -luneSmoke "$PWD/tools/out/smoke"
```
Leistung (.NET bzw. Editor über `-executeMethod Luneraria.Core.Tests.CoreBatch.Benchmark`): Leistung (.NET bzw. Editor über `-executeMethod Luneraria.Core.Tests.CoreBatch.Benchmark`):

View file

@ -59,6 +59,7 @@ Die Szene ändert kein Verhalten. Der Host treibt den Kern im festen Takt; die A
| --- | --- | | --- | --- |
| Transport TCP statt ENet | Der Host rechnet weiter autoritativ; Clients senden nur Absichten. Kampfrahmen laufen auf demselben Socket und werden über die Serial verworfen (wie `unreliable_ordered`). Unter Paketverlust kann TCP Head-of-Line-Blocking erzeugen — rein darstellerisch, kein Regelergebnis. | | Transport TCP statt ENet | Der Host rechnet weiter autoritativ; Clients senden nur Absichten. Kampfrahmen laufen auf demselben Socket und werden über die Serial verworfen (wie `unreliable_ordered`). Unter Paketverlust kann TCP Head-of-Line-Blocking erzeugen — rein darstellerisch, kein Regelergebnis. |
| Mehr-Runden-Loopback im Prozess | `net_rounds.py` startet zwei Godot-Prozesse; `NetRoundsSuite` koppelt zwei `LuneSession` über `LuneLoopback`. Protokoll, Prüfsummen, Übernahme und Wiedereinstieg sind dieselben Pfade. | | Mehr-Runden-Loopback im Prozess | `net_rounds.py` startet zwei Godot-Prozesse; `NetRoundsSuite` koppelt zwei `LuneSession` über `LuneLoopback`. Protokoll, Prüfsummen, Übernahme und Wiedereinstieg sind dieselben Pfade. |
| Rückkehr zum Startbildschirm (U4-Nachtrag) | Godots HUD-Knopf „Menü“ kehrt sofort zurück und gibt das Spielobjekt frei. Der Port fragt vorher (Dialog aus U3), behält die Szene und setzt den Treiber auf eine ruhende neue Partie. In beiden Fällen endet die Sitzung; keine Regel ist betroffen. |
## Technische Repräsentation ## Technische Repräsentation

View file

@ -64,7 +64,7 @@ Hinweis: Die Projektdateien des .NET-Harness (`tools/dotnet/*/*.csproj`) fehlten
| Bild | `screenshots/u3/motive-1920x1080/` (14 G3-Motive), dieselben als JPEG in `motive-1280x720/`, `motive-2560x1080/`, `motive-2560x1440/` (Nachtrag), `pruefung/` (Teile B–F), `aufloesungen/` (1152×720 … 2560×1440), `spieltest/` (Wellen 1–8) | | Bild | `screenshots/u3/motive-1920x1080/` (14 G3-Motive), dieselben als JPEG in `motive-1280x720/`, `motive-2560x1080/`, `motive-2560x1440/` (Nachtrag), `pruefung/` (Teile B–F), `aufloesungen/` (1152×720 … 2560×1440), `spieltest/` (Wellen 1–8) |
| Leistung | macOS-Player mit HUD, 116 Einheiten: 169 fps (Übersicht) und 218 fps (Mid nah). HUD-Skripte **0,090 bzw. 0,078 ms je Bild**; Simulation 0,09/0,04 ms, Ansichten 0,31/0,33 ms. Ausreißer beim Present (p95 10–13 ms) im Rahmen der U2D-Streuung. Rohdaten `logs/u3/player-benchmark-macos.json` | | Leistung | macOS-Player mit HUD, 116 Einheiten: 169 fps (Übersicht) und 218 fps (Mid nah). HUD-Skripte **0,090 bzw. 0,078 ms je Bild**; Simulation 0,09/0,04 ms, Ansichten 0,31/0,33 ms. Ausreißer beim Present (p95 10–13 ms) im Rahmen der U2D-Streuung. Rohdaten `logs/u3/player-benchmark-macos.json` |
| Bekannte Unterschiede | Godots deklarierte Schriftgewichte werden in der Referenz nie angewendet (String-Schlüssel „wght“); Port zeigt wie G3 Regular 400. Kein Hauptmenü, kein Grottenlicht. Details [U3-UI.md](U3-UI.md) und [KNOWN-BEHAVIOR-DIFFERENCES.md](KNOWN-BEHAVIOR-DIFFERENCES.md) | | Bekannte Unterschiede | Godots deklarierte Schriftgewichte werden in der Referenz nie angewendet (String-Schlüssel „wght“); Port zeigt wie G3 Regular 400. Kein Hauptmenü, kein Grottenlicht. Details [U3-UI.md](U3-UI.md) und [KNOWN-BEHAVIOR-DIFFERENCES.md](KNOWN-BEHAVIOR-DIFFERENCES.md) |
| Nachtrag (7. Oktober 2026) | Spieltest über die echte Oberfläche: Wellen 1–8 gespielt, Ende in der Planung von Welle 9 (`logs/u3/playtest-log.txt`). Tooltip-Wertzeilen ohne Umbruch wie Godots `GridContainer`. Die im U3D-Commit genannten Motivsätze in drei weiteren Auflösungen nachgeholt (vom U3-Stand `e2aadcb`). Befund für U4: Startbildschirm nicht in der Szene eingebunden | | Nachtrag (7. Oktober 2026) | Spieltest über die echte Oberfläche: Wellen 1–8 gespielt, Ende in der Planung von Welle 9 (`logs/u3/playtest-log.txt`). Tooltip-Wertzeilen ohne Umbruch wie Godots `GridContainer`. Die im U3D-Commit genannten Motivsätze in drei weiteren Auflösungen nachgeholt (vom U3-Stand `e2aadcb`). Befund für U4: Startbildschirm nicht in der Szene eingebunden (behoben im U4-Nachtrag) |
| Offen | Hauptmenü und Netzwerk (U4); Schriftgewichte, Grottenlicht und weitere Darstellung (U5); Windows-/Linux-Player (Build-Module) | | Offen | Hauptmenü und Netzwerk (U4); Schriftgewichte, Grottenlicht und weitere Darstellung (U5); Windows-/Linux-Player (Build-Module) |
| Commits | `luneraria/u3`: U3A `4dee9b1`, U3B/U3C `2b86c0d`, U3D `66867f3`; Nachtrag `luneraria/u3-nachtrag` = `527f7c5` (auf U5.2) | | Commits | `luneraria/u3`: U3A `4dee9b1`, U3B/U3C `2b86c0d`, U3D `66867f3`; Nachtrag `luneraria/u3-nachtrag` = `527f7c5` (auf U5.2) |
@ -74,11 +74,12 @@ Hinweis: Die Projektdateien des .NET-Harness (`tools/dotnet/*/*.csproj`) fehlten
| --- | --- | | --- | --- |
| Tests | Protokoll `NetSuite` **43/43** (Port von `test_lune_net.gd`), Mehr-Runden-Loopback `NetRoundsSuite` **12/12** (Port von `net_rounds.py`, In-Prozess mit `LuneLoopback`), `LuneWireTests` Round-trip. EditMode 29/29, PlayMode 13/13, Kerntests .NET 457/457 (402 + 43 + 12) | | Tests | Protokoll `NetSuite` **43/43** (Port von `test_lune_net.gd`), Mehr-Runden-Loopback `NetRoundsSuite` **12/12** (Port von `net_rounds.py`, In-Prozess mit `LuneLoopback`), `LuneWireTests` Round-trip. EditMode 29/29, PlayMode 13/13, Kerntests .NET 457/457 (402 + 43 + 12) |
| Parität | Bot-Partien unverändert `DCE95C96556F3A1A`, `7B8E43AEFC0D60C5`; Mindestumfang M4 erfüllt (Ready, private Planung, Bot-Übernahme, Wiedereinstieg, Snapshots, Revanche, Disconnect, sechs Plätze) | | Parität | Bot-Partien unverändert `DCE95C96556F3A1A`, `7B8E43AEFC0D60C5`; Mindestumfang M4 erfüllt (Ready, private Planung, Bot-Übernahme, Wiedereinstieg, Snapshots, Revanche, Disconnect, sechs Plätze) |
| Bild | Startbildschirm `LuneMainMenu` (Offline/Hosten/Beitreten); Partiefenster unverändert U3 | | Bild | Startbildschirm `LuneMainMenu` (Offline/Hosten/Beitreten), sichtbar erst seit dem Nachtrag: `screenshots/u4/startbildschirm/` (macOS-Player: Startbildschirm, Planung, Kampf, Verlassen-Dialog, zurück im Startbildschirm, zweite Partie); Partiefenster unverändert U3 |
| Leistung | keine eigenen Netz-Messungen; Rahmen 20/s wie in der Referenz (`LuneNet.FrameInterval` 0,05 s), 13 Floats je Einheit | | Leistung | keine eigenen Netz-Messungen; Rahmen 20/s wie in der Referenz (`LuneNet.FrameInterval` 0,05 s), 13 Floats je Einheit |
| Bekannte Unterschiede | TCP statt ENet (Rahmen auf demselben Socket, Verwerfen über Serial); Zwei-Prozess-Loopback als In-Prozess-Test; vereinfachtes Hauptmenü | | Bekannte Unterschiede | TCP statt ENet (Rahmen auf demselben Socket, Verwerfen über Serial); Zwei-Prozess-Loopback als In-Prozess-Test; vereinfachtes Hauptmenü; „Menü“ fragt vor der Rückkehr (Godot kehrt sofort zurück) |
| Nachtrag (7. Oktober 2026) | Startbildschirm in der Szene: `LuneWorldBuilder` legt `LuneNetPlay` an, das Menü hat eine eigene Fläche über dem HUD, „Menü“ → „Zum Menü“ führt zum Startbildschirm zurück. Tests und Benchmark ohne Startbildschirm (`LuneMatchDriver.DirectStart`, `-luneBenchmark`). EditMode 29/29, PlayMode 14/14 mit neuem `StartScreenTests` (26/26), Verify 718/0, Fingerabdrücke unverändert. macOS-Player-Selbsttest `-luneSmoke` 26/26 (`logs/u4/smoke-macos.json`, Bilder `screenshots/u4/startbildschirm/`). Bestätigungslauf am 8. Oktober auf dem Endstand bei offenem Mac: EditMode 29/29, PlayMode 14/14 in einem Zug. Leistung A/B gegen U5.2 (`1b31f9b`), je drei Läufe abwechselnd: Übersicht 111,0–113,9 zu 112,5–114,5 fps, Mid nah 145,2–148,9 zu 146,7–150,0 fps, HUD 0,09–0,11 ms. Die Mittelwerte liegen 0,3 bzw. 1,1 % auseinander, innerhalb der Streuung; kein messbarer Unterschied (`logs/u4/benchmark-ab/`). Details [U4-NETWORK.md](U4-NETWORK.md#nachtrag-startbildschirm-in-der-szene-7-oktober-2026) |
| Offen | Windows-/Linux-Player (Build-Module); Internetpartie über mehrere Rechner; optional unzuverlässiger Rahmenkanal | | Offen | Windows-/Linux-Player (Build-Module); Internetpartie über mehrere Rechner; optional unzuverlässiger Rahmenkanal |
| Commits | `luneraria/u4` `769e622` | | Commits | `luneraria/u4` `769e622`; Nachtrag `luneraria/u4-nachtrag` = `d590fde` (auf U5.2), Bestätigung und Benchmark im Commit danach |
## U5 · Visuelle Neuausrichtung (Vertical Slice) ## U5 · Visuelle Neuausrichtung (Vertical Slice)

View file

@ -140,7 +140,7 @@ Die Heldenkarte hat einen reservierten Befehlsplatz für die Ultimates der gepla
- Ein Linksklick im Feld wählt eine eigene Figur (innerhalb von 34 px). - Ein Linksklick im Feld wählt eine eigene Figur (innerhalb von 34 px).
- **Planung:** Die eigene Entwurfsarmee steht in ihrer Formation. Die eigenen Aufstellungsflächen leuchten; Lanes mit eigenen Einheiten sind hervorgehoben. - **Planung:** Die eigene Entwurfsarmee steht in ihrer Formation. Die eigenen Aufstellungsflächen leuchten; Lanes mit eigenen Einheiten sind hervorgehoben.
- **Optionen:** Sie pausieren einen laufenden Kampf offline. - **Optionen:** Sie pausieren einen laufenden Kampf offline.
- **Menü:** Ein Hauptmenü gibt es im Unity-Port noch nicht. „Menü“ fragt deshalb nach und beendet das Spiel. - **Menü:** „Menü“ fragt nach und kehrt zum Startbildschirm zurück (seit dem U4-Nachtrag, [U4-NETWORK.md](U4-NETWORK.md#nachtrag-startbildschirm-in-der-szene-7-oktober-2026)). Bis dahin fehlte der Startbildschirm in der Szene, und „Menü“ beendete das Spiel.
Bot-Partien (Tests, Benchmark) laufen unverändert; der Spielcontroller folgt dem Sitz der laufenden Partie. In der Szene ersetzt der Spielcontroller die U2-Debug-Anzeige. Bot-Partien (Tests, Benchmark) laufen unverändert; der Spielcontroller folgt dem Sitz der laufenden Partie. In der Szene ersetzt der Spielcontroller die U2-Debug-Anzeige.
@ -207,7 +207,7 @@ Befunde:
| Werte in Tooltip-Zeilen brachen um („7.500 / 7.500 (100“ und „%)“ in zwei Zeilen). Jede Wertzeile hatte eigene Spalten, und die Breite war auf 300 px begrenzt | Tooltip legt Wertzeilen wie Godots `GridContainer` an: drei Spalten (Symbol, Name, Wert), ohne Umbruch, die breiteste Zeile bestimmt die Breite. Nur Text, Hinweis und Warnung brechen bei 300 px um | | Werte in Tooltip-Zeilen brachen um („7.500 / 7.500 (100“ und „%)“ in zwei Zeilen). Jede Wertzeile hatte eigene Spalten, und die Breite war auf 300 px begrenzt | Tooltip legt Wertzeilen wie Godots `GridContainer` an: drei Spalten (Symbol, Name, Wert), ohne Umbruch, die breiteste Zeile bestimmt die Breite. Nur Text, Hinweis und Warnung brechen bei 300 px um |
| Die Heldenkarte öffnet bei „TALENT!“ den Reiter Werte | wie in G3 (`open_hero`); der Reiter „TALENTE ★“ zeigt die offene Wahl. Unverändert | | Die Heldenkarte öffnet bei „TALENT!“ den Reiter Werte | wie in G3 (`open_hero`); der Reiter „TALENTE ★“ zeigt die offene Wahl. Unverändert |
| Das Namensschild des Helden liegt im Getümmel über anderen Figuren | lesbar, wie in G3. Unverändert | | Das Namensschild des Helden liegt im Getümmel über anderen Figuren | lesbar, wie in G3. Unverändert |
| Der Startbildschirm aus U4 erscheint nicht: Nichts legt `LuneNetPlay` in die Szene. Der Verlassen-Dialog verspricht das Hauptmenü, beendet aber das Spiel | gehört zu U4, als eigene Aufgabe vorgemerkt | | Der Startbildschirm aus U4 erscheint nicht: Nichts legt `LuneNetPlay` in die Szene. Der Verlassen-Dialog verspricht das Hauptmenü, beendet aber das Spiel | gehört zu U4; behoben im U4-Nachtrag ([U4-NETWORK.md](U4-NETWORK.md#nachtrag-startbildschirm-in-der-szene-7-oktober-2026)) |
Bilder: `screenshots/u3/spieltest/` (Lane-Tooltip, Karten-Tooltip, Einheitendetails, Lane umkämpft, Held anwerben und angeworben, Items zu teuer, Held im Feld, Einsatz Bottom, Kampf Bottom, Talentwahl, Champion gebucht, Ankündigung Welle 8, Kampf Mid in Welle 8, Planung Welle 9). Bilder: `screenshots/u3/spieltest/` (Lane-Tooltip, Karten-Tooltip, Einheitendetails, Lane umkämpft, Held anwerben und angeworben, Items zu teuer, Held im Feld, Einsatz Bottom, Kampf Bottom, Talentwahl, Champion gebucht, Ankündigung Welle 8, Kampf Mid in Welle 8, Planung Welle 9).
@ -262,7 +262,7 @@ Der Vergleich der 14 Motive mit der Referenz brachte Darstellungen hervor, die z
### Bekannte Grenzen ### Bekannte Grenzen
- **Kein Hauptmenü** im Port; „Menü“ beendet das Spiel nach Rückfrage. Das Menü mit Netzwerk-Host/Join gehört zu U4 (dort gebaut, aber noch nicht in die Szene eingebunden, siehe Spieltest). - **Hauptmenü:** Der Startbildschirm mit Netzwerk-Host/Join gehört zu U4. Seit dem U4-Nachtrag ist er in der Szene; das Spiel beginnt dort, und „Menü“ kehrt nach Rückfrage dorthin zurück. Zur Zeit von U3 und des Spieltests fehlte er.
- **Grottenlicht** (Runen, Kristalle, Funken) fehlt; nur die ruhende Schlundkrone steht dort. - **Grottenlicht** (Runen, Kristalle, Funken) fehlt; nur die ruhende Schlundkrone steht dort.
- **Geplante Helden** behalten ihren reservierten Befehlsplatz, solange keiner dieser Helden spielbar ist. - **Geplante Helden** behalten ihren reservierten Befehlsplatz, solange keiner dieser Helden spielbar ist.
- **Windows- und Linux-Player** weiterhin an die fehlenden Build-Module gebunden ([U1.5-UNITY.md](U1.5-UNITY.md)). - **Windows- und Linux-Player** weiterhin an die fehlenden Build-Module gebunden ([U1.5-UNITY.md](U1.5-UNITY.md)).

View file

@ -1,6 +1,6 @@
# U4 · Netzwerk # U4 · Netzwerk
Stand: 2. Oktober 2026 · **fertig** (`luneraria/u4` = `769e622`). Stand: 2. Oktober 2026 · **fertig** (`luneraria/u4` = `769e622`). Nachtrag vom 7. Oktober 2026: Startbildschirm in der Szene ([unten](#nachtrag-startbildschirm-in-der-szene-7-oktober-2026)).
Es wurde keine neue Architektur erfunden. Übertragen ist der M4-Umfang (`lune_session.gd`, Protokoll 4), serverautoritativ: Es wurde keine neue Architektur erfunden. Übertragen ist der M4-Umfang (`lune_session.gd`, Protokoll 4), serverautoritativ:
- Clients senden Absichten. - Clients senden Absichten.
@ -16,7 +16,8 @@ Es wurde keine neue Architektur erfunden. Übertragen ist der M4-Umfang (`lune_s
| `LuneWire` | `Assets/Luneraria/Net/LuneWire.cs` | Codierung der Nachrichten (Art + Payload, Cursor-Codec) und der Kampfrahmen | | `LuneWire` | `Assets/Luneraria/Net/LuneWire.cs` | Codierung der Nachrichten (Art + Payload, Cursor-Codec) und der Kampfrahmen |
| `LuneTcp` | `Assets/Luneraria/Net/LuneTcp.cs` | TCP-Transport (Ersatz für ENet): ein Socket je Gast, zuverlässige Nachrichten und Rahmen, Pump auf dem Spielthread | | `LuneTcp` | `Assets/Luneraria/Net/LuneTcp.cs` | TCP-Transport (Ersatz für ENet): ein Socket je Gast, zuverlässige Nachrichten und Rahmen, Pump auf dem Spielthread |
| `LuneMainMenu` | `Assets/Luneraria/UI/Game/LuneMainMenu.cs` | Startbildschirm: Allein spielen, Hosten, Beitreten, Bund, Lane, Name, Adresse, Port | | `LuneMainMenu` | `Assets/Luneraria/UI/Game/LuneMainMenu.cs` | Startbildschirm: Allein spielen, Hosten, Beitreten, Bund, Lane, Name, Adresse, Port |
| `LuneNetPlay` | `Assets/Luneraria/UI/Game/LuneNetPlay.cs` | Schale um Sitzung und Transport (Port von `lune_main.gd`): Menü, Partie, Rückkehr, Revanche | | `LuneNetPlay` | `Assets/Luneraria/UI/Game/LuneNetPlay.cs` | Schale um Sitzung und Transport (Port von `lune_main.gd`): Menü, Partie, Rückkehr, Revanche. Seit dem Nachtrag legt `LuneWorldBuilder` sie in die Szene |
| `LuneSmoke` | `Assets/Luneraria/UI/Game/LuneSmoke.cs` | Selbsttest eines Players über den Startbildschirm (nach `lune_smoke.gd`), `-luneSmoke <Ordner>`; Nachtrag |
| `LuneGame` | `Assets/Luneraria/UI/Game/LuneGame.cs` | Befehle, Rückgängig und Bereit gehen im Netzspiel über die Sitzung; Gast zeigt nur Host-Rahmen | | `LuneGame` | `Assets/Luneraria/UI/Game/LuneGame.cs` | Befehle, Rückgängig und Bereit gehen im Netzspiel über die Sitzung; Gast zeigt nur Host-Rahmen |
Offline ist ein lokaler Host mit Bot-Plätzen (`LuneSession.HostOffline`). Hosten und Beitreten nutzen `LuneTcp` mit `LuneWire`. Peer 1 ist der Host; freie Plätze bleiben Bots. Offline ist ein lokaler Host mit Bot-Plätzen (`LuneSession.HostOffline`). Hosten und Beitreten nutzen `LuneTcp` mit `LuneWire`. Peer 1 ist der Host; freie Plätze bleiben Bots.
@ -72,10 +73,60 @@ Das Loopback der Mehr-Runden-Partie läuft im Prozess mit zwei `LuneSession`-Ins
| Mindestumfang: Ready, private Planung, Bot-Übernahme, Wiedereinstieg, Snapshots, Revanche, Disconnect, sechs Plätze | erfüllt | | Mindestumfang: Ready, private Planung, Bot-Übernahme, Wiedereinstieg, Snapshots, Revanche, Disconnect, sechs Plätze | erfüllt |
| Tests 43 + 12 | erfüllt | | Tests 43 + 12 | erfüllt |
## Nachtrag: Startbildschirm in der Szene (7. Oktober 2026)
Zweig `luneraria/u4-nachtrag` (`d590fde`), aufgesetzt auf `luneraria/u5.2` (`1b31f9b`, mit dem U3-Nachtrag).
**Befund** aus dem Spieltest des U3-Nachtrags: Nichts legte `LuneNetPlay` in die Szene. `G1_Map.unity` enthielt die Komponente nicht, `LuneWorldBuilder` fügte sie nicht hinzu, und es gab keinen Bootstrap. Das Spiel begann deshalb direkt in einer Partie (`LuneGame.Start` startet sie, wenn `Net` fehlt), und der Startbildschirm war unerreichbar. Der Verlassen-Dialog versprach das Hauptmenü, beendete aber das Spiel.
Beim Einbinden zeigten sich zwei weitere Fehler. Sie hätten den Startbildschirm auch mit der Komponente in der Szene verborgen:
- `LuneNetPlay.Start` legte ein `UIDocument` ohne `PanelSettings` an und hängte das Menü an kein Panel.
- Das HUD übernimmt das `UIDocument` des Match-Objekts und leert es bei jedem Partiestart (`LuneHud.Init`). Ein Menü dort wäre mit der ersten Partie verschwunden.
**Behebung:**
| Teil | Änderung |
| --- | --- |
| Szene | `LuneWorldBuilder` legt `LuneNetPlay` auf das Match-Objekt neben `LuneGame`, `LuneMatchDriver`, `LuneMatchView` und `LuneBenchmark`, mit Verweisen auf Spielcontroller, Treiber, Ansicht, Kamera und UI-Assets. Szene neu gebaut |
| Menüfläche | eigenes `UIDocument` (Objekt „Menu“) mit eigenen `PanelSettings`, über dem HUD (Sortierung 20 über 10). Die Wurzel nimmt keine Klicks, ein verborgenes Menü lässt das HUD frei |
| Start | Spielcontroller und Treiber warten. Die Kamera steht in der Übersicht still, denn die Felder Name und Adresse nehmen W, A, S, D, Q und E |
| Partie | „Allein spielen“, Hosten und Beitreten wie in U4. Jede Partie beginnt mit ungedrehter Kamera; Godot legt dafür je Partie ein neues Spielobjekt an |
| Rückkehr | „Menü“ → „Zum Menü“: HUD und Aufstellungsflächen verschwinden, der Spielcontroller ruht, die Sitzung endet. Der Treiber hält eine neue Partie, die nie läuft; die Karte ist leer wie beim Start |
| Bund-Wahl | Glutpakt und Wurzelbund zeigen die Wahl wie die Lane-Knöpfe; vorher fehlte die Markierung |
| Menübreite | Die vier Aktionsknöpfe brauchen etwa 650 px und ragten bei höchstens 640 px über den Rahmen (erstes Player-Bild); die Tafel darf jetzt 760 px breit werden |
| Verlassen-Dialog | mit Startbildschirm „ZUM MENÜ“; ohne ihn (Tests, Benchmark) sagt der Dialog, dass das Spiel endet, und der Knopf heißt „BEENDEN“ |
**Tests und Benchmark ohne Startbildschirm:** `LuneMatchDriver.DirectStart` (die beiden Szenenlader der Tests setzen ihn) und `-luneBenchmark` (`LuneBenchmark.Running`) halten `LuneNetPlay` heraus. Dann startet `LuneGame` die Partie der Szene wie vor U4, und die Fingerabdrücke der Bot-Partien bleiben gleich.
**Selbsttest im Player** (nach `lune_smoke.gd`, ergänzt um die Rückkehr): `<Player> -luneSmoke <Ordner>` drückt die echten Knöpfe (`LuneButton.Click`; Godots Selbsttest löst `pressed` aus) und prüft im Player auch, dass jeder Knopf in seiner Mitte zuoberst liegt. Jeder Schritt wird vom Bildschirm fotografiert. Am Ende schreibt der Test `report.json` und beendet sich mit 0 (bestanden) oder 1. Ablauf: Startbildschirm → „Allein spielen“ → Planung → „Startaufstellung“ und „Bereit“ → Kampf → „Menü“ → „Zum Menü“ → Startbildschirm → zweite Partie.
**Prüfung:**
| Prüfung | Ergebnis |
| --- | --- |
| `StartScreenTests` (PlayMode, neu) | 26/26 Prüfungen im Editor (Selbsttest ohne Bilder) |
| macOS-Player, `-luneSmoke` | 26/26 in 10,8 s (Release, Metal, 1920×1080). Bilder `screenshots/u4/startbildschirm/`, Bericht `logs/u4/smoke-macos.json`, Protokoll `logs/u4/smoke-macos-log.txt`. Im Bestätigungslauf mit neu gebautem Player wieder 26/26 |
| EditMode | 29/29 |
| PlayMode | 14/14 (13 + `StartScreenTests`), Verify A–I 718/0. Bestätigungslauf auf dem Endstand (`acf2572`) am 8. Oktober in einem Zug, 307 s |
| Fingerabdrücke der Bot-Partien | Seed 2 `DCE95C96556F3A1A` (12.690 Schritte), Seed 9 ab Runde 21 `7B8E43AEFC0D60C5` (4.195 Schritte): gleich dem Kern und unverändert, in der Szene auch bei 30, 60, 120 fps und unbegrenzt |
| Benchmark `-luneBenchmark` | `LuneNetPlay` bleibt aus, die Bot-Partie bleibt (Seed 9 ab Runde 21, bis 116 Einheiten). A/B gegen U5.2 unter gleichen Bedingungen, siehe unten |
**Leistung (A/B):** Je drei Benchmark-Läufe abwechselnd mit einem U5.2-Player (`1b31f9b`, ohne Nachtrag) und dem Nachtrag-Player (`acf2572`); zuvor ein verworfener Aufwärmlauf, weil macOS neue Programme beim ersten Start prüft. macOS-Player, Release, Metal, 1920×1080, VSync aus, Mac M5 bei normaler Desktop-Nutzung. Rohdaten `logs/u4/benchmark-ab/`.
| Player | Übersicht (Zoom 64) | Mid nah (Zoom 22) | HUD-Skripte je Bild | Einheiten |
| --- | --- | --- | --- | --- |
| U5.2 `1b31f9b` | 112,5 / 114,5 / 112,9 fps | 146,7 / 150,0 / 147,7 fps | 0,088–0,103 ms | 116 / 63 |
| U4-Nachtrag `acf2572` | 111,0 / 113,9 / 113,9 fps | 145,2 / 148,9 / 145,4 fps | 0,090–0,109 ms | 116 / 63 |
Die Mittelwerte liegen 0,3 % (Übersicht) und 1,1 % (Mid nah) auseinander. Das ist weniger als die Streuung zwischen den Läufen eines Players (bis 2,6 %). Im Benchmark baut `LuneNetPlay` kein Menü und hat keine Bildarbeit. Beide Player liegen unter den U5.2-Zahlen vom 2. Oktober (133 / 180 fps). Der Abstand trifft beide gleich und hängt an der Umgebung. Heute brauchen beide Player in der Übersicht länger als damals: Hauptthread 3,6–3,7 statt 3,1 ms, Render-Thread 4,2–4,5 statt 3,7 ms, Present-Wartezeit 5,3 statt 4,4 ms.
Die nächtlichen Wiederholungsläufe vom 7. auf den 8. Oktober zählen nicht. Der Mac war zugeklappt („Thermal Emergency Sleep“ um 23:35) und wachte nur alle 15 Minuten kurz für Wartung auf. Dabei rissen Echtzeitgrenzen: Bildratenläufe über 600 s, der 120-s-Wächter des Selbsttests und eine Kameraprüfung 0,6 s nach einem Lane-Klick. Der Bestätigungslauf bei geöffnetem Mac ersetzt sie.
## Bekannte Grenzen ## Bekannte Grenzen
- **Transport:** TCP statt ENet. Kampfrahmen laufen auf demselben Socket und werden über die Serial verworfen (wie `unreliable_ordered`). Unter Paketverlust kann TCP Head-of-Line-Blocking erzeugen; für eine Internetpartie wäre ein unzuverlässiger Kanal wie bei ENet der nächste Schritt. - **Transport:** TCP statt ENet. Kampfrahmen laufen auf demselben Socket und werden über die Serial verworfen (wie `unreliable_ordered`). Unter Paketverlust kann TCP Head-of-Line-Blocking erzeugen; für eine Internetpartie wäre ein unzuverlässiger Kanal wie bei ENet der nächste Schritt.
- **Zwei-Prozess-Loopback** (`net_rounds.py`) ist als In-Prozess-Test umgesetzt (`NetRoundsSuite`). Ein Sockets-Lauf über zwei Player ist optional. - **Zwei-Prozess-Loopback** (`net_rounds.py`) ist als In-Prozess-Test umgesetzt (`NetRoundsSuite`). Ein Sockets-Lauf über zwei Player ist optional.
- **Windows auf x64-Hardware** und Netz über mehrere Rechner sind nicht gemessen (Build-Module fehlen, siehe [U1.5-UNITY.md](U1.5-UNITY.md)). - **Windows auf x64-Hardware** und Netz über mehrere Rechner sind nicht gemessen (Build-Module fehlen, siehe [U1.5-UNITY.md](U1.5-UNITY.md)).
- **Hauptmenü** ist der Startbildschirm mit Offline/Hosten/Beitreten; das erweiterte Menü der Referenz (Emblem, Version, erweiterte Optionen) ist vereinfacht. - **Hauptmenü** ist der Startbildschirm mit Offline/Hosten/Beitreten; das erweiterte Menü der Referenz (Emblem, Version, erweiterte Optionen, 3D-Bühne) ist vereinfacht. Bis zum Nachtrag vom 7. Oktober 2026 war er nicht in der Szene. F1, M und F11 wirken erst in der Partie; in der Referenz auch im Menü.
- **„Menü“ im HUD** fragt vor der Rückkehr (Dialog aus U3); Godot kehrt sofort zurück.
- **Kein Audio-Netzwerk** und keine Lobby-Chat-Nachrichten (waren nicht Teil des M4-Mindestumfangs). - **Kein Audio-Netzwerk** und keine Lobby-Chat-Nachrichten (waren nicht Teil des M4-Mindestumfangs).

View file

@ -0,0 +1,14 @@
{
"build": "OSXPlayer player, 6000.3.20f1, release",
"device": "Mac17,2; Apple M5; Apple M5",
"graphics_api": "Metal",
"resolution": "1920x1080",
"vsync": 0,
"match": "seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat",
"windows": [
{"view": "overview (zoom 64)", "frames": 2222, "fps": 111.0, "frame_ms_avg": 9.01, "frame_ms_median": 6.31, "frame_ms_p95": 19.48, "frame_ms_p99": 24.20, "frame_ms_max": 37.92, "frames_over_16_7_ms": 208, "of_them_with_simulation_step": 18, "gc_collections": 27, "simulation_ms_per_frame": 0.150, "views_ms_per_frame": 0.312, "ui_ms_per_frame": 0.109, "units_alive_max": 116},
// timings overview (zoom 64): main thread avg 3.71 / median 3.57 / p95 5.19 ms, render thread avg 4.47 / median 4.42 / p95 5.36 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 5.30 / median 2.24 / p95 15.99 ms
{"view": "mid lane close (zoom 22)", "frames": 1454, "fps": 145.2, "frame_ms_avg": 6.88, "frame_ms_median": 4.89, "frame_ms_p95": 16.19, "frame_ms_p99": 20.28, "frame_ms_max": 28.97, "frames_over_16_7_ms": 53, "of_them_with_simulation_step": 8, "gc_collections": 10, "simulation_ms_per_frame": 0.068, "views_ms_per_frame": 0.395, "ui_ms_per_frame": 0.096, "units_alive_max": 63}
// timings mid lane close (zoom 22): main thread avg 3.01 / median 2.92 / p95 4.20 ms, render thread avg 3.53 / median 3.49 / p95 4.17 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 3.88 / median 1.75 / p95 13.37 ms
]
}

View file

@ -0,0 +1,14 @@
{
"build": "OSXPlayer player, 6000.3.20f1, release",
"device": "Mac17,2; Apple M5; Apple M5",
"graphics_api": "Metal",
"resolution": "1920x1080",
"vsync": 0,
"match": "seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat",
"windows": [
{"view": "overview (zoom 64)", "frames": 2278, "fps": 113.9, "frame_ms_avg": 8.78, "frame_ms_median": 5.91, "frame_ms_p95": 20.29, "frame_ms_p99": 24.14, "frame_ms_max": 39.11, "frames_over_16_7_ms": 279, "of_them_with_simulation_step": 31, "gc_collections": 28, "simulation_ms_per_frame": 0.137, "views_ms_per_frame": 0.296, "ui_ms_per_frame": 0.107, "units_alive_max": 116},
// timings overview (zoom 64): main thread avg 3.47 / median 3.35 / p95 4.84 ms, render thread avg 4.14 / median 4.11 / p95 4.84 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 5.31 / median 2.08 / p95 17.17 ms
{"view": "mid lane close (zoom 22)", "frames": 1489, "fps": 148.9, "frame_ms_avg": 6.72, "frame_ms_median": 4.69, "frame_ms_p95": 16.57, "frame_ms_p99": 20.87, "frame_ms_max": 27.87, "frames_over_16_7_ms": 68, "of_them_with_simulation_step": 9, "gc_collections": 9, "simulation_ms_per_frame": 0.067, "views_ms_per_frame": 0.369, "ui_ms_per_frame": 0.090, "units_alive_max": 63}
// timings mid lane close (zoom 22): main thread avg 2.91 / median 2.81 / p95 4.15 ms, render thread avg 3.40 / median 3.34 / p95 4.10 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 3.80 / median 1.66 / p95 13.95 ms
]
}

View file

@ -0,0 +1,14 @@
{
"build": "OSXPlayer player, 6000.3.20f1, release",
"device": "Mac17,2; Apple M5; Apple M5",
"graphics_api": "Metal",
"resolution": "1920x1080",
"vsync": 0,
"match": "seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat",
"windows": [
{"view": "overview (zoom 64)", "frames": 2278, "fps": 113.9, "frame_ms_avg": 8.78, "frame_ms_median": 5.90, "frame_ms_p95": 19.84, "frame_ms_p99": 24.07, "frame_ms_max": 31.30, "frames_over_16_7_ms": 226, "of_them_with_simulation_step": 31, "gc_collections": 27, "simulation_ms_per_frame": 0.133, "views_ms_per_frame": 0.289, "ui_ms_per_frame": 0.100, "units_alive_max": 116},
// timings overview (zoom 64): main thread avg 3.45 / median 3.34 / p95 4.85 ms, render thread avg 4.11 / median 4.09 / p95 4.88 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 5.33 / median 2.05 / p95 16.59 ms
{"view": "mid lane close (zoom 22)", "frames": 1455, "fps": 145.4, "frame_ms_avg": 6.88, "frame_ms_median": 4.93, "frame_ms_p95": 16.39, "frame_ms_p99": 19.99, "frame_ms_max": 31.61, "frames_over_16_7_ms": 54, "of_them_with_simulation_step": 2, "gc_collections": 10, "simulation_ms_per_frame": 0.066, "views_ms_per_frame": 0.385, "ui_ms_per_frame": 0.094, "units_alive_max": 63}
// timings mid lane close (zoom 22): main thread avg 3.08 / median 2.93 / p95 4.60 ms, render thread avg 3.54 / median 3.47 / p95 4.41 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 3.81 / median 1.84 / p95 13.45 ms
]
}

View file

@ -0,0 +1,14 @@
{
"build": "OSXPlayer player, 6000.3.20f1, release",
"device": "Mac17,2; Apple M5; Apple M5",
"graphics_api": "Metal",
"resolution": "1920x1080",
"vsync": 0,
"match": "seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat",
"windows": [
{"view": "overview (zoom 64)", "frames": 2251, "fps": 112.5, "frame_ms_avg": 8.89, "frame_ms_median": 6.09, "frame_ms_p95": 19.80, "frame_ms_p99": 24.20, "frame_ms_max": 32.50, "frames_over_16_7_ms": 200, "of_them_with_simulation_step": 27, "gc_collections": 27, "simulation_ms_per_frame": 0.139, "views_ms_per_frame": 0.297, "ui_ms_per_frame": 0.103, "units_alive_max": 116},
// timings overview (zoom 64): main thread avg 3.61 / median 3.46 / p95 5.21 ms, render thread avg 4.23 / median 4.18 / p95 5.10 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 5.27 / median 2.11 / p95 16.38 ms
{"view": "mid lane close (zoom 22)", "frames": 1469, "fps": 146.7, "frame_ms_avg": 6.82, "frame_ms_median": 4.80, "frame_ms_p95": 16.30, "frame_ms_p99": 20.28, "frame_ms_max": 28.26, "frames_over_16_7_ms": 46, "of_them_with_simulation_step": 7, "gc_collections": 10, "simulation_ms_per_frame": 0.071, "views_ms_per_frame": 0.384, "ui_ms_per_frame": 0.092, "units_alive_max": 63}
// timings mid lane close (zoom 22): main thread avg 2.98 / median 2.88 / p95 4.23 ms, render thread avg 3.46 / median 3.39 / p95 4.25 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 3.82 / median 1.73 / p95 13.50 ms
]
}

View file

@ -0,0 +1,14 @@
{
"build": "OSXPlayer player, 6000.3.20f1, release",
"device": "Mac17,2; Apple M5; Apple M5",
"graphics_api": "Metal",
"resolution": "1920x1080",
"vsync": 0,
"match": "seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat",
"windows": [
{"view": "overview (zoom 64)", "frames": 2290, "fps": 114.5, "frame_ms_avg": 8.73, "frame_ms_median": 5.86, "frame_ms_p95": 20.25, "frame_ms_p99": 24.29, "frame_ms_max": 30.15, "frames_over_16_7_ms": 276, "of_them_with_simulation_step": 36, "gc_collections": 27, "simulation_ms_per_frame": 0.135, "views_ms_per_frame": 0.284, "ui_ms_per_frame": 0.098, "units_alive_max": 116},
// timings overview (zoom 64): main thread avg 3.39 / median 3.27 / p95 4.75 ms, render thread avg 4.08 / median 4.07 / p95 4.77 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 5.34 / median 2.08 / p95 16.96 ms
{"view": "mid lane close (zoom 22)", "frames": 1500, "fps": 150.0, "frame_ms_avg": 6.67, "frame_ms_median": 4.62, "frame_ms_p95": 16.47, "frame_ms_p99": 20.54, "frame_ms_max": 24.49, "frames_over_16_7_ms": 60, "of_them_with_simulation_step": 6, "gc_collections": 10, "simulation_ms_per_frame": 0.067, "views_ms_per_frame": 0.369, "ui_ms_per_frame": 0.088, "units_alive_max": 63}
// timings mid lane close (zoom 22): main thread avg 2.80 / median 2.74 / p95 3.83 ms, render thread avg 3.34 / median 3.30 / p95 3.89 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 3.88 / median 1.74 / p95 13.78 ms
]
}

View file

@ -0,0 +1,14 @@
{
"build": "OSXPlayer player, 6000.3.20f1, release",
"device": "Mac17,2; Apple M5; Apple M5",
"graphics_api": "Metal",
"resolution": "1920x1080",
"vsync": 0,
"match": "seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat",
"windows": [
{"view": "overview (zoom 64)", "frames": 2259, "fps": 112.9, "frame_ms_avg": 8.85, "frame_ms_median": 6.02, "frame_ms_p95": 20.30, "frame_ms_p99": 24.38, "frame_ms_max": 31.61, "frames_over_16_7_ms": 286, "of_them_with_simulation_step": 24, "gc_collections": 27, "simulation_ms_per_frame": 0.131, "views_ms_per_frame": 0.286, "ui_ms_per_frame": 0.102, "units_alive_max": 116},
// timings overview (zoom 64): main thread avg 3.57 / median 3.39 / p95 5.10 ms, render thread avg 4.20 / median 4.17 / p95 4.99 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 5.29 / median 2.09 / p95 16.93 ms
{"view": "mid lane close (zoom 22)", "frames": 1477, "fps": 147.7, "frame_ms_avg": 6.77, "frame_ms_median": 4.79, "frame_ms_p95": 16.34, "frame_ms_p99": 20.67, "frame_ms_max": 24.58, "frames_over_16_7_ms": 59, "of_them_with_simulation_step": 6, "gc_collections": 10, "simulation_ms_per_frame": 0.067, "views_ms_per_frame": 0.366, "ui_ms_per_frame": 0.088, "units_alive_max": 63}
// timings mid lane close (zoom 22): main thread avg 2.89 / median 2.78 / p95 4.24 ms, render thread avg 3.44 / median 3.39 / p95 4.15 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 3.87 / median 1.78 / p95 13.70 ms
]
}

View file

@ -0,0 +1,33 @@
LUNE_SMOKE ok Startbildschirm sichtbar
LUNE_SMOKE ok Partie wartet: Spielcontroller und Treiber aus
LUNE_SMOKE ok vor dem Start kein HUD
LUNE_SMOKE ok „Allein spielen“ bedienbar
LUNE_SMOKE_SHOT 00-startbildschirm.jpg
LUNE_SMOKE ok Offline: Startbildschirm verborgen
LUNE_SMOKE ok Offline: Spielcontroller und Treiber laufen
LUNE_SMOKE ok Offline: HUD sichtbar
LUNE_SMOKE ok Offline: neue Partie, Sitz 1 (Mid)
LUNE_SMOKE ok Offline: Planung
LUNE_SMOKE ok Offline: lokale Sitzung
LUNE_SMOKE_SHOT 01-planung.jpg
LUNE_SMOKE ok „Startaufstellung“ bedienbar
LUNE_SMOKE ok Startaufstellung gekauft (3 Einheiten)
LUNE_SMOKE ok „Bereit“ bedienbar
LUNE_SMOKE ok Bereit: Kampf beginnt
LUNE_SMOKE ok Kampf: Figuren im Feld
LUNE_SMOKE_SHOT 02-kampf.jpg
LUNE_SMOKE ok „Menü“ bedienbar
LUNE_SMOKE ok Verlassen-Dialog mit „Zum Menü“
LUNE_SMOKE_SHOT 03-verlassen.jpg
LUNE_SMOKE ok Zurück: Startbildschirm sichtbar
LUNE_SMOKE ok Zurück: HUD verborgen
LUNE_SMOKE ok Zurück: Spielcontroller und Treiber aus
LUNE_SMOKE ok Zurück: Sitzung beendet
LUNE_SMOKE ok Zurück: keine Figuren im Feld
LUNE_SMOKE ok Zurück: Kamera ruht
LUNE_SMOKE ok Zurück: „Allein spielen“ bedienbar
LUNE_SMOKE_SHOT 04-zurueck-im-startbildschirm.jpg
LUNE_SMOKE ok Zweite Partie: Startbildschirm verborgen, HUD sichtbar
LUNE_SMOKE ok Zweite Partie: neue Partie in der Planung
LUNE_SMOKE_SHOT 05-zweite-partie.jpg
LUNE_SMOKE_PASS 26 ok, 0 Fehler

View file

@ -0,0 +1,11 @@
{
"build": "OSXPlayer player, 6000.3.20f1, release",
"device": "Mac17,2; Apple M5; Apple M5",
"graphics_api": "Metal",
"resolution": "1920x1080",
"seconds": 10.8,
"passed": ["Startbildschirm sichtbar", "Partie wartet: Spielcontroller und Treiber aus", "vor dem Start kein HUD", "„Allein spielen“ bedienbar", "Offline: Startbildschirm verborgen", "Offline: Spielcontroller und Treiber laufen", "Offline: HUD sichtbar", "Offline: neue Partie, Sitz 1 (Mid)", "Offline: Planung", "Offline: lokale Sitzung", "„Startaufstellung“ bedienbar", "Startaufstellung gekauft (3 Einheiten)", "„Bereit“ bedienbar", "Bereit: Kampf beginnt", "Kampf: Figuren im Feld", "„Menü“ bedienbar", "Verlassen-Dialog mit „Zum Menü“", "Zurück: Startbildschirm sichtbar", "Zurück: HUD verborgen", "Zurück: Spielcontroller und Treiber aus", "Zurück: Sitzung beendet", "Zurück: keine Figuren im Feld", "Zurück: Kamera ruht", "Zurück: „Allein spielen“ bedienbar", "Zweite Partie: Startbildschirm verborgen, HUD sichtbar", "Zweite Partie: neue Partie in der Planung"],
"failed": [],
"photos": ["00-startbildschirm.jpg", "01-planung.jpg", "02-kampf.jpg", "03-verlassen.jpg", "04-zurueck-im-startbildschirm.jpg", "05-zweite-partie.jpg"],
"result": "PASS"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB