@patterkit/play-helpers 0.2.0 → 0.3.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/dist/index.cjs +169 -23
- package/dist/index.d.cts +339 -4
- package/dist/index.d.ts +339 -4
- package/dist/index.js +168 -23
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
SAVE_SCHEMA: () => SAVE_SCHEMA,
|
|
24
24
|
applyLiveBundle: () => applyLiveBundle,
|
|
25
25
|
createAudioResolver: () => createAudioResolver,
|
|
26
|
+
createBundleInspector: () => createBundleInspector,
|
|
26
27
|
createDebugLink: () => createDebugLink,
|
|
27
28
|
createPropertyInspector: () => createPropertyInspector,
|
|
28
29
|
createStateLogger: () => createStateLogger,
|
|
@@ -263,8 +264,8 @@ function sameValue(a, b) {
|
|
|
263
264
|
function createPropertyInspector(engine, opts = {}) {
|
|
264
265
|
const doc = opts.container?.ownerDocument ?? document;
|
|
265
266
|
injectStyle(doc);
|
|
266
|
-
const
|
|
267
|
-
|
|
267
|
+
const el2 = doc.createElement("div");
|
|
268
|
+
el2.className = "pp-insp";
|
|
268
269
|
const heading = doc.createElement("h4");
|
|
269
270
|
heading.textContent = opts.title ?? "Runtime state";
|
|
270
271
|
const list = doc.createElement("div");
|
|
@@ -303,15 +304,15 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
303
304
|
input.click();
|
|
304
305
|
});
|
|
305
306
|
io.append(saveBtn, loadBtn);
|
|
306
|
-
|
|
307
|
+
el2.append(heading, io, list);
|
|
307
308
|
const rowRefreshers = [];
|
|
308
|
-
const buildRow = (
|
|
309
|
+
const buildRow = (row2) => {
|
|
309
310
|
const r = doc.createElement("div");
|
|
310
311
|
r.className = "pp-insp-row";
|
|
311
312
|
const label = doc.createElement("span");
|
|
312
313
|
label.className = "pp-insp-ref";
|
|
313
|
-
label.textContent =
|
|
314
|
-
label.title =
|
|
314
|
+
label.textContent = row2.ref;
|
|
315
|
+
label.title = row2.ref;
|
|
315
316
|
const ctl = doc.createElement("div");
|
|
316
317
|
ctl.className = "pp-insp-ctl";
|
|
317
318
|
const reset = doc.createElement("button");
|
|
@@ -323,37 +324,37 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
323
324
|
list.appendChild(r);
|
|
324
325
|
let read;
|
|
325
326
|
const commit = (v) => {
|
|
326
|
-
engine.setProperty(
|
|
327
|
+
engine.setProperty(row2.ref, v);
|
|
327
328
|
syncReset();
|
|
328
329
|
};
|
|
329
330
|
const focused = (node) => doc.activeElement === node;
|
|
330
331
|
function syncReset() {
|
|
331
|
-
reset.disabled = sameValue(engine.getProperty(
|
|
332
|
+
reset.disabled = sameValue(engine.getProperty(row2.ref), row2.default);
|
|
332
333
|
}
|
|
333
334
|
reset.addEventListener("click", () => {
|
|
334
|
-
engine.setProperty(
|
|
335
|
+
engine.setProperty(row2.ref, row2.default);
|
|
335
336
|
read();
|
|
336
337
|
syncReset();
|
|
337
338
|
});
|
|
338
|
-
if (
|
|
339
|
+
if (row2.type === "boolean") {
|
|
339
340
|
const cb = doc.createElement("input");
|
|
340
341
|
cb.type = "checkbox";
|
|
341
342
|
cb.addEventListener("change", () => commit(cb.checked));
|
|
342
343
|
ctl.appendChild(cb);
|
|
343
344
|
read = () => {
|
|
344
|
-
if (!focused(cb)) cb.checked = engine.getProperty(
|
|
345
|
+
if (!focused(cb)) cb.checked = engine.getProperty(row2.ref) === true;
|
|
345
346
|
};
|
|
346
|
-
} else if (
|
|
347
|
+
} else if (row2.type === "number") {
|
|
347
348
|
const inp = doc.createElement("input");
|
|
348
349
|
inp.type = "number";
|
|
349
350
|
inp.addEventListener("change", () => commit(Number(inp.value)));
|
|
350
351
|
ctl.appendChild(inp);
|
|
351
352
|
read = () => {
|
|
352
|
-
if (!focused(inp)) inp.value = String(engine.getProperty(
|
|
353
|
+
if (!focused(inp)) inp.value = String(engine.getProperty(row2.ref) ?? "");
|
|
353
354
|
};
|
|
354
|
-
} else if (
|
|
355
|
+
} else if (row2.type === "enum") {
|
|
355
356
|
const sel = doc.createElement("select");
|
|
356
|
-
for (const v of
|
|
357
|
+
for (const v of row2.values ?? []) {
|
|
357
358
|
const o = doc.createElement("option");
|
|
358
359
|
o.value = v;
|
|
359
360
|
o.textContent = v;
|
|
@@ -362,9 +363,9 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
362
363
|
sel.addEventListener("change", () => commit(sel.value));
|
|
363
364
|
ctl.appendChild(sel);
|
|
364
365
|
read = () => {
|
|
365
|
-
if (!focused(sel)) sel.value = String(engine.getProperty(
|
|
366
|
+
if (!focused(sel)) sel.value = String(engine.getProperty(row2.ref) ?? "");
|
|
366
367
|
};
|
|
367
|
-
} else if (
|
|
368
|
+
} else if (row2.type === "flags") {
|
|
368
369
|
const inp = doc.createElement("input");
|
|
369
370
|
inp.type = "text";
|
|
370
371
|
inp.placeholder = "comma, separated, flags";
|
|
@@ -372,7 +373,7 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
372
373
|
ctl.appendChild(inp);
|
|
373
374
|
read = () => {
|
|
374
375
|
if (!focused(inp)) {
|
|
375
|
-
const v = engine.getProperty(
|
|
376
|
+
const v = engine.getProperty(row2.ref);
|
|
376
377
|
inp.value = Array.isArray(v) ? v.join(", ") : "";
|
|
377
378
|
}
|
|
378
379
|
};
|
|
@@ -382,7 +383,7 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
382
383
|
inp.addEventListener("change", () => commit(inp.value));
|
|
383
384
|
ctl.appendChild(inp);
|
|
384
385
|
read = () => {
|
|
385
|
-
if (!focused(inp)) inp.value = String(engine.getProperty(
|
|
386
|
+
if (!focused(inp)) inp.value = String(engine.getProperty(row2.ref) ?? "");
|
|
386
387
|
};
|
|
387
388
|
}
|
|
388
389
|
read();
|
|
@@ -399,21 +400,165 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
399
400
|
empty.textContent = "No @patter properties.";
|
|
400
401
|
list.appendChild(empty);
|
|
401
402
|
} else {
|
|
402
|
-
for (const
|
|
403
|
+
for (const row2 of props) buildRow(row2);
|
|
403
404
|
}
|
|
404
405
|
const refresh = () => {
|
|
405
406
|
for (const fn of rowRefreshers) fn();
|
|
406
407
|
};
|
|
407
|
-
opts.container?.appendChild(
|
|
408
|
+
opts.container?.appendChild(el2);
|
|
408
409
|
const pollMs = opts.pollMs ?? 250;
|
|
409
410
|
let timer;
|
|
410
411
|
if (pollMs > 0) timer = setInterval(refresh, pollMs);
|
|
411
412
|
return {
|
|
412
|
-
el,
|
|
413
|
+
el: el2,
|
|
413
414
|
refresh,
|
|
414
415
|
destroy() {
|
|
415
416
|
if (timer !== void 0) clearInterval(timer);
|
|
416
|
-
|
|
417
|
+
el2.remove();
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/bundle-inspector.ts
|
|
423
|
+
var import_runtime2 = require("@patterkit/runtime");
|
|
424
|
+
var STYLE_ID2 = "pp-bundle-style";
|
|
425
|
+
var CSS2 = `
|
|
426
|
+
.pp-bundle{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:26rem;box-shadow:0 6px 20px rgba(21,32,30,.12)}
|
|
427
|
+
.pp-bundle h4{margin:0 0 .4rem;font:600 .72rem/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:#5c6b62}
|
|
428
|
+
.pp-bundle details{border-top:1px solid #e3dccf;padding:.3rem 0}
|
|
429
|
+
.pp-bundle details:first-of-type{border-top:0}
|
|
430
|
+
.pp-bundle summary{cursor:pointer;font-weight:600;color:#214f4b;list-style:none}
|
|
431
|
+
.pp-bundle summary::-webkit-details-marker{display:none}
|
|
432
|
+
.pp-bundle summary::before{content:"\\25B8";display:inline-block;width:1rem;color:#8a9691}
|
|
433
|
+
.pp-bundle details[open]>summary::before{content:"\\25BE"}
|
|
434
|
+
.pp-bundle-count{color:#8a9691;font-weight:400}
|
|
435
|
+
.pp-bundle-row{display:flex;gap:.4rem;margin:.16rem 0 .16rem 1rem}
|
|
436
|
+
.pp-bundle-key{flex:0 0 8rem;color:#5c6b62}
|
|
437
|
+
.pp-bundle-val,.pp-bundle-addr{font-family:ui-monospace,monospace;font-size:.78rem;color:#214f4b;word-break:break-all}
|
|
438
|
+
.pp-bundle-sub{margin-left:2rem}
|
|
439
|
+
.pp-bundle-empty{color:#8a9691;font-style:italic;margin-left:1rem}
|
|
440
|
+
.pp-bundle-warn{color:#8a3a2f;font-weight:600}
|
|
441
|
+
.pp-bundle-tag{font-size:.7rem;color:#5c6b62;border:1px solid #cfc7b8;border-radius:5px;padding:0 .25rem;margin-left:.3rem}
|
|
442
|
+
`;
|
|
443
|
+
function injectStyle2(doc) {
|
|
444
|
+
if (doc.getElementById(STYLE_ID2)) return;
|
|
445
|
+
const s = doc.createElement("style");
|
|
446
|
+
s.id = STYLE_ID2;
|
|
447
|
+
s.textContent = CSS2;
|
|
448
|
+
(doc.head ?? doc.documentElement).appendChild(s);
|
|
449
|
+
}
|
|
450
|
+
var el = (doc, tag, cls, text) => {
|
|
451
|
+
const e = doc.createElement(tag);
|
|
452
|
+
if (cls) e.className = cls;
|
|
453
|
+
if (text !== void 0) e.textContent = text;
|
|
454
|
+
return e;
|
|
455
|
+
};
|
|
456
|
+
function row(doc, key, value, valueClass = "pp-bundle-val") {
|
|
457
|
+
const r = el(doc, "div", "pp-bundle-row");
|
|
458
|
+
r.append(el(doc, "span", "pp-bundle-key", key), el(doc, "span", valueClass, value));
|
|
459
|
+
return r;
|
|
460
|
+
}
|
|
461
|
+
function propertyRow(doc, p) {
|
|
462
|
+
const r = el(doc, "div", "pp-bundle-row");
|
|
463
|
+
r.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", p.name));
|
|
464
|
+
const meta = el(doc, "span", "pp-bundle-val", p.type);
|
|
465
|
+
if (!p.hasDefault) meta.append(el(doc, "span", "pp-bundle-tag", "no default"));
|
|
466
|
+
r.append(meta);
|
|
467
|
+
return r;
|
|
468
|
+
}
|
|
469
|
+
function section(doc, name, label, count, open) {
|
|
470
|
+
const d = doc.createElement("details");
|
|
471
|
+
d.dataset["section"] = name;
|
|
472
|
+
d.open = open;
|
|
473
|
+
const s = doc.createElement("summary");
|
|
474
|
+
s.textContent = label;
|
|
475
|
+
if (count !== null) s.append(el(doc, "span", "pp-bundle-count", ` ${count}`));
|
|
476
|
+
d.append(s);
|
|
477
|
+
return d;
|
|
478
|
+
}
|
|
479
|
+
function createBundleInspector(bundle, opts = {}) {
|
|
480
|
+
const doc = opts.container?.ownerDocument ?? document;
|
|
481
|
+
injectStyle2(doc);
|
|
482
|
+
const d = (0, import_runtime2.describeBundle)(bundle);
|
|
483
|
+
const open = new Set(opts.open ?? ["identity", "addresses"]);
|
|
484
|
+
const root = el(doc, "div", "pp-bundle");
|
|
485
|
+
root.append(el(doc, "h4", void 0, opts.title ?? "Bundle"));
|
|
486
|
+
const id = section(doc, "identity", d.identity.project || "(unnamed project)", null, open.has("identity"));
|
|
487
|
+
if (d.identity.version) id.append(row(doc, "version", d.identity.version));
|
|
488
|
+
id.append(row(doc, "schema", d.identity.schema));
|
|
489
|
+
id.append(row(doc, "locales", `${d.identity.defaultLocale}${d.identity.locales.length > 1 ? ` (+${d.identity.locales.length - 1})` : ""}`));
|
|
490
|
+
id.append(row(doc, "strings", d.identity.localisation));
|
|
491
|
+
if (d.identity.voiced) id.append(row(doc, "voiced", "yes"));
|
|
492
|
+
if (d.identity.hash) id.append(row(doc, "hash", d.identity.hash));
|
|
493
|
+
if (d.identity.structureHash) id.append(row(doc, "structure", d.identity.structureHash));
|
|
494
|
+
if (d.identity.sourceDebug) {
|
|
495
|
+
id.append(row(doc, "build", "SOURCE DEBUG - not shippable", "pp-bundle-val pp-bundle-warn"));
|
|
496
|
+
}
|
|
497
|
+
root.append(id);
|
|
498
|
+
const addr = section(doc, "addresses", "Addresses", d.addresses.length, open.has("addresses"));
|
|
499
|
+
if (!d.addresses.length) addr.append(el(doc, "div", "pp-bundle-empty", "no scenes"));
|
|
500
|
+
for (const a of d.addresses) {
|
|
501
|
+
const r = el(doc, "div", "pp-bundle-row");
|
|
502
|
+
r.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", a.gameId), el(doc, "span", "pp-bundle-val", a.name));
|
|
503
|
+
addr.append(r);
|
|
504
|
+
for (const b of a.blocks) {
|
|
505
|
+
const br = el(doc, "div", "pp-bundle-row pp-bundle-sub");
|
|
506
|
+
br.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", b.gameId), el(doc, "span", "pp-bundle-val", b.name));
|
|
507
|
+
addr.append(br);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
root.append(addr);
|
|
511
|
+
const hostCount = d.hostScopes.reduce((n, s) => n + s.properties.length, 0);
|
|
512
|
+
const host = section(doc, "hostScopes", "Host properties", hostCount, open.has("hostScopes"));
|
|
513
|
+
if (!d.hostScopes.length) host.append(el(doc, "div", "pp-bundle-empty", "the game supplies nothing"));
|
|
514
|
+
for (const s of d.hostScopes) {
|
|
515
|
+
const head = el(doc, "div", "pp-bundle-row");
|
|
516
|
+
head.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", `@${s.token}`));
|
|
517
|
+
const meta = el(doc, "span", "pp-bundle-val", s.opaque ? "any name, unchecked" : `${s.properties.length} declared`);
|
|
518
|
+
if (!s.writable) meta.append(el(doc, "span", "pp-bundle-tag", "read-only"));
|
|
519
|
+
head.append(meta);
|
|
520
|
+
host.append(head);
|
|
521
|
+
for (const p of s.properties) {
|
|
522
|
+
const pr = propertyRow(doc, p);
|
|
523
|
+
pr.classList.add("pp-bundle-sub");
|
|
524
|
+
host.append(pr);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
root.append(host);
|
|
528
|
+
const ownedCount = d.properties.patter.length + d.properties.scene.reduce((n, s) => n + s.properties.length, 0);
|
|
529
|
+
const owned = section(doc, "properties", "Story properties", ownedCount, open.has("properties"));
|
|
530
|
+
if (!ownedCount) owned.append(el(doc, "div", "pp-bundle-empty", "none declared"));
|
|
531
|
+
for (const p of d.properties.patter) owned.append(propertyRow(doc, p));
|
|
532
|
+
for (const s of d.properties.scene) {
|
|
533
|
+
owned.append(row(doc, `@scene`, s.gameId, "pp-bundle-val pp-bundle-addr"));
|
|
534
|
+
for (const p of s.properties) {
|
|
535
|
+
const pr = propertyRow(doc, p);
|
|
536
|
+
pr.classList.add("pp-bundle-sub");
|
|
537
|
+
owned.append(pr);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
root.append(owned);
|
|
541
|
+
if (d.gameData.length) {
|
|
542
|
+
const gd2 = section(doc, "gameData", "Game data", d.gameData.reduce((n, g) => n + g.fields.length, 0), open.has("gameData"));
|
|
543
|
+
for (const g of d.gameData) {
|
|
544
|
+
gd2.append(row(doc, "on", g.kind));
|
|
545
|
+
for (const f of g.fields) {
|
|
546
|
+
const fr = el(doc, "div", "pp-bundle-row pp-bundle-sub");
|
|
547
|
+
fr.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", f.name), el(doc, "span", "pp-bundle-val", f.type));
|
|
548
|
+
gd2.append(fr);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
root.append(gd2);
|
|
552
|
+
}
|
|
553
|
+
const counts = section(doc, "counts", "Counts", null, open.has("counts"));
|
|
554
|
+
for (const [key, value] of Object.entries(d.counts)) counts.append(row(doc, key, String(value)));
|
|
555
|
+
root.append(counts);
|
|
556
|
+
opts.container?.append(root);
|
|
557
|
+
return {
|
|
558
|
+
el: root,
|
|
559
|
+
description: d,
|
|
560
|
+
destroy() {
|
|
561
|
+
root.remove();
|
|
417
562
|
}
|
|
418
563
|
};
|
|
419
564
|
}
|
|
@@ -436,6 +581,7 @@ function createAudioResolver(manifestJson, basePath) {
|
|
|
436
581
|
SAVE_SCHEMA,
|
|
437
582
|
applyLiveBundle,
|
|
438
583
|
createAudioResolver,
|
|
584
|
+
createBundleInspector,
|
|
439
585
|
createDebugLink,
|
|
440
586
|
createPropertyInspector,
|
|
441
587
|
createStateLogger,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SaveGame, Engine, StepResult, Bundle } from '@patterkit/runtime';
|
|
1
|
+
import { SaveGame, Engine, StepResult, Bundle as Bundle$1, BundleDescription } from '@patterkit/runtime';
|
|
2
2
|
|
|
3
3
|
declare const SAVE_SCHEMA = "patter/save@0";
|
|
4
4
|
interface SaveEnvelope {
|
|
@@ -107,7 +107,7 @@ interface LiveBundleResult {
|
|
|
107
107
|
/** The engine to keep using: the same instance for a "text" swap, a replacement for "structure". */
|
|
108
108
|
engine: Engine;
|
|
109
109
|
/** The parsed pushed bundle - hold on to it for the next apply's comparison. */
|
|
110
|
-
bundle: Bundle;
|
|
110
|
+
bundle: Bundle$1;
|
|
111
111
|
/** Which tier applied: "text" (strings-only, nothing restarted) or "structure" (full hot swap). */
|
|
112
112
|
kind: "text" | "structure";
|
|
113
113
|
}
|
|
@@ -117,7 +117,7 @@ interface LiveBundleResult {
|
|
|
117
117
|
* unparseable JSON - a structural swap's edge cases are absorbed by the §9.8 drift policy inside
|
|
118
118
|
* `hotSwap`, which never throws for ordinary edits.
|
|
119
119
|
*/
|
|
120
|
-
declare function applyLiveBundle(engine: Engine, current: Bundle, data: string): LiveBundleResult;
|
|
120
|
+
declare function applyLiveBundle(engine: Engine, current: Bundle$1, data: string): LiveBundleResult;
|
|
121
121
|
|
|
122
122
|
interface PropertyInspectorOptions {
|
|
123
123
|
/** Where to mount the panel. If omitted, append the returned `el` yourself. */
|
|
@@ -137,6 +137,341 @@ interface PropertyInspector {
|
|
|
137
137
|
}
|
|
138
138
|
declare function createPropertyInspector(engine: Engine, opts?: PropertyInspectorOptions): PropertyInspector;
|
|
139
139
|
|
|
140
|
+
type BinaryOp = "==" | "!=" | ">" | ">=" | "<" | "<=" | "+" | "-" | "*" | "/" | "and" | "or";
|
|
141
|
+
type UnaryOp = "not" | "neg";
|
|
142
|
+
type AstNode = ["b", boolean] | ["n", number] | ["s", string] | ["sv", string, string] | ["u", UnaryOp, AstNode] | ["bin", BinaryOp, AstNode, AstNode] | ["call", string, ...AstNode[]] | ["fd", "+" | "-", string];
|
|
143
|
+
|
|
144
|
+
type ScalarValue = boolean | number | string | string[];
|
|
145
|
+
/** Developer-defined host metadata (spec §17). Opaque to Patter. */
|
|
146
|
+
type GameData = Record<string, unknown>;
|
|
147
|
+
/** A scene/block id, or the reserved "END". */
|
|
148
|
+
type JumpTarget = string;
|
|
149
|
+
interface Jump {
|
|
150
|
+
to: JumpTarget;
|
|
151
|
+
/** "jump" (one-way, default) or "call" (jump-and-return via the flow callstack). */
|
|
152
|
+
mode?: "jump" | "call";
|
|
153
|
+
}
|
|
154
|
+
interface LineBeat {
|
|
155
|
+
id: string;
|
|
156
|
+
kind: "line";
|
|
157
|
+
/** Speaker; must be a member of the project cast (validated). */
|
|
158
|
+
character?: string;
|
|
159
|
+
/** Performance direction, language-neutral (never localised). */
|
|
160
|
+
direction?: string;
|
|
161
|
+
gameData?: GameData;
|
|
162
|
+
/** Author-defined freeform tags (#215): a cross-cutting label layer that travels to the runtime.
|
|
163
|
+
* At runtime a beat's tags are the UNION of its own and every ancestor's (scene → block → group(s) →
|
|
164
|
+
* snippet → beat). Each tag is letters/digits/symbols with NO comma and NO whitespace; deduped. */
|
|
165
|
+
tags?: string[];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Authorial voice / narration - speaker-less prose (spec §2). Never voiced; its
|
|
169
|
+
* localised text always permits inline `{@name}` interpolation (spec §16). This
|
|
170
|
+
* is the on-screen-text role (e.g. "A door slams!") - distinct from a game-event
|
|
171
|
+
* beat, which is a pure engine instruction with no localised content.
|
|
172
|
+
*/
|
|
173
|
+
interface TextBeat {
|
|
174
|
+
id: string;
|
|
175
|
+
kind: "text";
|
|
176
|
+
gameData?: GameData;
|
|
177
|
+
/** Author tags (#215). See LineBeat.tags. */
|
|
178
|
+
tags?: string[];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A GAME EVENT (spec §2): a pure engine instruction with no player-facing text - just `gameData` the host
|
|
182
|
+
* reads when the beat plays (comments / docs attach via the authoring file by id). Named "game event"
|
|
183
|
+
* rather than "action" because a screenplay's "action" is prose, which is our text beat. Player-facing
|
|
184
|
+
* words are a line or text beat instead.
|
|
185
|
+
*/
|
|
186
|
+
interface GameEventBeat {
|
|
187
|
+
id: string;
|
|
188
|
+
kind: "gameEvent";
|
|
189
|
+
gameData?: GameData;
|
|
190
|
+
/** Author tags (#215). See LineBeat.tags. */
|
|
191
|
+
tags?: string[];
|
|
192
|
+
}
|
|
193
|
+
type Beat = LineBeat | TextBeat | GameEventBeat;
|
|
194
|
+
type Selector = "run" | "branch" | "sequence" | "choice";
|
|
195
|
+
/** How a `sequence` walks its children. `specificity` = **Best match**: pick the eligible
|
|
196
|
+
* child whose condition most specifically fits the current state (the most atomic constraints
|
|
197
|
+
* actively holding it true); equally-specific ties break by the seeded shuffle. A child with no
|
|
198
|
+
* condition scores zero, so it acts as the filler that wins only when nothing more specific is
|
|
199
|
+
* eligible. Composes with `exhaust`: `repeat` re-scores every visit (re-pickable, the Best-match
|
|
200
|
+
* default), `once` uses each pick up so the group slides down to the filler (graceful degradation). */
|
|
201
|
+
type SelectorOrder = "sequential" | "shuffle" | "specificity";
|
|
202
|
+
/** What a `sequence` does after one full pass through its children. */
|
|
203
|
+
type SelectorExhaust = "once" | "repeat" | "stick";
|
|
204
|
+
/**
|
|
205
|
+
* `sequence` selector config (spec §4). One stateful picker with two orthogonal
|
|
206
|
+
* axes subsumes Ink's stopping / cycle / once / shuffle and their combinations.
|
|
207
|
+
* Defaults: `order: "sequential"`, `exhaust: "once"`. `shuffle` draws without
|
|
208
|
+
* replacement and never repeats a line back-to-back (built in).
|
|
209
|
+
*/
|
|
210
|
+
interface SequenceOptions {
|
|
211
|
+
order?: SelectorOrder;
|
|
212
|
+
exhaust?: SelectorExhaust;
|
|
213
|
+
}
|
|
214
|
+
/** An option's prompt beat (spec §5): a single line | text beat - the choice text. */
|
|
215
|
+
type PromptBeat = LineBeat | TextBeat;
|
|
216
|
+
type PropertyType = "boolean" | "number" | "string" | "flags" | "enum";
|
|
217
|
+
interface PropertyDecl {
|
|
218
|
+
name: string;
|
|
219
|
+
type: PropertyType;
|
|
220
|
+
default?: ScalarValue;
|
|
221
|
+
/**
|
|
222
|
+
* The orthogonal *sharing* axis (spec §7): is this property's value shared across
|
|
223
|
+
* all flows (one world value) or kept per-flow? It does NOT change the reference
|
|
224
|
+
* syntax - sharing is set here, on the declaration, not by a different scope token.
|
|
225
|
+
* The default depends on the scope it is declared in: a **global** property
|
|
226
|
+
* (project `properties` -> `@patter`) defaults to **shared**; a **scene-local**
|
|
227
|
+
* property (scene `sceneProps` -> `@scene`) defaults to **not shared** (per-flow).
|
|
228
|
+
*/
|
|
229
|
+
shared?: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Persistence axis, for **scene-local (`@scene`) properties** (spec §7). Default
|
|
232
|
+
* `false`: the value PERSISTS across scene re-entries (like every other property).
|
|
233
|
+
* `true`: the value is **reseeded to its default on every scene entry** - "fresh
|
|
234
|
+
* each playthrough" (Ink's `temp`). Orthogonal to `shared`. Ignored on global
|
|
235
|
+
* (`@patter`) properties, which always persist for the life of the piece.
|
|
236
|
+
*/
|
|
237
|
+
temporary?: boolean;
|
|
238
|
+
/** For enum / flags. */
|
|
239
|
+
values?: string[];
|
|
240
|
+
/** Free-text author note documenting what this property is for (authoring only; shown as a hint). */
|
|
241
|
+
purpose?: string;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A property of a host / world scope (`@world`, `@game`, ...). The same shape the
|
|
245
|
+
* `@wildwinter/scoperegistry` `scopeRegistrySpec` uses, declared structurally here
|
|
246
|
+
* so the model stays free of a runtime dependency. `default` seeds the standalone
|
|
247
|
+
* runtime's self-backed bag (see `HostScopeSpec`); `writable: false` makes the
|
|
248
|
+
* property read-only to the story (validated at compile time).
|
|
249
|
+
*/
|
|
250
|
+
interface HostScopeDecl {
|
|
251
|
+
name: string;
|
|
252
|
+
type: PropertyType;
|
|
253
|
+
values?: string[];
|
|
254
|
+
default?: ScalarValue;
|
|
255
|
+
writable?: boolean;
|
|
256
|
+
/** Free-text author note documenting the property (authoring only; shown as a hint). */
|
|
257
|
+
purpose?: string;
|
|
258
|
+
}
|
|
259
|
+
/** One scope (`token`) in a project's host-scope registry. */
|
|
260
|
+
interface HostScopeSpec {
|
|
261
|
+
/** The scope token after `@` (e.g. `"world"`). Must not collide with Patter's own
|
|
262
|
+
* `patter` / `scene` / `flow`. */
|
|
263
|
+
token: string;
|
|
264
|
+
/** Scope-level read/write default for its declarations (default true). */
|
|
265
|
+
writable?: boolean;
|
|
266
|
+
/** Property declarations; omit for an opaque scope (any name, unchecked). */
|
|
267
|
+
declarations?: HostScopeDecl[];
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A project's host-scope registry: the `scopeRegistrySpec` it OWNS (spec
|
|
271
|
+
* design/scope-registry.md §6). Makes `@world` (and any host scope) first-class
|
|
272
|
+
* in a standalone project: the compiler validates references into it, the runtime
|
|
273
|
+
* self-backs it from declaration defaults when no host resolver claims the token,
|
|
274
|
+
* and coverage drives its values. Structurally identical to scoperegistry's
|
|
275
|
+
* `ScopeRegistrySpec` so it threads straight through the compiler.
|
|
276
|
+
*/
|
|
277
|
+
interface HostScopeRegistry {
|
|
278
|
+
version: number;
|
|
279
|
+
scopes: HostScopeSpec[];
|
|
280
|
+
}
|
|
281
|
+
/** A character's grammatical gender, for localisation. Translators need it to inflect the speaker's own
|
|
282
|
+
* lines in gendered languages (adjectives, participles, pronouns), which the source text alone often
|
|
283
|
+
* cannot tell them. Absent means "not specified". Authoring-only: it never reaches the runtime bundle,
|
|
284
|
+
* but it IS carried into the localisation handoff formats as translator context (spec §14).
|
|
285
|
+
*
|
|
286
|
+
* Free text, not a closed set: real languages need more than three genders (common/utrum, animate/
|
|
287
|
+
* inanimate, and so on) and translators name them differently. `COMMON_GENDERS` seeds the editor's
|
|
288
|
+
* auto-suggest so the everyday values stay spelled consistently; anything else is still valid. */
|
|
289
|
+
type GrammaticalGender = string;
|
|
290
|
+
interface CastMember {
|
|
291
|
+
/** Canonical speaker name (matched by a beat's `character`); language-neutral key. */
|
|
292
|
+
name: string;
|
|
293
|
+
/** Localised player-facing name - a localisation id (project-level strings). */
|
|
294
|
+
displayName?: string;
|
|
295
|
+
/** Grammatical gender for translators (see `GrammaticalGender`); absent = not specified. */
|
|
296
|
+
gender?: GrammaticalGender;
|
|
297
|
+
/** Free-text production notes about the character (casting, voice, intent) - authoring only. */
|
|
298
|
+
notes?: string;
|
|
299
|
+
/** The voice actor cast for this character, if known - surfaced in the VO script export (spec §16).
|
|
300
|
+
* Authoring-only (not shipped in the runtime bundle). */
|
|
301
|
+
actor?: string;
|
|
302
|
+
gameData?: GameData;
|
|
303
|
+
}
|
|
304
|
+
/** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor` and
|
|
305
|
+
* `gender` are authoring / production / translation context, and the compiler drops them, so a shipped
|
|
306
|
+
* game never carries a real person's name or a writer's private notes. The compiler copies the shipping
|
|
307
|
+
* fields across explicitly (an allow-list), which is what actually keeps a new authoring field out of
|
|
308
|
+
* the bundle; this type states the resulting contract for anyone reading a bundle. */
|
|
309
|
+
type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender">;
|
|
310
|
+
/** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
|
|
311
|
+
* Beat kinds map straight through: dialogue = `line`, narration = `text`, game event = `gameEvent`. */
|
|
312
|
+
type GameDataNodeKind = "scene" | "block" | "snippet" | "line" | "text" | "gameEvent";
|
|
313
|
+
/** The value type of a gameData field - the property-type vocabulary plus a multiline-text variant.
|
|
314
|
+
* Drives the inspector's editor widget (text / textarea / number / toggle / enum dropdown). */
|
|
315
|
+
type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum";
|
|
316
|
+
/** One author-defined custom field on a node type (host-integration metadata, NOT expression state). */
|
|
317
|
+
interface GameDataField {
|
|
318
|
+
/** Field key - also the key a node stores its value under in `gameData` (values are name-keyed). */
|
|
319
|
+
name: string;
|
|
320
|
+
type: GameDataFieldType;
|
|
321
|
+
/** The value used when a node sets nothing. Storage is SPARSE - nodes hold only their overrides, and
|
|
322
|
+
* a reader falls back to this default (so changing it here propagates to every node that didn't set it). */
|
|
323
|
+
default?: ScalarValue;
|
|
324
|
+
/** Allowed values when `type` is "enum". */
|
|
325
|
+
values?: string[];
|
|
326
|
+
/** Free-text description of what the field is for - shown as a rollover hint in the inspector. */
|
|
327
|
+
purpose?: string;
|
|
328
|
+
}
|
|
329
|
+
/** Author-defined gameData fields, grouped by the node type they attach to (project-level schema). */
|
|
330
|
+
type GameDataFields = Partial<Record<GameDataNodeKind, GameDataField[]>>;
|
|
331
|
+
/** A compiled expression envelope: canonical source + pre-derived tagged-tuple AST. */
|
|
332
|
+
interface Expression {
|
|
333
|
+
src: string;
|
|
334
|
+
ast: AstNode;
|
|
335
|
+
}
|
|
336
|
+
type CompiledEffect = {
|
|
337
|
+
kind: "set";
|
|
338
|
+
target: string;
|
|
339
|
+
value: Expression;
|
|
340
|
+
};
|
|
341
|
+
interface CompiledSnippet {
|
|
342
|
+
id: string;
|
|
343
|
+
type: "snippet";
|
|
344
|
+
condition?: Expression;
|
|
345
|
+
beats?: Beat[];
|
|
346
|
+
onEnter?: CompiledEffect[];
|
|
347
|
+
onExit?: CompiledEffect[];
|
|
348
|
+
gameData?: GameData;
|
|
349
|
+
tags?: string[];
|
|
350
|
+
jump?: Jump;
|
|
351
|
+
secretUntilEligible?: boolean;
|
|
352
|
+
/** Option-position: repeatable (spec §5). Default false = once-only. */
|
|
353
|
+
sticky?: boolean;
|
|
354
|
+
/** Option-position: the choice's fallback, auto-followed when last (spec §5). */
|
|
355
|
+
fallback?: boolean;
|
|
356
|
+
}
|
|
357
|
+
interface CompiledGroup {
|
|
358
|
+
id: string;
|
|
359
|
+
type: "group";
|
|
360
|
+
condition?: Expression;
|
|
361
|
+
/** Default (omitted) = `"run"`. */
|
|
362
|
+
selector?: Selector;
|
|
363
|
+
/** Selector cursor shared across flows (default false = per-flow). */
|
|
364
|
+
shared?: boolean;
|
|
365
|
+
options?: SequenceOptions;
|
|
366
|
+
children: Array<CompiledGroup | CompiledSnippet>;
|
|
367
|
+
gameData?: GameData;
|
|
368
|
+
tags?: string[];
|
|
369
|
+
/** Option-position fields (spec §5) - only when a direct child of a `choice`. */
|
|
370
|
+
prompt?: PromptBeat;
|
|
371
|
+
secretUntilEligible?: boolean;
|
|
372
|
+
/** Repeatable (spec §5). Default false = once-only. */
|
|
373
|
+
sticky?: boolean;
|
|
374
|
+
/** The choice's fallback, auto-followed when last (spec §5). */
|
|
375
|
+
fallback?: boolean;
|
|
376
|
+
}
|
|
377
|
+
interface CompiledBlock {
|
|
378
|
+
id: string;
|
|
379
|
+
type: "block";
|
|
380
|
+
name: string;
|
|
381
|
+
/** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
|
|
382
|
+
gameId?: string;
|
|
383
|
+
children: Array<CompiledGroup | CompiledSnippet>;
|
|
384
|
+
gameData?: GameData;
|
|
385
|
+
tags?: string[];
|
|
386
|
+
}
|
|
387
|
+
interface CompiledScene {
|
|
388
|
+
id: string;
|
|
389
|
+
type: "scene";
|
|
390
|
+
name: string;
|
|
391
|
+
/** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
|
|
392
|
+
gameId?: string;
|
|
393
|
+
gameData?: GameData;
|
|
394
|
+
tags?: string[];
|
|
395
|
+
onEntry?: CompiledEffect[];
|
|
396
|
+
sceneProps?: PropertyDecl[];
|
|
397
|
+
blocks: CompiledBlock[];
|
|
398
|
+
}
|
|
399
|
+
interface Bundle {
|
|
400
|
+
schema: string;
|
|
401
|
+
/** `hash` fingerprints the WHOLE bundle (binds saves, gates staleness); `structureHash` is the same
|
|
402
|
+
* fingerprint with the string tables left out, so same structureHash + a different hash = a
|
|
403
|
+
* text-only edit, safe to hot-swap in place (live bundle refresh). */
|
|
404
|
+
content: {
|
|
405
|
+
project: string;
|
|
406
|
+
version?: string;
|
|
407
|
+
hash?: string;
|
|
408
|
+
structureHash?: string;
|
|
409
|
+
};
|
|
410
|
+
voiced: boolean;
|
|
411
|
+
locales: {
|
|
412
|
+
default: string;
|
|
413
|
+
included: string[];
|
|
414
|
+
};
|
|
415
|
+
/** Player-facing cast only: the compiler strips notes / actor / gender (see `BundleCastMember`). */
|
|
416
|
+
cast?: BundleCastMember[];
|
|
417
|
+
properties?: PropertyDecl[];
|
|
418
|
+
/** Host / world scope declarations, baked from the project so the runtime can self-back a declared
|
|
419
|
+
* scope (`@world`, ...) when no host resolver claims its token. Absent = no host scopes. */
|
|
420
|
+
scopeRegistry?: HostScopeRegistry;
|
|
421
|
+
gameDataFields?: GameDataFields;
|
|
422
|
+
scenes: Record<string, CompiledScene>;
|
|
423
|
+
/** locale -> (beatId -> text). In "embedded" localisation this carries every included locale; in "ids"
|
|
424
|
+
* it is EMPTY (the runtime emits beat IDs), unless `localisation.sourceDebug` embedded the source locale
|
|
425
|
+
* for debug playback. `content.hash` is computed over the FULL strings regardless, so the staleness gate
|
|
426
|
+
* is unaffected. */
|
|
427
|
+
strings: Record<string, Record<string, string>>;
|
|
428
|
+
/** How strings ship + resolve (spec §11). Absent = "embedded" (back-compat default): the runtime resolves
|
|
429
|
+
* `strings` per locale. "ids": the runtime emits beat IDs and the game localises them itself; `sourceDebug`
|
|
430
|
+
* means the source locale is embedded purely for debug playback and the runtime should flag the build as
|
|
431
|
+
* not shippable. */
|
|
432
|
+
localisation?: {
|
|
433
|
+
mode: "embedded" | "ids";
|
|
434
|
+
sourceDebug?: boolean;
|
|
435
|
+
};
|
|
436
|
+
/** Closed-caption delimiters baked from the project (#214). Absent = the default `(` / `)`; the
|
|
437
|
+
* runtime strips spans between them from line text when a game disables captions. */
|
|
438
|
+
closedCaptions?: CaptionDelimiters;
|
|
439
|
+
}
|
|
440
|
+
/** Closed-caption configuration (#214). `open`/`close` wrap a caption cue inside a dialogue line (both
|
|
441
|
+
* non-empty; they MAY be the same token, e.g. `*…*`). `character` names a cast member whose lines are a
|
|
442
|
+
* pure caption: when captions are off, ALL of that character's dialogue (and its speaker label) is
|
|
443
|
+
* omitted - delimiters or not - leaving a silent line that still fires (so audio plays). Absent / empty
|
|
444
|
+
* `character` resolves to the default `SFX` (you "disable" it simply by never using that speaker). */
|
|
445
|
+
interface CaptionDelimiters {
|
|
446
|
+
open: string;
|
|
447
|
+
close: string;
|
|
448
|
+
character?: string;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
interface BundleInspectorOptions {
|
|
452
|
+
/** Where to mount the panel. If omitted, append the returned `el` yourself. */
|
|
453
|
+
container?: HTMLElement;
|
|
454
|
+
/** Panel heading. Default "Bundle". */
|
|
455
|
+
title?: string;
|
|
456
|
+
/** Sections open on mount. Default: identity and addresses, the two an
|
|
457
|
+
* integrator reaches for first; the rest are a click away. */
|
|
458
|
+
open?: BundleSection[];
|
|
459
|
+
}
|
|
460
|
+
type BundleSection = "identity" | "addresses" | "hostScopes" | "properties" | "gameData" | "counts";
|
|
461
|
+
interface BundleInspector {
|
|
462
|
+
/** The panel root (already inside `container` if you passed one). */
|
|
463
|
+
readonly el: HTMLElement;
|
|
464
|
+
/** The description the panel is showing, for a caller that wants the data too. */
|
|
465
|
+
readonly description: BundleDescription;
|
|
466
|
+
/** Remove the panel from the DOM. */
|
|
467
|
+
destroy(): void;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Build a read-only panel describing a compiled bundle: what it is, what game
|
|
471
|
+
* code may call on it, and what the host must supply.
|
|
472
|
+
*/
|
|
473
|
+
declare function createBundleInspector(bundle: Bundle, opts?: BundleInspectorOptions): BundleInspector;
|
|
474
|
+
|
|
140
475
|
interface AudioResolver {
|
|
141
476
|
/** The full path/URL of a beat's winning audio take, or null when it has none. */
|
|
142
477
|
resolve(beatId: string): string | null;
|
|
@@ -148,4 +483,4 @@ interface AudioResolver {
|
|
|
148
483
|
*/
|
|
149
484
|
declare function createAudioResolver(manifestJson: string, basePath: string): AudioResolver;
|
|
150
485
|
|
|
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 };
|
|
486
|
+
export { type AudioResolver, type BundleInspector, type BundleInspectorOptions, type BundleSection, 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, createBundleInspector, createDebugLink, createPropertyInspector, createStateLogger, deserializeState, diffState, getProperty, loadState, saveState, serializeState, setProperties, setProperty, snapshotState };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SaveGame, Engine, StepResult, Bundle } from '@patterkit/runtime';
|
|
1
|
+
import { SaveGame, Engine, StepResult, Bundle as Bundle$1, BundleDescription } from '@patterkit/runtime';
|
|
2
2
|
|
|
3
3
|
declare const SAVE_SCHEMA = "patter/save@0";
|
|
4
4
|
interface SaveEnvelope {
|
|
@@ -107,7 +107,7 @@ interface LiveBundleResult {
|
|
|
107
107
|
/** The engine to keep using: the same instance for a "text" swap, a replacement for "structure". */
|
|
108
108
|
engine: Engine;
|
|
109
109
|
/** The parsed pushed bundle - hold on to it for the next apply's comparison. */
|
|
110
|
-
bundle: Bundle;
|
|
110
|
+
bundle: Bundle$1;
|
|
111
111
|
/** Which tier applied: "text" (strings-only, nothing restarted) or "structure" (full hot swap). */
|
|
112
112
|
kind: "text" | "structure";
|
|
113
113
|
}
|
|
@@ -117,7 +117,7 @@ interface LiveBundleResult {
|
|
|
117
117
|
* unparseable JSON - a structural swap's edge cases are absorbed by the §9.8 drift policy inside
|
|
118
118
|
* `hotSwap`, which never throws for ordinary edits.
|
|
119
119
|
*/
|
|
120
|
-
declare function applyLiveBundle(engine: Engine, current: Bundle, data: string): LiveBundleResult;
|
|
120
|
+
declare function applyLiveBundle(engine: Engine, current: Bundle$1, data: string): LiveBundleResult;
|
|
121
121
|
|
|
122
122
|
interface PropertyInspectorOptions {
|
|
123
123
|
/** Where to mount the panel. If omitted, append the returned `el` yourself. */
|
|
@@ -137,6 +137,341 @@ interface PropertyInspector {
|
|
|
137
137
|
}
|
|
138
138
|
declare function createPropertyInspector(engine: Engine, opts?: PropertyInspectorOptions): PropertyInspector;
|
|
139
139
|
|
|
140
|
+
type BinaryOp = "==" | "!=" | ">" | ">=" | "<" | "<=" | "+" | "-" | "*" | "/" | "and" | "or";
|
|
141
|
+
type UnaryOp = "not" | "neg";
|
|
142
|
+
type AstNode = ["b", boolean] | ["n", number] | ["s", string] | ["sv", string, string] | ["u", UnaryOp, AstNode] | ["bin", BinaryOp, AstNode, AstNode] | ["call", string, ...AstNode[]] | ["fd", "+" | "-", string];
|
|
143
|
+
|
|
144
|
+
type ScalarValue = boolean | number | string | string[];
|
|
145
|
+
/** Developer-defined host metadata (spec §17). Opaque to Patter. */
|
|
146
|
+
type GameData = Record<string, unknown>;
|
|
147
|
+
/** A scene/block id, or the reserved "END". */
|
|
148
|
+
type JumpTarget = string;
|
|
149
|
+
interface Jump {
|
|
150
|
+
to: JumpTarget;
|
|
151
|
+
/** "jump" (one-way, default) or "call" (jump-and-return via the flow callstack). */
|
|
152
|
+
mode?: "jump" | "call";
|
|
153
|
+
}
|
|
154
|
+
interface LineBeat {
|
|
155
|
+
id: string;
|
|
156
|
+
kind: "line";
|
|
157
|
+
/** Speaker; must be a member of the project cast (validated). */
|
|
158
|
+
character?: string;
|
|
159
|
+
/** Performance direction, language-neutral (never localised). */
|
|
160
|
+
direction?: string;
|
|
161
|
+
gameData?: GameData;
|
|
162
|
+
/** Author-defined freeform tags (#215): a cross-cutting label layer that travels to the runtime.
|
|
163
|
+
* At runtime a beat's tags are the UNION of its own and every ancestor's (scene → block → group(s) →
|
|
164
|
+
* snippet → beat). Each tag is letters/digits/symbols with NO comma and NO whitespace; deduped. */
|
|
165
|
+
tags?: string[];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Authorial voice / narration - speaker-less prose (spec §2). Never voiced; its
|
|
169
|
+
* localised text always permits inline `{@name}` interpolation (spec §16). This
|
|
170
|
+
* is the on-screen-text role (e.g. "A door slams!") - distinct from a game-event
|
|
171
|
+
* beat, which is a pure engine instruction with no localised content.
|
|
172
|
+
*/
|
|
173
|
+
interface TextBeat {
|
|
174
|
+
id: string;
|
|
175
|
+
kind: "text";
|
|
176
|
+
gameData?: GameData;
|
|
177
|
+
/** Author tags (#215). See LineBeat.tags. */
|
|
178
|
+
tags?: string[];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A GAME EVENT (spec §2): a pure engine instruction with no player-facing text - just `gameData` the host
|
|
182
|
+
* reads when the beat plays (comments / docs attach via the authoring file by id). Named "game event"
|
|
183
|
+
* rather than "action" because a screenplay's "action" is prose, which is our text beat. Player-facing
|
|
184
|
+
* words are a line or text beat instead.
|
|
185
|
+
*/
|
|
186
|
+
interface GameEventBeat {
|
|
187
|
+
id: string;
|
|
188
|
+
kind: "gameEvent";
|
|
189
|
+
gameData?: GameData;
|
|
190
|
+
/** Author tags (#215). See LineBeat.tags. */
|
|
191
|
+
tags?: string[];
|
|
192
|
+
}
|
|
193
|
+
type Beat = LineBeat | TextBeat | GameEventBeat;
|
|
194
|
+
type Selector = "run" | "branch" | "sequence" | "choice";
|
|
195
|
+
/** How a `sequence` walks its children. `specificity` = **Best match**: pick the eligible
|
|
196
|
+
* child whose condition most specifically fits the current state (the most atomic constraints
|
|
197
|
+
* actively holding it true); equally-specific ties break by the seeded shuffle. A child with no
|
|
198
|
+
* condition scores zero, so it acts as the filler that wins only when nothing more specific is
|
|
199
|
+
* eligible. Composes with `exhaust`: `repeat` re-scores every visit (re-pickable, the Best-match
|
|
200
|
+
* default), `once` uses each pick up so the group slides down to the filler (graceful degradation). */
|
|
201
|
+
type SelectorOrder = "sequential" | "shuffle" | "specificity";
|
|
202
|
+
/** What a `sequence` does after one full pass through its children. */
|
|
203
|
+
type SelectorExhaust = "once" | "repeat" | "stick";
|
|
204
|
+
/**
|
|
205
|
+
* `sequence` selector config (spec §4). One stateful picker with two orthogonal
|
|
206
|
+
* axes subsumes Ink's stopping / cycle / once / shuffle and their combinations.
|
|
207
|
+
* Defaults: `order: "sequential"`, `exhaust: "once"`. `shuffle` draws without
|
|
208
|
+
* replacement and never repeats a line back-to-back (built in).
|
|
209
|
+
*/
|
|
210
|
+
interface SequenceOptions {
|
|
211
|
+
order?: SelectorOrder;
|
|
212
|
+
exhaust?: SelectorExhaust;
|
|
213
|
+
}
|
|
214
|
+
/** An option's prompt beat (spec §5): a single line | text beat - the choice text. */
|
|
215
|
+
type PromptBeat = LineBeat | TextBeat;
|
|
216
|
+
type PropertyType = "boolean" | "number" | "string" | "flags" | "enum";
|
|
217
|
+
interface PropertyDecl {
|
|
218
|
+
name: string;
|
|
219
|
+
type: PropertyType;
|
|
220
|
+
default?: ScalarValue;
|
|
221
|
+
/**
|
|
222
|
+
* The orthogonal *sharing* axis (spec §7): is this property's value shared across
|
|
223
|
+
* all flows (one world value) or kept per-flow? It does NOT change the reference
|
|
224
|
+
* syntax - sharing is set here, on the declaration, not by a different scope token.
|
|
225
|
+
* The default depends on the scope it is declared in: a **global** property
|
|
226
|
+
* (project `properties` -> `@patter`) defaults to **shared**; a **scene-local**
|
|
227
|
+
* property (scene `sceneProps` -> `@scene`) defaults to **not shared** (per-flow).
|
|
228
|
+
*/
|
|
229
|
+
shared?: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Persistence axis, for **scene-local (`@scene`) properties** (spec §7). Default
|
|
232
|
+
* `false`: the value PERSISTS across scene re-entries (like every other property).
|
|
233
|
+
* `true`: the value is **reseeded to its default on every scene entry** - "fresh
|
|
234
|
+
* each playthrough" (Ink's `temp`). Orthogonal to `shared`. Ignored on global
|
|
235
|
+
* (`@patter`) properties, which always persist for the life of the piece.
|
|
236
|
+
*/
|
|
237
|
+
temporary?: boolean;
|
|
238
|
+
/** For enum / flags. */
|
|
239
|
+
values?: string[];
|
|
240
|
+
/** Free-text author note documenting what this property is for (authoring only; shown as a hint). */
|
|
241
|
+
purpose?: string;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A property of a host / world scope (`@world`, `@game`, ...). The same shape the
|
|
245
|
+
* `@wildwinter/scoperegistry` `scopeRegistrySpec` uses, declared structurally here
|
|
246
|
+
* so the model stays free of a runtime dependency. `default` seeds the standalone
|
|
247
|
+
* runtime's self-backed bag (see `HostScopeSpec`); `writable: false` makes the
|
|
248
|
+
* property read-only to the story (validated at compile time).
|
|
249
|
+
*/
|
|
250
|
+
interface HostScopeDecl {
|
|
251
|
+
name: string;
|
|
252
|
+
type: PropertyType;
|
|
253
|
+
values?: string[];
|
|
254
|
+
default?: ScalarValue;
|
|
255
|
+
writable?: boolean;
|
|
256
|
+
/** Free-text author note documenting the property (authoring only; shown as a hint). */
|
|
257
|
+
purpose?: string;
|
|
258
|
+
}
|
|
259
|
+
/** One scope (`token`) in a project's host-scope registry. */
|
|
260
|
+
interface HostScopeSpec {
|
|
261
|
+
/** The scope token after `@` (e.g. `"world"`). Must not collide with Patter's own
|
|
262
|
+
* `patter` / `scene` / `flow`. */
|
|
263
|
+
token: string;
|
|
264
|
+
/** Scope-level read/write default for its declarations (default true). */
|
|
265
|
+
writable?: boolean;
|
|
266
|
+
/** Property declarations; omit for an opaque scope (any name, unchecked). */
|
|
267
|
+
declarations?: HostScopeDecl[];
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A project's host-scope registry: the `scopeRegistrySpec` it OWNS (spec
|
|
271
|
+
* design/scope-registry.md §6). Makes `@world` (and any host scope) first-class
|
|
272
|
+
* in a standalone project: the compiler validates references into it, the runtime
|
|
273
|
+
* self-backs it from declaration defaults when no host resolver claims the token,
|
|
274
|
+
* and coverage drives its values. Structurally identical to scoperegistry's
|
|
275
|
+
* `ScopeRegistrySpec` so it threads straight through the compiler.
|
|
276
|
+
*/
|
|
277
|
+
interface HostScopeRegistry {
|
|
278
|
+
version: number;
|
|
279
|
+
scopes: HostScopeSpec[];
|
|
280
|
+
}
|
|
281
|
+
/** A character's grammatical gender, for localisation. Translators need it to inflect the speaker's own
|
|
282
|
+
* lines in gendered languages (adjectives, participles, pronouns), which the source text alone often
|
|
283
|
+
* cannot tell them. Absent means "not specified". Authoring-only: it never reaches the runtime bundle,
|
|
284
|
+
* but it IS carried into the localisation handoff formats as translator context (spec §14).
|
|
285
|
+
*
|
|
286
|
+
* Free text, not a closed set: real languages need more than three genders (common/utrum, animate/
|
|
287
|
+
* inanimate, and so on) and translators name them differently. `COMMON_GENDERS` seeds the editor's
|
|
288
|
+
* auto-suggest so the everyday values stay spelled consistently; anything else is still valid. */
|
|
289
|
+
type GrammaticalGender = string;
|
|
290
|
+
interface CastMember {
|
|
291
|
+
/** Canonical speaker name (matched by a beat's `character`); language-neutral key. */
|
|
292
|
+
name: string;
|
|
293
|
+
/** Localised player-facing name - a localisation id (project-level strings). */
|
|
294
|
+
displayName?: string;
|
|
295
|
+
/** Grammatical gender for translators (see `GrammaticalGender`); absent = not specified. */
|
|
296
|
+
gender?: GrammaticalGender;
|
|
297
|
+
/** Free-text production notes about the character (casting, voice, intent) - authoring only. */
|
|
298
|
+
notes?: string;
|
|
299
|
+
/** The voice actor cast for this character, if known - surfaced in the VO script export (spec §16).
|
|
300
|
+
* Authoring-only (not shipped in the runtime bundle). */
|
|
301
|
+
actor?: string;
|
|
302
|
+
gameData?: GameData;
|
|
303
|
+
}
|
|
304
|
+
/** The cast as it reaches a compiled bundle: the player-facing fields only. `notes`, `actor` and
|
|
305
|
+
* `gender` are authoring / production / translation context, and the compiler drops them, so a shipped
|
|
306
|
+
* game never carries a real person's name or a writer's private notes. The compiler copies the shipping
|
|
307
|
+
* fields across explicitly (an allow-list), which is what actually keeps a new authoring field out of
|
|
308
|
+
* the bundle; this type states the resulting contract for anyone reading a bundle. */
|
|
309
|
+
type BundleCastMember = Omit<CastMember, "notes" | "actor" | "gender">;
|
|
310
|
+
/** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
|
|
311
|
+
* Beat kinds map straight through: dialogue = `line`, narration = `text`, game event = `gameEvent`. */
|
|
312
|
+
type GameDataNodeKind = "scene" | "block" | "snippet" | "line" | "text" | "gameEvent";
|
|
313
|
+
/** The value type of a gameData field - the property-type vocabulary plus a multiline-text variant.
|
|
314
|
+
* Drives the inspector's editor widget (text / textarea / number / toggle / enum dropdown). */
|
|
315
|
+
type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum";
|
|
316
|
+
/** One author-defined custom field on a node type (host-integration metadata, NOT expression state). */
|
|
317
|
+
interface GameDataField {
|
|
318
|
+
/** Field key - also the key a node stores its value under in `gameData` (values are name-keyed). */
|
|
319
|
+
name: string;
|
|
320
|
+
type: GameDataFieldType;
|
|
321
|
+
/** The value used when a node sets nothing. Storage is SPARSE - nodes hold only their overrides, and
|
|
322
|
+
* a reader falls back to this default (so changing it here propagates to every node that didn't set it). */
|
|
323
|
+
default?: ScalarValue;
|
|
324
|
+
/** Allowed values when `type` is "enum". */
|
|
325
|
+
values?: string[];
|
|
326
|
+
/** Free-text description of what the field is for - shown as a rollover hint in the inspector. */
|
|
327
|
+
purpose?: string;
|
|
328
|
+
}
|
|
329
|
+
/** Author-defined gameData fields, grouped by the node type they attach to (project-level schema). */
|
|
330
|
+
type GameDataFields = Partial<Record<GameDataNodeKind, GameDataField[]>>;
|
|
331
|
+
/** A compiled expression envelope: canonical source + pre-derived tagged-tuple AST. */
|
|
332
|
+
interface Expression {
|
|
333
|
+
src: string;
|
|
334
|
+
ast: AstNode;
|
|
335
|
+
}
|
|
336
|
+
type CompiledEffect = {
|
|
337
|
+
kind: "set";
|
|
338
|
+
target: string;
|
|
339
|
+
value: Expression;
|
|
340
|
+
};
|
|
341
|
+
interface CompiledSnippet {
|
|
342
|
+
id: string;
|
|
343
|
+
type: "snippet";
|
|
344
|
+
condition?: Expression;
|
|
345
|
+
beats?: Beat[];
|
|
346
|
+
onEnter?: CompiledEffect[];
|
|
347
|
+
onExit?: CompiledEffect[];
|
|
348
|
+
gameData?: GameData;
|
|
349
|
+
tags?: string[];
|
|
350
|
+
jump?: Jump;
|
|
351
|
+
secretUntilEligible?: boolean;
|
|
352
|
+
/** Option-position: repeatable (spec §5). Default false = once-only. */
|
|
353
|
+
sticky?: boolean;
|
|
354
|
+
/** Option-position: the choice's fallback, auto-followed when last (spec §5). */
|
|
355
|
+
fallback?: boolean;
|
|
356
|
+
}
|
|
357
|
+
interface CompiledGroup {
|
|
358
|
+
id: string;
|
|
359
|
+
type: "group";
|
|
360
|
+
condition?: Expression;
|
|
361
|
+
/** Default (omitted) = `"run"`. */
|
|
362
|
+
selector?: Selector;
|
|
363
|
+
/** Selector cursor shared across flows (default false = per-flow). */
|
|
364
|
+
shared?: boolean;
|
|
365
|
+
options?: SequenceOptions;
|
|
366
|
+
children: Array<CompiledGroup | CompiledSnippet>;
|
|
367
|
+
gameData?: GameData;
|
|
368
|
+
tags?: string[];
|
|
369
|
+
/** Option-position fields (spec §5) - only when a direct child of a `choice`. */
|
|
370
|
+
prompt?: PromptBeat;
|
|
371
|
+
secretUntilEligible?: boolean;
|
|
372
|
+
/** Repeatable (spec §5). Default false = once-only. */
|
|
373
|
+
sticky?: boolean;
|
|
374
|
+
/** The choice's fallback, auto-followed when last (spec §5). */
|
|
375
|
+
fallback?: boolean;
|
|
376
|
+
}
|
|
377
|
+
interface CompiledBlock {
|
|
378
|
+
id: string;
|
|
379
|
+
type: "block";
|
|
380
|
+
name: string;
|
|
381
|
+
/** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
|
|
382
|
+
gameId?: string;
|
|
383
|
+
children: Array<CompiledGroup | CompiledSnippet>;
|
|
384
|
+
gameData?: GameData;
|
|
385
|
+
tags?: string[];
|
|
386
|
+
}
|
|
387
|
+
interface CompiledScene {
|
|
388
|
+
id: string;
|
|
389
|
+
type: "scene";
|
|
390
|
+
name: string;
|
|
391
|
+
/** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
|
|
392
|
+
gameId?: string;
|
|
393
|
+
gameData?: GameData;
|
|
394
|
+
tags?: string[];
|
|
395
|
+
onEntry?: CompiledEffect[];
|
|
396
|
+
sceneProps?: PropertyDecl[];
|
|
397
|
+
blocks: CompiledBlock[];
|
|
398
|
+
}
|
|
399
|
+
interface Bundle {
|
|
400
|
+
schema: string;
|
|
401
|
+
/** `hash` fingerprints the WHOLE bundle (binds saves, gates staleness); `structureHash` is the same
|
|
402
|
+
* fingerprint with the string tables left out, so same structureHash + a different hash = a
|
|
403
|
+
* text-only edit, safe to hot-swap in place (live bundle refresh). */
|
|
404
|
+
content: {
|
|
405
|
+
project: string;
|
|
406
|
+
version?: string;
|
|
407
|
+
hash?: string;
|
|
408
|
+
structureHash?: string;
|
|
409
|
+
};
|
|
410
|
+
voiced: boolean;
|
|
411
|
+
locales: {
|
|
412
|
+
default: string;
|
|
413
|
+
included: string[];
|
|
414
|
+
};
|
|
415
|
+
/** Player-facing cast only: the compiler strips notes / actor / gender (see `BundleCastMember`). */
|
|
416
|
+
cast?: BundleCastMember[];
|
|
417
|
+
properties?: PropertyDecl[];
|
|
418
|
+
/** Host / world scope declarations, baked from the project so the runtime can self-back a declared
|
|
419
|
+
* scope (`@world`, ...) when no host resolver claims its token. Absent = no host scopes. */
|
|
420
|
+
scopeRegistry?: HostScopeRegistry;
|
|
421
|
+
gameDataFields?: GameDataFields;
|
|
422
|
+
scenes: Record<string, CompiledScene>;
|
|
423
|
+
/** locale -> (beatId -> text). In "embedded" localisation this carries every included locale; in "ids"
|
|
424
|
+
* it is EMPTY (the runtime emits beat IDs), unless `localisation.sourceDebug` embedded the source locale
|
|
425
|
+
* for debug playback. `content.hash` is computed over the FULL strings regardless, so the staleness gate
|
|
426
|
+
* is unaffected. */
|
|
427
|
+
strings: Record<string, Record<string, string>>;
|
|
428
|
+
/** How strings ship + resolve (spec §11). Absent = "embedded" (back-compat default): the runtime resolves
|
|
429
|
+
* `strings` per locale. "ids": the runtime emits beat IDs and the game localises them itself; `sourceDebug`
|
|
430
|
+
* means the source locale is embedded purely for debug playback and the runtime should flag the build as
|
|
431
|
+
* not shippable. */
|
|
432
|
+
localisation?: {
|
|
433
|
+
mode: "embedded" | "ids";
|
|
434
|
+
sourceDebug?: boolean;
|
|
435
|
+
};
|
|
436
|
+
/** Closed-caption delimiters baked from the project (#214). Absent = the default `(` / `)`; the
|
|
437
|
+
* runtime strips spans between them from line text when a game disables captions. */
|
|
438
|
+
closedCaptions?: CaptionDelimiters;
|
|
439
|
+
}
|
|
440
|
+
/** Closed-caption configuration (#214). `open`/`close` wrap a caption cue inside a dialogue line (both
|
|
441
|
+
* non-empty; they MAY be the same token, e.g. `*…*`). `character` names a cast member whose lines are a
|
|
442
|
+
* pure caption: when captions are off, ALL of that character's dialogue (and its speaker label) is
|
|
443
|
+
* omitted - delimiters or not - leaving a silent line that still fires (so audio plays). Absent / empty
|
|
444
|
+
* `character` resolves to the default `SFX` (you "disable" it simply by never using that speaker). */
|
|
445
|
+
interface CaptionDelimiters {
|
|
446
|
+
open: string;
|
|
447
|
+
close: string;
|
|
448
|
+
character?: string;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
interface BundleInspectorOptions {
|
|
452
|
+
/** Where to mount the panel. If omitted, append the returned `el` yourself. */
|
|
453
|
+
container?: HTMLElement;
|
|
454
|
+
/** Panel heading. Default "Bundle". */
|
|
455
|
+
title?: string;
|
|
456
|
+
/** Sections open on mount. Default: identity and addresses, the two an
|
|
457
|
+
* integrator reaches for first; the rest are a click away. */
|
|
458
|
+
open?: BundleSection[];
|
|
459
|
+
}
|
|
460
|
+
type BundleSection = "identity" | "addresses" | "hostScopes" | "properties" | "gameData" | "counts";
|
|
461
|
+
interface BundleInspector {
|
|
462
|
+
/** The panel root (already inside `container` if you passed one). */
|
|
463
|
+
readonly el: HTMLElement;
|
|
464
|
+
/** The description the panel is showing, for a caller that wants the data too. */
|
|
465
|
+
readonly description: BundleDescription;
|
|
466
|
+
/** Remove the panel from the DOM. */
|
|
467
|
+
destroy(): void;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Build a read-only panel describing a compiled bundle: what it is, what game
|
|
471
|
+
* code may call on it, and what the host must supply.
|
|
472
|
+
*/
|
|
473
|
+
declare function createBundleInspector(bundle: Bundle, opts?: BundleInspectorOptions): BundleInspector;
|
|
474
|
+
|
|
140
475
|
interface AudioResolver {
|
|
141
476
|
/** The full path/URL of a beat's winning audio take, or null when it has none. */
|
|
142
477
|
resolve(beatId: string): string | null;
|
|
@@ -148,4 +483,4 @@ interface AudioResolver {
|
|
|
148
483
|
*/
|
|
149
484
|
declare function createAudioResolver(manifestJson: string, basePath: string): AudioResolver;
|
|
150
485
|
|
|
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 };
|
|
486
|
+
export { type AudioResolver, type BundleInspector, type BundleInspectorOptions, type BundleSection, 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, createBundleInspector, createDebugLink, createPropertyInspector, createStateLogger, deserializeState, diffState, getProperty, loadState, saveState, serializeState, setProperties, setProperty, snapshotState };
|
package/dist/index.js
CHANGED
|
@@ -223,8 +223,8 @@ function sameValue(a, b) {
|
|
|
223
223
|
function createPropertyInspector(engine, opts = {}) {
|
|
224
224
|
const doc = opts.container?.ownerDocument ?? document;
|
|
225
225
|
injectStyle(doc);
|
|
226
|
-
const
|
|
227
|
-
|
|
226
|
+
const el2 = doc.createElement("div");
|
|
227
|
+
el2.className = "pp-insp";
|
|
228
228
|
const heading = doc.createElement("h4");
|
|
229
229
|
heading.textContent = opts.title ?? "Runtime state";
|
|
230
230
|
const list = doc.createElement("div");
|
|
@@ -263,15 +263,15 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
263
263
|
input.click();
|
|
264
264
|
});
|
|
265
265
|
io.append(saveBtn, loadBtn);
|
|
266
|
-
|
|
266
|
+
el2.append(heading, io, list);
|
|
267
267
|
const rowRefreshers = [];
|
|
268
|
-
const buildRow = (
|
|
268
|
+
const buildRow = (row2) => {
|
|
269
269
|
const r = doc.createElement("div");
|
|
270
270
|
r.className = "pp-insp-row";
|
|
271
271
|
const label = doc.createElement("span");
|
|
272
272
|
label.className = "pp-insp-ref";
|
|
273
|
-
label.textContent =
|
|
274
|
-
label.title =
|
|
273
|
+
label.textContent = row2.ref;
|
|
274
|
+
label.title = row2.ref;
|
|
275
275
|
const ctl = doc.createElement("div");
|
|
276
276
|
ctl.className = "pp-insp-ctl";
|
|
277
277
|
const reset = doc.createElement("button");
|
|
@@ -283,37 +283,37 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
283
283
|
list.appendChild(r);
|
|
284
284
|
let read;
|
|
285
285
|
const commit = (v) => {
|
|
286
|
-
engine.setProperty(
|
|
286
|
+
engine.setProperty(row2.ref, v);
|
|
287
287
|
syncReset();
|
|
288
288
|
};
|
|
289
289
|
const focused = (node) => doc.activeElement === node;
|
|
290
290
|
function syncReset() {
|
|
291
|
-
reset.disabled = sameValue(engine.getProperty(
|
|
291
|
+
reset.disabled = sameValue(engine.getProperty(row2.ref), row2.default);
|
|
292
292
|
}
|
|
293
293
|
reset.addEventListener("click", () => {
|
|
294
|
-
engine.setProperty(
|
|
294
|
+
engine.setProperty(row2.ref, row2.default);
|
|
295
295
|
read();
|
|
296
296
|
syncReset();
|
|
297
297
|
});
|
|
298
|
-
if (
|
|
298
|
+
if (row2.type === "boolean") {
|
|
299
299
|
const cb = doc.createElement("input");
|
|
300
300
|
cb.type = "checkbox";
|
|
301
301
|
cb.addEventListener("change", () => commit(cb.checked));
|
|
302
302
|
ctl.appendChild(cb);
|
|
303
303
|
read = () => {
|
|
304
|
-
if (!focused(cb)) cb.checked = engine.getProperty(
|
|
304
|
+
if (!focused(cb)) cb.checked = engine.getProperty(row2.ref) === true;
|
|
305
305
|
};
|
|
306
|
-
} else if (
|
|
306
|
+
} else if (row2.type === "number") {
|
|
307
307
|
const inp = doc.createElement("input");
|
|
308
308
|
inp.type = "number";
|
|
309
309
|
inp.addEventListener("change", () => commit(Number(inp.value)));
|
|
310
310
|
ctl.appendChild(inp);
|
|
311
311
|
read = () => {
|
|
312
|
-
if (!focused(inp)) inp.value = String(engine.getProperty(
|
|
312
|
+
if (!focused(inp)) inp.value = String(engine.getProperty(row2.ref) ?? "");
|
|
313
313
|
};
|
|
314
|
-
} else if (
|
|
314
|
+
} else if (row2.type === "enum") {
|
|
315
315
|
const sel = doc.createElement("select");
|
|
316
|
-
for (const v of
|
|
316
|
+
for (const v of row2.values ?? []) {
|
|
317
317
|
const o = doc.createElement("option");
|
|
318
318
|
o.value = v;
|
|
319
319
|
o.textContent = v;
|
|
@@ -322,9 +322,9 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
322
322
|
sel.addEventListener("change", () => commit(sel.value));
|
|
323
323
|
ctl.appendChild(sel);
|
|
324
324
|
read = () => {
|
|
325
|
-
if (!focused(sel)) sel.value = String(engine.getProperty(
|
|
325
|
+
if (!focused(sel)) sel.value = String(engine.getProperty(row2.ref) ?? "");
|
|
326
326
|
};
|
|
327
|
-
} else if (
|
|
327
|
+
} else if (row2.type === "flags") {
|
|
328
328
|
const inp = doc.createElement("input");
|
|
329
329
|
inp.type = "text";
|
|
330
330
|
inp.placeholder = "comma, separated, flags";
|
|
@@ -332,7 +332,7 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
332
332
|
ctl.appendChild(inp);
|
|
333
333
|
read = () => {
|
|
334
334
|
if (!focused(inp)) {
|
|
335
|
-
const v = engine.getProperty(
|
|
335
|
+
const v = engine.getProperty(row2.ref);
|
|
336
336
|
inp.value = Array.isArray(v) ? v.join(", ") : "";
|
|
337
337
|
}
|
|
338
338
|
};
|
|
@@ -342,7 +342,7 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
342
342
|
inp.addEventListener("change", () => commit(inp.value));
|
|
343
343
|
ctl.appendChild(inp);
|
|
344
344
|
read = () => {
|
|
345
|
-
if (!focused(inp)) inp.value = String(engine.getProperty(
|
|
345
|
+
if (!focused(inp)) inp.value = String(engine.getProperty(row2.ref) ?? "");
|
|
346
346
|
};
|
|
347
347
|
}
|
|
348
348
|
read();
|
|
@@ -359,21 +359,165 @@ function createPropertyInspector(engine, opts = {}) {
|
|
|
359
359
|
empty.textContent = "No @patter properties.";
|
|
360
360
|
list.appendChild(empty);
|
|
361
361
|
} else {
|
|
362
|
-
for (const
|
|
362
|
+
for (const row2 of props) buildRow(row2);
|
|
363
363
|
}
|
|
364
364
|
const refresh = () => {
|
|
365
365
|
for (const fn of rowRefreshers) fn();
|
|
366
366
|
};
|
|
367
|
-
opts.container?.appendChild(
|
|
367
|
+
opts.container?.appendChild(el2);
|
|
368
368
|
const pollMs = opts.pollMs ?? 250;
|
|
369
369
|
let timer;
|
|
370
370
|
if (pollMs > 0) timer = setInterval(refresh, pollMs);
|
|
371
371
|
return {
|
|
372
|
-
el,
|
|
372
|
+
el: el2,
|
|
373
373
|
refresh,
|
|
374
374
|
destroy() {
|
|
375
375
|
if (timer !== void 0) clearInterval(timer);
|
|
376
|
-
|
|
376
|
+
el2.remove();
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/bundle-inspector.ts
|
|
382
|
+
import { describeBundle } from "@patterkit/runtime";
|
|
383
|
+
var STYLE_ID2 = "pp-bundle-style";
|
|
384
|
+
var CSS2 = `
|
|
385
|
+
.pp-bundle{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:26rem;box-shadow:0 6px 20px rgba(21,32,30,.12)}
|
|
386
|
+
.pp-bundle h4{margin:0 0 .4rem;font:600 .72rem/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:#5c6b62}
|
|
387
|
+
.pp-bundle details{border-top:1px solid #e3dccf;padding:.3rem 0}
|
|
388
|
+
.pp-bundle details:first-of-type{border-top:0}
|
|
389
|
+
.pp-bundle summary{cursor:pointer;font-weight:600;color:#214f4b;list-style:none}
|
|
390
|
+
.pp-bundle summary::-webkit-details-marker{display:none}
|
|
391
|
+
.pp-bundle summary::before{content:"\\25B8";display:inline-block;width:1rem;color:#8a9691}
|
|
392
|
+
.pp-bundle details[open]>summary::before{content:"\\25BE"}
|
|
393
|
+
.pp-bundle-count{color:#8a9691;font-weight:400}
|
|
394
|
+
.pp-bundle-row{display:flex;gap:.4rem;margin:.16rem 0 .16rem 1rem}
|
|
395
|
+
.pp-bundle-key{flex:0 0 8rem;color:#5c6b62}
|
|
396
|
+
.pp-bundle-val,.pp-bundle-addr{font-family:ui-monospace,monospace;font-size:.78rem;color:#214f4b;word-break:break-all}
|
|
397
|
+
.pp-bundle-sub{margin-left:2rem}
|
|
398
|
+
.pp-bundle-empty{color:#8a9691;font-style:italic;margin-left:1rem}
|
|
399
|
+
.pp-bundle-warn{color:#8a3a2f;font-weight:600}
|
|
400
|
+
.pp-bundle-tag{font-size:.7rem;color:#5c6b62;border:1px solid #cfc7b8;border-radius:5px;padding:0 .25rem;margin-left:.3rem}
|
|
401
|
+
`;
|
|
402
|
+
function injectStyle2(doc) {
|
|
403
|
+
if (doc.getElementById(STYLE_ID2)) return;
|
|
404
|
+
const s = doc.createElement("style");
|
|
405
|
+
s.id = STYLE_ID2;
|
|
406
|
+
s.textContent = CSS2;
|
|
407
|
+
(doc.head ?? doc.documentElement).appendChild(s);
|
|
408
|
+
}
|
|
409
|
+
var el = (doc, tag, cls, text) => {
|
|
410
|
+
const e = doc.createElement(tag);
|
|
411
|
+
if (cls) e.className = cls;
|
|
412
|
+
if (text !== void 0) e.textContent = text;
|
|
413
|
+
return e;
|
|
414
|
+
};
|
|
415
|
+
function row(doc, key, value, valueClass = "pp-bundle-val") {
|
|
416
|
+
const r = el(doc, "div", "pp-bundle-row");
|
|
417
|
+
r.append(el(doc, "span", "pp-bundle-key", key), el(doc, "span", valueClass, value));
|
|
418
|
+
return r;
|
|
419
|
+
}
|
|
420
|
+
function propertyRow(doc, p) {
|
|
421
|
+
const r = el(doc, "div", "pp-bundle-row");
|
|
422
|
+
r.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", p.name));
|
|
423
|
+
const meta = el(doc, "span", "pp-bundle-val", p.type);
|
|
424
|
+
if (!p.hasDefault) meta.append(el(doc, "span", "pp-bundle-tag", "no default"));
|
|
425
|
+
r.append(meta);
|
|
426
|
+
return r;
|
|
427
|
+
}
|
|
428
|
+
function section(doc, name, label, count, open) {
|
|
429
|
+
const d = doc.createElement("details");
|
|
430
|
+
d.dataset["section"] = name;
|
|
431
|
+
d.open = open;
|
|
432
|
+
const s = doc.createElement("summary");
|
|
433
|
+
s.textContent = label;
|
|
434
|
+
if (count !== null) s.append(el(doc, "span", "pp-bundle-count", ` ${count}`));
|
|
435
|
+
d.append(s);
|
|
436
|
+
return d;
|
|
437
|
+
}
|
|
438
|
+
function createBundleInspector(bundle, opts = {}) {
|
|
439
|
+
const doc = opts.container?.ownerDocument ?? document;
|
|
440
|
+
injectStyle2(doc);
|
|
441
|
+
const d = describeBundle(bundle);
|
|
442
|
+
const open = new Set(opts.open ?? ["identity", "addresses"]);
|
|
443
|
+
const root = el(doc, "div", "pp-bundle");
|
|
444
|
+
root.append(el(doc, "h4", void 0, opts.title ?? "Bundle"));
|
|
445
|
+
const id = section(doc, "identity", d.identity.project || "(unnamed project)", null, open.has("identity"));
|
|
446
|
+
if (d.identity.version) id.append(row(doc, "version", d.identity.version));
|
|
447
|
+
id.append(row(doc, "schema", d.identity.schema));
|
|
448
|
+
id.append(row(doc, "locales", `${d.identity.defaultLocale}${d.identity.locales.length > 1 ? ` (+${d.identity.locales.length - 1})` : ""}`));
|
|
449
|
+
id.append(row(doc, "strings", d.identity.localisation));
|
|
450
|
+
if (d.identity.voiced) id.append(row(doc, "voiced", "yes"));
|
|
451
|
+
if (d.identity.hash) id.append(row(doc, "hash", d.identity.hash));
|
|
452
|
+
if (d.identity.structureHash) id.append(row(doc, "structure", d.identity.structureHash));
|
|
453
|
+
if (d.identity.sourceDebug) {
|
|
454
|
+
id.append(row(doc, "build", "SOURCE DEBUG - not shippable", "pp-bundle-val pp-bundle-warn"));
|
|
455
|
+
}
|
|
456
|
+
root.append(id);
|
|
457
|
+
const addr = section(doc, "addresses", "Addresses", d.addresses.length, open.has("addresses"));
|
|
458
|
+
if (!d.addresses.length) addr.append(el(doc, "div", "pp-bundle-empty", "no scenes"));
|
|
459
|
+
for (const a of d.addresses) {
|
|
460
|
+
const r = el(doc, "div", "pp-bundle-row");
|
|
461
|
+
r.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", a.gameId), el(doc, "span", "pp-bundle-val", a.name));
|
|
462
|
+
addr.append(r);
|
|
463
|
+
for (const b of a.blocks) {
|
|
464
|
+
const br = el(doc, "div", "pp-bundle-row pp-bundle-sub");
|
|
465
|
+
br.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", b.gameId), el(doc, "span", "pp-bundle-val", b.name));
|
|
466
|
+
addr.append(br);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
root.append(addr);
|
|
470
|
+
const hostCount = d.hostScopes.reduce((n, s) => n + s.properties.length, 0);
|
|
471
|
+
const host = section(doc, "hostScopes", "Host properties", hostCount, open.has("hostScopes"));
|
|
472
|
+
if (!d.hostScopes.length) host.append(el(doc, "div", "pp-bundle-empty", "the game supplies nothing"));
|
|
473
|
+
for (const s of d.hostScopes) {
|
|
474
|
+
const head = el(doc, "div", "pp-bundle-row");
|
|
475
|
+
head.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", `@${s.token}`));
|
|
476
|
+
const meta = el(doc, "span", "pp-bundle-val", s.opaque ? "any name, unchecked" : `${s.properties.length} declared`);
|
|
477
|
+
if (!s.writable) meta.append(el(doc, "span", "pp-bundle-tag", "read-only"));
|
|
478
|
+
head.append(meta);
|
|
479
|
+
host.append(head);
|
|
480
|
+
for (const p of s.properties) {
|
|
481
|
+
const pr = propertyRow(doc, p);
|
|
482
|
+
pr.classList.add("pp-bundle-sub");
|
|
483
|
+
host.append(pr);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
root.append(host);
|
|
487
|
+
const ownedCount = d.properties.patter.length + d.properties.scene.reduce((n, s) => n + s.properties.length, 0);
|
|
488
|
+
const owned = section(doc, "properties", "Story properties", ownedCount, open.has("properties"));
|
|
489
|
+
if (!ownedCount) owned.append(el(doc, "div", "pp-bundle-empty", "none declared"));
|
|
490
|
+
for (const p of d.properties.patter) owned.append(propertyRow(doc, p));
|
|
491
|
+
for (const s of d.properties.scene) {
|
|
492
|
+
owned.append(row(doc, `@scene`, s.gameId, "pp-bundle-val pp-bundle-addr"));
|
|
493
|
+
for (const p of s.properties) {
|
|
494
|
+
const pr = propertyRow(doc, p);
|
|
495
|
+
pr.classList.add("pp-bundle-sub");
|
|
496
|
+
owned.append(pr);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
root.append(owned);
|
|
500
|
+
if (d.gameData.length) {
|
|
501
|
+
const gd2 = section(doc, "gameData", "Game data", d.gameData.reduce((n, g) => n + g.fields.length, 0), open.has("gameData"));
|
|
502
|
+
for (const g of d.gameData) {
|
|
503
|
+
gd2.append(row(doc, "on", g.kind));
|
|
504
|
+
for (const f of g.fields) {
|
|
505
|
+
const fr = el(doc, "div", "pp-bundle-row pp-bundle-sub");
|
|
506
|
+
fr.append(el(doc, "span", "pp-bundle-key pp-bundle-addr", f.name), el(doc, "span", "pp-bundle-val", f.type));
|
|
507
|
+
gd2.append(fr);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
root.append(gd2);
|
|
511
|
+
}
|
|
512
|
+
const counts = section(doc, "counts", "Counts", null, open.has("counts"));
|
|
513
|
+
for (const [key, value] of Object.entries(d.counts)) counts.append(row(doc, key, String(value)));
|
|
514
|
+
root.append(counts);
|
|
515
|
+
opts.container?.append(root);
|
|
516
|
+
return {
|
|
517
|
+
el: root,
|
|
518
|
+
description: d,
|
|
519
|
+
destroy() {
|
|
520
|
+
root.remove();
|
|
377
521
|
}
|
|
378
522
|
};
|
|
379
523
|
}
|
|
@@ -395,6 +539,7 @@ export {
|
|
|
395
539
|
SAVE_SCHEMA,
|
|
396
540
|
applyLiveBundle,
|
|
397
541
|
createAudioResolver,
|
|
542
|
+
createBundleInspector,
|
|
398
543
|
createDebugLink,
|
|
399
544
|
createPropertyInspector,
|
|
400
545
|
createStateLogger,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patterkit/play-helpers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@patterkit/runtime": "0.4.
|
|
37
|
+
"@patterkit/runtime": "0.4.3"
|
|
38
38
|
}
|
|
39
39
|
}
|