@jgengine/core 0.7.0 → 0.9.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 (329) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +2 -0
  3. package/dist/ai/flock.d.ts +36 -0
  4. package/dist/ai/flock.js +182 -0
  5. package/dist/ai/groupAssist.d.ts +19 -0
  6. package/dist/ai/groupAssist.js +59 -0
  7. package/dist/ai/laneSelect.d.ts +21 -0
  8. package/dist/ai/laneSelect.js +37 -0
  9. package/dist/ai/mobBrain.d.ts +68 -0
  10. package/dist/ai/mobBrain.js +115 -0
  11. package/dist/ai/spawnDirector.d.ts +5 -0
  12. package/dist/ai/spawnDirector.js +14 -2
  13. package/dist/ai/threat.d.ts +2 -0
  14. package/dist/ai/threat.js +34 -0
  15. package/dist/anim/easing.d.ts +14 -0
  16. package/dist/anim/easing.js +55 -0
  17. package/dist/behaviour/behaviour.d.ts +136 -0
  18. package/dist/behaviour/behaviour.js +388 -0
  19. package/dist/cards/cardPile.d.ts +1 -0
  20. package/dist/cards/cardPile.js +12 -3
  21. package/dist/cartridge/runtime.d.ts +15 -0
  22. package/dist/cartridge/runtime.js +556 -0
  23. package/dist/cartridge/spec.d.ts +188 -0
  24. package/dist/cartridge/spec.js +19 -0
  25. package/dist/cartridge/validate.d.ts +2 -0
  26. package/dist/cartridge/validate.js +78 -0
  27. package/dist/combat/abilityKit.d.ts +22 -2
  28. package/dist/combat/abilityKit.js +72 -13
  29. package/dist/combat/castRunner.d.ts +40 -0
  30. package/dist/combat/castRunner.js +56 -0
  31. package/dist/combat/comboPoints.d.ts +14 -0
  32. package/dist/combat/comboPoints.js +52 -0
  33. package/dist/combat/defensiveWindow.d.ts +1 -1
  34. package/dist/combat/defensiveWindow.js +5 -3
  35. package/dist/combat/effects.d.ts +1 -1
  36. package/dist/combat/effects.js +9 -6
  37. package/dist/combat/projectiles.d.ts +64 -6
  38. package/dist/combat/projectiles.js +232 -74
  39. package/dist/combat/resistance.d.ts +16 -4
  40. package/dist/combat/resistance.js +43 -7
  41. package/dist/combat/resourcePool.d.ts +20 -0
  42. package/dist/combat/resourcePool.js +55 -0
  43. package/dist/combat/shotOrigin.d.ts +40 -0
  44. package/dist/combat/shotOrigin.js +57 -0
  45. package/dist/combat/telegraph.d.ts +30 -1
  46. package/dist/combat/telegraph.js +61 -0
  47. package/dist/commands/commandRegistry.d.ts +1 -1
  48. package/dist/commands/commandRegistry.js +2 -1
  49. package/dist/crafting/recipe.js +4 -1
  50. package/dist/data/dataSource.d.ts +26 -0
  51. package/dist/data/dataSource.js +93 -0
  52. package/dist/data/devProxy.d.ts +11 -0
  53. package/dist/data/devProxy.js +38 -0
  54. package/dist/data/fetchJson.d.ts +20 -0
  55. package/dist/data/fetchJson.js +43 -0
  56. package/dist/data/jsonDataSource.d.ts +4 -0
  57. package/dist/data/jsonDataSource.js +6 -0
  58. package/dist/devtools/devtools.d.ts +205 -0
  59. package/dist/devtools/devtools.js +832 -0
  60. package/dist/devtools/transformTunables.d.ts +14 -0
  61. package/dist/devtools/transformTunables.js +38 -0
  62. package/dist/devtools/tunableSchema.d.ts +140 -0
  63. package/dist/devtools/tunableSchema.js +476 -0
  64. package/dist/faction/factions.d.ts +33 -0
  65. package/dist/faction/factions.js +76 -0
  66. package/dist/faction/reputation.d.ts +32 -0
  67. package/dist/faction/reputation.js +82 -0
  68. package/dist/game/chat.d.ts +68 -0
  69. package/dist/game/chat.js +173 -0
  70. package/dist/game/chatFilter.d.ts +15 -0
  71. package/dist/game/chatFilter.js +55 -0
  72. package/dist/game/controlGate.d.ts +4 -0
  73. package/dist/game/controlGate.js +7 -0
  74. package/dist/game/defineGame.d.ts +10 -2
  75. package/dist/game/defineGame.js +2 -1
  76. package/dist/game/events.d.ts +35 -0
  77. package/dist/game/gamePhase.d.ts +20 -0
  78. package/dist/game/gamePhase.js +21 -0
  79. package/dist/game/levelSequence.d.ts +44 -0
  80. package/dist/game/levelSequence.js +78 -0
  81. package/dist/game/lootTable.d.ts +10 -1
  82. package/dist/game/lootTable.js +27 -7
  83. package/dist/game/playableGame.d.ts +286 -6
  84. package/dist/game/playableGame.js +6 -1
  85. package/dist/game/progression.d.ts +1 -0
  86. package/dist/game/progression.js +23 -2
  87. package/dist/game/quest.d.ts +7 -3
  88. package/dist/game/quest.js +17 -8
  89. package/dist/game/race.d.ts +44 -1
  90. package/dist/game/race.js +116 -11
  91. package/dist/game/recordBook.d.ts +29 -0
  92. package/dist/game/recordBook.js +86 -0
  93. package/dist/game/social.d.ts +42 -0
  94. package/dist/game/social.js +122 -0
  95. package/dist/game/spawnPoints.d.ts +19 -0
  96. package/dist/game/spawnPoints.js +23 -0
  97. package/dist/game/talents.d.ts +47 -0
  98. package/dist/game/talents.js +140 -0
  99. package/dist/game/worldItem.d.ts +3 -0
  100. package/dist/game/worldItem.js +12 -6
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +1 -0
  103. package/dist/input/actionBindings.d.ts +10 -0
  104. package/dist/input/actionBindings.js +14 -0
  105. package/dist/input/axisInput.d.ts +26 -3
  106. package/dist/input/axisInput.js +44 -4
  107. package/dist/input/bindingOverrides.d.ts +25 -0
  108. package/dist/input/bindingOverrides.js +95 -0
  109. package/dist/input/gestureSurface.d.ts +31 -0
  110. package/dist/input/gestureSurface.js +120 -0
  111. package/dist/input/pointer.d.ts +45 -0
  112. package/dist/input/pointer.js +48 -0
  113. package/dist/input/pointerAxis.d.ts +30 -0
  114. package/dist/input/pointerAxis.js +31 -0
  115. package/dist/input/touchScheme.d.ts +89 -0
  116. package/dist/input/touchScheme.js +153 -0
  117. package/dist/item/affix.d.ts +1 -1
  118. package/dist/item/affix.js +1 -19
  119. package/dist/meta/changelog.d.ts +1 -1
  120. package/dist/meta/changelog.js +59 -1
  121. package/dist/movement/dash.d.ts +3 -0
  122. package/dist/movement/dash.js +19 -5
  123. package/dist/movement/glideModel.d.ts +44 -0
  124. package/dist/movement/glideModel.js +54 -0
  125. package/dist/movement/grappleSwing.d.ts +34 -0
  126. package/dist/movement/grappleSwing.js +56 -0
  127. package/dist/movement/leaderTrail.d.ts +29 -0
  128. package/dist/movement/leaderTrail.js +67 -0
  129. package/dist/movement/movementModel.d.ts +36 -3
  130. package/dist/movement/movementModel.js +81 -5
  131. package/dist/movement/steering.d.ts +27 -0
  132. package/dist/movement/steering.js +40 -0
  133. package/dist/movement/voxelController.d.ts +61 -0
  134. package/dist/movement/voxelController.js +229 -0
  135. package/dist/multiplayer/chatContract.d.ts +42 -0
  136. package/dist/multiplayer/chatContract.js +57 -0
  137. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  138. package/dist/multiplayer/feedWriteGate.js +12 -0
  139. package/dist/multiplayer/identity.d.ts +13 -0
  140. package/dist/multiplayer/identity.js +20 -0
  141. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  142. package/dist/multiplayer/poseSyncGate.js +13 -1
  143. package/dist/multiplayer/presenceModel.d.ts +17 -2
  144. package/dist/multiplayer/presenceModel.js +30 -4
  145. package/dist/multiplayer/voiceContract.d.ts +44 -0
  146. package/dist/multiplayer/voiceContract.js +108 -0
  147. package/dist/nav/corridors.d.ts +30 -0
  148. package/dist/nav/corridors.js +69 -0
  149. package/dist/nav/navConstrain.d.ts +14 -0
  150. package/dist/nav/navConstrain.js +26 -0
  151. package/dist/nav/navFromEnvironment.d.ts +11 -0
  152. package/dist/nav/navFromEnvironment.js +16 -0
  153. package/dist/nav/navGrid.d.ts +10 -0
  154. package/dist/nav/navGrid.js +48 -5
  155. package/dist/nav/pathFollow.d.ts +4 -2
  156. package/dist/nav/pathFollow.js +5 -3
  157. package/dist/nav/railGraph.d.ts +66 -0
  158. package/dist/nav/railGraph.js +129 -0
  159. package/dist/nav/timetable.d.ts +41 -0
  160. package/dist/nav/timetable.js +94 -0
  161. package/dist/physics/ballisticSweep.d.ts +18 -0
  162. package/dist/physics/ballisticSweep.js +49 -0
  163. package/dist/physics/buoyancy.d.ts +6 -0
  164. package/dist/physics/buoyancy.js +14 -0
  165. package/dist/physics/flowTube.d.ts +32 -0
  166. package/dist/physics/flowTube.js +64 -0
  167. package/dist/physics/forceVolume.d.ts +27 -0
  168. package/dist/physics/forceVolume.js +43 -5
  169. package/dist/physics/kinematicVehicle.d.ts +52 -0
  170. package/dist/physics/kinematicVehicle.js +81 -0
  171. package/dist/physics/physicsWorld.d.ts +54 -8
  172. package/dist/physics/physicsWorld.js +400 -79
  173. package/dist/physics/ragdoll.js +1 -1
  174. package/dist/physics/spatialGrid.d.ts +1 -1
  175. package/dist/physics/spatialGrid.js +5 -0
  176. package/dist/physics/vehicleBody.d.ts +6 -0
  177. package/dist/physics/vehicleBody.js +18 -18
  178. package/dist/puzzle/cellGrid.d.ts +26 -0
  179. package/dist/puzzle/cellGrid.js +110 -0
  180. package/dist/puzzle/fallingPiece.d.ts +24 -0
  181. package/dist/puzzle/fallingPiece.js +51 -0
  182. package/dist/random/nameGen.d.ts +18 -0
  183. package/dist/random/nameGen.js +35 -0
  184. package/dist/random/rng.d.ts +2 -0
  185. package/dist/random/rng.js +23 -0
  186. package/dist/random/seedLink.d.ts +5 -0
  187. package/dist/random/seedLink.js +35 -0
  188. package/dist/runtime/adapter.d.ts +35 -0
  189. package/dist/runtime/adapter.js +37 -1
  190. package/dist/runtime/cameraDirector.d.ts +15 -0
  191. package/dist/runtime/cameraDirector.js +31 -0
  192. package/dist/runtime/commandRunner.d.ts +2 -2
  193. package/dist/runtime/commandRunner.js +2 -2
  194. package/dist/runtime/gameContext.d.ts +65 -2
  195. package/dist/runtime/gameContext.js +280 -9
  196. package/dist/runtime/gameRuntime.d.ts +5 -1
  197. package/dist/runtime/gameRuntime.js +13 -12
  198. package/dist/runtime/hostPersistence.d.ts +30 -1
  199. package/dist/runtime/hostPersistence.js +70 -5
  200. package/dist/runtime/inputSnapshot.d.ts +12 -0
  201. package/dist/runtime/inputSnapshot.js +17 -0
  202. package/dist/runtime/motionIntents.d.ts +33 -0
  203. package/dist/runtime/motionIntents.js +43 -0
  204. package/dist/runtime/snapshot.d.ts +1 -0
  205. package/dist/runtime/snapshot.js +1 -1
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/runtime/visibility.d.ts +74 -0
  208. package/dist/runtime/visibility.js +127 -0
  209. package/dist/scene/colliders.d.ts +44 -0
  210. package/dist/scene/colliders.js +77 -0
  211. package/dist/scene/entityStore.d.ts +30 -5
  212. package/dist/scene/entityStore.js +83 -13
  213. package/dist/scene/movementSpeed.d.ts +35 -0
  214. package/dist/scene/movementSpeed.js +20 -0
  215. package/dist/scene/objectQuery.d.ts +29 -0
  216. package/dist/scene/objectQuery.js +140 -0
  217. package/dist/scene/objectStore.d.ts +13 -0
  218. package/dist/scene/objectStore.js +130 -6
  219. package/dist/scene/paintLayer.d.ts +15 -0
  220. package/dist/scene/paintLayer.js +53 -0
  221. package/dist/scene/sceneRaycast.d.ts +80 -0
  222. package/dist/scene/sceneRaycast.js +284 -0
  223. package/dist/scene/spatial.d.ts +17 -1
  224. package/dist/scene/spatial.js +140 -17
  225. package/dist/sensor/concealment.d.ts +21 -0
  226. package/dist/sensor/concealment.js +63 -0
  227. package/dist/sensor/freezeMonitor.d.ts +17 -0
  228. package/dist/sensor/freezeMonitor.js +35 -0
  229. package/dist/sensor/recordingBuffer.d.ts +6 -0
  230. package/dist/sensor/recordingBuffer.js +7 -0
  231. package/dist/sensor/replayLoop.d.ts +39 -0
  232. package/dist/sensor/replayLoop.js +64 -0
  233. package/dist/sensor/visionCone.d.ts +31 -0
  234. package/dist/sensor/visionCone.js +72 -0
  235. package/dist/session/extraction.d.ts +2 -2
  236. package/dist/session/extraction.js +13 -2
  237. package/dist/session/roles.d.ts +6 -0
  238. package/dist/session/roles.js +32 -0
  239. package/dist/session/roundState.d.ts +41 -17
  240. package/dist/session/roundState.js +67 -38
  241. package/dist/settings/settingsModel.d.ts +97 -0
  242. package/dist/settings/settingsModel.js +100 -0
  243. package/dist/stats/statModifiers.d.ts +5 -2
  244. package/dist/stats/statModifiers.js +32 -14
  245. package/dist/store/observableKeyedStore.js +8 -2
  246. package/dist/survival/decayMeter.d.ts +1 -1
  247. package/dist/survival/decayMeter.js +0 -1
  248. package/dist/tactics/fallingGrid.d.ts +62 -0
  249. package/dist/tactics/fallingGrid.js +158 -0
  250. package/dist/tactics/tacticalGrid.d.ts +11 -0
  251. package/dist/tactics/tacticalGrid.js +17 -0
  252. package/dist/time/beatClock.d.ts +18 -0
  253. package/dist/time/beatClock.js +24 -0
  254. package/dist/time/idleProgress.d.ts +29 -0
  255. package/dist/time/idleProgress.js +31 -0
  256. package/dist/time/serverTick.d.ts +7 -1
  257. package/dist/time/serverTick.js +27 -4
  258. package/dist/time/simClock.d.ts +25 -1
  259. package/dist/time/simClock.js +28 -3
  260. package/dist/time/stateSchedule.d.ts +57 -0
  261. package/dist/time/stateSchedule.js +121 -0
  262. package/dist/turn/commit.d.ts +2 -2
  263. package/dist/turn/commit.js +2 -2
  264. package/dist/turn/intent.d.ts +14 -0
  265. package/dist/turn/intent.js +29 -0
  266. package/dist/turn/turnLoop.d.ts +2 -0
  267. package/dist/turn/turnLoop.js +8 -1
  268. package/dist/ui/hudLayout.d.ts +58 -0
  269. package/dist/ui/hudLayout.js +207 -0
  270. package/dist/ui/hudScale.d.ts +44 -0
  271. package/dist/ui/hudScale.js +55 -0
  272. package/dist/visibility/assetStreaming.d.ts +66 -0
  273. package/dist/visibility/assetStreaming.js +201 -0
  274. package/dist/visibility/bounds.d.ts +66 -0
  275. package/dist/visibility/bounds.js +113 -0
  276. package/dist/visibility/camera.d.ts +21 -0
  277. package/dist/visibility/camera.js +3 -0
  278. package/dist/visibility/config.d.ts +21 -0
  279. package/dist/visibility/config.js +1 -0
  280. package/dist/visibility/diagnostics.d.ts +26 -0
  281. package/dist/visibility/diagnostics.js +36 -0
  282. package/dist/visibility/distance.d.ts +11 -0
  283. package/dist/visibility/distance.js +30 -0
  284. package/dist/visibility/frustum.d.ts +47 -0
  285. package/dist/visibility/frustum.js +186 -0
  286. package/dist/visibility/occlusion.d.ts +36 -0
  287. package/dist/visibility/occlusion.js +45 -0
  288. package/dist/visibility/settings.d.ts +75 -0
  289. package/dist/visibility/settings.js +49 -0
  290. package/dist/visibility/simulationCulling.d.ts +34 -0
  291. package/dist/visibility/simulationCulling.js +36 -0
  292. package/dist/visibility/spatialIndex.d.ts +45 -0
  293. package/dist/visibility/spatialIndex.js +198 -0
  294. package/dist/visibility/visibilitySystem.d.ts +90 -0
  295. package/dist/visibility/visibilitySystem.js +257 -0
  296. package/dist/world/buildings.d.ts +6 -0
  297. package/dist/world/buildings.js +152 -0
  298. package/dist/world/carve.d.ts +7 -2
  299. package/dist/world/carve.js +17 -7
  300. package/dist/world/cellStates.d.ts +45 -0
  301. package/dist/world/cellStates.js +103 -0
  302. package/dist/world/environmentSummary.d.ts +61 -0
  303. package/dist/world/environmentSummary.js +138 -0
  304. package/dist/world/features.d.ts +138 -9
  305. package/dist/world/features.js +73 -2
  306. package/dist/world/gridInstances.d.ts +12 -0
  307. package/dist/world/gridInstances.js +160 -0
  308. package/dist/world/lod.d.ts +17 -0
  309. package/dist/world/lod.js +66 -0
  310. package/dist/world/mapLayers.d.ts +60 -0
  311. package/dist/world/mapLayers.js +44 -0
  312. package/dist/world/massing.d.ts +45 -0
  313. package/dist/world/massing.js +581 -0
  314. package/dist/world/minimap.d.ts +13 -1
  315. package/dist/world/minimap.js +21 -2
  316. package/dist/world/placementController.d.ts +22 -1
  317. package/dist/world/placementController.js +56 -0
  318. package/dist/world/terrain.d.ts +62 -1
  319. package/dist/world/terrain.js +253 -9
  320. package/dist/world/voxelField.d.ts +54 -0
  321. package/dist/world/voxelField.js +241 -0
  322. package/dist/world/water.d.ts +7 -0
  323. package/dist/world/water.js +19 -4
  324. package/dist/world/weather.d.ts +3 -18
  325. package/dist/world/weather.js +4 -17
  326. package/dist/world/windZones.d.ts +47 -0
  327. package/dist/world/windZones.js +59 -0
  328. package/llms.txt +4983 -0
  329. package/package.json +14 -3
@@ -1,6 +1,17 @@
1
1
  import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
2
+ const CELL_SIZE = 1;
3
+ function cellIndexOf(value) {
4
+ return Math.floor(value / CELL_SIZE);
5
+ }
6
+ function cellKey(cx, cy, cz) {
7
+ return `${cx}:${cy}:${cz}`;
8
+ }
9
+ function cellKeyOf(position) {
10
+ return cellKey(cellIndexOf(position[0]), cellIndexOf(position[1]), cellIndexOf(position[2]));
11
+ }
2
12
  export function createObjectStore() {
3
13
  const store = createObservableKeyedStore();
14
+ const cellIndex = new Map();
4
15
  let nextCounter = 1;
5
16
  function generateId() {
6
17
  let id = `object-${nextCounter}`;
@@ -11,31 +22,64 @@ export function createObjectStore() {
11
22
  nextCounter += 1;
12
23
  return id;
13
24
  }
25
+ function indexAdd(instanceId, position) {
26
+ const key = cellKeyOf(position);
27
+ let bucket = cellIndex.get(key);
28
+ if (bucket === undefined) {
29
+ bucket = new Set();
30
+ cellIndex.set(key, bucket);
31
+ }
32
+ bucket.add(instanceId);
33
+ }
34
+ function indexRemove(instanceId, position) {
35
+ const key = cellKeyOf(position);
36
+ const bucket = cellIndex.get(key);
37
+ if (bucket === undefined)
38
+ return;
39
+ bucket.delete(instanceId);
40
+ if (bucket.size === 0)
41
+ cellIndex.delete(key);
42
+ }
14
43
  return {
15
44
  place(catalogId, x, y, z, options = {}) {
16
- if (options.instanceId !== undefined && store.has(options.instanceId)) {
17
- throw new Error(`Scene object id "${options.instanceId}" is already placed.`);
45
+ const existing = options.instanceId === undefined ? null : store.get(options.instanceId);
46
+ if (existing !== null && existing !== undefined) {
47
+ const onExisting = options.onExisting ?? "throw";
48
+ if (onExisting === "keep")
49
+ return options.instanceId;
50
+ if (onExisting === "throw")
51
+ throw new Error(`Scene object id "${options.instanceId}" is already placed.`);
52
+ indexRemove(options.instanceId, existing.position);
18
53
  }
19
54
  const instanceId = options.instanceId ?? generateId();
55
+ const position = [x, y, z];
20
56
  store.set(instanceId, {
21
57
  instanceId,
22
58
  catalogId,
23
- position: [x, y, z],
59
+ position,
24
60
  rotationY: options.rotation ?? 0,
25
61
  ...(options.parentSpace !== undefined ? { parentSpace: options.parentSpace } : {}),
62
+ ...(options.visual !== undefined ? { visual: options.visual } : {}),
26
63
  });
64
+ indexAdd(instanceId, position);
27
65
  return instanceId;
28
66
  },
29
67
  remove(instanceId) {
30
- const existed = store.has(instanceId);
68
+ const current = store.get(instanceId);
69
+ if (!current)
70
+ return false;
31
71
  store.delete(instanceId);
32
- return existed;
72
+ indexRemove(instanceId, current.position);
73
+ return true;
33
74
  },
34
75
  move(instanceId, x, y, z) {
35
76
  const current = store.get(instanceId);
36
77
  if (!current)
37
78
  return false;
38
- store.set(instanceId, { ...current, position: [x, y, z] });
79
+ const position = [x, y, z];
80
+ store.set(instanceId, { ...current, position });
81
+ indexRemove(instanceId, current.position);
82
+ indexAdd(instanceId, position);
39
83
  return true;
40
84
  },
41
85
  rotate(instanceId, rotationY) {
@@ -45,6 +89,20 @@ export function createObjectStore() {
45
89
  store.set(instanceId, { ...current, rotationY });
46
90
  return true;
47
91
  },
92
+ setVisual(instanceId, visual) {
93
+ const current = store.get(instanceId);
94
+ if (!current)
95
+ return false;
96
+ store.set(instanceId, {
97
+ instanceId: current.instanceId,
98
+ catalogId: current.catalogId,
99
+ position: current.position,
100
+ rotationY: current.rotationY,
101
+ ...(current.parentSpace !== undefined ? { parentSpace: current.parentSpace } : {}),
102
+ ...(visual !== undefined ? { visual } : {}),
103
+ });
104
+ return true;
105
+ },
48
106
  get(instanceId) {
49
107
  return store.get(instanceId) ?? null;
50
108
  },
@@ -54,10 +112,70 @@ export function createObjectStore() {
54
112
  return all;
55
113
  return all.filter((object) => object.parentSpace === filter.parentSpace);
56
114
  },
115
+ at(x, y, z, tolerance = 0) {
116
+ const minCx = cellIndexOf(x - tolerance);
117
+ const maxCx = cellIndexOf(x + tolerance);
118
+ const minCy = cellIndexOf(y - tolerance);
119
+ const maxCy = cellIndexOf(y + tolerance);
120
+ const minCz = cellIndexOf(z - tolerance);
121
+ const maxCz = cellIndexOf(z + tolerance);
122
+ const results = [];
123
+ for (let cx = minCx; cx <= maxCx; cx += 1) {
124
+ for (let cy = minCy; cy <= maxCy; cy += 1) {
125
+ for (let cz = minCz; cz <= maxCz; cz += 1) {
126
+ const bucket = cellIndex.get(cellKey(cx, cy, cz));
127
+ if (bucket === undefined)
128
+ continue;
129
+ for (const instanceId of bucket) {
130
+ const object = store.get(instanceId);
131
+ if (object === undefined)
132
+ continue;
133
+ const dx = object.position[0] - x;
134
+ const dy = object.position[1] - y;
135
+ const dz = object.position[2] - z;
136
+ if (Math.hypot(dx, dy, dz) <= tolerance)
137
+ results.push(object);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return results;
143
+ },
144
+ inBox(min, max) {
145
+ const hits = [];
146
+ const minCx = cellIndexOf(min[0]);
147
+ const maxCx = cellIndexOf(max[0]);
148
+ const minCy = cellIndexOf(min[1]);
149
+ const maxCy = cellIndexOf(max[1]);
150
+ const minCz = cellIndexOf(min[2]);
151
+ const maxCz = cellIndexOf(max[2]);
152
+ for (let cx = minCx; cx <= maxCx; cx += 1) {
153
+ for (let cy = minCy; cy <= maxCy; cy += 1) {
154
+ for (let cz = minCz; cz <= maxCz; cz += 1) {
155
+ const bucket = cellIndex.get(cellKey(cx, cy, cz));
156
+ if (bucket === undefined)
157
+ continue;
158
+ for (const instanceId of bucket) {
159
+ const object = store.get(instanceId);
160
+ if (object === undefined)
161
+ continue;
162
+ const [x, y, z] = object.position;
163
+ if (x >= min[0] && x <= max[0] &&
164
+ y >= min[1] && y <= max[1] &&
165
+ z >= min[2] && z <= max[2]) {
166
+ hits.push(object);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ return hits;
173
+ },
57
174
  clear() {
58
175
  for (const object of store.arraySnapshot()) {
59
176
  store.delete(object.instanceId);
60
177
  }
178
+ cellIndex.clear();
61
179
  },
62
180
  subscribe(listener) {
63
181
  return store.subscribe(listener);
@@ -67,3 +185,9 @@ export function createObjectStore() {
67
185
  },
68
186
  };
69
187
  }
188
+ export function objectVisualScale(visual) {
189
+ const scale = visual?.scale;
190
+ if (scale === undefined)
191
+ return [1, 1, 1];
192
+ return typeof scale === "number" ? [scale, scale, scale] : scale;
193
+ }
@@ -0,0 +1,15 @@
1
+ export interface PaintStroke {
2
+ u: number;
3
+ v: number;
4
+ radius: number;
5
+ color: string;
6
+ }
7
+ export interface PaintLayer {
8
+ paint(instanceId: string, stroke: PaintStroke): void;
9
+ strokes(instanceId: string): readonly PaintStroke[];
10
+ paintedIds(): readonly string[];
11
+ clear(instanceId?: string): void;
12
+ version(instanceId: string): number;
13
+ subscribe(listener: () => void): () => void;
14
+ }
15
+ export declare function createPaintLayer(): PaintLayer;
@@ -0,0 +1,53 @@
1
+ export function createPaintLayer() {
2
+ const strokesByInstance = new Map();
3
+ const versionByInstance = new Map();
4
+ const listeners = new Set();
5
+ function bumpVersion(instanceId) {
6
+ versionByInstance.set(instanceId, (versionByInstance.get(instanceId) ?? 0) + 1);
7
+ }
8
+ function notify() {
9
+ for (const listener of listeners)
10
+ listener();
11
+ }
12
+ return {
13
+ paint(instanceId, stroke) {
14
+ const list = strokesByInstance.get(instanceId);
15
+ if (list === undefined)
16
+ strokesByInstance.set(instanceId, [stroke]);
17
+ else
18
+ list.push(stroke);
19
+ bumpVersion(instanceId);
20
+ notify();
21
+ },
22
+ strokes(instanceId) {
23
+ return strokesByInstance.get(instanceId) ?? [];
24
+ },
25
+ paintedIds() {
26
+ return Array.from(strokesByInstance.keys());
27
+ },
28
+ clear(instanceId) {
29
+ if (instanceId === undefined) {
30
+ if (strokesByInstance.size === 0)
31
+ return;
32
+ for (const id of strokesByInstance.keys())
33
+ bumpVersion(id);
34
+ strokesByInstance.clear();
35
+ notify();
36
+ return;
37
+ }
38
+ if (!strokesByInstance.delete(instanceId))
39
+ return;
40
+ bumpVersion(instanceId);
41
+ notify();
42
+ },
43
+ version(instanceId) {
44
+ return versionByInstance.get(instanceId) ?? 0;
45
+ },
46
+ subscribe(listener) {
47
+ listeners.add(listener);
48
+ return () => {
49
+ listeners.delete(listener);
50
+ };
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,80 @@
1
+ import type { EntityPosition } from "./entityStore.js";
2
+ import { type ColliderPurpose, type EntityColliderSet } from "./colliders.js";
3
+ import type { SceneObject } from "./objectStore.js";
4
+ export type SceneRaycastTargetKind = "entity" | "object" | "terrain" | "wall";
5
+ export interface SceneRaycastHit {
6
+ targetKind: SceneRaycastTargetKind;
7
+ instanceId: string;
8
+ catalogId?: string;
9
+ colliderName: string;
10
+ purpose: ColliderPurpose;
11
+ damageEligible: boolean;
12
+ blocks: boolean;
13
+ distance: number;
14
+ point: EntityPosition;
15
+ normal: EntityPosition;
16
+ }
17
+ export interface SceneRaycastFilter {
18
+ entities?: boolean;
19
+ objects?: boolean;
20
+ terrain?: boolean;
21
+ walls?: boolean;
22
+ }
23
+ export interface SceneRaycastInput {
24
+ origin: EntityPosition;
25
+ direction: EntityPosition;
26
+ maxDistance: number;
27
+ excludeInstanceIds?: ReadonlySet<string> | readonly string[];
28
+ filter?: SceneRaycastFilter;
29
+ /** When set, only hits satisfying the predicate are kept (applied after geometry). */
30
+ accept?: (hit: SceneRaycastHit) => boolean;
31
+ }
32
+ export interface SceneEntityQuerySource {
33
+ list(): readonly {
34
+ id: string;
35
+ position: EntityPosition;
36
+ rotationY: number;
37
+ name?: string;
38
+ }[];
39
+ collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
40
+ inRadius?(center: EntityPosition, radius: number): readonly string[];
41
+ get?(instanceId: string): {
42
+ id: string;
43
+ position: EntityPosition;
44
+ rotationY: number;
45
+ name?: string;
46
+ } | null;
47
+ }
48
+ export interface SceneObjectQuerySource {
49
+ list(): readonly SceneObject[];
50
+ inBox?(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
51
+ collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
52
+ halfExtentsOf?(catalogId: string): EntityPosition | null | undefined;
53
+ }
54
+ export interface TerrainRaycastSource {
55
+ sampleHeight(x: number, z: number): number;
56
+ }
57
+ export interface WallSegment {
58
+ id: string;
59
+ /** Segment endpoints on the XZ plane; extruded by halfHeight along Y around yCenter. */
60
+ a: readonly [number, number];
61
+ b: readonly [number, number];
62
+ yCenter?: number;
63
+ halfHeight?: number;
64
+ thickness?: number;
65
+ }
66
+ export interface SceneRaycastDeps {
67
+ entities?: SceneEntityQuerySource;
68
+ objects?: SceneObjectQuerySource;
69
+ terrain?: TerrainRaycastSource;
70
+ walls?: readonly WallSegment[];
71
+ }
72
+ export interface SceneRaycastApi {
73
+ raycast(input: SceneRaycastInput): SceneRaycastHit | null;
74
+ raycastAll(input: SceneRaycastInput): SceneRaycastHit[];
75
+ }
76
+ /** First impact: nearest hit that blocks, or nearest hit if none block. */
77
+ export declare function firstImpact(hits: readonly SceneRaycastHit[]): SceneRaycastHit | null;
78
+ /** Hits up to and including the first blocking collider (damage hitboxes before a wall stay). */
79
+ export declare function hitsUntilBlocked(hits: readonly SceneRaycastHit[]): SceneRaycastHit[];
80
+ export declare function createSceneRaycast(deps: SceneRaycastDeps): SceneRaycastApi;
@@ -0,0 +1,284 @@
1
+ import { colliderBounds, colliderWorldCenter, defaultEntityColliders, defaultObjectColliders, resolveColliders, } from "./colliders.js";
2
+ import { intersectAabb, normalizeDirection } from "./objectQuery.js";
3
+ function excluded(exclude, id) {
4
+ if (exclude === undefined)
5
+ return false;
6
+ if (typeof exclude.has === "function") {
7
+ return exclude.has(id);
8
+ }
9
+ for (const value of exclude) {
10
+ if (value === id)
11
+ return true;
12
+ }
13
+ return false;
14
+ }
15
+ function enabled(filter, key) {
16
+ if (filter === undefined)
17
+ return true;
18
+ return filter[key] !== false;
19
+ }
20
+ function rayHitsSphere(origin, direction, center, radius, maxDistance) {
21
+ const ox = origin[0] - center[0];
22
+ const oy = origin[1] - center[1];
23
+ const oz = origin[2] - center[2];
24
+ const b = ox * direction[0] + oy * direction[1] + oz * direction[2];
25
+ const c = ox * ox + oy * oy + oz * oz - radius * radius;
26
+ const disc = b * b - c;
27
+ if (disc < 0)
28
+ return null;
29
+ const sqrt = Math.sqrt(disc);
30
+ let t = -b - sqrt;
31
+ if (t < 0)
32
+ t = -b + sqrt;
33
+ if (t < 0 || t > maxDistance)
34
+ return null;
35
+ const px = origin[0] + direction[0] * t - center[0];
36
+ const py = origin[1] + direction[1] * t - center[1];
37
+ const pz = origin[2] + direction[2] * t - center[2];
38
+ const len = Math.hypot(px, py, pz) || 1;
39
+ return { distance: t, normal: [px / len, py / len, pz / len] };
40
+ }
41
+ function rayHitsCollider(origin, direction, maxDistance, collider, position, rotationY) {
42
+ if (collider.shape.kind === "sphere") {
43
+ const center = colliderWorldCenter(collider, position, rotationY);
44
+ const hit = rayHitsSphere(origin, direction, center, collider.shape.radius, maxDistance);
45
+ if (hit === null)
46
+ return null;
47
+ return {
48
+ distance: hit.distance,
49
+ normal: hit.normal,
50
+ point: [
51
+ origin[0] + direction[0] * hit.distance,
52
+ origin[1] + direction[1] * hit.distance,
53
+ origin[2] + direction[2] * hit.distance,
54
+ ],
55
+ };
56
+ }
57
+ const bounds = colliderBounds(collider, position, rotationY);
58
+ const hit = intersectAabb(origin, direction, bounds.min, bounds.max, maxDistance);
59
+ if (hit === null)
60
+ return null;
61
+ return {
62
+ distance: hit.distance,
63
+ normal: hit.normal,
64
+ point: [
65
+ origin[0] + direction[0] * hit.distance,
66
+ origin[1] + direction[1] * hit.distance,
67
+ origin[2] + direction[2] * hit.distance,
68
+ ],
69
+ };
70
+ }
71
+ function pushColliderHits(hits, targetKind, instanceId, catalogId, colliders, position, rotationY, origin, direction, maxDistance) {
72
+ for (const collider of colliders) {
73
+ const hit = rayHitsCollider(origin, direction, maxDistance, collider, position, rotationY);
74
+ if (hit === null)
75
+ continue;
76
+ hits.push({
77
+ targetKind,
78
+ instanceId,
79
+ ...(catalogId !== undefined ? { catalogId } : {}),
80
+ colliderName: collider.name,
81
+ purpose: collider.purpose,
82
+ damageEligible: collider.damageEligible,
83
+ blocks: collider.blocks,
84
+ distance: hit.distance,
85
+ point: hit.point,
86
+ normal: hit.normal,
87
+ });
88
+ }
89
+ }
90
+ function wallAabb(wall) {
91
+ const thickness = wall.thickness ?? 0.25;
92
+ const halfHeight = wall.halfHeight ?? 1.5;
93
+ const yCenter = wall.yCenter ?? halfHeight;
94
+ const ax = wall.a[0];
95
+ const az = wall.a[1];
96
+ const bx = wall.b[0];
97
+ const bz = wall.b[1];
98
+ const dx = bx - ax;
99
+ const dz = bz - az;
100
+ const len = Math.hypot(dx, dz) || 1;
101
+ const nx = (-dz / len) * thickness;
102
+ const nz = (dx / len) * thickness;
103
+ const xs = [ax + nx, ax - nx, bx + nx, bx - nx];
104
+ const zs = [az + nz, az - nz, bz + nz, bz - nz];
105
+ const minX = Math.min(...xs);
106
+ const maxX = Math.max(...xs);
107
+ const minZ = Math.min(...zs);
108
+ const maxZ = Math.max(...zs);
109
+ return {
110
+ min: [minX, yCenter - halfHeight, minZ],
111
+ max: [maxX, yCenter + halfHeight, maxZ],
112
+ center: [(ax + bx) / 2, yCenter, (az + bz) / 2],
113
+ };
114
+ }
115
+ function rayHitsTerrain(origin, direction, maxDistance, terrain) {
116
+ const steps = Math.max(8, Math.ceil(maxDistance * 2));
117
+ const step = maxDistance / steps;
118
+ let prevAbove = null;
119
+ let prevT = 0;
120
+ for (let i = 0; i <= steps; i += 1) {
121
+ const t = Math.min(maxDistance, i * step);
122
+ const x = origin[0] + direction[0] * t;
123
+ const y = origin[1] + direction[1] * t;
124
+ const z = origin[2] + direction[2] * t;
125
+ const ground = terrain.sampleHeight(x, z);
126
+ const above = y >= ground;
127
+ if (prevAbove === true && !above) {
128
+ let lo = prevT;
129
+ let hi = t;
130
+ for (let refine = 0; refine < 8; refine += 1) {
131
+ const mid = (lo + hi) / 2;
132
+ const my = origin[1] + direction[1] * mid;
133
+ const mg = terrain.sampleHeight(origin[0] + direction[0] * mid, origin[2] + direction[2] * mid);
134
+ if (my >= mg)
135
+ lo = mid;
136
+ else
137
+ hi = mid;
138
+ }
139
+ const distance = hi;
140
+ const px = origin[0] + direction[0] * distance;
141
+ const py = origin[1] + direction[1] * distance;
142
+ const pz = origin[2] + direction[2] * distance;
143
+ return {
144
+ targetKind: "terrain",
145
+ instanceId: "terrain",
146
+ colliderName: "ground",
147
+ purpose: "physical",
148
+ damageEligible: false,
149
+ blocks: true,
150
+ distance,
151
+ point: [px, py, pz],
152
+ normal: [0, 1, 0],
153
+ };
154
+ }
155
+ prevAbove = above;
156
+ prevT = t;
157
+ }
158
+ return null;
159
+ }
160
+ function candidateEntityIds(entities, origin, maxDistance) {
161
+ if (entities.inRadius !== undefined && entities.get !== undefined) {
162
+ const ids = entities.inRadius(origin, maxDistance + 2);
163
+ const out = [];
164
+ for (const id of ids) {
165
+ const entity = entities.get(id);
166
+ if (entity !== null && entity !== undefined)
167
+ out.push(entity);
168
+ }
169
+ return out;
170
+ }
171
+ return entities.list();
172
+ }
173
+ function candidateObjects(objects, origin, direction, maxDistance) {
174
+ if (objects.inBox !== undefined) {
175
+ const end = [
176
+ origin[0] + direction[0] * maxDistance,
177
+ origin[1] + direction[1] * maxDistance,
178
+ origin[2] + direction[2] * maxDistance,
179
+ ];
180
+ const pad = 2;
181
+ const min = [
182
+ Math.min(origin[0], end[0]) - pad,
183
+ Math.min(origin[1], end[1]) - pad,
184
+ Math.min(origin[2], end[2]) - pad,
185
+ ];
186
+ const max = [
187
+ Math.max(origin[0], end[0]) + pad,
188
+ Math.max(origin[1], end[1]) + pad,
189
+ Math.max(origin[2], end[2]) + pad,
190
+ ];
191
+ return objects.inBox(min, max);
192
+ }
193
+ return objects.list();
194
+ }
195
+ function gatherHits(deps, input) {
196
+ const direction = normalizeDirection(input.direction);
197
+ if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
198
+ return [];
199
+ const hits = [];
200
+ const { origin, maxDistance } = input;
201
+ if (enabled(input.filter, "entities") && deps.entities !== undefined) {
202
+ for (const entity of candidateEntityIds(deps.entities, origin, maxDistance)) {
203
+ if (excluded(input.excludeInstanceIds, entity.id))
204
+ continue;
205
+ const set = deps.entities.collidersOf?.(entity.id) ?? defaultEntityColliders();
206
+ const colliders = resolveColliders(set);
207
+ pushColliderHits(hits, "entity", entity.id, entity.name, colliders.length > 0 ? colliders : resolveColliders(defaultEntityColliders()), entity.position, entity.rotationY, origin, direction, maxDistance);
208
+ }
209
+ }
210
+ if (enabled(input.filter, "objects") && deps.objects !== undefined) {
211
+ for (const object of candidateObjects(deps.objects, origin, direction, maxDistance)) {
212
+ if (excluded(input.excludeInstanceIds, object.instanceId))
213
+ continue;
214
+ const half = deps.objects.halfExtentsOf?.(object.catalogId) ?? undefined;
215
+ const set = deps.objects.collidersOf?.(object.instanceId) ??
216
+ defaultObjectColliders(half === null || half === undefined ? undefined : half);
217
+ const colliders = resolveColliders(set);
218
+ pushColliderHits(hits, "object", object.instanceId, object.catalogId, colliders, object.position, object.rotationY, origin, direction, maxDistance);
219
+ }
220
+ }
221
+ if (enabled(input.filter, "walls") && deps.walls !== undefined) {
222
+ for (const wall of deps.walls) {
223
+ if (excluded(input.excludeInstanceIds, wall.id))
224
+ continue;
225
+ const box = wallAabb(wall);
226
+ const hit = intersectAabb(origin, direction, box.min, box.max, maxDistance);
227
+ if (hit === null)
228
+ continue;
229
+ hits.push({
230
+ targetKind: "wall",
231
+ instanceId: wall.id,
232
+ colliderName: "wall",
233
+ purpose: "physical",
234
+ damageEligible: false,
235
+ blocks: true,
236
+ distance: hit.distance,
237
+ point: [
238
+ origin[0] + direction[0] * hit.distance,
239
+ origin[1] + direction[1] * hit.distance,
240
+ origin[2] + direction[2] * hit.distance,
241
+ ],
242
+ normal: hit.normal,
243
+ });
244
+ }
245
+ }
246
+ if (enabled(input.filter, "terrain") && deps.terrain !== undefined) {
247
+ const terrainHit = rayHitsTerrain(origin, direction, maxDistance, deps.terrain);
248
+ if (terrainHit !== null)
249
+ hits.push(terrainHit);
250
+ }
251
+ hits.sort((a, b) => a.distance - b.distance || a.instanceId.localeCompare(b.instanceId) || a.colliderName.localeCompare(b.colliderName));
252
+ if (input.accept !== undefined)
253
+ return hits.filter(input.accept);
254
+ return hits;
255
+ }
256
+ /** First impact: nearest hit that blocks, or nearest hit if none block. */
257
+ export function firstImpact(hits) {
258
+ for (const hit of hits) {
259
+ if (hit.blocks)
260
+ return hit;
261
+ }
262
+ return hits[0] ?? null;
263
+ }
264
+ /** Hits up to and including the first blocking collider (damage hitboxes before a wall stay). */
265
+ export function hitsUntilBlocked(hits) {
266
+ const out = [];
267
+ for (const hit of hits) {
268
+ out.push(hit);
269
+ if (hit.blocks)
270
+ break;
271
+ }
272
+ return out;
273
+ }
274
+ export function createSceneRaycast(deps) {
275
+ return {
276
+ raycast(input) {
277
+ const hits = gatherHits(deps, input);
278
+ return firstImpact(hits);
279
+ },
280
+ raycastAll(input) {
281
+ return gatherHits(deps, input);
282
+ },
283
+ };
284
+ }
@@ -18,10 +18,25 @@ export interface MoveTowardOptions {
18
18
  stopDistance?: number;
19
19
  dt: number;
20
20
  }
21
+ export interface SpatialGridOptions {
22
+ cellSize: number;
23
+ }
21
24
  export interface SpatialApiOptions {
22
25
  resolvePosition: (instanceId: string) => EntityPosition | undefined;
23
- candidates: () => string[];
26
+ candidates: () => readonly string[];
27
+ /**
28
+ * When omitted, `hasLineOfSight` always returns true for known entities (open-field bypass).
29
+ * Wire a wall/collision test here for combat LoS; `true` means the segment is blocked.
30
+ */
24
31
  occluder?: (from: EntityPosition, to: EntityPosition) => boolean;
32
+ /**
33
+ * Broadphase for `inRadius`/`queryArc`. Defaults to `{ cellSize: 8 }`. Pass `false` to force a
34
+ * linear scan. When enabled, the index rebuilds lazily on first use after `invalidate()` or when
35
+ * `getVersion()` advances — call `invalidate()` after moves unless a version source is wired.
36
+ */
37
+ grid?: SpatialGridOptions | false;
38
+ /** When this number changes between queries, the grid rebuilds without an explicit `invalidate()`. */
39
+ getVersion?: () => number;
25
40
  }
26
41
  export interface SpatialApi {
27
42
  distance(aInstanceId: string, bInstanceId: string): number | null;
@@ -29,6 +44,7 @@ export interface SpatialApi {
29
44
  hasLineOfSight(fromInstanceId: string, toInstanceId: string): boolean;
30
45
  queryArc(options: QueryArcOptions): string[];
31
46
  moveToward(instanceId: string, target: EntityPosition | string, options: MoveTowardOptions): EntityPosition | null;
47
+ invalidate(): void;
32
48
  }
33
49
  export declare function distanceBetween(a: EntityPosition, b: EntityPosition): number;
34
50
  export declare function createSpatialApi(options: SpatialApiOptions): SpatialApi;