@jgengine/core 0.6.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.
Files changed (330) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +2 -0
  3. package/dist/ai/crowd.d.ts +36 -0
  4. package/dist/ai/crowd.js +213 -0
  5. package/dist/ai/jobBoard.d.ts +49 -0
  6. package/dist/ai/jobBoard.js +155 -0
  7. package/dist/ai/spawnDirector.d.ts +61 -0
  8. package/dist/ai/spawnDirector.js +162 -0
  9. package/dist/ai/threat.d.ts +25 -0
  10. package/dist/ai/threat.js +67 -0
  11. package/dist/anim/easing.d.ts +14 -0
  12. package/dist/anim/easing.js +55 -0
  13. package/dist/audio/audioFalloff.d.ts +38 -0
  14. package/dist/audio/audioFalloff.js +32 -0
  15. package/dist/behaviour/behaviour.d.ts +136 -0
  16. package/dist/behaviour/behaviour.js +388 -0
  17. package/dist/board/laneBoard.d.ts +63 -0
  18. package/dist/board/laneBoard.js +114 -0
  19. package/dist/board/timelineBoard.d.ts +36 -0
  20. package/dist/board/timelineBoard.js +70 -0
  21. package/dist/cards/cardPile.d.ts +64 -0
  22. package/dist/cards/cardPile.js +175 -0
  23. package/dist/cards/modifierPipeline.d.ts +33 -0
  24. package/dist/cards/modifierPipeline.js +48 -0
  25. package/dist/combat/abilityKit.d.ts +47 -0
  26. package/dist/combat/abilityKit.js +139 -0
  27. package/dist/combat/animationState.d.ts +47 -0
  28. package/dist/combat/animationState.js +101 -0
  29. package/dist/combat/attackTags.d.ts +14 -0
  30. package/dist/combat/attackTags.js +26 -0
  31. package/dist/combat/breakMeters.d.ts +32 -0
  32. package/dist/combat/breakMeters.js +51 -0
  33. package/dist/combat/comboString.d.ts +35 -0
  34. package/dist/combat/comboString.js +59 -0
  35. package/dist/combat/death.d.ts +8 -0
  36. package/dist/combat/death.js +5 -2
  37. package/dist/combat/defensiveWindow.d.ts +34 -0
  38. package/dist/combat/defensiveWindow.js +66 -0
  39. package/dist/combat/downed.d.ts +35 -0
  40. package/dist/combat/downed.js +104 -0
  41. package/dist/combat/effects.d.ts +12 -1
  42. package/dist/combat/effects.js +31 -19
  43. package/dist/combat/hitReaction.d.ts +23 -0
  44. package/dist/combat/hitReaction.js +20 -0
  45. package/dist/combat/projectiles.d.ts +42 -5
  46. package/dist/combat/projectiles.js +98 -12
  47. package/dist/combat/resistance.d.ts +13 -0
  48. package/dist/combat/resistance.js +46 -0
  49. package/dist/combat/telegraph.d.ts +31 -0
  50. package/dist/combat/telegraph.js +51 -0
  51. package/dist/commands/commandRegistry.d.ts +1 -1
  52. package/dist/commands/commandRegistry.js +2 -1
  53. package/dist/crafting/crop.d.ts +59 -0
  54. package/dist/crafting/crop.js +181 -0
  55. package/dist/crafting/production.d.ts +64 -0
  56. package/dist/crafting/production.js +130 -0
  57. package/dist/crafting/recipe.d.ts +62 -0
  58. package/dist/crafting/recipe.js +101 -0
  59. package/dist/data/dataSource.d.ts +26 -0
  60. package/dist/data/dataSource.js +93 -0
  61. package/dist/data/devProxy.d.ts +11 -0
  62. package/dist/data/devProxy.js +38 -0
  63. package/dist/data/fetchJson.d.ts +20 -0
  64. package/dist/data/fetchJson.js +43 -0
  65. package/dist/data/jsonDataSource.d.ts +4 -0
  66. package/dist/data/jsonDataSource.js +6 -0
  67. package/dist/devtools/devtools.d.ts +157 -0
  68. package/dist/devtools/devtools.js +484 -0
  69. package/dist/devtools/transformTunables.d.ts +14 -0
  70. package/dist/devtools/transformTunables.js +38 -0
  71. package/dist/economy/sharedWallet.d.ts +28 -0
  72. package/dist/economy/sharedWallet.js +62 -0
  73. package/dist/economy/techTree.d.ts +40 -0
  74. package/dist/economy/techTree.js +66 -0
  75. package/dist/game/chat.d.ts +66 -0
  76. package/dist/game/chat.js +165 -0
  77. package/dist/game/cosmetics.d.ts +25 -0
  78. package/dist/game/cosmetics.js +51 -0
  79. package/dist/game/defineGame.d.ts +10 -2
  80. package/dist/game/defineGame.js +2 -1
  81. package/dist/game/events.d.ts +87 -0
  82. package/dist/game/levelSequence.d.ts +44 -0
  83. package/dist/game/levelSequence.js +78 -0
  84. package/dist/game/lootFilter.d.ts +33 -0
  85. package/dist/game/lootFilter.js +48 -0
  86. package/dist/game/ping.d.ts +67 -0
  87. package/dist/game/ping.js +86 -0
  88. package/dist/game/playableGame.d.ts +516 -8
  89. package/dist/game/playableGame.js +6 -1
  90. package/dist/game/progression.d.ts +1 -0
  91. package/dist/game/progression.js +23 -2
  92. package/dist/game/race.d.ts +100 -0
  93. package/dist/game/race.js +236 -0
  94. package/dist/game/runDraft.d.ts +34 -0
  95. package/dist/game/runDraft.js +97 -0
  96. package/dist/game/social.d.ts +69 -0
  97. package/dist/game/social.js +138 -0
  98. package/dist/game/spawnPoints.d.ts +19 -0
  99. package/dist/game/spawnPoints.js +23 -0
  100. package/dist/game/worldItem.d.ts +95 -0
  101. package/dist/game/worldItem.js +138 -0
  102. package/dist/input/actionBindings.d.ts +10 -0
  103. package/dist/input/actionBindings.js +14 -0
  104. package/dist/input/axisInput.d.ts +43 -0
  105. package/dist/input/axisInput.js +70 -0
  106. package/dist/input/gestureSurface.d.ts +31 -0
  107. package/dist/input/gestureSurface.js +120 -0
  108. package/dist/input/pointer.d.ts +69 -0
  109. package/dist/input/pointer.js +65 -0
  110. package/dist/input/touchScheme.d.ts +83 -0
  111. package/dist/input/touchScheme.js +115 -0
  112. package/dist/interaction/contextMenu.d.ts +30 -0
  113. package/dist/interaction/contextMenu.js +22 -0
  114. package/dist/interaction/qte.d.ts +20 -0
  115. package/dist/interaction/qte.js +24 -0
  116. package/dist/interaction/skillCheck.d.ts +20 -0
  117. package/dist/interaction/skillCheck.js +26 -0
  118. package/dist/inventory/shapedGrid.d.ts +56 -0
  119. package/dist/inventory/shapedGrid.js +176 -0
  120. package/dist/inventory/storageTier.d.ts +52 -0
  121. package/dist/inventory/storageTier.js +77 -0
  122. package/dist/item/affix.d.ts +54 -0
  123. package/dist/item/affix.js +115 -0
  124. package/dist/item/durability.d.ts +51 -0
  125. package/dist/item/durability.js +78 -0
  126. package/dist/item/modularItem.d.ts +46 -0
  127. package/dist/item/modularItem.js +75 -0
  128. package/dist/meta/changelog.d.ts +1 -1
  129. package/dist/meta/changelog.js +71 -1
  130. package/dist/movement/dash.d.ts +37 -0
  131. package/dist/movement/dash.js +65 -0
  132. package/dist/movement/movementModel.d.ts +36 -3
  133. package/dist/movement/movementModel.js +81 -5
  134. package/dist/movement/voxelController.d.ts +61 -0
  135. package/dist/movement/voxelController.js +229 -0
  136. package/dist/multiplayer/chatContract.d.ts +42 -0
  137. package/dist/multiplayer/chatContract.js +57 -0
  138. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  139. package/dist/multiplayer/combatSnapshot.js +89 -0
  140. package/dist/multiplayer/identity.d.ts +13 -0
  141. package/dist/multiplayer/identity.js +20 -0
  142. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  143. package/dist/multiplayer/lagCompensation.js +154 -0
  144. package/dist/multiplayer/matchmaking.d.ts +33 -0
  145. package/dist/multiplayer/matchmaking.js +60 -0
  146. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  147. package/dist/multiplayer/poseSyncGate.js +13 -1
  148. package/dist/multiplayer/presenceModel.d.ts +3 -0
  149. package/dist/multiplayer/presenceModel.js +15 -1
  150. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  151. package/dist/multiplayer/simultaneousCommit.js +53 -0
  152. package/dist/multiplayer/voiceContract.d.ts +44 -0
  153. package/dist/multiplayer/voiceContract.js +108 -0
  154. package/dist/nav/navConstrain.d.ts +14 -0
  155. package/dist/nav/navConstrain.js +26 -0
  156. package/dist/nav/navFromEnvironment.d.ts +11 -0
  157. package/dist/nav/navFromEnvironment.js +16 -0
  158. package/dist/nav/navGrid.d.ts +56 -0
  159. package/dist/nav/navGrid.js +265 -0
  160. package/dist/nav/pathFollow.d.ts +31 -0
  161. package/dist/nav/pathFollow.js +69 -0
  162. package/dist/physics/ballisticSweep.d.ts +18 -0
  163. package/dist/physics/ballisticSweep.js +49 -0
  164. package/dist/physics/buoyancy.d.ts +49 -0
  165. package/dist/physics/buoyancy.js +112 -0
  166. package/dist/physics/carryable.d.ts +46 -0
  167. package/dist/physics/carryable.js +110 -0
  168. package/dist/physics/damageZones.d.ts +56 -0
  169. package/dist/physics/damageZones.js +107 -0
  170. package/dist/physics/forceVolume.d.ts +48 -0
  171. package/dist/physics/forceVolume.js +119 -0
  172. package/dist/physics/physicsWorld.d.ts +124 -2
  173. package/dist/physics/physicsWorld.js +493 -41
  174. package/dist/physics/ragdoll.d.ts +49 -0
  175. package/dist/physics/ragdoll.js +103 -0
  176. package/dist/physics/spatialGrid.d.ts +41 -0
  177. package/dist/physics/spatialGrid.js +143 -0
  178. package/dist/physics/structure.d.ts +83 -0
  179. package/dist/physics/structure.js +0 -0
  180. package/dist/physics/traversal.d.ts +75 -0
  181. package/dist/physics/traversal.js +138 -0
  182. package/dist/physics/vehicleBody.d.ts +99 -0
  183. package/dist/physics/vehicleBody.js +233 -0
  184. package/dist/puzzle/cellGrid.d.ts +26 -0
  185. package/dist/puzzle/cellGrid.js +110 -0
  186. package/dist/puzzle/fallingPiece.d.ts +24 -0
  187. package/dist/puzzle/fallingPiece.js +51 -0
  188. package/dist/random/nameGen.d.ts +18 -0
  189. package/dist/random/nameGen.js +35 -0
  190. package/dist/random/rng.d.ts +2 -0
  191. package/dist/random/rng.js +23 -0
  192. package/dist/runtime/adapter.d.ts +35 -0
  193. package/dist/runtime/adapter.js +37 -1
  194. package/dist/runtime/cameraDirector.d.ts +10 -0
  195. package/dist/runtime/cameraDirector.js +25 -0
  196. package/dist/runtime/gameContext.d.ts +107 -3
  197. package/dist/runtime/gameContext.js +253 -7
  198. package/dist/runtime/hostPersistence.d.ts +39 -0
  199. package/dist/runtime/hostPersistence.js +54 -0
  200. package/dist/runtime/inputSnapshot.d.ts +7 -0
  201. package/dist/runtime/inputSnapshot.js +12 -0
  202. package/dist/runtime/motionIntents.d.ts +25 -0
  203. package/dist/runtime/motionIntents.js +25 -0
  204. package/dist/runtime/persistenceScope.d.ts +32 -0
  205. package/dist/runtime/persistenceScope.js +64 -0
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/scene/autoTarget.d.ts +15 -0
  208. package/dist/scene/autoTarget.js +54 -0
  209. package/dist/scene/behaviors.d.ts +13 -1
  210. package/dist/scene/behaviors.js +3 -0
  211. package/dist/scene/captureCheck.d.ts +7 -0
  212. package/dist/scene/captureCheck.js +11 -0
  213. package/dist/scene/entityStore.d.ts +9 -1
  214. package/dist/scene/entityStore.js +25 -2
  215. package/dist/scene/form.d.ts +40 -0
  216. package/dist/scene/form.js +56 -0
  217. package/dist/scene/mount.d.ts +62 -0
  218. package/dist/scene/mount.js +107 -0
  219. package/dist/scene/movementSpeed.d.ts +35 -0
  220. package/dist/scene/movementSpeed.js +20 -0
  221. package/dist/scene/objectQuery.d.ts +18 -0
  222. package/dist/scene/objectQuery.js +108 -0
  223. package/dist/scene/objectStore.d.ts +11 -0
  224. package/dist/scene/objectStore.js +109 -4
  225. package/dist/scene/paintLayer.d.ts +15 -0
  226. package/dist/scene/paintLayer.js +53 -0
  227. package/dist/scene/possession.d.ts +30 -0
  228. package/dist/scene/possession.js +51 -0
  229. package/dist/scene/roster.d.ts +25 -0
  230. package/dist/scene/roster.js +57 -0
  231. package/dist/scene/selection.d.ts +30 -0
  232. package/dist/scene/selection.js +38 -0
  233. package/dist/scene/spatial.d.ts +21 -0
  234. package/dist/scene/spatial.js +63 -1
  235. package/dist/scene/stationClaim.d.ts +43 -0
  236. package/dist/scene/stationClaim.js +109 -0
  237. package/dist/sensor/concealment.d.ts +21 -0
  238. package/dist/sensor/concealment.js +63 -0
  239. package/dist/sensor/freezeMonitor.d.ts +17 -0
  240. package/dist/sensor/freezeMonitor.js +35 -0
  241. package/dist/sensor/frustumSensor.d.ts +60 -0
  242. package/dist/sensor/frustumSensor.js +104 -0
  243. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  244. package/dist/sensor/hiddenStateProbe.js +40 -0
  245. package/dist/sensor/recordingBuffer.d.ts +29 -0
  246. package/dist/sensor/recordingBuffer.js +61 -0
  247. package/dist/sensor/revealQuery.d.ts +22 -0
  248. package/dist/sensor/revealQuery.js +36 -0
  249. package/dist/session/contestedChannel.d.ts +36 -0
  250. package/dist/session/contestedChannel.js +100 -0
  251. package/dist/session/extraction.d.ts +54 -0
  252. package/dist/session/extraction.js +108 -0
  253. package/dist/session/ring.d.ts +36 -0
  254. package/dist/session/ring.js +88 -0
  255. package/dist/session/roles.d.ts +6 -0
  256. package/dist/session/roles.js +32 -0
  257. package/dist/session/roundState.d.ts +79 -0
  258. package/dist/session/roundState.js +158 -0
  259. package/dist/stats/accumulatorMeter.d.ts +31 -0
  260. package/dist/stats/accumulatorMeter.js +81 -0
  261. package/dist/stats/eventMeter.d.ts +34 -0
  262. package/dist/stats/eventMeter.js +76 -0
  263. package/dist/stats/rollCheck.d.ts +15 -0
  264. package/dist/stats/rollCheck.js +15 -0
  265. package/dist/survival/decayMeter.d.ts +59 -0
  266. package/dist/survival/decayMeter.js +95 -0
  267. package/dist/survival/moodle.d.ts +48 -0
  268. package/dist/survival/moodle.js +91 -0
  269. package/dist/survival/regionHealth.d.ts +72 -0
  270. package/dist/survival/regionHealth.js +129 -0
  271. package/dist/tactics/fallingGrid.d.ts +62 -0
  272. package/dist/tactics/fallingGrid.js +158 -0
  273. package/dist/tactics/predictiveQuery.d.ts +34 -0
  274. package/dist/tactics/predictiveQuery.js +44 -0
  275. package/dist/tactics/snapshot.d.ts +18 -0
  276. package/dist/tactics/snapshot.js +65 -0
  277. package/dist/tactics/surface.d.ts +52 -0
  278. package/dist/tactics/surface.js +135 -0
  279. package/dist/tactics/tacticalGrid.d.ts +68 -0
  280. package/dist/tactics/tacticalGrid.js +261 -0
  281. package/dist/time/beatClock.d.ts +40 -0
  282. package/dist/time/beatClock.js +73 -0
  283. package/dist/time/simClock.d.ts +25 -1
  284. package/dist/time/simClock.js +28 -3
  285. package/dist/turn/commit.d.ts +27 -0
  286. package/dist/turn/commit.js +72 -0
  287. package/dist/turn/intent.d.ts +14 -0
  288. package/dist/turn/intent.js +29 -0
  289. package/dist/turn/turnLoop.d.ts +60 -0
  290. package/dist/turn/turnLoop.js +223 -0
  291. package/dist/world/buildPermissions.d.ts +47 -0
  292. package/dist/world/buildPermissions.js +134 -0
  293. package/dist/world/carve.d.ts +105 -0
  294. package/dist/world/carve.js +175 -0
  295. package/dist/world/connectors.d.ts +40 -0
  296. package/dist/world/connectors.js +67 -0
  297. package/dist/world/envField.d.ts +71 -0
  298. package/dist/world/envField.js +96 -0
  299. package/dist/world/environmentSummary.d.ts +53 -0
  300. package/dist/world/environmentSummary.js +129 -0
  301. package/dist/world/features.d.ts +87 -6
  302. package/dist/world/features.js +52 -2
  303. package/dist/world/fog.d.ts +40 -0
  304. package/dist/world/fog.js +137 -0
  305. package/dist/world/gridInstances.d.ts +13 -0
  306. package/dist/world/gridInstances.js +22 -0
  307. package/dist/world/markers.d.ts +52 -0
  308. package/dist/world/markers.js +96 -0
  309. package/dist/world/minimap.d.ts +42 -0
  310. package/dist/world/minimap.js +72 -0
  311. package/dist/world/placedStructureStore.d.ts +39 -0
  312. package/dist/world/placedStructureStore.js +95 -0
  313. package/dist/world/placementController.d.ts +69 -0
  314. package/dist/world/placementController.js +153 -0
  315. package/dist/world/realm.d.ts +61 -0
  316. package/dist/world/realm.js +66 -0
  317. package/dist/world/support.d.ts +27 -0
  318. package/dist/world/support.js +75 -0
  319. package/dist/world/terraform.d.ts +54 -0
  320. package/dist/world/terraform.js +186 -0
  321. package/dist/world/terrain.d.ts +23 -1
  322. package/dist/world/terrain.js +64 -1
  323. package/dist/world/voxelField.d.ts +54 -0
  324. package/dist/world/voxelField.js +241 -0
  325. package/dist/world/walls.d.ts +61 -0
  326. package/dist/world/walls.js +221 -0
  327. package/dist/world/weather.d.ts +92 -0
  328. package/dist/world/weather.js +169 -0
  329. package/llms.txt +4102 -0
  330. package/package.json +3 -2
@@ -14,11 +14,62 @@ function forwardXZ(aim) {
14
14
  }
15
15
  return [Math.sin(aim.yaw), Math.cos(aim.yaw)];
16
16
  }
17
+ function cellCoord(value, cellSize) {
18
+ return Math.floor(value / cellSize);
19
+ }
20
+ function gridCellKey(cx, cz) {
21
+ return `${cx}:${cz}`;
22
+ }
17
23
  export function createSpatialApi(options) {
18
- const { resolvePosition, candidates, occluder } = options;
24
+ const { resolvePosition, candidates, occluder, grid } = options;
25
+ const cellSize = grid?.cellSize;
26
+ let gridIndex = null;
27
+ let gridDirty = true;
19
28
  function resolveTarget(target) {
20
29
  return typeof target === "string" ? resolvePosition(target) : target;
21
30
  }
31
+ function ensureGrid() {
32
+ if (cellSize === undefined)
33
+ return null;
34
+ if (!gridDirty && gridIndex !== null)
35
+ return gridIndex;
36
+ const cells = new Map();
37
+ const indexed = new Set();
38
+ for (const instanceId of candidates()) {
39
+ const position = resolvePosition(instanceId);
40
+ if (position === undefined)
41
+ continue;
42
+ indexed.add(instanceId);
43
+ const key = gridCellKey(cellCoord(position[0], cellSize), cellCoord(position[2], cellSize));
44
+ let bucket = cells.get(key);
45
+ if (bucket === undefined) {
46
+ bucket = [];
47
+ cells.set(key, bucket);
48
+ }
49
+ bucket.push(instanceId);
50
+ }
51
+ gridIndex = { cells, indexed };
52
+ gridDirty = false;
53
+ return gridIndex;
54
+ }
55
+ function inRangeSet(index, centerX, centerZ, radius) {
56
+ const size = cellSize;
57
+ const cxMin = cellCoord(centerX - radius, size);
58
+ const cxMax = cellCoord(centerX + radius, size);
59
+ const czMin = cellCoord(centerZ - radius, size);
60
+ const czMax = cellCoord(centerZ + radius, size);
61
+ const near = new Set();
62
+ for (let cz = czMin; cz <= czMax; cz += 1) {
63
+ for (let cx = cxMin; cx <= cxMax; cx += 1) {
64
+ const bucket = index.cells.get(gridCellKey(cx, cz));
65
+ if (bucket === undefined)
66
+ continue;
67
+ for (const id of bucket)
68
+ near.add(id);
69
+ }
70
+ }
71
+ return near;
72
+ }
22
73
  return {
23
74
  distance(aInstanceId, bInstanceId) {
24
75
  const a = resolvePosition(aInstanceId);
@@ -32,11 +83,15 @@ export function createSpatialApi(options) {
32
83
  const centerPosition = resolveTarget(center);
33
84
  if (centerPosition === undefined)
34
85
  return [];
86
+ const index = ensureGrid();
87
+ const near = index === null ? null : inRangeSet(index, centerPosition[0], centerPosition[2], radius);
35
88
  return candidates().filter((instanceId) => {
36
89
  if (instanceId === centerId)
37
90
  return false;
38
91
  if (filter !== undefined && !filter(instanceId))
39
92
  return false;
93
+ if (near !== null && index.indexed.has(instanceId) && !near.has(instanceId))
94
+ return false;
40
95
  const position = resolvePosition(instanceId);
41
96
  return position !== undefined && distanceBetween(centerPosition, position) <= radius;
42
97
  });
@@ -58,9 +113,13 @@ export function createSpatialApi(options) {
58
113
  if (forward === null)
59
114
  return [];
60
115
  const minDot = Math.cos((halfAngleDeg * Math.PI) / 180);
116
+ const index = ensureGrid();
117
+ const near = index === null ? null : inRangeSet(index, origin[0], origin[2], radius);
61
118
  return candidates().filter((instanceId) => {
62
119
  if (instanceId === from)
63
120
  return false;
121
+ if (near !== null && index.indexed.has(instanceId) && !near.has(instanceId))
122
+ return false;
64
123
  const position = resolvePosition(instanceId);
65
124
  if (position === undefined)
66
125
  return false;
@@ -75,6 +134,9 @@ export function createSpatialApi(options) {
75
134
  return dot >= minDot;
76
135
  });
77
136
  },
137
+ invalidate() {
138
+ gridDirty = true;
139
+ },
78
140
  moveToward(instanceId, target, { speed, stopDistance = 0, dt }) {
79
141
  const current = resolvePosition(instanceId);
80
142
  const destination = resolveTarget(target);
@@ -0,0 +1,43 @@
1
+ import { MountController, type MountKit } from "./mount.js";
2
+ export interface Station {
3
+ id: string;
4
+ facet: string;
5
+ offset: readonly [number, number, number];
6
+ control?: boolean;
7
+ }
8
+ export interface SharedVehicleConfig {
9
+ id: string;
10
+ kit: MountKit;
11
+ stations: readonly Station[];
12
+ }
13
+ export type ClaimResult = {
14
+ ok: true;
15
+ station: Station;
16
+ } | {
17
+ ok: false;
18
+ reason: "unknown_vehicle" | "unknown_facet" | "station_taken" | "vehicle_full";
19
+ };
20
+ export declare class StationClaim {
21
+ private readonly controller;
22
+ private readonly vehicles;
23
+ constructor(controller?: MountController);
24
+ register(config: SharedVehicleConfig): void;
25
+ private resolveStation;
26
+ claim(playerId: string, vehicleId: string, facetOrStationId: string): ClaimResult;
27
+ release(playerId: string): string | null;
28
+ facetOf(playerId: string): string | null;
29
+ stationOf(playerId: string): Station | null;
30
+ controllerOf(vehicleId: string, facet: string): string | null;
31
+ crew(vehicleId: string): {
32
+ playerId: string;
33
+ stationId: string;
34
+ facet: string;
35
+ }[];
36
+ stationsOf(vehicleId: string): readonly Station[];
37
+ openFacets(vehicleId: string): string[];
38
+ driver(vehicleId: string): string | null;
39
+ cameraTarget(playerId: string): string;
40
+ driveTarget(playerId: string): string | null;
41
+ get mounts(): MountController;
42
+ }
43
+ export declare function createStationClaim(controller?: MountController): StationClaim;
@@ -0,0 +1,109 @@
1
+ import { createMountController, MountController } from "./mount.js";
2
+ export class StationClaim {
3
+ controller;
4
+ vehicles = new Map();
5
+ constructor(controller) {
6
+ this.controller = controller ?? createMountController();
7
+ }
8
+ register(config) {
9
+ const byStationId = new Map();
10
+ const facetToStation = new Map();
11
+ for (const station of config.stations) {
12
+ byStationId.set(station.id, station);
13
+ if (!facetToStation.has(station.facet))
14
+ facetToStation.set(station.facet, station.id);
15
+ }
16
+ this.vehicles.set(config.id, { config, byStationId, facetToStation });
17
+ this.controller.register({
18
+ id: config.id,
19
+ kit: config.kit,
20
+ seats: config.stations.map((station) => ({
21
+ id: station.id,
22
+ offset: station.offset,
23
+ control: station.control,
24
+ })),
25
+ });
26
+ }
27
+ resolveStation(vehicle, facetOrStationId) {
28
+ const direct = vehicle.byStationId.get(facetOrStationId);
29
+ if (direct !== undefined)
30
+ return direct;
31
+ const stationId = vehicle.facetToStation.get(facetOrStationId);
32
+ if (stationId === undefined)
33
+ return null;
34
+ return vehicle.byStationId.get(stationId) ?? null;
35
+ }
36
+ claim(playerId, vehicleId, facetOrStationId) {
37
+ const vehicle = this.vehicles.get(vehicleId);
38
+ if (vehicle === undefined)
39
+ return { ok: false, reason: "unknown_vehicle" };
40
+ const station = this.resolveStation(vehicle, facetOrStationId);
41
+ if (station === null)
42
+ return { ok: false, reason: "unknown_facet" };
43
+ const result = this.controller.mount(playerId, vehicleId, station.id);
44
+ if (!result.ok) {
45
+ return { ok: false, reason: result.reason === "seat_taken" ? "station_taken" : "vehicle_full" };
46
+ }
47
+ return { ok: true, station };
48
+ }
49
+ release(playerId) {
50
+ return this.controller.dismount(playerId);
51
+ }
52
+ facetOf(playerId) {
53
+ const ref = this.controller.seatOf(playerId);
54
+ if (ref === null)
55
+ return null;
56
+ return this.vehicles.get(ref.mountId)?.byStationId.get(ref.seatId)?.facet ?? null;
57
+ }
58
+ stationOf(playerId) {
59
+ const ref = this.controller.seatOf(playerId);
60
+ if (ref === null)
61
+ return null;
62
+ return this.vehicles.get(ref.mountId)?.byStationId.get(ref.seatId) ?? null;
63
+ }
64
+ controllerOf(vehicleId, facet) {
65
+ const vehicle = this.vehicles.get(vehicleId);
66
+ if (vehicle === undefined)
67
+ return null;
68
+ for (const occupant of this.controller.occupants(vehicleId)) {
69
+ if (vehicle.byStationId.get(occupant.seatId)?.facet === facet)
70
+ return occupant.riderId;
71
+ }
72
+ return null;
73
+ }
74
+ crew(vehicleId) {
75
+ const vehicle = this.vehicles.get(vehicleId);
76
+ if (vehicle === undefined)
77
+ return [];
78
+ return this.controller.occupants(vehicleId).map((occupant) => ({
79
+ playerId: occupant.riderId,
80
+ stationId: occupant.seatId,
81
+ facet: vehicle.byStationId.get(occupant.seatId)?.facet ?? "",
82
+ }));
83
+ }
84
+ stationsOf(vehicleId) {
85
+ return this.vehicles.get(vehicleId)?.config.stations ?? [];
86
+ }
87
+ openFacets(vehicleId) {
88
+ const vehicle = this.vehicles.get(vehicleId);
89
+ if (vehicle === undefined)
90
+ return [];
91
+ const taken = new Set(this.controller.occupants(vehicleId).map((o) => o.seatId));
92
+ return vehicle.config.stations.filter((s) => !taken.has(s.id)).map((s) => s.facet);
93
+ }
94
+ driver(vehicleId) {
95
+ return this.controller.driver(vehicleId);
96
+ }
97
+ cameraTarget(playerId) {
98
+ return this.controller.cameraTarget(playerId);
99
+ }
100
+ driveTarget(playerId) {
101
+ return this.controller.driveTarget(playerId);
102
+ }
103
+ get mounts() {
104
+ return this.controller;
105
+ }
106
+ }
107
+ export function createStationClaim(controller) {
108
+ return new StationClaim(controller);
109
+ }
@@ -0,0 +1,21 @@
1
+ export type ColorHex = string;
2
+ export declare function colorDistance(a: ColorHex, b: ColorHex): number;
3
+ export declare function concealmentScore(entityColors: readonly ColorHex[], backgroundColors: readonly ColorHex[]): number;
4
+ export interface ConcealmentTarget {
5
+ id: string;
6
+ entityColors: readonly ColorHex[];
7
+ backgroundColors: readonly ColorHex[];
8
+ }
9
+ export interface ConcealmentSample {
10
+ id: string;
11
+ score: number;
12
+ concealed: boolean;
13
+ dwellSeconds: number;
14
+ }
15
+ export interface ConcealmentSensor {
16
+ tick(targets: readonly ConcealmentTarget[], dt: number): ConcealmentSample[];
17
+ reset(id?: string): void;
18
+ }
19
+ export declare function createConcealmentSensor(config?: {
20
+ threshold?: number;
21
+ }): ConcealmentSensor;
@@ -0,0 +1,63 @@
1
+ function parseHex(hex) {
2
+ const match = /^#?([0-9a-fA-F]{6})$/.exec(hex.trim());
3
+ if (match === null)
4
+ return [0, 0, 0];
5
+ const value = parseInt(match[1], 16);
6
+ return [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];
7
+ }
8
+ function redmean(a, b) {
9
+ const rmean = (a[0] + b[0]) / 2;
10
+ const dr = a[0] - b[0];
11
+ const dg = a[1] - b[1];
12
+ const db = a[2] - b[2];
13
+ return Math.sqrt((2 + rmean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rmean) / 256) * db * db);
14
+ }
15
+ const REDMEAN_MAX = redmean([0, 0, 0], [255, 255, 255]);
16
+ export function colorDistance(a, b) {
17
+ const distance = redmean(parseHex(a), parseHex(b));
18
+ return REDMEAN_MAX === 0 ? 0 : Math.min(1, distance / REDMEAN_MAX);
19
+ }
20
+ export function concealmentScore(entityColors, backgroundColors) {
21
+ if (entityColors.length === 0 || backgroundColors.length === 0)
22
+ return 0;
23
+ let total = 0;
24
+ for (const entityColor of entityColors) {
25
+ let nearest = Number.POSITIVE_INFINITY;
26
+ for (const backgroundColor of backgroundColors) {
27
+ const distance = colorDistance(entityColor, backgroundColor);
28
+ if (distance < nearest)
29
+ nearest = distance;
30
+ }
31
+ total += nearest;
32
+ }
33
+ return 1 - total / entityColors.length;
34
+ }
35
+ export function createConcealmentSensor(config) {
36
+ const threshold = config?.threshold ?? 0.7;
37
+ const dwell = new Map();
38
+ return {
39
+ tick(targets, dt) {
40
+ const seen = new Set();
41
+ const samples = [];
42
+ for (const target of targets) {
43
+ seen.add(target.id);
44
+ const score = concealmentScore(target.entityColors, target.backgroundColors);
45
+ const concealed = score >= threshold;
46
+ const nextDwell = concealed ? (dwell.get(target.id) ?? 0) + dt : 0;
47
+ dwell.set(target.id, nextDwell);
48
+ samples.push({ id: target.id, score, concealed, dwellSeconds: nextDwell });
49
+ }
50
+ for (const id of dwell.keys()) {
51
+ if (!seen.has(id))
52
+ dwell.delete(id);
53
+ }
54
+ return samples;
55
+ },
56
+ reset(id) {
57
+ if (id === undefined)
58
+ dwell.clear();
59
+ else
60
+ dwell.delete(id);
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,17 @@
1
+ export interface FreezeSubject {
2
+ id: string;
3
+ groundSpeed: number;
4
+ }
5
+ export interface FreezeViolation {
6
+ id: string;
7
+ speed: number;
8
+ movedSeconds: number;
9
+ }
10
+ export interface FreezeMonitor {
11
+ tick(subjects: readonly FreezeSubject[], frozenIds: ReadonlySet<string>, dt: number): FreezeViolation[];
12
+ reset(id?: string): void;
13
+ }
14
+ export declare function createFreezeMonitor(config?: {
15
+ toleranceSpeed?: number;
16
+ graceSeconds?: number;
17
+ }): FreezeMonitor;
@@ -0,0 +1,35 @@
1
+ export function createFreezeMonitor(config) {
2
+ const toleranceSpeed = config?.toleranceSpeed ?? 0.05;
3
+ const graceSeconds = config?.graceSeconds ?? 0;
4
+ const moved = new Map();
5
+ return {
6
+ tick(subjects, frozenIds, dt) {
7
+ const seen = new Set();
8
+ const violations = [];
9
+ for (const subject of subjects) {
10
+ seen.add(subject.id);
11
+ const shouldHoldStill = frozenIds.has(subject.id) && subject.groundSpeed > toleranceSpeed;
12
+ if (!shouldHoldStill) {
13
+ moved.delete(subject.id);
14
+ continue;
15
+ }
16
+ const movedSeconds = (moved.get(subject.id) ?? 0) + dt;
17
+ moved.set(subject.id, movedSeconds);
18
+ if (movedSeconds > graceSeconds) {
19
+ violations.push({ id: subject.id, speed: subject.groundSpeed, movedSeconds });
20
+ }
21
+ }
22
+ for (const id of moved.keys()) {
23
+ if (!seen.has(id))
24
+ moved.delete(id);
25
+ }
26
+ return violations;
27
+ },
28
+ reset(id) {
29
+ if (id === undefined)
30
+ moved.clear();
31
+ else
32
+ moved.delete(id);
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,60 @@
1
+ import type { EntityPosition } from "../scene/entityStore.js";
2
+ export interface FrustumCamera {
3
+ position: EntityPosition;
4
+ lookAt: EntityPosition;
5
+ /** Vertical field of view in degrees. Default 55. */
6
+ fovDeg?: number;
7
+ /** Width / height. Default 16 / 9. */
8
+ aspect?: number;
9
+ near?: number;
10
+ far?: number;
11
+ }
12
+ export interface FrustumTarget {
13
+ id: string;
14
+ position: EntityPosition;
15
+ /** Subject radius in world units, used by the framing-size score. Default 0.5. */
16
+ radius?: number;
17
+ }
18
+ export interface FrustumProjection {
19
+ inView: boolean;
20
+ distance: number;
21
+ /** -1..1, 0 = screen center, negative = left/down. */
22
+ screenX: number;
23
+ screenY: number;
24
+ }
25
+ /**
26
+ * Project a world point into the camera's view. Pure geometry — no three.js —
27
+ * so a game can run "is this framed?" checks headlessly (photo-mode scoring,
28
+ * server-side kill-cam validation) as well as from the live render camera.
29
+ */
30
+ export declare function projectToView(camera: FrustumCamera, point: EntityPosition): FrustumProjection;
31
+ export interface FramingConfig {
32
+ /** World-unit distance from camera that reads as "ideally framed" size. Default 6. */
33
+ idealDistance?: number;
34
+ /** Weight of screen-center placement in the framing score. Default 0.6. */
35
+ centerWeight?: number;
36
+ /** Weight of subject size (distance-to-ideal) in the framing score. Default 0.4. */
37
+ sizeWeight?: number;
38
+ }
39
+ /** 0 (not framed) .. 1 (dead-center, ideal distance) framing quality for an in-view projection. */
40
+ export declare function framingScore(projection: FrustumProjection, config?: FramingConfig): number;
41
+ export interface FrustumSample {
42
+ id: string;
43
+ inView: boolean;
44
+ distance: number;
45
+ screenX: number;
46
+ screenY: number;
47
+ framing: number;
48
+ /** Seconds continuously in view, up to and including this tick; resets to 0 the instant the subject leaves view. */
49
+ dwellSeconds: number;
50
+ }
51
+ export interface FrustumSensor {
52
+ tick(camera: FrustumCamera, targets: readonly FrustumTarget[], dt: number): FrustumSample[];
53
+ reset(id?: string): void;
54
+ }
55
+ /**
56
+ * A view-frustum sensor on a held camera object: which entities are in frame,
57
+ * how well framed they are, and how long they've stayed on-screen (photo-mode
58
+ * "is this subject framed", Content Warning-style monster-filming scoring).
59
+ */
60
+ export declare function createFrustumSensor(config?: FramingConfig): FrustumSensor;
@@ -0,0 +1,104 @@
1
+ function subtract(a, b) {
2
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
3
+ }
4
+ function length(v) {
5
+ return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
6
+ }
7
+ function normalize(v) {
8
+ const len = length(v);
9
+ return len === 0 ? [0, 0, 0] : [v[0] / len, v[1] / len, v[2] / len];
10
+ }
11
+ function cross(a, b) {
12
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
13
+ }
14
+ function dot(a, b) {
15
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
16
+ }
17
+ const WORLD_UP = [0, 1, 0];
18
+ /**
19
+ * Project a world point into the camera's view. Pure geometry — no three.js —
20
+ * so a game can run "is this framed?" checks headlessly (photo-mode scoring,
21
+ * server-side kill-cam validation) as well as from the live render camera.
22
+ */
23
+ export function projectToView(camera, point) {
24
+ const forward = normalize(subtract(camera.lookAt, camera.position));
25
+ const relative = subtract(point, camera.position);
26
+ const distance = length(relative);
27
+ const depth = dot(relative, forward);
28
+ const near = camera.near ?? 0.1;
29
+ const far = camera.far ?? Number.POSITIVE_INFINITY;
30
+ if (forward[0] === 0 && forward[1] === 0 && forward[2] === 0) {
31
+ return { inView: false, distance, screenX: 0, screenY: 0 };
32
+ }
33
+ if (depth <= near || depth >= far) {
34
+ return { inView: false, distance, screenX: 0, screenY: 0 };
35
+ }
36
+ let right = normalize(cross(forward, WORLD_UP));
37
+ if (right[0] === 0 && right[1] === 0 && right[2] === 0) {
38
+ right = normalize(cross(forward, [1, 0, 0]));
39
+ }
40
+ const up = cross(right, forward);
41
+ const fovDeg = camera.fovDeg ?? 55;
42
+ const aspect = camera.aspect ?? 16 / 9;
43
+ const halfHeight = Math.tan((fovDeg * Math.PI) / 360);
44
+ const halfWidth = halfHeight * aspect;
45
+ const screenX = dot(relative, right) / (depth * halfWidth);
46
+ const screenY = dot(relative, up) / (depth * halfHeight);
47
+ const inView = Math.abs(screenX) <= 1 && Math.abs(screenY) <= 1;
48
+ return { inView, distance, screenX, screenY };
49
+ }
50
+ /** 0 (not framed) .. 1 (dead-center, ideal distance) framing quality for an in-view projection. */
51
+ export function framingScore(projection, config) {
52
+ if (!projection.inView)
53
+ return 0;
54
+ const idealDistance = config?.idealDistance ?? 6;
55
+ const centerWeight = config?.centerWeight ?? 0.6;
56
+ const sizeWeight = config?.sizeWeight ?? 0.4;
57
+ const radial = Math.min(1, Math.sqrt(projection.screenX * projection.screenX + projection.screenY * projection.screenY));
58
+ const centerScore = 1 - radial;
59
+ const sizeScore = idealDistance <= 0
60
+ ? 0
61
+ : Math.max(0, 1 - Math.abs(projection.distance - idealDistance) / idealDistance);
62
+ return Math.max(0, Math.min(1, centerWeight * centerScore + sizeWeight * sizeScore));
63
+ }
64
+ /**
65
+ * A view-frustum sensor on a held camera object: which entities are in frame,
66
+ * how well framed they are, and how long they've stayed on-screen (photo-mode
67
+ * "is this subject framed", Content Warning-style monster-filming scoring).
68
+ */
69
+ export function createFrustumSensor(config) {
70
+ const dwell = new Map();
71
+ return {
72
+ tick(camera, targets, dt) {
73
+ const seen = new Set();
74
+ const samples = [];
75
+ for (const target of targets) {
76
+ seen.add(target.id);
77
+ const projection = projectToView(camera, target.position);
78
+ const framing = framingScore(projection, config);
79
+ const nextDwell = projection.inView ? (dwell.get(target.id) ?? 0) + dt : 0;
80
+ dwell.set(target.id, nextDwell);
81
+ samples.push({
82
+ id: target.id,
83
+ inView: projection.inView,
84
+ distance: projection.distance,
85
+ screenX: projection.screenX,
86
+ screenY: projection.screenY,
87
+ framing,
88
+ dwellSeconds: nextDwell,
89
+ });
90
+ }
91
+ for (const id of dwell.keys()) {
92
+ if (!seen.has(id))
93
+ dwell.delete(id);
94
+ }
95
+ return samples;
96
+ },
97
+ reset(id) {
98
+ if (id === undefined)
99
+ dwell.clear();
100
+ else
101
+ dwell.delete(id);
102
+ },
103
+ };
104
+ }
@@ -0,0 +1,31 @@
1
+ import type { EntityPosition } from "../scene/entityStore.js";
2
+ export type HiddenStateValue = number | string | boolean;
3
+ export interface HiddenStateSource {
4
+ id: string;
5
+ position: EntityPosition;
6
+ variables: Readonly<Record<string, HiddenStateValue>>;
7
+ }
8
+ export interface SensorProbeOptions {
9
+ range: number;
10
+ variableId: string;
11
+ /** How reading `strength` falls off with distance. Default "linear". */
12
+ falloff?: "linear" | "none";
13
+ }
14
+ export interface SensorReading {
15
+ sourceId: string;
16
+ variableId: string;
17
+ value: HiddenStateValue;
18
+ /** 1 at the source, 0 at the edge of `range` ("none" falloff always reads 1 in range). */
19
+ strength: number;
20
+ distance: number;
21
+ }
22
+ /**
23
+ * A sensor verb: probe a hidden zone/entity state variable in range and surface
24
+ * a reading (EMF reader, spirit box, thermometer, geiger counter). Unlike
25
+ * `proximityPrompt` (a UI affordance), this reads world state the player has no
26
+ * other way to see — the strongest in-range reading wins, mirroring how a real
27
+ * handheld sensor needle settles on the loudest nearby source.
28
+ */
29
+ export declare function probeHiddenState(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): SensorReading | null;
30
+ /** All in-range readings for `variableId`, strongest first. */
31
+ export declare function probeHiddenStateAll(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): SensorReading[];
@@ -0,0 +1,40 @@
1
+ import { distanceBetween } from "../scene/spatial.js";
2
+ function strengthAt(distance, range, falloff) {
3
+ if (falloff === "none")
4
+ return 1;
5
+ if (range <= 0)
6
+ return 0;
7
+ return Math.max(0, 1 - distance / range);
8
+ }
9
+ /**
10
+ * A sensor verb: probe a hidden zone/entity state variable in range and surface
11
+ * a reading (EMF reader, spirit box, thermometer, geiger counter). Unlike
12
+ * `proximityPrompt` (a UI affordance), this reads world state the player has no
13
+ * other way to see — the strongest in-range reading wins, mirroring how a real
14
+ * handheld sensor needle settles on the loudest nearby source.
15
+ */
16
+ export function probeHiddenState(origin, sources, options) {
17
+ const readings = probeHiddenStateAll(origin, sources, options);
18
+ return readings.length === 0 ? null : readings[0];
19
+ }
20
+ /** All in-range readings for `variableId`, strongest first. */
21
+ export function probeHiddenStateAll(origin, sources, options) {
22
+ const { range, variableId, falloff = "linear" } = options;
23
+ const readings = [];
24
+ for (const source of sources) {
25
+ if (!(variableId in source.variables))
26
+ continue;
27
+ const distance = distanceBetween(origin, source.position);
28
+ if (distance > range)
29
+ continue;
30
+ readings.push({
31
+ sourceId: source.id,
32
+ variableId,
33
+ value: source.variables[variableId],
34
+ strength: strengthAt(distance, range, falloff),
35
+ distance,
36
+ });
37
+ }
38
+ readings.sort((a, b) => b.strength - a.strength);
39
+ return readings;
40
+ }
@@ -0,0 +1,29 @@
1
+ export interface RecordingFrame<T> {
2
+ /** Game-time seconds (from `ctx.time`), so pause/fast-forward scrub the recording consistently with the sim. */
3
+ t: number;
4
+ data: T;
5
+ }
6
+ export interface RecordingBufferOptions {
7
+ /** Evict frames older than `latestT - maxDurationSeconds`. Default unlimited. */
8
+ maxDurationSeconds?: number;
9
+ /** Hard cap on frame count, oldest evicted first. Default unlimited. */
10
+ maxFrames?: number;
11
+ }
12
+ export interface RecordingBuffer<T> {
13
+ append(t: number, data: T): void;
14
+ /** Nearest frame at or before `t`, or null if the buffer is empty or `t` precedes every frame. */
15
+ seek(t: number): RecordingFrame<T> | null;
16
+ /** Frames with `fromT <= t <= toT`, in recorded order. */
17
+ range(fromT: number, toT: number): RecordingFrame<T>[];
18
+ clear(): void;
19
+ duration(): number;
20
+ frames(): readonly RecordingFrame<T>[];
21
+ }
22
+ /**
23
+ * A session-recording buffer for replay / photo mode / kill-cam: append
24
+ * timestamped snapshots (poses, camera state, whatever `T` is) on game-time,
25
+ * then seek/scrub the recording independent of the live sim. Frames are
26
+ * expected in non-decreasing `t` order (one push per tick); `seek`/`range`
27
+ * binary-search on that assumption.
28
+ */
29
+ export declare function createRecordingBuffer<T>(options?: RecordingBufferOptions): RecordingBuffer<T>;