Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user