@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/anim/animGraph.d.ts
CHANGED
|
@@ -113,7 +113,10 @@ export interface AnimGraphOutput {
|
|
|
113
113
|
name: string;
|
|
114
114
|
clip: string;
|
|
115
115
|
}[];
|
|
116
|
+
/** Root-bone travel over this advance from `rootMotion` states, in the rig's local units. */
|
|
116
117
|
rootDelta?: [number, number, number];
|
|
118
|
+
/** `true` while any layer's current state has `rootMotion`, even on an advance with no travel. */
|
|
119
|
+
rootMotion?: true;
|
|
117
120
|
}
|
|
118
121
|
/** The evaluator handle: arm triggers, advance, inspect, snapshot and restore. */
|
|
119
122
|
export interface AnimGraphRuntime {
|
|
@@ -141,4 +144,12 @@ export declare function stateClipWeights(state: AnimState, params: AnimParams):
|
|
|
141
144
|
* @capability anim-graph data-first animation state machine with blend trees, crossfades, layers, and clip events
|
|
142
145
|
*/
|
|
143
146
|
export declare function createAnimGraphRuntime(initial: AnimGraph): AnimGraphRuntime;
|
|
147
|
+
/**
|
|
148
|
+
* Validates untrusted JSON (a saved scene document, a network payload) as an {@link AnimGraph}.
|
|
149
|
+
* Malformed states, transitions to unknown states, and bad conditions are dropped; a layer whose
|
|
150
|
+
* entry state is missing is dropped; the result is `undefined` when no layer survives.
|
|
151
|
+
*
|
|
152
|
+
* @capability anim-graph load an animation graph from saved JSON without trusting its shape
|
|
153
|
+
*/
|
|
154
|
+
export declare function parseAnimGraph(value: unknown): AnimGraph | undefined;
|
|
144
155
|
export {};
|
package/dist/anim/animGraph.js
CHANGED
|
@@ -197,6 +197,7 @@ export function createAnimGraphRuntime(initial) {
|
|
|
197
197
|
advance(dt, params, clips) {
|
|
198
198
|
const output = { clips: [], events: [] };
|
|
199
199
|
const rootDelta = [0, 0, 0];
|
|
200
|
+
let rootMotion = false;
|
|
200
201
|
for (const layer of graph.layers) {
|
|
201
202
|
const state = layers[layer.id];
|
|
202
203
|
if (state === undefined)
|
|
@@ -205,6 +206,8 @@ export function createAnimGraphRuntime(initial) {
|
|
|
205
206
|
if (def === undefined)
|
|
206
207
|
continue;
|
|
207
208
|
const layerWeight = layer.weight ?? 1;
|
|
209
|
+
if (def.rootMotion === true && layerWeight > 0)
|
|
210
|
+
rootMotion = true;
|
|
208
211
|
const before = state.time;
|
|
209
212
|
state.time += dt * stateSpeed(def);
|
|
210
213
|
const weights = stateClipWeights(def, params);
|
|
@@ -280,7 +283,132 @@ export function createAnimGraphRuntime(initial) {
|
|
|
280
283
|
triggers.clear();
|
|
281
284
|
if (rootDelta[0] !== 0 || rootDelta[1] !== 0 || rootDelta[2] !== 0)
|
|
282
285
|
output.rootDelta = rootDelta;
|
|
286
|
+
if (rootMotion)
|
|
287
|
+
output.rootMotion = true;
|
|
283
288
|
return output;
|
|
284
289
|
},
|
|
285
290
|
};
|
|
286
291
|
}
|
|
292
|
+
const COMPARES = [">", "<", ">=", "<=", "==", "!="];
|
|
293
|
+
function record(value) {
|
|
294
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
295
|
+
}
|
|
296
|
+
function optionalNumber(value) {
|
|
297
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
298
|
+
}
|
|
299
|
+
function parseState(value) {
|
|
300
|
+
const raw = record(value);
|
|
301
|
+
if (raw === null)
|
|
302
|
+
return null;
|
|
303
|
+
const extra = {
|
|
304
|
+
...(optionalNumber(raw.speed) === undefined ? {} : { speed: raw.speed }),
|
|
305
|
+
...(typeof raw.loop === "boolean" ? { loop: raw.loop } : {}),
|
|
306
|
+
...(typeof raw.rootMotion === "boolean" ? { rootMotion: raw.rootMotion } : {}),
|
|
307
|
+
};
|
|
308
|
+
if (raw.kind === "clip" && typeof raw.clip === "string")
|
|
309
|
+
return { kind: "clip", clip: raw.clip, ...extra };
|
|
310
|
+
if (!Array.isArray(raw.points))
|
|
311
|
+
return null;
|
|
312
|
+
if (raw.kind === "blend1D" && typeof raw.param === "string") {
|
|
313
|
+
const points = raw.points.flatMap((point) => {
|
|
314
|
+
const entry = record(point);
|
|
315
|
+
return entry !== null && typeof entry.clip === "string" && optionalNumber(entry.at) !== undefined ? [{ at: entry.at, clip: entry.clip }] : [];
|
|
316
|
+
});
|
|
317
|
+
return { kind: "blend1D", param: raw.param, points, ...extra };
|
|
318
|
+
}
|
|
319
|
+
if (raw.kind === "blend2D" && Array.isArray(raw.params) && raw.params.length === 2 && raw.params.every((param) => typeof param === "string")) {
|
|
320
|
+
const points = raw.points.flatMap((point) => {
|
|
321
|
+
const entry = record(point);
|
|
322
|
+
const at = entry?.at;
|
|
323
|
+
return entry !== null && typeof entry.clip === "string" && Array.isArray(at) && at.length === 2 && at.every((n) => optionalNumber(n) !== undefined)
|
|
324
|
+
? [{ at: [at[0], at[1]], clip: entry.clip }]
|
|
325
|
+
: [];
|
|
326
|
+
});
|
|
327
|
+
return { kind: "blend2D", params: [raw.params[0], raw.params[1]], points, ...extra };
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
function parseTransition(value, states) {
|
|
332
|
+
const raw = record(value);
|
|
333
|
+
if (raw === null || typeof raw.from !== "string" || typeof raw.to !== "string")
|
|
334
|
+
return null;
|
|
335
|
+
if (states[raw.to] === undefined || (raw.from !== "*" && states[raw.from] === undefined))
|
|
336
|
+
return null;
|
|
337
|
+
const when = Array.isArray(raw.when)
|
|
338
|
+
? raw.when.flatMap((condition) => {
|
|
339
|
+
const entry = record(condition);
|
|
340
|
+
return entry !== null &&
|
|
341
|
+
typeof entry.param === "string" &&
|
|
342
|
+
COMPARES.includes(entry.op) &&
|
|
343
|
+
(typeof entry.value === "number" || typeof entry.value === "boolean")
|
|
344
|
+
? [{ param: entry.param, op: entry.op, value: entry.value }]
|
|
345
|
+
: [];
|
|
346
|
+
})
|
|
347
|
+
: undefined;
|
|
348
|
+
return {
|
|
349
|
+
from: raw.from,
|
|
350
|
+
to: raw.to,
|
|
351
|
+
...(when === undefined || when.length === 0 ? {} : { when }),
|
|
352
|
+
...(typeof raw.trigger === "string" ? { trigger: raw.trigger } : {}),
|
|
353
|
+
...(optionalNumber(raw.duration) === undefined ? {} : { duration: Math.max(0, raw.duration) }),
|
|
354
|
+
...(optionalNumber(raw.exitTime) === undefined ? {} : { exitTime: raw.exitTime }),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Validates untrusted JSON (a saved scene document, a network payload) as an {@link AnimGraph}.
|
|
359
|
+
* Malformed states, transitions to unknown states, and bad conditions are dropped; a layer whose
|
|
360
|
+
* entry state is missing is dropped; the result is `undefined` when no layer survives.
|
|
361
|
+
*
|
|
362
|
+
* @capability anim-graph load an animation graph from saved JSON without trusting its shape
|
|
363
|
+
*/
|
|
364
|
+
export function parseAnimGraph(value) {
|
|
365
|
+
const raw = record(value);
|
|
366
|
+
if (raw === null || !Array.isArray(raw.layers))
|
|
367
|
+
return undefined;
|
|
368
|
+
const layers = [];
|
|
369
|
+
const seen = new Set();
|
|
370
|
+
for (const candidate of raw.layers) {
|
|
371
|
+
const layer = record(candidate);
|
|
372
|
+
if (layer === null || typeof layer.id !== "string" || seen.has(layer.id) || typeof layer.entry !== "string")
|
|
373
|
+
continue;
|
|
374
|
+
const rawStates = record(layer.states);
|
|
375
|
+
if (rawStates === null)
|
|
376
|
+
continue;
|
|
377
|
+
const states = {};
|
|
378
|
+
for (const [name, state] of Object.entries(rawStates)) {
|
|
379
|
+
const parsed = parseState(state);
|
|
380
|
+
if (parsed !== null)
|
|
381
|
+
states[name] = parsed;
|
|
382
|
+
}
|
|
383
|
+
if (states[layer.entry] === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
const transitions = Array.isArray(layer.transitions)
|
|
386
|
+
? layer.transitions.flatMap((transition) => {
|
|
387
|
+
const parsed = parseTransition(transition, states);
|
|
388
|
+
return parsed === null ? [] : [parsed];
|
|
389
|
+
})
|
|
390
|
+
: [];
|
|
391
|
+
const mask = Array.isArray(layer.mask) ? layer.mask.filter((prefix) => typeof prefix === "string") : undefined;
|
|
392
|
+
seen.add(layer.id);
|
|
393
|
+
layers.push({
|
|
394
|
+
id: layer.id,
|
|
395
|
+
entry: layer.entry,
|
|
396
|
+
states,
|
|
397
|
+
transitions,
|
|
398
|
+
...(mask === undefined ? {} : { mask }),
|
|
399
|
+
...(typeof layer.additive === "boolean" ? { additive: layer.additive } : {}),
|
|
400
|
+
...(optionalNumber(layer.weight) === undefined ? {} : { weight: Math.min(1, Math.max(0, layer.weight)) }),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (layers.length === 0)
|
|
404
|
+
return undefined;
|
|
405
|
+
const events = Array.isArray(raw.events)
|
|
406
|
+
? raw.events.flatMap((event) => {
|
|
407
|
+
const entry = record(event);
|
|
408
|
+
return entry !== null && typeof entry.clip === "string" && typeof entry.name === "string" && optionalNumber(entry.atSec) !== undefined
|
|
409
|
+
? [{ clip: entry.clip, atSec: entry.atSec, name: entry.name }]
|
|
410
|
+
: [];
|
|
411
|
+
})
|
|
412
|
+
: [];
|
|
413
|
+
return { layers, ...(events.length === 0 ? {} : { events }) };
|
|
414
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** A clip to bake: its name and length in seconds. */
|
|
2
|
+
export interface BoneTextureClipInput {
|
|
3
|
+
name: string;
|
|
4
|
+
duration: number;
|
|
5
|
+
}
|
|
6
|
+
/** One baked clip's rows in a {@link BoneTextureLayout}. */
|
|
7
|
+
export interface BoneTextureClip {
|
|
8
|
+
name: string;
|
|
9
|
+
/** First texture row of the clip. */
|
|
10
|
+
startFrame: number;
|
|
11
|
+
/** Rows baked for the clip, first and last pose included. */
|
|
12
|
+
frameCount: number;
|
|
13
|
+
duration: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Where every bone matrix of every baked frame lives in a vertex-animation texture: one row per
|
|
17
|
+
* frame, four RGBA texels (matrix columns) per bone. Plain data, so a baked crowd can be cached,
|
|
18
|
+
* shipped or checked in a test without a renderer.
|
|
19
|
+
*/
|
|
20
|
+
export interface BoneTextureLayout {
|
|
21
|
+
boneCount: number;
|
|
22
|
+
fps: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
clips: BoneTextureClip[];
|
|
26
|
+
}
|
|
27
|
+
/** Inputs for {@link planBoneTexture}. */
|
|
28
|
+
export interface BoneTextureInput {
|
|
29
|
+
boneCount: number;
|
|
30
|
+
clips: readonly BoneTextureClipInput[];
|
|
31
|
+
/** Baked samples per second. Default 30. */
|
|
32
|
+
fps?: number;
|
|
33
|
+
/** Largest texture side the target GPU accepts. Default 4096. */
|
|
34
|
+
maxTextureSize?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Plans a bone-matrix texture for a rig's clips. Throws when the rig or the clip set does not fit
|
|
38
|
+
* `maxTextureSize`; lower `fps` or bake fewer clips.
|
|
39
|
+
*
|
|
40
|
+
* @capability animation bake many clips of a rig into one bone-matrix texture for instanced crowds
|
|
41
|
+
*/
|
|
42
|
+
export declare function planBoneTexture(input: BoneTextureInput): BoneTextureLayout;
|
|
43
|
+
/** Two texture rows around a playback time and the blend between them. */
|
|
44
|
+
export interface BoneTextureSample {
|
|
45
|
+
rowA: number;
|
|
46
|
+
rowB: number;
|
|
47
|
+
t: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Finds the baked rows around `time` in a clip, wrapping when `loop` and holding the last pose
|
|
51
|
+
* otherwise. This is the CPU mirror of the crowd shader's lookup.
|
|
52
|
+
*
|
|
53
|
+
* @capability animation look up the baked frames and blend for a crowd member's playback time
|
|
54
|
+
*/
|
|
55
|
+
export declare function sampleBoneTexture(clip: BoneTextureClip, fps: number, time: number, loop: boolean, out?: BoneTextureSample): BoneTextureSample;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plans a bone-matrix texture for a rig's clips. Throws when the rig or the clip set does not fit
|
|
3
|
+
* `maxTextureSize`; lower `fps` or bake fewer clips.
|
|
4
|
+
*
|
|
5
|
+
* @capability animation bake many clips of a rig into one bone-matrix texture for instanced crowds
|
|
6
|
+
*/
|
|
7
|
+
export function planBoneTexture(input) {
|
|
8
|
+
const fps = input.fps ?? 30;
|
|
9
|
+
const maxSize = input.maxTextureSize ?? 4096;
|
|
10
|
+
if (!(fps > 0))
|
|
11
|
+
throw new Error(`planBoneTexture: fps must be positive, got ${fps}`);
|
|
12
|
+
const width = Math.max(1, input.boneCount) * 4;
|
|
13
|
+
if (width > maxSize)
|
|
14
|
+
throw new Error(`planBoneTexture: ${input.boneCount} bones need ${width} texels per row, over the ${maxSize} limit`);
|
|
15
|
+
const clips = [];
|
|
16
|
+
let row = 0;
|
|
17
|
+
for (const clip of input.clips) {
|
|
18
|
+
const duration = Math.max(0, clip.duration);
|
|
19
|
+
const frameCount = Math.max(2, Math.ceil(duration * fps) + 1);
|
|
20
|
+
clips.push({ name: clip.name, startFrame: row, frameCount, duration });
|
|
21
|
+
row += frameCount;
|
|
22
|
+
}
|
|
23
|
+
if (row > maxSize)
|
|
24
|
+
throw new Error(`planBoneTexture: ${row} frames exceed the ${maxSize}-row limit; lower fps or bake fewer clips`);
|
|
25
|
+
return { boneCount: input.boneCount, fps, width, height: Math.max(1, row), clips };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Finds the baked rows around `time` in a clip, wrapping when `loop` and holding the last pose
|
|
29
|
+
* otherwise. This is the CPU mirror of the crowd shader's lookup.
|
|
30
|
+
*
|
|
31
|
+
* @capability animation look up the baked frames and blend for a crowd member's playback time
|
|
32
|
+
*/
|
|
33
|
+
export function sampleBoneTexture(clip, fps, time, loop, out) {
|
|
34
|
+
const result = out ?? { rowA: 0, rowB: 0, t: 0 };
|
|
35
|
+
const span = clip.frameCount - 1;
|
|
36
|
+
const raw = time * fps;
|
|
37
|
+
const frame = loop ? ((raw % span) + span) % span : Math.min(Math.max(raw, 0), span);
|
|
38
|
+
const a = Math.floor(frame);
|
|
39
|
+
result.rowA = clip.startFrame + a;
|
|
40
|
+
result.rowB = clip.startFrame + Math.min(a + 1, span);
|
|
41
|
+
result.t = frame - a;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** One foot's animated ankle height and the ground found under it, both in world units. */
|
|
2
|
+
export interface FootGroundSample {
|
|
3
|
+
/** Animated ankle (foot bone) height this frame, before any correction. */
|
|
4
|
+
ankleY: number;
|
|
5
|
+
/** Ground height under the foot, or `null` when the probe found nothing. */
|
|
6
|
+
groundY: number | null;
|
|
7
|
+
}
|
|
8
|
+
/** Inputs for {@link placeFeet}. All heights are world units. */
|
|
9
|
+
export interface FootPlacementInput {
|
|
10
|
+
feet: readonly FootGroundSample[];
|
|
11
|
+
/** Height of the model origin, which the clips were authored to stand on. */
|
|
12
|
+
originY: number;
|
|
13
|
+
/** Ankle height above the sole in the rig's rest pose; the sole never ends below the ground. */
|
|
14
|
+
ankleHeight: number;
|
|
15
|
+
/** Largest correction applied to a foot or the pelvis, up or down. */
|
|
16
|
+
maxAdjust: number;
|
|
17
|
+
/** A lowest sole this far above its ground reads as airborne. */
|
|
18
|
+
airborneGap: number;
|
|
19
|
+
}
|
|
20
|
+
/** Result of {@link placeFeet}: where each ankle should go and how far the pelvis drops. */
|
|
21
|
+
export interface FootPlacement {
|
|
22
|
+
/** Vertical pelvis shift, `<= 0`, so the lowest planted foot can reach its ground. */
|
|
23
|
+
pelvisOffset: number;
|
|
24
|
+
/** Target ankle height per foot, or `null` when that foot has no ground or no correction. */
|
|
25
|
+
ankleTargets: (number | null)[];
|
|
26
|
+
/** `false` while the feet are clear of the ground, so callers fade the correction out. */
|
|
27
|
+
grounded: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolves foot placement for a rig whose clips were authored on flat ground at its origin.
|
|
31
|
+
* Each foot keeps its animated lift and moves by its ground's height above or below the origin,
|
|
32
|
+
* the sole is never left under the ground, and the pelvis drops by the deepest reach so both legs
|
|
33
|
+
* stay within length. Pure and allocation-light; the renderer applies the targets with
|
|
34
|
+
* {@link solveTwoBone}.
|
|
35
|
+
*
|
|
36
|
+
* @capability animation ground rigged feet on slopes and steps without hiding the swing leg
|
|
37
|
+
*/
|
|
38
|
+
export declare function placeFeet(input: FootPlacementInput, out?: FootPlacement): FootPlacement;
|
|
39
|
+
/** A bone as {@link inferLegChains} sees it: its name and its parent's name. */
|
|
40
|
+
export interface RigBone {
|
|
41
|
+
name: string;
|
|
42
|
+
parent: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** Thigh → shin → foot bone names for one leg. */
|
|
45
|
+
export interface LegChain {
|
|
46
|
+
root: string;
|
|
47
|
+
mid: string;
|
|
48
|
+
tip: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Finds leg chains on a humanoid rig by bone name: a thigh-like bone (`UpLeg`, `UpperLeg`, `Thigh`)
|
|
52
|
+
* with its first child and grandchild as shin and foot. Control, IK-target and twist bones are
|
|
53
|
+
* skipped. Returns chains in rig order; an empty array means the rig has no recognizable legs.
|
|
54
|
+
*
|
|
55
|
+
* @capability animation find thigh, shin and foot bones on a humanoid rig for foot IK
|
|
56
|
+
*/
|
|
57
|
+
export declare function inferLegChains(bones: readonly RigBone[]): LegChain[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function clamp(value, limit) {
|
|
2
|
+
return Math.max(-limit, Math.min(limit, value));
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Resolves foot placement for a rig whose clips were authored on flat ground at its origin.
|
|
6
|
+
* Each foot keeps its animated lift and moves by its ground's height above or below the origin,
|
|
7
|
+
* the sole is never left under the ground, and the pelvis drops by the deepest reach so both legs
|
|
8
|
+
* stay within length. Pure and allocation-light; the renderer applies the targets with
|
|
9
|
+
* {@link solveTwoBone}.
|
|
10
|
+
*
|
|
11
|
+
* @capability animation ground rigged feet on slopes and steps without hiding the swing leg
|
|
12
|
+
*/
|
|
13
|
+
export function placeFeet(input, out) {
|
|
14
|
+
const result = out ?? { pelvisOffset: 0, ankleTargets: [], grounded: false };
|
|
15
|
+
result.ankleTargets.length = input.feet.length;
|
|
16
|
+
const limit = Math.max(0, input.maxAdjust);
|
|
17
|
+
let lowestGap = Infinity;
|
|
18
|
+
let pelvis = 0;
|
|
19
|
+
for (let i = 0; i < input.feet.length; i += 1) {
|
|
20
|
+
const foot = input.feet[i];
|
|
21
|
+
if (foot.groundY === null) {
|
|
22
|
+
result.ankleTargets[i] = null;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
lowestGap = Math.min(lowestGap, foot.ankleY - input.ankleHeight - foot.groundY);
|
|
26
|
+
const followed = foot.ankleY + clamp(foot.groundY - input.originY, limit);
|
|
27
|
+
const target = Math.min(foot.ankleY + limit, Math.max(followed, foot.groundY + input.ankleHeight));
|
|
28
|
+
result.ankleTargets[i] = target;
|
|
29
|
+
pelvis = Math.min(pelvis, target - foot.ankleY);
|
|
30
|
+
}
|
|
31
|
+
result.grounded = lowestGap <= input.airborneGap;
|
|
32
|
+
result.pelvisOffset = result.grounded ? Math.max(-limit, pelvis) : 0;
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const THIGH = /(upleg|upperleg|thigh|femur)/;
|
|
36
|
+
const HELPER = /(ik|control|ctrl|twist|pole|target|roll)/;
|
|
37
|
+
/**
|
|
38
|
+
* Finds leg chains on a humanoid rig by bone name: a thigh-like bone (`UpLeg`, `UpperLeg`, `Thigh`)
|
|
39
|
+
* with its first child and grandchild as shin and foot. Control, IK-target and twist bones are
|
|
40
|
+
* skipped. Returns chains in rig order; an empty array means the rig has no recognizable legs.
|
|
41
|
+
*
|
|
42
|
+
* @capability animation find thigh, shin and foot bones on a humanoid rig for foot IK
|
|
43
|
+
*/
|
|
44
|
+
export function inferLegChains(bones) {
|
|
45
|
+
const children = new Map();
|
|
46
|
+
for (const bone of bones) {
|
|
47
|
+
if (bone.parent === null)
|
|
48
|
+
continue;
|
|
49
|
+
const list = children.get(bone.parent);
|
|
50
|
+
if (list === undefined)
|
|
51
|
+
children.set(bone.parent, [bone.name]);
|
|
52
|
+
else
|
|
53
|
+
list.push(bone.name);
|
|
54
|
+
}
|
|
55
|
+
const firstChild = (name) => children.get(name)?.find((child) => !HELPER.test(child.toLowerCase()));
|
|
56
|
+
const chains = [];
|
|
57
|
+
for (const bone of bones) {
|
|
58
|
+
const key = bone.name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
59
|
+
if (!THIGH.test(key) || HELPER.test(key))
|
|
60
|
+
continue;
|
|
61
|
+
const mid = firstChild(bone.name);
|
|
62
|
+
const tip = mid === undefined ? undefined : firstChild(mid);
|
|
63
|
+
if (mid !== undefined && tip !== undefined)
|
|
64
|
+
chains.push({ root: bone.name, mid, tip });
|
|
65
|
+
}
|
|
66
|
+
return chains;
|
|
67
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModelAnimationConfig } from "../game/playableGame.js";
|
|
1
2
|
import type { AnimGraph } from "./animGraph.js";
|
|
2
3
|
/** Inputs for {@link locomotionGraph}: the idle/walk/run clip names and the one-shot table a rig config already carries. */
|
|
3
4
|
export interface LocomotionGraphInput {
|
|
@@ -24,3 +25,11 @@ export declare const LOCOMOTION_LAYER = "base";
|
|
|
24
25
|
* @capability locomotion-graph build the default idle/walk/run plus one-shot graph from clip names
|
|
25
26
|
*/
|
|
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;
|
|
@@ -31,3 +31,32 @@ export function locomotionGraph(input) {
|
|
|
31
31
|
layers: [{ id: LOCOMOTION_LAYER, entry: "locomotion", states, transitions }],
|
|
32
32
|
};
|
|
33
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
|
+
}
|
|
@@ -27,6 +27,8 @@ 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;
|
|
30
32
|
/** Web Audio spatial panning configuration. When present, the shell uses a PannerNode. */
|
|
31
33
|
spatial?: {
|
|
32
34
|
panning: "hrtf" | "equalpower";
|
|
@@ -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;
|