@jgengine/core 0.8.0 → 0.10.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 (303) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -1,3 +1,4 @@
1
+ import { pointerAxisValue } from "./pointerAxis.js";
1
2
  export const NEUTRAL_AXIS = { throttle: 0, brake: 0, steer: 0, handbrake: 0 };
2
3
  export const AXIS_RANGE = {
3
4
  throttle: { min: 0, max: 1 },
@@ -25,8 +26,9 @@ function digitalTarget(binding, isDown) {
25
26
  /**
26
27
  * Analog control channel — distinct from the digital action bindings. Throttle/brake/steer/handbrake
27
28
  * are continuous values ramped from held keys (a keyboard feels like a pedal) or driven directly from
28
- * a gamepad axis via `setAnalog`. `sample(dt, isDown)` folds the current held-key state into the
29
- * smoothed value; a `setAnalog` override replaces the digital target for that axis until cleared.
29
+ * a gamepad axis via `setAnalog`. `sample(dt, isDown, pointer?)` folds the current held-key state into
30
+ * the smoothed value; a binding's `pointer` source takes over from its keys while a pointer is active,
31
+ * and a `setAnalog` override replaces both for that axis until cleared.
30
32
  */
31
33
  export class AxisChannel {
32
34
  bindings;
@@ -52,11 +54,15 @@ export class AxisChannel {
52
54
  get value() {
53
55
  return this.current;
54
56
  }
55
- sample(dt, isDown) {
57
+ sample(dt, isDown, pointer) {
56
58
  for (const axis of Object.keys(this.bindings)) {
57
59
  const range = AXIS_RANGE[axis];
58
60
  const override = this.overrides[axis];
59
- const target = override !== undefined ? override : clampAxis(digitalTarget(this.bindings[axis], isDown), range);
61
+ const binding = this.bindings[axis];
62
+ const fromPointer = binding.pointer === undefined ? null : pointerAxisValue(binding.pointer, pointer);
63
+ const target = override !== undefined
64
+ ? override
65
+ : clampAxis(fromPointer ?? digitalTarget(binding, isDown), range);
60
66
  this.current[axis] = clampAxis(rampToward(this.current[axis], target, this.smoothing, dt), range);
61
67
  }
62
68
  return this.current;
@@ -68,3 +74,53 @@ export const DRIVE_AXIS_BINDINGS = {
68
74
  steer: { positive: ["KeyD", "ArrowRight"], negative: ["KeyA", "ArrowLeft"] },
69
75
  handbrake: { positive: ["Space"] },
70
76
  };
77
+ const BIPOLAR_RANGE = { min: -1, max: 1 };
78
+ /**
79
+ * Instantaneous (unsmoothed) analog sample of an axis binding map — the per-frame target an
80
+ * `AxisChannel` ramps toward, exposed as a pure read for `ctx.input.axis` and headless sampling.
81
+ * Each axis is its binding's positive-minus-negative held state (or its pointer source while a pointer
82
+ * is active), clamped to the axis range; axes without a listed range default to bipolar `[-1, 1]`.
83
+ */
84
+ export function sampleAxisBindings(bindings, isDown, pointer, ranges) {
85
+ const out = {};
86
+ for (const axis of Object.keys(bindings)) {
87
+ const binding = bindings[axis];
88
+ const range = ranges?.[axis] ?? BIPOLAR_RANGE;
89
+ const fromPointer = binding.pointer === undefined ? null : pointerAxisValue(binding.pointer, pointer ?? null);
90
+ out[axis] = clampAxis(fromPointer ?? digitalTarget(binding, isDown), range);
91
+ }
92
+ return out;
93
+ }
94
+ export function createAxisChannel(config) {
95
+ const axes = Object.keys(config.bindings);
96
+ const smoothing = config.smoothing ?? 6;
97
+ const current = Object.fromEntries(axes.map((axis) => [axis, 0]));
98
+ const overrides = {};
99
+ function rangeOf(axis) {
100
+ return config.ranges?.[axis] ?? BIPOLAR_RANGE;
101
+ }
102
+ return {
103
+ value: current,
104
+ setAnalog(axis, value) {
105
+ overrides[axis] = clampAxis(value, rangeOf(axis));
106
+ },
107
+ clearAnalog(axis) {
108
+ delete overrides[axis];
109
+ },
110
+ reset() {
111
+ for (const axis of axes)
112
+ current[axis] = 0;
113
+ },
114
+ sample(dt, isDown, pointer) {
115
+ for (const axis of axes) {
116
+ const range = rangeOf(axis);
117
+ const override = overrides[axis];
118
+ const binding = config.bindings[axis];
119
+ const fromPointer = binding.pointer === undefined ? null : pointerAxisValue(binding.pointer, pointer);
120
+ const target = override !== undefined ? override : clampAxis(fromPointer ?? digitalTarget(binding, isDown), range);
121
+ current[axis] = clampAxis(rampToward(current[axis], target, smoothing, dt), range);
122
+ }
123
+ return current;
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,25 @@
1
+ import type { ActionCodes, ActionCodesMap } from "./actionBindings.js";
2
+ /**
3
+ * Player-rebound keys, keyed by action name. Values mirror an `ActionCodes`
4
+ * entry so hold/toggle/repeat semantics survive a rebind — the settings menu
5
+ * only swaps which physical codes drive the action.
6
+ */
7
+ export type BindingOverrides = Record<string, ActionCodes>;
8
+ export declare const BINDING_OVERRIDES_STORAGE_PREFIX = "jgengine:keybinds:";
9
+ interface WebStorageLike {
10
+ getItem(key: string): string | null;
11
+ setItem(key: string, value: string): void;
12
+ removeItem(key: string): void;
13
+ }
14
+ export declare function bindingOverridesStorageKey(gameId: string): string;
15
+ export declare function loadBindingOverrides(gameId: string, storage?: Pick<WebStorageLike, "getItem"> | null | undefined): BindingOverrides;
16
+ export declare function saveBindingOverride(gameId: string, action: string, codes: ActionCodes, storage?: Pick<WebStorageLike, "getItem" | "setItem" | "removeItem"> | null | undefined): BindingOverrides;
17
+ export declare function clearBindingOverride(gameId: string, action: string, storage?: Pick<WebStorageLike, "getItem" | "setItem" | "removeItem"> | null | undefined): BindingOverrides;
18
+ export declare function clearAllBindingOverrides(gameId: string, storage?: Pick<WebStorageLike, "removeItem"> | null | undefined): void;
19
+ /**
20
+ * Merge player rebinds over a game's authored `input` map. Only actions the
21
+ * game already declares can be overridden; unknown override keys are ignored so
22
+ * a stale localStorage entry can't inject phantom actions.
23
+ */
24
+ export declare function applyBindingOverrides<TAction extends string, TCode extends string>(input: ActionCodesMap<TAction, TCode>, overrides: BindingOverrides): ActionCodesMap<TAction, TCode>;
25
+ export {};
@@ -0,0 +1,95 @@
1
+ export const BINDING_OVERRIDES_STORAGE_PREFIX = "jgengine:keybinds:";
2
+ export function bindingOverridesStorageKey(gameId) {
3
+ return `${BINDING_OVERRIDES_STORAGE_PREFIX}${gameId}`;
4
+ }
5
+ function defaultStorage() {
6
+ try {
7
+ if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
8
+ return globalThis.localStorage ?? null;
9
+ }
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ return null;
15
+ }
16
+ function isActionCodes(value) {
17
+ if (Array.isArray(value))
18
+ return value.every((code) => typeof code === "string");
19
+ if (value === null || typeof value !== "object")
20
+ return false;
21
+ const modes = value;
22
+ const okList = (list) => list === undefined || (Array.isArray(list) && list.every((code) => typeof code === "string"));
23
+ return okList(modes.hold) && okList(modes.toggle);
24
+ }
25
+ export function loadBindingOverrides(gameId, storage = defaultStorage()) {
26
+ if (storage == null)
27
+ return {};
28
+ try {
29
+ const raw = storage.getItem(bindingOverridesStorageKey(gameId));
30
+ if (raw === null)
31
+ return {};
32
+ const parsed = JSON.parse(raw);
33
+ if (parsed === null || typeof parsed !== "object")
34
+ return {};
35
+ const result = {};
36
+ for (const [action, codes] of Object.entries(parsed)) {
37
+ if (isActionCodes(codes))
38
+ result[action] = codes;
39
+ }
40
+ return result;
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ }
46
+ function persist(gameId, overrides, storage) {
47
+ if (storage == null)
48
+ return;
49
+ try {
50
+ if (Object.keys(overrides).length === 0)
51
+ storage.removeItem(bindingOverridesStorageKey(gameId));
52
+ else
53
+ storage.setItem(bindingOverridesStorageKey(gameId), JSON.stringify(overrides));
54
+ }
55
+ catch {
56
+ /* ignore quota / private-mode failures */
57
+ }
58
+ }
59
+ export function saveBindingOverride(gameId, action, codes, storage = defaultStorage()) {
60
+ const overrides = { ...loadBindingOverrides(gameId, storage ?? undefined), [action]: codes };
61
+ persist(gameId, overrides, storage);
62
+ return overrides;
63
+ }
64
+ export function clearBindingOverride(gameId, action, storage = defaultStorage()) {
65
+ const overrides = { ...loadBindingOverrides(gameId, storage ?? undefined) };
66
+ delete overrides[action];
67
+ persist(gameId, overrides, storage);
68
+ return overrides;
69
+ }
70
+ export function clearAllBindingOverrides(gameId, storage = defaultStorage()) {
71
+ if (storage == null)
72
+ return;
73
+ try {
74
+ storage.removeItem(bindingOverridesStorageKey(gameId));
75
+ }
76
+ catch {
77
+ /* ignore */
78
+ }
79
+ }
80
+ /**
81
+ * Merge player rebinds over a game's authored `input` map. Only actions the
82
+ * game already declares can be overridden; unknown override keys are ignored so
83
+ * a stale localStorage entry can't inject phantom actions.
84
+ */
85
+ export function applyBindingOverrides(input, overrides) {
86
+ if (Object.keys(overrides).length === 0)
87
+ return input;
88
+ const result = { ...input };
89
+ for (const action of Object.keys(input)) {
90
+ const override = overrides[action];
91
+ if (override !== undefined)
92
+ result[action] = override;
93
+ }
94
+ return result;
95
+ }
@@ -0,0 +1,30 @@
1
+ export interface PointerAxisState {
2
+ /** Horizontal pointer position over the play surface in `[-1, 1]`: `-1` at the left edge, `+1` at the right. */
3
+ x: number;
4
+ /** Vertical pointer position over the play surface in `[-1, 1]`, screen convention: `-1` at the top edge, `+1` at the bottom — negate for aim-up. */
5
+ y: number;
6
+ /** `false` after the pointer leaves the surface; the last position is retained. */
7
+ active: boolean;
8
+ }
9
+ /** Maps one pointer axis onto an analog axis target — the pointer-backed counterpart of an `AxisBinding`'s key lists. */
10
+ export interface PointerAxisBinding {
11
+ source: "x" | "y";
12
+ invert?: boolean;
13
+ /** Fraction of the half-range around center treated as zero; the remainder rescales to the full `[-1, 1]`. */
14
+ deadzone?: number;
15
+ /** Response-curve exponent on the magnitude: `1` linear (default), `>1` finer control near center. */
16
+ curve?: number;
17
+ }
18
+ export interface PointerSurfaceRect {
19
+ left: number;
20
+ top: number;
21
+ width: number;
22
+ height: number;
23
+ }
24
+ /** Normalize client coordinates against a surface rect into a `PointerAxisState`, clamped to `[-1, 1]` per axis. */
25
+ export declare function normalizePointerToAxis(clientX: number, clientY: number, rect: PointerSurfaceRect): PointerAxisState;
26
+ /**
27
+ * Resolve a pointer binding against the current pointer state: `null` when no pointer is active
28
+ * (callers fall back to their digital target), otherwise the deadzone/curve-shaped value in `[-1, 1]`.
29
+ */
30
+ export declare function pointerAxisValue(binding: PointerAxisBinding, state: PointerAxisState | null | undefined): number | null;
@@ -0,0 +1,31 @@
1
+ /** Normalize client coordinates against a surface rect into a `PointerAxisState`, clamped to `[-1, 1]` per axis. */
2
+ export function normalizePointerToAxis(clientX, clientY, rect) {
3
+ const width = rect.width > 0 ? rect.width : 1;
4
+ const height = rect.height > 0 ? rect.height : 1;
5
+ return {
6
+ x: clampUnit(((clientX - rect.left) / width) * 2 - 1),
7
+ y: clampUnit(((clientY - rect.top) / height) * 2 - 1),
8
+ active: true,
9
+ };
10
+ }
11
+ /**
12
+ * Resolve a pointer binding against the current pointer state: `null` when no pointer is active
13
+ * (callers fall back to their digital target), otherwise the deadzone/curve-shaped value in `[-1, 1]`.
14
+ */
15
+ export function pointerAxisValue(binding, state) {
16
+ if (state === null || state === undefined || !state.active)
17
+ return null;
18
+ let value = binding.source === "x" ? state.x : state.y;
19
+ if (binding.invert === true)
20
+ value = -value;
21
+ const deadzone = binding.deadzone ?? 0;
22
+ const magnitude = Math.abs(value);
23
+ if (magnitude <= deadzone)
24
+ return 0;
25
+ const scaled = (magnitude - deadzone) / (1 - deadzone);
26
+ const curved = Math.pow(Math.min(scaled, 1), binding.curve ?? 1);
27
+ return Math.sign(value) * curved;
28
+ }
29
+ function clampUnit(value) {
30
+ return value < -1 ? -1 : value > 1 ? 1 : value;
31
+ }
@@ -36,6 +36,8 @@ export interface TouchButtonSpec {
36
36
  label?: string;
37
37
  /** Glyph name from the capture layer's icon catalog (`@jgengine/react/gameIcons` `GameIconName`); unset derives one from the action name, `false` forces the text label. */
38
38
  icon?: string | false;
39
+ /** Overrides the derived classification: `primary` renders as a large thumb button, `utility` as a small chip away from the thumb zones. */
40
+ kind?: TouchButtonKind;
39
41
  }
40
42
  export interface TouchControlsConfig {
41
43
  /** `false` removes the virtual joystick even when movement actions are bound. */
@@ -57,11 +59,14 @@ export interface TouchJoystick {
57
59
  left: string | null;
58
60
  right: string | null;
59
61
  }
62
+ export type TouchButtonKind = "primary" | "utility";
60
63
  export interface TouchButton {
61
64
  action: string;
62
65
  label: string;
63
66
  /** Glyph hint for the capture layer: a name to render, `false` to force text, null to derive from the action name. */
64
67
  icon: string | false | null;
68
+ /** `primary` = frequent gameplay verb, rendered as a large thumb button; `utility` = meta action (start, restart, pause, toggles), rendered as a small chip. */
69
+ kind: TouchButtonKind;
65
70
  }
66
71
  export interface TouchScheme {
67
72
  joystick: TouchJoystick | null;
@@ -70,6 +75,7 @@ export interface TouchScheme {
70
75
  look: boolean;
71
76
  lookSensitivity: number;
72
77
  }
78
+ export declare function touchButtonKind(action: string): TouchButtonKind;
73
79
  export declare function touchActionLabel(action: string): string;
74
80
  export interface DeriveTouchSchemeOptions {
75
81
  reserved: ReadonlySet<string>;
@@ -33,12 +33,45 @@ export function withTouchCodes(map) {
33
33
  }
34
34
  const DEFAULT_LOOK_SENSITIVITY = 0.005;
35
35
  const JOYSTICK_AXES = {
36
- up: ["moveForward"],
37
- down: ["moveBack"],
38
- left: ["moveLeft", "turnLeft"],
39
- right: ["moveRight", "turnRight"],
36
+ up: ["moveForward", "moveUp", "forward", "accelerate", "throttleUp", "pitchUp", "climb"],
37
+ down: ["moveBack", "moveBackward", "moveDown", "backward", "reverse", "brake", "throttleDown", "pitchDown", "dive"],
38
+ left: ["moveLeft", "turnLeft", "steerLeft", "yawLeft", "strafeLeft", "bankLeft", "rollLeft"],
39
+ right: ["moveRight", "turnRight", "steerRight", "yawRight", "strafeRight", "bankRight", "rollRight"],
40
40
  };
41
41
  const BUTTONABLE_RESERVED = new Set(["jump", "sprint", "interact", "useAbility"]);
42
+ const UTILITY_ACTIONS = new Set([
43
+ "start",
44
+ "restart",
45
+ "reset",
46
+ "retry",
47
+ "pause",
48
+ "resume",
49
+ "menu",
50
+ "quit",
51
+ "exit",
52
+ "mute",
53
+ "unmute",
54
+ "help",
55
+ "settings",
56
+ "options",
57
+ "map",
58
+ "inventory",
59
+ "scoreboard",
60
+ "leaderboard",
61
+ ]);
62
+ const UTILITY_PREFIXES = ["toggle", "cycle", "switch", "open", "close", "show", "hide", "zoom"];
63
+ export function touchButtonKind(action) {
64
+ if (UTILITY_ACTIONS.has(action))
65
+ return "utility";
66
+ for (const prefix of UTILITY_PREFIXES) {
67
+ if (!action.startsWith(prefix))
68
+ continue;
69
+ const next = action.charAt(prefix.length);
70
+ if (next !== "" && next === next.toUpperCase())
71
+ return "utility";
72
+ }
73
+ return "primary";
74
+ }
42
75
  export function touchActionLabel(action) {
43
76
  const spaced = action.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
44
77
  return spaced.charAt(0).toUpperCase() + spaced.slice(1);
@@ -90,8 +123,13 @@ export function deriveTouchScheme(input, { reserved, firstPerson, config }) {
90
123
  let buttons;
91
124
  if (config?.buttons !== undefined) {
92
125
  buttons = config.buttons.map((spec) => typeof spec === "string"
93
- ? { action: spec, label: touchActionLabel(spec), icon: null }
94
- : { action: spec.action, label: spec.label ?? touchActionLabel(spec.action), icon: spec.icon ?? null });
126
+ ? { action: spec, label: touchActionLabel(spec), icon: null, kind: touchButtonKind(spec) }
127
+ : {
128
+ action: spec.action,
129
+ label: spec.label ?? touchActionLabel(spec.action),
130
+ icon: spec.icon ?? null,
131
+ kind: spec.kind ?? touchButtonKind(spec.action),
132
+ });
95
133
  }
96
134
  else {
97
135
  buttons = actions
@@ -100,7 +138,7 @@ export function deriveTouchScheme(input, { reserved, firstPerson, config }) {
100
138
  .filter((action) => !consumedByGestures.has(action))
101
139
  .filter((action) => !hidden.has(action))
102
140
  .filter((action) => !reserved.has(action) || BUTTONABLE_RESERVED.has(action))
103
- .map((action) => ({ action, label: touchActionLabel(action), icon: null }));
141
+ .map((action) => ({ action, label: touchActionLabel(action), icon: null, kind: touchButtonKind(action) }));
104
142
  }
105
143
  const look = config?.look ?? firstPerson;
106
144
  if (joystick === null && buttons.length === 0 && gestures === null && !look)
@@ -7,6 +7,7 @@ export interface ContextVerb {
7
7
  /** Greyed-out and non-dispatchable when true (e.g. requirement unmet). */
8
8
  disabled?: boolean;
9
9
  }
10
+ /** Builds a {@link ContextVerb} for a right-click menu entry. */
10
11
  export declare function contextVerb(label: string, command: string, args?: Record<string, unknown>): ContextVerb;
11
12
  export interface ContextMenu {
12
13
  kind: ContextTargetKind;
@@ -1,3 +1,4 @@
1
+ /** Builds a {@link ContextVerb} for a right-click menu entry. */
1
2
  export function contextVerb(label, command, args) {
2
3
  return args === undefined ? { label, command } : { label, command, args };
3
4
  }
@@ -5,6 +5,8 @@ export interface PromptPoint {
5
5
  export interface KeybindPromptDisplay {
6
6
  kind: "keybind";
7
7
  actionId: string;
8
+ /** What the interaction does, shown beside the key glyph (e.g. `"Open Chest"`, `"Talk"`); omit for key-only. */
9
+ label?: string;
8
10
  }
9
11
  export interface GaugePromptDisplay {
10
12
  kind: "gauge";
@@ -15,7 +17,7 @@ export interface LabelPromptDisplay {
15
17
  text: string;
16
18
  }
17
19
  export type ProximityPromptDisplay = KeybindPromptDisplay | GaugePromptDisplay | LabelPromptDisplay;
18
- export declare function keybind(actionId: string): KeybindPromptDisplay;
20
+ export declare function keybind(actionId: string, label?: string): KeybindPromptDisplay;
19
21
  export declare function gauge(gaugeId: string): GaugePromptDisplay;
20
22
  export declare function label(text: string): LabelPromptDisplay;
21
23
  export interface PromptCommand {
@@ -1,5 +1,5 @@
1
- export function keybind(actionId) {
2
- return { kind: "keybind", actionId };
1
+ export function keybind(actionId, label) {
2
+ return label === undefined ? { kind: "keybind", actionId } : { kind: "keybind", actionId, label };
3
3
  }
4
4
  export function gauge(gaugeId) {
5
5
  return { kind: "gauge", gaugeId };
@@ -39,7 +39,7 @@ export function promptDisplaysEqual(a, b) {
39
39
  if (a === b)
40
40
  return true;
41
41
  if (a.kind === "keybind")
42
- return b.kind === "keybind" && a.actionId === b.actionId;
42
+ return b.kind === "keybind" && a.actionId === b.actionId && a.label === b.label;
43
43
  if (a.kind === "gauge")
44
44
  return b.kind === "gauge" && a.gaugeId === b.gaugeId;
45
45
  return b.kind === "label" && a.text === b.text;
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.8.0";
1
+ export declare const VERSION = "0.10.0";
2
2
  export interface ChangelogEntry {
3
3
  migrate: readonly string[];
4
4
  added: readonly string[];
@@ -1,5 +1,45 @@
1
- export const VERSION = "0.8.0";
1
+ export const VERSION = "0.10.0";
2
2
  export const CHANGELOG = {
3
+ "0.10.0": {
4
+ migrate: [
5
+ "Bump every @jgengine/* dependency to ^0.10.0 (the eight packages version in lockstep).",
6
+ "Nothing to change for existing games — the shared mobile-composition system (viewport allocation, region collision, --jg-* viewport vars) is wired automatically by the shell; HudPanels inside a HudCanvas register themselves. Legacy orientation: \"landscape\" | \"portrait\" keeps its advisory dismissible-hint behavior.",
7
+ "Opt a driving/landscape game into the strict gate: replace advisory orientation with the contract form orientation: { mobile: \"landscape-required\" } (rules: any · portrait/landscape advisory · portrait-required/landscape-required · unsupported). In portrait the engine shows a polished RotateDeviceScreen, suppresses input, freezes the sim, and unmounts the HUD/controls until the device is landscape.",
8
+ "Declare HUD intent where useful: HudPanel gains priority (critical/secondary/tertiary), mobileBehavior (hidden unmounts on phones, transient softens collision), allowOverlapWith, and collisionGroup — all optional; omitting them keeps prior behavior.",
9
+ "Mobile validation now also fails on overlap: bun run shoot <game> --device mobile|mobile-landscape|both exits non-zero on forbidden inter-element collisions, naming both regions.",
10
+ ],
11
+ added: [
12
+ "Shared mobile layout composition — @jgengine/core/ui/gameLayout (resolveLayoutMode, intersects, overlapArea, detectLayoutCollisions, computeGameplayRect, LayoutRegion, GameViewportLayout) and @jgengine/core/ui/orientation (resolveOrientationRequirement, orientationGateActive, MobileOrientationRule); @jgengine/react adds GameViewportProvider (mounted by the shell), hooks (useGameViewportLayout, useGameLayoutMode, useGameOrientation, useReservedControlZones, useLayoutCollisions, useRegisterLayoutRegion), and a headless RotateDeviceScreen. The provider tracks window.visualViewport, publishes --jg-viewport-*/--jg-visual-viewport-*/--jg-safe-* CSS vars, resolves the explicit layout mode, and detects forbidden region overlaps.",
13
+ "Mandatory orientation contract — PlayableGame.orientation accepts { mobile: <rule> }; landscape-required/portrait-required render an engine-owned rotate gate that blocks gameplay (input suppressed, simulation frozen, HUD/controls unmounted), self-correcting when the device is turned. Reduced-motion and safe-area respected; visualViewport-sized for mobile Safari/PWA.",
14
+ "Reserved touch-control zones — the touch dock registers its joystick / action-cluster / utility rectangles (runtime-measured) as control regions, so HUD placement and collision validation know exactly where controls sit.",
15
+ "HUD placement metadata — HudPanel gains priority, mobileBehavior, allowOverlapWith, collisionGroup, and registers as a collision-tracked layout region.",
16
+ "Mobile-landscape shoot device — bun run shoot --device mobile-landscape (844×390) plus collision-gate reads that fail mobile validation on forbidden overlaps.",
17
+ "Canyon Chase migrated as the reference landscape-required game — declares orientation: { mobile: \"landscape-required\" } and composes its HUD through coordinated HudPanel regions instead of independent fixed corners.",
18
+ ],
19
+ changed: [
20
+ "First-person projectile tracers now originate from the weapon muzzle instead of the camera/eye centerline; hit detection is unchanged (still crosshair-accurate), and non-viewmodel games and enemy tracers are unaffected.",
21
+ ],
22
+ removed: [],
23
+ },
24
+ "0.9.0": {
25
+ migrate: [
26
+ "Bump every @jgengine/* dependency to ^0.9.0 (the eight packages version in lockstep).",
27
+ "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\").",
28
+ "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.",
29
+ "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.",
30
+ ],
31
+ added: [
32
+ "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.",
33
+ "Graphics → UI scale — a player-facing graphics.uiScale slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform.",
34
+ "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.",
35
+ "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.",
36
+ "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.",
37
+ ],
38
+ changed: [
39
+ "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\"].",
40
+ ],
41
+ removed: [],
42
+ },
3
43
  "0.8.0": {
4
44
  migrate: [
5
45
  "Bump every @jgengine/* dependency to ^0.8.0 (the eight packages version in lockstep).",
@@ -30,6 +70,9 @@ export const CHANGELOG = {
30
70
  "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
71
  "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
72
  "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).",
73
+ "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.",
74
+ "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.",
75
+ "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.",
33
76
  ],
34
77
  changed: [],
35
78
  removed: [
@@ -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;