@jgengine/core 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/CHANGELOG.md +158 -7
  2. package/dist/ai/decisionGraph.d.ts +48 -4
  3. package/dist/ai/decisionGraph.js +153 -23
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/pursuit.d.ts +1 -1
  7. package/dist/ai/pursuit.js +1 -1
  8. package/dist/ai/threat.d.ts +8 -0
  9. package/dist/ai/threat.js +10 -0
  10. package/dist/anim/animGraph.d.ts +11 -0
  11. package/dist/anim/animGraph.js +128 -0
  12. package/dist/anim/boneTexture.d.ts +55 -0
  13. package/dist/anim/boneTexture.js +43 -0
  14. package/dist/anim/footPlacement.d.ts +57 -0
  15. package/dist/anim/footPlacement.js +67 -0
  16. package/dist/anim/locomotionGraph.d.ts +9 -0
  17. package/dist/anim/locomotionGraph.js +29 -0
  18. package/dist/audio/audioFalloff.d.ts +2 -0
  19. package/dist/audio/doppler.d.ts +21 -0
  20. package/dist/audio/doppler.js +32 -0
  21. package/dist/audio/engineLayers.d.ts +83 -0
  22. package/dist/audio/engineLayers.js +115 -0
  23. package/dist/combat/abilityKit.d.ts +1 -0
  24. package/dist/combat/abilityKit.js +1 -0
  25. package/dist/combat/death.d.ts +7 -0
  26. package/dist/combat/shotOrigin.d.ts +6 -0
  27. package/dist/combat/shotOrigin.js +6 -0
  28. package/dist/combat/weaponFeedback.d.ts +36 -0
  29. package/dist/combat/weaponFeedback.js +42 -0
  30. package/dist/combat/weaponHandling.d.ts +161 -0
  31. package/dist/combat/weaponHandling.js +202 -0
  32. package/dist/combat/weaponPresentation.d.ts +115 -0
  33. package/dist/combat/weaponPresentation.js +96 -0
  34. package/dist/devtools/warnOnce.d.ts +8 -0
  35. package/dist/devtools/warnOnce.js +18 -0
  36. package/dist/editor/document.js +3 -1
  37. package/dist/editor/environment.js +8 -0
  38. package/dist/editor/types.d.ts +4 -0
  39. package/dist/faction/reputation.d.ts +6 -0
  40. package/dist/faction/reputation.js +11 -0
  41. package/dist/game/cameraConfig.d.ts +51 -1
  42. package/dist/game/events.d.ts +6 -0
  43. package/dist/game/lootTable.d.ts +4 -0
  44. package/dist/game/lootTable.js +16 -0
  45. package/dist/game/playableGame.d.ts +84 -17
  46. package/dist/game/toasts.d.ts +8 -0
  47. package/dist/game/toasts.js +7 -0
  48. package/dist/game/vfxInstance.d.ts +9 -0
  49. package/dist/game/vfxInstance.js +12 -1
  50. package/dist/game/viewports.d.ts +43 -0
  51. package/dist/game/viewports.js +46 -0
  52. package/dist/game/worldItem.d.ts +5 -1
  53. package/dist/game/worldItem.js +0 -1
  54. package/dist/input/actionBindings.d.ts +15 -1
  55. package/dist/input/actionBindings.js +14 -3
  56. package/dist/input/actionContexts.d.ts +25 -1
  57. package/dist/input/actionContexts.js +82 -25
  58. package/dist/input/axisInput.d.ts +4 -0
  59. package/dist/input/axisInput.js +4 -0
  60. package/dist/input/axisShaper.d.ts +73 -0
  61. package/dist/input/axisShaper.js +105 -0
  62. package/dist/input/gamepadModel.d.ts +36 -2
  63. package/dist/input/gamepadModel.js +69 -30
  64. package/dist/input/haptics.d.ts +50 -0
  65. package/dist/input/haptics.js +74 -0
  66. package/dist/input/inputBuffer.d.ts +40 -0
  67. package/dist/input/inputBuffer.js +59 -0
  68. package/dist/input/lookChannel.d.ts +10 -0
  69. package/dist/input/lookChannel.js +6 -0
  70. package/dist/input/pointer.d.ts +2 -0
  71. package/dist/input/pointer.js +4 -0
  72. package/dist/item/affix.d.ts +6 -0
  73. package/dist/item/affix.js +6 -0
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +112 -1
  76. package/dist/movement/glideModel.d.ts +3 -0
  77. package/dist/movement/glideModel.js +6 -0
  78. package/dist/movement/grappleSwing.d.ts +2 -0
  79. package/dist/movement/grappleSwing.js +5 -1
  80. package/dist/movement/movementModel.d.ts +39 -0
  81. package/dist/movement/movementModel.js +62 -19
  82. package/dist/movement/movementProbe.d.ts +51 -0
  83. package/dist/movement/movementProbe.js +119 -0
  84. package/dist/movement/playerMovement.d.ts +21 -2
  85. package/dist/movement/playerMovement.js +89 -6
  86. package/dist/movement/solidObstacles.d.ts +9 -1
  87. package/dist/movement/solidObstacles.js +44 -2
  88. package/dist/movement/voxelController.js +7 -7
  89. package/dist/nav/navFromEnvironment.d.ts +9 -3
  90. package/dist/nav/navFromEnvironment.js +18 -12
  91. package/dist/nav/navMesh.d.ts +43 -2
  92. package/dist/nav/navMesh.js +534 -128
  93. package/dist/physics/aircraftDynamics.d.ts +390 -0
  94. package/dist/physics/aircraftDynamics.js +704 -0
  95. package/dist/physics/boatDynamics.d.ts +116 -0
  96. package/dist/physics/boatDynamics.js +160 -0
  97. package/dist/physics/drivableVehicle.d.ts +22 -8
  98. package/dist/physics/drivableVehicle.js +4 -2
  99. package/dist/physics/flightDynamics.d.ts +21 -4
  100. package/dist/physics/flightDynamics.js +28 -5
  101. package/dist/physics/handlingProbe.d.ts +249 -0
  102. package/dist/physics/handlingProbe.js +606 -0
  103. package/dist/physics/kinematicVehicle.d.ts +32 -2
  104. package/dist/physics/kinematicVehicle.js +15 -0
  105. package/dist/physics/physicsBackend.d.ts +12 -0
  106. package/dist/physics/physicsBackendConformance.js +34 -0
  107. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  108. package/dist/physics/physicsWorldBackend.js +33 -0
  109. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  110. package/dist/physics/vehicleBackendLink.js +102 -0
  111. package/dist/physics/vehicleDynamics.d.ts +374 -0
  112. package/dist/physics/vehicleDynamics.js +737 -0
  113. package/dist/physics/worldColliders.d.ts +1 -1
  114. package/dist/physics/worldColliders.js +35 -3
  115. package/dist/puzzle/cellGrid.d.ts +15 -9
  116. package/dist/puzzle/cellGrid.js +13 -9
  117. package/dist/render/lookPreset.d.ts +9 -2
  118. package/dist/render/lookPreset.js +29 -2
  119. package/dist/render/postProcessing.d.ts +30 -0
  120. package/dist/render/postProcessing.js +1 -1
  121. package/dist/runtime/cameraDirector.d.ts +17 -3
  122. package/dist/runtime/cameraDirector.js +25 -0
  123. package/dist/runtime/commandInput.d.ts +37 -0
  124. package/dist/runtime/commandInput.js +58 -0
  125. package/dist/runtime/commandRunner.d.ts +27 -0
  126. package/dist/runtime/commandRunner.js +31 -3
  127. package/dist/runtime/context/combat.d.ts +1 -0
  128. package/dist/runtime/context/combat.js +2 -1
  129. package/dist/runtime/context/deathLoot.d.ts +2 -0
  130. package/dist/runtime/context/deathLoot.js +1 -0
  131. package/dist/runtime/gameContext.js +7 -0
  132. package/dist/runtime/gameContextTypes.d.ts +19 -3
  133. package/dist/runtime/gameRuntime.d.ts +3 -1
  134. package/dist/runtime/gameRuntime.js +4 -0
  135. package/dist/runtime/inputSnapshot.d.ts +6 -0
  136. package/dist/runtime/inputSnapshot.js +10 -0
  137. package/dist/runtime/localPlayers.d.ts +73 -0
  138. package/dist/runtime/localPlayers.js +119 -0
  139. package/dist/runtime/transport.d.ts +9 -0
  140. package/dist/runtime/transport.js +7 -1
  141. package/dist/scene/behaviorRuntime.d.ts +12 -2
  142. package/dist/scene/behaviorRuntime.js +30 -14
  143. package/dist/scene/behaviors.d.ts +5 -1
  144. package/dist/scene/sceneKinds.d.ts +9 -0
  145. package/dist/scene/vehicleSeat.d.ts +5 -1
  146. package/dist/scene/vehicleSeat.js +5 -1
  147. package/dist/sensor/concealment.d.ts +6 -0
  148. package/dist/sensor/concealment.js +8 -0
  149. package/dist/sensor/freezeMonitor.d.ts +6 -0
  150. package/dist/sensor/freezeMonitor.js +8 -0
  151. package/dist/sensor/perception.d.ts +11 -1
  152. package/dist/sensor/perception.js +57 -10
  153. package/dist/sensor/recordingBuffer.d.ts +3 -0
  154. package/dist/sensor/recordingBuffer.js +8 -0
  155. package/dist/session/roundState.d.ts +4 -0
  156. package/dist/session/roundState.js +11 -0
  157. package/dist/settings/frameRateLimit.d.ts +16 -0
  158. package/dist/settings/frameRateLimit.js +37 -0
  159. package/dist/settings/graphicsProfile.d.ts +2 -0
  160. package/dist/settings/graphicsProfile.js +12 -0
  161. package/dist/settings/graphicsSettings.d.ts +40 -0
  162. package/dist/settings/graphicsSettings.js +81 -0
  163. package/dist/settings/settingsModel.d.ts +9 -1
  164. package/dist/settings/settingsModel.js +10 -0
  165. package/dist/stats/eventMeter.d.ts +1 -1
  166. package/dist/stats/eventMeter.js +1 -1
  167. package/dist/vfx/feedbackMixer.d.ts +61 -0
  168. package/dist/vfx/feedbackMixer.js +106 -0
  169. package/dist/visibility/simulationCulling.d.ts +8 -1
  170. package/dist/visibility/simulationCulling.js +7 -0
  171. package/dist/visibility/spatialIndex.d.ts +21 -0
  172. package/dist/visibility/spatialIndex.js +16 -0
  173. package/dist/world/authoredSolids.d.ts +46 -0
  174. package/dist/world/authoredSolids.js +75 -0
  175. package/dist/world/buildingGenerator.js +2 -2
  176. package/dist/world/buildingKit.d.ts +3 -1
  177. package/dist/world/buildings.d.ts +34 -2
  178. package/dist/world/buildings.js +11 -1
  179. package/dist/world/cityKind.d.ts +7 -0
  180. package/dist/world/cityKind.js +52 -1
  181. package/dist/world/features.d.ts +29 -2
  182. package/dist/world/features.js +6 -0
  183. package/dist/world/footprintGrid.d.ts +7 -0
  184. package/dist/world/footprintGrid.js +16 -0
  185. package/dist/world/interiors.d.ts +1 -0
  186. package/dist/world/interiors.js +5 -3
  187. package/dist/world/lod.d.ts +6 -0
  188. package/dist/world/lod.js +8 -0
  189. package/dist/world/terraform.d.ts +8 -0
  190. package/dist/world/terraform.js +7 -0
  191. package/dist/world/walls.d.ts +7 -0
  192. package/dist/world/walls.js +7 -0
  193. package/dist/world/worldSolids.d.ts +84 -0
  194. package/dist/world/worldSolids.js +240 -0
  195. package/dist/world.d.ts +2 -2
  196. package/dist/world.js +1 -1
  197. package/package.json +1 -1
  198. package/skills/ce-handoff/SKILL.md +1 -1
  199. package/skills/game-design/SKILL.md +2 -2
  200. package/skills/jgengine/SKILL.md +7 -5
  201. package/skills/jgengine/api.md +118 -89
  202. package/skills/jgengine/capabilities.md +28 -0
  203. package/skills/jgengine/recipes/minimal-game.md +3 -0
  204. package/skills/jgengine-combat/SKILL.md +5 -0
  205. package/skills/jgengine-combat/api.md +138 -103
  206. package/skills/jgengine-combat/capabilities.md +29 -1
  207. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  208. package/skills/jgengine-gameplay/api.md +462 -436
  209. package/skills/jgengine-gameplay/capabilities.md +24 -0
  210. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  211. package/skills/jgengine-world/SKILL.md +15 -3
  212. package/skills/jgengine-world/api.md +873 -626
  213. package/skills/jgengine-world/capabilities.md +130 -6
  214. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  215. package/skills/jgengine-world/recipes/controls.md +44 -0
  216. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  217. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  218. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  219. package/skills/jgengine-world/reference.md +31 -2
  220. package/skills/jgengine-world/visibility.md +112 -0
@@ -0,0 +1,115 @@
1
+ import type { WeaponHandlingFrame } from "./weaponHandling.js";
2
+ /** Camera-space position `[x, y, z]` in metres: `+x` right, `+y` up, `-z` forward. */
3
+ export type ViewmodelOffset = readonly [number, number, number];
4
+ /**
5
+ * How one weapon sits in first person. Offsets are camera-space metres, angles are radians, FOVs are
6
+ * degrees. Every field is optional; two weapons differ by the ones they set.
7
+ */
8
+ export interface WeaponPresentationTuning {
9
+ /** Viewmodel anchor at the hip (default `[0.34, -0.26, -0.72]`). */
10
+ hip?: ViewmodelOffset;
11
+ /** Viewmodel anchor fully aimed; put the sight on the camera axis (default `[0, -0.07, -0.55]`). */
12
+ ads?: ViewmodelOffset;
13
+ /** Viewmodel FOV in degrees, independent of the world FOV; omit to draw the viewmodel at the world FOV. */
14
+ viewmodelFov?: {
15
+ hip?: number;
16
+ ads?: number;
17
+ };
18
+ /** World FOV multiplier at full ADS, e.g. `0.8` for a light zoom (default `1`). */
19
+ adsZoom?: number;
20
+ /** Viewmodel lag behind look motion. */
21
+ sway?: {
22
+ /** Radians of lag per rad/s of look speed (default `0.02`). */
23
+ perRadPerSec?: number;
24
+ /** Largest lag, rad (default `0.08`). */
25
+ max?: number;
26
+ /** How fast lag follows its target, 1/s (default `12`). */
27
+ response?: number;
28
+ /** Lag multiplier at full ADS (default `0.3`). */
29
+ adsScale?: number;
30
+ };
31
+ /** Walk bob driven by the movement probe's speed and `bobPhase`. */
32
+ bob?: {
33
+ /** Side and vertical amplitude in metres at `fullSpeed` (default `[0.012, 0.01]`). */
34
+ amplitude?: readonly [number, number];
35
+ /** Ground speed that gives the full amplitude, m/s (default `4`). */
36
+ fullSpeed?: number;
37
+ /** Bob multiplier at full ADS (default `0.25`). */
38
+ adsScale?: number;
39
+ };
40
+ /** Viewmodel reaction to the handling's recoil offset. */
41
+ kick?: {
42
+ /** Metres pushed back per radian of recoil (default `1.2`). */
43
+ back?: number;
44
+ /** Viewmodel muzzle rise per radian of recoil (default `1.5`). */
45
+ rise?: number;
46
+ };
47
+ }
48
+ /** One frame of inputs: look motion, the movement probe, and the weapon's handling readout. */
49
+ export interface WeaponPresentationInput {
50
+ /** Look yaw speed, rad/s, positive turning left (the same sign as a camera yaw that grows left). */
51
+ lookYawRate: number;
52
+ /** Look pitch speed, rad/s, positive looking up. */
53
+ lookPitchRate: number;
54
+ /** Horizontal ground speed, m/s (e.g. `EntityRenderCues.speed`). */
55
+ speed: number;
56
+ /** `0..1` walk-cycle phase (e.g. `EntityRenderCues.bobPhase`). */
57
+ bobPhase: number;
58
+ handling: WeaponHandlingFrame;
59
+ }
60
+ /**
61
+ * Where the viewmodel and camera go this frame. First person applies all of it; third person and
62
+ * over-the-shoulder read `lookPitch`/`lookYaw`, `aimPitch`/`aimYaw` and `adsProgress` from the same frame.
63
+ */
64
+ export interface WeaponPose {
65
+ /** Viewmodel anchor, camera-space metres. */
66
+ offset: [number, number, number];
67
+ /** Viewmodel rotation about its anchor, rad: `pitch > 0` muzzle up, `yaw > 0` muzzle left, `roll > 0` tilts left. */
68
+ pitch: number;
69
+ yaw: number;
70
+ roll: number;
71
+ /** Viewmodel FOV in degrees, or `null` to use the world FOV. */
72
+ viewmodelFov: number | null;
73
+ /** Multiplier for the world FOV (ADS zoom). */
74
+ fovScale: number;
75
+ /** Offset to add to the camera look, rad: aim kick plus view punch. `lookYaw > 0` pulls right. */
76
+ lookPitch: number;
77
+ lookYaw: number;
78
+ /** Offset shots follow, rad; a third-person rig or upper-body aim reads this. */
79
+ aimPitch: number;
80
+ aimYaw: number;
81
+ /** `0` hip … `1` fully aimed. */
82
+ adsProgress: number;
83
+ }
84
+ /** Serializable presentation state: the current sway lag. */
85
+ export interface WeaponPresentationState {
86
+ swayPitch: number;
87
+ swayYaw: number;
88
+ }
89
+ /** Turns a {@link WeaponHandlingFrame} plus look and movement into a viewmodel and camera pose. */
90
+ export interface WeaponPresentation {
91
+ /** Advance sway by `dt` and return the pose. The returned object is reused between calls. */
92
+ update(dt: number, input: WeaponPresentationInput): WeaponPose;
93
+ pose(): WeaponPose;
94
+ retune(next: WeaponPresentationTuning): void;
95
+ snapshot(): WeaponPresentationState;
96
+ restore(state: WeaponPresentationState): void;
97
+ reset(): void;
98
+ }
99
+ /** Default hip anchor, matching the shell's built-in viewmodel. */
100
+ export declare const DEFAULT_VIEWMODEL_HIP: ViewmodelOffset;
101
+ /** Default ADS anchor: the built-in viewmodel's top rail on the camera axis. */
102
+ export declare const DEFAULT_VIEWMODEL_ADS: ViewmodelOffset;
103
+ /**
104
+ * Creates a {@link WeaponPresentation}: per-weapon viewmodel offset, ADS pose and viewmodel FOV, sway
105
+ * and inertia from look speed, bob from the movement probe, and a recoil kick, all driven by the same
106
+ * handling frame third-person and over-the-shoulder rigs read. Deterministic and allocation-free per update.
107
+ * @capability weapon-presentation per-weapon viewmodel offset, ADS pose, viewmodel FOV, sway, bob and recoil kick from a weapon handling frame
108
+ */
109
+ export declare function createWeaponPresentation(initial?: WeaponPresentationTuning): WeaponPresentation;
110
+ /**
111
+ * Scale for a viewmodel drawn at `viewmodelFov` through a camera at `worldFov` (both degrees): multiply
112
+ * the viewmodel's camera-space x and y by this and it projects as if rendered with its own FOV, without
113
+ * a second camera or render pass.
114
+ */
115
+ export declare function viewmodelFovScale(worldFov: number, viewmodelFov: number): number;
@@ -0,0 +1,96 @@
1
+ /** Default hip anchor, matching the shell's built-in viewmodel. */
2
+ export const DEFAULT_VIEWMODEL_HIP = [0.34, -0.26, -0.72];
3
+ /** Default ADS anchor: the built-in viewmodel's top rail on the camera axis. */
4
+ export const DEFAULT_VIEWMODEL_ADS = [0, -0.07, -0.55];
5
+ const TAU = Math.PI * 2;
6
+ const DEFAULT_BOB = [0.012, 0.01];
7
+ function lerp(hip, aimed, ads) {
8
+ return hip * (1 - ads) + aimed * ads;
9
+ }
10
+ function clamp(value, limit) {
11
+ return Math.max(-limit, Math.min(limit, value));
12
+ }
13
+ /**
14
+ * Creates a {@link WeaponPresentation}: per-weapon viewmodel offset, ADS pose and viewmodel FOV, sway
15
+ * and inertia from look speed, bob from the movement probe, and a recoil kick, all driven by the same
16
+ * handling frame third-person and over-the-shoulder rigs read. Deterministic and allocation-free per update.
17
+ * @capability weapon-presentation per-weapon viewmodel offset, ADS pose, viewmodel FOV, sway, bob and recoil kick from a weapon handling frame
18
+ */
19
+ export function createWeaponPresentation(initial = {}) {
20
+ let tuning = initial;
21
+ const state = { swayPitch: 0, swayYaw: 0 };
22
+ const out = {
23
+ offset: [...(tuning.hip ?? DEFAULT_VIEWMODEL_HIP)],
24
+ pitch: 0,
25
+ yaw: 0,
26
+ roll: 0,
27
+ viewmodelFov: tuning.viewmodelFov?.hip ?? null,
28
+ fovScale: 1,
29
+ lookPitch: 0,
30
+ lookYaw: 0,
31
+ aimPitch: 0,
32
+ aimYaw: 0,
33
+ adsProgress: 0,
34
+ };
35
+ return {
36
+ update(dt, input) {
37
+ const step = Math.max(0, dt);
38
+ const handling = input.handling;
39
+ const ads = Math.max(0, Math.min(1, handling.adsProgress));
40
+ const sway = tuning.sway;
41
+ const swayMax = sway?.max ?? 0.08;
42
+ const swayScale = lerp(1, sway?.adsScale ?? 0.3, ads);
43
+ const per = sway?.perRadPerSec ?? 0.02;
44
+ const targetYaw = clamp(-input.lookYawRate * per, swayMax) * swayScale;
45
+ const targetPitch = clamp(-input.lookPitchRate * per, swayMax) * swayScale;
46
+ const follow = 1 - Math.exp(-(sway?.response ?? 12) * step);
47
+ state.swayYaw += (targetYaw - state.swayYaw) * follow;
48
+ state.swayPitch += (targetPitch - state.swayPitch) * follow;
49
+ const bob = tuning.bob;
50
+ const [bobX, bobY] = bob?.amplitude ?? DEFAULT_BOB;
51
+ const bobScale = Math.min(1, Math.max(0, input.speed) / Math.max(1e-6, bob?.fullSpeed ?? 4)) * lerp(1, bob?.adsScale ?? 0.25, ads);
52
+ const phase = input.bobPhase * TAU;
53
+ const recoil = Math.hypot(handling.aimPitch + handling.cameraPitch, handling.aimYaw + handling.cameraYaw);
54
+ const hip = tuning.hip ?? DEFAULT_VIEWMODEL_HIP;
55
+ const aimed = tuning.ads ?? DEFAULT_VIEWMODEL_ADS;
56
+ out.offset[0] = lerp(hip[0], aimed[0], ads) + Math.sin(phase) * bobX * bobScale;
57
+ out.offset[1] = lerp(hip[1], aimed[1], ads) - Math.abs(Math.sin(phase)) * bobY * bobScale;
58
+ out.offset[2] = lerp(hip[2], aimed[2], ads) + recoil * (tuning.kick?.back ?? 1.2);
59
+ out.pitch = state.swayPitch + recoil * (tuning.kick?.rise ?? 1.5);
60
+ out.yaw = state.swayYaw;
61
+ out.roll = state.swayYaw * 0.5;
62
+ const hipFov = tuning.viewmodelFov?.hip ?? tuning.viewmodelFov?.ads;
63
+ const adsFov = tuning.viewmodelFov?.ads ?? hipFov;
64
+ out.viewmodelFov = hipFov === undefined || adsFov === undefined ? null : lerp(hipFov, adsFov, ads);
65
+ out.fovScale = lerp(1, tuning.adsZoom ?? 1, ads);
66
+ out.lookPitch = handling.aimPitch + handling.cameraPitch;
67
+ out.lookYaw = handling.aimYaw + handling.cameraYaw;
68
+ out.aimPitch = handling.aimPitch;
69
+ out.aimYaw = handling.aimYaw;
70
+ out.adsProgress = ads;
71
+ return out;
72
+ },
73
+ pose: () => out,
74
+ retune(next) {
75
+ tuning = next;
76
+ },
77
+ snapshot: () => ({ ...state }),
78
+ restore(next) {
79
+ state.swayPitch = next.swayPitch;
80
+ state.swayYaw = next.swayYaw;
81
+ },
82
+ reset() {
83
+ state.swayPitch = 0;
84
+ state.swayYaw = 0;
85
+ },
86
+ };
87
+ }
88
+ /**
89
+ * Scale for a viewmodel drawn at `viewmodelFov` through a camera at `worldFov` (both degrees): multiply
90
+ * the viewmodel's camera-space x and y by this and it projects as if rendered with its own FOV, without
91
+ * a second camera or render pass.
92
+ */
93
+ export function viewmodelFovScale(worldFov, viewmodelFov) {
94
+ const toHalf = (deg) => Math.tan((Math.max(1, Math.min(179, deg)) * Math.PI) / 360);
95
+ return toHalf(worldFov) / toHalf(viewmodelFov);
96
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Logs a dev-mode `console.warn` once per `site`, and never in a production build. Reach for it
3
+ * where a permissive config would otherwise fail silently — a clipped sky dome, an ignored tint.
4
+ * @capability warn-once emit a dev-only warning a single time per call site
5
+ */
6
+ export declare function warnOnce(site: string, message: string): void;
7
+ /** Clears the once-per-site record so a test can observe the warning again. @internal */
8
+ export declare function resetWarnOnce(): void;
@@ -0,0 +1,18 @@
1
+ import { isProductionEnvironment } from "./saveEndpoint.js";
2
+ const warned = new Set();
3
+ /**
4
+ * Logs a dev-mode `console.warn` once per `site`, and never in a production build. Reach for it
5
+ * where a permissive config would otherwise fail silently — a clipped sky dome, an ignored tint.
6
+ * @capability warn-once emit a dev-only warning a single time per call site
7
+ */
8
+ export function warnOnce(site, message) {
9
+ if (warned.has(site) || isProductionEnvironment())
10
+ return;
11
+ warned.add(site);
12
+ const host = globalThis;
13
+ host.console?.warn?.(message);
14
+ }
15
+ /** Clears the once-per-site record so a test can observe the warning again. @internal */
16
+ export function resetWarnOnce() {
17
+ warned.clear();
18
+ }
@@ -19,6 +19,8 @@ function cloneEnvironment(environment) {
19
19
  ...(environment.zenithColor === undefined ? {} : { zenithColor: environment.zenithColor }),
20
20
  ...(environment.sunIntensity === undefined ? {} : { sunIntensity: environment.sunIntensity }),
21
21
  ...(environment.ambientIntensity === undefined ? {} : { ambientIntensity: environment.ambientIntensity }),
22
+ ...(environment.sunAzimuth === undefined ? {} : { sunAzimuth: environment.sunAzimuth }),
23
+ ...(environment.sunElevation === undefined ? {} : { sunElevation: environment.sunElevation }),
22
24
  ...(environment.fog === undefined ? {} : { fog: { ...environment.fog } }),
23
25
  };
24
26
  }
@@ -1082,7 +1084,7 @@ function decodeEnvironment(value, path, errors) {
1082
1084
  failed = true;
1083
1085
  }
1084
1086
  }
1085
- for (const key of ["sunIntensity", "ambientIntensity"]) {
1087
+ for (const key of ["sunIntensity", "ambientIntensity", "sunAzimuth", "sunElevation"]) {
1086
1088
  if (value[key] === undefined)
1087
1089
  continue;
1088
1090
  if (typeof value[key] === "number" && Number.isFinite(value[key]))
@@ -25,6 +25,14 @@ export function skyConfigFromEnvironment(env) {
25
25
  ...(env.zenithColor === undefined ? {} : { zenithColor: env.zenithColor }),
26
26
  ...(env.sunIntensity === undefined ? {} : { sunIntensity: env.sunIntensity }),
27
27
  ...(env.ambientIntensity === undefined ? {} : { ambientIntensity: env.ambientIntensity }),
28
+ ...(env.sunAzimuth === undefined && env.sunElevation === undefined
29
+ ? {}
30
+ : {
31
+ sun: {
32
+ ...(env.sunAzimuth === undefined ? {} : { azimuth: env.sunAzimuth }),
33
+ ...(env.sunElevation === undefined ? {} : { elevation: env.sunElevation }),
34
+ },
35
+ }),
28
36
  ...(env.fog === undefined ? {} : { fog: { ...env.fog } }),
29
37
  };
30
38
  }
@@ -219,6 +219,10 @@ export interface EditorEnvironment {
219
219
  zenithColor?: string;
220
220
  sunIntensity?: number;
221
221
  ambientIntensity?: number;
222
+ /** Sun compass bearing in degrees (0 = -Z, 90 = +X); runtime `SkySunConfig.azimuth`. */
223
+ sunAzimuth?: number;
224
+ /** Sun height above the horizon in degrees; runtime `SkySunConfig.elevation`. */
225
+ sunElevation?: number;
222
226
  fog?: EditorFogConfig;
223
227
  }
224
228
  /** The full authored scene: every marker, volume, path, note, and sculpted terrain for a game. */
@@ -17,6 +17,10 @@ export interface ReputationLedgerConfig {
17
17
  min?: number;
18
18
  max?: number;
19
19
  }
20
+ /** Plain JSON standings of a {@link ReputationLedger}: stored values only, `initial` stays config. */
21
+ export interface ReputationLedgerState {
22
+ records: Record<string, Record<string, number>>;
23
+ }
20
24
  export interface ReputationLedger {
21
25
  standing(actorId: string, factionId: string): number;
22
26
  hasStanding(actorId: string, factionId: string): boolean;
@@ -26,6 +30,8 @@ export interface ReputationLedger {
26
30
  relation(actorId: string, factionId: string): FactionRelation;
27
31
  standings(actorId: string): Record<string, number>;
28
32
  reset(actorId: string, factionId?: string): void;
33
+ snapshot(): ReputationLedgerState;
34
+ restore(next: ReputationLedgerState): void;
29
35
  }
30
36
  export declare function createReputationLedger(config?: ReputationLedgerConfig): ReputationLedger;
31
37
  export interface EffectiveRelationInput {
@@ -77,6 +77,17 @@ export function createReputationLedger(config = {}) {
77
77
  else
78
78
  records.get(actorId)?.delete(factionId);
79
79
  },
80
+ snapshot() {
81
+ const out = {};
82
+ for (const [actorId, row] of records)
83
+ out[actorId] = Object.fromEntries(row);
84
+ return { records: out };
85
+ },
86
+ restore(next) {
87
+ records.clear();
88
+ for (const [actorId, row] of Object.entries(next.records))
89
+ records.set(actorId, new Map(Object.entries(row)));
90
+ },
80
91
  };
81
92
  }
82
93
  export function effectiveRelation(input) {
@@ -1,3 +1,5 @@
1
+ import type { WeaponHandlingFrame } from "../combat/weaponHandling.js";
2
+ import type { WeaponPresentationTuning } from "../combat/weaponPresentation.js";
1
3
  export interface CameraFollowState {
2
4
  entityId: string;
3
5
  target: {
@@ -12,6 +14,11 @@ export interface CameraFollowState {
12
14
  };
13
15
  distance: number;
14
16
  }
17
+ /** What a camera rig reads about the held weapon each frame; see `GameCameraConfig.weapon`. */
18
+ export interface CameraWeaponView {
19
+ handling: WeaponHandlingFrame;
20
+ presentation?: WeaponPresentationTuning;
21
+ }
15
22
  export interface FirstPersonCameraConfig {
16
23
  /** Camera height above the followed entity's origin. Default 1.6. */
17
24
  eyeHeight?: number;
@@ -170,11 +177,47 @@ export interface ChaseCameraConfig {
170
177
  lookHeight?: number;
171
178
  /** Spring-arm damping (higher = stiffer, less lag). Default 6. */
172
179
  springDamping?: number;
173
- /** Speed→FOV curve: FOV lerps from `base` to `max` as speed climbs to `speedForMax`. */
180
+ /**
181
+ * Speed→FOV curve: FOV lerps from `base` to `max` as speed climbs to `speedForMax`. Speed is the
182
+ * followed entity's sim `velocity` when it publishes one. `response` (default 6) eases the FOV
183
+ * toward the curve with `1-exp(-response*dt)`; `Infinity` applies it unsmoothed.
184
+ */
174
185
  fov?: {
175
186
  base?: number;
176
187
  max?: number;
177
188
  speedForMax?: number;
189
+ response?: number;
190
+ };
191
+ /** Pull the boom back by up to `extra` world units as speed climbs to `speedForMax` (default the FOV curve's `speedForMax`). */
192
+ distanceBySpeed?: {
193
+ extra: number;
194
+ speedForMax?: number;
195
+ };
196
+ /**
197
+ * Tilt the boom with the target's body pitch (`rotationX`) so the camera stays behind it on
198
+ * slopes and ramps. `blend` (default 1) is the fraction of the pitch followed, clamped to `max`
199
+ * radians (default 0.6) and eased by `response` (default 5). Omit the block to keep a level boom.
200
+ */
201
+ pitchFollow?: {
202
+ blend?: number;
203
+ max?: number;
204
+ response?: number;
205
+ };
206
+ /** How `ctx.camera.kickFov(degrees)` impulses feel: the kick decays at `decay` per second (default 7) and the summed kick is clamped to `max` degrees (default 20). */
207
+ fovKick?: {
208
+ decay?: number;
209
+ max?: number;
210
+ };
211
+ /** Input action that, while held, swings the chase view to look back at what is behind the target. */
212
+ lookBackAction?: string;
213
+ /**
214
+ * Pull the boom in past scene colliders, objects, terrain and walls via `ctx.scene.raycast`
215
+ * (entities never block). `radius` (default 0.3) keeps the lens off the surface; `minDistance`
216
+ * (default 0.8) is the closest the camera gets to the look pivot. Default on; `false` disables.
217
+ */
218
+ collision?: boolean | {
219
+ radius?: number;
220
+ minDistance?: number;
178
221
  };
179
222
  /** Procedural shake amplitude per unit of speed (adds to the trauma channel). Default 0.0. */
180
223
  shakePerSpeed?: number;
@@ -446,6 +489,13 @@ export interface GameCameraConfig {
446
489
  followEntityId?: string | null;
447
490
  /** Fired each frame after cameraFollow lock is applied. */
448
491
  onCameraFollow?: (state: CameraFollowState) => void;
492
+ /**
493
+ * The followed entity's held weapon, read every frame by the `first` and `shoulder` rigs. Return the
494
+ * weapon's `createWeaponHandling` frame and its presentation tuning, or `null` for none. First person
495
+ * poses the viewmodel from it (offset, ADS pose, viewmodel FOV, sway, bob, kick) and adds the recoil to
496
+ * the look; over-the-shoulder takes its ADS blend from `adsProgress` and adds the same recoil.
497
+ */
498
+ weapon?: (entityId: string) => CameraWeaponView | null;
449
499
  rotateSpeed?: number;
450
500
  zoomSpeed?: number;
451
501
  dampingFactor?: number;
@@ -236,6 +236,12 @@ export interface AudioLoopSetEvent {
236
236
  rate?: number;
237
237
  gain?: number;
238
238
  at?: readonly [number, number, number];
239
+ /** Emitter velocity (world units/s) for doppler on sounds that declare `doppler`. */
240
+ velocity?: readonly [number, number, number];
241
+ /** Lowpass cutoff in Hz. */
242
+ lowpass?: number;
243
+ /** Highpass cutoff in Hz. */
244
+ highpass?: number;
239
245
  }
240
246
  /** Stop and dispose the retained loop `id`; an unknown `id` is ignored (#1051). */
241
247
  export interface AudioLoopStopEvent {
@@ -38,6 +38,10 @@ export interface LootRegistry {
38
38
  register(def: LootTableDef): void;
39
39
  has(id: string): boolean;
40
40
  roll(id: string, rng?: () => number): Drop[];
41
+ /** Registered tables in registration order; plain JSON unless an entry uses `generate`. */
42
+ snapshot(): LootTableDef[];
43
+ /** Replaces every registered table with `next`, validating each. */
44
+ restore(next: readonly LootTableDef[]): void;
41
45
  }
42
46
  /** Options for {@link createLootRegistry} — inject a default RNG so bare `roll(id)` uses the world stream. */
43
47
  export interface LootRegistryOptions {
@@ -1,4 +1,10 @@
1
1
  import { resolveRng } from "../random/resolveRng.js";
2
+ function copyDef(def) {
3
+ return {
4
+ ...def,
5
+ entries: def.entries.map((entry) => ({ ...entry, count: typeof entry.count === "number" ? entry.count : [entry.count[0], entry.count[1]] })),
6
+ };
7
+ }
2
8
  function assertValidEntry(entry, mode) {
3
9
  const kinds = [entry.item !== undefined, entry.currency !== undefined, entry.generate !== undefined].filter(Boolean).length;
4
10
  if (kinds !== 1) {
@@ -88,6 +94,16 @@ export function createLootRegistry(options = {}) {
88
94
  }
89
95
  return drops;
90
96
  },
97
+ snapshot() {
98
+ return [...tables.values()].map(copyDef);
99
+ },
100
+ restore(next) {
101
+ for (const def of next)
102
+ assertValidDef(def);
103
+ tables.clear();
104
+ for (const def of next)
105
+ tables.set(def.id, copyDef(def));
106
+ },
91
107
  };
92
108
  }
93
109
  /**
@@ -6,6 +6,8 @@ import type { PostProcessingConfig } from "../render/postProcessing.js";
6
6
  import type { EnvironmentSource } from "../render/environment.js";
7
7
  import type { SpriteAtlas } from "../assets/spriteAtlas.js";
8
8
  import type { LookPreset } from "../render/lookPreset.js";
9
+ import type { GamepadFeelConfig } from "../input/gamepadModel.js";
10
+ import type { LocalPlayersConfig } from "../runtime/localPlayers.js";
9
11
  import type { TouchControlsConfig } from "../input/touchScheme.js";
10
12
  import type { GameSettingsConfig } from "../settings/settingsModel.js";
11
13
  import type { GraphicsProfile } from "../settings/graphicsProfile.js";
@@ -22,14 +24,21 @@ import type { SkyEnvironmentConfig } from "../world/features.js";
22
24
  import type { VisibilityConfig } from "../visibility/config.js";
23
25
  import type { GameDefinition, GameLoop } from "./defineGame.js";
24
26
  import type { GameCameraConfig } from "./cameraConfig.js";
27
+ import type { ViewportsConfig } from "./viewports.js";
25
28
  import type { LootFilterRule } from "./lootFilter.js";
26
29
  import type { RarityStyle } from "./worldItem.js";
27
- export type { CameraFollowState, CameraKeyframe, CameraProjection, CameraRigKind, CameraShakeConfig, ChaseCameraConfig, ChaseView, CinematicCameraConfig, FirstPersonCameraConfig, GameCameraConfig, InspectionCameraConfig, InspectionZoomAnchor, LockOnCameraConfig, ObserverCameraConfig, PlayerFovConfig, RtsCameraConfig, ShoulderCameraConfig, SideScrollCameraConfig, TopDownCameraConfig, TurntableCameraConfig, } from "./cameraConfig.js";
30
+ export type { CameraFollowState, CameraKeyframe, CameraProjection, CameraRigKind, CameraWeaponView, CameraShakeConfig, ChaseCameraConfig, ChaseView, CinematicCameraConfig, FirstPersonCameraConfig, GameCameraConfig, InspectionCameraConfig, InspectionZoomAnchor, LockOnCameraConfig, ObserverCameraConfig, PlayerFovConfig, RtsCameraConfig, ShoulderCameraConfig, SideScrollCameraConfig, TopDownCameraConfig, TurntableCameraConfig, } from "./cameraConfig.js";
28
31
  export { CAMERA_FRUSTUM_DEFAULTS } from "./cameraConfig.js";
32
+ /**
33
+ * Mouse/touch pointer bindings for `defineGame({ pointer })`.
34
+ *
35
+ * @capability pointer-commands click an entity or the ground to run a command (click-to-target, click-to-move, grab loot, marquee select)
36
+ */
29
37
  export interface PointerConfig {
30
38
  /**
31
- * Left-click on open ground runs this command with `{ point, entity, object }`
32
- * (click-to-move / ground-target). Suppresses the default left-click hotbar fire.
39
+ * Left-click runs this command with `{ point, entity, object }`; `entity` is the clicked entity id or
40
+ * null, so the same command covers click-to-move, ground-target and click-to-target
41
+ * (`ctx.scene.entity.setTarget`). Suppresses the default left-click hotbar fire.
33
42
  */
34
43
  moveCommand?: string;
35
44
  /** Enable left-drag marquee + single-click box-select of entities (RTS unit command). */
@@ -236,17 +245,14 @@ export interface ModelConfig {
236
245
  * rig's bind pose.
237
246
  */
238
247
  animation?: ModelAnimationConfig | "auto" | "none";
239
- /** Optional runtime inverse-kinematics targets for a rigged model. Bone names are resolved on the loaded scene. */
240
- ik?: {
241
- feet: readonly {
242
- root: string;
243
- mid: string;
244
- tip: string;
245
- }[];
246
- lookAt?: {
247
- bone: string;
248
- };
249
- };
248
+ /**
249
+ * Runtime inverse kinematics for a rigged model, applied after the animation mixer each frame.
250
+ * Feet follow slopes and steps while keeping each clip's swing lift, soles never sink below the
251
+ * ground, the pelvis drops so the lower leg can reach, and the correction fades out while airborne.
252
+ * `"auto"` finds the legs by bone name (`UpLeg`, `UpperLeg`, `Thigh`); bone names resolve on the
253
+ * loaded scene.
254
+ */
255
+ ik?: "auto" | ModelIkConfig;
250
256
  /** Props/weapons parented to named bones on this model's rig; each follows its bone through animation. */
251
257
  attachments?: readonly ModelAttachment[];
252
258
  /** Static kit-of-parts pieces stacked at fixed local offsets — no bone/rig involved. Use this for a compound entity assembled from several modular meshes (a castle keep from base + mid + roof pieces); use `attachments` for props parented to an animated rig's bones. Tag parts with a `role` to procedurally animate a rig-less character composition. */
@@ -254,6 +260,25 @@ export interface ModelConfig {
254
260
  /** Tuning for the procedural part-motion driver when any part carries a `role`; omit for defaults. */
255
261
  partMotion?: PartMotionParams;
256
262
  }
263
+ /** Bone names and limits for {@link ModelConfig.ik}. */
264
+ export interface ModelIkConfig {
265
+ /** Thigh → shin → foot chains; omit to find them by bone name. `[]` turns foot IK off. */
266
+ feet?: readonly {
267
+ root: string;
268
+ mid: string;
269
+ tip: string;
270
+ }[];
271
+ /** Bone shifted down when a foot has to reach lower ground; defaults to the first thigh's parent. */
272
+ pelvis?: string;
273
+ /** Largest foot or pelvis correction, as a fraction of leg length. Default `0.4`. */
274
+ maxAdjust?: number;
275
+ /** Tilt planted feet to the ground normal, `0..1`. Default `1`. */
276
+ alignToGround?: number;
277
+ /** Turns this bone toward the camera. */
278
+ lookAt?: {
279
+ bone: string;
280
+ };
281
+ }
257
282
  export interface ObjectStyle {
258
283
  color?: string;
259
284
  opacity?: number;
@@ -328,7 +353,7 @@ export interface BackdropConfig {
328
353
  sky?: SkyEnvironmentConfig;
329
354
  fog?: BackdropFogConfig;
330
355
  }
331
- /** Free-flight families moved into the walk controller so one seam covers ground + air — creative/spectator/noclip/hover are weightless, aircraft/rotorcraft stay on `flightDynamics`. */
356
+ /** Free-flight families moved into the walk controller so one seam covers ground + air — creative/spectator/noclip/hover are weightless, aircraft, rotorcraft and rockets use `physics/aircraftDynamics`. */
332
357
  export interface FlightConfig {
333
358
  /** Which free-flight family to use when flight is active. Default "creative". */
334
359
  mode?: "creative" | "spectator" | "noclip" | "hover";
@@ -355,6 +380,35 @@ export interface FlightConfig {
355
380
  /** Gate flight behind live game state — creative toggle, stamina check, etc. `false` stays walking. Default true when `flight` is present. */
356
381
  canFly?: (ctx: GameContext) => boolean;
357
382
  }
383
+ /** Feel knobs for the built-in walk controller; see {@link PlayerMovementConfig.feel}. */
384
+ export interface MovementFeelConfig {
385
+ /** Ground velocity response, 1/s (default 26). Lower drifts into and out of motion; higher is snappy. */
386
+ groundAcceleration?: number;
387
+ /** Air velocity response, 1/s (default 12). `0` commits to the jump arc; high values steer freely in the air. */
388
+ airAcceleration?: number;
389
+ /** Stopping response on the ground with no input, 1/s (default 18). Lower slides like ice. */
390
+ groundFriction?: number;
391
+ /** Sprint speed as a multiple of walk speed (default 2.25). */
392
+ runMultiplier?: number;
393
+ /** Crouch speed as a multiple of walk speed (default 0.45). */
394
+ crouchMultiplier?: number;
395
+ /** A jump pressed up to this many ms before landing fires on landing (default 0). */
396
+ jumpBufferMs?: number;
397
+ /** A jump pressed up to this many ms after walking off a ledge still fires (default 0). */
398
+ coyoteMs?: number;
399
+ /** Rising speed kept when jump is released early, 0..1 (default 1: no cut). Lower makes a tap a hop. */
400
+ jumpCutFactor?: number;
401
+ /** Gravity multiplier near the peak, while vertical speed is under `apexSpeed` (default 1). Below 1 hangs. */
402
+ apexGravityScale?: number;
403
+ /** Vertical speed under which `apexGravityScale` applies, m/s (default 1.5). */
404
+ apexSpeed?: number;
405
+ /** Gravity multiplier while falling (default 1). Above 1 drops faster than it rose. */
406
+ fallGravityScale?: number;
407
+ /** After landing, jumps wait and walk speed ramps back from `landingSpeedScale` over this many ms (default 0). */
408
+ landingRecoveryMs?: number;
409
+ /** Walk speed share right after landing when `landingRecoveryMs` is set (default 0.5). */
410
+ landingSpeedScale?: number;
411
+ }
358
412
  /** Movement-control levers for the shell-driven local player walk controller. */
359
413
  export interface PlayerMovementConfig {
360
414
  /** "free" (default) moves camera-relative across the plane; "axis" locks travel to one world axis; "grid" snaps each committed position to cell centers. */
@@ -379,6 +433,13 @@ export interface PlayerMovementConfig {
379
433
  canSprint?: (ctx: GameContext) => boolean;
380
434
  /** Fraction of walk speed while backpedalling (holding `moveBack`). Overrides the engine default (0.65). */
381
435
  backpedalMult?: number;
436
+ /**
437
+ * Walk-controller feel. Acceleration and friction are exponential response rates (1/s): higher reaches
438
+ * the target velocity or stops sooner. Omitted fields keep the engine defaults, so nothing changes until a
439
+ * game sets one. The jump fields apply to the kinematic walk; a `physics.backend` capsule or `collision.voxel`
440
+ * body jumps on the press frame with plain gravity. Tune against `measureMovement` (`@jgengine/core/movement/movementProbe`).
441
+ */
442
+ feel?: MovementFeelConfig;
382
443
  /** Radians/second the rendered body rotates toward its movement heading (shortest arc), so strafing/backpedalling read as a turn rather than an instant flip; also the rate the internally-integrated `turnLeft`/`turnRight` heading turns when the shell doesn't own yaw. Unset = body facing snaps instantly (no change to existing feel). */
383
444
  turnSpeed?: number;
384
445
  /** On-foot swimming when the terrain declares a `waterLevel` and the player is submerged: caps speed and floats them at the surface. `true` uses defaults; default off. */
@@ -393,8 +454,8 @@ export interface PlayerMovementConfig {
393
454
  * Free-flight — creative/spectator/noclip/hover — folded into the same walk controller so a game
394
455
  * doesn't hand-roll a second movement loop to get Minecraft-like flight. `true` uses creative defaults;
395
456
  * an object tunes speed/vertical/sprint/collide/canFly. Horizontal is always yaw-relative strafe
396
- * (A = left, D = right — never roll/bank), vertical is Space/Ctrl, and aircraft/rotorcraft remain
397
- * on `physics/flightDynamics`. The shell drives this automatically when present; `canFly` toggles
457
+ * (A = left, D = right — never roll/bank), vertical is Space/Ctrl, and aircraft, rotorcraft and rockets
458
+ * use `physics/aircraftDynamics`. The shell drives this automatically when present; `canFly` toggles
398
459
  * between walking and flying per tick without rebuilding the config.
399
460
  */
400
461
  flight?: FlightConfig | boolean;
@@ -537,6 +598,12 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
537
598
  pointer?: PointerConfig;
538
599
  /** Touch controls on coarse-pointer devices. Unset derives a scheme from `input` (virtual joystick for movement actions, on-screen buttons for the rest); a config refines it with gestures and curated buttons; `false` opts out. */
539
600
  touch?: TouchControlsConfig | false;
601
+ /** Gamepad stick deadzone, response curve and trigger deadzone the shell's pad poll applies before publishing analog values. Unset keeps the shell defaults (axial `0.12`/`0.95`, linear). */
602
+ gamepad?: GamepadFeelConfig;
603
+ /** Local seats on this screen for couch co-op. With `maxSlots > 1` each extra pad hot-joins its own seat on its first button press, the shell calls `loop.onNewPlayer(ctx, { userId, isNew: true })` for it, and its input lands on `localPlayers(ctx).local(slotId).input` instead of `ctx.input`. Unset keeps one seat: every device drives `ctx.player`. */
604
+ localPlayers?: LocalPlayersConfig;
605
+ /** Split-screen for local seats (`localPlayers`): one viewport per joined seat, each following its own entity. The primary seat keeps the main camera rig; post-processing is skipped while more than one viewport is drawn. */
606
+ viewports?: ViewportsConfig;
540
607
  /** Phone orientation contract. Legacy `"landscape"`/`"portrait"` stays advisory (a dismissible rotate hint). The object form `{ mobile: "landscape-required" }` is strict — the shell shows an engine-owned rotate screen and blocks gameplay until the device is turned. See `GameOrientation`. */
541
608
  orientation?: GameOrientation;
542
609
  /** Where the game is meant to be played. Default `["web", "mobile"]` — design-resolution HUD fit is on for every game: `HudCanvas` auto-scales from `hudFit.designSize` down to the live viewport, so the desktop layout shrinks to fit a phone instead of overflowing it. Declare `["web"]` to opt a desktop-only game out (compact displays fall back to the legacy fixed 0.85 zoom). */
@@ -15,6 +15,11 @@ export interface Toast<T = string> {
15
15
  export declare function appendToast<T>(toasts: readonly Toast<T>[], toast: Toast<T>, cap: number): readonly Toast<T>[];
16
16
  /** Drop every toast whose `expiresAt` is at or before `now`. Returns the same array when nothing expired. */
17
17
  export declare function pruneToasts<T>(toasts: readonly Toast<T>[], now: number): readonly Toast<T>[];
18
+ /** Plain JSON state of a {@link ToastQueue}: live toasts plus the id counter. */
19
+ export interface ToastQueueState<T = string> {
20
+ toasts: Toast<T>[];
21
+ counter: number;
22
+ }
18
23
  /** Stateful transient-toast list with a size cap and time-to-live eviction. */
19
24
  export interface ToastQueue<T = string> {
20
25
  /** Raise a toast at `now` living `ttlSeconds` (falling back to the queue default); returns it. */
@@ -25,6 +30,9 @@ export interface ToastQueue<T = string> {
25
30
  list(): readonly Toast<T>[];
26
31
  /** Remove all toasts. */
27
32
  clear(): void;
33
+ /** Copies the queue; each toast's `body` is shared, since the queue never mutates it. */
34
+ snapshot(): ToastQueueState<T>;
35
+ restore(next: ToastQueueState<T>): void;
28
36
  }
29
37
  /** Options for {@link createToastQueue}. */
30
38
  export interface ToastQueueOptions {