Release Healer Man 0.1.49

This commit is contained in:
phenom committed 2026-09-17 09:13:15 -04:00
1 parent 39425f2494
commit 612170afd0
91 files changed
+2278 -263

No files matched your search

+73 -11
View File
@@ -1,6 +1,6 @@
import {
abilityAtLevel,
isAbilityUnlocked,
learnedAbilityForBuild,
resourceProfileForClass,
type AbilityDefinition,
} from "../abilityCatalog";
import { aiHintsForAbility, canAbilityCastWhileMoving, targetingRulesForAbility } from "../abilityRules";
@@ -15,8 +15,9 @@ import type { PartyState } from "../partyStore";
import { usePartyStore } from "../partyStore";
import type { PartyRole } from "../partyRoles";
import type { BenchmarkScenarioDefinition } from "./benchmarkTypes";
import { COMBAT_ROTATION_VERSION, evaluateCombatRotation } from "../combatRotation";
export const BENCHMARK_POLICY_ID = "baseline-v3";
export const BENCHMARK_POLICY_ID = `rotation-v4:${COMBAT_ROTATION_VERSION}`;
export interface BenchmarkPolicyDecision {
readonly made: boolean;
@@ -68,12 +69,9 @@ function friendlyTargets(combat: CombatState, party: PartyState): readonly Frien
}
function learnedAbility(ability: AbilityDefinition, combat: CombatState): AbilityDefinition | null {
if (ability.passive || !isAbilityUnlocked(ability, combat.level)) return null;
const learnedRank = ability.talentEntryId === undefined
? undefined
: combat.talentRanks[`coa-entry-${ability.talentEntryId}`] ?? 0;
if (ability.talentEntryId !== undefined && !learnedRank) return null;
const ranked = abilityAtLevel(ability, combat.level, learnedRank);
if (ability.passive) return null;
const ranked = learnedAbilityForBuild(ability, combat.level, combat.talentRanks);
if (!ranked) return null;
if (ranked.effects.every((effect) => effect.kind === "unsupported" || effect.kind === "scripted")) return null;
return ranked;
}
@@ -195,13 +193,77 @@ export class BenchmarkController {
) return { made: false, cast: false, resourceBlocked: false, invalidFailures: 0 };
const targets = friendlyTargets(combat, party);
const candidates = combat.abilities
const availableAbilities = combat.abilities
.map((ability) => learnedAbility(ability, combat))
.filter((ability): ability is AbilityDefinition => ability !== null)
.filter((ability) => !moving || ability.castTimeMs <= 0 || canAbilityCastWhileMoving(ability))
.filter((ability) => !moving || ability.castTimeMs <= 0 || canAbilityCastWhileMoving(ability));
const candidates = availableAbilities
.map((ability) => scoreAbility(ability, role, scenario, combat, targets))
.filter((candidate): candidate is ScoredAbility => candidate !== null)
.sort((left, right) => right.score - left.score || left.ability.id.localeCompare(right.ability.id));
const hostileTarget = combat.selectedTargetId ? combat.mobs[combat.selectedTargetId] : null;
const sharedDecision = evaluateCombatRotation({
now,
classId: combat.classId,
secondaryClassId: combat.secondaryClassId,
specializationId: combat.specializationId,
role,
level: combat.level,
health: combat.health,
maxHealth: combat.maxHealth,
selfId: PLAYER_AGGRO_ID,
resources: combat.resourcePools,
maximumResources: combat.maxResourcePools,
primaryResource: resourceProfileForClass(combat.classId).type,
comboPoints: combat.comboPoints,
comboTargetId: combat.comboTargetId,
runes: combat.deathKnightRunes,
abilities: availableAbilities,
hostileTarget,
friendlyTargets: [
{ id: PLAYER_AGGRO_ID, health: combat.health, maxHealth: combat.maxHealth, fallen: combat.health <= 0 },
...party.members.map((member) => ({
id: member.id,
health: member.health,
maxHealth: member.maxHealth,
fallen: member.health <= 0,
})),
],
hostileCount: scenario.hostileTargets,
targetIsLoose: role === "tank" && Boolean(hostileTarget?.targetActorId && hostileTarget.targetActorId !== PLAYER_AGGRO_ID),
hasDangerousThreat: role !== "tank" && hostileTarget?.targetActorId === PLAYER_AGGRO_ID,
moving,
maintainedAbilityIds: new Set([
...combat.effects.filter((effect) => effect.sourceActorId === PLAYER_AGGRO_ID && effect.remainingTicks > 0)
.map((effect) => effect.sourceAbilityId),
...combat.auras.filter((aura) => aura.sourceId === PLAYER_AGGRO_ID
&& (aura.expiresAt === null || aura.expiresAt > now))
.flatMap((aura) => availableAbilities
.filter((ability) => ability.effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.id === aura.definition.id))
.map((ability) => ability.id)),
]),
activeAuraExclusiveGroups: new Set(combat.auras
.filter((aura) => aura.sourceId === PLAYER_AGGRO_ID && (aura.expiresAt === null || aura.expiresAt > now))
.flatMap((aura) => aura.definition.exclusiveGroup ? [aura.definition.exclusiveGroup] : [])),
activeSummonAbilityIds: new Set(combat.summons
.filter((summon) => summon.ownerActorId === PLAYER_AGGRO_ID && summon.expiresAt > now)
.map((summon) => summon.sourceAbilityId)),
hasActivePet: combat.summons.some((summon) => {
if (summon.ownerActorId !== PLAYER_AGGRO_ID || summon.expiresAt <= now) return false;
const source = availableAbilities.find((ability) => ability.id === summon.sourceAbilityId);
return source?.effects.some((effect) => effect.kind === "pet"
&& ["summon", "call", "resurrect", "tame"].includes(effect.action)) ?? false;
}),
});
if (sharedDecision) {
const friendlyTarget = sharedDecision.targetId
? targets.find((target) => target.id === sharedDecision.targetId) ?? null
: null;
const existing = candidates.findIndex((candidate) => candidate.ability.id === sharedDecision.ability.id);
if (existing >= 0) candidates.splice(existing, 1);
candidates.unshift({ ability: sharedDecision.ability, score: Number.POSITIVE_INFINITY, friendlyTarget });
}
// Rage healers must begin weapon attacks to fund their heals before allies are injured.
if (role === "healer" && combat.selectedTargetId