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