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]>
Fixes "[SceneServices] Bartender: could not resolve dialogueRunner".
Resolving a cross-scene dependency in Awake is a race by construction. The
dialogue runner lives in Systems; an NPC lives in the level. Open the level on
its own, or let the scenes load in the other order, and the NPC wakes with no
Systems scene in sight. Worse, the failed lookup was cached into the serialized
field, so it stayed broken for the rest of the session rather than recovering
once Systems appeared.
NPCStandIn and DialogueInteractable now resolve lazily through a property and
cache only a successful result, so any load order works. Awake still does a
silent best-effort pass, which keeps the common case free.
SceneServices splits in two accordingly. TryResolve is silent and is what
speculative callers use; Resolve logs an error and belongs only where the
dependency is genuinely required. Both now include inactive objects — a system
parked inactive is still the object we mean, and excluding it reported "not
found" for something sitting in the hierarchy. The error text now says where the
runner lives and how to open the scenes, instead of asking a question.
BootstrapTests only ever covered the happy order, which is why it stayed green
while this was broken. SceneOrderTests loads the level BEFORE Systems and holds
that NPCs still reach the runner. It also asserts it is reproducing the race —
it counts the NPCs whose Awake-time resolution failed and fails if that is zero,
so the test cannot quietly start passing for the wrong reason. It currently
reports 2 of 2, which is the bug this commit fixes.
EditMode 42/42, PlayMode 7/7, YarnCheck 9 files / 35 nodes.
Co-Authored-By: Claude Opus 5 <[email protected]>
No project code is in Assembly-CSharp any more. Five assemblies: Core (no
dependencies), Locomotion.Math, Skills, Game (everything else under Scripts),
and StarterAssets.
Game is one assembly rather than one per area, which is what the plan assumed.
Measuring the dependency graph first found three cycles, all through UI —
CharacterPanelView reaches into Dialogue, PlayerControlLock into Player, and
UILayerBootstrap into Cinematics — plus two edges a using-scan cannot see,
because NPCStandIn is in the global namespace. Assemblies cannot be circular, so
splitting further means relocating those three files. That is a code-movement
task, not an asmdef task, and nothing needs it yet.
StarterAssets had to get an assembly of its own. It had none, so it lived in
Assembly-CSharp, and an asmdef assembly cannot reference the predefined
assemblies — four runtime files use it, and all four would have stopped
compiling.
The payoff is immediate: BootstrapTests no longer needs reflection to reach
NPCStandIn and CinemachineFollowsPlayer, which is exactly why Phase 3 wanted
this.
Assets/Editor/PlayerBuild.cs is the headless build entry point. It reads the
enabled scenes, asserts Bootstrap is scene 0 because the player opens scene 0 on
launch, honours the -buildOutput the CLI forwards from -o, and calls
EditorApplication.Exit(1) on anything short of Succeeded — without which Unity
exits 0 on a build that produced nothing and CI goes green on it. Verified end to
end: a 172MB .app in about two minutes.
Two renames, both different from what the plan described. DialougueSkillComparison.cs
contains a class called SkillFunctions — the filename never matched the type, so
it is now SkillFunctions.cs. The two .yarn files with spaces in their names lost
the spaces rather than becoming kebab-case, which would have made them
inconsistent with Bartender.yarn and SC101.yarn; spaces were the actual problem.
Both renames preserved their .meta GUIDs, and the yarnproject graph keys and the
two character sheets were updated.
EditMode 42/42, PlayMode 5/5, YarnCheck 9 files / 35 nodes, make build green.
Co-Authored-By: Claude Opus 5 <[email protected]>
The game now starts from Bootstrap.unity, which additively loads Systems (the
dialogue runner, skills, UI layer, camera rig and the player) and then a level
(geometry, light, navmesh, reveal cameras). The level becomes the active scene so
new objects and lighting land there.
The player moved into Systems. It had been parented under Room_101 — under level
geometry — and it is persistent content, not level content. Moving it also
removes most of the cross-scene breakage on its own.
Unity nulls any serialized reference that crosses a scene boundary. Measured
rather than guessed: the pre-split scene was pulled from git and its null
references diffed against the split result. Exactly seven were lost — both NPCs'
dialogueRunner, dialogueUI and player, plus the reveal camera's tracking target.
A first naive audit reported 210, which turned out to be pre-existing Unity
defaults like Image.m_Material; the baseline diff is what separated the two.
Each of the seven now resolves at runtime. SceneServices.Resolve fills in a null
Inspector reference by searching the loaded scenes, keeping an assigned value if
there is one; CinemachineFollowsPlayer binds the reveal camera once the player
exists. Both follow the pattern already used by CameraFramingVolume and by
NPCStandIn's player lookup, rather than introducing a new one.
Assets/Tests/PlayMode is new, and it immediately paid for itself. BootstrapTests
loads Bootstrap and asserts the whole game comes up; on its first run it caught a
real bug the split had introduced. The player's NavMeshAgent lives in Systems
while the NavMesh is baked into the level, so during load the agent exists
off-mesh and ResetPath logs an error. ClickToMoveController now guards on
agent.isOnNavMesh rather than a bare null check. No EditMode test could have seen
that, because none of it has run yet.
Two of those checks reach their types by name through reflection: NPCStandIn and
the Cinematics namespace live in Assembly-CSharp, and an asmdef test assembly
cannot reference the predefined assemblies. Per-area asmdefs remove the need.
Build settings list all three scenes with Bootstrap at index 0, which
LoadSceneAsync by name requires.
EditMode 42/42, PlayMode 5/5, YarnCheck 9 files / 35 nodes.
Co-Authored-By: Claude Opus 5 <[email protected]>
DialogueInteractable, DialogueTransitionController, and DialogueUIVisibility are
reworked against the new UI layer: visibility is driven by UIStateController
rather than each component toggling canvases itself, which removes the
duplicated show/hide logic the two had accumulated.
NPCStandIn now ignores the interact button while UIStateController reports a
menu open, so pressing interact through an open panel no longer starts a
conversation behind it.
The yarnproject change is graph-editor node positions only.
Co-Authored-By: Claude Opus 5 <[email protected]>
Runtime UI under Scripts/UI: a UIStateController owning menu/dialogue state, a
PlayerControlLock so opening a panel suppresses world input, panel visibility
helpers, and the menu input binding.
Views: the dialogue stream presenter and its options presenter and message row,
the portrait HUD, and the player menu with character, journal, and tab-bar
views. Skills gains SkillsPanelView and SkillRowView, which is why the Skills
asmdef now references UnityEngine.UI.
Prefabs/UI holds the authored canvases (dialogue stream, HUD, player menu) and
their rows. UILayerBootstrap remains a hand-added runtime fallback for scenes
where those prefabs are not instantiated; it deliberately does not auto-spawn.
Editor/UILayerSetup.cs is the one-shot menu that builds and wires those prefabs
into a scene, with UILayerSetupAutoRun reminding after a script reload.
Art/UI/Placeholder carries the placeholder portrait the HUD falls back to.
Co-Authored-By: Claude Opus 5 <[email protected]>
Camera: Cinemachine-based rig with an anchor, a state director, point-of-
interest framing, pan input, and dialogue framing volumes.
Player: click-to-move over the NavMesh, a pointer input layer, and a motion
arbiter that reconciles direct stick input with navigation-driven movement.
The pure math is isolated in NightclubArcadia.Locomotion.Math with an EditMode
suite, so the arbitration rules are testable without a scene.
Interaction: IClickInteractable plus a bridge that routes pointer clicks to it.
Supporting project settings: adds the Cinemachine package, the Interactable and
Ground tags the raycasts depend on, and NavMesh agent dimensions matched to the
player capsule (radius 0.3, height 1.8, climb 0.25).
Co-Authored-By: Claude Opus 5 <[email protected]>