@jgengine/core 0.19.0 → 0.22.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 +364 -0
- package/dist/ai/decisionGraph.d.ts +2 -0
- package/dist/ai/decisionGraph.js +6 -0
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- package/dist/anim/animGraph.d.ts +33 -2
- package/dist/anim/animGraph.js +200 -12
- package/dist/anim/locomotionGraph.d.ts +11 -3
- package/dist/anim/locomotionGraph.js +25 -15
- package/dist/audio/audioFalloff.d.ts +4 -0
- package/dist/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +19 -0
- package/dist/combat/abilityKit.js +40 -4
- package/dist/combat/death.d.ts +4 -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/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -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 +116 -17
- package/dist/editor/index.d.ts +3 -0
- package/dist/editor/index.js +1 -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/terrainMaterials.d.ts +10 -0
- package/dist/editor/terrainMaterials.js +13 -0
- package/dist/editor/types.d.ts +11 -0
- package/dist/game/cameraConfig.d.ts +3 -1
- 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 +5 -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/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +52 -20
- 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/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -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/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 +348 -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/movementModel.d.ts +24 -1
- package/dist/movement/movementModel.js +74 -7
- package/dist/movement/playerMovement.d.ts +28 -1
- package/dist/movement/playerMovement.js +425 -102
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +3 -1
- package/dist/movement/solidObstacles.js +8 -5
- 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 +3 -3
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- package/dist/physics/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/kinematicVehicle.d.ts +8 -3
- package/dist/physics/kinematicVehicle.js +2 -2
- package/dist/physics/physicsWorldBackend.js +19 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleDynamics.d.ts +3 -2
- package/dist/physics/vehicleDynamics.js +2 -2
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- package/dist/runtime/cameraDirector.d.ts +39 -2
- package/dist/runtime/cameraDirector.js +162 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -0
- package/dist/runtime/context/combat.d.ts +11 -4
- package/dist/runtime/context/combat.js +178 -11
- package/dist/runtime/context/deathLoot.d.ts +9 -8
- package/dist/runtime/context/deathLoot.js +16 -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 +79 -13
- package/dist/runtime/gameContextTypes.d.ts +14 -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 +16 -4
- package/dist/runtime/inputSnapshot.js +31 -3
- 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/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 +26 -5
- package/dist/scene/behaviorRuntime.js +263 -16
- package/dist/scene/behaviors.d.ts +30 -1
- 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/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 +36 -2
- package/dist/scene/vehicleSeat.js +89 -2
- package/dist/stats/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +9 -2
- package/dist/stats/eventMeter.js +5 -2
- 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/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/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/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingKit.d.ts +8 -3
- package/dist/world/buildingKit.js +21 -8
- package/dist/world/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +3 -3
- package/dist/world/footprintGrid.d.ts +55 -0
- package/dist/world/footprintGrid.js +201 -0
- package/dist/world/grassKind.d.ts +10 -0
- package/dist/world/grassKind.js +22 -0
- package/dist/world/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +2 -2
- package/dist/world/terraform.js +46 -18
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +11 -0
- package/dist/world/worldSolids.js +112 -1
- package/dist/world.d.ts +5 -5
- package/dist/world.js +4 -4
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-combat/SKILL.md +8 -2
- package/skills/jgengine-combat/api.md +22 -10
- 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/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +131 -37
- package/skills/jgengine-gameplay/capabilities.md +68 -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 +119 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -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
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";
|
|
@@ -25,7 +28,7 @@ export declare function registerBehaviorActions(id: string, actions: Record<stri
|
|
|
25
28
|
/**
|
|
26
29
|
* Catch-up policy applied when a paused instance resumes. `freeze` (default, deterministic) discards
|
|
27
30
|
* the time spent paused; `advance` silently fast-forwards the instance by the paused duration without
|
|
28
|
-
* emitting intermediate pose writes. Only patrol honors `advance`; wander
|
|
31
|
+
* emitting intermediate pose writes. Only patrol honors `advance`; wander/pursue treat it as `freeze`.
|
|
29
32
|
*/
|
|
30
33
|
export type BehaviorResumePolicy = "freeze" | "advance";
|
|
31
34
|
/** Serializable snapshot of one behavior instance — round-trips exactly through
|
|
@@ -37,7 +40,7 @@ export type BehaviorSnapshot = {
|
|
|
37
40
|
readonly kind: "wander";
|
|
38
41
|
readonly origin: Waypoint;
|
|
39
42
|
readonly target: Waypoint | null;
|
|
40
|
-
} | {
|
|
43
|
+
} | PursueBehaviorSnapshot | {
|
|
41
44
|
readonly kind: "decisionGraph";
|
|
42
45
|
readonly graph: ReturnType<DecisionGraphRuntime["snapshot"]>;
|
|
43
46
|
readonly gate: InterestGateState;
|
|
@@ -45,10 +48,22 @@ export type BehaviorSnapshot = {
|
|
|
45
48
|
/** Seconds accrued since the last think. */
|
|
46
49
|
readonly pending?: number;
|
|
47
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
|
+
}
|
|
48
63
|
/** Inspection readout for editor/debug tooling, from {@link BehaviorControl.inspect}/{@link BehaviorControl.list}. */
|
|
49
64
|
export interface BehaviorInspection {
|
|
50
65
|
id: string;
|
|
51
|
-
kind: "patrol" | "wander" | "decisionGraph";
|
|
66
|
+
kind: "patrol" | "wander" | "decisionGraph" | "pursue";
|
|
52
67
|
status: BehaviorStatus;
|
|
53
68
|
reason: string | null;
|
|
54
69
|
}
|
|
@@ -82,15 +97,19 @@ export interface BehaviorControl {
|
|
|
82
97
|
list(): BehaviorInspection[];
|
|
83
98
|
/** The live, persistent blackboard of a decision graph instance for perception and game systems to write facts into, or `null`. */
|
|
84
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;
|
|
85
104
|
}
|
|
86
105
|
/**
|
|
87
|
-
* Advance every spawned entity carrying a
|
|
106
|
+
* Advance every spawned entity carrying a patrol, wander, pursuit or decision graph behavior one tick — the
|
|
88
107
|
* engine reads the descriptor, keeps the per-entity nav state itself, and poses the entity, so ambient
|
|
89
108
|
* traffic and idle NPC routes are register-once (attach the behavior at spawn) instead of a per-game
|
|
90
109
|
* per-frame `advancePathFollow` + `setPose` loop. Instances that are paused or disabled through
|
|
91
110
|
* {@link behaviorControl} retain their state and are skipped. The shell/host call this each frame; a game never does.
|
|
92
111
|
*
|
|
93
|
-
* @capability behavior-tick auto-advance patrol
|
|
112
|
+
* @capability behavior-tick auto-advance spawned patrol, wander, pursuit and decision graph behaviors with lifecycle control
|
|
94
113
|
*/
|
|
95
114
|
export declare function advanceBehaviors(ctx: GameContext, dt: number): void;
|
|
96
115
|
/**
|
|
@@ -101,3 +120,5 @@ export declare function advanceBehaviors(ctx: GameContext, dt: number): void;
|
|
|
101
120
|
* @capability behavior-control pause/disable/resume/seek/inspect behavior instances per entity
|
|
102
121
|
*/
|
|
103
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;
|
|
@@ -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. */
|
|
@@ -19,6 +26,8 @@ export function registerBehaviorActions(id, actions, options = {}) {
|
|
|
19
26
|
behaviorActions.delete(id);
|
|
20
27
|
};
|
|
21
28
|
}
|
|
29
|
+
// Bind unsaved policy without eagerly constructing the lazy AI runtime before boot-time restore.
|
|
30
|
+
const pursuitPolicies = new WeakMap();
|
|
22
31
|
function thinkCadence(behavior) {
|
|
23
32
|
return { wakeRadius: Number.POSITIVE_INFINITY, activeInterval: Math.max(0, behavior.thinkInterval ?? 0) };
|
|
24
33
|
}
|
|
@@ -33,12 +42,20 @@ function decisionGraphOf(entity) {
|
|
|
33
42
|
}
|
|
34
43
|
const runtimeOf = perContext((ctx) => {
|
|
35
44
|
const nav = new Map();
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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);
|
|
42
59
|
const patrol = patrolOf(entity);
|
|
43
60
|
if (patrol !== null) {
|
|
44
61
|
const config = { waypoints: patrol.waypoints, speed: patrol.speed, loop: patrol.loop };
|
|
@@ -51,7 +68,7 @@ const runtimeOf = perContext((ctx) => {
|
|
|
51
68
|
reason: null,
|
|
52
69
|
pausedElapsed: 0,
|
|
53
70
|
});
|
|
54
|
-
|
|
71
|
+
return;
|
|
55
72
|
}
|
|
56
73
|
const wander = wanderOf(entity);
|
|
57
74
|
if (wander !== null) {
|
|
@@ -65,13 +82,13 @@ const runtimeOf = perContext((ctx) => {
|
|
|
65
82
|
reason: null,
|
|
66
83
|
pausedElapsed: 0,
|
|
67
84
|
});
|
|
68
|
-
|
|
85
|
+
return;
|
|
69
86
|
}
|
|
70
87
|
const decisionGraph = decisionGraphOf(entity);
|
|
71
88
|
if (decisionGraph !== null) {
|
|
72
89
|
const registered = behaviorActions.get(decisionGraph.actions);
|
|
73
90
|
if (registered === undefined)
|
|
74
|
-
|
|
91
|
+
return;
|
|
75
92
|
const cadence = thinkCadence(decisionGraph);
|
|
76
93
|
nav.set(entity.id, {
|
|
77
94
|
kind: "decisionGraph",
|
|
@@ -89,15 +106,141 @@ const runtimeOf = perContext((ctx) => {
|
|
|
89
106
|
pausedElapsed: 0,
|
|
90
107
|
});
|
|
91
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
|
+
}
|
|
121
|
+
}
|
|
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);
|
|
92
132
|
}
|
|
93
|
-
for (const id of
|
|
133
|
+
for (const id of descriptors.keys())
|
|
94
134
|
if (!live.has(id))
|
|
95
|
-
|
|
135
|
+
refreshId(id);
|
|
96
136
|
};
|
|
97
|
-
ctx.scene.entity.
|
|
137
|
+
ctx.scene.entity.subscribeBehaviors(refreshId);
|
|
98
138
|
refresh();
|
|
99
139
|
return { nav };
|
|
100
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
|
+
}
|
|
101
244
|
function stepWander(ctx, id, nav, dt) {
|
|
102
245
|
const entity = ctx.scene.entity.get(id);
|
|
103
246
|
if (entity === null)
|
|
@@ -153,13 +296,13 @@ function posePatrol(ctx, id, nav, dt) {
|
|
|
153
296
|
});
|
|
154
297
|
}
|
|
155
298
|
/**
|
|
156
|
-
* Advance every spawned entity carrying a
|
|
299
|
+
* Advance every spawned entity carrying a patrol, wander, pursuit or decision graph behavior one tick — the
|
|
157
300
|
* engine reads the descriptor, keeps the per-entity nav state itself, and poses the entity, so ambient
|
|
158
301
|
* traffic and idle NPC routes are register-once (attach the behavior at spawn) instead of a per-game
|
|
159
302
|
* per-frame `advancePathFollow` + `setPose` loop. Instances that are paused or disabled through
|
|
160
303
|
* {@link behaviorControl} retain their state and are skipped. The shell/host call this each frame; a game never does.
|
|
161
304
|
*
|
|
162
|
-
* @capability behavior-tick auto-advance patrol
|
|
305
|
+
* @capability behavior-tick auto-advance spawned patrol, wander, pursuit and decision graph behaviors with lifecycle control
|
|
163
306
|
*/
|
|
164
307
|
export function advanceBehaviors(ctx, dt) {
|
|
165
308
|
if (dt <= 0)
|
|
@@ -167,10 +310,14 @@ export function advanceBehaviors(ctx, dt) {
|
|
|
167
310
|
const { nav } = runtimeOf(ctx);
|
|
168
311
|
if (nav.size === 0)
|
|
169
312
|
return;
|
|
313
|
+
let pursuitChanged = false;
|
|
170
314
|
for (const [id, entry] of nav) {
|
|
171
315
|
if (entry.status !== "active") {
|
|
172
|
-
if (entry.status === "paused")
|
|
316
|
+
if (entry.status === "paused") {
|
|
173
317
|
entry.pausedElapsed += dt;
|
|
318
|
+
if (entry.kind === "pursue")
|
|
319
|
+
pursuitChanged = true;
|
|
320
|
+
}
|
|
174
321
|
continue;
|
|
175
322
|
}
|
|
176
323
|
if (entry.kind === "patrol") {
|
|
@@ -180,6 +327,15 @@ export function advanceBehaviors(ctx, dt) {
|
|
|
180
327
|
else if (entry.kind === "wander") {
|
|
181
328
|
stepWander(ctx, id, entry, dt);
|
|
182
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
|
+
}
|
|
183
339
|
else {
|
|
184
340
|
const entity = ctx.scene.entity.get(id);
|
|
185
341
|
if (entity === null)
|
|
@@ -193,6 +349,8 @@ export function advanceBehaviors(ctx, dt) {
|
|
|
193
349
|
entry.runtime.tick({ ctx, entityId: id, dt: elapsed }, entry.blackboard, elapsed);
|
|
194
350
|
}
|
|
195
351
|
}
|
|
352
|
+
if (pursuitChanged)
|
|
353
|
+
ctx.touch();
|
|
196
354
|
}
|
|
197
355
|
/**
|
|
198
356
|
* Obtain the per-context {@link BehaviorControl} surface for suspending, resuming, seeking, serializing,
|
|
@@ -209,6 +367,8 @@ export function behaviorControl(ctx) {
|
|
|
209
367
|
return false;
|
|
210
368
|
entry.status = status;
|
|
211
369
|
entry.reason = reason;
|
|
370
|
+
if (entry.kind === "pursue")
|
|
371
|
+
ctx.touch();
|
|
212
372
|
return true;
|
|
213
373
|
};
|
|
214
374
|
return {
|
|
@@ -221,6 +381,8 @@ export function behaviorControl(ctx) {
|
|
|
221
381
|
entry.status = "paused";
|
|
222
382
|
entry.reason = reason ?? null;
|
|
223
383
|
entry.pausedElapsed = 0;
|
|
384
|
+
if (entry.kind === "pursue")
|
|
385
|
+
ctx.touch();
|
|
224
386
|
return true;
|
|
225
387
|
},
|
|
226
388
|
resume: (id, policy = "freeze") => {
|
|
@@ -235,6 +397,8 @@ export function behaviorControl(ctx) {
|
|
|
235
397
|
entry.status = "active";
|
|
236
398
|
entry.reason = null;
|
|
237
399
|
entry.pausedElapsed = 0;
|
|
400
|
+
if (entry.kind === "pursue")
|
|
401
|
+
ctx.touch();
|
|
238
402
|
return true;
|
|
239
403
|
},
|
|
240
404
|
disable: (id, reason) => setStatus(id, "disabled", reason ?? null),
|
|
@@ -245,6 +409,8 @@ export function behaviorControl(ctx) {
|
|
|
245
409
|
entry.status = "active";
|
|
246
410
|
entry.reason = null;
|
|
247
411
|
entry.pausedElapsed = 0;
|
|
412
|
+
if (entry.kind === "pursue")
|
|
413
|
+
ctx.touch();
|
|
248
414
|
return true;
|
|
249
415
|
},
|
|
250
416
|
seek: (id, progress) => {
|
|
@@ -262,6 +428,11 @@ export function behaviorControl(ctx) {
|
|
|
262
428
|
return { kind: "patrol", state: { ...entry.state } };
|
|
263
429
|
if (entry.kind === "wander")
|
|
264
430
|
return { kind: "wander", origin: [...entry.origin], target: entry.target === null ? null : [...entry.target] };
|
|
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() }) };
|
|
265
436
|
return { kind: "decisionGraph", graph: entry.runtime.snapshot(), gate: { ...entry.gate }, blackboard: { ...entry.blackboard }, pending: entry.pending };
|
|
266
437
|
},
|
|
267
438
|
restore: (id, snapshot) => {
|
|
@@ -284,6 +455,14 @@ export function behaviorControl(ctx) {
|
|
|
284
455
|
entry.target = snapshot.target === null ? null : [...snapshot.target];
|
|
285
456
|
return true;
|
|
286
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
|
+
}
|
|
287
466
|
return false;
|
|
288
467
|
},
|
|
289
468
|
inspect: (id) => {
|
|
@@ -302,5 +481,73 @@ export function behaviorControl(ctx) {
|
|
|
302
481
|
const entry = nav.get(id);
|
|
303
482
|
return entry?.kind === "decisionGraph" ? entry.blackboard : null;
|
|
304
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
|
+
},
|
|
305
498
|
};
|
|
306
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
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;
|
|
@@ -38,7 +61,13 @@ export interface DecisionGraphBehavior {
|
|
|
38
61
|
/** Seconds between graph ticks (`0` = every frame). Siblings are staggered; actions receive the elapsed time. */
|
|
39
62
|
thinkInterval?: number;
|
|
40
63
|
}
|
|
41
|
-
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;
|
|
42
71
|
export declare function wander({ radius }: {
|
|
43
72
|
radius: number;
|
|
44
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>;
|