@jgengine/core 0.18.1 → 0.21.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 +504 -7
- package/dist/ai/decisionGraph.d.ts +50 -4
- package/dist/ai/decisionGraph.js +159 -23
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- 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 +43 -1
- package/dist/anim/animGraph.js +326 -10
- 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 +18 -1
- package/dist/anim/locomotionGraph.js +42 -3
- package/dist/audio/audioFalloff.d.ts +6 -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/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +20 -0
- package/dist/combat/abilityKit.js +41 -4
- package/dist/combat/death.d.ts +11 -3
- package/dist/combat/death.js +3 -3
- package/dist/combat/magazine.d.ts +9 -0
- package/dist/combat/magazine.js +24 -2
- package/dist/combat/projectiles.d.ts +94 -2
- package/dist/combat/projectiles.js +282 -61
- package/dist/combat/regenShield.d.ts +14 -0
- package/dist/combat/regenShield.js +23 -7
- 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/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/economy/auctionBook.d.ts +1 -1
- package/dist/economy/auctionBook.js +11 -1
- package/dist/economy/listingBook.d.ts +1 -1
- package/dist/economy/listingBook.js +11 -1
- package/dist/economy/resourceLedger.js +3 -3
- package/dist/economy/shopStock.js +7 -6
- package/dist/economy/techTree.js +7 -6
- package/dist/economy/unlockPoints.js +3 -6
- package/dist/editor/commandApply.js +89 -16
- package/dist/editor/commands.d.ts +38 -1
- package/dist/editor/commands.js +320 -3
- package/dist/editor/creatorStorage.d.ts +59 -0
- package/dist/editor/creatorStorage.js +164 -0
- package/dist/editor/document.d.ts +4 -0
- package/dist/editor/document.js +119 -18
- package/dist/editor/environment.js +8 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/liveSync.d.ts +4 -1
- package/dist/editor/liveSync.js +26 -10
- package/dist/editor/materialAuthoring.d.ts +14 -0
- package/dist/editor/materialAuthoring.js +38 -0
- package/dist/editor/modelNodeVisibility.d.ts +11 -0
- package/dist/editor/modelNodeVisibility.js +29 -0
- package/dist/editor/movementCatalog.d.ts +90 -0
- package/dist/editor/movementCatalog.js +144 -0
- package/dist/editor/simulation.d.ts +60 -0
- package/dist/editor/simulation.js +202 -0
- package/dist/editor/staticPrefab.d.ts +17 -0
- package/dist/editor/staticPrefab.js +51 -0
- package/dist/editor/types.d.ts +15 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +54 -2
- package/dist/game/clipRoles.d.ts +2 -1
- package/dist/game/clipRoles.js +7 -3
- package/dist/game/contentValidation.d.ts +69 -0
- package/dist/game/contentValidation.js +118 -0
- package/dist/game/controlGate.d.ts +8 -0
- package/dist/game/controlGate.js +43 -6
- package/dist/game/defineGame.d.ts +32 -3
- package/dist/game/dialogueGraph.d.ts +20 -0
- package/dist/game/dialogueGraph.js +107 -0
- package/dist/game/events.d.ts +11 -0
- package/dist/game/keyValueStore.d.ts +17 -4
- package/dist/game/keyValueStore.js +35 -23
- package/dist/game/lootModifiers.d.ts +17 -0
- package/dist/game/lootModifiers.js +26 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +134 -35
- package/dist/game/quest.d.ts +15 -0
- package/dist/game/quest.js +209 -63
- package/dist/game/questCatalog.d.ts +50 -0
- package/dist/game/questCatalog.js +205 -0
- package/dist/game/saveStore.d.ts +6 -5
- package/dist/game/saveStore.js +57 -57
- package/dist/game/systemRuntime.d.ts +2 -2
- package/dist/game/systemRuntime.js +84 -33
- 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/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -0
- 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/interaction/promptRegistry.d.ts +5 -4
- package/dist/interaction/promptRegistry.js +8 -2
- package/dist/inventory/inventoryModel.js +9 -6
- package/dist/inventory/shapedGrid.d.ts +48 -9
- package/dist/inventory/shapedGrid.js +69 -10
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/item/durability.js +4 -0
- package/dist/item/itemInstanceRegistry.d.ts +32 -1
- package/dist/item/itemInstanceRegistry.js +111 -7
- package/dist/item/itemgen.d.ts +1 -1
- package/dist/item/itemgen.js +1 -1
- package/dist/item/modularItem.js +7 -1
- package/dist/material/appearanceSignals.d.ts +53 -0
- package/dist/material/appearanceSignals.js +47 -0
- package/dist/material/hairCards.d.ts +47 -0
- package/dist/material/hairCards.js +136 -0
- package/dist/material/materialAsset.d.ts +162 -0
- package/dist/material/materialAsset.js +382 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +444 -1
- package/dist/movement/characterController.d.ts +5 -2
- package/dist/movement/characterController.js +50 -23
- package/dist/movement/freeFlight.d.ts +2 -2
- package/dist/movement/freeFlight.js +3 -3
- 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 +63 -1
- package/dist/movement/movementModel.js +136 -26
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +48 -2
- package/dist/movement/playerMovement.js +493 -87
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +11 -1
- package/dist/movement/solidObstacles.js +48 -3
- package/dist/movement/terrainGrade.d.ts +13 -0
- package/dist/movement/terrainGrade.js +33 -0
- package/dist/movement/voxelController.d.ts +2 -2
- package/dist/movement/voxelController.js +10 -10
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- 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/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +40 -5
- package/dist/physics/kinematicVehicle.js +17 -2
- 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 +52 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +375 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- 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 +54 -3
- package/dist/runtime/cameraDirector.js +187 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -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 +12 -4
- package/dist/runtime/context/combat.js +179 -11
- package/dist/runtime/context/deathLoot.d.ts +11 -8
- package/dist/runtime/context/deathLoot.js +17 -16
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +13 -21
- package/dist/runtime/descriptors/baseline.js +34 -3
- package/dist/runtime/descriptors/features.d.ts +2 -1
- package/dist/runtime/descriptors/features.js +18 -0
- package/dist/runtime/gameContext.js +86 -13
- package/dist/runtime/gameContextTypes.d.ts +33 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/headlessRunner.d.ts +3 -1
- package/dist/runtime/headlessRunner.js +11 -11
- package/dist/runtime/hostedGameRunner.d.ts +24 -1
- package/dist/runtime/hostedGameRunner.js +156 -10
- package/dist/runtime/hostedWorldSession.d.ts +17 -5
- package/dist/runtime/hostedWorldSession.js +154 -16
- package/dist/runtime/hostedWorldStore.d.ts +2 -2
- package/dist/runtime/inputRecorder.d.ts +1 -1
- package/dist/runtime/inputRecorder.js +21 -1
- package/dist/runtime/inputSnapshot.d.ts +22 -4
- package/dist/runtime/inputSnapshot.js +41 -3
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/runtimeSave.d.ts +10 -6
- package/dist/runtime/runtimeSave.js +11 -5
- package/dist/runtime/simContext.d.ts +8 -3
- package/dist/runtime/simContext.js +7 -3
- package/dist/runtime/simSnapshot.js +2 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldReplication.d.ts +2 -0
- package/dist/runtime/worldReplication.js +1 -1
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/behaviorRuntime.d.ts +38 -7
- package/dist/scene/behaviorRuntime.js +293 -30
- package/dist/scene/behaviors.d.ts +35 -2
- package/dist/scene/behaviors.js +29 -0
- package/dist/scene/entityStore.d.ts +14 -3
- package/dist/scene/entityStore.js +45 -9
- package/dist/scene/mount.d.ts +9 -0
- package/dist/scene/mount.js +35 -0
- package/dist/scene/possession.d.ts +2 -0
- package/dist/scene/possession.js +21 -1
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/sceneRaycast.d.ts +7 -0
- package/dist/scene/sceneRaycast.js +55 -0
- package/dist/scene/spatial.d.ts +20 -1
- package/dist/scene/spatial.js +94 -2
- package/dist/scene/vehicleSeat.d.ts +41 -3
- package/dist/scene/vehicleSeat.js +94 -3
- 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/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +10 -3
- package/dist/stats/eventMeter.js +6 -3
- package/dist/store/defineStore.d.ts +3 -1
- package/dist/store/defineStore.js +3 -1
- package/dist/store/observableKeyedStore.d.ts +1 -1
- package/dist/store/observableKeyedStore.js +8 -6
- package/dist/time/simClock.d.ts +6 -2
- package/dist/time/simClock.js +5 -0
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +58 -6
- package/dist/vfx/particleDirector.js +113 -22
- package/dist/vfx/particles.d.ts +68 -5
- package/dist/vfx/particles.js +284 -32
- package/dist/visibility/assetStreaming.d.ts +21 -3
- package/dist/visibility/assetStreaming.js +108 -24
- package/dist/visibility/settings.d.ts +8 -2
- package/dist/visibility/settings.js +3 -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/visibility/visibilitySystem.js +1 -0
- package/dist/work/expedition.d.ts +110 -0
- package/dist/work/expedition.js +195 -0
- package/dist/work/staffedStation.d.ts +29 -0
- package/dist/work/staffedStation.js +27 -0
- package/dist/world/authoredAnimation.d.ts +38 -0
- package/dist/world/authoredAnimation.js +99 -0
- package/dist/world/authoredObjects.d.ts +24 -4
- package/dist/world/authoredObjects.js +43 -16
- package/dist/world/authoredSimulation.d.ts +74 -0
- package/dist/world/authoredSimulation.js +302 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +11 -4
- package/dist/world/buildingKit.js +21 -8
- 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/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +32 -5
- package/dist/world/features.js +6 -0
- package/dist/world/footprintGrid.d.ts +62 -0
- package/dist/world/footprintGrid.js +217 -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/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +9 -1
- package/dist/world/terraform.js +53 -18
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +95 -0
- package/dist/world/worldSolids.js +351 -0
- package/dist/world.d.ts +6 -6
- package/dist/world.js +5 -5
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-combat/SKILL.md +13 -2
- package/skills/jgengine-combat/api.md +150 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-combat/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +583 -463
- package/skills/jgengine-gameplay/capabilities.md +92 -0
- package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
- package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
- package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
- package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
- package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
- package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
- package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
- package/skills/jgengine-gameplay/reference-systems.md +123 -0
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createMountController, MountController } from "./mount.js";
|
|
2
|
+
import { validateCameraRig, validateChaseCameraTuning } from "../runtime/cameraDirector.js";
|
|
3
|
+
import { forgetPlayerMovement } from "../movement/playerMovement.js";
|
|
2
4
|
/**
|
|
3
5
|
* Composes `scene/mount`'s control-transfer bookkeeping with the seat/camera/movement-mode transition
|
|
4
6
|
* every enter/exit-vehicle flow needs (#533.2): boarding resolves a free seat and reports the camera
|
|
@@ -27,14 +29,21 @@ export class VehicleSeats {
|
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
exit(riderId, vehiclePose, offset = {}) {
|
|
30
|
-
const vehicleId = this.controller.
|
|
31
|
-
if (vehicleId ===
|
|
32
|
+
const vehicleId = this.controller.seatOf(riderId)?.mountId;
|
|
33
|
+
if (vehicleId === undefined)
|
|
32
34
|
return { ok: false, reason: "not_seated" };
|
|
33
35
|
const distance = offset.distance ?? 2.2;
|
|
36
|
+
if (!Array.isArray(vehiclePose.position) || vehiclePose.position.length !== 3 || !vehiclePose.position.every(Number.isFinite) ||
|
|
37
|
+
!Number.isFinite(vehiclePose.rotationY) || !Number.isFinite(distance) ||
|
|
38
|
+
(offset.side !== undefined && offset.side !== "left" && offset.side !== "right"))
|
|
39
|
+
return { ok: false, reason: "invalid_pose" };
|
|
34
40
|
const sign = offset.side === "left" ? -1 : 1;
|
|
35
41
|
const side = vehiclePose.rotationY + (Math.PI / 2) * sign;
|
|
36
42
|
const x = vehiclePose.position[0] + Math.sin(side) * distance;
|
|
37
43
|
const z = vehiclePose.position[2] + Math.cos(side) * distance;
|
|
44
|
+
if (!Number.isFinite(x) || !Number.isFinite(z))
|
|
45
|
+
return { ok: false, reason: "invalid_pose" };
|
|
46
|
+
this.controller.dismount(riderId);
|
|
38
47
|
return {
|
|
39
48
|
ok: true,
|
|
40
49
|
vehicleId,
|
|
@@ -49,11 +58,93 @@ export class VehicleSeats {
|
|
|
49
58
|
driverOf(vehicleId) {
|
|
50
59
|
return this.controller.driver(vehicleId);
|
|
51
60
|
}
|
|
61
|
+
/** The seated rider's controlled mount; `null` for passengers and unseated riders. */
|
|
62
|
+
drivenBy(riderId) {
|
|
63
|
+
return this.isSeated(riderId) ? this.controller.driveTarget(riderId) : null;
|
|
64
|
+
}
|
|
65
|
+
snapshot() {
|
|
66
|
+
return this.controller.snapshot();
|
|
67
|
+
}
|
|
68
|
+
restore(next) {
|
|
69
|
+
this.controller.restore(next);
|
|
70
|
+
}
|
|
71
|
+
reset() {
|
|
72
|
+
this.controller.reset();
|
|
73
|
+
}
|
|
52
74
|
get mounts() {
|
|
53
75
|
return this.controller;
|
|
54
76
|
}
|
|
55
77
|
}
|
|
56
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Builds a {@link VehicleSeats}, optionally over an existing `MountController` to share its occupancy.
|
|
80
|
+
*
|
|
81
|
+
* @capability vehicle-seats enter, exit and swap seats in a car or mount (GTA-style boarding) with driver and passenger slots
|
|
82
|
+
*/
|
|
57
83
|
export function createVehicleSeats(controller) {
|
|
58
84
|
return new VehicleSeats(controller);
|
|
59
85
|
}
|
|
86
|
+
function forgetRiderMovement(ctx, riderId) {
|
|
87
|
+
const possession = ctx.player.possession;
|
|
88
|
+
if (possession.active(riderId) === riderId)
|
|
89
|
+
forgetPlayerMovement(ctx, riderId);
|
|
90
|
+
for (const [userId, entityId] of Object.entries(possession.snapshotAll().active)) {
|
|
91
|
+
if (entityId === riderId)
|
|
92
|
+
forgetPlayerMovement(ctx, userId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Apply an existing seat transition to the explicitly named rider. Motor/input policy stays caller-owned.
|
|
96
|
+
* @capability board-vehicle board a rideable, freeze/hide its rider and apply caller camera policy
|
|
97
|
+
*/
|
|
98
|
+
export function boardVehicle(ctx, seats, riderId, vehicleId, options = {}) {
|
|
99
|
+
const rider = ctx.scene.entity.get(riderId);
|
|
100
|
+
if (rider === null)
|
|
101
|
+
return { ok: false, reason: "unknown_rider" };
|
|
102
|
+
if (ctx.scene.entity.get(vehicleId) === null)
|
|
103
|
+
return { ok: false, reason: "unknown_vehicle" };
|
|
104
|
+
if (options.camera !== false && options.rig !== undefined && options.rig !== null)
|
|
105
|
+
validateCameraRig(options.rig, options.config);
|
|
106
|
+
if (options.camera !== false && options.chase !== undefined)
|
|
107
|
+
validateChaseCameraTuning(options.chase);
|
|
108
|
+
const result = seats.enter(riderId, vehicleId, options);
|
|
109
|
+
if (!result.ok)
|
|
110
|
+
return result;
|
|
111
|
+
ctx.scene.entity.update(riderId, { hidden: true, movement: { ...rider.movement, ...result.riderMovementPatch } });
|
|
112
|
+
ctx.scene.entity.setVelocity(riderId, [0, 0, 0]);
|
|
113
|
+
forgetRiderMovement(ctx, riderId);
|
|
114
|
+
if (options.camera !== false) {
|
|
115
|
+
ctx.camera.follow(result.cameraTarget);
|
|
116
|
+
if (options.rig !== undefined)
|
|
117
|
+
ctx.camera.setRig(options.rig, options.config);
|
|
118
|
+
if (options.chase !== undefined)
|
|
119
|
+
ctx.camera.setChaseTuning(structuredClone(options.chase));
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
/** Apply dismount placement, restore rider movement/visibility and the configured camera rig.
|
|
124
|
+
* @capability leave-vehicle leave a rideable beside its current pose and restore on-foot camera/movement
|
|
125
|
+
*/
|
|
126
|
+
export function leaveVehicle(ctx, seats, riderId, options = {}) {
|
|
127
|
+
const rider = ctx.scene.entity.get(riderId);
|
|
128
|
+
if (rider === null)
|
|
129
|
+
return { ok: false, reason: "unknown_rider" };
|
|
130
|
+
const ref = seats.mounts.seatOf(riderId);
|
|
131
|
+
if (ref === null)
|
|
132
|
+
return { ok: false, reason: "not_seated" };
|
|
133
|
+
const vehicle = ctx.scene.entity.get(ref.mountId);
|
|
134
|
+
const pose = options.pose ?? (vehicle === null ? undefined : { position: vehicle.position, rotationY: vehicle.rotationY });
|
|
135
|
+
if (pose === undefined)
|
|
136
|
+
return { ok: false, reason: "unknown_vehicle" };
|
|
137
|
+
const result = seats.exit(riderId, pose, options);
|
|
138
|
+
if (!result.ok)
|
|
139
|
+
return result;
|
|
140
|
+
ctx.scene.entity.setPose(riderId, result.placement);
|
|
141
|
+
ctx.scene.entity.update(riderId, { hidden: false, movement: { ...rider.movement, ...result.riderMovementPatch } });
|
|
142
|
+
ctx.scene.entity.setVelocity(riderId, [0, 0, 0]);
|
|
143
|
+
forgetRiderMovement(ctx, riderId);
|
|
144
|
+
if (options.camera !== false) {
|
|
145
|
+
ctx.camera.follow(result.cameraTarget);
|
|
146
|
+
ctx.camera.setRig(null);
|
|
147
|
+
ctx.camera.setChaseTuning(null);
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|