cubeforge 0.3.7 → 0.3.9

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/dist/index.js CHANGED
@@ -805,6 +805,20 @@ function definePlugin(plugin) {
805
805
  return plugin;
806
806
  }
807
807
 
808
+ // ../../packages/core/src/pluginHotReload.ts
809
+ function hotReloadPlugin(engine, oldPlugin, newPlugin, oldSystems) {
810
+ const { ecs } = engine;
811
+ const toRemove = oldSystems ?? oldPlugin.systems;
812
+ for (const system of toRemove) {
813
+ ecs.removeSystem(system);
814
+ }
815
+ oldPlugin.onDestroy?.(engine);
816
+ for (const system of newPlugin.systems) {
817
+ ecs.addSystem(system);
818
+ }
819
+ newPlugin.onInit?.(engine);
820
+ }
821
+
808
822
  // ../../packages/input/src/keyboard.ts
809
823
  var Keyboard = class {
810
824
  held = /* @__PURE__ */ new Set();
@@ -1162,6 +1176,22 @@ function createTrail(opts) {
1162
1176
  };
1163
1177
  }
1164
1178
 
1179
+ // ../../packages/renderer/src/components/nineSlice.ts
1180
+ function createNineSlice(src, width, height, opts) {
1181
+ return {
1182
+ type: "NineSlice",
1183
+ src,
1184
+ width,
1185
+ height,
1186
+ borderTop: 8,
1187
+ borderRight: 8,
1188
+ borderBottom: 8,
1189
+ borderLeft: 8,
1190
+ zIndex: 0,
1191
+ ...opts
1192
+ };
1193
+ }
1194
+
1165
1195
  // ../../packages/renderer/src/shaders.ts
1166
1196
  var VERT_SRC = `#version 300 es
1167
1197
  layout(location = 0) in vec2 a_quadPos;
@@ -1498,11 +1528,26 @@ var RenderSystem = class {
1498
1528
  textureCache = /* @__PURE__ */ new Map();
1499
1529
  /** Insertion-order key list for LRU-style eviction. */
1500
1530
  textureCacheKeys = [];
1531
+ // ── Debug overlays ──────────────────────────────────────────────────────
1532
+ debugNavGrid = null;
1533
+ contactFlashPoints = [];
1534
+ // FPS tracking
1535
+ frameTimes = [];
1536
+ lastTimestamp = 0;
1537
+ /** Overlay a nav grid: green = walkable, red = blocked. Pass null to clear. */
1538
+ setDebugNavGrid(grid) {
1539
+ this.debugNavGrid = grid;
1540
+ }
1541
+ /** Flash a point on the canvas for one frame (world-space coords). */
1542
+ flashContactPoint(x, y) {
1543
+ this.contactFlashPoints.push({ x, y, ttl: 1 });
1544
+ }
1501
1545
  // ── Texture management (sprite textures — CLAMP_TO_EDGE) ──────────────────
1502
1546
  loadTexture(src) {
1503
1547
  const cached = this.textures.get(src);
1504
1548
  if (cached) return cached;
1505
- const existing = this.imageCache.get(src);
1549
+ const imgSrc = src.endsWith(":repeat") ? src.slice(0, -7) : src;
1550
+ const existing = this.imageCache.get(imgSrc);
1506
1551
  if (existing && existing.complete && existing.naturalWidth > 0) {
1507
1552
  const gl = this.gl;
1508
1553
  const tex = gl.createTexture();
@@ -1518,7 +1563,8 @@ var RenderSystem = class {
1518
1563
  }
1519
1564
  if (!existing) {
1520
1565
  const img = new Image();
1521
- img.src = src;
1566
+ img.src = imgSrc;
1567
+ const tiled = src.endsWith(":repeat");
1522
1568
  img.onload = () => {
1523
1569
  const gl = this.gl;
1524
1570
  const tex = gl.createTexture();
@@ -1527,11 +1573,12 @@ var RenderSystem = class {
1527
1573
  gl.generateMipmap(gl.TEXTURE_2D);
1528
1574
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
1529
1575
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1530
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1531
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1576
+ const wrap = tiled ? gl.REPEAT : gl.CLAMP_TO_EDGE;
1577
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
1578
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
1532
1579
  this.textures.set(src, tex);
1533
1580
  };
1534
- this.imageCache.set(src, img);
1581
+ this.imageCache.set(imgSrc, img);
1535
1582
  }
1536
1583
  return this.whiteTexture;
1537
1584
  }
@@ -1674,8 +1721,14 @@ var RenderSystem = class {
1674
1721
  if (dy > halfH) cam.y = t.y - halfH;
1675
1722
  else if (dy < -halfH) cam.y = t.y + halfH;
1676
1723
  } else if (cam.smoothing > 0) {
1677
- cam.x += (t.x - cam.x) * (1 - cam.smoothing);
1678
- cam.y += (t.y - cam.y) * (1 - cam.smoothing);
1724
+ const distSq = (t.x - cam.x) ** 2 + (t.y - cam.y) ** 2;
1725
+ if (distSq > 16e4) {
1726
+ cam.x = t.x;
1727
+ cam.y = t.y;
1728
+ } else {
1729
+ cam.x += (t.x - cam.x) * (1 - cam.smoothing);
1730
+ cam.y += (t.y - cam.y) * (1 - cam.smoothing);
1731
+ }
1679
1732
  } else {
1680
1733
  cam.x = t.x;
1681
1734
  cam.y = t.y;
@@ -1835,7 +1888,8 @@ var RenderSystem = class {
1835
1888
  const ss = world.getComponent(id, "SquashStretch");
1836
1889
  const scaleXMod = ss ? ss.currentScaleX : 1;
1837
1890
  const scaleYMod = ss ? ss.currentScaleY : 1;
1838
- const [r, g, b, a] = parseCSSColor(sprite.color);
1891
+ const hasTexture = sprite.image && sprite.image.complete && sprite.image.naturalWidth > 0;
1892
+ const [r, g, b, a] = hasTexture ? [1, 1, 1, 1] : parseCSSColor(sprite.color);
1839
1893
  const uv = getUVRect(sprite);
1840
1894
  this.writeInstance(
1841
1895
  batchCount * FLOATS_PER_INSTANCE,
@@ -2004,6 +2058,84 @@ var RenderSystem = class {
2004
2058
  }
2005
2059
  if (tCount > 0) this.flush(tCount, "__color__");
2006
2060
  }
2061
+ if (this.debugNavGrid) {
2062
+ const g = this.debugNavGrid;
2063
+ let ngCount = 0;
2064
+ for (let row = 0; row < g.rows; row++) {
2065
+ for (let col = 0; col < g.cols; col++) {
2066
+ if (ngCount >= MAX_INSTANCES) {
2067
+ this.flush(ngCount, "__color__");
2068
+ ngCount = 0;
2069
+ }
2070
+ const walkable = g.walkable[row * g.cols + col];
2071
+ const cx = col * g.cellSize + g.cellSize / 2;
2072
+ const cy = row * g.cellSize + g.cellSize / 2;
2073
+ this.writeInstance(
2074
+ ngCount * FLOATS_PER_INSTANCE,
2075
+ cx,
2076
+ cy,
2077
+ g.cellSize,
2078
+ g.cellSize,
2079
+ 0,
2080
+ 0.5,
2081
+ 0.5,
2082
+ 0,
2083
+ 0,
2084
+ false,
2085
+ walkable ? 0 : 1,
2086
+ walkable ? 1 : 0,
2087
+ 0,
2088
+ walkable ? 0.08 : 0.25,
2089
+ 0,
2090
+ 0,
2091
+ 1,
2092
+ 1
2093
+ );
2094
+ ngCount++;
2095
+ }
2096
+ }
2097
+ if (ngCount > 0) this.flush(ngCount, "__color__");
2098
+ }
2099
+ if (this.contactFlashPoints.length > 0) {
2100
+ let cfCount = 0;
2101
+ for (const pt of this.contactFlashPoints) {
2102
+ if (cfCount >= MAX_INSTANCES) {
2103
+ this.flush(cfCount, "__color__");
2104
+ cfCount = 0;
2105
+ }
2106
+ this.writeInstance(
2107
+ cfCount * FLOATS_PER_INSTANCE,
2108
+ pt.x,
2109
+ pt.y,
2110
+ 8,
2111
+ 8,
2112
+ 0,
2113
+ 0.5,
2114
+ 0.5,
2115
+ 0,
2116
+ 0,
2117
+ false,
2118
+ 1,
2119
+ 0.3,
2120
+ 0.3,
2121
+ 0.9,
2122
+ 0,
2123
+ 0,
2124
+ 1,
2125
+ 1
2126
+ );
2127
+ cfCount++;
2128
+ pt.ttl--;
2129
+ }
2130
+ if (cfCount > 0) this.flush(cfCount, "__color__");
2131
+ this.contactFlashPoints = this.contactFlashPoints.filter((p) => p.ttl > 0);
2132
+ }
2133
+ const now = performance.now();
2134
+ if (this.lastTimestamp > 0) {
2135
+ this.frameTimes.push(now - this.lastTimestamp);
2136
+ if (this.frameTimes.length > 60) this.frameTimes.shift();
2137
+ }
2138
+ this.lastTimestamp = now;
2007
2139
  }
2008
2140
  };
2009
2141
 
@@ -2099,6 +2231,18 @@ function createCapsuleCollider(width, height, opts) {
2099
2231
  };
2100
2232
  }
2101
2233
 
2234
+ // ../../packages/physics/src/components/compoundCollider.ts
2235
+ function createCompoundCollider(shapes, opts) {
2236
+ return {
2237
+ type: "CompoundCollider",
2238
+ shapes,
2239
+ isTrigger: false,
2240
+ layer: "default",
2241
+ mask: "*",
2242
+ ...opts
2243
+ };
2244
+ }
2245
+
2102
2246
  // ../../packages/physics/src/physicsSystem.ts
2103
2247
  function getAABB(transform, collider) {
2104
2248
  return {
@@ -2139,6 +2283,77 @@ function maskAllows(mask, layer) {
2139
2283
  function canInteract(a, b) {
2140
2284
  return maskAllows(a.mask, b.layer) && maskAllows(b.mask, a.layer);
2141
2285
  }
2286
+ function shapeToAABB(tx, ty, shape) {
2287
+ if (shape.type === "box") {
2288
+ return {
2289
+ cx: tx + shape.offsetX,
2290
+ cy: ty + shape.offsetY,
2291
+ hw: (shape.width ?? 0) / 2,
2292
+ hh: (shape.height ?? 0) / 2
2293
+ };
2294
+ }
2295
+ const r = shape.radius ?? 0;
2296
+ return {
2297
+ cx: tx + shape.offsetX,
2298
+ cy: ty + shape.offsetY,
2299
+ hw: r,
2300
+ hh: r
2301
+ };
2302
+ }
2303
+ function shapeOverlapsAABB(tx, ty, shape, other) {
2304
+ if (shape.type === "box") {
2305
+ return getOverlap(shapeToAABB(tx, ty, shape), other);
2306
+ }
2307
+ const r = shape.radius ?? 0;
2308
+ const cx = tx + shape.offsetX;
2309
+ const cy = ty + shape.offsetY;
2310
+ const nearX = Math.max(other.cx - other.hw, Math.min(cx, other.cx + other.hw));
2311
+ const nearY = Math.max(other.cy - other.hh, Math.min(cy, other.cy + other.hh));
2312
+ const dx = cx - nearX;
2313
+ const dy = cy - nearY;
2314
+ if (dx * dx + dy * dy >= r * r) return null;
2315
+ return getOverlap(shapeToAABB(tx, ty, shape), other);
2316
+ }
2317
+ function shapeOverlapsCircle(tx, ty, shape, cx, cy, cr) {
2318
+ if (shape.type === "circle") {
2319
+ const r = shape.radius ?? 0;
2320
+ const dx2 = tx + shape.offsetX - cx;
2321
+ const dy2 = ty + shape.offsetY - cy;
2322
+ return dx2 * dx2 + dy2 * dy2 < (r + cr) * (r + cr);
2323
+ }
2324
+ const aabb = shapeToAABB(tx, ty, shape);
2325
+ const nearX = Math.max(aabb.cx - aabb.hw, Math.min(cx, aabb.cx + aabb.hw));
2326
+ const nearY = Math.max(aabb.cy - aabb.hh, Math.min(cy, aabb.cy + aabb.hh));
2327
+ const dx = cx - nearX;
2328
+ const dy = cy - nearY;
2329
+ return dx * dx + dy * dy < cr * cr;
2330
+ }
2331
+ function getCompoundBounds(tx, ty, shapes) {
2332
+ let minX = Infinity;
2333
+ let minY = Infinity;
2334
+ let maxX = -Infinity;
2335
+ let maxY = -Infinity;
2336
+ for (const s2 of shapes) {
2337
+ const a = shapeToAABB(tx, ty, s2);
2338
+ const l = a.cx - a.hw;
2339
+ const r = a.cx + a.hw;
2340
+ const t = a.cy - a.hh;
2341
+ const b = a.cy + a.hh;
2342
+ if (l < minX) minX = l;
2343
+ if (r > maxX) maxX = r;
2344
+ if (t < minY) minY = t;
2345
+ if (b > maxY) maxY = b;
2346
+ }
2347
+ return {
2348
+ cx: (minX + maxX) / 2,
2349
+ cy: (minY + maxY) / 2,
2350
+ hw: (maxX - minX) / 2,
2351
+ hh: (maxY - minY) / 2
2352
+ };
2353
+ }
2354
+ function canInteractGeneric(aLayer, aMask, bLayer, bMask) {
2355
+ return maskAllows(aMask, bLayer) && maskAllows(bMask, aLayer);
2356
+ }
2142
2357
  function pairKey(a, b) {
2143
2358
  return a < b ? `${a}:${b}` : `${b}:${a}`;
2144
2359
  }
@@ -2153,6 +2368,7 @@ var PhysicsSystem = class {
2153
2368
  activeTriggerPairs = /* @__PURE__ */ new Map();
2154
2369
  activeCollisionPairs = /* @__PURE__ */ new Map();
2155
2370
  activeCirclePairs = /* @__PURE__ */ new Map();
2371
+ activeCompoundPairs = /* @__PURE__ */ new Map();
2156
2372
  // Previous-frame positions of static entities — used to compute platform carry delta.
2157
2373
  staticPrevPos = /* @__PURE__ */ new Map();
2158
2374
  setGravity(g) {
@@ -2207,6 +2423,12 @@ var PhysicsSystem = class {
2207
2423
  this.activeCirclePairs.delete(key);
2208
2424
  }
2209
2425
  }
2426
+ for (const [key, [a, b]] of this.activeCompoundPairs) {
2427
+ if (!world.hasEntity(a) || !world.hasEntity(b)) {
2428
+ this.events?.emit("compoundExit", { a, b });
2429
+ this.activeCompoundPairs.delete(key);
2430
+ }
2431
+ }
2210
2432
  const staticDelta = /* @__PURE__ */ new Map();
2211
2433
  for (const sid of statics) {
2212
2434
  const st = world.getComponent(sid, "Transform");
@@ -2509,6 +2731,101 @@ var PhysicsSystem = class {
2509
2731
  }
2510
2732
  this.activeCirclePairs = /* @__PURE__ */ new Map();
2511
2733
  }
2734
+ const allCompound = world.query("Transform", "CompoundCollider");
2735
+ if (allCompound.length > 0) {
2736
+ const currentCompoundPairs = /* @__PURE__ */ new Map();
2737
+ const allBoxEntities = world.query("Transform", "BoxCollider");
2738
+ for (const cid of allCompound) {
2739
+ const cc = world.getComponent(cid, "CompoundCollider");
2740
+ const ct = world.getComponent(cid, "Transform");
2741
+ for (const bid of allBoxEntities) {
2742
+ if (bid === cid) continue;
2743
+ const bc = world.getComponent(bid, "BoxCollider");
2744
+ if (!canInteractGeneric(cc.layer, cc.mask, bc.layer, bc.mask)) continue;
2745
+ const bt = world.getComponent(bid, "Transform");
2746
+ const boxAABB = getAABB(bt, bc);
2747
+ let hit = false;
2748
+ for (const shape of cc.shapes) {
2749
+ if (shapeOverlapsAABB(ct.x, ct.y, shape, boxAABB)) {
2750
+ hit = true;
2751
+ break;
2752
+ }
2753
+ }
2754
+ if (hit) currentCompoundPairs.set(pairKey(cid, bid), [cid, bid]);
2755
+ }
2756
+ }
2757
+ for (const cid of allCompound) {
2758
+ const cc = world.getComponent(cid, "CompoundCollider");
2759
+ const ct = world.getComponent(cid, "Transform");
2760
+ for (const oid of allCircles) {
2761
+ if (oid === cid) continue;
2762
+ const oc = world.getComponent(oid, "CircleCollider");
2763
+ if (!canInteractGeneric(cc.layer, cc.mask, oc.layer, oc.mask)) continue;
2764
+ const ot = world.getComponent(oid, "Transform");
2765
+ let hit = false;
2766
+ for (const shape of cc.shapes) {
2767
+ if (shapeOverlapsCircle(ct.x, ct.y, shape, ot.x + oc.offsetX, ot.y + oc.offsetY, oc.radius)) {
2768
+ hit = true;
2769
+ break;
2770
+ }
2771
+ }
2772
+ if (hit) currentCompoundPairs.set(pairKey(cid, oid), [cid, oid]);
2773
+ }
2774
+ }
2775
+ for (let i = 0; i < allCompound.length; i++) {
2776
+ for (let j = i + 1; j < allCompound.length; j++) {
2777
+ const ia = allCompound[i];
2778
+ const ib = allCompound[j];
2779
+ const ca = world.getComponent(ia, "CompoundCollider");
2780
+ const cb = world.getComponent(ib, "CompoundCollider");
2781
+ if (!canInteractGeneric(ca.layer, ca.mask, cb.layer, cb.mask)) continue;
2782
+ const ta = world.getComponent(ia, "Transform");
2783
+ const tb = world.getComponent(ib, "Transform");
2784
+ const boundsA = getCompoundBounds(ta.x, ta.y, ca.shapes);
2785
+ const boundsB = getCompoundBounds(tb.x, tb.y, cb.shapes);
2786
+ if (!getOverlap(boundsA, boundsB)) continue;
2787
+ let hit = false;
2788
+ outer2: for (const sa of ca.shapes) {
2789
+ const aabb = shapeToAABB(ta.x, ta.y, sa);
2790
+ for (const sb of cb.shapes) {
2791
+ if (sb.type === "circle") {
2792
+ const r = sb.radius ?? 0;
2793
+ if (shapeOverlapsCircle(ta.x, ta.y, sa, tb.x + sb.offsetX, tb.y + sb.offsetY, r)) {
2794
+ hit = true;
2795
+ break outer2;
2796
+ }
2797
+ } else {
2798
+ const bAABB = shapeToAABB(tb.x, tb.y, sb);
2799
+ if (getOverlap(aabb, bAABB)) {
2800
+ hit = true;
2801
+ break outer2;
2802
+ }
2803
+ }
2804
+ }
2805
+ }
2806
+ if (hit) currentCompoundPairs.set(pairKey(ia, ib), [ia, ib]);
2807
+ }
2808
+ }
2809
+ for (const [key, [a, b]] of currentCompoundPairs) {
2810
+ if (!this.activeCompoundPairs.has(key)) {
2811
+ this.events?.emit("compoundEnter", { a, b });
2812
+ } else {
2813
+ this.events?.emit("compoundStay", { a, b });
2814
+ }
2815
+ this.events?.emit("compound", { a, b });
2816
+ }
2817
+ for (const [key, [a, b]] of this.activeCompoundPairs) {
2818
+ if (!currentCompoundPairs.has(key)) {
2819
+ this.events?.emit("compoundExit", { a, b });
2820
+ }
2821
+ }
2822
+ this.activeCompoundPairs = currentCompoundPairs;
2823
+ } else if (this.activeCompoundPairs.size > 0) {
2824
+ for (const [, [a, b]] of this.activeCompoundPairs) {
2825
+ this.events?.emit("compoundExit", { a, b });
2826
+ }
2827
+ this.activeCompoundPairs = /* @__PURE__ */ new Map();
2828
+ }
2512
2829
  }
2513
2830
  };
2514
2831
 
@@ -2952,6 +3269,8 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
2952
3269
  const [activeTab, setActiveTab] = useState("entities");
2953
3270
  const [entitySearch, setEntitySearch] = useState("");
2954
3271
  const [contactLog, setContactLog] = useState([]);
3272
+ const [showNavGrid, setShowNavGrid] = useState(false);
3273
+ const [showContactFlash, setShowContactFlash] = useState(false);
2955
3274
  const frameRef = useRef2(0);
2956
3275
  useEffect2(() => {
2957
3276
  handle.onFrame = () => {
@@ -2977,10 +3296,24 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
2977
3296
  if (next.length > 20) next.length = 20;
2978
3297
  return next;
2979
3298
  });
3299
+ if (showContactFlash) {
3300
+ const renderer = engine.activeRenderSystem;
3301
+ if (renderer?.flashContactPoint) {
3302
+ const tA = engine.ecs.getComponent(a, "Transform");
3303
+ const tB = engine.ecs.getComponent(b, "Transform");
3304
+ if (tA && tB) {
3305
+ renderer.flashContactPoint((tA.x + tB.x) / 2, (tA.y + tB.y) / 2);
3306
+ } else if (tA) {
3307
+ renderer.flashContactPoint(tA.x, tA.y);
3308
+ } else if (tB) {
3309
+ renderer.flashContactPoint(tB.x, tB.y);
3310
+ }
3311
+ }
3312
+ }
2980
3313
  })
2981
3314
  );
2982
3315
  return () => unsubs.forEach((u) => u());
2983
- }, [engine]);
3316
+ }, [engine, showContactFlash]);
2984
3317
  const totalFrames = handle.buffer.length;
2985
3318
  const currentSnap = handle.buffer[selectedIdx];
2986
3319
  const handlePauseResume = useCallback(() => {
@@ -3003,6 +3336,51 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
3003
3336
  setSelectedIdx((i) => Math.min(handle.buffer.length - 1, i + 1));
3004
3337
  setSelectedEntity(null);
3005
3338
  }, [handle]);
3339
+ const handleToggleNavGrid = useCallback(() => {
3340
+ if (!engine) return;
3341
+ const renderer = engine.activeRenderSystem;
3342
+ if (!renderer?.setDebugNavGrid) return;
3343
+ if (showNavGrid) {
3344
+ renderer.setDebugNavGrid(null);
3345
+ setShowNavGrid(false);
3346
+ } else {
3347
+ const snap = handle.buffer[handle.buffer.length - 1];
3348
+ if (snap) {
3349
+ let maxX = 0, maxY = 0;
3350
+ for (const e of snap.entities) {
3351
+ const t = e.components.find((c) => c.type === "Transform");
3352
+ if (t) {
3353
+ if (t.x > maxX) maxX = t.x;
3354
+ if (t.y > maxY) maxY = t.y;
3355
+ }
3356
+ }
3357
+ const cellSize = 16;
3358
+ const cols = Math.ceil((maxX + 200) / cellSize);
3359
+ const rows = Math.ceil((maxY + 200) / cellSize);
3360
+ const walkable = new Uint8Array(cols * rows).fill(1);
3361
+ for (const e of snap.entities) {
3362
+ const t = e.components.find((c) => c.type === "Transform");
3363
+ const bc = e.components.find((c) => c.type === "BoxCollider");
3364
+ const rb = e.components.find((c) => c.type === "RigidBody");
3365
+ if (t && bc && (!rb || rb.isStatic)) {
3366
+ const left = t.x + (bc.offsetX ?? 0) - bc.width / 2;
3367
+ const top = t.y + (bc.offsetY ?? 0) - bc.height / 2;
3368
+ const c0 = Math.max(0, Math.floor(left / cellSize));
3369
+ const c1 = Math.min(cols - 1, Math.floor((left + bc.width) / cellSize));
3370
+ const r0 = Math.max(0, Math.floor(top / cellSize));
3371
+ const r1 = Math.min(rows - 1, Math.floor((top + bc.height) / cellSize));
3372
+ for (let r = r0; r <= r1; r++) {
3373
+ for (let c = c0; c <= c1; c++) {
3374
+ walkable[r * cols + c] = 0;
3375
+ }
3376
+ }
3377
+ }
3378
+ }
3379
+ renderer.setDebugNavGrid({ cols, rows, cellSize, walkable });
3380
+ setShowNavGrid(true);
3381
+ }
3382
+ }
3383
+ }, [engine, showNavGrid, handle]);
3006
3384
  const frameLabel = totalFrames === 0 ? "0/0" : `${selectedIdx + 1}/${totalFrames}`;
3007
3385
  const entities = currentSnap?.entities ?? [];
3008
3386
  const filtered = entitySearch ? entities.filter(
@@ -3042,9 +3420,27 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
3042
3420
  selectedEntityData
3043
3421
  }
3044
3422
  ),
3045
- activeTab === "perf" && /* @__PURE__ */ jsx(PerfTab, { fps, entityCount, compCount, timings }),
3423
+ activeTab === "perf" && /* @__PURE__ */ jsx(
3424
+ PerfTab,
3425
+ {
3426
+ fps,
3427
+ entityCount,
3428
+ compCount,
3429
+ timings,
3430
+ showNavGrid,
3431
+ onToggleNavGrid: handleToggleNavGrid
3432
+ }
3433
+ ),
3046
3434
  activeTab === "input" && /* @__PURE__ */ jsx(InputTab, { activeKeys, inputCtx }),
3047
- activeTab === "contacts" && /* @__PURE__ */ jsx(ContactsTab, { log: contactLog, onClear: () => setContactLog([]) }),
3435
+ activeTab === "contacts" && /* @__PURE__ */ jsx(
3436
+ ContactsTab,
3437
+ {
3438
+ log: contactLog,
3439
+ onClear: () => setContactLog([]),
3440
+ showFlash: showContactFlash,
3441
+ onToggleFlash: () => setShowContactFlash((v) => !v)
3442
+ }
3443
+ ),
3048
3444
  activeTab === "assets" && /* @__PURE__ */ jsx(AssetsTab, { assetCache })
3049
3445
  ] }),
3050
3446
  /* @__PURE__ */ jsxs("div", { style: s.bar, children: [
@@ -3143,7 +3539,7 @@ function EntitiesTab({ entities, entitySearch, onSearchChange, selectedEntity, o
3143
3539
  ] }, comp.type)) })
3144
3540
  ] });
3145
3541
  }
3146
- function PerfTab({ fps, entityCount, compCount, timings }) {
3542
+ function PerfTab({ fps, entityCount, compCount, timings, showNavGrid, onToggleNavGrid }) {
3147
3543
  const maxMs = 16.67;
3148
3544
  const stats = [
3149
3545
  { label: "FPS", value: String(fps), ok: fps >= 55 },
@@ -3155,6 +3551,10 @@ function PerfTab({ fps, entityCount, compCount, timings }) {
3155
3551
  /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: C.muted }, children: label }),
3156
3552
  /* @__PURE__ */ jsx("span", { style: { fontSize: 16, fontWeight: 700, color: ok ? C.ok : C.warn }, children: value })
3157
3553
  ] }, label)) }),
3554
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8, marginBottom: 10 }, children: /* @__PURE__ */ jsxs("button", { style: s.btn(showNavGrid), onClick: onToggleNavGrid, children: [
3555
+ showNavGrid ? "Hide" : "Show",
3556
+ " Nav Grid"
3557
+ ] }) }),
3158
3558
  timings && timings.size > 0 && /* @__PURE__ */ jsxs("div", { children: [
3159
3559
  /* @__PURE__ */ jsx("div", { style: { color: C.muted, fontSize: 9, marginBottom: 6, letterSpacing: "0.08em" }, children: "SYSTEM TIMING" }),
3160
3560
  Array.from(timings.entries()).map(([name, ms]) => {
@@ -3204,11 +3604,17 @@ function InputTab({ activeKeys, inputCtx }) {
3204
3604
  ] })
3205
3605
  ] });
3206
3606
  }
3207
- function ContactsTab({ log, onClear }) {
3607
+ function ContactsTab({ log, onClear, showFlash, onToggleFlash }) {
3208
3608
  return /* @__PURE__ */ jsxs("div", { style: { padding: "4px 0" }, children: [
3209
3609
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "4px 14px 8px" }, children: [
3210
3610
  /* @__PURE__ */ jsx("span", { style: { color: C.muted, fontSize: 9 }, children: "Last 20 contact events" }),
3211
- /* @__PURE__ */ jsx("button", { style: s.btn(false, true), onClick: onClear, children: "Clear" })
3611
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6 }, children: [
3612
+ /* @__PURE__ */ jsxs("button", { style: s.btn(showFlash), onClick: onToggleFlash, children: [
3613
+ showFlash ? "Hide" : "Show",
3614
+ " Flash"
3615
+ ] }),
3616
+ /* @__PURE__ */ jsx("button", { style: s.btn(false, true), onClick: onClear, children: "Clear" })
3617
+ ] })
3212
3618
  ] }),
3213
3619
  log.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "4px 14px", color: C.muted }, children: "No contacts yet" }) : log.map((entry, i) => /* @__PURE__ */ jsxs("div", { style: { padding: "3px 14px", display: "flex", gap: 10, alignItems: "center" }, children: [
3214
3620
  /* @__PURE__ */ jsxs("span", { style: { color: C.muted, fontSize: 9, minWidth: 46 }, children: [
@@ -3693,7 +4099,9 @@ function Sprite({
3693
4099
  atlas,
3694
4100
  frame,
3695
4101
  tileX,
3696
- tileY
4102
+ tileY,
4103
+ tileSizeX,
4104
+ tileSizeY
3697
4105
  }) {
3698
4106
  const resolvedFrameIndex = atlas && frame != null ? atlas[frame] ?? 0 : frameIndex;
3699
4107
  const engine = useContext5(EngineContext);
@@ -3716,7 +4124,9 @@ function Sprite({
3716
4124
  frameHeight,
3717
4125
  frameColumns,
3718
4126
  tileX,
3719
- tileY
4127
+ tileY,
4128
+ tileSizeX,
4129
+ tileSizeY
3720
4130
  });
3721
4131
  engine.ecs.addComponent(entityId, comp);
3722
4132
  if (src) {
@@ -3879,12 +4289,37 @@ function CapsuleCollider({
3879
4289
  return null;
3880
4290
  }
3881
4291
 
3882
- // src/components/Script.tsx
4292
+ // src/components/CompoundCollider.tsx
3883
4293
  import { useEffect as useEffect13, useContext as useContext11 } from "react";
3884
- function Script({ init, update }) {
4294
+ function CompoundCollider({
4295
+ shapes,
4296
+ isTrigger = false,
4297
+ layer = "default",
4298
+ mask = "*"
4299
+ }) {
3885
4300
  const engine = useContext11(EngineContext);
3886
4301
  const entityId = useContext11(EntityContext);
3887
4302
  useEffect13(() => {
4303
+ engine.ecs.addComponent(entityId, createCompoundCollider(shapes, { isTrigger, layer, mask }));
4304
+ const checkId = setTimeout(() => {
4305
+ if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "Transform")) {
4306
+ console.warn(`[Cubeforge] CompoundCollider on entity ${entityId} has no Transform. Physics requires Transform.`);
4307
+ }
4308
+ }, 0);
4309
+ return () => {
4310
+ clearTimeout(checkId);
4311
+ engine.ecs.removeComponent(entityId, "CompoundCollider");
4312
+ };
4313
+ }, []);
4314
+ return null;
4315
+ }
4316
+
4317
+ // src/components/Script.tsx
4318
+ import { useEffect as useEffect14, useContext as useContext12 } from "react";
4319
+ function Script({ init, update }) {
4320
+ const engine = useContext12(EngineContext);
4321
+ const entityId = useContext12(EntityContext);
4322
+ useEffect14(() => {
3888
4323
  if (init) {
3889
4324
  try {
3890
4325
  init(entityId, engine.ecs);
@@ -3899,7 +4334,7 @@ function Script({ init, update }) {
3899
4334
  }
3900
4335
 
3901
4336
  // src/components/Camera2D.tsx
3902
- import { useEffect as useEffect14, useContext as useContext12 } from "react";
4337
+ import { useEffect as useEffect15, useContext as useContext13 } from "react";
3903
4338
  function Camera2D({
3904
4339
  followEntity,
3905
4340
  x = 0,
@@ -3912,8 +4347,8 @@ function Camera2D({
3912
4347
  followOffsetX = 0,
3913
4348
  followOffsetY = 0
3914
4349
  }) {
3915
- const engine = useContext12(EngineContext);
3916
- useEffect14(() => {
4350
+ const engine = useContext13(EngineContext);
4351
+ useEffect15(() => {
3917
4352
  const entityId = engine.ecs.createEntity();
3918
4353
  engine.ecs.addComponent(entityId, createCamera2D({
3919
4354
  followEntityId: followEntity,
@@ -3929,7 +4364,7 @@ function Camera2D({
3929
4364
  }));
3930
4365
  return () => engine.ecs.destroyEntity(entityId);
3931
4366
  }, []);
3932
- useEffect14(() => {
4367
+ useEffect15(() => {
3933
4368
  const camId = engine.ecs.queryOne("Camera2D");
3934
4369
  if (camId === void 0) return;
3935
4370
  const cam = engine.ecs.getComponent(camId, "Camera2D");
@@ -3948,11 +4383,11 @@ function Camera2D({
3948
4383
  }
3949
4384
 
3950
4385
  // src/components/Animation.tsx
3951
- import { useEffect as useEffect15, useContext as useContext13 } from "react";
4386
+ import { useEffect as useEffect16, useContext as useContext14 } from "react";
3952
4387
  function Animation({ frames, fps = 12, loop = true, playing = true, onComplete, frameEvents }) {
3953
- const engine = useContext13(EngineContext);
3954
- const entityId = useContext13(EntityContext);
3955
- useEffect15(() => {
4388
+ const engine = useContext14(EngineContext);
4389
+ const entityId = useContext14(EntityContext);
4390
+ useEffect16(() => {
3956
4391
  const state = {
3957
4392
  type: "AnimationState",
3958
4393
  frames,
@@ -3970,7 +4405,7 @@ function Animation({ frames, fps = 12, loop = true, playing = true, onComplete,
3970
4405
  engine.ecs.removeComponent(entityId, "AnimationState");
3971
4406
  };
3972
4407
  }, []);
3973
- useEffect15(() => {
4408
+ useEffect16(() => {
3974
4409
  const anim = engine.ecs.getComponent(entityId, "AnimationState");
3975
4410
  if (!anim) return;
3976
4411
  const wasFramesChanged = anim.frames !== frames;
@@ -3990,11 +4425,11 @@ function Animation({ frames, fps = 12, loop = true, playing = true, onComplete,
3990
4425
  }
3991
4426
 
3992
4427
  // src/components/SquashStretch.tsx
3993
- import { useEffect as useEffect16, useContext as useContext14 } from "react";
4428
+ import { useEffect as useEffect17, useContext as useContext15 } from "react";
3994
4429
  function SquashStretch({ intensity = 0.2, recovery = 8 }) {
3995
- const engine = useContext14(EngineContext);
3996
- const entityId = useContext14(EntityContext);
3997
- useEffect16(() => {
4430
+ const engine = useContext15(EngineContext);
4431
+ const entityId = useContext15(EntityContext);
4432
+ useEffect17(() => {
3998
4433
  engine.ecs.addComponent(entityId, {
3999
4434
  type: "SquashStretch",
4000
4435
  intensity,
@@ -4008,7 +4443,7 @@ function SquashStretch({ intensity = 0.2, recovery = 8 }) {
4008
4443
  }
4009
4444
 
4010
4445
  // src/components/ParticleEmitter.tsx
4011
- import { useEffect as useEffect17, useContext as useContext15 } from "react";
4446
+ import { useEffect as useEffect18, useContext as useContext16 } from "react";
4012
4447
 
4013
4448
  // src/components/particlePresets.ts
4014
4449
  var PARTICLE_PRESETS = {
@@ -4093,9 +4528,9 @@ function ParticleEmitter({
4093
4528
  const resolvedColor = color ?? presetConfig.color ?? "#ffffff";
4094
4529
  const resolvedGravity = gravity ?? presetConfig.gravity ?? 200;
4095
4530
  const resolvedMaxParticles = maxParticles ?? presetConfig.maxParticles ?? 100;
4096
- const engine = useContext15(EngineContext);
4097
- const entityId = useContext15(EntityContext);
4098
- useEffect17(() => {
4531
+ const engine = useContext16(EngineContext);
4532
+ const entityId = useContext16(EntityContext);
4533
+ useEffect18(() => {
4099
4534
  engine.ecs.addComponent(entityId, {
4100
4535
  type: "ParticlePool",
4101
4536
  particles: [],
@@ -4113,7 +4548,7 @@ function ParticleEmitter({
4113
4548
  });
4114
4549
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
4115
4550
  }, []);
4116
- useEffect17(() => {
4551
+ useEffect18(() => {
4117
4552
  const pool = engine.ecs.getComponent(entityId, "ParticlePool");
4118
4553
  if (!pool) return;
4119
4554
  pool.active = active;
@@ -4347,7 +4782,7 @@ function Checkpoint({
4347
4782
  }
4348
4783
 
4349
4784
  // src/components/Tilemap.tsx
4350
- import { useEffect as useEffect18, useState as useState5, useContext as useContext16 } from "react";
4785
+ import { useEffect as useEffect19, useState as useState5, useContext as useContext17 } from "react";
4351
4786
  import { Fragment as Fragment4, jsx as jsx8 } from "react/jsx-runtime";
4352
4787
  var animatedTiles = /* @__PURE__ */ new Map();
4353
4788
  function getProperty(props, name) {
@@ -4372,9 +4807,9 @@ function Tilemap({
4372
4807
  onTileProperty,
4373
4808
  navGrid
4374
4809
  }) {
4375
- const engine = useContext16(EngineContext);
4810
+ const engine = useContext17(EngineContext);
4376
4811
  const [spawnedNodes, setSpawnedNodes] = useState5([]);
4377
- useEffect18(() => {
4812
+ useEffect19(() => {
4378
4813
  if (!engine) return;
4379
4814
  const createdEntities = [];
4380
4815
  async function load() {
@@ -4565,7 +5000,7 @@ function Tilemap({
4565
5000
  }
4566
5001
 
4567
5002
  // src/components/ParallaxLayer.tsx
4568
- import { useEffect as useEffect19, useContext as useContext17 } from "react";
5003
+ import { useEffect as useEffect20, useContext as useContext18 } from "react";
4569
5004
  import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
4570
5005
  function ParallaxLayerInner({
4571
5006
  src,
@@ -4577,9 +5012,9 @@ function ParallaxLayerInner({
4577
5012
  offsetX,
4578
5013
  offsetY
4579
5014
  }) {
4580
- const engine = useContext17(EngineContext);
4581
- const entityId = useContext17(EntityContext);
4582
- useEffect19(() => {
5015
+ const engine = useContext18(EngineContext);
5016
+ const entityId = useContext18(EntityContext);
5017
+ useEffect20(() => {
4583
5018
  engine.ecs.addComponent(entityId, {
4584
5019
  type: "ParallaxLayer",
4585
5020
  src,
@@ -4595,7 +5030,7 @@ function ParallaxLayerInner({
4595
5030
  });
4596
5031
  return () => engine.ecs.removeComponent(entityId, "ParallaxLayer");
4597
5032
  }, []);
4598
- useEffect19(() => {
5033
+ useEffect20(() => {
4599
5034
  const layer = engine.ecs.getComponent(entityId, "ParallaxLayer");
4600
5035
  if (!layer) return;
4601
5036
  layer.src = src;
@@ -4677,7 +5112,7 @@ var ScreenFlash = forwardRef((_, ref) => {
4677
5112
  ScreenFlash.displayName = "ScreenFlash";
4678
5113
 
4679
5114
  // src/components/CameraZone.tsx
4680
- import { useEffect as useEffect20, useContext as useContext18, useRef as useRef6 } from "react";
5115
+ import { useEffect as useEffect21, useContext as useContext19, useRef as useRef6 } from "react";
4681
5116
  import { Fragment as Fragment5, jsx as jsx11 } from "react/jsx-runtime";
4682
5117
  function CameraZone({
4683
5118
  x,
@@ -4689,10 +5124,10 @@ function CameraZone({
4689
5124
  targetY,
4690
5125
  children
4691
5126
  }) {
4692
- const engine = useContext18(EngineContext);
5127
+ const engine = useContext19(EngineContext);
4693
5128
  const prevFollowRef = useRef6(void 0);
4694
5129
  const activeRef = useRef6(false);
4695
- useEffect20(() => {
5130
+ useEffect21(() => {
4696
5131
  const eid = engine.ecs.createEntity();
4697
5132
  engine.ecs.addComponent(eid, createScript(() => {
4698
5133
  const cam = engine.ecs.queryOne("Camera2D");
@@ -4739,26 +5174,56 @@ function CameraZone({
4739
5174
  }
4740
5175
 
4741
5176
  // src/components/Trail.tsx
4742
- import { useEffect as useEffect21, useContext as useContext19 } from "react";
5177
+ import { useEffect as useEffect22, useContext as useContext20 } from "react";
4743
5178
  function Trail({ length = 20, color = "#ffffff", width = 3 }) {
4744
- const engine = useContext19(EngineContext);
4745
- const entityId = useContext19(EntityContext);
4746
- useEffect21(() => {
5179
+ const engine = useContext20(EngineContext);
5180
+ const entityId = useContext20(EntityContext);
5181
+ useEffect22(() => {
4747
5182
  engine.ecs.addComponent(entityId, createTrail({ length, color, width }));
4748
5183
  return () => engine.ecs.removeComponent(entityId, "Trail");
4749
5184
  }, []);
4750
5185
  return null;
4751
5186
  }
4752
5187
 
5188
+ // src/components/NineSlice.tsx
5189
+ import { useEffect as useEffect23, useContext as useContext21 } from "react";
5190
+ function NineSlice({
5191
+ src,
5192
+ width,
5193
+ height,
5194
+ borderTop = 8,
5195
+ borderRight = 8,
5196
+ borderBottom = 8,
5197
+ borderLeft = 8,
5198
+ zIndex = 0
5199
+ }) {
5200
+ const engine = useContext21(EngineContext);
5201
+ const entityId = useContext21(EntityContext);
5202
+ useEffect23(() => {
5203
+ engine.ecs.addComponent(
5204
+ entityId,
5205
+ createNineSlice(src, width, height, {
5206
+ borderTop,
5207
+ borderRight,
5208
+ borderBottom,
5209
+ borderLeft,
5210
+ zIndex
5211
+ })
5212
+ );
5213
+ return () => engine.ecs.removeComponent(entityId, "NineSlice");
5214
+ }, []);
5215
+ return null;
5216
+ }
5217
+
4753
5218
  // src/components/AssetLoader.tsx
4754
- import { useEffect as useEffect23 } from "react";
5219
+ import { useEffect as useEffect25 } from "react";
4755
5220
 
4756
5221
  // src/hooks/usePreload.ts
4757
- import { useState as useState6, useEffect as useEffect22, useContext as useContext20 } from "react";
5222
+ import { useState as useState6, useEffect as useEffect24, useContext as useContext22 } from "react";
4758
5223
  function usePreload(assets) {
4759
- const engine = useContext20(EngineContext);
5224
+ const engine = useContext22(EngineContext);
4760
5225
  const [state, setState] = useState6({ progress: assets.length === 0 ? 1 : 0, loaded: assets.length === 0, error: null });
4761
- useEffect22(() => {
5226
+ useEffect24(() => {
4762
5227
  if (assets.length === 0) {
4763
5228
  setState({ progress: 1, loaded: true, error: null });
4764
5229
  return;
@@ -4789,7 +5254,7 @@ function usePreload(assets) {
4789
5254
  import { Fragment as Fragment6, jsx as jsx12 } from "react/jsx-runtime";
4790
5255
  function AssetLoader({ assets, fallback = null, onError, children }) {
4791
5256
  const { loaded, error } = usePreload(assets);
4792
- useEffect23(() => {
5257
+ useEffect25(() => {
4793
5258
  if (error && onError) onError(error);
4794
5259
  }, [error, onError]);
4795
5260
  if (!loaded) {
@@ -4799,9 +5264,9 @@ function AssetLoader({ assets, fallback = null, onError, children }) {
4799
5264
  }
4800
5265
 
4801
5266
  // src/hooks/useGame.ts
4802
- import { useContext as useContext21 } from "react";
5267
+ import { useContext as useContext23 } from "react";
4803
5268
  function useGame() {
4804
- const engine = useContext21(EngineContext);
5269
+ const engine = useContext23(EngineContext);
4805
5270
  if (!engine) throw new Error("useGame must be used inside <Game>");
4806
5271
  return engine;
4807
5272
  }
@@ -4858,18 +5323,18 @@ function useSnapshot() {
4858
5323
  }
4859
5324
 
4860
5325
  // src/hooks/useEntity.ts
4861
- import { useContext as useContext22 } from "react";
5326
+ import { useContext as useContext24 } from "react";
4862
5327
  function useEntity() {
4863
- const id = useContext22(EntityContext);
5328
+ const id = useContext24(EntityContext);
4864
5329
  if (id === null) throw new Error("useEntity must be used inside <Entity>");
4865
5330
  return id;
4866
5331
  }
4867
5332
 
4868
5333
  // src/hooks/useDestroyEntity.ts
4869
- import { useCallback as useCallback2, useContext as useContext23 } from "react";
5334
+ import { useCallback as useCallback2, useContext as useContext25 } from "react";
4870
5335
  function useDestroyEntity() {
4871
- const engine = useContext23(EngineContext);
4872
- const entityId = useContext23(EntityContext);
5336
+ const engine = useContext25(EngineContext);
5337
+ const entityId = useContext25(EntityContext);
4873
5338
  if (!engine) throw new Error("useDestroyEntity must be used inside <Game>");
4874
5339
  if (entityId === null) throw new Error("useDestroyEntity must be used inside <Entity>");
4875
5340
  return useCallback2(() => {
@@ -4880,9 +5345,9 @@ function useDestroyEntity() {
4880
5345
  }
4881
5346
 
4882
5347
  // src/hooks/useInput.ts
4883
- import { useContext as useContext24 } from "react";
5348
+ import { useContext as useContext26 } from "react";
4884
5349
  function useInput() {
4885
- const engine = useContext24(EngineContext);
5350
+ const engine = useContext26(EngineContext);
4886
5351
  if (!engine) throw new Error("useInput must be used inside <Game>");
4887
5352
  return engine.input;
4888
5353
  }
@@ -4902,9 +5367,9 @@ function useInputMap(bindings) {
4902
5367
  }
4903
5368
 
4904
5369
  // src/hooks/useEvents.ts
4905
- import { useContext as useContext25, useEffect as useEffect24, useRef as useRef7 } from "react";
5370
+ import { useContext as useContext27, useEffect as useEffect26, useRef as useRef7 } from "react";
4906
5371
  function useEvents() {
4907
- const engine = useContext25(EngineContext);
5372
+ const engine = useContext27(EngineContext);
4908
5373
  if (!engine) throw new Error("useEvents must be used inside <Game>");
4909
5374
  return engine.events;
4910
5375
  }
@@ -4912,15 +5377,15 @@ function useEvent(event, handler) {
4912
5377
  const events = useEvents();
4913
5378
  const handlerRef = useRef7(handler);
4914
5379
  handlerRef.current = handler;
4915
- useEffect24(() => {
5380
+ useEffect26(() => {
4916
5381
  return events.on(event, (data) => handlerRef.current(data));
4917
5382
  }, [events, event]);
4918
5383
  }
4919
5384
 
4920
5385
  // src/hooks/useCoordinates.ts
4921
- import { useCallback as useCallback3, useContext as useContext26 } from "react";
5386
+ import { useCallback as useCallback3, useContext as useContext28 } from "react";
4922
5387
  function useCoordinates() {
4923
- const engine = useContext26(EngineContext);
5388
+ const engine = useContext28(EngineContext);
4924
5389
  const worldToScreen = useCallback3((wx, wy) => {
4925
5390
  const canvas = engine.canvas;
4926
5391
  const camId = engine.ecs.queryOne("Camera2D");
@@ -4947,9 +5412,9 @@ function useCoordinates() {
4947
5412
  }
4948
5413
 
4949
5414
  // src/hooks/useInputContext.ts
4950
- import { useEffect as useEffect25, useMemo as useMemo4 } from "react";
5415
+ import { useEffect as useEffect27, useMemo as useMemo4 } from "react";
4951
5416
  function useInputContext(ctx) {
4952
- useEffect25(() => {
5417
+ useEffect27(() => {
4953
5418
  if (!ctx) return;
4954
5419
  globalInputContext.push(ctx);
4955
5420
  return () => globalInputContext.pop(ctx);
@@ -4988,12 +5453,12 @@ function useInputRecorder() {
4988
5453
  }
4989
5454
 
4990
5455
  // src/hooks/useGamepad.ts
4991
- import { useEffect as useEffect26, useRef as useRef8, useState as useState7 } from "react";
5456
+ import { useEffect as useEffect28, useRef as useRef8, useState as useState7 } from "react";
4992
5457
  var EMPTY_STATE = { connected: false, axes: [], buttons: [] };
4993
5458
  function useGamepad(playerIndex = 0) {
4994
5459
  const [state, setState] = useState7(EMPTY_STATE);
4995
5460
  const rafRef = useRef8(0);
4996
- useEffect26(() => {
5461
+ useEffect28(() => {
4997
5462
  const poll = () => {
4998
5463
  const gp = navigator.getGamepads()[playerIndex];
4999
5464
  if (gp) {
@@ -5014,9 +5479,9 @@ function useGamepad(playerIndex = 0) {
5014
5479
  }
5015
5480
 
5016
5481
  // src/hooks/usePause.ts
5017
- import { useContext as useContext27, useState as useState8, useCallback as useCallback4 } from "react";
5482
+ import { useContext as useContext29, useState as useState8, useCallback as useCallback4 } from "react";
5018
5483
  function usePause() {
5019
- const engine = useContext27(EngineContext);
5484
+ const engine = useContext29(EngineContext);
5020
5485
  const [paused, setPaused] = useState8(false);
5021
5486
  const pause = useCallback4(() => {
5022
5487
  engine.loop.pause();
@@ -5085,19 +5550,19 @@ function useAISteering() {
5085
5550
  }
5086
5551
 
5087
5552
  // ../gameplay/src/hooks/useDamageZone.ts
5088
- import { useContext as useContext28 } from "react";
5553
+ import { useContext as useContext30 } from "react";
5089
5554
  function useDamageZone(damage, opts = {}) {
5090
- const engine = useContext28(EngineContext);
5555
+ const engine = useContext30(EngineContext);
5091
5556
  useTriggerEnter((other) => {
5092
5557
  engine.events.emit(`damage:${other}`, { amount: damage });
5093
5558
  }, { tag: opts.tag, layer: opts.layer });
5094
5559
  }
5095
5560
 
5096
5561
  // ../gameplay/src/hooks/useDropThrough.ts
5097
- import { useContext as useContext29, useCallback as useCallback7 } from "react";
5562
+ import { useContext as useContext31, useCallback as useCallback7 } from "react";
5098
5563
  function useDropThrough(frames = 8) {
5099
- const engine = useContext29(EngineContext);
5100
- const entityId = useContext29(EntityContext);
5564
+ const engine = useContext31(EngineContext);
5565
+ const entityId = useContext31(EntityContext);
5101
5566
  const dropThrough = useCallback7(() => {
5102
5567
  const rb = engine.ecs.getComponent(entityId, "RigidBody");
5103
5568
  if (rb) rb.dropThrough = frames;
@@ -5106,14 +5571,14 @@ function useDropThrough(frames = 8) {
5106
5571
  }
5107
5572
 
5108
5573
  // ../gameplay/src/hooks/useGameStateMachine.ts
5109
- import { useState as useState10, useRef as useRef9, useCallback as useCallback8, useEffect as useEffect27, useContext as useContext30 } from "react";
5574
+ import { useState as useState10, useRef as useRef9, useCallback as useCallback8, useEffect as useEffect29, useContext as useContext32 } from "react";
5110
5575
  function useGameStateMachine(states, initial) {
5111
- const engine = useContext30(EngineContext);
5576
+ const engine = useContext32(EngineContext);
5112
5577
  const [state, setState] = useState10(initial);
5113
5578
  const stateRef = useRef9(initial);
5114
5579
  const statesRef = useRef9(states);
5115
5580
  statesRef.current = states;
5116
- useEffect27(() => {
5581
+ useEffect29(() => {
5117
5582
  statesRef.current[initial]?.onEnter?.();
5118
5583
  }, []);
5119
5584
  const transition = useCallback8((to) => {
@@ -5124,7 +5589,7 @@ function useGameStateMachine(states, initial) {
5124
5589
  setState(to);
5125
5590
  statesRef.current[to]?.onEnter?.();
5126
5591
  }, []);
5127
- useEffect27(() => {
5592
+ useEffect29(() => {
5128
5593
  const eid = engine.ecs.createEntity();
5129
5594
  engine.ecs.addComponent(eid, createScript((_id, _world, _input, dt) => {
5130
5595
  statesRef.current[stateRef.current]?.onUpdate?.(dt);
@@ -5137,19 +5602,19 @@ function useGameStateMachine(states, initial) {
5137
5602
  }
5138
5603
 
5139
5604
  // ../gameplay/src/hooks/useHealth.ts
5140
- import { useRef as useRef10, useEffect as useEffect28, useContext as useContext31, useCallback as useCallback9 } from "react";
5605
+ import { useRef as useRef10, useEffect as useEffect30, useContext as useContext33, useCallback as useCallback9 } from "react";
5141
5606
  function useHealth(maxHp, opts = {}) {
5142
- const engine = useContext31(EngineContext);
5143
- const entityId = useContext31(EntityContext);
5607
+ const engine = useContext33(EngineContext);
5608
+ const entityId = useContext33(EntityContext);
5144
5609
  const hpRef = useRef10(maxHp);
5145
5610
  const invincibleRef = useRef10(false);
5146
5611
  const iFrameDuration = opts.iFrames ?? 1;
5147
5612
  const onDeathRef = useRef10(opts.onDeath);
5148
5613
  const onDamageRef = useRef10(opts.onDamage);
5149
- useEffect28(() => {
5614
+ useEffect30(() => {
5150
5615
  onDeathRef.current = opts.onDeath;
5151
5616
  });
5152
- useEffect28(() => {
5617
+ useEffect30(() => {
5153
5618
  onDamageRef.current = opts.onDamage;
5154
5619
  });
5155
5620
  const timerRef = useRef10(
@@ -5168,10 +5633,10 @@ function useHealth(maxHp, opts = {}) {
5168
5633
  if (hpRef.current <= 0) onDeathRef.current?.();
5169
5634
  }, [iFrameDuration]);
5170
5635
  const takeDamageRef = useRef10(takeDamage);
5171
- useEffect28(() => {
5636
+ useEffect30(() => {
5172
5637
  takeDamageRef.current = takeDamage;
5173
5638
  }, [takeDamage]);
5174
- useEffect28(() => {
5639
+ useEffect30(() => {
5175
5640
  return engine.events.on(`damage:${entityId}`, ({ amount }) => {
5176
5641
  takeDamageRef.current(amount);
5177
5642
  });
@@ -5206,10 +5671,10 @@ function useHealth(maxHp, opts = {}) {
5206
5671
  }
5207
5672
 
5208
5673
  // ../gameplay/src/hooks/useKinematicBody.ts
5209
- import { useContext as useContext32, useCallback as useCallback10 } from "react";
5674
+ import { useContext as useContext34, useCallback as useCallback10 } from "react";
5210
5675
  function useKinematicBody() {
5211
- const engine = useContext32(EngineContext);
5212
- const entityId = useContext32(EntityContext);
5676
+ const engine = useContext34(EngineContext);
5677
+ const entityId = useContext34(EntityContext);
5213
5678
  const moveAndCollide = useCallback10((dx, dy) => {
5214
5679
  const transform = engine.ecs.getComponent(entityId, "Transform");
5215
5680
  if (!transform) return { dx: 0, dy: 0 };
@@ -5296,13 +5761,13 @@ function useLevelTransition(initial) {
5296
5761
  }
5297
5762
 
5298
5763
  // ../gameplay/src/hooks/usePlatformerController.ts
5299
- import { useContext as useContext33, useEffect as useEffect29 } from "react";
5764
+ import { useContext as useContext35, useEffect as useEffect31 } from "react";
5300
5765
  function normalizeKeys(val, defaults) {
5301
5766
  if (!val) return defaults;
5302
5767
  return Array.isArray(val) ? val : [val];
5303
5768
  }
5304
5769
  function usePlatformerController(entityId, opts = {}) {
5305
- const engine = useContext33(EngineContext);
5770
+ const engine = useContext35(EngineContext);
5306
5771
  const {
5307
5772
  speed = 200,
5308
5773
  jumpForce = -500,
@@ -5315,7 +5780,7 @@ function usePlatformerController(entityId, opts = {}) {
5315
5780
  const leftKeys = normalizeKeys(bindings?.left, ["ArrowLeft", "KeyA", "a"]);
5316
5781
  const rightKeys = normalizeKeys(bindings?.right, ["ArrowRight", "KeyD", "d"]);
5317
5782
  const jumpKeys = normalizeKeys(bindings?.jump, ["Space", "ArrowUp", "KeyW", "w"]);
5318
- useEffect29(() => {
5783
+ useEffect31(() => {
5319
5784
  const state = {
5320
5785
  coyoteTimer: 0,
5321
5786
  jumpBuffer: 0,
@@ -5379,9 +5844,9 @@ function usePathfinding() {
5379
5844
  }
5380
5845
 
5381
5846
  // ../gameplay/src/hooks/usePersistedBindings.ts
5382
- import { useState as useState12, useCallback as useCallback13, useMemo as useMemo8, useContext as useContext34 } from "react";
5847
+ import { useState as useState12, useCallback as useCallback13, useMemo as useMemo8, useContext as useContext36 } from "react";
5383
5848
  function usePersistedBindings(storageKey, defaults) {
5384
- const engine = useContext34(EngineContext);
5849
+ const engine = useContext36(EngineContext);
5385
5850
  const input = engine.input;
5386
5851
  const [bindings, setBindings] = useState12(() => {
5387
5852
  try {
@@ -5486,11 +5951,11 @@ function useSave(key, defaultValue, opts = {}) {
5486
5951
  }
5487
5952
 
5488
5953
  // ../gameplay/src/hooks/useTopDownMovement.ts
5489
- import { useContext as useContext35, useEffect as useEffect30 } from "react";
5954
+ import { useContext as useContext37, useEffect as useEffect32 } from "react";
5490
5955
  function useTopDownMovement(entityId, opts = {}) {
5491
- const engine = useContext35(EngineContext);
5956
+ const engine = useContext37(EngineContext);
5492
5957
  const { speed = 200, normalizeDiagonal = true } = opts;
5493
- useEffect30(() => {
5958
+ useEffect32(() => {
5494
5959
  const updateFn = (id, world, input) => {
5495
5960
  if (!world.hasEntity(id)) return;
5496
5961
  const rb = world.getComponent(id, "RigidBody");
@@ -5514,8 +5979,190 @@ function useTopDownMovement(entityId, opts = {}) {
5514
5979
  }, []);
5515
5980
  }
5516
5981
 
5982
+ // ../gameplay/src/hooks/useDialogue.ts
5983
+ import { useState as useState14, useCallback as useCallback16, useRef as useRef13 } from "react";
5984
+ function useDialogue() {
5985
+ const [active, setActive] = useState14(false);
5986
+ const [currentId, setCurrentId] = useState14(null);
5987
+ const scriptRef = useRef13(null);
5988
+ const start = useCallback16((script, startId) => {
5989
+ scriptRef.current = script;
5990
+ const id = startId ?? Object.keys(script)[0];
5991
+ setCurrentId(id);
5992
+ setActive(true);
5993
+ }, []);
5994
+ const advance = useCallback16((choiceIndex) => {
5995
+ if (!scriptRef.current || !currentId) return;
5996
+ const line = scriptRef.current[currentId];
5997
+ if (!line) {
5998
+ setActive(false);
5999
+ setCurrentId(null);
6000
+ return;
6001
+ }
6002
+ if (line.choices && choiceIndex !== void 0) {
6003
+ const choice = line.choices[choiceIndex];
6004
+ if (choice?.next && scriptRef.current[choice.next]) {
6005
+ setCurrentId(choice.next);
6006
+ } else {
6007
+ setActive(false);
6008
+ setCurrentId(null);
6009
+ }
6010
+ } else {
6011
+ const keys = Object.keys(scriptRef.current);
6012
+ const idx = keys.indexOf(currentId);
6013
+ if (idx >= 0 && idx + 1 < keys.length) {
6014
+ setCurrentId(keys[idx + 1]);
6015
+ } else {
6016
+ setActive(false);
6017
+ setCurrentId(null);
6018
+ }
6019
+ }
6020
+ }, [currentId]);
6021
+ const close = useCallback16(() => {
6022
+ setActive(false);
6023
+ setCurrentId(null);
6024
+ scriptRef.current = null;
6025
+ }, []);
6026
+ const current = scriptRef.current && currentId ? scriptRef.current[currentId] ?? null : null;
6027
+ return { active, current, currentId, start, advance, close };
6028
+ }
6029
+
6030
+ // ../gameplay/src/hooks/useCutscene.ts
6031
+ import { useState as useState15, useCallback as useCallback17, useRef as useRef14, useEffect as useEffect33, useContext as useContext38 } from "react";
6032
+ function useCutscene() {
6033
+ const engine = useContext38(EngineContext);
6034
+ const [playing, setPlaying] = useState15(false);
6035
+ const [stepIndex, setStepIndex] = useState15(0);
6036
+ const stepsRef = useRef14([]);
6037
+ const timerRef = useRef14(0);
6038
+ const idxRef = useRef14(0);
6039
+ const playingRef = useRef14(false);
6040
+ const entityRef = useRef14(null);
6041
+ const finish = useCallback17(() => {
6042
+ playingRef.current = false;
6043
+ setPlaying(false);
6044
+ setStepIndex(0);
6045
+ idxRef.current = 0;
6046
+ if (entityRef.current !== null && engine.ecs.hasEntity(entityRef.current)) {
6047
+ engine.ecs.destroyEntity(entityRef.current);
6048
+ entityRef.current = null;
6049
+ }
6050
+ }, [engine.ecs]);
6051
+ const fireStep = useCallback17((step) => {
6052
+ if (step.type === "call") step.fn();
6053
+ if (step.type === "parallel") step.steps.forEach((s2) => {
6054
+ if (s2.type === "call") s2.fn();
6055
+ });
6056
+ }, []);
6057
+ const play = useCallback17((steps) => {
6058
+ stepsRef.current = steps;
6059
+ idxRef.current = 0;
6060
+ timerRef.current = 0;
6061
+ playingRef.current = true;
6062
+ setPlaying(true);
6063
+ setStepIndex(0);
6064
+ const step = steps[0];
6065
+ if (step) fireStep(step);
6066
+ const eid = engine.ecs.createEntity();
6067
+ entityRef.current = eid;
6068
+ engine.ecs.addComponent(eid, createScript((_id, _world, _input, dt) => {
6069
+ if (!playingRef.current) return;
6070
+ const allSteps = stepsRef.current;
6071
+ const idx = idxRef.current;
6072
+ if (idx >= allSteps.length) {
6073
+ finish();
6074
+ return;
6075
+ }
6076
+ const current = allSteps[idx];
6077
+ if (current.type === "wait") {
6078
+ timerRef.current += dt;
6079
+ if (timerRef.current >= current.duration) {
6080
+ timerRef.current = 0;
6081
+ idxRef.current++;
6082
+ setStepIndex(idxRef.current);
6083
+ const next = allSteps[idxRef.current];
6084
+ if (next) fireStep(next);
6085
+ if (idxRef.current >= allSteps.length) finish();
6086
+ }
6087
+ } else if (current.type === "call") {
6088
+ idxRef.current++;
6089
+ setStepIndex(idxRef.current);
6090
+ const next = allSteps[idxRef.current];
6091
+ if (next) fireStep(next);
6092
+ if (idxRef.current >= allSteps.length) finish();
6093
+ } else if (current.type === "parallel") {
6094
+ const waits = current.steps.filter((s2) => s2.type === "wait");
6095
+ const maxDuration = waits.length > 0 ? Math.max(...waits.map((w) => w.duration)) : 0;
6096
+ timerRef.current += dt;
6097
+ if (timerRef.current >= maxDuration) {
6098
+ timerRef.current = 0;
6099
+ idxRef.current++;
6100
+ setStepIndex(idxRef.current);
6101
+ const next = allSteps[idxRef.current];
6102
+ if (next) fireStep(next);
6103
+ if (idxRef.current >= allSteps.length) finish();
6104
+ }
6105
+ }
6106
+ }));
6107
+ }, [engine.ecs, finish, fireStep]);
6108
+ const skip = useCallback17(() => {
6109
+ for (let i = idxRef.current; i < stepsRef.current.length; i++) {
6110
+ const step = stepsRef.current[i];
6111
+ if (step.type === "call") step.fn();
6112
+ if (step.type === "parallel") step.steps.forEach((s2) => {
6113
+ if (s2.type === "call") s2.fn();
6114
+ });
6115
+ }
6116
+ finish();
6117
+ }, [finish]);
6118
+ useEffect33(() => {
6119
+ return () => {
6120
+ if (entityRef.current !== null && engine.ecs.hasEntity(entityRef.current)) {
6121
+ engine.ecs.destroyEntity(entityRef.current);
6122
+ }
6123
+ };
6124
+ }, [engine.ecs]);
6125
+ return { playing, stepIndex, play, skip };
6126
+ }
6127
+
6128
+ // ../gameplay/src/hooks/useGameStore.ts
6129
+ import { useSyncExternalStore, useCallback as useCallback18 } from "react";
6130
+ function createStore(initialState) {
6131
+ let state = { ...initialState };
6132
+ const listeners = /* @__PURE__ */ new Set();
6133
+ return {
6134
+ getState: () => state,
6135
+ setState: (partial) => {
6136
+ const updates = typeof partial === "function" ? partial(state) : partial;
6137
+ state = { ...state, ...updates };
6138
+ listeners.forEach((l) => l());
6139
+ },
6140
+ subscribe: (listener) => {
6141
+ listeners.add(listener);
6142
+ return () => {
6143
+ listeners.delete(listener);
6144
+ };
6145
+ }
6146
+ };
6147
+ }
6148
+ var stores = /* @__PURE__ */ new Map();
6149
+ function useGameStore(key, initialState) {
6150
+ if (!stores.has(key)) {
6151
+ stores.set(key, createStore(initialState));
6152
+ }
6153
+ const store = stores.get(key);
6154
+ const state = useSyncExternalStore(
6155
+ store.subscribe,
6156
+ store.getState
6157
+ );
6158
+ const setState = useCallback18((partial) => {
6159
+ store.setState(partial);
6160
+ }, [store]);
6161
+ return [state, setState];
6162
+ }
6163
+
5517
6164
  // ../../packages/audio/src/useSound.ts
5518
- import { useEffect as useEffect31, useRef as useRef13 } from "react";
6165
+ import { useEffect as useEffect34, useRef as useRef15 } from "react";
5519
6166
  var _audioCtx = null;
5520
6167
  function getAudioCtx() {
5521
6168
  if (!_audioCtx) _audioCtx = new AudioContext();
@@ -5582,13 +6229,13 @@ async function loadBuffer(src) {
5582
6229
  return buf;
5583
6230
  }
5584
6231
  function useSound(src, opts = {}) {
5585
- const bufferRef = useRef13(null);
5586
- const sourceRef = useRef13(null);
5587
- const gainRef = useRef13(null);
5588
- const volRef = useRef13(opts.volume ?? 1);
5589
- const loopRef = useRef13(opts.loop ?? false);
5590
- const groupRef = useRef13(opts.group);
5591
- useEffect31(() => {
6232
+ const bufferRef = useRef15(null);
6233
+ const sourceRef = useRef15(null);
6234
+ const gainRef = useRef15(null);
6235
+ const volRef = useRef15(opts.volume ?? 1);
6236
+ const loopRef = useRef15(opts.loop ?? false);
6237
+ const groupRef = useRef15(opts.group);
6238
+ useEffect34(() => {
5592
6239
  let cancelled = false;
5593
6240
  loadBuffer(src).then((buf) => {
5594
6241
  if (!cancelled) bufferRef.current = buf;
@@ -5732,10 +6379,12 @@ export {
5732
6379
  CapsuleCollider,
5733
6380
  Checkpoint,
5734
6381
  CircleCollider,
6382
+ CompoundCollider,
5735
6383
  Ease,
5736
6384
  Entity,
5737
6385
  Game,
5738
6386
  MovingPlatform,
6387
+ NineSlice,
5739
6388
  ParallaxLayer,
5740
6389
  ParticleEmitter,
5741
6390
  RenderSystem,
@@ -5752,8 +6401,10 @@ export {
5752
6401
  World,
5753
6402
  arrive,
5754
6403
  createAtlas,
6404
+ createCompoundCollider,
5755
6405
  createInputMap,
5756
6406
  createInputRecorder,
6407
+ createNineSlice,
5757
6408
  createPlayerInput,
5758
6409
  createSprite,
5759
6410
  createTag,
@@ -5765,6 +6416,7 @@ export {
5765
6416
  flee,
5766
6417
  getGroupVolume,
5767
6418
  globalInputContext,
6419
+ hotReloadPlugin,
5768
6420
  overlapBox,
5769
6421
  overlapCircle,
5770
6422
  patrol,
@@ -5788,14 +6440,17 @@ export {
5788
6440
  useCollisionExit,
5789
6441
  useCollisionStay,
5790
6442
  useCoordinates,
6443
+ useCutscene,
5791
6444
  useDamageZone,
5792
6445
  useDestroyEntity,
6446
+ useDialogue,
5793
6447
  useDropThrough,
5794
6448
  useEntity,
5795
6449
  useEvent,
5796
6450
  useEvents,
5797
6451
  useGame,
5798
6452
  useGameStateMachine,
6453
+ useGameStore,
5799
6454
  useGamepad,
5800
6455
  useHealth,
5801
6456
  useInput,