@jgengine/core 0.25.0 → 0.27.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 +109 -0
- package/dist/ai/rvo.d.ts +106 -0
- package/dist/ai/rvo.js +581 -0
- package/dist/ai/utilityBrain.d.ts +173 -0
- package/dist/ai/utilityBrain.js +277 -0
- package/dist/combat/cover.d.ts +169 -0
- package/dist/combat/cover.js +264 -0
- package/dist/combat/damageRuntime.d.ts +150 -0
- package/dist/combat/damageRuntime.js +208 -0
- package/dist/combat/damageTable.d.ts +116 -0
- package/dist/combat/damageTable.js +165 -0
- package/dist/combat/hitZones.d.ts +70 -0
- package/dist/combat/hitZones.js +77 -0
- package/dist/combat/magazine.d.ts +8 -0
- package/dist/combat/magazine.js +20 -2
- package/dist/combat/squadCombat.d.ts +167 -0
- package/dist/combat/squadCombat.js +557 -0
- package/dist/combat/squadTactics.d.ts +73 -0
- package/dist/combat/squadTactics.js +49 -0
- package/dist/combat/statusRuntime.d.ts +142 -0
- package/dist/combat/statusRuntime.js +236 -0
- package/dist/combat/weaponDefinition.d.ts +106 -0
- package/dist/combat/weaponDefinition.js +108 -0
- package/dist/combat/weaponSystem.d.ts +189 -0
- package/dist/combat/weaponSystem.js +469 -0
- package/dist/combat.d.ts +9 -0
- package/dist/combat.js +9 -0
- package/dist/devtools/devtools.d.ts +4 -2
- package/dist/devtools/devtools.js +2 -1
- package/dist/devtools/discover.d.ts +2 -1
- package/dist/devtools/discover.js +38 -0
- package/dist/devtools/frame.d.ts +0 -2
- package/dist/devtools/frame.js +1 -2
- package/dist/devtools/logBus.d.ts +58 -0
- package/dist/devtools/logBus.js +79 -0
- package/dist/devtools/logs.d.ts +6 -3
- package/dist/devtools/logs.js +17 -13
- package/dist/devtools/report.d.ts +120 -0
- package/dist/devtools/report.js +49 -0
- package/dist/devtools/reportEndpoint.d.ts +17 -0
- package/dist/devtools/reportEndpoint.js +7 -0
- package/dist/devtools/types.d.ts +48 -11
- package/dist/editor/document.js +210 -0
- package/dist/editor/environment.d.ts +48 -1
- package/dist/editor/environment.js +161 -0
- package/dist/editor/index.d.ts +6 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +54 -0
- package/dist/game/playableGame.d.ts +19 -2
- package/dist/gameplay.d.ts +9 -0
- package/dist/gameplay.js +9 -0
- package/dist/inventory/carryLoad.d.ts +48 -0
- package/dist/inventory/carryLoad.js +44 -0
- package/dist/inventory/equipment.d.ts +95 -0
- package/dist/inventory/equipment.js +169 -0
- package/dist/inventory/inventoryModel.d.ts +15 -6
- package/dist/inventory/inventoryModel.js +54 -5
- package/dist/inventory/shapedGrid.d.ts +26 -0
- package/dist/inventory/shapedGrid.js +47 -0
- package/dist/loot/lootGen.d.ts +186 -0
- package/dist/loot/lootGen.js +437 -0
- package/dist/loot/lootWeapon.d.ts +35 -0
- package/dist/loot/lootWeapon.js +70 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +92 -1
- package/dist/nav/laneGraph.d.ts +178 -0
- package/dist/nav/laneGraph.js +671 -0
- package/dist/nav/navCrowd.d.ts +138 -0
- package/dist/nav/navCrowd.js +412 -0
- package/dist/nav/navRuntime.d.ts +108 -0
- package/dist/nav/navRuntime.js +256 -0
- package/dist/nav/traffic.d.ts +223 -0
- package/dist/nav/traffic.js +919 -0
- package/dist/physics/kinematicVehicle.d.ts +36 -0
- package/dist/physics/kinematicVehicle.js +15 -1
- package/dist/physics/vehicleDamage.d.ts +136 -0
- package/dist/physics/vehicleDamage.js +214 -0
- package/dist/physics/vehicleParts.d.ts +62 -0
- package/dist/physics/vehicleParts.js +65 -0
- package/dist/progression/gearScore.d.ts +56 -0
- package/dist/progression/gearScore.js +59 -0
- package/dist/progression/skillTree.d.ts +129 -0
- package/dist/progression/skillTree.js +265 -0
- package/dist/progression/xpCurve.d.ts +60 -0
- package/dist/progression/xpCurve.js +141 -0
- package/dist/render/colorPipeline.d.ts +53 -0
- package/dist/render/colorPipeline.js +79 -0
- package/dist/render/lightProbes.d.ts +97 -0
- package/dist/render/lightProbes.js +221 -0
- package/dist/render/lookPreset.js +4 -4
- package/dist/render/physicalSky.d.ts +39 -0
- package/dist/render/physicalSky.js +89 -0
- package/dist/render/postProcessing.d.ts +40 -8
- package/dist/render/postProcessing.js +1 -1
- package/dist/render/skyOwnedLighting.d.ts +31 -0
- package/dist/render/skyOwnedLighting.js +46 -0
- package/dist/scene/builtinSceneKinds.js +2 -0
- package/dist/sensor/perception.d.ts +66 -5
- package/dist/sensor/perception.js +167 -42
- package/dist/sim/nearAgents.d.ts +68 -0
- package/dist/sim/nearAgents.js +152 -0
- package/dist/sim/needs.d.ts +60 -0
- package/dist/sim/needs.js +108 -0
- package/dist/sim/offScreen.d.ts +181 -0
- package/dist/sim/offScreen.js +450 -0
- package/dist/sim/routine.d.ts +79 -0
- package/dist/sim/routine.js +137 -0
- package/dist/visibility/visibilitySystem.js +21 -0
- package/dist/world/decalKind.d.ts +48 -0
- package/dist/world/decalKind.js +86 -0
- package/dist/world/features.d.ts +11 -1
- package/dist/world/features.js +1 -0
- package/dist/world/grassKind.d.ts +5 -0
- package/dist/world/grassKind.js +3 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world.d.ts +10 -1
- package/dist/world.js +9 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-combat/SKILL.md +19 -0
- package/skills/jgengine-combat/api.md +246 -0
- package/skills/jgengine-combat/capabilities.md +112 -0
- package/skills/jgengine-combat/reference.md +32 -0
- package/skills/jgengine-gameplay/SKILL.md +39 -0
- package/skills/jgengine-gameplay/api.md +221 -7
- package/skills/jgengine-gameplay/capabilities.md +64 -3
- package/skills/jgengine-gameplay/reference-systems.md +118 -0
- package/skills/jgengine-world/SKILL.md +7 -1
- package/skills/jgengine-world/api.md +222 -8
- package/skills/jgengine-world/capabilities.md +42 -2
- package/skills/jgengine-world/recipes/npc-senses.md +8 -2
- package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Vec3 } from "../world/geometry.js";
|
|
2
|
+
import type { NavRebuildStats, NavRegion, NavRuntime } from "./navRuntime.js";
|
|
3
|
+
import { type RvoTunables } from "../ai/rvo.js";
|
|
4
|
+
/** How one crowd agent was configured when it joined. */
|
|
5
|
+
export interface NavCrowdAgentSpec {
|
|
6
|
+
position: Vec3;
|
|
7
|
+
goal: Vec3;
|
|
8
|
+
/** World units per second; defaults to the crowd's `speed`. */
|
|
9
|
+
speed?: number;
|
|
10
|
+
/** Avoidance radius; defaults to the crowd's `radius`. */
|
|
11
|
+
radius?: number;
|
|
12
|
+
}
|
|
13
|
+
/** One crowd agent's live state, as a game or the devtools overlay reads it. */
|
|
14
|
+
export interface NavCrowdAgentView {
|
|
15
|
+
id: string;
|
|
16
|
+
position: Vec3;
|
|
17
|
+
velocity: readonly [number, number];
|
|
18
|
+
goal: Vec3;
|
|
19
|
+
/** Index of the waypoint it is steering toward; equals the waypoint count once the route is done. */
|
|
20
|
+
waypoint: number;
|
|
21
|
+
waypoints: number;
|
|
22
|
+
arrived: boolean;
|
|
23
|
+
/** True while the agent waits for its turn in the bounded re-plan budget. */
|
|
24
|
+
waitingForPlan: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Result of telling the crowd the nav mesh changed. */
|
|
27
|
+
export interface NavCrowdRevalidate {
|
|
28
|
+
/** Agents whose remaining route crossed replaced polygons and were queued for a new plan. */
|
|
29
|
+
queued: number;
|
|
30
|
+
/** Agents whose route stayed entirely on kept polygons and was left alone. */
|
|
31
|
+
untouched: number;
|
|
32
|
+
}
|
|
33
|
+
/** Counters a devtools probe or test reads. */
|
|
34
|
+
export interface NavCrowdStats {
|
|
35
|
+
agents: number;
|
|
36
|
+
arrived: number;
|
|
37
|
+
waitingForPlan: number;
|
|
38
|
+
/** Path searches run since creation — add, setGoal, queued plans and repaths. */
|
|
39
|
+
plans: number;
|
|
40
|
+
/** Path searches that returned no route. */
|
|
41
|
+
unroutable: number;
|
|
42
|
+
ticks: number;
|
|
43
|
+
/** Slowest single `tick` seen so far, in milliseconds. */
|
|
44
|
+
slowestTickMs: number;
|
|
45
|
+
}
|
|
46
|
+
/** Serializable crowd state: agents, their routes, and the avoidance tuning they run under. */
|
|
47
|
+
export interface NavCrowdSnapshot {
|
|
48
|
+
tunables: {
|
|
49
|
+
speed: number;
|
|
50
|
+
radius: number;
|
|
51
|
+
goalRadius: number;
|
|
52
|
+
spread: number;
|
|
53
|
+
waypointRadius: number;
|
|
54
|
+
replanBudget: number;
|
|
55
|
+
replanMs: number;
|
|
56
|
+
avoid: RvoTunables;
|
|
57
|
+
};
|
|
58
|
+
agents: {
|
|
59
|
+
id: string;
|
|
60
|
+
position: Vec3;
|
|
61
|
+
velocity: readonly [number, number];
|
|
62
|
+
goal: Vec3;
|
|
63
|
+
offsetX: number;
|
|
64
|
+
offsetZ: number;
|
|
65
|
+
speed: number;
|
|
66
|
+
radius: number;
|
|
67
|
+
waypoints: Vec3[];
|
|
68
|
+
waypoint: number;
|
|
69
|
+
arrived: boolean;
|
|
70
|
+
polys: number[];
|
|
71
|
+
waitingForPlan: boolean;
|
|
72
|
+
}[];
|
|
73
|
+
}
|
|
74
|
+
/** Tunables for {@link createNavCrowd}, every one retunable in play. */
|
|
75
|
+
export interface NavCrowdOptions {
|
|
76
|
+
/** Default walking speed for agents that do not set their own. */
|
|
77
|
+
speed?: number;
|
|
78
|
+
radius?: number;
|
|
79
|
+
/** Distance to the goal that counts as arrived. Default 0.5. */
|
|
80
|
+
goalRadius?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Radius, in world units, over which agents targeting the same goal fan out onto distinct spots
|
|
83
|
+
* (`ai/crowd`'s stable per-id `spreadOffset`). `0` walks every agent to the exact goal. Default 0.
|
|
84
|
+
*/
|
|
85
|
+
spread?: number;
|
|
86
|
+
/** Distance to a waypoint that advances to the next one. Default 0.6. */
|
|
87
|
+
waypointRadius?: number;
|
|
88
|
+
/** Most path searches one `tick` may run for queued agents. Default 8. */
|
|
89
|
+
replanBudget?: number;
|
|
90
|
+
/** Milliseconds of path searching one `tick` may spend, whatever the budget. Default 1.5. */
|
|
91
|
+
replanMs?: number;
|
|
92
|
+
/** Avoidance tuning for the internal {@link RvoCrowd}. */
|
|
93
|
+
avoid?: Partial<RvoTunables>;
|
|
94
|
+
/** Register a devtools probe under this name so the perf overlay shows crowd counters. */
|
|
95
|
+
devtoolsName?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Agents that cross an editor-baked nav mesh: each one plans a corridor with {@link NavRuntime},
|
|
99
|
+
* walks the funnel-smoothed waypoints, and keeps its distance through reciprocal velocity obstacle
|
|
100
|
+
* avoidance. Goals, arrivals and replans are serializable; a mesh edit only re-plans the agents
|
|
101
|
+
* whose remaining route actually crossed the rebuilt region.
|
|
102
|
+
*/
|
|
103
|
+
export interface NavCrowd {
|
|
104
|
+
/** Add an agent and plan its first route; false when the id is already present. */
|
|
105
|
+
add(id: string, spec: NavCrowdAgentSpec): boolean;
|
|
106
|
+
remove(id: string): boolean;
|
|
107
|
+
has(id: string): boolean;
|
|
108
|
+
count(): number;
|
|
109
|
+
ids(): readonly string[];
|
|
110
|
+
/** Send an agent somewhere new; it re-plans on the next tick within the re-plan budget. */
|
|
111
|
+
setGoal(id: string, goal: Vec3): boolean;
|
|
112
|
+
/** Plan one agent now, ignoring the budget. Returns false when no route exists. */
|
|
113
|
+
plan(id: string): boolean;
|
|
114
|
+
agent(id: string): NavCrowdAgentView | null;
|
|
115
|
+
/** Advance every agent by `dt` seconds. */
|
|
116
|
+
tick(dt: number): void;
|
|
117
|
+
/**
|
|
118
|
+
* Queue a re-plan for agents whose remaining route crossed polygons the last
|
|
119
|
+
* {@link NavRuntime.applyMesh} replaced or moved; routes on kept polygons are left intact.
|
|
120
|
+
*/
|
|
121
|
+
revalidate(stats: NavRebuildStats, region?: NavRegion): NavCrowdRevalidate;
|
|
122
|
+
stats(): NavCrowdStats;
|
|
123
|
+
retune(patch: NavCrowdOptions): void;
|
|
124
|
+
snapshot(): NavCrowdSnapshot;
|
|
125
|
+
restore(next: NavCrowdSnapshot): void;
|
|
126
|
+
/** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
|
|
127
|
+
dispose(): void;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Compose {@link createNavRuntime} and {@link createRvoCrowd} into a moving crowd: plan on the mesh,
|
|
131
|
+
* follow the smoothed corridor, avoid each other. Deterministic — agents advance in insertion order
|
|
132
|
+
* and the avoidance solve breaks ties by slot, not chance.
|
|
133
|
+
*
|
|
134
|
+
* @capability nav-crowd move many agents across an editor-baked nav mesh with avoidance
|
|
135
|
+
*/
|
|
136
|
+
export declare function createNavCrowd(options: NavCrowdOptions & {
|
|
137
|
+
nav: NavRuntime;
|
|
138
|
+
}): NavCrowd;
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { devtools } from "../devtools/devtools.js";
|
|
2
|
+
import { createRvoCrowd } from "../ai/rvo.js";
|
|
3
|
+
import { spreadOffset } from "../ai/crowd.js";
|
|
4
|
+
const EPSILON = 1e-9;
|
|
5
|
+
function sameVec(a, b) {
|
|
6
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Compose {@link createNavRuntime} and {@link createRvoCrowd} into a moving crowd: plan on the mesh,
|
|
10
|
+
* follow the smoothed corridor, avoid each other. Deterministic — agents advance in insertion order
|
|
11
|
+
* and the avoidance solve breaks ties by slot, not chance.
|
|
12
|
+
*
|
|
13
|
+
* @capability nav-crowd move many agents across an editor-baked nav mesh with avoidance
|
|
14
|
+
*/
|
|
15
|
+
export function createNavCrowd(options) {
|
|
16
|
+
const nav = options.nav;
|
|
17
|
+
let speed = Math.max(0, options.speed ?? 3);
|
|
18
|
+
let radius = Math.max(0, options.radius ?? 0.4);
|
|
19
|
+
let goalRadius = Math.max(0, options.goalRadius ?? 0.5);
|
|
20
|
+
let spread = Math.max(0, options.spread ?? 0);
|
|
21
|
+
let waypointRadius = Math.max(0, options.waypointRadius ?? 0.6);
|
|
22
|
+
let replanBudget = Math.max(1, Math.floor(options.replanBudget ?? 8));
|
|
23
|
+
let replanMs = Math.max(0, options.replanMs ?? 1.5);
|
|
24
|
+
const agents = [];
|
|
25
|
+
const agentOf = new Map();
|
|
26
|
+
const avoid = createRvoCrowd({ devtoolsName: undefined, ...options.avoid });
|
|
27
|
+
let ticks = 0;
|
|
28
|
+
let plans = 0;
|
|
29
|
+
let unroutable = 0;
|
|
30
|
+
let slowestTickMs = 0;
|
|
31
|
+
const motion = { x: 0, z: 0, vx: 0, vz: 0 };
|
|
32
|
+
let cachedBounds = [0, 0, 0, 0];
|
|
33
|
+
let cachedRebuilds = -1;
|
|
34
|
+
/** Whole-mesh XZ bounds, recomputed only after the runtime exchanged the mesh. */
|
|
35
|
+
function meshBounds() {
|
|
36
|
+
const rebuilds = nav.rebuilds();
|
|
37
|
+
if (rebuilds === cachedRebuilds)
|
|
38
|
+
return cachedBounds;
|
|
39
|
+
const verts = nav.mesh.verts;
|
|
40
|
+
let minX = Infinity;
|
|
41
|
+
let minZ = Infinity;
|
|
42
|
+
let maxX = -Infinity;
|
|
43
|
+
let maxZ = -Infinity;
|
|
44
|
+
for (let i = 0; i < verts.length; i += 3) {
|
|
45
|
+
const x = verts[i];
|
|
46
|
+
const z = verts[i + 2];
|
|
47
|
+
if (x < minX)
|
|
48
|
+
minX = x;
|
|
49
|
+
if (x > maxX)
|
|
50
|
+
maxX = x;
|
|
51
|
+
if (z < minZ)
|
|
52
|
+
minZ = z;
|
|
53
|
+
if (z > maxZ)
|
|
54
|
+
maxZ = z;
|
|
55
|
+
}
|
|
56
|
+
cachedBounds = Number.isFinite(minX) ? [minX, minZ, maxX, maxZ] : [0, 0, 0, 0];
|
|
57
|
+
cachedRebuilds = rebuilds;
|
|
58
|
+
return cachedBounds;
|
|
59
|
+
}
|
|
60
|
+
function probeLine() {
|
|
61
|
+
let arrived = 0;
|
|
62
|
+
let waiting = 0;
|
|
63
|
+
for (const agent of agents) {
|
|
64
|
+
if (agent.arrived)
|
|
65
|
+
arrived += 1;
|
|
66
|
+
if (agent.waitingForPlan)
|
|
67
|
+
waiting += 1;
|
|
68
|
+
}
|
|
69
|
+
return `${agents.length} agents ${arrived} arrived ${waiting} waiting ${plans} plans ${unroutable} unroutable ${ticks} ticks ${slowestTickMs.toFixed(2)} ms slowest`;
|
|
70
|
+
}
|
|
71
|
+
const unregister = options.devtoolsName === undefined ? null : devtools.probes.register(options.devtoolsName, () => probeLine());
|
|
72
|
+
function applyPath(agent, path) {
|
|
73
|
+
plans += 1;
|
|
74
|
+
if (path === null || path.points.length === 0) {
|
|
75
|
+
agent.waypoints = [];
|
|
76
|
+
agent.waypoint = 0;
|
|
77
|
+
agent.polys = [];
|
|
78
|
+
agent.arrived = false;
|
|
79
|
+
agent.waitingForPlan = false;
|
|
80
|
+
unroutable += 1;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
agent.waypoints = path.points.map((point) => [point[0], point[1], point[2]]);
|
|
84
|
+
agent.waypoint = agent.waypoints.length > 1 ? 1 : 0;
|
|
85
|
+
agent.polys = [...path.polys];
|
|
86
|
+
agent.arrived = false;
|
|
87
|
+
agent.waitingForPlan = false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function aim(agent) {
|
|
91
|
+
return [agent.goal[0] + agent.offsetX, agent.goal[1], agent.goal[2] + agent.offsetZ];
|
|
92
|
+
}
|
|
93
|
+
function replan(agent) {
|
|
94
|
+
return applyPath(agent, nav.plan([agent.x, agent.y, agent.z], aim(agent)));
|
|
95
|
+
}
|
|
96
|
+
function index(id) {
|
|
97
|
+
return agentOf.get(id);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
add(id, spec) {
|
|
101
|
+
if (agentOf.has(id))
|
|
102
|
+
return false;
|
|
103
|
+
const [offsetX, offsetZ] = spread > 0 ? spreadOffset(id, spread) : [0, 0];
|
|
104
|
+
const agent = {
|
|
105
|
+
id,
|
|
106
|
+
goal: spec.goal,
|
|
107
|
+
offsetX,
|
|
108
|
+
offsetZ,
|
|
109
|
+
speed: Math.max(0, spec.speed ?? speed),
|
|
110
|
+
radius: Math.max(0, spec.radius ?? radius),
|
|
111
|
+
waypoints: [],
|
|
112
|
+
waypoint: 0,
|
|
113
|
+
polys: [],
|
|
114
|
+
arrived: false,
|
|
115
|
+
waitingForPlan: false,
|
|
116
|
+
x: spec.position[0],
|
|
117
|
+
y: spec.position[1],
|
|
118
|
+
z: spec.position[2],
|
|
119
|
+
velocity: [0, 0],
|
|
120
|
+
};
|
|
121
|
+
agents.push(agent);
|
|
122
|
+
agentOf.set(id, agents.length - 1);
|
|
123
|
+
avoid.add(id, { x: spec.position[0], z: spec.position[2], radius: agent.radius, maxSpeed: agent.speed });
|
|
124
|
+
replan(agent);
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
remove(id) {
|
|
128
|
+
const at = index(id);
|
|
129
|
+
if (at === undefined)
|
|
130
|
+
return false;
|
|
131
|
+
const last = agents.pop();
|
|
132
|
+
agentOf.delete(id);
|
|
133
|
+
if (at < agents.length) {
|
|
134
|
+
agents[at] = last;
|
|
135
|
+
agentOf.set(last.id, at);
|
|
136
|
+
}
|
|
137
|
+
avoid.remove(id);
|
|
138
|
+
return true;
|
|
139
|
+
},
|
|
140
|
+
has: (id) => agentOf.has(id),
|
|
141
|
+
count: () => agents.length,
|
|
142
|
+
ids: () => agents.map((agent) => agent.id),
|
|
143
|
+
setGoal(id, goal) {
|
|
144
|
+
const at = index(id);
|
|
145
|
+
if (at === undefined)
|
|
146
|
+
return false;
|
|
147
|
+
const agent = agents[at];
|
|
148
|
+
if (agent.arrived && sameVec(agent.goal, goal))
|
|
149
|
+
return true;
|
|
150
|
+
agent.goal = goal;
|
|
151
|
+
agent.arrived = false;
|
|
152
|
+
agent.waitingForPlan = true;
|
|
153
|
+
return true;
|
|
154
|
+
},
|
|
155
|
+
plan(id) {
|
|
156
|
+
const at = index(id);
|
|
157
|
+
if (at === undefined)
|
|
158
|
+
return false;
|
|
159
|
+
return replan(agents[at]);
|
|
160
|
+
},
|
|
161
|
+
agent(id) {
|
|
162
|
+
const at = index(id);
|
|
163
|
+
if (at === undefined)
|
|
164
|
+
return null;
|
|
165
|
+
const agent = agents[at];
|
|
166
|
+
return {
|
|
167
|
+
id: agent.id,
|
|
168
|
+
position: [agent.x, agent.y, agent.z],
|
|
169
|
+
velocity: agent.velocity,
|
|
170
|
+
goal: agent.goal,
|
|
171
|
+
waypoint: agent.waypoint,
|
|
172
|
+
waypoints: agent.waypoints.length,
|
|
173
|
+
arrived: agent.arrived,
|
|
174
|
+
waitingForPlan: agent.waitingForPlan,
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
tick(dt) {
|
|
178
|
+
if (!(dt > 0) || agents.length === 0)
|
|
179
|
+
return;
|
|
180
|
+
const started = performance.now();
|
|
181
|
+
let budget = replanBudget;
|
|
182
|
+
const replanDeadline = started + replanMs;
|
|
183
|
+
for (const agent of agents) {
|
|
184
|
+
if (!agent.waitingForPlan)
|
|
185
|
+
continue;
|
|
186
|
+
if (budget <= 0)
|
|
187
|
+
break;
|
|
188
|
+
if (performance.now() > replanDeadline)
|
|
189
|
+
break;
|
|
190
|
+
budget -= 1;
|
|
191
|
+
replan(agent);
|
|
192
|
+
}
|
|
193
|
+
for (const agent of agents) {
|
|
194
|
+
const target = agent.arrived ? null : agent.waypoints[agent.waypoint];
|
|
195
|
+
if (target === null || target === undefined) {
|
|
196
|
+
// A route that ended short of the goal (congestion pushed it away) needs a new plan, or the
|
|
197
|
+
// agent would stand there with no target forever.
|
|
198
|
+
if (!agent.arrived && !agent.waitingForPlan && agent.waypoints.length > 0)
|
|
199
|
+
agent.waitingForPlan = true;
|
|
200
|
+
avoid.setPreferredVelocity(agent.id, 0, 0);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const dx = target[0] - agent.x;
|
|
204
|
+
const dz = target[2] - agent.z;
|
|
205
|
+
const distance = Math.hypot(dx, dz);
|
|
206
|
+
if (distance <= EPSILON) {
|
|
207
|
+
avoid.setPreferredVelocity(agent.id, 0, 0);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const want = Math.min(agent.speed, distance / dt);
|
|
211
|
+
avoid.setPreferredVelocity(agent.id, (dx / distance) * want, (dz / distance) * want);
|
|
212
|
+
}
|
|
213
|
+
avoid.step(dt);
|
|
214
|
+
const bounds = meshBounds();
|
|
215
|
+
for (const agent of agents) {
|
|
216
|
+
if (!avoid.readInto(agent.id, motion))
|
|
217
|
+
continue;
|
|
218
|
+
agent.velocity[0] = motion.vx;
|
|
219
|
+
agent.velocity[1] = motion.vz;
|
|
220
|
+
let x = motion.x;
|
|
221
|
+
let z = motion.z;
|
|
222
|
+
let y = agent.y;
|
|
223
|
+
// Avoidance pushes agents apart with no idea where the walls are; keep each one inside the
|
|
224
|
+
// baked mesh's bounds so a shove cannot walk it off the walkable surface. Interior walls stay
|
|
225
|
+
// the game's collision layer: this seam has the outer bound, not a baked obstacle field.
|
|
226
|
+
if (x < bounds[0] - 0.01 || x > bounds[2] + 0.01 || z < bounds[1] - 0.01 || z > bounds[3] + 0.01) {
|
|
227
|
+
const snapped = nav.closestPoint([x, y, z]);
|
|
228
|
+
if (snapped !== null) {
|
|
229
|
+
x = snapped[0];
|
|
230
|
+
y = snapped[1];
|
|
231
|
+
z = snapped[2];
|
|
232
|
+
avoid.setKinematics(agent.id, { x, z });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let advanced = true;
|
|
236
|
+
for (let guard = agent.waypoints.length + 1; advanced && guard > 0; guard -= 1) {
|
|
237
|
+
advanced = false;
|
|
238
|
+
const target = agent.waypoints[agent.waypoint];
|
|
239
|
+
if (target === undefined)
|
|
240
|
+
break;
|
|
241
|
+
if (Math.hypot(target[0] - x, target[2] - z) <= waypointRadius) {
|
|
242
|
+
y = target[1];
|
|
243
|
+
agent.waypoint += 1;
|
|
244
|
+
advanced = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
agent.x = x;
|
|
248
|
+
agent.y = y;
|
|
249
|
+
agent.z = z;
|
|
250
|
+
const goalDistance = Math.hypot(agent.goal[0] + agent.offsetX - x, agent.goal[2] + agent.offsetZ - z);
|
|
251
|
+
if (agent.waypoint >= agent.waypoints.length && goalDistance <= Math.max(goalRadius, waypointRadius)) {
|
|
252
|
+
agent.arrived = true;
|
|
253
|
+
agent.waypoints = [];
|
|
254
|
+
agent.waypoint = 0;
|
|
255
|
+
agent.polys = [];
|
|
256
|
+
}
|
|
257
|
+
else if (agent.arrived && goalDistance > Math.max(goalRadius, waypointRadius)) {
|
|
258
|
+
agent.arrived = false;
|
|
259
|
+
agent.waitingForPlan = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
ticks += 1;
|
|
263
|
+
slowestTickMs = Math.max(slowestTickMs, performance.now() - started);
|
|
264
|
+
},
|
|
265
|
+
revalidate(stats, region) {
|
|
266
|
+
let queued = 0;
|
|
267
|
+
let untouched = 0;
|
|
268
|
+
for (const agent of agents) {
|
|
269
|
+
if (agent.waitingForPlan) {
|
|
270
|
+
queued += 1;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (agent.waypoints.length === 0) {
|
|
274
|
+
untouched += 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
let crossed = false;
|
|
278
|
+
for (const poly of agent.polys) {
|
|
279
|
+
if (poly < 0 || poly >= stats.remap.length || stats.remap[poly] === -1) {
|
|
280
|
+
crossed = true;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!crossed && region !== undefined) {
|
|
285
|
+
for (let i = agent.waypoint; i < agent.waypoints.length && !crossed; i += 1) {
|
|
286
|
+
const point = agent.waypoints[i];
|
|
287
|
+
crossed = point[0] >= region.min[0] && point[0] <= region.max[0] && point[2] >= region.min[1] && point[2] <= region.max[1];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (crossed) {
|
|
291
|
+
agent.waitingForPlan = true;
|
|
292
|
+
queued += 1;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
untouched += 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { queued, untouched };
|
|
299
|
+
},
|
|
300
|
+
stats() {
|
|
301
|
+
let arrived = 0;
|
|
302
|
+
let waiting = 0;
|
|
303
|
+
for (const agent of agents) {
|
|
304
|
+
if (agent.arrived)
|
|
305
|
+
arrived += 1;
|
|
306
|
+
if (agent.waitingForPlan)
|
|
307
|
+
waiting += 1;
|
|
308
|
+
}
|
|
309
|
+
return { agents: agents.length, arrived, waitingForPlan: waiting, plans, unroutable, ticks, slowestTickMs };
|
|
310
|
+
},
|
|
311
|
+
retune(patch) {
|
|
312
|
+
if (patch.speed !== undefined)
|
|
313
|
+
speed = Math.max(0, patch.speed);
|
|
314
|
+
if (patch.radius !== undefined)
|
|
315
|
+
radius = Math.max(0, patch.radius);
|
|
316
|
+
if (patch.goalRadius !== undefined)
|
|
317
|
+
goalRadius = Math.max(0, patch.goalRadius);
|
|
318
|
+
if (patch.spread !== undefined) {
|
|
319
|
+
spread = Math.max(0, patch.spread);
|
|
320
|
+
for (const agent of agents) {
|
|
321
|
+
const [offsetX, offsetZ] = spread > 0 ? spreadOffset(agent.id, spread) : [0, 0];
|
|
322
|
+
agent.offsetX = offsetX;
|
|
323
|
+
agent.offsetZ = offsetZ;
|
|
324
|
+
agent.arrived = false;
|
|
325
|
+
agent.waitingForPlan = true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (patch.waypointRadius !== undefined)
|
|
329
|
+
waypointRadius = Math.max(0, patch.waypointRadius);
|
|
330
|
+
if (patch.replanBudget !== undefined)
|
|
331
|
+
replanBudget = Math.max(1, Math.floor(patch.replanBudget));
|
|
332
|
+
if (patch.replanMs !== undefined)
|
|
333
|
+
replanMs = Math.max(0, patch.replanMs);
|
|
334
|
+
if (patch.avoid !== undefined)
|
|
335
|
+
avoid.retune(patch.avoid);
|
|
336
|
+
},
|
|
337
|
+
snapshot() {
|
|
338
|
+
const avoidState = avoid.snapshot();
|
|
339
|
+
return {
|
|
340
|
+
tunables: {
|
|
341
|
+
speed,
|
|
342
|
+
radius,
|
|
343
|
+
goalRadius,
|
|
344
|
+
spread,
|
|
345
|
+
waypointRadius,
|
|
346
|
+
replanBudget,
|
|
347
|
+
replanMs,
|
|
348
|
+
avoid: { ...avoidState.tunables },
|
|
349
|
+
},
|
|
350
|
+
agents: agents.map((agent) => ({
|
|
351
|
+
id: agent.id,
|
|
352
|
+
position: [agent.x, agent.y, agent.z],
|
|
353
|
+
velocity: [agent.velocity[0], agent.velocity[1]],
|
|
354
|
+
goal: agent.goal,
|
|
355
|
+
offsetX: agent.offsetX,
|
|
356
|
+
offsetZ: agent.offsetZ,
|
|
357
|
+
speed: agent.speed,
|
|
358
|
+
radius: agent.radius,
|
|
359
|
+
waypoints: agent.waypoints.map((point) => [point[0], point[1], point[2]]),
|
|
360
|
+
waypoint: agent.waypoint,
|
|
361
|
+
arrived: agent.arrived,
|
|
362
|
+
polys: [...agent.polys],
|
|
363
|
+
waitingForPlan: agent.waitingForPlan,
|
|
364
|
+
})),
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
restore(next) {
|
|
368
|
+
agents.length = 0;
|
|
369
|
+
agentOf.clear();
|
|
370
|
+
speed = next.tunables.speed;
|
|
371
|
+
radius = next.tunables.radius;
|
|
372
|
+
goalRadius = next.tunables.goalRadius;
|
|
373
|
+
spread = Math.max(0, next.tunables.spread ?? 0);
|
|
374
|
+
waypointRadius = next.tunables.waypointRadius;
|
|
375
|
+
replanBudget = next.tunables.replanBudget;
|
|
376
|
+
replanMs = next.tunables.replanMs ?? 1.5;
|
|
377
|
+
avoid.restore({ tunables: { ...next.tunables.avoid }, agents: [] });
|
|
378
|
+
for (const saved of next.agents) {
|
|
379
|
+
const agent = {
|
|
380
|
+
id: saved.id,
|
|
381
|
+
goal: saved.goal,
|
|
382
|
+
offsetX: saved.offsetX,
|
|
383
|
+
offsetZ: saved.offsetZ,
|
|
384
|
+
speed: saved.speed,
|
|
385
|
+
radius: saved.radius,
|
|
386
|
+
waypoints: saved.waypoints.map((point) => [point[0], point[1], point[2]]),
|
|
387
|
+
waypoint: saved.waypoint,
|
|
388
|
+
polys: [...saved.polys],
|
|
389
|
+
arrived: saved.arrived,
|
|
390
|
+
waitingForPlan: saved.waitingForPlan,
|
|
391
|
+
x: saved.position[0],
|
|
392
|
+
y: saved.position[1],
|
|
393
|
+
z: saved.position[2],
|
|
394
|
+
velocity: [saved.velocity[0], saved.velocity[1]],
|
|
395
|
+
};
|
|
396
|
+
agents.push(agent);
|
|
397
|
+
agentOf.set(agent.id, agents.length - 1);
|
|
398
|
+
avoid.add(agent.id, {
|
|
399
|
+
x: saved.position[0],
|
|
400
|
+
z: saved.position[2],
|
|
401
|
+
radius: saved.radius,
|
|
402
|
+
maxSpeed: saved.speed,
|
|
403
|
+
vx: saved.velocity[0],
|
|
404
|
+
vz: saved.velocity[1],
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
dispose() {
|
|
409
|
+
unregister?.();
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Vec3 } from "../world/geometry.js";
|
|
2
|
+
import { type NavMeshData, type NavMeshLink, type NavMeshPath, type NavMeshQueryOptions } from "./navMesh.js";
|
|
3
|
+
/** What an authored off-mesh connection crosses; the runtime prices it by kind unless the author sets a cost. */
|
|
4
|
+
export type NavLinkKind = "door" | "ladder" | "jump" | "link";
|
|
5
|
+
/**
|
|
6
|
+
* A connection the scene author places between two spots — a door through a wall, a ladder to a
|
|
7
|
+
* deck, a jump across a gap. Authored as world points, not polygon ids, so it survives a re-bake:
|
|
8
|
+
* {@link NavRuntime.applyMesh} resolves each end onto the nearest walkable polygon.
|
|
9
|
+
*/
|
|
10
|
+
export interface AuthoredNavLink {
|
|
11
|
+
/** Stable authored id; reported by `unresolvedLinks()` when an end no longer touches the mesh. */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Traversal kind; prices the link when `cost` is absent. Default `"link"`. */
|
|
14
|
+
kind?: NavLinkKind;
|
|
15
|
+
/** World point the agent leaves from. */
|
|
16
|
+
from: Vec3;
|
|
17
|
+
/** World point the agent arrives at. */
|
|
18
|
+
to: Vec3;
|
|
19
|
+
/** Path cost in world units; overrides the kind's price. Non-finite or negative falls back to the kind. */
|
|
20
|
+
cost?: number;
|
|
21
|
+
}
|
|
22
|
+
/** An XZ box naming the authored area a scene edit touched, so routing state elsewhere is left alone. */
|
|
23
|
+
export interface NavRegion {
|
|
24
|
+
min: readonly [number, number];
|
|
25
|
+
max: readonly [number, number];
|
|
26
|
+
}
|
|
27
|
+
/** How one {@link NavRuntime.applyMesh} exchange changed the mesh. */
|
|
28
|
+
export interface NavRebuildStats {
|
|
29
|
+
/** Polygons in the new bake that have no predecessor. */
|
|
30
|
+
added: number;
|
|
31
|
+
/** Older polygons the new bake no longer contains. */
|
|
32
|
+
removed: number;
|
|
33
|
+
/** Older polygons the new bake still contains, matched by geometry — their routes stay valid. */
|
|
34
|
+
kept: number;
|
|
35
|
+
/** Old polygon index → its index in the new mesh, or `-1` when that polygon is gone. */
|
|
36
|
+
remap: Int32Array;
|
|
37
|
+
}
|
|
38
|
+
/** Serializable tuning and authored links of a {@link NavRuntime}; the baked geometry is replaced by `applyMesh`. */
|
|
39
|
+
export interface NavRuntimeSnapshot {
|
|
40
|
+
areaCosts: Record<number, number>;
|
|
41
|
+
maxNodes: number;
|
|
42
|
+
links: AuthoredNavLink[];
|
|
43
|
+
linkCosts: Partial<Record<NavLinkKind, number>>;
|
|
44
|
+
linkSnapRadius: number;
|
|
45
|
+
}
|
|
46
|
+
/** Runtime seam over a baked {@link NavMeshData}: route, price areas, resolve authored links, rebuild in place. */
|
|
47
|
+
export interface NavRuntime {
|
|
48
|
+
/** The mesh currently routed on — a bake plus the resolved links. */
|
|
49
|
+
readonly mesh: NavMeshData;
|
|
50
|
+
/** Cheapest polygon corridor between two points, funnel-smoothed by the mesh query. */
|
|
51
|
+
plan(from: Vec3, to: Vec3): NavMeshPath | null;
|
|
52
|
+
findPolygon(point: Vec3): number | null;
|
|
53
|
+
closestPoint(point: Vec3): Vec3 | null;
|
|
54
|
+
/** True when the straight segment stays on walkable, unblocked polygons. */
|
|
55
|
+
raycast(from: Vec3, to: Vec3): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Exchange the bake for a newer one. The new geometry is authoritative; polygons that survive the
|
|
58
|
+
* re-bake keep their identity through `remap`, so routing state over them stays valid and only the
|
|
59
|
+
* routes crossing changed polygons need re-planning.
|
|
60
|
+
*/
|
|
61
|
+
applyMesh(next: NavMeshData): NavRebuildStats;
|
|
62
|
+
/** Resolved links the router is using. */
|
|
63
|
+
links(): readonly NavMeshLink[];
|
|
64
|
+
/** Authored ids whose ends no longer reach the mesh. */
|
|
65
|
+
unresolvedLinks(): readonly string[];
|
|
66
|
+
/** How many times the mesh was exchanged. */
|
|
67
|
+
rebuilds(): number;
|
|
68
|
+
retune(patch: NavRuntimeOptions): void;
|
|
69
|
+
snapshot(): NavRuntimeSnapshot;
|
|
70
|
+
restore(next: NavRuntimeSnapshot): void;
|
|
71
|
+
/** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
|
|
72
|
+
dispose(): void;
|
|
73
|
+
}
|
|
74
|
+
/** Options for {@link createNavRuntime}. */
|
|
75
|
+
export interface NavRuntimeOptions extends NavMeshQueryOptions {
|
|
76
|
+
/** Authored off-mesh connections resolved onto the current mesh. */
|
|
77
|
+
links?: readonly AuthoredNavLink[];
|
|
78
|
+
/** Price per link kind, in path-cost units; defaults `door` 0.5, `jump` 1.5, `ladder` 4, `link` 0. */
|
|
79
|
+
linkCosts?: Partial<Record<NavLinkKind, number>>;
|
|
80
|
+
/** How far an authored link end may sit from the mesh before the link is reported unresolved. Default 2. */
|
|
81
|
+
linkSnapRadius?: number;
|
|
82
|
+
/** Register a devtools probe under this name so the perf overlay shows mesh and routing counters. */
|
|
83
|
+
devtoolsName?: string;
|
|
84
|
+
}
|
|
85
|
+
/** A bake stored on an editor scene document, structurally compatible with `EditorBake`. */
|
|
86
|
+
export interface SceneBake {
|
|
87
|
+
kind: string;
|
|
88
|
+
id: string;
|
|
89
|
+
data: unknown;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read the named navigation bake an editor wrote onto a scene document. Returns `null` when the
|
|
93
|
+
* document holds no such bake yet, and throws when the stored bake is not a valid nav mesh — a
|
|
94
|
+
* corrupt bake should fail at load, not as agents walking through walls.
|
|
95
|
+
*
|
|
96
|
+
* @capability navmesh-from-scene read the editor's baked nav mesh off a scene document
|
|
97
|
+
*/
|
|
98
|
+
export declare function navMeshFromBake(bakes: readonly SceneBake[] | undefined, id?: string): NavMeshData | null;
|
|
99
|
+
/**
|
|
100
|
+
* Route agents over the nav mesh an editor baked. Wraps {@link createNavMeshQuery}, resolves authored
|
|
101
|
+
* off-mesh links (doors, ladders, jumps) onto the current surface, and exchanges the mesh one scene
|
|
102
|
+
* region at a time so untouched polygons keep their ids and the agents routing over them keep their paths.
|
|
103
|
+
*
|
|
104
|
+
* @capability nav-runtime route agents on an editor-baked nav mesh with authored links and re-bakes that keep polygon identity
|
|
105
|
+
*/
|
|
106
|
+
export declare function createNavRuntime(options: NavRuntimeOptions & {
|
|
107
|
+
mesh: NavMeshData;
|
|
108
|
+
}): NavRuntime;
|