# interest-manager _(no submodules)_ ## 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.