153 lines
6.6 KiB
TypeScript
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,
|
|
});
|
|
}
|