@jgengine/core 0.24.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 +92 -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/notices.d.ts +4 -0
- package/dist/game/notices.js +4 -3
- package/dist/game/playableGame.d.ts +9 -1
- 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 +78 -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,221 @@
|
|
|
1
|
+
import { hexToRgb, mixHex } from "./color.js";
|
|
2
|
+
/**
|
|
3
|
+
* Baked irradiance volume: a grid of second-order (9-coefficient, RGB) spherical-harmonic probes
|
|
4
|
+
* over a box region, sampled at runtime for ambient lighting. It is the datum an interior has that
|
|
5
|
+
* a hemisphere light cannot: a room's fill is its own, not the sky's, and it varies across the space.
|
|
6
|
+
*
|
|
7
|
+
* Serializable and pure; the bake is deterministic heuristics (sky visibility, authored lamps and
|
|
8
|
+
* bounce colours), not a path tracer.
|
|
9
|
+
*/
|
|
10
|
+
/** SH coefficients per channel for the second band — 3 bands, real basis. */
|
|
11
|
+
export const SH_COEFFICIENTS = 9;
|
|
12
|
+
/** Floats stored per probe: {@link SH_COEFFICIENTS} × RGB. */
|
|
13
|
+
export const PROBE_STRIDE = SH_COEFFICIENTS * 3;
|
|
14
|
+
const SH_BAND_0 = 0.282095;
|
|
15
|
+
const SH_BAND_1 = 0.488603;
|
|
16
|
+
const SH_BAND_2_0 = 1.092548;
|
|
17
|
+
const SH_BAND_2_1 = 0.315392;
|
|
18
|
+
const SH_BAND_2_2 = 0.546274;
|
|
19
|
+
/**
|
|
20
|
+
* Real-basis SH values for a unit direction, into `out` (length 9):
|
|
21
|
+
* `[Y00, Y1-1, Y10, Y11, Y2-2, Y2-1, Y20, Y21, Y22]`.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export function shDirection(x, y, z, out) {
|
|
25
|
+
out[0] = SH_BAND_0;
|
|
26
|
+
out[1] = SH_BAND_1 * y;
|
|
27
|
+
out[2] = SH_BAND_1 * z;
|
|
28
|
+
out[3] = SH_BAND_1 * x;
|
|
29
|
+
out[4] = SH_BAND_2_0 * x * y;
|
|
30
|
+
out[5] = SH_BAND_2_0 * y * z;
|
|
31
|
+
out[6] = SH_BAND_2_1 * (3 * z * z - 1);
|
|
32
|
+
out[7] = SH_BAND_2_0 * x * z;
|
|
33
|
+
out[8] = SH_BAND_2_2 * (x * x - y * y);
|
|
34
|
+
}
|
|
35
|
+
/** Fibonacci sphere direction `index` of `count`, in the renderer's Y-up frame. */
|
|
36
|
+
function sphereDirection(index, count) {
|
|
37
|
+
const y = 1 - (2 * (index + 0.5)) / count;
|
|
38
|
+
const radius = Math.sqrt(Math.max(0, 1 - y * y));
|
|
39
|
+
const theta = index * 2.399963229728653;
|
|
40
|
+
return [Math.cos(theta) * radius, y, Math.sin(theta) * radius];
|
|
41
|
+
}
|
|
42
|
+
function scaledRgb(color, scale) {
|
|
43
|
+
const [r, g, b] = hexToRgb(color);
|
|
44
|
+
return [(r / 255) * scale, (g / 255) * scale, (b / 255) * scale];
|
|
45
|
+
}
|
|
46
|
+
/** World-space position of probe `(ix, iy, iz)`; the outer probes sit on the box faces.
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export function probePosition(volume, ix, iy, iz) {
|
|
50
|
+
const [nx, ny, nz] = volume.resolution;
|
|
51
|
+
const t = (index, count) => (count <= 1 ? 0.5 : index / (count - 1));
|
|
52
|
+
return [
|
|
53
|
+
volume.center[0] + (t(ix, nx) - 0.5) * volume.size[0],
|
|
54
|
+
volume.center[1] + (t(iy, ny) - 0.5) * volume.size[1],
|
|
55
|
+
volume.center[2] + (t(iz, nz) - 0.5) * volume.size[2],
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Bakes an irradiance volume from authored inputs: sky radiance where the region is open, an
|
|
60
|
+
* authored ceiling/floor fill where it is enclosed, plus inverse-square lamp radiance. Each probe
|
|
61
|
+
* projects its directional radiance into L2 SH, so ambient varies across the grid and keeps a lit
|
|
62
|
+
* side instead of flattening to one number.
|
|
63
|
+
* @capability light-probes bake an irradiance volume from authored sky, fill and lamp inputs
|
|
64
|
+
*/
|
|
65
|
+
export function bakeIrradianceVolume(input) {
|
|
66
|
+
const { settings } = input;
|
|
67
|
+
const count = Math.max(8, Math.min(256, Math.floor(input.samples ?? 64)));
|
|
68
|
+
const [nx, ny, nz] = input.resolution.map((value) => Math.max(1, Math.floor(value)));
|
|
69
|
+
const visibility = Math.max(0, Math.min(1, settings.skyVisibility));
|
|
70
|
+
const fill = Math.max(0, settings.fillIntensity ?? 1);
|
|
71
|
+
const sky = settings.skyColor ?? "#9ec4e8";
|
|
72
|
+
const ground = settings.groundColor ?? "#4c4a42";
|
|
73
|
+
const ceiling = settings.ceilingColor ?? "#8d8a80";
|
|
74
|
+
const floor = settings.floorColor ?? "#4a4438";
|
|
75
|
+
const lamps = settings.lights ?? [];
|
|
76
|
+
const samples = new Array(PROBE_STRIDE * nx * ny * nz).fill(0);
|
|
77
|
+
const basis = new Float64Array(SH_COEFFICIENTS);
|
|
78
|
+
const accum = new Float64Array(PROBE_STRIDE);
|
|
79
|
+
for (let iz = 0; iz < nz; iz += 1) {
|
|
80
|
+
for (let iy = 0; iy < ny; iy += 1) {
|
|
81
|
+
for (let ix = 0; ix < nx; ix += 1) {
|
|
82
|
+
const position = probePosition({ center: input.center, size: input.size, resolution: [nx, ny, nz] }, ix, iy, iz);
|
|
83
|
+
accum.fill(0);
|
|
84
|
+
for (let s = 0; s < count; s += 1) {
|
|
85
|
+
const [dx, dy, dz] = sphereDirection(s, count);
|
|
86
|
+
shDirection(dx, dy, dz, basis);
|
|
87
|
+
const radiance = radianceIn(position, dx, dy, dz, { visibility, fill, sky, ground, ceiling, floor, lamps });
|
|
88
|
+
for (let band = 0; band < SH_COEFFICIENTS; band += 1) {
|
|
89
|
+
const weighted = basis[band] * (4 * Math.PI) / count;
|
|
90
|
+
accum[band * 3] += radiance[0] * weighted;
|
|
91
|
+
accum[band * 3 + 1] += radiance[1] * weighted;
|
|
92
|
+
accum[band * 3 + 2] += radiance[2] * weighted;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const offset = ((iz * ny + iy) * nx + ix) * PROBE_STRIDE;
|
|
96
|
+
for (let index = 0; index < PROBE_STRIDE; index += 1)
|
|
97
|
+
samples[offset + index] = accum[index];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { id: input.id, center: [...input.center], size: [...input.size], resolution: [nx, ny, nz], samples };
|
|
102
|
+
}
|
|
103
|
+
function radianceIn(position, dx, dy, dz, inputs) {
|
|
104
|
+
const open = inputs.visibility;
|
|
105
|
+
const enclosed = 1 - inputs.visibility;
|
|
106
|
+
let color;
|
|
107
|
+
let scale;
|
|
108
|
+
if (dy >= 0) {
|
|
109
|
+
// Above the horizon an open probe sees sky; an enclosed one sees its own ceiling.
|
|
110
|
+
color = mixHex(inputs.ceiling, inputs.sky, open);
|
|
111
|
+
scale = 0.55 + 0.45 * dy;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Below the horizon: ground bounce outside, floor bounce inside.
|
|
115
|
+
color = mixHex(inputs.floor, inputs.ground, open);
|
|
116
|
+
scale = 0.55 + 0.45 * -dy;
|
|
117
|
+
}
|
|
118
|
+
scale *= enclosed * inputs.fill + open;
|
|
119
|
+
const radiance = scaledRgb(color, scale);
|
|
120
|
+
for (const lamp of inputs.lamps) {
|
|
121
|
+
const lx = lamp.position[0] - position[0];
|
|
122
|
+
const ly = lamp.position[1] - position[1];
|
|
123
|
+
const lz = lamp.position[2] - position[2];
|
|
124
|
+
const distanceSquared = lx * lx + ly * ly + lz * lz;
|
|
125
|
+
if (distanceSquared < 1e-4)
|
|
126
|
+
continue;
|
|
127
|
+
const reach = lamp.distance ?? 0;
|
|
128
|
+
if (reach > 0 && distanceSquared > reach * reach)
|
|
129
|
+
continue;
|
|
130
|
+
const distance = Math.sqrt(distanceSquared);
|
|
131
|
+
const facing = (dx * lx + dy * ly + dz * lz) / distance;
|
|
132
|
+
if (facing <= 0)
|
|
133
|
+
continue;
|
|
134
|
+
const falloff = (lamp.intensity ?? 1) / (distanceSquared + 1) * Math.pow(facing, 6);
|
|
135
|
+
const lampRadiant = scaledRgb(lamp.color ?? "#ffd6a0", falloff);
|
|
136
|
+
radiance[0] += lampRadiant[0];
|
|
137
|
+
radiance[1] += lampRadiant[1];
|
|
138
|
+
radiance[2] += lampRadiant[2];
|
|
139
|
+
}
|
|
140
|
+
return radiance;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The volume that governs a world position: the one whose box contains it, else the nearest by
|
|
144
|
+
* centre. A scene usually authors one volume per interior plus one for the exterior shell.
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
export function selectIrradianceVolume(volumes, position) {
|
|
148
|
+
let nearest;
|
|
149
|
+
let nearestDistance = Infinity;
|
|
150
|
+
for (const volume of volumes) {
|
|
151
|
+
if (volume.samples === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
const dx = Math.abs(position[0] - volume.center[0]) - volume.size[0] / 2;
|
|
154
|
+
const dy = Math.abs(position[1] - volume.center[1]) - volume.size[1] / 2;
|
|
155
|
+
const dz = Math.abs(position[2] - volume.center[2]) - volume.size[2] / 2;
|
|
156
|
+
const outside = Math.max(dx, dy, dz);
|
|
157
|
+
if (outside <= 0)
|
|
158
|
+
return volume;
|
|
159
|
+
const distance = Math.hypot(Math.max(dx, 0), Math.max(dy, 0), Math.max(dz, 0));
|
|
160
|
+
if (distance < nearestDistance) {
|
|
161
|
+
nearestDistance = distance;
|
|
162
|
+
nearest = volume;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return nearest;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Trilinearly samples the volume's SH at a world position, writing {@link PROBE_STRIDE} floats into
|
|
169
|
+
* `out`. Positions outside the grid clamp to its faces, so a probe volume never pops to black at its
|
|
170
|
+
* edge. Returns false when the volume has no baked samples.
|
|
171
|
+
* @internal
|
|
172
|
+
*/
|
|
173
|
+
export function sampleIrradianceVolume(volume, position, out) {
|
|
174
|
+
const samples = volume.samples;
|
|
175
|
+
if (samples === undefined || samples.length < PROBE_STRIDE)
|
|
176
|
+
return false;
|
|
177
|
+
const [nx, ny, nz] = volume.resolution;
|
|
178
|
+
const axis = (value, center, size, count) => {
|
|
179
|
+
if (count <= 1 || size === 0)
|
|
180
|
+
return { low: 0, high: 0, t: 0 };
|
|
181
|
+
const normalized = ((value - center) / size + 0.5) * (count - 1);
|
|
182
|
+
const clamped = Math.max(0, Math.min(count - 1, normalized));
|
|
183
|
+
const low = Math.floor(clamped);
|
|
184
|
+
return { low, high: Math.min(count - 1, low + 1), t: clamped - low };
|
|
185
|
+
};
|
|
186
|
+
const x = axis(position[0], volume.center[0], volume.size[0], nx);
|
|
187
|
+
const y = axis(position[1], volume.center[1], volume.size[1], ny);
|
|
188
|
+
const z = axis(position[2], volume.center[2], volume.size[2], nz);
|
|
189
|
+
out.fill(0);
|
|
190
|
+
const weights = [];
|
|
191
|
+
for (const [ix, wx] of [[x.low, 1 - x.t], [x.high, x.t]]) {
|
|
192
|
+
for (const [iy, wy] of [[y.low, 1 - y.t], [y.high, y.t]]) {
|
|
193
|
+
for (const [iz, wz] of [[z.low, 1 - z.t], [z.high, z.t]]) {
|
|
194
|
+
const weight = wx * wy * wz;
|
|
195
|
+
if (weight <= 0)
|
|
196
|
+
continue;
|
|
197
|
+
weights.push([ix, iy, iz]);
|
|
198
|
+
const offset = ((iz * ny + iy) * nx + ix) * PROBE_STRIDE;
|
|
199
|
+
for (let index = 0; index < PROBE_STRIDE; index += 1)
|
|
200
|
+
out[index] += (samples[offset + index] ?? 0) * weight;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return weights.length > 0;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Radiance along a direction from the SH, `Σ coeff·basis` clamped at zero. Used by tests and by
|
|
208
|
+
* tooling that previews a volume without a renderer.
|
|
209
|
+
* @internal
|
|
210
|
+
*/
|
|
211
|
+
export function radianceAlong(sh, normal) {
|
|
212
|
+
const basis = new Float64Array(SH_COEFFICIENTS);
|
|
213
|
+
shDirection(normal[0], normal[1], normal[2], basis);
|
|
214
|
+
const rgb = [0, 0, 0];
|
|
215
|
+
for (let band = 0; band < SH_COEFFICIENTS; band += 1) {
|
|
216
|
+
rgb[0] += (sh[band * 3] ?? 0) * basis[band];
|
|
217
|
+
rgb[1] += (sh[band * 3 + 1] ?? 0) * basis[band];
|
|
218
|
+
rgb[2] += (sh[band * 3 + 2] ?? 0) * basis[band];
|
|
219
|
+
}
|
|
220
|
+
return [Math.max(0, rgb[0]), Math.max(0, rgb[1]), Math.max(0, rgb[2])];
|
|
221
|
+
}
|
|
@@ -4,7 +4,7 @@ export const LOOK_PRESETS = ["neutral", "photoreal", "cinematic", "toon", "comic
|
|
|
4
4
|
export const CINEMATIC_SKY = { preset: "day" };
|
|
5
5
|
/** Tuned tone-map + bloom + gentle SSAO + vignette/grade stack — the shipped-game post look. */
|
|
6
6
|
export const CINEMATIC_POST_PROCESSING = {
|
|
7
|
-
toneMapping: "
|
|
7
|
+
toneMapping: "agx",
|
|
8
8
|
bloom: {},
|
|
9
9
|
ao: { intensity: 1.4 },
|
|
10
10
|
grade: {},
|
|
@@ -17,7 +17,7 @@ export const NEUTRAL_POST_PROCESSING = {
|
|
|
17
17
|
};
|
|
18
18
|
/** High-contrast physically based presentation for authored HDR environments. */
|
|
19
19
|
export const PHOTOREAL_POST_PROCESSING = {
|
|
20
|
-
toneMapping: "
|
|
20
|
+
toneMapping: "agx",
|
|
21
21
|
bloom: { strength: 0.28, radius: 0.58, threshold: 0.9 },
|
|
22
22
|
ao: { intensity: 1.25, radius: 1.6 },
|
|
23
23
|
grade: { saturation: 1.04, vignette: 0.08, grain: 0.004 },
|
|
@@ -31,7 +31,7 @@ export const TOON_POST_PROCESSING = {
|
|
|
31
31
|
};
|
|
32
32
|
/** Ink outlines, four cel bands and a punchy grade — a hand-inked comic look on any model. */
|
|
33
33
|
export const COMIC_POST_PROCESSING = {
|
|
34
|
-
toneMapping: "
|
|
34
|
+
toneMapping: "agx",
|
|
35
35
|
exposure: 1.05,
|
|
36
36
|
aa: "msaa",
|
|
37
37
|
bloom: { strength: 0.12, radius: 0.4, threshold: 1 },
|
|
@@ -41,7 +41,7 @@ export const COMIC_POST_PROCESSING = {
|
|
|
41
41
|
};
|
|
42
42
|
/** Chunky pixels and posterized colour with no smoothing — a low-resolution retro look. */
|
|
43
43
|
export const RETRO_POST_PROCESSING = {
|
|
44
|
-
toneMapping: "
|
|
44
|
+
toneMapping: "agx",
|
|
45
45
|
aa: false,
|
|
46
46
|
bloom: false,
|
|
47
47
|
ao: false,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Peak moonlight as a fraction of the authored sunlight, at the moon's highest point.
|
|
2
|
+
* Real moonlight is ~2.5e-6 of daylight; every game exaggerates it to stay readable, and this is
|
|
3
|
+
* the exaggeration, not a measurement. Games override it through `SkyEnvironmentConfig.moon`. */
|
|
4
|
+
export declare const MOONLIGHT_PEAK = 0.12;
|
|
5
|
+
/** Moonlight's colour: scattered sunlight, blue-shifted because the eye's scotopic response peaks there. */
|
|
6
|
+
export declare const MOONLIGHT_COLOR = "#a9c3e8";
|
|
7
|
+
/** Fog color once the sun is gone: the night sky's own haze, lifted enough to keep silhouettes readable. */
|
|
8
|
+
export declare const NIGHT_FOG_COLOR = "#0a0e16";
|
|
9
|
+
/** Elevation of a world-space direction in degrees; a zero vector reads as overhead. */
|
|
10
|
+
export declare function elevationOf(direction: readonly [number, number, number]): number;
|
|
11
|
+
/**
|
|
12
|
+
* Relative air mass along a line of sight at `elevationDeg` - Kasten & Young (1989). 1 at the
|
|
13
|
+
* zenith, ~38 at the horizon, and clamped below the horizon so a set sun does not run away.
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export declare function airMass(elevationDeg: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* Fraction of each RGB channel of sunlight that survives the atmosphere at `elevationDeg`:
|
|
19
|
+
* `exp(-τ · air mass)`. High sun keeps nearly all of it; a low sun loses the short wavelengths
|
|
20
|
+
* first, which is the red sunset.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare function sunTransmission(elevationDeg: number): [number, number, number];
|
|
24
|
+
/**
|
|
25
|
+
* The sun's colour at `elevationDeg`, normalised so its brightest channel is full: white overhead,
|
|
26
|
+
* orange at the horizon, moon-blue once it has set. Intensity stays the authored cycle's job.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sunlightColor(elevationDeg: number): string;
|
|
29
|
+
/** Perceived brightness (Rec. 709 luma) of a `#rrggbb` color, 0–1.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export declare function luminanceOf(color: string): number;
|
|
33
|
+
/**
|
|
34
|
+
* Moonlight at `sunElevationDeg`'s opposite point: full at the top of the moon's arc, zero while
|
|
35
|
+
* the moon is below the horizon, scaled by `peak` (a fraction of the authored sun intensity).
|
|
36
|
+
*/
|
|
37
|
+
export declare function moonIllumination(moonElevationDeg: number, peak?: number): number;
|
|
38
|
+
/** How visible the stars are: none in daylight, half at the first hint of a set sun, full once it is 6° under. */
|
|
39
|
+
export declare function starOpacity(sunElevationDeg: number): number;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { rgbToHex, hexToRgb } from "./color.js";
|
|
2
|
+
/**
|
|
3
|
+
* Analytic atmosphere for the day/night sky: Rayleigh air mass, sunlight reddening, moonlight and
|
|
4
|
+
* star visibility. The engine keeps a game's authored sky colors as its noon keyframe; these
|
|
5
|
+
* functions supply the parts a color table cannot — the sun's own tint as it drops toward the
|
|
6
|
+
* horizon, the moon that replaces it after sunset, and how much starlight shows.
|
|
7
|
+
*
|
|
8
|
+
* Pure and deterministic so the whole cycle is testable without a renderer.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Rayleigh scattering coefficient per RGB channel in m^-1 (680, 550, 440 nm, the standard
|
|
12
|
+
* β_R ∝ 1/λ⁴ curve) times an 8000 m scale height — the zenith optical depth `τ` of each channel.
|
|
13
|
+
*/
|
|
14
|
+
const RAYLEIGH_DEPTH = [0.0464, 0.108, 0.2648];
|
|
15
|
+
/** Peak moonlight as a fraction of the authored sunlight, at the moon's highest point.
|
|
16
|
+
* Real moonlight is ~2.5e-6 of daylight; every game exaggerates it to stay readable, and this is
|
|
17
|
+
* the exaggeration, not a measurement. Games override it through `SkyEnvironmentConfig.moon`. */
|
|
18
|
+
export const MOONLIGHT_PEAK = 0.12;
|
|
19
|
+
/** Moonlight's colour: scattered sunlight, blue-shifted because the eye's scotopic response peaks there. */
|
|
20
|
+
export const MOONLIGHT_COLOR = "#a9c3e8";
|
|
21
|
+
/** Fog color once the sun is gone: the night sky's own haze, lifted enough to keep silhouettes readable. */
|
|
22
|
+
export const NIGHT_FOG_COLOR = "#0a0e16";
|
|
23
|
+
const DEG = Math.PI / 180;
|
|
24
|
+
function clamp01(value) {
|
|
25
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
26
|
+
}
|
|
27
|
+
function smoothstep(edge0, edge1, value) {
|
|
28
|
+
const t = clamp01((value - edge0) / (edge1 - edge0));
|
|
29
|
+
return t * t * (3 - 2 * t);
|
|
30
|
+
}
|
|
31
|
+
/** Elevation of a world-space direction in degrees; a zero vector reads as overhead. */
|
|
32
|
+
export function elevationOf(direction) {
|
|
33
|
+
const length = Math.hypot(direction[0], direction[1], direction[2]);
|
|
34
|
+
if (length === 0)
|
|
35
|
+
return 90;
|
|
36
|
+
return Math.asin(Math.max(-1, Math.min(1, direction[1] / length))) / DEG;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Relative air mass along a line of sight at `elevationDeg` - Kasten & Young (1989). 1 at the
|
|
40
|
+
* zenith, ~38 at the horizon, and clamped below the horizon so a set sun does not run away.
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export function airMass(elevationDeg) {
|
|
44
|
+
const zenithDeg = 90 - Math.max(elevationDeg, 0);
|
|
45
|
+
if (zenithDeg >= 90)
|
|
46
|
+
return 38;
|
|
47
|
+
return 1 / (Math.cos(zenithDeg * DEG) + 0.50572 * Math.pow(96.07995 - zenithDeg, -1.6364));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fraction of each RGB channel of sunlight that survives the atmosphere at `elevationDeg`:
|
|
51
|
+
* `exp(-τ · air mass)`. High sun keeps nearly all of it; a low sun loses the short wavelengths
|
|
52
|
+
* first, which is the red sunset.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export function sunTransmission(elevationDeg) {
|
|
56
|
+
const mass = airMass(elevationDeg);
|
|
57
|
+
return [0, 1, 2].map((channel) => Math.exp(-RAYLEIGH_DEPTH[channel] * mass));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The sun's colour at `elevationDeg`, normalised so its brightest channel is full: white overhead,
|
|
61
|
+
* orange at the horizon, moon-blue once it has set. Intensity stays the authored cycle's job.
|
|
62
|
+
*/
|
|
63
|
+
export function sunlightColor(elevationDeg) {
|
|
64
|
+
if (elevationDeg <= -6)
|
|
65
|
+
return MOONLIGHT_COLOR;
|
|
66
|
+
const transmission = sunTransmission(Math.max(elevationDeg, 0));
|
|
67
|
+
const brightest = Math.max(transmission[0], transmission[1], transmission[2], 1e-6);
|
|
68
|
+
return rgbToHex((transmission[0] / brightest) * 255, (transmission[1] / brightest) * 255, (transmission[2] / brightest) * 255);
|
|
69
|
+
}
|
|
70
|
+
/** Perceived brightness (Rec. 709 luma) of a `#rrggbb` color, 0–1.
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export function luminanceOf(color) {
|
|
74
|
+
const [r, g, b] = hexToRgb(color);
|
|
75
|
+
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Moonlight at `sunElevationDeg`'s opposite point: full at the top of the moon's arc, zero while
|
|
79
|
+
* the moon is below the horizon, scaled by `peak` (a fraction of the authored sun intensity).
|
|
80
|
+
*/
|
|
81
|
+
export function moonIllumination(moonElevationDeg, peak = MOONLIGHT_PEAK) {
|
|
82
|
+
if (moonElevationDeg <= 0)
|
|
83
|
+
return 0;
|
|
84
|
+
return Math.sin(Math.min(moonElevationDeg, 90) * DEG) * Math.max(0, peak);
|
|
85
|
+
}
|
|
86
|
+
/** How visible the stars are: none in daylight, half at the first hint of a set sun, full once it is 6° under. */
|
|
87
|
+
export function starOpacity(sunElevationDeg) {
|
|
88
|
+
return 1 - smoothstep(-6, 0, sunElevationDeg);
|
|
89
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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;
|