@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,8 +1,15 @@
|
|
|
1
1
|
import { createDecisionGraphRuntime } from "../ai/decisionGraph.js";
|
|
2
|
-
import { advanceInterestGate, createInterestGateState } from "../ai/interestScheduler.js";
|
|
2
|
+
import { advanceInterestGate, createInterestGateState, interestPhase } from "../ai/interestScheduler.js";
|
|
3
|
+
import { advancePursuit, armPursuit, createPursuitState } from "../ai/pursuit.js";
|
|
4
|
+
import { acquireTarget } from "../ai/targetAcquisition.js";
|
|
5
|
+
import { createThreatTable } from "../ai/threat.js";
|
|
3
6
|
import { advancePathFollow, createPathFollow, pathFollowSeek, } from "../nav/pathFollow.js";
|
|
4
7
|
import { resolveWalkerStep } from "../movement/solidObstacles.js";
|
|
5
8
|
import { perContext } from "../runtime/perContext.js";
|
|
9
|
+
import { notifyAfter } from "../store/changeSignal.js";
|
|
10
|
+
import { visibleEntityIds } from "../runtime/worldProjection.js";
|
|
11
|
+
import { behaviorDataEqual } from "./behaviors.js";
|
|
12
|
+
import { distanceBetween } from "./spatial.js";
|
|
6
13
|
const DEFAULT_WANDER_SPEED = 1.5;
|
|
7
14
|
const WANDER_ARRIVAL = 0.6;
|
|
8
15
|
/** Below this share of its intended step a wanderer counts as blocked and re-rolls its target. */
|
|
@@ -11,14 +18,19 @@ const behaviorActions = new Map();
|
|
|
11
18
|
/** Register the named actions used by decision graph behavior descriptors.
|
|
12
19
|
* @capability behavior-actions register named callbacks for decision graph behavior descriptors
|
|
13
20
|
*/
|
|
14
|
-
export function registerBehaviorActions(id, actions) {
|
|
15
|
-
|
|
21
|
+
export function registerBehaviorActions(id, actions, options = {}) {
|
|
22
|
+
const entry = { actions, options };
|
|
23
|
+
behaviorActions.set(id, entry);
|
|
16
24
|
return () => {
|
|
17
|
-
if (behaviorActions.get(id) ===
|
|
25
|
+
if (behaviorActions.get(id) === entry)
|
|
18
26
|
behaviorActions.delete(id);
|
|
19
27
|
};
|
|
20
28
|
}
|
|
21
|
-
|
|
29
|
+
// Bind unsaved policy without eagerly constructing the lazy AI runtime before boot-time restore.
|
|
30
|
+
const pursuitPolicies = new WeakMap();
|
|
31
|
+
function thinkCadence(behavior) {
|
|
32
|
+
return { wakeRadius: Number.POSITIVE_INFINITY, activeInterval: Math.max(0, behavior.thinkInterval ?? 0) };
|
|
33
|
+
}
|
|
22
34
|
function patrolOf(entity) {
|
|
23
35
|
return entity.behaviors.find((b) => b.kind === "patrol") ?? null;
|
|
24
36
|
}
|
|
@@ -30,12 +42,20 @@ function decisionGraphOf(entity) {
|
|
|
30
42
|
}
|
|
31
43
|
const runtimeOf = perContext((ctx) => {
|
|
32
44
|
const nav = new Map();
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
const descriptors = new Map();
|
|
46
|
+
const refreshId = (id) => {
|
|
47
|
+
const entity = ctx.scene.entity.get(id);
|
|
48
|
+
const descriptor = entity === null ? undefined : patrolOf(entity) ?? wanderOf(entity) ?? decisionGraphOf(entity)
|
|
49
|
+
?? entity.behaviors.find(behavior => behavior.kind === "pursue");
|
|
50
|
+
if (entity !== null && descriptor !== undefined && nav.has(id) && behaviorDataEqual(descriptors.get(id), descriptor))
|
|
51
|
+
return;
|
|
52
|
+
const previous = nav.get(id);
|
|
53
|
+
nav.delete(id);
|
|
54
|
+
descriptors.delete(id);
|
|
55
|
+
try {
|
|
56
|
+
if (entity === null || descriptor === undefined)
|
|
57
|
+
return;
|
|
58
|
+
descriptors.set(id, descriptor);
|
|
39
59
|
const patrol = patrolOf(entity);
|
|
40
60
|
if (patrol !== null) {
|
|
41
61
|
const config = { waypoints: patrol.waypoints, speed: patrol.speed, loop: patrol.loop };
|
|
@@ -48,7 +68,7 @@ const runtimeOf = perContext((ctx) => {
|
|
|
48
68
|
reason: null,
|
|
49
69
|
pausedElapsed: 0,
|
|
50
70
|
});
|
|
51
|
-
|
|
71
|
+
return;
|
|
52
72
|
}
|
|
53
73
|
const wander = wanderOf(entity);
|
|
54
74
|
if (wander !== null) {
|
|
@@ -62,32 +82,165 @@ const runtimeOf = perContext((ctx) => {
|
|
|
62
82
|
reason: null,
|
|
63
83
|
pausedElapsed: 0,
|
|
64
84
|
});
|
|
65
|
-
|
|
85
|
+
return;
|
|
66
86
|
}
|
|
67
87
|
const decisionGraph = decisionGraphOf(entity);
|
|
68
88
|
if (decisionGraph !== null) {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
89
|
+
const registered = behaviorActions.get(decisionGraph.actions);
|
|
90
|
+
if (registered === undefined)
|
|
91
|
+
return;
|
|
92
|
+
const cadence = thinkCadence(decisionGraph);
|
|
72
93
|
nav.set(entity.id, {
|
|
73
94
|
kind: "decisionGraph",
|
|
74
95
|
behavior: decisionGraph,
|
|
75
|
-
runtime: createDecisionGraphRuntime(decisionGraph.graph, actions
|
|
76
|
-
|
|
96
|
+
runtime: createDecisionGraphRuntime(decisionGraph.graph, registered.actions, {
|
|
97
|
+
rng: () => ctx.rng(),
|
|
98
|
+
...(registered.options.onAbort === undefined ? {} : { onAbort: registered.options.onAbort }),
|
|
99
|
+
}),
|
|
100
|
+
gate: createInterestGateState(cadence, ctx.rng()),
|
|
101
|
+
cadence,
|
|
102
|
+
blackboard: { ...(decisionGraph.blackboard ?? {}) },
|
|
103
|
+
pending: 0,
|
|
77
104
|
status: "active",
|
|
78
105
|
reason: null,
|
|
79
106
|
pausedElapsed: 0,
|
|
80
107
|
});
|
|
81
108
|
}
|
|
109
|
+
const pursuing = entity.behaviors.find((behavior) => behavior.kind === "pursue");
|
|
110
|
+
if (pursuing?.kind === "pursue" && !nav.has(entity.id)) {
|
|
111
|
+
const cadence = { wakeRadius: Infinity, activeInterval: pursuing.thinkInterval ?? .1 };
|
|
112
|
+
const gate = createInterestGateState(cadence, interestPhase(entity.id));
|
|
113
|
+
gate.state = "active";
|
|
114
|
+
nav.set(entity.id, { kind: "pursue", behavior: pursuing,
|
|
115
|
+
home: [...(ctx.scene.entity.spawnPoseOf(entity.id)?.position ?? entity.position)],
|
|
116
|
+
targetId: null, returning: false, pursuit: createPursuitState(),
|
|
117
|
+
gate, cadence, pending: 0,
|
|
118
|
+
status: "active", reason: null, pausedElapsed: 0,
|
|
119
|
+
...(pursuing.threat === undefined ? {} : { threat: createThreatTable(pursuing.threat) }) });
|
|
120
|
+
}
|
|
82
121
|
}
|
|
83
|
-
|
|
122
|
+
finally {
|
|
123
|
+
if (previous?.kind === "decisionGraph")
|
|
124
|
+
previous.runtime.abort({ ctx, entityId: id, dt: 0 }, previous.blackboard);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const refresh = () => {
|
|
128
|
+
const live = new Set();
|
|
129
|
+
for (const entity of ctx.scene.entity.list()) {
|
|
130
|
+
live.add(entity.id);
|
|
131
|
+
refreshId(entity.id);
|
|
132
|
+
}
|
|
133
|
+
for (const id of descriptors.keys())
|
|
84
134
|
if (!live.has(id))
|
|
85
|
-
|
|
135
|
+
refreshId(id);
|
|
86
136
|
};
|
|
87
|
-
ctx.scene.entity.
|
|
137
|
+
ctx.scene.entity.subscribeBehaviors(refreshId);
|
|
88
138
|
refresh();
|
|
89
139
|
return { nav };
|
|
90
140
|
});
|
|
141
|
+
function resetPursue(ctx, id, entry) {
|
|
142
|
+
entry.targetId = null;
|
|
143
|
+
entry.returning = false;
|
|
144
|
+
entry.pursuit.attackCooldown = 0;
|
|
145
|
+
entry.pending = 0;
|
|
146
|
+
entry.gate = createInterestGateState(entry.cadence, interestPhase(id));
|
|
147
|
+
entry.gate.state = "active";
|
|
148
|
+
entry.threat?.clear();
|
|
149
|
+
ctx.scene.entity.setTarget(id, null);
|
|
150
|
+
}
|
|
151
|
+
function object(value) {
|
|
152
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
153
|
+
}
|
|
154
|
+
function nonnegative(value) {
|
|
155
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
156
|
+
}
|
|
157
|
+
function validPursueSnapshot(value) {
|
|
158
|
+
if (!object(value) || value.kind !== "pursue" || !Array.isArray(value.home) || value.home.length !== 3
|
|
159
|
+
|| !value.home.every(n => typeof n === "number" && Number.isFinite(n))
|
|
160
|
+
|| !(value.targetId === null || typeof value.targetId === "string")
|
|
161
|
+
|| !(value.explicitTargetId === null || typeof value.explicitTargetId === "string") || typeof value.returning !== "boolean"
|
|
162
|
+
|| !object(value.pursuit) || !nonnegative(value.pursuit.attackCooldown) || !nonnegative(value.pending)
|
|
163
|
+
|| !object(value.gate) || !(value.gate.state === "active" || value.gate.state === "dormant")
|
|
164
|
+
|| !nonnegative(value.gate.clock) || !nonnegative(value.gate.awakeHold)
|
|
165
|
+
|| !nonnegative(value.gate.phase) || value.gate.phase >= 1)
|
|
166
|
+
return false;
|
|
167
|
+
if (value.threat !== undefined) {
|
|
168
|
+
if (!object(value.threat) || !Array.isArray(value.threat.entries) || !nonnegative(value.threat.forcedRemaining)
|
|
169
|
+
|| !(value.threat.forcedSource === null || typeof value.threat.forcedSource === "string"))
|
|
170
|
+
return false;
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
for (const entry of value.threat.entries) {
|
|
173
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || seen.has(entry[0])
|
|
174
|
+
|| typeof entry[1] !== "number" || !Number.isFinite(entry[1]))
|
|
175
|
+
return false;
|
|
176
|
+
seen.add(entry[0]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
function restorePursue(entry, snapshot) {
|
|
182
|
+
entry.home = [...snapshot.home];
|
|
183
|
+
entry.targetId = snapshot.targetId;
|
|
184
|
+
entry.returning = snapshot.returning;
|
|
185
|
+
entry.pursuit = { ...snapshot.pursuit };
|
|
186
|
+
entry.gate = { ...snapshot.gate };
|
|
187
|
+
entry.pending = snapshot.pending;
|
|
188
|
+
if (entry.threat !== undefined && snapshot.threat !== undefined)
|
|
189
|
+
entry.threat.restore(snapshot.threat);
|
|
190
|
+
else
|
|
191
|
+
entry.threat?.clear();
|
|
192
|
+
}
|
|
193
|
+
function stepPursue(ctx, id, entry, dt) {
|
|
194
|
+
const entity = ctx.scene.entity.get(id);
|
|
195
|
+
if (entity === null)
|
|
196
|
+
return;
|
|
197
|
+
const { behavior, home } = entry;
|
|
198
|
+
const homeDistance = distanceBetween(entity.position, home);
|
|
199
|
+
const policy = pursuitPolicies.get(ctx);
|
|
200
|
+
const eligible = (candidate) => candidate !== id && ctx.scene.entity.get(candidate) !== null
|
|
201
|
+
&& ctx.scene.entity.canReceive(candidate, behavior.attack.effect, behavior.attack.amount) === null
|
|
202
|
+
&& (policy === undefined || policy(ctx, id, candidate) === true);
|
|
203
|
+
entry.threat?.decay(dt);
|
|
204
|
+
if (!entry.returning) {
|
|
205
|
+
const explicit = ctx.scene.entity.getTarget(id);
|
|
206
|
+
const forced = entry.threat?.forcedTarget() ?? null;
|
|
207
|
+
if (explicit !== null && eligible(explicit))
|
|
208
|
+
entry.targetId = explicit;
|
|
209
|
+
else if (forced !== null && eligible(forced))
|
|
210
|
+
entry.targetId = forced;
|
|
211
|
+
else {
|
|
212
|
+
const nearby = ctx.scene.entity.inRadius(id, behavior.aggroRadius, candidate => ctx.scene.entity.get(candidate)?.role === "player");
|
|
213
|
+
if (entry.targetId !== null && !nearby.includes(entry.targetId))
|
|
214
|
+
nearby.push(entry.targetId);
|
|
215
|
+
const held = entry.targetId;
|
|
216
|
+
entry.targetId = acquireTarget({ candidates: () => nearby, distance: ctx.scene.entity.distance,
|
|
217
|
+
range: (_self, candidate) => candidate === held ? Infinity : behavior.aggroRadius,
|
|
218
|
+
eligible: (_self, candidate) => eligible(candidate), score: (_self, candidate) => entry.threat?.threatOf(candidate) ?? 0,
|
|
219
|
+
tieBreak: (a, b) => (ctx.scene.entity.distance(id, a) ?? Infinity) - (ctx.scene.entity.distance(id, b) ?? Infinity)
|
|
220
|
+
|| (a < b ? -1 : a > b ? 1 : 0),
|
|
221
|
+
}, id, held).targetId;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const targetDistance = entry.targetId === null ? null : ctx.scene.entity.distance(id, entry.targetId);
|
|
225
|
+
const action = advancePursuit(entry.pursuit, dt, targetDistance, behavior.reach, behavior.cooldownMode, homeDistance, behavior.leashRange);
|
|
226
|
+
if (action === "leash" || (targetDistance === null && homeDistance > .1))
|
|
227
|
+
entry.returning = true;
|
|
228
|
+
if (entry.returning) {
|
|
229
|
+
entry.targetId = null;
|
|
230
|
+
ctx.scene.entity.setTarget(id, null);
|
|
231
|
+
ctx.scene.entity.moveTowardCommit(id, home, { speed: behavior.speed, stopDistance: 0, dt, face: true });
|
|
232
|
+
const position = ctx.scene.entity.get(id)?.position;
|
|
233
|
+
if (position !== undefined && distanceBetween(position, home) <= .1)
|
|
234
|
+
resetPursue(ctx, id, entry);
|
|
235
|
+
}
|
|
236
|
+
else if (action === "pursue" && entry.targetId !== null) {
|
|
237
|
+
ctx.scene.entity.moveTowardCommit(id, entry.targetId, { speed: behavior.speed, stopDistance: behavior.reach, dt, face: true });
|
|
238
|
+
}
|
|
239
|
+
else if (action === "attack" && entry.targetId !== null) {
|
|
240
|
+
ctx.scene.entity.effect({ from: id, to: entry.targetId, effect: behavior.attack.effect, via: { amount: behavior.attack.amount } });
|
|
241
|
+
armPursuit(entry.pursuit, behavior.attack.intervalSec);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
91
244
|
function stepWander(ctx, id, nav, dt) {
|
|
92
245
|
const entity = ctx.scene.entity.get(id);
|
|
93
246
|
if (entity === null)
|
|
@@ -143,13 +296,13 @@ function posePatrol(ctx, id, nav, dt) {
|
|
|
143
296
|
});
|
|
144
297
|
}
|
|
145
298
|
/**
|
|
146
|
-
* Advance every spawned entity carrying a
|
|
299
|
+
* Advance every spawned entity carrying a patrol, wander, pursuit or decision graph behavior one tick — the
|
|
147
300
|
* engine reads the descriptor, keeps the per-entity nav state itself, and poses the entity, so ambient
|
|
148
301
|
* traffic and idle NPC routes are register-once (attach the behavior at spawn) instead of a per-game
|
|
149
302
|
* per-frame `advancePathFollow` + `setPose` loop. Instances that are paused or disabled through
|
|
150
303
|
* {@link behaviorControl} retain their state and are skipped. The shell/host call this each frame; a game never does.
|
|
151
304
|
*
|
|
152
|
-
* @capability behavior-tick auto-advance patrol
|
|
305
|
+
* @capability behavior-tick auto-advance spawned patrol, wander, pursuit and decision graph behaviors with lifecycle control
|
|
153
306
|
*/
|
|
154
307
|
export function advanceBehaviors(ctx, dt) {
|
|
155
308
|
if (dt <= 0)
|
|
@@ -157,10 +310,14 @@ export function advanceBehaviors(ctx, dt) {
|
|
|
157
310
|
const { nav } = runtimeOf(ctx);
|
|
158
311
|
if (nav.size === 0)
|
|
159
312
|
return;
|
|
313
|
+
let pursuitChanged = false;
|
|
160
314
|
for (const [id, entry] of nav) {
|
|
161
315
|
if (entry.status !== "active") {
|
|
162
|
-
if (entry.status === "paused")
|
|
316
|
+
if (entry.status === "paused") {
|
|
163
317
|
entry.pausedElapsed += dt;
|
|
318
|
+
if (entry.kind === "pursue")
|
|
319
|
+
pursuitChanged = true;
|
|
320
|
+
}
|
|
164
321
|
continue;
|
|
165
322
|
}
|
|
166
323
|
if (entry.kind === "patrol") {
|
|
@@ -170,19 +327,30 @@ export function advanceBehaviors(ctx, dt) {
|
|
|
170
327
|
else if (entry.kind === "wander") {
|
|
171
328
|
stepWander(ctx, id, entry, dt);
|
|
172
329
|
}
|
|
330
|
+
else if (entry.kind === "pursue") {
|
|
331
|
+
entry.pending += dt;
|
|
332
|
+
pursuitChanged = true;
|
|
333
|
+
if (!advanceInterestGate(entry.gate, entry.cadence, dt, { proximity: 0 }).active)
|
|
334
|
+
continue;
|
|
335
|
+
const elapsed = entry.pending;
|
|
336
|
+
entry.pending = 0;
|
|
337
|
+
stepPursue(ctx, id, entry, elapsed);
|
|
338
|
+
}
|
|
173
339
|
else {
|
|
174
340
|
const entity = ctx.scene.entity.get(id);
|
|
175
341
|
if (entity === null)
|
|
176
342
|
continue;
|
|
177
|
-
|
|
343
|
+
entry.pending += dt;
|
|
344
|
+
const step = advanceInterestGate(entry.gate, entry.cadence, dt, { proximity: 0 });
|
|
178
345
|
if (!step.active)
|
|
179
346
|
continue;
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
entry.runtime.restore({ runningPath: null });
|
|
347
|
+
const elapsed = entry.pending;
|
|
348
|
+
entry.pending = 0;
|
|
349
|
+
entry.runtime.tick({ ctx, entityId: id, dt: elapsed }, entry.blackboard, elapsed);
|
|
184
350
|
}
|
|
185
351
|
}
|
|
352
|
+
if (pursuitChanged)
|
|
353
|
+
ctx.touch();
|
|
186
354
|
}
|
|
187
355
|
/**
|
|
188
356
|
* Obtain the per-context {@link BehaviorControl} surface for suspending, resuming, seeking, serializing,
|
|
@@ -199,6 +367,8 @@ export function behaviorControl(ctx) {
|
|
|
199
367
|
return false;
|
|
200
368
|
entry.status = status;
|
|
201
369
|
entry.reason = reason;
|
|
370
|
+
if (entry.kind === "pursue")
|
|
371
|
+
ctx.touch();
|
|
202
372
|
return true;
|
|
203
373
|
};
|
|
204
374
|
return {
|
|
@@ -211,6 +381,8 @@ export function behaviorControl(ctx) {
|
|
|
211
381
|
entry.status = "paused";
|
|
212
382
|
entry.reason = reason ?? null;
|
|
213
383
|
entry.pausedElapsed = 0;
|
|
384
|
+
if (entry.kind === "pursue")
|
|
385
|
+
ctx.touch();
|
|
214
386
|
return true;
|
|
215
387
|
},
|
|
216
388
|
resume: (id, policy = "freeze") => {
|
|
@@ -225,6 +397,8 @@ export function behaviorControl(ctx) {
|
|
|
225
397
|
entry.status = "active";
|
|
226
398
|
entry.reason = null;
|
|
227
399
|
entry.pausedElapsed = 0;
|
|
400
|
+
if (entry.kind === "pursue")
|
|
401
|
+
ctx.touch();
|
|
228
402
|
return true;
|
|
229
403
|
},
|
|
230
404
|
disable: (id, reason) => setStatus(id, "disabled", reason ?? null),
|
|
@@ -235,6 +409,8 @@ export function behaviorControl(ctx) {
|
|
|
235
409
|
entry.status = "active";
|
|
236
410
|
entry.reason = null;
|
|
237
411
|
entry.pausedElapsed = 0;
|
|
412
|
+
if (entry.kind === "pursue")
|
|
413
|
+
ctx.touch();
|
|
238
414
|
return true;
|
|
239
415
|
},
|
|
240
416
|
seek: (id, progress) => {
|
|
@@ -252,7 +428,12 @@ export function behaviorControl(ctx) {
|
|
|
252
428
|
return { kind: "patrol", state: { ...entry.state } };
|
|
253
429
|
if (entry.kind === "wander")
|
|
254
430
|
return { kind: "wander", origin: [...entry.origin], target: entry.target === null ? null : [...entry.target] };
|
|
255
|
-
|
|
431
|
+
if (entry.kind === "pursue")
|
|
432
|
+
return { kind: "pursue", home: [...entry.home], targetId: entry.targetId,
|
|
433
|
+
explicitTargetId: ctx.scene.entity.getTarget(id),
|
|
434
|
+
returning: entry.returning, pursuit: { ...entry.pursuit }, gate: { ...entry.gate }, pending: entry.pending,
|
|
435
|
+
...(entry.threat === undefined ? {} : { threat: entry.threat.snapshot() }) };
|
|
436
|
+
return { kind: "decisionGraph", graph: entry.runtime.snapshot(), gate: { ...entry.gate }, blackboard: { ...entry.blackboard }, pending: entry.pending };
|
|
256
437
|
},
|
|
257
438
|
restore: (id, snapshot) => {
|
|
258
439
|
const entry = nav.get(id);
|
|
@@ -265,6 +446,8 @@ export function behaviorControl(ctx) {
|
|
|
265
446
|
if (entry.kind === "decisionGraph" && snapshot.kind === "decisionGraph") {
|
|
266
447
|
entry.runtime.restore(snapshot.graph);
|
|
267
448
|
entry.gate = { ...snapshot.gate };
|
|
449
|
+
entry.blackboard = { ...(snapshot.blackboard ?? entry.behavior.blackboard ?? {}) };
|
|
450
|
+
entry.pending = snapshot.pending ?? 0;
|
|
268
451
|
return true;
|
|
269
452
|
}
|
|
270
453
|
if (entry.kind === "wander" && snapshot.kind === "wander") {
|
|
@@ -272,6 +455,14 @@ export function behaviorControl(ctx) {
|
|
|
272
455
|
entry.target = snapshot.target === null ? null : [...snapshot.target];
|
|
273
456
|
return true;
|
|
274
457
|
}
|
|
458
|
+
if (entry.kind === "pursue" && snapshot.kind === "pursue") {
|
|
459
|
+
if (!validPursueSnapshot(snapshot))
|
|
460
|
+
return false;
|
|
461
|
+
restorePursue(entry, snapshot);
|
|
462
|
+
ctx.scene.entity.setTarget(id, snapshot.explicitTargetId);
|
|
463
|
+
ctx.touch();
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
275
466
|
return false;
|
|
276
467
|
},
|
|
277
468
|
inspect: (id) => {
|
|
@@ -286,5 +477,77 @@ export function behaviorControl(ctx) {
|
|
|
286
477
|
out.push({ id, kind: entry.kind, status: entry.status, reason: entry.reason });
|
|
287
478
|
return out;
|
|
288
479
|
},
|
|
480
|
+
blackboard: (id) => {
|
|
481
|
+
const entry = nav.get(id);
|
|
482
|
+
return entry?.kind === "decisionGraph" ? entry.blackboard : null;
|
|
483
|
+
},
|
|
484
|
+
threat: (id) => {
|
|
485
|
+
const entry = nav.get(id);
|
|
486
|
+
if (entry?.kind !== "pursue" || entry.threat === undefined)
|
|
487
|
+
return null;
|
|
488
|
+
return entry.threatControl ??= notifyAfter(entry.threat, ["add", "set", "taunt", "remove", "clear", "restore", "decay"], ctx.touch);
|
|
489
|
+
},
|
|
490
|
+
reset: (id) => {
|
|
491
|
+
const entry = nav.get(id);
|
|
492
|
+
if (entry?.kind !== "pursue")
|
|
493
|
+
return false;
|
|
494
|
+
resetPursue(ctx, id, entry);
|
|
495
|
+
ctx.touch();
|
|
496
|
+
return true;
|
|
497
|
+
},
|
|
289
498
|
};
|
|
290
499
|
}
|
|
500
|
+
/** @internal Register persistence before boot-time restore without constructing an AI runtime. */
|
|
501
|
+
export function installPursuitPersistence(ctx, aoiRadius, eligible) {
|
|
502
|
+
if (eligible !== undefined)
|
|
503
|
+
pursuitPolicies.set(ctx, eligible);
|
|
504
|
+
ctx.game.registerReplicate?.({
|
|
505
|
+
key: "pursuitBehaviors",
|
|
506
|
+
snapshot() {
|
|
507
|
+
const control = behaviorControl(ctx);
|
|
508
|
+
const instances = [];
|
|
509
|
+
for (const [id, entry] of runtimeOf(ctx).nav) {
|
|
510
|
+
if (entry.kind !== "pursue")
|
|
511
|
+
continue;
|
|
512
|
+
instances.push({ id, snapshot: control.serialize(id),
|
|
513
|
+
status: entry.status, reason: entry.reason, pausedElapsed: entry.pausedElapsed });
|
|
514
|
+
}
|
|
515
|
+
return { version: 1, instances };
|
|
516
|
+
},
|
|
517
|
+
decode(raw) {
|
|
518
|
+
if (!object(raw) || raw.version !== 1 || !Array.isArray(raw.instances))
|
|
519
|
+
return null;
|
|
520
|
+
const seen = new Set();
|
|
521
|
+
for (const value of raw.instances) {
|
|
522
|
+
if (!object(value) || typeof value.id !== "string" || seen.has(value.id)
|
|
523
|
+
|| !validPursueSnapshot(value.snapshot) || !(value.status === "active" || value.status === "paused" || value.status === "disabled")
|
|
524
|
+
|| !(value.reason === null || typeof value.reason === "string") || !nonnegative(value.pausedElapsed)
|
|
525
|
+
|| !ctx.scene.entity.get(value.id)?.behaviors.some(behavior => behavior.kind === "pursue"))
|
|
526
|
+
return null;
|
|
527
|
+
seen.add(value.id);
|
|
528
|
+
}
|
|
529
|
+
return structuredClone(raw);
|
|
530
|
+
},
|
|
531
|
+
hydrate(data) {
|
|
532
|
+
const runtime = runtimeOf(ctx);
|
|
533
|
+
for (const record of data.instances) {
|
|
534
|
+
const entry = runtime.nav.get(record.id);
|
|
535
|
+
if (entry?.kind !== "pursue")
|
|
536
|
+
continue;
|
|
537
|
+
restorePursue(entry, record.snapshot);
|
|
538
|
+
entry.status = record.status;
|
|
539
|
+
entry.reason = record.reason;
|
|
540
|
+
entry.pausedElapsed = record.pausedElapsed;
|
|
541
|
+
}
|
|
542
|
+
for (const record of data.instances)
|
|
543
|
+
ctx.scene.entity.setTarget(record.id, record.snapshot.explicitTargetId);
|
|
544
|
+
ctx.touch();
|
|
545
|
+
},
|
|
546
|
+
...(aoiRadius === undefined ? {} : {
|
|
547
|
+
project(data, viewer, world) {
|
|
548
|
+
const visible = visibleEntityIds((world.entities ?? []), viewer, aoiRadius);
|
|
549
|
+
return { ...data, instances: data.instances.filter(record => visible.has(record.id)) };
|
|
550
|
+
},
|
|
551
|
+
}),
|
|
552
|
+
});
|
|
553
|
+
}
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { type ProximityPrompt } from "../interaction/proximityPrompt.js";
|
|
2
2
|
import type { PathProgress, Waypoint } from "../nav/pathFollow.js";
|
|
3
|
-
import type { DecisionGraph } from "../ai/decisionGraph.js";
|
|
3
|
+
import type { Blackboard, DecisionGraph } from "../ai/decisionGraph.js";
|
|
4
|
+
import type { CooldownMode } from "../ai/pursuit.js";
|
|
5
|
+
import type { ThreatTableConfig } from "../ai/threat.js";
|
|
6
|
+
/** Scheduled chase/attack behavior; the default declares hostility toward player-role entities.
|
|
7
|
+
* GameDefinition.pursuit.eligible can further restrict every target source using the live world.
|
|
8
|
+
* Explicit targets and optional threat override nearest-player acquisition, not eligibility.
|
|
9
|
+
*/
|
|
10
|
+
export interface PursueBehavior {
|
|
11
|
+
kind: "pursue";
|
|
12
|
+
aggroRadius: number;
|
|
13
|
+
reach: number;
|
|
14
|
+
leashRange: number;
|
|
15
|
+
speed: number;
|
|
16
|
+
attack: {
|
|
17
|
+
effect: string;
|
|
18
|
+
amount: number;
|
|
19
|
+
intervalSec: number;
|
|
20
|
+
};
|
|
21
|
+
/** Seconds between acquisition/movement/attack ticks; default .1. Zero advances every frame. */
|
|
22
|
+
thinkInterval?: number;
|
|
23
|
+
cooldownMode?: CooldownMode;
|
|
24
|
+
/** Opt into a per-instance table available through `behaviorControl(ctx).threat(id)`. */
|
|
25
|
+
threat?: ThreatTableConfig;
|
|
26
|
+
}
|
|
4
27
|
export interface WanderBehavior {
|
|
5
28
|
kind: "wander";
|
|
6
29
|
radius: number;
|
|
@@ -33,8 +56,18 @@ export interface DecisionGraphBehavior {
|
|
|
33
56
|
kind: "decisionGraph";
|
|
34
57
|
graph: DecisionGraph;
|
|
35
58
|
actions: string;
|
|
59
|
+
/** Initial facts for this entity's persistent blackboard; systems write more through `behaviorControl(ctx).blackboard(id)`. */
|
|
60
|
+
blackboard?: Blackboard;
|
|
61
|
+
/** Seconds between graph ticks (`0` = every frame). Siblings are staggered; actions receive the elapsed time. */
|
|
62
|
+
thinkInterval?: number;
|
|
36
63
|
}
|
|
37
|
-
export type BehaviorDescriptor = WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior | DecisionGraphBehavior;
|
|
64
|
+
export type BehaviorDescriptor = WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior | PursueBehavior | DecisionGraphBehavior;
|
|
65
|
+
/** @internal Compare declarative behavior data at replacement boundaries, independent of object key order. */
|
|
66
|
+
export declare function behaviorDataEqual(a: unknown, b: unknown): boolean;
|
|
67
|
+
/** Attach aggro, collision-aware chase, cooldown attack and latched return-to-spawn as data.
|
|
68
|
+
* @capability pursue-behavior scheduled pursuit with bounded target acquisition and saved cooldown/leash state
|
|
69
|
+
*/
|
|
70
|
+
export declare function pursue(options: Omit<PursueBehavior, "kind">): PursueBehavior;
|
|
38
71
|
export declare function wander({ radius }: {
|
|
39
72
|
radius: number;
|
|
40
73
|
}): WanderBehavior;
|
package/dist/scene/behaviors.js
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
import { command, keybind, proximityPrompt, } from "../interaction/proximityPrompt.js";
|
|
2
|
+
/** @internal Compare declarative behavior data at replacement boundaries, independent of object key order. */
|
|
3
|
+
export function behaviorDataEqual(a, b) {
|
|
4
|
+
if (Object.is(a, b))
|
|
5
|
+
return true;
|
|
6
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object")
|
|
7
|
+
return false;
|
|
8
|
+
if (Array.isArray(a) || Array.isArray(b))
|
|
9
|
+
return Array.isArray(a) && Array.isArray(b)
|
|
10
|
+
&& a.length === b.length && a.every((value, index) => behaviorDataEqual(value, b[index]));
|
|
11
|
+
const left = a, right = b;
|
|
12
|
+
const keys = Object.keys(left).filter(key => left[key] !== undefined);
|
|
13
|
+
return keys.length === Object.keys(right).filter(key => right[key] !== undefined).length
|
|
14
|
+
&& keys.every(key => Object.hasOwn(right, key) && behaviorDataEqual(left[key], right[key]));
|
|
15
|
+
}
|
|
16
|
+
/** Attach aggro, collision-aware chase, cooldown attack and latched return-to-spawn as data.
|
|
17
|
+
* @capability pursue-behavior scheduled pursuit with bounded target acquisition and saved cooldown/leash state
|
|
18
|
+
*/
|
|
19
|
+
export function pursue(options) {
|
|
20
|
+
for (const [name, value] of Object.entries({ aggroRadius: options.aggroRadius, reach: options.reach,
|
|
21
|
+
leashRange: options.leashRange, speed: options.speed, intervalSec: options.attack.intervalSec,
|
|
22
|
+
thinkInterval: options.thinkInterval ?? .1 })) {
|
|
23
|
+
if (!Number.isFinite(value) || value < 0)
|
|
24
|
+
throw new Error(`pursue: ${name} must be finite and nonnegative`);
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isFinite(options.attack.amount))
|
|
27
|
+
throw new Error("pursue: attack amount must be finite");
|
|
28
|
+
return { ...options, kind: "pursue", attack: { ...options.attack },
|
|
29
|
+
...(options.threat === undefined ? {} : { threat: { ...options.threat } }) };
|
|
30
|
+
}
|
|
2
31
|
export function wander({ radius }) {
|
|
3
32
|
return { kind: "wander", radius };
|
|
4
33
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BehaviorDescriptor } from "./behaviors.js";
|
|
2
2
|
export type EntityRole = "player" | "npc" | "prop";
|
|
3
3
|
export interface EntityMovement {
|
|
4
4
|
walkSpeed?: number;
|
|
@@ -17,7 +17,7 @@ export interface SceneEntity<TMeta = unknown> {
|
|
|
17
17
|
rotationY: number;
|
|
18
18
|
rotationX: number;
|
|
19
19
|
rotationZ: number;
|
|
20
|
-
/** World units per second, derived
|
|
20
|
+
/** World units per second, derived by `setPose` with `dt` or explicitly replaced by `setVelocity`. Zero until set. */
|
|
21
21
|
velocity: EntityPosition;
|
|
22
22
|
role: EntityRole;
|
|
23
23
|
movement: EntityMovement;
|
|
@@ -97,11 +97,14 @@ export interface EntityBlackboard {
|
|
|
97
97
|
/** Milliseconds until the armed cooldown elapses; 0 when ready. */
|
|
98
98
|
remaining(id: string, key: string, nowMs: number): number;
|
|
99
99
|
}
|
|
100
|
+
/** Entity state, pose writes, keyed behavior notifications and snapshot/hydration under stable instance IDs. */
|
|
100
101
|
export interface EntityStore<TMeta = unknown> {
|
|
101
102
|
spawn(name: string, options?: SpawnOptions<TMeta>): string;
|
|
102
103
|
despawn(id: string): boolean;
|
|
103
104
|
update(id: string, patch: EntityUpdatePatch<TMeta>): boolean;
|
|
104
105
|
setPose(id: string, pose: EntityPose): boolean;
|
|
106
|
+
/** Replace velocity without moving the entity. Copies a finite three-number tuple; invalid input or a missing id returns false unchanged. */
|
|
107
|
+
setVelocity(id: string, velocity: EntityPosition): boolean;
|
|
105
108
|
/** Register a constraint applied inside every `setPose` for this entity — the self-driven sibling of the shell's `beforeCommit` (#282.9): nav clamps, corridor walls, arena bounds without wrapping every call site. `null` clears; despawn clears automatically. */
|
|
106
109
|
setPoseConstraint(id: string, constraint: PoseConstraint | null): void;
|
|
107
110
|
get(id: string): SceneEntity<TMeta> | null;
|
|
@@ -112,6 +115,8 @@ export interface EntityStore<TMeta = unknown> {
|
|
|
112
115
|
subscribe(listener: () => void): () => void;
|
|
113
116
|
/** Notified only when the entity set changes (spawn/despawn/hydrate), never on a pose or field update — the render tree re-reconciles on membership, individual markers read live poses imperatively. */
|
|
114
117
|
subscribeMembership(listener: () => void): () => void;
|
|
118
|
+
/** Changed behavior data or removal, keyed by id and notified before ordinary subscribers. Equal-data hydration and pose writes do not fire. */
|
|
119
|
+
subscribeBehaviors(listener: (id: string) => void): () => void;
|
|
115
120
|
snapshot(): readonly SceneEntity<TMeta>[];
|
|
116
121
|
/**
|
|
117
122
|
* Apply a `snapshot()` (e.g. from an authoritative host) to this store — the counterpart of `snapshot()`
|
|
@@ -123,4 +128,10 @@ export interface EntityStore<TMeta = unknown> {
|
|
|
123
128
|
resetToSpawn(id: string): boolean;
|
|
124
129
|
blackboard: EntityBlackboard;
|
|
125
130
|
}
|
|
126
|
-
|
|
131
|
+
/** Commit hook for a derived index; runs after the write and before existing subscribers.
|
|
132
|
+
* `undefined` denotes a whole-store replacement. Value overwrites keep membership false.
|
|
133
|
+
*/
|
|
134
|
+
export interface EntityStoreOptions {
|
|
135
|
+
onChange?: (instanceId: string | undefined, membershipChanged: boolean) => void;
|
|
136
|
+
}
|
|
137
|
+
export declare function createEntityStore<TMeta = unknown>(options?: EntityStoreOptions): EntityStore<TMeta>;
|