Release Healer Man 0.1.6

This commit is contained in:
phenom committed 2026-08-20 14:05:50 -04:00
1 parent 55cfd43d66
commit b12fb84859
96 files changed
+10874 -609

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
import {
createDeathKnightRunes,
type CombatResourcePoolState,
type DeathKnightRuneState,
type DeathKnightRuneType,
} from "./combatActors";
export const MANA_FIVE_SECOND_RULE_MS = 5_000;
export const DEATH_KNIGHT_RUNE_RECHARGE_MS = 10_000;
export interface CombatResourceAdvanceRules {
readonly inCombat: boolean;
readonly actorLevel: number;
readonly now: number;
readonly deltaSeconds: number;
readonly manaSpiritRegenerationPerSecond?: number;
}
export function advanceCombatResourcePool(
pool: CombatResourcePoolState,
rules: CombatResourceAdvanceRules,
): CombatResourcePoolState {
const delta = Math.max(0, Math.min(1, Number.isFinite(rules.deltaSeconds) ? rules.deltaSeconds : 0));
let rate = pool.regenerationPerSecond;
if (pool.id === "mana" && rules.now - pool.lastSpentAt < MANA_FIVE_SECOND_RULE_MS) {
rate = Math.max(0, rate - Math.max(0, rules.manaSpiritRegenerationPerSecond ?? 0));
}
if ((pool.id === "rage" || pool.id === "runic-power") && !rules.inCombat) {
rate = pool.id === "rage" ? -3 : -2.5;
}
return Object.freeze({
...pool,
current: Math.max(0, Math.min(pool.maximum, pool.current + rate * delta)),
});
}
export interface RuneCost {
readonly blood?: number;
readonly frost?: number;
readonly unholy?: number;
}
export function availableDeathKnightRunes(
runes: readonly DeathKnightRuneState[],
now: number,
): readonly DeathKnightRuneState[] {
return runes.filter((rune) => rune.readyAt <= now);
}
function runeMatches(rune: DeathKnightRuneState, type: Exclude<DeathKnightRuneType, "death">): boolean {
return rune.type === type || rune.type === "death";
}
export function spendDeathKnightRunes(
runes: readonly DeathKnightRuneState[] | undefined,
cost: RuneCost,
now: number,
rechargeMs = DEATH_KNIGHT_RUNE_RECHARGE_MS,
): readonly DeathKnightRuneState[] | null {
const current = runes?.length === 6 ? [...runes] : [...createDeathKnightRunes(now)];
const spent = new Set<number>();
for (const type of ["blood", "frost", "unholy"] as const) {
const count = Math.max(0, Math.trunc(cost[type] ?? 0));
for (let index = 0; index < count; index += 1) {
const candidate = current.find((rune) => (
!spent.has(rune.slot) && rune.readyAt <= now && runeMatches(rune, type)
));
if (!candidate) return null;
spent.add(candidate.slot);
}
}
return Object.freeze(current.map((rune) => spent.has(rune.slot)
? Object.freeze({ ...rune, readyAt: now + Math.max(1, rechargeMs) })
: rune));
}
export function activateDeathKnightRunes(
runes: readonly DeathKnightRuneState[],
count: number,
): readonly DeathKnightRuneState[] {
let remaining = Math.max(0, Math.trunc(count));
return Object.freeze([...runes]
.sort((left, right) => right.readyAt - left.readyAt || left.slot - right.slot)
.map((rune) => {
if (remaining <= 0 || rune.readyAt <= 0) return rune;
remaining -= 1;
return Object.freeze({ ...rune, readyAt: 0 });
})
.sort((left, right) => left.slot - right.slot));
}