Release Healer Man 0.1.49
This commit is contained in:
1 parent
39425f2494
commit
612170afd0
91 files changed
+2278
-263
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user