@energy8platform/game-engine 0.11.0 → 0.13.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 CHANGED
@@ -100,7 +100,7 @@ import { GameApplication } from '@energy8platform/game-engine'; // f
100
100
  import { Scene, SceneManager } from '@energy8platform/game-engine/core';
101
101
  import { AssetManager } from '@energy8platform/game-engine/assets';
102
102
  import { AudioManager } from '@energy8platform/game-engine/audio';
103
- import { FlexContainer, Button, Label, Panel, Modal, Layout, ScrollContainer, Toast, ProgressBar, BalanceDisplay, WinDisplay, resolveView } from '@energy8platform/game-engine/ui';
103
+ import { FlexContainer, Button, Label, Panel, Modal, Layout, ScrollContainer, Toast, ProgressBar, BalanceDisplay, WinDisplay, Slider, Toggle, resolveView } from '@energy8platform/game-engine/ui';
104
104
  import { Tween, Timeline, Easing, SpriteAnimation } from '@energy8platform/game-engine/animation';
105
105
  import { DevBridge, FPSOverlay } from '@energy8platform/game-engine/debug';
106
106
  import { ReactScene, extendPixiElements, extendUIElements, useSDK, useViewport } from '@energy8platform/game-engine/react';
@@ -426,6 +426,26 @@ toolbar.addFlexChild(spacer, { flexGrow: 1 }); // with flex config
426
426
  toolbar.resize(800, 60);
427
427
  ```
428
428
 
429
+ **FlexItemConfig** — per-child options passed via `addFlexChild(child, config)` or JSX props:
430
+
431
+ | Property | Type | Default | Description |
432
+ | --- | --- | --- | --- |
433
+ | `flexGrow` | `number` | `0` | Flex grow factor |
434
+ | `flexShrink` | `number` | `1` | Flex shrink factor (`0` = don't shrink when content overflows) |
435
+ | `alignSelf` | `'auto' \| 'start' \| 'center' \| 'end' \| 'stretch'` | `'auto'` | Override parent's `alignItems` for this child |
436
+ | `flexExclude` | `boolean` | `false` | Exclude from flex layout (like `position: absolute`) |
437
+ | `layoutWidth` | `number` | — | Explicit width override for layout calculations |
438
+ | `layoutHeight` | `number` | — | Explicit height override for layout calculations |
439
+
440
+ ```typescript
441
+ // Fixed item that won't shrink + centered override
442
+ toolbar.addFlexChild(logo, { flexShrink: 0 });
443
+ toolbar.addFlexChild(badge, { alignSelf: 'center' });
444
+
445
+ // Background excluded from layout flow
446
+ toolbar.addFlexChild(background, { flexExclude: true });
447
+ ```
448
+
429
449
  ### Layout
430
450
 
431
451
  Higher-level layout with direction presets (`horizontal`/`vertical`/`grid`/`wrap`), viewport anchoring, and responsive breakpoints. Wraps FlexContainer.
@@ -494,6 +514,58 @@ const bar = new ProgressBar({
494
514
  });
495
515
  ```
496
516
 
517
+ ### Slider
518
+
519
+ Draggable slider with customizable track, fill, and handle views:
520
+
521
+ ```typescript
522
+ // Graphics-based
523
+ const volume = new Slider({
524
+ min: 0, max: 1, value: 0.5, step: 0.1,
525
+ width: 200, height: 8,
526
+ fillColor: 0xffd700,
527
+ onUpdate: (v) => audio.setVolume('music', v),
528
+ onChange: (v) => console.log('Final:', v),
529
+ });
530
+
531
+ // Asset-based
532
+ const volume = new Slider({
533
+ min: 0, max: 1, value: 0.5,
534
+ width: 200, height: 8,
535
+ trackView: 'slider-track',
536
+ fillView: 'slider-fill',
537
+ handleView: 'slider-handle',
538
+ onUpdate: (v) => audio.setVolume('music', v),
539
+ });
540
+ ```
541
+
542
+ `onUpdate` fires continuously during drag, `onChange` fires once when drag ends.
543
+
544
+ ### Toggle
545
+
546
+ Two-state toggle switch with animation:
547
+
548
+ ```typescript
549
+ // Graphics-based
550
+ const mute = new Toggle({
551
+ value: false,
552
+ width: 52, height: 28,
553
+ onColor: 0x22cc22, offColor: 0x666666,
554
+ onChange: (on) => audio.muteAll(!on),
555
+ });
556
+
557
+ // Asset-based (custom ON/OFF views with crossfade)
558
+ const ante = new Toggle({
559
+ value: false,
560
+ onView: 'toggle-on',
561
+ offView: 'toggle-off',
562
+ onChange: (on) => setAnteBet(on),
563
+ });
564
+
565
+ // Programmatic control
566
+ mute.forceSwitch(true);
567
+ ```
568
+
497
569
  ### ScrollContainer
498
570
 
499
571
  Touch/drag scrolling with mouse wheel, inertia, and optional visual scrollbar:
@@ -824,6 +896,21 @@ All engine UI components are config-based: the reconciler passes JSX props as a
824
896
  <label text="Item 1" />
825
897
  <label text="Item 2" />
826
898
  </scrollContainer>
899
+
900
+ {/* Slider */}
901
+ <slider min={0} max={1} value={volume} width={200} height={8}
902
+ fillColor={0xffd700} onUpdate={setVolume} />
903
+
904
+ {/* Toggle */}
905
+ <toggle value={isMuted} onColor={0x22cc22} onChange={setIsMuted} />
906
+
907
+ {/* Flex item props — work on any element inside <flexContainer> */}
908
+ <flexContainer direction="row" width={800} height={60}>
909
+ <graphics draw={drawBg} flexExclude /> {/* excluded from flow */}
910
+ <label text="Logo" flexShrink={0} /> {/* won't shrink */}
911
+ <container flexGrow={1} /> {/* fills remaining space */}
912
+ <button text="Menu" alignSelf="center" /> {/* centered on cross-axis */}
913
+ </flexContainer>
827
914
  ```
828
915
 
829
916
  **Dash-notation** for nested config: `colors-default={0xff0000}` → `{ colors: { default: 0xff0000 } }`, `style-fontSize={24}` → `{ style: { fontSize: 24 } }`.
package/bin/simulate.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env npx tsx
2
2
  import { SimulationRunner, formatSimulationResult } from '../src/lua/SimulationRunner';
3
3
  import { ParallelSimulationRunner } from '../src/lua/ParallelSimulationRunner';
4
+ import { NativeSimulationRunner, findNativeBinary, formatNativeResult } from '../src/lua/NativeSimulationRunner';
4
5
  import { cpus } from 'os';
5
- import { resolve } from 'path';
6
+ import { resolve, dirname } from 'path';
6
7
 
7
8
  // ─── Argument Parsing ───────────────────────────────────
8
9
 
@@ -10,9 +11,14 @@ function parseArgs(argv: string[]): Record<string, string> {
10
11
  const args: Record<string, string> = {};
11
12
  for (let i = 2; i < argv.length; i++) {
12
13
  const arg = argv[i];
13
- if (arg.startsWith('--') && i + 1 < argv.length) {
14
+ if (arg.startsWith('--')) {
14
15
  const key = arg.slice(2);
15
- args[key] = argv[++i];
16
+ // Boolean flags (no value)
17
+ if (key === 'native' || key === 'js') {
18
+ args[key] = 'true';
19
+ } else if (i + 1 < argv.length) {
20
+ args[key] = argv[++i];
21
+ }
16
22
  }
17
23
  }
18
24
  return args;
@@ -28,6 +34,8 @@ async function main() {
28
34
  const action = args.action ?? 'spin';
29
35
  const params = args.params ? JSON.parse(args.params) : undefined;
30
36
  const workers = args.workers ? parseInt(args.workers, 10) : cpus().length;
37
+ const useNative = args.native === 'true';
38
+ const useJs = args.js === 'true';
31
39
 
32
40
  // Load dev config
33
41
  let config: any;
@@ -52,14 +60,46 @@ async function main() {
52
60
  }
53
61
 
54
62
  const gameId = config.gameDefinition.id ?? 'unknown';
55
- const useParallel = workers > 1;
56
- console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action}, workers: ${useParallel ? workers : 1})...`);
63
+
64
+ // ─── Native binary detection ────────────────────────────
65
+ // Search in config dir first, then in the game-engine package root
66
+ const engineRoot = resolve(__dirname, '..');
67
+ const binaryPath = args.binary ?? (useJs ? null : (findNativeBinary(dirname(configPath)) ?? findNativeBinary(engineRoot)));
68
+
69
+ if (useNative && !binaryPath) {
70
+ console.error('Native simulation binary not found.');
71
+ console.error('Use --binary <path> or set SIMULATE_BINARY environment variable.');
72
+ process.exit(1);
73
+ }
57
74
 
58
75
  const onProgress = (completed: number, total: number) => {
59
76
  const pct = Math.round((completed / total) * 100);
60
77
  console.log(`Progress: ${completed.toLocaleString()}/${total.toLocaleString()} (${pct}%)`);
61
78
  };
62
79
 
80
+ // ─── Native binary path ─────────────────────────────────
81
+ if (binaryPath) {
82
+ console.log(`Using native binary: ${binaryPath}`);
83
+ console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action})...`);
84
+
85
+ const runner = new NativeSimulationRunner({
86
+ binaryPath,
87
+ script: config.luaScript,
88
+ gameDefinition: config.gameDefinition,
89
+ iterations,
90
+ bet,
91
+ action,
92
+ params,
93
+ });
94
+ const result = await runner.run();
95
+ console.log(formatNativeResult(result));
96
+ return;
97
+ }
98
+
99
+ // ─── JS simulation path ─────────────────────────────────
100
+ const useParallel = workers > 1;
101
+ console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action}, workers: ${useParallel ? workers : 1})...`);
102
+
63
103
  let result;
64
104
 
65
105
  if (useParallel) {
package/dist/index.cjs.js CHANGED
@@ -2966,6 +2966,37 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
2966
2966
  }
2967
2967
  }
2968
2968
  }
2969
+ // Shrink: if content overflows and mainSize is finite, shrink eligible items
2970
+ if (totalGrow === 0 && mainSize > 0) {
2971
+ const overflow = totalFixed + totalGap - mainSize;
2972
+ if (overflow > 0) {
2973
+ let totalShrinkable = 0;
2974
+ for (const item of items) {
2975
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
2976
+ if (shrink > 0) {
2977
+ totalShrinkable += isRow ? item.w : item.h;
2978
+ }
2979
+ }
2980
+ if (totalShrinkable > 0) {
2981
+ for (const item of items) {
2982
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
2983
+ if (shrink > 0) {
2984
+ const itemMain = isRow ? item.w : item.h;
2985
+ const reduction = overflow * (itemMain / totalShrinkable);
2986
+ const newSize = Math.max(0, itemMain - reduction);
2987
+ if (isRow) {
2988
+ item.w = newSize;
2989
+ item.child.width = newSize;
2990
+ }
2991
+ else {
2992
+ item.h = newSize;
2993
+ item.child.height = newSize;
2994
+ }
2995
+ }
2996
+ }
2997
+ }
2998
+ }
2999
+ }
2969
3000
  // Calculate total main size after flex
2970
3001
  let totalMain = totalGap;
2971
3002
  for (const item of items) {
@@ -3002,9 +3033,12 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
3002
3033
  for (const item of items) {
3003
3034
  const mainDim = isRow ? item.w : item.h;
3004
3035
  const crossDim = isRow ? item.h : item.w;
3005
- // Cross-axis alignment
3036
+ // Cross-axis alignment (alignSelf overrides align)
3037
+ const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
3038
+ ? item.child._flexConfig.alignSelf
3039
+ : align;
3006
3040
  let crossPos = crossOffset;
3007
- switch (align) {
3041
+ switch (effectiveAlign) {
3008
3042
  case 'start':
3009
3043
  break;
3010
3044
  case 'center':
@@ -3188,11 +3222,14 @@ class FlexContainer extends pixi_js.Container {
3188
3222
  const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
3189
3223
  const mainLimit = isRow ? contentW : contentH;
3190
3224
  const crossLimit = isRow ? contentH : contentW;
3191
- // Measure children
3192
- const measured = this._layoutChildren.map((child) => {
3225
+ // Measure children (skip flexExclude — they position themselves)
3226
+ const measured = [];
3227
+ for (const child of this._layoutChildren) {
3228
+ if (child._flexConfig?.flexExclude)
3229
+ continue;
3193
3230
  const { w, h, ox, oy } = measureChild(child);
3194
- return { child, w, h, ox, oy };
3195
- });
3231
+ measured.push({ child, w, h, ox, oy });
3232
+ }
3196
3233
  // Split into lines (if wrapping)
3197
3234
  const lines = [];
3198
3235
  if (flexWrap && mainLimit < Infinity) {
@@ -3235,7 +3272,12 @@ class FlexContainer extends pixi_js.Container {
3235
3272
  const mainStart = isRow ? pl : pt;
3236
3273
  // Offset items by padding
3237
3274
  const tempItems = line.map((item) => ({ ...item }));
3238
- layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
3275
+ // For single-line layouts, use the full available cross space for alignment;
3276
+ // for multi-line (wrapping), each line gets its own measured cross size.
3277
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity
3278
+ ? crossLimit
3279
+ : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
3280
+ layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
3239
3281
  // Apply main-axis padding offset
3240
3282
  for (const item of tempItems) {
3241
3283
  const origChild = line.find((l) => l.child === item.child);