@jgengine/core 0.25.0 → 0.26.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 +65 -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/gameplay.d.ts +4 -0
- package/dist/gameplay.js +4 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +56 -1
- 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/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/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/features.d.ts +8 -0
- package/dist/world.d.ts +5 -0
- package/dist/world.js +5 -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 +17 -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 +8 -0
- package/skills/jgengine-gameplay/api.md +71 -1
- package/skills/jgengine-gameplay/capabilities.md +16 -0
- package/skills/jgengine-world/SKILL.md +3 -1
- package/skills/jgengine-world/api.md +107 -3
- package/skills/jgengine-world/capabilities.md +22 -2
- package/skills/jgengine-world/recipes/npc-senses.md +8 -2
|
@@ -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;
|