@energy8platform/game-engine 0.34.0 → 0.34.2
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/flow.cjs.js.map +1 -1
- package/dist/flow.d.ts +15 -2
- package/dist/flow.esm.js.map +1 -1
- package/dist/host.cjs.js +63 -3
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +5 -0
- package/dist/host.esm.js +63 -3
- package/dist/host.esm.js.map +1 -1
- package/dist/scene-devtools.cjs.js +150 -7
- package/dist/scene-devtools.cjs.js.map +1 -1
- package/dist/scene-devtools.d.ts +59 -3
- package/dist/scene-devtools.esm.js +151 -8
- package/dist/scene-devtools.esm.js.map +1 -1
- package/dist/scene.cjs.js +37 -12
- package/dist/scene.cjs.js.map +1 -1
- package/dist/scene.d.ts +63 -3
- package/dist/scene.esm.js +37 -12
- package/dist/scene.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/host/createSlotGame.ts +33 -2
- package/src/host/shellConfig.ts +47 -2
package/dist/scene.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ interface SceneDoc {
|
|
|
11
11
|
width: number;
|
|
12
12
|
height: number;
|
|
13
13
|
};
|
|
14
|
+
/** Authoring modes the scene has views for (e.g. ['base','free_spins']); base is implicit. */
|
|
15
|
+
modes?: string[];
|
|
14
16
|
root: SceneNode;
|
|
15
17
|
}
|
|
16
18
|
interface SceneNode {
|
|
@@ -30,6 +32,13 @@ interface SceneNode {
|
|
|
30
32
|
* frame's `anticipation` / `bonus` looks. A state is an override, not a new node.
|
|
31
33
|
*/
|
|
32
34
|
states?: Record<string, NodeOverride>;
|
|
35
|
+
/**
|
|
36
|
+
* Per-GAME-MODE override, applied automatically when the runtime `mode` var matches
|
|
37
|
+
* (base / free_spins / …). This is why base and free_spins are ONE scene, not two: a
|
|
38
|
+
* node re-poses per mode instead of being duplicated into a separate scene. Layered
|
|
39
|
+
* after orientation, before the active state.
|
|
40
|
+
*/
|
|
41
|
+
modes?: Record<string, NodeOverride>;
|
|
33
42
|
/** Id of another node in this doc used as this node's mask. */
|
|
34
43
|
mask?: string;
|
|
35
44
|
/** Filters applied to the node's view; kinds resolve through the registry (core: blur). */
|
|
@@ -149,17 +158,21 @@ interface PinLayout extends LayoutCommon {
|
|
|
149
158
|
}
|
|
150
159
|
type PinEdge = 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
151
160
|
type LayoutRule = AbsoluteLayout | ViewportFractionLayout | CoverLayout | FrameFractionLayout | GridCellLayout | PinLayout;
|
|
152
|
-
type ScenePatch =
|
|
161
|
+
type ScenePatch =
|
|
162
|
+
/** `mode` targets that mode's override (node.modes[mode].props); else base props. */
|
|
163
|
+
{
|
|
153
164
|
op: 'set-props';
|
|
154
165
|
id: string;
|
|
155
166
|
props: Record<string, unknown>;
|
|
167
|
+
mode?: string;
|
|
156
168
|
}
|
|
157
|
-
/**
|
|
169
|
+
/** Targets, in precedence: `mode` (node.modes[mode]) → `orientation` (responsive) → base. */
|
|
158
170
|
| {
|
|
159
171
|
op: 'set-layout';
|
|
160
172
|
id: string;
|
|
161
173
|
layout: LayoutRule;
|
|
162
174
|
orientation?: Orientation;
|
|
175
|
+
mode?: string;
|
|
163
176
|
} | {
|
|
164
177
|
op: 'set-state';
|
|
165
178
|
id: string;
|
|
@@ -250,7 +263,7 @@ type LayoutResolution = {
|
|
|
250
263
|
};
|
|
251
264
|
declare function orientationOf(width: number, height: number, portraitFactor?: number): Orientation;
|
|
252
265
|
/** Base node + orientation override + active state override (state wins), for one frame. */
|
|
253
|
-
declare function effectiveNode(node: SceneNode, orientation: Orientation, state: string | null): {
|
|
266
|
+
declare function effectiveNode(node: SceneNode, orientation: Orientation, state: string | null, mode?: string | null): {
|
|
254
267
|
layout?: LayoutRule;
|
|
255
268
|
props: Record<string, unknown>;
|
|
256
269
|
visible: boolean;
|
|
@@ -501,6 +514,8 @@ interface TransformGizmoOptions {
|
|
|
501
514
|
app: Application;
|
|
502
515
|
applyPatch?: (patch: ScenePatch) => void;
|
|
503
516
|
getOrientation?: () => Orientation;
|
|
517
|
+
/** Current authoring mode; when set and not 'base', edits target that mode's override. */
|
|
518
|
+
getMode?: () => string | null;
|
|
504
519
|
/** Override screen→design conversion for move drags. Default: the node's parent world scale. */
|
|
505
520
|
designPerPixel?: (nodeId: string) => number;
|
|
506
521
|
}
|
|
@@ -556,6 +571,32 @@ declare abstract class Scene implements IScene {
|
|
|
556
571
|
onDestroy?(): void;
|
|
557
572
|
}
|
|
558
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Gate for the PROGRESSIVE (cascade / tumble) WIN readout.
|
|
576
|
+
*
|
|
577
|
+
* By default the WIN readout is entirely host-driven: cleared to 0 when a segment starts, set to
|
|
578
|
+
* that segment's win once `onSpin` resolves. A cascade game pays in steps, so its scene wants the
|
|
579
|
+
* number to climb WHILE the segment presents — `api.shell.reportWin(amountSoFar)`.
|
|
580
|
+
*
|
|
581
|
+
* The gate keeps the host in charge:
|
|
582
|
+
* - reports are honoured ONLY between `open()` (segment start, WIN already cleared) and `close()`
|
|
583
|
+
* (the host paints the segment's final value) — a scene still animating after an abort, or one
|
|
584
|
+
* reporting from a mode transition, can't overwrite the host's number,
|
|
585
|
+
* - `amountSoFar` is ABSOLUTE (the segment's win up to now, not the step delta), so a re-report or
|
|
586
|
+
* a collapse-to-final on skip is idempotent. Reports should be non-decreasing within a segment;
|
|
587
|
+
* a lower value counts the readout back DOWN,
|
|
588
|
+
* - garbage (NaN / Infinity) is dropped and negatives clamp to 0, so a math bug can't paint "NaN"
|
|
589
|
+
* into the bar.
|
|
590
|
+
*
|
|
591
|
+
* Pure + unit-testable: the shell paint is injected.
|
|
592
|
+
*/
|
|
593
|
+
interface WinReportOptions {
|
|
594
|
+
/** `false` snaps instead of counting up (e.g. collapsing to the final value on skip). */
|
|
595
|
+
animate?: boolean;
|
|
596
|
+
/** Count-up length in ms (shell default 450) — pass the cascade step's length. */
|
|
597
|
+
durationMs?: number;
|
|
598
|
+
}
|
|
599
|
+
|
|
559
600
|
/** Everything a scene needs to render one segment. The host builds it per segment. */
|
|
560
601
|
interface RenderContext {
|
|
561
602
|
/** Bet for this round (major units). Stable for the whole round. */
|
|
@@ -623,6 +664,25 @@ interface SceneShell {
|
|
|
623
664
|
bottom: number;
|
|
624
665
|
left: number;
|
|
625
666
|
};
|
|
667
|
+
/**
|
|
668
|
+
* Grow the shell's WIN readout WHILE a segment presents — for cascade/tumble games that pay in
|
|
669
|
+
* steps instead of one lump at the end.
|
|
670
|
+
*
|
|
671
|
+
* `amountSoFar` is ABSOLUTE: the win accumulated by this segment up to now (not the step's
|
|
672
|
+
* delta), so a re-report, a skip, or an aborted step can just restate the truth. The host owns
|
|
673
|
+
* the readout around it — it clears WIN to 0 when the segment starts and sets the segment's
|
|
674
|
+
* final value once `onSpin` resolves (counting up from your last report, so matching numbers
|
|
675
|
+
* produce no jump). In a bonus this moves WIN only; the Total Win accumulator still lands once
|
|
676
|
+
* per segment.
|
|
677
|
+
*
|
|
678
|
+
* Only honoured while a segment is presenting (inside `onSpin`); calls from anywhere else are
|
|
679
|
+
* ignored, so a scene still ticking after an abort can't overwrite the host's final number.
|
|
680
|
+
*
|
|
681
|
+
* `durationMs` sets this count-up's length (default 450ms) — pass your step length (or a shorter
|
|
682
|
+
* one under turbo) so each count-up finishes before the next step lands. `{ animate: false }`
|
|
683
|
+
* snaps, e.g. when collapsing to the final value on skip.
|
|
684
|
+
*/
|
|
685
|
+
reportWin(amountSoFar: number, opts?: WinReportOptions): void;
|
|
626
686
|
}
|
|
627
687
|
interface AutoplaySceneState {
|
|
628
688
|
running: boolean;
|
package/dist/scene.esm.js
CHANGED
|
@@ -10,9 +10,11 @@ function orientationOf(width, height, portraitFactor = 1) {
|
|
|
10
10
|
return width < height * portraitFactor ? 'portrait' : 'landscape';
|
|
11
11
|
}
|
|
12
12
|
/** Base node + orientation override + active state override (state wins), for one frame. */
|
|
13
|
-
function effectiveNode(node, orientation, state) {
|
|
13
|
+
function effectiveNode(node, orientation, state, mode) {
|
|
14
|
+
// Layer order: base → orientation → mode → active state (later wins).
|
|
14
15
|
const layers = [
|
|
15
16
|
node.responsive?.[orientation],
|
|
17
|
+
mode ? node.modes?.[mode] : undefined,
|
|
16
18
|
state ? node.states?.[state] : undefined,
|
|
17
19
|
];
|
|
18
20
|
let layout = node.layout;
|
|
@@ -2880,6 +2882,7 @@ function createReelSystem(opts) {
|
|
|
2880
2882
|
for (let i = 0; i < steps.length; i++) {
|
|
2881
2883
|
await tumble.step(steps[i], i, cOpts);
|
|
2882
2884
|
board = steps[i].settledGrid;
|
|
2885
|
+
await cOpts?.onStep?.(i, steps[i], tumble.multiplier);
|
|
2883
2886
|
}
|
|
2884
2887
|
if (config.cascade.multiplier.enabled)
|
|
2885
2888
|
log?.(`Cascade multiplier ×${tumble.multiplier}`);
|
|
@@ -2891,6 +2894,7 @@ function createReelSystem(opts) {
|
|
|
2891
2894
|
for (let i = 0; i < steps.length; i++) {
|
|
2892
2895
|
await reelStepCtl.step(steps[i], i, rOpts);
|
|
2893
2896
|
board = steps[i].settledGrid;
|
|
2897
|
+
await rOpts?.onStep?.(i, steps[i], reelStepCtl.multiplier);
|
|
2894
2898
|
}
|
|
2895
2899
|
if (config.cascade.multiplier.enabled)
|
|
2896
2900
|
log?.(`ReelStep multiplier ×${reelStepCtl.multiplier}`);
|
|
@@ -3434,9 +3438,10 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3434
3438
|
lastWidth = width;
|
|
3435
3439
|
lastHeight = height;
|
|
3436
3440
|
const orientation = orientationOf(width, height, opts.portraitFactor ?? 1);
|
|
3437
|
-
|
|
3441
|
+
const mode = typeof vars.mode === 'string' ? vars.mode : null;
|
|
3442
|
+
// Phase 1 — effective props (base + orientation + mode + state), applied before measuring.
|
|
3438
3443
|
for (const runtime of [...runtimes.values()]) {
|
|
3439
|
-
const eff = effectiveNode(runtime.node, orientation, runtime.state);
|
|
3444
|
+
const eff = effectiveNode(runtime.node, orientation, runtime.state, mode);
|
|
3440
3445
|
runtime.visibleBase = eff.visible;
|
|
3441
3446
|
const space = eff.props.space;
|
|
3442
3447
|
runtime.spaceSize =
|
|
@@ -3540,7 +3545,7 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3540
3545
|
const parentOriginY = parentIsFrame ? 0 : (runtime.parent?.bounds?.y ?? 0);
|
|
3541
3546
|
const parentScaleX = parentIsFrame ? 1 : (runtime.parent?.worldScaleX ?? 1);
|
|
3542
3547
|
const parentScaleY = parentIsFrame ? 1 : (runtime.parent?.worldScaleY ?? 1);
|
|
3543
|
-
const eff = effectiveNode(runtime.node, orientation, runtime.state);
|
|
3548
|
+
const eff = effectiveNode(runtime.node, orientation, runtime.state, mode);
|
|
3544
3549
|
const natural = runtime.instance.measure();
|
|
3545
3550
|
const rule = eff.layout;
|
|
3546
3551
|
// `space:'viewport'` resolves against the live viewport even inside a nested
|
|
@@ -3713,14 +3718,25 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3713
3718
|
const node = findNode(doc.root, patch.id);
|
|
3714
3719
|
if (!node)
|
|
3715
3720
|
return warnOnce(`patch:${patch.id}`, `patch: unknown node "${patch.id}"`);
|
|
3716
|
-
|
|
3721
|
+
if (patch.mode) {
|
|
3722
|
+
node.modes = { ...(node.modes ?? {}) };
|
|
3723
|
+
const over = node.modes[patch.mode] ?? {};
|
|
3724
|
+
node.modes[patch.mode] = { ...over, props: { ...(over.props ?? {}), ...patch.props } };
|
|
3725
|
+
}
|
|
3726
|
+
else {
|
|
3727
|
+
node.props = { ...(node.props ?? {}), ...patch.props };
|
|
3728
|
+
}
|
|
3717
3729
|
break;
|
|
3718
3730
|
}
|
|
3719
3731
|
case 'set-layout': {
|
|
3720
3732
|
const node = findNode(doc.root, patch.id);
|
|
3721
3733
|
if (!node)
|
|
3722
3734
|
return warnOnce(`patch:${patch.id}`, `patch: unknown node "${patch.id}"`);
|
|
3723
|
-
if (patch.
|
|
3735
|
+
if (patch.mode) {
|
|
3736
|
+
node.modes = { ...(node.modes ?? {}) };
|
|
3737
|
+
node.modes[patch.mode] = { ...(node.modes[patch.mode] ?? {}), layout: patch.layout };
|
|
3738
|
+
}
|
|
3739
|
+
else if (patch.orientation) {
|
|
3724
3740
|
node.responsive = { ...(node.responsive ?? {}) };
|
|
3725
3741
|
node.responsive[patch.orientation] = {
|
|
3726
3742
|
...(node.responsive[patch.orientation] ?? {}),
|
|
@@ -4137,6 +4153,7 @@ function createTransformGizmo(opts) {
|
|
|
4137
4153
|
layer.addChildAt(body, 1);
|
|
4138
4154
|
let nodeId = null;
|
|
4139
4155
|
let drag = null;
|
|
4156
|
+
const getMode = () => opts.getMode?.() ?? null;
|
|
4140
4157
|
const findDocNode = (id, node = handle.doc().root) => {
|
|
4141
4158
|
if (node.id === id)
|
|
4142
4159
|
return node;
|
|
@@ -4147,14 +4164,22 @@ function createTransformGizmo(opts) {
|
|
|
4147
4164
|
}
|
|
4148
4165
|
return undefined;
|
|
4149
4166
|
};
|
|
4150
|
-
/**
|
|
4167
|
+
/**
|
|
4168
|
+
* Effective layout rule + which override an edit should target. Editing in a non-base
|
|
4169
|
+
* mode targets that mode's override (so base and free_spins are edited independently);
|
|
4170
|
+
* otherwise the current orientation override, else base.
|
|
4171
|
+
*/
|
|
4151
4172
|
const effectiveRule = (id) => {
|
|
4152
4173
|
const node = findDocNode(id);
|
|
4153
4174
|
if (!node)
|
|
4154
|
-
return {};
|
|
4175
|
+
return { target: {} };
|
|
4176
|
+
const mode = getMode();
|
|
4177
|
+
if (mode && mode !== 'base') {
|
|
4178
|
+
return { rule: node.modes?.[mode]?.layout ?? node.layout, target: { mode } };
|
|
4179
|
+
}
|
|
4155
4180
|
const o = getOrientation();
|
|
4156
4181
|
const override = node.responsive?.[o]?.layout;
|
|
4157
|
-
return override ? { rule: override, orientation: o } : { rule: node.layout };
|
|
4182
|
+
return override ? { rule: override, target: { orientation: o } } : { rule: node.layout, target: {} };
|
|
4158
4183
|
};
|
|
4159
4184
|
const boxOf = (id) => {
|
|
4160
4185
|
const view = handle.node(id);
|
|
@@ -4168,7 +4193,7 @@ function createTransformGizmo(opts) {
|
|
|
4168
4193
|
if (!nodeId)
|
|
4169
4194
|
return;
|
|
4170
4195
|
const box = boxOf(nodeId);
|
|
4171
|
-
const { rule,
|
|
4196
|
+
const { rule, target } = effectiveRule(nodeId);
|
|
4172
4197
|
if (!box || !rule)
|
|
4173
4198
|
return;
|
|
4174
4199
|
const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
@@ -4176,7 +4201,7 @@ function createTransformGizmo(opts) {
|
|
|
4176
4201
|
handle: h,
|
|
4177
4202
|
startBox: box,
|
|
4178
4203
|
baseRule: JSON.parse(JSON.stringify(rule)),
|
|
4179
|
-
|
|
4204
|
+
target,
|
|
4180
4205
|
start: { x: e.global.x, y: e.global.y },
|
|
4181
4206
|
center,
|
|
4182
4207
|
startAngle: Math.atan2(e.global.y - center.y, e.global.x - center.x),
|
|
@@ -4200,7 +4225,7 @@ function createTransformGizmo(opts) {
|
|
|
4200
4225
|
const { factorX, factorY } = resizeFactors(drag.startBox, drag.handle, { x: e.global.x, y: e.global.y });
|
|
4201
4226
|
rule = resizeRule(drag.baseRule, factorX, factorY);
|
|
4202
4227
|
}
|
|
4203
|
-
applyPatch({ op: 'set-layout', id: nodeId, layout: rule, orientation: drag.orientation });
|
|
4228
|
+
applyPatch({ op: 'set-layout', id: nodeId, layout: rule, orientation: drag.target.orientation, mode: drag.target.mode });
|
|
4204
4229
|
};
|
|
4205
4230
|
const onUp = () => {
|
|
4206
4231
|
drag = null;
|