Update Healer Man server

This commit is contained in:
phenom committed 2026-10-04 22:51:21 -04:00
1 parent 6cc1b9217f
commit 6114a2295f
67 files changed
+6241 -347

No files matched your search

@@ -973,7 +973,7 @@ async function spellSupportTables(spellFile, fallbackRoot) {
};
}
function parseSpells(buffer, wantedIds, durations = new Map(), radii = new Map()) {
function parseSpells(buffer, wantedIds, durations = new Map(), radii = new Map(), sourceProfile) {
if (!buffer || buffer.toString("ascii", 0, 4) !== "WDBC") return new Map();
const recordCount = buffer.readUInt32LE(4);
const fieldCount = buffer.readUInt32LE(8);
@@ -1002,11 +1002,13 @@ function parseSpells(buffer, wantedIds, durations = new Map(), radii = new Map()
}
spells.set(id, {
id,
sourceProfile,
name: name.replace(/\|c[0-9a-f]{8}|\|r/gi, "").trim() || `Spell ${id}`,
recoveryTimeMs: unsigned(29),
school: schoolFor(unsigned(225)),
schoolMask: unsigned(225),
damageClass: unsigned(213),
rangeIndex: unsigned(46),
durationMs: durations.get(unsigned(40)) ?? 0,
effects: Array.from({ length: 3 }, (_, effectIndex) => ({
effectType: unsigned(71 + effectIndex),
@@ -1017,6 +1019,7 @@ function parseSpells(buffer, wantedIds, durations = new Map(), radii = new Map()
implicitTargetB: unsigned(shifted(89 + effectIndex)),
radius: radii.get(unsigned(shifted(92 + effectIndex))) ?? 0,
miscValue: signed(shifted(110 + effectIndex)),
periodMs: unsigned(98 + effectIndex),
})).filter((effect) => effect.effectType || effect.auraType),
});
}
@@ -1037,6 +1040,191 @@ const CONTROL_AURA_MECHANICS = Object.freeze(new Map([
[193, "slow"],
]));
const DIRECT_DAMAGE_EFFECT_TYPES = Object.freeze([2, 31, 58, 121]);
const pinnedEffect = (effectType, auraType, basePoints, targetA, targetB, options = {}) => ({
effectType,
auraType,
basePoints,
mechanic: options.mechanic ?? 0,
implicitTargetA: targetA,
implicitTargetB: targetB,
radius: options.radius ?? 0,
miscValue: options.miscValue ?? 0,
periodMs: options.periodMs ?? 0,
});
const pinnedSourceSpell = (rangeIndex, schoolMask, damageClass, durationMs, effects, contract = {}) => {
const { epochSignature, ...attackContract } = contract;
const signature = { rangeIndex, schoolMask, damageClass, durationMs, effects };
return {
signatures: {
ascension: signature,
epoch: epochSignature ?? signature,
},
...attackContract,
};
};
const PINNED_SOURCE_SPELLS = Object.freeze(new Map([
[7164, pinnedSourceSpell(1, 1, 0, 180_000, [pinnedEffect(6, 36, 0, 1, 0, { miscValue: 18 })], { unsupportedUtility: true })],
[7165, pinnedSourceSpell(1, 1, 2, -1, [pinnedEffect(6, 36, 0, 1, 0, { miscValue: 17 })], { unsupportedUtility: true })],
[16244, pinnedSourceSpell(1, 1, 1, 15_000, [pinnedEffect(6, 79, -10, 22, 15, { radius: 10, miscValue: 1 })], {
unsupportedUtility: true,
epochSignature: {
rangeIndex: 1, schoolMask: 1, damageClass: 1, durationMs: 30_000,
effects: [pinnedEffect(6, 99, -20, 22, 15, { radius: 10 })],
},
})],
[13730, pinnedSourceSpell(1, 1, 1, 30_000, [pinnedEffect(6, 166, -15, 22, 15, { radius: 10 })], {
unsupportedUtility: true,
epochSignature: {
rangeIndex: 1, schoolMask: 1, damageClass: 1, durationMs: 30_000,
effects: [pinnedEffect(6, 99, -10, 22, 15, { radius: 10 })],
},
})],
[3419, pinnedSourceSpell(1, 1, 0, 6_000, [pinnedEffect(6, 51, 75, 1, 0)], { unsupportedUtility: true })],
[9128, pinnedSourceSpell(1, 1, 1, 120_000, [pinnedEffect(6, 79, 15, 20, 0, { radius: 20, miscValue: 1 })], {
unsupportedUtility: true,
epochSignature: {
rangeIndex: 1, schoolMask: 1, damageClass: 1, durationMs: 120_000,
effects: [pinnedEffect(6, 99, 4, 20, 0, { radius: 20 })],
},
})],
[674, pinnedSourceSpell(1, 1, 1, 0, [pinnedEffect(40, 0, 1, 1, 0)], { unsupportedUtility: true })],
[29651, pinnedSourceSpell(1, 1, 0, 0, [pinnedEffect(40, 0, 1, 1, 0)], { unsupportedUtility: true })],
[2457, pinnedSourceSpell(1, 1, 0, -1, [
pinnedEffect(6, 36, 0, 1, 0, { miscValue: 17 }),
pinnedEffect(6, 280, 10, 1, 0),
pinnedEffect(6, 286, 100, 1, 0),
], {
unsupportedUtility: true,
epochSignature: {
rangeIndex: 1, schoolMask: 1, damageClass: 2, durationMs: -1,
effects: [
pinnedEffect(6, 36, 0, 1, 0, { miscValue: 17 }),
pinnedEffect(6, 280, 10, 1, 0),
],
},
})],
[3391, pinnedSourceSpell(1, 1, 2, 0, [pinnedEffect(19, 0, 2, 1, 0)], { unsupportedUtility: true })],
[6742, pinnedSourceSpell(4, 8, 1, 30_000, [pinnedEffect(6, 138, 30, 21, 0)], { unsupportedUtility: true })],
[6713, pinnedSourceSpell(2, 1, 2, 5_000, [
pinnedEffect(6, 67, 0, 6, 0),
pinnedEffect(6, 278, 1, 6, 0),
], {
unsupportedUtility: true,
epochSignature: {
rangeIndex: 2, schoolMask: 1, damageClass: 2, durationMs: 5_000,
effects: [pinnedEffect(6, 67, 0, 6, 0)],
},
})],
[17230, pinnedSourceSpell(2, 8, 0, 300_000, [pinnedEffect(6, 14, 6, 6, 0, { miscValue: 1 })], { unsupportedUtility: true })],
[6253, pinnedSourceSpell(2, 1, 2, 2_000, [pinnedEffect(6, 12, 1, 6, 0)], {
target: "primary",
delivery: "melee",
animation: "attack",
range: 5,
})],
[8242, pinnedSourceSpell(2, 1, 2, 2_000, [
pinnedEffect(6, 12, 1, 6, 0),
pinnedEffect(2, 0, 53, 6, 0),
], {
target: "primary",
delivery: "melee",
animation: "attack",
range: 5,
epochSignature: {
rangeIndex: 2, schoolMask: 1, damageClass: 2, durationMs: 2_000,
effects: [
pinnedEffect(6, 12, 1, 6, 0),
pinnedEffect(2, 0, 20, 6, 0),
],
},
})],
[11976, pinnedSourceSpell(2, 1, 2, 0, [pinnedEffect(58, 0, 5, 6, 0)], {
target: "primary",
delivery: "melee",
animation: "attack",
range: 5,
})],
[15657, pinnedSourceSpell(2, 1, 2, 0, [pinnedEffect(2, 0, 100, 6, 0)], {
target: "primary",
delivery: "melee",
animation: "attack",
range: 5,
epochSignature: {
rangeIndex: 2, schoolMask: 1, damageClass: 2, durationMs: 0,
effects: [pinnedEffect(58, 0, 20, 6, 0)],
},
})],
[11978, pinnedSourceSpell(2, 1, 2, 4_000, [pinnedEffect(68, 0, 1, 6, 0, { mechanic: 26 })], {
target: "primary",
delivery: "melee",
animation: "attack",
range: 5,
interruptControl: true,
preserveSourceDamage: true,
epochSignature: {
rangeIndex: 2, schoolMask: 1, damageClass: 2, durationMs: 6_000,
effects: [
pinnedEffect(2, 0, 2, 6, 0),
pinnedEffect(68, 0, 1, 6, 0, { mechanic: 26 }),
],
},
})],
[421, pinnedSourceSpell(4, 8, 1, 0, [pinnedEffect(2, 0, 287, 6, 0)], {
range: 30,
epochSignature: {
rangeIndex: 4, schoolMask: 8, damageClass: 1, durationMs: 0,
effects: [pinnedEffect(2, 0, 191, 6, 0)],
},
})],
[5246, pinnedSourceSpell(137, 1, 1, 5_000, [
pinnedEffect(64, 0, 1, 6, 0),
pinnedEffect(6, 7, 1, 22, 15, { radius: 8 }),
pinnedEffect(6, 31, 25, 22, 15, { radius: 8 }),
], {
target: "nearby-party",
delivery: "area",
range: 8,
radius: 8,
epochSignature: {
rangeIndex: 137, schoolMask: 1, damageClass: 1, durationMs: 8_000,
effects: [
pinnedEffect(64, 0, 1, 6, 0),
pinnedEffect(6, 7, 1, 22, 15, { radius: 8 }),
pinnedEffect(6, 31, 25, 22, 15, { radius: 8 }),
],
},
})],
[19134, pinnedSourceSpell(7, 1, 1, 4_000, [
pinnedEffect(64, 0, 1, 6, 0),
pinnedEffect(6, 7, 1, 22, 15, { radius: 8 }),
pinnedEffect(6, 31, 25, 22, 15, { radius: 8 }),
], {
target: "nearby-party",
delivery: "area",
range: 10,
radius: 8,
epochSignature: {
rangeIndex: 7, schoolMask: 1, damageClass: 1, durationMs: 6_000,
effects: [
pinnedEffect(64, 0, 1, 6, 0),
pinnedEffect(6, 7, 1, 22, 15, { radius: 8 }),
pinnedEffect(6, 31, 25, 22, 15, { radius: 8 }),
],
},
})],
[7964, pinnedSourceSpell(1, 1, 1, 4_000, [
pinnedEffect(6, 12, 1, 22, 15, { mechanic: 12, radius: 5 }),
pinnedEffect(3, 0, 1, 18, 0),
], {
target: "nearby-party",
delivery: "area",
range: 5,
radius: 5,
})],
]));
const AUXILIARY_NON_BOSS_CASTS = Object.freeze(new Set([
"26687:47669", // The stasis orb awakens Gortok; Gortok does not cast this spell.
"29307:54888", // The Colossus does not own the elemental's spawn/reset visual.
@@ -1049,7 +1237,7 @@ function spellSemantic(spell, boss, area) {
? 0
: spell.durationMs || (boss ? 8_000 : 5_000);
const damageMultiplier = area ? (boss ? 0.82 : 0.62) : boss ? 1.35 : 1.12;
const hasDamage = spell.effects.some((effect) => [2, 31, 58, 121].includes(effect.effectType));
const hasDamage = spell.effects.some((effect) => DIRECT_DAMAGE_EFFECT_TYPES.includes(effect.effectType));
const controlEffect = spell.effects.find((effect) => CONTROL_AURA_MECHANICS.has(effect.auraType));
const controlMechanic = /\b(?:sleep|slumber)\b/i.test(spell.name)
? "sleep"
@@ -1164,6 +1352,50 @@ function spellSemantic(spell, boss, area) {
return undefined;
}
function pinnedSourceSpellFor(spell) {
const contract = PINNED_SOURCE_SPELLS.get(spell.id);
if (!contract) return undefined;
const signature = contract.signatures[spell.sourceProfile];
if (!signature) {
throw new Error(
`Pinned Spell.dbc contract for spell ${spell.id} has no ${spell.sourceProfile} source profile.`,
);
}
const scalarFieldsMatch = ["rangeIndex", "schoolMask", "damageClass", "durationMs"]
.every((field) => spell[field] === signature[field]);
const effectsMatch = spell.effects.length === signature.effects.length
&& signature.effects.every((expected, index) => {
const actual = spell.effects[index];
return [
"effectType",
"auraType",
"basePoints",
"mechanic",
"implicitTargetA",
"implicitTargetB",
"radius",
"miscValue",
"periodMs",
].every((field) => actual[field] === expected[field]);
});
if (!scalarFieldsMatch || !effectsMatch) {
const actualSignature = {
rangeIndex: spell.rangeIndex,
schoolMask: spell.schoolMask,
damageClass: spell.damageClass,
durationMs: spell.durationMs,
effects: spell.effects,
};
throw new Error(
`Pinned ${spell.sourceProfile} Spell.dbc contract drift for spell ${spell.id}; `
+ `review its source effects before changing the import. `
+ `Expected ${JSON.stringify(signature)}; found ${JSON.stringify(actualSignature)}.`,
);
}
return contract;
}
function basicAttack(cooldownMs = 1_900) {
return {
id: "basic-attack",
@@ -1179,8 +1411,10 @@ function basicAttack(cooldownMs = 1_900) {
}
function spellAttack(spell, hash, boss) {
const pinnedSource = pinnedSourceSpellFor(spell);
const pinnedHostileMelee = pinnedSource?.delivery === "melee";
const area = /nova|roar|stomp|whirl|rain|volley|blizzard|explosion|quake|storm|breath|cleave|fear|horror|terror/i
.test(spell.name) || spell.id === 53030;
.test(spell.name) || spell.id === 53030 || pinnedSource?.delivery === "area";
const melee = spell.school === "physical"
&& /strike|slash|cleave|bash|smash|rend|bite|claw|pummel/i.test(spell.name);
const healing = /\b(?:heal|healing|mend|renew|rejuvenat\w*|restor\w*|holy light|lay on hands)\b/i
@@ -1202,14 +1436,28 @@ function spellAttack(spell, hash, boss) {
: null;
const aura = /\b(?:enrage|frenzy|shield|reflection|reflect|defensive aura|battle aura|berserker aura|haste|hasten)\b/i
.test(spell.name);
const semanticEffect = spellSemantic(spell, boss, area);
const hasDamage = spell.effects.some((entry) => DIRECT_DAMAGE_EFFECT_TYPES.includes(entry.effectType));
const defaultDamageMultiplier = area ? (boss ? 0.82 : 0.62) : boss ? 1.35 : 1.12;
const semanticEffect = pinnedSource?.interruptControl
? {
kind: "control",
mechanic: "interrupt",
durationMs: spell.durationMs,
...(pinnedSource.preserveSourceDamage && hasDamage
? { damageMultiplier: defaultDamageMultiplier }
: {}),
}
: pinnedSource?.effect ?? spellSemantic(spell, boss, area);
if (pinnedSource?.unsupportedUtility && !semanticEffect && !hasDamage) return null;
const semanticEffects = semanticEffect?.kind === "composite"
? semanticEffect.effects
: semanticEffect ? [semanticEffect] : [];
const semanticHas = (kind) => semanticEffects.some((entry) => entry.kind === kind);
const target = healing
? "lowest-health-friendly"
: semanticHas("aura") || aura
const target = pinnedSource?.target
? pinnedSource.target
: healing
? "lowest-health-friendly"
: semanticHas("aura") || aura
? "self"
: semanticEffects.length > 0 && semanticEffects.every((entry) => entry.kind === "summon")
? "self"
@@ -1245,7 +1493,7 @@ function spellAttack(spell, hash, boss) {
id: `spell-${spell.id}`,
name: spell.name,
spellId: spell.id,
delivery: target === "self" ? "area" : area ? "area" : melee ? "melee" : "projectile",
delivery: pinnedSource?.delivery ?? (target === "self" ? "area" : area ? "area" : melee ? "melee" : "projectile"),
target,
school: spell.school,
schoolMask: spell.schoolMask,
@@ -1254,11 +1502,11 @@ function spellAttack(spell, hash, boss) {
...(spell.damageClass === 1 ? { attackKind: "spell" }
: spell.damageClass === 2 ? { attackKind: "melee" }
: spell.damageClass === 3 ? { attackKind: "ranged" } : {}),
animation: melee ? "attack" : "cast",
range: target === "self" ? 0 : melee ? 5.5 : area ? 22 : 28,
animation: pinnedSource?.animation ?? (pinnedHostileMelee || melee ? "attack" : "cast"),
range: pinnedSource?.range ?? (target === "self" ? 0 : melee ? 5.5 : area ? 22 : 28),
cooldownMs: Math.max(2_500, Math.min(12_000, spell.recoveryTimeMs || 4_000 + hash % 3_500)),
damageMultiplier: effect ? 0 : area ? (boss ? 0.82 : 0.62) : boss ? 1.35 : 1.12,
...(area ? { radius: dbcRadius || (boss ? 11 : 8) } : {}),
...(area ? { radius: pinnedSource?.radius ?? (dbcRadius || (boss ? 11 : 8)) } : {}),
...(effect ? { effect } : {}),
...(healing
? {
@@ -1405,15 +1653,20 @@ function smartAiMechanicsFor(entity, boss, spells, worldAi) {
if (!spell) continue;
const hash = stableHash(`${entry}:${spellId}:${action.id}`);
const attack = spellAttack(spell, hash, true);
const target = [5, 6].includes(Number(action.target_type))
? "random-party"
: attack.target;
if (!attack) continue;
const targetType = Number(action.target_type);
const pinnedSource = PINNED_SOURCE_SPELLS.get(spellId);
const sourceTarget = pinnedSource?.target;
const target = sourceTarget
?? ([5, 6].includes(targetType) ? "random-party" : attack.target);
mechanics.push({
...attack,
...event,
id: `smart-${entry}-${action.id}-${slugPart(spell.name)}`,
target,
range: target === "self" ? attack.range : Math.max(attack.range, 28),
range: pinnedSource?.range !== undefined || target === "self"
? attack.range
: Math.max(attack.range, 28),
source,
});
} else if (actionType === 12) {
@@ -1526,7 +1779,8 @@ function combatFor(
: null;
})
.filter(Boolean)
.map((spell) => spellAttack(spell, hash, boss));
.map((spell) => spellAttack(spell, hash, boss))
.filter(Boolean);
const importedMechanics = smartAiMechanicsFor(entity, boss, spells, worldAi);
const authored = boss ? bossMechanics?.bosses?.[String(entry)] : undefined;
let mechanics = authored?.mode === "replace"
@@ -1814,6 +2068,7 @@ async function compile() {
wantedSpellIds,
ascensionSpellSupport.durations,
ascensionSpellSupport.radii,
"ascension",
)
: new Map();
const epochSpellSupport = await spellSupportTables(epochSpellDbc, defaultSpellSupportRoot);
@@ -1823,6 +2078,7 @@ async function compile() {
wantedSpellIds,
epochSpellSupport.durations,
epochSpellSupport.radii,
"epoch",
)
: ascensionSpells;
const standardGameObjectDisplays = await exists(standardGameObjectDisplayDbc)