Update Healer Man server

This commit is contained in:
phenom committed 2026-10-04 13:13:35 -04:00
1 parent 39458d5de6
commit 6cc1b9217f
203 files changed
+133929 -31271

No files matched your search

+74 -5
View File
@@ -1,18 +1,21 @@
import type { ClassId } from "../app/characterCatalog";
import type { AbilityDefinition, ResourceType } from "./abilityCatalog";
import { coaBarbarianDecapitateTargetAllowed } from "./coaBarbarianExecuteRuntime";
import { aiHintsForAbility, canAbilityCastWhileMoving, runeCostForAbility } from "./abilityRules";
import type { DeathKnightRuneState } from "./combatActors";
import { channelAmountEffects } from "./combatChannelAmounts";
import { spendDeathKnightRunes } from "./combatResources";
import { healingAbilityCanAidOthers } from "./healingAbilityRules";
import type { PartyRole } from "./partyRoles";
import type { CharacterStatRuleset } from "./characterStatRuleset";
import { nativeRotationAbilityAllowed, nativeRotationProfileFor } from "./romFiveRotation";
import {
currentCompanionCooldownPolicy,
type CompanionCooldownMechanicRule,
type CompanionCooldownPolicy,
} from "./companionCooldownPolicy";
export const COMBAT_ROTATION_VERSION = "playerbots-inspired-v6-reactive-ready-five";
export const COMBAT_ROTATION_VERSION = "playerbots-inspired-v8-native-rom-healer-reserve";
export interface RotationProfile {
readonly id: string;
@@ -43,6 +46,7 @@ export interface RotationContext {
readonly now: number;
readonly classId: ClassId;
readonly secondaryClassId?: ClassId | null;
readonly statRuleset?: CharacterStatRuleset;
readonly specializationId?: string | null;
readonly specialization?: string | null;
readonly role: PartyRole;
@@ -59,6 +63,10 @@ export interface RotationContext {
/** Learned Reaping rank, used only to expose its otherwise-omitted Blood Strike trigger fallback. */
readonly deathKnightReapingRank?: number;
readonly abilities: readonly AbilityDefinition[];
/** Full learned kit before affordability/cooldown filtering; resource reservations need this list. */
readonly learnedAbilities?: readonly AbilityDefinition[];
/** Current effective Mana cost after actor-specific aura/build adjustments. */
readonly manaCostForAbility?: (ability: AbilityDefinition) => number;
/** Learned actions capable of hostile damage, including unavailable ones. */
readonly knownDamagingAbilityNames?: ReadonlySet<string>;
readonly hostileTarget: RotationHostileTarget | null;
@@ -75,6 +83,12 @@ export interface RotationContext {
readonly requiredControl?: boolean;
readonly moving?: boolean;
readonly maintainedAbilityIds?: ReadonlySet<string>;
readonly maintainedPeriodicEffects?: readonly {
readonly abilityId: string;
readonly sourceActorId: string;
readonly targetId: string;
readonly remainingMs: number;
}[];
readonly activeAuraExclusiveGroups?: ReadonlySet<string>;
readonly activeSummonAbilityIds?: ReadonlySet<string>;
readonly hasActivePet?: boolean;
@@ -294,7 +308,13 @@ function isCombatMaintenanceAura(ability: AbilityDefinition): boolean {
export function abilityHasRotationSemantics(ability: AbilityDefinition): boolean {
return ability.effects.some((effect) => {
if (effect.kind === "unsupported" || effect.kind === "scripted" || effect.kind === "utility") return false;
if (effect.kind === "unsupported" || effect.kind === "scripted") return false;
// Killing Spree's player-facing effect is intentionally a utility marker;
// the cast creates a timed aura whose source script supplies the assaults.
if (effect.kind === "utility") return ability.source === "wow335-progression"
&& ability.classId === "rogue"
&& ability.dbcSpellId === 51690
&& ability.name === "Killing Spree";
if (effect.kind !== "apply-aura") return true;
return Boolean(
effect.control
@@ -369,7 +389,10 @@ function scoreAbility(
const resource = context.resources[context.primaryResource] ?? 0;
const maximumResource = context.maximumResources[context.primaryResource] ?? 0;
const resourceFraction = maximumResource > 0 ? resource / maximumResource : 1;
const maintained = (context.maintainedAbilityIds?.has(ability.id) ?? false)
const nativePeriodic = nativeRotationProfileFor(context) !== null
&& context.maintainedPeriodicEffects !== undefined
&& ability.effects.some((effect) => effect.kind === "dot" || effect.kind === "hot");
const maintained = (!nativePeriodic && (context.maintainedAbilityIds?.has(ability.id) ?? false))
|| ability.effects.some((effect) => effect.kind === "apply-aura"
&& Boolean(effect.aura.exclusiveGroup
&& context.activeAuraExclusiveGroups?.has(effect.aura.exclusiveGroup)));
@@ -402,6 +425,7 @@ function scoreAbility(
knownDamagingAbilityNames.has(name)
)));
if (!coaBarbarianDecapitateTargetAllowed(ability, context.hostileTarget)) return null;
const targetHealthGate = TARGET_HEALTH_GATES.get(ability.name);
if (targetHealthGate !== undefined && targetHealth > targetHealthGate) return null;
if ((context.hostileCount ?? 1) < minimumHostileTargetsForAbility(ability.name)) return null;
@@ -543,10 +567,18 @@ function scoreAbility(
if (maintenanceBuff && healingCategory
&& (ability.target === "friendly" || ability.target === "any") && !lowest) return null;
if (actionableHealing && !damaging) {
const healingTargetHealth = ability.target === "self" ? selfHealth : lowestHealth;
const healingTargetHealth = ability.target === "self"
&& !(nativeRotationProfileFor(context) && healingAbilityCanAidOthers(ability))
? selfHealth : lowestHealth;
if ((ability.target === "friendly" || ability.target === "any") && !lowest) return null;
if (healingTargetHealth >= 0.9) return null;
}
if (nativeRotationProfileFor(context)?.role === "healer"
&& ability.name === "Urgent Heal" && lowest && lowestHealth <= 0.3) {
return { ability, score: 1_015_500, ruleId: "native-emergency-heal",
reason: "a critically injured ally needs the fastest available direct heal",
targetId: lowest.id };
}
// Pet stance and recall buttons require an explicit positioning/order intent;
// treating them as maintenance actions otherwise starves real attacks.
if (categories.has("pet") && !petSummon && !categories.has("summon") && !damaging
@@ -680,8 +712,43 @@ function scoreAbility(
return { ability, score, ruleId, reason, targetId: targetForAbility(ability, context, lowest) };
}
function rotationManaCost(ability: AbilityDefinition, context: RotationContext): number {
if (context.manaCostForAbility) return context.manaCostForAbility(ability);
return (ability.costs?.length ? ability.costs : [ability.cost])
.filter((cost) => cost.resource === "mana")
.reduce((total, cost) => total + Math.max(0, cost.amount), 0);
}
function nativeEarlyPriestHealManaReserve(context: RotationContext): number {
if (context.statRuleset !== "rom" || context.classId !== "rom-priest"
|| context.role !== "healer" || context.level >= 10) return 0;
return (context.learnedAbilities ?? context.abilities)
.filter((ability) => !ability.passive
&& (ability.target === "friendly" || ability.target === "any")
&& healingAbilityCanAidOthers(ability)
&& ability.effects.some((effect) => effect.kind === "heal"))
.map((ability) => rotationManaCost(ability, context))
.filter((cost) => Number.isFinite(cost) && cost > 0)
.reduce((minimum, cost) => Math.min(minimum, cost), Number.POSITIVE_INFINITY);
}
/** True when spending this hostile ability would consume the early RoM Priest's learned direct-heal reserve. */
export function nativeEarlyPriestHealReserveBlocksAbility(
ability: AbilityDefinition,
context: RotationContext,
): boolean {
const reserve = nativeEarlyPriestHealManaReserve(context);
if (!Number.isFinite(reserve) || reserve <= 0 || ability.target !== "hostile"
|| !aiHintsForAbility(ability).categories.some((category) => category === "damage" || category === "execute")) {
return false;
}
const manaCost = rotationManaCost(ability, context);
if (!Number.isFinite(manaCost) || manaCost <= 0) return false;
return (context.resources.mana ?? 0) - manaCost < reserve;
}
export function evaluateCombatRotation(context: RotationContext): RotationDecision | null {
const baseRotationProfile = rotationProfileFor(
const baseRotationProfile = nativeRotationProfileFor(context) ?? rotationProfileFor(
context.classId,
context.specializationId,
context.specialization,
@@ -726,6 +793,8 @@ export function evaluateCombatRotation(context: RotationContext): RotationDecisi
return !runeCost || spendDeathKnightRunes(context.runes, runeCost, context.now) !== null;
})
.filter((ability) => context.canUseAbility?.(ability) ?? true)
.filter((ability) => !nativeEarlyPriestHealReserveBlocksAbility(ability, context))
.filter((ability) => nativeRotationAbilityAllowed(ability, context))
.map((ability) => scoreAbility(ability, context, rotationProfile, lowest))
.filter((candidate): candidate is ScoredRotationAbility => candidate !== null)
.sort((left, right) => right.score - left.score