Files
nightclub-arcadia/CLAUDE.md
T
lennartandClaude Opus 5 f5b302b3e3 add a Makefile for the common commands, and a test reporter
The invocations in CLAUDE.md were correct but long, and retyping them is how
flags get dropped. `make check` is the gate a change should pass: Yarn compiles,
voice sheets are in sync, EditMode is green.

`make test` and `make build` depend on a `lock` target that fails if the Editor
has the project open, which removes the most common confusing failure. It
filters out AssetImportWorker children, which are not a second Editor.

tools/ci/report_tests.py parses the NUnit results and exits non-zero on failure,
because Unity documents no common exit-code definition across the components
under test — the XML is the authority, not $?.

`make merge-driver` configures UnityYAMLMerge for the .gitattributes rules added
earlier. Note the binary lives in the Editor bundle under Contents/Helpers, not
Contents/Tools as most guides say; there is no Tools directory in Unity 6 on
macOS.

Also ignores the local .test-results/ and build/ output.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 20:29:39 +02:00

449 lines
22 KiB
Markdown

# 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)
├── writing/ # writer-facing context — see §4
├── tools/YarnCheck/ # .NET 9 Yarn compiler/player, no Unity needed
├── tools/writing/ # voice-sheet sync (md <-> skill_bible.json)
├── .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.4).
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.
---
## 2b. Everyday commands
A `Makefile` at the repo root wraps everything in this document. Prefer it — the exact
invocations live there instead of being retyped.
```bash
make # list targets
make check # yarn + voice sheets + EditMode — run this before committing
make test # EditMode suite, with a readable summary
make yarn # compile every Yarn script (seconds, no Unity)
make build # macOS player into build/
make lock # fail if the Editor has the project open
```
`make test` and `make build` refuse to run while the Editor is open, which removes the most
common way these commands fail confusingly.
### Merge driver — one-time per machine
`.gitattributes` marks Unity YAML for `unityyamlmerge`. Git needs to be told what that is, or
it falls back to a line-oriented text merge that can produce a scene Unity still loads and that
is quietly wrong:
```bash
make merge-driver
```
That points git at `UnityYAMLMerge` inside the Editor bundle (`Contents/Helpers/`, not
`Contents/Tools/` — the path in most online guides is wrong for Unity 6 on macOS).
## 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:
```bash
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:
```bash
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
```bash
"$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:
```bash
"$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
```bash
"$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
```bash
"$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:
```bash
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
```
The writer-facing context sits at the repo root, outside `Assets/` so it gets no `.meta` files:
```
writing/
├── README.md session entry point
├── STYLE.md register, tone moves, ban list, branching rules ← read first
├── YARN-PRIMER.md the one page of syntax a writer needs
├── GLOSSARY.md in-world and project vocabulary
├── voices/ the eleven skill voices, one sheet each (generated — see §4.7)
├── characters/ named NPCs, dialogue-facing
├── scenes/ per-scene briefs: intent, beats, checks, constraints
└── lore/ candidates.md — the three identity readings and their hard rules
```
Design material deeper than this lives in Obsidian, not the repo:
`~/Obsidian-Vaults/obsidian-vault/Projects/Active/NightclubArcadia/`. **`Ground Truth.md` there
is the solution document — never copy, quote, or summarise it into this repo**, including into
commit messages. The game's premise is that the protagonist's identity never resolves.
**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 The voice-sheet pipeline
The Skill Bible prose used to live as a `\n`-escaped string inside `skill_bible.json`, which no
writer could comfortably read or revise. It now lives in `writing/voices/*.md`, and truth flows
in opposite directions for the two halves of each sheet:
```
PROSE writing/voices/<id>.md -> skill_bible.json -> Skills/Definitions/*.asset
MACHINE skill_bible.json -> the "Machine fields" table in the .md
```
```bash
python3 tools/writing/sync_voices.py --check # verify; exit 1 on drift
python3 tools/writing/sync_voices.py --to-json # fold edited prose into the bible
python3 tools/writing/sync_voices.py --to-md # refresh the machine table from the bible
```
`tools/writing/voicelib.py` does the parsing and guarantees `notes -> dict -> notes` is identity
for all eleven skills. `--to-json` on unchanged sheets is byte-identical, so it is safe to run
at any time.
`VoiceSheetSyncTests` (EditMode) enforces the same invariant, so drift fails the build instead
of silently shipping a voice that no longer matches the sheet the next scene is written against.
It deliberately does not reimplement the markdown parser in C# — it asserts the shipped prose
appears verbatim in the sheet, which catches drift in either direction with no second parser to
keep in step.
### 4.7 Code-owned vs writer-owned
| Writer-owned (edit freely, no Unity) | Code-owned (needs an engineer) |
|---|---|
| `writing/**` | `Assets/Scripts/**/*.cs` |
| `Assets/Dialogue/**/*.yarn` | `Assets/Editor/**/*.cs` |
| `<<declare>>` lines in `Common.yarn` | Yarn command/function *implementations* |
| `writing/voices/*.md` (prose → the bible, §4.6) | `Assets/Skills/skill_bible.json` (machine fields) |
| `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 Voice-sheet sync
```bash
python3 tools/writing/sync_voices.py --check
```
Seconds, no Unity. Run it after touching `writing/voices/*.md` or `skill_bible.json` (§4.6).
### 5.3 One-click in-Editor run
`Tools ▸ Nightclub Arcadia ▸ Run Skill EditMode Tests` (`Assets/Editor/SkillSystemTestRunner.cs`).
### 5.4 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.
```bash
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.