@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,10 +1,17 @@
|
|
|
1
1
|
import { sampleAxisBindings } from "../input/axisInput.js";
|
|
2
|
+
import { createHapticChannels } from "../input/haptics.js";
|
|
2
3
|
export function createInputSnapshot() {
|
|
3
4
|
let heldSet = new Set();
|
|
4
5
|
let previousHeldSet = new Set();
|
|
5
6
|
let heldList = [];
|
|
6
7
|
let pointerState = null;
|
|
7
8
|
let analogValues = null;
|
|
9
|
+
let buffered = false;
|
|
10
|
+
let stepHeld = new Set();
|
|
11
|
+
const pendingPressed = new Set();
|
|
12
|
+
let stepPressed = new Set();
|
|
13
|
+
let stepReleased = new Set();
|
|
14
|
+
const haptics = new Map();
|
|
8
15
|
const value = (action) => {
|
|
9
16
|
const analog = analogValues?.[action];
|
|
10
17
|
if (analog !== undefined)
|
|
@@ -13,10 +20,41 @@ export function createInputSnapshot() {
|
|
|
13
20
|
};
|
|
14
21
|
return {
|
|
15
22
|
rumble: async () => false,
|
|
16
|
-
|
|
23
|
+
haptics(userId) {
|
|
24
|
+
let channels = haptics.get(userId);
|
|
25
|
+
if (channels === undefined) {
|
|
26
|
+
channels = createHapticChannels();
|
|
27
|
+
haptics.set(userId, channels);
|
|
28
|
+
}
|
|
29
|
+
return channels;
|
|
30
|
+
},
|
|
31
|
+
publish(held, edges) {
|
|
17
32
|
previousHeldSet = heldSet;
|
|
18
33
|
heldList = Object.freeze([...held]);
|
|
19
34
|
heldSet = new Set(held);
|
|
35
|
+
if (edges !== undefined) {
|
|
36
|
+
buffered = true;
|
|
37
|
+
if (edges.reset) {
|
|
38
|
+
pendingPressed.clear();
|
|
39
|
+
stepPressed.clear();
|
|
40
|
+
stepReleased.clear();
|
|
41
|
+
stepHeld = new Set(held);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
for (const action of edges.pressed ?? [])
|
|
45
|
+
pendingPressed.add(action);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
beginStep() {
|
|
50
|
+
buffered = true;
|
|
51
|
+
stepPressed = new Set(pendingPressed);
|
|
52
|
+
pendingPressed.clear();
|
|
53
|
+
for (const action of heldSet)
|
|
54
|
+
if (!stepHeld.has(action))
|
|
55
|
+
stepPressed.add(action);
|
|
56
|
+
stepReleased = new Set([...stepHeld].filter(action => !heldSet.has(action)));
|
|
57
|
+
stepHeld = new Set(heldSet);
|
|
20
58
|
},
|
|
21
59
|
publishPointer(state) {
|
|
22
60
|
pointerState = state === null ? null : Object.freeze({ ...state });
|
|
@@ -30,7 +68,7 @@ export function createInputSnapshot() {
|
|
|
30
68
|
value,
|
|
31
69
|
analog: () => analogValues,
|
|
32
70
|
axis: (bindings, ranges) => sampleAxisBindings(bindings, (action) => heldSet.has(action), pointerState, ranges, value),
|
|
33
|
-
justPressed: (action) => heldSet.has(action) && !previousHeldSet.has(action),
|
|
34
|
-
justReleased: (action) => !heldSet.has(action) && previousHeldSet.has(action),
|
|
71
|
+
justPressed: (action) => buffered ? stepPressed.has(action) : heldSet.has(action) && !previousHeldSet.has(action),
|
|
72
|
+
justReleased: (action) => buffered ? stepReleased.has(action) : !heldSet.has(action) && previousHeldSet.has(action),
|
|
35
73
|
};
|
|
36
74
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { GameContext } from "./gameContext.js";
|
|
2
|
+
import { type InputSnapshot } from "./inputSnapshot.js";
|
|
3
|
+
/** One local seat on a shared screen: a stable slot id, the user id the game spawns for it, and the device driving it. */
|
|
4
|
+
export interface LocalPlayerSlot {
|
|
5
|
+
readonly slotId: string;
|
|
6
|
+
readonly index: number;
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
/** Device that claimed the seat (`"gamepad:1"`), `null` for the primary seat before a device claims it. */
|
|
9
|
+
readonly deviceId: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** Retunable seat policy. */
|
|
12
|
+
export interface LocalPlayersConfig {
|
|
13
|
+
/** Seats on this screen, including the primary (default `1`: every device drives the primary player). */
|
|
14
|
+
maxSlots: number;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the first device to press something claims the primary seat alongside keyboard and touch
|
|
17
|
+
* (`"first"`, default), or the primary stays keyboard/touch-only and every pad opens a new seat (`"none"`).
|
|
18
|
+
*/
|
|
19
|
+
claimPrimary?: "first" | "none";
|
|
20
|
+
}
|
|
21
|
+
/** Options for {@link createLocalPlayers}. */
|
|
22
|
+
export interface LocalPlayersOptions extends LocalPlayersConfig {
|
|
23
|
+
/** User id of seat 0 — `ctx.player.userId`. */
|
|
24
|
+
primaryUserId: string;
|
|
25
|
+
/** Input of seat 0 — `ctx.input`; other seats get their own snapshot. */
|
|
26
|
+
primaryInput?: InputSnapshot;
|
|
27
|
+
/** User id for seat `index >= 1` (default `` `${primaryUserId}:p${index + 1}` ``). */
|
|
28
|
+
userIdFor?: (index: number) => string;
|
|
29
|
+
}
|
|
30
|
+
/** Result of {@link LocalPlayers.assign}. */
|
|
31
|
+
export interface LocalPlayerAssignment {
|
|
32
|
+
slot: LocalPlayerSlot;
|
|
33
|
+
/** True when this call opened the seat: spawn its entity through `onNewPlayer`. */
|
|
34
|
+
joined: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Serializable seat table. */
|
|
37
|
+
export interface LocalPlayersSnapshot {
|
|
38
|
+
maxSlots: number;
|
|
39
|
+
claimPrimary: "first" | "none";
|
|
40
|
+
slots: LocalPlayerSlot[];
|
|
41
|
+
}
|
|
42
|
+
/** Seats for couch co-op: devices hot-join into slots, each slot has its own input. */
|
|
43
|
+
export interface LocalPlayers {
|
|
44
|
+
/** Seat for a device that just produced input: its existing seat, the primary seat, a newly opened seat, or `null` when full. */
|
|
45
|
+
assign(deviceId: string): LocalPlayerAssignment | null;
|
|
46
|
+
/** Free a seat other than the primary; its device may hot-join again. */
|
|
47
|
+
release(slotId: string): boolean;
|
|
48
|
+
slots(): readonly LocalPlayerSlot[];
|
|
49
|
+
slot(slotId: string): LocalPlayerSlot | null;
|
|
50
|
+
slotForDevice(deviceId: string): LocalPlayerSlot | null;
|
|
51
|
+
/** The seat's user id and input, or `null` for an unknown seat. Seat 0 returns `ctx.player`'s id and `ctx.input`. */
|
|
52
|
+
local(slotId: string): {
|
|
53
|
+
userId: string;
|
|
54
|
+
input: InputSnapshot;
|
|
55
|
+
} | null;
|
|
56
|
+
/** Called after every assign that changes the table, release, restore and retune; returns an unsubscribe. */
|
|
57
|
+
subscribe(listener: () => void): () => void;
|
|
58
|
+
config(): Required<LocalPlayersConfig>;
|
|
59
|
+
/** Change seat count or claim policy mid-game; seats above a lowered `maxSlots` stay until released. */
|
|
60
|
+
retune(next: LocalPlayersConfig): void;
|
|
61
|
+
snapshot(): LocalPlayersSnapshot;
|
|
62
|
+
restore(snapshot: LocalPlayersSnapshot): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Seat table for local multiplayer on one screen.
|
|
66
|
+
* @capability local-players Couch co-op seats: devices hot-join into slots, each with its own user id and input snapshot.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createLocalPlayers(options: LocalPlayersOptions): LocalPlayers;
|
|
69
|
+
/**
|
|
70
|
+
* The seat table of a game context, created on first use with one seat for `ctx.player` and `ctx.input`.
|
|
71
|
+
* The shell retunes it from `defineGame({ localPlayers })` and hot-joins pads into it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function localPlayers(ctx: GameContext): LocalPlayers;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createInputSnapshot } from "./inputSnapshot.js";
|
|
2
|
+
const PRIMARY_SLOT = "slot:0";
|
|
3
|
+
/**
|
|
4
|
+
* Seat table for local multiplayer on one screen.
|
|
5
|
+
* @capability local-players Couch co-op seats: devices hot-join into slots, each with its own user id and input snapshot.
|
|
6
|
+
*/
|
|
7
|
+
export function createLocalPlayers(options) {
|
|
8
|
+
const userIdFor = options.userIdFor ?? ((index) => `${options.primaryUserId}:p${index + 1}`);
|
|
9
|
+
const primaryInput = options.primaryInput ?? createInputSnapshot();
|
|
10
|
+
let config = {
|
|
11
|
+
maxSlots: Math.max(1, Math.floor(options.maxSlots)),
|
|
12
|
+
claimPrimary: options.claimPrimary ?? "first",
|
|
13
|
+
};
|
|
14
|
+
let slots = [];
|
|
15
|
+
const inputs = new Map();
|
|
16
|
+
const listeners = new Set();
|
|
17
|
+
const changed = () => {
|
|
18
|
+
for (const listener of [...listeners])
|
|
19
|
+
listener();
|
|
20
|
+
};
|
|
21
|
+
const primarySlot = (deviceId) => Object.freeze({ slotId: PRIMARY_SLOT, index: 0, userId: options.primaryUserId, deviceId });
|
|
22
|
+
const reset = (next) => {
|
|
23
|
+
slots = next.map((slot) => Object.freeze({ ...slot }));
|
|
24
|
+
if (!slots.some((slot) => slot.index === 0))
|
|
25
|
+
slots.unshift(primarySlot(null));
|
|
26
|
+
slots.sort((a, b) => a.index - b.index);
|
|
27
|
+
for (const slotId of [...inputs.keys()]) {
|
|
28
|
+
if (!slots.some((slot) => slot.slotId === slotId))
|
|
29
|
+
inputs.delete(slotId);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
reset([]);
|
|
33
|
+
const inputFor = (slot) => {
|
|
34
|
+
if (slot.index === 0)
|
|
35
|
+
return primaryInput;
|
|
36
|
+
let input = inputs.get(slot.slotId);
|
|
37
|
+
if (input === undefined) {
|
|
38
|
+
input = createInputSnapshot();
|
|
39
|
+
inputs.set(slot.slotId, input);
|
|
40
|
+
}
|
|
41
|
+
return input;
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
assign(deviceId) {
|
|
45
|
+
const existing = slots.find((slot) => slot.deviceId === deviceId);
|
|
46
|
+
if (existing !== undefined)
|
|
47
|
+
return { slot: existing, joined: false };
|
|
48
|
+
if (config.claimPrimary === "first" || config.maxSlots === 1) {
|
|
49
|
+
const primary = slots[0];
|
|
50
|
+
if (primary.deviceId === null) {
|
|
51
|
+
slots[0] = primarySlot(deviceId);
|
|
52
|
+
changed();
|
|
53
|
+
return { slot: slots[0], joined: false };
|
|
54
|
+
}
|
|
55
|
+
if (config.maxSlots === 1)
|
|
56
|
+
return { slot: primary, joined: false };
|
|
57
|
+
}
|
|
58
|
+
if (slots.length >= config.maxSlots)
|
|
59
|
+
return null;
|
|
60
|
+
let index = 1;
|
|
61
|
+
while (slots.some((slot) => slot.index === index))
|
|
62
|
+
index += 1;
|
|
63
|
+
const slot = Object.freeze({ slotId: `slot:${index}`, index, userId: userIdFor(index), deviceId });
|
|
64
|
+
slots = [...slots, slot].sort((a, b) => a.index - b.index);
|
|
65
|
+
changed();
|
|
66
|
+
return { slot, joined: true };
|
|
67
|
+
},
|
|
68
|
+
release(slotId) {
|
|
69
|
+
if (slotId === PRIMARY_SLOT)
|
|
70
|
+
return false;
|
|
71
|
+
const next = slots.filter((slot) => slot.slotId !== slotId);
|
|
72
|
+
if (next.length === slots.length)
|
|
73
|
+
return false;
|
|
74
|
+
slots = next;
|
|
75
|
+
inputs.delete(slotId);
|
|
76
|
+
changed();
|
|
77
|
+
return true;
|
|
78
|
+
},
|
|
79
|
+
slots: () => slots,
|
|
80
|
+
slot: (slotId) => slots.find((slot) => slot.slotId === slotId) ?? null,
|
|
81
|
+
slotForDevice: (deviceId) => slots.find((slot) => slot.deviceId === deviceId) ?? null,
|
|
82
|
+
local(slotId) {
|
|
83
|
+
const slot = slots.find((candidate) => candidate.slotId === slotId);
|
|
84
|
+
return slot === undefined ? null : { userId: slot.userId, input: inputFor(slot) };
|
|
85
|
+
},
|
|
86
|
+
subscribe(listener) {
|
|
87
|
+
listeners.add(listener);
|
|
88
|
+
return () => {
|
|
89
|
+
listeners.delete(listener);
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
config: () => config,
|
|
93
|
+
retune(next) {
|
|
94
|
+
config = { maxSlots: Math.max(1, Math.floor(next.maxSlots)), claimPrimary: next.claimPrimary ?? config.claimPrimary };
|
|
95
|
+
changed();
|
|
96
|
+
},
|
|
97
|
+
snapshot() {
|
|
98
|
+
return { ...config, slots: slots.map((slot) => ({ ...slot })) };
|
|
99
|
+
},
|
|
100
|
+
restore(snapshot) {
|
|
101
|
+
config = { maxSlots: Math.max(1, Math.floor(snapshot.maxSlots)), claimPrimary: snapshot.claimPrimary };
|
|
102
|
+
reset(snapshot.slots);
|
|
103
|
+
changed();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const LOCAL_PLAYERS = new WeakMap();
|
|
108
|
+
/**
|
|
109
|
+
* The seat table of a game context, created on first use with one seat for `ctx.player` and `ctx.input`.
|
|
110
|
+
* The shell retunes it from `defineGame({ localPlayers })` and hot-joins pads into it.
|
|
111
|
+
*/
|
|
112
|
+
export function localPlayers(ctx) {
|
|
113
|
+
let players = LOCAL_PLAYERS.get(ctx);
|
|
114
|
+
if (players === undefined) {
|
|
115
|
+
players = createLocalPlayers({ maxSlots: 1, primaryUserId: ctx.player.userId, primaryInput: ctx.input });
|
|
116
|
+
LOCAL_PLAYERS.set(ctx, players);
|
|
117
|
+
}
|
|
118
|
+
return players;
|
|
119
|
+
}
|
|
@@ -2,7 +2,7 @@ import { type SaveBackend, type SaveStatus, type SaveTimers } from "../game/save
|
|
|
2
2
|
import type { WorldSnapshot } from "./worldSnapshot.js";
|
|
3
3
|
/**
|
|
4
4
|
* The narrow slice of a live `GameContext` a runtime save reads and writes — a
|
|
5
|
-
* `GameContext` satisfies it directly (`ctx.
|
|
5
|
+
* `GameContext` satisfies it directly (`ctx.state`/`ctx.restore`/`ctx.subscribe`).
|
|
6
6
|
* Depending on this instead of the full context keeps the save controller a deep,
|
|
7
7
|
* decoupled module: it captures and restores the *whole* opted-in world without
|
|
8
8
|
* knowing any subsystem.
|
|
@@ -10,6 +10,10 @@ import type { WorldSnapshot } from "./worldSnapshot.js";
|
|
|
10
10
|
export interface RuntimeSaveTarget {
|
|
11
11
|
snapshot(): WorldSnapshot;
|
|
12
12
|
hydrate(snapshot: WorldSnapshot): void;
|
|
13
|
+
/** Complete authoritative capture when the target distinguishes persistence from replication. */
|
|
14
|
+
state?(): WorldSnapshot;
|
|
15
|
+
/** Authoritative inverse of `state`; used together with it instead of replication hydration. */
|
|
16
|
+
restore?(state: WorldSnapshot): void;
|
|
13
17
|
subscribe(listener: () => void): () => void;
|
|
14
18
|
}
|
|
15
19
|
/** `"autosave"` writes a fresh capture on a trailing timer while the world changes (at most once per `autosaveMs`, so a never-idle world still persists); `"manual"` writes only on an explicit `save()`/`checkpoint()` — the save-point / quest-trigger model. */
|
|
@@ -39,8 +43,8 @@ export interface RuntimeSaveConfig {
|
|
|
39
43
|
export type RuntimeSaveOptions = Omit<RuntimeSaveConfig, "target">;
|
|
40
44
|
/**
|
|
41
45
|
* Whole-world save/load bound to a live world and a pluggable backend. `save()`
|
|
42
|
-
* captures `target.
|
|
43
|
-
* `target.
|
|
46
|
+
* captures `target.state()` and writes it; `load()` reads it back and
|
|
47
|
+
* calls `target.restore()`. Older targets use `snapshot()`/`hydrate()`. In `autosave` mode it also writes on a
|
|
44
48
|
* trailing timer while the world keeps changing. Named slots, versioned migration, and offline↔cloud
|
|
45
49
|
* (backend swap) all come for free from the underlying save store.
|
|
46
50
|
*/
|
|
@@ -51,13 +55,13 @@ export interface RuntimeSave {
|
|
|
51
55
|
save(): Promise<void>;
|
|
52
56
|
/** Semantic alias for `save()` — call it from a save point, a quest-complete or area-enter handler. */
|
|
53
57
|
checkpoint(): Promise<void>;
|
|
54
|
-
/** Load the active slot and hydrate the whole world; resolves `true` when a real save was applied, `false` for an empty slot. */
|
|
58
|
+
/** Load the active slot and hydrate the whole world; resolves `true` when a real save was applied, `false` for an empty slot or failed payload read. Failed reads never restore cached state. */
|
|
55
59
|
load(): Promise<boolean>;
|
|
56
|
-
/** Whether the active slot holds a saved world — gate a title-screen "Continue" on it. */
|
|
60
|
+
/** Whether the active slot holds a saved world — gate a title-screen "Continue" on it. Failed payload reads resolve false and report error status. */
|
|
57
61
|
hasSave(): Promise<boolean>;
|
|
58
62
|
/** Delete the active slot's save. */
|
|
59
63
|
clear(): Promise<void>;
|
|
60
|
-
/** Switch slot and load it, hydrating the world when that slot has a save. */
|
|
64
|
+
/** Switch slot and load it, hydrating the world when that slot has a save. Failed payload reads resolve false without restoring cached state. */
|
|
61
65
|
switchSlot(slot: string): Promise<boolean>;
|
|
62
66
|
/** The slots this game has written — back a load/save menu. */
|
|
63
67
|
slots(): Promise<string[]>;
|
|
@@ -48,7 +48,7 @@ export function createRuntimeSave(config) {
|
|
|
48
48
|
}
|
|
49
49
|
function persist() {
|
|
50
50
|
clearTimer();
|
|
51
|
-
store.set(config.target.snapshot());
|
|
51
|
+
store.set(config.target.state?.() ?? config.target.snapshot());
|
|
52
52
|
return store.save();
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
@@ -77,7 +77,10 @@ export function createRuntimeSave(config) {
|
|
|
77
77
|
return false;
|
|
78
78
|
restoring = true;
|
|
79
79
|
try {
|
|
80
|
-
config.target.
|
|
80
|
+
if (config.target.restore !== undefined)
|
|
81
|
+
config.target.restore(snapshot);
|
|
82
|
+
else
|
|
83
|
+
config.target.hydrate(snapshot);
|
|
81
84
|
}
|
|
82
85
|
finally {
|
|
83
86
|
restoring = false;
|
|
@@ -91,15 +94,18 @@ export function createRuntimeSave(config) {
|
|
|
91
94
|
save,
|
|
92
95
|
checkpoint: save,
|
|
93
96
|
async load() {
|
|
94
|
-
|
|
97
|
+
const snapshot = await store.load();
|
|
98
|
+
return store.status() !== "error" && applyLoaded(snapshot);
|
|
95
99
|
},
|
|
96
100
|
async hasSave() {
|
|
97
|
-
|
|
101
|
+
const snapshot = await store.load();
|
|
102
|
+
return store.status() !== "error" && hasContent(snapshot);
|
|
98
103
|
},
|
|
99
104
|
clear: () => store.clear(),
|
|
100
105
|
async switchSlot(slot) {
|
|
101
106
|
clearTimer();
|
|
102
|
-
|
|
107
|
+
const snapshot = await store.switchSlot(slot);
|
|
108
|
+
return store.status() !== "error" && applyLoaded(snapshot);
|
|
103
109
|
},
|
|
104
110
|
slots: () => store.slots(),
|
|
105
111
|
subscribe: (listener) => store.subscribe(listener),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EntityStore } from "../scene/entityStore.js";
|
|
2
|
+
import type { SimClock } from "../time/simClock.js";
|
|
2
3
|
import { type PoseHistory, type RenderPose } from "./poseInterpolation.js";
|
|
3
4
|
import { type SimAdvanceResult, type SimLoop, type SimulationConfig } from "./simLoop.js";
|
|
4
5
|
/** Where a registered stage runs inside one simulation step. */
|
|
@@ -7,7 +8,8 @@ export type SimStagePhase = "beforeMovement" | "afterMovement" | "afterTick";
|
|
|
7
8
|
export interface SimStage {
|
|
8
9
|
id: string;
|
|
9
10
|
phase: SimStagePhase;
|
|
10
|
-
|
|
11
|
+
/** dt is real seconds; gameDt is the scaled authoritative game-seconds for this step. */
|
|
12
|
+
run(dt: number, tick: number, gameDt: number): void;
|
|
11
13
|
}
|
|
12
14
|
/** Serializable `ctx.sim` state: loop position plus the interpolation pose history. */
|
|
13
15
|
export interface SimContextState {
|
|
@@ -21,8 +23,10 @@ export interface SimContextState {
|
|
|
21
23
|
*/
|
|
22
24
|
export interface SimContext {
|
|
23
25
|
readonly loop: SimLoop;
|
|
24
|
-
/** Advance
|
|
25
|
-
advance(realDt: number, body: (dt: number, tick: number) => void
|
|
26
|
+
/** Advance simulation and its game clock once per step. The callback receives real dt, tick and scaled game dt. Prediction can opt out of clock advancement. */
|
|
27
|
+
advance(realDt: number, body: (dt: number, tick: number, gameDt: number) => void, options?: {
|
|
28
|
+
advanceTime?: boolean;
|
|
29
|
+
}): SimAdvanceResult;
|
|
26
30
|
tick(): number;
|
|
27
31
|
/** Interpolation fraction for the current frame, 1 on a variable loop. */
|
|
28
32
|
alpha(): number;
|
|
@@ -42,4 +46,5 @@ export interface SimContext {
|
|
|
42
46
|
export declare function createSimContext(options: {
|
|
43
47
|
config?: SimulationConfig;
|
|
44
48
|
entities: EntityStore;
|
|
49
|
+
time?: Pick<SimClock, "advance">;
|
|
45
50
|
}): SimContext;
|
|
@@ -7,9 +7,10 @@ export function createSimContext(options) {
|
|
|
7
7
|
const poses = createPoseHistory({ snapDistance: loop.config().snapDistance });
|
|
8
8
|
const stages = new Map();
|
|
9
9
|
let advancing = false;
|
|
10
|
+
let currentGameDt = 0;
|
|
10
11
|
return {
|
|
11
12
|
loop,
|
|
12
|
-
advance(realDt, body) {
|
|
13
|
+
advance(realDt, body, stepOptions) {
|
|
13
14
|
if (advancing)
|
|
14
15
|
throw new Error("ctx.sim.advance is not reentrant");
|
|
15
16
|
advancing = true;
|
|
@@ -17,11 +18,14 @@ export function createSimContext(options) {
|
|
|
17
18
|
return loop.advance(realDt, (dt, tick) => {
|
|
18
19
|
if (loop.isFixed())
|
|
19
20
|
poses.beginStep(entities);
|
|
20
|
-
|
|
21
|
+
const gameDt = stepOptions?.advanceTime === false ? 0 : options.time?.advance(dt) ?? dt;
|
|
22
|
+
currentGameDt = gameDt;
|
|
23
|
+
body(dt, tick, gameDt);
|
|
21
24
|
});
|
|
22
25
|
}
|
|
23
26
|
finally {
|
|
24
27
|
advancing = false;
|
|
28
|
+
currentGameDt = 0;
|
|
25
29
|
}
|
|
26
30
|
},
|
|
27
31
|
tick: () => loop.tick(),
|
|
@@ -49,7 +53,7 @@ export function createSimContext(options) {
|
|
|
49
53
|
return;
|
|
50
54
|
const tick = loop.tick();
|
|
51
55
|
for (const stage of list)
|
|
52
|
-
stage.run(dt, tick);
|
|
56
|
+
stage.run(dt, tick, advancing ? currentGameDt : dt);
|
|
53
57
|
},
|
|
54
58
|
retune(config) {
|
|
55
59
|
loop.retune(config);
|
|
@@ -47,6 +47,8 @@ export function createContextSimSnapshot(ctx) {
|
|
|
47
47
|
capture: () => structuredClone(ctx.snapshot()),
|
|
48
48
|
restore: (data) => ctx.hydrate(structuredClone(data)),
|
|
49
49
|
});
|
|
50
|
+
registry.register({ id: "projectiles", capture: ctx.scene.entity.projectileState, restore: ctx.scene.entity.restoreProjectiles });
|
|
51
|
+
registry.register({ id: "environment", capture: ctx.environment.snapshot, restore: ctx.environment.restore });
|
|
50
52
|
registry.register({ id: "clock", capture: () => ctx.time.snapshot(), restore: (data) => ctx.time.hydrate(data) });
|
|
51
53
|
registry.register({ id: "sim", capture: () => ctx.sim.snapshot(), restore: (data) => ctx.sim.restore(data) });
|
|
52
54
|
if (rngStateOf(ctx.rng) !== null) {
|
|
@@ -63,16 +63,25 @@ export type GameRuntimeFeedView = {
|
|
|
63
63
|
entries: unknown[];
|
|
64
64
|
};
|
|
65
65
|
export type GameRuntimeTransport = {
|
|
66
|
+
/**
|
|
67
|
+
* Join a server. `sessionId` names this client session (one tab, one mounted view) so the host keeps
|
|
68
|
+
* the user a member while any of their sessions is still live.
|
|
69
|
+
*/
|
|
66
70
|
joinServer: (args: {
|
|
67
71
|
gameId: string;
|
|
68
72
|
serverId?: string;
|
|
69
73
|
role?: MultiplayerRole;
|
|
74
|
+
sessionId?: string;
|
|
70
75
|
}) => Promise<JoinServerOutcome>;
|
|
76
|
+
/** Leave a server. With `sessionId` only that session ends; the user leaves once no session remains. Without it the user leaves outright. */
|
|
71
77
|
leaveServer: (args: {
|
|
72
78
|
serverId: string;
|
|
79
|
+
sessionId?: string;
|
|
73
80
|
}) => Promise<void>;
|
|
74
81
|
runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>;
|
|
75
82
|
};
|
|
83
|
+
/** Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}). */
|
|
84
|
+
export declare function createTransportSessionId(): string;
|
|
76
85
|
export type FeedUnsubscribe = () => void;
|
|
77
86
|
export type GameRuntimeFeeds = {
|
|
78
87
|
subscribeServer: (serverId: string, onChange: (view: GameRuntimeServerView | null) => void) => FeedUnsubscribe;
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/** Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}). */
|
|
2
|
+
export function createTransportSessionId() {
|
|
3
|
+
const cryptoApi = globalThis.crypto;
|
|
4
|
+
if (cryptoApi?.randomUUID)
|
|
5
|
+
return cryptoApi.randomUUID();
|
|
6
|
+
return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
7
|
+
}
|
|
@@ -35,6 +35,8 @@ export interface WorldDiff {
|
|
|
35
35
|
*/
|
|
36
36
|
export interface WorldReplicatorOptions {
|
|
37
37
|
worldVersion?: () => number;
|
|
38
|
+
/** Cursor loaded with a persisted baseline; new commits continue above it. Default `0`. */
|
|
39
|
+
initialRevision?: number;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* Turns successive full {@link WorldSnapshot}s into per-client {@link WorldDiff}s. Each `commit()` re-reads the
|
|
@@ -10,7 +10,7 @@ const CORE_KEYS = new Set(["entities", "stats", "store"]);
|
|
|
10
10
|
export function createWorldReplicator(takeSnapshot, options = {}) {
|
|
11
11
|
const worldVersion = options.worldVersion;
|
|
12
12
|
let committedVersion;
|
|
13
|
-
let revision = 0;
|
|
13
|
+
let revision = options.initialRevision ?? 0;
|
|
14
14
|
const entities = new Map();
|
|
15
15
|
const removedEntities = new Map();
|
|
16
16
|
const stats = new Map();
|
|
@@ -16,6 +16,8 @@ export interface ModelDims {
|
|
|
16
16
|
}
|
|
17
17
|
export interface ModelAssetRef {
|
|
18
18
|
url: string;
|
|
19
|
+
/** Preserve a composed prefab's authored local origin instead of recentering its measured bounds. */
|
|
20
|
+
anchor?: "center" | "origin";
|
|
19
21
|
/** Measured at asset reindex: horizontal footprint, footprint center, and lowest Y in model space (pre-scale). Lets the shell auto-center and ground-snap corner-pivot kit models. */
|
|
20
22
|
dims?: ModelDims;
|
|
21
23
|
/** Opt-in compact triangle mesh extracted at asset reindex (see {@link "scene/collisionMesh".CollisionMeshData}) — feeds mesh-accurate hitboxes for concave models. */
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { DecisionAction, DecisionGraphRuntime } from "../ai/decisionGraph.js";
|
|
1
|
+
import type { Blackboard, DecisionAbort, DecisionAction, DecisionGraphRuntime } from "../ai/decisionGraph.js";
|
|
2
2
|
import { type InterestGateState } from "../ai/interestScheduler.js";
|
|
3
|
+
import { type PursuitState } from "../ai/pursuit.js";
|
|
4
|
+
import { type ThreatTable, type ThreatTableState } from "../ai/threat.js";
|
|
3
5
|
import { type PathFollowState, type PathProgress, type Waypoint } from "../nav/pathFollow.js";
|
|
4
6
|
import type { GameContext } from "../runtime/gameContext.js";
|
|
7
|
+
import type { GameDefinition } from "../game/defineGame.js";
|
|
5
8
|
/** Whether a behavior instance advances and writes pose (`active`), is temporarily suspended retaining
|
|
6
9
|
* state (`paused`), or is held off until re-enabled (`disabled`). */
|
|
7
10
|
export type BehaviorStatus = "active" | "paused" | "disabled";
|
|
@@ -13,14 +16,19 @@ export interface BehaviorActionContext {
|
|
|
13
16
|
}
|
|
14
17
|
/** Callback used by a registered decision graph behavior action. */
|
|
15
18
|
export type BehaviorAction = DecisionAction<BehaviorActionContext>;
|
|
19
|
+
/** Optional hooks for a registered behavior action set. */
|
|
20
|
+
export interface BehaviorActionOptions {
|
|
21
|
+
/** Called when an action running last think is pre-empted by another branch, to stop movement or release claims. */
|
|
22
|
+
onAbort?: DecisionAbort<BehaviorActionContext>;
|
|
23
|
+
}
|
|
16
24
|
/** Register the named actions used by decision graph behavior descriptors.
|
|
17
25
|
* @capability behavior-actions register named callbacks for decision graph behavior descriptors
|
|
18
26
|
*/
|
|
19
|
-
export declare function registerBehaviorActions(id: string, actions: Record<string, BehaviorAction
|
|
27
|
+
export declare function registerBehaviorActions(id: string, actions: Record<string, BehaviorAction>, options?: BehaviorActionOptions): () => void;
|
|
20
28
|
/**
|
|
21
29
|
* Catch-up policy applied when a paused instance resumes. `freeze` (default, deterministic) discards
|
|
22
30
|
* the time spent paused; `advance` silently fast-forwards the instance by the paused duration without
|
|
23
|
-
* emitting intermediate pose writes. Only patrol honors `advance`; wander
|
|
31
|
+
* emitting intermediate pose writes. Only patrol honors `advance`; wander/pursue treat it as `freeze`.
|
|
24
32
|
*/
|
|
25
33
|
export type BehaviorResumePolicy = "freeze" | "advance";
|
|
26
34
|
/** Serializable snapshot of one behavior instance — round-trips exactly through
|
|
@@ -32,15 +40,30 @@ export type BehaviorSnapshot = {
|
|
|
32
40
|
readonly kind: "wander";
|
|
33
41
|
readonly origin: Waypoint;
|
|
34
42
|
readonly target: Waypoint | null;
|
|
35
|
-
} | {
|
|
43
|
+
} | PursueBehaviorSnapshot | {
|
|
36
44
|
readonly kind: "decisionGraph";
|
|
37
45
|
readonly graph: ReturnType<DecisionGraphRuntime["snapshot"]>;
|
|
38
46
|
readonly gate: InterestGateState;
|
|
47
|
+
readonly blackboard?: Blackboard;
|
|
48
|
+
/** Seconds accrued since the last think. */
|
|
49
|
+
readonly pending?: number;
|
|
39
50
|
};
|
|
51
|
+
/** Plain pursuit progress, including its original spawn home and scheduled elapsed time. */
|
|
52
|
+
export interface PursueBehaviorSnapshot {
|
|
53
|
+
readonly kind: "pursue";
|
|
54
|
+
readonly home: Waypoint;
|
|
55
|
+
readonly targetId: string | null;
|
|
56
|
+
readonly explicitTargetId: string | null;
|
|
57
|
+
readonly returning: boolean;
|
|
58
|
+
readonly pursuit: PursuitState;
|
|
59
|
+
readonly gate: InterestGateState;
|
|
60
|
+
readonly pending: number;
|
|
61
|
+
readonly threat?: ThreatTableState;
|
|
62
|
+
}
|
|
40
63
|
/** Inspection readout for editor/debug tooling, from {@link BehaviorControl.inspect}/{@link BehaviorControl.list}. */
|
|
41
64
|
export interface BehaviorInspection {
|
|
42
65
|
id: string;
|
|
43
|
-
kind: "patrol" | "wander" | "decisionGraph";
|
|
66
|
+
kind: "patrol" | "wander" | "decisionGraph" | "pursue";
|
|
44
67
|
status: BehaviorStatus;
|
|
45
68
|
reason: string | null;
|
|
46
69
|
}
|
|
@@ -72,15 +95,21 @@ export interface BehaviorControl {
|
|
|
72
95
|
inspect(id: string): BehaviorInspection | null;
|
|
73
96
|
/** Inspect every live instance (bounded by spawned behavior entities). */
|
|
74
97
|
list(): BehaviorInspection[];
|
|
98
|
+
/** The live, persistent blackboard of a decision graph instance for perception and game systems to write facts into, or `null`. */
|
|
99
|
+
blackboard(id: string): Blackboard | null;
|
|
100
|
+
/** Optional pursuit threat table; explicit `setTarget` wins over taunt/threat and proximity. */
|
|
101
|
+
threat(id: string): ThreatTable | null;
|
|
102
|
+
/** Clear pursuit target/cooldown/threat and reset its cadence, retaining the original home. */
|
|
103
|
+
reset(id: string): boolean;
|
|
75
104
|
}
|
|
76
105
|
/**
|
|
77
|
-
* Advance every spawned entity carrying a
|
|
106
|
+
* Advance every spawned entity carrying a patrol, wander, pursuit or decision graph behavior one tick — the
|
|
78
107
|
* engine reads the descriptor, keeps the per-entity nav state itself, and poses the entity, so ambient
|
|
79
108
|
* traffic and idle NPC routes are register-once (attach the behavior at spawn) instead of a per-game
|
|
80
109
|
* per-frame `advancePathFollow` + `setPose` loop. Instances that are paused or disabled through
|
|
81
110
|
* {@link behaviorControl} retain their state and are skipped. The shell/host call this each frame; a game never does.
|
|
82
111
|
*
|
|
83
|
-
* @capability behavior-tick auto-advance patrol
|
|
112
|
+
* @capability behavior-tick auto-advance spawned patrol, wander, pursuit and decision graph behaviors with lifecycle control
|
|
84
113
|
*/
|
|
85
114
|
export declare function advanceBehaviors(ctx: GameContext, dt: number): void;
|
|
86
115
|
/**
|
|
@@ -91,3 +120,5 @@ export declare function advanceBehaviors(ctx: GameContext, dt: number): void;
|
|
|
91
120
|
* @capability behavior-control pause/disable/resume/seek/inspect behavior instances per entity
|
|
92
121
|
*/
|
|
93
122
|
export declare function behaviorControl(ctx: GameContext): BehaviorControl;
|
|
123
|
+
/** @internal Register persistence before boot-time restore without constructing an AI runtime. */
|
|
124
|
+
export declare function installPursuitPersistence(ctx: GameContext, aoiRadius?: number, eligible?: NonNullable<GameDefinition["pursuit"]>["eligible"]): void;
|