Releases: storylet-studio/storylets
Release list
Storyletter 0.1.0
The first public release.
Added
- The Storyletter editor: design storylets as cards in decks, set up the box they live in,
declare the hands your game deals, and edit it all directly on the plain files on disk. - Structure and logic: boxes, decks and cards, tag groups, hand templates and hands, a guided
condition editor over the five scopes (@world,@story,@box,@deck,@hand), outcomes
with effects, and qualities as ordered ladders of named stages. - The Board: deal a real hand from the same runtime your game ships with, see the ranked result
with a line-by-line trace answering "why this hand?", play outcomes, peek the stock, poke state
and advance turns. Live Link streams a running game's state into the editor. - Two canvases: a node canvas per deck showing how cards reach each other, and a map view where
zones are drawn outlines, hands stand where they stand in the world, and background pictures sit
behind them. Frames and stickies on both. - Coverage testing: seeded playthroughs reporting what your content can actually reach, per hand,
with never-dealt and never-played called out, an overlay that puts the last run on the canvases,
and a quick-fix for content gated on state nothing writes. - Review and documentation: threaded comments anchored anywhere, a Review Feedback walk over every
thread in the project, and per-class documentation notes. - Publishing: compile the
.storyletscbundle your game loads; publish a single-file playable HTML
page that needs no server, install or programmer; export the project as a readable workbook; and
send a whole project as one.storyletpack. - Project plumbing: version-control awareness (git, Perforce, Plastic, SVN) with lock-aware saves,
file associations, go-to-anything search, and undo across every edit. - Packaging. electron-builder configuration, the macOS and Windows icon pipelines, hardened
runtime entitlements, and a tag-driven release workflow, all following Patterpad's shape.
macOS builds are signed and notarised; Windows is deliberately unsigned, because a signed
Windows build writes its publisher intoapp-update.ymland every auto-update then fails
verification. - File associations. On macOS a
.storyletsproject is a PACKAGE, so Finder opens it as one
document rather than a folder to wander into;.storyletscand.storyletpackget their own
document icons. On Windows and Linux, where there is no package concept, the.storyletproj
file inside the folder is associated instead, along with the shard types.
The Village 0.1.0
The first release, alongside the toolkit going public.
Added
- The Village as a playable browser game. The full 86-card example: five drawn regions,
thirteen places, and a hand dealt wherever you stand. Click a place, read a card, play an
outcome, watch the world change around you. - The map is the interface. Each region is its own picture with its places marked on it, and
a place carrying something to do wears a count. Playing a card in one place can light up two
others, which is the storylet model made visible rather than described. - The real runtime, unmodified. It plays the same compiled
.storyletscbundle a shipped
game would, through@storylet-studio/runtime, with no engine-side special cases. That is
the point of it: it is evidence the API is usable, not a bespoke demo. - A journal and a state panel, so a reader can see what a play wrote to
@storyand why a
card became available.
The distribution
Two deliveries from one build, which is why they are described together:
- Playable at storylet.studio/village/, deployed with
the documentation site. Most people will meet it as a link. - A zip, for keeping and for reading:
dist/to open and play offline, andsrc/to read.
About 900 lines of framework-free TypeScript over the runtime's public API.
Storylet Engine Unreal 0.1.0
Added
- Shared scarcity (design/shared-scarcity.md):
sharedon aDeckand a
CardplussharedCopies, so a pile (or one card) is scarce across flows.
Claims count every live flow's board; a sharedredraw: neveris spent for
everyone on the first play; a finiteredrawstays personal. Two new trace
verdicts,ClaimedElsewhereandTaken. UStoryletEngine::GetRunLog()/ClearRunLog()andstorylets::Engine::log():
the RUN's log, every flow's events in one order, eachFStoryletLogEntry
carrying theFlowit happened in. The Runtime State panel draws it under
the engine's name, above the per-flow sections.- The save's shared half is now
{ props, spent }, carrying what a shared
one-shot took out of the world. - Flows (design/flows.md): the plugin is now
UStoryletEngine(the bundle,
the shared state,@world, andOpenFlow/GetFlow/Flows/
CloseFlow/Reset) plusUStoryletFlow(one playthrough: the play
verbs, the merged property view, the log,Close/IsClosed/
GetFlowId), both Blueprint-callable and both named after Patterplay's
UPatterEngine/UPatterFlowso a project running both engines reads
the same. This REPLACES theUStoryletSessionof the entries below,
which was engine and single flow in one object. Flow wrappers the game
holds re-bind by name across a save load or a live bundle push, so a
Blueprint variable pointing at a flow survives both. UStoryletSave(SaveStateToJson/LoadStateFromJson, Blueprint
function library, the shape of Patterplay'sUPatterSave): the
.storyletsavestring boundary moved off the session object. The file is
storylets/savefile@1- the engine's envelope, every live flow inside it,
and the current@worldvalues beside it.Storylets/Save.h: the save/restore code is now pure std C++ in the core,
with the UE layer a thin FString shim over it, so the clang TestHost
exercises a real round trip (it could not reach the FString version).- Live Link went to protocol v2 (design/live-link.md): a client now
attaches to the ENGINE, not to one flow.hellocarriesflows, new
flowOpen/flowCloseframes announce participants joining and leaving,
and everytraceandboardframe names itsflow. The client discovers
flows itself by diffing the engine's list before each forwarded event, so a
multi-participant run needs nothing extra from the host. The editor's Board
follows one flow at a time and remembers the last board per flow. Held to
the shared fixture inpackages/conformance/live-link/, which now scripts a
second participant opening, playing and closing. - Live Link (design/live-link.md):
FStoryletLiveLink(Create / Attach /
Detach / SetBuild / Close,OnBundleon the game thread, the static
ApplyLiveBundle(Engine, Data, Error)helper), the std-only
storylets::LiveLinkClientand frame builders inStorylets/LiveLink.h
it rides on,UStoryletFlow::SubscribeTrace/UnsubscribeTrace(C++
only, held at the wrapper so it survives a swap),UStoryletEngine:: ApplyLiveBundle(in place; Blueprint-callable) and
UStoryletBundle::GetBuildId. The WebSockets dependency is dropped in
Shipping, where the link compiles to no-ops. The TestHost replays the
shared fixturepackages/conformance/live-link/byte for byte. - The core's
deal,evict,playandturnstrace events now fire AFTER
the state they report has landed (the reference's reorder, pinned by the
Live Link fixture: a board read inside a trace handler shows the event's
effect). - The UE wrapper layer (stage 2):
UStoryletBundle(UDataAsset holding the
raw.storyletscJSON verbatim, compiled behind a Pimpl in PostLoad, with
LoadErrorreadable on a broken asset and the BP-callable
LoadFromJsonStringside door), the session object since split into
UStoryletEngine+UStoryletFlowabove (deal / dealMany /
dealAllHands / peek / board / outcomes / play / playAdvancing /
advanceTurns / getTurn / listBoxes / listProperties, typed property
accessors only across the Blueprint boundary; core exceptions caught here
and surfaced as error strings/logs), the BP view structs
(StoryletTypes.h),
FStoryletDebug(the live-engine registry, no-ops in Shipping), and the
one UE-JSON-meets-the-core bridge (StoryletJsonBridge). - The
StoryletEngineEditormodule: the.storyletscUFactory +
FReimportHandler (a broken bundle still imports, error on the asset), and
the "Storylet Engine Runtime State" nomad tab (Window > Tools) - per
registered engine: Save State... / Load State... file dialogs and the
shared properties, then per open flow the property examiner with
type-aware editors (toggle / number / text / enum / flags) behind a search
filter, per-row reset disabled at default, and read-only per-box turns
(title-or-gameId, from ListBoxes) and board
sections; ~4 Hz refresh with a signature rebuild gate and PIE
attach/detach. StoryletEngine.Smoke(runtime module automation test): the
UObject-boundary seams headless - JSON -> bundle -> engine -> flow -> deal ->
outcomes -> play -> typed access -> save/load round trip -> foreign-blob
rejection.- The sample project (
../StoryletEngineDemo): map-free, finds the plugin
as a sibling via AdditionalPluginDirectories, loads the exported village
example fromDemos/and plays a storylet on BeginPlay. Config/FilterPlugin.ini(LICENSE / README / CHANGELOG ship in the
packaged plugin).
Added (stage 1)
- The pure C++ runtime core (
Source/StoryletEngineRuntime/Public/Storylets/,
std-only header-implemented C++17, no Unreal types), transliterated from the
TS reference runtime:StoryletValue(+ the port's error types),
Mulberry32(+ the contractual shuffle), the insertion-ordered map (JS Map
semantics), the state kernel (PropertyBag,ScopeRegistry), the neutral
JsonValuetree, expression AST + evaluator + storylets dialect +
matched-constraint specificity, the compiled bundle model + loader + save
envelope, and the fullSession(deal / dealMany with the dealt slice,
peek, play, outcomes, board, listBoxes, per-box turns, cooldowns, claims and
copies, the home group, hand templates with holes and the composed @hand
environment with write-back routing, ranking with stable sorts and seeded
tie shuffles, save/load with the drifted-content contract, retained log +
trace events, property rows and path-addressed get/set). - The plugin scaffold:
StoryletEngine.uplugin(Runtime module only for now;
the Editor module joins in stage 2 with the Slate examiner and the
.storyletscfactory),StoryletEngineRuntime.Build.csand the minimal
module pair. - The clang TestHost (repo-side,
ports/unreal/TestHost/, never ships):
replays the whole conformance corpus (expressions, specificity, peek,
scripted) through the C++ core with its own tiny JSON parser feeding the
core's neutralJsonValue.
Storylet Engine Unity 0.1.0
Added
- Shared scarcity (design/shared-scarcity.md):
SharedonDeckandCardplus
SharedCopies, so a pile (or one card) is scarce across flows rather than
one each per participant. Claims count every live flow's board; a shared
redraw: neveris spent for everyone on the first play; a finiteredraw
stays personal, because a cooldown is an absolute turn of a per-flow clock.
Two new trace verdicts,ClaimedElsewhereandTaken, because "claimed"
and "cooldown" would point a participant at their own board and their own
clock, neither of which has anything to do with it. Engine.Log()/ClearLog(): the RUN's log, every flow's events in one
order with each entry naming itsFlow. A flow's own log cannot show a
story action in another flow moving shared state. The Runtime State window
draws it under the engine's name, above the per-flow sections.- The save's shared half is now
{ props, spent }rather than a bare
PropsPartition, carrying what a shared one-shot took out of the world. - Flows (design/flows.md):
Engineowns the bundle, the shared state and
@world; aFlowis one playthrough across it, and every play verb lives
there.OpenFlow/GetFlow/Flows/CloseFlow/Resetmanage them,
re-opening a name replaces that flow, and a closed handle is inert. Named
after Patter'sEngine/Flowso a project running both engines reads the
same.StoryletBundleAsset.CreateSessionis nowCreateEngine, and
StoryletDebugkeys on the ENGINE (Register(engine, label)): the Runtime
State window asks each engine for its open flows, so a flow opened or closed
later appears and disappears without a registry call.
This REPLACES thesessionof the entries below, which was engine and
single flow in one object: everything they say about a "session" is now
split between the two, with the play verbs on the flow and the bundle,
shared state and save on the engine. - The pure C# runtime layer, transliterated from the TS reference runtime:
StoryletValue,Mulberry32(+ the contractual shuffle), the state kernel
(PropertyBag,ScopeRegistry), expression AST + evaluator + storylets
dialect + matched-constraint specificity, the compiled bundle model and
save envelope, and the fullSession(deal / dealMany with the dealt
slice, peek, play, outcomes, board, per-box turns, cooldowns, claims and
copies, the home group, hand templates with holes and the composed @hand
environment with write-back routing, ranking with stable sorts and seeded
tie shuffles, save/load with the drifted-content contract, retained log +
trace events, property rows and path-addressed get/set). - The Newtonsoft JSON layer (
StoryletEngine.Runtime.Json): compiled-bundle
loading into the pure model. - The dotnet TestHost (repo-side, never ships): replays the whole
conformance corpus (expressions, specificity, peek, scripted) through the
C# runtime. StoryletSavein the Json layer: the.storyletsavestring boundary over
the runtime's own save envelope (SerializeState/
DeserializeState/LoadState; foreign or malformed blobs throw),
mirroring the play-helpers save API.- The UnityEngine-touching runtime layer (
StoryletEngine.Runtime.Unity):
StoryletBundleAsset(raw.storyletscJSON persisted verbatim, compiled
bundle rebuilt lazily, parse failures readable onLoadError,
CreateSession(seed)), and theStoryletDebugsession registry (weakly
held,OnChangedevent) the examiner reads. - The editor layer (
StoryletEngine.Editor):StoryletBundleImporter
(ScriptedImporter for.storyletsc; a broken bundle still imports with
the error readable on the asset) and the Runtime State window
(Window > Storylet Engine > Runtime State): per-session Save/Load of
.storyletsavefiles, the type-aware property examiner / editor with
per-row reset and a ~4 Hz refresh that spares the focused control, and
read-only per-box turns + board contents. - The Board demo, as a committed Unity project beside this package
(../StoryletEngineDemo): open it and press Play, no package install and no
sample import, because its manifest references this folder by relative path.
The exported Hamlet bundle dealt onto a playable board (hands as groups,
cards as buttons, outcomes beneath their card, a transcript of every deal,
play and turn), registered withStoryletDebug. The same demo ships for
Godot, Unreal and JavaScript. It replaces theSamples~/BoardDemoUPM
sample, which is retired: one home for the demo, so the two copies cannot
drift. .metafiles for every shipped file and folder (generated by Unity
6000.4.6f1 against an embedded checkout of this package).- Live Link went to protocol v2 (design/live-link.md): a client now
attaches to the ENGINE, not to one flow.hellocarriesflows, new
flowOpen/flowCloseframes announce participants joining and leaving,
and everytraceandboardframe names itsflow. The client discovers
flows itself by diffing the engine's list before each forwarded event, so a
multi-participant run needs nothing extra from the host. The editor's Board
follows one flow at a time and remembers the last board per flow. Held to
the shared fixture inpackages/conformance/live-link/, which now scripts a
second participant opening, playing and closing. - Live Link:
StoryletLiveLink(Runtime) joins a running game to
Storyletter over a loopback WebSocket on a worker thread:Attach(engine)
streams the session's trace and board snapshots to the editor's Board,
TryReceive(drained fromUpdate()) hands back pushed bundles, which
StoryletLiveBundle.TryParsePush+Apply(Json layer) swap in under the
run, state kept;SetBuildre-hellos.StoryletDebug.RegisterLinklets
the Runtime State window show the link's state. The demo wires it behind
#if UNITY_EDITOR || DEVELOPMENT_BUILD; the TestHost replays the shared
fixture (packages/conformance/live-link/) against it. Trace events now
fire after the state they report has landed (deal, evict, play, turns), as
the reference runtime does.
Storylet Engine JS 0.1.0
The first public release, shipping alongside the Unity, Unreal and Godot runtimes on the same
version. All four play the same .storyletsc bundle with the same behaviour, held to the
shared conformance corpus.
Added
- The engine: load a compiled bundle, hold the shared state (
@worldand@story), open
and close flows, save and load a game, subscribe to the trace and read the run log. - The flow: one playthrough.
peekthe stock,deala hand, read the ranked result with
the trace that says why each card is there, listoutcomes,playone, and advance turns. - Qualities, scarcity and cooldowns, as the corpus pins them: ordered stage ladders,
shareddecks and cards claimed across flows, and per-flow redraw clocks. describeBundle, which reports what a bundle offers without playing it.- The seeded PRNG (
makePrng,shuffleInPlace): mulberry32, the algorithm every runtime
implements identically, so the same seed deals the same hand in all four.
The download
The release zip carries @storylet-studio/runtime and @storylet-studio/play-helpers as
folders you copy into a project, plus storyletengine.min.js, a single self-contained
browser drop-in exposing window.StoryletEngine for a plain HTML page with no build step.
Both packages ship with their dependencies inlined, deliberately: nothing here is on npm,
so the zip has to stand on its own. The one exception is that play-helpers still imports
@storylet-studio/runtime from its sibling folder, because it wraps a live engine and a
private copy would leave the host with two of them.
Storylet Engine Godot 0.1.0
Added
- Shared scarcity (design/shared-scarcity.md):
sharedon a deck (or a card,
overriding its deck) plussharedCopies, so a pile is scarce across flows
rather than one each per participant. Claims count every live flow's board;
a sharedredraw: "never"is spent for everyone on the first play; a finite
redrawstays personal, because a cooldown is an absolute turn of a per-flow
clock. Two new trace verdicts,claimed-elsewhereandtaken. engine.log()/clear_log(): the RUN's log, every flow's events in one
order with each entry naming itsflow. The state panel draws it under the
engine's name, above the per-flow sections.- The save's shared half is now
{props, spent}, carrying what a shared
one-shot took out of the world. - Flows (design/flows.md):
StoryletEngineowns the bundle, the shared state
and@world; aStoryletFlowis one playthrough across it, and every play
verb lives there.open_flow/get_flow/flows/close_flow/
resetmanage them, re-opening a name replaces that flow, and a closed
handle is inert. Named after Patter's engine / flow so a project running
both engines reads the same.StoryletDebugkeys on the ENGINE
(register(engine, "label")): the Runtime State panel asks each engine for
its open flows, so a flow opened or closed later appears and disappears
without a registry call. Covered headlessly bytest/test_state_panel.gd.
This REPLACES thesessionof the entries below, which was engine and
single flow in one object: everything they say about a "session" is now
split between the two, with the play verbs on the flow and the bundle,
shared state and save on the engine. - The pure GDScript runtime: bundle loading (
.storyletsc), sessions
(deal / peek / play / outcomes / board / per-box turns / cooldowns /
claims and copies / templates and the composed@hand), the storylets
expression dialect, matched-constraint specificity ranking, the mulberry32
per-flow PRNG, and the save envelope with the.storyletsave
string boundary (StoryletSave). - The shared state kernel pieces:
StoryletPropertyBag(typed declared
properties, the firing rule, audit hook, one clone door) and
StoryletScopeRegistry(owned / foreign scopes, save fragments). - The
.storyletscEditorImportPlugin andStoryletBundleResource(raw JSON
held verbatim, lazily parsed; a broken bundle still imports with its error
readable on the asset). StoryletStatePanel, the in-game property examiner / editor with per-box
turns, board contents and Save/Load state, plus theStoryletDebug
session registry it reads.- The Hamlet demo scene and the headless corpus / smoke TestHosts
(maintainers; not shipped). - Live Link went to protocol v2 (design/live-link.md): a client now
attaches to the ENGINE, not to one flow.hellocarriesflows, new
flowOpen/flowCloseframes announce participants joining and leaving,
and everytraceandboardframe names itsflow. The client discovers
flows itself by diffing the engine's list before each forwarded event, so a
multi-participant run needs nothing extra from the host. The editor's Board
follows one flow at a time and remembers the last board per flow. Held to
the shared fixture inpackages/conformance/live-link/, which now scripts a
second participant opening, playing and closing. - Live Link:
StoryletLiveLinkstreams the attached engine's trace and
board snapshots to Storyletter (storyletengine/debug@1) and takes edited
bundles back into the running game (bundle_pushedthen
StoryletLiveLink.apply_live_bundle, the run carried across). Debug
builds only. Trace events now fire after the state they report has landed,
so a handler reading the session inside one sees the effect.
storyletengine CLI 0.1.0
storyletengine can say which build it is A standalone binary has no package.json beside it and no npm to ask, so before this there was no way to answer "what are you running?" - the first question any support conversation opens with, and one that matters more for a downloaded executable than for anything installed by a package manager. --version, -v, and a bare `version`, all three, because people try all three. The number comes from package.json, imported and inlined at BUILD time rather than hand-copied into a constant. Both shipping paths bundle the same entry and both inline the import, verified on real artifacts rather than assumed: the tsup dist/cli.js prints 0.1.0, and so does the Bun --compile signed macOS binary on all three spellings. That is what stops the binary disagreeing with the tag its release asset is named for. The test pins the printed string to the manifest rather than to a literal, so it cannot pass by agreeing with a copy of itself. Probed both ways it can fail: with the dispatch broken it goes red, and with the version hardcoded to 9.9.9 it goes red naming both numbers. It also asserts the flag appears in the usage text, since an undiscoverable flag is only half-shipped. Patter's CLI has the same gap and does not have this fix. Written up for them rather than left as a quiet divergence.