Files
healer-man/src/game/combatActors.ts
T
2026-09-28 22:46:45 -04:00

120 lines
4.1 KiB
TypeScript

import type { ClassId } from "../app/characterCatalog";
import type { DamageSchool } from "./combatAuras";
export type CombatActorKind = "player" | "companion" | "enemy" | "pet" | "totem" | "summon";
export type CombatResourceId = string;
export type DeathKnightRuneType = "blood" | "frost" | "unholy" | "death";
export type CombatActorPosition = readonly [number, number, number];
export interface CombatResourcePoolState {
readonly id: CombatResourceId;
readonly current: number;
readonly maximum: number;
readonly regenerationPerSecond: number;
/** Mana regeneration observes the five-second rule from this timestamp. */
readonly lastSpentAt: number;
}
export interface DeathKnightRuneState {
readonly slot: number;
readonly baseType: Exclude<DeathKnightRuneType, "death">;
readonly type: DeathKnightRuneType;
readonly readyAt: number;
readonly deathRuneExpiresAt?: number;
}
export interface CombatActorCastState {
readonly abilityId: string;
readonly school: DamageSchool;
readonly mode: "cast" | "channel";
readonly startedAt: number;
readonly completesAt: number;
readonly originalDurationMs: number;
readonly pushbackHits: number;
/** Simulation-space origin used for movement cancellation and completion validation. */
readonly origin?: CombatActorPosition;
readonly targetId?: string | null;
readonly channelTicksCompleted?: number;
}
/**
* Renderer-free snapshot shared by every combatant. Specialized player, party,
* and mob records may retain presentation fields, but combat rules consume
* this common contract.
*/
export interface CombatActorState {
readonly id: string;
/** Stable id used by threat tables even when a renderer entity is replaced. */
readonly threatActorId?: string;
readonly kind: CombatActorKind;
readonly ownerId?: string;
readonly classId?: ClassId;
readonly level: number;
readonly health: number;
readonly maxHealth: number;
readonly absorb: number;
readonly resources: Readonly<Record<CombatResourceId, CombatResourcePoolState>>;
readonly cooldowns: Readonly<Record<string, number>>;
readonly globalCooldownEndsAt: number;
readonly cast: CombatActorCastState | null;
readonly controlledUntil: number;
readonly schoolLockedUntil: Readonly<Partial<Record<DamageSchool, number>>>;
readonly comboPoints: number;
readonly comboTargetId: string | null;
readonly runes: readonly DeathKnightRuneState[];
/** Executable catalog abilities owned by pets, summons, and companions. */
readonly abilityIds?: readonly string[];
readonly alive: boolean;
}
export function createDeathKnightRunes(now = 0): readonly DeathKnightRuneState[] {
const types = ["blood", "blood", "frost", "frost", "unholy", "unholy"] as const;
return Object.freeze(types.map((type, slot) => Object.freeze({
slot,
baseType: type,
type,
readyAt: Math.max(0, now),
})));
}
export function normalizeActorResourcePool(
id: CombatResourceId,
current: number,
maximum: number,
regenerationPerSecond = 0,
lastSpentAt = 0,
): CombatResourcePoolState {
const safeMaximum = Math.max(0, Number.isFinite(maximum) ? maximum : 0);
return Object.freeze({
id,
current: Math.max(0, Math.min(safeMaximum, Number.isFinite(current) ? current : 0)),
maximum: safeMaximum,
regenerationPerSecond: Math.max(0, Number.isFinite(regenerationPerSecond) ? regenerationPerSecond : 0),
lastSpentAt: Math.max(0, Number.isFinite(lastSpentAt) ? lastSpentAt : 0),
});
}
export function spendActorResource(
pool: CombatResourcePoolState,
amount: number,
now: number,
): CombatResourcePoolState | null {
const requested = Math.max(0, Number.isFinite(amount) ? amount : 0);
if (pool.current + Number.EPSILON < requested) return null;
return Object.freeze({
...pool,
current: Math.max(0, pool.current - requested),
lastSpentAt: Math.max(pool.lastSpentAt, Number.isFinite(now) ? now : 0),
});
}
export function restoreActorResource(
pool: CombatResourcePoolState,
amount: number,
): CombatResourcePoolState {
return Object.freeze({
...pool,
current: Math.min(pool.maximum, pool.current + Math.max(0, Number.isFinite(amount) ? amount : 0)),
});
}