forked from phenom/healer-man
122 lines
3.5 KiB
TypeScript
122 lines
3.5 KiB
TypeScript
export const PLAYER_AGGRO_ID = "__player__";
|
|
|
|
export type AggroActorId = string;
|
|
export type AggroRole = "tank" | "healer" | "damage";
|
|
export type ThreatTable = Readonly<Record<AggroActorId, number>>;
|
|
|
|
export interface ThreatSource {
|
|
readonly actorId: AggroActorId;
|
|
readonly role: AggroRole;
|
|
readonly abilityId?: string;
|
|
/** Final scalar applied after the role multiplier, typically from talents. */
|
|
readonly threatScale?: number;
|
|
}
|
|
|
|
export interface ForcedTarget {
|
|
readonly actorId: AggroActorId;
|
|
readonly endsAt: number;
|
|
}
|
|
|
|
export const TANK_DAMAGE_THREAT_MULTIPLIER = 5;
|
|
export const DAMAGE_THREAT_MULTIPLIER = 1;
|
|
export const HEALING_THREAT_MULTIPLIER = 0.25;
|
|
export const PACK_PULL_THREAT = 1;
|
|
|
|
function safeThreat(value: number): number {
|
|
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
}
|
|
|
|
export function threatFromDamage(amount: number, role: AggroRole, threatScale = 1): number {
|
|
return safeThreat(amount) * (
|
|
role === "tank" ? TANK_DAMAGE_THREAT_MULTIPLIER : DAMAGE_THREAT_MULTIPLIER
|
|
) * safeThreat(threatScale);
|
|
}
|
|
|
|
export function threatFromHealing(amount: number, engagedMobCount: number): number {
|
|
const count = Math.max(0, Math.trunc(engagedMobCount));
|
|
return count > 0 ? safeThreat(amount) * HEALING_THREAT_MULTIPLIER / count : 0;
|
|
}
|
|
|
|
export function addActorThreat(
|
|
table: ThreatTable,
|
|
actorId: AggroActorId,
|
|
amount: number,
|
|
): ThreatTable {
|
|
const added = safeThreat(amount);
|
|
if (!actorId || added <= 0) return table;
|
|
return {
|
|
...table,
|
|
[actorId]: safeThreat(table[actorId] ?? 0) + added,
|
|
};
|
|
}
|
|
|
|
export function seedActorThreat(
|
|
table: ThreatTable,
|
|
actorId: AggroActorId,
|
|
amount = PACK_PULL_THREAT,
|
|
): ThreatTable {
|
|
if (!actorId || safeThreat(table[actorId] ?? 0) > 0) return table;
|
|
return addActorThreat(table, actorId, amount);
|
|
}
|
|
|
|
export function tauntThreat(
|
|
table: ThreatTable,
|
|
actorId: AggroActorId,
|
|
): ThreatTable {
|
|
const maximum = Object.values(table).reduce((highest, value) => (
|
|
Math.max(highest, safeThreat(value))
|
|
), 0);
|
|
return {
|
|
...table,
|
|
[actorId]: maximum + 1,
|
|
};
|
|
}
|
|
|
|
export interface AggroTargetResolution {
|
|
readonly targetActorId: AggroActorId | null;
|
|
readonly forcedTarget: ForcedTarget | null;
|
|
}
|
|
|
|
export function resolveAggroTarget(
|
|
table: ThreatTable,
|
|
eligibleActorIds: ReadonlySet<AggroActorId>,
|
|
currentTargetActorId: AggroActorId | null,
|
|
forcedTarget: ForcedTarget | null,
|
|
now: number,
|
|
): AggroTargetResolution {
|
|
const activeForced = forcedTarget
|
|
&& forcedTarget.endsAt > now
|
|
&& eligibleActorIds.has(forcedTarget.actorId)
|
|
? forcedTarget
|
|
: null;
|
|
if (activeForced) {
|
|
return { targetActorId: activeForced.actorId, forcedTarget: activeForced };
|
|
}
|
|
|
|
const ranked = Object.entries(table)
|
|
.filter(([actorId, threat]) => eligibleActorIds.has(actorId) && safeThreat(threat) > 0)
|
|
.sort(([leftId, leftThreat], [rightId, rightThreat]) => (
|
|
safeThreat(rightThreat) - safeThreat(leftThreat)
|
|
|| Number(rightId === currentTargetActorId) - Number(leftId === currentTargetActorId)
|
|
|| leftId.localeCompare(rightId)
|
|
));
|
|
return {
|
|
targetActorId: ranked[0]?.[0] ?? null,
|
|
forcedTarget: null,
|
|
};
|
|
}
|
|
|
|
export function pruneThreatTable(
|
|
table: ThreatTable,
|
|
eligibleActorIds: ReadonlySet<AggroActorId>,
|
|
): ThreatTable {
|
|
const entries = Object.entries(table).filter(([actorId, threat]) => (
|
|
eligibleActorIds.has(actorId) && safeThreat(threat) > 0
|
|
));
|
|
if (
|
|
entries.length === Object.keys(table).length
|
|
&& entries.every(([actorId, threat]) => table[actorId] === threat)
|
|
) return table;
|
|
return Object.fromEntries(entries);
|
|
}
|