cubeforge 0.7.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +87 -2
  2. package/dist/index.js +1066 -285
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -83,6 +83,20 @@ function applyDeltaSnapshot(baseline, delta) {
83
83
  }
84
84
  return { nextId: delta.nextId, rngState: delta.rngState, entities };
85
85
  }
86
+ function _componentsChanged(a, b) {
87
+ if (a.length !== b.length) return true;
88
+ for (let i = 0; i < a.length; i++) {
89
+ const ca = a[i];
90
+ const cb = b[i];
91
+ if (ca["type"] !== cb["type"]) return true;
92
+ const keysA = Object.keys(ca);
93
+ if (keysA.length !== Object.keys(cb).length) return true;
94
+ for (const k of keysA) {
95
+ if (ca[k] !== cb[k]) return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
86
100
  var ECSWorld = class {
87
101
  nextId = 0;
88
102
  // Secondary index: O(1) single-entity component lookup
@@ -201,6 +215,23 @@ var ECSWorld = class {
201
215
  hasComponent(id, type) {
202
216
  return this.componentIndex.get(id)?.has(type) ?? false;
203
217
  }
218
+ /**
219
+ * Returns live references to all components on an entity.
220
+ * Useful for editor / inspector tooling. Returns an empty array if the
221
+ * entity does not exist.
222
+ *
223
+ * **Do not mutate structural fields** (e.g. `type`) — doing so will
224
+ * desync the archetype index. Mutating value fields (x, y, color …) is safe.
225
+ */
226
+ getEntityComponents(id) {
227
+ const map = this.componentIndex.get(id);
228
+ if (!map) return [];
229
+ return [...map.values()];
230
+ }
231
+ /** Returns all live entity IDs currently in the world. */
232
+ getAllEntityIds() {
233
+ return [...this.componentIndex.keys()];
234
+ }
204
235
  // Flush pending dirty flags into the query cache immediately.
205
236
  // Called inline at the top of query() so any mid-frame mutation
206
237
  // (destroyEntity, addComponent, removeComponent) is reflected before
@@ -437,7 +468,7 @@ var ECSWorld = class {
437
468
  const removed = [];
438
469
  for (const entity of current.entities) {
439
470
  const base = baseMap.get(entity.id);
440
- if (!base || JSON.stringify(entity.components) !== JSON.stringify(base.components)) {
471
+ if (!base || _componentsChanged(entity.components, base.components)) {
441
472
  changed.push(entity);
442
473
  }
443
474
  }
@@ -2932,6 +2963,7 @@ function getSamplingKey(sampling) {
2932
2963
  return `${sampling.min}|${sampling.mag}`;
2933
2964
  }
2934
2965
  function getTextureKey(sprite) {
2966
+ if (sprite.dynamicSrc) return sprite.dynamicSrc;
2935
2967
  const src = sprite.image?.src || sprite.src;
2936
2968
  const samplingKey = getSamplingKey(sprite.sampling);
2937
2969
  const suffix = samplingKey ? `:s=${samplingKey}` : "";
@@ -3128,6 +3160,15 @@ var RenderSystem = class {
3128
3160
  // FPS tracking
3129
3161
  frameTimes = [];
3130
3162
  lastTimestamp = 0;
3163
+ // ── Dynamic canvas textures (texSubImage2D optimization) ─────────────────
3164
+ _dynamicCanvases = /* @__PURE__ */ new Map();
3165
+ // ── Idle frame skip ──────────────────────────────────────────────────────
3166
+ _idleSkip = false;
3167
+ _prevSceneHash = -1;
3168
+ _idleFBO = null;
3169
+ _idleTex = null;
3170
+ _idleFBOW = 0;
3171
+ _idleFBOH = 0;
3131
3172
  // ── Post-process ─────────────────────────────────────────────────────────
3132
3173
  _ppOptions = {};
3133
3174
  // Lazily created programs (null until first PP use)
@@ -3179,6 +3220,55 @@ var RenderSystem = class {
3179
3220
  setPostProcessOptions(opts) {
3180
3221
  this._ppOptions = opts;
3181
3222
  }
3223
+ /**
3224
+ * Enable idle frame skip. When enabled the renderer computes a hash of all
3225
+ * visible entity positions, frame indices, and camera state each frame. If
3226
+ * the hash matches the previous frame the GPU draw calls are skipped entirely
3227
+ * and the cached scene is blitted to screen instead.
3228
+ *
3229
+ * Best for scenes that are frequently static (pause menus, cutscenes, HUDs).
3230
+ * Animations and active particles automatically disable the skip for that frame.
3231
+ */
3232
+ setIdleFrameSkip(enabled) {
3233
+ this._idleSkip = enabled;
3234
+ }
3235
+ /**
3236
+ * Register an HTMLCanvasElement as a GPU texture that the renderer can sample.
3237
+ * The canvas is uploaded once on registration. Call {@link markDynamicCanvasDirty}
3238
+ * to schedule a re-upload before the next frame.
3239
+ *
3240
+ * Use the returned `id` as the `dynamicSrc` on a `<Sprite>` component.
3241
+ */
3242
+ registerDynamicCanvas(id, canvas) {
3243
+ const { gl } = this;
3244
+ const tex = gl.createTexture();
3245
+ gl.bindTexture(gl.TEXTURE_2D, tex);
3246
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
3247
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3248
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3249
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3250
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3251
+ this._dynamicCanvases.set(id, { canvas, tex, dirty: false });
3252
+ this.textures.set(id, tex);
3253
+ this.touchTexture(id);
3254
+ }
3255
+ /**
3256
+ * Mark a dynamic canvas as modified so it will be re-uploaded to the GPU
3257
+ * (via `texSubImage2D`) at the start of the next rendered frame.
3258
+ * Only call this after actually drawing new content to the canvas.
3259
+ */
3260
+ markDynamicCanvasDirty(id) {
3261
+ const entry = this._dynamicCanvases.get(id);
3262
+ if (entry) entry.dirty = true;
3263
+ }
3264
+ /** Remove a registered dynamic canvas and free its GPU texture. */
3265
+ unregisterDynamicCanvas(id) {
3266
+ const entry = this._dynamicCanvases.get(id);
3267
+ if (!entry) return;
3268
+ this.gl.deleteTexture(entry.tex);
3269
+ this._dynamicCanvases.delete(id);
3270
+ this.textures.delete(id);
3271
+ }
3182
3272
  get _anyPPEnabled() {
3183
3273
  const o = this._ppOptions;
3184
3274
  return (o.bloom?.enabled ?? false) || (o.vignette?.enabled ?? false) || (o.chromaticAberration?.enabled ?? false) || (o.scanlines?.enabled ?? false);
@@ -3501,6 +3591,71 @@ var RenderSystem = class {
3501
3591
  break;
3502
3592
  }
3503
3593
  }
3594
+ // ── Scene hash for idle frame skip ───────────────────────────────────────
3595
+ _computeSceneHash(world, camX, camY, zoom, shakeX, shakeY) {
3596
+ let h = 2166136261;
3597
+ const mix = (n) => {
3598
+ const i = n | 0;
3599
+ h = Math.imul(h ^ i & 255, 16777619);
3600
+ h = Math.imul(h ^ i >> 8 & 255, 16777619);
3601
+ h = Math.imul(h ^ i >> 16 & 255, 16777619);
3602
+ h = Math.imul(h ^ i >> 24 & 255, 16777619);
3603
+ };
3604
+ mix(camX * 100);
3605
+ mix(camY * 100);
3606
+ mix(zoom * 1e3);
3607
+ mix(shakeX * 1e3);
3608
+ mix(shakeY * 1e3);
3609
+ for (const id of world.query("Transform", "Sprite")) {
3610
+ const t = world.getComponent(id, "Transform");
3611
+ const s2 = world.getComponent(id, "Sprite");
3612
+ mix(id);
3613
+ mix(t.x * 100);
3614
+ mix(t.y * 100);
3615
+ mix(t.rotation * 1e3);
3616
+ mix(t.scaleX * 1e3);
3617
+ mix(t.scaleY * 1e3);
3618
+ mix(s2.frameIndex);
3619
+ mix(s2.visible ? 1 : 0);
3620
+ mix((s2.opacity ?? 1) * 255);
3621
+ }
3622
+ for (const id of world.query("AnimationState")) {
3623
+ const anim = world.getComponent(id, "AnimationState");
3624
+ if (anim.playing) return -1;
3625
+ }
3626
+ for (const id of world.query("ParticlePool")) {
3627
+ const pool = world.getComponent(id, "ParticlePool");
3628
+ if (pool.active || pool.particles.length > 0) return -1;
3629
+ }
3630
+ for (const id of world.query("SquashStretch")) {
3631
+ const ss = world.getComponent(id, "SquashStretch");
3632
+ mix(ss.currentScaleX * 1e3);
3633
+ mix(ss.currentScaleY * 1e3);
3634
+ }
3635
+ return h;
3636
+ }
3637
+ _ensureIdleFBO(w, h) {
3638
+ if (this._idleFBOW === w && this._idleFBOH === h && this._idleFBO) return;
3639
+ const { gl } = this;
3640
+ if (this._idleFBO) gl.deleteFramebuffer(this._idleFBO);
3641
+ if (this._idleTex) gl.deleteTexture(this._idleTex);
3642
+ const tex = gl.createTexture();
3643
+ gl.bindTexture(gl.TEXTURE_2D, tex);
3644
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
3645
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3646
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3647
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3648
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3649
+ const fbo = gl.createFramebuffer();
3650
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
3651
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
3652
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3653
+ gl.bindTexture(gl.TEXTURE_2D, null);
3654
+ this._idleFBO = fbo;
3655
+ this._idleTex = tex;
3656
+ this._idleFBOW = w;
3657
+ this._idleFBOH = h;
3658
+ }
3504
3659
  // ── Instanced draw call ────────────────────────────────────────────────────
3505
3660
  flush(count, textureKey, sampling, blendMode, shapeSpriteRef) {
3506
3661
  if (count === 0) return;
@@ -3721,6 +3876,24 @@ var RenderSystem = class {
3721
3876
  ss.currentScaleX += (tScX - ss.currentScaleX) * ss.recovery * dt;
3722
3877
  ss.currentScaleY += (tScY - ss.currentScaleY) * ss.recovery * dt;
3723
3878
  }
3879
+ for (const entry of this._dynamicCanvases.values()) {
3880
+ if (!entry.dirty) continue;
3881
+ gl.bindTexture(gl.TEXTURE_2D, entry.tex);
3882
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, entry.canvas);
3883
+ entry.dirty = false;
3884
+ }
3885
+ if (this._idleSkip) {
3886
+ this._ensureIdleFBO(W, H);
3887
+ const hash = this._computeSceneHash(world, camX, camY, zoom, shakeX, shakeY);
3888
+ if (hash !== -1 && hash === this._prevSceneHash) {
3889
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this._idleFBO);
3890
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
3891
+ gl.blitFramebuffer(0, 0, W, H, 0, 0, W, H, gl.COLOR_BUFFER_BIT, gl.NEAREST);
3892
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3893
+ return;
3894
+ }
3895
+ this._prevSceneHash = hash;
3896
+ }
3724
3897
  const ppEnabled = this._anyPPEnabled;
3725
3898
  if (ppEnabled) {
3726
3899
  this._ensurePPPrograms();
@@ -3866,7 +4039,7 @@ var RenderSystem = class {
3866
4039
  const ss = world.getComponent(id, "SquashStretch");
3867
4040
  const scaleXMod = ss ? ss.currentScaleX : 1;
3868
4041
  const scaleYMod = ss ? ss.currentScaleY : 1;
3869
- const hasTexture = sprite.image && sprite.image.complete && sprite.image.naturalWidth > 0;
4042
+ const hasTexture = sprite.image && sprite.image.complete && sprite.image.naturalWidth > 0 || sprite.dynamicSrc !== void 0 && this._dynamicCanvases.has(sprite.dynamicSrc);
3870
4043
  const opacity = sprite.opacity ?? 1;
3871
4044
  let [r, g, b, a] = hasTexture ? [1, 1, 1, 1] : parseCSSColor(sprite.color);
3872
4045
  a *= opacity;
@@ -4337,6 +4510,12 @@ var RenderSystem = class {
4337
4510
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
4338
4511
  this._applyPostProcess(W, H);
4339
4512
  }
4513
+ if (this._idleSkip && this._idleFBO) {
4514
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
4515
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._idleFBO);
4516
+ gl.blitFramebuffer(0, 0, W, H, 0, 0, W, H, gl.COLOR_BUFFER_BIT, gl.NEAREST);
4517
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
4518
+ }
4340
4519
  const now = performance.now();
4341
4520
  if (this.lastTimestamp > 0) {
4342
4521
  this.frameTimes.push(now - this.lastTimestamp);
@@ -7206,6 +7385,7 @@ function Sprite({
7206
7385
  height,
7207
7386
  color = "#ffffff",
7208
7387
  src,
7388
+ dynamicSrc,
7209
7389
  offsetX = 0,
7210
7390
  offsetY = 0,
7211
7391
  zIndex = 0,
@@ -7257,6 +7437,7 @@ function Sprite({
7257
7437
  height,
7258
7438
  color,
7259
7439
  src,
7440
+ dynamicSrc,
7260
7441
  offsetX,
7261
7442
  offsetY,
7262
7443
  zIndex,
@@ -7338,6 +7519,7 @@ function Sprite({
7338
7519
  const comp = engine.ecs.getComponent(entityId, "Sprite");
7339
7520
  if (!comp) return;
7340
7521
  comp.color = color;
7522
+ comp.dynamicSrc = dynamicSrc;
7341
7523
  comp.visible = visible;
7342
7524
  comp.flipX = flipX;
7343
7525
  comp.flipY = flipY;
@@ -7357,6 +7539,7 @@ function Sprite({
7357
7539
  comp.starInnerRadius = starInnerRadius;
7358
7540
  }, [
7359
7541
  color,
7542
+ dynamicSrc,
7360
7543
  visible,
7361
7544
  flipX,
7362
7545
  flipY,
@@ -8597,7 +8780,6 @@ function Checkpoint({
8597
8780
  // src/components/Tilemap.tsx
8598
8781
  import { useEffect as useEffect30, useState as useState6, useContext as useContext20 } from "react";
8599
8782
  import { Fragment as Fragment5, jsx as jsx10 } from "react/jsx-runtime";
8600
- var animatedTiles = /* @__PURE__ */ new Map();
8601
8783
  function getProperty(props, name) {
8602
8784
  return props?.find((p) => p.name === name)?.value;
8603
8785
  }
@@ -8629,6 +8811,7 @@ function Tilemap({
8629
8811
  useEffect30(() => {
8630
8812
  if (!engine) return;
8631
8813
  const createdEntities = [];
8814
+ const animatedTiles = /* @__PURE__ */ new Map();
8632
8815
  async function load() {
8633
8816
  let mapData;
8634
8817
  try {
@@ -8764,6 +8947,7 @@ function Tilemap({
8764
8947
  if (frame) {
8765
8948
  sprite.frame = { sx: frame.sx, sy: frame.sy, sw: frame.sw, sh: frame.sh };
8766
8949
  engine.assets.loadImage(frame.imageSrc).then((img) => {
8950
+ if (!engine.ecs.hasEntity(eid)) return;
8767
8951
  const s2 = engine.ecs.getComponent(eid, "Sprite");
8768
8952
  if (s2) s2.image = img;
8769
8953
  }).catch(() => {
@@ -8779,6 +8963,7 @@ function Tilemap({
8779
8963
  animatedTiles.set(eid, state);
8780
8964
  const firstFrameRegion = getFrameForLocalId(resolved.tileset, frames[0]);
8781
8965
  engine.assets.loadImage(firstFrameRegion.imageSrc).then((img) => {
8966
+ if (!engine.ecs.hasEntity(eid)) return;
8782
8967
  const s2 = engine.ecs.getComponent(eid, "Sprite");
8783
8968
  if (s2) {
8784
8969
  s2.image = img;
@@ -10256,11 +10441,51 @@ function useWebGLPostProcess(opts) {
10256
10441
  }, [engine, optsKey]);
10257
10442
  }
10258
10443
 
10444
+ // src/hooks/useDynamicCanvas.ts
10445
+ import { useCallback as useCallback7, useEffect as useEffect59, useMemo as useMemo10, useRef as useRef23 } from "react";
10446
+ function useDynamicCanvas(width, height) {
10447
+ const engine = useGame();
10448
+ const idRef = useRef23(`__dynamic__:${Math.random().toString(36).slice(2)}`);
10449
+ const id = idRef.current;
10450
+ const canvas = useMemo10(() => {
10451
+ const c = document.createElement("canvas");
10452
+ c.width = width;
10453
+ c.height = height;
10454
+ return c;
10455
+ }, [width, height]);
10456
+ const ctx = useMemo10(() => canvas.getContext("2d"), [canvas]);
10457
+ useEffect59(() => {
10458
+ const rs = engine.activeRenderSystem;
10459
+ rs.registerDynamicCanvas?.(id, canvas);
10460
+ return () => {
10461
+ rs.unregisterDynamicCanvas?.(id);
10462
+ };
10463
+ }, [engine, id, canvas]);
10464
+ const markDirty = useCallback7(() => {
10465
+ const rs = engine.activeRenderSystem;
10466
+ rs.markDynamicCanvasDirty?.(id);
10467
+ }, [engine, id]);
10468
+ return useMemo10(() => ({ id, canvas, ctx, markDirty }), [id, canvas, ctx, markDirty]);
10469
+ }
10470
+
10471
+ // src/hooks/useIdleFrameSkip.ts
10472
+ import { useEffect as useEffect60 } from "react";
10473
+ function useIdleFrameSkip(enabled = true) {
10474
+ const engine = useGame();
10475
+ useEffect60(() => {
10476
+ const rs = engine.activeRenderSystem;
10477
+ rs.setIdleFrameSkip?.(enabled);
10478
+ return () => {
10479
+ rs.setIdleFrameSkip?.(false);
10480
+ };
10481
+ }, [engine, enabled]);
10482
+ }
10483
+
10259
10484
  // src/hooks/useAudioListener.ts
10260
- import { useEffect as useEffect59, useContext as useContext49 } from "react";
10485
+ import { useEffect as useEffect61, useContext as useContext49 } from "react";
10261
10486
  function useAudioListener() {
10262
10487
  const engine = useContext49(EngineContext);
10263
- useEffect59(() => {
10488
+ useEffect61(() => {
10264
10489
  const eid = engine.ecs.createEntity();
10265
10490
  engine.ecs.addComponent(
10266
10491
  eid,
@@ -10304,13 +10529,13 @@ function useTouch() {
10304
10529
  }
10305
10530
 
10306
10531
  // src/hooks/useGestures.ts
10307
- import { useEffect as useEffect60, useRef as useRef23 } from "react";
10532
+ import { useEffect as useEffect62, useRef as useRef24 } from "react";
10308
10533
  function useGestures(handlers, opts = {}) {
10309
- const handlersRef = useRef23(handlers);
10534
+ const handlersRef = useRef24(handlers);
10310
10535
  handlersRef.current = handlers;
10311
- const optsRef = useRef23(opts);
10536
+ const optsRef = useRef24(opts);
10312
10537
  optsRef.current = opts;
10313
- useEffect60(() => {
10538
+ useEffect62(() => {
10314
10539
  const target = optsRef.current.target ?? window;
10315
10540
  let starts = [];
10316
10541
  let longPressTimer = null;
@@ -10470,14 +10695,14 @@ var touchHapticsControls = {
10470
10695
  };
10471
10696
 
10472
10697
  // src/hooks/useTimer.ts
10473
- import { useRef as useRef24, useCallback as useCallback7, useEffect as useEffect61, useContext as useContext51 } from "react";
10698
+ import { useRef as useRef25, useCallback as useCallback8, useEffect as useEffect63, useContext as useContext51 } from "react";
10474
10699
  function useTimer(duration, onComplete, opts) {
10475
10700
  const engine = useContext51(EngineContext);
10476
- const onCompleteRef = useRef24(onComplete);
10701
+ const onCompleteRef = useRef25(onComplete);
10477
10702
  onCompleteRef.current = onComplete;
10478
- const loopRef = useRef24(opts?.loop ?? false);
10703
+ const loopRef = useRef25(opts?.loop ?? false);
10479
10704
  loopRef.current = opts?.loop ?? false;
10480
- const timerRef = useRef24(null);
10705
+ const timerRef = useRef25(null);
10481
10706
  if (!timerRef.current) {
10482
10707
  timerRef.current = createTimer(
10483
10708
  duration,
@@ -10490,7 +10715,7 @@ function useTimer(duration, onComplete, opts) {
10490
10715
  opts?.autoStart ?? false
10491
10716
  );
10492
10717
  }
10493
- useEffect61(() => {
10718
+ useEffect63(() => {
10494
10719
  const eid = engine.ecs.createEntity();
10495
10720
  engine.ecs.addComponent(
10496
10721
  eid,
@@ -10502,13 +10727,13 @@ function useTimer(duration, onComplete, opts) {
10502
10727
  if (engine.ecs.hasEntity(eid)) engine.ecs.destroyEntity(eid);
10503
10728
  };
10504
10729
  }, [engine.ecs]);
10505
- const start2 = useCallback7(() => {
10730
+ const start2 = useCallback8(() => {
10506
10731
  timerRef.current.start();
10507
10732
  }, []);
10508
- const stop = useCallback7(() => {
10733
+ const stop = useCallback8(() => {
10509
10734
  timerRef.current.stop();
10510
10735
  }, []);
10511
- const reset = useCallback7(() => {
10736
+ const reset = useCallback8(() => {
10512
10737
  timerRef.current.reset();
10513
10738
  }, []);
10514
10739
  return {
@@ -10531,15 +10756,15 @@ function useTimer(duration, onComplete, opts) {
10531
10756
  }
10532
10757
 
10533
10758
  // src/hooks/useCoroutine.ts
10534
- import { useRef as useRef25, useEffect as useEffect62, useContext as useContext52 } from "react";
10759
+ import { useRef as useRef26, useEffect as useEffect64, useContext as useContext52 } from "react";
10535
10760
  var wait = (seconds) => ({ type: "wait", seconds });
10536
10761
  var waitFrames = (frames) => ({ type: "waitFrames", frames });
10537
10762
  var waitUntil = (condition) => ({ type: "waitUntil", condition });
10538
10763
  var nextCoroutineId = 1;
10539
10764
  function useCoroutine() {
10540
10765
  const engine = useContext52(EngineContext);
10541
- const coroutinesRef = useRef25(/* @__PURE__ */ new Map());
10542
- useEffect62(() => {
10766
+ const coroutinesRef = useRef26(/* @__PURE__ */ new Map());
10767
+ useEffect64(() => {
10543
10768
  const eid = engine.ecs.createEntity();
10544
10769
  engine.ecs.addComponent(
10545
10770
  eid,
@@ -10604,33 +10829,33 @@ function useCoroutine() {
10604
10829
  return coroutinesRef.current.size;
10605
10830
  }
10606
10831
  };
10607
- const controlsRef = useRef25(controls);
10832
+ const controlsRef = useRef26(controls);
10608
10833
  return controlsRef.current;
10609
10834
  }
10610
10835
 
10611
10836
  // src/hooks/useSceneManager.ts
10612
- import { useState as useState13, useCallback as useCallback8, useRef as useRef26 } from "react";
10837
+ import { useState as useState13, useCallback as useCallback9, useRef as useRef27 } from "react";
10613
10838
  function useSceneManager(initialScene) {
10614
10839
  const [stack, setStack] = useState13([initialScene]);
10615
- const stackRef = useRef26(stack);
10840
+ const stackRef = useRef27(stack);
10616
10841
  stackRef.current = stack;
10617
- const push = useCallback8((scene) => {
10842
+ const push = useCallback9((scene) => {
10618
10843
  setStack((prev) => [...prev, scene]);
10619
10844
  }, []);
10620
- const pop = useCallback8(() => {
10845
+ const pop = useCallback9(() => {
10621
10846
  const prev = stackRef.current;
10622
10847
  if (prev.length <= 1) return void 0;
10623
10848
  const popped = prev[prev.length - 1];
10624
10849
  setStack(prev.slice(0, -1));
10625
10850
  return popped;
10626
10851
  }, []);
10627
- const replace = useCallback8((scene) => {
10852
+ const replace = useCallback9((scene) => {
10628
10853
  setStack((prev) => [...prev.slice(0, -1), scene]);
10629
10854
  }, []);
10630
- const reset = useCallback8((scene) => {
10855
+ const reset = useCallback9((scene) => {
10631
10856
  setStack([scene]);
10632
10857
  }, []);
10633
- const has = useCallback8(
10858
+ const has = useCallback9(
10634
10859
  (scene) => {
10635
10860
  return stack.includes(scene);
10636
10861
  },
@@ -10648,7 +10873,7 @@ function useSceneManager(initialScene) {
10648
10873
  }
10649
10874
 
10650
10875
  // src/hooks/useSceneTransition.ts
10651
- import { useState as useState14, useCallback as useCallback9, useRef as useRef27, useEffect as useEffect63 } from "react";
10876
+ import { useState as useState14, useCallback as useCallback10, useRef as useRef28, useEffect as useEffect65 } from "react";
10652
10877
  function useSceneTransition(initialScene, defaultTransition) {
10653
10878
  const [stack, setStack] = useState14([initialScene]);
10654
10879
  const [transState, setTransState] = useState14({
@@ -10657,11 +10882,11 @@ function useSceneTransition(initialScene, defaultTransition) {
10657
10882
  effect: null,
10658
10883
  pendingAction: null
10659
10884
  });
10660
- const stackRef = useRef27(stack);
10885
+ const stackRef = useRef28(stack);
10661
10886
  stackRef.current = stack;
10662
- const transRef = useRef27(transState);
10887
+ const transRef = useRef28(transState);
10663
10888
  transRef.current = transState;
10664
- useEffect63(() => {
10889
+ useEffect65(() => {
10665
10890
  if (transState.phase === "idle") return;
10666
10891
  const effect = transState.effect;
10667
10892
  if (!effect || effect.type === "instant") return;
@@ -10690,7 +10915,7 @@ function useSceneTransition(initialScene, defaultTransition) {
10690
10915
  rafId2 = requestAnimationFrame(animate);
10691
10916
  return () => cancelAnimationFrame(rafId2);
10692
10917
  }, [transState.phase, transState.effect]);
10693
- const startTransition = useCallback9(
10918
+ const startTransition = useCallback10(
10694
10919
  (effect, action) => {
10695
10920
  const eff = effect ?? defaultTransition ?? { type: "instant" };
10696
10921
  if (eff.type === "instant" || isReducedMotionPreferred()) {
@@ -10706,13 +10931,13 @@ function useSceneTransition(initialScene, defaultTransition) {
10706
10931
  },
10707
10932
  [defaultTransition]
10708
10933
  );
10709
- const push = useCallback9(
10934
+ const push = useCallback10(
10710
10935
  (scene, transition) => {
10711
10936
  startTransition(transition, () => setStack((prev) => [...prev, scene]));
10712
10937
  },
10713
10938
  [startTransition]
10714
10939
  );
10715
- const pop = useCallback9(
10940
+ const pop = useCallback10(
10716
10941
  (transition) => {
10717
10942
  const prev = stackRef.current;
10718
10943
  if (prev.length <= 1) return void 0;
@@ -10722,13 +10947,13 @@ function useSceneTransition(initialScene, defaultTransition) {
10722
10947
  },
10723
10948
  [startTransition]
10724
10949
  );
10725
- const replace = useCallback9(
10950
+ const replace = useCallback10(
10726
10951
  (scene, transition) => {
10727
10952
  startTransition(transition, () => setStack((prev) => [...prev.slice(0, -1), scene]));
10728
10953
  },
10729
10954
  [startTransition]
10730
10955
  );
10731
- const reset = useCallback9(
10956
+ const reset = useCallback10(
10732
10957
  (scene, transition) => {
10733
10958
  startTransition(transition, () => setStack([scene]));
10734
10959
  },
@@ -10829,10 +11054,10 @@ function SceneTransitionOverlay({ controls }) {
10829
11054
  }
10830
11055
 
10831
11056
  // src/hooks/useHitstop.ts
10832
- import { useContext as useContext53, useCallback as useCallback10 } from "react";
11057
+ import { useContext as useContext53, useCallback as useCallback11 } from "react";
10833
11058
  function useHitstop() {
10834
11059
  const engine = useContext53(EngineContext);
10835
- const freeze = useCallback10(
11060
+ const freeze = useCallback11(
10836
11061
  (seconds) => {
10837
11062
  engine.loop.hitPause(seconds);
10838
11063
  },
@@ -10842,16 +11067,16 @@ function useHitstop() {
10842
11067
  }
10843
11068
 
10844
11069
  // src/hooks/useInputBuffer.ts
10845
- import { useMemo as useMemo10 } from "react";
11070
+ import { useMemo as useMemo11 } from "react";
10846
11071
  function useInputBuffer(opts) {
10847
- return useMemo10(() => new InputBuffer(opts), []);
11072
+ return useMemo11(() => new InputBuffer(opts), []);
10848
11073
  }
10849
11074
 
10850
11075
  // src/hooks/useComboDetector.ts
10851
- import { useMemo as useMemo11, useRef as useRef28 } from "react";
11076
+ import { useMemo as useMemo12, useRef as useRef29 } from "react";
10852
11077
  function useComboDetector(combos) {
10853
- const lastComboRef = useRef28(null);
10854
- const result = useMemo11(() => {
11078
+ const lastComboRef = useRef29(null);
11079
+ const result = useMemo12(() => {
10855
11080
  const detector = new ComboDetector({ combos });
10856
11081
  const api = {
10857
11082
  feed(action) {
@@ -10873,11 +11098,11 @@ function useComboDetector(combos) {
10873
11098
  }
10874
11099
 
10875
11100
  // src/hooks/useParent.ts
10876
- import { useEffect as useEffect64, useContext as useContext54 } from "react";
11101
+ import { useEffect as useEffect66, useContext as useContext54 } from "react";
10877
11102
  function useParent(childEntityId, parentEntityId) {
10878
11103
  const engine = useContext54(EngineContext);
10879
11104
  if (!engine) throw new Error("useParent must be used inside <Game>");
10880
- useEffect64(() => {
11105
+ useEffect66(() => {
10881
11106
  setParent(engine.ecs, childEntityId, parentEntityId);
10882
11107
  return () => {
10883
11108
  removeParent(engine.ecs, childEntityId);
@@ -10886,7 +11111,7 @@ function useParent(childEntityId, parentEntityId) {
10886
11111
  }
10887
11112
 
10888
11113
  // src/hooks/useAccessibility.ts
10889
- import { useCallback as useCallback11, useSyncExternalStore } from "react";
11114
+ import { useCallback as useCallback12, useSyncExternalStore } from "react";
10890
11115
  var _version = 0;
10891
11116
  var _listeners = /* @__PURE__ */ new Set();
10892
11117
  function subscribe(cb) {
@@ -10898,12 +11123,12 @@ function getSnapshot() {
10898
11123
  }
10899
11124
  function useAccessibility() {
10900
11125
  useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
10901
- const setOptions = useCallback11((opts) => {
11126
+ const setOptions = useCallback12((opts) => {
10902
11127
  setAccessibilityOptions(opts);
10903
11128
  _version++;
10904
11129
  for (const cb of _listeners) cb();
10905
11130
  }, []);
10906
- const announce = useCallback11((text, priority) => {
11131
+ const announce = useCallback12((text, priority) => {
10907
11132
  announceToScreenReader(text, priority);
10908
11133
  }, []);
10909
11134
  return {
@@ -10914,18 +11139,18 @@ function useAccessibility() {
10914
11139
  }
10915
11140
 
10916
11141
  // src/hooks/useHMR.ts
10917
- import { useEffect as useEffect65, useRef as useRef29, useMemo as useMemo12 } from "react";
11142
+ import { useEffect as useEffect67, useRef as useRef30, useMemo as useMemo13 } from "react";
10918
11143
  var idCounter = 0;
10919
11144
  function useHMR(hmrKey) {
10920
- const idRef = useRef29(null);
11145
+ const idRef = useRef30(null);
10921
11146
  if (idRef.current === null) {
10922
11147
  idRef.current = hmrKey ?? `__hmr_${idCounter++}`;
10923
11148
  }
10924
11149
  const key = idRef.current;
10925
11150
  const isHotReload = hmrLoadState(key) !== void 0;
10926
- const disposeRef = useRef29(null);
10927
- const acceptRef = useRef29(null);
10928
- useEffect65(() => {
11151
+ const disposeRef = useRef30(null);
11152
+ const acceptRef = useRef30(null);
11153
+ useEffect67(() => {
10929
11154
  const hot = import.meta.hot;
10930
11155
  if (!hot) return;
10931
11156
  hot.dispose(() => {
@@ -10939,7 +11164,7 @@ function useHMR(hmrKey) {
10939
11164
  acceptRef.current(prev);
10940
11165
  }
10941
11166
  }, [key]);
10942
- return useMemo12(
11167
+ return useMemo13(
10943
11168
  () => ({
10944
11169
  onDispose(handler) {
10945
11170
  disposeRef.current = handler;
@@ -10958,11 +11183,11 @@ function useHMR(hmrKey) {
10958
11183
  }
10959
11184
 
10960
11185
  // src/hooks/useSquashStretch.ts
10961
- import { useCallback as useCallback12, useContext as useContext55 } from "react";
11186
+ import { useCallback as useCallback13, useContext as useContext55 } from "react";
10962
11187
  function useSquashStretch() {
10963
11188
  const engine = useContext55(EngineContext);
10964
11189
  const entityId = useContext55(EntityContext);
10965
- const trigger = useCallback12(
11190
+ const trigger = useCallback13(
10966
11191
  (scaleX, scaleY) => {
10967
11192
  const ss = engine.ecs.getComponent(entityId, "SquashStretch");
10968
11193
  if (!ss) return;
@@ -10975,16 +11200,16 @@ function useSquashStretch() {
10975
11200
  }
10976
11201
 
10977
11202
  // src/hooks/useHistory.ts
10978
- import { useCallback as useCallback13, useContext as useContext56, useEffect as useEffect66, useRef as useRef30, useState as useState15 } from "react";
11203
+ import { useCallback as useCallback14, useContext as useContext56, useEffect as useEffect68, useRef as useRef31, useState as useState15 } from "react";
10979
11204
  function useHistory(options) {
10980
11205
  const engine = useContext56(EngineContext);
10981
11206
  const capacity = options?.capacity ?? 50;
10982
11207
  const bindKeys = options?.bindKeyboardShortcuts ?? false;
10983
- const stackRef = useRef30([]);
10984
- const indexRef = useRef30(-1);
11208
+ const stackRef = useRef31([]);
11209
+ const indexRef = useRef31(-1);
10985
11210
  const [version, setVersion] = useState15(0);
10986
- const bump = useCallback13(() => setVersion((v) => v + 1), []);
10987
- const push = useCallback13(() => {
11211
+ const bump = useCallback14(() => setVersion((v) => v + 1), []);
11212
+ const push = useCallback14(() => {
10988
11213
  const snap = engine.ecs.getSnapshot();
10989
11214
  const stack = stackRef.current;
10990
11215
  if (indexRef.current < stack.length - 1) {
@@ -10995,14 +11220,14 @@ function useHistory(options) {
10995
11220
  indexRef.current = stack.length - 1;
10996
11221
  bump();
10997
11222
  }, [engine, capacity, bump]);
10998
- const undo = useCallback13(() => {
11223
+ const undo = useCallback14(() => {
10999
11224
  if (indexRef.current <= 0) return;
11000
11225
  indexRef.current -= 1;
11001
11226
  engine.ecs.restoreSnapshot(stackRef.current[indexRef.current]);
11002
11227
  engine.loop.markDirty();
11003
11228
  bump();
11004
11229
  }, [engine, bump]);
11005
- const redo = useCallback13(() => {
11230
+ const redo = useCallback14(() => {
11006
11231
  const stack = stackRef.current;
11007
11232
  if (indexRef.current >= stack.length - 1) return;
11008
11233
  indexRef.current += 1;
@@ -11010,12 +11235,12 @@ function useHistory(options) {
11010
11235
  engine.loop.markDirty();
11011
11236
  bump();
11012
11237
  }, [engine, bump]);
11013
- const clear = useCallback13(() => {
11238
+ const clear = useCallback14(() => {
11014
11239
  stackRef.current = [];
11015
11240
  indexRef.current = -1;
11016
11241
  bump();
11017
11242
  }, [bump]);
11018
- useEffect66(() => {
11243
+ useEffect68(() => {
11019
11244
  if (!bindKeys) return;
11020
11245
  const onKey = (e) => {
11021
11246
  const mod = e.ctrlKey || e.metaKey;
@@ -11040,13 +11265,13 @@ function useHistory(options) {
11040
11265
  }
11041
11266
 
11042
11267
  // src/hooks/useSelection.tsx
11043
- import { createContext as createContext2, useCallback as useCallback14, useContext as useContext57, useEffect as useEffect67, useMemo as useMemo13, useState as useState16 } from "react";
11268
+ import { createContext as createContext2, useCallback as useCallback15, useContext as useContext57, useEffect as useEffect69, useMemo as useMemo14, useState as useState16 } from "react";
11044
11269
  import { jsx as jsx15 } from "react/jsx-runtime";
11045
11270
  var SelectionContext = createContext2(null);
11046
11271
  function Selection({ initial, onChange, children }) {
11047
11272
  const engine = useContext57(EngineContext);
11048
11273
  const [selected, setSelected] = useState16(initial ?? []);
11049
- useEffect67(() => {
11274
+ useEffect69(() => {
11050
11275
  if (!engine) return;
11051
11276
  return engine.ecs.onDestroyEntity((destroyedId) => {
11052
11277
  setSelected((prev) => {
@@ -11057,14 +11282,14 @@ function Selection({ initial, onChange, children }) {
11057
11282
  });
11058
11283
  });
11059
11284
  }, [engine, onChange]);
11060
- const commit = useCallback14(
11285
+ const commit = useCallback15(
11061
11286
  (next) => {
11062
11287
  setSelected(next);
11063
11288
  onChange?.(next);
11064
11289
  },
11065
11290
  [onChange]
11066
11291
  );
11067
- const select = useCallback14(
11292
+ const select = useCallback15(
11068
11293
  (id, opts) => {
11069
11294
  if (opts?.additive) {
11070
11295
  setSelected((prev) => {
@@ -11079,7 +11304,7 @@ function Selection({ initial, onChange, children }) {
11079
11304
  },
11080
11305
  [commit, onChange]
11081
11306
  );
11082
- const deselect = useCallback14(
11307
+ const deselect = useCallback15(
11083
11308
  (id) => {
11084
11309
  setSelected((prev) => {
11085
11310
  if (!prev.includes(id)) return prev;
@@ -11090,7 +11315,7 @@ function Selection({ initial, onChange, children }) {
11090
11315
  },
11091
11316
  [onChange]
11092
11317
  );
11093
- const toggle = useCallback14(
11318
+ const toggle = useCallback15(
11094
11319
  (id) => {
11095
11320
  setSelected((prev) => {
11096
11321
  const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id];
@@ -11100,9 +11325,9 @@ function Selection({ initial, onChange, children }) {
11100
11325
  },
11101
11326
  [onChange]
11102
11327
  );
11103
- const clear = useCallback14(() => commit([]), [commit]);
11104
- const isSelected = useCallback14((id) => selected.includes(id), [selected]);
11105
- const value = useMemo13(
11328
+ const clear = useCallback15(() => commit([]), [commit]);
11329
+ const isSelected = useCallback15((id) => selected.includes(id), [selected]);
11330
+ const value = useMemo14(
11106
11331
  () => ({ selected, select, deselect, toggle, clear, isSelected }),
11107
11332
  [selected, select, deselect, toggle, clear, isSelected]
11108
11333
  );
@@ -11115,10 +11340,10 @@ function useSelection() {
11115
11340
  }
11116
11341
 
11117
11342
  // src/components/TransformHandles.tsx
11118
- import { useContext as useContext58, useEffect as useEffect69, useRef as useRef31 } from "react";
11343
+ import { useContext as useContext58, useEffect as useEffect71, useRef as useRef32 } from "react";
11119
11344
 
11120
11345
  // src/hooks/useOverlayTick.ts
11121
- import { useEffect as useEffect68 } from "react";
11346
+ import { useEffect as useEffect70 } from "react";
11122
11347
  var callbacks = /* @__PURE__ */ new Set();
11123
11348
  var rafId = 0;
11124
11349
  function tick() {
@@ -11141,7 +11366,7 @@ function register(cb) {
11141
11366
  };
11142
11367
  }
11143
11368
  function useOverlayTick(callback, deps = []) {
11144
- useEffect68(() => {
11369
+ useEffect70(() => {
11145
11370
  return register(callback);
11146
11371
  }, deps);
11147
11372
  }
@@ -11157,8 +11382,8 @@ function TransformHandles({
11157
11382
  }) {
11158
11383
  const engine = useContext58(EngineContext);
11159
11384
  const selection = useSelection();
11160
- const overlayRef = useRef31(null);
11161
- const dragRef = useRef31(null);
11385
+ const overlayRef = useRef32(null);
11386
+ const dragRef = useRef32(null);
11162
11387
  useOverlayTick(() => {
11163
11388
  if (!engine) return;
11164
11389
  const overlay = overlayRef.current;
@@ -11193,7 +11418,7 @@ function TransformHandles({
11193
11418
  node.style.transform = `translate(-50%, -50%) rotate(${t.rotation}rad)`;
11194
11419
  }
11195
11420
  }, [engine, selection.selected]);
11196
- useEffect69(() => {
11421
+ useEffect71(() => {
11197
11422
  if (!engine) return;
11198
11423
  const onMove = (e) => {
11199
11424
  const drag = dragRef.current;
@@ -11417,7 +11642,7 @@ function worldToScreenCss(engine, wx, wy) {
11417
11642
  }
11418
11643
 
11419
11644
  // src/hooks/useSnap.ts
11420
- import { useCallback as useCallback15, useContext as useContext59 } from "react";
11645
+ import { useCallback as useCallback16, useContext as useContext59 } from "react";
11421
11646
  function useSnap(options) {
11422
11647
  const engine = useContext59(EngineContext);
11423
11648
  const gridX = typeof options?.grid === "number" ? options.grid : options?.grid?.x ?? 0;
@@ -11425,7 +11650,7 @@ function useSnap(options) {
11425
11650
  const snapToEntities = options?.snapToEntities ?? false;
11426
11651
  const threshold = options?.threshold ?? 8;
11427
11652
  const exclude = options?.exclude;
11428
- const snapToGrid = useCallback15(
11653
+ const snapToGrid = useCallback16(
11429
11654
  (x, y) => {
11430
11655
  const sx = gridX > 0 ? Math.round(x / gridX) * gridX : x;
11431
11656
  const sy = gridY > 0 ? Math.round(y / gridY) * gridY : y;
@@ -11433,7 +11658,7 @@ function useSnap(options) {
11433
11658
  },
11434
11659
  [gridX, gridY]
11435
11660
  );
11436
- const snap = useCallback15(
11661
+ const snap = useCallback16(
11437
11662
  (x, y) => {
11438
11663
  let outX = x;
11439
11664
  let outY = y;
@@ -11508,7 +11733,7 @@ function getBounds(ecs, id) {
11508
11733
  }
11509
11734
 
11510
11735
  // src/components/EditableText.tsx
11511
- import { useContext as useContext60, useEffect as useEffect70, useRef as useRef32 } from "react";
11736
+ import { useContext as useContext60, useEffect as useEffect72, useRef as useRef33 } from "react";
11512
11737
  import { jsx as jsx17 } from "react/jsx-runtime";
11513
11738
  function EditableText({
11514
11739
  value,
@@ -11532,8 +11757,8 @@ function EditableText({
11532
11757
  }) {
11533
11758
  const engine = useContext60(EngineContext);
11534
11759
  const entityId = useContext60(EntityContext);
11535
- const containerRef = useRef32(null);
11536
- const inputRef = useRef32(null);
11760
+ const containerRef = useRef33(null);
11761
+ const inputRef = useRef33(null);
11537
11762
  useOverlayTick(() => {
11538
11763
  if (!engine || entityId === null || entityId === void 0) return;
11539
11764
  const container = containerRef.current;
@@ -11560,7 +11785,7 @@ function EditableText({
11560
11785
  container.style.setProperty("--cubeforge-canvas-left", `${rect.left + window.scrollX}px`);
11561
11786
  container.style.setProperty("--cubeforge-canvas-top", `${rect.top + window.scrollY}px`);
11562
11787
  }, [engine, entityId, width, height, fontSize]);
11563
- useEffect70(() => {
11788
+ useEffect72(() => {
11564
11789
  if (autoFocus) inputRef.current?.focus();
11565
11790
  }, [autoFocus]);
11566
11791
  if (!engine) return null;
@@ -11691,7 +11916,7 @@ function copyRegion(source, region, scale) {
11691
11916
  }
11692
11917
 
11693
11918
  // src/components/A11yNode.tsx
11694
- import { useContext as useContext61, useRef as useRef33 } from "react";
11919
+ import { useContext as useContext61, useRef as useRef34 } from "react";
11695
11920
  import { jsx as jsx18 } from "react/jsx-runtime";
11696
11921
  function A11yNode({
11697
11922
  label,
@@ -11707,7 +11932,7 @@ function A11yNode({
11707
11932
  }) {
11708
11933
  const engine = useContext61(EngineContext);
11709
11934
  const entityId = useContext61(EntityContext);
11710
- const nodeRef = useRef33(null);
11935
+ const nodeRef = useRef34(null);
11711
11936
  useOverlayTick(() => {
11712
11937
  if (!engine || entityId === null || entityId === void 0) return;
11713
11938
  const node = nodeRef.current;
@@ -11796,7 +12021,7 @@ function worldToScreenCss3(engine, wx, wy) {
11796
12021
  }
11797
12022
 
11798
12023
  // src/components/VectorPath.tsx
11799
- import { useContext as useContext62, useRef as useRef34 } from "react";
12024
+ import { useContext as useContext62, useRef as useRef35 } from "react";
11800
12025
  import { jsx as jsx19 } from "react/jsx-runtime";
11801
12026
  function VectorPath({
11802
12027
  d,
@@ -11812,7 +12037,7 @@ function VectorPath({
11812
12037
  }) {
11813
12038
  const engine = useContext62(EngineContext);
11814
12039
  const entityId = useContext62(EntityContext);
11815
- const svgRef = useRef34(null);
12040
+ const svgRef = useRef35(null);
11816
12041
  useOverlayTick(() => {
11817
12042
  if (!engine || entityId === null || entityId === void 0) return;
11818
12043
  const svg = svgRef.current;
@@ -11873,12 +12098,12 @@ function worldToScreenCss4(engine, wx, wy) {
11873
12098
  }
11874
12099
 
11875
12100
  // src/hooks/useGrid.ts
11876
- import { useCallback as useCallback16, useContext as useContext63, useMemo as useMemo14, useRef as useRef35, useState as useState17 } from "react";
12101
+ import { useCallback as useCallback17, useContext as useContext63, useMemo as useMemo15, useRef as useRef36, useState as useState17 } from "react";
11877
12102
  function useGrid({ width, height, fill }) {
11878
12103
  const engine = useContext63(EngineContext);
11879
12104
  const [version, setVersion] = useState17(0);
11880
- const bump = useCallback16(() => setVersion((v) => v + 1), []);
11881
- const dataRef = useRef35(null);
12105
+ const bump = useCallback17(() => setVersion((v) => v + 1), []);
12106
+ const dataRef = useRef36(null);
11882
12107
  if (dataRef.current === null) {
11883
12108
  const arr = new Array(width * height);
11884
12109
  if (typeof fill === "function") {
@@ -11893,19 +12118,19 @@ function useGrid({ width, height, fill }) {
11893
12118
  }
11894
12119
  dataRef.current = arr;
11895
12120
  }
11896
- const markChanged = useCallback16(() => {
12121
+ const markChanged = useCallback17(() => {
11897
12122
  engine?.loop.markDirty();
11898
12123
  bump();
11899
12124
  }, [engine, bump]);
11900
- const inBounds = useCallback16((x, y) => x >= 0 && y >= 0 && x < width && y < height, [width, height]);
11901
- const get = useCallback16(
12125
+ const inBounds = useCallback17((x, y) => x >= 0 && y >= 0 && x < width && y < height, [width, height]);
12126
+ const get = useCallback17(
11902
12127
  (x, y) => {
11903
12128
  if (!inBounds(x, y)) return void 0;
11904
12129
  return dataRef.current[y * width + x];
11905
12130
  },
11906
12131
  [width, inBounds]
11907
12132
  );
11908
- const set = useCallback16(
12133
+ const set = useCallback17(
11909
12134
  (x, y, value) => {
11910
12135
  if (!inBounds(x, y)) return;
11911
12136
  const data = dataRef.current;
@@ -11916,7 +12141,7 @@ function useGrid({ width, height, fill }) {
11916
12141
  },
11917
12142
  [width, inBounds, markChanged]
11918
12143
  );
11919
- const swap = useCallback16(
12144
+ const swap = useCallback17(
11920
12145
  (ax, ay, bx, by) => {
11921
12146
  if (!inBounds(ax, ay) || !inBounds(bx, by)) return;
11922
12147
  const data = dataRef.current;
@@ -11930,7 +12155,7 @@ function useGrid({ width, height, fill }) {
11930
12155
  },
11931
12156
  [width, inBounds, markChanged]
11932
12157
  );
11933
- const forEach = useCallback16(
12158
+ const forEach = useCallback17(
11934
12159
  (cb) => {
11935
12160
  const data = dataRef.current;
11936
12161
  for (let y = 0; y < height; y++) {
@@ -11941,7 +12166,7 @@ function useGrid({ width, height, fill }) {
11941
12166
  },
11942
12167
  [width, height]
11943
12168
  );
11944
- const find = useCallback16(
12169
+ const find = useCallback17(
11945
12170
  (pred) => {
11946
12171
  const data = dataRef.current;
11947
12172
  for (let y = 0; y < height; y++) {
@@ -11954,7 +12179,7 @@ function useGrid({ width, height, fill }) {
11954
12179
  },
11955
12180
  [width, height]
11956
12181
  );
11957
- const count = useCallback16(
12182
+ const count = useCallback17(
11958
12183
  (pred) => {
11959
12184
  const data = dataRef.current;
11960
12185
  let n = 0;
@@ -11967,7 +12192,7 @@ function useGrid({ width, height, fill }) {
11967
12192
  },
11968
12193
  [width, height]
11969
12194
  );
11970
- const neighbors = useCallback16(
12195
+ const neighbors = useCallback17(
11971
12196
  (x, y, diagonal = false) => {
11972
12197
  const data = dataRef.current;
11973
12198
  const result = [];
@@ -11995,7 +12220,7 @@ function useGrid({ width, height, fill }) {
11995
12220
  },
11996
12221
  [width, inBounds]
11997
12222
  );
11998
- const fillAll = useCallback16(
12223
+ const fillAll = useCallback17(
11999
12224
  (value) => {
12000
12225
  const data = dataRef.current;
12001
12226
  if (typeof value === "function") {
@@ -12012,7 +12237,7 @@ function useGrid({ width, height, fill }) {
12012
12237
  },
12013
12238
  [width, height, markChanged]
12014
12239
  );
12015
- const toArray = useCallback16(() => {
12240
+ const toArray = useCallback17(() => {
12016
12241
  const data = dataRef.current;
12017
12242
  const out = new Array(height);
12018
12243
  for (let y = 0; y < height; y++) {
@@ -12022,7 +12247,7 @@ function useGrid({ width, height, fill }) {
12022
12247
  }
12023
12248
  return out;
12024
12249
  }, [width, height]);
12025
- const fromArray = useCallback16(
12250
+ const fromArray = useCallback17(
12026
12251
  (arr) => {
12027
12252
  const data = dataRef.current;
12028
12253
  for (let y = 0; y < height; y++) {
@@ -12037,7 +12262,7 @@ function useGrid({ width, height, fill }) {
12037
12262
  [width, height, markChanged]
12038
12263
  );
12039
12264
  void version;
12040
- return useMemo14(
12265
+ return useMemo15(
12041
12266
  () => ({
12042
12267
  width,
12043
12268
  height,
@@ -12058,7 +12283,7 @@ function useGrid({ width, height, fill }) {
12058
12283
  }
12059
12284
 
12060
12285
  // src/hooks/useTurnSystem.ts
12061
- import { useCallback as useCallback17, useContext as useContext64, useEffect as useEffect71, useMemo as useMemo15, useRef as useRef36, useState as useState18 } from "react";
12286
+ import { useCallback as useCallback18, useContext as useContext64, useEffect as useEffect73, useMemo as useMemo16, useRef as useRef37, useState as useState18 } from "react";
12062
12287
  function useTurnSystem({
12063
12288
  players,
12064
12289
  initialIndex = 0,
@@ -12071,11 +12296,11 @@ function useTurnSystem({
12071
12296
  const [activeIndex, setActiveIndex] = useState18(initialIndex % players.length);
12072
12297
  const [turn, setTurn] = useState18(0);
12073
12298
  const [isPending, setIsPending] = useState18(false);
12074
- const pendingRafId = useRef36(null);
12075
- const pendingRemaining = useRef36(0);
12076
- const pendingTarget = useRef36(null);
12077
- const pendingLastTime = useRef36(0);
12078
- const clearPending = useCallback17(() => {
12299
+ const pendingRafId = useRef37(null);
12300
+ const pendingRemaining = useRef37(0);
12301
+ const pendingTarget = useRef37(null);
12302
+ const pendingLastTime = useRef37(0);
12303
+ const clearPending = useCallback18(() => {
12079
12304
  if (pendingRafId.current !== null) {
12080
12305
  cancelAnimationFrame(pendingRafId.current);
12081
12306
  pendingRafId.current = null;
@@ -12083,13 +12308,13 @@ function useTurnSystem({
12083
12308
  pendingTarget.current = null;
12084
12309
  setIsPending(false);
12085
12310
  }, []);
12086
- const startedOnce = useRef36(false);
12087
- useEffect71(() => {
12311
+ const startedOnce = useRef37(false);
12312
+ useEffect73(() => {
12088
12313
  if (startedOnce.current) return;
12089
12314
  startedOnce.current = true;
12090
12315
  onTurnStart?.({ player: players[activeIndex], index: activeIndex, turn: 0 });
12091
12316
  }, []);
12092
- const applyChange = useCallback17(
12317
+ const applyChange = useCallback18(
12093
12318
  (nextIndex) => {
12094
12319
  const curr = players[activeIndex];
12095
12320
  onTurnEnd?.({ player: curr, index: activeIndex, turn });
@@ -12102,7 +12327,7 @@ function useTurnSystem({
12102
12327
  },
12103
12328
  [players, activeIndex, turn, onTurnStart, onTurnEnd, engine]
12104
12329
  );
12105
- const scheduleChange = useCallback17(
12330
+ const scheduleChange = useCallback18(
12106
12331
  (nextIndex) => {
12107
12332
  clearPending();
12108
12333
  if (aiDelay <= 0) {
@@ -12134,9 +12359,9 @@ function useTurnSystem({
12134
12359
  },
12135
12360
  [aiDelay, applyChange, clearPending, engine]
12136
12361
  );
12137
- const nextTurn = useCallback17(() => scheduleChange(activeIndex + 1), [scheduleChange, activeIndex]);
12138
- const prevTurn = useCallback17(() => scheduleChange(activeIndex - 1), [scheduleChange, activeIndex]);
12139
- const skipTo = useCallback17(
12362
+ const nextTurn = useCallback18(() => scheduleChange(activeIndex + 1), [scheduleChange, activeIndex]);
12363
+ const prevTurn = useCallback18(() => scheduleChange(activeIndex - 1), [scheduleChange, activeIndex]);
12364
+ const skipTo = useCallback18(
12140
12365
  (target) => {
12141
12366
  let idx;
12142
12367
  if (typeof target === "number") {
@@ -12149,19 +12374,19 @@ function useTurnSystem({
12149
12374
  },
12150
12375
  [players, scheduleChange]
12151
12376
  );
12152
- const reset = useCallback17(() => {
12377
+ const reset = useCallback18(() => {
12153
12378
  clearPending();
12154
12379
  setActiveIndex(initialIndex % players.length);
12155
12380
  setTurn(0);
12156
12381
  engine?.loop.markDirty();
12157
12382
  }, [players.length, initialIndex, clearPending, engine]);
12158
- useEffect71(
12383
+ useEffect73(
12159
12384
  () => () => {
12160
12385
  if (pendingRafId.current !== null) cancelAnimationFrame(pendingRafId.current);
12161
12386
  },
12162
12387
  []
12163
12388
  );
12164
- return useMemo15(
12389
+ return useMemo16(
12165
12390
  () => ({
12166
12391
  activePlayer: players[activeIndex],
12167
12392
  activeIndex,
@@ -12177,13 +12402,13 @@ function useTurnSystem({
12177
12402
  }
12178
12403
 
12179
12404
  // src/hooks/useHoverable.ts
12180
- import { useContext as useContext65, useEffect as useEffect72, useState as useState19 } from "react";
12405
+ import { useContext as useContext65, useEffect as useEffect74, useState as useState19 } from "react";
12181
12406
  function useHoverable(options) {
12182
12407
  const engine = useContext65(EngineContext);
12183
12408
  const ctxEntityId = useContext65(EntityContext);
12184
12409
  const entityId = options?.entityId ?? ctxEntityId;
12185
12410
  const [isHovered, setIsHovered] = useState19(false);
12186
- useEffect72(() => {
12411
+ useEffect74(() => {
12187
12412
  if (!engine || entityId === null || entityId === void 0 || options?.disabled) return;
12188
12413
  const canvas = engine.canvas;
12189
12414
  let hovered = false;
@@ -12260,7 +12485,7 @@ function screenToWorld(engine, cssX, cssY) {
12260
12485
  }
12261
12486
 
12262
12487
  // src/hooks/useDragDrop.ts
12263
- import { useContext as useContext66, useEffect as useEffect73, useRef as useRef37, useState as useState20 } from "react";
12488
+ import { useContext as useContext66, useEffect as useEffect75, useRef as useRef38, useState as useState20 } from "react";
12264
12489
  var dragStateByEngine = /* @__PURE__ */ new WeakMap();
12265
12490
  function getActiveDrag(engine) {
12266
12491
  return dragStateByEngine.get(engine) ?? null;
@@ -12285,9 +12510,9 @@ function useDraggable(options) {
12285
12510
  const entityId = options?.entityId ?? ctxEntityId;
12286
12511
  const [isDragging, setIsDragging] = useState20(false);
12287
12512
  const [position, setPosition] = useState20({ x: 0, y: 0 });
12288
- const optsRef = useRef37(options);
12513
+ const optsRef = useRef38(options);
12289
12514
  optsRef.current = options;
12290
- useEffect73(() => {
12515
+ useEffect75(() => {
12291
12516
  if (!engine || entityId === null || entityId === void 0 || options?.disabled) return;
12292
12517
  const canvas = engine.canvas;
12293
12518
  let dragging = false;
@@ -12397,9 +12622,9 @@ function useDroppable(options) {
12397
12622
  const entityId = options?.entityId ?? ctxEntityId;
12398
12623
  const [isOver, setIsOver] = useState20(false);
12399
12624
  const [hoveredBy, setHoveredBy] = useState20(null);
12400
- const optsRef = useRef37(options);
12625
+ const optsRef = useRef38(options);
12401
12626
  optsRef.current = options;
12402
- useEffect73(() => {
12627
+ useEffect75(() => {
12403
12628
  if (!engine || entityId === null || entityId === void 0 || options?.disabled) return;
12404
12629
  let currentlyOver = false;
12405
12630
  const check = () => {
@@ -12501,7 +12726,7 @@ function findDroppableUnder(engine, drag) {
12501
12726
  }
12502
12727
 
12503
12728
  // src/hooks/useKeyboardFocus.ts
12504
- import { useContext as useContext67, useEffect as useEffect74, useRef as useRef38, useState as useState21 } from "react";
12729
+ import { useContext as useContext67, useEffect as useEffect76, useRef as useRef39, useState as useState21 } from "react";
12505
12730
  var registry = /* @__PURE__ */ new Map();
12506
12731
  var focusedId = null;
12507
12732
  var subscribers = /* @__PURE__ */ new Set();
@@ -12518,9 +12743,9 @@ function setFocused(id) {
12518
12743
  }
12519
12744
  function useFocusable(options) {
12520
12745
  const entityIdCtx = useContext67(EntityContext);
12521
- const optsRef = useRef38(options);
12746
+ const optsRef = useRef39(options);
12522
12747
  optsRef.current = options;
12523
- useEffect74(() => {
12748
+ useEffect76(() => {
12524
12749
  if (entityIdCtx === null || entityIdCtx === void 0) return;
12525
12750
  const id = entityIdCtx;
12526
12751
  const entry = {
@@ -12540,14 +12765,14 @@ function useFocusable(options) {
12540
12765
  function useKeyboardFocus() {
12541
12766
  const engine = useContext67(EngineContext);
12542
12767
  const [focused, setFocusedState] = useState21(focusedId);
12543
- useEffect74(() => {
12768
+ useEffect76(() => {
12544
12769
  const cb = () => setFocusedState(focusedId);
12545
12770
  subscribers.add(cb);
12546
12771
  return () => {
12547
12772
  subscribers.delete(cb);
12548
12773
  };
12549
12774
  }, []);
12550
- useEffect74(() => {
12775
+ useEffect76(() => {
12551
12776
  if (!engine) return;
12552
12777
  const onKey = (e) => {
12553
12778
  const target = e.target;
@@ -12673,7 +12898,7 @@ function subscribeFocus(cb) {
12673
12898
  }
12674
12899
 
12675
12900
  // src/components/FocusRing.tsx
12676
- import { useContext as useContext68, useEffect as useEffect75, useRef as useRef39, useState as useState22 } from "react";
12901
+ import { useContext as useContext68, useEffect as useEffect77, useRef as useRef40, useState as useState22 } from "react";
12677
12902
  import { Fragment as Fragment9, jsx as jsx20, jsxs as jsxs9 } from "react/jsx-runtime";
12678
12903
  function FocusRing({
12679
12904
  color = "#4fc3f7",
@@ -12683,14 +12908,14 @@ function FocusRing({
12683
12908
  pulse = true
12684
12909
  }) {
12685
12910
  const engine = useContext68(EngineContext);
12686
- const ringRef = useRef39(null);
12911
+ const ringRef = useRef40(null);
12687
12912
  const [focused, setFocused2] = useState22(getFocusedEntityId());
12688
12913
  const [reducedMotion, setReducedMotion] = useState22(false);
12689
- useEffect75(() => {
12914
+ useEffect77(() => {
12690
12915
  const unsub = subscribeFocus(() => setFocused2(getFocusedEntityId()));
12691
12916
  return unsub;
12692
12917
  }, []);
12693
- useEffect75(() => {
12918
+ useEffect77(() => {
12694
12919
  if (typeof window === "undefined" || !window.matchMedia) return;
12695
12920
  const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
12696
12921
  setReducedMotion(mq.matches);
@@ -12827,10 +13052,10 @@ function listSavedScenes(prefix = "") {
12827
13052
  }
12828
13053
 
12829
13054
  // ../gameplay/src/hooks/useAnimationController.ts
12830
- import { useState as useState23, useCallback as useCallback18 } from "react";
13055
+ import { useState as useState23, useCallback as useCallback19 } from "react";
12831
13056
  function useAnimationController(states, initial) {
12832
13057
  const [stateName, setStateName] = useState23(initial);
12833
- const setState = useCallback18((next) => {
13058
+ const setState = useCallback19((next) => {
12834
13059
  setStateName((prev) => prev === next ? prev : next);
12835
13060
  }, []);
12836
13061
  const clip = states[stateName] ?? states[initial];
@@ -12852,39 +13077,39 @@ function useAnimationController(states, initial) {
12852
13077
  }
12853
13078
 
12854
13079
  // ../gameplay/src/hooks/useAISteering.ts
12855
- import { useCallback as useCallback19 } from "react";
13080
+ import { useCallback as useCallback20 } from "react";
12856
13081
  function useAISteering() {
12857
- const seek$ = useCallback19((pos, target, speed) => seek(pos, target, speed), []);
12858
- const flee$ = useCallback19((pos, threat, speed) => flee(pos, threat, speed), []);
12859
- const arrive$ = useCallback19(
13082
+ const seek$ = useCallback20((pos, target, speed) => seek(pos, target, speed), []);
13083
+ const flee$ = useCallback20((pos, threat, speed) => flee(pos, threat, speed), []);
13084
+ const arrive$ = useCallback20(
12860
13085
  (pos, target, speed, slowRadius) => arrive(pos, target, speed, slowRadius),
12861
13086
  []
12862
13087
  );
12863
- const patrol$ = useCallback19(
13088
+ const patrol$ = useCallback20(
12864
13089
  (pos, waypoints, speed, currentIdx, arriveThreshold) => patrol(pos, waypoints, speed, currentIdx, arriveThreshold),
12865
13090
  []
12866
13091
  );
12867
- const wander$ = useCallback19(
13092
+ const wander$ = useCallback20(
12868
13093
  (pos, angle, speed, jitter) => wander(pos, angle, speed, jitter),
12869
13094
  []
12870
13095
  );
12871
- const pursuit$ = useCallback19(
13096
+ const pursuit$ = useCallback20(
12872
13097
  (pos, targetPos, targetVel, speed, lookAhead) => pursuit(pos, targetPos, targetVel, speed, lookAhead),
12873
13098
  []
12874
13099
  );
12875
- const evade$ = useCallback19(
13100
+ const evade$ = useCallback20(
12876
13101
  (pos, threatPos, threatVel, speed, lookAhead) => evade(pos, threatPos, threatVel, speed, lookAhead),
12877
13102
  []
12878
13103
  );
12879
- const separation$ = useCallback19(
13104
+ const separation$ = useCallback20(
12880
13105
  (pos, neighbors, speed, radius) => separation(pos, neighbors, speed, radius),
12881
13106
  []
12882
13107
  );
12883
- const cohesion$ = useCallback19(
13108
+ const cohesion$ = useCallback20(
12884
13109
  (pos, neighbors, speed) => cohesion(pos, neighbors, speed),
12885
13110
  []
12886
13111
  );
12887
- const alignment$ = useCallback19(
13112
+ const alignment$ = useCallback20(
12888
13113
  (neighborVelocities, speed) => alignment(neighborVelocities, speed),
12889
13114
  []
12890
13115
  );
@@ -12915,11 +13140,11 @@ function useDamageZone(damage, opts = {}) {
12915
13140
  }
12916
13141
 
12917
13142
  // ../gameplay/src/hooks/useDropThrough.ts
12918
- import { useContext as useContext70, useCallback as useCallback20 } from "react";
13143
+ import { useContext as useContext70, useCallback as useCallback21 } from "react";
12919
13144
  function useDropThrough(frames = 8) {
12920
13145
  const engine = useContext70(EngineContext);
12921
13146
  const entityId = useContext70(EntityContext);
12922
- const dropThrough = useCallback20(() => {
13147
+ const dropThrough = useCallback21(() => {
12923
13148
  const rb = engine.ecs.getComponent(entityId, "RigidBody");
12924
13149
  if (rb) rb.dropThrough = frames;
12925
13150
  }, [engine.ecs, entityId, frames]);
@@ -12927,17 +13152,17 @@ function useDropThrough(frames = 8) {
12927
13152
  }
12928
13153
 
12929
13154
  // ../gameplay/src/hooks/useGameStateMachine.ts
12930
- import { useState as useState24, useRef as useRef40, useCallback as useCallback21, useEffect as useEffect76, useContext as useContext71 } from "react";
13155
+ import { useState as useState24, useRef as useRef41, useCallback as useCallback22, useEffect as useEffect78, useContext as useContext71 } from "react";
12931
13156
  function useGameStateMachine(states, initial) {
12932
13157
  const engine = useContext71(EngineContext);
12933
13158
  const [state, setState] = useState24(initial);
12934
- const stateRef = useRef40(initial);
12935
- const statesRef = useRef40(states);
13159
+ const stateRef = useRef41(initial);
13160
+ const statesRef = useRef41(states);
12936
13161
  statesRef.current = states;
12937
- useEffect76(() => {
13162
+ useEffect78(() => {
12938
13163
  statesRef.current[initial]?.onEnter?.();
12939
13164
  }, []);
12940
- const transition = useCallback21((to) => {
13165
+ const transition = useCallback22((to) => {
12941
13166
  const current = stateRef.current;
12942
13167
  if (current === to) return;
12943
13168
  statesRef.current[current]?.onExit?.();
@@ -12945,7 +13170,7 @@ function useGameStateMachine(states, initial) {
12945
13170
  setState(to);
12946
13171
  statesRef.current[to]?.onEnter?.();
12947
13172
  }, []);
12948
- useEffect76(() => {
13173
+ useEffect78(() => {
12949
13174
  const eid = engine.ecs.createEntity();
12950
13175
  engine.ecs.addComponent(
12951
13176
  eid,
@@ -12961,27 +13186,27 @@ function useGameStateMachine(states, initial) {
12961
13186
  }
12962
13187
 
12963
13188
  // ../gameplay/src/hooks/useHealth.ts
12964
- import { useRef as useRef41, useEffect as useEffect77, useContext as useContext72, useCallback as useCallback22 } from "react";
13189
+ import { useRef as useRef42, useEffect as useEffect79, useContext as useContext72, useCallback as useCallback23 } from "react";
12965
13190
  function useHealth(maxHp, opts = {}) {
12966
13191
  const engine = useContext72(EngineContext);
12967
13192
  const entityId = useContext72(EntityContext);
12968
- const hpRef = useRef41(maxHp);
12969
- const invincibleRef = useRef41(false);
13193
+ const hpRef = useRef42(maxHp);
13194
+ const invincibleRef = useRef42(false);
12970
13195
  const iFrameDuration = opts.iFrames ?? 1;
12971
- const onDeathRef = useRef41(opts.onDeath);
12972
- const onDamageRef = useRef41(opts.onDamage);
12973
- useEffect77(() => {
13196
+ const onDeathRef = useRef42(opts.onDeath);
13197
+ const onDamageRef = useRef42(opts.onDamage);
13198
+ useEffect79(() => {
12974
13199
  onDeathRef.current = opts.onDeath;
12975
13200
  });
12976
- useEffect77(() => {
13201
+ useEffect79(() => {
12977
13202
  onDamageRef.current = opts.onDamage;
12978
13203
  });
12979
- const timerRef = useRef41(
13204
+ const timerRef = useRef42(
12980
13205
  createTimer(iFrameDuration, () => {
12981
13206
  invincibleRef.current = false;
12982
13207
  })
12983
13208
  );
12984
- const takeDamage = useCallback22(
13209
+ const takeDamage = useCallback23(
12985
13210
  (amount = 1) => {
12986
13211
  if (invincibleRef.current || hpRef.current <= 0) return;
12987
13212
  hpRef.current = Math.max(0, hpRef.current - amount);
@@ -12994,28 +13219,28 @@ function useHealth(maxHp, opts = {}) {
12994
13219
  },
12995
13220
  [iFrameDuration]
12996
13221
  );
12997
- const takeDamageRef = useRef41(takeDamage);
12998
- useEffect77(() => {
13222
+ const takeDamageRef = useRef42(takeDamage);
13223
+ useEffect79(() => {
12999
13224
  takeDamageRef.current = takeDamage;
13000
13225
  }, [takeDamage]);
13001
- useEffect77(() => {
13226
+ useEffect79(() => {
13002
13227
  return engine.events.on(`damage:${entityId}`, ({ amount }) => {
13003
13228
  takeDamageRef.current(amount);
13004
13229
  });
13005
13230
  }, [engine.events, entityId]);
13006
- const heal = useCallback22(
13231
+ const heal = useCallback23(
13007
13232
  (amount) => {
13008
13233
  hpRef.current = Math.min(maxHp, hpRef.current + amount);
13009
13234
  },
13010
13235
  [maxHp]
13011
13236
  );
13012
- const setHp = useCallback22(
13237
+ const setHp = useCallback23(
13013
13238
  (hp) => {
13014
13239
  hpRef.current = Math.min(maxHp, Math.max(0, hp));
13015
13240
  },
13016
13241
  [maxHp]
13017
13242
  );
13018
- const update = useCallback22((dt) => {
13243
+ const update = useCallback23((dt) => {
13019
13244
  timerRef.current.update(dt);
13020
13245
  }, []);
13021
13246
  return {
@@ -13039,11 +13264,11 @@ function useHealth(maxHp, opts = {}) {
13039
13264
  }
13040
13265
 
13041
13266
  // ../gameplay/src/hooks/useKinematicBody.ts
13042
- import { useContext as useContext73, useCallback as useCallback23 } from "react";
13267
+ import { useContext as useContext73, useCallback as useCallback24 } from "react";
13043
13268
  function useKinematicBody() {
13044
13269
  const engine = useContext73(EngineContext);
13045
13270
  const entityId = useContext73(EntityContext);
13046
- const moveAndCollide = useCallback23(
13271
+ const moveAndCollide = useCallback24(
13047
13272
  (dx, dy) => {
13048
13273
  const transform = engine.ecs.getComponent(entityId, "Transform");
13049
13274
  if (!transform) return { dx: 0, dy: 0 };
@@ -13079,7 +13304,7 @@ function useKinematicBody() {
13079
13304
  },
13080
13305
  [engine.ecs, entityId]
13081
13306
  );
13082
- const setVelocity = useCallback23(
13307
+ const setVelocity = useCallback24(
13083
13308
  (vx, vy) => {
13084
13309
  const rb = engine.ecs.getComponent(entityId, "RigidBody");
13085
13310
  if (rb) {
@@ -13089,7 +13314,7 @@ function useKinematicBody() {
13089
13314
  },
13090
13315
  [engine.ecs, entityId]
13091
13316
  );
13092
- const setAngularVelocity = useCallback23(
13317
+ const setAngularVelocity = useCallback24(
13093
13318
  (w) => {
13094
13319
  const rb = engine.ecs.getComponent(entityId, "RigidBody");
13095
13320
  if (rb) rb.angularVelocity = w;
@@ -13100,12 +13325,12 @@ function useKinematicBody() {
13100
13325
  }
13101
13326
 
13102
13327
  // ../gameplay/src/hooks/useLevelTransition.ts
13103
- import { useState as useState25, useRef as useRef42, useCallback as useCallback24 } from "react";
13328
+ import { useState as useState25, useRef as useRef43, useCallback as useCallback25 } from "react";
13104
13329
  function useLevelTransition(initial) {
13105
13330
  const [currentLevel, setCurrentLevel] = useState25(initial);
13106
13331
  const [isTransitioning, setIsTransitioning] = useState25(false);
13107
- const overlayRef = useRef42(null);
13108
- const transitionTo = useCallback24((level, opts = {}) => {
13332
+ const overlayRef = useRef43(null);
13333
+ const transitionTo = useCallback25((level, opts = {}) => {
13109
13334
  const { duration = 0.4, type = "fade" } = opts;
13110
13335
  if (type === "instant") {
13111
13336
  setCurrentLevel(level);
@@ -13142,7 +13367,7 @@ function useLevelTransition(initial) {
13142
13367
  }
13143
13368
 
13144
13369
  // ../gameplay/src/hooks/usePlatformerController.ts
13145
- import { useContext as useContext74, useEffect as useEffect78 } from "react";
13370
+ import { useContext as useContext74, useEffect as useEffect80 } from "react";
13146
13371
  function normalizeKeys(val, defaults) {
13147
13372
  if (!val) return defaults;
13148
13373
  return Array.isArray(val) ? val : [val];
@@ -13166,7 +13391,7 @@ function usePlatformerController(entityId, opts = {}) {
13166
13391
  const leftKeys = normalizeKeys(bindings?.left, ["ArrowLeft", "KeyA", "a"]);
13167
13392
  const rightKeys = normalizeKeys(bindings?.right, ["ArrowRight", "KeyD", "d"]);
13168
13393
  const jumpKeys = normalizeKeys(bindings?.jump, ["Space", "ArrowUp", "KeyW", "w"]);
13169
- useEffect78(() => {
13394
+ useEffect80(() => {
13170
13395
  const state = {
13171
13396
  coyoteTimer: 0,
13172
13397
  jumpBuffer: 0,
@@ -13230,22 +13455,22 @@ function usePlatformerController(entityId, opts = {}) {
13230
13455
  }
13231
13456
 
13232
13457
  // ../gameplay/src/hooks/usePathfinding.ts
13233
- import { useCallback as useCallback25 } from "react";
13458
+ import { useCallback as useCallback26 } from "react";
13234
13459
  function usePathfinding() {
13235
- const createGrid$ = useCallback25(
13460
+ const createGrid$ = useCallback26(
13236
13461
  (cols, rows, cellSize) => createNavGrid(cols, rows, cellSize),
13237
13462
  []
13238
13463
  );
13239
- const setWalkable$ = useCallback25(
13464
+ const setWalkable$ = useCallback26(
13240
13465
  (grid, col, row, walkable) => setWalkable(grid, col, row, walkable),
13241
13466
  []
13242
13467
  );
13243
- const findPath$ = useCallback25((grid, start2, goal) => findPath(grid, start2, goal), []);
13468
+ const findPath$ = useCallback26((grid, start2, goal) => findPath(grid, start2, goal), []);
13244
13469
  return { createGrid: createGrid$, setWalkable: setWalkable$, findPath: findPath$ };
13245
13470
  }
13246
13471
 
13247
13472
  // ../gameplay/src/hooks/usePersistedBindings.ts
13248
- import { useState as useState26, useCallback as useCallback26, useMemo as useMemo16, useContext as useContext75 } from "react";
13473
+ import { useState as useState26, useCallback as useCallback27, useMemo as useMemo17, useContext as useContext75 } from "react";
13249
13474
  function usePersistedBindings(storageKey, defaults) {
13250
13475
  const engine = useContext75(EngineContext);
13251
13476
  const input = engine.input;
@@ -13257,7 +13482,7 @@ function usePersistedBindings(storageKey, defaults) {
13257
13482
  }
13258
13483
  return defaults;
13259
13484
  });
13260
- const normalized = useMemo16(() => {
13485
+ const normalized = useMemo17(() => {
13261
13486
  const out = {};
13262
13487
  for (const [action, keys] of Object.entries(bindings)) {
13263
13488
  if (typeof keys === "string") out[action] = [keys];
@@ -13265,7 +13490,7 @@ function usePersistedBindings(storageKey, defaults) {
13265
13490
  }
13266
13491
  return out;
13267
13492
  }, [bindings]);
13268
- const rebind = useCallback26(
13493
+ const rebind = useCallback27(
13269
13494
  (action, keys) => {
13270
13495
  setBindings((prev) => {
13271
13496
  const next = { ...prev, [action]: Array.isArray(keys) ? keys : [keys] };
@@ -13278,14 +13503,14 @@ function usePersistedBindings(storageKey, defaults) {
13278
13503
  },
13279
13504
  [storageKey]
13280
13505
  );
13281
- const reset = useCallback26(() => {
13506
+ const reset = useCallback27(() => {
13282
13507
  try {
13283
13508
  localStorage.removeItem(storageKey);
13284
13509
  } catch {
13285
13510
  }
13286
13511
  setBindings(defaults);
13287
13512
  }, [storageKey]);
13288
- return useMemo16(
13513
+ return useMemo17(
13289
13514
  () => ({
13290
13515
  bindings,
13291
13516
  rebind,
@@ -13299,21 +13524,21 @@ function usePersistedBindings(storageKey, defaults) {
13299
13524
  }
13300
13525
 
13301
13526
  // ../gameplay/src/hooks/useRestart.ts
13302
- import { useState as useState27, useCallback as useCallback27 } from "react";
13527
+ import { useState as useState27, useCallback as useCallback28 } from "react";
13303
13528
  function useRestart() {
13304
13529
  const [restartKey, setRestartKey] = useState27(0);
13305
- const restart = useCallback27(() => {
13530
+ const restart = useCallback28(() => {
13306
13531
  setRestartKey((k) => k + 1);
13307
13532
  }, []);
13308
13533
  return { restartKey, restart };
13309
13534
  }
13310
13535
 
13311
13536
  // ../gameplay/src/hooks/useSave.ts
13312
- import { useCallback as useCallback28, useRef as useRef43 } from "react";
13537
+ import { useCallback as useCallback29, useRef as useRef44 } from "react";
13313
13538
  function useSave(key, defaultValue, opts = {}) {
13314
13539
  const version = opts.version ?? 1;
13315
- const dataRef = useRef43(defaultValue);
13316
- const save = useCallback28(
13540
+ const dataRef = useRef44(defaultValue);
13541
+ const save = useCallback29(
13317
13542
  (value) => {
13318
13543
  dataRef.current = value;
13319
13544
  const slot = { version, data: value };
@@ -13325,7 +13550,7 @@ function useSave(key, defaultValue, opts = {}) {
13325
13550
  },
13326
13551
  [key, version]
13327
13552
  );
13328
- const load = useCallback28(() => {
13553
+ const load = useCallback29(() => {
13329
13554
  try {
13330
13555
  const raw = localStorage.getItem(key);
13331
13556
  if (!raw) return defaultValue;
@@ -13345,11 +13570,11 @@ function useSave(key, defaultValue, opts = {}) {
13345
13570
  return defaultValue;
13346
13571
  }
13347
13572
  }, [key, version]);
13348
- const clear = useCallback28(() => {
13573
+ const clear = useCallback29(() => {
13349
13574
  localStorage.removeItem(key);
13350
13575
  dataRef.current = defaultValue;
13351
13576
  }, [key]);
13352
- const exists = useCallback28(() => {
13577
+ const exists = useCallback29(() => {
13353
13578
  return localStorage.getItem(key) !== null;
13354
13579
  }, [key]);
13355
13580
  return {
@@ -13364,7 +13589,7 @@ function useSave(key, defaultValue, opts = {}) {
13364
13589
  }
13365
13590
 
13366
13591
  // ../gameplay/src/hooks/useIDBSave.ts
13367
- import { useCallback as useCallback29, useRef as useRef44 } from "react";
13592
+ import { useCallback as useCallback30, useRef as useRef45 } from "react";
13368
13593
  var DB_NAME = "cubeforge-saves";
13369
13594
  var STORE_NAME = "saves";
13370
13595
  function openDB() {
@@ -13382,12 +13607,12 @@ function openDB() {
13382
13607
  }
13383
13608
  function useIDBSave(key, defaultValue, opts = {}) {
13384
13609
  const version = opts.version ?? 1;
13385
- const dbRef = useRef44(null);
13386
- const getDB = useCallback29(() => {
13610
+ const dbRef = useRef45(null);
13611
+ const getDB = useCallback30(() => {
13387
13612
  if (!dbRef.current) dbRef.current = openDB();
13388
13613
  return dbRef.current;
13389
13614
  }, []);
13390
- const save = useCallback29(
13615
+ const save = useCallback30(
13391
13616
  async (value) => {
13392
13617
  const db = await getDB();
13393
13618
  const slot = { version, data: value };
@@ -13400,7 +13625,7 @@ function useIDBSave(key, defaultValue, opts = {}) {
13400
13625
  },
13401
13626
  [getDB, key, version]
13402
13627
  );
13403
- const load = useCallback29(async () => {
13628
+ const load = useCallback30(async () => {
13404
13629
  const db = await getDB();
13405
13630
  return new Promise((resolve, reject) => {
13406
13631
  const tx = db.transaction(STORE_NAME, "readonly");
@@ -13426,7 +13651,7 @@ function useIDBSave(key, defaultValue, opts = {}) {
13426
13651
  req.onerror = () => reject(req.error);
13427
13652
  });
13428
13653
  }, [getDB, key, version]);
13429
- const clear = useCallback29(async () => {
13654
+ const clear = useCallback30(async () => {
13430
13655
  const db = await getDB();
13431
13656
  return new Promise((resolve, reject) => {
13432
13657
  const tx = db.transaction(STORE_NAME, "readwrite");
@@ -13435,7 +13660,7 @@ function useIDBSave(key, defaultValue, opts = {}) {
13435
13660
  req.onerror = () => reject(req.error);
13436
13661
  });
13437
13662
  }, [getDB, key]);
13438
- const exists = useCallback29(async () => {
13663
+ const exists = useCallback30(async () => {
13439
13664
  const db = await getDB();
13440
13665
  return new Promise((resolve, reject) => {
13441
13666
  const tx = db.transaction(STORE_NAME, "readonly");
@@ -13448,7 +13673,7 @@ function useIDBSave(key, defaultValue, opts = {}) {
13448
13673
  }
13449
13674
 
13450
13675
  // ../gameplay/src/hooks/useSaveSlots.ts
13451
- import { useCallback as useCallback30, useState as useState28 } from "react";
13676
+ import { useCallback as useCallback31, useState as useState28 } from "react";
13452
13677
  function useSaveSlots(namespace, opts = {}) {
13453
13678
  const version = opts.version ?? 1;
13454
13679
  const indexKey = `cubeforge-saves:${namespace}:__index`;
@@ -13502,7 +13727,7 @@ function useSaveSlots(namespace, opts = {}) {
13502
13727
  const metas = readIndex().map((id) => readSlot(id)?.meta).filter((m) => m !== void 0).sort((a, b) => a.savedAt < b.savedAt ? 1 : -1);
13503
13728
  setSlots(metas);
13504
13729
  }
13505
- const save = useCallback30(
13730
+ const save = useCallback31(
13506
13731
  (id, data, label, thumbnail) => {
13507
13732
  const meta = {
13508
13733
  id,
@@ -13517,7 +13742,7 @@ function useSaveSlots(namespace, opts = {}) {
13517
13742
  // eslint-disable-next-line react-hooks/exhaustive-deps
13518
13743
  [namespace, version]
13519
13744
  );
13520
- const load = useCallback30(
13745
+ const load = useCallback31(
13521
13746
  (id) => {
13522
13747
  const entry = readSlot(id);
13523
13748
  if (!entry) return null;
@@ -13533,7 +13758,7 @@ function useSaveSlots(namespace, opts = {}) {
13533
13758
  // eslint-disable-next-line react-hooks/exhaustive-deps
13534
13759
  [namespace, version]
13535
13760
  );
13536
- const deleteFn = useCallback30(
13761
+ const deleteFn = useCallback31(
13537
13762
  (id) => {
13538
13763
  try {
13539
13764
  localStorage.removeItem(slotKey(id));
@@ -13545,7 +13770,7 @@ function useSaveSlots(namespace, opts = {}) {
13545
13770
  // eslint-disable-next-line react-hooks/exhaustive-deps
13546
13771
  [namespace]
13547
13772
  );
13548
- const copy = useCallback30(
13773
+ const copy = useCallback31(
13549
13774
  (fromId, toId) => {
13550
13775
  const entry = readSlot(fromId);
13551
13776
  if (!entry) return false;
@@ -13558,7 +13783,7 @@ function useSaveSlots(namespace, opts = {}) {
13558
13783
  // eslint-disable-next-line react-hooks/exhaustive-deps
13559
13784
  [namespace, version]
13560
13785
  );
13561
- const rename = useCallback30(
13786
+ const rename = useCallback31(
13562
13787
  (id, label) => {
13563
13788
  const entry = readSlot(id);
13564
13789
  if (!entry) return;
@@ -13568,10 +13793,10 @@ function useSaveSlots(namespace, opts = {}) {
13568
13793
  // eslint-disable-next-line react-hooks/exhaustive-deps
13569
13794
  [namespace]
13570
13795
  );
13571
- const listSlots = useCallback30(() => {
13796
+ const listSlots = useCallback31(() => {
13572
13797
  return readIndex().map((id) => readSlot(id)?.meta).filter((m) => m !== void 0).sort((a, b) => a.savedAt < b.savedAt ? 1 : -1);
13573
13798
  }, [namespace]);
13574
- const exists = useCallback30(
13799
+ const exists = useCallback31(
13575
13800
  (id) => {
13576
13801
  return readSlot(id) !== null;
13577
13802
  },
@@ -13593,7 +13818,7 @@ function useSaveSlots(namespace, opts = {}) {
13593
13818
  }
13594
13819
 
13595
13820
  // ../gameplay/src/hooks/useTopDownMovement.ts
13596
- import { useContext as useContext76, useEffect as useEffect79 } from "react";
13821
+ import { useContext as useContext76, useEffect as useEffect81 } from "react";
13597
13822
  function moveToward(current, target, maxDelta) {
13598
13823
  const diff = target - current;
13599
13824
  if (Math.abs(diff) <= maxDelta) return target;
@@ -13602,7 +13827,7 @@ function moveToward(current, target, maxDelta) {
13602
13827
  function useTopDownMovement(entityId, opts = {}) {
13603
13828
  const engine = useContext76(EngineContext);
13604
13829
  const { speed = 200, normalizeDiagonal = true, acceleration = Infinity, deceleration = acceleration } = opts;
13605
- useEffect79(() => {
13830
+ useEffect81(() => {
13606
13831
  const updateFn = (id, world, input, dt) => {
13607
13832
  if (!world.hasEntity(id)) return;
13608
13833
  const rb = world.getComponent(id, "RigidBody");
@@ -13642,18 +13867,18 @@ function useTopDownMovement(entityId, opts = {}) {
13642
13867
  }
13643
13868
 
13644
13869
  // ../gameplay/src/hooks/useDialogue.ts
13645
- import { useState as useState29, useCallback as useCallback31, useRef as useRef45 } from "react";
13870
+ import { useState as useState29, useCallback as useCallback32, useRef as useRef46 } from "react";
13646
13871
  function useDialogue() {
13647
13872
  const [active, setActive] = useState29(false);
13648
13873
  const [currentId, setCurrentId] = useState29(null);
13649
- const scriptRef = useRef45(null);
13650
- const start2 = useCallback31((script, startId) => {
13874
+ const scriptRef = useRef46(null);
13875
+ const start2 = useCallback32((script, startId) => {
13651
13876
  scriptRef.current = script;
13652
13877
  const id = startId ?? Object.keys(script)[0];
13653
13878
  setCurrentId(id);
13654
13879
  setActive(true);
13655
13880
  }, []);
13656
- const advance = useCallback31(
13881
+ const advance = useCallback32(
13657
13882
  (choiceIndex) => {
13658
13883
  if (!scriptRef.current || !currentId) return;
13659
13884
  const line = scriptRef.current[currentId];
@@ -13683,7 +13908,7 @@ function useDialogue() {
13683
13908
  },
13684
13909
  [currentId]
13685
13910
  );
13686
- const close = useCallback31(() => {
13911
+ const close = useCallback32(() => {
13687
13912
  setActive(false);
13688
13913
  setCurrentId(null);
13689
13914
  scriptRef.current = null;
@@ -13693,7 +13918,7 @@ function useDialogue() {
13693
13918
  }
13694
13919
 
13695
13920
  // ../gameplay/src/hooks/useDialogueTree.ts
13696
- import { useState as useState30, useCallback as useCallback32, useRef as useRef46 } from "react";
13921
+ import { useState as useState30, useCallback as useCallback33, useRef as useRef47 } from "react";
13697
13922
  function interpolate(text, vars) {
13698
13923
  return text.replace(/\{(\w+)\}/g, (_, key) => {
13699
13924
  const val = vars[key];
@@ -13711,8 +13936,8 @@ function useDialogueTree() {
13711
13936
  const [active, setActive] = useState30(false);
13712
13937
  const [currentId, setCurrentId] = useState30(null);
13713
13938
  const [variables, setVariables] = useState30({});
13714
- const scriptRef = useRef46(null);
13715
- const varsRef = useRef46({});
13939
+ const scriptRef = useRef47(null);
13940
+ const varsRef = useRef47({});
13716
13941
  function setCurrentNode(id) {
13717
13942
  if (!id || !scriptRef.current) {
13718
13943
  setActive(false);
@@ -13728,7 +13953,7 @@ function useDialogueTree() {
13728
13953
  node.onEnter?.(varsRef.current);
13729
13954
  setCurrentId(id);
13730
13955
  }
13731
- const start2 = useCallback32(
13956
+ const start2 = useCallback33(
13732
13957
  (script, startId, initialVars) => {
13733
13958
  scriptRef.current = script;
13734
13959
  const newVars = { ...varsRef.current, ...initialVars };
@@ -13741,7 +13966,7 @@ function useDialogueTree() {
13741
13966
  // eslint-disable-next-line react-hooks/exhaustive-deps
13742
13967
  []
13743
13968
  );
13744
- const advance = useCallback32(
13969
+ const advance = useCallback33(
13745
13970
  (choiceIndex) => {
13746
13971
  if (!scriptRef.current || !currentId) return;
13747
13972
  const node = scriptRef.current[currentId];
@@ -13780,7 +14005,7 @@ function useDialogueTree() {
13780
14005
  },
13781
14006
  [currentId]
13782
14007
  );
13783
- const jumpTo = useCallback32(
14008
+ const jumpTo = useCallback33(
13784
14009
  (id) => {
13785
14010
  if (!scriptRef.current?.[id]) return;
13786
14011
  const current2 = scriptRef.current[currentId ?? ""];
@@ -13789,11 +14014,11 @@ function useDialogueTree() {
13789
14014
  },
13790
14015
  [currentId]
13791
14016
  );
13792
- const setVar = useCallback32((key, value) => {
14017
+ const setVar = useCallback33((key, value) => {
13793
14018
  varsRef.current = { ...varsRef.current, [key]: value };
13794
14019
  setVariables({ ...varsRef.current });
13795
14020
  }, []);
13796
- const close = useCallback32(() => {
14021
+ const close = useCallback33(() => {
13797
14022
  if (scriptRef.current && currentId) {
13798
14023
  scriptRef.current[currentId]?.onExit?.(varsRef.current);
13799
14024
  }
@@ -13819,7 +14044,7 @@ function useDialogueTree() {
13819
14044
  }
13820
14045
 
13821
14046
  // ../gameplay/src/hooks/useBehaviorTree.ts
13822
- import { useRef as useRef47, useCallback as useCallback33 } from "react";
14047
+ import { useRef as useRef48, useCallback as useCallback34 } from "react";
13823
14048
  function btAction(fn) {
13824
14049
  return {
13825
14050
  tick(dt) {
@@ -14032,16 +14257,16 @@ function btFail(child) {
14032
14257
  };
14033
14258
  }
14034
14259
  function useBehaviorTree(rootFactory) {
14035
- const rootRef = useRef47(null);
14260
+ const rootRef = useRef48(null);
14036
14261
  if (!rootRef.current) rootRef.current = rootFactory();
14037
- const statusRef = useRef47("failure");
14038
- const tick2 = useCallback33((dt) => {
14262
+ const statusRef = useRef48("failure");
14263
+ const tick2 = useCallback34((dt) => {
14039
14264
  if (!rootRef.current) return "failure";
14040
14265
  const s2 = rootRef.current.tick(dt);
14041
14266
  statusRef.current = s2;
14042
14267
  return s2;
14043
14268
  }, []);
14044
- const reset = useCallback33(() => {
14269
+ const reset = useCallback34(() => {
14045
14270
  rootRef.current?.reset();
14046
14271
  }, []);
14047
14272
  return {
@@ -14162,17 +14387,17 @@ function DialogueBox({ dialogue, onAdvance, onClose, style = {} }) {
14162
14387
  }
14163
14388
 
14164
14389
  // ../gameplay/src/hooks/useCutscene.ts
14165
- import { useState as useState31, useCallback as useCallback34, useRef as useRef48, useEffect as useEffect80, useContext as useContext77 } from "react";
14390
+ import { useState as useState31, useCallback as useCallback35, useRef as useRef49, useEffect as useEffect82, useContext as useContext77 } from "react";
14166
14391
  function useCutscene() {
14167
14392
  const engine = useContext77(EngineContext);
14168
14393
  const [playing, setPlaying] = useState31(false);
14169
14394
  const [stepIndex, setStepIndex] = useState31(0);
14170
- const stepsRef = useRef48([]);
14171
- const timerRef = useRef48(0);
14172
- const idxRef = useRef48(0);
14173
- const playingRef = useRef48(false);
14174
- const entityRef = useRef48(null);
14175
- const finish = useCallback34(() => {
14395
+ const stepsRef = useRef49([]);
14396
+ const timerRef = useRef49(0);
14397
+ const idxRef = useRef49(0);
14398
+ const playingRef = useRef49(false);
14399
+ const entityRef = useRef49(null);
14400
+ const finish = useCallback35(() => {
14176
14401
  playingRef.current = false;
14177
14402
  setPlaying(false);
14178
14403
  setStepIndex(0);
@@ -14182,14 +14407,14 @@ function useCutscene() {
14182
14407
  entityRef.current = null;
14183
14408
  }
14184
14409
  }, [engine.ecs]);
14185
- const fireStep = useCallback34((step) => {
14410
+ const fireStep = useCallback35((step) => {
14186
14411
  if (step.type === "call") step.fn();
14187
14412
  if (step.type === "parallel")
14188
14413
  step.steps.forEach((s2) => {
14189
14414
  if (s2.type === "call") s2.fn();
14190
14415
  });
14191
14416
  }, []);
14192
- const play = useCallback34(
14417
+ const play = useCallback35(
14193
14418
  (steps) => {
14194
14419
  stepsRef.current = steps;
14195
14420
  idxRef.current = 0;
@@ -14246,7 +14471,7 @@ function useCutscene() {
14246
14471
  },
14247
14472
  [engine.ecs, finish, fireStep]
14248
14473
  );
14249
- const skip = useCallback34(() => {
14474
+ const skip = useCallback35(() => {
14250
14475
  for (let i = idxRef.current; i < stepsRef.current.length; i++) {
14251
14476
  const step = stepsRef.current[i];
14252
14477
  if (step.type === "call") step.fn();
@@ -14257,7 +14482,7 @@ function useCutscene() {
14257
14482
  }
14258
14483
  finish();
14259
14484
  }, [finish]);
14260
- useEffect80(() => {
14485
+ useEffect82(() => {
14261
14486
  return () => {
14262
14487
  if (entityRef.current !== null && engine.ecs.hasEntity(entityRef.current)) {
14263
14488
  engine.ecs.destroyEntity(entityRef.current);
@@ -14268,7 +14493,7 @@ function useCutscene() {
14268
14493
  }
14269
14494
 
14270
14495
  // ../gameplay/src/hooks/useGameStore.ts
14271
- import { useSyncExternalStore as useSyncExternalStore2, useCallback as useCallback35 } from "react";
14496
+ import { useSyncExternalStore as useSyncExternalStore2, useCallback as useCallback36 } from "react";
14272
14497
  function createStore(initialState) {
14273
14498
  let state = { ...initialState };
14274
14499
  const listeners = /* @__PURE__ */ new Set();
@@ -14294,7 +14519,7 @@ function useGameStore(key, initialState) {
14294
14519
  }
14295
14520
  const store = stores.get(key);
14296
14521
  const state = useSyncExternalStore2(store.subscribe, store.getState);
14297
- const setState = useCallback35(
14522
+ const setState = useCallback36(
14298
14523
  (partial) => {
14299
14524
  store.setState(partial);
14300
14525
  },
@@ -14304,21 +14529,21 @@ function useGameStore(key, initialState) {
14304
14529
  }
14305
14530
 
14306
14531
  // ../gameplay/src/hooks/useTween.ts
14307
- import { useRef as useRef49, useCallback as useCallback36, useEffect as useEffect81 } from "react";
14532
+ import { useRef as useRef50, useCallback as useCallback37, useEffect as useEffect83 } from "react";
14308
14533
  function useTween(opts) {
14309
- const rafRef = useRef49(null);
14310
- const startTimeRef = useRef49(0);
14311
- const runningRef = useRef49(false);
14312
- const optsRef = useRef49(opts);
14534
+ const rafRef = useRef50(null);
14535
+ const startTimeRef = useRef50(0);
14536
+ const runningRef = useRef50(false);
14537
+ const optsRef = useRef50(opts);
14313
14538
  optsRef.current = opts;
14314
- const stop = useCallback36(() => {
14539
+ const stop = useCallback37(() => {
14315
14540
  if (rafRef.current !== null) {
14316
14541
  cancelAnimationFrame(rafRef.current);
14317
14542
  rafRef.current = null;
14318
14543
  }
14319
14544
  runningRef.current = false;
14320
14545
  }, []);
14321
- const start2 = useCallback36(() => {
14546
+ const start2 = useCallback37(() => {
14322
14547
  stop();
14323
14548
  runningRef.current = true;
14324
14549
  startTimeRef.current = performance.now();
@@ -14340,12 +14565,12 @@ function useTween(opts) {
14340
14565
  };
14341
14566
  rafRef.current = requestAnimationFrame(tick2);
14342
14567
  }, [stop]);
14343
- useEffect81(() => {
14568
+ useEffect83(() => {
14344
14569
  if (opts.autoStart) {
14345
14570
  start2();
14346
14571
  }
14347
14572
  }, []);
14348
- useEffect81(() => {
14573
+ useEffect83(() => {
14349
14574
  return () => {
14350
14575
  if (rafRef.current !== null) {
14351
14576
  cancelAnimationFrame(rafRef.current);
@@ -14364,15 +14589,15 @@ function useTween(opts) {
14364
14589
  }
14365
14590
 
14366
14591
  // ../gameplay/src/hooks/useObjectPool.ts
14367
- import { useRef as useRef50, useMemo as useMemo17, useEffect as useEffect82 } from "react";
14592
+ import { useRef as useRef51, useMemo as useMemo18, useEffect as useEffect84 } from "react";
14368
14593
  function useObjectPool(factory, reset, initialSize) {
14369
- const poolRef = useRef50([]);
14370
- const activeRef = useRef50(0);
14371
- const factoryRef = useRef50(factory);
14594
+ const poolRef = useRef51([]);
14595
+ const activeRef = useRef51(0);
14596
+ const factoryRef = useRef51(factory);
14372
14597
  factoryRef.current = factory;
14373
- const resetRef = useRef50(reset);
14598
+ const resetRef = useRef51(reset);
14374
14599
  resetRef.current = reset;
14375
- useEffect82(() => {
14600
+ useEffect84(() => {
14376
14601
  if (initialSize != null && initialSize > 0) {
14377
14602
  const pool = poolRef.current;
14378
14603
  for (let i = 0; i < initialSize; i++) {
@@ -14380,7 +14605,7 @@ function useObjectPool(factory, reset, initialSize) {
14380
14605
  }
14381
14606
  }
14382
14607
  }, []);
14383
- return useMemo17(
14608
+ return useMemo18(
14384
14609
  () => ({
14385
14610
  acquire() {
14386
14611
  activeRef.current++;
@@ -14411,11 +14636,11 @@ function useObjectPool(factory, reset, initialSize) {
14411
14636
  }
14412
14637
 
14413
14638
  // ../gameplay/src/hooks/useForces.ts
14414
- import { useContext as useContext78, useMemo as useMemo18 } from "react";
14639
+ import { useContext as useContext78, useMemo as useMemo19 } from "react";
14415
14640
  function useForces() {
14416
14641
  const engine = useContext78(EngineContext);
14417
14642
  const entityId = useContext78(EntityContext);
14418
- return useMemo18(() => {
14643
+ return useMemo19(() => {
14419
14644
  const getRb = () => engine.ecs.getComponent(entityId, "RigidBody");
14420
14645
  const getTransform = () => engine.ecs.getComponent(entityId, "Transform");
14421
14646
  const getCenter = () => {
@@ -14483,11 +14708,11 @@ function useForces() {
14483
14708
  }
14484
14709
 
14485
14710
  // ../gameplay/src/hooks/useCharacterController.ts
14486
- import { useContext as useContext79, useMemo as useMemo19 } from "react";
14711
+ import { useContext as useContext79, useMemo as useMemo20 } from "react";
14487
14712
  function useCharacterController(config = {}) {
14488
14713
  const engine = useContext79(EngineContext);
14489
14714
  const entityId = useContext79(EntityContext);
14490
- return useMemo19(() => {
14715
+ return useMemo20(() => {
14491
14716
  const controller = new CharacterController(config);
14492
14717
  return {
14493
14718
  move(dx, dy) {
@@ -14508,7 +14733,7 @@ function createAtlas(names, _columns) {
14508
14733
  }
14509
14734
 
14510
14735
  // src/components/HUD.tsx
14511
- import { createContext as createContext3, useContext as useContext80, useMemo as useMemo20 } from "react";
14736
+ import { createContext as createContext3, useContext as useContext80, useMemo as useMemo21 } from "react";
14512
14737
  import { jsx as jsx22, jsxs as jsxs11 } from "react/jsx-runtime";
14513
14738
  var HUDContextRef = createContext3(null);
14514
14739
  function HUD({
@@ -14521,7 +14746,7 @@ function HUD({
14521
14746
  className,
14522
14747
  children
14523
14748
  }) {
14524
- const ctx = useMemo20(() => ({ padding, safeArea }), [padding, safeArea]);
14749
+ const ctx = useMemo21(() => ({ padding, safeArea }), [padding, safeArea]);
14525
14750
  const rootStyle = {
14526
14751
  position: "absolute",
14527
14752
  inset: 0,
@@ -14610,7 +14835,7 @@ function HUDBar({
14610
14835
  display: "flex",
14611
14836
  flexDirection: rtl ? "row-reverse" : "row"
14612
14837
  };
14613
- const labelStyle = {
14838
+ const labelStyle2 = {
14614
14839
  fontFamily: "system-ui, sans-serif",
14615
14840
  fontSize: 11,
14616
14841
  letterSpacing: 0.5,
@@ -14622,7 +14847,7 @@ function HUDBar({
14622
14847
  marginBottom: 4
14623
14848
  };
14624
14849
  return /* @__PURE__ */ jsxs11("div", { style: { display: "flex", flexDirection: "column", ...style }, "aria-label": label, children: [
14625
- (label || showValue) && /* @__PURE__ */ jsxs11("div", { style: labelStyle, children: [
14850
+ (label || showValue) && /* @__PURE__ */ jsxs11("div", { style: labelStyle2, children: [
14626
14851
  label && /* @__PURE__ */ jsx22("span", { children: label }),
14627
14852
  showValue && /* @__PURE__ */ jsxs11("span", { style: { fontVariantNumeric: "tabular-nums" }, children: [
14628
14853
  Math.round(value),
@@ -14694,7 +14919,7 @@ function definePrefab(name, defaults, render) {
14694
14919
  }
14695
14920
 
14696
14921
  // src/hooks/useNetworkSync.ts
14697
- import { useEffect as useEffect84, useRef as useRef52 } from "react";
14922
+ import { useEffect as useEffect86, useRef as useRef53 } from "react";
14698
14923
 
14699
14924
  // ../../packages/net/src/transport.ts
14700
14925
  function isBinaryTransport(t) {
@@ -15039,7 +15264,7 @@ function syncEntity(config) {
15039
15264
  }
15040
15265
 
15041
15266
  // ../../packages/net/src/useNetworkInput.ts
15042
- import { useState as useState32, useEffect as useEffect83, useRef as useRef51 } from "react";
15267
+ import { useState as useState32, useEffect as useEffect85, useRef as useRef52 } from "react";
15043
15268
  var INPUT_MSG_TYPE = "input:state";
15044
15269
  function useNetworkInput(config) {
15045
15270
  const { room, keys, input, tickRate = 20 } = config;
@@ -15048,8 +15273,8 @@ function useNetworkInput(config) {
15048
15273
  () => Object.fromEntries(keys.map((k) => [k, false]))
15049
15274
  );
15050
15275
  const [remoteInputs] = useState32(() => /* @__PURE__ */ new Map());
15051
- const localInputRef = useRef51(localInput);
15052
- useEffect83(() => {
15276
+ const localInputRef = useRef52(localInput);
15277
+ useEffect85(() => {
15053
15278
  let cleanupDom = null;
15054
15279
  if (!input) {
15055
15280
  let handleKeyDown2 = function(e) {
@@ -15263,9 +15488,9 @@ function lerpState(a, b, t) {
15263
15488
 
15264
15489
  // src/hooks/useNetworkSync.ts
15265
15490
  function useNetworkSync(entityId, components, room, world, owner, opts = {}) {
15266
- const optsRef = useRef52(opts);
15491
+ const optsRef = useRef53(opts);
15267
15492
  optsRef.current = opts;
15268
- useEffect84(() => {
15493
+ useEffect86(() => {
15269
15494
  const sync = syncEntity({
15270
15495
  entityId,
15271
15496
  components,
@@ -15280,18 +15505,18 @@ function useNetworkSync(entityId, components, room, world, owner, opts = {}) {
15280
15505
  }
15281
15506
 
15282
15507
  // src/hooks/useRemotePlayer.ts
15283
- import { useEffect as useEffect85, useRef as useRef53, useState as useState33 } from "react";
15508
+ import { useEffect as useEffect87, useRef as useRef54, useState as useState33 } from "react";
15284
15509
  var PEER_JOIN_MSG = "peer:join";
15285
15510
  var PEER_LEAVE_MSG = "peer:leave";
15286
15511
  function useRemotePlayer(config) {
15287
15512
  const { room, world, createEntity, destroyEntity } = config;
15288
15513
  const [players, setPlayers] = useState33(() => /* @__PURE__ */ new Map());
15289
- const playersRef = useRef53(players);
15290
- const createRef = useRef53(createEntity);
15514
+ const playersRef = useRef54(players);
15515
+ const createRef = useRef54(createEntity);
15291
15516
  createRef.current = createEntity;
15292
- const destroyRef = useRef53(destroyEntity);
15517
+ const destroyRef = useRef54(destroyEntity);
15293
15518
  destroyRef.current = destroyEntity;
15294
- useEffect85(() => {
15519
+ useEffect87(() => {
15295
15520
  function spawnPeer(peerId) {
15296
15521
  if (playersRef.current.has(peerId)) return;
15297
15522
  const entityId = createRef.current(peerId);
@@ -15330,12 +15555,558 @@ function useRemotePlayer(config) {
15330
15555
  }, [room, world]);
15331
15556
  return { players };
15332
15557
  }
15558
+
15559
+ // ../editor/src/components/SceneHierarchy.tsx
15560
+ import { jsx as jsx23, jsxs as jsxs12 } from "react/jsx-runtime";
15561
+ var panelStyle = {
15562
+ background: "#0d1520",
15563
+ borderRight: "1px solid #1a2a3a",
15564
+ color: "#c0d0e0",
15565
+ fontFamily: "system-ui, sans-serif",
15566
+ fontSize: 12,
15567
+ display: "flex",
15568
+ flexDirection: "column",
15569
+ overflow: "hidden",
15570
+ userSelect: "none"
15571
+ };
15572
+ var headerStyle = {
15573
+ padding: "6px 10px",
15574
+ fontSize: 10,
15575
+ fontWeight: 700,
15576
+ textTransform: "uppercase",
15577
+ letterSpacing: 1,
15578
+ color: "#4fc3f7",
15579
+ borderBottom: "1px solid #1a2a3a",
15580
+ flexShrink: 0
15581
+ };
15582
+ var listStyle = {
15583
+ flex: 1,
15584
+ overflowY: "auto",
15585
+ padding: "4px 0"
15586
+ };
15587
+ function entityRowStyle(selected) {
15588
+ return {
15589
+ display: "flex",
15590
+ alignItems: "center",
15591
+ gap: 6,
15592
+ padding: "3px 10px",
15593
+ cursor: "pointer",
15594
+ background: selected ? "#1a3050" : "transparent",
15595
+ borderLeft: selected ? "2px solid #4fc3f7" : "2px solid transparent",
15596
+ color: selected ? "#e0f0ff" : "#a0b4c8",
15597
+ transition: "background 80ms"
15598
+ };
15599
+ }
15600
+ var componentBadgeStyle = {
15601
+ fontSize: 9,
15602
+ background: "#1a2a3a",
15603
+ color: "#607080",
15604
+ borderRadius: 3,
15605
+ padding: "1px 4px",
15606
+ maxWidth: 60,
15607
+ overflow: "hidden",
15608
+ textOverflow: "ellipsis",
15609
+ whiteSpace: "nowrap"
15610
+ };
15611
+ function SceneHierarchy({ entities, selectedId, onSelect, width = 220, style }) {
15612
+ return /* @__PURE__ */ jsxs12("div", { style: { ...panelStyle, width, ...style }, children: [
15613
+ /* @__PURE__ */ jsx23("div", { style: headerStyle, children: "Scene Hierarchy" }),
15614
+ /* @__PURE__ */ jsxs12("div", { style: { padding: "4px 10px", borderBottom: "1px solid #1a2a3a", color: "#4a5a6a", fontSize: 10 }, children: [
15615
+ entities.length,
15616
+ " ",
15617
+ entities.length === 1 ? "entity" : "entities"
15618
+ ] }),
15619
+ /* @__PURE__ */ jsxs12("div", { style: listStyle, children: [
15620
+ entities.map((e) => /* @__PURE__ */ jsx23(
15621
+ "div",
15622
+ {
15623
+ style: entityRowStyle(e.id === selectedId),
15624
+ onClick: () => onSelect(e.id === selectedId ? null : e.id),
15625
+ children: /* @__PURE__ */ jsxs12("div", { style: { flex: 1, overflow: "hidden" }, children: [
15626
+ /* @__PURE__ */ jsx23("div", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: e.name }),
15627
+ /* @__PURE__ */ jsxs12("div", { style: { display: "flex", gap: 3, marginTop: 2, flexWrap: "wrap" }, children: [
15628
+ e.componentTypes.slice(0, 3).map((t) => /* @__PURE__ */ jsx23("span", { style: componentBadgeStyle, children: t }, t)),
15629
+ e.componentTypes.length > 3 && /* @__PURE__ */ jsxs12("span", { style: componentBadgeStyle, children: [
15630
+ "+",
15631
+ e.componentTypes.length - 3
15632
+ ] })
15633
+ ] })
15634
+ ] })
15635
+ },
15636
+ e.id
15637
+ )),
15638
+ entities.length === 0 && /* @__PURE__ */ jsx23("div", { style: { padding: "10px", color: "#3a5060", fontSize: 11, textAlign: "center" }, children: "No entities" })
15639
+ ] })
15640
+ ] });
15641
+ }
15642
+
15643
+ // ../editor/src/components/PropertyField.tsx
15644
+ import { jsx as jsx24, jsxs as jsxs13 } from "react/jsx-runtime";
15645
+ var fieldRow = {
15646
+ display: "flex",
15647
+ alignItems: "center",
15648
+ gap: 6,
15649
+ marginBottom: 4,
15650
+ minHeight: 24
15651
+ };
15652
+ var labelStyle = {
15653
+ flex: "0 0 90px",
15654
+ fontSize: 11,
15655
+ color: "#8899aa",
15656
+ overflow: "hidden",
15657
+ textOverflow: "ellipsis",
15658
+ whiteSpace: "nowrap",
15659
+ fontFamily: "system-ui, sans-serif",
15660
+ textTransform: "uppercase",
15661
+ letterSpacing: 0.5
15662
+ };
15663
+ var inputBase = {
15664
+ flex: 1,
15665
+ background: "#1a2030",
15666
+ border: "1px solid #2a3a50",
15667
+ borderRadius: 3,
15668
+ color: "#d0e0f0",
15669
+ padding: "2px 6px",
15670
+ fontSize: 11,
15671
+ fontFamily: "monospace",
15672
+ outline: "none",
15673
+ minWidth: 0
15674
+ };
15675
+ function NumberField({ label, value, onChange, step = 1 }) {
15676
+ return /* @__PURE__ */ jsxs13("div", { style: fieldRow, children: [
15677
+ /* @__PURE__ */ jsx24("span", { style: labelStyle, title: label, children: label }),
15678
+ /* @__PURE__ */ jsx24(
15679
+ "input",
15680
+ {
15681
+ type: "number",
15682
+ style: inputBase,
15683
+ value: isNaN(value) ? "" : value,
15684
+ step,
15685
+ onChange: (e) => {
15686
+ const v = parseFloat(e.target.value);
15687
+ if (!isNaN(v)) onChange(v);
15688
+ }
15689
+ }
15690
+ )
15691
+ ] });
15692
+ }
15693
+ function TextField({ label, value, onChange }) {
15694
+ return /* @__PURE__ */ jsxs13("div", { style: fieldRow, children: [
15695
+ /* @__PURE__ */ jsx24("span", { style: labelStyle, title: label, children: label }),
15696
+ /* @__PURE__ */ jsx24("input", { type: "text", style: inputBase, value, onChange: (e) => onChange(e.target.value) })
15697
+ ] });
15698
+ }
15699
+ function BoolField({ label, value, onChange }) {
15700
+ return /* @__PURE__ */ jsxs13("div", { style: fieldRow, children: [
15701
+ /* @__PURE__ */ jsx24("span", { style: labelStyle, title: label, children: label }),
15702
+ /* @__PURE__ */ jsx24(
15703
+ "input",
15704
+ {
15705
+ type: "checkbox",
15706
+ checked: value,
15707
+ style: { accentColor: "#4fc3f7", cursor: "pointer" },
15708
+ onChange: (e) => onChange(e.target.checked)
15709
+ }
15710
+ )
15711
+ ] });
15712
+ }
15713
+ function ColorField({ label, value, onChange }) {
15714
+ return /* @__PURE__ */ jsxs13("div", { style: fieldRow, children: [
15715
+ /* @__PURE__ */ jsx24("span", { style: labelStyle, title: label, children: label }),
15716
+ /* @__PURE__ */ jsxs13("div", { style: { display: "flex", gap: 4, flex: 1 }, children: [
15717
+ /* @__PURE__ */ jsx24(
15718
+ "input",
15719
+ {
15720
+ type: "color",
15721
+ value: value.startsWith("#") ? value.slice(0, 7) : "#ffffff",
15722
+ style: { width: 28, height: 22, padding: 1, border: "1px solid #2a3a50", borderRadius: 3, cursor: "pointer" },
15723
+ onChange: (e) => onChange(e.target.value)
15724
+ }
15725
+ ),
15726
+ /* @__PURE__ */ jsx24("input", { type: "text", style: { ...inputBase, flex: 1 }, value, onChange: (e) => onChange(e.target.value) })
15727
+ ] })
15728
+ ] });
15729
+ }
15730
+ function Vec2Field({ label, x, y, onChangeX, onChangeY, step = 1 }) {
15731
+ return /* @__PURE__ */ jsxs13("div", { style: fieldRow, children: [
15732
+ /* @__PURE__ */ jsx24("span", { style: labelStyle, title: label, children: label }),
15733
+ /* @__PURE__ */ jsxs13("div", { style: { display: "flex", gap: 4, flex: 1 }, children: [
15734
+ /* @__PURE__ */ jsx24(
15735
+ "input",
15736
+ {
15737
+ type: "number",
15738
+ style: { ...inputBase, flex: 1 },
15739
+ value: isNaN(x) ? "" : x,
15740
+ step,
15741
+ onChange: (e) => {
15742
+ const v = parseFloat(e.target.value);
15743
+ if (!isNaN(v)) onChangeX(v);
15744
+ }
15745
+ }
15746
+ ),
15747
+ /* @__PURE__ */ jsx24(
15748
+ "input",
15749
+ {
15750
+ type: "number",
15751
+ style: { ...inputBase, flex: 1 },
15752
+ value: isNaN(y) ? "" : y,
15753
+ step,
15754
+ onChange: (e) => {
15755
+ const v = parseFloat(e.target.value);
15756
+ if (!isNaN(v)) onChangeY(v);
15757
+ }
15758
+ }
15759
+ )
15760
+ ] })
15761
+ ] });
15762
+ }
15763
+
15764
+ // ../editor/src/components/EntityInspector.tsx
15765
+ import { Fragment as Fragment10, jsx as jsx25, jsxs as jsxs14 } from "react/jsx-runtime";
15766
+ var panelStyle2 = {
15767
+ background: "#0d1520",
15768
+ borderLeft: "1px solid #1a2a3a",
15769
+ color: "#c0d0e0",
15770
+ fontFamily: "system-ui, sans-serif",
15771
+ fontSize: 12,
15772
+ display: "flex",
15773
+ flexDirection: "column",
15774
+ overflow: "hidden",
15775
+ userSelect: "none"
15776
+ };
15777
+ var headerStyle2 = {
15778
+ padding: "6px 10px",
15779
+ fontSize: 10,
15780
+ fontWeight: 700,
15781
+ textTransform: "uppercase",
15782
+ letterSpacing: 1,
15783
+ color: "#4fc3f7",
15784
+ borderBottom: "1px solid #1a2a3a",
15785
+ flexShrink: 0
15786
+ };
15787
+ var scrollStyle = {
15788
+ flex: 1,
15789
+ overflowY: "auto",
15790
+ padding: "6px 0"
15791
+ };
15792
+ var componentSectionStyle = {
15793
+ marginBottom: 2,
15794
+ borderBottom: "1px solid #131d2a"
15795
+ };
15796
+ var componentHeaderStyle = {
15797
+ display: "flex",
15798
+ alignItems: "center",
15799
+ padding: "4px 10px",
15800
+ fontSize: 10,
15801
+ fontWeight: 700,
15802
+ textTransform: "uppercase",
15803
+ letterSpacing: 0.8,
15804
+ color: "#80a8c0",
15805
+ cursor: "default",
15806
+ background: "#0f1a28"
15807
+ };
15808
+ var fieldsStyle = {
15809
+ padding: "4px 10px 6px"
15810
+ };
15811
+ function isColorKey(key) {
15812
+ const lower = key.toLowerCase();
15813
+ return lower === "color" || lower === "background" || lower === "fill" || lower === "stroke" || lower.includes("color");
15814
+ }
15815
+ function ComponentSection({ comp }) {
15816
+ const compRecord = comp;
15817
+ const entries = Object.entries(comp).filter(([k]) => k !== "type");
15818
+ const rendered = /* @__PURE__ */ new Set();
15819
+ return /* @__PURE__ */ jsxs14("div", { style: componentSectionStyle, children: [
15820
+ /* @__PURE__ */ jsx25("div", { style: componentHeaderStyle, children: comp.type }),
15821
+ /* @__PURE__ */ jsxs14("div", { style: fieldsStyle, children: [
15822
+ entries.map(([key, val]) => {
15823
+ if (rendered.has(key)) return null;
15824
+ if (key === "x" && typeof val === "number" && typeof compRecord["y"] === "number") {
15825
+ rendered.add("x");
15826
+ rendered.add("y");
15827
+ return /* @__PURE__ */ jsx25(
15828
+ Vec2Field,
15829
+ {
15830
+ label: "pos",
15831
+ x: val,
15832
+ y: compRecord["y"],
15833
+ step: 0.5,
15834
+ onChangeX: (v) => {
15835
+ compRecord["x"] = v;
15836
+ },
15837
+ onChangeY: (v) => {
15838
+ compRecord["y"] = v;
15839
+ }
15840
+ },
15841
+ "xy"
15842
+ );
15843
+ }
15844
+ if (key === "followOffsetX" && typeof val === "number") {
15845
+ const oy = compRecord["followOffsetY"];
15846
+ if (typeof oy === "number") {
15847
+ rendered.add("followOffsetX");
15848
+ rendered.add("followOffsetY");
15849
+ return /* @__PURE__ */ jsx25(
15850
+ Vec2Field,
15851
+ {
15852
+ label: "offset",
15853
+ x: val,
15854
+ y: oy,
15855
+ step: 1,
15856
+ onChangeX: (v) => {
15857
+ compRecord["followOffsetX"] = v;
15858
+ },
15859
+ onChangeY: (v) => {
15860
+ compRecord["followOffsetY"] = v;
15861
+ }
15862
+ },
15863
+ "followOffset"
15864
+ );
15865
+ }
15866
+ }
15867
+ if (typeof val === "number") {
15868
+ rendered.add(key);
15869
+ return /* @__PURE__ */ jsx25(
15870
+ NumberField,
15871
+ {
15872
+ label: key,
15873
+ value: val,
15874
+ step: Math.abs(val) < 2 ? 0.01 : 1,
15875
+ onChange: (v) => {
15876
+ compRecord[key] = v;
15877
+ }
15878
+ },
15879
+ key
15880
+ );
15881
+ }
15882
+ if (typeof val === "string" && isColorKey(key)) {
15883
+ rendered.add(key);
15884
+ return /* @__PURE__ */ jsx25(
15885
+ ColorField,
15886
+ {
15887
+ label: key,
15888
+ value: val,
15889
+ onChange: (v) => {
15890
+ compRecord[key] = v;
15891
+ }
15892
+ },
15893
+ key
15894
+ );
15895
+ }
15896
+ if (typeof val === "string") {
15897
+ rendered.add(key);
15898
+ return /* @__PURE__ */ jsx25(
15899
+ TextField,
15900
+ {
15901
+ label: key,
15902
+ value: val,
15903
+ onChange: (v) => {
15904
+ compRecord[key] = v;
15905
+ }
15906
+ },
15907
+ key
15908
+ );
15909
+ }
15910
+ if (typeof val === "boolean") {
15911
+ rendered.add(key);
15912
+ return /* @__PURE__ */ jsx25(
15913
+ BoolField,
15914
+ {
15915
+ label: key,
15916
+ value: val,
15917
+ onChange: (v) => {
15918
+ compRecord[key] = v;
15919
+ }
15920
+ },
15921
+ key
15922
+ );
15923
+ }
15924
+ if (val !== null && val !== void 0 && typeof val !== "function") {
15925
+ rendered.add(key);
15926
+ const preview = typeof val === "object" ? JSON.stringify(val).slice(0, 60) : String(val);
15927
+ return /* @__PURE__ */ jsxs14(
15928
+ "div",
15929
+ {
15930
+ style: {
15931
+ display: "flex",
15932
+ gap: 6,
15933
+ marginBottom: 4,
15934
+ fontSize: 11,
15935
+ alignItems: "flex-start"
15936
+ },
15937
+ children: [
15938
+ /* @__PURE__ */ jsx25(
15939
+ "span",
15940
+ {
15941
+ style: {
15942
+ flex: "0 0 90px",
15943
+ color: "#6a7a8a",
15944
+ textTransform: "uppercase",
15945
+ fontSize: 10,
15946
+ letterSpacing: 0.5,
15947
+ paddingTop: 2
15948
+ },
15949
+ children: key
15950
+ }
15951
+ ),
15952
+ /* @__PURE__ */ jsx25(
15953
+ "span",
15954
+ {
15955
+ style: { flex: 1, color: "#506070", fontFamily: "monospace", wordBreak: "break-all", fontSize: 10 },
15956
+ children: preview
15957
+ }
15958
+ )
15959
+ ]
15960
+ },
15961
+ key
15962
+ );
15963
+ }
15964
+ return null;
15965
+ }),
15966
+ entries.length === 0 && /* @__PURE__ */ jsx25("div", { style: { color: "#3a5060", fontSize: 10 }, children: "No fields" })
15967
+ ] })
15968
+ ] });
15969
+ }
15970
+ function EntityInspector({ entity, components, width = 260, style }) {
15971
+ return /* @__PURE__ */ jsxs14("div", { style: { ...panelStyle2, width, ...style }, children: [
15972
+ /* @__PURE__ */ jsx25("div", { style: headerStyle2, children: "Inspector" }),
15973
+ entity ? /* @__PURE__ */ jsxs14(Fragment10, { children: [
15974
+ /* @__PURE__ */ jsxs14(
15975
+ "div",
15976
+ {
15977
+ style: {
15978
+ padding: "6px 10px",
15979
+ borderBottom: "1px solid #1a2a3a",
15980
+ color: "#d0e0f0",
15981
+ fontSize: 12,
15982
+ fontWeight: 600
15983
+ },
15984
+ children: [
15985
+ entity.name,
15986
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#4a6070", fontSize: 10, marginLeft: 6 }, children: [
15987
+ "#",
15988
+ entity.id
15989
+ ] })
15990
+ ]
15991
+ }
15992
+ ),
15993
+ /* @__PURE__ */ jsxs14("div", { style: scrollStyle, children: [
15994
+ components.map((comp) => /* @__PURE__ */ jsx25(ComponentSection, { comp }, comp.type)),
15995
+ components.length === 0 && /* @__PURE__ */ jsx25("div", { style: { padding: "10px", color: "#3a5060", fontSize: 11, textAlign: "center" }, children: "No components" })
15996
+ ] })
15997
+ ] }) : /* @__PURE__ */ jsx25(
15998
+ "div",
15999
+ {
16000
+ style: {
16001
+ flex: 1,
16002
+ display: "flex",
16003
+ alignItems: "center",
16004
+ justifyContent: "center",
16005
+ color: "#2a4050",
16006
+ fontSize: 11
16007
+ },
16008
+ children: "Select an entity"
16009
+ }
16010
+ )
16011
+ ] });
16012
+ }
16013
+
16014
+ // ../editor/src/hooks/useEditorState.ts
16015
+ import { useState as useState34, useCallback as useCallback38, useContext as useContext81, useEffect as useEffect88, useRef as useRef55 } from "react";
16016
+ function buildEntityInfo(engine) {
16017
+ const nameMap = /* @__PURE__ */ new Map();
16018
+ engine.entityIds.forEach((eid, name) => nameMap.set(eid, name));
16019
+ const ids = engine.ecs.getAllEntityIds();
16020
+ return ids.map((id) => {
16021
+ const comps = engine.ecs.getEntityComponents(id);
16022
+ return {
16023
+ id,
16024
+ name: nameMap.get(id) ?? `Entity #${id}`,
16025
+ componentTypes: comps.map((c) => c.type)
16026
+ };
16027
+ });
16028
+ }
16029
+ function useEditorState(refreshHz = 4) {
16030
+ const engine = useContext81(EngineContext);
16031
+ const [entities, setEntities] = useState34([]);
16032
+ const [selectedId, setSelectedId] = useState34(null);
16033
+ const intervalRef = useRef55(null);
16034
+ const refresh = useCallback38(() => {
16035
+ setEntities(buildEntityInfo(engine));
16036
+ }, [engine]);
16037
+ useEffect88(() => {
16038
+ refresh();
16039
+ intervalRef.current = setInterval(refresh, 1e3 / refreshHz);
16040
+ return () => {
16041
+ if (intervalRef.current !== null) clearInterval(intervalRef.current);
16042
+ };
16043
+ }, [refresh, refreshHz]);
16044
+ const select = useCallback38((id) => {
16045
+ setSelectedId(id);
16046
+ }, []);
16047
+ const selectedComponents = selectedId !== null ? engine.ecs.getEntityComponents(selectedId) : [];
16048
+ return { entities, selectedId, selectedComponents, select, refresh };
16049
+ }
16050
+
16051
+ // ../editor/src/components/EditorShell.tsx
16052
+ import { Fragment as Fragment11, jsx as jsx26, jsxs as jsxs15 } from "react/jsx-runtime";
16053
+ function EditorShell({
16054
+ children,
16055
+ active = true,
16056
+ hierarchyWidth = 220,
16057
+ inspectorWidth = 260,
16058
+ style
16059
+ }) {
16060
+ const state = useEditorState();
16061
+ const selectedEntity = state.entities.find((e) => e.id === state.selectedId) ?? null;
16062
+ if (!active) {
16063
+ return /* @__PURE__ */ jsx26(Fragment11, { children });
16064
+ }
16065
+ const shellStyle = {
16066
+ position: "absolute",
16067
+ inset: 0,
16068
+ display: "flex",
16069
+ flexDirection: "row",
16070
+ pointerEvents: "none",
16071
+ zIndex: 200,
16072
+ ...style
16073
+ };
16074
+ const sideStyle = {
16075
+ pointerEvents: "auto",
16076
+ flexShrink: 0,
16077
+ height: "100%",
16078
+ overflow: "hidden"
16079
+ };
16080
+ return /* @__PURE__ */ jsxs15("div", { style: shellStyle, children: [
16081
+ /* @__PURE__ */ jsx26("div", { style: sideStyle, children: /* @__PURE__ */ jsx26(
16082
+ SceneHierarchy,
16083
+ {
16084
+ entities: state.entities,
16085
+ selectedId: state.selectedId,
16086
+ onSelect: state.select,
16087
+ width: hierarchyWidth,
16088
+ style: { height: "100%" }
16089
+ }
16090
+ ) }),
16091
+ /* @__PURE__ */ jsx26("div", { style: { flex: 1, pointerEvents: "none" }, children }),
16092
+ /* @__PURE__ */ jsx26("div", { style: sideStyle, children: /* @__PURE__ */ jsx26(
16093
+ EntityInspector,
16094
+ {
16095
+ entity: selectedEntity,
16096
+ components: state.selectedComponents,
16097
+ width: inspectorWidth,
16098
+ style: { height: "100%" }
16099
+ }
16100
+ ) })
16101
+ ] });
16102
+ }
15333
16103
  export {
15334
16104
  A11yNode,
15335
16105
  AnimatedSprite,
15336
16106
  Animation,
15337
16107
  Animator,
15338
16108
  AssetLoader,
16109
+ BoolField,
15339
16110
  BoxCollider,
15340
16111
  COLLISION_DYNAMIC_DYNAMIC,
15341
16112
  COLLISION_DYNAMIC_KINEMATIC,
@@ -15351,6 +16122,7 @@ export {
15351
16122
  CircleCollider,
15352
16123
  ClientPrediction,
15353
16124
  CollisionPipeline,
16125
+ ColorField,
15354
16126
  ComboDetector,
15355
16127
  CompoundCollider,
15356
16128
  ConvexCollider,
@@ -15359,7 +16131,9 @@ export {
15359
16131
  DialogueBox,
15360
16132
  Ease,
15361
16133
  EditableText,
16134
+ EditorShell,
15362
16135
  Entity,
16136
+ EntityInspector,
15363
16137
  FocusRing,
15364
16138
  Game,
15365
16139
  Gradient,
@@ -15377,6 +16151,7 @@ export {
15377
16151
  Mask,
15378
16152
  MovingPlatform,
15379
16153
  NineSlice,
16154
+ NumberField,
15380
16155
  PARTICLE_PRESETS,
15381
16156
  ParallaxLayer,
15382
16157
  ParticleEmitter,
@@ -15384,6 +16159,7 @@ export {
15384
16159
  RenderSystem,
15385
16160
  RigidBody,
15386
16161
  Room,
16162
+ SceneHierarchy,
15387
16163
  SceneTransitionOverlay,
15388
16164
  ScreenFlash,
15389
16165
  Script,
@@ -15394,6 +16170,7 @@ export {
15394
16170
  SquashStretch,
15395
16171
  Stage,
15396
16172
  Text,
16173
+ TextField,
15397
16174
  TextureFilter,
15398
16175
  Tilemap,
15399
16176
  Trail,
@@ -15401,6 +16178,7 @@ export {
15401
16178
  TransformHandles,
15402
16179
  TriMeshCollider,
15403
16180
  TriangleCollider,
16181
+ Vec2Field,
15404
16182
  VectorPath,
15405
16183
  VirtualCamera,
15406
16184
  VirtualJoystick,
@@ -15578,6 +16356,8 @@ export {
15578
16356
  useDraggable,
15579
16357
  useDropThrough,
15580
16358
  useDroppable,
16359
+ useDynamicCanvas,
16360
+ useEditorState,
15581
16361
  useEntity,
15582
16362
  useEvent,
15583
16363
  useEvents,
@@ -15596,6 +16376,7 @@ export {
15596
16376
  useHitstop,
15597
16377
  useHoverable,
15598
16378
  useIDBSave,
16379
+ useIdleFrameSkip,
15599
16380
  useInput,
15600
16381
  useInputBuffer,
15601
16382
  useInputContext,