Files
nightclub-arcadia/CLAUDE.md
T
lennartandClaude Opus 5 5da782d21b add agent reference and restructure plan
CLAUDE.md documents repository layout, the Unity command line, the dialogue
content pipeline, validation, conventions, and known traps. AGENTS.md points at
it for non-Claude agents.

docs/restructure-plan.md covers both halves of the restructure: moving character
and voice prose out of JSON string fields into a writer-facing writing/ tree, and
migrating off the single-scene DialogueTest scaffold into Bootstrap/Systems/Level
scenes. Phase 0 is recorded as executed; everything from Phase 1 on is proposal.

Two findings in it were corrected after first being got wrong: the Unity CLI is
installed (just not on PATH for non-interactive shells) and does have build and
test commands, and the binary scene is not a stale save but a standing Unity
behaviour where Force Text converts only when the setting changes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 19:12:49 +02:00

18 KiB

Nightclub Arcadia — agent reference

Reference for coding agents (Claude Code and others) working in this repository. Human-facing design docs live in docs/. This file is the operational one: where things are, what is generated, and how to build/test/validate from the terminal.

Companion: AGENTS.md (pointer file, same content applies).


1. Repository layout

Repo root is nightlcub-arcadia (the typo is in the directory name, not a mistake to fix — it is the checkout path). The Unity project is a subdirectory:

/Users/lennart/Dev/nightlcub-arcadia/          # git root
├── CLAUDE.md, AGENTS.md                       # this reference
├── docs/                                      # design + plan docs (markdown)
├── tools/YarnCheck/                           # .NET 9 Yarn compiler/player, no Unity needed
├── .gitignore                                 # macOS / IDE / toolchain rules
└── NightclubArcadia/                          # ← the Unity project (-projectPath)
    ├── .gitignore                             # Unity rules (anchored, must stay here)
    ├── Assets/
    ├── Packages/                              # incl. embedded dev.yarnspinner.unity
    └── ProjectSettings/

Two .gitignore files, deliberately. The Unity one anchors every rule with a leading /, so it only works at the root of the Unity project folder. Do not merge them.

Unity 6000.5.8f1, URP 17.5.0, Cinemachine 3.1.7, Input System 1.20.0, AI Navigation 2.0.14, ProBuilder 6.1.2, Test Framework 1.7.0. Yarn Spinner is an embedded package at NightclubArcadia/Packages/dev.yarnspinner.unity (plus the textanimator addon) — it is committed, not resolved from a registry.


2. Ground rules

  1. Never hand-edit generated .asset files. Assets/Skills/Definitions/*.asset, Assets/Skills/SkillDatabase.asset, Assets/Candidates/Definitions/*.asset and Assets/Candidates/CandidateDatabase.asset are build output. Their sources are Assets/Skills/skill_bible.json and Assets/Candidates/candidates.json. Edits are overwritten by the Editor setup scripts. Edit the JSON.
  2. Never hand-edit scene or prefab YAML unless you have read the surrounding block. GUID/fileID surgery in .unity / .prefab files silently detaches components.
  3. One Unity instance per project path. Unity refuses to open a project in batch mode while the Editor has it open — a single instance at a time. Always check before any -batchmode invocation (§3.1).
  4. Prefer YarnCheck over Unity for dialogue validation. It is seconds, not minutes, and needs no Editor lock (§5.3).
  5. Do not commit on the user's behalf unless asked. Unity writes to tracked files (ProjectSettings/*, .meta, scenes) as a side effect of simply being open, so git status noise is expected and is not always yours.

3. Unity from the command line

3.0 Two different tools, both present

What it is Status
/Users/lennart/.unity/bin/unity The Unity CLI (v1.0.0-beta.6). Wraps the Editor for CI-shaped work: build, test, run, open, plus editor/license/cloud management. Installed. Prefer this.
/Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity The Editor executable. Raw -batchmode flags. Installed. Fallback, for flags the CLI does not expose.

The CLI is not on PATH for non-interactive shells — which unity fails even though the binary exists, and unity doctor reports check.binary-on-path warn. Always invoke it by absolute path, or export it:

export UNITY_CLI="/Users/lennart/.unity/bin/unity"
export UNITY="/Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity"
export UNITY_PROJECT="/Users/lennart/Dev/nightlcub-arcadia/NightclubArcadia"

unity doctor --no-banner confirms the environment: signed in via OAuth keyring, editor 6000.5.8f1 arm64 resolved from ProjectVersion.txt. Add --json to any command for machine-readable output and --no-banner to keep logs clean.

3.1 Always check the lock first

Only one Unity instance per project path. Check before any headless run:

pgrep -fl "Unity.app/Contents/MacOS/Unity" | grep -v AssetImportWorker

AssetImportWorker* processes are children of the main Editor — filter them out or you will misread the check. Temp/UnityLockfile and unity status are the other two signals.

3.2 Run tests

"$UNITY_CLI" test "$UNITY_PROJECT" --no-banner --mode EditMode \
  --output /tmp/editmode-results.xml \
  --report-format both --junit-output /tmp/editmode-junit.xml

Useful flags: --filter <pattern>, --retries <0-10> (reports tests that pass on retry as flaky), --rerun-failed, --shard N/M for parallel CI, --coverage, --timeout <seconds>.

--mode PlayMode runs the PlayMode suite. There are none yet — Assets/Tests/ is EditMode only, so that command reports zero tests. A PlayMode suite needs a new Assets/Tests/PlayMode/*.asmdef referencing UnityEngine.TestRunner with includePlatforms left empty.

Raw-Editor equivalent, if you need a flag the CLI does not expose:

"$UNITY" -batchmode -nographics -projectPath "$UNITY_PROJECT" \
  -runTests -testPlatform EditMode \
  -testResults /tmp/editmode-results.xml -logFile /tmp/unity-editmode.log

Do not add -quit to a -runTests run — the runner terminates the Editor itself, and -quit on top of async work can hang it.

Parse the results XML; do not trust the exit code. Unity's own docs state there is no common exit-code definition across the components under test. Read total/passed/failed off the root element and list any test-case whose result is not Passed.

3.3 Build a player

"$UNITY_CLI" build "$UNITY_PROJECT" --no-banner \
  --target StandaloneOSX -o /tmp/NightclubArcadia.app

Without --execute-method the CLI uses Unity's built-in build — a Build Profile on Unity 6+, or the legacy desktop player flags — and builds whatever scenes EditorBuildSettings enables. --profile <name> selects a profile from Assets/Settings/Build Profiles (the profile defines the target). -l/--log-file overrides the log path; the log streams to console unless --no-tail.

No build script exists yet. When one is added, put it at Assets/Editor/BuildPipeline.cs and call --execute-method NightclubArcadia.Editor.BuildPipeline.BuildMacOS. The CLI forwards the output path as -buildOutput and your method is responsible for honouring it. A build method must call EditorApplication.Exit(1) on failure, or Unity exits 0 and CI reports a green build that produced nothing.

3.4 Run an arbitrary Editor method

"$UNITY_CLI" run "$UNITY_PROJECT" --no-banner --timeout 900 -- \
  -executeMethod SomeClass.SomeStaticMethod

unity run owns -batchmode, -nographics, -quit and -logFile. Passing any of them after -- fails with a reserved-flag error. Pass only your own arguments.

Output does not stream to stdout. The log lands at $UNITY_PROJECT/Logs/Editor.log (the previous run rotates to Editor-prev.log), so tag your Debug.Log calls and grep for them:

grep -n "\[YourTag\]" "$UNITY_PROJECT/Logs/Editor.log" | tail -20

This is the mechanism for the JSON → ScriptableObject regeneration in Assets/Editor/SkillSystemSetup.cs and CandidateSystemSetup.cs. Read the actual class and method names out of those files before invoking — do not guess them.

4. The dialogue content pipeline

4.1 Where content lives

NightclubArcadia/Assets/Dialogue/
├── NightclubArcadia.yarnproject   # globs **/*.yarn — new files need no registration
├── Common.yarn                    # every <<declare>>, project-wide. Never played.
├── Commentary.yarn                # passive skill barks, one node per (context, skill)
├── Scenes/     SC101, SC102, Debug
├── Characters/ Bartender, Bradford Kane, Chevalier Cassian Thal
└── Objects/    Chair

Dialogue is already plain text in the repo. It is authored in .yarn files, not inside the Unity Editor, and any new .yarn file dropped anywhere under Assets/Dialogue/ is picked up automatically by the **/*.yarn glob in the .yarnproject. Adding a scene or a character requires no C#, no Unity, and no .yarnproject edit.

The one caveat: the .yarnproject also stores editorOptions.yarnScriptEditor.projectGraphPositions — per-file x/y coordinates for the Yarn graph editor. These churn whenever anyone opens the graph view. Treat that block as editor state; do not fight over it in review.

4.2 The variable contract

Every variable used anywhere must be <<declare>>d in Common.yarn. This is the whole point of that file: the compiler then catches $reputaton instead of silently creating a second variable at runtime. Adding a scene flag means adding a <<declare>> line — that is a writer action, not a code action.

Naming families that are load-bearing (enforced or lint-checked):

Family Meaning Checked by
$self_lean_<candidate_id> identity-lean counter CandidateYarnLintTests — id must exist in candidates.json
$found_<clue_id> clue coverage CandidateYarnLintTests — id must exist in candidates.json
$visited_<location_id> first arrival convention; unlockFlag in candidates.json may only name these
$errand_<errand_id> errand state same
$seen_<scene_id> scene witnessed convention
$check_* written by <<check>> never <<set>> by hand

4.3 Writer-facing Yarn vocabulary

Commands and functions registered by game code, i.e. the full set a writer may use:

Syntax Defined in Notes
<<check <skill> <Band>>> Skills/Yarn/YarnSkillCommands.cs writes $check_result, $check_roll, $check_total, $check_dc, $check_modifier, $check_degree, $check_skill
<<skill_mod <source> <skill> <n>>> same source = bucket key, e.g. scene, drunk
<<clear_skill_mods <source>>> same
skill_rank(<skill>) same function
top_skill(a, b, …) Dialogue/DialougueSkillComparison.cs returns its FIRST argument on a tie — argument order is a design decision, not incidental
alias_name(<name>) Dialogue/AliasNameGenerator.cs deterministic; $player_alias is a smart variable derived from it
<<enter_environment>> Dialogue/DialogueTransitionController.cs hands control back to the world

(The filename DialougueSkillComparison.cs is misspelled in the repo. Left alone deliberately — renaming it changes a .meta GUID. Fold it into a rename batch, not a drive-by fix.)

4.4 Flow control discipline

<<detour>> returns to the caller; <<jump>> does not and clears the return stack. The rule the existing files follow, and which new content must follow:

  • A node started directly by DialogueInteractable has nothing waiting on it → its branches <<jump>>, and each destination calls <<enter_environment>> itself.
  • A sub-conversation that must resume its caller → <<detour>>, and never <<jump>> inside it.

Nodes sharing a title: form a node group; Yarn runs the most specific variation whose when: conditions pass. Prefer adding a variation over growing an <<if>> chain.

4.5 How content reaches Unity

There is no manual sync step for dialogue. The chain is:

.yarn file saved  →  Unity's YarnProjectImporter recompiles the .yarnproject on focus
                  →  DialogueRunner plays the new nodes on next Play

The only things that need a human/Editor step are the data files, not the dialogue:

skill_bible.json  →  Tools ▸ Nightclub Arcadia ▸ (SkillSystemSetup)     →  Skills/Definitions/*.asset + SkillDatabase.asset
candidates.json   →  Tools ▸ Nightclub Arcadia ▸ (CandidateSystemSetup) →  Candidates/Definitions/*.asset + CandidateDatabase.asset

Those setup scripts also register a [DidReloadScripts] callback, so in practice the assets regenerate on script reload too. The JSON is the source of truth either way.

4.6 Code-owned vs writer-owned

Writer-owned (edit freely, no Unity) Code-owned (needs an engineer)
Assets/Dialogue/**/*.yarn Assets/Scripts/**/*.cs
<<declare>> lines in Common.yarn Assets/Editor/**/*.cs
Assets/Skills/skill_bible.json (prose fields) Yarn command/function implementations
Assets/Candidates/candidates.json (status, sceneId, notes, unlockFlag) .asset, .prefab, .unity, .meta
docs/ writing docs ProjectSettings/**

A writer must never need to open Unity or touch C# to write or revise a scene, a character, or a branch. If a writing task requires either, that is a pipeline bug — file it.


5. Validation

5.1 EditMode test suite

NightclubArcadia/Assets/Tests/EditMode/ — 7 files, ~870 LOC, two assemblies:

  • NightclubArcadia.Skills.Tests — SkillRosterTests, SkillCheckSystemTests, DcBandTests, CommentaryPickerTests, CandidateRegistryTests, CandidateYarnLintTests
  • NightclubArcadia.Locomotion.Math.Tests — MotionInputMathTests

CandidateYarnLintTests is a regex lint over Yarn source: it asserts every $self_lean_* / $found_* in any .yarn file resolves to a real id in CandidateDatabase. It is a tripwire, not a proof.

5.2 One-click in-Editor run

Tools ▸ Nightclub Arcadia ▸ Run Skill EditMode Tests (Assets/Editor/SkillSystemTestRunner.cs).

5.3 YarnCheck — validate dialogue without Unity ⭐

tools/YarnCheck is a .NET 9 console app that compiles and plays the Yarn scripts using the exact Yarn Spinner DLLs shipped in Packages/dev.yarnspinner.unity/Runtime/DLLs, so it can never drift from the compiler the game uses. This is the fastest correctness signal in the repo and it needs no Editor lock. Use it on every dialogue change.

cd /Users/lennart/Dev/nightlcub-arcadia/tools/YarnCheck

# compile everything — exit 1 + diagnostics on failure
dotnet run -- ../../NightclubArcadia/Assets/Dialogue

# compile, then play a node, choosing option 2, 2, 0, 0
dotnet run -- ../../NightclubArcadia/Assets/Dialogue Bartender_Talk 2 2 0 0

Leftover picks restart the node — that deliberately models the player walking away and coming back, which is how these scenes are actually played.

What it does not model (read traces with this in mind): Unity commands are printed, not executed, so <<check>> / <<enter_environment>> / <<skill_mod>> are stepped over and $check_result stays false — every check-gated branch shows its failure side. Yarn functions are stubbed: top_skill mirrors the real first-argument-wins tie behaviour, alias_name returns a placeholder, skill_rank returns 0. Variables reset to their <<declare>> defaults each run; there is no save state.

Requires the .NET 9 SDK. tools/YarnCheck/bin/ and obj/ are currently committed and should not be — see docs/restructure-plan.md.


6. Code conventions

  • Namespaces: NightclubArcadia.<Area> — Skills, Dialogue, UI, UI.Dialogue, UI.HUD, UI.Menu, Cinematics, Interaction, Player, Player.Math. Exception: NPCStandIn.cs sits in the global namespace. That is a scaffold tell, not a convention to copy — new runtime types always get a namespace.
  • Assembly definitions exist for NightclubArcadia.Skills and NightclubArcadia.Locomotion.Math only. Camera, Player, UI, Dialogue, NPCs and Interaction all currently compile into the default Assembly-CSharp. Widening asmdef coverage is planned, not done.
  • Comments in this codebase carry decision history — why a flag is written at the bottom of a node rather than the top, why four voices in SC-101 breaks the three-voice cap on purpose. Match that density. Do not strip such comments as "noise"; they are the design record.
  • Editor-only code lives under Assets/Editor/. Anything there is stripped from builds and may reference UnityEditor.

7. Traps

  1. Assets/Scenes/DialogueTest.unity is serialized as binary, and Unity will not convert it from a headless run. The project is set to Force Text (m_SerializationMode: 2) and the API agrees — EditorSettings.serializationMode reads ForceText — but Force Text governs conversion at the moment the setting changes, not on every save. A file that entered the project as binary stays binary. Four approaches were tried and all failed silently: AssetDatabase.ForceReserializeAssets does not rewrite scene files; OpenScene + MarkSceneDirty + SaveScene to the same path returns true and writes nothing, because MarkSceneDirty does not take in batch mode; SaveScene to a new path writes a fresh file that is also binary; and re-assigning serializationMode (Mixed → ForceText) from script does not trigger a reserialize pass. The fix is Editor-UI only: Project Settings ▸ Editor ▸ Asset Serialization, switch the mode and switch it back, which reserializes on apply. Until then the scene is not diffable or mergeable. It is committed and intact (14 roots).
  2. unity run reserves -batchmode, -nographics, -quit, -logFile. Passing any of them after -- is a hard error, and its output does not reach stdout — read Logs/Editor.log (§3.4).
  3. -quit with -runTests hangs or truncates. Omit it (§3.2).
  4. Exit code alone is not a test verdict. Parse the results XML (§3.2).
  5. which unity fails in non-interactive shells even though the CLI is installed. Use the absolute path /Users/lennart/.unity/bin/unity (§3.0).
  6. Editor lock — check before every headless run, filtering out AssetImportWorker children (§3.1).
  7. Regenerated assets — hand edits to Skills/Definitions/*, Candidates/Definitions/* and the two *Database.asset files are silently discarded (§2.1).
  8. top_skill ties resolve to the first argument. Reordering arguments changes the game.
  9. No .gitattributes — no Unity YAML merge driver, no LFS rule for Assets/Art/. Scene and prefab merges will conflict destructively under branching.
  10. Template leftovers still present: Assets/Scenes/SampleScene.unity, Assets/Readme.asset, Assets/TutorialInfo/. SampleScene is in the build scene list. Not load-bearing.