Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion Assets/Tests/PlayMode/FieldReservePickTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,36 @@ public IEnumerator ClickOnStartField_SelectsTheField()
bootstrap.StartGrayboxMatch();
Assert.IsTrue(bootstrap.IsMatchReady, "match did not start");

// Two frames: let every Awake/Start and the first model rebuilds settle.
// Two frames: let every Awake/Start and the first model rebuilds
// settle. MainMenuController.Start is among them, and it is the
// one that matters below.
yield return null;
yield return null;

MonoBehaviour input = FindByTypeName("RtsDeviceInput");
Assert.NotNull(input, "scene contains no RtsDeviceInput");

// THE COCKPIT HAS TO BE SWITCHED ON, and this test has to do it
// itself. Since package 21.8 the whole HUD root is inactive while
// the main menu owns the screen (#102), and the thing that turns
// it back on is MainMenuController.StartMatch — which this test
// deliberately bypasses by driving MatchBootstrap directly. Left
// off, RtsDeviceInput never runs a single Update, never binds its
// dispatcher, and every pick path below dereferences null.
//
// The OFF assertion keeps this line honest: it must run AFTER the
// two frames above, because the switch-off happens in
// MainMenuController.Start, not during scene activation — asserted
// one frame earlier it reads the scene file's default and passes
// for the wrong reason.
Assert.IsFalse(input.gameObject.activeInHierarchy,
"the HUD root is expected to be off while the main menu owns the screen — if this " +
"fails, the menu/match switch moved and the SetActive below is papering over it");
input.gameObject.SetActive(true);

// One more frame so the freshly enabled input binds its dispatcher.
yield return null;

// The serialized scene predates the 21.2 field: a missing YAML
// entry must materialise the C# default 2f — pin that assumption,
// it is exactly the kind of silent zero a scene upgrade swallows.
Expand Down
30 changes: 19 additions & 11 deletions Assets/Tests/PlayMode/MainMenuTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ namespace Nova.PlayMode.Tests
/// rank-4 assemblies, and quality/scripts/run_gate_check.py:183-188 forbids
/// any test assembly from referencing one — Nova.Presentation.Tests was
/// dissolved for exactly that reason in 5cdb0ce. The menu's contract with
/// those three components (it switches two of them off while it is up and
/// back on when the match starts) is still worth a test, so they are found
/// by type name. A name that no longer resolves fails loudly here; the
/// alternative was not testing the behaviour at all.
/// those components (it switches the camera rig off while it is up, and
/// the whole gameplay HUD ROOT with it — the one GameObject every
/// in-match HUD component, DebugHud included, lives on) is still worth a
/// test, so they are found by type name. A name that no longer resolves
/// fails loudly here; the alternative was not testing the behaviour at
/// all.
/// </para>
/// <para>
/// Run headless-with-graphics, never with -quit (same lane as
Expand Down Expand Up @@ -135,9 +137,14 @@ public IEnumerator BootstrapScene_OpensInTheMainMenuWithNoMatchRunning()
"this scene with no 'no match yet' guard: its LateUpdate reads scroll wheel, MMB, " +
"Z/X and the screen-edge pan every frame, so a pointer resting near an edge would " +
"pan the camera away from the HQ before the match even starts.");
Assert.IsFalse(DebugHudBehaviour().enabled,
"the debug HUD must be silent while the menu is up: its always-on status bar draws " +
"BEFORE its own F3 visibility check, so it would sit on top of the key art");
Behaviour debugHud = DebugHudBehaviour();
Assert.IsFalse(debugHud.gameObject.activeInHierarchy,
"the gameplay HUD root must be OFF while the menu is up: the menu switches the one " +
"GameObject every in-match HUD component lives on (MainMenuController.SetGameplayLayerActive), " +
"so the whole cockpit — including the debug HUD's always-on status bar, which draws " +
"BEFORE its own F3 visibility check — stays silent instead of sitting on the key art. " +
"The check goes through activeInHierarchy because the switch is the root's SetActive, " +
"not a per-component enabled toggle.");
}

// ------------------------------------------------------------------
Expand Down Expand Up @@ -213,11 +220,12 @@ public IEnumerator NewGame_StartsTheMatchHidesTheOverlayAndWakesTheCockpit()
Assert.IsTrue(CameraRig().enabled,
"the RTS camera must take over as the match starts; it was switched off only for " +
"as long as the menu was up");
Assert.IsTrue(DebugHudBehaviour().enabled,
"the status bar belongs to the cockpit and must come back with the match");
Assert.IsTrue(DebugHudBehaviour().gameObject.activeInHierarchy,
"the gameplay HUD root must come back with the match — the status bar and the whole " +
"cockpit ride on the one root object the menu switches");
Assert.IsFalse(MenuController().enabled,
"the menu switches itself off after starting: this sprint has no pause menu and no " +
"restart, so there is deliberately no way back into it");
"the menu switches itself off after starting; the way back in is a match-end or " +
"pause-menu button (ReturnToMenu re-enables it), not the start click itself");

uint tickAtStart = runner.Session.CurrentTick;
yield return new WaitForSeconds(2f);
Expand Down
96 changes: 96 additions & 0 deletions Assets/Tests/PlayMode/PauseMenuTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using Nova.Gameplay;

namespace Nova.PlayMode.Tests
{
/// <summary>
/// The two traps of package 21.8, pinned where they actually bite.
/// <para>
/// WHY THIS FILE NAMES THE TYPE AS A STRING: Nova.PlayMode.Tests
/// references Nova.Gameplay but NOT Nova.Presentation.UI, so
/// <c>PauseMenuHud</c> cannot be named as a type here — the same reason
/// <c>MainMenuTests</c> reaches for <c>DebugHud</c> that way.
/// <c>ModalSurfaceLink</c> IS reachable: it lives in Nova.Gameplay
/// precisely so both Presentation assemblies and this one can see it.
/// </para>
/// </summary>
public sealed class PauseMenuTests
{
private const string ScenePath = "Assets/_Project/Scenes/Bootstrap.unity";
private const string PauseMenuTypeName = "PauseMenuHud";
private const string DebugHudTypeName = "DebugHud";

/// <summary>
/// The pause menu is IN THE SCENE, on the switched root, and the modal
/// channel is clean while the main menu owns the screen.
/// <para>
/// THE SCENE IS THE POINT OF THIS TEST. Bootstrap.unity is machine
/// output that is committed, and it went stale once already: it sat
/// unchanged from 2026-08-08 while the generator moved three times, so
/// components wired only in the generator were simply absent from the
/// running game — code merged, CI green, nothing on screen. Without
/// this assertion the whole of 21.8 can ship and do nothing.
/// </para>
/// <para>
/// THE CLEAN CHANNEL IS THE SECOND POINT. <c>ModalSurfaceLink</c> is a
/// per-frame verdict published by the pause menu, and the way to the
/// main menu switches the HUD root — writer included — OFF. A writer
/// that stopped publishing while its last word was <c>true</c> would
/// leave every world gesture suspended for the rest of the session:
/// no selection, no orders, no camera edge-pan, and no component still
/// running that could ever clear it. That is why the writer resets in
/// OnDisable, and this is the assertion that keeps it there.
/// </para>
/// </summary>
[UnityTest]
public IEnumerator PauseMenu_IsWiredOntoTheHudRootAndLeavesNoStaleModalFlag()
{
yield return SceneManager.LoadSceneAsync(ScenePath, LoadSceneMode.Single);
yield return null;
yield return null;

Behaviour pauseMenu = RequireBehaviour(PauseMenuTypeName,
"the UI object carries no pause menu, so ESC does nothing and a player cannot leave " +
"a running match (#105). The scene is machine output — run " +
"Tools/Project Nova/Create Bootstrap Scene after any change to " +
"BootstrapSceneGenerator.CreateUiObject.");

Behaviour debugHud = RequireBehaviour(DebugHudTypeName,
"the UI object carries no debug HUD");

Assert.AreSame(debugHud.gameObject, pauseMenu.gameObject,
"the pause menu must live on the same GameObject as the rest of the cockpit: that " +
"root IS the menu/match switch (MainMenuController.SetGameplayLayerActive). A pause " +
"menu beside it would keep drawing over the main menu — which is exactly the defect " +
"(#102) this package exists to end.");

Assert.IsFalse(pauseMenu.gameObject.activeInHierarchy,
"the HUD root must be OFF while the main menu owns the screen");

Assert.IsFalse(ModalSurfaceLink.Open,
"no modal may be claimed while the main menu is up. This is the deadlock guard: the " +
"channel is a per-frame verdict whose only writer sits on the root that was just " +
"switched off, so a last word of 'true' would suspend every world gesture for the " +
"rest of the session with nothing left running to clear it.");
}

private static Behaviour RequireBehaviour(string typeName, string message)
{
// FindObjectsInactive.Include is load-bearing: the menu switches
// the HUD root off, and everything on it counts as inactive.
Behaviour[] all = Object.FindObjectsByType<Behaviour>(FindObjectsInactive.Include);
for (int i = 0; i < all.Length; i++)
{
if (all[i].GetType().Name == typeName) return all[i];
}

Assert.Fail($"{message} (looked for a component named '{typeName}'). If it was renamed, " +
"this file has to follow — it cannot reference the type.");
return null;
}
}
}
2 changes: 2 additions & 0 deletions Assets/Tests/PlayMode/PauseMenuTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 34 additions & 13 deletions Assets/_Project/Editor/BootstrapSceneGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,17 @@ private static void EnsureGlutrinneMapAsset()

/// <summary>
/// Input sampling, the world-space HUD markers (selection, rally
/// flags, placement ghost), the build bar, the command card and the
/// flags, placement ghost), the build bar, the command card, the
/// match frame (result/network panels), the pause menu and the
/// read-only debug overlay. The build bar and the command card are
/// additionally wired INTO the input component: clicks landing on a
/// HUD rect belong to the HUD and must not start a world selection
/// drag, place a building or resolve an order pick behind it.
/// drag, place a building or resolve an order pick behind it. The
/// pause menu is wired into the input's counterpart direction: it
/// reads the gesture-cancel stamp so ESC peels one layer at a time.
/// Everything lands on ONE GameObject so the main menu can silence
/// the whole cockpit with a single root switch — a component added
/// here is covered by that switch from the day it lands.
/// </summary>
private static GameObject CreateUiObject(MatchRunner runner, Camera camera)
{
Expand Down Expand Up @@ -375,6 +381,13 @@ private static GameObject CreateUiObject(MatchRunner runner, Camera camera)
// _menu is wired in CreateMainMenuObject — the menu object does
// not exist yet at this point in the generation order.

PauseMenuHud pauseMenu = uiObject.AddComponent<PauseMenuHud>();
WireReference(pauseMenu, "_runner", runner);
WireReference(pauseMenu, "_matchFrame", frame);
WireReference(pauseMenu, "_input", input);
// _menu is wired in CreateMainMenuObject — same ordering reason
// as the match frame above.

DebugHud hud = uiObject.AddComponent<DebugHud>();
WireReference(hud, "_runner", runner);
WireReference(hud, "_input", input);
Expand All @@ -389,15 +402,17 @@ private static GameObject CreateUiObject(MatchRunner runner, Camera camera)
/// CreateMatchObject) and MainMenuController starts the match from
/// "Neues Spiel".
/// <para>
/// The camera rig and the debug HUD are wired INTO the menu because
/// they are the only two components in this scene without a "no match
/// yet" guard: the rig would edge-pan and zoom while the player moves
/// the pointer over the menu, and the HUD's always-on status bar draws
/// before its own visibility check. The menu switches both off while
/// it is up. On the receiving side the rig is typed as Behaviour
/// (Nova.Presentation.UI may not reference Nova.Presentation), which
/// WireReference handles — it assigns object references, not typed
/// fields.
/// TWO things are wired INTO the menu because they are the scene's
/// only parts without a "no match yet" guard: the camera rig (it
/// would edge-pan and zoom while the player moves the pointer over
/// the menu) and the gameplay HUD ROOT — this one GameObject carries
/// every in-match HUD component, so the menu silences the whole
/// cockpit (including the debug HUD's always-on status bar, which
/// draws before its own visibility check) with a single root switch
/// that can never rot the way a component catalogue would. On the
/// receiving side the rig is typed as Behaviour (Nova.Presentation.UI
/// may not reference Nova.Presentation), which WireReference handles
/// — it assigns object references, not typed fields.
/// </para>
/// </summary>
private static void CreateMainMenuObject(
Expand Down Expand Up @@ -430,20 +445,26 @@ private static void CreateMainMenuObject(
WireReference(menu, "_bootstrap", runner.GetComponent<MatchBootstrap>());
WireReference(menu, "_music", music);
WireReference(menu, "_cameraRig", camera.GetComponent<RtsCameraController>());
WireReference(menu, "_debugHud", uiObject.GetComponent<DebugHud>());
WireReference(menu, "_gameplayHudRoot", uiObject);
WireReference(menu, "_keyArt",
MenuAssetSetup.LoadRequired<Texture2D>(MenuAssetSetup.KeyArtPath));
WireReference(menu, "_titleFont",
MenuAssetSetup.LoadRequired<Font>(MenuAssetSetup.TitleFontPath));
WireReference(menu, "_bodyFont",
MenuAssetSetup.LoadRequired<Font>(MenuAssetSetup.BodyFontPath));

// The match frame's "Hauptmenü" button calls back into this menu.
// The match frame's "Hauptmenü" and the pause menu's "Zum
// Hauptmenü"/"Spiel beenden" call back into this menu.
MatchFrameHud frame = uiObject.GetComponent<MatchFrameHud>();
if (frame != null)
{
WireReference(frame, "_menu", menu);
}
PauseMenuHud pauseMenu = uiObject.GetComponent<PauseMenuHud>();
if (pauseMenu != null)
{
WireReference(pauseMenu, "_menu", menu);
}

CreateIngameMusicObject(runner, menu, musicGroup);
}
Expand Down
Loading
Loading