@energy8platform/game-engine 0.34.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio.cjs.js +114 -59
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +25 -0
- package/dist/audio.esm.js +114 -59
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +222 -66
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +25 -0
- package/dist/core.esm.js +223 -67
- package/dist/core.esm.js.map +1 -1
- package/dist/flow.cjs.js +246 -0
- package/dist/flow.cjs.js.map +1 -1
- package/dist/flow.d.ts +192 -33
- package/dist/flow.esm.js +238 -1
- package/dist/flow.esm.js.map +1 -1
- package/dist/host.cjs.js +343 -82
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +82 -2
- package/dist/host.esm.js +344 -83
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +222 -66
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +72 -0
- package/dist/index.esm.js +223 -67
- package/dist/index.esm.js.map +1 -1
- package/dist/scene-devtools.cjs.js +529 -115
- package/dist/scene-devtools.cjs.js.map +1 -1
- package/dist/scene-devtools.d.ts +187 -34
- package/dist/scene-devtools.esm.js +529 -115
- package/dist/scene-devtools.esm.js.map +1 -1
- package/dist/scene.cjs.js +704 -46
- package/dist/scene.cjs.js.map +1 -1
- package/dist/scene.d.ts +228 -41
- package/dist/scene.esm.js +698 -47
- package/dist/scene.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/audio/AudioManager.ts +111 -53
- package/src/core/GameApplication.ts +47 -5
- package/src/host/buildConfig.ts +17 -4
- package/src/host/createSlotGame.ts +114 -12
- package/src/host/index.ts +3 -0
- package/src/host/types.ts +58 -0
- package/src/loading/LoadingScene.ts +76 -2
- package/src/loading/index.ts +6 -0
- package/src/types.ts +2 -0
package/dist/scene.esm.js
CHANGED
|
@@ -1,5 +1,247 @@
|
|
|
1
1
|
import { Ticker, Container, Graphics, Text, Sprite, BlurFilter, Texture, Rectangle, AnimatedSprite } from 'pixi.js';
|
|
2
2
|
|
|
3
|
+
// Scene composition: one scene doc extending another, and the diff between two resolved
|
|
4
|
+
// docs expressed as patches.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists: each game mode gets its OWN scene (base, free spins, a bonus screen), so
|
|
7
|
+
// you open it in the editor and lay it out directly instead of guessing at overrides. But a
|
|
8
|
+
// survey of nine shipped games found ~86% of modes are "the same board, re-skinned" — the
|
|
9
|
+
// delta is usually a background texture, a music track and one or two HUD widgets. Without
|
|
10
|
+
// inheritance every free-spins scene would be a copy of the base scene, and the two would
|
|
11
|
+
// drift apart the first time anyone moved the reels.
|
|
12
|
+
//
|
|
13
|
+
// So: `free-spins.scene.json` declares `extends: "base.scene.json"` and carries only what
|
|
14
|
+
// differs. And because no real game rebuilds its board when a mode starts — the reel system
|
|
15
|
+
// is constructed once and lives for the whole scene — switching modes applies the DIFF to
|
|
16
|
+
// the live scene rather than tearing it down.
|
|
17
|
+
/** A node in an extending doc may delete an inherited node instead of overriding it. */
|
|
18
|
+
const REMOVE_MARKER = '$remove';
|
|
19
|
+
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
20
|
+
/**
|
|
21
|
+
* Merge a child node over an inherited one. Only the fields the child states are replaced,
|
|
22
|
+
* so a free-spins scene that changes a background's texture keeps its layout, filters and
|
|
23
|
+
* everything else. `children` merge by id (see mergeChildren).
|
|
24
|
+
*/
|
|
25
|
+
function mergeNode(base, over) {
|
|
26
|
+
const out = { ...base };
|
|
27
|
+
if (over.type !== undefined && over.type !== base.type)
|
|
28
|
+
out.type = over.type;
|
|
29
|
+
if (over.name !== undefined)
|
|
30
|
+
out.name = over.name;
|
|
31
|
+
// Props merge key-wise, so `{ src: 'bg-fs' }` does not wipe a tint set in the base.
|
|
32
|
+
if (over.props)
|
|
33
|
+
out.props = { ...(base.props ?? {}), ...over.props };
|
|
34
|
+
if (over.layout !== undefined)
|
|
35
|
+
out.layout = over.layout;
|
|
36
|
+
if (over.responsive)
|
|
37
|
+
out.responsive = { ...(base.responsive ?? {}), ...over.responsive };
|
|
38
|
+
if (over.states)
|
|
39
|
+
out.states = { ...(base.states ?? {}), ...over.states };
|
|
40
|
+
if (over.mask !== undefined)
|
|
41
|
+
out.mask = over.mask;
|
|
42
|
+
if (over.filters !== undefined)
|
|
43
|
+
out.filters = over.filters;
|
|
44
|
+
if (over.visibleWhen !== undefined)
|
|
45
|
+
out.visibleWhen = over.visibleWhen;
|
|
46
|
+
if (over.anchorName !== undefined)
|
|
47
|
+
out.anchorName = over.anchorName;
|
|
48
|
+
out.children = mergeChildren(base.children, over.children);
|
|
49
|
+
if (out.children === undefined)
|
|
50
|
+
delete out.children;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Merge child lists by id. An id present in both is merged; an id only in the child is
|
|
55
|
+
* appended (so a mode can add a spins counter); `{id, type: '$remove'}` deletes an
|
|
56
|
+
* inherited node. Base order is preserved — z-order is structural, not incidental.
|
|
57
|
+
*/
|
|
58
|
+
function mergeChildren(base, over) {
|
|
59
|
+
if (!over)
|
|
60
|
+
return base ? clone(base) : undefined;
|
|
61
|
+
const byId = new Map();
|
|
62
|
+
for (const node of over)
|
|
63
|
+
byId.set(node.id, node);
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const node of base ?? []) {
|
|
66
|
+
const patch = byId.get(node.id);
|
|
67
|
+
byId.delete(node.id);
|
|
68
|
+
if (patch?.type === REMOVE_MARKER)
|
|
69
|
+
continue;
|
|
70
|
+
out.push(patch ? mergeNode(clone(node), patch) : clone(node));
|
|
71
|
+
}
|
|
72
|
+
// Anything the child introduced that the base did not have, in the order it declared it.
|
|
73
|
+
for (const node of over) {
|
|
74
|
+
if (byId.has(node.id) && node.type !== REMOVE_MARKER)
|
|
75
|
+
out.push(clone(node));
|
|
76
|
+
}
|
|
77
|
+
return out.length ? out : undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a doc's `extends` chain into a single self-contained doc. Docs without `extends`
|
|
81
|
+
* are returned as-is (cloned), so this is safe to call on everything.
|
|
82
|
+
*/
|
|
83
|
+
function resolveExtends(doc, opts) {
|
|
84
|
+
const maxDepth = opts.maxDepth ?? 8;
|
|
85
|
+
const chain = [];
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
let cursor = doc;
|
|
88
|
+
while (cursor) {
|
|
89
|
+
chain.unshift(cursor);
|
|
90
|
+
const ref = cursor.extends;
|
|
91
|
+
if (!ref)
|
|
92
|
+
break;
|
|
93
|
+
if (seen.has(ref))
|
|
94
|
+
throw new Error(`scene "${doc.id}": extends cycle at "${ref}"`);
|
|
95
|
+
seen.add(ref);
|
|
96
|
+
if (chain.length > maxDepth)
|
|
97
|
+
throw new Error(`scene "${doc.id}": extends chain deeper than ${maxDepth}`);
|
|
98
|
+
const parent = opts.load(ref);
|
|
99
|
+
if (!parent)
|
|
100
|
+
throw new Error(`scene "${doc.id}": cannot resolve extends "${ref}"`);
|
|
101
|
+
cursor = parent;
|
|
102
|
+
}
|
|
103
|
+
// Fold base → … → child.
|
|
104
|
+
let out = clone(chain[0]);
|
|
105
|
+
for (let i = 1; i < chain.length; i++) {
|
|
106
|
+
const over = chain[i];
|
|
107
|
+
out = {
|
|
108
|
+
...out,
|
|
109
|
+
id: over.id,
|
|
110
|
+
design: over.design ?? out.design,
|
|
111
|
+
root: mergeNode(out.root, over.root),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
delete out.extends;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function index(doc) {
|
|
118
|
+
const map = new Map();
|
|
119
|
+
const walk = (node, parentId, i) => {
|
|
120
|
+
map.set(node.id, { node, parentId, index: i });
|
|
121
|
+
(node.children ?? []).forEach((c, ci) => walk(c, node.id, ci));
|
|
122
|
+
};
|
|
123
|
+
walk(doc.root, null, 0);
|
|
124
|
+
return map;
|
|
125
|
+
}
|
|
126
|
+
const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
127
|
+
/**
|
|
128
|
+
* The patches that turn `from` into `to`. Used to switch game modes without rebuilding: a
|
|
129
|
+
* node both docs share and agree on is never touched, so the reel system, its symbols and
|
|
130
|
+
* any running animation survive the transition.
|
|
131
|
+
*
|
|
132
|
+
* Order matters — removals first (ids can be reused), then additions parent-before-child,
|
|
133
|
+
* then property updates.
|
|
134
|
+
*/
|
|
135
|
+
function diffDocs(from, to) {
|
|
136
|
+
const a = index(from);
|
|
137
|
+
const b = index(to);
|
|
138
|
+
const patches = [];
|
|
139
|
+
// Removed: only the topmost node of a removed subtree — removing it takes the rest.
|
|
140
|
+
for (const [id, loc] of a) {
|
|
141
|
+
if (b.has(id))
|
|
142
|
+
continue;
|
|
143
|
+
const parentGone = loc.parentId !== null && !b.has(loc.parentId);
|
|
144
|
+
if (!parentGone)
|
|
145
|
+
patches.push({ op: 'remove-node', id });
|
|
146
|
+
}
|
|
147
|
+
// Added, parents before children so the parent exists when the child lands.
|
|
148
|
+
const addedRoots = [];
|
|
149
|
+
for (const [id, loc] of b) {
|
|
150
|
+
if (a.has(id))
|
|
151
|
+
continue;
|
|
152
|
+
if (loc.parentId !== null && !a.has(loc.parentId) && !b.get(loc.parentId))
|
|
153
|
+
continue;
|
|
154
|
+
// Skip nodes whose parent is itself new — they ride along inside the parent's subtree.
|
|
155
|
+
if (loc.parentId !== null && !a.has(loc.parentId))
|
|
156
|
+
continue;
|
|
157
|
+
addedRoots.push({ id, loc });
|
|
158
|
+
}
|
|
159
|
+
for (const { loc } of addedRoots) {
|
|
160
|
+
patches.push({ op: 'add-node', parent: loc.parentId, node: clone(loc.node), index: loc.index });
|
|
161
|
+
}
|
|
162
|
+
// Changed, for nodes present in both.
|
|
163
|
+
for (const [id, next] of b) {
|
|
164
|
+
const prev = a.get(id);
|
|
165
|
+
if (!prev)
|
|
166
|
+
continue;
|
|
167
|
+
if (!same(prev.node.props, next.node.props)) {
|
|
168
|
+
// Keys the new doc dropped must be cleared, or they would linger from the old mode.
|
|
169
|
+
const props = {};
|
|
170
|
+
for (const key of Object.keys(prev.node.props ?? {}))
|
|
171
|
+
props[key] = undefined;
|
|
172
|
+
Object.assign(props, next.node.props ?? {});
|
|
173
|
+
patches.push({ op: 'set-props', id, props });
|
|
174
|
+
}
|
|
175
|
+
if (!same(prev.node.layout, next.node.layout)) {
|
|
176
|
+
patches.push({ op: 'set-layout', id, layout: (next.node.layout ?? null) });
|
|
177
|
+
}
|
|
178
|
+
if (!same(prev.node.name, next.node.name)) {
|
|
179
|
+
patches.push({ op: 'set-name', id, name: next.node.name });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return patches;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Field schemas — the authoring contract for a node type's props.
|
|
186
|
+
//
|
|
187
|
+
// Before this existed the inspector inferred a control from the CURRENT value, which meant
|
|
188
|
+
// a prop that wasn't set yet had no row and no way to be added: a freshly added sprite had
|
|
189
|
+
// `props:{}` and therefore no way to choose a texture at all. It also had no notion of an
|
|
190
|
+
// enum, an asset or a node reference, and it round-tripped booleans through text.
|
|
191
|
+
//
|
|
192
|
+
// A schema is declared once by the contribution that owns the kind, and is used three ways:
|
|
193
|
+
// the inspector renders from it, validation can check against it, and the agent reads the
|
|
194
|
+
// same text as documentation (`doc`) — the plugin's docs and the agent's prompt stay one
|
|
195
|
+
// thing, per docs/slot-ide.md §6.2.
|
|
196
|
+
/** Shared field definitions, so every kind describes `alpha` the same way. */
|
|
197
|
+
const ALPHA_FIELD = {
|
|
198
|
+
kind: 'number',
|
|
199
|
+
doc: 'Opacity, 0 = invisible, 1 = opaque.',
|
|
200
|
+
default: 1,
|
|
201
|
+
min: 0,
|
|
202
|
+
max: 1,
|
|
203
|
+
step: 0.05,
|
|
204
|
+
order: 90,
|
|
205
|
+
};
|
|
206
|
+
/** Fractions of a texture (0..1), used for both `region` (crop) and `inner` (frame hollow). */
|
|
207
|
+
const fracBoxFields = (doc) => ({
|
|
208
|
+
kind: 'object',
|
|
209
|
+
doc,
|
|
210
|
+
order: 60,
|
|
211
|
+
fields: {
|
|
212
|
+
left: { kind: 'number', default: 0, min: 0, max: 1, step: 0.01 },
|
|
213
|
+
top: { kind: 'number', default: 0, min: 0, max: 1, step: 0.01 },
|
|
214
|
+
width: { kind: 'number', default: 1, min: 0, max: 1, step: 0.01 },
|
|
215
|
+
height: { kind: 'number', default: 1, min: 0, max: 1, step: 0.01 },
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
function schemaFieldRows(schema, props) {
|
|
219
|
+
const rows = [];
|
|
220
|
+
if (!schema)
|
|
221
|
+
return rows;
|
|
222
|
+
const visit = (fields, prefix, container) => {
|
|
223
|
+
const entries = Object.entries(fields).sort((a, b) => (a[1].order ?? 1000) - (b[1].order ?? 1000) || a[0].localeCompare(b[0]));
|
|
224
|
+
for (const [key, field] of entries) {
|
|
225
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
226
|
+
const parent = container;
|
|
227
|
+
const value = parent && typeof parent === 'object' ? parent[key] : undefined;
|
|
228
|
+
if (field.kind === 'object' && field.fields) {
|
|
229
|
+
visit(field.fields, path, value);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
rows.push({ path, label: field.label ?? path, schema: field, value, unset: value === undefined });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
visit(schema, '', props);
|
|
237
|
+
return rows;
|
|
238
|
+
}
|
|
239
|
+
/** Props present on the node that the schema doesn't describe (plugin extras, hand-written). */
|
|
240
|
+
function extraPropKeys(schema, props) {
|
|
241
|
+
const known = new Set(Object.keys(schema ?? {}));
|
|
242
|
+
return Object.keys(props).filter((k) => !known.has(k));
|
|
243
|
+
}
|
|
244
|
+
|
|
3
245
|
// Pure layout core for scene-IR.
|
|
4
246
|
//
|
|
5
247
|
// `resolveLayoutRule` turns one LayoutRule into a Placement given a context of already
|
|
@@ -10,11 +252,10 @@ function orientationOf(width, height, portraitFactor = 1) {
|
|
|
10
252
|
return width < height * portraitFactor ? 'portrait' : 'landscape';
|
|
11
253
|
}
|
|
12
254
|
/** Base node + orientation override + active state override (state wins), for one frame. */
|
|
13
|
-
function effectiveNode(node, orientation, state
|
|
14
|
-
// Layer order: base → orientation →
|
|
255
|
+
function effectiveNode(node, orientation, state) {
|
|
256
|
+
// Layer order: base → orientation → active state (later wins).
|
|
15
257
|
const layers = [
|
|
16
258
|
node.responsive?.[orientation],
|
|
17
|
-
mode ? node.modes?.[mode] : undefined,
|
|
18
259
|
state ? node.states?.[state] : undefined,
|
|
19
260
|
];
|
|
20
261
|
let layout = node.layout;
|
|
@@ -2953,12 +3194,51 @@ function emptyBoard(cfg) {
|
|
|
2953
3194
|
// Everything else (ropes, meters, particles, game HUDs) arrives as a ScenePlugin through
|
|
2954
3195
|
// the same contribution shape — built-ins get no special powers.
|
|
2955
3196
|
const num$1 = (v, fallback) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);
|
|
3197
|
+
/** Word wrapping: without it a caption runs straight off the card it belongs to. */
|
|
3198
|
+
function wrapStyle(p) {
|
|
3199
|
+
const width = p.wrapWidth;
|
|
3200
|
+
if (typeof width !== 'number' || width <= 0)
|
|
3201
|
+
return {};
|
|
3202
|
+
return {
|
|
3203
|
+
wordWrap: true,
|
|
3204
|
+
wordWrapWidth: width,
|
|
3205
|
+
breakWords: p.breakWords === true,
|
|
3206
|
+
...(typeof p.lineHeight === 'number' ? { lineHeight: p.lineHeight } : {}),
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
/** Pixi's text drop shadow, from the doc's `dropShadow` object (absent → no shadow). */
|
|
3210
|
+
function dropShadowStyle(spec) {
|
|
3211
|
+
if (!spec || typeof spec !== 'object')
|
|
3212
|
+
return {};
|
|
3213
|
+
const d = spec;
|
|
3214
|
+
return {
|
|
3215
|
+
dropShadow: {
|
|
3216
|
+
color: typeof d.color === 'string' ? d.color : '#000000',
|
|
3217
|
+
blur: num$1(d.blur, 4),
|
|
3218
|
+
distance: num$1(d.distance, 3),
|
|
3219
|
+
angle: num$1(d.angle, Math.PI / 6),
|
|
3220
|
+
alpha: num$1(d.alpha, 1),
|
|
3221
|
+
},
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
2956
3224
|
const str = (v, fallback) => (typeof v === 'string' ? v : fallback);
|
|
2957
3225
|
// ── container / layer ───────────────────────────────────────────────────────────────
|
|
2958
3226
|
function containerContribution(kind) {
|
|
2959
3227
|
return {
|
|
2960
3228
|
kind,
|
|
2961
3229
|
agentDoc: 'Grouping node; order of children = z-order. Keep it un-laid-out (identity) unless you move a whole group. With props.space {width,height} it becomes a nested coordinate space: descendants lay out inside that fixed design space and its own rule places the space (cover → the diorama idiom). Natural size = space size, else 0×0.',
|
|
3230
|
+
schema: {
|
|
3231
|
+
space: {
|
|
3232
|
+
kind: 'object',
|
|
3233
|
+
doc: 'Turn this container into a nested coordinate space of that design size; descendants lay out inside it and this node\'s own rule places the space (cover → the diorama idiom).',
|
|
3234
|
+
order: 10,
|
|
3235
|
+
fields: {
|
|
3236
|
+
width: { kind: 'number', default: 1920, min: 1, step: 1 },
|
|
3237
|
+
height: { kind: 'number', default: 1080, min: 1, step: 1 },
|
|
3238
|
+
},
|
|
3239
|
+
},
|
|
3240
|
+
alpha: ALPHA_FIELD,
|
|
3241
|
+
},
|
|
2962
3242
|
defaults: { label: kind === 'layer' ? 'Layer' : 'Container', props: {} },
|
|
2963
3243
|
create(node) {
|
|
2964
3244
|
const view = new Container();
|
|
@@ -2981,6 +3261,22 @@ function containerContribution(kind) {
|
|
|
2981
3261
|
const rectContribution = {
|
|
2982
3262
|
kind: 'rect',
|
|
2983
3263
|
agentDoc: 'Solid rounded rectangle: plates behind text, dimmers, and mask targets (reference its id from another node\'s `mask`). Props: width, height, fill, alpha, radius, stroke {color,width}.',
|
|
3264
|
+
schema: {
|
|
3265
|
+
width: { kind: 'number', doc: 'Width in this node\'s coordinate space.', default: 100, min: 0, step: 1, order: 10 },
|
|
3266
|
+
height: { kind: 'number', doc: 'Height in this node\'s coordinate space.', default: 100, min: 0, step: 1, order: 11 },
|
|
3267
|
+
fill: { kind: 'color', doc: 'Fill colour.', default: '#ffffff', order: 20 },
|
|
3268
|
+
radius: { kind: 'number', doc: 'Corner radius in px (0 = square).', default: 0, min: 0, step: 1, order: 30 },
|
|
3269
|
+
stroke: {
|
|
3270
|
+
kind: 'object',
|
|
3271
|
+
doc: 'Optional outline.',
|
|
3272
|
+
order: 40,
|
|
3273
|
+
fields: {
|
|
3274
|
+
color: { kind: 'color', default: '#000000' },
|
|
3275
|
+
width: { kind: 'number', default: 1, min: 0, step: 1 },
|
|
3276
|
+
},
|
|
3277
|
+
},
|
|
3278
|
+
alpha: ALPHA_FIELD,
|
|
3279
|
+
},
|
|
2984
3280
|
defaults: {
|
|
2985
3281
|
label: 'Rectangle',
|
|
2986
3282
|
props: { width: 200, height: 120, fill: '#3355aa', radius: 12 },
|
|
@@ -3049,11 +3345,22 @@ function spriteInstance(node, ctx) {
|
|
|
3049
3345
|
applyProps: apply,
|
|
3050
3346
|
};
|
|
3051
3347
|
}
|
|
3348
|
+
const SPRITE_SCHEMA = {
|
|
3349
|
+
src: { kind: 'asset', accept: 'image', doc: 'Texture alias from the asset manifest.', order: 1 },
|
|
3350
|
+
tint: { kind: 'color', doc: 'Multiplies the texture colour; white = untinted.', default: '#ffffff', order: 20 },
|
|
3351
|
+
flipX: { kind: 'boolean', doc: 'Mirror horizontally.', default: false, order: 30 },
|
|
3352
|
+
region: fracBoxFields('Crop to a sub-rectangle of the texture, in fractions (0..1) — used to slice one atlas into left/right halves.'),
|
|
3353
|
+
alpha: ALPHA_FIELD,
|
|
3354
|
+
};
|
|
3052
3355
|
const spriteContribution = {
|
|
3053
3356
|
kind: 'sprite',
|
|
3054
3357
|
agentDoc: 'Static art. Props: src (texture alias), tint, alpha. Natural size = texture size; use layout to place/scale.',
|
|
3358
|
+
schema: SPRITE_SCHEMA,
|
|
3055
3359
|
defaults: {
|
|
3056
3360
|
label: 'Sprite',
|
|
3361
|
+
// Deliberately empty: a fresh sprite is invisible until it has a texture, and the fix
|
|
3362
|
+
// for that is that `src` is now a picker in the inspector the moment it is selected.
|
|
3363
|
+
// (A placeholder tint would only survive to discolour the art the user then picks.)
|
|
3057
3364
|
props: {},
|
|
3058
3365
|
layout: { mode: 'viewport-fraction', xFrac: 0.5, yFrac: 0.5, anchor: [0.5, 0.5] },
|
|
3059
3366
|
},
|
|
@@ -3062,12 +3369,46 @@ const spriteContribution = {
|
|
|
3062
3369
|
const reelFrameContribution = {
|
|
3063
3370
|
kind: 'reelFrame',
|
|
3064
3371
|
agentDoc: 'The reel frame art. Declare where its hollow is once in props.inner {left,top,width,height} (fractions of the texture); the grid then binds with layout {mode:"frame-fraction", frame:<this id>, use:"inner"} — never restate the numbers.',
|
|
3372
|
+
schema: {
|
|
3373
|
+
...SPRITE_SCHEMA,
|
|
3374
|
+
inner: fracBoxFields('Where the frame\'s hollow is, in fractions of the texture. Grids bind to it with {mode:"frame-fraction", use:"inner"} instead of restating numbers.'),
|
|
3375
|
+
},
|
|
3376
|
+
defaults: {
|
|
3377
|
+
label: 'Reel frame',
|
|
3378
|
+
props: { inner: { left: 0.06, top: 0.08, width: 0.88, height: 0.84 } },
|
|
3379
|
+
layout: { mode: 'viewport-fraction', xFrac: 0.5, yFrac: 0.5, widthFrac: 0.8, anchor: [0.5, 0.5] },
|
|
3380
|
+
},
|
|
3065
3381
|
create: spriteInstance,
|
|
3066
3382
|
};
|
|
3067
3383
|
// ── text ────────────────────────────────────────────────────────────────────────────
|
|
3068
3384
|
const textContribution = {
|
|
3069
3385
|
kind: 'text',
|
|
3070
3386
|
agentDoc: 'Vector text (labels, logos-as-type). Props: text, fontSize, fill, fontFamily, fontWeight, align, letterSpacing. For rolling win numbers prefer a bitmapNumber/prefab, not text.',
|
|
3387
|
+
schema: {
|
|
3388
|
+
text: { kind: 'text', doc: 'The string to draw.', default: '', order: 1 },
|
|
3389
|
+
fontSize: { kind: 'number', doc: 'Size in this node\'s coordinate space.', default: 32, min: 1, step: 1, order: 10 },
|
|
3390
|
+
fill: { kind: 'color', doc: 'Text colour.', default: '#ffffff', order: 11 },
|
|
3391
|
+
fontFamily: { kind: 'text', doc: 'CSS font stack. The font must be preloaded by the game.', default: 'Arial, sans-serif', order: 20 },
|
|
3392
|
+
fontWeight: { kind: 'enum', options: ['normal', 'bold'], default: 'normal', order: 21 },
|
|
3393
|
+
align: { kind: 'enum', options: ['left', 'center', 'right'], default: 'left', order: 22 },
|
|
3394
|
+
letterSpacing: { kind: 'number', default: 0, step: 1, order: 30 },
|
|
3395
|
+
wrapWidth: { kind: 'number', doc: 'Wrap the text at this width (design px). Unset = one line.', min: 0, step: 10, order: 32 },
|
|
3396
|
+
lineHeight: { kind: 'number', doc: 'Line spacing when wrapped.', min: 0, step: 1, order: 33 },
|
|
3397
|
+
breakWords: { kind: 'boolean', doc: 'Allow breaking inside a long word.', default: false, order: 34 },
|
|
3398
|
+
dropShadow: {
|
|
3399
|
+
kind: 'object',
|
|
3400
|
+
doc: 'Soft shadow behind the glyphs — what lifts a prompt off a busy background.',
|
|
3401
|
+
order: 40,
|
|
3402
|
+
fields: {
|
|
3403
|
+
color: { kind: 'color', default: '#000000' },
|
|
3404
|
+
blur: { kind: 'number', default: 4, min: 0, step: 1 },
|
|
3405
|
+
distance: { kind: 'number', default: 3, min: 0, step: 1 },
|
|
3406
|
+
angle: { kind: 'number', doc: 'Radians; default is down-right.', default: Math.PI / 6, step: 0.1 },
|
|
3407
|
+
alpha: { kind: 'number', default: 1, min: 0, max: 1, step: 0.05 },
|
|
3408
|
+
},
|
|
3409
|
+
},
|
|
3410
|
+
alpha: ALPHA_FIELD,
|
|
3411
|
+
},
|
|
3071
3412
|
defaults: {
|
|
3072
3413
|
label: 'Text',
|
|
3073
3414
|
props: { text: 'Text', fontSize: 48, fill: '#ffffff', fontWeight: 'bold' },
|
|
@@ -3084,6 +3425,8 @@ const textContribution = {
|
|
|
3084
3425
|
fontWeight: props.fontWeight ?? 'normal',
|
|
3085
3426
|
align: props.align ?? 'left',
|
|
3086
3427
|
letterSpacing: num$1(props.letterSpacing, 0),
|
|
3428
|
+
...dropShadowStyle(props.dropShadow),
|
|
3429
|
+
...wrapStyle(props),
|
|
3087
3430
|
},
|
|
3088
3431
|
});
|
|
3089
3432
|
return {
|
|
@@ -3095,6 +3438,15 @@ const textContribution = {
|
|
|
3095
3438
|
view.style.fill = p.fill ?? '#ffffff';
|
|
3096
3439
|
view.style.fontSize = num$1(p.fontSize, 32);
|
|
3097
3440
|
view.style.letterSpacing = num$1(p.letterSpacing, 0);
|
|
3441
|
+
const shadow = dropShadowStyle(p.dropShadow);
|
|
3442
|
+
view.style.dropShadow = shadow.dropShadow ?? false;
|
|
3443
|
+
const wrap = wrapStyle(p);
|
|
3444
|
+
view.style.wordWrap = wrap.wordWrap ?? false;
|
|
3445
|
+
if (wrap.wordWrapWidth !== undefined)
|
|
3446
|
+
view.style.wordWrapWidth = wrap.wordWrapWidth;
|
|
3447
|
+
if (wrap.lineHeight !== undefined)
|
|
3448
|
+
view.style.lineHeight = wrap.lineHeight;
|
|
3449
|
+
view.style.breakWords = wrap.breakWords ?? false;
|
|
3098
3450
|
view.alpha = num$1(p.alpha, 1);
|
|
3099
3451
|
},
|
|
3100
3452
|
};
|
|
@@ -3104,6 +3456,27 @@ const textContribution = {
|
|
|
3104
3456
|
const animatedSpriteContribution = {
|
|
3105
3457
|
kind: 'animatedSprite',
|
|
3106
3458
|
agentDoc: 'Frame animation from a grid spritesheet. Props: sheet {alias, cols, rows}, frame (static frame index, default 0), fps + playing:true for an ambient loop. Choreographed playback belongs to flow/animation clips, not here.',
|
|
3459
|
+
schema: {
|
|
3460
|
+
sheet: {
|
|
3461
|
+
kind: 'object',
|
|
3462
|
+
doc: 'A grid spritesheet: one image cut into cols x rows equal frames.',
|
|
3463
|
+
order: 1,
|
|
3464
|
+
fields: {
|
|
3465
|
+
alias: { kind: 'asset', accept: 'spritesheet', doc: 'Sheet texture alias.' },
|
|
3466
|
+
cols: { kind: 'number', default: 1, min: 1, step: 1 },
|
|
3467
|
+
rows: { kind: 'number', default: 1, min: 1, step: 1 },
|
|
3468
|
+
},
|
|
3469
|
+
},
|
|
3470
|
+
frame: { kind: 'number', doc: 'Static frame shown when not playing.', default: 0, min: 0, step: 1, order: 10 },
|
|
3471
|
+
fps: { kind: 'number', doc: 'Playback rate when playing.', default: 12, min: 0, step: 1, order: 11 },
|
|
3472
|
+
playing: { kind: 'boolean', doc: 'Loop continuously. Choreographed playback belongs in flow, not here.', default: false, order: 12 },
|
|
3473
|
+
alpha: ALPHA_FIELD,
|
|
3474
|
+
},
|
|
3475
|
+
defaults: {
|
|
3476
|
+
label: 'Animated sprite',
|
|
3477
|
+
props: { sheet: { cols: 1, rows: 1 }, frame: 0, fps: 12 },
|
|
3478
|
+
layout: { mode: 'viewport-fraction', xFrac: 0.5, yFrac: 0.5, anchor: [0.5, 0.5] },
|
|
3479
|
+
},
|
|
3107
3480
|
create(node, ctx) {
|
|
3108
3481
|
const view = new Container();
|
|
3109
3482
|
let animated = null;
|
|
@@ -3150,6 +3523,21 @@ const animatedSpriteContribution = {
|
|
|
3150
3523
|
// ── reelGrid ────────────────────────────────────────────────────────────────────────
|
|
3151
3524
|
const reelGridContribution = {
|
|
3152
3525
|
kind: 'reelGrid',
|
|
3526
|
+
schema: {
|
|
3527
|
+
preset: {
|
|
3528
|
+
kind: 'enum',
|
|
3529
|
+
doc: 'Named reel-system preset; `config` layers on top of it.',
|
|
3530
|
+
options: Object.keys(PRESETS),
|
|
3531
|
+
order: 1,
|
|
3532
|
+
},
|
|
3533
|
+
config: { kind: 'json', doc: 'Partial ReelSystemConfig override (columns, rows, spin motion, anticipation, tumble...).', order: 10 },
|
|
3534
|
+
board: { kind: 'json', doc: 'Symbol ids as string[][], column-major — the statically shown board.', order: 20 },
|
|
3535
|
+
},
|
|
3536
|
+
defaults: {
|
|
3537
|
+
label: 'Reel grid',
|
|
3538
|
+
props: { preset: Object.keys(PRESETS)[0] },
|
|
3539
|
+
layout: { mode: 'viewport-fraction', xFrac: 0.5, yFrac: 0.5, widthFrac: 0.6, anchor: [0.5, 0.5] },
|
|
3540
|
+
},
|
|
3153
3541
|
agentDoc: 'The reel window, delegating to createReelSystem. Props: preset (named ReelSystemConfig preset), config (DeepPartial override), board (string[][] of symbol ids, column-major). Bind into the frame hollow with {mode:"frame-fraction", frame:"<frameId>", use:"inner"}. Needs `resolveSymbol` passed to createSceneFromDoc.',
|
|
3154
3542
|
create(node, ctx) {
|
|
3155
3543
|
if (!ctx.resolveSymbol) {
|
|
@@ -3223,6 +3611,10 @@ const prefabContribution = {
|
|
|
3223
3611
|
const BUILTIN_FILTER_KINDS = [
|
|
3224
3612
|
{
|
|
3225
3613
|
kind: 'blur',
|
|
3614
|
+
schema: {
|
|
3615
|
+
strength: { kind: 'number', doc: 'Blur radius.', default: 8, min: 0, step: 1 },
|
|
3616
|
+
quality: { kind: 'number', doc: 'Passes — higher is smoother and costlier.', default: 4, min: 1, step: 1 },
|
|
3617
|
+
},
|
|
3226
3618
|
agentDoc: 'Gaussian blur: {kind:"blur", strength, quality}. Shadow/glow layers idiom.',
|
|
3227
3619
|
create(spec) {
|
|
3228
3620
|
return new BlurFilter({
|
|
@@ -3438,10 +3830,9 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3438
3830
|
lastWidth = width;
|
|
3439
3831
|
lastHeight = height;
|
|
3440
3832
|
const orientation = orientationOf(width, height, opts.portraitFactor ?? 1);
|
|
3441
|
-
|
|
3442
|
-
// Phase 1 — effective props (base + orientation + mode + state), applied before measuring.
|
|
3833
|
+
// Phase 1 — effective props (base + orientation + state), applied before measuring.
|
|
3443
3834
|
for (const runtime of [...runtimes.values()]) {
|
|
3444
|
-
const eff = effectiveNode(runtime.node, orientation, runtime.state
|
|
3835
|
+
const eff = effectiveNode(runtime.node, orientation, runtime.state);
|
|
3445
3836
|
runtime.visibleBase = eff.visible;
|
|
3446
3837
|
const space = eff.props.space;
|
|
3447
3838
|
runtime.spaceSize =
|
|
@@ -3545,7 +3936,7 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3545
3936
|
const parentOriginY = parentIsFrame ? 0 : (runtime.parent?.bounds?.y ?? 0);
|
|
3546
3937
|
const parentScaleX = parentIsFrame ? 1 : (runtime.parent?.worldScaleX ?? 1);
|
|
3547
3938
|
const parentScaleY = parentIsFrame ? 1 : (runtime.parent?.worldScaleY ?? 1);
|
|
3548
|
-
const eff = effectiveNode(runtime.node, orientation, runtime.state
|
|
3939
|
+
const eff = effectiveNode(runtime.node, orientation, runtime.state);
|
|
3549
3940
|
const natural = runtime.instance.measure();
|
|
3550
3941
|
const rule = eff.layout;
|
|
3551
3942
|
// `space:'viewport'` resolves against the live viewport even inside a nested
|
|
@@ -3712,29 +4103,48 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3712
4103
|
runtime.state = state;
|
|
3713
4104
|
relayout();
|
|
3714
4105
|
};
|
|
4106
|
+
/** Warn (once per key) and report the rejection back to the caller. */
|
|
4107
|
+
const reject = (key, msg) => {
|
|
4108
|
+
warnOnce(key, msg);
|
|
4109
|
+
return { ok: false, error: msg };
|
|
4110
|
+
};
|
|
3715
4111
|
const applyPatch = (patch) => {
|
|
4112
|
+
// Each case fills this with the patch that puts the doc back exactly as it was — the
|
|
4113
|
+
// editor's undo stack simply replays it. Captured BEFORE the mutation, necessarily.
|
|
4114
|
+
let inverse;
|
|
3716
4115
|
switch (patch.op) {
|
|
3717
4116
|
case 'set-props': {
|
|
3718
4117
|
const node = findNode(doc.root, patch.id);
|
|
3719
4118
|
if (!node)
|
|
3720
|
-
return
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
}
|
|
4119
|
+
return reject(`patch:${patch.id}`, `patch: unknown node "${patch.id}"`);
|
|
4120
|
+
// Undo restores each touched key to what it held — `undefined` for keys that were
|
|
4121
|
+
// absent, which the prop readers treat as "not set" and JSON.stringify drops.
|
|
4122
|
+
const before = node.props ?? {};
|
|
4123
|
+
const prev = {};
|
|
4124
|
+
for (const key of Object.keys(patch.props))
|
|
4125
|
+
prev[key] = before[key];
|
|
4126
|
+
inverse = { op: 'set-props', id: patch.id, props: prev };
|
|
4127
|
+
node.props = { ...(node.props ?? {}), ...patch.props };
|
|
3729
4128
|
break;
|
|
3730
4129
|
}
|
|
3731
4130
|
case 'set-layout': {
|
|
3732
4131
|
const node = findNode(doc.root, patch.id);
|
|
3733
4132
|
if (!node)
|
|
3734
|
-
return
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
4133
|
+
return reject(`patch:${patch.id}`, `patch: unknown node "${patch.id}"`);
|
|
4134
|
+
// Undo puts back the rule that occupied this exact slot — `null` when the slot did
|
|
4135
|
+
// not exist, so undoing an edit that CREATED an override removes it again.
|
|
4136
|
+
const prevRule = patch.orientation ? node.responsive?.[patch.orientation]?.layout : node.layout;
|
|
4137
|
+
inverse = {
|
|
4138
|
+
op: 'set-layout',
|
|
4139
|
+
id: patch.id,
|
|
4140
|
+
layout: prevRule ? JSON.parse(JSON.stringify(prevRule)) : null,
|
|
4141
|
+
orientation: patch.orientation,
|
|
4142
|
+
};
|
|
4143
|
+
if (patch.layout === null) {
|
|
4144
|
+
if (patch.orientation)
|
|
4145
|
+
delete node.responsive?.[patch.orientation]?.layout;
|
|
4146
|
+
else
|
|
4147
|
+
delete node.layout;
|
|
3738
4148
|
}
|
|
3739
4149
|
else if (patch.orientation) {
|
|
3740
4150
|
node.responsive = { ...(node.responsive ?? {}) };
|
|
@@ -3748,31 +4158,57 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3748
4158
|
}
|
|
3749
4159
|
break;
|
|
3750
4160
|
}
|
|
3751
|
-
case 'set-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
4161
|
+
case 'set-name': {
|
|
4162
|
+
const node = findNode(doc.root, patch.id);
|
|
4163
|
+
if (!node)
|
|
4164
|
+
return reject(`patch:${patch.id}`, `patch: unknown node "${patch.id}"`);
|
|
4165
|
+
inverse = { op: 'set-name', id: patch.id, name: node.name };
|
|
4166
|
+
if (patch.name === undefined || patch.name === '')
|
|
4167
|
+
delete node.name;
|
|
4168
|
+
else
|
|
4169
|
+
node.name = patch.name;
|
|
4170
|
+
break;
|
|
4171
|
+
}
|
|
4172
|
+
case 'set-state': {
|
|
4173
|
+
// Runtime-only, so the previous value lives on the runtime, not in the doc.
|
|
4174
|
+
const prevState = runtimes.get(patch.id)?.state ?? null;
|
|
4175
|
+
setState(patch.id, patch.state);
|
|
4176
|
+
return { ok: true, inverse: { op: 'set-state', id: patch.id, state: prevState } };
|
|
4177
|
+
}
|
|
4178
|
+
case 'set-var': {
|
|
4179
|
+
const prevVar = vars[patch.name];
|
|
4180
|
+
setVar(patch.name, patch.value);
|
|
4181
|
+
return { ok: true, inverse: { op: 'set-var', name: patch.name, value: prevVar } };
|
|
4182
|
+
}
|
|
3755
4183
|
case 'add-node': {
|
|
3756
4184
|
const parentRuntime = runtimes.get(patch.parent);
|
|
3757
4185
|
if (!parentRuntime)
|
|
3758
|
-
return
|
|
4186
|
+
return reject(`add:${patch.parent}`, `add-node: unknown parent "${patch.parent}"`);
|
|
3759
4187
|
const clash = collectIds(patch.node).find((id) => runtimes.has(id));
|
|
3760
4188
|
if (clash)
|
|
3761
|
-
return
|
|
4189
|
+
return reject(`add-clash:${clash}`, `add-node: id "${clash}" already exists`);
|
|
3762
4190
|
if (!registry.nodeType(patch.node.type)) {
|
|
3763
|
-
return
|
|
4191
|
+
return reject(`add-kind:${patch.node.type}`, `add-node: unknown type "${patch.node.type}"`);
|
|
3764
4192
|
}
|
|
3765
4193
|
(parentRuntime.node.children ??= []).splice(patch.index ?? parentRuntime.node.children.length, 0, patch.node);
|
|
3766
4194
|
buildInto(patch.node, parentRuntime, patch.index ?? Number.MAX_SAFE_INTEGER);
|
|
3767
4195
|
wireMasks();
|
|
4196
|
+
inverse = { op: 'remove-node', id: patch.node.id };
|
|
3768
4197
|
break;
|
|
3769
4198
|
}
|
|
3770
4199
|
case 'remove-node': {
|
|
3771
4200
|
if (patch.id === doc.root.id)
|
|
3772
|
-
return
|
|
4201
|
+
return reject('rm-root', 'remove-node: cannot remove the root');
|
|
3773
4202
|
const loc = locate(patch.id);
|
|
3774
4203
|
if (!loc)
|
|
3775
|
-
return
|
|
4204
|
+
return reject(`rm:${patch.id}`, `remove-node: unknown node "${patch.id}"`);
|
|
4205
|
+
// The whole subtree goes with it, so undo needs a deep copy taken before the splice.
|
|
4206
|
+
inverse = {
|
|
4207
|
+
op: 'add-node',
|
|
4208
|
+
parent: loc.parent.id,
|
|
4209
|
+
node: JSON.parse(JSON.stringify(loc.parent.children[loc.index])),
|
|
4210
|
+
index: loc.index,
|
|
4211
|
+
};
|
|
3776
4212
|
destroyRuntimeTree(patch.id);
|
|
3777
4213
|
loc.parent.children.splice(loc.index, 1);
|
|
3778
4214
|
wireMasks();
|
|
@@ -3780,17 +4216,18 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3780
4216
|
}
|
|
3781
4217
|
case 'move-node': {
|
|
3782
4218
|
if (patch.id === doc.root.id)
|
|
3783
|
-
return
|
|
4219
|
+
return reject('mv-root', 'move-node: cannot move the root');
|
|
3784
4220
|
const loc = locate(patch.id);
|
|
3785
4221
|
const source = findNode(doc.root, patch.id);
|
|
3786
4222
|
const parentRuntime = runtimes.get(patch.parent);
|
|
3787
4223
|
if (!loc || !source)
|
|
3788
|
-
return
|
|
4224
|
+
return reject(`mv:${patch.id}`, `move-node: unknown node "${patch.id}"`);
|
|
3789
4225
|
if (!parentRuntime)
|
|
3790
|
-
return
|
|
4226
|
+
return reject(`mv-p:${patch.parent}`, `move-node: unknown parent "${patch.parent}"`);
|
|
3791
4227
|
if (collectIds(source).includes(patch.parent)) {
|
|
3792
|
-
return
|
|
4228
|
+
return reject(`mv-cycle:${patch.id}`, `move-node: cannot move "${patch.id}" into itself/its descendant "${patch.parent}"`);
|
|
3793
4229
|
}
|
|
4230
|
+
inverse = { op: 'move-node', id: patch.id, parent: loc.parent.id, index: loc.index };
|
|
3794
4231
|
// `index` is the FINAL position in the destination's children (post-removal).
|
|
3795
4232
|
const [node] = loc.parent.children.splice(loc.index, 1);
|
|
3796
4233
|
destroyRuntimeTree(patch.id); // rebuild the moved subtree under the new parent
|
|
@@ -3803,11 +4240,11 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3803
4240
|
}
|
|
3804
4241
|
case 'duplicate-node': {
|
|
3805
4242
|
if (patch.id === doc.root.id)
|
|
3806
|
-
return
|
|
4243
|
+
return reject('dup-root', 'duplicate-node: cannot duplicate the root');
|
|
3807
4244
|
const loc = locate(patch.id);
|
|
3808
4245
|
const source = findNode(doc.root, patch.id);
|
|
3809
4246
|
if (!loc || !source)
|
|
3810
|
-
return
|
|
4247
|
+
return reject(`dup:${patch.id}`, `duplicate-node: unknown node "${patch.id}"`);
|
|
3811
4248
|
let suffix = '-copy';
|
|
3812
4249
|
while (collectIds(source).some((id) => runtimes.has(`${id}${suffix}`)))
|
|
3813
4250
|
suffix += '2';
|
|
@@ -3816,10 +4253,12 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3816
4253
|
loc.parent.children.splice(loc.index + 1, 0, clone);
|
|
3817
4254
|
buildInto(clone, parentRuntime, loc.index + 1);
|
|
3818
4255
|
wireMasks();
|
|
4256
|
+
inverse = { op: 'remove-node', id: clone.id };
|
|
3819
4257
|
break;
|
|
3820
4258
|
}
|
|
3821
4259
|
}
|
|
3822
4260
|
relayout();
|
|
4261
|
+
return { ok: true, inverse };
|
|
3823
4262
|
};
|
|
3824
4263
|
return {
|
|
3825
4264
|
view: rootView,
|
|
@@ -3839,6 +4278,20 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3839
4278
|
patch: applyPatch,
|
|
3840
4279
|
doc: () => doc,
|
|
3841
4280
|
palette: () => registry.palette(),
|
|
4281
|
+
schemas() {
|
|
4282
|
+
const out = {};
|
|
4283
|
+
for (const kind of registry.kinds()) {
|
|
4284
|
+
const schema = registry.nodeType(kind)?.schema;
|
|
4285
|
+
if (schema)
|
|
4286
|
+
out[kind] = schema;
|
|
4287
|
+
}
|
|
4288
|
+
for (const name of registry.prefabNames()) {
|
|
4289
|
+
const schema = registry.prefab(name)?.schema;
|
|
4290
|
+
if (schema)
|
|
4291
|
+
out[`prefab:${name}`] = schema;
|
|
4292
|
+
}
|
|
4293
|
+
return out;
|
|
4294
|
+
},
|
|
3842
4295
|
resetProps() {
|
|
3843
4296
|
for (const runtime of runtimes.values())
|
|
3844
4297
|
runtime.appliedProps = 'stale';
|
|
@@ -4153,7 +4606,10 @@ function createTransformGizmo(opts) {
|
|
|
4153
4606
|
layer.addChildAt(body, 1);
|
|
4154
4607
|
let nodeId = null;
|
|
4155
4608
|
let drag = null;
|
|
4156
|
-
|
|
4609
|
+
/** Press origin, to tell a click apart from a drag on pointerup. */
|
|
4610
|
+
let downAt = null;
|
|
4611
|
+
let moved = false;
|
|
4612
|
+
const DRAG_SLOP = 4; // px before a press counts as a drag
|
|
4157
4613
|
const findDocNode = (id, node = handle.doc().root) => {
|
|
4158
4614
|
if (node.id === id)
|
|
4159
4615
|
return node;
|
|
@@ -4165,18 +4621,13 @@ function createTransformGizmo(opts) {
|
|
|
4165
4621
|
return undefined;
|
|
4166
4622
|
};
|
|
4167
4623
|
/**
|
|
4168
|
-
* Effective layout rule + which override an edit should target
|
|
4169
|
-
*
|
|
4170
|
-
* otherwise the current orientation override, else base.
|
|
4624
|
+
* Effective layout rule + which override an edit should target: the current orientation's
|
|
4625
|
+
* override when one exists, else the base rule.
|
|
4171
4626
|
*/
|
|
4172
4627
|
const effectiveRule = (id) => {
|
|
4173
4628
|
const node = findDocNode(id);
|
|
4174
4629
|
if (!node)
|
|
4175
4630
|
return { target: {} };
|
|
4176
|
-
const mode = getMode();
|
|
4177
|
-
if (mode && mode !== 'base') {
|
|
4178
|
-
return { rule: node.modes?.[mode]?.layout ?? node.layout, target: { mode } };
|
|
4179
|
-
}
|
|
4180
4631
|
const o = getOrientation();
|
|
4181
4632
|
const override = node.responsive?.[o]?.layout;
|
|
4182
4633
|
return override ? { rule: override, target: { orientation: o } } : { rule: node.layout, target: {} };
|
|
@@ -4197,6 +4648,9 @@ function createTransformGizmo(opts) {
|
|
|
4197
4648
|
if (!box || !rule)
|
|
4198
4649
|
return;
|
|
4199
4650
|
const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
4651
|
+
downAt = { x: e.global.x, y: e.global.y, handle: h, alt: e.altKey };
|
|
4652
|
+
moved = false;
|
|
4653
|
+
opts.onGestureStart?.();
|
|
4200
4654
|
drag = {
|
|
4201
4655
|
handle: h,
|
|
4202
4656
|
startBox: box,
|
|
@@ -4210,6 +4664,11 @@ function createTransformGizmo(opts) {
|
|
|
4210
4664
|
const onMove = (e) => {
|
|
4211
4665
|
if (!drag || !nodeId)
|
|
4212
4666
|
return;
|
|
4667
|
+
if (!moved && downAt && (Math.abs(e.global.x - downAt.x) > DRAG_SLOP || Math.abs(e.global.y - downAt.y) > DRAG_SLOP)) {
|
|
4668
|
+
moved = true;
|
|
4669
|
+
}
|
|
4670
|
+
if (!moved)
|
|
4671
|
+
return; // a press that has not travelled yet is still a potential click
|
|
4213
4672
|
let rule;
|
|
4214
4673
|
if (drag.handle === 'body') {
|
|
4215
4674
|
const s = dpp(nodeId);
|
|
@@ -4225,10 +4684,20 @@ function createTransformGizmo(opts) {
|
|
|
4225
4684
|
const { factorX, factorY } = resizeFactors(drag.startBox, drag.handle, { x: e.global.x, y: e.global.y });
|
|
4226
4685
|
rule = resizeRule(drag.baseRule, factorX, factorY);
|
|
4227
4686
|
}
|
|
4228
|
-
applyPatch({ op: 'set-layout', id: nodeId, layout: rule, orientation: drag.target.orientation
|
|
4687
|
+
applyPatch({ op: 'set-layout', id: nodeId, layout: rule, orientation: drag.target.orientation });
|
|
4229
4688
|
};
|
|
4230
4689
|
const onUp = () => {
|
|
4690
|
+
if (drag)
|
|
4691
|
+
opts.onGestureEnd?.();
|
|
4231
4692
|
drag = null;
|
|
4693
|
+
// Body press that never moved = a selection click, not a move.
|
|
4694
|
+
if (downAt && !moved && downAt.handle === 'body') {
|
|
4695
|
+
const { x, y, alt } = downAt;
|
|
4696
|
+
downAt = null;
|
|
4697
|
+
opts.onBodyClick?.(x, y, alt);
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
downAt = null;
|
|
4232
4701
|
};
|
|
4233
4702
|
app.stage.on('pointermove', onMove);
|
|
4234
4703
|
app.stage.on('pointerup', onUp);
|
|
@@ -4271,6 +4740,24 @@ function createTransformGizmo(opts) {
|
|
|
4271
4740
|
redraw();
|
|
4272
4741
|
},
|
|
4273
4742
|
refresh: redraw,
|
|
4743
|
+
nudge(dxScreen, dyScreen) {
|
|
4744
|
+
if (!nodeId)
|
|
4745
|
+
return;
|
|
4746
|
+
const { rule, target } = effectiveRule(nodeId);
|
|
4747
|
+
if (!rule)
|
|
4748
|
+
return;
|
|
4749
|
+
// Bracketed as a gesture so it lands on the undo stack as exactly one step.
|
|
4750
|
+
opts.onGestureStart?.();
|
|
4751
|
+
const s = dpp(nodeId);
|
|
4752
|
+
applyPatch({
|
|
4753
|
+
op: 'set-layout',
|
|
4754
|
+
id: nodeId,
|
|
4755
|
+
layout: nudgeRule(rule, dxScreen * s, dyScreen * s),
|
|
4756
|
+
orientation: target.orientation,
|
|
4757
|
+
});
|
|
4758
|
+
opts.onGestureEnd?.();
|
|
4759
|
+
redraw();
|
|
4760
|
+
},
|
|
4274
4761
|
drag(h, from, to) {
|
|
4275
4762
|
onDown(h, fakeEvent(from.x, from.y));
|
|
4276
4763
|
onMove(fakeEvent(to.x, to.y));
|
|
@@ -4358,6 +4845,12 @@ function formatValue(value, format) {
|
|
|
4358
4845
|
}
|
|
4359
4846
|
const tweenStep = {
|
|
4360
4847
|
kind: 'tween',
|
|
4848
|
+
schema: {
|
|
4849
|
+
node: { kind: 'nodeRef', doc: 'Scene node to animate.', order: 1 },
|
|
4850
|
+
to: { kind: 'json', doc: 'Target numeric view props, e.g. {"alpha":1,"y":40}.', order: 10 },
|
|
4851
|
+
ms: { kind: 'number', doc: 'Duration; scaled by turbo, zero under skip.', default: 300, min: 0, step: 10, order: 20 },
|
|
4852
|
+
ease: { kind: 'text', doc: 'Easing name from the Easing table.', default: 'linear', order: 30 },
|
|
4853
|
+
},
|
|
4361
4854
|
agentDoc: 'Animate numeric view properties of a scene node (alpha, x, y, rotation; scale animates both axes). Transient — the doc is untouched, and any relayout (setVar/setState/resize) re-asserts layout-owned props (x/y/scale/rotation) from the doc. Persist a move via a set-layout patch, not a tween. Turbo divides ms; skip jumps to the end values.',
|
|
4362
4855
|
async run(raw, rt) {
|
|
4363
4856
|
const step = raw;
|
|
@@ -4396,6 +4889,10 @@ const tweenStep = {
|
|
|
4396
4889
|
};
|
|
4397
4890
|
const soundStep = {
|
|
4398
4891
|
kind: 'sound',
|
|
4892
|
+
schema: {
|
|
4893
|
+
cue: { kind: 'enum', doc: 'A cue declared in the doc\'s `cues`.', options: [], order: 1 },
|
|
4894
|
+
action: { kind: 'enum', options: ['play', 'stop'], default: 'play', order: 10 },
|
|
4895
|
+
},
|
|
4399
4896
|
agentDoc: 'Play/stop a cue from flow.cues (rotation/jitter handled by the runner). Never reference audio files directly.',
|
|
4400
4897
|
run(raw, rt) {
|
|
4401
4898
|
const step = raw;
|
|
@@ -4404,6 +4901,10 @@ const soundStep = {
|
|
|
4404
4901
|
};
|
|
4405
4902
|
const setStateStep = {
|
|
4406
4903
|
kind: 'setState',
|
|
4904
|
+
schema: {
|
|
4905
|
+
node: { kind: 'nodeRef', doc: 'Node whose named variant changes.', order: 1 },
|
|
4906
|
+
state: { kind: 'text', doc: 'A state declared on that node; empty clears it.', order: 10 },
|
|
4907
|
+
},
|
|
4407
4908
|
agentDoc: "Switch a node's named state (frame anticipation/bonus look). Same operation the inspector and agent use.",
|
|
4408
4909
|
run(raw, rt) {
|
|
4409
4910
|
const step = raw;
|
|
@@ -4412,6 +4913,10 @@ const setStateStep = {
|
|
|
4412
4913
|
};
|
|
4413
4914
|
const setVarStep = {
|
|
4414
4915
|
kind: 'setVar',
|
|
4916
|
+
schema: {
|
|
4917
|
+
name: { kind: 'text', doc: 'Runtime var name (drives visibleWhen).', order: 1 },
|
|
4918
|
+
value: { kind: 'text', doc: 'Literal, or a $ctx reference.', order: 10 },
|
|
4919
|
+
},
|
|
4415
4920
|
agentDoc: "Set a runtime var driving visibleWhen (e.g. mode). Use for mode transitions ('setVar mode free_spins').",
|
|
4416
4921
|
run(raw, rt) {
|
|
4417
4922
|
const step = raw;
|
|
@@ -4420,6 +4925,10 @@ const setVarStep = {
|
|
|
4420
4925
|
};
|
|
4421
4926
|
const setPropsStep = {
|
|
4422
4927
|
kind: 'setProps',
|
|
4928
|
+
schema: {
|
|
4929
|
+
node: { kind: 'nodeRef', order: 1 },
|
|
4930
|
+
props: { kind: 'json', doc: 'Props to merge onto the node.', order: 10 },
|
|
4931
|
+
},
|
|
4423
4932
|
agentDoc: 'Transient prop write on the live instance (badge values, board swaps during presentation). The scene doc is NOT modified.',
|
|
4424
4933
|
run(raw, rt) {
|
|
4425
4934
|
const step = raw;
|
|
@@ -4448,6 +4957,14 @@ function findDocNode(rt, id) {
|
|
|
4448
4957
|
}
|
|
4449
4958
|
const countUpStep = {
|
|
4450
4959
|
kind: 'countUp',
|
|
4960
|
+
schema: {
|
|
4961
|
+
node: { kind: 'nodeRef', doc: 'Node holding the number (a text or a prefab).', order: 1 },
|
|
4962
|
+
prop: { kind: 'text', default: 'value', order: 5 },
|
|
4963
|
+
from: { kind: 'text', doc: 'Start value; literal or $ctx. Defaults to the current one.', order: 10 },
|
|
4964
|
+
to: { kind: 'text', doc: 'End value; literal or $ctx (e.g. $win).', order: 11 },
|
|
4965
|
+
ms: { kind: 'number', default: 1000, min: 0, step: 50, order: 20 },
|
|
4966
|
+
format: { kind: 'enum', options: ['int', 'space'], order: 30 },
|
|
4967
|
+
},
|
|
4451
4968
|
agentDoc: "Animated number roll on an instance prop (default 'value' — badge prefabs). `to` may be '$win' (ctx ref). Turbo shortens, skip jumps to the final value.",
|
|
4452
4969
|
async run(raw, rt) {
|
|
4453
4970
|
const step = raw;
|
|
@@ -4475,6 +4992,10 @@ const countUpStep = {
|
|
|
4475
4992
|
};
|
|
4476
4993
|
const waitStep = {
|
|
4477
4994
|
kind: 'wait',
|
|
4995
|
+
schema: {
|
|
4996
|
+
ms: { kind: 'number', doc: 'Pause; scaled by turbo, released by skip.', default: 300, min: 0, step: 10, order: 1 },
|
|
4997
|
+
until: { kind: 'enum', doc: "'tap' waits for the player instead of a duration.", options: ['tap'], order: 10 },
|
|
4998
|
+
},
|
|
4478
4999
|
agentDoc: "Pause: {ms} (turbo-scaled) or {until:'tap'} (released by tap or skip). Prefer explicit waits over baking delays into tweens.",
|
|
4479
5000
|
async run(raw, rt) {
|
|
4480
5001
|
const step = raw;
|
|
@@ -4485,6 +5006,9 @@ const waitStep = {
|
|
|
4485
5006
|
};
|
|
4486
5007
|
const ifStep = {
|
|
4487
5008
|
kind: 'if',
|
|
5009
|
+
schema: {
|
|
5010
|
+
when: { kind: 'text', doc: 'Guard: `<ctxField> <op> <literal>`, e.g. `win >= 20`.', order: 1 },
|
|
5011
|
+
},
|
|
4488
5012
|
agentDoc: "Branch on the fire() ctx: {when:'win >= 100', then:[…], else:[…]}. Bare name = truthy check.",
|
|
4489
5013
|
async run(raw, rt) {
|
|
4490
5014
|
const step = raw;
|
|
@@ -4493,6 +5017,7 @@ const ifStep = {
|
|
|
4493
5017
|
};
|
|
4494
5018
|
const parallelStep = {
|
|
4495
5019
|
kind: 'parallel',
|
|
5020
|
+
schema: {},
|
|
4496
5021
|
agentDoc: 'Run tracks concurrently and await them all: {steps: [[…], […]]}. Each track is an independent sequence.',
|
|
4497
5022
|
async run(raw, rt) {
|
|
4498
5023
|
const step = raw;
|
|
@@ -4501,6 +5026,7 @@ const parallelStep = {
|
|
|
4501
5026
|
};
|
|
4502
5027
|
const seqStep = {
|
|
4503
5028
|
kind: 'seq',
|
|
5029
|
+
schema: {},
|
|
4504
5030
|
agentDoc: 'Nested sequence (grouping inside parallel tracks).',
|
|
4505
5031
|
async run(raw, rt) {
|
|
4506
5032
|
const step = raw;
|
|
@@ -4509,6 +5035,12 @@ const seqStep = {
|
|
|
4509
5035
|
};
|
|
4510
5036
|
const forEachStep = {
|
|
4511
5037
|
kind: 'forEach',
|
|
5038
|
+
schema: {
|
|
5039
|
+
items: { kind: 'text', doc: 'A $ctx array to walk, e.g. $cascadeSteps.', order: 1 },
|
|
5040
|
+
as: { kind: 'text', doc: 'Name each element binds to inside ($item by default).', default: 'item', order: 10 },
|
|
5041
|
+
mode: { kind: 'enum', options: ['sequential', 'parallel'], default: 'sequential', order: 20 },
|
|
5042
|
+
staggerMs: { kind: 'number', doc: 'Delay between elements.', default: 0, min: 0, step: 10, order: 30 },
|
|
5043
|
+
},
|
|
4512
5044
|
agentDoc: "Fan out over a ctx collection: {items:'$jars', as:'jar', mode:'parallel', staggerMs:90, steps:[…]}. Nested steps read `$jar` / `$jarIndex`. THE primitive for staggered jar flights, per-cell transmutes, cascade histories — validated on 3 games.",
|
|
4513
5045
|
async run(raw, rt) {
|
|
4514
5046
|
const step = raw;
|
|
@@ -4536,6 +5068,10 @@ const forEachStep = {
|
|
|
4536
5068
|
};
|
|
4537
5069
|
const codeStep = {
|
|
4538
5070
|
kind: 'code',
|
|
5071
|
+
schema: {
|
|
5072
|
+
ref: { kind: 'text', doc: 'Key in the host-provided `code` map — the escape hatch.', order: 1 },
|
|
5073
|
+
args: { kind: 'json', order: 10 },
|
|
5074
|
+
},
|
|
4539
5075
|
agentDoc: 'Escape hatch: named choreography registered in createFlowRunner({code}). Use when the step vocabulary genuinely cannot express it — then consider a plugin step.',
|
|
4540
5076
|
async run(raw, rt) {
|
|
4541
5077
|
const step = raw;
|
|
@@ -4560,6 +5096,73 @@ const BUILTIN_FLOW_STEPS = [
|
|
|
4560
5096
|
codeStep,
|
|
4561
5097
|
];
|
|
4562
5098
|
|
|
5099
|
+
// Flow as a graph: stable step ids, and the layout that turns a flow event into nodes and
|
|
5100
|
+
// edges for the canvas editor.
|
|
5101
|
+
//
|
|
5102
|
+
// The IR is a TREE (a step list, with `seq`/`parallel`/`if`/`forEach` nesting more lists),
|
|
5103
|
+
// not a free DAG — control flow is strictly structural, there are no jumps. So the canvas
|
|
5104
|
+
// draws a nested flow diagram rather than a general node soup: consecutive steps chain
|
|
5105
|
+
// left to right, and a container is a box its children are laid out inside. That is the
|
|
5106
|
+
// honest picture of what actually runs; a flat DAG would have to invent joins the runtime
|
|
5107
|
+
// does not have.
|
|
5108
|
+
//
|
|
5109
|
+
// Layout is automatic by default so an existing flow.json opens as a readable graph with
|
|
5110
|
+
// no migration. A step only gets `ui: {x, y}` once someone drags it.
|
|
5111
|
+
/** Child lists a container step owns, by kind. `parallel` holds a list PER track. */
|
|
5112
|
+
function containerTracks(step) {
|
|
5113
|
+
const kind = step.do;
|
|
5114
|
+
if (kind === 'parallel') {
|
|
5115
|
+
const tracks = step.steps;
|
|
5116
|
+
return Array.isArray(tracks) && Array.isArray(tracks[0]) ? tracks : [];
|
|
5117
|
+
}
|
|
5118
|
+
if (kind === 'seq' || kind === 'forEach') {
|
|
5119
|
+
const list = step.steps;
|
|
5120
|
+
return Array.isArray(list) ? [list] : [];
|
|
5121
|
+
}
|
|
5122
|
+
if (kind === 'if') {
|
|
5123
|
+
const s = step;
|
|
5124
|
+
return [s.then ?? [], s.else ?? []];
|
|
5125
|
+
}
|
|
5126
|
+
return [];
|
|
5127
|
+
}
|
|
5128
|
+
/**
|
|
5129
|
+
* Give every step a stable id, in place-ish (returns the same doc object). Ids are only
|
|
5130
|
+
* assigned where missing, so re-running is a no-op and saved ids survive.
|
|
5131
|
+
*/
|
|
5132
|
+
function ensureStepIds(doc, prefix = 's') {
|
|
5133
|
+
const used = new Set();
|
|
5134
|
+
const collect = (list) => {
|
|
5135
|
+
for (const step of list) {
|
|
5136
|
+
if (typeof step.id === 'string')
|
|
5137
|
+
used.add(step.id);
|
|
5138
|
+
for (const track of containerTracks(step))
|
|
5139
|
+
collect(track);
|
|
5140
|
+
}
|
|
5141
|
+
};
|
|
5142
|
+
for (const list of Object.values(doc.on))
|
|
5143
|
+
collect(list);
|
|
5144
|
+
let n = 0;
|
|
5145
|
+
const fresh = () => {
|
|
5146
|
+
let id;
|
|
5147
|
+
do {
|
|
5148
|
+
id = `${prefix}${++n}`;
|
|
5149
|
+
} while (used.has(id));
|
|
5150
|
+
used.add(id);
|
|
5151
|
+
return id;
|
|
5152
|
+
};
|
|
5153
|
+
const assign = (list) => {
|
|
5154
|
+
for (const step of list) {
|
|
5155
|
+
if (typeof step.id !== 'string' || step.id === '')
|
|
5156
|
+
step.id = fresh();
|
|
5157
|
+
for (const track of containerTracks(step))
|
|
5158
|
+
assign(track);
|
|
5159
|
+
}
|
|
5160
|
+
};
|
|
5161
|
+
for (const list of Object.values(doc.on))
|
|
5162
|
+
assign(list);
|
|
5163
|
+
return doc;
|
|
5164
|
+
}
|
|
5165
|
+
|
|
4563
5166
|
// Flow-IR interpreter: createFlowRunner(flowDoc, { scene, … }).fire(event, ctx) executes
|
|
4564
5167
|
// the event's steps and returns a Trace.
|
|
4565
5168
|
//
|
|
@@ -4601,6 +5204,8 @@ function validateFlowDoc(doc, registry) {
|
|
|
4601
5204
|
function createFlowRunner(doc, opts) {
|
|
4602
5205
|
const log = opts.log ?? ((msg) => console.warn(`[flow] ${msg}`));
|
|
4603
5206
|
const registry = createFlowStepRegistry(opts.plugins ?? [], BUILTIN_FLOW_STEPS);
|
|
5207
|
+
// Hand-written docs omit ids; assign them once so traces and the canvas can address steps.
|
|
5208
|
+
ensureStepIds(doc);
|
|
4604
5209
|
const errors = validateFlowDoc(doc, registry);
|
|
4605
5210
|
if (errors.length > 0)
|
|
4606
5211
|
throw new Error(`flow doc "${doc.id}" is invalid:\n ${errors.join('\n ')}`);
|
|
@@ -4708,6 +5313,9 @@ function createFlowRunner(doc, opts) {
|
|
|
4708
5313
|
continue; // validated; defensive
|
|
4709
5314
|
trace({
|
|
4710
5315
|
do: step.do,
|
|
5316
|
+
// Which step this came from, so the canvas can highlight it and a scrub can
|
|
5317
|
+
// point at it — `do` alone cannot tell two identical tweens apart.
|
|
5318
|
+
stepId: typeof step.id === 'string' ? step.id : undefined,
|
|
4711
5319
|
node: typeof step.node === 'string' ? step.node : undefined,
|
|
4712
5320
|
});
|
|
4713
5321
|
// trace() may flip `halted` on THIS entry — the halting step still executes
|
|
@@ -4773,6 +5381,10 @@ function createFlowRunner(doc, opts) {
|
|
|
4773
5381
|
};
|
|
4774
5382
|
return {
|
|
4775
5383
|
fire,
|
|
5384
|
+
stepKinds: () => registry.kinds().map((kind) => {
|
|
5385
|
+
const c = registry.step(kind);
|
|
5386
|
+
return { kind, schema: c?.schema, agentDoc: c?.agentDoc };
|
|
5387
|
+
}),
|
|
4776
5388
|
replay(event, untilEntry) {
|
|
4777
5389
|
const ctx = lastCtx.get(event);
|
|
4778
5390
|
if (!ctx)
|
|
@@ -4814,9 +5426,16 @@ function createFlowRunner(doc, opts) {
|
|
|
4814
5426
|
// and the game ships no GameScene code for whatever the docs express.
|
|
4815
5427
|
/** Build the createSlotGame `scenes` list from a StageDoc — every entry a DocScene. */
|
|
4816
5428
|
function buildDocScenes(stage, shared) {
|
|
4817
|
-
|
|
5429
|
+
// Mode views are not host scenes — they belong to the scene they extend.
|
|
5430
|
+
const modeScenes = {};
|
|
5431
|
+
for (const entry of stage.scenes)
|
|
5432
|
+
if (entry.mode)
|
|
5433
|
+
modeScenes[entry.mode] = entry.scene;
|
|
5434
|
+
return stage.scenes
|
|
5435
|
+
.filter((entry) => !entry.mode)
|
|
5436
|
+
.map((entry) => ({
|
|
4818
5437
|
key: entry.key,
|
|
4819
|
-
scene: new DocScene({ ...shared, scene: entry.scene, flow: entry.flow, advanceOnTap: entry.advanceOnTap }),
|
|
5438
|
+
scene: new DocScene({ ...shared, scene: entry.scene, modeScenes, flow: entry.flow, advanceOnTap: entry.advanceOnTap }),
|
|
4820
5439
|
skipOnReplay: entry.skipOnReplay,
|
|
4821
5440
|
}));
|
|
4822
5441
|
}
|
|
@@ -4939,16 +5558,48 @@ class DocScene extends Scene {
|
|
|
4939
5558
|
onSpinStart() {
|
|
4940
5559
|
void this.fire('spinStart');
|
|
4941
5560
|
}
|
|
5561
|
+
/** The resolved doc currently on screen, so a mode switch diffs from the right baseline. */
|
|
5562
|
+
activeDoc_ = null;
|
|
5563
|
+
activeDocId_ = null;
|
|
4942
5564
|
async onSpin(result, ctx) {
|
|
4943
5565
|
await this.fire('result', this.ctxOf(result, ctx));
|
|
4944
5566
|
}
|
|
4945
5567
|
async onEnterMode(result, ctx) {
|
|
4946
5568
|
this.handle_?.setVar('mode', ctx.mode);
|
|
5569
|
+
this.applySceneFor(ctx.mode);
|
|
4947
5570
|
await this.fire('enterMode', this.ctxOf(result, ctx));
|
|
4948
5571
|
}
|
|
4949
5572
|
async onExitMode(result, ctx) {
|
|
4950
5573
|
await this.fire('exitMode', this.ctxOf(result, ctx));
|
|
4951
5574
|
this.handle_?.setVar('mode', 'BASE');
|
|
5575
|
+
this.applySceneFor(null); // back to the base scene
|
|
5576
|
+
}
|
|
5577
|
+
/**
|
|
5578
|
+
* Move the live scene to the doc for `mode` (null = the base scene) by applying the diff.
|
|
5579
|
+
* A no-op when the mode has no scene of its own, which is the common case: most modes
|
|
5580
|
+
* differ only in what flow does.
|
|
5581
|
+
*/
|
|
5582
|
+
applySceneFor(mode) {
|
|
5583
|
+
const target = mode ? this.opts.modeScenes?.[mode] : this.opts.scene;
|
|
5584
|
+
if (!target || !this.handle_)
|
|
5585
|
+
return;
|
|
5586
|
+
const resolved = this.resolveDoc(target);
|
|
5587
|
+
if (resolved.id === this.activeDocId_)
|
|
5588
|
+
return;
|
|
5589
|
+
for (const patch of diffDocs(this.activeDoc_ ?? this.opts.scene, resolved)) {
|
|
5590
|
+
const result = this.handle_.patch(patch);
|
|
5591
|
+
if (result.ok === false)
|
|
5592
|
+
this.opts.log?.(`mode scene "${resolved.id}": ${result.error}`);
|
|
5593
|
+
}
|
|
5594
|
+
this.activeDoc_ = resolved;
|
|
5595
|
+
this.activeDocId_ = resolved.id;
|
|
5596
|
+
}
|
|
5597
|
+
/** Fold a doc's `extends` chain, looking parents up among the scenes this game declared. */
|
|
5598
|
+
resolveDoc(doc) {
|
|
5599
|
+
const pool = { [this.opts.scene.id]: this.opts.scene, ...(this.opts.modeScenes ?? {}) };
|
|
5600
|
+
return resolveExtends(doc, {
|
|
5601
|
+
load: (ref) => pool[ref] ?? Object.values(pool).find((d) => d.id === ref),
|
|
5602
|
+
});
|
|
4952
5603
|
}
|
|
4953
5604
|
onSpinEnd(result, ctx) {
|
|
4954
5605
|
void this.fire('spinEnd', this.ctxOf(result, ctx));
|
|
@@ -4964,5 +5615,5 @@ function createDocScene(opts) {
|
|
|
4964
5615
|
return new DocScene(opts);
|
|
4965
5616
|
}
|
|
4966
5617
|
|
|
4967
|
-
export { BUILTIN_FILTER_KINDS, BUILTIN_NODE_TYPES, DocScene, buildDocScenes, createDocScene, createInnerCalibrator, createSceneFromDoc, createSceneRegistry, createTransformGizmo, dependencyOf, dragInnerEdge, edgePoint, effectiveNode, evalVisibleWhen, handlePoint, nudgeRule, orientationOf, placementBounds, resizeFactors, resizeRule, resolveLayoutRule, setRuleRotation, sizeLever, validateSceneDoc };
|
|
5618
|
+
export { ALPHA_FIELD, BUILTIN_FILTER_KINDS, BUILTIN_NODE_TYPES, DocScene, REMOVE_MARKER, buildDocScenes, createDocScene, createInnerCalibrator, createSceneFromDoc, createSceneRegistry, createTransformGizmo, dependencyOf, diffDocs, dragInnerEdge, edgePoint, effectiveNode, evalVisibleWhen, extraPropKeys, fracBoxFields, handlePoint, nudgeRule, orientationOf, placementBounds, resizeFactors, resizeRule, resolveExtends, resolveLayoutRule, schemaFieldRows, setRuleRotation, sizeLever, validateSceneDoc };
|
|
4968
5619
|
//# sourceMappingURL=scene.esm.js.map
|