cubeforge 0.3.8 → 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,6 +1528,20 @@ 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);
@@ -2014,6 +2058,84 @@ var RenderSystem = class {
2014
2058
  }
2015
2059
  if (tCount > 0) this.flush(tCount, "__color__");
2016
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;
2017
2139
  }
2018
2140
  };
2019
2141
 
@@ -2109,6 +2231,18 @@ function createCapsuleCollider(width, height, opts) {
2109
2231
  };
2110
2232
  }
2111
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
+
2112
2246
  // ../../packages/physics/src/physicsSystem.ts
2113
2247
  function getAABB(transform, collider) {
2114
2248
  return {
@@ -2149,6 +2283,77 @@ function maskAllows(mask, layer) {
2149
2283
  function canInteract(a, b) {
2150
2284
  return maskAllows(a.mask, b.layer) && maskAllows(b.mask, a.layer);
2151
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
+ }
2152
2357
  function pairKey(a, b) {
2153
2358
  return a < b ? `${a}:${b}` : `${b}:${a}`;
2154
2359
  }
@@ -2163,6 +2368,7 @@ var PhysicsSystem = class {
2163
2368
  activeTriggerPairs = /* @__PURE__ */ new Map();
2164
2369
  activeCollisionPairs = /* @__PURE__ */ new Map();
2165
2370
  activeCirclePairs = /* @__PURE__ */ new Map();
2371
+ activeCompoundPairs = /* @__PURE__ */ new Map();
2166
2372
  // Previous-frame positions of static entities — used to compute platform carry delta.
2167
2373
  staticPrevPos = /* @__PURE__ */ new Map();
2168
2374
  setGravity(g) {
@@ -2217,6 +2423,12 @@ var PhysicsSystem = class {
2217
2423
  this.activeCirclePairs.delete(key);
2218
2424
  }
2219
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
+ }
2220
2432
  const staticDelta = /* @__PURE__ */ new Map();
2221
2433
  for (const sid of statics) {
2222
2434
  const st = world.getComponent(sid, "Transform");
@@ -2519,6 +2731,101 @@ var PhysicsSystem = class {
2519
2731
  }
2520
2732
  this.activeCirclePairs = /* @__PURE__ */ new Map();
2521
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
+ }
2522
2829
  }
2523
2830
  };
2524
2831
 
@@ -2962,6 +3269,8 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
2962
3269
  const [activeTab, setActiveTab] = useState("entities");
2963
3270
  const [entitySearch, setEntitySearch] = useState("");
2964
3271
  const [contactLog, setContactLog] = useState([]);
3272
+ const [showNavGrid, setShowNavGrid] = useState(false);
3273
+ const [showContactFlash, setShowContactFlash] = useState(false);
2965
3274
  const frameRef = useRef2(0);
2966
3275
  useEffect2(() => {
2967
3276
  handle.onFrame = () => {
@@ -2987,10 +3296,24 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
2987
3296
  if (next.length > 20) next.length = 20;
2988
3297
  return next;
2989
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
+ }
2990
3313
  })
2991
3314
  );
2992
3315
  return () => unsubs.forEach((u) => u());
2993
- }, [engine]);
3316
+ }, [engine, showContactFlash]);
2994
3317
  const totalFrames = handle.buffer.length;
2995
3318
  const currentSnap = handle.buffer[selectedIdx];
2996
3319
  const handlePauseResume = useCallback(() => {
@@ -3013,6 +3336,51 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
3013
3336
  setSelectedIdx((i) => Math.min(handle.buffer.length - 1, i + 1));
3014
3337
  setSelectedEntity(null);
3015
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]);
3016
3384
  const frameLabel = totalFrames === 0 ? "0/0" : `${selectedIdx + 1}/${totalFrames}`;
3017
3385
  const entities = currentSnap?.entities ?? [];
3018
3386
  const filtered = entitySearch ? entities.filter(
@@ -3052,9 +3420,27 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
3052
3420
  selectedEntityData
3053
3421
  }
3054
3422
  ),
3055
- 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
+ ),
3056
3434
  activeTab === "input" && /* @__PURE__ */ jsx(InputTab, { activeKeys, inputCtx }),
3057
- 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
+ ),
3058
3444
  activeTab === "assets" && /* @__PURE__ */ jsx(AssetsTab, { assetCache })
3059
3445
  ] }),
3060
3446
  /* @__PURE__ */ jsxs("div", { style: s.bar, children: [
@@ -3153,7 +3539,7 @@ function EntitiesTab({ entities, entitySearch, onSearchChange, selectedEntity, o
3153
3539
  ] }, comp.type)) })
3154
3540
  ] });
3155
3541
  }
3156
- function PerfTab({ fps, entityCount, compCount, timings }) {
3542
+ function PerfTab({ fps, entityCount, compCount, timings, showNavGrid, onToggleNavGrid }) {
3157
3543
  const maxMs = 16.67;
3158
3544
  const stats = [
3159
3545
  { label: "FPS", value: String(fps), ok: fps >= 55 },
@@ -3165,6 +3551,10 @@ function PerfTab({ fps, entityCount, compCount, timings }) {
3165
3551
  /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: C.muted }, children: label }),
3166
3552
  /* @__PURE__ */ jsx("span", { style: { fontSize: 16, fontWeight: 700, color: ok ? C.ok : C.warn }, children: value })
3167
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
+ ] }) }),
3168
3558
  timings && timings.size > 0 && /* @__PURE__ */ jsxs("div", { children: [
3169
3559
  /* @__PURE__ */ jsx("div", { style: { color: C.muted, fontSize: 9, marginBottom: 6, letterSpacing: "0.08em" }, children: "SYSTEM TIMING" }),
3170
3560
  Array.from(timings.entries()).map(([name, ms]) => {
@@ -3214,11 +3604,17 @@ function InputTab({ activeKeys, inputCtx }) {
3214
3604
  ] })
3215
3605
  ] });
3216
3606
  }
3217
- function ContactsTab({ log, onClear }) {
3607
+ function ContactsTab({ log, onClear, showFlash, onToggleFlash }) {
3218
3608
  return /* @__PURE__ */ jsxs("div", { style: { padding: "4px 0" }, children: [
3219
3609
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "4px 14px 8px" }, children: [
3220
3610
  /* @__PURE__ */ jsx("span", { style: { color: C.muted, fontSize: 9 }, children: "Last 20 contact events" }),
3221
- /* @__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
+ ] })
3222
3618
  ] }),
3223
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: [
3224
3620
  /* @__PURE__ */ jsxs("span", { style: { color: C.muted, fontSize: 9, minWidth: 46 }, children: [
@@ -3703,7 +4099,9 @@ function Sprite({
3703
4099
  atlas,
3704
4100
  frame,
3705
4101
  tileX,
3706
- tileY
4102
+ tileY,
4103
+ tileSizeX,
4104
+ tileSizeY
3707
4105
  }) {
3708
4106
  const resolvedFrameIndex = atlas && frame != null ? atlas[frame] ?? 0 : frameIndex;
3709
4107
  const engine = useContext5(EngineContext);
@@ -3726,7 +4124,9 @@ function Sprite({
3726
4124
  frameHeight,
3727
4125
  frameColumns,
3728
4126
  tileX,
3729
- tileY
4127
+ tileY,
4128
+ tileSizeX,
4129
+ tileSizeY
3730
4130
  });
3731
4131
  engine.ecs.addComponent(entityId, comp);
3732
4132
  if (src) {
@@ -3889,12 +4289,37 @@ function CapsuleCollider({
3889
4289
  return null;
3890
4290
  }
3891
4291
 
3892
- // src/components/Script.tsx
4292
+ // src/components/CompoundCollider.tsx
3893
4293
  import { useEffect as useEffect13, useContext as useContext11 } from "react";
3894
- function Script({ init, update }) {
4294
+ function CompoundCollider({
4295
+ shapes,
4296
+ isTrigger = false,
4297
+ layer = "default",
4298
+ mask = "*"
4299
+ }) {
3895
4300
  const engine = useContext11(EngineContext);
3896
4301
  const entityId = useContext11(EntityContext);
3897
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(() => {
3898
4323
  if (init) {
3899
4324
  try {
3900
4325
  init(entityId, engine.ecs);
@@ -3909,7 +4334,7 @@ function Script({ init, update }) {
3909
4334
  }
3910
4335
 
3911
4336
  // src/components/Camera2D.tsx
3912
- import { useEffect as useEffect14, useContext as useContext12 } from "react";
4337
+ import { useEffect as useEffect15, useContext as useContext13 } from "react";
3913
4338
  function Camera2D({
3914
4339
  followEntity,
3915
4340
  x = 0,
@@ -3922,8 +4347,8 @@ function Camera2D({
3922
4347
  followOffsetX = 0,
3923
4348
  followOffsetY = 0
3924
4349
  }) {
3925
- const engine = useContext12(EngineContext);
3926
- useEffect14(() => {
4350
+ const engine = useContext13(EngineContext);
4351
+ useEffect15(() => {
3927
4352
  const entityId = engine.ecs.createEntity();
3928
4353
  engine.ecs.addComponent(entityId, createCamera2D({
3929
4354
  followEntityId: followEntity,
@@ -3939,7 +4364,7 @@ function Camera2D({
3939
4364
  }));
3940
4365
  return () => engine.ecs.destroyEntity(entityId);
3941
4366
  }, []);
3942
- useEffect14(() => {
4367
+ useEffect15(() => {
3943
4368
  const camId = engine.ecs.queryOne("Camera2D");
3944
4369
  if (camId === void 0) return;
3945
4370
  const cam = engine.ecs.getComponent(camId, "Camera2D");
@@ -3958,11 +4383,11 @@ function Camera2D({
3958
4383
  }
3959
4384
 
3960
4385
  // src/components/Animation.tsx
3961
- import { useEffect as useEffect15, useContext as useContext13 } from "react";
4386
+ import { useEffect as useEffect16, useContext as useContext14 } from "react";
3962
4387
  function Animation({ frames, fps = 12, loop = true, playing = true, onComplete, frameEvents }) {
3963
- const engine = useContext13(EngineContext);
3964
- const entityId = useContext13(EntityContext);
3965
- useEffect15(() => {
4388
+ const engine = useContext14(EngineContext);
4389
+ const entityId = useContext14(EntityContext);
4390
+ useEffect16(() => {
3966
4391
  const state = {
3967
4392
  type: "AnimationState",
3968
4393
  frames,
@@ -3980,7 +4405,7 @@ function Animation({ frames, fps = 12, loop = true, playing = true, onComplete,
3980
4405
  engine.ecs.removeComponent(entityId, "AnimationState");
3981
4406
  };
3982
4407
  }, []);
3983
- useEffect15(() => {
4408
+ useEffect16(() => {
3984
4409
  const anim = engine.ecs.getComponent(entityId, "AnimationState");
3985
4410
  if (!anim) return;
3986
4411
  const wasFramesChanged = anim.frames !== frames;
@@ -4000,11 +4425,11 @@ function Animation({ frames, fps = 12, loop = true, playing = true, onComplete,
4000
4425
  }
4001
4426
 
4002
4427
  // src/components/SquashStretch.tsx
4003
- import { useEffect as useEffect16, useContext as useContext14 } from "react";
4428
+ import { useEffect as useEffect17, useContext as useContext15 } from "react";
4004
4429
  function SquashStretch({ intensity = 0.2, recovery = 8 }) {
4005
- const engine = useContext14(EngineContext);
4006
- const entityId = useContext14(EntityContext);
4007
- useEffect16(() => {
4430
+ const engine = useContext15(EngineContext);
4431
+ const entityId = useContext15(EntityContext);
4432
+ useEffect17(() => {
4008
4433
  engine.ecs.addComponent(entityId, {
4009
4434
  type: "SquashStretch",
4010
4435
  intensity,
@@ -4018,7 +4443,7 @@ function SquashStretch({ intensity = 0.2, recovery = 8 }) {
4018
4443
  }
4019
4444
 
4020
4445
  // src/components/ParticleEmitter.tsx
4021
- import { useEffect as useEffect17, useContext as useContext15 } from "react";
4446
+ import { useEffect as useEffect18, useContext as useContext16 } from "react";
4022
4447
 
4023
4448
  // src/components/particlePresets.ts
4024
4449
  var PARTICLE_PRESETS = {
@@ -4103,9 +4528,9 @@ function ParticleEmitter({
4103
4528
  const resolvedColor = color ?? presetConfig.color ?? "#ffffff";
4104
4529
  const resolvedGravity = gravity ?? presetConfig.gravity ?? 200;
4105
4530
  const resolvedMaxParticles = maxParticles ?? presetConfig.maxParticles ?? 100;
4106
- const engine = useContext15(EngineContext);
4107
- const entityId = useContext15(EntityContext);
4108
- useEffect17(() => {
4531
+ const engine = useContext16(EngineContext);
4532
+ const entityId = useContext16(EntityContext);
4533
+ useEffect18(() => {
4109
4534
  engine.ecs.addComponent(entityId, {
4110
4535
  type: "ParticlePool",
4111
4536
  particles: [],
@@ -4123,7 +4548,7 @@ function ParticleEmitter({
4123
4548
  });
4124
4549
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
4125
4550
  }, []);
4126
- useEffect17(() => {
4551
+ useEffect18(() => {
4127
4552
  const pool = engine.ecs.getComponent(entityId, "ParticlePool");
4128
4553
  if (!pool) return;
4129
4554
  pool.active = active;
@@ -4357,7 +4782,7 @@ function Checkpoint({
4357
4782
  }
4358
4783
 
4359
4784
  // src/components/Tilemap.tsx
4360
- 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";
4361
4786
  import { Fragment as Fragment4, jsx as jsx8 } from "react/jsx-runtime";
4362
4787
  var animatedTiles = /* @__PURE__ */ new Map();
4363
4788
  function getProperty(props, name) {
@@ -4382,9 +4807,9 @@ function Tilemap({
4382
4807
  onTileProperty,
4383
4808
  navGrid
4384
4809
  }) {
4385
- const engine = useContext16(EngineContext);
4810
+ const engine = useContext17(EngineContext);
4386
4811
  const [spawnedNodes, setSpawnedNodes] = useState5([]);
4387
- useEffect18(() => {
4812
+ useEffect19(() => {
4388
4813
  if (!engine) return;
4389
4814
  const createdEntities = [];
4390
4815
  async function load() {
@@ -4575,7 +5000,7 @@ function Tilemap({
4575
5000
  }
4576
5001
 
4577
5002
  // src/components/ParallaxLayer.tsx
4578
- import { useEffect as useEffect19, useContext as useContext17 } from "react";
5003
+ import { useEffect as useEffect20, useContext as useContext18 } from "react";
4579
5004
  import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
4580
5005
  function ParallaxLayerInner({
4581
5006
  src,
@@ -4587,9 +5012,9 @@ function ParallaxLayerInner({
4587
5012
  offsetX,
4588
5013
  offsetY
4589
5014
  }) {
4590
- const engine = useContext17(EngineContext);
4591
- const entityId = useContext17(EntityContext);
4592
- useEffect19(() => {
5015
+ const engine = useContext18(EngineContext);
5016
+ const entityId = useContext18(EntityContext);
5017
+ useEffect20(() => {
4593
5018
  engine.ecs.addComponent(entityId, {
4594
5019
  type: "ParallaxLayer",
4595
5020
  src,
@@ -4605,7 +5030,7 @@ function ParallaxLayerInner({
4605
5030
  });
4606
5031
  return () => engine.ecs.removeComponent(entityId, "ParallaxLayer");
4607
5032
  }, []);
4608
- useEffect19(() => {
5033
+ useEffect20(() => {
4609
5034
  const layer = engine.ecs.getComponent(entityId, "ParallaxLayer");
4610
5035
  if (!layer) return;
4611
5036
  layer.src = src;
@@ -4687,7 +5112,7 @@ var ScreenFlash = forwardRef((_, ref) => {
4687
5112
  ScreenFlash.displayName = "ScreenFlash";
4688
5113
 
4689
5114
  // src/components/CameraZone.tsx
4690
- 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";
4691
5116
  import { Fragment as Fragment5, jsx as jsx11 } from "react/jsx-runtime";
4692
5117
  function CameraZone({
4693
5118
  x,
@@ -4699,10 +5124,10 @@ function CameraZone({
4699
5124
  targetY,
4700
5125
  children
4701
5126
  }) {
4702
- const engine = useContext18(EngineContext);
5127
+ const engine = useContext19(EngineContext);
4703
5128
  const prevFollowRef = useRef6(void 0);
4704
5129
  const activeRef = useRef6(false);
4705
- useEffect20(() => {
5130
+ useEffect21(() => {
4706
5131
  const eid = engine.ecs.createEntity();
4707
5132
  engine.ecs.addComponent(eid, createScript(() => {
4708
5133
  const cam = engine.ecs.queryOne("Camera2D");
@@ -4749,26 +5174,56 @@ function CameraZone({
4749
5174
  }
4750
5175
 
4751
5176
  // src/components/Trail.tsx
4752
- import { useEffect as useEffect21, useContext as useContext19 } from "react";
5177
+ import { useEffect as useEffect22, useContext as useContext20 } from "react";
4753
5178
  function Trail({ length = 20, color = "#ffffff", width = 3 }) {
4754
- const engine = useContext19(EngineContext);
4755
- const entityId = useContext19(EntityContext);
4756
- useEffect21(() => {
5179
+ const engine = useContext20(EngineContext);
5180
+ const entityId = useContext20(EntityContext);
5181
+ useEffect22(() => {
4757
5182
  engine.ecs.addComponent(entityId, createTrail({ length, color, width }));
4758
5183
  return () => engine.ecs.removeComponent(entityId, "Trail");
4759
5184
  }, []);
4760
5185
  return null;
4761
5186
  }
4762
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
+
4763
5218
  // src/components/AssetLoader.tsx
4764
- import { useEffect as useEffect23 } from "react";
5219
+ import { useEffect as useEffect25 } from "react";
4765
5220
 
4766
5221
  // src/hooks/usePreload.ts
4767
- 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";
4768
5223
  function usePreload(assets) {
4769
- const engine = useContext20(EngineContext);
5224
+ const engine = useContext22(EngineContext);
4770
5225
  const [state, setState] = useState6({ progress: assets.length === 0 ? 1 : 0, loaded: assets.length === 0, error: null });
4771
- useEffect22(() => {
5226
+ useEffect24(() => {
4772
5227
  if (assets.length === 0) {
4773
5228
  setState({ progress: 1, loaded: true, error: null });
4774
5229
  return;
@@ -4799,7 +5254,7 @@ function usePreload(assets) {
4799
5254
  import { Fragment as Fragment6, jsx as jsx12 } from "react/jsx-runtime";
4800
5255
  function AssetLoader({ assets, fallback = null, onError, children }) {
4801
5256
  const { loaded, error } = usePreload(assets);
4802
- useEffect23(() => {
5257
+ useEffect25(() => {
4803
5258
  if (error && onError) onError(error);
4804
5259
  }, [error, onError]);
4805
5260
  if (!loaded) {
@@ -4809,9 +5264,9 @@ function AssetLoader({ assets, fallback = null, onError, children }) {
4809
5264
  }
4810
5265
 
4811
5266
  // src/hooks/useGame.ts
4812
- import { useContext as useContext21 } from "react";
5267
+ import { useContext as useContext23 } from "react";
4813
5268
  function useGame() {
4814
- const engine = useContext21(EngineContext);
5269
+ const engine = useContext23(EngineContext);
4815
5270
  if (!engine) throw new Error("useGame must be used inside <Game>");
4816
5271
  return engine;
4817
5272
  }
@@ -4868,18 +5323,18 @@ function useSnapshot() {
4868
5323
  }
4869
5324
 
4870
5325
  // src/hooks/useEntity.ts
4871
- import { useContext as useContext22 } from "react";
5326
+ import { useContext as useContext24 } from "react";
4872
5327
  function useEntity() {
4873
- const id = useContext22(EntityContext);
5328
+ const id = useContext24(EntityContext);
4874
5329
  if (id === null) throw new Error("useEntity must be used inside <Entity>");
4875
5330
  return id;
4876
5331
  }
4877
5332
 
4878
5333
  // src/hooks/useDestroyEntity.ts
4879
- import { useCallback as useCallback2, useContext as useContext23 } from "react";
5334
+ import { useCallback as useCallback2, useContext as useContext25 } from "react";
4880
5335
  function useDestroyEntity() {
4881
- const engine = useContext23(EngineContext);
4882
- const entityId = useContext23(EntityContext);
5336
+ const engine = useContext25(EngineContext);
5337
+ const entityId = useContext25(EntityContext);
4883
5338
  if (!engine) throw new Error("useDestroyEntity must be used inside <Game>");
4884
5339
  if (entityId === null) throw new Error("useDestroyEntity must be used inside <Entity>");
4885
5340
  return useCallback2(() => {
@@ -4890,9 +5345,9 @@ function useDestroyEntity() {
4890
5345
  }
4891
5346
 
4892
5347
  // src/hooks/useInput.ts
4893
- import { useContext as useContext24 } from "react";
5348
+ import { useContext as useContext26 } from "react";
4894
5349
  function useInput() {
4895
- const engine = useContext24(EngineContext);
5350
+ const engine = useContext26(EngineContext);
4896
5351
  if (!engine) throw new Error("useInput must be used inside <Game>");
4897
5352
  return engine.input;
4898
5353
  }
@@ -4912,9 +5367,9 @@ function useInputMap(bindings) {
4912
5367
  }
4913
5368
 
4914
5369
  // src/hooks/useEvents.ts
4915
- 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";
4916
5371
  function useEvents() {
4917
- const engine = useContext25(EngineContext);
5372
+ const engine = useContext27(EngineContext);
4918
5373
  if (!engine) throw new Error("useEvents must be used inside <Game>");
4919
5374
  return engine.events;
4920
5375
  }
@@ -4922,15 +5377,15 @@ function useEvent(event, handler) {
4922
5377
  const events = useEvents();
4923
5378
  const handlerRef = useRef7(handler);
4924
5379
  handlerRef.current = handler;
4925
- useEffect24(() => {
5380
+ useEffect26(() => {
4926
5381
  return events.on(event, (data) => handlerRef.current(data));
4927
5382
  }, [events, event]);
4928
5383
  }
4929
5384
 
4930
5385
  // src/hooks/useCoordinates.ts
4931
- import { useCallback as useCallback3, useContext as useContext26 } from "react";
5386
+ import { useCallback as useCallback3, useContext as useContext28 } from "react";
4932
5387
  function useCoordinates() {
4933
- const engine = useContext26(EngineContext);
5388
+ const engine = useContext28(EngineContext);
4934
5389
  const worldToScreen = useCallback3((wx, wy) => {
4935
5390
  const canvas = engine.canvas;
4936
5391
  const camId = engine.ecs.queryOne("Camera2D");
@@ -4957,9 +5412,9 @@ function useCoordinates() {
4957
5412
  }
4958
5413
 
4959
5414
  // src/hooks/useInputContext.ts
4960
- import { useEffect as useEffect25, useMemo as useMemo4 } from "react";
5415
+ import { useEffect as useEffect27, useMemo as useMemo4 } from "react";
4961
5416
  function useInputContext(ctx) {
4962
- useEffect25(() => {
5417
+ useEffect27(() => {
4963
5418
  if (!ctx) return;
4964
5419
  globalInputContext.push(ctx);
4965
5420
  return () => globalInputContext.pop(ctx);
@@ -4998,12 +5453,12 @@ function useInputRecorder() {
4998
5453
  }
4999
5454
 
5000
5455
  // src/hooks/useGamepad.ts
5001
- 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";
5002
5457
  var EMPTY_STATE = { connected: false, axes: [], buttons: [] };
5003
5458
  function useGamepad(playerIndex = 0) {
5004
5459
  const [state, setState] = useState7(EMPTY_STATE);
5005
5460
  const rafRef = useRef8(0);
5006
- useEffect26(() => {
5461
+ useEffect28(() => {
5007
5462
  const poll = () => {
5008
5463
  const gp = navigator.getGamepads()[playerIndex];
5009
5464
  if (gp) {
@@ -5024,9 +5479,9 @@ function useGamepad(playerIndex = 0) {
5024
5479
  }
5025
5480
 
5026
5481
  // src/hooks/usePause.ts
5027
- 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";
5028
5483
  function usePause() {
5029
- const engine = useContext27(EngineContext);
5484
+ const engine = useContext29(EngineContext);
5030
5485
  const [paused, setPaused] = useState8(false);
5031
5486
  const pause = useCallback4(() => {
5032
5487
  engine.loop.pause();
@@ -5095,19 +5550,19 @@ function useAISteering() {
5095
5550
  }
5096
5551
 
5097
5552
  // ../gameplay/src/hooks/useDamageZone.ts
5098
- import { useContext as useContext28 } from "react";
5553
+ import { useContext as useContext30 } from "react";
5099
5554
  function useDamageZone(damage, opts = {}) {
5100
- const engine = useContext28(EngineContext);
5555
+ const engine = useContext30(EngineContext);
5101
5556
  useTriggerEnter((other) => {
5102
5557
  engine.events.emit(`damage:${other}`, { amount: damage });
5103
5558
  }, { tag: opts.tag, layer: opts.layer });
5104
5559
  }
5105
5560
 
5106
5561
  // ../gameplay/src/hooks/useDropThrough.ts
5107
- import { useContext as useContext29, useCallback as useCallback7 } from "react";
5562
+ import { useContext as useContext31, useCallback as useCallback7 } from "react";
5108
5563
  function useDropThrough(frames = 8) {
5109
- const engine = useContext29(EngineContext);
5110
- const entityId = useContext29(EntityContext);
5564
+ const engine = useContext31(EngineContext);
5565
+ const entityId = useContext31(EntityContext);
5111
5566
  const dropThrough = useCallback7(() => {
5112
5567
  const rb = engine.ecs.getComponent(entityId, "RigidBody");
5113
5568
  if (rb) rb.dropThrough = frames;
@@ -5116,14 +5571,14 @@ function useDropThrough(frames = 8) {
5116
5571
  }
5117
5572
 
5118
5573
  // ../gameplay/src/hooks/useGameStateMachine.ts
5119
- 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";
5120
5575
  function useGameStateMachine(states, initial) {
5121
- const engine = useContext30(EngineContext);
5576
+ const engine = useContext32(EngineContext);
5122
5577
  const [state, setState] = useState10(initial);
5123
5578
  const stateRef = useRef9(initial);
5124
5579
  const statesRef = useRef9(states);
5125
5580
  statesRef.current = states;
5126
- useEffect27(() => {
5581
+ useEffect29(() => {
5127
5582
  statesRef.current[initial]?.onEnter?.();
5128
5583
  }, []);
5129
5584
  const transition = useCallback8((to) => {
@@ -5134,7 +5589,7 @@ function useGameStateMachine(states, initial) {
5134
5589
  setState(to);
5135
5590
  statesRef.current[to]?.onEnter?.();
5136
5591
  }, []);
5137
- useEffect27(() => {
5592
+ useEffect29(() => {
5138
5593
  const eid = engine.ecs.createEntity();
5139
5594
  engine.ecs.addComponent(eid, createScript((_id, _world, _input, dt) => {
5140
5595
  statesRef.current[stateRef.current]?.onUpdate?.(dt);
@@ -5147,19 +5602,19 @@ function useGameStateMachine(states, initial) {
5147
5602
  }
5148
5603
 
5149
5604
  // ../gameplay/src/hooks/useHealth.ts
5150
- 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";
5151
5606
  function useHealth(maxHp, opts = {}) {
5152
- const engine = useContext31(EngineContext);
5153
- const entityId = useContext31(EntityContext);
5607
+ const engine = useContext33(EngineContext);
5608
+ const entityId = useContext33(EntityContext);
5154
5609
  const hpRef = useRef10(maxHp);
5155
5610
  const invincibleRef = useRef10(false);
5156
5611
  const iFrameDuration = opts.iFrames ?? 1;
5157
5612
  const onDeathRef = useRef10(opts.onDeath);
5158
5613
  const onDamageRef = useRef10(opts.onDamage);
5159
- useEffect28(() => {
5614
+ useEffect30(() => {
5160
5615
  onDeathRef.current = opts.onDeath;
5161
5616
  });
5162
- useEffect28(() => {
5617
+ useEffect30(() => {
5163
5618
  onDamageRef.current = opts.onDamage;
5164
5619
  });
5165
5620
  const timerRef = useRef10(
@@ -5178,10 +5633,10 @@ function useHealth(maxHp, opts = {}) {
5178
5633
  if (hpRef.current <= 0) onDeathRef.current?.();
5179
5634
  }, [iFrameDuration]);
5180
5635
  const takeDamageRef = useRef10(takeDamage);
5181
- useEffect28(() => {
5636
+ useEffect30(() => {
5182
5637
  takeDamageRef.current = takeDamage;
5183
5638
  }, [takeDamage]);
5184
- useEffect28(() => {
5639
+ useEffect30(() => {
5185
5640
  return engine.events.on(`damage:${entityId}`, ({ amount }) => {
5186
5641
  takeDamageRef.current(amount);
5187
5642
  });
@@ -5216,10 +5671,10 @@ function useHealth(maxHp, opts = {}) {
5216
5671
  }
5217
5672
 
5218
5673
  // ../gameplay/src/hooks/useKinematicBody.ts
5219
- import { useContext as useContext32, useCallback as useCallback10 } from "react";
5674
+ import { useContext as useContext34, useCallback as useCallback10 } from "react";
5220
5675
  function useKinematicBody() {
5221
- const engine = useContext32(EngineContext);
5222
- const entityId = useContext32(EntityContext);
5676
+ const engine = useContext34(EngineContext);
5677
+ const entityId = useContext34(EntityContext);
5223
5678
  const moveAndCollide = useCallback10((dx, dy) => {
5224
5679
  const transform = engine.ecs.getComponent(entityId, "Transform");
5225
5680
  if (!transform) return { dx: 0, dy: 0 };
@@ -5306,13 +5761,13 @@ function useLevelTransition(initial) {
5306
5761
  }
5307
5762
 
5308
5763
  // ../gameplay/src/hooks/usePlatformerController.ts
5309
- import { useContext as useContext33, useEffect as useEffect29 } from "react";
5764
+ import { useContext as useContext35, useEffect as useEffect31 } from "react";
5310
5765
  function normalizeKeys(val, defaults) {
5311
5766
  if (!val) return defaults;
5312
5767
  return Array.isArray(val) ? val : [val];
5313
5768
  }
5314
5769
  function usePlatformerController(entityId, opts = {}) {
5315
- const engine = useContext33(EngineContext);
5770
+ const engine = useContext35(EngineContext);
5316
5771
  const {
5317
5772
  speed = 200,
5318
5773
  jumpForce = -500,
@@ -5325,7 +5780,7 @@ function usePlatformerController(entityId, opts = {}) {
5325
5780
  const leftKeys = normalizeKeys(bindings?.left, ["ArrowLeft", "KeyA", "a"]);
5326
5781
  const rightKeys = normalizeKeys(bindings?.right, ["ArrowRight", "KeyD", "d"]);
5327
5782
  const jumpKeys = normalizeKeys(bindings?.jump, ["Space", "ArrowUp", "KeyW", "w"]);
5328
- useEffect29(() => {
5783
+ useEffect31(() => {
5329
5784
  const state = {
5330
5785
  coyoteTimer: 0,
5331
5786
  jumpBuffer: 0,
@@ -5389,9 +5844,9 @@ function usePathfinding() {
5389
5844
  }
5390
5845
 
5391
5846
  // ../gameplay/src/hooks/usePersistedBindings.ts
5392
- 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";
5393
5848
  function usePersistedBindings(storageKey, defaults) {
5394
- const engine = useContext34(EngineContext);
5849
+ const engine = useContext36(EngineContext);
5395
5850
  const input = engine.input;
5396
5851
  const [bindings, setBindings] = useState12(() => {
5397
5852
  try {
@@ -5496,11 +5951,11 @@ function useSave(key, defaultValue, opts = {}) {
5496
5951
  }
5497
5952
 
5498
5953
  // ../gameplay/src/hooks/useTopDownMovement.ts
5499
- import { useContext as useContext35, useEffect as useEffect30 } from "react";
5954
+ import { useContext as useContext37, useEffect as useEffect32 } from "react";
5500
5955
  function useTopDownMovement(entityId, opts = {}) {
5501
- const engine = useContext35(EngineContext);
5956
+ const engine = useContext37(EngineContext);
5502
5957
  const { speed = 200, normalizeDiagonal = true } = opts;
5503
- useEffect30(() => {
5958
+ useEffect32(() => {
5504
5959
  const updateFn = (id, world, input) => {
5505
5960
  if (!world.hasEntity(id)) return;
5506
5961
  const rb = world.getComponent(id, "RigidBody");
@@ -5524,8 +5979,190 @@ function useTopDownMovement(entityId, opts = {}) {
5524
5979
  }, []);
5525
5980
  }
5526
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
+
5527
6164
  // ../../packages/audio/src/useSound.ts
5528
- import { useEffect as useEffect31, useRef as useRef13 } from "react";
6165
+ import { useEffect as useEffect34, useRef as useRef15 } from "react";
5529
6166
  var _audioCtx = null;
5530
6167
  function getAudioCtx() {
5531
6168
  if (!_audioCtx) _audioCtx = new AudioContext();
@@ -5592,13 +6229,13 @@ async function loadBuffer(src) {
5592
6229
  return buf;
5593
6230
  }
5594
6231
  function useSound(src, opts = {}) {
5595
- const bufferRef = useRef13(null);
5596
- const sourceRef = useRef13(null);
5597
- const gainRef = useRef13(null);
5598
- const volRef = useRef13(opts.volume ?? 1);
5599
- const loopRef = useRef13(opts.loop ?? false);
5600
- const groupRef = useRef13(opts.group);
5601
- 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(() => {
5602
6239
  let cancelled = false;
5603
6240
  loadBuffer(src).then((buf) => {
5604
6241
  if (!cancelled) bufferRef.current = buf;
@@ -5742,10 +6379,12 @@ export {
5742
6379
  CapsuleCollider,
5743
6380
  Checkpoint,
5744
6381
  CircleCollider,
6382
+ CompoundCollider,
5745
6383
  Ease,
5746
6384
  Entity,
5747
6385
  Game,
5748
6386
  MovingPlatform,
6387
+ NineSlice,
5749
6388
  ParallaxLayer,
5750
6389
  ParticleEmitter,
5751
6390
  RenderSystem,
@@ -5762,8 +6401,10 @@ export {
5762
6401
  World,
5763
6402
  arrive,
5764
6403
  createAtlas,
6404
+ createCompoundCollider,
5765
6405
  createInputMap,
5766
6406
  createInputRecorder,
6407
+ createNineSlice,
5767
6408
  createPlayerInput,
5768
6409
  createSprite,
5769
6410
  createTag,
@@ -5775,6 +6416,7 @@ export {
5775
6416
  flee,
5776
6417
  getGroupVolume,
5777
6418
  globalInputContext,
6419
+ hotReloadPlugin,
5778
6420
  overlapBox,
5779
6421
  overlapCircle,
5780
6422
  patrol,
@@ -5798,14 +6440,17 @@ export {
5798
6440
  useCollisionExit,
5799
6441
  useCollisionStay,
5800
6442
  useCoordinates,
6443
+ useCutscene,
5801
6444
  useDamageZone,
5802
6445
  useDestroyEntity,
6446
+ useDialogue,
5803
6447
  useDropThrough,
5804
6448
  useEntity,
5805
6449
  useEvent,
5806
6450
  useEvents,
5807
6451
  useGame,
5808
6452
  useGameStateMachine,
6453
+ useGameStore,
5809
6454
  useGamepad,
5810
6455
  useHealth,
5811
6456
  useInput,