@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ProximityPrompt } from "../interaction/proximityPrompt.js";
|
|
2
2
|
import type { PathProgress, Waypoint } from "../nav/pathFollow.js";
|
|
3
|
+
import type { Blackboard, DecisionGraph } from "../ai/decisionGraph.js";
|
|
3
4
|
export interface WanderBehavior {
|
|
4
5
|
kind: "wander";
|
|
5
6
|
radius: number;
|
|
@@ -27,7 +28,17 @@ export interface PromptableBehavior {
|
|
|
27
28
|
export interface PlayerBehavior {
|
|
28
29
|
kind: "player";
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
/** Decision graph behavior descriptor using a named registered action set. */
|
|
32
|
+
export interface DecisionGraphBehavior {
|
|
33
|
+
kind: "decisionGraph";
|
|
34
|
+
graph: DecisionGraph;
|
|
35
|
+
actions: string;
|
|
36
|
+
/** Initial facts for this entity's persistent blackboard; systems write more through `behaviorControl(ctx).blackboard(id)`. */
|
|
37
|
+
blackboard?: Blackboard;
|
|
38
|
+
/** Seconds between graph ticks (`0` = every frame). Siblings are staggered; actions receive the elapsed time. */
|
|
39
|
+
thinkInterval?: number;
|
|
40
|
+
}
|
|
41
|
+
export type BehaviorDescriptor = WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior | DecisionGraphBehavior;
|
|
31
42
|
export declare function wander({ radius }: {
|
|
32
43
|
radius: number;
|
|
33
44
|
}): WanderBehavior;
|
|
@@ -37,6 +37,15 @@ export const SCATTER_SCHEMA = {
|
|
|
37
37
|
{ type: "seed", key: "seed", label: "seed", default: "" },
|
|
38
38
|
],
|
|
39
39
|
};
|
|
40
|
+
const LIGHT_SCHEMA = {
|
|
41
|
+
fields: [
|
|
42
|
+
{ type: "select", key: "type", label: "type", options: [{ value: "point", label: "Point" }, { value: "spot", label: "Spot" }], default: "point" },
|
|
43
|
+
{ type: "color", key: "color", label: "color", default: "#ffd6a0" },
|
|
44
|
+
{ type: "number", key: "intensity", label: "intensity", min: 0, step: 0.1, default: 2 },
|
|
45
|
+
{ type: "number", key: "range", label: "range", min: 0, step: 0.5, default: 12 },
|
|
46
|
+
{ type: "bool", key: "castShadow", label: "cast shadow", default: false },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
40
49
|
let registered = false;
|
|
41
50
|
/** Idempotently registers the engine's built-in environment scene kinds + generators. @internal */
|
|
42
51
|
export function registerBuiltinSceneKinds() {
|
|
@@ -65,6 +74,7 @@ export function registerBuiltinSceneKinds() {
|
|
|
65
74
|
return describeScatterCoverage(densityCoverage("scatter", area, density));
|
|
66
75
|
},
|
|
67
76
|
});
|
|
77
|
+
registerSceneKind({ kind: "light", target: "marker", label: "Light", addCategory: "Environment", accent: "#fbbf24", schema: LIGHT_SCHEMA });
|
|
68
78
|
registerWaterKind();
|
|
69
79
|
registerGrassKind();
|
|
70
80
|
registerSoilKind();
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* party ship a new authorable "studio" (pole line, water, bookcase) — schema + resolver here, a
|
|
6
6
|
* matching renderer in `shell` — without editing editor or engine files. Scatter is the proof adopter.
|
|
7
7
|
*/
|
|
8
|
+
import type { WorldSolid } from "../world/worldSolids.js";
|
|
8
9
|
/** Which document collection a scene kind lives in. Drives placement + which `meta` bag holds params. */
|
|
9
10
|
export type SceneKindTarget = "path" | "marker" | "volume";
|
|
10
11
|
/** A closed-polygon area vs an open polyline, for path-target kinds. Ignored for marker/volume kinds. */
|
|
@@ -212,6 +213,12 @@ export interface SceneKindDefinition<TResolved = unknown> {
|
|
|
212
213
|
* marker/volume/path; `params` is `parseParams(schema, object.meta)`.
|
|
213
214
|
*/
|
|
214
215
|
resolve?: (object: SceneKindObject, params: ParsedParams, context: SceneKindResolveContext) => TResolved;
|
|
216
|
+
/**
|
|
217
|
+
* World-space collision for a resolved object. Omitted, the kind is render-only. `syncAuthoredSolids`
|
|
218
|
+
* (`world/authoredSolids`) writes these into `ctx.world.solids`, so movement, NPCs, physics
|
|
219
|
+
* backends and nav grids all collide with what the studio draws.
|
|
220
|
+
*/
|
|
221
|
+
solids?: (resolved: TResolved, object: SceneKindObject, params: ParsedParams, context: SceneKindResolveContext) => readonly WorldSolid[];
|
|
215
222
|
/** Optional one-line readout under the inspector fields (estimate, dimensions) — owned by the studio. */
|
|
216
223
|
note?: (object: SceneKindObject, params: ParsedParams) => string;
|
|
217
224
|
/**
|
|
@@ -258,6 +265,8 @@ export interface SceneKindObject {
|
|
|
258
265
|
export interface SceneKindResolveContext {
|
|
259
266
|
sampleHeight?: (x: number, z: number) => number;
|
|
260
267
|
sampleNormal?: (x: number, z: number) => readonly [number, number, number];
|
|
268
|
+
/** Every studio object in the same document, for kinds that read siblings (a `city` reads its `cityzone` volumes). */
|
|
269
|
+
objects?: readonly SceneKindObject[];
|
|
261
270
|
}
|
|
262
271
|
/**
|
|
263
272
|
* Register a scene kind — the plug-in point for a new parametric studio. Idempotent per `kind` (last
|
|
@@ -64,5 +64,9 @@ export declare class VehicleSeats {
|
|
|
64
64
|
driverOf(vehicleId: string): string | null;
|
|
65
65
|
get mounts(): MountController;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Builds a {@link VehicleSeats}, optionally over an existing `MountController` to share its occupancy.
|
|
69
|
+
*
|
|
70
|
+
* @capability vehicle-seats enter, exit and swap seats in a car or mount (GTA-style boarding) with driver and passenger slots
|
|
71
|
+
*/
|
|
68
72
|
export declare function createVehicleSeats(controller?: MountController): VehicleSeats;
|
|
@@ -53,7 +53,11 @@ export class VehicleSeats {
|
|
|
53
53
|
return this.controller;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Builds a {@link VehicleSeats}, optionally over an existing `MountController` to share its occupancy.
|
|
58
|
+
*
|
|
59
|
+
* @capability vehicle-seats enter, exit and swap seats in a car or mount (GTA-style boarding) with driver and passenger slots
|
|
60
|
+
*/
|
|
57
61
|
export function createVehicleSeats(controller) {
|
|
58
62
|
return new VehicleSeats(controller);
|
|
59
63
|
}
|
|
@@ -14,9 +14,15 @@ export interface ConcealmentSample {
|
|
|
14
14
|
concealed: boolean;
|
|
15
15
|
dwellSeconds: number;
|
|
16
16
|
}
|
|
17
|
+
/** Plain JSON state of a {@link ConcealmentSensor}: seconds each target has stayed concealed. */
|
|
18
|
+
export interface ConcealmentSensorState {
|
|
19
|
+
dwell: Record<string, number>;
|
|
20
|
+
}
|
|
17
21
|
export interface ConcealmentSensor {
|
|
18
22
|
tick(targets: readonly ConcealmentTarget[], dt: number): ConcealmentSample[];
|
|
19
23
|
reset(id?: string): void;
|
|
24
|
+
snapshot(): ConcealmentSensorState;
|
|
25
|
+
restore(next: ConcealmentSensorState): void;
|
|
20
26
|
}
|
|
21
27
|
/** @internal */
|
|
22
28
|
export declare function createConcealmentSensor(config?: {
|
|
@@ -62,5 +62,13 @@ export function createConcealmentSensor(config) {
|
|
|
62
62
|
else
|
|
63
63
|
dwell.delete(id);
|
|
64
64
|
},
|
|
65
|
+
snapshot() {
|
|
66
|
+
return { dwell: Object.fromEntries(dwell) };
|
|
67
|
+
},
|
|
68
|
+
restore(next) {
|
|
69
|
+
dwell.clear();
|
|
70
|
+
for (const [id, seconds] of Object.entries(next.dwell))
|
|
71
|
+
dwell.set(id, seconds);
|
|
72
|
+
},
|
|
65
73
|
};
|
|
66
74
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** A world position used by perception, in `[x, y, z]` order. */
|
|
2
|
+
export type PerceptionPosition = readonly [number, number, number];
|
|
3
|
+
/** The kinds of stimulus the perception service can retain. */
|
|
4
|
+
export type PerceptionStimulusKind = "sight" | "sound" | "damage";
|
|
5
|
+
/** An event that can seed an observer's memory. */
|
|
6
|
+
export interface PerceptionStimulus {
|
|
7
|
+
kind: PerceptionStimulusKind;
|
|
8
|
+
sourceId: string;
|
|
9
|
+
position: PerceptionPosition;
|
|
10
|
+
/** Multiplies `hearingRange` for this sound: a footstep might be `0.3`, a gunshot `4`. Default `1`. */
|
|
11
|
+
loudness?: number;
|
|
12
|
+
at: number;
|
|
13
|
+
}
|
|
14
|
+
/** The observer pose used for a sight check. */
|
|
15
|
+
export interface PerceptionObserver {
|
|
16
|
+
id: string;
|
|
17
|
+
position: PerceptionPosition;
|
|
18
|
+
yaw: number;
|
|
19
|
+
}
|
|
20
|
+
/** A candidate target considered by a sight check. */
|
|
21
|
+
export interface PerceptionCandidate {
|
|
22
|
+
id: string;
|
|
23
|
+
position: PerceptionPosition;
|
|
24
|
+
}
|
|
25
|
+
/** A remembered target and its linearly decaying confidence. */
|
|
26
|
+
export interface PerceptionMemory {
|
|
27
|
+
targetId: string;
|
|
28
|
+
lastSeenAt: number;
|
|
29
|
+
lastKnownPos: PerceptionPosition;
|
|
30
|
+
confidence: number;
|
|
31
|
+
}
|
|
32
|
+
/** Configuration for sight, hearing, memory decay, and optional occlusion. */
|
|
33
|
+
export interface PerceptionConfig {
|
|
34
|
+
sightRange: number;
|
|
35
|
+
sightConeDeg: number;
|
|
36
|
+
hearingRange: number;
|
|
37
|
+
memorySeconds: number;
|
|
38
|
+
/** Most stimuli retained at once; the oldest are dropped first. Default 256. */
|
|
39
|
+
maxStimuli?: number;
|
|
40
|
+
occluded?: (from: PerceptionPosition, to: PerceptionPosition) => boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Serializable state for a perception service. */
|
|
43
|
+
export interface PerceptionSnapshot {
|
|
44
|
+
memories: Array<{
|
|
45
|
+
observerId: string;
|
|
46
|
+
memory: PerceptionMemory;
|
|
47
|
+
}>;
|
|
48
|
+
stimuli: Array<PerceptionStimulus & {
|
|
49
|
+
seq?: number;
|
|
50
|
+
}>;
|
|
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;
|
|
55
|
+
}
|
|
56
|
+
/** Stateful perception API with retuning and save/restore support. */
|
|
57
|
+
export interface PerceptionService {
|
|
58
|
+
pushStimulus(stimulus: PerceptionStimulus): void;
|
|
59
|
+
observe(observer: PerceptionObserver, candidates: readonly PerceptionCandidate[], nowMs: number): void;
|
|
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;
|
|
63
|
+
retune(patch: Partial<PerceptionConfig>): void;
|
|
64
|
+
snapshot(): PerceptionSnapshot;
|
|
65
|
+
restore(snapshot: PerceptionSnapshot): void;
|
|
66
|
+
}
|
|
67
|
+
/** Creates a deterministic sight, sound, and damage perception service with expiring target memory.
|
|
68
|
+
* @capability perception-service retain sight, sound, and damage observations with range, cone, occlusion, and decay policy
|
|
69
|
+
*/
|
|
70
|
+
export declare function createPerception(initial: PerceptionConfig): PerceptionService;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
function distance(a, b) {
|
|
2
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
3
|
+
}
|
|
4
|
+
function confidenceAt(memory, nowMs, memoryMs) {
|
|
5
|
+
return memory.confidence * Math.max(0, 1 - Math.max(0, nowMs - memory.lastSeenAt) / memoryMs);
|
|
6
|
+
}
|
|
7
|
+
function inSight(observer, target, config) {
|
|
8
|
+
const dx = target[0] - observer.position[0];
|
|
9
|
+
const dz = target[2] - observer.position[2];
|
|
10
|
+
const range = Math.hypot(dx, dz);
|
|
11
|
+
if (range > config.sightRange)
|
|
12
|
+
return false;
|
|
13
|
+
if (range < 1e-9)
|
|
14
|
+
return true;
|
|
15
|
+
let delta = Math.atan2(dx, dz) - observer.yaw;
|
|
16
|
+
while (delta > Math.PI)
|
|
17
|
+
delta -= Math.PI * 2;
|
|
18
|
+
while (delta < -Math.PI)
|
|
19
|
+
delta += Math.PI * 2;
|
|
20
|
+
return Math.abs(delta) <= (config.sightConeDeg * Math.PI) / 360;
|
|
21
|
+
}
|
|
22
|
+
function validate(config) {
|
|
23
|
+
if (!Number.isFinite(config.sightRange) || config.sightRange < 0)
|
|
24
|
+
throw new Error("sightRange must be non-negative.");
|
|
25
|
+
if (!Number.isFinite(config.sightConeDeg) || config.sightConeDeg < 0 || config.sightConeDeg > 360)
|
|
26
|
+
throw new Error("sightConeDeg must be between 0 and 360.");
|
|
27
|
+
if (!Number.isFinite(config.hearingRange) || config.hearingRange < 0)
|
|
28
|
+
throw new Error("hearingRange must be non-negative.");
|
|
29
|
+
if (!Number.isFinite(config.memorySeconds) || config.memorySeconds <= 0)
|
|
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.");
|
|
33
|
+
}
|
|
34
|
+
const DEFAULT_MAX_STIMULI = 256;
|
|
35
|
+
/** Creates a deterministic sight, sound, and damage perception service with expiring target memory.
|
|
36
|
+
* @capability perception-service retain sight, sound, and damage observations with range, cone, occlusion, and decay policy
|
|
37
|
+
*/
|
|
38
|
+
export function createPerception(initial) {
|
|
39
|
+
validate(initial);
|
|
40
|
+
let config = { ...initial };
|
|
41
|
+
let nowMs = 0;
|
|
42
|
+
const stimuli = [];
|
|
43
|
+
const heard = new Map();
|
|
44
|
+
let nextSeq = 1;
|
|
45
|
+
const memories = new Map();
|
|
46
|
+
function remember(observerId, targetId, position, at, confidence) {
|
|
47
|
+
if (confidence <= 0)
|
|
48
|
+
return;
|
|
49
|
+
let observerMemories = memories.get(observerId);
|
|
50
|
+
if (observerMemories === undefined) {
|
|
51
|
+
observerMemories = new Map();
|
|
52
|
+
memories.set(observerId, observerMemories);
|
|
53
|
+
}
|
|
54
|
+
const previous = observerMemories.get(targetId);
|
|
55
|
+
if (previous === undefined || at >= previous.lastSeenAt) {
|
|
56
|
+
observerMemories.set(targetId, { targetId, lastSeenAt: at, lastKnownPos: [...position], confidence });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function prune() {
|
|
60
|
+
const memoryMs = config.memorySeconds * 1000;
|
|
61
|
+
for (const [observerId, observerMemories] of memories) {
|
|
62
|
+
for (const [targetId, memory] of observerMemories) {
|
|
63
|
+
if (confidenceAt(memory, nowMs, memoryMs) <= 0)
|
|
64
|
+
observerMemories.delete(targetId);
|
|
65
|
+
}
|
|
66
|
+
if (observerMemories.size === 0)
|
|
67
|
+
memories.delete(observerId);
|
|
68
|
+
}
|
|
69
|
+
const cutoff = nowMs - memoryMs;
|
|
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);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
pushStimulus(stimulus) {
|
|
78
|
+
if (!Number.isFinite(stimulus.at))
|
|
79
|
+
throw new Error("stimulus.at must be finite.");
|
|
80
|
+
if (stimulus.kind === "sound" && stimulus.loudness !== undefined && (!Number.isFinite(stimulus.loudness) || stimulus.loudness < 0)) {
|
|
81
|
+
throw new Error("stimulus.loudness must be non-negative.");
|
|
82
|
+
}
|
|
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++ });
|
|
87
|
+
nowMs = Math.max(nowMs, stimulus.at);
|
|
88
|
+
prune();
|
|
89
|
+
},
|
|
90
|
+
observe(observer, candidates, observedAt) {
|
|
91
|
+
if (!Number.isFinite(observedAt))
|
|
92
|
+
throw new Error("nowMs must be finite.");
|
|
93
|
+
nowMs = observedAt;
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
if (!inSight(observer, candidate.position, config))
|
|
96
|
+
continue;
|
|
97
|
+
if (config.occluded?.(observer.position, candidate.position))
|
|
98
|
+
continue;
|
|
99
|
+
remember(observer.id, candidate.id, candidate.position, nowMs, 1);
|
|
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;
|
|
106
|
+
for (const stimulus of stimuli) {
|
|
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)
|
|
115
|
+
continue;
|
|
116
|
+
const range = distance(observer.position, stimulus.position);
|
|
117
|
+
if (stimulus.kind === "sound") {
|
|
118
|
+
const audible = config.hearingRange * (stimulus.loudness ?? 1);
|
|
119
|
+
if (range >= audible)
|
|
120
|
+
continue;
|
|
121
|
+
remember(observer.id, stimulus.sourceId, stimulus.position, stimulus.at, 1 - range / audible);
|
|
122
|
+
}
|
|
123
|
+
else if (stimulus.kind === "damage") {
|
|
124
|
+
remember(observer.id, stimulus.sourceId, stimulus.position, stimulus.at, 1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
heard.set(observer.id, Math.min(judged, firstFuture - 1));
|
|
128
|
+
prune();
|
|
129
|
+
},
|
|
130
|
+
memory(observerId) {
|
|
131
|
+
const observerMemories = memories.get(observerId);
|
|
132
|
+
if (observerMemories === undefined)
|
|
133
|
+
return [];
|
|
134
|
+
const memoryMs = config.memorySeconds * 1000;
|
|
135
|
+
const result = [];
|
|
136
|
+
for (const memory of observerMemories.values()) {
|
|
137
|
+
const confidence = confidenceAt(memory, nowMs, memoryMs);
|
|
138
|
+
if (confidence > 0)
|
|
139
|
+
result.push({ ...memory, lastKnownPos: [...memory.lastKnownPos], confidence });
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
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
|
+
},
|
|
153
|
+
retune(patch) {
|
|
154
|
+
config = { ...config, ...patch };
|
|
155
|
+
validate(config);
|
|
156
|
+
prune();
|
|
157
|
+
},
|
|
158
|
+
snapshot() {
|
|
159
|
+
const result = {
|
|
160
|
+
memories: [],
|
|
161
|
+
stimuli: stimuli.map((stimulus) => ({ ...stimulus, position: [...stimulus.position] })),
|
|
162
|
+
nowMs,
|
|
163
|
+
heard: Object.fromEntries(heard),
|
|
164
|
+
nextSeq,
|
|
165
|
+
};
|
|
166
|
+
for (const [observerId, observerMemories] of memories)
|
|
167
|
+
for (const memory of observerMemories.values())
|
|
168
|
+
result.memories.push({ observerId, memory: { ...memory, lastKnownPos: [...memory.lastKnownPos] } });
|
|
169
|
+
return result;
|
|
170
|
+
},
|
|
171
|
+
restore(snapshot) {
|
|
172
|
+
memories.clear();
|
|
173
|
+
stimuli.length = 0;
|
|
174
|
+
nowMs = snapshot.nowMs;
|
|
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);
|
|
184
|
+
for (const entry of snapshot.memories)
|
|
185
|
+
remember(entry.observerId, entry.memory.targetId, entry.memory.lastKnownPos, entry.memory.lastSeenAt, entry.memory.confidence);
|
|
186
|
+
prune();
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { GraphicsQuality } from "./settingsModel.js";
|
|
2
|
+
/** Serializable renderer budget selected by the player's graphics tier. */
|
|
3
|
+
export interface GraphicsProfile {
|
|
4
|
+
renderScale: number;
|
|
5
|
+
shadowMapSize: 512 | 1024 | 2048 | 4096;
|
|
6
|
+
cascades: 1 | 2 | 3 | 4;
|
|
7
|
+
drawDistance: number;
|
|
8
|
+
particleCap: number;
|
|
9
|
+
/** Anisotropic texture filtering samples; clamped to the GPU maximum. 1 is plain trilinear. */
|
|
10
|
+
anisotropy: 1 | 2 | 4 | 8 | 16;
|
|
11
|
+
postStages: {
|
|
12
|
+
ao: boolean;
|
|
13
|
+
bloom: boolean;
|
|
14
|
+
dof: boolean;
|
|
15
|
+
smaa: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Conservative defaults that preserve the existing high-quality shell behavior. */
|
|
19
|
+
export declare const DEFAULT_GRAPHICS_PROFILES: Record<GraphicsQuality, GraphicsProfile>;
|
|
20
|
+
/** Resolve one tier and apply game-authored field and post-stage overrides. */
|
|
21
|
+
export declare function resolveGraphicsProfile(quality: GraphicsQuality, overrides?: Partial<GraphicsProfile>): GraphicsProfile;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Conservative defaults that preserve the existing high-quality shell behavior. */
|
|
2
|
+
export const DEFAULT_GRAPHICS_PROFILES = {
|
|
3
|
+
low: {
|
|
4
|
+
renderScale: 1,
|
|
5
|
+
shadowMapSize: 512,
|
|
6
|
+
cascades: 1,
|
|
7
|
+
drawDistance: 120,
|
|
8
|
+
particleCap: 128,
|
|
9
|
+
anisotropy: 2,
|
|
10
|
+
postStages: { ao: false, bloom: true, dof: false, smaa: true },
|
|
11
|
+
},
|
|
12
|
+
medium: {
|
|
13
|
+
renderScale: 1.5,
|
|
14
|
+
shadowMapSize: 1024,
|
|
15
|
+
cascades: 2,
|
|
16
|
+
drawDistance: 240,
|
|
17
|
+
particleCap: 256,
|
|
18
|
+
anisotropy: 4,
|
|
19
|
+
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
20
|
+
},
|
|
21
|
+
high: {
|
|
22
|
+
renderScale: 2,
|
|
23
|
+
shadowMapSize: 2048,
|
|
24
|
+
cascades: 4,
|
|
25
|
+
drawDistance: 400,
|
|
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,
|
|
37
|
+
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/** Resolve one tier and apply game-authored field and post-stage overrides. */
|
|
41
|
+
export function resolveGraphicsProfile(quality, overrides) {
|
|
42
|
+
const base = DEFAULT_GRAPHICS_PROFILES[quality];
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
...overrides,
|
|
46
|
+
postStages: { ...base.postStages, ...overrides?.postStages },
|
|
47
|
+
};
|
|
48
|
+
}
|