cubeforge 0.0.5 → 0.0.6

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,15 @@
1
+ import React from 'react';
2
+ import type { WorldSnapshot } from '@cubeforge/core';
3
+ import type { ECSWorld, GameLoop } from '@cubeforge/core';
4
+ export declare const MAX_DEVTOOLS_FRAMES = 600;
5
+ export interface DevToolsHandle {
6
+ buffer: WorldSnapshot[];
7
+ onFrame?: () => void;
8
+ }
9
+ interface DevToolsProps {
10
+ handle: DevToolsHandle;
11
+ loop: GameLoop;
12
+ ecs: ECSWorld;
13
+ }
14
+ export declare function DevToolsOverlay({ handle, loop, ecs }: DevToolsProps): React.ReactPortal;
15
+ export {};
@@ -21,11 +21,17 @@ interface GameProps {
21
21
  scale?: 'none' | 'contain' | 'pixel';
22
22
  /** Called once the engine is ready — receives pause/resume/reset controls */
23
23
  onReady?: (controls: GameControls) => void;
24
+ /** Enable time-travel debugging overlay (frame scrubber + entity inspector). */
25
+ devtools?: boolean;
26
+ /** Run the simulation in deterministic mode using a seeded RNG. */
27
+ deterministic?: boolean;
28
+ /** Seed for the deterministic RNG (default 0). Only used when deterministic=true. */
29
+ seed?: number;
24
30
  /** Custom plugins to register after core systems. Each plugin's systems run after Render. */
25
31
  plugins?: Plugin[];
26
32
  style?: CSSProperties;
27
33
  className?: string;
28
34
  children?: React.ReactNode;
29
35
  }
30
- export declare function Game({ width, height, gravity, debug, scale, onReady, plugins, style, className, children, }: GameProps): import("react/jsx-runtime").JSX.Element;
36
+ export declare function Game({ width, height, gravity, debug, devtools, scale, deterministic, seed, onReady, plugins, style, className, children, }: GameProps): import("react/jsx-runtime").JSX.Element;
31
37
  export {};
package/dist/index.d.ts CHANGED
@@ -27,9 +27,10 @@ export type { SpriteAtlas } from './components/spriteAtlas';
27
27
  export { createAtlas } from './components/spriteAtlas';
28
28
  export type { EngineState } from './context';
29
29
  export type { GameControls } from './components/Game';
30
+ export type { DevToolsHandle } from './components/DevTools';
30
31
  export type { PlatformerControllerOptions } from './hooks/usePlatformerController';
31
32
  export type { TopDownMovementOptions } from './hooks/useTopDownMovement';
32
- export type { EntityId, ECSWorld, ScriptUpdateFn, Plugin } from '@cubeforge/core';
33
+ export type { EntityId, ECSWorld, ScriptUpdateFn, Plugin, WorldSnapshot } from '@cubeforge/core';
33
34
  export { definePlugin } from '@cubeforge/core';
34
35
  export type { InputManager } from '@cubeforge/input';
35
36
  export type { TransformComponent } from '@cubeforge/core';
package/dist/index.js CHANGED
@@ -1,48 +1,94 @@
1
1
  // src/components/Game.tsx
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
3
3
  // ../core/src/ecs/world.ts
4
4
  class ECSWorld {
5
5
  nextId = 0;
6
- entities = new Set;
7
- components = new Map;
6
+ componentIndex = new Map;
7
+ _rngState = 0;
8
+ _deterministic = false;
9
+ archetypes = new Map;
10
+ entityArchetype = new Map;
8
11
  systems = [];
9
12
  queryCache = new Map;
10
13
  dirtyTypes = new Set;
11
14
  dirtyAll = false;
15
+ getOrCreateArchetype(types) {
16
+ const arr = [...types].sort();
17
+ const key = arr.join("\x00");
18
+ let arch = this.archetypes.get(key);
19
+ if (!arch) {
20
+ arch = { key, types: new Set(arr), entities: [] };
21
+ this.archetypes.set(key, arch);
22
+ }
23
+ return arch;
24
+ }
25
+ moveToArchetype(id, newArch) {
26
+ const oldKey = this.entityArchetype.get(id);
27
+ if (oldKey !== undefined) {
28
+ const oldArch = this.archetypes.get(oldKey);
29
+ if (oldArch) {
30
+ const idx = oldArch.entities.indexOf(id);
31
+ if (idx !== -1)
32
+ oldArch.entities.splice(idx, 1);
33
+ }
34
+ }
35
+ newArch.entities.push(id);
36
+ this.entityArchetype.set(id, newArch.key);
37
+ }
12
38
  createEntity() {
13
39
  const id = this.nextId++;
14
- this.entities.add(id);
15
- this.components.set(id, new Map);
40
+ this.componentIndex.set(id, new Map);
41
+ const emptyArch = this.getOrCreateArchetype([]);
42
+ emptyArch.entities.push(id);
43
+ this.entityArchetype.set(id, emptyArch.key);
16
44
  this.dirtyAll = true;
17
45
  return id;
18
46
  }
19
47
  destroyEntity(id) {
20
- const comps = this.components.get(id);
48
+ const comps = this.componentIndex.get(id);
21
49
  if (comps) {
22
- for (const type of comps.keys()) {
50
+ for (const type of comps.keys())
23
51
  this.dirtyTypes.add(type);
52
+ }
53
+ const archKey = this.entityArchetype.get(id);
54
+ if (archKey !== undefined) {
55
+ const arch = this.archetypes.get(archKey);
56
+ if (arch) {
57
+ const idx = arch.entities.indexOf(id);
58
+ if (idx !== -1)
59
+ arch.entities.splice(idx, 1);
24
60
  }
25
61
  }
26
- this.entities.delete(id);
27
- this.components.delete(id);
62
+ this.componentIndex.delete(id);
63
+ this.entityArchetype.delete(id);
28
64
  this.dirtyAll = true;
29
65
  }
30
66
  hasEntity(id) {
31
- return this.entities.has(id);
67
+ return this.componentIndex.has(id);
32
68
  }
33
69
  addComponent(id, component) {
34
- this.components.get(id)?.set(component.type, component);
70
+ const comps = this.componentIndex.get(id);
71
+ if (!comps)
72
+ return;
73
+ comps.set(component.type, component);
35
74
  this.dirtyTypes.add(component.type);
75
+ const newArch = this.getOrCreateArchetype(comps.keys());
76
+ this.moveToArchetype(id, newArch);
36
77
  }
37
78
  removeComponent(id, type) {
38
- this.components.get(id)?.delete(type);
79
+ const comps = this.componentIndex.get(id);
80
+ if (!comps)
81
+ return;
82
+ comps.delete(type);
39
83
  this.dirtyTypes.add(type);
84
+ const newArch = this.getOrCreateArchetype(comps.keys());
85
+ this.moveToArchetype(id, newArch);
40
86
  }
41
87
  getComponent(id, type) {
42
- return this.components.get(id)?.get(type);
88
+ return this.componentIndex.get(id)?.get(type);
43
89
  }
44
90
  hasComponent(id, type) {
45
- return this.components.get(id)?.has(type) ?? false;
91
+ return this.componentIndex.get(id)?.has(type) ?? false;
46
92
  }
47
93
  query(...types) {
48
94
  const key = types.slice().sort().join("\x00");
@@ -50,36 +96,60 @@ class ECSWorld {
50
96
  if (cached)
51
97
  return cached;
52
98
  const result = [];
53
- for (const id of this.entities) {
54
- const comps = this.components.get(id);
55
- let match = true;
56
- for (const t of types) {
57
- if (!comps.has(t)) {
58
- match = false;
59
- break;
60
- }
99
+ for (const arch of this.archetypes.values()) {
100
+ if (types.every((t) => arch.types.has(t))) {
101
+ for (const id of arch.entities)
102
+ result.push(id);
61
103
  }
62
- if (match)
63
- result.push(id);
64
104
  }
65
105
  this.queryCache.set(key, result);
66
106
  return result;
67
107
  }
68
108
  queryOne(...types) {
69
- for (const id of this.entities) {
70
- const comps = this.components.get(id);
71
- let match = true;
72
- for (const t of types) {
73
- if (!comps.has(t)) {
74
- match = false;
75
- break;
76
- }
109
+ for (const arch of this.archetypes.values()) {
110
+ if (types.every((t) => arch.types.has(t))) {
111
+ if (arch.entities.length > 0)
112
+ return arch.entities[0];
77
113
  }
78
- if (match)
79
- return id;
80
114
  }
81
115
  return;
82
116
  }
117
+ setDeterministicSeed(seed) {
118
+ this._rngState = seed >>> 0;
119
+ this._deterministic = true;
120
+ }
121
+ rng() {
122
+ if (!this._deterministic)
123
+ return Math.random();
124
+ this._rngState = Math.imul(this._rngState, 1664525) + 1013904223 >>> 0;
125
+ return this._rngState / 4294967296;
126
+ }
127
+ getSnapshot() {
128
+ const entities = [];
129
+ for (const [id, comps] of this.componentIndex) {
130
+ const components = [];
131
+ for (const comp of comps.values()) {
132
+ components.push(JSON.parse(JSON.stringify(comp)));
133
+ }
134
+ entities.push({ id, components });
135
+ }
136
+ return { nextId: this.nextId, rngState: this._rngState, entities };
137
+ }
138
+ restoreSnapshot(snapshot) {
139
+ this.clear();
140
+ this.nextId = snapshot.nextId;
141
+ this._rngState = snapshot.rngState;
142
+ for (const { id, components } of snapshot.entities) {
143
+ const compMap = new Map;
144
+ for (const comp of components)
145
+ compMap.set(comp.type, comp);
146
+ this.componentIndex.set(id, compMap);
147
+ const arch = this.getOrCreateArchetype(compMap.keys());
148
+ arch.entities.push(id);
149
+ this.entityArchetype.set(id, arch.key);
150
+ }
151
+ this.dirtyAll = true;
152
+ }
83
153
  addSystem(system) {
84
154
  this.systems.push(system);
85
155
  }
@@ -110,15 +180,18 @@ class ECSWorld {
110
180
  }
111
181
  }
112
182
  clear() {
113
- this.entities.clear();
114
- this.components.clear();
183
+ this.componentIndex.clear();
184
+ this.archetypes.clear();
185
+ this.entityArchetype.clear();
115
186
  this.queryCache.clear();
116
187
  this.dirtyTypes.clear();
117
188
  this.dirtyAll = false;
118
189
  this.nextId = 0;
190
+ this._rngState = 0;
191
+ this._deterministic = false;
119
192
  }
120
193
  get entityCount() {
121
- return this.entities.size;
194
+ return this.componentIndex.size;
122
195
  }
123
196
  }
124
197
  // ../core/src/loop/gameLoop.ts
@@ -673,8 +746,8 @@ class RenderSystem {
673
746
  if (cam.shakeTimer < 0)
674
747
  cam.shakeTimer = 0;
675
748
  const progress = cam.shakeDuration > 0 ? cam.shakeTimer / cam.shakeDuration : 0;
676
- shakeX = (Math.random() * 2 - 1) * cam.shakeIntensity * progress;
677
- shakeY = (Math.random() * 2 - 1) * cam.shakeIntensity * progress;
749
+ shakeX = (world2.rng() * 2 - 1) * cam.shakeIntensity * progress;
750
+ shakeY = (world2.rng() * 2 - 1) * cam.shakeIntensity * progress;
678
751
  }
679
752
  camX = cam.x;
680
753
  camY = cam.y;
@@ -755,10 +828,27 @@ class RenderSystem {
755
828
  ctx.translate(canvas.width / 2 - camX * zoom + shakeX, canvas.height / 2 - camY * zoom + shakeY);
756
829
  ctx.scale(zoom, zoom);
757
830
  const renderables = world2.query("Transform", "Sprite");
831
+ const textureKey = (id) => {
832
+ const sprite = world2.getComponent(id, "Sprite");
833
+ if (sprite.image && sprite.image.src)
834
+ return sprite.image.src;
835
+ if (sprite.src)
836
+ return sprite.src;
837
+ return `__color__:${sprite.color}`;
838
+ };
758
839
  renderables.sort((a, b) => {
759
- const za = world2.getComponent(a, "Sprite").zIndex;
760
- const zb = world2.getComponent(b, "Sprite").zIndex;
761
- return za - zb;
840
+ const sa = world2.getComponent(a, "Sprite");
841
+ const sb = world2.getComponent(b, "Sprite");
842
+ const zDiff = sa.zIndex - sb.zIndex;
843
+ if (zDiff !== 0)
844
+ return zDiff;
845
+ const ka = textureKey(a);
846
+ const kb = textureKey(b);
847
+ if (ka < kb)
848
+ return -1;
849
+ if (ka > kb)
850
+ return 1;
851
+ return 0;
762
852
  });
763
853
  for (const id of renderables) {
764
854
  const transform2 = world2.getComponent(id, "Transform");
@@ -809,8 +899,8 @@ class RenderSystem {
809
899
  const spawnCount = Math.floor(pool.timer * pool.rate);
810
900
  pool.timer -= spawnCount / pool.rate;
811
901
  for (let i = 0;i < spawnCount && pool.particles.length < pool.maxParticles; i++) {
812
- const angle = pool.angle + (Math.random() - 0.5) * pool.spread;
813
- const speed = pool.speed * (0.5 + Math.random() * 0.5);
902
+ const angle = pool.angle + (world2.rng() - 0.5) * pool.spread;
903
+ const speed = pool.speed * (0.5 + world2.rng() * 0.5);
814
904
  pool.particles.push({
815
905
  x: t.x,
816
906
  y: t.y,
@@ -1253,14 +1343,318 @@ class DebugSystem {
1253
1343
  }
1254
1344
  }
1255
1345
 
1346
+ // src/components/DevTools.tsx
1347
+ import React from "react";
1348
+ import { createPortal } from "react-dom";
1349
+ import { useState, useEffect, useCallback } from "react";
1350
+ import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
1351
+ var MAX_DEVTOOLS_FRAMES = 600;
1352
+ var css = {
1353
+ overlay: {
1354
+ position: "fixed",
1355
+ bottom: 0,
1356
+ left: 0,
1357
+ right: 0,
1358
+ zIndex: 99999,
1359
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Courier New', monospace",
1360
+ fontSize: 11,
1361
+ color: "#cdd6f4",
1362
+ userSelect: "none",
1363
+ pointerEvents: "auto"
1364
+ },
1365
+ bar: {
1366
+ background: "rgba(11,13,20,0.97)",
1367
+ borderTop: "1px solid #2a3048",
1368
+ padding: "6px 12px",
1369
+ display: "flex",
1370
+ alignItems: "center",
1371
+ gap: 10,
1372
+ height: 40
1373
+ },
1374
+ badge: {
1375
+ fontSize: 9,
1376
+ fontWeight: 700,
1377
+ letterSpacing: "0.1em",
1378
+ color: "#4fc3f7",
1379
+ background: "rgba(79,195,247,0.1)",
1380
+ border: "1px solid rgba(79,195,247,0.2)",
1381
+ borderRadius: 4,
1382
+ padding: "2px 6px",
1383
+ flexShrink: 0
1384
+ },
1385
+ btn: (active = false, danger = false) => ({
1386
+ background: active ? "rgba(79,195,247,0.15)" : "rgba(255,255,255,0.04)",
1387
+ border: `1px solid ${active ? "rgba(79,195,247,0.3)" : "#2a3048"}`,
1388
+ borderRadius: 4,
1389
+ color: danger ? "#f38ba8" : active ? "#4fc3f7" : "#6b7a9e",
1390
+ cursor: "pointer",
1391
+ padding: "3px 8px",
1392
+ fontSize: 10,
1393
+ fontFamily: "inherit",
1394
+ display: "flex",
1395
+ alignItems: "center",
1396
+ gap: 4,
1397
+ transition: "all 0.1s",
1398
+ flexShrink: 0
1399
+ }),
1400
+ scrubber: {
1401
+ flex: 1,
1402
+ accentColor: "#4fc3f7",
1403
+ cursor: "pointer",
1404
+ height: 4
1405
+ },
1406
+ counter: {
1407
+ color: "#6b7a9e",
1408
+ fontSize: 10,
1409
+ whiteSpace: "nowrap",
1410
+ flexShrink: 0,
1411
+ minWidth: 80,
1412
+ textAlign: "right"
1413
+ },
1414
+ panel: {
1415
+ background: "rgba(11,13,20,0.97)",
1416
+ borderTop: "1px solid #1f2435",
1417
+ maxHeight: 260,
1418
+ overflowY: "auto",
1419
+ padding: "8px 0"
1420
+ },
1421
+ entityRow: (selected) => ({
1422
+ display: "flex",
1423
+ alignItems: "center",
1424
+ gap: 8,
1425
+ padding: "4px 12px",
1426
+ cursor: "pointer",
1427
+ background: selected ? "rgba(79,195,247,0.06)" : "transparent",
1428
+ borderLeft: `2px solid ${selected ? "#4fc3f7" : "transparent"}`
1429
+ }),
1430
+ entityId: {
1431
+ color: "#4fc3f7",
1432
+ minWidth: 28,
1433
+ fontSize: 10
1434
+ },
1435
+ compPill: {
1436
+ fontSize: 9,
1437
+ background: "rgba(79,195,247,0.08)",
1438
+ border: "1px solid rgba(79,195,247,0.12)",
1439
+ borderRadius: 3,
1440
+ padding: "1px 5px",
1441
+ color: "#6b7a9e"
1442
+ },
1443
+ detailPanel: {
1444
+ background: "rgba(18,21,31,0.98)",
1445
+ borderTop: "1px solid #1f2435",
1446
+ padding: "10px 14px",
1447
+ maxHeight: 200,
1448
+ overflowY: "auto"
1449
+ },
1450
+ kv: {
1451
+ display: "grid",
1452
+ gridTemplateColumns: "140px 1fr",
1453
+ gap: "2px 12px",
1454
+ lineHeight: 1.8
1455
+ },
1456
+ key: { color: "#6b7a9e" },
1457
+ val: { color: "#cdd6f4" }
1458
+ };
1459
+ function DevToolsOverlay({ handle, loop, ecs }) {
1460
+ const [, forceUpdate] = useState(0);
1461
+ const [paused, setPaused] = useState(false);
1462
+ const [selectedIdx, setSelectedIdx] = useState(0);
1463
+ const [panelOpen, setPanelOpen] = useState(false);
1464
+ const [selectedEntity, setSelectedEntity] = useState(null);
1465
+ useEffect(() => {
1466
+ handle.onFrame = () => {
1467
+ if (!paused) {
1468
+ forceUpdate((n) => n + 1);
1469
+ setSelectedIdx(Math.max(0, handle.buffer.length - 1));
1470
+ }
1471
+ };
1472
+ return () => {
1473
+ handle.onFrame = undefined;
1474
+ };
1475
+ }, [handle, paused]);
1476
+ const totalFrames = handle.buffer.length;
1477
+ const currentSnap = handle.buffer[selectedIdx];
1478
+ const handlePauseResume = useCallback(() => {
1479
+ if (paused) {
1480
+ if (currentSnap)
1481
+ ecs.restoreSnapshot(currentSnap);
1482
+ loop.resume();
1483
+ setPaused(false);
1484
+ setSelectedEntity(null);
1485
+ } else {
1486
+ loop.pause();
1487
+ setPaused(true);
1488
+ setSelectedIdx(Math.max(0, handle.buffer.length - 1));
1489
+ }
1490
+ }, [paused, currentSnap, ecs, loop, handle]);
1491
+ const stepBack = useCallback(() => {
1492
+ setSelectedIdx((i) => Math.max(0, i - 1));
1493
+ setSelectedEntity(null);
1494
+ }, []);
1495
+ const stepForward = useCallback(() => {
1496
+ setSelectedIdx((i) => Math.min(handle.buffer.length - 1, i + 1));
1497
+ setSelectedEntity(null);
1498
+ }, [handle]);
1499
+ const frameLabel = totalFrames === 0 ? "0 / 0" : `${selectedIdx + 1} / ${totalFrames}`;
1500
+ const entities = currentSnap?.entities ?? [];
1501
+ const selectedEntityData = selectedEntity !== null ? entities.find((e) => e.id === selectedEntity) : null;
1502
+ return createPortal(/* @__PURE__ */ jsxDEV("div", {
1503
+ style: css.overlay,
1504
+ children: [
1505
+ panelOpen && paused && /* @__PURE__ */ jsxDEV(Fragment, {
1506
+ children: [
1507
+ /* @__PURE__ */ jsxDEV("div", {
1508
+ style: css.panel,
1509
+ children: [
1510
+ entities.length === 0 && /* @__PURE__ */ jsxDEV("div", {
1511
+ style: { padding: "4px 14px", color: "#3d4666" },
1512
+ children: "No entities"
1513
+ }, undefined, false, undefined, this),
1514
+ entities.map((e) => /* @__PURE__ */ jsxDEV("div", {
1515
+ style: css.entityRow(selectedEntity === e.id),
1516
+ onClick: () => setSelectedEntity((s) => s === e.id ? null : e.id),
1517
+ children: [
1518
+ /* @__PURE__ */ jsxDEV("span", {
1519
+ style: css.entityId,
1520
+ children: [
1521
+ "#",
1522
+ e.id
1523
+ ]
1524
+ }, undefined, true, undefined, this),
1525
+ /* @__PURE__ */ jsxDEV("div", {
1526
+ style: { display: "flex", gap: 4, flexWrap: "wrap" },
1527
+ children: e.components.map((c) => /* @__PURE__ */ jsxDEV("span", {
1528
+ style: css.compPill,
1529
+ children: c.type
1530
+ }, c.type, false, undefined, this))
1531
+ }, undefined, false, undefined, this)
1532
+ ]
1533
+ }, e.id, true, undefined, this))
1534
+ ]
1535
+ }, undefined, true, undefined, this),
1536
+ selectedEntityData && /* @__PURE__ */ jsxDEV("div", {
1537
+ style: css.detailPanel,
1538
+ children: selectedEntityData.components.map((comp) => /* @__PURE__ */ jsxDEV("div", {
1539
+ style: { marginBottom: 10 },
1540
+ children: [
1541
+ /* @__PURE__ */ jsxDEV("div", {
1542
+ style: { color: "#4fc3f7", fontSize: 10, fontWeight: 700, marginBottom: 4 },
1543
+ children: comp.type
1544
+ }, undefined, false, undefined, this),
1545
+ /* @__PURE__ */ jsxDEV("div", {
1546
+ style: css.kv,
1547
+ children: Object.entries(comp).filter(([k]) => k !== "type").map(([k, v]) => /* @__PURE__ */ jsxDEV(React.Fragment, {
1548
+ children: [
1549
+ /* @__PURE__ */ jsxDEV("span", {
1550
+ style: css.key,
1551
+ children: k
1552
+ }, undefined, false, undefined, this),
1553
+ /* @__PURE__ */ jsxDEV("span", {
1554
+ style: css.val,
1555
+ children: formatValue(v)
1556
+ }, undefined, false, undefined, this)
1557
+ ]
1558
+ }, k, true, undefined, this))
1559
+ }, undefined, false, undefined, this)
1560
+ ]
1561
+ }, comp.type, true, undefined, this))
1562
+ }, undefined, false, undefined, this)
1563
+ ]
1564
+ }, undefined, true, undefined, this),
1565
+ /* @__PURE__ */ jsxDEV("div", {
1566
+ style: css.bar,
1567
+ children: [
1568
+ /* @__PURE__ */ jsxDEV("span", {
1569
+ style: css.badge,
1570
+ children: "DEVTOOLS"
1571
+ }, undefined, false, undefined, this),
1572
+ /* @__PURE__ */ jsxDEV("button", {
1573
+ style: css.btn(paused),
1574
+ onClick: handlePauseResume,
1575
+ children: paused ? "▶ Resume" : "⏸ Pause"
1576
+ }, undefined, false, undefined, this),
1577
+ paused && /* @__PURE__ */ jsxDEV(Fragment, {
1578
+ children: [
1579
+ /* @__PURE__ */ jsxDEV("button", {
1580
+ style: css.btn(),
1581
+ onClick: stepBack,
1582
+ children: "◀◀"
1583
+ }, undefined, false, undefined, this),
1584
+ /* @__PURE__ */ jsxDEV("button", {
1585
+ style: css.btn(),
1586
+ onClick: stepForward,
1587
+ children: "▶▶"
1588
+ }, undefined, false, undefined, this)
1589
+ ]
1590
+ }, undefined, true, undefined, this),
1591
+ /* @__PURE__ */ jsxDEV("input", {
1592
+ type: "range",
1593
+ min: 0,
1594
+ max: Math.max(0, totalFrames - 1),
1595
+ value: selectedIdx,
1596
+ style: css.scrubber,
1597
+ onChange: (e) => {
1598
+ const idx = Number(e.target.value);
1599
+ setSelectedIdx(idx);
1600
+ setSelectedEntity(null);
1601
+ if (!paused) {
1602
+ loop.pause();
1603
+ setPaused(true);
1604
+ }
1605
+ }
1606
+ }, undefined, false, undefined, this),
1607
+ /* @__PURE__ */ jsxDEV("span", {
1608
+ style: css.counter,
1609
+ children: frameLabel
1610
+ }, undefined, false, undefined, this),
1611
+ paused && /* @__PURE__ */ jsxDEV("button", {
1612
+ style: css.btn(panelOpen),
1613
+ onClick: () => setPanelOpen((o) => !o),
1614
+ children: [
1615
+ panelOpen ? "▾" : "▸",
1616
+ " Entities (",
1617
+ entities.length,
1618
+ ")"
1619
+ ]
1620
+ }, undefined, true, undefined, this),
1621
+ /* @__PURE__ */ jsxDEV("button", {
1622
+ style: css.btn(false, true),
1623
+ onClick: () => {
1624
+ handle.buffer.length = 0;
1625
+ setSelectedIdx(0);
1626
+ forceUpdate((n) => n + 1);
1627
+ },
1628
+ children: "Clear"
1629
+ }, undefined, false, undefined, this)
1630
+ ]
1631
+ }, undefined, true, undefined, this)
1632
+ ]
1633
+ }, undefined, true, undefined, this), document.body);
1634
+ }
1635
+ function formatValue(v) {
1636
+ if (typeof v === "number")
1637
+ return v.toFixed(2);
1638
+ if (typeof v === "boolean")
1639
+ return v ? "true" : "false";
1640
+ if (v === null || v === undefined)
1641
+ return "—";
1642
+ if (typeof v === "object")
1643
+ return JSON.stringify(v).slice(0, 60);
1644
+ return String(v);
1645
+ }
1646
+
1256
1647
  // src/components/Game.tsx
1257
- import { jsxDEV } from "react/jsx-dev-runtime";
1648
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
1258
1649
  function Game({
1259
1650
  width = 800,
1260
1651
  height = 600,
1261
1652
  gravity = 980,
1262
1653
  debug = false,
1654
+ devtools = false,
1263
1655
  scale = "none",
1656
+ deterministic = false,
1657
+ seed = 0,
1264
1658
  onReady,
1265
1659
  plugins,
1266
1660
  style,
@@ -1269,10 +1663,13 @@ function Game({
1269
1663
  }) {
1270
1664
  const canvasRef = useRef(null);
1271
1665
  const wrapperRef = useRef(null);
1272
- const [engine, setEngine] = useState(null);
1273
- useEffect(() => {
1666
+ const [engine, setEngine] = useState2(null);
1667
+ const devtoolsHandle = useRef({ buffer: [] });
1668
+ useEffect2(() => {
1274
1669
  const canvas = canvasRef.current;
1275
1670
  const ecs = new ECSWorld;
1671
+ if (deterministic)
1672
+ ecs.setDeterministicSeed(seed);
1276
1673
  const input = new InputManager;
1277
1674
  const renderer = new Canvas2DRenderer(canvas);
1278
1675
  const events = new EventBus;
@@ -1294,6 +1691,13 @@ function Game({
1294
1691
  const loop = new GameLoop((dt) => {
1295
1692
  ecs.update(dt);
1296
1693
  input.flush();
1694
+ if (devtools) {
1695
+ const handle = devtoolsHandle.current;
1696
+ handle.buffer.push(ecs.getSnapshot());
1697
+ if (handle.buffer.length > MAX_DEVTOOLS_FRAMES)
1698
+ handle.buffer.shift();
1699
+ handle.onFrame?.();
1700
+ }
1297
1701
  });
1298
1702
  const state = { ecs, input, renderer, physics, events, assets, loop, canvas, entityIds };
1299
1703
  setEngine(state);
@@ -1339,7 +1743,7 @@ function Game({
1339
1743
  resizeObserver?.disconnect();
1340
1744
  };
1341
1745
  }, []);
1342
- useEffect(() => {
1746
+ useEffect2(() => {
1343
1747
  engine?.physics.setGravity(gravity);
1344
1748
  }, [gravity, engine]);
1345
1749
  const canvasStyle = {
@@ -1349,13 +1753,13 @@ function Game({
1349
1753
  ...style
1350
1754
  };
1351
1755
  const wrapperStyle = scale === "contain" ? { position: "relative", width, height, overflow: "visible" } : {};
1352
- return /* @__PURE__ */ jsxDEV(EngineContext.Provider, {
1756
+ return /* @__PURE__ */ jsxDEV2(EngineContext.Provider, {
1353
1757
  value: engine,
1354
1758
  children: [
1355
- /* @__PURE__ */ jsxDEV("div", {
1759
+ /* @__PURE__ */ jsxDEV2("div", {
1356
1760
  ref: wrapperRef,
1357
1761
  style: wrapperStyle,
1358
- children: /* @__PURE__ */ jsxDEV("canvas", {
1762
+ children: /* @__PURE__ */ jsxDEV2("canvas", {
1359
1763
  ref: canvasRef,
1360
1764
  width,
1361
1765
  height,
@@ -1363,22 +1767,27 @@ function Game({
1363
1767
  className
1364
1768
  }, undefined, false, undefined, this)
1365
1769
  }, undefined, false, undefined, this),
1366
- engine && children
1770
+ engine && children,
1771
+ engine && devtools && /* @__PURE__ */ jsxDEV2(DevToolsOverlay, {
1772
+ handle: devtoolsHandle.current,
1773
+ loop: engine.loop,
1774
+ ecs: engine.ecs
1775
+ }, undefined, false, undefined, this)
1367
1776
  ]
1368
1777
  }, undefined, true, undefined, this);
1369
1778
  }
1370
1779
  // src/components/World.tsx
1371
- import { useEffect as useEffect2, useContext } from "react";
1372
- import { jsxDEV as jsxDEV2, Fragment } from "react/jsx-dev-runtime";
1780
+ import { useEffect as useEffect3, useContext } from "react";
1781
+ import { jsxDEV as jsxDEV3, Fragment as Fragment2 } from "react/jsx-dev-runtime";
1373
1782
  function World({ gravity, background = "#1a1a2e", children }) {
1374
1783
  const engine = useContext(EngineContext);
1375
- useEffect2(() => {
1784
+ useEffect3(() => {
1376
1785
  if (!engine)
1377
1786
  return;
1378
1787
  if (gravity !== undefined)
1379
1788
  engine.physics.setGravity(gravity);
1380
1789
  }, [gravity, engine]);
1381
- useEffect2(() => {
1790
+ useEffect3(() => {
1382
1791
  if (!engine)
1383
1792
  return;
1384
1793
  const camId = engine.ecs.queryOne("Camera2D");
@@ -1390,17 +1799,17 @@ function World({ gravity, background = "#1a1a2e", children }) {
1390
1799
  engine.canvas.style.background = background;
1391
1800
  }
1392
1801
  }, [background, engine]);
1393
- return /* @__PURE__ */ jsxDEV2(Fragment, {
1802
+ return /* @__PURE__ */ jsxDEV3(Fragment2, {
1394
1803
  children
1395
1804
  }, undefined, false, undefined, this);
1396
1805
  }
1397
1806
  // src/components/Entity.tsx
1398
- import { useEffect as useEffect3, useContext as useContext2, useState as useState2 } from "react";
1399
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
1807
+ import { useEffect as useEffect4, useContext as useContext2, useState as useState3 } from "react";
1808
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
1400
1809
  function Entity({ id, tags = [], children }) {
1401
1810
  const engine = useContext2(EngineContext);
1402
- const [entityId, setEntityId] = useState2(null);
1403
- useEffect3(() => {
1811
+ const [entityId, setEntityId] = useState3(null);
1812
+ useEffect4(() => {
1404
1813
  const eid = engine.ecs.createEntity();
1405
1814
  if (id) {
1406
1815
  if (engine.entityIds.has(id)) {
@@ -1419,21 +1828,21 @@ function Entity({ id, tags = [], children }) {
1419
1828
  }, []);
1420
1829
  if (entityId === null)
1421
1830
  return null;
1422
- return /* @__PURE__ */ jsxDEV3(EntityContext.Provider, {
1831
+ return /* @__PURE__ */ jsxDEV4(EntityContext.Provider, {
1423
1832
  value: entityId,
1424
1833
  children
1425
1834
  }, undefined, false, undefined, this);
1426
1835
  }
1427
1836
  // src/components/Transform.tsx
1428
- import { useEffect as useEffect4, useContext as useContext3 } from "react";
1837
+ import { useEffect as useEffect5, useContext as useContext3 } from "react";
1429
1838
  function Transform({ x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1 }) {
1430
1839
  const engine = useContext3(EngineContext);
1431
1840
  const entityId = useContext3(EntityContext);
1432
- useEffect4(() => {
1841
+ useEffect5(() => {
1433
1842
  engine.ecs.addComponent(entityId, createTransform(x, y, rotation, scaleX, scaleY));
1434
1843
  return () => engine.ecs.removeComponent(entityId, "Transform");
1435
1844
  }, []);
1436
- useEffect4(() => {
1845
+ useEffect5(() => {
1437
1846
  const comp = engine.ecs.getComponent(entityId, "Transform");
1438
1847
  if (comp) {
1439
1848
  comp.x = x;
@@ -1446,7 +1855,7 @@ function Transform({ x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1 }) {
1446
1855
  return null;
1447
1856
  }
1448
1857
  // src/components/Sprite.tsx
1449
- import { useEffect as useEffect5, useContext as useContext4 } from "react";
1858
+ import { useEffect as useEffect6, useContext as useContext4 } from "react";
1450
1859
  function Sprite({
1451
1860
  width,
1452
1861
  height,
@@ -1469,7 +1878,7 @@ function Sprite({
1469
1878
  const resolvedFrameIndex = atlas && frame != null ? atlas[frame] ?? 0 : frameIndex;
1470
1879
  const engine = useContext4(EngineContext);
1471
1880
  const entityId = useContext4(EntityContext);
1472
- useEffect5(() => {
1881
+ useEffect6(() => {
1473
1882
  const comp = createSprite({
1474
1883
  width,
1475
1884
  height,
@@ -1497,7 +1906,7 @@ function Sprite({
1497
1906
  }
1498
1907
  return () => engine.ecs.removeComponent(entityId, "Sprite");
1499
1908
  }, []);
1500
- useEffect5(() => {
1909
+ useEffect6(() => {
1501
1910
  const comp = engine.ecs.getComponent(entityId, "Sprite");
1502
1911
  if (!comp)
1503
1912
  return;
@@ -1510,7 +1919,7 @@ function Sprite({
1510
1919
  return null;
1511
1920
  }
1512
1921
  // src/components/RigidBody.tsx
1513
- import { useEffect as useEffect6, useContext as useContext5 } from "react";
1922
+ import { useEffect as useEffect7, useContext as useContext5 } from "react";
1514
1923
  function RigidBody({
1515
1924
  mass = 1,
1516
1925
  gravityScale = 1,
@@ -1522,14 +1931,14 @@ function RigidBody({
1522
1931
  }) {
1523
1932
  const engine = useContext5(EngineContext);
1524
1933
  const entityId = useContext5(EntityContext);
1525
- useEffect6(() => {
1934
+ useEffect7(() => {
1526
1935
  engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy }));
1527
1936
  return () => engine.ecs.removeComponent(entityId, "RigidBody");
1528
1937
  }, []);
1529
1938
  return null;
1530
1939
  }
1531
1940
  // src/components/BoxCollider.tsx
1532
- import { useEffect as useEffect7, useContext as useContext6 } from "react";
1941
+ import { useEffect as useEffect8, useContext as useContext6 } from "react";
1533
1942
  function BoxCollider({
1534
1943
  width,
1535
1944
  height,
@@ -1540,7 +1949,7 @@ function BoxCollider({
1540
1949
  }) {
1541
1950
  const engine = useContext6(EngineContext);
1542
1951
  const entityId = useContext6(EntityContext);
1543
- useEffect7(() => {
1952
+ useEffect8(() => {
1544
1953
  engine.ecs.addComponent(entityId, createBoxCollider(width, height, { offsetX, offsetY, isTrigger, layer }));
1545
1954
  const checkId = setTimeout(() => {
1546
1955
  if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "Transform")) {
@@ -1555,11 +1964,11 @@ function BoxCollider({
1555
1964
  return null;
1556
1965
  }
1557
1966
  // src/components/Script.tsx
1558
- import { useEffect as useEffect8, useContext as useContext7 } from "react";
1967
+ import { useEffect as useEffect9, useContext as useContext7 } from "react";
1559
1968
  function Script({ init, update }) {
1560
1969
  const engine = useContext7(EngineContext);
1561
1970
  const entityId = useContext7(EntityContext);
1562
- useEffect8(() => {
1971
+ useEffect9(() => {
1563
1972
  if (init) {
1564
1973
  try {
1565
1974
  init(entityId, engine.ecs);
@@ -1573,7 +1982,7 @@ function Script({ init, update }) {
1573
1982
  return null;
1574
1983
  }
1575
1984
  // src/components/Camera2D.tsx
1576
- import { useEffect as useEffect9, useContext as useContext8 } from "react";
1985
+ import { useEffect as useEffect10, useContext as useContext8 } from "react";
1577
1986
  function Camera2D({
1578
1987
  followEntity,
1579
1988
  zoom = 1,
@@ -1583,7 +1992,7 @@ function Camera2D({
1583
1992
  deadZone
1584
1993
  }) {
1585
1994
  const engine = useContext8(EngineContext);
1586
- useEffect9(() => {
1995
+ useEffect10(() => {
1587
1996
  const entityId = engine.ecs.createEntity();
1588
1997
  engine.ecs.addComponent(entityId, createCamera2D({
1589
1998
  followEntityId: followEntity,
@@ -1595,7 +2004,7 @@ function Camera2D({
1595
2004
  }));
1596
2005
  return () => engine.ecs.destroyEntity(entityId);
1597
2006
  }, []);
1598
- useEffect9(() => {
2007
+ useEffect10(() => {
1599
2008
  const camId = engine.ecs.queryOne("Camera2D");
1600
2009
  if (camId === undefined)
1601
2010
  return;
@@ -1610,11 +2019,11 @@ function Camera2D({
1610
2019
  return null;
1611
2020
  }
1612
2021
  // src/components/Animation.tsx
1613
- import { useEffect as useEffect10, useContext as useContext9 } from "react";
2022
+ import { useEffect as useEffect11, useContext as useContext9 } from "react";
1614
2023
  function Animation({ frames, fps = 12, loop = true, playing = true }) {
1615
2024
  const engine = useContext9(EngineContext);
1616
2025
  const entityId = useContext9(EntityContext);
1617
- useEffect10(() => {
2026
+ useEffect11(() => {
1618
2027
  const state = {
1619
2028
  type: "AnimationState",
1620
2029
  frames,
@@ -1629,7 +2038,7 @@ function Animation({ frames, fps = 12, loop = true, playing = true }) {
1629
2038
  engine.ecs.removeComponent(entityId, "AnimationState");
1630
2039
  };
1631
2040
  }, []);
1632
- useEffect10(() => {
2041
+ useEffect11(() => {
1633
2042
  const anim = engine.ecs.getComponent(entityId, "AnimationState");
1634
2043
  if (!anim)
1635
2044
  return;
@@ -1640,11 +2049,11 @@ function Animation({ frames, fps = 12, loop = true, playing = true }) {
1640
2049
  return null;
1641
2050
  }
1642
2051
  // src/components/SquashStretch.tsx
1643
- import { useEffect as useEffect11, useContext as useContext10 } from "react";
2052
+ import { useEffect as useEffect12, useContext as useContext10 } from "react";
1644
2053
  function SquashStretch({ intensity = 0.2, recovery = 8 }) {
1645
2054
  const engine = useContext10(EngineContext);
1646
2055
  const entityId = useContext10(EntityContext);
1647
- useEffect11(() => {
2056
+ useEffect12(() => {
1648
2057
  engine.ecs.addComponent(entityId, {
1649
2058
  type: "SquashStretch",
1650
2059
  intensity,
@@ -1657,7 +2066,7 @@ function SquashStretch({ intensity = 0.2, recovery = 8 }) {
1657
2066
  return null;
1658
2067
  }
1659
2068
  // src/components/ParticleEmitter.tsx
1660
- import { useEffect as useEffect12, useContext as useContext11 } from "react";
2069
+ import { useEffect as useEffect13, useContext as useContext11 } from "react";
1661
2070
 
1662
2071
  // src/components/particlePresets.ts
1663
2072
  var PARTICLE_PRESETS = {
@@ -1744,7 +2153,7 @@ function ParticleEmitter({
1744
2153
  const resolvedMaxParticles = maxParticles ?? presetConfig.maxParticles ?? 100;
1745
2154
  const engine = useContext11(EngineContext);
1746
2155
  const entityId = useContext11(EntityContext);
1747
- useEffect12(() => {
2156
+ useEffect13(() => {
1748
2157
  engine.ecs.addComponent(entityId, {
1749
2158
  type: "ParticlePool",
1750
2159
  particles: [],
@@ -1762,7 +2171,7 @@ function ParticleEmitter({
1762
2171
  });
1763
2172
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
1764
2173
  }, []);
1765
- useEffect12(() => {
2174
+ useEffect13(() => {
1766
2175
  const pool = engine.ecs.getComponent(entityId, "ParticlePool");
1767
2176
  if (!pool)
1768
2177
  return;
@@ -1771,7 +2180,7 @@ function ParticleEmitter({
1771
2180
  return null;
1772
2181
  }
1773
2182
  // src/components/MovingPlatform.tsx
1774
- import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
2183
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
1775
2184
  var platformPhases = new Map;
1776
2185
  function MovingPlatform({
1777
2186
  x1,
@@ -1783,26 +2192,26 @@ function MovingPlatform({
1783
2192
  duration = 3,
1784
2193
  color = "#37474f"
1785
2194
  }) {
1786
- return /* @__PURE__ */ jsxDEV4(Entity, {
2195
+ return /* @__PURE__ */ jsxDEV5(Entity, {
1787
2196
  children: [
1788
- /* @__PURE__ */ jsxDEV4(Transform, {
2197
+ /* @__PURE__ */ jsxDEV5(Transform, {
1789
2198
  x: x1,
1790
2199
  y: y1
1791
2200
  }, undefined, false, undefined, this),
1792
- /* @__PURE__ */ jsxDEV4(Sprite, {
2201
+ /* @__PURE__ */ jsxDEV5(Sprite, {
1793
2202
  width,
1794
2203
  height,
1795
2204
  color,
1796
2205
  zIndex: 5
1797
2206
  }, undefined, false, undefined, this),
1798
- /* @__PURE__ */ jsxDEV4(RigidBody, {
2207
+ /* @__PURE__ */ jsxDEV5(RigidBody, {
1799
2208
  isStatic: true
1800
2209
  }, undefined, false, undefined, this),
1801
- /* @__PURE__ */ jsxDEV4(BoxCollider, {
2210
+ /* @__PURE__ */ jsxDEV5(BoxCollider, {
1802
2211
  width,
1803
2212
  height
1804
2213
  }, undefined, false, undefined, this),
1805
- /* @__PURE__ */ jsxDEV4(Script, {
2214
+ /* @__PURE__ */ jsxDEV5(Script, {
1806
2215
  init: () => {},
1807
2216
  update: (id, world2, _input, dt) => {
1808
2217
  if (!world2.hasEntity(id))
@@ -1821,7 +2230,7 @@ function MovingPlatform({
1821
2230
  }, undefined, true, undefined, this);
1822
2231
  }
1823
2232
  // src/components/Checkpoint.tsx
1824
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
2233
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
1825
2234
  function Checkpoint({
1826
2235
  x,
1827
2236
  y,
@@ -1830,25 +2239,25 @@ function Checkpoint({
1830
2239
  color = "#ffd54f",
1831
2240
  onActivate
1832
2241
  }) {
1833
- return /* @__PURE__ */ jsxDEV5(Entity, {
2242
+ return /* @__PURE__ */ jsxDEV6(Entity, {
1834
2243
  tags: ["checkpoint"],
1835
2244
  children: [
1836
- /* @__PURE__ */ jsxDEV5(Transform, {
2245
+ /* @__PURE__ */ jsxDEV6(Transform, {
1837
2246
  x,
1838
2247
  y
1839
2248
  }, undefined, false, undefined, this),
1840
- /* @__PURE__ */ jsxDEV5(Sprite, {
2249
+ /* @__PURE__ */ jsxDEV6(Sprite, {
1841
2250
  width,
1842
2251
  height,
1843
2252
  color,
1844
2253
  zIndex: 5
1845
2254
  }, undefined, false, undefined, this),
1846
- /* @__PURE__ */ jsxDEV5(BoxCollider, {
2255
+ /* @__PURE__ */ jsxDEV6(BoxCollider, {
1847
2256
  width,
1848
2257
  height,
1849
2258
  isTrigger: true
1850
2259
  }, undefined, false, undefined, this),
1851
- /* @__PURE__ */ jsxDEV5(Script, {
2260
+ /* @__PURE__ */ jsxDEV6(Script, {
1852
2261
  init: () => {},
1853
2262
  update: (id, world2) => {
1854
2263
  if (!world2.hasEntity(id))
@@ -1877,8 +2286,8 @@ function Checkpoint({
1877
2286
  }, undefined, true, undefined, this);
1878
2287
  }
1879
2288
  // src/components/Tilemap.tsx
1880
- import { useEffect as useEffect13, useState as useState3, useContext as useContext12 } from "react";
1881
- import { jsxDEV as jsxDEV6, Fragment as Fragment2 } from "react/jsx-dev-runtime";
2289
+ import { useEffect as useEffect14, useState as useState4, useContext as useContext12 } from "react";
2290
+ import { jsxDEV as jsxDEV7, Fragment as Fragment3 } from "react/jsx-dev-runtime";
1882
2291
  var animatedTiles = new Map;
1883
2292
  function getProperty(props, name) {
1884
2293
  return props?.find((p) => p.name === name)?.value;
@@ -1902,8 +2311,8 @@ function Tilemap({
1902
2311
  onTileProperty
1903
2312
  }) {
1904
2313
  const engine = useContext12(EngineContext);
1905
- const [spawnedNodes, setSpawnedNodes] = useState3([]);
1906
- useEffect13(() => {
2314
+ const [spawnedNodes, setSpawnedNodes] = useState4([]);
2315
+ useEffect14(() => {
1907
2316
  if (!engine)
1908
2317
  return;
1909
2318
  const createdEntities = [];
@@ -2094,13 +2503,13 @@ function Tilemap({
2094
2503
  }, [src]);
2095
2504
  if (spawnedNodes.length === 0)
2096
2505
  return null;
2097
- return /* @__PURE__ */ jsxDEV6(Fragment2, {
2506
+ return /* @__PURE__ */ jsxDEV7(Fragment3, {
2098
2507
  children: spawnedNodes
2099
2508
  }, undefined, false, undefined, this);
2100
2509
  }
2101
2510
  // src/components/ParallaxLayer.tsx
2102
- import { useEffect as useEffect14, useContext as useContext13 } from "react";
2103
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
2511
+ import { useEffect as useEffect15, useContext as useContext13 } from "react";
2512
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
2104
2513
  function ParallaxLayerInner({
2105
2514
  src,
2106
2515
  speedX,
@@ -2113,7 +2522,7 @@ function ParallaxLayerInner({
2113
2522
  }) {
2114
2523
  const engine = useContext13(EngineContext);
2115
2524
  const entityId = useContext13(EntityContext);
2116
- useEffect14(() => {
2525
+ useEffect15(() => {
2117
2526
  engine.ecs.addComponent(entityId, {
2118
2527
  type: "ParallaxLayer",
2119
2528
  src,
@@ -2129,7 +2538,7 @@ function ParallaxLayerInner({
2129
2538
  });
2130
2539
  return () => engine.ecs.removeComponent(entityId, "ParallaxLayer");
2131
2540
  }, []);
2132
- useEffect14(() => {
2541
+ useEffect15(() => {
2133
2542
  const layer = engine.ecs.getComponent(entityId, "ParallaxLayer");
2134
2543
  if (!layer)
2135
2544
  return;
@@ -2154,13 +2563,13 @@ function ParallaxLayer({
2154
2563
  offsetX = 0,
2155
2564
  offsetY = 0
2156
2565
  }) {
2157
- return /* @__PURE__ */ jsxDEV7(Entity, {
2566
+ return /* @__PURE__ */ jsxDEV8(Entity, {
2158
2567
  children: [
2159
- /* @__PURE__ */ jsxDEV7(Transform, {
2568
+ /* @__PURE__ */ jsxDEV8(Transform, {
2160
2569
  x: 0,
2161
2570
  y: 0
2162
2571
  }, undefined, false, undefined, this),
2163
- /* @__PURE__ */ jsxDEV7(ParallaxLayerInner, {
2572
+ /* @__PURE__ */ jsxDEV8(ParallaxLayerInner, {
2164
2573
  src,
2165
2574
  speedX,
2166
2575
  speedY,
@@ -2175,7 +2584,7 @@ function ParallaxLayer({
2175
2584
  }
2176
2585
  // src/components/ScreenFlash.tsx
2177
2586
  import { forwardRef, useImperativeHandle, useRef as useRef2 } from "react";
2178
- import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
2587
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
2179
2588
  var ScreenFlash = forwardRef((_, ref) => {
2180
2589
  const divRef = useRef2(null);
2181
2590
  useImperativeHandle(ref, () => ({
@@ -2197,7 +2606,7 @@ var ScreenFlash = forwardRef((_, ref) => {
2197
2606
  });
2198
2607
  }
2199
2608
  }));
2200
- return /* @__PURE__ */ jsxDEV8("div", {
2609
+ return /* @__PURE__ */ jsxDEV9("div", {
2201
2610
  ref: divRef,
2202
2611
  style: {
2203
2612
  position: "absolute",
@@ -2235,7 +2644,7 @@ function useInput() {
2235
2644
  return engine.input;
2236
2645
  }
2237
2646
  // src/hooks/useEvents.ts
2238
- import { useContext as useContext17, useEffect as useEffect15 } from "react";
2647
+ import { useContext as useContext17, useEffect as useEffect16 } from "react";
2239
2648
  function useEvents() {
2240
2649
  const engine = useContext17(EngineContext);
2241
2650
  if (!engine)
@@ -2244,12 +2653,12 @@ function useEvents() {
2244
2653
  }
2245
2654
  function useEvent(event, handler) {
2246
2655
  const events = useEvents();
2247
- useEffect15(() => {
2656
+ useEffect16(() => {
2248
2657
  return events.on(event, handler);
2249
2658
  }, [events, event]);
2250
2659
  }
2251
2660
  // src/hooks/usePlatformerController.ts
2252
- import { useContext as useContext18, useEffect as useEffect16 } from "react";
2661
+ import { useContext as useContext18, useEffect as useEffect17 } from "react";
2253
2662
  function usePlatformerController(entityId, opts = {}) {
2254
2663
  const engine = useContext18(EngineContext);
2255
2664
  const {
@@ -2259,7 +2668,7 @@ function usePlatformerController(entityId, opts = {}) {
2259
2668
  coyoteTime = 0.08,
2260
2669
  jumpBuffer = 0.08
2261
2670
  } = opts;
2262
- useEffect16(() => {
2671
+ useEffect17(() => {
2263
2672
  const state = { coyoteTimer: 0, jumpBuffer: 0, jumpsLeft: maxJumps };
2264
2673
  const updateFn = (id, world2, input, dt) => {
2265
2674
  if (!world2.hasEntity(id))
@@ -2308,11 +2717,11 @@ function usePlatformerController(entityId, opts = {}) {
2308
2717
  }, []);
2309
2718
  }
2310
2719
  // src/hooks/useTopDownMovement.ts
2311
- import { useContext as useContext19, useEffect as useEffect17 } from "react";
2720
+ import { useContext as useContext19, useEffect as useEffect18 } from "react";
2312
2721
  function useTopDownMovement(entityId, opts = {}) {
2313
2722
  const engine = useContext19(EngineContext);
2314
2723
  const { speed = 200, normalizeDiagonal = true } = opts;
2315
- useEffect17(() => {
2724
+ useEffect18(() => {
2316
2725
  const updateFn = (id, world2, input) => {
2317
2726
  if (!world2.hasEntity(id))
2318
2727
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {