cubeforge 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,8 +32,8 @@ npm install cubeforge react react-dom
32
32
  ## What's included
33
33
 
34
34
  - **ECS** — archetype-based entity-component-system with query caching
35
- - **Physics** — two-pass AABB, capsule colliders, kinematic bodies, one-way platforms, 60 Hz fixed timestep
36
- - **Renderer** — WebGL2 instanced renderer by default; Canvas2D opt-in via `renderer={Canvas2DRenderSystem}`
35
+ - **Physics** — two-pass AABB, kinematic bodies, one-way platforms, 60 Hz fixed timestep
36
+ - **Renderer** — WebGL2 instanced renderer by default
37
37
  - **Input** — keyboard, mouse, gamepad, per-player input maps, input contexts, recording/playback
38
38
  - **Audio** — Web Audio API with volume groups, fade, crossfade, ducking (`useSound`)
39
39
  - **Gameplay hooks** — `usePlatformerController`, `useTopDownMovement`, `useHealth`, `useSave`, `useGameStateMachine`, `useLevelTransition`, `usePathfinding`, `useAISteering`, and more
package/dist/index.d.ts CHANGED
@@ -153,8 +153,10 @@ interface RigidBodyProps {
153
153
  lockX?: boolean;
154
154
  /** Prevent any vertical movement — velocity.y is zeroed every frame (disables gravity) */
155
155
  lockY?: boolean;
156
+ /** Enable continuous collision detection to prevent tunneling through thin colliders */
157
+ ccd?: boolean;
156
158
  }
157
- declare function RigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY, }: RigidBodyProps): null;
159
+ declare function RigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY, ccd, }: RigidBodyProps): null;
158
160
 
159
161
  interface BoxColliderProps {
160
162
  width: number;
package/dist/index.js CHANGED
@@ -249,14 +249,16 @@ function findByTag(world, tag) {
249
249
 
250
250
  // ../../packages/core/src/loop/gameLoop.ts
251
251
  var GameLoop = class {
252
- constructor(onTick) {
252
+ constructor(onTick, options) {
253
253
  this.onTick = onTick;
254
+ this.fixedDt = options?.fixedDt;
254
255
  }
255
256
  rafId = 0;
256
257
  lastTime = 0;
257
258
  running = false;
258
259
  paused = false;
259
260
  hitPauseTimer = 0;
261
+ fixedDt;
260
262
  hitPause(duration) {
261
263
  this.hitPauseTimer = duration;
262
264
  }
@@ -292,8 +294,9 @@ var GameLoop = class {
292
294
  }
293
295
  frame = (time) => {
294
296
  if (!this.running) return;
295
- const dt = Math.min((time - this.lastTime) / 1e3, 0.1);
297
+ const rawDt = Math.min((time - this.lastTime) / 1e3, 0.1);
296
298
  this.lastTime = time;
299
+ const dt = this.fixedDt ?? rawDt;
297
300
  if (this.hitPauseTimer > 0) {
298
301
  this.hitPauseTimer -= dt;
299
302
  } else {
@@ -345,6 +348,11 @@ var AssetManager = class {
345
348
  _loaded = 0;
346
349
  _total = 0;
347
350
  _progressListeners = /* @__PURE__ */ new Set();
351
+ /** Base URL prefix applied to all asset paths starting with '/'. Set by Game component. */
352
+ baseURL = "";
353
+ resolve(src) {
354
+ return this.baseURL && src.startsWith("/") ? this.baseURL + src : src;
355
+ }
348
356
  getAudioContext() {
349
357
  if (!this.audioCtx) {
350
358
  this.audioCtx = new AudioContext();
@@ -373,28 +381,29 @@ var AssetManager = class {
373
381
  return () => this._progressListeners.delete(cb);
374
382
  }
375
383
  async loadImage(src) {
376
- if (this.imagePromises.has(src)) return this.imagePromises.get(src);
384
+ const resolved = this.resolve(src);
385
+ if (this.imagePromises.has(resolved)) return this.imagePromises.get(resolved);
377
386
  this._total++;
378
387
  this.emitProgress();
379
388
  const promise = (async () => {
380
389
  const img = new Image();
381
- img.src = src;
390
+ img.src = resolved;
382
391
  try {
383
392
  await new Promise((resolve, reject) => {
384
393
  img.onload = () => resolve();
385
394
  img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
386
395
  });
387
396
  } catch (err) {
388
- console.warn(`[Cubeforge] Failed to load image: ${src}`);
397
+ console.warn(`[Cubeforge] Failed to load image: ${resolved}`);
389
398
  throw err;
390
399
  } finally {
391
400
  this._loaded++;
392
401
  this.emitProgress();
393
402
  }
394
- this.images.set(src, img);
403
+ this.images.set(resolved, img);
395
404
  return img;
396
405
  })();
397
- this.imagePromises.set(src, promise);
406
+ this.imagePromises.set(resolved, promise);
398
407
  return promise;
399
408
  }
400
409
  /** Resolves once every image that has been requested via loadImage() is settled. */
@@ -402,23 +411,24 @@ var AssetManager = class {
402
411
  await Promise.allSettled([...this.imagePromises.values()]);
403
412
  }
404
413
  getImage(src) {
405
- return this.images.get(src);
414
+ return this.images.get(this.resolve(src));
406
415
  }
407
416
  /** Returns a read-only snapshot of all loaded images keyed by src. */
408
417
  getLoadedImages() {
409
418
  return this.images;
410
419
  }
411
420
  async loadAudio(src) {
412
- if (this.audio.has(src)) return this.audio.get(src);
421
+ const resolved = this.resolve(src);
422
+ if (this.audio.has(resolved)) return this.audio.get(resolved);
413
423
  const ctx = this.getAudioContext();
414
424
  try {
415
- const response = await fetch(src);
425
+ const response = await fetch(resolved);
416
426
  const arrayBuffer = await response.arrayBuffer();
417
427
  const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
418
- this.audio.set(src, audioBuffer);
428
+ this.audio.set(resolved, audioBuffer);
419
429
  return audioBuffer;
420
430
  } catch (err) {
421
- console.warn(`[Cubeforge] Failed to load audio: ${src}`);
431
+ console.warn(`[Cubeforge] Failed to load audio: ${resolved}`);
422
432
  throw err;
423
433
  }
424
434
  }
@@ -940,11 +950,16 @@ var Mouse = class {
940
950
  var InputManager = class {
941
951
  keyboard = new Keyboard();
942
952
  mouse = new Mouse();
953
+ _attachedElement = null;
943
954
  attach(canvas) {
955
+ if (this._attachedElement === canvas) return;
956
+ if (this._attachedElement) this.detach();
957
+ this._attachedElement = canvas;
944
958
  this.keyboard.attach(window);
945
959
  this.mouse.attach(canvas);
946
960
  }
947
961
  detach() {
962
+ this._attachedElement = null;
948
963
  this.keyboard.detach();
949
964
  this.mouse.detach();
950
965
  }
@@ -1384,6 +1399,7 @@ function parseCSSColor(css) {
1384
1399
  // ../../packages/renderer/src/webglRenderSystem.ts
1385
1400
  var FLOATS_PER_INSTANCE = 18;
1386
1401
  var MAX_INSTANCES = 8192;
1402
+ var MAX_SPRITE_TEXTURES = 512;
1387
1403
  var MAX_TEXT_CACHE = 200;
1388
1404
  function compileShader(gl, type, src) {
1389
1405
  const shader = gl.createShader(type);
@@ -1564,6 +1580,8 @@ var RenderSystem = class {
1564
1580
  instanceData;
1565
1581
  whiteTexture;
1566
1582
  textures = /* @__PURE__ */ new Map();
1583
+ /** Tracks texture access order for LRU eviction (most recent at end). */
1584
+ textureLRU = [];
1567
1585
  imageCache = /* @__PURE__ */ new Map();
1568
1586
  // Cached uniform locations — sprite program
1569
1587
  uCamPos;
@@ -1596,6 +1614,21 @@ var RenderSystem = class {
1596
1614
  getDefaultSampling() {
1597
1615
  return this._defaultSampling;
1598
1616
  }
1617
+ // ── Sprite texture LRU helpers ──────────────────────────────────────────
1618
+ /** Record a texture key as recently used; evict LRU entries if over limit. */
1619
+ touchTexture(key) {
1620
+ const idx = this.textureLRU.indexOf(key);
1621
+ if (idx !== -1) this.textureLRU.splice(idx, 1);
1622
+ this.textureLRU.push(key);
1623
+ while (this.textureLRU.length > MAX_SPRITE_TEXTURES) {
1624
+ const evict = this.textureLRU.shift();
1625
+ const tex = this.textures.get(evict);
1626
+ if (tex) {
1627
+ this.gl.deleteTexture(tex);
1628
+ this.textures.delete(evict);
1629
+ }
1630
+ }
1631
+ }
1599
1632
  // ── Debug overlays ──────────────────────────────────────────────────────
1600
1633
  debugNavGrid = null;
1601
1634
  contactFlashPoints = [];
@@ -1613,7 +1646,10 @@ var RenderSystem = class {
1613
1646
  // ── Texture management (sprite textures — CLAMP_TO_EDGE) ──────────────────
1614
1647
  loadTexture(src) {
1615
1648
  const cached = this.textures.get(src);
1616
- if (cached) return cached;
1649
+ if (cached) {
1650
+ this.touchTexture(src);
1651
+ return cached;
1652
+ }
1617
1653
  let imgSrc = src;
1618
1654
  const sampIdx = imgSrc.indexOf(":s=");
1619
1655
  if (sampIdx !== -1) imgSrc = imgSrc.slice(0, sampIdx);
@@ -1629,6 +1665,7 @@ var RenderSystem = class {
1629
1665
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1630
1666
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1631
1667
  this.textures.set(src, tex);
1668
+ this.touchTexture(src);
1632
1669
  return tex;
1633
1670
  }
1634
1671
  if (!existing) {
@@ -1646,6 +1683,11 @@ var RenderSystem = class {
1646
1683
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
1647
1684
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
1648
1685
  this.textures.set(src, tex);
1686
+ this.touchTexture(src);
1687
+ };
1688
+ img.onerror = () => {
1689
+ console.warn(`[WebGLRenderSystem] Failed to load image: ${imgSrc}`);
1690
+ this.imageCache.delete(imgSrc);
1649
1691
  };
1650
1692
  this.imageCache.set(imgSrc, img);
1651
1693
  }
@@ -1936,6 +1978,9 @@ var RenderSystem = class {
1936
1978
  gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_S, wrap);
1937
1979
  gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_T, wrap);
1938
1980
  this.textures.set(cacheKey, tex);
1981
+ this.touchTexture(cacheKey);
1982
+ } else if (cacheKey) {
1983
+ this.touchTexture(cacheKey);
1939
1984
  }
1940
1985
  } else if (sprite.src && !sprite.image) {
1941
1986
  let img = this.imageCache.get(sprite.src);
@@ -1951,6 +1996,11 @@ var RenderSystem = class {
1951
1996
  gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MIN_FILTER, gl2.NEAREST);
1952
1997
  gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MAG_FILTER, gl2.NEAREST);
1953
1998
  this.textures.set(img.src, tex);
1999
+ this.touchTexture(img.src);
2000
+ };
2001
+ img.onerror = () => {
2002
+ console.warn(`[WebGLRenderSystem] Failed to load image: ${img.src}`);
2003
+ this.imageCache.delete(img.src);
1954
2004
  };
1955
2005
  }
1956
2006
  sprite.image = img;
@@ -2259,6 +2309,7 @@ function createRigidBody(opts) {
2259
2309
  lockY: false,
2260
2310
  isKinematic: false,
2261
2311
  dropThrough: 0,
2312
+ ccd: false,
2262
2313
  ...opts
2263
2314
  };
2264
2315
  }
@@ -2330,6 +2381,14 @@ function getAABB(transform, collider) {
2330
2381
  hh: collider.height / 2
2331
2382
  };
2332
2383
  }
2384
+ function getCapsuleAABB(transform, capsule) {
2385
+ return {
2386
+ cx: transform.x + capsule.offsetX,
2387
+ cy: transform.y + capsule.offsetY,
2388
+ hw: capsule.width / 2,
2389
+ hh: capsule.height / 2
2390
+ };
2391
+ }
2333
2392
  function getOverlap(a, b) {
2334
2393
  const dx = a.cx - b.cx;
2335
2394
  const dy = a.cy - b.cy;
@@ -2432,6 +2491,36 @@ function getCompoundBounds(tx, ty, shapes) {
2432
2491
  function canInteractGeneric(aLayer, aMask, bLayer, bMask) {
2433
2492
  return maskAllows(aMask, bLayer) && maskAllows(bMask, aLayer);
2434
2493
  }
2494
+ function sweepAABB(aCx, aCy, aHw, aHh, dx, dy, b) {
2495
+ const eHw = b.hw + aHw;
2496
+ const eHh = b.hh + aHh;
2497
+ const left = b.cx - eHw;
2498
+ const right = b.cx + eHw;
2499
+ const top = b.cy - eHh;
2500
+ const bottom = b.cy + eHh;
2501
+ let tmin = -Infinity;
2502
+ let tmax = Infinity;
2503
+ if (dx !== 0) {
2504
+ const t1 = (left - aCx) / dx;
2505
+ const t2 = (right - aCx) / dx;
2506
+ tmin = Math.max(tmin, Math.min(t1, t2));
2507
+ tmax = Math.min(tmax, Math.max(t1, t2));
2508
+ } else if (aCx < left || aCx > right) {
2509
+ return null;
2510
+ }
2511
+ if (dy !== 0) {
2512
+ const t1 = (top - aCy) / dy;
2513
+ const t2 = (bottom - aCy) / dy;
2514
+ tmin = Math.max(tmin, Math.min(t1, t2));
2515
+ tmax = Math.min(tmax, Math.max(t1, t2));
2516
+ } else if (aCy < top || aCy > bottom) {
2517
+ return null;
2518
+ }
2519
+ if (tmax < 0 || tmin > tmax || tmin > 1) return null;
2520
+ const t = Math.max(0, tmin);
2521
+ if (t > 1) return null;
2522
+ return t;
2523
+ }
2435
2524
  function pairKey(a, b) {
2436
2525
  return a < b ? `${a}:${b}` : `${b}:${a}`;
2437
2526
  }
@@ -2442,11 +2531,15 @@ var PhysicsSystem = class {
2442
2531
  }
2443
2532
  accumulator = 0;
2444
2533
  FIXED_DT = 1 / 60;
2534
+ /** Maximum accumulated time (seconds). Prevents hundreds of sub-steps when
2535
+ * the tab is backgrounded and dt spikes on resume. 0.1s ≈ 6 steps at 60Hz. */
2536
+ MAX_ACCUMULATOR = 0.1;
2445
2537
  // Active contact sets — updated each physics step.
2446
2538
  activeTriggerPairs = /* @__PURE__ */ new Map();
2447
2539
  activeCollisionPairs = /* @__PURE__ */ new Map();
2448
2540
  activeCirclePairs = /* @__PURE__ */ new Map();
2449
2541
  activeCompoundPairs = /* @__PURE__ */ new Map();
2542
+ activeCapsulePairs = /* @__PURE__ */ new Map();
2450
2543
  // Previous-frame positions of static entities — used to compute platform carry delta.
2451
2544
  staticPrevPos = /* @__PURE__ */ new Map();
2452
2545
  setGravity(g) {
@@ -2454,8 +2547,8 @@ var PhysicsSystem = class {
2454
2547
  }
2455
2548
  update(world, dt) {
2456
2549
  this.accumulator += dt;
2457
- if (this.accumulator > 5 * this.FIXED_DT) {
2458
- this.accumulator = 5 * this.FIXED_DT;
2550
+ if (this.accumulator > this.MAX_ACCUMULATOR) {
2551
+ this.accumulator = this.MAX_ACCUMULATOR;
2459
2552
  }
2460
2553
  while (this.accumulator >= this.FIXED_DT) {
2461
2554
  this.step(world, this.FIXED_DT);
@@ -2483,6 +2576,12 @@ var PhysicsSystem = class {
2483
2576
  if (rb.isStatic) statics.push(id);
2484
2577
  else dynamics.push(id);
2485
2578
  }
2579
+ const allCapsule = world.query("Transform", "RigidBody", "CapsuleCollider");
2580
+ const capsuleDynamics = [];
2581
+ for (const id of allCapsule) {
2582
+ const rb = world.getComponent(id, "RigidBody");
2583
+ if (!rb.isStatic) capsuleDynamics.push(id);
2584
+ }
2486
2585
  for (const [key, [a, b]] of this.activeTriggerPairs) {
2487
2586
  if (!world.hasEntity(a) || !world.hasEntity(b)) {
2488
2587
  this.events?.emit("triggerExit", { a, b });
@@ -2507,6 +2606,12 @@ var PhysicsSystem = class {
2507
2606
  this.activeCompoundPairs.delete(key);
2508
2607
  }
2509
2608
  }
2609
+ for (const [key, [a, b]] of this.activeCapsulePairs) {
2610
+ if (!world.hasEntity(a) || !world.hasEntity(b)) {
2611
+ this.events?.emit("capsuleExit", { a, b });
2612
+ this.activeCapsulePairs.delete(key);
2613
+ }
2614
+ }
2510
2615
  const staticDelta = /* @__PURE__ */ new Map();
2511
2616
  for (const sid of statics) {
2512
2617
  const st = world.getComponent(sid, "Transform");
@@ -2541,6 +2646,24 @@ var PhysicsSystem = class {
2541
2646
  if (rb.lockY) rb.vy = 0;
2542
2647
  if (rb.dropThrough > 0) rb.dropThrough--;
2543
2648
  }
2649
+ const ccdPrev = /* @__PURE__ */ new Map();
2650
+ for (const id of dynamics) {
2651
+ const rb = world.getComponent(id, "RigidBody");
2652
+ if (rb.ccd) {
2653
+ const t = world.getComponent(id, "Transform");
2654
+ ccdPrev.set(id, { x: t.x, y: t.y });
2655
+ }
2656
+ }
2657
+ for (const id of capsuleDynamics) {
2658
+ const rb = world.getComponent(id, "RigidBody");
2659
+ rb.onGround = false;
2660
+ rb.isNearGround = false;
2661
+ if (rb.isKinematic) continue;
2662
+ if (!rb.lockY) rb.vy += this.gravity * rb.gravityScale * dt;
2663
+ if (rb.lockX) rb.vx = 0;
2664
+ if (rb.lockY) rb.vy = 0;
2665
+ if (rb.dropThrough > 0) rb.dropThrough--;
2666
+ }
2544
2667
  for (const id of dynamics) {
2545
2668
  const transform = world.getComponent(id, "Transform");
2546
2669
  const rb = world.getComponent(id, "RigidBody");
@@ -2571,6 +2694,36 @@ var PhysicsSystem = class {
2571
2694
  }
2572
2695
  }
2573
2696
  }
2697
+ for (const id of capsuleDynamics) {
2698
+ const transform = world.getComponent(id, "Transform");
2699
+ const rb = world.getComponent(id, "RigidBody");
2700
+ const cap = world.getComponent(id, "CapsuleCollider");
2701
+ transform.x += rb.vx * dt;
2702
+ if (!cap.isTrigger) {
2703
+ const dynAABB = getCapsuleAABB(transform, cap);
2704
+ const candidateCells = this.getCells(dynAABB.cx, dynAABB.cy, dynAABB.hw, dynAABB.hh);
2705
+ const checked = /* @__PURE__ */ new Set();
2706
+ for (const cell of candidateCells) {
2707
+ const bucket = staticGrid.get(cell);
2708
+ if (!bucket) continue;
2709
+ for (const sid of bucket) {
2710
+ if (checked.has(sid)) continue;
2711
+ checked.add(sid);
2712
+ const st = world.getComponent(sid, "Transform");
2713
+ const sc = world.getComponent(sid, "BoxCollider");
2714
+ if (sc.isTrigger) continue;
2715
+ if (sc.slope !== 0) continue;
2716
+ if (!canInteractGeneric(cap.layer, cap.mask, sc.layer, sc.mask)) continue;
2717
+ const ov = getOverlap(getCapsuleAABB(transform, cap), getAABB(st, sc));
2718
+ if (!ov) continue;
2719
+ if (Math.abs(ov.x) < Math.abs(ov.y)) {
2720
+ transform.x += ov.x;
2721
+ rb.vx = rb.bounce > 0 ? -rb.vx * rb.bounce : 0;
2722
+ }
2723
+ }
2724
+ }
2725
+ }
2726
+ }
2574
2727
  for (const id of dynamics) {
2575
2728
  const transform = world.getComponent(id, "Transform");
2576
2729
  const rb = world.getComponent(id, "RigidBody");
@@ -2630,6 +2783,127 @@ var PhysicsSystem = class {
2630
2783
  }
2631
2784
  }
2632
2785
  }
2786
+ for (const [id, prev] of ccdPrev) {
2787
+ const transform = world.getComponent(id, "Transform");
2788
+ const rb = world.getComponent(id, "RigidBody");
2789
+ const col = world.getComponent(id, "BoxCollider");
2790
+ const totalDx = transform.x - prev.x;
2791
+ const totalDy = transform.y - prev.y;
2792
+ const moveLen = Math.abs(totalDx) + Math.abs(totalDy);
2793
+ const halfSize = Math.min(col.width, col.height) / 2;
2794
+ if (moveLen <= halfSize) continue;
2795
+ const startCx = prev.x + col.offsetX;
2796
+ const startCy = prev.y + col.offsetY;
2797
+ const sweepDx = totalDx;
2798
+ const sweepDy = totalDy;
2799
+ const hw = col.width / 2;
2800
+ const hh = col.height / 2;
2801
+ let earliestT = 1;
2802
+ let hitSid = null;
2803
+ const endCx = startCx + sweepDx;
2804
+ const endCy = startCy + sweepDy;
2805
+ const minCx = Math.min(startCx, endCx);
2806
+ const maxCx = Math.max(startCx, endCx);
2807
+ const minCy = Math.min(startCy, endCy);
2808
+ const maxCy = Math.max(startCy, endCy);
2809
+ const sweepCells = this.getCells(
2810
+ (minCx + maxCx) / 2,
2811
+ (minCy + maxCy) / 2,
2812
+ (maxCx - minCx) / 2 + hw,
2813
+ (maxCy - minCy) / 2 + hh
2814
+ );
2815
+ const checked = /* @__PURE__ */ new Set();
2816
+ for (const cell of sweepCells) {
2817
+ const bucket = staticGrid.get(cell);
2818
+ if (!bucket) continue;
2819
+ for (const sid of bucket) {
2820
+ if (checked.has(sid)) continue;
2821
+ checked.add(sid);
2822
+ const st = world.getComponent(sid, "Transform");
2823
+ const sc = world.getComponent(sid, "BoxCollider");
2824
+ if (sc.isTrigger) continue;
2825
+ if (!canInteract(col, sc)) continue;
2826
+ const staticAABB = getAABB(st, sc);
2827
+ const t = sweepAABB(startCx, startCy, hw, hh, sweepDx, sweepDy, staticAABB);
2828
+ if (t !== null && t < earliestT) {
2829
+ earliestT = t;
2830
+ hitSid = sid;
2831
+ }
2832
+ }
2833
+ }
2834
+ if (hitSid !== null && earliestT < 1) {
2835
+ const eps = 0.01;
2836
+ const clampedT = Math.max(0, earliestT - eps / (Math.hypot(sweepDx, sweepDy) || 1));
2837
+ transform.x = prev.x + totalDx * clampedT;
2838
+ transform.y = prev.y + totalDy * clampedT;
2839
+ const st = world.getComponent(hitSid, "Transform");
2840
+ const sc = world.getComponent(hitSid, "BoxCollider");
2841
+ const staticAABB = getAABB(st, sc);
2842
+ const contactCx = prev.x + col.offsetX + totalDx * earliestT;
2843
+ const contactCy = prev.y + col.offsetY + totalDy * earliestT;
2844
+ const dxFromCenter = contactCx - staticAABB.cx;
2845
+ const dyFromCenter = contactCy - staticAABB.cy;
2846
+ const overlapX = hw + staticAABB.hw - Math.abs(dxFromCenter);
2847
+ const overlapY = hh + staticAABB.hh - Math.abs(dyFromCenter);
2848
+ if (overlapX > overlapY) {
2849
+ rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
2850
+ if (dyFromCenter < 0) {
2851
+ rb.onGround = true;
2852
+ if (rb.friction < 1) rb.vx *= rb.friction;
2853
+ }
2854
+ } else {
2855
+ rb.vx = rb.bounce > 0 ? -rb.vx * rb.bounce : 0;
2856
+ }
2857
+ }
2858
+ }
2859
+ for (const id of capsuleDynamics) {
2860
+ const transform = world.getComponent(id, "Transform");
2861
+ const rb = world.getComponent(id, "RigidBody");
2862
+ const cap = world.getComponent(id, "CapsuleCollider");
2863
+ transform.y += rb.vy * dt;
2864
+ if (!cap.isTrigger) {
2865
+ const dynAABB = getCapsuleAABB(transform, cap);
2866
+ const candidateCells = this.getCells(dynAABB.cx, dynAABB.cy, dynAABB.hw, dynAABB.hh);
2867
+ const checked = /* @__PURE__ */ new Set();
2868
+ for (const cell of candidateCells) {
2869
+ const bucket = staticGrid.get(cell);
2870
+ if (!bucket) continue;
2871
+ for (const sid of bucket) {
2872
+ if (checked.has(sid)) continue;
2873
+ checked.add(sid);
2874
+ const st = world.getComponent(sid, "Transform");
2875
+ const sc = world.getComponent(sid, "BoxCollider");
2876
+ if (sc.isTrigger) continue;
2877
+ if (!canInteractGeneric(cap.layer, cap.mask, sc.layer, sc.mask)) continue;
2878
+ if (sc.slope !== 0) continue;
2879
+ const ov = getOverlap(getCapsuleAABB(transform, cap), getAABB(st, sc));
2880
+ if (!ov) continue;
2881
+ if (Math.abs(ov.y) <= Math.abs(ov.x)) {
2882
+ if (sc.oneWay) {
2883
+ if (rb.dropThrough > 0) continue;
2884
+ if (ov.y >= 0) continue;
2885
+ const platformTop = st.y + sc.offsetY - sc.height / 2;
2886
+ const prevEntityBottom = transform.y - rb.vy * dt + cap.offsetY + cap.height / 2;
2887
+ if (prevEntityBottom > platformTop) continue;
2888
+ }
2889
+ transform.y += ov.y;
2890
+ if (ov.y < 0) {
2891
+ rb.onGround = true;
2892
+ if (rb.friction < 1) rb.vx *= rb.friction;
2893
+ const delta = staticDelta.get(sid);
2894
+ if (delta) {
2895
+ transform.x += delta.dx;
2896
+ if (delta.dy < 0) transform.y += delta.dy;
2897
+ }
2898
+ }
2899
+ rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
2900
+ }
2901
+ }
2902
+ }
2903
+ }
2904
+ }
2905
+ const POSITION_SLOP = 0.5;
2906
+ const CORRECTION_FACTOR = 0.4;
2633
2907
  const currentCollisionPairs = /* @__PURE__ */ new Map();
2634
2908
  for (let i = 0; i < dynamics.length; i++) {
2635
2909
  for (let j = i + 1; j < dynamics.length; j++) {
@@ -2660,10 +2934,14 @@ var PhysicsSystem = class {
2660
2934
  rba.onGround = true;
2661
2935
  }
2662
2936
  }
2663
- ta.x += ov.x / 2;
2664
- ta.y += ov.y / 2;
2665
- tb.x -= ov.x / 2;
2666
- tb.y -= ov.y / 2;
2937
+ const absOx = Math.abs(ov.x);
2938
+ const absOy = Math.abs(ov.y);
2939
+ const corrX = absOx > POSITION_SLOP ? Math.sign(ov.x) * (absOx - POSITION_SLOP) * CORRECTION_FACTOR : 0;
2940
+ const corrY = absOy > POSITION_SLOP ? Math.sign(ov.y) * (absOy - POSITION_SLOP) * CORRECTION_FACTOR : 0;
2941
+ ta.x += corrX / 2;
2942
+ ta.y += corrY / 2;
2943
+ tb.x -= corrX / 2;
2944
+ tb.y -= corrY / 2;
2667
2945
  const key = pairKey(ia, ib);
2668
2946
  currentCollisionPairs.set(key, [ia, ib]);
2669
2947
  }
@@ -2716,6 +2994,40 @@ var PhysicsSystem = class {
2716
2994
  }
2717
2995
  }
2718
2996
  }
2997
+ for (const id of capsuleDynamics) {
2998
+ const rb = world.getComponent(id, "RigidBody");
2999
+ if (rb.onGround) {
3000
+ rb.isNearGround = true;
3001
+ continue;
3002
+ }
3003
+ const transform = world.getComponent(id, "Transform");
3004
+ const cap = world.getComponent(id, "CapsuleCollider");
3005
+ const probeAABB = {
3006
+ cx: transform.x + cap.offsetX,
3007
+ cy: transform.y + cap.offsetY + 2,
3008
+ hw: cap.width / 2,
3009
+ hh: cap.height / 2
3010
+ };
3011
+ const candidateCells = this.getCells(probeAABB.cx, probeAABB.cy, probeAABB.hw, probeAABB.hh);
3012
+ const checked = /* @__PURE__ */ new Set();
3013
+ outerCapsule: for (const cell of candidateCells) {
3014
+ const bucket = staticGrid.get(cell);
3015
+ if (!bucket) continue;
3016
+ for (const sid of bucket) {
3017
+ if (checked.has(sid)) continue;
3018
+ checked.add(sid);
3019
+ const st = world.getComponent(sid, "Transform");
3020
+ const sc = world.getComponent(sid, "BoxCollider");
3021
+ if (sc.isTrigger) continue;
3022
+ if (!canInteractGeneric(cap.layer, cap.mask, sc.layer, sc.mask)) continue;
3023
+ const ov = getOverlap(probeAABB, getAABB(st, sc));
3024
+ if (ov && Math.abs(ov.y) <= Math.abs(ov.x) && ov.y < 0) {
3025
+ rb.isNearGround = true;
3026
+ break outerCapsule;
3027
+ }
3028
+ }
3029
+ }
3030
+ }
2719
3031
  const allWithCollider = world.query("Transform", "BoxCollider");
2720
3032
  const currentTriggerPairs = /* @__PURE__ */ new Map();
2721
3033
  for (let i = 0; i < allWithCollider.length; i++) {
@@ -2904,6 +3216,55 @@ var PhysicsSystem = class {
2904
3216
  }
2905
3217
  this.activeCompoundPairs = /* @__PURE__ */ new Map();
2906
3218
  }
3219
+ if (allCapsule.length > 0) {
3220
+ const currentCapsulePairs = /* @__PURE__ */ new Map();
3221
+ const allBoxForCapsule = world.query("Transform", "BoxCollider");
3222
+ for (const cid of allCapsule) {
3223
+ const cc = world.getComponent(cid, "CapsuleCollider");
3224
+ const ct = world.getComponent(cid, "Transform");
3225
+ const capsuleAABB = getCapsuleAABB(ct, cc);
3226
+ for (const bid of allBoxForCapsule) {
3227
+ if (bid === cid) continue;
3228
+ const bc = world.getComponent(bid, "BoxCollider");
3229
+ if (!canInteractGeneric(cc.layer, cc.mask, bc.layer, bc.mask)) continue;
3230
+ const bt = world.getComponent(bid, "Transform");
3231
+ const ov = getOverlap(capsuleAABB, getAABB(bt, bc));
3232
+ if (ov) currentCapsulePairs.set(pairKey(cid, bid), [cid, bid]);
3233
+ }
3234
+ }
3235
+ for (let i = 0; i < allCapsule.length; i++) {
3236
+ for (let j = i + 1; j < allCapsule.length; j++) {
3237
+ const ia = allCapsule[i];
3238
+ const ib = allCapsule[j];
3239
+ const ca = world.getComponent(ia, "CapsuleCollider");
3240
+ const cb = world.getComponent(ib, "CapsuleCollider");
3241
+ if (!canInteractGeneric(ca.layer, ca.mask, cb.layer, cb.mask)) continue;
3242
+ const ta = world.getComponent(ia, "Transform");
3243
+ const tb = world.getComponent(ib, "Transform");
3244
+ const ov = getOverlap(getCapsuleAABB(ta, ca), getCapsuleAABB(tb, cb));
3245
+ if (ov) currentCapsulePairs.set(pairKey(ia, ib), [ia, ib]);
3246
+ }
3247
+ }
3248
+ for (const [key, [a, b]] of currentCapsulePairs) {
3249
+ if (!this.activeCapsulePairs.has(key)) {
3250
+ this.events?.emit("capsuleEnter", { a, b });
3251
+ } else {
3252
+ this.events?.emit("capsuleStay", { a, b });
3253
+ }
3254
+ this.events?.emit("capsule", { a, b });
3255
+ }
3256
+ for (const [key, [a, b]] of this.activeCapsulePairs) {
3257
+ if (!currentCapsulePairs.has(key)) {
3258
+ this.events?.emit("capsuleExit", { a, b });
3259
+ }
3260
+ }
3261
+ this.activeCapsulePairs = currentCapsulePairs;
3262
+ } else if (this.activeCapsulePairs.size > 0) {
3263
+ for (const [, [a, b]] of this.activeCapsulePairs) {
3264
+ this.events?.emit("capsuleExit", { a, b });
3265
+ }
3266
+ this.activeCapsulePairs = /* @__PURE__ */ new Map();
3267
+ }
2907
3268
  }
2908
3269
  };
2909
3270
 
@@ -3886,6 +4247,8 @@ function Game({
3886
4247
  const input = new InputManager();
3887
4248
  const events = new EventBus();
3888
4249
  const assets = new AssetManager();
4250
+ const viteEnv = import.meta.env;
4251
+ assets.baseURL = (viteEnv?.BASE_URL ?? "/").replace(/\/$/, "");
3889
4252
  ecs.assets = assets;
3890
4253
  const physics = new PhysicsSystem(gravity, events);
3891
4254
  const entityIds = /* @__PURE__ */ new Map();
@@ -3919,7 +4282,7 @@ function Game({
3919
4282
  if (handle.buffer.length > MAX_DEVTOOLS_FRAMES) handle.buffer.shift();
3920
4283
  handle.onFrame?.();
3921
4284
  }
3922
- });
4285
+ }, deterministic ? { fixedDt: 1 / 60 } : void 0);
3923
4286
  const state = {
3924
4287
  ecs,
3925
4288
  input,
@@ -4010,6 +4373,12 @@ function Game({
4010
4373
  cancelled = true;
4011
4374
  };
4012
4375
  }, [engine]);
4376
+ useEffect3(() => {
4377
+ if (!engine) return;
4378
+ const canvas = engine.canvas;
4379
+ if (canvas.width !== width) canvas.width = width;
4380
+ if (canvas.height !== height) canvas.height = height;
4381
+ }, [width, height, engine]);
4013
4382
  useEffect3(() => {
4014
4383
  engine?.physics.setGravity(gravity);
4015
4384
  }, [gravity, engine]);
@@ -4130,7 +4499,7 @@ function Entity({ id, tags = [], children }) {
4130
4499
  setEntityId(eid);
4131
4500
  return () => {
4132
4501
  engine.ecs.destroyEntity(eid);
4133
- if (id) engine.entityIds.delete(id);
4502
+ if (id && engine.entityIds.get(id) === eid) engine.entityIds.delete(id);
4134
4503
  };
4135
4504
  }, []);
4136
4505
  if (entityId === null) return null;
@@ -4213,13 +4582,12 @@ function Sprite({
4213
4582
  });
4214
4583
  engine.ecs.addComponent(entityId, comp);
4215
4584
  if (src) {
4216
- const viteEnv = import.meta.env;
4217
- const base = (viteEnv?.BASE_URL ?? "/").replace(/\/$/, "");
4218
- const resolvedSrc = base && src.startsWith("/") ? base + src : src;
4219
- comp.src = resolvedSrc;
4220
- engine.assets.loadImage(resolvedSrc).then((img) => {
4585
+ engine.assets.loadImage(src).then((img) => {
4221
4586
  const c = engine.ecs.getComponent(entityId, "Sprite");
4222
- if (c) c.image = img;
4587
+ if (c) {
4588
+ c.image = img;
4589
+ c.src = img.src;
4590
+ }
4223
4591
  }).catch(console.error);
4224
4592
  }
4225
4593
  return () => engine.ecs.removeComponent(entityId, "Sprite");
@@ -4293,12 +4661,13 @@ function RigidBody({
4293
4661
  vx = 0,
4294
4662
  vy = 0,
4295
4663
  lockX = false,
4296
- lockY = false
4664
+ lockY = false,
4665
+ ccd = false
4297
4666
  }) {
4298
4667
  const engine = useContext7(EngineContext);
4299
4668
  const entityId = useContext7(EntityContext);
4300
4669
  useEffect9(() => {
4301
- engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY }));
4670
+ engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY, ccd }));
4302
4671
  return () => engine.ecs.removeComponent(entityId, "RigidBody");
4303
4672
  }, []);
4304
4673
  return null;
@@ -4398,19 +4767,24 @@ function CompoundCollider({
4398
4767
  }
4399
4768
 
4400
4769
  // src/components/Script.tsx
4401
- import { useEffect as useEffect14, useContext as useContext12 } from "react";
4770
+ import { useEffect as useEffect14, useContext as useContext12, useRef as useRef4 } from "react";
4402
4771
  function Script({ init, update }) {
4403
4772
  const engine = useContext12(EngineContext);
4404
4773
  const entityId = useContext12(EntityContext);
4774
+ const initRef = useRef4(init);
4775
+ initRef.current = init;
4776
+ const updateRef = useRef4(update);
4777
+ updateRef.current = update;
4405
4778
  useEffect14(() => {
4406
- if (init) {
4779
+ if (initRef.current) {
4407
4780
  try {
4408
- init(entityId, engine.ecs);
4781
+ initRef.current(entityId, engine.ecs);
4409
4782
  } catch (err) {
4410
4783
  console.error(`[Cubeforge] Script init error on entity ${entityId}:`, err);
4411
4784
  }
4412
4785
  }
4413
- engine.ecs.addComponent(entityId, createScript(update));
4786
+ const stableUpdate = (id, world, input, dt) => updateRef.current(id, world, input, dt);
4787
+ engine.ecs.addComponent(entityId, createScript(stableUpdate));
4414
4788
  return () => engine.ecs.removeComponent(entityId, "Script");
4415
4789
  }, []);
4416
4790
  return null;
@@ -4640,7 +5014,7 @@ function ParticleEmitter({
4640
5014
  }
4641
5015
 
4642
5016
  // src/components/VirtualJoystick.tsx
4643
- import { useRef as useRef4 } from "react";
5017
+ import { useRef as useRef5 } from "react";
4644
5018
 
4645
5019
  // src/hooks/useVirtualInput.ts
4646
5020
  var _axes = { x: 0, y: 0 };
@@ -4674,10 +5048,10 @@ function VirtualJoystick({
4674
5048
  actionLabel = "A",
4675
5049
  actionName = "action"
4676
5050
  }) {
4677
- const baseRef = useRef4(null);
4678
- const stickRef = useRef4(null);
4679
- const activePtr = useRef4(null);
4680
- const baseCenterRef = useRef4({ x: 0, y: 0 });
5051
+ const baseRef = useRef5(null);
5052
+ const stickRef = useRef5(null);
5053
+ const activePtr = useRef5(null);
5054
+ const baseCenterRef = useRef5({ x: 0, y: 0 });
4681
5055
  const radius = size / 2 - 16;
4682
5056
  const applyStickPosition = (dx, dy) => {
4683
5057
  if (!stickRef.current) return;
@@ -5156,10 +5530,10 @@ function ParallaxLayer({
5156
5530
  }
5157
5531
 
5158
5532
  // src/components/ScreenFlash.tsx
5159
- import { forwardRef, useImperativeHandle, useRef as useRef5 } from "react";
5533
+ import { forwardRef, useImperativeHandle, useRef as useRef6 } from "react";
5160
5534
  import { jsx as jsx10 } from "react/jsx-runtime";
5161
5535
  var ScreenFlash = forwardRef((_, ref) => {
5162
- const divRef = useRef5(null);
5536
+ const divRef = useRef6(null);
5163
5537
  useImperativeHandle(ref, () => ({
5164
5538
  flash(color, duration) {
5165
5539
  const el = divRef.current;
@@ -5195,7 +5569,7 @@ var ScreenFlash = forwardRef((_, ref) => {
5195
5569
  ScreenFlash.displayName = "ScreenFlash";
5196
5570
 
5197
5571
  // src/components/CameraZone.tsx
5198
- import { useEffect as useEffect21, useContext as useContext19, useRef as useRef6 } from "react";
5572
+ import { useEffect as useEffect21, useContext as useContext19, useRef as useRef7 } from "react";
5199
5573
  import { Fragment as Fragment5, jsx as jsx11 } from "react/jsx-runtime";
5200
5574
  function CameraZone({
5201
5575
  x,
@@ -5208,8 +5582,8 @@ function CameraZone({
5208
5582
  children
5209
5583
  }) {
5210
5584
  const engine = useContext19(EngineContext);
5211
- const prevFollowRef = useRef6(void 0);
5212
- const activeRef = useRef6(false);
5585
+ const prevFollowRef = useRef7(void 0);
5586
+ const activeRef = useRef7(false);
5213
5587
  useEffect21(() => {
5214
5588
  const eid = engine.ecs.createEntity();
5215
5589
  engine.ecs.addComponent(eid, createScript(() => {
@@ -5450,7 +5824,7 @@ function useInputMap(bindings) {
5450
5824
  }
5451
5825
 
5452
5826
  // src/hooks/useEvents.ts
5453
- import { useContext as useContext27, useEffect as useEffect26, useRef as useRef7 } from "react";
5827
+ import { useContext as useContext27, useEffect as useEffect26, useRef as useRef8 } from "react";
5454
5828
  function useEvents() {
5455
5829
  const engine = useContext27(EngineContext);
5456
5830
  if (!engine) throw new Error("useEvents must be used inside <Game>");
@@ -5458,7 +5832,7 @@ function useEvents() {
5458
5832
  }
5459
5833
  function useEvent(event, handler) {
5460
5834
  const events = useEvents();
5461
- const handlerRef = useRef7(handler);
5835
+ const handlerRef = useRef8(handler);
5462
5836
  handlerRef.current = handler;
5463
5837
  useEffect26(() => {
5464
5838
  return events.on(event, (data) => handlerRef.current(data));
@@ -5536,11 +5910,11 @@ function useInputRecorder() {
5536
5910
  }
5537
5911
 
5538
5912
  // src/hooks/useGamepad.ts
5539
- import { useEffect as useEffect28, useRef as useRef8, useState as useState7 } from "react";
5913
+ import { useEffect as useEffect28, useRef as useRef9, useState as useState7 } from "react";
5540
5914
  var EMPTY_STATE = { connected: false, axes: [], buttons: [] };
5541
5915
  function useGamepad(playerIndex = 0) {
5542
5916
  const [state, setState] = useState7(EMPTY_STATE);
5543
- const rafRef = useRef8(0);
5917
+ const rafRef = useRef9(0);
5544
5918
  useEffect28(() => {
5545
5919
  const poll = () => {
5546
5920
  const gp = navigator.getGamepads()[playerIndex];
@@ -5654,12 +6028,12 @@ function useDropThrough(frames = 8) {
5654
6028
  }
5655
6029
 
5656
6030
  // ../gameplay/src/hooks/useGameStateMachine.ts
5657
- import { useState as useState10, useRef as useRef9, useCallback as useCallback8, useEffect as useEffect29, useContext as useContext32 } from "react";
6031
+ import { useState as useState10, useRef as useRef10, useCallback as useCallback8, useEffect as useEffect29, useContext as useContext32 } from "react";
5658
6032
  function useGameStateMachine(states, initial) {
5659
6033
  const engine = useContext32(EngineContext);
5660
6034
  const [state, setState] = useState10(initial);
5661
- const stateRef = useRef9(initial);
5662
- const statesRef = useRef9(states);
6035
+ const stateRef = useRef10(initial);
6036
+ const statesRef = useRef10(states);
5663
6037
  statesRef.current = states;
5664
6038
  useEffect29(() => {
5665
6039
  statesRef.current[initial]?.onEnter?.();
@@ -5685,22 +6059,22 @@ function useGameStateMachine(states, initial) {
5685
6059
  }
5686
6060
 
5687
6061
  // ../gameplay/src/hooks/useHealth.ts
5688
- import { useRef as useRef10, useEffect as useEffect30, useContext as useContext33, useCallback as useCallback9 } from "react";
6062
+ import { useRef as useRef11, useEffect as useEffect30, useContext as useContext33, useCallback as useCallback9 } from "react";
5689
6063
  function useHealth(maxHp, opts = {}) {
5690
6064
  const engine = useContext33(EngineContext);
5691
6065
  const entityId = useContext33(EntityContext);
5692
- const hpRef = useRef10(maxHp);
5693
- const invincibleRef = useRef10(false);
6066
+ const hpRef = useRef11(maxHp);
6067
+ const invincibleRef = useRef11(false);
5694
6068
  const iFrameDuration = opts.iFrames ?? 1;
5695
- const onDeathRef = useRef10(opts.onDeath);
5696
- const onDamageRef = useRef10(opts.onDamage);
6069
+ const onDeathRef = useRef11(opts.onDeath);
6070
+ const onDamageRef = useRef11(opts.onDamage);
5697
6071
  useEffect30(() => {
5698
6072
  onDeathRef.current = opts.onDeath;
5699
6073
  });
5700
6074
  useEffect30(() => {
5701
6075
  onDamageRef.current = opts.onDamage;
5702
6076
  });
5703
- const timerRef = useRef10(
6077
+ const timerRef = useRef11(
5704
6078
  createTimer(iFrameDuration, () => {
5705
6079
  invincibleRef.current = false;
5706
6080
  })
@@ -5715,7 +6089,7 @@ function useHealth(maxHp, opts = {}) {
5715
6089
  }
5716
6090
  if (hpRef.current <= 0) onDeathRef.current?.();
5717
6091
  }, [iFrameDuration]);
5718
- const takeDamageRef = useRef10(takeDamage);
6092
+ const takeDamageRef = useRef11(takeDamage);
5719
6093
  useEffect30(() => {
5720
6094
  takeDamageRef.current = takeDamage;
5721
6095
  }, [takeDamage]);
@@ -5802,11 +6176,11 @@ function useKinematicBody() {
5802
6176
  }
5803
6177
 
5804
6178
  // ../gameplay/src/hooks/useLevelTransition.ts
5805
- import { useState as useState11, useRef as useRef11, useCallback as useCallback11 } from "react";
6179
+ import { useState as useState11, useRef as useRef12, useCallback as useCallback11 } from "react";
5806
6180
  function useLevelTransition(initial) {
5807
6181
  const [currentLevel, setCurrentLevel] = useState11(initial);
5808
6182
  const [isTransitioning, setIsTransitioning] = useState11(false);
5809
- const overlayRef = useRef11(null);
6183
+ const overlayRef = useRef12(null);
5810
6184
  const transitionTo = useCallback11((level, opts = {}) => {
5811
6185
  const { duration = 0.4, type = "fade" } = opts;
5812
6186
  if (type === "instant") {
@@ -5985,10 +6359,10 @@ function useRestart() {
5985
6359
  }
5986
6360
 
5987
6361
  // ../gameplay/src/hooks/useSave.ts
5988
- import { useCallback as useCallback15, useRef as useRef12 } from "react";
6362
+ import { useCallback as useCallback15, useRef as useRef13 } from "react";
5989
6363
  function useSave(key, defaultValue, opts = {}) {
5990
6364
  const version = opts.version ?? 1;
5991
- const dataRef = useRef12(defaultValue);
6365
+ const dataRef = useRef13(defaultValue);
5992
6366
  const save = useCallback15((value) => {
5993
6367
  dataRef.current = value;
5994
6368
  const slot = { version, data: value };
@@ -6063,11 +6437,11 @@ function useTopDownMovement(entityId, opts = {}) {
6063
6437
  }
6064
6438
 
6065
6439
  // ../gameplay/src/hooks/useDialogue.ts
6066
- import { useState as useState14, useCallback as useCallback16, useRef as useRef13 } from "react";
6440
+ import { useState as useState14, useCallback as useCallback16, useRef as useRef14 } from "react";
6067
6441
  function useDialogue() {
6068
6442
  const [active, setActive] = useState14(false);
6069
6443
  const [currentId, setCurrentId] = useState14(null);
6070
- const scriptRef = useRef13(null);
6444
+ const scriptRef = useRef14(null);
6071
6445
  const start = useCallback16((script, startId) => {
6072
6446
  scriptRef.current = script;
6073
6447
  const id = startId ?? Object.keys(script)[0];
@@ -6111,16 +6485,16 @@ function useDialogue() {
6111
6485
  }
6112
6486
 
6113
6487
  // ../gameplay/src/hooks/useCutscene.ts
6114
- import { useState as useState15, useCallback as useCallback17, useRef as useRef14, useEffect as useEffect33, useContext as useContext38 } from "react";
6488
+ import { useState as useState15, useCallback as useCallback17, useRef as useRef15, useEffect as useEffect33, useContext as useContext38 } from "react";
6115
6489
  function useCutscene() {
6116
6490
  const engine = useContext38(EngineContext);
6117
6491
  const [playing, setPlaying] = useState15(false);
6118
6492
  const [stepIndex, setStepIndex] = useState15(0);
6119
- const stepsRef = useRef14([]);
6120
- const timerRef = useRef14(0);
6121
- const idxRef = useRef14(0);
6122
- const playingRef = useRef14(false);
6123
- const entityRef = useRef14(null);
6493
+ const stepsRef = useRef15([]);
6494
+ const timerRef = useRef15(0);
6495
+ const idxRef = useRef15(0);
6496
+ const playingRef = useRef15(false);
6497
+ const entityRef = useRef15(null);
6124
6498
  const finish = useCallback17(() => {
6125
6499
  playingRef.current = false;
6126
6500
  setPlaying(false);
@@ -6245,7 +6619,7 @@ function useGameStore(key, initialState) {
6245
6619
  }
6246
6620
 
6247
6621
  // ../../packages/audio/src/useSound.ts
6248
- import { useEffect as useEffect34, useRef as useRef15 } from "react";
6622
+ import { useEffect as useEffect34, useRef as useRef16 } from "react";
6249
6623
  var _audioCtx = null;
6250
6624
  function getAudioCtx() {
6251
6625
  if (!_audioCtx) _audioCtx = new AudioContext();
@@ -6312,12 +6686,12 @@ async function loadBuffer(src) {
6312
6686
  return buf;
6313
6687
  }
6314
6688
  function useSound(src, opts = {}) {
6315
- const bufferRef = useRef15(null);
6316
- const sourceRef = useRef15(null);
6317
- const gainRef = useRef15(null);
6318
- const volRef = useRef15(opts.volume ?? 1);
6319
- const loopRef = useRef15(opts.loop ?? false);
6320
- const groupRef = useRef15(opts.group);
6689
+ const bufferRef = useRef16(null);
6690
+ const sourceRef = useRef16(null);
6691
+ const gainRef = useRef16(null);
6692
+ const volRef = useRef16(opts.volume ?? 1);
6693
+ const loopRef = useRef16(opts.loop ?? false);
6694
+ const groupRef = useRef16(opts.group);
6321
6695
  useEffect34(() => {
6322
6696
  let cancelled = false;
6323
6697
  loadBuffer(src).then((buf) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",