@jgengine/core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/CHANGELOG.md +16 -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.js +1 -1
  11. package/dist/ai/threat.d.ts +2 -0
  12. package/dist/ai/threat.js +34 -0
  13. package/dist/cartridge/runtime.d.ts +15 -0
  14. package/dist/cartridge/runtime.js +556 -0
  15. package/dist/cartridge/spec.d.ts +188 -0
  16. package/dist/cartridge/spec.js +19 -0
  17. package/dist/cartridge/validate.d.ts +2 -0
  18. package/dist/cartridge/validate.js +78 -0
  19. package/dist/combat/abilityKit.d.ts +17 -2
  20. package/dist/combat/abilityKit.js +62 -13
  21. package/dist/combat/castRunner.d.ts +40 -0
  22. package/dist/combat/castRunner.js +56 -0
  23. package/dist/combat/comboPoints.d.ts +14 -0
  24. package/dist/combat/comboPoints.js +52 -0
  25. package/dist/combat/defensiveWindow.d.ts +1 -1
  26. package/dist/combat/defensiveWindow.js +5 -3
  27. package/dist/combat/projectiles.d.ts +33 -12
  28. package/dist/combat/projectiles.js +201 -129
  29. package/dist/combat/resistance.d.ts +16 -4
  30. package/dist/combat/resistance.js +43 -7
  31. package/dist/combat/resourcePool.d.ts +20 -0
  32. package/dist/combat/resourcePool.js +55 -0
  33. package/dist/combat/shotOrigin.d.ts +40 -0
  34. package/dist/combat/shotOrigin.js +57 -0
  35. package/dist/combat/telegraph.d.ts +25 -0
  36. package/dist/combat/telegraph.js +47 -0
  37. package/dist/crafting/recipe.js +4 -1
  38. package/dist/devtools/devtools.d.ts +72 -24
  39. package/dist/devtools/devtools.js +449 -101
  40. package/dist/devtools/transformTunables.js +3 -3
  41. package/dist/devtools/tunableSchema.d.ts +140 -0
  42. package/dist/devtools/tunableSchema.js +476 -0
  43. package/dist/faction/factions.d.ts +33 -0
  44. package/dist/faction/factions.js +76 -0
  45. package/dist/faction/reputation.d.ts +32 -0
  46. package/dist/faction/reputation.js +82 -0
  47. package/dist/game/chat.d.ts +2 -0
  48. package/dist/game/chat.js +9 -1
  49. package/dist/game/chatFilter.d.ts +15 -0
  50. package/dist/game/chatFilter.js +55 -0
  51. package/dist/game/controlGate.d.ts +4 -0
  52. package/dist/game/controlGate.js +7 -0
  53. package/dist/game/defineGame.d.ts +2 -0
  54. package/dist/game/events.d.ts +10 -0
  55. package/dist/game/gamePhase.d.ts +20 -0
  56. package/dist/game/gamePhase.js +21 -0
  57. package/dist/game/lootTable.d.ts +10 -1
  58. package/dist/game/lootTable.js +27 -7
  59. package/dist/game/playableGame.d.ts +50 -1
  60. package/dist/game/playableGame.js +1 -1
  61. package/dist/game/quest.d.ts +7 -3
  62. package/dist/game/quest.js +17 -8
  63. package/dist/game/race.d.ts +39 -1
  64. package/dist/game/race.js +96 -11
  65. package/dist/game/recordBook.d.ts +29 -0
  66. package/dist/game/recordBook.js +86 -0
  67. package/dist/game/talents.d.ts +47 -0
  68. package/dist/game/talents.js +140 -0
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.js +1 -0
  71. package/dist/input/axisInput.d.ts +26 -3
  72. package/dist/input/axisInput.js +44 -4
  73. package/dist/input/bindingOverrides.d.ts +25 -0
  74. package/dist/input/bindingOverrides.js +95 -0
  75. package/dist/input/pointerAxis.d.ts +30 -0
  76. package/dist/input/pointerAxis.js +31 -0
  77. package/dist/input/touchScheme.d.ts +6 -0
  78. package/dist/input/touchScheme.js +45 -7
  79. package/dist/meta/changelog.d.ts +1 -1
  80. package/dist/meta/changelog.js +23 -1
  81. package/dist/movement/dash.d.ts +3 -0
  82. package/dist/movement/dash.js +19 -5
  83. package/dist/movement/glideModel.d.ts +44 -0
  84. package/dist/movement/glideModel.js +54 -0
  85. package/dist/movement/grappleSwing.d.ts +34 -0
  86. package/dist/movement/grappleSwing.js +56 -0
  87. package/dist/movement/leaderTrail.d.ts +29 -0
  88. package/dist/movement/leaderTrail.js +67 -0
  89. package/dist/movement/steering.d.ts +27 -0
  90. package/dist/movement/steering.js +40 -0
  91. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  92. package/dist/multiplayer/feedWriteGate.js +12 -0
  93. package/dist/multiplayer/presenceModel.d.ts +14 -2
  94. package/dist/multiplayer/presenceModel.js +15 -3
  95. package/dist/nav/corridors.d.ts +30 -0
  96. package/dist/nav/corridors.js +69 -0
  97. package/dist/nav/navGrid.js +30 -4
  98. package/dist/nav/railGraph.d.ts +66 -0
  99. package/dist/nav/railGraph.js +129 -0
  100. package/dist/nav/timetable.d.ts +41 -0
  101. package/dist/nav/timetable.js +94 -0
  102. package/dist/physics/buoyancy.d.ts +6 -0
  103. package/dist/physics/buoyancy.js +14 -0
  104. package/dist/physics/flowTube.d.ts +32 -0
  105. package/dist/physics/flowTube.js +64 -0
  106. package/dist/physics/forceVolume.d.ts +27 -0
  107. package/dist/physics/forceVolume.js +38 -0
  108. package/dist/physics/kinematicVehicle.d.ts +52 -0
  109. package/dist/physics/kinematicVehicle.js +81 -0
  110. package/dist/physics/physicsWorld.d.ts +13 -6
  111. package/dist/physics/physicsWorld.js +169 -40
  112. package/dist/physics/ragdoll.js +1 -1
  113. package/dist/physics/vehicleBody.js +2 -1
  114. package/dist/random/seedLink.d.ts +5 -0
  115. package/dist/random/seedLink.js +35 -0
  116. package/dist/runtime/cameraDirector.d.ts +6 -1
  117. package/dist/runtime/cameraDirector.js +6 -0
  118. package/dist/runtime/commandRunner.d.ts +2 -2
  119. package/dist/runtime/commandRunner.js +2 -2
  120. package/dist/runtime/gameContext.d.ts +21 -0
  121. package/dist/runtime/gameContext.js +184 -9
  122. package/dist/runtime/gameRuntime.d.ts +5 -1
  123. package/dist/runtime/gameRuntime.js +13 -12
  124. package/dist/runtime/hostPersistence.d.ts +9 -1
  125. package/dist/runtime/hostPersistence.js +23 -8
  126. package/dist/runtime/inputSnapshot.d.ts +5 -0
  127. package/dist/runtime/inputSnapshot.js +5 -0
  128. package/dist/runtime/motionIntents.d.ts +14 -6
  129. package/dist/runtime/motionIntents.js +20 -2
  130. package/dist/runtime/snapshot.d.ts +1 -0
  131. package/dist/runtime/snapshot.js +1 -1
  132. package/dist/runtime/visibility.d.ts +74 -0
  133. package/dist/runtime/visibility.js +127 -0
  134. package/dist/scene/colliders.d.ts +44 -0
  135. package/dist/scene/colliders.js +77 -0
  136. package/dist/scene/entityStore.d.ts +22 -5
  137. package/dist/scene/entityStore.js +59 -12
  138. package/dist/scene/objectQuery.d.ts +13 -2
  139. package/dist/scene/objectQuery.js +34 -2
  140. package/dist/scene/objectStore.d.ts +2 -0
  141. package/dist/scene/objectStore.js +31 -12
  142. package/dist/scene/sceneRaycast.d.ts +80 -0
  143. package/dist/scene/sceneRaycast.js +284 -0
  144. package/dist/scene/spatial.d.ts +11 -16
  145. package/dist/scene/spatial.js +99 -38
  146. package/dist/sensor/recordingBuffer.d.ts +6 -0
  147. package/dist/sensor/recordingBuffer.js +7 -0
  148. package/dist/sensor/replayLoop.d.ts +39 -0
  149. package/dist/sensor/replayLoop.js +64 -0
  150. package/dist/sensor/visionCone.d.ts +31 -0
  151. package/dist/sensor/visionCone.js +72 -0
  152. package/dist/session/extraction.d.ts +2 -2
  153. package/dist/session/extraction.js +13 -2
  154. package/dist/settings/settingsModel.d.ts +97 -0
  155. package/dist/settings/settingsModel.js +100 -0
  156. package/dist/stats/statModifiers.d.ts +5 -2
  157. package/dist/stats/statModifiers.js +32 -14
  158. package/dist/store/observableKeyedStore.js +8 -2
  159. package/dist/survival/decayMeter.d.ts +1 -1
  160. package/dist/survival/decayMeter.js +0 -1
  161. package/dist/time/beatClock.d.ts +18 -0
  162. package/dist/time/beatClock.js +24 -0
  163. package/dist/time/idleProgress.d.ts +29 -0
  164. package/dist/time/idleProgress.js +31 -0
  165. package/dist/time/serverTick.d.ts +7 -1
  166. package/dist/time/serverTick.js +27 -4
  167. package/dist/time/stateSchedule.d.ts +57 -0
  168. package/dist/time/stateSchedule.js +121 -0
  169. package/dist/turn/commit.d.ts +2 -2
  170. package/dist/turn/commit.js +2 -2
  171. package/dist/ui/hudLayout.d.ts +58 -0
  172. package/dist/ui/hudLayout.js +207 -0
  173. package/dist/ui/hudScale.d.ts +44 -0
  174. package/dist/ui/hudScale.js +55 -0
  175. package/dist/visibility/assetStreaming.d.ts +66 -0
  176. package/dist/visibility/assetStreaming.js +201 -0
  177. package/dist/visibility/bounds.d.ts +66 -0
  178. package/dist/visibility/bounds.js +113 -0
  179. package/dist/visibility/camera.d.ts +21 -0
  180. package/dist/visibility/camera.js +3 -0
  181. package/dist/visibility/config.d.ts +21 -0
  182. package/dist/visibility/config.js +1 -0
  183. package/dist/visibility/diagnostics.d.ts +26 -0
  184. package/dist/visibility/diagnostics.js +36 -0
  185. package/dist/visibility/distance.d.ts +11 -0
  186. package/dist/visibility/distance.js +30 -0
  187. package/dist/visibility/frustum.d.ts +47 -0
  188. package/dist/visibility/frustum.js +186 -0
  189. package/dist/visibility/occlusion.d.ts +36 -0
  190. package/dist/visibility/occlusion.js +45 -0
  191. package/dist/visibility/settings.d.ts +75 -0
  192. package/dist/visibility/settings.js +49 -0
  193. package/dist/visibility/simulationCulling.d.ts +34 -0
  194. package/dist/visibility/simulationCulling.js +36 -0
  195. package/dist/visibility/spatialIndex.d.ts +45 -0
  196. package/dist/visibility/spatialIndex.js +198 -0
  197. package/dist/visibility/visibilitySystem.d.ts +90 -0
  198. package/dist/visibility/visibilitySystem.js +257 -0
  199. package/dist/world/buildings.d.ts +6 -0
  200. package/dist/world/buildings.js +152 -0
  201. package/dist/world/carve.d.ts +7 -2
  202. package/dist/world/carve.js +17 -7
  203. package/dist/world/cellStates.d.ts +45 -0
  204. package/dist/world/cellStates.js +103 -0
  205. package/dist/world/environmentSummary.d.ts +10 -2
  206. package/dist/world/environmentSummary.js +11 -2
  207. package/dist/world/features.d.ts +56 -8
  208. package/dist/world/features.js +24 -3
  209. package/dist/world/gridInstances.d.ts +7 -8
  210. package/dist/world/gridInstances.js +145 -7
  211. package/dist/world/lod.d.ts +17 -0
  212. package/dist/world/lod.js +66 -0
  213. package/dist/world/mapLayers.d.ts +60 -0
  214. package/dist/world/mapLayers.js +44 -0
  215. package/dist/world/massing.d.ts +45 -0
  216. package/dist/world/massing.js +581 -0
  217. package/dist/world/minimap.d.ts +13 -1
  218. package/dist/world/minimap.js +21 -2
  219. package/dist/world/terrain.d.ts +42 -3
  220. package/dist/world/terrain.js +197 -16
  221. package/dist/world/water.d.ts +7 -0
  222. package/dist/world/water.js +19 -4
  223. package/dist/world/weather.d.ts +3 -18
  224. package/dist/world/weather.js +4 -17
  225. package/dist/world/windZones.d.ts +47 -0
  226. package/dist/world/windZones.js +59 -0
  227. package/llms.txt +2585 -1704
  228. 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,37 @@ 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
+ export function createAxisChannel(config) {
79
+ const axes = Object.keys(config.bindings);
80
+ const smoothing = config.smoothing ?? 6;
81
+ const current = Object.fromEntries(axes.map((axis) => [axis, 0]));
82
+ const overrides = {};
83
+ function rangeOf(axis) {
84
+ return config.ranges?.[axis] ?? BIPOLAR_RANGE;
85
+ }
86
+ return {
87
+ value: current,
88
+ setAnalog(axis, value) {
89
+ overrides[axis] = clampAxis(value, rangeOf(axis));
90
+ },
91
+ clearAnalog(axis) {
92
+ delete overrides[axis];
93
+ },
94
+ reset() {
95
+ for (const axis of axes)
96
+ current[axis] = 0;
97
+ },
98
+ sample(dt, isDown, pointer) {
99
+ for (const axis of axes) {
100
+ const range = rangeOf(axis);
101
+ const override = overrides[axis];
102
+ const binding = config.bindings[axis];
103
+ const fromPointer = binding.pointer === undefined ? null : pointerAxisValue(binding.pointer, pointer);
104
+ const target = override !== undefined ? override : clampAxis(fromPointer ?? digitalTarget(binding, isDown), range);
105
+ current[axis] = clampAxis(rampToward(current[axis], target, smoothing, dt), range);
106
+ }
107
+ return current;
108
+ },
109
+ };
110
+ }
@@ -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)
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.8.0";
1
+ export declare const VERSION = "0.9.0";
2
2
  export interface ChangelogEntry {
3
3
  migrate: readonly string[];
4
4
  added: readonly string[];
@@ -1,5 +1,24 @@
1
- export const VERSION = "0.8.0";
1
+ export const VERSION = "0.9.0";
2
2
  export const CHANGELOG = {
3
+ "0.9.0": {
4
+ migrate: [
5
+ "Bump every @jgengine/* dependency to ^0.9.0 (the eight packages version in lockstep).",
6
+ "Mobile HUD fit is now on by default: design-resolution HUD scaling applies to every game with no config change. A desktop-only game that must keep the legacy fixed 0.85 compact zoom declares platforms: [\"web\"] (without \"mobile\").",
7
+ "Everything else is additive — VisibilitySystem defaults are enabled automatically but only affect renderers that read the policy, and existing games keep every 0.8.0 API unchanged.",
8
+ "Assets now resolve from this repo's self-hosted mirror (DEFAULT_RELEASE_BASE); games that pinned the previous base can override releaseBase if they need the old host.",
9
+ ],
10
+ added: [
11
+ "Mobile HUD fit — design-resolution UI scaling applies to every game by default: HudCanvas scales the whole HUD from PlayableGame.hudFit.designSize (default 1600×900, clamps minScale/maxScale 0.4–1) so desktop-authored layouts shrink to fit a phone; pure math in @jgengine/core/ui/hudScale (hudScaleForViewport, resolveHudFit, rectOverflow, overflowingPanels), wired by @jgengine/react's HudViewportProvider.",
12
+ "Graphics → UI scale — a player-facing graphics.uiScale slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform.",
13
+ "HUD overflow gate — HudCanvas reports panels that escape the viewport on a data-hud-overflow attribute; bun run shoot <game> --device mobile|both exits non-zero naming the offenders.",
14
+ "Automatic visibility & streaming defaults — VisibilitySystem (exported from @jgengine/core) gives every scene renderer-agnostic culling and asset-streaming policy with no per-object wiring: distance culling, preload margins, hysteresis, delayed unloading, multi-camera awareness, and per-object overrides.",
15
+ "Self-hosted asset mirror — DEFAULT_RELEASE_BASE points at this repo's rolling packs release; a catalog-driven mirror-assets workflow (weekly cron + dispatch) keeps it in sync.",
16
+ ],
17
+ changed: [
18
+ "Mobile HUD fit is on by default — design-resolution HUD scaling now applies to every game with no config change (was opt-in via platforms: [\"web\", \"mobile\"]); a desktop-only game keeps the legacy fixed 0.85 compact zoom via platforms: [\"web\"].",
19
+ ],
20
+ removed: [],
21
+ },
3
22
  "0.8.0": {
4
23
  migrate: [
5
24
  "Bump every @jgengine/* dependency to ^0.8.0 (the eight packages version in lockstep).",
@@ -30,6 +49,9 @@ export const CHANGELOG = {
30
49
  "Object spatial queries + entity patching — ctx.scene.object.at/inBox/raycast/raycastAll over unit-box objects; ctx.scene.entity.update(id, patch) for name/position/rotation/role/movement/behaviors/meta; per-instance renderObject/objectStyles overrides; pointerService.worldHitCenter() + pointer-lock center-ray aiming (#165).",
31
50
  "Controller movement config — PlayerMovementConfig (mode: free/axis/grid, axis, cellSize, collideObjects, beforeCommit pre-commit hook) for the shell-driven walk controller; defineGame.physics.gravity/jumpVelocity are honored by the built-in walk controller (distinct from the standalone physics/physicsWorld rigid-body sim); ctx.player.motion.impulse/setVerticalVelocity/setY/takePending (MotionIntents); entity.spawnPoseOf/resetToSpawn/resetAllToSpawn (#162).",
32
51
  "Model material/animation + paint — ModelConfig.tint/metalness/roughness/animation (GLTF clip playback, paused pose holds); PointerHit.uv + pointerService.sampleSurface() for material-aware picking; ctx.scene.entity.paint runtime paint layer, auto-rendered via a per-instance canvas texture with no per-game wiring; remote-player appearance tint recolors the shell's default capsule (#151).",
52
+ "Factions & reputation — faction/factions (createFactionGraph faction-vs-faction relation matrix, createFactionRoster entity membership + relationBetweenEntities/isHostile/hostilesOf) turns 'who is my enemy' into a data lookup feeding scene/targeting and ai/threat instead of a per-game role-string check; faction/reputation (createReputationLedger, DEFAULT_REPUTATION_TIERS Hated→Exalted, tierForStanding, effectiveRelation) tracks per-actor standing and overlays it on the base faction relation for at-war reputation grinds.",
53
+ "Threat taunt — ai/threat gained taunt(source, durationSeconds)/forcedTarget(): a taunt forces the mob onto a tank for its window (overriding highest) and lifts the taunter to the current top threat so aggro holds afterward; decay(dt) counts the window down.",
54
+ "Devtools discovery covers every game variable — discover.scanTable recurses nested plain objects and arrays into dotted paths (camera.chase.distance, waves.0.speed; 5 levels, 512 entries per scan, dedupes a value reachable through two tables), tunableDiscoveryPlugin rewrites annotated and derived single-line export consts (Math.sqrt(...), PLAYER_HEIGHT - 0.15) not just literals, DevtoolsOverlay self-scans the PlayableGame and core MOVEMENT_TUNING so physics/camera/movement/world config tune with zero exports in any host, resolvePhysicsTuning reads gravity/jumpVelocity live per frame, and the Tune tab gained a filter box.",
33
55
  ],
34
56
  changed: [],
35
57
  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;
@@ -0,0 +1,54 @@
1
+ export function createGlideModel(config = {}) {
2
+ const gravity = (config.gravity ?? 20) * (config.gravityScale ?? 0.25);
3
+ const thrustAccel = config.thrustAccel ?? 10;
4
+ const forwardSpeed = config.forwardSpeed ?? 12;
5
+ const yawRate = config.yawRate ?? 1.6;
6
+ const climbAccel = config.climbAccel ?? 0;
7
+ const maxFallSpeed = config.maxFallSpeed ?? 8;
8
+ const baseControl = config.control ?? 1;
9
+ let x = 0;
10
+ let y = 0;
11
+ let z = 0;
12
+ let heading = 0;
13
+ let vx = 0;
14
+ let vy = 0;
15
+ let vz = 0;
16
+ function snapshot() {
17
+ return { position: [x, y, z], velocity: [vx, vy, vz], heading };
18
+ }
19
+ return {
20
+ launch(position, nextHeading, initialSpeed = forwardSpeed) {
21
+ x = position[0];
22
+ y = position[1];
23
+ z = position[2];
24
+ heading = nextHeading;
25
+ vx = Math.sin(heading) * initialSpeed;
26
+ vy = 0;
27
+ vz = Math.cos(heading) * initialSpeed;
28
+ },
29
+ pose: snapshot,
30
+ step(dt, input = {}, externalVelocity) {
31
+ if (dt <= 0)
32
+ return snapshot();
33
+ const control = Math.max(0, Math.min(1, input.control ?? baseControl));
34
+ heading += (input.yaw ?? 0) * yawRate * control * dt;
35
+ const fx = Math.sin(heading);
36
+ const fz = Math.cos(heading);
37
+ const along = vx * fx + vz * fz;
38
+ const cruise = (forwardSpeed - along) * Math.min(1, (thrustAccel / Math.max(1, forwardSpeed)) * dt);
39
+ vx += fx * cruise * control;
40
+ vz += fz * cruise * control;
41
+ vy -= gravity * dt;
42
+ const throttle = Math.max(0, Math.min(1, input.throttle ?? 0));
43
+ if (climbAccel > 0 && throttle > 0)
44
+ vy += climbAccel * throttle * control * dt;
45
+ if (vy < -maxFallSpeed)
46
+ vy = -maxFallSpeed;
47
+ const [ex, ey, ez] = externalVelocity ?? [0, 0, 0];
48
+ x += (vx + ex) * dt;
49
+ y += (vy + ey) * dt;
50
+ z += (vz + ez) * dt;
51
+ return snapshot();
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,34 @@
1
+ export type SwingVec3 = readonly [number, number, number];
2
+ export interface GrappleSwingConfig {
3
+ /** Rope shortens by this many units/s while `reeling`. Default `0` (fixed length). */
4
+ reelSpeed?: number;
5
+ minLength?: number;
6
+ /** Extra spring pull toward the anchor when the rope is taut (elastic feel); default `0` (hard constraint only). */
7
+ stiffness?: number;
8
+ /** Velocity damping per second while attached; default `0.1`. */
9
+ damping?: number;
10
+ }
11
+ export interface GrappleSwingState {
12
+ attached: boolean;
13
+ anchor: SwingVec3 | null;
14
+ ropeLength: number;
15
+ }
16
+ export interface GrappleSwingStep {
17
+ position: SwingVec3;
18
+ velocity: SwingVec3;
19
+ }
20
+ /**
21
+ * A rope-swing constraint for kinematic controllers (#282.5) — the walk-controller-compatible
22
+ * sibling of `physics/traversal`'s rigid-body `Grapple`. Fire at an anchor, feed each frame's
23
+ * integrated position/velocity through `step`, and commit what comes back (via `beforeCommit`
24
+ * or `setPose`): positions beyond the rope clamp to its sphere and velocity loses its outward
25
+ * radial component, which is exactly a pendulum swing.
26
+ */
27
+ export interface GrappleSwing {
28
+ fire(anchor: SwingVec3, from: SwingVec3): void;
29
+ release(): void;
30
+ state(): GrappleSwingState;
31
+ /** Constrain one frame. `reeling` shortens the rope; returns the corrected position/velocity. */
32
+ step(position: SwingVec3, velocity: SwingVec3, dt: number, reeling?: boolean): GrappleSwingStep;
33
+ }
34
+ export declare function createGrappleSwing(config?: GrappleSwingConfig): GrappleSwing;