Authoring grammar

JSON document layout, nodes, choices, gates, effects, and conventions for Blackbox story projects.

A Blackbox game is a set of authored JSON documents. scenario.json lists chapters; each chapter file holds nodes; nodes hold text blocks and choices. Edit content with patch_documents (granular) or save_documents (whole document); add a chapter with add_chapter; then lint_project and simulate_project to validate and explore reachability.

These shapes mirror the canonical JSON wire format. Use lint_project for validation and the MCP server for patch_documents, save_documents, and add_chapter.

Documents

scenario.jsoncom.blackbox.scenario

FieldType
formatVersionnumber (1)
titlestring
revisionstring, e.g. "1.0" (story version label, not the editor revision)
randomSeedinteger (optional)
defaultStatsobject of stat name -> integer, e.g. { resolve: 2, insight: 2 }
itemsRefstring path, default items.json
charactersRefstring path, default characters.json
assetsRefstring path, default assets.json
catalogRefstring path to events/flags catalog, e.g. catalog.json (optional)
libraryRefstring path, default library.json
cookRefstring path, default bundle.cook.json
deathNodeinline node used as the default death screen (optional)
chaptersarray of { id, title, ref } in play order

chapter_<id>.jsoncom.blackbox.chapter

FieldType
formatVersionnumber (1)
idstring, matches the scenario chapter id
titlestring
startNodeIdstring, id of the node the chapter opens on
nodesobject of nodeId -> Node (see node shape)

items.jsoncom.blackbox.items

FieldType
itemsobject of itemId -> { id, name, description?, examineText?, iconRef? }

characters.jsoncom.blackbox.characters

FieldType
charactersobject of characterId -> { id, name, subtitle?, color?, portraitRef?, voiceRef? }

assets.jsoncom.blackbox.assets.bundle

FieldType
texturesobject of id -> texture descriptor (path under textures/)
musicobject of id -> music descriptor (path under music/)
sfxobject of id -> sfx descriptor (path under sfx/)

Upload the binary files with upload_media before referencing them here.

catalog.jsoncom.blackbox.catalog

FieldType
eventsobject of eventId -> { title, description?, internal? } (story beats)
flagsobject of flagId -> { title, description?, internal? } (branchable state)

Referenced by scenario.catalogRef. patch_documents collections 'event' and 'flag' write here.

library.jsoncom.blackbox.library

FieldType
snippetsobject of id -> reusable text block(s); reference as "@<id>" or { "$snippet": "<id>" }
templatesobject of id -> node template; reference via node "$extends"
conditionsobject of name -> named gate; reference via { "type": "condition", "id": "<name>" }

Node

A single story beat. Key in the chapter's nodes map must equal its id.

FieldType
idstring
titlestring
mode"normal" (default) | "game_over" (ends the run) | "ending" (offers restart)
onEnterarray of Effect, applied when the node is entered
backgroundRefstring texture id (optional)
textarray of TextBlock
choicesarray of Choice
$extendsstring template id from library.templates (optional, advanced)
$mergeobject controlling array-merge behaviour when $extends is set (advanced)

Text block

Either a snippet reference ("@<id>" string, or { "$snippet": "<id>", params? }) or an inline block keyed by kind.

Kinds: paragraph, dialogue, thought, stage_direction.

FieldType
kindone of the kinds above
textstring
speakercharacter id (for dialogue/thought)
side"left" | "right" | "center" (speaker placement)
whenGate; block only shows when the gate passes
elsestring shown instead when when fails
actorcharacter id; sugar for when: { type: hasFlag, flag: actor<id> }

Choice

An option presented at a node.

FieldType
idstring (unique within the node)
labelstring shown to the player
sfxsfx id played on select (optional)
gototarget nodeId in the SAME chapter (omit when using action)
actioncross-chapter / menu transition instead of goto (see actions). e.g. { type: gotoChapter, chapterId, nodeId? }
effectsarray of Effect applied when the choice is taken
requiresGate; when unmet the choice is hidden, or disabled if disabledReason is set
whenGate; the choice only appears when it passes
unlessGate; the choice is hidden when it passes
disabledReasonstring shown when requires is unmet (keeps the choice visible but disabled)
whenDisabledReasonstring shown when when is unmet but you still want it visible
unlessDisabledReasonstring shown when unless matches but you still want it visible
checkSkillCheck; resolves the choice via a dice roll (see check)

A choice needs exactly one resolution: goto, action, or check.

Gates

A gate is either a single { type, ... } node or an ARRAY of nodes (array = logical AND). Most leaf gates accept an optional disabledReason.

TypeShape
hasItem{ type: hasItem, itemId, count?: 1, disabledReason? }
hasFlag{ type: hasFlag, flag, value?: any, disabledReason? }
statGte{ type: statGte, stat, value, disabledReason? }
statLte{ type: statLte, stat, value, disabledReason? }
statEq{ type: statEq, stat, value, disabledReason? }
visited{ type: visited, nodeId, disabledReason? }
atNode{ type: atNode, nodeId, disabledReason? }
relationshipGte{ type: relationshipGte, characterId, metric, value, disabledReason? }
relationshipLte{ type: relationshipLte, characterId, metric, value, disabledReason? }
relationshipEq{ type: relationshipEq, characterId, metric, value, disabledReason? }
actorPresent{ type: actorPresent, characterId, disabledReason? }
condition{ type: condition, id, disabledReason? } — reference a named gate in library.conditions
all{ type: all, conditions: Gate[] }
any{ type: any, conditions: Gate[] }
not{ type: not, condition: Gate }

Effects

Applied via node.onEnter or choice.effects. *Expr variants take a string expression.

TypeShape
setFlag{ type: setFlag, flag, value } or { type: setFlag, flag, valueExpr }
modifyStat{ type: modifyStat, stat, amount } or { type: modifyStat, stat, amountExpr }
addItem{ type: addItem, itemId, count?: 1 } or { ..., countExpr }
removeItem{ type: removeItem, itemId, count?: 1 } or { ..., countExpr }
addEvent{ type: addEvent, eventId }
playMusic{ type: playMusic, track }
stopMusic{ type: stopMusic }
playSfx{ type: playSfx, sfx }
roll{ type: roll, ..., storeFlag? } (advanced; stores a dice result into a flag)
modifyRelationship{ type: modifyRelationship, characterId, metric, amount } or { ..., amountExpr }
setActorPresent{ type: setActorPresent, characterId, present }

Actions

Used on a choice instead of goto for non-local transitions.

TypeShape
gotoChapter{ type: gotoChapter, chapterId, nodeId? } — nodeId defaults to that chapter's startNodeId
restartGame{ type: restartGame, startNodeId }
openMainMenu{ type: openMainMenu }
openLoadMenu{ type: openLoadMenu }

Skill check

A skill check on a choice. Rolls against a stat versus a difficulty.

FieldType
statstring stat name
difficultyinteger target number
modifierexpression added to the roll (optional)
labelstring shown during the check (optional)
rollMode"normal" (default) | "advantage" | "disadvantage"
maxAttemptsinteger (optional)
onSuccess{ effects?: Effect[], goto?: nodeId }
onFailure{ effects?: Effect[], goto?: nodeId }
onExhausted{ effects?: Effect[], goto?: nodeId } when maxAttempts is used (optional)

Conventions

  • Name a new chapter file chapter_<id>.json and register it in scenario.chapters.
  • Convention: the chapter's start node id is <chapterId>_start.
  • Node ids are unique within a chapter; goto stays within the chapter, gotoChapter crosses chapters.
  • Reachable endings use node mode "ending"; failure states use "game_over".
  • Run lint_project for structural errors and simulate_project to confirm endings are reachable.