@aura3d/scripting 1.4.0 → 1.4.2

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.
@@ -1,440 +0,0 @@
1
- export type NetworkReplicationMode = "all-clients" | "owner-only" | "all-except-owner" | "none";
2
-
3
- export interface NetworkReplicationFixtureOptions {
4
- readonly seed?: number;
5
- readonly tickRate?: number;
6
- readonly latencyMs?: number;
7
- readonly jitterMs?: number;
8
- readonly interestRadius?: number;
9
- readonly clientPosition?: readonly [number, number, number];
10
- }
11
-
12
- export interface NetworkInputFrame {
13
- readonly sequence: number;
14
- readonly timestampMs: number;
15
- readonly forward: boolean;
16
- readonly right: boolean;
17
- readonly fire: boolean;
18
- }
19
-
20
- export interface NetworkEntityState {
21
- readonly id: string;
22
- readonly ownerId: string;
23
- readonly replicationMode: NetworkReplicationMode;
24
- readonly position: readonly [number, number, number];
25
- readonly velocity: readonly [number, number, number];
26
- readonly health: number;
27
- readonly ammo: number;
28
- readonly animation: string;
29
- readonly timestampMs: number;
30
- readonly lastProcessedInput: number;
31
- }
32
-
33
- export interface NetworkPredictionSummary {
34
- readonly inputCount: number;
35
- readonly predictedSequence: number;
36
- readonly acknowledgedSequence: number;
37
- readonly pendingInputs: number;
38
- readonly predictionError: number;
39
- readonly smoothCorrection: readonly [number, number, number];
40
- readonly replayedPosition: readonly [number, number, number];
41
- readonly reconciliationAccepted: boolean;
42
- }
43
-
44
- export interface NetworkDeltaSummary {
45
- readonly changedFields: readonly string[];
46
- readonly removedFields: readonly string[];
47
- readonly fullSnapshotBytes: number;
48
- readonly deltaBytes: number;
49
- readonly compressionRatio: number;
50
- readonly reconstructedMatches: boolean;
51
- readonly bytesSaved: number;
52
- }
53
-
54
- export interface NetworkInterestSummary {
55
- readonly playerId: string;
56
- readonly radius: number;
57
- readonly added: readonly string[];
58
- readonly removed: readonly string[];
59
- readonly unchanged: readonly string[];
60
- readonly relevant: readonly string[];
61
- readonly culled: readonly string[];
62
- readonly gridCellCount: number;
63
- }
64
-
65
- export interface NetworkInterpolationSummary {
66
- readonly sampleTimestampMs: number;
67
- readonly beforeTimestampMs: number;
68
- readonly afterTimestampMs: number;
69
- readonly interpolatedPosition: readonly [number, number, number];
70
- readonly extrapolatedPosition: readonly [number, number, number];
71
- }
72
-
73
- export interface NetworkReplicationFixture {
74
- readonly source: "origin-master-net-prediction-replication-adapted";
75
- readonly claimBoundary: string;
76
- readonly blockedClaims: readonly string[];
77
- readonly tickRate: number;
78
- readonly latencyMs: number;
79
- readonly jitterMs: number;
80
- readonly inputFrames: readonly NetworkInputFrame[];
81
- readonly clientPredicted: NetworkEntityState;
82
- readonly serverAuthoritative: NetworkEntityState;
83
- readonly prediction: NetworkPredictionSummary;
84
- readonly delta: NetworkDeltaSummary;
85
- readonly interest: NetworkInterestSummary;
86
- readonly interpolation: NetworkInterpolationSummary;
87
- readonly hash: string;
88
- }
89
-
90
- type MutableEntityState = {
91
- id: string;
92
- ownerId: string;
93
- replicationMode: NetworkReplicationMode;
94
- position: [number, number, number];
95
- velocity: [number, number, number];
96
- health: number;
97
- ammo: number;
98
- animation: string;
99
- timestampMs: number;
100
- lastProcessedInput: number;
101
- };
102
-
103
- const source = "origin-master-net-prediction-replication-adapted" as const;
104
- const blockedClaims = [
105
- "real socket transport",
106
- "authoritative production multiplayer server",
107
- "matchmaking/lobby service parity",
108
- "rollback netcode parity",
109
- "voice chat transport parity",
110
- "anti-cheat security parity",
111
- "Unity Netcode parity",
112
- "Unreal replication parity"
113
- ] as const;
114
-
115
- export function sampleNetworkReplicationFixture(options: NetworkReplicationFixtureOptions = {}): NetworkReplicationFixture {
116
- const seed = integerOption(options.seed ?? 2026, "seed");
117
- const tickRate = positiveNumber(options.tickRate ?? 60, "tickRate");
118
- const latencyMs = nonNegativeNumber(options.latencyMs ?? 64, "latencyMs");
119
- const jitterMs = nonNegativeNumber(options.jitterMs ?? 11, "jitterMs");
120
- const interestRadius = positiveNumber(options.interestRadius ?? 18, "interestRadius");
121
- const clientPosition = vec3Option(options.clientPosition ?? [0, 0, 0], "clientPosition");
122
- const inputFrames = buildInputFrames(seed, tickRate, latencyMs, jitterMs);
123
- const initial = entityState({
124
- id: "net-player",
125
- ownerId: "player-a",
126
- replicationMode: "all-clients",
127
- position: [0, 0, 0],
128
- velocity: [0, 0, 0],
129
- health: 100,
130
- ammo: 30,
131
- animation: "idle",
132
- timestampMs: 1_000,
133
- lastProcessedInput: 99
134
- });
135
- const clientPredicted = inputFrames.reduce((state, input) => applyInput(state, input, 1 / tickRate), initial);
136
- const acknowledgedSequence = inputFrames[2]?.sequence ?? inputFrames[0]?.sequence ?? 100;
137
- const serverAuthoritative = entityState({
138
- ...clientPredicted,
139
- position: [
140
- round3(clientPredicted.position[0] - 0.18),
141
- round3(clientPredicted.position[1]),
142
- round3(clientPredicted.position[2] + 0.14)
143
- ],
144
- velocity: [round3(clientPredicted.velocity[0] * 0.88), 0, round3(clientPredicted.velocity[2] * 0.9)],
145
- health: 92,
146
- ammo: 29,
147
- timestampMs: clientPredicted.timestampMs - latencyMs,
148
- lastProcessedInput: acknowledgedSequence
149
- });
150
- const pending = inputFrames.filter((input) => input.sequence > acknowledgedSequence);
151
- const error = vectorDelta(serverAuthoritative.position, clientPredicted.position);
152
- const smoothCorrection: [number, number, number] = [round3(error[0] * 0.18), round3(error[1] * 0.18), round3(error[2] * 0.18)];
153
- const replayed = pending.reduce((state, input) => applyInput(state, input, 1 / tickRate), serverAuthoritative);
154
- const baseSnapshot = stateMap(initial);
155
- const currentSnapshot = stateMap(serverAuthoritative);
156
- const delta = createDeltaSummary(baseSnapshot, currentSnapshot);
157
- const interest = createInterestSummary(clientPosition, interestRadius);
158
- const interpolation = createInterpolationSummary();
159
- const prediction: NetworkPredictionSummary = {
160
- inputCount: inputFrames.length,
161
- predictedSequence: clientPredicted.lastProcessedInput,
162
- acknowledgedSequence,
163
- pendingInputs: pending.length,
164
- predictionError: round3(vectorLength(error)),
165
- smoothCorrection,
166
- replayedPosition: replayed.position,
167
- reconciliationAccepted: delta.reconstructedMatches && pending.length > 0 && vectorLength(error) > 0
168
- };
169
- const hash = stableHash([
170
- source,
171
- tickRate,
172
- latencyMs,
173
- jitterMs,
174
- prediction.predictionError,
175
- prediction.pendingInputs,
176
- delta.changedFields.join(","),
177
- interest.relevant.join(","),
178
- interpolation.interpolatedPosition.join(",")
179
- ].join("|"));
180
- return {
181
- source,
182
- claimBoundary: "This fixture adapts old local networking algorithms into deterministic runtime telemetry only; it does not open sockets, run a server, synchronize browsers, or claim production multiplayer.",
183
- blockedClaims,
184
- tickRate,
185
- latencyMs,
186
- jitterMs,
187
- inputFrames,
188
- clientPredicted,
189
- serverAuthoritative,
190
- prediction,
191
- delta,
192
- interest,
193
- interpolation,
194
- hash
195
- };
196
- }
197
-
198
- function buildInputFrames(seed: number, tickRate: number, latencyMs: number, jitterMs: number): NetworkInputFrame[] {
199
- const start = 2_000 + (seed % 29) + Math.round(latencyMs * 0.25);
200
- const step = Math.round(1_000 / tickRate);
201
- return Array.from({ length: 6 }, (_, index) => ({
202
- sequence: 100 + index,
203
- timestampMs: start + index * step + Math.round(Math.sin(seed + index) * jitterMs),
204
- forward: index !== 4,
205
- right: index >= 1 && index <= 3,
206
- fire: index === 2
207
- }));
208
- }
209
-
210
- function applyInput(state: NetworkEntityState, input: NetworkInputFrame, deltaSeconds: number): NetworkEntityState {
211
- const speed = input.fire ? 4.4 : 5.2;
212
- const vx = input.right ? speed : round3(state.velocity[0] * 0.55);
213
- const vz = input.forward ? -speed : round3(state.velocity[2] * 0.45);
214
- return entityState({
215
- ...state,
216
- position: [
217
- round3(state.position[0] + vx * deltaSeconds),
218
- round3(state.position[1]),
219
- round3(state.position[2] + vz * deltaSeconds)
220
- ],
221
- velocity: [round3(vx), 0, round3(vz)],
222
- ammo: input.fire ? Math.max(0, state.ammo - 1) : state.ammo,
223
- animation: input.forward || input.right ? "run" : "idle",
224
- timestampMs: input.timestampMs,
225
- lastProcessedInput: input.sequence
226
- });
227
- }
228
-
229
- function createDeltaSummary(base: ReadonlyMap<string, unknown>, current: ReadonlyMap<string, unknown>): NetworkDeltaSummary {
230
- const changedFields: string[] = [];
231
- const removedFields: string[] = [];
232
- const reconstructed = new Map(base);
233
- for (const [key, value] of current) {
234
- if (!valuesEqual(base.get(key), value)) {
235
- changedFields.push(key);
236
- reconstructed.set(key, value);
237
- }
238
- }
239
- for (const key of base.keys()) {
240
- if (!current.has(key)) {
241
- removedFields.push(key);
242
- reconstructed.delete(key);
243
- }
244
- }
245
- const fullSnapshotBytes = estimateSnapshotBytes(current);
246
- const deltaBytes = 18 + changedFields.reduce((sum, key) => sum + key.length + estimateValueBytes(current.get(key)), 0) + removedFields.reduce((sum, key) => sum + key.length + 1, 0);
247
- return {
248
- changedFields: changedFields.sort(),
249
- removedFields: removedFields.sort(),
250
- fullSnapshotBytes,
251
- deltaBytes,
252
- compressionRatio: round3(deltaBytes / fullSnapshotBytes),
253
- reconstructedMatches: mapsEqual(reconstructed, current),
254
- bytesSaved: fullSnapshotBytes - deltaBytes
255
- };
256
- }
257
-
258
- function createInterestSummary(clientPosition: readonly [number, number, number], radius: number): NetworkInterestSummary {
259
- const previous = new Set(["net-player", "net-distant-prop"]);
260
- const entities: readonly NetworkEntityState[] = [
261
- entityState({ id: "net-player", ownerId: "player-a", replicationMode: "all-clients", position: [1, 0, -2], velocity: [0, 0, 0], health: 92, ammo: 29, animation: "run", timestampMs: 2_096, lastProcessedInput: 105 }),
262
- entityState({ id: "net-enemy-alpha", ownerId: "server", replicationMode: "all-clients", position: [8, 0, -6], velocity: [0, 0, 0], health: 60, ammo: 0, animation: "alert", timestampMs: 2_096, lastProcessedInput: -1 }),
263
- entityState({ id: "net-loot-health", ownerId: "server", replicationMode: "all-clients", position: [-5, 0, 3], velocity: [0, 0, 0], health: 1, ammo: 0, animation: "idle", timestampMs: 2_096, lastProcessedInput: -1 }),
264
- entityState({ id: "net-owner-secret", ownerId: "player-b", replicationMode: "owner-only", position: [2, 0, 1], velocity: [0, 0, 0], health: 100, ammo: 0, animation: "idle", timestampMs: 2_096, lastProcessedInput: -1 }),
265
- entityState({ id: "net-distant-prop", ownerId: "server", replicationMode: "all-clients", position: [48, 0, 2], velocity: [0, 0, 0], health: 1, ammo: 0, animation: "idle", timestampMs: 2_096, lastProcessedInput: -1 })
266
- ];
267
- const relevant = entities
268
- .filter((entity) => shouldReplicate(entity, "player-a") && vectorLength(vectorDelta(entity.position, clientPosition)) <= radius)
269
- .map((entity) => entity.id)
270
- .sort();
271
- const next = new Set(relevant);
272
- const added = relevant.filter((id) => !previous.has(id));
273
- const removed = [...previous].filter((id) => !next.has(id)).sort();
274
- const unchanged = relevant.filter((id) => previous.has(id));
275
- const culled = entities.map((entity) => entity.id).filter((id) => !next.has(id)).sort();
276
- const gridCells = new Set(entities.map((entity) => `${Math.floor(entity.position[0] / 16)}:${Math.floor(entity.position[2] / 16)}`));
277
- return {
278
- playerId: "player-a",
279
- radius,
280
- added,
281
- removed,
282
- unchanged,
283
- relevant,
284
- culled,
285
- gridCellCount: gridCells.size
286
- };
287
- }
288
-
289
- function createInterpolationSummary(): NetworkInterpolationSummary {
290
- const before = entityState({
291
- id: "net-enemy-alpha",
292
- ownerId: "server",
293
- replicationMode: "all-clients",
294
- position: [4, 0, -3],
295
- velocity: [1.6, 0, -0.8],
296
- health: 60,
297
- ammo: 0,
298
- animation: "run",
299
- timestampMs: 1_000,
300
- lastProcessedInput: -1
301
- });
302
- const after = entityState({
303
- id: before.id,
304
- ownerId: before.ownerId,
305
- replicationMode: before.replicationMode,
306
- position: [5, 0, -3.5],
307
- velocity: [before.velocity[0], before.velocity[1], before.velocity[2]],
308
- health: before.health,
309
- ammo: before.ammo,
310
- animation: before.animation,
311
- timestampMs: 1_050,
312
- lastProcessedInput: before.lastProcessedInput
313
- });
314
- const sampleTimestampMs = 1_025;
315
- const t = (sampleTimestampMs - before.timestampMs) / (after.timestampMs - before.timestampMs);
316
- const interpolatedPosition: [number, number, number] = [
317
- round3(before.position[0] + (after.position[0] - before.position[0]) * t),
318
- round3(before.position[1] + (after.position[1] - before.position[1]) * t),
319
- round3(before.position[2] + (after.position[2] - before.position[2]) * t)
320
- ];
321
- const extrapolatedPosition: [number, number, number] = [
322
- round3(after.position[0] + after.velocity[0] * 0.1),
323
- round3(after.position[1] + after.velocity[1] * 0.1),
324
- round3(after.position[2] + after.velocity[2] * 0.1)
325
- ];
326
- return {
327
- sampleTimestampMs,
328
- beforeTimestampMs: before.timestampMs,
329
- afterTimestampMs: after.timestampMs,
330
- interpolatedPosition,
331
- extrapolatedPosition
332
- };
333
- }
334
-
335
- function stateMap(state: NetworkEntityState): ReadonlyMap<string, unknown> {
336
- return new Map<string, unknown>([
337
- ["x", state.position[0]],
338
- ["y", state.position[1]],
339
- ["z", state.position[2]],
340
- ["vx", state.velocity[0]],
341
- ["vz", state.velocity[2]],
342
- ["health", state.health],
343
- ["ammo", state.ammo],
344
- ["animation", state.animation],
345
- ["lastProcessedInput", state.lastProcessedInput]
346
- ]);
347
- }
348
-
349
- function entityState(state: MutableEntityState): NetworkEntityState {
350
- return {
351
- id: state.id,
352
- ownerId: state.ownerId,
353
- replicationMode: state.replicationMode,
354
- position: [round3(state.position[0]), round3(state.position[1]), round3(state.position[2])],
355
- velocity: [round3(state.velocity[0]), round3(state.velocity[1]), round3(state.velocity[2])],
356
- health: Math.max(0, Math.round(state.health)),
357
- ammo: Math.max(0, Math.round(state.ammo)),
358
- animation: state.animation,
359
- timestampMs: Math.round(state.timestampMs),
360
- lastProcessedInput: Math.round(state.lastProcessedInput)
361
- };
362
- }
363
-
364
- function shouldReplicate(entity: NetworkEntityState, playerId: string): boolean {
365
- if (entity.replicationMode === "none") return false;
366
- if (entity.replicationMode === "owner-only") return entity.ownerId === playerId;
367
- if (entity.replicationMode === "all-except-owner") return entity.ownerId !== playerId;
368
- return true;
369
- }
370
-
371
- function vectorDelta(left: readonly [number, number, number], right: readonly [number, number, number]): [number, number, number] {
372
- return [left[0] - right[0], left[1] - right[1], left[2] - right[2]];
373
- }
374
-
375
- function vectorLength(value: readonly [number, number, number]): number {
376
- return Math.hypot(value[0], value[1], value[2]);
377
- }
378
-
379
- function valuesEqual(left: unknown, right: unknown): boolean {
380
- if (typeof left === "number" && typeof right === "number") return Math.abs(left - right) < 0.0001;
381
- return left === right;
382
- }
383
-
384
- function mapsEqual(left: ReadonlyMap<string, unknown>, right: ReadonlyMap<string, unknown>): boolean {
385
- if (left.size !== right.size) return false;
386
- for (const [key, value] of left) {
387
- if (!valuesEqual(value, right.get(key))) return false;
388
- }
389
- return true;
390
- }
391
-
392
- function estimateSnapshotBytes(snapshot: ReadonlyMap<string, unknown>): number {
393
- let bytes = 16;
394
- for (const [key, value] of snapshot) {
395
- bytes += key.length + estimateValueBytes(value);
396
- }
397
- return bytes;
398
- }
399
-
400
- function estimateValueBytes(value: unknown): number {
401
- if (typeof value === "number") return 9;
402
- if (typeof value === "string") return 3 + value.length;
403
- if (typeof value === "boolean") return 2;
404
- return 1;
405
- }
406
-
407
- function vec3Option(value: readonly [number, number, number], name: string): [number, number, number] {
408
- if (!Array.isArray(value) || value.length !== 3 || value.some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
409
- throw new RangeError(`${name} must be a finite vec3.`);
410
- }
411
- return [value[0], value[1], value[2]];
412
- }
413
-
414
- function integerOption(value: number, name: string): number {
415
- if (!Number.isInteger(value)) throw new RangeError(`${name} must be an integer.`);
416
- return value;
417
- }
418
-
419
- function positiveNumber(value: number, name: string): number {
420
- if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be positive.`);
421
- return value;
422
- }
423
-
424
- function nonNegativeNumber(value: number, name: string): number {
425
- if (!Number.isFinite(value) || value < 0) throw new RangeError(`${name} must be non-negative.`);
426
- return value;
427
- }
428
-
429
- function round3(value: number): number {
430
- return Math.round(value * 1_000) / 1_000;
431
- }
432
-
433
- function stableHash(value: string): string {
434
- let hash = 0x811c9dc5;
435
- for (let index = 0; index < value.length; index += 1) {
436
- hash ^= value.charCodeAt(index);
437
- hash = Math.imul(hash, 0x01000193);
438
- }
439
- return (hash >>> 0).toString(16).padStart(8, "0");
440
- }
package/src/Perception.ts DELETED
@@ -1,163 +0,0 @@
1
- export type PerceptionPoint = readonly [number, number];
2
-
3
- export interface PerceptionTarget {
4
- readonly id: string;
5
- readonly position: PerceptionPoint;
6
- readonly priority?: number;
7
- }
8
-
9
- export interface PerceptionSensorOptions {
10
- readonly position: PerceptionPoint;
11
- readonly forward?: PerceptionPoint;
12
- readonly range?: number;
13
- readonly fovRadians?: number;
14
- readonly peripheralRadians?: number;
15
- readonly minConfidence?: number;
16
- readonly memoryDecayPerSecond?: number;
17
- readonly forgetBelowConfidence?: number;
18
- }
19
-
20
- export interface PerceptionHit {
21
- readonly id: string;
22
- readonly position: PerceptionPoint;
23
- readonly distance: number;
24
- readonly angleRadians: number;
25
- readonly confidence: number;
26
- readonly peripheral: boolean;
27
- readonly justEntered: boolean;
28
- }
29
-
30
- export interface PerceptionMemory {
31
- readonly id: string;
32
- readonly lastKnownPosition: PerceptionPoint;
33
- readonly confidence: number;
34
- readonly lastSeenSeconds: number;
35
- readonly seenCount: number;
36
- }
37
-
38
- export interface PerceptionSnapshot {
39
- readonly visible: readonly PerceptionHit[];
40
- readonly memories: readonly PerceptionMemory[];
41
- readonly closestVisible?: PerceptionHit;
42
- readonly strongestMemory?: PerceptionMemory;
43
- readonly enteredIds: readonly string[];
44
- readonly forgottenIds: readonly string[];
45
- }
46
-
47
- export class PerceptionSensor {
48
- private position: PerceptionPoint;
49
- private forward: PerceptionPoint;
50
- private readonly range: number;
51
- private readonly fovRadians: number;
52
- private readonly peripheralRadians: number;
53
- private readonly minConfidence: number;
54
- private readonly memoryDecayPerSecond: number;
55
- private readonly forgetBelowConfidence: number;
56
- private readonly memories = new Map<string, PerceptionMemory>();
57
- private readonly visibleLastTick = new Set<string>();
58
- private elapsedSeconds = 0;
59
-
60
- constructor(options: PerceptionSensorOptions) {
61
- this.position = options.position;
62
- this.forward = normalize(options.forward ?? [1, 0]);
63
- this.range = options.range ?? 3;
64
- this.fovRadians = options.fovRadians ?? Math.PI / 2;
65
- this.peripheralRadians = options.peripheralRadians ?? Math.PI;
66
- this.minConfidence = options.minConfidence ?? 0.05;
67
- this.memoryDecayPerSecond = options.memoryDecayPerSecond ?? 0.25;
68
- this.forgetBelowConfidence = options.forgetBelowConfidence ?? 0.05;
69
- if (this.range <= 0 || this.fovRadians <= 0 || this.peripheralRadians < this.fovRadians) {
70
- throw new RangeError("PerceptionSensor requires positive range/fov and peripheralRadians >= fovRadians.");
71
- }
72
- }
73
-
74
- updateTransform(position: PerceptionPoint, forward: PerceptionPoint = this.forward): void {
75
- this.position = position;
76
- this.forward = normalize(forward);
77
- }
78
-
79
- scan(targets: readonly PerceptionTarget[], deltaSeconds: number): PerceptionSnapshot {
80
- this.elapsedSeconds += Math.max(0, deltaSeconds);
81
- const previousVisible = new Set(this.visibleLastTick);
82
- this.visibleLastTick.clear();
83
- const visible: PerceptionHit[] = [];
84
- const forgottenIds: string[] = [];
85
-
86
- for (const [id, memory] of [...this.memories]) {
87
- const decayed = Math.max(0, memory.confidence - this.memoryDecayPerSecond * Math.max(0, deltaSeconds));
88
- if (decayed < this.forgetBelowConfidence) {
89
- this.memories.delete(id);
90
- forgottenIds.push(id);
91
- } else {
92
- this.memories.set(id, { ...memory, confidence: round3(decayed) });
93
- }
94
- }
95
-
96
- for (const target of targets) {
97
- const hit = this.evaluateTarget(target, previousVisible.has(target.id));
98
- if (!hit || hit.confidence < this.minConfidence) continue;
99
- visible.push(hit);
100
- this.visibleLastTick.add(target.id);
101
- const previous = this.memories.get(target.id);
102
- this.memories.set(target.id, {
103
- id: target.id,
104
- lastKnownPosition: [round3(target.position[0]), round3(target.position[1])],
105
- confidence: hit.confidence,
106
- lastSeenSeconds: round3(this.elapsedSeconds),
107
- seenCount: (previous?.seenCount ?? 0) + 1
108
- });
109
- }
110
-
111
- visible.sort((left, right) => right.confidence - left.confidence || left.distance - right.distance || left.id.localeCompare(right.id));
112
- const memories = [...this.memories.values()].sort((left, right) => right.confidence - left.confidence || left.id.localeCompare(right.id));
113
- return {
114
- visible,
115
- memories,
116
- closestVisible: [...visible].sort((left, right) => left.distance - right.distance)[0],
117
- strongestMemory: memories[0],
118
- enteredIds: visible.filter((hit) => hit.justEntered).map((hit) => hit.id),
119
- forgottenIds
120
- };
121
- }
122
-
123
- private evaluateTarget(target: PerceptionTarget, wasVisible: boolean): PerceptionHit | undefined {
124
- const offset: PerceptionPoint = [target.position[0] - this.position[0], target.position[1] - this.position[1]];
125
- const distance = length(offset);
126
- if (distance > this.range || distance === 0) return undefined;
127
- const direction = normalize(offset);
128
- const angle = Math.acos(Math.max(-1, Math.min(1, dot(this.forward, direction))));
129
- if (angle > this.peripheralRadians / 2) return undefined;
130
- const peripheral = angle > this.fovRadians / 2;
131
- const distanceConfidence = 1 - distance / this.range;
132
- const angleConfidence = peripheral
133
- ? 0.35 * (1 - (angle - this.fovRadians / 2) / Math.max(0.001, (this.peripheralRadians - this.fovRadians) / 2))
134
- : 1 - angle / Math.max(0.001, this.fovRadians / 2) * 0.4;
135
- const priority = target.priority ?? 1;
136
- return {
137
- id: target.id,
138
- position: [round3(target.position[0]), round3(target.position[1])],
139
- distance: round3(distance),
140
- angleRadians: round3(angle),
141
- confidence: round3(Math.max(0, Math.min(1, distanceConfidence * angleConfidence * priority))),
142
- peripheral,
143
- justEntered: !wasVisible
144
- };
145
- }
146
- }
147
-
148
- function normalize(value: PerceptionPoint): PerceptionPoint {
149
- const magnitude = length(value);
150
- return magnitude === 0 ? [1, 0] : [value[0] / magnitude, value[1] / magnitude];
151
- }
152
-
153
- function length(value: PerceptionPoint): number {
154
- return Math.hypot(value[0], value[1]);
155
- }
156
-
157
- function dot(left: PerceptionPoint, right: PerceptionPoint): number {
158
- return left[0] * right[0] + left[1] * right[1];
159
- }
160
-
161
- function round3(value: number): number {
162
- return Number(value.toFixed(3));
163
- }