Update Healer Man server

This commit is contained in:
phenom committed 2026-10-06 22:25:34 -04:00
1 parent 6114a2295f
commit 69809f7228
593 files changed
+903643 -40774

No files matched your search

+394
View File
@@ -0,0 +1,394 @@
import type { ActiveAura } from "./combatAuras";
import type { ItemStats, WeaponStats } from "./itemStats";
import type {
EnemyDisarmAbilityRequirement,
EnemyDisarmEquippedItem,
EnemyDisarmTargetWeapons,
EnemyDisarmWeaponChannel,
EnemyWeaponDebuffProfile,
} from "./enemyWeaponDebuffs";
const DISARM_TAG_PREFIX = "enemy-disarm:";
export interface DisarmLoadoutItem {
readonly weaponType?: string | null;
readonly inventorySlot?: string;
readonly armorType?: string | null;
readonly stats?: Pick<ItemStats, "meleeWeapon" | "rangedWeapon">;
}
export interface DisarmWeaponLoadout {
readonly "main-hand"?: DisarmLoadoutItem | null;
readonly "off-hand"?: DisarmLoadoutItem | null;
readonly ranged?: DisarmLoadoutItem | null;
}
export interface ActiveEnemyDisarm {
readonly profile: EnemyWeaponDebuffProfile;
readonly channels: ReadonlySet<EnemyDisarmWeaponChannel>;
}
export interface EnemyDisarmItemDependentAuraRequirement extends EnemyDisarmAbilityRequirement {
/** AuraDefinition.id produced by the source ability. */
readonly auraDefinitionId: string;
readonly passive: boolean;
}
export interface EnemyDisarmSpellWeaponMetadata {
/** Spell.dbc DmgClass: 2 melee, 3 ranged; other classes do not select a weapon channel here. */
readonly damageClass: number;
/** Spell.dbc AttributesEx: USES_RANGED_SLOT. */
readonly attributes0?: number;
/** Spell.dbc AttributesEx3: required off-hand selects the off-hand attack. */
readonly attributes3?: number;
/** Spell.dbc EquippedItemSubClassMask, used by core IsRangedWeaponSpell. */
readonly equippedItemSubclassMask?: number;
/** Core's Hunter-family exception or a pre-resolved IsRangedWeaponSpell value. */
readonly isRangedWeaponSpell?: boolean;
}
const CHANNELS: readonly EnemyDisarmWeaponChannel[] = ["main-hand", "off-hand", "ranged"];
const SPELL_DAMAGE_CLASS_MELEE = 2;
const SPELL_DAMAGE_CLASS_RANGED = 3;
const SPELL_ATTR0_USES_RANGED_SLOT = 0x00000002;
const SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON = 0x01000000;
const RANGED_WEAPON_SUBCLASS_MASK = (1 << 2) | (1 << 3) | (1 << 16) | (1 << 18);
/** AzerothCore's disarmed player base range and BASE_ATTACK_TIME. */
export const ENEMY_DISARM_UNARMED_MAIN_HAND: Readonly<WeaponStats> = Object.freeze({
damageMin: 1,
damageMax: 2,
damageSchool: "physical",
speedMs: 2_000,
});
/**
* Read only exact tags emitted by the pinned source-backed 6713 contract.
* Unknown tags/profiles are ignored so native RoM state cannot be mistaken
* for a WoW disarm.
*/
export function activeEnemyDisarms(
auras: readonly ActiveAura[],
targetId: string,
now: number,
): readonly ActiveEnemyDisarm[] {
const byProfile = new Map<EnemyWeaponDebuffProfile, Set<EnemyDisarmWeaponChannel>>();
for (const aura of auras) {
if (aura.targetId !== targetId || (aura.expiresAt !== null && aura.expiresAt <= now)) continue;
for (const tag of aura.definition.utilityTags ?? []) {
if (!tag.startsWith(DISARM_TAG_PREFIX)) continue;
const [, profile, channelName] = tag.split(":");
if ((profile !== "ascension" && profile !== "epoch")
|| (channelName !== "main-hand" && channelName !== "off-hand" && channelName !== "ranged")) continue;
const channels = byProfile.get(profile) ?? new Set<EnemyDisarmWeaponChannel>();
channels.add(channelName);
byProfile.set(profile, channels);
}
}
return [...byProfile].map(([profile, channels]) => ({ profile, channels }));
}
/** The union of only currently active, recognized source Disarm channels. */
export function activeEnemyDisarmChannels(
auras: readonly ActiveAura[],
targetId: string,
now: number,
): ReadonlySet<EnemyDisarmWeaponChannel> {
return new Set(activeEnemyDisarms(auras, targetId, now).flatMap((disarm) => [...disarm.channels]));
}
export function enemyDisarmBlocksActorChannel(
auras: readonly ActiveAura[],
targetId: string,
now: number,
channel: EnemyDisarmWeaponChannel,
): boolean {
return activeEnemyDisarmChannels(auras, targetId, now).has(channel);
}
/** Build the core's usable-target-weapon check from an already-resolved loadout. */
export function enemyDisarmTargetWeaponsFromLoadout(
loadout: DisarmWeaponLoadout,
feralForm = false,
): EnemyDisarmTargetWeapons {
const hasMeleeWeapon = (item: DisarmLoadoutItem | null | undefined): boolean => Boolean(
item?.weaponType && item.stats?.meleeWeapon,
);
return {
mainHand: hasMeleeWeapon(loadout["main-hand"]),
offHand: hasMeleeWeapon(loadout["off-hand"]),
ranged: Boolean(loadout.ranged?.weaponType && loadout.ranged.stats?.rangedWeapon),
feralForm,
};
}
/** Create a combat-only view matching core's disarmed main-hand punch. */
export function enemyDisarmAdjustedWeaponStats<T extends Pick<ItemStats, "meleeWeapon" | "rangedWeapon">>(
gearStats: T,
channels: ReadonlySet<EnemyDisarmWeaponChannel>,
feralForm = false,
): T {
if ((!channels.has("main-hand") || feralForm) && !channels.has("ranged")) return gearStats;
return {
...gearStats,
...(channels.has("main-hand") && !feralForm ? { meleeWeapon: ENEMY_DISARM_UNARMED_MAIN_HAND } : {}),
...(channels.has("ranged") ? { rangedWeapon: null } : {}),
};
}
/**
* Select Spell.cpp's weapon attack type from the source spell fields. Unknown
* damage classes return undefined and leave their ordinary spell damage alone.
*/
export function enemyDisarmAbilityWeaponChannel(
spell: EnemyDisarmSpellWeaponMetadata,
): EnemyDisarmWeaponChannel | undefined {
if (spell.damageClass === SPELL_DAMAGE_CLASS_MELEE) {
return ((spell.attributes3 ?? 0) & SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON) !== 0
? "off-hand"
: "main-hand";
}
if (spell.damageClass !== SPELL_DAMAGE_CLASS_RANGED) return undefined;
const isRangedWeaponSpell = spell.isRangedWeaponSpell
?? (((spell.attributes0 ?? 0) & SPELL_ATTR0_USES_RANGED_SLOT) !== 0
|| (((spell.equippedItemSubclassMask ?? 0) & RANGED_WEAPON_SUBCLASS_MASK) !== 0));
return isRangedWeaponSpell ? "ranged" : "main-hand";
}
/**
* Return only the selected weapon's Disarm-adjusted contribution. A main-hand
* punch retains source unarmed damage even though the equipped item remains;
* disabled off-hand/ranged attacks have no weapon contribution.
*/
export function enemyDisarmAdjustedAbilityWeapon(
weapon: WeaponStats | null | undefined,
channel: EnemyDisarmWeaponChannel | undefined,
disarmedChannels: ReadonlySet<EnemyDisarmWeaponChannel>,
feralForm = false,
): WeaponStats | null {
if (!channel || !disarmedChannels.has(channel)) return weapon ?? null;
if (channel === "main-hand") return feralForm ? weapon ?? null : ENEMY_DISARM_UNARMED_MAIN_HAND;
return null;
}
/**
* The creature Spell::CheckItems gate is limited to damage-class melee/ranged
* spells and their selected attack channel. Player spell casts use different
* item-requirement rules and must not call this as a blanket cast restriction.
*/
export function enemyDisarmBlocksNpcWeaponAbility(
auras: readonly ActiveAura[],
targetId: string,
now: number,
spell: EnemyDisarmSpellWeaponMetadata,
): boolean {
const channel = enemyDisarmAbilityWeaponChannel(spell);
return channel !== undefined && enemyDisarmBlocksActorChannel(auras, targetId, now, channel);
}
const WEAPON_SUBCLASS: Readonly<Record<string, number>> = Object.freeze({
axe: 0,
bow: 2,
gun: 3,
mace: 4,
polearm: 6,
sword: 7,
staff: 10,
fist: 13,
"fist-weapon": 13,
dagger: 15,
throwing: 16,
thrown: 16,
spear: 17,
crossbow: 18,
wand: 19,
});
function itemInventoryType(item: DisarmLoadoutItem, channel: EnemyDisarmWeaponChannel): number | undefined {
if (item.inventorySlot === "one-hand") return 13;
if (item.inventorySlot === "two-hand") return 17;
if (item.inventorySlot === "main-hand") return 21;
if (item.inventorySlot === "off-hand") return 22;
if (item.inventorySlot !== "ranged" && channel !== "ranged") return undefined;
if (item.weaponType === "bow" || item.weaponType === "crossbow" || item.weaponType === "gun") return 15;
if (item.weaponType === "throwing" || item.weaponType === "thrown") return 25;
if (item.weaponType === "wand") return 26;
return undefined;
}
/** Map recognized equipped weapon types to Spell.dbc class-2 subclasses. */
export function enemyDisarmEquippedWeaponItem(
channel: EnemyDisarmWeaponChannel,
item: DisarmLoadoutItem | null | undefined,
): EnemyDisarmEquippedItem | null {
if (!item?.weaponType || !item.stats
|| (channel === "ranged" ? !item.stats.rangedWeapon : !item.stats.meleeWeapon)) return null;
const baseSubclass = WEAPON_SUBCLASS[item.weaponType];
if (baseSubclass === undefined) return null;
const subclass = item.weaponType === "axe" && item.inventorySlot === "two-hand"
? 1
: item.weaponType === "mace" && item.inventorySlot === "two-hand"
? 5
: item.weaponType === "sword" && item.inventorySlot === "two-hand"
? 8
: baseSubclass;
return {
channel,
itemClass: 2,
subclass,
...(itemInventoryType(item, channel) !== undefined
? { inventoryType: itemInventoryType(item, channel) }
: {}),
};
}
export function enemyDisarmRequirementCandidates(
loadout: DisarmWeaponLoadout,
): readonly EnemyDisarmEquippedItem[] {
return CHANNELS.flatMap((channel) => {
const item = channel === "main-hand" ? loadout["main-hand"]
: channel === "off-hand" ? loadout["off-hand"] : loadout.ranged;
const candidate = enemyDisarmEquippedWeaponItem(channel, item);
return candidate ? [candidate] : [];
});
}
/** Source profile of one unexpired Disarm that removes every matching item. */
export function enemyDisarmInvalidatingProfile(
auras: readonly ActiveAura[],
targetId: string,
now: number,
requirement: EnemyDisarmAbilityRequirement,
equippedItems: readonly EnemyDisarmEquippedItem[],
): EnemyWeaponDebuffProfile | undefined {
if (requirement.equippedItemClass < 0) return undefined;
const match = activeEnemyDisarms(auras, targetId, now).find(({ profile, channels }) => {
const matching = equippedItems.filter((item) => equippedItemMatchesRequirement(item, requirement));
return matching.length > 0 && matching.every((item) => channels.has(item.channel));
});
return match?.profile;
}
function equippedItemMatchesRequirement(
item: EnemyDisarmEquippedItem,
requirement: EnemyDisarmAbilityRequirement,
): boolean {
if (item.itemClass !== requirement.equippedItemClass) return false;
if (requirement.equippedItemSubclassMask !== 0
&& (!Number.isInteger(item.subclass) || item.subclass < 0 || item.subclass > 31
|| ((requirement.equippedItemSubclassMask >>> item.subclass) & 1) === 0)) return false;
if ((requirement.equippedItemInventoryMask ?? 0) !== 0
&& (!Number.isInteger(item.inventoryType) || item.inventoryType! < 0 || item.inventoryType! > 31
|| ((requirement.equippedItemInventoryMask! >>> item.inventoryType!) & 1) === 0)) return false;
return true;
}
/**
* Identify self-cast passive item auras whose every matching equipped weapon
* is currently disarmed. This is an effective-aura view: the persistent aura
* objects stay available so the passive can become effective again when the
* timed Disarm expires. Active nonpassive aura spells follow the core's
* explicit keep-aura exception.
*/
export function enemyDisarmItemDependentAuraSuppressionIds(
auras: readonly ActiveAura[],
casterId: string,
now: number,
requirements: readonly EnemyDisarmItemDependentAuraRequirement[],
loadout: DisarmWeaponLoadout,
feralForm = false,
): ReadonlySet<string> {
if (feralForm) return new Set();
const disabledChannels = activeEnemyDisarmChannels(auras, casterId, now);
if (disabledChannels.size === 0) return new Set();
const byAuraId = new Map(requirements.map((requirement) => [requirement.auraDefinitionId, requirement]));
const equippedItems = enemyDisarmRequirementCandidates(loadout);
const suppressed = new Set<string>();
for (const aura of auras) {
if (aura.sourceId !== casterId || (aura.expiresAt !== null && aura.expiresAt <= now)) continue;
const requirement = byAuraId.get(aura.definition.id);
if (!requirement?.passive || requirement.equippedItemClass < 0) continue;
const matching = equippedItems.filter((item) => equippedItemMatchesRequirement(item, requirement));
if (matching.length > 0 && matching.every((item) => disabledChannels.has(item.channel))) {
suppressed.add(aura.instanceId);
}
}
return suppressed;
}
/**
* Filter item-requirement aura effects for one attack channel, matching the
* core's attack-type-specific weapon-aura recalculation. The same active aura
* may therefore contribute to an enabled off-hand while being absent from a
* disarmed main-hand calculation.
*/
export function enemyDisarmAurasForWeaponChannel(
auras: readonly ActiveAura[],
targetId: string,
now: number,
channel: EnemyDisarmWeaponChannel,
requirements: readonly EnemyDisarmItemDependentAuraRequirement[],
loadout: DisarmWeaponLoadout,
feralForm = false,
): readonly ActiveAura[] {
if (feralForm) return auras;
const channels = activeEnemyDisarmChannels(auras, targetId, now);
const byAuraId = new Map(requirements.map((requirement) => [requirement.auraDefinitionId, requirement]));
const equippedItems = enemyDisarmRequirementCandidates(loadout);
return auras.filter((aura) => {
if (aura.targetId !== targetId || (aura.expiresAt !== null && aura.expiresAt <= now)) return true;
const requirement = byAuraId.get(aura.definition.id);
if (!requirement || requirement.equippedItemClass < 0) return true;
return !channels.has(channel) && equippedItems.some((item) => item.channel === channel
&& equippedItemMatchesRequirement(item, requirement));
});
}
export interface EnemyDisarmCastRequirement extends EnemyDisarmAbilityRequirement {
readonly attributes3?: number;
readonly passive?: boolean;
readonly createsAura?: boolean;
}
const REQUIRES_MAIN_HAND_WEAPON_ATTRIBUTE3 = 0x00000400;
const REQUIRES_OFF_HAND_WEAPON_ATTRIBUTE3 = 0x01000000;
/**
* Mirrors the player-side source cast gate for a DBC class-2 requirement.
* Main/off-hand attribute checks are strict slot checks. Generic equipment
* checks allow an aura-producing, nonpassive spell to complete when the
* matching weapon remains equipped but Disarm makes it unusable.
*/
export function enemyDisarmBlocksAbilityCast(
auras: readonly ActiveAura[],
targetId: string,
now: number,
requirement: EnemyDisarmCastRequirement,
equippedItems: readonly EnemyDisarmEquippedItem[],
): boolean {
if (requirement.equippedItemClass < 0) return false;
const active = activeEnemyDisarms(auras, targetId, now);
if (active.length === 0) return false;
const matching = equippedItems.filter((item) => equippedItemMatchesRequirement(item, requirement));
if (matching.length === 0) return false;
const attributes3 = requirement.attributes3 ?? 0;
const requiredSlots = [
...((attributes3 & REQUIRES_MAIN_HAND_WEAPON_ATTRIBUTE3) !== 0 ? ["main-hand" as const] : []),
...((attributes3 & REQUIRES_OFF_HAND_WEAPON_ATTRIBUTE3) !== 0 ? ["off-hand" as const] : []),
];
if (requiredSlots.length > 0) {
return requiredSlots.some((slot) => matching.some((item) => item.channel === slot)
&& active.some((disarm) => disarm.channels.has(slot)));
}
const allMatchingItemsDisabled = matching.every((item) => active.some((disarm) => (
disarm.channels.has(item.channel)
)));
if (!allMatchingItemsDisabled) return false;
if (!requirement.passive && requirement.createsAura) return false;
return true;
}