cubeforge 0.3.14 → 0.3.15

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 +196 -11
  2. package/dist/index.js +1087 -308
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/Game.tsx
2
- import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
2
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState2 } from "react";
3
3
 
4
4
  // ../../packages/core/src/ecs/world.ts
5
5
  var ECSWorld = class {
@@ -734,14 +734,95 @@ var ScriptSystem = class {
734
734
  // ../../packages/core/src/tween.ts
735
735
  var Ease = {
736
736
  linear: (t) => t,
737
+ // Quad
737
738
  easeInQuad: (t) => t * t,
738
739
  easeOutQuad: (t) => t * (2 - t),
739
740
  easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
741
+ // Cubic
742
+ easeInCubic: (t) => t * t * t,
743
+ easeOutCubic: (t) => {
744
+ const t1 = t - 1;
745
+ return t1 * t1 * t1 + 1;
746
+ },
747
+ easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
748
+ // Quart
749
+ easeInQuart: (t) => t * t * t * t,
750
+ easeOutQuart: (t) => {
751
+ const t1 = t - 1;
752
+ return 1 - t1 * t1 * t1 * t1;
753
+ },
754
+ easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (t - 1) * (t - 1) * (t - 1) * (t - 1),
755
+ // Quint
756
+ easeInQuint: (t) => t * t * t * t * t,
757
+ easeOutQuint: (t) => {
758
+ const t1 = t - 1;
759
+ return 1 + t1 * t1 * t1 * t1 * t1;
760
+ },
761
+ easeInOutQuint: (t) => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (t - 1) * (t - 1) * (t - 1) * (t - 1) * (t - 1),
762
+ // Sine
763
+ easeInSine: (t) => 1 - Math.cos(t * Math.PI / 2),
764
+ easeOutSine: (t) => Math.sin(t * Math.PI / 2),
765
+ easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
766
+ // Expo
767
+ easeInExpo: (t) => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
768
+ easeOutExpo: (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
769
+ easeInOutExpo: (t) => {
770
+ if (t === 0) return 0;
771
+ if (t === 1) return 1;
772
+ return t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2;
773
+ },
774
+ // Circ
775
+ easeInCirc: (t) => 1 - Math.sqrt(1 - t * t),
776
+ easeOutCirc: (t) => Math.sqrt(1 - (t - 1) * (t - 1)),
777
+ easeInOutCirc: (t) => t < 0.5 ? (1 - Math.sqrt(1 - 4 * t * t)) / 2 : (Math.sqrt(1 - (-2 * t + 2) * (-2 * t + 2)) + 1) / 2,
778
+ // Back
779
+ easeInBack: (t) => {
780
+ const c1 = 1.70158;
781
+ const c3 = c1 + 1;
782
+ return c3 * t * t * t - c1 * t * t;
783
+ },
740
784
  easeOutBack: (t) => {
741
785
  const c1 = 1.70158;
742
786
  const c3 = c1 + 1;
743
787
  return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
744
- }
788
+ },
789
+ easeInOutBack: (t) => {
790
+ const c1 = 1.70158;
791
+ const c2 = c1 * 1.525;
792
+ return t < 0.5 ? 2 * t * (2 * t) * ((c2 + 1) * 2 * t - c2) / 2 : ((2 * t - 2) * (2 * t - 2) * ((c2 + 1) * (2 * t - 2) + c2) + 2) / 2;
793
+ },
794
+ // Elastic
795
+ easeInElastic: (t) => {
796
+ if (t === 0 || t === 1) return t;
797
+ return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * (2 * Math.PI / 3));
798
+ },
799
+ easeOutElastic: (t) => {
800
+ if (t === 0 || t === 1) return t;
801
+ return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1;
802
+ },
803
+ easeInOutElastic: (t) => {
804
+ if (t === 0 || t === 1) return t;
805
+ const c5 = 2 * Math.PI / 4.5;
806
+ return t < 0.5 ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2 : Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5) / 2 + 1;
807
+ },
808
+ // Bounce
809
+ easeOutBounce: (t) => {
810
+ const n1 = 7.5625;
811
+ const d1 = 2.75;
812
+ if (t < 1 / d1) return n1 * t * t;
813
+ if (t < 2 / d1) {
814
+ const t22 = t - 1.5 / d1;
815
+ return n1 * t22 * t22 + 0.75;
816
+ }
817
+ if (t < 2.5 / d1) {
818
+ const t22 = t - 2.25 / d1;
819
+ return n1 * t22 * t22 + 0.9375;
820
+ }
821
+ const t2 = t - 2.625 / d1;
822
+ return n1 * t2 * t2 + 0.984375;
823
+ },
824
+ easeInBounce: (t) => 1 - Ease.easeOutBounce(1 - t),
825
+ easeInOutBounce: (t) => t < 0.5 ? (1 - Ease.easeOutBounce(1 - 2 * t)) / 2 : (1 + Ease.easeOutBounce(2 * t - 1)) / 2
745
826
  };
746
827
  function tween(from, to, duration, ease = Ease.linear, onUpdate, onComplete) {
747
828
  let elapsed = 0;
@@ -767,6 +848,87 @@ function tween(from, to, duration, ease = Ease.linear, onUpdate, onComplete) {
767
848
  };
768
849
  }
769
850
 
851
+ // ../../packages/core/src/tweenTimeline.ts
852
+ function createTimeline() {
853
+ const entries = [];
854
+ let running = false;
855
+ let currentTween = null;
856
+ let delayTimer = null;
857
+ let rafId = null;
858
+ let lastTime = 0;
859
+ function clearCurrent() {
860
+ if (currentTween) {
861
+ currentTween.stop();
862
+ currentTween = null;
863
+ }
864
+ if (delayTimer !== null) {
865
+ clearTimeout(delayTimer);
866
+ delayTimer = null;
867
+ }
868
+ if (rafId !== null) {
869
+ cancelAnimationFrame(rafId);
870
+ rafId = null;
871
+ }
872
+ }
873
+ function tick(now) {
874
+ if (!running || !currentTween) return;
875
+ const dt = (now - lastTime) / 1e3;
876
+ lastTime = now;
877
+ currentTween.update(dt);
878
+ if (!currentTween.isComplete) {
879
+ rafId = requestAnimationFrame(tick);
880
+ }
881
+ }
882
+ function playEntry(index) {
883
+ if (index >= entries.length) {
884
+ running = false;
885
+ return;
886
+ }
887
+ const entry = entries[index];
888
+ const delay = entry.delay ?? 0;
889
+ const startTween = () => {
890
+ if (!running) return;
891
+ currentTween = tween(
892
+ entry.from,
893
+ entry.to,
894
+ entry.duration,
895
+ entry.ease ?? Ease.linear,
896
+ entry.onUpdate,
897
+ () => {
898
+ entry.onComplete?.();
899
+ playEntry(index + 1);
900
+ }
901
+ );
902
+ lastTime = performance.now();
903
+ rafId = requestAnimationFrame(tick);
904
+ };
905
+ if (delay > 0) {
906
+ delayTimer = setTimeout(startTween, delay * 1e3);
907
+ } else {
908
+ startTween();
909
+ }
910
+ }
911
+ const timeline = {
912
+ add(entry) {
913
+ entries.push(entry);
914
+ return timeline;
915
+ },
916
+ start() {
917
+ clearCurrent();
918
+ running = true;
919
+ playEntry(0);
920
+ },
921
+ stop() {
922
+ running = false;
923
+ clearCurrent();
924
+ },
925
+ isRunning() {
926
+ return running;
927
+ }
928
+ };
929
+ return timeline;
930
+ }
931
+
770
932
  // ../../packages/core/src/timer.ts
771
933
  function createTimer(duration, onComplete, autoStart = false) {
772
934
  let _duration = duration;
@@ -831,6 +993,48 @@ function hotReloadPlugin(engine, oldPlugin, newPlugin, oldSystems) {
831
993
  newPlugin.onInit?.(engine);
832
994
  }
833
995
 
996
+ // ../../packages/core/src/tilemapMerge.ts
997
+ function mergeTileColliders(solidGrid, tileWidth, tileHeight, originX, originY) {
998
+ const rows = solidGrid.length;
999
+ if (rows === 0) return [];
1000
+ const cols = solidGrid[0].length;
1001
+ const visited = [];
1002
+ for (let r = 0; r < rows; r++) {
1003
+ visited[r] = new Array(cols).fill(false);
1004
+ }
1005
+ const rects = [];
1006
+ for (let row = 0; row < rows; row++) {
1007
+ for (let col = 0; col < cols; col++) {
1008
+ if (!solidGrid[row][col] || visited[row][col]) continue;
1009
+ let maxCol = col;
1010
+ while (maxCol + 1 < cols && solidGrid[row][maxCol + 1] && !visited[row][maxCol + 1]) {
1011
+ maxCol++;
1012
+ }
1013
+ let maxRow = row;
1014
+ outer:
1015
+ for (let r = row + 1; r < rows; r++) {
1016
+ for (let c = col; c <= maxCol; c++) {
1017
+ if (!solidGrid[r][c] || visited[r][c]) break outer;
1018
+ }
1019
+ maxRow = r;
1020
+ }
1021
+ for (let r = row; r <= maxRow; r++) {
1022
+ for (let c = col; c <= maxCol; c++) {
1023
+ visited[r][c] = true;
1024
+ }
1025
+ }
1026
+ const spanCols = maxCol - col + 1;
1027
+ const spanRows = maxRow - row + 1;
1028
+ const width = spanCols * tileWidth;
1029
+ const height = spanRows * tileHeight;
1030
+ const x = originX + col * tileWidth + width / 2;
1031
+ const y = originY + row * tileHeight + height / 2;
1032
+ rects.push({ x, y, width, height });
1033
+ }
1034
+ }
1035
+ return rects;
1036
+ }
1037
+
834
1038
  // ../../packages/input/src/keyboard.ts
835
1039
  var Keyboard = class {
836
1040
  held = /* @__PURE__ */ new Set();
@@ -1156,13 +1360,36 @@ function createSprite(opts) {
1156
1360
  zIndex: 0,
1157
1361
  visible: true,
1158
1362
  flipX: false,
1363
+ flipY: false,
1159
1364
  anchorX: 0.5,
1160
1365
  anchorY: 0.5,
1161
1366
  frameIndex: 0,
1367
+ blendMode: "normal",
1368
+ layer: "default",
1162
1369
  ...opts
1163
1370
  };
1164
1371
  }
1165
1372
 
1373
+ // ../../packages/renderer/src/renderLayers.ts
1374
+ var defaultLayers = [
1375
+ { name: "background", order: -100 },
1376
+ { name: "default", order: 0 },
1377
+ { name: "foreground", order: 100 },
1378
+ { name: "ui", order: 200 }
1379
+ ];
1380
+ function createRenderLayerManager(layers = defaultLayers) {
1381
+ const map = /* @__PURE__ */ new Map();
1382
+ for (const l of layers) map.set(l.name, l.order);
1383
+ return {
1384
+ addLayer(name, order) {
1385
+ map.set(name, order);
1386
+ },
1387
+ getOrder(name) {
1388
+ return map.get(name) ?? 0;
1389
+ }
1390
+ };
1391
+ }
1392
+
1166
1393
  // ../../packages/renderer/src/textureFilter.ts
1167
1394
  var TextureFilter = {
1168
1395
  /** Nearest-neighbor — sharp pixels, ideal for pixel art */
@@ -1269,8 +1496,9 @@ layout(location = 4) in float i_rot;
1269
1496
  layout(location = 5) in vec2 i_anchor;
1270
1497
  layout(location = 6) in vec2 i_offset;
1271
1498
  layout(location = 7) in float i_flipX;
1272
- layout(location = 8) in vec4 i_color;
1273
- layout(location = 9) in vec4 i_uvRect;
1499
+ layout(location = 8) in float i_flipY;
1500
+ layout(location = 9) in vec4 i_color;
1501
+ layout(location = 10) in vec4 i_uvRect;
1274
1502
 
1275
1503
  uniform vec2 u_camPos;
1276
1504
  uniform float u_zoom;
@@ -1286,6 +1514,8 @@ void main() {
1286
1514
 
1287
1515
  // Horizontal flip
1288
1516
  if (i_flipX > 0.5) local.x = -local.x;
1517
+ // Vertical flip
1518
+ if (i_flipY > 0.5) local.y = -local.y;
1289
1519
 
1290
1520
  // Rotate around local origin
1291
1521
  float c = cos(i_rot);
@@ -1397,9 +1627,9 @@ function parseCSSColor(css) {
1397
1627
  }
1398
1628
 
1399
1629
  // ../../packages/renderer/src/webglRenderSystem.ts
1400
- var FLOATS_PER_INSTANCE = 18;
1401
- var MAX_INSTANCES = 8192;
1402
- var MAX_SPRITE_TEXTURES = 512;
1630
+ var FLOATS_PER_INSTANCE = 19;
1631
+ var MAX_INSTANCES = 16384;
1632
+ var MAX_SPRITE_TEXTURES = 1024;
1403
1633
  var MAX_TEXT_CACHE = 200;
1404
1634
  function compileShader(gl, type, src) {
1405
1635
  const shader = gl.createShader(type);
@@ -1531,8 +1761,9 @@ var RenderSystem = class {
1531
1761
  addAttr(5, 2);
1532
1762
  addAttr(6, 2);
1533
1763
  addAttr(7, 1);
1534
- addAttr(8, 4);
1764
+ addAttr(8, 1);
1535
1765
  addAttr(9, 4);
1766
+ addAttr(10, 4);
1536
1767
  gl.bindVertexArray(null);
1537
1768
  gl.useProgram(this.program);
1538
1769
  this.uCamPos = gl.getUniformLocation(this.program, "u_camPos");
@@ -1604,6 +1835,8 @@ var RenderSystem = class {
1604
1835
  textureCache = /* @__PURE__ */ new Map();
1605
1836
  /** Insertion-order key list for LRU-style eviction. */
1606
1837
  textureCacheKeys = [];
1838
+ // ── Render layer manager ────────────────────────────────────────────────
1839
+ layers = createRenderLayerManager();
1607
1840
  // ── Texture sampling ────────────────────────────────────────────────────
1608
1841
  _defaultSampling = DEFAULT_SAMPLING;
1609
1842
  /** Set the global default texture sampling mode for all sprites that don't specify their own. */
@@ -1768,10 +2001,29 @@ var RenderSystem = class {
1768
2001
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, toGLMinFilter(gl, min));
1769
2002
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, toGLMagFilter(gl, mag));
1770
2003
  }
2004
+ // ── Blend mode helper ────────────────────────────────────────────────────
2005
+ applyBlendMode(mode) {
2006
+ const { gl } = this;
2007
+ switch (mode) {
2008
+ case "additive":
2009
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
2010
+ break;
2011
+ case "multiply":
2012
+ gl.blendFunc(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA);
2013
+ break;
2014
+ case "screen":
2015
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR);
2016
+ break;
2017
+ default:
2018
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
2019
+ break;
2020
+ }
2021
+ }
1771
2022
  // ── Instanced draw call ────────────────────────────────────────────────────
1772
- flush(count, textureKey, sampling) {
2023
+ flush(count, textureKey, sampling, blendMode) {
1773
2024
  if (count === 0) return;
1774
2025
  const { gl } = this;
2026
+ if (blendMode && blendMode !== "normal") this.applyBlendMode(blendMode);
1775
2027
  const isColor = textureKey.startsWith("__color__");
1776
2028
  const tex = isColor ? this.whiteTexture : this.loadTexture(textureKey);
1777
2029
  gl.bindTexture(gl.TEXTURE_2D, tex);
@@ -1781,6 +2033,7 @@ var RenderSystem = class {
1781
2033
  gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
1782
2034
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.instanceData, 0, count * FLOATS_PER_INSTANCE);
1783
2035
  gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
2036
+ if (blendMode && blendMode !== "normal") this.applyBlendMode("normal");
1784
2037
  }
1785
2038
  flushWithTex(count, tex, useTexture) {
1786
2039
  if (count === 0) return;
@@ -1793,7 +2046,7 @@ var RenderSystem = class {
1793
2046
  gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
1794
2047
  }
1795
2048
  // ── Write one sprite instance into instanceData ───────────────────────────
1796
- writeInstance(base, x, y, w, h, rot, anchorX, anchorY, offsetX, offsetY, flipX, r, g, b, a, u, v, uw, vh) {
2049
+ writeInstance(base, x, y, w, h, rot, anchorX, anchorY, offsetX, offsetY, flipX, flipY, r, g, b, a, u, v, uw, vh) {
1797
2050
  const d = this.instanceData;
1798
2051
  d[base + 0] = x;
1799
2052
  d[base + 1] = y;
@@ -1805,14 +2058,15 @@ var RenderSystem = class {
1805
2058
  d[base + 7] = offsetX;
1806
2059
  d[base + 8] = offsetY;
1807
2060
  d[base + 9] = flipX ? 1 : 0;
1808
- d[base + 10] = r;
1809
- d[base + 11] = g;
1810
- d[base + 12] = b;
1811
- d[base + 13] = a;
1812
- d[base + 14] = u;
1813
- d[base + 15] = v;
1814
- d[base + 16] = uw;
1815
- d[base + 17] = vh;
2061
+ d[base + 10] = flipY ? 1 : 0;
2062
+ d[base + 11] = r;
2063
+ d[base + 12] = g;
2064
+ d[base + 13] = b;
2065
+ d[base + 14] = a;
2066
+ d[base + 15] = u;
2067
+ d[base + 16] = v;
2068
+ d[base + 17] = uw;
2069
+ d[base + 18] = vh;
1816
2070
  }
1817
2071
  // ── Main update loop ───────────────────────────────────────────────────────
1818
2072
  update(world, dt) {
@@ -1947,6 +2201,8 @@ var RenderSystem = class {
1947
2201
  renderables.sort((a, b) => {
1948
2202
  const sa = world.getComponent(a, "Sprite");
1949
2203
  const sb = world.getComponent(b, "Sprite");
2204
+ const ld = this.layers.getOrder(sa.layer) - this.layers.getOrder(sb.layer);
2205
+ if (ld !== 0) return ld;
1950
2206
  const zd = sa.zIndex - sb.zIndex;
1951
2207
  if (zd !== 0) return zd;
1952
2208
  const ka = getTextureKey(sa), kb = getTextureKey(sb);
@@ -1955,9 +2211,10 @@ var RenderSystem = class {
1955
2211
  let batchCount = 0;
1956
2212
  let batchKey = "";
1957
2213
  let batchSampling;
2214
+ let batchBlendMode = "normal";
1958
2215
  for (let i = 0; i <= renderables.length; i++) {
1959
2216
  if (i === renderables.length) {
1960
- this.flush(batchCount, batchKey, batchSampling);
2217
+ this.flush(batchCount, batchKey, batchSampling, batchBlendMode);
1961
2218
  break;
1962
2219
  }
1963
2220
  const id = renderables[i];
@@ -2006,12 +2263,14 @@ var RenderSystem = class {
2006
2263
  sprite.image = img;
2007
2264
  }
2008
2265
  const key = getTextureKey(sprite);
2009
- if (key !== batchKey && batchCount > 0 || batchCount >= MAX_INSTANCES) {
2010
- this.flush(batchCount, batchKey, batchSampling);
2266
+ const spriteBlend = sprite.blendMode ?? "normal";
2267
+ if ((key !== batchKey || spriteBlend !== batchBlendMode) && batchCount > 0 || batchCount >= MAX_INSTANCES) {
2268
+ this.flush(batchCount, batchKey, batchSampling, batchBlendMode);
2011
2269
  batchCount = 0;
2012
2270
  }
2013
2271
  batchKey = key;
2014
2272
  batchSampling = sprite.sampling;
2273
+ batchBlendMode = spriteBlend;
2015
2274
  const ss = world.getComponent(id, "SquashStretch");
2016
2275
  const scaleXMod = ss ? ss.currentScaleX : 1;
2017
2276
  const scaleYMod = ss ? ss.currentScaleY : 1;
@@ -2030,6 +2289,7 @@ var RenderSystem = class {
2030
2289
  sprite.offsetX,
2031
2290
  sprite.offsetY,
2032
2291
  sprite.flipX,
2292
+ sprite.flipY ?? false,
2033
2293
  r,
2034
2294
  g,
2035
2295
  b,
@@ -2053,10 +2313,11 @@ var RenderSystem = class {
2053
2313
  if (!text.visible) continue;
2054
2314
  const entry = this.getOrCreateTextTexture(text);
2055
2315
  if (!entry) continue;
2056
- this.flush(batchCount, batchKey, batchSampling);
2316
+ this.flush(batchCount, batchKey, batchSampling, batchBlendMode);
2057
2317
  batchCount = 0;
2058
2318
  batchKey = "";
2059
2319
  batchSampling = void 0;
2320
+ batchBlendMode = "normal";
2060
2321
  this.writeInstance(
2061
2322
  0,
2062
2323
  transform.x + text.offsetX,
@@ -2070,6 +2331,9 @@ var RenderSystem = class {
2070
2331
  0,
2071
2332
  0,
2072
2333
  false,
2334
+ // flipX
2335
+ false,
2336
+ // flipY
2073
2337
  1,
2074
2338
  1,
2075
2339
  1,
@@ -2093,15 +2357,33 @@ var RenderSystem = class {
2093
2357
  return p.life > 0;
2094
2358
  });
2095
2359
  if (pool.active && pool.particles.length < pool.maxParticles) {
2096
- pool.timer += dt;
2097
- const spawnCount = Math.floor(pool.timer * pool.rate);
2098
- pool.timer -= spawnCount / pool.rate;
2360
+ let spawnCount;
2361
+ if (pool.burstCount != null && pool.burstCount > 0) {
2362
+ spawnCount = pool.burstCount;
2363
+ pool.active = false;
2364
+ } else {
2365
+ pool.timer += dt;
2366
+ spawnCount = Math.floor(pool.timer * pool.rate);
2367
+ pool.timer -= spawnCount / pool.rate;
2368
+ }
2099
2369
  for (let i = 0; i < spawnCount && pool.particles.length < pool.maxParticles; i++) {
2100
2370
  const angle = pool.angle + (world.rng() - 0.5) * pool.spread;
2101
2371
  const speed = pool.speed * (0.5 + world.rng() * 0.5);
2372
+ let ox = 0;
2373
+ let oy = 0;
2374
+ const shape = pool.emitShape ?? "point";
2375
+ if (shape === "circle") {
2376
+ const r = (pool.emitRadius ?? 0) * Math.sqrt(world.rng());
2377
+ const a = world.rng() * Math.PI * 2;
2378
+ ox = Math.cos(a) * r;
2379
+ oy = Math.sin(a) * r;
2380
+ } else if (shape === "box") {
2381
+ ox = (world.rng() - 0.5) * (pool.emitWidth ?? 0);
2382
+ oy = (world.rng() - 0.5) * (pool.emitHeight ?? 0);
2383
+ }
2102
2384
  pool.particles.push({
2103
- x: t.x,
2104
- y: t.y,
2385
+ x: t.x + ox,
2386
+ y: t.y + oy,
2105
2387
  vx: Math.cos(angle) * speed,
2106
2388
  vy: Math.sin(angle) * speed,
2107
2389
  life: pool.particleLife,
@@ -2133,6 +2415,7 @@ var RenderSystem = class {
2133
2415
  0,
2134
2416
  0,
2135
2417
  false,
2418
+ false,
2136
2419
  r,
2137
2420
  g,
2138
2421
  b,
@@ -2173,6 +2456,7 @@ var RenderSystem = class {
2173
2456
  0,
2174
2457
  0,
2175
2458
  false,
2459
+ false,
2176
2460
  tr,
2177
2461
  tg,
2178
2462
  tb,
@@ -2210,6 +2494,7 @@ var RenderSystem = class {
2210
2494
  0,
2211
2495
  0,
2212
2496
  false,
2497
+ false,
2213
2498
  walkable ? 0 : 1,
2214
2499
  walkable ? 1 : 0,
2215
2500
  0,
@@ -2243,6 +2528,7 @@ var RenderSystem = class {
2243
2528
  0,
2244
2529
  0,
2245
2530
  false,
2531
+ false,
2246
2532
  1,
2247
2533
  0.3,
2248
2534
  0.3,
@@ -2292,6 +2578,72 @@ var Canvas2DRenderer = class {
2292
2578
  }
2293
2579
  };
2294
2580
 
2581
+ // ../../packages/renderer/src/postProcess.ts
2582
+ function createPostProcessStack() {
2583
+ const effects = [];
2584
+ return {
2585
+ add(effect) {
2586
+ if (!effects.includes(effect)) {
2587
+ effects.push(effect);
2588
+ }
2589
+ },
2590
+ remove(effect) {
2591
+ const idx = effects.indexOf(effect);
2592
+ if (idx !== -1) effects.splice(idx, 1);
2593
+ },
2594
+ apply(ctx, width, height, dt) {
2595
+ for (const effect of effects) {
2596
+ ctx.save();
2597
+ effect(ctx, width, height, dt);
2598
+ ctx.restore();
2599
+ }
2600
+ },
2601
+ clear() {
2602
+ effects.length = 0;
2603
+ }
2604
+ };
2605
+ }
2606
+ function vignetteEffect(intensity = 0.4) {
2607
+ return (ctx, width, height) => {
2608
+ const cx = width / 2;
2609
+ const cy = height / 2;
2610
+ const radius = Math.sqrt(cx * cx + cy * cy);
2611
+ const gradient = ctx.createRadialGradient(cx, cy, radius * 0.3, cx, cy, radius);
2612
+ gradient.addColorStop(0, "rgba(0,0,0,0)");
2613
+ gradient.addColorStop(1, `rgba(0,0,0,${intensity})`);
2614
+ ctx.fillStyle = gradient;
2615
+ ctx.fillRect(0, 0, width, height);
2616
+ };
2617
+ }
2618
+ function scanlineEffect(gap = 3, opacity = 0.15) {
2619
+ return (ctx, width, height) => {
2620
+ ctx.fillStyle = `rgba(0,0,0,${opacity})`;
2621
+ for (let y = 0; y < height; y += gap) {
2622
+ ctx.fillRect(0, y, width, 1);
2623
+ }
2624
+ };
2625
+ }
2626
+ function chromaticAberrationEffect(offset = 2) {
2627
+ return (ctx, width, height) => {
2628
+ if (width === 0 || height === 0) return;
2629
+ const imageData = ctx.getImageData(0, 0, width, height);
2630
+ const { data } = imageData;
2631
+ const copy = new Uint8ClampedArray(data);
2632
+ for (let y = 0; y < height; y++) {
2633
+ for (let x = 0; x < width; x++) {
2634
+ const i = (y * width + x) * 4;
2635
+ const srcR = Math.min(x + offset, width - 1);
2636
+ const iR = (y * width + srcR) * 4;
2637
+ data[i] = copy[iR];
2638
+ const srcB = Math.max(x - offset, 0);
2639
+ const iB = (y * width + srcB) * 4;
2640
+ data[i + 2] = copy[iB + 2];
2641
+ }
2642
+ }
2643
+ ctx.putImageData(imageData, 0, 0);
2644
+ };
2645
+ }
2646
+
2295
2647
  // ../../packages/physics/src/components/rigidbody.ts
2296
2648
  function createRigidBody(opts) {
2297
2649
  return {
@@ -2310,6 +2662,9 @@ function createRigidBody(opts) {
2310
2662
  isKinematic: false,
2311
2663
  dropThrough: 0,
2312
2664
  ccd: false,
2665
+ angularVelocity: 0,
2666
+ angularDamping: 0,
2667
+ linearDamping: 0,
2313
2668
  ...opts
2314
2669
  };
2315
2670
  }
@@ -2644,6 +2999,15 @@ var PhysicsSystem = class {
2644
2999
  if (!rb.lockY) rb.vy += this.gravity * rb.gravityScale * dt;
2645
3000
  if (rb.lockX) rb.vx = 0;
2646
3001
  if (rb.lockY) rb.vy = 0;
3002
+ if (rb.linearDamping > 0) {
3003
+ rb.vx *= 1 - rb.linearDamping;
3004
+ rb.vy *= 1 - rb.linearDamping;
3005
+ }
3006
+ if (rb.angularVelocity !== 0) {
3007
+ const transform = world.getComponent(id, "Transform");
3008
+ transform.rotation += rb.angularVelocity * dt;
3009
+ if (rb.angularDamping > 0) rb.angularVelocity *= 1 - rb.angularDamping;
3010
+ }
2647
3011
  if (rb.dropThrough > 0) rb.dropThrough--;
2648
3012
  }
2649
3013
  const ccdPrev = /* @__PURE__ */ new Map();
@@ -2662,6 +3026,15 @@ var PhysicsSystem = class {
2662
3026
  if (!rb.lockY) rb.vy += this.gravity * rb.gravityScale * dt;
2663
3027
  if (rb.lockX) rb.vx = 0;
2664
3028
  if (rb.lockY) rb.vy = 0;
3029
+ if (rb.linearDamping > 0) {
3030
+ rb.vx *= 1 - rb.linearDamping;
3031
+ rb.vy *= 1 - rb.linearDamping;
3032
+ }
3033
+ if (rb.angularVelocity !== 0) {
3034
+ const transform = world.getComponent(id, "Transform");
3035
+ transform.rotation += rb.angularVelocity * dt;
3036
+ if (rb.angularDamping > 0) rb.angularVelocity *= 1 - rb.angularDamping;
3037
+ }
2665
3038
  if (rb.dropThrough > 0) rb.dropThrough--;
2666
3039
  }
2667
3040
  for (const id of dynamics) {
@@ -3543,10 +3916,48 @@ function useCircleStay(handler, opts) {
3543
3916
  useContactEvent("circleStay", handler, opts);
3544
3917
  }
3545
3918
 
3919
+ // ../context/src/useCollidingWith.ts
3920
+ import { useContext as useContext2, useEffect as useEffect2, useRef as useRef2 } from "react";
3921
+ function useCollidingWith() {
3922
+ const engine = useContext2(EngineContext);
3923
+ const entityId = useContext2(EntityContext);
3924
+ if (!engine) throw new Error("useCollidingWith hook must be used inside <Game>");
3925
+ if (entityId === null) throw new Error("useCollidingWith hook must be used inside <Entity>");
3926
+ const setRef = useRef2(/* @__PURE__ */ new Set());
3927
+ useEffect2(() => {
3928
+ const set = setRef.current;
3929
+ function handleEnter({ a, b }) {
3930
+ const isA = a === entityId;
3931
+ const isB = b === entityId;
3932
+ if (!isA && !isB) return;
3933
+ set.add(isA ? b : a);
3934
+ }
3935
+ function handleExit({ a, b }) {
3936
+ const isA = a === entityId;
3937
+ const isB = b === entityId;
3938
+ if (!isA && !isB) return;
3939
+ set.delete(isA ? b : a);
3940
+ }
3941
+ const unsubs = [
3942
+ engine.events.on("collisionEnter", handleEnter),
3943
+ engine.events.on("collisionExit", handleExit),
3944
+ engine.events.on("triggerEnter", handleEnter),
3945
+ engine.events.on("triggerExit", handleExit),
3946
+ engine.events.on("circleEnter", handleEnter),
3947
+ engine.events.on("circleExit", handleExit)
3948
+ ];
3949
+ return () => {
3950
+ unsubs.forEach((unsub) => unsub());
3951
+ set.clear();
3952
+ };
3953
+ }, [engine.events, entityId]);
3954
+ return setRef.current;
3955
+ }
3956
+
3546
3957
  // ../devtools/src/DevTools.tsx
3547
3958
  import React from "react";
3548
3959
  import { createPortal } from "react-dom";
3549
- import { useState, useEffect as useEffect2, useCallback, useRef as useRef2 } from "react";
3960
+ import { useState, useEffect as useEffect3, useCallback, useRef as useRef3 } from "react";
3550
3961
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3551
3962
  var MAX_DEVTOOLS_FRAMES = 600;
3552
3963
  var C = {
@@ -3710,8 +4121,8 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
3710
4121
  const [contactLog, setContactLog] = useState([]);
3711
4122
  const [showNavGrid, setShowNavGrid] = useState(false);
3712
4123
  const [showContactFlash, setShowContactFlash] = useState(false);
3713
- const frameRef = useRef2(0);
3714
- useEffect2(() => {
4124
+ const frameRef = useRef3(0);
4125
+ useEffect3(() => {
3715
4126
  handle.onFrame = () => {
3716
4127
  frameRef.current++;
3717
4128
  if (!paused) {
@@ -3723,7 +4134,7 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
3723
4134
  handle.onFrame = void 0;
3724
4135
  };
3725
4136
  }, [handle, paused]);
3726
- useEffect2(() => {
4137
+ useEffect3(() => {
3727
4138
  if (!engine) return;
3728
4139
  const events = engine.events;
3729
4140
  const types = ["triggerEnter", "triggerExit", "collisionEnter", "collisionExit", "circleEnter", "circleExit"];
@@ -4234,13 +4645,13 @@ function Game({
4234
4645
  className,
4235
4646
  children
4236
4647
  }) {
4237
- const canvasRef = useRef3(null);
4238
- const debugCanvasRef = useRef3(null);
4239
- const wrapperRef = useRef3(null);
4648
+ const canvasRef = useRef4(null);
4649
+ const debugCanvasRef = useRef4(null);
4650
+ const wrapperRef = useRef4(null);
4240
4651
  const [engine, setEngine] = useState2(null);
4241
4652
  const [assetsReady, setAssetsReady] = useState2(asyncAssets);
4242
- const devtoolsHandle = useRef3({ buffer: [] });
4243
- useEffect3(() => {
4653
+ const devtoolsHandle = useRef4({ buffer: [] });
4654
+ useEffect4(() => {
4244
4655
  const canvas = canvasRef.current;
4245
4656
  const ecs = new ECSWorld();
4246
4657
  if (deterministic) ecs.setDeterministicSeed(seed);
@@ -4283,6 +4694,7 @@ function Game({
4283
4694
  handle.onFrame?.();
4284
4695
  }
4285
4696
  }, deterministic ? { fixedDt: 1 / 60 } : void 0);
4697
+ const postProcessStack = createPostProcessStack();
4286
4698
  const state = {
4287
4699
  ecs,
4288
4700
  input,
@@ -4293,7 +4705,8 @@ function Game({
4293
4705
  loop,
4294
4706
  canvas,
4295
4707
  entityIds,
4296
- systemTimings
4708
+ systemTimings,
4709
+ postProcessStack
4297
4710
  };
4298
4711
  setEngine(state);
4299
4712
  if (plugins) {
@@ -4355,7 +4768,7 @@ function Game({
4355
4768
  }
4356
4769
  };
4357
4770
  }, []);
4358
- useEffect3(() => {
4771
+ useEffect4(() => {
4359
4772
  if (!engine) return;
4360
4773
  let cancelled = false;
4361
4774
  if (asyncAssets) {
@@ -4373,13 +4786,13 @@ function Game({
4373
4786
  cancelled = true;
4374
4787
  };
4375
4788
  }, [engine]);
4376
- useEffect3(() => {
4789
+ useEffect4(() => {
4377
4790
  if (!engine) return;
4378
4791
  const canvas = engine.canvas;
4379
4792
  if (canvas.width !== width) canvas.width = width;
4380
4793
  if (canvas.height !== height) canvas.height = height;
4381
4794
  }, [width, height, engine]);
4382
- useEffect3(() => {
4795
+ useEffect4(() => {
4383
4796
  engine?.physics.setGravity(gravity);
4384
4797
  }, [gravity, engine]);
4385
4798
  const canvasStyle = {
@@ -4460,15 +4873,15 @@ function Game({
4460
4873
  }
4461
4874
 
4462
4875
  // src/components/World.tsx
4463
- import { useEffect as useEffect4, useContext as useContext2 } from "react";
4876
+ import { useEffect as useEffect5, useContext as useContext3 } from "react";
4464
4877
  import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
4465
4878
  function World({ gravity, background = "#1a1a2e", children }) {
4466
- const engine = useContext2(EngineContext);
4467
- useEffect4(() => {
4879
+ const engine = useContext3(EngineContext);
4880
+ useEffect5(() => {
4468
4881
  if (!engine) return;
4469
4882
  if (gravity !== void 0) engine.physics.setGravity(gravity);
4470
4883
  }, [gravity, engine]);
4471
- useEffect4(() => {
4884
+ useEffect5(() => {
4472
4885
  if (!engine) return;
4473
4886
  const camId = engine.ecs.queryOne("Camera2D");
4474
4887
  if (camId !== void 0) {
@@ -4482,12 +4895,17 @@ function World({ gravity, background = "#1a1a2e", children }) {
4482
4895
  }
4483
4896
 
4484
4897
  // src/components/Entity.tsx
4485
- import { useEffect as useEffect5, useContext as useContext3, useState as useState3 } from "react";
4898
+ import { useEffect as useEffect6, useContext as useContext4, useState as useState3 } from "react";
4486
4899
  import { jsx as jsx4 } from "react/jsx-runtime";
4487
4900
  function Entity({ id, tags = [], children }) {
4488
- const engine = useContext3(EngineContext);
4901
+ const engine = useContext4(EngineContext);
4489
4902
  const [entityId, setEntityId] = useState3(null);
4490
- useEffect5(() => {
4903
+ if (process.env.NODE_ENV !== "production") {
4904
+ if (!engine) {
4905
+ console.warn("[Cubeforge] <Entity> must be inside a <World>. No EngineContext found.");
4906
+ }
4907
+ }
4908
+ useEffect6(() => {
4491
4909
  const eid = engine.ecs.createEntity();
4492
4910
  if (id) {
4493
4911
  if (engine.entityIds.has(id)) {
@@ -4507,15 +4925,15 @@ function Entity({ id, tags = [], children }) {
4507
4925
  }
4508
4926
 
4509
4927
  // src/components/Transform.tsx
4510
- import { useEffect as useEffect6, useContext as useContext4 } from "react";
4928
+ import { useEffect as useEffect7, useContext as useContext5 } from "react";
4511
4929
  function Transform({ x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1 }) {
4512
- const engine = useContext4(EngineContext);
4513
- const entityId = useContext4(EntityContext);
4514
- useEffect6(() => {
4930
+ const engine = useContext5(EngineContext);
4931
+ const entityId = useContext5(EntityContext);
4932
+ useEffect7(() => {
4515
4933
  engine.ecs.addComponent(entityId, createTransform(x, y, rotation, scaleX, scaleY));
4516
4934
  return () => engine.ecs.removeComponent(entityId, "Transform");
4517
4935
  }, []);
4518
- useEffect6(() => {
4936
+ useEffect7(() => {
4519
4937
  const comp = engine.ecs.getComponent(entityId, "Transform");
4520
4938
  if (comp) {
4521
4939
  comp.x = x;
@@ -4529,7 +4947,7 @@ function Transform({ x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1 }) {
4529
4947
  }
4530
4948
 
4531
4949
  // src/components/Sprite.tsx
4532
- import { useEffect as useEffect7, useContext as useContext5 } from "react";
4950
+ import { useEffect as useEffect8, useContext as useContext6 } from "react";
4533
4951
  function Sprite({
4534
4952
  width,
4535
4953
  height,
@@ -4540,6 +4958,7 @@ function Sprite({
4540
4958
  zIndex = 0,
4541
4959
  visible = true,
4542
4960
  flipX = false,
4961
+ flipY = false,
4543
4962
  anchorX = 0.5,
4544
4963
  anchorY = 0.5,
4545
4964
  frameIndex = 0,
@@ -4552,12 +4971,22 @@ function Sprite({
4552
4971
  tileY,
4553
4972
  tileSizeX,
4554
4973
  tileSizeY,
4555
- sampling
4974
+ sampling,
4975
+ blendMode = "normal",
4976
+ layer = "default"
4556
4977
  }) {
4557
4978
  const resolvedFrameIndex = atlas && frame != null ? atlas[frame] ?? 0 : frameIndex;
4558
- const engine = useContext5(EngineContext);
4559
- const entityId = useContext5(EntityContext);
4560
- useEffect7(() => {
4979
+ const engine = useContext6(EngineContext);
4980
+ const entityId = useContext6(EntityContext);
4981
+ if (process.env.NODE_ENV !== "production") {
4982
+ if (entityId === null) {
4983
+ console.warn("[Cubeforge] <Sprite> must be inside an <Entity>. No EntityContext found.");
4984
+ }
4985
+ if ((frameWidth != null || frameHeight != null || frameColumns != null) && !src) {
4986
+ console.warn("[Cubeforge] <Sprite> has frameWidth/frameHeight/frameColumns but no `src`. Sprite-sheet props require an image source.");
4987
+ }
4988
+ }
4989
+ useEffect8(() => {
4561
4990
  const comp = createSprite({
4562
4991
  width,
4563
4992
  height,
@@ -4568,6 +4997,7 @@ function Sprite({
4568
4997
  zIndex,
4569
4998
  visible,
4570
4999
  flipX,
5000
+ flipY,
4571
5001
  anchorX,
4572
5002
  anchorY,
4573
5003
  frameIndex: resolvedFrameIndex,
@@ -4578,7 +5008,9 @@ function Sprite({
4578
5008
  tileY,
4579
5009
  tileSizeX,
4580
5010
  tileSizeY,
4581
- sampling
5011
+ sampling,
5012
+ blendMode,
5013
+ layer
4582
5014
  });
4583
5015
  engine.ecs.addComponent(entityId, comp);
4584
5016
  if (src) {
@@ -4592,20 +5024,23 @@ function Sprite({
4592
5024
  }
4593
5025
  return () => engine.ecs.removeComponent(entityId, "Sprite");
4594
5026
  }, []);
4595
- useEffect7(() => {
5027
+ useEffect8(() => {
4596
5028
  const comp = engine.ecs.getComponent(entityId, "Sprite");
4597
5029
  if (!comp) return;
4598
5030
  comp.color = color;
4599
5031
  comp.visible = visible;
4600
5032
  comp.flipX = flipX;
5033
+ comp.flipY = flipY;
4601
5034
  comp.zIndex = zIndex;
4602
5035
  comp.frameIndex = resolvedFrameIndex;
4603
- }, [color, visible, flipX, zIndex, resolvedFrameIndex, engine, entityId]);
5036
+ comp.blendMode = blendMode;
5037
+ comp.layer = layer;
5038
+ }, [color, visible, flipX, flipY, zIndex, resolvedFrameIndex, blendMode, layer, engine, entityId]);
4604
5039
  return null;
4605
5040
  }
4606
5041
 
4607
5042
  // src/components/Text.tsx
4608
- import { useEffect as useEffect8, useContext as useContext6 } from "react";
5043
+ import { useEffect as useEffect9, useContext as useContext7 } from "react";
4609
5044
  function Text({
4610
5045
  text,
4611
5046
  fontSize = 16,
@@ -4619,9 +5054,9 @@ function Text({
4619
5054
  offsetX = 0,
4620
5055
  offsetY = 0
4621
5056
  }) {
4622
- const engine = useContext6(EngineContext);
4623
- const entityId = useContext6(EntityContext);
4624
- useEffect8(() => {
5057
+ const engine = useContext7(EngineContext);
5058
+ const entityId = useContext7(EntityContext);
5059
+ useEffect9(() => {
4625
5060
  const comp = {
4626
5061
  type: "Text",
4627
5062
  text,
@@ -4639,7 +5074,7 @@ function Text({
4639
5074
  engine.ecs.addComponent(entityId, comp);
4640
5075
  return () => engine.ecs.removeComponent(entityId, "Text");
4641
5076
  }, []);
4642
- useEffect8(() => {
5077
+ useEffect9(() => {
4643
5078
  const comp = engine.ecs.getComponent(entityId, "Text");
4644
5079
  if (!comp) return;
4645
5080
  comp.text = text;
@@ -4651,7 +5086,7 @@ function Text({
4651
5086
  }
4652
5087
 
4653
5088
  // src/components/RigidBody.tsx
4654
- import { useEffect as useEffect9, useContext as useContext7 } from "react";
5089
+ import { useEffect as useEffect10, useContext as useContext8 } from "react";
4655
5090
  function RigidBody({
4656
5091
  mass = 1,
4657
5092
  gravityScale = 1,
@@ -4662,19 +5097,27 @@ function RigidBody({
4662
5097
  vy = 0,
4663
5098
  lockX = false,
4664
5099
  lockY = false,
4665
- ccd = false
5100
+ ccd = false,
5101
+ angularVelocity = 0,
5102
+ angularDamping = 0,
5103
+ linearDamping = 0
4666
5104
  }) {
4667
- const engine = useContext7(EngineContext);
4668
- const entityId = useContext7(EntityContext);
4669
- useEffect9(() => {
4670
- engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY, ccd }));
5105
+ const engine = useContext8(EngineContext);
5106
+ const entityId = useContext8(EntityContext);
5107
+ if (process.env.NODE_ENV !== "production") {
5108
+ if (entityId === null) {
5109
+ console.warn("[Cubeforge] <RigidBody> must be inside an <Entity>. No EntityContext found.");
5110
+ }
5111
+ }
5112
+ useEffect10(() => {
5113
+ engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY, ccd, angularVelocity, angularDamping, linearDamping }));
4671
5114
  return () => engine.ecs.removeComponent(entityId, "RigidBody");
4672
5115
  }, []);
4673
5116
  return null;
4674
5117
  }
4675
5118
 
4676
5119
  // src/components/BoxCollider.tsx
4677
- import { useEffect as useEffect10, useContext as useContext8 } from "react";
5120
+ import { useEffect as useEffect11, useContext as useContext9 } from "react";
4678
5121
  function BoxCollider({
4679
5122
  width,
4680
5123
  height,
@@ -4685,13 +5128,18 @@ function BoxCollider({
4685
5128
  mask = "*",
4686
5129
  oneWay = false
4687
5130
  }) {
4688
- const engine = useContext8(EngineContext);
4689
- const entityId = useContext8(EntityContext);
4690
- useEffect10(() => {
5131
+ const engine = useContext9(EngineContext);
5132
+ const entityId = useContext9(EntityContext);
5133
+ useEffect11(() => {
4691
5134
  engine.ecs.addComponent(entityId, createBoxCollider(width, height, { offsetX, offsetY, isTrigger, layer, mask, oneWay }));
4692
5135
  const checkId = setTimeout(() => {
4693
- if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "Transform")) {
4694
- console.warn(`[Cubeforge] BoxCollider on entity ${entityId} has no Transform. Physics requires Transform.`);
5136
+ if (process.env.NODE_ENV !== "production") {
5137
+ if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "Transform")) {
5138
+ console.warn(`[Cubeforge] BoxCollider on entity ${entityId} has no Transform. Physics requires Transform.`);
5139
+ }
5140
+ if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "RigidBody")) {
5141
+ console.warn(`[Cubeforge] BoxCollider on entity ${entityId} has no RigidBody. Add a <RigidBody> sibling for physics to work.`);
5142
+ }
4695
5143
  }
4696
5144
  }, 0);
4697
5145
  return () => {
@@ -4703,7 +5151,7 @@ function BoxCollider({
4703
5151
  }
4704
5152
 
4705
5153
  // src/components/CircleCollider.tsx
4706
- import { useEffect as useEffect11, useContext as useContext9 } from "react";
5154
+ import { useEffect as useEffect12, useContext as useContext10 } from "react";
4707
5155
  function CircleCollider({
4708
5156
  radius,
4709
5157
  offsetX = 0,
@@ -4712,9 +5160,9 @@ function CircleCollider({
4712
5160
  layer = "default",
4713
5161
  mask = "*"
4714
5162
  }) {
4715
- const engine = useContext9(EngineContext);
4716
- const entityId = useContext9(EntityContext);
4717
- useEffect11(() => {
5163
+ const engine = useContext10(EngineContext);
5164
+ const entityId = useContext10(EntityContext);
5165
+ useEffect12(() => {
4718
5166
  engine.ecs.addComponent(entityId, createCircleCollider(radius, { offsetX, offsetY, isTrigger, layer, mask }));
4719
5167
  return () => engine.ecs.removeComponent(entityId, "CircleCollider");
4720
5168
  }, []);
@@ -4722,7 +5170,7 @@ function CircleCollider({
4722
5170
  }
4723
5171
 
4724
5172
  // src/components/CapsuleCollider.tsx
4725
- import { useEffect as useEffect12, useContext as useContext10 } from "react";
5173
+ import { useEffect as useEffect13, useContext as useContext11 } from "react";
4726
5174
  function CapsuleCollider({
4727
5175
  width,
4728
5176
  height,
@@ -4732,9 +5180,9 @@ function CapsuleCollider({
4732
5180
  layer = "default",
4733
5181
  mask = "*"
4734
5182
  }) {
4735
- const engine = useContext10(EngineContext);
4736
- const entityId = useContext10(EntityContext);
4737
- useEffect12(() => {
5183
+ const engine = useContext11(EngineContext);
5184
+ const entityId = useContext11(EntityContext);
5185
+ useEffect13(() => {
4738
5186
  engine.ecs.addComponent(entityId, createCapsuleCollider(width, height, { offsetX, offsetY, isTrigger, layer, mask }));
4739
5187
  return () => engine.ecs.removeComponent(entityId, "CapsuleCollider");
4740
5188
  }, []);
@@ -4742,16 +5190,16 @@ function CapsuleCollider({
4742
5190
  }
4743
5191
 
4744
5192
  // src/components/CompoundCollider.tsx
4745
- import { useEffect as useEffect13, useContext as useContext11 } from "react";
5193
+ import { useEffect as useEffect14, useContext as useContext12 } from "react";
4746
5194
  function CompoundCollider({
4747
5195
  shapes,
4748
5196
  isTrigger = false,
4749
5197
  layer = "default",
4750
5198
  mask = "*"
4751
5199
  }) {
4752
- const engine = useContext11(EngineContext);
4753
- const entityId = useContext11(EntityContext);
4754
- useEffect13(() => {
5200
+ const engine = useContext12(EngineContext);
5201
+ const entityId = useContext12(EntityContext);
5202
+ useEffect14(() => {
4755
5203
  engine.ecs.addComponent(entityId, createCompoundCollider(shapes, { isTrigger, layer, mask }));
4756
5204
  const checkId = setTimeout(() => {
4757
5205
  if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "Transform")) {
@@ -4767,15 +5215,20 @@ function CompoundCollider({
4767
5215
  }
4768
5216
 
4769
5217
  // src/components/Script.tsx
4770
- import { useEffect as useEffect14, useContext as useContext12, useRef as useRef4 } from "react";
5218
+ import { useEffect as useEffect15, useContext as useContext13, useRef as useRef5 } from "react";
4771
5219
  function Script({ init, update }) {
4772
- const engine = useContext12(EngineContext);
4773
- const entityId = useContext12(EntityContext);
4774
- const initRef = useRef4(init);
5220
+ const engine = useContext13(EngineContext);
5221
+ const entityId = useContext13(EntityContext);
5222
+ if (process.env.NODE_ENV !== "production") {
5223
+ if (entityId === null) {
5224
+ console.warn("[Cubeforge] <Script> must be inside an <Entity>. No EntityContext found.");
5225
+ }
5226
+ }
5227
+ const initRef = useRef5(init);
4775
5228
  initRef.current = init;
4776
- const updateRef = useRef4(update);
5229
+ const updateRef = useRef5(update);
4777
5230
  updateRef.current = update;
4778
- useEffect14(() => {
5231
+ useEffect15(() => {
4779
5232
  if (initRef.current) {
4780
5233
  try {
4781
5234
  initRef.current(entityId, engine.ecs);
@@ -4791,7 +5244,7 @@ function Script({ init, update }) {
4791
5244
  }
4792
5245
 
4793
5246
  // src/components/Camera2D.tsx
4794
- import { useEffect as useEffect15, useContext as useContext13 } from "react";
5247
+ import { useEffect as useEffect16, useContext as useContext14 } from "react";
4795
5248
  function Camera2D({
4796
5249
  followEntity,
4797
5250
  x = 0,
@@ -4804,8 +5257,8 @@ function Camera2D({
4804
5257
  followOffsetX = 0,
4805
5258
  followOffsetY = 0
4806
5259
  }) {
4807
- const engine = useContext13(EngineContext);
4808
- useEffect15(() => {
5260
+ const engine = useContext14(EngineContext);
5261
+ useEffect16(() => {
4809
5262
  const entityId = engine.ecs.createEntity();
4810
5263
  engine.ecs.addComponent(entityId, createCamera2D({
4811
5264
  followEntityId: followEntity,
@@ -4821,7 +5274,7 @@ function Camera2D({
4821
5274
  }));
4822
5275
  return () => engine.ecs.destroyEntity(entityId);
4823
5276
  }, []);
4824
- useEffect15(() => {
5277
+ useEffect16(() => {
4825
5278
  const camId = engine.ecs.queryOne("Camera2D");
4826
5279
  if (camId === void 0) return;
4827
5280
  const cam = engine.ecs.getComponent(camId, "Camera2D");
@@ -4840,11 +5293,11 @@ function Camera2D({
4840
5293
  }
4841
5294
 
4842
5295
  // src/components/Animation.tsx
4843
- import { useEffect as useEffect16, useContext as useContext14 } from "react";
5296
+ import { useEffect as useEffect17, useContext as useContext15 } from "react";
4844
5297
  function Animation({ frames, fps = 12, loop = true, playing = true, onComplete, frameEvents }) {
4845
- const engine = useContext14(EngineContext);
4846
- const entityId = useContext14(EntityContext);
4847
- useEffect16(() => {
5298
+ const engine = useContext15(EngineContext);
5299
+ const entityId = useContext15(EntityContext);
5300
+ useEffect17(() => {
4848
5301
  const state = {
4849
5302
  type: "AnimationState",
4850
5303
  frames,
@@ -4862,7 +5315,7 @@ function Animation({ frames, fps = 12, loop = true, playing = true, onComplete,
4862
5315
  engine.ecs.removeComponent(entityId, "AnimationState");
4863
5316
  };
4864
5317
  }, []);
4865
- useEffect16(() => {
5318
+ useEffect17(() => {
4866
5319
  const anim = engine.ecs.getComponent(entityId, "AnimationState");
4867
5320
  if (!anim) return;
4868
5321
  const wasFramesChanged = anim.frames !== frames;
@@ -4881,12 +5334,104 @@ function Animation({ frames, fps = 12, loop = true, playing = true, onComplete,
4881
5334
  return null;
4882
5335
  }
4883
5336
 
5337
+ // src/components/AnimatedSprite.tsx
5338
+ import { useMemo } from "react";
5339
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
5340
+ function defineAnimations(clips) {
5341
+ return clips;
5342
+ }
5343
+ function AnimatedSprite(props) {
5344
+ const {
5345
+ width,
5346
+ height,
5347
+ src,
5348
+ color,
5349
+ offsetX,
5350
+ offsetY,
5351
+ zIndex,
5352
+ visible,
5353
+ flipX,
5354
+ flipY,
5355
+ anchorX,
5356
+ anchorY,
5357
+ frameWidth,
5358
+ frameHeight,
5359
+ frameColumns,
5360
+ atlas,
5361
+ frame,
5362
+ tileX,
5363
+ tileY,
5364
+ tileSizeX,
5365
+ tileSizeY,
5366
+ sampling,
5367
+ blendMode
5368
+ } = props;
5369
+ const animProps = useResolvedAnimation(props);
5370
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
5371
+ /* @__PURE__ */ jsx5(
5372
+ Sprite,
5373
+ {
5374
+ width,
5375
+ height,
5376
+ src,
5377
+ color,
5378
+ offsetX,
5379
+ offsetY,
5380
+ zIndex,
5381
+ visible,
5382
+ flipX,
5383
+ flipY,
5384
+ anchorX,
5385
+ anchorY,
5386
+ frameWidth,
5387
+ frameHeight,
5388
+ frameColumns,
5389
+ atlas,
5390
+ frame,
5391
+ tileX,
5392
+ tileY,
5393
+ tileSizeX,
5394
+ tileSizeY,
5395
+ sampling,
5396
+ blendMode
5397
+ }
5398
+ ),
5399
+ /* @__PURE__ */ jsx5(Animation, { ...animProps })
5400
+ ] });
5401
+ }
5402
+ function useResolvedAnimation(props) {
5403
+ const clip = props.animations ? props.animations[props.current] ?? Object.values(props.animations)[0] : null;
5404
+ const frames = useMemo(
5405
+ () => clip ? clip.frames : props.frames,
5406
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5407
+ [clip ? props.current : props.frames]
5408
+ );
5409
+ if (clip) {
5410
+ return {
5411
+ frames,
5412
+ fps: clip.fps,
5413
+ loop: clip.loop,
5414
+ playing: true,
5415
+ onComplete: clip.onComplete,
5416
+ frameEvents: void 0
5417
+ };
5418
+ }
5419
+ return {
5420
+ frames,
5421
+ fps: props.fps,
5422
+ loop: props.loop,
5423
+ playing: props.playing,
5424
+ onComplete: props.onComplete,
5425
+ frameEvents: props.frameEvents
5426
+ };
5427
+ }
5428
+
4884
5429
  // src/components/SquashStretch.tsx
4885
- import { useEffect as useEffect17, useContext as useContext15 } from "react";
5430
+ import { useEffect as useEffect18, useContext as useContext16 } from "react";
4886
5431
  function SquashStretch({ intensity = 0.2, recovery = 8 }) {
4887
- const engine = useContext15(EngineContext);
4888
- const entityId = useContext15(EntityContext);
4889
- useEffect17(() => {
5432
+ const engine = useContext16(EngineContext);
5433
+ const entityId = useContext16(EntityContext);
5434
+ useEffect18(() => {
4890
5435
  engine.ecs.addComponent(entityId, {
4891
5436
  type: "SquashStretch",
4892
5437
  intensity,
@@ -4900,7 +5445,7 @@ function SquashStretch({ intensity = 0.2, recovery = 8 }) {
4900
5445
  }
4901
5446
 
4902
5447
  // src/components/ParticleEmitter.tsx
4903
- import { useEffect as useEffect18, useContext as useContext16 } from "react";
5448
+ import { useEffect as useEffect19, useContext as useContext17 } from "react";
4904
5449
 
4905
5450
  // src/components/particlePresets.ts
4906
5451
  var PARTICLE_PRESETS = {
@@ -4973,7 +5518,12 @@ function ParticleEmitter({
4973
5518
  particleSize,
4974
5519
  color,
4975
5520
  gravity,
4976
- maxParticles
5521
+ maxParticles,
5522
+ burstCount,
5523
+ emitShape,
5524
+ emitRadius,
5525
+ emitWidth,
5526
+ emitHeight
4977
5527
  }) {
4978
5528
  const presetConfig = preset ? PARTICLE_PRESETS[preset] : {};
4979
5529
  const resolvedRate = rate ?? presetConfig.rate ?? 20;
@@ -4985,9 +5535,9 @@ function ParticleEmitter({
4985
5535
  const resolvedColor = color ?? presetConfig.color ?? "#ffffff";
4986
5536
  const resolvedGravity = gravity ?? presetConfig.gravity ?? 200;
4987
5537
  const resolvedMaxParticles = maxParticles ?? presetConfig.maxParticles ?? 100;
4988
- const engine = useContext16(EngineContext);
4989
- const entityId = useContext16(EntityContext);
4990
- useEffect18(() => {
5538
+ const engine = useContext17(EngineContext);
5539
+ const entityId = useContext17(EntityContext);
5540
+ useEffect19(() => {
4991
5541
  engine.ecs.addComponent(entityId, {
4992
5542
  type: "ParticlePool",
4993
5543
  particles: [],
@@ -5001,11 +5551,16 @@ function ParticleEmitter({
5001
5551
  particleLife: resolvedParticleLife,
5002
5552
  particleSize: resolvedParticleSize,
5003
5553
  color: resolvedColor,
5004
- gravity: resolvedGravity
5554
+ gravity: resolvedGravity,
5555
+ burstCount,
5556
+ emitShape,
5557
+ emitRadius,
5558
+ emitWidth,
5559
+ emitHeight
5005
5560
  });
5006
5561
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
5007
5562
  }, []);
5008
- useEffect18(() => {
5563
+ useEffect19(() => {
5009
5564
  const pool = engine.ecs.getComponent(entityId, "ParticlePool");
5010
5565
  if (!pool) return;
5011
5566
  pool.active = active;
@@ -5014,7 +5569,7 @@ function ParticleEmitter({
5014
5569
  }
5015
5570
 
5016
5571
  // src/components/VirtualJoystick.tsx
5017
- import { useRef as useRef5 } from "react";
5572
+ import { useRef as useRef6 } from "react";
5018
5573
 
5019
5574
  // src/hooks/useVirtualInput.ts
5020
5575
  var _axes = { x: 0, y: 0 };
@@ -5039,7 +5594,7 @@ function useVirtualInput() {
5039
5594
  }
5040
5595
 
5041
5596
  // src/components/VirtualJoystick.tsx
5042
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
5597
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
5043
5598
  function VirtualJoystick({
5044
5599
  size = 120,
5045
5600
  position = "left",
@@ -5048,10 +5603,10 @@ function VirtualJoystick({
5048
5603
  actionLabel = "A",
5049
5604
  actionName = "action"
5050
5605
  }) {
5051
- const baseRef = useRef5(null);
5052
- const stickRef = useRef5(null);
5053
- const activePtr = useRef5(null);
5054
- const baseCenterRef = useRef5({ x: 0, y: 0 });
5606
+ const baseRef = useRef6(null);
5607
+ const stickRef = useRef6(null);
5608
+ const activePtr = useRef6(null);
5609
+ const baseCenterRef = useRef6({ x: 0, y: 0 });
5055
5610
  const radius = size / 2 - 16;
5056
5611
  const applyStickPosition = (dx, dy) => {
5057
5612
  if (!stickRef.current) return;
@@ -5096,8 +5651,8 @@ function VirtualJoystick({
5096
5651
  };
5097
5652
  const cornerStyle = position === "left" ? { left: 24, bottom: 24 } : { right: 24, bottom: 24 };
5098
5653
  const actionCorner = position === "left" ? { right: 24, bottom: 24 } : { left: 24, bottom: 24 };
5099
- return /* @__PURE__ */ jsxs3(Fragment3, { children: [
5100
- /* @__PURE__ */ jsx5(
5654
+ return /* @__PURE__ */ jsxs4(Fragment4, { children: [
5655
+ /* @__PURE__ */ jsx6(
5101
5656
  "div",
5102
5657
  {
5103
5658
  onPointerDown: handlePointerDown,
@@ -5118,7 +5673,7 @@ function VirtualJoystick({
5118
5673
  ...style
5119
5674
  },
5120
5675
  ref: baseRef,
5121
- children: /* @__PURE__ */ jsx5(
5676
+ children: /* @__PURE__ */ jsx6(
5122
5677
  "div",
5123
5678
  {
5124
5679
  ref: stickRef,
@@ -5138,7 +5693,7 @@ function VirtualJoystick({
5138
5693
  )
5139
5694
  }
5140
5695
  ),
5141
- actionButton && /* @__PURE__ */ jsx5(
5696
+ actionButton && /* @__PURE__ */ jsx6(
5142
5697
  "div",
5143
5698
  {
5144
5699
  onPointerDown: (e) => {
@@ -5173,7 +5728,7 @@ function VirtualJoystick({
5173
5728
  }
5174
5729
 
5175
5730
  // src/components/MovingPlatform.tsx
5176
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
5731
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
5177
5732
  var platformPhases = /* @__PURE__ */ new Map();
5178
5733
  function MovingPlatform({
5179
5734
  x1,
@@ -5185,12 +5740,12 @@ function MovingPlatform({
5185
5740
  duration = 3,
5186
5741
  color = "#37474f"
5187
5742
  }) {
5188
- return /* @__PURE__ */ jsxs4(Entity, { children: [
5189
- /* @__PURE__ */ jsx6(Transform, { x: x1, y: y1 }),
5190
- /* @__PURE__ */ jsx6(Sprite, { width, height, color, zIndex: 5 }),
5191
- /* @__PURE__ */ jsx6(RigidBody, { isStatic: true }),
5192
- /* @__PURE__ */ jsx6(BoxCollider, { width, height }),
5193
- /* @__PURE__ */ jsx6(
5743
+ return /* @__PURE__ */ jsxs5(Entity, { children: [
5744
+ /* @__PURE__ */ jsx7(Transform, { x: x1, y: y1 }),
5745
+ /* @__PURE__ */ jsx7(Sprite, { width, height, color, zIndex: 5 }),
5746
+ /* @__PURE__ */ jsx7(RigidBody, { isStatic: true }),
5747
+ /* @__PURE__ */ jsx7(BoxCollider, { width, height }),
5748
+ /* @__PURE__ */ jsx7(
5194
5749
  Script,
5195
5750
  {
5196
5751
  init: () => {
@@ -5212,7 +5767,7 @@ function MovingPlatform({
5212
5767
 
5213
5768
  // src/components/Checkpoint.tsx
5214
5769
  import { useState as useState4 } from "react";
5215
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
5770
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
5216
5771
  function CheckpointActivator({ onActivate }) {
5217
5772
  const [used, setUsed] = useState4(false);
5218
5773
  useTriggerEnter(() => {
@@ -5230,17 +5785,17 @@ function Checkpoint({
5230
5785
  color = "#ffd54f",
5231
5786
  onActivate
5232
5787
  }) {
5233
- return /* @__PURE__ */ jsxs5(Entity, { tags: ["checkpoint"], children: [
5234
- /* @__PURE__ */ jsx7(Transform, { x, y }),
5235
- /* @__PURE__ */ jsx7(Sprite, { width, height, color, zIndex: 5 }),
5236
- /* @__PURE__ */ jsx7(BoxCollider, { width, height, isTrigger: true }),
5237
- /* @__PURE__ */ jsx7(CheckpointActivator, { onActivate })
5788
+ return /* @__PURE__ */ jsxs6(Entity, { tags: ["checkpoint"], children: [
5789
+ /* @__PURE__ */ jsx8(Transform, { x, y }),
5790
+ /* @__PURE__ */ jsx8(Sprite, { width, height, color, zIndex: 5 }),
5791
+ /* @__PURE__ */ jsx8(BoxCollider, { width, height, isTrigger: true }),
5792
+ /* @__PURE__ */ jsx8(CheckpointActivator, { onActivate })
5238
5793
  ] });
5239
5794
  }
5240
5795
 
5241
5796
  // src/components/Tilemap.tsx
5242
- import { useEffect as useEffect19, useState as useState5, useContext as useContext17 } from "react";
5243
- import { Fragment as Fragment4, jsx as jsx8 } from "react/jsx-runtime";
5797
+ import { useEffect as useEffect20, useState as useState5, useContext as useContext18 } from "react";
5798
+ import { Fragment as Fragment5, jsx as jsx9 } from "react/jsx-runtime";
5244
5799
  var animatedTiles = /* @__PURE__ */ new Map();
5245
5800
  function getProperty(props, name) {
5246
5801
  return props?.find((p) => p.name === name)?.value;
@@ -5262,11 +5817,12 @@ function Tilemap({
5262
5817
  collisionLayer = "collision",
5263
5818
  triggerLayer: triggerLayerName = "triggers",
5264
5819
  onTileProperty,
5265
- navGrid
5820
+ navGrid,
5821
+ mergeColliders = true
5266
5822
  }) {
5267
- const engine = useContext17(EngineContext);
5823
+ const engine = useContext18(EngineContext);
5268
5824
  const [spawnedNodes, setSpawnedNodes] = useState5([]);
5269
- useEffect19(() => {
5825
+ useEffect20(() => {
5270
5826
  if (!engine) return;
5271
5827
  const createdEntities = [];
5272
5828
  async function load() {
@@ -5334,32 +5890,54 @@ function Tilemap({
5334
5890
  }
5335
5891
  }
5336
5892
  if (collision || trigger) {
5337
- for (let row = 0; row < mapData.height; row++) {
5338
- let col = 0;
5339
- while (col < mapData.width) {
5340
- const i = row * mapData.width + col;
5341
- const gid = layer.data[i];
5342
- if (gid === 0) {
5343
- col++;
5344
- continue;
5893
+ if (mergeColliders) {
5894
+ const solidGrid = [];
5895
+ for (let row = 0; row < mapData.height; row++) {
5896
+ solidGrid[row] = [];
5897
+ for (let col = 0; col < mapData.width; col++) {
5898
+ solidGrid[row][col] = layer.data[row * mapData.width + col] !== 0;
5345
5899
  }
5346
- let runLength = 1;
5347
- while (col + runLength < mapData.width && layer.data[row * mapData.width + col + runLength] !== 0) {
5348
- runLength++;
5349
- }
5350
- const runWidth = runLength * tilewidth;
5351
- const x = col * tilewidth + runWidth / 2;
5352
- const y = row * tileheight + tileheight / 2;
5900
+ }
5901
+ const merged = mergeTileColliders(solidGrid, tilewidth, tileheight, 0, 0);
5902
+ for (const rect of merged) {
5353
5903
  const eid = engine.ecs.createEntity();
5354
5904
  createdEntities.push(eid);
5355
- engine.ecs.addComponent(eid, createTransform(x, y));
5905
+ engine.ecs.addComponent(eid, createTransform(rect.x, rect.y));
5356
5906
  if (collision) {
5357
5907
  engine.ecs.addComponent(eid, createRigidBody({ isStatic: true }));
5358
- engine.ecs.addComponent(eid, createBoxCollider(runWidth, tileheight));
5908
+ engine.ecs.addComponent(eid, createBoxCollider(rect.width, rect.height));
5359
5909
  } else {
5360
- engine.ecs.addComponent(eid, createBoxCollider(runWidth, tileheight, { isTrigger: true }));
5910
+ engine.ecs.addComponent(eid, createBoxCollider(rect.width, rect.height, { isTrigger: true }));
5911
+ }
5912
+ }
5913
+ } else {
5914
+ for (let row = 0; row < mapData.height; row++) {
5915
+ let col = 0;
5916
+ while (col < mapData.width) {
5917
+ const i = row * mapData.width + col;
5918
+ const gid = layer.data[i];
5919
+ if (gid === 0) {
5920
+ col++;
5921
+ continue;
5922
+ }
5923
+ let runLength = 1;
5924
+ while (col + runLength < mapData.width && layer.data[row * mapData.width + col + runLength] !== 0) {
5925
+ runLength++;
5926
+ }
5927
+ const runWidth = runLength * tilewidth;
5928
+ const x = col * tilewidth + runWidth / 2;
5929
+ const y = row * tileheight + tileheight / 2;
5930
+ const eid = engine.ecs.createEntity();
5931
+ createdEntities.push(eid);
5932
+ engine.ecs.addComponent(eid, createTransform(x, y));
5933
+ if (collision) {
5934
+ engine.ecs.addComponent(eid, createRigidBody({ isStatic: true }));
5935
+ engine.ecs.addComponent(eid, createBoxCollider(runWidth, tileheight));
5936
+ } else {
5937
+ engine.ecs.addComponent(eid, createBoxCollider(runWidth, tileheight, { isTrigger: true }));
5938
+ }
5939
+ col += runLength;
5361
5940
  }
5362
- col += runLength;
5363
5941
  }
5364
5942
  }
5365
5943
  } else {
@@ -5453,12 +6031,12 @@ function Tilemap({
5453
6031
  };
5454
6032
  }, [src]);
5455
6033
  if (spawnedNodes.length === 0) return null;
5456
- return /* @__PURE__ */ jsx8(Fragment4, { children: spawnedNodes });
6034
+ return /* @__PURE__ */ jsx9(Fragment5, { children: spawnedNodes });
5457
6035
  }
5458
6036
 
5459
6037
  // src/components/ParallaxLayer.tsx
5460
- import { useEffect as useEffect20, useContext as useContext18 } from "react";
5461
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
6038
+ import { useEffect as useEffect21, useContext as useContext19 } from "react";
6039
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
5462
6040
  function ParallaxLayerInner({
5463
6041
  src,
5464
6042
  speedX,
@@ -5469,9 +6047,9 @@ function ParallaxLayerInner({
5469
6047
  offsetX,
5470
6048
  offsetY
5471
6049
  }) {
5472
- const engine = useContext18(EngineContext);
5473
- const entityId = useContext18(EntityContext);
5474
- useEffect20(() => {
6050
+ const engine = useContext19(EngineContext);
6051
+ const entityId = useContext19(EntityContext);
6052
+ useEffect21(() => {
5475
6053
  engine.ecs.addComponent(entityId, {
5476
6054
  type: "ParallaxLayer",
5477
6055
  src,
@@ -5487,7 +6065,7 @@ function ParallaxLayerInner({
5487
6065
  });
5488
6066
  return () => engine.ecs.removeComponent(entityId, "ParallaxLayer");
5489
6067
  }, []);
5490
- useEffect20(() => {
6068
+ useEffect21(() => {
5491
6069
  const layer = engine.ecs.getComponent(entityId, "ParallaxLayer");
5492
6070
  if (!layer) return;
5493
6071
  layer.src = src;
@@ -5511,9 +6089,9 @@ function ParallaxLayer({
5511
6089
  offsetX = 0,
5512
6090
  offsetY = 0
5513
6091
  }) {
5514
- return /* @__PURE__ */ jsxs6(Entity, { children: [
5515
- /* @__PURE__ */ jsx9(Transform, { x: 0, y: 0 }),
5516
- /* @__PURE__ */ jsx9(
6092
+ return /* @__PURE__ */ jsxs7(Entity, { children: [
6093
+ /* @__PURE__ */ jsx10(Transform, { x: 0, y: 0 }),
6094
+ /* @__PURE__ */ jsx10(
5517
6095
  ParallaxLayerInner,
5518
6096
  {
5519
6097
  src,
@@ -5530,10 +6108,10 @@ function ParallaxLayer({
5530
6108
  }
5531
6109
 
5532
6110
  // src/components/ScreenFlash.tsx
5533
- import { forwardRef, useImperativeHandle, useRef as useRef6 } from "react";
5534
- import { jsx as jsx10 } from "react/jsx-runtime";
6111
+ import { forwardRef, useImperativeHandle, useRef as useRef7 } from "react";
6112
+ import { jsx as jsx11 } from "react/jsx-runtime";
5535
6113
  var ScreenFlash = forwardRef((_, ref) => {
5536
- const divRef = useRef6(null);
6114
+ const divRef = useRef7(null);
5537
6115
  useImperativeHandle(ref, () => ({
5538
6116
  flash(color, duration) {
5539
6117
  const el = divRef.current;
@@ -5551,7 +6129,7 @@ var ScreenFlash = forwardRef((_, ref) => {
5551
6129
  });
5552
6130
  }
5553
6131
  }));
5554
- return /* @__PURE__ */ jsx10(
6132
+ return /* @__PURE__ */ jsx11(
5555
6133
  "div",
5556
6134
  {
5557
6135
  ref: divRef,
@@ -5569,8 +6147,8 @@ var ScreenFlash = forwardRef((_, ref) => {
5569
6147
  ScreenFlash.displayName = "ScreenFlash";
5570
6148
 
5571
6149
  // src/components/CameraZone.tsx
5572
- import { useEffect as useEffect21, useContext as useContext19, useRef as useRef7 } from "react";
5573
- import { Fragment as Fragment5, jsx as jsx11 } from "react/jsx-runtime";
6150
+ import { useEffect as useEffect22, useContext as useContext20, useRef as useRef8 } from "react";
6151
+ import { Fragment as Fragment6, jsx as jsx12 } from "react/jsx-runtime";
5574
6152
  function CameraZone({
5575
6153
  x,
5576
6154
  y,
@@ -5581,10 +6159,10 @@ function CameraZone({
5581
6159
  targetY,
5582
6160
  children
5583
6161
  }) {
5584
- const engine = useContext19(EngineContext);
5585
- const prevFollowRef = useRef7(void 0);
5586
- const activeRef = useRef7(false);
5587
- useEffect21(() => {
6162
+ const engine = useContext20(EngineContext);
6163
+ const prevFollowRef = useRef8(void 0);
6164
+ const activeRef = useRef8(false);
6165
+ useEffect22(() => {
5588
6166
  const eid = engine.ecs.createEntity();
5589
6167
  engine.ecs.addComponent(eid, createScript(() => {
5590
6168
  const cam = engine.ecs.queryOne("Camera2D");
@@ -5627,15 +6205,15 @@ function CameraZone({
5627
6205
  if (engine.ecs.hasEntity(eid)) engine.ecs.destroyEntity(eid);
5628
6206
  };
5629
6207
  }, [engine.ecs, x, y, width, height, watchTag, targetX, targetY]);
5630
- return /* @__PURE__ */ jsx11(Fragment5, { children: children ?? null });
6208
+ return /* @__PURE__ */ jsx12(Fragment6, { children: children ?? null });
5631
6209
  }
5632
6210
 
5633
6211
  // src/components/Trail.tsx
5634
- import { useEffect as useEffect22, useContext as useContext20 } from "react";
6212
+ import { useEffect as useEffect23, useContext as useContext21 } from "react";
5635
6213
  function Trail({ length = 20, color = "#ffffff", width = 3 }) {
5636
- const engine = useContext20(EngineContext);
5637
- const entityId = useContext20(EntityContext);
5638
- useEffect22(() => {
6214
+ const engine = useContext21(EngineContext);
6215
+ const entityId = useContext21(EntityContext);
6216
+ useEffect23(() => {
5639
6217
  engine.ecs.addComponent(entityId, createTrail({ length, color, width }));
5640
6218
  return () => engine.ecs.removeComponent(entityId, "Trail");
5641
6219
  }, []);
@@ -5643,7 +6221,7 @@ function Trail({ length = 20, color = "#ffffff", width = 3 }) {
5643
6221
  }
5644
6222
 
5645
6223
  // src/components/NineSlice.tsx
5646
- import { useEffect as useEffect23, useContext as useContext21 } from "react";
6224
+ import { useEffect as useEffect24, useContext as useContext22 } from "react";
5647
6225
  function NineSlice({
5648
6226
  src,
5649
6227
  width,
@@ -5654,9 +6232,9 @@ function NineSlice({
5654
6232
  borderLeft = 8,
5655
6233
  zIndex = 0
5656
6234
  }) {
5657
- const engine = useContext21(EngineContext);
5658
- const entityId = useContext21(EntityContext);
5659
- useEffect23(() => {
6235
+ const engine = useContext22(EngineContext);
6236
+ const entityId = useContext22(EntityContext);
6237
+ useEffect24(() => {
5660
6238
  engine.ecs.addComponent(
5661
6239
  entityId,
5662
6240
  createNineSlice(src, width, height, {
@@ -5673,14 +6251,14 @@ function NineSlice({
5673
6251
  }
5674
6252
 
5675
6253
  // src/components/AssetLoader.tsx
5676
- import { useEffect as useEffect25 } from "react";
6254
+ import { useEffect as useEffect26 } from "react";
5677
6255
 
5678
6256
  // src/hooks/usePreload.ts
5679
- import { useState as useState6, useEffect as useEffect24, useContext as useContext22 } from "react";
6257
+ import { useState as useState6, useEffect as useEffect25, useContext as useContext23 } from "react";
5680
6258
  function usePreload(assets) {
5681
- const engine = useContext22(EngineContext);
6259
+ const engine = useContext23(EngineContext);
5682
6260
  const [state, setState] = useState6({ progress: assets.length === 0 ? 1 : 0, loaded: assets.length === 0, error: null });
5683
- useEffect24(() => {
6261
+ useEffect25(() => {
5684
6262
  if (assets.length === 0) {
5685
6263
  setState({ progress: 1, loaded: true, error: null });
5686
6264
  return;
@@ -5708,31 +6286,31 @@ function usePreload(assets) {
5708
6286
  }
5709
6287
 
5710
6288
  // src/components/AssetLoader.tsx
5711
- import { Fragment as Fragment6, jsx as jsx12 } from "react/jsx-runtime";
6289
+ import { Fragment as Fragment7, jsx as jsx13 } from "react/jsx-runtime";
5712
6290
  function AssetLoader({ assets, fallback = null, onError, children }) {
5713
6291
  const { loaded, error } = usePreload(assets);
5714
- useEffect25(() => {
6292
+ useEffect26(() => {
5715
6293
  if (error && onError) onError(error);
5716
6294
  }, [error, onError]);
5717
6295
  if (!loaded) {
5718
- return /* @__PURE__ */ jsx12(Fragment6, { children: fallback });
6296
+ return /* @__PURE__ */ jsx13(Fragment7, { children: fallback });
5719
6297
  }
5720
- return /* @__PURE__ */ jsx12(Fragment6, { children });
6298
+ return /* @__PURE__ */ jsx13(Fragment7, { children });
5721
6299
  }
5722
6300
 
5723
6301
  // src/hooks/useGame.ts
5724
- import { useContext as useContext23 } from "react";
6302
+ import { useContext as useContext24 } from "react";
5725
6303
  function useGame() {
5726
- const engine = useContext23(EngineContext);
6304
+ const engine = useContext24(EngineContext);
5727
6305
  if (!engine) throw new Error("useGame must be used inside <Game>");
5728
6306
  return engine;
5729
6307
  }
5730
6308
 
5731
6309
  // src/hooks/useCamera.ts
5732
- import { useMemo } from "react";
6310
+ import { useMemo as useMemo2 } from "react";
5733
6311
  function useCamera() {
5734
6312
  const engine = useGame();
5735
- return useMemo(() => ({
6313
+ return useMemo2(() => ({
5736
6314
  shake(intensity, duration) {
5737
6315
  const cams = engine.ecs.query("Camera2D");
5738
6316
  if (cams.length === 0) return;
@@ -5770,28 +6348,28 @@ function useCamera() {
5770
6348
  }
5771
6349
 
5772
6350
  // src/hooks/useSnapshot.ts
5773
- import { useMemo as useMemo2 } from "react";
6351
+ import { useMemo as useMemo3 } from "react";
5774
6352
  function useSnapshot() {
5775
6353
  const engine = useGame();
5776
- return useMemo2(() => ({
6354
+ return useMemo3(() => ({
5777
6355
  save: () => engine.ecs.getSnapshot(),
5778
6356
  restore: (snapshot) => engine.ecs.restoreSnapshot(snapshot)
5779
6357
  }), [engine]);
5780
6358
  }
5781
6359
 
5782
6360
  // src/hooks/useEntity.ts
5783
- import { useContext as useContext24 } from "react";
6361
+ import { useContext as useContext25 } from "react";
5784
6362
  function useEntity() {
5785
- const id = useContext24(EntityContext);
6363
+ const id = useContext25(EntityContext);
5786
6364
  if (id === null) throw new Error("useEntity must be used inside <Entity>");
5787
6365
  return id;
5788
6366
  }
5789
6367
 
5790
6368
  // src/hooks/useDestroyEntity.ts
5791
- import { useCallback as useCallback2, useContext as useContext25 } from "react";
6369
+ import { useCallback as useCallback2, useContext as useContext26 } from "react";
5792
6370
  function useDestroyEntity() {
5793
- const engine = useContext25(EngineContext);
5794
- const entityId = useContext25(EntityContext);
6371
+ const engine = useContext26(EngineContext);
6372
+ const entityId = useContext26(EntityContext);
5795
6373
  if (!engine) throw new Error("useDestroyEntity must be used inside <Game>");
5796
6374
  if (entityId === null) throw new Error("useDestroyEntity must be used inside <Entity>");
5797
6375
  return useCallback2(() => {
@@ -5802,19 +6380,19 @@ function useDestroyEntity() {
5802
6380
  }
5803
6381
 
5804
6382
  // src/hooks/useInput.ts
5805
- import { useContext as useContext26 } from "react";
6383
+ import { useContext as useContext27 } from "react";
5806
6384
  function useInput() {
5807
- const engine = useContext26(EngineContext);
6385
+ const engine = useContext27(EngineContext);
5808
6386
  if (!engine) throw new Error("useInput must be used inside <Game>");
5809
6387
  return engine.input;
5810
6388
  }
5811
6389
 
5812
6390
  // src/hooks/useInputMap.ts
5813
- import { useMemo as useMemo3 } from "react";
6391
+ import { useMemo as useMemo4 } from "react";
5814
6392
  function useInputMap(bindings) {
5815
6393
  const input = useInput();
5816
- const map = useMemo3(() => createInputMap(bindings), [JSON.stringify(bindings)]);
5817
- return useMemo3(() => ({
6394
+ const map = useMemo4(() => createInputMap(bindings), [JSON.stringify(bindings)]);
6395
+ return useMemo4(() => ({
5818
6396
  isActionDown: (action) => map.isActionDown(input, action),
5819
6397
  isActionPressed: (action) => map.isActionPressed(input, action),
5820
6398
  isActionReleased: (action) => map.isActionReleased(input, action),
@@ -5824,25 +6402,25 @@ function useInputMap(bindings) {
5824
6402
  }
5825
6403
 
5826
6404
  // src/hooks/useEvents.ts
5827
- import { useContext as useContext27, useEffect as useEffect26, useRef as useRef8 } from "react";
6405
+ import { useContext as useContext28, useEffect as useEffect27, useRef as useRef9 } from "react";
5828
6406
  function useEvents() {
5829
- const engine = useContext27(EngineContext);
6407
+ const engine = useContext28(EngineContext);
5830
6408
  if (!engine) throw new Error("useEvents must be used inside <Game>");
5831
6409
  return engine.events;
5832
6410
  }
5833
6411
  function useEvent(event, handler) {
5834
6412
  const events = useEvents();
5835
- const handlerRef = useRef8(handler);
6413
+ const handlerRef = useRef9(handler);
5836
6414
  handlerRef.current = handler;
5837
- useEffect26(() => {
6415
+ useEffect27(() => {
5838
6416
  return events.on(event, (data) => handlerRef.current(data));
5839
6417
  }, [events, event]);
5840
6418
  }
5841
6419
 
5842
6420
  // src/hooks/useCoordinates.ts
5843
- import { useCallback as useCallback3, useContext as useContext28 } from "react";
6421
+ import { useCallback as useCallback3, useContext as useContext29 } from "react";
5844
6422
  function useCoordinates() {
5845
- const engine = useContext28(EngineContext);
6423
+ const engine = useContext29(EngineContext);
5846
6424
  const worldToScreen = useCallback3((wx, wy) => {
5847
6425
  const canvas = engine.canvas;
5848
6426
  const camId = engine.ecs.queryOne("Camera2D");
@@ -5869,14 +6447,14 @@ function useCoordinates() {
5869
6447
  }
5870
6448
 
5871
6449
  // src/hooks/useInputContext.ts
5872
- import { useEffect as useEffect27, useMemo as useMemo4 } from "react";
6450
+ import { useEffect as useEffect28, useMemo as useMemo5 } from "react";
5873
6451
  function useInputContext(ctx) {
5874
- useEffect27(() => {
6452
+ useEffect28(() => {
5875
6453
  if (!ctx) return;
5876
6454
  globalInputContext.push(ctx);
5877
6455
  return () => globalInputContext.pop(ctx);
5878
6456
  }, [ctx]);
5879
- return useMemo4(() => ({
6457
+ return useMemo5(() => ({
5880
6458
  push: (c) => globalInputContext.push(c),
5881
6459
  pop: (c) => globalInputContext.pop(c),
5882
6460
  get active() {
@@ -5886,17 +6464,17 @@ function useInputContext(ctx) {
5886
6464
  }
5887
6465
 
5888
6466
  // src/hooks/usePlayerInput.ts
5889
- import { useMemo as useMemo5 } from "react";
6467
+ import { useMemo as useMemo6 } from "react";
5890
6468
  function usePlayerInput(playerId, bindings) {
5891
6469
  const input = useInput();
5892
- return useMemo5(() => createPlayerInput(playerId, bindings, input), [playerId, input, JSON.stringify(bindings)]);
6470
+ return useMemo6(() => createPlayerInput(playerId, bindings, input), [playerId, input, JSON.stringify(bindings)]);
5893
6471
  }
5894
6472
 
5895
6473
  // src/hooks/useLocalMultiplayer.ts
5896
- import { useMemo as useMemo6 } from "react";
6474
+ import { useMemo as useMemo7 } from "react";
5897
6475
  function useLocalMultiplayer(bindingsPerPlayer) {
5898
6476
  const input = useInput();
5899
- return useMemo6(
6477
+ return useMemo7(
5900
6478
  () => bindingsPerPlayer.map((bindings, i) => createPlayerInput(i + 1, bindings, input)),
5901
6479
  // eslint-disable-next-line react-hooks/exhaustive-deps
5902
6480
  [input, JSON.stringify(bindingsPerPlayer)]
@@ -5904,18 +6482,18 @@ function useLocalMultiplayer(bindingsPerPlayer) {
5904
6482
  }
5905
6483
 
5906
6484
  // src/hooks/useInputRecorder.ts
5907
- import { useMemo as useMemo7 } from "react";
6485
+ import { useMemo as useMemo8 } from "react";
5908
6486
  function useInputRecorder() {
5909
- return useMemo7(() => createInputRecorder(), []);
6487
+ return useMemo8(() => createInputRecorder(), []);
5910
6488
  }
5911
6489
 
5912
6490
  // src/hooks/useGamepad.ts
5913
- import { useEffect as useEffect28, useRef as useRef9, useState as useState7 } from "react";
6491
+ import { useEffect as useEffect29, useRef as useRef10, useState as useState7 } from "react";
5914
6492
  var EMPTY_STATE = { connected: false, axes: [], buttons: [] };
5915
6493
  function useGamepad(playerIndex = 0) {
5916
6494
  const [state, setState] = useState7(EMPTY_STATE);
5917
- const rafRef = useRef9(0);
5918
- useEffect28(() => {
6495
+ const rafRef = useRef10(0);
6496
+ useEffect29(() => {
5919
6497
  const poll = () => {
5920
6498
  const gp = navigator.getGamepads()[playerIndex];
5921
6499
  if (gp) {
@@ -5936,9 +6514,9 @@ function useGamepad(playerIndex = 0) {
5936
6514
  }
5937
6515
 
5938
6516
  // src/hooks/usePause.ts
5939
- import { useContext as useContext29, useState as useState8, useCallback as useCallback4 } from "react";
6517
+ import { useContext as useContext30, useState as useState8, useCallback as useCallback4 } from "react";
5940
6518
  function usePause() {
5941
- const engine = useContext29(EngineContext);
6519
+ const engine = useContext30(EngineContext);
5942
6520
  const [paused, setPaused] = useState8(false);
5943
6521
  const pause = useCallback4(() => {
5944
6522
  engine.loop.pause();
@@ -5955,10 +6533,75 @@ function usePause() {
5955
6533
  return { paused, pause, resume, toggle };
5956
6534
  }
5957
6535
 
6536
+ // src/hooks/useProfiler.ts
6537
+ import { useContext as useContext31, useEffect as useEffect30, useRef as useRef11, useState as useState9 } from "react";
6538
+ var EMPTY = {
6539
+ fps: 0,
6540
+ frameTime: 0,
6541
+ entityCount: 0,
6542
+ systemTimings: /* @__PURE__ */ new Map()
6543
+ };
6544
+ function useProfiler() {
6545
+ const engine = useContext31(EngineContext);
6546
+ const [data, setData] = useState9(EMPTY);
6547
+ const frameTimesRef = useRef11([]);
6548
+ const lastUpdateRef = useRef11(0);
6549
+ const prevTimeRef = useRef11(0);
6550
+ useEffect30(() => {
6551
+ if (!engine) return;
6552
+ let rafId;
6553
+ const frameTimes = frameTimesRef.current;
6554
+ const sample = (now) => {
6555
+ if (prevTimeRef.current > 0) {
6556
+ frameTimes.push(now - prevTimeRef.current);
6557
+ }
6558
+ prevTimeRef.current = now;
6559
+ if (now - lastUpdateRef.current >= 500) {
6560
+ lastUpdateRef.current = now;
6561
+ let fps = 0;
6562
+ let avgFrameTime = 0;
6563
+ if (frameTimes.length > 0) {
6564
+ const sum = frameTimes.reduce((a, b) => a + b, 0);
6565
+ avgFrameTime = sum / frameTimes.length;
6566
+ fps = avgFrameTime > 0 ? 1e3 / avgFrameTime : 0;
6567
+ frameTimes.length = 0;
6568
+ }
6569
+ setData({
6570
+ fps: Math.round(fps * 10) / 10,
6571
+ frameTime: Math.round(avgFrameTime * 100) / 100,
6572
+ entityCount: engine.ecs.entityCount,
6573
+ systemTimings: new Map(engine.systemTimings)
6574
+ });
6575
+ }
6576
+ rafId = requestAnimationFrame(sample);
6577
+ };
6578
+ rafId = requestAnimationFrame(sample);
6579
+ return () => {
6580
+ cancelAnimationFrame(rafId);
6581
+ frameTimes.length = 0;
6582
+ prevTimeRef.current = 0;
6583
+ lastUpdateRef.current = 0;
6584
+ };
6585
+ }, [engine]);
6586
+ return data;
6587
+ }
6588
+
6589
+ // src/hooks/usePostProcess.ts
6590
+ import { useEffect as useEffect31 } from "react";
6591
+ function usePostProcess(effect) {
6592
+ const engine = useGame();
6593
+ useEffect31(() => {
6594
+ engine.postProcessStack.add(effect);
6595
+ return () => {
6596
+ engine.postProcessStack.remove(effect);
6597
+ };
6598
+ }, [engine, effect]);
6599
+ }
6600
+
5958
6601
  // ../gameplay/src/hooks/useAnimationController.ts
5959
- import { useState as useState9, useCallback as useCallback5 } from "react";
6602
+ import { useState as useState10, useCallback as useCallback5 } from "react";
5960
6603
  function useAnimationController(states, initial) {
5961
- const [stateName, setStateName] = useState9(initial);
6604
+ const [stateName, setStateName] = useState10(initial);
5962
6605
  const setState = useCallback5((next) => {
5963
6606
  setStateName((prev) => prev === next ? prev : next);
5964
6607
  }, []);
@@ -6007,19 +6650,19 @@ function useAISteering() {
6007
6650
  }
6008
6651
 
6009
6652
  // ../gameplay/src/hooks/useDamageZone.ts
6010
- import { useContext as useContext30 } from "react";
6653
+ import { useContext as useContext32 } from "react";
6011
6654
  function useDamageZone(damage, opts = {}) {
6012
- const engine = useContext30(EngineContext);
6655
+ const engine = useContext32(EngineContext);
6013
6656
  useTriggerEnter((other) => {
6014
6657
  engine.events.emit(`damage:${other}`, { amount: damage });
6015
6658
  }, { tag: opts.tag, layer: opts.layer });
6016
6659
  }
6017
6660
 
6018
6661
  // ../gameplay/src/hooks/useDropThrough.ts
6019
- import { useContext as useContext31, useCallback as useCallback7 } from "react";
6662
+ import { useContext as useContext33, useCallback as useCallback7 } from "react";
6020
6663
  function useDropThrough(frames = 8) {
6021
- const engine = useContext31(EngineContext);
6022
- const entityId = useContext31(EntityContext);
6664
+ const engine = useContext33(EngineContext);
6665
+ const entityId = useContext33(EntityContext);
6023
6666
  const dropThrough = useCallback7(() => {
6024
6667
  const rb = engine.ecs.getComponent(entityId, "RigidBody");
6025
6668
  if (rb) rb.dropThrough = frames;
@@ -6028,14 +6671,14 @@ function useDropThrough(frames = 8) {
6028
6671
  }
6029
6672
 
6030
6673
  // ../gameplay/src/hooks/useGameStateMachine.ts
6031
- import { useState as useState10, useRef as useRef10, useCallback as useCallback8, useEffect as useEffect29, useContext as useContext32 } from "react";
6674
+ import { useState as useState11, useRef as useRef12, useCallback as useCallback8, useEffect as useEffect32, useContext as useContext34 } from "react";
6032
6675
  function useGameStateMachine(states, initial) {
6033
- const engine = useContext32(EngineContext);
6034
- const [state, setState] = useState10(initial);
6035
- const stateRef = useRef10(initial);
6036
- const statesRef = useRef10(states);
6676
+ const engine = useContext34(EngineContext);
6677
+ const [state, setState] = useState11(initial);
6678
+ const stateRef = useRef12(initial);
6679
+ const statesRef = useRef12(states);
6037
6680
  statesRef.current = states;
6038
- useEffect29(() => {
6681
+ useEffect32(() => {
6039
6682
  statesRef.current[initial]?.onEnter?.();
6040
6683
  }, []);
6041
6684
  const transition = useCallback8((to) => {
@@ -6046,7 +6689,7 @@ function useGameStateMachine(states, initial) {
6046
6689
  setState(to);
6047
6690
  statesRef.current[to]?.onEnter?.();
6048
6691
  }, []);
6049
- useEffect29(() => {
6692
+ useEffect32(() => {
6050
6693
  const eid = engine.ecs.createEntity();
6051
6694
  engine.ecs.addComponent(eid, createScript((_id, _world, _input, dt) => {
6052
6695
  statesRef.current[stateRef.current]?.onUpdate?.(dt);
@@ -6059,22 +6702,22 @@ function useGameStateMachine(states, initial) {
6059
6702
  }
6060
6703
 
6061
6704
  // ../gameplay/src/hooks/useHealth.ts
6062
- import { useRef as useRef11, useEffect as useEffect30, useContext as useContext33, useCallback as useCallback9 } from "react";
6705
+ import { useRef as useRef13, useEffect as useEffect33, useContext as useContext35, useCallback as useCallback9 } from "react";
6063
6706
  function useHealth(maxHp, opts = {}) {
6064
- const engine = useContext33(EngineContext);
6065
- const entityId = useContext33(EntityContext);
6066
- const hpRef = useRef11(maxHp);
6067
- const invincibleRef = useRef11(false);
6707
+ const engine = useContext35(EngineContext);
6708
+ const entityId = useContext35(EntityContext);
6709
+ const hpRef = useRef13(maxHp);
6710
+ const invincibleRef = useRef13(false);
6068
6711
  const iFrameDuration = opts.iFrames ?? 1;
6069
- const onDeathRef = useRef11(opts.onDeath);
6070
- const onDamageRef = useRef11(opts.onDamage);
6071
- useEffect30(() => {
6712
+ const onDeathRef = useRef13(opts.onDeath);
6713
+ const onDamageRef = useRef13(opts.onDamage);
6714
+ useEffect33(() => {
6072
6715
  onDeathRef.current = opts.onDeath;
6073
6716
  });
6074
- useEffect30(() => {
6717
+ useEffect33(() => {
6075
6718
  onDamageRef.current = opts.onDamage;
6076
6719
  });
6077
- const timerRef = useRef11(
6720
+ const timerRef = useRef13(
6078
6721
  createTimer(iFrameDuration, () => {
6079
6722
  invincibleRef.current = false;
6080
6723
  })
@@ -6089,11 +6732,11 @@ function useHealth(maxHp, opts = {}) {
6089
6732
  }
6090
6733
  if (hpRef.current <= 0) onDeathRef.current?.();
6091
6734
  }, [iFrameDuration]);
6092
- const takeDamageRef = useRef11(takeDamage);
6093
- useEffect30(() => {
6735
+ const takeDamageRef = useRef13(takeDamage);
6736
+ useEffect33(() => {
6094
6737
  takeDamageRef.current = takeDamage;
6095
6738
  }, [takeDamage]);
6096
- useEffect30(() => {
6739
+ useEffect33(() => {
6097
6740
  return engine.events.on(`damage:${entityId}`, ({ amount }) => {
6098
6741
  takeDamageRef.current(amount);
6099
6742
  });
@@ -6128,10 +6771,10 @@ function useHealth(maxHp, opts = {}) {
6128
6771
  }
6129
6772
 
6130
6773
  // ../gameplay/src/hooks/useKinematicBody.ts
6131
- import { useContext as useContext34, useCallback as useCallback10 } from "react";
6774
+ import { useContext as useContext36, useCallback as useCallback10 } from "react";
6132
6775
  function useKinematicBody() {
6133
- const engine = useContext34(EngineContext);
6134
- const entityId = useContext34(EntityContext);
6776
+ const engine = useContext36(EngineContext);
6777
+ const entityId = useContext36(EntityContext);
6135
6778
  const moveAndCollide = useCallback10((dx, dy) => {
6136
6779
  const transform = engine.ecs.getComponent(entityId, "Transform");
6137
6780
  if (!transform) return { dx: 0, dy: 0 };
@@ -6176,11 +6819,11 @@ function useKinematicBody() {
6176
6819
  }
6177
6820
 
6178
6821
  // ../gameplay/src/hooks/useLevelTransition.ts
6179
- import { useState as useState11, useRef as useRef12, useCallback as useCallback11 } from "react";
6822
+ import { useState as useState12, useRef as useRef14, useCallback as useCallback11 } from "react";
6180
6823
  function useLevelTransition(initial) {
6181
- const [currentLevel, setCurrentLevel] = useState11(initial);
6182
- const [isTransitioning, setIsTransitioning] = useState11(false);
6183
- const overlayRef = useRef12(null);
6824
+ const [currentLevel, setCurrentLevel] = useState12(initial);
6825
+ const [isTransitioning, setIsTransitioning] = useState12(false);
6826
+ const overlayRef = useRef14(null);
6184
6827
  const transitionTo = useCallback11((level, opts = {}) => {
6185
6828
  const { duration = 0.4, type = "fade" } = opts;
6186
6829
  if (type === "instant") {
@@ -6218,13 +6861,13 @@ function useLevelTransition(initial) {
6218
6861
  }
6219
6862
 
6220
6863
  // ../gameplay/src/hooks/usePlatformerController.ts
6221
- import { useContext as useContext35, useEffect as useEffect31 } from "react";
6864
+ import { useContext as useContext37, useEffect as useEffect34 } from "react";
6222
6865
  function normalizeKeys(val, defaults) {
6223
6866
  if (!val) return defaults;
6224
6867
  return Array.isArray(val) ? val : [val];
6225
6868
  }
6226
6869
  function usePlatformerController(entityId, opts = {}) {
6227
- const engine = useContext35(EngineContext);
6870
+ const engine = useContext37(EngineContext);
6228
6871
  const {
6229
6872
  speed = 200,
6230
6873
  jumpForce = -500,
@@ -6237,7 +6880,7 @@ function usePlatformerController(entityId, opts = {}) {
6237
6880
  const leftKeys = normalizeKeys(bindings?.left, ["ArrowLeft", "KeyA", "a"]);
6238
6881
  const rightKeys = normalizeKeys(bindings?.right, ["ArrowRight", "KeyD", "d"]);
6239
6882
  const jumpKeys = normalizeKeys(bindings?.jump, ["Space", "ArrowUp", "KeyW", "w"]);
6240
- useEffect31(() => {
6883
+ useEffect34(() => {
6241
6884
  const state = {
6242
6885
  coyoteTimer: 0,
6243
6886
  jumpBuffer: 0,
@@ -6301,11 +6944,11 @@ function usePathfinding() {
6301
6944
  }
6302
6945
 
6303
6946
  // ../gameplay/src/hooks/usePersistedBindings.ts
6304
- import { useState as useState12, useCallback as useCallback13, useMemo as useMemo8, useContext as useContext36 } from "react";
6947
+ import { useState as useState13, useCallback as useCallback13, useMemo as useMemo9, useContext as useContext38 } from "react";
6305
6948
  function usePersistedBindings(storageKey, defaults) {
6306
- const engine = useContext36(EngineContext);
6949
+ const engine = useContext38(EngineContext);
6307
6950
  const input = engine.input;
6308
- const [bindings, setBindings] = useState12(() => {
6951
+ const [bindings, setBindings] = useState13(() => {
6309
6952
  try {
6310
6953
  const stored = localStorage.getItem(storageKey);
6311
6954
  if (stored) return { ...defaults, ...JSON.parse(stored) };
@@ -6313,7 +6956,7 @@ function usePersistedBindings(storageKey, defaults) {
6313
6956
  }
6314
6957
  return defaults;
6315
6958
  });
6316
- const normalized = useMemo8(() => {
6959
+ const normalized = useMemo9(() => {
6317
6960
  const out = {};
6318
6961
  for (const [action, keys] of Object.entries(bindings)) {
6319
6962
  if (typeof keys === "string") out[action] = [keys];
@@ -6338,7 +6981,7 @@ function usePersistedBindings(storageKey, defaults) {
6338
6981
  }
6339
6982
  setBindings(defaults);
6340
6983
  }, [storageKey]);
6341
- return useMemo8(() => ({
6984
+ return useMemo9(() => ({
6342
6985
  bindings,
6343
6986
  rebind,
6344
6987
  reset,
@@ -6349,9 +6992,9 @@ function usePersistedBindings(storageKey, defaults) {
6349
6992
  }
6350
6993
 
6351
6994
  // ../gameplay/src/hooks/useRestart.ts
6352
- import { useState as useState13, useCallback as useCallback14 } from "react";
6995
+ import { useState as useState14, useCallback as useCallback14 } from "react";
6353
6996
  function useRestart() {
6354
- const [restartKey, setRestartKey] = useState13(0);
6997
+ const [restartKey, setRestartKey] = useState14(0);
6355
6998
  const restart = useCallback14(() => {
6356
6999
  setRestartKey((k) => k + 1);
6357
7000
  }, []);
@@ -6359,10 +7002,10 @@ function useRestart() {
6359
7002
  }
6360
7003
 
6361
7004
  // ../gameplay/src/hooks/useSave.ts
6362
- import { useCallback as useCallback15, useRef as useRef13 } from "react";
7005
+ import { useCallback as useCallback15, useRef as useRef15 } from "react";
6363
7006
  function useSave(key, defaultValue, opts = {}) {
6364
7007
  const version = opts.version ?? 1;
6365
- const dataRef = useRef13(defaultValue);
7008
+ const dataRef = useRef15(defaultValue);
6366
7009
  const save = useCallback15((value) => {
6367
7010
  dataRef.current = value;
6368
7011
  const slot = { version, data: value };
@@ -6408,11 +7051,11 @@ function useSave(key, defaultValue, opts = {}) {
6408
7051
  }
6409
7052
 
6410
7053
  // ../gameplay/src/hooks/useTopDownMovement.ts
6411
- import { useContext as useContext37, useEffect as useEffect32 } from "react";
7054
+ import { useContext as useContext39, useEffect as useEffect35 } from "react";
6412
7055
  function useTopDownMovement(entityId, opts = {}) {
6413
- const engine = useContext37(EngineContext);
7056
+ const engine = useContext39(EngineContext);
6414
7057
  const { speed = 200, normalizeDiagonal = true } = opts;
6415
- useEffect32(() => {
7058
+ useEffect35(() => {
6416
7059
  const updateFn = (id, world, input) => {
6417
7060
  if (!world.hasEntity(id)) return;
6418
7061
  const rb = world.getComponent(id, "RigidBody");
@@ -6437,11 +7080,11 @@ function useTopDownMovement(entityId, opts = {}) {
6437
7080
  }
6438
7081
 
6439
7082
  // ../gameplay/src/hooks/useDialogue.ts
6440
- import { useState as useState14, useCallback as useCallback16, useRef as useRef14 } from "react";
7083
+ import { useState as useState15, useCallback as useCallback16, useRef as useRef16 } from "react";
6441
7084
  function useDialogue() {
6442
- const [active, setActive] = useState14(false);
6443
- const [currentId, setCurrentId] = useState14(null);
6444
- const scriptRef = useRef14(null);
7085
+ const [active, setActive] = useState15(false);
7086
+ const [currentId, setCurrentId] = useState15(null);
7087
+ const scriptRef = useRef16(null);
6445
7088
  const start = useCallback16((script, startId) => {
6446
7089
  scriptRef.current = script;
6447
7090
  const id = startId ?? Object.keys(script)[0];
@@ -6485,16 +7128,16 @@ function useDialogue() {
6485
7128
  }
6486
7129
 
6487
7130
  // ../gameplay/src/hooks/useCutscene.ts
6488
- import { useState as useState15, useCallback as useCallback17, useRef as useRef15, useEffect as useEffect33, useContext as useContext38 } from "react";
7131
+ import { useState as useState16, useCallback as useCallback17, useRef as useRef17, useEffect as useEffect36, useContext as useContext40 } from "react";
6489
7132
  function useCutscene() {
6490
- const engine = useContext38(EngineContext);
6491
- const [playing, setPlaying] = useState15(false);
6492
- const [stepIndex, setStepIndex] = useState15(0);
6493
- const stepsRef = useRef15([]);
6494
- const timerRef = useRef15(0);
6495
- const idxRef = useRef15(0);
6496
- const playingRef = useRef15(false);
6497
- const entityRef = useRef15(null);
7133
+ const engine = useContext40(EngineContext);
7134
+ const [playing, setPlaying] = useState16(false);
7135
+ const [stepIndex, setStepIndex] = useState16(0);
7136
+ const stepsRef = useRef17([]);
7137
+ const timerRef = useRef17(0);
7138
+ const idxRef = useRef17(0);
7139
+ const playingRef = useRef17(false);
7140
+ const entityRef = useRef17(null);
6498
7141
  const finish = useCallback17(() => {
6499
7142
  playingRef.current = false;
6500
7143
  setPlaying(false);
@@ -6572,7 +7215,7 @@ function useCutscene() {
6572
7215
  }
6573
7216
  finish();
6574
7217
  }, [finish]);
6575
- useEffect33(() => {
7218
+ useEffect36(() => {
6576
7219
  return () => {
6577
7220
  if (entityRef.current !== null && engine.ecs.hasEntity(entityRef.current)) {
6578
7221
  engine.ecs.destroyEntity(entityRef.current);
@@ -6618,8 +7261,112 @@ function useGameStore(key, initialState) {
6618
7261
  return [state, setState];
6619
7262
  }
6620
7263
 
7264
+ // ../gameplay/src/hooks/useTween.ts
7265
+ import { useRef as useRef18, useCallback as useCallback19, useEffect as useEffect37 } from "react";
7266
+ function useTween(opts) {
7267
+ const rafRef = useRef18(null);
7268
+ const startTimeRef = useRef18(0);
7269
+ const runningRef = useRef18(false);
7270
+ const optsRef = useRef18(opts);
7271
+ optsRef.current = opts;
7272
+ const stop = useCallback19(() => {
7273
+ if (rafRef.current !== null) {
7274
+ cancelAnimationFrame(rafRef.current);
7275
+ rafRef.current = null;
7276
+ }
7277
+ runningRef.current = false;
7278
+ }, []);
7279
+ const start = useCallback19(() => {
7280
+ stop();
7281
+ runningRef.current = true;
7282
+ startTimeRef.current = performance.now();
7283
+ const tick = (now) => {
7284
+ if (!runningRef.current) return;
7285
+ const { from, to, duration, ease, onUpdate, onComplete } = optsRef.current;
7286
+ const easeFn = ease ?? Ease.linear;
7287
+ const elapsed = (now - startTimeRef.current) / 1e3;
7288
+ const t = duration > 0 ? Math.min(elapsed / duration, 1) : 1;
7289
+ const value = from + (to - from) * easeFn(t);
7290
+ onUpdate(value);
7291
+ if (t >= 1) {
7292
+ runningRef.current = false;
7293
+ rafRef.current = null;
7294
+ onComplete?.();
7295
+ } else {
7296
+ rafRef.current = requestAnimationFrame(tick);
7297
+ }
7298
+ };
7299
+ rafRef.current = requestAnimationFrame(tick);
7300
+ }, [stop]);
7301
+ useEffect37(() => {
7302
+ if (opts.autoStart) {
7303
+ start();
7304
+ }
7305
+ }, []);
7306
+ useEffect37(() => {
7307
+ return () => {
7308
+ if (rafRef.current !== null) {
7309
+ cancelAnimationFrame(rafRef.current);
7310
+ rafRef.current = null;
7311
+ }
7312
+ runningRef.current = false;
7313
+ };
7314
+ }, []);
7315
+ return {
7316
+ start,
7317
+ stop,
7318
+ get isRunning() {
7319
+ return runningRef.current;
7320
+ }
7321
+ };
7322
+ }
7323
+
7324
+ // ../gameplay/src/hooks/useObjectPool.ts
7325
+ import { useRef as useRef19, useMemo as useMemo10, useEffect as useEffect38 } from "react";
7326
+ function useObjectPool(factory, reset, initialSize) {
7327
+ const poolRef = useRef19([]);
7328
+ const activeRef = useRef19(0);
7329
+ const factoryRef = useRef19(factory);
7330
+ factoryRef.current = factory;
7331
+ const resetRef = useRef19(reset);
7332
+ resetRef.current = reset;
7333
+ useEffect38(() => {
7334
+ if (initialSize != null && initialSize > 0) {
7335
+ const pool = poolRef.current;
7336
+ for (let i = 0; i < initialSize; i++) {
7337
+ pool.push(factoryRef.current());
7338
+ }
7339
+ }
7340
+ }, []);
7341
+ return useMemo10(() => ({
7342
+ acquire() {
7343
+ activeRef.current++;
7344
+ if (poolRef.current.length > 0) {
7345
+ return poolRef.current.pop();
7346
+ }
7347
+ return factoryRef.current();
7348
+ },
7349
+ release(obj) {
7350
+ resetRef.current(obj);
7351
+ activeRef.current = Math.max(0, activeRef.current - 1);
7352
+ poolRef.current.push(obj);
7353
+ },
7354
+ prewarm(count) {
7355
+ for (let i = 0; i < count; i++) {
7356
+ poolRef.current.push(factoryRef.current());
7357
+ }
7358
+ },
7359
+ get activeCount() {
7360
+ return activeRef.current;
7361
+ },
7362
+ get poolSize() {
7363
+ return poolRef.current.length;
7364
+ }
7365
+ }), []);
7366
+ }
7367
+
6621
7368
  // ../../packages/audio/src/useSound.ts
6622
- import { useEffect as useEffect34, useRef as useRef16 } from "react";
7369
+ import { useEffect as useEffect39, useRef as useRef20 } from "react";
6623
7370
  var _audioCtx = null;
6624
7371
  function getAudioCtx() {
6625
7372
  if (!_audioCtx) _audioCtx = new AudioContext();
@@ -6686,13 +7433,13 @@ async function loadBuffer(src) {
6686
7433
  return buf;
6687
7434
  }
6688
7435
  function useSound(src, opts = {}) {
6689
- const bufferRef = useRef16(null);
6690
- const sourceRef = useRef16(null);
6691
- const gainRef = useRef16(null);
6692
- const volRef = useRef16(opts.volume ?? 1);
6693
- const loopRef = useRef16(opts.loop ?? false);
6694
- const groupRef = useRef16(opts.group);
6695
- useEffect34(() => {
7436
+ const bufferRef = useRef20(null);
7437
+ const sourceRef = useRef20(null);
7438
+ const gainRef = useRef20(null);
7439
+ const volRef = useRef20(opts.volume ?? 1);
7440
+ const loopRef = useRef20(opts.loop ?? false);
7441
+ const groupRef = useRef20(opts.group);
7442
+ useEffect39(() => {
6696
7443
  let cancelled = false;
6697
7444
  loadBuffer(src).then((buf) => {
6698
7445
  if (!cancelled) bufferRef.current = buf;
@@ -6714,7 +7461,7 @@ function useSound(src, opts = {}) {
6714
7461
  };
6715
7462
  }, [src]);
6716
7463
  const getDestination = () => groupRef.current ? getGroupGainNode(groupRef.current) : getGroupGainNode("master");
6717
- const play = () => {
7464
+ const play = (playOpts) => {
6718
7465
  if (!bufferRef.current) return;
6719
7466
  const ctx = getAudioCtx();
6720
7467
  if (ctx.state === "suspended") void ctx.resume();
@@ -6733,7 +7480,12 @@ function useSound(src, opts = {}) {
6733
7480
  source.buffer = bufferRef.current;
6734
7481
  source.loop = loopRef.current;
6735
7482
  source.connect(gain);
6736
- source.start();
7483
+ const delay = playOpts?.delay;
7484
+ if (delay && delay > 0) {
7485
+ source.start(ctx.currentTime + delay);
7486
+ } else {
7487
+ source.start();
7488
+ }
6737
7489
  source.onended = () => {
6738
7490
  sourceRef.current = null;
6739
7491
  };
@@ -6827,7 +7579,19 @@ function createAtlas(names, _columns) {
6827
7579
  });
6828
7580
  return atlas;
6829
7581
  }
7582
+
7583
+ // src/utils/prefab.ts
7584
+ import { memo } from "react";
7585
+ function definePrefab(name, defaults, render) {
7586
+ const component = memo((props) => {
7587
+ const merged = { ...defaults, ...props };
7588
+ return render(merged);
7589
+ });
7590
+ component.displayName = name;
7591
+ return component;
7592
+ }
6830
7593
  export {
7594
+ AnimatedSprite,
6831
7595
  Animation,
6832
7596
  AssetLoader,
6833
7597
  BoxCollider,
@@ -6858,29 +7622,38 @@ export {
6858
7622
  VirtualJoystick,
6859
7623
  World,
6860
7624
  arrive,
7625
+ chromaticAberrationEffect,
6861
7626
  createAtlas,
6862
7627
  createCompoundCollider,
6863
7628
  createInputMap,
6864
7629
  createInputRecorder,
6865
7630
  createNineSlice,
6866
7631
  createPlayerInput,
7632
+ createPostProcessStack,
7633
+ createRenderLayerManager,
6867
7634
  createSprite,
6868
7635
  createTag,
7636
+ createTimeline,
6869
7637
  createTimer,
6870
7638
  createTransform,
7639
+ defaultLayers,
7640
+ defineAnimations,
6871
7641
  definePlugin,
7642
+ definePrefab,
6872
7643
  duck,
6873
7644
  findByTag,
6874
7645
  flee,
6875
7646
  getGroupVolume,
6876
7647
  globalInputContext,
6877
7648
  hotReloadPlugin,
7649
+ mergeTileColliders,
6878
7650
  overlapBox,
6879
7651
  overlapCircle,
6880
7652
  patrol,
6881
7653
  preloadManifest,
6882
7654
  raycast,
6883
7655
  raycastAll,
7656
+ scanlineEffect,
6884
7657
  seek,
6885
7658
  setGroupMute,
6886
7659
  setGroupVolume,
@@ -6894,6 +7667,7 @@ export {
6894
7667
  useCircleEnter,
6895
7668
  useCircleExit,
6896
7669
  useCircleStay,
7670
+ useCollidingWith,
6897
7671
  useCollisionEnter,
6898
7672
  useCollisionExit,
6899
7673
  useCollisionStay,
@@ -6918,12 +7692,15 @@ export {
6918
7692
  useKinematicBody,
6919
7693
  useLevelTransition,
6920
7694
  useLocalMultiplayer,
7695
+ useObjectPool,
6921
7696
  usePathfinding,
6922
7697
  usePause,
6923
7698
  usePersistedBindings,
6924
7699
  usePlatformerController,
6925
7700
  usePlayerInput,
7701
+ usePostProcess,
6926
7702
  usePreload,
7703
+ useProfiler,
6927
7704
  useRestart,
6928
7705
  useSave,
6929
7706
  useSnapshot,
@@ -6932,6 +7709,8 @@ export {
6932
7709
  useTriggerEnter,
6933
7710
  useTriggerExit,
6934
7711
  useTriggerStay,
7712
+ useTween,
6935
7713
  useVirtualInput,
7714
+ vignetteEffect,
6936
7715
  wander
6937
7716
  };