@storylet-studio/runtime 0.8.1 → 0.9.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 +2 -2
- package/dist/index.cjs +248 -1045
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -20
- package/dist/index.d.ts +56 -20
- package/dist/index.js +183 -969
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/dist/index.cjs
CHANGED
|
@@ -23,944 +23,24 @@ __export(index_exports, {
|
|
|
23
23
|
Engine: () => Engine,
|
|
24
24
|
Flow: () => Flow,
|
|
25
25
|
describeBundle: () => describeBundle,
|
|
26
|
-
makePrng: () => makePrng,
|
|
27
|
-
shuffleInPlace: () => shuffleInPlace
|
|
26
|
+
makePrng: () => import_expr.makePrng,
|
|
27
|
+
shuffleInPlace: () => import_expr.shuffleInPlace
|
|
28
28
|
});
|
|
29
29
|
module.exports = __toCommonJS(index_exports);
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
case "s":
|
|
39
|
-
return { kind: "string", value: node[1] };
|
|
40
|
-
case "sv":
|
|
41
|
-
return { kind: "scopedvar", scope: node[1], name: node[2] };
|
|
42
|
-
case "u":
|
|
43
|
-
return { kind: "unary", op: node[1], operand: deserialiseAst(node[2]) };
|
|
44
|
-
case "bin":
|
|
45
|
-
return { kind: "binary", op: node[1], left: deserialiseAst(node[2]), right: deserialiseAst(node[3]) };
|
|
46
|
-
case "call": {
|
|
47
|
-
const args = node.slice(2).map(deserialiseAst);
|
|
48
|
-
return { kind: "call", name: node[1], args };
|
|
49
|
-
}
|
|
50
|
-
case "fd":
|
|
51
|
-
return { kind: "flagdelta", sign: node[1], name: node[2] };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ../../../expr/packages/expr/src/evaluate.ts
|
|
56
|
-
var EvalError = class extends Error {
|
|
57
|
-
constructor(message) {
|
|
58
|
-
super(message);
|
|
59
|
-
this.name = "EvalError";
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
function evaluate(node, ctx, dialect) {
|
|
63
|
-
const missingPolicy = new Map(
|
|
64
|
-
dialect.scopes.map((s) => [s.token, s.missing ?? "false"])
|
|
65
|
-
);
|
|
66
|
-
const rec = (n) => {
|
|
67
|
-
switch (n.kind) {
|
|
68
|
-
case "bool":
|
|
69
|
-
return n.value;
|
|
70
|
-
case "number":
|
|
71
|
-
return n.value;
|
|
72
|
-
case "string":
|
|
73
|
-
return n.value;
|
|
74
|
-
case "scopedvar": {
|
|
75
|
-
const scope = ctx.scopes[n.scope];
|
|
76
|
-
if (scope === void 0) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
const val = typeof scope.get === "function" ? scope.get(n.name) : scope[n.name];
|
|
80
|
-
if (val === void 0) {
|
|
81
|
-
if (missingPolicy.get(n.scope) === "throw") {
|
|
82
|
-
throw new EvalError(`@${n.scope}.${n.name} is not declared on the current ${n.scope}.`);
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
return val;
|
|
87
|
-
}
|
|
88
|
-
case "call": {
|
|
89
|
-
if (n.name === "advance" && !dialect.functions[n.name]) {
|
|
90
|
-
const arg = n.args[0];
|
|
91
|
-
if (n.args.length !== 1 || arg === void 0) {
|
|
92
|
-
throw new EvalError(`advance() takes exactly 1 argument, got ${n.args.length}`);
|
|
93
|
-
}
|
|
94
|
-
const ladder = ladderOf(arg, ctx);
|
|
95
|
-
if (ladder === void 0) {
|
|
96
|
-
throw new EvalError("advance() needs a quality reference (@scope.name of a quality property)");
|
|
97
|
-
}
|
|
98
|
-
const current = stageIndex(rec(arg), ladder, "advance");
|
|
99
|
-
return ladder[Math.min(current + 1, ladder.length - 1)];
|
|
100
|
-
}
|
|
101
|
-
const def = dialect.functions[n.name];
|
|
102
|
-
if (!def) throw new EvalError(`unknown function '${n.name}'`);
|
|
103
|
-
return def.eval(n.args, { evaluate: rec, ctx });
|
|
104
|
-
}
|
|
105
|
-
case "flagdelta":
|
|
106
|
-
throw new EvalError("flagdelta node is only valid as an argument to a flag-delta function");
|
|
107
|
-
case "unary": {
|
|
108
|
-
if (n.op === "not") {
|
|
109
|
-
const val2 = rec(n.operand);
|
|
110
|
-
if (typeof val2 !== "boolean") throw new EvalError(`'not' requires a boolean operand, got ${typeof val2}`);
|
|
111
|
-
return !val2;
|
|
112
|
-
}
|
|
113
|
-
const val = rec(n.operand);
|
|
114
|
-
if (typeof val !== "number") throw new EvalError(`unary '-' requires a numeric operand, got ${typeof val}`);
|
|
115
|
-
return -val;
|
|
116
|
-
}
|
|
117
|
-
case "binary": {
|
|
118
|
-
if (n.op === "and") {
|
|
119
|
-
const l = rec(n.left);
|
|
120
|
-
if (typeof l !== "boolean") throw new EvalError(`'and' requires boolean operands, left is ${typeof l}`);
|
|
121
|
-
if (!l) return false;
|
|
122
|
-
const r = rec(n.right);
|
|
123
|
-
if (typeof r !== "boolean") throw new EvalError(`'and' requires boolean operands, right is ${typeof r}`);
|
|
124
|
-
return r;
|
|
125
|
-
}
|
|
126
|
-
if (n.op === "or") {
|
|
127
|
-
const l = rec(n.left);
|
|
128
|
-
if (typeof l !== "boolean") throw new EvalError(`'or' requires boolean operands, left is ${typeof l}`);
|
|
129
|
-
if (l) return true;
|
|
130
|
-
const r = rec(n.right);
|
|
131
|
-
if (typeof r !== "boolean") throw new EvalError(`'or' requires boolean operands, right is ${typeof r}`);
|
|
132
|
-
return r;
|
|
133
|
-
}
|
|
134
|
-
const left = rec(n.left);
|
|
135
|
-
const right = rec(n.right);
|
|
136
|
-
const lLadder = ladderOf(n.left, ctx);
|
|
137
|
-
const rLadder = ladderOf(n.right, ctx);
|
|
138
|
-
const ladder = lLadder ?? rLadder;
|
|
139
|
-
if (ladder !== void 0) {
|
|
140
|
-
if (lLadder && rLadder && !sameLadder(lLadder, rLadder)) {
|
|
141
|
-
if (n.op === ">" || n.op === ">=" || n.op === "<" || n.op === "<=") {
|
|
142
|
-
throw new EvalError(`'${n.op}' compares two different qualities, whose stage orders are unrelated`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
switch (n.op) {
|
|
146
|
-
case ">":
|
|
147
|
-
return stageIndex(left, ladder, ">") > stageIndex(right, ladder, ">");
|
|
148
|
-
case ">=":
|
|
149
|
-
return stageIndex(left, ladder, ">=") >= stageIndex(right, ladder, ">=");
|
|
150
|
-
case "<":
|
|
151
|
-
return stageIndex(left, ladder, "<") < stageIndex(right, ladder, "<");
|
|
152
|
-
case "<=":
|
|
153
|
-
return stageIndex(left, ladder, "<=") <= stageIndex(right, ladder, "<=");
|
|
154
|
-
case "+":
|
|
155
|
-
case "-":
|
|
156
|
-
case "*":
|
|
157
|
-
case "/":
|
|
158
|
-
throw new EvalError(`'${n.op}' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);
|
|
159
|
-
default:
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
switch (n.op) {
|
|
164
|
-
case "==":
|
|
165
|
-
return valueEquals(left, right);
|
|
166
|
-
case "!=":
|
|
167
|
-
return !valueEquals(left, right);
|
|
168
|
-
case ">":
|
|
169
|
-
assertNumbers(left, right, ">");
|
|
170
|
-
return left > right;
|
|
171
|
-
case ">=":
|
|
172
|
-
assertNumbers(left, right, ">=");
|
|
173
|
-
return left >= right;
|
|
174
|
-
case "<":
|
|
175
|
-
assertNumbers(left, right, "<");
|
|
176
|
-
return left < right;
|
|
177
|
-
case "<=":
|
|
178
|
-
assertNumbers(left, right, "<=");
|
|
179
|
-
return left <= right;
|
|
180
|
-
case "+":
|
|
181
|
-
if (typeof left === "number" && typeof right === "number") return left + right;
|
|
182
|
-
if (typeof left === "string" && typeof right === "string") return left + right;
|
|
183
|
-
throw new EvalError(`'+' requires two numbers or two strings, got ${typeof left} and ${typeof right}`);
|
|
184
|
-
case "-":
|
|
185
|
-
assertNumbers(left, right, "-");
|
|
186
|
-
return left - right;
|
|
187
|
-
case "*":
|
|
188
|
-
assertNumbers(left, right, "*");
|
|
189
|
-
return left * right;
|
|
190
|
-
case "/":
|
|
191
|
-
assertNumbers(left, right, "/");
|
|
192
|
-
if (right === 0) throw new EvalError("division by zero");
|
|
193
|
-
return left / right;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
return rec(node);
|
|
199
|
-
}
|
|
200
|
-
function valueEquals(a, b) {
|
|
201
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
202
|
-
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
203
|
-
if (a.length !== b.length) return false;
|
|
204
|
-
const x = [...a].sort();
|
|
205
|
-
const y = [...b].sort();
|
|
206
|
-
for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
return a === b;
|
|
210
|
-
}
|
|
211
|
-
function assertNumbers(l, r, op) {
|
|
212
|
-
if (typeof l !== "number" || typeof r !== "number") {
|
|
213
|
-
throw new EvalError(`'${op}' requires numeric operands, got ${typeof l} and ${typeof r}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
function ladderOf(node, ctx) {
|
|
217
|
-
if (node.kind !== "scopedvar" || ctx.qualities === void 0) return void 0;
|
|
218
|
-
return ctx.qualities(node.scope, node.name);
|
|
219
|
-
}
|
|
220
|
-
function stageIndex(value, ladder, op) {
|
|
221
|
-
if (typeof value !== "string") {
|
|
222
|
-
throw new EvalError(`'${op}' on a quality compares stages, got ${typeof value}`);
|
|
223
|
-
}
|
|
224
|
-
const i = ladder.indexOf(value);
|
|
225
|
-
if (i < 0) throw new EvalError(`"${value}" is not a stage of this quality (stages: ${ladder.join(", ")})`);
|
|
226
|
-
return i;
|
|
227
|
-
}
|
|
228
|
-
var sameLadder = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
229
|
-
|
|
230
|
-
// ../../../expr/packages/expr/src/prng.ts
|
|
231
|
-
function toUint32(seed) {
|
|
232
|
-
if (Number.isNaN(seed) || !Number.isFinite(seed)) return 0;
|
|
233
|
-
const modded = Math.trunc(seed) % 4294967296;
|
|
234
|
-
return modded < 0 ? modded + 4294967296 : modded;
|
|
235
|
-
}
|
|
236
|
-
function makePrng(seed) {
|
|
237
|
-
let s = toUint32(seed);
|
|
238
|
-
return {
|
|
239
|
-
next() {
|
|
240
|
-
s = s + 1831565813 >>> 0;
|
|
241
|
-
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
242
|
-
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
243
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
244
|
-
},
|
|
245
|
-
state() {
|
|
246
|
-
return s;
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function shuffleInPlace(arr, prng) {
|
|
251
|
-
for (let i = arr.length - 1; i > 0; i--) {
|
|
252
|
-
const j = Math.floor(prng.next() * (i + 1));
|
|
253
|
-
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ../../../expr/packages/expr-specificity/src/index.ts
|
|
258
|
-
var CHECK_FLAGS_COUNTING_CALL = {
|
|
259
|
-
name: "check_flags",
|
|
260
|
-
count: (node) => Math.max(1, node.args.length - 1)
|
|
261
|
-
};
|
|
262
|
-
var DEFAULT_COUNTING_CALLS = [CHECK_FLAGS_COUNTING_CALL];
|
|
263
|
-
function matchedSpecificity(node, evalTruthy, opts) {
|
|
264
|
-
const countingCalls = opts?.countingCalls ?? DEFAULT_COUNTING_CALLS;
|
|
265
|
-
return walk(node, opts?.want ?? true, evalTruthy, countingCalls);
|
|
266
|
-
}
|
|
267
|
-
function walk(node, want, evalTruthy, countingCalls) {
|
|
268
|
-
if (node.kind === "binary" && (node.op === "and" || node.op === "or")) {
|
|
269
|
-
const l = walk(node.left, want, evalTruthy, countingCalls);
|
|
270
|
-
const r = walk(node.right, want, evalTruthy, countingCalls);
|
|
271
|
-
const behaveAsAnd = node.op === "and" === want;
|
|
272
|
-
if (behaveAsAnd) return l > 0 && r > 0 ? l + r : 0;
|
|
273
|
-
return Math.max(l, r);
|
|
274
|
-
}
|
|
275
|
-
if (node.kind === "unary" && node.op === "not") {
|
|
276
|
-
return walk(node.operand, !want, evalTruthy, countingCalls);
|
|
277
|
-
}
|
|
278
|
-
if (node.kind === "call") {
|
|
279
|
-
const rule = countingCalls.find((c) => c.name === node.name);
|
|
280
|
-
if (rule) {
|
|
281
|
-
const operands = rule.count(node);
|
|
282
|
-
const holds = evalTruthy(node);
|
|
283
|
-
if (want) return holds ? operands : 0;
|
|
284
|
-
return holds ? 0 : 1;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return evalTruthy(node) === want ? 1 : 0;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ../dialect/src/engine-scopes.ts
|
|
291
|
-
var ENGINE_SCOPES = [
|
|
292
|
-
{ token: "patter", engine: "Patterplay", means: "Patter's shared globals" },
|
|
293
|
-
{ token: "story", engine: "Storylet Engine", means: "the Storylet Engine's shared @story properties" }
|
|
294
|
-
];
|
|
295
|
-
|
|
296
|
-
// ../dialect/src/index.ts
|
|
297
|
-
var NEVER_PLAYED = 9999;
|
|
298
|
-
var host = (h) => h.ctx.host ?? {};
|
|
299
|
-
var stringArg = (fn, args, h, i) => {
|
|
300
|
-
const v = h.evaluate(args[i]);
|
|
301
|
-
if (typeof v !== "string" || v === "") {
|
|
302
|
-
throw new EvalError(`${fn}() argument ${i + 1} must be a non-empty string`);
|
|
303
|
-
}
|
|
304
|
-
return v;
|
|
305
|
-
};
|
|
306
|
-
var flagsArg = (fn, args, h) => {
|
|
307
|
-
if (args.length === 0) {
|
|
308
|
-
throw new EvalError(`${fn}() requires at least one argument (the flags property)`);
|
|
309
|
-
}
|
|
310
|
-
const v = h.evaluate(args[0]);
|
|
311
|
-
if (Array.isArray(v)) return v;
|
|
312
|
-
if (v === false) return [];
|
|
313
|
-
throw new EvalError(`${fn}() first argument must be a flags property`);
|
|
314
|
-
};
|
|
315
|
-
var OWN_SCOPES = ["story", "world", "box", "deck", "hand"];
|
|
316
|
-
var EXTERNAL_SCOPES = ENGINE_SCOPES.map((s) => s.token).filter((t) => !OWN_SCOPES.includes(t));
|
|
317
|
-
var storyletsDialect = {
|
|
318
|
-
// A missing property in a PRESENT scope is always an error: every property
|
|
319
|
-
// is declared with a default, so absence means a publish bug, a drifted
|
|
320
|
-
// save, or a foreign scope the host never fed (schema 6.2). The same holds
|
|
321
|
-
// for another engine's scope: `@patter.glod` is a typo, and it says so the
|
|
322
|
-
// first time the card is evaluated rather than quietly reading false.
|
|
323
|
-
scopes: [
|
|
324
|
-
...OWN_SCOPES.map((token) => ({ token, missing: "throw" })),
|
|
325
|
-
...EXTERNAL_SCOPES.map((token) => ({ token, missing: "throw" }))
|
|
326
|
-
],
|
|
327
|
-
defaultScope: "story",
|
|
328
|
-
functions: {
|
|
329
|
-
random: {
|
|
330
|
-
minArgs: 2,
|
|
331
|
-
maxArgs: 2,
|
|
332
|
-
returnType: "number",
|
|
333
|
-
eval(args, h) {
|
|
334
|
-
if (args.length !== 2) throw new EvalError("random(a, b) requires exactly 2 arguments");
|
|
335
|
-
const nextRandom = host(h).nextRandom;
|
|
336
|
-
if (!nextRandom) throw new EvalError("random() called without a PRNG in context");
|
|
337
|
-
const a = h.evaluate(args[0]);
|
|
338
|
-
const b = h.evaluate(args[1]);
|
|
339
|
-
if (typeof a !== "number" || typeof b !== "number") {
|
|
340
|
-
throw new EvalError("random(a, b) arguments must be numbers");
|
|
341
|
-
}
|
|
342
|
-
if (!Number.isInteger(a) || !Number.isInteger(b)) {
|
|
343
|
-
throw new EvalError("random(a, b) arguments must be integers");
|
|
344
|
-
}
|
|
345
|
-
const lo = Math.min(a, b);
|
|
346
|
-
const hi = Math.max(a, b);
|
|
347
|
-
return Math.floor(nextRandom() * (hi - lo + 1)) + lo;
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
check_flags: {
|
|
351
|
-
minArgs: 1,
|
|
352
|
-
returnType: "boolean",
|
|
353
|
-
flagDeltaArgs: true,
|
|
354
|
-
eval(args, h) {
|
|
355
|
-
const flags = flagsArg("check_flags", args, h);
|
|
356
|
-
for (let i = 1; i < args.length; i++) {
|
|
357
|
-
const arg = args[i];
|
|
358
|
-
if (arg.kind !== "flagdelta") {
|
|
359
|
-
throw new EvalError("check_flags() flag args must be +flagName or -flagName");
|
|
360
|
-
}
|
|
361
|
-
if (arg.sign === "+" ? !flags.includes(arg.name) : flags.includes(arg.name)) {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
set_flags: {
|
|
369
|
-
minArgs: 1,
|
|
370
|
-
returnType: "flags",
|
|
371
|
-
flagDeltaArgs: true,
|
|
372
|
-
eval(args, h) {
|
|
373
|
-
const result = [...flagsArg("set_flags", args, h)];
|
|
374
|
-
for (let i = 1; i < args.length; i++) {
|
|
375
|
-
const arg = args[i];
|
|
376
|
-
if (arg.kind !== "flagdelta") {
|
|
377
|
-
throw new EvalError("set_flags() flag args must be +flagName or -flagName");
|
|
378
|
-
}
|
|
379
|
-
if (arg.sign === "+") {
|
|
380
|
-
if (!result.includes(arg.name)) result.push(arg.name);
|
|
381
|
-
} else {
|
|
382
|
-
const idx = result.indexOf(arg.name);
|
|
383
|
-
if (idx >= 0) result.splice(idx, 1);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return result.sort();
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
count_played: {
|
|
390
|
-
minArgs: 1,
|
|
391
|
-
maxArgs: 1,
|
|
392
|
-
returnType: "number",
|
|
393
|
-
eval(args, h) {
|
|
394
|
-
const card = stringArg("count_played", args, h, 0);
|
|
395
|
-
const fn = host(h).countPlayed;
|
|
396
|
-
if (!fn) throw new EvalError("count_played() called without a play log in context");
|
|
397
|
-
return fn(card);
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
turns_since_played: {
|
|
401
|
-
minArgs: 1,
|
|
402
|
-
maxArgs: 1,
|
|
403
|
-
returnType: "number",
|
|
404
|
-
eval(args, h) {
|
|
405
|
-
const card = stringArg("turns_since_played", args, h, 0);
|
|
406
|
-
const fn = host(h).turnsSincePlayed;
|
|
407
|
-
if (!fn) throw new EvalError("turns_since_played() called without a play log in context");
|
|
408
|
-
return fn(card);
|
|
409
|
-
}
|
|
410
|
-
},
|
|
411
|
-
count_played_in: {
|
|
412
|
-
minArgs: 2,
|
|
413
|
-
maxArgs: 2,
|
|
414
|
-
returnType: "number",
|
|
415
|
-
eval(args, h) {
|
|
416
|
-
const dimension = stringArg("count_played_in", args, h, 0);
|
|
417
|
-
const value = stringArg("count_played_in", args, h, 1);
|
|
418
|
-
const fn = host(h).countPlayedIn;
|
|
419
|
-
if (!fn) throw new EvalError("count_played_in() called without a play log in context");
|
|
420
|
-
return fn(dimension, value);
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
turns_since_played_in: {
|
|
424
|
-
minArgs: 2,
|
|
425
|
-
maxArgs: 2,
|
|
426
|
-
returnType: "number",
|
|
427
|
-
eval(args, h) {
|
|
428
|
-
const dimension = stringArg("turns_since_played_in", args, h, 0);
|
|
429
|
-
const value = stringArg("turns_since_played_in", args, h, 1);
|
|
430
|
-
const fn = host(h).turnsSincePlayedIn;
|
|
431
|
-
if (!fn) throw new EvalError("turns_since_played_in() called without a play log in context");
|
|
432
|
-
return fn(dimension, value);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
// ../model/src/index.ts
|
|
439
|
-
function gameIdify(text) {
|
|
440
|
-
return text.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
441
|
-
}
|
|
442
|
-
function effectiveGameId(entity) {
|
|
443
|
-
const pinned = entity.gameId?.trim();
|
|
444
|
-
if (pinned) return pinned;
|
|
445
|
-
const fromTitle = entity.title ? gameIdify(entity.title) : "";
|
|
446
|
-
return fromTitle || entity.id;
|
|
447
|
-
}
|
|
448
|
-
function valueAddresses(bundle) {
|
|
449
|
-
const tags = [];
|
|
450
|
-
for (const box of bundle.boxes) {
|
|
451
|
-
const boxGameId = effectiveGameId(box);
|
|
452
|
-
for (const group of box.tagGroups) {
|
|
453
|
-
for (const tag of group.tags) {
|
|
454
|
-
const gameId = effectiveGameId(tag);
|
|
455
|
-
tags.push({ id: tag.id, gameId, qualified: `${boxGameId}/${gameId}` });
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
const forms = /* @__PURE__ */ new Map();
|
|
460
|
-
for (const tag of tags) {
|
|
461
|
-
const list = forms.get(tag.gameId) ?? [];
|
|
462
|
-
if (!list.includes(tag.qualified)) list.push(tag.qualified);
|
|
463
|
-
forms.set(tag.gameId, list);
|
|
464
|
-
}
|
|
465
|
-
const print = /* @__PURE__ */ new Map();
|
|
466
|
-
const accept = /* @__PURE__ */ new Map();
|
|
467
|
-
const repeated = /* @__PURE__ */ new Map();
|
|
468
|
-
for (const tag of tags) {
|
|
469
|
-
const candidates = forms.get(tag.gameId) ?? [tag.qualified];
|
|
470
|
-
const ambiguous = candidates.length > 1;
|
|
471
|
-
print.set(tag.id, ambiguous ? tag.qualified : tag.gameId);
|
|
472
|
-
if (!accept.has(tag.qualified)) accept.set(tag.qualified, tag.id);
|
|
473
|
-
if (!ambiguous && !accept.has(tag.gameId)) accept.set(tag.gameId, tag.id);
|
|
474
|
-
if (ambiguous) repeated.set(tag.gameId, candidates);
|
|
475
|
-
}
|
|
476
|
-
return { print, accept, repeated };
|
|
477
|
-
}
|
|
478
|
-
function ambiguousValueAddressMessage(segment, name, candidates) {
|
|
479
|
-
const forms = candidates.map((q) => `"value.${q}.${name}"`);
|
|
480
|
-
const list = forms.length <= 1 ? forms[0] ?? "" : `${forms.slice(0, -1).join(", ")} or ${forms[forms.length - 1]}`;
|
|
481
|
-
return `"value.${segment}.${name}" names a tag in ${candidates.length} boxes; write ${list}`;
|
|
482
|
-
}
|
|
483
|
-
var PLACE_GROUP = "place";
|
|
484
|
-
var HOLE_REF = /^@(hand|world|story)\.([a-z][a-z0-9_-]*)$/;
|
|
485
|
-
var isHoleRef = (value) => value.startsWith("@");
|
|
486
|
-
var parseHoleRef = (value) => {
|
|
487
|
-
const m = HOLE_REF.exec(value);
|
|
488
|
-
return m === null ? void 0 : { scope: m[1], name: m[2] };
|
|
489
|
-
};
|
|
490
|
-
var SAVE_SCHEMA = "storylets/save@2";
|
|
491
|
-
var SAVE_SCHEMA_V1 = "storylets/save@1";
|
|
31
|
+
// src/engine.ts
|
|
32
|
+
var import_expr2 = require("@wildwinter/expr");
|
|
33
|
+
var import_expr_specificity = require("@wildwinter/expr-specificity");
|
|
34
|
+
var import_dialect = require("@storylet-studio/dialect");
|
|
35
|
+
var import_model = require("@storylet-studio/model");
|
|
36
|
+
var import_model2 = require("@storylet-studio/model");
|
|
37
|
+
var import_scoperegistry = require("@wildwinter/scoperegistry");
|
|
492
38
|
|
|
493
|
-
//
|
|
494
|
-
var
|
|
495
|
-
/** The live values record (stable identity across reseed, so an
|
|
496
|
-
* EvalContext built over it stays valid). Read-path for evaluation;
|
|
497
|
-
* writes go through `set` so the firing rule applies. */
|
|
498
|
-
values = {};
|
|
499
|
-
decls = /* @__PURE__ */ new Map();
|
|
500
|
-
subscribers = /* @__PURE__ */ new Set();
|
|
501
|
-
auditors = /* @__PURE__ */ new Set();
|
|
502
|
-
/** Name normalisation policy: lowercase by default (the registry's
|
|
503
|
-
* long-standing contract); a product whose names are case-significant
|
|
504
|
-
* passes identity. */
|
|
505
|
-
norm;
|
|
506
|
-
/** The address prefix this bag's rows carry, separator included (`@`,
|
|
507
|
-
* `@scene.`, `world.`, `deck.<id>.`). Empty means a row's path is its name. */
|
|
508
|
-
pathPrefix;
|
|
509
|
-
constructor(declarations = [], opts) {
|
|
510
|
-
this.norm = opts?.normalise ?? ((n) => n.toLowerCase());
|
|
511
|
-
this.pathPrefix = opts?.pathPrefix ?? "";
|
|
512
|
-
this.seed(declarations);
|
|
513
|
-
}
|
|
514
|
-
seed(declarations) {
|
|
515
|
-
for (const d of declarations) {
|
|
516
|
-
const name = this.norm(d.name);
|
|
517
|
-
this.decls.set(name, d);
|
|
518
|
-
this.values[name] = structuredClone(d.default ?? defaultFor(d));
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
get(name) {
|
|
522
|
-
return this.values[this.norm(name)];
|
|
523
|
-
}
|
|
524
|
-
/** A name as this bag keys it: its normalisation policy applied. The registry
|
|
525
|
-
* uses it to key quality ladders and the validation schema the bag's own way,
|
|
526
|
-
* so a case-significant (identity) bag is not quietly folded to lower case
|
|
527
|
-
* one layer up. */
|
|
528
|
-
normalise(name) {
|
|
529
|
-
return this.norm(name);
|
|
530
|
-
}
|
|
531
|
-
/** Write a property. Engine writes (the default) notify subscribers;
|
|
532
|
-
* pass `silent: true` for a host write, which reaches only the audit
|
|
533
|
-
* hook. Throws on a read-only property unless the caller says it is the
|
|
534
|
-
* HOST (`host: true`), for whom `writable: false` was never a rule - it is
|
|
535
|
-
* the story's promise, not the game's. `silent` and `host` are separate on
|
|
536
|
-
* purpose: one is about who hears the write, the other about who may make
|
|
537
|
-
* it. Returns the change. */
|
|
538
|
-
set(name, value, opts) {
|
|
539
|
-
const n = this.norm(name);
|
|
540
|
-
if (!opts?.host && this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
|
|
541
|
-
const change = {
|
|
542
|
-
name: n,
|
|
543
|
-
prev: this.values[n],
|
|
544
|
-
next: value,
|
|
545
|
-
silent: opts?.silent ?? false,
|
|
546
|
-
reason: opts?.reason
|
|
547
|
-
};
|
|
548
|
-
this.values[n] = value;
|
|
549
|
-
for (const audit of this.auditors) audit(change);
|
|
550
|
-
if (!change.silent) for (const fn of this.subscribers) fn(change);
|
|
551
|
-
return change;
|
|
552
|
-
}
|
|
553
|
-
/** Notified of engine (non-silent) writes. Returns the unsubscribe. */
|
|
554
|
-
subscribe(fn) {
|
|
555
|
-
this.subscribers.add(fn);
|
|
556
|
-
return () => this.subscribers.delete(fn);
|
|
557
|
-
}
|
|
558
|
-
/** Notified of EVERY write, silent or not. Returns the unsubscribe. */
|
|
559
|
-
onAudit(fn) {
|
|
560
|
-
this.auditors.add(fn);
|
|
561
|
-
return () => this.auditors.delete(fn);
|
|
562
|
-
}
|
|
563
|
-
/** Examiner rows: the declared surface only (stray values are storage,
|
|
564
|
-
* not surface). */
|
|
565
|
-
rows() {
|
|
566
|
-
return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name, this.pathPrefix));
|
|
567
|
-
}
|
|
568
|
-
declarations() {
|
|
569
|
-
return [...this.decls.values()];
|
|
570
|
-
}
|
|
571
|
-
/** The one sanctioned copy door: values deep-copied, declarations
|
|
572
|
-
* duplicated, the normalisation policy carried, subscriptions NOT
|
|
573
|
-
* carried. */
|
|
574
|
-
clone() {
|
|
575
|
-
const c = new _PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });
|
|
576
|
-
c.decls = new Map(this.decls);
|
|
577
|
-
Object.assign(c.values, structuredClone(this.values));
|
|
578
|
-
return c;
|
|
579
|
-
}
|
|
580
|
-
/** Clear and re-seed from new declarations, in place (the values record
|
|
581
|
-
* keeps its identity, so contexts built over it stay valid). */
|
|
582
|
-
reseed(declarations) {
|
|
583
|
-
for (const k of Object.keys(this.values)) delete this.values[k];
|
|
584
|
-
this.decls.clear();
|
|
585
|
-
this.seed(declarations);
|
|
586
|
-
}
|
|
587
|
-
/** Bare values, ready to embed in a product's save. */
|
|
588
|
-
save() {
|
|
589
|
-
return structuredClone(this.values);
|
|
590
|
-
}
|
|
591
|
-
/** Lay saved values over the current ones (call after a fresh seed:
|
|
592
|
-
* orphans land as strays, new declarations keep their defaults; the
|
|
593
|
-
* product decides whether to prune). Does not fire events. */
|
|
594
|
-
load(values) {
|
|
595
|
-
for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
|
|
596
|
-
}
|
|
597
|
-
};
|
|
598
|
-
function rowFor(d, value, writable, name, pathPrefix = "") {
|
|
599
|
-
const rowName = name ?? d.name.toLowerCase();
|
|
600
|
-
return {
|
|
601
|
-
name: rowName,
|
|
602
|
-
path: pathPrefix + rowName,
|
|
603
|
-
type: d.type,
|
|
604
|
-
value,
|
|
605
|
-
default: d.default ?? defaultFor(d),
|
|
606
|
-
...d.values !== void 0 ? { values: d.values } : {},
|
|
607
|
-
// `stages` was added to the row so an examiner could offer a quality's ladder
|
|
608
|
-
// instead of a free-text box, and then never populated here: every quality row
|
|
609
|
-
// this function built came out without one. Fixed 2026-09-02.
|
|
610
|
-
...d.stages !== void 0 ? { stages: d.stages } : {},
|
|
611
|
-
writable: writable ?? d.writable ?? true
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
var lowerCase = (name) => name.toLowerCase();
|
|
615
|
-
var SAVE_FRAGMENT_VERSION = 1;
|
|
616
|
-
var ScopeRegistry = class {
|
|
617
|
-
scopes = /* @__PURE__ */ new Map();
|
|
618
|
-
/** Values loaded for keys nobody has registered yet, waiting to be claimed. */
|
|
619
|
-
parked = /* @__PURE__ */ new Map();
|
|
620
|
-
rev = 0;
|
|
621
|
-
/**
|
|
622
|
-
* A counter that moves whenever a scope is registered or removed, and at no
|
|
623
|
-
* other time: it starts at 0 and each registration or removal adds 1. Values
|
|
624
|
-
* changing does not move it. A caller that caches a context built by
|
|
625
|
-
* `toEvalContext()` rebuilds it when this moves, because the context's set of
|
|
626
|
-
* scopes is fixed when it is built while the values it reads stay live.
|
|
627
|
-
*/
|
|
628
|
-
get revision() {
|
|
629
|
-
return this.rev;
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
|
-
* Register a scope this registry **owns and stores**. Its bag is seeded from
|
|
633
|
-
* each declaration's `default` (or a type default). Owned scopes are
|
|
634
|
-
* type-checked (declarations) and serialized by `save`/`load`.
|
|
635
|
-
*
|
|
636
|
-
* The third argument may be the path prefix alone (the pre-0.7 form) or an
|
|
637
|
-
* options object.
|
|
638
|
-
*/
|
|
639
|
-
defineOwned(token, declarations, opts) {
|
|
640
|
-
const o = typeof opts === "string" ? { pathPrefix: opts } : opts ?? {};
|
|
641
|
-
const bag = new PropertyBag(declarations, {
|
|
642
|
-
pathPrefix: o.pathPrefix ?? `${token}.`,
|
|
643
|
-
...o.normalise ? { normalise: o.normalise } : {}
|
|
644
|
-
});
|
|
645
|
-
return this.mountOwned(token, bag, o.owner !== void 0 ? { owner: o.owner } : void 0);
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Attach an EXISTING bag as an owned scope: an engine (or a host) holds the
|
|
649
|
-
* bag and this registry reads, writes, lists and saves it like its own.
|
|
650
|
-
*
|
|
651
|
-
* If values were loaded for this key before anyone registered it, the bag
|
|
652
|
-
* claims them now: laid over its seeded defaults by the bag's own `load` rule.
|
|
653
|
-
*/
|
|
654
|
-
mountOwned(token, bag, opts) {
|
|
655
|
-
this.assertFree(token, opts?.owner);
|
|
656
|
-
this.scopes.set(token, { kind: "owned", bag, ...opts?.owner !== void 0 ? { owner: opts.owner } : {} });
|
|
657
|
-
this.rev++;
|
|
658
|
-
const waiting = this.parked.get(token);
|
|
659
|
-
if (waiting) {
|
|
660
|
-
bag.load(waiting);
|
|
661
|
-
this.parked.delete(token);
|
|
662
|
-
}
|
|
663
|
-
return this;
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Unregister a scope. With `{ keep: true }` an owned scope's values are parked
|
|
667
|
-
* and handed back when the same key is next registered, which is how a live
|
|
668
|
-
* reload hands an engine's state to its replacement. Throws on an unknown key.
|
|
669
|
-
*/
|
|
670
|
-
remove(token, opts) {
|
|
671
|
-
const e = this.scopes.get(token);
|
|
672
|
-
if (!e) throw new Error(`unknown scope '@${token}'`);
|
|
673
|
-
if (opts?.keep && e.kind === "owned") this.parked.set(token, e.bag.save());
|
|
674
|
-
this.scopes.delete(token);
|
|
675
|
-
this.rev++;
|
|
676
|
-
return this;
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* Drop parked values nobody claimed. Parked values are kept in the next save by
|
|
680
|
-
* default, so nothing loaded is lost to a flow or deck that simply has not
|
|
681
|
-
* reopened yet; a game that knows they are dead drops them here.
|
|
682
|
-
*
|
|
683
|
-
* With a `prefix`, only keys starting with it are dropped: an engine resetting
|
|
684
|
-
* itself drops its own instance keys (`my-engine/`) and leaves every other
|
|
685
|
-
* engine's alone.
|
|
686
|
-
*/
|
|
687
|
-
discardParked(prefix) {
|
|
688
|
-
if (prefix === void 0) this.parked.clear();
|
|
689
|
-
else for (const key of [...this.parked.keys()]) if (key.startsWith(prefix)) this.parked.delete(key);
|
|
690
|
-
return this;
|
|
691
|
-
}
|
|
692
|
-
/** An owned scope's bag (subscribe, audit, rows live there). */
|
|
693
|
-
ownedBag(token) {
|
|
694
|
-
const e = this.scopes.get(token);
|
|
695
|
-
if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
|
|
696
|
-
return e.bag;
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Re-initialise an existing **owned** scope's bag from new declarations,
|
|
700
|
-
* clearing its current values. For scope-local state that resets on a context
|
|
701
|
-
* change (e.g. entering a new scene / site / deck) without disturbing other
|
|
702
|
-
* scopes. Mutates the bag in place, so an `EvalContext` already built from this
|
|
703
|
-
* registry stays valid.
|
|
704
|
-
*/
|
|
705
|
-
reseedOwned(token, declarations) {
|
|
706
|
-
this.ownedBag(token).reseed(declarations);
|
|
707
|
-
return this;
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Register a **foreign** scope backed by a host `{ get, set? }` resolver. The
|
|
711
|
-
* values live in the host/other engine and are never stored or saved here.
|
|
712
|
-
* `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used
|
|
713
|
-
* only for validation; omit them for an opaque scope.
|
|
714
|
-
*/
|
|
715
|
-
defineForeign(token, resolver, declarations = [], opts = true) {
|
|
716
|
-
const o = typeof opts === "boolean" ? { writable: opts } : opts;
|
|
717
|
-
this.assertFree(token, o.owner);
|
|
718
|
-
const norm = o.normalise ?? lowerCase;
|
|
719
|
-
const decls = /* @__PURE__ */ new Map();
|
|
720
|
-
for (const d of declarations) decls.set(norm(d.name), d);
|
|
721
|
-
this.scopes.set(token, {
|
|
722
|
-
kind: "foreign",
|
|
723
|
-
resolver,
|
|
724
|
-
decls,
|
|
725
|
-
scopeWritable: o.writable ?? true,
|
|
726
|
-
norm,
|
|
727
|
-
...o.owner !== void 0 ? { owner: o.owner } : {}
|
|
728
|
-
});
|
|
729
|
-
this.rev++;
|
|
730
|
-
return this;
|
|
731
|
-
}
|
|
732
|
-
has(token) {
|
|
733
|
-
return this.scopes.has(token);
|
|
734
|
-
}
|
|
735
|
-
/** Read a property; undefined if the scope or property is not present. */
|
|
736
|
-
get(scope, name) {
|
|
737
|
-
const e = this.scopes.get(scope);
|
|
738
|
-
if (!e) return void 0;
|
|
739
|
-
return e.kind === "owned" ? e.bag.get(name) : e.resolver.get(e.norm(name));
|
|
740
|
-
}
|
|
741
|
-
/** Write a property (an ENGINE write: the bag's subscribers fire; use
|
|
742
|
-
* the bag directly for silent host writes). Throws on an unknown scope.
|
|
743
|
-
*
|
|
744
|
-
* `writable: false` is the STORY's promise, so a story write is refused and
|
|
745
|
-
* a HOST write is not: pass `{ host: true }` from a host's own surface (its
|
|
746
|
-
* `setProperty`, its tooling, a coverage driver) and never from the path an
|
|
747
|
-
* outcome or effect takes. A foreign scope whose resolver has no `set` is
|
|
748
|
-
* refused for everyone, host included - that is not a rule to bypass, it is
|
|
749
|
-
* a game that gave no way to write. */
|
|
750
|
-
set(scope, name, value, opts) {
|
|
751
|
-
const e = this.scopes.get(scope);
|
|
752
|
-
if (!e) throw new Error(`unknown scope '@${scope}'`);
|
|
753
|
-
if (e.kind === "owned") {
|
|
754
|
-
try {
|
|
755
|
-
e.bag.set(name, value, opts?.host ? { host: true } : void 0);
|
|
756
|
-
} catch {
|
|
757
|
-
throw new Error(`'@${scope}.${name}' is read-only`);
|
|
758
|
-
}
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
const n = e.norm(name);
|
|
762
|
-
if (!e.resolver.set) throw new Error(`'@${scope}.${name}' is read-only`);
|
|
763
|
-
if (!opts?.host && !this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);
|
|
764
|
-
e.resolver.set(n, value);
|
|
765
|
-
}
|
|
766
|
-
foreignWritable(e, name) {
|
|
767
|
-
if (!e.resolver.set) return false;
|
|
768
|
-
return e.decls.get(name)?.writable ?? e.scopeWritable;
|
|
769
|
-
}
|
|
770
|
-
/** Examiner rows across every scope with a declared surface: owned bags
|
|
771
|
-
* first, then declared foreign scopes (values read through, writability
|
|
772
|
-
* reflecting the resolver). Opaque foreign scopes are not listed. */
|
|
773
|
-
listProperties() {
|
|
774
|
-
const out = [];
|
|
775
|
-
for (const [token, e] of this.scopes) {
|
|
776
|
-
const owner = e.owner !== void 0 ? { owner: e.owner } : {};
|
|
777
|
-
if (e.kind === "owned") {
|
|
778
|
-
for (const row of e.bag.rows()) out.push({ scope: token, ...owner, ...row });
|
|
779
|
-
} else {
|
|
780
|
-
for (const [n, d] of e.decls) {
|
|
781
|
-
out.push({
|
|
782
|
-
scope: token,
|
|
783
|
-
...owner,
|
|
784
|
-
...rowFor(d, e.resolver.get(n), this.foreignWritable(e, n), n, `${token}.`)
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
return out;
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
|
|
793
|
-
* bags, foreign scopes as their resolvers. `host` carries dialect-function
|
|
794
|
-
* callbacks (PRNG, tag lookups) and is passed through untouched.
|
|
795
|
-
*/
|
|
796
|
-
toEvalContext(host2, opts) {
|
|
797
|
-
const view = this.view(opts?.aliases);
|
|
798
|
-
const scopes = {};
|
|
799
|
-
for (const [token, e] of view) scopes[token] = e.kind === "owned" ? e.bag.values : e.resolver;
|
|
800
|
-
const qualities = this.qualityLadders(view);
|
|
801
|
-
return qualities.size === 0 ? { scopes, host: host2 } : {
|
|
802
|
-
scopes,
|
|
803
|
-
host: host2,
|
|
804
|
-
qualities: (scope, name) => {
|
|
805
|
-
const e = view.get(scope);
|
|
806
|
-
return e ? qualities.get(scope)?.get(normOf(e)(name)) : void 0;
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* The scopes an expression sees: every registered key under its own token,
|
|
812
|
-
* then each alias token pointing at its key's entry (an alias shadows a key of
|
|
813
|
-
* the same name). Keys an engine uses for instance bags (`engine/flow-2/...`)
|
|
814
|
-
* are not valid expression tokens, so they are present but unreachable.
|
|
815
|
-
*/
|
|
816
|
-
view(aliases) {
|
|
817
|
-
const out = new Map(this.scopes);
|
|
818
|
-
for (const [token, key] of Object.entries(aliases ?? {})) {
|
|
819
|
-
const e = this.scopes.get(key);
|
|
820
|
-
if (!e) throw new Error(`alias '@${token}' names '${key}', which is not registered`);
|
|
821
|
-
out.set(token, e);
|
|
822
|
-
}
|
|
823
|
-
return out;
|
|
824
|
-
}
|
|
825
|
-
/** Every quality declaration's ladder, keyed scope token then name (the
|
|
826
|
-
* scope's own normalisation). */
|
|
827
|
-
qualityLadders(view) {
|
|
828
|
-
const out = /* @__PURE__ */ new Map();
|
|
829
|
-
for (const [token, e] of view) {
|
|
830
|
-
for (const [n, d] of declsOf(e)) {
|
|
831
|
-
if (d.type !== "quality" || d.stages === void 0) continue;
|
|
832
|
-
let m = out.get(token);
|
|
833
|
-
if (!m) {
|
|
834
|
-
m = /* @__PURE__ */ new Map();
|
|
835
|
-
out.set(token, m);
|
|
836
|
-
}
|
|
837
|
-
m.set(n, d.stages);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
return out;
|
|
841
|
-
}
|
|
842
|
-
/**
|
|
843
|
-
* Build the `ExpressionSchema` expr's validator consumes. Scopes with no
|
|
844
|
-
* declarations are **omitted** (opaque - references into them are not flagged);
|
|
845
|
-
* declared scopes contribute their property types for validation. Aliases
|
|
846
|
-
* apply as they do to `toEvalContext`, so a condition written against `@scene`
|
|
847
|
-
* validates against the instance bag the engine names.
|
|
848
|
-
*/
|
|
849
|
-
toSchema(opts) {
|
|
850
|
-
const properties = /* @__PURE__ */ new Map();
|
|
851
|
-
for (const [token, e] of this.view(opts?.aliases)) {
|
|
852
|
-
const decls = declsOf(e);
|
|
853
|
-
if (decls.length === 0) continue;
|
|
854
|
-
const m = /* @__PURE__ */ new Map();
|
|
855
|
-
for (const [n, d] of decls) m.set(n, {
|
|
856
|
-
type: d.type,
|
|
857
|
-
enumValues: d.values,
|
|
858
|
-
...d.stages !== void 0 ? { stages: d.stages } : {}
|
|
859
|
-
});
|
|
860
|
-
properties.set(token, m);
|
|
861
|
-
}
|
|
862
|
-
return { properties };
|
|
863
|
-
}
|
|
864
|
-
/** Serialize **owned** scopes (foreign scopes are the game's, and the game
|
|
865
|
-
* saves them), as bare bags keyed by token, plus any values still parked, so
|
|
866
|
-
* a save taken before every engine has re-registered loses nothing. The
|
|
867
|
-
* registry knows nothing about game saves: a game embeds this in its own. */
|
|
868
|
-
save() {
|
|
869
|
-
const out = {};
|
|
870
|
-
for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = e.bag.save();
|
|
871
|
-
for (const [token, vals] of this.parked) out[token] = structuredClone(vals);
|
|
872
|
-
return out;
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* Restore from a `save` blob. An owned scope lays its section over its current
|
|
876
|
-
* values (the bag's `load` rule). A section for a key nobody has registered
|
|
877
|
-
* yet is PARKED and handed over when that key registers, so a game can load
|
|
878
|
-
* its registry before its engines have reopened their flows or decks. A
|
|
879
|
-
* section for a foreign scope is ignored: those values are the game's.
|
|
880
|
-
*
|
|
881
|
-
* A load replaces whatever was parked before it: it is a whole restore, and
|
|
882
|
-
* residue from an earlier load must not leak into this one.
|
|
883
|
-
*
|
|
884
|
-
* Changed in 0.7.0: sections for unregistered keys used to be dropped.
|
|
885
|
-
*/
|
|
886
|
-
load(blob, opts) {
|
|
887
|
-
if (!opts?.keepParked) this.parked.clear();
|
|
888
|
-
for (const [token, vals] of Object.entries(blob)) {
|
|
889
|
-
const e = this.scopes.get(token);
|
|
890
|
-
if (e?.kind === "owned") e.bag.load(vals);
|
|
891
|
-
else if (!e) this.parked.set(token, structuredClone(vals));
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* `save()` wrapped with a version stamp.
|
|
896
|
-
*
|
|
897
|
-
* @deprecated Versioning belongs to the save that embeds the values; no
|
|
898
|
-
* engine ever called this. Embed `save()` in your own versioned save.
|
|
899
|
-
* Removed at the next breaking release.
|
|
900
|
-
*/
|
|
901
|
-
saveFragment() {
|
|
902
|
-
return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Restore from a versioned fragment; an unsupported version throws.
|
|
906
|
-
*
|
|
907
|
-
* @deprecated See `saveFragment`. Removed at the next breaking release.
|
|
908
|
-
*/
|
|
909
|
-
loadFragment(fragment) {
|
|
910
|
-
if (fragment.version !== SAVE_FRAGMENT_VERSION) {
|
|
911
|
-
throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);
|
|
912
|
-
}
|
|
913
|
-
this.load(fragment.scopes);
|
|
914
|
-
}
|
|
915
|
-
/**
|
|
916
|
-
* A token is taken once. There is no reserved-token list: a clash surfaces
|
|
917
|
-
* here, the moment a game combines its engines, which is the only moment
|
|
918
|
-
* anyone knows which engines are present. With owners recorded the error says
|
|
919
|
-
* whose token it already is.
|
|
920
|
-
*/
|
|
921
|
-
assertFree(token, owner) {
|
|
922
|
-
const e = this.scopes.get(token);
|
|
923
|
-
if (!e) return;
|
|
924
|
-
const by = e.owner !== void 0 ? ` by ${e.owner}` : "";
|
|
925
|
-
const wants = owner !== void 0 ? ` (wanted by ${owner})` : "";
|
|
926
|
-
throw new Error(`scope '@${token}' is already registered${by}${wants}`);
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
function declsOf(e) {
|
|
930
|
-
if (e.kind === "foreign") return [...e.decls.entries()];
|
|
931
|
-
return e.bag.declarations().map((d) => [e.bag.normalise(d.name), d]);
|
|
932
|
-
}
|
|
933
|
-
function normOf(e) {
|
|
934
|
-
return e.kind === "foreign" ? e.norm : (n) => e.bag.normalise(n);
|
|
935
|
-
}
|
|
936
|
-
function defaultFor(d) {
|
|
937
|
-
if (d.default !== void 0) return d.default;
|
|
938
|
-
switch (d.type) {
|
|
939
|
-
case "boolean":
|
|
940
|
-
return false;
|
|
941
|
-
case "number":
|
|
942
|
-
return 0;
|
|
943
|
-
case "string":
|
|
944
|
-
return "";
|
|
945
|
-
case "enum":
|
|
946
|
-
return d.values?.[0] ?? "";
|
|
947
|
-
case "flags":
|
|
948
|
-
return [];
|
|
949
|
-
// A quality starts at the first rung of its ladder.
|
|
950
|
-
case "quality":
|
|
951
|
-
return d.stages?.[0] ?? "";
|
|
952
|
-
// Unreachable for a well-typed declaration, and deliberately present anyway: a bundle
|
|
953
|
-
// is DATA, and a hand-edited or newer-than-this-build one can carry a type string the
|
|
954
|
-
// union does not have. Falling off the switch would seed `undefined`, which is not a
|
|
955
|
-
// ScalarValue and travels a long way before it fails. Patterplay's copy of this had the
|
|
956
|
-
// guard and this one did not, which is the drift you only find by removing a duplicate.
|
|
957
|
-
default:
|
|
958
|
-
return false;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
39
|
+
// src/prng.ts
|
|
40
|
+
var import_expr = require("@wildwinter/expr");
|
|
961
41
|
|
|
962
42
|
// src/engine.ts
|
|
963
|
-
var tagKey = (groupId, tagId) => `${groupId}${tagId}`;
|
|
43
|
+
var tagKey = (boxId, groupId, tagId) => `${boxId}${groupId}${tagId}`;
|
|
964
44
|
var cardIsShared = (card, deckShared) => card.shared ?? deckShared;
|
|
965
45
|
var sharedCap = (card) => card.sharedCopies ?? card.copies ?? 1;
|
|
966
46
|
var OWNER = "Storylet Engine";
|
|
@@ -1005,7 +85,7 @@ function sectionsOf(p, keyOf, out) {
|
|
|
1005
85
|
for (const [id, values] of Object.entries(p[kind])) if (Object.keys(values).length > 0) out[keyOf(kind, id)] = values;
|
|
1006
86
|
}
|
|
1007
87
|
}
|
|
1008
|
-
var bagFromDecls = (decls, pathPrefix) => new PropertyBag(decls, { normalise: (n) => n, pathPrefix });
|
|
88
|
+
var bagFromDecls = (decls, pathPrefix) => new import_scoperegistry.PropertyBag(decls, { normalise: (n) => n, pathPrefix });
|
|
1009
89
|
function conditionPasses(v) {
|
|
1010
90
|
if (typeof v === "boolean") return v;
|
|
1011
91
|
if (typeof v === "number") return v !== 0;
|
|
@@ -1017,22 +97,24 @@ var isShared = (scope, d) => d.shared ?? SCOPE_DEFAULT_SHARED[scope];
|
|
|
1017
97
|
var sharedHalf = (scope, decls) => decls.filter((d) => isShared(scope, d));
|
|
1018
98
|
var flowHalf = (scope, decls) => decls.filter((d) => !isShared(scope, d));
|
|
1019
99
|
var OWNED_SCOPES = ["box", "deck", "hand", "value"];
|
|
100
|
+
var emptyOwnerIndex = () => ({ gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map(), zoneQualified: /* @__PURE__ */ new Map() });
|
|
1020
101
|
var emptyOwnerIndexes = () => ({
|
|
1021
|
-
box:
|
|
1022
|
-
deck:
|
|
1023
|
-
hand:
|
|
1024
|
-
value:
|
|
102
|
+
box: emptyOwnerIndex(),
|
|
103
|
+
deck: emptyOwnerIndex(),
|
|
104
|
+
hand: emptyOwnerIndex(),
|
|
105
|
+
value: emptyOwnerIndex()
|
|
1025
106
|
});
|
|
1026
107
|
var indexOwner = (index, entity) => {
|
|
1027
|
-
const gameId = effectiveGameId(entity);
|
|
108
|
+
const gameId = (0, import_model.effectiveGameId)(entity);
|
|
1028
109
|
index.gameId.set(entity.id, gameId);
|
|
1029
110
|
if (!index.id.has(gameId)) index.id.set(gameId, entity.id);
|
|
1030
111
|
};
|
|
1031
112
|
var indexValueOwners = (index, bundle) => {
|
|
1032
|
-
const addresses = valueAddresses(bundle);
|
|
113
|
+
const addresses = (0, import_model.valueAddresses)(bundle);
|
|
1033
114
|
for (const [id, segment] of addresses.print) index.gameId.set(id, segment);
|
|
1034
115
|
for (const [segment, id] of addresses.accept) index.id.set(segment, id);
|
|
1035
116
|
for (const [gameId, candidates] of addresses.repeated) index.repeated.set(gameId, candidates);
|
|
117
|
+
for (const [segment, zone] of addresses.zoneQualified) index.zoneQualified.set(segment, zone);
|
|
1036
118
|
};
|
|
1037
119
|
var handDeclsOf = (internals, hand) => {
|
|
1038
120
|
if (hand.template !== void 0) {
|
|
@@ -1044,6 +126,8 @@ var addressOf = (internals, kind, id) => `${kind}.${internals.owners[kind].gameI
|
|
|
1044
126
|
var resolveOwner = (internals, kind, segment) => {
|
|
1045
127
|
const candidates = internals.owners[kind].repeated.get(segment);
|
|
1046
128
|
if (candidates !== void 0) return { ambiguous: candidates };
|
|
129
|
+
const zone = internals.owners[kind].zoneQualified.get(segment);
|
|
130
|
+
if (zone !== void 0) return { zone };
|
|
1047
131
|
const byGameId = internals.owners[kind].id.get(segment);
|
|
1048
132
|
if (byGameId !== void 0) return { id: byGameId, legacy: false };
|
|
1049
133
|
if (internals.owners[kind].gameId.has(segment)) return { id: segment, legacy: true };
|
|
@@ -1052,7 +136,8 @@ var resolveOwner = (internals, kind, segment) => {
|
|
|
1052
136
|
var ownerOrThrow = (internals, kind, segment, name) => {
|
|
1053
137
|
const owner = resolveOwner(internals, kind, segment);
|
|
1054
138
|
if (owner === void 0) throw new Error(`no ${kind} store "${segment}"`);
|
|
1055
|
-
if ("ambiguous" in owner) throw new Error(ambiguousValueAddressMessage(segment, name, owner.ambiguous));
|
|
139
|
+
if ("ambiguous" in owner) throw new Error((0, import_model.ambiguousValueAddressMessage)(segment, name, owner.ambiguous));
|
|
140
|
+
if ("zone" in owner) throw new Error((0, import_model.zoneQualifiedValueAddressMessage)(segment, owner.zone, name));
|
|
1056
141
|
return owner;
|
|
1057
142
|
};
|
|
1058
143
|
var legacyAddressMessage = (internals, kind, segment, name) => `"${kind}.${segment}.${name}" names the ${kind} by its internal id; write "${addressOf(internals, kind, segment)}.${name}". The internal-id form is refused after the next release.`;
|
|
@@ -1070,9 +155,12 @@ var buildPartition = (internals, half) => {
|
|
|
1070
155
|
hand: new Map(b.boxes.flatMap((box) => box.hands.map(
|
|
1071
156
|
(hand) => [hand.id, bagFromDecls(half("hand", handDeclsOf(internals, hand)), at("hand", hand.id))]
|
|
1072
157
|
))),
|
|
1073
|
-
|
|
158
|
+
// Every box's tags, then the project map's zones ONCE (design/project-
|
|
159
|
+
// map-contract.md 3.3): a zone is one bag per partition, whichever boxes'
|
|
160
|
+
// hands are dealt to it.
|
|
161
|
+
value: new Map((0, import_model.allTagGroups)(b).flatMap((group) => group.tags.map(
|
|
1074
162
|
(tag) => [tag.id, bagFromDecls(half("value", tag.properties ?? []), at("value", tag.id))]
|
|
1075
|
-
)))
|
|
163
|
+
)))
|
|
1076
164
|
};
|
|
1077
165
|
};
|
|
1078
166
|
var partitionValues = (p) => ({
|
|
@@ -1189,6 +277,68 @@ function finishReport(bundle, saved, flows, draft) {
|
|
|
1189
277
|
retypedProperties
|
|
1190
278
|
};
|
|
1191
279
|
}
|
|
280
|
+
var refuseUnreadableBundle = (bundle) => {
|
|
281
|
+
const schema = bundle.schema;
|
|
282
|
+
if (typeof schema !== "string" || !import_model.BUNDLE_SCHEMAS.includes(schema)) {
|
|
283
|
+
throw new Error(`unsupported bundle schema: ${String(schema)} (this runtime reads ${import_model.BUNDLE_SCHEMAS.join(" and ")})`);
|
|
284
|
+
}
|
|
285
|
+
const problems = [];
|
|
286
|
+
const map = bundle.map?.group;
|
|
287
|
+
const mapName = map !== void 0 ? (0, import_model.effectiveGameId)(map) : void 0;
|
|
288
|
+
if (map !== void 0 && mapName === import_model.PLACE_GROUP) {
|
|
289
|
+
problems.push(`the project map's tag group is called "${import_model.PLACE_GROUP}", which is reserved for a box's own hands`);
|
|
290
|
+
}
|
|
291
|
+
const boxTags = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const box of bundle.boxes) {
|
|
293
|
+
for (const group of box.tagGroups) {
|
|
294
|
+
for (const tag of group.tags) {
|
|
295
|
+
const gameId = (0, import_model.effectiveGameId)(tag);
|
|
296
|
+
if (!boxTags.has(gameId)) boxTags.set(gameId, { box: (0, import_model.effectiveGameId)(box), group: (0, import_model.effectiveGameId)(group) });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const tag of map?.tags ?? []) {
|
|
301
|
+
const zone = (0, import_model.effectiveGameId)(tag);
|
|
302
|
+
const clash = boxTags.get(zone);
|
|
303
|
+
if (clash !== void 0) {
|
|
304
|
+
problems.push(`the project map's zone "${zone}" has the name of tag "${zone}" in box "${clash.box}", group "${clash.group}", so "value.${zone}.<name>" would name two things`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const box of bundle.boxes) {
|
|
308
|
+
const boxName = (0, import_model.effectiveGameId)(box);
|
|
309
|
+
if (box.usesMap === true) {
|
|
310
|
+
if (map === void 0) {
|
|
311
|
+
problems.push(`box "${boxName}" uses the project map, but the bundle has no map`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const twin = box.tagGroups.find((g) => (0, import_model.effectiveGameId)(g) === mapName);
|
|
315
|
+
if (twin !== void 0) {
|
|
316
|
+
problems.push(`box "${boxName}" uses the project map and declares its own tag group "${mapName}", the map's name`);
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (map === void 0) continue;
|
|
321
|
+
const names = (where) => {
|
|
322
|
+
problems.push(`box "${boxName}" is not on the project map, but ${where} names the map's tag group "${mapName}"`);
|
|
323
|
+
};
|
|
324
|
+
for (const deck of box.decks) {
|
|
325
|
+
for (const card of deck.cards) {
|
|
326
|
+
if (card.tags?.[map.id] !== void 0) names(`card "${(0, import_model.effectiveGameId)(card)}"`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const template of box.handTemplates) {
|
|
330
|
+
if (template.bindings?.[map.id] !== void 0 || template.chooses?.includes(map.id) === true) {
|
|
331
|
+
names(`hand template "${(0, import_model.effectiveGameId)(template)}"`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const hand of box.hands) {
|
|
335
|
+
if (hand.chosen?.[map.id] !== void 0 || hand.rule?.bindings?.[map.id] !== void 0) {
|
|
336
|
+
names(`hand "${(0, import_model.effectiveGameId)(hand)}"`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (problems.length > 0) throw new Error(`bundle refused: ${problems.join("; ")}`);
|
|
341
|
+
};
|
|
1192
342
|
var Engine = class _Engine {
|
|
1193
343
|
internals;
|
|
1194
344
|
seed;
|
|
@@ -1205,6 +355,7 @@ var Engine = class _Engine {
|
|
|
1205
355
|
* hotSwap puts this engine back exactly as it was. */
|
|
1206
356
|
sharedMounts = [];
|
|
1207
357
|
constructor(bundle, opts = {}) {
|
|
358
|
+
refuseUnreadableBundle(bundle);
|
|
1208
359
|
this.creationOptions = opts;
|
|
1209
360
|
this.seed = opts.seed ?? 0;
|
|
1210
361
|
this.onReplacedFlow = opts.onReplacedFlow;
|
|
@@ -1229,7 +380,7 @@ var Engine = class _Engine {
|
|
|
1229
380
|
flowDecls: { story: [], box: /* @__PURE__ */ new Map(), deck: /* @__PURE__ */ new Map(), hand: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() },
|
|
1230
381
|
sharedDecls: { story: [], box: /* @__PURE__ */ new Map(), deck: /* @__PURE__ */ new Map(), hand: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() },
|
|
1231
382
|
shared: void 0,
|
|
1232
|
-
registry: opts.registry ?? new ScopeRegistry(),
|
|
383
|
+
registry: opts.registry ?? new import_scoperegistry.ScopeRegistry(),
|
|
1233
384
|
ownsRegistry: opts.registry === void 0,
|
|
1234
385
|
selfWorld: false,
|
|
1235
386
|
registryView: () => view(),
|
|
@@ -1263,7 +414,7 @@ var Engine = class _Engine {
|
|
|
1263
414
|
indexValueOwners(internals.owners.value, bundle);
|
|
1264
415
|
for (const box of bundle.boxes) {
|
|
1265
416
|
internals.boxesById.set(box.id, box);
|
|
1266
|
-
internals.boxesByGameId.set(effectiveGameId(box), box);
|
|
417
|
+
internals.boxesByGameId.set((0, import_model.effectiveGameId)(box), box);
|
|
1267
418
|
indexOwner(internals.owners.box, box);
|
|
1268
419
|
for (const group of box.tagGroups) {
|
|
1269
420
|
internals.groupsById.set(group.id, { group, box });
|
|
@@ -1275,7 +426,7 @@ var Engine = class _Engine {
|
|
|
1275
426
|
for (const card of deck.cards) {
|
|
1276
427
|
const entry = { card, deck, box };
|
|
1277
428
|
internals.cardsById.set(card.id, entry);
|
|
1278
|
-
internals.cardsByGameId.set(effectiveGameId(card), entry);
|
|
429
|
+
internals.cardsByGameId.set((0, import_model.effectiveGameId)(card), entry);
|
|
1279
430
|
if (card.shared === true) internals.hasShared = true;
|
|
1280
431
|
}
|
|
1281
432
|
}
|
|
@@ -1284,10 +435,14 @@ var Engine = class _Engine {
|
|
|
1284
435
|
}
|
|
1285
436
|
for (const hand of box.hands) {
|
|
1286
437
|
internals.handsById.set(hand.id, { hand, box });
|
|
1287
|
-
internals.handsByGameId.set(effectiveGameId(hand), { hand, box });
|
|
438
|
+
internals.handsByGameId.set((0, import_model.effectiveGameId)(hand), { hand, box });
|
|
1288
439
|
indexOwner(internals.owners.hand, hand);
|
|
1289
440
|
}
|
|
1290
441
|
}
|
|
442
|
+
if (bundle.map !== void 0) {
|
|
443
|
+
internals.groupsById.set(bundle.map.group.id, { group: bundle.map.group });
|
|
444
|
+
if (bundle.map.group.required === true) internals.requiredGroups.add(bundle.map.group.id);
|
|
445
|
+
}
|
|
1291
446
|
this.initLadders();
|
|
1292
447
|
const declSet = (half) => ({
|
|
1293
448
|
story: half("story", bundle.story.properties),
|
|
@@ -1298,9 +453,9 @@ var Engine = class _Engine {
|
|
|
1298
453
|
hand: new Map(bundle.boxes.flatMap((box) => box.hands.map(
|
|
1299
454
|
(hand) => [hand.id, half("hand", handDeclsOf(internals, hand))]
|
|
1300
455
|
))),
|
|
1301
|
-
value: new Map(
|
|
456
|
+
value: new Map((0, import_model.allTagGroups)(bundle).flatMap((group) => group.tags.map(
|
|
1302
457
|
(tag) => [tag.id, half("value", tag.properties ?? [])]
|
|
1303
|
-
)))
|
|
458
|
+
)))
|
|
1304
459
|
});
|
|
1305
460
|
internals.flowDecls = declSet(flowHalf);
|
|
1306
461
|
internals.sharedDecls = declSet(sharedHalf);
|
|
@@ -1362,8 +517,8 @@ var Engine = class _Engine {
|
|
|
1362
517
|
reg.set("world", n, v, { host: true });
|
|
1363
518
|
} : void 0;
|
|
1364
519
|
} else {
|
|
1365
|
-
internals.worldSet = (n, v,
|
|
1366
|
-
reg.set("world", n, v,
|
|
520
|
+
internals.worldSet = (n, v, host) => {
|
|
521
|
+
reg.set("world", n, v, host === true ? { host: true } : void 0);
|
|
1367
522
|
};
|
|
1368
523
|
}
|
|
1369
524
|
}
|
|
@@ -1401,6 +556,7 @@ var Engine = class _Engine {
|
|
|
1401
556
|
}
|
|
1402
557
|
for (const hand of box.hands) internals.ladders.hand.set(hand.id, grab(handDeclsOf(internals, hand)));
|
|
1403
558
|
}
|
|
559
|
+
for (const tag of b.map?.group.tags ?? []) internals.ladders.value.set(tag.id, grab(tag.properties));
|
|
1404
560
|
const any = (m) => [...m.values()].some((x) => x.size > 0);
|
|
1405
561
|
internals.hasQualities = internals.ladders.world.size > 0 || internals.ladders.story.size > 0 || any(internals.ladders.box) || any(internals.ladders.deck) || any(internals.ladders.value) || any(internals.ladders.hand);
|
|
1406
562
|
}
|
|
@@ -1722,13 +878,37 @@ var Engine = class _Engine {
|
|
|
1722
878
|
* saves it once itself, beside each engine's envelope. */
|
|
1723
879
|
saveGame() {
|
|
1724
880
|
return structuredClone({
|
|
1725
|
-
schema: SAVE_SCHEMA,
|
|
881
|
+
schema: import_model2.SAVE_SCHEMA,
|
|
1726
882
|
content: this.internals.bundle.content,
|
|
1727
|
-
...this.internals.ownsRegistry ? { registry: this.
|
|
883
|
+
...this.internals.ownsRegistry ? { registry: this.registrySection() } : {},
|
|
1728
884
|
shared: { spent: [...this.spent].sort() },
|
|
1729
885
|
flows: Object.fromEntries([...this.flowsById].map(([id, flow]) => [id, flow.snapshot(false)]))
|
|
1730
886
|
});
|
|
1731
887
|
}
|
|
888
|
+
/** The registry's values in CANONICAL order, the order a load rebuilds them
|
|
889
|
+
* in: the engine-wide keys as the constructor registered them, then each
|
|
890
|
+
* flow's keys in `flows()` order (each flow's own registration order), then
|
|
891
|
+
* anything else the registry holds (values still waiting for a key), as the
|
|
892
|
+
* registry lists it. The registry itself lists keys in registration order,
|
|
893
|
+
* and a flow replaced in place (`open()` above keeps its slot in
|
|
894
|
+
* `flowsById`) re-registers its keys at the END, so `openFlow("a");
|
|
895
|
+
* openFlow("b"); openFlow("a")` saved b's keys before a's while a load
|
|
896
|
+
* rebuilt a's first: the same run, different `.storyletsave` bytes, and a
|
|
897
|
+
* save loaded and saved again no longer equal to itself. It is the
|
|
898
|
+
* 2026-08-29 rule carried into the section save@2 moved the per-flow values
|
|
899
|
+
* to (2026-10-01). Order does not matter on READ (`partitionsFromSections`
|
|
900
|
+
* sorts by key shape), so a save written in the old order loads as before. */
|
|
901
|
+
registrySection() {
|
|
902
|
+
const all = this.internals.registry.save();
|
|
903
|
+
const out = {};
|
|
904
|
+
const take = (key) => {
|
|
905
|
+
if (Object.prototype.hasOwnProperty.call(all, key) && !Object.prototype.hasOwnProperty.call(out, key)) out[key] = all[key];
|
|
906
|
+
};
|
|
907
|
+
for (const { key } of this.sharedMounts) take(key);
|
|
908
|
+
for (const flow of this.flowsById.values()) for (const key of flow.registeredKeys()) take(key);
|
|
909
|
+
for (const key of Object.keys(all)) take(key);
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
1732
912
|
/** ONE flow's blob, to park a visit that is walking away: the same shape
|
|
1733
913
|
* the envelope carries per flow, and the same shape `openFlow`'s `restore`
|
|
1734
914
|
* option takes back (design/engine-server.md 4.1). Saving the whole
|
|
@@ -1798,11 +978,11 @@ var Engine = class _Engine {
|
|
|
1798
978
|
* previewLoad and loadGame share it. */
|
|
1799
979
|
planLoad(envelope) {
|
|
1800
980
|
const schema = envelope.schema;
|
|
1801
|
-
if (schema !== SAVE_SCHEMA && schema !== SAVE_SCHEMA_V1) throw new Error(`unsupported save schema: ${String(schema)}`);
|
|
981
|
+
if (schema !== import_model2.SAVE_SCHEMA && schema !== import_model2.SAVE_SCHEMA_V1) throw new Error(`unsupported save schema: ${String(schema)}`);
|
|
1802
982
|
const draft = emptyDraft();
|
|
1803
983
|
const flowIds = new Set(Object.keys(envelope.flows ?? {}));
|
|
1804
984
|
let moved;
|
|
1805
|
-
if (envelope.schema === SAVE_SCHEMA_V1) {
|
|
985
|
+
if (envelope.schema === import_model2.SAVE_SCHEMA_V1) {
|
|
1806
986
|
moved = {
|
|
1807
987
|
shared: envelope.shared?.props ?? emptyPartitionValues(),
|
|
1808
988
|
flows: new Map(Object.entries(envelope.flows ?? {}).map(([id, f]) => [id, f.props ?? emptyPartitionValues()])),
|
|
@@ -1852,7 +1032,7 @@ var Engine = class _Engine {
|
|
|
1852
1032
|
}
|
|
1853
1033
|
const cardName = (cardId) => {
|
|
1854
1034
|
const entry = internals.cardsById.get(cardId);
|
|
1855
|
-
return entry ? effectiveGameId(entry.card) : cardId;
|
|
1035
|
+
return entry ? (0, import_model.effectiveGameId)(entry.card) : cardId;
|
|
1856
1036
|
};
|
|
1857
1037
|
const board = {};
|
|
1858
1038
|
const restored = /* @__PURE__ */ new Map();
|
|
@@ -1864,7 +1044,7 @@ var Engine = class _Engine {
|
|
|
1864
1044
|
}
|
|
1865
1045
|
continue;
|
|
1866
1046
|
}
|
|
1867
|
-
const hand = effectiveGameId(known.hand);
|
|
1047
|
+
const hand = (0, import_model.effectiveGameId)(known.hand);
|
|
1868
1048
|
const kept = [];
|
|
1869
1049
|
for (const cardId of ids) {
|
|
1870
1050
|
const entry = internals.cardsById.get(cardId);
|
|
@@ -1875,7 +1055,7 @@ var Engine = class _Engine {
|
|
|
1875
1055
|
if (otherClaims !== void 0 && cardIsShared(entry.card, entry.deck.shared ?? false)) {
|
|
1876
1056
|
const held = (otherClaims.get(cardId) ?? 0) + (restored.get(cardId) ?? 0);
|
|
1877
1057
|
if (held >= sharedCap(entry.card)) {
|
|
1878
|
-
draft.evicted.push({ flow: id, hand, card: effectiveGameId(entry.card), reason: "claimed-elsewhere" });
|
|
1058
|
+
draft.evicted.push({ flow: id, hand, card: (0, import_model.effectiveGameId)(entry.card), reason: "claimed-elsewhere" });
|
|
1879
1059
|
continue;
|
|
1880
1060
|
}
|
|
1881
1061
|
restored.set(cardId, (restored.get(cardId) ?? 0) + 1);
|
|
@@ -1949,7 +1129,7 @@ var Flow = class {
|
|
|
1949
1129
|
this.engine = engine;
|
|
1950
1130
|
this.internals = internals;
|
|
1951
1131
|
this.id = id;
|
|
1952
|
-
this.prng = makePrng(seed);
|
|
1132
|
+
this.prng = (0, import_expr.makePrng)(seed);
|
|
1953
1133
|
this.stores = buildPartition(internals, flowHalf);
|
|
1954
1134
|
const put = (key, bag) => {
|
|
1955
1135
|
if (bag.declarations().length === 0) return;
|
|
@@ -2050,7 +1230,7 @@ var Flow = class {
|
|
|
2050
1230
|
node(expr) {
|
|
2051
1231
|
let node = this.internals.nodeCache.get(expr);
|
|
2052
1232
|
if (!node) {
|
|
2053
|
-
node = deserialiseAst(expr.ast);
|
|
1233
|
+
node = (0, import_expr2.deserialiseAst)(expr.ast);
|
|
2054
1234
|
this.internals.nodeCache.set(expr, node);
|
|
2055
1235
|
}
|
|
2056
1236
|
return node;
|
|
@@ -2058,9 +1238,16 @@ var Flow = class {
|
|
|
2058
1238
|
/** Tag group names are box-scoped: two boxes may name a group the same way
|
|
2059
1239
|
* (schema 1 - boxes namespace their groups), so a name is only ever
|
|
2060
1240
|
* resolved inside the box being asked, never bundle-wide. Ids are
|
|
2061
|
-
* project-unique and accepted here too, still confined to the box.
|
|
1241
|
+
* project-unique and accepted here too, still confined to the box.
|
|
1242
|
+
*
|
|
1243
|
+
* A box on the project map sees ONE namespace: its own groups, then the
|
|
1244
|
+
* map's group (design/project-map-contract.md 3.1). A box that has not
|
|
1245
|
+
* opted in does not see the map's name at all, so a peek naming it there
|
|
1246
|
+
* is the ordinary unknown-group refusal. Own groups first is stated for
|
|
1247
|
+
* determinism only: a bundle that loads never has the two share a name. */
|
|
2062
1248
|
groupInBox(box, ref) {
|
|
2063
|
-
|
|
1249
|
+
const groups = (0, import_model.groupsOfBox)(this.internals.bundle, box);
|
|
1250
|
+
return groups.find((g) => (0, import_model.effectiveGameId)(g) === ref) ?? groups.find((g) => g.id === ref);
|
|
2064
1251
|
}
|
|
2065
1252
|
/** Fold one play into the indexes. O(the card's tags), not O(the log). */
|
|
2066
1253
|
indexPlay(record) {
|
|
@@ -2070,7 +1257,7 @@ var Flow = class {
|
|
|
2070
1257
|
if (!entry) return;
|
|
2071
1258
|
for (const [groupId, tagIds] of Object.entries(entry.card.tags ?? {})) {
|
|
2072
1259
|
for (const tagId of tagIds) {
|
|
2073
|
-
const key = tagKey(groupId, tagId);
|
|
1260
|
+
const key = tagKey(entry.box.id, groupId, tagId);
|
|
2074
1261
|
this.tagPlayCount.set(key, (this.tagPlayCount.get(key) ?? 0) + 1);
|
|
2075
1262
|
this.lastPlayInTag.set(key, record);
|
|
2076
1263
|
}
|
|
@@ -2086,8 +1273,10 @@ var Flow = class {
|
|
|
2086
1273
|
for (const record of this.playLog) this.indexPlay(record);
|
|
2087
1274
|
}
|
|
2088
1275
|
/** `box` is the box whose ask is being evaluated: the play-history
|
|
2089
|
-
* functions take a bare group name, so it resolves there
|
|
2090
|
-
*
|
|
1276
|
+
* functions take a bare group name, so it resolves there, and they count
|
|
1277
|
+
* only that box's own plays (the box is in the index key). That was
|
|
1278
|
+
* automatic while every group was a box's; a project-map zone is shared,
|
|
1279
|
+
* and its history is still not (design/project-map-contract.md 3.7, D7).
|
|
2091
1280
|
* History is THIS flow's: countPlayed answers "have I done this". */
|
|
2092
1281
|
/** One host per box, built once.
|
|
2093
1282
|
*
|
|
@@ -2112,11 +1301,11 @@ var Flow = class {
|
|
|
2112
1301
|
const keyOf = (group, tag) => {
|
|
2113
1302
|
const found = this.groupInBox(box, group);
|
|
2114
1303
|
const t = found?.tags.find((v) => v.gameId === tag);
|
|
2115
|
-
return found && t ? tagKey(found.id, t.id) : void 0;
|
|
1304
|
+
return found && t ? tagKey(box.id, found.id, t.id) : void 0;
|
|
2116
1305
|
};
|
|
2117
1306
|
const since = (record) => {
|
|
2118
1307
|
const entry = this.internals.cardsByGameId.get(record.card);
|
|
2119
|
-
if (!entry) return NEVER_PLAYED;
|
|
1308
|
+
if (!entry) return import_dialect.NEVER_PLAYED;
|
|
2120
1309
|
return (this.turnCounts.get(entry.box.id) ?? 0) - record.turn;
|
|
2121
1310
|
};
|
|
2122
1311
|
return {
|
|
@@ -2124,7 +1313,7 @@ var Flow = class {
|
|
|
2124
1313
|
countPlayed: (card) => this.playCount.get(card) ?? 0,
|
|
2125
1314
|
turnsSincePlayed: (card) => {
|
|
2126
1315
|
const last = this.lastPlayOf.get(card);
|
|
2127
|
-
return last ? since(last) : NEVER_PLAYED;
|
|
1316
|
+
return last ? since(last) : import_dialect.NEVER_PLAYED;
|
|
2128
1317
|
},
|
|
2129
1318
|
countPlayedIn: (group, tag) => {
|
|
2130
1319
|
const key = keyOf(group, tag);
|
|
@@ -2133,7 +1322,7 @@ var Flow = class {
|
|
|
2133
1322
|
turnsSincePlayedIn: (group, tag) => {
|
|
2134
1323
|
const key = keyOf(group, tag);
|
|
2135
1324
|
const last = key === void 0 ? void 0 : this.lastPlayInTag.get(key);
|
|
2136
|
-
return last ? since(last) : NEVER_PLAYED;
|
|
1325
|
+
return last ? since(last) : import_dialect.NEVER_PLAYED;
|
|
2137
1326
|
}
|
|
2138
1327
|
};
|
|
2139
1328
|
}
|
|
@@ -2172,7 +1361,7 @@ var Flow = class {
|
|
|
2172
1361
|
return source.kind === "value" ? this.internals.ladders.value.get(source.id)?.get(name) : source.kind === "hand" ? this.internals.ladders.hand.get(source.id)?.get(name) : void 0;
|
|
2173
1362
|
}
|
|
2174
1363
|
eval(expr, ctx) {
|
|
2175
|
-
return evaluate(this.node(expr), ctx, storyletsDialect);
|
|
1364
|
+
return (0, import_expr2.evaluate)(this.node(expr), ctx, import_dialect.storyletsDialect);
|
|
2176
1365
|
}
|
|
2177
1366
|
passes(expr, ctx, where) {
|
|
2178
1367
|
if (!expr) return true;
|
|
@@ -2197,35 +1386,35 @@ var Flow = class {
|
|
|
2197
1386
|
let condition;
|
|
2198
1387
|
if (hand.template !== void 0) {
|
|
2199
1388
|
const template = this.internals.templatesById.get(hand.template);
|
|
2200
|
-
if (!template) throw new Error(`hand "${effectiveGameId(hand)}": unknown template "${hand.template}"`);
|
|
1389
|
+
if (!template) throw new Error(`hand "${(0, import_model.effectiveGameId)(hand)}": unknown template "${hand.template}"`);
|
|
2201
1390
|
for (const [groupId, tagId] of Object.entries(template.bindings ?? {})) {
|
|
2202
1391
|
boundTags.set(groupId, tagId);
|
|
2203
1392
|
}
|
|
2204
1393
|
for (const [groupId, tagId] of Object.entries(hand.chosen ?? {})) {
|
|
2205
|
-
if (isHoleRef(tagId)) {
|
|
1394
|
+
if ((0, import_model.isHoleRef)(tagId)) {
|
|
2206
1395
|
this.fillHoleFromProperty(hand, groupId, tagId, boundTags, askNames);
|
|
2207
1396
|
continue;
|
|
2208
1397
|
}
|
|
2209
1398
|
boundTags.set(groupId, tagId);
|
|
2210
1399
|
const found = this.internals.groupsById.get(groupId);
|
|
2211
1400
|
const tag = found?.group.tags.find((t) => t.id === tagId);
|
|
2212
|
-
if (found && tag) askNames[effectiveGameId(found.group)] = effectiveGameId(tag);
|
|
1401
|
+
if (found && tag) askNames[(0, import_model.effectiveGameId)(found.group)] = (0, import_model.effectiveGameId)(tag);
|
|
2213
1402
|
}
|
|
2214
1403
|
condition = template.condition;
|
|
2215
1404
|
} else {
|
|
2216
1405
|
for (const [groupId, tagId] of Object.entries(hand.rule?.bindings ?? {})) {
|
|
2217
|
-
if (isHoleRef(tagId)) {
|
|
1406
|
+
if ((0, import_model.isHoleRef)(tagId)) {
|
|
2218
1407
|
this.fillHoleFromProperty(hand, groupId, tagId, boundTags, askNames);
|
|
2219
1408
|
continue;
|
|
2220
1409
|
}
|
|
2221
1410
|
boundTags.set(groupId, tagId);
|
|
2222
1411
|
const found = this.internals.groupsById.get(groupId);
|
|
2223
1412
|
const tag = found?.group.tags.find((t) => t.id === tagId);
|
|
2224
|
-
if (found && tag) askNames[effectiveGameId(found.group)] = effectiveGameId(tag);
|
|
1413
|
+
if (found && tag) askNames[(0, import_model.effectiveGameId)(found.group)] = (0, import_model.effectiveGameId)(tag);
|
|
2225
1414
|
}
|
|
2226
1415
|
condition = hand.rule?.condition;
|
|
2227
1416
|
}
|
|
2228
|
-
boundTags.set(PLACE_GROUP, hand.id);
|
|
1417
|
+
boundTags.set(import_model.PLACE_GROUP, hand.id);
|
|
2229
1418
|
this.bindStateGroups(box, boundTags, askNames);
|
|
2230
1419
|
return { box, hand, ...condition !== void 0 ? { condition } : {}, boundTags, askNames };
|
|
2231
1420
|
}
|
|
@@ -2235,20 +1424,20 @@ var Flow = class {
|
|
|
2235
1424
|
const boundTags = /* @__PURE__ */ new Map();
|
|
2236
1425
|
const askNames = {};
|
|
2237
1426
|
for (const [groupRef, tagRef] of Object.entries(criteria)) {
|
|
2238
|
-
if (groupRef === PLACE_GROUP) {
|
|
1427
|
+
if (groupRef === import_model.PLACE_GROUP) {
|
|
2239
1428
|
const hand = this.internals.handsByGameId.get(tagRef) ?? this.internals.handsById.get(tagRef);
|
|
2240
1429
|
if (!hand) throw new Error(`peek: unknown hand "${tagRef}" in home criteria`);
|
|
2241
|
-
boundTags.set(PLACE_GROUP, hand.hand.id);
|
|
1430
|
+
boundTags.set(import_model.PLACE_GROUP, hand.hand.id);
|
|
2242
1431
|
continue;
|
|
2243
1432
|
}
|
|
2244
1433
|
const found = this.groupInBox(box, groupRef);
|
|
2245
1434
|
if (!found) {
|
|
2246
|
-
throw new Error(`peek: unknown tag group "${groupRef}" in box "${effectiveGameId(box)}"`);
|
|
1435
|
+
throw new Error(`peek: unknown tag group "${groupRef}" in box "${(0, import_model.effectiveGameId)(box)}"`);
|
|
2247
1436
|
}
|
|
2248
1437
|
const tag = this.tagByGameId(found, tagRef) ?? found.tags.find((t) => t.id === tagRef);
|
|
2249
|
-
if (!tag) throw new Error(`peek: unknown tag "${tagRef}" in group "${effectiveGameId(found)}"`);
|
|
1438
|
+
if (!tag) throw new Error(`peek: unknown tag "${tagRef}" in group "${(0, import_model.effectiveGameId)(found)}"`);
|
|
2250
1439
|
boundTags.set(found.id, tag.id);
|
|
2251
|
-
askNames[effectiveGameId(found)] = effectiveGameId(tag);
|
|
1440
|
+
askNames[(0, import_model.effectiveGameId)(found)] = (0, import_model.effectiveGameId)(tag);
|
|
2252
1441
|
}
|
|
2253
1442
|
this.bindStateGroups(box, boundTags, askNames);
|
|
2254
1443
|
return { box, boundTags, askNames };
|
|
@@ -2271,9 +1460,9 @@ var Flow = class {
|
|
|
2271
1460
|
*/
|
|
2272
1461
|
fillHoleFromProperty(hand, groupId, ref, boundTags, askNames) {
|
|
2273
1462
|
const found = this.internals.groupsById.get(groupId);
|
|
2274
|
-
const groupName = found ? effectiveGameId(found.group) : groupId;
|
|
2275
|
-
const where = `hand ${effectiveGameId(hand)}, tag group ${groupName}`;
|
|
2276
|
-
const parsed = parseHoleRef(ref);
|
|
1463
|
+
const groupName = found ? (0, import_model.effectiveGameId)(found.group) : groupId;
|
|
1464
|
+
const where = `hand ${(0, import_model.effectiveGameId)(hand)}, tag group ${groupName}`;
|
|
1465
|
+
const parsed = (0, import_model.parseHoleRef)(ref);
|
|
2277
1466
|
if (!parsed) {
|
|
2278
1467
|
this.emit({ type: "diagnostic", where, message: `"${ref}" is not a @hand, @world or @story property reference` });
|
|
2279
1468
|
return;
|
|
@@ -2297,13 +1486,13 @@ var Flow = class {
|
|
|
2297
1486
|
return;
|
|
2298
1487
|
}
|
|
2299
1488
|
const wanted = typeof value === "string" ? value : String(value);
|
|
2300
|
-
const tag = found.group.tags.find((t) => effectiveGameId(t) === wanted);
|
|
1489
|
+
const tag = found.group.tags.find((t) => (0, import_model.effectiveGameId)(t) === wanted);
|
|
2301
1490
|
if (!tag) {
|
|
2302
1491
|
this.emit({ type: "diagnostic", where, message: `${ref} is "${wanted}", which is not one of the tags of "${groupName}"` });
|
|
2303
1492
|
return;
|
|
2304
1493
|
}
|
|
2305
1494
|
boundTags.set(groupId, tag.id);
|
|
2306
|
-
askNames[groupName] = effectiveGameId(tag);
|
|
1495
|
+
askNames[groupName] = (0, import_model.effectiveGameId)(tag);
|
|
2307
1496
|
}
|
|
2308
1497
|
/**
|
|
2309
1498
|
* Bind every `boundBy` group in the box from the property it names.
|
|
@@ -2319,28 +1508,28 @@ var Flow = class {
|
|
|
2319
1508
|
* is what says otherwise.
|
|
2320
1509
|
*/
|
|
2321
1510
|
bindStateGroups(box, boundTags, askNames) {
|
|
2322
|
-
for (const group of
|
|
1511
|
+
for (const group of (0, import_model.groupsOfBox)(this.internals.bundle, box)) {
|
|
2323
1512
|
if (group.boundBy === void 0 || boundTags.has(group.id)) continue;
|
|
2324
1513
|
const ref = /^@(world|story)\.([a-z][a-z0-9_-]*)$/.exec(group.boundBy);
|
|
2325
1514
|
if (!ref) {
|
|
2326
|
-
this.emit({ type: "diagnostic", where: `tag group ${effectiveGameId(group)}`, message: `boundBy "${group.boundBy}" is not a @world or @story property reference` });
|
|
1515
|
+
this.emit({ type: "diagnostic", where: `tag group ${(0, import_model.effectiveGameId)(group)}`, message: `boundBy "${group.boundBy}" is not a @world or @story property reference` });
|
|
2327
1516
|
continue;
|
|
2328
1517
|
}
|
|
2329
1518
|
let value;
|
|
2330
1519
|
try {
|
|
2331
1520
|
value = this.getProperty(`${ref[1]}.${ref[2]}`);
|
|
2332
1521
|
} catch {
|
|
2333
|
-
this.emit({ type: "diagnostic", where: `tag group ${effectiveGameId(group)}`, message: `boundBy "${group.boundBy}" names a property that is not declared` });
|
|
1522
|
+
this.emit({ type: "diagnostic", where: `tag group ${(0, import_model.effectiveGameId)(group)}`, message: `boundBy "${group.boundBy}" names a property that is not declared` });
|
|
2334
1523
|
continue;
|
|
2335
1524
|
}
|
|
2336
1525
|
const wanted = typeof value === "string" ? value : String(value);
|
|
2337
|
-
const tag = group.tags.find((t) => effectiveGameId(t) === wanted);
|
|
1526
|
+
const tag = group.tags.find((t) => (0, import_model.effectiveGameId)(t) === wanted);
|
|
2338
1527
|
if (!tag) {
|
|
2339
|
-
this.emit({ type: "diagnostic", where: `tag group ${effectiveGameId(group)}`, message: `${group.boundBy} is "${wanted}", which is not one of its tags` });
|
|
1528
|
+
this.emit({ type: "diagnostic", where: `tag group ${(0, import_model.effectiveGameId)(group)}`, message: `${group.boundBy} is "${wanted}", which is not one of its tags` });
|
|
2340
1529
|
continue;
|
|
2341
1530
|
}
|
|
2342
1531
|
boundTags.set(group.id, tag.id);
|
|
2343
|
-
askNames[effectiveGameId(group)] = effectiveGameId(tag);
|
|
1532
|
+
askNames[(0, import_model.effectiveGameId)(group)] = (0, import_model.effectiveGameId)(tag);
|
|
2344
1533
|
}
|
|
2345
1534
|
}
|
|
2346
1535
|
// --- @hand composition (schema 3.6) -------------------------------------------
|
|
@@ -2357,7 +1546,7 @@ var Flow = class {
|
|
|
2357
1546
|
const bag = {};
|
|
2358
1547
|
const sources = /* @__PURE__ */ new Map();
|
|
2359
1548
|
for (const [groupId, tagId] of ask.boundTags) {
|
|
2360
|
-
if (groupId === PLACE_GROUP) continue;
|
|
1549
|
+
if (groupId === import_model.PLACE_GROUP) continue;
|
|
2361
1550
|
for (const [name, value] of Object.entries(this.valuesOf("value", tagId))) {
|
|
2362
1551
|
bag[name] = value;
|
|
2363
1552
|
sources.set(name, { kind: "value", id: tagId });
|
|
@@ -2412,13 +1601,13 @@ var Flow = class {
|
|
|
2412
1601
|
* the bound tag or omits the group (wildcard); the home group inverts -
|
|
2413
1602
|
* a homed card requires a matching home binding (schema 2.4). */
|
|
2414
1603
|
tagsMatch(card, boundTags) {
|
|
2415
|
-
const home = card.tags?.[PLACE_GROUP];
|
|
1604
|
+
const home = card.tags?.[import_model.PLACE_GROUP];
|
|
2416
1605
|
if (home !== void 0 && home.length > 0) {
|
|
2417
|
-
const bound = boundTags.get(PLACE_GROUP);
|
|
1606
|
+
const bound = boundTags.get(import_model.PLACE_GROUP);
|
|
2418
1607
|
if (bound === void 0 || !home.includes(bound)) return false;
|
|
2419
1608
|
}
|
|
2420
1609
|
for (const [groupId, tagId] of boundTags) {
|
|
2421
|
-
if (groupId === PLACE_GROUP) continue;
|
|
1610
|
+
if (groupId === import_model.PLACE_GROUP) continue;
|
|
2422
1611
|
const tags = card.tags?.[groupId];
|
|
2423
1612
|
if (tags === void 0) {
|
|
2424
1613
|
if (this.internals.requiredGroups.has(groupId)) return false;
|
|
@@ -2436,9 +1625,9 @@ var Flow = class {
|
|
|
2436
1625
|
const { box } = ask;
|
|
2437
1626
|
const handEnv = this.buildHandEnv(ask);
|
|
2438
1627
|
const verdict = (card, v) => {
|
|
2439
|
-
trace?.push({ id: effectiveGameId(card), verdict: v });
|
|
1628
|
+
trace?.push({ id: (0, import_model.effectiveGameId)(card), verdict: v });
|
|
2440
1629
|
};
|
|
2441
|
-
if (!this.passes(ask.condition, this.evalCtx(box, void 0, handEnv), `hand ${ask.hand ? effectiveGameId(ask.hand) : ""} condition`)) {
|
|
1630
|
+
if (!this.passes(ask.condition, this.evalCtx(box, void 0, handEnv), `hand ${ask.hand ? (0, import_model.effectiveGameId)(ask.hand) : ""} condition`)) {
|
|
2442
1631
|
return { ordered: [], handEnv };
|
|
2443
1632
|
}
|
|
2444
1633
|
const gateOk = /* @__PURE__ */ new Map();
|
|
@@ -2503,9 +1692,9 @@ var Flow = class {
|
|
|
2503
1692
|
let spec = 0;
|
|
2504
1693
|
if (box.ranking.specificity && card.condition) {
|
|
2505
1694
|
const node = this.node(card.condition);
|
|
2506
|
-
spec = matchedSpecificity(node, (n) => {
|
|
1695
|
+
spec = (0, import_expr_specificity.matchedSpecificity)(node, (n) => {
|
|
2507
1696
|
try {
|
|
2508
|
-
return conditionPasses(evaluate(n, deckCtx, storyletsDialect));
|
|
1697
|
+
return conditionPasses((0, import_expr2.evaluate)(n, deckCtx, import_dialect.storyletsDialect));
|
|
2509
1698
|
} catch {
|
|
2510
1699
|
return false;
|
|
2511
1700
|
}
|
|
@@ -2521,13 +1710,13 @@ var Flow = class {
|
|
|
2521
1710
|
while (j < scored.length && scored[j].priority === scored[i].priority && scored[j].spec === scored[i].spec) j++;
|
|
2522
1711
|
if (j - i > 1) {
|
|
2523
1712
|
const run = scored.slice(i, j);
|
|
2524
|
-
shuffleInPlace(run, this.prng);
|
|
1713
|
+
(0, import_expr.shuffleInPlace)(run, this.prng);
|
|
2525
1714
|
for (let k = 0; k < run.length; k++) scored[i + k] = run[k];
|
|
2526
1715
|
}
|
|
2527
1716
|
i = j;
|
|
2528
1717
|
}
|
|
2529
1718
|
for (const s of scored) {
|
|
2530
|
-
trace?.push({ id: effectiveGameId(s.entry.card), verdict: "dealt", priority: s.priority, specificity: s.spec });
|
|
1719
|
+
trace?.push({ id: (0, import_model.effectiveGameId)(s.entry.card), verdict: "dealt", priority: s.priority, specificity: s.spec });
|
|
2531
1720
|
}
|
|
2532
1721
|
return { ordered: scored.map((s) => s.entry), handEnv };
|
|
2533
1722
|
}
|
|
@@ -2543,7 +1732,7 @@ var Flow = class {
|
|
|
2543
1732
|
const { card } = entry;
|
|
2544
1733
|
return {
|
|
2545
1734
|
id: card.id,
|
|
2546
|
-
gameId: effectiveGameId(card),
|
|
1735
|
+
gameId: (0, import_model.effectiveGameId)(card),
|
|
2547
1736
|
...card.title !== void 0 ? { title: card.title } : {},
|
|
2548
1737
|
...card.purpose !== void 0 ? { purpose: card.purpose } : {},
|
|
2549
1738
|
...card.fields !== void 0 ? { fields: card.fields } : {}
|
|
@@ -2574,16 +1763,16 @@ var Flow = class {
|
|
|
2574
1763
|
const { ordered } = this.runAsk(ask, (card, shared) => this.claimVerdict(card, shared, claimCounts, worldClaims), trace);
|
|
2575
1764
|
const listed = n === void 0 ? ordered : ordered.slice(0, Math.max(n, 0));
|
|
2576
1765
|
if (trace) {
|
|
2577
|
-
this.capTrace(trace, new Set(listed.map((e) => effectiveGameId(e.card))));
|
|
2578
|
-
this.emit({ type: "peek", box: effectiveGameId(box), criteria, cards: trace }, this.turnCounts.get(box.id) ?? 0);
|
|
1766
|
+
this.capTrace(trace, new Set(listed.map((e) => (0, import_model.effectiveGameId)(e.card))));
|
|
1767
|
+
this.emit({ type: "peek", box: (0, import_model.effectiveGameId)(box), criteria, cards: trace }, this.turnCounts.get(box.id) ?? 0);
|
|
2579
1768
|
}
|
|
2580
|
-
return { box: effectiveGameId(box), cards: listed.map((e) => this.view(e)) };
|
|
1769
|
+
return { box: (0, import_model.effectiveGameId)(box), cards: listed.map((e) => this.view(e)) };
|
|
2581
1770
|
}
|
|
2582
1771
|
/** Refresh one hand (schema 3.5); returns its new shape. */
|
|
2583
1772
|
deal(handRef) {
|
|
2584
1773
|
this.assertOpen();
|
|
2585
1774
|
const { hand } = this.resolveHand(handRef);
|
|
2586
|
-
return this.dealMany([handRef])[effectiveGameId(hand)] ?? [];
|
|
1775
|
+
return this.dealMany([handRef])[(0, import_model.effectiveGameId)(hand)] ?? [];
|
|
2587
1776
|
}
|
|
2588
1777
|
/** Re-deal several / all hands (schema 3.5): seeded hand-order shuffle
|
|
2589
1778
|
* (fairness), evict, seed the ledger from survivors, fill in order.
|
|
@@ -2592,7 +1781,7 @@ var Flow = class {
|
|
|
2592
1781
|
dealMany(handRefs) {
|
|
2593
1782
|
this.assertOpen();
|
|
2594
1783
|
const dealt = (handRefs ?? [...this.internals.handsById.keys()].sort()).map((ref) => this.resolveHand(ref));
|
|
2595
|
-
shuffleInPlace(dealt, this.prng);
|
|
1784
|
+
(0, import_expr.shuffleInPlace)(dealt, this.prng);
|
|
2596
1785
|
for (const { hand, box } of dealt) {
|
|
2597
1786
|
const ask = this.askForHand(hand, box);
|
|
2598
1787
|
const handEnv = this.buildHandEnv(ask);
|
|
@@ -2605,7 +1794,7 @@ var Flow = class {
|
|
|
2605
1794
|
const evicted = [];
|
|
2606
1795
|
const evict = (cardId, reason) => {
|
|
2607
1796
|
const known = this.internals.cardsById.get(cardId);
|
|
2608
|
-
evicted.push({ card: known ? effectiveGameId(known.card) : cardId, reason });
|
|
1797
|
+
evicted.push({ card: known ? (0, import_model.effectiveGameId)(known.card) : cardId, reason });
|
|
2609
1798
|
return false;
|
|
2610
1799
|
};
|
|
2611
1800
|
const survivors = (this.boardContents.get(hand.id) ?? []).filter((cardId) => {
|
|
@@ -2622,7 +1811,7 @@ var Flow = class {
|
|
|
2622
1811
|
});
|
|
2623
1812
|
this.boardContents.set(hand.id, survivors);
|
|
2624
1813
|
if (this.tracing) {
|
|
2625
|
-
for (const e of evicted) this.emit({ type: "evict", hand: effectiveGameId(hand), card: e.card, reason: e.reason }, turn);
|
|
1814
|
+
for (const e of evicted) this.emit({ type: "evict", hand: (0, import_model.effectiveGameId)(hand), card: e.card, reason: e.reason }, turn);
|
|
2626
1815
|
}
|
|
2627
1816
|
}
|
|
2628
1817
|
const claimCounts = this.claims();
|
|
@@ -2647,12 +1836,12 @@ var Flow = class {
|
|
|
2647
1836
|
worldClaims.set(id, (worldClaims.get(id) ?? 0) + 1);
|
|
2648
1837
|
}
|
|
2649
1838
|
if (trace) {
|
|
2650
|
-
this.capTrace(trace, new Set(taking.map((e) => effectiveGameId(e.card))));
|
|
2651
|
-
this.emit({ type: "deal", hand: effectiveGameId(hand), cards: trace }, this.turnCounts.get(box.id) ?? 0);
|
|
1839
|
+
this.capTrace(trace, new Set(taking.map((e) => (0, import_model.effectiveGameId)(e.card))));
|
|
1840
|
+
this.emit({ type: "deal", hand: (0, import_model.effectiveGameId)(hand), cards: trace }, this.turnCounts.get(box.id) ?? 0);
|
|
2652
1841
|
}
|
|
2653
1842
|
}
|
|
2654
1843
|
return Object.fromEntries(dealt.map(({ hand }) => [
|
|
2655
|
-
effectiveGameId(hand),
|
|
1844
|
+
(0, import_model.effectiveGameId)(hand),
|
|
2656
1845
|
(this.boardContents.get(hand.id) ?? []).map((id) => this.view(this.internals.cardsById.get(id)))
|
|
2657
1846
|
]));
|
|
2658
1847
|
}
|
|
@@ -2674,7 +1863,7 @@ var Flow = class {
|
|
|
2674
1863
|
keep = box.id;
|
|
2675
1864
|
}
|
|
2676
1865
|
return Object.fromEntries([...this.boardContents.entries()].filter(([handId]) => keep === void 0 || this.internals.handsById.get(handId).box.id === keep).map(([handId, ids]) => [
|
|
2677
|
-
effectiveGameId(this.internals.handsById.get(handId).hand),
|
|
1866
|
+
(0, import_model.effectiveGameId)(this.internals.handsById.get(handId).hand),
|
|
2678
1867
|
ids.map((id) => this.view(this.internals.cardsById.get(id)))
|
|
2679
1868
|
]));
|
|
2680
1869
|
}
|
|
@@ -2684,7 +1873,7 @@ var Flow = class {
|
|
|
2684
1873
|
if (!entry) throw new Error(`unknown card "${cardId}"`);
|
|
2685
1874
|
const { hand, box } = this.resolveHand(handRef);
|
|
2686
1875
|
if (!(this.boardContents.get(hand.id) ?? []).includes(entry.card.id)) {
|
|
2687
|
-
throw new Error(`card "${effectiveGameId(entry.card)}" is not dealt to hand "${effectiveGameId(hand)}"`);
|
|
1876
|
+
throw new Error(`card "${(0, import_model.effectiveGameId)(entry.card)}" is not dealt to hand "${(0, import_model.effectiveGameId)(hand)}"`);
|
|
2688
1877
|
}
|
|
2689
1878
|
return { entry, ask: this.askForHand(hand, box) };
|
|
2690
1879
|
}
|
|
@@ -2696,7 +1885,7 @@ var Flow = class {
|
|
|
2696
1885
|
const ctx = this.evalCtx(entry.box, entry.deck, this.buildHandEnv(ask));
|
|
2697
1886
|
return entry.card.outcomes.map((o) => ({
|
|
2698
1887
|
id: o.id,
|
|
2699
|
-
gameId: effectiveGameId(o),
|
|
1888
|
+
gameId: (0, import_model.effectiveGameId)(o),
|
|
2700
1889
|
...o.title !== void 0 ? { title: o.title } : {},
|
|
2701
1890
|
...o.purpose !== void 0 ? { purpose: o.purpose } : {},
|
|
2702
1891
|
...o.fields !== void 0 ? { fields: o.fields } : {},
|
|
@@ -2720,14 +1909,14 @@ var Flow = class {
|
|
|
2720
1909
|
const { entry, ask } = this.resolveDealt(cardId, from);
|
|
2721
1910
|
const bare = outcomeGameId === "";
|
|
2722
1911
|
if (bare && entry.card.outcomes.length > 0) {
|
|
2723
|
-
throw new Error(`card "${effectiveGameId(entry.card)}" has outcomes (${entry.card.outcomes.map((o) => effectiveGameId(o)).join(", ")}); name the one played`);
|
|
1912
|
+
throw new Error(`card "${(0, import_model.effectiveGameId)(entry.card)}" has outcomes (${entry.card.outcomes.map((o) => (0, import_model.effectiveGameId)(o)).join(", ")}); name the one played`);
|
|
2724
1913
|
}
|
|
2725
|
-
const outcome = bare ? void 0 : entry.card.outcomes.find((o) => effectiveGameId(o) === outcomeGameId);
|
|
2726
|
-
if (!bare && !outcome) throw new Error(`card "${effectiveGameId(entry.card)}" has no outcome "${outcomeGameId}"`);
|
|
1914
|
+
const outcome = bare ? void 0 : entry.card.outcomes.find((o) => (0, import_model.effectiveGameId)(o) === outcomeGameId);
|
|
1915
|
+
if (!bare && !outcome) throw new Error(`card "${(0, import_model.effectiveGameId)(entry.card)}" has no outcome "${outcomeGameId}"`);
|
|
2727
1916
|
const handEnv = this.buildHandEnv(ask);
|
|
2728
1917
|
const ctx = this.evalCtx(entry.box, entry.deck, handEnv);
|
|
2729
1918
|
if (outcome && !this.passes(outcome.condition, ctx)) {
|
|
2730
|
-
throw new Error(`outcome "${outcomeGameId}" on "${effectiveGameId(entry.card)}" is gated shut`);
|
|
1919
|
+
throw new Error(`outcome "${outcomeGameId}" on "${(0, import_model.effectiveGameId)(entry.card)}" is gated shut`);
|
|
2731
1920
|
}
|
|
2732
1921
|
const perPlay = entry.box.turn !== void 0 ? 0 : this.internals.bundle.settings.playAdvancesTurns;
|
|
2733
1922
|
const newTurn = (this.turnCounts.get(entry.box.id) ?? 0) + (opts.advanceTurns ?? perPlay);
|
|
@@ -2739,8 +1928,8 @@ var Flow = class {
|
|
|
2739
1928
|
const { path, prev } = this.applyWrite(target, value, entry, handEnv);
|
|
2740
1929
|
if (this.tracing) this.emit({ type: "write", target, path, value, ...prev !== void 0 ? { prev } : {} }, newTurn);
|
|
2741
1930
|
}
|
|
2742
|
-
const outcomeId = outcome ? effectiveGameId(outcome) : "";
|
|
2743
|
-
const record = { card: effectiveGameId(entry.card), outcome: outcomeId, turn: newTurn };
|
|
1931
|
+
const outcomeId = outcome ? (0, import_model.effectiveGameId)(outcome) : "";
|
|
1932
|
+
const record = { card: (0, import_model.effectiveGameId)(entry.card), outcome: outcomeId, turn: newTurn };
|
|
2744
1933
|
this.playLog.push(record);
|
|
2745
1934
|
this.indexPlay(record);
|
|
2746
1935
|
if (entry.card.redraw === "never") {
|
|
@@ -2755,7 +1944,7 @@ var Flow = class {
|
|
|
2755
1944
|
(this.boardContents.get(handId) ?? []).filter((id) => id !== entry.card.id)
|
|
2756
1945
|
);
|
|
2757
1946
|
this.turnCounts.set(entry.box.id, newTurn);
|
|
2758
|
-
if (this.tracing) this.emit({ type: "play", card: effectiveGameId(entry.card), outcome: outcomeId, turn: newTurn }, newTurn);
|
|
1947
|
+
if (this.tracing) this.emit({ type: "play", card: (0, import_model.effectiveGameId)(entry.card), outcome: outcomeId, turn: newTurn }, newTurn);
|
|
2759
1948
|
}
|
|
2760
1949
|
/** One owned property's address, owner segment and all (4.4). */
|
|
2761
1950
|
address(kind, id) {
|
|
@@ -2820,7 +2009,7 @@ var Flow = class {
|
|
|
2820
2009
|
if (!box) throw new Error(`unknown box "${boxRef}"`);
|
|
2821
2010
|
const next = (this.turnCounts.get(box.id) ?? 0) + n;
|
|
2822
2011
|
this.turnCounts.set(box.id, next);
|
|
2823
|
-
if (this.tracing) this.emit({ type: "turns", box: effectiveGameId(box), turn: next }, next);
|
|
2012
|
+
if (this.tracing) this.emit({ type: "turns", box: (0, import_model.effectiveGameId)(box), turn: next }, next);
|
|
2824
2013
|
}
|
|
2825
2014
|
// --- state access (host surface + test tooling) ---------------------------------
|
|
2826
2015
|
/** Every box, bundle order: identity + this flow's clock (the enumeration
|
|
@@ -2829,7 +2018,7 @@ var Flow = class {
|
|
|
2829
2018
|
this.assertOpen();
|
|
2830
2019
|
return this.internals.bundle.boxes.map((b) => ({
|
|
2831
2020
|
id: b.id,
|
|
2832
|
-
gameId: effectiveGameId(b),
|
|
2021
|
+
gameId: (0, import_model.effectiveGameId)(b),
|
|
2833
2022
|
...b.title !== void 0 ? { title: b.title } : {},
|
|
2834
2023
|
turn: this.turnCounts.get(b.id) ?? 0
|
|
2835
2024
|
}));
|
|
@@ -2966,7 +2155,7 @@ var Flow = class {
|
|
|
2966
2155
|
for (const [boxId, turn] of Object.entries(saved.turns ?? {})) {
|
|
2967
2156
|
if (this.turnCounts.has(boxId)) this.turnCounts.set(boxId, turn);
|
|
2968
2157
|
}
|
|
2969
|
-
this.prng = makePrng(saved.prng);
|
|
2158
|
+
this.prng = (0, import_expr.makePrng)(saved.prng);
|
|
2970
2159
|
this.cooldowns = saved.cooldowns ?? {};
|
|
2971
2160
|
this.playLog = saved.playLog ?? [];
|
|
2972
2161
|
this.rebuildPlayIndex();
|
|
@@ -2978,6 +2167,7 @@ var Flow = class {
|
|
|
2978
2167
|
};
|
|
2979
2168
|
|
|
2980
2169
|
// src/describe.ts
|
|
2170
|
+
var import_model3 = require("@storylet-studio/model");
|
|
2981
2171
|
var summarise = (decls) => decls.map((d) => ({
|
|
2982
2172
|
name: d.name,
|
|
2983
2173
|
type: d.type,
|
|
@@ -2993,14 +2183,14 @@ var handDecls = (hand, box) => {
|
|
|
2993
2183
|
}
|
|
2994
2184
|
return hand.properties ?? [];
|
|
2995
2185
|
};
|
|
2996
|
-
var movableHoles = (hand, box) => {
|
|
2186
|
+
var movableHoles = (bundle, hand, box) => {
|
|
2997
2187
|
const filled = hand.template !== void 0 ? hand.chosen : hand.rule?.bindings;
|
|
2998
2188
|
const out = [];
|
|
2999
2189
|
for (const [groupId, value] of Object.entries(filled ?? {})) {
|
|
3000
|
-
if (!isHoleRef(value)) continue;
|
|
3001
|
-
const group = box.
|
|
2190
|
+
if (!(0, import_model3.isHoleRef)(value)) continue;
|
|
2191
|
+
const group = (0, import_model3.groupsOfBox)(bundle, box).find((g) => g.id === groupId);
|
|
3002
2192
|
if (group === void 0) continue;
|
|
3003
|
-
out.push({ group: effectiveGameId(group), from: value });
|
|
2193
|
+
out.push({ group: (0, import_model3.effectiveGameId)(group), from: value });
|
|
3004
2194
|
}
|
|
3005
2195
|
return out;
|
|
3006
2196
|
};
|
|
@@ -3018,17 +2208,18 @@ function describeBundle(bundle) {
|
|
|
3018
2208
|
];
|
|
3019
2209
|
const totals = { boxes: 0, decks: 0, cards: 0, hands: 0, templates: 0, tagGroups: 0 };
|
|
3020
2210
|
for (const box of bundle.boxes) {
|
|
3021
|
-
const boxGameId = effectiveGameId(box);
|
|
2211
|
+
const boxGameId = (0, import_model3.effectiveGameId)(box);
|
|
3022
2212
|
const cards = box.decks.reduce((n, deck) => n + deck.cards.length, 0);
|
|
3023
2213
|
boxes.push({
|
|
3024
2214
|
gameId: boxGameId,
|
|
3025
2215
|
...box.title !== void 0 ? { title: box.title } : {},
|
|
2216
|
+
...box.usesMap === true ? { usesMap: true } : {},
|
|
3026
2217
|
ranking: { specificity: box.ranking.specificity },
|
|
3027
2218
|
...box.turn !== void 0 ? { turn: { seconds: box.turn.seconds } } : {},
|
|
3028
2219
|
...durableCardCount(box) > 0 ? { durableCards: durableCardCount(box) } : {},
|
|
3029
2220
|
tagGroups: box.tagGroups.map((group) => ({
|
|
3030
|
-
gameId: effectiveGameId(group),
|
|
3031
|
-
tags: group.tags.map((tag) => effectiveGameId(tag))
|
|
2221
|
+
gameId: (0, import_model3.effectiveGameId)(group),
|
|
2222
|
+
tags: group.tags.map((tag) => (0, import_model3.effectiveGameId)(tag))
|
|
3032
2223
|
})),
|
|
3033
2224
|
counts: {
|
|
3034
2225
|
decks: box.decks.length,
|
|
@@ -3046,13 +2237,13 @@ function describeBundle(bundle) {
|
|
|
3046
2237
|
totals.tagGroups += box.tagGroups.length;
|
|
3047
2238
|
for (const hand of box.hands) {
|
|
3048
2239
|
const template = hand.template !== void 0 ? box.handTemplates.find((t) => t.id === hand.template) : void 0;
|
|
3049
|
-
const movable = movableHoles(hand, box);
|
|
2240
|
+
const movable = movableHoles(bundle, hand, box);
|
|
3050
2241
|
hands.push({
|
|
3051
|
-
gameId: effectiveGameId(hand),
|
|
2242
|
+
gameId: (0, import_model3.effectiveGameId)(hand),
|
|
3052
2243
|
...hand.title !== void 0 ? { title: hand.title } : {},
|
|
3053
2244
|
box: boxGameId,
|
|
3054
2245
|
slots: handSlots(hand, box),
|
|
3055
|
-
...template !== void 0 ? { template: effectiveGameId(template) } : {},
|
|
2246
|
+
...template !== void 0 ? { template: (0, import_model3.effectiveGameId)(template) } : {},
|
|
3056
2247
|
...movable.length > 0 ? { movable } : {}
|
|
3057
2248
|
});
|
|
3058
2249
|
}
|
|
@@ -3067,14 +2258,23 @@ function describeBundle(bundle) {
|
|
|
3067
2258
|
});
|
|
3068
2259
|
};
|
|
3069
2260
|
push("box", boxGameId, box.properties);
|
|
3070
|
-
for (const deck of box.decks) push("deck", effectiveGameId(deck), deck.properties);
|
|
3071
|
-
for (const hand of box.hands) push("hand", effectiveGameId(hand), handDecls(hand, box));
|
|
2261
|
+
for (const deck of box.decks) push("deck", (0, import_model3.effectiveGameId)(deck), deck.properties);
|
|
2262
|
+
for (const hand of box.hands) push("hand", (0, import_model3.effectiveGameId)(hand), handDecls(hand, box));
|
|
3072
2263
|
for (const group of box.tagGroups) {
|
|
3073
2264
|
for (const tag of group.tags) {
|
|
3074
|
-
push("tag", effectiveGameId(tag), tag.properties ?? [], effectiveGameId(group));
|
|
2265
|
+
push("tag", (0, import_model3.effectiveGameId)(tag), tag.properties ?? [], (0, import_model3.effectiveGameId)(group));
|
|
3075
2266
|
}
|
|
3076
2267
|
}
|
|
3077
2268
|
}
|
|
2269
|
+
const map = bundle.map;
|
|
2270
|
+
if (map !== void 0) {
|
|
2271
|
+
const group = (0, import_model3.effectiveGameId)(map.group);
|
|
2272
|
+
for (const tag of map.group.tags) {
|
|
2273
|
+
const decls = tag.properties ?? [];
|
|
2274
|
+
if (decls.length > 0) properties.push({ scope: "tag", owner: (0, import_model3.effectiveGameId)(tag), group, properties: summarise(decls) });
|
|
2275
|
+
}
|
|
2276
|
+
totals.tagGroups += 1;
|
|
2277
|
+
}
|
|
3078
2278
|
return {
|
|
3079
2279
|
identity: {
|
|
3080
2280
|
schema: bundle.schema,
|
|
@@ -3087,13 +2287,16 @@ function describeBundle(bundle) {
|
|
|
3087
2287
|
boxes,
|
|
3088
2288
|
hands,
|
|
3089
2289
|
properties,
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
2290
|
+
...map !== void 0 ? {
|
|
2291
|
+
map: {
|
|
2292
|
+
group: (0, import_model3.effectiveGameId)(map.group),
|
|
2293
|
+
tags: map.group.tags.map((tag) => (0, import_model3.effectiveGameId)(tag)),
|
|
2294
|
+
boxes: bundle.boxes.filter((box) => box.usesMap === true).map((box) => (0, import_model3.effectiveGameId)(box)),
|
|
2295
|
+
zones: map.geometry?.zones.length ?? 0,
|
|
2296
|
+
backgrounds: map.geometry?.backgrounds?.length ?? 0,
|
|
2297
|
+
sites: Object.fromEntries(Object.entries(map.geometry?.sites ?? {}).map(([box, sites]) => [box, sites.length]))
|
|
2298
|
+
}
|
|
2299
|
+
} : {}
|
|
3097
2300
|
};
|
|
3098
2301
|
}
|
|
3099
2302
|
// Annotate the CommonJS export names for ESM import in node:
|