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,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.