Release Healer Man 0.1.41

This commit is contained in:
phenom committed 2026-09-13 15:09:35 -04:00
1 parent 197a64117c
commit c3f8a69b51
42 files changed
+886 -127

No files matched your search

+54 -5
View File
@@ -6,14 +6,16 @@ import {
import { aiHintsForAbility, canAbilityCastWhileMoving, targetingRulesForAbility } from "../abilityRules";
import { PLAYER_AGGRO_ID } from "../aggro";
import type { CastFailureReason, CombatState } from "../combatStore";
import { useCombatStore } from "../combatStore";
import { equipmentAdjustedCombatAmount, resolvedAbilityCosts, useCombatStore } from "../combatStore";
import { getMobPosition } from "../mobRuntimeRegistry";
import { getPartyRuntimePosition } from "../partyRuntimeRegistry";
import { healingAbilityCanAidOthers, healingAbilityHasPendingHot, healingAbilityIsArea } from "../healingAbilityRules";
import type { PartyState } from "../partyStore";
import { usePartyStore } from "../partyStore";
import type { PartyRole } from "../partyRoles";
import type { BenchmarkScenarioDefinition } from "./benchmarkTypes";
export const BENCHMARK_POLICY_ID = "baseline-v1";
export const BENCHMARK_POLICY_ID = "baseline-v3";
export interface BenchmarkPolicyDecision {
readonly made: boolean;
@@ -116,14 +118,35 @@ function scoreAbility(
const canHeal = categories.has("heal") || categories.has("emergency-heal") || categories.has("area-heal");
const canShield = categories.has("mitigation") && ability.effects.some((effect) => effect.kind === "shield");
if ((!canHeal && !canShield) || !lowest || injured.length === 0) return null;
friendlyTarget = ability.target === "self"
friendlyTarget = !healingAbilityCanAidOthers(ability)
? targets.find((target) => target.id === PLAYER_AGGRO_ID) ?? lowest
: lowest;
if (friendlyTarget.missingPercent <= 0.005) return null;
const pendingHot = healingAbilityHasPendingHot(ability, PLAYER_AGGRO_ID, friendlyTarget.id, combat.effects);
const directHeal = ability.effects.some(effect => effect.kind === "heal");
if (pendingHot && !directHeal) return null;
score += canHeal ? 100 : 70;
score += friendlyTarget.missingPercent * 180;
const healingAmount = ability.effects.reduce((total, effect) => total + (effect.kind === "heal" || effect.kind === "hot"
? (equipmentAdjustedCombatAmount(effect, combat.level, combat.gearStats, combat.classId, ability)
+ (effect.kind === "heal" ? friendlyTarget!.maxHealth * (effect.percentMaxHealth ?? 0) : 0))
* (effect.kind === "hot" ? Math.min(3, effect.ticks) : 1) : 0), 0);
score += Math.min(70, healingAmount / Math.max(1, friendlyTarget.maxHealth) * 100);
if (ability.effects.some(effect => effect.kind === "heal" && (effect.percentMaxHealth ?? 0) >= 0.5)) {
score += friendlyTarget.missingPercent >= 0.4 ? 100 : -100;
}
const healthCost = resolvedAbilityCosts(ability, combat.talentModifiers, combat).find(cost => cost.resource === "health")?.amount ?? 0;
if (healthCost > 0) {
if (combat.health / combat.maxHealth < 0.5) return null;
if (friendlyTarget.id === PLAYER_AGGRO_ID && healingAbilityCanAidOthers(ability)) return null;
score -= healthCost / combat.maxHealth * 500;
}
if (pendingHot && friendlyTarget.missingPercent < 0.5) score -= 120;
if (categories.has("emergency-heal")) score += friendlyTarget.missingPercent >= 0.5 ? 100 : -15;
if (categories.has("area-heal") || area) score += injured.length >= 2 ? 35 * injured.length : -35;
if (healingAbilityIsArea(ability)) score += injured.length >= 2 ? 35 * injured.length : -35;
if (!directHeal && friendlyTarget.missingPercent >= 0.5) score -= 60;
if (canShield) score += friendlyTarget.missingPercent >= 0.2 ? 45 : -20;
if (canShield && (combat.actors[friendlyTarget.id]?.absorb ?? 0) > 0) return null;
} else {
const hostileDamage = categories.has("damage") || categories.has("execute") || categories.has("debuff");
if (role === "damage" && !hostileDamage) return null;
@@ -176,6 +199,31 @@ export class BenchmarkController {
.filter((candidate): candidate is ScoredAbility => candidate !== null)
.sort((left, right) => right.score - left.score || left.ability.id.localeCompare(right.ability.id));
// Rage healers must begin weapon attacks to fund their heals before allies are injured.
if (role === "healer" && combat.selectedTargetId
&& combat.abilities.some(ability => ability.effects.some(effect => effect.kind === "heal") && ability.cost.resource === "rage")) {
const starter = combat.abilities.map(ability => learnedAbility(ability, combat)).find(ability => ability
&& ability.target === "hostile" && ability.range.max <= 5
&& ability.effects.some(effect => effect.kind === "damage")
&& resolvedAbilityCosts(ability, combat.talentModifiers, combat).every(cost => cost.amount === 0));
if (starter && !combat.autoAttackTargetId) candidates.push({ ability: starter, score: 0, friendlyTarget: null });
if (combat.resource < 35) {
const generators = combat.abilities.map(ability => learnedAbility(ability, combat)).filter(ability => ability
&& ability.target === "hostile" && ability.effects.some(effect => (effect.kind === "resource"
&& (!effect.resource || effect.resource === "rage") && effect.amount > 0)
|| ((effect.kind === "damage" || effect.kind === "dot")
&& effect.onDamageResource?.resource === "rage" && effect.onDamageResource.amount > 0))
&& resolvedAbilityCosts(ability, combat.talentModifiers, combat).every(cost => cost.resource !== "rage"
&& (cost.resource !== "health" || (combat.health / combat.maxHealth > 0.65 && cost.amount / combat.maxHealth <= 0.1))));
for (const generator of generators) {
if (!generator) continue;
const needsRageHeal = targets.some(target => target.id !== PLAYER_AGGRO_ID && target.missingPercent > 0.1);
candidates.push({ ability: generator, score: needsRageHeal && combat.resource < 20 ? 400 : -10, friendlyTarget: null });
candidates.sort((left, right) => right.score - left.score || left.ability.id.localeCompare(right.ability.id));
}
}
}
let resourceBlocked = false;
let invalidFailures = 0;
for (const candidate of candidates) {
@@ -203,7 +251,8 @@ export class BenchmarkController {
...(targeting.shape === "ground"
? { groundPosition: ability.target === "hostile" || ability.target === "any"
? hostilePosition ?? combat.playerPosition
: combat.playerPosition }
: ability.target === "self" || friendlyTarget?.id === PLAYER_AGGRO_ID ? combat.playerPosition
: getPartyRuntimePosition(friendlyTarget?.id ?? "") ?? combat.playerPosition }
: {}),
});
if (result.ok) {