cubeforge 0.0.6 → 0.0.8

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
@@ -90,7 +90,27 @@ class ECSWorld {
90
90
  hasComponent(id, type) {
91
91
  return this.componentIndex.get(id)?.has(type) ?? false;
92
92
  }
93
+ flushDirty() {
94
+ if (this.dirtyAll) {
95
+ this.queryCache.clear();
96
+ this.dirtyAll = false;
97
+ this.dirtyTypes.clear();
98
+ } else if (this.dirtyTypes.size > 0) {
99
+ for (const key of this.queryCache.keys()) {
100
+ if (key === "") {
101
+ this.queryCache.delete(key);
102
+ continue;
103
+ }
104
+ const keyTypes = key.split("\x00");
105
+ if (keyTypes.some((t) => this.dirtyTypes.has(t))) {
106
+ this.queryCache.delete(key);
107
+ }
108
+ }
109
+ this.dirtyTypes.clear();
110
+ }
111
+ }
93
112
  query(...types) {
113
+ this.flushDirty();
94
114
  const key = types.slice().sort().join("\x00");
95
115
  const cached = this.queryCache.get(key);
96
116
  if (cached)
@@ -106,6 +126,7 @@ class ECSWorld {
106
126
  return result;
107
127
  }
108
128
  queryOne(...types) {
129
+ this.flushDirty();
109
130
  for (const arch of this.archetypes.values()) {
110
131
  if (types.every((t) => arch.types.has(t))) {
111
132
  if (arch.entities.length > 0)
@@ -114,6 +135,23 @@ class ECSWorld {
114
135
  }
115
136
  return;
116
137
  }
138
+ findByTag(tag) {
139
+ for (const id of this.query("Tag")) {
140
+ const t = this.getComponent(id, "Tag");
141
+ if (t?.tags.includes(tag))
142
+ return id;
143
+ }
144
+ return;
145
+ }
146
+ findAllByTag(tag) {
147
+ const result = [];
148
+ for (const id of this.query("Tag")) {
149
+ const t = this.getComponent(id, "Tag");
150
+ if (t?.tags.includes(tag))
151
+ result.push(id);
152
+ }
153
+ return result;
154
+ }
117
155
  setDeterministicSeed(seed) {
118
156
  this._rngState = seed >>> 0;
119
157
  this._deterministic = true;
@@ -159,22 +197,6 @@ class ECSWorld {
159
197
  this.systems.splice(idx, 1);
160
198
  }
161
199
  update(dt) {
162
- if (this.dirtyAll) {
163
- this.queryCache.clear();
164
- } else if (this.dirtyTypes.size > 0) {
165
- for (const key of this.queryCache.keys()) {
166
- if (key === "") {
167
- this.queryCache.delete(key);
168
- continue;
169
- }
170
- const keyTypes = key.split("\x00");
171
- if (keyTypes.some((t) => this.dirtyTypes.has(t))) {
172
- this.queryCache.delete(key);
173
- }
174
- }
175
- }
176
- this.dirtyAll = false;
177
- this.dirtyTypes.clear();
178
200
  for (const system of this.systems) {
179
201
  system.update(this, dt);
180
202
  }
@@ -194,6 +216,16 @@ class ECSWorld {
194
216
  return this.componentIndex.size;
195
217
  }
196
218
  }
219
+ // ../core/src/ecs/worldQueries.ts
220
+ function findByTag(world, tag) {
221
+ const results = [];
222
+ for (const id of world.query("Tag")) {
223
+ const t = world.getComponent(id, "Tag");
224
+ if (t?.tags.includes(tag))
225
+ results.push(id);
226
+ }
227
+ return results;
228
+ }
197
229
  // ../core/src/loop/gameLoop.ts
198
230
  class GameLoop {
199
231
  onTick;
@@ -288,6 +320,7 @@ class EventBus {
288
320
  // ../core/src/assets/assetManager.ts
289
321
  class AssetManager {
290
322
  images = new Map;
323
+ imagePromises = new Map;
291
324
  audio = new Map;
292
325
  audioCtx = null;
293
326
  activeSources = new Map;
@@ -298,21 +331,28 @@ class AssetManager {
298
331
  return this.audioCtx;
299
332
  }
300
333
  async loadImage(src) {
301
- if (this.images.has(src))
302
- return this.images.get(src);
303
- const img = new Image;
304
- img.src = src;
305
- try {
306
- await new Promise((resolve, reject) => {
307
- img.onload = () => resolve();
308
- img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
309
- });
310
- } catch (err) {
311
- console.warn(`[Cubeforge] Failed to load image: ${src}`);
312
- throw err;
313
- }
314
- this.images.set(src, img);
315
- return img;
334
+ if (this.imagePromises.has(src))
335
+ return this.imagePromises.get(src);
336
+ const promise = (async () => {
337
+ const img = new Image;
338
+ img.src = src;
339
+ try {
340
+ await new Promise((resolve, reject) => {
341
+ img.onload = () => resolve();
342
+ img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
343
+ });
344
+ } catch (err) {
345
+ console.warn(`[Cubeforge] Failed to load image: ${src}`);
346
+ throw err;
347
+ }
348
+ this.images.set(src, img);
349
+ return img;
350
+ })();
351
+ this.imagePromises.set(src, promise);
352
+ return promise;
353
+ }
354
+ async waitForImages() {
355
+ await Promise.allSettled([...this.imagePromises.values()]);
316
356
  }
317
357
  getImage(src) {
318
358
  return this.images.get(src);
@@ -605,6 +645,24 @@ class InputManager {
605
645
  return this.keyboard.isReleased(key);
606
646
  }
607
647
  }
648
+ // ../input/src/inputMap.ts
649
+ function createInputMap(bindings) {
650
+ const normalized = {};
651
+ for (const [action, keys] of Object.entries(bindings)) {
652
+ normalized[action] = Array.isArray(keys) ? keys : [keys];
653
+ }
654
+ return {
655
+ isActionDown(input, action) {
656
+ return (normalized[action] ?? []).some((k) => input.isDown(k));
657
+ },
658
+ isActionPressed(input, action) {
659
+ return (normalized[action] ?? []).some((k) => input.isPressed(k));
660
+ },
661
+ isActionReleased(input, action) {
662
+ return (normalized[action] ?? []).some((k) => input.isReleased(k));
663
+ }
664
+ };
665
+ }
608
666
  // ../renderer/src/canvas2d.ts
609
667
  class Canvas2DRenderer {
610
668
  canvas;
@@ -656,6 +714,8 @@ function createCamera2D(opts) {
656
714
  zoom: 1,
657
715
  smoothing: 0,
658
716
  background: "#1a1a2e",
717
+ followOffsetX: 0,
718
+ followOffsetY: 0,
659
719
  shakeIntensity: 0,
660
720
  shakeDuration: 0,
661
721
  shakeTimer: 0,
@@ -712,25 +772,27 @@ class RenderSystem {
712
772
  if (targetId !== undefined) {
713
773
  const targetTransform = world2.getComponent(targetId, "Transform");
714
774
  if (targetTransform) {
775
+ const tx = targetTransform.x + (cam.followOffsetX ?? 0);
776
+ const ty = targetTransform.y + (cam.followOffsetY ?? 0);
715
777
  if (cam.deadZone) {
716
778
  const halfW = cam.deadZone.w / 2;
717
779
  const halfH = cam.deadZone.h / 2;
718
- const dx = targetTransform.x - cam.x;
719
- const dy = targetTransform.y - cam.y;
780
+ const dx = tx - cam.x;
781
+ const dy = ty - cam.y;
720
782
  if (dx > halfW)
721
- cam.x = targetTransform.x - halfW;
783
+ cam.x = tx - halfW;
722
784
  else if (dx < -halfW)
723
- cam.x = targetTransform.x + halfW;
785
+ cam.x = tx + halfW;
724
786
  if (dy > halfH)
725
- cam.y = targetTransform.y - halfH;
787
+ cam.y = ty - halfH;
726
788
  else if (dy < -halfH)
727
- cam.y = targetTransform.y + halfH;
789
+ cam.y = ty + halfH;
728
790
  } else if (cam.smoothing > 0) {
729
- cam.x += (targetTransform.x - cam.x) * (1 - cam.smoothing);
730
- cam.y += (targetTransform.y - cam.y) * (1 - cam.smoothing);
791
+ cam.x += (tx - cam.x) * (1 - cam.smoothing);
792
+ cam.y += (ty - cam.y) * (1 - cam.smoothing);
731
793
  } else {
732
- cam.x = targetTransform.x;
733
- cam.y = targetTransform.y;
794
+ cam.x = tx;
795
+ cam.y = ty;
734
796
  }
735
797
  }
736
798
  }
@@ -764,7 +826,16 @@ class RenderSystem {
764
826
  anim.timer -= frameDuration;
765
827
  anim.currentIndex++;
766
828
  if (anim.currentIndex >= anim.frames.length) {
767
- anim.currentIndex = anim.loop ? 0 : anim.frames.length - 1;
829
+ if (anim.loop) {
830
+ anim.currentIndex = 0;
831
+ } else {
832
+ anim.currentIndex = anim.frames.length - 1;
833
+ anim.playing = false;
834
+ if (anim.onComplete && !anim._completed) {
835
+ anim._completed = true;
836
+ anim.onComplete();
837
+ }
838
+ }
768
839
  }
769
840
  }
770
841
  sprite.frameIndex = anim.frames[anim.currentIndex];
@@ -958,6 +1029,8 @@ function createRigidBody(opts) {
958
1029
  isNearGround: false,
959
1030
  bounce: 0,
960
1031
  friction: 0.85,
1032
+ lockX: false,
1033
+ lockY: false,
961
1034
  ...opts
962
1035
  };
963
1036
  }
@@ -971,7 +1044,22 @@ function createBoxCollider(width, height, opts) {
971
1044
  offsetY: 0,
972
1045
  isTrigger: false,
973
1046
  layer: "default",
1047
+ mask: "*",
974
1048
  slope: 0,
1049
+ oneWay: false,
1050
+ ...opts
1051
+ };
1052
+ }
1053
+ // ../physics/src/components/circleCollider.ts
1054
+ function createCircleCollider(radius, opts) {
1055
+ return {
1056
+ type: "CircleCollider",
1057
+ radius,
1058
+ offsetX: 0,
1059
+ offsetY: 0,
1060
+ isTrigger: false,
1061
+ layer: "default",
1062
+ mask: "*",
975
1063
  ...opts
976
1064
  };
977
1065
  }
@@ -1009,12 +1097,27 @@ function getSlopeSurfaceY(st, sc, worldX) {
1009
1097
  const angleRad = sc.slope * (Math.PI / 180);
1010
1098
  return cy - hh + dx * Math.tan(angleRad);
1011
1099
  }
1100
+ function maskAllows(mask, layer) {
1101
+ if (mask === "*")
1102
+ return true;
1103
+ return Array.isArray(mask) && mask.includes(layer);
1104
+ }
1105
+ function canInteract(a, b) {
1106
+ return maskAllows(a.mask, b.layer) && maskAllows(b.mask, a.layer);
1107
+ }
1108
+ function pairKey(a, b) {
1109
+ return a < b ? `${a}:${b}` : `${b}:${a}`;
1110
+ }
1012
1111
 
1013
1112
  class PhysicsSystem {
1014
1113
  gravity;
1015
1114
  events;
1016
1115
  accumulator = 0;
1017
1116
  FIXED_DT = 1 / 60;
1117
+ activeTriggerPairs = new Map;
1118
+ activeCollisionPairs = new Map;
1119
+ activeCirclePairs = new Map;
1120
+ staticPrevPos = new Map;
1018
1121
  constructor(gravity, events) {
1019
1122
  this.gravity = gravity;
1020
1123
  this.events = events;
@@ -1055,6 +1158,30 @@ class PhysicsSystem {
1055
1158
  else
1056
1159
  dynamics.push(id);
1057
1160
  }
1161
+ for (const [key, [a, b]] of this.activeTriggerPairs) {
1162
+ if (!world2.hasEntity(a) || !world2.hasEntity(b)) {
1163
+ this.events?.emit("triggerExit", { a, b });
1164
+ this.activeTriggerPairs.delete(key);
1165
+ }
1166
+ }
1167
+ for (const [key, [a, b]] of this.activeCollisionPairs) {
1168
+ if (!world2.hasEntity(a) || !world2.hasEntity(b)) {
1169
+ this.events?.emit("collisionExit", { a, b });
1170
+ this.activeCollisionPairs.delete(key);
1171
+ }
1172
+ }
1173
+ const staticDelta = new Map;
1174
+ for (const sid of statics) {
1175
+ const st = world2.getComponent(sid, "Transform");
1176
+ const prev = this.staticPrevPos.get(sid);
1177
+ if (prev)
1178
+ staticDelta.set(sid, { dx: st.x - prev.x, dy: st.y - prev.y });
1179
+ this.staticPrevPos.set(sid, { x: st.x, y: st.y });
1180
+ }
1181
+ for (const sid of this.staticPrevPos.keys()) {
1182
+ if (!world2.hasEntity(sid))
1183
+ this.staticPrevPos.delete(sid);
1184
+ }
1058
1185
  const staticGrid = new Map;
1059
1186
  for (const sid of statics) {
1060
1187
  const st = world2.getComponent(sid, "Transform");
@@ -1073,7 +1200,12 @@ class PhysicsSystem {
1073
1200
  const rb = world2.getComponent(id, "RigidBody");
1074
1201
  rb.onGround = false;
1075
1202
  rb.isNearGround = false;
1076
- rb.vy += this.gravity * rb.gravityScale * dt;
1203
+ if (!rb.lockY)
1204
+ rb.vy += this.gravity * rb.gravityScale * dt;
1205
+ if (rb.lockX)
1206
+ rb.vx = 0;
1207
+ if (rb.lockY)
1208
+ rb.vy = 0;
1077
1209
  }
1078
1210
  for (const id of dynamics) {
1079
1211
  const transform2 = world2.getComponent(id, "Transform");
@@ -1098,6 +1230,8 @@ class PhysicsSystem {
1098
1230
  continue;
1099
1231
  if (sc.slope !== 0)
1100
1232
  continue;
1233
+ if (!canInteract(col, sc))
1234
+ continue;
1101
1235
  const ov = getOverlap(getAABB(transform2, col), getAABB(st, sc));
1102
1236
  if (!ov)
1103
1237
  continue;
@@ -1130,6 +1264,8 @@ class PhysicsSystem {
1130
1264
  const sc = world2.getComponent(sid, "BoxCollider");
1131
1265
  if (sc.isTrigger)
1132
1266
  continue;
1267
+ if (!canInteract(col, sc))
1268
+ continue;
1133
1269
  if (sc.slope !== 0) {
1134
1270
  const ov2 = getOverlap(getAABB(transform2, col), getAABB(st, sc));
1135
1271
  if (!ov2)
@@ -1150,11 +1286,25 @@ class PhysicsSystem {
1150
1286
  if (!ov)
1151
1287
  continue;
1152
1288
  if (Math.abs(ov.y) <= Math.abs(ov.x)) {
1289
+ if (sc.oneWay) {
1290
+ if (ov.y >= 0)
1291
+ continue;
1292
+ const platformTop = st.y + sc.offsetY - sc.height / 2;
1293
+ const prevEntityBottom = transform2.y - rb.vy * dt + col.offsetY + col.height / 2;
1294
+ if (prevEntityBottom > platformTop)
1295
+ continue;
1296
+ }
1153
1297
  transform2.y += ov.y;
1154
1298
  if (ov.y < 0) {
1155
1299
  rb.onGround = true;
1156
1300
  if (rb.friction < 1)
1157
1301
  rb.vx *= rb.friction;
1302
+ const delta = staticDelta.get(sid);
1303
+ if (delta) {
1304
+ transform2.x += delta.dx;
1305
+ if (delta.dy < 0)
1306
+ transform2.y += delta.dy;
1307
+ }
1158
1308
  }
1159
1309
  rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
1160
1310
  }
@@ -1162,6 +1312,7 @@ class PhysicsSystem {
1162
1312
  }
1163
1313
  }
1164
1314
  }
1315
+ const currentCollisionPairs = new Map;
1165
1316
  for (let i = 0;i < dynamics.length; i++) {
1166
1317
  for (let j = i + 1;j < dynamics.length; j++) {
1167
1318
  const ia = dynamics[i];
@@ -1173,10 +1324,10 @@ class PhysicsSystem {
1173
1324
  const ov = getOverlap(getAABB(ta, ca), getAABB(tb, cb));
1174
1325
  if (!ov)
1175
1326
  continue;
1176
- if (ca.isTrigger || cb.isTrigger) {
1177
- this.events?.emit("trigger", { a: ia, b: ib });
1327
+ if (!canInteract(ca, cb))
1328
+ continue;
1329
+ if (ca.isTrigger || cb.isTrigger)
1178
1330
  continue;
1179
- }
1180
1331
  const rba = world2.getComponent(ia, "RigidBody");
1181
1332
  const rbb = world2.getComponent(ib, "RigidBody");
1182
1333
  if (Math.abs(ov.y) <= Math.abs(ov.x)) {
@@ -1198,9 +1349,22 @@ class PhysicsSystem {
1198
1349
  ta.y += ov.y / 2;
1199
1350
  tb.x -= ov.x / 2;
1200
1351
  tb.y -= ov.y / 2;
1201
- this.events?.emit("collision", { a: ia, b: ib });
1352
+ const key = pairKey(ia, ib);
1353
+ currentCollisionPairs.set(key, [ia, ib]);
1354
+ }
1355
+ }
1356
+ for (const [key, [a, b]] of currentCollisionPairs) {
1357
+ if (!this.activeCollisionPairs.has(key)) {
1358
+ this.events?.emit("collisionEnter", { a, b });
1202
1359
  }
1360
+ this.events?.emit("collision", { a, b });
1203
1361
  }
1362
+ for (const [key, [a, b]] of this.activeCollisionPairs) {
1363
+ if (!currentCollisionPairs.has(key)) {
1364
+ this.events?.emit("collisionExit", { a, b });
1365
+ }
1366
+ }
1367
+ this.activeCollisionPairs = currentCollisionPairs;
1204
1368
  for (const id of dynamics) {
1205
1369
  const rb = world2.getComponent(id, "RigidBody");
1206
1370
  if (rb.onGround) {
@@ -1230,6 +1394,8 @@ class PhysicsSystem {
1230
1394
  const sc = world2.getComponent(sid, "BoxCollider");
1231
1395
  if (sc.isTrigger)
1232
1396
  continue;
1397
+ if (!canInteract(col, sc))
1398
+ continue;
1233
1399
  const ov = getOverlap(probeAABB, getAABB(st, sc));
1234
1400
  if (ov && Math.abs(ov.y) <= Math.abs(ov.x) && ov.y < 0) {
1235
1401
  rb.isNearGround = true;
@@ -1238,7 +1404,194 @@ class PhysicsSystem {
1238
1404
  }
1239
1405
  }
1240
1406
  }
1407
+ const allWithCollider = world2.query("Transform", "BoxCollider");
1408
+ const currentTriggerPairs = new Map;
1409
+ for (let i = 0;i < allWithCollider.length; i++) {
1410
+ for (let j = i + 1;j < allWithCollider.length; j++) {
1411
+ const ia = allWithCollider[i];
1412
+ const ib = allWithCollider[j];
1413
+ const ca = world2.getComponent(ia, "BoxCollider");
1414
+ const cb = world2.getComponent(ib, "BoxCollider");
1415
+ if (!ca.isTrigger && !cb.isTrigger)
1416
+ continue;
1417
+ if (!canInteract(ca, cb))
1418
+ continue;
1419
+ const ta = world2.getComponent(ia, "Transform");
1420
+ const tb = world2.getComponent(ib, "Transform");
1421
+ const ov = getOverlap(getAABB(ta, ca), getAABB(tb, cb));
1422
+ if (!ov)
1423
+ continue;
1424
+ const key = pairKey(ia, ib);
1425
+ currentTriggerPairs.set(key, [ia, ib]);
1426
+ }
1427
+ }
1428
+ for (const [key, [a, b]] of currentTriggerPairs) {
1429
+ if (!this.activeTriggerPairs.has(key)) {
1430
+ this.events?.emit("triggerEnter", { a, b });
1431
+ }
1432
+ this.events?.emit("trigger", { a, b });
1433
+ }
1434
+ for (const [key, [a, b]] of this.activeTriggerPairs) {
1435
+ if (!currentTriggerPairs.has(key)) {
1436
+ this.events?.emit("triggerExit", { a, b });
1437
+ }
1438
+ }
1439
+ this.activeTriggerPairs = currentTriggerPairs;
1440
+ const allCircles = world2.query("Transform", "CircleCollider");
1441
+ if (allCircles.length > 0) {
1442
+ const currentCirclePairs = new Map;
1443
+ for (let i = 0;i < allCircles.length; i++) {
1444
+ for (let j = i + 1;j < allCircles.length; j++) {
1445
+ const ia = allCircles[i];
1446
+ const ib = allCircles[j];
1447
+ const ca = world2.getComponent(ia, "CircleCollider");
1448
+ const cb = world2.getComponent(ib, "CircleCollider");
1449
+ if (!maskAllows(ca.mask, cb.layer) || !maskAllows(cb.mask, ca.layer))
1450
+ continue;
1451
+ const ta = world2.getComponent(ia, "Transform");
1452
+ const tb = world2.getComponent(ib, "Transform");
1453
+ const dx = ta.x + ca.offsetX - (tb.x + cb.offsetX);
1454
+ const dy = ta.y + ca.offsetY - (tb.y + cb.offsetY);
1455
+ if (dx * dx + dy * dy < (ca.radius + cb.radius) ** 2) {
1456
+ currentCirclePairs.set(pairKey(ia, ib), [ia, ib]);
1457
+ }
1458
+ }
1459
+ }
1460
+ const allBoxes = world2.query("Transform", "BoxCollider");
1461
+ for (const cid of allCircles) {
1462
+ const cc = world2.getComponent(cid, "CircleCollider");
1463
+ const ct = world2.getComponent(cid, "Transform");
1464
+ const cx = ct.x + cc.offsetX;
1465
+ const cy = ct.y + cc.offsetY;
1466
+ for (const bid of allBoxes) {
1467
+ if (bid === cid)
1468
+ continue;
1469
+ const bc = world2.getComponent(bid, "BoxCollider");
1470
+ if (!maskAllows(cc.mask, bc.layer) || !maskAllows(bc.mask, cc.layer))
1471
+ continue;
1472
+ const bt = world2.getComponent(bid, "Transform");
1473
+ const bx = bt.x + bc.offsetX;
1474
+ const by = bt.y + bc.offsetY;
1475
+ const nearX = Math.max(bx - bc.width / 2, Math.min(cx, bx + bc.width / 2));
1476
+ const nearY = Math.max(by - bc.height / 2, Math.min(cy, by + bc.height / 2));
1477
+ const dx = cx - nearX;
1478
+ const dy = cy - nearY;
1479
+ if (dx * dx + dy * dy < cc.radius * cc.radius) {
1480
+ currentCirclePairs.set(pairKey(cid, bid), [cid, bid]);
1481
+ }
1482
+ }
1483
+ }
1484
+ for (const [key, [a, b]] of currentCirclePairs) {
1485
+ if (!this.activeCirclePairs.has(key)) {
1486
+ this.events?.emit("circleEnter", { a, b });
1487
+ }
1488
+ this.events?.emit("circle", { a, b });
1489
+ }
1490
+ for (const [key, [a, b]] of this.activeCirclePairs) {
1491
+ if (!currentCirclePairs.has(key)) {
1492
+ this.events?.emit("circleExit", { a, b });
1493
+ }
1494
+ }
1495
+ this.activeCirclePairs = currentCirclePairs;
1496
+ }
1497
+ }
1498
+ }
1499
+ // ../physics/src/queries.ts
1500
+ function passesFilter(world2, id, col, opts) {
1501
+ if (opts.exclude?.includes(id))
1502
+ return false;
1503
+ if (opts.layer && col.layer !== opts.layer)
1504
+ return false;
1505
+ if (opts.tag) {
1506
+ const t = world2.getComponent(id, "Tag");
1507
+ if (!t?.tags.includes(opts.tag))
1508
+ return false;
1509
+ }
1510
+ return true;
1511
+ }
1512
+ function overlapBox(world2, cx, cy, hw, hh, opts = {}) {
1513
+ const results = [];
1514
+ for (const id of world2.query("Transform", "BoxCollider")) {
1515
+ const t = world2.getComponent(id, "Transform");
1516
+ const c = world2.getComponent(id, "BoxCollider");
1517
+ if (!passesFilter(world2, id, c, opts))
1518
+ continue;
1519
+ const ecx = t.x + c.offsetX;
1520
+ const ecy = t.y + c.offsetY;
1521
+ const ehw = c.width / 2;
1522
+ const ehh = c.height / 2;
1523
+ if (Math.abs(ecx - cx) < hw + ehw && Math.abs(ecy - cy) < hh + ehh) {
1524
+ results.push(id);
1525
+ }
1241
1526
  }
1527
+ return results;
1528
+ }
1529
+ function raycast(world2, origin, direction, maxDistance, opts = {}) {
1530
+ const len = Math.hypot(direction.x, direction.y);
1531
+ if (len === 0)
1532
+ return null;
1533
+ const dx = direction.x / len;
1534
+ const dy = direction.y / len;
1535
+ let closest = null;
1536
+ for (const id of world2.query("Transform", "BoxCollider")) {
1537
+ const t = world2.getComponent(id, "Transform");
1538
+ const c = world2.getComponent(id, "BoxCollider");
1539
+ if (!opts.includeTriggers && c.isTrigger)
1540
+ continue;
1541
+ if (!passesFilter(world2, id, c, opts))
1542
+ continue;
1543
+ const cx = t.x + c.offsetX;
1544
+ const cy = t.y + c.offsetY;
1545
+ const hw = c.width / 2;
1546
+ const hh = c.height / 2;
1547
+ const left = cx - hw;
1548
+ const right = cx + hw;
1549
+ const top = cy - hh;
1550
+ const bottom = cy + hh;
1551
+ let tmin = -Infinity;
1552
+ let tmax = Infinity;
1553
+ if (dx !== 0) {
1554
+ const t1 = (left - origin.x) / dx;
1555
+ const t2 = (right - origin.x) / dx;
1556
+ tmin = Math.max(tmin, Math.min(t1, t2));
1557
+ tmax = Math.min(tmax, Math.max(t1, t2));
1558
+ } else if (origin.x < left || origin.x > right) {
1559
+ continue;
1560
+ }
1561
+ if (dy !== 0) {
1562
+ const t1 = (top - origin.y) / dy;
1563
+ const t2 = (bottom - origin.y) / dy;
1564
+ tmin = Math.max(tmin, Math.min(t1, t2));
1565
+ tmax = Math.min(tmax, Math.max(t1, t2));
1566
+ } else if (origin.y < top || origin.y > bottom) {
1567
+ continue;
1568
+ }
1569
+ if (tmax < 0 || tmin > tmax || tmin > maxDistance)
1570
+ continue;
1571
+ const dist = Math.max(0, tmin);
1572
+ if (closest && dist >= closest.distance)
1573
+ continue;
1574
+ const hitX = origin.x + dx * tmin;
1575
+ const hitY = origin.y + dy * tmin;
1576
+ let nx = 0;
1577
+ let ny = 0;
1578
+ const edgeEps = 0.001;
1579
+ if (Math.abs(hitX - left) < edgeEps)
1580
+ nx = -1;
1581
+ else if (Math.abs(hitX - right) < edgeEps)
1582
+ nx = 1;
1583
+ else if (Math.abs(hitY - top) < edgeEps)
1584
+ ny = -1;
1585
+ else if (Math.abs(hitY - bottom) < edgeEps)
1586
+ ny = 1;
1587
+ closest = {
1588
+ entityId: id,
1589
+ distance: dist,
1590
+ point: { x: hitX, y: hitY },
1591
+ normal: { x: nx, y: ny }
1592
+ };
1593
+ }
1594
+ return closest;
1242
1595
  }
1243
1596
  // src/context.ts
1244
1597
  import { createContext } from "react";
@@ -1655,8 +2008,10 @@ function Game({
1655
2008
  scale = "none",
1656
2009
  deterministic = false,
1657
2010
  seed = 0,
2011
+ asyncAssets = false,
1658
2012
  onReady,
1659
2013
  plugins,
2014
+ renderer: CustomRenderer,
1660
2015
  style,
1661
2016
  className,
1662
2017
  children
@@ -1664,6 +2019,7 @@ function Game({
1664
2019
  const canvasRef = useRef(null);
1665
2020
  const wrapperRef = useRef(null);
1666
2021
  const [engine, setEngine] = useState2(null);
2022
+ const [assetsReady, setAssetsReady] = useState2(asyncAssets);
1667
2023
  const devtoolsHandle = useRef({ buffer: [] });
1668
2024
  useEffect2(() => {
1669
2025
  const canvas = canvasRef.current;
@@ -1671,13 +2027,21 @@ function Game({
1671
2027
  if (deterministic)
1672
2028
  ecs.setDeterministicSeed(seed);
1673
2029
  const input = new InputManager;
1674
- const renderer = new Canvas2DRenderer(canvas);
1675
2030
  const events = new EventBus;
1676
2031
  const assets = new AssetManager;
1677
2032
  const physics = new PhysicsSystem(gravity, events);
1678
2033
  const entityIds = new Map;
1679
- const renderSystem2 = new RenderSystem(renderer, entityIds);
1680
- const debugSystem = debug ? new DebugSystem(renderer) : null;
2034
+ let canvas2d2;
2035
+ let builtinRenderSystem;
2036
+ let renderSystem2;
2037
+ if (CustomRenderer) {
2038
+ renderSystem2 = new CustomRenderer(canvas, entityIds);
2039
+ } else {
2040
+ canvas2d2 = new Canvas2DRenderer(canvas);
2041
+ builtinRenderSystem = new RenderSystem(canvas2d2, entityIds);
2042
+ renderSystem2 = builtinRenderSystem;
2043
+ }
2044
+ const debugSystem = debug && canvas2d2 ? new DebugSystem(canvas2d2) : null;
1681
2045
  ecs.addSystem(new ScriptSystem(input));
1682
2046
  ecs.addSystem(physics);
1683
2047
  ecs.addSystem(renderSystem2);
@@ -1699,7 +2063,7 @@ function Game({
1699
2063
  handle.onFrame?.();
1700
2064
  }
1701
2065
  });
1702
- const state = { ecs, input, renderer, physics, events, assets, loop, canvas, entityIds };
2066
+ const state = { ecs, input, renderer: canvas2d2, renderSystem: builtinRenderSystem, physics, events, assets, loop, canvas, entityIds };
1703
2067
  setEngine(state);
1704
2068
  if (plugins) {
1705
2069
  for (const plugin2 of plugins) {
@@ -1709,7 +2073,6 @@ function Game({
1709
2073
  plugin2.onInit?.(state);
1710
2074
  }
1711
2075
  }
1712
- loop.start();
1713
2076
  onReady?.({
1714
2077
  pause: () => loop.pause(),
1715
2078
  resume: () => loop.resume(),
@@ -1743,6 +2106,25 @@ function Game({
1743
2106
  resizeObserver?.disconnect();
1744
2107
  };
1745
2108
  }, []);
2109
+ useEffect2(() => {
2110
+ if (!engine)
2111
+ return;
2112
+ let cancelled = false;
2113
+ if (asyncAssets) {
2114
+ engine.loop.start();
2115
+ setAssetsReady(true);
2116
+ return;
2117
+ }
2118
+ engine.assets.waitForImages().then(() => {
2119
+ if (!cancelled) {
2120
+ engine.loop.start();
2121
+ setAssetsReady(true);
2122
+ }
2123
+ });
2124
+ return () => {
2125
+ cancelled = true;
2126
+ };
2127
+ }, [engine]);
1746
2128
  useEffect2(() => {
1747
2129
  engine?.physics.setGravity(gravity);
1748
2130
  }, [gravity, engine]);
@@ -1752,21 +2134,71 @@ function Game({
1752
2134
  imageRendering: scale === "pixel" ? "pixelated" : undefined,
1753
2135
  ...style
1754
2136
  };
1755
- const wrapperStyle = scale === "contain" ? { position: "relative", width, height, overflow: "visible" } : {};
2137
+ const wrapperStyle = {
2138
+ position: "relative",
2139
+ display: "inline-block",
2140
+ ...scale === "contain" ? { width, height, overflow: "visible" } : {}
2141
+ };
1756
2142
  return /* @__PURE__ */ jsxDEV2(EngineContext.Provider, {
1757
2143
  value: engine,
1758
2144
  children: [
1759
2145
  /* @__PURE__ */ jsxDEV2("div", {
1760
2146
  ref: wrapperRef,
1761
2147
  style: wrapperStyle,
1762
- children: /* @__PURE__ */ jsxDEV2("canvas", {
1763
- ref: canvasRef,
1764
- width,
1765
- height,
1766
- style: canvasStyle,
1767
- className
1768
- }, undefined, false, undefined, this)
1769
- }, undefined, false, undefined, this),
2148
+ children: [
2149
+ /* @__PURE__ */ jsxDEV2("canvas", {
2150
+ ref: canvasRef,
2151
+ width,
2152
+ height,
2153
+ style: canvasStyle,
2154
+ className
2155
+ }, undefined, false, undefined, this),
2156
+ !assetsReady && /* @__PURE__ */ jsxDEV2("div", {
2157
+ style: {
2158
+ position: "absolute",
2159
+ inset: 0,
2160
+ display: "flex",
2161
+ flexDirection: "column",
2162
+ alignItems: "center",
2163
+ justifyContent: "center",
2164
+ background: "#0a0a0f",
2165
+ pointerEvents: "none"
2166
+ },
2167
+ children: [
2168
+ /* @__PURE__ */ jsxDEV2("div", {
2169
+ style: { display: "flex", gap: 6, marginBottom: 12 },
2170
+ children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxDEV2("div", {
2171
+ style: {
2172
+ width: 8,
2173
+ height: 8,
2174
+ borderRadius: "50%",
2175
+ background: "#4fc3f7",
2176
+ animation: "cubeforge-loading-dot 1.2s ease-in-out infinite",
2177
+ animationDelay: `${i * 0.2}s`
2178
+ }
2179
+ }, i, false, undefined, this))
2180
+ }, undefined, false, undefined, this),
2181
+ /* @__PURE__ */ jsxDEV2("span", {
2182
+ style: {
2183
+ fontFamily: "monospace",
2184
+ fontSize: 11,
2185
+ letterSpacing: 3,
2186
+ color: "#37474f"
2187
+ },
2188
+ children: "LOADING"
2189
+ }, undefined, false, undefined, this),
2190
+ /* @__PURE__ */ jsxDEV2("style", {
2191
+ children: `
2192
+ @keyframes cubeforge-loading-dot {
2193
+ 0%, 80%, 100% { transform: scale(0.6); opacity: 0.3; }
2194
+ 40% { transform: scale(1); opacity: 1; }
2195
+ }
2196
+ `
2197
+ }, undefined, false, undefined, this)
2198
+ ]
2199
+ }, undefined, true, undefined, this)
2200
+ ]
2201
+ }, undefined, true, undefined, this),
1770
2202
  engine && children,
1771
2203
  engine && devtools && /* @__PURE__ */ jsxDEV2(DevToolsOverlay, {
1772
2204
  handle: devtoolsHandle.current,
@@ -1898,7 +2330,10 @@ function Sprite({
1898
2330
  });
1899
2331
  engine.ecs.addComponent(entityId, comp);
1900
2332
  if (src) {
1901
- engine.assets.loadImage(src).then((img) => {
2333
+ const viteEnv = import.meta.env;
2334
+ const base = (viteEnv?.BASE_URL ?? "/").replace(/\/$/, "");
2335
+ const resolvedSrc = base && src.startsWith("/") ? base + src : src;
2336
+ engine.assets.loadImage(resolvedSrc).then((img) => {
1902
2337
  const c = engine.ecs.getComponent(entityId, "Sprite");
1903
2338
  if (c)
1904
2339
  c.image = img;
@@ -1927,12 +2362,14 @@ function RigidBody({
1927
2362
  bounce = 0,
1928
2363
  friction = 0.85,
1929
2364
  vx = 0,
1930
- vy = 0
2365
+ vy = 0,
2366
+ lockX = false,
2367
+ lockY = false
1931
2368
  }) {
1932
2369
  const engine = useContext5(EngineContext);
1933
2370
  const entityId = useContext5(EntityContext);
1934
2371
  useEffect7(() => {
1935
- engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy }));
2372
+ engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY }));
1936
2373
  return () => engine.ecs.removeComponent(entityId, "RigidBody");
1937
2374
  }, []);
1938
2375
  return null;
@@ -1945,12 +2382,14 @@ function BoxCollider({
1945
2382
  offsetX = 0,
1946
2383
  offsetY = 0,
1947
2384
  isTrigger = false,
1948
- layer = "default"
2385
+ layer = "default",
2386
+ mask = "*",
2387
+ oneWay = false
1949
2388
  }) {
1950
2389
  const engine = useContext6(EngineContext);
1951
2390
  const entityId = useContext6(EntityContext);
1952
2391
  useEffect8(() => {
1953
- engine.ecs.addComponent(entityId, createBoxCollider(width, height, { offsetX, offsetY, isTrigger, layer }));
2392
+ engine.ecs.addComponent(entityId, createBoxCollider(width, height, { offsetX, offsetY, isTrigger, layer, mask, oneWay }));
1954
2393
  const checkId = setTimeout(() => {
1955
2394
  if (engine.ecs.hasEntity(entityId) && !engine.ecs.hasComponent(entityId, "Transform")) {
1956
2395
  console.warn(`[Cubeforge] BoxCollider on entity ${entityId} has no Transform. Physics requires Transform.`);
@@ -1963,12 +2402,30 @@ function BoxCollider({
1963
2402
  }, []);
1964
2403
  return null;
1965
2404
  }
1966
- // src/components/Script.tsx
2405
+ // src/components/CircleCollider.tsx
1967
2406
  import { useEffect as useEffect9, useContext as useContext7 } from "react";
1968
- function Script({ init, update }) {
2407
+ function CircleCollider({
2408
+ radius,
2409
+ offsetX = 0,
2410
+ offsetY = 0,
2411
+ isTrigger = false,
2412
+ layer = "default",
2413
+ mask = "*"
2414
+ }) {
1969
2415
  const engine = useContext7(EngineContext);
1970
2416
  const entityId = useContext7(EntityContext);
1971
2417
  useEffect9(() => {
2418
+ engine.ecs.addComponent(entityId, createCircleCollider(radius, { offsetX, offsetY, isTrigger, layer, mask }));
2419
+ return () => engine.ecs.removeComponent(entityId, "CircleCollider");
2420
+ }, []);
2421
+ return null;
2422
+ }
2423
+ // src/components/Script.tsx
2424
+ import { useEffect as useEffect10, useContext as useContext8 } from "react";
2425
+ function Script({ init, update }) {
2426
+ const engine = useContext8(EngineContext);
2427
+ const entityId = useContext8(EntityContext);
2428
+ useEffect10(() => {
1972
2429
  if (init) {
1973
2430
  try {
1974
2431
  init(entityId, engine.ecs);
@@ -1982,29 +2439,37 @@ function Script({ init, update }) {
1982
2439
  return null;
1983
2440
  }
1984
2441
  // src/components/Camera2D.tsx
1985
- import { useEffect as useEffect10, useContext as useContext8 } from "react";
2442
+ import { useEffect as useEffect11, useContext as useContext9 } from "react";
1986
2443
  function Camera2D({
1987
2444
  followEntity,
2445
+ x = 0,
2446
+ y = 0,
1988
2447
  zoom = 1,
1989
2448
  smoothing = 0,
1990
2449
  background = "#1a1a2e",
1991
2450
  bounds,
1992
- deadZone
2451
+ deadZone,
2452
+ followOffsetX = 0,
2453
+ followOffsetY = 0
1993
2454
  }) {
1994
- const engine = useContext8(EngineContext);
1995
- useEffect10(() => {
2455
+ const engine = useContext9(EngineContext);
2456
+ useEffect11(() => {
1996
2457
  const entityId = engine.ecs.createEntity();
1997
2458
  engine.ecs.addComponent(entityId, createCamera2D({
1998
2459
  followEntityId: followEntity,
2460
+ x,
2461
+ y,
1999
2462
  zoom,
2000
2463
  smoothing,
2001
2464
  background,
2002
2465
  bounds,
2003
- deadZone
2466
+ deadZone,
2467
+ followOffsetX,
2468
+ followOffsetY
2004
2469
  }));
2005
2470
  return () => engine.ecs.destroyEntity(entityId);
2006
2471
  }, []);
2007
- useEffect10(() => {
2472
+ useEffect11(() => {
2008
2473
  const camId = engine.ecs.queryOne("Camera2D");
2009
2474
  if (camId === undefined)
2010
2475
  return;
@@ -2015,15 +2480,17 @@ function Camera2D({
2015
2480
  cam.background = background;
2016
2481
  cam.bounds = bounds;
2017
2482
  cam.deadZone = deadZone;
2018
- }, [followEntity, zoom, smoothing, background, bounds, deadZone, engine]);
2483
+ cam.followOffsetX = followOffsetX;
2484
+ cam.followOffsetY = followOffsetY;
2485
+ }, [followEntity, zoom, smoothing, background, bounds, deadZone, followOffsetX, followOffsetY, engine]);
2019
2486
  return null;
2020
2487
  }
2021
2488
  // src/components/Animation.tsx
2022
- import { useEffect as useEffect11, useContext as useContext9 } from "react";
2023
- function Animation({ frames, fps = 12, loop = true, playing = true }) {
2024
- const engine = useContext9(EngineContext);
2025
- const entityId = useContext9(EntityContext);
2026
- useEffect11(() => {
2489
+ import { useEffect as useEffect12, useContext as useContext10 } from "react";
2490
+ function Animation({ frames, fps = 12, loop = true, playing = true, onComplete }) {
2491
+ const engine = useContext10(EngineContext);
2492
+ const entityId = useContext10(EntityContext);
2493
+ useEffect12(() => {
2027
2494
  const state = {
2028
2495
  type: "AnimationState",
2029
2496
  frames,
@@ -2031,29 +2498,39 @@ function Animation({ frames, fps = 12, loop = true, playing = true }) {
2031
2498
  loop,
2032
2499
  playing,
2033
2500
  currentIndex: 0,
2034
- timer: 0
2501
+ timer: 0,
2502
+ _completed: false,
2503
+ onComplete
2035
2504
  };
2036
2505
  engine.ecs.addComponent(entityId, state);
2037
2506
  return () => {
2038
2507
  engine.ecs.removeComponent(entityId, "AnimationState");
2039
2508
  };
2040
2509
  }, []);
2041
- useEffect11(() => {
2510
+ useEffect12(() => {
2042
2511
  const anim = engine.ecs.getComponent(entityId, "AnimationState");
2043
2512
  if (!anim)
2044
2513
  return;
2514
+ const wasFramesChanged = anim.frames !== frames;
2045
2515
  anim.playing = playing;
2046
2516
  anim.fps = fps;
2047
2517
  anim.loop = loop;
2048
- }, [playing, fps, loop, engine, entityId]);
2518
+ anim.onComplete = onComplete;
2519
+ if (wasFramesChanged) {
2520
+ anim.frames = frames;
2521
+ anim.currentIndex = 0;
2522
+ anim.timer = 0;
2523
+ anim._completed = false;
2524
+ }
2525
+ }, [playing, fps, loop, frames, onComplete, engine, entityId]);
2049
2526
  return null;
2050
2527
  }
2051
2528
  // src/components/SquashStretch.tsx
2052
- import { useEffect as useEffect12, useContext as useContext10 } from "react";
2529
+ import { useEffect as useEffect13, useContext as useContext11 } from "react";
2053
2530
  function SquashStretch({ intensity = 0.2, recovery = 8 }) {
2054
- const engine = useContext10(EngineContext);
2055
- const entityId = useContext10(EntityContext);
2056
- useEffect12(() => {
2531
+ const engine = useContext11(EngineContext);
2532
+ const entityId = useContext11(EntityContext);
2533
+ useEffect13(() => {
2057
2534
  engine.ecs.addComponent(entityId, {
2058
2535
  type: "SquashStretch",
2059
2536
  intensity,
@@ -2066,7 +2543,7 @@ function SquashStretch({ intensity = 0.2, recovery = 8 }) {
2066
2543
  return null;
2067
2544
  }
2068
2545
  // src/components/ParticleEmitter.tsx
2069
- import { useEffect as useEffect13, useContext as useContext11 } from "react";
2546
+ import { useEffect as useEffect14, useContext as useContext12 } from "react";
2070
2547
 
2071
2548
  // src/components/particlePresets.ts
2072
2549
  var PARTICLE_PRESETS = {
@@ -2151,9 +2628,9 @@ function ParticleEmitter({
2151
2628
  const resolvedColor = color ?? presetConfig.color ?? "#ffffff";
2152
2629
  const resolvedGravity = gravity ?? presetConfig.gravity ?? 200;
2153
2630
  const resolvedMaxParticles = maxParticles ?? presetConfig.maxParticles ?? 100;
2154
- const engine = useContext11(EngineContext);
2155
- const entityId = useContext11(EntityContext);
2156
- useEffect13(() => {
2631
+ const engine = useContext12(EngineContext);
2632
+ const entityId = useContext12(EntityContext);
2633
+ useEffect14(() => {
2157
2634
  engine.ecs.addComponent(entityId, {
2158
2635
  type: "ParticlePool",
2159
2636
  particles: [],
@@ -2171,7 +2648,7 @@ function ParticleEmitter({
2171
2648
  });
2172
2649
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
2173
2650
  }, []);
2174
- useEffect13(() => {
2651
+ useEffect14(() => {
2175
2652
  const pool = engine.ecs.getComponent(entityId, "ParticlePool");
2176
2653
  if (!pool)
2177
2654
  return;
@@ -2229,8 +2706,70 @@ function MovingPlatform({
2229
2706
  ]
2230
2707
  }, undefined, true, undefined, this);
2231
2708
  }
2709
+ // src/components/Checkpoint.tsx
2710
+ import { useState as useState4 } from "react";
2711
+
2712
+ // src/hooks/useContact.ts
2713
+ import { useContext as useContext13, useEffect as useEffect15 } from "react";
2714
+ function useContactEvent(eventName, handler, opts) {
2715
+ const engine = useContext13(EngineContext);
2716
+ const entityId = useContext13(EntityContext);
2717
+ if (!engine)
2718
+ throw new Error(`${eventName} hook must be used inside <Game>`);
2719
+ if (entityId === null)
2720
+ throw new Error(`${eventName} hook must be used inside <Entity>`);
2721
+ useEffect15(() => {
2722
+ return engine.events.on(eventName, ({ a, b }) => {
2723
+ const isA = a === entityId;
2724
+ const isB = b === entityId;
2725
+ if (!isA && !isB)
2726
+ return;
2727
+ const other = isA ? b : a;
2728
+ if (opts?.tag) {
2729
+ const tagComp = engine.ecs.getComponent(other, "Tag");
2730
+ if (!tagComp?.tags.includes(opts.tag))
2731
+ return;
2732
+ }
2733
+ if (opts?.layer) {
2734
+ const col = engine.ecs.getComponent(other, "BoxCollider");
2735
+ if (col?.layer !== opts.layer)
2736
+ return;
2737
+ }
2738
+ handler(other);
2739
+ });
2740
+ }, [engine.events, engine.ecs, entityId, opts?.tag, opts?.layer]);
2741
+ }
2742
+ function useTriggerEnter(handler, opts) {
2743
+ useContactEvent("triggerEnter", handler, opts);
2744
+ }
2745
+ function useTriggerExit(handler, opts) {
2746
+ useContactEvent("triggerExit", handler, opts);
2747
+ }
2748
+ function useCollisionEnter(handler, opts) {
2749
+ useContactEvent("collisionEnter", handler, opts);
2750
+ }
2751
+ function useCollisionExit(handler, opts) {
2752
+ useContactEvent("collisionExit", handler, opts);
2753
+ }
2754
+ function useCircleEnter(handler, opts) {
2755
+ useContactEvent("circleEnter", handler, opts);
2756
+ }
2757
+ function useCircleExit(handler, opts) {
2758
+ useContactEvent("circleExit", handler, opts);
2759
+ }
2760
+
2232
2761
  // src/components/Checkpoint.tsx
2233
2762
  import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
2763
+ function CheckpointActivator({ onActivate }) {
2764
+ const [used, setUsed] = useState4(false);
2765
+ useTriggerEnter(() => {
2766
+ if (used)
2767
+ return;
2768
+ setUsed(true);
2769
+ onActivate?.();
2770
+ }, { tag: "player" });
2771
+ return null;
2772
+ }
2234
2773
  function Checkpoint({
2235
2774
  x,
2236
2775
  y,
@@ -2257,36 +2796,14 @@ function Checkpoint({
2257
2796
  height,
2258
2797
  isTrigger: true
2259
2798
  }, undefined, false, undefined, this),
2260
- /* @__PURE__ */ jsxDEV6(Script, {
2261
- init: () => {},
2262
- update: (id, world2) => {
2263
- if (!world2.hasEntity(id))
2264
- return;
2265
- const ct = world2.getComponent(id, "Transform");
2266
- if (!ct)
2267
- return;
2268
- for (const pid of world2.query("Tag")) {
2269
- const tag2 = world2.getComponent(pid, "Tag");
2270
- if (!tag2?.tags.includes("player"))
2271
- continue;
2272
- const pt = world2.getComponent(pid, "Transform");
2273
- if (!pt)
2274
- continue;
2275
- const dx = Math.abs(pt.x - ct.x);
2276
- const dy = Math.abs(pt.y - ct.y);
2277
- if (dx < width / 2 + 16 && dy < height / 2 + 20) {
2278
- onActivate?.();
2279
- world2.destroyEntity(id);
2280
- return;
2281
- }
2282
- }
2283
- }
2799
+ /* @__PURE__ */ jsxDEV6(CheckpointActivator, {
2800
+ onActivate
2284
2801
  }, undefined, false, undefined, this)
2285
2802
  ]
2286
2803
  }, undefined, true, undefined, this);
2287
2804
  }
2288
2805
  // src/components/Tilemap.tsx
2289
- import { useEffect as useEffect14, useState as useState4, useContext as useContext12 } from "react";
2806
+ import { useEffect as useEffect16, useState as useState5, useContext as useContext14 } from "react";
2290
2807
  import { jsxDEV as jsxDEV7, Fragment as Fragment3 } from "react/jsx-dev-runtime";
2291
2808
  var animatedTiles = new Map;
2292
2809
  function getProperty(props, name) {
@@ -2310,9 +2827,9 @@ function Tilemap({
2310
2827
  triggerLayer: triggerLayerName = "triggers",
2311
2828
  onTileProperty
2312
2829
  }) {
2313
- const engine = useContext12(EngineContext);
2314
- const [spawnedNodes, setSpawnedNodes] = useState4([]);
2315
- useEffect14(() => {
2830
+ const engine = useContext14(EngineContext);
2831
+ const [spawnedNodes, setSpawnedNodes] = useState5([]);
2832
+ useEffect16(() => {
2316
2833
  if (!engine)
2317
2834
  return;
2318
2835
  const createdEntities = [];
@@ -2508,7 +3025,7 @@ function Tilemap({
2508
3025
  }, undefined, false, undefined, this);
2509
3026
  }
2510
3027
  // src/components/ParallaxLayer.tsx
2511
- import { useEffect as useEffect15, useContext as useContext13 } from "react";
3028
+ import { useEffect as useEffect17, useContext as useContext15 } from "react";
2512
3029
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
2513
3030
  function ParallaxLayerInner({
2514
3031
  src,
@@ -2520,9 +3037,9 @@ function ParallaxLayerInner({
2520
3037
  offsetX,
2521
3038
  offsetY
2522
3039
  }) {
2523
- const engine = useContext13(EngineContext);
2524
- const entityId = useContext13(EntityContext);
2525
- useEffect15(() => {
3040
+ const engine = useContext15(EngineContext);
3041
+ const entityId = useContext15(EntityContext);
3042
+ useEffect17(() => {
2526
3043
  engine.ecs.addComponent(entityId, {
2527
3044
  type: "ParallaxLayer",
2528
3045
  src,
@@ -2538,7 +3055,7 @@ function ParallaxLayerInner({
2538
3055
  });
2539
3056
  return () => engine.ecs.removeComponent(entityId, "ParallaxLayer");
2540
3057
  }, []);
2541
- useEffect15(() => {
3058
+ useEffect17(() => {
2542
3059
  const layer = engine.ecs.getComponent(entityId, "ParallaxLayer");
2543
3060
  if (!layer)
2544
3061
  return;
@@ -2620,47 +3137,111 @@ var ScreenFlash = forwardRef((_, ref) => {
2620
3137
  });
2621
3138
  ScreenFlash.displayName = "ScreenFlash";
2622
3139
  // src/hooks/useGame.ts
2623
- import { useContext as useContext14 } from "react";
3140
+ import { useContext as useContext16 } from "react";
2624
3141
  function useGame() {
2625
- const engine = useContext14(EngineContext);
3142
+ const engine = useContext16(EngineContext);
2626
3143
  if (!engine)
2627
3144
  throw new Error("useGame must be used inside <Game>");
2628
3145
  return engine;
2629
3146
  }
3147
+ // src/hooks/useCamera.ts
3148
+ import { useMemo } from "react";
3149
+ function useCamera() {
3150
+ const engine = useGame();
3151
+ return useMemo(() => ({
3152
+ shake(intensity, duration) {
3153
+ engine.renderSystem?.triggerShake(intensity, duration);
3154
+ },
3155
+ setFollowOffset(x, y) {
3156
+ const camId = engine.ecs.queryOne("Camera2D");
3157
+ if (camId === undefined)
3158
+ return;
3159
+ const cam = engine.ecs.getComponent(camId, "Camera2D");
3160
+ if (cam) {
3161
+ cam.followOffsetX = x;
3162
+ cam.followOffsetY = y;
3163
+ }
3164
+ },
3165
+ setPosition(x, y) {
3166
+ const camId = engine.ecs.queryOne("Camera2D");
3167
+ if (camId === undefined)
3168
+ return;
3169
+ const cam = engine.ecs.getComponent(camId, "Camera2D");
3170
+ if (cam) {
3171
+ cam.x = x;
3172
+ cam.y = y;
3173
+ }
3174
+ },
3175
+ setZoom(zoom) {
3176
+ const camId = engine.ecs.queryOne("Camera2D");
3177
+ if (camId === undefined)
3178
+ return;
3179
+ const cam = engine.ecs.getComponent(camId, "Camera2D");
3180
+ if (cam)
3181
+ cam.zoom = zoom;
3182
+ }
3183
+ }), [engine]);
3184
+ }
3185
+ // src/hooks/useSnapshot.ts
3186
+ import { useMemo as useMemo2 } from "react";
3187
+ function useSnapshot() {
3188
+ const engine = useGame();
3189
+ return useMemo2(() => ({
3190
+ save: () => engine.ecs.getSnapshot(),
3191
+ restore: (snapshot) => engine.ecs.restoreSnapshot(snapshot)
3192
+ }), [engine]);
3193
+ }
2630
3194
  // src/hooks/useEntity.ts
2631
- import { useContext as useContext15 } from "react";
3195
+ import { useContext as useContext17 } from "react";
2632
3196
  function useEntity() {
2633
- const id = useContext15(EntityContext);
3197
+ const id = useContext17(EntityContext);
2634
3198
  if (id === null)
2635
3199
  throw new Error("useEntity must be used inside <Entity>");
2636
3200
  return id;
2637
3201
  }
2638
3202
  // src/hooks/useInput.ts
2639
- import { useContext as useContext16 } from "react";
3203
+ import { useContext as useContext18 } from "react";
2640
3204
  function useInput() {
2641
- const engine = useContext16(EngineContext);
3205
+ const engine = useContext18(EngineContext);
2642
3206
  if (!engine)
2643
3207
  throw new Error("useInput must be used inside <Game>");
2644
3208
  return engine.input;
2645
3209
  }
3210
+ // src/hooks/useInputMap.ts
3211
+ import { useMemo as useMemo3 } from "react";
3212
+ function useInputMap(bindings) {
3213
+ const input = useInput();
3214
+ const normalized = useMemo3(() => {
3215
+ const out = {};
3216
+ for (const [action, keys] of Object.entries(bindings)) {
3217
+ out[action] = Array.isArray(keys) ? keys : [keys];
3218
+ }
3219
+ return out;
3220
+ }, []);
3221
+ return useMemo3(() => ({
3222
+ isActionDown: (action) => (normalized[action] ?? []).some((k) => input.isDown(k)),
3223
+ isActionPressed: (action) => (normalized[action] ?? []).some((k) => input.isPressed(k)),
3224
+ isActionReleased: (action) => (normalized[action] ?? []).some((k) => input.isReleased(k))
3225
+ }), [input, normalized]);
3226
+ }
2646
3227
  // src/hooks/useEvents.ts
2647
- import { useContext as useContext17, useEffect as useEffect16 } from "react";
3228
+ import { useContext as useContext19, useEffect as useEffect18 } from "react";
2648
3229
  function useEvents() {
2649
- const engine = useContext17(EngineContext);
3230
+ const engine = useContext19(EngineContext);
2650
3231
  if (!engine)
2651
3232
  throw new Error("useEvents must be used inside <Game>");
2652
3233
  return engine.events;
2653
3234
  }
2654
3235
  function useEvent(event, handler) {
2655
3236
  const events = useEvents();
2656
- useEffect16(() => {
3237
+ useEffect18(() => {
2657
3238
  return events.on(event, handler);
2658
3239
  }, [events, event]);
2659
3240
  }
2660
3241
  // src/hooks/usePlatformerController.ts
2661
- import { useContext as useContext18, useEffect as useEffect17 } from "react";
3242
+ import { useContext as useContext20, useEffect as useEffect19 } from "react";
2662
3243
  function usePlatformerController(entityId, opts = {}) {
2663
- const engine = useContext18(EngineContext);
3244
+ const engine = useContext20(EngineContext);
2664
3245
  const {
2665
3246
  speed = 200,
2666
3247
  jumpForce = -500,
@@ -2668,7 +3249,7 @@ function usePlatformerController(entityId, opts = {}) {
2668
3249
  coyoteTime = 0.08,
2669
3250
  jumpBuffer = 0.08
2670
3251
  } = opts;
2671
- useEffect17(() => {
3252
+ useEffect19(() => {
2672
3253
  const state = { coyoteTimer: 0, jumpBuffer: 0, jumpsLeft: maxJumps };
2673
3254
  const updateFn = (id, world2, input, dt) => {
2674
3255
  if (!world2.hasEntity(id))
@@ -2717,11 +3298,11 @@ function usePlatformerController(entityId, opts = {}) {
2717
3298
  }, []);
2718
3299
  }
2719
3300
  // src/hooks/useTopDownMovement.ts
2720
- import { useContext as useContext19, useEffect as useEffect18 } from "react";
3301
+ import { useContext as useContext21, useEffect as useEffect20 } from "react";
2721
3302
  function useTopDownMovement(entityId, opts = {}) {
2722
- const engine = useContext19(EngineContext);
3303
+ const engine = useContext21(EngineContext);
2723
3304
  const { speed = 200, normalizeDiagonal = true } = opts;
2724
- useEffect18(() => {
3305
+ useEffect20(() => {
2725
3306
  const updateFn = (id, world2, input) => {
2726
3307
  if (!world2.hasEntity(id))
2727
3308
  return;
@@ -2755,15 +3336,31 @@ function createAtlas(names, _columns) {
2755
3336
  return atlas;
2756
3337
  }
2757
3338
  export {
3339
+ useTriggerExit,
3340
+ useTriggerEnter,
2758
3341
  useTopDownMovement,
3342
+ useSnapshot,
2759
3343
  usePlatformerController,
3344
+ useInputMap,
2760
3345
  useInput,
2761
3346
  useGame,
2762
3347
  useEvents,
2763
3348
  useEvent,
2764
3349
  useEntity,
3350
+ useCollisionExit,
3351
+ useCollisionEnter,
3352
+ useCircleExit,
3353
+ useCircleEnter,
3354
+ useCamera,
2765
3355
  tween,
3356
+ raycast,
3357
+ overlapBox,
3358
+ findByTag,
2766
3359
  definePlugin,
3360
+ createTransform,
3361
+ createTag,
3362
+ createSprite,
3363
+ createInputMap,
2767
3364
  createAtlas,
2768
3365
  World,
2769
3366
  Transform,
@@ -2779,6 +3376,7 @@ export {
2779
3376
  Game,
2780
3377
  Entity,
2781
3378
  Ease,
3379
+ CircleCollider,
2782
3380
  Checkpoint,
2783
3381
  Camera2D,
2784
3382
  BoxCollider,