import type { ClassId } from "../app/characterCatalog"; import type { OnlineMobTransform, OnlineRelayEventInput, OnlineSessionPlayer, } from "../app/onlineSessionTypes"; import { normalizeActorResourcePool, type CombatActorState, } from "./combatActors"; const ONLINE_PLAYER_PREFIX = "online-player:"; let active = false; let authority = true; let localAccountId: string | null = null; let playerActors = new Map(); let players = new Map(); let mobTransforms = new Map(); let interactionSequence = 0; let interactions: OnlineRelayEventInput[] = []; export function onlinePlayerActorId(accountId: string): string { return `${ONLINE_PLAYER_PREFIX}${accountId}`; } export function onlineAccountIdFromActor(actorId: string): string | null { return actorId.startsWith(ONLINE_PLAYER_PREFIX) ? actorId.slice(ONLINE_PLAYER_PREFIX.length) : null; } export function isOnlinePlayerActorId(actorId: string): boolean { return actorId.startsWith(ONLINE_PLAYER_PREFIX); } export function configureOnlineSessionRuntime(input: { readonly active: boolean; readonly authority: boolean; readonly localAccountId: string | null; readonly players?: readonly OnlineSessionPlayer[]; readonly mobTransforms?: Readonly>; }): void { active = input.active; authority = !input.active || input.authority; localAccountId = input.localAccountId; players = new Map((input.players ?? []).map((player) => [player.accountId, player])); mobTransforms = new Map(Object.entries(input.mobTransforms ?? {})); const nextActors = new Map(); for (const player of input.players ?? []) { if (player.accountId === input.localAccountId || !player.character) continue; const id = onlinePlayerActorId(player.accountId); const maximum = Math.max(0, player.maxResource); const resourceId = player.resourceName.toLowerCase() || "resource"; nextActors.set(id, { id, threatActorId: id, kind: "companion", ownerId: player.accountId, classId: player.character.classId as ClassId, level: player.character.level, health: player.health, maxHealth: player.maxHealth, absorb: 0, resources: maximum > 0 ? { [resourceId]: normalizeActorResourcePool(resourceId, player.resource, maximum) } : {}, cooldowns: {}, globalCooldownEndsAt: 0, cast: player.activeCast ? { abilityId: player.activeCast.abilityId, school: player.activeCast.school, mode: player.activeCast.mode, startedAt: player.activeCast.startedAt, completesAt: player.activeCast.completesAt, originalDurationMs: player.activeCast.originalDurationMs, pushbackHits: player.activeCast.pushbackHits, origin: player.activeCast.origin, targetId: player.activeCast.targetId, } : null, controlledUntil: 0, schoolLockedUntil: {}, comboPoints: 0, comboTargetId: null, runes: [], alive: player.health > 0, }); } playerActors = nextActors; if (!input.active) interactions = []; } export function onlineSessionIsActive(): boolean { return active; } export function onlineSessionOwnsWorldSimulation(): boolean { return !active || authority; } export function onlineSessionLocalAccountId(): string | null { return localAccountId; } export function onlinePlayerRuntimeActors(): readonly CombatActorState[] { return [...playerActors.values()]; } export function onlinePlayerRuntimeActor(actorId: string): CombatActorState | null { return playerActors.get(actorId) ?? null; } export function onlineSessionPlayer(accountId: string): OnlineSessionPlayer | null { return players.get(accountId) ?? null; } export function onlineSynchronizedMobTransform(id: string): OnlineMobTransform | null { return active && !authority ? mobTransforms.get(id) ?? null : null; } export function requestOnlineInteraction(kind: "loot" | "portal", targetActorId = ""): boolean { if (!active || authority) return false; interactionSequence += 1; interactions.push({ clientEventId: `interaction:${kind}:${Date.now()}:${interactionSequence}`, kind, sourceActorId: localAccountId ? onlinePlayerActorId(localAccountId) : "", targetActorId, occurredAt: Date.now(), }); return true; } export function drainOnlineInteractions(): readonly OnlineRelayEventInput[] { const pending = interactions; interactions = []; return pending; }