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

@@ -1003,13 +1003,37 @@ function parseSpells(buffer, wantedIds, durations = new Map(), radii = new Map()
spells.set(id, {
id,
sourceProfile,
spellMechanic: unsigned(3),
attributes0: unsigned(4),
attributes1: unsigned(5),
attributes2: unsigned(6),
attributes3: unsigned(7),
attributes4: unsigned(8),
attributes5: unsigned(9),
attributes6: unsigned(10),
attributes7: unsigned(11),
stanceMask0: unsigned(12),
stanceMask1: unsigned(13),
stanceExcludeMask0: unsigned(14),
stanceExcludeMask1: unsigned(15),
equippedItemClass: signed(68),
equippedItemSubclassMask: unsigned(69),
equippedItemInventoryMask: unsigned(70),
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),
maxAffectedTargets: unsigned(212),
rangeIndex: unsigned(46),
durationMs: durations.get(unsigned(40)) ?? 0,
durationIndex: unsigned(40),
baseLevel: unsigned(38),
spellLevel: unsigned(39),
maxLevel: unsigned(37),
stackAmount: unsigned(49),
scalesWithCreatureLevel: (unsigned(4) & 0x00080000) !== 0,
dispel: unsigned(2),
effects: Array.from({ length: 3 }, (_, effectIndex) => ({
effectType: unsigned(71 + effectIndex),
auraType: unsigned(shifted(95 + effectIndex)),
@@ -1018,8 +1042,15 @@ function parseSpells(buffer, wantedIds, durations = new Map(), radii = new Map()
implicitTargetA: unsigned(shifted(86 + effectIndex)),
implicitTargetB: unsigned(shifted(89 + effectIndex)),
radius: radii.get(unsigned(shifted(92 + effectIndex))) ?? 0,
radiusIndex: unsigned(shifted(92 + effectIndex)),
miscValue: signed(shifted(110 + effectIndex)),
triggerSpellId: unsigned(shifted(116 + effectIndex)),
periodMs: unsigned(98 + effectIndex),
chainTargets: unsigned(shifted(104 + effectIndex)),
chainAmplitude: buffer.readFloatLE(base + shifted(216 + effectIndex) * 4),
chainFromCaster: (unsigned(6) & 0x00001000) !== 0,
dieSides: signed(74 + effectIndex),
pointsPerLevel: buffer.readFloatLE(base + shifted(77 + effectIndex) * 4),
})).filter((effect) => effect.effectType || effect.auraType),
});
}
@@ -1053,44 +1084,131 @@ const pinnedEffect = (effectType, auraType, basePoints, targetA, targetB, option
periodMs: options.periodMs ?? 0,
});
const pinnedSourceSpell = (rangeIndex, schoolMask, damageClass, durationMs, effects, contract = {}) => {
const { epochSignature, ...attackContract } = contract;
const signature = { rangeIndex, schoolMask, damageClass, durationMs, effects };
const { epochSignature, sourceScalars, ...attackContract } = contract;
const signature = { rangeIndex, schoolMask, damageClass, durationMs, effects, ...sourceScalars };
return {
signatures: {
ascension: signature,
epoch: epochSignature ?? signature,
epoch: epochSignature ? { ...signature, ...epochSignature } : signature,
},
...attackContract,
};
};
const shoutSourceScalars = Object.freeze({
spellMechanic: 5, attributes0: 0, attributes1: 0, attributes2: 0,
attributes3: 0, attributes4: 0, attributes5: 0, attributes6: 0, attributes7: 0,
stackAmount: 0, dispel: 0, maxAffectedTargets: 0,
});
const stanceSourceScalars = Object.freeze({
attributes0: 327_696, dispel: 0, stackAmount: 0,
stanceMask0: 0, stanceMask1: 0, stanceExcludeMask0: 0, stanceExcludeMask1: 0,
equippedItemClass: -1, equippedItemSubclassMask: 0, equippedItemInventoryMask: 0,
});
const stancePassiveScalars = Object.freeze({ ...stanceSourceScalars, attributes0: 327_888, durationIndex: 0 });
const stanceEffect = (auraType, basePoints, miscValue) => ({
...pinnedEffect(6, auraType, basePoints, 1, 0, { miscValue }), triggerSpellId: 0, radiusIndex: 0,
});
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 })],
[8599, pinnedSourceSpell(1, 1, 0, 120_000, [
pinnedEffect(6, 79, 30, 1, 0, { miscValue: 1 }), pinnedEffect(6, 138, 30, 1, 0),
], {
sourceAura: true, target: "self", delivery: "area", range: 0,
sourceScalars: { stackAmount: 0, dispel: 9 },
epochSignature: { rangeIndex: 1, schoolMask: 1, damageClass: 0, durationMs: 120_000,
effects: [pinnedEffect(6, 79, 10, 1, 0, { miscValue: 1 }), pinnedEffect(6, 138, 30, 1, 0)] },
})],
[6547, pinnedSourceSpell(2, 1, 2, 15_000, [{ ...pinnedEffect(6, 3, 14, 6, 0, { mechanic: 15, periodMs: 3_000 }), dieSides: 1, pointsPerLevel: 0 }], {
target: "primary", delivery: "melee", animation: "attack", range: 5, periodicDamage: true, preserveDamageMultiplier: true,
sourceScalars: { baseLevel: 20, spellLevel: 20, maxLevel: 0, stackAmount: 0, scalesWithCreatureLevel: false },
epochSignature: { rangeIndex: 2, schoolMask: 1, damageClass: 2, durationMs: 15_000,
effects: [{ ...pinnedEffect(6, 3, 10, 6, 0, { mechanic: 15, periodMs: 3_000 }), dieSides: 1, pointsPerLevel: 0 }] },
})],
[16509, pinnedSourceSpell(2, 1, 2, 15_000, [{ ...pinnedEffect(6, 3, 27, 6, 0, { mechanic: 15, periodMs: 3_000 }), dieSides: 1, pointsPerLevel: 0 }], {
target: "primary", delivery: "melee", animation: "attack", range: 5, periodicDamage: true, preserveDamageMultiplier: true,
sourceScalars: { baseLevel: 20, spellLevel: 20, maxLevel: 0, stackAmount: 0, scalesWithCreatureLevel: true },
})],
[7164, pinnedSourceSpell(1, 1, 0, 180_000, [stanceEffect(36, 0, 18)], {
sourceAura: true, target: "self", delivery: "area", range: 0, radius: 0,
sourceScalars: { ...stanceSourceScalars, durationIndex: 25 },
})],
[7165, pinnedSourceSpell(1, 1, 2, -1, [stanceEffect(36, 0, 17)], {
sourceAura: true, target: "self", delivery: "area", range: 0, radius: 0,
sourceScalars: { ...stanceSourceScalars, durationIndex: 21 },
})],
// Core HandleShapeshiftBoosts owns these child passives. Pin them even though
// they are not imported as independently cast attacks.
[7376, pinnedSourceSpell(1, 1, 0, 0, [
stanceEffect(87, -10, 127), stanceEffect(79, -5, 127), stanceEffect(4, 42, 127),
], { sourceScalars: stancePassiveScalars,
epochSignature: { rangeIndex: 1, schoolMask: 1, damageClass: 0, durationMs: 0,
effects: [stanceEffect(87, -10, 127), stanceEffect(79, -5, 127), stanceEffect(10, 45, 127)] },
})],
[21156, pinnedSourceSpell(1, 1, 0, 0, [stanceEffect(10, 0, 127), stanceEffect(280, 0, 0)], {
sourceScalars: stancePassiveScalars,
epochSignature: { rangeIndex: 1, schoolMask: 1, damageClass: 0, durationMs: 0,
effects: [stanceEffect(10, -20, 127)] },
})],
[16244, pinnedSourceSpell(1, 1, 1, 15_000, [pinnedEffect(6, 79, -10, 22, 15, { radius: 10, miscValue: 1 })], {
unsupportedUtility: true,
sourceAura: true, target: "nearby-party", delivery: "area", range: 10, radius: 10,
sourceScalars: { stackAmount: 0, dispel: 0 },
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,
sourceAura: true, target: "nearby-party", delivery: "area", range: 10, radius: 10,
sourceScalars: { stackAmount: 0, dispel: 0 },
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 })],
[3419, pinnedSourceSpell(1, 1, 0, 6_000, [pinnedEffect(6, 51, 75, 1, 0)], {
sourceAura: true, target: "self", delivery: "area", range: 0, radius: 0,
sourceScalars: { stackAmount: 0, dispel: 0 },
})],
[9128, pinnedSourceSpell(1, 1, 1, 120_000, [pinnedEffect(6, 79, 15, 20, 0, { radius: 20, miscValue: 1 })], {
unsupportedUtility: true,
sourceAura: true, target: "nearby-friendly", delivery: "area", range: 0, radius: 20,
sourceScalars: { stackAmount: 0, dispel: 0 },
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 })],
[674, pinnedSourceSpell(1, 1, 1, 0, [pinnedEffect(40, 0, 1, 1, 0)], {
target: "self", delivery: "area", range: 0, radius: 0,
weaponCapability: "dual-wield",
sourceScalars: { attributes0: 336 },
epochSignature: { attributes0: 80 },
})],
...[
[47294, 4, 8, 20, 5],
[47295, 7, 16, 40, 10],
[47296, 10, 25, 60, 15],
].map(([id, criticalChanceModifier, effectModifier, epochChance, epochModifier]) => [id,
pinnedSourceSpell(1, 1, 0, 0, [
pinnedEffect(6, 253, 100, 1, 0),
pinnedEffect(6, 107, criticalChanceModifier, 1, 0, { miscValue: 7 }),
pinnedEffect(6, 107, effectModifier, 1, 0, { miscValue: 3 }),
], {
// Duration index 0 is serialized as 0 here; the guarded passive import is permanent.
sourceAura: true, target: "self", delivery: "area", range: 0, radius: 0,
sourceScalars: { attributes0: 464, dispel: 0, stackAmount: 0, durationIndex: 0 },
epochSignature: { effects: [
pinnedEffect(6, 253, epochChance, 1, 0),
pinnedEffect(6, 107, epochModifier, 1, 0, { miscValue: 7 }),
] },
}),
]),
[29651, pinnedSourceSpell(1, 1, 0, 0, [pinnedEffect(40, 0, 1, 1, 0)], {
target: "self", delivery: "area", range: 0, radius: 0,
weaponCapability: "dual-wield",
sourceScalars: { attributes0: 80 },
})],
[2457, pinnedSourceSpell(1, 1, 0, -1, [
pinnedEffect(6, 36, 0, 1, 0, { miscValue: 17 }),
pinnedEffect(6, 280, 10, 1, 0),
@@ -1105,19 +1223,29 @@ const PINNED_SOURCE_SPELLS = Object.freeze(new Map([
],
},
})],
[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 })],
[3391, pinnedSourceSpell(1, 1, 2, 0, [{ ...pinnedEffect(19, 0, 2, 1, 0), dieSides: 1 }], {
target: "self", delivery: "area", range: 0, radius: 0, extraAttackCount: 2,
sourceScalars: { attributes0: 262_160 },
})],
[6742, pinnedSourceSpell(4, 8, 1, 30_000, [pinnedEffect(6, 138, 30, 21, 0)], {
sourceAura: true, target: "selected-friendly", delivery: "area", range: 30, radius: 0,
sourceScalars: { stackAmount: 0, dispel: 1 },
})],
[6713, pinnedSourceSpell(2, 1, 2, 5_000, [
pinnedEffect(6, 67, 0, 6, 0),
pinnedEffect(6, 278, 1, 6, 0),
], {
unsupportedUtility: true,
sourceAura: true, target: "primary", delivery: "melee", range: 5, radius: 0,
sourceScalars: { stackAmount: 0, dispel: 0 },
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 })],
[17230, pinnedSourceSpell(2, 8, 0, 300_000, [pinnedEffect(6, 14, 6, 6, 0, { miscValue: 1 })], {
sourceAura: true, target: "primary", delivery: "melee", range: 5, radius: 0,
sourceScalars: { stackAmount: 0, dispel: 3 },
})],
[6253, pinnedSourceSpell(2, 1, 2, 2_000, [pinnedEffect(6, 12, 1, 6, 0)], {
target: "primary",
delivery: "melee",
@@ -1171,36 +1299,45 @@ const PINNED_SOURCE_SPELLS = Object.freeze(new Map([
],
},
})],
[421, pinnedSourceSpell(4, 8, 1, 0, [pinnedEffect(2, 0, 287, 6, 0)], {
[421, pinnedSourceSpell(4, 8, 1, 0, [{ ...pinnedEffect(2, 0, 287, 6, 0), chainTargets: 3, chainAmplitude: Math.fround(0.6), chainFromCaster: false }], {
range: 30,
chain: { targetCount: 3, jumpRange: 10, damageMultiplier: 0.6 },
epochChain: { targetCount: 3, jumpRange: 10, damageMultiplier: 0.7 },
epochSignature: {
rangeIndex: 4, schoolMask: 8, damageClass: 1, durationMs: 0,
effects: [pinnedEffect(2, 0, 191, 6, 0)],
effects: [{ ...pinnedEffect(2, 0, 191, 6, 0), chainTargets: 3, chainAmplitude: Math.fround(0.7), chainFromCaster: false }],
},
})],
[5246, pinnedSourceSpell(137, 1, 1, 5_000, [
pinnedEffect(64, 0, 1, 6, 0),
{ ...pinnedEffect(64, 0, 1, 6, 0), triggerSpellId: 20511 },
pinnedEffect(6, 7, 1, 22, 15, { radius: 8 }),
pinnedEffect(6, 31, 25, 22, 15, { radius: 8 }),
], {
triggeredShout: true,
sourceScalars: { ...shoutSourceScalars, attributes6: 33_554_432, maxAffectedTargets: 5 },
target: "nearby-party",
delivery: "area",
range: 8,
radius: 8,
epochSignature: {
rangeIndex: 137, schoolMask: 1, damageClass: 1, durationMs: 8_000,
attributes1: 136,
attributes0: 327_696,
attributes6: 8_388_608,
effects: [
pinnedEffect(64, 0, 1, 6, 0),
{ ...pinnedEffect(64, 0, 1, 6, 0), triggerSpellId: 20511 },
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(64, 0, 1, 6, 0), triggerSpellId: 29544 },
pinnedEffect(6, 7, 1, 22, 15, { radius: 8 }),
pinnedEffect(6, 31, 25, 22, 15, { radius: 8 }),
], {
triggeredShout: true,
sourceScalars: { ...shoutSourceScalars, attributes0: 327_696 },
target: "nearby-party",
delivery: "area",
range: 10,
@@ -1208,12 +1345,27 @@ const PINNED_SOURCE_SPELLS = Object.freeze(new Map([
epochSignature: {
rangeIndex: 7, schoolMask: 1, damageClass: 1, durationMs: 6_000,
effects: [
pinnedEffect(64, 0, 1, 6, 0),
{ ...pinnedEffect(64, 0, 1, 6, 0), triggerSpellId: 29544 },
pinnedEffect(6, 7, 1, 22, 15, { radius: 8 }),
pinnedEffect(6, 31, 25, 22, 15, { radius: 8 }),
],
},
})],
[20511, pinnedSourceSpell(11, 1, 1, 5_000, [
pinnedEffect(6, 12, 0, 6, 0),
pinnedEffect(6, 87, 15, 6, 0, { miscValue: 1 }),
], {
sourceScalars: { ...shoutSourceScalars, attributes3: 589_824, attributes4: 8_388_608 },
epochSignature: {
rangeIndex: 11, schoolMask: 1, damageClass: 0, durationMs: 8_000,
attributes3: 0, attributes4: 0,
effects: [pinnedEffect(6, 12, 0, 6, 0)],
},
})],
[29544, pinnedSourceSpell(11, 1, 0, 3_000, [pinnedEffect(6, 7, 0, 6, 0)], {
sourceScalars: shoutSourceScalars,
epochSignature: { durationMs: 6_000 },
})],
[7964, pinnedSourceSpell(1, 1, 1, 4_000, [
pinnedEffect(6, 12, 1, 22, 15, { mechanic: 12, radius: 5 }),
pinnedEffect(3, 0, 1, 18, 0),
@@ -1362,7 +1514,7 @@ function pinnedSourceSpellFor(spell) {
`Pinned Spell.dbc contract for spell ${spell.id} has no ${spell.sourceProfile} source profile.`,
);
}
const scalarFieldsMatch = ["rangeIndex", "schoolMask", "damageClass", "durationMs"]
const scalarFieldsMatch = Object.keys(signature).filter((field) => field !== "effects")
.every((field) => spell[field] === signature[field]);
const effectsMatch = spell.effects.length === signature.effects.length
&& signature.effects.every((expected, index) => {
@@ -1377,16 +1529,11 @@ function pinnedSourceSpellFor(spell) {
"radius",
"miscValue",
"periodMs",
].every((field) => actual[field] === expected[field]);
].every((field) => actual[field] === expected[field])
&& ["chainTargets", "chainAmplitude", "chainFromCaster", "dieSides", "pointsPerLevel", "triggerSpellId", "radiusIndex"].every((field) => expected[field] === undefined || actual[field] === expected[field]);
});
if (!scalarFieldsMatch || !effectsMatch) {
const actualSignature = {
rangeIndex: spell.rangeIndex,
schoolMask: spell.schoolMask,
damageClass: spell.damageClass,
durationMs: spell.durationMs,
effects: spell.effects,
};
const actualSignature = Object.fromEntries(Object.keys(signature).map((field) => [field, spell[field]]));
throw new Error(
`Pinned ${spell.sourceProfile} Spell.dbc contract drift for spell ${spell.id}; `
+ `review its source effects before changing the import. `
@@ -1438,7 +1585,22 @@ function spellAttack(spell, hash, boss) {
.test(spell.name);
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
const semanticEffect = pinnedSource?.weaponCapability
? { kind: "weapon-capability", capability: pinnedSource.weaponCapability,
sourceSpellId: spell.id, sourceProfile: spell.sourceProfile }
: pinnedSource?.extraAttackCount
? { kind: "extra-attacks", sourceSpellId: spell.id,
sourceProfile: spell.sourceProfile, count: pinnedSource.extraAttackCount }
: pinnedSource?.triggeredShout
? { kind: "triggered-shout", sourceSpellId: spell.id, sourceProfile: spell.sourceProfile }
: pinnedSource?.sourceAura
? { kind: ["nearby-party", "primary"].includes(pinnedSource.target) ? "target-aura" : "aura", aura: `spell-${spell.id}`,
durationMs: spell.durationMs < 0 ? 0 : spell.durationMs,
wowAuraSource: { spellId: spell.id, sourceProfile: spell.sourceProfile } }
: pinnedSource?.periodicDamage
? { kind: "dot", multiplier: 0, ticks: 5, intervalMs: 3_000,
wowPeriodicSource: { spellId: spell.id, sourceProfile: spell.sourceProfile === "epoch" ? "epoch-client" : "ascension-patch-d" } }
: pinnedSource?.interruptControl
? {
kind: "control",
mechanic: "interrupt",
@@ -1505,7 +1667,8 @@ function spellAttack(spell, hash, boss) {
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,
damageMultiplier: effect && !pinnedSource?.preserveDamageMultiplier ? 0 : area ? (boss ? 0.82 : 0.62) : boss ? 1.35 : 1.12,
...(pinnedSource?.chain ? { chain: spell.sourceProfile === "epoch" ? pinnedSource.epochChain : pinnedSource.chain } : {}),
...(area ? { radius: pinnedSource?.radius ?? (dbcRadius || (boss ? 11 : 8)) } : {}),
...(effect ? { effect } : {}),
...(healing
@@ -1574,13 +1737,31 @@ function validateBossMechanicsCatalog(catalog) {
}
function smartAiEventProperties(row) {
const reviewedSelfAura = Number(row.action_type) === 11
&& [7164, 7165, 3419, 6742, 674, 29651].includes(Number(row.action_param1))
&& Number(row.target_type) === 1;
const reviewedOnce = reviewedSelfAura && (Number(row.event_flags) & 1) !== 0
? { maximumCasts: 1 } : {};
const weaponCapability = [674, 29651].includes(Number(row.action_param1));
if (row.event_type === 1 && reviewedSelfAura) {
return {
activation: "out-of-combat",
initialCooldownMs: [Number(row.event_param1) || 0, Number(row.event_param2) || Number(row.event_param1) || 0],
repeatCooldownMs: weaponCapability ? [Number(row.event_param3) || 0, Number(row.event_param4) || 0]
: [Math.max(1, Number(row.event_param3) || 1_000), Math.max(1, Number(row.event_param4) || Number(row.event_param3) || 1_000)],
cooldownMs: Math.max(1_000, Number(row.event_param3) || 1_000),
...reviewedOnce,
};
}
if (row.event_type === 0) {
const initial = [Number(row.event_param1) || 0, Number(row.event_param2) || Number(row.event_param1) || 0];
const repeat = [Number(row.event_param3) || 1_000, Number(row.event_param4) || Number(row.event_param3) || 1_000];
const repeat = weaponCapability ? [Number(row.event_param3) || 0, Number(row.event_param4) || 0]
: [Number(row.event_param3) || 1_000, Number(row.event_param4) || Number(row.event_param3) || 1_000];
return {
initialCooldownMs: initial,
repeatCooldownMs: repeat,
cooldownMs: Math.max(1_000, Math.round((repeat[0] + repeat[1]) / 2)),
...reviewedOnce,
};
}
if (row.event_type === 2) {
@@ -1604,6 +1785,17 @@ function smartAiEventProperties(row) {
cooldownMs: 1_000,
};
}
if (row.event_type === 16 && Number(row.action_type) === 11
&& Number(row.action_param1) === 6742 && Number(row.target_type) === 7) {
return {
activation: Number(row.event_param5) ? undefined : "always",
initialCooldownMs: [0, 0],
repeatCooldownMs: [Number(row.event_param3) || 10_000, Number(row.event_param4) || 10_000],
cooldownMs: Number(row.event_param3) || 10_000,
condition: { kind: "friendly-missing-aura", spellId: Number(row.event_param1),
range: Number(row.event_param2), onlyInCombat: Boolean(Number(row.event_param5)) },
};
}
if (row.event_type === 14) {
return {
initialCooldownMs: [0, 0],
@@ -1620,9 +1812,11 @@ function smartAiEventProperties(row) {
}
function smartAiMechanicsFor(entity, boss, spells, worldAi) {
if (!boss) return [];
const entry = Number(entity.entry);
const rows = worldAi?.smartScriptsByEntry?.[String(entry)] ?? [];
const rows = (worldAi?.smartScriptsByEntry?.[String(entry)] ?? []).filter((row) => boss
|| Number(row.action_type) === 11 && (Number(row.target_type) === 1
&& [7164, 7165, 3419].includes(Number(row.action_param1))
|| [6742, 6713, 17230, 674, 29651, 3391].includes(Number(row.action_param1))));
const rowsById = new Map(rows.map((row) => [Number(row.id), row]));
const mechanics = [];
for (const trigger of rows.filter((row) => Number(row.event_type) !== 61)) {
@@ -1645,6 +1839,7 @@ function smartAiMechanicsFor(entity, boss, spells, worldAi) {
eventType: Number(trigger.event_type) || 0,
actionType,
targetType: Number(action.target_type) || 0,
...(Number(action.action_type) === 11 ? { castFlags: Number(action.action_param2) || 0 } : {}),
comment: String(action.comment ?? trigger.comment ?? ""),
};
if (actionType === 11) {
@@ -1657,14 +1852,14 @@ function smartAiMechanicsFor(entity, boss, spells, worldAi) {
const targetType = Number(action.target_type);
const pinnedSource = PINNED_SOURCE_SPELLS.get(spellId);
const sourceTarget = pinnedSource?.target;
const target = sourceTarget
const target = spellId === 6742 && targetType === 1 ? "self" : sourceTarget
?? ([5, 6].includes(targetType) ? "random-party" : attack.target);
mechanics.push({
...attack,
...event,
id: `smart-${entry}-${action.id}-${slugPart(spell.name)}`,
target,
range: pinnedSource?.range !== undefined || target === "self"
range: target === "self" ? 0 : pinnedSource?.range !== undefined
? attack.range
: Math.max(attack.range, 28),
source,
@@ -1728,7 +1923,10 @@ function uniqueMechanics(mechanics) {
const seenSpellIds = new Set();
return mechanics.filter((mechanic) => {
if (seenIds.has(mechanic.id)) return false;
if (mechanic.spellId && seenSpellIds.has(mechanic.spellId)) return false;
// Hamhock has separate self-once and selected-friendly missing-buff events.
// Keep each sourced event while still suppressing the generic template cast.
if (mechanic.spellId && seenSpellIds.has(mechanic.spellId)
&& !(mechanic.spellId === 6742 && mechanic.source?.actionType === 11)) return false;
seenIds.add(mechanic.id);
if (mechanic.spellId) seenSpellIds.add(mechanic.spellId);
return true;
@@ -2039,7 +2237,7 @@ async function compile() {
runtimeOverrides.set(recipe.slug, override);
}
const drafts = new Map();
const wantedSpellIds = new Set();
const wantedSpellIds = new Set([7164, 7165, 7376, 21156, 3419, 5246, 19134, 20511, 29544, 47294, 47295, 47296]);
for (const recipe of recipes) {
const file = path.join(workRoot, recipe.slug, "runtime-draft.json");
if (!await exists(file)) continue;
@@ -2081,6 +2279,15 @@ async function compile() {
"epoch",
)
: ascensionSpells;
for (const [profile, spells] of [["ascension", ascensionSpells], ["epoch", epochSpells]]) {
for (const spellId of [7164, 7165, 7376, 21156, 3419, 5246, 19134, 20511, 29544, 47294, 47295, 47296]) {
const spell = spells.get(spellId);
if (!spell || spell.sourceProfile !== profile) {
throw new Error(`Missing pinned ${profile} stance/block source spell ${spellId}.`);
}
pinnedSourceSpellFor(spell);
}
}
const standardGameObjectDisplays = await exists(standardGameObjectDisplayDbc)
? parseGameObjectDisplays(await readFile(standardGameObjectDisplayDbc))
: new Map();
@@ -2361,6 +2568,7 @@ async function compile() {
members: [{
id: "member",
entityId: spawn.entityId,
...(Number.isSafeInteger(spawn.guid) && spawn.guid > 0 ? { sourceCreatureGuid: spawn.guid } : {}),
role: "leader",
formationOffset: [0, 0, 0],
}],
@@ -2371,6 +2579,7 @@ async function compile() {
staticSpawns.push({
id: spawn.id,
entityId: spawn.entityId,
...(Number.isSafeInteger(spawn.guid) && spawn.guid > 0 ? { sourceCreatureGuid: spawn.guid } : {}),
position: transformedPosition,
yaw: spawn.yaw,
spawnMask: spawn.spawnMask,