Update Healer Man server

This commit is contained in:
phenom committed 2026-09-20 11:23:13 -04:00
1 parent 1fedb9368d
commit cc3b2fa4d5
27 files changed
+1988 -83

No files matched your search

+51 -7
View File
@@ -24,6 +24,10 @@ export interface PartyAbilityContext {
readonly target: MobCombatState | null;
readonly targetIsLoose?: boolean;
readonly hasDangerousThreat?: boolean;
readonly enemyUsesMeleeAttacks?: boolean;
readonly incomingMeleeDanger?: boolean;
readonly needsEscape?: boolean;
readonly moving?: boolean;
readonly requiredControl?: boolean;
readonly allyNeedsDispel?: boolean;
readonly fallenAlly?: boolean;
@@ -35,8 +39,11 @@ export interface PartyAbilityContext {
readonly activeAuraExclusiveGroups?: ReadonlySet<string>;
readonly activeSummonAbilityIds?: ReadonlySet<string>;
readonly hasActivePet?: boolean;
readonly availableConditionalAbilityNames?: ReadonlySet<string>;
readonly canUseAbility?: (ability: AbilityDefinition) => boolean;
readonly incomingDefensiveMechanic?: CompanionCooldownMechanicRule | null;
/** Inner Focus-style setup lets the next direct healing spell ignore resource costs. */
readonly nextHealingSpellFree?: boolean;
}
function currentResource(member: PartyMember, type: ResourceType): number {
@@ -52,12 +59,17 @@ function abilityCosts(member: PartyMember, ability: AbilityDefinition): readonly
}));
}
export function partyAbilityIsReady(member: PartyMember, ability: AbilityDefinition, now: number): boolean {
export function partyAbilityIsReady(
member: PartyMember,
ability: AbilityDefinition,
now: number,
ignoreResourceCosts = false,
): boolean {
if (ability.passive || !learnedAbilityForBuild(ability, member.level, member.talentRanks)) return false;
if (member.globalCooldownEndsAt > now || (member.cooldowns[ability.id] ?? 0) > now) return false;
if ((member.schoolLockedUntil[ability.castRules?.school ?? "physical"] ?? 0) > now) return false;
if (aiHintsForAbility(ability).categories.includes("taunt") && member.tauntReadyAt > now) return false;
if (abilityCosts(member, ability).some(cost => cost.resource === "health"
if (!ignoreResourceCosts && abilityCosts(member, ability).some(cost => cost.resource === "health"
? member.health <= cost.amount : currentResource(member, cost.resource) < cost.amount)) return false;
const runeCost = member.classId === "death-knight" ? runeCostForAbility(ability) : null;
return !runeCost || spendDeathKnightRunes(member.runes, runeCost, now) !== null;
@@ -74,7 +86,7 @@ function isComboFinisher(ability: AbilityDefinition): boolean {
|| (effect.kind === "combo" && effect.amount < 0));
}
const RESERVED_SIGNATURE_ACTIONS = new Set(["Mortal Strike"]);
const RESERVED_SIGNATURE_ACTIONS = new Set(["Mortal Strike", "Mutilate", "Envenom"]);
function shouldReserveForHigherPriority(
member: PartyMember,
@@ -87,8 +99,10 @@ function shouldReserveForHigherPriority(
if (!rotation || decisionIndex <= 0) return false;
return rotation.abilityPriority.slice(0, decisionIndex).some((name) => {
const candidate = learned.find((ability) => ability.name === name);
if (!candidate || candidate.passive || isComboFinisher(candidate)) return false;
if (!candidate || candidate.passive) return false;
if (!RESERVED_SIGNATURE_ACTIONS.has(candidate.name)) return false;
if (isComboFinisher(candidate)
&& (member.comboPoints < 4 || member.comboTargetId !== decision.targetId)) return false;
if ((member.cooldowns[candidate.id] ?? 0) > now) return false;
if ((member.schoolLockedUntil[candidate.castRules?.school ?? "physical"] ?? 0) > now) return false;
const categories = aiHintsForAbility(candidate).categories;
@@ -141,7 +155,12 @@ export function choosePartyRotationDecision(
comboPoints: member.comboPoints,
comboTargetId: member.comboTargetId,
runes: member.runes,
abilities: learned.filter((ability) => partyAbilityIsReady(member, ability, context.now)),
abilities: learned.filter((ability) => partyAbilityIsReady(
member,
ability,
context.now,
Boolean(context.nextHealingSpellFree && ability.effects.some((effect) => effect.kind === "heal")),
)),
knownDamagingAbilityNames: new Set(learned.filter((ability) => {
const categories = aiHintsForAbility(ability).categories;
return categories.includes("damage") || categories.includes("execute");
@@ -151,11 +170,16 @@ export function choosePartyRotationDecision(
hostileCount: context.hostileCount,
targetIsLoose: context.targetIsLoose,
hasDangerousThreat: context.hasDangerousThreat,
enemyUsesMeleeAttacks: context.enemyUsesMeleeAttacks,
incomingMeleeDanger: context.incomingMeleeDanger,
needsEscape: context.needsEscape,
moving: context.moving,
requiredControl: context.requiredControl,
maintainedAbilityIds: context.maintainedAbilityIds,
activeAuraExclusiveGroups: context.activeAuraExclusiveGroups,
activeSummonAbilityIds: context.activeSummonAbilityIds,
hasActivePet: context.hasActivePet,
availableConditionalAbilityNames: context.availableConditionalAbilityNames,
canUseAbility: context.canUseAbility,
cooldownPolicy: currentCompanionCooldownPolicy(),
incomingDefensiveMechanic: context.incomingDefensiveMechanic,
@@ -176,13 +200,14 @@ export function commitPartyAbilityUse(
member: PartyMember,
ability: AbilityDefinition,
now: number,
options: { readonly ignoreResourceCosts?: boolean } = {},
): PartyMember | null {
if (!partyAbilityIsReady(member, ability, now)) return null;
if (!partyAbilityIsReady(member, ability, now, options.ignoreResourceCosts)) return null;
const resourcePools = { ...member.resourcePools };
const lastResourceSpentAt = { ...member.lastResourceSpentAt };
let resource = member.resource;
let health = member.health;
for (const cost of abilityCosts(member, ability)) {
for (const cost of options.ignoreResourceCosts ? [] : abilityCosts(member, ability)) {
const amount = cost.amount;
if (cost.resource === "health") { health -= amount; continue; }
const current = currentResource(member, cost.resource);
@@ -214,7 +239,26 @@ export function partyAbilityAmount(
member: PartyMember,
ability: AbilityDefinition,
effect: AbilityEffect,
poisonDoses = 0,
): number {
if (effect.kind === "finisher-damage" && effect.poison) {
const consumedDoses = Math.max(0, Math.min(
effect.poison.maximumDoses,
Math.floor(poisonDoses),
));
return equipmentAdjustedCombatAmount(
{
coefficient: 0,
basePoints: effect.poison.basePointsPerDose * consumedDoses,
dieSides: 1,
attackPowerCoefficient: effect.poison.attackPowerCoefficientPerDose * consumedDoses,
},
member.level,
member.gearStats,
member.classId,
ability,
);
}
const coefficient = "coefficient" in effect
? effect.coefficient
: effect.kind === "finisher-damage"