Release Healer Man 0.1.9

This commit is contained in:
phenom committed 2026-08-30 14:16:56 -04:00
1 parent c245a199b6
commit d060296ade
74 files changed
+5901 -334

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
import {
abilityAtLevel,
isAbilityUnlocked,
type AbilityDefinition,
} from "../abilityCatalog";
import { aiHintsForAbility, canAbilityCastWhileMoving, targetingRulesForAbility } from "../abilityRules";
import { PLAYER_AGGRO_ID } from "../aggro";
import type { CastFailureReason, CombatState } from "../combatStore";
import { useCombatStore } from "../combatStore";
import { getMobPosition } from "../mobRuntimeRegistry";
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 interface BenchmarkPolicyDecision {
readonly made: boolean;
readonly cast: boolean;
readonly resourceBlocked: boolean;
readonly invalidFailures: number;
}
interface FriendlyTarget {
readonly id: string;
readonly health: number;
readonly maxHealth: number;
readonly missingPercent: number;
}
interface ScoredAbility {
readonly ability: AbilityDefinition;
readonly score: number;
readonly friendlyTarget: FriendlyTarget | null;
}
const TRANSIENT_FAILURES = new Set<CastFailureReason>([
"already-casting",
"global-cooldown",
"cooldown",
"not-enough-resource",
"not-enough-runes",
"condition-not-met",
"player-controlled",
"school-locked",
]);
function healthTarget(id: string, health: number, maxHealth: number): FriendlyTarget {
return {
id,
health,
maxHealth,
missingPercent: maxHealth > 0 ? Math.max(0, 1 - health / maxHealth) : 0,
};
}
function friendlyTargets(combat: CombatState, party: PartyState): readonly FriendlyTarget[] {
return [
healthTarget(PLAYER_AGGRO_ID, combat.health, combat.maxHealth),
...party.members.map((member) => healthTarget(member.id, member.health, member.maxHealth)),
]
.filter((target) => target.health > 0)
.sort((left, right) => right.missingPercent - left.missingPercent || left.id.localeCompare(right.id));
}
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 (ranked.effects.every((effect) => effect.kind === "unsupported" || effect.kind === "scripted")) return null;
return ranked;
}
function isAreaAbility(ability: AbilityDefinition): boolean {
const targeting = targetingRulesForAbility(ability);
return targeting.shape !== "unit"
|| (targeting.maximumTargets ?? 1) > 1
|| (targeting.radius ?? ability.radius ?? 0) > 0;
}
function throughputWeight(ability: AbilityDefinition): number {
let weight = 0;
for (const effect of ability.effects) {
if (effect.kind === "damage" || effect.kind === "heal" || effect.kind === "shield" || effect.kind === "dot" || effect.kind === "hot") {
weight += Math.max(0, effect.coefficient) * 10;
weight += Math.max(0, effect.basePoints ?? 0) * 0.01;
} else if (effect.kind === "finisher-damage") {
weight += Math.max(0, effect.baseCoefficient + effect.perComboCoefficient * 3) * 10;
} else if (effect.kind === "execute") {
weight += Math.max(0, effect.coefficient) * 10;
}
}
return Math.min(40, weight);
}
function scoreAbility(
ability: AbilityDefinition,
role: PartyRole,
scenario: BenchmarkScenarioDefinition,
combat: CombatState,
targets: readonly FriendlyTarget[],
): ScoredAbility | null {
const hints = aiHintsForAbility(ability);
const categories = new Set(hints.categories);
const injured = targets.filter((target) => target.missingPercent > 0.005);
const lowest = targets[0] ?? null;
const area = isAreaAbility(ability);
let score = hints.priority ?? 0;
let friendlyTarget: FriendlyTarget | null = null;
if (role === "healer") {
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"
? targets.find((target) => target.id === PLAYER_AGGRO_ID) ?? lowest
: lowest;
score += canHeal ? 100 : 70;
score += friendlyTarget.missingPercent * 180;
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 (canShield) score += friendlyTarget.missingPercent >= 0.2 ? 45 : -20;
} else {
const hostileDamage = categories.has("damage") || categories.has("execute") || categories.has("debuff");
if (role === "damage" && !hostileDamage) return null;
if (role === "tank" && !hostileDamage && !categories.has("mitigation") && !categories.has("taunt")) return null;
if (hostileDamage) score += 100 + throughputWeight(ability);
if (categories.has("execute")) {
const target = combat.selectedTargetId ? combat.mobs[combat.selectedTargetId] : null;
const executeActive = Boolean(target && target.health / Math.max(1, target.maxHealth) <= 0.2);
score += executeActive ? 120 : -180;
}
if (scenario.hostileTargets > 1) score += area ? 60 + scenario.hostileTargets * 4 : -15;
else if (area) score -= 15;
if (ability.effects.some((effect) => effect.kind === "dot")
&& combat.effects.some((effect) => effect.sourceAbilityId === ability.id && effect.targetId === combat.selectedTargetId)) {
score -= 250;
}
if (role === "tank") {
const playerHealth = combat.health / Math.max(1, combat.maxHealth);
if (categories.has("mitigation")) score += playerHealth < 0.8 ? 150 : 20;
if (categories.has("taunt")) {
const looseMobs = Object.values(combat.mobs).filter((mob) => !mob.dead && mob.targetActorId !== PLAYER_AGGRO_ID).length;
score += looseMobs > 0 ? 220 : -80;
}
}
}
return { ability, score, friendlyTarget };
}
/** A deterministic, shared baseline policy. It is intentionally not an optimizer. */
export class BenchmarkController {
#nextDecisionAt = 0;
act(now: number, role: PartyRole, scenario: BenchmarkScenarioDefinition, moving: boolean): BenchmarkPolicyDecision {
const combat = useCombatStore.getState();
const party = usePartyStore.getState();
if (
now < this.#nextDecisionAt
|| combat.health <= 0
|| combat.activeCast
|| combat.globalCooldownEndsAt > now
|| combat.controlledUntil > now
) return { made: false, cast: false, resourceBlocked: false, invalidFailures: 0 };
const targets = friendlyTargets(combat, party);
const candidates = combat.abilities
.map((ability) => learnedAbility(ability, combat))
.filter((ability): ability is AbilityDefinition => ability !== null)
.filter((ability) => !moving || ability.castTimeMs <= 0 || canAbilityCastWhileMoving(ability))
.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));
let resourceBlocked = false;
let invalidFailures = 0;
for (const candidate of candidates) {
const { ability, friendlyTarget } = candidate;
let targetActorId: string | null | undefined;
if (ability.target === "friendly" || (ability.target === "any" && friendlyTarget)) {
if (friendlyTarget?.id === PLAYER_AGGRO_ID) {
party.selectSelf();
targetActorId = null;
} else if (friendlyTarget) {
party.selectMember(friendlyTarget.id);
targetActorId = friendlyTarget.id;
}
} else if (ability.target === "hostile" || ability.target === "any") {
party.clearSelection();
useCombatStore.getState().selectFriendlyActor(null);
targetActorId = combat.selectedTargetId;
}
const hostilePosition = combat.selectedTargetId ? getMobPosition(combat.selectedTargetId) : null;
const targeting = targetingRulesForAbility(ability);
const result = useCombatStore.getState().castAbility(ability.id, {
now,
origin: combat.playerPosition,
targetActorId,
...(targeting.shape === "ground"
? { groundPosition: ability.target === "hostile" || ability.target === "any"
? hostilePosition ?? combat.playerPosition
: combat.playerPosition }
: {}),
});
if (result.ok) {
this.#nextDecisionAt = now + 50;
return { made: true, cast: true, resourceBlocked: false, invalidFailures };
}
if (result.reason === "not-enough-resource" || result.reason === "not-enough-runes") resourceBlocked = true;
if (result.reason && !TRANSIENT_FAILURES.has(result.reason)) invalidFailures += 1;
}
this.#nextDecisionAt = now + 1_500;
return { made: true, cast: false, resourceBlocked, invalidFailures };
}
}