@storylet-studio/runtime 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.cjs +149 -27
- 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 +155 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,12 +3,16 @@ import { deserialiseAst, evaluate } from "@wildwinter/expr";
|
|
|
3
3
|
import { matchedSpecificity } from "@wildwinter/expr-specificity";
|
|
4
4
|
import { storyletsDialect, NEVER_PLAYED } from "@storylet-studio/dialect";
|
|
5
5
|
import {
|
|
6
|
+
BUNDLE_SCHEMAS,
|
|
6
7
|
PLACE_GROUP,
|
|
8
|
+
allTagGroups,
|
|
7
9
|
ambiguousValueAddressMessage,
|
|
8
10
|
effectiveGameId,
|
|
11
|
+
groupsOfBox,
|
|
9
12
|
isHoleRef,
|
|
10
13
|
parseHoleRef,
|
|
11
|
-
valueAddresses
|
|
14
|
+
valueAddresses,
|
|
15
|
+
zoneQualifiedValueAddressMessage
|
|
12
16
|
} from "@storylet-studio/model";
|
|
13
17
|
import { SAVE_SCHEMA, SAVE_SCHEMA_V1 } from "@storylet-studio/model";
|
|
14
18
|
import { PropertyBag as StateBag, ScopeRegistry } from "@wildwinter/scoperegistry";
|
|
@@ -17,7 +21,7 @@ import { PropertyBag as StateBag, ScopeRegistry } from "@wildwinter/scoperegistr
|
|
|
17
21
|
import { makePrng, shuffleInPlace } from "@wildwinter/expr";
|
|
18
22
|
|
|
19
23
|
// src/engine.ts
|
|
20
|
-
var tagKey = (groupId, tagId) => `${groupId}${tagId}`;
|
|
24
|
+
var tagKey = (boxId, groupId, tagId) => `${boxId}${groupId}${tagId}`;
|
|
21
25
|
var cardIsShared = (card, deckShared) => card.shared ?? deckShared;
|
|
22
26
|
var sharedCap = (card) => card.sharedCopies ?? card.copies ?? 1;
|
|
23
27
|
var OWNER = "Storylet Engine";
|
|
@@ -74,11 +78,12 @@ var isShared = (scope, d) => d.shared ?? SCOPE_DEFAULT_SHARED[scope];
|
|
|
74
78
|
var sharedHalf = (scope, decls) => decls.filter((d) => isShared(scope, d));
|
|
75
79
|
var flowHalf = (scope, decls) => decls.filter((d) => !isShared(scope, d));
|
|
76
80
|
var OWNED_SCOPES = ["box", "deck", "hand", "value"];
|
|
81
|
+
var emptyOwnerIndex = () => ({ gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map(), zoneQualified: /* @__PURE__ */ new Map() });
|
|
77
82
|
var emptyOwnerIndexes = () => ({
|
|
78
|
-
box:
|
|
79
|
-
deck:
|
|
80
|
-
hand:
|
|
81
|
-
value:
|
|
83
|
+
box: emptyOwnerIndex(),
|
|
84
|
+
deck: emptyOwnerIndex(),
|
|
85
|
+
hand: emptyOwnerIndex(),
|
|
86
|
+
value: emptyOwnerIndex()
|
|
82
87
|
});
|
|
83
88
|
var indexOwner = (index, entity) => {
|
|
84
89
|
const gameId = effectiveGameId(entity);
|
|
@@ -90,6 +95,7 @@ var indexValueOwners = (index, bundle) => {
|
|
|
90
95
|
for (const [id, segment] of addresses.print) index.gameId.set(id, segment);
|
|
91
96
|
for (const [segment, id] of addresses.accept) index.id.set(segment, id);
|
|
92
97
|
for (const [gameId, candidates] of addresses.repeated) index.repeated.set(gameId, candidates);
|
|
98
|
+
for (const [segment, zone] of addresses.zoneQualified) index.zoneQualified.set(segment, zone);
|
|
93
99
|
};
|
|
94
100
|
var handDeclsOf = (internals, hand) => {
|
|
95
101
|
if (hand.template !== void 0) {
|
|
@@ -101,6 +107,8 @@ var addressOf = (internals, kind, id) => `${kind}.${internals.owners[kind].gameI
|
|
|
101
107
|
var resolveOwner = (internals, kind, segment) => {
|
|
102
108
|
const candidates = internals.owners[kind].repeated.get(segment);
|
|
103
109
|
if (candidates !== void 0) return { ambiguous: candidates };
|
|
110
|
+
const zone = internals.owners[kind].zoneQualified.get(segment);
|
|
111
|
+
if (zone !== void 0) return { zone };
|
|
104
112
|
const byGameId = internals.owners[kind].id.get(segment);
|
|
105
113
|
if (byGameId !== void 0) return { id: byGameId, legacy: false };
|
|
106
114
|
if (internals.owners[kind].gameId.has(segment)) return { id: segment, legacy: true };
|
|
@@ -110,6 +118,7 @@ var ownerOrThrow = (internals, kind, segment, name) => {
|
|
|
110
118
|
const owner = resolveOwner(internals, kind, segment);
|
|
111
119
|
if (owner === void 0) throw new Error(`no ${kind} store "${segment}"`);
|
|
112
120
|
if ("ambiguous" in owner) throw new Error(ambiguousValueAddressMessage(segment, name, owner.ambiguous));
|
|
121
|
+
if ("zone" in owner) throw new Error(zoneQualifiedValueAddressMessage(segment, owner.zone, name));
|
|
113
122
|
return owner;
|
|
114
123
|
};
|
|
115
124
|
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.`;
|
|
@@ -127,9 +136,12 @@ var buildPartition = (internals, half) => {
|
|
|
127
136
|
hand: new Map(b.boxes.flatMap((box) => box.hands.map(
|
|
128
137
|
(hand) => [hand.id, bagFromDecls(half("hand", handDeclsOf(internals, hand)), at("hand", hand.id))]
|
|
129
138
|
))),
|
|
130
|
-
|
|
139
|
+
// Every box's tags, then the project map's zones ONCE (design/project-
|
|
140
|
+
// map-contract.md 3.3): a zone is one bag per partition, whichever boxes'
|
|
141
|
+
// hands are dealt to it.
|
|
142
|
+
value: new Map(allTagGroups(b).flatMap((group) => group.tags.map(
|
|
131
143
|
(tag) => [tag.id, bagFromDecls(half("value", tag.properties ?? []), at("value", tag.id))]
|
|
132
|
-
)))
|
|
144
|
+
)))
|
|
133
145
|
};
|
|
134
146
|
};
|
|
135
147
|
var partitionValues = (p) => ({
|
|
@@ -246,6 +258,68 @@ function finishReport(bundle, saved, flows, draft) {
|
|
|
246
258
|
retypedProperties
|
|
247
259
|
};
|
|
248
260
|
}
|
|
261
|
+
var refuseUnreadableBundle = (bundle) => {
|
|
262
|
+
const schema = bundle.schema;
|
|
263
|
+
if (typeof schema !== "string" || !BUNDLE_SCHEMAS.includes(schema)) {
|
|
264
|
+
throw new Error(`unsupported bundle schema: ${String(schema)} (this runtime reads ${BUNDLE_SCHEMAS.join(" and ")})`);
|
|
265
|
+
}
|
|
266
|
+
const problems = [];
|
|
267
|
+
const map = bundle.map?.group;
|
|
268
|
+
const mapName = map !== void 0 ? effectiveGameId(map) : void 0;
|
|
269
|
+
if (map !== void 0 && mapName === PLACE_GROUP) {
|
|
270
|
+
problems.push(`the project map's tag group is called "${PLACE_GROUP}", which is reserved for a box's own hands`);
|
|
271
|
+
}
|
|
272
|
+
const boxTags = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const box of bundle.boxes) {
|
|
274
|
+
for (const group of box.tagGroups) {
|
|
275
|
+
for (const tag of group.tags) {
|
|
276
|
+
const gameId = effectiveGameId(tag);
|
|
277
|
+
if (!boxTags.has(gameId)) boxTags.set(gameId, { box: effectiveGameId(box), group: effectiveGameId(group) });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const tag of map?.tags ?? []) {
|
|
282
|
+
const zone = effectiveGameId(tag);
|
|
283
|
+
const clash = boxTags.get(zone);
|
|
284
|
+
if (clash !== void 0) {
|
|
285
|
+
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`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const box of bundle.boxes) {
|
|
289
|
+
const boxName = effectiveGameId(box);
|
|
290
|
+
if (box.usesMap === true) {
|
|
291
|
+
if (map === void 0) {
|
|
292
|
+
problems.push(`box "${boxName}" uses the project map, but the bundle has no map`);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const twin = box.tagGroups.find((g) => effectiveGameId(g) === mapName);
|
|
296
|
+
if (twin !== void 0) {
|
|
297
|
+
problems.push(`box "${boxName}" uses the project map and declares its own tag group "${mapName}", the map's name`);
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (map === void 0) continue;
|
|
302
|
+
const names = (where) => {
|
|
303
|
+
problems.push(`box "${boxName}" is not on the project map, but ${where} names the map's tag group "${mapName}"`);
|
|
304
|
+
};
|
|
305
|
+
for (const deck of box.decks) {
|
|
306
|
+
for (const card of deck.cards) {
|
|
307
|
+
if (card.tags?.[map.id] !== void 0) names(`card "${effectiveGameId(card)}"`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const template of box.handTemplates) {
|
|
311
|
+
if (template.bindings?.[map.id] !== void 0 || template.chooses?.includes(map.id) === true) {
|
|
312
|
+
names(`hand template "${effectiveGameId(template)}"`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const hand of box.hands) {
|
|
316
|
+
if (hand.chosen?.[map.id] !== void 0 || hand.rule?.bindings?.[map.id] !== void 0) {
|
|
317
|
+
names(`hand "${effectiveGameId(hand)}"`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (problems.length > 0) throw new Error(`bundle refused: ${problems.join("; ")}`);
|
|
322
|
+
};
|
|
249
323
|
var Engine = class _Engine {
|
|
250
324
|
internals;
|
|
251
325
|
seed;
|
|
@@ -262,6 +336,7 @@ var Engine = class _Engine {
|
|
|
262
336
|
* hotSwap puts this engine back exactly as it was. */
|
|
263
337
|
sharedMounts = [];
|
|
264
338
|
constructor(bundle, opts = {}) {
|
|
339
|
+
refuseUnreadableBundle(bundle);
|
|
265
340
|
this.creationOptions = opts;
|
|
266
341
|
this.seed = opts.seed ?? 0;
|
|
267
342
|
this.onReplacedFlow = opts.onReplacedFlow;
|
|
@@ -345,6 +420,10 @@ var Engine = class _Engine {
|
|
|
345
420
|
indexOwner(internals.owners.hand, hand);
|
|
346
421
|
}
|
|
347
422
|
}
|
|
423
|
+
if (bundle.map !== void 0) {
|
|
424
|
+
internals.groupsById.set(bundle.map.group.id, { group: bundle.map.group });
|
|
425
|
+
if (bundle.map.group.required === true) internals.requiredGroups.add(bundle.map.group.id);
|
|
426
|
+
}
|
|
348
427
|
this.initLadders();
|
|
349
428
|
const declSet = (half) => ({
|
|
350
429
|
story: half("story", bundle.story.properties),
|
|
@@ -355,9 +434,9 @@ var Engine = class _Engine {
|
|
|
355
434
|
hand: new Map(bundle.boxes.flatMap((box) => box.hands.map(
|
|
356
435
|
(hand) => [hand.id, half("hand", handDeclsOf(internals, hand))]
|
|
357
436
|
))),
|
|
358
|
-
value: new Map(bundle
|
|
437
|
+
value: new Map(allTagGroups(bundle).flatMap((group) => group.tags.map(
|
|
359
438
|
(tag) => [tag.id, half("value", tag.properties ?? [])]
|
|
360
|
-
)))
|
|
439
|
+
)))
|
|
361
440
|
});
|
|
362
441
|
internals.flowDecls = declSet(flowHalf);
|
|
363
442
|
internals.sharedDecls = declSet(sharedHalf);
|
|
@@ -458,6 +537,7 @@ var Engine = class _Engine {
|
|
|
458
537
|
}
|
|
459
538
|
for (const hand of box.hands) internals.ladders.hand.set(hand.id, grab(handDeclsOf(internals, hand)));
|
|
460
539
|
}
|
|
540
|
+
for (const tag of b.map?.group.tags ?? []) internals.ladders.value.set(tag.id, grab(tag.properties));
|
|
461
541
|
const any = (m) => [...m.values()].some((x) => x.size > 0);
|
|
462
542
|
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);
|
|
463
543
|
}
|
|
@@ -781,11 +861,35 @@ var Engine = class _Engine {
|
|
|
781
861
|
return structuredClone({
|
|
782
862
|
schema: SAVE_SCHEMA,
|
|
783
863
|
content: this.internals.bundle.content,
|
|
784
|
-
...this.internals.ownsRegistry ? { registry: this.
|
|
864
|
+
...this.internals.ownsRegistry ? { registry: this.registrySection() } : {},
|
|
785
865
|
shared: { spent: [...this.spent].sort() },
|
|
786
866
|
flows: Object.fromEntries([...this.flowsById].map(([id, flow]) => [id, flow.snapshot(false)]))
|
|
787
867
|
});
|
|
788
868
|
}
|
|
869
|
+
/** The registry's values in CANONICAL order, the order a load rebuilds them
|
|
870
|
+
* in: the engine-wide keys as the constructor registered them, then each
|
|
871
|
+
* flow's keys in `flows()` order (each flow's own registration order), then
|
|
872
|
+
* anything else the registry holds (values still waiting for a key), as the
|
|
873
|
+
* registry lists it. The registry itself lists keys in registration order,
|
|
874
|
+
* and a flow replaced in place (`open()` above keeps its slot in
|
|
875
|
+
* `flowsById`) re-registers its keys at the END, so `openFlow("a");
|
|
876
|
+
* openFlow("b"); openFlow("a")` saved b's keys before a's while a load
|
|
877
|
+
* rebuilt a's first: the same run, different `.storyletsave` bytes, and a
|
|
878
|
+
* save loaded and saved again no longer equal to itself. It is the
|
|
879
|
+
* 2026-08-29 rule carried into the section save@2 moved the per-flow values
|
|
880
|
+
* to (2026-10-01). Order does not matter on READ (`partitionsFromSections`
|
|
881
|
+
* sorts by key shape), so a save written in the old order loads as before. */
|
|
882
|
+
registrySection() {
|
|
883
|
+
const all = this.internals.registry.save();
|
|
884
|
+
const out = {};
|
|
885
|
+
const take = (key) => {
|
|
886
|
+
if (Object.prototype.hasOwnProperty.call(all, key) && !Object.prototype.hasOwnProperty.call(out, key)) out[key] = all[key];
|
|
887
|
+
};
|
|
888
|
+
for (const { key } of this.sharedMounts) take(key);
|
|
889
|
+
for (const flow of this.flowsById.values()) for (const key of flow.registeredKeys()) take(key);
|
|
890
|
+
for (const key of Object.keys(all)) take(key);
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
789
893
|
/** ONE flow's blob, to park a visit that is walking away: the same shape
|
|
790
894
|
* the envelope carries per flow, and the same shape `openFlow`'s `restore`
|
|
791
895
|
* option takes back (design/engine-server.md 4.1). Saving the whole
|
|
@@ -1115,9 +1219,16 @@ var Flow = class {
|
|
|
1115
1219
|
/** Tag group names are box-scoped: two boxes may name a group the same way
|
|
1116
1220
|
* (schema 1 - boxes namespace their groups), so a name is only ever
|
|
1117
1221
|
* resolved inside the box being asked, never bundle-wide. Ids are
|
|
1118
|
-
* project-unique and accepted here too, still confined to the box.
|
|
1222
|
+
* project-unique and accepted here too, still confined to the box.
|
|
1223
|
+
*
|
|
1224
|
+
* A box on the project map sees ONE namespace: its own groups, then the
|
|
1225
|
+
* map's group (design/project-map-contract.md 3.1). A box that has not
|
|
1226
|
+
* opted in does not see the map's name at all, so a peek naming it there
|
|
1227
|
+
* is the ordinary unknown-group refusal. Own groups first is stated for
|
|
1228
|
+
* determinism only: a bundle that loads never has the two share a name. */
|
|
1119
1229
|
groupInBox(box, ref) {
|
|
1120
|
-
|
|
1230
|
+
const groups = groupsOfBox(this.internals.bundle, box);
|
|
1231
|
+
return groups.find((g) => effectiveGameId(g) === ref) ?? groups.find((g) => g.id === ref);
|
|
1121
1232
|
}
|
|
1122
1233
|
/** Fold one play into the indexes. O(the card's tags), not O(the log). */
|
|
1123
1234
|
indexPlay(record) {
|
|
@@ -1127,7 +1238,7 @@ var Flow = class {
|
|
|
1127
1238
|
if (!entry) return;
|
|
1128
1239
|
for (const [groupId, tagIds] of Object.entries(entry.card.tags ?? {})) {
|
|
1129
1240
|
for (const tagId of tagIds) {
|
|
1130
|
-
const key = tagKey(groupId, tagId);
|
|
1241
|
+
const key = tagKey(entry.box.id, groupId, tagId);
|
|
1131
1242
|
this.tagPlayCount.set(key, (this.tagPlayCount.get(key) ?? 0) + 1);
|
|
1132
1243
|
this.lastPlayInTag.set(key, record);
|
|
1133
1244
|
}
|
|
@@ -1143,8 +1254,10 @@ var Flow = class {
|
|
|
1143
1254
|
for (const record of this.playLog) this.indexPlay(record);
|
|
1144
1255
|
}
|
|
1145
1256
|
/** `box` is the box whose ask is being evaluated: the play-history
|
|
1146
|
-
* functions take a bare group name, so it resolves there
|
|
1147
|
-
*
|
|
1257
|
+
* functions take a bare group name, so it resolves there, and they count
|
|
1258
|
+
* only that box's own plays (the box is in the index key). That was
|
|
1259
|
+
* automatic while every group was a box's; a project-map zone is shared,
|
|
1260
|
+
* and its history is still not (design/project-map-contract.md 3.7, D7).
|
|
1148
1261
|
* History is THIS flow's: countPlayed answers "have I done this". */
|
|
1149
1262
|
/** One host per box, built once.
|
|
1150
1263
|
*
|
|
@@ -1169,7 +1282,7 @@ var Flow = class {
|
|
|
1169
1282
|
const keyOf = (group, tag) => {
|
|
1170
1283
|
const found = this.groupInBox(box, group);
|
|
1171
1284
|
const t = found?.tags.find((v) => v.gameId === tag);
|
|
1172
|
-
return found && t ? tagKey(found.id, t.id) : void 0;
|
|
1285
|
+
return found && t ? tagKey(box.id, found.id, t.id) : void 0;
|
|
1173
1286
|
};
|
|
1174
1287
|
const since = (record) => {
|
|
1175
1288
|
const entry = this.internals.cardsByGameId.get(record.card);
|
|
@@ -1376,7 +1489,7 @@ var Flow = class {
|
|
|
1376
1489
|
* is what says otherwise.
|
|
1377
1490
|
*/
|
|
1378
1491
|
bindStateGroups(box, boundTags, askNames) {
|
|
1379
|
-
for (const group of box
|
|
1492
|
+
for (const group of groupsOfBox(this.internals.bundle, box)) {
|
|
1380
1493
|
if (group.boundBy === void 0 || boundTags.has(group.id)) continue;
|
|
1381
1494
|
const ref = /^@(world|story)\.([a-z][a-z0-9_-]*)$/.exec(group.boundBy);
|
|
1382
1495
|
if (!ref) {
|
|
@@ -2035,7 +2148,7 @@ var Flow = class {
|
|
|
2035
2148
|
};
|
|
2036
2149
|
|
|
2037
2150
|
// src/describe.ts
|
|
2038
|
-
import { effectiveGameId as effectiveGameId2, isHoleRef as isHoleRef2 } from "@storylet-studio/model";
|
|
2151
|
+
import { effectiveGameId as effectiveGameId2, groupsOfBox as groupsOfBox2, isHoleRef as isHoleRef2 } from "@storylet-studio/model";
|
|
2039
2152
|
var summarise = (decls) => decls.map((d) => ({
|
|
2040
2153
|
name: d.name,
|
|
2041
2154
|
type: d.type,
|
|
@@ -2051,12 +2164,12 @@ var handDecls = (hand, box) => {
|
|
|
2051
2164
|
}
|
|
2052
2165
|
return hand.properties ?? [];
|
|
2053
2166
|
};
|
|
2054
|
-
var movableHoles = (hand, box) => {
|
|
2167
|
+
var movableHoles = (bundle, hand, box) => {
|
|
2055
2168
|
const filled = hand.template !== void 0 ? hand.chosen : hand.rule?.bindings;
|
|
2056
2169
|
const out = [];
|
|
2057
2170
|
for (const [groupId, value] of Object.entries(filled ?? {})) {
|
|
2058
2171
|
if (!isHoleRef2(value)) continue;
|
|
2059
|
-
const group = box.
|
|
2172
|
+
const group = groupsOfBox2(bundle, box).find((g) => g.id === groupId);
|
|
2060
2173
|
if (group === void 0) continue;
|
|
2061
2174
|
out.push({ group: effectiveGameId2(group), from: value });
|
|
2062
2175
|
}
|
|
@@ -2081,6 +2194,7 @@ function describeBundle(bundle) {
|
|
|
2081
2194
|
boxes.push({
|
|
2082
2195
|
gameId: boxGameId,
|
|
2083
2196
|
...box.title !== void 0 ? { title: box.title } : {},
|
|
2197
|
+
...box.usesMap === true ? { usesMap: true } : {},
|
|
2084
2198
|
ranking: { specificity: box.ranking.specificity },
|
|
2085
2199
|
...box.turn !== void 0 ? { turn: { seconds: box.turn.seconds } } : {},
|
|
2086
2200
|
...durableCardCount(box) > 0 ? { durableCards: durableCardCount(box) } : {},
|
|
@@ -2104,7 +2218,7 @@ function describeBundle(bundle) {
|
|
|
2104
2218
|
totals.tagGroups += box.tagGroups.length;
|
|
2105
2219
|
for (const hand of box.hands) {
|
|
2106
2220
|
const template = hand.template !== void 0 ? box.handTemplates.find((t) => t.id === hand.template) : void 0;
|
|
2107
|
-
const movable = movableHoles(hand, box);
|
|
2221
|
+
const movable = movableHoles(bundle, hand, box);
|
|
2108
2222
|
hands.push({
|
|
2109
2223
|
gameId: effectiveGameId2(hand),
|
|
2110
2224
|
...hand.title !== void 0 ? { title: hand.title } : {},
|
|
@@ -2133,6 +2247,15 @@ function describeBundle(bundle) {
|
|
|
2133
2247
|
}
|
|
2134
2248
|
}
|
|
2135
2249
|
}
|
|
2250
|
+
const map = bundle.map;
|
|
2251
|
+
if (map !== void 0) {
|
|
2252
|
+
const group = effectiveGameId2(map.group);
|
|
2253
|
+
for (const tag of map.group.tags) {
|
|
2254
|
+
const decls = tag.properties ?? [];
|
|
2255
|
+
if (decls.length > 0) properties.push({ scope: "tag", owner: effectiveGameId2(tag), group, properties: summarise(decls) });
|
|
2256
|
+
}
|
|
2257
|
+
totals.tagGroups += 1;
|
|
2258
|
+
}
|
|
2136
2259
|
return {
|
|
2137
2260
|
identity: {
|
|
2138
2261
|
schema: bundle.schema,
|
|
@@ -2145,13 +2268,16 @@ function describeBundle(bundle) {
|
|
|
2145
2268
|
boxes,
|
|
2146
2269
|
hands,
|
|
2147
2270
|
properties,
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2271
|
+
...map !== void 0 ? {
|
|
2272
|
+
map: {
|
|
2273
|
+
group: effectiveGameId2(map.group),
|
|
2274
|
+
tags: map.group.tags.map((tag) => effectiveGameId2(tag)),
|
|
2275
|
+
boxes: bundle.boxes.filter((box) => box.usesMap === true).map((box) => effectiveGameId2(box)),
|
|
2276
|
+
zones: map.geometry?.zones.length ?? 0,
|
|
2277
|
+
backgrounds: map.geometry?.backgrounds?.length ?? 0,
|
|
2278
|
+
sites: Object.fromEntries(Object.entries(map.geometry?.sites ?? {}).map(([box, sites]) => [box, sites.length]))
|
|
2279
|
+
}
|
|
2280
|
+
} : {}
|
|
2155
2281
|
};
|
|
2156
2282
|
}
|
|
2157
2283
|
export {
|