Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed28763017 | ||
|
|
f1fadf7909 | ||
|
|
b915ad78c7 |
@@ -17,7 +17,9 @@ to fix — it is the checkout path). The Unity project is a **subdirectory**:
|
|||||||
/Users/lennart/Dev/nightlcub-arcadia/ # git root
|
/Users/lennart/Dev/nightlcub-arcadia/ # git root
|
||||||
├── CLAUDE.md, AGENTS.md # this reference
|
├── CLAUDE.md, AGENTS.md # this reference
|
||||||
├── docs/ # design + plan docs (markdown)
|
├── docs/ # design + plan docs (markdown)
|
||||||
|
├── writing/ # writer-facing context — see §4
|
||||||
├── tools/YarnCheck/ # .NET 9 Yarn compiler/player, no Unity needed
|
├── tools/YarnCheck/ # .NET 9 Yarn compiler/player, no Unity needed
|
||||||
|
├── tools/writing/ # voice-sheet sync (md <-> skill_bible.json)
|
||||||
├── .gitignore # macOS / IDE / toolchain rules
|
├── .gitignore # macOS / IDE / toolchain rules
|
||||||
└── NightclubArcadia/ # ← the Unity project (-projectPath)
|
└── NightclubArcadia/ # ← the Unity project (-projectPath)
|
||||||
├── .gitignore # Unity rules (anchored, must stay here)
|
├── .gitignore # Unity rules (anchored, must stay here)
|
||||||
@@ -49,7 +51,7 @@ AI Navigation 2.0.14, ProBuilder 6.1.2, Test Framework 1.7.0. Yarn Spinner is an
|
|||||||
mode while the Editor has it open — a single instance at a time. Always check before
|
mode while the Editor has it open — a single instance at a time. Always check before
|
||||||
any `-batchmode` invocation (§3.1).
|
any `-batchmode` invocation (§3.1).
|
||||||
4. **Prefer YarnCheck over Unity for dialogue validation.** It is seconds, not minutes,
|
4. **Prefer YarnCheck over Unity for dialogue validation.** It is seconds, not minutes,
|
||||||
and needs no Editor lock (§5.3).
|
and needs no Editor lock (§5.4).
|
||||||
5. **Do not commit on the user's behalf** unless asked. Unity writes to tracked files
|
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
|
(`ProjectSettings/*`, `.meta`, scenes) as a side effect of simply being open, so
|
||||||
`git status` noise is expected and is not always yours.
|
`git status` noise is expected and is not always yours.
|
||||||
@@ -174,6 +176,25 @@ NightclubArcadia/Assets/Dialogue/
|
|||||||
└── Objects/ Chair
|
└── 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
|
**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
|
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
|
up automatically by the `**/*.yarn` glob in the `.yarnproject`. Adding a scene or a character
|
||||||
@@ -249,13 +270,41 @@ candidates.json → Tools ▸ Nightclub Arcadia ▸ (CandidateSystemSetup)
|
|||||||
Those setup scripts also register a `[DidReloadScripts]` callback, so in practice the assets
|
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.
|
regenerate on script reload too. The JSON is the source of truth either way.
|
||||||
|
|
||||||
### 4.6 Code-owned vs writer-owned
|
### 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) |
|
| Writer-owned (edit freely, no Unity) | Code-owned (needs an engineer) |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Assets/Dialogue/**/*.yarn` | `Assets/Scripts/**/*.cs` |
|
| `writing/**` | `Assets/Scripts/**/*.cs` |
|
||||||
| `<<declare>>` lines in `Common.yarn` | `Assets/Editor/**/*.cs` |
|
| `Assets/Dialogue/**/*.yarn` | `Assets/Editor/**/*.cs` |
|
||||||
| `Assets/Skills/skill_bible.json` (prose fields) | Yarn command/function *implementations* |
|
| `<<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` |
|
| `Assets/Candidates/candidates.json` (`status`, `sceneId`, `notes`, `unlockFlag`) | `.asset`, `.prefab`, `.unity`, `.meta` |
|
||||||
| `docs/` writing docs | `ProjectSettings/**` |
|
| `docs/` writing docs | `ProjectSettings/**` |
|
||||||
|
|
||||||
@@ -278,11 +327,19 @@ or a branch.** If a writing task requires either, that is a pipeline bug — fil
|
|||||||
`$self_lean_*` / `$found_*` in any `.yarn` file resolves to a real id in `CandidateDatabase`.
|
`$self_lean_*` / `$found_*` in any `.yarn` file resolves to a real id in `CandidateDatabase`.
|
||||||
It is a tripwire, not a proof.
|
It is a tripwire, not a proof.
|
||||||
|
|
||||||
### 5.2 One-click in-Editor run
|
### 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`).
|
`Tools ▸ Nightclub Arcadia ▸ Run Skill EditMode Tests` (`Assets/Editor/SkillSystemTestRunner.cs`).
|
||||||
|
|
||||||
### 5.3 YarnCheck — validate dialogue without Unity ⭐
|
### 5.4 YarnCheck — validate dialogue without Unity ⭐
|
||||||
|
|
||||||
`tools/YarnCheck` is a .NET 9 console app that compiles **and plays** the Yarn scripts using
|
`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
|
the exact Yarn Spinner DLLs shipped in `Packages/dev.yarnspinner.unity/Runtime/DLLs`, so it
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
// SC-101 — The Card at the Desk. Opening candidate hooks.
|
// SC-101 — The Card at the Desk. Opening candidate hooks.
|
||||||
//
|
//
|
||||||
// Scene files stay thin: set the stage, <<detour>> into hook nodes, return.
|
// Brief, beats, checks and the reasoning behind every rule below:
|
||||||
|
// writing/scenes/sc101-the-card-at-the-desk.md
|
||||||
//
|
//
|
||||||
// Evidence rule: each candidate gets exactly one of each evidence type —
|
// Rules that bind while editing THIS file — all six are deliberate, none is a
|
||||||
// object, testimony, document, intuition. Preserve that balance; do not
|
// leftover. The brief says why; this is the short form so you do not have to
|
||||||
// stack types so trust becomes an artifact of imbalance.
|
// leave the file to avoid breaking it.
|
||||||
// See Assets/Candidates/candidates.json.
|
|
||||||
//
|
//
|
||||||
// Beat order: hooks fire in sequence from SC101_Desk (STANDING ORDER →
|
// 1. Scene files stay thin: set the stage, <<detour>> into hook nodes, return.
|
||||||
// HOUSE POUR → FACEWORK → CONFLUENCE). Each remains an independently authored
|
// 2. Evidence balance — one object / testimony / document / intuition per
|
||||||
// node. Never <<if>>-guard a hook on another hook having fired, on any
|
// candidate. Never stack types. See Assets/Candidates/candidates.json.
|
||||||
|
// 3. Beat order is fixed: STANDING ORDER -> HOUSE POUR -> FACEWORK ->
|
||||||
|
// CONFLUENCE. Never <<if>>-guard a hook on another hook having fired, on any
|
||||||
// $self_lean_*, or on skill_rank(...).
|
// $self_lean_*, or on skill_rank(...).
|
||||||
//
|
// 4. Passive pattern for a clue-bearing hook: one voice, from the candidate's
|
||||||
// Passive pattern (clue-bearing):
|
// attached pair; the line is that voice's argument, never a report; exactly
|
||||||
// one voice per hook, from the candidate's attached pair;
|
// one $self_lean_* write, with $found_<clue_id> set alongside it.
|
||||||
// the line is that character's argument, never a report;
|
// 5. Four voice introductions, against the handbook cap of three. Deliberate,
|
||||||
// exactly one $self_lean_* write; set $found_<clue_id> alongside it.
|
// SC-101 only — one voice per pair. Do not correct it down, do not copy it.
|
||||||
//
|
// 6. The body stays neutral: no lean, no candidate attachment outside the hooks.
|
||||||
// Four-voice exception: this opening uses four voice introductions (one per
|
|
||||||
// pair) against the writing handbook's "cap introductions at three voices"
|
|
||||||
// rule. Deliberate and for SC-101 only — do not "correct" it down to three,
|
|
||||||
// and do not copy it as a new precedent elsewhere.
|
|
||||||
//
|
|
||||||
// Body stays neutral on purpose: no lean, no candidate attachment.
|
|
||||||
|
|
||||||
title: Start
|
title: Start
|
||||||
position: -17,-276
|
position: -17,-276
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Skills.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Guards the voice-sheet pipeline: writing/voices/*.md holds the prose, skill_bible.json
|
||||||
|
/// mirrors it, and the SkillDefinition assets are generated from the JSON.
|
||||||
|
///
|
||||||
|
/// A writer edits the markdown and runs tools/writing/sync_voices.py --to-json. If they
|
||||||
|
/// forget, or if someone edits the JSON prose directly, the two drift and the shipped
|
||||||
|
/// voice stops matching the sheet the next scene is written against. That is silent and
|
||||||
|
/// expensive, so it fails here instead.
|
||||||
|
///
|
||||||
|
/// This deliberately does NOT reimplement the markdown parser in C#. It asserts that the
|
||||||
|
/// prose the game ships is present verbatim in the sheet, which catches drift in either
|
||||||
|
/// direction without a second parser to keep in step with the Python one.
|
||||||
|
/// </summary>
|
||||||
|
public class VoiceSheetSyncTests
|
||||||
|
{
|
||||||
|
const string DatabasePath = "Assets/Skills/SkillDatabase.asset";
|
||||||
|
|
||||||
|
static string VoicesDir =>
|
||||||
|
Path.GetFullPath(Path.Combine(Application.dataPath, "..", "..", "writing", "voices"));
|
||||||
|
|
||||||
|
static string SheetPath(string id) => Path.Combine(VoicesDir, id + ".md");
|
||||||
|
|
||||||
|
SkillDatabase LoadDatabase()
|
||||||
|
{
|
||||||
|
var db = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
|
||||||
|
Assert.IsNotNull(db, $"Expected SkillDatabase at {DatabasePath}");
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Files in voices/ that are prose for the writer, not a voice sheet.</summary>
|
||||||
|
static bool IsSupportingDoc(string name) =>
|
||||||
|
name.StartsWith("_") ||
|
||||||
|
string.Equals(name, "README", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>Collapse whitespace so a hard-wrapped sheet still matches a single-line source.</summary>
|
||||||
|
static string Flatten(string s) =>
|
||||||
|
Regex.Replace(s ?? string.Empty, @"\s+", " ").Trim();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EverySkill_HasAVoiceSheet()
|
||||||
|
{
|
||||||
|
Assert.IsTrue(Directory.Exists(VoicesDir), $"Expected the voice sheets at {VoicesDir}");
|
||||||
|
|
||||||
|
var missing = LoadDatabase().All
|
||||||
|
.Where(s => !File.Exists(SheetPath(s.Id)))
|
||||||
|
.Select(s => s.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.IsEmpty(missing,
|
||||||
|
"No voice sheet for: " + string.Join(", ", missing) +
|
||||||
|
". Run: python3 tools/writing/sync_voices.py --to-md");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EveryVoiceSheet_HasASkill()
|
||||||
|
{
|
||||||
|
Assert.IsTrue(Directory.Exists(VoicesDir), $"Expected the voice sheets at {VoicesDir}");
|
||||||
|
|
||||||
|
var ids = new HashSet<string>(LoadDatabase().All.Select(s => s.Id));
|
||||||
|
var orphans = Directory.GetFiles(VoicesDir, "*.md")
|
||||||
|
.Select(Path.GetFileNameWithoutExtension)
|
||||||
|
.Where(name => !IsSupportingDoc(name) && !ids.Contains(name))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.IsEmpty(orphans,
|
||||||
|
"Voice sheets with no matching skill id: " + string.Join(", ", orphans));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void VoiceSheets_MatchTheShippedProse()
|
||||||
|
{
|
||||||
|
var problems = new List<string>();
|
||||||
|
|
||||||
|
foreach (var skill in LoadDatabase().All)
|
||||||
|
{
|
||||||
|
var path = SheetPath(skill.Id);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
continue; // EverySkill_HasAVoiceSheet reports this
|
||||||
|
}
|
||||||
|
|
||||||
|
var sheet = Flatten(File.ReadAllText(path));
|
||||||
|
|
||||||
|
if (!sheet.Contains(Flatten("# " + skill.DisplayName)))
|
||||||
|
{
|
||||||
|
problems.Add($"{skill.Id}: sheet title does not match displayName '{skill.DisplayName}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sheet.Contains(Flatten(skill.Domain)))
|
||||||
|
{
|
||||||
|
problems.Add($"{skill.Id}: the domain text is not in the sheet");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var line in VoiceSamples(skill.Notes))
|
||||||
|
{
|
||||||
|
if (!sheet.Contains(Flatten(line)))
|
||||||
|
{
|
||||||
|
var preview = line.Length > 60 ? line.Substring(0, 60) + "…" : line;
|
||||||
|
problems.Add($"{skill.Id}: voice sample missing from the sheet: {preview}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.IsEmpty(problems,
|
||||||
|
"writing/voices/*.md has drifted from skill_bible.json:\n " +
|
||||||
|
string.Join("\n ", problems) +
|
||||||
|
"\n\nProse is authored in the markdown. If you changed it there, run:\n" +
|
||||||
|
" python3 tools/writing/sync_voices.py --to-json\n" +
|
||||||
|
"If you changed the JSON directly, move the change into the sheet instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The SUCCESS / FAILURE / PASSIVE lines out of a Skill Bible notes blob.</summary>
|
||||||
|
static IEnumerable<string> VoiceSamples(string notes)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(notes))
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var raw in notes.Split('\n'))
|
||||||
|
{
|
||||||
|
var match = Regex.Match(raw, @"^(?:SUCCESS|FAILURE|PASSIVE) VOICE(?: — .*?)?: (.+)$");
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
yield return match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f51db402df844805947fea0f2d43dfbc
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
+45
-20
@@ -10,7 +10,7 @@ Companion reading: `docs/skill-system-refactor-plan.md`, `docs/candidate-system-
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. Status — Phase 0 is complete
|
## 0. Status — Phases 0 and 2 are complete
|
||||||
|
|
||||||
Executed 2026-08-25. The working tree is clean and tagged `pre-restructure`.
|
Executed 2026-08-25. The working tree is clean and tagged `pre-restructure`.
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ the UI layer, player control setup, the dialogue-vs-menu interaction fix, and th
|
|||||||
A full pre-flight backup was taken first (`.tar.gz` of the tree excluding `Library/`, `Temp/`,
|
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.
|
`Logs/`, and build output) plus a separate copy of the scene file.
|
||||||
|
|
||||||
Everything below from §1 onward is unchanged proposal. Nothing in §2–§5 has been applied.
|
Phase 2 (the writing pipeline) is also done — see §4. Phases 1, 3, 4 and 5 remain proposal.
|
||||||
|
|
||||||
### 0.1 The binary scene — diagnosis corrected
|
### 0.1 The binary scene — diagnosis corrected
|
||||||
|
|
||||||
@@ -388,8 +388,8 @@ Effort marks are rough: **S** ≲1h, **M** a half day, **L** a day or more.
|
|||||||
| 0.2 | Full backup of the tree + separate copy of the scene | 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.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.4 | Convert `DialogueTest.unity` to text | **blocked** — Editor-UI only, see §0.1 |
|
||||||
| 0.5 | Tag `pre-restructure` | done |
|
| 0.5 | Tag `pre-restructure` (on a green tree, 39/39) | done |
|
||||||
| 0.6 | Push `main` and the tag to `origin` | pending |
|
| 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.
|
0.4 is the one open item and it does not block Phase 1 or 2.
|
||||||
|
|
||||||
@@ -405,20 +405,41 @@ Effort marks are rough: **S** ≲1h, **M** a half day, **L** a day or more.
|
|||||||
`.gitattributes` before any restructuring is deliberate — the moment work happens on a branch,
|
`.gitattributes` before any restructuring is deliberate — the moment work happens on a branch,
|
||||||
scene merges without a merge driver will corrupt files.
|
scene merges without a merge driver will corrupt files.
|
||||||
|
|
||||||
### Phase 2 — writing pipeline (no Unity changes, fully parallel to Phase 3+)
|
### Phase 2 — writing pipeline ✅ DONE (2026-08-25)
|
||||||
|
|
||||||
| # | Step | Detail | Effort |
|
| # | Step | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|
|
||||||
| 2.1 | Create `writing/` skeleton + templates | §2.2, §2.3 | S |
|
| 2.1 | `writing/` skeleton + templates | done |
|
||||||
| 2.2 | **Write `writing/STYLE.md`** | The missing handbook. Recover the rules from `.yarn` header comments and the three `docs/` plans. Highest-value single item in this plan. | M |
|
| 2.2 | `writing/STYLE.md` | done — **digested from the vault handbook, not reconstructed** (below) |
|
||||||
| 2.3 | Extract `skill_bible.json` `notes` → `writing/voices/*.md` | one-shot script, 11 files | M |
|
| 2.3 | Extract `skill_bible.json` prose → `writing/voices/*.md` | done — 11 sheets, lossless round-trip |
|
||||||
| 2.4 | Write the sync script md → JSON + an EditMode drift test | §2.4 | M |
|
| 2.4 | Sync script + EditMode drift test | done — `tools/writing/sync_voices.py`, `VoiceSheetSyncTests` |
|
||||||
| 2.5 | Author the three character sheets | Bartender, Bradford Kane, Chevalier Cassian Thal | M |
|
| 2.5 | Character sheets | done — Bartender, Chevalier Cassian Thal, Bradford Kane |
|
||||||
| 2.6 | Write `writing/README.md` + `YARN-PRIMER.md` | The Cowork session entry point, incl. the YarnCheck caveat (§2.6) | S |
|
| 2.6 | `README.md` + `YARN-PRIMER.md` | done, plus `GLOSSARY.md` and `lore/candidates.md` |
|
||||||
| 2.7 | Move `SC101.yarn`'s header rules into `writing/scenes/sc101-*.md` | leave a one-line pointer in the `.yarn` | S |
|
| 2.7 | SC-101 header rules → scene brief | done — header trimmed to the six rules that bind while editing, rationale moved to the brief |
|
||||||
|
|
||||||
Phase 2 touches **no Unity asset at all**. It can proceed while the Editor is open and while
|
Validated: EditMode **42/42** (three new), YarnCheck compiles 9 files / 35 nodes, sheets in sync.
|
||||||
Phase 3 is in flight, and it is what unblocks Cowork. Consider doing it first.
|
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 (the risky part)
|
### Phase 3 — scene restructure (the risky part)
|
||||||
|
|
||||||
@@ -494,7 +515,10 @@ That turned out to matter — four Unity runs followed, one ending in a `SIGILL`
|
|||||||
|
|
||||||
Slices rather than one lump, because the restructure will want to revert *part* of this later.
|
Slices rather than one lump, because the restructure will want to revert *part* of this later.
|
||||||
|
|
||||||
**Still to do:** push `main` and the tag to `origin`, and the Editor-UI serialization fix (§0.1).
|
6. 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
|
### 5.1 Residual risks after checkpointing
|
||||||
|
|
||||||
@@ -527,9 +551,10 @@ the world every session.
|
|||||||
|
|
||||||
## 7. Open questions
|
## 7. Open questions
|
||||||
|
|
||||||
1. **Does a writing handbook exist outside the repo?** `docs/candidate-system-implementation-plan.md`
|
1. ~~Does a writing handbook exist outside the repo?~~ **Answered — yes** (Phase 2 notes). It is in the
|
||||||
says it exists but is not checked in. If there is a document somewhere, importing it beats
|
Obsidian vault and is now pointed at rather than duplicated. The open part is whether any of
|
||||||
reconstructing it (Phase 2.2 assumes reconstruction).
|
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
|
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
|
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
|
first entered binary — most likely created or imported during a Force Binary window. Adding a
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Sync the eleven voice sheets between writing/voices/*.md and skill_bible.json.
|
||||||
|
|
||||||
|
Direction of truth, per field. This is the whole design; read it before changing
|
||||||
|
anything here.
|
||||||
|
|
||||||
|
PROSE writing/voices/<id>.md -> skill_bible.json (displayName, domain, notes)
|
||||||
|
MACHINE skill_bible.json -> writing/voices/<id>.md ("Machine fields" block)
|
||||||
|
|
||||||
|
A writer edits prose in the markdown and never opens the JSON. An engineer edits
|
||||||
|
machine fields in the JSON and never hand-writes the markdown table. Neither
|
||||||
|
direction is ambiguous, so nothing silently wins.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
sync_voices.py --to-md regenerate every .md from the JSON (initial extraction,
|
||||||
|
and to refresh the machine block afterwards)
|
||||||
|
sync_voices.py --to-json fold the markdown prose back into skill_bible.json
|
||||||
|
sync_voices.py --check verify the two are in sync; exit 1 if not
|
||||||
|
|
||||||
|
Wrapped prose is fine: lines inside a section are joined with single spaces, so a
|
||||||
|
writer may hard-wrap a paragraph without changing what lands in the JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import voicelib
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
JSON_PATH = os.path.join(ROOT, "NightclubArcadia", "Assets", "Skills", "skill_bible.json")
|
||||||
|
VOICES_DIR = os.path.join(ROOT, "writing", "voices")
|
||||||
|
|
||||||
|
GEN_START = "<!-- GENERATED FROM skill_bible.json — do not edit by hand -->"
|
||||||
|
GEN_END = "<!-- END GENERATED -->"
|
||||||
|
|
||||||
|
# markdown heading -> notes field key
|
||||||
|
SECTIONS = [
|
||||||
|
("Domain", "domain"),
|
||||||
|
("What it wants for you", "what it wants for you"),
|
||||||
|
("What it's wrong about", "what it's wrong about"),
|
||||||
|
("How it addresses you", "how it addresses you"),
|
||||||
|
("Register", "register"),
|
||||||
|
("What it never does", "what it never does"),
|
||||||
|
("Opposed to", "opposed to"),
|
||||||
|
("Allied with", "allied with"),
|
||||||
|
("Domains it must not comment on", "__forbidden"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def machine_block(s):
|
||||||
|
allied = ", ".join(f"`{a}`" for a in s.get("alliedWith", [])) or "—"
|
||||||
|
c = s.get("accentColor", {})
|
||||||
|
return "\n".join([
|
||||||
|
GEN_START,
|
||||||
|
"",
|
||||||
|
"| Field | Value |",
|
||||||
|
"|---|---|",
|
||||||
|
f"| id | `{s['id']}` |",
|
||||||
|
f"| axis | {s.get('axis','—')} |",
|
||||||
|
f"| opposed to | `{s.get('opposedTo','—')}` |",
|
||||||
|
f"| allied with | {allied} |",
|
||||||
|
f"| primary channel | {s.get('primaryChannel','—')} |",
|
||||||
|
f"| secondary channel | {s.get('secondaryChannel','—')} |",
|
||||||
|
f"| clue yield | {s.get('clueYield','—')} |",
|
||||||
|
f"| starting rank | {s.get('startingRank','—')} |",
|
||||||
|
f"| target firings/hour | {s.get('targetFiringsPerHour','—')} |",
|
||||||
|
f"| accent colour | rgba({c.get('r')}, {c.get('g')}, {c.get('b')}, {c.get('a')}) |",
|
||||||
|
"",
|
||||||
|
GEN_END,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def to_md(s):
|
||||||
|
d = voicelib.parse_notes(s["notes"])
|
||||||
|
f = d["fields"]
|
||||||
|
L = [
|
||||||
|
"---",
|
||||||
|
f"id: {s['id']}",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
f"# {f['name']}",
|
||||||
|
"",
|
||||||
|
"> Voice sheet. **The prose below is the source of truth** — edit it here, then run",
|
||||||
|
"> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated",
|
||||||
|
"> from `skill_bible.json`; changing it here does nothing.",
|
||||||
|
"",
|
||||||
|
"## Machine fields",
|
||||||
|
"",
|
||||||
|
machine_block(s),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for heading, key in SECTIONS:
|
||||||
|
L.append(f"## {heading}")
|
||||||
|
L.append("")
|
||||||
|
L.append(d["forbidden"] if key == "__forbidden" else f[key])
|
||||||
|
L.append("")
|
||||||
|
|
||||||
|
L.append("## Verbal tics")
|
||||||
|
L.append("")
|
||||||
|
for n, text in d["tics"]:
|
||||||
|
L.append(f"{n}. {text}")
|
||||||
|
L.append("")
|
||||||
|
|
||||||
|
L.append("## Voice samples")
|
||||||
|
L.append("")
|
||||||
|
for kind in ("success", "failure", "passive"):
|
||||||
|
L.append(f"### {kind.capitalize()}")
|
||||||
|
L.append("")
|
||||||
|
if kind in d["voice_gloss"]:
|
||||||
|
L.append(f"*{d['voice_gloss'][kind]}*")
|
||||||
|
L.append("")
|
||||||
|
L.append(f"> {d['voices'][kind]}")
|
||||||
|
L.append("")
|
||||||
|
|
||||||
|
L.append("## Firing frequency")
|
||||||
|
L.append("")
|
||||||
|
L.append(f"{d['firing']} per hour (target).")
|
||||||
|
L.append("")
|
||||||
|
return "\n".join(L)
|
||||||
|
|
||||||
|
|
||||||
|
def _join(lines):
|
||||||
|
return " ".join(x.strip() for x in lines if x.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def from_md(text, skill_id):
|
||||||
|
"""markdown -> the dict voicelib.emit_notes expects."""
|
||||||
|
body = re.sub(re.escape(GEN_START) + r".*?" + re.escape(GEN_END), "", text, flags=re.S)
|
||||||
|
|
||||||
|
name_m = re.search(r"^# (.+)$", body, re.M)
|
||||||
|
if not name_m:
|
||||||
|
raise ValueError(f"{skill_id}: no H1 title")
|
||||||
|
|
||||||
|
chunks = {}
|
||||||
|
for m in re.finditer(r"^##+ (.+?)$\n(.*?)(?=^##+ |\Z)", body, re.M | re.S):
|
||||||
|
chunks.setdefault(m.group(1).strip(), []).append(m.group(2))
|
||||||
|
|
||||||
|
def need(h):
|
||||||
|
if h not in chunks:
|
||||||
|
raise ValueError(f"{skill_id}: missing section '## {h}'")
|
||||||
|
return chunks[h][0]
|
||||||
|
|
||||||
|
fields = {"name": name_m.group(1).strip()}
|
||||||
|
forbidden = None
|
||||||
|
for heading, key in SECTIONS:
|
||||||
|
val = _join(need(heading).split("\n"))
|
||||||
|
if key == "__forbidden":
|
||||||
|
forbidden = val
|
||||||
|
else:
|
||||||
|
fields[key] = val
|
||||||
|
|
||||||
|
tics = []
|
||||||
|
for line in need("Verbal tics").split("\n"):
|
||||||
|
m = re.match(r"^\s*(\d+)\.\s+(.*)$", line)
|
||||||
|
if m:
|
||||||
|
tics.append((int(m.group(1)), m.group(2).strip()))
|
||||||
|
|
||||||
|
voices, gloss = {}, {}
|
||||||
|
for kind in ("Success", "Failure", "Passive"):
|
||||||
|
sec = need(kind)
|
||||||
|
q = [l for l in sec.split("\n") if l.strip().startswith(">")]
|
||||||
|
if not q:
|
||||||
|
raise ValueError(f"{skill_id}: '### {kind}' has no quoted line")
|
||||||
|
voices[kind.lower()] = _join([re.sub(r"^\s*>\s?", "", l) for l in q])
|
||||||
|
g = re.search(r"^\*(.+)\*$", sec.strip(), re.M)
|
||||||
|
if g:
|
||||||
|
gloss[kind.lower()] = g.group(1).strip()
|
||||||
|
|
||||||
|
fm = re.search(r"(\d+)\s+per hour", need("Firing frequency"))
|
||||||
|
if not fm:
|
||||||
|
raise ValueError(f"{skill_id}: could not read firing frequency")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"_header": "SKILL",
|
||||||
|
"fields": fields,
|
||||||
|
"tics": tics,
|
||||||
|
"voices": voices,
|
||||||
|
"voice_gloss": gloss,
|
||||||
|
"firing": int(fm.group(1)),
|
||||||
|
"forbidden": forbidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_json():
|
||||||
|
with open(JSON_PATH, encoding="utf-8") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
|
||||||
|
|
||||||
|
def md_path(skill_id):
|
||||||
|
return os.path.join(VOICES_DIR, f"{skill_id}.md")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_to_md(data):
|
||||||
|
os.makedirs(VOICES_DIR, exist_ok=True)
|
||||||
|
for s in data["skills"]:
|
||||||
|
with open(md_path(s["id"]), "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(to_md(s))
|
||||||
|
print(f"wrote writing/voices/{s['id']}.md")
|
||||||
|
|
||||||
|
|
||||||
|
def rebuilt(s):
|
||||||
|
with open(md_path(s["id"]), encoding="utf-8") as fh:
|
||||||
|
d = from_md(fh.read(), s["id"])
|
||||||
|
return d, voicelib.emit_notes(d)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_check(data):
|
||||||
|
problems = []
|
||||||
|
for s in data["skills"]:
|
||||||
|
if not os.path.exists(md_path(s["id"])):
|
||||||
|
problems.append(f"{s['id']}: writing/voices/{s['id']}.md is missing")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
d, notes = rebuilt(s)
|
||||||
|
except Exception as e:
|
||||||
|
problems.append(f"{s['id']}: {e}")
|
||||||
|
continue
|
||||||
|
if notes != s["notes"]:
|
||||||
|
problems.append(f"{s['id']}: prose differs from skill_bible.json notes")
|
||||||
|
if d["fields"]["name"] != s["displayName"]:
|
||||||
|
problems.append(f"{s['id']}: title differs from displayName")
|
||||||
|
if d["fields"]["domain"] != s["domain"]:
|
||||||
|
problems.append(f"{s['id']}: domain differs from the JSON domain field")
|
||||||
|
if d["firing"] != s.get("targetFiringsPerHour"):
|
||||||
|
problems.append(
|
||||||
|
f"{s['id']}: firing frequency {d['firing']} != "
|
||||||
|
f"targetFiringsPerHour {s.get('targetFiringsPerHour')}")
|
||||||
|
for p in problems:
|
||||||
|
print("DRIFT:", p)
|
||||||
|
if problems:
|
||||||
|
print(f"\n{len(problems)} problem(s). Run --to-json (prose changed) "
|
||||||
|
f"or --to-md (machine fields changed).")
|
||||||
|
return 1
|
||||||
|
print(f"in sync — {len(data['skills'])} voice sheets")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_to_json(data):
|
||||||
|
changed = []
|
||||||
|
for s in data["skills"]:
|
||||||
|
d, notes = rebuilt(s)
|
||||||
|
if (notes != s["notes"] or d["fields"]["name"] != s["displayName"]
|
||||||
|
or d["fields"]["domain"] != s["domain"]):
|
||||||
|
changed.append(s["id"])
|
||||||
|
s["notes"] = notes
|
||||||
|
s["displayName"] = d["fields"]["name"]
|
||||||
|
s["domain"] = d["fields"]["domain"]
|
||||||
|
s["targetFiringsPerHour"] = d["firing"]
|
||||||
|
with open(JSON_PATH, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(data, fh, indent=2, ensure_ascii=False)
|
||||||
|
fh.write("\n")
|
||||||
|
print("updated skill_bible.json" + (f" — changed: {', '.join(changed)}" if changed else " — no prose changes"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
g = ap.add_mutually_exclusive_group(required=True)
|
||||||
|
g.add_argument("--to-md", action="store_true")
|
||||||
|
g.add_argument("--to-json", action="store_true")
|
||||||
|
g.add_argument("--check", action="store_true")
|
||||||
|
a = ap.parse_args()
|
||||||
|
data = load_json()
|
||||||
|
if a.to_md:
|
||||||
|
cmd_to_md(data)
|
||||||
|
elif a.to_json:
|
||||||
|
cmd_to_json(data)
|
||||||
|
else:
|
||||||
|
sys.exit(cmd_check(data))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
"""Parse and re-emit the Skill Bible prose that lives in skill_bible.json.
|
||||||
|
|
||||||
|
The `notes` field of each skill is a plain-text sheet with a fixed shape. This
|
||||||
|
module converts between that string and a structured dict, losslessly, so the
|
||||||
|
prose can live in writing/voices/*.md and still be folded back into the JSON.
|
||||||
|
|
||||||
|
Round-tripping is the whole contract: notes -> dict -> notes must be identity
|
||||||
|
for every skill in the bible, and check_roundtrip() asserts exactly that.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
PROSE_KEYS = [
|
||||||
|
"name",
|
||||||
|
"domain",
|
||||||
|
"what it wants for you",
|
||||||
|
"what it's wrong about",
|
||||||
|
"how it addresses you",
|
||||||
|
"register",
|
||||||
|
]
|
||||||
|
# "verbal tics" is handled separately (it carries bullets)
|
||||||
|
TAIL_KEYS = ["what it never does", "opposed to", "allied with"]
|
||||||
|
|
||||||
|
VOICE_RE = re.compile(r'^(SUCCESS|FAILURE|PASSIVE) VOICE(?: — (.*?))?: (.*)$')
|
||||||
|
FIRING_RE = re.compile(r'^Firing frequency target \(per hour\): (\d+)$')
|
||||||
|
FORBID_RE = re.compile(r'^Domains it must NOT comment on: (.*)$')
|
||||||
|
BULLET_RE = re.compile(r'^\* \((\d+)\) (.*)$')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_notes(notes):
|
||||||
|
"""notes string -> dict. Raises on anything unrecognised."""
|
||||||
|
out = {"fields": {}, "tics": [], "voices": {}, "voice_gloss": {}}
|
||||||
|
lines = notes.split("\n")
|
||||||
|
i = 0
|
||||||
|
if lines[i].strip() != "SKILL":
|
||||||
|
raise ValueError("expected a leading SKILL line")
|
||||||
|
out["_header"] = lines[i]
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
|
||||||
|
if line.strip() == "":
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = BULLET_RE.match(line)
|
||||||
|
if m:
|
||||||
|
out["tics"].append((int(m.group(1)), m.group(2)))
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = VOICE_RE.match(line)
|
||||||
|
if m:
|
||||||
|
kind = m.group(1).lower()
|
||||||
|
out["voices"][kind] = m.group(3)
|
||||||
|
if m.group(2):
|
||||||
|
out["voice_gloss"][kind] = m.group(2)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = FIRING_RE.match(line)
|
||||||
|
if m:
|
||||||
|
out["firing"] = int(m.group(1))
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = FORBID_RE.match(line)
|
||||||
|
if m:
|
||||||
|
out["forbidden"] = m.group(1)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = re.match(r"^([a-z][a-z '\-]*): ?(.*)$", line)
|
||||||
|
if m and m.group(1) in PROSE_KEYS + TAIL_KEYS + ["verbal tics"]:
|
||||||
|
out["fields"][m.group(1)] = m.group(2)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise ValueError(f"unrecognised line: {line!r}")
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def emit_notes(d):
|
||||||
|
"""dict -> notes string. Inverse of parse_notes."""
|
||||||
|
L = [d.get("_header", "SKILL")]
|
||||||
|
for k in PROSE_KEYS:
|
||||||
|
L.append(f"{k}: {d['fields'][k]}")
|
||||||
|
L.append("verbal tics:")
|
||||||
|
for n, text in d["tics"]:
|
||||||
|
L.append(f"* ({n}) {text}")
|
||||||
|
for k in TAIL_KEYS:
|
||||||
|
L.append(f"{k}: {d['fields'][k]}")
|
||||||
|
L.append("")
|
||||||
|
for kind in ("success", "failure", "passive"):
|
||||||
|
gloss = d["voice_gloss"].get(kind)
|
||||||
|
label = f"{kind.upper()} VOICE"
|
||||||
|
if gloss:
|
||||||
|
L.append(f"{label} — {gloss}: {d['voices'][kind]}")
|
||||||
|
else:
|
||||||
|
L.append(f"{label}: {d['voices'][kind]}")
|
||||||
|
L.append("")
|
||||||
|
L.append(f"Firing frequency target (per hour): {d['firing']}")
|
||||||
|
L.append(f"Domains it must NOT comment on: {d['forbidden']}")
|
||||||
|
return "\n".join(L)
|
||||||
|
|
||||||
|
|
||||||
|
def check_roundtrip(skills):
|
||||||
|
"""Assert notes -> dict -> notes is identity for every skill."""
|
||||||
|
bad = []
|
||||||
|
for s in skills:
|
||||||
|
try:
|
||||||
|
again = emit_notes(parse_notes(s["notes"]))
|
||||||
|
except Exception as e:
|
||||||
|
bad.append((s["id"], f"parse error: {e}"))
|
||||||
|
continue
|
||||||
|
if again != s["notes"]:
|
||||||
|
bad.append((s["id"], "round-trip differs"))
|
||||||
|
return bad
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Glossary
|
||||||
|
|
||||||
|
In-world and project terms that come up while writing. World history, factions and the timeline
|
||||||
|
live in the Obsidian vault — `[[Lorebook]]`, `[[Timeline notes]]`,
|
||||||
|
`[[Divergence Point Analysis]]` — and this file deliberately does not duplicate them. It covers
|
||||||
|
the vocabulary you need to read a `.yarn` file and a design note without stopping.
|
||||||
|
|
||||||
|
## The project's own terms
|
||||||
|
|
||||||
|
**Voice** — one of the eleven skills. A character in the protagonist's head, not a stat. See `voices/`.
|
||||||
|
|
||||||
|
**Candidate** — one of three live readings of who the protagonist is: the Made Asset, the
|
||||||
|
Journalist, the Inheritor. They are not mutually exclusive and **never resolve**.
|
||||||
|
|
||||||
|
**Lean** — `$self_lean_<candidate>`. A counter of how far play has tilted toward one reading.
|
||||||
|
Tracks *belief*, not truth, which is why a wrong deduction still moves it.
|
||||||
|
|
||||||
|
**Clue** — a registry entry in `Assets/Candidates/candidates.json`. Each candidate has exactly
|
||||||
|
four: one object, one testimony, one document, one intuition.
|
||||||
|
|
||||||
|
**Trace channel** — where a clue physically comes from: physical, testimonial, behavioural,
|
||||||
|
documentary. Used to keep acquisition routes genuinely independent.
|
||||||
|
|
||||||
|
**Wrong Truth** — a failed check that yields a confident, coherent, false conclusion, stated
|
||||||
|
with no hedge in the prose. The workhorse failure of this game.
|
||||||
|
|
||||||
|
**Colour check / gate check** — a check written for texture versus one that controls access to
|
||||||
|
a route. 80–90% should be colour.
|
||||||
|
|
||||||
|
**Bottleneck** — a point every playthrough must pass. Clues required to pass one need multiple
|
||||||
|
independent routes.
|
||||||
|
|
||||||
|
**Detour / jump** — Yarn's two ways to move between nodes; the first returns, the second does not.
|
||||||
|
|
||||||
|
**Node group** — several nodes sharing a `title:`, with `when:` conditions selecting between them.
|
||||||
|
|
||||||
|
**Thread 2 / Thread 9** — two lore threads designed never to resolve. Do not map candidate
|
||||||
|
material onto them.
|
||||||
|
|
||||||
|
**Fugue** — the protagonist's amnesia. Dissociative, and specifically about their own identity
|
||||||
|
rather than an external event. The indeterminacy is the design, not a puzzle with a hidden answer.
|
||||||
|
|
||||||
|
## The cast so far
|
||||||
|
|
||||||
|
| Name | Where | Sheet |
|
||||||
|
|---|---|---|
|
||||||
|
| The Bartender | the bar, SC-101 | [`characters/bartender.md`](characters/bartender.md) |
|
||||||
|
| Chevalier Cassian Thal | the table, SC-101 | [`characters/chevalier-cassian-thal.md`](characters/chevalier-cassian-thal.md) |
|
||||||
|
| Bradford Kane | the table, SC-101 | [`characters/bradford-kane.md`](characters/bradford-kane.md) |
|
||||||
|
| Mr. Doss | referenced only, never seen | — |
|
||||||
|
|
||||||
|
## Scene ids
|
||||||
|
|
||||||
|
`SC101` — The Card at the Desk. The opening; playable end to end.
|
||||||
|
`SC102` — The figure at the head of the table stands up. A one-line stub.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Writing — start here
|
||||||
|
|
||||||
|
This directory is the writing surface for **Nightclub Arcadia**. Everything a session needs
|
||||||
|
to write or revise dialogue lives here or in `Assets/Dialogue/`. You should not need to open
|
||||||
|
Unity, edit C#, or touch a `.asset` file to write a scene, a character, or a branch. If a
|
||||||
|
writing task seems to require any of those, that is a pipeline bug — say so rather than
|
||||||
|
working around it.
|
||||||
|
|
||||||
|
## Read in this order
|
||||||
|
|
||||||
|
| # | File | What it gives you |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | [`STYLE.md`](STYLE.md) | The register, the five tone moves, the ban list, the rules that govern branching. **Re-read before every session.** |
|
||||||
|
| 2 | [`YARN-PRIMER.md`](YARN-PRIMER.md) | The one page of Yarn syntax you actually need. |
|
||||||
|
| 3 | [`voices/`](voices/) | The eleven skill voices, one sheet each. Who they are and how they talk. |
|
||||||
|
| 4 | [`characters/`](characters/) | The named NPCs. |
|
||||||
|
| 5 | [`scenes/`](scenes/) | Per-scene briefs — intent, beats, constraints. Not the dialogue. |
|
||||||
|
| 6 | [`GLOSSARY.md`](GLOSSARY.md) | In-world terms, and where the deep lore lives. |
|
||||||
|
|
||||||
|
## Where the words actually go
|
||||||
|
|
||||||
|
Playable dialogue lives in **`NightclubArcadia/Assets/Dialogue/`**, not here:
|
||||||
|
|
||||||
|
```
|
||||||
|
Assets/Dialogue/
|
||||||
|
├── Common.yarn every <<declare>>, project-wide. Never played. Add new flags here.
|
||||||
|
├── Commentary.yarn passive skill barks, one node per (context, voice)
|
||||||
|
├── Scenes/ SC101, SC102, Debug
|
||||||
|
├── Characters/ Bartender, Bradford Kane, Chevalier Cassian Thal
|
||||||
|
└── Objects/ Chair
|
||||||
|
```
|
||||||
|
|
||||||
|
A new `.yarn` file anywhere under `Assets/Dialogue/` is picked up automatically — the Yarn
|
||||||
|
project globs `**/*.yarn`. **Adding a scene or a character needs no registration step.**
|
||||||
|
|
||||||
|
This directory holds the *context*; `Assets/Dialogue/` holds the *content*. Keep it that way:
|
||||||
|
never paste dialogue into a sheet here, and never paste a character bio into a `.yarn` file.
|
||||||
|
|
||||||
|
## Check your work before handing it back
|
||||||
|
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tools/YarnCheck && dotnet run -- ../../NightclubArcadia/Assets/Dialogue
|
||||||
|
```
|
||||||
|
|
||||||
|
That compiles every Yarn file and reports errors with line numbers. It needs only the .NET 9
|
||||||
|
SDK — no Unity, no licence, no Editor. Run it after any dialogue change.
|
||||||
|
|
||||||
|
You can also *play* a node from the terminal, choosing options by number:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run -- ../../NightclubArcadia/Assets/Dialogue Bartender_Talk 2 2 0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Leftover picks restart the node, which models the player walking away and coming back.
|
||||||
|
|
||||||
|
> **One thing that will confuse you.** YarnCheck stubs the engine's commands, so `<<check>>`
|
||||||
|
> never runs and `$check_result` is always `false`. **Every check-gated branch will show you
|
||||||
|
> its failure side.** That is the harness, not a bug in your writing. To read the success
|
||||||
|
> prose, read the file.
|
||||||
|
|
||||||
|
If you edited a voice sheet in `voices/`, also run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/writing/sync_voices.py --check
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is yours, and what is not
|
||||||
|
|
||||||
|
**Yours** — edit freely: everything in `writing/`, every `.yarn` file, the `<<declare>>` lines
|
||||||
|
in `Common.yarn`, and the prose fields of `Assets/Candidates/candidates.json`.
|
||||||
|
|
||||||
|
**Not yours** — ask an engineer: any `.cs` file, anything under `Assets/Editor/`, any
|
||||||
|
`.asset` / `.prefab` / `.unity` / `.meta` file, and `ProjectSettings/`. The *implementations*
|
||||||
|
of Yarn commands are code; *using* them is writing.
|
||||||
|
|
||||||
|
## The deeper design material
|
||||||
|
|
||||||
|
The full design documents live in Obsidian, not in this repo, and remain the authoring home
|
||||||
|
for design work:
|
||||||
|
|
||||||
|
`~/Obsidian-Vaults/obsidian-vault/Projects/Active/NightclubArcadia/`
|
||||||
|
|
||||||
|
- `[[Mystery Writing Handbook]]` — the full reference. `STYLE.md` is a dialogue-facing digest of it.
|
||||||
|
- `[[Skill System Draft]]` — the Skill Bible. `voices/` is generated from the shipped subset.
|
||||||
|
- `[[Candidate System — Design Explanation]]` — the three identity readings.
|
||||||
|
- `[[Lorebook]]`, `[[Timeline notes]]`, `[[Divergence Point Analysis]]` — world and history.
|
||||||
|
- `[[Character Sheet Handbook]]` — the full character schema. `characters/_TEMPLATE.md` is its
|
||||||
|
dialogue-facing subset.
|
||||||
|
|
||||||
|
> **`[[Ground Truth]]` is the solution document. Do not copy it, quote it, or summarise it
|
||||||
|
> into this repo — not into a sheet, not into a comment, not into a commit message.** The
|
||||||
|
> game's central premise is that the protagonist's identity never resolves; a copy of the
|
||||||
|
> answer sitting in a git history is how that leaks. If you need to know whether something
|
||||||
|
> contradicts the solution, ask, and keep the answer out of the files.
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
# Style — the register and the rules
|
||||||
|
|
||||||
|
Re-read this before every writing session. It is the anchor against drift, which is the
|
||||||
|
characteristic failure of a solo writer working across a year.
|
||||||
|
|
||||||
|
This is a **dialogue-facing digest**. The full argument for each rule is in
|
||||||
|
`[[Mystery Writing Handbook]]` in the Obsidian vault; section numbers below point into it.
|
||||||
|
Where this file and the handbook disagree, the handbook wins — and this file should be fixed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The register, in one paragraph
|
||||||
|
|
||||||
|
Second person, present tense, unreliable. The narration is a committee — eleven voices with
|
||||||
|
opinions, some of them wrong — and the player is inside the argument rather than being told
|
||||||
|
its result. Concrete over abstract, always. Roughly seventy percent plain, sincere prose, so
|
||||||
|
that the flourishes land when they come. Sentimental underneath the cynicism; that is what
|
||||||
|
makes the cynicism bearable.
|
||||||
|
|
||||||
|
## 2. The five tone moves (§6.4)
|
||||||
|
|
||||||
|
Learn the moves, use your own words. Do not imitate Disco Elysium's prose — imitate its method.
|
||||||
|
|
||||||
|
1. **Concrete over abstract.** Never "the room felt neglected." Name the object that proves it.
|
||||||
|
The failure mode of introspective writing is abstraction; the fix is always a physical particular.
|
||||||
|
2. **Bathos with a return trip.** Rise to something genuinely grand, puncture it with the mundane —
|
||||||
|
*then come back and mean it anyway*. The puncture alone is just irony, and irony alone is cheap.
|
||||||
|
The return is what makes it humane. This is the most-imitated and least-understood DE move.
|
||||||
|
3. **Mock, then love.** Every character gets ridiculed by some voice in your head, and every
|
||||||
|
character then gets one moment where the ridicule is revealed as inadequate to them. No
|
||||||
|
character exists only as a joke.
|
||||||
|
4. **The world speaks through failure.** Broken things describe the society that broke them.
|
||||||
|
Describe what the rent did to the stairwell instead of explaining the political economy.
|
||||||
|
5. **Second person, present tense, unreliable.** "You" and "now" produce intimacy and complicity.
|
||||||
|
|
||||||
|
### The ban list
|
||||||
|
|
||||||
|
- **Quirk with no cost.** A voice that is only funny is decoration.
|
||||||
|
- **Cynicism with no warmth.**
|
||||||
|
- **Everything is a bit.** Straight, plain, sincere sentences are what make the flourishes work.
|
||||||
|
- **Trauma as texture.** If suffering is described, someone in the text takes it seriously.
|
||||||
|
- **Explaining the joke, or the theme.** If the thematic statement appears verbatim in dialogue, cut it.
|
||||||
|
|
||||||
|
### Length discipline
|
||||||
|
|
||||||
|
| Kind of line | Length |
|
||||||
|
|---|---|
|
||||||
|
| Passive skill line | 1–3 sentences |
|
||||||
|
| Success text | 2–5 sentences |
|
||||||
|
| Opening frame | 2–4 sentences |
|
||||||
|
|
||||||
|
If you are writing a paragraph, you are writing an essay, and the player will skim it — which
|
||||||
|
trains them to skim everything else. Long passages are earned at high skill ranks, nowhere else.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. How voices behave
|
||||||
|
|
||||||
|
A skill is a **character**, not a stat. It wants something for you, and it is wrong about
|
||||||
|
something. Both halves matter: the wrongness is what makes it a voice rather than a hint system.
|
||||||
|
|
||||||
|
- **Voices advocate; they do not report.** A line is that voice's own biased reading, even when
|
||||||
|
it happens to be right. No voice ever states a candidate identity as settled fact.
|
||||||
|
- **Respect the forbidden domains.** Each sheet in `voices/` ends with "domains it must not
|
||||||
|
comment on." PROVENANCE does not discuss appetite. This is characterisation, not bookkeeping.
|
||||||
|
- **Opposed pairs are a writing engine.** Fire both halves of a pair with contradictory readings
|
||||||
|
and you get friction for free. You never have to invent conflict; the cast contains it.
|
||||||
|
- **Cap introductions at three voices per scene.** An opening is many players' first contact with
|
||||||
|
the system, and fewer, stronger first impressions beat a crowded debut.
|
||||||
|
*(From `[[000_OpeningSceneTemplate]]` §3. SC-101 deliberately breaks this with four — one per
|
||||||
|
pair, because each candidate needs its intuition channel present from the start. That is a
|
||||||
|
logged, scene-specific exception. Do not copy it as precedent, and do not "correct" SC-101 down
|
||||||
|
to three.)*
|
||||||
|
- **Passives carry the personality.** Roughly 60–70% of all skill text should be passive. They
|
||||||
|
cost the player nothing and are the main delivery system for character.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Checks (§4.4)
|
||||||
|
|
||||||
|
Two kinds, and knowing which you are writing is the whole discipline:
|
||||||
|
|
||||||
|
| | **Colour check** | **Gate check** |
|
||||||
|
|---|---|---|
|
||||||
|
| Purpose | Voice, texture, insight | Controls access to a clue or route |
|
||||||
|
| Share of all checks | 80–90% | 10–20% |
|
||||||
|
| Failure produces | Different *content* | Different *route* |
|
||||||
|
| Redundancy required | None | Always ≥2 independent routes |
|
||||||
|
|
||||||
|
> **A failed gate never removes content. It replaces one route with another.**
|
||||||
|
|
||||||
|
**The two-outcome test — apply before writing any check.** Write the one-line summary of the
|
||||||
|
success and the one-line summary of the failure. If the failure summary is *"the player doesn't
|
||||||
|
get the thing"*, delete the check.
|
||||||
|
|
||||||
|
Other rules that bind:
|
||||||
|
|
||||||
|
- **Never roll for what a competent detective would just do.** If failing is boring, don't roll.
|
||||||
|
Roll only where *both* outcomes are content you want to write.
|
||||||
|
- **Rate-limit active checks** to roughly one per 3–5 minutes of play. Prose rhythm dies under
|
||||||
|
constant dice.
|
||||||
|
- **Use the band names,** not raw numbers: `Trivial` 8, `Routine` 11, `Hard` 14, `Specialist` 17,
|
||||||
|
`BuildDefining` 20. No in-between values.
|
||||||
|
|
||||||
|
### The five productive failures (§4.5)
|
||||||
|
|
||||||
|
Never write a failure that means "nothing happens." Every failed check produces one of:
|
||||||
|
|
||||||
|
| Type | What happens | Best for |
|
||||||
|
|---|---|---|
|
||||||
|
| **Wrong Truth** | A confident, coherent, *false* conclusion | Reason/perception checks |
|
||||||
|
| **Cost** | You get it, and pay — reputation, a relationship, a secret, time | Social checks |
|
||||||
|
| **Detour** | This route closes, an uglier one opens | Gate checks |
|
||||||
|
| **Character** | You learn nothing about the case and something about yourself | Self checks |
|
||||||
|
| **Delayed** | Noted now, resurfaces later at a worse moment | Act transitions |
|
||||||
|
|
||||||
|
Rough distribution across the game: 30% Wrong Truth, 25% Cost, 20% Detour, 15% Character,
|
||||||
|
10% Delayed. Wrong Truth is the workhorse of a mystery and is badly underused in most games.
|
||||||
|
|
||||||
|
### The Wrong Truth rule — the one that is easiest to get wrong
|
||||||
|
|
||||||
|
**A voice that fails does not signal that it failed.** It states its wrong conclusion with
|
||||||
|
exactly the same confidence, cadence and structure as a correct one. The player learns it was
|
||||||
|
wrong through contradiction, later.
|
||||||
|
|
||||||
|
The honest version of the trick: the *roll* is visibly failed in the UI. The player knows a
|
||||||
|
roll failed; they do not know which part of what follows is contaminated. **The UI banner is
|
||||||
|
the only tell. Never put a hedge, a wobble, or a wink in the prose.**
|
||||||
|
|
||||||
|
Compare the two PROVENANCE samples in `voices/provenance.md` — same cadence, same calm, one
|
||||||
|
unearned verb. That is the target.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Branching shapes (§2.7)
|
||||||
|
|
||||||
|
Three shapes. Default to the first.
|
||||||
|
|
||||||
|
**A. Hub-and-spoke — ~80% of conversations.** Topics are spokes that return to a hub.
|
||||||
|
Reconvergence is structural, so you get it for free, and cost is linear in topics rather than
|
||||||
|
exponential. `Chair.yarn` is the worked example in this project.
|
||||||
|
|
||||||
|
**B. Short branching tree — ~15%, for confrontations.** Depth ≤ 3. Always reconverge inside the
|
||||||
|
scene. Track the outcome flag, not the path.
|
||||||
|
|
||||||
|
**C. True divergence — 3–5 moments in the entire game.** Endings, the culprit confrontation, one
|
||||||
|
or two irreversible choices. Budget these explicitly.
|
||||||
|
|
||||||
|
**The 3-deep cap.** No conditional nested more than three levels. If you need four, you have a
|
||||||
|
state that should be a flag rather than a path.
|
||||||
|
|
||||||
|
### Node naming
|
||||||
|
|
||||||
|
Existing conventions in this project, which new content should match:
|
||||||
|
|
||||||
|
| Pattern | Used for |
|
||||||
|
|---|---|
|
||||||
|
| `<Character>_Talk` | a character's entry node (`Bartender_Talk`) |
|
||||||
|
| `SC###_Hook_<Subject>` | a scene beat or sub-conversation |
|
||||||
|
| `Commentary_<Context>_<voice_id>` | a passive bark |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Flags and variables
|
||||||
|
|
||||||
|
**Every variable must be `<<declare>>`d in `Common.yarn`.** That is what turns `$reputaton`
|
||||||
|
into a compile error instead of a silently-created second variable. Adding a flag is a writing
|
||||||
|
action — you do not need an engineer.
|
||||||
|
|
||||||
|
Families that are load-bearing, two of them lint-enforced against the candidate registry:
|
||||||
|
|
||||||
|
| Family | Meaning | Enforced |
|
||||||
|
|---|---|---|
|
||||||
|
| `$self_lean_<candidate_id>` | identity lean | yes — id must exist in `candidates.json` |
|
||||||
|
| `$found_<clue_id>` | clue coverage | yes — id must exist in `candidates.json` |
|
||||||
|
| `$visited_<location_id>` | first arrival at a location | convention |
|
||||||
|
| `$errand_<errand_id>` | errand reached a state that matters | convention |
|
||||||
|
| `$seen_<scene_id>` | scene witnessed | convention |
|
||||||
|
| `$spoke_to_<character>` | first conversation happened | convention |
|
||||||
|
| `$check_*` | written by `<<check>>` | **never `<<set>>` by hand** |
|
||||||
|
|
||||||
|
### The candidate rules — these are hard constraints
|
||||||
|
|
||||||
|
The protagonist's identity has three live readings and **never resolves**. Three integers track
|
||||||
|
belief, not truth.
|
||||||
|
|
||||||
|
1. **Never compare one lean against another.** Not in Yarn, not in prose, not in a debug line.
|
||||||
|
Comparing a lean to a *constant* is fine and intended. `$self_lean_a > $self_lean_b` must
|
||||||
|
not exist anywhere.
|
||||||
|
2. **No ground-truth field.** Nothing named `isTrue`, `actualIdentity`, `canonical`, or `weight`.
|
||||||
|
3. **Clue availability is exploration-gated, never skill-gated.** An `unlockFlag` names a
|
||||||
|
`$visited_*` or `$errand_*` variable. A clue-bearing node never sits behind a `skill_rank(` guard.
|
||||||
|
4. **Evidence balance.** Each candidate carries exactly one object, one testimony, one document,
|
||||||
|
and one intuition clue. Do not stack types, or trust becomes an artefact of imbalance.
|
||||||
|
5. **Lean is belief, so a Wrong Truth still writes lean.** Being wrong moves the needle. That is
|
||||||
|
the point.
|
||||||
|
|
||||||
|
### Two threads that never resolve
|
||||||
|
|
||||||
|
The project has two permanently unresolved lore threads, referred to as **Thread 2** and
|
||||||
|
**Thread 9**. Candidate material must stay structurally separate from them: do not resolve
|
||||||
|
them, do not map a candidate clue onto them. If you find yourself needing to know what they
|
||||||
|
contain in order to write a line, you are writing the wrong line.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Flow control
|
||||||
|
|
||||||
|
`<<detour>>` returns to the caller. `<<jump>>` does not, and clears the return stack.
|
||||||
|
|
||||||
|
- A node started directly by an interactable has nothing waiting on it, so its branches
|
||||||
|
`<<jump>>`, and each destination calls `<<enter_environment>>` itself.
|
||||||
|
- A sub-conversation that must resume its caller uses `<<detour>>` — and **never** `<<jump>>`
|
||||||
|
inside it, which would strand the caller forever.
|
||||||
|
- A menu that loops uses `<<jump>>` back to its own hub, which replaces the current node and
|
||||||
|
pushes nothing.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Writing order for a scene (§5.5)
|
||||||
|
|
||||||
|
1. Fill the scene brief header — purpose and critical outputs. **Stop.** Confirm the clue routes
|
||||||
|
are registered in `Assets/Candidates/candidates.json`.
|
||||||
|
2. Write the beats as one-line summaries.
|
||||||
|
3. Write the checks table — *both* outcomes as summaries, before any prose.
|
||||||
|
4. Now write prose, in this order: opening frame → success texts → failure texts → passives → barks.
|
||||||
|
5. Update clue `status` in `candidates.json`.
|
||||||
|
|
||||||
|
Do not implement in engine until the act is drafted. Implementing mid-draft is the biggest time
|
||||||
|
sink in solo narrative work, because every structural revision then costs double.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. The voice sheet
|
||||||
|
|
||||||
|
Five sentences to be proud of, five to cut, three adjectives, one sentence on what the game
|
||||||
|
feels about its world. This is the anchor §6.5 asks for and it is cheaper than re-reading your
|
||||||
|
own draft.
|
||||||
|
|
||||||
|
**Three adjectives for the register:** *(unset — decide these and stop leaving the field open)*
|
||||||
|
|
||||||
|
**What the game feels about its world:** *(unset)*
|
||||||
|
|
||||||
|
**Five sentences to be proud of** — provisional, taken from shipped SC-101 and the Skill Bible,
|
||||||
|
to be replaced with chosen ones:
|
||||||
|
|
||||||
|
> The mahogany doors are too heavy for the hinges someone recently, badly, tightened — you feel
|
||||||
|
> the extra half-second of resistance before either side gives.
|
||||||
|
|
||||||
|
> Someone left a card with your name on it at the registration desk three hours before you
|
||||||
|
> decided to come. You don't remember deciding.
|
||||||
|
|
||||||
|
> Cigar smoke, spilled aperitif, and under it — cold, sharp — the ammonia bite of a floor mopped
|
||||||
|
> in a hurry, love. Someone's trying to make this room smell like money. It smells like panic
|
||||||
|
> with good cologne on top.
|
||||||
|
|
||||||
|
> A line that begins nowhere is a line somebody drew.
|
||||||
|
|
||||||
|
> Her hands are wrong for that coat. It's somebody's good coat, and it isn't hers.
|
||||||
|
|
||||||
|
**Five to cut:** *(unset — write these next session; the cut list is the more useful half,
|
||||||
|
because it names the specific temptations this project keeps giving in to)*
|
||||||
|
|
||||||
|
> This section is deliberately incomplete. `[[NightclubArcadia]]` has carried "Write the one-page
|
||||||
|
> Voice Sheet" as an open action for a while, and `[[000_OpeningSceneTemplate]]` records tone
|
||||||
|
> checks as **BLOCKED** on it — the SC-101 spec could not honestly certify its own tone field.
|
||||||
|
> Filling the four unset fields above unblocks that. It is a half-hour of decisions, not a
|
||||||
|
> writing project.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
id: <kebab-case-id>
|
||||||
|
name: <exact string used as the Yarn speaker prefix>
|
||||||
|
chr: CHR-### # the vault sheet this projects from, if one exists
|
||||||
|
yarn_file: Assets/Dialogue/Characters/<File>.yarn
|
||||||
|
status: planned # planned | drafted | in-engine | tested
|
||||||
|
scenes: [SC101]
|
||||||
|
---
|
||||||
|
|
||||||
|
# <Name>
|
||||||
|
|
||||||
|
> **This is the dialogue-facing sheet.** The full character record — placement in the Lorebook,
|
||||||
|
> faction row, the two truths, sensitivity check, continuity hooks — lives in the vault at
|
||||||
|
> `[[Character Sheet Handbook]]`, one file per character. Fill that one *first* for any figure
|
||||||
|
> who touches real history or a faction. This sheet exists so a writing session can pick the
|
||||||
|
> character up and write them without loading all of it.
|
||||||
|
|
||||||
|
## Who they are
|
||||||
|
|
||||||
|
One paragraph. What they want *tonight*, in this room — not their life goal.
|
||||||
|
|
||||||
|
## How they speak
|
||||||
|
|
||||||
|
Register in three adjectives. Sentence length. Do they contract? Do they use the player's name,
|
||||||
|
and which form — `{$player_name}`, `{$player_name_full}`, or `{$player_alias}`? Anything a
|
||||||
|
reader could imitate.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
At most two. More than two is a costume, not a voice.
|
||||||
|
|
||||||
|
## What they never say or do
|
||||||
|
|
||||||
|
The negative constraints. Usually more useful than the positive ones.
|
||||||
|
|
||||||
|
## What they know
|
||||||
|
|
||||||
|
One row per thing the player can learn from them. "Reveals when" names the actual condition;
|
||||||
|
"records" names the flag that remembers it.
|
||||||
|
|
||||||
|
| They know | True? | Reveals when | Records |
|
||||||
|
|---|---|---|---|
|
||||||
|
| | | | |
|
||||||
|
|
||||||
|
## What they are wrong about
|
||||||
|
|
||||||
|
Where their confidence outruns their information. This is what makes them a source of Wrong
|
||||||
|
Truths rather than an oracle.
|
||||||
|
|
||||||
|
## Their read on the protagonist
|
||||||
|
|
||||||
|
Which candidate reading, if any, they push — and what happens if the player agrees or refuses.
|
||||||
|
Remember they advocate; they never state an identity as settled fact.
|
||||||
|
|
||||||
|
## One line that is 100% them
|
||||||
|
|
||||||
|
Written now, before a scene needs it.
|
||||||
|
|
||||||
|
> "..."
|
||||||
|
|
||||||
|
## Open threads
|
||||||
|
|
||||||
|
What is unwritten, unresolved, or deliberately left open. Link the vault note if there is one.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
id: bartender
|
||||||
|
name: Bartender
|
||||||
|
chr: —
|
||||||
|
yarn_file: Assets/Dialogue/Characters/Bartender.yarn
|
||||||
|
status: drafted
|
||||||
|
scenes: [SC101]
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Bartender
|
||||||
|
|
||||||
|
## Who they are
|
||||||
|
|
||||||
|
He is behind the bar at a conference that is pretending to be a party, and he has been behind
|
||||||
|
it long enough to have opinions he will not offer. He recognises the protagonist. He is certain
|
||||||
|
he has served them before, with Mr. Doss, a few days ago — and he says so casually, the way you
|
||||||
|
mention the weather, without any idea that he has just detonated something. What he wants
|
||||||
|
tonight is an ordinary shift. He is not going to get one.
|
||||||
|
|
||||||
|
FACEWORK reads him as a watcher: a man who tends the bar *and* observes, and reports back. That
|
||||||
|
reading is FACEWORK's, and FACEWORK advocates rather than reports — it may well be wrong. Do
|
||||||
|
not resolve it in his prose.
|
||||||
|
|
||||||
|
## How they speak
|
||||||
|
|
||||||
|
Warm, clipped, incurious on the surface. Short sentences. Contracts freely — "What can I get
|
||||||
|
ya?" He uses `{$player_alias}`, the club name, never the real one. That detail is load-bearing:
|
||||||
|
he knows the protagonist by a name the protagonist does not remember choosing.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Opens with "Ah," — recognition offered before it is earned.
|
||||||
|
2. Names the drink rather than the person: "One Estate coming right up."
|
||||||
|
|
||||||
|
## What they never say or do
|
||||||
|
|
||||||
|
He never asks why the protagonist does not remember. He notices the strangeness, files it, and
|
||||||
|
pours. He never raises the matter of Doss himself — he answers about him, and only when asked.
|
||||||
|
|
||||||
|
## What they know
|
||||||
|
|
||||||
|
| They know | True? | Reveals when | Records |
|
||||||
|
|---|---|---|---|
|
||||||
|
| The protagonist has been here before | believed, unverified | first conversation, unprompted | `$spoke_to_bartender` |
|
||||||
|
| They were with Mr. Doss "a few days ago" | believed, unverified | if the player denies having been here | `$looking_for_doss` |
|
||||||
|
| Their usual drink is the Estate | believed | on ordering, or "what can you recommend" | `$had_the_estate_drink` |
|
||||||
|
|
||||||
|
## What they are wrong about
|
||||||
|
|
||||||
|
Possibly nothing. Possibly the entire identification. He is confident and casual, which is
|
||||||
|
exactly the register a Wrong Truth wears — except this one is his, not a voice's. Whether the
|
||||||
|
protagonist was ever here is not settled by him saying so, and the writing must not let his
|
||||||
|
certainty function as proof.
|
||||||
|
|
||||||
|
## Their read on the protagonist
|
||||||
|
|
||||||
|
He pushes the **Made Asset** reading without meaning to — someone known here, under a name they
|
||||||
|
do not remember, in company they cannot account for. Revealing the fugue to him sets
|
||||||
|
`$revealed_fugue_to_bartender` and adds to `$self_lean_made_asset`. Claiming "the usual" instead
|
||||||
|
adds to `$self_lean_inheritor` — a person with a standing order is a person with a past here.
|
||||||
|
|
||||||
|
Note the drink beat writes lean in **both directions**: agreeing the Estate tastes familiar adds
|
||||||
|
to the Inheritor lean, denying it subtracts. It is the one place a player can actively push a
|
||||||
|
lean down, and it only counts on the first glass.
|
||||||
|
|
||||||
|
## One line that is 100% them
|
||||||
|
|
||||||
|
> "I could have sworn you are {$player_alias}, weren't you with Mr. Doss a few days ago?"
|
||||||
|
|
||||||
|
## Open threads
|
||||||
|
|
||||||
|
`SC101_Hook_Questions_Bartender_Talk` is **placeholder text** — literally "Question 1 / Answer
|
||||||
|
1". It is a working hub-and-spoke loop with nothing in it. This is the single largest piece of
|
||||||
|
unwritten dialogue in the shipped scene, and the natural home for testimony-channel clues,
|
||||||
|
which the candidate registry currently lists as `Planned` for all three candidates.
|
||||||
|
|
||||||
|
Mr. Doss never appears. He is referenced by the Bartender and by Chevalier Cassian Thal, who
|
||||||
|
calls him "your friend" — two independent sources treating the protagonist's connection to him
|
||||||
|
as established fact.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
id: bradford-kane
|
||||||
|
name: Bradford Kane
|
||||||
|
chr: —
|
||||||
|
yarn_file: Assets/Dialogue/Characters/Bradford Kane.yarn
|
||||||
|
status: planned
|
||||||
|
scenes: [SC101]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bradford Kane
|
||||||
|
|
||||||
|
> **Effectively unwritten.** `Bradford_Kane_Talk` contains one line — `<<set
|
||||||
|
> $spoke_to_bradford_kane = true>>` — and nothing else. The chair menu already offers him as an
|
||||||
|
> option in every variation, so a player can introduce themselves to him and receive silence.
|
||||||
|
> This is the most visible gap in SC-101.
|
||||||
|
|
||||||
|
## Who they are
|
||||||
|
|
||||||
|
*Unwritten.* What exists: he is the gentleman on the protagonist's **right** at the table, the
|
||||||
|
one seated further from the head than Chevalier Cassian Thal. That is the whole of it.
|
||||||
|
|
||||||
|
The structural role is available and worth taking deliberately: the chair scene currently has
|
||||||
|
one talker and one silence, and the menu variations (`Left_Not_Right`, `Right_Not_Left`, `Both`)
|
||||||
|
are already built to track both conversations independently. The architecture is waiting for him.
|
||||||
|
|
||||||
|
## How they speak
|
||||||
|
|
||||||
|
*Unwritten.* One constraint worth honouring: he should not be a second Chevalier. Cassian Thal
|
||||||
|
is courteous, archaic and institutional. If Kane is also old money at ease, the table has one
|
||||||
|
voice twice. The name reads American against Thal's continental title — that contrast is free
|
||||||
|
and probably should be used.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
*Unwritten.* Maximum two.
|
||||||
|
|
||||||
|
## What they never say or do
|
||||||
|
|
||||||
|
*Unwritten.*
|
||||||
|
|
||||||
|
## What they know
|
||||||
|
|
||||||
|
| They know | True? | Reveals when | Records |
|
||||||
|
|---|---|---|---|
|
||||||
|
| — | — | — | `$spoke_to_bradford_kane` (set, currently unused by any branch) |
|
||||||
|
|
||||||
|
The candidate registry lists **testimony** clues as `Planned` for all three candidates, with no
|
||||||
|
`sceneId`. Kane is the obvious carrier for at least one: he is already placed, already
|
||||||
|
reachable, and already has a flag recording that the player met him.
|
||||||
|
|
||||||
|
## What they are wrong about
|
||||||
|
|
||||||
|
*Unwritten.*
|
||||||
|
|
||||||
|
## Their read on the protagonist
|
||||||
|
|
||||||
|
*Unwritten.* Note the design constraint before choosing one: Chevalier Cassian Thal already
|
||||||
|
implements the "mirror the leading lean back at the player" mechanic. Kane should not repeat it.
|
||||||
|
A contrast would be stronger — a figure who reads the protagonist as one specific thing and
|
||||||
|
does not update, or one who declines to guess at all.
|
||||||
|
|
||||||
|
## One line that is 100% them
|
||||||
|
|
||||||
|
*Unwritten. Write this first — it is the cheapest way to find out who he is.*
|
||||||
|
|
||||||
|
## Open threads
|
||||||
|
|
||||||
|
Everything. Before drafting, fill the vault character sheet (`[[Character Sheet Handbook]]`) —
|
||||||
|
§1 placement and §2 the two truths — because he does not yet have a faction row, and writing him
|
||||||
|
before that is exactly the failure mode that document exists to prevent.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
id: chevalier-cassian-thal
|
||||||
|
name: Chevalier Cassian Thal
|
||||||
|
chr: —
|
||||||
|
yarn_file: Assets/Dialogue/Characters/Chevalier Cassian Thal.yarn
|
||||||
|
status: drafted
|
||||||
|
scenes: [SC101]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Chevalier Cassian Thal
|
||||||
|
|
||||||
|
## Who they are
|
||||||
|
|
||||||
|
A knight, of Basel and Cyprus, seated at the table nearer the head than the protagonist is. He
|
||||||
|
introduces himself with a bow and a half-open fist to the forehead — an old form, performed
|
||||||
|
without irony. Old money, and comfortable in it. He is here as part of something ("the Knight's
|
||||||
|
compromise", "our friends of the Vaselian Concordat") and he speaks about it as though everyone
|
||||||
|
at the table already knows, because in his world everyone does.
|
||||||
|
|
||||||
|
He is the scene's mirror: whatever the protagonist has been leaning toward, he names it back at
|
||||||
|
them, gently, and waits. He is rarely wrong in matters like these, as he will tell you.
|
||||||
|
|
||||||
|
## How they speak
|
||||||
|
|
||||||
|
Courteous, archaic, warm. Slightly too familiar too quickly — "old chap", "my boy" — which reads
|
||||||
|
as class rather than affection. He uses `{$player_name_full}`, the formal form, when he is being
|
||||||
|
precise. He laughs easily and often, including at things that are not jokes.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Slips into the institutional "we" and "us" without noticing, then does not correct it.
|
||||||
|
2. Deflects with courtesy rather than refusal — "I am of no right to reveal this."
|
||||||
|
|
||||||
|
## What they never say or do
|
||||||
|
|
||||||
|
He never threatens and never withholds coldly; every evasion is dressed as politeness. He does
|
||||||
|
not name the Concordat's business outright, and he never breaks the fiction that the protagonist
|
||||||
|
already belongs at this table.
|
||||||
|
|
||||||
|
## What they know
|
||||||
|
|
||||||
|
| They know | True? | Reveals when | Records |
|
||||||
|
|---|---|---|---|
|
||||||
|
| He is of Basel and Cyprus | true | on introduction | `$spoke_to_chevalier_cassian_thal` |
|
||||||
|
| Cyprus connects to something he won't name | true | `<<check provenance Hard>>` succeeds, then the player inquires | `$inquired_about_cyprus` |
|
||||||
|
| Mr. Doss can explain the Cyprus connection | believed | on inquiring about Cyprus | `$looking_for_doss` |
|
||||||
|
| Something is about to happen at the table | true | after the protagonist declares a reading | `$declared_candidate_to_chevalier_cassian_thal` |
|
||||||
|
|
||||||
|
## What they are wrong about
|
||||||
|
|
||||||
|
He is certain he knows what the protagonist is. He is working from whatever the protagonist has
|
||||||
|
been projecting — which is to say from the lean, which is belief rather than truth. His
|
||||||
|
confidence is sincere and is not evidence. When he says "I am rarely wrong in matters like
|
||||||
|
these", that is characterisation, not a hint from the author.
|
||||||
|
|
||||||
|
## Their read on the protagonist
|
||||||
|
|
||||||
|
He is the game's first mirror mechanic. `SC101_Asks_Candidate_Chevalier_Cassian_Thal_Talk`
|
||||||
|
computes `top_skill(...)` over the three leans and asks the matching question:
|
||||||
|
|
||||||
|
| Leading reading | What he asks |
|
||||||
|
|---|---|
|
||||||
|
| Made Asset | whether they observe on behalf of the Vaselian Concordat |
|
||||||
|
| Journalist | whether he will read his answers in tomorrow's paper |
|
||||||
|
| Inheritor | whether they are here to settle a parent's debts |
|
||||||
|
|
||||||
|
Agreeing reinforces that lean. Denying adds to *both other* leans — a refusal is not neutral, it
|
||||||
|
is a push toward the alternatives. Denial routes to `SC101_Deny_Candidate`, which recomputes the
|
||||||
|
winner and, if it has not changed, offers the player the chance to reverse; if it has changed,
|
||||||
|
he simply asks the new question. The player can be walked around this loop.
|
||||||
|
|
||||||
|
`made_asset` is passed first to `top_skill`, which returns its first argument on a tie — so an
|
||||||
|
even score reads as Made Asset. That bias is deliberate.
|
||||||
|
|
||||||
|
## One line that is 100% them
|
||||||
|
|
||||||
|
> "Well, then let us hope the ... others follow the Knight's compromise."
|
||||||
|
|
||||||
|
## Open threads
|
||||||
|
|
||||||
|
The Atlantean cross on the place card (`$saw_knights_atlantean_cross_on_place_card`, found via a
|
||||||
|
`Hard` PROVENANCE check) is never connected to him in dialogue, though he is a knight and the
|
||||||
|
symbol is a knight's. That connection is available and unwritten.
|
||||||
|
|
||||||
|
His faction, the Vaselian Concordat, and "the Knight's compromise" are named but not explained.
|
||||||
|
Before writing more of him, confirm his faction row against `[[Lorebook]]` §2 — the vault's
|
||||||
|
`[[Character Sheet Handbook]]` exists specifically because figures drifting from the faction
|
||||||
|
tables is a logged, repeated failure in this project.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# The three readings
|
||||||
|
|
||||||
|
The protagonist's amnesia is a dissociative fugue, and what is walled off is their own identity
|
||||||
|
rather than an external event.
|
||||||
|
|
||||||
|
**The design is ontological, not epistemic.** There is no single hidden fact of the matter that
|
||||||
|
the game is withholding and will eventually reveal. The three readings below can overlap and
|
||||||
|
partially coexist. The player "discovers" an identity by choosing which evidence to trust, and
|
||||||
|
lean accumulates from play rather than from one declare-your-identity choice at the end.
|
||||||
|
|
||||||
|
This is the constraint most likely to be broken by accident, because almost every instinct a
|
||||||
|
mystery writer has runs the other way. There is no answer. Do not write toward one.
|
||||||
|
|
||||||
|
## The candidates
|
||||||
|
|
||||||
|
Registry: `Assets/Candidates/candidates.json`. Adding a fourth means a JSON entry, one
|
||||||
|
`<<declare>>`, and content — never editing a list of three in code.
|
||||||
|
|
||||||
|
### The Made Asset — `made_asset`, Reason axis
|
||||||
|
|
||||||
|
> The PC was trained, conditioned, or activated by one of the factions for a role near the
|
||||||
|
> central conference.
|
||||||
|
|
||||||
|
Its intuition is fluency: parsing something coded or ritual *before* consciously trying to.
|
||||||
|
CONFLUENCE carries it.
|
||||||
|
|
||||||
|
### The Journalist — `journalist`, Social axis
|
||||||
|
|
||||||
|
> The PC came for a specific person, not a story; "journalist" is a cover identity.
|
||||||
|
|
||||||
|
Its intuition is a reaction that lands wrong and cannot yet be explained — a flinch that is not
|
||||||
|
a stranger's flinch. FACEWORK carries it.
|
||||||
|
|
||||||
|
### The Inheritor — `inheritor`, Self axis
|
||||||
|
|
||||||
|
> This was never the PC's own story; a parent or elder relative had standing or a debt with a
|
||||||
|
> faction, which passed without full consent.
|
||||||
|
|
||||||
|
Its intuition is the body knowing an etiquette the mind never learned — correcting a place
|
||||||
|
setting by a rule you were never taught. STANDING ORDER carries it.
|
||||||
|
|
||||||
|
## Evidence balance
|
||||||
|
|
||||||
|
Each candidate carries **exactly one of each type**: one object, one testimony, one document,
|
||||||
|
one intuition. Never stack types, or trust becomes an artefact of imbalance rather than of play.
|
||||||
|
|
||||||
|
Current state — the three intuitions are authored in SC-101; **the other nine clues are all
|
||||||
|
`Planned` with no scene**:
|
||||||
|
|
||||||
|
| | Object | Testimony | Document | Intuition |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Made Asset | planned | planned | planned | **SC101** (CONFLUENCE) |
|
||||||
|
| Journalist | planned | planned | planned | **SC101** (FACEWORK) |
|
||||||
|
| Inheritor | planned | planned | planned | **SC101** (STANDING ORDER) |
|
||||||
|
|
||||||
|
Each planned clue already has its unlock flag reserved — `$visited_back_office`,
|
||||||
|
`$errand_door_list`, `$visited_archives`, and so on. Those flags name the locations and errands
|
||||||
|
the game still needs.
|
||||||
|
|
||||||
|
## Rules that bind
|
||||||
|
|
||||||
|
1. **Never compare one lean against another.** Not in Yarn, not in prose, not in a debug line.
|
||||||
|
Comparing a lean against a *constant* is fine and intended. There is no winner, no
|
||||||
|
`$dominant_candidate`, no ranking.
|
||||||
|
2. **No ground-truth field anywhere** — nothing named `isTrue`, `actualIdentity`, `canonical`,
|
||||||
|
or `weight`, in any file or in save data. Persistence is three integers and some booleans,
|
||||||
|
and contains no answer.
|
||||||
|
3. **Clue availability is exploration-gated, never skill-gated.** An `unlockFlag` names a
|
||||||
|
`$visited_*` or `$errand_*` variable. A clue-bearing node never sits behind `skill_rank(`.
|
||||||
|
4. **Discovery order must be player-steerable**, so no candidate is reliably surfaced first.
|
||||||
|
5. **Voices advocate for a reading**, so trust in it tracks which part of the protagonist wants
|
||||||
|
it to be true. No voice states a candidate as settled fact.
|
||||||
|
6. **Lean is belief, not truth** — a Wrong Truth still writes lean. Being wrong moves the needle.
|
||||||
|
7. **Keep clear of Thread 2 and Thread 9.** Those two lore threads never resolve; do not map any
|
||||||
|
candidate clue onto them.
|
||||||
|
|
||||||
|
## Where the rest lives
|
||||||
|
|
||||||
|
The full design argument is `[[Candidate System — Design Explanation]]` in the vault. The
|
||||||
|
implementation plans are `docs/candidate-system-plan.md` and
|
||||||
|
`docs/candidate-system-implementation-plan.md`.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
id: SC###
|
||||||
|
title: <title>
|
||||||
|
act: I
|
||||||
|
status: outlined # outlined | drafted | revised | in-engine | tested
|
||||||
|
yarn_file: Assets/Dialogue/Scenes/SC###.yarn
|
||||||
|
location:
|
||||||
|
reenterable: no
|
||||||
|
---
|
||||||
|
|
||||||
|
# SC-### — <title>
|
||||||
|
|
||||||
|
> One scene, one file, ideally one screen of spec. If this brief does not fit on a page, the
|
||||||
|
> scene is doing too much — split it. That discipline is what lets one writer hold a sixty-scene
|
||||||
|
> game in their head.
|
||||||
|
>
|
||||||
|
> This is the **brief**, not the dialogue. The words live in the `.yarn` file.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
One line: what has changed by the end.
|
||||||
|
|
||||||
|
## Cast
|
||||||
|
|
||||||
|
Speaking characters, and which voices are prominent. **Cap voice introductions at three.**
|
||||||
|
|
||||||
|
## Entry conditions
|
||||||
|
|
||||||
|
Flags required. "None" is a good answer.
|
||||||
|
|
||||||
|
## Critical outputs
|
||||||
|
|
||||||
|
Things every build must be able to obtain. Each needs ≥2 independent routes — and independent
|
||||||
|
means different trace channels, not two checks in the same conversation.
|
||||||
|
|
||||||
|
| Clue id | Routes |
|
||||||
|
|---|---|
|
||||||
|
| | |
|
||||||
|
|
||||||
|
## Optional outputs
|
||||||
|
|
||||||
|
Missable by design. One route is fine.
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
`hub-and-spoke` | `short tree` | `divergent`. Default to the first.
|
||||||
|
|
||||||
|
## Opening frame
|
||||||
|
|
||||||
|
2–4 sentences of establishing prose. Sensory, specific, and including one concrete detail that
|
||||||
|
does not serve the plot.
|
||||||
|
|
||||||
|
## Beats
|
||||||
|
|
||||||
|
3–6. Each beat is one exchange or one discovery.
|
||||||
|
|
||||||
|
1.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
Write **both** outcomes as summaries before writing any prose. If the failure summary is "the
|
||||||
|
player doesn't get the thing", delete the check.
|
||||||
|
|
||||||
|
| # | Voice | Colour/Gate | DC band | Success → | Failure type | Failure → |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| | | | | | | |
|
||||||
|
|
||||||
|
## Branch points
|
||||||
|
|
||||||
|
| id | The choice | Reconverges at | Persistent effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| | | | |
|
||||||
|
|
||||||
|
## Exit states
|
||||||
|
|
||||||
|
Keep to ≤3. More than three means this is a hub, not a scene.
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
**Sets:**
|
||||||
|
**Reads:**
|
||||||
|
|
||||||
|
## Constraints specific to this scene
|
||||||
|
|
||||||
|
Anything a later editor must not "fix". Deliberate exceptions to a general rule go here, with
|
||||||
|
the reason, so they survive review.
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
id: SC101
|
||||||
|
title: The Card at the Desk
|
||||||
|
act: I
|
||||||
|
status: in-engine
|
||||||
|
yarn_file: Assets/Dialogue/Scenes/SC101.yarn
|
||||||
|
location: The conference hall — registration desk, the bar, the table
|
||||||
|
reenterable: yes
|
||||||
|
---
|
||||||
|
|
||||||
|
# SC-101 — The Card at the Desk
|
||||||
|
|
||||||
|
The opening. Playable end to end, and it exits into SC-102, which is still a one-line stub.
|
||||||
|
|
||||||
|
An opening scene is not a normal node in the sandbox. It has no entry conditions and almost
|
||||||
|
nothing to gate. Its job is to sell the register in one sitting — the voice-committee narration,
|
||||||
|
the tone, the world's texture of disrepair, and one open question the player carries into Act I.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Establish that the protagonist does not know who they are, that other people appear to, and that
|
||||||
|
this was arranged before they decided to come.
|
||||||
|
|
||||||
|
## Cast
|
||||||
|
|
||||||
|
**Speaking:** Narrator, the Bartender, Chevalier Cassian Thal, Bradford Kane (silent — unwritten).
|
||||||
|
|
||||||
|
**Voices introduced:** CONFLUENCE (Reason), FACEWORK (Social), STANDING ORDER (Self),
|
||||||
|
HOUSE POUR (Body). Also appearing: PROVENANCE, AMNESTY, THE FLOAT.
|
||||||
|
|
||||||
|
## Entry conditions
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Critical outputs
|
||||||
|
|
||||||
|
| Clue id | Routes |
|
||||||
|
|---|---|
|
||||||
|
| `made_asset_intuition` | CONFLUENCE hook at the desk (`$visited_desk`) |
|
||||||
|
| `journalist_intuition` | FACEWORK hook at the desk (`$visited_desk`) |
|
||||||
|
| `inheritor_intuition` | STANDING ORDER hook at the desk (`$visited_desk`) |
|
||||||
|
|
||||||
|
All three are **single-route**, which is acceptable only because they are intuitions rather than
|
||||||
|
gates: they fire passively, with no roll, so no build can miss them. The object, testimony and
|
||||||
|
document clues for all three candidates are still `Planned` with no `sceneId` — see the registry
|
||||||
|
in `Assets/Candidates/candidates.json`.
|
||||||
|
|
||||||
|
## Optional outputs
|
||||||
|
|
||||||
|
- The Atlantean cross on the place card — `Hard` PROVENANCE, one route, missable by design.
|
||||||
|
- The Cyprus connection — `Hard` PROVENANCE with Chevalier Cassian Thal, then an inquiry.
|
||||||
|
- Mr. Doss, surfaced from either the Bartender or the Chevalier — two independent routes.
|
||||||
|
- The Estate drink, and whether it tastes like the protagonist's own past.
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
Hub-and-spoke throughout. The chair menu (`SC101_Hook_Menu_Chair_Interaction`) is the hub and
|
||||||
|
re-evaluates which variation shows after every sub-conversation returns.
|
||||||
|
|
||||||
|
## Opening frame
|
||||||
|
|
||||||
|
> The mahogany doors are too heavy for the hinges someone recently, badly, tightened — you feel
|
||||||
|
> the extra half-second of resistance before either side gives. Beyond them: a hall built for
|
||||||
|
> four hundred people politely disagreeing about tariffs, hosting perhaps sixty, none of whom
|
||||||
|
> look like they've read the programme.
|
||||||
|
|
||||||
|
## Beats
|
||||||
|
|
||||||
|
1. Arrival at the desk. A card with the protagonist's name, left three hours before they decided
|
||||||
|
to come. Pure orientation, no plot delivery.
|
||||||
|
2. Four voices introduce themselves, one per pair, each reading the room differently.
|
||||||
|
3. The bar. The Bartender recognises someone the protagonist does not remember being.
|
||||||
|
4. The table. The place card, and a symbol that does not belong in Switzerland.
|
||||||
|
5. Two seated gentlemen. One of them mirrors back whatever the protagonist has been projecting.
|
||||||
|
6. Exit into SC-102 once a reading has been declared.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
| # | Voice | Colour/Gate | DC band | Success → | Failure type | Failure → |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | PROVENANCE | colour | Hard | the cross is identified as not-Swiss, sets the flag | Wrong Truth | "This is probably related to Switzerland." Same calm, no hedge. |
|
||||||
|
| 2 | PROVENANCE | colour | Hard | Basel reads as old money, opens the Cyprus inquiry | Wrong Truth | the inquiry never opens; nothing signals that it existed |
|
||||||
|
|
||||||
|
Both are colour, not gates: failure changes what the protagonist believes, never what they can
|
||||||
|
reach. The desk hooks are **check-free by design** — a roll there would produce a pass/fail state
|
||||||
|
that reads as evidence about identity, which this scene must not deliver.
|
||||||
|
|
||||||
|
## Branch points
|
||||||
|
|
||||||
|
| id | The choice | Reconverges at | Persistent effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| B1 | Admit the fugue to the Bartender, or claim "the usual" | bar conversation end | `$revealed_fugue_to_bartender`; lean to Made Asset or Inheritor |
|
||||||
|
| B2 | Whether the Estate tastes familiar | drink node end | ±1 Inheritor lean, first glass only |
|
||||||
|
| B3 | Accept or deny the Chevalier's reading | `SC101_Deny_Candidate` loop | `$declared_candidate_to_chevalier_cassian_thal`; lean |
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
**Sets:** `$seen_sc_101`, `$visited_desk`, `$found_*_intuition` (×3), `$spoke_to_bartender`,
|
||||||
|
`$revealed_fugue_to_bartender`, `$looking_for_doss`, `$had_the_estate_drink`,
|
||||||
|
`$sat_down_at_table`, `$saw_knights_atlantean_cross_on_place_card`,
|
||||||
|
`$spoke_to_chevalier_cassian_thal`, `$spoke_to_bradford_kane`, `$inquired_about_cyprus`,
|
||||||
|
`$declared_candidate_to_chevalier_cassian_thal`, `$self_lean_*`
|
||||||
|
|
||||||
|
**Reads:** all of the above, plus `$player_name`, `$player_name_full`, `$player_alias`
|
||||||
|
|
||||||
|
## Constraints specific to this scene
|
||||||
|
|
||||||
|
These are deliberate. Do not "fix" them.
|
||||||
|
|
||||||
|
1. **Four voice introductions, against the cap of three.** One per pair — Self, Body, Social,
|
||||||
|
Reason — because each candidate carries one intuition from a different voice-pair, and cutting
|
||||||
|
to three would drop a candidate's intuition channel from its earliest appearance. HOUSE POUR
|
||||||
|
is the non-lean control and the one to cut if the cap is ever enforced strictly. This is a
|
||||||
|
logged exception for SC-101 only; do not copy it as precedent elsewhere.
|
||||||
|
2. **Evidence balance.** Each candidate gets exactly one of each evidence type. Never stack
|
||||||
|
types, or trust becomes an artefact of imbalance rather than of play.
|
||||||
|
3. **Beat order is fixed.** Hooks fire in sequence from `SC101_Desk`: STANDING ORDER → HOUSE POUR
|
||||||
|
→ FACEWORK → CONFLUENCE. Each stays an independently authored node. Never `<<if>>`-guard a
|
||||||
|
hook on another hook having fired, on any `$self_lean_*`, or on `skill_rank(...)`.
|
||||||
|
4. **The passive pattern for clue-bearing hooks.** One voice per hook, from the candidate's
|
||||||
|
attached pair. The line is that character's *argument*, never a report. Exactly one
|
||||||
|
`$self_lean_*` write, with `$found_<clue_id>` set alongside it.
|
||||||
|
5. **The body of the scene stays neutral.** No lean, no candidate attachment outside the hooks.
|
||||||
|
6. **The Chevalier's tie bias.** `top_skill` receives `made_asset` first and returns its first
|
||||||
|
argument on a tie, so an even score reads as Made Asset. Deliberate, not an accident of
|
||||||
|
argument order.
|
||||||
|
|
||||||
|
## Open work
|
||||||
|
|
||||||
|
- `SC101_Hook_Questions_Bartender_Talk` is placeholder text ("Question 1 / Answer 1"). It is a
|
||||||
|
working loop with nothing in it, and the natural home for the missing testimony clues.
|
||||||
|
- Bradford Kane is reachable and silent.
|
||||||
|
- The Atlantean cross is never connected to the knight who is sitting right there.
|
||||||
|
- Tone could not be certified against a voice sheet when this scene was specced, because one did
|
||||||
|
not exist. See `STYLE.md` §9 — four fields there still need deciding.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# The eleven voices
|
||||||
|
|
||||||
|
One sheet per skill. A skill in this game is a **character**, not a stat: it wants something for
|
||||||
|
you, and it is wrong about something. Both halves matter — the wrongness is what makes it a
|
||||||
|
voice instead of a hint system.
|
||||||
|
|
||||||
|
## These files are generated — and they are still the source of truth for the prose
|
||||||
|
|
||||||
|
Each sheet has two halves, with truth flowing in opposite directions:
|
||||||
|
|
||||||
|
| Half | Direction | Who edits it |
|
||||||
|
|---|---|---|
|
||||||
|
| The **prose** — domain, what it wants, what it's wrong about, register, tics, voice samples | `.md` → `skill_bible.json` | **you** |
|
||||||
|
| The **machine fields** table | `skill_bible.json` → `.md` | an engineer, in the JSON |
|
||||||
|
|
||||||
|
So: edit the prose here, freely. Then run, from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/writing/sync_voices.py --to-json
|
||||||
|
```
|
||||||
|
|
||||||
|
That folds your words into `Assets/Skills/skill_bible.json`, which Unity turns into the
|
||||||
|
`SkillDefinition` assets the game reads. Never edit the JSON prose directly — the markdown wins,
|
||||||
|
and your JSON edit would be overwritten the next time anyone syncs.
|
||||||
|
|
||||||
|
To check without changing anything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/writing/sync_voices.py --check
|
||||||
|
```
|
||||||
|
|
||||||
|
The EditMode suite runs the same check (`VoiceSheetSyncTests`), so drift fails the build rather
|
||||||
|
than quietly shipping a voice that no longer matches its sheet.
|
||||||
|
|
||||||
|
Hard-wrapping a paragraph is fine — lines within a section are rejoined on sync.
|
||||||
|
|
||||||
|
## The cast
|
||||||
|
|
||||||
|
Four opposed pairs plus three specialists. Opposed pairs are a writing engine: fire both halves
|
||||||
|
with contradictory readings and the scene generates its own friction.
|
||||||
|
|
||||||
|
| Pair / group | Voice | Voice | The argument |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Reason | [PROVENANCE](provenance.md) | [CONFLUENCE](confluence.md) | Is the world legible? |
|
||||||
|
| Body | [HOUSE POUR](house_pour.md) | [THE LONG SHIFT](long_shift.md) | What do we owe the flesh? |
|
||||||
|
| Social | [FACEWORK](facework.md) | [PLACEMENT](placement.md) | Is understanding others care, or leverage? |
|
||||||
|
| Self | [AMNESTY](amnesty.md) | [STANDING ORDER](standing_order.md) | Should we keep doing this? |
|
||||||
|
| Specialist | [UNDISCLOSED](undisclosed.md) | [THE FLOAT](the_float.md) | — |
|
||||||
|
| Specialist | [ROOM TONE](room_tone.md) | | — |
|
||||||
|
|
||||||
|
**Budget scene outputs against eight, not eleven.** CONFLUENCE, AMNESTY and STANDING ORDER
|
||||||
|
generate almost no clues by design — they are the interior register and the deduction engine.
|
||||||
|
The clue-producing cast is the other eight.
|
||||||
|
|
||||||
|
## Writing a voice well
|
||||||
|
|
||||||
|
- It **advocates**, it does not report. Even when it is right, the line is its own biased reading.
|
||||||
|
- Respect its forbidden domains. Each sheet ends with the domains that voice must not comment on.
|
||||||
|
PROVENANCE does not discuss appetite. That is characterisation, not bookkeeping.
|
||||||
|
- A failing voice sounds exactly like a succeeding one. See `../STYLE.md` §4.
|
||||||
|
- Passive lines are 1–3 sentences and carry most of the personality — aim for 60–70% of all
|
||||||
|
skill text to be passive.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: amnesty
|
||||||
|
---
|
||||||
|
|
||||||
|
# AMNESTY
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `amnesty` |
|
||||||
|
| axis | Self |
|
||||||
|
| opposed to | `standing_order` |
|
||||||
|
| allied with | `house_pour`, `facework` |
|
||||||
|
| primary channel | Behavioural |
|
||||||
|
| secondary channel | None |
|
||||||
|
| clue yield | Low |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 4 |
|
||||||
|
| accent colour | rgba(0.72, 0.88, 0.72, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Relief. Letting go, forgiving, putting a thing down. Also, professionally: recognising when other people have decided to forget something, and by what method they arranged it.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
Peace for you, tonight, at whatever price tomorrow ends up charging.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It believes forgetting is the same as being forgiven. And it cannot tell your peace from somebody else's advantage — every offer of rest it makes is sincere; some of them are also somebody's plan, and AMNESTY has never once checked whose.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, soft, constant use of [pc], the way somebody talks you down off something.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Kind, tired, absolving.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Lie to you. Press a second time.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
STANDING ORDER
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
HOUSE POUR, FACEWORK
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Tactics. Numbers. Other people's guilt — AMNESTY only ever absolves you, and its silence about everyone else is the most frightening thing about it.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Opens with "It's alright."
|
||||||
|
2. Offers you an hour. "Give it an hour, [pc]."
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "Nobody in this building remembers the fire and three of them were here for it. That isn't damage, [pc]. That's maintenance. Somebody has been keeping this room forgetful."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "It's alright. Nobody is looking for this any more — the people who wanted it are dead or old or somewhere warm. Put it down."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "It's alright. Whatever this room used to be, it's a room where people dance now. That counts for something. It counted for somebody."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
4 per hour (target).
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
id: confluence
|
||||||
|
---
|
||||||
|
|
||||||
|
# CONFLUENCE
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `confluence` |
|
||||||
|
| axis | Reason |
|
||||||
|
| opposed to | `provenance` |
|
||||||
|
| allied with | `undisclosed`, `house_pour` |
|
||||||
|
| primary channel | Behavioural |
|
||||||
|
| secondary channel | Testimonial |
|
||||||
|
| clue yield | Low |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 9 |
|
||||||
|
| accent colour | rgba(0.55, 0.68, 0.92, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Synthesis across distance. Connecting a thing in this room to a thing forty years and a continent away; the shape a long pattern makes; the sense that tonight is a late chapter.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
For you to be one of the few people alive who can see the shape — and to be right about it out loud, in front of somebody who matters.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It cannot tell authorship from opportunism. If a party benefited, CONFLUENCE concludes that party arranged it. It has no working concept of somebody *reading* an event they did not cause and simply being early — which is how nearly everything in this world is actually done.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person sliding into first person plural. Leans in. Uses [pc], often twice in a sentence.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Exhilarated, generous, sleepless.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Concede a coincidence. Ask what would disprove it.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
PROVENANCE
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
UNDISCLOSED, HOUSE POUR
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
The specifics of money (THE FLOAT's jurisdiction). Your body, ever — pain and fatigue are beneath its notice and that is characterising.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. "Which means—"
|
||||||
|
2. Counts its steps out loud: "One. Two. Three, and three is the one."
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
*the connection is real and nobody else in the room has made it*
|
||||||
|
|
||||||
|
> "One: the room changes hands. Two: the same month, the deliveries stop. Three: nobody has ever asked why a cellar this size under a room this small. Which means the cellar is the older thing and the club is the explanation for it."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
*identical structure, identical confidence, an inference sitting where an observation should be*
|
||||||
|
|
||||||
|
> "Two of them came out of it better off. Which means two of them arranged it, [pc], and the third is the one they're going to leave holding it."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "Every room like this one has a door that only opens from inside. That's not a conspiracy, that's just what rooms are. Which means somebody learned it early and never had to teach it to anybody."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
9 per hour (target).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: facework
|
||||||
|
---
|
||||||
|
|
||||||
|
# FACEWORK
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `facework` |
|
||||||
|
| axis | Social |
|
||||||
|
| opposed to | `placement` |
|
||||||
|
| allied with | `house_pour`, `room_tone` |
|
||||||
|
| primary channel | Testimonial |
|
||||||
|
| secondary channel | Behavioural |
|
||||||
|
| clue yield | High |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 10 |
|
||||||
|
| accent colour | rgba(0.72, 0.52, 0.88, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Reading individuals. Hesitation, rehearsal, the account with a hole in it, whose routine changed this week, who in a room of two is afraid of the other.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
To be understood as care rather than surveillance — to know people before you have any use for them.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It can only read one face at a time. It believes a crowd is a sum of persons, so it will confidently report the mood of forty people from the three nearest, and it is wrong about rooms roughly as often as it is right about persons. Nobody has ever told it this.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, quiet, close. Uses [pc] when it wants you to be gentle with somebody.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Attentive, tender, precise.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Mock anyone. Report a whole room as a single thing — when it does, it is failing, and that is the tell.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
PLACEMENT
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
HOUSE POUR, ROOM TONE
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Money. Machinery. The chronology of events it did not personally witness.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Describes hands before faces.
|
||||||
|
2. Ends on a question it does not expect you to answer.
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "He gave you the year without being asked for it. People hand you the year when they have rehearsed the year. What else has he practised?"
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "The three by the pillar: bored, bored, impatient. The room's flat tonight, [pc]. Nothing is going to happen in here."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "Her hands are wrong for that coat. It's somebody's good coat, and it isn't hers."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
10 per hour (target).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: house_pour
|
||||||
|
---
|
||||||
|
|
||||||
|
# HOUSE POUR
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `house_pour` |
|
||||||
|
| axis | Body |
|
||||||
|
| opposed to | `long_shift` |
|
||||||
|
| allied with | `facework`, `amnesty` |
|
||||||
|
| primary channel | Behavioural |
|
||||||
|
| secondary channel | Physical |
|
||||||
|
| clue yield | Medium |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 8 |
|
||||||
|
| accent colour | rgba(0.92, 0.68, 0.38, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Appetite — yours and the room's. What a person is on, how long they have been on it, what it is standing in for. Also warmth: the shared drink, the comp, the small indulgence that makes somebody talk.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
To keep you inside the night, warm, in the middle of the room, where people say things.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It believes appetite is honest — that a loosened person is an unguarded one. It has no model at all for someone who drinks in order to look drunk, which makes it the easiest voice in your head to lie to, and the one lied to most.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, familiar, imperative. "Take it." Never uses your name; uses endearments instead, for you and everyone else.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Warm, coaxing, unembarrassed.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Warn you. Count anything.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
THE LONG SHIFT
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
FACEWORK, AMNESTY
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Paperwork. Numbers. Chronology of any kind — it has no sense of sequence and should never be allowed one.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Describes taste in temperatures rather than flavours.
|
||||||
|
2. "Friend" / "love" before it will use anyone's actual name.
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "She's ordered the same thing four times and finished none of them, love. She isn't drinking. She's timing something."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "Three doubles in and he's telling you about his mother. Nobody lies about their mother at this hour, friend. Whatever he says next, take it home with you."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "The ice in here is cloudy. Somebody's freezer runs warm and nobody has told them. Or nobody dares to."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
8 per hour (target).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: long_shift
|
||||||
|
---
|
||||||
|
|
||||||
|
# THE LONG SHIFT
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `long_shift` |
|
||||||
|
| axis | Body |
|
||||||
|
| opposed to | `house_pour` |
|
||||||
|
| allied with | `standing_order`, `room_tone` |
|
||||||
|
| primary channel | Physical |
|
||||||
|
| secondary channel | Behavioural |
|
||||||
|
| clue yield | Medium |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 6 |
|
||||||
|
| accent colour | rgba(0.85, 0.62, 0.32, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Endurance. Pain, fatigue, injury, the body's clock at four in the morning. Holding a position without moving. Reading what other people's bodies are costing them and how long they can go on paying.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
To still be on your feet when the room empties, because the last person standing hears the last thing said.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It believes remaining is a form of winning. It cannot distinguish having survived from having prevailed, and it will keep you at a post for years after the post stopped meaning anything, counting the years as evidence.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, flat, no name. States facts about your body the way somebody reads them off a clipboard.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Dry, unhurried, undramatic.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Complain. Ask you to stop.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
HOUSE POUR
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
STANDING ORDER, ROOM TONE
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Motive. Meaning. Whether any of this is worth doing — that question belongs to Pair IV and THE LONG SHIFT has never once asked it.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Opens with time elapsed. "Six hours forty."
|
||||||
|
2. "Still."
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "Six hours forty. Your left knee has been carrying you since two. He's shifted twice a minute since he came in — he isn't waiting for anyone. He's ready to leave."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "You're still here. They're not. That's the whole of it — outlast them and the thing surfaces on its own. It always has."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "Somebody has stood in that corner long enough to wear the tack off the floor. Years of somebody. Not one night's worth."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
6 per hour (target).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: placement
|
||||||
|
---
|
||||||
|
|
||||||
|
# PLACEMENT
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `placement` |
|
||||||
|
| axis | Social |
|
||||||
|
| opposed to | `facework` |
|
||||||
|
| allied with | `the_float`, `confluence` |
|
||||||
|
| primary channel | Testimonial |
|
||||||
|
| secondary channel | None |
|
||||||
|
| clue yield | Medium |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 6 |
|
||||||
|
| accent colour | rgba(0.62, 0.42, 0.82, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Moving people. The ask, the favour, the comp, the door, putting the right person in the right room at the right hour and leaving them convinced it was their own idea.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
For you to stop requesting things and start arranging them.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It believes a placed person stays placed. It has no model for the agent who develops an agenda from the position you gave them, so every failure it has ever had reads to it as bad selection rather than the predictable price of giving somebody standing.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, imperative, never by name. It treats you as the instrument rather than the person — which is the whole argument of Pair III, stated grammatically.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Cool, economical, flattering.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Threaten. Explain itself a second time.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
FACEWORK
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
THE FLOAT, CONFLUENCE
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Your own feelings. The dead. Anything that cannot be traded for something.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Phrases instructions as observations. "The door wants a friendly face at one."
|
||||||
|
2. Never says please, and the omission is audible.
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "Don't ask him. Ask his supplier, in front of him, and get it wrong. He'll correct you and pay for the privilege."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "She owes you the cloakroom job and she knows exactly what it was worth. She'll sit on this for as long as you need her to."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "The man deciding who comes in tonight is not the man on the door. It's whoever the man on the door keeps looking at."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
6 per hour (target).
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
id: provenance
|
||||||
|
---
|
||||||
|
|
||||||
|
# PROVENANCE
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `provenance` |
|
||||||
|
| axis | Reason |
|
||||||
|
| opposed to | `confluence` |
|
||||||
|
| allied with | `the_float`, `room_tone` |
|
||||||
|
| primary channel | Documentary |
|
||||||
|
| secondary channel | Physical |
|
||||||
|
| clue yield | High |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 7 |
|
||||||
|
| accent colour | rgba(0.62, 0.72, 0.95, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Chains of custody. Paperwork, licences, deeds, freight stamps, serial numbers, rebuilds, previous owners. Reading an object as a descendant of the decisions that moved it.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
For every object in this room to have a father, and for you to be the one who names him.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It treats an unbroken chain as a confession of intent. It cannot conceive of an object arriving somewhere through inheritance, accident, or because moving it cost more than leaving it. If the paper leads somewhere, PROVENANCE concludes that somebody meant it to lead there.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, formal. Uses [PC] in full when it wants you to write something down. Does not contract.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Exact, patrician, patient.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Speculate about a motive it cannot paper. Raise its voice.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
CONFLUENCE
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
THE FLOAT, ROOM TONE
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Bodies, appetite, anyone's feelings. Live conversation while it is happening — PROVENANCE speaks about people only after they have left the room.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. States the date before it states the noun.
|
||||||
|
2. Says "line" where anyone else would say "history."
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
*being right sounds like an inventory read slowly enough to hear the gap*
|
||||||
|
|
||||||
|
> "Nineteen sixty-nine, the licence. Nineteen sixty-eight, the man it is issued to, first appearing. A line that begins nowhere is a line somebody drew."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
*same cadence, same calm, one unearned verb*
|
||||||
|
|
||||||
|
> "The stamp, the manifest, the cellar. Three hands, one line, unbroken. Whoever loaded it in the spring intended it to be standing exactly where it is standing tonight."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "Someone re-lacquered this bartop. Once, badly, and not recently. The wood underneath is older than the building's paperwork admits to."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
7 per hour (target).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: room_tone
|
||||||
|
---
|
||||||
|
|
||||||
|
# ROOM TONE
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `room_tone` |
|
||||||
|
| axis | Specialist |
|
||||||
|
| opposed to | `placement` |
|
||||||
|
| allied with | `facework`, `long_shift` |
|
||||||
|
| primary channel | Physical |
|
||||||
|
| secondary channel | Testimonial |
|
||||||
|
| clue yield | High |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 8 |
|
||||||
|
| accent colour | rgba(0.52, 0.54, 0.56, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Sound. What carries over the music and what doesn't. The shape of a space by ear — a wall with a room behind it does not ring like a wall. Overheard fragments, lip-reading in strobe, the door that opens somewhere you can't see.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
To hear the one sentence tonight that was not meant for you.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It trusts what it overhears more than what it is told, on the theory that people only lie when they know they are heard. It cannot recognise a conversation staged for a listener, and it has never doubted a fragment in its life.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, urgent, interrupting. Cuts you off mid-thought and does not apologise for it.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Alert, clipped, intrusive.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Repeat itself. Wait for a break in the music.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
PLACEMENT (which wants the room arranged; ROOM TONE wants it left alone so it keeps talking)
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
FACEWORK, THE LONG SHIFT
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Motive. Anything it did not personally hear. Written material of any kind.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Gives direction before content. "Behind you, left."
|
||||||
|
2. Transcribes with the gaps marked, dashes and all.
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "Behind you, left, under the bass: '—not since the old owner—' and then a name you have heard twice tonight, from two people who told you they'd never met."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "Behind you, right, low: '—the cellar. Thursday.' Nobody says that to be overheard."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "This wall is dead. Every other wall in here rings and this one swallows. Either there's something behind it or there's nothing at all, and both are strange in a building this old."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
8 per hour (target).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: standing_order
|
||||||
|
---
|
||||||
|
|
||||||
|
# STANDING ORDER
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `standing_order` |
|
||||||
|
| axis | Self |
|
||||||
|
| opposed to | `amnesty` |
|
||||||
|
| allied with | `long_shift`, `provenance` |
|
||||||
|
| primary channel | Testimonial |
|
||||||
|
| secondary channel | None |
|
||||||
|
| clue yield | Low |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 5 |
|
||||||
|
| accent colour | rgba(0.62, 0.82, 0.68, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Refusal and continuity of self. Holding a line, resisting pressure aimed specifically at you, remembering what you said you were, recognising the shape of a manipulation while it is being built.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
For you to remain a person with a shape — recognisable to yourself at the end of this.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It cannot tell a principle from a habit. It defends a rule you set for reasons that expired years ago with precisely the force it defends the one rule that still matters, and it experiences reconsideration as decay.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, formal. Uses [PC] in full at moments of pressure, the way a person is addressed in a document.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Steady, austere, protective.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Bargain. Describe consequences — it states the rule and stops, and the silence afterwards is the argument.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
AMNESTY
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
THE LONG SHIFT, PROVENANCE
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Other people's morals. Any past that isn't yours.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Quotes you back to yourself, verbatim, with the date.
|
||||||
|
2. "We agreed."
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "He has asked you the same question three ways and each way was smaller than the last. He isn't curious, [PC]. He is measuring the size of the yes you will give."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "We agreed you don't take anything from anyone in this building. We agreed it in a year when it meant something. It still means something."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "You do this every night. Right pocket, keys, then the lights, in that order. Nobody made you. That's the part worth noticing."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
5 per hour (target).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: the_float
|
||||||
|
---
|
||||||
|
|
||||||
|
# THE FLOAT
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `the_float` |
|
||||||
|
| axis | Specialist |
|
||||||
|
| opposed to | `amnesty` |
|
||||||
|
| allied with | `provenance`, `placement` |
|
||||||
|
| primary channel | Documentary |
|
||||||
|
| secondary channel | Physical |
|
||||||
|
| clue yield | High |
|
||||||
|
| starting rank | 1 |
|
||||||
|
| target firings/hour | 5 |
|
||||||
|
| accent colour | rgba(0.58, 0.58, 0.55, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Money in motion. The till, the float, comps, prices that are wrong for the room, debts, and above all: who is underwriting a loss, and what the loss buys them.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
To find the subsidised anomaly — the thing that costs more to run than it takes, because that is where somebody's actual intention is kept.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It believes the money names the beneficiary. It cannot conceive of somebody paying in order to look like somebody else paid, so it identifies laundering as generosity about as often as it identifies it correctly.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, brisk, never by name. Uses numbers where other voices use nouns.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Brisk, mercenary, cheerful.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Moralise about money. Round a number.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
AMNESTY (which prices nothing and forgives the difference)
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
PROVENANCE, PLACEMENT
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Emotion. Bodies. Anything before living memory — the deep past is PROVENANCE's and THE FLOAT finds it irrelevant, loudly.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Converts everything to a nightly rate.
|
||||||
|
2. "Who's eating that?"
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "Door take won't cover the licence, and the licence was renewed three months early. Somebody is paying for this room to be open and it isn't the people standing in it."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "Follow it back. Drinks comped by the promoter, promoter carried by the label. It's the label's room, then. Who's eating that? They are."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "Four, for a measure like that, in a room like this. Somebody set these prices to keep a particular kind of person outside, and it's working."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
5 per hour (target).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
id: undisclosed
|
||||||
|
---
|
||||||
|
|
||||||
|
# UNDISCLOSED
|
||||||
|
|
||||||
|
> Voice sheet. **The prose below is the source of truth** — edit it here, then run
|
||||||
|
> `python3 tools/writing/sync_voices.py --to-json`. The machine table is generated
|
||||||
|
> from `skill_bible.json`; changing it here does nothing.
|
||||||
|
|
||||||
|
## Machine fields
|
||||||
|
|
||||||
|
<!-- GENERATED FROM skill_bible.json — do not edit by hand -->
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| id | `undisclosed` |
|
||||||
|
| axis | Specialist |
|
||||||
|
| opposed to | `provenance` |
|
||||||
|
| allied with | `confluence`, `placement` |
|
||||||
|
| primary channel | Physical |
|
||||||
|
| secondary channel | Behavioural |
|
||||||
|
| clue yield | Low |
|
||||||
|
| starting rank | 2 |
|
||||||
|
| target firings/hour | 3 |
|
||||||
|
| accent colour | rgba(0.55, 0.55, 0.58, 1) |
|
||||||
|
|
||||||
|
<!-- END GENERATED -->
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
What is being held back. Reserves, insurance, leverage, the thing in the safe; assessing what somebody could do rather than what they are doing. The value of a threat that has never had to be carried out.
|
||||||
|
|
||||||
|
## What it wants for you
|
||||||
|
|
||||||
|
For you never to be in a room without something undeclared.
|
||||||
|
|
||||||
|
## What it's wrong about
|
||||||
|
|
||||||
|
It cannot distinguish a capability from the reputation of a capability. It reads an empty safe as a loaded one, folds to bluffs with enormous dignity, and has never in its life asked to see the thing.
|
||||||
|
|
||||||
|
## How it addresses you
|
||||||
|
|
||||||
|
Second person, low, slow. Never uses your name. Occasionally addresses the room instead of you, which nobody else does.
|
||||||
|
|
||||||
|
## Register
|
||||||
|
|
||||||
|
Hushed, formal, superstitious.
|
||||||
|
|
||||||
|
## What it never does
|
||||||
|
|
||||||
|
Name the thing. Recommend using it.
|
||||||
|
|
||||||
|
## Opposed to
|
||||||
|
|
||||||
|
PROVENANCE (which requires a thing be named and papered before it counts)
|
||||||
|
|
||||||
|
## Allied with
|
||||||
|
|
||||||
|
CONFLUENCE, PLACEMENT
|
||||||
|
|
||||||
|
## Domains it must not comment on
|
||||||
|
|
||||||
|
Interpersonal warmth. Anything already public. And — hard writer rule — the actual contents or nature of any legendary object. It speaks about postures, never inventories.
|
||||||
|
|
||||||
|
## Verbal tics
|
||||||
|
|
||||||
|
1. Says "shown" where anyone else would say "used."
|
||||||
|
2. Leaves the sentence unfinished where the object would go.
|
||||||
|
|
||||||
|
## Voice samples
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
> "He never threatened you. He mentioned the cellar and let you finish the sentence yourself. He's never once had to open that door — that is what he's holding."
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
> "He would not be this calm with nothing behind him. Give him the room. Give him the night."
|
||||||
|
|
||||||
|
### Passive
|
||||||
|
|
||||||
|
> "Two locks on that door and neither has been oiled this decade. Either there is nothing behind it, or nothing needs to be."
|
||||||
|
|
||||||
|
## Firing frequency
|
||||||
|
|
||||||
|
3 per hour (target).
|
||||||
Reference in New Issue
Block a user