120 lines
4.1 KiB
TypeScript
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)),
|
|
});
|
|
}
|