@jgengine/core 0.18.0 → 0.19.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 (318) hide show
  1. package/CHANGELOG.md +265 -2
  2. package/dist/ai/decisionGraph.d.ts +96 -0
  3. package/dist/ai/decisionGraph.js +212 -0
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/mobBrainGraph.d.ts +3 -0
  7. package/dist/ai/mobBrainGraph.js +25 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/tacticalQueries.d.ts +30 -0
  11. package/dist/ai/tacticalQueries.js +45 -0
  12. package/dist/ai/threat.d.ts +8 -0
  13. package/dist/ai/threat.js +10 -0
  14. package/dist/anim/animGraph.d.ts +155 -0
  15. package/dist/anim/animGraph.js +414 -0
  16. package/dist/anim/boneTexture.d.ts +55 -0
  17. package/dist/anim/boneTexture.js +43 -0
  18. package/dist/anim/footPlacement.d.ts +57 -0
  19. package/dist/anim/footPlacement.js +67 -0
  20. package/dist/anim/ikSolver.d.ts +51 -0
  21. package/dist/anim/ikSolver.js +124 -0
  22. package/dist/anim/locomotionGraph.d.ts +35 -0
  23. package/dist/anim/locomotionGraph.js +62 -0
  24. package/dist/assets/spriteAtlas.d.ts +21 -0
  25. package/dist/assets/spriteAtlas.js +103 -0
  26. package/dist/audio/audioFalloff.d.ts +12 -0
  27. package/dist/audio/doppler.d.ts +21 -0
  28. package/dist/audio/doppler.js +32 -0
  29. package/dist/audio/engineLayers.d.ts +83 -0
  30. package/dist/audio/engineLayers.js +115 -0
  31. package/dist/combat/abilityKit.d.ts +1 -0
  32. package/dist/combat/abilityKit.js +1 -0
  33. package/dist/combat/death.d.ts +7 -0
  34. package/dist/combat/shotOrigin.d.ts +6 -0
  35. package/dist/combat/shotOrigin.js +6 -0
  36. package/dist/combat/weaponFeedback.d.ts +36 -0
  37. package/dist/combat/weaponFeedback.js +42 -0
  38. package/dist/combat/weaponHandling.d.ts +161 -0
  39. package/dist/combat/weaponHandling.js +202 -0
  40. package/dist/combat/weaponPresentation.d.ts +115 -0
  41. package/dist/combat/weaponPresentation.js +96 -0
  42. package/dist/devtools/warnOnce.d.ts +8 -0
  43. package/dist/devtools/warnOnce.js +18 -0
  44. package/dist/economy/currency.d.ts +16 -2
  45. package/dist/economy/currency.js +55 -14
  46. package/dist/economy/wallet.d.ts +5 -4
  47. package/dist/economy/wallet.js +8 -4
  48. package/dist/editor/commandApply.js +1 -0
  49. package/dist/editor/commands.d.ts +4 -1
  50. package/dist/editor/document.d.ts +2 -1
  51. package/dist/editor/document.js +92 -1
  52. package/dist/editor/environment.d.ts +3 -0
  53. package/dist/editor/environment.js +24 -0
  54. package/dist/editor/index.d.ts +4 -2
  55. package/dist/editor/index.js +1 -1
  56. package/dist/editor/types.d.ts +20 -0
  57. package/dist/faction/reputation.d.ts +6 -0
  58. package/dist/faction/reputation.js +11 -0
  59. package/dist/game/cameraConfig.d.ts +59 -1
  60. package/dist/game/chat.d.ts +22 -1
  61. package/dist/game/chat.js +48 -19
  62. package/dist/game/controlGate.d.ts +6 -0
  63. package/dist/game/controlGate.js +30 -1
  64. package/dist/game/defineGame.d.ts +10 -1
  65. package/dist/game/events.d.ts +14 -1
  66. package/dist/game/lootTable.d.ts +4 -0
  67. package/dist/game/lootTable.js +16 -0
  68. package/dist/game/pixelPerfect.d.ts +25 -0
  69. package/dist/game/pixelPerfect.js +29 -0
  70. package/dist/game/playableGame.d.ts +167 -6
  71. package/dist/game/systemRuntime.js +6 -1
  72. package/dist/game/toasts.d.ts +8 -0
  73. package/dist/game/toasts.js +7 -0
  74. package/dist/game/vfxInstance.d.ts +9 -0
  75. package/dist/game/vfxInstance.js +12 -1
  76. package/dist/game/viewports.d.ts +43 -0
  77. package/dist/game/viewports.js +46 -0
  78. package/dist/game/worldItem.d.ts +5 -1
  79. package/dist/game/worldItem.js +0 -1
  80. package/dist/input/actionBindings.d.ts +17 -2
  81. package/dist/input/actionBindings.js +24 -11
  82. package/dist/input/actionContexts.d.ts +45 -0
  83. package/dist/input/actionContexts.js +113 -0
  84. package/dist/input/axisInput.d.ts +4 -0
  85. package/dist/input/axisInput.js +4 -0
  86. package/dist/input/axisShaper.d.ts +73 -0
  87. package/dist/input/axisShaper.js +105 -0
  88. package/dist/input/gamepadModel.d.ts +79 -0
  89. package/dist/input/gamepadModel.js +108 -0
  90. package/dist/input/haptics.d.ts +50 -0
  91. package/dist/input/haptics.js +74 -0
  92. package/dist/input/inputBuffer.d.ts +40 -0
  93. package/dist/input/inputBuffer.js +59 -0
  94. package/dist/input/lookChannel.d.ts +10 -0
  95. package/dist/input/lookChannel.js +6 -0
  96. package/dist/input/pointer.d.ts +2 -0
  97. package/dist/input/pointer.js +4 -0
  98. package/dist/item/affix.d.ts +6 -0
  99. package/dist/item/affix.js +6 -0
  100. package/dist/meta/changelog.d.ts +1 -1
  101. package/dist/meta/changelog.js +196 -1
  102. package/dist/movement/characterController.d.ts +76 -0
  103. package/dist/movement/characterController.js +216 -0
  104. package/dist/movement/freeFlight.d.ts +119 -0
  105. package/dist/movement/freeFlight.js +294 -0
  106. package/dist/movement/glideModel.d.ts +3 -0
  107. package/dist/movement/glideModel.js +6 -0
  108. package/dist/movement/grappleSwing.d.ts +2 -0
  109. package/dist/movement/grappleSwing.js +5 -1
  110. package/dist/movement/movementModel.d.ts +39 -0
  111. package/dist/movement/movementModel.js +62 -19
  112. package/dist/movement/movementProbe.d.ts +51 -0
  113. package/dist/movement/movementProbe.js +119 -0
  114. package/dist/movement/playerMovement.d.ts +30 -3
  115. package/dist/movement/playerMovement.js +304 -7
  116. package/dist/movement/serverStep.d.ts +5 -0
  117. package/dist/movement/serverStep.js +6 -0
  118. package/dist/movement/solidObstacles.d.ts +39 -8
  119. package/dist/movement/solidObstacles.js +71 -6
  120. package/dist/movement/voxelController.js +7 -7
  121. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  122. package/dist/multiplayer/poseSyncGate.js +9 -1
  123. package/dist/multiplayer/presenceContract.d.ts +6 -0
  124. package/dist/multiplayer/presenceModel.d.ts +2 -0
  125. package/dist/multiplayer/presenceModel.js +11 -0
  126. package/dist/nav/navFromEnvironment.d.ts +9 -3
  127. package/dist/nav/navFromEnvironment.js +18 -12
  128. package/dist/nav/navMesh.d.ts +74 -0
  129. package/dist/nav/navMesh.js +570 -0
  130. package/dist/physics/aircraftDynamics.d.ts +390 -0
  131. package/dist/physics/aircraftDynamics.js +704 -0
  132. package/dist/physics/boatDynamics.d.ts +116 -0
  133. package/dist/physics/boatDynamics.js +160 -0
  134. package/dist/physics/drivableVehicle.d.ts +22 -8
  135. package/dist/physics/drivableVehicle.js +4 -2
  136. package/dist/physics/flightDynamics.d.ts +29 -4
  137. package/dist/physics/flightDynamics.js +28 -5
  138. package/dist/physics/handlingProbe.d.ts +249 -0
  139. package/dist/physics/handlingProbe.js +606 -0
  140. package/dist/physics/kinematicVehicle.d.ts +37 -2
  141. package/dist/physics/kinematicVehicle.js +15 -0
  142. package/dist/physics/physicsBackend.d.ts +203 -0
  143. package/dist/physics/physicsBackend.js +49 -0
  144. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  145. package/dist/physics/physicsBackendConformance.js +151 -0
  146. package/dist/physics/physicsWorldBackend.d.ts +45 -0
  147. package/dist/physics/physicsWorldBackend.js +518 -0
  148. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  149. package/dist/physics/vehicleBackendLink.js +102 -0
  150. package/dist/physics/vehicleDynamics.d.ts +374 -0
  151. package/dist/physics/vehicleDynamics.js +737 -0
  152. package/dist/physics/worldColliders.d.ts +13 -0
  153. package/dist/physics/worldColliders.js +128 -0
  154. package/dist/puzzle/cellGrid.d.ts +15 -9
  155. package/dist/puzzle/cellGrid.js +13 -9
  156. package/dist/random/rng.d.ts +12 -1
  157. package/dist/random/rng.js +20 -3
  158. package/dist/render/environment.d.ts +23 -0
  159. package/dist/render/environment.js +1 -0
  160. package/dist/render/lookPreset.d.ts +19 -6
  161. package/dist/render/lookPreset.js +56 -4
  162. package/dist/render/postProcessing.d.ts +40 -1
  163. package/dist/render/postProcessing.js +1 -1
  164. package/dist/render/sprite2d.d.ts +38 -0
  165. package/dist/render/sprite2d.js +40 -0
  166. package/dist/runtime/adapter.d.ts +20 -1
  167. package/dist/runtime/adapter.js +8 -1
  168. package/dist/runtime/cameraDirector.d.ts +17 -3
  169. package/dist/runtime/cameraDirector.js +25 -0
  170. package/dist/runtime/commandInput.d.ts +52 -0
  171. package/dist/runtime/commandInput.js +71 -0
  172. package/dist/runtime/commandRunner.d.ts +39 -7
  173. package/dist/runtime/commandRunner.js +35 -5
  174. package/dist/runtime/context/combat.d.ts +1 -0
  175. package/dist/runtime/context/combat.js +2 -1
  176. package/dist/runtime/context/deathLoot.d.ts +2 -0
  177. package/dist/runtime/context/deathLoot.js +1 -0
  178. package/dist/runtime/gameContext.js +18 -1
  179. package/dist/runtime/gameContextTypes.d.ts +33 -7
  180. package/dist/runtime/gameRuntime.d.ts +23 -5
  181. package/dist/runtime/gameRuntime.js +21 -4
  182. package/dist/runtime/headlessRunner.d.ts +10 -0
  183. package/dist/runtime/headlessRunner.js +26 -8
  184. package/dist/runtime/hostPolicy.d.ts +3 -2
  185. package/dist/runtime/hostPolicy.js +8 -10
  186. package/dist/runtime/hostedGameRunner.js +34 -6
  187. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  188. package/dist/runtime/hostedWorldSession.js +29 -6
  189. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  190. package/dist/runtime/hostedWorldStore.js +1 -0
  191. package/dist/runtime/inputRecorder.d.ts +30 -0
  192. package/dist/runtime/inputRecorder.js +59 -0
  193. package/dist/runtime/inputSnapshot.d.ts +14 -0
  194. package/dist/runtime/inputSnapshot.js +11 -0
  195. package/dist/runtime/localPlayers.d.ts +73 -0
  196. package/dist/runtime/localPlayers.js +119 -0
  197. package/dist/runtime/poseInterpolation.d.ts +36 -0
  198. package/dist/runtime/poseInterpolation.js +91 -0
  199. package/dist/runtime/prediction.d.ts +45 -0
  200. package/dist/runtime/prediction.js +62 -0
  201. package/dist/runtime/simContext.d.ts +45 -0
  202. package/dist/runtime/simContext.js +65 -0
  203. package/dist/runtime/simLoop.d.ts +52 -0
  204. package/dist/runtime/simLoop.js +72 -0
  205. package/dist/runtime/simSnapshot.d.ts +32 -0
  206. package/dist/runtime/simSnapshot.js +60 -0
  207. package/dist/runtime/snapshot.d.ts +8 -0
  208. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  209. package/dist/runtime/snapshotBuffer.js +58 -0
  210. package/dist/runtime/transport.d.ts +41 -2
  211. package/dist/runtime/transport.js +7 -1
  212. package/dist/runtime/worldChunks.d.ts +2 -0
  213. package/dist/runtime/worldChunks.js +14 -0
  214. package/dist/runtime/worldProjection.d.ts +9 -0
  215. package/dist/runtime/worldProjection.js +17 -1
  216. package/dist/runtime/worldSnapshot.d.ts +6 -0
  217. package/dist/runtime/worldSnapshot.js +3 -0
  218. package/dist/scene/behaviorRuntime.d.ts +29 -1
  219. package/dist/scene/behaviorRuntime.js +71 -4
  220. package/dist/scene/behaviors.d.ts +12 -1
  221. package/dist/scene/builtinSceneKinds.js +10 -0
  222. package/dist/scene/sceneKinds.d.ts +9 -0
  223. package/dist/scene/vehicleSeat.d.ts +5 -1
  224. package/dist/scene/vehicleSeat.js +5 -1
  225. package/dist/sensor/concealment.d.ts +6 -0
  226. package/dist/sensor/concealment.js +8 -0
  227. package/dist/sensor/freezeMonitor.d.ts +6 -0
  228. package/dist/sensor/freezeMonitor.js +8 -0
  229. package/dist/sensor/perception.d.ts +70 -0
  230. package/dist/sensor/perception.js +189 -0
  231. package/dist/sensor/recordingBuffer.d.ts +3 -0
  232. package/dist/sensor/recordingBuffer.js +8 -0
  233. package/dist/session/roundState.d.ts +4 -0
  234. package/dist/session/roundState.js +11 -0
  235. package/dist/settings/frameRateLimit.d.ts +16 -0
  236. package/dist/settings/frameRateLimit.js +37 -0
  237. package/dist/settings/graphicsProfile.d.ts +21 -0
  238. package/dist/settings/graphicsProfile.js +48 -0
  239. package/dist/settings/graphicsSettings.d.ts +40 -0
  240. package/dist/settings/graphicsSettings.js +81 -0
  241. package/dist/settings/settingsModel.d.ts +9 -1
  242. package/dist/settings/settingsModel.js +10 -0
  243. package/dist/stats/eventMeter.d.ts +1 -1
  244. package/dist/stats/eventMeter.js +1 -1
  245. package/dist/time/accrueSince.d.ts +10 -0
  246. package/dist/time/accrueSince.js +9 -0
  247. package/dist/time/rateWindow.d.ts +16 -0
  248. package/dist/time/rateWindow.js +14 -0
  249. package/dist/time/serverTick.d.ts +21 -3
  250. package/dist/time/serverTick.js +14 -13
  251. package/dist/vfx/feedbackMixer.d.ts +61 -0
  252. package/dist/vfx/feedbackMixer.js +106 -0
  253. package/dist/vfx/particleDirector.d.ts +68 -0
  254. package/dist/vfx/particleDirector.js +81 -0
  255. package/dist/visibility/simulationCulling.d.ts +8 -1
  256. package/dist/visibility/simulationCulling.js +7 -0
  257. package/dist/visibility/spatialIndex.d.ts +21 -0
  258. package/dist/visibility/spatialIndex.js +16 -0
  259. package/dist/world/authoredSolids.d.ts +46 -0
  260. package/dist/world/authoredSolids.js +75 -0
  261. package/dist/world/buildingGenerator.js +2 -2
  262. package/dist/world/buildingKit.d.ts +3 -1
  263. package/dist/world/buildings.d.ts +46 -2
  264. package/dist/world/buildings.js +53 -3
  265. package/dist/world/cityContent.js +103 -25
  266. package/dist/world/cityKind.d.ts +139 -0
  267. package/dist/world/cityKind.js +367 -16
  268. package/dist/world/features.d.ts +56 -3
  269. package/dist/world/features.js +13 -2
  270. package/dist/world/footprintGrid.d.ts +7 -0
  271. package/dist/world/footprintGrid.js +16 -0
  272. package/dist/world/interiors.d.ts +1 -0
  273. package/dist/world/interiors.js +5 -3
  274. package/dist/world/lod.d.ts +6 -0
  275. package/dist/world/lod.js +8 -0
  276. package/dist/world/placement.d.ts +7 -0
  277. package/dist/world/placement.js +2 -0
  278. package/dist/world/placementController.d.ts +1 -1
  279. package/dist/world/placementController.js +7 -0
  280. package/dist/world/raceCircuit.d.ts +168 -0
  281. package/dist/world/raceCircuit.js +797 -0
  282. package/dist/world/terraform.d.ts +8 -0
  283. package/dist/world/terraform.js +7 -0
  284. package/dist/world/terrain.d.ts +5 -1
  285. package/dist/world/terrain.js +5 -0
  286. package/dist/world/territory.d.ts +97 -0
  287. package/dist/world/territory.js +182 -0
  288. package/dist/world/walls.d.ts +7 -0
  289. package/dist/world/walls.js +7 -0
  290. package/dist/world/worldSolids.d.ts +84 -0
  291. package/dist/world/worldSolids.js +240 -0
  292. package/dist/world.d.ts +5 -3
  293. package/dist/world.js +4 -2
  294. package/package.json +1 -1
  295. package/skills/ce-handoff/SKILL.md +2 -2
  296. package/skills/game-design/SKILL.md +11 -1
  297. package/skills/jgengine/SKILL.md +9 -5
  298. package/skills/jgengine/api.md +205 -101
  299. package/skills/jgengine/capabilities.md +56 -0
  300. package/skills/jgengine/recipes/minimal-game.md +3 -0
  301. package/skills/jgengine-combat/SKILL.md +5 -0
  302. package/skills/jgengine-combat/api.md +138 -103
  303. package/skills/jgengine-combat/capabilities.md +29 -1
  304. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  305. package/skills/jgengine-gameplay/SKILL.md +10 -0
  306. package/skills/jgengine-gameplay/api.md +501 -453
  307. package/skills/jgengine-gameplay/capabilities.md +36 -0
  308. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  309. package/skills/jgengine-world/SKILL.md +21 -3
  310. package/skills/jgengine-world/api.md +1172 -625
  311. package/skills/jgengine-world/capabilities.md +229 -3
  312. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  313. package/skills/jgengine-world/recipes/controls.md +44 -0
  314. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  315. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  316. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  317. package/skills/jgengine-world/reference.md +31 -2
  318. package/skills/jgengine-world/visibility.md +112 -0
@@ -0,0 +1,216 @@
1
+ function resolve(config) {
2
+ return {
3
+ radius: config.radius,
4
+ height: config.height,
5
+ stepHeight: config.stepHeight ?? 0.35,
6
+ maxSlopeDeg: config.maxSlopeDeg ?? 50,
7
+ skinWidth: config.skinWidth ?? 0.02,
8
+ crouchHeight: config.crouchHeight ?? config.height * 0.6,
9
+ snapDistance: config.snapDistance ?? 0.3,
10
+ maxSlides: config.maxSlides ?? 4,
11
+ mask: config.mask ?? 0xffffffff,
12
+ };
13
+ }
14
+ /**
15
+ * Collide-and-slide capsule controller over any {@link PhysicsBackend}: horizontal slide along walls, step-up over
16
+ * ledges, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry read from
17
+ * the ground body's velocity. Pure over the backend's `shapecast`/`overlap`; the caller owns input and gravity policy.
18
+ *
19
+ * @capability character-controller capsule walk over the physics backend — slide, step-up, slopes, crouch, platforms
20
+ */
21
+ export function createCharacterController(initial) {
22
+ let cfg = resolve(initial);
23
+ let state = {
24
+ position: [0, 0, 0],
25
+ verticalVelocity: 0,
26
+ grounded: false,
27
+ groundNormal: [0, 1, 0],
28
+ groundBody: null,
29
+ crouching: false,
30
+ };
31
+ function currentHeight() {
32
+ return state.crouching ? cfg.crouchHeight : cfg.height;
33
+ }
34
+ function shapeFor(height) {
35
+ const halfHeight = Math.max(0, height / 2 - cfg.radius);
36
+ return { kind: "capsule", radius: cfg.radius, halfHeight };
37
+ }
38
+ function centerFor(position, height) {
39
+ return [position[0], position[1] + height / 2, position[2]];
40
+ }
41
+ function walkable(normal) {
42
+ return normal[1] >= Math.cos((cfg.maxSlopeDeg * Math.PI) / 180);
43
+ }
44
+ function cast(backend, from, height, motion, self) {
45
+ return backend.shapecast({
46
+ shape: shapeFor(height),
47
+ position: centerFor(from, height),
48
+ motion,
49
+ mask: cfg.mask,
50
+ ...(self === undefined ? {} : { exclude: self }),
51
+ });
52
+ }
53
+ /** Move along `motion`, stopping `skinWidth` short of the first hit. Returns the fraction travelled and the hit. */
54
+ function sweep(backend, pos, height, motion, self) {
55
+ const len = Math.hypot(motion[0], motion[1], motion[2]);
56
+ if (!(len > 1e-9))
57
+ return { fraction: 1, hit: null };
58
+ const hit = cast(backend, pos, height, motion, self);
59
+ if (hit === null) {
60
+ pos[0] += motion[0];
61
+ pos[1] += motion[1];
62
+ pos[2] += motion[2];
63
+ return { fraction: 1, hit: null };
64
+ }
65
+ const back = cfg.skinWidth / len;
66
+ const fraction = Math.max(0, hit.toi - back);
67
+ pos[0] += motion[0] * fraction;
68
+ pos[1] += motion[1] * fraction;
69
+ pos[2] += motion[2] * fraction;
70
+ return { fraction, hit };
71
+ }
72
+ return {
73
+ config: () => cfg,
74
+ retune(next) {
75
+ cfg = resolve({ ...cfg, ...next });
76
+ },
77
+ state: () => state,
78
+ snapshot: () => ({
79
+ position: [state.position[0], state.position[1], state.position[2]],
80
+ verticalVelocity: state.verticalVelocity,
81
+ grounded: state.grounded,
82
+ groundNormal: [state.groundNormal[0], state.groundNormal[1], state.groundNormal[2]],
83
+ groundBody: state.groundBody,
84
+ crouching: state.crouching,
85
+ }),
86
+ restore(next) {
87
+ state = {
88
+ position: [next.position[0], next.position[1], next.position[2]],
89
+ verticalVelocity: next.verticalVelocity,
90
+ grounded: next.grounded,
91
+ groundNormal: [next.groundNormal[0], next.groundNormal[1], next.groundNormal[2]],
92
+ groundBody: next.groundBody,
93
+ crouching: next.crouching,
94
+ };
95
+ },
96
+ shape: () => shapeFor(currentHeight()),
97
+ center: () => centerFor(state.position, currentHeight()),
98
+ move(backend, input) {
99
+ const { dt, self } = input;
100
+ const result = {
101
+ moved: [0, 0, 0],
102
+ grounded: false,
103
+ groundNormal: [0, 1, 0],
104
+ hitWall: false,
105
+ hitCeiling: false,
106
+ steppedUp: false,
107
+ crouchBlocked: false,
108
+ };
109
+ const start = [state.position[0], state.position[1], state.position[2]];
110
+ const pos = [start[0], start[1], start[2]];
111
+ if (input.crouch === true && !state.crouching) {
112
+ state.crouching = true;
113
+ }
114
+ else if (input.crouch === false && state.crouching) {
115
+ const standing = backend.overlap({
116
+ shape: shapeFor(cfg.height),
117
+ position: centerFor(pos, cfg.height),
118
+ mask: cfg.mask,
119
+ ...(self === undefined ? {} : { exclude: self }),
120
+ });
121
+ if (standing.length === 0)
122
+ state.crouching = false;
123
+ else
124
+ result.crouchBlocked = true;
125
+ }
126
+ const height = currentHeight();
127
+ let carry = [0, 0, 0];
128
+ if (state.grounded && state.groundBody !== null) {
129
+ const ground = backend.body(state.groundBody);
130
+ if (ground !== null)
131
+ carry = [ground.velocity[0] * dt, ground.velocity[1] * dt, ground.velocity[2] * dt];
132
+ }
133
+ let remaining = [input.motion[0] + carry[0], 0, input.motion[2] + carry[2]];
134
+ for (let i = 0; i < cfg.maxSlides; i += 1) {
135
+ const { hit, fraction } = sweep(backend, pos, height, remaining, self);
136
+ if (hit === null)
137
+ break;
138
+ const left = [remaining[0] * (1 - fraction), remaining[1] * (1 - fraction), remaining[2] * (1 - fraction)];
139
+ const dot = left[0] * hit.normal[0] + left[1] * hit.normal[1] + left[2] * hit.normal[2];
140
+ const slid = [
141
+ left[0] - hit.normal[0] * dot,
142
+ left[1] - hit.normal[1] * dot,
143
+ left[2] - hit.normal[2] * dot,
144
+ ];
145
+ if (!walkable(hit.normal)) {
146
+ result.hitWall = true;
147
+ slid[1] = 0;
148
+ if (cfg.stepHeight > 0 && !result.steppedUp && tryStepUp(backend, pos, height, remaining, self)) {
149
+ result.steppedUp = true;
150
+ result.hitWall = false;
151
+ break;
152
+ }
153
+ }
154
+ if (Math.hypot(slid[0], slid[1], slid[2]) < 1e-6)
155
+ break;
156
+ remaining = slid;
157
+ }
158
+ if (input.gravity !== undefined)
159
+ state.verticalVelocity -= input.gravity * dt;
160
+ if (input.jumpVelocity !== undefined && state.grounded) {
161
+ state.verticalVelocity = input.jumpVelocity;
162
+ state.grounded = false;
163
+ }
164
+ const wasGrounded = state.grounded;
165
+ let vertical = state.verticalVelocity * dt + carry[1];
166
+ if (wasGrounded && vertical <= 0)
167
+ vertical -= cfg.snapDistance;
168
+ const { hit: verticalHit } = sweep(backend, pos, height, [0, vertical, 0], self);
169
+ if (verticalHit !== null && vertical > 0) {
170
+ result.hitCeiling = true;
171
+ state.verticalVelocity = 0;
172
+ state.grounded = false;
173
+ state.groundBody = null;
174
+ }
175
+ else if (verticalHit !== null && walkable(verticalHit.normal)) {
176
+ state.grounded = true;
177
+ state.groundNormal = [verticalHit.normal[0], verticalHit.normal[1], verticalHit.normal[2]];
178
+ state.groundBody = verticalHit.body;
179
+ if (state.verticalVelocity < 0)
180
+ state.verticalVelocity = 0;
181
+ // A contact at t=0 means touching or already inside; lift by the skin so the next casts start clear.
182
+ if (verticalHit.toi <= 0)
183
+ pos[1] += cfg.skinWidth;
184
+ }
185
+ else {
186
+ state.grounded = false;
187
+ state.groundBody = null;
188
+ state.groundNormal = [0, 1, 0];
189
+ }
190
+ state.position = pos;
191
+ result.moved = [pos[0] - start[0], pos[1] - start[1], pos[2] - start[2]];
192
+ result.grounded = state.grounded;
193
+ result.groundNormal = [state.groundNormal[0], state.groundNormal[1], state.groundNormal[2]];
194
+ return result;
195
+ },
196
+ };
197
+ function tryStepUp(backend, pos, height, motion, self) {
198
+ const probe = [pos[0], pos[1], pos[2]];
199
+ const up = sweep(backend, probe, height, [0, cfg.stepHeight, 0], self);
200
+ if (up.hit !== null && up.fraction <= 0)
201
+ return false;
202
+ const forward = sweep(backend, probe, height, motion, self);
203
+ if (forward.hit !== null && forward.fraction < 0.05)
204
+ return false;
205
+ const drop = -(probe[1] - pos[1]);
206
+ const down = sweep(backend, probe, height, [0, drop, 0], self);
207
+ if (down.hit === null || !walkable(down.hit.normal))
208
+ return false;
209
+ if (probe[1] <= pos[1] + 1e-6)
210
+ return false;
211
+ pos[0] = probe[0];
212
+ pos[1] = probe[1];
213
+ pos[2] = probe[2];
214
+ return true;
215
+ }
216
+ }
@@ -0,0 +1,119 @@
1
+ import { type AnalogMoveIntent, type CollisionObstacle, type MovementKeysState } from "./movementModel.js";
2
+ import { type AxisBinding } from "../input/axisInput.js";
3
+ import type { PointerAxisState } from "../input/pointerAxis.js";
4
+ import type { ChaseCameraTuning } from "../runtime/cameraDirector.js";
5
+ /** Free-flight families — character-scale 6DOF movement, distinct from vehicle aerodynamics. */
6
+ export type FreeFlightMode = "creative" | "spectator" | "noclip" | "hover";
7
+ /** Per-axis bindings for free-flight — which actions drive each flight axis. Positive is forward/right/up. */
8
+ export interface FreeFlightBindings {
9
+ /** Forward/back — default `moveForward` / `moveBack` (W/S). */
10
+ forward?: AxisBinding;
11
+ /** Strafe right/left — default `moveRight` / `moveLeft` (D/A). */
12
+ strafe?: AxisBinding;
13
+ /** Ascend/descend — default `jump` / `crouch` (Space / Ctrl+C). */
14
+ vertical?: AxisBinding;
15
+ /** Sprint — default `sprint` (Shift) held. */
16
+ sprint?: AxisBinding;
17
+ }
18
+ /** Data-first tuning for one free-flight profile. */
19
+ export interface FreeFlightTuning {
20
+ mode: FreeFlightMode;
21
+ /** Horizontal base speed (units/s) before sprint. Default 8. */
22
+ speed?: number;
23
+ /** Vertical base speed for ascend/descend. Default = `speed`. */
24
+ verticalSpeed?: number;
25
+ /** Sprint multiplier while shift is held. Default 1.8. */
26
+ sprintMultiplier?: number;
27
+ /** Lerp rate toward target velocity. Default 20. */
28
+ acceleration?: number;
29
+ /** Extra damping applied when no input (0 = rely on lerp). Default 0. */
30
+ damping?: number;
31
+ /** Downward acceleration for hover mode. Default 0 (weightless). */
32
+ gravity?: number;
33
+ /** Upward thrust acceleration while ascending in hover. Default 32. */
34
+ thrust?: number;
35
+ /** When true, forward moves along the look pitch (spectator). Default false. */
36
+ alignWithLook?: boolean;
37
+ /** Top vertical fall speed in hover. Default 20. */
38
+ maxFallSpeed?: number;
39
+ /** Whether to slide against world solids. Default true for creative/hover, false for spectator/noclip. */
40
+ collide?: boolean;
41
+ /** Axis bindings for this flight profile — which actions drive each axis. Unlisted axes keep the defaults above. */
42
+ bindings?: FreeFlightBindings;
43
+ /** Camera POV while this flight is active — chase tuning overlay applied on mount, cleared on dismount. Null clears. */
44
+ camera?: ChaseCameraTuning | null;
45
+ }
46
+ /** Velocity state for a free-flight actor — serializable and ownable by the caller. */
47
+ export interface FreeFlightState {
48
+ vx: number;
49
+ vy: number;
50
+ vz: number;
51
+ }
52
+ /** Intent for one free-flight tick — forward/right from WASD/analog, vertical from jump/crouch. */
53
+ export interface FreeFlightIntent {
54
+ forward: number;
55
+ right: number;
56
+ vertical: number;
57
+ sprint: boolean;
58
+ moving: boolean;
59
+ }
60
+ /** World displacement produced by one free-flight tick. */
61
+ export interface FreeFlightStep {
62
+ stepX: number;
63
+ stepY: number;
64
+ stepZ: number;
65
+ }
66
+ /** Stateful handle for any free-flight actor with snapshot/restore/retune. */
67
+ export interface FreeFlightController {
68
+ tick(dt: number, intent: FreeFlightIntent, yaw: number, pitch?: number): FreeFlightStep;
69
+ velocity(): readonly [number, number, number];
70
+ snapshot(): FreeFlightState;
71
+ restore(next: FreeFlightState): void;
72
+ retune(next: FreeFlightTuning): void;
73
+ reset(): void;
74
+ }
75
+ /** Create an empty flight velocity state. */
76
+ export declare function createFreeFlightState(): FreeFlightState;
77
+ /**
78
+ * Translate held keys + analog into a free-flight intent.
79
+ * Vertical is `space - (control|c)` so jump ascends and crouch descends — the same
80
+ * keys that walk/jump/crouch use, re-mapped for flight without re-binding.
81
+ */
82
+ export declare function resolveFreeFlightIntent(keys: MovementKeysState, analog: AnalogMoveIntent | null | undefined): FreeFlightIntent;
83
+ /**
84
+ * Translate live input (held actions + analog + pointer) into a flight intent using
85
+ * per-profile axis bindings. Unlisted bindings fall back to the defaults above so
86
+ * existing games keep WASD/Space/Ctrl/Shift without declaring anything.
87
+ */
88
+ export declare function resolveFreeFlightIntentFromInput(isDown: (action: string) => boolean, value: (action: string) => number, pointer: PointerAxisState | null, bindings?: FreeFlightBindings): FreeFlightIntent;
89
+ /**
90
+ * Advance one frame of free-flight kinematics.
91
+ * Horizontal uses yaw-relative strafe (A = left, D = right) — never roll/bank.
92
+ * Vertical is independent of pitch unless `alignWithLook` is true (spectator).
93
+ */
94
+ export declare function advanceFreeFlight(state: FreeFlightState, intent: FreeFlightIntent, yaw: number, pitch: number | undefined, dt: number, tuning: FreeFlightTuning): FreeFlightStep;
95
+ /** Resolve a flight step against world solids (XZ slide + Y clamp). */
96
+ export declare function resolveFlightStep(position: readonly [number, number, number], stepX: number, stepY: number, stepZ: number, obstacles: readonly CollisionObstacle[], radius?: number): {
97
+ stepX: number;
98
+ stepY: number;
99
+ stepZ: number;
100
+ };
101
+ /**
102
+ * Totally free flight for any actor — character, camera, drone, or debug rig.
103
+ * Horizontal is always yaw-relative strafe; vertical is jump/crouch. Use `alignWithLook`
104
+ * for a 6DOF spectator that flies where the camera looks.
105
+ * @capability free-flight camera-relative creative/spectator/noclip/hover flight with correct strafe, sprint, and optional gravity
106
+ */
107
+ export declare function createFreeFlightController(initialTuning: FreeFlightTuning, options?: {
108
+ yaw?: number;
109
+ pitch?: number;
110
+ state?: FreeFlightState;
111
+ }): FreeFlightController;
112
+ /** Preset for Minecraft-like creative flight — weightless, collides, yaw-relative strafe. */
113
+ export declare const CREATIVE_FLIGHT_TUNING: FreeFlightTuning;
114
+ /** Preset for spectator — weightless, noclips, flies where the camera looks. */
115
+ export declare const SPECTATOR_FLIGHT_TUNING: FreeFlightTuning;
116
+ /** Preset for noclip — weightless, noclips, yaw-relative with independent vertical. */
117
+ export declare const NOCLIP_FLIGHT_TUNING: FreeFlightTuning;
118
+ /** Preset for hover/jetpack — gravity + thrust, useful for short bursts. */
119
+ export declare const HOVER_FLIGHT_TUNING: FreeFlightTuning;
@@ -0,0 +1,294 @@
1
+ import { MOVEMENT_TUNING } from "./movementModel.js";
2
+ import { DEFAULT_OBSTACLE_PLAYER_RADIUS } from "./movementModel.js";
3
+ import { sampleAxisBindings } from "../input/axisInput.js";
4
+ const ANALOG_EPSILON = 0.02;
5
+ /** Create an empty flight velocity state. */
6
+ export function createFreeFlightState() {
7
+ return { vx: 0, vy: 0, vz: 0 };
8
+ }
9
+ const DEFAULT_FORWARD_BINDING = { positive: ["moveForward"], negative: ["moveBack"] };
10
+ const DEFAULT_STRAFE_BINDING = { positive: ["moveRight"], negative: ["moveLeft"] };
11
+ const DEFAULT_VERTICAL_BINDING = { positive: ["jump"], negative: ["crouch", "control", "c"] };
12
+ const DEFAULT_SPRINT_BINDING = { positive: ["sprint"] };
13
+ /**
14
+ * Translate held keys + analog into a free-flight intent.
15
+ * Vertical is `space - (control|c)` so jump ascends and crouch descends — the same
16
+ * keys that walk/jump/crouch use, re-mapped for flight without re-binding.
17
+ */
18
+ export function resolveFreeFlightIntent(keys, analog) {
19
+ let forward = (keys.w ? 1 : 0) - (keys.s ? 1 : 0);
20
+ let right = (keys.d ? 1 : 0) - (keys.a ? 1 : 0);
21
+ if (analog !== undefined && analog !== null) {
22
+ forward = Math.abs(analog.forward) < ANALOG_EPSILON ? 0 : Math.max(-1, Math.min(1, analog.forward));
23
+ right = Math.abs(analog.right) < ANALOG_EPSILON ? 0 : Math.max(-1, Math.min(1, analog.right));
24
+ }
25
+ const vertical = (keys.space ? 1 : 0) - (keys.control || keys.c ? 1 : 0);
26
+ const sprint = keys.shift && vertical >= 0;
27
+ const moving = forward !== 0 || right !== 0 || vertical !== 0;
28
+ return { forward, right, vertical, sprint, moving };
29
+ }
30
+ /**
31
+ * Translate live input (held actions + analog + pointer) into a flight intent using
32
+ * per-profile axis bindings. Unlisted bindings fall back to the defaults above so
33
+ * existing games keep WASD/Space/Ctrl/Shift without declaring anything.
34
+ */
35
+ export function resolveFreeFlightIntentFromInput(isDown, value, pointer, bindings) {
36
+ const forwardBinding = bindings?.forward ?? DEFAULT_FORWARD_BINDING;
37
+ const strafeBinding = bindings?.strafe ?? DEFAULT_STRAFE_BINDING;
38
+ const verticalBinding = bindings?.vertical ?? DEFAULT_VERTICAL_BINDING;
39
+ const sprintBinding = bindings?.sprint ?? DEFAULT_SPRINT_BINDING;
40
+ const forward = sampleAxisBindings({ forward: forwardBinding }, isDown, pointer, undefined, value).forward;
41
+ const right = sampleAxisBindings({ strafe: strafeBinding }, isDown, pointer, undefined, value).strafe;
42
+ const vertical = sampleAxisBindings({ vertical: verticalBinding }, isDown, pointer, undefined, value).vertical;
43
+ const sprintVal = sampleAxisBindings({ sprint: sprintBinding }, isDown, pointer, { sprint: { min: 0, max: 1 } }, value).sprint;
44
+ const sprint = sprintVal > 0.5;
45
+ const moving = forward !== 0 || right !== 0 || vertical !== 0;
46
+ return { forward, right, vertical, sprint, moving };
47
+ }
48
+ /**
49
+ * Advance one frame of free-flight kinematics.
50
+ * Horizontal uses yaw-relative strafe (A = left, D = right) — never roll/bank.
51
+ * Vertical is independent of pitch unless `alignWithLook` is true (spectator).
52
+ */
53
+ export function advanceFreeFlight(state, intent, yaw, pitch, dt, tuning) {
54
+ const delta = Math.min(dt, MOVEMENT_TUNING.maxFrameSeconds);
55
+ if (delta <= 0)
56
+ return { stepX: 0, stepY: 0, stepZ: 0 };
57
+ const speed = tuning.speed ?? 8;
58
+ const verticalSpeed = tuning.verticalSpeed ?? speed;
59
+ const sprintMult = tuning.sprintMultiplier ?? 1.8;
60
+ const accel = tuning.acceleration ?? 20;
61
+ const damping = tuning.damping ?? 0;
62
+ const gravity = tuning.gravity ?? 0;
63
+ const thrust = tuning.thrust ?? 32;
64
+ const maxFall = tuning.maxFallSpeed ?? 20;
65
+ const align = tuning.alignWithLook === true && pitch !== undefined;
66
+ const sprintScale = intent.sprint ? sprintMult : 1;
67
+ let targetX = 0;
68
+ let targetZ = 0;
69
+ let targetY = 0;
70
+ if (align) {
71
+ const cp = Math.cos(pitch);
72
+ const sp = Math.sin(pitch);
73
+ const sy = Math.sin(yaw);
74
+ const cy = Math.cos(yaw);
75
+ const fx = sy * cp;
76
+ const fy = sp;
77
+ const fz = cy * cp;
78
+ const rx = cy;
79
+ const rz = -sy;
80
+ const rawX = fx * intent.forward + rx * intent.right;
81
+ const rawY = fy * intent.forward;
82
+ const rawZ = fz * intent.forward + rz * intent.right;
83
+ const hLen = Math.hypot(rawX, rawY, rawZ);
84
+ if (hLen > 1e-6) {
85
+ const scale = Math.min(1, hLen) / hLen;
86
+ const sx = rawX * scale;
87
+ const sy2 = rawY * scale;
88
+ const sz = rawZ * scale;
89
+ const horizScale = speed * sprintScale;
90
+ const vertScale = verticalSpeed * sprintScale;
91
+ targetX = sx * horizScale;
92
+ targetY = sy2 * horizScale + intent.vertical * vertScale;
93
+ targetZ = sz * horizScale;
94
+ }
95
+ else {
96
+ targetY = intent.vertical * verticalSpeed * sprintScale;
97
+ }
98
+ }
99
+ else {
100
+ const sy = Math.sin(yaw);
101
+ const cy = Math.cos(yaw);
102
+ let fx = sy;
103
+ let fz = cy;
104
+ const lenSq = fx * fx + fz * fz;
105
+ if (lenSq < 1e-6) {
106
+ fx = 0;
107
+ fz = -1;
108
+ }
109
+ else {
110
+ const inv = 1 / Math.sqrt(lenSq);
111
+ fx *= inv;
112
+ fz *= inv;
113
+ }
114
+ const rx = -fz;
115
+ const rz = fx;
116
+ let rawX = fx * intent.forward + rx * intent.right;
117
+ let rawZ = fz * intent.forward + rz * intent.right;
118
+ const len = Math.hypot(rawX, rawZ);
119
+ if (len > 1e-6) {
120
+ const scale = Math.min(1, len) / len;
121
+ rawX *= scale;
122
+ rawZ *= scale;
123
+ }
124
+ else {
125
+ rawX = 0;
126
+ rawZ = 0;
127
+ }
128
+ targetX = rawX * speed * sprintScale;
129
+ targetZ = rawZ * speed * sprintScale;
130
+ targetY = intent.vertical * verticalSpeed * sprintScale;
131
+ }
132
+ if (gravity > 0) {
133
+ const alpha = 1 - Math.exp(-accel * delta);
134
+ state.vx += (targetX - state.vx) * alpha;
135
+ state.vz += (targetZ - state.vz) * alpha;
136
+ if (damping > 0 && !intent.moving) {
137
+ const damp = Math.exp(-damping * delta);
138
+ state.vx *= damp;
139
+ state.vz *= damp;
140
+ }
141
+ const thrustAccel = intent.vertical * thrust;
142
+ state.vy += (thrustAccel - gravity) * delta;
143
+ if (intent.vertical !== 0) {
144
+ const vyTarget = targetY;
145
+ const vyAlpha = 1 - Math.exp(-accel * delta);
146
+ state.vy += (vyTarget - state.vy) * vyAlpha * 0.5;
147
+ }
148
+ if (damping > 0 && intent.vertical === 0) {
149
+ state.vy *= Math.exp(-damping * delta * 0.5);
150
+ }
151
+ if (state.vy < -maxFall)
152
+ state.vy = -maxFall;
153
+ }
154
+ else {
155
+ const alpha = 1 - Math.exp(-accel * delta);
156
+ state.vx += (targetX - state.vx) * alpha;
157
+ state.vy += (targetY - state.vy) * alpha;
158
+ state.vz += (targetZ - state.vz) * alpha;
159
+ if (damping > 0 && !intent.moving) {
160
+ const damp = Math.exp(-damping * delta);
161
+ state.vx *= damp;
162
+ state.vy *= damp;
163
+ state.vz *= damp;
164
+ }
165
+ }
166
+ return { stepX: state.vx * delta, stepY: state.vy * delta, stepZ: state.vz * delta };
167
+ }
168
+ /** Resolve a flight step against world solids (XZ slide + Y clamp). */
169
+ export function resolveFlightStep(position, stepX, stepY, stepZ, obstacles, radius = DEFAULT_OBSTACLE_PLAYER_RADIUS) {
170
+ const feetY = position[1];
171
+ const headY = feetY + 1.8;
172
+ let clampedY = stepY;
173
+ const nextY = feetY + stepY;
174
+ const nextHead = nextY + 1.8;
175
+ for (const ob of obstacles) {
176
+ const ox = ob.position[0] + (ob.offset?.[0] ?? 0);
177
+ const oy = ob.position[1] + (ob.offset?.[1] ?? 0);
178
+ const oz = ob.position[2] + (ob.offset?.[2] ?? 0);
179
+ const hx = ob.halfExtents?.[0] ?? 0.5;
180
+ const hy = ob.halfExtents?.[1] ?? 0.5;
181
+ const hz = ob.halfExtents?.[2] ?? 0.5;
182
+ if (ob.boxes !== undefined && ob.boxes.length > 0) {
183
+ for (const box of ob.boxes) {
184
+ const minX = ox + box.min[0] - radius;
185
+ const maxX = ox + box.max[0] + radius;
186
+ const minY = oy + box.min[1];
187
+ const maxY = oy + box.max[1];
188
+ const minZ = oz + box.min[2] - radius;
189
+ const maxZ = oz + box.max[2] + radius;
190
+ const insideXZ = position[0] > minX && position[0] < maxX && position[2] > minZ && position[2] < maxZ;
191
+ if (!insideXZ)
192
+ continue;
193
+ if (stepY > 0 && feetY < maxY && nextHead > minY && nextHead > maxY && feetY < minY) {
194
+ clampedY = Math.min(clampedY, minY - headY - 1e-3);
195
+ }
196
+ else if (stepY < 0 && headY > minY && nextY < maxY && nextY < minY && headY > maxY) {
197
+ clampedY = Math.max(clampedY, maxY - feetY + 1e-3);
198
+ }
199
+ }
200
+ continue;
201
+ }
202
+ const minX = ox - hx - radius;
203
+ const maxX = ox + hx + radius;
204
+ const minY = oy - hy;
205
+ const maxY = oy + hy;
206
+ const minZ = oz - hz - radius;
207
+ const maxZ = oz + hz + radius;
208
+ const insideXZ = position[0] > minX && position[0] < maxX && position[2] > minZ && position[2] < maxZ;
209
+ if (!insideXZ)
210
+ continue;
211
+ if (stepY > 0 && feetY < maxY && nextHead > minY && nextHead > maxY && feetY < minY) {
212
+ clampedY = Math.min(clampedY, minY - headY - 1e-3);
213
+ }
214
+ else if (stepY < 0 && headY > minY && nextY < maxY && nextY < minY && headY > maxY) {
215
+ clampedY = Math.max(clampedY, maxY - feetY + 1e-3);
216
+ }
217
+ }
218
+ return { stepX, stepY: clampedY, stepZ };
219
+ }
220
+ /**
221
+ * Totally free flight for any actor — character, camera, drone, or debug rig.
222
+ * Horizontal is always yaw-relative strafe; vertical is jump/crouch. Use `alignWithLook`
223
+ * for a 6DOF spectator that flies where the camera looks.
224
+ * @capability free-flight camera-relative creative/spectator/noclip/hover flight with correct strafe, sprint, and optional gravity
225
+ */
226
+ export function createFreeFlightController(initialTuning, options = {}) {
227
+ let tuning = initialTuning;
228
+ const state = options.state !== undefined ? { ...options.state } : createFreeFlightState();
229
+ let yaw = options.yaw ?? 0;
230
+ let pitch = options.pitch ?? 0;
231
+ return {
232
+ tick(dt, intent, nextYaw, nextPitch) {
233
+ yaw = nextYaw;
234
+ if (nextPitch !== undefined)
235
+ pitch = nextPitch;
236
+ return advanceFreeFlight(state, intent, yaw, pitch, dt, tuning);
237
+ },
238
+ velocity: () => [state.vx, state.vy, state.vz],
239
+ snapshot: () => ({ ...state }),
240
+ restore(next) {
241
+ state.vx = next.vx;
242
+ state.vy = next.vy;
243
+ state.vz = next.vz;
244
+ },
245
+ retune(next) {
246
+ tuning = next;
247
+ },
248
+ reset() {
249
+ state.vx = 0;
250
+ state.vy = 0;
251
+ state.vz = 0;
252
+ },
253
+ };
254
+ }
255
+ /** Preset for Minecraft-like creative flight — weightless, collides, yaw-relative strafe. */
256
+ export const CREATIVE_FLIGHT_TUNING = {
257
+ mode: "creative",
258
+ speed: 8,
259
+ verticalSpeed: 8,
260
+ sprintMultiplier: 2.2,
261
+ acceleration: 22,
262
+ collide: true,
263
+ };
264
+ /** Preset for spectator — weightless, noclips, flies where the camera looks. */
265
+ export const SPECTATOR_FLIGHT_TUNING = {
266
+ mode: "spectator",
267
+ speed: 12,
268
+ verticalSpeed: 12,
269
+ sprintMultiplier: 2.5,
270
+ acceleration: 18,
271
+ collide: false,
272
+ alignWithLook: true,
273
+ };
274
+ /** Preset for noclip — weightless, noclips, yaw-relative with independent vertical. */
275
+ export const NOCLIP_FLIGHT_TUNING = {
276
+ mode: "noclip",
277
+ speed: 14,
278
+ verticalSpeed: 14,
279
+ sprintMultiplier: 3,
280
+ acceleration: 18,
281
+ collide: false,
282
+ alignWithLook: false,
283
+ };
284
+ /** Preset for hover/jetpack — gravity + thrust, useful for short bursts. */
285
+ export const HOVER_FLIGHT_TUNING = {
286
+ mode: "hover",
287
+ speed: 6,
288
+ verticalSpeed: 6,
289
+ sprintMultiplier: 1.6,
290
+ acceleration: 16,
291
+ gravity: 18,
292
+ thrust: 34,
293
+ collide: true,
294
+ };
@@ -40,6 +40,9 @@ export interface GlideModel {
40
40
  step(dt: number, input?: GlideInput, externalVelocity?: GlideVec3): GlideStep;
41
41
  pose(): GlideStep;
42
42
  launch(position: GlideVec3, heading: number, initialSpeed?: number): void;
43
+ /** Plain JSON copy of the glider's position, velocity and heading. */
44
+ snapshot(): GlideStep;
45
+ restore(next: GlideStep): void;
43
46
  }
44
47
  /**
45
48
  * Gliding/wingsuit descent control — lift, drag, and steering from a launch.
@@ -32,6 +32,12 @@ export function createGlideModel(config = {}) {
32
32
  vz = Math.cos(heading) * initialSpeed;
33
33
  },
34
34
  pose: snapshot,
35
+ snapshot,
36
+ restore(next) {
37
+ [x, y, z] = next.position;
38
+ [vx, vy, vz] = next.velocity;
39
+ heading = next.heading;
40
+ },
35
41
  step(dt, input = {}, externalVelocity) {
36
42
  if (dt <= 0)
37
43
  return snapshot();
@@ -28,6 +28,8 @@ export interface GrappleSwing {
28
28
  fire(anchor: SwingVec3, from: SwingVec3): void;
29
29
  release(): void;
30
30
  state(): GrappleSwingState;
31
+ /** Puts a {@link GrappleSwing.state} back; `anchor: null` detaches. */
32
+ restore(next: GrappleSwingState): void;
31
33
  /** Constrain one frame. `reeling` shortens the rope; returns the corrected position/velocity. */
32
34
  step(position: SwingVec3, velocity: SwingVec3, dt: number, reeling?: boolean): GrappleSwingStep;
33
35
  }
@@ -18,7 +18,11 @@ export function createGrappleSwing(config = {}) {
18
18
  release() {
19
19
  anchor = null;
20
20
  },
21
- state: () => ({ attached: anchor !== null, anchor, ropeLength }),
21
+ state: () => ({ attached: anchor !== null, anchor: anchor === null ? null : [anchor[0], anchor[1], anchor[2]], ropeLength }),
22
+ restore(next) {
23
+ anchor = next.anchor === null ? null : [next.anchor[0], next.anchor[1], next.anchor[2]];
24
+ ropeLength = next.ropeLength;
25
+ },
22
26
  step(position, velocity, dt, reeling = false) {
23
27
  if (anchor === null || dt <= 0)
24
28
  return { position, velocity };