forked from phenom/healer-man
Release Healer Man 0.1.29
This commit is contained in:
1 parent
8c9809d3e0
commit
544c1bee91
120 files changed
+4363
-1036
No files matched your search
+50
-11
@@ -330,6 +330,10 @@ export interface DiminishingReturnState {
|
||||
}
|
||||
|
||||
export interface MobCombatState {
|
||||
/** When the hostile actor is a player, preserve its actual defense and resources. */
|
||||
readonly characterDefender?: CombatDefenderStats;
|
||||
readonly characterShield?: number;
|
||||
readonly characterResources?: Readonly<Partial<Record<ResourceType, number>>>;
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly serverEntry: number | null;
|
||||
@@ -1176,7 +1180,7 @@ function abilityAttackKind(classId: ClassId, ability: AbilityDefinition): WotlkA
|
||||
return "spell";
|
||||
}
|
||||
|
||||
function abilityDamageSchool(classId: ClassId, ability: AbilityDefinition): DamageSchool {
|
||||
export function abilityDamageSchool(classId: ClassId, ability: AbilityDefinition): DamageSchool {
|
||||
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";
|
||||
@@ -1328,7 +1332,7 @@ function resolvePlayerDamageResult(
|
||||
attackKind,
|
||||
damageSchool: school,
|
||||
attacker,
|
||||
defender: npcCombatDefender(mob, stats.level),
|
||||
defender: mob.characterDefender ?? npcCombatDefender(mob, stats.level),
|
||||
rules: immune
|
||||
? {
|
||||
...rules,
|
||||
@@ -1710,6 +1714,15 @@ function talentAdjustedIncomingDamage(amount: number, modifiers: readonly Talent
|
||||
));
|
||||
}
|
||||
|
||||
/** Renderer-independent player defenses for hostile-player combat adapters. */
|
||||
export function characterCombatDefender(state: CombatState): CombatDefenderStats {
|
||||
const stats = applyAuraStats(playerCharacterStats(state.classId, state.level, state.gearStats,
|
||||
state.raceId, state.secondaryClassId, state.talentModifiers), aurasForEntity(state.auras, PLAYER_AURA_ENTITY_ID));
|
||||
const defender = combatDefenderFromCharacterStats(stats);
|
||||
const multiplier = talentAdjustedIncomingDamage(10_000, state.talentModifiers) / 10_000;
|
||||
return { ...defender, damageReductionPercent: (1 - (1 - (defender.damageReductionPercent ?? 0) / 100) * multiplier) * 100 };
|
||||
}
|
||||
|
||||
function normalizedSettings(
|
||||
current: GameplaySettings,
|
||||
patch: Partial<GameplaySettings>,
|
||||
@@ -2534,11 +2547,13 @@ function damageMobInWork(
|
||||
now,
|
||||
});
|
||||
work.auras = [...auraAbsorb.auras];
|
||||
const characterAbsorbed = Math.min(mob.characterShield ?? 0, auraAbsorb.remainingDamage);
|
||||
const remainingDamage = Math.max(0, auraAbsorb.remainingDamage - characterAbsorbed);
|
||||
const adjustedCombatResult = combatResult
|
||||
? { ...combatResult, finalDamage: Math.max(0, Math.round(auraAbsorb.remainingDamage)) }
|
||||
? { ...combatResult, finalDamage: Math.max(0, Math.round(remainingDamage)) }
|
||||
: undefined;
|
||||
if (adjustedCombatResult) work.combatResults.push(adjustedCombatResult);
|
||||
const applied = Math.min(mob.health, Math.max(0, Math.round(auraAbsorb.remainingDamage)));
|
||||
const applied = Math.min(mob.health, Math.max(0, Math.round(remainingDamage)));
|
||||
recordCombatEventInWork(work, {
|
||||
occurredAt: now,
|
||||
kind: "damage",
|
||||
@@ -2549,7 +2564,7 @@ function damageMobInWork(
|
||||
rawAmount: combatResult?.rawDamage ?? Math.max(0, Math.round(amount)),
|
||||
effectiveAmount: applied,
|
||||
overheal: 0,
|
||||
absorbed: auraAbsorb.absorbed,
|
||||
absorbed: auraAbsorb.absorbed + characterAbsorbed,
|
||||
critical: combatResult?.critical ?? false,
|
||||
outcome: combatResult?.outcome ?? "hit",
|
||||
});
|
||||
@@ -2586,6 +2601,7 @@ function damageMobInWork(
|
||||
);
|
||||
work.mobs[id] = {
|
||||
...engagedMob,
|
||||
...(mob.characterShield !== undefined ? { characterShield: mob.characterShield - characterAbsorbed } : {}),
|
||||
health,
|
||||
dead: killed,
|
||||
hasLoot,
|
||||
@@ -2609,7 +2625,9 @@ function damageMobInWork(
|
||||
combatPhase: killed ? "idle" : "chasing",
|
||||
homePosition: engagedMob.homePosition ?? getMobPosition(id),
|
||||
nextAttackAt: killed ? 0 : (mob.nextAttackAt > now ? mob.nextAttackAt : now + 450),
|
||||
activeCast: killed ? null : engagedMob.activeCast,
|
||||
activeCast: killed ? null : mob.characterDefender && applied > 0 && engagedMob.activeCast
|
||||
? { ...engagedMob.activeCast, ...applyWrathCastPushback(engagedMob.activeCast as unknown as ActiveCast, now, `${source.actorId}:${now}`) }
|
||||
: engagedMob.activeCast,
|
||||
woundRevision: canPlayWoundAnimation ? mob.woundRevision + 1 : mob.woundRevision,
|
||||
nextWoundAnimationAt: canPlayWoundAnimation
|
||||
? now + MOB_WOUND_ANIMATION_COOLDOWN_MS
|
||||
@@ -2737,19 +2755,19 @@ function applyStatus(
|
||||
"stun", "fear", "root", "sleep", "blind", "freeze", "charm", "confuse", "knockdown",
|
||||
]);
|
||||
const previousDiminishing = mob.controlDiminishingReturns?.[effect.status];
|
||||
const applications = mob.boss && diminishingStatuses.has(effect.status) && previousDiminishing?.resetAt && previousDiminishing.resetAt > now
|
||||
const applications = (mob.boss || mob.characterDefender) && diminishingStatuses.has(effect.status) && previousDiminishing?.resetAt && previousDiminishing.resetAt > now
|
||||
? previousDiminishing.applications
|
||||
: 0;
|
||||
const multiplier = mob.boss && diminishingStatuses.has(effect.status)
|
||||
const multiplier = (mob.boss || mob.characterDefender) && diminishingStatuses.has(effect.status)
|
||||
? [1, 0.5, 0.25, 0][Math.min(3, applications)]
|
||||
: 1;
|
||||
const durationMs = Math.round(baseDurationMs * multiplier);
|
||||
const controlDiminishingReturns = mob.boss && diminishingStatuses.has(effect.status)
|
||||
const controlDiminishingReturns = (mob.boss || mob.characterDefender) && diminishingStatuses.has(effect.status)
|
||||
? {
|
||||
...mob.controlDiminishingReturns,
|
||||
[effect.status]: {
|
||||
applications: Math.min(3, applications + 1),
|
||||
resetAt: now + 15_000,
|
||||
resetAt: now + (mob.characterDefender ? durationMs : 0) + 15_000,
|
||||
},
|
||||
}
|
||||
: mob.controlDiminishingReturns;
|
||||
@@ -4986,6 +5004,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
const candidates = effect.maxTargets ? affectedMobIds.slice(0, effect.maxTargets) : affectedMobIds;
|
||||
for (const id of candidates) {
|
||||
const mob = work.mobs[id];
|
||||
if (mob?.characterDefender && ability.effects.some((candidate) => candidate.kind === "apply-aura" && candidate.control?.kind === effect.status)) continue;
|
||||
if (mob) work.mobs[id] = applyStatus(mob, {
|
||||
...effect,
|
||||
durationMs: talentAdjustedDuration(effect.durationMs, ability, modifiers),
|
||||
@@ -5010,6 +5029,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
: [targetId ?? PLAYER_AURA_ENTITY_ID];
|
||||
for (const recipientId of recipients) {
|
||||
const previousAuras = work.auras;
|
||||
if (effect.control && work.mobs[recipientId]?.characterDefender && work.mobs[recipientId].mechanicImmunities.includes(effect.control.kind as AzerothCoreMechanic)) continue;
|
||||
work.auras = [...applyCombatAura(previousAuras, {
|
||||
definition: auraDefinition,
|
||||
sourceId: PLAYER_AURA_ENTITY_ID,
|
||||
@@ -5032,12 +5052,18 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
}
|
||||
if (effect.control && work.mobs[recipientId]) {
|
||||
const durationMs = auraDefinition.durationMs ?? 1_000;
|
||||
const previousStatuses = work.mobs[recipientId].statuses;
|
||||
work.mobs[recipientId] = applyStatus(work.mobs[recipientId], {
|
||||
kind: "status",
|
||||
status: effect.control.kind as MobStatusKind,
|
||||
durationMs,
|
||||
...(effect.control.magnitude === undefined ? {} : { magnitude: effect.control.magnitude }),
|
||||
}, `aura:${auraDefinition.id}`, now);
|
||||
if (work.mobs[recipientId].characterDefender) {
|
||||
const applied = work.mobs[recipientId].statuses.find((status) => status.sourceAbilityId === `aura:${auraDefinition.id}`);
|
||||
if (work.mobs[recipientId].statuses === previousStatuses) work.auras = [...previousAuras];
|
||||
else if (applied) work.auras = work.auras.map((aura) => aura.targetId === recipientId && aura.definition.id === auraDefinition.id ? { ...aura, expiresAt: applied.endsAt } : aura);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (effect.kind === "dispel") {
|
||||
@@ -5140,7 +5166,20 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
?? (resource === activeResourceType ? work.maxResource : defaultResourceMaximum(resource));
|
||||
const drainAmount = effect.percentage ? maximum * effect.amount / 100 : effect.amount;
|
||||
if (ability.target === "hostile") {
|
||||
const returned = effect.burn ? 0 : drainAmount;
|
||||
let drainedFromPlayers = 0;
|
||||
let hasPlayerTarget = false;
|
||||
for (const id of affectedMobIds) {
|
||||
const mob = work.mobs[id];
|
||||
if (!mob?.characterResources) continue;
|
||||
hasPlayerTarget = true;
|
||||
const current = mob.characterResources[resource] ?? 0;
|
||||
const requested = effect.percentage ? mob.maxMana * effect.amount / 100 : effect.amount;
|
||||
const drained = Math.min(current, requested);
|
||||
drainedFromPlayers += drained;
|
||||
work.mobs[id] = { ...mob, characterResources: { ...mob.characterResources, [resource]: current - drained } };
|
||||
if (effect.burn && drained > 0) damageMobInWork(state.classId, work, id, drained, now, castThreatSource, damageSchool);
|
||||
}
|
||||
const returned = effect.burn ? 0 : hasPlayerTarget ? drainedFromPlayers : drainAmount;
|
||||
if (returned > 0) {
|
||||
work.resourcePools[resource] = clamp((work.resourcePools[resource] ?? 0) + returned, 0, maximum);
|
||||
if (resource === activeResourceType) work.resource = work.resourcePools[resource] ?? work.resource;
|
||||
|
||||
Reference in new issue
Block a user