@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
|
@@ -28,6 +28,8 @@ export interface PoseState {
|
|
|
28
28
|
/** Restore every entity's pose and aim from a {@link snapshotAll} payload. */
|
|
29
29
|
hydrateAll(state: PoseSnapshot): void;
|
|
30
30
|
}
|
|
31
|
+
/** Restore one owned pose on synchronous failure; external implementations retain their semantic setter policy. @internal */
|
|
32
|
+
export declare function withPoseRollback<T>(state: PoseState, instanceId: string, operation: () => T): T;
|
|
31
33
|
/**
|
|
32
34
|
* Stance/pose transitions — stand, crouch, prone — that change the hitbox and movement.
|
|
33
35
|
*
|
|
@@ -23,6 +23,32 @@ export const POSE_HITBOX = {
|
|
|
23
23
|
speedMultiplier: MOVEMENT_TUNING.runSpeedMultiplier,
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
|
+
const ownedPoses = new WeakMap();
|
|
27
|
+
/** Restore one owned pose on synchronous failure; external implementations retain their semantic setter policy. @internal */
|
|
28
|
+
export function withPoseRollback(state, instanceId, operation) {
|
|
29
|
+
const poses = ownedPoses.get(state.getPose);
|
|
30
|
+
const previous = poses === undefined ? state.getPose(instanceId) : poses.get(instanceId);
|
|
31
|
+
try {
|
|
32
|
+
return operation();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (poses !== undefined) {
|
|
36
|
+
if (previous === undefined)
|
|
37
|
+
poses.delete(instanceId);
|
|
38
|
+
else
|
|
39
|
+
poses.set(instanceId, previous);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
try {
|
|
43
|
+
state.setPose(instanceId, previous);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
26
52
|
/**
|
|
27
53
|
* Stance/pose transitions — stand, crouch, prone — that change the hitbox and movement.
|
|
28
54
|
*
|
|
@@ -37,7 +63,7 @@ export function createPoseState(resolveAllowed) {
|
|
|
37
63
|
function allowedAims(instanceId) {
|
|
38
64
|
return resolveAllowed(instanceId)?.aim ?? ["hip"];
|
|
39
65
|
}
|
|
40
|
-
|
|
66
|
+
const state = {
|
|
41
67
|
getPose(instanceId) {
|
|
42
68
|
return poses.get(instanceId) ?? "standing";
|
|
43
69
|
},
|
|
@@ -74,4 +100,6 @@ export function createPoseState(resolveAllowed) {
|
|
|
74
100
|
aims.set(id, aim);
|
|
75
101
|
},
|
|
76
102
|
};
|
|
103
|
+
ownedPoses.set(state.getPose, poses);
|
|
104
|
+
return state;
|
|
77
105
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GameContext } from "../runtime/gameContext.js";
|
|
2
2
|
import { resolveColliders, type ResolvedCollider } from "../scene/colliders.js";
|
|
3
3
|
import type { EntityPosition } from "../scene/entityStore.js";
|
|
4
|
-
import type { WorldSolid } from "../world/worldSolids.js";
|
|
4
|
+
import type { WorldSolid, WorldSolids } from "../world/worldSolids.js";
|
|
5
5
|
import { type CollisionObstacle } from "./movementModel.js";
|
|
6
6
|
/** Largest blocking-collider extent in the scene, split so tower height cannot inflate the XZ query. */
|
|
7
7
|
export interface ObstacleReach {
|
|
@@ -16,6 +16,8 @@ export interface ObstacleReach {
|
|
|
16
16
|
* pure rules package with no `GameContext` to import can satisfy it from its own object list.
|
|
17
17
|
*/
|
|
18
18
|
export interface SolidObstacleSource {
|
|
19
|
+
/** Optional indexed world geometry that is not represented by scene objects. */
|
|
20
|
+
solids?: Pick<WorldSolids, "count" | "inBox">;
|
|
19
21
|
list(): readonly {
|
|
20
22
|
instanceId: string;
|
|
21
23
|
position: EntityPosition;
|
|
@@ -15,7 +15,7 @@ const OBSTACLE_MAX_HALF_EXTENT = 4;
|
|
|
15
15
|
const OBSTACLE_HORIZONTAL_CAP = 22;
|
|
16
16
|
/**
|
|
17
17
|
* Vertical broadphase floor/cap. A walker only needs nearby ground props / step-ups — never full tower
|
|
18
|
-
* height. Using building height for Y was the
|
|
18
|
+
* height. Using building height for Y was the Harbor Heat pose killer: a 50 m tower turned `inBox` into
|
|
19
19
|
* a 100^3 empty cell walk every frame.
|
|
20
20
|
*/
|
|
21
21
|
const OBSTACLE_VERTICAL_FLOOR = 3;
|
|
@@ -189,13 +189,15 @@ export function obstacleFromSolid(solid) {
|
|
|
189
189
|
*/
|
|
190
190
|
export function solidObstaclesNear(ctx, position, reachX, reachZ, height = WALKER_HEIGHT) {
|
|
191
191
|
const obstacles = sourceObstaclesNear(ctx.scene.object, solidObstacleReach(ctx), position, reachX, reachZ, height);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
appendWorldSolidObstacles(obstacles, ctx.world.solids, position, reachX, reachZ, height);
|
|
193
|
+
return obstacles;
|
|
194
|
+
}
|
|
195
|
+
function appendWorldSolidObstacles(obstacles, solids, position, reachX, reachZ, height) {
|
|
196
|
+
if (solids === undefined || solids.count() === 0)
|
|
197
|
+
return;
|
|
195
198
|
for (const solid of solids.inBox([position[0] - reachX, position[1] - height, position[2] - reachZ], [position[0] + reachX, position[1] + 2 * height, position[2] + reachZ])) {
|
|
196
199
|
obstacles.push(obstacleFromSolid(solid));
|
|
197
200
|
}
|
|
198
|
-
return obstacles;
|
|
199
201
|
}
|
|
200
202
|
/** {@link solidObstaclesNear} against a bare source and an already-resolved reach. */
|
|
201
203
|
export function sourceObstaclesNear(source, reach, position, reachX, reachZ, height = WALKER_HEIGHT) {
|
|
@@ -223,6 +225,7 @@ export function sourceObstaclesNear(source, reach, position, reachX, reachZ, hei
|
|
|
223
225
|
obstacles.push(obstacleFromCollider(collider, object.position, object.rotationY));
|
|
224
226
|
}
|
|
225
227
|
}
|
|
228
|
+
appendWorldSolidObstacles(obstacles, source.solids, position, reachX, reachZ, height);
|
|
226
229
|
return obstacles;
|
|
227
230
|
}
|
|
228
231
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constrain an uphill heightfield step by its sampled rise/run grade. Try the full step, then
|
|
3
|
+
* X-only, then Z-only, preserving downhill travel and sliding along a traversable axis.
|
|
4
|
+
* The sampler can represent a game's terrain policy independently of the ground used for feet.
|
|
5
|
+
* This endpoint test also applies while jumping; it is not continuous terrain collision.
|
|
6
|
+
* @capability uphill-grade-step constrain sampled heightfield ascent with axis sliding and a configurable terrain-policy sampler
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveTerrainGradeStep(sampler: {
|
|
9
|
+
sampleHeight(x: number, z: number): number;
|
|
10
|
+
} | ((x: number, z: number) => number), position: readonly [number, number, number], stepX: number, stepZ: number, maxClimbGrade: number): {
|
|
11
|
+
stepX: number;
|
|
12
|
+
stepZ: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constrain an uphill heightfield step by its sampled rise/run grade. Try the full step, then
|
|
3
|
+
* X-only, then Z-only, preserving downhill travel and sliding along a traversable axis.
|
|
4
|
+
* The sampler can represent a game's terrain policy independently of the ground used for feet.
|
|
5
|
+
* This endpoint test also applies while jumping; it is not continuous terrain collision.
|
|
6
|
+
* @capability uphill-grade-step constrain sampled heightfield ascent with axis sliding and a configurable terrain-policy sampler
|
|
7
|
+
*/
|
|
8
|
+
export function resolveTerrainGradeStep(sampler, position, stepX, stepZ, maxClimbGrade) {
|
|
9
|
+
if (!Number.isFinite(maxClimbGrade) || maxClimbGrade < 0) {
|
|
10
|
+
throw new RangeError("maxClimbGrade must be finite and nonnegative.");
|
|
11
|
+
}
|
|
12
|
+
const startX = position[0];
|
|
13
|
+
const startZ = position[2];
|
|
14
|
+
const sampleHeight = (x, z) => {
|
|
15
|
+
const height = typeof sampler === "function" ? sampler(x, z) : sampler.sampleHeight(x, z);
|
|
16
|
+
if (!Number.isFinite(height)) {
|
|
17
|
+
throw new RangeError(`climbGradeHeight/sampleHeight must return a finite height at (${x}, ${z}); received ${height}.`);
|
|
18
|
+
}
|
|
19
|
+
return height;
|
|
20
|
+
};
|
|
21
|
+
const height = sampleHeight(startX, startZ);
|
|
22
|
+
const allowed = (dx, dz) => {
|
|
23
|
+
const distance = Math.hypot(dx, dz);
|
|
24
|
+
return distance < 0.0001 || (sampleHeight(startX + dx, startZ + dz) - height) / distance <= maxClimbGrade;
|
|
25
|
+
};
|
|
26
|
+
if (allowed(stepX, stepZ))
|
|
27
|
+
return { stepX, stepZ };
|
|
28
|
+
if (allowed(stepX, 0))
|
|
29
|
+
return { stepX, stepZ: 0 };
|
|
30
|
+
if (allowed(0, stepZ))
|
|
31
|
+
return { stepX: 0, stepZ };
|
|
32
|
+
return { stepX: 0, stepZ: 0 };
|
|
33
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* block at cell y therefore has its top face at world y+1, so a player standing
|
|
20
20
|
* on the surface layer (cell y=-1) rests with their feet at y=0.
|
|
21
21
|
*/
|
|
22
|
-
import { type MovementIntent, type MovementTuningOverrides } from "./movementModel.js";
|
|
22
|
+
import { type MovementIntent, type MovementTuningOverrides, type MotionFrameOptions } from "./movementModel.js";
|
|
23
23
|
export type SolidQuery = (x: number, y: number, z: number) => boolean;
|
|
24
24
|
/** Footprint + height of the player box, in world units. */
|
|
25
25
|
export interface VoxelPlayerDims {
|
|
@@ -58,4 +58,4 @@ export type GroundHeightQuery = (x: number, z: number) => number;
|
|
|
58
58
|
* supported (resets jump state like landing on a solid cell). Where terrain and
|
|
59
59
|
* a solid cell's top disagree, the higher of the two wins.
|
|
60
60
|
*/
|
|
61
|
-
export declare function advanceVoxelPlayer(body: VoxelPlayerBody, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, isSolid: SolidQuery, dims?: VoxelPlayerDims, tuning?: MovementTuningOverrides, groundHeight?: GroundHeightQuery): void;
|
|
61
|
+
export declare function advanceVoxelPlayer(body: VoxelPlayerBody, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, isSolid: SolidQuery, dims?: VoxelPlayerDims, tuning?: MovementTuningOverrides, groundHeight?: GroundHeightQuery, options?: Pick<MotionFrameOptions, "authoritativeStep">): void;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* block at cell y therefore has its top face at world y+1, so a player standing
|
|
20
20
|
* on the surface layer (cell y=-1) rests with their feet at y=0.
|
|
21
21
|
*/
|
|
22
|
-
import { MOVEMENT_TUNING } from "./movementModel.js";
|
|
22
|
+
import { MOVEMENT_TUNING, motionStepSeconds } from "./movementModel.js";
|
|
23
23
|
export const DEFAULT_VOXEL_DIMS = {
|
|
24
24
|
halfWidth: 0.3,
|
|
25
25
|
height: 1.8,
|
|
@@ -172,8 +172,8 @@ function moveAxis(body, axis, delta, isSolid, dims) {
|
|
|
172
172
|
* supported (resets jump state like landing on a solid cell). Where terrain and
|
|
173
173
|
* a solid cell's top disagree, the higher of the two wins.
|
|
174
174
|
*/
|
|
175
|
-
export function advanceVoxelPlayer(body, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, isSolid, dims = DEFAULT_VOXEL_DIMS, tuning, groundHeight) {
|
|
176
|
-
const dt =
|
|
175
|
+
export function advanceVoxelPlayer(body, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, isSolid, dims = DEFAULT_VOXEL_DIMS, tuning, groundHeight, options) {
|
|
176
|
+
const dt = motionStepSeconds(rawDeltaSeconds, options?.authoritativeStep);
|
|
177
177
|
const gravityAcceleration = tuning?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration;
|
|
178
178
|
const jumpVelocity = tuning?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity;
|
|
179
179
|
const target = targetHorizontalVelocity(intent, forwardX, forwardZ, baseSpeed, tuning);
|
package/dist/nav/navGrid.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface FindPathOptions {
|
|
|
17
17
|
clearance?: number;
|
|
18
18
|
/** Multiplier (>=1) applied to the base cost of stepping from one cell center to a neighbour's. Use to penalize slopes, terrain, etc. */
|
|
19
19
|
stepCost?: (from: NavPoint, to: NavPoint) => number;
|
|
20
|
+
/** Additional edge/shortcut policy. Called after grid occupancy checks. */
|
|
21
|
+
canTraverse?: (from: NavPoint, to: NavPoint) => boolean;
|
|
22
|
+
/** Maximum expanded cells. Omit for the legacy whole-grid search. */
|
|
23
|
+
maxNodes?: number;
|
|
20
24
|
}
|
|
21
25
|
export interface NavGrid {
|
|
22
26
|
readonly cols: number;
|
|
@@ -44,6 +48,20 @@ export declare function createNavGrid(config: NavGridConfig): NavGrid;
|
|
|
44
48
|
* the nearest walkable cell so a click on an obstacle still routes to its edge.
|
|
45
49
|
*/
|
|
46
50
|
export declare function findPath(grid: NavGrid, from: NavPoint, to: NavPoint, options?: FindPathOptions): NavPoint[] | null;
|
|
51
|
+
/** Bounded grid search distinguishes exhausted work from a proven unreachable goal. */
|
|
52
|
+
export type FindPathResult = {
|
|
53
|
+
status: "path";
|
|
54
|
+
points: NavPoint[];
|
|
55
|
+
visited: number;
|
|
56
|
+
} | {
|
|
57
|
+
status: "no-path" | "budget";
|
|
58
|
+
visited: number;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Same endpoint snapping as {@link findPath}, with explicit search-budget exhaustion.
|
|
62
|
+
* @capability bounded-grid-route distinguish no path from exhausted grid search work
|
|
63
|
+
*/
|
|
64
|
+
export declare function findPathResult(grid: NavGrid, from: NavPoint, to: NavPoint, options?: FindPathOptions): FindPathResult;
|
|
47
65
|
/**
|
|
48
66
|
* `FindPathOptions.stepCost` factory that penalizes steep terrain: cost is
|
|
49
67
|
* `1 + weight * |Δheight| / horizontalDistance`, so with the default weight a
|
|
@@ -53,4 +71,4 @@ export declare function slopeStepCost(field: {
|
|
|
53
71
|
sampleHeight(x: number, z: number): number;
|
|
54
72
|
}, weight?: number): (from: NavPoint, to: NavPoint) => number;
|
|
55
73
|
/** Remove waypoints the mover can skip because it has clear line-of-sight past them. */
|
|
56
|
-
export declare function smoothPath(grid: NavGrid, points: readonly NavPoint[]): NavPoint[];
|
|
74
|
+
export declare function smoothPath(grid: NavGrid, points: readonly NavPoint[], canTraverse?: (from: NavPoint, to: NavPoint) => boolean): NavPoint[];
|
package/dist/nav/navGrid.js
CHANGED
|
@@ -174,14 +174,27 @@ function nearestPassable(grid, point, passable) {
|
|
|
174
174
|
* the nearest walkable cell so a click on an obstacle still routes to its edge.
|
|
175
175
|
*/
|
|
176
176
|
export function findPath(grid, from, to, options = {}) {
|
|
177
|
+
const result = findPathResult(grid, from, to, options);
|
|
178
|
+
return result.status === "path" ? result.points : null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Same endpoint snapping as {@link findPath}, with explicit search-budget exhaustion.
|
|
182
|
+
* @capability bounded-grid-route distinguish no path from exhausted grid search work
|
|
183
|
+
*/
|
|
184
|
+
export function findPathResult(grid, from, to, options = {}) {
|
|
185
|
+
const maxNodes = options.maxNodes ?? Number.POSITIVE_INFINITY;
|
|
186
|
+
if (!(maxNodes >= 0))
|
|
187
|
+
throw new Error("navGrid maxNodes must be nonnegative.");
|
|
177
188
|
const passable = withClearance(grid, options.clearance ?? 0);
|
|
178
189
|
const stepCost = options.stepCost;
|
|
179
190
|
const startCell = grid.cellAt(from);
|
|
180
191
|
const goalCell = grid.cellAt(to);
|
|
181
192
|
const start = passable(startCell.col, startCell.row) ? startCell : nearestPassable(grid, from, passable);
|
|
182
|
-
const
|
|
193
|
+
const exactGoal = passable(goalCell.col, goalCell.row);
|
|
194
|
+
const goal = exactGoal ? goalCell : nearestPassable(grid, to, passable);
|
|
183
195
|
if (start === null || goal === null)
|
|
184
|
-
return
|
|
196
|
+
return { status: "no-path", visited: 0 };
|
|
197
|
+
const pointAt = (col, row) => exactGoal && col === goal.col && row === goal.row ? to : grid.center(col, row);
|
|
185
198
|
const cols = grid.cols;
|
|
186
199
|
const keyOf = (col, row) => row * cols + col;
|
|
187
200
|
const startKey = keyOf(start.col, start.row);
|
|
@@ -222,6 +235,8 @@ export function findPath(grid, from, to, options = {}) {
|
|
|
222
235
|
found = true;
|
|
223
236
|
break;
|
|
224
237
|
}
|
|
238
|
+
if (closed.size >= maxNodes)
|
|
239
|
+
return { status: "budget", visited: closed.size };
|
|
225
240
|
closed.add(current.key);
|
|
226
241
|
const col = current.key % cols;
|
|
227
242
|
const row = (current.key - col) / cols;
|
|
@@ -236,7 +251,9 @@ export function findPath(grid, from, to, options = {}) {
|
|
|
236
251
|
const nKey = keyOf(nc, nr);
|
|
237
252
|
if (closed.has(nKey))
|
|
238
253
|
continue;
|
|
239
|
-
|
|
254
|
+
if (options.canTraverse !== undefined && !options.canTraverse(pointAt(col, row), pointAt(nc, nr)))
|
|
255
|
+
continue;
|
|
256
|
+
const multiplier = stepCost === undefined ? 1 : Math.max(1, stepCost(pointAt(col, row), pointAt(nc, nr)));
|
|
240
257
|
const tentative = g + step * multiplier;
|
|
241
258
|
if (tentative < (gScore.get(nKey) ?? Number.POSITIVE_INFINITY)) {
|
|
242
259
|
gScore.set(nKey, tentative);
|
|
@@ -246,7 +263,7 @@ export function findPath(grid, from, to, options = {}) {
|
|
|
246
263
|
}
|
|
247
264
|
}
|
|
248
265
|
if (!found)
|
|
249
|
-
return
|
|
266
|
+
return { status: "no-path", visited: closed.size };
|
|
250
267
|
const cells = [];
|
|
251
268
|
let cursor = goalKey;
|
|
252
269
|
while (cursor !== undefined) {
|
|
@@ -254,10 +271,9 @@ export function findPath(grid, from, to, options = {}) {
|
|
|
254
271
|
cells.unshift({ col, row: (cursor - col) / cols });
|
|
255
272
|
cursor = cameFrom.get(cursor);
|
|
256
273
|
}
|
|
257
|
-
const raw = cells.map((cell) =>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return options.smooth === false ? raw : smoothPath(grid, raw);
|
|
274
|
+
const raw = cells.map((cell) => [...pointAt(cell.col, cell.row)]);
|
|
275
|
+
const points = options.smooth === false ? raw : smoothPath(grid, raw, options.canTraverse);
|
|
276
|
+
return { status: "path", points, visited: closed.size };
|
|
261
277
|
}
|
|
262
278
|
const DEFAULT_SLOPE_STEP_WEIGHT = 1;
|
|
263
279
|
/**
|
|
@@ -275,13 +291,13 @@ export function slopeStepCost(field, weight = DEFAULT_SLOPE_STEP_WEIGHT) {
|
|
|
275
291
|
};
|
|
276
292
|
}
|
|
277
293
|
/** Remove waypoints the mover can skip because it has clear line-of-sight past them. */
|
|
278
|
-
export function smoothPath(grid, points) {
|
|
294
|
+
export function smoothPath(grid, points, canTraverse) {
|
|
279
295
|
if (points.length <= 2)
|
|
280
296
|
return points.slice();
|
|
281
297
|
const result = [points[0]];
|
|
282
298
|
let anchor = 0;
|
|
283
299
|
for (let i = 2; i < points.length; i += 1) {
|
|
284
|
-
if (!grid.lineOfSight(points[anchor], points[i])) {
|
|
300
|
+
if (!grid.lineOfSight(points[anchor], points[i]) || (canTraverse !== undefined && !canTraverse(points[anchor], points[i]))) {
|
|
285
301
|
result.push(points[i - 1]);
|
|
286
302
|
anchor = i - 1;
|
|
287
303
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CollisionObstacle } from "../movement/movementModel.js";
|
|
2
|
+
import type { Aabb } from "../world/geometry.js";
|
|
3
|
+
import { type NavPoint } from "./navGrid.js";
|
|
4
|
+
import type { Waypoint } from "./pathFollow.js";
|
|
5
|
+
/** Caller-owned local collision query and terrain policy; no movement or perception state. */
|
|
6
|
+
export interface SolidRouteOptions {
|
|
7
|
+
bounds: Aabb;
|
|
8
|
+
cellSize: number;
|
|
9
|
+
/** Query the indexed movement geometry, including mover clearance outside these bounds. */
|
|
10
|
+
obstaclesInBounds: (bounds: Aabb) => Iterable<CollisionObstacle>;
|
|
11
|
+
radius?: number;
|
|
12
|
+
height?: number;
|
|
13
|
+
/** Intermediate waypoint elevation. Defaults to the start's feet height. */
|
|
14
|
+
sampleHeight?: (x: number, z: number) => number;
|
|
15
|
+
/** Terrain/door policy for every edge and smoothing shortcut. */
|
|
16
|
+
canTraverse?: (from: Waypoint, to: Waypoint) => boolean;
|
|
17
|
+
/** Cost multiplier >=1. Suppresses smoothing by default to retain the priced route. */
|
|
18
|
+
stepCost?: (from: NavPoint, to: NavPoint) => number;
|
|
19
|
+
smooth?: boolean;
|
|
20
|
+
/** Grid allocation cap. Default 4096. */
|
|
21
|
+
maxCells?: number;
|
|
22
|
+
/** Gathered obstacle/sub-box cap. Default 256. */
|
|
23
|
+
maxBoxes?: number;
|
|
24
|
+
/** Search expansion cap. Default 1024. */
|
|
25
|
+
maxNodes?: number;
|
|
26
|
+
/** Collision-box tests and terrain-policy calls combined. Default 100000. */
|
|
27
|
+
maxChecks?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Work counters exclude the caller's spatial-query implementation. */
|
|
30
|
+
export interface SolidRouteWork {
|
|
31
|
+
cells: number;
|
|
32
|
+
boxes: number;
|
|
33
|
+
visited: number;
|
|
34
|
+
checks: number;
|
|
35
|
+
}
|
|
36
|
+
/** Failures never carry a partial or snapped route. */
|
|
37
|
+
export type SolidRouteResult = {
|
|
38
|
+
status: "path";
|
|
39
|
+
waypoints: Waypoint[];
|
|
40
|
+
work: SolidRouteWork;
|
|
41
|
+
} | {
|
|
42
|
+
status: "no-path" | "budget";
|
|
43
|
+
work: SolidRouteWork;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Plan an exact-endpoint local route over indexed movement boxes using the shared nav grid.
|
|
47
|
+
* Every edge is swept at body width; bounded failure leaves retry/stop policy with the caller.
|
|
48
|
+
* @capability solid-route bounded local planning over indexed collision geometry
|
|
49
|
+
*/
|
|
50
|
+
export declare function planSolidRoute(from: Waypoint, to: Waypoint, options: SolidRouteOptions): SolidRouteResult;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createNavGrid, findPathResult } from "./navGrid.js";
|
|
2
|
+
function intersects(from, to, box, radius, height) {
|
|
3
|
+
if (box.max[1] <= Math.min(from[1], to[1]) || box.min[1] >= Math.max(from[1], to[1]) + height)
|
|
4
|
+
return false;
|
|
5
|
+
let enter = 0;
|
|
6
|
+
let exit = 1;
|
|
7
|
+
for (const axis of [0, 2]) {
|
|
8
|
+
const min = box.min[axis] - radius;
|
|
9
|
+
const max = box.max[axis] + radius;
|
|
10
|
+
const delta = to[axis] - from[axis];
|
|
11
|
+
if (delta === 0) {
|
|
12
|
+
if (from[axis] <= min || from[axis] >= max)
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
const a = (min - from[axis]) / delta;
|
|
17
|
+
const b = (max - from[axis]) / delta;
|
|
18
|
+
enter = Math.max(enter, Math.min(a, b));
|
|
19
|
+
exit = Math.min(exit, Math.max(a, b));
|
|
20
|
+
if (enter >= exit)
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return enter < exit;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Plan an exact-endpoint local route over indexed movement boxes using the shared nav grid.
|
|
28
|
+
* Every edge is swept at body width; bounded failure leaves retry/stop policy with the caller.
|
|
29
|
+
* @capability solid-route bounded local planning over indexed collision geometry
|
|
30
|
+
*/
|
|
31
|
+
export function planSolidRoute(from, to, options) {
|
|
32
|
+
const { bounds, cellSize } = options;
|
|
33
|
+
const radius = options.radius ?? 0.3;
|
|
34
|
+
const height = options.height ?? 1.8;
|
|
35
|
+
const maxCells = options.maxCells ?? 4096;
|
|
36
|
+
const maxBoxes = options.maxBoxes ?? 256;
|
|
37
|
+
const maxNodes = options.maxNodes ?? 1024;
|
|
38
|
+
const maxChecks = options.maxChecks ?? 100000;
|
|
39
|
+
if (![...from, ...to, bounds.minX, bounds.maxX, bounds.minZ, bounds.maxZ, cellSize, radius, height].every(Number.isFinite) ||
|
|
40
|
+
cellSize <= 0 || radius < 0 || height <= 0 || bounds.maxX <= bounds.minX || bounds.maxZ <= bounds.minZ ||
|
|
41
|
+
![maxCells, maxBoxes, maxNodes, maxChecks].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
42
|
+
throw new Error("solidRoute requires finite geometry and nonnegative integer budgets.");
|
|
43
|
+
}
|
|
44
|
+
const work = { cells: 0, boxes: 0, visited: 0, checks: 0 };
|
|
45
|
+
const failure = (status) => ({ status, work });
|
|
46
|
+
const inside = (point) => point[0] >= bounds.minX && point[0] < bounds.maxX && point[2] >= bounds.minZ && point[2] < bounds.maxZ;
|
|
47
|
+
if (!inside(from) || !inside(to))
|
|
48
|
+
return failure("no-path");
|
|
49
|
+
const cells = Math.ceil((bounds.maxX - bounds.minX) / cellSize) * Math.ceil((bounds.maxZ - bounds.minZ) / cellSize);
|
|
50
|
+
if (cells > maxCells)
|
|
51
|
+
return failure("budget");
|
|
52
|
+
const boxes = [];
|
|
53
|
+
const query = { minX: bounds.minX - radius, maxX: bounds.maxX + radius, minZ: bounds.minZ - radius, maxZ: bounds.maxZ + radius };
|
|
54
|
+
for (const obstacle of options.obstaclesInBounds(query)) {
|
|
55
|
+
const half = obstacle.halfExtents ?? [0.5, 0.5, 0.5];
|
|
56
|
+
const offset = obstacle.offset ?? [0, 0, 0];
|
|
57
|
+
const local = obstacle.boxes?.length ? obstacle.boxes : [{
|
|
58
|
+
min: [offset[0] - half[0], offset[1] - half[1], offset[2] - half[2]],
|
|
59
|
+
max: [offset[0] + half[0], offset[1] + half[1], offset[2] + half[2]],
|
|
60
|
+
}];
|
|
61
|
+
for (const box of local) {
|
|
62
|
+
if (boxes.length >= maxBoxes)
|
|
63
|
+
return failure("budget");
|
|
64
|
+
const worldBox = {
|
|
65
|
+
min: [obstacle.position[0] + box.min[0], obstacle.position[1] + box.min[1], obstacle.position[2] + box.min[2]],
|
|
66
|
+
max: [obstacle.position[0] + box.max[0], obstacle.position[1] + box.max[1], obstacle.position[2] + box.max[2]],
|
|
67
|
+
};
|
|
68
|
+
if (![...worldBox.min, ...worldBox.max].every(Number.isFinite) || worldBox.min.some((value, axis) => value > worldBox.max[axis])) {
|
|
69
|
+
throw new Error("solidRoute obstacle boxes must be finite and ordered.");
|
|
70
|
+
}
|
|
71
|
+
boxes.push(worldBox);
|
|
72
|
+
work.boxes++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let exhausted = false;
|
|
76
|
+
const spend = () => {
|
|
77
|
+
if (work.checks >= maxChecks) {
|
|
78
|
+
exhausted = true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
work.checks++;
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
const clear = (a, b) => {
|
|
85
|
+
if (exhausted)
|
|
86
|
+
return false;
|
|
87
|
+
for (const box of boxes) {
|
|
88
|
+
if (!spend() || intersects(a, b, box, radius, height))
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return options.canTraverse === undefined || (a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) ||
|
|
92
|
+
(spend() && options.canTraverse(a, b));
|
|
93
|
+
};
|
|
94
|
+
if (!clear(from, from) || !clear(to, to))
|
|
95
|
+
return failure(exhausted ? "budget" : "no-path");
|
|
96
|
+
const directClear = clear(from, to);
|
|
97
|
+
if (options.stepCost === undefined && directClear)
|
|
98
|
+
return { status: "path", waypoints: [[...from], [...to]], work };
|
|
99
|
+
if (exhausted)
|
|
100
|
+
return failure("budget");
|
|
101
|
+
const grid = createNavGrid({ bounds, cellSize });
|
|
102
|
+
const start = grid.cellAt([from[0], from[2]]);
|
|
103
|
+
const goal = grid.cellAt([to[0], to[2]]);
|
|
104
|
+
if (start.col === goal.col && start.row === goal.row) {
|
|
105
|
+
return directClear ? { status: "path", waypoints: [[...from], [...to]], work } : failure("no-path");
|
|
106
|
+
}
|
|
107
|
+
const center = grid.center;
|
|
108
|
+
grid.center = (col, row) => col === start.col && row === start.row ? [from[0], from[2]] :
|
|
109
|
+
col === goal.col && row === goal.row ? [to[0], to[2]] : center(col, row);
|
|
110
|
+
const waypoint = (point) => {
|
|
111
|
+
if (point[0] === from[0] && point[1] === from[2])
|
|
112
|
+
return from;
|
|
113
|
+
if (point[0] === to[0] && point[1] === to[2])
|
|
114
|
+
return to;
|
|
115
|
+
const y = options.sampleHeight?.(point[0], point[1]) ?? from[1];
|
|
116
|
+
if (!Number.isFinite(y))
|
|
117
|
+
throw new Error("solidRoute sampleHeight must be finite.");
|
|
118
|
+
return [point[0], y, point[1]];
|
|
119
|
+
};
|
|
120
|
+
for (let row = 0; row < grid.rows; row++) {
|
|
121
|
+
for (let col = 0; col < grid.cols; col++) {
|
|
122
|
+
const point = waypoint(grid.center(col, row));
|
|
123
|
+
grid.setWalkable(col, row, inside(point) && clear(point, point));
|
|
124
|
+
work.cells++;
|
|
125
|
+
if (exhausted)
|
|
126
|
+
return failure("budget");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const result = findPathResult(grid, [from[0], from[2]], [to[0], to[2]], {
|
|
130
|
+
maxNodes, stepCost: options.stepCost, smooth: options.smooth ?? options.stepCost === undefined,
|
|
131
|
+
canTraverse: (a, b) => clear(waypoint(a), waypoint(b)),
|
|
132
|
+
});
|
|
133
|
+
work.visited = result.visited;
|
|
134
|
+
if (exhausted)
|
|
135
|
+
return failure("budget");
|
|
136
|
+
if (result.status !== "path")
|
|
137
|
+
return failure(result.status);
|
|
138
|
+
const waypoints = result.points.map((point) => [...waypoint(point)]);
|
|
139
|
+
for (let i = 1; i < waypoints.length; i++) {
|
|
140
|
+
if (!clear(waypoints[i - 1], waypoints[i]))
|
|
141
|
+
return failure(exhausted ? "budget" : "no-path");
|
|
142
|
+
}
|
|
143
|
+
return { status: "path", waypoints, work };
|
|
144
|
+
}
|
|
@@ -11,8 +11,15 @@ export interface BallisticSweepOptions {
|
|
|
11
11
|
radius?: number;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* between the last clear sample and the hit sample. Returns `null` when the whole arc is clear.
|
|
14
|
+
* Sweeps each short chord of the closed-form arc through live body AABBs, including sleeping bodies.
|
|
15
|
+
* Thin cover between samples blocks; `step` controls the approximation of the curved path.
|
|
17
16
|
*/
|
|
18
17
|
export declare function createBallisticSweep(world: PhysicsWorld, options?: BallisticSweepOptions): BallisticSweep;
|
|
18
|
+
/** Contact fraction for a point segment against a translating axis-aligned box; inflate extents for a projectile radius.
|
|
19
|
+
* @capability moving-box-sweep detect contact with boxes crossing a projectile segment between simulation poses
|
|
20
|
+
*/
|
|
21
|
+
export declare function sweepMovingBounds(from: readonly [number, number, number], to: readonly [number, number, number], targetFrom: readonly [number, number, number], targetTo: readonly [number, number, number], halfExtents: readonly [number, number, number]): number | null;
|
|
22
|
+
/** Contact fraction for a point segment against a translating sphere; add the projectile radius to `radius`.
|
|
23
|
+
* @capability moving-sphere-sweep detect contact with spheres crossing a projectile segment between simulation poses
|
|
24
|
+
*/
|
|
25
|
+
export declare function sweepMovingSphere(from: readonly [number, number, number], to: readonly [number, number, number], targetFrom: readonly [number, number, number], targetTo: readonly [number, number, number], radius: number): number | null;
|
|
@@ -1,49 +1,73 @@
|
|
|
1
|
+
import { intersectAabb } from "../scene/objectQuery.js";
|
|
1
2
|
const DEFAULT_SWEEP_STEP = 1 / 60;
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* between the last clear sample and the hit sample. Returns `null` when the whole arc is clear.
|
|
4
|
+
* Sweeps each short chord of the closed-form arc through live body AABBs, including sleeping bodies.
|
|
5
|
+
* Thin cover between samples blocks; `step` controls the approximation of the curved path.
|
|
6
6
|
*/
|
|
7
7
|
export function createBallisticSweep(world, options = {}) {
|
|
8
8
|
const step = options.step ?? DEFAULT_SWEEP_STEP;
|
|
9
9
|
const radius = options.radius ?? 0;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!world.isAlive(i))
|
|
13
|
-
continue;
|
|
14
|
-
if (Math.abs(x - world.posX[i]) > world.halfX[i] + radius)
|
|
15
|
-
continue;
|
|
16
|
-
if (Math.abs(y - world.posY[i]) > world.halfY[i] + radius)
|
|
17
|
-
continue;
|
|
18
|
-
if (Math.abs(z - world.posZ[i]) > world.halfZ[i] + radius)
|
|
19
|
-
continue;
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
10
|
+
if (!Number.isFinite(step) || step <= 0 || !Number.isFinite(radius) || radius < 0)
|
|
11
|
+
throw new RangeError("Ballistic sweep requires a finite positive step and nonnegative radius");
|
|
24
12
|
return (origin, velocity, gravity, maxTime) => {
|
|
13
|
+
if (!origin.every(Number.isFinite) || !velocity.every(Number.isFinite) || !Number.isFinite(gravity) || !Number.isFinite(maxTime) || maxTime < 0 || Math.ceil(maxTime / step) > 100_000)
|
|
14
|
+
throw new RangeError("Ballistic sweep requires finite inputs within its 100000 segment budget");
|
|
25
15
|
const pointAt = (t) => [
|
|
26
16
|
origin[0] + velocity[0] * t,
|
|
27
17
|
origin[1] + velocity[1] * t - 0.5 * gravity * t * t,
|
|
28
18
|
origin[2] + velocity[2] * t,
|
|
29
19
|
];
|
|
30
|
-
let lastClear = 0;
|
|
31
20
|
let t = 0;
|
|
32
21
|
for (;;) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
22
|
+
const nextTime = Math.min(t + step, maxTime);
|
|
23
|
+
const from = pointAt(t);
|
|
24
|
+
const to = pointAt(nextTime);
|
|
25
|
+
let fraction = null;
|
|
26
|
+
for (let i = 0; i < world.highWater; i += 1) {
|
|
27
|
+
if (!world.isAlive(i))
|
|
28
|
+
continue;
|
|
29
|
+
const contact = sweepMovingBounds(from, to, [world.posX[i], world.posY[i], world.posZ[i]], [world.posX[i], world.posY[i], world.posZ[i]], [world.halfX[i] + radius, world.halfY[i] + radius, world.halfZ[i] + radius]);
|
|
30
|
+
if (contact !== null && (fraction === null || contact < fraction))
|
|
31
|
+
fraction = contact;
|
|
32
|
+
}
|
|
33
|
+
if (fraction !== null) {
|
|
34
|
+
const time = t + (nextTime - t) * fraction;
|
|
35
|
+
return { point: pointAt(time), time };
|
|
42
36
|
}
|
|
43
37
|
if (t >= maxTime)
|
|
44
38
|
return null;
|
|
45
|
-
|
|
46
|
-
t = Math.min(t + step, maxTime);
|
|
39
|
+
t = nextTime;
|
|
47
40
|
}
|
|
48
41
|
};
|
|
49
42
|
}
|
|
43
|
+
/** Contact fraction for a point segment against a translating axis-aligned box; inflate extents for a projectile radius.
|
|
44
|
+
* @capability moving-box-sweep detect contact with boxes crossing a projectile segment between simulation poses
|
|
45
|
+
*/
|
|
46
|
+
export function sweepMovingBounds(from, to, targetFrom, targetTo, halfExtents) {
|
|
47
|
+
const origin = [from[0] - targetFrom[0], from[1] - targetFrom[1], from[2] - targetFrom[2]];
|
|
48
|
+
const motion = [to[0] - targetTo[0] - origin[0], to[1] - targetTo[1] - origin[1], to[2] - targetTo[2] - origin[2]];
|
|
49
|
+
return intersectAabb(origin, motion, [-halfExtents[0], -halfExtents[1], -halfExtents[2]], halfExtents, 1)?.distance ?? null;
|
|
50
|
+
}
|
|
51
|
+
/** Contact fraction for a point segment against a translating sphere; add the projectile radius to `radius`.
|
|
52
|
+
* @capability moving-sphere-sweep detect contact with spheres crossing a projectile segment between simulation poses
|
|
53
|
+
*/
|
|
54
|
+
export function sweepMovingSphere(from, to, targetFrom, targetTo, radius) {
|
|
55
|
+
const ox = from[0] - targetFrom[0];
|
|
56
|
+
const oy = from[1] - targetFrom[1];
|
|
57
|
+
const oz = from[2] - targetFrom[2];
|
|
58
|
+
const dx = to[0] - targetTo[0] - ox;
|
|
59
|
+
const dy = to[1] - targetTo[1] - oy;
|
|
60
|
+
const dz = to[2] - targetTo[2] - oz;
|
|
61
|
+
const c = ox * ox + oy * oy + oz * oz - radius * radius;
|
|
62
|
+
if (c <= 0)
|
|
63
|
+
return 0;
|
|
64
|
+
const a = dx * dx + dy * dy + dz * dz;
|
|
65
|
+
if (a <= 1e-18)
|
|
66
|
+
return null;
|
|
67
|
+
const b = ox * dx + oy * dy + oz * dz;
|
|
68
|
+
const discriminant = b * b - a * c;
|
|
69
|
+
if (discriminant < 0)
|
|
70
|
+
return null;
|
|
71
|
+
const fraction = (-b - Math.sqrt(discriminant)) / a;
|
|
72
|
+
return fraction >= 0 && fraction <= 1 ? fraction : null;
|
|
73
|
+
}
|