Update Healer Man server

This commit is contained in:
phenom committed 2026-09-23 16:14:19 -04:00
1 parent f90ba7d989
commit 189b94d6b7
51 files changed
+3448 -154

No files matched your search

+143 -18
View File
@@ -13,6 +13,7 @@ import {
} from "./abilityCatalog";
import {
aiHintsForAbility,
abilityRequiresOutOfCombat,
canAbilityCastWhileMoving,
targetingRulesForAbility,
} from "./abilityRules";
@@ -21,7 +22,7 @@ import { useCombatStore } from "./combatStore";
import { companionResourceMaximum, memberFromCompanionActor } from "./combatActorAuthority";
import type { MobVector3 } from "./mobPopulation";
import { requireDungeonDefinition } from "./dungeonRegistry";
import { getMobPosition } from "./mobRuntimeRegistry";
import { getMobPosition, getMobRuntimeTransform } from "./mobRuntimeRegistry";
import {
getPartyRuntimePosition,
partyRuntimeIsRejoining,
@@ -44,6 +45,7 @@ import {
type WotlkAttackKind,
} from "./wotlkStats";
import { useWailingEncounterStore } from "./wailingCavernsEncounter";
import { resolvedEquipmentLoadout } from "./heirloomEquipment";
import {
emitCombatPresentation,
presentationDeliveryForAbility,
@@ -79,6 +81,7 @@ import {
ROGUE_DEADLY_POISON_DOSE_TAG,
ROGUE_ENVENOM_BUFF_TAG,
} from "./roguePoison";
import { romAbilityNeedsShield, romEffectsForCast, romSelfDrainPercent, romTargetBleedCount } from "./romCombatMechanics";
const OUT_OF_COMBAT_REGEN_FRACTION = 0.06;
const ATTACK_ACQUISITION_RANGE = 30;
@@ -753,6 +756,34 @@ function partyAbilityTargetIsValid(
if (castOrigin && !canAbilityCastWhileMoving(ability) && distanceSquared(origin, castOrigin) > 0.04) return false;
const mob = combat.mobs[targetId];
const hostile = Boolean(mob);
if (abilityRequiresOutOfCombat(ability)
&& !(ability.source === "runewaker" && member.classId === "rom-rogue"
&& combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes("rom-vanish")))
&& Object.values(combat.mobs).some((candidate) => candidate.engaged && !candidate.dead)) return false;
if (ability.name === "Kill Shot" && mob && mob.health / Math.max(1, mob.maxHealth) > 0.2) return false;
if (ability.name === "Power Word: Shield" && combat.auras.some((aura) => aura.targetId === targetId
&& aura.definition.id === "wow335:priest:weakened-soul")) return false;
if (ability.name === "Enraged Regeneration" && !combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.disposition === "buff" && aura.definition.dispelCategory === "enrage"
&& !aura.definition.name.startsWith("Enraged Regeneration"))) return false;
if (ability.source === "runewaker") {
if (ability.dbcSpellId === 490151 && (member.comboTargetId !== targetId || member.comboPoints <= 0)) return false;
if (ability.dbcSpellId === 490347 && !combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes("rom-poisonous"))) return false;
if (ability.dbcSpellId === 491171 && !combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes("rom-charged"))) return false;
if (ability.dbcSpellId === 491172 && !combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes("rom-static-field-charge"))) return false;
if (ability.dbcSpellId === 490311 || romAbilityNeedsShield(ability)) {
const loadout = resolvedEquipmentLoadout(combat.equipment, combat.inventory, {
ownerId: member.id, ownerKind: "party", classId: member.classId,
specializationId: member.specializationId, role: member.role, level: member.level,
});
if (ability.dbcSpellId === 490311 && loadout["main-hand"]?.weaponType !== "dagger") return false;
if (romAbilityNeedsShield(ability) && loadout["off-hand"]?.armorType !== "shield") return false;
}
}
const targetPosition = hostile ? getMobPosition(targetId) : partyFriendlyPosition(targetId);
if (!targetPosition || mob?.dead) return false;
const targetActor = combat.actors[targetId];
@@ -1100,10 +1131,21 @@ function executePartyAbility(
castStartAlreadyEmitted = false,
): PartyAbilityExecutionResult {
const combat = useCombatStore.getState();
if (ability.name === "Enraged Regeneration" && !combat.consumeActorEnrage(member.id, now)) {
return { member, effectiveAmount: 0 };
}
const origin = getPartyRuntimePosition(member.id) ?? combat.playerPosition;
const hostileTargets = combat.mobs[targetId] ? partyHostileTargets(ability, targetId, origin, member.id) : [];
const friendlyTargets = combat.mobs[targetId] ? [] : partyHealingTargets(ability, targetId, members);
const consumesColdBlood = ability.name !== "Cold Blood" && hostileTargets.length > 0
&& ability.effects.some((effect) => ["damage", "dot", "finisher-damage"].includes(effect.kind))
&& combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes("next-offensive-critical"));
const spellSchool = presentationSchoolForAbility(ability);
const usesCombustion = spellSchool === "fire" && hostileTargets.length > 0
&& ability.effects.some((effect) => effect.kind === "damage")
&& combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes("fire-crit-stacking"));
const groundAreaCreated = targetingRulesForAbility(ability).shape === "ground"
? Boolean(useCombatStore.getState().createActorAreaEffect(
member.id,
@@ -1148,12 +1190,41 @@ function executePartyAbility(
}
let runtimeMember = member;
let effectiveAmount = 0;
for (const effect of ability.effects) {
const castEffects = romEffectsForCast(ability, {
sealStacks: member.comboTargetId === targetId ? member.comboPoints : 0,
bleedCount: romTargetBleedCount(combat.effects, member.id, targetId),
charged: combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes("rom-charged")),
mainHandDagger: member.classId === "rom-rogue" && resolvedEquipmentLoadout(combat.equipment, combat.inventory, {
ownerId: member.id, ownerKind: "party", classId: member.classId,
specializationId: member.specializationId, role: member.role, level: member.level,
})["main-hand"]?.weaponType === "dagger",
casterMaxHealth: member.maxHealth,
});
for (const effect of castEffects) {
if (ability.name === "Penance" && combat.mobs[targetId]
&& (effect.kind === "heal" || effect.kind === "hot")) continue;
if (ability.name === "Penance" && !combat.mobs[targetId]
&& (effect.kind === "damage" || effect.kind === "dot")) continue;
if (effect.kind === "damage" || effect.kind === "dot" || effect.kind === "finisher-damage") {
if (effect.kind === "dot" && romSelfDrainPercent(ability.dbcSpellId) > 0) {
combat.applyTimedActorEffect(member.id, member.id, ability, effect, now);
continue;
}
for (const hostileTargetId of hostileTargets) {
if (effect.kind === "dot") {
if (!groundAreaCreated) combat.applyTimedActorEffect(member.id, hostileTargetId, ability, effect, now);
if (!groundAreaCreated) combat.applyTimedActorEffect(
member.id, hostileTargetId, ability,
ability.name === "Rupture"
? { ...effect, ticks: Math.max(4, Math.min(8, 3 + runtimeMember.comboPoints)) }
: effect,
now,
);
} else {
const sunderStacks = effect.kind === "damage" && effect.bonusPerSunderStack
? useCombatStore.getState().auras.find((aura) => aura.targetId === hostileTargetId
&& aura.definition.id === "wow335:warrior:sunder-armor")?.stacks ?? 0
: 0;
const result = combat.damageMob(
hostileTargetId,
partyAbilityAmount(
@@ -1161,10 +1232,16 @@ function executePartyAbility(
ability,
effect,
envenomDosesByTarget.get(hostileTargetId) ?? 0,
),
) + sunderStacks * (effect.kind === "damage" ? effect.bonusPerSunderStack ?? 0 : 0),
now,
{ actorId: member.id, role: member.role, abilityId: ability.id },
spellSchool,
consumesColdBlood || usesCombustion ? {
attackKind: consumesColdBlood ? "melee" : "spell", abilityId: ability.id,
seed: `${member.id}:${ability.id}:${hostileTargetId}:${now}:special-crit`,
criticalChanceBonusPercent: consumesColdBlood ? 100 : 0,
...(ability.name === "Shiv" ? { rules: { canCrit: false } } : {}),
} : undefined,
);
effectiveAmount += result.amount;
if (
@@ -1207,6 +1284,17 @@ function executePartyAbility(
now,
});
effectiveAmount += result.effectiveAmount;
if (ability.name === "Divine Hymn") {
useCombatStore.getState().applyActorAura(member.id, friendlyTargetId, ability, {
kind: "apply-aura", recipient: "ability-target",
aura: {
id: "wow335:priest:divine-hymn-healing", name: "Divine Hymn", disposition: "buff",
durationMs: 8_000, maxStacks: 1, stackBehavior: "refresh",
modifiers: [{ kind: "healing", direction: "taken", operation: "percent", value: 0.03 }],
},
sourceEffectType: 6, sourceAuraType: 118, miscValue: 0, triggerSpellId: 0,
}, now);
}
}
} else if (effect.kind === "hot") {
if (!groundAreaCreated) for (const friendlyTargetId of healingRecipientIds(effect, member.id, targetId, combat.actors,
@@ -1254,10 +1342,11 @@ function executePartyAbility(
}
} else if (effect.kind === "status") {
for (const hostileTargetId of hostileTargets.slice(0, effect.maxTargets)) {
combat.controlMob(hostileTargetId, ability.id, effect.status, effect.durationMs, now);
combat.controlMob(hostileTargetId, ability.id, effect.status, effect.durationMs, now,
effect.breakOnDamage, member.id);
}
} else if (effect.kind === "apply-aura") {
if (ability.name === "Inner Focus") continue;
if (ability.name === "Inner Focus" || ability.name === "Divine Hymn") continue;
const recipients = effect.scope === "party"
? [
...(simulatedPartyExcludesPlayer() ? [] : [PLAYER_AGGRO_ID]),
@@ -1267,7 +1356,9 @@ function executePartyAbility(
? [member.id]
: combat.mobs[targetId] ? hostileTargets : friendlyTargets;
for (const recipientId of recipients) {
const appliedEffect = ability.name === "Envenom"
const appliedEffect = ability.name === "Rupture"
? { ...effect, aura: { ...effect.aura, durationMs: Math.max(4, Math.min(8, 3 + runtimeMember.comboPoints)) * 2_000 } }
: ability.name === "Envenom"
&& effect.aura.utilityTags?.includes(ROGUE_ENVENOM_BUFF_TAG)
? {
...effect,
@@ -1316,8 +1407,15 @@ function executePartyAbility(
}
}
} else if (effect.kind === "combo") {
if (ability.dbcSpellId === 490151) {
useCombatStore.setState((state) => ({
effects: state.effects.filter((entry) => !(entry.kind === "dot"
&& entry.tag === "rom-holy-seal" && entry.sourceActorId === member.id
&& entry.targetId === targetId)),
}));
}
const current = runtimeMember.comboTargetId === targetId ? runtimeMember.comboPoints : 0;
const comboPoints = Math.max(0, Math.min(5, current + effect.amount));
const comboPoints = Math.max(0, Math.min(member.classId === "rom-knight" ? 4 : 5, current + effect.amount));
runtimeMember = {
...runtimeMember,
comboPoints,
@@ -1345,7 +1443,21 @@ function executePartyAbility(
useCombatStore.getState().summonActor(member.id, ability, summonEffect, origin, summonTargetId, now);
} else if (effect.kind === "movement") {
const targetPosition = combat.mobs[targetId] ? getMobPosition(targetId) : partyFriendlyPosition(targetId);
if (effect.toward === "away") {
if (ability.dbcSpellId === 490344) {
const target = getMobRuntimeTransform(targetId);
if (target) updatePartyRuntimePosition(member.id,
target.position[0] - Math.sin(target.yaw) * 1.5,
target.position[1],
target.position[2] - Math.cos(target.yaw) * 1.5);
} else if (ability.dbcSpellId === 490150 && targetPosition) {
const dx = targetPosition[0] - origin[0];
const dz = targetPosition[2] - origin[2];
const length = Math.max(0.001, Math.hypot(dx, dz));
updatePartyRuntimePosition(member.id,
targetPosition[0] - dx / length * 1.5,
targetPosition[1],
targetPosition[2] - dz / length * 1.5);
} else if (effect.toward === "away") {
const danger = Object.values(combat.mobs)
.filter((mob) => mob.engaged && !mob.dead)
.sort((left, right) => (
@@ -1417,6 +1529,12 @@ function executePartyAbility(
}
}
}
if (ability.name === "Chimera Shot" && effectiveAmount > 0 && combat.mobs[targetId]) {
effectiveAmount += useCombatStore.getState().triggerChimeraShot(member.id, targetId, ability.id, now);
}
if (consumesColdBlood) useCombatStore.getState().consumeActorAuraTag(member.id, "next-offensive-critical");
if (ability.dbcSpellId === 491171) useCombatStore.getState().consumeActorAuraTag(member.id, "rom-charged");
if (ability.dbcSpellId === 491172) useCombatStore.getState().consumeActorAuraTag(member.id, "rom-static-field-charge");
emitPartyAbility(member, ability, targetId, "release", now, !castStartAlreadyEmitted);
emitPartyAbility(member, ability, targetId, "impact", now, !castStartAlreadyEmitted);
if (effectiveAmount > 0 && ability.effects.some((effect) => ["heal", "hot", "shield", "resurrection"].includes(effect.kind))) {
@@ -1602,16 +1720,23 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
nextMembers[index] = { ...member, activeCast: null, activeCastTargetId: null, status: "advancing", nextActionAt: now + 250 };
continue;
}
const interval = Math.max(1, cast.originalDurationMs / 5);
const totalTicks = castingAbility.name === "Divine Hymn" ? 4 : 5;
const interval = Math.max(1, cast.originalDurationMs / totalTicks);
let completed = cast.channelTicksCompleted ?? 0;
while (completed < 5 && cast.startedAt + (completed + 1) * interval <= now) {
const effects: AbilityEffect[] = channelAmountEffects(castingAbility, 5).map(({ effect, multiplier }) => ({
kind: effect.kind === "heal" || effect.kind === "hot" ? "heal" : "damage",
coefficient: 0, basePoints: Math.round(partyAbilityAmount(member, castingAbility, effect) * multiplier),
bonusPowerCoefficient: 0, sourceLevel: member.level,
...(effect.kind === "heal" && effect.percentMaxHealth !== undefined
? { percentMaxHealth: effect.percentMaxHealth * multiplier } : {}),
}));
while (completed < totalTicks && cast.startedAt + (completed + 1) * interval <= now) {
const effects: AbilityEffect[] = channelAmountEffects(castingAbility, totalTicks).map(({ effect, multiplier }) => {
const amount = Math.round(partyAbilityAmount(member, castingAbility, effect) * multiplier);
if (effect.kind === "heal" || effect.kind === "hot") return {
kind: "heal", coefficient: 0, basePoints: amount, sourceLevel: member.level,
...("scope" in effect ? {
scope: effect.scope, center: effect.center, radius: effect.radius,
maximumTargets: effect.maximumTargets, recipient: effect.recipient,
} : {}),
...(effect.kind === "heal" && effect.percentMaxHealth !== undefined
? { percentMaxHealth: effect.percentMaxHealth * multiplier } : {}),
};
return { kind: "damage", coefficient: 0, basePoints: amount, sourceLevel: member.level };
});
const execution = executePartyAbility(member, { ...castingAbility, effects }, castTargetId,
cast.startedAt + (completed + 1) * interval, nextMembers, 0, true);
member = execution.member;