Correct combat events, rotations and explicit weapon mechanics #21
No files matched your search
@@ -174,6 +174,52 @@ describe("talent runtime modifiers", () => {
|
||||
expect(applyTalentModifierValue(10_000, disciplineModifiers, "cooldown-flat-ms", { abilityName: "Mortal Strike" })).toBe(10_000);
|
||||
});
|
||||
|
||||
it("keeps Morbidity's Death Coil clauses and Death and Decay cooldown on their named spells", () => {
|
||||
const morbidity = TALENT_NODES.find((node) => node.id === "talent-1933")!;
|
||||
expect(morbidity.name).toBe("Morbidity");
|
||||
expect(morbidity.rankDescriptions[2]).toBe(
|
||||
"Increases the damage and healing of Death Coil by 15% and reduces the cooldown on Death and Decay by 15 sec.",
|
||||
);
|
||||
|
||||
const modifiers = talentModifiersForRanks({ [morbidity.id]: 3 }, "death-knight");
|
||||
expect(modifiers).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "damage-percent", amount: 15 }),
|
||||
expect.objectContaining({ kind: "healing-percent", amount: 15 }),
|
||||
expect.objectContaining({ kind: "cooldown-flat-ms", amount: -15_000, scope: "cooldown on Death and Decay" }),
|
||||
]));
|
||||
|
||||
expect(applyTalentModifierValue(30_000, modifiers, "cooldown-flat-ms", {
|
||||
abilityName: "Death and Decay",
|
||||
})).toBe(15_000);
|
||||
for (const [abilityName, baseCooldownMs] of [
|
||||
["Dark Command", 8_000],
|
||||
["Death Grip", 35_000],
|
||||
["Death Coil", 8_000],
|
||||
] as const) {
|
||||
expect(
|
||||
applyTalentModifierValue(baseCooldownMs, modifiers, "cooldown-flat-ms", { abilityName }),
|
||||
abilityName,
|
||||
).toBe(baseCooldownMs);
|
||||
}
|
||||
|
||||
expect(applyTalentModifierValue(100, modifiers, "damage-percent", { abilityName: "Death Coil" })).toBeCloseTo(115);
|
||||
expect(applyTalentModifierValue(100, modifiers, "healing-percent", { abilityName: "Death Coil" })).toBeCloseTo(115);
|
||||
expect(applyTalentModifierValue(100, modifiers, "damage-percent", { abilityName: "Icy Touch" })).toBe(100);
|
||||
expect(applyTalentModifierValue(100, modifiers, "healing-percent", { abilityName: "Death Strike" })).toBe(100);
|
||||
|
||||
const broadCooldown = parseTalentModifiers("Reduces the cooldown of all your spells by 2 sec.")
|
||||
.map((modifier) => ({
|
||||
...modifier,
|
||||
sourceTalentId: "test:broad-cooldown",
|
||||
sourceTalentName: "Broad Cooldown",
|
||||
rank: 1,
|
||||
description: "Reduces the cooldown of all your spells by 2 sec.",
|
||||
}));
|
||||
for (const abilityName of ["Fireball", "Icy Touch"]) {
|
||||
expect(applyTalentModifierValue(10_000, broadCooldown, "cooldown-flat-ms", { abilityName })).toBe(8_000);
|
||||
}
|
||||
});
|
||||
|
||||
it("scopes WoW Mage Impact's rank-scaled damage bonus to Fire Blast and Meteor", () => {
|
||||
const impact = TALENT_NODES.find((node) => node.id === "talent-30")!;
|
||||
|
||||
|
||||
@@ -223,8 +223,12 @@ export function parseTalentModifiers(description: string): readonly ParsedModifi
|
||||
if (castTime) add({ kind: "cast-time-flat-ms", amount: -Number(castTime[1]) * 1000, unit: "milliseconds", scope: sentence });
|
||||
const cooldown = /\bcasts?\s+reduce(?:s)?\s+(?:the\s+)?cooldown\b/i.test(sentence)
|
||||
? null
|
||||
: sentence.match(/reduces? (?:the )?cooldown (?:of |on )?.*? by (\d+(?:\.\d+)?) sec/i);
|
||||
if (cooldown) add({ kind: "cooldown-flat-ms", amount: -Number(cooldown[1]) * 1000, unit: "milliseconds", scope: sentence });
|
||||
: sentence.match(/\breduces?\s+(?:the\s+)?cooldown(?:\s+(of|on)\s+(.+?))?\s+by\s+(\d+(?:\.\d+)?)\s+secs?\b/i);
|
||||
if (cooldown) {
|
||||
const target = cooldown[2]?.trim();
|
||||
const scope = target ? `cooldown ${cooldown[1]} ${target}` : "cooldown";
|
||||
add({ kind: "cooldown-flat-ms", amount: -Number(cooldown[3]) * 1000, unit: "milliseconds", scope });
|
||||
}
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
@@ -445,6 +449,20 @@ export interface TalentModifierContext {
|
||||
readonly tags?: readonly string[];
|
||||
}
|
||||
|
||||
function namedModifierTarget(scope: string): string | undefined {
|
||||
const scopeWithoutAmount = scope.replace(/\s+by\s+-?\d+(?:\.\d+)?\s*(?:%|secs?)(?:\s*[.!?])?$/i, "");
|
||||
const target = scopeWithoutAmount.match(/\b(?:of|on|with|for|to)\s+(?:your\s+)?([^,.!?]+)$/)?.[1]?.trim();
|
||||
if (!target) return undefined;
|
||||
|
||||
// Generic descriptors describe a category of spells, effects, or targets.
|
||||
// Keep those modifiers broad; remaining words indicate a named ability.
|
||||
const remainingTarget = target.toLowerCase()
|
||||
.replace(/\b(?:a|an|the|your|all|any|each|every|both|other|next|additional|total|maximum|direct|periodic|instant|damaging|healing|helpful|harmful|melee|ranged|magic|physical|arcane|fire|frost|holy|nature|shadow|spell|spells|ability|abilities|attack|attacks|damage|cooldown|cost|cast|casting|time|effect|effects|critical|strike|chance|done|dealt|taken|suffered|received|target|targets|enemy|enemies|ally|allies|friendly|hostile|party|raid|members|you|self|both|and|or|of|on|with|for|to|by)\b/g, " ")
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim();
|
||||
return remainingTarget ? target.toLowerCase() : undefined;
|
||||
}
|
||||
|
||||
function modifierApplies(modifier: TalentModifier, context: TalentModifierContext): boolean {
|
||||
const scope = modifier.scope.toLowerCase();
|
||||
const tags = new Set((context.tags ?? []).map((tag) => tag.toLowerCase()));
|
||||
@@ -469,6 +487,8 @@ function modifierApplies(modifier: TalentModifier, context: TalentModifierContex
|
||||
}
|
||||
if (context.abilityName) {
|
||||
const normalizedName = context.abilityName.toLowerCase();
|
||||
const namedTarget = namedModifierTarget(scope);
|
||||
if (namedTarget && !namedTarget.includes(normalizedName)) return false;
|
||||
const namedScope = /(?:damage|healing|cost|cooldown|cast(?:ing)? time|effect|critical (?:effect |strike )?chance|chance to critically).*?\b(?:by|of|with) your\b/.test(scope);
|
||||
const genericScope = /\byour (?:all |instant |periodic |healing |damage |helpful |harmful )*(?:spells?|abilities?|attacks?)\b/.test(scope)
|
||||
|| /\byour (?:arcane|fire|frost|holy|nature|shadow) spells?\b/.test(scope);
|
||||
|
||||
Reference in new issue
Block a user