@patterkit/play-helpers 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 +106 -0
- package/dist/index.cjs +414 -0
- package/dist/index.d.cts +151 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +373 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @patterkit/play-helpers
|
|
2
|
+
|
|
3
|
+
Thin game-integration helpers around [@patterkit/runtime](../runtime)'s `Engine` -
|
|
4
|
+
the **Patterplay JS** companion. None are required to play a bundle; they smooth
|
|
5
|
+
the host wiring most games end up writing anyway.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @patterkit/play-helpers
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Save / load
|
|
12
|
+
|
|
13
|
+
Wrap the engine's whole-game snapshot in a tagged, versioned envelope - drop it into
|
|
14
|
+
localStorage or a file, and a foreign blob throws instead of corrupting a run.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { serializeState, deserializeState } from "@patterkit/play-helpers";
|
|
18
|
+
|
|
19
|
+
localStorage.setItem("slot1", serializeState(engine));
|
|
20
|
+
deserializeState(engine, localStorage.getItem("slot1")!); // throws on a non-patter blob
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`saveState` / `loadState` are the object-level variants (no JSON string).
|
|
24
|
+
|
|
25
|
+
## Runtime properties
|
|
26
|
+
|
|
27
|
+
Read / write `@patter` globals, `@scene` props, or a wired foreign scope at runtime -
|
|
28
|
+
e.g. the game pushing inventory into the dialogue.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { setProperty, setProperties, getProperty } from "@patterkit/play-helpers";
|
|
32
|
+
|
|
33
|
+
setProperties(engine, { "@hp": 10, "@scene.locked": false });
|
|
34
|
+
getProperty(engine, "@hp"); // 10
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
(Localisation needs no helper: an **Embedded** bundle carries its strings - construct with
|
|
38
|
+
`{ locale }` or switch live via `engine.setLocale()`; an **IDs-only** bundle emits beat IDs
|
|
39
|
+
your game localises itself, re-interpolated with `flow.interpolate()`. See the
|
|
40
|
+
[Localisation guide](https://patterkit.dev/play/localisation/).)
|
|
41
|
+
|
|
42
|
+
## State logger
|
|
43
|
+
|
|
44
|
+
A debug companion that watches the mutable runtime state (`@patter` / `@scene` /
|
|
45
|
+
visit counts, shared + per-flow) and reports what changed between captures. `logStep`
|
|
46
|
+
traces each played step, including its `gameData` (the host-event channel).
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createStateLogger } from "@patterkit/play-helpers";
|
|
50
|
+
|
|
51
|
+
const log = createStateLogger(engine, { label: "main" });
|
|
52
|
+
const step = flow.advance();
|
|
53
|
+
log.logStep(step); // [main] line WATCHMAN: "..." gameData={...}
|
|
54
|
+
log.capture(); // [main] @patter.bell_tolls: 0 -> 1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`snapshotState(engine)` / `diffState(a, b)` are the underlying pure functions.
|
|
58
|
+
|
|
59
|
+
## Live Link (Patterpad debug + hot reload)
|
|
60
|
+
|
|
61
|
+
`createDebugLink` streams the running story's cursor to Patterpad over a localhost
|
|
62
|
+
WebSocket, so the editor follows the game like a debugger - and receives **live bundle
|
|
63
|
+
pushes** back when the author edits, for hot reload without restarting:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createDebugLink, applyLiveBundle } from "@patterkit/play-helpers";
|
|
67
|
+
|
|
68
|
+
let engine = new Engine(bundle);
|
|
69
|
+
const link = createDebugLink({
|
|
70
|
+
build: bundle.content.hash,
|
|
71
|
+
onBundle: (msg) => {
|
|
72
|
+
({ engine, bundle } = applyLiveBundle(engine, bundle, msg.data));
|
|
73
|
+
link.setBuild(msg.build); // re-hello under the new build id
|
|
74
|
+
rerender();
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
link.flowOpened("main");
|
|
78
|
+
// after each step: link.observe(flowId, sceneId, beatId, step.type)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`applyLiveBundle` picks the cheapest tier itself: same `structureHash` -> in-place
|
|
82
|
+
`replaceStrings` (`kind: "text"`, nothing lost); structural change -> `hotSwap` to a fresh
|
|
83
|
+
engine restored from the old one (`kind: "structure"`). Wire it behind a dev flag - it is
|
|
84
|
+
a development tool, not a shipping feature.
|
|
85
|
+
|
|
86
|
+
## Property inspector
|
|
87
|
+
|
|
88
|
+
`createPropertyInspector(engine)` builds a small DOM panel of the engine's `@patter`
|
|
89
|
+
properties with type-aware editors (toggle / number / text / enum / flags) and
|
|
90
|
+
reset-to-default - the JS equivalent of the Unity / Unreal / Godot runtime-state panels.
|
|
91
|
+
`refresh()` re-reads values; `destroy()` unhooks; `pollMs` auto-refreshes.
|
|
92
|
+
|
|
93
|
+
## Audio resolution
|
|
94
|
+
|
|
95
|
+
`createAudioResolver(manifestJson, basePath)` reads a `patteraudio.json` manifest (the
|
|
96
|
+
sidecar Patterpad's Build writes next to the audio root) and resolves a beat to its
|
|
97
|
+
**winning take**:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createAudioResolver } from "@patterkit/play-helpers";
|
|
101
|
+
|
|
102
|
+
const audio = createAudioResolver(await (await fetch("audio/patteraudio.json")).text(), "audio");
|
|
103
|
+
const src = audio.resolve(step.id); // "audio/scratch/beat42.wav" | null
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It resolves; **you** play (an `<audio>` element, your engine's mixer, anything).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
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
|
+
SAVE_SCHEMA: () => SAVE_SCHEMA,
|
|
24
|
+
applyLiveBundle: () => applyLiveBundle,
|
|
25
|
+
createAudioResolver: () => createAudioResolver,
|
|
26
|
+
createDebugLink: () => createDebugLink,
|
|
27
|
+
createPropertyInspector: () => createPropertyInspector,
|
|
28
|
+
createStateLogger: () => createStateLogger,
|
|
29
|
+
deserializeState: () => deserializeState,
|
|
30
|
+
diffState: () => diffState,
|
|
31
|
+
getProperty: () => getProperty,
|
|
32
|
+
loadState: () => loadState,
|
|
33
|
+
saveState: () => saveState,
|
|
34
|
+
serializeState: () => serializeState,
|
|
35
|
+
setProperties: () => setProperties,
|
|
36
|
+
setProperty: () => setProperty,
|
|
37
|
+
snapshotState: () => snapshotState
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/save.ts
|
|
42
|
+
var SAVE_SCHEMA = "patter/save@0";
|
|
43
|
+
function saveState(engine) {
|
|
44
|
+
return { schema: SAVE_SCHEMA, save: engine.saveGame() };
|
|
45
|
+
}
|
|
46
|
+
function loadState(engine, env) {
|
|
47
|
+
if (!env || env.schema !== SAVE_SCHEMA || !env.save) {
|
|
48
|
+
throw new Error(`loadState: not a ${SAVE_SCHEMA} envelope`);
|
|
49
|
+
}
|
|
50
|
+
engine.loadGame(env.save);
|
|
51
|
+
}
|
|
52
|
+
function serializeState(engine) {
|
|
53
|
+
return JSON.stringify(saveState(engine));
|
|
54
|
+
}
|
|
55
|
+
function deserializeState(engine, json) {
|
|
56
|
+
loadState(engine, JSON.parse(json));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/properties.ts
|
|
60
|
+
function getProperty(engine, ref) {
|
|
61
|
+
return engine.getProperty(ref);
|
|
62
|
+
}
|
|
63
|
+
function setProperty(engine, ref, value) {
|
|
64
|
+
engine.setProperty(ref, value);
|
|
65
|
+
}
|
|
66
|
+
function setProperties(engine, values) {
|
|
67
|
+
for (const [ref, value] of Object.entries(values)) engine.setProperty(ref, value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/logger.ts
|
|
71
|
+
var eq = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
72
|
+
function snapshotState(engine) {
|
|
73
|
+
const save = engine.saveGame();
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [name, v] of Object.entries(save.shared.patter ?? {})) out[`@patter.${name}`] = v;
|
|
76
|
+
for (const [scene, vals] of Object.entries(save.stageBags))
|
|
77
|
+
for (const [name, v] of Object.entries(vals)) out[`@scene:${scene}.${name}`] = v;
|
|
78
|
+
for (const [id, n] of Object.entries(save.sharedVisits)) out[`visit:${id}`] = n;
|
|
79
|
+
for (const [fid, snap] of Object.entries(save.flows)) {
|
|
80
|
+
for (const [name, v] of Object.entries(snap.scopes.patter ?? {})) out[`${fid}/@patter.${name}`] = v;
|
|
81
|
+
for (const [scene, vals] of Object.entries(snap.sceneBags))
|
|
82
|
+
for (const [name, v] of Object.entries(vals)) out[`${fid}/@scene:${scene}.${name}`] = v;
|
|
83
|
+
for (const [id, n] of Object.entries(snap.visits)) out[`${fid}/visit:${id}`] = n;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function diffState(prev, next) {
|
|
88
|
+
const changes = [];
|
|
89
|
+
const keys = [.../* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)])].sort();
|
|
90
|
+
for (const path of keys) {
|
|
91
|
+
const from = prev[path], to = next[path];
|
|
92
|
+
if (!eq(from, to)) changes.push({ path, ...from !== void 0 ? { from } : {}, ...to !== void 0 ? { to } : {} });
|
|
93
|
+
}
|
|
94
|
+
return changes;
|
|
95
|
+
}
|
|
96
|
+
var fmt = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
|
|
97
|
+
function describeStep(step) {
|
|
98
|
+
switch (step.type) {
|
|
99
|
+
case "line":
|
|
100
|
+
return `line ${step.character ?? "?"}: ${JSON.stringify(step.text)}${gd(step.gameData)}`;
|
|
101
|
+
case "text":
|
|
102
|
+
return `text: ${JSON.stringify(step.text)}${gd(step.gameData)}`;
|
|
103
|
+
case "gameEvent":
|
|
104
|
+
return `game event ${step.id}${gd(step.gameData)}`;
|
|
105
|
+
case "choice":
|
|
106
|
+
return `choice (${step.options.length} option${step.options.length === 1 ? "" : "s"})`;
|
|
107
|
+
case "end":
|
|
108
|
+
return "end";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
var gd = (data) => data ? ` gameData=${JSON.stringify(data)}` : "";
|
|
112
|
+
function createStateLogger(engine, opts = {}) {
|
|
113
|
+
const sink = opts.sink ?? ((line) => console.log(line));
|
|
114
|
+
const tag = opts.label ? `[${opts.label}] ` : "";
|
|
115
|
+
let baseline = snapshotState(engine);
|
|
116
|
+
return {
|
|
117
|
+
snapshot: () => snapshotState(engine),
|
|
118
|
+
capture() {
|
|
119
|
+
const next = snapshotState(engine);
|
|
120
|
+
const changes = diffState(baseline, next);
|
|
121
|
+
baseline = next;
|
|
122
|
+
for (const c of changes) sink(`${tag}${c.path}: ${fmt(c.from)} -> ${fmt(c.to)}`);
|
|
123
|
+
return changes;
|
|
124
|
+
},
|
|
125
|
+
logStep(step) {
|
|
126
|
+
sink(`${tag}${describeStep(step)}`);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/debug.ts
|
|
132
|
+
var OPEN = 1;
|
|
133
|
+
function createDebugLink(opts) {
|
|
134
|
+
const url = opts.url ?? "ws://127.0.0.1:4471";
|
|
135
|
+
const Ctor = opts.WebSocket ?? globalThis.WebSocket;
|
|
136
|
+
const flows = /* @__PURE__ */ new Set();
|
|
137
|
+
let queue = [];
|
|
138
|
+
let sock = null;
|
|
139
|
+
let closed = false;
|
|
140
|
+
let build = opts.build;
|
|
141
|
+
const flush = () => {
|
|
142
|
+
if (!sock || sock.readyState !== OPEN) return;
|
|
143
|
+
for (const m of queue) {
|
|
144
|
+
try {
|
|
145
|
+
sock.send(m);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
queue = [];
|
|
150
|
+
};
|
|
151
|
+
const post = (msg) => {
|
|
152
|
+
if (closed) return;
|
|
153
|
+
queue.push(JSON.stringify(msg));
|
|
154
|
+
flush();
|
|
155
|
+
};
|
|
156
|
+
if (!Ctor) {
|
|
157
|
+
return { flowOpened() {
|
|
158
|
+
}, observe() {
|
|
159
|
+
}, flowClosed() {
|
|
160
|
+
}, setBuild() {
|
|
161
|
+
}, close() {
|
|
162
|
+
closed = true;
|
|
163
|
+
} };
|
|
164
|
+
}
|
|
165
|
+
const sendHello = () => {
|
|
166
|
+
const hello = JSON.stringify({ t: "hello", v: 1, build, project: opts.project, flows: [...flows] });
|
|
167
|
+
try {
|
|
168
|
+
sock?.send(hello);
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
sock = new Ctor(url);
|
|
174
|
+
sock.addEventListener("open", () => {
|
|
175
|
+
sendHello();
|
|
176
|
+
flush();
|
|
177
|
+
});
|
|
178
|
+
sock.addEventListener("message", (ev) => {
|
|
179
|
+
if (!opts.onBundle || typeof ev.data !== "string") return;
|
|
180
|
+
try {
|
|
181
|
+
const msg = JSON.parse(ev.data);
|
|
182
|
+
if (msg.t === "bundle" && typeof msg.build === "string" && typeof msg.data === "string") {
|
|
183
|
+
opts.onBundle({ build: msg.build, data: msg.data });
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
sock.addEventListener("error", () => {
|
|
189
|
+
});
|
|
190
|
+
sock.addEventListener("close", () => {
|
|
191
|
+
sock = null;
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
sock = null;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
flowOpened(flowId) {
|
|
198
|
+
flows.add(flowId);
|
|
199
|
+
post({ t: "flowOpen", flow: flowId });
|
|
200
|
+
},
|
|
201
|
+
flowClosed(flowId) {
|
|
202
|
+
flows.delete(flowId);
|
|
203
|
+
post({ t: "flowClose", flow: flowId });
|
|
204
|
+
},
|
|
205
|
+
observe(flowId, sceneId, beatId, type, choiceId) {
|
|
206
|
+
post({ t: "frame", flow: flowId, sceneId, beatId, type, choiceId });
|
|
207
|
+
},
|
|
208
|
+
setBuild(next) {
|
|
209
|
+
if (closed || next === build) return;
|
|
210
|
+
build = next;
|
|
211
|
+
if (sock && sock.readyState === OPEN) sendHello();
|
|
212
|
+
},
|
|
213
|
+
close() {
|
|
214
|
+
closed = true;
|
|
215
|
+
queue = [];
|
|
216
|
+
try {
|
|
217
|
+
sock?.close();
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
sock = null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/refresh.ts
|
|
226
|
+
var import_runtime = require("@patterkit/runtime");
|
|
227
|
+
function applyLiveBundle(engine, current, data) {
|
|
228
|
+
const next = JSON.parse(data);
|
|
229
|
+
const sameStructure = current.content.structureHash !== void 0 && current.content.structureHash === next.content.structureHash;
|
|
230
|
+
if (sameStructure) {
|
|
231
|
+
engine.replaceStrings(next);
|
|
232
|
+
return { engine, bundle: next, kind: "text" };
|
|
233
|
+
}
|
|
234
|
+
return { engine: engine.hotSwap(next), bundle: next, kind: "structure" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/inspector.ts
|
|
238
|
+
var STYLE_ID = "pp-inspector-style";
|
|
239
|
+
var CSS = `
|
|
240
|
+
.pp-insp{font:13px/1.4 ui-sans-serif,system-ui,sans-serif;color:#15201e;background:#f4efe6;border:1px solid #cfc7b8;border-radius:10px;padding:.6rem .7rem;max-width:22rem;box-shadow:0 6px 20px rgba(21,32,30,.12)}
|
|
241
|
+
.pp-insp h4{margin:0 0 .4rem;font:600 .72rem/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:#5c6b62}
|
|
242
|
+
.pp-insp-empty{color:#8a9691;font-style:italic}
|
|
243
|
+
.pp-insp-row{display:flex;align-items:center;gap:.4rem;margin:.18rem 0}
|
|
244
|
+
.pp-insp-ref{flex:0 0 8rem;font-family:ui-monospace,monospace;font-size:.78rem;color:#214f4b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
245
|
+
.pp-insp-ctl{flex:1;min-width:0;display:flex}
|
|
246
|
+
.pp-insp-ctl input[type=text],.pp-insp-ctl input[type=number],.pp-insp-ctl select{width:100%;box-sizing:border-box;font:inherit;padding:.15rem .3rem;border:1px solid #cfc7b8;border-radius:6px;background:#fff;color:inherit}
|
|
247
|
+
.pp-insp-reset{flex:0 0 auto;border:1px solid #cfc7b8;background:#fff;border-radius:6px;cursor:pointer;color:#5c6b62;width:1.6rem;height:1.6rem;line-height:1;padding:0}
|
|
248
|
+
.pp-insp-reset:disabled{opacity:.35;cursor:default}
|
|
249
|
+
`;
|
|
250
|
+
function injectStyle(doc) {
|
|
251
|
+
if (doc.getElementById(STYLE_ID)) return;
|
|
252
|
+
const s = doc.createElement("style");
|
|
253
|
+
s.id = STYLE_ID;
|
|
254
|
+
s.textContent = CSS;
|
|
255
|
+
(doc.head ?? doc.documentElement).appendChild(s);
|
|
256
|
+
}
|
|
257
|
+
function sameValue(a, b) {
|
|
258
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
259
|
+
return a === b;
|
|
260
|
+
}
|
|
261
|
+
function createPropertyInspector(engine, opts = {}) {
|
|
262
|
+
const doc = opts.container?.ownerDocument ?? document;
|
|
263
|
+
injectStyle(doc);
|
|
264
|
+
const el = doc.createElement("div");
|
|
265
|
+
el.className = "pp-insp";
|
|
266
|
+
const heading = doc.createElement("h4");
|
|
267
|
+
heading.textContent = opts.title ?? "Runtime state";
|
|
268
|
+
const list = doc.createElement("div");
|
|
269
|
+
el.append(heading, list);
|
|
270
|
+
const rowRefreshers = [];
|
|
271
|
+
const buildRow = (row) => {
|
|
272
|
+
const r = doc.createElement("div");
|
|
273
|
+
r.className = "pp-insp-row";
|
|
274
|
+
const label = doc.createElement("span");
|
|
275
|
+
label.className = "pp-insp-ref";
|
|
276
|
+
label.textContent = row.ref;
|
|
277
|
+
label.title = row.ref;
|
|
278
|
+
const ctl = doc.createElement("div");
|
|
279
|
+
ctl.className = "pp-insp-ctl";
|
|
280
|
+
const reset = doc.createElement("button");
|
|
281
|
+
reset.className = "pp-insp-reset";
|
|
282
|
+
reset.type = "button";
|
|
283
|
+
reset.textContent = "\u21BA";
|
|
284
|
+
reset.title = "Reset to default";
|
|
285
|
+
r.append(label, ctl, reset);
|
|
286
|
+
list.appendChild(r);
|
|
287
|
+
let read;
|
|
288
|
+
const commit = (v) => {
|
|
289
|
+
engine.setProperty(row.ref, v);
|
|
290
|
+
syncReset();
|
|
291
|
+
};
|
|
292
|
+
const focused = (node) => doc.activeElement === node;
|
|
293
|
+
function syncReset() {
|
|
294
|
+
reset.disabled = sameValue(engine.getProperty(row.ref), row.default);
|
|
295
|
+
}
|
|
296
|
+
reset.addEventListener("click", () => {
|
|
297
|
+
engine.setProperty(row.ref, row.default);
|
|
298
|
+
read();
|
|
299
|
+
syncReset();
|
|
300
|
+
});
|
|
301
|
+
if (row.type === "boolean") {
|
|
302
|
+
const cb = doc.createElement("input");
|
|
303
|
+
cb.type = "checkbox";
|
|
304
|
+
cb.addEventListener("change", () => commit(cb.checked));
|
|
305
|
+
ctl.appendChild(cb);
|
|
306
|
+
read = () => {
|
|
307
|
+
if (!focused(cb)) cb.checked = engine.getProperty(row.ref) === true;
|
|
308
|
+
};
|
|
309
|
+
} else if (row.type === "number") {
|
|
310
|
+
const inp = doc.createElement("input");
|
|
311
|
+
inp.type = "number";
|
|
312
|
+
inp.addEventListener("change", () => commit(Number(inp.value)));
|
|
313
|
+
ctl.appendChild(inp);
|
|
314
|
+
read = () => {
|
|
315
|
+
if (!focused(inp)) inp.value = String(engine.getProperty(row.ref) ?? "");
|
|
316
|
+
};
|
|
317
|
+
} else if (row.type === "enum") {
|
|
318
|
+
const sel = doc.createElement("select");
|
|
319
|
+
for (const v of row.values ?? []) {
|
|
320
|
+
const o = doc.createElement("option");
|
|
321
|
+
o.value = v;
|
|
322
|
+
o.textContent = v;
|
|
323
|
+
sel.appendChild(o);
|
|
324
|
+
}
|
|
325
|
+
sel.addEventListener("change", () => commit(sel.value));
|
|
326
|
+
ctl.appendChild(sel);
|
|
327
|
+
read = () => {
|
|
328
|
+
if (!focused(sel)) sel.value = String(engine.getProperty(row.ref) ?? "");
|
|
329
|
+
};
|
|
330
|
+
} else if (row.type === "flags") {
|
|
331
|
+
const inp = doc.createElement("input");
|
|
332
|
+
inp.type = "text";
|
|
333
|
+
inp.placeholder = "comma, separated, flags";
|
|
334
|
+
inp.addEventListener("change", () => commit(inp.value.split(",").map((s) => s.trim()).filter((s) => s.length > 0)));
|
|
335
|
+
ctl.appendChild(inp);
|
|
336
|
+
read = () => {
|
|
337
|
+
if (!focused(inp)) {
|
|
338
|
+
const v = engine.getProperty(row.ref);
|
|
339
|
+
inp.value = Array.isArray(v) ? v.join(", ") : "";
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
} else {
|
|
343
|
+
const inp = doc.createElement("input");
|
|
344
|
+
inp.type = "text";
|
|
345
|
+
inp.addEventListener("change", () => commit(inp.value));
|
|
346
|
+
ctl.appendChild(inp);
|
|
347
|
+
read = () => {
|
|
348
|
+
if (!focused(inp)) inp.value = String(engine.getProperty(row.ref) ?? "");
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
read();
|
|
352
|
+
syncReset();
|
|
353
|
+
rowRefreshers.push(() => {
|
|
354
|
+
read();
|
|
355
|
+
syncReset();
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
const props = engine.listProperties();
|
|
359
|
+
if (props.length === 0) {
|
|
360
|
+
const empty = doc.createElement("div");
|
|
361
|
+
empty.className = "pp-insp-empty";
|
|
362
|
+
empty.textContent = "No @patter properties.";
|
|
363
|
+
list.appendChild(empty);
|
|
364
|
+
} else {
|
|
365
|
+
for (const row of props) buildRow(row);
|
|
366
|
+
}
|
|
367
|
+
const refresh = () => {
|
|
368
|
+
for (const fn of rowRefreshers) fn();
|
|
369
|
+
};
|
|
370
|
+
opts.container?.appendChild(el);
|
|
371
|
+
const pollMs = opts.pollMs ?? 250;
|
|
372
|
+
let timer;
|
|
373
|
+
if (pollMs > 0) timer = setInterval(refresh, pollMs);
|
|
374
|
+
return {
|
|
375
|
+
el,
|
|
376
|
+
refresh,
|
|
377
|
+
destroy() {
|
|
378
|
+
if (timer !== void 0) clearInterval(timer);
|
|
379
|
+
el.remove();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/audio.ts
|
|
385
|
+
function createAudioResolver(manifestJson, basePath) {
|
|
386
|
+
const manifest = JSON.parse(manifestJson);
|
|
387
|
+
const clips = manifest.clips ?? {};
|
|
388
|
+
const base = basePath.replace(/[/\\]+$/, "");
|
|
389
|
+
return {
|
|
390
|
+
resolve(beatId) {
|
|
391
|
+
const clip = clips[beatId];
|
|
392
|
+
if (!clip || !clip.file) return null;
|
|
393
|
+
return base ? `${base}/${clip.file}` : clip.file;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
398
|
+
0 && (module.exports = {
|
|
399
|
+
SAVE_SCHEMA,
|
|
400
|
+
applyLiveBundle,
|
|
401
|
+
createAudioResolver,
|
|
402
|
+
createDebugLink,
|
|
403
|
+
createPropertyInspector,
|
|
404
|
+
createStateLogger,
|
|
405
|
+
deserializeState,
|
|
406
|
+
diffState,
|
|
407
|
+
getProperty,
|
|
408
|
+
loadState,
|
|
409
|
+
saveState,
|
|
410
|
+
serializeState,
|
|
411
|
+
setProperties,
|
|
412
|
+
setProperty,
|
|
413
|
+
snapshotState
|
|
414
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { SaveGame, Engine, StepResult, Bundle } from '@patterkit/runtime';
|
|
2
|
+
|
|
3
|
+
declare const SAVE_SCHEMA = "patter/save@0";
|
|
4
|
+
interface SaveEnvelope {
|
|
5
|
+
schema: typeof SAVE_SCHEMA;
|
|
6
|
+
/** The engine save-game: shared `@patter`/`@scene` state, visit counts, and every live flow. */
|
|
7
|
+
save: SaveGame;
|
|
8
|
+
}
|
|
9
|
+
/** Capture the whole game as a tagged envelope (wraps `engine.saveGame()`). */
|
|
10
|
+
declare function saveState(engine: Engine): SaveEnvelope;
|
|
11
|
+
/** Restore a {@link saveState} envelope into an engine (fresh or live). Throws on a foreign/blank blob. */
|
|
12
|
+
declare function loadState(engine: Engine, env: SaveEnvelope): void;
|
|
13
|
+
/** Serialise the whole game to a JSON string (envelope + save-game) - drop into localStorage or a file. */
|
|
14
|
+
declare function serializeState(engine: Engine): string;
|
|
15
|
+
/** Parse + restore a {@link serializeState} string. Throws on malformed JSON or a foreign envelope. */
|
|
16
|
+
declare function deserializeState(engine: Engine, json: string): void;
|
|
17
|
+
|
|
18
|
+
/** A property value the engine accepts (a Patter scalar: number / boolean / string / string[] flags). */
|
|
19
|
+
type PropertyValue = Parameters<Engine["setProperty"]>[1];
|
|
20
|
+
/** Read a runtime property (`@hp`, `@scene.locked`, a foreign `@world.x`). Undefined when unset. */
|
|
21
|
+
declare function getProperty(engine: Engine, ref: string): PropertyValue | undefined;
|
|
22
|
+
/** Set one runtime property. Mirrors `engine.setProperty`, exported here for discoverability + symmetry. */
|
|
23
|
+
declare function setProperty(engine: Engine, ref: string, value: PropertyValue): void;
|
|
24
|
+
/** Set many at once, e.g. `setProperties(engine, { "@hp": 10, "@scene.locked": false })`. */
|
|
25
|
+
declare function setProperties(engine: Engine, values: Record<string, PropertyValue>): void;
|
|
26
|
+
|
|
27
|
+
/** A flattened runtime-state value (a Patter scalar: number / boolean / string / string[] flags). */
|
|
28
|
+
type StateValue = NonNullable<ReturnType<Engine["getProperty"]>>;
|
|
29
|
+
/** A flattened snapshot: dotted path -> value. Paths: `@patter.x`, `@scene:scene.x`, `visit:nodeId`,
|
|
30
|
+
* and `flow/...` for a flow's not-shared locals. */
|
|
31
|
+
type StateSnapshot = Record<string, StateValue>;
|
|
32
|
+
interface StateChange {
|
|
33
|
+
path: string;
|
|
34
|
+
from?: StateValue;
|
|
35
|
+
to?: StateValue;
|
|
36
|
+
}
|
|
37
|
+
/** Flatten the engine's whole-game state into a path -> value map (shared scopes + every live flow). */
|
|
38
|
+
declare function snapshotState(engine: Engine): StateSnapshot;
|
|
39
|
+
/** The sorted set of paths that differ between two snapshots (added / removed / changed). */
|
|
40
|
+
declare function diffState(prev: StateSnapshot, next: StateSnapshot): StateChange[];
|
|
41
|
+
interface StateLoggerOptions {
|
|
42
|
+
/** Where lines go; defaults to `console.log`. */
|
|
43
|
+
sink?: (line: string) => void;
|
|
44
|
+
/** Prefix tag for every line, e.g. the flow / save-slot name. */
|
|
45
|
+
label?: string;
|
|
46
|
+
}
|
|
47
|
+
interface StateLogger {
|
|
48
|
+
/** The current flattened state (no logging). */
|
|
49
|
+
snapshot(): StateSnapshot;
|
|
50
|
+
/** Diff since the last capture, log each change, and re-baseline. Returns the changes. */
|
|
51
|
+
capture(): StateChange[];
|
|
52
|
+
/** Trace one played step (line / text / game-event / choice / end), including any `gameData`. */
|
|
53
|
+
logStep(step: StepResult): void;
|
|
54
|
+
}
|
|
55
|
+
/** Create a state logger over an engine. Call `capture()` after each `advance`/`choose` to log mutations. */
|
|
56
|
+
declare function createStateLogger(engine: Engine, opts?: StateLoggerOptions): StateLogger;
|
|
57
|
+
|
|
58
|
+
/** A minimal structural type for a WebSocket implementation (browsers + Node 21+ have a global one). */
|
|
59
|
+
interface DebugSocketLike {
|
|
60
|
+
readyState: number;
|
|
61
|
+
send(data: string): void;
|
|
62
|
+
close(): void;
|
|
63
|
+
addEventListener(type: "open" | "close" | "error", listener: () => void): void;
|
|
64
|
+
/** Incoming editor messages (live bundle refresh). Optional so a bare-bones send-only socket still fits. */
|
|
65
|
+
addEventListener(type: "message", listener: (ev: {
|
|
66
|
+
data: unknown;
|
|
67
|
+
}) => void): void;
|
|
68
|
+
}
|
|
69
|
+
type DebugSocketCtor = new (url: string) => DebugSocketLike;
|
|
70
|
+
interface DebugLinkOptions {
|
|
71
|
+
/** The running bundle's build identity - pass `bundle.content.hash`. Lets the editor detect a stale build. */
|
|
72
|
+
build: string;
|
|
73
|
+
/** Optional project name, shown in the editor's debug panel. */
|
|
74
|
+
project?: string;
|
|
75
|
+
/** Editor WebSocket URL. Default `ws://127.0.0.1:4471`. */
|
|
76
|
+
url?: string;
|
|
77
|
+
/** A WebSocket constructor to use instead of the global one (Node < 21, or tests with `ws`). */
|
|
78
|
+
WebSocket?: DebugSocketCtor;
|
|
79
|
+
/** Live bundle refresh: the editor pushed a freshly compiled bundle. `data` is the .patterc JSON;
|
|
80
|
+
* hand it (with your current engine + bundle) to `applyLiveBundle`, re-bind your flow handles if it
|
|
81
|
+
* hot-swapped, then call `link.setBuild(build)`. Never called with malformed payloads. */
|
|
82
|
+
onBundle?: (msg: {
|
|
83
|
+
build: string;
|
|
84
|
+
data: string;
|
|
85
|
+
}) => void;
|
|
86
|
+
}
|
|
87
|
+
interface DebugLink {
|
|
88
|
+
/** Tell the editor a flow opened (so it can list it in the follow selector). */
|
|
89
|
+
flowOpened(flowId: string): void;
|
|
90
|
+
/** Report the current position of a flow - call after each advance()/choose(). */
|
|
91
|
+
observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;
|
|
92
|
+
/** Tell the editor a flow closed. */
|
|
93
|
+
flowClosed(flowId: string): void;
|
|
94
|
+
/** After applying a pushed bundle: report the build now running (re-hellos, so the editor's
|
|
95
|
+
* match/stale pill updates and it stops re-pushing the same bundle). */
|
|
96
|
+
setBuild(build: string): void;
|
|
97
|
+
/** Close the link. */
|
|
98
|
+
close(): void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Open a live debug link to Patterpad. Returns a handle whose calls are no-ops once the editor disconnects
|
|
102
|
+
* or if it was never listening - safe to leave wired into a shipping build behind a flag.
|
|
103
|
+
*/
|
|
104
|
+
declare function createDebugLink(opts: DebugLinkOptions): DebugLink;
|
|
105
|
+
|
|
106
|
+
interface LiveBundleResult {
|
|
107
|
+
/** The engine to keep using: the same instance for a "text" swap, a replacement for "structure". */
|
|
108
|
+
engine: Engine;
|
|
109
|
+
/** The parsed pushed bundle - hold on to it for the next apply's comparison. */
|
|
110
|
+
bundle: Bundle;
|
|
111
|
+
/** Which tier applied: "text" (strings-only, nothing restarted) or "structure" (full hot swap). */
|
|
112
|
+
kind: "text" | "structure";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Apply a bundle the editor pushed over the debug link. `current` is the bundle `engine` is running
|
|
116
|
+
* (needed for the structure-hash comparison); `data` is the pushed .patterc JSON. Throws only on
|
|
117
|
+
* unparseable JSON - a structural swap's edge cases are absorbed by the §9.8 drift policy inside
|
|
118
|
+
* `hotSwap`, which never throws for ordinary edits.
|
|
119
|
+
*/
|
|
120
|
+
declare function applyLiveBundle(engine: Engine, current: Bundle, data: string): LiveBundleResult;
|
|
121
|
+
|
|
122
|
+
interface PropertyInspectorOptions {
|
|
123
|
+
/** Where to mount the panel. If omitted, append the returned `el` yourself. */
|
|
124
|
+
container?: HTMLElement;
|
|
125
|
+
/** Panel heading. Default "Runtime state". */
|
|
126
|
+
title?: string;
|
|
127
|
+
/** Live-refresh interval in ms; 0 disables polling (call `refresh()` yourself). Default 250. */
|
|
128
|
+
pollMs?: number;
|
|
129
|
+
}
|
|
130
|
+
interface PropertyInspector {
|
|
131
|
+
/** The panel root (already inside `container` if you passed one). */
|
|
132
|
+
readonly el: HTMLElement;
|
|
133
|
+
/** Re-read every property and update the editors, skipping the field you're editing. */
|
|
134
|
+
refresh(): void;
|
|
135
|
+
/** Stop polling and remove the panel from the DOM. */
|
|
136
|
+
destroy(): void;
|
|
137
|
+
}
|
|
138
|
+
declare function createPropertyInspector(engine: Engine, opts?: PropertyInspectorOptions): PropertyInspector;
|
|
139
|
+
|
|
140
|
+
interface AudioResolver {
|
|
141
|
+
/** The full path/URL of a beat's winning audio take, or null when it has none. */
|
|
142
|
+
resolve(beatId: string): string | null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build an audio resolver from a `patteraudio.json` manifest string plus the base path/URL its files live
|
|
146
|
+
* under (wherever you deployed the audio folder). `resolve(beatId)` joins the base with the winning clip's
|
|
147
|
+
* relative path, or returns null when the beat has no recording. Never throws on a missing beat.
|
|
148
|
+
*/
|
|
149
|
+
declare function createAudioResolver(manifestJson: string, basePath: string): AudioResolver;
|
|
150
|
+
|
|
151
|
+
export { type AudioResolver, type DebugLink, type DebugLinkOptions, type DebugSocketLike, type LiveBundleResult, type PropertyInspector, type PropertyInspectorOptions, type PropertyValue, SAVE_SCHEMA, type SaveEnvelope, type StateChange, type StateLogger, type StateLoggerOptions, type StateSnapshot, type StateValue, applyLiveBundle, createAudioResolver, createDebugLink, createPropertyInspector, createStateLogger, deserializeState, diffState, getProperty, loadState, saveState, serializeState, setProperties, setProperty, snapshotState };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { SaveGame, Engine, StepResult, Bundle } from '@patterkit/runtime';
|
|
2
|
+
|
|
3
|
+
declare const SAVE_SCHEMA = "patter/save@0";
|
|
4
|
+
interface SaveEnvelope {
|
|
5
|
+
schema: typeof SAVE_SCHEMA;
|
|
6
|
+
/** The engine save-game: shared `@patter`/`@scene` state, visit counts, and every live flow. */
|
|
7
|
+
save: SaveGame;
|
|
8
|
+
}
|
|
9
|
+
/** Capture the whole game as a tagged envelope (wraps `engine.saveGame()`). */
|
|
10
|
+
declare function saveState(engine: Engine): SaveEnvelope;
|
|
11
|
+
/** Restore a {@link saveState} envelope into an engine (fresh or live). Throws on a foreign/blank blob. */
|
|
12
|
+
declare function loadState(engine: Engine, env: SaveEnvelope): void;
|
|
13
|
+
/** Serialise the whole game to a JSON string (envelope + save-game) - drop into localStorage or a file. */
|
|
14
|
+
declare function serializeState(engine: Engine): string;
|
|
15
|
+
/** Parse + restore a {@link serializeState} string. Throws on malformed JSON or a foreign envelope. */
|
|
16
|
+
declare function deserializeState(engine: Engine, json: string): void;
|
|
17
|
+
|
|
18
|
+
/** A property value the engine accepts (a Patter scalar: number / boolean / string / string[] flags). */
|
|
19
|
+
type PropertyValue = Parameters<Engine["setProperty"]>[1];
|
|
20
|
+
/** Read a runtime property (`@hp`, `@scene.locked`, a foreign `@world.x`). Undefined when unset. */
|
|
21
|
+
declare function getProperty(engine: Engine, ref: string): PropertyValue | undefined;
|
|
22
|
+
/** Set one runtime property. Mirrors `engine.setProperty`, exported here for discoverability + symmetry. */
|
|
23
|
+
declare function setProperty(engine: Engine, ref: string, value: PropertyValue): void;
|
|
24
|
+
/** Set many at once, e.g. `setProperties(engine, { "@hp": 10, "@scene.locked": false })`. */
|
|
25
|
+
declare function setProperties(engine: Engine, values: Record<string, PropertyValue>): void;
|
|
26
|
+
|
|
27
|
+
/** A flattened runtime-state value (a Patter scalar: number / boolean / string / string[] flags). */
|
|
28
|
+
type StateValue = NonNullable<ReturnType<Engine["getProperty"]>>;
|
|
29
|
+
/** A flattened snapshot: dotted path -> value. Paths: `@patter.x`, `@scene:scene.x`, `visit:nodeId`,
|
|
30
|
+
* and `flow/...` for a flow's not-shared locals. */
|
|
31
|
+
type StateSnapshot = Record<string, StateValue>;
|
|
32
|
+
interface StateChange {
|
|
33
|
+
path: string;
|
|
34
|
+
from?: StateValue;
|
|
35
|
+
to?: StateValue;
|
|
36
|
+
}
|
|
37
|
+
/** Flatten the engine's whole-game state into a path -> value map (shared scopes + every live flow). */
|
|
38
|
+
declare function snapshotState(engine: Engine): StateSnapshot;
|
|
39
|
+
/** The sorted set of paths that differ between two snapshots (added / removed / changed). */
|
|
40
|
+
declare function diffState(prev: StateSnapshot, next: StateSnapshot): StateChange[];
|
|
41
|
+
interface StateLoggerOptions {
|
|
42
|
+
/** Where lines go; defaults to `console.log`. */
|
|
43
|
+
sink?: (line: string) => void;
|
|
44
|
+
/** Prefix tag for every line, e.g. the flow / save-slot name. */
|
|
45
|
+
label?: string;
|
|
46
|
+
}
|
|
47
|
+
interface StateLogger {
|
|
48
|
+
/** The current flattened state (no logging). */
|
|
49
|
+
snapshot(): StateSnapshot;
|
|
50
|
+
/** Diff since the last capture, log each change, and re-baseline. Returns the changes. */
|
|
51
|
+
capture(): StateChange[];
|
|
52
|
+
/** Trace one played step (line / text / game-event / choice / end), including any `gameData`. */
|
|
53
|
+
logStep(step: StepResult): void;
|
|
54
|
+
}
|
|
55
|
+
/** Create a state logger over an engine. Call `capture()` after each `advance`/`choose` to log mutations. */
|
|
56
|
+
declare function createStateLogger(engine: Engine, opts?: StateLoggerOptions): StateLogger;
|
|
57
|
+
|
|
58
|
+
/** A minimal structural type for a WebSocket implementation (browsers + Node 21+ have a global one). */
|
|
59
|
+
interface DebugSocketLike {
|
|
60
|
+
readyState: number;
|
|
61
|
+
send(data: string): void;
|
|
62
|
+
close(): void;
|
|
63
|
+
addEventListener(type: "open" | "close" | "error", listener: () => void): void;
|
|
64
|
+
/** Incoming editor messages (live bundle refresh). Optional so a bare-bones send-only socket still fits. */
|
|
65
|
+
addEventListener(type: "message", listener: (ev: {
|
|
66
|
+
data: unknown;
|
|
67
|
+
}) => void): void;
|
|
68
|
+
}
|
|
69
|
+
type DebugSocketCtor = new (url: string) => DebugSocketLike;
|
|
70
|
+
interface DebugLinkOptions {
|
|
71
|
+
/** The running bundle's build identity - pass `bundle.content.hash`. Lets the editor detect a stale build. */
|
|
72
|
+
build: string;
|
|
73
|
+
/** Optional project name, shown in the editor's debug panel. */
|
|
74
|
+
project?: string;
|
|
75
|
+
/** Editor WebSocket URL. Default `ws://127.0.0.1:4471`. */
|
|
76
|
+
url?: string;
|
|
77
|
+
/** A WebSocket constructor to use instead of the global one (Node < 21, or tests with `ws`). */
|
|
78
|
+
WebSocket?: DebugSocketCtor;
|
|
79
|
+
/** Live bundle refresh: the editor pushed a freshly compiled bundle. `data` is the .patterc JSON;
|
|
80
|
+
* hand it (with your current engine + bundle) to `applyLiveBundle`, re-bind your flow handles if it
|
|
81
|
+
* hot-swapped, then call `link.setBuild(build)`. Never called with malformed payloads. */
|
|
82
|
+
onBundle?: (msg: {
|
|
83
|
+
build: string;
|
|
84
|
+
data: string;
|
|
85
|
+
}) => void;
|
|
86
|
+
}
|
|
87
|
+
interface DebugLink {
|
|
88
|
+
/** Tell the editor a flow opened (so it can list it in the follow selector). */
|
|
89
|
+
flowOpened(flowId: string): void;
|
|
90
|
+
/** Report the current position of a flow - call after each advance()/choose(). */
|
|
91
|
+
observe(flowId: string, sceneId: string | null, beatId: string | null, type: string, choiceId?: string): void;
|
|
92
|
+
/** Tell the editor a flow closed. */
|
|
93
|
+
flowClosed(flowId: string): void;
|
|
94
|
+
/** After applying a pushed bundle: report the build now running (re-hellos, so the editor's
|
|
95
|
+
* match/stale pill updates and it stops re-pushing the same bundle). */
|
|
96
|
+
setBuild(build: string): void;
|
|
97
|
+
/** Close the link. */
|
|
98
|
+
close(): void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Open a live debug link to Patterpad. Returns a handle whose calls are no-ops once the editor disconnects
|
|
102
|
+
* or if it was never listening - safe to leave wired into a shipping build behind a flag.
|
|
103
|
+
*/
|
|
104
|
+
declare function createDebugLink(opts: DebugLinkOptions): DebugLink;
|
|
105
|
+
|
|
106
|
+
interface LiveBundleResult {
|
|
107
|
+
/** The engine to keep using: the same instance for a "text" swap, a replacement for "structure". */
|
|
108
|
+
engine: Engine;
|
|
109
|
+
/** The parsed pushed bundle - hold on to it for the next apply's comparison. */
|
|
110
|
+
bundle: Bundle;
|
|
111
|
+
/** Which tier applied: "text" (strings-only, nothing restarted) or "structure" (full hot swap). */
|
|
112
|
+
kind: "text" | "structure";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Apply a bundle the editor pushed over the debug link. `current` is the bundle `engine` is running
|
|
116
|
+
* (needed for the structure-hash comparison); `data` is the pushed .patterc JSON. Throws only on
|
|
117
|
+
* unparseable JSON - a structural swap's edge cases are absorbed by the §9.8 drift policy inside
|
|
118
|
+
* `hotSwap`, which never throws for ordinary edits.
|
|
119
|
+
*/
|
|
120
|
+
declare function applyLiveBundle(engine: Engine, current: Bundle, data: string): LiveBundleResult;
|
|
121
|
+
|
|
122
|
+
interface PropertyInspectorOptions {
|
|
123
|
+
/** Where to mount the panel. If omitted, append the returned `el` yourself. */
|
|
124
|
+
container?: HTMLElement;
|
|
125
|
+
/** Panel heading. Default "Runtime state". */
|
|
126
|
+
title?: string;
|
|
127
|
+
/** Live-refresh interval in ms; 0 disables polling (call `refresh()` yourself). Default 250. */
|
|
128
|
+
pollMs?: number;
|
|
129
|
+
}
|
|
130
|
+
interface PropertyInspector {
|
|
131
|
+
/** The panel root (already inside `container` if you passed one). */
|
|
132
|
+
readonly el: HTMLElement;
|
|
133
|
+
/** Re-read every property and update the editors, skipping the field you're editing. */
|
|
134
|
+
refresh(): void;
|
|
135
|
+
/** Stop polling and remove the panel from the DOM. */
|
|
136
|
+
destroy(): void;
|
|
137
|
+
}
|
|
138
|
+
declare function createPropertyInspector(engine: Engine, opts?: PropertyInspectorOptions): PropertyInspector;
|
|
139
|
+
|
|
140
|
+
interface AudioResolver {
|
|
141
|
+
/** The full path/URL of a beat's winning audio take, or null when it has none. */
|
|
142
|
+
resolve(beatId: string): string | null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build an audio resolver from a `patteraudio.json` manifest string plus the base path/URL its files live
|
|
146
|
+
* under (wherever you deployed the audio folder). `resolve(beatId)` joins the base with the winning clip's
|
|
147
|
+
* relative path, or returns null when the beat has no recording. Never throws on a missing beat.
|
|
148
|
+
*/
|
|
149
|
+
declare function createAudioResolver(manifestJson: string, basePath: string): AudioResolver;
|
|
150
|
+
|
|
151
|
+
export { type AudioResolver, type DebugLink, type DebugLinkOptions, type DebugSocketLike, type LiveBundleResult, type PropertyInspector, type PropertyInspectorOptions, type PropertyValue, SAVE_SCHEMA, type SaveEnvelope, type StateChange, type StateLogger, type StateLoggerOptions, type StateSnapshot, type StateValue, applyLiveBundle, createAudioResolver, createDebugLink, createPropertyInspector, createStateLogger, deserializeState, diffState, getProperty, loadState, saveState, serializeState, setProperties, setProperty, snapshotState };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// src/save.ts
|
|
2
|
+
var SAVE_SCHEMA = "patter/save@0";
|
|
3
|
+
function saveState(engine) {
|
|
4
|
+
return { schema: SAVE_SCHEMA, save: engine.saveGame() };
|
|
5
|
+
}
|
|
6
|
+
function loadState(engine, env) {
|
|
7
|
+
if (!env || env.schema !== SAVE_SCHEMA || !env.save) {
|
|
8
|
+
throw new Error(`loadState: not a ${SAVE_SCHEMA} envelope`);
|
|
9
|
+
}
|
|
10
|
+
engine.loadGame(env.save);
|
|
11
|
+
}
|
|
12
|
+
function serializeState(engine) {
|
|
13
|
+
return JSON.stringify(saveState(engine));
|
|
14
|
+
}
|
|
15
|
+
function deserializeState(engine, json) {
|
|
16
|
+
loadState(engine, JSON.parse(json));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/properties.ts
|
|
20
|
+
function getProperty(engine, ref) {
|
|
21
|
+
return engine.getProperty(ref);
|
|
22
|
+
}
|
|
23
|
+
function setProperty(engine, ref, value) {
|
|
24
|
+
engine.setProperty(ref, value);
|
|
25
|
+
}
|
|
26
|
+
function setProperties(engine, values) {
|
|
27
|
+
for (const [ref, value] of Object.entries(values)) engine.setProperty(ref, value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/logger.ts
|
|
31
|
+
var eq = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
32
|
+
function snapshotState(engine) {
|
|
33
|
+
const save = engine.saveGame();
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [name, v] of Object.entries(save.shared.patter ?? {})) out[`@patter.${name}`] = v;
|
|
36
|
+
for (const [scene, vals] of Object.entries(save.stageBags))
|
|
37
|
+
for (const [name, v] of Object.entries(vals)) out[`@scene:${scene}.${name}`] = v;
|
|
38
|
+
for (const [id, n] of Object.entries(save.sharedVisits)) out[`visit:${id}`] = n;
|
|
39
|
+
for (const [fid, snap] of Object.entries(save.flows)) {
|
|
40
|
+
for (const [name, v] of Object.entries(snap.scopes.patter ?? {})) out[`${fid}/@patter.${name}`] = v;
|
|
41
|
+
for (const [scene, vals] of Object.entries(snap.sceneBags))
|
|
42
|
+
for (const [name, v] of Object.entries(vals)) out[`${fid}/@scene:${scene}.${name}`] = v;
|
|
43
|
+
for (const [id, n] of Object.entries(snap.visits)) out[`${fid}/visit:${id}`] = n;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function diffState(prev, next) {
|
|
48
|
+
const changes = [];
|
|
49
|
+
const keys = [.../* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)])].sort();
|
|
50
|
+
for (const path of keys) {
|
|
51
|
+
const from = prev[path], to = next[path];
|
|
52
|
+
if (!eq(from, to)) changes.push({ path, ...from !== void 0 ? { from } : {}, ...to !== void 0 ? { to } : {} });
|
|
53
|
+
}
|
|
54
|
+
return changes;
|
|
55
|
+
}
|
|
56
|
+
var fmt = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
|
|
57
|
+
function describeStep(step) {
|
|
58
|
+
switch (step.type) {
|
|
59
|
+
case "line":
|
|
60
|
+
return `line ${step.character ?? "?"}: ${JSON.stringify(step.text)}${gd(step.gameData)}`;
|
|
61
|
+
case "text":
|
|
62
|
+
return `text: ${JSON.stringify(step.text)}${gd(step.gameData)}`;
|
|
63
|
+
case "gameEvent":
|
|
64
|
+
return `game event ${step.id}${gd(step.gameData)}`;
|
|
65
|
+
case "choice":
|
|
66
|
+
return `choice (${step.options.length} option${step.options.length === 1 ? "" : "s"})`;
|
|
67
|
+
case "end":
|
|
68
|
+
return "end";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var gd = (data) => data ? ` gameData=${JSON.stringify(data)}` : "";
|
|
72
|
+
function createStateLogger(engine, opts = {}) {
|
|
73
|
+
const sink = opts.sink ?? ((line) => console.log(line));
|
|
74
|
+
const tag = opts.label ? `[${opts.label}] ` : "";
|
|
75
|
+
let baseline = snapshotState(engine);
|
|
76
|
+
return {
|
|
77
|
+
snapshot: () => snapshotState(engine),
|
|
78
|
+
capture() {
|
|
79
|
+
const next = snapshotState(engine);
|
|
80
|
+
const changes = diffState(baseline, next);
|
|
81
|
+
baseline = next;
|
|
82
|
+
for (const c of changes) sink(`${tag}${c.path}: ${fmt(c.from)} -> ${fmt(c.to)}`);
|
|
83
|
+
return changes;
|
|
84
|
+
},
|
|
85
|
+
logStep(step) {
|
|
86
|
+
sink(`${tag}${describeStep(step)}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/debug.ts
|
|
92
|
+
var OPEN = 1;
|
|
93
|
+
function createDebugLink(opts) {
|
|
94
|
+
const url = opts.url ?? "ws://127.0.0.1:4471";
|
|
95
|
+
const Ctor = opts.WebSocket ?? globalThis.WebSocket;
|
|
96
|
+
const flows = /* @__PURE__ */ new Set();
|
|
97
|
+
let queue = [];
|
|
98
|
+
let sock = null;
|
|
99
|
+
let closed = false;
|
|
100
|
+
let build = opts.build;
|
|
101
|
+
const flush = () => {
|
|
102
|
+
if (!sock || sock.readyState !== OPEN) return;
|
|
103
|
+
for (const m of queue) {
|
|
104
|
+
try {
|
|
105
|
+
sock.send(m);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
queue = [];
|
|
110
|
+
};
|
|
111
|
+
const post = (msg) => {
|
|
112
|
+
if (closed) return;
|
|
113
|
+
queue.push(JSON.stringify(msg));
|
|
114
|
+
flush();
|
|
115
|
+
};
|
|
116
|
+
if (!Ctor) {
|
|
117
|
+
return { flowOpened() {
|
|
118
|
+
}, observe() {
|
|
119
|
+
}, flowClosed() {
|
|
120
|
+
}, setBuild() {
|
|
121
|
+
}, close() {
|
|
122
|
+
closed = true;
|
|
123
|
+
} };
|
|
124
|
+
}
|
|
125
|
+
const sendHello = () => {
|
|
126
|
+
const hello = JSON.stringify({ t: "hello", v: 1, build, project: opts.project, flows: [...flows] });
|
|
127
|
+
try {
|
|
128
|
+
sock?.send(hello);
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
sock = new Ctor(url);
|
|
134
|
+
sock.addEventListener("open", () => {
|
|
135
|
+
sendHello();
|
|
136
|
+
flush();
|
|
137
|
+
});
|
|
138
|
+
sock.addEventListener("message", (ev) => {
|
|
139
|
+
if (!opts.onBundle || typeof ev.data !== "string") return;
|
|
140
|
+
try {
|
|
141
|
+
const msg = JSON.parse(ev.data);
|
|
142
|
+
if (msg.t === "bundle" && typeof msg.build === "string" && typeof msg.data === "string") {
|
|
143
|
+
opts.onBundle({ build: msg.build, data: msg.data });
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
sock.addEventListener("error", () => {
|
|
149
|
+
});
|
|
150
|
+
sock.addEventListener("close", () => {
|
|
151
|
+
sock = null;
|
|
152
|
+
});
|
|
153
|
+
} catch {
|
|
154
|
+
sock = null;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
flowOpened(flowId) {
|
|
158
|
+
flows.add(flowId);
|
|
159
|
+
post({ t: "flowOpen", flow: flowId });
|
|
160
|
+
},
|
|
161
|
+
flowClosed(flowId) {
|
|
162
|
+
flows.delete(flowId);
|
|
163
|
+
post({ t: "flowClose", flow: flowId });
|
|
164
|
+
},
|
|
165
|
+
observe(flowId, sceneId, beatId, type, choiceId) {
|
|
166
|
+
post({ t: "frame", flow: flowId, sceneId, beatId, type, choiceId });
|
|
167
|
+
},
|
|
168
|
+
setBuild(next) {
|
|
169
|
+
if (closed || next === build) return;
|
|
170
|
+
build = next;
|
|
171
|
+
if (sock && sock.readyState === OPEN) sendHello();
|
|
172
|
+
},
|
|
173
|
+
close() {
|
|
174
|
+
closed = true;
|
|
175
|
+
queue = [];
|
|
176
|
+
try {
|
|
177
|
+
sock?.close();
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
sock = null;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/refresh.ts
|
|
186
|
+
import "@patterkit/runtime";
|
|
187
|
+
function applyLiveBundle(engine, current, data) {
|
|
188
|
+
const next = JSON.parse(data);
|
|
189
|
+
const sameStructure = current.content.structureHash !== void 0 && current.content.structureHash === next.content.structureHash;
|
|
190
|
+
if (sameStructure) {
|
|
191
|
+
engine.replaceStrings(next);
|
|
192
|
+
return { engine, bundle: next, kind: "text" };
|
|
193
|
+
}
|
|
194
|
+
return { engine: engine.hotSwap(next), bundle: next, kind: "structure" };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/inspector.ts
|
|
198
|
+
var STYLE_ID = "pp-inspector-style";
|
|
199
|
+
var CSS = `
|
|
200
|
+
.pp-insp{font:13px/1.4 ui-sans-serif,system-ui,sans-serif;color:#15201e;background:#f4efe6;border:1px solid #cfc7b8;border-radius:10px;padding:.6rem .7rem;max-width:22rem;box-shadow:0 6px 20px rgba(21,32,30,.12)}
|
|
201
|
+
.pp-insp h4{margin:0 0 .4rem;font:600 .72rem/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:#5c6b62}
|
|
202
|
+
.pp-insp-empty{color:#8a9691;font-style:italic}
|
|
203
|
+
.pp-insp-row{display:flex;align-items:center;gap:.4rem;margin:.18rem 0}
|
|
204
|
+
.pp-insp-ref{flex:0 0 8rem;font-family:ui-monospace,monospace;font-size:.78rem;color:#214f4b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
205
|
+
.pp-insp-ctl{flex:1;min-width:0;display:flex}
|
|
206
|
+
.pp-insp-ctl input[type=text],.pp-insp-ctl input[type=number],.pp-insp-ctl select{width:100%;box-sizing:border-box;font:inherit;padding:.15rem .3rem;border:1px solid #cfc7b8;border-radius:6px;background:#fff;color:inherit}
|
|
207
|
+
.pp-insp-reset{flex:0 0 auto;border:1px solid #cfc7b8;background:#fff;border-radius:6px;cursor:pointer;color:#5c6b62;width:1.6rem;height:1.6rem;line-height:1;padding:0}
|
|
208
|
+
.pp-insp-reset:disabled{opacity:.35;cursor:default}
|
|
209
|
+
`;
|
|
210
|
+
function injectStyle(doc) {
|
|
211
|
+
if (doc.getElementById(STYLE_ID)) return;
|
|
212
|
+
const s = doc.createElement("style");
|
|
213
|
+
s.id = STYLE_ID;
|
|
214
|
+
s.textContent = CSS;
|
|
215
|
+
(doc.head ?? doc.documentElement).appendChild(s);
|
|
216
|
+
}
|
|
217
|
+
function sameValue(a, b) {
|
|
218
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
219
|
+
return a === b;
|
|
220
|
+
}
|
|
221
|
+
function createPropertyInspector(engine, opts = {}) {
|
|
222
|
+
const doc = opts.container?.ownerDocument ?? document;
|
|
223
|
+
injectStyle(doc);
|
|
224
|
+
const el = doc.createElement("div");
|
|
225
|
+
el.className = "pp-insp";
|
|
226
|
+
const heading = doc.createElement("h4");
|
|
227
|
+
heading.textContent = opts.title ?? "Runtime state";
|
|
228
|
+
const list = doc.createElement("div");
|
|
229
|
+
el.append(heading, list);
|
|
230
|
+
const rowRefreshers = [];
|
|
231
|
+
const buildRow = (row) => {
|
|
232
|
+
const r = doc.createElement("div");
|
|
233
|
+
r.className = "pp-insp-row";
|
|
234
|
+
const label = doc.createElement("span");
|
|
235
|
+
label.className = "pp-insp-ref";
|
|
236
|
+
label.textContent = row.ref;
|
|
237
|
+
label.title = row.ref;
|
|
238
|
+
const ctl = doc.createElement("div");
|
|
239
|
+
ctl.className = "pp-insp-ctl";
|
|
240
|
+
const reset = doc.createElement("button");
|
|
241
|
+
reset.className = "pp-insp-reset";
|
|
242
|
+
reset.type = "button";
|
|
243
|
+
reset.textContent = "\u21BA";
|
|
244
|
+
reset.title = "Reset to default";
|
|
245
|
+
r.append(label, ctl, reset);
|
|
246
|
+
list.appendChild(r);
|
|
247
|
+
let read;
|
|
248
|
+
const commit = (v) => {
|
|
249
|
+
engine.setProperty(row.ref, v);
|
|
250
|
+
syncReset();
|
|
251
|
+
};
|
|
252
|
+
const focused = (node) => doc.activeElement === node;
|
|
253
|
+
function syncReset() {
|
|
254
|
+
reset.disabled = sameValue(engine.getProperty(row.ref), row.default);
|
|
255
|
+
}
|
|
256
|
+
reset.addEventListener("click", () => {
|
|
257
|
+
engine.setProperty(row.ref, row.default);
|
|
258
|
+
read();
|
|
259
|
+
syncReset();
|
|
260
|
+
});
|
|
261
|
+
if (row.type === "boolean") {
|
|
262
|
+
const cb = doc.createElement("input");
|
|
263
|
+
cb.type = "checkbox";
|
|
264
|
+
cb.addEventListener("change", () => commit(cb.checked));
|
|
265
|
+
ctl.appendChild(cb);
|
|
266
|
+
read = () => {
|
|
267
|
+
if (!focused(cb)) cb.checked = engine.getProperty(row.ref) === true;
|
|
268
|
+
};
|
|
269
|
+
} else if (row.type === "number") {
|
|
270
|
+
const inp = doc.createElement("input");
|
|
271
|
+
inp.type = "number";
|
|
272
|
+
inp.addEventListener("change", () => commit(Number(inp.value)));
|
|
273
|
+
ctl.appendChild(inp);
|
|
274
|
+
read = () => {
|
|
275
|
+
if (!focused(inp)) inp.value = String(engine.getProperty(row.ref) ?? "");
|
|
276
|
+
};
|
|
277
|
+
} else if (row.type === "enum") {
|
|
278
|
+
const sel = doc.createElement("select");
|
|
279
|
+
for (const v of row.values ?? []) {
|
|
280
|
+
const o = doc.createElement("option");
|
|
281
|
+
o.value = v;
|
|
282
|
+
o.textContent = v;
|
|
283
|
+
sel.appendChild(o);
|
|
284
|
+
}
|
|
285
|
+
sel.addEventListener("change", () => commit(sel.value));
|
|
286
|
+
ctl.appendChild(sel);
|
|
287
|
+
read = () => {
|
|
288
|
+
if (!focused(sel)) sel.value = String(engine.getProperty(row.ref) ?? "");
|
|
289
|
+
};
|
|
290
|
+
} else if (row.type === "flags") {
|
|
291
|
+
const inp = doc.createElement("input");
|
|
292
|
+
inp.type = "text";
|
|
293
|
+
inp.placeholder = "comma, separated, flags";
|
|
294
|
+
inp.addEventListener("change", () => commit(inp.value.split(",").map((s) => s.trim()).filter((s) => s.length > 0)));
|
|
295
|
+
ctl.appendChild(inp);
|
|
296
|
+
read = () => {
|
|
297
|
+
if (!focused(inp)) {
|
|
298
|
+
const v = engine.getProperty(row.ref);
|
|
299
|
+
inp.value = Array.isArray(v) ? v.join(", ") : "";
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
} else {
|
|
303
|
+
const inp = doc.createElement("input");
|
|
304
|
+
inp.type = "text";
|
|
305
|
+
inp.addEventListener("change", () => commit(inp.value));
|
|
306
|
+
ctl.appendChild(inp);
|
|
307
|
+
read = () => {
|
|
308
|
+
if (!focused(inp)) inp.value = String(engine.getProperty(row.ref) ?? "");
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
read();
|
|
312
|
+
syncReset();
|
|
313
|
+
rowRefreshers.push(() => {
|
|
314
|
+
read();
|
|
315
|
+
syncReset();
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
const props = engine.listProperties();
|
|
319
|
+
if (props.length === 0) {
|
|
320
|
+
const empty = doc.createElement("div");
|
|
321
|
+
empty.className = "pp-insp-empty";
|
|
322
|
+
empty.textContent = "No @patter properties.";
|
|
323
|
+
list.appendChild(empty);
|
|
324
|
+
} else {
|
|
325
|
+
for (const row of props) buildRow(row);
|
|
326
|
+
}
|
|
327
|
+
const refresh = () => {
|
|
328
|
+
for (const fn of rowRefreshers) fn();
|
|
329
|
+
};
|
|
330
|
+
opts.container?.appendChild(el);
|
|
331
|
+
const pollMs = opts.pollMs ?? 250;
|
|
332
|
+
let timer;
|
|
333
|
+
if (pollMs > 0) timer = setInterval(refresh, pollMs);
|
|
334
|
+
return {
|
|
335
|
+
el,
|
|
336
|
+
refresh,
|
|
337
|
+
destroy() {
|
|
338
|
+
if (timer !== void 0) clearInterval(timer);
|
|
339
|
+
el.remove();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/audio.ts
|
|
345
|
+
function createAudioResolver(manifestJson, basePath) {
|
|
346
|
+
const manifest = JSON.parse(manifestJson);
|
|
347
|
+
const clips = manifest.clips ?? {};
|
|
348
|
+
const base = basePath.replace(/[/\\]+$/, "");
|
|
349
|
+
return {
|
|
350
|
+
resolve(beatId) {
|
|
351
|
+
const clip = clips[beatId];
|
|
352
|
+
if (!clip || !clip.file) return null;
|
|
353
|
+
return base ? `${base}/${clip.file}` : clip.file;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
export {
|
|
358
|
+
SAVE_SCHEMA,
|
|
359
|
+
applyLiveBundle,
|
|
360
|
+
createAudioResolver,
|
|
361
|
+
createDebugLink,
|
|
362
|
+
createPropertyInspector,
|
|
363
|
+
createStateLogger,
|
|
364
|
+
deserializeState,
|
|
365
|
+
diffState,
|
|
366
|
+
getProperty,
|
|
367
|
+
loadState,
|
|
368
|
+
saveState,
|
|
369
|
+
serializeState,
|
|
370
|
+
setProperties,
|
|
371
|
+
setProperty,
|
|
372
|
+
snapshotState
|
|
373
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patterkit/play-helpers",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin game-integration helpers around @patterkit/runtime: save/load serialisation, runtime property setters, a state logger, the Patterpad Live Link client + hot reload, a property inspector, and audio resolution. The Patterplay JS companion.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ian Thomas",
|
|
8
|
+
"homepage": "https://patterkit.dev",
|
|
9
|
+
"repository": { "type": "git", "url": "git+https://github.com/patterkit/patter.git", "directory": "packages/play-helpers" },
|
|
10
|
+
"bugs": "https://github.com/patterkit/patter/issues",
|
|
11
|
+
"publishConfig": { "access": "public" },
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": ["dist", "README.md"],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@patterkit/runtime": "0.1.0"
|
|
29
|
+
}
|
|
30
|
+
}
|