An interactive fiction engine that replicates Inky's preview player using SugarCube Twee files. Stories render as a continuous scroll of accumulating text with fade-in paragraphs, dividers between passages, and styled choices — matching Inky's aesthetic and behavior.
The first story built with it is The Cliff Wraith, live at: https://mumblequatch.github.io/the-cliff-wraith/
260303_ CYOA Project/
├── Script/
│ └── 15_The_Cliff_Wraith.twee ← latest clean version
├── inky-player-template.twee ← start new stories from this
├── publish-twee.sh ← reference copy of the publish script
└── handoff-notes.md ← this file
Installed on Mac:
~/scripts/publish-twee— the publish script (invoked via Keyboard Maestro)~/.tweego/— tweego compiler + SugarCube 2.37.3 (auto-installed on first publish)
GitHub: mumblequatch/the-cliff-wraith — GitHub Pages enabled, deploys from main.
The StoryScript and StoryStylesheet passages contain a custom rendering engine that intercepts SugarCube's normal output. Key mechanics:
- SugarCube's
#passagesdiv is hidden (viavisibility: hidden, NOTdisplay: none— the latter breaks the:passagedisplayevent). - A custom
#scroll-container>#text-bufferstructure is injected into the body. - On each
:passagedisplayevent, the engine grabs the passage's raw HTML, splits it on double<br>breaks (SugarCube does NOT wrap text in<p>tags), wraps each block in a<p>, and appends it to#text-buffer. - Link detection: if a
<p>contains adata-passagelink AND surrounding text, it's an inline link (stays in prose, always blue). If a<p>contains ONLY a link, it's a standalone choice (grey on desktop hover, blue on mobile/tap). - Fade-in uses jQuery
.animate()with 200ms minimum separation between elements. - Smart scroll: short passages scroll to show content at the bottom; tall passages scroll to the top of the new content so the reader starts at the beginning.
- Session reset:
sessionStorage.clear()on load so browser refresh restarts the story.
<<set>>, <<if>>/<<else>>/<<elseif>>, and <<link>> all work. SugarCube processes them before :passagedisplay fires, so the engine only ever sees the resulting HTML. Example from The Cliff Wraith:
:: take_card
<<set $hasCard to true>>You slip the card into your jacket pocket.
:: meeting_amanda
<<if $hasCard>>You pat your pocket — Kendrick's card is still there.<<else>>You wish you'd taken that cabbie's card. Too late now.<</if>>
- Copy
inky-player-template.tweeand rename it. - Replace
My New Storyin:: StoryTitle. - Generate a new IFID (any UUID generator, or
uuidgenin Terminal). - Set
"start"in:: StoryDatato your first passage name. - Write passages. The
.tweeextension is just a text file — you can rename to.txtto edit, then rename back.
The KM shortcut triggers ~/scripts/publish-twee, which:
- Opens a file picker for the
.tweefile - Compiles to HTML via tweego
- Prompts for a GitHub repo name (defaults to the StoryTitle, slugified)
- Creates the repo if it doesn't exist, or updates it if it does
- Enables GitHub Pages and opens the URL in the browser
First run auto-installs tweego (x64 binary, works on Apple Silicon via Rosetta) and SugarCube 2.37.3 to ~/.tweego/.
- Font: Atkinson Hyperlegible Next, weight 300, 19px, line-height 1.75
- Colors:
#CCCCCCtext on#1A1A1Abackground - Choices:
#555grey on desktop (blue#06C2FEon hover), blue by default on mobile - Inline links: always
#06C2FE, underline on hover - Dividers: 1px
#333between passages - Max width: 580px centered column
- SugarCube does NOT wrap passage text in
<p>elements. It renders raw text nodes +<br>+<a>. The engine handles this by splitting on<br><br>and wrapping blocks manually. #passagesmust usevisibility: hidden(notdisplay: none) or SugarCube won't fire its:passagedisplayevent.- The tweego macOS build is x64 only — no arm64 release exists. Works fine on Apple Silicon through Rosetta.
- The
!characters in the Dropbox path (!Inbox/!PRO/) require single quotes in zsh to avoid history expansion.