Update Healer Man server
This commit is contained in:
1 parent
1fedb9368d
commit
cc3b2fa4d5
27 files changed
+1988
-83
No files matched your search
+483
-21
@@ -64,6 +64,7 @@ import {
|
||||
advanceCombatResourcePool,
|
||||
spendDeathKnightRunes,
|
||||
} from "./combatResources";
|
||||
import { cooldownResetCandidates } from "./cooldownReset";
|
||||
import { resolveHealing, type HealingResolution } from "./combatHealing";
|
||||
import {
|
||||
awardExperience,
|
||||
@@ -172,6 +173,19 @@ import {
|
||||
type WotlkAttackKind,
|
||||
} from "./wotlkStats";
|
||||
import { healingRecipientIds } from "./healingAbilityRules";
|
||||
import {
|
||||
ROGUE_DEADLY_POISON_AURA,
|
||||
ROGUE_DEADLY_POISON_COATING_TAG,
|
||||
ROGUE_DEADLY_POISON_DOSE_TAG,
|
||||
ROGUE_ENVENOM_BUFF_TAG,
|
||||
ROGUE_INSTANT_POISON_TAG,
|
||||
hasActiveAuraTag,
|
||||
matchingAuraStacks,
|
||||
rogueDeadlyPoisonTickDamage,
|
||||
rogueEnvenomPreservesDeadlyPoison,
|
||||
rogueInstantPoisonDamage,
|
||||
roguePoisonApplicationChances,
|
||||
} from "./roguePoison";
|
||||
import {
|
||||
combatAttackerFromCharacterStats,
|
||||
combatDefenderFromCharacterStats,
|
||||
@@ -583,6 +597,13 @@ export interface DamageResult {
|
||||
readonly combatResult?: CombatResult;
|
||||
}
|
||||
|
||||
export interface RoguePoisonProcResult {
|
||||
readonly instantProcs: number;
|
||||
readonly deadlyApplications: number;
|
||||
readonly deadlyStacks: number;
|
||||
readonly damage: number;
|
||||
}
|
||||
|
||||
interface PlayerDamageRollContext {
|
||||
readonly attackKind: WotlkAttackKind;
|
||||
readonly seed: string;
|
||||
@@ -731,7 +752,26 @@ export interface CombatState {
|
||||
school?: DamageSchool,
|
||||
rollContext?: PlayerDamageRollContext,
|
||||
) => DamageResult;
|
||||
triggerRogueWeaponPoisons: (
|
||||
sourceActorId: AggroActorId,
|
||||
targetId: string,
|
||||
sourceAbilityId: string,
|
||||
hitCount?: number,
|
||||
now?: number,
|
||||
) => RoguePoisonProcResult;
|
||||
consumeRogueDeadlyPoison: (
|
||||
sourceActorId: AggroActorId,
|
||||
targetId: string,
|
||||
maximumDoses: number,
|
||||
now?: number,
|
||||
) => number;
|
||||
addHealingThreat: (source: ThreatSource, effectiveHealing: number) => void;
|
||||
applyActorThreatEffect: (
|
||||
actorId: AggroActorId,
|
||||
effect: Extract<AbilityEffect, { readonly kind: "threat" }>,
|
||||
redirectTargetActorId?: AggroActorId | null,
|
||||
now?: number,
|
||||
) => void;
|
||||
tauntMob: (id: string, source: ThreatSource, durationMs: number, now?: number) => boolean;
|
||||
interruptMob: (id: string, sourceAbilityId: string, lockoutMs: number, now?: number) => boolean;
|
||||
controlMob: (id: string, sourceAbilityId: string, status: MobStatusKind, durationMs: number, now?: number) => boolean;
|
||||
@@ -1219,6 +1259,7 @@ function abilityAttackKind(classId: ClassId, ability: AbilityDefinition): WotlkA
|
||||
}
|
||||
|
||||
export function abilityDamageSchool(classId: ClassId, ability: AbilityDefinition): DamageSchool {
|
||||
if (["Envenom", "Instant Poison", "Deadly Poison"].includes(ability.name)) return "nature";
|
||||
if (abilityAttackKind(classId, ability) !== "spell") return "physical";
|
||||
const searchable = `${ability.id} ${ability.name} ${ability.description}`.toLowerCase();
|
||||
if (/\b(?:fire|flame|immolate|incinerate|searing|lava)\b/.test(searchable)) return "fire";
|
||||
@@ -2797,6 +2838,23 @@ function resolveAndDamageMobInWork(
|
||||
attackKind,
|
||||
}, now);
|
||||
}
|
||||
if (
|
||||
combatResult.landed
|
||||
&& result.amount > 0
|
||||
&& attackKind === "melee"
|
||||
&& !periodic
|
||||
&& !["Envenom", "Instant Poison", "Deadly Poison"].includes(ability?.name ?? "")
|
||||
) {
|
||||
const hitCount = ability?.name === "Mutilate" || (rules.isAutoAttack && rules.isDualWielding) ? 2 : 1;
|
||||
applyRogueWeaponPoisonsInWork(
|
||||
work,
|
||||
source.actorId,
|
||||
id,
|
||||
ability?.id ?? source.abilityId ?? "melee-hit",
|
||||
hitCount,
|
||||
now,
|
||||
);
|
||||
}
|
||||
if (result.killed) {
|
||||
evaluateProcsInWork(work, {
|
||||
trigger: "kill",
|
||||
@@ -3119,9 +3177,7 @@ function executeTriggeredSpellInWork(
|
||||
};
|
||||
}
|
||||
} else if (effect.kind === "cooldown-reset") {
|
||||
const candidates = effect.abilityName
|
||||
? work.abilities.filter((candidate) => candidate.name.toLowerCase() === effect.abilityName!.toLowerCase())
|
||||
: work.abilities;
|
||||
const candidates = cooldownResetCandidates(work.abilities, ability.id, effect.abilityName);
|
||||
for (const candidate of candidates) {
|
||||
if (effect.mode === "reset") work.cooldowns[candidate.id] = 0;
|
||||
else {
|
||||
@@ -3223,6 +3279,206 @@ function timedEffect(
|
||||
};
|
||||
}
|
||||
|
||||
interface RoguePoisonSourceStats {
|
||||
readonly classId: ClassId;
|
||||
readonly level: number;
|
||||
readonly attackPower: number;
|
||||
readonly spellPenetration: number;
|
||||
}
|
||||
|
||||
function roguePoisonSourceStats(
|
||||
work: MutableCastWork,
|
||||
sourceActorId: AggroActorId,
|
||||
): RoguePoisonSourceStats | null {
|
||||
if (sourceActorId === PLAYER_AGGRO_ID) {
|
||||
if (baseClassFor(work.classId) !== "rogue") return null;
|
||||
const stats = playerCharacterStats(
|
||||
work.classId,
|
||||
work.progression.level,
|
||||
work.gearStats,
|
||||
work.raceId,
|
||||
work.secondaryClassId ?? null,
|
||||
work.talentModifiers,
|
||||
);
|
||||
return {
|
||||
classId: work.classId,
|
||||
level: work.progression.level,
|
||||
attackPower: stats.attackPower,
|
||||
spellPenetration: work.gearStats.spellPenetration,
|
||||
};
|
||||
}
|
||||
const member = usePartyStore.getState().members.find((candidate) => candidate.id === sourceActorId);
|
||||
if (!member || baseClassFor(member.classId) !== "rogue") return null;
|
||||
const stats = deriveCharacterStats(member.classId, member.level, member.gearStats, member.raceId);
|
||||
return {
|
||||
classId: member.classId,
|
||||
level: member.level,
|
||||
attackPower: stats.attackPower,
|
||||
spellPenetration: member.gearStats.spellPenetration,
|
||||
};
|
||||
}
|
||||
|
||||
function replaceDeadlyPoisonEffect(
|
||||
work: MutableCastWork,
|
||||
sourceActorId: AggroActorId,
|
||||
targetId: string,
|
||||
stacks: number,
|
||||
now: number,
|
||||
refreshDuration: boolean,
|
||||
): void {
|
||||
const ability = abilityById("rogue-deadly-poison");
|
||||
const source = roguePoisonSourceStats(work, sourceActorId);
|
||||
const existing = work.effects.find((effect) => (
|
||||
effect.kind === "dot"
|
||||
&& effect.sourceActorId === sourceActorId
|
||||
&& effect.targetId === targetId
|
||||
&& effect.sourceAbilityId === "rogue-deadly-poison"
|
||||
));
|
||||
work.effects = work.effects.filter((effect) => effect !== existing);
|
||||
if (!ability || !source || stacks <= 0) return;
|
||||
const amount = rogueDeadlyPoisonTickDamage(source.level, source.attackPower, stacks);
|
||||
if (!refreshDuration && existing) {
|
||||
work.effects.push({ ...existing, fixedAmount: amount });
|
||||
return;
|
||||
}
|
||||
work.effects.push({
|
||||
...timedEffect(
|
||||
"dot",
|
||||
ability.id,
|
||||
sourceActorId,
|
||||
targetId,
|
||||
{ coefficient: 0 },
|
||||
4,
|
||||
3_000,
|
||||
now,
|
||||
ROGUE_DEADLY_POISON_DOSE_TAG,
|
||||
"nature",
|
||||
source.spellPenetration,
|
||||
),
|
||||
fixedAmount: amount,
|
||||
});
|
||||
}
|
||||
|
||||
function applyRogueWeaponPoisonsInWork(
|
||||
work: MutableCastWork,
|
||||
sourceActorId: AggroActorId,
|
||||
targetId: string,
|
||||
sourceAbilityId: string,
|
||||
hitCount: number,
|
||||
now: number,
|
||||
): RoguePoisonProcResult {
|
||||
const source = roguePoisonSourceStats(work, sourceActorId);
|
||||
const target = work.mobs[targetId];
|
||||
if (!source || !target || target.dead) {
|
||||
return { instantProcs: 0, deadlyApplications: 0, deadlyStacks: 0, damage: 0 };
|
||||
}
|
||||
const instantCoating = hasActiveAuraTag(work.auras, ROGUE_INSTANT_POISON_TAG, sourceActorId, now);
|
||||
const deadlyCoating = hasActiveAuraTag(work.auras, ROGUE_DEADLY_POISON_COATING_TAG, sourceActorId, now);
|
||||
const envenomBuff = hasActiveAuraTag(work.auras, ROGUE_ENVENOM_BUFF_TAG, sourceActorId, now);
|
||||
const isShiv = abilityById(sourceAbilityId)?.name === "Shiv";
|
||||
const chances = roguePoisonApplicationChances(envenomBuff);
|
||||
let instantProcs = 0;
|
||||
let deadlyApplications = 0;
|
||||
let damage = 0;
|
||||
|
||||
const applyInstant = (): void => {
|
||||
if (!instantCoating || work.mobs[targetId]?.dead) return;
|
||||
const result = damageMobInWork(
|
||||
work.classId,
|
||||
work,
|
||||
targetId,
|
||||
rogueInstantPoisonDamage(source.level, source.attackPower),
|
||||
now,
|
||||
threatSourceForActor(work, sourceActorId, "rogue-instant-poison", true),
|
||||
"nature",
|
||||
source.spellPenetration,
|
||||
);
|
||||
if (result.amount <= 0) return;
|
||||
instantProcs += 1;
|
||||
damage += result.amount;
|
||||
};
|
||||
|
||||
for (let hitIndex = 0; hitIndex < Math.max(1, Math.floor(hitCount)); hitIndex += 1) {
|
||||
if (work.mobs[targetId]?.dead) break;
|
||||
if (!isShiv && instantCoating && stableCombatUnit(
|
||||
`${sourceActorId}:${targetId}:${sourceAbilityId}:${now}:${hitIndex}:instant`,
|
||||
) < chances.instant) {
|
||||
applyInstant();
|
||||
}
|
||||
if (!deadlyCoating || (!isShiv && stableCombatUnit(
|
||||
`${sourceActorId}:${targetId}:${sourceAbilityId}:${now}:${hitIndex}:deadly`,
|
||||
) >= chances.deadly)) continue;
|
||||
|
||||
const previousStacks = matchingAuraStacks(
|
||||
work.auras,
|
||||
ROGUE_DEADLY_POISON_DOSE_TAG,
|
||||
sourceActorId,
|
||||
targetId,
|
||||
now,
|
||||
);
|
||||
const application = applyCombatAura(work.auras, {
|
||||
definition: ROGUE_DEADLY_POISON_AURA,
|
||||
sourceId: sourceActorId,
|
||||
targetId,
|
||||
now,
|
||||
});
|
||||
work.auras = [...application.auras];
|
||||
deadlyApplications += 1;
|
||||
recordCombatEventInWork(work, {
|
||||
occurredAt: now,
|
||||
kind: "aura",
|
||||
sourceActorId,
|
||||
targetActorId: targetId,
|
||||
abilityId: "rogue-deadly-poison",
|
||||
school: "nature",
|
||||
rawAmount: 0,
|
||||
effectiveAmount: application.aura.stacks,
|
||||
overheal: 0,
|
||||
absorbed: 0,
|
||||
critical: false,
|
||||
});
|
||||
replaceDeadlyPoisonEffect(work, sourceActorId, targetId, application.aura.stacks, now, true);
|
||||
// At five doses, another Deadly Poison application triggers the poison on
|
||||
// the other weapon instead of creating a sixth dose.
|
||||
if (previousStacks >= 5) applyInstant();
|
||||
}
|
||||
return {
|
||||
instantProcs,
|
||||
deadlyApplications,
|
||||
deadlyStacks: matchingAuraStacks(
|
||||
work.auras,
|
||||
ROGUE_DEADLY_POISON_DOSE_TAG,
|
||||
sourceActorId,
|
||||
targetId,
|
||||
now,
|
||||
),
|
||||
damage,
|
||||
};
|
||||
}
|
||||
|
||||
function consumeRogueDeadlyPoisonInWork(
|
||||
work: MutableCastWork,
|
||||
sourceActorId: AggroActorId,
|
||||
targetId: string,
|
||||
maximumDoses: number,
|
||||
now: number,
|
||||
): number {
|
||||
const index = work.auras.findIndex((aura) => (
|
||||
aura.sourceId === sourceActorId
|
||||
&& aura.targetId === targetId
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now)
|
||||
&& aura.definition.utilityTags?.includes(ROGUE_DEADLY_POISON_DOSE_TAG)
|
||||
));
|
||||
if (index < 0) return 0;
|
||||
const aura = work.auras[index];
|
||||
const consumed = Math.min(aura.stacks, Math.max(0, Math.floor(maximumDoses)));
|
||||
const remaining = aura.stacks - consumed;
|
||||
if (remaining <= 0) work.auras.splice(index, 1);
|
||||
else work.auras[index] = { ...aura, stacks: remaining };
|
||||
replaceDeadlyPoisonEffect(work, sourceActorId, targetId, remaining, now, false);
|
||||
return consumed;
|
||||
}
|
||||
|
||||
function entityAbsorbAmount(auras: readonly ActiveAura[], entityId: string): number {
|
||||
return aurasForEntity(auras, entityId).reduce((total, aura) => (
|
||||
total + aura.absorbs.reduce((sum, absorb) => sum + Math.max(0, absorb.remaining), 0)
|
||||
@@ -4800,20 +5056,57 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
grantDamageResourceInWork(work, effect, result.amount, castThreatSource.actorId, ability.id, now);
|
||||
}
|
||||
} else if (effect.kind === "finisher-damage") {
|
||||
const coefficient = effect.baseCoefficient + effect.perComboCoefficient * work.comboPoints;
|
||||
const amount = talentAdjustedAmount(
|
||||
equipmentAdjustedCombatAmount(
|
||||
coefficient,
|
||||
work.progression.level,
|
||||
work.gearStats,
|
||||
state.classId,
|
||||
ability,
|
||||
),
|
||||
modifiers,
|
||||
"damage-percent",
|
||||
ability,
|
||||
);
|
||||
const comboPoints = work.comboPoints;
|
||||
for (const id of affectedMobIds) {
|
||||
const envenomDoses = effect.poison
|
||||
? Math.min(
|
||||
comboPoints,
|
||||
effect.poison.maximumDoses,
|
||||
matchingAuraStacks(
|
||||
work.auras,
|
||||
effect.poison.doseAuraTag,
|
||||
castThreatSource.actorId,
|
||||
id,
|
||||
now,
|
||||
),
|
||||
)
|
||||
: 0;
|
||||
if (
|
||||
effect.poison
|
||||
&& envenomDoses > 0
|
||||
&& !rogueEnvenomPreservesDeadlyPoison(
|
||||
work.talentRanks,
|
||||
`${castThreatSource.actorId}:${id}:${ability.id}:${now}:master-poisoner`,
|
||||
)
|
||||
) {
|
||||
consumeRogueDeadlyPoisonInWork(
|
||||
work,
|
||||
castThreatSource.actorId,
|
||||
id,
|
||||
envenomDoses,
|
||||
now,
|
||||
);
|
||||
}
|
||||
const scaling = effect.poison
|
||||
? {
|
||||
coefficient: 0,
|
||||
basePoints: effect.poison.basePointsPerDose * envenomDoses,
|
||||
dieSides: 1,
|
||||
attackPowerCoefficient: effect.poison.attackPowerCoefficientPerDose * envenomDoses,
|
||||
}
|
||||
: effect.baseCoefficient + effect.perComboCoefficient * comboPoints;
|
||||
const amount = talentAdjustedAmount(
|
||||
equipmentAdjustedCombatAmount(
|
||||
scaling,
|
||||
work.progression.level,
|
||||
work.gearStats,
|
||||
state.classId,
|
||||
ability,
|
||||
),
|
||||
modifiers,
|
||||
"damage-percent",
|
||||
ability,
|
||||
);
|
||||
resolveAndDamageMobInWork(
|
||||
state.classId,
|
||||
work,
|
||||
@@ -5267,12 +5560,18 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
}, ability.id, now);
|
||||
}
|
||||
} else if (effect.kind === "apply-aura") {
|
||||
const sourceDurationMs = holyAuraFriendly && ability.name === "Holy Aura" && effect.aura.durationMs !== null
|
||||
const sourceDurationMs = ability.name === "Envenom"
|
||||
&& effect.aura.utilityTags?.includes(ROGUE_ENVENOM_BUFF_TAG)
|
||||
? (work.comboPoints + 1) * 1_000
|
||||
: holyAuraFriendly && ability.name === "Holy Aura" && effect.aura.durationMs !== null
|
||||
? Math.max(1_000, effect.aura.durationMs - 2_000)
|
||||
: effect.aura.durationMs;
|
||||
const durationMs = sourceDurationMs === null
|
||||
? null
|
||||
: talentAdjustedDuration(sourceDurationMs, ability, modifiers);
|
||||
: ability.name === "Envenom"
|
||||
&& effect.aura.utilityTags?.includes(ROGUE_ENVENOM_BUFF_TAG)
|
||||
? sourceDurationMs
|
||||
: talentAdjustedDuration(sourceDurationMs, ability, modifiers);
|
||||
const auraDefinition = auraDefinitionForAbility(effect.aura, ability, durationMs);
|
||||
const recipients = effect.scope === "party"
|
||||
? [PLAYER_AURA_ENTITY_ID, ...usePartyStore.getState().members.filter((member) => member.health > 0).map((member) => member.id)]
|
||||
@@ -5681,9 +5980,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
}
|
||||
}
|
||||
for (const adjustment of cooldownAdjustments) {
|
||||
const candidates = adjustment.abilityName
|
||||
? state.abilities.filter((candidate) => candidate.name.toLowerCase() === adjustment.abilityName!.toLowerCase())
|
||||
: state.abilities;
|
||||
const candidates = cooldownResetCandidates(state.abilities, ability.id, adjustment.abilityName);
|
||||
for (const candidate of candidates) {
|
||||
if (adjustment.mode === "reset") cooldowns[candidate.id] = 0;
|
||||
else {
|
||||
@@ -5861,6 +6158,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
);
|
||||
set({
|
||||
mobs: work.mobs,
|
||||
effects: work.effects,
|
||||
auras: work.auras,
|
||||
combatEvents: work.combatEvents,
|
||||
actors: work.actors,
|
||||
@@ -5896,6 +6194,134 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
return result;
|
||||
},
|
||||
|
||||
triggerRogueWeaponPoisons: (
|
||||
sourceActorId,
|
||||
targetId,
|
||||
sourceAbilityId,
|
||||
hitCount = 1,
|
||||
now = combatNow(),
|
||||
) => {
|
||||
const state = get();
|
||||
const progression = progressionFromState(state);
|
||||
const work: MutableCastWork = {
|
||||
classId: state.classId,
|
||||
abilities: state.abilities,
|
||||
mobs: { ...state.mobs },
|
||||
effects: [...state.effects],
|
||||
auras: [...state.auras],
|
||||
summons: [...state.summons],
|
||||
areaEffects: [...state.areaEffects],
|
||||
combatEvents: [...state.combatEvents],
|
||||
actors: { ...state.actors },
|
||||
cooldowns: { ...state.cooldowns },
|
||||
health: state.health,
|
||||
maxHealth: state.maxHealth,
|
||||
shield: state.shield,
|
||||
shieldExpiresAt: state.shieldExpiresAt,
|
||||
resource: state.resource,
|
||||
maxResource: state.maxResource,
|
||||
resourcePools: { ...state.resourcePools },
|
||||
maxResourcePools: { ...state.maxResourcePools },
|
||||
lastResourceSpentAt: { ...state.lastResourceSpentAt },
|
||||
comboPoints: state.comboPoints,
|
||||
comboTargetId: state.comboTargetId,
|
||||
deathKnightRunes: state.deathKnightRunes,
|
||||
progression,
|
||||
totalDamage: 0,
|
||||
totalHealing: 0,
|
||||
combatResults: [],
|
||||
talentRanks: state.talentRanks,
|
||||
talentModifiers: state.talentModifiers,
|
||||
gearStats: state.gearStats,
|
||||
raceId: state.raceId,
|
||||
secondaryClassId: state.secondaryClassId,
|
||||
};
|
||||
const result = applyRogueWeaponPoisonsInWork(
|
||||
work,
|
||||
sourceActorId,
|
||||
targetId,
|
||||
sourceAbilityId,
|
||||
hitCount,
|
||||
now,
|
||||
);
|
||||
set({
|
||||
mobs: work.mobs,
|
||||
effects: work.effects,
|
||||
auras: work.auras,
|
||||
combatEvents: work.combatEvents,
|
||||
actors: work.actors,
|
||||
cooldowns: work.cooldowns,
|
||||
level: progression.level,
|
||||
xp: progression.xp,
|
||||
xpToNext: progression.xpToNext,
|
||||
health: progression.health,
|
||||
maxHealth: progression.maxHealth,
|
||||
resource: clamp(work.resource, 0, work.maxResource),
|
||||
maxResource: work.maxResource,
|
||||
selectedTargetId: work.mobs[targetId]?.dead && state.selectedTargetId === targetId
|
||||
? null
|
||||
: state.selectedTargetId,
|
||||
autoAttackTargetId: work.mobs[targetId]?.dead && state.autoAttackTargetId === targetId
|
||||
? null
|
||||
: state.autoAttackTargetId,
|
||||
nextAutoAttackAt: work.mobs[targetId]?.dead && state.autoAttackTargetId === targetId
|
||||
? 0
|
||||
: state.nextAutoAttackAt,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
consumeRogueDeadlyPoison: (
|
||||
sourceActorId,
|
||||
targetId,
|
||||
maximumDoses,
|
||||
now = combatNow(),
|
||||
) => {
|
||||
const state = get();
|
||||
const work: MutableCastWork = {
|
||||
classId: state.classId,
|
||||
abilities: state.abilities,
|
||||
mobs: { ...state.mobs },
|
||||
effects: [...state.effects],
|
||||
auras: [...state.auras],
|
||||
summons: [...state.summons],
|
||||
areaEffects: [...state.areaEffects],
|
||||
combatEvents: [...state.combatEvents],
|
||||
actors: { ...state.actors },
|
||||
cooldowns: { ...state.cooldowns },
|
||||
health: state.health,
|
||||
maxHealth: state.maxHealth,
|
||||
shield: state.shield,
|
||||
shieldExpiresAt: state.shieldExpiresAt,
|
||||
resource: state.resource,
|
||||
maxResource: state.maxResource,
|
||||
resourcePools: { ...state.resourcePools },
|
||||
maxResourcePools: { ...state.maxResourcePools },
|
||||
lastResourceSpentAt: { ...state.lastResourceSpentAt },
|
||||
comboPoints: state.comboPoints,
|
||||
comboTargetId: state.comboTargetId,
|
||||
deathKnightRunes: state.deathKnightRunes,
|
||||
progression: progressionFromState(state),
|
||||
totalDamage: 0,
|
||||
totalHealing: 0,
|
||||
combatResults: [],
|
||||
talentRanks: state.talentRanks,
|
||||
talentModifiers: state.talentModifiers,
|
||||
gearStats: state.gearStats,
|
||||
raceId: state.raceId,
|
||||
secondaryClassId: state.secondaryClassId,
|
||||
};
|
||||
const consumed = consumeRogueDeadlyPoisonInWork(
|
||||
work,
|
||||
sourceActorId,
|
||||
targetId,
|
||||
maximumDoses,
|
||||
now,
|
||||
);
|
||||
if (consumed > 0) set({ auras: work.auras, effects: work.effects });
|
||||
return consumed;
|
||||
},
|
||||
|
||||
addHealingThreat: (source, effectiveHealing) => set((state) => {
|
||||
const engagedIds = Object.values(state.mobs)
|
||||
.filter((mob) => mob.engaged && !mob.dead)
|
||||
@@ -5913,6 +6339,42 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
return { mobs };
|
||||
}),
|
||||
|
||||
applyActorThreatEffect: (actorId, effect, redirectTargetActorId = null, now = combatNow()) => set((state) => {
|
||||
const mobs: Record<string, MobCombatState> = { ...state.mobs };
|
||||
let changed = false;
|
||||
for (const [id, mob] of Object.entries(mobs)) {
|
||||
if (!mob.engaged || mob.dead) continue;
|
||||
const currentThreat = mob.threatByActor[actorId] ?? 0;
|
||||
const nextThreat = effect.mode === "redirect"
|
||||
? 0
|
||||
: effect.mode === "percent"
|
||||
? currentThreat * Math.max(0, 1 + effect.amount / 100)
|
||||
: Math.max(0, currentThreat + effect.amount);
|
||||
const threatByActor = {
|
||||
...mob.threatByActor,
|
||||
[actorId]: nextThreat,
|
||||
...(effect.mode === "redirect" && redirectTargetActorId
|
||||
? { [redirectTargetActorId]: (mob.threatByActor[redirectTargetActorId] ?? 0) + currentThreat }
|
||||
: {}),
|
||||
};
|
||||
const resolution = resolveAggroTarget(
|
||||
threatByActor,
|
||||
eligibleAggroActorIds(state, mob),
|
||||
mob.targetActorId,
|
||||
mob.forcedTarget,
|
||||
now,
|
||||
);
|
||||
mobs[id] = {
|
||||
...mob,
|
||||
threatByActor,
|
||||
targetActorId: resolution.targetActorId,
|
||||
forcedTarget: resolution.forcedTarget,
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
return changed ? { mobs } : state;
|
||||
}),
|
||||
|
||||
tauntMob: (id, source, durationMs, now = combatNow()) => {
|
||||
const target = get().mobs[id];
|
||||
if (!target || target.dead || durationMs <= 0 || (target.navigationEvadeUntil ?? 0) > now) return false;
|
||||
|
||||
Reference in new issue
Block a user