@storylet-studio/model 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/index.cjs +574 -0
- package/dist/index.d.cts +1113 -0
- package/dist/index.d.ts +1113 -0
- package/dist/index.js +500 -0
- package/package.json +32 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
import { ScalarValue, Expression } from '@wildwinter/expr';
|
|
2
|
+
export { AstNode, Expression, RESERVED_PROPERTY_NAMES, ScalarValue, isCaseOnlyPropertyName, isValidPropertyName, propertyNameify } from '@wildwinter/expr';
|
|
3
|
+
|
|
4
|
+
/** The template's key in every `templates` bag it appears in. */
|
|
5
|
+
declare const SPATIAL = "spatial";
|
|
6
|
+
/** A zone's outline, in the map's own coordinate space (the same space the view
|
|
7
|
+
* sidecar's sites use). No units: a map is to its own scale. */
|
|
8
|
+
type Polygon = ViewPoint[];
|
|
9
|
+
/**
|
|
10
|
+
* Where something sits in the stack: bigger is nearer the front.
|
|
11
|
+
*
|
|
12
|
+
* SPARSE, and the fallback is what makes it so: an item without one takes its
|
|
13
|
+
* position in the list, which is the order everything already drew in, so a
|
|
14
|
+
* project that has never been restacked looks exactly as it did. Only a moved
|
|
15
|
+
* item gains a number, set midway between its new neighbours - the same scheme
|
|
16
|
+
* cards, decks and hands use for authored order, and merge-clean for the same
|
|
17
|
+
* reason: two authors restacking different things touch different entries.
|
|
18
|
+
*
|
|
19
|
+
* Generic on purpose. Zones need it now; BACKGROUNDS will need the identical
|
|
20
|
+
* thing, in their own band below the zones, and this is the piece they share.
|
|
21
|
+
*/
|
|
22
|
+
type Stacked = {
|
|
23
|
+
id: string;
|
|
24
|
+
z?: number;
|
|
25
|
+
};
|
|
26
|
+
/** A stack move, in the vocabulary every drawing tool uses. */
|
|
27
|
+
type StackMove = "front" | "forward" | "backward" | "back";
|
|
28
|
+
/** Back to front: the order to DRAW in, so the frontmost lands on top. */
|
|
29
|
+
declare function stacked<T extends Stacked>(items: T[]): T[];
|
|
30
|
+
/**
|
|
31
|
+
* The `z` that puts `id` where the move asks, or undefined when it is already
|
|
32
|
+
* there (so a no-op never writes a file or costs an undo step).
|
|
33
|
+
*
|
|
34
|
+
* Moving one place uses the midpoint between the item it passes and the one
|
|
35
|
+
* beyond, which is why the numbers stay sparse and nothing has to be renumbered.
|
|
36
|
+
*/
|
|
37
|
+
declare function restack<T extends Stacked>(items: T[], id: string, move: StackMove): number | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* A background image behind a map: a picture of the real space, so a designer
|
|
40
|
+
* (and later an experience runner) can map content onto somewhere physical.
|
|
41
|
+
*
|
|
42
|
+
* NOT a player-facing asset, which is what settles most of its design. The bytes
|
|
43
|
+
* are never touched - no transcoding, no downscaling - because the file on disk
|
|
44
|
+
* is also what a runtime server will one day serve, and legibility at a glance
|
|
45
|
+
* matters more than fidelity.
|
|
46
|
+
*
|
|
47
|
+
* On the GROUP rather than on a zone, because zones cross images: a site plan is
|
|
48
|
+
* one picture with a dozen zones traced over it, and half of them straddle two
|
|
49
|
+
* sheets. Several per group, composing one picture - tiles of a large site, or
|
|
50
|
+
* deliberate overlaps - never alternates, so nothing here is exclusive with
|
|
51
|
+
* anything else.
|
|
52
|
+
*/
|
|
53
|
+
interface SpatialBackground {
|
|
54
|
+
/** Stable id, so two authors adding images do not collide. */
|
|
55
|
+
id: string;
|
|
56
|
+
/** The file's name inside the box's `assets/` folder. A NAME, not a path:
|
|
57
|
+
* assets belong to their box and travel with it. */
|
|
58
|
+
file: string;
|
|
59
|
+
/** Where it sits, in map units. Placement and scale in one rectangle, because
|
|
60
|
+
* a separate scale factor is a second thing to reason about and there is no
|
|
61
|
+
* rotation to make it worth having. */
|
|
62
|
+
x: number;
|
|
63
|
+
y: number;
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
/** 0 to 1, default 1. A tracing base wants to sit back. */
|
|
67
|
+
opacity?: number;
|
|
68
|
+
/** Its place in the stack among the OTHER BACKGROUNDS (see `stacked`).
|
|
69
|
+
* Backgrounds are a band strictly below the zones, structurally, so no value
|
|
70
|
+
* here can put an image over a zone. */
|
|
71
|
+
z?: number;
|
|
72
|
+
/** Out of the way while working on something else. */
|
|
73
|
+
hidden?: boolean;
|
|
74
|
+
/** Locked, the diagram-tool convention: invisible to the pointer, so clicks
|
|
75
|
+
* pass through to whatever is above. What a tracing base should be once it is
|
|
76
|
+
* placed. */
|
|
77
|
+
locked?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** What the spatial template keeps on a GROUP: the marker, and the pictures
|
|
80
|
+
* behind the map. A bag rather than a boolean precisely so this could arrive
|
|
81
|
+
* without a schema change - the marker and the configuration are one object, so
|
|
82
|
+
* a group is spatial exactly when it has one. */
|
|
83
|
+
interface SpatialGroup {
|
|
84
|
+
/** Present and true for a spatial group. */
|
|
85
|
+
map: true;
|
|
86
|
+
backgrounds?: SpatialBackground[];
|
|
87
|
+
}
|
|
88
|
+
/** Is this group a map? */
|
|
89
|
+
declare function isSpatial(group: TagGroup): boolean;
|
|
90
|
+
/** The group's spatial configuration, or undefined when it is an ordinary group. */
|
|
91
|
+
declare function spatialOf(group: TagGroup): SpatialGroup | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* The group's backgrounds, in DRAW order (back to front), skipping any entry
|
|
94
|
+
* that is not one.
|
|
95
|
+
*
|
|
96
|
+
* Forgiving in the same way `polygonOf` is, and for the same reason: a
|
|
97
|
+
* hand-edited or badly merged shard must not throw inside a canvas. Anything
|
|
98
|
+
* malformed reads as absent HERE and is reported by validation instead, which is
|
|
99
|
+
* a place an author can see it.
|
|
100
|
+
*/
|
|
101
|
+
declare function backgroundsOf(group: TagGroup): SpatialBackground[];
|
|
102
|
+
/**
|
|
103
|
+
* Where a dropped picture lands: fitted inside 60% of the viewport, in BOTH axes,
|
|
104
|
+
* centred on `at`, in map units.
|
|
105
|
+
*
|
|
106
|
+
* It does NOT define the map's coordinate space and does not arrive at one pixel
|
|
107
|
+
* to one unit (a 2816px site plan would fill the county). The requirement is
|
|
108
|
+
* narrower and more useful: whatever the zoom, a picture arrives at a size
|
|
109
|
+
* comfortable to GRAB.
|
|
110
|
+
*
|
|
111
|
+
* Computed once, at drop, and stored. Never re-derived from the camera, or the
|
|
112
|
+
* picture would move about when somebody zoomed.
|
|
113
|
+
*
|
|
114
|
+
* 60% of the shorter side was the first rule and it was wrong: scale-invariant as
|
|
115
|
+
* intended, but it put a 1.83:1 plan at a third of the width of a wide window,
|
|
116
|
+
* which is fiddly to grab. Fitting both axes measures 55% x 60% of the view for
|
|
117
|
+
* that image, at every zoom.
|
|
118
|
+
*/
|
|
119
|
+
declare function droppedRect(natural: {
|
|
120
|
+
width: number;
|
|
121
|
+
height: number;
|
|
122
|
+
}, view: {
|
|
123
|
+
width: number;
|
|
124
|
+
height: number;
|
|
125
|
+
}, scale: number, at: ViewPoint): Rect;
|
|
126
|
+
/** Replace the group's backgrounds, returning the new `templates` bag. Keeps the
|
|
127
|
+
* marker and every other template's bag, as every writer here does. */
|
|
128
|
+
declare function withBackgrounds(group: TagGroup, backgrounds: SpatialBackground[]): TagGroup["templates"];
|
|
129
|
+
/** A zone's outline, or undefined when the tag has never been drawn. Anything
|
|
130
|
+
* malformed reads as undefined here and is REPORTED by validation instead: a
|
|
131
|
+
* canvas that threw on a hand-edited shard would be a poor way to find out. */
|
|
132
|
+
declare function polygonOf(tag: Tag): Polygon | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Mark a group spatial, or clear the marker, returning the new `templates` bag.
|
|
135
|
+
*
|
|
136
|
+
* Every one of these writers PRESERVES keys it does not own, because a shard may
|
|
137
|
+
* carry another template's bag (or a newer version of this app's) and dropping it
|
|
138
|
+
* would be data loss on a file somebody else owns.
|
|
139
|
+
*/
|
|
140
|
+
declare function withSpatialGroup(group: TagGroup, on: boolean): TagGroup["templates"];
|
|
141
|
+
/** A zone's place in the stack, or undefined when it has never been moved. */
|
|
142
|
+
declare function zOf(tag: Tag): number | undefined;
|
|
143
|
+
/** Set a zone's place in the stack, returning the new `templates` bag. */
|
|
144
|
+
declare function withZ(tag: Tag, z: number): Tag["templates"];
|
|
145
|
+
/** Set or clear a zone's outline, returning the new `templates` bag. */
|
|
146
|
+
declare function withPolygon(tag: Tag, polygon: Polygon | undefined): Tag["templates"];
|
|
147
|
+
interface Rect {
|
|
148
|
+
x: number;
|
|
149
|
+
y: number;
|
|
150
|
+
width: number;
|
|
151
|
+
height: number;
|
|
152
|
+
}
|
|
153
|
+
/** The axis-aligned box around a polygon: what a canvas needs to hit-test
|
|
154
|
+
* cheaply, to place a selection ring, and to frame a fit. */
|
|
155
|
+
declare function polygonBounds(polygon: Polygon): Rect;
|
|
156
|
+
/**
|
|
157
|
+
* A polygon's centre of AREA, not the average of its vertices.
|
|
158
|
+
*
|
|
159
|
+
* The two agree only for regular shapes: on an outline with a cluster of vertices
|
|
160
|
+
* along a fiddly coastline, the vertex average is dragged towards the crowded side.
|
|
161
|
+
* Falls back to the vertex average for a degenerate (zero-area) polygon, where
|
|
162
|
+
* there is no better answer.
|
|
163
|
+
*
|
|
164
|
+
* NOT the place to put a label: see `labelPoint`. The centre of area of a concave
|
|
165
|
+
* shape can lie outside the shape, which an L-shaped zone demonstrates in one
|
|
166
|
+
* line of arithmetic.
|
|
167
|
+
*/
|
|
168
|
+
declare function centroid(polygon: Polygon): ViewPoint;
|
|
169
|
+
/**
|
|
170
|
+
* Where a zone's LABEL goes: a point guaranteed to be inside the zone.
|
|
171
|
+
*
|
|
172
|
+
* The centre of area is the obvious answer and it is wrong for concave outlines.
|
|
173
|
+
* An L-shaped zone puts its centre of area in the notch, so a label placed there
|
|
174
|
+
* sits outside its own zone, next to whatever is drawn in the gap. Since half the
|
|
175
|
+
* zones anyone draws over a floor plan are L-shaped corridors, this needs to be
|
|
176
|
+
* right rather than usually right.
|
|
177
|
+
*
|
|
178
|
+
* So: the centre of area when that is inside, and otherwise the middle of the
|
|
179
|
+
* widest run of the zone along the horizontal line through it. That keeps the
|
|
180
|
+
* label vertically where the eye expects and moves it sideways into the shape,
|
|
181
|
+
* which for an L means "along the arm". Cheap, stable as the polygon is dragged,
|
|
182
|
+
* and no substitute for the proper pole-of-inaccessibility if a zone ever needs
|
|
183
|
+
* one.
|
|
184
|
+
*/
|
|
185
|
+
declare function labelPoint(polygon: Polygon, opts?: {
|
|
186
|
+
bias?: "middle" | "top";
|
|
187
|
+
}): ViewPoint;
|
|
188
|
+
/**
|
|
189
|
+
* Is this point inside the polygon? The ray-casting test, which handles concave
|
|
190
|
+
* outlines and self-touching ones alike.
|
|
191
|
+
*
|
|
192
|
+
* This is the function that earns the map its keep: dragging a hand's pin from
|
|
193
|
+
* the docks into the market is not a cosmetic act, it rebinds the hand, and this
|
|
194
|
+
* is how the view knows which zone the pin was dropped in.
|
|
195
|
+
*/
|
|
196
|
+
declare function pointInPolygon(point: ViewPoint, polygon: Polygon): boolean;
|
|
197
|
+
/**
|
|
198
|
+
* Which zone a point falls in, given the zones of a spatial group in draw order.
|
|
199
|
+
*
|
|
200
|
+
* Zones may overlap (a room inside a wing, a market square inside a district), so
|
|
201
|
+
* "which one" needs an answer rather than a shrug: the FRONTMOST match wins,
|
|
202
|
+
* because that is the one drawn on top and therefore the one the author sees at
|
|
203
|
+
* that point. Front is the stack (see `stacked`), which for a project nobody has
|
|
204
|
+
* restacked is the order the zones are listed in, exactly as before. Returns
|
|
205
|
+
* undefined for a point in open space.
|
|
206
|
+
*/
|
|
207
|
+
declare function zoneAt(point: ViewPoint, zones: (Stacked & {
|
|
208
|
+
polygon: Polygon;
|
|
209
|
+
})[]): string | undefined;
|
|
210
|
+
/**
|
|
211
|
+
* EVERY zone a point falls in, frontmost first.
|
|
212
|
+
*
|
|
213
|
+
* `zoneAt` is this list's head, so the two cannot disagree about which zone wins.
|
|
214
|
+
* The tail is what an author needs telling about: zones are allowed to overlap,
|
|
215
|
+
* so a site can sit inside two outlines while belonging to exactly one of them,
|
|
216
|
+
* and a picture that shows containment the model does not have is a picture that
|
|
217
|
+
* misleads. The editor uses the length of this to say so.
|
|
218
|
+
*/
|
|
219
|
+
declare function zonesAt(point: ViewPoint, zones: (Stacked & {
|
|
220
|
+
polygon: Polygon;
|
|
221
|
+
})[]): string[];
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* What a hand's relationship to one tag group is.
|
|
225
|
+
*
|
|
226
|
+
* `tag` is the tag id it is bound to, absent when the route exists but is not
|
|
227
|
+
* filled in yet (a hole nobody has chosen for). `editable` says whether this
|
|
228
|
+
* hand can be rebound on its own: the whole point of the distinction.
|
|
229
|
+
*/
|
|
230
|
+
type HandBinding =
|
|
231
|
+
/** A hole the hand's template declares and this hand fills. */
|
|
232
|
+
{
|
|
233
|
+
kind: "chosen";
|
|
234
|
+
tag?: string;
|
|
235
|
+
editable: true;
|
|
236
|
+
}
|
|
237
|
+
/** A standalone hand's own binding. */
|
|
238
|
+
| {
|
|
239
|
+
kind: "rule";
|
|
240
|
+
tag?: string;
|
|
241
|
+
editable: true;
|
|
242
|
+
}
|
|
243
|
+
/** The template binds it for every instance: not this hand's to change. */
|
|
244
|
+
| {
|
|
245
|
+
kind: "fixed";
|
|
246
|
+
tag: string;
|
|
247
|
+
editable: false;
|
|
248
|
+
}
|
|
249
|
+
/** The hand has nothing to do with this group. */
|
|
250
|
+
| {
|
|
251
|
+
kind: "none";
|
|
252
|
+
editable: false;
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* How `hand` is bound to `groupId`, given its template (undefined for a
|
|
256
|
+
* standalone hand, or when the template has gone missing).
|
|
257
|
+
*
|
|
258
|
+
* Order matters: a standalone hand answers from its own rule, and a template
|
|
259
|
+
* instance prefers the HOLE, because a template that both binds a group and
|
|
260
|
+
* declares it as a hole is malformed and the hole is the one an instance can
|
|
261
|
+
* actually fill.
|
|
262
|
+
*/
|
|
263
|
+
declare function handBinding<E>(hand: Hand<E>, template: HandTemplate<E> | undefined, groupId: string): HandBinding;
|
|
264
|
+
/**
|
|
265
|
+
* Take `hand` OFF its binding for `groupId`, IN PLACE, and say whether anything
|
|
266
|
+
* changed.
|
|
267
|
+
*
|
|
268
|
+
* The hand is left LOOSE, which for a template instance with a hole is an error
|
|
269
|
+
* the compiler already names ("missing chosen tag ... a hand is fully concrete").
|
|
270
|
+
* That is the point of clearing rather than quietly keeping the old value: a hand
|
|
271
|
+
* whose pin has ended up outside every zone genuinely has no zone, and an error
|
|
272
|
+
* an author can see beats a link that is silently wrong.
|
|
273
|
+
*/
|
|
274
|
+
declare function unbindHand<E>(hand: Hand<E>, template: HandTemplate<E> | undefined, groupId: string): boolean;
|
|
275
|
+
/**
|
|
276
|
+
* Bind `hand` to `tagId` for `groupId`, IN PLACE, and say whether anything
|
|
277
|
+
* changed.
|
|
278
|
+
*
|
|
279
|
+
* Refuses anything the binding says is not this hand's to change, so a caller
|
|
280
|
+
* cannot rebind one instance and silently move its siblings. The caller is
|
|
281
|
+
* expected to have asked `handBinding` first and offered the gesture only where
|
|
282
|
+
* it means something; this is the backstop rather than the manners.
|
|
283
|
+
*/
|
|
284
|
+
declare function bindHand<E>(hand: Hand<E>, template: HandTemplate<E> | undefined, groupId: string, tagId: string): boolean;
|
|
285
|
+
|
|
286
|
+
/** The frames on a canvas, in DRAW order (back to front). */
|
|
287
|
+
declare function framesOf(canvas: CanvasFurniture | undefined): Frame[];
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The comment sidecar for one box.
|
|
291
|
+
*
|
|
292
|
+
* A flat LIST rather than a map: a thread carries its own anchor, so there is
|
|
293
|
+
* no key to hang it on. Id'd so the merge engine can tell two reviewers apart.
|
|
294
|
+
*/
|
|
295
|
+
interface NotesShard {
|
|
296
|
+
schema: string;
|
|
297
|
+
comments?: Comment[];
|
|
298
|
+
}
|
|
299
|
+
/** One message in a thread. The author is a NAME, stamped at posting time, not a
|
|
300
|
+
* reference to anything: people leave projects and their words stay. */
|
|
301
|
+
interface CommentMessage {
|
|
302
|
+
author: string;
|
|
303
|
+
/** ISO 8601, stamped when posted. */
|
|
304
|
+
ts: string;
|
|
305
|
+
body: string;
|
|
306
|
+
/**
|
|
307
|
+
* A TOMBSTONE: withdrawn, but still a turn in the conversation.
|
|
308
|
+
*
|
|
309
|
+
* A reply removed outright would renumber the argument around it - the answer
|
|
310
|
+
* to a question nobody can see any more reads as a non-sequitur - so what is
|
|
311
|
+
* left records who spoke and when, and that they took it back. The body is
|
|
312
|
+
* EMPTIED when this is set: "deleted" has to mean gone from the file, or the
|
|
313
|
+
* word is a lie to somebody who typed something they regret.
|
|
314
|
+
*
|
|
315
|
+
* Absent on every message that was never withdrawn, so an ordinary thread's
|
|
316
|
+
* shard is unchanged.
|
|
317
|
+
*/
|
|
318
|
+
deleted?: true;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Where a thread is DRAWN, when it was dropped on a canvas rather than opened
|
|
322
|
+
* from an editor (design/annotation.md 3).
|
|
323
|
+
*
|
|
324
|
+
* `canvas` names the canvas it appears on: a deck id, or `map:<boxId>` for a
|
|
325
|
+
* box's map. `x`/`y` are that canvas's own coordinates when the thread is
|
|
326
|
+
* anchored to the canvas itself, and an OFFSET from the item's origin when the
|
|
327
|
+
* thread's `anchor` names an item on that canvas. One field, two readings,
|
|
328
|
+
* distinguished by a single comparison - `anchor === canvas` - because the
|
|
329
|
+
* alternative was a second key that could disagree with the first.
|
|
330
|
+
*
|
|
331
|
+
* A thread with no `mark` is not drawn anywhere: it was opened from an editor,
|
|
332
|
+
* and it lives in that document's topline where it always did.
|
|
333
|
+
*/
|
|
334
|
+
interface CommentMark {
|
|
335
|
+
canvas: string;
|
|
336
|
+
x: number;
|
|
337
|
+
y: number;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* A thread, anchored to the id of the thing it is about.
|
|
341
|
+
*
|
|
342
|
+
* `anchor` is the SUBJECT: an item id, or a canvas id for a comment about a
|
|
343
|
+
* place rather than a thing. `mark` is only about where it is drawn.
|
|
344
|
+
*/
|
|
345
|
+
interface Comment {
|
|
346
|
+
id: string;
|
|
347
|
+
anchor: string;
|
|
348
|
+
/** Marked complete: hidden unless the reviewer asks to see resolved threads. */
|
|
349
|
+
resolved?: boolean;
|
|
350
|
+
/** Present when this thread is a marker on a canvas. */
|
|
351
|
+
mark?: CommentMark;
|
|
352
|
+
messages: CommentMessage[];
|
|
353
|
+
}
|
|
354
|
+
/** Is this thread drawn as a marker, and is it following an item or sitting on
|
|
355
|
+
* the canvas? Answers both questions at once, so no caller has to remember the
|
|
356
|
+
* `anchor === canvas` rule. */
|
|
357
|
+
declare function markOf(thread: Comment): {
|
|
358
|
+
canvas: string;
|
|
359
|
+
x: number;
|
|
360
|
+
y: number;
|
|
361
|
+
item?: string;
|
|
362
|
+
} | undefined;
|
|
363
|
+
/** The threads drawn on one canvas, in the order they were written. */
|
|
364
|
+
declare const marksOn: (shard: NotesShard | undefined, canvas: string) => Comment[];
|
|
365
|
+
/** Every thread in the shard, forgiving of a mangled one. */
|
|
366
|
+
declare function commentsOf(shard: NotesShard | undefined): Comment[];
|
|
367
|
+
/** The threads about one thing, oldest first (their order in the shard). */
|
|
368
|
+
declare const threadsFor: (shard: NotesShard | undefined, anchor: string) => Comment[];
|
|
369
|
+
/** How many UNRESOLVED threads each anchor has: what a speech-bubble shows. A
|
|
370
|
+
* resolved thread is done, and a badge that kept counting it would never
|
|
371
|
+
* return to zero. */
|
|
372
|
+
declare function openThreadCounts(shard: NotesShard | undefined): Record<string, number>;
|
|
373
|
+
|
|
374
|
+
/** A confident reading of what `src` writes, or undefined. */
|
|
375
|
+
declare function inferDeclFromWrite(src: string): Pick<PropertyDecl, "type" | "default"> | undefined;
|
|
376
|
+
|
|
377
|
+
type PropertyType = "boolean" | "number" | "string" | "enum" | "flags" | "quality";
|
|
378
|
+
/** A property declaration: @world / @story / @box / @deck / tag / hand
|
|
379
|
+
* state. A declared property always has a value (`default` is required);
|
|
380
|
+
* referencing an undeclared property is a publish-time error. */
|
|
381
|
+
interface PropertyDecl {
|
|
382
|
+
name: string;
|
|
383
|
+
type: PropertyType;
|
|
384
|
+
default: ScalarValue;
|
|
385
|
+
values?: string[];
|
|
386
|
+
/**
|
|
387
|
+
* A quality's ordered ladder of stage names (design/quality.md). Order IS
|
|
388
|
+
* the meaning: `>=` compares by position here, and `advance()` steps along
|
|
389
|
+
* it. The one order-semantic list in the format, accepted as such: it is a
|
|
390
|
+
* declaration, and inserting a stage mid-ladder is the design's whole point.
|
|
391
|
+
*/
|
|
392
|
+
stages?: string[];
|
|
393
|
+
/**
|
|
394
|
+
* The sharing axis (design/flows.md, Patter's flag adopted): is this
|
|
395
|
+
* property's value one world value across all flows, or a copy per flow?
|
|
396
|
+
* It does NOT change reference syntax - sharing is set here, on the
|
|
397
|
+
* declaration, not by a different scope token. Absent = the scope
|
|
398
|
+
* default: `@story` shared; box, deck, hand and tag properties per-flow.
|
|
399
|
+
* On a `@world` declaration the flag is a validation error - `@world` is
|
|
400
|
+
* the game's own state, always engine-level, never per-flow.
|
|
401
|
+
*/
|
|
402
|
+
shared?: boolean;
|
|
403
|
+
purpose?: string;
|
|
404
|
+
}
|
|
405
|
+
/** A card-template field (box-defined). Data for the host; the engine never
|
|
406
|
+
* interprets fields and they are not addressable from expressions. */
|
|
407
|
+
interface FieldDecl {
|
|
408
|
+
name: string;
|
|
409
|
+
type: PropertyType;
|
|
410
|
+
default: ScalarValue;
|
|
411
|
+
values?: string[];
|
|
412
|
+
purpose?: string;
|
|
413
|
+
}
|
|
414
|
+
/** Cooldown policy, in turns (schema 3.4). */
|
|
415
|
+
type RedrawPolicy = "always" | "never" | number;
|
|
416
|
+
/** Slugify a human label into a filename- / address-safe gameId. */
|
|
417
|
+
declare function gameIdify(text: string): string;
|
|
418
|
+
declare function isValidGameId(gameId: string): boolean;
|
|
419
|
+
|
|
420
|
+
/** The effective address: a pinned gameId, else derived from the title, else
|
|
421
|
+
* the immutable id (so there is always something addressable). */
|
|
422
|
+
declare function effectiveGameId(entity: {
|
|
423
|
+
gameId?: string;
|
|
424
|
+
title?: string;
|
|
425
|
+
id: string;
|
|
426
|
+
}): string;
|
|
427
|
+
/**
|
|
428
|
+
* The first free gameId of the form `base`, `base-2`, `base-3`, ... not already
|
|
429
|
+
* in `taken`.
|
|
430
|
+
*
|
|
431
|
+
* A gameId is API - `deal()` and the play log speak it - so a name minted for a
|
|
432
|
+
* new or duplicated entity must not collide with an existing one. This lived in
|
|
433
|
+
* two copies, one in the editor and one in the CLI's kit scaffolder, character
|
|
434
|
+
* for character the same; two copies of an addressing rule can drift, and a
|
|
435
|
+
* drift here means the same act produces different addresses depending on which
|
|
436
|
+
* program did it. It is here, beside `gameIdify`, because both programs need it
|
|
437
|
+
* and no UI touches it.
|
|
438
|
+
*/
|
|
439
|
+
declare function freeGameId(base: string, taken: ReadonlySet<string>): string;
|
|
440
|
+
/**
|
|
441
|
+
* The first free TITLE of the form `base`, `base 2`, `base 3`, ... whose
|
|
442
|
+
* derived gameId is not already in `taken`.
|
|
443
|
+
*
|
|
444
|
+
* The sibling of `freeGameId` for the "New box", "New deck" case, where the
|
|
445
|
+
* author is given a title and the address follows from it. The dedupe is on the
|
|
446
|
+
* DERIVED gameId rather than the title, because two titles that slug to one
|
|
447
|
+
* address are the collision that matters.
|
|
448
|
+
*/
|
|
449
|
+
/**
|
|
450
|
+
* An id-sorted collection in the order a person should SEE it.
|
|
451
|
+
*
|
|
452
|
+
* Storage is sorted by immutable id (source rule 5) so that two authors adding
|
|
453
|
+
* one item each never touch the same line. That makes array position useless as
|
|
454
|
+
* display order, so the order the author arranged rides in a sparse `order`
|
|
455
|
+
* field, with position as the fallback and id to break a tie.
|
|
456
|
+
*
|
|
457
|
+
* One definition, because this rule has to give the same answer in four places
|
|
458
|
+
* that a reader compares side by side: the compiler (what the bundle carries),
|
|
459
|
+
* the editor (what the card document lists), the exports, and Find. When they
|
|
460
|
+
* disagree, the editor shows one order and the game plays another.
|
|
461
|
+
*/
|
|
462
|
+
declare function byDisplayOrder<T extends {
|
|
463
|
+
id?: string;
|
|
464
|
+
order?: number;
|
|
465
|
+
}>(items: readonly T[]): T[];
|
|
466
|
+
declare function freeTitle(base: string, taken: ReadonlySet<string>): string;
|
|
467
|
+
interface Outcome<E> {
|
|
468
|
+
id: string;
|
|
469
|
+
gameId?: string;
|
|
470
|
+
title?: string;
|
|
471
|
+
purpose?: string;
|
|
472
|
+
/** Authored display order (sparse; one without it falls back to its id
|
|
473
|
+
* position). Unlike `Card.order` this one IS compiled into the bundle:
|
|
474
|
+
* which option is offered first is authorial, and a host reading a dealt
|
|
475
|
+
* card's outcomes is building the player's menu. */
|
|
476
|
+
order?: number;
|
|
477
|
+
/** Gating; availability is always evaluated against current state. */
|
|
478
|
+
condition?: E;
|
|
479
|
+
/** Target ("@scope.name") -> expression; all right-hand sides evaluate
|
|
480
|
+
* against pre-play state (schema 3.7). */
|
|
481
|
+
changes: Record<string, E>;
|
|
482
|
+
}
|
|
483
|
+
interface Card<E> {
|
|
484
|
+
id: string;
|
|
485
|
+
gameId?: string;
|
|
486
|
+
title?: string;
|
|
487
|
+
purpose?: string;
|
|
488
|
+
/** Authored display order within the deck (sparse; a card without one falls
|
|
489
|
+
* back to its id position). Merges as a per-card value, so id-sorted storage
|
|
490
|
+
* stays merge-clean (Reboot 7.4); dropped from the compiled bundle. */
|
|
491
|
+
order?: number;
|
|
492
|
+
condition?: E;
|
|
493
|
+
/** Default 0; an expression must evaluate to a number. */
|
|
494
|
+
priority: number | E;
|
|
495
|
+
redraw: RedrawPolicy;
|
|
496
|
+
/** Tags: tag group id -> tag ids. An absent group is a wildcard (matches
|
|
497
|
+
* any binding of it), except the reserved home group, whose default
|
|
498
|
+
* inverts (schema 2.4). Editors and fixtures speak gameIds; stored
|
|
499
|
+
* references are ids. */
|
|
500
|
+
tags?: Record<string, string[]>;
|
|
501
|
+
/** How many hands may hold this card at once (schema 3.5): integer >= 1,
|
|
502
|
+
* default 1. One copy is the exclusivity rule; copies: N is the
|
|
503
|
+
* deliberate opt-out for interchangeable filler. Always counted WITHIN a
|
|
504
|
+
* flow, whether or not the card is shared. */
|
|
505
|
+
copies?: number;
|
|
506
|
+
/** Scarcity across flows (design/shared-scarcity.md). Absent takes the
|
|
507
|
+
* deck's flag; set here it overrides the deck, so a single unique card can
|
|
508
|
+
* stay in the content it belongs to. A shared card's claims count every
|
|
509
|
+
* flow's board, and a shared `redraw: "never"` is spent for everyone the
|
|
510
|
+
* first time anyone plays it.
|
|
511
|
+
*
|
|
512
|
+
* A finite `redraw` stays PER FLOW even when shared: a cooldown is an
|
|
513
|
+
* absolute turn of the card's box clock and clocks are per flow, so
|
|
514
|
+
* "3 turns of whose clock?" has no answer. A world-wide timer is a @world
|
|
515
|
+
* question, not an engine one (shared-scarcity 9.3.3). */
|
|
516
|
+
shared?: boolean;
|
|
517
|
+
/** The world cap: how many hands ACROSS EVERY FLOW may hold this at once.
|
|
518
|
+
* Read only when the card is shared, and defaults to `copies`, so the
|
|
519
|
+
* common case writes one number and "five in the world, one to a customer"
|
|
520
|
+
* is `copies: 1, sharedCopies: 5`. */
|
|
521
|
+
sharedCopies?: number;
|
|
522
|
+
/** Card-template data: field name -> value, validated at publish. */
|
|
523
|
+
fields?: Record<string, ScalarValue>;
|
|
524
|
+
outcomes: Outcome<E>[];
|
|
525
|
+
}
|
|
526
|
+
interface Deck<E> {
|
|
527
|
+
id: string;
|
|
528
|
+
gameId?: string;
|
|
529
|
+
title?: string;
|
|
530
|
+
purpose?: string;
|
|
531
|
+
/** The deck gate, evaluated once per draw in the draw's environment. */
|
|
532
|
+
condition?: E;
|
|
533
|
+
/** This pile is scarce across flows (design/shared-scarcity.md): every card
|
|
534
|
+
* in it is shared unless the card says otherwise. The container is where
|
|
535
|
+
* Patter puts its own shared-memory flag, and the deck is our container. */
|
|
536
|
+
shared?: boolean;
|
|
537
|
+
properties: PropertyDecl[];
|
|
538
|
+
cards: Card<E>[];
|
|
539
|
+
}
|
|
540
|
+
interface Tag {
|
|
541
|
+
id: string;
|
|
542
|
+
gameId?: string;
|
|
543
|
+
/**
|
|
544
|
+
* This tag's own starting values for properties its GROUP declares
|
|
545
|
+
* (design/hand-typing.md). The group says what the property IS; a tag says
|
|
546
|
+
* only where it starts, so "every zone has a haunting level" is written once
|
|
547
|
+
* and "the cave starts at 2" is written where it belongs.
|
|
548
|
+
*
|
|
549
|
+
* A name here that the group does not declare is an error: it would be a
|
|
550
|
+
* value for nothing.
|
|
551
|
+
*/
|
|
552
|
+
values?: Record<string, ScalarValue>;
|
|
553
|
+
/** Authored display order (sparse; one without it falls back to its id
|
|
554
|
+
* position). Merges as a per-item value, so id-sorted storage stays
|
|
555
|
+
* merge-clean (Reboot 7.4). */
|
|
556
|
+
order?: number;
|
|
557
|
+
properties?: PropertyDecl[];
|
|
558
|
+
/** Template-of-play extras (e.g. spatial geometry). Source only: preserved
|
|
559
|
+
* in shards, never compiled into the bundle. */
|
|
560
|
+
templates?: Record<string, unknown>;
|
|
561
|
+
}
|
|
562
|
+
/** A named axis for cross-cutting cards (schema 2.4, renamed from
|
|
563
|
+
* Dimension). Tags are declared, not freeform. */
|
|
564
|
+
interface TagGroup {
|
|
565
|
+
id: string;
|
|
566
|
+
gameId?: string;
|
|
567
|
+
purpose?: string;
|
|
568
|
+
/**
|
|
569
|
+
* A property reference (`"@story.act"`) whose value names a tag in this group
|
|
570
|
+
* by gameId. The engine reads it at every ask and binds the group, exactly as
|
|
571
|
+
* if the asking hand had chosen that tag; a hand's own binding wins.
|
|
572
|
+
*
|
|
573
|
+
* For an axis driven by STATE rather than by place: acts, chapters, a
|
|
574
|
+
* difficulty band. Without it, only a hand can bind a group, so such an axis
|
|
575
|
+
* had nowhere to gate and every card needed its own condition.
|
|
576
|
+
*
|
|
577
|
+
* A reference rather than an expression on purpose (design/where-and-
|
|
578
|
+
* selectors.md Part B): a computed binding belongs in a property the outcomes
|
|
579
|
+
* maintain, and an expression here would make this type generic for no gain.
|
|
580
|
+
*/
|
|
581
|
+
boundBy?: string;
|
|
582
|
+
/**
|
|
583
|
+
* What omitting this group means for a card. Default false: omission is a
|
|
584
|
+
* wildcard, so the card matches whatever the group is bound to. True inverts
|
|
585
|
+
* it, so a card that names no tag here is unavailable wherever the group IS
|
|
586
|
+
* bound (and unaffected where it is not).
|
|
587
|
+
*
|
|
588
|
+
* `place` is the built-in instance of this pair: bound to the asking hand,
|
|
589
|
+
* and inverted per card rather than per group.
|
|
590
|
+
*/
|
|
591
|
+
required?: boolean;
|
|
592
|
+
/** Authored display order (sparse; one without it falls back to its id
|
|
593
|
+
* position). Merges as a per-item value, so id-sorted storage stays
|
|
594
|
+
* merge-clean (Reboot 7.4). */
|
|
595
|
+
order?: number;
|
|
596
|
+
/**
|
|
597
|
+
* Properties EVERY tag in this group has (design/hand-typing.md). The
|
|
598
|
+
* declaration lives here and each tag carries only its own starting value in
|
|
599
|
+
* `Tag.values`, which is the separation the format was missing: a tag's own
|
|
600
|
+
* `properties` entry has to restate the type on every tag purely in order to
|
|
601
|
+
* say the value, and a tag added later silently arrives without it.
|
|
602
|
+
*
|
|
603
|
+
* Compiled by FLATTENING onto each tag, so the bundle keeps its per-tag
|
|
604
|
+
* shape and no runtime, port or bundle schema changes: source is where the
|
|
605
|
+
* author works and where merges happen, the bundle is a compiled artefact
|
|
606
|
+
* that can afford to be explicit.
|
|
607
|
+
*
|
|
608
|
+
* A tag may still declare its own `properties` for a group whose tags
|
|
609
|
+
* genuinely differ. Declaring the same NAME both ways is an error.
|
|
610
|
+
*/
|
|
611
|
+
properties?: PropertyDecl[];
|
|
612
|
+
tags: Tag[];
|
|
613
|
+
/** Template-of-play extras for the GROUP, the same bag its tags carry: this is
|
|
614
|
+
* where a group is marked spatial and where that template keeps its own
|
|
615
|
+
* group-level configuration. Source only, preserved but never compiled.
|
|
616
|
+
*
|
|
617
|
+
* A bag rather than a `spatial: true` flag because the marker and the
|
|
618
|
+
* configuration are one thing (see model/spatial.ts), and because core is not
|
|
619
|
+
* meant to grow a field per template of play. */
|
|
620
|
+
templates?: Record<string, unknown>;
|
|
621
|
+
}
|
|
622
|
+
/** The reserved tag group (schema 2.4): present in every box without
|
|
623
|
+
* declaration, its tags the box's hand ids. Every hand implicitly binds it to
|
|
624
|
+
* itself; a card that names a place is available only at that place.
|
|
625
|
+
*
|
|
626
|
+
* Called `place` rather than `home` since 2026-08-21: one word for one thing
|
|
627
|
+
* across the format, the editor and the exports. "Where" is the QUESTION a
|
|
628
|
+
* card answers (at a place, or anywhere in a region); "place" is the direct
|
|
629
|
+
* half of that answer. `home` was a metaphor an author had to learn, and it
|
|
630
|
+
* leaked into hand-edited shards and the docs. */
|
|
631
|
+
declare const PLACE_GROUP = "place";
|
|
632
|
+
/** A declared kind of hand (schema 2.6): live-inherited, author-side only,
|
|
633
|
+
* never called from game code. One condition governs every instance. */
|
|
634
|
+
interface HandTemplate<E> {
|
|
635
|
+
id: string;
|
|
636
|
+
gameId?: string;
|
|
637
|
+
title?: string;
|
|
638
|
+
purpose?: string;
|
|
639
|
+
/** Authored display order (sparse; one without it falls back to its id
|
|
640
|
+
* position). Merges as a per-item value, so id-sorted storage stays
|
|
641
|
+
* merge-clean (Reboot 7.4). */
|
|
642
|
+
order?: number;
|
|
643
|
+
/** Fixed tag bindings: tag group id -> tag id. */
|
|
644
|
+
bindings?: Record<string, string>;
|
|
645
|
+
/** The holes: tag group ids each instance fills (one tag each). */
|
|
646
|
+
chooses?: string[];
|
|
647
|
+
/** Shared availability condition, ANDed in (schema 3.1); evaluated per
|
|
648
|
+
* instance against that instance's composed @hand. */
|
|
649
|
+
condition?: E;
|
|
650
|
+
/** Default slot cap. */
|
|
651
|
+
slots: number | "unbounded";
|
|
652
|
+
/** Declared @hand state every instance carries. */
|
|
653
|
+
properties: PropertyDecl[];
|
|
654
|
+
}
|
|
655
|
+
/** A standalone hand's inline rule (schema 2.6): owned by the hand. */
|
|
656
|
+
interface HandRule<E> {
|
|
657
|
+
bindings?: Record<string, string>;
|
|
658
|
+
condition?: E;
|
|
659
|
+
slots: number | "unbounded";
|
|
660
|
+
}
|
|
661
|
+
/** A hand (schema 2.6): a template instance (template + chosen) or a
|
|
662
|
+
* standalone hand (rule). Exactly one of template / rule. Fully concrete:
|
|
663
|
+
* deal is name-only. */
|
|
664
|
+
interface Hand<E> {
|
|
665
|
+
id: string;
|
|
666
|
+
/** The name deal() is called with; a rename is a breaking change
|
|
667
|
+
* (Reboot 7.4). */
|
|
668
|
+
gameId?: string;
|
|
669
|
+
title?: string;
|
|
670
|
+
purpose?: string;
|
|
671
|
+
/** Hand template id (not gameId). */
|
|
672
|
+
template?: string;
|
|
673
|
+
/** Template instances: tag group id -> tag id, one per `chooses` hole. */
|
|
674
|
+
chosen?: Record<string, string>;
|
|
675
|
+
/** Standalone hands: the inline rule. */
|
|
676
|
+
rule?: HandRule<E>;
|
|
677
|
+
/** Override; defaults to the template's / rule's slots. The ONLY template
|
|
678
|
+
* field an instance may override (schema 2.6). */
|
|
679
|
+
slots?: number;
|
|
680
|
+
/** Standalone hands' own @hand state (template instances inherit the
|
|
681
|
+
* template's declarations). */
|
|
682
|
+
properties?: PropertyDecl[];
|
|
683
|
+
/** Authored display order within the box (sparse; authoring-only, never
|
|
684
|
+
* compiled into the bundle - the compiler's explicit field list drops it). */
|
|
685
|
+
order?: number;
|
|
686
|
+
/** Template-of-play extras (e.g. a spatial pin). Source only. */
|
|
687
|
+
templates?: Record<string, unknown>;
|
|
688
|
+
}
|
|
689
|
+
interface Box<E> {
|
|
690
|
+
id: string;
|
|
691
|
+
gameId?: string;
|
|
692
|
+
title?: string;
|
|
693
|
+
purpose?: string;
|
|
694
|
+
/** The only per-box ranking policy (Reboot 2.2). */
|
|
695
|
+
ranking: {
|
|
696
|
+
specificity: boolean;
|
|
697
|
+
};
|
|
698
|
+
/** The card template: what every card in this box carries. */
|
|
699
|
+
fields: FieldDecl[];
|
|
700
|
+
properties: PropertyDecl[];
|
|
701
|
+
tagGroups: TagGroup[];
|
|
702
|
+
decks: Deck<E>[];
|
|
703
|
+
handTemplates: HandTemplate<E>[];
|
|
704
|
+
hands: Hand<E>[];
|
|
705
|
+
}
|
|
706
|
+
declare const BUNDLE_SCHEMA = "storylets/bundle@0";
|
|
707
|
+
/** Binds bundles to shards (staleness gate) and saves to bundles. */
|
|
708
|
+
interface BundleContent {
|
|
709
|
+
project: string;
|
|
710
|
+
version: string;
|
|
711
|
+
/** hash32 over the canonical source shards (schema 2.8). */
|
|
712
|
+
hash: string;
|
|
713
|
+
}
|
|
714
|
+
interface BundleSettings {
|
|
715
|
+
playAdvancesTurns: number;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* A map that a bundle was asked to carry: one spatial tag group's geometry,
|
|
719
|
+
* flattened for a host to draw (design/graphical-views.md 2, "The map MAY ship").
|
|
720
|
+
*
|
|
721
|
+
* INERT PAYLOAD. Nothing in the engine reads this and nothing ever will: the
|
|
722
|
+
* runtime deals in tag names. It is here so a host that wants an in-game map does
|
|
723
|
+
* not have to invent its own export, and it is absent unless the project asked
|
|
724
|
+
* for it (`export.map`), so a build that does not want a map carries no bytes.
|
|
725
|
+
*
|
|
726
|
+
* GAME IDS throughout, never internal ids. Internal ids are authoring identity
|
|
727
|
+
* and mean nothing outside the project; a host matches these against the same
|
|
728
|
+
* names it passes to `peek`. There is nothing here to strip either, which is why
|
|
729
|
+
* `metadata: "stripped"` needs no special case: no titles, no purposes.
|
|
730
|
+
*
|
|
731
|
+
* Sites are deliberately NOT here. A site is where an author parked a hand while
|
|
732
|
+
* working, held in the view sidecar precisely because it is not content, and a
|
|
733
|
+
* host that wants to place a hand already has its zone from the compiled binding.
|
|
734
|
+
*/
|
|
735
|
+
interface BundleMap {
|
|
736
|
+
/** The owning box, by gameId (tag groups are box-scoped). */
|
|
737
|
+
box: string;
|
|
738
|
+
/** The tag group this is a map of, by gameId. */
|
|
739
|
+
group: string;
|
|
740
|
+
/** One entry per zone that has been drawn; a tag with no polygon is not a
|
|
741
|
+
* place yet and is left out rather than shipped as an empty shape. */
|
|
742
|
+
zones: {
|
|
743
|
+
tag: string;
|
|
744
|
+
polygon: ViewPoint[];
|
|
745
|
+
}[];
|
|
746
|
+
/** Background pictures, back to front, as bundle-relative paths. Hidden ones
|
|
747
|
+
* do not ship: what an author put away is not something to spring on a host. */
|
|
748
|
+
backgrounds?: BundleBackground[];
|
|
749
|
+
}
|
|
750
|
+
/** One shipped picture. `locked` and `hidden` are authoring state and do not
|
|
751
|
+
* travel; the draw order is the array order. */
|
|
752
|
+
interface BundleBackground {
|
|
753
|
+
/** Where the file sits relative to the bundle ("assets/<box>/<file>"). */
|
|
754
|
+
file: string;
|
|
755
|
+
x: number;
|
|
756
|
+
y: number;
|
|
757
|
+
width: number;
|
|
758
|
+
height: number;
|
|
759
|
+
opacity?: number;
|
|
760
|
+
}
|
|
761
|
+
interface Bundle {
|
|
762
|
+
schema: typeof BUNDLE_SCHEMA;
|
|
763
|
+
content: BundleContent;
|
|
764
|
+
metadata: "full" | "stripped";
|
|
765
|
+
settings: BundleSettings;
|
|
766
|
+
world: {
|
|
767
|
+
properties: PropertyDecl[];
|
|
768
|
+
/** ScopeRegistrySpec (@wildwinter/scoperegistry): the owned/foreign
|
|
769
|
+
* split. Absent = engine-owned @world (standalone play). */
|
|
770
|
+
registry?: unknown;
|
|
771
|
+
};
|
|
772
|
+
story: {
|
|
773
|
+
properties: PropertyDecl[];
|
|
774
|
+
};
|
|
775
|
+
boxes: Box<Expression>[];
|
|
776
|
+
/** Maps, when the project asked for them. Absent is the normal state. */
|
|
777
|
+
maps?: BundleMap[];
|
|
778
|
+
}
|
|
779
|
+
declare const SAVE_SCHEMA = "storylets/save@1";
|
|
780
|
+
interface PlayRecord {
|
|
781
|
+
/** Card and outcome by gameId (feeds the play-history functions). */
|
|
782
|
+
card: string;
|
|
783
|
+
outcome: string;
|
|
784
|
+
turn: number;
|
|
785
|
+
}
|
|
786
|
+
/** A property bag: name -> value. */
|
|
787
|
+
type PropertyBag = Record<string, ScalarValue>;
|
|
788
|
+
/** The per-scope property partitions one side of the sharing flag holds:
|
|
789
|
+
* a save carries one of these for the shared values and one per flow
|
|
790
|
+
* (design/flows.md). NO world key, in either: @world is the game's own
|
|
791
|
+
* state, resolved through the world resolver and saved by whoever owns
|
|
792
|
+
* it - "host saves its container once, each engine saves its own
|
|
793
|
+
* envelope" (engine-runtimes.md 3.1). */
|
|
794
|
+
interface PropsPartition {
|
|
795
|
+
story: PropertyBag;
|
|
796
|
+
box: Record<string, PropertyBag>;
|
|
797
|
+
deck: Record<string, PropertyBag>;
|
|
798
|
+
hand: Record<string, PropertyBag>;
|
|
799
|
+
/** Tag state, keyed by tag id. */
|
|
800
|
+
value: Record<string, PropertyBag>;
|
|
801
|
+
}
|
|
802
|
+
/** One flow's snapshot inside the envelope (schema 4). */
|
|
803
|
+
interface FlowSave {
|
|
804
|
+
/** The per-flow property partitions. */
|
|
805
|
+
props: PropsPartition;
|
|
806
|
+
/** Per-box turn counters, keyed by box id (schema 3.4) - per flow: there
|
|
807
|
+
* is deliberately no global turn. */
|
|
808
|
+
turns: Record<string, number>;
|
|
809
|
+
/** mulberry32 state, uint32 (schema 3.3), per flow. */
|
|
810
|
+
prng: number;
|
|
811
|
+
/** Absolute next-eligible turn (of the card's box's clock) per card id;
|
|
812
|
+
* MAX_SAFE_INTEGER = never (deliberately not Infinity, which
|
|
813
|
+
* JSON-serialises to null). */
|
|
814
|
+
cooldowns: Record<string, number>;
|
|
815
|
+
/** Hand contents (card ids, in dealt order), keyed by hand id. The claims
|
|
816
|
+
* ledger is derived from this (schema 3.5). */
|
|
817
|
+
board: Record<string, string[]>;
|
|
818
|
+
playLog: PlayRecord[];
|
|
819
|
+
}
|
|
820
|
+
/** The whole engine, one envelope: the shared partitions once, then every
|
|
821
|
+
* live flow keyed by its id - Patter's shape (one shared blob + N flow
|
|
822
|
+
* blobs; multi-flow and save/load are the same feature). */
|
|
823
|
+
/** The engine's half of a save: what every flow shares. Properties, and the
|
|
824
|
+
* cards a shared `redraw: "never"` has taken out of the world for good
|
|
825
|
+
* (design/shared-scarcity.md). Claims are NOT here: they are derived from the
|
|
826
|
+
* live boards, and each flow's board rides its own blob. */
|
|
827
|
+
interface SharedSave {
|
|
828
|
+
props: PropsPartition;
|
|
829
|
+
/** Card ids, sorted, so a save is byte-stable for a diff. */
|
|
830
|
+
spent: string[];
|
|
831
|
+
}
|
|
832
|
+
interface SaveEnvelope {
|
|
833
|
+
schema: typeof SAVE_SCHEMA;
|
|
834
|
+
content: BundleContent;
|
|
835
|
+
shared: SharedSave;
|
|
836
|
+
flows: Record<string, FlowSave>;
|
|
837
|
+
}
|
|
838
|
+
/** The .storyletsave FILE: the HOST's file, not the engine's - the engine's
|
|
839
|
+
* envelope plus, when the host keeps one, its @world container. This is
|
|
840
|
+
* "host saves its container once, each engine saves its own envelope"
|
|
841
|
+
* folded into one file for the single-host case; the ENGINE never reads or
|
|
842
|
+
* writes `world` (loadGame takes the envelope alone). */
|
|
843
|
+
declare const SAVEFILE_SCHEMA = "storylets/savefile@1";
|
|
844
|
+
interface SaveFile {
|
|
845
|
+
schema: typeof SAVEFILE_SCHEMA;
|
|
846
|
+
engine: SaveEnvelope;
|
|
847
|
+
/** The host's @world values, saved and restored by the host. */
|
|
848
|
+
world?: PropertyBag;
|
|
849
|
+
}
|
|
850
|
+
/** The project folder: a macOS package, a plain folder elsewhere. */
|
|
851
|
+
declare const PROJECT_FOLDER_EXTENSION = ".storylets";
|
|
852
|
+
/** The compiled bundle (strict JSON; generated, never hand-edited). */
|
|
853
|
+
declare const BUNDLE_EXTENSION = ".storyletsc";
|
|
854
|
+
/**
|
|
855
|
+
* Where a shipped background sits, relative to the bundle file.
|
|
856
|
+
*
|
|
857
|
+
* One function so the compiler (which writes the name into the bundle) and the
|
|
858
|
+
* export op (which writes the bytes) cannot drift apart: a path agreed in two
|
|
859
|
+
* places is a path that eventually disagrees. Per BOX, because two boxes may
|
|
860
|
+
* each have their own `plan.png` and a build must not silently keep one of them.
|
|
861
|
+
*/
|
|
862
|
+
declare const bundleAssetPath: (boxGameId: string, file: string) => string;
|
|
863
|
+
/** Per-type shard extensions, JSON5 inside (source doc section 2). */
|
|
864
|
+
declare const SHARD_EXTENSIONS: {
|
|
865
|
+
readonly project: ".storyletproj";
|
|
866
|
+
readonly box: ".storyletbox";
|
|
867
|
+
readonly tags: ".storylettags";
|
|
868
|
+
readonly hands: ".storylethands";
|
|
869
|
+
readonly deck: ".storyletdeck";
|
|
870
|
+
/** The arrangement layer: where things SIT, never what they are. Its own shard
|
|
871
|
+
* because positions churn (an afternoon of tidying a canvas touches every
|
|
872
|
+
* card) and content does not, so a designer arranging and a writer editing
|
|
873
|
+
* never collide on one file (design/graphical-views.md section 1.2). */
|
|
874
|
+
readonly view: ".storyletview";
|
|
875
|
+
/** Threaded comments: content-ADJACENT, so neither in a content shard (a
|
|
876
|
+
* writer's deck edit must not conflict with a reviewer's comment) nor in the
|
|
877
|
+
* arrangement sidecar (this is not where anything sits). One per box,
|
|
878
|
+
* id-keyed (design/annotation.md). Documentation NOTES used to share this
|
|
879
|
+
* file and were retired: `purpose` already says why a thing exists, and
|
|
880
|
+
* Patterpad's typed routing has no destination here. */
|
|
881
|
+
readonly notes: ".storyletnotes";
|
|
882
|
+
};
|
|
883
|
+
declare const PROJECT_SCHEMA = "storylets/project@0";
|
|
884
|
+
declare const BOX_SCHEMA = "storylets/box@0";
|
|
885
|
+
declare const TAGS_SCHEMA = "storylets/tags@0";
|
|
886
|
+
declare const HANDS_SCHEMA = "storylets/hands@0";
|
|
887
|
+
declare const DECK_SCHEMA = "storylets/deck@0";
|
|
888
|
+
declare const VIEW_SCHEMA = "storylets/view@0";
|
|
889
|
+
/** The comment sidecar's schema. Still called "notes" on disk: the file already
|
|
890
|
+
* held both, and renaming it would break every project for no gain. */
|
|
891
|
+
declare const NOTES_SCHEMA = "storylets/notes@0";
|
|
892
|
+
/** A point in a canvas's own coordinates. */
|
|
893
|
+
interface ViewPoint {
|
|
894
|
+
x: number;
|
|
895
|
+
y: number;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Canvas furniture: what an author draws AROUND the content to make sense of it
|
|
899
|
+
* (design/graphical-views.md 3, "Frames and sites").
|
|
900
|
+
*
|
|
901
|
+
* Both canvases carry the same thing, which is why they share a type: a node
|
|
902
|
+
* canvas and a map are different views of different material, but "put a box
|
|
903
|
+
* round this lot and call it act two" is the same thought on either.
|
|
904
|
+
*
|
|
905
|
+
* It lives in the view sidecar because it is ARRANGEMENT. Nothing here is
|
|
906
|
+
* content: no runtime reads it, no bundle carries it, and deleting the sidecar
|
|
907
|
+
* loses only the drawing. Threaded comments are the
|
|
908
|
+
* other thing entirely - they attach to entities and they travel - but a canvas
|
|
909
|
+
* DRAWS their markers, while owning none of them.
|
|
910
|
+
*/
|
|
911
|
+
interface CanvasFurniture {
|
|
912
|
+
/** Titled areas behind the content, back to front (see `stacked`).
|
|
913
|
+
*
|
|
914
|
+
* There was a second kind, a `stickies` list, retired on 2026-08-10
|
|
915
|
+
* (design/annotation.md): a dropped comment marker does the same job in a
|
|
916
|
+
* fraction of the space, and an annotation that takes as much room as the
|
|
917
|
+
* thing it is about is a bad trade on a canvas. */
|
|
918
|
+
frames?: Frame[];
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* A titled area behind a group of things: Unreal's comment box.
|
|
922
|
+
*
|
|
923
|
+
* Deliberately dumb about what is inside it. It has no membership list and
|
|
924
|
+
* computes none: a frame is a thing an author DREW, and the cards under it are
|
|
925
|
+
* whatever happens to be under it now. That is what keeps it honest when content
|
|
926
|
+
* moves, and it is the same reasoning that keeps a zone's sites out of the map's
|
|
927
|
+
* sidecar.
|
|
928
|
+
*/
|
|
929
|
+
interface Frame extends ViewPoint {
|
|
930
|
+
id: string;
|
|
931
|
+
w: number;
|
|
932
|
+
h: number;
|
|
933
|
+
/** Shown in the frame's bar, and the handle it is dragged by. */
|
|
934
|
+
title?: string;
|
|
935
|
+
/** One of the furniture palette's names (see `FURNITURE_COLOURS`); the theme
|
|
936
|
+
* decides what that looks like, so a frame does not carry a hex value that
|
|
937
|
+
* would fight the palette on the day somebody switches theme. */
|
|
938
|
+
colour?: string;
|
|
939
|
+
/** Place in the frame band (sparse, `stacked`). Frames can nest. */
|
|
940
|
+
z?: number;
|
|
941
|
+
}
|
|
942
|
+
/** The furniture palette: names, not colours. The theme maps them, so the same
|
|
943
|
+
* shard reads correctly on linen and on baize. */
|
|
944
|
+
declare const FURNITURE_COLOURS: readonly ["paper", "amber", "sage", "sky", "rose", "slate"];
|
|
945
|
+
type FurnitureColour = typeof FURNITURE_COLOURS[number];
|
|
946
|
+
/** One deck's node canvas: where its cards sit, and the furniture around them.
|
|
947
|
+
* Sparse throughout. A card with no entry lays out by default, and an entry for
|
|
948
|
+
* a card that no longer exists is inert, so there is no referential integrity to
|
|
949
|
+
* maintain against content that moves underneath. */
|
|
950
|
+
interface DeckCanvas extends CanvasFurniture {
|
|
951
|
+
/** Keyed by CARD id. */
|
|
952
|
+
cards?: Record<string, ViewPoint>;
|
|
953
|
+
}
|
|
954
|
+
/** The box's map: where its hands sit in space, and the furniture around them. */
|
|
955
|
+
interface BoxMap extends CanvasFurniture {
|
|
956
|
+
/** Keyed by HAND id. WHERE a site is, and nothing else.
|
|
957
|
+
*
|
|
958
|
+
* Which zone it is IN is not recorded here, and deliberately (2026-08-06,
|
|
959
|
+
* with the rebinding drag): a hand that binds a zone already says so in its
|
|
960
|
+
* own shard, as `chosen` or as a rule binding, and that is the truth the
|
|
961
|
+
* runtime deals from. A copy here could only ever go on to disagree with it,
|
|
962
|
+
* and a site whose recorded zone contradicts the hand it stands for would be
|
|
963
|
+
* the most misleading thing on the map.
|
|
964
|
+
*
|
|
965
|
+
* Called `pins` until 2026-08-10 (design/annotation.md). No compatibility
|
|
966
|
+
* branch: the only projects that exist are the examples in this repo, and they
|
|
967
|
+
* were edited. */
|
|
968
|
+
sites?: Record<string, ViewPoint>;
|
|
969
|
+
}
|
|
970
|
+
/** The arrangement layer for one box: where things SIT, never what they are.
|
|
971
|
+
*
|
|
972
|
+
* Its own shard on purpose (design/graphical-views.md section 1.2). Positions
|
|
973
|
+
* churn, content does not: an afternoon of tidying a canvas touches every card,
|
|
974
|
+
* and if that lived in the deck shard then a designer arranging and a writer
|
|
975
|
+
* editing card text would collide on one file all day, while a content review
|
|
976
|
+
* would be full of coordinates. Keyed by id throughout so the existing merge
|
|
977
|
+
* engine handles two designers rearranging different things without a conflict.
|
|
978
|
+
*
|
|
979
|
+
* Source-only. It never reaches the compiled bundle, exactly as `order` does
|
|
980
|
+
* not: the compiler reads the fields it names and this is not among them. */
|
|
981
|
+
interface ViewShard {
|
|
982
|
+
schema: typeof VIEW_SCHEMA;
|
|
983
|
+
/** Keyed by DECK id: one node canvas each. */
|
|
984
|
+
canvases?: Record<string, DeckCanvas>;
|
|
985
|
+
map?: BoxMap;
|
|
986
|
+
}
|
|
987
|
+
/** A coverage input driver: during a coverage run the harness feeds a
|
|
988
|
+
* host-seam property (`@world.x`) values from `values`, so content gated on
|
|
989
|
+
* external state gets exercised (Patter's coverageDrivers, carried whole). */
|
|
990
|
+
interface CoverageDriver {
|
|
991
|
+
/** "initial": set once as each playthrough starts. "recurring": re-rolled
|
|
992
|
+
* per turn at the cadence, so one run passes through several states. */
|
|
993
|
+
kind: "initial" | "recurring";
|
|
994
|
+
/** For recurring drivers: how often to re-roll per turn (default "sometimes"). */
|
|
995
|
+
cadence?: "rarely" | "sometimes" | "often";
|
|
996
|
+
/** The pool the harness picks from (uniform). Empty = inert. */
|
|
997
|
+
values: ScalarValue[];
|
|
998
|
+
}
|
|
999
|
+
/** Authoring-side coverage configuration (never compiled into the bundle). */
|
|
1000
|
+
interface CoverageConfig {
|
|
1001
|
+
/** Property drivers, keyed by ref ("@world.danger"). */
|
|
1002
|
+
drivers?: Record<string, CoverageDriver>;
|
|
1003
|
+
}
|
|
1004
|
+
interface ProjectShard {
|
|
1005
|
+
schema: typeof PROJECT_SCHEMA;
|
|
1006
|
+
project: {
|
|
1007
|
+
id: string;
|
|
1008
|
+
name: string;
|
|
1009
|
+
version: string;
|
|
1010
|
+
};
|
|
1011
|
+
settings: BundleSettings;
|
|
1012
|
+
/** Coverage drivers + argument domains (authoring/testing config; stays
|
|
1013
|
+
* out of the compiled bundle). */
|
|
1014
|
+
coverage?: CoverageConfig;
|
|
1015
|
+
/** Validation switches (authoring config; never compiled). Off is written
|
|
1016
|
+
* as ABSENT, like `export.map`: a shard says what an author chose. */
|
|
1017
|
+
validation?: {
|
|
1018
|
+
/** Also warn when state is WRITTEN but nothing reads it. Off by default:
|
|
1019
|
+
* cards are routinely written ahead of the content that will read them,
|
|
1020
|
+
* so mid-development this warning is mostly noise. The read side (a gate
|
|
1021
|
+
* on state nothing writes) always warns, because that kills cards now. */
|
|
1022
|
+
warnUnreadWrites?: boolean;
|
|
1023
|
+
};
|
|
1024
|
+
world: {
|
|
1025
|
+
properties: PropertyDecl[];
|
|
1026
|
+
registry?: unknown;
|
|
1027
|
+
};
|
|
1028
|
+
story: {
|
|
1029
|
+
properties: PropertyDecl[];
|
|
1030
|
+
};
|
|
1031
|
+
/** Templates of play: configuration bags keyed by template name. Core
|
|
1032
|
+
* validates only what it knows. */
|
|
1033
|
+
templates: Record<string, unknown>;
|
|
1034
|
+
export: {
|
|
1035
|
+
bundle: string;
|
|
1036
|
+
metadata: "full" | "stripped";
|
|
1037
|
+
/**
|
|
1038
|
+
* Does a `.storyletpack` carry the boxes' binary assets (background images)?
|
|
1039
|
+
*
|
|
1040
|
+
* Default false, and a project-level DEFAULT rather than a rule: a pack is a
|
|
1041
|
+
* delivery, so the caller can override it per pack (2026-08-07). Some
|
|
1042
|
+
* projects would benefit from sending their pictures in certain
|
|
1043
|
+
* circumstances and others never would, which is why neither "always" nor
|
|
1044
|
+
* "never" is the answer.
|
|
1045
|
+
*
|
|
1046
|
+
* Nothing to do with the compiled bundle, which has its own switch: `map`.
|
|
1047
|
+
*/
|
|
1048
|
+
packAssets?: boolean;
|
|
1049
|
+
/**
|
|
1050
|
+
* Does the compiled bundle carry the maps (zone shapes and background
|
|
1051
|
+
* pictures)?
|
|
1052
|
+
*
|
|
1053
|
+
* Default false, and the default matters: geometry is authoring data, the
|
|
1054
|
+
* runtime deals in tag names, and a shipping build should carry nothing it
|
|
1055
|
+
* does not use. But a host that wants an in-game map should not have to
|
|
1056
|
+
* invent its own export, and it is most useful early - a prototype with a
|
|
1057
|
+
* real map beats a prototype with a list of zone names.
|
|
1058
|
+
*
|
|
1059
|
+
* It sits beside `metadata` on purpose: that is already the switch for
|
|
1060
|
+
* "authoring data that may or may not ship", and this is its sibling rather
|
|
1061
|
+
* than a new concept. With it on, `export` also writes the background files
|
|
1062
|
+
* next to the bundle, and `describeBundle` says what is in there.
|
|
1063
|
+
*/
|
|
1064
|
+
map?: boolean;
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
interface BoxShard {
|
|
1068
|
+
schema: typeof BOX_SCHEMA;
|
|
1069
|
+
box: {
|
|
1070
|
+
id: string;
|
|
1071
|
+
gameId?: string;
|
|
1072
|
+
title?: string;
|
|
1073
|
+
purpose?: string;
|
|
1074
|
+
/** Authored display order among boxes (sparse; absent falls back to the
|
|
1075
|
+
* folder-name position). Authoring-only, like a card's (never compiled
|
|
1076
|
+
* into the bundle); merges as a per-field value. */
|
|
1077
|
+
order?: number;
|
|
1078
|
+
ranking: {
|
|
1079
|
+
specificity: boolean;
|
|
1080
|
+
};
|
|
1081
|
+
fields: FieldDecl[];
|
|
1082
|
+
properties: PropertyDecl[];
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
/** The box's tag groups: how its cards are filed. */
|
|
1086
|
+
interface TagsShard {
|
|
1087
|
+
schema: typeof TAGS_SCHEMA;
|
|
1088
|
+
groups: TagGroup[];
|
|
1089
|
+
}
|
|
1090
|
+
/** The box's hand templates + hands (the writer/programmer contract). */
|
|
1091
|
+
interface HandsShard {
|
|
1092
|
+
schema: typeof HANDS_SCHEMA;
|
|
1093
|
+
templates: HandTemplate<string>[];
|
|
1094
|
+
hands: Hand<string>[];
|
|
1095
|
+
}
|
|
1096
|
+
interface DeckShard {
|
|
1097
|
+
schema: typeof DECK_SCHEMA;
|
|
1098
|
+
deck: {
|
|
1099
|
+
id: string;
|
|
1100
|
+
gameId?: string;
|
|
1101
|
+
title?: string;
|
|
1102
|
+
purpose?: string;
|
|
1103
|
+
condition?: string;
|
|
1104
|
+
/** Scarce across flows: see Deck.shared. */
|
|
1105
|
+
shared?: boolean;
|
|
1106
|
+
/** Authored display order within the box (sparse; see BoxShard). */
|
|
1107
|
+
order?: number;
|
|
1108
|
+
properties: PropertyDecl[];
|
|
1109
|
+
};
|
|
1110
|
+
cards: Card<string>[];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export { BOX_SCHEMA, BUNDLE_EXTENSION, BUNDLE_SCHEMA, type Box, type BoxMap, type BoxShard, type Bundle, type BundleBackground, type BundleContent, type BundleMap, type BundleSettings, type CanvasFurniture, type Card, type Comment, type CommentMark, type CommentMessage, type CoverageConfig, type CoverageDriver, DECK_SCHEMA, type Deck, type DeckCanvas, type DeckShard, FURNITURE_COLOURS, type FieldDecl, type FlowSave, type Frame, type FurnitureColour, HANDS_SCHEMA, type Hand, type HandBinding, type HandRule, type HandTemplate, type HandsShard, NOTES_SCHEMA, type NotesShard, type Outcome, PLACE_GROUP, PROJECT_FOLDER_EXTENSION, PROJECT_SCHEMA, type PlayRecord, type Polygon, type ProjectShard, type PropertyBag, type PropertyDecl, type PropertyType, type PropsPartition, type Rect, type RedrawPolicy, SAVEFILE_SCHEMA, SAVE_SCHEMA, SHARD_EXTENSIONS, SPATIAL, type SaveEnvelope, type SaveFile, type SharedSave, type SpatialBackground, type SpatialGroup, type StackMove, type Stacked, TAGS_SCHEMA, type Tag, type TagGroup, type TagsShard, VIEW_SCHEMA, type ViewPoint, type ViewShard, backgroundsOf, bindHand, bundleAssetPath, byDisplayOrder, centroid, commentsOf, droppedRect, effectiveGameId, framesOf, freeGameId, freeTitle, gameIdify, handBinding, inferDeclFromWrite, isSpatial, isValidGameId, labelPoint, markOf, marksOn, openThreadCounts, pointInPolygon, polygonBounds, polygonOf, restack, spatialOf, stacked, threadsFor, unbindHand, withBackgrounds, withPolygon, withSpatialGroup, withZ, zOf, zoneAt, zonesAt };
|