import { naturePowerMaximumForCharacter, resourceProfileForClass, type ResourceType } from "./abilityCatalog"; import { normalizeActorResourcePool, type CombatActorState, } from "./combatActors"; import type { PartyMember } from "./partyStore"; import { baseClassFor, isRomClassId } from "../app/characterCatalog"; import { deriveCharacterStats } from "./wotlkStats"; export function companionResourceMaximum(member: PartyMember, id: string): number { return id === member.resourceType ? member.maxResource : id === "mana" && !isRomClassId(member.classId) ? deriveCharacterStats(baseClassFor(member.classId), member.level, member.gearStats, member.raceId).mana : id === "nature-power" ? naturePowerMaximumForCharacter(member.classId, member.secondaryClassId, member.level) : id === "psi" ? 6 : 100; } /** * Creates the authoritative companion actor. Existing actor values win over * the party-frame compatibility record once the actor has entered combat. */ export function companionActorFromMember( member: PartyMember, absorb = 0, existing?: CombatActorState | null, ): CombatActorState { const profile = resourceProfileForClass(member.classId); const resources = existing?.kind === "companion" ? existing.resources : Object.fromEntries(Object.entries(member.resourcePools).map(([id, current]) => [id, normalizeActorResourcePool( id, current ?? 0, companionResourceMaximum(member, id), id === member.resourceType ? profile.regenerationPerSecond : 0, member.lastResourceSpentAt[id as ResourceType] ?? 0, ), ])); const actor = existing?.kind === "companion" ? existing : null; return Object.freeze({ id: member.id, threatActorId: member.id, kind: "companion" as const, classId: member.classId, level: member.level, health: actor?.health ?? member.health, maxHealth: actor?.maxHealth ?? member.maxHealth, absorb, resources, cooldowns: actor?.cooldowns ?? member.cooldowns, globalCooldownEndsAt: actor?.globalCooldownEndsAt ?? member.globalCooldownEndsAt, cast: actor?.cast ?? member.activeCast, controlledUntil: actor?.controlledUntil ?? member.controlledUntil, schoolLockedUntil: actor?.schoolLockedUntil ?? member.schoolLockedUntil, comboPoints: actor?.comboPoints ?? member.comboPoints, comboTargetId: actor?.comboTargetId ?? member.comboTargetId, runes: actor?.runes ?? member.runes, abilityIds: actor?.abilityIds, alive: (actor?.health ?? member.health) > 0, }); } /** Party and scene consumers receive a read-compatible mirror of actor state. */ export function memberFromCompanionActor(member: PartyMember, actor?: CombatActorState | null): PartyMember { if (!actor || actor.kind !== "companion") return member; const resourcePools = Object.fromEntries(Object.entries(actor.resources).map(([id, pool]) => [id, pool.current])) as Partial>; const lastResourceSpentAt = Object.fromEntries(Object.entries(actor.resources).map(([id, pool]) => [id, pool.lastSpentAt])) as Partial>; const primary = actor.resources[member.resourceType]; return { ...member, health: actor.health, maxHealth: actor.maxHealth, resource: primary?.current ?? member.resource, maxResource: primary?.maximum ?? member.maxResource, resourcePools, lastResourceSpentAt, comboPoints: actor.comboPoints, comboTargetId: actor.comboTargetId, cooldowns: actor.cooldowns, globalCooldownEndsAt: actor.globalCooldownEndsAt, activeCast: actor.cast, activeCastTargetId: actor.cast?.targetId ?? member.activeCastTargetId, schoolLockedUntil: actor.schoolLockedUntil, runes: actor.runes, controlledUntil: actor.controlledUntil, status: actor.health <= 0 ? "down" : member.status, }; } export function companionActorWithMemberRuntime( actor: CombatActorState, member: PartyMember, ): CombatActorState { const profile = resourceProfileForClass(member.classId); const resources = Object.fromEntries(Object.entries(member.resourcePools).map(([id, current]) => [id, normalizeActorResourcePool( id, current ?? 0, companionResourceMaximum(member, id), id === member.resourceType ? profile.regenerationPerSecond : actor.resources[id]?.regenerationPerSecond ?? 0, member.lastResourceSpentAt[id as ResourceType] ?? actor.resources[id]?.lastSpentAt ?? 0, ), ])); return Object.freeze({ ...actor, health: member.health, maxHealth: member.maxHealth, resources, cooldowns: member.cooldowns, globalCooldownEndsAt: member.globalCooldownEndsAt, cast: member.activeCast ? { ...member.activeCast, targetId: member.activeCastTargetId } : null, controlledUntil: member.controlledUntil, schoolLockedUntil: member.schoolLockedUntil, comboPoints: member.comboPoints, comboTargetId: member.comboTargetId, runes: member.runes, alive: member.health > 0, }); }