@patterkit/runtime 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/index.cjs +127 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -2
- package/dist/index.d.ts +73 -2
- package/dist/index.js +127 -2
- package/dist/index.js.map +1 -1
- package/dist/patterplay.min.js +2 -2
- package/dist/patterplay.min.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,35 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [0.3.1] - 2026-07-22
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Version bump only, to keep the four Patterplay runtimes in lockstep. This release fixes
|
|
24
|
+
Unreal-only build issues (see the Unreal changelog and #25); the JavaScript runtime is unchanged.
|
|
25
|
+
|
|
26
|
+
## [0.3.0] - 2026-07-21
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **Host navigation.** `flow.goto(scene, block?)` sends a running flow to a Game ID address, behaving
|
|
30
|
+
exactly like an authored `go` jump: the target scene's `onEntry` runs, arriving counts as a visit, and
|
|
31
|
+
the callstack is replaced (pending call-returns discarded). Being a host action it lands immediately -
|
|
32
|
+
the rest of the snippet being delivered is abandoned and a pending choice dropped - and it MOVES the
|
|
33
|
+
cursor without resetting the flow, so variation, visit counts and per-flow properties carry on. Returns
|
|
34
|
+
`false` with the cursor untouched when the address does not resolve; a block address is scene-scoped.
|
|
35
|
+
- **`engine.runFlow(name, scene, block?)`**, the one-call form: opens the named flow if it does not
|
|
36
|
+
exist, moves it if it does, runs to the next stop and returns the beats played. Reusing the name is the
|
|
37
|
+
point - a flow owns its selector cursors, so a shuffle keeps its bag and a "once each" list keeps its
|
|
38
|
+
place from call to call. `[]` means the address has nothing left to give; an unresolvable address
|
|
39
|
+
throws, so the two are never confused.
|
|
40
|
+
- `flow.isClosed`, and `engine.sceneAddress` / `engine.blockAddress` are now matched by all four runtimes
|
|
41
|
+
(they were JS-only).
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- Dropping a flow now FINISHES it. `closeFlow`, `engine.reset()` and re-opening a name all leave the old
|
|
45
|
+
`Flow` inert (`advance()` reports the end, `goto()` refuses), so a stale reference a game still holds
|
|
46
|
+
can no longer keep running scene entry effects and moving shared state. Re-opening a name still
|
|
47
|
+
replaces (and so resets) that flow - use `runFlow` when a speaker's variation state should carry on.
|
|
48
|
+
|
|
20
49
|
## [0.2.2] - 2026-07-13
|
|
21
50
|
|
|
22
51
|
### Changed
|
package/dist/index.cjs
CHANGED
|
@@ -153,6 +153,9 @@ var Engine = class _Engine {
|
|
|
153
153
|
nodeIndex,
|
|
154
154
|
blockIndex,
|
|
155
155
|
blockById,
|
|
156
|
+
sceneGameIdToId: this.sceneGameIdToId,
|
|
157
|
+
blockGameIdToId: this.blockGameIdToId,
|
|
158
|
+
// same instances the engine resolves with
|
|
156
159
|
tagIndex: buildTagIndex(bundle),
|
|
157
160
|
shared,
|
|
158
161
|
patterSharedDecls,
|
|
@@ -261,16 +264,56 @@ var Engine = class _Engine {
|
|
|
261
264
|
/**
|
|
262
265
|
* Open (and start) a named flow. Each flow has its own cursor, PRNG, and per-flow
|
|
263
266
|
* half of the scopes (not-shared `@patter`/`@scene`); all flows share the shared
|
|
264
|
-
* half.
|
|
267
|
+
* half.
|
|
268
|
+
*
|
|
269
|
+
* Re-opening an existing id REPLACES it with a fresh flow, and CLOSES the old one
|
|
270
|
+
* ({@link Flow.close}) so a host still holding it cannot keep driving the shared world. Replacing is
|
|
271
|
+
* therefore a reset: that name's cursor, visit counts, selector cursors (so any shuffle / once-each
|
|
272
|
+
* position) and per-flow properties all start over.
|
|
273
|
+
*
|
|
274
|
+
* Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
|
|
275
|
+
* call to reach for when you want a speaker's variation state to carry on.
|
|
265
276
|
*/
|
|
266
277
|
openFlow(id, opts = {}) {
|
|
267
278
|
const sceneId = this.resolveSceneRef(opts.scene);
|
|
268
279
|
const blockId = this.resolveBlockRef(sceneId, opts.block);
|
|
280
|
+
this.flowsById.get(id)?.close();
|
|
269
281
|
const flow = new Flow(id, this.host, opts.seed ?? this.defaultSeed);
|
|
270
282
|
this.flowsById.set(id, flow);
|
|
271
283
|
flow.start(sceneId, blockId);
|
|
272
284
|
return flow;
|
|
273
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* "Play this address and give me everything it produced" - the one-call form of the bark / one-shot
|
|
288
|
+
* pattern. The NAMED flow is reused if it already exists (moved with {@link Flow.goto}) and opened at
|
|
289
|
+
* the address if not, then run to its next stop, returning every beat it played.
|
|
290
|
+
*
|
|
291
|
+
* Calling it again with the SAME NAME does NOT replace the flow - it reuses it, and that is the whole
|
|
292
|
+
* point. A flow owns its selector cursors, visit counts and per-flow properties, so reusing one lets a
|
|
293
|
+
* **shuffle keep its bag** and an **"once each" list keep its place**: successive calls give the next
|
|
294
|
+
* variation instead of replaying the first forever. (A fresh flow each time would reset all of it,
|
|
295
|
+
* unless every such group happened to be authored `shared`.) Use one name per independent speaker;
|
|
296
|
+
* different names never share per-flow state.
|
|
297
|
+
*
|
|
298
|
+
* This is exactly where it differs from {@link openFlow}, which REPLACES a flow of the same name and
|
|
299
|
+
* so resets that variation state. Never mix the two on one name unless you mean to start over.
|
|
300
|
+
*
|
|
301
|
+
* Returns the played beats in order - `[]` means the address had nothing left to give (an exhausted
|
|
302
|
+
* variation list, say), which is the signal to fall back to other content. It THROWS on an address
|
|
303
|
+
* that does not resolve: unlike `goto` (a navigation primitive, where probing is legitimate), naming a
|
|
304
|
+
* location here asserts it exists, and keeping `[]` unambiguous is worth more than a soft failure.
|
|
305
|
+
*
|
|
306
|
+
* A run that stops at a CHOICE returns the beats up to it and leaves the choice pending on the flow -
|
|
307
|
+
* fetch it with `engine.getFlow(name)?.getChoices()`.
|
|
308
|
+
*/
|
|
309
|
+
runFlow(flow, scene, block) {
|
|
310
|
+
const existing = this.flowsById.get(flow);
|
|
311
|
+
if (!existing) return this.openFlow(flow, { scene, block }).advanceToStop().played;
|
|
312
|
+
if (!existing.goto(scene, block)) {
|
|
313
|
+
throw new Error(`runFlow: address not found: ${scene}${block === void 0 ? "" : ` / ${block}`}`);
|
|
314
|
+
}
|
|
315
|
+
return existing.advanceToStop().played;
|
|
316
|
+
}
|
|
274
317
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
275
318
|
resolveSceneRef(ref) {
|
|
276
319
|
if (ref == null) return void 0;
|
|
@@ -408,8 +451,10 @@ var Engine = class _Engine {
|
|
|
408
451
|
flows() {
|
|
409
452
|
return [...this.flowsById.values()];
|
|
410
453
|
}
|
|
411
|
-
/** Close (remove) a flow.
|
|
454
|
+
/** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
|
|
455
|
+
* holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
|
|
412
456
|
closeFlow(id) {
|
|
457
|
+
this.flowsById.get(id)?.close();
|
|
413
458
|
this.flowsById.delete(id);
|
|
414
459
|
}
|
|
415
460
|
/**
|
|
@@ -419,6 +464,7 @@ var Engine = class _Engine {
|
|
|
419
464
|
* After reset, open fresh flows with `openFlow`.
|
|
420
465
|
*/
|
|
421
466
|
reset() {
|
|
467
|
+
for (const flow of this.flowsById.values()) flow.close();
|
|
422
468
|
this.flowsById.clear();
|
|
423
469
|
this.host.shared.reseedOwned("patter", this.host.patterSharedDecls);
|
|
424
470
|
this.host.sharedVisits.clear();
|
|
@@ -510,6 +556,9 @@ var Flow = class {
|
|
|
510
556
|
// `activeSnippet`/`beatIndex`.
|
|
511
557
|
started = false;
|
|
512
558
|
flowEnded = false;
|
|
559
|
+
/** Closed by the engine (see `close()`). Terminal, and distinct from `flowEnded`: an ENDED flow is
|
|
560
|
+
* merely out of content and `goto` revives it; a CLOSED one is finished for good. */
|
|
561
|
+
closed = false;
|
|
513
562
|
currentSceneId = null;
|
|
514
563
|
stack = [];
|
|
515
564
|
activeSnippet = null;
|
|
@@ -616,6 +665,81 @@ var Flow = class {
|
|
|
616
665
|
reset(sceneId, blockId) {
|
|
617
666
|
this.start(sceneId, blockId);
|
|
618
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
|
|
670
|
+
* `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
|
|
671
|
+
* `call` returns are discarded, just as a goto inside a call does.
|
|
672
|
+
*
|
|
673
|
+
* `scene` and `block` are host-facing gameIds (spec §6) or internal ids; `block` is scene-scoped, so
|
|
674
|
+
* it is looked up within `scene`. `"END"` ends the flow. To move within the current scene, pass the
|
|
675
|
+
* current scene's address again (`flow.currentScene` -> `engine.sceneAddress`).
|
|
676
|
+
*
|
|
677
|
+
* This is HOST navigation, not authoring, and it takes effect IMMEDIATELY: any beats left in the
|
|
678
|
+
* snippet being delivered are abandoned, and a pending choice is dropped. The format stops an AUTHOR
|
|
679
|
+
* writing a divert into the middle of a snippet; a host teleport is out-of-band, like `reset()` or
|
|
680
|
+
* `loadGame()`. A flow that never started starts here; one that already ended resumes here.
|
|
681
|
+
*
|
|
682
|
+
* Returns false - leaving the cursor exactly where it was - if the address does not resolve. Per-flow
|
|
683
|
+
* state (properties, visit counts, selector cursors) is untouched either way: this MOVES, never resets.
|
|
684
|
+
*/
|
|
685
|
+
goto(scene, block) {
|
|
686
|
+
if (this.closed) return false;
|
|
687
|
+
if (scene === "END") {
|
|
688
|
+
this.started = true;
|
|
689
|
+
this.pendingChoice = null;
|
|
690
|
+
this.pendingPromptBeat = null;
|
|
691
|
+
this.pendingPromptOwnerId = null;
|
|
692
|
+
this.activeSnippet = null;
|
|
693
|
+
this.beatIndex = 0;
|
|
694
|
+
this.flowEnded = true;
|
|
695
|
+
this.stack = [];
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
const sceneId = this.host.sceneGameIdToId.get(scene) ?? (this.host.bundle.scenes[scene] ? scene : void 0);
|
|
699
|
+
if (sceneId === void 0) return false;
|
|
700
|
+
let blockId;
|
|
701
|
+
if (block !== void 0) {
|
|
702
|
+
blockId = this.host.blockGameIdToId.get(sceneId)?.get(block) ?? (this.host.blockIndex.get(block)?.sceneId === sceneId ? block : void 0);
|
|
703
|
+
if (blockId === void 0) return false;
|
|
704
|
+
}
|
|
705
|
+
if (!this.started) {
|
|
706
|
+
this.start(sceneId, blockId);
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
this.pendingChoice = null;
|
|
710
|
+
this.pendingPromptBeat = null;
|
|
711
|
+
this.pendingPromptOwnerId = null;
|
|
712
|
+
this.activeSnippet = null;
|
|
713
|
+
this.beatIndex = 0;
|
|
714
|
+
this.flowEnded = false;
|
|
715
|
+
this.enterTarget(blockId ?? sceneId, "jump");
|
|
716
|
+
this.settle();
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Finish this flow for good. Engine-managed: `engine.closeFlow(id)`, `engine.reset()`, and re-opening
|
|
721
|
+
* a name with `engine.openFlow` all call it on the flow being dropped.
|
|
722
|
+
*
|
|
723
|
+
* A dropped flow used to stay fully live: unregistered and invisible to `engine.flows()`, but a host
|
|
724
|
+
* still holding the object could keep advancing it, and every scene `onEntry`, shared property, world
|
|
725
|
+
* visit count and shared selector cursor it touched still landed on the engine. Closing makes that
|
|
726
|
+
* stale reference inert - `advance()` reports the end and `goto()` refuses - so a forgotten reference
|
|
727
|
+
* cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
|
|
728
|
+
*/
|
|
729
|
+
close() {
|
|
730
|
+
this.closed = true;
|
|
731
|
+
this.flowEnded = true;
|
|
732
|
+
this.stack = [];
|
|
733
|
+
this.activeSnippet = null;
|
|
734
|
+
this.beatIndex = 0;
|
|
735
|
+
this.pendingChoice = null;
|
|
736
|
+
this.pendingPromptBeat = null;
|
|
737
|
+
this.pendingPromptOwnerId = null;
|
|
738
|
+
}
|
|
739
|
+
/** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
|
|
740
|
+
get isClosed() {
|
|
741
|
+
return this.closed;
|
|
742
|
+
}
|
|
619
743
|
/** The scene the cursor is currently in - set on entry and whenever a jump crosses scenes. Read
|
|
620
744
|
* right after `advance()` to know which scene the just-played beat lives in (tooling that mirrors
|
|
621
745
|
* the playhead, e.g. an editor following a cross-scene jump). `null` before the flow has started. */
|
|
@@ -624,6 +748,7 @@ var Flow = class {
|
|
|
624
748
|
}
|
|
625
749
|
/** Run until the next line, game event, choice, or the end of the flow. */
|
|
626
750
|
advance() {
|
|
751
|
+
if (this.closed) return { type: "end" };
|
|
627
752
|
if (!this.started) throw new Error("flow has not been started");
|
|
628
753
|
if (this.pendingPromptBeat) {
|
|
629
754
|
const b = this.pendingPromptBeat;
|