@jgengine/core 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +2 -0
  3. package/dist/ai/crowd.d.ts +36 -0
  4. package/dist/ai/crowd.js +213 -0
  5. package/dist/ai/jobBoard.d.ts +49 -0
  6. package/dist/ai/jobBoard.js +155 -0
  7. package/dist/ai/spawnDirector.d.ts +61 -0
  8. package/dist/ai/spawnDirector.js +162 -0
  9. package/dist/ai/threat.d.ts +25 -0
  10. package/dist/ai/threat.js +67 -0
  11. package/dist/anim/easing.d.ts +14 -0
  12. package/dist/anim/easing.js +55 -0
  13. package/dist/audio/audioFalloff.d.ts +38 -0
  14. package/dist/audio/audioFalloff.js +32 -0
  15. package/dist/behaviour/behaviour.d.ts +136 -0
  16. package/dist/behaviour/behaviour.js +388 -0
  17. package/dist/board/laneBoard.d.ts +63 -0
  18. package/dist/board/laneBoard.js +114 -0
  19. package/dist/board/timelineBoard.d.ts +36 -0
  20. package/dist/board/timelineBoard.js +70 -0
  21. package/dist/cards/cardPile.d.ts +64 -0
  22. package/dist/cards/cardPile.js +175 -0
  23. package/dist/cards/modifierPipeline.d.ts +33 -0
  24. package/dist/cards/modifierPipeline.js +48 -0
  25. package/dist/combat/abilityKit.d.ts +47 -0
  26. package/dist/combat/abilityKit.js +139 -0
  27. package/dist/combat/animationState.d.ts +47 -0
  28. package/dist/combat/animationState.js +101 -0
  29. package/dist/combat/attackTags.d.ts +14 -0
  30. package/dist/combat/attackTags.js +26 -0
  31. package/dist/combat/breakMeters.d.ts +32 -0
  32. package/dist/combat/breakMeters.js +51 -0
  33. package/dist/combat/comboString.d.ts +35 -0
  34. package/dist/combat/comboString.js +59 -0
  35. package/dist/combat/death.d.ts +8 -0
  36. package/dist/combat/death.js +5 -2
  37. package/dist/combat/defensiveWindow.d.ts +34 -0
  38. package/dist/combat/defensiveWindow.js +66 -0
  39. package/dist/combat/downed.d.ts +35 -0
  40. package/dist/combat/downed.js +104 -0
  41. package/dist/combat/effects.d.ts +12 -1
  42. package/dist/combat/effects.js +31 -19
  43. package/dist/combat/hitReaction.d.ts +23 -0
  44. package/dist/combat/hitReaction.js +20 -0
  45. package/dist/combat/projectiles.d.ts +42 -5
  46. package/dist/combat/projectiles.js +98 -12
  47. package/dist/combat/resistance.d.ts +13 -0
  48. package/dist/combat/resistance.js +46 -0
  49. package/dist/combat/telegraph.d.ts +31 -0
  50. package/dist/combat/telegraph.js +51 -0
  51. package/dist/commands/commandRegistry.d.ts +1 -1
  52. package/dist/commands/commandRegistry.js +2 -1
  53. package/dist/crafting/crop.d.ts +59 -0
  54. package/dist/crafting/crop.js +181 -0
  55. package/dist/crafting/production.d.ts +64 -0
  56. package/dist/crafting/production.js +130 -0
  57. package/dist/crafting/recipe.d.ts +62 -0
  58. package/dist/crafting/recipe.js +101 -0
  59. package/dist/data/dataSource.d.ts +26 -0
  60. package/dist/data/dataSource.js +93 -0
  61. package/dist/data/devProxy.d.ts +11 -0
  62. package/dist/data/devProxy.js +38 -0
  63. package/dist/data/fetchJson.d.ts +20 -0
  64. package/dist/data/fetchJson.js +43 -0
  65. package/dist/data/jsonDataSource.d.ts +4 -0
  66. package/dist/data/jsonDataSource.js +6 -0
  67. package/dist/devtools/devtools.d.ts +157 -0
  68. package/dist/devtools/devtools.js +484 -0
  69. package/dist/devtools/transformTunables.d.ts +14 -0
  70. package/dist/devtools/transformTunables.js +38 -0
  71. package/dist/economy/sharedWallet.d.ts +28 -0
  72. package/dist/economy/sharedWallet.js +62 -0
  73. package/dist/economy/techTree.d.ts +40 -0
  74. package/dist/economy/techTree.js +66 -0
  75. package/dist/game/chat.d.ts +66 -0
  76. package/dist/game/chat.js +165 -0
  77. package/dist/game/cosmetics.d.ts +25 -0
  78. package/dist/game/cosmetics.js +51 -0
  79. package/dist/game/defineGame.d.ts +10 -2
  80. package/dist/game/defineGame.js +2 -1
  81. package/dist/game/events.d.ts +87 -0
  82. package/dist/game/levelSequence.d.ts +44 -0
  83. package/dist/game/levelSequence.js +78 -0
  84. package/dist/game/lootFilter.d.ts +33 -0
  85. package/dist/game/lootFilter.js +48 -0
  86. package/dist/game/ping.d.ts +67 -0
  87. package/dist/game/ping.js +86 -0
  88. package/dist/game/playableGame.d.ts +516 -8
  89. package/dist/game/playableGame.js +6 -1
  90. package/dist/game/progression.d.ts +1 -0
  91. package/dist/game/progression.js +23 -2
  92. package/dist/game/race.d.ts +100 -0
  93. package/dist/game/race.js +236 -0
  94. package/dist/game/runDraft.d.ts +34 -0
  95. package/dist/game/runDraft.js +97 -0
  96. package/dist/game/social.d.ts +69 -0
  97. package/dist/game/social.js +138 -0
  98. package/dist/game/spawnPoints.d.ts +19 -0
  99. package/dist/game/spawnPoints.js +23 -0
  100. package/dist/game/worldItem.d.ts +95 -0
  101. package/dist/game/worldItem.js +138 -0
  102. package/dist/input/actionBindings.d.ts +10 -0
  103. package/dist/input/actionBindings.js +14 -0
  104. package/dist/input/axisInput.d.ts +43 -0
  105. package/dist/input/axisInput.js +70 -0
  106. package/dist/input/gestureSurface.d.ts +31 -0
  107. package/dist/input/gestureSurface.js +120 -0
  108. package/dist/input/pointer.d.ts +69 -0
  109. package/dist/input/pointer.js +65 -0
  110. package/dist/input/touchScheme.d.ts +83 -0
  111. package/dist/input/touchScheme.js +115 -0
  112. package/dist/interaction/contextMenu.d.ts +30 -0
  113. package/dist/interaction/contextMenu.js +22 -0
  114. package/dist/interaction/qte.d.ts +20 -0
  115. package/dist/interaction/qte.js +24 -0
  116. package/dist/interaction/skillCheck.d.ts +20 -0
  117. package/dist/interaction/skillCheck.js +26 -0
  118. package/dist/inventory/shapedGrid.d.ts +56 -0
  119. package/dist/inventory/shapedGrid.js +176 -0
  120. package/dist/inventory/storageTier.d.ts +52 -0
  121. package/dist/inventory/storageTier.js +77 -0
  122. package/dist/item/affix.d.ts +54 -0
  123. package/dist/item/affix.js +115 -0
  124. package/dist/item/durability.d.ts +51 -0
  125. package/dist/item/durability.js +78 -0
  126. package/dist/item/modularItem.d.ts +46 -0
  127. package/dist/item/modularItem.js +75 -0
  128. package/dist/meta/changelog.d.ts +1 -1
  129. package/dist/meta/changelog.js +71 -1
  130. package/dist/movement/dash.d.ts +37 -0
  131. package/dist/movement/dash.js +65 -0
  132. package/dist/movement/movementModel.d.ts +36 -3
  133. package/dist/movement/movementModel.js +81 -5
  134. package/dist/movement/voxelController.d.ts +61 -0
  135. package/dist/movement/voxelController.js +229 -0
  136. package/dist/multiplayer/chatContract.d.ts +42 -0
  137. package/dist/multiplayer/chatContract.js +57 -0
  138. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  139. package/dist/multiplayer/combatSnapshot.js +89 -0
  140. package/dist/multiplayer/identity.d.ts +13 -0
  141. package/dist/multiplayer/identity.js +20 -0
  142. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  143. package/dist/multiplayer/lagCompensation.js +154 -0
  144. package/dist/multiplayer/matchmaking.d.ts +33 -0
  145. package/dist/multiplayer/matchmaking.js +60 -0
  146. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  147. package/dist/multiplayer/poseSyncGate.js +13 -1
  148. package/dist/multiplayer/presenceModel.d.ts +3 -0
  149. package/dist/multiplayer/presenceModel.js +15 -1
  150. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  151. package/dist/multiplayer/simultaneousCommit.js +53 -0
  152. package/dist/multiplayer/voiceContract.d.ts +44 -0
  153. package/dist/multiplayer/voiceContract.js +108 -0
  154. package/dist/nav/navConstrain.d.ts +14 -0
  155. package/dist/nav/navConstrain.js +26 -0
  156. package/dist/nav/navFromEnvironment.d.ts +11 -0
  157. package/dist/nav/navFromEnvironment.js +16 -0
  158. package/dist/nav/navGrid.d.ts +56 -0
  159. package/dist/nav/navGrid.js +265 -0
  160. package/dist/nav/pathFollow.d.ts +31 -0
  161. package/dist/nav/pathFollow.js +69 -0
  162. package/dist/physics/ballisticSweep.d.ts +18 -0
  163. package/dist/physics/ballisticSweep.js +49 -0
  164. package/dist/physics/buoyancy.d.ts +49 -0
  165. package/dist/physics/buoyancy.js +112 -0
  166. package/dist/physics/carryable.d.ts +46 -0
  167. package/dist/physics/carryable.js +110 -0
  168. package/dist/physics/damageZones.d.ts +56 -0
  169. package/dist/physics/damageZones.js +107 -0
  170. package/dist/physics/forceVolume.d.ts +48 -0
  171. package/dist/physics/forceVolume.js +119 -0
  172. package/dist/physics/physicsWorld.d.ts +124 -2
  173. package/dist/physics/physicsWorld.js +493 -41
  174. package/dist/physics/ragdoll.d.ts +49 -0
  175. package/dist/physics/ragdoll.js +103 -0
  176. package/dist/physics/spatialGrid.d.ts +41 -0
  177. package/dist/physics/spatialGrid.js +143 -0
  178. package/dist/physics/structure.d.ts +83 -0
  179. package/dist/physics/structure.js +0 -0
  180. package/dist/physics/traversal.d.ts +75 -0
  181. package/dist/physics/traversal.js +138 -0
  182. package/dist/physics/vehicleBody.d.ts +99 -0
  183. package/dist/physics/vehicleBody.js +233 -0
  184. package/dist/puzzle/cellGrid.d.ts +26 -0
  185. package/dist/puzzle/cellGrid.js +110 -0
  186. package/dist/puzzle/fallingPiece.d.ts +24 -0
  187. package/dist/puzzle/fallingPiece.js +51 -0
  188. package/dist/random/nameGen.d.ts +18 -0
  189. package/dist/random/nameGen.js +35 -0
  190. package/dist/random/rng.d.ts +2 -0
  191. package/dist/random/rng.js +23 -0
  192. package/dist/runtime/adapter.d.ts +35 -0
  193. package/dist/runtime/adapter.js +37 -1
  194. package/dist/runtime/cameraDirector.d.ts +10 -0
  195. package/dist/runtime/cameraDirector.js +25 -0
  196. package/dist/runtime/gameContext.d.ts +107 -3
  197. package/dist/runtime/gameContext.js +253 -7
  198. package/dist/runtime/hostPersistence.d.ts +39 -0
  199. package/dist/runtime/hostPersistence.js +54 -0
  200. package/dist/runtime/inputSnapshot.d.ts +7 -0
  201. package/dist/runtime/inputSnapshot.js +12 -0
  202. package/dist/runtime/motionIntents.d.ts +25 -0
  203. package/dist/runtime/motionIntents.js +25 -0
  204. package/dist/runtime/persistenceScope.d.ts +32 -0
  205. package/dist/runtime/persistenceScope.js +64 -0
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/scene/autoTarget.d.ts +15 -0
  208. package/dist/scene/autoTarget.js +54 -0
  209. package/dist/scene/behaviors.d.ts +13 -1
  210. package/dist/scene/behaviors.js +3 -0
  211. package/dist/scene/captureCheck.d.ts +7 -0
  212. package/dist/scene/captureCheck.js +11 -0
  213. package/dist/scene/entityStore.d.ts +9 -1
  214. package/dist/scene/entityStore.js +25 -2
  215. package/dist/scene/form.d.ts +40 -0
  216. package/dist/scene/form.js +56 -0
  217. package/dist/scene/mount.d.ts +62 -0
  218. package/dist/scene/mount.js +107 -0
  219. package/dist/scene/movementSpeed.d.ts +35 -0
  220. package/dist/scene/movementSpeed.js +20 -0
  221. package/dist/scene/objectQuery.d.ts +18 -0
  222. package/dist/scene/objectQuery.js +108 -0
  223. package/dist/scene/objectStore.d.ts +11 -0
  224. package/dist/scene/objectStore.js +109 -4
  225. package/dist/scene/paintLayer.d.ts +15 -0
  226. package/dist/scene/paintLayer.js +53 -0
  227. package/dist/scene/possession.d.ts +30 -0
  228. package/dist/scene/possession.js +51 -0
  229. package/dist/scene/roster.d.ts +25 -0
  230. package/dist/scene/roster.js +57 -0
  231. package/dist/scene/selection.d.ts +30 -0
  232. package/dist/scene/selection.js +38 -0
  233. package/dist/scene/spatial.d.ts +21 -0
  234. package/dist/scene/spatial.js +63 -1
  235. package/dist/scene/stationClaim.d.ts +43 -0
  236. package/dist/scene/stationClaim.js +109 -0
  237. package/dist/sensor/concealment.d.ts +21 -0
  238. package/dist/sensor/concealment.js +63 -0
  239. package/dist/sensor/freezeMonitor.d.ts +17 -0
  240. package/dist/sensor/freezeMonitor.js +35 -0
  241. package/dist/sensor/frustumSensor.d.ts +60 -0
  242. package/dist/sensor/frustumSensor.js +104 -0
  243. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  244. package/dist/sensor/hiddenStateProbe.js +40 -0
  245. package/dist/sensor/recordingBuffer.d.ts +29 -0
  246. package/dist/sensor/recordingBuffer.js +61 -0
  247. package/dist/sensor/revealQuery.d.ts +22 -0
  248. package/dist/sensor/revealQuery.js +36 -0
  249. package/dist/session/contestedChannel.d.ts +36 -0
  250. package/dist/session/contestedChannel.js +100 -0
  251. package/dist/session/extraction.d.ts +54 -0
  252. package/dist/session/extraction.js +108 -0
  253. package/dist/session/ring.d.ts +36 -0
  254. package/dist/session/ring.js +88 -0
  255. package/dist/session/roles.d.ts +6 -0
  256. package/dist/session/roles.js +32 -0
  257. package/dist/session/roundState.d.ts +79 -0
  258. package/dist/session/roundState.js +158 -0
  259. package/dist/stats/accumulatorMeter.d.ts +31 -0
  260. package/dist/stats/accumulatorMeter.js +81 -0
  261. package/dist/stats/eventMeter.d.ts +34 -0
  262. package/dist/stats/eventMeter.js +76 -0
  263. package/dist/stats/rollCheck.d.ts +15 -0
  264. package/dist/stats/rollCheck.js +15 -0
  265. package/dist/survival/decayMeter.d.ts +59 -0
  266. package/dist/survival/decayMeter.js +95 -0
  267. package/dist/survival/moodle.d.ts +48 -0
  268. package/dist/survival/moodle.js +91 -0
  269. package/dist/survival/regionHealth.d.ts +72 -0
  270. package/dist/survival/regionHealth.js +129 -0
  271. package/dist/tactics/fallingGrid.d.ts +62 -0
  272. package/dist/tactics/fallingGrid.js +158 -0
  273. package/dist/tactics/predictiveQuery.d.ts +34 -0
  274. package/dist/tactics/predictiveQuery.js +44 -0
  275. package/dist/tactics/snapshot.d.ts +18 -0
  276. package/dist/tactics/snapshot.js +65 -0
  277. package/dist/tactics/surface.d.ts +52 -0
  278. package/dist/tactics/surface.js +135 -0
  279. package/dist/tactics/tacticalGrid.d.ts +68 -0
  280. package/dist/tactics/tacticalGrid.js +261 -0
  281. package/dist/time/beatClock.d.ts +40 -0
  282. package/dist/time/beatClock.js +73 -0
  283. package/dist/time/simClock.d.ts +25 -1
  284. package/dist/time/simClock.js +28 -3
  285. package/dist/turn/commit.d.ts +27 -0
  286. package/dist/turn/commit.js +72 -0
  287. package/dist/turn/intent.d.ts +14 -0
  288. package/dist/turn/intent.js +29 -0
  289. package/dist/turn/turnLoop.d.ts +60 -0
  290. package/dist/turn/turnLoop.js +223 -0
  291. package/dist/world/buildPermissions.d.ts +47 -0
  292. package/dist/world/buildPermissions.js +134 -0
  293. package/dist/world/carve.d.ts +105 -0
  294. package/dist/world/carve.js +175 -0
  295. package/dist/world/connectors.d.ts +40 -0
  296. package/dist/world/connectors.js +67 -0
  297. package/dist/world/envField.d.ts +71 -0
  298. package/dist/world/envField.js +96 -0
  299. package/dist/world/environmentSummary.d.ts +53 -0
  300. package/dist/world/environmentSummary.js +129 -0
  301. package/dist/world/features.d.ts +87 -6
  302. package/dist/world/features.js +52 -2
  303. package/dist/world/fog.d.ts +40 -0
  304. package/dist/world/fog.js +137 -0
  305. package/dist/world/gridInstances.d.ts +13 -0
  306. package/dist/world/gridInstances.js +22 -0
  307. package/dist/world/markers.d.ts +52 -0
  308. package/dist/world/markers.js +96 -0
  309. package/dist/world/minimap.d.ts +42 -0
  310. package/dist/world/minimap.js +72 -0
  311. package/dist/world/placedStructureStore.d.ts +39 -0
  312. package/dist/world/placedStructureStore.js +95 -0
  313. package/dist/world/placementController.d.ts +69 -0
  314. package/dist/world/placementController.js +153 -0
  315. package/dist/world/realm.d.ts +61 -0
  316. package/dist/world/realm.js +66 -0
  317. package/dist/world/support.d.ts +27 -0
  318. package/dist/world/support.js +75 -0
  319. package/dist/world/terraform.d.ts +54 -0
  320. package/dist/world/terraform.js +186 -0
  321. package/dist/world/terrain.d.ts +23 -1
  322. package/dist/world/terrain.js +64 -1
  323. package/dist/world/voxelField.d.ts +54 -0
  324. package/dist/world/voxelField.js +241 -0
  325. package/dist/world/walls.d.ts +61 -0
  326. package/dist/world/walls.js +221 -0
  327. package/dist/world/weather.d.ts +92 -0
  328. package/dist/world/weather.js +169 -0
  329. package/llms.txt +4102 -0
  330. package/package.json +3 -2
@@ -0,0 +1,75 @@
1
+ export function slotById(def, slotId) {
2
+ return def.slots.find((s) => s.id === slotId) ?? null;
3
+ }
4
+ export function slotAccepts(slot, category) {
5
+ return typeof slot.accepts === "string" ? slot.accepts === category : slot.accepts.includes(category);
6
+ }
7
+ export function install(def, installed, slotId, part) {
8
+ const slot = slotById(def, slotId);
9
+ if (slot === null)
10
+ return { status: "rejected", reason: "unknown-slot" };
11
+ if (!slotAccepts(slot, part.category))
12
+ return { status: "rejected", reason: "wrong-category" };
13
+ if (installed.some((i) => i.slotId === slotId))
14
+ return { status: "rejected", reason: "slot-occupied" };
15
+ return { status: "ok", installed: [...installed, { slotId, part }] };
16
+ }
17
+ export function uninstall(installed, slotId) {
18
+ return installed.filter((i) => i.slotId !== slotId);
19
+ }
20
+ export function partInSlot(installed, slotId) {
21
+ return installed.find((i) => i.slotId === slotId)?.part ?? null;
22
+ }
23
+ export function computeEffectiveStats(def, installed) {
24
+ const stats = { ...def.baseStats };
25
+ for (const { part } of installed) {
26
+ if (part.stats === undefined)
27
+ continue;
28
+ for (const [key, value] of Object.entries(part.stats))
29
+ stats[key] = (stats[key] ?? 0) + value;
30
+ }
31
+ for (const { part } of installed) {
32
+ if (part.multipliers === undefined)
33
+ continue;
34
+ for (const [key, factor] of Object.entries(part.multipliers))
35
+ stats[key] = (stats[key] ?? 0) * factor;
36
+ }
37
+ return stats;
38
+ }
39
+ export function missingRequiredSlots(def, installed) {
40
+ return def.slots.filter((s) => s.required === true && !installed.some((i) => i.slotId === s.id)).map((s) => s.id);
41
+ }
42
+ export function isComplete(def, installed) {
43
+ return missingRequiredSlots(def, installed).length === 0;
44
+ }
45
+ export function createModularItem(def, initial = []) {
46
+ let installed = initial;
47
+ return {
48
+ def,
49
+ parts() {
50
+ return installed;
51
+ },
52
+ install(slotId, part) {
53
+ const result = install(def, installed, slotId, part);
54
+ if (result.status === "ok")
55
+ installed = result.installed;
56
+ return result;
57
+ },
58
+ uninstall(slotId) {
59
+ installed = uninstall(installed, slotId);
60
+ return installed;
61
+ },
62
+ partInSlot(slotId) {
63
+ return partInSlot(installed, slotId);
64
+ },
65
+ effectiveStats() {
66
+ return computeEffectiveStats(def, installed);
67
+ },
68
+ missingRequired() {
69
+ return missingRequiredSlots(def, installed);
70
+ },
71
+ isComplete() {
72
+ return isComplete(def, installed);
73
+ },
74
+ };
75
+ }
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.6.0";
1
+ export declare const VERSION = "0.8.0";
2
2
  export interface ChangelogEntry {
3
3
  migrate: readonly string[];
4
4
  added: readonly string[];
@@ -1,5 +1,75 @@
1
- export const VERSION = "0.6.0";
1
+ export const VERSION = "0.8.0";
2
2
  export const CHANGELOG = {
3
+ "0.8.0": {
4
+ migrate: [
5
+ "Bump every @jgengine/* dependency to ^0.8.0 (the eight packages version in lockstep).",
6
+ "Additive only, except for gameui — every other 0.7.0 API is unchanged; opt into any of the below by importing it directly.",
7
+ "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.",
8
+ "leveling({ thresholdMode: 'cumulative' }) is opt-in; the default 'perLevel' behavior is unchanged.",
9
+ "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.",
10
+ ],
11
+ added: [
12
+ "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.",
13
+ "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.",
14
+ "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.",
15
+ "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.",
16
+ "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.",
17
+ "ws() (@jgengine/core/runtime/adapter) gained an optional url field, carried through by resolveShellMultiplayer.",
18
+ "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).",
19
+ "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).",
20
+ "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).",
21
+ "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).",
22
+ "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).",
23
+ "Declared-action intent board — turn/intent createIntentBoard for one-turn-ahead intents (Slay-the-Spire style): declare/peek/all/consume/clear (#168).",
24
+ "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).",
25
+ "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).",
26
+ "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).",
27
+ "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).",
28
+ "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).",
29
+ "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).",
30
+ "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).",
31
+ "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).",
32
+ "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).",
33
+ ],
34
+ changed: [],
35
+ removed: [
36
+ "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.",
37
+ ],
38
+ },
39
+ "0.7.0": {
40
+ migrate: [
41
+ "Bump every @jgengine/* dependency to ^0.7.0 (the eight packages version in lockstep).",
42
+ "0.7.0 is additive — every 0.6.0 API is unchanged, so no code change is required to upgrade; existing games keep the orbit/first-person camera, single-player-entity control, and every existing primitive exactly as before.",
43
+ "Opt into any new system by importing it directly: a camera rig via camera.rig; a sensor/* probe with @jgengine/shell/vision renderers; an ai/* director over the nav/ navmesh; turn/* + tactics/* for turn-based games; cards/* + board/* for deckbuilders; crafting/* for recipes/production/farming; survival/* + world/{envField,weather,realm} for survival; combat/{abilityKit,animationState,defensiveWindow} for action feel; physics/{vehicleBody,traversal,structure,ragdoll} for vehicles/destruction; session/* for contested/round/downed/ring/extraction machines; and multiplayer/* for lag-comp/hidden-commit/matchmaking.",
44
+ "entityStore.update() now also accepts name, so possession/form can retarget an instance's catalog id without despawn/respawn.",
45
+ ],
46
+ added: [
47
+ "Navigation & pointer input — nav/navGrid (A* walkable grid), nav/pathFollow, input/pointer (PointerHit), scene/selection, interaction/contextMenu, and PlayableGame.pointer (worldHit, marquee select, right-click verbs, move/aim commands) in @jgengine/shell.",
48
+ "Camera rig library — eight rigs on PlayableGame.camera (topDown/iso, rts, shoulder, lockOn, chase + cockpit/hood/rear, orbit, first), a cinematic keyframe path, rig cross-fade, followEntityId: null, and a shared camera-shake/trauma channel (cameraShake / useCameraShake).",
49
+ "Physics constraints & actors — PhysicsWorld joints (hinge/fixed/distance/spring), onCollision gameplay-event hook, physics/ragdoll, physics/carryable, physics/forceVolume + platformCarry, and physics/spatialGrid broad-phase.",
50
+ "Vehicles, mounts & racing — physics/vehicleBody (suspension + grip curve), input/axisInput, physics/buoyancy boat, scene/mount multi-seat rideable, physics/damageZones crash stages, and game/race (raceTrack + raceState).",
51
+ "Traversal & destruction — physics/traversal (grapple/glide), world/carve (voxel carve/deposit + terrain write-back), and physics/structure (integrity graph → debris collapse).",
52
+ "AI director/behavior/crowds — ai/spawnDirector, ai/threat, ai/jobBoard, ai/crowd flow field, and a scene/behaviors patrol descriptor over the navmesh.",
53
+ "Abilities, resources & cooldowns — combat/abilityKit (four-state slots), stats/accumulatorMeter + stats/eventMeter (ult/streak), scene/autoTarget, combat/resistance matrix, game/runDraft, and @jgengine/react useAbilitySlots / useEventMeter.",
54
+ "Character combat feel — combat/animationState, combat/attackTags, combat/defensiveWindow, combat/comboString, combat/breakMeters (stagger/buildup), movement/dash, combat/hitReaction (hitstop + shake), combat/telegraph, and typed float-text styling.",
55
+ "Item & gear — item/durability, item/affix roller, item/modularItem, and inventory/storageTier (carried vs banked + delivery/insurance/consolation).",
56
+ "Objective/mode/session machines — session/contestedChannel, session/roundState, combat/downed, session/ring, session/extraction, and runtime/persistenceScope (run-vs-meta split with HostPersistence.resetScenario).",
57
+ "Turn-based & tactics — turn/turnLoop (+ action-economy pools), turn/commit (immediate/simultaneous/rewind), tactics/tacticalGrid, tactics/predictiveQuery, tactics/snapshot, tactics/surface, and shared combat/effects resolveAreaTargets.",
58
+ "Card & board stack — cards/cardPile, cards/modifierPipeline, board/laneBoard, board/timelineBoard, inventory/shapedGrid, and @jgengine/react dragLayer.",
59
+ "Crafting, tech & production — crafting/recipe graph, economy/techTree (prereq-gated unlocks), crafting/production buildings + conveyor/power, and crafting/crop farming state machine.",
60
+ "Survival & environment — survival/decayMeter, survival/moodle, survival/regionHealth, world/envField, world/weather (+ coarse fire grid), world/realm composition, and @jgengine/shell FireSpreadLayer.",
61
+ "World items & loot — game/worldItem (third scene bucket), game/lootFilter, onDeath.dropMode world scatter, PlayableGame.worldItem render binding, and @jgengine/react useWorldItems / useNearestWorldItem.",
62
+ "Placement, building & terraform — world/placementController, world/connectors, world/support, world/walls, world/placedStructureStore, world/terraform (editable terrain write-back), world/buildPermissions, and @jgengine/shell ghost/tint/brush renderers.",
63
+ "Map, HUD & ping — world/markers, world/fog, world/minimap, game/ping, @jgengine/react Minimap/Compass/WorldMap + useMarkers/useFog, and @jgengine/shell bakeTerrainMap + MapMarkerBeacons.",
64
+ "Audio & voice — audio/audioFalloff contract, time/beatClock, @jgengine/shell positional emitters (PlayableGame.audio + entitySounds/objectSounds), and @jgengine/ws voiceChannel router.",
65
+ "Interaction verbs & minigames — interaction/skillCheck, interaction/qte, scene/captureCheck + scene/roster, stats/rollCheck, dialogue skill-check gates, and @jgengine/react SkillCheckBar/QteTrack/CaptureOdds + useRoster.",
66
+ "Sensors, vision & observer — sensor/revealQuery, sensor/hiddenStateProbe, sensor/frustumSensor, sensor/recordingBuffer, the observer camera rig, and @jgengine/shell vision/replay renderers.",
67
+ "Player embodiment & expression — scene/possession, scene/form (shapeshift), game/cosmetics loadouts, and social emote broadcast over the presence layer.",
68
+ "Multiplayer depth — multiplayer/lagCompensation (position history + rewind), multiplayer/simultaneousCommit, multiplayer/combatSnapshot replay, scene/stationClaim shared-vehicle facets, economy/sharedWallet, and multiplayer/matchmaking (browse/filter/join-by-code) with @jgengine/node host support.",
69
+ ],
70
+ changed: [],
71
+ removed: [],
72
+ },
3
73
  "0.6.0": {
4
74
  migrate: [
5
75
  "Bump every @jgengine/* dependency to ^0.6.0 (the eight packages version in lockstep).",
@@ -0,0 +1,37 @@
1
+ export interface DashConfig {
2
+ distance: number;
3
+ durationMs: number;
4
+ iframes: {
5
+ fromMs: number;
6
+ toMs: number;
7
+ };
8
+ staminaCost: number;
9
+ staminaMax: number;
10
+ staminaRegenPerSecond: number;
11
+ cooldownMs: number;
12
+ }
13
+ export interface DashDirection {
14
+ x: number;
15
+ z: number;
16
+ }
17
+ export type DashRejection = {
18
+ reason: "no-stamina" | "cooldown" | "dashing";
19
+ };
20
+ export interface DashBurst {
21
+ direction: DashDirection;
22
+ durationMs: number;
23
+ distance: number;
24
+ }
25
+ export declare function iframeActive(config: DashConfig, elapsedMs: number): boolean;
26
+ export declare function dashEase(t: number): number;
27
+ export declare function dashOffset(config: DashConfig, dir: DashDirection, elapsedMs: number): [number, number, number];
28
+ export interface DashState {
29
+ tryDash(direction: DashDirection, nowMs: number): DashRejection | DashBurst;
30
+ tick(dtSeconds: number, nowMs: number): void;
31
+ stamina(): number;
32
+ staminaFraction(): number;
33
+ isDashing(nowMs: number): boolean;
34
+ isInvulnerable(nowMs: number): boolean;
35
+ offset(nowMs: number): [number, number, number];
36
+ }
37
+ export declare function createDashState(config: DashConfig): DashState;
@@ -0,0 +1,65 @@
1
+ export function iframeActive(config, elapsedMs) {
2
+ return elapsedMs >= config.iframes.fromMs && elapsedMs < config.iframes.toMs;
3
+ }
4
+ export function dashEase(t) {
5
+ const clamped = Math.max(0, Math.min(1, t));
6
+ return 1 - (1 - clamped) * (1 - clamped);
7
+ }
8
+ export function dashOffset(config, dir, elapsedMs) {
9
+ const traveled = dashEase(elapsedMs / config.durationMs) * config.distance;
10
+ const len = Math.hypot(dir.x, dir.z);
11
+ if (len === 0)
12
+ return [0, 0, 0];
13
+ return [(dir.x / len) * traveled, 0, (dir.z / len) * traveled];
14
+ }
15
+ export function createDashState(config) {
16
+ let stamina = config.staminaMax;
17
+ let dashStartMs = null;
18
+ let dashDir = { x: 0, z: 0 };
19
+ let cooldownUntil = 0;
20
+ function elapsed(nowMs) {
21
+ if (dashStartMs === null)
22
+ return null;
23
+ const dt = nowMs - dashStartMs;
24
+ return dt >= 0 && dt < config.durationMs ? dt : null;
25
+ }
26
+ return {
27
+ tryDash(direction, nowMs) {
28
+ if (elapsed(nowMs) !== null)
29
+ return { reason: "dashing" };
30
+ if (nowMs < cooldownUntil)
31
+ return { reason: "cooldown" };
32
+ if (stamina < config.staminaCost)
33
+ return { reason: "no-stamina" };
34
+ stamina -= config.staminaCost;
35
+ dashStartMs = nowMs;
36
+ dashDir = direction;
37
+ cooldownUntil = nowMs + config.cooldownMs;
38
+ return { direction, durationMs: config.durationMs, distance: config.distance };
39
+ },
40
+ tick(dtSeconds, nowMs) {
41
+ if (dashStartMs !== null && nowMs - dashStartMs >= config.durationMs)
42
+ dashStartMs = null;
43
+ stamina = Math.min(config.staminaMax, stamina + config.staminaRegenPerSecond * dtSeconds);
44
+ },
45
+ stamina() {
46
+ return stamina;
47
+ },
48
+ staminaFraction() {
49
+ return config.staminaMax <= 0 ? 0 : stamina / config.staminaMax;
50
+ },
51
+ isDashing(nowMs) {
52
+ return elapsed(nowMs) !== null;
53
+ },
54
+ isInvulnerable(nowMs) {
55
+ const dt = elapsed(nowMs);
56
+ return dt !== null && iframeActive(config, dt);
57
+ },
58
+ offset(nowMs) {
59
+ const dt = elapsed(nowMs);
60
+ if (dt === null)
61
+ return [0, 0, 0];
62
+ return dashOffset(config, dashDir, dt);
63
+ },
64
+ };
65
+ }
@@ -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. */
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Voxel character controller (pure model).
3
+ *
4
+ * The base first-person controller in `movementModel.ts` assumes flat ground at
5
+ * y=0 — it integrates gravity into a `jumpOffset` and never asks the world how
6
+ * tall the ground is. That is fine for a plaza demo but wrong for any game where
7
+ * the player walks on placed blocks and digs down into the world: they float on
8
+ * an invisible floor, clip through walls, and can never descend.
9
+ *
10
+ * This module resolves the player as an axis-aligned box against a set of solid
11
+ * unit cells (a `SolidQuery`), so the same held-keys → intent pipeline drives a
12
+ * body that stands on blocks, falls into holes, is stopped by walls, and steps
13
+ * up single-block ledges. Keeping it renderer-free means the shell can drive it
14
+ * from live scene objects while the descent/landing/step-up behaviour stays unit
15
+ * testable without three.js.
16
+ *
17
+ * Cell convention matches the shell's object rendering and the voxel raycast:
18
+ * cell (x,y,z) is the solid box X∈[x-0.5,x+0.5], Y∈[y,y+1], Z∈[z-0.5,z+0.5]. A
19
+ * block at cell y therefore has its top face at world y+1, so a player standing
20
+ * on the surface layer (cell y=-1) rests with their feet at y=0.
21
+ */
22
+ import { type MovementIntent, type MovementTuningOverrides } from "./movementModel.js";
23
+ export type SolidQuery = (x: number, y: number, z: number) => boolean;
24
+ /** Footprint + height of the player box, in world units. */
25
+ export interface VoxelPlayerDims {
26
+ /** Half the box width on both horizontal axes. */
27
+ halfWidth: number;
28
+ /** Box height measured up from the feet. */
29
+ height: number;
30
+ /** Tallest ledge the player walks up without jumping. */
31
+ stepHeight: number;
32
+ }
33
+ export declare const DEFAULT_VOXEL_DIMS: VoxelPlayerDims;
34
+ /** Mutable kinematic state carried between frames by the voxel controller. */
35
+ export interface VoxelPlayerBody {
36
+ x: number;
37
+ y: number;
38
+ z: number;
39
+ velocityX: number;
40
+ velocityY: number;
41
+ velocityZ: number;
42
+ grounded: boolean;
43
+ jumpHeld: boolean;
44
+ }
45
+ export declare function createVoxelPlayerBody(x: number, y: number, z: number): VoxelPlayerBody;
46
+ /** Samples the terrain floor height at a point; unset means flat ground plays no part in support. */
47
+ export type GroundHeightQuery = (x: number, z: number) => number;
48
+ /**
49
+ * Advance one frame of voxel-collided player kinematics. Mutates `body`
50
+ * (position, velocity, grounded) in place.
51
+ *
52
+ * `forwardX`/`forwardZ` are the camera heading projected onto the ground plane
53
+ * (need not be normalized). Pulling this out of the render loop is what lets us
54
+ * assert "dig the block underfoot and the player drops a level" in a unit test.
55
+ *
56
+ * `groundHeight`, when supplied, is an additional solid floor sampled at the
57
+ * body's (x, z): the player never sinks below it and standing on it counts as
58
+ * supported (resets jump state like landing on a solid cell). Where terrain and
59
+ * a solid cell's top disagree, the higher of the two wins.
60
+ */
61
+ export declare function advanceVoxelPlayer(body: VoxelPlayerBody, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, isSolid: SolidQuery, dims?: VoxelPlayerDims, tuning?: MovementTuningOverrides, groundHeight?: GroundHeightQuery): void;