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