@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
|
@@ -31,6 +31,14 @@ export interface RegenShieldConfig {
|
|
|
31
31
|
*/
|
|
32
32
|
pool?: RegenShieldPool;
|
|
33
33
|
}
|
|
34
|
+
/** Complete mutable shield state, including pending external damage detection. */
|
|
35
|
+
export interface RegenShieldState {
|
|
36
|
+
max: number;
|
|
37
|
+
current: number;
|
|
38
|
+
sinceDamageMs: number;
|
|
39
|
+
activeDelayMs: number;
|
|
40
|
+
observed: number | null;
|
|
41
|
+
}
|
|
34
42
|
/**
|
|
35
43
|
* A shield pool that stops regenerating for `regenDelayMs` after every hit, then refills at
|
|
36
44
|
* `regenPerSecond` — the delayed-regen primitive that replaces snapshot-comparing stat values per
|
|
@@ -38,6 +46,10 @@ export interface RegenShieldConfig {
|
|
|
38
46
|
* regenerates once it elapses.
|
|
39
47
|
*/
|
|
40
48
|
export interface RegenShield {
|
|
49
|
+
/** Detached pool value, bounds, and regen timing. */
|
|
50
|
+
state(): RegenShieldState;
|
|
51
|
+
/** Restore state; returns false without mutation when an external pool's max differs. */
|
|
52
|
+
restore(next: RegenShieldState): boolean;
|
|
41
53
|
current(): number;
|
|
42
54
|
max(): number;
|
|
43
55
|
setMax(value: number): void;
|
|
@@ -49,6 +61,8 @@ export interface RegenShield {
|
|
|
49
61
|
/** Absorb `amount` and reset the regen grace timer; returns damage that got through (0 while the shield holds). */
|
|
50
62
|
damage(amount: number): number;
|
|
51
63
|
/** Add `amount` without touching the regen timer (a pickup or scripted refill). */
|
|
64
|
+
refill(amount: number): void;
|
|
65
|
+
/** @deprecated Use `refill(amount)` for pickups; `restore(state)` loads persisted state. */
|
|
52
66
|
restore(amount: number): void;
|
|
53
67
|
set(value: number): void;
|
|
54
68
|
/**
|
|
@@ -39,7 +39,30 @@ export function createRegenShield(config) {
|
|
|
39
39
|
sinceDamageMs = 0;
|
|
40
40
|
observed = live;
|
|
41
41
|
}
|
|
42
|
+
function refill(amount) {
|
|
43
|
+
if (amount <= 0)
|
|
44
|
+
return;
|
|
45
|
+
syncExternal();
|
|
46
|
+
write(readCurrent() + amount);
|
|
47
|
+
observed = pool === undefined ? observed : readCurrent();
|
|
48
|
+
}
|
|
49
|
+
function restore(next) {
|
|
50
|
+
if (typeof next === "number")
|
|
51
|
+
return refill(next);
|
|
52
|
+
if (pool !== undefined && readMax() !== next.max)
|
|
53
|
+
return false;
|
|
54
|
+
if (pool === undefined)
|
|
55
|
+
ownMax = Math.max(0, next.max);
|
|
56
|
+
write(next.current);
|
|
57
|
+
sinceDamageMs = Math.max(0, next.sinceDamageMs);
|
|
58
|
+
activeDelayMs = Math.max(0, next.activeDelayMs);
|
|
59
|
+
observed = pool === undefined ? null : next.observed;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
42
62
|
return {
|
|
63
|
+
state: () => ({ max: readMax(), current: readCurrent(), sinceDamageMs, activeDelayMs, observed }),
|
|
64
|
+
restore,
|
|
65
|
+
refill,
|
|
43
66
|
current: readCurrent,
|
|
44
67
|
max: readMax,
|
|
45
68
|
setMax(value) {
|
|
@@ -65,13 +88,6 @@ export function createRegenShield(config) {
|
|
|
65
88
|
observed = pool === undefined ? observed : readCurrent();
|
|
66
89
|
return overflow;
|
|
67
90
|
},
|
|
68
|
-
restore(amount) {
|
|
69
|
-
if (amount <= 0)
|
|
70
|
-
return;
|
|
71
|
-
syncExternal();
|
|
72
|
-
write(readCurrent() + amount);
|
|
73
|
-
observed = pool === undefined ? observed : readCurrent();
|
|
74
|
-
},
|
|
75
91
|
set(value) {
|
|
76
92
|
write(value);
|
|
77
93
|
observed = pool === undefined ? observed : readCurrent();
|
|
@@ -64,6 +64,12 @@ export declare const DEFAULT_EYE_HEIGHT: number;
|
|
|
64
64
|
export declare function eyeHeightFromColliders(set: EntityColliderSet | null | undefined): number;
|
|
65
65
|
export declare function aimDirection(aim: Aim): EntityPosition | null;
|
|
66
66
|
export declare function aimSpreadDeg(aim: Aim): number;
|
|
67
|
+
/**
|
|
68
|
+
* Resolves a shot's world origin and direction from the shooter's eye or muzzle and an {@link Aim}, ready for
|
|
69
|
+
* `ctx.scene.raycast` — the aim half of a hitscan weapon.
|
|
70
|
+
*
|
|
71
|
+
* @capability hitscan-shot aim a hitscan shot (shoot a gun): resolve origin and direction from the eye or muzzle for a raycast
|
|
72
|
+
*/
|
|
67
73
|
export declare function resolveShot(deps: ShotOriginDeps, from: string, aim: Aim, policy?: ShotOriginPolicy): ResolvedShot | null;
|
|
68
74
|
/**
|
|
69
75
|
* Resolves a `converge` shot with scene knowledge: fires from the gun muzzle but bends the direction
|
|
@@ -29,6 +29,12 @@ export function aimDirection(aim) {
|
|
|
29
29
|
export function aimSpreadDeg(aim) {
|
|
30
30
|
return "origin" in aim ? 0 : aim.spread ?? 0;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolves a shot's world origin and direction from the shooter's eye or muzzle and an {@link Aim}, ready for
|
|
34
|
+
* `ctx.scene.raycast` — the aim half of a hitscan weapon.
|
|
35
|
+
*
|
|
36
|
+
* @capability hitscan-shot aim a hitscan shot (shoot a gun): resolve origin and direction from the eye or muzzle for a raycast
|
|
37
|
+
*/
|
|
32
38
|
export function resolveShot(deps, from, aim, policy = { kind: "eye" }) {
|
|
33
39
|
const aimDir = aimDirection(aim);
|
|
34
40
|
if (aimDir === null && policy.kind !== "camera" && policy.kind !== "world")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { WeaponHandlingFrame, WeaponShot } from "./weaponHandling.js";
|
|
2
|
+
/**
|
|
3
|
+
* Signals {@link createWeaponFeedbackSignals} reports each tick, named for `createFeedbackMixer` routes.
|
|
4
|
+
* `shot`, `kick` and `impact` are pulses that read non-zero only on the tick they happened; the rest follow
|
|
5
|
+
* the handling frame.
|
|
6
|
+
*/
|
|
7
|
+
export type WeaponFeedbackSignal = "shot" | "kick" | "impact" | "recoil" | "cameraKick" | "spread" | "ads";
|
|
8
|
+
/** Serializable pending pulses, so a snapshot taken between a shot and the next read loses nothing. */
|
|
9
|
+
export interface WeaponFeedbackState {
|
|
10
|
+
shots: number;
|
|
11
|
+
kick: number;
|
|
12
|
+
impact: number;
|
|
13
|
+
}
|
|
14
|
+
/** Collects weapon events between presentation ticks and hands them to a feedback mixer as one signal record. */
|
|
15
|
+
export interface WeaponFeedbackSignals {
|
|
16
|
+
/** Record a shot from `WeaponHandling.fire()`. Several shots before one `read` sum. */
|
|
17
|
+
shot(shot: WeaponShot): void;
|
|
18
|
+
/** Record a hit landing, in the game's own units (damage, `0..1` strength). Several impacts keep the largest. */
|
|
19
|
+
impact(strength: number): void;
|
|
20
|
+
/**
|
|
21
|
+
* Return this tick's signals and clear the pulses. `frame` (from `WeaponHandling.tick`) fills `recoil`,
|
|
22
|
+
* `cameraKick`, `spread` and `ads`; without it they read `0`. The record is reused between calls.
|
|
23
|
+
*/
|
|
24
|
+
read(frame?: WeaponHandlingFrame): Readonly<Record<WeaponFeedbackSignal, number>>;
|
|
25
|
+
snapshot(): WeaponFeedbackState;
|
|
26
|
+
restore(state: WeaponFeedbackState): void;
|
|
27
|
+
reset(): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates {@link WeaponFeedbackSignals}: the bridge from shots, recoil kick and impacts to `createFeedbackMixer`
|
|
31
|
+
* routes (camera shake, audio, rumble). Route `kick` with no smoothing into camera-shake trauma, `impact` with
|
|
32
|
+
* a `release` into rumble, and `ads`/`spread` into continuous audio or FOV targets. Deterministic and
|
|
33
|
+
* allocation-free per read.
|
|
34
|
+
* @capability weapon-feedback turn weapon shots, recoil kick and impacts into per-tick feedback-mixer signals for camera shake, audio and rumble
|
|
35
|
+
*/
|
|
36
|
+
export declare function createWeaponFeedbackSignals(): WeaponFeedbackSignals;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates {@link WeaponFeedbackSignals}: the bridge from shots, recoil kick and impacts to `createFeedbackMixer`
|
|
3
|
+
* routes (camera shake, audio, rumble). Route `kick` with no smoothing into camera-shake trauma, `impact` with
|
|
4
|
+
* a `release` into rumble, and `ads`/`spread` into continuous audio or FOV targets. Deterministic and
|
|
5
|
+
* allocation-free per read.
|
|
6
|
+
* @capability weapon-feedback turn weapon shots, recoil kick and impacts into per-tick feedback-mixer signals for camera shake, audio and rumble
|
|
7
|
+
*/
|
|
8
|
+
export function createWeaponFeedbackSignals() {
|
|
9
|
+
const pending = { shots: 0, kick: 0, impact: 0 };
|
|
10
|
+
const out = { shot: 0, kick: 0, impact: 0, recoil: 0, cameraKick: 0, spread: 0, ads: 0 };
|
|
11
|
+
return {
|
|
12
|
+
shot(shot) {
|
|
13
|
+
pending.shots += 1;
|
|
14
|
+
pending.kick += Math.hypot(shot.kickPitch, shot.kickYaw);
|
|
15
|
+
},
|
|
16
|
+
impact(strength) {
|
|
17
|
+
pending.impact = Math.max(pending.impact, strength);
|
|
18
|
+
},
|
|
19
|
+
read(frame) {
|
|
20
|
+
out.shot = pending.shots;
|
|
21
|
+
out.kick = pending.kick;
|
|
22
|
+
out.impact = pending.impact;
|
|
23
|
+
out.recoil = frame === undefined ? 0 : Math.hypot(frame.aimPitch, frame.aimYaw);
|
|
24
|
+
out.cameraKick = frame === undefined ? 0 : Math.hypot(frame.cameraPitch, frame.cameraYaw);
|
|
25
|
+
out.spread = frame?.spread ?? 0;
|
|
26
|
+
out.ads = frame?.adsProgress ?? 0;
|
|
27
|
+
pending.shots = 0;
|
|
28
|
+
pending.kick = 0;
|
|
29
|
+
pending.impact = 0;
|
|
30
|
+
return out;
|
|
31
|
+
},
|
|
32
|
+
snapshot: () => ({ ...pending }),
|
|
33
|
+
restore(state) {
|
|
34
|
+
Object.assign(pending, state);
|
|
35
|
+
},
|
|
36
|
+
reset() {
|
|
37
|
+
pending.shots = 0;
|
|
38
|
+
pending.kick = 0;
|
|
39
|
+
pending.impact = 0;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** Recoil per shot, in radians of aim. `pitch > 0` climbs the muzzle, `yaw > 0` pulls right. */
|
|
2
|
+
export interface WeaponRecoilTuning {
|
|
3
|
+
/**
|
|
4
|
+
* Fixed kick per shot index as `[pitch, yaw]` rad — the learnable part of a spray pattern. Past its end the
|
|
5
|
+
* last entry repeats. Omit for a flat `pitch` kick every shot.
|
|
6
|
+
*/
|
|
7
|
+
pattern?: readonly (readonly [number, number])[];
|
|
8
|
+
/** Kick per shot when there is no `pattern`, rad (default `0`). */
|
|
9
|
+
pitch?: number;
|
|
10
|
+
/** Random extra kick, rad, drawn per shot from the injected `random` within ±this cone (default `0`). */
|
|
11
|
+
randomCone?: number;
|
|
12
|
+
/** Share of each kick that moves the camera rather than the aim, `0..1` (default `0`: all aim). */
|
|
13
|
+
cameraShare?: number;
|
|
14
|
+
/** Rate the accumulated kick recovers toward centre, rad/s (default `0`: none). */
|
|
15
|
+
recoverRate?: number;
|
|
16
|
+
/** Seconds after a shot before recovery starts (default `0.1`). */
|
|
17
|
+
recoverDelay?: number;
|
|
18
|
+
/** Aim kick multiplier while aiming down sights (default `1`). */
|
|
19
|
+
adsScale?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Cone of fire, as a half-angle in radians. */
|
|
22
|
+
export interface WeaponSpreadTuning {
|
|
23
|
+
/** Resting spread, rad. */
|
|
24
|
+
base: number;
|
|
25
|
+
/** Spread added per shot (bloom), rad (default `0`). */
|
|
26
|
+
perShot?: number;
|
|
27
|
+
/** Largest bloom, rad (default `base + 10 · perShot`). */
|
|
28
|
+
max?: number;
|
|
29
|
+
/** Rate bloom recovers toward `base`, rad/s (default `0`). */
|
|
30
|
+
recoverRate?: number;
|
|
31
|
+
/** Seconds after a shot before bloom recovers (default `0.1`). */
|
|
32
|
+
recoverDelay?: number;
|
|
33
|
+
/** Multipliers on the final spread for stance and aim; each defaults to `1`. */
|
|
34
|
+
ads?: number;
|
|
35
|
+
crouching?: number;
|
|
36
|
+
moving?: number;
|
|
37
|
+
airborne?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Tuning for {@link createWeaponHandling}. Every number is an angle in radians or a time in seconds, so two
|
|
41
|
+
* weapons differ by numbers a person can reason about: a controllable rifle climbs a little and recovers
|
|
42
|
+
* fast; a shotgun kicks hard and blooms wide.
|
|
43
|
+
*/
|
|
44
|
+
export interface WeaponHandlingTuning {
|
|
45
|
+
recoil?: WeaponRecoilTuning;
|
|
46
|
+
spread: WeaponSpreadTuning;
|
|
47
|
+
/** Seconds to go fully into or out of aim-down-sights (default `0.2`). */
|
|
48
|
+
adsTime?: number;
|
|
49
|
+
}
|
|
50
|
+
/** Stance and aim for one tick. */
|
|
51
|
+
export interface WeaponStance {
|
|
52
|
+
ads?: boolean;
|
|
53
|
+
crouching?: boolean;
|
|
54
|
+
moving?: boolean;
|
|
55
|
+
airborne?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** Serializable handling state. */
|
|
58
|
+
export interface WeaponHandlingState {
|
|
59
|
+
aimPitch: number;
|
|
60
|
+
aimYaw: number;
|
|
61
|
+
cameraPitch: number;
|
|
62
|
+
cameraYaw: number;
|
|
63
|
+
bloom: number;
|
|
64
|
+
shotIndex: number;
|
|
65
|
+
sinceShot: number;
|
|
66
|
+
adsProgress: number;
|
|
67
|
+
}
|
|
68
|
+
/** What one shot did: the cone to sample and the kick it applied. */
|
|
69
|
+
export interface WeaponShot {
|
|
70
|
+
/** Half-angle of the cone this shot fires into, rad, with stance multipliers applied. */
|
|
71
|
+
spread: number;
|
|
72
|
+
/** Aim kick applied by this shot, rad. */
|
|
73
|
+
kickPitch: number;
|
|
74
|
+
kickYaw: number;
|
|
75
|
+
}
|
|
76
|
+
/** Live handling readout: the offsets to add to aim and camera, current spread and ADS blend. */
|
|
77
|
+
export interface WeaponHandlingFrame {
|
|
78
|
+
/** Recoil offset to add to the aim direction, rad. */
|
|
79
|
+
aimPitch: number;
|
|
80
|
+
aimYaw: number;
|
|
81
|
+
/** Recoil offset to add to the camera only (view punch), rad. */
|
|
82
|
+
cameraPitch: number;
|
|
83
|
+
cameraYaw: number;
|
|
84
|
+
/** Current half-angle spread with stance multipliers, rad. */
|
|
85
|
+
spread: number;
|
|
86
|
+
/** `0` hip … `1` fully aimed. */
|
|
87
|
+
adsProgress: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The player-to-weapon relationship as state: recoil that climbs and recovers, spread that blooms and
|
|
91
|
+
* settles, and an aim-down-sights blend. Cadence, ammo and hits stay in `weaponFire`/`magazine`; this owns
|
|
92
|
+
* how the weapon feels between them. First-person, third-person and over-the-shoulder presentations read
|
|
93
|
+
* the same frame.
|
|
94
|
+
*/
|
|
95
|
+
export interface WeaponHandling {
|
|
96
|
+
/** Register one shot (call when the cadence fires) and return its spread and kick. */
|
|
97
|
+
fire(stance?: WeaponStance): WeaponShot;
|
|
98
|
+
/** Advance recovery, bloom decay and ADS by `dt`. */
|
|
99
|
+
tick(dt: number, stance?: WeaponStance): WeaponHandlingFrame;
|
|
100
|
+
frame(): WeaponHandlingFrame;
|
|
101
|
+
retune(next: WeaponHandlingTuning): void;
|
|
102
|
+
snapshot(): WeaponHandlingState;
|
|
103
|
+
restore(state: WeaponHandlingState): void;
|
|
104
|
+
reset(): void;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Creates a {@link WeaponHandling}. Pass `random` (e.g. `ctx.rng` or `seededRng(seed)`) for the random
|
|
108
|
+
* recoil cone; without it the cone is skipped, so the result stays deterministic either way.
|
|
109
|
+
* @capability weapon-handling recoil patterns with camera kick and recovery, spread bloom, stance multipliers and ADS as serializable state
|
|
110
|
+
*/
|
|
111
|
+
export declare function createWeaponHandling(initial: WeaponHandlingTuning, options?: {
|
|
112
|
+
random?: () => number;
|
|
113
|
+
}): WeaponHandling;
|
|
114
|
+
/** Deterministic weapon-feel metrics from {@link measureWeapon}. */
|
|
115
|
+
export interface WeaponReport {
|
|
116
|
+
/** Spread of the first shot from rest, rad. */
|
|
117
|
+
firstShotSpread: number;
|
|
118
|
+
/** Spread of the tenth shot of a held burst, rad. */
|
|
119
|
+
tenthShotSpread: number;
|
|
120
|
+
/** Spread of every shot in the burst, rad; `spreadByShot[n - 1]` is the spread after `n - 1` shots of bloom. */
|
|
121
|
+
spreadByShot: number[];
|
|
122
|
+
/** Aim climb after a full burst, rad; set `burst` to the magazine size for climb over a magazine. */
|
|
123
|
+
burstClimb: number;
|
|
124
|
+
/** Seconds after the burst until aim recoil is back within 5% of its peak and bloom within 5% of base. */
|
|
125
|
+
resetSeconds: number;
|
|
126
|
+
/** Seconds to go fully into aim-down-sights. */
|
|
127
|
+
adsSeconds: number;
|
|
128
|
+
/** Shots until the expected damage reaches `targetHealth` (`0` without `damage`, `Infinity` if it never does). */
|
|
129
|
+
shotsToKill: number;
|
|
130
|
+
/** Seconds from the first to the killing shot at `interval` (`0` without `damage` or when one shot kills). */
|
|
131
|
+
timeToKill: number;
|
|
132
|
+
}
|
|
133
|
+
/** Options for {@link measureWeapon}. */
|
|
134
|
+
export interface WeaponProbeOptions {
|
|
135
|
+
/** Seconds between shots (`60 / rpm`). */
|
|
136
|
+
interval: number;
|
|
137
|
+
/** Shots in the held burst (default `30`). */
|
|
138
|
+
burst?: number;
|
|
139
|
+
/** Damage per projectile at the muzzle. */
|
|
140
|
+
damage?: number;
|
|
141
|
+
/** Projectiles per shot, e.g. shotgun pellets (default `1`). */
|
|
142
|
+
pellets?: number;
|
|
143
|
+
targetHealth?: number;
|
|
144
|
+
/** Distance to the target, m. Without it every projectile hits. */
|
|
145
|
+
range?: number;
|
|
146
|
+
/** Radius of the target's hittable area, m (default `0.4`). */
|
|
147
|
+
targetRadius?: number;
|
|
148
|
+
/** Damage multiplier at a range, the game's falloff policy (default: none). */
|
|
149
|
+
damageAt?: (range: number) => number;
|
|
150
|
+
/** Stance the burst and kill run are fired in (e.g. `{ ads: true }` after the ADS time). */
|
|
151
|
+
stance?: WeaponStance;
|
|
152
|
+
dt?: number;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fires a held burst through a fresh handling instance at a fixed interval and reports spread growth,
|
|
156
|
+
* climb, reset time, ADS time and time-to-kill. Time-to-kill counts expected damage: each projectile hits
|
|
157
|
+
* with the share of its spread cone the target covers at `range`, scaled by `damageAt(range)`, so a
|
|
158
|
+
* blooming rifle and a wide shotgun compare at the distance the game cares about.
|
|
159
|
+
* @capability weapon-metrics measure a weapon's feel as numbers — spread per shot, climb over a magazine, reset time, ADS time, time-to-kill at a range
|
|
160
|
+
*/
|
|
161
|
+
export declare function measureWeapon(create: () => WeaponHandling, options: WeaponProbeOptions): WeaponReport;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
function fresh(tuning) {
|
|
2
|
+
return {
|
|
3
|
+
aimPitch: 0,
|
|
4
|
+
aimYaw: 0,
|
|
5
|
+
cameraPitch: 0,
|
|
6
|
+
cameraYaw: 0,
|
|
7
|
+
bloom: tuning.spread.base,
|
|
8
|
+
shotIndex: 0,
|
|
9
|
+
// Finite so a snapshot survives JSON; anything past every delay behaves the same.
|
|
10
|
+
sinceShot: 1e9,
|
|
11
|
+
adsProgress: 0,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function toward(value, amount) {
|
|
15
|
+
if (value === 0)
|
|
16
|
+
return 0;
|
|
17
|
+
return Math.abs(value) <= amount ? 0 : value - Math.sign(value) * amount;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a {@link WeaponHandling}. Pass `random` (e.g. `ctx.rng` or `seededRng(seed)`) for the random
|
|
21
|
+
* recoil cone; without it the cone is skipped, so the result stays deterministic either way.
|
|
22
|
+
* @capability weapon-handling recoil patterns with camera kick and recovery, spread bloom, stance multipliers and ADS as serializable state
|
|
23
|
+
*/
|
|
24
|
+
export function createWeaponHandling(initial, options = {}) {
|
|
25
|
+
let tuning = initial;
|
|
26
|
+
const random = options.random;
|
|
27
|
+
const state = fresh(tuning);
|
|
28
|
+
const out = { aimPitch: 0, aimYaw: 0, cameraPitch: 0, cameraYaw: 0, spread: 0, adsProgress: 0 };
|
|
29
|
+
function spreadFor(stance) {
|
|
30
|
+
const spread = tuning.spread;
|
|
31
|
+
const ads = 1 + ((spread.ads ?? 1) - 1) * state.adsProgress;
|
|
32
|
+
const crouch = stance?.crouching === true ? (spread.crouching ?? 1) : 1;
|
|
33
|
+
const moving = stance?.moving === true ? (spread.moving ?? 1) : 1;
|
|
34
|
+
const air = stance?.airborne === true ? (spread.airborne ?? 1) : 1;
|
|
35
|
+
return state.bloom * ads * crouch * moving * air;
|
|
36
|
+
}
|
|
37
|
+
function write(stance) {
|
|
38
|
+
out.aimPitch = state.aimPitch;
|
|
39
|
+
out.aimYaw = state.aimYaw;
|
|
40
|
+
out.cameraPitch = state.cameraPitch;
|
|
41
|
+
out.cameraYaw = state.cameraYaw;
|
|
42
|
+
out.spread = spreadFor(stance);
|
|
43
|
+
out.adsProgress = state.adsProgress;
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
fire(stance) {
|
|
48
|
+
const recoil = tuning.recoil;
|
|
49
|
+
const spread = spreadFor(stance);
|
|
50
|
+
let kickPitch = 0;
|
|
51
|
+
let kickYaw = 0;
|
|
52
|
+
if (recoil !== undefined) {
|
|
53
|
+
const pattern = recoil.pattern;
|
|
54
|
+
const entry = pattern !== undefined && pattern.length > 0 ? pattern[Math.min(state.shotIndex, pattern.length - 1)] : undefined;
|
|
55
|
+
kickPitch = entry?.[0] ?? recoil.pitch ?? 0;
|
|
56
|
+
kickYaw = entry?.[1] ?? 0;
|
|
57
|
+
const cone = recoil.randomCone ?? 0;
|
|
58
|
+
if (cone > 0 && random !== undefined) {
|
|
59
|
+
kickPitch += (random() * 2 - 1) * cone;
|
|
60
|
+
kickYaw += (random() * 2 - 1) * cone;
|
|
61
|
+
}
|
|
62
|
+
const scale = 1 + ((recoil.adsScale ?? 1) - 1) * state.adsProgress;
|
|
63
|
+
kickPitch *= scale;
|
|
64
|
+
kickYaw *= scale;
|
|
65
|
+
const cameraShare = Math.max(0, Math.min(1, recoil.cameraShare ?? 0));
|
|
66
|
+
state.aimPitch += kickPitch * (1 - cameraShare);
|
|
67
|
+
state.aimYaw += kickYaw * (1 - cameraShare);
|
|
68
|
+
state.cameraPitch += kickPitch * cameraShare;
|
|
69
|
+
state.cameraYaw += kickYaw * cameraShare;
|
|
70
|
+
}
|
|
71
|
+
const perShot = tuning.spread.perShot ?? 0;
|
|
72
|
+
const max = tuning.spread.max ?? tuning.spread.base + perShot * 10;
|
|
73
|
+
state.bloom = Math.min(max, state.bloom + perShot);
|
|
74
|
+
state.shotIndex += 1;
|
|
75
|
+
state.sinceShot = 0;
|
|
76
|
+
return { spread, kickPitch, kickYaw };
|
|
77
|
+
},
|
|
78
|
+
tick(dt, stance) {
|
|
79
|
+
const step = Math.max(0, dt);
|
|
80
|
+
state.sinceShot += step;
|
|
81
|
+
const adsTime = Math.max(1e-3, tuning.adsTime ?? 0.2);
|
|
82
|
+
const adsTarget = stance?.ads === true ? 1 : 0;
|
|
83
|
+
state.adsProgress += Math.max(-step / adsTime, Math.min(step / adsTime, adsTarget - state.adsProgress));
|
|
84
|
+
const recoil = tuning.recoil;
|
|
85
|
+
if (recoil !== undefined && state.sinceShot >= (recoil.recoverDelay ?? 0.1)) {
|
|
86
|
+
const amount = (recoil.recoverRate ?? 0) * step;
|
|
87
|
+
state.aimPitch = toward(state.aimPitch, amount);
|
|
88
|
+
state.aimYaw = toward(state.aimYaw, amount);
|
|
89
|
+
state.cameraPitch = toward(state.cameraPitch, amount);
|
|
90
|
+
state.cameraYaw = toward(state.cameraYaw, amount);
|
|
91
|
+
if (state.aimPitch === 0 && state.aimYaw === 0)
|
|
92
|
+
state.shotIndex = 0;
|
|
93
|
+
}
|
|
94
|
+
const spread = tuning.spread;
|
|
95
|
+
if (state.sinceShot >= (spread.recoverDelay ?? 0.1)) {
|
|
96
|
+
state.bloom = Math.max(spread.base, state.bloom - (spread.recoverRate ?? 0) * step);
|
|
97
|
+
}
|
|
98
|
+
return write(stance);
|
|
99
|
+
},
|
|
100
|
+
frame: () => write(undefined),
|
|
101
|
+
retune(next) {
|
|
102
|
+
tuning = next;
|
|
103
|
+
state.bloom = Math.max(next.spread.base, state.bloom);
|
|
104
|
+
},
|
|
105
|
+
snapshot: () => ({ ...state }),
|
|
106
|
+
restore(next) {
|
|
107
|
+
Object.assign(state, next);
|
|
108
|
+
},
|
|
109
|
+
reset() {
|
|
110
|
+
Object.assign(state, fresh(tuning));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function hitChance(spread, range, radius) {
|
|
115
|
+
if (range === undefined || range <= 0)
|
|
116
|
+
return 1;
|
|
117
|
+
const coneRadius = range * Math.tan(Math.max(0, spread));
|
|
118
|
+
return coneRadius <= radius ? 1 : (radius / coneRadius) ** 2;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Fires a held burst through a fresh handling instance at a fixed interval and reports spread growth,
|
|
122
|
+
* climb, reset time, ADS time and time-to-kill. Time-to-kill counts expected damage: each projectile hits
|
|
123
|
+
* with the share of its spread cone the target covers at `range`, scaled by `damageAt(range)`, so a
|
|
124
|
+
* blooming rifle and a wide shotgun compare at the distance the game cares about.
|
|
125
|
+
* @capability weapon-metrics measure a weapon's feel as numbers — spread per shot, climb over a magazine, reset time, ADS time, time-to-kill at a range
|
|
126
|
+
*/
|
|
127
|
+
export function measureWeapon(create, options) {
|
|
128
|
+
const dt = options.dt ?? 1 / 120;
|
|
129
|
+
const burst = options.burst ?? 30;
|
|
130
|
+
const interval = Math.max(dt, options.interval);
|
|
131
|
+
const stance = options.stance;
|
|
132
|
+
const settle = (weapon) => {
|
|
133
|
+
if (stance?.ads !== true)
|
|
134
|
+
return;
|
|
135
|
+
for (let i = 0; i < Math.ceil(5 / dt) && weapon.frame().adsProgress < 1; i += 1)
|
|
136
|
+
weapon.tick(dt, stance);
|
|
137
|
+
};
|
|
138
|
+
const hold = (weapon) => {
|
|
139
|
+
for (let t = 0; t < interval - 1e-9; t += dt)
|
|
140
|
+
weapon.tick(dt, stance);
|
|
141
|
+
};
|
|
142
|
+
const weapon = create();
|
|
143
|
+
settle(weapon);
|
|
144
|
+
const spreadByShot = [];
|
|
145
|
+
for (let shot = 0; shot < burst; shot += 1) {
|
|
146
|
+
spreadByShot.push(weapon.fire(stance).spread);
|
|
147
|
+
hold(weapon);
|
|
148
|
+
}
|
|
149
|
+
const peak = weapon.snapshot();
|
|
150
|
+
const burstClimb = peak.aimPitch;
|
|
151
|
+
const base = weapon.frame().spread;
|
|
152
|
+
let resetSeconds = Number.POSITIVE_INFINITY;
|
|
153
|
+
const baseSpread = create().frame().spread;
|
|
154
|
+
for (let i = 1; i <= Math.ceil(10 / dt); i += 1) {
|
|
155
|
+
const frame = weapon.tick(dt);
|
|
156
|
+
const aimSettled = Math.abs(frame.aimPitch) <= Math.abs(peak.aimPitch) * 0.05 + 1e-9;
|
|
157
|
+
const spreadSettled = frame.spread <= baseSpread + (base - baseSpread) * 0.05 + 1e-9;
|
|
158
|
+
if (aimSettled && spreadSettled) {
|
|
159
|
+
resetSeconds = i * dt;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const ads = create();
|
|
164
|
+
let adsSeconds = Number.POSITIVE_INFINITY;
|
|
165
|
+
for (let i = 1; i <= Math.ceil(5 / dt); i += 1) {
|
|
166
|
+
if (ads.tick(dt, { ads: true }).adsProgress >= 1) {
|
|
167
|
+
adsSeconds = i * dt;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const damage = options.damage ?? 0;
|
|
172
|
+
let shotsToKill = 0;
|
|
173
|
+
if (damage > 0) {
|
|
174
|
+
const health = options.targetHealth ?? 0;
|
|
175
|
+
const perHit = damage * (options.range !== undefined && options.damageAt !== undefined ? options.damageAt(options.range) : 1);
|
|
176
|
+
const pellets = Math.max(1, Math.floor(options.pellets ?? 1));
|
|
177
|
+
const radius = options.targetRadius ?? 0.4;
|
|
178
|
+
const shooter = create();
|
|
179
|
+
settle(shooter);
|
|
180
|
+
let dealt = 0;
|
|
181
|
+
shotsToKill = Number.POSITIVE_INFINITY;
|
|
182
|
+
for (let shot = 1; shot <= 10_000 && perHit > 0; shot += 1) {
|
|
183
|
+
dealt += perHit * pellets * hitChance(shooter.fire(stance).spread, options.range, radius);
|
|
184
|
+
if (dealt >= health - 1e-9) {
|
|
185
|
+
shotsToKill = shot;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
hold(shooter);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const timeToKill = shotsToKill > 0 ? (shotsToKill - 1) * interval : 0;
|
|
192
|
+
return {
|
|
193
|
+
firstShotSpread: spreadByShot[0] ?? 0,
|
|
194
|
+
tenthShotSpread: spreadByShot[9] ?? 0,
|
|
195
|
+
spreadByShot,
|
|
196
|
+
burstClimb,
|
|
197
|
+
resetSeconds,
|
|
198
|
+
adsSeconds,
|
|
199
|
+
shotsToKill,
|
|
200
|
+
timeToKill,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { WeaponHandlingFrame } from "./weaponHandling.js";
|
|
2
|
+
/** Camera-space position `[x, y, z]` in metres: `+x` right, `+y` up, `-z` forward. */
|
|
3
|
+
export type ViewmodelOffset = readonly [number, number, number];
|
|
4
|
+
/**
|
|
5
|
+
* How one weapon sits in first person. Offsets are camera-space metres, angles are radians, FOVs are
|
|
6
|
+
* degrees. Every field is optional; two weapons differ by the ones they set.
|
|
7
|
+
*/
|
|
8
|
+
export interface WeaponPresentationTuning {
|
|
9
|
+
/** Viewmodel anchor at the hip (default `[0.34, -0.26, -0.72]`). */
|
|
10
|
+
hip?: ViewmodelOffset;
|
|
11
|
+
/** Viewmodel anchor fully aimed; put the sight on the camera axis (default `[0, -0.07, -0.55]`). */
|
|
12
|
+
ads?: ViewmodelOffset;
|
|
13
|
+
/** Viewmodel FOV in degrees, independent of the world FOV; omit to draw the viewmodel at the world FOV. */
|
|
14
|
+
viewmodelFov?: {
|
|
15
|
+
hip?: number;
|
|
16
|
+
ads?: number;
|
|
17
|
+
};
|
|
18
|
+
/** World FOV multiplier at full ADS, e.g. `0.8` for a light zoom (default `1`). */
|
|
19
|
+
adsZoom?: number;
|
|
20
|
+
/** Viewmodel lag behind look motion. */
|
|
21
|
+
sway?: {
|
|
22
|
+
/** Radians of lag per rad/s of look speed (default `0.02`). */
|
|
23
|
+
perRadPerSec?: number;
|
|
24
|
+
/** Largest lag, rad (default `0.08`). */
|
|
25
|
+
max?: number;
|
|
26
|
+
/** How fast lag follows its target, 1/s (default `12`). */
|
|
27
|
+
response?: number;
|
|
28
|
+
/** Lag multiplier at full ADS (default `0.3`). */
|
|
29
|
+
adsScale?: number;
|
|
30
|
+
};
|
|
31
|
+
/** Walk bob driven by the movement probe's speed and `bobPhase`. */
|
|
32
|
+
bob?: {
|
|
33
|
+
/** Side and vertical amplitude in metres at `fullSpeed` (default `[0.012, 0.01]`). */
|
|
34
|
+
amplitude?: readonly [number, number];
|
|
35
|
+
/** Ground speed that gives the full amplitude, m/s (default `4`). */
|
|
36
|
+
fullSpeed?: number;
|
|
37
|
+
/** Bob multiplier at full ADS (default `0.25`). */
|
|
38
|
+
adsScale?: number;
|
|
39
|
+
};
|
|
40
|
+
/** Viewmodel reaction to the handling's recoil offset. */
|
|
41
|
+
kick?: {
|
|
42
|
+
/** Metres pushed back per radian of recoil (default `1.2`). */
|
|
43
|
+
back?: number;
|
|
44
|
+
/** Viewmodel muzzle rise per radian of recoil (default `1.5`). */
|
|
45
|
+
rise?: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** One frame of inputs: look motion, the movement probe, and the weapon's handling readout. */
|
|
49
|
+
export interface WeaponPresentationInput {
|
|
50
|
+
/** Look yaw speed, rad/s, positive turning left (the same sign as a camera yaw that grows left). */
|
|
51
|
+
lookYawRate: number;
|
|
52
|
+
/** Look pitch speed, rad/s, positive looking up. */
|
|
53
|
+
lookPitchRate: number;
|
|
54
|
+
/** Horizontal ground speed, m/s (e.g. `EntityRenderCues.speed`). */
|
|
55
|
+
speed: number;
|
|
56
|
+
/** `0..1` walk-cycle phase (e.g. `EntityRenderCues.bobPhase`). */
|
|
57
|
+
bobPhase: number;
|
|
58
|
+
handling: WeaponHandlingFrame;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Where the viewmodel and camera go this frame. First person applies all of it; third person and
|
|
62
|
+
* over-the-shoulder read `lookPitch`/`lookYaw`, `aimPitch`/`aimYaw` and `adsProgress` from the same frame.
|
|
63
|
+
*/
|
|
64
|
+
export interface WeaponPose {
|
|
65
|
+
/** Viewmodel anchor, camera-space metres. */
|
|
66
|
+
offset: [number, number, number];
|
|
67
|
+
/** Viewmodel rotation about its anchor, rad: `pitch > 0` muzzle up, `yaw > 0` muzzle left, `roll > 0` tilts left. */
|
|
68
|
+
pitch: number;
|
|
69
|
+
yaw: number;
|
|
70
|
+
roll: number;
|
|
71
|
+
/** Viewmodel FOV in degrees, or `null` to use the world FOV. */
|
|
72
|
+
viewmodelFov: number | null;
|
|
73
|
+
/** Multiplier for the world FOV (ADS zoom). */
|
|
74
|
+
fovScale: number;
|
|
75
|
+
/** Offset to add to the camera look, rad: aim kick plus view punch. `lookYaw > 0` pulls right. */
|
|
76
|
+
lookPitch: number;
|
|
77
|
+
lookYaw: number;
|
|
78
|
+
/** Offset shots follow, rad; a third-person rig or upper-body aim reads this. */
|
|
79
|
+
aimPitch: number;
|
|
80
|
+
aimYaw: number;
|
|
81
|
+
/** `0` hip … `1` fully aimed. */
|
|
82
|
+
adsProgress: number;
|
|
83
|
+
}
|
|
84
|
+
/** Serializable presentation state: the current sway lag. */
|
|
85
|
+
export interface WeaponPresentationState {
|
|
86
|
+
swayPitch: number;
|
|
87
|
+
swayYaw: number;
|
|
88
|
+
}
|
|
89
|
+
/** Turns a {@link WeaponHandlingFrame} plus look and movement into a viewmodel and camera pose. */
|
|
90
|
+
export interface WeaponPresentation {
|
|
91
|
+
/** Advance sway by `dt` and return the pose. The returned object is reused between calls. */
|
|
92
|
+
update(dt: number, input: WeaponPresentationInput): WeaponPose;
|
|
93
|
+
pose(): WeaponPose;
|
|
94
|
+
retune(next: WeaponPresentationTuning): void;
|
|
95
|
+
snapshot(): WeaponPresentationState;
|
|
96
|
+
restore(state: WeaponPresentationState): void;
|
|
97
|
+
reset(): void;
|
|
98
|
+
}
|
|
99
|
+
/** Default hip anchor, matching the shell's built-in viewmodel. */
|
|
100
|
+
export declare const DEFAULT_VIEWMODEL_HIP: ViewmodelOffset;
|
|
101
|
+
/** Default ADS anchor: the built-in viewmodel's top rail on the camera axis. */
|
|
102
|
+
export declare const DEFAULT_VIEWMODEL_ADS: ViewmodelOffset;
|
|
103
|
+
/**
|
|
104
|
+
* Creates a {@link WeaponPresentation}: per-weapon viewmodel offset, ADS pose and viewmodel FOV, sway
|
|
105
|
+
* and inertia from look speed, bob from the movement probe, and a recoil kick, all driven by the same
|
|
106
|
+
* handling frame third-person and over-the-shoulder rigs read. Deterministic and allocation-free per update.
|
|
107
|
+
* @capability weapon-presentation per-weapon viewmodel offset, ADS pose, viewmodel FOV, sway, bob and recoil kick from a weapon handling frame
|
|
108
|
+
*/
|
|
109
|
+
export declare function createWeaponPresentation(initial?: WeaponPresentationTuning): WeaponPresentation;
|
|
110
|
+
/**
|
|
111
|
+
* Scale for a viewmodel drawn at `viewmodelFov` through a camera at `worldFov` (both degrees): multiply
|
|
112
|
+
* the viewmodel's camera-space x and y by this and it projects as if rendered with its own FOV, without
|
|
113
|
+
* a second camera or render pass.
|
|
114
|
+
*/
|
|
115
|
+
export declare function viewmodelFovScale(worldFov: number, viewmodelFov: number): number;
|