@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,23 +1,5 @@
1
1
  import { pickWeighted } from "../world/scatterItems.js";
2
- function hashSeed(seed) {
3
- const text = typeof seed === "number" ? seed.toString() : seed;
4
- let h = 2166136261 >>> 0;
5
- for (let i = 0; i < text.length; i++) {
6
- h ^= text.charCodeAt(i);
7
- h = Math.imul(h, 16777619);
8
- }
9
- return h >>> 0;
10
- }
11
- export function seededRng(seed) {
12
- let a = hashSeed(seed) >>> 0;
13
- return () => {
14
- a = (a + 0x6d2b79f5) >>> 0;
15
- let t = a;
16
- t = Math.imul(t ^ (t >>> 15), t | 1);
17
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
18
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
19
- };
20
- }
2
+ export { seededRng } from "../random/rng.js";
21
3
  function resolveRoll(roll, rng) {
22
4
  if (typeof roll === "number")
23
5
  return roll;
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.7.0";
1
+ export declare const VERSION = "0.9.0";
2
2
  export interface ChangelogEntry {
3
3
  migrate: readonly string[];
4
4
  added: readonly string[];
@@ -1,5 +1,63 @@
1
- export const VERSION = "0.7.0";
1
+ export const VERSION = "0.9.0";
2
2
  export const CHANGELOG = {
3
+ "0.9.0": {
4
+ migrate: [
5
+ "Bump every @jgengine/* dependency to ^0.9.0 (the eight packages version in lockstep).",
6
+ "Mobile HUD fit is now on by default: design-resolution HUD scaling applies to every game with no config change. A desktop-only game that must keep the legacy fixed 0.85 compact zoom declares platforms: [\"web\"] (without \"mobile\").",
7
+ "Everything else is additive — VisibilitySystem defaults are enabled automatically but only affect renderers that read the policy, and existing games keep every 0.8.0 API unchanged.",
8
+ "Assets now resolve from this repo's self-hosted mirror (DEFAULT_RELEASE_BASE); games that pinned the previous base can override releaseBase if they need the old host.",
9
+ ],
10
+ added: [
11
+ "Mobile HUD fit — design-resolution UI scaling applies to every game by default: HudCanvas scales the whole HUD from PlayableGame.hudFit.designSize (default 1600×900, clamps minScale/maxScale 0.4–1) so desktop-authored layouts shrink to fit a phone; pure math in @jgengine/core/ui/hudScale (hudScaleForViewport, resolveHudFit, rectOverflow, overflowingPanels), wired by @jgengine/react's HudViewportProvider.",
12
+ "Graphics → UI scale — a player-facing graphics.uiScale slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform.",
13
+ "HUD overflow gate — HudCanvas reports panels that escape the viewport on a data-hud-overflow attribute; bun run shoot <game> --device mobile|both exits non-zero naming the offenders.",
14
+ "Automatic visibility & streaming defaults — VisibilitySystem (exported from @jgengine/core) gives every scene renderer-agnostic culling and asset-streaming policy with no per-object wiring: distance culling, preload margins, hysteresis, delayed unloading, multi-camera awareness, and per-object overrides.",
15
+ "Self-hosted asset mirror — DEFAULT_RELEASE_BASE points at this repo's rolling packs release; a catalog-driven mirror-assets workflow (weekly cron + dispatch) keeps it in sync.",
16
+ ],
17
+ changed: [
18
+ "Mobile HUD fit is on by default — design-resolution HUD scaling now applies to every game with no config change (was opt-in via platforms: [\"web\", \"mobile\"]); a desktop-only game keeps the legacy fixed 0.85 compact zoom via platforms: [\"web\"].",
19
+ ],
20
+ removed: [],
21
+ },
22
+ "0.8.0": {
23
+ migrate: [
24
+ "Bump every @jgengine/* dependency to ^0.8.0 (the eight packages version in lockstep).",
25
+ "Additive only, except for gameui — every other 0.7.0 API is unchanged; opt into any of the below by importing it directly.",
26
+ "Breaking: replace any @jgengine/react/gameui import with the equivalent registry component (npx shadcn@latest add https://jgengine.com/r/<name>.json), and swap GameUiThemeProvider for --jg-* CSS variables on a wrapper element. GameIcon and friends moved to @jgengine/react/gameIcons.",
27
+ "leveling({ thresholdMode: 'cumulative' }) is opt-in; the default 'perLevel' behavior is unchanged.",
28
+ "defineGame.physics.gravity/jumpVelocity are now read by the built-in kinematics controller every frame — if a game already set them expecting a no-op, jump/fall now actually reflects them; omit both to keep the previous defaults.",
29
+ ],
30
+ added: [
31
+ "Transport pipe seam (@jgengine/ws/pipe) — createWsBackend runs over any bidirectional string channel, not just a raw WebSocket, via TransportPipe/TransportPipeHandlers/TransportPipeFactory, with webSocketPipe as the default.",
32
+ "Browser-safe authoritative host — createGameHost and memoryPersistence moved from @jgengine/node to @jgengine/ws/host (zero Node dependencies; @jgengine/node re-exports both unchanged). @jgengine/ws/hostRouter's createHostRouter extracts the ws wire-protocol session logic into a transport-agnostic HostRouter; @jgengine/node's createGameWsServer is now a thin binding of this router onto the ws npm package. loopbackPipe connects a createWsBackend straight into an in-process router.",
33
+ "Socket.IO transport — @jgengine/ws/socketIoPipe (socketIoPipe, createSocketIoBackend) and @jgengine/node/socketIoServer (attachGameSocketIoServer) ride the existing ws JSON protocol over socket.io's send/message frames, no socket.io dependency in either package's types. New socketIo() adapter in @jgengine/core/runtime/adapter.",
34
+ "WebRTC peer-to-peer (@jgengine/ws/peer) — one browser tab hosts authoritatively with no server process: createPeerHost/createPeerGuest, encodePeerSignal/decodePeerSignal for copy-pasteable signaling codes, broadcastChannelSignaling for same-origin multi-tab play. New p2p() adapter.",
35
+ "LAN adapter + Fly sugar — lan() in @jgengine/core/runtime/adapter resolves to the page's own hostname over ws, no URL configuration; fly() is ws sugar for a Fly.io deploy. apps/dev's Vite server listens on the network and exposes ?p2p=host/join query params.",
36
+ "ws() (@jgengine/core/runtime/adapter) gained an optional url field, carried through by resolveShellMultiplayer.",
37
+ "Cumulative leveling — leveling({ thresholdMode: 'cumulative' }) tracks xp as a lifetime total that resolves upward across levels and clamps at the max-level threshold once capped (#12).",
38
+ "Direction-aware pool depletion — EffectSystem.canReceive(instanceId, effect, magnitude?) takes an optional signed magnitude; negative checks the opposite direction and returns 'pools-depleted' only when every stat in the receive order is already at max, so heals reach fully-depleted targets (#168).",
39
+ "Puzzle primitives — puzzle/cellGrid (uniform-cell boards: line-clear, match-3 cascade, run detection) and puzzle/fallingPiece (rotation-state shapes, ghost drop, lock delay, classic gravity/level/score curves) for Tetris/match-3 games (#166).",
40
+ "Voxel field — world/voxelField (createVoxelField, chunked block lattice, neighbors/exposedFaces, 3D DDA raycast, dirty-tracked chunkVersion) for voxel games and instanced renderers; assert on field.summary() the way environment worlds assert on summarizeEnvironment (#166).",
41
+ "defineGame games may omit assets (an empty catalog is injected); PlayableGame.presentation: 'hud' mounts no 3D canvas/camera/pointer for board/card/menu games; an environment() world auto-renders as the shell's backdrop when PlayableGame.environment is unset (#166).",
42
+ "Declared-action intent board — turn/intent createIntentBoard for one-turn-ahead intents (Slay-the-Spire style): declare/peek/all/consume/clear (#168).",
43
+ "turnLoop lifecycle hooks — config.onTurnStart/onTurnEnd fire on every advanceTurn(); ctx.game.turn.loop(id, config) lazily creates/returns a notify-wrapped TurnLoop so every mutation (advanceTurn/advancePhase/advanceRound/spend/gain/refill/setOrder/...) auto-bumps ctx.version() with no manual wiring (#163/#168).",
44
+ "ctx.game.store — a reactive per-game keyed store (set/delete/get/has/subscribe/mapSnapshot/arraySnapshot) plus @jgengine/react's useGameStore selector hook, replacing hand-rolled module-level stores for ad-hoc reactive game state; ctx.game.cards.pile(id, config?) lazily creates/returns a notify-wrapped CardPile the same way; createCardPile gained an onChange hook for headless use; CommandDefinition.apply may return void for side-effect-only commands (#163).",
45
+ "Camera — sideScroll rig (fixed lateral follow for 2.5D platformers/beat-'em-ups), a none rig (no camera mounted; pairs with PlayableGame.presentation: 'hud'), rts.pan: false (static backdrop camera: no pan/edge-scroll/rotate/zoom, still re-centers on the follow target), and the observer rig now defaults to the local player when bind is unset (#167).",
46
+ "Sensors + session — sensor/concealment (colorDistance/concealmentScore/createConcealmentSensor), sensor/freezeMonitor (createFreezeMonitor), session/roles (assignRoles); createRoundState's RoundConfig.teams accepts per-team roles and an optional winCondition that evaluate() checks each tick, and takes an optional phaseOrder for arbitrary named phase cycles (concludeRound/evaluate settle only while the current phase is neither the first nor the last entry) (#151).",
47
+ "Appearance replication — presence rows carry an optional per-slot appearance channel (cosmetic ids, hex tints, model keys) alongside pose, riding the existing pose message with no protocol bump; wire ctx.player.cosmetics.get(userId) into the outgoing pose (#151).",
48
+ "ctx.input — a per-frame held-action snapshot (publish(held)/isDown(action)/held()) without bumping ctx.version(); action bindings gained repeatMs (repeat-fire while held); every command resolved from a bound action now carries aim; pointer.secondaryCommand runs a command on right-click off the same raycast as move/ping (#164).",
49
+ "Object spatial queries + entity patching — ctx.scene.object.at/inBox/raycast/raycastAll over unit-box objects; ctx.scene.entity.update(id, patch) for name/position/rotation/role/movement/behaviors/meta; per-instance renderObject/objectStyles overrides; pointerService.worldHitCenter() + pointer-lock center-ray aiming (#165).",
50
+ "Controller movement config — PlayerMovementConfig (mode: free/axis/grid, axis, cellSize, collideObjects, beforeCommit pre-commit hook) for the shell-driven walk controller; defineGame.physics.gravity/jumpVelocity are honored by the built-in walk controller (distinct from the standalone physics/physicsWorld rigid-body sim); ctx.player.motion.impulse/setVerticalVelocity/setY/takePending (MotionIntents); entity.spawnPoseOf/resetToSpawn/resetAllToSpawn (#162).",
51
+ "Model material/animation + paint — ModelConfig.tint/metalness/roughness/animation (GLTF clip playback, paused pose holds); PointerHit.uv + pointerService.sampleSurface() for material-aware picking; ctx.scene.entity.paint runtime paint layer, auto-rendered via a per-instance canvas texture with no per-game wiring; remote-player appearance tint recolors the shell's default capsule (#151).",
52
+ "Factions & reputation — faction/factions (createFactionGraph faction-vs-faction relation matrix, createFactionRoster entity membership + relationBetweenEntities/isHostile/hostilesOf) turns 'who is my enemy' into a data lookup feeding scene/targeting and ai/threat instead of a per-game role-string check; faction/reputation (createReputationLedger, DEFAULT_REPUTATION_TIERS Hated→Exalted, tierForStanding, effectiveRelation) tracks per-actor standing and overlays it on the base faction relation for at-war reputation grinds.",
53
+ "Threat taunt — ai/threat gained taunt(source, durationSeconds)/forcedTarget(): a taunt forces the mob onto a tank for its window (overriding highest) and lifts the taunter to the current top threat so aggro holds afterward; decay(dt) counts the window down.",
54
+ "Devtools discovery covers every game variable — discover.scanTable recurses nested plain objects and arrays into dotted paths (camera.chase.distance, waves.0.speed; 5 levels, 512 entries per scan, dedupes a value reachable through two tables), tunableDiscoveryPlugin rewrites annotated and derived single-line export consts (Math.sqrt(...), PLAYER_HEIGHT - 0.15) not just literals, DevtoolsOverlay self-scans the PlayableGame and core MOVEMENT_TUNING so physics/camera/movement/world config tune with zero exports in any host, resolvePhysicsTuning reads gravity/jumpVelocity live per frame, and the Tune tab gained a filter box.",
55
+ ],
56
+ changed: [],
57
+ removed: [
58
+ "The gameui component kit (@jgengine/react/gameui) — the themed HUD kit and its GameUiThemeProvider/useGameUiTheme theming. Breaking for anyone importing @jgengine/react/gameui. Now ships as installable shadcn registry items at https://jgengine.com/r/<name>.json, styled with Tailwind + --jg-* CSS variables. GameIcon and friends moved to @jgengine/react/gameIcons.",
59
+ ],
60
+ },
3
61
  "0.7.0": {
4
62
  migrate: [
5
63
  "Bump every @jgengine/* dependency to ^0.7.0 (the eight packages version in lockstep).",
@@ -24,7 +24,9 @@ export interface DashBurst {
24
24
  }
25
25
  export declare function iframeActive(config: DashConfig, elapsedMs: number): boolean;
26
26
  export declare function dashEase(t: number): number;
27
+ export declare function dashDisplacement(config: DashConfig, dir: DashDirection, elapsedMs: number): [number, number, number];
27
28
  export declare function dashOffset(config: DashConfig, dir: DashDirection, elapsedMs: number): [number, number, number];
29
+ export declare function dashFrameDelta(config: DashConfig, dir: DashDirection, previousElapsedMs: number, elapsedMs: number): [number, number, number];
28
30
  export interface DashState {
29
31
  tryDash(direction: DashDirection, nowMs: number): DashRejection | DashBurst;
30
32
  tick(dtSeconds: number, nowMs: number): void;
@@ -32,6 +34,7 @@ export interface DashState {
32
34
  staminaFraction(): number;
33
35
  isDashing(nowMs: number): boolean;
34
36
  isInvulnerable(nowMs: number): boolean;
37
+ displacement(nowMs: number): [number, number, number];
35
38
  offset(nowMs: number): [number, number, number];
36
39
  }
37
40
  export declare function createDashState(config: DashConfig): DashState;
@@ -5,13 +5,21 @@ export function dashEase(t) {
5
5
  const clamped = Math.max(0, Math.min(1, t));
6
6
  return 1 - (1 - clamped) * (1 - clamped);
7
7
  }
8
- export function dashOffset(config, dir, elapsedMs) {
8
+ export function dashDisplacement(config, dir, elapsedMs) {
9
9
  const traveled = dashEase(elapsedMs / config.durationMs) * config.distance;
10
10
  const len = Math.hypot(dir.x, dir.z);
11
11
  if (len === 0)
12
12
  return [0, 0, 0];
13
13
  return [(dir.x / len) * traveled, 0, (dir.z / len) * traveled];
14
14
  }
15
+ export function dashOffset(config, dir, elapsedMs) {
16
+ return dashDisplacement(config, dir, elapsedMs);
17
+ }
18
+ export function dashFrameDelta(config, dir, previousElapsedMs, elapsedMs) {
19
+ const previous = dashDisplacement(config, dir, previousElapsedMs);
20
+ const current = dashDisplacement(config, dir, elapsedMs);
21
+ return [current[0] - previous[0], current[1] - previous[1], current[2] - previous[2]];
22
+ }
15
23
  export function createDashState(config) {
16
24
  let stamina = config.staminaMax;
17
25
  let dashStartMs = null;
@@ -23,6 +31,12 @@ export function createDashState(config) {
23
31
  const dt = nowMs - dashStartMs;
24
32
  return dt >= 0 && dt < config.durationMs ? dt : null;
25
33
  }
34
+ function displacementAt(nowMs) {
35
+ const dt = elapsed(nowMs);
36
+ if (dt === null)
37
+ return [0, 0, 0];
38
+ return dashDisplacement(config, dashDir, dt);
39
+ }
26
40
  return {
27
41
  tryDash(direction, nowMs) {
28
42
  if (elapsed(nowMs) !== null)
@@ -55,11 +69,11 @@ export function createDashState(config) {
55
69
  const dt = elapsed(nowMs);
56
70
  return dt !== null && iframeActive(config, dt);
57
71
  },
72
+ displacement(nowMs) {
73
+ return displacementAt(nowMs);
74
+ },
58
75
  offset(nowMs) {
59
- const dt = elapsed(nowMs);
60
- if (dt === null)
61
- return [0, 0, 0];
62
- return dashOffset(config, dashDir, dt);
76
+ return displacementAt(nowMs);
63
77
  },
64
78
  };
65
79
  }
@@ -0,0 +1,44 @@
1
+ export type GlideVec3 = readonly [number, number, number];
2
+ export interface GlideModelConfig {
3
+ /** Fraction of gravity felt while gliding; default `0.25`. */
4
+ gravityScale?: number;
5
+ /** Gravity used before scaling; default `20`. */
6
+ gravity?: number;
7
+ /** Forward cruise acceleration toward `forwardSpeed`; default `10`. */
8
+ thrustAccel?: number;
9
+ /** Cruise speed the glider settles toward along its heading; default `12`. */
10
+ forwardSpeed?: number;
11
+ /** Yaw radians/s at full steer input; default `1.6`. */
12
+ yawRate?: number;
13
+ /** Vertical thrust (units/s²) at full throttle — powered climb; default `0` (unpowered glide). */
14
+ climbAccel?: number;
15
+ /** Terminal fall speed; default `8`. */
16
+ maxFallSpeed?: number;
17
+ /** Control softness in `[0, 1]` — `1` full authority, lower values (turbulence) blunt steering and thrust; default `1`. */
18
+ control?: number;
19
+ }
20
+ export interface GlideInput {
21
+ /** Steering in `[-1, 1]` (positive turns toward +x when facing +z). */
22
+ yaw?: number;
23
+ /** Powered climb in `[0, 1]` (needs `climbAccel`). */
24
+ throttle?: number;
25
+ /** Runtime control-authority override in `[0, 1]` — schedule-varying turbulence; overrides the config `control`. */
26
+ control?: number;
27
+ }
28
+ export interface GlideStep {
29
+ position: GlideVec3;
30
+ velocity: GlideVec3;
31
+ heading: number;
32
+ }
33
+ /**
34
+ * A kinematic glider (#282.6) — the walk-controller-compatible sibling of `physics/traversal`'s
35
+ * rigid-body `Glide`, with the yaw and powered-climb authority that one lacks. External flows —
36
+ * `physics/flowTube` corridors, `world/windZones` schedules — compose per tick through
37
+ * `externalVelocity`, and turbulence degrades control through the `control` scalar.
38
+ */
39
+ export interface GlideModel {
40
+ step(dt: number, input?: GlideInput, externalVelocity?: GlideVec3): GlideStep;
41
+ pose(): GlideStep;
42
+ launch(position: GlideVec3, heading: number, initialSpeed?: number): void;
43
+ }
44
+ export declare function createGlideModel(config?: GlideModelConfig): GlideModel;
@@ -0,0 +1,54 @@
1
+ export function createGlideModel(config = {}) {
2
+ const gravity = (config.gravity ?? 20) * (config.gravityScale ?? 0.25);
3
+ const thrustAccel = config.thrustAccel ?? 10;
4
+ const forwardSpeed = config.forwardSpeed ?? 12;
5
+ const yawRate = config.yawRate ?? 1.6;
6
+ const climbAccel = config.climbAccel ?? 0;
7
+ const maxFallSpeed = config.maxFallSpeed ?? 8;
8
+ const baseControl = config.control ?? 1;
9
+ let x = 0;
10
+ let y = 0;
11
+ let z = 0;
12
+ let heading = 0;
13
+ let vx = 0;
14
+ let vy = 0;
15
+ let vz = 0;
16
+ function snapshot() {
17
+ return { position: [x, y, z], velocity: [vx, vy, vz], heading };
18
+ }
19
+ return {
20
+ launch(position, nextHeading, initialSpeed = forwardSpeed) {
21
+ x = position[0];
22
+ y = position[1];
23
+ z = position[2];
24
+ heading = nextHeading;
25
+ vx = Math.sin(heading) * initialSpeed;
26
+ vy = 0;
27
+ vz = Math.cos(heading) * initialSpeed;
28
+ },
29
+ pose: snapshot,
30
+ step(dt, input = {}, externalVelocity) {
31
+ if (dt <= 0)
32
+ return snapshot();
33
+ const control = Math.max(0, Math.min(1, input.control ?? baseControl));
34
+ heading += (input.yaw ?? 0) * yawRate * control * dt;
35
+ const fx = Math.sin(heading);
36
+ const fz = Math.cos(heading);
37
+ const along = vx * fx + vz * fz;
38
+ const cruise = (forwardSpeed - along) * Math.min(1, (thrustAccel / Math.max(1, forwardSpeed)) * dt);
39
+ vx += fx * cruise * control;
40
+ vz += fz * cruise * control;
41
+ vy -= gravity * dt;
42
+ const throttle = Math.max(0, Math.min(1, input.throttle ?? 0));
43
+ if (climbAccel > 0 && throttle > 0)
44
+ vy += climbAccel * throttle * control * dt;
45
+ if (vy < -maxFallSpeed)
46
+ vy = -maxFallSpeed;
47
+ const [ex, ey, ez] = externalVelocity ?? [0, 0, 0];
48
+ x += (vx + ex) * dt;
49
+ y += (vy + ey) * dt;
50
+ z += (vz + ez) * dt;
51
+ return snapshot();
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,34 @@
1
+ export type SwingVec3 = readonly [number, number, number];
2
+ export interface GrappleSwingConfig {
3
+ /** Rope shortens by this many units/s while `reeling`. Default `0` (fixed length). */
4
+ reelSpeed?: number;
5
+ minLength?: number;
6
+ /** Extra spring pull toward the anchor when the rope is taut (elastic feel); default `0` (hard constraint only). */
7
+ stiffness?: number;
8
+ /** Velocity damping per second while attached; default `0.1`. */
9
+ damping?: number;
10
+ }
11
+ export interface GrappleSwingState {
12
+ attached: boolean;
13
+ anchor: SwingVec3 | null;
14
+ ropeLength: number;
15
+ }
16
+ export interface GrappleSwingStep {
17
+ position: SwingVec3;
18
+ velocity: SwingVec3;
19
+ }
20
+ /**
21
+ * A rope-swing constraint for kinematic controllers (#282.5) — the walk-controller-compatible
22
+ * sibling of `physics/traversal`'s rigid-body `Grapple`. Fire at an anchor, feed each frame's
23
+ * integrated position/velocity through `step`, and commit what comes back (via `beforeCommit`
24
+ * or `setPose`): positions beyond the rope clamp to its sphere and velocity loses its outward
25
+ * radial component, which is exactly a pendulum swing.
26
+ */
27
+ export interface GrappleSwing {
28
+ fire(anchor: SwingVec3, from: SwingVec3): void;
29
+ release(): void;
30
+ state(): GrappleSwingState;
31
+ /** Constrain one frame. `reeling` shortens the rope; returns the corrected position/velocity. */
32
+ step(position: SwingVec3, velocity: SwingVec3, dt: number, reeling?: boolean): GrappleSwingStep;
33
+ }
34
+ export declare function createGrappleSwing(config?: GrappleSwingConfig): GrappleSwing;
@@ -0,0 +1,56 @@
1
+ export function createGrappleSwing(config = {}) {
2
+ const reelSpeed = config.reelSpeed ?? 0;
3
+ const minLength = config.minLength ?? 1;
4
+ const stiffness = config.stiffness ?? 0;
5
+ const damping = config.damping ?? 0.1;
6
+ let anchor = null;
7
+ let ropeLength = 0;
8
+ return {
9
+ fire(nextAnchor, from) {
10
+ anchor = nextAnchor;
11
+ ropeLength = Math.max(minLength, Math.hypot(from[0] - nextAnchor[0], from[1] - nextAnchor[1], from[2] - nextAnchor[2]));
12
+ },
13
+ release() {
14
+ anchor = null;
15
+ },
16
+ state: () => ({ attached: anchor !== null, anchor, ropeLength }),
17
+ step(position, velocity, dt, reeling = false) {
18
+ if (anchor === null || dt <= 0)
19
+ return { position, velocity };
20
+ if (reeling && reelSpeed > 0)
21
+ ropeLength = Math.max(minLength, ropeLength - reelSpeed * dt);
22
+ let px = position[0];
23
+ let py = position[1];
24
+ let pz = position[2];
25
+ let vx = velocity[0];
26
+ let vy = velocity[1];
27
+ let vz = velocity[2];
28
+ const dx = px - anchor[0];
29
+ const dy = py - anchor[1];
30
+ const dz = pz - anchor[2];
31
+ const distance = Math.hypot(dx, dy, dz);
32
+ if (distance > ropeLength && distance > 1e-9) {
33
+ const nx = dx / distance;
34
+ const ny = dy / distance;
35
+ const nz = dz / distance;
36
+ px = anchor[0] + nx * ropeLength;
37
+ py = anchor[1] + ny * ropeLength;
38
+ pz = anchor[2] + nz * ropeLength;
39
+ const radial = vx * nx + vy * ny + vz * nz;
40
+ if (radial > 0) {
41
+ vx -= nx * radial;
42
+ vy -= ny * radial;
43
+ vz -= nz * radial;
44
+ }
45
+ if (stiffness > 0) {
46
+ const pull = (distance - ropeLength) * stiffness * dt;
47
+ vx -= nx * pull;
48
+ vy -= ny * pull;
49
+ vz -= nz * pull;
50
+ }
51
+ }
52
+ const damp = Math.max(0, 1 - damping * dt);
53
+ return { position: [px, py, pz], velocity: [vx * damp, vy * damp, vz * damp] };
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,29 @@
1
+ export type TrailPoint = readonly [number, number, number];
2
+ export interface LeaderTrailConfig {
3
+ /** Arc-length gap between consecutive followers. */
4
+ spacing: number;
5
+ /** Followers the trail must be able to serve; history is capped to cover them. Default `16`. */
6
+ maxFollowers?: number;
7
+ /** Minimum leader movement before a new trail sample is recorded; default `spacing / 8`. */
8
+ sampleDistance?: number;
9
+ }
10
+ export interface TrailPose {
11
+ position: TrailPoint;
12
+ /** Yaw (radians) of the trail direction at this point, matching engine `rotationY`. */
13
+ heading: number;
14
+ }
15
+ /**
16
+ * A live breadcrumb trail recorded from a moving leader, with followers placed by arc-length
17
+ * behind the head — conga lines, convoys, duckling chains, snake bodies. Call `record` with the
18
+ * leader's position every tick; read `followerAt(i)` for the pose `spacing * (i + 1)` behind.
19
+ */
20
+ export interface LeaderTrail {
21
+ record(position: TrailPoint): void;
22
+ /** Pose for follower `index` (0 = closest behind the leader); `null` until enough trail exists. */
23
+ followerAt(index: number): TrailPose | null;
24
+ /** Recorded arc length available behind the leader. */
25
+ length(): number;
26
+ /** Drop all history (teleport, respawn) and optionally restart from a position. */
27
+ reset(position?: TrailPoint): void;
28
+ }
29
+ export declare function createLeaderTrail(config: LeaderTrailConfig): LeaderTrail;
@@ -0,0 +1,67 @@
1
+ export function createLeaderTrail(config) {
2
+ if (!(config.spacing > 0))
3
+ throw new Error(`leader trail spacing must be positive, got ${config.spacing}`);
4
+ const spacing = config.spacing;
5
+ const maxFollowers = Math.max(1, config.maxFollowers ?? 16);
6
+ const sampleDistance = config.sampleDistance ?? spacing / 8;
7
+ const capacity = spacing * (maxFollowers + 1);
8
+ let points = [];
9
+ let lengths = [];
10
+ let total = 0;
11
+ function trim() {
12
+ while (total > capacity && lengths.length > 0) {
13
+ const tailSegment = lengths[0];
14
+ if (total - tailSegment < capacity)
15
+ break;
16
+ lengths.shift();
17
+ points.shift();
18
+ total -= tailSegment;
19
+ }
20
+ }
21
+ return {
22
+ record(position) {
23
+ const head = points[points.length - 1];
24
+ if (head === undefined) {
25
+ points.push(position);
26
+ return;
27
+ }
28
+ const step = Math.hypot(position[0] - head[0], position[1] - head[1], position[2] - head[2]);
29
+ if (step < sampleDistance)
30
+ return;
31
+ points.push(position);
32
+ lengths.push(step);
33
+ total += step;
34
+ trim();
35
+ },
36
+ followerAt(index) {
37
+ const behind = spacing * (index + 1);
38
+ if (behind > total)
39
+ return null;
40
+ let remaining = behind;
41
+ for (let i = lengths.length - 1; i >= 0; i -= 1) {
42
+ const segment = lengths[i];
43
+ const to = points[i + 1];
44
+ const from = points[i];
45
+ if (remaining <= segment) {
46
+ const fraction = segment <= 1e-9 ? 0 : remaining / segment;
47
+ return {
48
+ position: [
49
+ to[0] + (from[0] - to[0]) * fraction,
50
+ to[1] + (from[1] - to[1]) * fraction,
51
+ to[2] + (from[2] - to[2]) * fraction,
52
+ ],
53
+ heading: Math.atan2(to[0] - from[0], to[2] - from[2]),
54
+ };
55
+ }
56
+ remaining -= segment;
57
+ }
58
+ return null;
59
+ },
60
+ length: () => total,
61
+ reset(position) {
62
+ points = position === undefined ? [] : [position];
63
+ lengths = [];
64
+ total = 0;
65
+ },
66
+ };
67
+ }
@@ -6,8 +6,9 @@
6
6
  * integration. Keeping it framework-free means the kinematics controller,
7
7
  * the keyboard handler and the on-screen touch D-pad all speak the same
8
8
  * vocabulary, and the translation is unit testable without a renderer.
9
- * DOM capture, pointer lock, and the per-frame look channel live in
10
- * movementInput.ts.
9
+ * DOM capture lives in `@jgengine/shell`'s GamePlayerShell (keyboard and
10
+ * touch controls) and its camera rigs (pointer lock, the per-frame look
11
+ * channel).
11
12
  */
12
13
  export type MovementKey = "w" | "a" | "s" | "d" | "shift" | "control" | "c" | "space";
13
14
  export type MovementKeysState = Record<MovementKey, boolean>;
@@ -79,6 +80,14 @@ export interface MovementFrameStep {
79
80
  stepX: number;
80
81
  stepZ: number;
81
82
  }
83
+ /**
84
+ * Per-game overrides for the gravity/jump feel, sourced from
85
+ * `GameDefinition.physics`. Omitted fields fall back to {@link MOVEMENT_TUNING}.
86
+ */
87
+ export interface MovementTuningOverrides {
88
+ gravityAcceleration?: number;
89
+ jumpVelocity?: number;
90
+ }
82
91
  /**
83
92
  * Advance one frame of avatar kinematics. Mutates `motion` (velocity, jump,
84
93
  * gravity, grounded) and returns the horizontal step to commit through the
@@ -89,7 +98,31 @@ export interface MovementFrameStep {
89
98
  * this out of the render loop is what lets us assert "held forward for N frames
90
99
  * crosses the room" in a unit test instead of only in the live game.
91
100
  */
92
- export declare function advancePlayerMotion(motion: PlayerMotionState, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number): MovementFrameStep;
101
+ export declare function advancePlayerMotion(motion: PlayerMotionState, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, tuning?: MovementTuningOverrides): MovementFrameStep;
102
+ /** A placed scene object the walking player collides against as a circle-vs-AABB obstacle. */
103
+ export interface CollisionObstacle {
104
+ position: readonly [number, number, number];
105
+ /** Box half-size on each axis. Default `[0.5, 0.5, 0.5]` (unit-box scene objects). */
106
+ halfExtents?: readonly [number, number, number];
107
+ }
108
+ /**
109
+ * Resolve a horizontal step against nearby placed objects with classic
110
+ * axis-separated sliding (try X, then Z against the post-X position) so
111
+ * walking into a wall stops the blocked axis but keeps sliding along the
112
+ * other. Obstacles are treated as circle-vs-AABB in the X/Z plane, inflated
113
+ * by `playerRadius`; an obstacle whose vertical span misses the player's
114
+ * feet-to-head span is skipped entirely. Callers should pre-filter to nearby
115
+ * objects — this also early-exits per obstacle on horizontal distance.
116
+ */
117
+ export declare function resolveObstacleStep(current: readonly [number, number, number], stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], playerRadius?: number): MovementFrameStep;
118
+ /** Zeroes the off-axis component so travel is locked to a single world axis (`PlayerMovementConfig.mode: "axis"`). */
119
+ export declare function constrainStepToAxis(stepX: number, stepZ: number, axis: "x" | "z"): MovementFrameStep;
120
+ /**
121
+ * Snap a world position to its containing cell's center, matching the
122
+ * bounds-free form of `navGrid`'s and `tacticalGrid`'s `floor` + half-cell
123
+ * convention (`PlayerMovementConfig.mode: "grid"`).
124
+ */
125
+ export declare function snapPositionToGrid(x: number, z: number, cellSize: number): [number, number];
93
126
  /** Peak jump height from MOVEMENT_TUNING.jumpVelocity + gravity, with small buffer. */
94
127
  export declare const MAX_JUMP_OFFSET = 1.15;
95
128
  /** Camera yaw looks along -Z; character mesh faces +Z at body rotation.y = 0. */
@@ -6,8 +6,9 @@
6
6
  * integration. Keeping it framework-free means the kinematics controller,
7
7
  * the keyboard handler and the on-screen touch D-pad all speak the same
8
8
  * vocabulary, and the translation is unit testable without a renderer.
9
- * DOM capture, pointer lock, and the per-frame look channel live in
10
- * movementInput.ts.
9
+ * DOM capture lives in `@jgengine/shell`'s GamePlayerShell (keyboard and
10
+ * touch controls) and its camera rigs (pointer lock, the per-frame look
11
+ * channel).
11
12
  */
12
13
  export function createEmptyMovementKeys() {
13
14
  return { w: false, a: false, s: false, d: false, shift: false, control: false, c: false, space: false };
@@ -98,9 +99,11 @@ export function createPlayerMotionState() {
98
99
  * this out of the render loop is what lets us assert "held forward for N frames
99
100
  * crosses the room" in a unit test instead of only in the live game.
100
101
  */
101
- export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds) {
102
+ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, tuning) {
102
103
  const deltaSeconds = Math.min(rawDeltaSeconds, MOVEMENT_TUNING.maxFrameSeconds);
103
104
  const targetSpeed = resolveTargetSpeed(intent, baseSpeed);
105
+ const gravityAcceleration = tuning?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration;
106
+ const jumpVelocity = tuning?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity;
104
107
  let targetVelocityX = 0;
105
108
  let targetVelocityZ = 0;
106
109
  if (intent.moving) {
@@ -145,12 +148,12 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
145
148
  }
146
149
  const jumpPressed = intent.jumping;
147
150
  if (jumpPressed && !motion.jumpHeld && motion.grounded && !intent.crouching) {
148
- motion.verticalVelocity = MOVEMENT_TUNING.jumpVelocity;
151
+ motion.verticalVelocity = jumpVelocity;
149
152
  motion.grounded = false;
150
153
  }
151
154
  motion.jumpHeld = jumpPressed;
152
155
  if (!motion.grounded || motion.verticalVelocity > 0) {
153
- motion.verticalVelocity -= MOVEMENT_TUNING.gravityAcceleration * deltaSeconds;
156
+ motion.verticalVelocity -= gravityAcceleration * deltaSeconds;
154
157
  motion.jumpOffset += motion.verticalVelocity * deltaSeconds;
155
158
  if (motion.jumpOffset <= 0) {
156
159
  motion.jumpOffset = 0;
@@ -163,6 +166,79 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
163
166
  stepZ: motion.horizontalVelocityZ * deltaSeconds,
164
167
  };
165
168
  }
169
+ const DEFAULT_OBSTACLE_HALF_EXTENTS = [0.5, 0.5, 0.5];
170
+ const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
171
+ /** Feet-to-head span used for the obstacle's vertical overlap test; matches DEFAULT_VOXEL_DIMS.height. */
172
+ const OBSTACLE_PLAYER_HEIGHT = 1.8;
173
+ function clampAxisStep(originalCoord, step, min, max) {
174
+ if (step === 0)
175
+ return 0;
176
+ const target = originalCoord + step;
177
+ if (target <= min || target >= max)
178
+ return step;
179
+ if (step > 0)
180
+ return originalCoord <= min ? min - originalCoord : 0;
181
+ return originalCoord >= max ? max - originalCoord : 0;
182
+ }
183
+ /**
184
+ * Resolve a horizontal step against nearby placed objects with classic
185
+ * axis-separated sliding (try X, then Z against the post-X position) so
186
+ * walking into a wall stops the blocked axis but keeps sliding along the
187
+ * other. Obstacles are treated as circle-vs-AABB in the X/Z plane, inflated
188
+ * by `playerRadius`; an obstacle whose vertical span misses the player's
189
+ * feet-to-head span is skipped entirely. Callers should pre-filter to nearby
190
+ * objects — this also early-exits per obstacle on horizontal distance.
191
+ */
192
+ export function resolveObstacleStep(current, stepX, stepZ, obstacles, playerRadius = DEFAULT_OBSTACLE_PLAYER_RADIUS) {
193
+ const feetY = current[1];
194
+ const headY = feetY + OBSTACLE_PLAYER_HEIGHT;
195
+ const reachX = Math.abs(stepX) + playerRadius;
196
+ const reachZ = Math.abs(stepZ) + playerRadius;
197
+ const nearby = [];
198
+ for (const obstacle of obstacles) {
199
+ const halfExtents = obstacle.halfExtents ?? DEFAULT_OBSTACLE_HALF_EXTENTS;
200
+ const obstacleBottom = obstacle.position[1] - halfExtents[1];
201
+ const obstacleTop = obstacle.position[1] + halfExtents[1];
202
+ if (obstacleTop <= feetY || obstacleBottom >= headY)
203
+ continue;
204
+ const dx = obstacle.position[0] - current[0];
205
+ const dz = obstacle.position[2] - current[2];
206
+ if (Math.abs(dx) > halfExtents[0] + reachX || Math.abs(dz) > halfExtents[2] + reachZ)
207
+ continue;
208
+ nearby.push({
209
+ minX: obstacle.position[0] - halfExtents[0] - playerRadius,
210
+ maxX: obstacle.position[0] + halfExtents[0] + playerRadius,
211
+ minZ: obstacle.position[2] - halfExtents[2] - playerRadius,
212
+ maxZ: obstacle.position[2] + halfExtents[2] + playerRadius,
213
+ });
214
+ }
215
+ let resultX = stepX;
216
+ for (const box of nearby) {
217
+ if (current[2] <= box.minZ || current[2] >= box.maxZ)
218
+ continue;
219
+ resultX = clampAxisStep(current[0], resultX, box.minX, box.maxX);
220
+ }
221
+ const nextX = current[0] + resultX;
222
+ let resultZ = stepZ;
223
+ for (const box of nearby) {
224
+ if (nextX <= box.minX || nextX >= box.maxX)
225
+ continue;
226
+ resultZ = clampAxisStep(current[2], resultZ, box.minZ, box.maxZ);
227
+ }
228
+ return { stepX: resultX, stepZ: resultZ };
229
+ }
230
+ /** Zeroes the off-axis component so travel is locked to a single world axis (`PlayerMovementConfig.mode: "axis"`). */
231
+ export function constrainStepToAxis(stepX, stepZ, axis) {
232
+ return axis === "x" ? { stepX, stepZ: 0 } : { stepX: 0, stepZ };
233
+ }
234
+ /**
235
+ * Snap a world position to its containing cell's center, matching the
236
+ * bounds-free form of `navGrid`'s and `tacticalGrid`'s `floor` + half-cell
237
+ * convention (`PlayerMovementConfig.mode: "grid"`).
238
+ */
239
+ export function snapPositionToGrid(x, z, cellSize) {
240
+ return [(Math.floor(x / cellSize) + 0.5) * cellSize, (Math.floor(z / cellSize) + 0.5) * cellSize];
241
+ }
166
242
  /** Peak jump height from MOVEMENT_TUNING.jumpVelocity + gravity, with small buffer. */
167
243
  export const MAX_JUMP_OFFSET = 1.15;
168
244
  /** Camera yaw looks along -Z; character mesh faces +Z at body rotation.y = 0. */