@energy8platform/game-engine 0.10.11 → 0.12.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.
Files changed (50) hide show
  1. package/README.md +272 -74
  2. package/dist/index.cjs.js +1322 -296
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.d.ts +369 -46
  5. package/dist/index.esm.js +1323 -298
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/lua.cjs.js +8 -18
  8. package/dist/lua.cjs.js.map +1 -1
  9. package/dist/lua.d.ts +0 -2
  10. package/dist/lua.esm.js +8 -18
  11. package/dist/lua.esm.js.map +1 -1
  12. package/dist/react.cjs.js +2848 -35
  13. package/dist/react.cjs.js.map +1 -1
  14. package/dist/react.d.ts +17 -6
  15. package/dist/react.esm.js +2848 -36
  16. package/dist/react.esm.js.map +1 -1
  17. package/dist/ui.cjs.js +1913 -592
  18. package/dist/ui.cjs.js.map +1 -1
  19. package/dist/ui.d.ts +528 -46
  20. package/dist/ui.esm.js +1911 -594
  21. package/dist/ui.esm.js.map +1 -1
  22. package/dist/vite.cjs.js +1 -11
  23. package/dist/vite.cjs.js.map +1 -1
  24. package/dist/vite.d.ts +1 -1
  25. package/dist/vite.esm.js +1 -11
  26. package/dist/vite.esm.js.map +1 -1
  27. package/package.json +3 -18
  28. package/src/index.ts +3 -3
  29. package/src/lua/LuaEngine.ts +8 -18
  30. package/src/react/applyProps.ts +90 -2
  31. package/src/react/extendAll.ts +29 -6
  32. package/src/react/index.ts +1 -1
  33. package/src/react/jsx.d.ts +249 -0
  34. package/src/react/reconciler.ts +80 -7
  35. package/src/ui/BalanceDisplay.ts +31 -38
  36. package/src/ui/Button.ts +217 -53
  37. package/src/ui/FlexContainer.ts +529 -0
  38. package/src/ui/Label.ts +13 -0
  39. package/src/ui/Layout.ts +86 -87
  40. package/src/ui/Modal.ts +11 -1
  41. package/src/ui/Panel.ts +108 -36
  42. package/src/ui/ProgressBar.ts +85 -31
  43. package/src/ui/ScrollContainer.ts +397 -45
  44. package/src/ui/Slider.ts +241 -0
  45. package/src/ui/Toast.ts +47 -17
  46. package/src/ui/Toggle.ts +201 -0
  47. package/src/ui/WinDisplay.ts +51 -39
  48. package/src/ui/index.ts +9 -11
  49. package/src/ui/view.ts +28 -0
  50. package/src/vite/index.ts +1 -11
@@ -0,0 +1,529 @@
1
+ import { Container } from 'pixi.js';
2
+
3
+ // ─── Types ───────────────────────────────────────────────
4
+
5
+ export type FlexDirection = 'row' | 'column';
6
+ export type JustifyContent = 'start' | 'center' | 'end' | 'space-between' | 'space-around';
7
+ export type AlignItems = 'start' | 'center' | 'end' | 'stretch';
8
+
9
+ export type AlignSelf = 'auto' | 'start' | 'center' | 'end' | 'stretch';
10
+
11
+ export interface FlexItemConfig {
12
+ /** Flex grow factor (0 = fixed size) */
13
+ flexGrow?: number;
14
+ /** Flex shrink factor (0 = don't shrink, default: 1) */
15
+ flexShrink?: number;
16
+ /** Explicit width override for layout calculations */
17
+ layoutWidth?: number;
18
+ /** Explicit height override for layout calculations */
19
+ layoutHeight?: number;
20
+ /** Override parent's alignItems for this child */
21
+ alignSelf?: AlignSelf;
22
+ /** Exclude from flex layout (acts like position: absolute) */
23
+ flexExclude?: boolean;
24
+ }
25
+
26
+ export interface FlexContainerConfig {
27
+ /** Layout direction (default: 'row') */
28
+ direction?: FlexDirection;
29
+ /** Main-axis distribution (default: 'start') */
30
+ justifyContent?: JustifyContent;
31
+ /** Cross-axis alignment (default: 'start') */
32
+ alignItems?: AlignItems;
33
+ /** Gap between children in pixels (default: 0) */
34
+ gap?: number;
35
+ /** Padding [top, right, bottom, left] or single number (default: 0) */
36
+ padding?: number | [number, number, number, number];
37
+ /** Enable wrapping to next line (default: false) */
38
+ flexWrap?: boolean;
39
+ /** Maximum width before wrapping (only with flexWrap) */
40
+ maxWidth?: number;
41
+ /** Maximum height before wrapping (only with flexWrap + column) */
42
+ maxHeight?: number;
43
+ /** Explicit container width (used for cross-axis alignment/stretch) */
44
+ width?: number;
45
+ /** Explicit container height (used for cross-axis alignment/stretch) */
46
+ height?: number;
47
+ }
48
+
49
+ // ─── Helpers ─────────────────────────────────────────────
50
+
51
+ function normalizePadding(p: number | [number, number, number, number]): [number, number, number, number] {
52
+ return typeof p === 'number' ? [p, p, p, p] : p;
53
+ }
54
+
55
+ /** Measure a child's size and bounds offset for layout purposes */
56
+ function measureChild(child: Container & { _flexConfig?: FlexItemConfig }): { w: number; h: number; ox: number; oy: number } {
57
+ const cfg = child._flexConfig;
58
+ if (cfg?.layoutWidth !== undefined && cfg?.layoutHeight !== undefined) {
59
+ return { w: cfg.layoutWidth, h: cfg.layoutHeight, ox: 0, oy: 0 };
60
+ }
61
+
62
+ // For FlexContainers, use their explicit size if set
63
+ if (child instanceof FlexContainer) {
64
+ const fc = child;
65
+ if (fc._explicitWidth > 0 && fc._explicitHeight > 0) {
66
+ return { w: fc._explicitWidth, h: fc._explicitHeight, ox: 0, oy: 0 };
67
+ }
68
+ }
69
+
70
+ // Use localBounds to get the true visual extent and origin offset.
71
+ // This handles children with non-zero anchors (e.g. Button, Label with centered text).
72
+ const bounds = child.getLocalBounds();
73
+ const w = cfg?.layoutWidth ?? bounds.width;
74
+ const h = cfg?.layoutHeight ?? bounds.height;
75
+ return { w, h, ox: bounds.x, oy: bounds.y };
76
+ }
77
+
78
+ // ─── Layout items within a single line ───────────────────
79
+
80
+ interface LineItem {
81
+ child: Container & { _flexConfig?: FlexItemConfig };
82
+ w: number;
83
+ h: number;
84
+ /** Local bounds origin offset (x) — compensates for centered anchors */
85
+ ox: number;
86
+ /** Local bounds origin offset (y) */
87
+ oy: number;
88
+ }
89
+
90
+ function layoutLine(
91
+ items: LineItem[],
92
+ isRow: boolean,
93
+ mainSize: number,
94
+ justify: JustifyContent,
95
+ align: AlignItems,
96
+ gap: number,
97
+ crossOffset: number,
98
+ crossSize: number,
99
+ ): void {
100
+ if (items.length === 0) return;
101
+
102
+ // Compute total fixed main size and flex grow total
103
+ let totalFixed = 0;
104
+ let totalGrow = 0;
105
+ for (const item of items) {
106
+ const grow = item.child._flexConfig?.flexGrow ?? 0;
107
+ if (grow > 0) {
108
+ totalGrow += grow;
109
+ } else {
110
+ totalFixed += isRow ? item.w : item.h;
111
+ }
112
+ }
113
+
114
+ const totalGap = gap * (items.length - 1);
115
+ const availableForFlex = Math.max(0, mainSize - totalFixed - totalGap);
116
+
117
+ // Resolve flex sizes
118
+ if (totalGrow > 0) {
119
+ for (const item of items) {
120
+ const grow = item.child._flexConfig?.flexGrow ?? 0;
121
+ if (grow > 0) {
122
+ const flexSize = (grow / totalGrow) * availableForFlex;
123
+ if (isRow) {
124
+ item.w = flexSize;
125
+ item.child.width = flexSize;
126
+ } else {
127
+ item.h = flexSize;
128
+ item.child.height = flexSize;
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ // Shrink: if content overflows and mainSize is finite, shrink eligible items
135
+ if (totalGrow === 0 && mainSize > 0) {
136
+ const overflow = totalFixed + totalGap - mainSize;
137
+ if (overflow > 0) {
138
+ let totalShrinkable = 0;
139
+ for (const item of items) {
140
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
141
+ if (shrink > 0) {
142
+ totalShrinkable += isRow ? item.w : item.h;
143
+ }
144
+ }
145
+ if (totalShrinkable > 0) {
146
+ for (const item of items) {
147
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
148
+ if (shrink > 0) {
149
+ const itemMain = isRow ? item.w : item.h;
150
+ const reduction = overflow * (itemMain / totalShrinkable);
151
+ const newSize = Math.max(0, itemMain - reduction);
152
+ if (isRow) {
153
+ item.w = newSize;
154
+ item.child.width = newSize;
155
+ } else {
156
+ item.h = newSize;
157
+ item.child.height = newSize;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ // Calculate total main size after flex
166
+ let totalMain = totalGap;
167
+ for (const item of items) {
168
+ totalMain += isRow ? item.w : item.h;
169
+ }
170
+
171
+ // Justify: compute starting offset and extra spacing
172
+ let mainOffset = 0;
173
+ let extraGap = 0;
174
+
175
+ switch (justify) {
176
+ case 'start':
177
+ break;
178
+ case 'center':
179
+ mainOffset = Math.max(0, (mainSize - totalMain) / 2);
180
+ break;
181
+ case 'end':
182
+ mainOffset = Math.max(0, mainSize - totalMain);
183
+ break;
184
+ case 'space-between':
185
+ if (items.length > 1) {
186
+ extraGap = Math.max(0, (mainSize - totalMain + totalGap) / (items.length - 1)) - gap;
187
+ }
188
+ break;
189
+ case 'space-around':
190
+ if (items.length > 0) {
191
+ const totalSpace = Math.max(0, mainSize - totalMain + totalGap);
192
+ const segment = totalSpace / items.length;
193
+ mainOffset = segment / 2;
194
+ extraGap = segment - gap;
195
+ }
196
+ break;
197
+ }
198
+
199
+ // Position each item
200
+ let pos = mainOffset;
201
+ for (const item of items) {
202
+ const mainDim = isRow ? item.w : item.h;
203
+ const crossDim = isRow ? item.h : item.w;
204
+
205
+ // Cross-axis alignment (alignSelf overrides align)
206
+ const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
207
+ ? item.child._flexConfig.alignSelf
208
+ : align;
209
+ let crossPos = crossOffset;
210
+ switch (effectiveAlign) {
211
+ case 'start':
212
+ break;
213
+ case 'center':
214
+ crossPos += (crossSize - crossDim) / 2;
215
+ break;
216
+ case 'end':
217
+ crossPos += crossSize - crossDim;
218
+ break;
219
+ case 'stretch':
220
+ if (isRow) {
221
+ item.child.height = crossSize;
222
+ } else {
223
+ item.child.width = crossSize;
224
+ }
225
+ break;
226
+ }
227
+
228
+ // Compensate for local bounds offset (e.g. centered anchors)
229
+ if (isRow) {
230
+ item.child.x = pos - item.ox;
231
+ item.child.y = crossPos - item.oy;
232
+ } else {
233
+ item.child.x = crossPos - item.ox;
234
+ item.child.y = pos - item.oy;
235
+ }
236
+
237
+ pos += mainDim + gap + extraGap;
238
+ }
239
+ }
240
+
241
+ // ─── FlexContainer ───────────────────────────────────────
242
+
243
+ /**
244
+ * Lightweight flexbox-like layout container for PixiJS.
245
+ *
246
+ * Supports row/column direction, justify/align, gap, padding, wrapping,
247
+ * and flex-grow distribution. Zero external dependencies.
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * const toolbar = new FlexContainer({
252
+ * direction: 'row',
253
+ * justifyContent: 'space-between',
254
+ * alignItems: 'center',
255
+ * gap: 16,
256
+ * padding: 12,
257
+ * });
258
+ *
259
+ * toolbar.addFlexChild(button1);
260
+ * toolbar.addFlexChild(button2);
261
+ * toolbar.resize(800, 60);
262
+ * ```
263
+ */
264
+ export class FlexContainer extends Container {
265
+ readonly __uiComponent = true as const;
266
+
267
+ private _config: Required<Pick<FlexContainerConfig, 'direction' | 'justifyContent' | 'alignItems' | 'gap' | 'flexWrap'>>;
268
+ private _padding: [number, number, number, number];
269
+ private _maxWidth: number;
270
+ private _maxHeight: number;
271
+ /** @internal */ _explicitWidth: number;
272
+ /** @internal */ _explicitHeight: number;
273
+ private _layoutChildren: (Container & { _flexConfig?: FlexItemConfig })[] = [];
274
+ private _layoutDirty = true;
275
+
276
+ constructor(config: FlexContainerConfig = {}) {
277
+ super();
278
+
279
+ this._config = {
280
+ direction: config.direction ?? 'row',
281
+ justifyContent: config.justifyContent ?? 'start',
282
+ alignItems: config.alignItems ?? 'start',
283
+ gap: config.gap ?? 0,
284
+ flexWrap: config.flexWrap ?? false,
285
+ };
286
+
287
+ this._padding = normalizePadding(config.padding ?? 0);
288
+ this._maxWidth = config.maxWidth ?? Infinity;
289
+ this._maxHeight = config.maxHeight ?? Infinity;
290
+ this._explicitWidth = config.width ?? 0;
291
+ this._explicitHeight = config.height ?? 0;
292
+ }
293
+
294
+ // ─── Public API ──────────────────────────────────────
295
+
296
+ /** Add a child with optional flex config. Also registers in flex layout. */
297
+ addFlexChild(child: Container, flexConfig?: FlexItemConfig): this {
298
+ if (flexConfig) (child as any)._flexConfig = flexConfig;
299
+ if (!this._layoutChildren.includes(child as any)) {
300
+ this._layoutChildren.push(child as any);
301
+ this._layoutDirty = true;
302
+ }
303
+ super.addChild(child);
304
+ return this;
305
+ }
306
+
307
+ /** Remove a child from flex layout and display list */
308
+ removeFlexChild(child: Container): this {
309
+ const idx = this._layoutChildren.indexOf(child as any);
310
+ if (idx !== -1) {
311
+ this._layoutChildren.splice(idx, 1);
312
+ this._layoutDirty = true;
313
+ }
314
+ super.removeChild(child);
315
+ return this;
316
+ }
317
+
318
+ /** Remove all flex children */
319
+ clearFlexChildren(): this {
320
+ for (const child of this._layoutChildren) {
321
+ super.removeChild(child);
322
+ }
323
+ this._layoutChildren.length = 0;
324
+ this._layoutDirty = true;
325
+ return this;
326
+ }
327
+
328
+ /**
329
+ * Override addChild so children automatically participate in flex layout.
330
+ * This enables declarative usage from React JSX.
331
+ */
332
+ override addChild<T extends Container>(...children: T[]): T {
333
+ for (const child of children) {
334
+ if (!this._layoutChildren.includes(child as any)) {
335
+ this._layoutChildren.push(child as any);
336
+ this._layoutDirty = true;
337
+ }
338
+ }
339
+ const result = super.addChild(...children);
340
+ if (this._layoutDirty) this.updateLayout();
341
+ return result;
342
+ }
343
+
344
+ override removeChild<T extends Container>(...children: T[]): T {
345
+ for (const child of children) {
346
+ const idx = this._layoutChildren.indexOf(child as any);
347
+ if (idx !== -1) {
348
+ this._layoutChildren.splice(idx, 1);
349
+ this._layoutDirty = true;
350
+ }
351
+ }
352
+ return super.removeChild(...children);
353
+ }
354
+
355
+ /** Get all flex layout children (read-only) */
356
+ get flexChildren(): readonly Container[] {
357
+ return this._layoutChildren;
358
+ }
359
+
360
+ /** Update the container size and recalculate layout */
361
+ resize(width: number, height: number): void {
362
+ this._explicitWidth = width;
363
+ this._explicitHeight = height;
364
+ this._layoutDirty = true;
365
+ this.updateLayout();
366
+ }
367
+
368
+ /** Update layout direction */
369
+ setDirection(direction: FlexDirection): void {
370
+ this._config.direction = direction;
371
+ this._layoutDirty = true;
372
+ }
373
+
374
+ /** Update justifyContent */
375
+ setJustifyContent(justify: JustifyContent): void {
376
+ this._config.justifyContent = justify;
377
+ this._layoutDirty = true;
378
+ }
379
+
380
+ /** Update alignItems */
381
+ setAlignItems(align: AlignItems): void {
382
+ this._config.alignItems = align;
383
+ this._layoutDirty = true;
384
+ }
385
+
386
+ /** Update gap */
387
+ setGap(gap: number): void {
388
+ this._config.gap = gap;
389
+ this._layoutDirty = true;
390
+ }
391
+
392
+ /** Update padding */
393
+ setPadding(padding: number | [number, number, number, number]): void {
394
+ this._padding = normalizePadding(padding);
395
+ this._layoutDirty = true;
396
+ }
397
+
398
+ /**
399
+ * Recalculate and apply layout positions for all children.
400
+ * Called automatically by `resize()`. Call manually after
401
+ * adding/removing children without resize.
402
+ */
403
+ updateLayout(): void {
404
+ this._layoutDirty = false;
405
+ const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
406
+ const [pt, pr, pb, pl] = this._padding;
407
+ const isRow = direction === 'row';
408
+
409
+ const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
410
+ const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
411
+ const mainLimit = isRow ? contentW : contentH;
412
+ const crossLimit = isRow ? contentH : contentW;
413
+
414
+ // Measure children (skip flexExclude — they position themselves)
415
+ const measured: LineItem[] = [];
416
+ for (const child of this._layoutChildren) {
417
+ if (child._flexConfig?.flexExclude) continue;
418
+ const { w, h, ox, oy } = measureChild(child);
419
+ measured.push({ child, w, h, ox, oy });
420
+ }
421
+
422
+ // Split into lines (if wrapping)
423
+ const lines: LineItem[][] = [];
424
+ if (flexWrap && mainLimit < Infinity) {
425
+ let currentLine: LineItem[] = [];
426
+ let lineMain = 0;
427
+
428
+ for (const item of measured) {
429
+ const itemMain = isRow ? item.w : item.h;
430
+ const wouldBe = lineMain + (currentLine.length > 0 ? gap : 0) + itemMain;
431
+
432
+ if (currentLine.length > 0 && wouldBe > mainLimit) {
433
+ lines.push(currentLine);
434
+ currentLine = [item];
435
+ lineMain = itemMain;
436
+ } else {
437
+ currentLine.push(item);
438
+ lineMain = wouldBe;
439
+ }
440
+ }
441
+ if (currentLine.length > 0) lines.push(currentLine);
442
+ } else {
443
+ lines.push(measured);
444
+ }
445
+
446
+ // Compute cross size per line
447
+ const lineCrossSizes: number[] = lines.map((line) => {
448
+ let maxCross = 0;
449
+ for (const item of line) {
450
+ const cross = isRow ? item.h : item.w;
451
+ if (cross > maxCross) maxCross = cross;
452
+ }
453
+ return maxCross;
454
+ });
455
+
456
+ // Layout each line
457
+ let crossOffset = isRow ? pt : pl;
458
+ for (let i = 0; i < lines.length; i++) {
459
+ const line = lines[i];
460
+ const lineCross = lineCrossSizes[i];
461
+ const mainStart = isRow ? pl : pt;
462
+
463
+ // Offset items by padding
464
+ const tempItems = line.map((item) => ({ ...item }));
465
+
466
+ // For single-line layouts, use the full available cross space for alignment;
467
+ // for multi-line (wrapping), each line gets its own measured cross size.
468
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity
469
+ ? crossLimit
470
+ : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
471
+
472
+ layoutLine(
473
+ tempItems,
474
+ isRow,
475
+ mainLimit < Infinity ? mainLimit : 0,
476
+ mainLimit < Infinity ? justifyContent : 'start',
477
+ alignItems,
478
+ gap,
479
+ crossOffset,
480
+ effectiveCross,
481
+ );
482
+
483
+ // Apply main-axis padding offset
484
+ for (const item of tempItems) {
485
+ const origChild = line.find((l) => l.child === item.child)!;
486
+ origChild.child.x = item.child.x + (isRow ? mainStart : 0);
487
+ origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
488
+ }
489
+
490
+ crossOffset += lineCross + gap;
491
+ }
492
+ }
493
+
494
+ /** Computed content size (after layout) */
495
+ getContentSize(): { width: number; height: number } {
496
+ if (this._layoutDirty) this.updateLayout();
497
+
498
+ let maxX = 0;
499
+ let maxY = 0;
500
+ for (const child of this._layoutChildren) {
501
+ const { w, h } = measureChild(child);
502
+ maxX = Math.max(maxX, child.x + w);
503
+ maxY = Math.max(maxY, child.y + h);
504
+ }
505
+
506
+ const [, pr, pb] = this._padding;
507
+ return { width: maxX + pr, height: maxY + pb };
508
+ }
509
+
510
+ /** React reconciler update hook — applies changed config props */
511
+ updateConfig(changed: Record<string, any>): void {
512
+ if ('direction' in changed) this.setDirection(changed.direction);
513
+ if ('justifyContent' in changed) this.setJustifyContent(changed.justifyContent);
514
+ if ('alignItems' in changed) this.setAlignItems(changed.alignItems);
515
+ if ('gap' in changed) this.setGap(changed.gap);
516
+ if ('padding' in changed) this.setPadding(changed.padding);
517
+ if ('flexWrap' in changed) { this._config.flexWrap = changed.flexWrap; this._layoutDirty = true; }
518
+ if ('width' in changed || 'height' in changed) {
519
+ this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
520
+ return; // resize calls updateLayout
521
+ }
522
+ if (this._layoutDirty) this.updateLayout();
523
+ }
524
+
525
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
526
+ this._layoutChildren.length = 0;
527
+ super.destroy(options);
528
+ }
529
+ }
package/src/ui/Label.ts CHANGED
@@ -23,6 +23,8 @@ export interface LabelConfig {
23
23
  * ```
24
24
  */
25
25
  export class Label extends Container {
26
+ readonly __uiComponent = true as const;
27
+
26
28
  private _text: Text;
27
29
  private _maxWidth: number;
28
30
  private _autoFit: boolean;
@@ -99,6 +101,17 @@ export class Label extends Container {
99
101
  }).format(value);
100
102
  }
101
103
 
104
+ /** React reconciler update hook */
105
+ updateConfig(changed: Record<string, any>): void {
106
+ if ('text' in changed) this.text = changed.text;
107
+ if ('maxWidth' in changed) this.maxWidth = changed.maxWidth;
108
+ if ('autoFit' in changed) { this._autoFit = changed.autoFit; this.fitText(); }
109
+ if ('style' in changed && typeof changed.style === 'object') {
110
+ Object.assign(this._text.style, changed.style);
111
+ this.fitText();
112
+ }
113
+ }
114
+
102
115
  private fitText(): void {
103
116
  if (!this._autoFit || this._maxWidth === Infinity) return;
104
117