cubeforge 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,48 +1,94 @@
1
1
  // src/components/Game.tsx
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
3
3
  // ../core/src/ecs/world.ts
4
4
  class ECSWorld {
5
5
  nextId = 0;
6
- entities = new Set;
7
- components = new Map;
6
+ componentIndex = new Map;
7
+ _rngState = 0;
8
+ _deterministic = false;
9
+ archetypes = new Map;
10
+ entityArchetype = new Map;
8
11
  systems = [];
9
12
  queryCache = new Map;
10
13
  dirtyTypes = new Set;
11
14
  dirtyAll = false;
15
+ getOrCreateArchetype(types) {
16
+ const arr = [...types].sort();
17
+ const key = arr.join("\x00");
18
+ let arch = this.archetypes.get(key);
19
+ if (!arch) {
20
+ arch = { key, types: new Set(arr), entities: [] };
21
+ this.archetypes.set(key, arch);
22
+ }
23
+ return arch;
24
+ }
25
+ moveToArchetype(id, newArch) {
26
+ const oldKey = this.entityArchetype.get(id);
27
+ if (oldKey !== undefined) {
28
+ const oldArch = this.archetypes.get(oldKey);
29
+ if (oldArch) {
30
+ const idx = oldArch.entities.indexOf(id);
31
+ if (idx !== -1)
32
+ oldArch.entities.splice(idx, 1);
33
+ }
34
+ }
35
+ newArch.entities.push(id);
36
+ this.entityArchetype.set(id, newArch.key);
37
+ }
12
38
  createEntity() {
13
39
  const id = this.nextId++;
14
- this.entities.add(id);
15
- this.components.set(id, new Map);
40
+ this.componentIndex.set(id, new Map);
41
+ const emptyArch = this.getOrCreateArchetype([]);
42
+ emptyArch.entities.push(id);
43
+ this.entityArchetype.set(id, emptyArch.key);
16
44
  this.dirtyAll = true;
17
45
  return id;
18
46
  }
19
47
  destroyEntity(id) {
20
- const comps = this.components.get(id);
48
+ const comps = this.componentIndex.get(id);
21
49
  if (comps) {
22
- for (const type of comps.keys()) {
50
+ for (const type of comps.keys())
23
51
  this.dirtyTypes.add(type);
52
+ }
53
+ const archKey = this.entityArchetype.get(id);
54
+ if (archKey !== undefined) {
55
+ const arch = this.archetypes.get(archKey);
56
+ if (arch) {
57
+ const idx = arch.entities.indexOf(id);
58
+ if (idx !== -1)
59
+ arch.entities.splice(idx, 1);
24
60
  }
25
61
  }
26
- this.entities.delete(id);
27
- this.components.delete(id);
62
+ this.componentIndex.delete(id);
63
+ this.entityArchetype.delete(id);
28
64
  this.dirtyAll = true;
29
65
  }
30
66
  hasEntity(id) {
31
- return this.entities.has(id);
67
+ return this.componentIndex.has(id);
32
68
  }
33
69
  addComponent(id, component) {
34
- this.components.get(id)?.set(component.type, component);
70
+ const comps = this.componentIndex.get(id);
71
+ if (!comps)
72
+ return;
73
+ comps.set(component.type, component);
35
74
  this.dirtyTypes.add(component.type);
75
+ const newArch = this.getOrCreateArchetype(comps.keys());
76
+ this.moveToArchetype(id, newArch);
36
77
  }
37
78
  removeComponent(id, type) {
38
- this.components.get(id)?.delete(type);
79
+ const comps = this.componentIndex.get(id);
80
+ if (!comps)
81
+ return;
82
+ comps.delete(type);
39
83
  this.dirtyTypes.add(type);
84
+ const newArch = this.getOrCreateArchetype(comps.keys());
85
+ this.moveToArchetype(id, newArch);
40
86
  }
41
87
  getComponent(id, type) {
42
- return this.components.get(id)?.get(type);
88
+ return this.componentIndex.get(id)?.get(type);
43
89
  }
44
90
  hasComponent(id, type) {
45
- return this.components.get(id)?.has(type) ?? false;
91
+ return this.componentIndex.get(id)?.has(type) ?? false;
46
92
  }
47
93
  query(...types) {
48
94
  const key = types.slice().sort().join("\x00");
@@ -50,36 +96,77 @@ class ECSWorld {
50
96
  if (cached)
51
97
  return cached;
52
98
  const result = [];
53
- for (const id of this.entities) {
54
- const comps = this.components.get(id);
55
- let match = true;
56
- for (const t of types) {
57
- if (!comps.has(t)) {
58
- match = false;
59
- break;
60
- }
99
+ for (const arch of this.archetypes.values()) {
100
+ if (types.every((t) => arch.types.has(t))) {
101
+ for (const id of arch.entities)
102
+ result.push(id);
61
103
  }
62
- if (match)
63
- result.push(id);
64
104
  }
65
105
  this.queryCache.set(key, result);
66
106
  return result;
67
107
  }
68
108
  queryOne(...types) {
69
- for (const id of this.entities) {
70
- const comps = this.components.get(id);
71
- let match = true;
72
- for (const t of types) {
73
- if (!comps.has(t)) {
74
- match = false;
75
- break;
76
- }
109
+ for (const arch of this.archetypes.values()) {
110
+ if (types.every((t) => arch.types.has(t))) {
111
+ if (arch.entities.length > 0)
112
+ return arch.entities[0];
77
113
  }
78
- if (match)
114
+ }
115
+ return;
116
+ }
117
+ findByTag(tag) {
118
+ for (const id of this.query("Tag")) {
119
+ const t = this.getComponent(id, "Tag");
120
+ if (t?.tags.includes(tag))
79
121
  return id;
80
122
  }
81
123
  return;
82
124
  }
125
+ findAllByTag(tag) {
126
+ const result = [];
127
+ for (const id of this.query("Tag")) {
128
+ const t = this.getComponent(id, "Tag");
129
+ if (t?.tags.includes(tag))
130
+ result.push(id);
131
+ }
132
+ return result;
133
+ }
134
+ setDeterministicSeed(seed) {
135
+ this._rngState = seed >>> 0;
136
+ this._deterministic = true;
137
+ }
138
+ rng() {
139
+ if (!this._deterministic)
140
+ return Math.random();
141
+ this._rngState = Math.imul(this._rngState, 1664525) + 1013904223 >>> 0;
142
+ return this._rngState / 4294967296;
143
+ }
144
+ getSnapshot() {
145
+ const entities = [];
146
+ for (const [id, comps] of this.componentIndex) {
147
+ const components = [];
148
+ for (const comp of comps.values()) {
149
+ components.push(JSON.parse(JSON.stringify(comp)));
150
+ }
151
+ entities.push({ id, components });
152
+ }
153
+ return { nextId: this.nextId, rngState: this._rngState, entities };
154
+ }
155
+ restoreSnapshot(snapshot) {
156
+ this.clear();
157
+ this.nextId = snapshot.nextId;
158
+ this._rngState = snapshot.rngState;
159
+ for (const { id, components } of snapshot.entities) {
160
+ const compMap = new Map;
161
+ for (const comp of components)
162
+ compMap.set(comp.type, comp);
163
+ this.componentIndex.set(id, compMap);
164
+ const arch = this.getOrCreateArchetype(compMap.keys());
165
+ arch.entities.push(id);
166
+ this.entityArchetype.set(id, arch.key);
167
+ }
168
+ this.dirtyAll = true;
169
+ }
83
170
  addSystem(system) {
84
171
  this.systems.push(system);
85
172
  }
@@ -110,17 +197,30 @@ class ECSWorld {
110
197
  }
111
198
  }
112
199
  clear() {
113
- this.entities.clear();
114
- this.components.clear();
200
+ this.componentIndex.clear();
201
+ this.archetypes.clear();
202
+ this.entityArchetype.clear();
115
203
  this.queryCache.clear();
116
204
  this.dirtyTypes.clear();
117
205
  this.dirtyAll = false;
118
206
  this.nextId = 0;
207
+ this._rngState = 0;
208
+ this._deterministic = false;
119
209
  }
120
210
  get entityCount() {
121
- return this.entities.size;
211
+ return this.componentIndex.size;
122
212
  }
123
213
  }
214
+ // ../core/src/ecs/worldQueries.ts
215
+ function findByTag(world, tag) {
216
+ const results = [];
217
+ for (const id of world.query("Tag")) {
218
+ const t = world.getComponent(id, "Tag");
219
+ if (t?.tags.includes(tag))
220
+ results.push(id);
221
+ }
222
+ return results;
223
+ }
124
224
  // ../core/src/loop/gameLoop.ts
125
225
  class GameLoop {
126
226
  onTick;
@@ -532,6 +632,24 @@ class InputManager {
532
632
  return this.keyboard.isReleased(key);
533
633
  }
534
634
  }
635
+ // ../input/src/inputMap.ts
636
+ function createInputMap(bindings) {
637
+ const normalized = {};
638
+ for (const [action, keys] of Object.entries(bindings)) {
639
+ normalized[action] = Array.isArray(keys) ? keys : [keys];
640
+ }
641
+ return {
642
+ isActionDown(input, action) {
643
+ return (normalized[action] ?? []).some((k) => input.isDown(k));
644
+ },
645
+ isActionPressed(input, action) {
646
+ return (normalized[action] ?? []).some((k) => input.isPressed(k));
647
+ },
648
+ isActionReleased(input, action) {
649
+ return (normalized[action] ?? []).some((k) => input.isReleased(k));
650
+ }
651
+ };
652
+ }
535
653
  // ../renderer/src/canvas2d.ts
536
654
  class Canvas2DRenderer {
537
655
  canvas;
@@ -583,6 +701,8 @@ function createCamera2D(opts) {
583
701
  zoom: 1,
584
702
  smoothing: 0,
585
703
  background: "#1a1a2e",
704
+ followOffsetX: 0,
705
+ followOffsetY: 0,
586
706
  shakeIntensity: 0,
587
707
  shakeDuration: 0,
588
708
  shakeTimer: 0,
@@ -639,25 +759,27 @@ class RenderSystem {
639
759
  if (targetId !== undefined) {
640
760
  const targetTransform = world2.getComponent(targetId, "Transform");
641
761
  if (targetTransform) {
762
+ const tx = targetTransform.x + (cam.followOffsetX ?? 0);
763
+ const ty = targetTransform.y + (cam.followOffsetY ?? 0);
642
764
  if (cam.deadZone) {
643
765
  const halfW = cam.deadZone.w / 2;
644
766
  const halfH = cam.deadZone.h / 2;
645
- const dx = targetTransform.x - cam.x;
646
- const dy = targetTransform.y - cam.y;
767
+ const dx = tx - cam.x;
768
+ const dy = ty - cam.y;
647
769
  if (dx > halfW)
648
- cam.x = targetTransform.x - halfW;
770
+ cam.x = tx - halfW;
649
771
  else if (dx < -halfW)
650
- cam.x = targetTransform.x + halfW;
772
+ cam.x = tx + halfW;
651
773
  if (dy > halfH)
652
- cam.y = targetTransform.y - halfH;
774
+ cam.y = ty - halfH;
653
775
  else if (dy < -halfH)
654
- cam.y = targetTransform.y + halfH;
776
+ cam.y = ty + halfH;
655
777
  } else if (cam.smoothing > 0) {
656
- cam.x += (targetTransform.x - cam.x) * (1 - cam.smoothing);
657
- cam.y += (targetTransform.y - cam.y) * (1 - cam.smoothing);
778
+ cam.x += (tx - cam.x) * (1 - cam.smoothing);
779
+ cam.y += (ty - cam.y) * (1 - cam.smoothing);
658
780
  } else {
659
- cam.x = targetTransform.x;
660
- cam.y = targetTransform.y;
781
+ cam.x = tx;
782
+ cam.y = ty;
661
783
  }
662
784
  }
663
785
  }
@@ -673,8 +795,8 @@ class RenderSystem {
673
795
  if (cam.shakeTimer < 0)
674
796
  cam.shakeTimer = 0;
675
797
  const progress = cam.shakeDuration > 0 ? cam.shakeTimer / cam.shakeDuration : 0;
676
- shakeX = (Math.random() * 2 - 1) * cam.shakeIntensity * progress;
677
- shakeY = (Math.random() * 2 - 1) * cam.shakeIntensity * progress;
798
+ shakeX = (world2.rng() * 2 - 1) * cam.shakeIntensity * progress;
799
+ shakeY = (world2.rng() * 2 - 1) * cam.shakeIntensity * progress;
678
800
  }
679
801
  camX = cam.x;
680
802
  camY = cam.y;
@@ -691,7 +813,16 @@ class RenderSystem {
691
813
  anim.timer -= frameDuration;
692
814
  anim.currentIndex++;
693
815
  if (anim.currentIndex >= anim.frames.length) {
694
- anim.currentIndex = anim.loop ? 0 : anim.frames.length - 1;
816
+ if (anim.loop) {
817
+ anim.currentIndex = 0;
818
+ } else {
819
+ anim.currentIndex = anim.frames.length - 1;
820
+ anim.playing = false;
821
+ if (anim.onComplete && !anim._completed) {
822
+ anim._completed = true;
823
+ anim.onComplete();
824
+ }
825
+ }
695
826
  }
696
827
  }
697
828
  sprite.frameIndex = anim.frames[anim.currentIndex];
@@ -755,10 +886,27 @@ class RenderSystem {
755
886
  ctx.translate(canvas.width / 2 - camX * zoom + shakeX, canvas.height / 2 - camY * zoom + shakeY);
756
887
  ctx.scale(zoom, zoom);
757
888
  const renderables = world2.query("Transform", "Sprite");
889
+ const textureKey = (id) => {
890
+ const sprite = world2.getComponent(id, "Sprite");
891
+ if (sprite.image && sprite.image.src)
892
+ return sprite.image.src;
893
+ if (sprite.src)
894
+ return sprite.src;
895
+ return `__color__:${sprite.color}`;
896
+ };
758
897
  renderables.sort((a, b) => {
759
- const za = world2.getComponent(a, "Sprite").zIndex;
760
- const zb = world2.getComponent(b, "Sprite").zIndex;
761
- return za - zb;
898
+ const sa = world2.getComponent(a, "Sprite");
899
+ const sb = world2.getComponent(b, "Sprite");
900
+ const zDiff = sa.zIndex - sb.zIndex;
901
+ if (zDiff !== 0)
902
+ return zDiff;
903
+ const ka = textureKey(a);
904
+ const kb = textureKey(b);
905
+ if (ka < kb)
906
+ return -1;
907
+ if (ka > kb)
908
+ return 1;
909
+ return 0;
762
910
  });
763
911
  for (const id of renderables) {
764
912
  const transform2 = world2.getComponent(id, "Transform");
@@ -809,8 +957,8 @@ class RenderSystem {
809
957
  const spawnCount = Math.floor(pool.timer * pool.rate);
810
958
  pool.timer -= spawnCount / pool.rate;
811
959
  for (let i = 0;i < spawnCount && pool.particles.length < pool.maxParticles; i++) {
812
- const angle = pool.angle + (Math.random() - 0.5) * pool.spread;
813
- const speed = pool.speed * (0.5 + Math.random() * 0.5);
960
+ const angle = pool.angle + (world2.rng() - 0.5) * pool.spread;
961
+ const speed = pool.speed * (0.5 + world2.rng() * 0.5);
814
962
  pool.particles.push({
815
963
  x: t.x,
816
964
  y: t.y,
@@ -868,6 +1016,8 @@ function createRigidBody(opts) {
868
1016
  isNearGround: false,
869
1017
  bounce: 0,
870
1018
  friction: 0.85,
1019
+ lockX: false,
1020
+ lockY: false,
871
1021
  ...opts
872
1022
  };
873
1023
  }
@@ -881,7 +1031,22 @@ function createBoxCollider(width, height, opts) {
881
1031
  offsetY: 0,
882
1032
  isTrigger: false,
883
1033
  layer: "default",
1034
+ mask: "*",
884
1035
  slope: 0,
1036
+ oneWay: false,
1037
+ ...opts
1038
+ };
1039
+ }
1040
+ // ../physics/src/components/circleCollider.ts
1041
+ function createCircleCollider(radius, opts) {
1042
+ return {
1043
+ type: "CircleCollider",
1044
+ radius,
1045
+ offsetX: 0,
1046
+ offsetY: 0,
1047
+ isTrigger: false,
1048
+ layer: "default",
1049
+ mask: "*",
885
1050
  ...opts
886
1051
  };
887
1052
  }
@@ -919,12 +1084,27 @@ function getSlopeSurfaceY(st, sc, worldX) {
919
1084
  const angleRad = sc.slope * (Math.PI / 180);
920
1085
  return cy - hh + dx * Math.tan(angleRad);
921
1086
  }
1087
+ function maskAllows(mask, layer) {
1088
+ if (mask === "*")
1089
+ return true;
1090
+ return Array.isArray(mask) && mask.includes(layer);
1091
+ }
1092
+ function canInteract(a, b) {
1093
+ return maskAllows(a.mask, b.layer) && maskAllows(b.mask, a.layer);
1094
+ }
1095
+ function pairKey(a, b) {
1096
+ return a < b ? `${a}:${b}` : `${b}:${a}`;
1097
+ }
922
1098
 
923
1099
  class PhysicsSystem {
924
1100
  gravity;
925
1101
  events;
926
1102
  accumulator = 0;
927
1103
  FIXED_DT = 1 / 60;
1104
+ activeTriggerPairs = new Map;
1105
+ activeCollisionPairs = new Map;
1106
+ activeCirclePairs = new Map;
1107
+ staticPrevPos = new Map;
928
1108
  constructor(gravity, events) {
929
1109
  this.gravity = gravity;
930
1110
  this.events = events;
@@ -965,6 +1145,30 @@ class PhysicsSystem {
965
1145
  else
966
1146
  dynamics.push(id);
967
1147
  }
1148
+ for (const [key, [a, b]] of this.activeTriggerPairs) {
1149
+ if (!world2.hasEntity(a) || !world2.hasEntity(b)) {
1150
+ this.events?.emit("triggerExit", { a, b });
1151
+ this.activeTriggerPairs.delete(key);
1152
+ }
1153
+ }
1154
+ for (const [key, [a, b]] of this.activeCollisionPairs) {
1155
+ if (!world2.hasEntity(a) || !world2.hasEntity(b)) {
1156
+ this.events?.emit("collisionExit", { a, b });
1157
+ this.activeCollisionPairs.delete(key);
1158
+ }
1159
+ }
1160
+ const staticDelta = new Map;
1161
+ for (const sid of statics) {
1162
+ const st = world2.getComponent(sid, "Transform");
1163
+ const prev = this.staticPrevPos.get(sid);
1164
+ if (prev)
1165
+ staticDelta.set(sid, { dx: st.x - prev.x, dy: st.y - prev.y });
1166
+ this.staticPrevPos.set(sid, { x: st.x, y: st.y });
1167
+ }
1168
+ for (const sid of this.staticPrevPos.keys()) {
1169
+ if (!world2.hasEntity(sid))
1170
+ this.staticPrevPos.delete(sid);
1171
+ }
968
1172
  const staticGrid = new Map;
969
1173
  for (const sid of statics) {
970
1174
  const st = world2.getComponent(sid, "Transform");
@@ -983,7 +1187,12 @@ class PhysicsSystem {
983
1187
  const rb = world2.getComponent(id, "RigidBody");
984
1188
  rb.onGround = false;
985
1189
  rb.isNearGround = false;
986
- rb.vy += this.gravity * rb.gravityScale * dt;
1190
+ if (!rb.lockY)
1191
+ rb.vy += this.gravity * rb.gravityScale * dt;
1192
+ if (rb.lockX)
1193
+ rb.vx = 0;
1194
+ if (rb.lockY)
1195
+ rb.vy = 0;
987
1196
  }
988
1197
  for (const id of dynamics) {
989
1198
  const transform2 = world2.getComponent(id, "Transform");
@@ -1008,6 +1217,8 @@ class PhysicsSystem {
1008
1217
  continue;
1009
1218
  if (sc.slope !== 0)
1010
1219
  continue;
1220
+ if (!canInteract(col, sc))
1221
+ continue;
1011
1222
  const ov = getOverlap(getAABB(transform2, col), getAABB(st, sc));
1012
1223
  if (!ov)
1013
1224
  continue;
@@ -1040,6 +1251,8 @@ class PhysicsSystem {
1040
1251
  const sc = world2.getComponent(sid, "BoxCollider");
1041
1252
  if (sc.isTrigger)
1042
1253
  continue;
1254
+ if (!canInteract(col, sc))
1255
+ continue;
1043
1256
  if (sc.slope !== 0) {
1044
1257
  const ov2 = getOverlap(getAABB(transform2, col), getAABB(st, sc));
1045
1258
  if (!ov2)
@@ -1060,11 +1273,25 @@ class PhysicsSystem {
1060
1273
  if (!ov)
1061
1274
  continue;
1062
1275
  if (Math.abs(ov.y) <= Math.abs(ov.x)) {
1276
+ if (sc.oneWay) {
1277
+ if (ov.y >= 0)
1278
+ continue;
1279
+ const platformTop = st.y + sc.offsetY - sc.height / 2;
1280
+ const prevEntityBottom = transform2.y - rb.vy * dt + col.offsetY + col.height / 2;
1281
+ if (prevEntityBottom > platformTop)
1282
+ continue;
1283
+ }
1063
1284
  transform2.y += ov.y;
1064
1285
  if (ov.y < 0) {
1065
1286
  rb.onGround = true;
1066
1287
  if (rb.friction < 1)
1067
1288
  rb.vx *= rb.friction;
1289
+ const delta = staticDelta.get(sid);
1290
+ if (delta) {
1291
+ transform2.x += delta.dx;
1292
+ if (delta.dy < 0)
1293
+ transform2.y += delta.dy;
1294
+ }
1068
1295
  }
1069
1296
  rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
1070
1297
  }
@@ -1072,6 +1299,7 @@ class PhysicsSystem {
1072
1299
  }
1073
1300
  }
1074
1301
  }
1302
+ const currentCollisionPairs = new Map;
1075
1303
  for (let i = 0;i < dynamics.length; i++) {
1076
1304
  for (let j = i + 1;j < dynamics.length; j++) {
1077
1305
  const ia = dynamics[i];
@@ -1083,10 +1311,10 @@ class PhysicsSystem {
1083
1311
  const ov = getOverlap(getAABB(ta, ca), getAABB(tb, cb));
1084
1312
  if (!ov)
1085
1313
  continue;
1086
- if (ca.isTrigger || cb.isTrigger) {
1087
- this.events?.emit("trigger", { a: ia, b: ib });
1314
+ if (!canInteract(ca, cb))
1315
+ continue;
1316
+ if (ca.isTrigger || cb.isTrigger)
1088
1317
  continue;
1089
- }
1090
1318
  const rba = world2.getComponent(ia, "RigidBody");
1091
1319
  const rbb = world2.getComponent(ib, "RigidBody");
1092
1320
  if (Math.abs(ov.y) <= Math.abs(ov.x)) {
@@ -1108,9 +1336,22 @@ class PhysicsSystem {
1108
1336
  ta.y += ov.y / 2;
1109
1337
  tb.x -= ov.x / 2;
1110
1338
  tb.y -= ov.y / 2;
1111
- this.events?.emit("collision", { a: ia, b: ib });
1339
+ const key = pairKey(ia, ib);
1340
+ currentCollisionPairs.set(key, [ia, ib]);
1112
1341
  }
1113
1342
  }
1343
+ for (const [key, [a, b]] of currentCollisionPairs) {
1344
+ if (!this.activeCollisionPairs.has(key)) {
1345
+ this.events?.emit("collisionEnter", { a, b });
1346
+ }
1347
+ this.events?.emit("collision", { a, b });
1348
+ }
1349
+ for (const [key, [a, b]] of this.activeCollisionPairs) {
1350
+ if (!currentCollisionPairs.has(key)) {
1351
+ this.events?.emit("collisionExit", { a, b });
1352
+ }
1353
+ }
1354
+ this.activeCollisionPairs = currentCollisionPairs;
1114
1355
  for (const id of dynamics) {
1115
1356
  const rb = world2.getComponent(id, "RigidBody");
1116
1357
  if (rb.onGround) {
@@ -1140,6 +1381,8 @@ class PhysicsSystem {
1140
1381
  const sc = world2.getComponent(sid, "BoxCollider");
1141
1382
  if (sc.isTrigger)
1142
1383
  continue;
1384
+ if (!canInteract(col, sc))
1385
+ continue;
1143
1386
  const ov = getOverlap(probeAABB, getAABB(st, sc));
1144
1387
  if (ov && Math.abs(ov.y) <= Math.abs(ov.x) && ov.y < 0) {
1145
1388
  rb.isNearGround = true;
@@ -1148,7 +1391,194 @@ class PhysicsSystem {
1148
1391
  }
1149
1392
  }
1150
1393
  }
1394
+ const allWithCollider = world2.query("Transform", "BoxCollider");
1395
+ const currentTriggerPairs = new Map;
1396
+ for (let i = 0;i < allWithCollider.length; i++) {
1397
+ for (let j = i + 1;j < allWithCollider.length; j++) {
1398
+ const ia = allWithCollider[i];
1399
+ const ib = allWithCollider[j];
1400
+ const ca = world2.getComponent(ia, "BoxCollider");
1401
+ const cb = world2.getComponent(ib, "BoxCollider");
1402
+ if (!ca.isTrigger && !cb.isTrigger)
1403
+ continue;
1404
+ if (!canInteract(ca, cb))
1405
+ continue;
1406
+ const ta = world2.getComponent(ia, "Transform");
1407
+ const tb = world2.getComponent(ib, "Transform");
1408
+ const ov = getOverlap(getAABB(ta, ca), getAABB(tb, cb));
1409
+ if (!ov)
1410
+ continue;
1411
+ const key = pairKey(ia, ib);
1412
+ currentTriggerPairs.set(key, [ia, ib]);
1413
+ }
1414
+ }
1415
+ for (const [key, [a, b]] of currentTriggerPairs) {
1416
+ if (!this.activeTriggerPairs.has(key)) {
1417
+ this.events?.emit("triggerEnter", { a, b });
1418
+ }
1419
+ this.events?.emit("trigger", { a, b });
1420
+ }
1421
+ for (const [key, [a, b]] of this.activeTriggerPairs) {
1422
+ if (!currentTriggerPairs.has(key)) {
1423
+ this.events?.emit("triggerExit", { a, b });
1424
+ }
1425
+ }
1426
+ this.activeTriggerPairs = currentTriggerPairs;
1427
+ const allCircles = world2.query("Transform", "CircleCollider");
1428
+ if (allCircles.length > 0) {
1429
+ const currentCirclePairs = new Map;
1430
+ for (let i = 0;i < allCircles.length; i++) {
1431
+ for (let j = i + 1;j < allCircles.length; j++) {
1432
+ const ia = allCircles[i];
1433
+ const ib = allCircles[j];
1434
+ const ca = world2.getComponent(ia, "CircleCollider");
1435
+ const cb = world2.getComponent(ib, "CircleCollider");
1436
+ if (!maskAllows(ca.mask, cb.layer) || !maskAllows(cb.mask, ca.layer))
1437
+ continue;
1438
+ const ta = world2.getComponent(ia, "Transform");
1439
+ const tb = world2.getComponent(ib, "Transform");
1440
+ const dx = ta.x + ca.offsetX - (tb.x + cb.offsetX);
1441
+ const dy = ta.y + ca.offsetY - (tb.y + cb.offsetY);
1442
+ if (dx * dx + dy * dy < (ca.radius + cb.radius) ** 2) {
1443
+ currentCirclePairs.set(pairKey(ia, ib), [ia, ib]);
1444
+ }
1445
+ }
1446
+ }
1447
+ const allBoxes = world2.query("Transform", "BoxCollider");
1448
+ for (const cid of allCircles) {
1449
+ const cc = world2.getComponent(cid, "CircleCollider");
1450
+ const ct = world2.getComponent(cid, "Transform");
1451
+ const cx = ct.x + cc.offsetX;
1452
+ const cy = ct.y + cc.offsetY;
1453
+ for (const bid of allBoxes) {
1454
+ if (bid === cid)
1455
+ continue;
1456
+ const bc = world2.getComponent(bid, "BoxCollider");
1457
+ if (!maskAllows(cc.mask, bc.layer) || !maskAllows(bc.mask, cc.layer))
1458
+ continue;
1459
+ const bt = world2.getComponent(bid, "Transform");
1460
+ const bx = bt.x + bc.offsetX;
1461
+ const by = bt.y + bc.offsetY;
1462
+ const nearX = Math.max(bx - bc.width / 2, Math.min(cx, bx + bc.width / 2));
1463
+ const nearY = Math.max(by - bc.height / 2, Math.min(cy, by + bc.height / 2));
1464
+ const dx = cx - nearX;
1465
+ const dy = cy - nearY;
1466
+ if (dx * dx + dy * dy < cc.radius * cc.radius) {
1467
+ currentCirclePairs.set(pairKey(cid, bid), [cid, bid]);
1468
+ }
1469
+ }
1470
+ }
1471
+ for (const [key, [a, b]] of currentCirclePairs) {
1472
+ if (!this.activeCirclePairs.has(key)) {
1473
+ this.events?.emit("circleEnter", { a, b });
1474
+ }
1475
+ this.events?.emit("circle", { a, b });
1476
+ }
1477
+ for (const [key, [a, b]] of this.activeCirclePairs) {
1478
+ if (!currentCirclePairs.has(key)) {
1479
+ this.events?.emit("circleExit", { a, b });
1480
+ }
1481
+ }
1482
+ this.activeCirclePairs = currentCirclePairs;
1483
+ }
1484
+ }
1485
+ }
1486
+ // ../physics/src/queries.ts
1487
+ function passesFilter(world2, id, col, opts) {
1488
+ if (opts.exclude?.includes(id))
1489
+ return false;
1490
+ if (opts.layer && col.layer !== opts.layer)
1491
+ return false;
1492
+ if (opts.tag) {
1493
+ const t = world2.getComponent(id, "Tag");
1494
+ if (!t?.tags.includes(opts.tag))
1495
+ return false;
1496
+ }
1497
+ return true;
1498
+ }
1499
+ function overlapBox(world2, cx, cy, hw, hh, opts = {}) {
1500
+ const results = [];
1501
+ for (const id of world2.query("Transform", "BoxCollider")) {
1502
+ const t = world2.getComponent(id, "Transform");
1503
+ const c = world2.getComponent(id, "BoxCollider");
1504
+ if (!passesFilter(world2, id, c, opts))
1505
+ continue;
1506
+ const ecx = t.x + c.offsetX;
1507
+ const ecy = t.y + c.offsetY;
1508
+ const ehw = c.width / 2;
1509
+ const ehh = c.height / 2;
1510
+ if (Math.abs(ecx - cx) < hw + ehw && Math.abs(ecy - cy) < hh + ehh) {
1511
+ results.push(id);
1512
+ }
1151
1513
  }
1514
+ return results;
1515
+ }
1516
+ function raycast(world2, origin, direction, maxDistance, opts = {}) {
1517
+ const len = Math.hypot(direction.x, direction.y);
1518
+ if (len === 0)
1519
+ return null;
1520
+ const dx = direction.x / len;
1521
+ const dy = direction.y / len;
1522
+ let closest = null;
1523
+ for (const id of world2.query("Transform", "BoxCollider")) {
1524
+ const t = world2.getComponent(id, "Transform");
1525
+ const c = world2.getComponent(id, "BoxCollider");
1526
+ if (!opts.includeTriggers && c.isTrigger)
1527
+ continue;
1528
+ if (!passesFilter(world2, id, c, opts))
1529
+ continue;
1530
+ const cx = t.x + c.offsetX;
1531
+ const cy = t.y + c.offsetY;
1532
+ const hw = c.width / 2;
1533
+ const hh = c.height / 2;
1534
+ const left = cx - hw;
1535
+ const right = cx + hw;
1536
+ const top = cy - hh;
1537
+ const bottom = cy + hh;
1538
+ let tmin = -Infinity;
1539
+ let tmax = Infinity;
1540
+ if (dx !== 0) {
1541
+ const t1 = (left - origin.x) / dx;
1542
+ const t2 = (right - origin.x) / dx;
1543
+ tmin = Math.max(tmin, Math.min(t1, t2));
1544
+ tmax = Math.min(tmax, Math.max(t1, t2));
1545
+ } else if (origin.x < left || origin.x > right) {
1546
+ continue;
1547
+ }
1548
+ if (dy !== 0) {
1549
+ const t1 = (top - origin.y) / dy;
1550
+ const t2 = (bottom - origin.y) / dy;
1551
+ tmin = Math.max(tmin, Math.min(t1, t2));
1552
+ tmax = Math.min(tmax, Math.max(t1, t2));
1553
+ } else if (origin.y < top || origin.y > bottom) {
1554
+ continue;
1555
+ }
1556
+ if (tmax < 0 || tmin > tmax || tmin > maxDistance)
1557
+ continue;
1558
+ const dist = Math.max(0, tmin);
1559
+ if (closest && dist >= closest.distance)
1560
+ continue;
1561
+ const hitX = origin.x + dx * tmin;
1562
+ const hitY = origin.y + dy * tmin;
1563
+ let nx = 0;
1564
+ let ny = 0;
1565
+ const edgeEps = 0.001;
1566
+ if (Math.abs(hitX - left) < edgeEps)
1567
+ nx = -1;
1568
+ else if (Math.abs(hitX - right) < edgeEps)
1569
+ nx = 1;
1570
+ else if (Math.abs(hitY - top) < edgeEps)
1571
+ ny = -1;
1572
+ else if (Math.abs(hitY - bottom) < edgeEps)
1573
+ ny = 1;
1574
+ closest = {
1575
+ entityId: id,
1576
+ distance: dist,
1577
+ point: { x: hitX, y: hitY },
1578
+ normal: { x: nx, y: ny }
1579
+ };
1580
+ }
1581
+ return closest;
1152
1582
  }
1153
1583
  // src/context.ts
1154
1584
  import { createContext } from "react";
@@ -1253,34 +1683,350 @@ class DebugSystem {
1253
1683
  }
1254
1684
  }
1255
1685
 
1686
+ // src/components/DevTools.tsx
1687
+ import React from "react";
1688
+ import { createPortal } from "react-dom";
1689
+ import { useState, useEffect, useCallback } from "react";
1690
+ import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
1691
+ var MAX_DEVTOOLS_FRAMES = 600;
1692
+ var css = {
1693
+ overlay: {
1694
+ position: "fixed",
1695
+ bottom: 0,
1696
+ left: 0,
1697
+ right: 0,
1698
+ zIndex: 99999,
1699
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Courier New', monospace",
1700
+ fontSize: 11,
1701
+ color: "#cdd6f4",
1702
+ userSelect: "none",
1703
+ pointerEvents: "auto"
1704
+ },
1705
+ bar: {
1706
+ background: "rgba(11,13,20,0.97)",
1707
+ borderTop: "1px solid #2a3048",
1708
+ padding: "6px 12px",
1709
+ display: "flex",
1710
+ alignItems: "center",
1711
+ gap: 10,
1712
+ height: 40
1713
+ },
1714
+ badge: {
1715
+ fontSize: 9,
1716
+ fontWeight: 700,
1717
+ letterSpacing: "0.1em",
1718
+ color: "#4fc3f7",
1719
+ background: "rgba(79,195,247,0.1)",
1720
+ border: "1px solid rgba(79,195,247,0.2)",
1721
+ borderRadius: 4,
1722
+ padding: "2px 6px",
1723
+ flexShrink: 0
1724
+ },
1725
+ btn: (active = false, danger = false) => ({
1726
+ background: active ? "rgba(79,195,247,0.15)" : "rgba(255,255,255,0.04)",
1727
+ border: `1px solid ${active ? "rgba(79,195,247,0.3)" : "#2a3048"}`,
1728
+ borderRadius: 4,
1729
+ color: danger ? "#f38ba8" : active ? "#4fc3f7" : "#6b7a9e",
1730
+ cursor: "pointer",
1731
+ padding: "3px 8px",
1732
+ fontSize: 10,
1733
+ fontFamily: "inherit",
1734
+ display: "flex",
1735
+ alignItems: "center",
1736
+ gap: 4,
1737
+ transition: "all 0.1s",
1738
+ flexShrink: 0
1739
+ }),
1740
+ scrubber: {
1741
+ flex: 1,
1742
+ accentColor: "#4fc3f7",
1743
+ cursor: "pointer",
1744
+ height: 4
1745
+ },
1746
+ counter: {
1747
+ color: "#6b7a9e",
1748
+ fontSize: 10,
1749
+ whiteSpace: "nowrap",
1750
+ flexShrink: 0,
1751
+ minWidth: 80,
1752
+ textAlign: "right"
1753
+ },
1754
+ panel: {
1755
+ background: "rgba(11,13,20,0.97)",
1756
+ borderTop: "1px solid #1f2435",
1757
+ maxHeight: 260,
1758
+ overflowY: "auto",
1759
+ padding: "8px 0"
1760
+ },
1761
+ entityRow: (selected) => ({
1762
+ display: "flex",
1763
+ alignItems: "center",
1764
+ gap: 8,
1765
+ padding: "4px 12px",
1766
+ cursor: "pointer",
1767
+ background: selected ? "rgba(79,195,247,0.06)" : "transparent",
1768
+ borderLeft: `2px solid ${selected ? "#4fc3f7" : "transparent"}`
1769
+ }),
1770
+ entityId: {
1771
+ color: "#4fc3f7",
1772
+ minWidth: 28,
1773
+ fontSize: 10
1774
+ },
1775
+ compPill: {
1776
+ fontSize: 9,
1777
+ background: "rgba(79,195,247,0.08)",
1778
+ border: "1px solid rgba(79,195,247,0.12)",
1779
+ borderRadius: 3,
1780
+ padding: "1px 5px",
1781
+ color: "#6b7a9e"
1782
+ },
1783
+ detailPanel: {
1784
+ background: "rgba(18,21,31,0.98)",
1785
+ borderTop: "1px solid #1f2435",
1786
+ padding: "10px 14px",
1787
+ maxHeight: 200,
1788
+ overflowY: "auto"
1789
+ },
1790
+ kv: {
1791
+ display: "grid",
1792
+ gridTemplateColumns: "140px 1fr",
1793
+ gap: "2px 12px",
1794
+ lineHeight: 1.8
1795
+ },
1796
+ key: { color: "#6b7a9e" },
1797
+ val: { color: "#cdd6f4" }
1798
+ };
1799
+ function DevToolsOverlay({ handle, loop, ecs }) {
1800
+ const [, forceUpdate] = useState(0);
1801
+ const [paused, setPaused] = useState(false);
1802
+ const [selectedIdx, setSelectedIdx] = useState(0);
1803
+ const [panelOpen, setPanelOpen] = useState(false);
1804
+ const [selectedEntity, setSelectedEntity] = useState(null);
1805
+ useEffect(() => {
1806
+ handle.onFrame = () => {
1807
+ if (!paused) {
1808
+ forceUpdate((n) => n + 1);
1809
+ setSelectedIdx(Math.max(0, handle.buffer.length - 1));
1810
+ }
1811
+ };
1812
+ return () => {
1813
+ handle.onFrame = undefined;
1814
+ };
1815
+ }, [handle, paused]);
1816
+ const totalFrames = handle.buffer.length;
1817
+ const currentSnap = handle.buffer[selectedIdx];
1818
+ const handlePauseResume = useCallback(() => {
1819
+ if (paused) {
1820
+ if (currentSnap)
1821
+ ecs.restoreSnapshot(currentSnap);
1822
+ loop.resume();
1823
+ setPaused(false);
1824
+ setSelectedEntity(null);
1825
+ } else {
1826
+ loop.pause();
1827
+ setPaused(true);
1828
+ setSelectedIdx(Math.max(0, handle.buffer.length - 1));
1829
+ }
1830
+ }, [paused, currentSnap, ecs, loop, handle]);
1831
+ const stepBack = useCallback(() => {
1832
+ setSelectedIdx((i) => Math.max(0, i - 1));
1833
+ setSelectedEntity(null);
1834
+ }, []);
1835
+ const stepForward = useCallback(() => {
1836
+ setSelectedIdx((i) => Math.min(handle.buffer.length - 1, i + 1));
1837
+ setSelectedEntity(null);
1838
+ }, [handle]);
1839
+ const frameLabel = totalFrames === 0 ? "0 / 0" : `${selectedIdx + 1} / ${totalFrames}`;
1840
+ const entities = currentSnap?.entities ?? [];
1841
+ const selectedEntityData = selectedEntity !== null ? entities.find((e) => e.id === selectedEntity) : null;
1842
+ return createPortal(/* @__PURE__ */ jsxDEV("div", {
1843
+ style: css.overlay,
1844
+ children: [
1845
+ panelOpen && paused && /* @__PURE__ */ jsxDEV(Fragment, {
1846
+ children: [
1847
+ /* @__PURE__ */ jsxDEV("div", {
1848
+ style: css.panel,
1849
+ children: [
1850
+ entities.length === 0 && /* @__PURE__ */ jsxDEV("div", {
1851
+ style: { padding: "4px 14px", color: "#3d4666" },
1852
+ children: "No entities"
1853
+ }, undefined, false, undefined, this),
1854
+ entities.map((e) => /* @__PURE__ */ jsxDEV("div", {
1855
+ style: css.entityRow(selectedEntity === e.id),
1856
+ onClick: () => setSelectedEntity((s) => s === e.id ? null : e.id),
1857
+ children: [
1858
+ /* @__PURE__ */ jsxDEV("span", {
1859
+ style: css.entityId,
1860
+ children: [
1861
+ "#",
1862
+ e.id
1863
+ ]
1864
+ }, undefined, true, undefined, this),
1865
+ /* @__PURE__ */ jsxDEV("div", {
1866
+ style: { display: "flex", gap: 4, flexWrap: "wrap" },
1867
+ children: e.components.map((c) => /* @__PURE__ */ jsxDEV("span", {
1868
+ style: css.compPill,
1869
+ children: c.type
1870
+ }, c.type, false, undefined, this))
1871
+ }, undefined, false, undefined, this)
1872
+ ]
1873
+ }, e.id, true, undefined, this))
1874
+ ]
1875
+ }, undefined, true, undefined, this),
1876
+ selectedEntityData && /* @__PURE__ */ jsxDEV("div", {
1877
+ style: css.detailPanel,
1878
+ children: selectedEntityData.components.map((comp) => /* @__PURE__ */ jsxDEV("div", {
1879
+ style: { marginBottom: 10 },
1880
+ children: [
1881
+ /* @__PURE__ */ jsxDEV("div", {
1882
+ style: { color: "#4fc3f7", fontSize: 10, fontWeight: 700, marginBottom: 4 },
1883
+ children: comp.type
1884
+ }, undefined, false, undefined, this),
1885
+ /* @__PURE__ */ jsxDEV("div", {
1886
+ style: css.kv,
1887
+ children: Object.entries(comp).filter(([k]) => k !== "type").map(([k, v]) => /* @__PURE__ */ jsxDEV(React.Fragment, {
1888
+ children: [
1889
+ /* @__PURE__ */ jsxDEV("span", {
1890
+ style: css.key,
1891
+ children: k
1892
+ }, undefined, false, undefined, this),
1893
+ /* @__PURE__ */ jsxDEV("span", {
1894
+ style: css.val,
1895
+ children: formatValue(v)
1896
+ }, undefined, false, undefined, this)
1897
+ ]
1898
+ }, k, true, undefined, this))
1899
+ }, undefined, false, undefined, this)
1900
+ ]
1901
+ }, comp.type, true, undefined, this))
1902
+ }, undefined, false, undefined, this)
1903
+ ]
1904
+ }, undefined, true, undefined, this),
1905
+ /* @__PURE__ */ jsxDEV("div", {
1906
+ style: css.bar,
1907
+ children: [
1908
+ /* @__PURE__ */ jsxDEV("span", {
1909
+ style: css.badge,
1910
+ children: "DEVTOOLS"
1911
+ }, undefined, false, undefined, this),
1912
+ /* @__PURE__ */ jsxDEV("button", {
1913
+ style: css.btn(paused),
1914
+ onClick: handlePauseResume,
1915
+ children: paused ? "▶ Resume" : "⏸ Pause"
1916
+ }, undefined, false, undefined, this),
1917
+ paused && /* @__PURE__ */ jsxDEV(Fragment, {
1918
+ children: [
1919
+ /* @__PURE__ */ jsxDEV("button", {
1920
+ style: css.btn(),
1921
+ onClick: stepBack,
1922
+ children: "◀◀"
1923
+ }, undefined, false, undefined, this),
1924
+ /* @__PURE__ */ jsxDEV("button", {
1925
+ style: css.btn(),
1926
+ onClick: stepForward,
1927
+ children: "▶▶"
1928
+ }, undefined, false, undefined, this)
1929
+ ]
1930
+ }, undefined, true, undefined, this),
1931
+ /* @__PURE__ */ jsxDEV("input", {
1932
+ type: "range",
1933
+ min: 0,
1934
+ max: Math.max(0, totalFrames - 1),
1935
+ value: selectedIdx,
1936
+ style: css.scrubber,
1937
+ onChange: (e) => {
1938
+ const idx = Number(e.target.value);
1939
+ setSelectedIdx(idx);
1940
+ setSelectedEntity(null);
1941
+ if (!paused) {
1942
+ loop.pause();
1943
+ setPaused(true);
1944
+ }
1945
+ }
1946
+ }, undefined, false, undefined, this),
1947
+ /* @__PURE__ */ jsxDEV("span", {
1948
+ style: css.counter,
1949
+ children: frameLabel
1950
+ }, undefined, false, undefined, this),
1951
+ paused && /* @__PURE__ */ jsxDEV("button", {
1952
+ style: css.btn(panelOpen),
1953
+ onClick: () => setPanelOpen((o) => !o),
1954
+ children: [
1955
+ panelOpen ? "▾" : "▸",
1956
+ " Entities (",
1957
+ entities.length,
1958
+ ")"
1959
+ ]
1960
+ }, undefined, true, undefined, this),
1961
+ /* @__PURE__ */ jsxDEV("button", {
1962
+ style: css.btn(false, true),
1963
+ onClick: () => {
1964
+ handle.buffer.length = 0;
1965
+ setSelectedIdx(0);
1966
+ forceUpdate((n) => n + 1);
1967
+ },
1968
+ children: "Clear"
1969
+ }, undefined, false, undefined, this)
1970
+ ]
1971
+ }, undefined, true, undefined, this)
1972
+ ]
1973
+ }, undefined, true, undefined, this), document.body);
1974
+ }
1975
+ function formatValue(v) {
1976
+ if (typeof v === "number")
1977
+ return v.toFixed(2);
1978
+ if (typeof v === "boolean")
1979
+ return v ? "true" : "false";
1980
+ if (v === null || v === undefined)
1981
+ return "—";
1982
+ if (typeof v === "object")
1983
+ return JSON.stringify(v).slice(0, 60);
1984
+ return String(v);
1985
+ }
1986
+
1256
1987
  // src/components/Game.tsx
1257
- import { jsxDEV } from "react/jsx-dev-runtime";
1988
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
1258
1989
  function Game({
1259
1990
  width = 800,
1260
1991
  height = 600,
1261
1992
  gravity = 980,
1262
1993
  debug = false,
1994
+ devtools = false,
1263
1995
  scale = "none",
1996
+ deterministic = false,
1997
+ seed = 0,
1264
1998
  onReady,
1265
1999
  plugins,
2000
+ renderer: CustomRenderer,
1266
2001
  style,
1267
2002
  className,
1268
2003
  children
1269
2004
  }) {
1270
2005
  const canvasRef = useRef(null);
1271
2006
  const wrapperRef = useRef(null);
1272
- const [engine, setEngine] = useState(null);
1273
- useEffect(() => {
2007
+ const [engine, setEngine] = useState2(null);
2008
+ const devtoolsHandle = useRef({ buffer: [] });
2009
+ useEffect2(() => {
1274
2010
  const canvas = canvasRef.current;
1275
2011
  const ecs = new ECSWorld;
2012
+ if (deterministic)
2013
+ ecs.setDeterministicSeed(seed);
1276
2014
  const input = new InputManager;
1277
- const renderer = new Canvas2DRenderer(canvas);
1278
2015
  const events = new EventBus;
1279
2016
  const assets = new AssetManager;
1280
2017
  const physics = new PhysicsSystem(gravity, events);
1281
2018
  const entityIds = new Map;
1282
- const renderSystem2 = new RenderSystem(renderer, entityIds);
1283
- const debugSystem = debug ? new DebugSystem(renderer) : null;
2019
+ let canvas2d2;
2020
+ let builtinRenderSystem;
2021
+ let renderSystem2;
2022
+ if (CustomRenderer) {
2023
+ renderSystem2 = new CustomRenderer(canvas, entityIds);
2024
+ } else {
2025
+ canvas2d2 = new Canvas2DRenderer(canvas);
2026
+ builtinRenderSystem = new RenderSystem(canvas2d2, entityIds);
2027
+ renderSystem2 = builtinRenderSystem;
2028
+ }
2029
+ const debugSystem = debug && canvas2d2 ? new DebugSystem(canvas2d2) : null;
1284
2030
  ecs.addSystem(new ScriptSystem(input));
1285
2031
  ecs.addSystem(physics);
1286
2032
  ecs.addSystem(renderSystem2);
@@ -1294,8 +2040,15 @@ function Game({
1294
2040
  const loop = new GameLoop((dt) => {
1295
2041
  ecs.update(dt);
1296
2042
  input.flush();
2043
+ if (devtools) {
2044
+ const handle = devtoolsHandle.current;
2045
+ handle.buffer.push(ecs.getSnapshot());
2046
+ if (handle.buffer.length > MAX_DEVTOOLS_FRAMES)
2047
+ handle.buffer.shift();
2048
+ handle.onFrame?.();
2049
+ }
1297
2050
  });
1298
- const state = { ecs, input, renderer, physics, events, assets, loop, canvas, entityIds };
2051
+ const state = { ecs, input, renderer: canvas2d2, renderSystem: builtinRenderSystem, physics, events, assets, loop, canvas, entityIds };
1299
2052
  setEngine(state);
1300
2053
  if (plugins) {
1301
2054
  for (const plugin2 of plugins) {
@@ -1339,7 +2092,7 @@ function Game({
1339
2092
  resizeObserver?.disconnect();
1340
2093
  };
1341
2094
  }, []);
1342
- useEffect(() => {
2095
+ useEffect2(() => {
1343
2096
  engine?.physics.setGravity(gravity);
1344
2097
  }, [gravity, engine]);
1345
2098
  const canvasStyle = {
@@ -1349,13 +2102,13 @@ function Game({
1349
2102
  ...style
1350
2103
  };
1351
2104
  const wrapperStyle = scale === "contain" ? { position: "relative", width, height, overflow: "visible" } : {};
1352
- return /* @__PURE__ */ jsxDEV(EngineContext.Provider, {
2105
+ return /* @__PURE__ */ jsxDEV2(EngineContext.Provider, {
1353
2106
  value: engine,
1354
2107
  children: [
1355
- /* @__PURE__ */ jsxDEV("div", {
2108
+ /* @__PURE__ */ jsxDEV2("div", {
1356
2109
  ref: wrapperRef,
1357
2110
  style: wrapperStyle,
1358
- children: /* @__PURE__ */ jsxDEV("canvas", {
2111
+ children: /* @__PURE__ */ jsxDEV2("canvas", {
1359
2112
  ref: canvasRef,
1360
2113
  width,
1361
2114
  height,
@@ -1363,22 +2116,27 @@ function Game({
1363
2116
  className
1364
2117
  }, undefined, false, undefined, this)
1365
2118
  }, undefined, false, undefined, this),
1366
- engine && children
2119
+ engine && children,
2120
+ engine && devtools && /* @__PURE__ */ jsxDEV2(DevToolsOverlay, {
2121
+ handle: devtoolsHandle.current,
2122
+ loop: engine.loop,
2123
+ ecs: engine.ecs
2124
+ }, undefined, false, undefined, this)
1367
2125
  ]
1368
2126
  }, undefined, true, undefined, this);
1369
2127
  }
1370
2128
  // src/components/World.tsx
1371
- import { useEffect as useEffect2, useContext } from "react";
1372
- import { jsxDEV as jsxDEV2, Fragment } from "react/jsx-dev-runtime";
2129
+ import { useEffect as useEffect3, useContext } from "react";
2130
+ import { jsxDEV as jsxDEV3, Fragment as Fragment2 } from "react/jsx-dev-runtime";
1373
2131
  function World({ gravity, background = "#1a1a2e", children }) {
1374
2132
  const engine = useContext(EngineContext);
1375
- useEffect2(() => {
2133
+ useEffect3(() => {
1376
2134
  if (!engine)
1377
2135
  return;
1378
2136
  if (gravity !== undefined)
1379
2137
  engine.physics.setGravity(gravity);
1380
2138
  }, [gravity, engine]);
1381
- useEffect2(() => {
2139
+ useEffect3(() => {
1382
2140
  if (!engine)
1383
2141
  return;
1384
2142
  const camId = engine.ecs.queryOne("Camera2D");
@@ -1390,17 +2148,17 @@ function World({ gravity, background = "#1a1a2e", children }) {
1390
2148
  engine.canvas.style.background = background;
1391
2149
  }
1392
2150
  }, [background, engine]);
1393
- return /* @__PURE__ */ jsxDEV2(Fragment, {
2151
+ return /* @__PURE__ */ jsxDEV3(Fragment2, {
1394
2152
  children
1395
2153
  }, undefined, false, undefined, this);
1396
2154
  }
1397
2155
  // src/components/Entity.tsx
1398
- import { useEffect as useEffect3, useContext as useContext2, useState as useState2 } from "react";
1399
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
2156
+ import { useEffect as useEffect4, useContext as useContext2, useState as useState3 } from "react";
2157
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
1400
2158
  function Entity({ id, tags = [], children }) {
1401
2159
  const engine = useContext2(EngineContext);
1402
- const [entityId, setEntityId] = useState2(null);
1403
- useEffect3(() => {
2160
+ const [entityId, setEntityId] = useState3(null);
2161
+ useEffect4(() => {
1404
2162
  const eid = engine.ecs.createEntity();
1405
2163
  if (id) {
1406
2164
  if (engine.entityIds.has(id)) {
@@ -1419,21 +2177,21 @@ function Entity({ id, tags = [], children }) {
1419
2177
  }, []);
1420
2178
  if (entityId === null)
1421
2179
  return null;
1422
- return /* @__PURE__ */ jsxDEV3(EntityContext.Provider, {
2180
+ return /* @__PURE__ */ jsxDEV4(EntityContext.Provider, {
1423
2181
  value: entityId,
1424
2182
  children
1425
2183
  }, undefined, false, undefined, this);
1426
2184
  }
1427
2185
  // src/components/Transform.tsx
1428
- import { useEffect as useEffect4, useContext as useContext3 } from "react";
2186
+ import { useEffect as useEffect5, useContext as useContext3 } from "react";
1429
2187
  function Transform({ x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1 }) {
1430
2188
  const engine = useContext3(EngineContext);
1431
2189
  const entityId = useContext3(EntityContext);
1432
- useEffect4(() => {
2190
+ useEffect5(() => {
1433
2191
  engine.ecs.addComponent(entityId, createTransform(x, y, rotation, scaleX, scaleY));
1434
2192
  return () => engine.ecs.removeComponent(entityId, "Transform");
1435
2193
  }, []);
1436
- useEffect4(() => {
2194
+ useEffect5(() => {
1437
2195
  const comp = engine.ecs.getComponent(entityId, "Transform");
1438
2196
  if (comp) {
1439
2197
  comp.x = x;
@@ -1446,7 +2204,7 @@ function Transform({ x = 0, y = 0, rotation = 0, scaleX = 1, scaleY = 1 }) {
1446
2204
  return null;
1447
2205
  }
1448
2206
  // src/components/Sprite.tsx
1449
- import { useEffect as useEffect5, useContext as useContext4 } from "react";
2207
+ import { useEffect as useEffect6, useContext as useContext4 } from "react";
1450
2208
  function Sprite({
1451
2209
  width,
1452
2210
  height,
@@ -1469,7 +2227,7 @@ function Sprite({
1469
2227
  const resolvedFrameIndex = atlas && frame != null ? atlas[frame] ?? 0 : frameIndex;
1470
2228
  const engine = useContext4(EngineContext);
1471
2229
  const entityId = useContext4(EntityContext);
1472
- useEffect5(() => {
2230
+ useEffect6(() => {
1473
2231
  const comp = createSprite({
1474
2232
  width,
1475
2233
  height,
@@ -1497,7 +2255,7 @@ function Sprite({
1497
2255
  }
1498
2256
  return () => engine.ecs.removeComponent(entityId, "Sprite");
1499
2257
  }, []);
1500
- useEffect5(() => {
2258
+ useEffect6(() => {
1501
2259
  const comp = engine.ecs.getComponent(entityId, "Sprite");
1502
2260
  if (!comp)
1503
2261
  return;
@@ -1510,7 +2268,7 @@ function Sprite({
1510
2268
  return null;
1511
2269
  }
1512
2270
  // src/components/RigidBody.tsx
1513
- import { useEffect as useEffect6, useContext as useContext5 } from "react";
2271
+ import { useEffect as useEffect7, useContext as useContext5 } from "react";
1514
2272
  function RigidBody({
1515
2273
  mass = 1,
1516
2274
  gravityScale = 1,
@@ -1518,30 +2276,34 @@ function RigidBody({
1518
2276
  bounce = 0,
1519
2277
  friction = 0.85,
1520
2278
  vx = 0,
1521
- vy = 0
2279
+ vy = 0,
2280
+ lockX = false,
2281
+ lockY = false
1522
2282
  }) {
1523
2283
  const engine = useContext5(EngineContext);
1524
2284
  const entityId = useContext5(EntityContext);
1525
- useEffect6(() => {
1526
- engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy }));
2285
+ useEffect7(() => {
2286
+ engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY }));
1527
2287
  return () => engine.ecs.removeComponent(entityId, "RigidBody");
1528
2288
  }, []);
1529
2289
  return null;
1530
2290
  }
1531
2291
  // src/components/BoxCollider.tsx
1532
- import { useEffect as useEffect7, useContext as useContext6 } from "react";
2292
+ import { useEffect as useEffect8, useContext as useContext6 } from "react";
1533
2293
  function BoxCollider({
1534
2294
  width,
1535
2295
  height,
1536
2296
  offsetX = 0,
1537
2297
  offsetY = 0,
1538
2298
  isTrigger = false,
1539
- layer = "default"
2299
+ layer = "default",
2300
+ mask = "*",
2301
+ oneWay = false
1540
2302
  }) {
1541
2303
  const engine = useContext6(EngineContext);
1542
2304
  const entityId = useContext6(EntityContext);
1543
- useEffect7(() => {
1544
- engine.ecs.addComponent(entityId, createBoxCollider(width, height, { offsetX, offsetY, isTrigger, layer }));
2305
+ useEffect8(() => {
2306
+ engine.ecs.addComponent(entityId, createBoxCollider(width, height, { offsetX, offsetY, isTrigger, layer, mask, oneWay }));
1545
2307
  const checkId = setTimeout(() => {
1546
2308
  if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "Transform")) {
1547
2309
  console.warn(`[Cubeforge] BoxCollider on entity ${entityId} has no Transform. Physics requires Transform.`);
@@ -1554,12 +2316,30 @@ function BoxCollider({
1554
2316
  }, []);
1555
2317
  return null;
1556
2318
  }
1557
- // src/components/Script.tsx
1558
- import { useEffect as useEffect8, useContext as useContext7 } from "react";
1559
- function Script({ init, update }) {
2319
+ // src/components/CircleCollider.tsx
2320
+ import { useEffect as useEffect9, useContext as useContext7 } from "react";
2321
+ function CircleCollider({
2322
+ radius,
2323
+ offsetX = 0,
2324
+ offsetY = 0,
2325
+ isTrigger = false,
2326
+ layer = "default",
2327
+ mask = "*"
2328
+ }) {
1560
2329
  const engine = useContext7(EngineContext);
1561
2330
  const entityId = useContext7(EntityContext);
1562
- useEffect8(() => {
2331
+ useEffect9(() => {
2332
+ engine.ecs.addComponent(entityId, createCircleCollider(radius, { offsetX, offsetY, isTrigger, layer, mask }));
2333
+ return () => engine.ecs.removeComponent(entityId, "CircleCollider");
2334
+ }, []);
2335
+ return null;
2336
+ }
2337
+ // src/components/Script.tsx
2338
+ import { useEffect as useEffect10, useContext as useContext8 } from "react";
2339
+ function Script({ init, update }) {
2340
+ const engine = useContext8(EngineContext);
2341
+ const entityId = useContext8(EntityContext);
2342
+ useEffect10(() => {
1563
2343
  if (init) {
1564
2344
  try {
1565
2345
  init(entityId, engine.ecs);
@@ -1573,17 +2353,19 @@ function Script({ init, update }) {
1573
2353
  return null;
1574
2354
  }
1575
2355
  // src/components/Camera2D.tsx
1576
- import { useEffect as useEffect9, useContext as useContext8 } from "react";
2356
+ import { useEffect as useEffect11, useContext as useContext9 } from "react";
1577
2357
  function Camera2D({
1578
2358
  followEntity,
1579
2359
  zoom = 1,
1580
2360
  smoothing = 0,
1581
2361
  background = "#1a1a2e",
1582
2362
  bounds,
1583
- deadZone
2363
+ deadZone,
2364
+ followOffsetX = 0,
2365
+ followOffsetY = 0
1584
2366
  }) {
1585
- const engine = useContext8(EngineContext);
1586
- useEffect9(() => {
2367
+ const engine = useContext9(EngineContext);
2368
+ useEffect11(() => {
1587
2369
  const entityId = engine.ecs.createEntity();
1588
2370
  engine.ecs.addComponent(entityId, createCamera2D({
1589
2371
  followEntityId: followEntity,
@@ -1591,11 +2373,13 @@ function Camera2D({
1591
2373
  smoothing,
1592
2374
  background,
1593
2375
  bounds,
1594
- deadZone
2376
+ deadZone,
2377
+ followOffsetX,
2378
+ followOffsetY
1595
2379
  }));
1596
2380
  return () => engine.ecs.destroyEntity(entityId);
1597
2381
  }, []);
1598
- useEffect9(() => {
2382
+ useEffect11(() => {
1599
2383
  const camId = engine.ecs.queryOne("Camera2D");
1600
2384
  if (camId === undefined)
1601
2385
  return;
@@ -1606,15 +2390,17 @@ function Camera2D({
1606
2390
  cam.background = background;
1607
2391
  cam.bounds = bounds;
1608
2392
  cam.deadZone = deadZone;
1609
- }, [followEntity, zoom, smoothing, background, bounds, deadZone, engine]);
2393
+ cam.followOffsetX = followOffsetX;
2394
+ cam.followOffsetY = followOffsetY;
2395
+ }, [followEntity, zoom, smoothing, background, bounds, deadZone, followOffsetX, followOffsetY, engine]);
1610
2396
  return null;
1611
2397
  }
1612
2398
  // src/components/Animation.tsx
1613
- import { useEffect as useEffect10, useContext as useContext9 } from "react";
1614
- function Animation({ frames, fps = 12, loop = true, playing = true }) {
1615
- const engine = useContext9(EngineContext);
1616
- const entityId = useContext9(EntityContext);
1617
- useEffect10(() => {
2399
+ import { useEffect as useEffect12, useContext as useContext10 } from "react";
2400
+ function Animation({ frames, fps = 12, loop = true, playing = true, onComplete }) {
2401
+ const engine = useContext10(EngineContext);
2402
+ const entityId = useContext10(EntityContext);
2403
+ useEffect12(() => {
1618
2404
  const state = {
1619
2405
  type: "AnimationState",
1620
2406
  frames,
@@ -1622,29 +2408,39 @@ function Animation({ frames, fps = 12, loop = true, playing = true }) {
1622
2408
  loop,
1623
2409
  playing,
1624
2410
  currentIndex: 0,
1625
- timer: 0
2411
+ timer: 0,
2412
+ _completed: false,
2413
+ onComplete
1626
2414
  };
1627
2415
  engine.ecs.addComponent(entityId, state);
1628
2416
  return () => {
1629
2417
  engine.ecs.removeComponent(entityId, "AnimationState");
1630
2418
  };
1631
2419
  }, []);
1632
- useEffect10(() => {
2420
+ useEffect12(() => {
1633
2421
  const anim = engine.ecs.getComponent(entityId, "AnimationState");
1634
2422
  if (!anim)
1635
2423
  return;
2424
+ const wasFramesChanged = anim.frames !== frames;
1636
2425
  anim.playing = playing;
1637
2426
  anim.fps = fps;
1638
2427
  anim.loop = loop;
1639
- }, [playing, fps, loop, engine, entityId]);
2428
+ anim.onComplete = onComplete;
2429
+ if (wasFramesChanged) {
2430
+ anim.frames = frames;
2431
+ anim.currentIndex = 0;
2432
+ anim.timer = 0;
2433
+ anim._completed = false;
2434
+ }
2435
+ }, [playing, fps, loop, frames, onComplete, engine, entityId]);
1640
2436
  return null;
1641
2437
  }
1642
2438
  // src/components/SquashStretch.tsx
1643
- import { useEffect as useEffect11, useContext as useContext10 } from "react";
2439
+ import { useEffect as useEffect13, useContext as useContext11 } from "react";
1644
2440
  function SquashStretch({ intensity = 0.2, recovery = 8 }) {
1645
- const engine = useContext10(EngineContext);
1646
- const entityId = useContext10(EntityContext);
1647
- useEffect11(() => {
2441
+ const engine = useContext11(EngineContext);
2442
+ const entityId = useContext11(EntityContext);
2443
+ useEffect13(() => {
1648
2444
  engine.ecs.addComponent(entityId, {
1649
2445
  type: "SquashStretch",
1650
2446
  intensity,
@@ -1657,7 +2453,7 @@ function SquashStretch({ intensity = 0.2, recovery = 8 }) {
1657
2453
  return null;
1658
2454
  }
1659
2455
  // src/components/ParticleEmitter.tsx
1660
- import { useEffect as useEffect12, useContext as useContext11 } from "react";
2456
+ import { useEffect as useEffect14, useContext as useContext12 } from "react";
1661
2457
 
1662
2458
  // src/components/particlePresets.ts
1663
2459
  var PARTICLE_PRESETS = {
@@ -1742,9 +2538,9 @@ function ParticleEmitter({
1742
2538
  const resolvedColor = color ?? presetConfig.color ?? "#ffffff";
1743
2539
  const resolvedGravity = gravity ?? presetConfig.gravity ?? 200;
1744
2540
  const resolvedMaxParticles = maxParticles ?? presetConfig.maxParticles ?? 100;
1745
- const engine = useContext11(EngineContext);
1746
- const entityId = useContext11(EntityContext);
1747
- useEffect12(() => {
2541
+ const engine = useContext12(EngineContext);
2542
+ const entityId = useContext12(EntityContext);
2543
+ useEffect14(() => {
1748
2544
  engine.ecs.addComponent(entityId, {
1749
2545
  type: "ParticlePool",
1750
2546
  particles: [],
@@ -1762,7 +2558,7 @@ function ParticleEmitter({
1762
2558
  });
1763
2559
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
1764
2560
  }, []);
1765
- useEffect12(() => {
2561
+ useEffect14(() => {
1766
2562
  const pool = engine.ecs.getComponent(entityId, "ParticlePool");
1767
2563
  if (!pool)
1768
2564
  return;
@@ -1771,7 +2567,7 @@ function ParticleEmitter({
1771
2567
  return null;
1772
2568
  }
1773
2569
  // src/components/MovingPlatform.tsx
1774
- import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
2570
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
1775
2571
  var platformPhases = new Map;
1776
2572
  function MovingPlatform({
1777
2573
  x1,
@@ -1783,26 +2579,26 @@ function MovingPlatform({
1783
2579
  duration = 3,
1784
2580
  color = "#37474f"
1785
2581
  }) {
1786
- return /* @__PURE__ */ jsxDEV4(Entity, {
2582
+ return /* @__PURE__ */ jsxDEV5(Entity, {
1787
2583
  children: [
1788
- /* @__PURE__ */ jsxDEV4(Transform, {
2584
+ /* @__PURE__ */ jsxDEV5(Transform, {
1789
2585
  x: x1,
1790
2586
  y: y1
1791
2587
  }, undefined, false, undefined, this),
1792
- /* @__PURE__ */ jsxDEV4(Sprite, {
2588
+ /* @__PURE__ */ jsxDEV5(Sprite, {
1793
2589
  width,
1794
2590
  height,
1795
2591
  color,
1796
2592
  zIndex: 5
1797
2593
  }, undefined, false, undefined, this),
1798
- /* @__PURE__ */ jsxDEV4(RigidBody, {
2594
+ /* @__PURE__ */ jsxDEV5(RigidBody, {
1799
2595
  isStatic: true
1800
2596
  }, undefined, false, undefined, this),
1801
- /* @__PURE__ */ jsxDEV4(BoxCollider, {
2597
+ /* @__PURE__ */ jsxDEV5(BoxCollider, {
1802
2598
  width,
1803
2599
  height
1804
2600
  }, undefined, false, undefined, this),
1805
- /* @__PURE__ */ jsxDEV4(Script, {
2601
+ /* @__PURE__ */ jsxDEV5(Script, {
1806
2602
  init: () => {},
1807
2603
  update: (id, world2, _input, dt) => {
1808
2604
  if (!world2.hasEntity(id))
@@ -1821,7 +2617,69 @@ function MovingPlatform({
1821
2617
  }, undefined, true, undefined, this);
1822
2618
  }
1823
2619
  // src/components/Checkpoint.tsx
1824
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
2620
+ import { useState as useState4 } from "react";
2621
+
2622
+ // src/hooks/useContact.ts
2623
+ import { useContext as useContext13, useEffect as useEffect15 } from "react";
2624
+ function useContactEvent(eventName, handler, opts) {
2625
+ const engine = useContext13(EngineContext);
2626
+ const entityId = useContext13(EntityContext);
2627
+ if (!engine)
2628
+ throw new Error(`${eventName} hook must be used inside <Game>`);
2629
+ if (entityId === null)
2630
+ throw new Error(`${eventName} hook must be used inside <Entity>`);
2631
+ useEffect15(() => {
2632
+ return engine.events.on(eventName, ({ a, b }) => {
2633
+ const isA = a === entityId;
2634
+ const isB = b === entityId;
2635
+ if (!isA && !isB)
2636
+ return;
2637
+ const other = isA ? b : a;
2638
+ if (opts?.tag) {
2639
+ const tagComp = engine.ecs.getComponent(other, "Tag");
2640
+ if (!tagComp?.tags.includes(opts.tag))
2641
+ return;
2642
+ }
2643
+ if (opts?.layer) {
2644
+ const col = engine.ecs.getComponent(other, "BoxCollider");
2645
+ if (col?.layer !== opts.layer)
2646
+ return;
2647
+ }
2648
+ handler(other);
2649
+ });
2650
+ }, [engine.events, engine.ecs, entityId, opts?.tag, opts?.layer]);
2651
+ }
2652
+ function useTriggerEnter(handler, opts) {
2653
+ useContactEvent("triggerEnter", handler, opts);
2654
+ }
2655
+ function useTriggerExit(handler, opts) {
2656
+ useContactEvent("triggerExit", handler, opts);
2657
+ }
2658
+ function useCollisionEnter(handler, opts) {
2659
+ useContactEvent("collisionEnter", handler, opts);
2660
+ }
2661
+ function useCollisionExit(handler, opts) {
2662
+ useContactEvent("collisionExit", handler, opts);
2663
+ }
2664
+ function useCircleEnter(handler, opts) {
2665
+ useContactEvent("circleEnter", handler, opts);
2666
+ }
2667
+ function useCircleExit(handler, opts) {
2668
+ useContactEvent("circleExit", handler, opts);
2669
+ }
2670
+
2671
+ // src/components/Checkpoint.tsx
2672
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
2673
+ function CheckpointActivator({ onActivate }) {
2674
+ const [used, setUsed] = useState4(false);
2675
+ useTriggerEnter(() => {
2676
+ if (used)
2677
+ return;
2678
+ setUsed(true);
2679
+ onActivate?.();
2680
+ }, { tag: "player" });
2681
+ return null;
2682
+ }
1825
2683
  function Checkpoint({
1826
2684
  x,
1827
2685
  y,
@@ -1830,55 +2688,33 @@ function Checkpoint({
1830
2688
  color = "#ffd54f",
1831
2689
  onActivate
1832
2690
  }) {
1833
- return /* @__PURE__ */ jsxDEV5(Entity, {
2691
+ return /* @__PURE__ */ jsxDEV6(Entity, {
1834
2692
  tags: ["checkpoint"],
1835
2693
  children: [
1836
- /* @__PURE__ */ jsxDEV5(Transform, {
2694
+ /* @__PURE__ */ jsxDEV6(Transform, {
1837
2695
  x,
1838
2696
  y
1839
2697
  }, undefined, false, undefined, this),
1840
- /* @__PURE__ */ jsxDEV5(Sprite, {
2698
+ /* @__PURE__ */ jsxDEV6(Sprite, {
1841
2699
  width,
1842
2700
  height,
1843
2701
  color,
1844
2702
  zIndex: 5
1845
2703
  }, undefined, false, undefined, this),
1846
- /* @__PURE__ */ jsxDEV5(BoxCollider, {
2704
+ /* @__PURE__ */ jsxDEV6(BoxCollider, {
1847
2705
  width,
1848
2706
  height,
1849
2707
  isTrigger: true
1850
2708
  }, undefined, false, undefined, this),
1851
- /* @__PURE__ */ jsxDEV5(Script, {
1852
- init: () => {},
1853
- update: (id, world2) => {
1854
- if (!world2.hasEntity(id))
1855
- return;
1856
- const ct = world2.getComponent(id, "Transform");
1857
- if (!ct)
1858
- return;
1859
- for (const pid of world2.query("Tag")) {
1860
- const tag2 = world2.getComponent(pid, "Tag");
1861
- if (!tag2?.tags.includes("player"))
1862
- continue;
1863
- const pt = world2.getComponent(pid, "Transform");
1864
- if (!pt)
1865
- continue;
1866
- const dx = Math.abs(pt.x - ct.x);
1867
- const dy = Math.abs(pt.y - ct.y);
1868
- if (dx < width / 2 + 16 && dy < height / 2 + 20) {
1869
- onActivate?.();
1870
- world2.destroyEntity(id);
1871
- return;
1872
- }
1873
- }
1874
- }
2709
+ /* @__PURE__ */ jsxDEV6(CheckpointActivator, {
2710
+ onActivate
1875
2711
  }, undefined, false, undefined, this)
1876
2712
  ]
1877
2713
  }, undefined, true, undefined, this);
1878
2714
  }
1879
2715
  // src/components/Tilemap.tsx
1880
- import { useEffect as useEffect13, useState as useState3, useContext as useContext12 } from "react";
1881
- import { jsxDEV as jsxDEV6, Fragment as Fragment2 } from "react/jsx-dev-runtime";
2716
+ import { useEffect as useEffect16, useState as useState5, useContext as useContext14 } from "react";
2717
+ import { jsxDEV as jsxDEV7, Fragment as Fragment3 } from "react/jsx-dev-runtime";
1882
2718
  var animatedTiles = new Map;
1883
2719
  function getProperty(props, name) {
1884
2720
  return props?.find((p) => p.name === name)?.value;
@@ -1901,9 +2737,9 @@ function Tilemap({
1901
2737
  triggerLayer: triggerLayerName = "triggers",
1902
2738
  onTileProperty
1903
2739
  }) {
1904
- const engine = useContext12(EngineContext);
1905
- const [spawnedNodes, setSpawnedNodes] = useState3([]);
1906
- useEffect13(() => {
2740
+ const engine = useContext14(EngineContext);
2741
+ const [spawnedNodes, setSpawnedNodes] = useState5([]);
2742
+ useEffect16(() => {
1907
2743
  if (!engine)
1908
2744
  return;
1909
2745
  const createdEntities = [];
@@ -2094,13 +2930,13 @@ function Tilemap({
2094
2930
  }, [src]);
2095
2931
  if (spawnedNodes.length === 0)
2096
2932
  return null;
2097
- return /* @__PURE__ */ jsxDEV6(Fragment2, {
2933
+ return /* @__PURE__ */ jsxDEV7(Fragment3, {
2098
2934
  children: spawnedNodes
2099
2935
  }, undefined, false, undefined, this);
2100
2936
  }
2101
2937
  // src/components/ParallaxLayer.tsx
2102
- import { useEffect as useEffect14, useContext as useContext13 } from "react";
2103
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
2938
+ import { useEffect as useEffect17, useContext as useContext15 } from "react";
2939
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
2104
2940
  function ParallaxLayerInner({
2105
2941
  src,
2106
2942
  speedX,
@@ -2111,9 +2947,9 @@ function ParallaxLayerInner({
2111
2947
  offsetX,
2112
2948
  offsetY
2113
2949
  }) {
2114
- const engine = useContext13(EngineContext);
2115
- const entityId = useContext13(EntityContext);
2116
- useEffect14(() => {
2950
+ const engine = useContext15(EngineContext);
2951
+ const entityId = useContext15(EntityContext);
2952
+ useEffect17(() => {
2117
2953
  engine.ecs.addComponent(entityId, {
2118
2954
  type: "ParallaxLayer",
2119
2955
  src,
@@ -2129,7 +2965,7 @@ function ParallaxLayerInner({
2129
2965
  });
2130
2966
  return () => engine.ecs.removeComponent(entityId, "ParallaxLayer");
2131
2967
  }, []);
2132
- useEffect14(() => {
2968
+ useEffect17(() => {
2133
2969
  const layer = engine.ecs.getComponent(entityId, "ParallaxLayer");
2134
2970
  if (!layer)
2135
2971
  return;
@@ -2154,13 +2990,13 @@ function ParallaxLayer({
2154
2990
  offsetX = 0,
2155
2991
  offsetY = 0
2156
2992
  }) {
2157
- return /* @__PURE__ */ jsxDEV7(Entity, {
2993
+ return /* @__PURE__ */ jsxDEV8(Entity, {
2158
2994
  children: [
2159
- /* @__PURE__ */ jsxDEV7(Transform, {
2995
+ /* @__PURE__ */ jsxDEV8(Transform, {
2160
2996
  x: 0,
2161
2997
  y: 0
2162
2998
  }, undefined, false, undefined, this),
2163
- /* @__PURE__ */ jsxDEV7(ParallaxLayerInner, {
2999
+ /* @__PURE__ */ jsxDEV8(ParallaxLayerInner, {
2164
3000
  src,
2165
3001
  speedX,
2166
3002
  speedY,
@@ -2175,7 +3011,7 @@ function ParallaxLayer({
2175
3011
  }
2176
3012
  // src/components/ScreenFlash.tsx
2177
3013
  import { forwardRef, useImperativeHandle, useRef as useRef2 } from "react";
2178
- import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
3014
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
2179
3015
  var ScreenFlash = forwardRef((_, ref) => {
2180
3016
  const divRef = useRef2(null);
2181
3017
  useImperativeHandle(ref, () => ({
@@ -2197,7 +3033,7 @@ var ScreenFlash = forwardRef((_, ref) => {
2197
3033
  });
2198
3034
  }
2199
3035
  }));
2200
- return /* @__PURE__ */ jsxDEV8("div", {
3036
+ return /* @__PURE__ */ jsxDEV9("div", {
2201
3037
  ref: divRef,
2202
3038
  style: {
2203
3039
  position: "absolute",
@@ -2211,47 +3047,111 @@ var ScreenFlash = forwardRef((_, ref) => {
2211
3047
  });
2212
3048
  ScreenFlash.displayName = "ScreenFlash";
2213
3049
  // src/hooks/useGame.ts
2214
- import { useContext as useContext14 } from "react";
3050
+ import { useContext as useContext16 } from "react";
2215
3051
  function useGame() {
2216
- const engine = useContext14(EngineContext);
3052
+ const engine = useContext16(EngineContext);
2217
3053
  if (!engine)
2218
3054
  throw new Error("useGame must be used inside <Game>");
2219
3055
  return engine;
2220
3056
  }
3057
+ // src/hooks/useCamera.ts
3058
+ import { useMemo } from "react";
3059
+ function useCamera() {
3060
+ const engine = useGame();
3061
+ return useMemo(() => ({
3062
+ shake(intensity, duration) {
3063
+ engine.renderSystem?.triggerShake(intensity, duration);
3064
+ },
3065
+ setFollowOffset(x, y) {
3066
+ const camId = engine.ecs.queryOne("Camera2D");
3067
+ if (camId === undefined)
3068
+ return;
3069
+ const cam = engine.ecs.getComponent(camId, "Camera2D");
3070
+ if (cam) {
3071
+ cam.followOffsetX = x;
3072
+ cam.followOffsetY = y;
3073
+ }
3074
+ },
3075
+ setPosition(x, y) {
3076
+ const camId = engine.ecs.queryOne("Camera2D");
3077
+ if (camId === undefined)
3078
+ return;
3079
+ const cam = engine.ecs.getComponent(camId, "Camera2D");
3080
+ if (cam) {
3081
+ cam.x = x;
3082
+ cam.y = y;
3083
+ }
3084
+ },
3085
+ setZoom(zoom) {
3086
+ const camId = engine.ecs.queryOne("Camera2D");
3087
+ if (camId === undefined)
3088
+ return;
3089
+ const cam = engine.ecs.getComponent(camId, "Camera2D");
3090
+ if (cam)
3091
+ cam.zoom = zoom;
3092
+ }
3093
+ }), [engine]);
3094
+ }
3095
+ // src/hooks/useSnapshot.ts
3096
+ import { useMemo as useMemo2 } from "react";
3097
+ function useSnapshot() {
3098
+ const engine = useGame();
3099
+ return useMemo2(() => ({
3100
+ save: () => engine.ecs.getSnapshot(),
3101
+ restore: (snapshot) => engine.ecs.restoreSnapshot(snapshot)
3102
+ }), [engine]);
3103
+ }
2221
3104
  // src/hooks/useEntity.ts
2222
- import { useContext as useContext15 } from "react";
3105
+ import { useContext as useContext17 } from "react";
2223
3106
  function useEntity() {
2224
- const id = useContext15(EntityContext);
3107
+ const id = useContext17(EntityContext);
2225
3108
  if (id === null)
2226
3109
  throw new Error("useEntity must be used inside <Entity>");
2227
3110
  return id;
2228
3111
  }
2229
3112
  // src/hooks/useInput.ts
2230
- import { useContext as useContext16 } from "react";
3113
+ import { useContext as useContext18 } from "react";
2231
3114
  function useInput() {
2232
- const engine = useContext16(EngineContext);
3115
+ const engine = useContext18(EngineContext);
2233
3116
  if (!engine)
2234
3117
  throw new Error("useInput must be used inside <Game>");
2235
3118
  return engine.input;
2236
3119
  }
3120
+ // src/hooks/useInputMap.ts
3121
+ import { useMemo as useMemo3 } from "react";
3122
+ function useInputMap(bindings) {
3123
+ const input = useInput();
3124
+ const normalized = useMemo3(() => {
3125
+ const out = {};
3126
+ for (const [action, keys] of Object.entries(bindings)) {
3127
+ out[action] = Array.isArray(keys) ? keys : [keys];
3128
+ }
3129
+ return out;
3130
+ }, []);
3131
+ return useMemo3(() => ({
3132
+ isActionDown: (action) => (normalized[action] ?? []).some((k) => input.isDown(k)),
3133
+ isActionPressed: (action) => (normalized[action] ?? []).some((k) => input.isPressed(k)),
3134
+ isActionReleased: (action) => (normalized[action] ?? []).some((k) => input.isReleased(k))
3135
+ }), [input, normalized]);
3136
+ }
2237
3137
  // src/hooks/useEvents.ts
2238
- import { useContext as useContext17, useEffect as useEffect15 } from "react";
3138
+ import { useContext as useContext19, useEffect as useEffect18 } from "react";
2239
3139
  function useEvents() {
2240
- const engine = useContext17(EngineContext);
3140
+ const engine = useContext19(EngineContext);
2241
3141
  if (!engine)
2242
3142
  throw new Error("useEvents must be used inside <Game>");
2243
3143
  return engine.events;
2244
3144
  }
2245
3145
  function useEvent(event, handler) {
2246
3146
  const events = useEvents();
2247
- useEffect15(() => {
3147
+ useEffect18(() => {
2248
3148
  return events.on(event, handler);
2249
3149
  }, [events, event]);
2250
3150
  }
2251
3151
  // src/hooks/usePlatformerController.ts
2252
- import { useContext as useContext18, useEffect as useEffect16 } from "react";
3152
+ import { useContext as useContext20, useEffect as useEffect19 } from "react";
2253
3153
  function usePlatformerController(entityId, opts = {}) {
2254
- const engine = useContext18(EngineContext);
3154
+ const engine = useContext20(EngineContext);
2255
3155
  const {
2256
3156
  speed = 200,
2257
3157
  jumpForce = -500,
@@ -2259,7 +3159,7 @@ function usePlatformerController(entityId, opts = {}) {
2259
3159
  coyoteTime = 0.08,
2260
3160
  jumpBuffer = 0.08
2261
3161
  } = opts;
2262
- useEffect16(() => {
3162
+ useEffect19(() => {
2263
3163
  const state = { coyoteTimer: 0, jumpBuffer: 0, jumpsLeft: maxJumps };
2264
3164
  const updateFn = (id, world2, input, dt) => {
2265
3165
  if (!world2.hasEntity(id))
@@ -2308,11 +3208,11 @@ function usePlatformerController(entityId, opts = {}) {
2308
3208
  }, []);
2309
3209
  }
2310
3210
  // src/hooks/useTopDownMovement.ts
2311
- import { useContext as useContext19, useEffect as useEffect17 } from "react";
3211
+ import { useContext as useContext21, useEffect as useEffect20 } from "react";
2312
3212
  function useTopDownMovement(entityId, opts = {}) {
2313
- const engine = useContext19(EngineContext);
3213
+ const engine = useContext21(EngineContext);
2314
3214
  const { speed = 200, normalizeDiagonal = true } = opts;
2315
- useEffect17(() => {
3215
+ useEffect20(() => {
2316
3216
  const updateFn = (id, world2, input) => {
2317
3217
  if (!world2.hasEntity(id))
2318
3218
  return;
@@ -2346,15 +3246,28 @@ function createAtlas(names, _columns) {
2346
3246
  return atlas;
2347
3247
  }
2348
3248
  export {
3249
+ useTriggerExit,
3250
+ useTriggerEnter,
2349
3251
  useTopDownMovement,
3252
+ useSnapshot,
2350
3253
  usePlatformerController,
3254
+ useInputMap,
2351
3255
  useInput,
2352
3256
  useGame,
2353
3257
  useEvents,
2354
3258
  useEvent,
2355
3259
  useEntity,
3260
+ useCollisionExit,
3261
+ useCollisionEnter,
3262
+ useCircleExit,
3263
+ useCircleEnter,
3264
+ useCamera,
2356
3265
  tween,
3266
+ raycast,
3267
+ overlapBox,
3268
+ findByTag,
2357
3269
  definePlugin,
3270
+ createInputMap,
2358
3271
  createAtlas,
2359
3272
  World,
2360
3273
  Transform,
@@ -2370,6 +3283,7 @@ export {
2370
3283
  Game,
2371
3284
  Entity,
2372
3285
  Ease,
3286
+ CircleCollider,
2373
3287
  Checkpoint,
2374
3288
  Camera2D,
2375
3289
  BoxCollider,