@storylet-studio/with-patter 0.1.0
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/README.md +27 -0
- package/dist/index.cjs +138 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +92 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @storylet-studio/with-patter
|
|
2
|
+
|
|
3
|
+
Play a dealt storylet card as the [Patter](https://patterkit.dev) scene named after it, and take the
|
|
4
|
+
outcome the scene reaches. The contract is by name: a card's `gameId` is its scene's address, and a
|
|
5
|
+
scene says which outcome it reached by labelling the option the player takes with the outcome's
|
|
6
|
+
`gameId` in its Game Data, or with a `gameEvent` carrying one. A card with one outcome needs neither.
|
|
7
|
+
|
|
8
|
+
DOM-free and engine-agnostic about the Storylet side: you deal, you draw, you play the outcome.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { Performer } from "@storylet-studio/with-patter";
|
|
12
|
+
|
|
13
|
+
// Both engines on your game's one ScopeRegistry, as in the Storylets-with-Patter guide.
|
|
14
|
+
const performer = new Performer(patter, new Set(["village"]));
|
|
15
|
+
|
|
16
|
+
const card = flow.deal("the-inn")[0];
|
|
17
|
+
const outcomes = flow.outcomes(card.gameId, "the-inn"); // { gameId, available }
|
|
18
|
+
let p = performer.start(card, "village", outcomes);
|
|
19
|
+
// draw p.transcript; while p.options, let the player pick:
|
|
20
|
+
p = performer.choose(p, pickedOptionId, outcomes);
|
|
21
|
+
// once p.ended, p.outcome is the outcome to play (or p.problem says why there isn't one)
|
|
22
|
+
flow.play(card.gameId, p.outcome, "the-inn");
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
One Patter flow per performed box, named after the box, entered with `goto` for each card, so
|
|
26
|
+
Patter's memory (visits, shuffles) carries across the box's cards. An option is marked not
|
|
27
|
+
`enabled` when Patter's condition on it fails or the outcome it leads to is gated shut.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Performer: () => Performer,
|
|
24
|
+
sceneIdFor: () => sceneIdFor
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/performer.ts
|
|
29
|
+
function sceneIdFor(engine, bundle, ref) {
|
|
30
|
+
if (bundle.scenes[ref]) return ref;
|
|
31
|
+
return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);
|
|
32
|
+
}
|
|
33
|
+
var MAX_STEPS = 500;
|
|
34
|
+
var Performer = class {
|
|
35
|
+
constructor(patter, boxes, sceneIdOf = () => void 0) {
|
|
36
|
+
this.patter = patter;
|
|
37
|
+
this.boxes = boxes;
|
|
38
|
+
this.sceneIdOf = sceneIdOf;
|
|
39
|
+
}
|
|
40
|
+
patter;
|
|
41
|
+
boxes;
|
|
42
|
+
sceneIdOf;
|
|
43
|
+
/** Where to report the position; set once Patterpad's Live Link is up. */
|
|
44
|
+
link;
|
|
45
|
+
/** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */
|
|
46
|
+
setEngine(patter) {
|
|
47
|
+
this.patter = patter;
|
|
48
|
+
}
|
|
49
|
+
/** Does the project say Patter performs this box? */
|
|
50
|
+
performs(boxGameId) {
|
|
51
|
+
return this.boxes.has(boxGameId);
|
|
52
|
+
}
|
|
53
|
+
/** Start a card's scene: its flow entered at the scene named after the card, run to the first
|
|
54
|
+
* choice or to its end. */
|
|
55
|
+
start(card, boxGameId, outcomes) {
|
|
56
|
+
const sceneId = this.sceneIdOf(card.gameId);
|
|
57
|
+
const p = { card: card.id, box: boxGameId, transcript: [], ended: false, ...sceneId !== void 0 ? { sceneId } : {} };
|
|
58
|
+
let flow;
|
|
59
|
+
try {
|
|
60
|
+
const existing = this.patter.getFlow(boxGameId);
|
|
61
|
+
if (existing) {
|
|
62
|
+
if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named "${card.gameId}".` };
|
|
63
|
+
flow = existing;
|
|
64
|
+
} else {
|
|
65
|
+
flow = this.patter.openFlow(boxGameId, { scene: card.gameId });
|
|
66
|
+
this.link?.flowOpened(boxGameId);
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { ...p, ended: true, problem: `The Patter project has no scene named "${card.gameId}" (${e instanceof Error ? e.message : String(e)}).` };
|
|
70
|
+
}
|
|
71
|
+
return this.run(p, flow, outcomes);
|
|
72
|
+
}
|
|
73
|
+
/** The player picks an option; the scene runs on to its next choice or its end. */
|
|
74
|
+
choose(p, optionId, outcomes) {
|
|
75
|
+
const flow = this.patter.getFlow(p.box);
|
|
76
|
+
const option = p.options?.find((o) => o.id === optionId);
|
|
77
|
+
if (!flow || !option || !option.enabled) return p;
|
|
78
|
+
flow.choose(optionId);
|
|
79
|
+
this.link?.observe(p.box, p.sceneId ?? null, null, "choose", optionId);
|
|
80
|
+
const next = {
|
|
81
|
+
...p,
|
|
82
|
+
options: void 0,
|
|
83
|
+
transcript: [...p.transcript, { kind: "chose", text: option.text }],
|
|
84
|
+
...option.outcome !== void 0 ? { lastLabel: option.outcome } : {}
|
|
85
|
+
};
|
|
86
|
+
return this.run(next, flow, outcomes);
|
|
87
|
+
}
|
|
88
|
+
/** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own
|
|
89
|
+
* properties are the registry's, and the Board resets those with the rest. */
|
|
90
|
+
reset() {
|
|
91
|
+
for (const box of this.boxes) {
|
|
92
|
+
if (this.patter.getFlow(box)) this.patter.closeFlow(box);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
run(start, flow, outcomes) {
|
|
96
|
+
const p = { ...start, transcript: [...start.transcript] };
|
|
97
|
+
for (let i = 0; i < MAX_STEPS; i++) {
|
|
98
|
+
const step = flow.advance();
|
|
99
|
+
this.link?.observe(p.box, p.sceneId ?? null, "id" in step ? step.id : null, step.type);
|
|
100
|
+
if (step.type === "line") {
|
|
101
|
+
p.transcript.push({ kind: "line", ...step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}, text: step.text });
|
|
102
|
+
} else if (step.type === "text") {
|
|
103
|
+
p.transcript.push({ kind: "text", text: step.text });
|
|
104
|
+
} else if (step.type === "gameEvent") {
|
|
105
|
+
const named = step.gameData?.["outcome"];
|
|
106
|
+
if (typeof named === "string") p.lastEvent = named;
|
|
107
|
+
} else if (step.type === "choice") {
|
|
108
|
+
p.options = step.options.map((o) => {
|
|
109
|
+
const named = o.gameData?.["outcome"];
|
|
110
|
+
const outcome = typeof named === "string" ? named : void 0;
|
|
111
|
+
const shut = outcome !== void 0 && outcomes.find((x) => x.gameId === outcome)?.available === false;
|
|
112
|
+
return { id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut, ...outcome !== void 0 ? { outcome } : {} };
|
|
113
|
+
});
|
|
114
|
+
return p;
|
|
115
|
+
} else {
|
|
116
|
+
return this.finish(p, outcomes);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };
|
|
120
|
+
}
|
|
121
|
+
/** Last word wins: an event, else the option's label, else the card's only outcome. */
|
|
122
|
+
finish(p, outcomes) {
|
|
123
|
+
const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0].gameId : void 0);
|
|
124
|
+
if (reached === void 0) {
|
|
125
|
+
return { ...p, ended: true, problem: "The scene ended without saying which outcome it reached." };
|
|
126
|
+
}
|
|
127
|
+
if (!outcomes.some((o) => o.gameId === reached)) {
|
|
128
|
+
return { ...p, ended: true, problem: `The scene reached "${reached}", which this card doesn't have.` };
|
|
129
|
+
}
|
|
130
|
+
return { ...p, ended: true, outcome: reached };
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
134
|
+
0 && (module.exports = {
|
|
135
|
+
Performer,
|
|
136
|
+
sceneIdFor
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/performer.ts"],"sourcesContent":["// @storylet-studio/with-patter: perform a dealt storylet card as the Patter scene named after it.\nexport { Performer, sceneIdFor } from \"./performer.js\";\nexport type { Beat, CardOutcome, Performance, PerformerLink, SceneOption } from \"./performer.js\";\n","// ---------------------------------------------------------------------------\n// Performing a dealt card through Patter: the card's gameId names a Patter\n// scene, the scene runs, and the scene decides which of the card's outcomes\n// was reached (the Storylets-with-Patter contract; the Hamlet's\n// `performance.js`, lifted so a game, Storyletter's Board and the playable\n// page all run the same code).\n//\n// The Hamlet's rules, kept exactly because they are the host's contract:\n// - ONE Patter flow per performed box, named after the box, entered with\n// `goto` for each card: a flow is Patter's memory, and a fresh one per card\n// would forget its visits and restart its seeded shuffles (joint demo\n// finding 14).\n// - The outcome is the LAST word: a gameEvent carrying one, else the label on\n// the option the player took, else the card's only outcome (finding 15).\n// - An option is greyed when either engine says no: Patter's `eligible`, or\n// the Storylet Engine's gate on the outcome the option names.\n//\n// This module knows nothing of the DOM, or of the Storylet Engine: the host\n// deals, draws what `start` and `choose` return, and plays the outcome.\n// ---------------------------------------------------------------------------\n\nimport type { Bundle as PatterBundle, Engine as PatterEngine, Flow as PatterFlow, StepResult } from \"@patterkit/runtime\";\n\n/**\n * A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a\n * reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no\n * scene matches.\n */\nexport function sceneIdFor(engine: PatterEngine, bundle: PatterBundle, ref: string): string | undefined {\n if (bundle.scenes[ref]) return ref;\n return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);\n}\n\n/** One thing the scene has said, for the transcript. */\nexport type Beat =\n | { kind: \"line\"; who?: string; text: string }\n | { kind: \"text\"; text: string }\n /** The player's pick, shown back in the transcript. */\n | { kind: \"chose\"; text: string };\n\nexport interface SceneOption {\n id: string;\n text: string;\n /** False when Patter's condition fails, or the outcome it names is gated shut. */\n enabled: boolean;\n /** The outcome the option names, when it names one. */\n outcome?: string;\n}\n\n/** A card being performed: what has been said, and what the player can do now. */\nexport interface Performance {\n card: string;\n box: string;\n /** The scene's internal id, for reporting the position to Patterpad. */\n sceneId?: string;\n transcript: Beat[];\n /** The choice on screen, when the scene is waiting on one. */\n options?: SceneOption[];\n /** True once the scene has run to its end. */\n ended: boolean;\n /** The outcome the scene reached (last word wins); undefined until it ends, and after it\n * ends when nothing named one and the card has several. */\n outcome?: string;\n /** Why the scene cannot be performed, or why it ended without an outcome. */\n problem?: string;\n /** Bookkeeping for the resolution. */\n lastEvent?: string;\n lastLabel?: string;\n}\n\n/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */\nexport interface CardOutcome { gameId: string; available: boolean }\n\n/** Where the Board reports its position, so Patterpad's playhead follows the line being played:\n * the subset of play-helpers' `DebugLink` this needs. */\nexport interface PerformerLink {\n flowOpened(flowId: string): void;\n observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;\n}\n\n/** A guard against a scene that loops without asking anything. */\nconst MAX_STEPS = 500;\n\nexport class Performer {\n /** Where to report the position; set once Patterpad's Live Link is up. */\n link: PerformerLink | undefined;\n\n constructor(\n private patter: PatterEngine,\n private readonly boxes: ReadonlySet<string>,\n /** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */\n private readonly sceneIdOf: (ref: string) => string | undefined = () => undefined,\n ) {}\n\n /** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */\n setEngine(patter: PatterEngine): void {\n this.patter = patter;\n }\n\n /** Does the project say Patter performs this box? */\n performs(boxGameId: string): boolean {\n return this.boxes.has(boxGameId);\n }\n\n /** Start a card's scene: its flow entered at the scene named after the card, run to the first\n * choice or to its end. */\n start(card: { id: string; gameId: string }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance {\n const sceneId = this.sceneIdOf(card.gameId);\n const p: Performance = { card: card.id, box: boxGameId, transcript: [], ended: false, ...(sceneId !== undefined ? { sceneId } : {}) };\n let flow: PatterFlow;\n try {\n // The engine's own answer, not a list kept here: a loaded save brings its flows back.\n const existing = this.patter.getFlow(boxGameId);\n if (existing) {\n if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\".` };\n flow = existing;\n } else {\n flow = this.patter.openFlow(boxGameId, { scene: card.gameId });\n this.link?.flowOpened(boxGameId);\n }\n } catch (e) {\n return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\" (${e instanceof Error ? e.message : String(e)}).` };\n }\n return this.run(p, flow, outcomes);\n }\n\n /** The player picks an option; the scene runs on to its next choice or its end. */\n choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance {\n const flow = this.patter.getFlow(p.box);\n const option = p.options?.find((o) => o.id === optionId);\n if (!flow || !option || !option.enabled) return p;\n flow.choose(optionId);\n this.link?.observe(p.box, p.sceneId ?? null, null, \"choose\", optionId);\n const next: Performance = {\n ...p, options: undefined, transcript: [...p.transcript, { kind: \"chose\", text: option.text }],\n ...(option.outcome !== undefined ? { lastLabel: option.outcome } : {}),\n };\n return this.run(next, flow, outcomes);\n }\n\n /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own\n * properties are the registry's, and the Board resets those with the rest. */\n reset(): void {\n for (const box of this.boxes) {\n if (this.patter.getFlow(box)) this.patter.closeFlow(box);\n }\n }\n\n private run(start: Performance, flow: PatterFlow, outcomes: readonly CardOutcome[]): Performance {\n const p: Performance = { ...start, transcript: [...start.transcript] };\n for (let i = 0; i < MAX_STEPS; i++) {\n const step: StepResult = flow.advance();\n this.link?.observe(p.box, p.sceneId ?? null, \"id\" in step ? step.id : null, step.type);\n if (step.type === \"line\") {\n p.transcript.push({ kind: \"line\", ...(step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}), text: step.text });\n } else if (step.type === \"text\") {\n p.transcript.push({ kind: \"text\", text: step.text });\n } else if (step.type === \"gameEvent\") {\n const named = step.gameData?.[\"outcome\"];\n if (typeof named === \"string\") p.lastEvent = named;\n } else if (step.type === \"choice\") {\n p.options = step.options.map((o) => {\n const named = o.gameData?.[\"outcome\"];\n const outcome = typeof named === \"string\" ? named : undefined;\n // Two gates, each engine's own: Patter's condition, and ours on the outcome it names.\n const shut = outcome !== undefined && outcomes.find((x) => x.gameId === outcome)?.available === false;\n return { id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut, ...(outcome !== undefined ? { outcome } : {}) };\n });\n return p;\n } else {\n return this.finish(p, outcomes);\n }\n }\n return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };\n }\n\n /** Last word wins: an event, else the option's label, else the card's only outcome. */\n private finish(p: Performance, outcomes: readonly CardOutcome[]): Performance {\n const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0]!.gameId : undefined);\n if (reached === undefined) {\n return { ...p, ended: true, problem: \"The scene ended without saying which outcome it reached.\" };\n }\n if (!outcomes.some((o) => o.gameId === reached)) {\n return { ...p, ended: true, problem: `The scene reached \"${reached}\", which this card doesn't have.` };\n }\n return { ...p, ended: true, outcome: reached };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC4BO,SAAS,WAAW,QAAsB,QAAsB,KAAiC;AACtG,MAAI,OAAO,OAAO,GAAG,EAAG,QAAO;AAC/B,SAAO,OAAO,KAAK,OAAO,MAAM,EAAE,KAAK,CAAC,OAAO,OAAO,aAAa,EAAE,MAAM,GAAG;AAChF;AAkDA,IAAM,YAAY;AAEX,IAAM,YAAN,MAAgB;AAAA,EAIrB,YACU,QACS,OAEA,YAAiD,MAAM,QACxE;AAJQ;AACS;AAEA;AAAA,EAChB;AAAA,EAJO;AAAA,EACS;AAAA,EAEA;AAAA;AAAA,EANnB;AAAA;AAAA,EAUA,UAAU,QAA4B;AACpC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,SAAS,WAA4B;AACnC,WAAO,KAAK,MAAM,IAAI,SAAS;AAAA,EACjC;AAAA;AAAA;AAAA,EAIA,MAAM,MAAsC,WAAmB,UAA+C;AAC5G,UAAM,UAAU,KAAK,UAAU,KAAK,MAAM;AAC1C,UAAM,IAAiB,EAAE,MAAM,KAAK,IAAI,KAAK,WAAW,YAAY,CAAC,GAAG,OAAO,OAAO,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC,EAAG;AACpI,QAAI;AACJ,QAAI;AAEF,YAAM,WAAW,KAAK,OAAO,QAAQ,SAAS;AAC9C,UAAI,UAAU;AACZ,YAAI,CAAC,SAAS,KAAK,KAAK,MAAM,EAAG,QAAO,EAAE,GAAG,GAAG,OAAO,MAAM,SAAS,0CAA0C,KAAK,MAAM,KAAK;AAChI,eAAO;AAAA,MACT,OAAO;AACL,eAAO,KAAK,OAAO,SAAS,WAAW,EAAE,OAAO,KAAK,OAAO,CAAC;AAC7D,aAAK,MAAM,WAAW,SAAS;AAAA,MACjC;AAAA,IACF,SAAS,GAAG;AACV,aAAO,EAAE,GAAG,GAAG,OAAO,MAAM,SAAS,0CAA0C,KAAK,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,KAAK;AAAA,IACjJ;AACA,WAAO,KAAK,IAAI,GAAG,MAAM,QAAQ;AAAA,EACnC;AAAA;AAAA,EAGA,OAAO,GAAgB,UAAkB,UAA+C;AACtF,UAAM,OAAO,KAAK,OAAO,QAAQ,EAAE,GAAG;AACtC,UAAM,SAAS,EAAE,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ;AACvD,QAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,QAAS,QAAO;AAChD,SAAK,OAAO,QAAQ;AACpB,SAAK,MAAM,QAAQ,EAAE,KAAK,EAAE,WAAW,MAAM,MAAM,UAAU,QAAQ;AACrE,UAAM,OAAoB;AAAA,MACxB,GAAG;AAAA,MAAG,SAAS;AAAA,MAAW,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,SAAS,MAAM,OAAO,KAAK,CAAC;AAAA,MAC5F,GAAI,OAAO,YAAY,SAAY,EAAE,WAAW,OAAO,QAAQ,IAAI,CAAC;AAAA,IACtE;AACA,WAAO,KAAK,IAAI,MAAM,MAAM,QAAQ;AAAA,EACtC;AAAA;AAAA;AAAA,EAIA,QAAc;AACZ,eAAW,OAAO,KAAK,OAAO;AAC5B,UAAI,KAAK,OAAO,QAAQ,GAAG,EAAG,MAAK,OAAO,UAAU,GAAG;AAAA,IACzD;AAAA,EACF;AAAA,EAEQ,IAAI,OAAoB,MAAkB,UAA+C;AAC/F,UAAM,IAAiB,EAAE,GAAG,OAAO,YAAY,CAAC,GAAG,MAAM,UAAU,EAAE;AACrE,aAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,YAAM,OAAmB,KAAK,QAAQ;AACtC,WAAK,MAAM,QAAQ,EAAE,KAAK,EAAE,WAAW,MAAM,QAAQ,OAAO,KAAK,KAAK,MAAM,KAAK,IAAI;AACrF,UAAI,KAAK,SAAS,QAAQ;AACxB,UAAE,WAAW,KAAK,EAAE,MAAM,QAAQ,GAAI,KAAK,iBAAiB,KAAK,YAAY,EAAE,KAAK,KAAK,iBAAiB,KAAK,UAAU,IAAI,CAAC,GAAI,MAAM,KAAK,KAAK,CAAC;AAAA,MACrJ,WAAW,KAAK,SAAS,QAAQ;AAC/B,UAAE,WAAW,KAAK,EAAE,MAAM,QAAQ,MAAM,KAAK,KAAK,CAAC;AAAA,MACrD,WAAW,KAAK,SAAS,aAAa;AACpC,cAAM,QAAQ,KAAK,WAAW,SAAS;AACvC,YAAI,OAAO,UAAU,SAAU,GAAE,YAAY;AAAA,MAC/C,WAAW,KAAK,SAAS,UAAU;AACjC,UAAE,UAAU,KAAK,QAAQ,IAAI,CAAC,MAAM;AAClC,gBAAM,QAAQ,EAAE,WAAW,SAAS;AACpC,gBAAM,UAAU,OAAO,UAAU,WAAW,QAAQ;AAEpD,gBAAM,OAAO,YAAY,UAAa,SAAS,KAAK,CAAC,MAAM,EAAE,WAAW,OAAO,GAAG,cAAc;AAChG,iBAAO,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,SAAS,EAAE,YAAY,CAAC,MAAM,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC,EAAG;AAAA,QAC/H,CAAC;AACD,eAAO;AAAA,MACT,OAAO;AACL,eAAO,KAAK,OAAO,GAAG,QAAQ;AAAA,MAChC;AAAA,IACF;AACA,WAAO,EAAE,GAAG,GAAG,OAAO,MAAM,SAAS,iBAAiB,SAAS,qCAAqC;AAAA,EACtG;AAAA;AAAA,EAGQ,OAAO,GAAgB,UAA+C;AAC5E,UAAM,UAAU,EAAE,aAAa,EAAE,cAAc,SAAS,WAAW,IAAI,SAAS,CAAC,EAAG,SAAS;AAC7F,QAAI,YAAY,QAAW;AACzB,aAAO,EAAE,GAAG,GAAG,OAAO,MAAM,SAAS,2DAA2D;AAAA,IAClG;AACA,QAAI,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,WAAW,OAAO,GAAG;AAC/C,aAAO,EAAE,GAAG,GAAG,OAAO,MAAM,SAAS,sBAAsB,OAAO,mCAAmC;AAAA,IACvG;AACA,WAAO,EAAE,GAAG,GAAG,OAAO,MAAM,SAAS,QAAQ;AAAA,EAC/C;AACF;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Engine, Bundle } from '@patterkit/runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a
|
|
5
|
+
* reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no
|
|
6
|
+
* scene matches.
|
|
7
|
+
*/
|
|
8
|
+
declare function sceneIdFor(engine: Engine, bundle: Bundle, ref: string): string | undefined;
|
|
9
|
+
/** One thing the scene has said, for the transcript. */
|
|
10
|
+
type Beat = {
|
|
11
|
+
kind: "line";
|
|
12
|
+
who?: string;
|
|
13
|
+
text: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "text";
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
/** The player's pick, shown back in the transcript. */
|
|
19
|
+
| {
|
|
20
|
+
kind: "chose";
|
|
21
|
+
text: string;
|
|
22
|
+
};
|
|
23
|
+
interface SceneOption {
|
|
24
|
+
id: string;
|
|
25
|
+
text: string;
|
|
26
|
+
/** False when Patter's condition fails, or the outcome it names is gated shut. */
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
/** The outcome the option names, when it names one. */
|
|
29
|
+
outcome?: string;
|
|
30
|
+
}
|
|
31
|
+
/** A card being performed: what has been said, and what the player can do now. */
|
|
32
|
+
interface Performance {
|
|
33
|
+
card: string;
|
|
34
|
+
box: string;
|
|
35
|
+
/** The scene's internal id, for reporting the position to Patterpad. */
|
|
36
|
+
sceneId?: string;
|
|
37
|
+
transcript: Beat[];
|
|
38
|
+
/** The choice on screen, when the scene is waiting on one. */
|
|
39
|
+
options?: SceneOption[];
|
|
40
|
+
/** True once the scene has run to its end. */
|
|
41
|
+
ended: boolean;
|
|
42
|
+
/** The outcome the scene reached (last word wins); undefined until it ends, and after it
|
|
43
|
+
* ends when nothing named one and the card has several. */
|
|
44
|
+
outcome?: string;
|
|
45
|
+
/** Why the scene cannot be performed, or why it ended without an outcome. */
|
|
46
|
+
problem?: string;
|
|
47
|
+
/** Bookkeeping for the resolution. */
|
|
48
|
+
lastEvent?: string;
|
|
49
|
+
lastLabel?: string;
|
|
50
|
+
}
|
|
51
|
+
/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */
|
|
52
|
+
interface CardOutcome {
|
|
53
|
+
gameId: string;
|
|
54
|
+
available: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Where the Board reports its position, so Patterpad's playhead follows the line being played:
|
|
57
|
+
* the subset of play-helpers' `DebugLink` this needs. */
|
|
58
|
+
interface PerformerLink {
|
|
59
|
+
flowOpened(flowId: string): void;
|
|
60
|
+
observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;
|
|
61
|
+
}
|
|
62
|
+
declare class Performer {
|
|
63
|
+
private patter;
|
|
64
|
+
private readonly boxes;
|
|
65
|
+
/** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */
|
|
66
|
+
private readonly sceneIdOf;
|
|
67
|
+
/** Where to report the position; set once Patterpad's Live Link is up. */
|
|
68
|
+
link: PerformerLink | undefined;
|
|
69
|
+
constructor(patter: Engine, boxes: ReadonlySet<string>,
|
|
70
|
+
/** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */
|
|
71
|
+
sceneIdOf?: (ref: string) => string | undefined);
|
|
72
|
+
/** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */
|
|
73
|
+
setEngine(patter: Engine): void;
|
|
74
|
+
/** Does the project say Patter performs this box? */
|
|
75
|
+
performs(boxGameId: string): boolean;
|
|
76
|
+
/** Start a card's scene: its flow entered at the scene named after the card, run to the first
|
|
77
|
+
* choice or to its end. */
|
|
78
|
+
start(card: {
|
|
79
|
+
id: string;
|
|
80
|
+
gameId: string;
|
|
81
|
+
}, boxGameId: string, outcomes: readonly CardOutcome[]): Performance;
|
|
82
|
+
/** The player picks an option; the scene runs on to its next choice or its end. */
|
|
83
|
+
choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance;
|
|
84
|
+
/** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own
|
|
85
|
+
* properties are the registry's, and the Board resets those with the rest. */
|
|
86
|
+
reset(): void;
|
|
87
|
+
private run;
|
|
88
|
+
/** Last word wins: an event, else the option's label, else the card's only outcome. */
|
|
89
|
+
private finish;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { type Beat, type CardOutcome, type Performance, Performer, type PerformerLink, type SceneOption, sceneIdFor };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Engine, Bundle } from '@patterkit/runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a
|
|
5
|
+
* reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no
|
|
6
|
+
* scene matches.
|
|
7
|
+
*/
|
|
8
|
+
declare function sceneIdFor(engine: Engine, bundle: Bundle, ref: string): string | undefined;
|
|
9
|
+
/** One thing the scene has said, for the transcript. */
|
|
10
|
+
type Beat = {
|
|
11
|
+
kind: "line";
|
|
12
|
+
who?: string;
|
|
13
|
+
text: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "text";
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
/** The player's pick, shown back in the transcript. */
|
|
19
|
+
| {
|
|
20
|
+
kind: "chose";
|
|
21
|
+
text: string;
|
|
22
|
+
};
|
|
23
|
+
interface SceneOption {
|
|
24
|
+
id: string;
|
|
25
|
+
text: string;
|
|
26
|
+
/** False when Patter's condition fails, or the outcome it names is gated shut. */
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
/** The outcome the option names, when it names one. */
|
|
29
|
+
outcome?: string;
|
|
30
|
+
}
|
|
31
|
+
/** A card being performed: what has been said, and what the player can do now. */
|
|
32
|
+
interface Performance {
|
|
33
|
+
card: string;
|
|
34
|
+
box: string;
|
|
35
|
+
/** The scene's internal id, for reporting the position to Patterpad. */
|
|
36
|
+
sceneId?: string;
|
|
37
|
+
transcript: Beat[];
|
|
38
|
+
/** The choice on screen, when the scene is waiting on one. */
|
|
39
|
+
options?: SceneOption[];
|
|
40
|
+
/** True once the scene has run to its end. */
|
|
41
|
+
ended: boolean;
|
|
42
|
+
/** The outcome the scene reached (last word wins); undefined until it ends, and after it
|
|
43
|
+
* ends when nothing named one and the card has several. */
|
|
44
|
+
outcome?: string;
|
|
45
|
+
/** Why the scene cannot be performed, or why it ended without an outcome. */
|
|
46
|
+
problem?: string;
|
|
47
|
+
/** Bookkeeping for the resolution. */
|
|
48
|
+
lastEvent?: string;
|
|
49
|
+
lastLabel?: string;
|
|
50
|
+
}
|
|
51
|
+
/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */
|
|
52
|
+
interface CardOutcome {
|
|
53
|
+
gameId: string;
|
|
54
|
+
available: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Where the Board reports its position, so Patterpad's playhead follows the line being played:
|
|
57
|
+
* the subset of play-helpers' `DebugLink` this needs. */
|
|
58
|
+
interface PerformerLink {
|
|
59
|
+
flowOpened(flowId: string): void;
|
|
60
|
+
observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;
|
|
61
|
+
}
|
|
62
|
+
declare class Performer {
|
|
63
|
+
private patter;
|
|
64
|
+
private readonly boxes;
|
|
65
|
+
/** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */
|
|
66
|
+
private readonly sceneIdOf;
|
|
67
|
+
/** Where to report the position; set once Patterpad's Live Link is up. */
|
|
68
|
+
link: PerformerLink | undefined;
|
|
69
|
+
constructor(patter: Engine, boxes: ReadonlySet<string>,
|
|
70
|
+
/** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */
|
|
71
|
+
sceneIdOf?: (ref: string) => string | undefined);
|
|
72
|
+
/** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */
|
|
73
|
+
setEngine(patter: Engine): void;
|
|
74
|
+
/** Does the project say Patter performs this box? */
|
|
75
|
+
performs(boxGameId: string): boolean;
|
|
76
|
+
/** Start a card's scene: its flow entered at the scene named after the card, run to the first
|
|
77
|
+
* choice or to its end. */
|
|
78
|
+
start(card: {
|
|
79
|
+
id: string;
|
|
80
|
+
gameId: string;
|
|
81
|
+
}, boxGameId: string, outcomes: readonly CardOutcome[]): Performance;
|
|
82
|
+
/** The player picks an option; the scene runs on to its next choice or its end. */
|
|
83
|
+
choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance;
|
|
84
|
+
/** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own
|
|
85
|
+
* properties are the registry's, and the Board resets those with the rest. */
|
|
86
|
+
reset(): void;
|
|
87
|
+
private run;
|
|
88
|
+
/** Last word wins: an event, else the option's label, else the card's only outcome. */
|
|
89
|
+
private finish;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { type Beat, type CardOutcome, type Performance, Performer, type PerformerLink, type SceneOption, sceneIdFor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/performer.ts
|
|
2
|
+
function sceneIdFor(engine, bundle, ref) {
|
|
3
|
+
if (bundle.scenes[ref]) return ref;
|
|
4
|
+
return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);
|
|
5
|
+
}
|
|
6
|
+
var MAX_STEPS = 500;
|
|
7
|
+
var Performer = class {
|
|
8
|
+
constructor(patter, boxes, sceneIdOf = () => void 0) {
|
|
9
|
+
this.patter = patter;
|
|
10
|
+
this.boxes = boxes;
|
|
11
|
+
this.sceneIdOf = sceneIdOf;
|
|
12
|
+
}
|
|
13
|
+
patter;
|
|
14
|
+
boxes;
|
|
15
|
+
sceneIdOf;
|
|
16
|
+
/** Where to report the position; set once Patterpad's Live Link is up. */
|
|
17
|
+
link;
|
|
18
|
+
/** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */
|
|
19
|
+
setEngine(patter) {
|
|
20
|
+
this.patter = patter;
|
|
21
|
+
}
|
|
22
|
+
/** Does the project say Patter performs this box? */
|
|
23
|
+
performs(boxGameId) {
|
|
24
|
+
return this.boxes.has(boxGameId);
|
|
25
|
+
}
|
|
26
|
+
/** Start a card's scene: its flow entered at the scene named after the card, run to the first
|
|
27
|
+
* choice or to its end. */
|
|
28
|
+
start(card, boxGameId, outcomes) {
|
|
29
|
+
const sceneId = this.sceneIdOf(card.gameId);
|
|
30
|
+
const p = { card: card.id, box: boxGameId, transcript: [], ended: false, ...sceneId !== void 0 ? { sceneId } : {} };
|
|
31
|
+
let flow;
|
|
32
|
+
try {
|
|
33
|
+
const existing = this.patter.getFlow(boxGameId);
|
|
34
|
+
if (existing) {
|
|
35
|
+
if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named "${card.gameId}".` };
|
|
36
|
+
flow = existing;
|
|
37
|
+
} else {
|
|
38
|
+
flow = this.patter.openFlow(boxGameId, { scene: card.gameId });
|
|
39
|
+
this.link?.flowOpened(boxGameId);
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { ...p, ended: true, problem: `The Patter project has no scene named "${card.gameId}" (${e instanceof Error ? e.message : String(e)}).` };
|
|
43
|
+
}
|
|
44
|
+
return this.run(p, flow, outcomes);
|
|
45
|
+
}
|
|
46
|
+
/** The player picks an option; the scene runs on to its next choice or its end. */
|
|
47
|
+
choose(p, optionId, outcomes) {
|
|
48
|
+
const flow = this.patter.getFlow(p.box);
|
|
49
|
+
const option = p.options?.find((o) => o.id === optionId);
|
|
50
|
+
if (!flow || !option || !option.enabled) return p;
|
|
51
|
+
flow.choose(optionId);
|
|
52
|
+
this.link?.observe(p.box, p.sceneId ?? null, null, "choose", optionId);
|
|
53
|
+
const next = {
|
|
54
|
+
...p,
|
|
55
|
+
options: void 0,
|
|
56
|
+
transcript: [...p.transcript, { kind: "chose", text: option.text }],
|
|
57
|
+
...option.outcome !== void 0 ? { lastLabel: option.outcome } : {}
|
|
58
|
+
};
|
|
59
|
+
return this.run(next, flow, outcomes);
|
|
60
|
+
}
|
|
61
|
+
/** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own
|
|
62
|
+
* properties are the registry's, and the Board resets those with the rest. */
|
|
63
|
+
reset() {
|
|
64
|
+
for (const box of this.boxes) {
|
|
65
|
+
if (this.patter.getFlow(box)) this.patter.closeFlow(box);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
run(start, flow, outcomes) {
|
|
69
|
+
const p = { ...start, transcript: [...start.transcript] };
|
|
70
|
+
for (let i = 0; i < MAX_STEPS; i++) {
|
|
71
|
+
const step = flow.advance();
|
|
72
|
+
this.link?.observe(p.box, p.sceneId ?? null, "id" in step ? step.id : null, step.type);
|
|
73
|
+
if (step.type === "line") {
|
|
74
|
+
p.transcript.push({ kind: "line", ...step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}, text: step.text });
|
|
75
|
+
} else if (step.type === "text") {
|
|
76
|
+
p.transcript.push({ kind: "text", text: step.text });
|
|
77
|
+
} else if (step.type === "gameEvent") {
|
|
78
|
+
const named = step.gameData?.["outcome"];
|
|
79
|
+
if (typeof named === "string") p.lastEvent = named;
|
|
80
|
+
} else if (step.type === "choice") {
|
|
81
|
+
p.options = step.options.map((o) => {
|
|
82
|
+
const named = o.gameData?.["outcome"];
|
|
83
|
+
const outcome = typeof named === "string" ? named : void 0;
|
|
84
|
+
const shut = outcome !== void 0 && outcomes.find((x) => x.gameId === outcome)?.available === false;
|
|
85
|
+
return { id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut, ...outcome !== void 0 ? { outcome } : {} };
|
|
86
|
+
});
|
|
87
|
+
return p;
|
|
88
|
+
} else {
|
|
89
|
+
return this.finish(p, outcomes);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };
|
|
93
|
+
}
|
|
94
|
+
/** Last word wins: an event, else the option's label, else the card's only outcome. */
|
|
95
|
+
finish(p, outcomes) {
|
|
96
|
+
const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0].gameId : void 0);
|
|
97
|
+
if (reached === void 0) {
|
|
98
|
+
return { ...p, ended: true, problem: "The scene ended without saying which outcome it reached." };
|
|
99
|
+
}
|
|
100
|
+
if (!outcomes.some((o) => o.gameId === reached)) {
|
|
101
|
+
return { ...p, ended: true, problem: `The scene reached "${reached}", which this card doesn't have.` };
|
|
102
|
+
}
|
|
103
|
+
return { ...p, ended: true, outcome: reached };
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
export {
|
|
107
|
+
Performer,
|
|
108
|
+
sceneIdFor
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/performer.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Performing a dealt card through Patter: the card's gameId names a Patter\n// scene, the scene runs, and the scene decides which of the card's outcomes\n// was reached (the Storylets-with-Patter contract; the Hamlet's\n// `performance.js`, lifted so a game, Storyletter's Board and the playable\n// page all run the same code).\n//\n// The Hamlet's rules, kept exactly because they are the host's contract:\n// - ONE Patter flow per performed box, named after the box, entered with\n// `goto` for each card: a flow is Patter's memory, and a fresh one per card\n// would forget its visits and restart its seeded shuffles (joint demo\n// finding 14).\n// - The outcome is the LAST word: a gameEvent carrying one, else the label on\n// the option the player took, else the card's only outcome (finding 15).\n// - An option is greyed when either engine says no: Patter's `eligible`, or\n// the Storylet Engine's gate on the outcome the option names.\n//\n// This module knows nothing of the DOM, or of the Storylet Engine: the host\n// deals, draws what `start` and `choose` return, and plays the outcome.\n// ---------------------------------------------------------------------------\n\nimport type { Bundle as PatterBundle, Engine as PatterEngine, Flow as PatterFlow, StepResult } from \"@patterkit/runtime\";\n\n/**\n * A card's scene reference (its gameId) to the scene's internal id, as Patter's runtime resolves a\n * reference: an internal id first, else a scene's address by Patter's own rules. Undefined when no\n * scene matches.\n */\nexport function sceneIdFor(engine: PatterEngine, bundle: PatterBundle, ref: string): string | undefined {\n if (bundle.scenes[ref]) return ref;\n return Object.keys(bundle.scenes).find((id) => engine.sceneAddress(id) === ref);\n}\n\n/** One thing the scene has said, for the transcript. */\nexport type Beat =\n | { kind: \"line\"; who?: string; text: string }\n | { kind: \"text\"; text: string }\n /** The player's pick, shown back in the transcript. */\n | { kind: \"chose\"; text: string };\n\nexport interface SceneOption {\n id: string;\n text: string;\n /** False when Patter's condition fails, or the outcome it names is gated shut. */\n enabled: boolean;\n /** The outcome the option names, when it names one. */\n outcome?: string;\n}\n\n/** A card being performed: what has been said, and what the player can do now. */\nexport interface Performance {\n card: string;\n box: string;\n /** The scene's internal id, for reporting the position to Patterpad. */\n sceneId?: string;\n transcript: Beat[];\n /** The choice on screen, when the scene is waiting on one. */\n options?: SceneOption[];\n /** True once the scene has run to its end. */\n ended: boolean;\n /** The outcome the scene reached (last word wins); undefined until it ends, and after it\n * ends when nothing named one and the card has several. */\n outcome?: string;\n /** Why the scene cannot be performed, or why it ended without an outcome. */\n problem?: string;\n /** Bookkeeping for the resolution. */\n lastEvent?: string;\n lastLabel?: string;\n}\n\n/** An outcome of the card, as the Storylet Engine reports it (`Table.outcomes`). */\nexport interface CardOutcome { gameId: string; available: boolean }\n\n/** Where the Board reports its position, so Patterpad's playhead follows the line being played:\n * the subset of play-helpers' `DebugLink` this needs. */\nexport interface PerformerLink {\n flowOpened(flowId: string): void;\n observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;\n}\n\n/** A guard against a scene that loops without asking anything. */\nconst MAX_STEPS = 500;\n\nexport class Performer {\n /** Where to report the position; set once Patterpad's Live Link is up. */\n link: PerformerLink | undefined;\n\n constructor(\n private patter: PatterEngine,\n private readonly boxes: ReadonlySet<string>,\n /** A card's scene reference (its gameId) to the scene's internal id, as the runtime resolves it. */\n private readonly sceneIdOf: (ref: string) => string | undefined = () => undefined,\n ) {}\n\n /** A live refresh replaced the engine (a structural hot swap): carry on with the new one. */\n setEngine(patter: PatterEngine): void {\n this.patter = patter;\n }\n\n /** Does the project say Patter performs this box? */\n performs(boxGameId: string): boolean {\n return this.boxes.has(boxGameId);\n }\n\n /** Start a card's scene: its flow entered at the scene named after the card, run to the first\n * choice or to its end. */\n start(card: { id: string; gameId: string }, boxGameId: string, outcomes: readonly CardOutcome[]): Performance {\n const sceneId = this.sceneIdOf(card.gameId);\n const p: Performance = { card: card.id, box: boxGameId, transcript: [], ended: false, ...(sceneId !== undefined ? { sceneId } : {}) };\n let flow: PatterFlow;\n try {\n // The engine's own answer, not a list kept here: a loaded save brings its flows back.\n const existing = this.patter.getFlow(boxGameId);\n if (existing) {\n if (!existing.goto(card.gameId)) return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\".` };\n flow = existing;\n } else {\n flow = this.patter.openFlow(boxGameId, { scene: card.gameId });\n this.link?.flowOpened(boxGameId);\n }\n } catch (e) {\n return { ...p, ended: true, problem: `The Patter project has no scene named \"${card.gameId}\" (${e instanceof Error ? e.message : String(e)}).` };\n }\n return this.run(p, flow, outcomes);\n }\n\n /** The player picks an option; the scene runs on to its next choice or its end. */\n choose(p: Performance, optionId: string, outcomes: readonly CardOutcome[]): Performance {\n const flow = this.patter.getFlow(p.box);\n const option = p.options?.find((o) => o.id === optionId);\n if (!flow || !option || !option.enabled) return p;\n flow.choose(optionId);\n this.link?.observe(p.box, p.sceneId ?? null, null, \"choose\", optionId);\n const next: Performance = {\n ...p, options: undefined, transcript: [...p.transcript, { kind: \"chose\", text: option.text }],\n ...(option.outcome !== undefined ? { lastLabel: option.outcome } : {}),\n };\n return this.run(next, flow, outcomes);\n }\n\n /** A new run: every box's flow closes, so the next card opens a fresh one. Patter's own\n * properties are the registry's, and the Board resets those with the rest. */\n reset(): void {\n for (const box of this.boxes) {\n if (this.patter.getFlow(box)) this.patter.closeFlow(box);\n }\n }\n\n private run(start: Performance, flow: PatterFlow, outcomes: readonly CardOutcome[]): Performance {\n const p: Performance = { ...start, transcript: [...start.transcript] };\n for (let i = 0; i < MAX_STEPS; i++) {\n const step: StepResult = flow.advance();\n this.link?.observe(p.box, p.sceneId ?? null, \"id\" in step ? step.id : null, step.type);\n if (step.type === \"line\") {\n p.transcript.push({ kind: \"line\", ...(step.characterName ?? step.character ? { who: step.characterName ?? step.character } : {}), text: step.text });\n } else if (step.type === \"text\") {\n p.transcript.push({ kind: \"text\", text: step.text });\n } else if (step.type === \"gameEvent\") {\n const named = step.gameData?.[\"outcome\"];\n if (typeof named === \"string\") p.lastEvent = named;\n } else if (step.type === \"choice\") {\n p.options = step.options.map((o) => {\n const named = o.gameData?.[\"outcome\"];\n const outcome = typeof named === \"string\" ? named : undefined;\n // Two gates, each engine's own: Patter's condition, and ours on the outcome it names.\n const shut = outcome !== undefined && outcomes.find((x) => x.gameId === outcome)?.available === false;\n return { id: o.id, text: o.prompt?.text || o.id, enabled: o.eligible && !shut, ...(outcome !== undefined ? { outcome } : {}) };\n });\n return p;\n } else {\n return this.finish(p, outcomes);\n }\n }\n return { ...p, ended: true, problem: `The scene ran ${MAX_STEPS} steps without a choice or an end.` };\n }\n\n /** Last word wins: an event, else the option's label, else the card's only outcome. */\n private finish(p: Performance, outcomes: readonly CardOutcome[]): Performance {\n const reached = p.lastEvent ?? p.lastLabel ?? (outcomes.length === 1 ? outcomes[0]!.gameId : undefined);\n if (reached === undefined) {\n return { ...p, ended: true, problem: \"The scene ended without saying which outcome it reached.\" };\n }\n if (!outcomes.some((o) => o.gameId === reached)) {\n return { ...p, ended: true, problem: `The scene reached \"${reached}\", which this card doesn't have.` };\n }\n return { ...p, ended: true, outcome: reached };\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@storylet-studio/with-patter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Storylets with Patter: perform a dealt card as the Patter scene named after it, and take the outcome the scene reaches. DOM-free, for a game, the Board and the playable page alike.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/storylet-studio/storylets.git",
|
|
9
|
+
"directory": "packages/with-patter"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"author": "Ian Thomas",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@patterkit/runtime": ">=0.14.0 <1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"storylets",
|
|
39
|
+
"patter",
|
|
40
|
+
"storylet-studio",
|
|
41
|
+
"patterkit",
|
|
42
|
+
"interactive-fiction",
|
|
43
|
+
"dialogue"
|
|
44
|
+
]
|
|
45
|
+
}
|