@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.
- package/CHANGELOG.md +158 -7
- package/dist/ai/decisionGraph.d.ts +48 -4
- package/dist/ai/decisionGraph.js +153 -23
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +11 -0
- package/dist/anim/animGraph.js +128 -0
- package/dist/anim/boneTexture.d.ts +55 -0
- package/dist/anim/boneTexture.js +43 -0
- package/dist/anim/footPlacement.d.ts +57 -0
- package/dist/anim/footPlacement.js +67 -0
- package/dist/anim/locomotionGraph.d.ts +9 -0
- package/dist/anim/locomotionGraph.js +29 -0
- package/dist/audio/audioFalloff.d.ts +2 -0
- package/dist/audio/doppler.d.ts +21 -0
- package/dist/audio/doppler.js +32 -0
- package/dist/audio/engineLayers.d.ts +83 -0
- package/dist/audio/engineLayers.js +115 -0
- package/dist/combat/abilityKit.d.ts +1 -0
- package/dist/combat/abilityKit.js +1 -0
- package/dist/combat/death.d.ts +7 -0
- package/dist/combat/shotOrigin.d.ts +6 -0
- package/dist/combat/shotOrigin.js +6 -0
- package/dist/combat/weaponFeedback.d.ts +36 -0
- package/dist/combat/weaponFeedback.js +42 -0
- package/dist/combat/weaponHandling.d.ts +161 -0
- package/dist/combat/weaponHandling.js +202 -0
- package/dist/combat/weaponPresentation.d.ts +115 -0
- package/dist/combat/weaponPresentation.js +96 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/editor/document.js +3 -1
- package/dist/editor/environment.js +8 -0
- package/dist/editor/types.d.ts +4 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +51 -1
- package/dist/game/events.d.ts +6 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/playableGame.d.ts +84 -17
- package/dist/game/toasts.d.ts +8 -0
- package/dist/game/toasts.js +7 -0
- package/dist/game/vfxInstance.d.ts +9 -0
- package/dist/game/vfxInstance.js +12 -1
- package/dist/game/viewports.d.ts +43 -0
- package/dist/game/viewports.js +46 -0
- package/dist/game/worldItem.d.ts +5 -1
- package/dist/game/worldItem.js +0 -1
- package/dist/input/actionBindings.d.ts +15 -1
- package/dist/input/actionBindings.js +14 -3
- package/dist/input/actionContexts.d.ts +25 -1
- package/dist/input/actionContexts.js +82 -25
- package/dist/input/axisInput.d.ts +4 -0
- package/dist/input/axisInput.js +4 -0
- package/dist/input/axisShaper.d.ts +73 -0
- package/dist/input/axisShaper.js +105 -0
- package/dist/input/gamepadModel.d.ts +36 -2
- package/dist/input/gamepadModel.js +69 -30
- package/dist/input/haptics.d.ts +50 -0
- package/dist/input/haptics.js +74 -0
- package/dist/input/inputBuffer.d.ts +40 -0
- package/dist/input/inputBuffer.js +59 -0
- package/dist/input/lookChannel.d.ts +10 -0
- package/dist/input/lookChannel.js +6 -0
- package/dist/input/pointer.d.ts +2 -0
- package/dist/input/pointer.js +4 -0
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +112 -1
- package/dist/movement/glideModel.d.ts +3 -0
- package/dist/movement/glideModel.js +6 -0
- package/dist/movement/grappleSwing.d.ts +2 -0
- package/dist/movement/grappleSwing.js +5 -1
- package/dist/movement/movementModel.d.ts +39 -0
- package/dist/movement/movementModel.js +62 -19
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +21 -2
- package/dist/movement/playerMovement.js +89 -6
- package/dist/movement/solidObstacles.d.ts +9 -1
- package/dist/movement/solidObstacles.js +44 -2
- package/dist/movement/voxelController.js +7 -7
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/boatDynamics.d.ts +116 -0
- package/dist/physics/boatDynamics.js +160 -0
- package/dist/physics/drivableVehicle.d.ts +22 -8
- package/dist/physics/drivableVehicle.js +4 -2
- package/dist/physics/flightDynamics.d.ts +21 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +32 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +12 -0
- package/dist/physics/physicsBackendConformance.js +34 -0
- package/dist/physics/physicsWorldBackend.d.ts +1 -0
- package/dist/physics/physicsWorldBackend.js +33 -0
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +374 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/worldColliders.d.ts +1 -1
- package/dist/physics/worldColliders.js +35 -3
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/render/lookPreset.d.ts +9 -2
- package/dist/render/lookPreset.js +29 -2
- package/dist/render/postProcessing.d.ts +30 -0
- package/dist/render/postProcessing.js +1 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +37 -0
- package/dist/runtime/commandInput.js +58 -0
- package/dist/runtime/commandRunner.d.ts +27 -0
- package/dist/runtime/commandRunner.js +31 -3
- package/dist/runtime/context/combat.d.ts +1 -0
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/deathLoot.d.ts +2 -0
- package/dist/runtime/context/deathLoot.js +1 -0
- package/dist/runtime/gameContext.js +7 -0
- package/dist/runtime/gameContextTypes.d.ts +19 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/inputSnapshot.d.ts +6 -0
- package/dist/runtime/inputSnapshot.js +10 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/scene/behaviorRuntime.d.ts +12 -2
- package/dist/scene/behaviorRuntime.js +30 -14
- package/dist/scene/behaviors.d.ts +5 -1
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/vehicleSeat.d.ts +5 -1
- package/dist/scene/vehicleSeat.js +5 -1
- package/dist/sensor/concealment.d.ts +6 -0
- package/dist/sensor/concealment.js +8 -0
- package/dist/sensor/freezeMonitor.d.ts +6 -0
- package/dist/sensor/freezeMonitor.js +8 -0
- package/dist/sensor/perception.d.ts +11 -1
- package/dist/sensor/perception.js +57 -10
- package/dist/sensor/recordingBuffer.d.ts +3 -0
- package/dist/sensor/recordingBuffer.js +8 -0
- package/dist/session/roundState.d.ts +4 -0
- package/dist/session/roundState.js +11 -0
- package/dist/settings/frameRateLimit.d.ts +16 -0
- package/dist/settings/frameRateLimit.js +37 -0
- package/dist/settings/graphicsProfile.d.ts +2 -0
- package/dist/settings/graphicsProfile.js +12 -0
- package/dist/settings/graphicsSettings.d.ts +40 -0
- package/dist/settings/graphicsSettings.js +81 -0
- package/dist/settings/settingsModel.d.ts +9 -1
- package/dist/settings/settingsModel.js +10 -0
- package/dist/stats/eventMeter.d.ts +1 -1
- package/dist/stats/eventMeter.js +1 -1
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/visibility/simulationCulling.d.ts +8 -1
- package/dist/visibility/simulationCulling.js +7 -0
- package/dist/visibility/spatialIndex.d.ts +21 -0
- package/dist/visibility/spatialIndex.js +16 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +3 -1
- package/dist/world/buildings.d.ts +34 -2
- package/dist/world/buildings.js +11 -1
- package/dist/world/cityKind.d.ts +7 -0
- package/dist/world/cityKind.js +52 -1
- package/dist/world/features.d.ts +29 -2
- package/dist/world/features.js +6 -0
- package/dist/world/footprintGrid.d.ts +7 -0
- package/dist/world/footprintGrid.js +16 -0
- package/dist/world/interiors.d.ts +1 -0
- package/dist/world/interiors.js +5 -3
- package/dist/world/lod.d.ts +6 -0
- package/dist/world/lod.js +8 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/worldSolids.d.ts +84 -0
- package/dist/world/worldSolids.js +240 -0
- package/dist/world.d.ts +2 -2
- package/dist/world.js +1 -1
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +7 -5
- package/skills/jgengine/api.md +118 -89
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-combat/SKILL.md +5 -0
- package/skills/jgengine-combat/api.md +138 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-gameplay/api.md +462 -436
- package/skills/jgengine-gameplay/capabilities.md +24 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +15 -3
- package/skills/jgengine-world/api.md +873 -626
- package/skills/jgengine-world/capabilities.md +130 -6
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
package/dist/game/toasts.js
CHANGED
|
@@ -40,5 +40,12 @@ export function createToastQueue(options = {}) {
|
|
|
40
40
|
clear() {
|
|
41
41
|
toasts = [];
|
|
42
42
|
},
|
|
43
|
+
snapshot() {
|
|
44
|
+
return { toasts: toasts.map((toast) => ({ ...toast })), counter };
|
|
45
|
+
},
|
|
46
|
+
restore(next) {
|
|
47
|
+
toasts = next.toasts.map((toast) => ({ ...toast }));
|
|
48
|
+
counter = next.counter;
|
|
49
|
+
},
|
|
43
50
|
};
|
|
44
51
|
}
|
|
@@ -115,6 +115,15 @@ export interface VfxInstanceStore {
|
|
|
115
115
|
tick(nowMs?: number): number;
|
|
116
116
|
/** Dispose every instance (scene change / reset), emitting a `stop` op for each. */
|
|
117
117
|
clear(): void;
|
|
118
|
+
/** Plain JSON copy of every live instance plus the id counter. */
|
|
119
|
+
snapshot(): VfxInstanceStoreState;
|
|
120
|
+
/** Replaces the live set: emits `stop` for each current instance, then `upsert` for each restored one. */
|
|
121
|
+
restore(next: VfxInstanceStoreState): void;
|
|
122
|
+
}
|
|
123
|
+
/** Plain JSON state of a {@link VfxInstanceStore}. */
|
|
124
|
+
export interface VfxInstanceStoreState {
|
|
125
|
+
instances: VfxInstanceState[];
|
|
126
|
+
seq: number;
|
|
118
127
|
}
|
|
119
128
|
/** Options for {@link createVfxInstanceStore}. */
|
|
120
129
|
export interface VfxInstanceStoreOptions {
|
package/dist/game/vfxInstance.js
CHANGED
|
@@ -92,5 +92,16 @@ export function createVfxInstanceStore(options = {}) {
|
|
|
92
92
|
for (const id of [...instances.keys()])
|
|
93
93
|
stop(id);
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
function snapshot() {
|
|
96
|
+
return { instances: structuredClone([...instances.values()]), seq };
|
|
97
|
+
}
|
|
98
|
+
function restore(next) {
|
|
99
|
+
clear();
|
|
100
|
+
seq = next.seq;
|
|
101
|
+
for (const state of structuredClone(next.instances)) {
|
|
102
|
+
instances.set(state.id, state);
|
|
103
|
+
emit({ op: "upsert", id: state.id, instance: state });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { upsert, update, stop, get, list, count, tick, clear, snapshot, restore };
|
|
96
107
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ChaseCameraConfig, TopDownCameraConfig } from "./cameraConfig.js";
|
|
2
|
+
/** A screen region as fractions of the canvas, `[x, y, width, height]` in `0..1` with the origin at the top left. */
|
|
3
|
+
export type ViewportRect = readonly [number, number, number, number];
|
|
4
|
+
/** The follow camera an extra viewport runs; the primary seat keeps the game's main camera rig. */
|
|
5
|
+
export interface ViewportCameraConfig {
|
|
6
|
+
/** Default: `"topDown"` when the game's main rig is top-down, otherwise `"chase"`. */
|
|
7
|
+
rig?: "chase" | "topDown";
|
|
8
|
+
chase?: ChaseCameraConfig;
|
|
9
|
+
topDown?: TopDownCameraConfig;
|
|
10
|
+
}
|
|
11
|
+
/** One viewport: which local seat it shows and where. */
|
|
12
|
+
export interface ViewportDef {
|
|
13
|
+
/** Local seat id (`"slot:0"`, `"slot:1"`, …) from `localPlayers(ctx)`. */
|
|
14
|
+
slot: string;
|
|
15
|
+
rect: ViewportRect;
|
|
16
|
+
camera?: ViewportCameraConfig;
|
|
17
|
+
}
|
|
18
|
+
/** `defineGame({ viewports })`: split-screen layout for local seats. */
|
|
19
|
+
export interface ViewportsConfig {
|
|
20
|
+
/** `"auto"` (default) splits the screen evenly across the seats currently joined; a list pins explicit rects. */
|
|
21
|
+
layout?: "auto" | readonly ViewportDef[];
|
|
22
|
+
/** Two-seat split: `"vertical"` puts them side by side (default), `"horizontal"` stacks them. */
|
|
23
|
+
split?: "vertical" | "horizontal";
|
|
24
|
+
/** Follow camera for seats whose viewport has none. */
|
|
25
|
+
camera?: ViewportCameraConfig;
|
|
26
|
+
}
|
|
27
|
+
/** A viewport in canvas pixels with a bottom-left origin, ready for `gl.setViewport`/`setScissor`. */
|
|
28
|
+
export interface ViewportPixels {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Even split-screen rects for seats in order: one fills the screen, two split side by side (or stacked),
|
|
36
|
+
* three put the first on a full-width top half, four make a 2×2 grid, more fill a grid row by row.
|
|
37
|
+
* @capability split-screen Split the screen into one viewport per local seat, each with its own follow camera.
|
|
38
|
+
*/
|
|
39
|
+
export declare function splitViewports(slots: readonly string[], split?: "vertical" | "horizontal"): ViewportDef[];
|
|
40
|
+
/** The viewports to draw for the seats currently joined: the explicit layout filtered to joined seats, or an even split. */
|
|
41
|
+
export declare function resolveViewports(config: ViewportsConfig | undefined, slots: readonly string[]): ViewportDef[];
|
|
42
|
+
/** Convert a top-left fractional rect into whole canvas pixels with a bottom-left origin; neighbours share edges exactly. */
|
|
43
|
+
export declare function viewportPixels(rect: ViewportRect, width: number, height: number): ViewportPixels;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function gridRects(count) {
|
|
2
|
+
const columns = Math.ceil(Math.sqrt(count));
|
|
3
|
+
const rows = Math.ceil(count / columns);
|
|
4
|
+
const rects = [];
|
|
5
|
+
for (let index = 0; index < count; index += 1) {
|
|
6
|
+
const row = Math.floor(index / columns);
|
|
7
|
+
const inRow = row === rows - 1 ? count - row * columns : columns;
|
|
8
|
+
const column = index - row * columns;
|
|
9
|
+
rects.push([column / inRow, row / rows, 1 / inRow, 1 / rows]);
|
|
10
|
+
}
|
|
11
|
+
return rects;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Even split-screen rects for seats in order: one fills the screen, two split side by side (or stacked),
|
|
15
|
+
* three put the first on a full-width top half, four make a 2×2 grid, more fill a grid row by row.
|
|
16
|
+
* @capability split-screen Split the screen into one viewport per local seat, each with its own follow camera.
|
|
17
|
+
*/
|
|
18
|
+
export function splitViewports(slots, split = "vertical") {
|
|
19
|
+
const count = slots.length;
|
|
20
|
+
let rects;
|
|
21
|
+
if (count <= 1)
|
|
22
|
+
rects = [[0, 0, 1, 1]];
|
|
23
|
+
else if (count === 2) {
|
|
24
|
+
rects = split === "vertical" ? [[0, 0, 0.5, 1], [0.5, 0, 0.5, 1]] : [[0, 0, 1, 0.5], [0, 0.5, 1, 0.5]];
|
|
25
|
+
}
|
|
26
|
+
else if (count === 3)
|
|
27
|
+
rects = [[0, 0, 1, 0.5], [0, 0.5, 0.5, 0.5], [0.5, 0.5, 0.5, 0.5]];
|
|
28
|
+
else
|
|
29
|
+
rects = gridRects(count);
|
|
30
|
+
return slots.map((slot, index) => ({ slot, rect: rects[index] }));
|
|
31
|
+
}
|
|
32
|
+
/** The viewports to draw for the seats currently joined: the explicit layout filtered to joined seats, or an even split. */
|
|
33
|
+
export function resolveViewports(config, slots) {
|
|
34
|
+
const layout = config?.layout ?? "auto";
|
|
35
|
+
if (layout === "auto")
|
|
36
|
+
return splitViewports(slots, config?.split);
|
|
37
|
+
return layout.filter((def) => slots.includes(def.slot));
|
|
38
|
+
}
|
|
39
|
+
/** Convert a top-left fractional rect into whole canvas pixels with a bottom-left origin; neighbours share edges exactly. */
|
|
40
|
+
export function viewportPixels(rect, width, height) {
|
|
41
|
+
const left = Math.round(rect[0] * width);
|
|
42
|
+
const right = Math.round((rect[0] + rect[2]) * width);
|
|
43
|
+
const top = Math.round(rect[1] * height);
|
|
44
|
+
const bottom = Math.round((rect[1] + rect[3]) * height);
|
|
45
|
+
return { x: left, y: height - bottom, width: Math.max(1, right - left), height: Math.max(1, bottom - top) };
|
|
46
|
+
}
|
package/dist/game/worldItem.d.ts
CHANGED
|
@@ -62,7 +62,6 @@ export declare function selectNearestWorldItem(candidates: readonly {
|
|
|
62
62
|
/**
|
|
63
63
|
* Spawn and track pickup-able items scattered in the world, including drops that scatter on death.
|
|
64
64
|
*
|
|
65
|
-
* @capability world-drops spawn pickup-able items in the world, including death drops
|
|
66
65
|
* @internal
|
|
67
66
|
*/
|
|
68
67
|
export declare function createWorldItemStore(deps: WorldItemStoreDeps): WorldItemStore;
|
|
@@ -87,6 +86,11 @@ export interface ResolvedDeathDrops {
|
|
|
87
86
|
* @internal
|
|
88
87
|
*/
|
|
89
88
|
export declare function resolveDeathDrops(drops: readonly Drop[], options: ResolveDeathDropsOptions): ResolvedDeathDrops;
|
|
89
|
+
/**
|
|
90
|
+
* Per-rarity look of a dropped item: beam, color and label. Set via `defineGame({ worldItem: { rarityStyle } })`.
|
|
91
|
+
*
|
|
92
|
+
* @capability loot-beams rarity-colored loot beams and labels on dropped items via defineGame({ worldItem: { rarityStyle, beamHeight } })
|
|
93
|
+
*/
|
|
90
94
|
export interface RarityStyle {
|
|
91
95
|
color?: string;
|
|
92
96
|
beam?: boolean;
|
package/dist/game/worldItem.js
CHANGED
|
@@ -39,7 +39,6 @@ export function selectNearestWorldItem(candidates, from, radius) {
|
|
|
39
39
|
/**
|
|
40
40
|
* Spawn and track pickup-able items scattered in the world, including drops that scatter on death.
|
|
41
41
|
*
|
|
42
|
-
* @capability world-drops spawn pickup-able items in the world, including death drops
|
|
43
42
|
* @internal
|
|
44
43
|
*/
|
|
45
44
|
export function createWorldItemStore(deps) {
|
|
@@ -34,7 +34,13 @@ export type ActionCodes<TCode extends string = string> = readonly TCode[] | {
|
|
|
34
34
|
toggle?: readonly TCode[];
|
|
35
35
|
repeatMs?: number;
|
|
36
36
|
};
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Maps each game action name to the input codes (hold/toggle keys, repeat rate) that trigger it. In a
|
|
39
|
+
* `defineGame({ input })`, pressing an action runs the same-named command with `{ yaw, pitch, aim }`;
|
|
40
|
+
* `repeatMs` re-fires it while held (automatic weapons).
|
|
41
|
+
*
|
|
42
|
+
* @capability fire-input bind a key or mouse button to a named action that runs the same-named command with the aim; repeatMs auto-fires while held
|
|
43
|
+
*/
|
|
38
44
|
export type ActionCodesMap<TAction extends string = string, TCode extends string = string> = Record<TAction, ActionCodes<TCode>>;
|
|
39
45
|
/** @internal */
|
|
40
46
|
export declare function actionRepeatMs(codes: ActionCodes | undefined): number | undefined;
|
|
@@ -82,6 +88,14 @@ export interface ActionStateTracker<TAction extends string> {
|
|
|
82
88
|
wasPressed(action: TAction): boolean;
|
|
83
89
|
endFrame(): void;
|
|
84
90
|
reset(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Swap the binding map without dropping what is physically held: a key still down counts toward
|
|
93
|
+
* whatever action it maps to now, with no fresh press edge. Toggle state survives for actions that
|
|
94
|
+
* come back later.
|
|
95
|
+
*/
|
|
96
|
+
rebind(map: ActionStateBindingMap<TAction, string>): void;
|
|
97
|
+
/** The action names in the current binding map. */
|
|
98
|
+
actions(): readonly TAction[];
|
|
85
99
|
}
|
|
86
100
|
/** @internal */
|
|
87
101
|
export declare function createActionStateTracker<TAction extends string, TCode extends string = string>(map: ActionStateBindingMap<TAction, TCode>): ActionStateTracker<TAction>;
|
|
@@ -179,9 +179,9 @@ function resolveActionBindingModes(config) {
|
|
|
179
179
|
}
|
|
180
180
|
/** @internal */
|
|
181
181
|
export function createActionStateTracker(map) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
let actions = Object.keys(map);
|
|
183
|
+
let modesByAction = new Map(actions.map((action) => [action, resolveActionBindingModes(map[action])]));
|
|
184
|
+
let heldCodesByAction = new Map(actions.map((action) => [action, new Set()]));
|
|
185
185
|
const toggledActions = new Set();
|
|
186
186
|
const pressedThisFrame = new Set();
|
|
187
187
|
const activeCodes = new Set();
|
|
@@ -240,5 +240,16 @@ export function createActionStateTracker(map) {
|
|
|
240
240
|
pressedThisFrame.clear();
|
|
241
241
|
activeCodes.clear();
|
|
242
242
|
},
|
|
243
|
+
rebind(next) {
|
|
244
|
+
actions = Object.keys(next);
|
|
245
|
+
modesByAction = new Map(actions.map((action) => [action, resolveActionBindingModes(next[action])]));
|
|
246
|
+
heldCodesByAction = new Map(actions.map((action) => [action, new Set()]));
|
|
247
|
+
for (const code of activeCodes) {
|
|
248
|
+
const holdAction = findAction(code, (modes) => modes.hold);
|
|
249
|
+
if (holdAction !== null)
|
|
250
|
+
heldCodesByAction.get(holdAction).add(code);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
actions: () => actions,
|
|
243
254
|
};
|
|
244
255
|
}
|
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
import type { ActionCodesMap } from "./actionBindings.js";
|
|
2
|
+
import type { AxisBinding } from "./axisInput.js";
|
|
3
|
+
import type { AxisShapeConfig } from "./axisShaper.js";
|
|
4
|
+
/** Serializable axis shaping a context may carry: {@link AxisShapeConfig} without the `scale` callback. */
|
|
5
|
+
export type ActionContextAxisShape = Omit<AxisShapeConfig, "scale">;
|
|
2
6
|
/** Named action-binding layer that may optionally expose lower layers. */
|
|
3
7
|
export interface ActionContext {
|
|
4
8
|
id: string;
|
|
5
9
|
codes: ActionCodesMap;
|
|
6
10
|
passthrough: boolean;
|
|
11
|
+
/** Axis bindings (by action name) this context brings, e.g. `steer` while driving; read with `activeAxes()`. */
|
|
12
|
+
axes?: Record<string, AxisBinding>;
|
|
13
|
+
/** Per-axis shaping this context brings; feed it to `createAxisShaper(...).retune` when the stack changes. */
|
|
14
|
+
shaping?: Record<string, ActionContextAxisShape>;
|
|
7
15
|
}
|
|
8
16
|
/** Serializable state for an action-context stack. */
|
|
9
17
|
export interface ActionContextStackSnapshot {
|
|
10
18
|
contexts: ActionContext[];
|
|
11
19
|
}
|
|
20
|
+
/** Axis bindings and shaping merged across the active contexts. */
|
|
21
|
+
export interface ActiveContextAxes {
|
|
22
|
+
bindings: Record<string, AxisBinding>;
|
|
23
|
+
shaping: Record<string, ActionContextAxisShape>;
|
|
24
|
+
}
|
|
12
25
|
/** Mutable layered action-map stack with snapshot and restore support. */
|
|
13
26
|
export interface ActionContextStack {
|
|
14
27
|
push(context: ActionContext): void;
|
|
15
28
|
pop(id: string): boolean;
|
|
16
29
|
active(): ActionCodesMap;
|
|
30
|
+
/** Axis bindings and shaping of the active contexts, merged top-down like {@link active}. */
|
|
31
|
+
activeAxes(): ActiveContextAxes;
|
|
32
|
+
/** Ids of the stacked contexts, bottom first. */
|
|
33
|
+
ids(): readonly string[];
|
|
34
|
+
/** Bumps on every push, pop and restore; cheap change detection for per-frame readers. */
|
|
35
|
+
version(): number;
|
|
36
|
+
/** Called after every push, pop and restore; returns an unsubscribe. The shell rebinds live input here. */
|
|
37
|
+
subscribe(listener: () => void): () => void;
|
|
17
38
|
snapshot(): ActionContextStackSnapshot;
|
|
18
39
|
restore(snapshot: ActionContextStackSnapshot): void;
|
|
19
40
|
}
|
|
20
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Creates a serializable stack of layered action maps for menus and gameplay modes.
|
|
43
|
+
* @capability action-contexts Swap bindings live for on-foot, driving, menu or build modes by pushing and popping layered action maps.
|
|
44
|
+
*/
|
|
21
45
|
export declare function createActionContextStack(): ActionContextStack;
|
|
@@ -12,45 +12,102 @@ function cloneCodes(codes) {
|
|
|
12
12
|
}
|
|
13
13
|
return result;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
function cloneAxes(axes) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [axis, binding] of Object.entries(axes)) {
|
|
18
|
+
result[axis] = {
|
|
19
|
+
...binding,
|
|
20
|
+
positive: [...binding.positive],
|
|
21
|
+
...(binding.negative === undefined ? {} : { negative: [...binding.negative] }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
function cloneShaping(shaping) {
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [axis, shape] of Object.entries(shaping)) {
|
|
29
|
+
result[axis] = {
|
|
30
|
+
...(shape.digital === undefined ? {} : { digital: { ...shape.digital } }),
|
|
31
|
+
...(shape.analog === undefined ? {} : { analog: { ...shape.analog } }),
|
|
32
|
+
...(shape.range === undefined ? {} : { range: { ...shape.range } }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function cloneContext(context) {
|
|
38
|
+
return {
|
|
39
|
+
id: context.id,
|
|
40
|
+
codes: cloneCodes(context.codes),
|
|
41
|
+
passthrough: context.passthrough,
|
|
42
|
+
...(context.axes === undefined ? {} : { axes: cloneAxes(context.axes) }),
|
|
43
|
+
...(context.shaping === undefined ? {} : { shaping: cloneShaping(context.shaping) }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function mergeTopDown(contexts, pick) {
|
|
47
|
+
const merged = {};
|
|
48
|
+
for (let index = contexts.length - 1; index >= 0; index -= 1) {
|
|
49
|
+
const context = contexts[index];
|
|
50
|
+
const layer = pick(context);
|
|
51
|
+
if (layer !== undefined) {
|
|
52
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
53
|
+
if (merged[key] === undefined)
|
|
54
|
+
merged[key] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!context.passthrough)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
return merged;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a serializable stack of layered action maps for menus and gameplay modes.
|
|
64
|
+
* @capability action-contexts Swap bindings live for on-foot, driving, menu or build modes by pushing and popping layered action maps.
|
|
65
|
+
*/
|
|
16
66
|
export function createActionContextStack() {
|
|
17
67
|
let contexts = [];
|
|
68
|
+
let version = 0;
|
|
69
|
+
const listeners = new Set();
|
|
70
|
+
const changed = () => {
|
|
71
|
+
version += 1;
|
|
72
|
+
for (const listener of [...listeners])
|
|
73
|
+
listener();
|
|
74
|
+
};
|
|
18
75
|
return {
|
|
19
76
|
push(context) {
|
|
20
|
-
contexts = [...contexts.filter((current) => current.id !== context.id),
|
|
21
|
-
|
|
22
|
-
codes: cloneCodes(context.codes),
|
|
23
|
-
passthrough: context.passthrough,
|
|
24
|
-
}];
|
|
77
|
+
contexts = [...contexts.filter((current) => current.id !== context.id), cloneContext(context)];
|
|
78
|
+
changed();
|
|
25
79
|
},
|
|
26
80
|
pop(id) {
|
|
27
81
|
const next = contexts.filter((context) => context.id !== id);
|
|
28
|
-
const
|
|
82
|
+
const removed = next.length !== contexts.length;
|
|
29
83
|
contexts = next;
|
|
30
|
-
|
|
84
|
+
if (removed)
|
|
85
|
+
changed();
|
|
86
|
+
return removed;
|
|
31
87
|
},
|
|
32
88
|
active() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
89
|
+
return mergeTopDown(contexts, (context) => context.codes);
|
|
90
|
+
},
|
|
91
|
+
activeAxes() {
|
|
92
|
+
return {
|
|
93
|
+
bindings: mergeTopDown(contexts, (context) => context.axes),
|
|
94
|
+
shaping: mergeTopDown(contexts, (context) => context.shaping),
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
ids: () => contexts.map((context) => context.id),
|
|
98
|
+
version: () => version,
|
|
99
|
+
subscribe(listener) {
|
|
100
|
+
listeners.add(listener);
|
|
101
|
+
return () => {
|
|
102
|
+
listeners.delete(listener);
|
|
103
|
+
};
|
|
44
104
|
},
|
|
45
105
|
snapshot() {
|
|
46
|
-
return { contexts: contexts.map(
|
|
106
|
+
return { contexts: contexts.map(cloneContext) };
|
|
47
107
|
},
|
|
48
108
|
restore(snapshot) {
|
|
49
|
-
contexts = snapshot.contexts.map(
|
|
50
|
-
|
|
51
|
-
codes: cloneCodes(context.codes),
|
|
52
|
-
passthrough: context.passthrough,
|
|
53
|
-
}));
|
|
109
|
+
contexts = snapshot.contexts.map(cloneContext);
|
|
110
|
+
changed();
|
|
54
111
|
},
|
|
55
112
|
};
|
|
56
113
|
}
|
|
@@ -47,6 +47,10 @@ export declare class AxisChannel {
|
|
|
47
47
|
get value(): AxisInput;
|
|
48
48
|
sample(dt: number, isDown: (code: string) => boolean, pointer?: PointerAxisState | null): AxisInput;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Car axes bound to raw key codes, for an {@link AxisChannel} sampled with a key-code `isDown`. `ctx.input.axis`
|
|
52
|
+
* samples action names instead, so bind it to the game's input actions (e.g. `moveForward`), not this map.
|
|
53
|
+
*/
|
|
50
54
|
export declare const DRIVE_AXIS_BINDINGS: AxisBindingMap;
|
|
51
55
|
export interface GenericAxisConfig<TAxes extends string> {
|
|
52
56
|
bindings: Record<TAxes, AxisBinding>;
|
package/dist/input/axisInput.js
CHANGED
|
@@ -86,6 +86,10 @@ export class AxisChannel {
|
|
|
86
86
|
return this.current;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Car axes bound to raw key codes, for an {@link AxisChannel} sampled with a key-code `isDown`. `ctx.input.axis`
|
|
91
|
+
* samples action names instead, so bind it to the game's input actions (e.g. `moveForward`), not this map.
|
|
92
|
+
*/
|
|
89
93
|
export const DRIVE_AXIS_BINDINGS = {
|
|
90
94
|
throttle: { positive: ["KeyW", "ArrowUp"] },
|
|
91
95
|
brake: { positive: ["KeyS", "ArrowDown"] },
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AxisBinding, AxisRange } from "./axisInput.js";
|
|
2
|
+
/**
|
|
3
|
+
* How one axis responds to one kind of source. Keys want ramps (a held key should feel like a pedal
|
|
4
|
+
* being pressed, not a switch); sticks want deadzone and curve but no lag.
|
|
5
|
+
*/
|
|
6
|
+
export interface AxisShapeProfile {
|
|
7
|
+
/** Inner deadzone `0..1`; the remaining travel is rescaled so output still starts at 0 (default `0`). */
|
|
8
|
+
deadzone?: number;
|
|
9
|
+
/** Raw magnitude at which output reaches full scale, `0..1` (default `1`). */
|
|
10
|
+
saturation?: number;
|
|
11
|
+
/** Response exponent applied after the deadzone: `1` linear, `>1` finer near centre (default `1`). */
|
|
12
|
+
curve?: number;
|
|
13
|
+
/** Units/s the output may move away from zero (default `Infinity`: no ramp). */
|
|
14
|
+
riseRate?: number;
|
|
15
|
+
/** Units/s the output may move back toward zero while the input keeps its sign (default `riseRate`). */
|
|
16
|
+
fallRate?: number;
|
|
17
|
+
/** Units/s the output moves when the input is released or reversed — self-centring (default `fallRate`). */
|
|
18
|
+
returnRate?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Shaping for one named axis. */
|
|
21
|
+
export interface AxisShapeConfig {
|
|
22
|
+
/** Profile for keys and buttons (default: pass through). */
|
|
23
|
+
digital?: AxisShapeProfile;
|
|
24
|
+
/** Profile for sticks, triggers and other continuous sources (default: pass through). */
|
|
25
|
+
analog?: AxisShapeProfile;
|
|
26
|
+
/** Output range (default `[-1, 1]`; use `{ min: 0, max: 1 }` for pedals). */
|
|
27
|
+
range?: AxisRange;
|
|
28
|
+
/**
|
|
29
|
+
* Multiplier from a caller-supplied signal, applied to the target before ramping — speed-sensitive
|
|
30
|
+
* steering (`speed => 1 / (1 + speed / 30)`), weapon-sway-by-stamina, and similar. Default `1`.
|
|
31
|
+
*/
|
|
32
|
+
scale?: (signal: number) => number;
|
|
33
|
+
}
|
|
34
|
+
/** Config for {@link createAxisShaper}: one entry per axis. */
|
|
35
|
+
export interface AxisShaperConfig<TAxes extends string> {
|
|
36
|
+
axes: Record<TAxes, AxisShapeConfig>;
|
|
37
|
+
}
|
|
38
|
+
/** Per-sample options for {@link AxisShaper.shape}. */
|
|
39
|
+
export interface AxisShapeSample<TAxes extends string> {
|
|
40
|
+
/** Axes whose raw value came from an analog source this frame; unlisted axes use the digital profile. */
|
|
41
|
+
analog?: Partial<Record<TAxes, boolean>> | ReadonlySet<TAxes>;
|
|
42
|
+
/** Signal passed to each axis's `scale` callback (speed, stamina, zoom). */
|
|
43
|
+
signal?: number;
|
|
44
|
+
}
|
|
45
|
+
/** Shaped axis output, one number per axis. */
|
|
46
|
+
export type AxisValues<TAxes extends string> = Record<TAxes, number>;
|
|
47
|
+
/** Stateful per-axis response shaping between raw input and a sim. */
|
|
48
|
+
export interface AxisShaper<TAxes extends string> {
|
|
49
|
+
/** Shape one frame of raw axis values (as returned by `ctx.input.axis`) and return the shaped output. */
|
|
50
|
+
shape(dt: number, raw: Readonly<Record<TAxes, number>>, sample?: AxisShapeSample<TAxes>): AxisValues<TAxes>;
|
|
51
|
+
/** The last shaped output. */
|
|
52
|
+
value(): Readonly<AxisValues<TAxes>>;
|
|
53
|
+
/** Replace the shaping config (settings screen, per-vehicle feel) without resetting the current output. */
|
|
54
|
+
retune(next: AxisShaperConfig<TAxes>): void;
|
|
55
|
+
snapshot(): AxisValues<TAxes>;
|
|
56
|
+
restore(state: Readonly<AxisValues<TAxes>>): void;
|
|
57
|
+
/** Zero every axis. */
|
|
58
|
+
reset(): void;
|
|
59
|
+
}
|
|
60
|
+
/** Deadzone, saturation and curve applied to one raw value; sign is preserved. */
|
|
61
|
+
export declare function shapeAxisValue(raw: number, profile: AxisShapeProfile | undefined): number;
|
|
62
|
+
/**
|
|
63
|
+
* Creates an {@link AxisShaper}: deadzone, curve and ramp rates per axis, chosen per frame by whether the
|
|
64
|
+
* source was a key or a stick. It sits between `ctx.input.axis(...)` and any sim (a vehicle, an aircraft,
|
|
65
|
+
* aim), so a held key eases in and self-centres while a stick stays direct. Deterministic and serializable.
|
|
66
|
+
* @capability axis-shaping shape raw input per axis — deadzone, response curve, keyboard ramp, self-centring, speed-sensitive scaling
|
|
67
|
+
*/
|
|
68
|
+
export declare function createAxisShaper<TAxes extends string>(initial: AxisShaperConfig<TAxes>): AxisShaper<TAxes>;
|
|
69
|
+
/**
|
|
70
|
+
* Which axes an analog source is driving this frame: an axis counts as analog when any action bound to it
|
|
71
|
+
* has a published analog magnitude (`ctx.input.analog()`). Feed the result to {@link AxisShaper.shape}.
|
|
72
|
+
*/
|
|
73
|
+
export declare function analogAxes<TAxes extends string>(bindings: Readonly<Record<TAxes, AxisBinding>>, analog: Readonly<Record<string, number>> | null): Set<TAxes>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const FULL_RANGE = { min: -1, max: 1 };
|
|
2
|
+
function clampTo(value, range) {
|
|
3
|
+
return value < range.min ? range.min : value > range.max ? range.max : value;
|
|
4
|
+
}
|
|
5
|
+
/** Deadzone, saturation and curve applied to one raw value; sign is preserved. */
|
|
6
|
+
export function shapeAxisValue(raw, profile) {
|
|
7
|
+
if (profile === undefined)
|
|
8
|
+
return raw;
|
|
9
|
+
const deadzone = Math.min(0.99, Math.max(0, profile.deadzone ?? 0));
|
|
10
|
+
const saturation = Math.max(deadzone + 0.01, Math.min(1, profile.saturation ?? 1));
|
|
11
|
+
const magnitude = Math.abs(raw);
|
|
12
|
+
if (magnitude <= deadzone)
|
|
13
|
+
return 0;
|
|
14
|
+
const normalized = Math.min(1, (magnitude - deadzone) / (saturation - deadzone));
|
|
15
|
+
const curved = normalized ** Math.max(0.1, profile.curve ?? 1);
|
|
16
|
+
return Math.sign(raw) * curved;
|
|
17
|
+
}
|
|
18
|
+
function rateFor(current, target, profile) {
|
|
19
|
+
const rise = profile?.riseRate ?? Number.POSITIVE_INFINITY;
|
|
20
|
+
const fall = profile?.fallRate ?? rise;
|
|
21
|
+
const back = profile?.returnRate ?? fall;
|
|
22
|
+
if (target === 0 || (current !== 0 && Math.sign(target) !== Math.sign(current)))
|
|
23
|
+
return back;
|
|
24
|
+
return Math.abs(target) >= Math.abs(current) ? rise : fall;
|
|
25
|
+
}
|
|
26
|
+
function isAnalog(axis, analog) {
|
|
27
|
+
if (analog === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
if (analog instanceof Set)
|
|
30
|
+
return analog.has(axis);
|
|
31
|
+
return analog[axis] === true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates an {@link AxisShaper}: deadzone, curve and ramp rates per axis, chosen per frame by whether the
|
|
35
|
+
* source was a key or a stick. It sits between `ctx.input.axis(...)` and any sim (a vehicle, an aircraft,
|
|
36
|
+
* aim), so a held key eases in and self-centres while a stick stays direct. Deterministic and serializable.
|
|
37
|
+
* @capability axis-shaping shape raw input per axis — deadzone, response curve, keyboard ramp, self-centring, speed-sensitive scaling
|
|
38
|
+
*/
|
|
39
|
+
export function createAxisShaper(initial) {
|
|
40
|
+
let config = initial;
|
|
41
|
+
const names = Object.keys(config.axes);
|
|
42
|
+
const current = {};
|
|
43
|
+
for (const name of names)
|
|
44
|
+
current[name] = 0;
|
|
45
|
+
return {
|
|
46
|
+
shape(dt, raw, sample) {
|
|
47
|
+
const step = Math.max(0, dt);
|
|
48
|
+
for (const name of Object.keys(config.axes)) {
|
|
49
|
+
const axis = config.axes[name];
|
|
50
|
+
const range = axis.range ?? FULL_RANGE;
|
|
51
|
+
const profile = isAnalog(name, sample?.analog) ? axis.analog : axis.digital;
|
|
52
|
+
const scale = axis.scale === undefined ? 1 : axis.scale(sample?.signal ?? 0);
|
|
53
|
+
const target = clampTo(shapeAxisValue(raw[name] ?? 0, profile) * scale, range);
|
|
54
|
+
const from = current[name] ?? 0;
|
|
55
|
+
const rate = rateFor(from, target, profile);
|
|
56
|
+
let next;
|
|
57
|
+
if (!Number.isFinite(rate))
|
|
58
|
+
next = target;
|
|
59
|
+
else if (target !== 0 && from !== 0 && Math.sign(target) !== Math.sign(from)) {
|
|
60
|
+
// A reversal returns to centre at the return rate, then builds the other way at the rise rate.
|
|
61
|
+
const toZero = Math.min(Math.abs(from), rate * step);
|
|
62
|
+
const left = step - toZero / Math.max(rate, 1e-9);
|
|
63
|
+
next = Math.abs(from) > toZero ? from - Math.sign(from) * toZero : Math.sign(target) * Math.min(Math.abs(target), (profile?.riseRate ?? Number.POSITIVE_INFINITY) * left);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const delta = target - from;
|
|
67
|
+
const move = rate * step;
|
|
68
|
+
next = Math.abs(delta) <= move ? target : from + Math.sign(delta) * move;
|
|
69
|
+
}
|
|
70
|
+
current[name] = clampTo(next, range);
|
|
71
|
+
}
|
|
72
|
+
return current;
|
|
73
|
+
},
|
|
74
|
+
value: () => current,
|
|
75
|
+
retune(next) {
|
|
76
|
+
config = next;
|
|
77
|
+
for (const name of Object.keys(next.axes))
|
|
78
|
+
current[name] ??= 0;
|
|
79
|
+
},
|
|
80
|
+
snapshot: () => ({ ...current }),
|
|
81
|
+
restore(state) {
|
|
82
|
+
Object.assign(current, state);
|
|
83
|
+
},
|
|
84
|
+
reset() {
|
|
85
|
+
for (const name of Object.keys(current))
|
|
86
|
+
current[name] = 0;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Which axes an analog source is driving this frame: an axis counts as analog when any action bound to it
|
|
92
|
+
* has a published analog magnitude (`ctx.input.analog()`). Feed the result to {@link AxisShaper.shape}.
|
|
93
|
+
*/
|
|
94
|
+
export function analogAxes(bindings, analog) {
|
|
95
|
+
const out = new Set();
|
|
96
|
+
if (analog === null)
|
|
97
|
+
return out;
|
|
98
|
+
for (const name of Object.keys(bindings)) {
|
|
99
|
+
const binding = bindings[name];
|
|
100
|
+
const codes = binding.negative === undefined ? binding.positive : [...binding.positive, ...binding.negative];
|
|
101
|
+
if (codes.some((code) => analog[code] !== undefined))
|
|
102
|
+
out.add(name);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|