@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
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import type { AutoExposureConfig } from "./colorPipeline.js";
|
|
2
|
+
/**
|
|
3
|
+
* Renderer tone-mapping curve applied by the post chain's output stage. `"agx"` is the pipeline
|
|
4
|
+
* default: it keeps hue in saturated highlights and rolls a sun-lit surface off gradually, where
|
|
5
|
+
* `"aces"` skews bright colours toward white.
|
|
6
|
+
*/
|
|
2
7
|
export type ToneMappingMode = "aces" | "agx" | "reinhard" | "cineon" | "linear" | "none";
|
|
8
|
+
/**
|
|
9
|
+
* A baked display-space LUT cube: `size³` RGB entries with red varying fastest, three bytes per entry
|
|
10
|
+
* in 0..255. Kept as plain data so a scene document can author one and a test can assert on it.
|
|
11
|
+
*/
|
|
12
|
+
export interface LutCube {
|
|
13
|
+
/** Edge length of the cube, 2..16. */
|
|
14
|
+
size: number;
|
|
15
|
+
/** `size³ × 3` bytes, red fastest, then green, then blue. */
|
|
16
|
+
data: Uint8Array;
|
|
17
|
+
}
|
|
18
|
+
/** A grade's LUT: a strip PNG (one `size`-wide tile per blue slice) or a baked cube. */
|
|
19
|
+
export type GradeLut = {
|
|
20
|
+
url: string;
|
|
21
|
+
size?: number;
|
|
22
|
+
} | {
|
|
23
|
+
cube: LutCube;
|
|
24
|
+
};
|
|
3
25
|
/** UnrealBloom stage — soft HDR glow around bright pixels (sun, glints, emissive). */
|
|
4
26
|
export interface BloomConfig {
|
|
5
27
|
/** Glow intensity. Default 0.32. */
|
|
@@ -26,11 +48,12 @@ export interface AoConfig {
|
|
|
26
48
|
}
|
|
27
49
|
/** Final colour-grade stage: lift/gain/gamma, saturation, vignette, film grain — applied in display space after tone mapping. */
|
|
28
50
|
export interface GradeConfig {
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Colour LUT applied after the numeric grade, in display space. Either a strip PNG (one
|
|
53
|
+
* `size`-wide tile per blue slice, trilinearly sampled) or a baked {@link LutCube} such as the one
|
|
54
|
+
* a scene document's authored grading produces.
|
|
55
|
+
*/
|
|
56
|
+
lut?: GradeLut;
|
|
34
57
|
/** Additive shadow tint (RGB, ~0..0.1). Default cool [0.012, 0.010, 0.018]. */
|
|
35
58
|
lift?: readonly [number, number, number];
|
|
36
59
|
/** Multiplicative highlight tint (RGB, ~1). Default warm [1.05, 1.02, 0.98]. */
|
|
@@ -99,10 +122,19 @@ export type PostAaMode = "smaa" | "msaa" | false;
|
|
|
99
122
|
export interface PostProcessingConfig {
|
|
100
123
|
/** Master switch; a config object with `enabled: false` mounts nothing. Default true. */
|
|
101
124
|
enabled?: boolean;
|
|
102
|
-
/** Tone-mapping curve the output stage applies. Default "
|
|
125
|
+
/** Tone-mapping curve the output stage applies. Default "agx". */
|
|
103
126
|
toneMapping?: ToneMappingMode;
|
|
104
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Exposure multiplier applied before tone mapping. Default 1. With `autoExposure` on this is the
|
|
129
|
+
* baseline the meter corrects around, not a fixed value.
|
|
130
|
+
*/
|
|
105
131
|
exposure?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Clamped auto-exposure: meters the rendered frame and adapts exposure toward
|
|
134
|
+
* {@link import("./colorPipeline.js").MID_GREY}, bounded by `min`/`max`. Omitted uses
|
|
135
|
+
* {@link import("./colorPipeline.js").DEFAULT_AUTO_EXPOSURE} on every tier; `false` pins `exposure`.
|
|
136
|
+
*/
|
|
137
|
+
autoExposure?: AutoExposureConfig | false;
|
|
106
138
|
/**
|
|
107
139
|
* Antialiasing. Default `"smaa"` (plus a light MSAA resolve on the beauty target).
|
|
108
140
|
* Set `"msaa"` to skip the SMAA pass, or `false` for neither.
|
|
@@ -7,7 +7,7 @@ export const DEFAULT_AO = { radius: 1.3, intensity: 1.3, distanceFalloff: 2.8, b
|
|
|
7
7
|
* is left off by default (it needs a per-scene focus distance); set `dof` to enable it.
|
|
8
8
|
*/
|
|
9
9
|
export const STUDIO_STAGE_POST = {
|
|
10
|
-
toneMapping: "
|
|
10
|
+
toneMapping: "agx",
|
|
11
11
|
exposure: 1.05,
|
|
12
12
|
bloom: { strength: 0.28, radius: 0.6, threshold: 0.8 },
|
|
13
13
|
ao: { radius: 1.1, intensity: 1.5, distanceFalloff: 3, blend: 1 },
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { LightingConfig } from "../game/playableGame.js";
|
|
2
|
+
import type { SkyEnvironmentConfig } from "../world/features.js";
|
|
3
|
+
/**
|
|
4
|
+
* Which half of a game's authored `lighting` a time-of-day sky takes over.
|
|
5
|
+
*
|
|
6
|
+
* A time-of-day sky has to move the sun or the whole world stays lit like noon after dark: the
|
|
7
|
+
* dome turns black while the ground keeps its midday key light. So under a day cycle the first
|
|
8
|
+
* directional light is the **sun**, and `ambient`/`hemisphere` are the **noon keyframes** of the
|
|
9
|
+
* ambient fill — the same convention the sky already uses for `horizonColor`/`zenithColor`. Every
|
|
10
|
+
* other light (fill directionals, point and spot lamps) stays exactly as authored.
|
|
11
|
+
*/
|
|
12
|
+
export interface SkyOwnedLighting {
|
|
13
|
+
/** Sun + ambient terms the sky now drives from the day cycle; intensities read as noon values. */
|
|
14
|
+
sky: LightingConfig;
|
|
15
|
+
/** Lights the sky does not own; mounted as authored. */
|
|
16
|
+
rest: LightingConfig;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Splits authored lighting into the sun/ambient terms a time-of-day sky drives and the rest.
|
|
20
|
+
* Returns an empty `sky` when the game authored no lighting, so callers can keep engine defaults.
|
|
21
|
+
* @capability day-night-sky hand the sun and ambient fill to the day cycle while authored lamps keep running
|
|
22
|
+
*/
|
|
23
|
+
export declare function splitSkyOwnedLighting(lighting: LightingConfig | undefined): SkyOwnedLighting;
|
|
24
|
+
/**
|
|
25
|
+
* The authored `lighting` as the day cycle's **noon keyframes**, filling only the sky fields the
|
|
26
|
+
* game left unset. A game's midday sun, fill and fog therefore become the peak the cycle scales
|
|
27
|
+
* and cross-fades from, so an authored world gets a sunset and a night without re-authoring the
|
|
28
|
+
* same numbers in the sky. Authored sky fields always win over the lighting config.
|
|
29
|
+
* @capability day-night-sky read authored lighting as the day cycle's noon keyframe
|
|
30
|
+
*/
|
|
31
|
+
export declare function skyNoonKeyframes<T extends SkyEnvironmentConfig>(sky: T, lighting: LightingConfig | undefined, fogColor?: string): T;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits authored lighting into the sun/ambient terms a time-of-day sky drives and the rest.
|
|
3
|
+
* Returns an empty `sky` when the game authored no lighting, so callers can keep engine defaults.
|
|
4
|
+
* @capability day-night-sky hand the sun and ambient fill to the day cycle while authored lamps keep running
|
|
5
|
+
*/
|
|
6
|
+
export function splitSkyOwnedLighting(lighting) {
|
|
7
|
+
if (lighting === undefined)
|
|
8
|
+
return { sky: {}, rest: {} };
|
|
9
|
+
const directional = lighting.directional ?? [];
|
|
10
|
+
const [sun, ...fill] = directional;
|
|
11
|
+
const sky = {
|
|
12
|
+
...(sun === undefined ? {} : { directional: [sun] }),
|
|
13
|
+
...(lighting.ambient === undefined ? {} : { ambient: lighting.ambient }),
|
|
14
|
+
...(lighting.hemisphere === undefined ? {} : { hemisphere: lighting.hemisphere }),
|
|
15
|
+
};
|
|
16
|
+
const rest = {
|
|
17
|
+
...(fill.length === 0 ? {} : { directional: fill }),
|
|
18
|
+
...(lighting.point === undefined ? {} : { point: lighting.point }),
|
|
19
|
+
...(lighting.spot === undefined ? {} : { spot: lighting.spot }),
|
|
20
|
+
...(lighting.maxDynamicLights === undefined ? {} : { maxDynamicLights: lighting.maxDynamicLights }),
|
|
21
|
+
...(lighting.iblAmbientScale === undefined ? {} : { iblAmbientScale: lighting.iblAmbientScale }),
|
|
22
|
+
...(lighting.fallbackKey === undefined ? {} : { fallbackKey: lighting.fallbackKey }),
|
|
23
|
+
};
|
|
24
|
+
return { sky, rest };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The authored `lighting` as the day cycle's **noon keyframes**, filling only the sky fields the
|
|
28
|
+
* game left unset. A game's midday sun, fill and fog therefore become the peak the cycle scales
|
|
29
|
+
* and cross-fades from, so an authored world gets a sunset and a night without re-authoring the
|
|
30
|
+
* same numbers in the sky. Authored sky fields always win over the lighting config.
|
|
31
|
+
* @capability day-night-sky read authored lighting as the day cycle's noon keyframe
|
|
32
|
+
*/
|
|
33
|
+
export function skyNoonKeyframes(sky, lighting, fogColor) {
|
|
34
|
+
const sun = lighting?.directional?.[0];
|
|
35
|
+
const fill = lighting?.hemisphere?.intensity ?? lighting?.ambient?.intensity;
|
|
36
|
+
const noon = { ...sky };
|
|
37
|
+
if (noon.sunIntensity === undefined && sun?.intensity !== undefined)
|
|
38
|
+
noon.sunIntensity = sun.intensity;
|
|
39
|
+
if (noon.ambientIntensity === undefined && fill !== undefined)
|
|
40
|
+
noon.ambientIntensity = fill;
|
|
41
|
+
if (noon.sunColor === undefined && sun?.color !== undefined)
|
|
42
|
+
noon.sunColor = sun.color;
|
|
43
|
+
if (noon.fogColor === undefined && fogColor !== undefined)
|
|
44
|
+
noon.fogColor = fogColor;
|
|
45
|
+
return noon;
|
|
46
|
+
}
|
|
@@ -13,6 +13,7 @@ import { registerWaterKind } from "../world/waterKind.js";
|
|
|
13
13
|
import { registerGrassKind } from "../world/grassKind.js";
|
|
14
14
|
import { registerSoilKind } from "../world/soilKind.js";
|
|
15
15
|
import { registerPoleLineKind } from "../world/poleLineKind.js";
|
|
16
|
+
import { registerDecalKind } from "../world/decalKind.js";
|
|
16
17
|
import { registerBuildingGenerator } from "../world/buildingGenerator.js";
|
|
17
18
|
import { registerCityKind } from "../world/cityKind.js";
|
|
18
19
|
import { describeScatterCoverage, densityCoverage } from "../world/scatterCoverage.js";
|
|
@@ -80,5 +81,6 @@ export function registerBuiltinSceneKinds() {
|
|
|
80
81
|
registerSoilKind();
|
|
81
82
|
registerCityKind();
|
|
82
83
|
registerPoleLineKind();
|
|
84
|
+
registerDecalKind();
|
|
83
85
|
registerBuildingGenerator();
|
|
84
86
|
}
|
|
@@ -25,11 +25,17 @@ export interface PerceptionCandidate {
|
|
|
25
25
|
/** A remembered target and its linearly decaying confidence. */
|
|
26
26
|
export interface PerceptionMemory {
|
|
27
27
|
targetId: string;
|
|
28
|
+
/** When the fact was observed, on the same clock `observe` is given; its age is `now − lastSeenAt`. */
|
|
28
29
|
lastSeenAt: number;
|
|
29
30
|
lastKnownPos: PerceptionPosition;
|
|
31
|
+
/** 1 at the moment of the sighting, falling to 0 across `memorySeconds`. */
|
|
30
32
|
confidence: number;
|
|
33
|
+
/** Whose senses produced the fact: this observer's id for its own sighting, a squadmate's for a shared one. */
|
|
34
|
+
sourceId: string;
|
|
35
|
+
/** Observe pass that recorded the fact, so a shared sighting is attributable to the tick it arrived. */
|
|
36
|
+
sourceTick: number;
|
|
31
37
|
}
|
|
32
|
-
/** Configuration for sight, hearing, memory decay, and optional occlusion. */
|
|
38
|
+
/** Configuration for sight, hearing, memory decay, squad sharing and optional occlusion. */
|
|
33
39
|
export interface PerceptionConfig {
|
|
34
40
|
sightRange: number;
|
|
35
41
|
sightConeDeg: number;
|
|
@@ -37,7 +43,43 @@ export interface PerceptionConfig {
|
|
|
37
43
|
memorySeconds: number;
|
|
38
44
|
/** Most stimuli retained at once; the oldest are dropped first. Default 256. */
|
|
39
45
|
maxStimuli?: number;
|
|
40
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Cheap "nothing blocks the segment from `from` to `to`" test, supplied by the game's collision
|
|
48
|
+
* layer. Only called for pairs already inside range and cone, so the costly test sees the pruned
|
|
49
|
+
* set. Omit for open ground.
|
|
50
|
+
*/
|
|
51
|
+
isSegmentClear?: (from: PerceptionPosition, to: PerceptionPosition) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The other members of an observer's squad, by id. A sighting one member makes is written into
|
|
54
|
+
* each mate's memory with the seeing member's id and the pass it happened on. Omit for lone
|
|
55
|
+
* observers. Keep the returned set small — the roster is the caller's, so the caller bounds it.
|
|
56
|
+
*/
|
|
57
|
+
squadMates?: (observerId: string) => Iterable<string>;
|
|
58
|
+
/** Most sighting shares one pass may write, so a large squad cannot blow the tick budget. Default 64. */
|
|
59
|
+
maxSharesPerTick?: number;
|
|
60
|
+
/** Register a devtools probe under this name so the overlay shows perception work and memory. */
|
|
61
|
+
devtoolsName?: string;
|
|
62
|
+
}
|
|
63
|
+
/** Counters a perf test or a devtools probe reads. Reset with {@link PerceptionService.resetStats}. */
|
|
64
|
+
export interface PerceptionStats {
|
|
65
|
+
/** Observe passes run. Keeps counting across `resetStats`, since it is also the recorded tick. */
|
|
66
|
+
passes: number;
|
|
67
|
+
/** `(observer, candidate)` pairs examined — one check per pair per pass. */
|
|
68
|
+
pairChecks: number;
|
|
69
|
+
/** Pairs rejected by distance before any cone or occlusion work. */
|
|
70
|
+
rangeCulled: number;
|
|
71
|
+
/** Pairs inside sight range, so they paid the cone test. */
|
|
72
|
+
coneTests: number;
|
|
73
|
+
/** Calls into the injected `isSegmentClear`. */
|
|
74
|
+
occlusionChecks: number;
|
|
75
|
+
/** Pairs that produced the observer's own sighting. */
|
|
76
|
+
sightings: number;
|
|
77
|
+
/** Sound and damage stimuli an observer resolved inside its hearing range. */
|
|
78
|
+
heard: number;
|
|
79
|
+
/** Sightings written into a squadmate's memory. */
|
|
80
|
+
shares: number;
|
|
81
|
+
/** Sightings the per-pass share cap kept from being broadcast. */
|
|
82
|
+
shareDropped: number;
|
|
41
83
|
}
|
|
42
84
|
/** Serializable state for a perception service. */
|
|
43
85
|
export interface PerceptionSnapshot {
|
|
@@ -52,19 +94,38 @@ export interface PerceptionSnapshot {
|
|
|
52
94
|
/** Per observer, the last stimulus sequence number it has already judged. Absent in older saves. */
|
|
53
95
|
heard?: Record<string, number>;
|
|
54
96
|
nextSeq?: number;
|
|
97
|
+
/** Observe passes recorded, which is also the tick a memory carries. Absent in older saves. */
|
|
98
|
+
passes?: number;
|
|
55
99
|
}
|
|
56
100
|
/** Stateful perception API with retuning and save/restore support. */
|
|
57
101
|
export interface PerceptionService {
|
|
58
102
|
pushStimulus(stimulus: PerceptionStimulus): void;
|
|
59
103
|
observe(observer: PerceptionObserver, candidates: readonly PerceptionCandidate[], nowMs: number): void;
|
|
60
104
|
memory(observerId: string): PerceptionMemory[];
|
|
105
|
+
/** One remembered target with its confidence now, or `null` when never seen or already faded. */
|
|
106
|
+
memoryOf(observerId: string, targetId: string): PerceptionMemory | null;
|
|
61
107
|
/** Drop one remembered target, or everything an observer remembers (despawn, reset, disguise). */
|
|
62
108
|
forget(observerId: string, targetId?: string): void;
|
|
63
|
-
|
|
109
|
+
stats(): PerceptionStats;
|
|
110
|
+
/** Zero the work counters, keeping the pass count that memories are stamped with. */
|
|
111
|
+
resetStats(): void;
|
|
112
|
+
retune(patch: Partial<Omit<PerceptionConfig, "devtoolsName">>): void;
|
|
64
113
|
snapshot(): PerceptionSnapshot;
|
|
65
114
|
restore(snapshot: PerceptionSnapshot): void;
|
|
115
|
+
/** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
|
|
116
|
+
dispose(): void;
|
|
66
117
|
}
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
118
|
+
/**
|
|
119
|
+
* Creates a deterministic sight, sound, and damage perception service with expiring target memory.
|
|
120
|
+
* Signals are judged in the order the caller supplies them, never from the wall clock.
|
|
121
|
+
*
|
|
122
|
+
* A sighting needs range, cone and a clear line; a sound carries `hearingRange × loudness` and
|
|
123
|
+
* fades with the distance it crossed, so a distant footstep is inaudible while a near gunshot is
|
|
124
|
+
* not. Memory is the only record of "seen recently": each fact fades linearly over `memorySeconds`
|
|
125
|
+
* and is then gone. With `squadMates` injected, a sighting one member makes lands in every mate's
|
|
126
|
+
* memory stamped with the seeing member and the pass, which is what lets a squad react to one pair
|
|
127
|
+
* of eyes without each member re-seeing anything.
|
|
128
|
+
*
|
|
129
|
+
* @capability perception-service retain sightings, sounds and damage with cone, range, occlusion, decaying memory and shared squad knowledge
|
|
69
130
|
*/
|
|
70
131
|
export declare function createPerception(initial: PerceptionConfig): PerceptionService;
|
|
@@ -1,24 +1,10 @@
|
|
|
1
|
+
import { devtools } from "../devtools/devtools.js";
|
|
1
2
|
function distance(a, b) {
|
|
2
3
|
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
3
4
|
}
|
|
4
5
|
function confidenceAt(memory, nowMs, memoryMs) {
|
|
5
6
|
return memory.confidence * Math.max(0, 1 - Math.max(0, nowMs - memory.lastSeenAt) / memoryMs);
|
|
6
7
|
}
|
|
7
|
-
function inSight(observer, target, config) {
|
|
8
|
-
const dx = target[0] - observer.position[0];
|
|
9
|
-
const dz = target[2] - observer.position[2];
|
|
10
|
-
const range = Math.hypot(dx, dz);
|
|
11
|
-
if (range > config.sightRange)
|
|
12
|
-
return false;
|
|
13
|
-
if (range < 1e-9)
|
|
14
|
-
return true;
|
|
15
|
-
let delta = Math.atan2(dx, dz) - observer.yaw;
|
|
16
|
-
while (delta > Math.PI)
|
|
17
|
-
delta -= Math.PI * 2;
|
|
18
|
-
while (delta < -Math.PI)
|
|
19
|
-
delta += Math.PI * 2;
|
|
20
|
-
return Math.abs(delta) <= (config.sightConeDeg * Math.PI) / 360;
|
|
21
|
-
}
|
|
22
8
|
function validate(config) {
|
|
23
9
|
if (!Number.isFinite(config.sightRange) || config.sightRange < 0)
|
|
24
10
|
throw new Error("sightRange must be non-negative.");
|
|
@@ -30,20 +16,48 @@ function validate(config) {
|
|
|
30
16
|
throw new Error("memorySeconds must be positive.");
|
|
31
17
|
if (config.maxStimuli !== undefined && (!Number.isInteger(config.maxStimuli) || config.maxStimuli < 1))
|
|
32
18
|
throw new Error("maxStimuli must be a positive integer.");
|
|
19
|
+
if (config.maxSharesPerTick !== undefined && (!Number.isInteger(config.maxSharesPerTick) || config.maxSharesPerTick < 0)) {
|
|
20
|
+
throw new Error("maxSharesPerTick must be a non-negative integer.");
|
|
21
|
+
}
|
|
33
22
|
}
|
|
34
23
|
const DEFAULT_MAX_STIMULI = 256;
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
const DEFAULT_MAX_SHARES = 64;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a deterministic sight, sound, and damage perception service with expiring target memory.
|
|
27
|
+
* Signals are judged in the order the caller supplies them, never from the wall clock.
|
|
28
|
+
*
|
|
29
|
+
* A sighting needs range, cone and a clear line; a sound carries `hearingRange × loudness` and
|
|
30
|
+
* fades with the distance it crossed, so a distant footstep is inaudible while a near gunshot is
|
|
31
|
+
* not. Memory is the only record of "seen recently": each fact fades linearly over `memorySeconds`
|
|
32
|
+
* and is then gone. With `squadMates` injected, a sighting one member makes lands in every mate's
|
|
33
|
+
* memory stamped with the seeing member and the pass, which is what lets a squad react to one pair
|
|
34
|
+
* of eyes without each member re-seeing anything.
|
|
35
|
+
*
|
|
36
|
+
* @capability perception-service retain sightings, sounds and damage with cone, range, occlusion, decaying memory and shared squad knowledge
|
|
37
37
|
*/
|
|
38
38
|
export function createPerception(initial) {
|
|
39
39
|
validate(initial);
|
|
40
40
|
let config = { ...initial };
|
|
41
|
+
let maxShares = config.maxSharesPerTick ?? DEFAULT_MAX_SHARES;
|
|
41
42
|
let nowMs = 0;
|
|
43
|
+
let passes = 0;
|
|
42
44
|
const stimuli = [];
|
|
43
45
|
const heard = new Map();
|
|
44
46
|
let nextSeq = 1;
|
|
45
47
|
const memories = new Map();
|
|
46
|
-
|
|
48
|
+
const stats = {
|
|
49
|
+
passes: 0,
|
|
50
|
+
pairChecks: 0,
|
|
51
|
+
rangeCulled: 0,
|
|
52
|
+
coneTests: 0,
|
|
53
|
+
occlusionChecks: 0,
|
|
54
|
+
sightings: 0,
|
|
55
|
+
heard: 0,
|
|
56
|
+
shares: 0,
|
|
57
|
+
shareDropped: 0,
|
|
58
|
+
};
|
|
59
|
+
const fresh = [];
|
|
60
|
+
function remember(observerId, targetId, position, at, confidence, sourceId, sourceTick) {
|
|
47
61
|
if (confidence <= 0)
|
|
48
62
|
return;
|
|
49
63
|
let observerMemories = memories.get(observerId);
|
|
@@ -53,26 +67,61 @@ export function createPerception(initial) {
|
|
|
53
67
|
}
|
|
54
68
|
const previous = observerMemories.get(targetId);
|
|
55
69
|
if (previous === undefined || at >= previous.lastSeenAt) {
|
|
56
|
-
observerMemories.set(targetId, {
|
|
70
|
+
observerMemories.set(targetId, {
|
|
71
|
+
targetId,
|
|
72
|
+
lastSeenAt: at,
|
|
73
|
+
lastKnownPos: [...position],
|
|
74
|
+
confidence,
|
|
75
|
+
sourceId,
|
|
76
|
+
sourceTick,
|
|
77
|
+
});
|
|
57
78
|
}
|
|
58
79
|
}
|
|
59
|
-
function
|
|
80
|
+
function pruneObserver(observerId) {
|
|
81
|
+
const observerMemories = memories.get(observerId);
|
|
82
|
+
if (observerMemories === undefined)
|
|
83
|
+
return;
|
|
60
84
|
const memoryMs = config.memorySeconds * 1000;
|
|
61
|
-
for (const [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
observerMemories.delete(targetId);
|
|
65
|
-
}
|
|
66
|
-
if (observerMemories.size === 0)
|
|
67
|
-
memories.delete(observerId);
|
|
85
|
+
for (const [targetId, memory] of observerMemories) {
|
|
86
|
+
if (confidenceAt(memory, nowMs, memoryMs) <= 0)
|
|
87
|
+
observerMemories.delete(targetId);
|
|
68
88
|
}
|
|
69
|
-
|
|
89
|
+
if (observerMemories.size === 0)
|
|
90
|
+
memories.delete(observerId);
|
|
91
|
+
}
|
|
92
|
+
function pruneStimuli() {
|
|
93
|
+
const cutoff = nowMs - config.memorySeconds * 1000;
|
|
70
94
|
let drop = Math.max(0, stimuli.length - (config.maxStimuli ?? DEFAULT_MAX_STIMULI));
|
|
71
95
|
while (drop < stimuli.length && stimuli[drop].at < cutoff)
|
|
72
96
|
drop += 1;
|
|
73
97
|
if (drop > 0)
|
|
74
98
|
stimuli.splice(0, drop);
|
|
75
99
|
}
|
|
100
|
+
/** Every observer's memory, not just the one that just observed — saving, retuning and restoring only. */
|
|
101
|
+
function pruneAll() {
|
|
102
|
+
for (const observerId of memories.keys())
|
|
103
|
+
pruneObserver(observerId);
|
|
104
|
+
pruneStimuli();
|
|
105
|
+
}
|
|
106
|
+
function share(mateId, sighting, seerId, tick) {
|
|
107
|
+
const existing = memories.get(mateId)?.get(sighting.id);
|
|
108
|
+
// The mate's own sighting at or after this instant is better evidence than a hand-off.
|
|
109
|
+
if (existing !== undefined && existing.lastSeenAt >= nowMs)
|
|
110
|
+
return;
|
|
111
|
+
remember(mateId, sighting.id, sighting.position, nowMs, 1, seerId, tick);
|
|
112
|
+
}
|
|
113
|
+
function rememberedCount() {
|
|
114
|
+
let total = 0;
|
|
115
|
+
for (const observerMemories of memories.values())
|
|
116
|
+
total += observerMemories.size;
|
|
117
|
+
return total;
|
|
118
|
+
}
|
|
119
|
+
function debugLine() {
|
|
120
|
+
const work = `${stats.pairChecks} pairs ${stats.rangeCulled} culled ${stats.occlusionChecks} los ${stats.sightings} sights ${stats.heard} heard`;
|
|
121
|
+
const squad = stats.shareDropped > 0 ? `${stats.shares} shared ${stats.shareDropped} dropped` : `${stats.shares} shared`;
|
|
122
|
+
return `${memories.size} observers ${rememberedCount()} remembered · ${work} · ${squad}`;
|
|
123
|
+
}
|
|
124
|
+
const unregister = config.devtoolsName === undefined ? null : devtools.probes.register(config.devtoolsName, () => debugLine());
|
|
76
125
|
return {
|
|
77
126
|
pushStimulus(stimulus) {
|
|
78
127
|
if (!Number.isFinite(stimulus.at))
|
|
@@ -85,21 +134,65 @@ export function createPerception(initial) {
|
|
|
85
134
|
index -= 1;
|
|
86
135
|
stimuli.splice(index, 0, { ...stimulus, position: [...stimulus.position], seq: nextSeq++ });
|
|
87
136
|
nowMs = Math.max(nowMs, stimulus.at);
|
|
88
|
-
|
|
137
|
+
pruneStimuli();
|
|
89
138
|
},
|
|
90
139
|
observe(observer, candidates, observedAt) {
|
|
91
140
|
if (!Number.isFinite(observedAt))
|
|
92
141
|
throw new Error("nowMs must be finite.");
|
|
93
142
|
nowMs = observedAt;
|
|
143
|
+
passes += 1;
|
|
144
|
+
stats.passes = passes;
|
|
145
|
+
const tick = passes;
|
|
146
|
+
const halfAngle = (config.sightConeDeg * Math.PI) / 360;
|
|
147
|
+
const isClear = config.isSegmentClear;
|
|
148
|
+
fresh.length = 0;
|
|
94
149
|
for (const candidate of candidates) {
|
|
95
|
-
|
|
150
|
+
stats.pairChecks += 1;
|
|
151
|
+
const dx = candidate.position[0] - observer.position[0];
|
|
152
|
+
const dz = candidate.position[2] - observer.position[2];
|
|
153
|
+
const range = Math.sqrt(dx * dx + dz * dz);
|
|
154
|
+
if (range > config.sightRange) {
|
|
155
|
+
stats.rangeCulled += 1;
|
|
96
156
|
continue;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
157
|
+
}
|
|
158
|
+
stats.coneTests += 1;
|
|
159
|
+
if (range >= 1e-9) {
|
|
160
|
+
let delta = Math.atan2(dx, dz) - observer.yaw;
|
|
161
|
+
while (delta > Math.PI)
|
|
162
|
+
delta -= Math.PI * 2;
|
|
163
|
+
while (delta < -Math.PI)
|
|
164
|
+
delta += Math.PI * 2;
|
|
165
|
+
if (Math.abs(delta) > halfAngle)
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (isClear !== undefined) {
|
|
169
|
+
stats.occlusionChecks += 1;
|
|
170
|
+
if (!isClear(observer.position, candidate.position))
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
stats.sightings += 1;
|
|
174
|
+
remember(observer.id, candidate.id, candidate.position, nowMs, 1, observer.id, tick);
|
|
175
|
+
fresh.push(candidate);
|
|
100
176
|
}
|
|
101
|
-
|
|
102
|
-
|
|
177
|
+
const mates = config.squadMates;
|
|
178
|
+
if (mates !== undefined && fresh.length > 0) {
|
|
179
|
+
let budget = maxShares;
|
|
180
|
+
for (const sighting of fresh) {
|
|
181
|
+
for (const mateId of mates(observer.id)) {
|
|
182
|
+
if (mateId === observer.id)
|
|
183
|
+
continue;
|
|
184
|
+
if (budget <= 0) {
|
|
185
|
+
stats.shareDropped += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
budget -= 1;
|
|
189
|
+
stats.shares += 1;
|
|
190
|
+
share(mateId, sighting, observer.id, tick);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Each observer judges a stimulus once, from where it stood when it first observed after the
|
|
195
|
+
// stimulus; re-judging every tick would make an old noise louder as the observer walks toward it.
|
|
103
196
|
const judgedThrough = heard.get(observer.id) ?? 0;
|
|
104
197
|
let judged = judgedThrough;
|
|
105
198
|
let firstFuture = Number.POSITIVE_INFINITY;
|
|
@@ -118,14 +211,16 @@ export function createPerception(initial) {
|
|
|
118
211
|
const audible = config.hearingRange * (stimulus.loudness ?? 1);
|
|
119
212
|
if (range >= audible)
|
|
120
213
|
continue;
|
|
121
|
-
|
|
214
|
+
stats.heard += 1;
|
|
215
|
+
remember(observer.id, stimulus.sourceId, stimulus.position, stimulus.at, 1 - range / audible, observer.id, tick);
|
|
122
216
|
}
|
|
123
217
|
else if (stimulus.kind === "damage") {
|
|
124
|
-
|
|
218
|
+
stats.heard += 1;
|
|
219
|
+
remember(observer.id, stimulus.sourceId, stimulus.position, stimulus.at, 1, observer.id, tick);
|
|
125
220
|
}
|
|
126
221
|
}
|
|
127
222
|
heard.set(observer.id, Math.min(judged, firstFuture - 1));
|
|
128
|
-
|
|
223
|
+
pruneObserver(observer.id);
|
|
129
224
|
},
|
|
130
225
|
memory(observerId) {
|
|
131
226
|
const observerMemories = memories.get(observerId);
|
|
@@ -140,6 +235,15 @@ export function createPerception(initial) {
|
|
|
140
235
|
}
|
|
141
236
|
return result;
|
|
142
237
|
},
|
|
238
|
+
memoryOf(observerId, targetId) {
|
|
239
|
+
const memory = memories.get(observerId)?.get(targetId);
|
|
240
|
+
if (memory === undefined)
|
|
241
|
+
return null;
|
|
242
|
+
const confidence = confidenceAt(memory, nowMs, config.memorySeconds * 1000);
|
|
243
|
+
if (confidence <= 0)
|
|
244
|
+
return null;
|
|
245
|
+
return { ...memory, lastKnownPos: [...memory.lastKnownPos], confidence };
|
|
246
|
+
},
|
|
143
247
|
forget(observerId, targetId) {
|
|
144
248
|
if (targetId === undefined) {
|
|
145
249
|
memories.delete(observerId);
|
|
@@ -150,18 +254,32 @@ export function createPerception(initial) {
|
|
|
150
254
|
if (observerMemories?.size === 0)
|
|
151
255
|
memories.delete(observerId);
|
|
152
256
|
},
|
|
257
|
+
stats: () => ({ ...stats }),
|
|
258
|
+
resetStats() {
|
|
259
|
+
stats.pairChecks = 0;
|
|
260
|
+
stats.rangeCulled = 0;
|
|
261
|
+
stats.coneTests = 0;
|
|
262
|
+
stats.occlusionChecks = 0;
|
|
263
|
+
stats.sightings = 0;
|
|
264
|
+
stats.heard = 0;
|
|
265
|
+
stats.shares = 0;
|
|
266
|
+
stats.shareDropped = 0;
|
|
267
|
+
},
|
|
153
268
|
retune(patch) {
|
|
154
269
|
config = { ...config, ...patch };
|
|
155
270
|
validate(config);
|
|
156
|
-
|
|
271
|
+
maxShares = config.maxSharesPerTick ?? DEFAULT_MAX_SHARES;
|
|
272
|
+
pruneAll();
|
|
157
273
|
},
|
|
158
274
|
snapshot() {
|
|
275
|
+
pruneAll();
|
|
159
276
|
const result = {
|
|
160
277
|
memories: [],
|
|
161
278
|
stimuli: stimuli.map((stimulus) => ({ ...stimulus, position: [...stimulus.position] })),
|
|
162
279
|
nowMs,
|
|
163
280
|
heard: Object.fromEntries(heard),
|
|
164
281
|
nextSeq,
|
|
282
|
+
passes,
|
|
165
283
|
};
|
|
166
284
|
for (const [observerId, observerMemories] of memories)
|
|
167
285
|
for (const memory of observerMemories.values())
|
|
@@ -172,6 +290,8 @@ export function createPerception(initial) {
|
|
|
172
290
|
memories.clear();
|
|
173
291
|
stimuli.length = 0;
|
|
174
292
|
nowMs = snapshot.nowMs;
|
|
293
|
+
passes = snapshot.passes ?? 0;
|
|
294
|
+
stats.passes = passes;
|
|
175
295
|
heard.clear();
|
|
176
296
|
for (const [observerId, seq] of Object.entries(snapshot.heard ?? {}))
|
|
177
297
|
heard.set(observerId, seq);
|
|
@@ -181,9 +301,14 @@ export function createPerception(initial) {
|
|
|
181
301
|
stimuli.push({ ...stimulus, position: [...stimulus.position], seq });
|
|
182
302
|
}
|
|
183
303
|
nextSeq = Math.max(snapshot.nextSeq ?? 0, seq + 1);
|
|
184
|
-
for (const entry of snapshot.memories)
|
|
185
|
-
|
|
186
|
-
|
|
304
|
+
for (const entry of snapshot.memories) {
|
|
305
|
+
const saved = entry.memory;
|
|
306
|
+
remember(entry.observerId, saved.targetId, saved.lastKnownPos, saved.lastSeenAt, saved.confidence, saved.sourceId ?? entry.observerId, saved.sourceTick ?? 0);
|
|
307
|
+
}
|
|
308
|
+
pruneAll();
|
|
309
|
+
},
|
|
310
|
+
dispose() {
|
|
311
|
+
unregister?.();
|
|
187
312
|
},
|
|
188
313
|
};
|
|
189
314
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Vec3 } from "../world/geometry.js";
|
|
2
|
+
import { type NavCrowd, type NavCrowdOptions } from "../nav/navCrowd.js";
|
|
3
|
+
import type { NavRuntime } from "../nav/navRuntime.js";
|
|
4
|
+
import { type UtilityBrainSnapshot, type UtilityCandidate, type UtilityInterrupt, type UtilityOption } from "../ai/utilityBrain.js";
|
|
5
|
+
import { type NeedDefinition, type NeedSnapshot } from "./needs.js";
|
|
6
|
+
import type { NearAgents } from "./offScreen.js";
|
|
7
|
+
/** Serializable state of one near agent: where it walks, what it needs, what it last chose. */
|
|
8
|
+
export interface NearAgentSnapshot {
|
|
9
|
+
id: string;
|
|
10
|
+
position: Vec3;
|
|
11
|
+
goal: Vec3;
|
|
12
|
+
place: string | null;
|
|
13
|
+
activity: string | null;
|
|
14
|
+
travelling: boolean;
|
|
15
|
+
needs: NeedSnapshot;
|
|
16
|
+
brain: UtilityBrainSnapshot;
|
|
17
|
+
}
|
|
18
|
+
/** Everything a near agent is doing, for a HUD, a probe or a test. Allocates. */
|
|
19
|
+
export interface NearAgentView {
|
|
20
|
+
id: string;
|
|
21
|
+
position: Vec3;
|
|
22
|
+
goal: Vec3;
|
|
23
|
+
place: string | null;
|
|
24
|
+
activity: string | null;
|
|
25
|
+
travelling: boolean;
|
|
26
|
+
/** The utility pick for this tick; `null` before the first decision or with no candidates. */
|
|
27
|
+
pick: string | null;
|
|
28
|
+
needs: NeedSnapshot;
|
|
29
|
+
}
|
|
30
|
+
/** What the near side needs from the game: a nav mesh, the shared need definitions, and what agents can do. */
|
|
31
|
+
export interface NearAgentsOptions {
|
|
32
|
+
nav: NavRuntime;
|
|
33
|
+
/** Need definitions shared with the off-screen world, so a far decay and a near tick agree. */
|
|
34
|
+
needs: readonly NeedDefinition[];
|
|
35
|
+
/** Utility options each brain scores; omit for agents that only carry needs and walk. */
|
|
36
|
+
options?: readonly UtilityOption[];
|
|
37
|
+
interrupts?: readonly UtilityInterrupt[];
|
|
38
|
+
/** Candidates the agent may pick this tick (where it stands, or anything the game offers). */
|
|
39
|
+
candidates?: (agent: NearAgentView) => readonly UtilityCandidate[];
|
|
40
|
+
crowd?: NavCrowdOptions;
|
|
41
|
+
/** Register a devtools probe under this name so the overlay shows near-agent decisions. */
|
|
42
|
+
devtoolsName?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The near half of the off-screen world: one nav crowd for movement, one need set and utility brain
|
|
46
|
+
* per agent for what it wants. Resident identity survives the handoff because the off-screen world
|
|
47
|
+
* spawns and despawns through this seam and keeps the saved state.
|
|
48
|
+
*/
|
|
49
|
+
export interface NearCrowdAgents extends NearAgents {
|
|
50
|
+
/** Utility decisions taken since creation — near agents run brains, far ones do not exist here. */
|
|
51
|
+
decisions(): number;
|
|
52
|
+
view(id: string): NearAgentView | null;
|
|
53
|
+
crowd(): NavCrowd;
|
|
54
|
+
snapshot(): {
|
|
55
|
+
agents: NearAgentSnapshot[];
|
|
56
|
+
};
|
|
57
|
+
restore(next: {
|
|
58
|
+
agents: readonly NearAgentSnapshot[];
|
|
59
|
+
}): void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create the near-side simulation: agents on a nav mesh with decaying needs and a utility brain each.
|
|
63
|
+
* The off-screen world decides which agents exist here, so a resident beyond its radius costs this
|
|
64
|
+
* seam nothing at all — no path, no need tick, no decision.
|
|
65
|
+
*
|
|
66
|
+
* @capability near-agents nav crowd plus per-agent needs and utility brains for on-screen residents
|
|
67
|
+
*/
|
|
68
|
+
export declare function createNearAgents(options: NearAgentsOptions): NearCrowdAgents;
|