Skip to content

Releases: storylet-studio/storylets

Storyletter 0.1.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 16:39

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 .storyletsc bundle 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 into app-update.yml and every auto-update then fails
    verification.
  • File associations. On macOS a .storylets project is a PACKAGE, so Finder opens it as one
    document rather than a folder to wander into; .storyletsc and .storyletpack get 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

Choose a tag to compare

@github-actions github-actions released this 30 Aug 21:07

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 .storyletsc bundle 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 @story and 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, and src/ to read.
    About 900 lines of framework-free TypeScript over the runtime's public API.

Storylet Engine Unreal 0.1.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 19:36

Added

  • Shared scarcity (design/shared-scarcity.md): shared on a Deck and a
    Card plus sharedCopies, so a pile (or one card) is scarce across flows.
    Claims count every live flow's board; a shared redraw: never is spent for
    everyone on the first play; a finite redraw stays personal. Two new trace
    verdicts, ClaimedElsewhere and Taken.
  • UStoryletEngine::GetRunLog() / ClearRunLog() and storylets::Engine::log():
    the RUN's log, every flow's events in one order, each FStoryletLogEntry
    carrying the Flow it 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, and OpenFlow / GetFlow / Flows /
    CloseFlow / Reset) plus UStoryletFlow (one playthrough: the play
    verbs, the merged property view, the log, Close / IsClosed /
    GetFlowId), both Blueprint-callable and both named after Patterplay's
    UPatterEngine / UPatterFlow so a project running both engines reads
    the same. This REPLACES the UStoryletSession of 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's UPatterSave): the
    .storyletsave string boundary moved off the session object. The file is
    storylets/savefile@1 - the engine's envelope, every live flow inside it,
    and the current @world values 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. hello carries flows, new
    flowOpen / flowClose frames announce participants joining and leaving,
    and every trace and board frame names its flow. 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 in packages/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, OnBundle on the game thread, the static
    ApplyLiveBundle(Engine, Data, Error) helper), the std-only
    storylets::LiveLinkClient and frame builders in Storylets/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 fixture packages/conformance/live-link/ byte for byte.
  • The core's deal, evict, play and turns trace 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 .storyletsc JSON verbatim, compiled behind a Pimpl in PostLoad, with
    LoadError readable on a broken asset and the BP-callable
    LoadFromJsonString side door), the session object since split into
    UStoryletEngine + UStoryletFlow above (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 StoryletEngineEditor module: the .storyletsc UFactory +
    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 from Demos/ 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
    JsonValue tree, expression AST + evaluator + storylets dialect +
    matched-constraint specificity, the compiled bundle model + loader + save
    envelope, and the full Session (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
    .storyletsc factory), StoryletEngineRuntime.Build.cs and 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 neutral JsonValue.

Storylet Engine Unity 0.1.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 19:35

Added

  • Shared scarcity (design/shared-scarcity.md): Shared on Deck and Card plus
    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: never is spent for everyone on the first play; a finite redraw
    stays personal, because a cooldown is an absolute turn of a per-flow clock.
    Two new trace verdicts, ClaimedElsewhere and Taken, 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 its Flow. 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): Engine owns the bundle, the shared state and
    @world; a Flow is one playthrough across it, and every play verb lives
    there. OpenFlow / GetFlow / Flows / CloseFlow / Reset manage 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. StoryletBundleAsset.CreateSession is now CreateEngine, and
    StoryletDebug keys 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 the session of 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 full Session (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.
  • StoryletSave in the Json layer: the .storyletsave string 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 .storyletsc JSON persisted verbatim, compiled
    bundle rebuilt lazily, parse failures readable on LoadError,
    CreateSession(seed)), and the StoryletDebug session registry (weakly
    held, OnChanged event) 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
    .storyletsave files, 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 with StoryletDebug. The same demo ships for
    Godot, Unreal and JavaScript. It replaces the Samples~/BoardDemo UPM
    sample, which is retired: one home for the demo, so the two copies cannot
    drift.
  • .meta files 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. hello carries flows, new
    flowOpen / flowClose frames announce participants joining and leaving,
    and every trace and board frame names its flow. 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 in packages/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 from Update()) hands back pushed bundles, which
    StoryletLiveBundle.TryParsePush + Apply (Json layer) swap in under the
    run, state kept; SetBuild re-hellos. StoryletDebug.RegisterLink lets
    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

Choose a tag to compare

@github-actions github-actions released this 30 Aug 19:41

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 (@world and @story), open
    and close flows, save and load a game, subscribe to the trace and read the run log.
  • The flow: one playthrough. peek the stock, deal a hand, read the ranked result with
    the trace that says why each card is there, list outcomes, play one, and advance turns.
  • Qualities, scarcity and cooldowns, as the corpus pins them: ordered stage ladders,
    shared decks 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

Choose a tag to compare

@github-actions github-actions released this 30 Aug 19:35

Added

  • Shared scarcity (design/shared-scarcity.md): shared on a deck (or a card,
    overriding its deck) plus sharedCopies, so a pile is scarce across flows
    rather than one each per participant. Claims count every live flow's board;
    a shared redraw: "never" is spent for everyone on the first play; a finite
    redraw stays personal, because a cooldown is an absolute turn of a per-flow
    clock. Two new trace verdicts, claimed-elsewhere and taken.
  • engine.log() / clear_log(): the RUN's log, every flow's events in one
    order with each entry naming its flow. 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): StoryletEngine owns the bundle, the shared state
    and @world; a StoryletFlow is one playthrough across it, and every play
    verb lives there. open_flow / get_flow / flows / close_flow /
    reset manage 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. StoryletDebug keys 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 by test/test_state_panel.gd.
    This REPLACES the session of 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 .storyletsc EditorImportPlugin and StoryletBundleResource (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 the StoryletDebug
    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. hello carries flows, new
    flowOpen / flowClose frames announce participants joining and leaving,
    and every trace and board frame names its flow. 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 in packages/conformance/live-link/, which now scripts a
    second participant opening, playing and closing.
  • Live Link: StoryletLiveLink streams the attached engine's trace and
    board snapshots to Storyletter (storyletengine/debug@1) and takes edited
    bundles back into the running game (bundle_pushed then
    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

Choose a tag to compare

@github-actions github-actions released this 30 Aug 19:26
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.