@jgengine/core 0.6.0 → 0.7.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.
Files changed (222) hide show
  1. package/CHANGELOG.md +167 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +2 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +281 -4
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +35 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +83 -0
  116. package/dist/physics/physicsWorld.js +261 -1
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +64 -2
  128. package/dist/runtime/gameContext.js +154 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/autoTarget.d.ts +15 -0
  134. package/dist/scene/autoTarget.js +54 -0
  135. package/dist/scene/behaviors.d.ts +13 -1
  136. package/dist/scene/behaviors.js +3 -0
  137. package/dist/scene/captureCheck.d.ts +7 -0
  138. package/dist/scene/captureCheck.js +11 -0
  139. package/dist/scene/entityStore.d.ts +1 -1
  140. package/dist/scene/form.d.ts +40 -0
  141. package/dist/scene/form.js +56 -0
  142. package/dist/scene/mount.d.ts +62 -0
  143. package/dist/scene/mount.js +107 -0
  144. package/dist/scene/possession.d.ts +30 -0
  145. package/dist/scene/possession.js +51 -0
  146. package/dist/scene/roster.d.ts +25 -0
  147. package/dist/scene/roster.js +57 -0
  148. package/dist/scene/selection.d.ts +30 -0
  149. package/dist/scene/selection.js +38 -0
  150. package/dist/scene/stationClaim.d.ts +43 -0
  151. package/dist/scene/stationClaim.js +109 -0
  152. package/dist/sensor/frustumSensor.d.ts +60 -0
  153. package/dist/sensor/frustumSensor.js +104 -0
  154. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  155. package/dist/sensor/hiddenStateProbe.js +40 -0
  156. package/dist/sensor/recordingBuffer.d.ts +29 -0
  157. package/dist/sensor/recordingBuffer.js +61 -0
  158. package/dist/sensor/revealQuery.d.ts +22 -0
  159. package/dist/sensor/revealQuery.js +36 -0
  160. package/dist/session/contestedChannel.d.ts +36 -0
  161. package/dist/session/contestedChannel.js +100 -0
  162. package/dist/session/extraction.d.ts +54 -0
  163. package/dist/session/extraction.js +108 -0
  164. package/dist/session/ring.d.ts +36 -0
  165. package/dist/session/ring.js +88 -0
  166. package/dist/session/roundState.d.ts +55 -0
  167. package/dist/session/roundState.js +129 -0
  168. package/dist/stats/accumulatorMeter.d.ts +31 -0
  169. package/dist/stats/accumulatorMeter.js +81 -0
  170. package/dist/stats/eventMeter.d.ts +34 -0
  171. package/dist/stats/eventMeter.js +76 -0
  172. package/dist/stats/rollCheck.d.ts +15 -0
  173. package/dist/stats/rollCheck.js +15 -0
  174. package/dist/survival/decayMeter.d.ts +59 -0
  175. package/dist/survival/decayMeter.js +95 -0
  176. package/dist/survival/moodle.d.ts +48 -0
  177. package/dist/survival/moodle.js +91 -0
  178. package/dist/survival/regionHealth.d.ts +72 -0
  179. package/dist/survival/regionHealth.js +129 -0
  180. package/dist/tactics/predictiveQuery.d.ts +34 -0
  181. package/dist/tactics/predictiveQuery.js +44 -0
  182. package/dist/tactics/snapshot.d.ts +18 -0
  183. package/dist/tactics/snapshot.js +65 -0
  184. package/dist/tactics/surface.d.ts +52 -0
  185. package/dist/tactics/surface.js +135 -0
  186. package/dist/tactics/tacticalGrid.d.ts +57 -0
  187. package/dist/tactics/tacticalGrid.js +244 -0
  188. package/dist/time/beatClock.d.ts +40 -0
  189. package/dist/time/beatClock.js +73 -0
  190. package/dist/turn/commit.d.ts +27 -0
  191. package/dist/turn/commit.js +72 -0
  192. package/dist/turn/turnLoop.d.ts +58 -0
  193. package/dist/turn/turnLoop.js +216 -0
  194. package/dist/world/buildPermissions.d.ts +47 -0
  195. package/dist/world/buildPermissions.js +134 -0
  196. package/dist/world/carve.d.ts +105 -0
  197. package/dist/world/carve.js +175 -0
  198. package/dist/world/connectors.d.ts +40 -0
  199. package/dist/world/connectors.js +67 -0
  200. package/dist/world/envField.d.ts +71 -0
  201. package/dist/world/envField.js +96 -0
  202. package/dist/world/fog.d.ts +40 -0
  203. package/dist/world/fog.js +137 -0
  204. package/dist/world/markers.d.ts +52 -0
  205. package/dist/world/markers.js +96 -0
  206. package/dist/world/minimap.d.ts +42 -0
  207. package/dist/world/minimap.js +72 -0
  208. package/dist/world/placedStructureStore.d.ts +39 -0
  209. package/dist/world/placedStructureStore.js +95 -0
  210. package/dist/world/placementController.d.ts +48 -0
  211. package/dist/world/placementController.js +97 -0
  212. package/dist/world/realm.d.ts +61 -0
  213. package/dist/world/realm.js +66 -0
  214. package/dist/world/support.d.ts +27 -0
  215. package/dist/world/support.js +75 -0
  216. package/dist/world/terraform.d.ts +54 -0
  217. package/dist/world/terraform.js +186 -0
  218. package/dist/world/walls.d.ts +61 -0
  219. package/dist/world/walls.js +221 -0
  220. package/dist/world/weather.d.ts +92 -0
  221. package/dist/world/weather.js +169 -0
  222. package/package.json +1 -1
@@ -0,0 +1,56 @@
1
+ import type { CollisionEvent } from "./physicsWorld.js";
2
+ export interface DamageZoneDef {
3
+ id: string;
4
+ thresholds: readonly number[];
5
+ detachStage?: number;
6
+ }
7
+ export interface DamageZoneState {
8
+ id: string;
9
+ accumulated: number;
10
+ stage: number;
11
+ detached: boolean;
12
+ }
13
+ export interface DamageTransition {
14
+ zone: string;
15
+ stage: number;
16
+ previousStage: number;
17
+ detached: boolean;
18
+ disabled: boolean;
19
+ accumulated: number;
20
+ total: number;
21
+ }
22
+ export interface DamageModelConfig {
23
+ zones: readonly DamageZoneDef[];
24
+ disableAt?: number;
25
+ onStage?: (transition: DamageTransition) => void;
26
+ onDetach?: (zoneId: string, accumulated: number) => void;
27
+ }
28
+ /**
29
+ * Coarse crash-damage model (issue #86 — stages, not soft-body). Each zone accumulates contact impulse
30
+ * fed from G3's `onCollision` `CollisionEvent`; crossing a threshold bumps the zone to the next discrete
31
+ * stage (the caller swaps the visual/collider), an optional `detachStage` ejects a part as debris once,
32
+ * and a total-impulse `disableAt` flips the whole body to a disabled state. `absorb` returns a transition
33
+ * only when something actually changed, so it doubles as the event source for HUD/feed.
34
+ */
35
+ export declare class DamageModel {
36
+ private readonly zones;
37
+ private readonly order;
38
+ private readonly disableAt;
39
+ private readonly onStage?;
40
+ private readonly onDetach?;
41
+ private totalImpulse;
42
+ private disabledFlag;
43
+ constructor(config: DamageModelConfig);
44
+ get total(): number;
45
+ get disabled(): boolean;
46
+ stageOf(zoneId: string): number;
47
+ states(): readonly DamageZoneState[];
48
+ absorb(zoneId: string, impulse: number): DamageTransition | null;
49
+ /**
50
+ * Route a raw {@link CollisionEvent} into a zone by a caller-supplied selector (front/rear/side from
51
+ * `nx`/`nz`, or which body index took the hit). The event's `impulse` feeds `absorb`.
52
+ */
53
+ routeCollision(event: CollisionEvent, resolveZone: (event: CollisionEvent) => string | null): DamageTransition | null;
54
+ reset(): void;
55
+ }
56
+ export declare function createDamageModel(config: DamageModelConfig): DamageModel;
@@ -0,0 +1,107 @@
1
+ function stageFor(thresholds, accumulated) {
2
+ let stage = 0;
3
+ for (const t of thresholds) {
4
+ if (accumulated >= t)
5
+ stage += 1;
6
+ else
7
+ break;
8
+ }
9
+ return stage;
10
+ }
11
+ /**
12
+ * Coarse crash-damage model (issue #86 — stages, not soft-body). Each zone accumulates contact impulse
13
+ * fed from G3's `onCollision` `CollisionEvent`; crossing a threshold bumps the zone to the next discrete
14
+ * stage (the caller swaps the visual/collider), an optional `detachStage` ejects a part as debris once,
15
+ * and a total-impulse `disableAt` flips the whole body to a disabled state. `absorb` returns a transition
16
+ * only when something actually changed, so it doubles as the event source for HUD/feed.
17
+ */
18
+ export class DamageModel {
19
+ zones = new Map();
20
+ order = [];
21
+ disableAt;
22
+ onStage;
23
+ onDetach;
24
+ totalImpulse = 0;
25
+ disabledFlag = false;
26
+ constructor(config) {
27
+ for (const def of config.zones) {
28
+ this.zones.set(def.id, { def, state: { id: def.id, accumulated: 0, stage: 0, detached: false } });
29
+ this.order.push(def.id);
30
+ }
31
+ this.disableAt = config.disableAt ?? null;
32
+ this.onStage = config.onStage;
33
+ this.onDetach = config.onDetach;
34
+ }
35
+ get total() {
36
+ return this.totalImpulse;
37
+ }
38
+ get disabled() {
39
+ return this.disabledFlag;
40
+ }
41
+ stageOf(zoneId) {
42
+ return this.zones.get(zoneId)?.state.stage ?? 0;
43
+ }
44
+ states() {
45
+ return this.order.map((id) => {
46
+ const s = this.zones.get(id).state;
47
+ return { id: s.id, accumulated: s.accumulated, stage: s.stage, detached: s.detached };
48
+ });
49
+ }
50
+ absorb(zoneId, impulse) {
51
+ const entry = this.zones.get(zoneId);
52
+ if (entry === undefined || impulse <= 0)
53
+ return null;
54
+ const { def, state } = entry;
55
+ const previousStage = state.stage;
56
+ const wasDisabled = this.disabledFlag;
57
+ state.accumulated += impulse;
58
+ this.totalImpulse += impulse;
59
+ state.stage = stageFor(def.thresholds, state.accumulated);
60
+ let detachedNow = false;
61
+ if (def.detachStage !== undefined &&
62
+ !state.detached &&
63
+ state.stage >= def.detachStage) {
64
+ state.detached = true;
65
+ detachedNow = true;
66
+ this.onDetach?.(zoneId, state.accumulated);
67
+ }
68
+ if (this.disableAt !== null && this.totalImpulse >= this.disableAt)
69
+ this.disabledFlag = true;
70
+ const changed = state.stage !== previousStage || detachedNow || this.disabledFlag !== wasDisabled;
71
+ if (!changed)
72
+ return null;
73
+ const transition = {
74
+ zone: zoneId,
75
+ stage: state.stage,
76
+ previousStage,
77
+ detached: state.detached,
78
+ disabled: this.disabledFlag,
79
+ accumulated: state.accumulated,
80
+ total: this.totalImpulse,
81
+ };
82
+ this.onStage?.(transition);
83
+ return transition;
84
+ }
85
+ /**
86
+ * Route a raw {@link CollisionEvent} into a zone by a caller-supplied selector (front/rear/side from
87
+ * `nx`/`nz`, or which body index took the hit). The event's `impulse` feeds `absorb`.
88
+ */
89
+ routeCollision(event, resolveZone) {
90
+ const zone = resolveZone(event);
91
+ if (zone === null)
92
+ return null;
93
+ return this.absorb(zone, Math.abs(event.impulse));
94
+ }
95
+ reset() {
96
+ for (const { state } of this.zones.values()) {
97
+ state.accumulated = 0;
98
+ state.stage = 0;
99
+ state.detached = false;
100
+ }
101
+ this.totalImpulse = 0;
102
+ this.disabledFlag = false;
103
+ }
104
+ }
105
+ export function createDamageModel(config) {
106
+ return new DamageModel(config);
107
+ }
@@ -0,0 +1,48 @@
1
+ import type { PhysicsBounds, PhysicsWorld } from "./physicsWorld.js";
2
+ export type ForceMode = "impulse" | "velocity" | "accelerate";
3
+ export interface ForceVolumeConfig {
4
+ /** Region a body's center must be inside to be affected. */
5
+ bounds: PhysicsBounds;
6
+ /** `impulse` adds to velocity, `velocity` sets it, `accelerate` adds force·dt each tick. */
7
+ force: readonly [number, number, number];
8
+ /** Default `impulse`. */
9
+ mode?: ForceMode;
10
+ /** Fire only when a body first enters (boost pad) rather than every tick inside (fan/wind). Default false. */
11
+ once?: boolean;
12
+ }
13
+ /**
14
+ * A trigger region that pushes bodies passing through it — boost pads (`impulse` + `once`),
15
+ * conveyors (`velocity`), fans/wind (`accelerate`). Call `apply` each tick; `once` mode fires only
16
+ * on entry by tracking membership between ticks.
17
+ */
18
+ export declare class ForceVolume {
19
+ readonly bounds: PhysicsBounds;
20
+ readonly mode: ForceMode;
21
+ readonly once: boolean;
22
+ private readonly fx;
23
+ private readonly fy;
24
+ private readonly fz;
25
+ private members;
26
+ private nextMembers;
27
+ constructor(config: ForceVolumeConfig);
28
+ apply(world: PhysicsWorld, dt: number): void;
29
+ }
30
+ export interface PlatformCarryConfig {
31
+ /** Vertical gap between a rider's base and the platform top counted as "standing on". Default 0.12. */
32
+ contactTolerance?: number;
33
+ }
34
+ /**
35
+ * Carries bodies standing on a moving platform by composing their transform with the platform's
36
+ * per-`step` delta — moving/rotating lifts and conveyor floors (Fall Guys, Gang Beasts). The
37
+ * platform is a body the game repositions each frame; riders are detected by overlap on its top face.
38
+ */
39
+ export declare class PlatformCarry {
40
+ private readonly world;
41
+ private readonly platform;
42
+ private readonly tolerance;
43
+ private prevX;
44
+ private prevY;
45
+ private prevZ;
46
+ constructor(world: PhysicsWorld, platform: number, config?: PlatformCarryConfig);
47
+ step(): void;
48
+ }
@@ -0,0 +1,119 @@
1
+ function inside(bounds, x, y, z) {
2
+ return (x >= bounds.min[0] &&
3
+ x <= bounds.max[0] &&
4
+ y >= bounds.min[1] &&
5
+ y <= bounds.max[1] &&
6
+ z >= bounds.min[2] &&
7
+ z <= bounds.max[2]);
8
+ }
9
+ /**
10
+ * A trigger region that pushes bodies passing through it — boost pads (`impulse` + `once`),
11
+ * conveyors (`velocity`), fans/wind (`accelerate`). Call `apply` each tick; `once` mode fires only
12
+ * on entry by tracking membership between ticks.
13
+ */
14
+ export class ForceVolume {
15
+ bounds;
16
+ mode;
17
+ once;
18
+ fx;
19
+ fy;
20
+ fz;
21
+ members = new Set();
22
+ nextMembers = new Set();
23
+ constructor(config) {
24
+ this.bounds = config.bounds;
25
+ this.mode = config.mode ?? "impulse";
26
+ this.once = config.once ?? false;
27
+ this.fx = config.force[0];
28
+ this.fy = config.force[1];
29
+ this.fz = config.force[2];
30
+ }
31
+ apply(world, dt) {
32
+ const next = this.nextMembers;
33
+ next.clear();
34
+ const count = world.count;
35
+ for (let i = 0; i < count; i += 1) {
36
+ if (world.invMass[i] === 0)
37
+ continue;
38
+ if (!inside(this.bounds, world.posX[i], world.posY[i], world.posZ[i]))
39
+ continue;
40
+ const wasInside = this.members.has(i);
41
+ next.add(i);
42
+ if (this.once && wasInside)
43
+ continue;
44
+ switch (this.mode) {
45
+ case "velocity":
46
+ world.velX[i] = this.fx;
47
+ world.velY[i] = this.fy;
48
+ world.velZ[i] = this.fz;
49
+ break;
50
+ case "accelerate":
51
+ world.velX[i] += this.fx * dt;
52
+ world.velY[i] += this.fy * dt;
53
+ world.velZ[i] += this.fz * dt;
54
+ break;
55
+ default:
56
+ world.velX[i] += this.fx;
57
+ world.velY[i] += this.fy;
58
+ world.velZ[i] += this.fz;
59
+ }
60
+ world.wake(i);
61
+ }
62
+ this.nextMembers = this.members;
63
+ this.members = next;
64
+ }
65
+ }
66
+ /**
67
+ * Carries bodies standing on a moving platform by composing their transform with the platform's
68
+ * per-`step` delta — moving/rotating lifts and conveyor floors (Fall Guys, Gang Beasts). The
69
+ * platform is a body the game repositions each frame; riders are detected by overlap on its top face.
70
+ */
71
+ export class PlatformCarry {
72
+ world;
73
+ platform;
74
+ tolerance;
75
+ prevX;
76
+ prevY;
77
+ prevZ;
78
+ constructor(world, platform, config = {}) {
79
+ this.world = world;
80
+ this.platform = platform;
81
+ this.tolerance = config.contactTolerance ?? 0.12;
82
+ this.prevX = world.posX[platform];
83
+ this.prevY = world.posY[platform];
84
+ this.prevZ = world.posZ[platform];
85
+ }
86
+ step() {
87
+ const w = this.world;
88
+ const p = this.platform;
89
+ const dx = w.posX[p] - this.prevX;
90
+ const dy = w.posY[p] - this.prevY;
91
+ const dz = w.posZ[p] - this.prevZ;
92
+ const topY = this.prevY + w.halfY[p];
93
+ const px = this.prevX;
94
+ const pz = this.prevZ;
95
+ this.prevX = w.posX[p];
96
+ this.prevY = w.posY[p];
97
+ this.prevZ = w.posZ[p];
98
+ if (dx === 0 && dy === 0 && dz === 0)
99
+ return;
100
+ const hx = w.halfX[p];
101
+ const hz = w.halfZ[p];
102
+ const count = w.count;
103
+ for (let i = 0; i < count; i += 1) {
104
+ if (i === p)
105
+ continue;
106
+ const base = w.posY[i] - w.halfY[i];
107
+ if (base < topY - this.tolerance || base > topY + this.tolerance)
108
+ continue;
109
+ if (Math.abs(w.posX[i] - px) > hx + w.halfX[i])
110
+ continue;
111
+ if (Math.abs(w.posZ[i] - pz) > hz + w.halfZ[i])
112
+ continue;
113
+ w.posX[i] += dx;
114
+ w.posY[i] += dy;
115
+ w.posZ[i] += dz;
116
+ w.wake(i);
117
+ }
118
+ }
119
+ }
@@ -35,6 +35,10 @@ export interface PhysicsWorldConfig {
35
35
  sleepThresholdSteps?: number;
36
36
  /** Approach speed a contact must exceed to wake a sleeping body (resting contacts don't). Default 0.5. */
37
37
  wakeThreshold?: number;
38
+ /** Maximum simultaneous joints the constraint buffers can hold. Default 256. */
39
+ jointCapacity?: number;
40
+ /** Baumgarte fraction applied to hard-joint positional error per iteration, 0..1. Default 0.5. */
41
+ jointCorrection?: number;
38
42
  }
39
43
  export interface PhysicsStats {
40
44
  count: number;
@@ -56,6 +60,42 @@ export interface AddBodyOptions {
56
60
  /** Seed the body asleep (a settled bed pays no integration cost until woken). */
57
61
  asleep?: boolean;
58
62
  }
63
+ /**
64
+ * `hinge`/`fixed`/`distance` are hard bilateral constraints; `spring` is a soft PD constraint.
65
+ * No angular state → `hinge`/`fixed` both pin the shared anchor (`axis` is metadata), `distance`
66
+ * holds a fixed separation, `spring` drives toward `restLength` (suspension, follow-point carry).
67
+ */
68
+ export type JointKind = "hinge" | "distance" | "spring" | "fixed";
69
+ export interface JointOptions {
70
+ /** First body index. */
71
+ bodyA: number;
72
+ /** Second body index, or omit / < 0 to anchor A to a fixed world point (`anchorB`). */
73
+ bodyB?: number;
74
+ /** Local offset of the anchor on A, in world axes (no rotation). Default [0,0,0]. */
75
+ anchorA?: readonly [number, number, number];
76
+ /** Local offset on B, or — when B is a world anchor — the world point itself. Default [0,0,0]. */
77
+ anchorB?: readonly [number, number, number];
78
+ /** Target separation for `distance`/`spring`. Defaults to the anchors' current distance. */
79
+ restLength?: number;
80
+ /** Spring restoring rate. Default 40. */
81
+ stiffness?: number;
82
+ /** Spring damping (relative-velocity bleed). Default 6. */
83
+ damping?: number;
84
+ /** Clamp on the per-substep spring impulse (a follow-point that never yanks). Default unbounded. */
85
+ maxImpulse?: number;
86
+ /** Hinge axis, retained as metadata (this sim has no angular DOF). */
87
+ axis?: readonly [number, number, number];
88
+ }
89
+ /** A contact reported to `onCollision`. The object is reused each call — read/copy, never retain. */
90
+ export interface CollisionEvent {
91
+ a: number;
92
+ b: number;
93
+ nx: number;
94
+ ny: number;
95
+ nz: number;
96
+ approachSpeed: number;
97
+ impulse: number;
98
+ }
59
99
  /** Grid cell containing a point, clamped into the grid. Pure — exported for tests. */
60
100
  export declare function cellCoord(value: number, min: number, cellSize: number, cells: number): number;
61
101
  /** Linear cell index from clamped 3D coords. Pure — exported for tests. */
@@ -77,6 +117,8 @@ export declare class PhysicsWorld {
77
117
  readonly sleepMove2: number;
78
118
  readonly sleepSteps: number;
79
119
  readonly wakeThreshold: number;
120
+ readonly jointCapacity: number;
121
+ readonly jointCorrection: number;
80
122
  readonly posX: Float32Array;
81
123
  readonly posY: Float32Array;
82
124
  readonly posZ: Float32Array;
@@ -105,6 +147,27 @@ export declare class PhysicsWorld {
105
147
  private readonly awakeList;
106
148
  private readonly awakeSlot;
107
149
  private awakeCount;
150
+ private readonly jSpring;
151
+ private readonly jActive;
152
+ private readonly jBodyA;
153
+ private readonly jBodyB;
154
+ private readonly jAnchorAX;
155
+ private readonly jAnchorAY;
156
+ private readonly jAnchorAZ;
157
+ private readonly jAnchorBX;
158
+ private readonly jAnchorBY;
159
+ private readonly jAnchorBZ;
160
+ private readonly jRest;
161
+ private readonly jStiffness;
162
+ private readonly jDamping;
163
+ private readonly jMaxImpulse;
164
+ private readonly jFree;
165
+ private jFreeCount;
166
+ private jointHigh;
167
+ private jointActiveCount;
168
+ private collisionListener;
169
+ private collisionMinSpeed;
170
+ private readonly collisionEvent;
108
171
  private bodyCount;
109
172
  private accumulator;
110
173
  private readonly stats;
@@ -118,6 +181,25 @@ export declare class PhysicsWorld {
118
181
  nz: number;
119
182
  total: number;
120
183
  };
184
+ get jointCount(): number;
185
+ /**
186
+ * Deliver every impacting contact (approach speed ≥ `minApproachSpeed`) to `listener` during
187
+ * `step`. The event object is reused — read/copy it, never retain. Pass `null` to detach. This is
188
+ * the seam crash-damage and destruction read; contacts otherwise stay inside the sim.
189
+ */
190
+ onCollision(listener: ((e: CollisionEvent) => void) | null, minApproachSpeed?: number): void;
191
+ hingeJoint(options: JointOptions): number;
192
+ fixedJoint(options: JointOptions): number;
193
+ distanceJoint(options: JointOptions): number;
194
+ springJoint(options: JointOptions): number;
195
+ private addJoint;
196
+ removeJoint(id: number): void;
197
+ /** Move a joint's world anchor point (only meaningful when `bodyB` is a world anchor). */
198
+ setJointAnchor(id: number, x: number, y: number, z: number): void;
199
+ setJointRest(id: number, restLength: number): void;
200
+ private jointDistance;
201
+ /** Write [ax,ay,az,bx,by,bz] world endpoints per active joint into `out`; returns the joint count. */
202
+ readJointSegments(out: Float32Array): number;
121
203
  addBody(options: AddBodyOptions): number;
122
204
  isSleeping(i: number): boolean;
123
205
  wake(i: number): void;
@@ -132,5 +214,6 @@ export declare class PhysicsWorld {
132
214
  private cellOf;
133
215
  private solve;
134
216
  private resolve;
217
+ private solveJoints;
135
218
  private updateSleep;
136
219
  }