@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.
@@ -0,0 +1,367 @@
1
+ import { spawn } from 'child_process';
2
+ import { writeFile, unlink } from 'fs/promises';
3
+ import { join, dirname } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { randomBytes } from 'crypto';
6
+ import { execSync } from 'child_process';
7
+ import { fileURLToPath } from 'url';
8
+ import type { GameDefinition, SimulationResult } from './types';
9
+
10
+ // ─── Types ──────────────────────────────────────────────
11
+
12
+ export interface NativeSimulationConfig {
13
+ /** Path to native simulation binary */
14
+ binaryPath: string;
15
+ /** Lua script source code */
16
+ script: string;
17
+ /** Platform game definition */
18
+ gameDefinition: GameDefinition;
19
+ /** Number of iterations */
20
+ iterations: number;
21
+ /** Bet amount */
22
+ bet: number;
23
+ /** Action to simulate (default: auto-detect by binary) */
24
+ action?: string;
25
+ /** Action params (buy_bonus, ante_bet, etc.) */
26
+ params?: Record<string, unknown>;
27
+ /** Progress callback */
28
+ onProgress?: (completed: number, total: number) => void;
29
+ }
30
+
31
+ export interface StageStats {
32
+ totalWin: number;
33
+ spinCount: number;
34
+ hitCount: number;
35
+ maxWin: number;
36
+ rtp: number;
37
+ perSpinRtp: number;
38
+ hitFrequency: number;
39
+ avgWin: number;
40
+ }
41
+
42
+ export interface DistributionBucket {
43
+ label: string;
44
+ count: number;
45
+ pct: number;
46
+ }
47
+
48
+ export interface NativeSimulationResult extends SimulationResult {
49
+ /** Iterations per second */
50
+ speed?: number;
51
+ /** Number of parallel workers used */
52
+ workersUsed?: number;
53
+ /** Per-stage breakdown */
54
+ perStage?: Record<string, StageStats>;
55
+ /** Win distribution histogram */
56
+ winDistribution?: DistributionBucket[];
57
+ }
58
+
59
+ // ─── Go JSON output shape (snake_case) ──────────────────
60
+
61
+ interface GoSimulationOutput {
62
+ game_id: string;
63
+ speed: number;
64
+ total_rtp: number;
65
+ hit_frequency: number;
66
+ max_win: number;
67
+ max_win_hits: number;
68
+ total_bet: number;
69
+ total_win: number;
70
+ iterations: number;
71
+ workers_used: number;
72
+ duration_sec: number;
73
+ bonus_triggered: number;
74
+ bonus_spins_total: number;
75
+ per_stage_stats?: Record<string, {
76
+ total_win: number;
77
+ spin_count: number;
78
+ hit_count: number;
79
+ max_win: number;
80
+ rtp: number;
81
+ per_spin_rtp: number;
82
+ hit_frequency: number;
83
+ avg_win: number;
84
+ }>;
85
+ win_distribution?: Array<{
86
+ label: string;
87
+ count: number;
88
+ pct: number;
89
+ }>;
90
+ }
91
+
92
+ // ─── Runner ─────────────────────────────────────────────
93
+
94
+ export class NativeSimulationRunner {
95
+ private config: NativeSimulationConfig;
96
+
97
+ constructor(config: NativeSimulationConfig) {
98
+ this.config = config;
99
+ }
100
+
101
+ async run(): Promise<NativeSimulationResult> {
102
+ const { binaryPath, script, gameDefinition, iterations, bet, action, params } = this.config;
103
+ const id = randomBytes(8).toString('hex');
104
+ const tmpDir = tmpdir();
105
+ const luaPath = join(tmpDir, `sim-${id}.lua`);
106
+ const configPath = join(tmpDir, `sim-${id}.json`);
107
+
108
+ try {
109
+ // Write temp files
110
+ await Promise.all([
111
+ writeFile(luaPath, script, 'utf-8'),
112
+ writeFile(configPath, JSON.stringify({ ...gameDefinition, script_path: luaPath }), 'utf-8'),
113
+ ]);
114
+
115
+ // Build CLI args
116
+ const args = [
117
+ '-config', configPath,
118
+ '-iterations', String(iterations),
119
+ '-bet', String(bet),
120
+ '-format', 'json',
121
+ ];
122
+ if (action) {
123
+ args.push('-action', action);
124
+ }
125
+ if (params && Object.keys(params).length > 0) {
126
+ args.push('-params', JSON.stringify(params));
127
+ }
128
+
129
+ // Execute binary
130
+ const output = await this.exec(binaryPath, args);
131
+
132
+ // Parse JSON output
133
+ const json: GoSimulationOutput = JSON.parse(output);
134
+ return mapGoResult(json);
135
+ } finally {
136
+ // Cleanup temp files
137
+ await Promise.allSettled([unlink(luaPath), unlink(configPath)]);
138
+ }
139
+ }
140
+
141
+ private exec(binary: string, args: string[]): Promise<string> {
142
+ return new Promise((resolve, reject) => {
143
+ const child = spawn(binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
144
+
145
+ let stdout = '';
146
+ let stderr = '';
147
+
148
+ child.stdout.on('data', (chunk: Buffer) => {
149
+ stdout += chunk.toString();
150
+ });
151
+
152
+ child.stderr.on('data', (chunk: Buffer) => {
153
+ stderr += chunk.toString();
154
+ });
155
+
156
+ child.on('error', (err) => {
157
+ reject(new Error(`Failed to execute simulation binary: ${err.message}`));
158
+ });
159
+
160
+ child.on('close', (code) => {
161
+ if (code !== 0) {
162
+ reject(new Error(`Simulation binary exited with code ${code}: ${stderr.trim()}`));
163
+ } else {
164
+ resolve(stdout);
165
+ }
166
+ });
167
+ });
168
+ }
169
+ }
170
+
171
+ // ─── Result mapping ─────────────────────────────────────
172
+
173
+ function mapGoResult(json: GoSimulationOutput): NativeSimulationResult {
174
+ const baseStage = json.per_stage_stats?.base_game;
175
+ const baseGameRtp = baseStage?.rtp ?? 0;
176
+ const baseGameWin = baseStage?.total_win ?? 0;
177
+
178
+ const perStage = json.per_stage_stats
179
+ ? Object.fromEntries(
180
+ Object.entries(json.per_stage_stats).map(([key, s]) => [
181
+ key,
182
+ {
183
+ totalWin: s.total_win,
184
+ spinCount: s.spin_count,
185
+ hitCount: s.hit_count,
186
+ maxWin: s.max_win,
187
+ rtp: s.rtp,
188
+ perSpinRtp: s.per_spin_rtp,
189
+ hitFrequency: s.hit_frequency,
190
+ avgWin: s.avg_win,
191
+ },
192
+ ]),
193
+ )
194
+ : undefined;
195
+
196
+ return {
197
+ gameId: json.game_id,
198
+ action: 'spin',
199
+ iterations: json.iterations,
200
+ durationMs: Math.round(json.duration_sec * 1000),
201
+ totalRtp: json.total_rtp,
202
+ baseGameRtp,
203
+ bonusRtp: json.total_rtp - baseGameRtp,
204
+ hitFrequency: json.hit_frequency,
205
+ maxWin: json.max_win,
206
+ maxWinHits: json.max_win_hits,
207
+ bonusTriggered: json.bonus_triggered,
208
+ bonusSpinsPlayed: json.bonus_spins_total,
209
+ speed: json.speed,
210
+ workersUsed: json.workers_used,
211
+ perStage,
212
+ winDistribution: json.win_distribution,
213
+ _raw: {
214
+ totalWagered: json.total_bet,
215
+ totalWon: json.total_win,
216
+ baseGameWin,
217
+ bonusWin: json.total_win - baseGameWin,
218
+ hits: json.iterations > 0 ? Math.round((json.hit_frequency * json.iterations) / 100) : 0,
219
+ },
220
+ };
221
+ }
222
+
223
+ // ─── Binary discovery ───────────────────────────────────
224
+
225
+ /**
226
+ * Search for a native simulation binary in standard locations.
227
+ * Returns the absolute path if found, null otherwise.
228
+ */
229
+ export function findNativeBinary(baseDir?: string): string | null {
230
+ // 1. Explicit env var
231
+ const envPath = process.env.SIMULATE_BINARY;
232
+ if (envPath && isExecutable(envPath)) {
233
+ return envPath;
234
+ }
235
+
236
+ const platform = process.platform; // darwin, linux, win32
237
+ const nodeArch = process.arch; // arm64, x64
238
+ const goArch = nodeArch === 'x64' ? 'amd64' : nodeArch;
239
+ const goPlatform = platform === 'win32' ? 'windows' : platform;
240
+ const ext = platform === 'win32' ? '.exe' : '';
241
+
242
+ const names = [
243
+ `simulate-${goPlatform}-${goArch}${ext}`,
244
+ `simulation-${goPlatform}-${goArch}${ext}`,
245
+ `simulate${ext}`,
246
+ `simulation${ext}`,
247
+ ];
248
+
249
+ // Search directories: user's project first, then this package's bin/
250
+ const searchDirs: string[] = [];
251
+ if (baseDir) searchDirs.push(baseDir);
252
+
253
+ // This package's root (where postinstall downloads the binary)
254
+ try {
255
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
256
+ if (!searchDirs.includes(pkgRoot)) searchDirs.push(pkgRoot);
257
+ } catch {
258
+ // fallback for CJS
259
+ if (typeof __dirname !== 'undefined') {
260
+ const pkgRoot = join(__dirname, '..');
261
+ if (!searchDirs.includes(pkgRoot)) searchDirs.push(pkgRoot);
262
+ }
263
+ }
264
+
265
+ for (const dir of searchDirs) {
266
+ for (const name of names) {
267
+ const candidate = join(dir, 'bin', name);
268
+ if (isExecutable(candidate)) return candidate;
269
+ }
270
+ }
271
+
272
+ // Check $PATH
273
+ for (const bin of ['simulate', 'simulation']) {
274
+ try {
275
+ const cmd = platform === 'win32' ? `where ${bin}` : `which ${bin}`;
276
+ const result = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
277
+ if (result) return result.split('\n')[0];
278
+ } catch {
279
+ // not found
280
+ }
281
+ }
282
+
283
+ return null;
284
+ }
285
+
286
+ function isExecutable(path: string): boolean {
287
+ try {
288
+ const { accessSync, constants } = require('fs');
289
+ accessSync(path, constants.X_OK);
290
+ return true;
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+
296
+ // ─── Extended formatting ────────────────────────────────
297
+
298
+ /** Format a NativeSimulationResult with per-stage and distribution data */
299
+ export function formatNativeResult(result: NativeSimulationResult): string {
300
+ const lines: string[] = [
301
+ '',
302
+ '--- Simulation Results ---',
303
+ `Game: ${result.gameId}`,
304
+ `Iterations: ${result.iterations.toLocaleString()}`,
305
+ `Duration: ${(result.durationMs / 1000).toFixed(1)}s`,
306
+ ];
307
+
308
+ if (result.speed) {
309
+ lines.push(`Speed: ${Math.round(result.speed).toLocaleString()} iterations/sec`);
310
+ }
311
+ if (result.workersUsed) {
312
+ lines.push(`Workers: ${result.workersUsed}`);
313
+ }
314
+
315
+ lines.push(
316
+ '',
317
+ '--- Total ---',
318
+ `Total RTP: ${result.totalRtp.toFixed(2)}%`,
319
+ `Base Game RTP: ${result.baseGameRtp.toFixed(2)}%`,
320
+ `Bonus RTP: ${result.bonusRtp.toFixed(2)}%`,
321
+ `Hit Frequency: ${result.hitFrequency.toFixed(2)}%`,
322
+ `Max Win: ${result.maxWin.toFixed(2)}x`,
323
+ `Max Win Cap Hits: ${result.maxWinHits}`,
324
+ );
325
+
326
+ if (result.bonusTriggered > 0) {
327
+ const frequency = Math.round(result.iterations / result.bonusTriggered);
328
+ lines.push(
329
+ '',
330
+ '--- Bonus Stats ---',
331
+ `Bonus Triggered: ${result.bonusTriggered.toLocaleString()} (1 in ${frequency} spins)`,
332
+ `Bonus Spins Total: ${result.bonusSpinsPlayed.toLocaleString()}`,
333
+ );
334
+ }
335
+
336
+ // Per-stage breakdown
337
+ if (result.perStage && Object.keys(result.perStage).length > 0) {
338
+ lines.push('', '--- Per-Stage Breakdown ---');
339
+ const header = 'Stage | Spins | RTP (contrib) | Per-Spin RTP | Hit Freq | Avg Win | Max Win';
340
+ lines.push(header);
341
+ lines.push('-'.repeat(header.length));
342
+
343
+ for (const [stage, stats] of Object.entries(result.perStage)) {
344
+ lines.push(
345
+ `${stage.padEnd(20)} | ${String(stats.spinCount).padStart(10)} | ` +
346
+ `${stats.rtp.toFixed(2).padStart(12)}% | ` +
347
+ `${stats.perSpinRtp.toFixed(2).padStart(11)}% | ` +
348
+ `${stats.hitFrequency.toFixed(2).padStart(8)}% | ` +
349
+ `${stats.avgWin.toFixed(3).padStart(8)}x | ` +
350
+ `${stats.maxWin.toFixed(2).padStart(8)}x`,
351
+ );
352
+ }
353
+ }
354
+
355
+ // Win distribution
356
+ if (result.winDistribution && result.winDistribution.length > 0) {
357
+ lines.push('', '--- Win Distribution ---');
358
+ for (const bucket of result.winDistribution) {
359
+ const bar = '█'.repeat(Math.round(bucket.pct / 2));
360
+ lines.push(
361
+ `${bucket.label.padEnd(10)} ${String(bucket.count).padStart(10)} (${bucket.pct.toFixed(2).padStart(6)}%) ${bar}`,
362
+ );
363
+ }
364
+ }
365
+
366
+ return lines.join('\n');
367
+ }
package/src/lua/index.ts CHANGED
@@ -5,6 +5,13 @@ export { SessionManager } from './SessionManager';
5
5
  export { PersistentState } from './PersistentState';
6
6
  export { SimulationRunner, formatSimulationResult } from './SimulationRunner';
7
7
  export { ParallelSimulationRunner } from './ParallelSimulationRunner';
8
+ export { NativeSimulationRunner, findNativeBinary, formatNativeResult } from './NativeSimulationRunner';
9
+ export type {
10
+ NativeSimulationConfig,
11
+ NativeSimulationResult,
12
+ StageStats,
13
+ DistributionBucket,
14
+ } from './NativeSimulationRunner';
8
15
  export type {
9
16
  GameDefinition,
10
17
  ActionDefinition,
@@ -1,4 +1,6 @@
1
1
  const RESERVED = new Set(['children', 'key', 'ref']);
2
+ /** Props handled by the reconciler as flex item config, not forwarded to components */
3
+ const FLEX_ITEM_PROPS = new Set(['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude']);
2
4
 
3
5
  // ─── UI Component helpers ────────────────────────────────
4
6
 
@@ -11,7 +13,7 @@ export function extractConfig(props: Record<string, any>): Record<string, any> {
11
13
  const config: Record<string, any> = {};
12
14
 
13
15
  for (const key in props) {
14
- if (RESERVED.has(key) || isEventProp(key)) continue;
16
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS.has(key) || isEventProp(key)) continue;
15
17
 
16
18
  if (key.includes('-')) {
17
19
  const parts = key.split('-');
@@ -41,7 +43,7 @@ export function diffConfig(
41
43
 
42
44
  // New or changed props
43
45
  for (const key in newProps) {
44
- if (RESERVED.has(key) || isEventProp(key)) continue;
46
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS.has(key) || isEventProp(key)) continue;
45
47
  if (newProps[key] !== oldProps[key]) {
46
48
  if (key.includes('-')) {
47
49
  const parts = key.split('-');
@@ -149,7 +151,7 @@ export function applyProps(
149
151
  ): void {
150
152
  // Remove old props not in newProps
151
153
  for (const key in oldProps) {
152
- if (RESERVED.has(key) || key in newProps) continue;
154
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS.has(key) || key in newProps) continue;
153
155
 
154
156
  const pixiEvent = REACT_TO_PIXI_EVENTS[key];
155
157
  if (pixiEvent) {
@@ -169,7 +171,7 @@ export function applyProps(
169
171
 
170
172
  // Apply new props
171
173
  for (const key in newProps) {
172
- if (RESERVED.has(key)) continue;
174
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS.has(key)) continue;
173
175
 
174
176
  const value = newProps[key];
175
177
  const pixiEvent = REACT_TO_PIXI_EVENTS[key];
@@ -17,6 +17,7 @@ import { extend } from './catalogue';
17
17
  import {
18
18
  Button, Label, Panel, FlexContainer, ProgressBar,
19
19
  ScrollContainer, Modal, Toast, BalanceDisplay, WinDisplay, Layout,
20
+ Slider, Toggle,
20
21
  } from '../ui';
21
22
 
22
23
  /**
@@ -60,6 +61,7 @@ export function extendUIElements(): void {
60
61
  extend({
61
62
  Button, Label, Panel, FlexContainer, ProgressBar,
62
63
  ScrollContainer, Modal, Toast, BalanceDisplay, WinDisplay, Layout,
64
+ Slider, Toggle,
63
65
  });
64
66
  }
65
67
 
@@ -14,7 +14,7 @@ import type { ViewInput } from '../ui/view';
14
14
  import type { ButtonConfig, ButtonState } from '../ui/Button';
15
15
  import type { LabelConfig } from '../ui/Label';
16
16
  import type { PanelConfig } from '../ui/Panel';
17
- import type { FlexContainerConfig } from '../ui/FlexContainer';
17
+ import type { FlexContainerConfig, FlexItemConfig, AlignSelf } from '../ui/FlexContainer';
18
18
  import type { ProgressBarConfig } from '../ui/ProgressBar';
19
19
  import type { ScrollContainerConfig } from '../ui/ScrollContainer';
20
20
  import type { ModalConfig } from '../ui/Modal';
@@ -22,6 +22,8 @@ import type { ToastConfig } from '../ui/Toast';
22
22
  import type { BalanceDisplayConfig } from '../ui/BalanceDisplay';
23
23
  import type { WinDisplayConfig } from '../ui/WinDisplay';
24
24
  import type { LayoutConfig } from '../ui/Layout';
25
+ import type { SliderConfig } from '../ui/Slider';
26
+ import type { ToggleConfig } from '../ui/Toggle';
25
27
 
26
28
  // ─── Event props ─────────────────────────────────────────
27
29
 
@@ -80,6 +82,14 @@ interface BaseProps extends PixiEventProps {
80
82
 
81
83
  // Allow scale as number (uniform)
82
84
  scale?: number | { x: number; y: number };
85
+
86
+ // Flex item props (used when child of <flexContainer>)
87
+ flexGrow?: number;
88
+ flexShrink?: number;
89
+ layoutWidth?: number;
90
+ layoutHeight?: number;
91
+ alignSelf?: AlignSelf;
92
+ flexExclude?: boolean;
83
93
  }
84
94
 
85
95
  // ─── PixiJS primitive elements ───────────────────────────
@@ -189,6 +199,21 @@ interface WinDisplayComponentProps extends BaseProps, WinDisplayConfig {}
189
199
 
190
200
  interface LayoutComponentProps extends BaseProps, LayoutConfig {}
191
201
 
202
+ interface SliderComponentProps extends BaseProps, Omit<SliderConfig, 'width' | 'height'> {
203
+ width?: number;
204
+ height?: number;
205
+ onUpdate?: (value: number) => void;
206
+ onChange?: (value: number) => void;
207
+ }
208
+
209
+ interface ToggleComponentProps extends BaseProps, Omit<ToggleConfig, 'width' | 'height'> {
210
+ width?: number;
211
+ height?: number;
212
+ onView?: ViewInput;
213
+ offView?: ViewInput;
214
+ onChange?: (value: boolean) => void;
215
+ }
216
+
192
217
  // ─── JSX IntrinsicElements ───────────────────────────────
193
218
 
194
219
  declare global {
@@ -215,6 +240,8 @@ declare global {
215
240
  balanceDisplay: BalanceDisplayComponentProps;
216
241
  winDisplay: WinDisplayComponentProps;
217
242
  layout: LayoutComponentProps;
243
+ slider: SliderComponentProps;
244
+ toggle: ToggleComponentProps;
218
245
  }
219
246
  }
220
247
  }
@@ -3,6 +3,33 @@ import { DefaultEventPriority } from 'react-reconciler/constants';
3
3
  import { Container } from 'pixi.js';
4
4
  import { catalogue } from './catalogue';
5
5
  import { applyProps, hasEventProps, extractConfig, diffConfig, applyEventProps } from './applyProps';
6
+ import { FlexContainer } from '../ui/FlexContainer';
7
+ import type { FlexItemConfig } from '../ui/FlexContainer';
8
+
9
+ /** Flex item prop names that should be forwarded to _flexConfig on the child */
10
+ const FLEX_ITEM_PROPS = ['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude'] as const;
11
+
12
+ /** Extract FlexItemConfig from props if any flex item props are present */
13
+ function extractFlexItemConfig(props: Record<string, any>): FlexItemConfig | undefined {
14
+ let config: FlexItemConfig | undefined;
15
+ for (const key of FLEX_ITEM_PROPS) {
16
+ if (key in props) {
17
+ if (!config) config = {};
18
+ (config as any)[key] = props[key];
19
+ }
20
+ }
21
+ return config;
22
+ }
23
+
24
+ /** Apply flex item config to a child being added to a FlexContainer */
25
+ function addChildToFlex(parent: FlexContainer, child: Container & { _flexConfig?: FlexItemConfig }): void {
26
+ const flexConfig = child._flexConfig;
27
+ if (flexConfig && Object.keys(flexConfig).length > 0) {
28
+ parent.addFlexChild(child, flexConfig);
29
+ } else {
30
+ parent.addChild(child);
31
+ }
32
+ }
6
33
 
7
34
  function toPascalCase(str: string): string {
8
35
  return str.charAt(0).toUpperCase() + str.slice(1);
@@ -54,6 +81,12 @@ const hostConfig: Reconciler.HostConfig<
54
81
  instance.eventMode = 'static';
55
82
  }
56
83
 
84
+ // Store flex item config for when this child is added to a FlexContainer parent
85
+ const flexItemConfig = extractFlexItemConfig(props);
86
+ if (flexItemConfig) {
87
+ instance._flexConfig = { ...instance._flexConfig, ...flexItemConfig };
88
+ }
89
+
57
90
  return instance;
58
91
  },
59
92
 
@@ -64,11 +97,23 @@ const hostConfig: Reconciler.HostConfig<
64
97
  },
65
98
 
66
99
  appendInitialChild(parent, child) {
67
- if (child instanceof Container) parent.addChild(child);
100
+ if (child instanceof Container) {
101
+ if (parent instanceof FlexContainer) {
102
+ addChildToFlex(parent, child);
103
+ } else {
104
+ parent.addChild(child);
105
+ }
106
+ }
68
107
  },
69
108
 
70
109
  appendChild(parent, child) {
71
- if (child instanceof Container) parent.addChild(child);
110
+ if (child instanceof Container) {
111
+ if (parent instanceof FlexContainer) {
112
+ addChildToFlex(parent, child);
113
+ } else {
114
+ parent.addChild(child);
115
+ }
116
+ }
72
117
  },
73
118
 
74
119
  appendChildToContainer(container, child) {
@@ -116,6 +161,17 @@ const hostConfig: Reconciler.HostConfig<
116
161
  applyProps(instance, newProps, oldProps);
117
162
  }
118
163
 
164
+ // Update flex item config if parent is FlexContainer
165
+ const newFlexConfig = extractFlexItemConfig(newProps);
166
+ const oldFlexConfig = extractFlexItemConfig(oldProps);
167
+ if (newFlexConfig || oldFlexConfig) {
168
+ instance._flexConfig = { ...instance._flexConfig, ...newFlexConfig };
169
+ // Trigger parent relayout
170
+ if (instance.parent instanceof FlexContainer) {
171
+ instance.parent.updateLayout();
172
+ }
173
+ }
174
+
119
175
  if (hasEventProps(newProps) && instance.eventMode === 'auto') {
120
176
  instance.eventMode = 'static';
121
177
  }