Files
nightclub-arcadia/writing/YARN-PRIMER.md
T
lennartandClaude Opus 5 f1fadf7909 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]>
2026-08-25 20:23:42 +02:00

4.4 KiB

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

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.