Update Healer Man server

This commit is contained in:
phenom committed 2026-09-23 16:14:19 -04:00
1 parent f90ba7d989
commit 189b94d6b7
51 files changed
+3448 -154

No files matched your search

+105 -19
View File
@@ -171,7 +171,7 @@ function auraModifiers(effect: Wow335Effect): readonly AuraModifier[] {
case 13: return [{ kind: "damage", direction: "dealt", school: schoolFromMask(effect.miscValue), operation: "flat", value: effect.basePoints }];
case 14: return [{ kind: "damage", direction: "taken", school: schoolFromMask(effect.miscValue), operation: "flat", value: effect.basePoints }];
case 20: return flatStat("health-regeneration");
case 22:
case 22: return effect.miscValue === 1 ? flatStat("armor") : flatStat(resistanceStat);
case 83:
case 123:
case 143: return flatStat(resistanceStat);
@@ -210,13 +210,13 @@ function auraModifiers(effect: Wow335Effect): readonly AuraModifier[] {
case 49: return percentStat("dodge");
case 50: return percentStat("critical-healing");
case 51: return percentStat("block");
case 52:
case 187:
case 188:
case 197: return percentStat("critical-strike");
case 54:
case 184:
case 185: return percentStat("hit");
case 52: return percentStat("critical-strike");
case 187: return [{ kind: "stat", stat: "incoming-melee-critical-strike", operation: "flat", value: effect.basePoints }];
case 188: return [{ kind: "stat", stat: "incoming-ranged-critical-strike", operation: "flat", value: effect.basePoints }];
case 197: return [{ kind: "stat", stat: "incoming-critical-strike", operation: "flat", value: effect.basePoints }];
case 54: return percentStat("hit");
case 184: return [{ kind: "stat", stat: "incoming-melee-hit", operation: "flat", value: effect.basePoints }];
case 185: return [{ kind: "stat", stat: "incoming-ranged-hit", operation: "flat", value: effect.basePoints }];
case 55:
case 186:
case 199: return percentStat("spell-hit");
@@ -447,6 +447,14 @@ function namedAuraForEffect(
const school = schoolFromMask(effect.miscValue);
const auraId = `wow335:${rank.spellId}:effect:${effectIndex}:aura:${effect.auraType}`;
const modifiers = [...auraModifiers(effect)];
if (abilityName === "Inner Fire") {
for (const [index, modifier] of modifiers.entries()) modifiers[index] = { ...modifier, perStack: false };
}
if (abilityName === "Molten Armor" && effect.auraType === 220) {
modifiers.splice(0, modifiers.length, {
kind: "stat", stat: "spirit-to-spell-crit-rating", operation: "flat", value: effect.basePoints / 100,
});
}
if (abilityName === "Greater Blessing of Sanctuary" && effect.auraType === 4) {
modifiers.push(
{ kind: "damage", direction: "taken", operation: "percent", value: -0.03 },
@@ -623,8 +631,9 @@ function namedAuraForEffect(
? { hideFromAuraStrip: true }
: {}),
durationMs,
maxStacks: 1,
stackBehavior: "refresh",
maxStacks: abilityName === "Inner Fire" ? 20 : 1,
...(abilityName === "Inner Fire" ? { initialStacks: 20 } : {}),
stackBehavior: abilityName === "Inner Fire" ? "replace" : "refresh",
...(exclusiveGroupForAbility(classId, abilityName) || abilityName === "Light's Beacon"
? { exclusiveGroup: abilityName === "Light's Beacon" ? "paladin:beacon" : exclusiveGroupForAbility(classId, abilityName) }
: {}),
@@ -758,10 +767,36 @@ function classScriptEffects(
case "warrior:Execute": return [{ kind: "execute", coefficient: 1.8 }];
case "warrior:Charge": return [{ kind: "status", status: "stun", durationMs: 1_500 }];
case "warrior:Intercept": return [{ kind: "status", status: "stun", durationMs: 3_000 }];
case "warrior:Devastate": return [
{
kind: "damage",
coefficient: 1,
bonusPerSunderStack: rank.effects.find((effect) => effect.effectType === 121)?.basePoints ?? 0,
},
{
kind: "apply-aura",
aura: {
id: "wow335:warrior:sunder-armor", name: "Sunder Armor", disposition: "debuff", durationMs: 30_000,
maxStacks: 5, stackBehavior: "add",
modifiers: [{ kind: "damage", direction: "taken", school: "physical", operation: "percent", value: 0.04, perStack: true }],
},
sourceEffectType: 6, sourceAuraType: 4, miscValue: 0, triggerSpellId: 0,
},
];
case "warrior:Last Stand": return [{
kind: "apply-aura",
recipient: "caster",
aura: {
id: "wow335:warrior:last-stand", name: abilityName, disposition: "buff",
durationMs: 20_000, maxStacks: 1, stackBehavior: "refresh",
modifiers: [{ kind: "stat", stat: "maximum-health", operation: "percent", value: 0.3 }],
},
sourceEffectType: 6, sourceAuraType: 133, miscValue: 0, triggerSpellId: 0,
}];
case "warrior:Sunder Armor": return [{
kind: "apply-aura",
aura: {
id: `wow335:${rank.spellId}:sunder-armor`, name: abilityName, disposition: "debuff", durationMs: 30_000,
id: "wow335:warrior:sunder-armor", name: abilityName, disposition: "debuff", durationMs: 30_000,
maxStacks: 5, stackBehavior: "add",
modifiers: [{ kind: "damage", direction: "taken", school: "physical", operation: "percent", value: 0.04, perStack: true }],
},
@@ -793,6 +828,11 @@ function classScriptEffects(
case "hunter:Freezing Arrow": return [{ kind: "status", status: "freeze", durationMs: 10_000 }];
case "hunter:Feign Death": return [{ kind: "threat", mode: "percent", amount: -100 }];
case "hunter:Readiness": return [{ kind: "cooldown-reset", mode: "reset" }];
case "hunter:Chimera Shot": return [{ kind: "damage", coefficient: 1.25 }];
case "hunter:Kill Shot": return [{
kind: "damage", coefficient: 2,
bonusFlat: rank.effects.find((effect) => effect.effectType === 17)?.basePoints ?? 0,
}];
// Volley's player-facing spell is a six-second channel aura whose Arcane
// pulses live in same-rank triggered child rows. Keep the channel/cost but
// make the six ground-area pulses executable by players and companions.
@@ -807,7 +847,7 @@ function classScriptEffects(
tag: "volley",
}] : [];
}
case "rogue:Shiv": return [{ kind: "damage", coefficient: 1 }];
case "rogue:Shiv": return [{ kind: "damage", coefficient: 1 }, { kind: "combo", amount: 1 }];
case "rogue:Killing Spree": return [{
kind: "dot", coefficient: 0.5, ticks: 5, intervalMs: 500, tag: "killing-spree",
}];
@@ -843,7 +883,8 @@ function classScriptEffects(
durationMs: rank.durationMs > 0 ? rank.durationMs : 20_000,
maxStacks: 1,
stackBehavior: "refresh",
modifiers: [{ kind: "stat", stat: "critical-strike", operation: "flat", value: 1 }],
modifiers: [{ kind: "stat", stat: "critical-strike", operation: "flat", value: 100 }],
utilityTags: ["next-offensive-critical"],
},
recipient: "caster",
sourceEffectType: 6,
@@ -887,10 +928,17 @@ function classScriptEffects(
// trigger spells. Keep the imported cast/cost/rank data and supply only the
// missing executable payload.
case "priest:Mind Flay": return [{ kind: "damage", coefficient: 0.9 }];
case "priest:Penance": return [{ kind: "heal", coefficient: 1.8 }];
case "priest:Penance": return [
{ kind: "heal", coefficient: 0.6 },
{ kind: "hot", coefficient: 0.6, ticks: 2, intervalMs: 1_000 },
{ kind: "damage", coefficient: 0.6 },
{ kind: "dot", coefficient: 0.6, ticks: 2, intervalMs: 1_000, tag: "penance" },
];
case "priest:Divine Hymn": return [{
kind: "heal", coefficient: 1, scope: "party", center: "caster", radius: 40, maximumTargets: 3,
}];
case "priest:Shadowfiend": return [
{ kind: "summon", creatureId: 19668, coefficient: 0.5, durationMs: 15_000 },
{ kind: "resource", resource: "mana", amount: 30, percentage: true },
];
case "priest:Inner Focus": return [{
kind: "apply-aura",
@@ -971,6 +1019,18 @@ function classScriptEffects(
case "hunter:Misdirection": return [{ kind: "threat", mode: "redirect", amount: 100 }];
case "rogue:Tricks of the Trade": return [{ kind: "threat", mode: "redirect", amount: 100 }];
case "mage:Blink": return [{ kind: "movement", movement: "leap", toward: "away" }];
case "mage:Combustion": return [{
kind: "apply-aura", recipient: "caster",
aura: {
id: "wow335:mage:combustion", name: abilityName, disposition: "buff",
durationMs: null, maxStacks: 100, stackBehavior: "replace",
utilityTags: ["fire-crit-stacking"],
},
sourceEffectType: 6, sourceAuraType: 4, miscValue: 0, triggerSpellId: 0,
}];
case "mage:Mirror Image": return Array.from({ length: 3 }, () => ({
kind: "summon" as const, creatureId: 58836, coefficient: 0.25, durationMs: 30_000,
}));
// Arcane Missiles stores its per-tick payload in a server-triggered child spell
// that is not a player-facing SkillLine entry. Model the channel payload here so
// both players and shared-rotation consumers execute the imported spell instead
@@ -1275,7 +1335,9 @@ function effectsForRank(classId: BaseClassId, rank: Wow335Rank, abilityName: str
}
const auraOnlyStance = classId === "death-knight"
&& ["Blood Presence", "Frost Presence", "Unholy Presence"].includes(abilityName);
if (!auraOnlyStance && !effects.some((effect) => ["damage", "heal", "dot", "hot", "shield"].includes(effect.kind))) {
if (!auraOnlyStance && !(classId === "warrior" && abilityName === "Last Stand")
&& !(classId === "mage" && abilityName === "Molten Armor")
&& !effects.some((effect) => ["damage", "heal", "dot", "hot", "shield"].includes(effect.kind))) {
if (classId === "death-knight" && abilityName === "Death Coil") {
effects.unshift({ kind: "damage", coefficient: 1 });
} else if (/\b(?:heals?|restore|mend|rejuvenat)\w*\b/i.test(rank.description)) {
@@ -1293,6 +1355,18 @@ function effectsForRank(classId: BaseClassId, rank: Wow335Rank, abilityName: str
&& (effect.kind === "damage" || effect.kind === "heal"))
|| (classId === "priest" && abilityName === "Penance"
&& (effect.kind === "damage" || effect.kind === "heal"))
|| (classId === "priest" && abilityName === "Divine Hymn" && effect.kind === "heal")
|| (classId === "warrior" && ["Devastate", "Last Stand"].includes(abilityName)
&& (effect.kind === "damage" || effect.kind === "heal"))
|| (classId === "hunter" && abilityName === "Chimera Shot" && effect.kind === "damage")
|| (classId === "hunter" && abilityName === "Kill Shot" && effect.kind === "damage")
|| (classId === "priest" && abilityName === "Shadowfiend"
&& (effect.kind === "summon" || effect.kind === "trigger-spell"))
|| (classId === "mage" && abilityName === "Combustion"
&& (effect.kind === "damage" || effect.kind === "apply-aura" || effect.kind === "trigger-spell"))
|| (classId === "mage" && abilityName === "Mirror Image"
&& (effect.kind === "summon" || effect.kind === "trigger-spell"))
|| (classId === "rogue" && abilityName === "Cold Blood" && effect.kind === "apply-aura")
|| (classId === "shaman" && abilityName === "Feral Spirit" && effect.kind === "summon")
|| (classId === "warrior" && abilityName === "Bladestorm" && effect.kind === "dot")
|| (classId === "paladin" && abilityName === "Hammer of the Righteous" && effect.kind === "damage")
@@ -1443,9 +1517,20 @@ function abilityDefinition(classId: BaseClassId, chain: Wow335Chain): AbilityDef
const sourceRanks = chain.ranks.some((rank) => rank.rank > 0)
? chain.ranks.filter((rank) => rank.rank > 0)
: chain.ranks;
const ranks = sourceRanks.map((rank, index) => rankDefinition(classId, resource, rank, index, chain.name));
// Holy Nova's DBC chain stores damage and healing as separate same-rank
// spells. The damage spell is the cast (and pays mana); its healing partner
// is a second payload of that same cast, not another selectable rank.
const castRanks = classId === "priest" && chain.name === "Holy Nova"
? sourceRanks.filter((rank) => rank.effects.some((effect) => effect.effectType === 2))
.map((rank) => {
const healing = sourceRanks.find((candidate) => candidate.rank === rank.rank
&& candidate.effects.some((effect) => effect.effectType === 10));
return healing ? { ...rank, effects: [...rank.effects, ...healing.effects] } : rank;
})
: sourceRanks;
const ranks = castRanks.map((rank, index) => rankDefinition(classId, resource, rank, index, chain.name));
const first = ranks[0];
const firstRaw = sourceRanks[0];
const firstRaw = castRanks[0];
if (!first || !firstRaw) return null;
const target = abilityNameTarget(chain.name) ?? targetForRank(firstRaw, first.effects);
const radius = Math.max(
@@ -1506,7 +1591,8 @@ function abilityNameRadius(abilityName: string): number {
function abilityNameTarget(abilityName: string): AbilityTarget | undefined {
if (["Bladestorm", "Divine Hymn", "Tranquility", "Holy Nova", "Hellfire", "Army of the Dead", "Fan of Knives"].includes(abilityName)) return "self";
if (abilityName === "Volley") return "hostile";
if (abilityName === "Penance") return "friendly";
if (abilityName === "Penance") return "any";
if (["Last Stand", "Enraged Regeneration", "Molten Armor", "Combustion"].includes(abilityName)) return "self";
if (abilityName === "Death Coil") return "hostile";
return undefined;
}