@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 +88 -1
- package/bin/simulate.ts +45 -5
- package/dist/index.cjs.js +49 -7
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.esm.js +49 -7
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +237 -0
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +59 -2
- package/dist/lua.esm.js +236 -2
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +667 -215
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.esm.js +667 -215
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +407 -7
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +156 -2
- package/dist/ui.esm.js +406 -8
- package/dist/ui.esm.js.map +1 -1
- package/package.json +4 -2
- package/scripts/install-simulate.mjs +100 -0
- package/src/lua/NativeSimulationRunner.ts +367 -0
- package/src/lua/index.ts +7 -0
- package/src/react/applyProps.ts +6 -4
- package/src/react/extendAll.ts +2 -0
- package/src/react/jsx.d.ts +28 -1
- package/src/react/reconciler.ts +58 -2
- package/src/ui/FlexContainer.ts +57 -7
- package/src/ui/Slider.ts +241 -0
- package/src/ui/Toggle.ts +201 -0
- package/src/ui/index.ts +5 -1
|
@@ -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,
|
package/src/react/applyProps.ts
CHANGED
|
@@ -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];
|
package/src/react/extendAll.ts
CHANGED
|
@@ -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
|
|
package/src/react/jsx.d.ts
CHANGED
|
@@ -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
|
}
|
package/src/react/reconciler.ts
CHANGED
|
@@ -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)
|
|
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)
|
|
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
|
}
|