@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
@@ -1,6 +1,6 @@
1
- import { Container, type ColorSource } from 'pixi.js';
2
- import { ScrollBox as PixiScrollBox } from '@pixi/ui';
3
- import type { ScrollBoxOptions } from '@pixi/ui';
1
+ import { Container, Graphics, type ColorSource, Ticker } from 'pixi.js';
2
+ import { resolveView } from './view';
3
+ import type { ViewInput } from './view';
4
4
 
5
5
  // ─── Types ───────────────────────────────────────────────
6
6
 
@@ -28,27 +28,33 @@ export interface ScrollContainerConfig {
28
28
  /** Padding */
29
29
  padding?: number;
30
30
 
31
- /** Disable dynamic rendering (render all items even when offscreen) */
32
- disableDynamicRendering?: boolean;
33
-
34
31
  /** Disable easing/inertia */
35
32
  disableEasing?: boolean;
36
33
 
37
- /** Global scroll scroll even when mouse is not over the component */
38
- globalScroll?: boolean;
34
+ /** Show scrollbar indicator (default: false) */
35
+ scrollbar?: boolean;
36
+
37
+ /** Custom scrollbar thumb view (string texture name, Texture, or Container) */
38
+ thumbView?: ViewInput;
39
+
40
+ /** Scrollbar width in px (default: 6) */
41
+ scrollbarWidth?: number;
42
+
43
+ /** Scrollbar padding from edge (default: 4) */
44
+ scrollbarPadding?: number;
45
+
46
+ /** Scrollbar color (when no thumbView, default: 0xaaaaaa) */
47
+ scrollbarColor?: number;
48
+
49
+ /** Scrollbar alpha (default: 0.5) */
50
+ scrollbarAlpha?: number;
39
51
  }
40
52
 
41
- const DIRECTION_MAP: Record<ScrollDirection, 'vertical' | 'horizontal' | 'bidirectional'> = {
42
- vertical: 'vertical',
43
- horizontal: 'horizontal',
44
- both: 'bidirectional',
45
- };
53
+ const DECELERATION = 0.95;
54
+ const MIN_VELOCITY = 0.5;
46
55
 
47
56
  /**
48
- * Scrollable container powered by `@pixi/ui` ScrollBox.
49
- *
50
- * Provides touch/drag scrolling, mouse wheel support, inertia, and
51
- * dynamic rendering optimization for off-screen items.
57
+ * Scrollable container with touch/drag, mouse wheel, and inertia.
52
58
  *
53
59
  * @example
54
60
  * ```ts
@@ -66,61 +72,407 @@ const DIRECTION_MAP: Record<ScrollDirection, 'vertical' | 'horizontal' | 'bidire
66
72
  * scene.container.addChild(scroll);
67
73
  * ```
68
74
  */
69
- export class ScrollContainer extends PixiScrollBox {
70
- private _scrollConfig: ScrollContainerConfig;
75
+ export class ScrollContainer extends Container {
76
+ readonly __uiComponent = true as const;
77
+
78
+ private _viewport: { width: number; height: number };
79
+ private _internalSetup = true;
80
+ private _content: Container;
81
+ private _maskGfx: Graphics;
82
+ private _bg: Graphics | null = null;
83
+ private _scrollConfig: Required<Pick<ScrollContainerConfig, 'direction' | 'elementsMargin' | 'padding' | 'borderRadius' | 'disableEasing'>>;
84
+ private _items: Container[] = [];
85
+
86
+ // Scrollbar
87
+ private _scrollbar: Container | null = null;
88
+ private _scrollbarConfig: { width: number; padding: number };
89
+
90
+ // Drag state
91
+ private _dragging = false;
92
+ private _dragStart = { x: 0, y: 0 };
93
+ private _contentStart = { x: 0, y: 0 };
94
+ private _velocity = { x: 0, y: 0 };
95
+ private _lastDragPos = { x: 0, y: 0 };
96
+ private _lastDragTime = 0;
97
+ private _inertiaActive = false;
98
+
99
+ // Bound handlers for cleanup
100
+ private _onTickBound: ((ticker: Ticker) => void) | null = null;
101
+ private _onWheelBound: ((e: WheelEvent) => void) | null = null;
71
102
 
72
103
  constructor(config: ScrollContainerConfig) {
73
- const options: ScrollBoxOptions = {
74
- width: config.width,
75
- height: config.height,
76
- type: DIRECTION_MAP[config.direction ?? 'vertical'],
77
- radius: config.borderRadius ?? 0,
104
+ super();
105
+
106
+ this._viewport = { width: config.width, height: config.height };
107
+ this._scrollConfig = {
108
+ direction: config.direction ?? 'vertical',
78
109
  elementsMargin: config.elementsMargin ?? 0,
79
110
  padding: config.padding ?? 0,
80
- disableDynamicRendering: config.disableDynamicRendering ?? false,
111
+ borderRadius: config.borderRadius ?? 0,
81
112
  disableEasing: config.disableEasing ?? false,
82
- globalScroll: config.globalScroll ?? true,
83
113
  };
84
114
 
115
+ // Background
85
116
  if (config.backgroundColor !== undefined) {
86
- options.background = config.backgroundColor;
117
+ this._bg = new Graphics();
118
+ this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
119
+ .fill(config.backgroundColor);
120
+ this.addChild(this._bg);
121
+ }
122
+
123
+ // Mask
124
+ this._maskGfx = new Graphics();
125
+ this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
126
+ .fill(0xffffff);
127
+ this.addChild(this._maskGfx);
128
+
129
+ // Content container
130
+ this._content = new Container();
131
+ this._content.mask = this._maskGfx;
132
+ this.addChild(this._content);
133
+
134
+ // Interaction
135
+ this.eventMode = 'static';
136
+ this.hitArea = { contains: (x: number, y: number) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
137
+
138
+ this.on('pointerdown', this._onPointerDown, this);
139
+ this.on('pointermove', this._onPointerMove, this);
140
+ this.on('pointerup', this._onPointerUp, this);
141
+ this.on('pointerupoutside', this._onPointerUp, this);
142
+
143
+ // Mouse wheel
144
+ this._onWheelBound = this._onWheel.bind(this);
145
+
146
+ // Scrollbar
147
+ const sbWidth = config.scrollbarWidth ?? 6;
148
+ const sbPadding = config.scrollbarPadding ?? 4;
149
+ this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
150
+
151
+ if (config.scrollbar) {
152
+ const customThumb = resolveView(config.thumbView);
153
+ if (customThumb) {
154
+ this._scrollbar = customThumb;
155
+ } else {
156
+ const g = new Graphics();
157
+ g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
158
+ g.alpha = config.scrollbarAlpha ?? 0.5;
159
+ this._scrollbar = g;
160
+ }
161
+ this._scrollbar.visible = false;
162
+ super.addChild(this._scrollbar);
87
163
  }
88
164
 
89
- super(options);
165
+ this._internalSetup = false;
166
+ }
90
167
 
91
- this._scrollConfig = config;
168
+ /**
169
+ * Override addChild so external children are routed to scroll content.
170
+ * Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
171
+ */
172
+ override addChild<T extends Container>(...children: T[]): T {
173
+ if (this._internalSetup) {
174
+ return super.addChild(...children);
175
+ }
176
+ for (const child of children) {
177
+ this.addItem(child as any);
178
+ }
179
+ return children[0];
92
180
  }
93
181
 
94
- /** Set scrollable content. Replaces any existing content. */
95
- setContent(content: Container): void {
96
- // Remove existing items
97
- const existing = this.items;
98
- if (existing.length > 0) {
99
- for (let i = existing.length - 1; i >= 0; i--) {
100
- this.removeItem(i);
182
+ override removeChild<T extends Container>(...children: T[]): T {
183
+ if (this._internalSetup) {
184
+ return super.removeChild(...children);
185
+ }
186
+ for (const child of children) {
187
+ const idx = this._items.indexOf(child as any);
188
+ if (idx !== -1) {
189
+ this._items.splice(idx, 1);
190
+ this._content.removeChild(child);
101
191
  }
102
192
  }
193
+ this.layoutItems();
194
+ return children[0];
195
+ }
103
196
 
104
- // Add all children from the content container
197
+ /** React reconciler update hook */
198
+ updateConfig(changed: Record<string, any>): void {
199
+ if ('width' in changed || 'height' in changed) {
200
+ this.setViewportSize(
201
+ changed.width ?? this._viewport.width,
202
+ changed.height ?? this._viewport.height,
203
+ );
204
+ }
205
+ }
206
+
207
+ /** Enable mouse wheel scrolling (call after adding to stage) */
208
+ enableWheel(canvas: HTMLCanvasElement): void {
209
+ if (this._onWheelBound) {
210
+ canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
211
+ }
212
+ }
213
+
214
+ /** Set scrollable content. Replaces any existing items. */
215
+ setContent(content: Container): void {
216
+ this.clearItems();
105
217
  const children = [...content.children] as Container[];
106
- if (children.length > 0) {
107
- this.addItems(children);
218
+ for (const child of children) {
219
+ this.addItem(child);
108
220
  }
109
221
  }
110
222
 
111
223
  /** Add a single item */
112
- addItem(...items: Container[]): Container {
113
- this.addItems(items);
114
- return items[0];
224
+ addItem(child: Container): this {
225
+ this._items.push(child);
226
+ this._content.addChild(child);
227
+ this.layoutItems();
228
+ return this;
229
+ }
230
+
231
+ /** Remove all items */
232
+ clearItems(): void {
233
+ for (const item of this._items) {
234
+ this._content.removeChild(item);
235
+ }
236
+ this._items.length = 0;
237
+ }
238
+
239
+ /** Get items */
240
+ get items(): readonly Container[] {
241
+ return this._items;
115
242
  }
116
243
 
117
- /** Scroll to make a specific item/child visible */
244
+ /** Scroll to make a specific item index visible */
118
245
  scrollToItem(index: number): void {
119
- this.scrollTo(index);
246
+ if (index < 0 || index >= this._items.length) return;
247
+ const item = this._items[index];
248
+ const isVert = this._scrollConfig.direction !== 'horizontal';
249
+
250
+ if (isVert) {
251
+ this._content.y = -item.y + this._scrollConfig.padding;
252
+ } else {
253
+ this._content.x = -item.x + this._scrollConfig.padding;
254
+ }
255
+ this.clampScroll();
120
256
  }
121
257
 
122
258
  /** Current scroll position */
123
259
  get scrollPosition(): { x: number; y: number } {
124
- return { x: this.scrollX, y: this.scrollY };
260
+ return { x: this._content.x, y: this._content.y };
261
+ }
262
+
263
+ /** Resize the scroll viewport */
264
+ setViewportSize(width: number, height: number): void {
265
+ this._viewport.width = width;
266
+ this._viewport.height = height;
267
+
268
+ this._maskGfx.clear();
269
+ this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
270
+
271
+ if (this._bg) {
272
+ this._bg.clear();
273
+ this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
274
+ .fill(0xffffff); // color will be overridden if needed
275
+ }
276
+
277
+ this.clampScroll();
278
+ }
279
+
280
+ // ─── Layout ──────────────────────────────────────────
281
+
282
+ private layoutItems(): void {
283
+ const { direction, elementsMargin, padding } = this._scrollConfig;
284
+ const isVert = direction !== 'horizontal';
285
+ let pos = padding;
286
+
287
+ for (const item of this._items) {
288
+ if (isVert) {
289
+ item.x = padding;
290
+ item.y = pos;
291
+ pos += item.height + elementsMargin;
292
+ } else {
293
+ item.x = pos;
294
+ item.y = padding;
295
+ pos += item.width + elementsMargin;
296
+ }
297
+ }
298
+ }
299
+
300
+ // ─── Drag handling ───────────────────────────────────
301
+
302
+ private _onPointerDown(e: import('pixi.js').FederatedPointerEvent): void {
303
+ this._dragging = true;
304
+ this._inertiaActive = false;
305
+ this._dragStart.x = e.globalX;
306
+ this._dragStart.y = e.globalY;
307
+ this._contentStart.x = this._content.x;
308
+ this._contentStart.y = this._content.y;
309
+ this._lastDragPos.x = e.globalX;
310
+ this._lastDragPos.y = e.globalY;
311
+ this._lastDragTime = Date.now();
312
+ this._velocity.x = 0;
313
+ this._velocity.y = 0;
314
+
315
+ this.stopInertia();
316
+ }
317
+
318
+ private _onPointerMove(e: import('pixi.js').FederatedPointerEvent): void {
319
+ if (!this._dragging) return;
320
+
321
+ const dx = e.globalX - this._dragStart.x;
322
+ const dy = e.globalY - this._dragStart.y;
323
+ const { direction } = this._scrollConfig;
324
+
325
+ if (direction !== 'horizontal') {
326
+ this._content.y = this._contentStart.y + dy;
327
+ }
328
+ if (direction !== 'vertical') {
329
+ this._content.x = this._contentStart.x + dx;
330
+ }
331
+
332
+ // Track velocity
333
+ const now = Date.now();
334
+ const dt = now - this._lastDragTime;
335
+ if (dt > 0) {
336
+ this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
337
+ this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
338
+ }
339
+ this._lastDragPos.x = e.globalX;
340
+ this._lastDragPos.y = e.globalY;
341
+ this._lastDragTime = now;
342
+
343
+ this.clampScroll();
344
+ }
345
+
346
+ private _onPointerUp(): void {
347
+ if (!this._dragging) return;
348
+ this._dragging = false;
349
+
350
+ if (!this._scrollConfig.disableEasing &&
351
+ (Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
352
+ this.startInertia();
353
+ }
354
+ }
355
+
356
+ // ─── Inertia ─────────────────────────────────────────
357
+
358
+ private startInertia(): void {
359
+ this._inertiaActive = true;
360
+ this._onTickBound = this._inertiaTick.bind(this);
361
+ Ticker.shared.add(this._onTickBound);
362
+ }
363
+
364
+ private stopInertia(): void {
365
+ if (this._onTickBound && this._inertiaActive) {
366
+ Ticker.shared.remove(this._onTickBound);
367
+ this._inertiaActive = false;
368
+ }
369
+ }
370
+
371
+ private _inertiaTick(): void {
372
+ const { direction } = this._scrollConfig;
373
+
374
+ if (direction !== 'horizontal') {
375
+ this._content.y += this._velocity.y;
376
+ this._velocity.y *= DECELERATION;
377
+ }
378
+ if (direction !== 'vertical') {
379
+ this._content.x += this._velocity.x;
380
+ this._velocity.x *= DECELERATION;
381
+ }
382
+
383
+ this.clampScroll();
384
+
385
+ if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
386
+ this.stopInertia();
387
+ }
388
+ }
389
+
390
+ // ─── Mouse wheel ─────────────────────────────────────
391
+
392
+ private _onWheel(e: WheelEvent): void {
393
+ const { direction } = this._scrollConfig;
394
+ e.preventDefault();
395
+
396
+ if (direction !== 'horizontal') {
397
+ this._content.y -= e.deltaY;
398
+ }
399
+ if (direction !== 'vertical') {
400
+ this._content.x -= e.deltaX;
401
+ }
402
+
403
+ this.clampScroll();
404
+ }
405
+
406
+ // ─── Scroll bounds ───────────────────────────────────
407
+
408
+ private clampScroll(): void {
409
+ const { direction } = this._scrollConfig;
410
+ const bounds = this._content.getLocalBounds();
411
+
412
+ if (direction !== 'horizontal') {
413
+ const contentHeight = bounds.height + bounds.y;
414
+ const maxScroll = Math.min(0, this._viewport.height - contentHeight);
415
+ this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
416
+ }
417
+
418
+ if (direction !== 'vertical') {
419
+ const contentWidth = bounds.width + bounds.x;
420
+ const maxScroll = Math.min(0, this._viewport.width - contentWidth);
421
+ this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
422
+ }
423
+
424
+ this.updateScrollbar();
425
+ }
426
+
427
+ private updateScrollbar(): void {
428
+ if (!this._scrollbar) return;
429
+ const { direction } = this._scrollConfig;
430
+ const { width: sbW, padding: sbPad } = this._scrollbarConfig;
431
+ const bounds = this._content.getLocalBounds();
432
+ const isVert = direction !== 'horizontal';
433
+
434
+ if (isVert) {
435
+ const contentH = bounds.height + bounds.y;
436
+ if (contentH <= this._viewport.height) {
437
+ this._scrollbar.visible = false;
438
+ return;
439
+ }
440
+ this._scrollbar.visible = true;
441
+ const ratio = this._viewport.height / contentH;
442
+ const thumbH = Math.max(20, this._viewport.height * ratio);
443
+ const scrollRange = this._viewport.height - thumbH;
444
+ const scrollProgress = -this._content.y / (contentH - this._viewport.height);
445
+
446
+ this._scrollbar.x = this._viewport.width - sbW - sbPad;
447
+ this._scrollbar.y = scrollProgress * scrollRange;
448
+ this._scrollbar.height = thumbH;
449
+ this._scrollbar.width = sbW;
450
+ } else {
451
+ const contentW = bounds.width + bounds.x;
452
+ if (contentW <= this._viewport.width) {
453
+ this._scrollbar.visible = false;
454
+ return;
455
+ }
456
+ this._scrollbar.visible = true;
457
+ const ratio = this._viewport.width / contentW;
458
+ const thumbW = Math.max(20, this._viewport.width * ratio);
459
+ const scrollRange = this._viewport.width - thumbW;
460
+ const scrollProgress = -this._content.x / (contentW - this._viewport.width);
461
+
462
+ this._scrollbar.y = this._viewport.height - sbW - sbPad;
463
+ this._scrollbar.x = scrollProgress * scrollRange;
464
+ this._scrollbar.width = thumbW;
465
+ this._scrollbar.height = sbW;
466
+ }
467
+ }
468
+
469
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
470
+ this.stopInertia();
471
+ this.off('pointerdown', this._onPointerDown, this);
472
+ this.off('pointermove', this._onPointerMove, this);
473
+ this.off('pointerup', this._onPointerUp, this);
474
+ this.off('pointerupoutside', this._onPointerUp, this);
475
+ this._items.length = 0;
476
+ super.destroy(options);
125
477
  }
126
478
  }