Files
lennartandClaude Opus 5 9a0618fd31 replace the NPC scaffold with data-driven prefabs, and retire the builders
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]>
2026-08-25 21:53:06 +02:00

38 KiB
Raw Permalink Blame History

Restructure Plan — writer-facing content pipeline + migration off the test scaffold

Status: proposal, not executed. Nothing in this document has been applied to the repository. Two files were written as part of producing it — CLAUDE.md and AGENTS.md at the repo root — because they were requested as deliverables. No asset, script, scene or setting was moved, renamed or altered.

Companion reading: docs/skill-system-refactor-plan.md, docs/candidate-system-plan.md, docs/candidate-system-implementation-plan.md.


0. Status — all phases complete

Executed 2026-08-25. The working tree is clean and tagged pre-restructure.

All previously uncommitted work is now in git across five commits: camera/locomotion/interaction, the UI layer, player control setup, the dialogue-vs-menu interaction fix, and the scene itself. A full pre-flight backup was taken first (.tar.gz of the tree excluding Library/, Temp/, Logs/, and build output) plus a separate copy of the scene file.

Phases 1 through 5 are also done — see §4. The restructure described by this document is finished; what remains in §2 and §3 is the target state, now reached.

0.1 The binary scene — RESOLVED in Phase 3

This went through two wrong diagnoses before the real one. Recorded in full because the wrong turns are the instructive part.

First theory (wrong): the scene had been saved during a temporary Force Binary session and merely needed re-saving.

Second theory (wrong, but closer): Force Text only converts at the moment the setting changes, so a file that entered binary stays binary — and the fix is therefore an Editor-UI toggle. Four headless approaches were tried and all failed silently with success return codes: ForceReserializeAssets does not rewrite scenes; MarkSceneDirty + SaveScene writes nothing in batch mode; SaveScene to a new path reproduces binary; re-assigning serializationMode triggers no reserialize pass.

The actual cause: the NavMeshSurface on the Navigation root held its baked NavMeshData embedded in the scene rather than as an asset. NavMeshData prefers binary serialization, and a single such object forces the entire scene file to binary no matter what the project setting says. That is why every attempt to re-save it failed — the file was being written correctly each time, in the only format its contents allowed.

Found by bisection: moving the roots one at a time into a fresh scene, thirteen produced text files and Navigation produced a binary one. Extracting the data to Scenes/Levels/SC101_ConferenceHall/NavMesh-Navigation.asset — which is what baking from the NavMeshSurface inspector produces anyway; the embedded copy was the anomaly — made the scene serialize as text. No Editor-UI step was required.

The scene is now Assets/Scenes/Levels/SC101_ConferenceHall.unity, 43KB of YAML, diffable and mergeable, with the UnityYAMLMerge driver configured (Phase 1). The generalised lesson is trap 1 in CLAUDE.md.

0.2 The Unity CLI — finding corrected

The initial survey reported the standalone unity CLI as not installed and lacking build/test commands. It is installed, at /Users/lennart/.unity/bin/unity (v1.0.0-beta.6), and it has both build and test, plus run, open, and editor management. The original check failed because the binary is not on PATH for non-interactive shells — unity doctor flags this itself as check.binary-on-path warn.

It is the better tool for CI work than raw -batchmode: unity test handles NUnit and JUnit reports, --retries with flaky detection, --rerun-failed, and --shard N/M. See CLAUDE.md §3 for the full command set and the gotchas (unity run reserves -batchmode, -nographics, -quit and -logFile; its output goes to Logs/Editor.log, not stdout).


1. What the survey actually found

The brief described a proof-of-concept where dialogue is authored inside the Editor. That is not what is in the repository. The state is considerably more advanced, and that changes what the work is.

1.1 Already correct — do not "fix" these

Thing State
Dialogue is already plain text 679 lines across 9 .yarn files under Assets/Dialogue/{Scenes,Characters,Objects}/. No Editor authoring anywhere.
The .yarnproject glob **/*.yarn. A new scene or character file is picked up with zero registration. Nothing to build here.
A JSON → ScriptableObject codegen pipeline exists skill_bible.json and candidates.json are the source of truth; Assets/Editor/SkillSystemSetup.cs and CandidateSystemSetup.cs regenerate the .asset files. This is exactly the right pattern and it is already load-bearing.
A headless Yarn validator exists tools/YarnCheck compiles and plays Yarn against the game's own Yarn Spinner DLLs, no Unity needed.
A real EditMode suite exists ~870 LOC / 7 files, including CandidateYarnLintTests, which lints Yarn source against the candidate registry.
Variable discipline exists Common.yarn declares everything project-wide so typos are compile errors.

1.2 The real gap for a Cowork writer

It is not the .yarn files. It is three things:

  1. The writing handbook does not exist as a file. SC101.yarn cites a "cap introductions at three voices" rule and docs/candidate-system-implementation-plan.md says outright that the handbook is not in this repo. So the rules a writer must follow are partly in someone's head, partly scattered across code comments. A Cowork session cannot read a handbook that isn't checked in — which is precisely the "re-explaining the world each time" problem.

  2. The best character material is trapped inside JSON string fields. skill_bible.json holds, per skill, a notes field containing an entire embedded prose sheet — domain, what it wants for you, what it's wrong about, register, verbal tics, SUCCESS/FAILURE/PASSIVE voice samples, firing frequency, forbidden domains. It is excellent writing. It is stored as a single \n-escaped string in a machine file. A writer cannot comfortably read or revise it, and every edit risks breaking the JSON.

  3. NPC characters have no sheets at all. The Bartender, Bradford Kane and Chevalier Cassian Thal exist only as their .yarn lines. There is no record of who they are, what they know, or how they speak.

1.3 Scaffold vs. load-bearing

Verdict Items
Load-bearing Scripts/Skills/** (25 files, asmdef'd), Scripts/UI/** (13 files, ~1660 LOC), Scripts/Camera/** (7 files, ~727 LOC), Scripts/Player/** (5 files, ~654 LOC + Locomotion.Math asmdef), Scripts/Interaction/**, Scripts/Dialogue/**, Assets/Dialogue/**, Assets/Skills/**, Assets/Candidates/**, Assets/Prefabs/UI/**, Assets/Tests/**, tools/YarnCheck
Scaffold — replace NPCStandIn.cs (global namespace, sphere-primitive placeholder, hardwired [F] prompt, "drop this on a sphere" in its own docstring), Assets/Editor/YarnDemoSetup.cs (targets DialogueTest.unity by literal path), Assets/Scenes/DialogueTest.unity itself
Scaffold — keep for now Assets/Editor/UILayerSetup.cs (818 lines), PlayerControlSetup.cs (316), UILayerSetupAutoRun.cs. These are one-shot scene/prefab builders. They are how the scene got wired and they still work; retiring them is a later step, not this one.
Template leftovers — delete Assets/Scenes/SampleScene.unity, Assets/Readme.asset, Assets/TutorialInfo/
Should not be committed tools/YarnCheck/bin/, tools/YarnCheck/obj/, .DS_Store files

1.4 Infrastructure gaps

  • No .gitattributes. No Unity YAML merge driver, no LFS rule for Assets/Art/. Scene and prefab merges will conflict destructively the first time work happens on two branches.
  • asmdef coverage is partial. Only Skills and Locomotion.Math. Camera, Player, UI, Dialogue, NPCs and Interaction all land in Assembly-CSharp, so every C# edit recompiles everything.
  • No PlayMode tests. Assets/Tests/ is EditMode only.
  • No build entry point. No -executeMethod build script; EditorBuildSettings lists SampleScene and DialogueTest.
  • Single branch, no remote work in flight. main only, origin/main in sync.

2. Part 1 — externalizing dialogue and character writing

2.1 The principle

Prose lives in markdown. Machine-readable structure lives in JSON. Playable content lives in .yarn. Nothing that a writer needs to read is stored as an escaped string inside a data file.

The existing JSON-is-source-of-truth pattern is kept and extended — not replaced. What changes is that the prose half moves out of the JSON into markdown with frontmatter, and the JSON keeps only the fields code actually reads.

2.2 Proposed structure

A new top-level writing/ directory at the repo root, deliberately outside Assets/:

writing/                              ← the Cowork working directory
├── README.md                         # "start here" for a writing session
├── STYLE.md                          # the missing writing handbook, finally checked in
├── GLOSSARY.md                       # world terms, factions, place names, in-fiction jargon
├── YARN-PRIMER.md                     # the ~1 page of Yarn syntax a writer needs
├── characters/
│   ├── _TEMPLATE.md
│   ├── bartender.md
│   ├── bradford-kane.md
│   └── chevalier-cassian-thal.md
├── voices/                           # the eleven skills, as readable sheets
│   ├── _TEMPLATE.md
│   ├── provenance.md
│   ├── confluence.md
│   └── … (facework, the-float, room-tone, amnesty, house-pour,
│          long-shift, placement, standing-order, undisclosed)
├── scenes/
│   ├── _TEMPLATE.md
│   ├── sc101-the-card-at-the-desk.md   # brief, beats, tone — NOT the dialogue
│   └── sc102-….md
└── lore/
    ├── candidates.md                 # the three readings, in prose
    └── world.md                      # the club, the conference, the night

Playable dialogue stays exactly where it is, in NightclubArcadia/Assets/Dialogue/. Do not move the .yarn files. They are already correct, Unity's importer watches that path, and the .meta files there carry GUIDs that scene references depend on.

So the writer's day looks like: read writing/, write .yarn. Two directories, no Unity.

Why root and not Assets/writing/: anything under Assets/ gets a .meta file, gets imported, and shows up in the Unity project window. Markdown notes should not be Unity assets. Root-level also means a Cowork session can be pointed at writing/ alone.

2.3 File format: markdown + YAML frontmatter

Frontmatter carries the few fields a script may want to check; the body is unconstrained prose.

writing/characters/_TEMPLATE.md:

---
id: bartender                 # matches the Yarn speaker name, lowercased/kebabed
name: The Bartender           # exact string used as the Yarn speaker prefix
yarn_file: Assets/Dialogue/Characters/Bartender.yarn
status: authored              # planned | drafted | authored
scenes: [SC101]
knows: [doss, the-estate-drink]
---

## Who they are
One paragraph. What they want tonight.

## How they speak
Register, sentence length, contractions, what they never say.

## What they know
Bullet list. For each: does the player learn it, under what condition, and which
`$flag` records it.

## What they must never do
The negative constraints. Usually more useful than the positive ones.

## Sample lines
Three to five. Not for shipping — for calibration.

writing/voices/_TEMPLATE.md mirrors the structure already present in skill_bible.json's notes field, promoted to real headings — domain, what it wants for you, what it's wrong about, how it addresses you, register, verbal tics, what it never does, opposed/allied, SUCCESS / FAILURE / PASSIVE voice, firing frequency, forbidden domains. That structure was already good; it just needs to be readable.

writing/scenes/_TEMPLATE.md carries the brief and the beat list, plus the constraints that are currently buried in .yarn header comments — the evidence-balance rule, the beat order, the passive-pattern rule, any deliberate exception. SC101.yarn's 25-line header comment is the model; it should live here and be referenced from the .yarn file, not duplicated.

2.4 Migrating the Skill Bible without losing the JSON pipeline

skill_bible.json currently holds both machine fields (axis, opposedTo, alliedWith, targetFiringsPerHour, accentColor, startingRank, primaryChannel, clueYield) and the prose blob (notes, plus a duplicated domain).

Proposed split, once:

  1. Write a one-shot script that reads skill_bible.json and emits writing/voices/<id>.md — frontmatter from the machine fields, body from the parsed notes.
  2. SkillDefinition.notes stays in the C# type and keeps rendering wherever it renders. The generator that produces it now reads the markdown body instead of the JSON string.
  3. skill_bible.json remains the machine source. Either it keeps a notes field regenerated from the markdown, or SkillSystemSetup.cs learns to read writing/voices/*.md directly.

Recommendation: the first. writing/voices/*.md is the human source; a small script folds prose back into skill_bible.json; SkillSystemSetup.cs is untouched. That keeps the existing, tested Editor pipeline unchanged and confines the new machinery to one script — which matters because SkillRosterTests asserts against the generated assets.

The direction of truth must be written down and never ambiguous:

writing/voices/*.md  ──(sync script)──▶  skill_bible.json  ──(SkillSystemSetup)──▶  *.asset
   human writes here          machine mirror                     build output

An EditMode test should assert the mirror is in sync, so a JSON hand-edit fails CI instead of silently winning.

2.5 The flow back into Unity

Change What the writer does Sync step Verify
New/edited dialogue line, branch, node edit .yarn, save none — importer picks it up dotnet run YarnCheck
New scene or character .yarn file create file under Assets/Dialogue/ none — **/*.yarn glob YarnCheck
New variable add <<declare>> to Common.yarn none YarnCheck
Voice prose edit writing/voices/*.md run sync script → JSON EditMode tests
Character sheet, scene brief, lore, style edit writing/*.md none — docs only —
Candidate/clue status, new clue id edit candidates.json Editor menu or script reload CandidateYarnLintTests

The everyday writing loop has no manual sync step at all. That is the design goal and it is already met for .yarn; the work is to reach the same property for character and voice material.

2.6 Validation a writer can run alone

cd tools/YarnCheck
dotnet run -- ../../NightclubArcadia/Assets/Dialogue                    # does it compile?
dotnet run -- ../../NightclubArcadia/Assets/Dialogue Bartender_Talk 2 2 0 0   # does it play?

Requires only the .NET 9 SDK — no Unity, no license, no Editor lock. This should be the first thing writing/README.md tells a new session to run, and the last thing it tells them to run before handing work back.

Caveat that must be stated plainly in the README, because it will otherwise cause confusion: YarnCheck stubs <<check>>, so $check_result is always false and every check-gated branch shows its failure side. A writer seeing only failure prose has not found a bug.

2.7 Scope boundary

Writer-owned: writing/**, Assets/Dialogue/**/*.yarn, prose fields in the two JSON files. Code-owned: all C#, all .asset / .prefab / .unity / .meta, all ProjectSettings/, and the implementation of any Yarn command or function.

A writer needing a new Yarn command is a request to engineering, not a task they can complete — and that is the only such boundary. Everything else about writing a scene must be self-serve.


3. Part 2 — target project structure

3.1 Scenes

The current model — one scene holding everything — is what makes DialogueTest.unity both irreplaceable and unmergeable. The target is additive scene loading, which is also the standard answer to Unity scene merge conflicts.

Assets/Scenes/
├── Bootstrap.unity          # entry point. Systems only, no geometry. First in build list.
├── Systems/
│   └── Systems.unity        # DialogueRunner, UIStateController, PlayerSkills, Commentary,
│                            #   camera director. Loaded additively by Bootstrap, never unloaded.
├── Levels/
│   └── SC101_ConferenceHall.unity   # geometry, lights, nav mesh, NPC + interactable placement
└── Dev/
    └── Sandbox.unity        # scratch scene, explicitly disposable

Bootstrap loads Systems then the requested level. The payoff is direct: two people (or a person and an agent) can edit level content and systems wiring without touching the same file, and a corrupted level scene no longer takes the systems layer with it.

DialogueTest.unity becomes Levels/SC101_ConferenceHall.unity — renamed and split, not rebuilt. Its contents are the asset; only its scope changes.

3.2 Folders

Mostly already right. The deltas:

Assets/
├── Art/                     ← exists (placeholder portrait). Add Environment/, Characters/, VFX/
├── Audio/                   ← new: Music/, SFX/, VO/
├── Dialogue/                ← unchanged. Do not move.
├── Candidates/  Skills/     ← unchanged (JSON + generated assets)
├── Prefabs/
│   ├── UI/                  ← exists
│   ├── Characters/          ← new: real NPC prefabs, replacing sphere stand-ins
│   ├── Interactables/       ← new: chair, door, object interactables
│   └── Systems/             ← new: Bootstrap/Systems prefabs
├── Scenes/                  ← per §3.1
├── Scripts/
│   ├── Core/                ← new: bootstrap, scene loading, save/load, service locator
│   ├── Camera/ Player/ Interaction/ Dialogue/ UI/ Skills/   ← unchanged
│   └── NPCs/                ← NPCStandIn replaced by a real NPC component (§4, step 6)
├── Settings/                ← unchanged (URP)
└── Tests/
    ├── EditMode/            ← unchanged
    └── PlayMode/            ← new

3.3 Conventions

  • Scenes: PascalCase. Levels prefixed with their scene id — SC101_ConferenceHall.
  • Prefabs: PascalCase, no _Prefab suffix. Variants suffixed _Variant.
  • Scripts: one public type per file, filename = type name. Namespace NightclubArcadia.<Area>, matching the folder. No global-namespace runtime types.
  • Yarn nodes: the existing conventions hold and are already consistent — <Character>_Talk, SC###_Hook_<Subject>, Commentary_<Context>_<skill_id>.
  • Yarn variables: the families in CLAUDE.md §4.2. These are lint-enforced; treat them as API.
  • asmdefs: one per top-level Scripts/ area, named NightclubArcadia.<Area>.
  • .yarn filenames may contain spaces (Bradford Kane.yarn) — the glob handles it, but every shell command touching them needs quoting. Worth normalising to kebab-case during a rename batch; low priority.

4. The migration plan

Incremental, ordered, each step independently committable and revertible. Steps 0–2 are the safety work and should happen before anything is moved.

Effort marks are rough: S ≲1h, M a half day, L a day or more.

Phase 0 — checkpoint ✅ DONE (2026-08-25)

# Step Status
0.1 Close the Unity Editor done
0.2 Full backup of the tree + separate copy of the scene done
0.3 Commit all uncommitted work in logical slices (§5) done — 5 commits
0.4 Convert DialogueTest.unity to text blocked — Editor-UI only, see §0.1
0.5 Tag pre-restructure (on a green tree, 39/39) done
0.6 Push main and the tag to origin done

0.4 is the one open item and it does not block Phase 1 or 2.

Phase 1 — repo hygiene ✅ DONE (2026-08-25)

# Step Status
1.1 .gitattributes done — UnityYAMLMerge driver, LF pinned, binaries marked
1.2 Untrack tools/YarnCheck/{bin,obj} no-op — already ignored, never tracked
1.3 Remove committed .DS_Store no-op — already ignored, never tracked
1.4 Makefile wrapping the CLAUDE.md commands done, plus tools/ci/report_tests.py

1.2 and 1.3 were wrong in the original plan: both were listed from a filesystem find, not from git ls-files. Nothing was ever tracked, and .gitignore already covers both.

Line endings were renormalized in one deliberate pass (74 files, all CRLF→LF, git diff -w empty) rather than left to surface as phantom diffs later. LFS was not enabled: the remote is self-hosted and its LFS support is unverified, and turning the filter on against a server that lacks it breaks pushing. The rules and the migration step are recorded in .gitattributes.

make merge-driver configures UnityYAMLMerge. Note the binary is at Contents/Helpers/, not Contents/Tools/ as most guides claim — there is no Tools directory in Unity 6 on macOS.

Phase 2 — writing pipeline ✅ DONE (2026-08-25)

# Step Status
2.1 writing/ skeleton + templates done
2.2 writing/STYLE.md done — digested from the vault handbook, not reconstructed (below)
2.3 Extract skill_bible.json prose → writing/voices/*.md done — 11 sheets, lossless round-trip
2.4 Sync script + EditMode drift test done — tools/writing/sync_voices.py, VoiceSheetSyncTests
2.5 Character sheets done — Bartender, Chevalier Cassian Thal, Bradford Kane
2.6 README.md + YARN-PRIMER.md done, plus GLOSSARY.md and lore/candidates.md
2.7 SC-101 header rules → scene brief done — header trimmed to the six rules that bind while editing, rationale moved to the brief

Validated: EditMode 42/42 (three new), YarnCheck compiles 9 files / 35 nodes, sheets in sync. The drift test was verified to actually fail on drift, not merely to pass.

Phase 2 — what changed against the plan

Two things, both discovered during execution.

The handbook exists. docs/skill-system-refactor-plan.md §2 names it, and it is at ~/Obsidian-Vaults/obsidian-vault/Projects/Active/NightclubArcadia/ — a 874-line [[Mystery Writing Handbook]], plus [[Character Sheet Handbook]], [[Skill System Draft]], [[Lorebook]], [[Candidate System — Design Explanation]], scene specs, and [[Ground Truth]]. So §2.2 became a digest with pointers rather than a reconstruction, which is both more accurate and honest about where the authoring home is. The vault remains that home.

[[Ground Truth]] must never enter this repo. It is the solution document. The premise of the game is that the protagonist's identity never resolves, and a copy of the answer in a git history is exactly how that leaks. This is now stated in writing/README.md and CLAUDE.md §4.1.

One thing surfaced that is worth acting on independently: [[000_OpeningSceneTemplate]] records SC-101's tone check as BLOCKED because the one-page Voice Sheet has never been written — the scene spec could not honestly certify its own tone field. writing/STYLE.md §9 now holds that sheet with four fields explicitly unset. Filling them is about half an hour of decisions and it unblocks a check that has been stuck for a while.

Phase 3 — scene restructure ✅ DONE (2026-08-25)

# Step Status
3.1 Delete template leftovers done — SampleScene, Readme.asset, TutorialInfo
3.2 Create Bootstrap + Systems/Systems done, both text
3.3 Copy rebuild DialogueTest → Levels/SC101_ConferenceHall done — see below
3.4 Move systems objects into Systems.unity done — 10 roots incl. the player
3.5 Additive loading in Scripts/Core/ done — GameBootstrap, SceneServices
3.6 Verify parity, delete DialogueTest.unity done — verified by diff, then deleted
3.7 Retarget YarnDemoSetup.cs done — all four setup menus share ScenePaths

Validated: EditMode 42/42, PlayMode 5/5, YarnCheck 9 files / 35 nodes.

Phase 3 — what changed against the plan

The binary scene is fixed, and the cause was not what §0.1 assumed. The NavMeshSurface held its baked NavMeshData embedded in the scene. That type prefers binary serialization, and one such object forces the entire file to binary regardless of Force Text — which is why toggling the setting, re-saving, and saving to a new path all did nothing. Bisecting the roots one at a time found it: thirteen saved as text alone, Navigation did not. Extracting the data to Scenes/Levels/SC101_ConferenceHall/NavMesh-Navigation.asset — what baking from the inspector produces anyway — made the scene text. No Editor-UI step was needed after all.

So 3.3 became a rebuild, not a copy: saving the binary scene to a new path reproduces binary, so all 14 roots were moved into a fresh scene instead. Verified roots 14→14, cross-root references 25→25, zero missing scripts, render settings carried across by hand.

The player moved to Systems. It had been parented under Room_101, i.e. under level geometry. It is persistent content, and moving it removes most of the cross-scene breakage by itself.

The split nulled exactly seven references, measured by auditing the pre-split scene from git and diffing: both NPCs' dialogueRunner, dialogueUI and player, plus the reveal camera's tracking target. A first, naive audit reported 210 — those turned out to be pre-existing Unity defaults (Image.m_Material and friends), which is why the before/after baseline mattered.

A real bug surfaced, and only PlayMode could see it. The player's NavMeshAgent is 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. This is exactly the class of failure an EditMode suite cannot reach, and it is the argument for having written BootstrapTests rather than eyeballing the scene.

Phase 4.2 came early. Assets/Tests/PlayMode now exists. Two of its checks use reflection, because an asmdef assembly cannot reference Assembly-CSharp where NPCStandIn and the Cinematics namespace live — Phase 4.1 (per-area asmdefs) is what removes that.

Phase 4 — code structure ✅ DONE (2026-08-25)

# Step Status
4.1 Assemblies per area done — as 3 assemblies, not 7; see below
4.2 Assets/Tests/PlayMode/ + asmdef done early, in Phase 3
4.3 Assets/Editor/PlayerBuild.cs done and verified — a real 172MB .app
4.4 Rename batch done — both renames differed from the plan; see below

Validated: EditMode 42/42, PlayMode 5/5, YarnCheck 9 files / 35 nodes, make build green.

Phase 4 — what changed against the plan

One-asmdef-per-area is not achievable without moving code. The plan assumed seven assemblies (Camera, Player, Interaction, Dialogue, UI, NPCs, Core). Measuring the dependency graph first found three cycles, all through UI:

Cycle Caused by
UI ↔ Dialogue UI/Menu/CharacterPanelView.cs → NightclubArcadia.Dialogue
UI ↔ Player UI/PlayerControlLock.cs → NightclubArcadia.Player
UI ↔ Camera UI/UILayerBootstrap.cs → NightclubArcadia.Cinematics

There are also two edges a using-scan cannot see, because the types are in the global namespace: Interaction → NPCStandIn and Dialogue → NPCStandIn.

Assemblies cannot be circular, so the split landed as Core / Game / Skills / Locomotion.Math plus a new StarterAssets assembly. Game is the cyclic cluster kept whole. Splitting it further is a code-movement task — relocate those three files — not an asmdef task, and it is not worth doing until something needs it.

StarterAssets had to get an assembly too. It had none, so it was in Assembly-CSharp, and an asmdef assembly cannot reference the predefined assemblies. Four runtime files use it. Without giving it one, moving our code into assemblies would have broken all four.

The payoff was immediate: BootstrapTests no longer needs reflection. It references NPCStandIn and CinemachineFollowsPlayer directly, which is what Phase 3 flagged as the reason to do this.

Both renames in 4.4 were not what the plan described.

  • DialougueSkillComparison.cs contains a class called SkillFunctions. The filename never matched the type, so the fix was SkillFunctions.cs, not DialogueSkillComparison.cs. It is attached to nothing in any scene and its GUID was preserved through the rename.
  • The .yarn files were renamed to remove spaces, not to kebab-case: BradfordKane.yarn, ChevalierCassianThal.yarn. Kebab-case would have made them inconsistent with Bartender.yarn, Common.yarn and SC101.yarn, and spaces were the actual problem. The .yarnproject graph keys and the two character sheets in writing/ were updated to match.

Phase 5 — replace the scaffold ✅ DONE (2026-08-25)

# Step Status
5.1 Data-driven NPC component done — InteractableDefinition + DialogueInteractor
5.2 Prefabs/Characters/ prefabs done — Bartender, and Chair under Prefabs/Interactables/
5.3 Migrate scene NPCs off NPCStandIn, delete it done — label and prompt logic extracted, not discarded
5.4 Retire the one-shot *Setup.cs builders done — four deleted

Validated: EditMode 42/42, PlayMode 7/7, YarnCheck 9 files / 35 nodes.

Phase 5 — what changed against the plan

The second "NPC" was the chair. The scene had two spheres. One was the Bartender; the other was named Bartender (1) in the hierarchy but configured with npcName: Chair and Chair_Interaction — a duplicate whose data was changed and whose name never was. That is precisely the failure mode of identity living in per-instance fields, and it decided the design: identity moved into an asset, and the migration matched objects by yarn node, not by name.

So the plan's "character definition" became InteractableDefinition, covering both a character and a talkable object, and the prefabs split across Prefabs/Characters/ and Prefabs/Interactables/.

Three components were deleted, not one. DialogueInteractable turned out to be 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 directly. Net: three components replaced by one, plus two small reusable pieces (WorldSpaceLabel, InteractionPrompt) lifted out of the scaffold rather than thrown away.

5.4 went further than "retire". The plan said retire the builders once prefabs were authored. They were also, by then, actively harmful — YarnDemoSetup destroyed a level scene and PlayerControlSetup turned Systems binary — and after deleting NPCStandIn they no longer compiled. They are deleted, not merely disabled. UILayerSetup's 818 lines of wiring knowledge now live in the committed UI prefabs, and in git history if ever needed.

Characters got the same JSON pipeline as Skills and Candidates. Assets/Characters/characters.json is the source of truth; CharacterSetup regenerates the definition assets on script reload and deletes orphans. Adding an NPC is a JSON entry and a prefab, with no code and no Unity required for the data half.

4.6 Keep / relocate / replace, at a glance

Verdict Items
Keep as-is Assets/Dialogue/**, Assets/Skills/**, Assets/Candidates/**, Scripts/Skills/**, Scripts/UI/**, Scripts/Camera/**, Scripts/Player/**, Scripts/Interaction/**, Scripts/Dialogue/**, Assets/Tests/EditMode/**, Assets/Prefabs/UI/**, tools/YarnCheck, Assets/Settings/**
Rename / relocate DialogueTest.unity → Levels/SC101_ConferenceHall.unity (+ split); skill_bible.json prose → writing/voices/*.md; SC101.yarn header rules → writing/scenes/; DialougueSkillComparison.cs spelling; .yarn filenames with spaces
Replace NPCStandIn.cs → real NPC component + prefabs; YarnDemoSetup.cs → retargeted or retired; single-scene model → Bootstrap/Systems/Level
Delete SampleScene.unity, Readme.asset, TutorialInfo/, committed bin/ obj/ .DS_Store
Add .gitattributes, writing/**, Scripts/Core/**, Tests/PlayMode/**, Editor/BuildPipeline.cs, per-area asmdefs, Assets/Audio/

5. Checkpointing — what was done

Roughly 3,100 lines of C# across untracked directories, 291 changed lines in tracked scripts, 5 UI prefabs and the scene were uncommitted. All of it is now in git.

Order used, which differs from the original draft on one point: the safe text work was committed before any Unity run, so that a headless Editor invocation could not endanger it. That turned out to matter — four Unity runs followed, one ending in a SIGILL on teardown.

  1. Verified the Editor was closed and no lockfile was held.
  2. Took a full backup: a .tar.gz of the tree excluding Library/, Temp/, Logs/ and build output, plus a separate copy of the scene file.
  3. Committed in five slices, in dependency order:
Commit Contents
add camera rig, click-to-move locomotion, and click interaction Scripts/{Camera,Player,Interaction}, locomotion math tests, Cinemachine package, Interactable/Ground tags, NavMesh agent dimensions
add UI layer: dialogue stream, HUD, player menu, and skills panel Scripts/UI, Skills UI views + asmdef reference, Prefabs/UI, placeholder art, UILayerSetup
wire player control setup and tune the third-person controller PlayerControlSetup.cs, StarterAssets input/armature/controller
suppress world interaction while a menu panel is open Scripts/Dialogue, Scripts/NPCs, yarnproject graph positions
check in the DialogueTest scene and add it to the build settings the scene (binary, deliberately) + EditorBuildSettings
  1. Attempted the binary→text conversion four ways; all failed (§0.1). Committed the scene as binary rather than leave it untracked — a valid, intact, opening scene in an awkward format beats days of wiring living only on disk.
  2. Tagged pre-restructure.

Slices rather than one lump, because the restructure will want to revert part of this later.

  1. Fixed two wrong expectations in the locomotion math tests (both test bugs, not production bugs); EditMode is 39/39. Moved the tag onto that green tree and pushed.

Still to do: the Editor-UI serialization fix (§0.1). That is the only open Phase 0 item.

5.1 Residual risks after checkpointing

Risk Mitigation
Scene re-save produces a text file that differs semantically from the binary one Play SC-101 end to end before committing; compare against the pre-migration run
Renaming assets outside Unity orphans .meta GUIDs Rename inside the Editor, always
Scene merge conflicts once branching starts .gitattributes merge driver in Phase 1.1, before any branch
Deleting UILayerSetup.cs too early loses wiring knowledge Phase 5.4 is last, and only after prefabs are authored and committed
Binary scene state recurs Verify EditorSettings shows Force Text; add a CI check that *.unity files start with %YAML

6. Sequencing recommendation

Phase 0 today. It is under an hour and it is the difference between a recoverable project and a lost week.

Then Phase 2, before Phase 3. The writing pipeline touches no Unity asset, cannot break the game, and is what actually unblocks the stated goal of writing with Cowork. Phase 3 is the risky work and it does not need to come first. writing/STYLE.md (2.2) is the single highest-value item in this document — it is the artifact whose absence is currently forcing you to re-explain the world every session.

Phase 1 alongside Phase 2, since .gitattributes must land before any branching.

Phase 3–5 after, in order, small commits, playtest between each.


7. Open questions

  1. Does a writing handbook exist outside the repo? Answered — yes (Phase 2 notes). It is in the Obsidian vault and is now pointed at rather than duplicated. The open part is whether any of it should eventually move into the repo wholesale; the current answer is no, because the vault is reachable from writing sessions and duplication would drift.
  2. How did the scene become binary? Answered (§0.1): Force Text only converts when the setting changes, so anything that entered binary stays binary. The open part is how it first entered binary — most likely created or imported during a Force Binary window. Adding a CI check that every *.unity starts with %YAML would catch a recurrence cheaply.
  3. Is SC101_ConferenceHall the right level granularity, or should the conference hall split further (entrance / hall / bar / back office) given the $visited_* flag families already declared in Common.yarn? Those flags imply at least seven distinct locations.
  4. Target platform(s)? EditorBuildSettings and the URP settings carry both PC and Mobile render pipeline assets. It affects whether Phase 4.3's build script targets one platform or several.
  5. Should writing/ be a separate repository so Cowork sessions never see Unity? Recommendation: no — the coupling to .yarn files and candidates.json is tight enough that split repos would drift. A single repo with a clear directory boundary is the better trade.