forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
f864b5aea7
commit
fdb6739f8d
10 files changed
+236
-16
No files matched your search
@@ -550,6 +550,12 @@ Deadly application at four-plus combo points only while the pending Envenom
|
|||||||
lacks enough doses. Envenom normally consumes the used doses; Orin's pinned 3/3
|
lacks enough doses. Envenom normally consumes the used doses; Orin's pinned 3/3
|
||||||
Master Poisoner build preserves them 100% of the time as specified by the
|
Master Poisoner build preserves them 100% of the time as specified by the
|
||||||
talent.
|
talent.
|
||||||
|
Ready DPS pack logic is deliberately thresholded and cluster-aware:
|
||||||
|
Marksmanship uses Multi-Shot at two nearby engaged targets and Volley at three,
|
||||||
|
while Fire uses Flamestrike and Assassination uses Fan of Knives at three.
|
||||||
|
Volley's six Arcane pulses are recovered from its DBC trigger child, Fan of
|
||||||
|
Knives is centered on the Rogue, active ground effects are not replaced early,
|
||||||
|
and companion AoE does not pull unengaged enemies.
|
||||||
|
|
||||||
## Code map
|
## Code map
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ The leftmost available action wins after reactive rules, learned-talent gating,
|
|||||||
| Paladin | Protection | Devotion Aura; Blessing of Might; Hammer of the Righteous; Shield of Righteousness; Holy Shield; Consecration; Judgement of Wisdom |
|
| Paladin | Protection | Devotion Aura; Blessing of Might; Hammer of the Righteous; Shield of Righteousness; Holy Shield; Consecration; Judgement of Wisdom |
|
||||||
| Paladin | Retribution | Retribution Aura; Blessing of Might; Divine Storm; Crusader Strike; Judgement of Wisdom; Consecration; Exorcism; Hammer of Wrath |
|
| Paladin | Retribution | Retribution Aura; Blessing of Might; Divine Storm; Crusader Strike; Judgement of Wisdom; Consecration; Exorcism; Hammer of Wrath |
|
||||||
| Hunter | Beast Mastery | Aspect of the Dragonhawk/Hawk; Call Pet; Kill Command; Bestial Wrath; Serpent Sting; Arcane Shot; Multi-Shot; Steady Shot |
|
| Hunter | Beast Mastery | Aspect of the Dragonhawk/Hawk; Call Pet; Kill Command; Bestial Wrath; Serpent Sting; Arcane Shot; Multi-Shot; Steady Shot |
|
||||||
| Hunter | Marksmanship | Aspect of the Dragonhawk/Hawk; Call Pet; Misdirection; Rapid Fire; Readiness; Kill Shot; Chimera Shot; Aimed Shot; Serpent Sting; Arcane Shot; Steady Shot; Silencing Shot; Disengage; Feign Death |
|
| Hunter | Marksmanship | Aspect of the Dragonhawk/Hawk; Call Pet; Misdirection; Rapid Fire; Readiness; Kill Shot; Volley (3+); Multi-Shot (2+); Chimera Shot; Aimed Shot; Serpent Sting; Arcane Shot; Steady Shot; Silencing Shot; Disengage; Feign Death |
|
||||||
| Hunter | Survival | Aspect of the Dragonhawk/Hawk; Call Pet; Kill Shot; Explosive Shot; Black Arrow; Serpent Sting; Aimed Shot; Arcane Shot; Steady Shot |
|
| Hunter | Survival | Aspect of the Dragonhawk/Hawk; Call Pet; Kill Shot; Explosive Shot; Black Arrow; Serpent Sting; Aimed Shot; Arcane Shot; Steady Shot |
|
||||||
| Rogue | Assassination | Deadly/Instant Poison; Tricks of the Trade; Cold Blood; Envenom; Shiv; Rupture; Mutilate; Eviscerate; Sinister Strike; Kick; Evasion; Cloak of Shadows; Vanish |
|
| Rogue | Assassination | Deadly/Instant Poison; Tricks of the Trade; Cold Blood; Fan of Knives (3+); Envenom; Shiv; Rupture; Mutilate; Eviscerate; Sinister Strike; Kick; Evasion; Cloak of Shadows; Vanish |
|
||||||
| Rogue | Combat | Killing Spree; Blade Flurry; Eviscerate; Sinister Strike |
|
| Rogue | Combat | Killing Spree; Blade Flurry; Eviscerate; Sinister Strike |
|
||||||
| Rogue | Subtlety | Shadow Dance; Rupture; Eviscerate; Hemorrhage; Backstab |
|
| Rogue | Subtlety | Shadow Dance; Rupture; Eviscerate; Hemorrhage; Backstab |
|
||||||
| Priest | Discipline | Inner Fire; Pain Suppression; Inner Focus; Power Infusion; Shadowfiend; Divine Hymn; Penance; Power Word: Shield; Flash/Greater Heal; Renew; Dispel Magic; Cure Disease; Resurrection |
|
| Priest | Discipline | Inner Fire; Pain Suppression; Inner Focus; Power Infusion; Shadowfiend; Divine Hymn; Penance; Power Word: Shield; Flash/Greater Heal; Renew; Dispel Magic; Cure Disease; Resurrection |
|
||||||
@@ -56,7 +56,7 @@ The leftmost available action wins after reactive rules, learned-talent gating,
|
|||||||
| Shaman | Enhancement | Windfury Weapon (Rockbiter fallback); Lightning Shield; Feral Spirit; Stormstrike; Lava Lash; Earth Shock; Lightning Bolt |
|
| Shaman | Enhancement | Windfury Weapon (Rockbiter fallback); Lightning Shield; Feral Spirit; Stormstrike; Lava Lash; Earth Shock; Lightning Bolt |
|
||||||
| Shaman | Restoration | Water Shield; Rockbiter Weapon; Lightning Shield; Riptide; Chain Heal; Lesser Healing Wave; Healing Wave; Earth Shield |
|
| Shaman | Restoration | Water Shield; Rockbiter Weapon; Lightning Shield; Riptide; Chain Heal; Lesser Healing Wave; Healing Wave; Earth Shield |
|
||||||
| Mage | Arcane | Mage Armor; Arcane Barrage; Arcane Blast; Arcane Missiles |
|
| Mage | Arcane | Mage Armor; Arcane Barrage; Arcane Blast; Arcane Missiles |
|
||||||
| Mage | Fire | Molten Armor; Combustion; Mirror Image; Living Bomb; Fireball; Scorch; Fire Blast; Counterspell; Blink; Ice Block; Fire/Frost Ward |
|
| Mage | Fire | Molten Armor; Combustion; Mirror Image; Flamestrike (3+); Living Bomb; Fireball; Scorch; Fire Blast; Counterspell; Blink; Ice Block; Fire/Frost Ward |
|
||||||
| Mage | Frost | Frost Armor; Deep Freeze; Frostbolt; Ice Lance; Cone of Cold |
|
| Mage | Frost | Frost Armor; Deep Freeze; Frostbolt; Ice Lance; Cone of Cold |
|
||||||
| Warlock | Affliction | Fel Armor; Felhunter; Haunt; Unstable Affliction; Corruption; Curse of Agony; Drain Soul; Shadow Bolt |
|
| Warlock | Affliction | Fel Armor; Felhunter; Haunt; Unstable Affliction; Corruption; Curse of Agony; Drain Soul; Shadow Bolt |
|
||||||
| Warlock | Demonology | Fel Armor; Felguard; Metamorphosis; Immolate; Corruption; Shadow Bolt |
|
| Warlock | Demonology | Fel Armor; Felguard; Metamorphosis; Immolate; Corruption; Shadow Bolt |
|
||||||
@@ -106,6 +106,13 @@ reset itself and monopolize the Hunter rotation. Blink and Disengage create
|
|||||||
distance from threat, low-health danger, or an imminent strong melee hit;
|
distance from threat, low-health danger, or an imminent strong melee hit;
|
||||||
Fire Blast is the instant Fire action available while moving.
|
Fire Blast is the instant Fire action available while moving.
|
||||||
|
|
||||||
|
Ready DPS AoE uses local engaged packs rather than every hostile in the
|
||||||
|
encounter. Marksmanship uses Multi-Shot when two targets fit its three-target
|
||||||
|
chain and channels Volley into clustered packs of three or more. Fire uses
|
||||||
|
Flamestrike at three or more targets and allows its ground fire to finish
|
||||||
|
ticking before recasting. Assassination uses the full-circle Fan of Knives at
|
||||||
|
three or more targets. Companion AoE never counts or damages unengaged packs.
|
||||||
|
|
||||||
Assassination treats Envenom as its primary four-plus-combo-point finisher,
|
Assassination treats Envenom as its primary four-plus-combo-point finisher,
|
||||||
matching the pinned Playerbots Assassination trigger, and banks Energy rather
|
matching the pinned Playerbots Assassination trigger, and banks Energy rather
|
||||||
than spending those points on cheaper Rupture while Envenom becomes affordable.
|
than spending those points on cheaper Rupture while Envenom becomes affordable.
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import type {
|
|||||||
AbilityTargetingRules,
|
AbilityTargetingRules,
|
||||||
} from "./abilityCatalog";
|
} from "./abilityCatalog";
|
||||||
|
|
||||||
const CHAIN_ABILITIES = new Set(["chain heal", "chain lightning", "prayer of mending"]);
|
const CHAIN_ABILITIES = new Set(["chain heal", "chain lightning", "multi-shot", "prayer of mending"]);
|
||||||
const GROUND_ABILITIES = new Set([
|
const GROUND_ABILITIES = new Set([
|
||||||
"blizzard", "rain of fire", "flamestrike", "death and decay", "consecration",
|
"blizzard", "rain of fire", "flamestrike", "death and decay", "consecration",
|
||||||
"volley", "hurricane", "earthquake", "meteor", "healing rain",
|
"volley", "hurricane", "earthquake", "meteor", "healing rain",
|
||||||
]);
|
]);
|
||||||
const CONE_ABILITIES = new Set([
|
const CONE_ABILITIES = new Set([
|
||||||
"cone of cold", "dragon's breath", "dragons breath", "shockwave", "cleave",
|
"cone of cold", "dragon's breath", "dragons breath", "shockwave", "cleave",
|
||||||
"swipe", "swipe (cat)", "fan of knives", "whirlwind",
|
"swipe", "swipe (cat)", "whirlwind",
|
||||||
]);
|
]);
|
||||||
const BEHIND_ABILITIES = new Set(["backstab", "ambush", "garrote", "ravage", "shred", "pounce"]);
|
const BEHIND_ABILITIES = new Set(["backstab", "ambush", "garrote", "ravage", "shred", "pounce"]);
|
||||||
const MOVEMENT_CAST_ABILITIES = new Set(["scorch", "steady shot", "spiritwalker's grace"]);
|
const MOVEMENT_CAST_ABILITIES = new Set(["scorch", "steady shot", "spiritwalker's grace"]);
|
||||||
@@ -62,7 +62,7 @@ export function targetingRulesForAbility(ability: AbilityDefinition): AbilityTar
|
|||||||
requiresBehindTarget: hostile && BEHIND_ABILITIES.has(name),
|
requiresBehindTarget: hostile && BEHIND_ABILITIES.has(name),
|
||||||
attackArcRadians: shape === "cone" ? Math.PI / 2 : Math.PI,
|
attackArcRadians: shape === "cone" ? Math.PI / 2 : Math.PI,
|
||||||
...(ability.radius ? { radius: ability.radius } : {}),
|
...(ability.radius ? { radius: ability.radius } : {}),
|
||||||
...(shape === "chain" ? { maximumTargets: 3, jumpRange: 12 } : {}),
|
...(shape === "chain" ? { maximumTargets: 3, jumpRange: name === "multi-shot" ? 10 : 12 } : {}),
|
||||||
...(shape === "ground" ? { groundRange: ability.range.max > 0 ? ability.range.max : name.endsWith(" trap") || name === "consecration" ? 0 : 30, radius: ability.radius ?? 8 } : {}),
|
...(shape === "ground" ? { groundRange: ability.range.max > 0 ? ability.range.max : name.endsWith(" trap") || name === "consecration" ? 0 : 30, radius: ability.radius ?? 8 } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,9 +209,9 @@ describe("shared combat rotations", () => {
|
|||||||
const contracts = [
|
const contracts = [
|
||||||
{ classId: "warrior" as const, specialization: "Protection", id: "warrior:protection", actions: ["Defensive Stance", "Taunt", "Shield Bash", "Shield Slam", "Revenge", "Devastate", "Thunder Clap", "Shield Block", "Shield Wall", "Last Stand", "Enraged Regeneration"] },
|
{ classId: "warrior" as const, specialization: "Protection", id: "warrior:protection", actions: ["Defensive Stance", "Taunt", "Shield Bash", "Shield Slam", "Revenge", "Devastate", "Thunder Clap", "Shield Block", "Shield Wall", "Last Stand", "Enraged Regeneration"] },
|
||||||
{ classId: "priest" as const, specialization: "Discipline", id: "priest:discipline", actions: ["Penance", "Power Word: Shield", "Flash Heal", "Greater Heal", "Renew", "Dispel Magic", "Cure Disease", "Resurrection", "Pain Suppression", "Inner Focus", "Power Infusion", "Shadowfiend"] },
|
{ classId: "priest" as const, specialization: "Discipline", id: "priest:discipline", actions: ["Penance", "Power Word: Shield", "Flash Heal", "Greater Heal", "Renew", "Dispel Magic", "Cure Disease", "Resurrection", "Pain Suppression", "Inner Focus", "Power Infusion", "Shadowfiend"] },
|
||||||
{ classId: "hunter" as const, specialization: "Marksmanship", id: "hunter:marksmanship", actions: ["Call Pet", "Aspect of the Dragonhawk", "Serpent Sting", "Chimera Shot", "Aimed Shot", "Arcane Shot", "Steady Shot", "Kill Shot", "Rapid Fire", "Readiness", "Misdirection", "Feign Death", "Silencing Shot", "Disengage"] },
|
{ classId: "hunter" as const, specialization: "Marksmanship", id: "hunter:marksmanship", actions: ["Call Pet", "Aspect of the Dragonhawk", "Serpent Sting", "Chimera Shot", "Aimed Shot", "Arcane Shot", "Steady Shot", "Multi-Shot", "Volley", "Kill Shot", "Rapid Fire", "Readiness", "Misdirection", "Feign Death", "Silencing Shot", "Disengage"] },
|
||||||
{ classId: "mage" as const, specialization: "Fire", id: "mage:fire", actions: ["Molten Armor", "Living Bomb", "Fireball", "Scorch", "Fire Blast", "Combustion", "Mirror Image", "Counterspell", "Fire Ward", "Frost Ward", "Ice Block", "Blink"] },
|
{ classId: "mage" as const, specialization: "Fire", id: "mage:fire", actions: ["Molten Armor", "Living Bomb", "Fireball", "Scorch", "Fire Blast", "Flamestrike", "Combustion", "Mirror Image", "Counterspell", "Fire Ward", "Frost Ward", "Ice Block", "Blink"] },
|
||||||
{ classId: "rogue" as const, specialization: "Assassination", id: "rogue:assassination", actions: ["Instant Poison", "Deadly Poison", "Mutilate", "Shiv", "Rupture", "Envenom", "Cold Blood", "Kick", "Tricks of the Trade", "Evasion", "Cloak of Shadows", "Vanish"] },
|
{ classId: "rogue" as const, specialization: "Assassination", id: "rogue:assassination", actions: ["Instant Poison", "Deadly Poison", "Mutilate", "Fan of Knives", "Shiv", "Rupture", "Envenom", "Cold Blood", "Kick", "Tricks of the Trade", "Evasion", "Cloak of Shadows", "Vanish"] },
|
||||||
];
|
];
|
||||||
for (const contract of contracts) {
|
for (const contract of contracts) {
|
||||||
const profile = rotationProfileFor(contract.classId, contract.id)!;
|
const profile = rotationProfileFor(contract.classId, contract.id)!;
|
||||||
@@ -432,6 +432,45 @@ describe("shared combat rotations", () => {
|
|||||||
.toBe("Envenom");
|
.toBe("Envenom");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses Ready DPS area actions only at their authored pack thresholds", () => {
|
||||||
|
const multiShot = ability("hunter", "Multi-Shot");
|
||||||
|
const volley = ability("hunter", "Volley");
|
||||||
|
const hunter = context({
|
||||||
|
classId: "hunter",
|
||||||
|
specializationId: "hunter:marksmanship",
|
||||||
|
primaryResource: "mana",
|
||||||
|
resources: { mana: 10_000 },
|
||||||
|
maximumResources: { mana: 10_000 },
|
||||||
|
abilities: [volley, multiShot],
|
||||||
|
});
|
||||||
|
expect(evaluateCombatRotation({ ...hunter, hostileCount: 1 })).toBeNull();
|
||||||
|
expect(evaluateCombatRotation({ ...hunter, hostileCount: 2 })?.ability.name).toBe("Multi-Shot");
|
||||||
|
expect(evaluateCombatRotation({ ...hunter, hostileCount: 3 })?.ability.name).toBe("Volley");
|
||||||
|
|
||||||
|
const flamestrike = ability("mage", "Flamestrike");
|
||||||
|
const fire = context({
|
||||||
|
classId: "mage",
|
||||||
|
specializationId: "mage:fire",
|
||||||
|
primaryResource: "mana",
|
||||||
|
resources: { mana: 10_000 },
|
||||||
|
maximumResources: { mana: 10_000 },
|
||||||
|
abilities: [flamestrike],
|
||||||
|
});
|
||||||
|
expect(evaluateCombatRotation({ ...fire, hostileCount: 2 })).toBeNull();
|
||||||
|
expect(evaluateCombatRotation({ ...fire, hostileCount: 3 })?.ability.name).toBe("Flamestrike");
|
||||||
|
|
||||||
|
const fan = ability("rogue", "Fan of Knives");
|
||||||
|
const assassination = context({
|
||||||
|
specializationId: "rogue:assassination",
|
||||||
|
primaryResource: "energy",
|
||||||
|
resources: { energy: 100 },
|
||||||
|
maximumResources: { energy: 100 },
|
||||||
|
abilities: [fan],
|
||||||
|
});
|
||||||
|
expect(evaluateCombatRotation({ ...assassination, hostileCount: 2 })).toBeNull();
|
||||||
|
expect(evaluateCombatRotation({ ...assassination, hostileCount: 3 })?.ability.name).toBe("Fan of Knives");
|
||||||
|
});
|
||||||
|
|
||||||
it("reacts before throughput and conserves long offensive cooldowns for durable targets", () => {
|
it("reacts before throughput and conserves long offensive cooldowns for durable targets", () => {
|
||||||
const kick = ability("rogue", "Kick");
|
const kick = ability("rogue", "Kick");
|
||||||
const strike = ability("rogue", "Sinister Strike");
|
const strike = ability("rogue", "Sinister Strike");
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ export const CLASSIC_ROTATION_PROFILES: readonly RotationProfile[] = Object.free
|
|||||||
profile("paladin:protection", "paladin", ["Devotion Aura", "Blessing of Might", "Seal of Vengeance", "Seal of Corruption", "Seal of Wisdom", "Seal of Righteousness", "Hammer of the Righteous", "Shield of Righteousness", "Holy Shield", "Consecration", "Judgement of Wisdom"]),
|
profile("paladin:protection", "paladin", ["Devotion Aura", "Blessing of Might", "Seal of Vengeance", "Seal of Corruption", "Seal of Wisdom", "Seal of Righteousness", "Hammer of the Righteous", "Shield of Righteousness", "Holy Shield", "Consecration", "Judgement of Wisdom"]),
|
||||||
profile("paladin:retribution", "paladin", ["Retribution Aura", "Devotion Aura", "Blessing of Might", "Seal of Command", "Seal of Vengeance", "Seal of Corruption", "Seal of Righteousness", "Divine Storm", "Crusader Strike", "Judgement of Wisdom", "Consecration", "Exorcism", "Hammer of Wrath"]),
|
profile("paladin:retribution", "paladin", ["Retribution Aura", "Devotion Aura", "Blessing of Might", "Seal of Command", "Seal of Vengeance", "Seal of Corruption", "Seal of Righteousness", "Divine Storm", "Crusader Strike", "Judgement of Wisdom", "Consecration", "Exorcism", "Hammer of Wrath"]),
|
||||||
profile("hunter:beast-mastery", "hunter", ["Aspect of the Dragonhawk", "Aspect of the Hawk", "Call Pet", "Kill Command", "Bestial Wrath", "Serpent Sting", "Arcane Shot", "Multi-Shot", "Steady Shot"]),
|
profile("hunter:beast-mastery", "hunter", ["Aspect of the Dragonhawk", "Aspect of the Hawk", "Call Pet", "Kill Command", "Bestial Wrath", "Serpent Sting", "Arcane Shot", "Multi-Shot", "Steady Shot"]),
|
||||||
profile("hunter:marksmanship", "hunter", ["Aspect of the Dragonhawk", "Aspect of the Hawk", "Call Pet", "Misdirection", "Rapid Fire", "Readiness", "Kill Shot", "Chimera Shot", "Aimed Shot", "Serpent Sting", "Arcane Shot", "Steady Shot", "Silencing Shot", "Disengage", "Feign Death"]),
|
profile("hunter:marksmanship", "hunter", ["Aspect of the Dragonhawk", "Aspect of the Hawk", "Call Pet", "Misdirection", "Rapid Fire", "Readiness", "Kill Shot", "Volley", "Multi-Shot", "Chimera Shot", "Aimed Shot", "Serpent Sting", "Arcane Shot", "Steady Shot", "Silencing Shot", "Disengage", "Feign Death"]),
|
||||||
profile("hunter:survival", "hunter", ["Aspect of the Dragonhawk", "Aspect of the Hawk", "Call Pet", "Kill Shot", "Explosive Shot", "Black Arrow", "Serpent Sting", "Aimed Shot", "Arcane Shot", "Steady Shot"]),
|
profile("hunter:survival", "hunter", ["Aspect of the Dragonhawk", "Aspect of the Hawk", "Call Pet", "Kill Shot", "Explosive Shot", "Black Arrow", "Serpent Sting", "Aimed Shot", "Arcane Shot", "Steady Shot"]),
|
||||||
profile("rogue:assassination", "rogue", ["Deadly Poison", "Instant Poison", "Tricks of the Trade", "Cold Blood", "Envenom", "Shiv", "Rupture", "Mutilate", "Eviscerate", "Sinister Strike", "Kick", "Evasion", "Cloak of Shadows", "Vanish"]),
|
profile("rogue:assassination", "rogue", ["Deadly Poison", "Instant Poison", "Tricks of the Trade", "Cold Blood", "Fan of Knives", "Envenom", "Shiv", "Rupture", "Mutilate", "Eviscerate", "Sinister Strike", "Kick", "Evasion", "Cloak of Shadows", "Vanish"]),
|
||||||
profile("rogue:combat", "rogue", ["Killing Spree", "Blade Flurry", "Eviscerate", "Sinister Strike"]),
|
profile("rogue:combat", "rogue", ["Killing Spree", "Blade Flurry", "Eviscerate", "Sinister Strike"]),
|
||||||
profile("rogue:subtlety", "rogue", ["Shadow Dance", "Rupture", "Eviscerate", "Hemorrhage", "Backstab"]),
|
profile("rogue:subtlety", "rogue", ["Shadow Dance", "Rupture", "Eviscerate", "Hemorrhage", "Backstab"]),
|
||||||
profile("priest:discipline", "priest", ["Inner Fire", "Pain Suppression", "Inner Focus", "Power Infusion", "Shadowfiend", "Divine Hymn", "Penance", "Power Word: Shield", "Flash Heal", "Greater Heal", "Renew", "Dispel Magic", "Cure Disease", "Resurrection"]),
|
profile("priest:discipline", "priest", ["Inner Fire", "Pain Suppression", "Inner Focus", "Power Infusion", "Shadowfiend", "Divine Hymn", "Penance", "Power Word: Shield", "Flash Heal", "Greater Heal", "Renew", "Dispel Magic", "Cure Disease", "Resurrection"]),
|
||||||
@@ -126,7 +126,7 @@ export const CLASSIC_ROTATION_PROFILES: readonly RotationProfile[] = Object.free
|
|||||||
profile("shaman:enhancement", "shaman", ["Windfury Weapon", "Rockbiter Weapon", "Lightning Shield", "Feral Spirit", "Stormstrike", "Lava Lash", "Earth Shock", "Lightning Bolt"]),
|
profile("shaman:enhancement", "shaman", ["Windfury Weapon", "Rockbiter Weapon", "Lightning Shield", "Feral Spirit", "Stormstrike", "Lava Lash", "Earth Shock", "Lightning Bolt"]),
|
||||||
profile("shaman:restoration", "shaman", ["Water Shield", "Rockbiter Weapon", "Lightning Shield", "Riptide", "Chain Heal", "Lesser Healing Wave", "Healing Wave", "Earth Shield"]),
|
profile("shaman:restoration", "shaman", ["Water Shield", "Rockbiter Weapon", "Lightning Shield", "Riptide", "Chain Heal", "Lesser Healing Wave", "Healing Wave", "Earth Shield"]),
|
||||||
profile("mage:arcane", "mage", ["Mage Armor", "Ice Armor", "Frost Armor", "Arcane Barrage", "Arcane Blast", "Arcane Missiles"]),
|
profile("mage:arcane", "mage", ["Mage Armor", "Ice Armor", "Frost Armor", "Arcane Barrage", "Arcane Blast", "Arcane Missiles"]),
|
||||||
profile("mage:fire", "mage", ["Molten Armor", "Combustion", "Mirror Image", "Living Bomb", "Fireball", "Scorch", "Fire Blast", "Counterspell", "Blink", "Ice Block", "Fire Ward", "Frost Ward"]),
|
profile("mage:fire", "mage", ["Molten Armor", "Combustion", "Mirror Image", "Flamestrike", "Living Bomb", "Fireball", "Scorch", "Fire Blast", "Counterspell", "Blink", "Ice Block", "Fire Ward", "Frost Ward"]),
|
||||||
profile("mage:frost", "mage", ["Frost Armor", "Ice Armor", "Mage Armor", "Deep Freeze", "Frostbolt", "Ice Lance", "Cone of Cold"]),
|
profile("mage:frost", "mage", ["Frost Armor", "Ice Armor", "Mage Armor", "Deep Freeze", "Frostbolt", "Ice Lance", "Cone of Cold"]),
|
||||||
profile("warlock:affliction", "warlock", ["Fel Armor", "Demon Armor", "Demon Skin", "Summon Felhunter", "Summon Voidwalker", "Summon Imp", "Haunt", "Unstable Affliction", "Corruption", "Curse of Agony", "Drain Soul", "Shadow Bolt"]),
|
profile("warlock:affliction", "warlock", ["Fel Armor", "Demon Armor", "Demon Skin", "Summon Felhunter", "Summon Voidwalker", "Summon Imp", "Haunt", "Unstable Affliction", "Corruption", "Curse of Agony", "Drain Soul", "Shadow Bolt"]),
|
||||||
profile("warlock:demonology", "warlock", ["Fel Armor", "Demon Armor", "Demon Skin", "Summon Felguard", "Summon Voidwalker", "Summon Imp", "Metamorphosis", "Immolate", "Corruption", "Shadow Bolt"]),
|
profile("warlock:demonology", "warlock", ["Fel Armor", "Demon Armor", "Demon Skin", "Summon Felguard", "Summon Voidwalker", "Summon Imp", "Metamorphosis", "Immolate", "Corruption", "Shadow Bolt"]),
|
||||||
@@ -162,6 +162,17 @@ const TARGET_HEALTH_GATES = new Map<string, number>([
|
|||||||
["Kill Shot", 0.2],
|
["Kill Shot", 0.2],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const MINIMUM_HOSTILE_TARGETS = new Map<string, number>([
|
||||||
|
["Multi-Shot", 2],
|
||||||
|
["Volley", 3],
|
||||||
|
["Flamestrike", 3],
|
||||||
|
["Fan of Knives", 3],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function minimumHostileTargetsForAbility(abilityName: string): number {
|
||||||
|
return MINIMUM_HOSTILE_TARGETS.get(abilityName) ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
const BEAR_FORM_ACTION_NAMES = new Set([
|
const BEAR_FORM_ACTION_NAMES = new Set([
|
||||||
"Bash",
|
"Bash",
|
||||||
"Challenging Roar",
|
"Challenging Roar",
|
||||||
@@ -387,6 +398,7 @@ function scoreAbility(
|
|||||||
|
|
||||||
const targetHealthGate = TARGET_HEALTH_GATES.get(ability.name);
|
const targetHealthGate = TARGET_HEALTH_GATES.get(ability.name);
|
||||||
if (targetHealthGate !== undefined && targetHealth > targetHealthGate) return null;
|
if (targetHealthGate !== undefined && targetHealth > targetHealthGate) return null;
|
||||||
|
if ((context.hostileCount ?? 1) < minimumHostileTargetsForAbility(ability.name)) return null;
|
||||||
if (CONDITIONAL_ABILITY_NAMES.has(ability.name)
|
if (CONDITIONAL_ABILITY_NAMES.has(ability.name)
|
||||||
&& !context.availableConditionalAbilityNames?.has(ability.name)) return null;
|
&& !context.availableConditionalAbilityNames?.has(ability.name)) return null;
|
||||||
if (context.classId === "druid") {
|
if (context.classId === "druid") {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRun
|
|||||||
import { advanceDungeonPartyCombat, partyUsesRangedAttacks, resetPartyRuntimeClock } from "./partyRuntime";
|
import { advanceDungeonPartyCombat, partyUsesRangedAttacks, resetPartyRuntimeClock } from "./partyRuntime";
|
||||||
import { subscribeCombatPresentation } from "./combatPresentation";
|
import { subscribeCombatPresentation } from "./combatPresentation";
|
||||||
import { abilitiesForClass } from "./abilityCatalog";
|
import { abilitiesForClass } from "./abilityCatalog";
|
||||||
|
import { runtimePartyTalentRanksForLevel } from "./partyTalentRuntime";
|
||||||
|
|
||||||
function partyTemplates(level: number): PartyMember[] {
|
function partyTemplates(level: number): PartyMember[] {
|
||||||
const templates = new Map<string, PartyMember>();
|
const templates = new Map<string, PartyMember>();
|
||||||
@@ -76,6 +77,66 @@ describe("NPC sustained damage audit", () => {
|
|||||||
expect(useCombatStore.getState().mobs["area:c"].health).toBe(1_000);
|
expect(useCombatStore.getState().mobs["area:c"].health).toBe(1_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const row of [
|
||||||
|
{ classId: "hunter" as const, specialization: "Marksmanship", specializationId: "hunter:marksmanship", abilityName: "Multi-Shot", targets: 2 },
|
||||||
|
{ classId: "hunter" as const, specialization: "Marksmanship", specializationId: "hunter:marksmanship", abilityName: "Volley", targets: 3 },
|
||||||
|
{ classId: "mage" as const, specialization: "Fire", specializationId: "mage:fire", abilityName: "Flamestrike", targets: 3 },
|
||||||
|
{ classId: "rogue" as const, specialization: "Assassination", specializationId: "rogue:assassination", abilityName: "Fan of Knives", targets: 3 },
|
||||||
|
]) {
|
||||||
|
it(`${row.specialization} ${row.abilityName} damages its ${row.targets}-target Ready threshold`, () => {
|
||||||
|
clearMobRuntimeRegistry();
|
||||||
|
clearPartyRuntimeRegistry();
|
||||||
|
resetPartyRuntimeClock();
|
||||||
|
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
|
||||||
|
const member = createConfiguredPartyMember({
|
||||||
|
name: `${row.specialization} area test`,
|
||||||
|
classId: row.classId,
|
||||||
|
specialization: row.specialization,
|
||||||
|
specializationId: row.specializationId,
|
||||||
|
talentTreeId: `${row.classId}-${row.specialization.toLowerCase()}`,
|
||||||
|
talentRanks: runtimePartyTalentRanksForLevel(row.classId, row.specialization, 80),
|
||||||
|
role: "damage",
|
||||||
|
}, 80, 0, `${row.classId}-${row.abilityName}-area`);
|
||||||
|
const ability = abilitiesForClass(row.classId).find((candidate) => candidate.name === row.abilityName)!;
|
||||||
|
usePartyStore.getState().initializeParty(
|
||||||
|
`${row.classId}-${row.abilityName}-area`,
|
||||||
|
80,
|
||||||
|
useCombatStore.getState().health,
|
||||||
|
"damage",
|
||||||
|
);
|
||||||
|
usePartyStore.setState({
|
||||||
|
members: [{
|
||||||
|
...member,
|
||||||
|
cooldowns: Object.fromEntries(abilitiesForClass(row.classId)
|
||||||
|
.filter((candidate) => candidate.id !== ability.id)
|
||||||
|
.map((candidate) => [candidate.id, 60_000])),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
useCombatStore.getState().synchronizeActors();
|
||||||
|
registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||||
|
const targetIds = Array.from({ length: row.targets }, (_, index) => `area-ready-${index}`);
|
||||||
|
for (const [index, id] of targetIds.entries()) {
|
||||||
|
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100_000, xpReward: 0 });
|
||||||
|
setMobPosition(id, [index * 2, 0, 0]);
|
||||||
|
useCombatStore.getState().damageMob(id, 1, 100);
|
||||||
|
}
|
||||||
|
updatePartyRuntimeTargetVisibility(member.id, targetIds[0], true);
|
||||||
|
|
||||||
|
for (let now = 1_000; now <= 12_000; now += 100) {
|
||||||
|
useCombatStore.getState().tick(0.1, now);
|
||||||
|
advanceDungeonPartyCombat(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hitTargets = new Set(useCombatStore.getState().combatEvents
|
||||||
|
.filter((event) => event.kind === "damage"
|
||||||
|
&& event.sourceActorId === member.id
|
||||||
|
&& event.abilityId === ability.id
|
||||||
|
&& event.effectiveAmount > 0)
|
||||||
|
.map((event) => event.targetActorId));
|
||||||
|
expect(hitTargets).toEqual(new Set(targetIds));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("generates Bear-form Rage from weapon hits and regenerates Cat-form Energy", () => {
|
it("generates Bear-form Rage from weapon hits and regenerates Cat-form Energy", () => {
|
||||||
clearMobRuntimeRegistry();
|
clearMobRuntimeRegistry();
|
||||||
clearPartyRuntimeRegistry();
|
clearPartyRuntimeRegistry();
|
||||||
|
|||||||
@@ -479,6 +479,52 @@ describe("party combat runtime", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("taunts a loose add off an ally while the rest of the party stays on the boss", () => {
|
||||||
|
const party = usePartyStore.getState();
|
||||||
|
const tank = party.members.find((member) => member.role === "tank")!;
|
||||||
|
const damage = party.members.find((member) => member.role === "damage")!;
|
||||||
|
for (const member of party.members) registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||||
|
|
||||||
|
useCombatStore.getState().registerMob("boss-with-add", {
|
||||||
|
name: "Boss with add",
|
||||||
|
boss: true,
|
||||||
|
maxHealth: 10_000,
|
||||||
|
xpReward: 0,
|
||||||
|
});
|
||||||
|
useCombatStore.getState().registerMob("boss-loose-add", {
|
||||||
|
name: "Loose add",
|
||||||
|
maxHealth: 1_000,
|
||||||
|
xpReward: 0,
|
||||||
|
});
|
||||||
|
setMobPosition("boss-with-add", [2, 0, 0]);
|
||||||
|
setMobPosition("boss-loose-add", [8, 0, 0]);
|
||||||
|
useCombatStore.getState().damageMob("boss-with-add", 1, 100, {
|
||||||
|
actorId: tank.id,
|
||||||
|
role: "tank",
|
||||||
|
});
|
||||||
|
useCombatStore.getState().damageMob("boss-loose-add", 1, 100, {
|
||||||
|
actorId: damage.id,
|
||||||
|
role: "damage",
|
||||||
|
});
|
||||||
|
useCombatStore.getState().selectMob("boss-with-add");
|
||||||
|
for (const member of party.members) {
|
||||||
|
updatePartyRuntimeTargetVisibility(member.id, "boss-with-add", true);
|
||||||
|
updatePartyRuntimeTargetVisibility(member.id, "boss-loose-add", true);
|
||||||
|
}
|
||||||
|
useCombatStore.getState().advanceMobCombat(200);
|
||||||
|
|
||||||
|
advanceDungeonPartyCombat(1_000);
|
||||||
|
|
||||||
|
const runtime = usePartyStore.getState();
|
||||||
|
expect(runtime.memberTargetIds[tank.id]).toBe("boss-loose-add");
|
||||||
|
expect(runtime.memberTargetIds[damage.id]).toBe("boss-with-add");
|
||||||
|
expect(useCombatStore.getState().mobs["boss-loose-add"].forcedTarget).toMatchObject({
|
||||||
|
actorId: tank.id,
|
||||||
|
endsAt: 4_000,
|
||||||
|
});
|
||||||
|
expect(runtime.members.find((member) => member.id === tank.id)?.tauntReadyAt).toBe(9_000);
|
||||||
|
});
|
||||||
|
|
||||||
it("shares an eight-second tank taunt cooldown across loose mobs", () => {
|
it("shares an eight-second tank taunt cooldown across loose mobs", () => {
|
||||||
const party = usePartyStore.getState();
|
const party = usePartyStore.getState();
|
||||||
const tank = party.members.find((member) => member.role === "tank")!;
|
const tank = party.members.find((member) => member.role === "tank")!;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
commitPartyAbilityUse,
|
commitPartyAbilityUse,
|
||||||
partyAbilityAmount,
|
partyAbilityAmount,
|
||||||
} from "./partyAbilityAi";
|
} from "./partyAbilityAi";
|
||||||
|
import { minimumHostileTargetsForAbility } from "./combatRotation";
|
||||||
import { emitRotationDecision } from "./rotationTelemetry";
|
import { emitRotationDecision } from "./rotationTelemetry";
|
||||||
import { imminentCooldownMechanic } from "./companionCooldownPolicy";
|
import { imminentCooldownMechanic } from "./companionCooldownPolicy";
|
||||||
import { normalizeActorResourcePool } from "./combatActors";
|
import { normalizeActorResourcePool } from "./combatActors";
|
||||||
@@ -799,7 +800,7 @@ function partyHostileTargets(
|
|||||||
for (const id of ordered) {
|
for (const id of ordered) {
|
||||||
if (selected.length >= (targeting.maximumTargets ?? 3)) break;
|
if (selected.length >= (targeting.maximumTargets ?? 3)) break;
|
||||||
const position = getMobPosition(id);
|
const position = getMobPosition(id);
|
||||||
if (!position || combat.mobs[id]?.dead) continue;
|
if (!position || !combat.mobs[id]?.engaged || combat.mobs[id]?.dead) continue;
|
||||||
if (selected.length > 0 && distanceSquared(previous, position) > (targeting.jumpRange ?? 12) ** 2) continue;
|
if (selected.length > 0 && distanceSquared(previous, position) > (targeting.jumpRange ?? 12) ** 2) continue;
|
||||||
if (!combatHasLineOfSight(previous, position)) continue;
|
if (!combatHasLineOfSight(previous, position)) continue;
|
||||||
selected.push(id);
|
selected.push(id);
|
||||||
@@ -812,7 +813,7 @@ function partyHostileTargets(
|
|||||||
return Object.values(combat.mobs)
|
return Object.values(combat.mobs)
|
||||||
.filter((mob) => {
|
.filter((mob) => {
|
||||||
const position = getMobPosition(mob.id);
|
const position = getMobPosition(mob.id);
|
||||||
return !mob.dead && Boolean(position
|
return mob.engaged && !mob.dead && Boolean(position
|
||||||
&& distanceSquared(origin, position) <= (targeting.radius ?? ability.radius ?? ability.range.max) ** 2
|
&& distanceSquared(origin, position) <= (targeting.radius ?? ability.radius ?? ability.range.max) ** 2
|
||||||
&& combatHasLineOfSight(origin, position)
|
&& combatHasLineOfSight(origin, position)
|
||||||
&& (!memberSpatial || isPositionInFacingArc(memberSpatial, position, targeting.attackArcRadians ?? Math.PI / 2)));
|
&& (!memberSpatial || isPositionInFacingArc(memberSpatial, position, targeting.attackArcRadians ?? Math.PI / 2)));
|
||||||
@@ -826,7 +827,7 @@ function partyHostileTargets(
|
|||||||
return Object.values(combat.mobs)
|
return Object.values(combat.mobs)
|
||||||
.filter((mob) => {
|
.filter((mob) => {
|
||||||
const position = getMobPosition(mob.id);
|
const position = getMobPosition(mob.id);
|
||||||
return !mob.dead && Boolean(position
|
return mob.engaged && !mob.dead && Boolean(position
|
||||||
&& distanceSquared(center, position) <= radius * radius
|
&& distanceSquared(center, position) <= radius * radius
|
||||||
&& combatHasLineOfSight(center, position));
|
&& combatHasLineOfSight(center, position));
|
||||||
})
|
})
|
||||||
@@ -1824,7 +1825,14 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
|
|||||||
const abilityTargetId = (ability.target === "self" && !hostileSelfArea) || ability.target === "friendly"
|
const abilityTargetId = (ability.target === "self" && !hostileSelfArea) || ability.target === "friendly"
|
||||||
? member.id
|
? member.id
|
||||||
: memberTargetId;
|
: memberTargetId;
|
||||||
|
const minimumHostiles = minimumHostileTargetsForAbility(ability.name);
|
||||||
return partyAbilityTargetIsValid(runtimeMember, ability, abilityTargetId)
|
return partyAbilityTargetIsValid(runtimeMember, ability, abilityTargetId)
|
||||||
|
&& (minimumHostiles <= 1 || partyHostileTargets(
|
||||||
|
ability,
|
||||||
|
abilityTargetId,
|
||||||
|
memberPosition,
|
||||||
|
member.id,
|
||||||
|
).length >= minimumHostiles)
|
||||||
&& (ability.name !== "Envenom" || matchingAuraStacks(
|
&& (ability.name !== "Envenom" || matchingAuraStacks(
|
||||||
currentCombat.auras,
|
currentCombat.auras,
|
||||||
ROGUE_DEADLY_POISON_DOSE_TAG,
|
ROGUE_DEADLY_POISON_DOSE_TAG,
|
||||||
@@ -1845,6 +1853,8 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
|
|||||||
// Let maintained effects tick instead of resetting their first tick on every GCD.
|
// Let maintained effects tick instead of resetting their first tick on every GCD.
|
||||||
&& !currentCombat.effects.some((effect) => effect.sourceActorId === member.id
|
&& !currentCombat.effects.some((effect) => effect.sourceActorId === member.id
|
||||||
&& effect.sourceAbilityId === ability.id && effect.targetId === abilityTargetId && effect.remainingTicks > 0)
|
&& effect.sourceAbilityId === ability.id && effect.targetId === abilityTargetId && effect.remainingTicks > 0)
|
||||||
|
&& !currentCombat.areaEffects.some((effect) => effect.sourceActorId === member.id
|
||||||
|
&& effect.sourceAbilityId === ability.id && effect.expiresAt > now)
|
||||||
&& !ability.effects.some((effect) => effect.kind === "apply-aura"
|
&& !ability.effects.some((effect) => effect.kind === "apply-aura"
|
||||||
&& currentCombat.auras.some((aura) => aura.sourceId === member.id
|
&& currentCombat.auras.some((aura) => aura.sourceId === member.id
|
||||||
&& aura.definition.id === effect.aura.id && aura.targetId === abilityTargetId));
|
&& aura.definition.id === effect.aura.id && aura.targetId === abilityTargetId));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { abilityAtLevel, type AbilityDefinition, type AbilityEffect } from "./abilityCatalog";
|
import { abilityAtLevel, type AbilityDefinition, type AbilityEffect } from "./abilityCatalog";
|
||||||
|
import { aiHintsForAbility, targetingRulesForAbility } from "./abilityRules";
|
||||||
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
|
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
|
||||||
|
|
||||||
function byId(id: string): AbilityDefinition {
|
function byId(id: string): AbilityDefinition {
|
||||||
@@ -41,6 +42,28 @@ describe("WoW 3.3.5 executable effect mapping", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps the Ready DPS area actions to executable multi-target shapes", () => {
|
||||||
|
const multiShot = abilityAtLevel(byId("wow335-hunter-multi-shot"), 80);
|
||||||
|
expect(targetingRulesForAbility(multiShot)).toMatchObject({
|
||||||
|
shape: "chain",
|
||||||
|
maximumTargets: 3,
|
||||||
|
jumpRange: 10,
|
||||||
|
});
|
||||||
|
expect(aiHintsForAbility(multiShot).categories).toContain("area-damage");
|
||||||
|
|
||||||
|
const volley = abilityAtLevel(byId("wow335-hunter-volley"), 80);
|
||||||
|
expect(volley).toMatchObject({ target: "hostile", castMode: "channel", castTimeMs: 6_000, radius: 8 });
|
||||||
|
expect(targetingRulesForAbility(volley).shape).toBe("ground");
|
||||||
|
expect(effectsOfKind(volley, "dot")).toEqual([
|
||||||
|
expect.objectContaining({ ticks: 6, intervalMs: 1_000, tag: "volley" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const fan = abilityAtLevel(byId("wow335-rogue-fan-of-knives"), 80);
|
||||||
|
expect(fan).toMatchObject({ target: "self", radius: 8 });
|
||||||
|
expect(targetingRulesForAbility(fan).shape).toBe("circle");
|
||||||
|
expect(aiHintsForAbility(fan).categories).toContain("area-damage");
|
||||||
|
});
|
||||||
|
|
||||||
it("gives every imported classic rank executable or explicit utility semantics", () => {
|
it("gives every imported classic rank executable or explicit utility semantics", () => {
|
||||||
const effects = Object.values(WOW335_ABILITIES_BY_CLASS)
|
const effects = Object.values(WOW335_ABILITIES_BY_CLASS)
|
||||||
.flat()
|
.flat()
|
||||||
|
|||||||
@@ -793,6 +793,20 @@ function classScriptEffects(
|
|||||||
case "hunter:Freezing Arrow": return [{ kind: "status", status: "freeze", durationMs: 10_000 }];
|
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:Feign Death": return [{ kind: "threat", mode: "percent", amount: -100 }];
|
||||||
case "hunter:Readiness": return [{ kind: "cooldown-reset", mode: "reset" }];
|
case "hunter:Readiness": return [{ kind: "cooldown-reset", mode: "reset" }];
|
||||||
|
// 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.
|
||||||
|
case "hunter:Volley": {
|
||||||
|
const pulse = rank.effects.find((effect) => effect.effectType === 27);
|
||||||
|
return pulse ? [{
|
||||||
|
kind: "dot",
|
||||||
|
coefficient: fallbackCoefficient(pulse, rank),
|
||||||
|
...amountScaling(pulse, rank),
|
||||||
|
ticks: 6,
|
||||||
|
intervalMs: 1_000,
|
||||||
|
tag: "volley",
|
||||||
|
}] : [];
|
||||||
|
}
|
||||||
case "rogue:Shiv": return [{ kind: "damage", coefficient: 1 }];
|
case "rogue:Shiv": return [{ kind: "damage", coefficient: 1 }];
|
||||||
case "rogue:Killing Spree": return [{
|
case "rogue:Killing Spree": return [{
|
||||||
kind: "dot", coefficient: 0.5, ticks: 5, intervalMs: 500, tag: "killing-spree",
|
kind: "dot", coefficient: 0.5, ticks: 5, intervalMs: 500, tag: "killing-spree",
|
||||||
@@ -1286,6 +1300,7 @@ function effectsForRank(classId: BaseClassId, rank: Wow335Rank, abilityName: str
|
|||||||
|| (classId === "warrior" && abilityName === "Bloodrage"
|
|| (classId === "warrior" && abilityName === "Bloodrage"
|
||||||
&& (effect.kind === "resource" || effect.kind === "heal"))
|
&& (effect.kind === "resource" || effect.kind === "heal"))
|
||||||
|| (classId === "priest" && abilityName === "Inner Focus" && effect.kind === "apply-aura")
|
|| (classId === "priest" && abilityName === "Inner Focus" && effect.kind === "apply-aura")
|
||||||
|
|| (classId === "hunter" && abilityName === "Volley" && effect.kind === "apply-aura")
|
||||||
|| (classId === "rogue" && abilityName === "Envenom" && effect.kind === "apply-aura")
|
|| (classId === "rogue" && abilityName === "Envenom" && effect.kind === "apply-aura")
|
||||||
|| (classId === "rogue" && abilityName === "Vanish" && effect.kind === "movement")
|
|| (classId === "rogue" && abilityName === "Vanish" && effect.kind === "movement")
|
||||||
))
|
))
|
||||||
@@ -1489,7 +1504,8 @@ function abilityNameRadius(abilityName: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function abilityNameTarget(abilityName: string): AbilityTarget | undefined {
|
function abilityNameTarget(abilityName: string): AbilityTarget | undefined {
|
||||||
if (["Bladestorm", "Divine Hymn", "Tranquility", "Holy Nova", "Hellfire", "Army of the Dead"].includes(abilityName)) return "self";
|
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 "friendly";
|
||||||
if (abilityName === "Death Coil") return "hostile";
|
if (abilityName === "Death Coil") return "hostile";
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
Reference in new issue
Block a user