first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# interest-manager.internal
## Purpose
Pure geometry helpers backing `InterestManager`: Euclidean
`vectorDistance` (z defaults to 0 for 2D interop), `zoneContains`
discriminating on `ZoneShape` (`circular` uses radius distance,
`rectangular` uses half-extent checks honouring optional `depth`, `custom`
delegates to the zone-supplied `containsPoint` predicate), and
`calcPriority` clamping a `maxPriority - distance × decay + zoneBonus`
score into `[0, maxPriority]`.
Extracting these keeps `interest-manager.ts` under the per-file line
budget and isolates the geometric primitives so they can be unit-tested
without instantiating the manager.
## Features
- `vectorDistance(a, b)` — 3D Euclidean distance with 2D fallback.
- `zoneContains(zone, point)` — discriminated containment test across
circular, rectangular (optionally 3D), and custom zones.
- `calcPriority(d, n, max, decay)` — clamped priority score with a small
bonus per shared subscribed zone.
## Usage: Basic
```ts
import {
vectorDistance,
zoneContains,
} from "./interest-manager.internal.js";
vectorDistance({ x: 0, y: 0 }, { x: 3, y: 4 }); // 5
zoneContains(
{
id: "z",
shape: "circular",
center: { x: 0, y: 0 },
radius: 5,
priority: 1,
},
{ x: 4, y: 0 }
); // true
```
## Usage: Advanced
```ts
import { calcPriority } from "./interest-manager.internal.js";
// At max distance the bonus still keeps shared-zone entities visible.
calcPriority(80, 3, 10, 0.1); // 3.5
```
## API Reference
- `vectorDistance(a: Vector3, b: Vector3): number`
- `zoneContains(zone: InterestZone, point: Vector3): boolean`
- `calcPriority(distance: number, sharedZoneCount: number, maxPriority:
number, decayRate: number): number`
## Implementation Notes
`zoneContains` treats a missing `depth` on rectangular zones as 2D
containment (no z-check), which matches the migrated upstream behaviour
and lets callers seamlessly mix 2D and 3D entities. The custom-zone path
intentionally invokes the user predicate without exception trapping —
callers are responsible for ensuring `containsPoint` is total, and any
thrown error propagates up through `InterestManager.isInZone`. The
priority formula gives shared-zone entities a half-unit lift per
subscribed zone so that interesting entities don't fall out of the top-N
slice at the edge of `viewDistance`.

View File

@@ -0,0 +1,117 @@
# interest-manager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`InterestManager` implements area-of-interest (AOI) culling for open-world
multiplayer games: it tracks zones, entities, and players in memory and
returns the bounded slice of entities each player can see, ranked by
priority. The class is the standard mechanism for shrinking broadcast
scope from O(world) to O(perceived) updates.
It is dependency-injectable: a `Clock` source provides timestamps and an
OpenTelemetry `Meter` produces visibility-calculation counters and
duration histograms. Configuration is validated with Zod; invalid input
raises `GAME_SERVER.INVALID_INTEREST_CONFIG`. Unknown players passed to
`updatePlayerPosition` raise
`GAME_SERVER.INTEREST_PLAYER_NOT_REGISTERED`.
## Features
- Three zone shapes: circular, rectangular (optionally 3D), custom
(predicate-based).
- Per-player `viewDistance` override (defaults to
`config.defaultViewDistance`).
- Priority-weighted visibility ranking via `maxPriority - distance ×
decay + sharedZones × 0.5` with output clamped into `[0, maxPriority]`.
- `getVisibleEntities` slices to `maxEntitiesPerPlayer`.
- `calculateVisibilityChanges` emits enter/exit deltas only when the set
actually changes (returns `null` on no-op).
- Typed event registry: `entity:added/removed`,
`player:subscribed/unsubscribed`, `visibility:changed`.
- OTel: `game_server.interest.visibility_calc_total` (counter),
`game_server.interest.visibility_calc_duration_ms` (histogram).
## Usage: Basic
```ts
import { InterestManager } from "@suaveplan/game-server/enhanced/interest-management/interest-manager";
const aoi = new InterestManager({ defaultViewDistance: 50 });
aoi.registerPlayer("p1", { x: 0, y: 0 });
aoi.updateEntity("npc", { x: 10, y: 0 }, { kind: "merchant" });
const visible = aoi.getVisibleEntities("p1");
```
## Usage: Advanced
```ts
import { InterestManager } from "@suaveplan/game-server/enhanced/interest-management/interest-manager";
const aoi = new InterestManager<{ name: string }>(
{
defaultViewDistance: 80,
maxEntitiesPerPlayer: 100,
priorityDecayRate: 0.05,
maxPriority: 20,
},
{ clock: () => testNow }
);
aoi.registerZone({
id: "downtown",
shape: "rectangular",
center: { x: 0, y: 0, z: 0 },
width: 200,
height: 200,
depth: 30,
priority: 5,
});
aoi.registerPlayer("p1", { x: 1, y: 1, z: 0 });
aoi.subscribeToZone("p1", "downtown");
const unsub = aoi.on("visibility:changed", (change) => {
syncEnterExitToClient(change.playerId, change.entered, change.exited);
});
// In the server tick:
aoi.updateEntity("merchant", { x: 5, y: 5, z: 0 }, { name: "Sera" });
aoi.calculateVisibilityChanges("p1");
unsub();
```
## API Reference
- `new InterestManager(config, deps?)` — validates `config` with
`InterestManagerConfigSchema`; throws
`GAME_SERVER.INVALID_INTEREST_CONFIG` on failure.
- `registerZone(zone)` / `unregisterZone(zoneId)`; throws
`GAME_SERVER.INTEREST_REGION_INVALID` on empty `zone.id`.
- `updateEntity(id, position, data?, type?)` / `removeEntity(id)`.
- `registerPlayer(id, position, viewDistance?)` /
`updatePlayerPosition(id, position)` / `unregisterPlayer(id)`.
- `subscribeToZone(playerId, zoneId)` /
`unsubscribeFromZone(playerId, zoneId)`.
- `getVisibleEntities(playerId)` / `calculateVisibilityChanges(playerId)`.
- `isInZone(zoneId, point)` / `getZonesAtPoint(point)` / `getZone` /
`getAllZones` / `getEntity` / `getStats` / `dispose`.
- `on(event, listener) → unsubscribe`; `off(event, listener)`.
## Implementation Notes
Geometry primitives (`vectorDistance`, `zoneContains`, `calcPriority`)
live in `interest-manager.internal.ts` so the main class is dominated by
lifecycle, dispatch, and metrics emission. `getVisibleEntities` is the
hot path; it sorts the candidate set in descending priority and slices to
`maxEntitiesPerPlayer` so the result is always bounded. The shared-zones
walk only checks zones the player explicitly subscribed to, so an entity
in an unsubscribed zone is treated as if the zone were absent. Visibility
deltas are derived against the previous result rather than a snapshot of
the world to support incremental synchronisation. `dispose` clears every
internal collection so the manager can be safely garbage-collected after
shutdown.