Files
healer-man/src/game/combatActorAuthority.ts
T

153 lines
6.6 KiB
TypeScript

import { naturePowerMaximumForCharacter, resourceProfileForClass, type ResourceType } from "./abilityCatalog";
import {
normalizeActorResourcePool,
type CombatActorState,
} from "./combatActors";
import type { PartyMember } from "./partyStore";
import { isRomClassId } from "../app/characterCatalog";
import { deriveCharacterStats } from "./wotlkStats";
export function companionResourceMaximum(member: PartyMember, id: string): number {
if (id === member.resourceType) return member.maxResource;
const mirroredMaximum = member.resourceMaximums?.[id as ResourceType];
if (mirroredMaximum !== undefined) return mirroredMaximum;
return id === "mana"
&& (member.statRuleset === "rom" || !isRomClassId(member.classId))
? deriveCharacterStats(
member.classId, member.level, member.gearStats, member.raceId, {},
member.secondaryClassId, member.gender, member.statRuleset ?? "wow",
).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);
let resources: CombatActorState["resources"];
if (existing?.kind === "companion" && member.statRuleset === "rom" && isRomClassId(member.classId)) {
const previousResources = existing.resources;
const nextResources: Record<string, ReturnType<typeof normalizeActorResourcePool>> = {};
const resourceIds = new Set([...Object.keys(previousResources), ...Object.keys(member.resourcePools)]);
for (const id of resourceIds) {
const previous = previousResources[id];
const nextMaximum = companionResourceMaximum(member, id);
const hasChangedMaximum = previous !== undefined && Math.abs(previous.maximum - nextMaximum) > 1e-6;
const current = hasChangedMaximum
? member.resourcePools[id as ResourceType] ?? previous.current
: previous?.current ?? member.resourcePools[id as ResourceType] ?? 0;
nextResources[id] = normalizeActorResourcePool(
id,
current,
nextMaximum,
id === member.resourceType ? profile.regenerationPerSecond : previous?.regenerationPerSecond ?? 0,
member.lastResourceSpentAt[id as ResourceType] ?? previous?.lastSpentAt ?? 0,
);
}
resources = nextResources;
} else if (existing?.kind === "companion") {
resources = existing.resources;
} else {
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,
statRuleset: member.statRuleset,
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<Record<ResourceType, number>>;
const resourceMaximums = Object.fromEntries(Object.entries(actor.resources).map(([id, pool]) => [id, pool.maximum])) as Partial<Record<ResourceType, number>>;
const lastResourceSpentAt = Object.fromEntries(Object.entries(actor.resources).map(([id, pool]) => [id, pool.lastSpentAt])) as Partial<Record<ResourceType, number>>;
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,
resourceMaximums,
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,
});
}