Update Healer Man server
This commit is contained in:
1 parent
f90ba7d989
commit
189b94d6b7
51 files changed
+3448
-154
No files matched your search
+143
-18
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user