@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
@@ -0,0 +1,77 @@
1
+ export const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
2
+ export const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
3
+ export function defaultEntityColliders() {
4
+ return {
5
+ hitboxes: [
6
+ {
7
+ name: "body",
8
+ purpose: "damage",
9
+ shape: { kind: "sphere", radius: DEFAULT_ENTITY_HIT_RADIUS },
10
+ damageEligible: true,
11
+ blocks: false,
12
+ },
13
+ ],
14
+ };
15
+ }
16
+ export function defaultObjectColliders(halfExtents = DEFAULT_OBJECT_HALF_EXTENTS) {
17
+ return {
18
+ body: {
19
+ name: "body",
20
+ purpose: "physical",
21
+ shape: { kind: "aabb", halfExtents },
22
+ damageEligible: false,
23
+ blocks: true,
24
+ },
25
+ };
26
+ }
27
+ export function resolveColliders(set) {
28
+ if (set === null || set === undefined)
29
+ return [];
30
+ const out = [];
31
+ if (set.body !== undefined)
32
+ out.push(resolveOne(set.body));
33
+ if (set.hitboxes !== undefined) {
34
+ for (const hitbox of set.hitboxes)
35
+ out.push(resolveOne(hitbox));
36
+ }
37
+ return out;
38
+ }
39
+ function resolveOne(def) {
40
+ const damageEligible = def.damageEligible ?? def.purpose === "damage";
41
+ const blocks = def.blocks ?? def.purpose === "physical";
42
+ return {
43
+ name: def.name,
44
+ purpose: def.purpose,
45
+ shape: def.shape,
46
+ damageEligible,
47
+ blocks,
48
+ };
49
+ }
50
+ export function worldOffset(local, position, rotationY) {
51
+ if (local === undefined)
52
+ return position;
53
+ const cos = Math.cos(rotationY);
54
+ const sin = Math.sin(rotationY);
55
+ const lx = local[0];
56
+ const ly = local[1];
57
+ const lz = local[2];
58
+ return [position[0] + lx * cos + lz * sin, position[1] + ly, position[2] - lx * sin + lz * cos];
59
+ }
60
+ export function colliderWorldCenter(collider, position, rotationY) {
61
+ return worldOffset(collider.shape.offset, position, rotationY);
62
+ }
63
+ export function colliderBounds(collider, position, rotationY) {
64
+ const center = colliderWorldCenter(collider, position, rotationY);
65
+ if (collider.shape.kind === "sphere") {
66
+ const r = collider.shape.radius;
67
+ return {
68
+ min: [center[0] - r, center[1] - r, center[2] - r],
69
+ max: [center[0] + r, center[1] + r, center[2] + r],
70
+ };
71
+ }
72
+ const h = collider.shape.halfExtents;
73
+ return {
74
+ min: [center[0] - h[0], center[1] - h[1], center[2] - h[2]],
75
+ max: [center[0] + h[0], center[1] + h[1], center[2] + h[2]],
76
+ };
77
+ }
@@ -2,6 +2,7 @@ import type { BehaviorDescriptor } from "./behaviors.js";
2
2
  export type EntityRole = "player" | "npc" | "prop";
3
3
  export interface EntityMovement {
4
4
  walkSpeed?: number;
5
+ frozen?: boolean;
5
6
  }
6
7
  export type EntityPosition = readonly [number, number, number];
7
8
  export type SpawnPositionInput = EntityPosition | {
@@ -9,7 +10,7 @@ export type SpawnPositionInput = EntityPosition | {
9
10
  y: number;
10
11
  z: number;
11
12
  };
12
- export interface SceneEntity<TMeta = undefined> {
13
+ export interface SceneEntity<TMeta = unknown> {
13
14
  id: string;
14
15
  name: string;
15
16
  position: EntityPosition;
@@ -21,11 +22,13 @@ export interface SceneEntity<TMeta = undefined> {
21
22
  role: EntityRole;
22
23
  movement: EntityMovement;
23
24
  behaviors: readonly BehaviorDescriptor[];
25
+ /** Game-defined per-instance data set at `spawn`/`update` and carried to `renderEntity`/queries — narrow with a cast or type guard on read (#286.1); never encode state in the id/name string. */
24
26
  meta: TMeta;
25
27
  }
26
28
  /** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */
27
29
  export declare function groundSpeed(entity: SceneEntity<unknown>): number;
28
- export interface SpawnOptions<TMeta = undefined> {
30
+ export declare function movedWhileFrozen(entity: SceneEntity<unknown>, threshold?: number): boolean;
31
+ export interface SpawnOptions<TMeta = unknown> {
29
32
  id?: string;
30
33
  position?: SpawnPositionInput;
31
34
  rotationY?: number;
@@ -35,6 +38,8 @@ export interface SpawnOptions<TMeta = undefined> {
35
38
  movement?: EntityMovement;
36
39
  behaviors?: readonly BehaviorDescriptor[];
37
40
  meta?: TMeta;
41
+ /** When `id` is already spawned: `"throw"` (default), `"replace"` (fresh spawn over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
42
+ onExisting?: "throw" | "replace" | "keep";
38
43
  }
39
44
  export interface EntityPose {
40
45
  position: SpawnPositionInput;
@@ -44,15 +49,35 @@ export interface EntityPose {
44
49
  /** Seconds since the previous pose; when > 0 the store derives `velocity` from the position delta. Omit for teleports (velocity unchanged). */
45
50
  dt?: number;
46
51
  }
47
- export interface EntityStore<TMeta = undefined> {
52
+ export interface SpawnPose {
53
+ position: EntityPosition;
54
+ rotationY: number;
55
+ }
56
+ export interface PoseConstraintFrame {
57
+ entityId: string;
58
+ current: EntityPosition;
59
+ next: EntityPosition;
60
+ dt?: number;
61
+ }
62
+ /** Return a replacement position to constrain the step, or nothing to accept it. */
63
+ export type PoseConstraint = (frame: PoseConstraintFrame) => readonly [number, number, number] | undefined | void;
64
+ export type EntityUpdatePatch<TMeta = unknown> = Partial<Pick<SceneEntity<TMeta>, "name" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">> & {
65
+ /** Accepts the same friendly shapes as `spawn`/`setPose` (#286.13). Raw patch semantics — velocity is not derived; use `setPose` with `dt` for that. */
66
+ position?: SpawnPositionInput;
67
+ };
68
+ export interface EntityStore<TMeta = unknown> {
48
69
  spawn(name: string, options?: SpawnOptions<TMeta>): string;
49
70
  despawn(id: string): boolean;
50
- update(id: string, patch: Partial<Pick<SceneEntity<TMeta>, "name" | "position" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">>): boolean;
71
+ update(id: string, patch: EntityUpdatePatch<TMeta>): boolean;
51
72
  setPose(id: string, pose: EntityPose): boolean;
73
+ /** Register a constraint applied inside every `setPose` for this entity — the self-driven sibling of the shell's `beforeCommit` (#282.9): nav clamps, corridor walls, arena bounds without wrapping every call site. `null` clears; despawn clears automatically. */
74
+ setPoseConstraint(id: string, constraint: PoseConstraint | null): void;
52
75
  get(id: string): SceneEntity<TMeta> | null;
53
76
  list(): readonly SceneEntity<TMeta>[];
54
77
  clear(): void;
55
78
  subscribe(listener: () => void): () => void;
56
79
  snapshot(): readonly SceneEntity<TMeta>[];
80
+ spawnPoseOf(id: string): SpawnPose | null;
81
+ resetToSpawn(id: string): boolean;
57
82
  }
58
- export declare function createEntityStore<TMeta = undefined>(): EntityStore<TMeta>;
83
+ export declare function createEntityStore<TMeta = unknown>(): EntityStore<TMeta>;
@@ -3,6 +3,13 @@ import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
3
3
  export function groundSpeed(entity) {
4
4
  return Math.hypot(entity.velocity[0], entity.velocity[2]);
5
5
  }
6
+ const DEFAULT_FROZEN_MOVE_THRESHOLD = 1e-6;
7
+ export function movedWhileFrozen(entity, threshold = DEFAULT_FROZEN_MOVE_THRESHOLD) {
8
+ if (entity.movement.frozen !== true)
9
+ return false;
10
+ const speed = Math.hypot(entity.velocity[0], entity.velocity[1], entity.velocity[2]);
11
+ return speed > threshold;
12
+ }
6
13
  function toEntityPosition(position) {
7
14
  if (position === undefined)
8
15
  return [0, 0, 0];
@@ -12,6 +19,8 @@ function toEntityPosition(position) {
12
19
  }
13
20
  export function createEntityStore() {
14
21
  const store = createObservableKeyedStore();
22
+ const spawnPoses = new Map();
23
+ const constraints = new Map();
15
24
  let nextCounter = 1;
16
25
  function generateId() {
17
26
  let id = `entity-${nextCounter}`;
@@ -25,14 +34,20 @@ export function createEntityStore() {
25
34
  return {
26
35
  spawn(name, options = {}) {
27
36
  if (options.id !== undefined && store.has(options.id)) {
28
- throw new Error(`Scene entity id "${options.id}" is already spawned.`);
37
+ const onExisting = options.onExisting ?? "throw";
38
+ if (onExisting === "keep")
39
+ return options.id;
40
+ if (onExisting === "throw")
41
+ throw new Error(`Scene entity id "${options.id}" is already spawned.`);
29
42
  }
30
43
  const id = options.id ?? generateId();
44
+ const position = toEntityPosition(options.position);
45
+ const rotationY = options.rotationY ?? 0;
31
46
  store.set(id, {
32
47
  id,
33
48
  name,
34
- position: toEntityPosition(options.position),
35
- rotationY: options.rotationY ?? 0,
49
+ position,
50
+ rotationY,
36
51
  rotationX: options.rotationX ?? 0,
37
52
  rotationZ: options.rotationZ ?? 0,
38
53
  velocity: [0, 0, 0],
@@ -41,25 +56,64 @@ export function createEntityStore() {
41
56
  behaviors: options.behaviors ?? [],
42
57
  meta: options.meta,
43
58
  });
59
+ spawnPoses.set(id, { position, rotationY });
44
60
  return id;
45
61
  },
46
62
  despawn(id) {
47
63
  const existed = store.has(id);
48
64
  store.delete(id);
65
+ spawnPoses.delete(id);
66
+ constraints.delete(id);
49
67
  return existed;
50
68
  },
51
69
  update(id, patch) {
52
70
  const current = store.get(id);
53
71
  if (!current)
54
72
  return false;
55
- store.set(id, { ...current, ...patch });
73
+ if (patch.position !== undefined)
74
+ current.position = toEntityPosition(patch.position);
75
+ if (patch.name !== undefined)
76
+ current.name = patch.name;
77
+ if (patch.rotationY !== undefined)
78
+ current.rotationY = patch.rotationY;
79
+ if (patch.rotationX !== undefined)
80
+ current.rotationX = patch.rotationX;
81
+ if (patch.rotationZ !== undefined)
82
+ current.rotationZ = patch.rotationZ;
83
+ if (patch.role !== undefined)
84
+ current.role = patch.role;
85
+ if (patch.movement !== undefined)
86
+ current.movement = patch.movement;
87
+ if (patch.behaviors !== undefined)
88
+ current.behaviors = patch.behaviors;
89
+ if (patch.meta !== undefined)
90
+ current.meta = patch.meta;
91
+ store.set(id, current);
56
92
  return true;
57
93
  },
94
+ setPoseConstraint(id, constraint) {
95
+ if (constraint === null)
96
+ constraints.delete(id);
97
+ else
98
+ constraints.set(id, constraint);
99
+ },
58
100
  setPose(id, pose) {
59
101
  const current = store.get(id);
60
102
  if (!current)
61
103
  return false;
62
- const position = toEntityPosition(pose.position);
104
+ let position = toEntityPosition(pose.position);
105
+ const constraint = constraints.get(id);
106
+ if (constraint !== undefined) {
107
+ const frame = {
108
+ entityId: id,
109
+ current: current.position,
110
+ next: position,
111
+ ...(pose.dt === undefined ? {} : { dt: pose.dt }),
112
+ };
113
+ const replacement = constraint(frame);
114
+ if (replacement !== undefined)
115
+ position = toEntityPosition(replacement);
116
+ }
63
117
  const velocity = pose.dt !== undefined && pose.dt > 0
64
118
  ? [
65
119
  (position[0] - current.position[0]) / pose.dt,
@@ -67,14 +121,15 @@ export function createEntityStore() {
67
121
  (position[2] - current.position[2]) / pose.dt,
68
122
  ]
69
123
  : current.velocity;
70
- store.set(id, {
71
- ...current,
72
- position,
73
- velocity,
74
- rotationY: pose.rotationY ?? current.rotationY,
75
- rotationX: pose.rotationX ?? current.rotationX,
76
- rotationZ: pose.rotationZ ?? current.rotationZ,
77
- });
124
+ current.position = position;
125
+ current.velocity = velocity;
126
+ if (pose.rotationY !== undefined)
127
+ current.rotationY = pose.rotationY;
128
+ if (pose.rotationX !== undefined)
129
+ current.rotationX = pose.rotationX;
130
+ if (pose.rotationZ !== undefined)
131
+ current.rotationZ = pose.rotationZ;
132
+ store.set(id, current);
78
133
  return true;
79
134
  },
80
135
  get(id) {
@@ -87,6 +142,7 @@ export function createEntityStore() {
87
142
  for (const entity of store.arraySnapshot()) {
88
143
  store.delete(entity.id);
89
144
  }
145
+ constraints.clear();
90
146
  },
91
147
  subscribe(listener) {
92
148
  return store.subscribe(listener);
@@ -94,5 +150,19 @@ export function createEntityStore() {
94
150
  snapshot() {
95
151
  return store.arraySnapshot();
96
152
  },
153
+ spawnPoseOf(id) {
154
+ return spawnPoses.get(id) ?? null;
155
+ },
156
+ resetToSpawn(id) {
157
+ const pose = spawnPoses.get(id);
158
+ const current = store.get(id);
159
+ if (pose === undefined || current === undefined)
160
+ return false;
161
+ current.position = pose.position;
162
+ current.rotationY = pose.rotationY;
163
+ current.velocity = [0, 0, 0];
164
+ store.set(id, current);
165
+ return true;
166
+ },
97
167
  };
98
168
  }
@@ -0,0 +1,35 @@
1
+ import type { StatValue } from "./entityStats.js";
2
+ export interface MovementSpeedModifiers {
3
+ multiplier?: number;
4
+ flatBonus?: number;
5
+ }
6
+ /** `base * (multiplier ?? 1) + (flatBonus ?? 0)`, clamped to never go negative. */
7
+ export declare function deriveWalkSpeed(base: number, modifiers: MovementSpeedModifiers): number;
8
+ export interface MovementSpeedDeps {
9
+ stats: {
10
+ get(instanceId: string, statId: string): StatValue | null;
11
+ };
12
+ entities: {
13
+ get(id: string): {
14
+ movement?: {
15
+ walkSpeed?: number;
16
+ };
17
+ } | null;
18
+ update(id: string, patch: {
19
+ movement: {
20
+ walkSpeed: number;
21
+ };
22
+ }): boolean;
23
+ };
24
+ }
25
+ export interface MovementSpeedConfig {
26
+ baseSpeed: number;
27
+ multiplierStat?: string;
28
+ flatBonusStat?: string;
29
+ }
30
+ /**
31
+ * Recomputes `movement.walkSpeed` for an entity from its stats and writes it back via `deps.entities.update`.
32
+ * Either stat id is optional; an omitted or unresolved stat contributes its neutral value (multiplier 1,
33
+ * flat bonus 0) rather than failing. Returns false only when the entity itself doesn't exist.
34
+ */
35
+ export declare function applyStatDrivenSpeed(deps: MovementSpeedDeps, instanceId: string, config: MovementSpeedConfig): boolean;
@@ -0,0 +1,20 @@
1
+ /** `base * (multiplier ?? 1) + (flatBonus ?? 0)`, clamped to never go negative. */
2
+ export function deriveWalkSpeed(base, modifiers) {
3
+ const multiplier = modifiers.multiplier ?? 1;
4
+ const flatBonus = modifiers.flatBonus ?? 0;
5
+ return Math.max(0, base * multiplier + flatBonus);
6
+ }
7
+ /**
8
+ * Recomputes `movement.walkSpeed` for an entity from its stats and writes it back via `deps.entities.update`.
9
+ * Either stat id is optional; an omitted or unresolved stat contributes its neutral value (multiplier 1,
10
+ * flat bonus 0) rather than failing. Returns false only when the entity itself doesn't exist.
11
+ */
12
+ export function applyStatDrivenSpeed(deps, instanceId, config) {
13
+ const entity = deps.entities.get(instanceId);
14
+ if (entity === null)
15
+ return false;
16
+ const multiplier = config.multiplierStat !== undefined ? deps.stats.get(instanceId, config.multiplierStat)?.current : undefined;
17
+ const flatBonus = config.flatBonusStat !== undefined ? deps.stats.get(instanceId, config.flatBonusStat)?.current : undefined;
18
+ const walkSpeed = deriveWalkSpeed(config.baseSpeed, { multiplier, flatBonus });
19
+ return deps.entities.update(instanceId, { movement: { ...entity.movement, walkSpeed } });
20
+ }
@@ -0,0 +1,29 @@
1
+ import type { EntityPosition } from "./entityStore.js";
2
+ import type { SceneObject } from "./objectStore.js";
3
+ export interface ObjectRaycastInput {
4
+ origin: EntityPosition;
5
+ direction: EntityPosition;
6
+ maxDistance: number;
7
+ halfExtents?: EntityPosition;
8
+ filter?: (object: SceneObject) => boolean;
9
+ }
10
+ export interface ObjectRaycastHit {
11
+ instanceId: string;
12
+ catalogId: string;
13
+ distance: number;
14
+ point: EntityPosition;
15
+ normal: EntityPosition;
16
+ }
17
+ /** Optional broadphase for large object sets — typically `ObjectStore.inBox`. */
18
+ export interface ObjectQueryBroadphase {
19
+ inBox(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
20
+ }
21
+ export type ObjectRaycastSource = readonly SceneObject[] | ObjectQueryBroadphase;
22
+ declare function normalize(direction: EntityPosition): EntityPosition;
23
+ declare function intersectAabb(origin: EntityPosition, direction: EntityPosition, min: EntityPosition, max: EntityPosition, maxDistance: number): {
24
+ distance: number;
25
+ normal: EntityPosition;
26
+ } | null;
27
+ export declare function raycastObjects(source: ObjectRaycastSource, input: ObjectRaycastInput): ObjectRaycastHit | null;
28
+ export declare function raycastObjectsAll(source: ObjectRaycastSource, input: ObjectRaycastInput): ObjectRaycastHit[];
29
+ export { intersectAabb, normalize as normalizeDirection };
@@ -0,0 +1,140 @@
1
+ const DEFAULT_HALF_EXTENTS = [0.5, 0.5, 0.5];
2
+ function normalize(direction) {
3
+ const length = Math.hypot(direction[0], direction[1], direction[2]);
4
+ if (length === 0)
5
+ return [0, 0, 0];
6
+ return [direction[0] / length, direction[1] / length, direction[2] / length];
7
+ }
8
+ function isBroadphase(source) {
9
+ return !Array.isArray(source) && typeof source.inBox === "function";
10
+ }
11
+ function intersectAabb(origin, direction, min, max, maxDistance) {
12
+ let tMin = 0;
13
+ let tMax = maxDistance;
14
+ let normalAxis = -1;
15
+ let normalSign = 0;
16
+ for (let axis = 0; axis < 3; axis += 1) {
17
+ const originAxis = origin[axis];
18
+ const dirAxis = direction[axis];
19
+ const minAxis = min[axis];
20
+ const maxAxis = max[axis];
21
+ if (Math.abs(dirAxis) < 1e-12) {
22
+ if (originAxis < minAxis || originAxis > maxAxis)
23
+ return null;
24
+ continue;
25
+ }
26
+ const invD = 1 / dirAxis;
27
+ const t1 = (minAxis - originAxis) * invD;
28
+ const t2 = (maxAxis - originAxis) * invD;
29
+ const minIsNear = t1 <= t2;
30
+ const tNearAxis = minIsNear ? t1 : t2;
31
+ const tFarAxis = minIsNear ? t2 : t1;
32
+ const nearFaceSign = minIsNear ? -1 : 1;
33
+ if (tNearAxis > tMin) {
34
+ tMin = tNearAxis;
35
+ normalAxis = axis;
36
+ normalSign = nearFaceSign;
37
+ }
38
+ tMax = Math.min(tMax, tFarAxis);
39
+ if (tMin > tMax)
40
+ return null;
41
+ }
42
+ if (tMin > maxDistance || tMin < 0)
43
+ return null;
44
+ const normal = normalAxis === 0
45
+ ? [normalSign, 0, 0]
46
+ : normalAxis === 1
47
+ ? [0, normalSign, 0]
48
+ : normalAxis === 2
49
+ ? [0, 0, normalSign]
50
+ : [0, 1, 0];
51
+ return { distance: tMin, normal };
52
+ }
53
+ function hitFor(object, origin, direction, halfExtents, maxDistance) {
54
+ const min = [
55
+ object.position[0] - halfExtents[0],
56
+ object.position[1] - halfExtents[1],
57
+ object.position[2] - halfExtents[2],
58
+ ];
59
+ const max = [
60
+ object.position[0] + halfExtents[0],
61
+ object.position[1] + halfExtents[1],
62
+ object.position[2] + halfExtents[2],
63
+ ];
64
+ const hit = intersectAabb(origin, direction, min, max, maxDistance);
65
+ if (hit === null)
66
+ return null;
67
+ return {
68
+ instanceId: object.instanceId,
69
+ catalogId: object.catalogId,
70
+ distance: hit.distance,
71
+ point: [
72
+ origin[0] + direction[0] * hit.distance,
73
+ origin[1] + direction[1] * hit.distance,
74
+ origin[2] + direction[2] * hit.distance,
75
+ ],
76
+ normal: hit.normal,
77
+ };
78
+ }
79
+ function raySegmentBounds(origin, direction, maxDistance, halfExtents) {
80
+ const endX = origin[0] + direction[0] * maxDistance;
81
+ const endY = origin[1] + direction[1] * maxDistance;
82
+ const endZ = origin[2] + direction[2] * maxDistance;
83
+ const padX = halfExtents[0];
84
+ const padY = halfExtents[1];
85
+ const padZ = halfExtents[2];
86
+ return {
87
+ min: [
88
+ Math.min(origin[0], endX) - padX,
89
+ Math.min(origin[1], endY) - padY,
90
+ Math.min(origin[2], endZ) - padZ,
91
+ ],
92
+ max: [
93
+ Math.max(origin[0], endX) + padX,
94
+ Math.max(origin[1], endY) + padY,
95
+ Math.max(origin[2], endZ) + padZ,
96
+ ],
97
+ };
98
+ }
99
+ function resolveCandidates(source, origin, direction, maxDistance, halfExtents) {
100
+ if (!isBroadphase(source))
101
+ return source;
102
+ const bounds = raySegmentBounds(origin, direction, maxDistance, halfExtents);
103
+ return source.inBox(bounds.min, bounds.max);
104
+ }
105
+ export function raycastObjects(source, input) {
106
+ const direction = normalize(input.direction);
107
+ if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
108
+ return null;
109
+ const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
110
+ const objects = resolveCandidates(source, input.origin, direction, input.maxDistance, halfExtents);
111
+ let nearest = null;
112
+ for (const object of objects) {
113
+ if (input.filter !== undefined && !input.filter(object))
114
+ continue;
115
+ const hit = hitFor(object, input.origin, direction, halfExtents, input.maxDistance);
116
+ if (hit === null)
117
+ continue;
118
+ if (nearest === null || hit.distance < nearest.distance)
119
+ nearest = hit;
120
+ }
121
+ return nearest;
122
+ }
123
+ export function raycastObjectsAll(source, input) {
124
+ const direction = normalize(input.direction);
125
+ if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
126
+ return [];
127
+ const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
128
+ const objects = resolveCandidates(source, input.origin, direction, input.maxDistance, halfExtents);
129
+ const hits = [];
130
+ for (const object of objects) {
131
+ if (input.filter !== undefined && !input.filter(object))
132
+ continue;
133
+ const hit = hitFor(object, input.origin, direction, halfExtents, input.maxDistance);
134
+ if (hit !== null)
135
+ hits.push(hit);
136
+ }
137
+ hits.sort((a, b) => a.distance - b.distance);
138
+ return hits;
139
+ }
140
+ export { intersectAabb, normalize as normalizeDirection };
@@ -1,15 +1,24 @@
1
1
  import type { EntityPosition } from "./entityStore.js";
2
+ export interface ObjectVisual {
3
+ scale?: number | readonly [number, number, number];
4
+ color?: string;
5
+ opacity?: number;
6
+ }
2
7
  export interface SceneObject {
3
8
  instanceId: string;
4
9
  catalogId: string;
5
10
  position: EntityPosition;
6
11
  rotationY: number;
7
12
  parentSpace?: string;
13
+ visual?: ObjectVisual;
8
14
  }
9
15
  export interface PlaceOptions {
10
16
  instanceId?: string;
11
17
  parentSpace?: string;
12
18
  rotation?: number;
19
+ visual?: ObjectVisual;
20
+ /** When `instanceId` is already placed: `"throw"` (default), `"replace"` (fresh placement over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
21
+ onExisting?: "throw" | "replace" | "keep";
13
22
  }
14
23
  export interface ObjectListFilter {
15
24
  parentSpace?: string;
@@ -19,10 +28,14 @@ export interface ObjectStore {
19
28
  remove(instanceId: string): boolean;
20
29
  move(instanceId: string, x: number, y: number, z: number): boolean;
21
30
  rotate(instanceId: string, rotationY: number): boolean;
31
+ setVisual(instanceId: string, visual: ObjectVisual | undefined): boolean;
22
32
  get(instanceId: string): SceneObject | null;
23
33
  list(filter?: ObjectListFilter): readonly SceneObject[];
34
+ at(x: number, y: number, z: number, tolerance?: number): SceneObject[];
35
+ inBox(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
24
36
  clear(): void;
25
37
  subscribe(listener: () => void): () => void;
26
38
  snapshot(): readonly SceneObject[];
27
39
  }
28
40
  export declare function createObjectStore(): ObjectStore;
41
+ export declare function objectVisualScale(visual: ObjectVisual | undefined): readonly [number, number, number];