NPCStandIn was written to be dropped on a sphere and carried its identity in per-instance public fields. The cost of that showed up in the scene itself: of the two spheres, one was the Bartender and the other was named "Bartender (1)" in the hierarchy while being configured as the Chair — data changed, name never was. Nothing outside the scene could see what existed, and two copies could disagree. Identity now lives in an InteractableDefinition asset and the scene only places a prefab. The migration matched the old objects by yarn node rather than by name, which is the only reason the chair ended up as the chair. InteractableDefinition rather than the planned "character definition": it covers a character and a talkable object equally, so Bartender.prefab sits under Prefabs/Characters and Chair.prefab under Prefabs/Interactables. Three components collapse into one. DialogueInteractable was in no scene at all — a dead trigger-zone variant of the same idea. ClickInteractableBridge existed only to adapt NPCStandIn without modifying it, a constraint that died with NPCStandIn; DialogueInteractor implements IClickInteractable itself. The parts worth keeping were lifted out rather than discarded: WorldSpaceLabel and InteractionPrompt are now normal components on prefab children, so a label can be seen and positioned in the Editor instead of existing only at runtime. InputDeviceTracker moved into NightclubArcadia.Core and gained a namespace, leaving no global-namespace runtime types. Characters get the same pipeline as Skills and Candidates: Assets/Characters/characters.json is the source of truth, CharacterSetup regenerates the definition assets on script reload and removes orphans. Adding an NPC is a JSON entry plus a prefab — no code, and no Unity for the data half. Phase 5.4 deletes the one-shot builders rather than disabling them. UILayerSetup, PlayerControlSetup, UILayerSetupAutoRun and YarnDemoSetup assumed the single-scene layout, had already destroyed a level scene and turned Systems binary once each, and after NPCStandIn went they no longer compiled. The prefabs they used to generate are authored and committed; the wiring knowledge is in those prefabs and in git history. The asset generators — Skill, Candidate, Character — are untouched and still work. EditMode 42/42, PlayMode 7/7, YarnCheck 9 files / 35 nodes. All three scenes still text. Co-Authored-By: Claude Opus 5 <[email protected]>
135 lines
5.3 KiB
C#
135 lines
5.3 KiB
C#
using System.Collections;
|
|
using System.Linq;
|
|
using NUnit.Framework;
|
|
using NightclubArcadia.Cinematics;
|
|
using NightclubArcadia.Interaction;
|
|
using Unity.Cinemachine;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
using Yarn.Unity;
|
|
|
|
namespace NightclubArcadia.PlayMode.Tests
|
|
{
|
|
/// <summary>
|
|
/// Proves the scene split actually comes up.
|
|
///
|
|
/// Splitting Systems out of the level nulled seven serialized references that
|
|
/// Unity cannot carry across a scene boundary — the two NPCs' dialogue runner, UI
|
|
/// and player fields, and the reveal camera's tracking target. Each is supposed to
|
|
/// be resolved at runtime instead. An EditMode test cannot see any of that, because
|
|
/// none of it has run. This can.
|
|
///
|
|
/// If this suite fails, the game does not start. Treat it that way.
|
|
/// </summary>
|
|
public class BootstrapTests
|
|
{
|
|
const string Bootstrap = "Bootstrap";
|
|
const string Systems = "Systems";
|
|
const string Level = "SC101_ConferenceHall";
|
|
|
|
[UnitySetUp]
|
|
public IEnumerator LoadFromBootstrap()
|
|
{
|
|
yield return SceneManager.LoadSceneAsync(Bootstrap, LoadSceneMode.Single);
|
|
|
|
// GameBootstrap loads the other two additively over a few frames.
|
|
var deadline = Time.realtimeSinceStartup + 20f;
|
|
while (Time.realtimeSinceStartup < deadline)
|
|
{
|
|
if (SceneManager.GetSceneByName(Systems).isLoaded &&
|
|
SceneManager.GetSceneByName(Level).isLoaded)
|
|
{
|
|
break;
|
|
}
|
|
yield return null;
|
|
}
|
|
|
|
// let Awake/Start run on everything that just loaded
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Bootstrap_LoadsSystemsAndLevel()
|
|
{
|
|
Assert.IsTrue(SceneManager.GetSceneByName(Systems).isLoaded, "Systems scene did not load");
|
|
Assert.IsTrue(SceneManager.GetSceneByName(Level).isLoaded, "Level scene did not load");
|
|
Assert.AreEqual(Level, SceneManager.GetActiveScene().name,
|
|
"The level should be the active scene, so new objects and lighting land there");
|
|
}
|
|
|
|
[Test]
|
|
public void ThePlayerExists_AndIsTagged()
|
|
{
|
|
var player = GameObject.FindGameObjectWithTag("Player");
|
|
Assert.IsNotNull(player, "No object tagged Player — the reveal camera and the NPCs both need it");
|
|
}
|
|
|
|
[Test]
|
|
public void TheDialogueRunnerExists()
|
|
{
|
|
Assert.IsNotNull(Object.FindFirstObjectByType<DialogueRunner>(),
|
|
"No DialogueRunner in any loaded scene");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regression: "Can't start dialogue: no Yarn Project has been configured."
|
|
///
|
|
/// Asserting the runner merely exists is not enough — it auto-starts on Start(),
|
|
/// and a runner with no project throws that at load with nothing else wrong.
|
|
/// The project is a serialized asset reference supplied by a prefab override, so
|
|
/// it breaks silently if the override is ever dropped.
|
|
/// </summary>
|
|
[Test]
|
|
public void TheDialogueRunner_HasItsYarnProject()
|
|
{
|
|
var runners = Object.FindObjectsByType<DialogueRunner>(
|
|
FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
|
|
Assert.AreEqual(1, runners.Length,
|
|
"Expected exactly one DialogueRunner across the loaded scenes, found " +
|
|
string.Join(", ", runners.Select(r => $"'{r.name}' in {r.gameObject.scene.name}")));
|
|
|
|
var runner = runners[0];
|
|
Assert.IsNotNull(runner.YarnProject,
|
|
$"'{runner.name}' in scene '{runner.gameObject.scene.name}' has no Yarn Project. " +
|
|
"It auto-starts, so this is the 'no Yarn Project has been configured' error.");
|
|
Assert.IsNotEmpty(runner.YarnProject.NodeNames, "The Yarn Project compiled no nodes");
|
|
}
|
|
|
|
[Test]
|
|
public void EveryInteractor_ResolvedItsDialogueRunner()
|
|
{
|
|
var interactors = Object.FindObjectsByType<DialogueInteractor>(
|
|
FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
Assert.IsNotEmpty(interactors, "Expected interactors in the level scene");
|
|
|
|
foreach (var interactor in interactors)
|
|
{
|
|
Assert.IsNotNull(interactor.ResolvedRunner,
|
|
$"'{interactor.name}' did not resolve a DialogueRunner. It lives in the " +
|
|
"Systems scene, so the serialized reference is gone and SceneServices has " +
|
|
"to find it.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void RevealCameras_BoundToThePlayer()
|
|
{
|
|
var binders = Object.FindObjectsByType<CinemachineFollowsPlayer>(
|
|
FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
Assert.IsNotEmpty(binders, "Expected at least one runtime-bound camera");
|
|
|
|
foreach (var binder in binders)
|
|
{
|
|
Assert.IsNotNull(binder.GetComponent<CinemachineCamera>().Follow,
|
|
$"'{binder.name}' never bound its Follow target to the player");
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|