@jgengine/core 0.18.0 → 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 +265 -2
- package/dist/ai/decisionGraph.d.ts +96 -0
- package/dist/ai/decisionGraph.js +212 -0
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +155 -0
- package/dist/anim/animGraph.js +414 -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/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +35 -0
- package/dist/anim/locomotionGraph.js +62 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +12 -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/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +92 -1
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +24 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +20 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +59 -1
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +14 -1
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +167 -6
- package/dist/game/systemRuntime.js +6 -1
- 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 +17 -2
- package/dist/input/actionBindings.js +24 -11
- package/dist/input/actionContexts.d.ts +45 -0
- package/dist/input/actionContexts.js +113 -0
- 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 +79 -0
- package/dist/input/gamepadModel.js +108 -0
- 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 +196 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- 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 +30 -3
- package/dist/movement/playerMovement.js +304 -7
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +39 -8
- package/dist/movement/solidObstacles.js +71 -6
- package/dist/movement/voxelController.js +7 -7
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +74 -0
- package/dist/nav/navMesh.js +570 -0
- 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 +29 -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 +37 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +203 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +151 -0
- package/dist/physics/physicsWorldBackend.d.ts +45 -0
- package/dist/physics/physicsWorldBackend.js +518 -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 +13 -0
- package/dist/physics/worldColliders.js +128 -0
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +19 -6
- package/dist/render/lookPreset.js +56 -4
- package/dist/render/postProcessing.d.ts +40 -1
- package/dist/render/postProcessing.js +1 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +52 -0
- package/dist/runtime/commandInput.js +71 -0
- package/dist/runtime/commandRunner.d.ts +39 -7
- package/dist/runtime/commandRunner.js +35 -5
- 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 +18 -1
- package/dist/runtime/gameContextTypes.d.ts +33 -7
- package/dist/runtime/gameRuntime.d.ts +23 -5
- package/dist/runtime/gameRuntime.js +21 -4
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +14 -0
- package/dist/runtime/inputSnapshot.js +11 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +41 -2
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +29 -1
- package/dist/scene/behaviorRuntime.js +71 -4
- package/dist/scene/behaviors.d.ts +12 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- 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 +70 -0
- package/dist/sensor/perception.js +189 -0
- 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 +21 -0
- package/dist/settings/graphicsProfile.js +48 -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/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -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 +46 -2
- package/dist/world/buildings.js +53 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +139 -0
- package/dist/world/cityKind.js +367 -16
- package/dist/world/features.d.ts +56 -3
- package/dist/world/features.js +13 -2
- 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/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -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 +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -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/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +501 -453
- package/skills/jgengine-gameplay/capabilities.md +36 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- 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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** A three-dimensional point or direction represented without a renderer dependency. */
|
|
2
|
+
export type Vec3 = readonly [number, number, number];
|
|
3
|
+
/** A mutable three-dimensional tuple used by allocation-aware solver outputs. */
|
|
4
|
+
export type MutableVec3 = [number, number, number];
|
|
5
|
+
/** A quaternion in `(x, y, z, w)` order. */
|
|
6
|
+
export type Quat = readonly [number, number, number, number];
|
|
7
|
+
/** Inputs for {@link solveTwoBone}. */
|
|
8
|
+
export interface TwoBoneInput {
|
|
9
|
+
root: Vec3;
|
|
10
|
+
mid: Vec3;
|
|
11
|
+
tip: Vec3;
|
|
12
|
+
target: Vec3;
|
|
13
|
+
pole: Vec3;
|
|
14
|
+
}
|
|
15
|
+
/** Reused output buffers filled by {@link solveTwoBone}. */
|
|
16
|
+
export interface TwoBoneOutput {
|
|
17
|
+
mid: MutableVec3;
|
|
18
|
+
tip: MutableVec3;
|
|
19
|
+
}
|
|
20
|
+
/** Iteration budget and convergence threshold for {@link solveFabrik}. */
|
|
21
|
+
export interface FabrikOptions {
|
|
22
|
+
iterations?: number;
|
|
23
|
+
tolerance?: number;
|
|
24
|
+
}
|
|
25
|
+
/** Inputs for {@link lookAt}. Angles are in radians. */
|
|
26
|
+
export interface LookAtInput {
|
|
27
|
+
from: Vec3;
|
|
28
|
+
target: Vec3;
|
|
29
|
+
up: Vec3;
|
|
30
|
+
maxYaw?: number;
|
|
31
|
+
maxPitch?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Solves a two-segment chain analytically, placing the middle and tip joints toward `target` and `pole`.
|
|
35
|
+
* The root remains fixed, and unreachable targets are clamped to the chain's maximum reach.
|
|
36
|
+
*
|
|
37
|
+
* @capability animation inverse kinematics solve a two-bone chain from a target and bend pole
|
|
38
|
+
*/
|
|
39
|
+
export declare function solveTwoBone(input: TwoBoneInput, out: TwoBoneOutput): TwoBoneOutput;
|
|
40
|
+
/**
|
|
41
|
+
* Solves a chain with the FABRIK forward-and-backward reaching algorithm.
|
|
42
|
+
* Returned tuples are new values; the input chain is not modified.
|
|
43
|
+
*
|
|
44
|
+
* @capability animation inverse kinematics solve an arbitrary tuple chain toward a target
|
|
45
|
+
*/
|
|
46
|
+
export declare function solveFabrik(chain: readonly Vec3[], target: Vec3, options?: FabrikOptions): MutableVec3[];
|
|
47
|
+
/** Returns a quaternion that points a +Z bone from `from` toward `target`, with optional yaw and pitch limits.
|
|
48
|
+
*
|
|
49
|
+
* @capability animation inverse kinematics orient a bone toward a target with constrained yaw and pitch
|
|
50
|
+
*/
|
|
51
|
+
export declare function lookAt(input: LookAtInput): Quat;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const EPSILON = 1e-8;
|
|
2
|
+
function distance(a, b) {
|
|
3
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
4
|
+
}
|
|
5
|
+
function normalize(x, y, z) {
|
|
6
|
+
const length = Math.hypot(x, y, z);
|
|
7
|
+
return length > EPSILON ? [x / length, y / length, z / length] : [0, 0, 0];
|
|
8
|
+
}
|
|
9
|
+
function cross(a, b) {
|
|
10
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
11
|
+
}
|
|
12
|
+
function addScaled(origin, direction, amount) {
|
|
13
|
+
return [origin[0] + direction[0] * amount, origin[1] + direction[1] * amount, origin[2] + direction[2] * amount];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Solves a two-segment chain analytically, placing the middle and tip joints toward `target` and `pole`.
|
|
17
|
+
* The root remains fixed, and unreachable targets are clamped to the chain's maximum reach.
|
|
18
|
+
*
|
|
19
|
+
* @capability animation inverse kinematics solve a two-bone chain from a target and bend pole
|
|
20
|
+
*/
|
|
21
|
+
export function solveTwoBone(input, out) {
|
|
22
|
+
const firstLength = distance(input.root, input.mid);
|
|
23
|
+
const secondLength = distance(input.mid, input.tip);
|
|
24
|
+
const toTarget = [input.target[0] - input.root[0], input.target[1] - input.root[1], input.target[2] - input.root[2]];
|
|
25
|
+
const targetDistance = Math.hypot(...toTarget);
|
|
26
|
+
const direction = targetDistance > EPSILON ? normalize(...toTarget) : normalize(input.mid[0] - input.root[0], input.mid[1] - input.root[1], input.mid[2] - input.root[2]);
|
|
27
|
+
const minReach = Math.abs(firstLength - secondLength);
|
|
28
|
+
const reach = Math.min(Math.max(targetDistance, minReach), firstLength + secondLength);
|
|
29
|
+
const clampedTarget = addScaled(input.root, direction, reach);
|
|
30
|
+
let bendNormal = normalize(...cross(direction, [input.pole[0] - input.root[0], input.pole[1] - input.root[1], input.pole[2] - input.root[2]]));
|
|
31
|
+
if (Math.hypot(...bendNormal) <= EPSILON) {
|
|
32
|
+
const fallback = Math.abs(direction[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
|
|
33
|
+
bendNormal = normalize(...cross(direction, fallback));
|
|
34
|
+
}
|
|
35
|
+
const bendDirection = normalize(...cross(bendNormal, direction));
|
|
36
|
+
const along = (firstLength * firstLength - secondLength * secondLength + reach * reach) / (2 * reach || 1);
|
|
37
|
+
const height = Math.sqrt(Math.max(0, firstLength * firstLength - along * along));
|
|
38
|
+
const solvedMid = addScaled(addScaled(input.root, direction, along), bendDirection, height);
|
|
39
|
+
out.mid[0] = solvedMid[0];
|
|
40
|
+
out.mid[1] = solvedMid[1];
|
|
41
|
+
out.mid[2] = solvedMid[2];
|
|
42
|
+
out.tip[0] = clampedTarget[0];
|
|
43
|
+
out.tip[1] = clampedTarget[1];
|
|
44
|
+
out.tip[2] = clampedTarget[2];
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Solves a chain with the FABRIK forward-and-backward reaching algorithm.
|
|
49
|
+
* Returned tuples are new values; the input chain is not modified.
|
|
50
|
+
*
|
|
51
|
+
* @capability animation inverse kinematics solve an arbitrary tuple chain toward a target
|
|
52
|
+
*/
|
|
53
|
+
export function solveFabrik(chain, target, options = {}) {
|
|
54
|
+
if (chain.length === 0)
|
|
55
|
+
return [];
|
|
56
|
+
if (chain.length === 1)
|
|
57
|
+
return [[chain[0][0], chain[0][1], chain[0][2]]];
|
|
58
|
+
const points = chain.map((point) => [point[0], point[1], point[2]]);
|
|
59
|
+
const lengths = chain.slice(1).map((point, index) => distance(chain[index], point));
|
|
60
|
+
const root = points[0];
|
|
61
|
+
const totalLength = lengths.reduce((sum, length) => sum + length, 0);
|
|
62
|
+
const rootDistance = distance(root, target);
|
|
63
|
+
const iterations = Math.max(0, Math.floor(options.iterations ?? 10));
|
|
64
|
+
const tolerance = Math.max(0, options.tolerance ?? 1e-3);
|
|
65
|
+
if (rootDistance >= totalLength) {
|
|
66
|
+
const direction = normalize(target[0] - root[0], target[1] - root[1], target[2] - root[2]);
|
|
67
|
+
for (let i = 1; i < points.length; i += 1)
|
|
68
|
+
points[i] = addScaled(points[i - 1], direction, lengths[i - 1]);
|
|
69
|
+
return points;
|
|
70
|
+
}
|
|
71
|
+
for (let iteration = 0; iteration < iterations; iteration += 1) {
|
|
72
|
+
points[points.length - 1] = [target[0], target[1], target[2]];
|
|
73
|
+
for (let i = points.length - 2; i >= 0; i -= 1) {
|
|
74
|
+
const direction = normalize(points[i][0] - points[i + 1][0], points[i][1] - points[i + 1][1], points[i][2] - points[i + 1][2]);
|
|
75
|
+
points[i] = addScaled(points[i + 1], direction, lengths[i]);
|
|
76
|
+
}
|
|
77
|
+
points[0] = [root[0], root[1], root[2]];
|
|
78
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
79
|
+
const direction = normalize(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1], points[i][2] - points[i - 1][2]);
|
|
80
|
+
points[i] = addScaled(points[i - 1], direction, lengths[i - 1]);
|
|
81
|
+
}
|
|
82
|
+
if (distance(points[points.length - 1], target) <= tolerance)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
return points;
|
|
86
|
+
}
|
|
87
|
+
function quaternionFromBasis(right, up, forward) {
|
|
88
|
+
const trace = right[0] + up[1] + forward[2];
|
|
89
|
+
if (trace > 0) {
|
|
90
|
+
const s = Math.sqrt(trace + 1) * 2;
|
|
91
|
+
return [(up[2] - forward[1]) / s, (forward[0] - right[2]) / s, (right[1] - up[0]) / s, 0.25 * s];
|
|
92
|
+
}
|
|
93
|
+
if (right[0] > up[1] && right[0] > forward[2]) {
|
|
94
|
+
const s = Math.sqrt(1 + right[0] - up[1] - forward[2]) * 2;
|
|
95
|
+
return [0.25 * s, (right[1] + up[0]) / s, (right[2] + forward[0]) / s, (up[2] - forward[1]) / s];
|
|
96
|
+
}
|
|
97
|
+
if (up[1] > forward[2]) {
|
|
98
|
+
const s = Math.sqrt(1 + up[1] - right[0] - forward[2]) * 2;
|
|
99
|
+
return [(right[1] + up[0]) / s, 0.25 * s, (up[2] + forward[1]) / s, (forward[0] - right[2]) / s];
|
|
100
|
+
}
|
|
101
|
+
const s = Math.sqrt(1 + forward[2] - right[0] - up[1]) * 2;
|
|
102
|
+
return [(right[2] + forward[0]) / s, (up[2] + forward[1]) / s, 0.25 * s, (right[1] - up[0]) / s];
|
|
103
|
+
}
|
|
104
|
+
/** Returns a quaternion that points a +Z bone from `from` toward `target`, with optional yaw and pitch limits.
|
|
105
|
+
*
|
|
106
|
+
* @capability animation inverse kinematics orient a bone toward a target with constrained yaw and pitch
|
|
107
|
+
*/
|
|
108
|
+
export function lookAt(input) {
|
|
109
|
+
let direction = normalize(input.target[0] - input.from[0], input.target[1] - input.from[1], input.target[2] - input.from[2]);
|
|
110
|
+
if (Math.hypot(...direction) <= EPSILON)
|
|
111
|
+
return [0, 0, 0, 1];
|
|
112
|
+
let yaw = Math.atan2(direction[0], direction[2]);
|
|
113
|
+
let pitch = Math.atan2(direction[1], Math.hypot(direction[0], direction[2]));
|
|
114
|
+
if (input.maxYaw !== undefined)
|
|
115
|
+
yaw = Math.max(-Math.abs(input.maxYaw), Math.min(Math.abs(input.maxYaw), yaw));
|
|
116
|
+
if (input.maxPitch !== undefined)
|
|
117
|
+
pitch = Math.max(-Math.abs(input.maxPitch), Math.min(Math.abs(input.maxPitch), pitch));
|
|
118
|
+
direction = [Math.sin(yaw) * Math.cos(pitch), Math.sin(pitch), Math.cos(yaw) * Math.cos(pitch)];
|
|
119
|
+
let right = normalize(...cross(input.up, direction));
|
|
120
|
+
if (Math.hypot(...right) <= EPSILON)
|
|
121
|
+
right = normalize(...cross([0, 1, 0], direction));
|
|
122
|
+
const up = cross(direction, right);
|
|
123
|
+
return quaternionFromBasis(right, up, direction);
|
|
124
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ModelAnimationConfig } from "../game/playableGame.js";
|
|
2
|
+
import type { AnimGraph } from "./animGraph.js";
|
|
3
|
+
/** Inputs for {@link locomotionGraph}: the idle/walk/run clip names and the one-shot table a rig config already carries. */
|
|
4
|
+
export interface LocomotionGraphInput {
|
|
5
|
+
idle: string;
|
|
6
|
+
walk: string;
|
|
7
|
+
run?: string;
|
|
8
|
+
/** Speed (world units/sec) above which the character walks. Default 0.5. */
|
|
9
|
+
walkSpeed?: number;
|
|
10
|
+
/** Speed above which it runs. Default 6. */
|
|
11
|
+
runSpeed?: number;
|
|
12
|
+
/** Crossfade seconds between locomotion states. Default 0.2. */
|
|
13
|
+
fadeSec?: number;
|
|
14
|
+
/** One-shot clips keyed by trigger name; `death` holds its last frame instead of returning. */
|
|
15
|
+
oneShots?: Readonly<Record<string, string>>;
|
|
16
|
+
}
|
|
17
|
+
/** The parameter name the shell feeds with the entity's smoothed ground speed. */
|
|
18
|
+
export declare const LOCOMOTION_SPEED_PARAM = "speed";
|
|
19
|
+
/** Layer id the locomotion graph uses; query `runtime.stateOf(LOCOMOTION_LAYER)`. */
|
|
20
|
+
export declare const LOCOMOTION_LAYER = "base";
|
|
21
|
+
/**
|
|
22
|
+
* The engine's default locomotion as an authored graph: a speed-driven blend between idle, walk, and run, plus a
|
|
23
|
+
* state per one-shot that plays once and returns (or clamps for `death`). What `useModelAnimation` used to hardcode.
|
|
24
|
+
*
|
|
25
|
+
* @capability locomotion-graph build the default idle/walk/run plus one-shot graph from clip names
|
|
26
|
+
*/
|
|
27
|
+
export declare function locomotionGraph(input: LocomotionGraphInput): AnimGraph;
|
|
28
|
+
/**
|
|
29
|
+
* The graph a model animation config plays: its `graph`, or the {@link locomotionGraph} its
|
|
30
|
+
* `states` and `oneShots` describe (a `string[]` one-shot uses its first variant). `undefined` for a
|
|
31
|
+
* single-clip config. The shell plays this and the editor inspects it, so both see the same graph.
|
|
32
|
+
*
|
|
33
|
+
* @capability locomotion-graph resolve the animation graph a model config plays
|
|
34
|
+
*/
|
|
35
|
+
export declare function animGraphFromConfig(config: ModelAnimationConfig): AnimGraph | undefined;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** The parameter name the shell feeds with the entity's smoothed ground speed. */
|
|
2
|
+
export const LOCOMOTION_SPEED_PARAM = "speed";
|
|
3
|
+
/** Layer id the locomotion graph uses; query `runtime.stateOf(LOCOMOTION_LAYER)`. */
|
|
4
|
+
export const LOCOMOTION_LAYER = "base";
|
|
5
|
+
/**
|
|
6
|
+
* The engine's default locomotion as an authored graph: a speed-driven blend between idle, walk, and run, plus a
|
|
7
|
+
* state per one-shot that plays once and returns (or clamps for `death`). What `useModelAnimation` used to hardcode.
|
|
8
|
+
*
|
|
9
|
+
* @capability locomotion-graph build the default idle/walk/run plus one-shot graph from clip names
|
|
10
|
+
*/
|
|
11
|
+
export function locomotionGraph(input) {
|
|
12
|
+
const walkSpeed = input.walkSpeed ?? 0.5;
|
|
13
|
+
const runSpeed = input.runSpeed ?? 6;
|
|
14
|
+
const fade = input.fadeSec ?? 0.2;
|
|
15
|
+
const points = [
|
|
16
|
+
{ at: 0, clip: input.idle },
|
|
17
|
+
{ at: walkSpeed, clip: input.walk },
|
|
18
|
+
...(input.run === undefined ? [] : [{ at: runSpeed, clip: input.run }]),
|
|
19
|
+
];
|
|
20
|
+
const states = {
|
|
21
|
+
locomotion: { kind: "blend1D", param: LOCOMOTION_SPEED_PARAM, points },
|
|
22
|
+
};
|
|
23
|
+
const transitions = [];
|
|
24
|
+
for (const [name, clip] of Object.entries(input.oneShots ?? {})) {
|
|
25
|
+
states[name] = { kind: "clip", clip, loop: false };
|
|
26
|
+
transitions.push({ from: "*", to: name, trigger: name, duration: 0.1 });
|
|
27
|
+
if (name !== "death")
|
|
28
|
+
transitions.push({ from: name, to: "locomotion", exitTime: 1, duration: fade });
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
layers: [{ id: LOCOMOTION_LAYER, entry: "locomotion", states, transitions }],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The graph a model animation config plays: its `graph`, or the {@link locomotionGraph} its
|
|
36
|
+
* `states` and `oneShots` describe (a `string[]` one-shot uses its first variant). `undefined` for a
|
|
37
|
+
* single-clip config. The shell plays this and the editor inspects it, so both see the same graph.
|
|
38
|
+
*
|
|
39
|
+
* @capability locomotion-graph resolve the animation graph a model config plays
|
|
40
|
+
*/
|
|
41
|
+
export function animGraphFromConfig(config) {
|
|
42
|
+
if (config.graph !== undefined)
|
|
43
|
+
return config.graph;
|
|
44
|
+
const states = config.states;
|
|
45
|
+
if (states === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
const oneShots = {};
|
|
48
|
+
for (const [event, spec] of Object.entries(config.oneShots ?? {})) {
|
|
49
|
+
const clip = typeof spec === "string" ? spec : spec[0];
|
|
50
|
+
if (clip !== undefined)
|
|
51
|
+
oneShots[event] = clip;
|
|
52
|
+
}
|
|
53
|
+
return locomotionGraph({
|
|
54
|
+
idle: states.idle,
|
|
55
|
+
walk: states.walk,
|
|
56
|
+
...(states.run === undefined ? {} : { run: states.run }),
|
|
57
|
+
walkSpeed: states.walkSpeed,
|
|
58
|
+
runSpeed: states.runSpeed,
|
|
59
|
+
fadeSec: states.fadeSec,
|
|
60
|
+
...(Object.keys(oneShots).length === 0 ? {} : { oneShots }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Portable image atlas data shared by renderers and headless game logic. */
|
|
2
|
+
export interface SpriteAtlas {
|
|
3
|
+
image: string;
|
|
4
|
+
size: [number, number];
|
|
5
|
+
frames: Record<string, {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
w: number;
|
|
9
|
+
h: number;
|
|
10
|
+
pivot?: [number, number];
|
|
11
|
+
}>;
|
|
12
|
+
animations: Record<string, {
|
|
13
|
+
frames: string[];
|
|
14
|
+
fps: number;
|
|
15
|
+
loop?: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/** Convert Aseprite JSON exports (hash or array frame layouts) to the portable atlas format. */
|
|
19
|
+
export declare function fromAseprite(json: unknown): SpriteAtlas;
|
|
20
|
+
/** Convert TexturePacker JSON exports to the portable atlas format. */
|
|
21
|
+
export declare function fromTexturePacker(json: unknown): SpriteAtlas;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
function object(value, label) {
|
|
2
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3
|
+
throw new Error(`${label} must be an object.`);
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
function number(value, label) {
|
|
7
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
8
|
+
throw new Error(`${label} must be a finite number.`);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function frame(value, label) {
|
|
12
|
+
const data = object(value, label);
|
|
13
|
+
const x = number(data.x, `${label}.x`);
|
|
14
|
+
const y = number(data.y, `${label}.y`);
|
|
15
|
+
const w = number(data.w ?? data.width, `${label}.w`);
|
|
16
|
+
const h = number(data.h ?? data.height, `${label}.h`);
|
|
17
|
+
if (w <= 0 || h <= 0 || x < 0 || y < 0)
|
|
18
|
+
throw new Error(`${label} has invalid bounds.`);
|
|
19
|
+
const pivotValue = data.pivot;
|
|
20
|
+
const pivot = pivotValue === undefined ? undefined : [number(pivotValue[0], `${label}.pivot[0]`), number(pivotValue[1], `${label}.pivot[1]`)];
|
|
21
|
+
return pivot ? { x, y, w, h, pivot } : { x, y, w, h };
|
|
22
|
+
}
|
|
23
|
+
function imageAndSize(root) {
|
|
24
|
+
const meta = object(root.meta, "meta");
|
|
25
|
+
const image = typeof meta.image === "string" ? meta.image : typeof root.image === "string" ? root.image : undefined;
|
|
26
|
+
if (!image)
|
|
27
|
+
throw new Error("Sprite atlas image is required.");
|
|
28
|
+
const rawSize = object(meta.size ?? root.size, "size");
|
|
29
|
+
const size = [number(rawSize.w ?? rawSize.width, "size[0]"), number(rawSize.h ?? rawSize.height, "size[1]")];
|
|
30
|
+
if (size[0] <= 0 || size[1] <= 0)
|
|
31
|
+
throw new Error("Sprite atlas size must be positive.");
|
|
32
|
+
return { image, size };
|
|
33
|
+
}
|
|
34
|
+
function animationFrames(names, fps = 10, loop = true) {
|
|
35
|
+
if (!names.length || !Number.isFinite(fps) || fps <= 0)
|
|
36
|
+
throw new Error("Animation requires frames and a positive fps.");
|
|
37
|
+
return { frames: names, fps, ...(loop ? {} : { loop: false }) };
|
|
38
|
+
}
|
|
39
|
+
/** Convert Aseprite JSON exports (hash or array frame layouts) to the portable atlas format. */
|
|
40
|
+
export function fromAseprite(json) {
|
|
41
|
+
const root = object(json, "Aseprite export");
|
|
42
|
+
const framesRoot = root.frames;
|
|
43
|
+
const frames = {};
|
|
44
|
+
const durations = {};
|
|
45
|
+
const names = [];
|
|
46
|
+
if (Array.isArray(framesRoot)) {
|
|
47
|
+
for (const [index, entry] of framesRoot.entries()) {
|
|
48
|
+
const e = object(entry, `frames[${index}]`);
|
|
49
|
+
const name = typeof e.filename === "string" ? e.filename : String(index);
|
|
50
|
+
frames[name] = frame(e.frame, `frames[${index}].frame`);
|
|
51
|
+
if (e.duration !== undefined)
|
|
52
|
+
durations[name] = number(e.duration, `frames[${index}].duration`);
|
|
53
|
+
names.push(name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
for (const [name, entry] of Object.entries(object(framesRoot, "frames"))) {
|
|
58
|
+
const e = object(entry, `frames.${name}`);
|
|
59
|
+
frames[name] = frame(e.frame ?? e, `frames.${name}`);
|
|
60
|
+
if (e.duration !== undefined)
|
|
61
|
+
durations[name] = number(e.duration, `frames.${name}.duration`);
|
|
62
|
+
names.push(name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!names.length)
|
|
66
|
+
throw new Error("Aseprite export must contain frames.");
|
|
67
|
+
const size = imageAndSize(root);
|
|
68
|
+
const animations = {};
|
|
69
|
+
const tags = Array.isArray(root.meta?.frameTags) ? root.meta.frameTags : [];
|
|
70
|
+
for (const [index, tagValue] of tags.entries()) {
|
|
71
|
+
const tag = object(tagValue, `meta.frameTags[${index}]`);
|
|
72
|
+
const name = String(tag.name ?? "");
|
|
73
|
+
const from = Number(tag.from);
|
|
74
|
+
const to = Number(tag.to);
|
|
75
|
+
if (!name || !Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to >= names.length || from > to)
|
|
76
|
+
throw new Error(`Invalid animation tag at index ${index}.`);
|
|
77
|
+
const direction = tag.direction === "reverse" ? -1 : 1;
|
|
78
|
+
const ordered = names.slice(from, to + 1);
|
|
79
|
+
if (direction < 0)
|
|
80
|
+
ordered.reverse();
|
|
81
|
+
const totalDuration = ordered.reduce((sum, frameName) => sum + (durations[frameName] ?? 100), 0);
|
|
82
|
+
animations[name] = animationFrames(ordered, ordered.length * 1000 / totalDuration, tag.direction !== "pingpong");
|
|
83
|
+
}
|
|
84
|
+
if (!Object.keys(animations).length)
|
|
85
|
+
animations.default = animationFrames(names);
|
|
86
|
+
return { image: size.image, size: size.size, frames, animations };
|
|
87
|
+
}
|
|
88
|
+
/** Convert TexturePacker JSON exports to the portable atlas format. */
|
|
89
|
+
export function fromTexturePacker(json) {
|
|
90
|
+
const root = object(json, "TexturePacker export");
|
|
91
|
+
const size = imageAndSize(root);
|
|
92
|
+
const frames = {};
|
|
93
|
+
const source = Array.isArray(root.frames) ? root.frames.map((entry, index) => [String((object(entry, `frames[${index}]`).filename ?? index)), entry]) : Object.entries(object(root.frames, "frames"));
|
|
94
|
+
for (const [name, value] of source) {
|
|
95
|
+
const e = object(value, `frames.${name}`);
|
|
96
|
+
frames[name] = frame(e.frame ?? e, `frames.${name}.frame`);
|
|
97
|
+
}
|
|
98
|
+
const names = Object.keys(frames);
|
|
99
|
+
if (!names.length)
|
|
100
|
+
throw new Error("TexturePacker export must contain frames.");
|
|
101
|
+
const animations = { default: animationFrames(names) };
|
|
102
|
+
return { image: size.image, size: size.size, frames, animations };
|
|
103
|
+
}
|
|
@@ -27,6 +27,18 @@ export interface SoundDef {
|
|
|
27
27
|
/** Positional emitters attenuate by distance from the listener; non-positional sounds (UI, music) play at flat gain. Default true. */
|
|
28
28
|
positional?: boolean;
|
|
29
29
|
falloff?: AudioFalloffConfig;
|
|
30
|
+
/** Doppler strength for retained loops: 1 is physical, 0 or absent is off. Emitter velocity comes from `setLoop({ velocity })`, listener velocity from the camera. */
|
|
31
|
+
doppler?: number;
|
|
32
|
+
/** Web Audio spatial panning configuration. When present, the shell uses a PannerNode. */
|
|
33
|
+
spatial?: {
|
|
34
|
+
panning: "hrtf" | "equalpower";
|
|
35
|
+
refDistance?: number;
|
|
36
|
+
maxDistance?: number;
|
|
37
|
+
rolloff?: number;
|
|
38
|
+
coneInner?: number;
|
|
39
|
+
coneOuter?: number;
|
|
40
|
+
coneOuterGain?: number;
|
|
41
|
+
};
|
|
30
42
|
}
|
|
31
43
|
export declare function computeFalloffGain(distance: number, config?: AudioFalloffConfig): number;
|
|
32
44
|
export declare function resolveEmitterGain(distance: number, sound: Pick<SoundDef, "gain" | "positional" | "falloff">, busGain: number): number;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Speed of sound in air at 20 °C, metres per second — the default for {@link dopplerRate}. */
|
|
2
|
+
export declare const SPEED_OF_SOUND = 343;
|
|
3
|
+
type Vec3Tuple = readonly [number, number, number];
|
|
4
|
+
/** Options for {@link dopplerRate}. */
|
|
5
|
+
export interface DopplerOptions {
|
|
6
|
+
/** Scales both velocities; 1 is physical, 0 disables the shift. Default 1. */
|
|
7
|
+
factor?: number;
|
|
8
|
+
/** World units per second sound travels. Default {@link SPEED_OF_SOUND}. */
|
|
9
|
+
speedOfSound?: number;
|
|
10
|
+
/** Clamp window for the returned multiplier. Default `[0.5, 2]`. */
|
|
11
|
+
range?: readonly [number, number];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Doppler pitch multiplier for an emitter heard by a listener: `(c + vListener) / (c - vEmitter)`,
|
|
15
|
+
* where each speed is the component along the line between them, positive when closing. Multiply a
|
|
16
|
+
* loop's playback rate by it. Pure and allocation-free.
|
|
17
|
+
*
|
|
18
|
+
* @capability doppler-pitch pitch a moving sound up as it approaches and down as it passes, from listener and emitter velocity
|
|
19
|
+
*/
|
|
20
|
+
export declare function dopplerRate(listenerPosition: Vec3Tuple, listenerVelocity: Vec3Tuple, emitterPosition: Vec3Tuple, emitterVelocity: Vec3Tuple, options?: DopplerOptions): number;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Speed of sound in air at 20 °C, metres per second — the default for {@link dopplerRate}. */
|
|
2
|
+
export const SPEED_OF_SOUND = 343;
|
|
3
|
+
/**
|
|
4
|
+
* Doppler pitch multiplier for an emitter heard by a listener: `(c + vListener) / (c - vEmitter)`,
|
|
5
|
+
* where each speed is the component along the line between them, positive when closing. Multiply a
|
|
6
|
+
* loop's playback rate by it. Pure and allocation-free.
|
|
7
|
+
*
|
|
8
|
+
* @capability doppler-pitch pitch a moving sound up as it approaches and down as it passes, from listener and emitter velocity
|
|
9
|
+
*/
|
|
10
|
+
export function dopplerRate(listenerPosition, listenerVelocity, emitterPosition, emitterVelocity, options = {}) {
|
|
11
|
+
const factor = options.factor ?? 1;
|
|
12
|
+
const c = options.speedOfSound ?? SPEED_OF_SOUND;
|
|
13
|
+
const min = options.range?.[0] ?? 0.5;
|
|
14
|
+
const max = options.range?.[1] ?? 2;
|
|
15
|
+
const dx = emitterPosition[0] - listenerPosition[0];
|
|
16
|
+
const dy = emitterPosition[1] - listenerPosition[1];
|
|
17
|
+
const dz = emitterPosition[2] - listenerPosition[2];
|
|
18
|
+
const distance = Math.hypot(dx, dy, dz);
|
|
19
|
+
if (factor === 0 || c <= 0 || distance < 1e-6)
|
|
20
|
+
return 1;
|
|
21
|
+
const ux = dx / distance;
|
|
22
|
+
const uy = dy / distance;
|
|
23
|
+
const uz = dz / distance;
|
|
24
|
+
const listenerClosing = factor * (listenerVelocity[0] * ux + listenerVelocity[1] * uy + listenerVelocity[2] * uz);
|
|
25
|
+
const emitterClosing = -factor * (emitterVelocity[0] * ux + emitterVelocity[1] * uy + emitterVelocity[2] * uz);
|
|
26
|
+
const numerator = c + Math.max(-c * 0.99, listenerClosing);
|
|
27
|
+
const denominator = c - Math.min(c * 0.99, emitterClosing);
|
|
28
|
+
const rate = numerator / denominator;
|
|
29
|
+
if (!Number.isFinite(rate))
|
|
30
|
+
return 1;
|
|
31
|
+
return rate < min ? min : rate > max ? max : rate;
|
|
32
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** One recorded engine sample, keyed by the rpm it was recorded at. */
|
|
2
|
+
export interface EngineLayer {
|
|
3
|
+
/** Catalog sound id of the loop. */
|
|
4
|
+
sound: string;
|
|
5
|
+
/** Rpm the sample was recorded at; playback rate is `rpm / layer.rpm`. */
|
|
6
|
+
rpm: number;
|
|
7
|
+
/** Which load set the layer belongs to: `"on"` (throttle), `"off"` (coast/overrun) or `"any"` (always). Default `"any"`. */
|
|
8
|
+
load?: "on" | "off" | "any";
|
|
9
|
+
/** Extra gain for this layer. Default 1. */
|
|
10
|
+
gain?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Config for {@link createEngineLayers}. */
|
|
13
|
+
export interface EngineLayersConfig {
|
|
14
|
+
/** Prefix for the retained loop ids, one per layer: `${id}:${index}`. */
|
|
15
|
+
id: string;
|
|
16
|
+
layers: readonly EngineLayer[];
|
|
17
|
+
/** Load window the off-load set crossfades to the on-load set across. Default `[0.15, 0.6]`. */
|
|
18
|
+
loadCrossfade?: readonly [number, number];
|
|
19
|
+
/** Exponential rate the load follows `engineLoad` with, so a lifted throttle does not click between sets. Default 10. */
|
|
20
|
+
loadResponse?: number;
|
|
21
|
+
}
|
|
22
|
+
/** One layer's live mix. */
|
|
23
|
+
export interface EngineLayerMix {
|
|
24
|
+
/** Retained loop id. */
|
|
25
|
+
id: string;
|
|
26
|
+
sound: string;
|
|
27
|
+
gain: number;
|
|
28
|
+
rate: number;
|
|
29
|
+
}
|
|
30
|
+
/** Serializable {@link EngineLayers} state. */
|
|
31
|
+
export interface EngineLayersState {
|
|
32
|
+
load: number;
|
|
33
|
+
}
|
|
34
|
+
/** Per-tick parameters {@link EngineLayers.play} forwards to every layer's loop. */
|
|
35
|
+
export interface EngineLayersPlayOptions {
|
|
36
|
+
at?: readonly [number, number, number];
|
|
37
|
+
velocity?: readonly [number, number, number];
|
|
38
|
+
/** Overall gain multiplier. Default 1. */
|
|
39
|
+
gain?: number;
|
|
40
|
+
lowpass?: number;
|
|
41
|
+
highpass?: number;
|
|
42
|
+
}
|
|
43
|
+
/** The part of `ctx.game.audio` {@link EngineLayers} drives. */
|
|
44
|
+
export interface EngineLayersAudio {
|
|
45
|
+
loop(id: string, sound: string, options?: {
|
|
46
|
+
at?: readonly [number, number, number];
|
|
47
|
+
}): void;
|
|
48
|
+
setLoop(id: string, params: {
|
|
49
|
+
rate?: number;
|
|
50
|
+
gain?: number;
|
|
51
|
+
at?: readonly [number, number, number];
|
|
52
|
+
velocity?: readonly [number, number, number];
|
|
53
|
+
lowpass?: number;
|
|
54
|
+
highpass?: number;
|
|
55
|
+
}): void;
|
|
56
|
+
stopLoop(id: string): void;
|
|
57
|
+
}
|
|
58
|
+
/** A layered engine loop: N rpm-keyed samples crossfaded by rpm, with on- and off-load sets picked by engine load. */
|
|
59
|
+
export interface EngineLayers {
|
|
60
|
+
/** Advance by `dt` and return each layer's gain and rate. The returned array and its entries are reused between calls. */
|
|
61
|
+
update(dt: number, signals: {
|
|
62
|
+
rpm: number;
|
|
63
|
+
load: number;
|
|
64
|
+
}): readonly EngineLayerMix[];
|
|
65
|
+
/** Start (idempotently) and update every layer's retained loop from the last {@link update}. */
|
|
66
|
+
play(audio: EngineLayersAudio, options?: EngineLayersPlayOptions): void;
|
|
67
|
+
/** Stop every layer's loop. */
|
|
68
|
+
stop(audio: EngineLayersAudio): void;
|
|
69
|
+
mix(): readonly EngineLayerMix[];
|
|
70
|
+
retune(next: EngineLayersConfig): void;
|
|
71
|
+
snapshot(): EngineLayersState;
|
|
72
|
+
restore(state: EngineLayersState): void;
|
|
73
|
+
reset(): void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Layered engine sound: each set of rpm-keyed samples is equal-power crossfaded between the two
|
|
77
|
+
* samples bracketing the current rpm and pitched by `rpm / layer.rpm`; the on- and off-load sets are
|
|
78
|
+
* equal-power crossfaded by smoothed `engineLoad`. A config with only one load set plays it at every
|
|
79
|
+
* load. `play` drives `ctx.game.audio` directly, so a game needs no per-layer glue.
|
|
80
|
+
*
|
|
81
|
+
* @capability engine-layers layered engine audio: rpm-keyed samples crossfaded, with on-load and off-load sets picked by engine load
|
|
82
|
+
*/
|
|
83
|
+
export declare function createEngineLayers(initial: EngineLayersConfig): EngineLayers;
|