@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
|
@@ -29,17 +29,51 @@ export interface GamepadDeadzone {
|
|
|
29
29
|
inner: number;
|
|
30
30
|
outer: number;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Read-only view of one sampled gamepad. The browser `Gamepad` satisfies it structurally, so a poll
|
|
34
|
+
* loop can resolve `navigator.getGamepads()` entries without copying them.
|
|
35
|
+
*/
|
|
36
|
+
export interface GamepadSample {
|
|
37
|
+
readonly axes: ArrayLike<number>;
|
|
38
|
+
readonly buttons: ArrayLike<{
|
|
39
|
+
readonly pressed: boolean;
|
|
40
|
+
readonly value: number;
|
|
41
|
+
}>;
|
|
42
|
+
readonly connected: boolean;
|
|
43
|
+
}
|
|
32
44
|
/** Options for resolving one gamepad snapshot into action state. */
|
|
33
45
|
export interface ResolveGamepadFrameOptions {
|
|
34
46
|
deadzone: GamepadDeadzone;
|
|
47
|
+
/** Response exponent for sticks and analog buttons: `1` linear, `>1` finer near rest (default `1`). */
|
|
48
|
+
curve?: number;
|
|
49
|
+
/** Inner deadzone `0..1` for analog button values (triggers); below it the button reads 0 and is not held (default `0`). */
|
|
50
|
+
triggerDeadzone?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Game-level pad feel read by the shell's gamepad poll. A number `deadzone` is the axial inner
|
|
54
|
+
* deadzone with a `0.95` outer edge.
|
|
55
|
+
*/
|
|
56
|
+
export interface GamepadFeelConfig {
|
|
57
|
+
/** Stick deadzone (default axial `0.12` inner, `0.95` outer). */
|
|
58
|
+
deadzone?: number | GamepadDeadzone;
|
|
59
|
+
/** Response exponent shared by sticks and triggers (default `1`). */
|
|
35
60
|
curve?: number;
|
|
61
|
+
/** Trigger inner deadzone `0..1` (default `0`). */
|
|
62
|
+
triggerDeadzone?: number;
|
|
36
63
|
}
|
|
64
|
+
/** Shell default stick deadzone. */
|
|
65
|
+
export declare const DEFAULT_GAMEPAD_DEADZONE: GamepadDeadzone;
|
|
66
|
+
/** Resolve a {@link GamepadFeelConfig} into the options {@link resolveGamepadFrame} takes. */
|
|
67
|
+
export declare function gamepadFeelOptions(config: GamepadFeelConfig | undefined): ResolveGamepadFrameOptions;
|
|
37
68
|
/** The digital and analog action state produced by a gamepad frame. */
|
|
38
69
|
export interface GamepadFrame {
|
|
39
70
|
held: string[];
|
|
40
71
|
analog: Record<string, number>;
|
|
41
72
|
}
|
|
42
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Resolve one sampled gamepad into held actions and shaped analog action values. Sticks go through
|
|
75
|
+
* the deadzone and curve; analog buttons (triggers) through `triggerDeadzone` and the same curve.
|
|
76
|
+
* Pass `out` to reuse a frame across polls; it is cleared and returned.
|
|
43
77
|
* @capability gamepad-input Resolve sampled gamepad input into actions with deadzones and response curves.
|
|
44
78
|
*/
|
|
45
|
-
export declare function resolveGamepadFrame(snapshot:
|
|
79
|
+
export declare function resolveGamepadFrame(snapshot: GamepadSample, bindings: GamepadBindings, options: ResolveGamepadFrameOptions, out?: GamepadFrame): GamepadFrame;
|
|
@@ -7,63 +7,102 @@ export const GAMEPAD_GLYPH_SETS = {
|
|
|
7
7
|
};
|
|
8
8
|
/** Lowercase alias for consumers that prefer data-oriented naming. */
|
|
9
9
|
export const gamepadGlyphSets = GAMEPAD_GLYPH_SETS;
|
|
10
|
+
/** Shell default stick deadzone. */
|
|
11
|
+
export const DEFAULT_GAMEPAD_DEADZONE = { kind: "axial", inner: 0.12, outer: 0.95 };
|
|
12
|
+
/** Resolve a {@link GamepadFeelConfig} into the options {@link resolveGamepadFrame} takes. */
|
|
13
|
+
export function gamepadFeelOptions(config) {
|
|
14
|
+
const deadzone = config?.deadzone;
|
|
15
|
+
return {
|
|
16
|
+
deadzone: deadzone === undefined
|
|
17
|
+
? DEFAULT_GAMEPAD_DEADZONE
|
|
18
|
+
: typeof deadzone === "number"
|
|
19
|
+
? { kind: "axial", inner: deadzone, outer: DEFAULT_GAMEPAD_DEADZONE.outer }
|
|
20
|
+
: deadzone,
|
|
21
|
+
curve: config?.curve ?? 1,
|
|
22
|
+
triggerDeadzone: config?.triggerDeadzone ?? 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
function bindingCodes(codes) {
|
|
11
26
|
if (Array.isArray(codes))
|
|
12
27
|
return codes;
|
|
13
28
|
const modes = codes;
|
|
14
29
|
return [...(modes.hold ?? []), ...(modes.toggle ?? [])];
|
|
15
30
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
function clampUnit(value) {
|
|
32
|
+
return Number.isFinite(value) ? (value < -1 ? -1 : value > 1 ? 1 : value) : 0;
|
|
33
|
+
}
|
|
34
|
+
function rescale(magnitude, inner, outer, curve) {
|
|
35
|
+
if (magnitude <= inner)
|
|
19
36
|
return 0;
|
|
20
|
-
const normalized = magnitude >=
|
|
21
|
-
return
|
|
37
|
+
const normalized = magnitude >= outer ? 1 : (magnitude - inner) / (outer - inner);
|
|
38
|
+
return curve === 1 ? normalized : Math.pow(normalized, curve);
|
|
22
39
|
}
|
|
40
|
+
const axisCodes = new Map();
|
|
23
41
|
function readAxis(code) {
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
let parsed = axisCodes.get(code);
|
|
43
|
+
if (parsed === undefined) {
|
|
44
|
+
const match = /^padaxis:(\d+)([+-])$/.exec(code);
|
|
45
|
+
parsed = match === null ? null : { index: Number(match[1]), sign: match[2] === "+" ? 1 : -1 };
|
|
46
|
+
axisCodes.set(code, parsed);
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
26
49
|
}
|
|
27
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Resolve one sampled gamepad into held actions and shaped analog action values. Sticks go through
|
|
52
|
+
* the deadzone and curve; analog buttons (triggers) through `triggerDeadzone` and the same curve.
|
|
53
|
+
* Pass `out` to reuse a frame across polls; it is cleared and returned.
|
|
28
54
|
* @capability gamepad-input Resolve sampled gamepad input into actions with deadzones and response curves.
|
|
29
55
|
*/
|
|
30
|
-
export function resolveGamepadFrame(snapshot, bindings, options) {
|
|
56
|
+
export function resolveGamepadFrame(snapshot, bindings, options, out = { held: [], analog: {} }) {
|
|
57
|
+
out.held.length = 0;
|
|
58
|
+
for (const key in out.analog)
|
|
59
|
+
delete out.analog[key];
|
|
31
60
|
if (!snapshot.connected)
|
|
32
|
-
return
|
|
61
|
+
return out;
|
|
33
62
|
const inner = Math.max(0, options.deadzone.inner);
|
|
34
63
|
const outer = Math.max(inner + Number.EPSILON, options.deadzone.outer);
|
|
35
64
|
const curve = options.curve === undefined ? 1 : Math.max(Number.EPSILON, options.curve);
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
const triggerInner = Math.min(0.99, Math.max(0, options.triggerDeadzone ?? 0));
|
|
66
|
+
const radial = options.deadzone.kind === "radial";
|
|
67
|
+
let radialFactor = 0;
|
|
68
|
+
if (radial) {
|
|
69
|
+
let sumSquares = 0;
|
|
70
|
+
for (let index = 0; index < snapshot.axes.length; index += 1) {
|
|
71
|
+
const value = clampUnit(snapshot.axes[index]);
|
|
72
|
+
sumSquares += value * value;
|
|
73
|
+
}
|
|
74
|
+
const magnitude = Math.sqrt(sumSquares);
|
|
75
|
+
radialFactor = magnitude === 0 ? 0 : rescale(Math.min(1, magnitude), inner, outer, curve) / magnitude;
|
|
44
76
|
}
|
|
45
|
-
const
|
|
46
|
-
const analog = {};
|
|
47
|
-
for (const action of Object.keys(bindings)) {
|
|
77
|
+
for (const action in bindings) {
|
|
48
78
|
let actionAnalog = 0;
|
|
49
79
|
let isHeld = false;
|
|
50
80
|
for (const code of bindingCodes(bindings[action])) {
|
|
51
81
|
if (code.startsWith("pad:")) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
const button = snapshot.buttons[Number(code.slice(4))];
|
|
83
|
+
if (button === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
const raw = Number.isFinite(button.value) ? Math.max(0, Math.min(1, button.value)) : 0;
|
|
86
|
+
const value = rescale(raw, triggerInner, 1, curve);
|
|
87
|
+
if (button.pressed && (value > 0 || raw === 0))
|
|
55
88
|
isHeld = true;
|
|
56
|
-
|
|
89
|
+
if (value > actionAnalog)
|
|
90
|
+
actionAnalog = value;
|
|
57
91
|
continue;
|
|
58
92
|
}
|
|
59
93
|
const axis = readAxis(code);
|
|
60
|
-
if (axis
|
|
61
|
-
|
|
94
|
+
if (axis === null)
|
|
95
|
+
continue;
|
|
96
|
+
const raw = clampUnit(snapshot.axes[axis.index] ?? 0);
|
|
97
|
+
const shaped = radial ? raw * radialFactor : Math.sign(raw) * rescale(Math.abs(raw), inner, outer, curve);
|
|
98
|
+
const value = shaped * axis.sign;
|
|
99
|
+
if (value > actionAnalog)
|
|
100
|
+
actionAnalog = value;
|
|
62
101
|
}
|
|
63
102
|
if (isHeld || actionAnalog > 0)
|
|
64
|
-
held.push(action);
|
|
103
|
+
out.held.push(action);
|
|
65
104
|
if (actionAnalog !== 0)
|
|
66
|
-
analog[action] = actionAnalog;
|
|
105
|
+
out.analog[action] = actionAnalog;
|
|
67
106
|
}
|
|
68
|
-
return
|
|
107
|
+
return out;
|
|
69
108
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Motor intensities, each `0..1`: `strong` is the low-frequency motor, `weak` the high-frequency one. */
|
|
2
|
+
export interface HapticLevel {
|
|
3
|
+
strong: number;
|
|
4
|
+
weak: number;
|
|
5
|
+
}
|
|
6
|
+
/** A decaying one-shot on a channel: starts at `strong`/`weak` and fades to zero over `ms`. */
|
|
7
|
+
export interface HapticPulse extends HapticLevel {
|
|
8
|
+
ms: number;
|
|
9
|
+
}
|
|
10
|
+
/** Options for {@link createHapticChannels}. */
|
|
11
|
+
export interface HapticChannelsOptions {
|
|
12
|
+
/** Scale applied to channels below the highest active priority (default `0.4`), so an impact cuts through the engine hum. */
|
|
13
|
+
duck?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Serializable channel state. */
|
|
16
|
+
export interface HapticChannelsSnapshot {
|
|
17
|
+
duck: number;
|
|
18
|
+
channels: {
|
|
19
|
+
name: string;
|
|
20
|
+
strong: number;
|
|
21
|
+
weak: number;
|
|
22
|
+
priority: number;
|
|
23
|
+
remainingMs: number | null;
|
|
24
|
+
totalMs: number | null;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Named continuous rumble channels for one player, mixed by priority each frame. Game code sets
|
|
29
|
+
* levels from its own telemetry (engine rpm, road surface, impacts); the shell mixes and drives the pad.
|
|
30
|
+
*/
|
|
31
|
+
export interface HapticChannels {
|
|
32
|
+
/** Hold a channel at a level until changed; `0`/`0` silences it. Higher `priority` (default `0`) ducks lower channels. */
|
|
33
|
+
set(channel: string, level: HapticLevel, priority?: number): void;
|
|
34
|
+
/** Start a fading one-shot on a channel, replacing what it held. */
|
|
35
|
+
pulse(channel: string, pulse: HapticPulse, priority?: number): void;
|
|
36
|
+
/** Silence one channel, or all of them. */
|
|
37
|
+
clear(channel?: string): void;
|
|
38
|
+
/** Advance pulses by `dt` seconds and return the mixed level. */
|
|
39
|
+
mix(dt: number): HapticLevel;
|
|
40
|
+
/** The last mixed level. */
|
|
41
|
+
level(): HapticLevel;
|
|
42
|
+
retune(options: HapticChannelsOptions): void;
|
|
43
|
+
snapshot(): HapticChannelsSnapshot;
|
|
44
|
+
restore(snapshot: HapticChannelsSnapshot): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-player haptic channels mixed by priority.
|
|
48
|
+
* @capability haptics Continuous gamepad rumble from named channels (engine, road, impact) mixed by priority, plus fading pulses.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createHapticChannels(options?: HapticChannelsOptions): HapticChannels;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const clamp01 = (value) => (Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0);
|
|
2
|
+
/**
|
|
3
|
+
* Per-player haptic channels mixed by priority.
|
|
4
|
+
* @capability haptics Continuous gamepad rumble from named channels (engine, road, impact) mixed by priority, plus fading pulses.
|
|
5
|
+
*/
|
|
6
|
+
export function createHapticChannels(options = {}) {
|
|
7
|
+
let duck = clamp01(options.duck ?? 0.4);
|
|
8
|
+
const channels = new Map();
|
|
9
|
+
const last = { strong: 0, weak: 0 };
|
|
10
|
+
const write = (name, channel) => {
|
|
11
|
+
if (channel.strong === 0 && channel.weak === 0)
|
|
12
|
+
channels.delete(name);
|
|
13
|
+
else
|
|
14
|
+
channels.set(name, channel);
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
set(name, level, priority = 0) {
|
|
18
|
+
write(name, { strong: clamp01(level.strong), weak: clamp01(level.weak), priority, remainingMs: null, totalMs: null });
|
|
19
|
+
},
|
|
20
|
+
pulse(name, pulse, priority = 0) {
|
|
21
|
+
const ms = Math.max(0, pulse.ms);
|
|
22
|
+
if (ms === 0) {
|
|
23
|
+
channels.delete(name);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
write(name, { strong: clamp01(pulse.strong), weak: clamp01(pulse.weak), priority, remainingMs: ms, totalMs: ms });
|
|
27
|
+
},
|
|
28
|
+
clear(name) {
|
|
29
|
+
if (name === undefined)
|
|
30
|
+
channels.clear();
|
|
31
|
+
else
|
|
32
|
+
channels.delete(name);
|
|
33
|
+
},
|
|
34
|
+
mix(dt) {
|
|
35
|
+
const stepMs = Math.max(0, dt) * 1000;
|
|
36
|
+
let top = -Infinity;
|
|
37
|
+
for (const [name, channel] of channels) {
|
|
38
|
+
if (channel.remainingMs !== null) {
|
|
39
|
+
channel.remainingMs -= stepMs;
|
|
40
|
+
if (channel.remainingMs <= 0) {
|
|
41
|
+
channels.delete(name);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (channel.priority > top)
|
|
46
|
+
top = channel.priority;
|
|
47
|
+
}
|
|
48
|
+
let strong = 0;
|
|
49
|
+
let weak = 0;
|
|
50
|
+
for (const channel of channels.values()) {
|
|
51
|
+
const fade = channel.remainingMs === null || channel.totalMs === null ? 1 : channel.remainingMs / channel.totalMs;
|
|
52
|
+
const scale = (channel.priority >= top ? 1 : duck) * fade;
|
|
53
|
+
strong = Math.max(strong, channel.strong * scale);
|
|
54
|
+
weak = Math.max(weak, channel.weak * scale);
|
|
55
|
+
}
|
|
56
|
+
last.strong = strong;
|
|
57
|
+
last.weak = weak;
|
|
58
|
+
return last;
|
|
59
|
+
},
|
|
60
|
+
level: () => last,
|
|
61
|
+
retune(next) {
|
|
62
|
+
duck = clamp01(next.duck ?? duck);
|
|
63
|
+
},
|
|
64
|
+
snapshot() {
|
|
65
|
+
return { duck, channels: [...channels].map(([name, channel]) => ({ name, ...channel })) };
|
|
66
|
+
},
|
|
67
|
+
restore(snapshot) {
|
|
68
|
+
duck = clamp01(snapshot.duck);
|
|
69
|
+
channels.clear();
|
|
70
|
+
for (const { name, ...channel } of snapshot.channels)
|
|
71
|
+
channels.set(name, { ...channel });
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** One action's press history inside an {@link InputBuffer}. */
|
|
2
|
+
export interface BufferedAction {
|
|
3
|
+
pressedAt: number;
|
|
4
|
+
previousPressAt: number | null;
|
|
5
|
+
releasedAt: number | null;
|
|
6
|
+
consumed: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Serializable state for an {@link InputBuffer}. */
|
|
9
|
+
export interface InputBufferSnapshot {
|
|
10
|
+
windowMs: number;
|
|
11
|
+
actions: Record<string, BufferedAction>;
|
|
12
|
+
}
|
|
13
|
+
/** Remembers recent action presses so a press slightly early (a jump before landing) still counts. Times are caller-supplied ms, so it runs the same on a client, a host or in a replay. */
|
|
14
|
+
export interface InputBuffer {
|
|
15
|
+
/** Record a press edge. */
|
|
16
|
+
press(action: string, nowMs: number): void;
|
|
17
|
+
/** Record a release edge; {@link InputBuffer.hold} stops growing. */
|
|
18
|
+
release(action: string, nowMs: number): void;
|
|
19
|
+
/** `true` once for a press no older than `windowMs` (default: the buffer's window), then the press is spent. */
|
|
20
|
+
consume(action: string, nowMs: number, windowMs?: number): boolean;
|
|
21
|
+
/** `true` while `nowMs` is within `windowMs` of `groundedAtMs` — late-jump grace after leaving a ledge. `null` never qualifies. */
|
|
22
|
+
coyote(groundedAtMs: number | null, nowMs: number, windowMs: number): boolean;
|
|
23
|
+
/** How long the action has been held, ms; `0` when released or never pressed. */
|
|
24
|
+
hold(action: string, nowMs: number): number;
|
|
25
|
+
/** `true` when the last two presses were at most `gapMs` apart and the latest is no older than `gapMs`. */
|
|
26
|
+
doubleTap(action: string, nowMs: number, gapMs: number): boolean;
|
|
27
|
+
/** Change the default buffer window. */
|
|
28
|
+
retune(next: {
|
|
29
|
+
windowMs: number;
|
|
30
|
+
}): void;
|
|
31
|
+
snapshot(): InputBufferSnapshot;
|
|
32
|
+
restore(snapshot: InputBufferSnapshot): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates an input buffer for jump buffering, coyote time, hold duration and double taps.
|
|
36
|
+
* @capability input-buffer buffer early presses, coyote time, hold duration and double tap for responsive controls
|
|
37
|
+
*/
|
|
38
|
+
export declare function createInputBuffer(options: {
|
|
39
|
+
windowMs: number;
|
|
40
|
+
}): InputBuffer;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an input buffer for jump buffering, coyote time, hold duration and double taps.
|
|
3
|
+
* @capability input-buffer buffer early presses, coyote time, hold duration and double tap for responsive controls
|
|
4
|
+
*/
|
|
5
|
+
export function createInputBuffer(options) {
|
|
6
|
+
let windowMs = options.windowMs;
|
|
7
|
+
let actions = new Map();
|
|
8
|
+
return {
|
|
9
|
+
press(action, nowMs) {
|
|
10
|
+
const previous = actions.get(action);
|
|
11
|
+
actions.set(action, {
|
|
12
|
+
pressedAt: nowMs,
|
|
13
|
+
previousPressAt: previous === undefined ? null : previous.pressedAt,
|
|
14
|
+
releasedAt: null,
|
|
15
|
+
consumed: false,
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
release(action, nowMs) {
|
|
19
|
+
const entry = actions.get(action);
|
|
20
|
+
if (entry !== undefined && entry.releasedAt === null)
|
|
21
|
+
entry.releasedAt = nowMs;
|
|
22
|
+
},
|
|
23
|
+
consume(action, nowMs, window = windowMs) {
|
|
24
|
+
const entry = actions.get(action);
|
|
25
|
+
if (entry === undefined || entry.consumed || nowMs - entry.pressedAt > window)
|
|
26
|
+
return false;
|
|
27
|
+
entry.consumed = true;
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
coyote(groundedAtMs, nowMs, window) {
|
|
31
|
+
return groundedAtMs !== null && nowMs - groundedAtMs <= window;
|
|
32
|
+
},
|
|
33
|
+
hold(action, nowMs) {
|
|
34
|
+
const entry = actions.get(action);
|
|
35
|
+
if (entry === undefined || entry.releasedAt !== null)
|
|
36
|
+
return 0;
|
|
37
|
+
return Math.max(0, nowMs - entry.pressedAt);
|
|
38
|
+
},
|
|
39
|
+
doubleTap(action, nowMs, gapMs) {
|
|
40
|
+
const entry = actions.get(action);
|
|
41
|
+
if (entry === undefined || entry.previousPressAt === null)
|
|
42
|
+
return false;
|
|
43
|
+
return entry.pressedAt - entry.previousPressAt <= gapMs && nowMs - entry.pressedAt <= gapMs;
|
|
44
|
+
},
|
|
45
|
+
retune(next) {
|
|
46
|
+
windowMs = next.windowMs;
|
|
47
|
+
},
|
|
48
|
+
snapshot() {
|
|
49
|
+
const copy = {};
|
|
50
|
+
for (const [action, entry] of actions)
|
|
51
|
+
copy[action] = { ...entry };
|
|
52
|
+
return { windowMs, actions: copy };
|
|
53
|
+
},
|
|
54
|
+
restore(snapshot) {
|
|
55
|
+
windowMs = snapshot.windowMs;
|
|
56
|
+
actions = new Map(Object.entries(snapshot.actions).map(([action, entry]) => [action, { ...entry }]));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -9,6 +9,14 @@ export interface LookDeltas {
|
|
|
9
9
|
yaw: number;
|
|
10
10
|
pitch: number;
|
|
11
11
|
}
|
|
12
|
+
/** Plain JSON state of a {@link LookChannel}: the pending pixel deltas and the committed pose. */
|
|
13
|
+
export interface LookChannelState {
|
|
14
|
+
deltaX: number;
|
|
15
|
+
deltaY: number;
|
|
16
|
+
yaw: number;
|
|
17
|
+
pitch: number;
|
|
18
|
+
verticalOffset: number;
|
|
19
|
+
}
|
|
12
20
|
export interface LookChannel {
|
|
13
21
|
accumulate(dx: number, dy: number): void;
|
|
14
22
|
/** Yaw/pitch deltas (radians) for this frame; clears the accumulator. */
|
|
@@ -19,6 +27,8 @@ export interface LookChannel {
|
|
|
19
27
|
readPitch(): number;
|
|
20
28
|
setVerticalOffset(offset: number): void;
|
|
21
29
|
readVerticalOffset(): number;
|
|
30
|
+
snapshot(): LookChannelState;
|
|
31
|
+
restore(next: LookChannelState): void;
|
|
22
32
|
}
|
|
23
33
|
export interface LookChannelOptions {
|
|
24
34
|
/** Radians of rotation per pixel of pointer travel. */
|
|
@@ -40,5 +40,11 @@ export function createLookChannel({ sensitivity, maxVerticalOffset = Infinity })
|
|
|
40
40
|
readVerticalOffset() {
|
|
41
41
|
return verticalOffset;
|
|
42
42
|
},
|
|
43
|
+
snapshot() {
|
|
44
|
+
return { deltaX, deltaY, yaw, pitch, verticalOffset };
|
|
45
|
+
},
|
|
46
|
+
restore(next) {
|
|
47
|
+
({ deltaX, deltaY, yaw, pitch, verticalOffset } = next);
|
|
48
|
+
},
|
|
43
49
|
};
|
|
44
50
|
}
|
package/dist/input/pointer.d.ts
CHANGED
|
@@ -65,5 +65,7 @@ export interface DragCapture {
|
|
|
65
65
|
cancel(): void;
|
|
66
66
|
/** The current drag state, or `null` when idle. */
|
|
67
67
|
state(): DragState | null;
|
|
68
|
+
/** Resumes the drag a {@link DragCapture.state} described (its `origin` and `current`), or goes idle on `null`. */
|
|
69
|
+
restore(next: DragState | null): void;
|
|
68
70
|
}
|
|
69
71
|
export declare function createDragCapture(config?: DragCaptureConfig): DragCapture;
|
package/dist/input/pointer.js
CHANGED
|
@@ -61,5 +61,9 @@ export function createDragCapture(config = {}) {
|
|
|
61
61
|
current = null;
|
|
62
62
|
},
|
|
63
63
|
state: snapshot,
|
|
64
|
+
restore(next) {
|
|
65
|
+
origin = next === null ? null : [next.origin[0], next.origin[1], next.origin[2]];
|
|
66
|
+
current = next === null ? null : [next.current[0], next.current[1], next.current[2]];
|
|
67
|
+
},
|
|
64
68
|
};
|
|
65
69
|
}
|
package/dist/item/affix.d.ts
CHANGED
|
@@ -51,4 +51,10 @@ export interface AffixRoller {
|
|
|
51
51
|
roll(base: ItemBaseDef, rarityId: string, rng: () => number): RolledItem;
|
|
52
52
|
rollRandom(base: ItemBaseDef, rng: () => number): RolledItem;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Rolls randomized gear from a base item: a weighted rarity tier scales base stats and picks
|
|
56
|
+
* prefix/suffix affixes that name and modify the item. Deterministic for a given `rng`.
|
|
57
|
+
*
|
|
58
|
+
* @capability affix-roller roll randomized gear (guns, armor, loot) with rarity tiers, stat scaling and prefix/suffix affixes from a seed
|
|
59
|
+
*/
|
|
54
60
|
export declare function createAffixRoller(config: RollerConfig): AffixRoller;
|
package/dist/item/affix.js
CHANGED
|
@@ -20,6 +20,12 @@ function assertConfig(config) {
|
|
|
20
20
|
throw new Error(`rarity "${tier.id}" weight must be positive`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Rolls randomized gear from a base item: a weighted rarity tier scales base stats and picks
|
|
25
|
+
* prefix/suffix affixes that name and modify the item. Deterministic for a given `rng`.
|
|
26
|
+
*
|
|
27
|
+
* @capability affix-roller roll randomized gear (guns, armor, loot) with rarity tiers, stat scaling and prefix/suffix affixes from a seed
|
|
28
|
+
*/
|
|
23
29
|
export function createAffixRoller(config) {
|
|
24
30
|
assertConfig(config);
|
|
25
31
|
const poolById = new Map(config.pools.map((p) => [p.id, p]));
|
package/dist/meta/changelog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.19.0";
|
|
3
3
|
/** One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md). */
|
|
4
4
|
export interface ChangelogEntry {
|
|
5
5
|
migrate: readonly string[];
|