Skip to content

Reference

Seven types. Each names a specific function in a playable RPG quest or scene.

TypeFunctionCanvas color
hookEntry point or open thread — quest opener or future-thread seedYellow #EAB308
sceneNarrative or location beat — the “where and what”Blue #3B82F6
choicePlayer decision point — branches the board, sets state flagsPurple #8B5CF6
encounterCombat, puzzle, social conflict, or high-stakes obstacleRed #EF4444
revealInformation, twist, clue, or game-state unlock deliveredOrange #F97316
npc_beatCharacter interaction with dialogue branch logicGreen #22C55E
consequenceWorld-state outcome — what changes after a choice or eventGray #6B7280

Domain rules (enforced by validateRpgStoryboard):

  • choice and consequence frames must carry at least one stateChanges entry
  • reveal frames must carry at least one entryCondition or stateChange
  • No frame content may contain tabletop-drift terms

These five types are the core default connection vocabulary, which the RPG domain consumes directly. Other verticals (marketing, cinematic, future fourth) override this set with their own connection grammar — they do not extend it; they replace it.

TypeMeaningCanvas style
sequenceLinear progression — beat A leads to beat BSolid gray, 1.5px
choicePlayer-driven branch — one of N paths opensDashed purple, 2.5px
consequenceOutcome arc — state change drives the next beatSolid red, 2.5px
optionalConditional or skippable pathDashed dark, 1.5px
fallbackAlternate route if primary path is blockedDashed orange, 2px
TypeMeaning
sequenceLinear progression
choiceAudience segment path
dependencyUpstream deliverable required before this frame
approvalGate requiring sign-off
consequenceOutcome arc
optionalConditional path
TypeMeaning
sequenceLinear shot progression
match_cutVisual continuity cut between shots
cutawayInsert shot away from main action
reactionResponse shot tied to previous action
transitionExplicit transition effect between shots
continuityContinuity dependency between shots
parallel_actionCross-cutting / simultaneous action
fallbackAlternate coverage if primary fails

Domains own their connection vocabulary via StoryboardConnection<TConnectionType>. Core validates structural integrity (broken refs, duplicates) regardless of vocabulary.


getBeatStatus(frame) in @storyboard-os/rpg-domain is the authoritative source of readiness. The app renders the result; the domain decides it.

LevelMeaning
readyAt least 3 of the 4 spec fields completed (designerNotes, requiredAssets, testCriteria, implementationChecklist) — spec score ≥ 3 out of 4. No domain violations.
partialSome spec present but incomplete — 1 or 2 of the 4 spec fields completed (spec score 1–2).
draftNo spec present (score = 0). Exists structurally but carries no implementation depth.
blockedDomain violation: choice/consequence missing stateChanges, or reveal missing both entryConditions and stateChanges.
type MissingSpecReason =
| 'no_state_changes' // blocking: choice/consequence/reveal domain rule
| 'no_entry_or_state_change' // blocking: reveal domain rule
| 'no_designer_notes' // spec gap
| 'no_required_assets' // spec gap
| 'no_test_criteria' // spec gap
| 'no_implementation_checklist' // spec gap
| 'no_stakes' // advisory
| 'no_possible_outcomes'; // advisory

BLOCKING_REASONS (exported from @storyboard-os/rpg-domain) is the runtime set of blocking reasons. The app uses it to distinguish blockers (red ⚠) from spec gaps (gray –) in the inspector.


getFrameSignal(frame)
// → { stateChangeSummary, branchConditionSummary, readiness, specCoverage }
getFrameBadges(frame)
// → FrameBadgeDescriptor[] (STATE badge + SPEC/PARTIAL/DRAFT/BLOCKED readiness badge)
getChoiceBranchCount(frame, connections)
// → number
getBeatStatus(frame)
// → BeatStatusLevel: 'ready' | 'partial' | 'draft' | 'blocked'
getStoryboardReadiness(storyboard)
// → StoryboardReadinessSummary: { total, ready, partial, draft, blocked, byFrame }
BLOCKING_REASONS
// → ReadonlySet<MissingSpecReason>
STORYBOARD_TEMPLATES
// → StoryboardTemplateDefinition[] — all three production templates
getStoryboardTemplate(id: StoryboardTemplateId)
// → StoryboardTemplateDefinition
createStoryboardFromTemplate(templateId)
// → Storyboard with all frames fully specified

All functions are pure and immutable — they return a new project and never mutate input.

createProject(input: CreateProjectInput)
// → RpgStoryboardProject
updateFramePosition(project, frameId, position)
// → RpgStoryboardProject
updateFrameBasics(project, frameId, patch: FrameBasicsPatch)
// → RpgStoryboardProject (title, summary only — never spec fields)
updateFrameContent(project, frameId, content: Partial<FrameContent>)
// → RpgStoryboardProject
setChecklistItemComplete(project, frameId, itemIndex, complete)
// → RpgStoryboardProject (writes to progress, never to spec)
setTestCriterionComplete(project, frameId, itemIndex, complete)
// → RpgStoryboardProject (writes to progress, never to spec)
getFrameProgress(project, frameId)
// → FrameProgress (safe: returns empty record if no data yet)
getProjectProgress(project)
// → ProjectProgressSummary: { totalChecklist, doneChecklist, totalTests, doneTests }

Template boards (quest handoff):

generateHandoff(storyboard)
// → QuestHandoff: readiness summary + beats in topological order
generateMarkdown(handoff)
// → Markdown string for developer handoff document

Project boards (project handoff):

generateProjectHandoff(project)
// → ProjectHandoff: project identity + edited content + progress overlay
generateProjectMarkdown(handoff)
// → Markdown with [x]/[ ] per item, project header, progress summary
validateRpgStoryboard(storyboard)
// → ValidationResult: { valid, errors[] }
// Runs core structural rules + RPG domain rules (stateChanges, entryConditions, tabletop-drift)

getMarketingFrameBadges(frame)
// → per-frame badges: STATE, GATE, SPEC
getMarketingFrameSignal(frame)
// → readiness + signal details per frame
getMarketingBeatStatus(frame)
// → BeatStatusLevel: 'ready' | 'partial' | 'draft' | 'blocked'
getCampaignReadiness(storyboard)
// → counts by level across campaign
getCampaignLaunchReadiness(storyboard)
// → LaunchReadinessSummary: level, critical path, blockers
getCampaignCriticalPath(storyboard)
// → longest path to launch_event (topological sort)
getApprovalGateSignals(storyboard)
// → per-approval: status, blocksLaunch, requirements
getMeasurementLoopSignals(storyboard)
// → per-measurement: hasMetrics, isLoop, connections
MARKETING_TEMPLATES
// → product_launch | brand_awareness | content_campaign
getMarketingTemplate(id)
createMarketingStoryboard(templateId)
validateMarketingStoryboard(storyboard)
// → ValidationResult: { valid, errors[] }
generateCampaignBrief(storyboard)
// → CampaignBrief: campaign-scoped handoff for execution team
generateCampaignMarkdown(brief)
// → Markdown for campaign brief

getCinematicFrameBadges(frame)
// → per-frame badges: CAM, VFX, SFX, SPEC/PARTIAL/DRAFT/BLOCKED
getCinematicFrameSignal(frame)
// → readiness + camera/VFX/audio/continuity indicators
getCinematicBeatStatus(frame)
// → BeatStatusLevel: 'ready' | 'partial' | 'draft' | 'blocked'
getSequenceReadiness(storyboard)
// → counts by level across sequence
getSequenceProductionSignals(storyboard)
// → ProductionSignals: health, continuity risk, VFX burden, audio burden,
// camera complexity, duration rollup, blocked shots, pressure summary
CINEMATIC_TEMPLATES
// → trailer_flow | cutscene_sequence | explainer_video
getCinematicTemplate(id)
createCinematicStoryboard(templateId)
validateCinematicStoryboard(storyboard)
// → ValidationResult: { valid, errors[] }
generateProductionBrief(storyboard)
// → ProductionBrief: per-shot camera, assets, readiness
generateProductionMarkdown(brief)
// → Markdown for production brief

validateStoryboard(storyboard)
// → ValidationResult (structural rules only — accepts any connection vocabulary)

Generic types: StoryboardFrame<TFrameType, TContent, TAnnotationType>, StoryboardConnection<TConnectionType>, Storyboard<TFrame, TConnection>, StoryboardProject<TStoryboard>, AnyStoryboardConnection, StoryboardTemplateDefinition<TId, TStoryboard>


const routes = createStoryboardRoutes({ storyboardBasePath: '/storyboards' });
routes.boardRoute(storyboardId)
routes.frameRoute(storyboardId, frameId)
routes.projectRoute(projectId)

<StoryboardCanvas
frames={CanvasFrame[]}
connections={CanvasConnection[]}
config={StoryboardCanvasConfig}
ref={ViewportHandle}
autoFit={boolean}
onFrameClick={(id) => void}
onConnectionClick={(id) => void}
onFrameDragEnd={(id, position) => void}
/>

Viewport handle:

interface ViewportHandle {
fitToFrames(): void;
resetView(): void;
zoomIn(): void;
zoomOut(): void;
centerOnFrame(frame: CanvasFrame): void;
getScale(): number;
}

Standalone viewport math (pure functions, no React/Konva):

fitViewToFrames(frames, containerSize, padding)
centerOnFrame(frame, containerSize, currentScale)
zoomAtPoint(currentState, delta, point)
zoomFromCenter(currentState, delta, containerSize)
clampScale(scale) // enforces [MIN_SCALE=0.1, MAX_SCALE=4]

KeyAction
FFit all frames to viewport
0Reset view (scale 1, origin)
+ / =Zoom in
-Zoom out
EscapeDeselect frame / connection