@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
|
@@ -7,9 +7,15 @@ export interface FreezeViolation {
|
|
|
7
7
|
speed: number;
|
|
8
8
|
movedSeconds: number;
|
|
9
9
|
}
|
|
10
|
+
/** Plain JSON state of a {@link FreezeMonitor}: seconds each frozen subject has kept moving. */
|
|
11
|
+
export interface FreezeMonitorState {
|
|
12
|
+
moved: Record<string, number>;
|
|
13
|
+
}
|
|
10
14
|
export interface FreezeMonitor {
|
|
11
15
|
tick(subjects: readonly FreezeSubject[], frozenIds: ReadonlySet<string>, dt: number): FreezeViolation[];
|
|
12
16
|
reset(id?: string): void;
|
|
17
|
+
snapshot(): FreezeMonitorState;
|
|
18
|
+
restore(next: FreezeMonitorState): void;
|
|
13
19
|
}
|
|
14
20
|
/** @internal */
|
|
15
21
|
export declare function createFreezeMonitor(config?: {
|
|
@@ -32,5 +32,13 @@ export function createFreezeMonitor(config) {
|
|
|
32
32
|
else
|
|
33
33
|
moved.delete(id);
|
|
34
34
|
},
|
|
35
|
+
snapshot() {
|
|
36
|
+
return { moved: Object.fromEntries(moved) };
|
|
37
|
+
},
|
|
38
|
+
restore(next) {
|
|
39
|
+
moved.clear();
|
|
40
|
+
for (const [id, seconds] of Object.entries(next.moved))
|
|
41
|
+
moved.set(id, seconds);
|
|
42
|
+
},
|
|
35
43
|
};
|
|
36
44
|
}
|
|
@@ -7,6 +7,7 @@ export interface PerceptionStimulus {
|
|
|
7
7
|
kind: PerceptionStimulusKind;
|
|
8
8
|
sourceId: string;
|
|
9
9
|
position: PerceptionPosition;
|
|
10
|
+
/** Multiplies `hearingRange` for this sound: a footstep might be `0.3`, a gunshot `4`. Default `1`. */
|
|
10
11
|
loudness?: number;
|
|
11
12
|
at: number;
|
|
12
13
|
}
|
|
@@ -34,6 +35,8 @@ export interface PerceptionConfig {
|
|
|
34
35
|
sightConeDeg: number;
|
|
35
36
|
hearingRange: number;
|
|
36
37
|
memorySeconds: number;
|
|
38
|
+
/** Most stimuli retained at once; the oldest are dropped first. Default 256. */
|
|
39
|
+
maxStimuli?: number;
|
|
37
40
|
occluded?: (from: PerceptionPosition, to: PerceptionPosition) => boolean;
|
|
38
41
|
}
|
|
39
42
|
/** Serializable state for a perception service. */
|
|
@@ -42,14 +45,21 @@ export interface PerceptionSnapshot {
|
|
|
42
45
|
observerId: string;
|
|
43
46
|
memory: PerceptionMemory;
|
|
44
47
|
}>;
|
|
45
|
-
stimuli: PerceptionStimulus
|
|
48
|
+
stimuli: Array<PerceptionStimulus & {
|
|
49
|
+
seq?: number;
|
|
50
|
+
}>;
|
|
46
51
|
nowMs: number;
|
|
52
|
+
/** Per observer, the last stimulus sequence number it has already judged. Absent in older saves. */
|
|
53
|
+
heard?: Record<string, number>;
|
|
54
|
+
nextSeq?: number;
|
|
47
55
|
}
|
|
48
56
|
/** Stateful perception API with retuning and save/restore support. */
|
|
49
57
|
export interface PerceptionService {
|
|
50
58
|
pushStimulus(stimulus: PerceptionStimulus): void;
|
|
51
59
|
observe(observer: PerceptionObserver, candidates: readonly PerceptionCandidate[], nowMs: number): void;
|
|
52
60
|
memory(observerId: string): PerceptionMemory[];
|
|
61
|
+
/** Drop one remembered target, or everything an observer remembers (despawn, reset, disguise). */
|
|
62
|
+
forget(observerId: string, targetId?: string): void;
|
|
53
63
|
retune(patch: Partial<PerceptionConfig>): void;
|
|
54
64
|
snapshot(): PerceptionSnapshot;
|
|
55
65
|
restore(snapshot: PerceptionSnapshot): void;
|
|
@@ -28,7 +28,10 @@ function validate(config) {
|
|
|
28
28
|
throw new Error("hearingRange must be non-negative.");
|
|
29
29
|
if (!Number.isFinite(config.memorySeconds) || config.memorySeconds <= 0)
|
|
30
30
|
throw new Error("memorySeconds must be positive.");
|
|
31
|
+
if (config.maxStimuli !== undefined && (!Number.isInteger(config.maxStimuli) || config.maxStimuli < 1))
|
|
32
|
+
throw new Error("maxStimuli must be a positive integer.");
|
|
31
33
|
}
|
|
34
|
+
const DEFAULT_MAX_STIMULI = 256;
|
|
32
35
|
/** Creates a deterministic sight, sound, and damage perception service with expiring target memory.
|
|
33
36
|
* @capability perception-service retain sight, sound, and damage observations with range, cone, occlusion, and decay policy
|
|
34
37
|
*/
|
|
@@ -37,6 +40,8 @@ export function createPerception(initial) {
|
|
|
37
40
|
let config = { ...initial };
|
|
38
41
|
let nowMs = 0;
|
|
39
42
|
const stimuli = [];
|
|
43
|
+
const heard = new Map();
|
|
44
|
+
let nextSeq = 1;
|
|
40
45
|
const memories = new Map();
|
|
41
46
|
function remember(observerId, targetId, position, at, confidence) {
|
|
42
47
|
if (confidence <= 0)
|
|
@@ -62,8 +67,11 @@ export function createPerception(initial) {
|
|
|
62
67
|
memories.delete(observerId);
|
|
63
68
|
}
|
|
64
69
|
const cutoff = nowMs - memoryMs;
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
let drop = Math.max(0, stimuli.length - (config.maxStimuli ?? DEFAULT_MAX_STIMULI));
|
|
71
|
+
while (drop < stimuli.length && stimuli[drop].at < cutoff)
|
|
72
|
+
drop += 1;
|
|
73
|
+
if (drop > 0)
|
|
74
|
+
stimuli.splice(0, drop);
|
|
67
75
|
}
|
|
68
76
|
return {
|
|
69
77
|
pushStimulus(stimulus) {
|
|
@@ -72,8 +80,10 @@ export function createPerception(initial) {
|
|
|
72
80
|
if (stimulus.kind === "sound" && stimulus.loudness !== undefined && (!Number.isFinite(stimulus.loudness) || stimulus.loudness < 0)) {
|
|
73
81
|
throw new Error("stimulus.loudness must be non-negative.");
|
|
74
82
|
}
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
let index = stimuli.length;
|
|
84
|
+
while (index > 0 && stimuli[index - 1].at > stimulus.at)
|
|
85
|
+
index -= 1;
|
|
86
|
+
stimuli.splice(index, 0, { ...stimulus, position: [...stimulus.position], seq: nextSeq++ });
|
|
77
87
|
nowMs = Math.max(nowMs, stimulus.at);
|
|
78
88
|
prune();
|
|
79
89
|
},
|
|
@@ -88,20 +98,33 @@ export function createPerception(initial) {
|
|
|
88
98
|
continue;
|
|
89
99
|
remember(observer.id, candidate.id, candidate.position, nowMs, 1);
|
|
90
100
|
}
|
|
101
|
+
// Each observer judges a stimulus once, from where it stood when it first observed after the stimulus;
|
|
102
|
+
// re-judging every tick would make an old noise louder as the observer walks toward it.
|
|
103
|
+
const judgedThrough = heard.get(observer.id) ?? 0;
|
|
104
|
+
let judged = judgedThrough;
|
|
105
|
+
let firstFuture = Number.POSITIVE_INFINITY;
|
|
91
106
|
for (const stimulus of stimuli) {
|
|
92
|
-
if (stimulus.
|
|
107
|
+
if (stimulus.seq <= judgedThrough)
|
|
108
|
+
continue;
|
|
109
|
+
if (stimulus.at > nowMs) {
|
|
110
|
+
firstFuture = Math.min(firstFuture, stimulus.seq);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
judged = Math.max(judged, stimulus.seq);
|
|
114
|
+
if (stimulus.sourceId === observer.id)
|
|
93
115
|
continue;
|
|
94
116
|
const range = distance(observer.position, stimulus.position);
|
|
95
117
|
if (stimulus.kind === "sound") {
|
|
96
|
-
|
|
118
|
+
const audible = config.hearingRange * (stimulus.loudness ?? 1);
|
|
119
|
+
if (range >= audible)
|
|
97
120
|
continue;
|
|
98
|
-
|
|
99
|
-
remember(observer.id, stimulus.sourceId, stimulus.position, stimulus.at, falloff * (stimulus.loudness ?? 1));
|
|
121
|
+
remember(observer.id, stimulus.sourceId, stimulus.position, stimulus.at, 1 - range / audible);
|
|
100
122
|
}
|
|
101
123
|
else if (stimulus.kind === "damage") {
|
|
102
124
|
remember(observer.id, stimulus.sourceId, stimulus.position, stimulus.at, 1);
|
|
103
125
|
}
|
|
104
126
|
}
|
|
127
|
+
heard.set(observer.id, Math.min(judged, firstFuture - 1));
|
|
105
128
|
prune();
|
|
106
129
|
},
|
|
107
130
|
memory(observerId) {
|
|
@@ -117,13 +140,29 @@ export function createPerception(initial) {
|
|
|
117
140
|
}
|
|
118
141
|
return result;
|
|
119
142
|
},
|
|
143
|
+
forget(observerId, targetId) {
|
|
144
|
+
if (targetId === undefined) {
|
|
145
|
+
memories.delete(observerId);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const observerMemories = memories.get(observerId);
|
|
149
|
+
observerMemories?.delete(targetId);
|
|
150
|
+
if (observerMemories?.size === 0)
|
|
151
|
+
memories.delete(observerId);
|
|
152
|
+
},
|
|
120
153
|
retune(patch) {
|
|
121
154
|
config = { ...config, ...patch };
|
|
122
155
|
validate(config);
|
|
123
156
|
prune();
|
|
124
157
|
},
|
|
125
158
|
snapshot() {
|
|
126
|
-
const result = {
|
|
159
|
+
const result = {
|
|
160
|
+
memories: [],
|
|
161
|
+
stimuli: stimuli.map((stimulus) => ({ ...stimulus, position: [...stimulus.position] })),
|
|
162
|
+
nowMs,
|
|
163
|
+
heard: Object.fromEntries(heard),
|
|
164
|
+
nextSeq,
|
|
165
|
+
};
|
|
127
166
|
for (const [observerId, observerMemories] of memories)
|
|
128
167
|
for (const memory of observerMemories.values())
|
|
129
168
|
result.memories.push({ observerId, memory: { ...memory, lastKnownPos: [...memory.lastKnownPos] } });
|
|
@@ -133,7 +172,15 @@ export function createPerception(initial) {
|
|
|
133
172
|
memories.clear();
|
|
134
173
|
stimuli.length = 0;
|
|
135
174
|
nowMs = snapshot.nowMs;
|
|
136
|
-
|
|
175
|
+
heard.clear();
|
|
176
|
+
for (const [observerId, seq] of Object.entries(snapshot.heard ?? {}))
|
|
177
|
+
heard.set(observerId, seq);
|
|
178
|
+
let seq = 0;
|
|
179
|
+
for (const stimulus of snapshot.stimuli) {
|
|
180
|
+
seq = stimulus.seq ?? seq + 1;
|
|
181
|
+
stimuli.push({ ...stimulus, position: [...stimulus.position], seq });
|
|
182
|
+
}
|
|
183
|
+
nextSeq = Math.max(snapshot.nextSeq ?? 0, seq + 1);
|
|
137
184
|
for (const entry of snapshot.memories)
|
|
138
185
|
remember(entry.observerId, entry.memory.targetId, entry.memory.lastKnownPos, entry.memory.lastSeenAt, entry.memory.confidence);
|
|
139
186
|
prune();
|
|
@@ -24,6 +24,9 @@ export interface RecordingBuffer<T> {
|
|
|
24
24
|
clear(): void;
|
|
25
25
|
duration(): number;
|
|
26
26
|
frames(): readonly RecordingFrame<T>[];
|
|
27
|
+
/** Copies the frame list; each frame's `data` is shared, since the buffer never mutates it. */
|
|
28
|
+
snapshot(): RecordingFrame<T>[];
|
|
29
|
+
restore(next: readonly RecordingFrame<T>[]): void;
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
32
|
* A session-recording buffer for replay / photo mode / kill-cam: append
|
|
@@ -65,5 +65,13 @@ export function createRecordingBuffer(options = {}) {
|
|
|
65
65
|
frames() {
|
|
66
66
|
return frames;
|
|
67
67
|
},
|
|
68
|
+
snapshot() {
|
|
69
|
+
return frames.map((frame) => ({ t: frame.t, data: frame.data }));
|
|
70
|
+
},
|
|
71
|
+
restore(next) {
|
|
72
|
+
frames.length = 0;
|
|
73
|
+
for (const frame of next)
|
|
74
|
+
frames.push({ t: frame.t, data: frame.data });
|
|
75
|
+
},
|
|
68
76
|
};
|
|
69
77
|
}
|
|
@@ -56,6 +56,8 @@ export interface RoundSnapshot<TPhase extends string = RoundPhase> {
|
|
|
56
56
|
lossStreaks: Record<string, number>;
|
|
57
57
|
roles: Record<string, string | undefined>;
|
|
58
58
|
matchOver: boolean;
|
|
59
|
+
/** Winner of the round being settled, or null; drives `economyFor` until the next round starts. */
|
|
60
|
+
pendingWinner: string | null;
|
|
59
61
|
}
|
|
60
62
|
export interface RoundState<TPhase extends string = RoundPhase> {
|
|
61
63
|
tick(dt: number): RoundEvent<TPhase>[];
|
|
@@ -71,6 +73,8 @@ export interface RoundState<TPhase extends string = RoundPhase> {
|
|
|
71
73
|
/** The role tag configured for `team`, if any. */
|
|
72
74
|
roleOf(team: string): string | undefined;
|
|
73
75
|
snapshot(): RoundSnapshot<TPhase>;
|
|
76
|
+
/** Puts a {@link RoundState.snapshot} back; phase hooks stay registered. */
|
|
77
|
+
restore(next: RoundSnapshot<TPhase>): void;
|
|
74
78
|
}
|
|
75
79
|
/** @internal */
|
|
76
80
|
export declare function lossBonusFor(rule: LossBonusRule | undefined, streak: number): number;
|
|
@@ -104,6 +104,7 @@ export function createRoundState(config) {
|
|
|
104
104
|
lossStreaks: { ...lossStreaks },
|
|
105
105
|
roles: { ...roles },
|
|
106
106
|
matchOver,
|
|
107
|
+
pendingWinner,
|
|
107
108
|
};
|
|
108
109
|
}
|
|
109
110
|
function concludeRoundImpl(winner) {
|
|
@@ -156,5 +157,15 @@ export function createRoundState(config) {
|
|
|
156
157
|
economyFor,
|
|
157
158
|
roleOf: (team) => roles[team],
|
|
158
159
|
snapshot: currentSnapshot,
|
|
160
|
+
restore(next) {
|
|
161
|
+
round = next.round;
|
|
162
|
+
phase = next.phase;
|
|
163
|
+
timeLeft = next.timeLeft;
|
|
164
|
+
matchOver = next.matchOver;
|
|
165
|
+
pendingWinner = next.pendingWinner;
|
|
166
|
+
Object.assign(scores, next.scores);
|
|
167
|
+
Object.assign(lossStreaks, next.lossStreaks);
|
|
168
|
+
Object.assign(roles, next.roles);
|
|
169
|
+
},
|
|
159
170
|
};
|
|
160
171
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type SettingOption, type SettingsStore } from "./settingsModel.js";
|
|
2
|
+
/** Stored value for "render every display refresh" — the browser's vsync-locked rate. */
|
|
3
|
+
export declare const FRAME_RATE_LIMIT_DISPLAY = "display";
|
|
4
|
+
/**
|
|
5
|
+
* Frame-rate limit choices. Browsers always present on the display's vsync, so the uncapped
|
|
6
|
+
* option runs at the monitor's refresh rate; the numeric caps skip refreshes to save GPU and battery.
|
|
7
|
+
*/
|
|
8
|
+
export declare const FRAME_RATE_LIMIT_OPTIONS: readonly SettingOption[];
|
|
9
|
+
/** The player's frame-rate cap in frames per second, or 0 for the display rate. */
|
|
10
|
+
export declare function readFrameRateLimit(store: Pick<SettingsStore, "get">): number;
|
|
11
|
+
/**
|
|
12
|
+
* Frame pacing for a capped render loop driven by display refreshes. Returns the new frame
|
|
13
|
+
* anchor when a frame should render at `now`, or `null` to skip this refresh. The anchor keeps
|
|
14
|
+
* the remainder so a 60 cap on a 144 Hz display averages 60 instead of drifting to 48.
|
|
15
|
+
*/
|
|
16
|
+
export declare function paceFrame(now: number, anchor: number, fps: number): number | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SETTING_IDS } from "./settingsModel.js";
|
|
2
|
+
/** Stored value for "render every display refresh" — the browser's vsync-locked rate. */
|
|
3
|
+
export const FRAME_RATE_LIMIT_DISPLAY = "display";
|
|
4
|
+
/**
|
|
5
|
+
* Frame-rate limit choices. Browsers always present on the display's vsync, so the uncapped
|
|
6
|
+
* option runs at the monitor's refresh rate; the numeric caps skip refreshes to save GPU and battery.
|
|
7
|
+
*/
|
|
8
|
+
export const FRAME_RATE_LIMIT_OPTIONS = [
|
|
9
|
+
{ value: FRAME_RATE_LIMIT_DISPLAY, label: "V-Sync" },
|
|
10
|
+
{ value: "30", label: "30" },
|
|
11
|
+
{ value: "60", label: "60" },
|
|
12
|
+
{ value: "120", label: "120" },
|
|
13
|
+
{ value: "144", label: "144" },
|
|
14
|
+
];
|
|
15
|
+
/** The player's frame-rate cap in frames per second, or 0 for the display rate. */
|
|
16
|
+
export function readFrameRateLimit(store) {
|
|
17
|
+
const raw = store.get(SETTING_IDS.graphicsFrameRateLimit, FRAME_RATE_LIMIT_DISPLAY);
|
|
18
|
+
const fps = Number(raw);
|
|
19
|
+
return Number.isFinite(fps) && fps > 0 ? fps : 0;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Frame pacing for a capped render loop driven by display refreshes. Returns the new frame
|
|
23
|
+
* anchor when a frame should render at `now`, or `null` to skip this refresh. The anchor keeps
|
|
24
|
+
* the remainder so a 60 cap on a 144 Hz display averages 60 instead of drifting to 48.
|
|
25
|
+
*/
|
|
26
|
+
export function paceFrame(now, anchor, fps) {
|
|
27
|
+
if (fps <= 0)
|
|
28
|
+
return now;
|
|
29
|
+
const interval = 1000 / fps;
|
|
30
|
+
const elapsed = now - anchor;
|
|
31
|
+
// Half a millisecond of slack absorbs rAF timestamp jitter so a 60 cap on 60 Hz never skips.
|
|
32
|
+
if (elapsed < interval - 0.5)
|
|
33
|
+
return null;
|
|
34
|
+
if (elapsed > interval * 4)
|
|
35
|
+
return now;
|
|
36
|
+
return anchor + Math.max(1, Math.floor((elapsed + 0.5) / interval)) * interval;
|
|
37
|
+
}
|
|
@@ -6,6 +6,8 @@ export interface GraphicsProfile {
|
|
|
6
6
|
cascades: 1 | 2 | 3 | 4;
|
|
7
7
|
drawDistance: number;
|
|
8
8
|
particleCap: number;
|
|
9
|
+
/** Anisotropic texture filtering samples; clamped to the GPU maximum. 1 is plain trilinear. */
|
|
10
|
+
anisotropy: 1 | 2 | 4 | 8 | 16;
|
|
9
11
|
postStages: {
|
|
10
12
|
ao: boolean;
|
|
11
13
|
bloom: boolean;
|
|
@@ -6,6 +6,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
6
6
|
cascades: 1,
|
|
7
7
|
drawDistance: 120,
|
|
8
8
|
particleCap: 128,
|
|
9
|
+
anisotropy: 2,
|
|
9
10
|
postStages: { ao: false, bloom: true, dof: false, smaa: true },
|
|
10
11
|
},
|
|
11
12
|
medium: {
|
|
@@ -14,6 +15,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
14
15
|
cascades: 2,
|
|
15
16
|
drawDistance: 240,
|
|
16
17
|
particleCap: 256,
|
|
18
|
+
anisotropy: 4,
|
|
17
19
|
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
18
20
|
},
|
|
19
21
|
high: {
|
|
@@ -22,6 +24,16 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
22
24
|
cascades: 4,
|
|
23
25
|
drawDistance: 400,
|
|
24
26
|
particleCap: 512,
|
|
27
|
+
anisotropy: 8,
|
|
28
|
+
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
29
|
+
},
|
|
30
|
+
ultra: {
|
|
31
|
+
renderScale: 2,
|
|
32
|
+
shadowMapSize: 4096,
|
|
33
|
+
cascades: 4,
|
|
34
|
+
drawDistance: 800,
|
|
35
|
+
particleCap: 1024,
|
|
36
|
+
anisotropy: 16,
|
|
25
37
|
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
26
38
|
},
|
|
27
39
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type GraphicsProfile } from "./graphicsProfile.js";
|
|
2
|
+
import { type GraphicsQuality, type SettingsStore } from "./settingsModel.js";
|
|
3
|
+
/** The post-processing stages a player can toggle individually on top of the quality tier. */
|
|
4
|
+
export type GraphicsPostStage = keyof GraphicsProfile["postStages"];
|
|
5
|
+
/** Ordered stage list used to build settings rows. */
|
|
6
|
+
export declare const GRAPHICS_POST_STAGES: readonly GraphicsPostStage[];
|
|
7
|
+
/** Player-facing labels for each post stage. */
|
|
8
|
+
export declare const GRAPHICS_POST_STAGE_LABELS: Record<GraphicsPostStage, string>;
|
|
9
|
+
/** Setting id that stores one stage toggle (`graphics.post.<stage>`). */
|
|
10
|
+
export declare function graphicsPostStageSettingId(stage: GraphicsPostStage): string;
|
|
11
|
+
/** Lowest player render scale; the value is the device-pixel-ratio cap handed to the canvas. */
|
|
12
|
+
export declare const RENDER_SCALE_MIN = 0.5;
|
|
13
|
+
/** Highest player render scale. */
|
|
14
|
+
export declare const RENDER_SCALE_MAX = 2;
|
|
15
|
+
/** Render-scale slider increment. */
|
|
16
|
+
export declare const RENDER_SCALE_STEP = 0.05;
|
|
17
|
+
/** Texture-filtering choices offered to the player; the value is the anisotropy sample count. */
|
|
18
|
+
export declare const ANISOTROPY_OPTIONS: readonly {
|
|
19
|
+
value: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}[];
|
|
22
|
+
/** Per-tier profile overrides a game authors with `defineGame({ graphics })`. */
|
|
23
|
+
export type GraphicsProfileOverrides = Partial<Record<GraphicsQuality, Partial<GraphicsProfile>>>;
|
|
24
|
+
/** The resolved player graphics choice: the tier plus the profile after per-stage and render-scale overrides. */
|
|
25
|
+
export interface GraphicsSettingsState {
|
|
26
|
+
quality: GraphicsQuality;
|
|
27
|
+
profile: GraphicsProfile;
|
|
28
|
+
}
|
|
29
|
+
/** The stored quality tier, falling back to the default when the value is missing or unknown. */
|
|
30
|
+
export declare function readGraphicsQuality(store: Pick<SettingsStore, "get">): GraphicsQuality;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the player's graphics state from a settings store. The tier profile (with the game's
|
|
33
|
+
* overrides) supplies every default; a stored render scale or stage toggle replaces its field.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readGraphicsSettings(store: Pick<SettingsStore, "get">, overrides?: GraphicsProfileOverrides): GraphicsSettingsState;
|
|
36
|
+
/**
|
|
37
|
+
* Select a quality tier and re-apply its render scale, texture filtering and stage defaults, so picking a preset
|
|
38
|
+
* resets the individual overrides instead of leaving stale toggles behind.
|
|
39
|
+
*/
|
|
40
|
+
export declare function applyGraphicsQuality(store: Pick<SettingsStore, "set">, quality: GraphicsQuality, overrides?: GraphicsProfileOverrides): GraphicsProfile;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { resolveGraphicsProfile } from "./graphicsProfile.js";
|
|
2
|
+
import { DEFAULT_GRAPHICS_QUALITY, GRAPHICS_QUALITY_OPTIONS, SETTING_IDS, } from "./settingsModel.js";
|
|
3
|
+
/** Ordered stage list used to build settings rows. */
|
|
4
|
+
export const GRAPHICS_POST_STAGES = ["ao", "bloom", "dof", "smaa"];
|
|
5
|
+
/** Player-facing labels for each post stage. */
|
|
6
|
+
export const GRAPHICS_POST_STAGE_LABELS = {
|
|
7
|
+
ao: "Ambient occlusion",
|
|
8
|
+
bloom: "Bloom",
|
|
9
|
+
dof: "Depth of field",
|
|
10
|
+
smaa: "Anti-aliasing (SMAA)",
|
|
11
|
+
};
|
|
12
|
+
/** Setting id that stores one stage toggle (`graphics.post.<stage>`). */
|
|
13
|
+
export function graphicsPostStageSettingId(stage) {
|
|
14
|
+
return `graphics.post.${stage}`;
|
|
15
|
+
}
|
|
16
|
+
/** Lowest player render scale; the value is the device-pixel-ratio cap handed to the canvas. */
|
|
17
|
+
export const RENDER_SCALE_MIN = 0.5;
|
|
18
|
+
/** Highest player render scale. */
|
|
19
|
+
export const RENDER_SCALE_MAX = 2;
|
|
20
|
+
/** Render-scale slider increment. */
|
|
21
|
+
export const RENDER_SCALE_STEP = 0.05;
|
|
22
|
+
/** Texture-filtering choices offered to the player; the value is the anisotropy sample count. */
|
|
23
|
+
export const ANISOTROPY_OPTIONS = [
|
|
24
|
+
{ value: "1", label: "Off" },
|
|
25
|
+
{ value: "2", label: "2x" },
|
|
26
|
+
{ value: "4", label: "4x" },
|
|
27
|
+
{ value: "8", label: "8x" },
|
|
28
|
+
{ value: "16", label: "16x" },
|
|
29
|
+
];
|
|
30
|
+
const ANISOTROPY_VALUES = [1, 2, 4, 8, 16];
|
|
31
|
+
function toAnisotropy(raw, fallback) {
|
|
32
|
+
const value = Number(raw);
|
|
33
|
+
return ANISOTROPY_VALUES.find((option) => option === value) ?? fallback;
|
|
34
|
+
}
|
|
35
|
+
const QUALITY_VALUES = new Set(GRAPHICS_QUALITY_OPTIONS.map((option) => option.value));
|
|
36
|
+
function isGraphicsQuality(value) {
|
|
37
|
+
return typeof value === "string" && QUALITY_VALUES.has(value);
|
|
38
|
+
}
|
|
39
|
+
function clampRenderScale(value) {
|
|
40
|
+
return Math.min(RENDER_SCALE_MAX, Math.max(RENDER_SCALE_MIN, value));
|
|
41
|
+
}
|
|
42
|
+
/** The stored quality tier, falling back to the default when the value is missing or unknown. */
|
|
43
|
+
export function readGraphicsQuality(store) {
|
|
44
|
+
const raw = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
|
|
45
|
+
return isGraphicsQuality(raw) ? raw : DEFAULT_GRAPHICS_QUALITY;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the player's graphics state from a settings store. The tier profile (with the game's
|
|
49
|
+
* overrides) supplies every default; a stored render scale or stage toggle replaces its field.
|
|
50
|
+
*/
|
|
51
|
+
export function readGraphicsSettings(store, overrides) {
|
|
52
|
+
const quality = readGraphicsQuality(store);
|
|
53
|
+
const tier = resolveGraphicsProfile(quality, overrides?.[quality]);
|
|
54
|
+
const postStages = { ...tier.postStages };
|
|
55
|
+
for (const stage of GRAPHICS_POST_STAGES) {
|
|
56
|
+
postStages[stage] = store.get(graphicsPostStageSettingId(stage), tier.postStages[stage]);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
quality,
|
|
60
|
+
profile: {
|
|
61
|
+
...tier,
|
|
62
|
+
renderScale: clampRenderScale(store.get(SETTING_IDS.graphicsRenderScale, tier.renderScale)),
|
|
63
|
+
anisotropy: toAnisotropy(store.get(SETTING_IDS.graphicsAnisotropy, String(tier.anisotropy)), tier.anisotropy),
|
|
64
|
+
postStages,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Select a quality tier and re-apply its render scale, texture filtering and stage defaults, so picking a preset
|
|
70
|
+
* resets the individual overrides instead of leaving stale toggles behind.
|
|
71
|
+
*/
|
|
72
|
+
export function applyGraphicsQuality(store, quality, overrides) {
|
|
73
|
+
const tier = resolveGraphicsProfile(quality, overrides?.[quality]);
|
|
74
|
+
store.set(SETTING_IDS.graphicsQuality, quality);
|
|
75
|
+
store.set(SETTING_IDS.graphicsRenderScale, tier.renderScale);
|
|
76
|
+
store.set(SETTING_IDS.graphicsAnisotropy, String(tier.anisotropy));
|
|
77
|
+
for (const stage of GRAPHICS_POST_STAGES) {
|
|
78
|
+
store.set(graphicsPostStageSettingId(stage), tier.postStages[stage]);
|
|
79
|
+
}
|
|
80
|
+
return tier;
|
|
81
|
+
}
|
|
@@ -67,10 +67,18 @@ export declare const SETTING_IDS: {
|
|
|
67
67
|
readonly graphicsQuality: "graphics.quality";
|
|
68
68
|
readonly graphicsShadows: "graphics.shadows";
|
|
69
69
|
readonly graphicsUiScale: "graphics.uiScale";
|
|
70
|
+
readonly graphicsRenderScale: "graphics.renderScale";
|
|
71
|
+
readonly graphicsPostAo: "graphics.post.ao";
|
|
72
|
+
readonly graphicsPostBloom: "graphics.post.bloom";
|
|
73
|
+
readonly graphicsPostDof: "graphics.post.dof";
|
|
74
|
+
readonly graphicsPostSmaa: "graphics.post.smaa";
|
|
75
|
+
readonly graphicsFrameRateLimit: "graphics.frameRateLimit";
|
|
76
|
+
readonly graphicsAnisotropy: "graphics.anisotropy";
|
|
77
|
+
readonly graphicsShowFps: "graphics.showFps";
|
|
70
78
|
readonly touchStyle: "controls.touchStyle";
|
|
71
79
|
readonly touchJoystick: "controls.touchJoystick";
|
|
72
80
|
};
|
|
73
|
-
export type GraphicsQuality = "low" | "medium" | "high";
|
|
81
|
+
export type GraphicsQuality = "low" | "medium" | "high" | "ultra";
|
|
74
82
|
export declare const GRAPHICS_QUALITY_OPTIONS: readonly SettingOption[];
|
|
75
83
|
export declare const DEFAULT_MASTER_VOLUME = 1;
|
|
76
84
|
export declare const DEFAULT_GRAPHICS_QUALITY: GraphicsQuality;
|
|
@@ -11,6 +11,14 @@ export const SETTING_IDS = {
|
|
|
11
11
|
graphicsQuality: "graphics.quality",
|
|
12
12
|
graphicsShadows: "graphics.shadows",
|
|
13
13
|
graphicsUiScale: "graphics.uiScale",
|
|
14
|
+
graphicsRenderScale: "graphics.renderScale",
|
|
15
|
+
graphicsPostAo: "graphics.post.ao",
|
|
16
|
+
graphicsPostBloom: "graphics.post.bloom",
|
|
17
|
+
graphicsPostDof: "graphics.post.dof",
|
|
18
|
+
graphicsPostSmaa: "graphics.post.smaa",
|
|
19
|
+
graphicsFrameRateLimit: "graphics.frameRateLimit",
|
|
20
|
+
graphicsAnisotropy: "graphics.anisotropy",
|
|
21
|
+
graphicsShowFps: "graphics.showFps",
|
|
14
22
|
touchStyle: "controls.touchStyle",
|
|
15
23
|
touchJoystick: "controls.touchJoystick",
|
|
16
24
|
};
|
|
@@ -18,6 +26,7 @@ export const GRAPHICS_QUALITY_OPTIONS = [
|
|
|
18
26
|
{ value: "low", label: "Low" },
|
|
19
27
|
{ value: "medium", label: "Medium" },
|
|
20
28
|
{ value: "high", label: "High" },
|
|
29
|
+
{ value: "ultra", label: "Ultra" },
|
|
21
30
|
];
|
|
22
31
|
export const DEFAULT_MASTER_VOLUME = 1;
|
|
23
32
|
export const DEFAULT_GRAPHICS_QUALITY = "high";
|
|
@@ -31,6 +40,7 @@ export const GRAPHICS_QUALITY_DPR = {
|
|
|
31
40
|
low: 1,
|
|
32
41
|
medium: 1.5,
|
|
33
42
|
high: 2,
|
|
43
|
+
ultra: 2,
|
|
34
44
|
};
|
|
35
45
|
export function busVolumeSettingId(busId) {
|
|
36
46
|
return `sound.bus.${busId}`;
|
|
@@ -34,6 +34,6 @@ export interface EventMeter {
|
|
|
34
34
|
/**
|
|
35
35
|
* A heat/hype gauge that rises as tagged events land and cools between them, firing when it fills or breaks — the streak/overdrive meter shooters and fighters hand-roll.
|
|
36
36
|
*
|
|
37
|
-
* @capability event-meter a heat/streak gauge that builds from repeated
|
|
37
|
+
* @capability event-meter a heat/streak/alert gauge (wanted level, rage, combo) that builds from repeated events and cools down over time
|
|
38
38
|
*/
|
|
39
39
|
export declare function createEventMeter(config: EventMeterConfig): EventMeter;
|
package/dist/stats/eventMeter.js
CHANGED
|
@@ -5,7 +5,7 @@ function readyOf(meter, mode) {
|
|
|
5
5
|
/**
|
|
6
6
|
* A heat/hype gauge that rises as tagged events land and cools between them, firing when it fills or breaks — the streak/overdrive meter shooters and fighters hand-roll.
|
|
7
7
|
*
|
|
8
|
-
* @capability event-meter a heat/streak gauge that builds from repeated
|
|
8
|
+
* @capability event-meter a heat/streak/alert gauge (wanted level, rage, combo) that builds from repeated events and cools down over time
|
|
9
9
|
*/
|
|
10
10
|
export function createEventMeter(config) {
|
|
11
11
|
const mode = config.mode ?? "hold";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Piecewise-linear map from a signal value to an output value; points in ascending input order, ends clamp. */
|
|
2
|
+
export type FeedbackCurve = readonly (readonly [number, number])[];
|
|
3
|
+
/** One wire from a sim signal to a named output. */
|
|
4
|
+
export interface FeedbackRoute<TSignal extends string, TTarget extends string> {
|
|
5
|
+
signal: TSignal;
|
|
6
|
+
target: TTarget;
|
|
7
|
+
/** Signal → output mapping (default identity). */
|
|
8
|
+
curve?: FeedbackCurve;
|
|
9
|
+
/** Units/s the routed value may rise (default `Infinity`: follows instantly). */
|
|
10
|
+
attack?: number;
|
|
11
|
+
/** Units/s the routed value may fall (default `attack`). */
|
|
12
|
+
release?: number;
|
|
13
|
+
}
|
|
14
|
+
/** A one-shot fired when a signal crosses a threshold (landing thud, gear-change clunk, impact rumble). */
|
|
15
|
+
export interface FeedbackEvent<TSignal extends string> {
|
|
16
|
+
id: string;
|
|
17
|
+
signal: TSignal;
|
|
18
|
+
/** Fires when the signal rises to or above this value from below it. */
|
|
19
|
+
threshold: number;
|
|
20
|
+
/** Seconds before the same event may fire again (default `0`). */
|
|
21
|
+
cooldown?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Config for {@link createFeedbackMixer}. */
|
|
24
|
+
export interface FeedbackMixerConfig<TSignal extends string, TTarget extends string> {
|
|
25
|
+
routes: readonly FeedbackRoute<TSignal, TTarget>[];
|
|
26
|
+
/** How routes landing on the same target combine (default `"sum"`). */
|
|
27
|
+
combine?: Partial<Record<TTarget, "sum" | "max">>;
|
|
28
|
+
/** Value a target starts from before routes add to it (default `0`). */
|
|
29
|
+
base?: Partial<Record<TTarget, number>>;
|
|
30
|
+
events?: readonly FeedbackEvent<TSignal>[];
|
|
31
|
+
}
|
|
32
|
+
/** Serializable mixer state: smoothed route values, last signal per event, and event cooldowns. */
|
|
33
|
+
export interface FeedbackMixerState {
|
|
34
|
+
routes: number[];
|
|
35
|
+
eventSignals: number[];
|
|
36
|
+
cooldowns: number[];
|
|
37
|
+
}
|
|
38
|
+
/** Maps sim telemetry to presentation parameters through declared routes, curves and smoothing. */
|
|
39
|
+
export interface FeedbackMixer<TSignal extends string, TTarget extends string> {
|
|
40
|
+
/**
|
|
41
|
+
* Advance by `dt` with this tick's signals and return every target's value. The returned record is reused
|
|
42
|
+
* between calls, so read it before the next update. Missing signals read as `0`.
|
|
43
|
+
*/
|
|
44
|
+
update(dt: number, signals: Readonly<Partial<Record<TSignal, number>>>): Readonly<Record<TTarget, number>>;
|
|
45
|
+
/** True when event `id` fired on the last update. */
|
|
46
|
+
fired(id: string): boolean;
|
|
47
|
+
value(): Readonly<Record<TTarget, number>>;
|
|
48
|
+
retune(next: FeedbackMixerConfig<TSignal, TTarget>): void;
|
|
49
|
+
snapshot(): FeedbackMixerState;
|
|
50
|
+
restore(state: FeedbackMixerState): void;
|
|
51
|
+
reset(): void;
|
|
52
|
+
}
|
|
53
|
+
/** Sample a {@link FeedbackCurve}; an empty curve is the identity. */
|
|
54
|
+
export declare function sampleFeedbackCurve(curve: FeedbackCurve | undefined, x: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Creates a {@link FeedbackMixer}: the one place a game declares how sim telemetry drives presentation —
|
|
57
|
+
* rpm to engine pitch, tire saturation to squeal and rumble, speed to FOV, landing speed to a thud — instead
|
|
58
|
+
* of hand-writing that glue per vehicle, aircraft or weapon. Deterministic, allocation-free per update.
|
|
59
|
+
* @capability feedback-mixer map sim telemetry to camera, audio and haptic parameters through curves, smoothing and threshold events
|
|
60
|
+
*/
|
|
61
|
+
export declare function createFeedbackMixer<TSignal extends string, TTarget extends string>(initial: FeedbackMixerConfig<TSignal, TTarget>): FeedbackMixer<TSignal, TTarget>;
|