Release Healer Man 0.1.29

This commit is contained in:
phenom committed 2026-09-11 14:26:15 -04:00
1 parent 8c9809d3e0
commit 544c1bee91
120 files changed
+4363 -1036

No files matched your search

+50 -11
View File
@@ -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;