@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/dist/index.d.cts
CHANGED
|
@@ -270,6 +270,10 @@ interface FlowHost {
|
|
|
270
270
|
sceneId: string;
|
|
271
271
|
}>;
|
|
272
272
|
blockById: Map<string, CompiledBlock>;
|
|
273
|
+
/** Host-facing addresses (spec §6), shared with the engine: scene gameId -> internal id (project-wide),
|
|
274
|
+
* and per-scene block gameId -> internal id. A flow needs them to resolve `goto` by address. */
|
|
275
|
+
sceneGameIdToId: Map<string, string>;
|
|
276
|
+
blockGameIdToId: Map<string, Map<string, string>>;
|
|
273
277
|
/** Author tags (#215): node id -> accumulated tags (own + every ancestor's, deduped). Built once. */
|
|
274
278
|
tagIndex: Map<string, string[]>;
|
|
275
279
|
/** The SHARED `@patter` globals (owned scope "patter") + world properties (`@world`). */
|
|
@@ -382,9 +386,41 @@ declare class Engine {
|
|
|
382
386
|
/**
|
|
383
387
|
* Open (and start) a named flow. Each flow has its own cursor, PRNG, and per-flow
|
|
384
388
|
* half of the scopes (not-shared `@patter`/`@scene`); all flows share the shared
|
|
385
|
-
* half.
|
|
389
|
+
* half.
|
|
390
|
+
*
|
|
391
|
+
* Re-opening an existing id REPLACES it with a fresh flow, and CLOSES the old one
|
|
392
|
+
* ({@link Flow.close}) so a host still holding it cannot keep driving the shared world. Replacing is
|
|
393
|
+
* therefore a reset: that name's cursor, visit counts, selector cursors (so any shuffle / once-each
|
|
394
|
+
* position) and per-flow properties all start over.
|
|
395
|
+
*
|
|
396
|
+
* Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
|
|
397
|
+
* call to reach for when you want a speaker's variation state to carry on.
|
|
386
398
|
*/
|
|
387
399
|
openFlow(id: string, opts?: OpenFlowOptions): Flow;
|
|
400
|
+
/**
|
|
401
|
+
* "Play this address and give me everything it produced" - the one-call form of the bark / one-shot
|
|
402
|
+
* pattern. The NAMED flow is reused if it already exists (moved with {@link Flow.goto}) and opened at
|
|
403
|
+
* the address if not, then run to its next stop, returning every beat it played.
|
|
404
|
+
*
|
|
405
|
+
* Calling it again with the SAME NAME does NOT replace the flow - it reuses it, and that is the whole
|
|
406
|
+
* point. A flow owns its selector cursors, visit counts and per-flow properties, so reusing one lets a
|
|
407
|
+
* **shuffle keep its bag** and an **"once each" list keep its place**: successive calls give the next
|
|
408
|
+
* variation instead of replaying the first forever. (A fresh flow each time would reset all of it,
|
|
409
|
+
* unless every such group happened to be authored `shared`.) Use one name per independent speaker;
|
|
410
|
+
* different names never share per-flow state.
|
|
411
|
+
*
|
|
412
|
+
* This is exactly where it differs from {@link openFlow}, which REPLACES a flow of the same name and
|
|
413
|
+
* so resets that variation state. Never mix the two on one name unless you mean to start over.
|
|
414
|
+
*
|
|
415
|
+
* Returns the played beats in order - `[]` means the address had nothing left to give (an exhausted
|
|
416
|
+
* variation list, say), which is the signal to fall back to other content. It THROWS on an address
|
|
417
|
+
* that does not resolve: unlike `goto` (a navigation primitive, where probing is legitimate), naming a
|
|
418
|
+
* location here asserts it exists, and keeping `[]` unambiguous is worth more than a soft failure.
|
|
419
|
+
*
|
|
420
|
+
* A run that stops at a CHOICE returns the beats up to it and leaves the choice pending on the flow -
|
|
421
|
+
* fetch it with `engine.getFlow(name)?.getChoices()`.
|
|
422
|
+
*/
|
|
423
|
+
runFlow(flow: string, scene: string, block?: string): AdvanceToStopResult["played"];
|
|
388
424
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
389
425
|
private resolveSceneRef;
|
|
390
426
|
/** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
|
|
@@ -425,7 +461,8 @@ declare class Engine {
|
|
|
425
461
|
getFlow(id: string): Flow | undefined;
|
|
426
462
|
/** All currently-open flows. */
|
|
427
463
|
flows(): Flow[];
|
|
428
|
-
/** Close (remove) a flow.
|
|
464
|
+
/** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
|
|
465
|
+
* holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
|
|
429
466
|
closeFlow(id: string): void;
|
|
430
467
|
/**
|
|
431
468
|
* Reset the whole game to its initial state: drop every flow, re-seed the shared
|
|
@@ -458,6 +495,9 @@ declare class Flow {
|
|
|
458
495
|
private rngState;
|
|
459
496
|
private started;
|
|
460
497
|
private flowEnded;
|
|
498
|
+
/** Closed by the engine (see `close()`). Terminal, and distinct from `flowEnded`: an ENDED flow is
|
|
499
|
+
* merely out of content and `goto` revives it; a CLOSED one is finished for good. */
|
|
500
|
+
private closed;
|
|
461
501
|
private currentSceneId;
|
|
462
502
|
private stack;
|
|
463
503
|
private activeSnippet;
|
|
@@ -485,6 +525,37 @@ declare class Flow {
|
|
|
485
525
|
* untouched. A clearer-named alias of `start()`.
|
|
486
526
|
*/
|
|
487
527
|
reset(sceneId?: string, blockId?: string): void;
|
|
528
|
+
/**
|
|
529
|
+
* Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
|
|
530
|
+
* `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
|
|
531
|
+
* `call` returns are discarded, just as a goto inside a call does.
|
|
532
|
+
*
|
|
533
|
+
* `scene` and `block` are host-facing gameIds (spec §6) or internal ids; `block` is scene-scoped, so
|
|
534
|
+
* it is looked up within `scene`. `"END"` ends the flow. To move within the current scene, pass the
|
|
535
|
+
* current scene's address again (`flow.currentScene` -> `engine.sceneAddress`).
|
|
536
|
+
*
|
|
537
|
+
* This is HOST navigation, not authoring, and it takes effect IMMEDIATELY: any beats left in the
|
|
538
|
+
* snippet being delivered are abandoned, and a pending choice is dropped. The format stops an AUTHOR
|
|
539
|
+
* writing a divert into the middle of a snippet; a host teleport is out-of-band, like `reset()` or
|
|
540
|
+
* `loadGame()`. A flow that never started starts here; one that already ended resumes here.
|
|
541
|
+
*
|
|
542
|
+
* Returns false - leaving the cursor exactly where it was - if the address does not resolve. Per-flow
|
|
543
|
+
* state (properties, visit counts, selector cursors) is untouched either way: this MOVES, never resets.
|
|
544
|
+
*/
|
|
545
|
+
goto(scene: string, block?: string): boolean;
|
|
546
|
+
/**
|
|
547
|
+
* Finish this flow for good. Engine-managed: `engine.closeFlow(id)`, `engine.reset()`, and re-opening
|
|
548
|
+
* a name with `engine.openFlow` all call it on the flow being dropped.
|
|
549
|
+
*
|
|
550
|
+
* A dropped flow used to stay fully live: unregistered and invisible to `engine.flows()`, but a host
|
|
551
|
+
* still holding the object could keep advancing it, and every scene `onEntry`, shared property, world
|
|
552
|
+
* visit count and shared selector cursor it touched still landed on the engine. Closing makes that
|
|
553
|
+
* stale reference inert - `advance()` reports the end and `goto()` refuses - so a forgotten reference
|
|
554
|
+
* cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
|
|
555
|
+
*/
|
|
556
|
+
close(): void;
|
|
557
|
+
/** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
|
|
558
|
+
get isClosed(): boolean;
|
|
488
559
|
/** The scene the cursor is currently in - set on entry and whenever a jump crosses scenes. Read
|
|
489
560
|
* right after `advance()` to know which scene the just-played beat lives in (tooling that mirrors
|
|
490
561
|
* the playhead, e.g. an editor following a cross-scene jump). `null` before the flow has started. */
|
package/dist/index.d.ts
CHANGED
|
@@ -270,6 +270,10 @@ interface FlowHost {
|
|
|
270
270
|
sceneId: string;
|
|
271
271
|
}>;
|
|
272
272
|
blockById: Map<string, CompiledBlock>;
|
|
273
|
+
/** Host-facing addresses (spec §6), shared with the engine: scene gameId -> internal id (project-wide),
|
|
274
|
+
* and per-scene block gameId -> internal id. A flow needs them to resolve `goto` by address. */
|
|
275
|
+
sceneGameIdToId: Map<string, string>;
|
|
276
|
+
blockGameIdToId: Map<string, Map<string, string>>;
|
|
273
277
|
/** Author tags (#215): node id -> accumulated tags (own + every ancestor's, deduped). Built once. */
|
|
274
278
|
tagIndex: Map<string, string[]>;
|
|
275
279
|
/** The SHARED `@patter` globals (owned scope "patter") + world properties (`@world`). */
|
|
@@ -382,9 +386,41 @@ declare class Engine {
|
|
|
382
386
|
/**
|
|
383
387
|
* Open (and start) a named flow. Each flow has its own cursor, PRNG, and per-flow
|
|
384
388
|
* half of the scopes (not-shared `@patter`/`@scene`); all flows share the shared
|
|
385
|
-
* half.
|
|
389
|
+
* half.
|
|
390
|
+
*
|
|
391
|
+
* Re-opening an existing id REPLACES it with a fresh flow, and CLOSES the old one
|
|
392
|
+
* ({@link Flow.close}) so a host still holding it cannot keep driving the shared world. Replacing is
|
|
393
|
+
* therefore a reset: that name's cursor, visit counts, selector cursors (so any shuffle / once-each
|
|
394
|
+
* position) and per-flow properties all start over.
|
|
395
|
+
*
|
|
396
|
+
* Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
|
|
397
|
+
* call to reach for when you want a speaker's variation state to carry on.
|
|
386
398
|
*/
|
|
387
399
|
openFlow(id: string, opts?: OpenFlowOptions): Flow;
|
|
400
|
+
/**
|
|
401
|
+
* "Play this address and give me everything it produced" - the one-call form of the bark / one-shot
|
|
402
|
+
* pattern. The NAMED flow is reused if it already exists (moved with {@link Flow.goto}) and opened at
|
|
403
|
+
* the address if not, then run to its next stop, returning every beat it played.
|
|
404
|
+
*
|
|
405
|
+
* Calling it again with the SAME NAME does NOT replace the flow - it reuses it, and that is the whole
|
|
406
|
+
* point. A flow owns its selector cursors, visit counts and per-flow properties, so reusing one lets a
|
|
407
|
+
* **shuffle keep its bag** and an **"once each" list keep its place**: successive calls give the next
|
|
408
|
+
* variation instead of replaying the first forever. (A fresh flow each time would reset all of it,
|
|
409
|
+
* unless every such group happened to be authored `shared`.) Use one name per independent speaker;
|
|
410
|
+
* different names never share per-flow state.
|
|
411
|
+
*
|
|
412
|
+
* This is exactly where it differs from {@link openFlow}, which REPLACES a flow of the same name and
|
|
413
|
+
* so resets that variation state. Never mix the two on one name unless you mean to start over.
|
|
414
|
+
*
|
|
415
|
+
* Returns the played beats in order - `[]` means the address had nothing left to give (an exhausted
|
|
416
|
+
* variation list, say), which is the signal to fall back to other content. It THROWS on an address
|
|
417
|
+
* that does not resolve: unlike `goto` (a navigation primitive, where probing is legitimate), naming a
|
|
418
|
+
* location here asserts it exists, and keeping `[]` unambiguous is worth more than a soft failure.
|
|
419
|
+
*
|
|
420
|
+
* A run that stops at a CHOICE returns the beats up to it and leaves the choice pending on the flow -
|
|
421
|
+
* fetch it with `engine.getFlow(name)?.getChoices()`.
|
|
422
|
+
*/
|
|
423
|
+
runFlow(flow: string, scene: string, block?: string): AdvanceToStopResult["played"];
|
|
388
424
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
389
425
|
private resolveSceneRef;
|
|
390
426
|
/** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
|
|
@@ -425,7 +461,8 @@ declare class Engine {
|
|
|
425
461
|
getFlow(id: string): Flow | undefined;
|
|
426
462
|
/** All currently-open flows. */
|
|
427
463
|
flows(): Flow[];
|
|
428
|
-
/** Close (remove) a flow.
|
|
464
|
+
/** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
|
|
465
|
+
* holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
|
|
429
466
|
closeFlow(id: string): void;
|
|
430
467
|
/**
|
|
431
468
|
* Reset the whole game to its initial state: drop every flow, re-seed the shared
|
|
@@ -458,6 +495,9 @@ declare class Flow {
|
|
|
458
495
|
private rngState;
|
|
459
496
|
private started;
|
|
460
497
|
private flowEnded;
|
|
498
|
+
/** Closed by the engine (see `close()`). Terminal, and distinct from `flowEnded`: an ENDED flow is
|
|
499
|
+
* merely out of content and `goto` revives it; a CLOSED one is finished for good. */
|
|
500
|
+
private closed;
|
|
461
501
|
private currentSceneId;
|
|
462
502
|
private stack;
|
|
463
503
|
private activeSnippet;
|
|
@@ -485,6 +525,37 @@ declare class Flow {
|
|
|
485
525
|
* untouched. A clearer-named alias of `start()`.
|
|
486
526
|
*/
|
|
487
527
|
reset(sceneId?: string, blockId?: string): void;
|
|
528
|
+
/**
|
|
529
|
+
* Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
|
|
530
|
+
* `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
|
|
531
|
+
* `call` returns are discarded, just as a goto inside a call does.
|
|
532
|
+
*
|
|
533
|
+
* `scene` and `block` are host-facing gameIds (spec §6) or internal ids; `block` is scene-scoped, so
|
|
534
|
+
* it is looked up within `scene`. `"END"` ends the flow. To move within the current scene, pass the
|
|
535
|
+
* current scene's address again (`flow.currentScene` -> `engine.sceneAddress`).
|
|
536
|
+
*
|
|
537
|
+
* This is HOST navigation, not authoring, and it takes effect IMMEDIATELY: any beats left in the
|
|
538
|
+
* snippet being delivered are abandoned, and a pending choice is dropped. The format stops an AUTHOR
|
|
539
|
+
* writing a divert into the middle of a snippet; a host teleport is out-of-band, like `reset()` or
|
|
540
|
+
* `loadGame()`. A flow that never started starts here; one that already ended resumes here.
|
|
541
|
+
*
|
|
542
|
+
* Returns false - leaving the cursor exactly where it was - if the address does not resolve. Per-flow
|
|
543
|
+
* state (properties, visit counts, selector cursors) is untouched either way: this MOVES, never resets.
|
|
544
|
+
*/
|
|
545
|
+
goto(scene: string, block?: string): boolean;
|
|
546
|
+
/**
|
|
547
|
+
* Finish this flow for good. Engine-managed: `engine.closeFlow(id)`, `engine.reset()`, and re-opening
|
|
548
|
+
* a name with `engine.openFlow` all call it on the flow being dropped.
|
|
549
|
+
*
|
|
550
|
+
* A dropped flow used to stay fully live: unregistered and invisible to `engine.flows()`, but a host
|
|
551
|
+
* still holding the object could keep advancing it, and every scene `onEntry`, shared property, world
|
|
552
|
+
* visit count and shared selector cursor it touched still landed on the engine. Closing makes that
|
|
553
|
+
* stale reference inert - `advance()` reports the end and `goto()` refuses - so a forgotten reference
|
|
554
|
+
* cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
|
|
555
|
+
*/
|
|
556
|
+
close(): void;
|
|
557
|
+
/** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
|
|
558
|
+
get isClosed(): boolean;
|
|
488
559
|
/** The scene the cursor is currently in - set on entry and whenever a jump crosses scenes. Read
|
|
489
560
|
* right after `advance()` to know which scene the just-played beat lives in (tooling that mirrors
|
|
490
561
|
* the playhead, e.g. an editor following a cross-scene jump). `null` before the flow has started. */
|
package/dist/index.js
CHANGED
|
@@ -122,6 +122,9 @@ var Engine = class _Engine {
|
|
|
122
122
|
nodeIndex,
|
|
123
123
|
blockIndex,
|
|
124
124
|
blockById,
|
|
125
|
+
sceneGameIdToId: this.sceneGameIdToId,
|
|
126
|
+
blockGameIdToId: this.blockGameIdToId,
|
|
127
|
+
// same instances the engine resolves with
|
|
125
128
|
tagIndex: buildTagIndex(bundle),
|
|
126
129
|
shared,
|
|
127
130
|
patterSharedDecls,
|
|
@@ -230,16 +233,56 @@ var Engine = class _Engine {
|
|
|
230
233
|
/**
|
|
231
234
|
* Open (and start) a named flow. Each flow has its own cursor, PRNG, and per-flow
|
|
232
235
|
* half of the scopes (not-shared `@patter`/`@scene`); all flows share the shared
|
|
233
|
-
* half.
|
|
236
|
+
* half.
|
|
237
|
+
*
|
|
238
|
+
* Re-opening an existing id REPLACES it with a fresh flow, and CLOSES the old one
|
|
239
|
+
* ({@link Flow.close}) so a host still holding it cannot keep driving the shared world. Replacing is
|
|
240
|
+
* therefore a reset: that name's cursor, visit counts, selector cursors (so any shuffle / once-each
|
|
241
|
+
* position) and per-flow properties all start over.
|
|
242
|
+
*
|
|
243
|
+
* Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
|
|
244
|
+
* call to reach for when you want a speaker's variation state to carry on.
|
|
234
245
|
*/
|
|
235
246
|
openFlow(id, opts = {}) {
|
|
236
247
|
const sceneId = this.resolveSceneRef(opts.scene);
|
|
237
248
|
const blockId = this.resolveBlockRef(sceneId, opts.block);
|
|
249
|
+
this.flowsById.get(id)?.close();
|
|
238
250
|
const flow = new Flow(id, this.host, opts.seed ?? this.defaultSeed);
|
|
239
251
|
this.flowsById.set(id, flow);
|
|
240
252
|
flow.start(sceneId, blockId);
|
|
241
253
|
return flow;
|
|
242
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* "Play this address and give me everything it produced" - the one-call form of the bark / one-shot
|
|
257
|
+
* pattern. The NAMED flow is reused if it already exists (moved with {@link Flow.goto}) and opened at
|
|
258
|
+
* the address if not, then run to its next stop, returning every beat it played.
|
|
259
|
+
*
|
|
260
|
+
* Calling it again with the SAME NAME does NOT replace the flow - it reuses it, and that is the whole
|
|
261
|
+
* point. A flow owns its selector cursors, visit counts and per-flow properties, so reusing one lets a
|
|
262
|
+
* **shuffle keep its bag** and an **"once each" list keep its place**: successive calls give the next
|
|
263
|
+
* variation instead of replaying the first forever. (A fresh flow each time would reset all of it,
|
|
264
|
+
* unless every such group happened to be authored `shared`.) Use one name per independent speaker;
|
|
265
|
+
* different names never share per-flow state.
|
|
266
|
+
*
|
|
267
|
+
* This is exactly where it differs from {@link openFlow}, which REPLACES a flow of the same name and
|
|
268
|
+
* so resets that variation state. Never mix the two on one name unless you mean to start over.
|
|
269
|
+
*
|
|
270
|
+
* Returns the played beats in order - `[]` means the address had nothing left to give (an exhausted
|
|
271
|
+
* variation list, say), which is the signal to fall back to other content. It THROWS on an address
|
|
272
|
+
* that does not resolve: unlike `goto` (a navigation primitive, where probing is legitimate), naming a
|
|
273
|
+
* location here asserts it exists, and keeping `[]` unambiguous is worth more than a soft failure.
|
|
274
|
+
*
|
|
275
|
+
* A run that stops at a CHOICE returns the beats up to it and leaves the choice pending on the flow -
|
|
276
|
+
* fetch it with `engine.getFlow(name)?.getChoices()`.
|
|
277
|
+
*/
|
|
278
|
+
runFlow(flow, scene, block) {
|
|
279
|
+
const existing = this.flowsById.get(flow);
|
|
280
|
+
if (!existing) return this.openFlow(flow, { scene, block }).advanceToStop().played;
|
|
281
|
+
if (!existing.goto(scene, block)) {
|
|
282
|
+
throw new Error(`runFlow: address not found: ${scene}${block === void 0 ? "" : ` / ${block}`}`);
|
|
283
|
+
}
|
|
284
|
+
return existing.advanceToStop().played;
|
|
285
|
+
}
|
|
243
286
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
244
287
|
resolveSceneRef(ref) {
|
|
245
288
|
if (ref == null) return void 0;
|
|
@@ -377,8 +420,10 @@ var Engine = class _Engine {
|
|
|
377
420
|
flows() {
|
|
378
421
|
return [...this.flowsById.values()];
|
|
379
422
|
}
|
|
380
|
-
/** Close (remove) a flow.
|
|
423
|
+
/** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
|
|
424
|
+
* holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
|
|
381
425
|
closeFlow(id) {
|
|
426
|
+
this.flowsById.get(id)?.close();
|
|
382
427
|
this.flowsById.delete(id);
|
|
383
428
|
}
|
|
384
429
|
/**
|
|
@@ -388,6 +433,7 @@ var Engine = class _Engine {
|
|
|
388
433
|
* After reset, open fresh flows with `openFlow`.
|
|
389
434
|
*/
|
|
390
435
|
reset() {
|
|
436
|
+
for (const flow of this.flowsById.values()) flow.close();
|
|
391
437
|
this.flowsById.clear();
|
|
392
438
|
this.host.shared.reseedOwned("patter", this.host.patterSharedDecls);
|
|
393
439
|
this.host.sharedVisits.clear();
|
|
@@ -479,6 +525,9 @@ var Flow = class {
|
|
|
479
525
|
// `activeSnippet`/`beatIndex`.
|
|
480
526
|
started = false;
|
|
481
527
|
flowEnded = false;
|
|
528
|
+
/** Closed by the engine (see `close()`). Terminal, and distinct from `flowEnded`: an ENDED flow is
|
|
529
|
+
* merely out of content and `goto` revives it; a CLOSED one is finished for good. */
|
|
530
|
+
closed = false;
|
|
482
531
|
currentSceneId = null;
|
|
483
532
|
stack = [];
|
|
484
533
|
activeSnippet = null;
|
|
@@ -585,6 +634,81 @@ var Flow = class {
|
|
|
585
634
|
reset(sceneId, blockId) {
|
|
586
635
|
this.start(sceneId, blockId);
|
|
587
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
|
|
639
|
+
* `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
|
|
640
|
+
* `call` returns are discarded, just as a goto inside a call does.
|
|
641
|
+
*
|
|
642
|
+
* `scene` and `block` are host-facing gameIds (spec §6) or internal ids; `block` is scene-scoped, so
|
|
643
|
+
* it is looked up within `scene`. `"END"` ends the flow. To move within the current scene, pass the
|
|
644
|
+
* current scene's address again (`flow.currentScene` -> `engine.sceneAddress`).
|
|
645
|
+
*
|
|
646
|
+
* This is HOST navigation, not authoring, and it takes effect IMMEDIATELY: any beats left in the
|
|
647
|
+
* snippet being delivered are abandoned, and a pending choice is dropped. The format stops an AUTHOR
|
|
648
|
+
* writing a divert into the middle of a snippet; a host teleport is out-of-band, like `reset()` or
|
|
649
|
+
* `loadGame()`. A flow that never started starts here; one that already ended resumes here.
|
|
650
|
+
*
|
|
651
|
+
* Returns false - leaving the cursor exactly where it was - if the address does not resolve. Per-flow
|
|
652
|
+
* state (properties, visit counts, selector cursors) is untouched either way: this MOVES, never resets.
|
|
653
|
+
*/
|
|
654
|
+
goto(scene, block) {
|
|
655
|
+
if (this.closed) return false;
|
|
656
|
+
if (scene === "END") {
|
|
657
|
+
this.started = true;
|
|
658
|
+
this.pendingChoice = null;
|
|
659
|
+
this.pendingPromptBeat = null;
|
|
660
|
+
this.pendingPromptOwnerId = null;
|
|
661
|
+
this.activeSnippet = null;
|
|
662
|
+
this.beatIndex = 0;
|
|
663
|
+
this.flowEnded = true;
|
|
664
|
+
this.stack = [];
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
const sceneId = this.host.sceneGameIdToId.get(scene) ?? (this.host.bundle.scenes[scene] ? scene : void 0);
|
|
668
|
+
if (sceneId === void 0) return false;
|
|
669
|
+
let blockId;
|
|
670
|
+
if (block !== void 0) {
|
|
671
|
+
blockId = this.host.blockGameIdToId.get(sceneId)?.get(block) ?? (this.host.blockIndex.get(block)?.sceneId === sceneId ? block : void 0);
|
|
672
|
+
if (blockId === void 0) return false;
|
|
673
|
+
}
|
|
674
|
+
if (!this.started) {
|
|
675
|
+
this.start(sceneId, blockId);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
this.pendingChoice = null;
|
|
679
|
+
this.pendingPromptBeat = null;
|
|
680
|
+
this.pendingPromptOwnerId = null;
|
|
681
|
+
this.activeSnippet = null;
|
|
682
|
+
this.beatIndex = 0;
|
|
683
|
+
this.flowEnded = false;
|
|
684
|
+
this.enterTarget(blockId ?? sceneId, "jump");
|
|
685
|
+
this.settle();
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Finish this flow for good. Engine-managed: `engine.closeFlow(id)`, `engine.reset()`, and re-opening
|
|
690
|
+
* a name with `engine.openFlow` all call it on the flow being dropped.
|
|
691
|
+
*
|
|
692
|
+
* A dropped flow used to stay fully live: unregistered and invisible to `engine.flows()`, but a host
|
|
693
|
+
* still holding the object could keep advancing it, and every scene `onEntry`, shared property, world
|
|
694
|
+
* visit count and shared selector cursor it touched still landed on the engine. Closing makes that
|
|
695
|
+
* stale reference inert - `advance()` reports the end and `goto()` refuses - so a forgotten reference
|
|
696
|
+
* cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
|
|
697
|
+
*/
|
|
698
|
+
close() {
|
|
699
|
+
this.closed = true;
|
|
700
|
+
this.flowEnded = true;
|
|
701
|
+
this.stack = [];
|
|
702
|
+
this.activeSnippet = null;
|
|
703
|
+
this.beatIndex = 0;
|
|
704
|
+
this.pendingChoice = null;
|
|
705
|
+
this.pendingPromptBeat = null;
|
|
706
|
+
this.pendingPromptOwnerId = null;
|
|
707
|
+
}
|
|
708
|
+
/** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
|
|
709
|
+
get isClosed() {
|
|
710
|
+
return this.closed;
|
|
711
|
+
}
|
|
588
712
|
/** The scene the cursor is currently in - set on entry and whenever a jump crosses scenes. Read
|
|
589
713
|
* right after `advance()` to know which scene the just-played beat lives in (tooling that mirrors
|
|
590
714
|
* the playhead, e.g. an editor following a cross-scene jump). `null` before the flow has started. */
|
|
@@ -593,6 +717,7 @@ var Flow = class {
|
|
|
593
717
|
}
|
|
594
718
|
/** Run until the next line, game event, choice, or the end of the flow. */
|
|
595
719
|
advance() {
|
|
720
|
+
if (this.closed) return { type: "end" };
|
|
596
721
|
if (!this.started) throw new Error("flow has not been started");
|
|
597
722
|
if (this.pendingPromptBeat) {
|
|
598
723
|
const b = this.pendingPromptBeat;
|