@jgengine/core 0.7.0 → 0.8.0
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/CHANGELOG.md +47 -0
- package/README.md +2 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +13 -1
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- package/dist/combat/abilityKit.d.ts +5 -0
- package/dist/combat/abilityKit.js +10 -0
- package/dist/combat/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/telegraph.d.ts +5 -1
- package/dist/combat/telegraph.js +14 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/defineGame.d.ts +8 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +25 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/playableGame.d.ts +237 -6
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +5 -0
- package/dist/game/race.js +20 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +37 -1
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +10 -0
- package/dist/nav/navGrid.js +18 -1
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/forceVolume.js +5 -5
- package/dist/physics/physicsWorld.d.ts +41 -2
- package/dist/physics/physicsWorld.js +232 -40
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +16 -17
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +44 -2
- package/dist/runtime/gameContext.js +100 -4
- package/dist/runtime/hostPersistence.d.ts +21 -0
- package/dist/runtime/hostPersistence.js +50 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/entityStore.d.ts +8 -0
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
export const SHAPE_BOX = 0;
|
|
2
|
+
export const SHAPE_SPHERE = 1;
|
|
1
3
|
const FLAG_SLEEPING = 1;
|
|
2
4
|
const FLAG_STATIC = 2;
|
|
5
|
+
const FLAG_DEAD = 4;
|
|
6
|
+
/** Cap on `nx*ny*nz` broadphase cells — guards a huge-bounds/tiny-cellSize config from hanging `step()`. */
|
|
7
|
+
export const MAX_BROADPHASE_CELLS = 1_000_000;
|
|
3
8
|
/** Grid cell containing a point, clamped into the grid. Pure — exported for tests. */
|
|
4
9
|
export function cellCoord(value, min, cellSize, cells) {
|
|
5
10
|
const raw = Math.floor((value - min) / cellSize);
|
|
@@ -39,6 +44,8 @@ export class PhysicsWorld {
|
|
|
39
44
|
halfZ;
|
|
40
45
|
invMass;
|
|
41
46
|
flags;
|
|
47
|
+
/** `SHAPE_BOX` (0) or `SHAPE_SPHERE` (1) per body; spheres keep their radius in halfX/halfY/halfZ. */
|
|
48
|
+
shape;
|
|
42
49
|
/** 1 for a body that took part in a contact this frame; reset each `step()`. Render tint reads this. */
|
|
43
50
|
contact;
|
|
44
51
|
sleepTimer;
|
|
@@ -76,6 +83,12 @@ export class PhysicsWorld {
|
|
|
76
83
|
jFreeCount = 0;
|
|
77
84
|
jointHigh = 0;
|
|
78
85
|
jointActiveCount = 0;
|
|
86
|
+
// Removed bodies tombstone (FLAG_DEAD) rather than compact: `id` is the SoA slot itself, handed
|
|
87
|
+
// out to callers and stored raw in joints/game state, so a slot can never move once allocated.
|
|
88
|
+
// `bodyHigh` is the high-water mark of ever-used slots; `bodyFree` recycles dead ones for addBody.
|
|
89
|
+
bodyFree;
|
|
90
|
+
bodyFreeCount = 0;
|
|
91
|
+
bodyHigh = 0;
|
|
79
92
|
collisionListener = null;
|
|
80
93
|
collisionMinSpeed = 0;
|
|
81
94
|
collisionEvent = {
|
|
@@ -131,6 +144,7 @@ export class PhysicsWorld {
|
|
|
131
144
|
this.halfZ = new Float32Array(cap);
|
|
132
145
|
this.invMass = new Float32Array(cap);
|
|
133
146
|
this.flags = new Uint8Array(cap);
|
|
147
|
+
this.shape = new Uint8Array(cap);
|
|
134
148
|
this.contact = new Uint8Array(cap);
|
|
135
149
|
this.sleepTimer = new Uint16Array(cap);
|
|
136
150
|
this.prevX = new Float32Array(cap);
|
|
@@ -141,12 +155,18 @@ export class PhysicsWorld {
|
|
|
141
155
|
this.ny = Math.max(1, Math.ceil(span(1) / this.cellSize));
|
|
142
156
|
this.nz = Math.max(1, Math.ceil(span(2) / this.cellSize));
|
|
143
157
|
this.numCells = this.nx * this.ny * this.nz;
|
|
158
|
+
if (this.numCells > MAX_BROADPHASE_CELLS) {
|
|
159
|
+
throw new Error(`PhysicsWorld: broadphase grid too large (${this.nx}x${this.ny}x${this.nz} = ${this.numCells} cells, ` +
|
|
160
|
+
`cap ${MAX_BROADPHASE_CELLS}). bounds=[${config.bounds.min}]..[${config.bounds.max}] cellSize=${this.cellSize}; ` +
|
|
161
|
+
`raise cellSize or shrink bounds.`);
|
|
162
|
+
}
|
|
144
163
|
this.cellStart = new Int32Array(this.numCells + 1);
|
|
145
164
|
this.cursor = new Int32Array(this.numCells);
|
|
146
165
|
this.sorted = new Int32Array(cap);
|
|
147
166
|
this.bodyCell = new Int32Array(cap);
|
|
148
167
|
this.awakeList = new Int32Array(cap);
|
|
149
168
|
this.awakeSlot = new Int32Array(cap).fill(-1);
|
|
169
|
+
this.bodyFree = new Int32Array(cap);
|
|
150
170
|
const jc = this.jointCapacity;
|
|
151
171
|
this.jSpring = new Uint8Array(jc);
|
|
152
172
|
this.jActive = new Uint8Array(jc);
|
|
@@ -184,6 +204,10 @@ export class PhysicsWorld {
|
|
|
184
204
|
get count() {
|
|
185
205
|
return this.bodyCount;
|
|
186
206
|
}
|
|
207
|
+
/** One past the highest slot `addBody` has ever handed out — the range `removeBody` leaves holes in. */
|
|
208
|
+
get highWater() {
|
|
209
|
+
return this.bodyHigh;
|
|
210
|
+
}
|
|
187
211
|
get cells() {
|
|
188
212
|
return { nx: this.nx, ny: this.ny, nz: this.nz, total: this.numCells };
|
|
189
213
|
}
|
|
@@ -254,6 +278,12 @@ export class PhysicsWorld {
|
|
|
254
278
|
this.jAnchorBY[id] = y;
|
|
255
279
|
this.jAnchorBZ[id] = z;
|
|
256
280
|
}
|
|
281
|
+
/** Move a joint's local anchor offset on A (world axes, no rotation applied by the solver). */
|
|
282
|
+
setJointAnchorA(id, x, y, z) {
|
|
283
|
+
this.jAnchorAX[id] = x;
|
|
284
|
+
this.jAnchorAY[id] = y;
|
|
285
|
+
this.jAnchorAZ[id] = z;
|
|
286
|
+
}
|
|
257
287
|
setJointRest(id, restLength) {
|
|
258
288
|
this.jRest[id] = restLength;
|
|
259
289
|
}
|
|
@@ -291,9 +321,16 @@ export class PhysicsWorld {
|
|
|
291
321
|
return n;
|
|
292
322
|
}
|
|
293
323
|
addBody(options) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
324
|
+
let i;
|
|
325
|
+
if (this.bodyFreeCount > 0) {
|
|
326
|
+
i = this.bodyFree[--this.bodyFreeCount];
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
if (this.bodyHigh >= this.capacity)
|
|
330
|
+
throw new Error("PhysicsWorld: capacity exceeded");
|
|
331
|
+
i = this.bodyHigh++;
|
|
332
|
+
}
|
|
333
|
+
this.bodyCount += 1;
|
|
297
334
|
this.posX[i] = options.position[0];
|
|
298
335
|
this.posY[i] = options.position[1];
|
|
299
336
|
this.posZ[i] = options.position[2];
|
|
@@ -301,9 +338,18 @@ export class PhysicsWorld {
|
|
|
301
338
|
this.velX[i] = v?.[0] ?? 0;
|
|
302
339
|
this.velY[i] = v?.[1] ?? 0;
|
|
303
340
|
this.velZ[i] = v?.[2] ?? 0;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
341
|
+
if (options.shape === "sphere") {
|
|
342
|
+
this.halfX[i] = options.radius;
|
|
343
|
+
this.halfY[i] = options.radius;
|
|
344
|
+
this.halfZ[i] = options.radius;
|
|
345
|
+
this.shape[i] = SHAPE_SPHERE;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
this.halfX[i] = options.halfExtents[0];
|
|
349
|
+
this.halfY[i] = options.halfExtents[1];
|
|
350
|
+
this.halfZ[i] = options.halfExtents[2];
|
|
351
|
+
this.shape[i] = SHAPE_BOX;
|
|
352
|
+
}
|
|
307
353
|
const isStatic = options.static === true;
|
|
308
354
|
const mass = options.mass ?? 1;
|
|
309
355
|
this.invMass[i] = isStatic || mass <= 0 ? 0 : 1 / mass;
|
|
@@ -314,17 +360,69 @@ export class PhysicsWorld {
|
|
|
314
360
|
f |= FLAG_SLEEPING;
|
|
315
361
|
this.flags[i] = f;
|
|
316
362
|
this.sleepTimer[i] = 0;
|
|
363
|
+
this.contact[i] = 0;
|
|
317
364
|
this.awakeSlot[i] = -1;
|
|
318
365
|
this.bodyCell[i] = this.cellOf(i);
|
|
319
366
|
if ((f & FLAG_SLEEPING) === 0)
|
|
320
367
|
this.addAwake(i);
|
|
321
368
|
return i;
|
|
322
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Remove a body: it drops out of integration, the solver, and the broadphase, and its slot is
|
|
372
|
+
* queued for the next `addBody` to reuse. `id` and every other body's `id` stay valid — a dead
|
|
373
|
+
* slot is tombstoned (`FLAG_DEAD`), never swapped, since ids are handed out raw and stored as-is
|
|
374
|
+
* in joints and game state. There is no persistent contact set to consult, so — conservatively —
|
|
375
|
+
* any sleeping body whose AABB touches the removed body's (padded by `slop`) is woken; a body
|
|
376
|
+
* that was resting on it always qualifies.
|
|
377
|
+
*/
|
|
378
|
+
removeBody(id) {
|
|
379
|
+
if (id < 0 || id >= this.bodyHigh)
|
|
380
|
+
return;
|
|
381
|
+
if ((this.flags[id] & FLAG_DEAD) !== 0)
|
|
382
|
+
return;
|
|
383
|
+
this.wakeTouching(id);
|
|
384
|
+
this.removeAwake(id);
|
|
385
|
+
this.flags[id] = FLAG_DEAD;
|
|
386
|
+
this.invMass[id] = 0;
|
|
387
|
+
this.velX[id] = 0;
|
|
388
|
+
this.velY[id] = 0;
|
|
389
|
+
this.velZ[id] = 0;
|
|
390
|
+
this.sleepTimer[id] = 0;
|
|
391
|
+
this.contact[id] = 0;
|
|
392
|
+
this.bodyCount -= 1;
|
|
393
|
+
this.bodyFree[this.bodyFreeCount++] = id;
|
|
394
|
+
}
|
|
395
|
+
wakeTouching(id) {
|
|
396
|
+
const pad = this.slop + 0.01;
|
|
397
|
+
const minX = this.posX[id] - this.halfX[id] - pad;
|
|
398
|
+
const maxX = this.posX[id] + this.halfX[id] + pad;
|
|
399
|
+
const minY = this.posY[id] - this.halfY[id] - pad;
|
|
400
|
+
const maxY = this.posY[id] + this.halfY[id] + pad;
|
|
401
|
+
const minZ = this.posZ[id] - this.halfZ[id] - pad;
|
|
402
|
+
const maxZ = this.posZ[id] + this.halfZ[id] + pad;
|
|
403
|
+
for (let j = 0; j < this.bodyHigh; j += 1) {
|
|
404
|
+
if (j === id)
|
|
405
|
+
continue;
|
|
406
|
+
const fj = this.flags[j];
|
|
407
|
+
if ((fj & (FLAG_DEAD | FLAG_STATIC)) !== 0 || (fj & FLAG_SLEEPING) === 0)
|
|
408
|
+
continue;
|
|
409
|
+
if (this.posX[j] - this.halfX[j] > maxX || this.posX[j] + this.halfX[j] < minX)
|
|
410
|
+
continue;
|
|
411
|
+
if (this.posY[j] - this.halfY[j] > maxY || this.posY[j] + this.halfY[j] < minY)
|
|
412
|
+
continue;
|
|
413
|
+
if (this.posZ[j] - this.halfZ[j] > maxZ || this.posZ[j] + this.halfZ[j] < minZ)
|
|
414
|
+
continue;
|
|
415
|
+
this.wake(j);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
323
418
|
isSleeping(i) {
|
|
324
419
|
return (this.flags[i] & FLAG_SLEEPING) !== 0;
|
|
325
420
|
}
|
|
421
|
+
isAlive(i) {
|
|
422
|
+
return (this.flags[i] & FLAG_DEAD) === 0;
|
|
423
|
+
}
|
|
326
424
|
wake(i) {
|
|
327
|
-
if ((this.flags[i] & FLAG_STATIC) !== 0)
|
|
425
|
+
if ((this.flags[i] & (FLAG_STATIC | FLAG_DEAD)) !== 0)
|
|
328
426
|
return;
|
|
329
427
|
this.flags[i] &= ~FLAG_SLEEPING;
|
|
330
428
|
this.sleepTimer[i] = 0;
|
|
@@ -333,9 +431,40 @@ export class PhysicsWorld {
|
|
|
333
431
|
this.prevZ[i] = this.posZ[i];
|
|
334
432
|
this.addAwake(i);
|
|
335
433
|
}
|
|
434
|
+
/** Overwrite a body's velocity directly, waking it if asleep. */
|
|
435
|
+
setVelocity(id, x, y, z) {
|
|
436
|
+
if ((this.flags[id] & FLAG_DEAD) !== 0)
|
|
437
|
+
return;
|
|
438
|
+
this.velX[id] = x;
|
|
439
|
+
this.velY[id] = y;
|
|
440
|
+
this.velZ[id] = z;
|
|
441
|
+
this.wake(id);
|
|
442
|
+
}
|
|
443
|
+
/** Move a body, keeping its current velocity, and wake it if asleep. */
|
|
444
|
+
setPosition(id, x, y, z) {
|
|
445
|
+
if ((this.flags[id] & FLAG_DEAD) !== 0)
|
|
446
|
+
return;
|
|
447
|
+
this.posX[id] = x;
|
|
448
|
+
this.posY[id] = y;
|
|
449
|
+
this.posZ[id] = z;
|
|
450
|
+
this.prevX[id] = x;
|
|
451
|
+
this.prevY[id] = y;
|
|
452
|
+
this.prevZ[id] = z;
|
|
453
|
+
this.bodyCell[id] = this.cellOf(id);
|
|
454
|
+
this.wake(id);
|
|
455
|
+
}
|
|
456
|
+
/** Move a body and zero its velocity — a hard reset (respawn, teleporter) instead of a slide. */
|
|
457
|
+
teleport(id, x, y, z) {
|
|
458
|
+
this.setPosition(id, x, y, z);
|
|
459
|
+
if ((this.flags[id] & FLAG_DEAD) !== 0)
|
|
460
|
+
return;
|
|
461
|
+
this.velX[id] = 0;
|
|
462
|
+
this.velY[id] = 0;
|
|
463
|
+
this.velZ[id] = 0;
|
|
464
|
+
}
|
|
336
465
|
wakeAll() {
|
|
337
|
-
for (let i = 0; i < this.
|
|
338
|
-
if ((this.flags[i] & FLAG_STATIC) === 0) {
|
|
466
|
+
for (let i = 0; i < this.bodyHigh; i += 1) {
|
|
467
|
+
if ((this.flags[i] & (FLAG_STATIC | FLAG_DEAD)) === 0) {
|
|
339
468
|
this.flags[i] &= ~FLAG_SLEEPING;
|
|
340
469
|
this.sleepTimer[i] = 0;
|
|
341
470
|
this.addAwake(i);
|
|
@@ -344,6 +473,8 @@ export class PhysicsWorld {
|
|
|
344
473
|
}
|
|
345
474
|
clear() {
|
|
346
475
|
this.bodyCount = 0;
|
|
476
|
+
this.bodyHigh = 0;
|
|
477
|
+
this.bodyFreeCount = 0;
|
|
347
478
|
this.accumulator = 0;
|
|
348
479
|
this.awakeCount = 0;
|
|
349
480
|
this.jointHigh = 0;
|
|
@@ -360,7 +491,7 @@ export class PhysicsWorld {
|
|
|
360
491
|
let substeps = 0;
|
|
361
492
|
this.stats.contacts = 0;
|
|
362
493
|
this.stats.pairs = 0;
|
|
363
|
-
this.contact.fill(0, 0, this.
|
|
494
|
+
this.contact.fill(0, 0, this.bodyHigh);
|
|
364
495
|
while (this.accumulator >= this.fixedDt) {
|
|
365
496
|
this.substep(this.fixedDt);
|
|
366
497
|
this.accumulator -= this.fixedDt;
|
|
@@ -453,7 +584,7 @@ export class PhysicsWorld {
|
|
|
453
584
|
}
|
|
454
585
|
}
|
|
455
586
|
buildGrid() {
|
|
456
|
-
const n = this.
|
|
587
|
+
const n = this.bodyHigh;
|
|
457
588
|
const start = this.cellStart;
|
|
458
589
|
start.fill(0);
|
|
459
590
|
// Sleeping bodies never move, so their cell index is cached from addBody / their last awake
|
|
@@ -463,13 +594,19 @@ export class PhysicsWorld {
|
|
|
463
594
|
const i = this.awakeList[a];
|
|
464
595
|
this.bodyCell[i] = this.cellOf(i);
|
|
465
596
|
}
|
|
466
|
-
|
|
597
|
+
// Dead slots (tombstoned by removeBody) sit inside [0, bodyHigh) but never enter the grid.
|
|
598
|
+
for (let i = 0; i < n; i += 1) {
|
|
599
|
+
if ((this.flags[i] & FLAG_DEAD) !== 0)
|
|
600
|
+
continue;
|
|
467
601
|
start[this.bodyCell[i] + 1] += 1;
|
|
602
|
+
}
|
|
468
603
|
for (let c = 0; c < this.numCells; c += 1) {
|
|
469
604
|
start[c + 1] += start[c];
|
|
470
605
|
this.cursor[c] = start[c];
|
|
471
606
|
}
|
|
472
607
|
for (let i = 0; i < n; i += 1) {
|
|
608
|
+
if ((this.flags[i] & FLAG_DEAD) !== 0)
|
|
609
|
+
continue;
|
|
473
610
|
const c = this.bodyCell[i];
|
|
474
611
|
this.sorted[this.cursor[c]++] = i;
|
|
475
612
|
}
|
|
@@ -520,39 +657,94 @@ export class PhysicsWorld {
|
|
|
520
657
|
}
|
|
521
658
|
}
|
|
522
659
|
resolve(i, j, restitutionPass, countPass) {
|
|
523
|
-
const dx = this.posX[j] - this.posX[i];
|
|
524
|
-
const px = this.halfX[i] + this.halfX[j] - Math.abs(dx);
|
|
525
|
-
if (px <= 0)
|
|
526
|
-
return;
|
|
527
|
-
const dy = this.posY[j] - this.posY[i];
|
|
528
|
-
const py = this.halfY[i] + this.halfY[j] - Math.abs(dy);
|
|
529
|
-
if (py <= 0)
|
|
530
|
-
return;
|
|
531
|
-
const dz = this.posZ[j] - this.posZ[i];
|
|
532
|
-
const pz = this.halfZ[i] + this.halfZ[j] - Math.abs(dz);
|
|
533
|
-
if (pz <= 0)
|
|
534
|
-
return;
|
|
535
|
-
if (countPass) {
|
|
536
|
-
this.stats.pairs += 1;
|
|
537
|
-
this.stats.contacts += 1;
|
|
538
|
-
this.contact[i] = 1;
|
|
539
|
-
this.contact[j] = 1;
|
|
540
|
-
}
|
|
541
660
|
let nX = 0;
|
|
542
661
|
let nY = 0;
|
|
543
662
|
let nZ = 0;
|
|
544
|
-
let pen;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
663
|
+
let pen = 0;
|
|
664
|
+
const sphereI = this.shape[i] === SHAPE_SPHERE;
|
|
665
|
+
const sphereJ = this.shape[j] === SHAPE_SPHERE;
|
|
666
|
+
let needsAxisNormal = true;
|
|
667
|
+
if (sphereI && sphereJ) {
|
|
668
|
+
const dx = this.posX[j] - this.posX[i];
|
|
669
|
+
const dy = this.posY[j] - this.posY[i];
|
|
670
|
+
const dz = this.posZ[j] - this.posZ[i];
|
|
671
|
+
const radiusSum = this.halfX[i] + this.halfX[j];
|
|
672
|
+
const dist2 = dx * dx + dy * dy + dz * dz;
|
|
673
|
+
if (dist2 >= radiusSum * radiusSum)
|
|
674
|
+
return;
|
|
675
|
+
const dist = Math.sqrt(dist2);
|
|
676
|
+
if (dist > 1e-9) {
|
|
677
|
+
nX = dx / dist;
|
|
678
|
+
nY = dy / dist;
|
|
679
|
+
nZ = dz / dist;
|
|
680
|
+
pen = radiusSum - dist;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
nY = 1;
|
|
684
|
+
pen = radiusSum;
|
|
685
|
+
}
|
|
686
|
+
needsAxisNormal = false;
|
|
548
687
|
}
|
|
549
|
-
else if (
|
|
550
|
-
|
|
551
|
-
|
|
688
|
+
else if (sphereI !== sphereJ) {
|
|
689
|
+
const box = sphereI ? j : i;
|
|
690
|
+
const sph = sphereI ? i : j;
|
|
691
|
+
const loX = this.posX[box] - this.halfX[box];
|
|
692
|
+
const hiX = this.posX[box] + this.halfX[box];
|
|
693
|
+
const loY = this.posY[box] - this.halfY[box];
|
|
694
|
+
const hiY = this.posY[box] + this.halfY[box];
|
|
695
|
+
const loZ = this.posZ[box] - this.halfZ[box];
|
|
696
|
+
const hiZ = this.posZ[box] + this.halfZ[box];
|
|
697
|
+
const sx = this.posX[sph];
|
|
698
|
+
const sy = this.posY[sph];
|
|
699
|
+
const sz = this.posZ[sph];
|
|
700
|
+
const dx = sx - (sx < loX ? loX : sx > hiX ? hiX : sx);
|
|
701
|
+
const dy = sy - (sy < loY ? loY : sy > hiY ? hiY : sy);
|
|
702
|
+
const dz = sz - (sz < loZ ? loZ : sz > hiZ ? hiZ : sz);
|
|
703
|
+
const dist2 = dx * dx + dy * dy + dz * dz;
|
|
704
|
+
if (dist2 > 1e-12) {
|
|
705
|
+
const radius = this.halfX[sph];
|
|
706
|
+
if (dist2 >= radius * radius)
|
|
707
|
+
return;
|
|
708
|
+
const dist = Math.sqrt(dist2);
|
|
709
|
+
const towardSphere = sph === j ? 1 / dist : -1 / dist;
|
|
710
|
+
nX = dx * towardSphere;
|
|
711
|
+
nY = dy * towardSphere;
|
|
712
|
+
nZ = dz * towardSphere;
|
|
713
|
+
pen = radius - dist;
|
|
714
|
+
needsAxisNormal = false;
|
|
715
|
+
}
|
|
552
716
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
717
|
+
if (needsAxisNormal) {
|
|
718
|
+
const dx = this.posX[j] - this.posX[i];
|
|
719
|
+
const px = this.halfX[i] + this.halfX[j] - Math.abs(dx);
|
|
720
|
+
if (px <= 0)
|
|
721
|
+
return;
|
|
722
|
+
const dy = this.posY[j] - this.posY[i];
|
|
723
|
+
const py = this.halfY[i] + this.halfY[j] - Math.abs(dy);
|
|
724
|
+
if (py <= 0)
|
|
725
|
+
return;
|
|
726
|
+
const dz = this.posZ[j] - this.posZ[i];
|
|
727
|
+
const pz = this.halfZ[i] + this.halfZ[j] - Math.abs(dz);
|
|
728
|
+
if (pz <= 0)
|
|
729
|
+
return;
|
|
730
|
+
if (px <= py && px <= pz) {
|
|
731
|
+
nX = dx < 0 ? -1 : 1;
|
|
732
|
+
pen = px;
|
|
733
|
+
}
|
|
734
|
+
else if (py <= pz) {
|
|
735
|
+
nY = dy < 0 ? -1 : 1;
|
|
736
|
+
pen = py;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
nZ = dz < 0 ? -1 : 1;
|
|
740
|
+
pen = pz;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (countPass) {
|
|
744
|
+
this.stats.pairs += 1;
|
|
745
|
+
this.stats.contacts += 1;
|
|
746
|
+
this.contact[i] = 1;
|
|
747
|
+
this.contact[j] = 1;
|
|
556
748
|
}
|
|
557
749
|
const rvx = this.velX[j] - this.velX[i];
|
|
558
750
|
const rvy = this.velY[j] - this.velY[i];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MAX_BROADPHASE_CELLS } from "./physicsWorld.js";
|
|
1
2
|
/**
|
|
2
3
|
* A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap
|
|
3
4
|
* same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each
|
|
@@ -30,6 +31,10 @@ export class SpatialGrid {
|
|
|
30
31
|
this.nx = Math.max(1, Math.ceil(spanX / config.cellSize));
|
|
31
32
|
this.nz = Math.max(1, Math.ceil(spanZ / config.cellSize));
|
|
32
33
|
this.numCells = this.nx * this.nz;
|
|
34
|
+
if (this.numCells > MAX_BROADPHASE_CELLS) {
|
|
35
|
+
throw new Error(`SpatialGrid: grid too large (${this.nx}x${this.nz} = ${this.numCells} cells, cap ${MAX_BROADPHASE_CELLS}). ` +
|
|
36
|
+
`bounds=[${config.bounds.min}]..[${config.bounds.max}] cellSize=${config.cellSize}; raise cellSize or shrink bounds.`);
|
|
37
|
+
}
|
|
33
38
|
this.cellStart = new Int32Array(this.numCells + 1);
|
|
34
39
|
this.cursor = new Int32Array(this.numCells);
|
|
35
40
|
this.sorted = new Int32Array(config.capacity);
|
|
@@ -77,6 +77,12 @@ export declare class VehicleBody {
|
|
|
77
77
|
private readonly surfaceFriction;
|
|
78
78
|
private groundedCount;
|
|
79
79
|
constructor(world: PhysicsWorld, config: VehicleBodyConfig);
|
|
80
|
+
/**
|
|
81
|
+
* A wheel's local offset rotated by the current heading — the ground-side mount and the
|
|
82
|
+
* chassis-side spring anchor both derive from this, so the suspension axis stays vertical (not a
|
|
83
|
+
* mix of rotated and unrotated ends) as the chassis turns.
|
|
84
|
+
*/
|
|
85
|
+
private rotateOffset;
|
|
80
86
|
private wheelMount;
|
|
81
87
|
get position(): [number, number, number];
|
|
82
88
|
get forward(): [number, number];
|
|
@@ -101,7 +101,7 @@ export class VehicleBody {
|
|
|
101
101
|
const rest = wheel.restLength ?? this.suspensionRest;
|
|
102
102
|
const id = world.springJoint({
|
|
103
103
|
bodyA: this.chassis,
|
|
104
|
-
anchorA:
|
|
104
|
+
anchorA: this.rotateOffset(wheel.offset),
|
|
105
105
|
anchorB: [worldPos[0], worldPos[1] - rest, worldPos[2]],
|
|
106
106
|
restLength: rest,
|
|
107
107
|
stiffness,
|
|
@@ -118,15 +118,20 @@ export class VehicleBody {
|
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
/**
|
|
122
|
+
* A wheel's local offset rotated by the current heading — the ground-side mount and the
|
|
123
|
+
* chassis-side spring anchor both derive from this, so the suspension axis stays vertical (not a
|
|
124
|
+
* mix of rotated and unrotated ends) as the chassis turns.
|
|
125
|
+
*/
|
|
126
|
+
rotateOffset(offset) {
|
|
122
127
|
const sin = Math.sin(this.heading);
|
|
123
128
|
const cos = Math.cos(this.heading);
|
|
124
|
-
const [ox, oy, oz] =
|
|
125
|
-
return [
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
];
|
|
129
|
+
const [ox, oy, oz] = offset;
|
|
130
|
+
return [ox * cos + oz * sin, oy, -ox * sin + oz * cos];
|
|
131
|
+
}
|
|
132
|
+
wheelMount(wheel) {
|
|
133
|
+
const [dx, dy, dz] = this.rotateOffset(wheel.offset);
|
|
134
|
+
return [this.world.posX[this.chassis] + dx, this.world.posY[this.chassis] + dy, this.world.posZ[this.chassis] + dz];
|
|
130
135
|
}
|
|
131
136
|
get position() {
|
|
132
137
|
return [this.world.posX[this.chassis], this.world.posY[this.chassis], this.world.posZ[this.chassis]];
|
|
@@ -149,16 +154,8 @@ export class VehicleBody {
|
|
|
149
154
|
return this.wheels.length;
|
|
150
155
|
}
|
|
151
156
|
resetTo(position, heading) {
|
|
152
|
-
|
|
153
|
-
const c = this.chassis;
|
|
154
|
-
w.posX[c] = position[0];
|
|
155
|
-
w.posY[c] = position[1];
|
|
156
|
-
w.posZ[c] = position[2];
|
|
157
|
-
w.velX[c] = 0;
|
|
158
|
-
w.velY[c] = 0;
|
|
159
|
-
w.velZ[c] = 0;
|
|
157
|
+
this.world.teleport(this.chassis, position[0], position[1], position[2]);
|
|
160
158
|
this.heading = heading;
|
|
161
|
-
w.wake(c);
|
|
162
159
|
}
|
|
163
160
|
update(dt, input) {
|
|
164
161
|
const w = this.world;
|
|
@@ -212,6 +209,8 @@ export class VehicleBody {
|
|
|
212
209
|
const gap = mount[1] - (groundY + radius);
|
|
213
210
|
const maxTravel = rest + radius;
|
|
214
211
|
const grounded = gap <= maxTravel;
|
|
212
|
+
const anchorA = this.rotateOffset(wheel.offset);
|
|
213
|
+
w.setJointAnchorA(this.springs[i], anchorA[0], anchorA[1], anchorA[2]);
|
|
215
214
|
w.setJointAnchor(this.springs[i], mount[0], groundY + radius, mount[2]);
|
|
216
215
|
state.worldX = mount[0];
|
|
217
216
|
state.worldY = groundY + radius;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface CellGrid<T> {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
readonly cells: readonly (T | null)[];
|
|
5
|
+
}
|
|
6
|
+
export interface CellRun<T> {
|
|
7
|
+
readonly value: T;
|
|
8
|
+
readonly cells: readonly {
|
|
9
|
+
readonly x: number;
|
|
10
|
+
readonly y: number;
|
|
11
|
+
}[];
|
|
12
|
+
readonly direction: "row" | "column";
|
|
13
|
+
}
|
|
14
|
+
export declare function createCellGrid<T>(width: number, height: number): CellGrid<T>;
|
|
15
|
+
export declare function inGridBounds<T>(grid: CellGrid<T>, x: number, y: number): boolean;
|
|
16
|
+
export declare function cellAt<T>(grid: CellGrid<T>, x: number, y: number): T | null;
|
|
17
|
+
export declare function withCell<T>(grid: CellGrid<T>, x: number, y: number, value: T | null): CellGrid<T>;
|
|
18
|
+
export declare function withCells<T>(grid: CellGrid<T>, entries: readonly {
|
|
19
|
+
readonly x: number;
|
|
20
|
+
readonly y: number;
|
|
21
|
+
readonly value: T | null;
|
|
22
|
+
}[]): CellGrid<T>;
|
|
23
|
+
export declare function fullRows<T>(grid: CellGrid<T>): number[];
|
|
24
|
+
export declare function clearRows<T>(grid: CellGrid<T>, rows: readonly number[]): CellGrid<T>;
|
|
25
|
+
export declare function collapseColumns<T>(grid: CellGrid<T>): CellGrid<T>;
|
|
26
|
+
export declare function findRuns<T>(grid: CellGrid<T>, minLength: number, matches?: (a: T, b: T) => boolean): CellRun<T>[];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export function createCellGrid(width, height) {
|
|
2
|
+
return { width, height, cells: new Array(width * height).fill(null) };
|
|
3
|
+
}
|
|
4
|
+
export function inGridBounds(grid, x, y) {
|
|
5
|
+
return x >= 0 && x < grid.width && y >= 0 && y < grid.height;
|
|
6
|
+
}
|
|
7
|
+
export function cellAt(grid, x, y) {
|
|
8
|
+
if (!inGridBounds(grid, x, y))
|
|
9
|
+
return null;
|
|
10
|
+
return grid.cells[y * grid.width + x] ?? null;
|
|
11
|
+
}
|
|
12
|
+
export function withCell(grid, x, y, value) {
|
|
13
|
+
if (!inGridBounds(grid, x, y))
|
|
14
|
+
return grid;
|
|
15
|
+
const cells = grid.cells.slice();
|
|
16
|
+
cells[y * grid.width + x] = value;
|
|
17
|
+
return { width: grid.width, height: grid.height, cells };
|
|
18
|
+
}
|
|
19
|
+
export function withCells(grid, entries) {
|
|
20
|
+
const cells = grid.cells.slice();
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (inGridBounds(grid, entry.x, entry.y))
|
|
23
|
+
cells[entry.y * grid.width + entry.x] = entry.value;
|
|
24
|
+
}
|
|
25
|
+
return { width: grid.width, height: grid.height, cells };
|
|
26
|
+
}
|
|
27
|
+
export function fullRows(grid) {
|
|
28
|
+
const rows = [];
|
|
29
|
+
for (let y = 0; y < grid.height; y += 1) {
|
|
30
|
+
let full = true;
|
|
31
|
+
for (let x = 0; x < grid.width; x += 1) {
|
|
32
|
+
if (cellAt(grid, x, y) === null) {
|
|
33
|
+
full = false;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (full)
|
|
38
|
+
rows.push(y);
|
|
39
|
+
}
|
|
40
|
+
return rows;
|
|
41
|
+
}
|
|
42
|
+
export function clearRows(grid, rows) {
|
|
43
|
+
const removed = new Set(rows);
|
|
44
|
+
const kept = [];
|
|
45
|
+
for (let y = 0; y < grid.height; y += 1) {
|
|
46
|
+
if (removed.has(y))
|
|
47
|
+
continue;
|
|
48
|
+
kept.push(grid.cells.slice(y * grid.width, y * grid.width + grid.width));
|
|
49
|
+
}
|
|
50
|
+
const clearedCount = grid.height - kept.length;
|
|
51
|
+
const cells = [];
|
|
52
|
+
for (let i = 0; i < clearedCount; i += 1)
|
|
53
|
+
cells.push(...new Array(grid.width).fill(null));
|
|
54
|
+
for (const row of kept)
|
|
55
|
+
cells.push(...row);
|
|
56
|
+
return { width: grid.width, height: grid.height, cells };
|
|
57
|
+
}
|
|
58
|
+
export function collapseColumns(grid) {
|
|
59
|
+
const cells = grid.cells.slice();
|
|
60
|
+
for (let x = 0; x < grid.width; x += 1) {
|
|
61
|
+
const values = [];
|
|
62
|
+
for (let y = 0; y < grid.height; y += 1) {
|
|
63
|
+
const value = cellAt(grid, x, y);
|
|
64
|
+
if (value !== null)
|
|
65
|
+
values.push(value);
|
|
66
|
+
}
|
|
67
|
+
const emptyCount = grid.height - values.length;
|
|
68
|
+
for (let y = 0; y < grid.height; y += 1) {
|
|
69
|
+
cells[y * grid.width + x] = y < emptyCount ? null : values[y - emptyCount];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { width: grid.width, height: grid.height, cells };
|
|
73
|
+
}
|
|
74
|
+
function scanRuns(length, cellAtIndex, minLength, matches) {
|
|
75
|
+
const runs = [];
|
|
76
|
+
let start = 0;
|
|
77
|
+
for (let i = 1; i <= length; i += 1) {
|
|
78
|
+
const prev = cellAtIndex(i - 1);
|
|
79
|
+
const cur = i < length ? cellAtIndex(i) : null;
|
|
80
|
+
const continues = prev !== null && cur !== null && matches(prev, cur);
|
|
81
|
+
if (!continues) {
|
|
82
|
+
const runLength = i - start;
|
|
83
|
+
const value = cellAtIndex(start);
|
|
84
|
+
if (runLength >= minLength && value !== null)
|
|
85
|
+
runs.push({ value, start, end: i });
|
|
86
|
+
start = i;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return runs;
|
|
90
|
+
}
|
|
91
|
+
export function findRuns(grid, minLength, matches = (a, b) => a === b) {
|
|
92
|
+
const runs = [];
|
|
93
|
+
for (let y = 0; y < grid.height; y += 1) {
|
|
94
|
+
for (const run of scanRuns(grid.width, (x) => cellAt(grid, x, y), minLength, matches)) {
|
|
95
|
+
const cells = [];
|
|
96
|
+
for (let x = run.start; x < run.end; x += 1)
|
|
97
|
+
cells.push({ x, y });
|
|
98
|
+
runs.push({ value: run.value, cells, direction: "row" });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (let x = 0; x < grid.width; x += 1) {
|
|
102
|
+
for (const run of scanRuns(grid.height, (y) => cellAt(grid, x, y), minLength, matches)) {
|
|
103
|
+
const cells = [];
|
|
104
|
+
for (let y = run.start; y < run.end; y += 1)
|
|
105
|
+
cells.push({ x, y });
|
|
106
|
+
runs.push({ value: run.value, cells, direction: "column" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return runs;
|
|
110
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type CellGrid } from "./cellGrid.js";
|
|
2
|
+
export interface FallingPiece<TShape extends string = string> {
|
|
3
|
+
readonly shape: TShape;
|
|
4
|
+
readonly rotation: number;
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
}
|
|
8
|
+
export type ShapeTable<TShape extends string = string> = Record<TShape, readonly (readonly (readonly [number, number])[])[]>;
|
|
9
|
+
export declare function pieceCells<TShape extends string>(table: ShapeTable<TShape>, piece: FallingPiece<TShape>): readonly (readonly [number, number])[];
|
|
10
|
+
export declare function pieceCollides<T, TShape extends string>(grid: CellGrid<T>, table: ShapeTable<TShape>, piece: FallingPiece<TShape>): boolean;
|
|
11
|
+
export declare function mergePiece<T, TShape extends string>(grid: CellGrid<T>, table: ShapeTable<TShape>, piece: FallingPiece<TShape>, value: T): CellGrid<T>;
|
|
12
|
+
export declare function dropDistance<T, TShape extends string>(grid: CellGrid<T>, table: ShapeTable<TShape>, piece: FallingPiece<TShape>): number;
|
|
13
|
+
export declare function gravityInterval(level: number, base?: number, perLevel?: number, min?: number): number;
|
|
14
|
+
export declare function levelForLines(lines: number, linesPerLevel?: number): number;
|
|
15
|
+
export declare function lineScore(cleared: number, level: number, table?: readonly number[]): number;
|
|
16
|
+
export interface LockDelayState {
|
|
17
|
+
readonly delaySeconds: number;
|
|
18
|
+
readonly elapsed: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function createLockDelay(delaySeconds: number): LockDelayState;
|
|
21
|
+
export declare function stepLockDelay(state: LockDelayState, grounded: boolean, dt: number): {
|
|
22
|
+
state: LockDelayState;
|
|
23
|
+
locked: boolean;
|
|
24
|
+
};
|