add a writer-facing content pipeline under writing/

Dialogue was already plain text, so the gap was never the .yarn files. It was
that the character material a writing session needs was either missing or stored
somewhere a writer could not comfortably work.

Three things were wrong. The writing handbook was not in the repo and was cited
by SC101.yarn as though it were. The best voice material — register, verbal tics,
success/failure/passive samples — was a \n-escaped string inside a JSON field.
The named NPCs had no sheets at all, existing only as their Yarn lines.

writing/ now holds the dialogue-facing layer, at the repo root so it gets no
.meta files and is not a Unity asset:

  STYLE.md          register, the five tone moves, the ban list, branching
                    shapes, the Wrong Truth rule, flag families, candidate rules
  YARN-PRIMER.md    the one page of syntax needed to write a scene
  GLOSSARY.md       in-world and project vocabulary
  voices/           the eleven skill voices, one readable sheet each
  characters/       Bartender, Chevalier Cassian Thal, Bradford Kane
  scenes/           SC-101's brief: intent, beats, checks, constraints
  lore/             the three identity readings and the rules that bind them

STYLE.md is a digest of the Mystery Writing Handbook in the Obsidian vault, with
pointers rather than copies. The vault stays the authoring home for design work,
and Ground Truth — the solution document — deliberately stays out of this repo.

The voice sheets are generated from skill_bible.json but are the source of truth
for the prose. tools/writing/sync_voices.py folds edits back; voicelib.py
guarantees notes -> dict -> notes is identity for all eleven, so --to-json on
unchanged sheets is byte-identical. VoiceSheetSyncTests fails the build on drift
in either direction, and was verified to fail on real drift rather than merely
to pass.

SC101.yarn's header is trimmed from prose rationale to the six rules that bind
while editing that file, with the reasoning moved to the scene brief.

EditMode 42/42. YarnCheck compiles 9 files / 35 nodes.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 20:23:42 +02:00
co-authored by Claude Opus 5
parent b915ad78c7
commit f1fadf7909
28 changed files with 2766 additions and 21 deletions
+149
View File
@@ -0,0 +1,149 @@
# Yarn, in one page
Everything below is used somewhere in `Assets/Dialogue/`. Nothing else is needed to write a
scene. Full language docs: <https://docs.yarnspinner.dev>.
## A file
```
// Comments start with two slashes. Use them — this project keeps its design
// reasoning in file headers, and that is the record of why things are the way
// they are. Do not strip them.
title: Bartender_Talk
---
Bartender: Ah, {$player_alias}, back again?
Narrator: What does he mean? "Back again?"
===
```
`title:` names the node. `---` ends the header, `===` ends the node. One file can hold many
nodes. A `position:` line may appear in the header — that is the graph editor's coordinates,
not something you write by hand.
## Who is speaking
The text before the first colon is the speaker.
```
Narrator: The mahogany doors are too heavy for the hinges.
Bartender: What can I get ya?
PROVENANCE: Nineteen sixty-nine, the licence.
```
Skill voices are spoken in **UPPERCASE**, exactly as they appear in `voices/`. `Narrator:` is
the room and the body. A named character uses their display name.
## Player choices
```
-> Back? I have never been here.
<<set $revealed_fugue_to_bartender = true>>
Narrator: The bartender squints with a critical look.
-> Yes. I'll take the usual.
Narrator: He doesn't even flinch.
```
Indentation is what nests the consequence under the option. Options are presented together;
whichever is chosen runs its indented block.
## Conditions
```
<<if $spoke_to_bartender>>
Bartender: Ah, still hazy? A drink?
<<elseif $reputation > 10>>
Bartender: On the house.
<<else>>
Bartender: And you are?
<<endif>>
```
Comparison words also work: `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `and`, `or`, `not`.
## Variables
Every variable must first be declared in `Common.yarn`:
```
<<declare $spoke_to_bartender = false as bool>>
<<declare $reputation = 0 as number>>
<<declare $player_name = "Vesper" as string>>
```
Then set and read it anywhere:
```
<<set $spoke_to_bartender = true>>
<<set $self_lean_made_asset += 1>>
Bartender: Ah, {$player_alias}.
```
`{$variable}` interpolates into a line. **If you skip the `<<declare>>`, the compiler cannot
catch your typos** — that is the entire reason `Common.yarn` exists.
## Moving between nodes
```
<<detour Chevalier_Cassian_Thal_Talk>> // goes there, comes BACK here
<<jump SC101_Hook_Menu_Chair_Interaction>> // goes there, never returns
<<stop>> // ends the conversation immediately
```
Getting this wrong is the most common structural bug in this project. See `STYLE.md` §7 —
and never `<<jump>>` out of a node you were `<<detour>>`ed into.
## Node groups — variations without an if-chain
Several nodes may share one `title:`. Yarn runs the most specific variation whose `when:`
conditions pass:
```
title: Bartender_Talk
when: $reputation > 10
---
Bartender: Your money's no good here.
===
```
Callers just `<<detour Bartender_Talk>>` and do not care which variation ran. Adding a new
situation later means adding a node, not editing a growing `<<if>>`.
## The commands this game adds
| Syntax | What it does |
|---|---|
| `<<check <voice> <Band>>>` | Roll. Bands: `Trivial` `Routine` `Hard` `Specialist` `BuildDefining`. Writes `$check_result` and the other `$check_*` variables. |
| `<<skill_mod <source> <voice> <n>>>` | Temporary modifier. `source` is a bucket key, e.g. `scene`, `drunk`. |
| `<<clear_skill_mods <source>>>` | Drops a bucket. |
| `<<enter_environment>>` | Hands control back to the world. Ends the dialogue cleanly. |
And two functions:
| Syntax | What it returns |
|---|---|
| `skill_rank(<voice>)` | the player's rank in that voice |
| `top_skill(a, b, …)` | the highest of the named voices — **returns its FIRST argument on a tie**, so argument order is a design decision |
A check in practice:
```
<<check provenance Hard>>
<<if $check_result>>
PROVENANCE: Basel? Makes total sense.
<<else>>
PROVENANCE: This is probably related to Switzerland.
<<endif>>
```
Note that the failure branch is written with the same confidence as the success branch. That is
the Wrong Truth rule, and it is not optional — `STYLE.md` §4.
## Checking your work
```bash
cd tools/YarnCheck && dotnet run -- ../../NightclubArcadia/Assets/Dialogue
```
Remember that this harness never runs `<<check>>`, so `$check_result` is always `false` and you
will only ever see failure branches when playing through it.