cubeforge 0.3.12 → 0.3.14
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/README.md +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +450 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,8 +32,8 @@ npm install cubeforge react react-dom
|
|
|
32
32
|
## What's included
|
|
33
33
|
|
|
34
34
|
- **ECS** — archetype-based entity-component-system with query caching
|
|
35
|
-
- **Physics** — two-pass AABB,
|
|
36
|
-
- **Renderer** — WebGL2 instanced renderer by default
|
|
35
|
+
- **Physics** — two-pass AABB, kinematic bodies, one-way platforms, 60 Hz fixed timestep
|
|
36
|
+
- **Renderer** — WebGL2 instanced renderer by default
|
|
37
37
|
- **Input** — keyboard, mouse, gamepad, per-player input maps, input contexts, recording/playback
|
|
38
38
|
- **Audio** — Web Audio API with volume groups, fade, crossfade, ducking (`useSound`)
|
|
39
39
|
- **Gameplay hooks** — `usePlatformerController`, `useTopDownMovement`, `useHealth`, `useSave`, `useGameStateMachine`, `useLevelTransition`, `usePathfinding`, `useAISteering`, and more
|
package/dist/index.d.ts
CHANGED
|
@@ -153,8 +153,10 @@ interface RigidBodyProps {
|
|
|
153
153
|
lockX?: boolean;
|
|
154
154
|
/** Prevent any vertical movement — velocity.y is zeroed every frame (disables gravity) */
|
|
155
155
|
lockY?: boolean;
|
|
156
|
+
/** Enable continuous collision detection to prevent tunneling through thin colliders */
|
|
157
|
+
ccd?: boolean;
|
|
156
158
|
}
|
|
157
|
-
declare function RigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY, }: RigidBodyProps): null;
|
|
159
|
+
declare function RigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY, ccd, }: RigidBodyProps): null;
|
|
158
160
|
|
|
159
161
|
interface BoxColliderProps {
|
|
160
162
|
width: number;
|
package/dist/index.js
CHANGED
|
@@ -249,14 +249,16 @@ function findByTag(world, tag) {
|
|
|
249
249
|
|
|
250
250
|
// ../../packages/core/src/loop/gameLoop.ts
|
|
251
251
|
var GameLoop = class {
|
|
252
|
-
constructor(onTick) {
|
|
252
|
+
constructor(onTick, options) {
|
|
253
253
|
this.onTick = onTick;
|
|
254
|
+
this.fixedDt = options?.fixedDt;
|
|
254
255
|
}
|
|
255
256
|
rafId = 0;
|
|
256
257
|
lastTime = 0;
|
|
257
258
|
running = false;
|
|
258
259
|
paused = false;
|
|
259
260
|
hitPauseTimer = 0;
|
|
261
|
+
fixedDt;
|
|
260
262
|
hitPause(duration) {
|
|
261
263
|
this.hitPauseTimer = duration;
|
|
262
264
|
}
|
|
@@ -292,8 +294,9 @@ var GameLoop = class {
|
|
|
292
294
|
}
|
|
293
295
|
frame = (time) => {
|
|
294
296
|
if (!this.running) return;
|
|
295
|
-
const
|
|
297
|
+
const rawDt = Math.min((time - this.lastTime) / 1e3, 0.1);
|
|
296
298
|
this.lastTime = time;
|
|
299
|
+
const dt = this.fixedDt ?? rawDt;
|
|
297
300
|
if (this.hitPauseTimer > 0) {
|
|
298
301
|
this.hitPauseTimer -= dt;
|
|
299
302
|
} else {
|
|
@@ -345,6 +348,11 @@ var AssetManager = class {
|
|
|
345
348
|
_loaded = 0;
|
|
346
349
|
_total = 0;
|
|
347
350
|
_progressListeners = /* @__PURE__ */ new Set();
|
|
351
|
+
/** Base URL prefix applied to all asset paths starting with '/'. Set by Game component. */
|
|
352
|
+
baseURL = "";
|
|
353
|
+
resolve(src) {
|
|
354
|
+
return this.baseURL && src.startsWith("/") ? this.baseURL + src : src;
|
|
355
|
+
}
|
|
348
356
|
getAudioContext() {
|
|
349
357
|
if (!this.audioCtx) {
|
|
350
358
|
this.audioCtx = new AudioContext();
|
|
@@ -373,28 +381,29 @@ var AssetManager = class {
|
|
|
373
381
|
return () => this._progressListeners.delete(cb);
|
|
374
382
|
}
|
|
375
383
|
async loadImage(src) {
|
|
376
|
-
|
|
384
|
+
const resolved = this.resolve(src);
|
|
385
|
+
if (this.imagePromises.has(resolved)) return this.imagePromises.get(resolved);
|
|
377
386
|
this._total++;
|
|
378
387
|
this.emitProgress();
|
|
379
388
|
const promise = (async () => {
|
|
380
389
|
const img = new Image();
|
|
381
|
-
img.src =
|
|
390
|
+
img.src = resolved;
|
|
382
391
|
try {
|
|
383
392
|
await new Promise((resolve, reject) => {
|
|
384
393
|
img.onload = () => resolve();
|
|
385
394
|
img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
|
|
386
395
|
});
|
|
387
396
|
} catch (err) {
|
|
388
|
-
console.warn(`[Cubeforge] Failed to load image: ${
|
|
397
|
+
console.warn(`[Cubeforge] Failed to load image: ${resolved}`);
|
|
389
398
|
throw err;
|
|
390
399
|
} finally {
|
|
391
400
|
this._loaded++;
|
|
392
401
|
this.emitProgress();
|
|
393
402
|
}
|
|
394
|
-
this.images.set(
|
|
403
|
+
this.images.set(resolved, img);
|
|
395
404
|
return img;
|
|
396
405
|
})();
|
|
397
|
-
this.imagePromises.set(
|
|
406
|
+
this.imagePromises.set(resolved, promise);
|
|
398
407
|
return promise;
|
|
399
408
|
}
|
|
400
409
|
/** Resolves once every image that has been requested via loadImage() is settled. */
|
|
@@ -402,23 +411,24 @@ var AssetManager = class {
|
|
|
402
411
|
await Promise.allSettled([...this.imagePromises.values()]);
|
|
403
412
|
}
|
|
404
413
|
getImage(src) {
|
|
405
|
-
return this.images.get(src);
|
|
414
|
+
return this.images.get(this.resolve(src));
|
|
406
415
|
}
|
|
407
416
|
/** Returns a read-only snapshot of all loaded images keyed by src. */
|
|
408
417
|
getLoadedImages() {
|
|
409
418
|
return this.images;
|
|
410
419
|
}
|
|
411
420
|
async loadAudio(src) {
|
|
412
|
-
|
|
421
|
+
const resolved = this.resolve(src);
|
|
422
|
+
if (this.audio.has(resolved)) return this.audio.get(resolved);
|
|
413
423
|
const ctx = this.getAudioContext();
|
|
414
424
|
try {
|
|
415
|
-
const response = await fetch(
|
|
425
|
+
const response = await fetch(resolved);
|
|
416
426
|
const arrayBuffer = await response.arrayBuffer();
|
|
417
427
|
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
|
418
|
-
this.audio.set(
|
|
428
|
+
this.audio.set(resolved, audioBuffer);
|
|
419
429
|
return audioBuffer;
|
|
420
430
|
} catch (err) {
|
|
421
|
-
console.warn(`[Cubeforge] Failed to load audio: ${
|
|
431
|
+
console.warn(`[Cubeforge] Failed to load audio: ${resolved}`);
|
|
422
432
|
throw err;
|
|
423
433
|
}
|
|
424
434
|
}
|
|
@@ -940,11 +950,16 @@ var Mouse = class {
|
|
|
940
950
|
var InputManager = class {
|
|
941
951
|
keyboard = new Keyboard();
|
|
942
952
|
mouse = new Mouse();
|
|
953
|
+
_attachedElement = null;
|
|
943
954
|
attach(canvas) {
|
|
955
|
+
if (this._attachedElement === canvas) return;
|
|
956
|
+
if (this._attachedElement) this.detach();
|
|
957
|
+
this._attachedElement = canvas;
|
|
944
958
|
this.keyboard.attach(window);
|
|
945
959
|
this.mouse.attach(canvas);
|
|
946
960
|
}
|
|
947
961
|
detach() {
|
|
962
|
+
this._attachedElement = null;
|
|
948
963
|
this.keyboard.detach();
|
|
949
964
|
this.mouse.detach();
|
|
950
965
|
}
|
|
@@ -1384,6 +1399,7 @@ function parseCSSColor(css) {
|
|
|
1384
1399
|
// ../../packages/renderer/src/webglRenderSystem.ts
|
|
1385
1400
|
var FLOATS_PER_INSTANCE = 18;
|
|
1386
1401
|
var MAX_INSTANCES = 8192;
|
|
1402
|
+
var MAX_SPRITE_TEXTURES = 512;
|
|
1387
1403
|
var MAX_TEXT_CACHE = 200;
|
|
1388
1404
|
function compileShader(gl, type, src) {
|
|
1389
1405
|
const shader = gl.createShader(type);
|
|
@@ -1564,6 +1580,8 @@ var RenderSystem = class {
|
|
|
1564
1580
|
instanceData;
|
|
1565
1581
|
whiteTexture;
|
|
1566
1582
|
textures = /* @__PURE__ */ new Map();
|
|
1583
|
+
/** Tracks texture access order for LRU eviction (most recent at end). */
|
|
1584
|
+
textureLRU = [];
|
|
1567
1585
|
imageCache = /* @__PURE__ */ new Map();
|
|
1568
1586
|
// Cached uniform locations — sprite program
|
|
1569
1587
|
uCamPos;
|
|
@@ -1596,6 +1614,21 @@ var RenderSystem = class {
|
|
|
1596
1614
|
getDefaultSampling() {
|
|
1597
1615
|
return this._defaultSampling;
|
|
1598
1616
|
}
|
|
1617
|
+
// ── Sprite texture LRU helpers ──────────────────────────────────────────
|
|
1618
|
+
/** Record a texture key as recently used; evict LRU entries if over limit. */
|
|
1619
|
+
touchTexture(key) {
|
|
1620
|
+
const idx = this.textureLRU.indexOf(key);
|
|
1621
|
+
if (idx !== -1) this.textureLRU.splice(idx, 1);
|
|
1622
|
+
this.textureLRU.push(key);
|
|
1623
|
+
while (this.textureLRU.length > MAX_SPRITE_TEXTURES) {
|
|
1624
|
+
const evict = this.textureLRU.shift();
|
|
1625
|
+
const tex = this.textures.get(evict);
|
|
1626
|
+
if (tex) {
|
|
1627
|
+
this.gl.deleteTexture(tex);
|
|
1628
|
+
this.textures.delete(evict);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1599
1632
|
// ── Debug overlays ──────────────────────────────────────────────────────
|
|
1600
1633
|
debugNavGrid = null;
|
|
1601
1634
|
contactFlashPoints = [];
|
|
@@ -1613,7 +1646,10 @@ var RenderSystem = class {
|
|
|
1613
1646
|
// ── Texture management (sprite textures — CLAMP_TO_EDGE) ──────────────────
|
|
1614
1647
|
loadTexture(src) {
|
|
1615
1648
|
const cached = this.textures.get(src);
|
|
1616
|
-
if (cached)
|
|
1649
|
+
if (cached) {
|
|
1650
|
+
this.touchTexture(src);
|
|
1651
|
+
return cached;
|
|
1652
|
+
}
|
|
1617
1653
|
let imgSrc = src;
|
|
1618
1654
|
const sampIdx = imgSrc.indexOf(":s=");
|
|
1619
1655
|
if (sampIdx !== -1) imgSrc = imgSrc.slice(0, sampIdx);
|
|
@@ -1629,6 +1665,7 @@ var RenderSystem = class {
|
|
|
1629
1665
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1630
1666
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1631
1667
|
this.textures.set(src, tex);
|
|
1668
|
+
this.touchTexture(src);
|
|
1632
1669
|
return tex;
|
|
1633
1670
|
}
|
|
1634
1671
|
if (!existing) {
|
|
@@ -1646,6 +1683,11 @@ var RenderSystem = class {
|
|
|
1646
1683
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
|
|
1647
1684
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
|
|
1648
1685
|
this.textures.set(src, tex);
|
|
1686
|
+
this.touchTexture(src);
|
|
1687
|
+
};
|
|
1688
|
+
img.onerror = () => {
|
|
1689
|
+
console.warn(`[WebGLRenderSystem] Failed to load image: ${imgSrc}`);
|
|
1690
|
+
this.imageCache.delete(imgSrc);
|
|
1649
1691
|
};
|
|
1650
1692
|
this.imageCache.set(imgSrc, img);
|
|
1651
1693
|
}
|
|
@@ -1936,6 +1978,9 @@ var RenderSystem = class {
|
|
|
1936
1978
|
gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_S, wrap);
|
|
1937
1979
|
gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_WRAP_T, wrap);
|
|
1938
1980
|
this.textures.set(cacheKey, tex);
|
|
1981
|
+
this.touchTexture(cacheKey);
|
|
1982
|
+
} else if (cacheKey) {
|
|
1983
|
+
this.touchTexture(cacheKey);
|
|
1939
1984
|
}
|
|
1940
1985
|
} else if (sprite.src && !sprite.image) {
|
|
1941
1986
|
let img = this.imageCache.get(sprite.src);
|
|
@@ -1951,6 +1996,11 @@ var RenderSystem = class {
|
|
|
1951
1996
|
gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MIN_FILTER, gl2.NEAREST);
|
|
1952
1997
|
gl2.texParameteri(gl2.TEXTURE_2D, gl2.TEXTURE_MAG_FILTER, gl2.NEAREST);
|
|
1953
1998
|
this.textures.set(img.src, tex);
|
|
1999
|
+
this.touchTexture(img.src);
|
|
2000
|
+
};
|
|
2001
|
+
img.onerror = () => {
|
|
2002
|
+
console.warn(`[WebGLRenderSystem] Failed to load image: ${img.src}`);
|
|
2003
|
+
this.imageCache.delete(img.src);
|
|
1954
2004
|
};
|
|
1955
2005
|
}
|
|
1956
2006
|
sprite.image = img;
|
|
@@ -2259,6 +2309,7 @@ function createRigidBody(opts) {
|
|
|
2259
2309
|
lockY: false,
|
|
2260
2310
|
isKinematic: false,
|
|
2261
2311
|
dropThrough: 0,
|
|
2312
|
+
ccd: false,
|
|
2262
2313
|
...opts
|
|
2263
2314
|
};
|
|
2264
2315
|
}
|
|
@@ -2330,6 +2381,14 @@ function getAABB(transform, collider) {
|
|
|
2330
2381
|
hh: collider.height / 2
|
|
2331
2382
|
};
|
|
2332
2383
|
}
|
|
2384
|
+
function getCapsuleAABB(transform, capsule) {
|
|
2385
|
+
return {
|
|
2386
|
+
cx: transform.x + capsule.offsetX,
|
|
2387
|
+
cy: transform.y + capsule.offsetY,
|
|
2388
|
+
hw: capsule.width / 2,
|
|
2389
|
+
hh: capsule.height / 2
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2333
2392
|
function getOverlap(a, b) {
|
|
2334
2393
|
const dx = a.cx - b.cx;
|
|
2335
2394
|
const dy = a.cy - b.cy;
|
|
@@ -2432,6 +2491,36 @@ function getCompoundBounds(tx, ty, shapes) {
|
|
|
2432
2491
|
function canInteractGeneric(aLayer, aMask, bLayer, bMask) {
|
|
2433
2492
|
return maskAllows(aMask, bLayer) && maskAllows(bMask, aLayer);
|
|
2434
2493
|
}
|
|
2494
|
+
function sweepAABB(aCx, aCy, aHw, aHh, dx, dy, b) {
|
|
2495
|
+
const eHw = b.hw + aHw;
|
|
2496
|
+
const eHh = b.hh + aHh;
|
|
2497
|
+
const left = b.cx - eHw;
|
|
2498
|
+
const right = b.cx + eHw;
|
|
2499
|
+
const top = b.cy - eHh;
|
|
2500
|
+
const bottom = b.cy + eHh;
|
|
2501
|
+
let tmin = -Infinity;
|
|
2502
|
+
let tmax = Infinity;
|
|
2503
|
+
if (dx !== 0) {
|
|
2504
|
+
const t1 = (left - aCx) / dx;
|
|
2505
|
+
const t2 = (right - aCx) / dx;
|
|
2506
|
+
tmin = Math.max(tmin, Math.min(t1, t2));
|
|
2507
|
+
tmax = Math.min(tmax, Math.max(t1, t2));
|
|
2508
|
+
} else if (aCx < left || aCx > right) {
|
|
2509
|
+
return null;
|
|
2510
|
+
}
|
|
2511
|
+
if (dy !== 0) {
|
|
2512
|
+
const t1 = (top - aCy) / dy;
|
|
2513
|
+
const t2 = (bottom - aCy) / dy;
|
|
2514
|
+
tmin = Math.max(tmin, Math.min(t1, t2));
|
|
2515
|
+
tmax = Math.min(tmax, Math.max(t1, t2));
|
|
2516
|
+
} else if (aCy < top || aCy > bottom) {
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
if (tmax < 0 || tmin > tmax || tmin > 1) return null;
|
|
2520
|
+
const t = Math.max(0, tmin);
|
|
2521
|
+
if (t > 1) return null;
|
|
2522
|
+
return t;
|
|
2523
|
+
}
|
|
2435
2524
|
function pairKey(a, b) {
|
|
2436
2525
|
return a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
2437
2526
|
}
|
|
@@ -2442,11 +2531,15 @@ var PhysicsSystem = class {
|
|
|
2442
2531
|
}
|
|
2443
2532
|
accumulator = 0;
|
|
2444
2533
|
FIXED_DT = 1 / 60;
|
|
2534
|
+
/** Maximum accumulated time (seconds). Prevents hundreds of sub-steps when
|
|
2535
|
+
* the tab is backgrounded and dt spikes on resume. 0.1s ≈ 6 steps at 60Hz. */
|
|
2536
|
+
MAX_ACCUMULATOR = 0.1;
|
|
2445
2537
|
// Active contact sets — updated each physics step.
|
|
2446
2538
|
activeTriggerPairs = /* @__PURE__ */ new Map();
|
|
2447
2539
|
activeCollisionPairs = /* @__PURE__ */ new Map();
|
|
2448
2540
|
activeCirclePairs = /* @__PURE__ */ new Map();
|
|
2449
2541
|
activeCompoundPairs = /* @__PURE__ */ new Map();
|
|
2542
|
+
activeCapsulePairs = /* @__PURE__ */ new Map();
|
|
2450
2543
|
// Previous-frame positions of static entities — used to compute platform carry delta.
|
|
2451
2544
|
staticPrevPos = /* @__PURE__ */ new Map();
|
|
2452
2545
|
setGravity(g) {
|
|
@@ -2454,8 +2547,8 @@ var PhysicsSystem = class {
|
|
|
2454
2547
|
}
|
|
2455
2548
|
update(world, dt) {
|
|
2456
2549
|
this.accumulator += dt;
|
|
2457
|
-
if (this.accumulator >
|
|
2458
|
-
this.accumulator =
|
|
2550
|
+
if (this.accumulator > this.MAX_ACCUMULATOR) {
|
|
2551
|
+
this.accumulator = this.MAX_ACCUMULATOR;
|
|
2459
2552
|
}
|
|
2460
2553
|
while (this.accumulator >= this.FIXED_DT) {
|
|
2461
2554
|
this.step(world, this.FIXED_DT);
|
|
@@ -2483,6 +2576,12 @@ var PhysicsSystem = class {
|
|
|
2483
2576
|
if (rb.isStatic) statics.push(id);
|
|
2484
2577
|
else dynamics.push(id);
|
|
2485
2578
|
}
|
|
2579
|
+
const allCapsule = world.query("Transform", "RigidBody", "CapsuleCollider");
|
|
2580
|
+
const capsuleDynamics = [];
|
|
2581
|
+
for (const id of allCapsule) {
|
|
2582
|
+
const rb = world.getComponent(id, "RigidBody");
|
|
2583
|
+
if (!rb.isStatic) capsuleDynamics.push(id);
|
|
2584
|
+
}
|
|
2486
2585
|
for (const [key, [a, b]] of this.activeTriggerPairs) {
|
|
2487
2586
|
if (!world.hasEntity(a) || !world.hasEntity(b)) {
|
|
2488
2587
|
this.events?.emit("triggerExit", { a, b });
|
|
@@ -2507,6 +2606,12 @@ var PhysicsSystem = class {
|
|
|
2507
2606
|
this.activeCompoundPairs.delete(key);
|
|
2508
2607
|
}
|
|
2509
2608
|
}
|
|
2609
|
+
for (const [key, [a, b]] of this.activeCapsulePairs) {
|
|
2610
|
+
if (!world.hasEntity(a) || !world.hasEntity(b)) {
|
|
2611
|
+
this.events?.emit("capsuleExit", { a, b });
|
|
2612
|
+
this.activeCapsulePairs.delete(key);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2510
2615
|
const staticDelta = /* @__PURE__ */ new Map();
|
|
2511
2616
|
for (const sid of statics) {
|
|
2512
2617
|
const st = world.getComponent(sid, "Transform");
|
|
@@ -2541,6 +2646,24 @@ var PhysicsSystem = class {
|
|
|
2541
2646
|
if (rb.lockY) rb.vy = 0;
|
|
2542
2647
|
if (rb.dropThrough > 0) rb.dropThrough--;
|
|
2543
2648
|
}
|
|
2649
|
+
const ccdPrev = /* @__PURE__ */ new Map();
|
|
2650
|
+
for (const id of dynamics) {
|
|
2651
|
+
const rb = world.getComponent(id, "RigidBody");
|
|
2652
|
+
if (rb.ccd) {
|
|
2653
|
+
const t = world.getComponent(id, "Transform");
|
|
2654
|
+
ccdPrev.set(id, { x: t.x, y: t.y });
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
for (const id of capsuleDynamics) {
|
|
2658
|
+
const rb = world.getComponent(id, "RigidBody");
|
|
2659
|
+
rb.onGround = false;
|
|
2660
|
+
rb.isNearGround = false;
|
|
2661
|
+
if (rb.isKinematic) continue;
|
|
2662
|
+
if (!rb.lockY) rb.vy += this.gravity * rb.gravityScale * dt;
|
|
2663
|
+
if (rb.lockX) rb.vx = 0;
|
|
2664
|
+
if (rb.lockY) rb.vy = 0;
|
|
2665
|
+
if (rb.dropThrough > 0) rb.dropThrough--;
|
|
2666
|
+
}
|
|
2544
2667
|
for (const id of dynamics) {
|
|
2545
2668
|
const transform = world.getComponent(id, "Transform");
|
|
2546
2669
|
const rb = world.getComponent(id, "RigidBody");
|
|
@@ -2571,6 +2694,36 @@ var PhysicsSystem = class {
|
|
|
2571
2694
|
}
|
|
2572
2695
|
}
|
|
2573
2696
|
}
|
|
2697
|
+
for (const id of capsuleDynamics) {
|
|
2698
|
+
const transform = world.getComponent(id, "Transform");
|
|
2699
|
+
const rb = world.getComponent(id, "RigidBody");
|
|
2700
|
+
const cap = world.getComponent(id, "CapsuleCollider");
|
|
2701
|
+
transform.x += rb.vx * dt;
|
|
2702
|
+
if (!cap.isTrigger) {
|
|
2703
|
+
const dynAABB = getCapsuleAABB(transform, cap);
|
|
2704
|
+
const candidateCells = this.getCells(dynAABB.cx, dynAABB.cy, dynAABB.hw, dynAABB.hh);
|
|
2705
|
+
const checked = /* @__PURE__ */ new Set();
|
|
2706
|
+
for (const cell of candidateCells) {
|
|
2707
|
+
const bucket = staticGrid.get(cell);
|
|
2708
|
+
if (!bucket) continue;
|
|
2709
|
+
for (const sid of bucket) {
|
|
2710
|
+
if (checked.has(sid)) continue;
|
|
2711
|
+
checked.add(sid);
|
|
2712
|
+
const st = world.getComponent(sid, "Transform");
|
|
2713
|
+
const sc = world.getComponent(sid, "BoxCollider");
|
|
2714
|
+
if (sc.isTrigger) continue;
|
|
2715
|
+
if (sc.slope !== 0) continue;
|
|
2716
|
+
if (!canInteractGeneric(cap.layer, cap.mask, sc.layer, sc.mask)) continue;
|
|
2717
|
+
const ov = getOverlap(getCapsuleAABB(transform, cap), getAABB(st, sc));
|
|
2718
|
+
if (!ov) continue;
|
|
2719
|
+
if (Math.abs(ov.x) < Math.abs(ov.y)) {
|
|
2720
|
+
transform.x += ov.x;
|
|
2721
|
+
rb.vx = rb.bounce > 0 ? -rb.vx * rb.bounce : 0;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2574
2727
|
for (const id of dynamics) {
|
|
2575
2728
|
const transform = world.getComponent(id, "Transform");
|
|
2576
2729
|
const rb = world.getComponent(id, "RigidBody");
|
|
@@ -2630,6 +2783,127 @@ var PhysicsSystem = class {
|
|
|
2630
2783
|
}
|
|
2631
2784
|
}
|
|
2632
2785
|
}
|
|
2786
|
+
for (const [id, prev] of ccdPrev) {
|
|
2787
|
+
const transform = world.getComponent(id, "Transform");
|
|
2788
|
+
const rb = world.getComponent(id, "RigidBody");
|
|
2789
|
+
const col = world.getComponent(id, "BoxCollider");
|
|
2790
|
+
const totalDx = transform.x - prev.x;
|
|
2791
|
+
const totalDy = transform.y - prev.y;
|
|
2792
|
+
const moveLen = Math.abs(totalDx) + Math.abs(totalDy);
|
|
2793
|
+
const halfSize = Math.min(col.width, col.height) / 2;
|
|
2794
|
+
if (moveLen <= halfSize) continue;
|
|
2795
|
+
const startCx = prev.x + col.offsetX;
|
|
2796
|
+
const startCy = prev.y + col.offsetY;
|
|
2797
|
+
const sweepDx = totalDx;
|
|
2798
|
+
const sweepDy = totalDy;
|
|
2799
|
+
const hw = col.width / 2;
|
|
2800
|
+
const hh = col.height / 2;
|
|
2801
|
+
let earliestT = 1;
|
|
2802
|
+
let hitSid = null;
|
|
2803
|
+
const endCx = startCx + sweepDx;
|
|
2804
|
+
const endCy = startCy + sweepDy;
|
|
2805
|
+
const minCx = Math.min(startCx, endCx);
|
|
2806
|
+
const maxCx = Math.max(startCx, endCx);
|
|
2807
|
+
const minCy = Math.min(startCy, endCy);
|
|
2808
|
+
const maxCy = Math.max(startCy, endCy);
|
|
2809
|
+
const sweepCells = this.getCells(
|
|
2810
|
+
(minCx + maxCx) / 2,
|
|
2811
|
+
(minCy + maxCy) / 2,
|
|
2812
|
+
(maxCx - minCx) / 2 + hw,
|
|
2813
|
+
(maxCy - minCy) / 2 + hh
|
|
2814
|
+
);
|
|
2815
|
+
const checked = /* @__PURE__ */ new Set();
|
|
2816
|
+
for (const cell of sweepCells) {
|
|
2817
|
+
const bucket = staticGrid.get(cell);
|
|
2818
|
+
if (!bucket) continue;
|
|
2819
|
+
for (const sid of bucket) {
|
|
2820
|
+
if (checked.has(sid)) continue;
|
|
2821
|
+
checked.add(sid);
|
|
2822
|
+
const st = world.getComponent(sid, "Transform");
|
|
2823
|
+
const sc = world.getComponent(sid, "BoxCollider");
|
|
2824
|
+
if (sc.isTrigger) continue;
|
|
2825
|
+
if (!canInteract(col, sc)) continue;
|
|
2826
|
+
const staticAABB = getAABB(st, sc);
|
|
2827
|
+
const t = sweepAABB(startCx, startCy, hw, hh, sweepDx, sweepDy, staticAABB);
|
|
2828
|
+
if (t !== null && t < earliestT) {
|
|
2829
|
+
earliestT = t;
|
|
2830
|
+
hitSid = sid;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
if (hitSid !== null && earliestT < 1) {
|
|
2835
|
+
const eps = 0.01;
|
|
2836
|
+
const clampedT = Math.max(0, earliestT - eps / (Math.hypot(sweepDx, sweepDy) || 1));
|
|
2837
|
+
transform.x = prev.x + totalDx * clampedT;
|
|
2838
|
+
transform.y = prev.y + totalDy * clampedT;
|
|
2839
|
+
const st = world.getComponent(hitSid, "Transform");
|
|
2840
|
+
const sc = world.getComponent(hitSid, "BoxCollider");
|
|
2841
|
+
const staticAABB = getAABB(st, sc);
|
|
2842
|
+
const contactCx = prev.x + col.offsetX + totalDx * earliestT;
|
|
2843
|
+
const contactCy = prev.y + col.offsetY + totalDy * earliestT;
|
|
2844
|
+
const dxFromCenter = contactCx - staticAABB.cx;
|
|
2845
|
+
const dyFromCenter = contactCy - staticAABB.cy;
|
|
2846
|
+
const overlapX = hw + staticAABB.hw - Math.abs(dxFromCenter);
|
|
2847
|
+
const overlapY = hh + staticAABB.hh - Math.abs(dyFromCenter);
|
|
2848
|
+
if (overlapX > overlapY) {
|
|
2849
|
+
rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
|
|
2850
|
+
if (dyFromCenter < 0) {
|
|
2851
|
+
rb.onGround = true;
|
|
2852
|
+
if (rb.friction < 1) rb.vx *= rb.friction;
|
|
2853
|
+
}
|
|
2854
|
+
} else {
|
|
2855
|
+
rb.vx = rb.bounce > 0 ? -rb.vx * rb.bounce : 0;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
for (const id of capsuleDynamics) {
|
|
2860
|
+
const transform = world.getComponent(id, "Transform");
|
|
2861
|
+
const rb = world.getComponent(id, "RigidBody");
|
|
2862
|
+
const cap = world.getComponent(id, "CapsuleCollider");
|
|
2863
|
+
transform.y += rb.vy * dt;
|
|
2864
|
+
if (!cap.isTrigger) {
|
|
2865
|
+
const dynAABB = getCapsuleAABB(transform, cap);
|
|
2866
|
+
const candidateCells = this.getCells(dynAABB.cx, dynAABB.cy, dynAABB.hw, dynAABB.hh);
|
|
2867
|
+
const checked = /* @__PURE__ */ new Set();
|
|
2868
|
+
for (const cell of candidateCells) {
|
|
2869
|
+
const bucket = staticGrid.get(cell);
|
|
2870
|
+
if (!bucket) continue;
|
|
2871
|
+
for (const sid of bucket) {
|
|
2872
|
+
if (checked.has(sid)) continue;
|
|
2873
|
+
checked.add(sid);
|
|
2874
|
+
const st = world.getComponent(sid, "Transform");
|
|
2875
|
+
const sc = world.getComponent(sid, "BoxCollider");
|
|
2876
|
+
if (sc.isTrigger) continue;
|
|
2877
|
+
if (!canInteractGeneric(cap.layer, cap.mask, sc.layer, sc.mask)) continue;
|
|
2878
|
+
if (sc.slope !== 0) continue;
|
|
2879
|
+
const ov = getOverlap(getCapsuleAABB(transform, cap), getAABB(st, sc));
|
|
2880
|
+
if (!ov) continue;
|
|
2881
|
+
if (Math.abs(ov.y) <= Math.abs(ov.x)) {
|
|
2882
|
+
if (sc.oneWay) {
|
|
2883
|
+
if (rb.dropThrough > 0) continue;
|
|
2884
|
+
if (ov.y >= 0) continue;
|
|
2885
|
+
const platformTop = st.y + sc.offsetY - sc.height / 2;
|
|
2886
|
+
const prevEntityBottom = transform.y - rb.vy * dt + cap.offsetY + cap.height / 2;
|
|
2887
|
+
if (prevEntityBottom > platformTop) continue;
|
|
2888
|
+
}
|
|
2889
|
+
transform.y += ov.y;
|
|
2890
|
+
if (ov.y < 0) {
|
|
2891
|
+
rb.onGround = true;
|
|
2892
|
+
if (rb.friction < 1) rb.vx *= rb.friction;
|
|
2893
|
+
const delta = staticDelta.get(sid);
|
|
2894
|
+
if (delta) {
|
|
2895
|
+
transform.x += delta.dx;
|
|
2896
|
+
if (delta.dy < 0) transform.y += delta.dy;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
rb.vy = rb.bounce > 0 ? -rb.vy * rb.bounce : 0;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
const POSITION_SLOP = 0.5;
|
|
2906
|
+
const CORRECTION_FACTOR = 0.4;
|
|
2633
2907
|
const currentCollisionPairs = /* @__PURE__ */ new Map();
|
|
2634
2908
|
for (let i = 0; i < dynamics.length; i++) {
|
|
2635
2909
|
for (let j = i + 1; j < dynamics.length; j++) {
|
|
@@ -2660,10 +2934,14 @@ var PhysicsSystem = class {
|
|
|
2660
2934
|
rba.onGround = true;
|
|
2661
2935
|
}
|
|
2662
2936
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2937
|
+
const absOx = Math.abs(ov.x);
|
|
2938
|
+
const absOy = Math.abs(ov.y);
|
|
2939
|
+
const corrX = absOx > POSITION_SLOP ? Math.sign(ov.x) * (absOx - POSITION_SLOP) * CORRECTION_FACTOR : 0;
|
|
2940
|
+
const corrY = absOy > POSITION_SLOP ? Math.sign(ov.y) * (absOy - POSITION_SLOP) * CORRECTION_FACTOR : 0;
|
|
2941
|
+
ta.x += corrX / 2;
|
|
2942
|
+
ta.y += corrY / 2;
|
|
2943
|
+
tb.x -= corrX / 2;
|
|
2944
|
+
tb.y -= corrY / 2;
|
|
2667
2945
|
const key = pairKey(ia, ib);
|
|
2668
2946
|
currentCollisionPairs.set(key, [ia, ib]);
|
|
2669
2947
|
}
|
|
@@ -2716,6 +2994,40 @@ var PhysicsSystem = class {
|
|
|
2716
2994
|
}
|
|
2717
2995
|
}
|
|
2718
2996
|
}
|
|
2997
|
+
for (const id of capsuleDynamics) {
|
|
2998
|
+
const rb = world.getComponent(id, "RigidBody");
|
|
2999
|
+
if (rb.onGround) {
|
|
3000
|
+
rb.isNearGround = true;
|
|
3001
|
+
continue;
|
|
3002
|
+
}
|
|
3003
|
+
const transform = world.getComponent(id, "Transform");
|
|
3004
|
+
const cap = world.getComponent(id, "CapsuleCollider");
|
|
3005
|
+
const probeAABB = {
|
|
3006
|
+
cx: transform.x + cap.offsetX,
|
|
3007
|
+
cy: transform.y + cap.offsetY + 2,
|
|
3008
|
+
hw: cap.width / 2,
|
|
3009
|
+
hh: cap.height / 2
|
|
3010
|
+
};
|
|
3011
|
+
const candidateCells = this.getCells(probeAABB.cx, probeAABB.cy, probeAABB.hw, probeAABB.hh);
|
|
3012
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3013
|
+
outerCapsule: for (const cell of candidateCells) {
|
|
3014
|
+
const bucket = staticGrid.get(cell);
|
|
3015
|
+
if (!bucket) continue;
|
|
3016
|
+
for (const sid of bucket) {
|
|
3017
|
+
if (checked.has(sid)) continue;
|
|
3018
|
+
checked.add(sid);
|
|
3019
|
+
const st = world.getComponent(sid, "Transform");
|
|
3020
|
+
const sc = world.getComponent(sid, "BoxCollider");
|
|
3021
|
+
if (sc.isTrigger) continue;
|
|
3022
|
+
if (!canInteractGeneric(cap.layer, cap.mask, sc.layer, sc.mask)) continue;
|
|
3023
|
+
const ov = getOverlap(probeAABB, getAABB(st, sc));
|
|
3024
|
+
if (ov && Math.abs(ov.y) <= Math.abs(ov.x) && ov.y < 0) {
|
|
3025
|
+
rb.isNearGround = true;
|
|
3026
|
+
break outerCapsule;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
2719
3031
|
const allWithCollider = world.query("Transform", "BoxCollider");
|
|
2720
3032
|
const currentTriggerPairs = /* @__PURE__ */ new Map();
|
|
2721
3033
|
for (let i = 0; i < allWithCollider.length; i++) {
|
|
@@ -2904,6 +3216,55 @@ var PhysicsSystem = class {
|
|
|
2904
3216
|
}
|
|
2905
3217
|
this.activeCompoundPairs = /* @__PURE__ */ new Map();
|
|
2906
3218
|
}
|
|
3219
|
+
if (allCapsule.length > 0) {
|
|
3220
|
+
const currentCapsulePairs = /* @__PURE__ */ new Map();
|
|
3221
|
+
const allBoxForCapsule = world.query("Transform", "BoxCollider");
|
|
3222
|
+
for (const cid of allCapsule) {
|
|
3223
|
+
const cc = world.getComponent(cid, "CapsuleCollider");
|
|
3224
|
+
const ct = world.getComponent(cid, "Transform");
|
|
3225
|
+
const capsuleAABB = getCapsuleAABB(ct, cc);
|
|
3226
|
+
for (const bid of allBoxForCapsule) {
|
|
3227
|
+
if (bid === cid) continue;
|
|
3228
|
+
const bc = world.getComponent(bid, "BoxCollider");
|
|
3229
|
+
if (!canInteractGeneric(cc.layer, cc.mask, bc.layer, bc.mask)) continue;
|
|
3230
|
+
const bt = world.getComponent(bid, "Transform");
|
|
3231
|
+
const ov = getOverlap(capsuleAABB, getAABB(bt, bc));
|
|
3232
|
+
if (ov) currentCapsulePairs.set(pairKey(cid, bid), [cid, bid]);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
for (let i = 0; i < allCapsule.length; i++) {
|
|
3236
|
+
for (let j = i + 1; j < allCapsule.length; j++) {
|
|
3237
|
+
const ia = allCapsule[i];
|
|
3238
|
+
const ib = allCapsule[j];
|
|
3239
|
+
const ca = world.getComponent(ia, "CapsuleCollider");
|
|
3240
|
+
const cb = world.getComponent(ib, "CapsuleCollider");
|
|
3241
|
+
if (!canInteractGeneric(ca.layer, ca.mask, cb.layer, cb.mask)) continue;
|
|
3242
|
+
const ta = world.getComponent(ia, "Transform");
|
|
3243
|
+
const tb = world.getComponent(ib, "Transform");
|
|
3244
|
+
const ov = getOverlap(getCapsuleAABB(ta, ca), getCapsuleAABB(tb, cb));
|
|
3245
|
+
if (ov) currentCapsulePairs.set(pairKey(ia, ib), [ia, ib]);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
for (const [key, [a, b]] of currentCapsulePairs) {
|
|
3249
|
+
if (!this.activeCapsulePairs.has(key)) {
|
|
3250
|
+
this.events?.emit("capsuleEnter", { a, b });
|
|
3251
|
+
} else {
|
|
3252
|
+
this.events?.emit("capsuleStay", { a, b });
|
|
3253
|
+
}
|
|
3254
|
+
this.events?.emit("capsule", { a, b });
|
|
3255
|
+
}
|
|
3256
|
+
for (const [key, [a, b]] of this.activeCapsulePairs) {
|
|
3257
|
+
if (!currentCapsulePairs.has(key)) {
|
|
3258
|
+
this.events?.emit("capsuleExit", { a, b });
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
this.activeCapsulePairs = currentCapsulePairs;
|
|
3262
|
+
} else if (this.activeCapsulePairs.size > 0) {
|
|
3263
|
+
for (const [, [a, b]] of this.activeCapsulePairs) {
|
|
3264
|
+
this.events?.emit("capsuleExit", { a, b });
|
|
3265
|
+
}
|
|
3266
|
+
this.activeCapsulePairs = /* @__PURE__ */ new Map();
|
|
3267
|
+
}
|
|
2907
3268
|
}
|
|
2908
3269
|
};
|
|
2909
3270
|
|
|
@@ -3886,6 +4247,8 @@ function Game({
|
|
|
3886
4247
|
const input = new InputManager();
|
|
3887
4248
|
const events = new EventBus();
|
|
3888
4249
|
const assets = new AssetManager();
|
|
4250
|
+
const viteEnv = import.meta.env;
|
|
4251
|
+
assets.baseURL = (viteEnv?.BASE_URL ?? "/").replace(/\/$/, "");
|
|
3889
4252
|
ecs.assets = assets;
|
|
3890
4253
|
const physics = new PhysicsSystem(gravity, events);
|
|
3891
4254
|
const entityIds = /* @__PURE__ */ new Map();
|
|
@@ -3919,7 +4282,7 @@ function Game({
|
|
|
3919
4282
|
if (handle.buffer.length > MAX_DEVTOOLS_FRAMES) handle.buffer.shift();
|
|
3920
4283
|
handle.onFrame?.();
|
|
3921
4284
|
}
|
|
3922
|
-
});
|
|
4285
|
+
}, deterministic ? { fixedDt: 1 / 60 } : void 0);
|
|
3923
4286
|
const state = {
|
|
3924
4287
|
ecs,
|
|
3925
4288
|
input,
|
|
@@ -4010,6 +4373,12 @@ function Game({
|
|
|
4010
4373
|
cancelled = true;
|
|
4011
4374
|
};
|
|
4012
4375
|
}, [engine]);
|
|
4376
|
+
useEffect3(() => {
|
|
4377
|
+
if (!engine) return;
|
|
4378
|
+
const canvas = engine.canvas;
|
|
4379
|
+
if (canvas.width !== width) canvas.width = width;
|
|
4380
|
+
if (canvas.height !== height) canvas.height = height;
|
|
4381
|
+
}, [width, height, engine]);
|
|
4013
4382
|
useEffect3(() => {
|
|
4014
4383
|
engine?.physics.setGravity(gravity);
|
|
4015
4384
|
}, [gravity, engine]);
|
|
@@ -4130,7 +4499,7 @@ function Entity({ id, tags = [], children }) {
|
|
|
4130
4499
|
setEntityId(eid);
|
|
4131
4500
|
return () => {
|
|
4132
4501
|
engine.ecs.destroyEntity(eid);
|
|
4133
|
-
if (id) engine.entityIds.delete(id);
|
|
4502
|
+
if (id && engine.entityIds.get(id) === eid) engine.entityIds.delete(id);
|
|
4134
4503
|
};
|
|
4135
4504
|
}, []);
|
|
4136
4505
|
if (entityId === null) return null;
|
|
@@ -4213,13 +4582,12 @@ function Sprite({
|
|
|
4213
4582
|
});
|
|
4214
4583
|
engine.ecs.addComponent(entityId, comp);
|
|
4215
4584
|
if (src) {
|
|
4216
|
-
|
|
4217
|
-
const base = (viteEnv?.BASE_URL ?? "/").replace(/\/$/, "");
|
|
4218
|
-
const resolvedSrc = base && src.startsWith("/") ? base + src : src;
|
|
4219
|
-
comp.src = resolvedSrc;
|
|
4220
|
-
engine.assets.loadImage(resolvedSrc).then((img) => {
|
|
4585
|
+
engine.assets.loadImage(src).then((img) => {
|
|
4221
4586
|
const c = engine.ecs.getComponent(entityId, "Sprite");
|
|
4222
|
-
if (c)
|
|
4587
|
+
if (c) {
|
|
4588
|
+
c.image = img;
|
|
4589
|
+
c.src = img.src;
|
|
4590
|
+
}
|
|
4223
4591
|
}).catch(console.error);
|
|
4224
4592
|
}
|
|
4225
4593
|
return () => engine.ecs.removeComponent(entityId, "Sprite");
|
|
@@ -4293,12 +4661,13 @@ function RigidBody({
|
|
|
4293
4661
|
vx = 0,
|
|
4294
4662
|
vy = 0,
|
|
4295
4663
|
lockX = false,
|
|
4296
|
-
lockY = false
|
|
4664
|
+
lockY = false,
|
|
4665
|
+
ccd = false
|
|
4297
4666
|
}) {
|
|
4298
4667
|
const engine = useContext7(EngineContext);
|
|
4299
4668
|
const entityId = useContext7(EntityContext);
|
|
4300
4669
|
useEffect9(() => {
|
|
4301
|
-
engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY }));
|
|
4670
|
+
engine.ecs.addComponent(entityId, createRigidBody({ mass, gravityScale, isStatic, bounce, friction, vx, vy, lockX, lockY, ccd }));
|
|
4302
4671
|
return () => engine.ecs.removeComponent(entityId, "RigidBody");
|
|
4303
4672
|
}, []);
|
|
4304
4673
|
return null;
|
|
@@ -4398,19 +4767,24 @@ function CompoundCollider({
|
|
|
4398
4767
|
}
|
|
4399
4768
|
|
|
4400
4769
|
// src/components/Script.tsx
|
|
4401
|
-
import { useEffect as useEffect14, useContext as useContext12 } from "react";
|
|
4770
|
+
import { useEffect as useEffect14, useContext as useContext12, useRef as useRef4 } from "react";
|
|
4402
4771
|
function Script({ init, update }) {
|
|
4403
4772
|
const engine = useContext12(EngineContext);
|
|
4404
4773
|
const entityId = useContext12(EntityContext);
|
|
4774
|
+
const initRef = useRef4(init);
|
|
4775
|
+
initRef.current = init;
|
|
4776
|
+
const updateRef = useRef4(update);
|
|
4777
|
+
updateRef.current = update;
|
|
4405
4778
|
useEffect14(() => {
|
|
4406
|
-
if (
|
|
4779
|
+
if (initRef.current) {
|
|
4407
4780
|
try {
|
|
4408
|
-
|
|
4781
|
+
initRef.current(entityId, engine.ecs);
|
|
4409
4782
|
} catch (err) {
|
|
4410
4783
|
console.error(`[Cubeforge] Script init error on entity ${entityId}:`, err);
|
|
4411
4784
|
}
|
|
4412
4785
|
}
|
|
4413
|
-
|
|
4786
|
+
const stableUpdate = (id, world, input, dt) => updateRef.current(id, world, input, dt);
|
|
4787
|
+
engine.ecs.addComponent(entityId, createScript(stableUpdate));
|
|
4414
4788
|
return () => engine.ecs.removeComponent(entityId, "Script");
|
|
4415
4789
|
}, []);
|
|
4416
4790
|
return null;
|
|
@@ -4640,7 +5014,7 @@ function ParticleEmitter({
|
|
|
4640
5014
|
}
|
|
4641
5015
|
|
|
4642
5016
|
// src/components/VirtualJoystick.tsx
|
|
4643
|
-
import { useRef as
|
|
5017
|
+
import { useRef as useRef5 } from "react";
|
|
4644
5018
|
|
|
4645
5019
|
// src/hooks/useVirtualInput.ts
|
|
4646
5020
|
var _axes = { x: 0, y: 0 };
|
|
@@ -4674,10 +5048,10 @@ function VirtualJoystick({
|
|
|
4674
5048
|
actionLabel = "A",
|
|
4675
5049
|
actionName = "action"
|
|
4676
5050
|
}) {
|
|
4677
|
-
const baseRef =
|
|
4678
|
-
const stickRef =
|
|
4679
|
-
const activePtr =
|
|
4680
|
-
const baseCenterRef =
|
|
5051
|
+
const baseRef = useRef5(null);
|
|
5052
|
+
const stickRef = useRef5(null);
|
|
5053
|
+
const activePtr = useRef5(null);
|
|
5054
|
+
const baseCenterRef = useRef5({ x: 0, y: 0 });
|
|
4681
5055
|
const radius = size / 2 - 16;
|
|
4682
5056
|
const applyStickPosition = (dx, dy) => {
|
|
4683
5057
|
if (!stickRef.current) return;
|
|
@@ -5156,10 +5530,10 @@ function ParallaxLayer({
|
|
|
5156
5530
|
}
|
|
5157
5531
|
|
|
5158
5532
|
// src/components/ScreenFlash.tsx
|
|
5159
|
-
import { forwardRef, useImperativeHandle, useRef as
|
|
5533
|
+
import { forwardRef, useImperativeHandle, useRef as useRef6 } from "react";
|
|
5160
5534
|
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
5161
5535
|
var ScreenFlash = forwardRef((_, ref) => {
|
|
5162
|
-
const divRef =
|
|
5536
|
+
const divRef = useRef6(null);
|
|
5163
5537
|
useImperativeHandle(ref, () => ({
|
|
5164
5538
|
flash(color, duration) {
|
|
5165
5539
|
const el = divRef.current;
|
|
@@ -5195,7 +5569,7 @@ var ScreenFlash = forwardRef((_, ref) => {
|
|
|
5195
5569
|
ScreenFlash.displayName = "ScreenFlash";
|
|
5196
5570
|
|
|
5197
5571
|
// src/components/CameraZone.tsx
|
|
5198
|
-
import { useEffect as useEffect21, useContext as useContext19, useRef as
|
|
5572
|
+
import { useEffect as useEffect21, useContext as useContext19, useRef as useRef7 } from "react";
|
|
5199
5573
|
import { Fragment as Fragment5, jsx as jsx11 } from "react/jsx-runtime";
|
|
5200
5574
|
function CameraZone({
|
|
5201
5575
|
x,
|
|
@@ -5208,8 +5582,8 @@ function CameraZone({
|
|
|
5208
5582
|
children
|
|
5209
5583
|
}) {
|
|
5210
5584
|
const engine = useContext19(EngineContext);
|
|
5211
|
-
const prevFollowRef =
|
|
5212
|
-
const activeRef =
|
|
5585
|
+
const prevFollowRef = useRef7(void 0);
|
|
5586
|
+
const activeRef = useRef7(false);
|
|
5213
5587
|
useEffect21(() => {
|
|
5214
5588
|
const eid = engine.ecs.createEntity();
|
|
5215
5589
|
engine.ecs.addComponent(eid, createScript(() => {
|
|
@@ -5450,7 +5824,7 @@ function useInputMap(bindings) {
|
|
|
5450
5824
|
}
|
|
5451
5825
|
|
|
5452
5826
|
// src/hooks/useEvents.ts
|
|
5453
|
-
import { useContext as useContext27, useEffect as useEffect26, useRef as
|
|
5827
|
+
import { useContext as useContext27, useEffect as useEffect26, useRef as useRef8 } from "react";
|
|
5454
5828
|
function useEvents() {
|
|
5455
5829
|
const engine = useContext27(EngineContext);
|
|
5456
5830
|
if (!engine) throw new Error("useEvents must be used inside <Game>");
|
|
@@ -5458,7 +5832,7 @@ function useEvents() {
|
|
|
5458
5832
|
}
|
|
5459
5833
|
function useEvent(event, handler) {
|
|
5460
5834
|
const events = useEvents();
|
|
5461
|
-
const handlerRef =
|
|
5835
|
+
const handlerRef = useRef8(handler);
|
|
5462
5836
|
handlerRef.current = handler;
|
|
5463
5837
|
useEffect26(() => {
|
|
5464
5838
|
return events.on(event, (data) => handlerRef.current(data));
|
|
@@ -5536,11 +5910,11 @@ function useInputRecorder() {
|
|
|
5536
5910
|
}
|
|
5537
5911
|
|
|
5538
5912
|
// src/hooks/useGamepad.ts
|
|
5539
|
-
import { useEffect as useEffect28, useRef as
|
|
5913
|
+
import { useEffect as useEffect28, useRef as useRef9, useState as useState7 } from "react";
|
|
5540
5914
|
var EMPTY_STATE = { connected: false, axes: [], buttons: [] };
|
|
5541
5915
|
function useGamepad(playerIndex = 0) {
|
|
5542
5916
|
const [state, setState] = useState7(EMPTY_STATE);
|
|
5543
|
-
const rafRef =
|
|
5917
|
+
const rafRef = useRef9(0);
|
|
5544
5918
|
useEffect28(() => {
|
|
5545
5919
|
const poll = () => {
|
|
5546
5920
|
const gp = navigator.getGamepads()[playerIndex];
|
|
@@ -5654,12 +6028,12 @@ function useDropThrough(frames = 8) {
|
|
|
5654
6028
|
}
|
|
5655
6029
|
|
|
5656
6030
|
// ../gameplay/src/hooks/useGameStateMachine.ts
|
|
5657
|
-
import { useState as useState10, useRef as
|
|
6031
|
+
import { useState as useState10, useRef as useRef10, useCallback as useCallback8, useEffect as useEffect29, useContext as useContext32 } from "react";
|
|
5658
6032
|
function useGameStateMachine(states, initial) {
|
|
5659
6033
|
const engine = useContext32(EngineContext);
|
|
5660
6034
|
const [state, setState] = useState10(initial);
|
|
5661
|
-
const stateRef =
|
|
5662
|
-
const statesRef =
|
|
6035
|
+
const stateRef = useRef10(initial);
|
|
6036
|
+
const statesRef = useRef10(states);
|
|
5663
6037
|
statesRef.current = states;
|
|
5664
6038
|
useEffect29(() => {
|
|
5665
6039
|
statesRef.current[initial]?.onEnter?.();
|
|
@@ -5685,22 +6059,22 @@ function useGameStateMachine(states, initial) {
|
|
|
5685
6059
|
}
|
|
5686
6060
|
|
|
5687
6061
|
// ../gameplay/src/hooks/useHealth.ts
|
|
5688
|
-
import { useRef as
|
|
6062
|
+
import { useRef as useRef11, useEffect as useEffect30, useContext as useContext33, useCallback as useCallback9 } from "react";
|
|
5689
6063
|
function useHealth(maxHp, opts = {}) {
|
|
5690
6064
|
const engine = useContext33(EngineContext);
|
|
5691
6065
|
const entityId = useContext33(EntityContext);
|
|
5692
|
-
const hpRef =
|
|
5693
|
-
const invincibleRef =
|
|
6066
|
+
const hpRef = useRef11(maxHp);
|
|
6067
|
+
const invincibleRef = useRef11(false);
|
|
5694
6068
|
const iFrameDuration = opts.iFrames ?? 1;
|
|
5695
|
-
const onDeathRef =
|
|
5696
|
-
const onDamageRef =
|
|
6069
|
+
const onDeathRef = useRef11(opts.onDeath);
|
|
6070
|
+
const onDamageRef = useRef11(opts.onDamage);
|
|
5697
6071
|
useEffect30(() => {
|
|
5698
6072
|
onDeathRef.current = opts.onDeath;
|
|
5699
6073
|
});
|
|
5700
6074
|
useEffect30(() => {
|
|
5701
6075
|
onDamageRef.current = opts.onDamage;
|
|
5702
6076
|
});
|
|
5703
|
-
const timerRef =
|
|
6077
|
+
const timerRef = useRef11(
|
|
5704
6078
|
createTimer(iFrameDuration, () => {
|
|
5705
6079
|
invincibleRef.current = false;
|
|
5706
6080
|
})
|
|
@@ -5715,7 +6089,7 @@ function useHealth(maxHp, opts = {}) {
|
|
|
5715
6089
|
}
|
|
5716
6090
|
if (hpRef.current <= 0) onDeathRef.current?.();
|
|
5717
6091
|
}, [iFrameDuration]);
|
|
5718
|
-
const takeDamageRef =
|
|
6092
|
+
const takeDamageRef = useRef11(takeDamage);
|
|
5719
6093
|
useEffect30(() => {
|
|
5720
6094
|
takeDamageRef.current = takeDamage;
|
|
5721
6095
|
}, [takeDamage]);
|
|
@@ -5802,11 +6176,11 @@ function useKinematicBody() {
|
|
|
5802
6176
|
}
|
|
5803
6177
|
|
|
5804
6178
|
// ../gameplay/src/hooks/useLevelTransition.ts
|
|
5805
|
-
import { useState as useState11, useRef as
|
|
6179
|
+
import { useState as useState11, useRef as useRef12, useCallback as useCallback11 } from "react";
|
|
5806
6180
|
function useLevelTransition(initial) {
|
|
5807
6181
|
const [currentLevel, setCurrentLevel] = useState11(initial);
|
|
5808
6182
|
const [isTransitioning, setIsTransitioning] = useState11(false);
|
|
5809
|
-
const overlayRef =
|
|
6183
|
+
const overlayRef = useRef12(null);
|
|
5810
6184
|
const transitionTo = useCallback11((level, opts = {}) => {
|
|
5811
6185
|
const { duration = 0.4, type = "fade" } = opts;
|
|
5812
6186
|
if (type === "instant") {
|
|
@@ -5985,10 +6359,10 @@ function useRestart() {
|
|
|
5985
6359
|
}
|
|
5986
6360
|
|
|
5987
6361
|
// ../gameplay/src/hooks/useSave.ts
|
|
5988
|
-
import { useCallback as useCallback15, useRef as
|
|
6362
|
+
import { useCallback as useCallback15, useRef as useRef13 } from "react";
|
|
5989
6363
|
function useSave(key, defaultValue, opts = {}) {
|
|
5990
6364
|
const version = opts.version ?? 1;
|
|
5991
|
-
const dataRef =
|
|
6365
|
+
const dataRef = useRef13(defaultValue);
|
|
5992
6366
|
const save = useCallback15((value) => {
|
|
5993
6367
|
dataRef.current = value;
|
|
5994
6368
|
const slot = { version, data: value };
|
|
@@ -6063,11 +6437,11 @@ function useTopDownMovement(entityId, opts = {}) {
|
|
|
6063
6437
|
}
|
|
6064
6438
|
|
|
6065
6439
|
// ../gameplay/src/hooks/useDialogue.ts
|
|
6066
|
-
import { useState as useState14, useCallback as useCallback16, useRef as
|
|
6440
|
+
import { useState as useState14, useCallback as useCallback16, useRef as useRef14 } from "react";
|
|
6067
6441
|
function useDialogue() {
|
|
6068
6442
|
const [active, setActive] = useState14(false);
|
|
6069
6443
|
const [currentId, setCurrentId] = useState14(null);
|
|
6070
|
-
const scriptRef =
|
|
6444
|
+
const scriptRef = useRef14(null);
|
|
6071
6445
|
const start = useCallback16((script, startId) => {
|
|
6072
6446
|
scriptRef.current = script;
|
|
6073
6447
|
const id = startId ?? Object.keys(script)[0];
|
|
@@ -6111,16 +6485,16 @@ function useDialogue() {
|
|
|
6111
6485
|
}
|
|
6112
6486
|
|
|
6113
6487
|
// ../gameplay/src/hooks/useCutscene.ts
|
|
6114
|
-
import { useState as useState15, useCallback as useCallback17, useRef as
|
|
6488
|
+
import { useState as useState15, useCallback as useCallback17, useRef as useRef15, useEffect as useEffect33, useContext as useContext38 } from "react";
|
|
6115
6489
|
function useCutscene() {
|
|
6116
6490
|
const engine = useContext38(EngineContext);
|
|
6117
6491
|
const [playing, setPlaying] = useState15(false);
|
|
6118
6492
|
const [stepIndex, setStepIndex] = useState15(0);
|
|
6119
|
-
const stepsRef =
|
|
6120
|
-
const timerRef =
|
|
6121
|
-
const idxRef =
|
|
6122
|
-
const playingRef =
|
|
6123
|
-
const entityRef =
|
|
6493
|
+
const stepsRef = useRef15([]);
|
|
6494
|
+
const timerRef = useRef15(0);
|
|
6495
|
+
const idxRef = useRef15(0);
|
|
6496
|
+
const playingRef = useRef15(false);
|
|
6497
|
+
const entityRef = useRef15(null);
|
|
6124
6498
|
const finish = useCallback17(() => {
|
|
6125
6499
|
playingRef.current = false;
|
|
6126
6500
|
setPlaying(false);
|
|
@@ -6245,7 +6619,7 @@ function useGameStore(key, initialState) {
|
|
|
6245
6619
|
}
|
|
6246
6620
|
|
|
6247
6621
|
// ../../packages/audio/src/useSound.ts
|
|
6248
|
-
import { useEffect as useEffect34, useRef as
|
|
6622
|
+
import { useEffect as useEffect34, useRef as useRef16 } from "react";
|
|
6249
6623
|
var _audioCtx = null;
|
|
6250
6624
|
function getAudioCtx() {
|
|
6251
6625
|
if (!_audioCtx) _audioCtx = new AudioContext();
|
|
@@ -6312,12 +6686,12 @@ async function loadBuffer(src) {
|
|
|
6312
6686
|
return buf;
|
|
6313
6687
|
}
|
|
6314
6688
|
function useSound(src, opts = {}) {
|
|
6315
|
-
const bufferRef =
|
|
6316
|
-
const sourceRef =
|
|
6317
|
-
const gainRef =
|
|
6318
|
-
const volRef =
|
|
6319
|
-
const loopRef =
|
|
6320
|
-
const groupRef =
|
|
6689
|
+
const bufferRef = useRef16(null);
|
|
6690
|
+
const sourceRef = useRef16(null);
|
|
6691
|
+
const gainRef = useRef16(null);
|
|
6692
|
+
const volRef = useRef16(opts.volume ?? 1);
|
|
6693
|
+
const loopRef = useRef16(opts.loop ?? false);
|
|
6694
|
+
const groupRef = useRef16(opts.group);
|
|
6321
6695
|
useEffect34(() => {
|
|
6322
6696
|
let cancelled = false;
|
|
6323
6697
|
loadBuffer(src).then((buf) => {
|