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

+198 -20
View File
@@ -1,3 +1,5 @@
import { nativeTrainingAutoAttackProfile } from "./romNativeCombatAmount";
import { romNativePlayerHitProfile } from "./romNativeCombatResolution";
import {
abilitiesForClass,
abilitiesForCharacter,
@@ -53,6 +55,7 @@ import {
import { usePartyStore, type PartyCommand, type PartyMember } from "./partyStore";
import { baseClassFor, classById, isRomClassId } from "../app/characterCatalog";
import { useGameStore } from "./store";
import { coaBarbarianDecapitateTargetAllowed } from "./coaBarbarianExecuteRuntime";
import { useManastormStore } from "./manastormStore";
import { WOW335_GLYPH_OF_SWIFTMEND_ID } from "./wow335GlyphCatalog";
import {
@@ -73,14 +76,20 @@ import {
} from "./wow335WarriorUnbridledWrathRuntime";
import { activeDungeonCompletedObjectiveIds } from "./dungeonEncounterRuntime";
import { PLAYER_AGGRO_ID, type ThreatSource } from "./aggro";
import type { Wow335KillingSpreeWeaponSnapshot } from "./wow335RogueKillingSpreeRuntime";
import {
applyAura as applyCombatAura,
resolveDamageValue as resolveAuraDamageValue,
resolveResourceValue as resolveAuraResourceValue,
resolveStatValue as resolveAuraStatValue,
type ActiveAura,
type DispelCategory,
} from "./combatAuras";
import { romPriestHolyBlessingDamage, romPriestHolyBlessingStats } from "./romPriestHolyBlessingRuntime";
import {
ROM_PRIEST_HOLY_BLESSING_TAG,
romPriestHolyBlessingDamage,
romPriestHolyBlessingStats,
} from "./romPriestHolyBlessingRuntime";
import { romKnightGodsVengeanceShouldResetShadowStep } from "./romKnightOverrides";
import {
romChampionHasForgeRankTwoAura,
@@ -157,6 +166,11 @@ import { imminentCooldownMechanic } from "./companionCooldownPolicy";
import { normalizeActorResourcePool, type DeathKnightRuneState } from "./combatActors";
import { enemyControlPreventsActions } from "./enemyAttacks";
import { advanceCombatResourcePool } from "./combatResources";
import {
advanceRomNativeRecoveryClock,
romNativePeriodicManaEffectDuring,
romNativeRecoveryPulseAmounts,
} from "./romNativeRecovery";
import { baseMissChancePercent } from "./combatResults";
import { wow335DeathKnightSuddenDoomTriggerForLandedHit } from "./wow335DeathKnightSuddenDoomRuntime";
import {
@@ -987,6 +1001,7 @@ function partyAbilityAmountWithHolyBlessing(
effect: AbilityEffect,
poisonDoses = 0,
now = Date.now(),
nativeTargetContext?: import("./romNativeCombatAmount").RomNativeDamageTargetContext,
): number {
const state = useCombatStore.getState();
const base = deriveCharacterStats(
@@ -1005,10 +1020,14 @@ function partyAbilityAmountWithHolyBlessing(
(value, stat) => resolveAuraStatValue(value, auras, stat),
base.intellect,
);
if (member.statRuleset === "rom") {
return partyAbilityAmount(member, ability, effect, poisonDoses, intellect,
auraAdjustedCharacterStats(base, state.auras, member.id, now).romNative, nativeTargetContext);
}
if (member.classId !== "rom-priest") {
return partyAbilityAmount(member, ability, effect, poisonDoses, intellect);
}
const bonus = member.statRuleset === "rom" ? 0 : romPriestHolyBlessingStats(base, auras).spellPowerBonus;
const bonus = romPriestHolyBlessingStats(base, auras).spellPowerBonus;
if (bonus <= 0) return partyAbilityAmount(member, ability, effect, poisonDoses, intellect);
return partyAbilityAmount({
...member,
@@ -1023,8 +1042,9 @@ function partyWarlockAbilityAmount(
poisonDoses = 0,
targetId?: string,
now = Date.now(),
nativeTargetContext?: import("./romNativeCombatAmount").RomNativeDamageTargetContext,
): number {
const amount = partyAbilityAmountWithHolyBlessing(member, ability, effect, poisonDoses, now);
const amount = partyAbilityAmountWithHolyBlessing(member, ability, effect, poisonDoses, now, nativeTargetContext);
if (member.classId === "warlock" && ability.name === "Conflagrate" && effect.kind === "damage") {
const conflagrateDot = targetId
? activePartyWarlockConflagrateDot(useCombatStore.getState().effects, member.id, targetId, now)
@@ -1187,6 +1207,7 @@ export function partyAttackDamage(
& Partial<Pick<PartyMember,
"id" | "classId" | "specialization" | "gearStats" | "raceId" | "gender" | "statRuleset" | "secondaryClassId"
>>,
now = Date.now(),
): number {
const coefficient = member.role === "tank" ? 0.85 : member.role === "healer" ? 0.55 : 1.25;
const gear = member.gearStats;
@@ -1209,8 +1230,10 @@ export function partyAttackDamage(
member.secondaryClassId,
member.gender ?? "male",
"rom",
), useCombatStore.getState().auras, member.id ?? "").romNative
), useCombatStore.getState().auras, member.id ?? "", now).romNative
: undefined;
const nativeAuto = kind !== "spell" ? nativeTrainingAutoAttackProfile(nativeStats, kind === "ranged" ? "ranged" : "main-hand") : undefined;
if (nativeAuto) return Math.max(1, Math.round(nativeAuto.damage));
const weapon = kind === "ranged" ? gear.rangedWeapon : kind === "melee" ? gear.meleeWeapon : null;
const equipmentPower = nativeStats
? kind === "spell"
@@ -1745,6 +1768,13 @@ function tankPriorityTargetId(
}
function memberSwingMs(member: PartyMember, now: number): number {
const native = member.statRuleset === "rom" ? auraAdjustedCharacterStats(deriveCharacterStats(
member.classId, member.level, member.gearStats, member.raceId, {}, member.secondaryClassId, member.gender, "rom",
), useCombatStore.getState().auras, member.id, now).romNative : undefined;
const physicalAuto = !partyUsesRangedAttacks(member) || baseClassFor(member.classId) === "hunter";
const nativeAuto = physicalAuto
? nativeTrainingAutoAttackProfile(native, member.classId === "rom-scout" ? "ranged" : "main-hand")
: undefined;
const base = member.role === "tank"
? 1_800
: member.classId === "rogue"
@@ -1767,6 +1797,8 @@ function memberSwingMs(member: PartyMember, now: number): number {
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
));
const effectiveHaste = resolveAuraStatValue(hasteRating, memberAuras, hasteStat);
if (nativeAuto) return Math.max(500, Math.round(hastedDuration(nativeAuto.intervalMs, effectiveHaste)
/ raceAffixHasteMultiplier(now)));
return Math.round(hastedDuration(
base,
effectiveHaste,
@@ -1850,8 +1882,13 @@ function partyRogueOffhandAttackProfile(
const auras = useCombatStore.getState().auras.filter((aura) => (
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
));
const attackPower = partyShamanAttackPower(stats.attackPower, auras);
const nativeAuto = nativeTrainingAutoAttackProfile(auraAdjustedCharacterStats(stats, useCombatStore.getState().auras, member.id, now).romNative, "off-hand");
const meleeHastePercent = resolveAuraStatValue(stats.meleeHastePercent, auras, "melee-haste");
if (nativeAuto) return { damage: Math.max(1, Math.round(nativeAuto.damage
* (masteryActive ? 1 + ROM_ROGUE_OFF_HAND_MASTERY_DAMAGE_PERCENT / 100 : 1))),
intervalMs: Math.max(500, Math.round(hastedDuration(nativeAuto.intervalMs, meleeHastePercent)
/ raceAffixHasteMultiplier(now))) };
const attackPower = partyShamanAttackPower(stats.attackPower, auras);
const fullWeaponDamage = (weapon.damageMin + weapon.damageMax) / 2
+ attackPower / 14 * (weapon.speedMs / 1_000);
const masteryMultiplier = masteryActive
@@ -2314,7 +2351,7 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
? "rom-rogue-dual-wield-main-hand"
: forgeAutoAttackActive ? "rom-champion-forge-auto-attack" : "scent-of-blood-auto-attack"}`;
useCombatStore.getState().notifyOwnerHostileAction(member.id, targetId);
const ownerAttackDamage = partyAttackDamage(member);
const ownerAttackDamage = partyAttackDamage(member, now);
const result = useCombatStore.getState().damageMob(targetId, ownerAttackDamage, now,
{ actorId: member.id, role: member.role, abilityId }, "physical", needsAttackRoll ? {
attackKind: goForTheThroatRank > 0 ? "ranged" : "melee",
@@ -2450,6 +2487,7 @@ export function partyAbilityTargetIsValid(
&& 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 (!coaBarbarianDecapitateTargetAllowed(ability, mob)) 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
@@ -2596,6 +2634,19 @@ function partyHostileTargets(
return primaryPosition && combatHasLineOfSight(origin, primaryPosition) ? [primaryTargetId] : [];
}
function partyKillingSpreeTargetIds(primaryTargetId: string, origin: MobVector3): readonly string[] {
const combat = useCombatStore.getState();
return [...new Set([primaryTargetId, ...Object.keys(combat.mobs)])]
.filter((id) => {
const mob = combat.mobs[id];
const position = getMobPosition(id);
return Boolean(mob && mob.engaged && !mob.dead && mob.health > 0 && position
&& distanceSquared(origin, position) <= 10 ** 2
&& combatHasLineOfSight(origin, position));
})
.sort();
}
function partyHealingTargets(
ability: AbilityDefinition,
primaryTargetId: string,
@@ -3111,11 +3162,13 @@ function executePartyAbility(
&& 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 = member.classId === "shaman"
&& ability.source === "wow335-progression"
&& ["Lightning Bolt", "Chain Lightning"].includes(ability.name)
? abilityDamageSchool("shaman", ability)
: presentationSchoolForAbility(ability);
const spellSchool = ability.source === "runewaker"
? abilityDamageSchool(member.classId, ability)
: member.classId === "shaman"
&& ability.source === "wow335-progression"
&& ["Lightning Bolt", "Chain Lightning"].includes(ability.name)
? abilityDamageSchool("shaman", ability)
: presentationSchoolForAbility(ability);
const consumesShamanNaturesGuardianCrit = member.classId === "shaman"
&& (hostileTargets.length > 0 || friendlyTargets.length > 0)
&& ability.effects.some((effect) => effect.kind === "damage"
@@ -3357,7 +3410,7 @@ function executePartyAbility(
combat.applyTimedActorEffect(member.id, member.id, ability, effect, now);
continue;
}
for (const hostileTargetId of hostileTargets) {
for (const [targetOrdinal, hostileTargetId] of hostileTargets.entries()) {
if (darkEnergyStrikeGroup === "dark"
&& !romChampionDarkEnergyStrikeGroup0SurvivedTargets.has(hostileTargetId)) continue;
if (effect.kind === "dot") {
@@ -3391,6 +3444,7 @@ function executePartyAbility(
envenomDosesByTarget.get(hostileTargetId) ?? 0,
hostileTargetId,
now,
{ targetCount: hostileTargets.length, targetOrdinal },
);
const targetStatuses = useCombatStore.getState().mobs[hostileTargetId]?.statuses ?? [];
const mageTargetDamageAmount = member.classId === "mage"
@@ -3534,7 +3588,7 @@ function executePartyAbility(
&& combat.auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes(ROM_WARLOCK_PRIEST_SPREADING_PAIN_CRIT_TAG)
&& (aura.expiresAt === null || aura.expiresAt > now));
const partyUsesCombatRollContext = drumsOfWarShot || lightOfDayHit || deathKnightSuddenDoomTrigger
const partyUsesCombatRollContext = member.statRuleset === "rom" || drumsOfWarShot || lightOfDayHit || deathKnightSuddenDoomTrigger
|| deathKnightScentOfBloodMeleeHit
|| deathKnightBloodyVengeanceHit
|| deathKnightBloodGorgedHit
@@ -3559,7 +3613,8 @@ function executePartyAbility(
? member.talentRanks[WOW335_MAGE_MOLTEN_FURY_TALENT_ID] ?? 0
: 0;
const partyDamageTargetBeforeHit = useCombatStore.getState().mobs[hostileTargetId];
const damageSchool = effect.kind === "damage" && isRomChampionDarkEnergyStrike
const damageSchool = effect.kind === "damage"
&& (ability.source === "runewaker" || isRomChampionDarkEnergyStrike)
? effect.school ?? spellSchool
: spellSchool;
const partyRawDirectDamageBase = mageTargetDamageAmount
@@ -3601,6 +3656,27 @@ function executePartyAbility(
partyDamageTargetBeforeHit.health / partyDamageTargetBeforeHit.maxHealth,
)
: partyDirectDamageWithMoltenFury;
const partyDirectDamageWithRunewakerAuras = effect.kind === "damage"
&& ability.source === "runewaker"
&& !partyUsesCombatRollContext
? resolveAuraDamageValue(
partyDirectDamageWithMercilessCombat,
combat.auras.filter((aura) => aura.targetId === member.id
&& (aura.expiresAt === null || aura.expiresAt > now)
&& !aura.definition.utilityTags?.includes(ROM_PRIEST_HOLY_BLESSING_TAG)),
{
direction: "dealt",
school: damageSchool,
attackKind: effect.romNativeSource
&& romNativePlayerHitProfile(
effect.romNativeSource.skillId,
effect.romNativeSource.componentId,
)?.damageChannel === "physical"
? member.classId === "rom-scout" ? "ranged" : "melee"
: "spell",
},
)
: partyDirectDamageWithMercilessCombat;
const maelstromWeaponPvpDamageReductionPercent = partyDamageTargetBeforeHit?.characterDefender
&& damageSchool !== "physical"
? wow335ShamanMaelstromWeaponPvpDamageReductionPercent(
@@ -3611,7 +3687,7 @@ function executePartyAbility(
: 0;
const result = combat.damageMob(
hostileTargetId,
Math.round(partyDirectDamageWithMercilessCombat
Math.round(partyDirectDamageWithRunewakerAuras
* (1 - maelstromWeaponPvpDamageReductionPercent / 100)),
now,
{
@@ -3621,7 +3697,10 @@ function executePartyAbility(
},
damageSchool,
partyUsesCombatRollContext ? {
attackKind: drumsOfWarShot ? "ranged"
attackKind: member.statRuleset === "rom" && effect.kind === "damage" && effect.romNativeSource
? romNativePlayerHitProfile(effect.romNativeSource.skillId, effect.romNativeSource.componentId)?.damageChannel === "physical"
? member.classId === "rom-scout" ? "ranged" : "melee" : "spell"
: drumsOfWarShot ? "ranged"
: hunterGoForTheThroatAttackKind
?? (deathKnightScentOfBloodMeleeHit || deathKnightBloodGorgedHit
|| shamanWeaponMeleeHit || godsSwordDirectPhysicalHit || darkEnergyStrikePhysicalHit
@@ -3651,7 +3730,9 @@ function executePartyAbility(
canParry: false,
canBlock: false,
} }
: ability.name === "Shiv" ? { rules: { canCrit: false } } : {}),
: { rules: { ...(ability.name === "Shiv" ? { canCrit: false } : {}),
romNativeSource: effect.kind === "damage" ? effect.romNativeSource : undefined,
romNativeTargetOrdinal: targetOrdinal } }),
} : undefined,
);
if (darkEnergyStrikeGroup === "physical" && hostileTargetId === targetId
@@ -3985,6 +4066,36 @@ function executePartyAbility(
if (effect.kind === "finisher-damage") {
runtimeMember = { ...runtimeMember, comboPoints: 0, comboTargetId: null };
}
} else if (effect.kind === "utility"
&& effect.action === "class-script"
&& ability.name === "Killing Spree"
&& baseClassFor(member.classId) === "rogue") {
const targetIds = partyKillingSpreeTargetIds(targetId, origin);
const loadout = resolvedEquipmentLoadout(combat.equipment, combat.inventory, {
ownerId: member.id,
ownerKind: "party",
classId: member.classId,
secondaryClassId: member.secondaryClassId,
specializationId: member.specializationId,
role: member.role,
level: member.level,
});
const hands: Wow335KillingSpreeWeaponSnapshot[] = (["main-hand", "off-hand"] as const)
.flatMap((hand) => {
const item = loadout[hand];
const weapon = item?.stats.meleeWeapon ?? (hand === "main-hand" ? member.gearStats.meleeWeapon : null);
return weapon ? [{
hand,
...(item?.weaponType ? { weaponType: item.weaponType } : {}),
damageMin: weapon.damageMin,
damageMax: weapon.damageMax,
speedMs: weapon.speedMs,
}] : [];
});
if (targetIds.length > 0) {
useCombatStore.getState().beginKillingSpree(member.id, ability, targetIds, hands, member.role, now);
combat = useCombatStore.getState();
}
} else if (effect.kind === "utility"
&& effect.action === "class-script"
&& ability.name === "Pestilence"
@@ -4102,6 +4213,7 @@ function executePartyAbility(
* (riptideHot ? PARTY_SHAMAN_RIPTIDE_CHAIN_HEAL_MULTIPLIER : 1));
const result = useCombatStore.getState().healActor(friendlyTargetId, requested, {
sourceActorId: member.id,
romNativeSource: effect.romNativeSource,
abilityId: ability.id,
school: healingSchool,
criticalChance,
@@ -4868,10 +4980,65 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
partyOrigin,
);
if (!targetId && party.command !== "stop") targetId = activeRaceOrbId();
const nativePartyInCombat = Boolean(targetId) || Object.values(combat.mobs).some((mob) => (
!mob.dead && (mob.engaged || Object.values(mob.threatByActor).some((threat) => threat > 0))
));
members = members.map((member) => {
const profile = resourceProfileForClass(member.classId);
const memberAuras = combat.auras.filter((aura) => aura.targetId === member.id
&& (aura.expiresAt === null || aura.expiresAt > now));
const nativeRecoveryBaseStats = member.statRuleset === "rom" && isRomClassId(member.classId)
? deriveCharacterStats(
member.classId, member.level, member.gearStats, member.raceId, {},
member.secondaryClassId, member.gender, "rom",
)
: undefined;
const nativeRecoveryBase = nativeRecoveryBaseStats?.romNative;
const living = member.health > 0;
const recoveryClockAdvance = living
? advanceRomNativeRecoveryClock(
member.romNativeRecoveryClock,
member.statRuleset ?? "wow",
"party-npc",
deltaSeconds * 1_000,
)
: { clock: undefined, pulseOffsetsMs: [] as readonly number[] };
let nativeHealthAfterRecovery = member.health;
let nativeManaPulseAmount = 0;
if (living && nativeRecoveryBaseStats && nativeRecoveryBase) {
const intervalStartAt = now - deltaSeconds * 1_000;
for (const offsetMs of recoveryClockAdvance.pulseOffsetsMs) {
const pulseAt = intervalStartAt + offsetMs;
const pulseStats = auraAdjustedCharacterStats(
nativeRecoveryBaseStats,
combat.auras.filter((aura) => aura.expiresAt === null || aura.expiresAt > pulseAt),
member.id,
pulseAt,
).romNative;
if (!pulseStats) continue;
const amount = romNativeRecoveryPulseAmounts(pulseStats, "party-npc", nativePartyInCombat);
nativeHealthAfterRecovery += amount.health;
nativeManaPulseAmount += amount.mana;
}
}
nativeHealthAfterRecovery = Math.min(member.maxHealth, nativeHealthAfterRecovery);
const nativeManaMaximum = companionResourceMaximum(member, "mana");
const nativeManaPoolEnabled = Boolean(nativeRecoveryBase?.usesMana && nativeManaMaximum > 0);
const nativeManaPeriodicAmount = nativeManaPoolEnabled && living
? romNativePeriodicManaEffectDuring(
combat.auras.filter((aura) => aura.targetId === member.id),
now - deltaSeconds * 1_000,
now,
nativeManaMaximum,
)
: 0;
const nativeManaAfterRecovery = nativeManaPoolEnabled
? Math.max(0, Math.min(
nativeManaMaximum,
(member.resourcePools.mana ?? (member.resourceType === "mana" ? member.resource : 0))
+ (living ? nativeManaPulseAmount + nativeManaPeriodicAmount : 0),
))
: undefined;
const passiveRates: Partial<Record<ResourceType, number>> = {
mana: 4,
rage: 0,
@@ -4880,7 +5047,9 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
"runic-power": 0,
};
const resourcePools: Partial<Record<ResourceType, number>> = { ...member.resourcePools };
if (nativeManaAfterRecovery !== undefined) resourcePools.mana = nativeManaAfterRecovery;
for (const type of Object.keys(resourcePools) as ResourceType[]) {
if (type === "mana" && nativeManaAfterRecovery !== undefined) continue;
const manaRegeneration = member.gearStats.mp5 / 5 + member.gearStats.spirit * 0.02;
const baseRegeneration = type === member.resourceType
? (profile.type === "rage" || profile.type === "runic-power" ? 0 : profile.regenerationPerSecond + manaRegeneration)
@@ -4923,8 +5092,10 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
}
return {
...member,
health: nativeHealthAfterRecovery,
resource: resourcePools[member.resourceType] ?? member.resource,
resourcePools,
romNativeRecoveryClock: living ? recoveryClockAdvance.clock : undefined,
warriorAngerManagementRageRemainderMs,
cooldowns: Object.fromEntries(Object.entries(member.cooldowns).filter(([, readyAt]) => readyAt > now)),
schoolLockedUntil: Object.fromEntries(Object.entries(member.schoolLockedUntil).filter(([, readyAt]) => (readyAt ?? 0) > now)),
@@ -4939,6 +5110,7 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
members = members.map((member) => {
if (
member.health <= 0
|| member.statRuleset === "rom"
|| (
member.controlledUntil > now
&& enemyControlPreventsActions(member.controlMechanic)
@@ -5569,9 +5741,15 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
members = members.map((member) => {
const actor = actorsAfterActions[member.id];
const baseline = authoritativeMembers.find((candidate) => candidate.id === member.id);
return actor?.kind === "companion" && baseline && actor.health !== baseline.health
? { ...member, health: actor.health, maxHealth: actor.maxHealth }
: member;
const health = actor?.kind === "companion" && baseline && actor.health !== baseline.health
? actor.health
: member.health;
return {
...(actor?.kind === "companion" && baseline && actor.health !== baseline.health
? { ...member, health: actor.health, maxHealth: actor.maxHealth }
: member),
romNativeRecoveryClock: health <= 0 ? undefined : member.romNativeRecoveryClock,
};
});
const targetMapChanged = (