Die Figuren (Einheiten, Monster, Völker, Helden) bekommen den Materialweg der Welt; dazu Umgebungsverdeckung für die ganze Szene. - Surface-Materialien für alle 146 Figurenmaterialien (142 neu unter Materials/Units/, die LN_Boss_* gehörten schon zu den Gebäuden). Farbe, Rauigkeit und Emission bleiben die Blender-Werte; Metalle werden metallisch (vorher war keines der 414 Figurenmaterialien metallisch), ein Randlicht hebt die Silhouetten im Draufblick vom Boden ab. - Oberflächenstruktur je Materialfamilie im eigenen Raum der Figur, nur in den HUD-Porträts. Im Feld (30-60 px hohe Figuren) war sie nicht zu sehen und kostete rund 10 % Bildrate. - Ringborkes 4k-Satz liegt am Schnittholz (RB_Cutwood), wohin er gehört; U5.3 hatte ihn an die Rinde gebunden. Verwaistes RB_Bark.mat entfernt. - SSAO über LuneProjectSetup: nach den opaken Objekten, halbe Auflösung, Radius 0,35 m, Ausblendung ab 260 m. Standardmäßig an (Entscheidung des Projektinhabers), in den Optionen abschaltbar, -luneSsao 0 für eine Sitzung. Die Variante mit Tiefen-Normalen-Vorpass kostete 19-28 %. - Shader: Steinreliefkarte nur noch für Gelände, Stein, Pflaster, Erde. - Werkzeug: Übersichtsbilder mit LUNE_DRESS=1 eingekleidet wie im Spiel; das Materialset wird nach der leeren Vorschauszene geladen (sie entlädt unreferenzierte Assets). - Tests: EditMode 29/29, PlayMode 14/14, Verify 718/0, Player-Selbsttest 30/30 (4 neue zur SSAO-Option), Fingerabdrücke unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). - Leistung (A/B mit U5.3 im selben Lauf): Mid nah 177,7 -> 148,5 fps (-16 %), Karte 127,8 -> 111,5 fps (-13 %); ohne SSAO -3,5 / -4,5 %. - Doku: U5-VISUAL (U5.4), PORT-STATUS, PORT-QA; Bilder screenshots/u5.4/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
252 lines
13 KiB
C#
252 lines
13 KiB
C#
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. Since U5.4 it also flips the SSAO option twice (player only; the
|
|
/// stored choice ends as it was). Run: <player> -luneSmoke <folder>. 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");
|
|
|
|
// U5.4: the option "Umgebungsverdeckung" switches the renderer's SSAO (player only; in
|
|
// the editor the renderer is a project asset and stays as it is).
|
|
if (!Application.isEditor && game.Hud != null)
|
|
{
|
|
UnityEngine.Rendering.Universal.ScriptableRendererFeature ssao = LuneSettingsPanel.AmbientOcclusionFeature();
|
|
bool wanted = LunePreferences.AmbientOcclusion;
|
|
Check("SSAO folgt der Einstellung", ssao != null && ssao.isActive == wanted);
|
|
game.Hud.Settings.Show();
|
|
yield return new WaitForSecondsRealtime(.5f);
|
|
LuneSwitch option = game.Hud.Settings.Query<LuneSwitch>().ToList().FirstOrDefault(o => Shown(o) && o.Text == "Umgebungsverdeckung (SSAO)");
|
|
Check("Option „Umgebungsverdeckung“ sichtbar", option != null);
|
|
yield return Photo("06-optionen");
|
|
option?.Click();
|
|
yield return null;
|
|
Check("Option schaltet SSAO um", ssao != null && ssao.isActive == !wanted && LunePreferences.AmbientOcclusion == !wanted);
|
|
option?.Click();
|
|
yield return null;
|
|
Check("Option stellt SSAO zurück", ssao != null && ssao.isActive == wanted && LunePreferences.AmbientOcclusion == wanted);
|
|
game.Hud.Settings.Close();
|
|
yield return new WaitForSecondsRealtime(.3f);
|
|
}
|
|
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("\"", "\\\"") + "\"";
|
|
}
|
|
}
|
|
}
|