@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/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
- /** Without `orientation` replaces the base rule; with it, that orientation's override. */
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
- // Phase 1 effective props (base + orientation + state), applied before measuring.
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
- node.props = { ...(node.props ?? {}), ...patch.props };
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.orientation) {
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
- /** Effective layout rule for the current orientation (base or portrait/landscape override). */
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, orientation } = effectiveRule(nodeId);
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
- orientation,
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;