Update Healer Man server

This commit is contained in:
phenom committed 2026-10-04 13:13:35 -04:00
1 parent 39458d5de6
commit 6cc1b9217f
203 files changed
+133929 -31271

No files matched your search

+61 -3
View File
@@ -1,3 +1,5 @@
import { enemyDamageTypeSourceForSpell, resolveEnemyDamageType, type EnemyDamageOrigin } from "./mobDamageTypes";
import { romNativeEnemyPeriodicDamageSources } from "./romNativePeriodicDamage";
import {
normalizeRunewakerDamageFormula,
type RunewakerDamageFormulaSource,
@@ -72,6 +74,8 @@ export interface EnemyAttackTelegraph {
export type EnemyAtomicAbilityEffect =
| { readonly kind: "damage"; readonly multiplier: number }
| { readonly kind: "dot"; readonly multiplier: number; readonly ticks: number; readonly intervalMs: number;
readonly sourceSkillId?: number; readonly componentId?: number }
| { readonly kind: "heal"; readonly multiplier: number }
| {
readonly kind: "leech";
@@ -162,6 +166,19 @@ export interface EnemyAttackDefinition {
readonly delivery: EnemyAttackDelivery;
readonly target: EnemyAttackTarget;
readonly school: EnemyDamageSchool;
/** Hit-table channel, independent from projectile/area presentation and damage school. */
readonly attackKind?: "melee" | "ranged" | "spell";
readonly damageClass?: number;
/** Original WoW school bits; composite schools retain the authored scalar policy. */
readonly schoolMask?: number;
/** Source RuneWaker HP calculation channel (0 magic, 1 physical). */
readonly runewakerEffectType?: number;
readonly runewakerComponentId?: number;
readonly damageOrigin?: EnemyDamageOrigin;
/** Runtime pulse of a previously applied periodic debuff. */
readonly periodicTick?: boolean;
readonly targetMitigationSnapshotted?: boolean;
readonly periodicNoKill?: boolean;
readonly animation: EnemyAttackAnimation;
/** Zero keeps the legacy instant-execution behavior. */
readonly castTimeMs?: number;
@@ -184,6 +201,8 @@ export interface EnemyAttackDefinition {
export interface EnemyCombatDefinition {
readonly serverEntry?: number;
/** RuneWaker NPC source identity; independent from AzerothCore creature entries. */
readonly runewakerNpcId?: number;
readonly level?: number;
readonly healthMultiplier?: number;
readonly damageMultiplier?: number;
@@ -226,6 +245,7 @@ function harmfulEnemyAttack(value: EnemyAttackDefinition): boolean {
if (value.effect.kind === "composite") {
return value.effect.effects.some((effect) => (
effect.kind === "damage"
|| effect.kind === "dot"
|| effect.kind === "leech"
|| effect.kind === "control"
|| effect.kind === "target-aura"
@@ -233,6 +253,7 @@ function harmfulEnemyAttack(value: EnemyAttackDefinition): boolean {
));
}
return value.effect.kind === "damage"
|| value.effect.kind === "dot"
|| value.effect.kind === "leech"
|| value.effect.kind === "control"
|| value.effect.kind === "target-aura"
@@ -310,6 +331,7 @@ function normalizeTelegraph(value: EnemyAttackTelegraph): EnemyAttackTelegraph {
export function normalizeEnemyAttack(
value: EnemyAttackDefinition,
fallback: EnemyAttackDefinition = DEFAULT_ENEMY_ATTACK,
runewakerNpcId?: number,
): EnemyAttackDefinition {
const cooldownMs = Math.round(finiteAtLeast(value.cooldownMs, fallback.cooldownMs, 1));
const telegraph = value.telegraph ?? inferEnemyAttackTelegraph(value);
@@ -321,6 +343,33 @@ export function normalizeEnemyAttack(
telegraph?.warningMs ?? 0,
);
const runewakerFormula = normalizeRunewakerDamageFormula(value.runewakerFormula);
const damageOrigin = value.damageOrigin ?? (runewakerFormula || value.id.startsWith("rw-") ? "runewaker" : "wow335");
const sourceType = value.spellId ? enemyDamageTypeSourceForSpell(damageOrigin, value.spellId) : undefined;
// Script/control placeholder spell IDs must not override authored damage packets.
const sourceHasDamage = sourceType && ("components" in sourceType
? sourceType.components.length > 0 : sourceType.damageEffects.length > 0);
const damageType = resolveEnemyDamageType({ ...value,
...(sourceHasDamage ? { origin: damageOrigin,
componentId: value.runewakerComponentId ?? runewakerFormula?.componentId } : {}),
});
const periodicSources = damageOrigin === "runewaker" && value.spellId
? romNativeEnemyPeriodicDamageSources(value.spellId, runewakerNpcId) : [];
const uniquePeriodicSources = periodicSources.filter((source, index, all) => all.findIndex((entry) =>
entry.sourceSkillId === source.sourceSkillId && entry.component.componentId === source.component.componentId) === index);
const periodicEffects: EnemyAtomicAbilityEffect[] = uniquePeriodicSources.map((source) => ({
kind: "dot", multiplier: value.damageMultiplier,
ticks: Math.max(1, Math.trunc(source.component.effectTimeSeconds / source.component.dotTimeSeconds)),
intervalMs: source.component.dotTimeSeconds * 1_000,
sourceSkillId: source.sourceSkillId, componentId: source.component.componentId,
}));
const baseEffect = value.effect ?? { kind: "damage" as const, multiplier: value.damageMultiplier };
const existingEffects = baseEffect.kind === "composite" ? baseEffect.effects : [baseEffect];
const directEffects = (runewakerFormula?.powerKind === "dot-only" ? [] : existingEffects)
.filter((effect) => !(effect.kind === "dot" && periodicEffects.some((periodic) => periodic.kind === "dot"
&& periodic.componentId === effect.componentId && periodic.sourceSkillId === effect.sourceSkillId)));
const resolvedEffect = periodicEffects.length
? { kind: "composite" as const, effects: [...directEffects, ...periodicEffects] }
: value.effect;
return Object.freeze({
id: value.id.trim() || fallback.id,
name: value.name.trim() || fallback.name,
@@ -329,7 +378,15 @@ export function normalizeEnemyAttack(
: {}),
delivery: value.delivery,
target: value.target,
school: value.school,
school: damageType.school ?? value.school,
damageOrigin,
...(damageType.attackKind ? { attackKind: damageType.attackKind } : {}),
...(damageType.damageClass !== undefined ? { damageClass: damageType.damageClass } : {}),
...(damageType.schoolMask !== undefined ? { schoolMask: damageType.schoolMask } : {}),
...(damageType.sourceDamageClass === "magical" ? { runewakerEffectType: 0 }
: damageType.sourceDamageClass === "physical" ? { runewakerEffectType: 1 }
: value.runewakerEffectType !== undefined ? { runewakerEffectType: value.runewakerEffectType } : {}),
...(value.runewakerComponentId ? { runewakerComponentId: value.runewakerComponentId } : {}),
animation: value.animation,
castTimeMs,
interruptible: value.interruptible ?? fallback.interruptible ?? true,
@@ -350,7 +407,7 @@ export function normalizeEnemyAttack(
? { radius: finiteAtLeast(value.radius, 0, 0) }
: {}),
...(telegraph ? { telegraph: normalizeTelegraph(telegraph) } : {}),
...(value.effect ? { effect: value.effect } : {}),
...(resolvedEffect ? { effect: resolvedEffect } : {}),
...(value.condition ? { condition: value.condition } : {}),
...(value.source ? { source: value.source } : {}),
});
@@ -359,9 +416,10 @@ export function normalizeEnemyAttack(
export function normalizeEnemyAttacks(
attacks: readonly EnemyAttackDefinition[] | undefined,
fallback: EnemyAttackDefinition = DEFAULT_ENEMY_ATTACK,
runewakerNpcId?: number,
): readonly EnemyAttackDefinition[] {
const source = attacks?.length ? attacks : [fallback];
const normalized = source.map((attack) => normalizeEnemyAttack(attack, fallback));
const normalized = source.map((attack) => normalizeEnemyAttack(attack, fallback, runewakerNpcId));
const seen = new Set<string>();
return Object.freeze(normalized.filter((attack) => {
if (seen.has(attack.id)) return false;