Progress
Living notes on where we are and what's likely next.
---
Recent decisions
- Greenfield posture. Nothing is published yet. We're free to break old JSON, restructure, etc. — no migration burden.
- Cinematic skin lives at
engine/skins/cinematic/. Built fresh as a separate Vite app fromdemo-react. Theengine/skins/directory is the home for additional skins as they're built (phone, VN, scene-explorable, etc.). - Story Maker is the paid product, runtime is free. Ink/Inkle-style. No hosting on our side.
- Doppelganger story is being re-authored from
initial-outline.txtin Story Maker. We're NOT porting old JSON. Vertical slice first (opening + first fork), expand later. - Authoring loop: user authors in Story Maker → exports to
engine/skins/cinematic/public/doppelganger.json→ cinematic skin auto-loads on reload. Dogfood loop catches gaps in both the editor and the skin simultaneously. - GT/OC/OCD/BT branch labels in the outline are treated as plain branch names, not deep semantics.
- Dialogue Editor architecture: the engine's data model (one event = one NPC beat with choices) stays unchanged. Story Maker presents linear chains as a single threaded form. No engine refactor.
---
What's working
Engine (engine/src/)
- Headless story runtime, immutable state, save/load, scripted conversations, LLM character integration via
CharacterEngine, event-levelscene(background/atmosphere/audio), charactervisuals(avatar/portrait/expressions/accentColor), per-event dialogue metadata - 89 tests passing
Cinematic skin (engine/skins/cinematic/)
Layered architecture, all reading directly from engine via useTimeline / useNotifications:
BackgroundLayer— crossfade between event backgrounds, gradient fallbackAtmosphereLayer— CSS filter overlays (atmosphere-rain,-golden-hour,-dark,-fog,-noir)CharacterStageLayer— portrait with mood/expression-keyed lookup, letter+color fallbackDialogueBoxLayer— chapter, speaker name in accent color, mood labelChoicesLayer— animated choice buttons, lower-rightNotificationLayer— toasts, upper-rightAudioController— looped ambient with swap on event change- Loads
/doppelganger.jsonfrompublic/; clean missing/error boot screens
Story Maker (engine/story-maker/)
- Existing: list view (tree-nested), graph view (ReactFlow), character editor, discoveries, world settings, preview, import/export, localStorage save
- NEW: Dialogue Editor modal — multi-turn authoring as a single threaded form
- Accordion: one response open per node
- Auto-creates new dialogue events for replies (
{PARENT_ID}_REPLY,_REPLY_2, etc.) - New replies inherit parent's speaker
- "Go to Scene" picker — link to existing non-dialogue event OR create new stub
- Unlink button to reset a connection
- Cycle detection (shows "↻ existing thread: X")
- Cross-thread continuations show "→ continues to scene: X" with click-to-navigate
- EventEditor reorganized for dialogue: Dialogue section moved directly under Type/Act/Trigger; Choices section hidden when type === 'dialogue' (managed via Dialogue Editor)
- EventList filters internal dialogue nodes — only thread heads + non-dialogue scenes show in the tree; chained replies are reachable only via the Dialogue Editor
- Dialogue flow icon next to dialogue events in the list
Engine cleanup
- Removed pre-existing unused imports (
TimelineEventinEngine.ts,ConditioninTimelineManager.ts) - All tests still passing
Markdown rendering
- Tiny dependency-free parser at
engine/src/react/markdown.tsx, exported viaengine/src/react.ts - Supports
# H1,## H2,bold,italic. Blank line = paragraph; single\n=<br /> - Wired into cinematic skin's
DialogueBoxLayerand Story Maker'sGamePreview - Cinematic skin's default narrative-italic dropped — authors now italicize via
...(cleaner; conflicts with italic-on-italic browser behavior otherwise) MarkdownHintshared component (info icon + popover) on Content field in EventEditor and NPC line in Dialogue Editor
Cinematic skin polish
- Dialogue vs narrative differentiation: dialogue is wrapped in a
dialogue-blockwith an accent-colored left bar (uses speaker'saccentColorvia CSS variable). Narrative is muted prose, no chrome. Easy to tell apart at a glance. - GameControls (Save / Restart) in top-right. Persists to
localStorageundercinematic-skin-save. Auto-restores on mount if a save exists. Restart confirms, clears storage, restarts engine viarestart(). - Notifications moved to top-left to clear the right corner for controls; slide-in animation flipped to match.
- Dialogue breadcrumb thread: while the player is in a chain of dialogue events, prior NPC lines + their picks are stacked above the current line. Auto-resets when the player exits to a non-dialogue scene. Past beats render at 55% opacity; player beats use a distinct cyan-blue accent (
--color-player). Thread auto-scrolls to bottom on update. - Known limitation: the breadcrumb thread is built from the React provider's
historystate, which isn't persisted bysaveState(). After Save/reload, breadcrumbs are empty until the player makes more moves in the thread. Fix would be persisting history alongside engine state incinematic-skin-save.
Direct file save (Story Maker export)
engine/story-maker/src/utils/saveGameFile.tsuses the File System Access API (window.showSaveFilePicker) so Export opens a system save dialog — author picks the destination, file lands directly inengine/skins/cinematic/public/- Handle is cached in module memory so subsequent saves in the same session overwrite the picked file with no prompt;
idhint persists the directory across reloads - Falls back to the previous download flow on Safari/Firefox (
fallback: truein result, "Downloaded" status indicator) exportGameno longer imported directly in App; lives behindsaveGameToFile
Asset convention
- Per-skin assets live under each skin's
public/assets/{images,audio,npc_images}/ - Cinematic skin ships these as empty
.gitkeep'd directories so the convention is visible/copyable for future skins - Path convention: leading slash, e.g.
/assets/images/coffee-shop.jpg,/assets/npc_images/maya-portrait.png ImageFieldacceptssubfolderprop ('images'|'npc_images') and generates the right path; shows a hint pointing at the on-disk location- CharacterEditor avatar/portrait now use
ImageFieldwithsubfolder="npc_images"; expression placeholder updated - EventEditor audio placeholder updated to
/assets/audio/... - Story Maker doesn't serve assets itself — Story Maker's preview won't render images, but the side-by-side cinematic skin will. Author manages files on disk; Story Maker just generates correct path strings.
---
In progress
- User is authoring Doppelganger story in Story Maker. Source material:
initial-outline.txt. Scope: opening + first fork as vertical slice for the cinematic skin demo. Will expand as the FE catches up.
---
Known friction / deferred items
Skin (Layer 1 still incomplete per FRONTEND_DISPLAY.md)
- Type-on text reveal for dialogue (1C) — text just appears
- Particle effects for atmosphere — currently CSS-only filters/gradients, no rain/dust particles
- Music ducking on dialogue — audio plays at constant volume
- Conversations / chat with characters — the cinematic skin only renders the timeline, no chat UI yet. The chat layer lands when Doppelganger's content needs it.
Story Maker
- Pre-existing tsc warnings in
CharacterEditor,gameStore,StoryGraph,GamePreview,EventEditor(unused imports/params). Not from recent work; left alone.
Resolved: StoryGraph now filters internal dialogue events — only thread heads and non-dialogue scenes show in the graph. Tree view still shows all events (intentional).
Bigger items not started
- Phase 4: Plugin system + phone skin extraction (per ENGINE_PLAN.md)
- Phase 5: Migrate Doppelganger frontend to engine (per ENGINE_PLAN.md)
- Multi-character staging — single biggest cinematic upgrade per FRONTEND_DISPLAY.md Layer 3. Requires engine schema extension (
SceneStaging.cast[]) plus Story Maker stage editor plus skin support. - VN-style or 2D explorable skin — proves the abstraction; better marketing demo per FRONTEND_DISPLAY.md.
- Character system overhaul per ENGINE_IMPROVEMENTS.md (visuals are partially done; behavior config / dynamic story-context / conversation triggers / character effects-on-events still gaps).
---
Open design questions
- When does multi-character staging come in — alongside the next skin, or wait for Doppelganger content to need it?
- Schema vs UX for any future "feels clunky" authoring complaints — Dialogue Editor proved we can solve a lot with editor UX before touching the engine. Reuse that bias.
---
Deployment
Both apps deploy as Cloudflare Workers (the unified Workers & Pages flow — .workers.dev, not classic .pages.dev). Auto-rebuilds on push to main.
Cinematic skin (public demo)
- Project name:
doppelganger-test(URL:doppelganger-test.<account>.workers.dev) - Root directory:
engine/skins/cinematic - Build command:
npm install --prefix ../.. && npm install && npm run build - Build output:
dist
Story Maker (editor)
- Project name:
story-maker - Root directory:
engine/story-maker - Build command:
npm install --prefix .. && npm install && npm run build - Build output:
dist wrangler.jsoncat the project root configures SPA fallback (not_found_handling: single-page-application)
Gotchas (each cost a deploy cycle to discover)
- Monorepo install dance: each app imports engine source from
../../src/...directly. Cloudflare only runsnpm installin the project root, soengine/node_modulesis empty andtsc -bcan't resolvereactfrom engine sources. Fix:npm install --prefix ../..(or..for story-maker) before the local install. - Two copies of React → null hooks: the install dance creates separate
reactinstalls inengine/node_modulesand the app'snode_modules. Without dedupe, Vite bundles both →useRefetc. fail at runtime. Fix:resolve.dedupe: ['react', 'react-dom']in each app'svite.config.ts. - SPA fallback via
_redirectsis rejected: Cloudflare's validator flags/* /index.html 200as a "potential infinite loop" because they auto-strip.htmlfrom URLs. Usewrangler.jsoncwithassets.not_found_handling: "single-page-application"instead. - Worker project needs
wrangler.jsonc: when Cloudflare's deploy treats the project as a Worker (not classic Pages), the build needs an explicitwrangler.jsoncpointing at./dist. Thenamefield must match the existing project name on Cloudflare or it'll create a new one. - Pre-existing tsc unused-import errors blocked the production build:
tsc -bis strict and refuses to build with unused imports. Cleaned up in commit83775f4. Worth keeping the build clean going forward.
Update flow
To ship a new version of the demo: drop new doppelganger.json into engine/skins/cinematic/public/ → commit → push. Cloudflare rebuilds in ~1 min.
The Story Maker URL persists state per-browser via localStorage; it's not a multi-tenant editor. Anyone visiting will see the bundled demo (the-vanishing.json) on first load.
---
Likely next sessions
- More authoring → more friction discovery → more Story Maker / skin tweaks
- Type-on text reveal + particle atmosphere effects (Layer 1C) once the user wants more cinematic polish
- Multi-character staging when Doppelganger has a scene that demands it (group dialogue, eavesdropping, etc.)
- Second skin (VN-style or scene-explorable) once the cinematic one is solid
- Add
wrangler.jsoncto cinematic skin too for explicit/reproducible config (currently relies on auto-detection)