@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,248 @@
1
+ const SQRT2 = Math.SQRT2;
2
+ function clampInt(value, min, max) {
3
+ return value < min ? min : value > max ? max : value;
4
+ }
5
+ export function createNavGrid(config) {
6
+ const { bounds, cellSize } = config;
7
+ if (!(cellSize > 0))
8
+ throw new Error("navGrid cellSize must be positive.");
9
+ const diagonal = config.diagonal ?? true;
10
+ const width = Math.max(bounds.maxX - bounds.minX, cellSize);
11
+ const depth = Math.max(bounds.maxZ - bounds.minZ, cellSize);
12
+ const cols = Math.max(1, Math.ceil(width / cellSize));
13
+ const rows = Math.max(1, Math.ceil(depth / cellSize));
14
+ const walkable = new Uint8Array(cols * rows).fill(1);
15
+ const index = (col, row) => row * cols + col;
16
+ const inBounds = (col, row) => col >= 0 && col < cols && row >= 0 && row < rows;
17
+ function cellAt(point) {
18
+ return {
19
+ col: clampInt(Math.floor((point[0] - bounds.minX) / cellSize), 0, cols - 1),
20
+ row: clampInt(Math.floor((point[1] - bounds.minZ) / cellSize), 0, rows - 1),
21
+ };
22
+ }
23
+ function center(col, row) {
24
+ return [bounds.minX + (col + 0.5) * cellSize, bounds.minZ + (row + 0.5) * cellSize];
25
+ }
26
+ function markAabb(aabb, value) {
27
+ const min = cellAt([aabb.minX, aabb.minZ]);
28
+ const max = cellAt([aabb.maxX, aabb.maxZ]);
29
+ for (let row = min.row; row <= max.row; row += 1) {
30
+ for (let col = min.col; col <= max.col; col += 1)
31
+ walkable[index(col, row)] = value;
32
+ }
33
+ }
34
+ const grid = {
35
+ cols,
36
+ rows,
37
+ cellSize,
38
+ bounds,
39
+ diagonal,
40
+ isWalkable: (col, row) => inBounds(col, row) && walkable[index(col, row)] === 1,
41
+ setWalkable(col, row, next) {
42
+ if (inBounds(col, row))
43
+ walkable[index(col, row)] = next ? 1 : 0;
44
+ },
45
+ blockAabb: (aabb) => markAabb(aabb, 0),
46
+ clearAabb: (aabb) => markAabb(aabb, 1),
47
+ reset: (next) => walkable.fill(next ? 1 : 0),
48
+ inBounds,
49
+ cellAt,
50
+ center,
51
+ nearestWalkable(point) {
52
+ const start = cellAt(point);
53
+ if (walkable[index(start.col, start.row)] === 1)
54
+ return start;
55
+ const maxRadius = Math.max(cols, rows);
56
+ for (let radius = 1; radius <= maxRadius; radius += 1) {
57
+ for (let row = start.row - radius; row <= start.row + radius; row += 1) {
58
+ for (let col = start.col - radius; col <= start.col + radius; col += 1) {
59
+ const onRing = Math.abs(col - start.col) === radius || Math.abs(row - start.row) === radius;
60
+ if (!onRing || !inBounds(col, row))
61
+ continue;
62
+ if (walkable[index(col, row)] === 1)
63
+ return { col, row };
64
+ }
65
+ }
66
+ }
67
+ return null;
68
+ },
69
+ lineOfSight(from, to) {
70
+ const a = cellAt(from);
71
+ const b = cellAt(to);
72
+ let x = a.col;
73
+ let y = a.row;
74
+ const dx = Math.abs(b.col - a.col);
75
+ const dy = Math.abs(b.row - a.row);
76
+ const sx = a.col < b.col ? 1 : -1;
77
+ const sy = a.row < b.row ? 1 : -1;
78
+ let err = dx - dy;
79
+ for (;;) {
80
+ if (walkable[index(x, y)] === 0)
81
+ return false;
82
+ if (x === b.col && y === b.row)
83
+ return true;
84
+ const e2 = 2 * err;
85
+ if (e2 > -dy) {
86
+ err -= dy;
87
+ x += sx;
88
+ }
89
+ if (e2 < dx) {
90
+ err += dx;
91
+ y += sy;
92
+ }
93
+ }
94
+ },
95
+ };
96
+ return grid;
97
+ }
98
+ function heapPush(heap, node) {
99
+ heap.push(node);
100
+ let i = heap.length - 1;
101
+ while (i > 0) {
102
+ const parent = (i - 1) >> 1;
103
+ if (heap[parent].f <= heap[i].f)
104
+ break;
105
+ [heap[parent], heap[i]] = [heap[i], heap[parent]];
106
+ i = parent;
107
+ }
108
+ }
109
+ function heapPop(heap) {
110
+ const top = heap[0];
111
+ const last = heap.pop();
112
+ if (heap.length > 0 && last !== undefined) {
113
+ heap[0] = last;
114
+ let i = 0;
115
+ for (;;) {
116
+ const left = i * 2 + 1;
117
+ const right = left + 1;
118
+ let smallest = i;
119
+ if (left < heap.length && heap[left].f < heap[smallest].f)
120
+ smallest = left;
121
+ if (right < heap.length && heap[right].f < heap[smallest].f)
122
+ smallest = right;
123
+ if (smallest === i)
124
+ break;
125
+ [heap[smallest], heap[i]] = [heap[i], heap[smallest]];
126
+ i = smallest;
127
+ }
128
+ }
129
+ return top;
130
+ }
131
+ function withClearance(grid, clearance) {
132
+ if (!(clearance > 0))
133
+ return (col, row) => grid.isWalkable(col, row);
134
+ const pad = Math.max(0, Math.ceil(clearance / grid.cellSize / 2));
135
+ return (col, row) => {
136
+ for (let dy = -pad; dy <= pad; dy += 1) {
137
+ for (let dx = -pad; dx <= pad; dx += 1) {
138
+ if (!grid.isWalkable(col + dx, row + dy))
139
+ return false;
140
+ }
141
+ }
142
+ return true;
143
+ };
144
+ }
145
+ /**
146
+ * A* over the walkable grid. Returns a polyline of world-space `[x, z]` waypoints
147
+ * from `from` to `to`, or `null` when no route exists. Blocked start/goal snap to
148
+ * the nearest walkable cell so a click on an obstacle still routes to its edge.
149
+ */
150
+ export function findPath(grid, from, to, options = {}) {
151
+ const passable = withClearance(grid, options.clearance ?? 0);
152
+ const startCell = grid.cellAt(from);
153
+ const goalCell = grid.cellAt(to);
154
+ const start = passable(startCell.col, startCell.row) ? startCell : grid.nearestWalkable(from);
155
+ const goal = passable(goalCell.col, goalCell.row) ? goalCell : grid.nearestWalkable(to);
156
+ if (start === null || goal === null)
157
+ return null;
158
+ const cols = grid.cols;
159
+ const keyOf = (col, row) => row * cols + col;
160
+ const startKey = keyOf(start.col, start.row);
161
+ const goalKey = keyOf(goal.col, goal.row);
162
+ const gScore = new Map([[startKey, 0]]);
163
+ const cameFrom = new Map();
164
+ const closed = new Set();
165
+ const heap = [];
166
+ const heuristic = (col, row) => {
167
+ const dc = Math.abs(col - goal.col);
168
+ const dr = Math.abs(row - goal.row);
169
+ return grid.diagonal ? dc + dr + (SQRT2 - 2) * Math.min(dc, dr) : dc + dr;
170
+ };
171
+ heapPush(heap, { key: startKey, f: heuristic(start.col, start.row) });
172
+ const neighbours = grid.diagonal
173
+ ? [
174
+ [1, 0, 1],
175
+ [-1, 0, 1],
176
+ [0, 1, 1],
177
+ [0, -1, 1],
178
+ [1, 1, SQRT2],
179
+ [1, -1, SQRT2],
180
+ [-1, 1, SQRT2],
181
+ [-1, -1, SQRT2],
182
+ ]
183
+ : [
184
+ [1, 0, 1],
185
+ [-1, 0, 1],
186
+ [0, 1, 1],
187
+ [0, -1, 1],
188
+ ];
189
+ let found = false;
190
+ while (heap.length > 0) {
191
+ const current = heapPop(heap);
192
+ if (closed.has(current.key))
193
+ continue;
194
+ if (current.key === goalKey) {
195
+ found = true;
196
+ break;
197
+ }
198
+ closed.add(current.key);
199
+ const col = current.key % cols;
200
+ const row = (current.key - col) / cols;
201
+ const g = gScore.get(current.key);
202
+ for (const [dx, dy, step] of neighbours) {
203
+ const nc = col + dx;
204
+ const nr = row + dy;
205
+ if (!passable(nc, nr))
206
+ continue;
207
+ if (dx !== 0 && dy !== 0 && (!passable(col + dx, row) || !passable(col, row + dy)))
208
+ continue;
209
+ const nKey = keyOf(nc, nr);
210
+ if (closed.has(nKey))
211
+ continue;
212
+ const tentative = g + step;
213
+ if (tentative < (gScore.get(nKey) ?? Number.POSITIVE_INFINITY)) {
214
+ gScore.set(nKey, tentative);
215
+ cameFrom.set(nKey, current.key);
216
+ heapPush(heap, { key: nKey, f: tentative + heuristic(nc, nr) });
217
+ }
218
+ }
219
+ }
220
+ if (!found)
221
+ return null;
222
+ const cells = [];
223
+ let cursor = goalKey;
224
+ while (cursor !== undefined) {
225
+ const col = cursor % cols;
226
+ cells.unshift({ col, row: (cursor - col) / cols });
227
+ cursor = cameFrom.get(cursor);
228
+ }
229
+ const raw = cells.map((cell) => grid.center(cell.col, cell.row));
230
+ if (passable(goalCell.col, goalCell.row))
231
+ raw[raw.length - 1] = [to[0], to[1]];
232
+ return options.smooth === false ? raw : smoothPath(grid, raw);
233
+ }
234
+ /** Remove waypoints the mover can skip because it has clear line-of-sight past them. */
235
+ export function smoothPath(grid, points) {
236
+ if (points.length <= 2)
237
+ return points.slice();
238
+ const result = [points[0]];
239
+ let anchor = 0;
240
+ for (let i = 2; i < points.length; i += 1) {
241
+ if (!grid.lineOfSight(points[anchor], points[i])) {
242
+ result.push(points[i - 1]);
243
+ anchor = i - 1;
244
+ }
245
+ }
246
+ result.push(points[points.length - 1]);
247
+ return result;
248
+ }
@@ -0,0 +1,29 @@
1
+ import type { NavPoint } from "./navGrid.js";
2
+ export type Waypoint = readonly [number, number, number];
3
+ export interface PathFollowConfig {
4
+ waypoints: readonly Waypoint[];
5
+ /** World units per second. */
6
+ speed: number;
7
+ /** Wrap back to the first waypoint after the last (never `done`). Default false. */
8
+ loop?: boolean;
9
+ }
10
+ export interface PathFollowState {
11
+ position: Waypoint;
12
+ /** Index of the waypoint the mover is currently travelling toward. */
13
+ target: number;
14
+ /** Yaw (radians) of the current travel direction, matching engine `rotationY`. */
15
+ heading: number;
16
+ done: boolean;
17
+ /** Total world-distance covered so far — drives progress/leak logic in tower defense. */
18
+ distanceTravelled: number;
19
+ }
20
+ /** Lift an A* / navmesh `[x, z]` polyline onto a `y` plane for a path-follower. */
21
+ export declare function pathFromNav(points: readonly NavPoint[], y?: number): Waypoint[];
22
+ export declare function createPathFollow(config: PathFollowConfig): PathFollowState;
23
+ /**
24
+ * Advance a path-follower by `speed * dt` along its authored polyline. Pure — returns
25
+ * the next state. Crosses multiple waypoints in one step, loops when configured, and
26
+ * reports `done` at the end of a non-looping path. No navmesh required (#52); feed it a
27
+ * navmesh route via `pathFromNav` for click-to-move (#51).
28
+ */
29
+ export declare function advancePathFollow(config: PathFollowConfig, state: PathFollowState, dt: number): PathFollowState;
@@ -0,0 +1,67 @@
1
+ /** Lift an A* / navmesh `[x, z]` polyline onto a `y` plane for a path-follower. */
2
+ export function pathFromNav(points, y = 0) {
3
+ return points.map((point) => [point[0], y, point[1]]);
4
+ }
5
+ export function createPathFollow(config) {
6
+ const first = config.waypoints[0];
7
+ const start = first ?? [0, 0, 0];
8
+ return {
9
+ position: start,
10
+ target: config.waypoints.length > 1 ? 1 : 0,
11
+ heading: 0,
12
+ done: config.waypoints.length <= 1,
13
+ distanceTravelled: 0,
14
+ };
15
+ }
16
+ const EPSILON = 1e-9;
17
+ /**
18
+ * Advance a path-follower by `speed * dt` along its authored polyline. Pure — returns
19
+ * the next state. Crosses multiple waypoints in one step, loops when configured, and
20
+ * reports `done` at the end of a non-looping path. No navmesh required (#52); feed it a
21
+ * navmesh route via `pathFromNav` for click-to-move (#51).
22
+ */
23
+ export function advancePathFollow(config, state, dt) {
24
+ const { waypoints, speed } = config;
25
+ const loop = config.loop ?? false;
26
+ const count = waypoints.length;
27
+ if (count <= 1 || state.done || dt <= 0 || speed <= 0)
28
+ return state;
29
+ let position = state.position;
30
+ let target = state.target;
31
+ let heading = state.heading;
32
+ let distanceTravelled = state.distanceTravelled;
33
+ let budget = speed * dt;
34
+ let done = false;
35
+ let guard = count * 4 + 4;
36
+ while (budget > EPSILON && !done && guard > 0) {
37
+ guard -= 1;
38
+ const dest = waypoints[target];
39
+ const dx = dest[0] - position[0];
40
+ const dy = dest[1] - position[1];
41
+ const dz = dest[2] - position[2];
42
+ const dist = Math.hypot(dx, dy, dz);
43
+ if (dist > EPSILON)
44
+ heading = Math.atan2(dx, dz);
45
+ if (dist <= budget) {
46
+ position = dest;
47
+ budget -= dist;
48
+ distanceTravelled += dist;
49
+ if (target >= count - 1) {
50
+ if (loop)
51
+ target = 0;
52
+ else
53
+ done = true;
54
+ }
55
+ else {
56
+ target += 1;
57
+ }
58
+ }
59
+ else {
60
+ const t = budget / dist;
61
+ position = [position[0] + dx * t, position[1] + dy * t, position[2] + dz * t];
62
+ distanceTravelled += budget;
63
+ budget = 0;
64
+ }
65
+ }
66
+ return { position, target, heading, done, distanceTravelled };
67
+ }
@@ -0,0 +1,49 @@
1
+ import type { PhysicsWorld } from "./physicsWorld.js";
2
+ import type { WaterSurface } from "../world/water.js";
3
+ import type { AxisInput } from "../input/axisInput.js";
4
+ export interface BuoyantBodyConfig {
5
+ body: number;
6
+ water: WaterSurface;
7
+ heading?: number;
8
+ hullPoints?: readonly (readonly [number, number])[];
9
+ hullDepth?: number;
10
+ buoyancy?: number;
11
+ verticalDrag?: number;
12
+ linearDrag?: number;
13
+ engineAccel?: number;
14
+ reverseAccel?: number;
15
+ topSpeed?: number;
16
+ turnRate?: number;
17
+ turnSpeedRef?: number;
18
+ }
19
+ /**
20
+ * Floats a {@link PhysicsWorld} body on a CPU {@link WaterSurface}: each hull sample point pushes the
21
+ * body up by its submerged depth (Archimedes, coarse), with vertical and horizontal water drag so the
22
+ * hull settles at the waterline and rides the Gerstner waves. Passing an {@link AxisInput} drives it as
23
+ * a boat — throttle thrusts along the heading, steer yaws, a keel bleeds sideways slip. Call
24
+ * `update(dt, time, input?)` before the shared `world.step(dt)`.
25
+ */
26
+ export declare class BuoyantBody {
27
+ heading: number;
28
+ private readonly world;
29
+ private readonly water;
30
+ private readonly body;
31
+ private readonly hull;
32
+ private readonly hullDepth;
33
+ private readonly buoyancy;
34
+ private readonly verticalDrag;
35
+ private readonly linearDrag;
36
+ private readonly engineAccel;
37
+ private readonly reverseAccel;
38
+ private readonly topSpeed;
39
+ private readonly turnRate;
40
+ private readonly turnSpeedRef;
41
+ private submergedFlag;
42
+ constructor(world: PhysicsWorld, config: BuoyantBodyConfig);
43
+ get position(): [number, number, number];
44
+ get forward(): [number, number];
45
+ get speed(): number;
46
+ get submerged(): boolean;
47
+ update(dt: number, time: number, input?: AxisInput): void;
48
+ }
49
+ export declare function createBuoyantBody(world: PhysicsWorld, config: BuoyantBodyConfig): BuoyantBody;
@@ -0,0 +1,112 @@
1
+ const DEFAULT_HULL = [
2
+ [-0.8, 1.6],
3
+ [0.8, 1.6],
4
+ [-0.8, -1.6],
5
+ [0.8, -1.6],
6
+ ];
7
+ /**
8
+ * Floats a {@link PhysicsWorld} body on a CPU {@link WaterSurface}: each hull sample point pushes the
9
+ * body up by its submerged depth (Archimedes, coarse), with vertical and horizontal water drag so the
10
+ * hull settles at the waterline and rides the Gerstner waves. Passing an {@link AxisInput} drives it as
11
+ * a boat — throttle thrusts along the heading, steer yaws, a keel bleeds sideways slip. Call
12
+ * `update(dt, time, input?)` before the shared `world.step(dt)`.
13
+ */
14
+ export class BuoyantBody {
15
+ heading;
16
+ world;
17
+ water;
18
+ body;
19
+ hull;
20
+ hullDepth;
21
+ buoyancy;
22
+ verticalDrag;
23
+ linearDrag;
24
+ engineAccel;
25
+ reverseAccel;
26
+ topSpeed;
27
+ turnRate;
28
+ turnSpeedRef;
29
+ submergedFlag = false;
30
+ constructor(world, config) {
31
+ this.world = world;
32
+ this.heading = config.heading ?? 0;
33
+ this.water = config.water;
34
+ this.body = config.body;
35
+ this.hull = config.hullPoints ?? DEFAULT_HULL;
36
+ this.hullDepth = config.hullDepth ?? 0.5;
37
+ this.buoyancy = config.buoyancy ?? 26;
38
+ this.verticalDrag = config.verticalDrag ?? 3;
39
+ this.linearDrag = config.linearDrag ?? 1.2;
40
+ this.engineAccel = config.engineAccel ?? 10;
41
+ this.reverseAccel = config.reverseAccel ?? 5;
42
+ this.topSpeed = config.topSpeed ?? 14;
43
+ this.turnRate = config.turnRate ?? 1.2;
44
+ this.turnSpeedRef = config.turnSpeedRef ?? 4;
45
+ }
46
+ get position() {
47
+ return [this.world.posX[this.body], this.world.posY[this.body], this.world.posZ[this.body]];
48
+ }
49
+ get forward() {
50
+ return [Math.sin(this.heading), Math.cos(this.heading)];
51
+ }
52
+ get speed() {
53
+ const [fx, fz] = this.forward;
54
+ return this.world.velX[this.body] * fx + this.world.velZ[this.body] * fz;
55
+ }
56
+ get submerged() {
57
+ return this.submergedFlag;
58
+ }
59
+ update(dt, time, input) {
60
+ const w = this.world;
61
+ const b = this.body;
62
+ const sin = Math.sin(this.heading);
63
+ const cos = Math.cos(this.heading);
64
+ let submergedSum = 0;
65
+ let submergedCount = 0;
66
+ for (const [lx, lz] of this.hull) {
67
+ const px = w.posX[b] + lx * cos + lz * sin;
68
+ const pz = w.posZ[b] - lx * sin + lz * cos;
69
+ const surface = this.water.height(px, pz, time);
70
+ const bottom = w.posY[b] - this.hullDepth;
71
+ const depth = surface - bottom;
72
+ if (depth > 0) {
73
+ submergedSum += depth;
74
+ submergedCount += 1;
75
+ }
76
+ }
77
+ if (submergedCount > 0) {
78
+ const avgDepth = submergedSum / this.hull.length;
79
+ w.velY[b] += avgDepth * this.buoyancy * dt;
80
+ w.velY[b] -= w.velY[b] * Math.min(1, this.verticalDrag * dt);
81
+ const drag = Math.max(0, 1 - this.linearDrag * dt);
82
+ w.velX[b] *= drag;
83
+ w.velZ[b] *= drag;
84
+ this.submergedFlag = true;
85
+ w.wake(b);
86
+ }
87
+ else {
88
+ this.submergedFlag = false;
89
+ }
90
+ if (input !== undefined && this.submergedFlag) {
91
+ const speed = this.speed;
92
+ const steerScale = Math.min(1, Math.abs(speed) / this.turnSpeedRef);
93
+ this.heading += input.steer * this.turnRate * steerScale * dt;
94
+ const [fx, fz] = this.forward;
95
+ let accel = 0;
96
+ if (input.throttle > 0 && speed < this.topSpeed)
97
+ accel += input.throttle * this.engineAccel;
98
+ if (input.brake > 0 && speed > -this.topSpeed)
99
+ accel -= input.brake * this.reverseAccel;
100
+ w.velX[b] += fx * accel * dt;
101
+ w.velZ[b] += fz * accel * dt;
102
+ const lateral = -w.velX[b] * fz + w.velZ[b] * fx;
103
+ const forwardSpeed = w.velX[b] * fx + w.velZ[b] * fz;
104
+ const keptLateral = lateral * Math.max(0, 1 - 2 * dt);
105
+ w.velX[b] = fx * forwardSpeed - fz * keptLateral;
106
+ w.velZ[b] = fz * forwardSpeed + fx * keptLateral;
107
+ }
108
+ }
109
+ }
110
+ export function createBuoyantBody(world, config) {
111
+ return new BuoyantBody(world, config);
112
+ }
@@ -0,0 +1,46 @@
1
+ import type { PhysicsWorld } from "./physicsWorld.js";
2
+ export interface CarryableConfig {
3
+ /** Spring stiffness pulling the held body toward the follow point. Default 60. */
4
+ followStiffness?: number;
5
+ /** Spring damping on the follow constraint. Default 12. */
6
+ followDamping?: number;
7
+ /** Clamp on the follow impulse so a heavy item lags instead of teleporting. Default 40. */
8
+ maxForce?: number;
9
+ /** Mass one owner hauls at full speed; heavier → encumbered / needs more owners. Default 8. */
10
+ carryCapacity?: number;
11
+ }
12
+ /**
13
+ * Movement multiplier (1 = unhindered, →0 = crushed) for a body of `mass` carried by `owners`.
14
+ * Pure — the HUD/movement kit reads it to slow a laden hauler (Lethal Company) and to gate items
15
+ * that need 2+ people (R.E.P.O.).
16
+ */
17
+ export declare function carrySpeedMultiplier(mass: number, carryCapacity: number, owners: number): number;
18
+ /**
19
+ * A grabbed physics object following a moving hold point through a spring constraint (the pick —
20
+ * a raycast — is the caller's/shell's job; core owns the constraint). Supports shared multi-owner
21
+ * carry (the follow point is the average of owners' hold points), an encumbrance read, and
22
+ * drop/throw. Reuses `PhysicsWorld.springJoint` to a world anchor moved each frame.
23
+ */
24
+ export declare class Carryable {
25
+ readonly body: number;
26
+ private readonly world;
27
+ private readonly stiffness;
28
+ private readonly damping;
29
+ private readonly maxForce;
30
+ private readonly capacity;
31
+ private readonly holds;
32
+ private joint;
33
+ constructor(world: PhysicsWorld, body: number, config?: CarryableConfig);
34
+ get held(): boolean;
35
+ get owners(): number;
36
+ private mass;
37
+ speedMultiplier(): number;
38
+ /** Add an owner grabbing at a hold point; the first owner creates the follow constraint. */
39
+ grab(ownerId: number, x: number, y: number, z: number): void;
40
+ setHoldPoint(ownerId: number, x: number, y: number, z: number): void;
41
+ release(ownerId: number): void;
42
+ /** Recompute the follow point (average of live hold points) and drive the constraint. */
43
+ update(): void;
44
+ drop(): void;
45
+ throw(vx: number, vy: number, vz: number): void;
46
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Movement multiplier (1 = unhindered, →0 = crushed) for a body of `mass` carried by `owners`.
3
+ * Pure — the HUD/movement kit reads it to slow a laden hauler (Lethal Company) and to gate items
4
+ * that need 2+ people (R.E.P.O.).
5
+ */
6
+ export function carrySpeedMultiplier(mass, carryCapacity, owners) {
7
+ if (owners <= 0)
8
+ return 0;
9
+ if (mass <= 0)
10
+ return 1;
11
+ const scale = (carryCapacity * owners) / mass;
12
+ return scale < 0 ? 0 : scale > 1 ? 1 : scale;
13
+ }
14
+ /**
15
+ * A grabbed physics object following a moving hold point through a spring constraint (the pick —
16
+ * a raycast — is the caller's/shell's job; core owns the constraint). Supports shared multi-owner
17
+ * carry (the follow point is the average of owners' hold points), an encumbrance read, and
18
+ * drop/throw. Reuses `PhysicsWorld.springJoint` to a world anchor moved each frame.
19
+ */
20
+ export class Carryable {
21
+ body;
22
+ world;
23
+ stiffness;
24
+ damping;
25
+ maxForce;
26
+ capacity;
27
+ holds = new Map();
28
+ joint = -1;
29
+ constructor(world, body, config = {}) {
30
+ this.world = world;
31
+ this.body = body;
32
+ this.stiffness = config.followStiffness ?? 60;
33
+ this.damping = config.followDamping ?? 12;
34
+ this.maxForce = config.maxForce ?? 40;
35
+ this.capacity = config.carryCapacity ?? 8;
36
+ }
37
+ get held() {
38
+ return this.holds.size > 0;
39
+ }
40
+ get owners() {
41
+ return this.holds.size;
42
+ }
43
+ mass() {
44
+ const im = this.world.invMass[this.body];
45
+ return im > 0 ? 1 / im : Number.POSITIVE_INFINITY;
46
+ }
47
+ speedMultiplier() {
48
+ return carrySpeedMultiplier(this.mass(), this.capacity, this.holds.size);
49
+ }
50
+ /** Add an owner grabbing at a hold point; the first owner creates the follow constraint. */
51
+ grab(ownerId, x, y, z) {
52
+ this.holds.set(ownerId, [x, y, z]);
53
+ if (this.joint < 0) {
54
+ this.joint = this.world.springJoint({
55
+ bodyA: this.body,
56
+ restLength: 0,
57
+ stiffness: this.stiffness,
58
+ damping: this.damping,
59
+ maxImpulse: this.maxForce,
60
+ anchorB: [x, y, z],
61
+ });
62
+ }
63
+ this.world.wake(this.body);
64
+ }
65
+ setHoldPoint(ownerId, x, y, z) {
66
+ const h = this.holds.get(ownerId);
67
+ if (h === undefined)
68
+ return;
69
+ h[0] = x;
70
+ h[1] = y;
71
+ h[2] = z;
72
+ }
73
+ release(ownerId) {
74
+ this.holds.delete(ownerId);
75
+ if (this.holds.size === 0 && this.joint >= 0) {
76
+ this.world.removeJoint(this.joint);
77
+ this.joint = -1;
78
+ }
79
+ }
80
+ /** Recompute the follow point (average of live hold points) and drive the constraint. */
81
+ update() {
82
+ if (this.joint < 0 || this.holds.size === 0)
83
+ return;
84
+ let tx = 0;
85
+ let ty = 0;
86
+ let tz = 0;
87
+ for (const h of this.holds.values()) {
88
+ tx += h[0];
89
+ ty += h[1];
90
+ tz += h[2];
91
+ }
92
+ const inv = 1 / this.holds.size;
93
+ this.world.setJointAnchor(this.joint, tx * inv, ty * inv, tz * inv);
94
+ this.world.wake(this.body);
95
+ }
96
+ drop() {
97
+ this.holds.clear();
98
+ if (this.joint >= 0) {
99
+ this.world.removeJoint(this.joint);
100
+ this.joint = -1;
101
+ }
102
+ }
103
+ throw(vx, vy, vz) {
104
+ this.drop();
105
+ this.world.velX[this.body] = vx;
106
+ this.world.velY[this.body] = vy;
107
+ this.world.velZ[this.body] = vz;
108
+ this.world.wake(this.body);
109
+ }
110
+ }