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 { /// /// 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: <player> -luneSmoke <folder>. The PlayMode test runs it /// in the editor without photos. /// public sealed class LuneSmoke : MonoBehaviour { /// The whole run takes about 20 s; after this the smoke test gives up and reports. const float Limit = 120f; public LuneNetPlay Net; /// Folder for the photos and report.json; empty: neither (tests). public string Folder = ""; public bool QuitWhenDone = true; public bool Done { get; private set; } public readonly List Passed = new List(); public readonly List Failed = new List(); readonly List _photos = new List(); float _started; /// Adds the smoke test when the command line asks for it (lune_main.gd: --lune-smoke). 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(); 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); } /// Displayed in a panel, with no hidden ancestor. 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; } /// /// 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. /// 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)); } /// The first shown button with this text (icon buttons carry it in a label). static LuneButton Button(VisualElement root, string text) { if (root == null) return null; return root.Query().ToList().FirstOrDefault(b => Shown(b) && (b.Text == text || b.Query