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