Skip to content

Instantly share code, notes, and snippets.

@cgdangelo
Last active February 15, 2018 15:30
Show Gist options
  • Save cgdangelo/1c7d3fa34a38bf796b8fdab0ae7b0876 to your computer and use it in GitHub Desktop.
Save cgdangelo/1c7d3fa34a38bf796b8fdab0ae7b0876 to your computer and use it in GitHub Desktop.
// ==========================================================================
// Dedmonwakeen's DPS-DPM Simulator.
// Send questions to [email protected]
// ==========================================================================
#include "simulationcraft.hpp"
namespace { // UNNAMED NAMESPACE
// ==========================================================================
// Mage
// ==========================================================================
// BfA TODO:
// - Arcane Power, Rune of Power, Incater's Flow, Combustion, Bone Chilling,
// Arcane T20 2pc no longer global damage mods (they still have pet damage
// mod, double check), MAKE SURE they work correctly with pet spells
//
// - Spec passives + Erosion apply to all mage spells (shouldn't be too
// relevant)
//
// - Arcane Charge changes + Clearcasting, mastery + ABar change, AM doesn't
// generate AC, baseline Temporal Flux
//
// - New Arcane talents (Rule of Threes, Mana Adept, NYI in last row)
//
// - Arcane Intellect (probably gonna be simc wide)
//
// - double check coef changes
//
// - Clearcasting + extra mana from mastery (shouldn't reduce proc chance,
// hopefully)
//
// - Glacial Spike seems to be double dipping on the updated damage mod effects
// (Rune of Power, etc).
// Forward declarations
struct mage_t;
namespace pets {
namespace water_elemental {
struct water_elemental_pet_t;
}
}
struct state_switch_t {
private:
bool state;
timespan_t last_enable;
timespan_t last_disable;
public:
state_switch_t() {
reset();
}
bool enable(timespan_t now) {
if (last_enable == now) {
return false;
}
state = true;
last_enable = now;
return true;
}
bool disable(timespan_t now) {
if (last_disable == now) {
return false;
}
state = false;
last_disable = now;
return true;
}
bool on() {
return state;
}
timespan_t duration(timespan_t now) {
if (!state) {
return timespan_t::zero();
}
return now - last_enable;
}
void reset() {
state = false;
last_enable = timespan_t::min();
last_disable = timespan_t::min();
}
};
/// Icicle container object, contains a timestamp and its corresponding icicle data!
struct icicle_tuple_t {
timespan_t timestamp;
double damage;
};
struct mage_td_t : public actor_target_data_t {
struct dots_t {
dot_t *conflagration_dot;
dot_t *ignite;
dot_t *living_bomb;
dot_t *nether_tempest;
} dots;
struct debuffs_t {
buff_t *erosion;
buff_t *slow;
buff_t *frost_bomb;
buff_t *water_jet;
buff_t *winters_chill;
buff_t *frozen;
} debuffs;
mage_td_t(player_t *target, mage_t *mage);
};
struct buff_stack_benefit_t {
const buff_t *buff;
std::vector<benefit_t *> buff_stack_benefit;
buff_stack_benefit_t(const buff_t *_buff,
const std::string &prefix) :
buff(_buff),
buff_stack_benefit() {
for (int i = 0; i <= buff->max_stack(); i++) {
buff_stack_benefit.push_back(buff->player->
get_benefit(prefix + " " +
buff->data().name_cstr() + " " +
util::to_string(i)));
}
}
void update() {
for (std::size_t i = 0; i < buff_stack_benefit.size(); ++i) {
buff_stack_benefit[i]->update(i == as<unsigned>(buff->check()));
}
}
};
struct cooldown_reduction_data_t {
const cooldown_t *cd;
luxurious_sample_data_t *effective;
luxurious_sample_data_t *wasted;
cooldown_reduction_data_t(const cooldown_t *cooldown, const std::string &name) :
cd(cooldown) {
player_t *p = cd->player;
effective = p->get_sample_data(name + " effective cooldown reduction");
wasted = p->get_sample_data(name + " wasted cooldown reduction");
}
void add(timespan_t reduction) {
assert(effective);
assert(wasted);
timespan_t remaining = timespan_t::zero();
if (cd->charges > 1) {
if (cd->recharge_event) {
remaining = cd->current_charge_remains() +
(cd->charges - cd->current_charge - 1) * cd->duration;
}
} else {
remaining = cd->remains();
}
double reduction_sec = -reduction.total_seconds();
double remaining_sec = remaining.total_seconds();
double effective_sec = std::min(reduction_sec, remaining_sec);
effective->add(effective_sec);
double wasted_sec = reduction_sec - effective_sec;
wasted->add(wasted_sec);
}
};
struct cooldown_waste_data_t : private noncopyable {
const cooldown_t *cd;
double buffer;
extended_sample_data_t normal;
extended_sample_data_t cumulative;
cooldown_waste_data_t(const cooldown_t *cooldown, bool simple = true) :
cd(cooldown),
buffer(0.0),
normal(cd->name_str + " cooldown waste", simple),
cumulative(cd->name_str + " cooldown cumulative waste", simple) {}
bool may_add(timespan_t cd_override = timespan_t::min()) const {
return (cd->duration > timespan_t::zero() || cd_override > timespan_t::zero())
&& ((cd->charges == 1 && cd->up()) || (cd->charges >= 2 && cd->current_charge == cd->charges));
}
void add(timespan_t cd_override = timespan_t::min(), timespan_t time_to_execute = timespan_t::zero()) {
if (may_add(cd_override)) {
double wasted = (cd->sim.current_time() - cd->last_charged).total_seconds();
if (cd->charges == 1) {
// Waste caused by execute time is unavoidable for single charge spells,
// don't count it.
wasted -= time_to_execute.total_seconds();
}
normal.add(wasted);
buffer += wasted;
}
}
bool active() const {
return normal.count() > 0 && cumulative.sum() > 0;
}
void merge(const cooldown_waste_data_t &other) {
normal.merge(other.normal);
cumulative.merge(other.cumulative);
}
void analyze() {
normal.analyze();
cumulative.analyze();
}
void datacollection_begin() {
buffer = 0.0;
}
void datacollection_end() {
if (may_add())
buffer += (cd->sim.current_time() - cd->last_charged).total_seconds();
cumulative.add(buffer);
buffer = 0.0;
}
};
struct proc_source_t : private noncopyable {
const std::string name_str;
auto_dispose<std::vector<proc_t *> > procs;
proc_source_t(sim_t &sim, const std::string &name, size_t count) :
name_str(name) {
for (size_t i = 0; i < count; i++) {
procs.push_back(new proc_t(sim, name_str + " " + util::to_string(i)));
}
}
void occur(size_t index) {
assert(index < procs.size());
procs[index]->occur();
}
const proc_t &get(size_t index) const {
assert(index < procs.size());
return *procs[index];
}
bool active() const {
return range::find_if(procs, [](proc_t *p) { return p->count.sum() > 0.0; }) != procs.end();
}
void reset() {
range::for_each(procs, std::mem_fn(&proc_t::reset));
}
void merge(const proc_source_t &other) {
for (size_t i = 0; i < procs.size(); i++) {
procs[i]->merge(*other.procs[i]);
}
}
void datacollection_begin() {
range::for_each(procs, std::mem_fn(&proc_t::datacollection_begin));
}
void datacollection_end() {
range::for_each(procs, std::mem_fn(&proc_t::datacollection_end));
}
};
struct mage_t : public player_t {
public:
// Icicles
std::vector<icicle_tuple_t> icicles;
action_t *icicle;
event_t *icicle_event;
// Ignite
action_t *ignite;
event_t *ignite_spread_event;
// Active
player_t *last_bomb_target;
// State switches for rotation selection
state_switch_t burn_phase;
// Ground AoE tracking
std::map<std::string, timespan_t> ground_aoe_expiration;
// Miscellaneous
double distance_from_rune;
double global_cinder_count;
timespan_t firestarter_time;
int blessing_of_wisdom_count;
bool allow_shimmer_lance;
// Data collection
auto_dispose<std::vector<cooldown_waste_data_t *> > cooldown_waste_data_list;
auto_dispose<std::vector<proc_source_t *> > proc_source_list;
// Cached actions
struct actions_t {
action_t *arcane_assault;
action_t *frost_bomb_explosion;
action_t *legendary_arcane_orb;
action_t *legendary_meteor;
action_t *legendary_comet_storm;
action_t *unstable_magic_explosion;
} action;
// Benefits
struct benefits_t {
struct arcane_charge_benefits_t {
buff_stack_benefit_t *arcane_barrage;
buff_stack_benefit_t *arcane_blast;
buff_stack_benefit_t *nether_tempest;
} arcane_charge;
buff_stack_benefit_t *magtheridons_might;
buff_stack_benefit_t *ray_of_frost;
buff_stack_benefit_t *zannesu_journey;
} benefits;
// Buffs
struct buffs_t {
// Arcane
buff_t *arcane_charge;
buff_t *arcane_familiar;
buff_t *arcane_power;
buff_t *chrono_shift;
buff_t *clearcasting;
buff_t *crackling_energy; // T20 2pc Arcane
buff_t *presence_of_mind;
buff_t *expanding_mind; // T21 2pc Arcane
buff_t *quick_thinker; // T21 4pc Arcane
buff_t *rule_of_threes;
// Fire
buff_t *combustion;
buff_t *contained_infernal_core; // 7.2.5 legendary shoulder, tracking buff
buff_t *critical_massive; // T20 4pc Fire
buff_t *enhanced_pyrotechnics;
buff_t *erupting_infernal_core; // 7.2.5 legendary shoulder, primed buff
buff_t *frenetic_speed;
buff_t *heating_up;
buff_t *hot_streak;
buff_t *ignition; // T20 2pc Fire
buff_t *streaking; // T19 4pc Fire
buff_t *inferno; // T21 4pc Fire
// Frost
buff_t *brain_freeze;
buff_t *fingers_of_frost;
buff_t *frozen_mass; // T20 2pc Frost
buff_t
*icicles; // Buff to track icicles - doesn't always line up with icicle count though!
buff_t *icy_veins;
buff_t *rage_of_the_frost_wyrm; // 7.2.5 legendary head, primed buff
buff_t *shattered_fragments_of_sindragosa; // 7.2.5 legendary head, tracking buff
buff_t *arctic_blast; // T21 4pc Frost
// Talents
buff_t *bone_chilling;
buff_t *ice_floes;
buff_t *incanters_flow;
buff_t *ray_of_frost;
buff_t *rune_of_power;
// Legendary
buff_t *cord_of_infinity;
buff_t *kaelthas_ultimate_ability;
buff_t *lady_vashjs_grasp;
buff_t *magtheridons_might;
buff_t *rhonins_assaulting_armwraps;
buff_t *zannesu_journey;
haste_buff_t *sephuzs_secret;
// Miscellaneous Buffs
buff_t *greater_blessing_of_widsom;
buff_t *t19_oh_buff;
buff_t *shimmer;
} buffs;
// Cooldowns
struct cooldowns_t {
cooldown_t *combustion;
cooldown_t *cone_of_cold;
cooldown_t *evocation;
cooldown_t *frost_nova;
cooldown_t *frozen_orb;
cooldown_t *icy_veins;
cooldown_t *presence_of_mind;
cooldown_t *ray_of_frost;
cooldown_t *time_warp;
} cooldowns;
// Gains
struct gains_t {
gain_t *greater_blessing_of_wisdom;
gain_t *evocation;
gain_t *mystic_kilt_of_the_rune_master;
} gains;
// Pets
struct pets_t {
pets::water_elemental::water_elemental_pet_t *water_elemental;
std::vector<pet_t *> mirror_images;
pets_t() : water_elemental(nullptr) {}
} pets;
// Procs
struct procs_t {
proc_t *heating_up_generated; // Crits without HU/HS
proc_t *heating_up_removed; // Non-crits with HU >200ms after application
proc_t *heating_up_ib_converted; // IBs used on HU
proc_t *hot_streak; // Total HS generated
proc_t *hot_streak_pyromaniac; // Total HS from Pyromaniac
proc_t *hot_streak_spell; // HU/HS spell impacts
proc_t *hot_streak_spell_crit; // HU/HS spell crits
proc_t *hot_streak_spell_crit_wasted; // HU/HS spell crits with HS
proc_t *ignite_applied; // Direct ignite applications
proc_t *ignite_spread; // Spread events
proc_t *ignite_new_spread; // Spread to new target
proc_t *ignite_overwrite; // Spread to target with existing ignite
proc_t *brain_freeze_flurry;
proc_t *fingers_of_frost_wasted;
} procs;
// Sample data
struct sample_data_t {
cooldown_reduction_data_t *blizzard;
cooldown_reduction_data_t *t20_4pc;
luxurious_sample_data_t *glacial_spike_base;
luxurious_sample_data_t *glacial_spike_icicles;
extended_sample_data_t *icy_veins_duration;
extended_sample_data_t *burn_duration_history;
extended_sample_data_t *burn_initial_mana;
} sample_data;
// Specializations
struct specializations_t {
// Arcane
const spell_data_t *arcane_barrage_2;
const spell_data_t *arcane_charge;
const spell_data_t *arcane_mage;
const spell_data_t *clearcasting;
const spell_data_t *evocation_2;
const spell_data_t *savant;
// Fire
const spell_data_t *critical_mass;
const spell_data_t *critical_mass_2;
const spell_data_t *enhanced_pyrotechnics;
const spell_data_t *fire_blast_2;
const spell_data_t *fire_blast_3;
const spell_data_t *fire_mage;
const spell_data_t *hot_streak;
const spell_data_t *ignite;
// Frost
const spell_data_t *brain_freeze;
const spell_data_t *brain_freeze_2;
const spell_data_t *blizzard_2;
const spell_data_t *fingers_of_frost;
const spell_data_t *frost_mage;
const spell_data_t *icicles;
const spell_data_t *icicles_driver;
const spell_data_t *shatter;
const spell_data_t *shatter_2;
} spec;
// State
struct state_t {
bool brain_freeze_active;
bool fingers_of_frost_active;
int flurry_bolt_count;
} state;
// Talents
struct talents_list_t {
// Tier 15
const spell_data_t *rule_of_threes;
const spell_data_t *mana_adept;
const spell_data_t *arcane_familiar;
const spell_data_t *pyromaniac;
const spell_data_t *conflagration;
const spell_data_t *firestarter;
const spell_data_t *ray_of_frost;
const spell_data_t *lonely_winter;
const spell_data_t *bone_chilling;
// Tier 30
const spell_data_t *shimmer;
const spell_data_t *slipstream;
const spell_data_t *blast_wave;
const spell_data_t *ice_floes;
const spell_data_t *mana_shield; // NYI
const spell_data_t *blazing_soul; // NYI
const spell_data_t *glacial_insulation; // NYI
// Tier 45
const spell_data_t *mirror_image;
const spell_data_t *rune_of_power;
const spell_data_t *incanters_flow;
// Tier 60
const spell_data_t *supernova;
const spell_data_t *charged_up;
const spell_data_t *resonance;
const spell_data_t *alexstraszas_fury;
const spell_data_t *flame_on;
const spell_data_t *phoenix_flames;
const spell_data_t *ice_nova;
const spell_data_t *frozen_touch;
const spell_data_t *splitting_ice;
// Tier 75
const spell_data_t *chrono_shift;
const spell_data_t *frenetic_speed;
const spell_data_t *frigid_winds; // NYI
const spell_data_t *ring_of_frost; // NYI
const spell_data_t *ice_ward;
// Tier 90
const spell_data_t *nether_tempest;
const spell_data_t *living_bomb;
const spell_data_t *frost_bomb;
const spell_data_t *unstable_magic;
const spell_data_t *erosion;
const spell_data_t *flame_patch;
const spell_data_t *arctic_gale;
// Tier 100
const spell_data_t *overpowered;
const spell_data_t *temporal_flux;
const spell_data_t *arcane_orb;
const spell_data_t *kindling;
const spell_data_t *cinderstorm;
const spell_data_t *meteor;
const spell_data_t *thermal_void;
const spell_data_t *glacial_spike;
const spell_data_t *comet_storm;
} talents;
struct uptimes_t {
uptime_t *burn_phase;
uptime_t *conserve_phase;
} uptime;
public:
mage_t(sim_t *sim, const std::string &name, race_e r = RACE_NIGHT_ELF);
~mage_t();
// Character Definition
virtual std::string get_special_use_items(const std::string &item = std::string(), bool specials = false);
virtual void init_spells() override;
virtual void init_base_stats() override;
virtual void create_buffs() override;
virtual void create_options() override;
virtual void init_gains() override;
virtual void init_procs() override;
virtual void init_benefits() override;
virtual void init_uptimes() override;
virtual void invalidate_cache(cache_e c) override;
virtual void init_resources(bool force) override;
virtual void recalculate_resource_max(resource_e rt) override;
virtual void reset() override;
virtual expr_t *create_expression(action_t *, const std::string &name) override;
virtual action_t *create_action(const std::string &name, const std::string &options) override;
virtual bool create_actions() override;
virtual void create_pets() override;
virtual resource_e primary_resource() const override { return RESOURCE_MANA; }
virtual role_e primary_role() const override { return ROLE_SPELL; }
virtual stat_e convert_hybrid_stat(stat_e s) const override;
virtual stat_e primary_stat() const override { return STAT_INTELLECT; }
virtual double mana_regen_per_second() const override;
virtual double composite_player_multiplier(school_e school) const override;
virtual double composite_player_critical_damage_multiplier(const action_state_t *) const override;
virtual double composite_player_pet_damage_multiplier(const action_state_t *) const override;
virtual double composite_spell_crit_chance() const override;
virtual double composite_spell_crit_rating() const override;
virtual double composite_spell_haste() const override;
virtual double composite_mastery_rating() const override;
virtual double matching_gear_multiplier(attribute_e attr) const override;
virtual void update_movement(timespan_t duration) override;
virtual void stun() override;
virtual double temporary_movement_modifier() const override;
virtual double passive_movement_modifier() const override;
virtual void arise() override;
virtual void combat_begin() override;
virtual void combat_end() override;
virtual std::string create_profile(save_e) override;
virtual void copy_from(player_t *) override;
virtual void merge(player_t &) override;
virtual void analyze(sim_t &) override;
virtual void datacollection_begin() override;
virtual void datacollection_end() override;
target_specific_t<mage_td_t> target_data;
virtual mage_td_t *get_target_data(player_t *target) const override {
mage_td_t *&td = target_data[target];
if (!td) {
td = new mage_td_t(target, const_cast<mage_t *>(this));
}
return td;
}
cooldown_waste_data_t *get_cooldown_waste_data(cooldown_t *cd) {
for (auto cdw : cooldown_waste_data_list) {
if (cdw->cd->name_str == cd->name_str)
return cdw;
}
auto cdw = new cooldown_waste_data_t(cd);
cooldown_waste_data_list.push_back(cdw);
return cdw;
}
proc_source_t *get_proc_source(const std::string &name, size_t count) {
for (auto ps : proc_source_list) {
if (ps->name_str == name) {
return ps;
}
}
auto ps = new proc_source_t(*sim, name, count);
proc_source_list.push_back(ps);
return ps;
}
// Public mage functions:
double get_icicle();
void trigger_icicle(const action_state_t *trigger_state, bool chain = false, player_t *chain_target = nullptr);
bool apply_crowd_control(const action_state_t *state, spell_mechanic type);
void apl_precombat();
void apl_arcane();
void apl_fire();
void apl_frost();
void apl_default();
virtual void init_action_list() override;
std::string default_potion() const override;
std::string default_flask() const override;
std::string default_food() const override;
std::string default_rune() const override;
};
// Common spell base for mage and mage pet spells.
struct mage_spell_base_t : public spell_t {
struct affected_by_t {
// Permanent damage increase.
bool arcane_mage;
bool fire_mage;
bool frost_mage;
// Temporary damage increase.
bool arcane_power;
bool bone_chilling;
bool crackling_energy;
bool incanters_flow;
bool rune_of_power;
// Misc
bool combustion;
bool erosion;
bool ice_floes;
bool shatter;
affected_by_t() :
arcane_mage(true),
fire_mage(true),
frost_mage(true),
arcane_power(true),
bone_chilling(true),
crackling_energy(true),
incanters_flow(true),
rune_of_power(true),
combustion(true),
erosion(true),
ice_floes(false),
shatter(false) {}
} affected_by;
// Mage player or owner for pet spells.
mage_t *mage;
mage_spell_base_t(const std::string &n, player_t *p, const spell_data_t *s, mage_t *m) :
spell_t(n, p, s),
affected_by(),
mage(m) {
may_crit = tick_may_crit = true;
weapon_multiplier = 0.0;
}
virtual void init() override {
spell_t::init();
if (affected_by.arcane_mage)
base_multiplier *= 1.0 + mage->spec.arcane_mage->effectN(1).percent();
if (affected_by.fire_mage)
base_multiplier *= 1.0 + mage->spec.fire_mage->effectN(1).percent();
if (affected_by.frost_mage)
base_multiplier *= 1.0 + mage->spec.frost_mage->effectN(1).percent();
}
virtual double action_multiplier() const override {
double m = spell_t::action_multiplier();
if (affected_by.arcane_power)
m *= 1.0 + mage->buffs.arcane_power->check_value();
if (affected_by.bone_chilling)
m *= 1.0 + mage->buffs.bone_chilling->check_stack_value();
if (affected_by.crackling_energy)
m *= 1.0 + mage->buffs.crackling_energy->check_value();
if (affected_by.incanters_flow)
m *= 1.0 + mage->buffs.incanters_flow->check_stack_value();
if (affected_by.rune_of_power)
m *= 1.0 + mage->buffs.rune_of_power->check_value();
return m;
}
double composite_crit_chance() const override {
double c = spell_t::composite_crit_chance();
if (affected_by.combustion)
c += mage->buffs.combustion->check_value();
return c;
}
virtual double composite_target_multiplier(player_t *target) const override {
double tm = spell_t::composite_target_multiplier(target);
if (affected_by.erosion && mage->target_data[target]) {
tm *= 1.0 + mage->target_data[target]->debuffs.erosion->check_stack_value();
}
return tm;
}
};
namespace pets {
struct mage_pet_t : public pet_t {
mage_pet_t(sim_t *sim, mage_t *owner, std::string pet_name,
bool guardian = false, bool dynamic = false)
: pet_t(sim, owner, pet_name, guardian, dynamic) {}
const mage_t *o() const {
return static_cast<mage_t *>( owner );
}
mage_t *o() {
return static_cast<mage_t *>( owner );
}
};
struct mage_pet_spell_t : public mage_spell_base_t {
mage_pet_spell_t(const std::string &n, mage_pet_t *p, const spell_data_t *s)
: mage_spell_base_t(n, p, s, p->o()) {
if (p->o()->bugs) {
affected_by.erosion = false;
}
}
mage_t *o() {
return static_cast<mage_pet_t *>( player )->o();
}
const mage_t *o() const {
return static_cast<mage_pet_t *>( player )->o();
}
};
namespace water_elemental {
// ==========================================================================
// Pet Water Elemental
// ==========================================================================
struct water_elemental_pet_t : public mage_pet_t {
water_elemental_pet_t(sim_t *sim, mage_t *owner)
: mage_pet_t(sim, owner, "water_elemental") {
owner_coeff.sp_from_sp = 0.75;
}
virtual void init_action_list() override {
clear_action_priority_lists();
auto default_list = get_action_priority_list("default");
default_list->add_action(this, find_pet_spell("Water Jet"), "Water Jet");
default_list->add_action(this, find_pet_spell("Waterbolt"), "Waterbolt");
// Default
use_default_action_list = true;
mage_pet_t::init_action_list();
}
virtual action_t *create_action(const std::string &name,
const std::string &options_str) override;
};
struct water_elemental_spell_t : public mage_pet_spell_t {
water_elemental_spell_t(const std::string &n, mage_pet_t *p, const spell_data_t *s)
: mage_pet_spell_t(n, p, s) {}
virtual double action_multiplier() const override {
double am = mage_pet_spell_t::action_multiplier();
if (o()->spec.icicles->ok()) {
am *= 1.0 + o()->cache.mastery_value();
}
return am;
}
};
struct waterbolt_t : public water_elemental_spell_t {
waterbolt_t(water_elemental_pet_t *p, const std::string &options_str)
: water_elemental_spell_t("waterbolt", p, p->find_pet_spell("Waterbolt")) {
parse_options(options_str);
trigger_gcd = timespan_t::zero();
}
virtual timespan_t execute_time() const override {
timespan_t cast_time = water_elemental_spell_t::execute_time();
// For some reason welly seems to have a cap'd rate of cast of
// 1.5/second. Instead of modeling this as a cooldown/GCD (like it is in game)
// we model it as a capped cast time, with 1.5 being the lowest it can go.
return std::max(cast_time, timespan_t::from_seconds(1.5));
}
};
struct freeze_t : public water_elemental_spell_t {
proc_t *proc_fof;
freeze_t(water_elemental_pet_t *p) :
water_elemental_spell_t("freeze", p, p->find_pet_spell("Freeze")) {
background = true;
aoe = -1;
internal_cooldown = p->get_cooldown("wj_freeze");
internal_cooldown->duration = data().category_cooldown();
}
virtual bool init_finished() override {
proc_fof = o()->get_proc(std::string("Fingers of Frost from ") + data().name_cstr());
return water_elemental_spell_t::init_finished();
}
virtual void impact(action_state_t *s) override {
water_elemental_spell_t::impact(s);
bool success = o()->apply_crowd_control(s, MECHANIC_ROOT);
if (success) {
o()->buffs.fingers_of_frost->trigger();
o()->buffs.fingers_of_frost->predict();
proc_fof->occur();
}
}
};
struct water_jet_t : public water_elemental_spell_t {
// queued water jet spell, auto cast water jet spell
bool queued;
bool autocast;
water_jet_t(water_elemental_pet_t *p, const std::string &options_str)
: water_elemental_spell_t("water_jet", p, p->find_pet_spell("Water Jet")),
queued(false),
autocast(true) {
parse_options(options_str);
channeled = tick_zero = true;
internal_cooldown = p->get_cooldown("wj_freeze");
internal_cooldown->duration = data().category_cooldown();
}
virtual void execute() override {
// If this is a queued execute, disable queued status
if (!autocast && queued)
queued = false;
// Don't execute Water Jet if Water Elemental used Freeze
// during the cast
if (internal_cooldown->up()) {
water_elemental_spell_t::execute();
}
}
virtual void impact(action_state_t *s) override {
water_elemental_spell_t::impact(s);
o()->get_target_data(s->target)
->debuffs.water_jet->trigger(1, buff_t::DEFAULT_VALUE(), 1.0, composite_dot_duration(s));
}
virtual void last_tick(dot_t *d) override {
water_elemental_spell_t::last_tick(d);
// If the channel is cancelled early, remove the debuff.
o()->get_target_data(d->target)
->debuffs.water_jet->expire();
}
virtual bool ready() override {
// Not ready, until the owner gives permission to cast
if (!autocast && !queued)
return false;
return water_elemental_spell_t::ready();
}
virtual void reset() override {
water_elemental_spell_t::reset();
queued = false;
}
};
action_t *water_elemental_pet_t::create_action(const std::string &name,
const std::string &options_str) {
if (name == "waterbolt")
return new waterbolt_t(this, options_str);
if (name == "water_jet")
return new water_jet_t(this, options_str);
return mage_pet_t::create_action(name, options_str);
}
} // water_elemental
namespace mirror_image {
// ==========================================================================
// Pet Mirror Image
// ==========================================================================
struct mirror_image_pet_t : public mage_pet_t {
buff_t *arcane_charge;
mirror_image_pet_t(sim_t *sim, mage_t *owner)
: mage_pet_t(sim, owner, "mirror_image", true), arcane_charge(nullptr) {
owner_coeff.sp_from_sp = 1.00;
}
virtual action_t *create_action(const std::string &name,
const std::string &options_str) override;
virtual void init_action_list() override {
switch (o()->specialization()) {
case MAGE_FIRE:action_list_str = "fireball";
break;
case MAGE_ARCANE:action_list_str = "arcane_blast";
break;
case MAGE_FROST:action_list_str = "frostbolt";
break;
default:break;
}
mage_pet_t::init_action_list();
}
virtual void create_buffs() override {
mage_pet_t::create_buffs();
// MI Arcane Charge is hardcoded as 25% damage increase.
arcane_charge = buff_creator_t(this, "arcane_charge", o()->spec.arcane_charge)
.default_value(0.25);
}
};
struct mirror_image_spell_t : public mage_pet_spell_t {
mirror_image_spell_t(const std::string &n, mirror_image_pet_t *p,
const spell_data_t *s)
: mage_pet_spell_t(n, p, s) {}
virtual bool init_finished() override {
if (p()->o()->pets.mirror_images[0]) {
stats = p()->o()->pets.mirror_images[0]->get_stats(name_str);
}
return mage_pet_spell_t::init_finished();
}
mirror_image_pet_t *p() const {
return static_cast<mirror_image_pet_t *>( player );
}
};
struct arcane_blast_t : public mirror_image_spell_t {
arcane_blast_t(mirror_image_pet_t *p, const std::string &options_str)
: mirror_image_spell_t("arcane_blast", p, p->find_pet_spell("Arcane Blast")) {
parse_options(options_str);
}
virtual void execute() override {
mirror_image_spell_t::execute();
p()->arcane_charge->trigger();
}
virtual double action_multiplier() const override {
double am = mirror_image_spell_t::action_multiplier();
am *= 1.0 + p()->arcane_charge->check_stack_value();
return am;
}
};
struct fireball_t : public mirror_image_spell_t {
fireball_t(mirror_image_pet_t *p, const std::string &options_str)
: mirror_image_spell_t("fireball", p, p->find_pet_spell("Fireball")) {
parse_options(options_str);
}
};
struct frostbolt_t : public mirror_image_spell_t {
frostbolt_t(mirror_image_pet_t *p, const std::string &options_str)
: mirror_image_spell_t("frostbolt", p, p->find_pet_spell("Frostbolt")) {
parse_options(options_str);
}
};
action_t *mirror_image_pet_t::create_action(const std::string &name,
const std::string &options_str) {
if (name == "arcane_blast")
return new arcane_blast_t(this, options_str);
if (name == "fireball")
return new fireball_t(this, options_str);
if (name == "frostbolt")
return new frostbolt_t(this, options_str);
return mage_pet_t::create_action(name, options_str);
}
} // mirror_image
} // pets
// Cinderstorm impact helper event ============================================
namespace events {
struct cinder_impact_event_t : public event_t {
action_t *cinder;
player_t *target;
cinder_impact_event_t(actor_t &m, action_t *c, player_t *t,
timespan_t impact_time) :
event_t(m, impact_time), cinder(c), target(t) {}
virtual const char *name() const override { return "cinder_impact_event"; }
virtual void execute() override {
cinder->set_target(target);
cinder->execute();
}
};
}
namespace buffs {
struct erosion_t : public buff_t {
// Erosion debuff =============================================================
struct erosion_event_t : public event_t {
erosion_t *debuff;
const spell_data_t *data;
static timespan_t delta_time(const spell_data_t *data,
bool player_triggered) {
// Erosion debuff decays 3 seconds after direct application by a player,
// followed by a 1 stack every second
if (player_triggered) {
return data->duration();
}
return data->effectN(1).period();
}
erosion_event_t(actor_t &m, erosion_t *_debuff, const spell_data_t *_data,
bool player_triggered = false)
: event_t(m, delta_time(_data, player_triggered)),
debuff(_debuff),
data(_data) {}
virtual const char *name() const override { return "erosion_decay_event"; }
virtual void execute() override {
debuff->decay_event = nullptr;
debuff->decrement();
// Always update the parent debuff's reference to the decay event, so that it
// can be cancelled upon a new application of the debuff
if (debuff->check() > 0) {
debuff->decay_event = make_event<erosion_event_t>(
sim(), *debuff->source, debuff, data);
}
}
};
const spell_data_t *erosion_event_data;
event_t *decay_event;
erosion_t(mage_td_t *td) :
buff_t(buff_creator_t(*td, "erosion",
td->source->find_spell(210134))),
erosion_event_data(td->source->find_spell(210154)),
decay_event(nullptr) {
set_default_value(data().effectN(1).percent());
}
virtual bool trigger(int stacks, double value,
double chance, timespan_t duration) override {
bool triggered = buff_t::trigger(stacks, value, chance, duration);
if (triggered) {
if (decay_event) {
event_t::cancel(decay_event);
}
decay_event = make_event<erosion_event_t>(*sim, *source, this, erosion_event_data, true);
}
return triggered;
}
virtual void expire_override(int expiration_stacks, timespan_t remaining_duration) override {
buff_t::expire_override(expiration_stacks, remaining_duration);
event_t::cancel(decay_event);
}
virtual void reset() override {
event_t::cancel(decay_event);
buff_t::reset();
}
};
// Custom buffs ===============================================================
struct brain_freeze_buff_t : public buff_t {
brain_freeze_buff_t(mage_t *p) :
buff_t(buff_creator_t(p, "brain_freeze", p->find_spell(190446))) {}
virtual void execute(int stacks, double value, timespan_t duration) override {
buff_t::execute(stacks, value, duration);
auto mage = debug_cast<mage_t *>(player);
if (mage->sets->has_set_bonus(MAGE_FROST, T20, B4)) {
timespan_t cd_reduction = -100 * mage->sets->set(MAGE_FROST, T20, B4)->effectN(1).time_value();
mage->sample_data.t20_4pc->add(cd_reduction);
mage->cooldowns.frozen_orb->adjust(cd_reduction);
}
}
virtual void refresh(int stacks, double value, timespan_t duration) override {
buff_t::refresh(stacks, value, duration);
// The T21 4pc buff seems to be triggered on refresh as well as expire.
// As of build 25881, 2018-01-22.
debug_cast<mage_t *>(player)->buffs.arctic_blast->trigger();
}
virtual void expire_override(int stacks, timespan_t duration) override {
buff_t::expire_override(stacks, duration);
debug_cast<mage_t *>(player)->buffs.arctic_blast->trigger();
}
};
struct incanters_flow_t : public buff_t {
incanters_flow_t(mage_t *p) :
buff_t(p, "incanters_flow", p->find_spell(116267)) // Buff is a separate spell
{
set_duration(p->sim->max_time * 3); // Long enough duration to trip twice_expected_event
set_period(p->talents.incanters_flow->effectN(1).period()); // Period is in the talent
set_tick_behavior(BUFF_TICK_CLIP);
set_default_value(data().effectN(1).percent());
}
virtual void bump(int stacks, double value) override {
if (check() == max_stack())
reverse = true;
else
buff_t::bump(stacks, value);
}
virtual void decrement(int stacks, double value) override {
if (check() == 1)
reverse = false;
else
buff_t::decrement(stacks, value);
}
};
struct icy_veins_buff_t : public haste_buff_t {
icy_veins_buff_t(mage_t *p) :
haste_buff_t(haste_buff_creator_t(p, "icy_veins", p->find_spell(12472))) {
set_default_value(data().effectN(1).percent());
set_cooldown(timespan_t::zero());
buff_duration += p->talents.thermal_void->effectN(2).time_value();
}
virtual void expire_override(int stacks, timespan_t duration) override {
buff_t::expire_override(stacks, duration);
auto mage = debug_cast<mage_t *>(player);
mage->buffs.lady_vashjs_grasp->expire();
if (mage->talents.thermal_void->ok() && duration == timespan_t::zero()) {
mage->sample_data.icy_veins_duration->add(elapsed(sim->current_time()).total_seconds());
}
}
};
struct lady_vashjs_grasp_t : public buff_t {
proc_t *proc_fof;
lady_vashjs_grasp_t(mage_t *p) :
buff_t(buff_creator_t(p, "lady_vashjs_grasp", p->find_spell(208147))) {
// Disable by default.
default_chance = 0.0;
tick_zero = true;
set_tick_callback([this, p](buff_t * /* buff */, int /* ticks */, const timespan_t & /* tick_time */ ) {
p->buffs.fingers_of_frost->trigger();
p->buffs.fingers_of_frost->predict();
proc_fof->occur();
});
}
};
struct ray_of_frost_buff_t : public buff_t {
timespan_t rof_cd;
ray_of_frost_buff_t(mage_t *p) :
buff_t(buff_creator_t(p, "ray_of_frost", p->find_spell(208141))) {
set_default_value(data().effectN(1).percent());
const spell_data_t *rof_data = p->find_spell(205021);
rof_cd = rof_data->cooldown() - rof_data->duration();
}
virtual void expire_override(int stacks, timespan_t duration) override {
buff_t::expire_override(stacks, duration);
auto mage = debug_cast<mage_t *>(player);
// 205021 is the spell id for Ray of Frost
if (mage->channeling && mage->channeling->id == 205021) {
mage->channeling->interrupt_action();
}
mage->cooldowns.ray_of_frost->start(rof_cd);
}
};
} // buffs
namespace actions {
// ============================================================================
// Mage Spell
// ==========================================================================
struct mage_spell_state_t : public action_state_t {
// Simple bitfield for tracking sources of the Frozen effect.
unsigned frozen;
mage_spell_state_t(action_t *action, player_t *target) :
action_state_t(action, target),
frozen(0u) {}
virtual void initialize() override {
action_state_t::initialize();
frozen = 0u;
}
virtual std::ostringstream &debug_str(std::ostringstream &s) override {
action_state_t::debug_str(s) << " frozen=" << (frozen != 0u);
return s;
}
virtual void copy_state(const action_state_t *s) override {
action_state_t::copy_state(s);
auto mss = debug_cast<const mage_spell_state_t *>(s);
frozen = mss->frozen;
}
virtual double composite_crit_chance() const override {
double c = action_state_t::composite_crit_chance();
if (frozen) {
auto p = debug_cast<const mage_t *>(action->player);
// Multiplier is not in spell data, apparently.
c *= 1.5;
c += p->spec.shatter->effectN(2).percent() + p->spec.shatter_2->effectN(1).percent();
}
return c;
}
};
struct mage_spell_t : public mage_spell_base_t {
static const snapshot_state_e STATE_FROZEN = STATE_TGT_USER_1;
enum frozen_type_e {
FROZEN_WINTERS_CHILL = 0,
FROZEN_ROOT,
FROZEN_FINGERS_OF_FROST,
FROZEN_MAX
};
enum frozen_flag_e {
FF_WINTERS_CHILL = 1 << FROZEN_WINTERS_CHILL,
FF_ROOT = 1 << FROZEN_ROOT,
FF_FINGERS_OF_FROST = 1 << FROZEN_FINGERS_OF_FROST
};
bool track_cd_waste;
cooldown_waste_data_t *cd_waste;
public:
mage_spell_t(const std::string &n, mage_t *p,
const spell_data_t *s = spell_data_t::nil()) :
mage_spell_base_t(n, p, s, p),
track_cd_waste(false),
cd_waste(nullptr) {
affected_by.ice_floes = data().affected_by(p->talents.ice_floes->effectN(1));
track_cd_waste = data().cooldown() > timespan_t::zero() || data().charge_cooldown() > timespan_t::zero();
}
virtual void init() override {
mage_spell_base_t::init();
if (harmful && affected_by.shatter && p()->spec.shatter->ok()) {
snapshot_flags |= STATE_FROZEN;
update_flags |= STATE_FROZEN;
}
}
virtual bool init_finished() override {
if (track_cd_waste && sim->report_details != 0) {
cd_waste = p()->get_cooldown_waste_data(cooldown);
}
return mage_spell_base_t::init_finished();
}
mage_t *p() { return static_cast<mage_t *>( player ); }
const mage_t *p() const { return static_cast<mage_t *>( player ); }
mage_td_t *td(player_t *t) const { return p()->get_target_data(t); }
virtual action_state_t *new_state() override {
return new mage_spell_state_t(this, target);
}
virtual unsigned frozen(const action_state_t *s) const {
const mage_td_t *td = p()->target_data[s->target];
if (!td)
return 0u;
unsigned source = 0u;
if (td->debuffs.winters_chill->check())
source |= FF_WINTERS_CHILL;
if (td->debuffs.frozen->check())
source |= FF_ROOT;
return source;
}
virtual void snapshot_internal(action_state_t *s, unsigned flags, dmg_e rt) override {
if (flags & STATE_FROZEN) {
debug_cast<mage_spell_state_t *>(s)->frozen = frozen(s);
}
mage_spell_base_t::snapshot_internal(s, flags, rt);
}
virtual double cost() const override {
double c = mage_spell_base_t::cost();
if (p()->buffs.arcane_power->check()) {
c *= 1.0 + p()->buffs.arcane_power->data().effectN(2).percent()
+ p()->talents.overpowered->effectN(2).percent();
}
return c;
}
virtual void consume_resource() override {
mage_spell_base_t::consume_resource();
if (!harmful || current_resource() != RESOURCE_MANA || !p()->spec.clearcasting->ok()) {
return;
}
// TODO: Right now it uses max mana including mastery and other effects, probably makes more sense if
// it used base mana (might be a bug).
double mana_pct_consumed = last_resource_cost / p()->resources.max[RESOURCE_MANA];
double cc_proc_chance = mana_pct_consumed * p()->spec.clearcasting->effectN(1).percent();
p()->buffs.clearcasting->trigger(1, buff_t::DEFAULT_VALUE(), cc_proc_chance);
// TODO: Arcane Explosion consumes its own CC proc. Check.
}
virtual void update_ready(timespan_t cd) override {
if (cd_waste)
cd_waste->add(cd, time_to_execute);
mage_spell_base_t::update_ready(cd);
}
virtual bool usable_moving() const override {
if (p()->buffs.ice_floes->check() && affected_by.ice_floes) {
return true;
}
return mage_spell_base_t::usable_moving();
}
virtual void execute() override {
mage_spell_base_t::execute();
if (background)
return;
if (affected_by.ice_floes
&& p()->talents.ice_floes->ok()
&& time_to_execute > timespan_t::zero()
&& p()->buffs.ice_floes->up()) {
p()->buffs.ice_floes->decrement();
}
}
void trigger_unstable_magic(action_state_t *state);
// Helper methods for 7.2.5 fire shoulders and frost head.
void trigger_legendary_effect(buff_t *tracking_buff, buff_t *primed_buff, action_t *action, player_t *target) {
if (tracking_buff->check() == tracking_buff->max_stack() - 2) {
tracking_buff->expire();
primed_buff->trigger();
} else if (primed_buff->check() == 0) {
tracking_buff->trigger();
} else {
action->set_target(target);
action->execute();
// It looks like the debuff expiration is slightly delayed in game, allowing two spells
// impacting at the same time to trigger multiple Meteors or Comet Storms.
// As of build 25881, 2018-01-22.
primed_buff->expire(p()->bugs ? timespan_t::from_millis(30) : timespan_t::zero());
}
}
};
typedef residual_action::residual_periodic_action_t<mage_spell_t> residual_action_t;
// ============================================================================
// Arcane Mage Spell
// ============================================================================
struct arcane_mage_spell_t : public mage_spell_t {
bool triggers_erosion;
arcane_mage_spell_t(const std::string &n, mage_t *p,
const spell_data_t *s = spell_data_t::nil()) :
mage_spell_t(n, p, s),
triggers_erosion(true) {}
virtual void init() override {
mage_spell_t::init();
if (!harmful) {
triggers_erosion = false;
}
}
double savant_damage_bonus() const {
// TODO: Arcane Charge effect 2 and Savant effect 3 have the relevant into for Arcane Barrage,
// but the mastery snapshot bug makes it awkward to use (we can only store one in buff's value).
return p()->spec.arcane_charge->effectN(1).percent() +
p()->cache.mastery() * p()->spec.savant->effectN(2).mastery_value();
}
void trigger_arcane_charge(int stacks = 1) {
buff_t *ac = p()->buffs.arcane_charge;
if (p()->bugs) {
// The damage bonus given by mastery seems to be snapshot at the moment
// Arcane Charge is gained. As long as the stack number remains the same,
// any future changes to mastery will have no effect.
// As of build 25881, 2018-01-22.
if (ac->check() < ac->max_stack()) {
ac->trigger(stacks, savant_damage_bonus());
}
} else {
ac->trigger(stacks);
}
}
double arcane_charge_damage_bonus(bool arcane_barrage = false) const {
double per_ac_bonus = p()->bugs ? p()->buffs.arcane_charge->check_value() : savant_damage_bonus();
if (arcane_barrage) {
per_ac_bonus *= 0.5;
}
return 1.0 + p()->buffs.arcane_charge->check() * per_ac_bonus;
}
virtual void impact(action_state_t *s) override {
mage_spell_t::impact(s);
if (p()->talents.erosion->ok() && result_is_hit(s->result) && triggers_erosion) {
td(s->target)->debuffs.erosion->trigger();
}
}
};
// ============================================================================
// Fire Mage Spell
// ============================================================================
struct ignite_spell_state_t : public mage_spell_state_t {
bool hot_streak;
ignite_spell_state_t(action_t *action, player_t *target) :
mage_spell_state_t(action, target),
hot_streak(false) {}
virtual void initialize() override {
mage_spell_state_t::initialize();
hot_streak = false;
}
virtual std::ostringstream &debug_str(std::ostringstream &s) override {
mage_spell_state_t::debug_str(s) << " hot_streak=" << hot_streak;
return s;
}
virtual void copy_state(const action_state_t *s) override {
mage_spell_state_t::copy_state(s);
hot_streak = debug_cast<const ignite_spell_state_t *>(s)->hot_streak;
}
};
struct fire_mage_spell_t : public mage_spell_t {
bool triggers_hot_streak;
bool triggers_ignite;
fire_mage_spell_t(const std::string &n, mage_t *p,
const spell_data_t *s = spell_data_t::nil()) :
mage_spell_t(n, p, s),
triggers_hot_streak(false),
triggers_ignite(false) {}
// Use only after schedule_execute, which sets time_to_execute.
bool benefits_from_hot_streak(bool benefit_tracking = false) const {
// In-game, only instant cast Pyroblast and Flamestrike benefit from (and
// consume) Hot Streak.
return (benefit_tracking ? p()->buffs.hot_streak->up() : p()->buffs.hot_streak->check())
&& time_to_execute == timespan_t::zero();
}
virtual void impact(action_state_t *s) override {
mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
if (triggers_ignite) {
trigger_ignite(s);
}
if (triggers_hot_streak) {
handle_hot_streak(s);
}
}
}
void handle_hot_streak(action_state_t *s) {
mage_t *p = this->p();
if (!p->spec.hot_streak->ok())
return;
p->procs.hot_streak_spell->occur();
bool guaranteed = s->composite_crit_chance() >= 1.0;
if (s->result == RESULT_CRIT) {
p->procs.hot_streak_spell_crit->occur();
// Crit with HS => wasted crit
if (p->buffs.hot_streak->check()) {
p->procs.hot_streak_spell_crit_wasted->occur();
} else {
// Crit with HU => convert to HS
if (p->buffs.heating_up->up()) {
p->procs.hot_streak->occur();
// Check if HS was triggered by IB
if (s->action->data().id() == 108853) {
p->procs.heating_up_ib_converted->occur();
}
bool hu_react = p->buffs.heating_up->stack_react() > 0;
p->buffs.heating_up->expire();
p->buffs.hot_streak->trigger();
if (guaranteed && hu_react)
p->buffs.hot_streak->predict();
//TODO: Add proc tracking to this to track from talent or non-talent sources.
if (p->sets->has_set_bonus(MAGE_FIRE, T19, B4) &&
rng().roll(p->sets->set(MAGE_FIRE, T19, B4)->effectN(1).percent())) {
p->buffs.streaking->trigger();
}
}
// Crit without HU => generate HU
else {
p->procs.heating_up_generated->occur();
p->buffs.heating_up->trigger(
1, buff_t::DEFAULT_VALUE(), -1.0,
p->buffs.heating_up->buff_duration * p->cache.spell_speed());
if (guaranteed) {
p->buffs.heating_up->predict();
}
}
}
} else // Non-crit
{
// Non-crit with HU => remove HU
if (p->buffs.heating_up->check()) {
if (p->buffs.heating_up->elapsed(sim->current_time()) >
timespan_t::from_millis(200)) {
p->procs.heating_up_removed->occur();
p->buffs.heating_up->expire();
if (sim->debug) {
sim->out_log.printf("Heating up removed by non-crit");
}
} else {
if (sim->debug) {
sim->out_log.printf(
"Heating up removal ignored due to 200 ms protection");
}
}
}
}
}
virtual double composite_ignite_multiplier(const action_state_t * /* s */ ) const { return 1.0; }
void trigger_ignite(action_state_t *state) {
if (!p()->spec.ignite->ok())
return;
if (!result_is_hit(state->result))
return;
double m = state->target_da_multiplier;
if (m <= 0.0)
return;
double amount = state->result_total / m * p()->cache.mastery_value();
if (amount <= 0.0)
return;
// TODO: Use client data from hot streak
amount *= composite_ignite_multiplier(state);
bool ignite_exists = p()->ignite->get_dot(state->target)->is_ticking();
residual_action::trigger(p()->ignite, state->target, amount);
if (!ignite_exists) {
p()->procs.ignite_applied->occur();
}
}
bool firestarter_active(player_t *target) const {
if (!p()->talents.firestarter->ok())
return false;
// Check for user-specified override.
if (p()->firestarter_time > timespan_t::zero()) {
return sim->current_time() < p()->firestarter_time;
} else {
return target->health_percentage() > p()->talents.firestarter->effectN(1).base_value();
}
}
// Helper methods for Contained Infernal Core.
void trigger_infernal_core(player_t *target) {
trigger_legendary_effect(p()->buffs.contained_infernal_core,
p()->buffs.erupting_infernal_core,
p()->action.legendary_meteor,
target);
}
};
// ============================================================================
// Frost Mage Spell
// ============================================================================
// Some Frost spells snapshot on impact (rather than execute). This is handled via
// the calculate_on_impact flag.
//
// When set to true:
// * All snapshot flags are moved from snapshot_flags to impact_flags.
// * calculate_result and calculate_direct_amount don't do any calculations.
// * On spell impact:
// - State is snapshot via frost_mage_spell_t::impact_state.
// - Result is calculated via frost_mage_spell_t::calculate_impact_result.
// - Amount is calculated via frost_mage_spell_t::calculate_impact_direct_amount.
//
// The previous functions are virtual and can be overridden when needed.
struct frost_mage_spell_t : public mage_spell_t {
bool chills;
bool calculate_on_impact;
proc_t *proc_fof;
bool track_shatter;
proc_source_t *shatter_source;
unsigned impact_flags;
frost_mage_spell_t(const std::string &n, mage_t *p,
const spell_data_t *s = spell_data_t::nil())
: mage_spell_t(n, p, s),
chills(false),
calculate_on_impact(false),
proc_fof(nullptr),
track_shatter(false),
shatter_source(nullptr),
impact_flags(0u) {
affected_by.shatter = true;
}
virtual void init() override {
mage_spell_t::init();
if (calculate_on_impact) {
std::swap(snapshot_flags, impact_flags);
}
}
virtual bool init_finished() override {
if (track_shatter && sim->report_details != 0) {
shatter_source = p()->get_proc_source("Shatter/" + name_str, FROZEN_MAX);
}
return mage_spell_t::init_finished();
}
struct brain_freeze_delay_event_t : public event_t {
mage_t *mage;
brain_freeze_delay_event_t(mage_t *p, timespan_t delay)
: event_t(*p, delay), mage(p) {}
virtual const char *name() const override {
return "brain_freeze_delay";
}
virtual void execute() override {
mage->buffs.brain_freeze->trigger();
}
};
void trigger_fof(double chance, int stacks = 1, proc_t *source = nullptr) {
if (!source)
source = proc_fof;
bool success = p()->buffs.fingers_of_frost->trigger(stacks, buff_t::DEFAULT_VALUE(), chance);
if (success) {
if (!source) {
assert(false);
return;
}
for (int i = 0; i < stacks; i++)
source->occur();
if (chance >= 1.0)
p()->buffs.fingers_of_frost->predict();
}
}
void trigger_brain_freeze(double chance) {
if (rng().roll(chance)) {
if (p()->buffs.brain_freeze->check()) {
// Brain Freeze was already active, delay the new application
make_event<brain_freeze_delay_event_t>(*sim, p(), timespan_t::from_seconds(0.15));
} else {
p()->buffs.brain_freeze->trigger();
}
}
}
void trigger_icicle_gain(action_state_t *state) {
if (!p()->spec.icicles->ok())
return;
if (!result_is_hit(state->result))
return;
// Do not create zero damage Icicles (e.g. due to invulnerability events).
double m = state->target_da_multiplier;
if (m <= 0.0)
return;
double amount = state->result_total / m * p()->cache.mastery_value();
if (amount <= 0.0)
return;
if (p()->talents.splitting_ice->ok()) {
amount *= 1.0 + p()->talents.splitting_ice->effectN(3).percent();
}
assert(as<int>(p()->icicles.size()) <=
p()->spec.icicles->effectN(2).base_value());
// Shoot one if capped
if (as<int>(p()->icicles.size()) ==
p()->spec.icicles->effectN(2).base_value()) {
p()->trigger_icicle(state);
}
icicle_tuple_t tuple{p()->sim->current_time(), amount};
p()->icicles.push_back(tuple);
if (p()->sim->debug) {
p()->sim->out_debug.printf("%s icicle gain, damage=%f, total=%u",
p()->name(),
amount,
as<unsigned>(p()->icicles.size()));
}
}
virtual void impact_state(action_state_t *s, dmg_e rt) { snapshot_internal(s, impact_flags, rt); }
virtual double calculate_direct_amount(action_state_t *s) const override {
if (!calculate_on_impact) {
return mage_spell_t::calculate_direct_amount(s);
} else {
// Don't do any extra work, this result won't be used.
return 0.0;
}
}
virtual double calculate_impact_direct_amount(action_state_t *s) const {
return mage_spell_t::calculate_direct_amount(s);
}
virtual result_e calculate_result(action_state_t *s) const override {
if (!calculate_on_impact) {
return mage_spell_t::calculate_result(s);
} else {
// Don't do any extra work, this result won't be used.
return RESULT_NONE;
}
}
virtual result_e calculate_impact_result(action_state_t *s) const { return mage_spell_t::calculate_result(s); }
void record_shatter_source(const action_state_t *s, proc_source_t *source = nullptr) {
unsigned frozen = debug_cast<const mage_spell_state_t *>(s)->frozen;
if (!frozen)
return;
if (!source)
source = shatter_source;
assert(source);
if (frozen & FF_WINTERS_CHILL)
source->occur(FROZEN_WINTERS_CHILL);
else if (frozen & ~FF_FINGERS_OF_FROST)
source->occur(FROZEN_ROOT);
else
source->occur(FROZEN_FINGERS_OF_FROST);
}
virtual void impact(action_state_t *s) override {
if (calculate_on_impact) {
// Re-call functions here, before the impact call to do the damage calculations as we impact.
impact_state(s, amount_type(s));
s->result = calculate_impact_result(s);
s->result_amount = calculate_impact_direct_amount(s);
}
mage_spell_t::impact(s);
if (result_is_hit(s->result) && shatter_source) {
record_shatter_source(s);
}
if (result_is_hit(s->result) && chills && p()->talents.bone_chilling->ok()) {
p()->buffs.bone_chilling->trigger();
}
}
// Helper methods for Shattered Fragments of Sindragosa.
void trigger_shattered_fragments(player_t *target) {
trigger_legendary_effect(p()->buffs.shattered_fragments_of_sindragosa,
p()->buffs.rage_of_the_frost_wyrm,
p()->action.legendary_comet_storm,
target);
}
};
// Icicles ==================================================================
struct icicle_t : public frost_mage_spell_t {
icicle_t(mage_t *p) :
frost_mage_spell_t("icicle", p, p->find_spell(148022)) {
may_crit = affected_by.shatter = false;
proc = background = true;
base_dd_min = base_dd_max = 1.0;
if (p->talents.splitting_ice->ok()) {
aoe = 1 + p->talents.splitting_ice->effectN(1).base_value();
base_aoe_multiplier *= p->talents.splitting_ice
->effectN(2).percent();
}
}
virtual void init() override {
frost_mage_spell_t::init();
snapshot_flags &= STATE_NO_MULTIPLIER;
snapshot_flags |= STATE_TGT_MUL_DA;
}
};
// Presence of Mind Spell ===================================================
struct presence_of_mind_t : public arcane_mage_spell_t {
presence_of_mind_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("presence_of_mind", p,
p->find_specialization_spell("Presence of Mind")) {
parse_options(options_str);
harmful = false;
}
virtual bool ready() override {
if (p()->buffs.presence_of_mind->check()) {
return false;
}
return arcane_mage_spell_t::ready();
}
virtual void execute() override {
arcane_mage_spell_t::execute();
p()->buffs.presence_of_mind
->trigger(p()->buffs.presence_of_mind->max_stack());
if (p()->sets->has_set_bonus(MAGE_ARCANE, T20, B2)) {
trigger_arcane_charge(4);
p()->buffs.crackling_energy->trigger();
}
}
};
// Conflagration Spell =====================================================
struct conflagration_dot_t : public fire_mage_spell_t {
conflagration_dot_t(mage_t *p) :
fire_mage_spell_t("conflagration_dot", p, p->find_spell(226757)) {
hasted_ticks = false;
tick_may_crit = may_crit = false;
background = true;
}
};
struct conflagration_t : public fire_mage_spell_t {
conflagration_t(mage_t *p) :
fire_mage_spell_t("conflagration_explosion", p, p->talents.conflagration) {
parse_effect_data(p->find_spell(205345)->effectN(1));
callbacks = false;
background = true;
aoe = -1;
}
};
// Ignite Spell ===================================================================
struct ignite_t : public residual_action_t {
conflagration_t *conflagration;
ignite_t(mage_t *p) :
residual_action_t("ignite", p, p->find_spell(12846)),
conflagration(nullptr) {
dot_duration = p->find_spell(12654)->duration();
base_tick_time = p->find_spell(12654)->effectN(1).period();
school = SCHOOL_FIRE;
//!! NOTE NOTE NOTE !! This is super dangerous and means we have to be extra careful with correctly
// flagging thats that proc off events, to not proc off ignite if they shouldn't!
callbacks = true;
if (p->talents.conflagration->ok()) {
conflagration = new conflagration_t(p);
}
}
virtual void init() override {
residual_action_t::init();
snapshot_flags |= STATE_TGT_MUL_TA;
update_flags |= STATE_TGT_MUL_TA;
}
virtual void tick(dot_t *dot) override {
residual_action_t::tick(dot);
if (p()->talents.conflagration->ok() &&
rng().roll(p()->talents.conflagration->effectN(1).percent())) {
conflagration->set_target(dot->target);
conflagration->execute();
}
}
};
// Arcane Barrage Spell =======================================================
struct arcane_barrage_t : public arcane_mage_spell_t {
double mystic_kilt_of_the_rune_master_regen;
double mantle_of_the_first_kirin_tor_chance;
arcane_barrage_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("arcane_barrage", p, p->find_specialization_spell("Arcane Barrage")),
mystic_kilt_of_the_rune_master_regen(0.0),
mantle_of_the_first_kirin_tor_chance(0.0) {
parse_options(options_str);
base_aoe_multiplier *= data().effectN(2).percent();
cooldown->hasted = true;
if (p->action.legendary_arcane_orb) {
add_child(p->action.legendary_arcane_orb);
}
}
virtual void execute() override {
// Mantle of the First Kirin Tor has some really weird interactions. When ABar is cast, the number
// of targets is decided first, then the roll for Arcane Orb happens. If it succeeds, Orb is cast
// and the mage gains an Arcane Charge. This extra charge counts towards the bonus damage and also
// towards Mystic Kilt of the Rune Master. After everything is done, Arcane Charges are reset.
//
// Hard to tell which part (if any) is a bug.
// TODO: Check this.
int charges = p()->buffs.arcane_charge->check();
if (p()->spec.arcane_barrage_2->ok())
aoe = (charges == 0) ? 0 : 1 + charges;
if (rng().roll(mantle_of_the_first_kirin_tor_chance * charges)) {
assert(p()->action.legendary_arcane_orb);
p()->action.legendary_arcane_orb->set_target(target);
p()->action.legendary_arcane_orb->execute();
// Update charges for Mystic Kilt of the Rune Master mana gain.
charges = p()->buffs.arcane_charge->check();
}
p()->benefits.arcane_charge.arcane_barrage->update();
if (mystic_kilt_of_the_rune_master_regen > 0 && charges > 0) {
p()->resource_gain(
RESOURCE_MANA,
charges * mystic_kilt_of_the_rune_master_regen * p()->resources.max[RESOURCE_MANA],
p()->gains.mystic_kilt_of_the_rune_master);
}
arcane_mage_spell_t::execute();
if (p()->sets->has_set_bonus(MAGE_ARCANE, T21, B2)) {
p()->buffs.expanding_mind
->trigger(1, charges * p()->sets->set(MAGE_ARCANE, T21, B2)->effectN(1).percent());
}
p()->buffs.arcane_charge->expire();
}
virtual void impact(action_state_t *s) override {
arcane_mage_spell_t::impact(s);
if (p()->talents.chrono_shift->ok()) {
p()->buffs.chrono_shift->trigger();
}
}
virtual double action_multiplier() const override {
double am = arcane_mage_spell_t::action_multiplier();
am *= arcane_charge_damage_bonus(true);
if (p()->talents.resonance->ok()) {
int targets = std::min(n_targets(), as<int>(target_list().size()));
am *= 1.0 + p()->talents.resonance->effectN(1).percent() * targets;
}
return am;
}
};
// Arcane Blast Spell =======================================================
struct arcane_blast_t : public arcane_mage_spell_t {
arcane_blast_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("arcane_blast", p, p->find_specialization_spell("Arcane Blast")) {
parse_options(options_str);
if (p->specialization() == MAGE_ARCANE && p->action.unstable_magic_explosion) {
add_child(p->action.unstable_magic_explosion);
}
}
virtual double cost() const override {
double c = arcane_mage_spell_t::cost();
c *= 1.0 + p()->buffs.arcane_charge->check() *
p()->spec.arcane_charge->effectN(5).percent();
c *= 1.0 + p()->buffs.rule_of_threes->check_value();
//TODO: Find a work-around to remove hardcoding
if (p()->buffs.rhonins_assaulting_armwraps->check()) {
c = 0;
}
return c;
}
virtual void execute() override {
p()->benefits.arcane_charge.arcane_blast->update();
arcane_mage_spell_t::execute();
p()->buffs.rhonins_assaulting_armwraps->expire();
if (last_resource_cost == 0) {
p()->buffs.rule_of_threes->expire();
}
p()->buffs.arcane_charge->up();
if (hit_any_target) {
trigger_arcane_charge();
// Behavior assumes only builders ( AE/AB ) will trigger. If CU ends up triggering, will need to change.
// For now, only AB seems to trigger it.
if (p()->talents.rule_of_threes->ok() && p()->buffs.arcane_charge->check() == 3) {
p()->buffs.rule_of_threes->trigger();
}
}
if (p()->buffs.presence_of_mind->up()) {
p()->buffs.presence_of_mind->decrement();
}
p()->buffs.t19_oh_buff->trigger();
p()->buffs.quick_thinker->trigger();
}
virtual double action_multiplier() const override {
double am = arcane_mage_spell_t::action_multiplier();
am *= arcane_charge_damage_bonus();
return am;
}
virtual timespan_t execute_time() const override {
if (p()->buffs.presence_of_mind->check()) {
return timespan_t::zero();
}
timespan_t t = arcane_mage_spell_t::execute_time();
t *= 1.0 + p()->buffs.arcane_charge->check() *
p()->spec.arcane_charge->effectN(4).percent();
return t;
}
virtual void impact(action_state_t *s) override {
arcane_mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
trigger_unstable_magic(s);
}
}
};
// Arcane Explosion Spell =====================================================
struct arcane_explosion_t : public arcane_mage_spell_t {
arcane_explosion_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("arcane_explosion", p,
p->find_specialization_spell("Arcane Explosion")) {
parse_options(options_str);
aoe = -1;
}
virtual double cost() const override {
double c = arcane_mage_spell_t::cost();
c *= 1.0 + p()->buffs.clearcasting->check_value();
return c;
}
virtual void execute() override {
arcane_mage_spell_t::execute();
if (last_resource_cost == 0) {
p()->buffs.clearcasting->expire();
}
p()->buffs.quick_thinker->trigger();
}
};
// Arcane Intellect Spell ===================================================
struct arcane_intellect_t : public mage_spell_t {
arcane_intellect_t(mage_t *p, const std::string &options_str) :
mage_spell_t("arcane_intellect", p, p->find_class_spell("Arcane Intellect")) {
parse_options(options_str);
harmful = false;
ignore_false_positive = true;
background = sim->overrides.arcane_intellect != 0;
}
virtual void execute() override {
mage_spell_t::execute();
if (!sim->overrides.arcane_intellect)
sim->auras.arcane_intellect->trigger();
}
};
// Arcane Missiles Spell ====================================================
struct arcane_missiles_tick_t : public arcane_mage_spell_t {
arcane_missiles_tick_t(mage_t *p) :
arcane_mage_spell_t("arcane_missiles_tick", p,
p->find_specialization_spell("Arcane Missiles")
->effectN(2).trigger()) {
background = true;
}
virtual void impact(action_state_t *s) override {
arcane_mage_spell_t::impact(s);
p()->buffs.cord_of_infinity->trigger();
}
};
struct am_state_t : public mage_spell_state_t {
timespan_t extra_tick_time_reduction;
am_state_t(action_t *action, player_t *target) :
mage_spell_state_t(action, target),
extra_tick_time_reduction() {}
virtual void initialize() override {
mage_spell_state_t::initialize();
extra_tick_time_reduction = timespan_t::zero();
}
virtual std::ostringstream &debug_str(std::ostringstream &s) override {
mage_spell_state_t::debug_str(s)
<< " extra_tick_time_reduction=" << extra_tick_time_reduction;
return s;
}
virtual void copy_state(const action_state_t *other) override {
mage_spell_state_t::copy_state(other);
extra_tick_time_reduction = debug_cast<const am_state_t *>(other)->extra_tick_time_reduction;
}
};
struct arcane_missiles_t : public arcane_mage_spell_t {
timespan_t mana_adept_reduction;
arcane_missiles_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("arcane_missiles", p,
p->find_specialization_spell("Arcane Missiles")) {
parse_options(options_str);
may_miss = false;
triggers_erosion = false;
dot_duration = data().duration();
base_tick_time = data().effectN(2).period();
tick_zero = true;
channeled = true;
hasted_ticks = false;
dynamic_tick_action = true;
tick_action = new arcane_missiles_tick_t(p);
if (p->talents.mana_adept->ok()) {
mana_adept_reduction = p->talents.mana_adept->effectN(1).trigger()->effectN(1).time_value();
}
}
// Flag Arcane Missiles as direct damage for triggering effects
virtual dmg_e amount_type(const action_state_t * /* state */, bool /* periodic */) const override {
return DMG_DIRECT;
}
virtual action_state_t *new_state() override { return new am_state_t(this, target); }
// We need to snapshot any tick time reduction effect here so that it correctly affects the whole
// channel.
virtual void snapshot_state(action_state_t *state, dmg_e rt) override {
arcane_mage_spell_t::snapshot_state(state, rt);
if (p()->talents.mana_adept->ok()
&& p()->resources.pct(RESOURCE_MANA) > p()->talents.mana_adept->effectN(1).percent()) {
debug_cast<am_state_t *>(state)->extra_tick_time_reduction = mana_adept_reduction;
}
}
virtual timespan_t composite_dot_duration(const action_state_t *s) const override {
// AM channel duration is a bit fuzzy, it will go above or below the standard 2 s
// to make sure it has the correct number of ticks.
timespan_t full_duration = dot_duration * s->haste;
timespan_t tick_duration = tick_time(s);
double ticks = std::round(full_duration / tick_duration);
return ticks * tick_duration;
}
virtual timespan_t tick_time(const action_state_t *s) const override {
timespan_t t = base_tick_time;
t += debug_cast<const am_state_t *>(s)->extra_tick_time_reduction;
t *= s->haste;
return t;
}
virtual double last_tick_factor(const dot_t *, const timespan_t &, const timespan_t &) const override {
// AM always does full damage, even on "partial" ticks.
return 1.0;
}
timespan_t calculate_dot_refresh_duration(const dot_t *dot, timespan_t triggered_duration) const override {
// 30% refresh rule apparently works with AM as well!
return triggered_duration + std::min(triggered_duration * 0.3, dot->remains());
}
virtual double cost() const override {
double c = arcane_mage_spell_t::cost();
c *= 1.0 + p()->buffs.rule_of_threes->check_value();
c *= 1.0 + p()->buffs.clearcasting->check_value();
return c;
}
virtual void execute() override {
arcane_mage_spell_t::execute();
p()->buffs.rhonins_assaulting_armwraps->trigger();
if (last_resource_cost == 0) {
if (p()->buffs.clearcasting->check()) {
p()->buffs.clearcasting->expire();
} else {
p()->buffs.rule_of_threes->expire();
}
}
if (p()->sets->has_set_bonus(MAGE_ARCANE, T19, B4)) {
p()->cooldowns.evocation
->adjust(-1000 * p()->sets->set(MAGE_ARCANE, T19, B4)->effectN(1).time_value());
}
if (p()->sets->has_set_bonus(MAGE_ARCANE, T20, B4)) {
p()->cooldowns.presence_of_mind
->adjust(-100 * p()->sets->set(MAGE_ARCANE, T20, B4)->effectN(1).time_value());
}
p()->buffs.quick_thinker->trigger();
}
virtual bool usable_moving() const override {
if (p()->talents.slipstream->ok())
return true;
return arcane_mage_spell_t::usable_moving();
}
};
// Arcane Orb Spell ===========================================================
struct arcane_orb_bolt_t : public arcane_mage_spell_t {
arcane_orb_bolt_t(mage_t *p, bool legendary) :
arcane_mage_spell_t(legendary ? "legendary_arcane_orb_bolt" : "arcane_orb_bolt",
p, p->find_spell(153640)) {
aoe = -1;
background = true;
}
virtual void impact(action_state_t *s) override {
arcane_mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
trigger_arcane_charge();
p()->buffs.quick_thinker->trigger();
}
}
};
struct arcane_orb_t : public arcane_mage_spell_t {
arcane_orb_bolt_t *orb_bolt;
arcane_orb_t(mage_t *p, const std::string &options_str, bool legendary = false) :
arcane_mage_spell_t(legendary ? "legendary_arcane_orb" : "arcane_orb", p,
p->find_talent_spell("Arcane Orb", SPEC_NONE, false, !legendary)),
orb_bolt(new arcane_orb_bolt_t(p, legendary)) {
parse_options(options_str);
may_miss = false;
may_crit = false;
triggers_erosion = false;
if (legendary) {
background = true;
base_costs[RESOURCE_MANA] = 0;
}
add_child(orb_bolt);
}
virtual void execute() override {
arcane_mage_spell_t::execute();
trigger_arcane_charge();
}
virtual timespan_t travel_time() const override {
return timespan_t::from_seconds(
std::max(0.1, (player->get_player_distance(*target) - 10.0) / 16.0));
}
virtual void impact(action_state_t *s) override {
arcane_mage_spell_t::impact(s);
orb_bolt->set_target(s->target);
orb_bolt->execute();
}
};
// Arcane Power Spell =======================================================
struct arcane_power_t : public arcane_mage_spell_t {
arcane_power_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("arcane_power", p,
p->find_specialization_spell("Arcane Power")) {
parse_options(options_str);
harmful = false;
}
virtual void execute() override {
arcane_mage_spell_t::execute();
p()->buffs.arcane_power->trigger();
}
};
// Blast Wave Spell ==========================================================
struct blast_wave_t : public fire_mage_spell_t {
blast_wave_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("blast_wave", p, p->talents.blast_wave) {
parse_options(options_str);
aoe = -1;
}
};
// Blink Spell ==============================================================
struct blink_t : public mage_spell_t {
blink_t(mage_t *p, const std::string &options_str) :
mage_spell_t("blink", p, p->find_class_spell("Blink")) {
parse_options(options_str);
harmful = false;
ignore_false_positive = true;
base_teleport_distance = data().effectN(1).radius_max();
movement_directionality = MOVEMENT_OMNI;
if (p->talents.shimmer->ok()) {
background = true;
}
}
};
// Blizzard Spell ===========================================================
struct blizzard_shard_t : public frost_mage_spell_t {
blizzard_shard_t(mage_t *p) :
frost_mage_spell_t("blizzard_shard", p, p->find_spell(190357)) {
aoe = -1;
background = true;
ground_aoe = true;
base_multiplier *= 1.0 + p->talents.arctic_gale->effectN(1).percent();
chills = true;
}
virtual void execute() override {
frost_mage_spell_t::execute();
if (hit_any_target) {
timespan_t base_cd_reduction = -10.0 * p()->spec.blizzard_2->effectN(1).time_value();
timespan_t total_cd_reduction = num_targets_hit * base_cd_reduction;
p()->sample_data.blizzard->add(total_cd_reduction);
p()->cooldowns.frozen_orb->adjust(total_cd_reduction);
}
}
virtual double composite_persistent_multiplier(const action_state_t *s) const override {
double cpm = frost_mage_spell_t::composite_persistent_multiplier(s);
cpm *= 1.0 + p()->buffs.zannesu_journey->check_stack_value();
return cpm;
}
};
struct blizzard_t : public frost_mage_spell_t {
blizzard_shard_t *blizzard_shard;
blizzard_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("blizzard", p, p->find_specialization_spell("Blizzard")),
blizzard_shard(new blizzard_shard_t(p)) {
parse_options(options_str);
add_child(blizzard_shard);
cooldown->hasted = true;
dot_duration = timespan_t::zero(); // This is just a driver for the ground effect.
may_miss = false;
may_crit = affected_by.shatter = false;
}
virtual double false_positive_pct() const override {
// Players are probably less likely to accidentally use blizzard than other spells.
return (frost_mage_spell_t::false_positive_pct() / 2);
}
virtual void execute() override {
frost_mage_spell_t::execute();
if (p()->buffs.zannesu_journey->default_chance != 0.0) {
p()->benefits.zannesu_journey->update();
}
timespan_t ground_aoe_duration = data().duration() * player->cache.spell_speed();
p()->ground_aoe_expiration[name_str]
= sim->current_time() + ground_aoe_duration;
make_event<ground_aoe_event_t>(*sim, p(), ground_aoe_params_t()
.target(execute_state->target)
.duration(ground_aoe_duration)
.action(blizzard_shard)
.hasted(ground_aoe_params_t::SPELL_SPEED));
p()->buffs.zannesu_journey->expire();
}
};
// Charged Up Spell =========================================================
struct charged_up_t : public arcane_mage_spell_t {
charged_up_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("charged_up", p, p->talents.charged_up) {
parse_options(options_str);
harmful = false;
}
virtual void execute() override {
arcane_mage_spell_t::execute();
trigger_arcane_charge(4);
for (int i = 0; i < 4; i++)
if (p()->buffs.quick_thinker->trigger())
break;
}
};
// Cinderstorm Spell ==========================================================
// Cinderstorm travel mechanism:
// http://blue.mmo-champion.com/topic/409203-theorycrafting-questions/#post114
// "9.17 degrees" is assumed to be a rounded value of 0.16 radians.
// For distance k and deviation angle x, the arclength is k * x / sin(x).
// From testing, cinders have a variable velocity, averaging ~30 yards/second.
struct cinder_t : public fire_mage_spell_t {
cinder_t(mage_t *p) :
fire_mage_spell_t("cinder", p, p->find_spell(198928)) {
background = true;
aoe = -1;
triggers_ignite = true;
}
virtual double composite_target_multiplier(player_t *target) const override {
double m = fire_mage_spell_t::composite_target_multiplier(target);
if (p()->ignite->get_dot(target)->is_ticking()) {
m *= 1.0 + p()->talents.cinderstorm->effectN(1).percent();
}
return m;
}
};
struct cinderstorm_t : public fire_mage_spell_t {
cinder_t *cinder;
int cinder_count;
const double cinder_velocity_mean = 30.0; // Yards per second
const double cinder_velocity_range = 6.0; // Yards per second
const double cinder_converge_mean = 31.0; // Yards
const double cinder_converge_range = 2.0; // Yards
const double cinder_angle = 0.16; // Radians
cinderstorm_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("cinderstorm", p, p->talents.cinderstorm),
cinder(new cinder_t(p)),
cinder_count(6) {
add_option(opt_int("cinders", cinder_count));
parse_options(options_str);
cooldown->hasted = true;
add_child(cinder);
}
virtual void execute() override {
if (p()->global_cinder_count > 0) {
cinder_count = static_cast<int>( p()->global_cinder_count );
}
fire_mage_spell_t::execute();
double target_dist = player->get_player_distance(*execute_state->target);
double cinder_converge_distance =
rng().range(cinder_converge_mean - cinder_converge_range,
cinder_converge_mean + cinder_converge_range);
// When cinder_count < 6, we assume "curviest" cinders are first to miss
for (int i = 1; i <= cinder_count; i++) {
// TODO: Optimize this code by caching theta and trig functions
timespan_t travel_time;
// Cinder deviation angle from "forward"
double theta = cinder_angle * i;
// Radius of arc drawn by cinder
double radius = cinder_converge_distance / (2.0 * sin(theta));
// Randomized cinder velocity
double cinder_velocity =
rng().range(cinder_velocity_mean - cinder_velocity_range,
cinder_velocity_mean + cinder_velocity_range);
if (target_dist > cinder_converge_distance) {
// Time spent "curving around"
timespan_t arc_time =
timespan_t::from_seconds(radius * 2 * theta / cinder_velocity);
// Time spent travelling straight at an angle, after curving
timespan_t straight_time = timespan_t::from_seconds(
// Residual distance beyond point of convergence
(target_dist - cinder_converge_distance) /
// Divided by magnitude of velocity in forward direction
(cinder_velocity * cos(theta))
);
// Travel time is equal to the sum of traversing arc and straight path
travel_time = arc_time + straight_time;
} else {
// Use Cinderstorm's arc's symmetry to simplify calculations
// First calculate the offset distance and angle from halfway
double offset_dist = target_dist - (cinder_converge_distance / 2.0);
double offset_angle = asin(offset_dist / radius);
// Using this offset, we calculate the arc angle traced before impact,
// which also gives us arc length
double arc_angle = theta + offset_angle;
double arc_dist = radius * arc_angle;
// Divide by cinder velocity to obtain travel time
travel_time = timespan_t::from_seconds(arc_dist / cinder_velocity);
}
make_event<events::cinder_impact_event_t>(*sim, *p(), cinder, target,
travel_time);
}
}
};
// Cold Snap Spell ============================================================
struct cold_snap_t : public frost_mage_spell_t {
cold_snap_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("cold_snap", p, p->find_specialization_spell("Cold Snap")) {
parse_options(options_str);
harmful = false;
};
virtual void execute() override {
frost_mage_spell_t::execute();
p()->cooldowns.cone_of_cold->reset(false);
p()->cooldowns.frost_nova->reset(false);
}
};
// Combustion Spell ===========================================================
struct combustion_t : public fire_mage_spell_t {
combustion_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("combustion", p, p->find_specialization_spell("Combustion")) {
parse_options(options_str);
dot_duration = timespan_t::zero();
harmful = false;
}
virtual void execute() override {
fire_mage_spell_t::execute();
p()->buffs.combustion->trigger();
if (p()->sets->has_set_bonus(MAGE_FIRE, T21, B4)) {
p()->buffs.inferno->trigger();
}
}
};
// Comet Storm Spell =======================================================
struct comet_storm_projectile_t : public frost_mage_spell_t {
comet_storm_projectile_t(mage_t *p, bool legendary) :
frost_mage_spell_t(legendary ? "legendary_comet_storm_projectile" : "comet_storm_projectile", p,
p->find_spell(153596)) {
aoe = -1;
background = true;
}
};
struct comet_storm_t : public frost_mage_spell_t {
comet_storm_projectile_t *projectile;
comet_storm_t(mage_t *p, const std::string &options_str, bool legendary = false) :
frost_mage_spell_t(legendary ? "legendary_comet_storm" : "comet_storm",
p, p->find_talent_spell("Comet Storm", SPEC_NONE, false, !legendary)),
projectile(new comet_storm_projectile_t(p, legendary)) {
parse_options(options_str);
may_miss = false;
may_crit = affected_by.shatter = false;
add_child(projectile);
if (legendary) {
background = true;
base_costs[RESOURCE_MANA] = 0;
}
}
virtual timespan_t travel_time() const override {
return timespan_t::from_seconds(1.0);
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
timespan_t ground_aoe_duration = timespan_t::from_seconds(1.2);
p()->ground_aoe_expiration[name_str]
= sim->current_time() + ground_aoe_duration;
make_event<ground_aoe_event_t>(*sim, p(), ground_aoe_params_t()
.pulse_time(timespan_t::from_seconds(0.2))
.target(s->target)
.duration(ground_aoe_duration)
.action(projectile), true);
}
};
// Cone of Cold Spell =======================================================
struct cone_of_cold_t : public frost_mage_spell_t {
cone_of_cold_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("cone_of_cold", p,
p->find_specialization_spell("Cone of Cold")) {
parse_options(options_str);
aoe = -1;
chills = true;
}
};
// Counterspell Spell =======================================================
struct counterspell_t : public mage_spell_t {
counterspell_t(mage_t *p, const std::string &options_str) :
mage_spell_t("counterspell", p, p->find_class_spell("Counterspell")) {
parse_options(options_str);
may_miss = may_crit = false;
ignore_false_positive = true;
}
virtual void execute() override {
mage_spell_t::execute();
p()->apply_crowd_control(execute_state, MECHANIC_INTERRUPT);
}
virtual bool ready() override {
if (!target->debuffs.casting || !target->debuffs.casting->check()) {
return false;
}
return mage_spell_t::ready();
}
};
// Dragon's Breath Spell ====================================================
struct dragons_breath_t : public fire_mage_spell_t {
dragons_breath_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("dragons_breath", p,
p->find_specialization_spell("Dragon's Breath")) {
parse_options(options_str);
aoe = -1;
if (p->talents.alexstraszas_fury->ok()) {
base_crit = 1.0;
}
}
virtual void impact(action_state_t *s) override {
fire_mage_spell_t::impact(s);
if (p()->talents.alexstraszas_fury->ok() && s->chain_target == 0) {
handle_hot_streak(s);
}
p()->apply_crowd_control(s, MECHANIC_DISORIENT);
}
};
// Evocation Spell ==========================================================
struct evocation_t : public arcane_mage_spell_t {
evocation_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("evocation", p,
p->find_specialization_spell("Evocation")) {
parse_options(options_str);
base_tick_time = timespan_t::from_seconds(2.0);
channeled = true;
dot_duration = data().duration();
harmful = false;
hasted_ticks = false;
tick_zero = true;
ignore_false_positive = true;
cooldown->duration *= 1.0 + p->spec.evocation_2->effectN(1).percent();
}
virtual void tick(dot_t *d) override {
arcane_mage_spell_t::tick(d);
double mana_gain = p()->resources.max[RESOURCE_MANA] * data().effectN(1).percent();
p()->resource_gain(RESOURCE_MANA, mana_gain, p()->gains.evocation);
}
virtual bool usable_moving() const override {
if (p()->talents.slipstream->ok())
return true;
return arcane_mage_spell_t::usable_moving();
}
};
// Fireball Spell ===========================================================
struct fireball_t : public fire_mage_spell_t {
conflagration_dot_t *conflagration_dot;
fireball_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("fireball", p, p->find_class_spell("Fireball")),
conflagration_dot(new conflagration_dot_t(p)) {
parse_options(options_str);
triggers_hot_streak = true;
triggers_ignite = true;
add_child(conflagration_dot);
if (p->specialization() == MAGE_FIRE && p->action.unstable_magic_explosion) {
add_child(p->action.unstable_magic_explosion);
}
}
virtual timespan_t travel_time() const override {
timespan_t t = fire_mage_spell_t::travel_time();
return std::min(timespan_t::from_seconds(0.75), t);
}
virtual void execute() override {
fire_mage_spell_t::execute();
if (p()->sets->has_set_bonus(MAGE_FIRE, T20, B2)) {
p()->buffs.ignition->trigger();
}
p()->buffs.t19_oh_buff->trigger();
}
virtual void impact(action_state_t *s) override {
fire_mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
if (s->result == RESULT_CRIT) {
p()->buffs.enhanced_pyrotechnics->expire();
} else {
p()->buffs.enhanced_pyrotechnics->trigger();
}
if (p()->talents.kindling->ok() && s->result == RESULT_CRIT) {
p()->cooldowns.combustion
->adjust(-1000 * p()->talents.kindling
->effectN(1).time_value());
}
if (p()->talents.conflagration->ok()) {
conflagration_dot->set_target(s->target);
conflagration_dot->execute();
}
trigger_unstable_magic(s);
trigger_infernal_core(s->target);
}
}
virtual double composite_target_crit_chance(player_t *target) const override {
double c = fire_mage_spell_t::composite_target_crit_chance(target);
if (firestarter_active(target)) {
c = 1.0;
}
return c;
}
virtual double composite_crit_chance() const override {
double c = fire_mage_spell_t::composite_crit_chance();
c += p()->buffs.enhanced_pyrotechnics->check_stack_value();
return c;
}
};
// Flame Patch Spell ==========================================================
struct flame_patch_t : public fire_mage_spell_t {
flame_patch_t(mage_t *p) :
fire_mage_spell_t("flame_patch", p, p->talents.flame_patch) {
parse_effect_data(p->find_spell(205472)->effectN(1));
aoe = -1;
ground_aoe = background = true;
school = SCHOOL_FIRE;
}
// Override damage type to avoid triggering Doom Nova
virtual dmg_e amount_type(const action_state_t * /* state */,
bool /* periodic */) const override {
return DMG_OVER_TIME;
}
};
// Flamestrike Spell ==========================================================
struct flamestrike_t : public fire_mage_spell_t {
flame_patch_t *flame_patch;
timespan_t flame_patch_duration;
flamestrike_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("flamestrike", p,
p->find_specialization_spell("Flamestrike")),
flame_patch(new flame_patch_t(p)),
flame_patch_duration(p->find_spell(205470)->duration()) {
parse_options(options_str);
triggers_ignite = true;
aoe = -1;
add_child(flame_patch);
}
virtual action_state_t *new_state() override {
return new ignite_spell_state_t(this, target);
}
virtual timespan_t execute_time() const override {
if (p()->buffs.hot_streak->check()) {
return timespan_t::zero();
}
return fire_mage_spell_t::execute_time();
}
virtual void execute() override {
bool hot_streak = benefits_from_hot_streak(true);
fire_mage_spell_t::execute();
// Ignition/Critical Massive buffs are removed shortly after Flamestrike/Pyroblast cast.
// In a situation where you're hardcasting FS/PB followed by a Hot Streak FS/FB, both
// spells actually benefit. As of build 25881, 2018-01-22.
p()->buffs.ignition->expire(p()->bugs ? timespan_t::from_millis(15) : timespan_t::zero());
p()->buffs.critical_massive->expire(p()->bugs ? timespan_t::from_millis(15) : timespan_t::zero());
if (hot_streak) {
p()->buffs.hot_streak->expire();
if (p()->talents.pyromaniac->ok()
&& rng().roll(p()->talents.pyromaniac->effectN(1).percent())) {
p()->procs.hot_streak->occur();
p()->procs.hot_streak_pyromaniac->occur();
p()->buffs.hot_streak->trigger();
}
}
}
virtual void impact(action_state_t *state) override {
fire_mage_spell_t::impact(state);
if (p()->sets->has_set_bonus(MAGE_FIRE, T20, B4) && state->result == RESULT_CRIT) {
p()->buffs.critical_massive->trigger();
}
if (state->chain_target == 0 && p()->talents.flame_patch->ok()) {
p()->ground_aoe_expiration[flame_patch->name_str]
= sim->current_time() + flame_patch_duration;
make_event<ground_aoe_event_t>(*sim, p(), ground_aoe_params_t()
.target(state->target)
.duration(flame_patch_duration)
.action(flame_patch)
.hasted(ground_aoe_params_t::SPELL_SPEED));
}
}
virtual void snapshot_state(action_state_t *s, dmg_e rt) override {
fire_mage_spell_t::snapshot_state(s, rt);
debug_cast<ignite_spell_state_t *>(s)->hot_streak = benefits_from_hot_streak();
}
virtual double composite_ignite_multiplier(const action_state_t *s) const override {
return debug_cast<const ignite_spell_state_t *>(s)->hot_streak ? 2.0 : 1.0;
}
virtual double action_multiplier() const override {
double am = fire_mage_spell_t::action_multiplier();
am *= 1.0 + p()->buffs.critical_massive->value();
return am;
}
virtual double composite_crit_chance() const override {
double c = fire_mage_spell_t::composite_crit_chance();
if (p()->buffs.ignition->up()) {
c += 1.0;
}
return c;
}
};
// Flurry Spell ===============================================================
struct flurry_bolt_t : public frost_mage_spell_t {
flurry_bolt_t(mage_t *p) :
frost_mage_spell_t("flurry_bolt", p, p->find_spell(228354)) {
background = true;
chills = true;
base_multiplier *= 1.0 + p->talents.lonely_winter->effectN(1).percent();
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
p()->state.flurry_bolt_count++;
if (p()->state.brain_freeze_active) {
td(s->target)->debuffs.winters_chill->trigger();
}
}
virtual double action_multiplier() const override {
double am = frost_mage_spell_t::action_multiplier();
if (p()->state.brain_freeze_active) {
am *= 1.0 + p()->buffs.brain_freeze->data().effectN(2).percent();
}
// In-game testing shows that 6 successive Flurry bolt impacts (with no cast
// in between to reset the counter) results in the following bonus from T20 2pc:
//
// 1st 2nd 3rd 4th 5th 6th
// +0% +25% +50% +25% +25% +25%
int adjusted_bolt_count = p()->state.flurry_bolt_count;
if (adjusted_bolt_count > 2)
adjusted_bolt_count = 1;
am *= 1.0 + adjusted_bolt_count
* p()->sets->set(MAGE_FROST, T21, B2)->effectN(1).percent();
return am;
}
};
struct flurry_t : public frost_mage_spell_t {
flurry_bolt_t *flurry_bolt;
flurry_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("flurry", p, p->find_specialization_spell("Flurry")),
flurry_bolt(new flurry_bolt_t(p)) {
parse_options(options_str);
may_miss = false;
may_crit = affected_by.shatter = false;
add_child(flurry_bolt);
}
virtual void init() override {
frost_mage_spell_t::init();
// Snapshot haste for bolt impact timing.
snapshot_flags |= STATE_HASTE;
}
virtual timespan_t execute_time() const override {
if (p()->buffs.brain_freeze->check()) {
return timespan_t::zero();
}
return frost_mage_spell_t::execute_time();
}
virtual void execute() override {
frost_mage_spell_t::execute();
bool brain_freeze = p()->buffs.brain_freeze->up();
p()->state.brain_freeze_active = brain_freeze;
p()->buffs.brain_freeze->expire();
p()->state.flurry_bolt_count = 0;
p()->buffs.zannesu_journey->trigger();
if (brain_freeze)
p()->procs.brain_freeze_flurry->occur();
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
trigger_shattered_fragments(s->target);
// TODO: Remove hardcoded values once it exists in spell data for bolt impact timing.
timespan_t pulse_time = timespan_t::from_seconds(0.4);
make_event<ground_aoe_event_t>(*sim, p(), ground_aoe_params_t()
.pulse_time(pulse_time * s->haste)
.target(s->target)
.n_pulses(data().effectN(1).base_value())
.action(flurry_bolt), true);
}
};
// Frost Bomb Spell ===========================================================
struct frost_bomb_explosion_t : public frost_mage_spell_t {
frost_bomb_explosion_t(mage_t *p) :
frost_mage_spell_t("frost_bomb_explosion", p, p->find_spell(113092)) {
background = true;
callbacks = false;
radius = data().effectN(2).radius_max();
aoe = -1;
parse_effect_data(data().effectN(1));
base_aoe_multiplier *= data().effectN(2).sp_coeff() / data().effectN(1).sp_coeff();
}
};
struct frost_bomb_t : public frost_mage_spell_t {
frost_bomb_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("frost_bomb", p, p->talents.frost_bomb) {
parse_options(options_str);
// Frost Bomb no longer has ticking damage.
dot_duration = timespan_t::zero();
may_crit = affected_by.shatter = false;
if (p->action.frost_bomb_explosion) {
add_child(p->action.frost_bomb_explosion);
}
}
virtual void execute() override {
frost_mage_spell_t::execute();
if (hit_any_target) {
if (p()->last_bomb_target != nullptr &&
p()->last_bomb_target != execute_state->target) {
td(p()->last_bomb_target)->debuffs.frost_bomb->expire();
}
p()->last_bomb_target = execute_state->target;
}
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
td(s->target)->debuffs.frost_bomb->trigger();
}
}
};
// Frostbolt Spell ==========================================================
struct frostbolt_t : public frost_mage_spell_t {
proc_t *proc_fof_water_jet;
frostbolt_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("frostbolt", p, p->find_specialization_spell("Frostbolt")) {
parse_options(options_str);
parse_effect_data(p->find_spell(228597)->effectN(1));
if (p->spec.icicles->ok()) {
add_child(p->icicle);
}
if (p->specialization() == MAGE_FROST && p->action.unstable_magic_explosion) {
add_child(p->action.unstable_magic_explosion);
}
base_multiplier *= 1.0 + p->talents.lonely_winter->effectN(1).percent();
chills = true;
calculate_on_impact = true;
track_shatter = true;
}
virtual bool init_finished() override {
proc_fof = p()->get_proc(std::string("Fingers of Frost from ") + data().name_cstr());
proc_fof_water_jet = p()->get_proc("Fingers of Frost from Water Jet");
return frost_mage_spell_t::init_finished();
}
virtual void execute() override {
frost_mage_spell_t::execute();
p()->buffs.icicles->trigger();
double fof_proc_chance = p()->spec.fingers_of_frost->effectN(1).percent();
fof_proc_chance *= 1.0 + p()->talents.frozen_touch->effectN(1).percent();
trigger_fof(fof_proc_chance);
double bf_proc_chance = p()->spec.brain_freeze->effectN(1).percent();
bf_proc_chance += p()->sets->set(MAGE_FROST, T19, B2)->effectN(1).percent();
trigger_brain_freeze(bf_proc_chance);
p()->buffs.t19_oh_buff->trigger();
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
if (!result_is_hit(s->result))
return;
trigger_icicle_gain(s);
if (td(s->target)->debuffs.water_jet->check()) {
trigger_fof(1.0, 1, proc_fof_water_jet);
}
trigger_unstable_magic(s);
trigger_shattered_fragments(s->target);
}
};
// Frost Nova Spell ========================================================
struct frost_nova_t : public mage_spell_t {
frost_nova_t(mage_t *p, const std::string &options_str) :
mage_spell_t("frost_nova", p, p->find_class_spell("Frost Nova")) {
parse_options(options_str);
aoe = -1;
affected_by.shatter = true;
cooldown->charges += p->talents.ice_ward->effectN(1).base_value();
}
virtual void impact(action_state_t *s) override {
mage_spell_t::impact(s);
p()->apply_crowd_control(s, MECHANIC_ROOT);
}
};
// Ice Time Super Frost Nova ================================================
struct ice_time_nova_t : public frost_mage_spell_t {
ice_time_nova_t(mage_t *p) :
frost_mage_spell_t("ice_time_nova", p, p->find_spell(235235)) {
background = true;
aoe = -1;
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
p()->apply_crowd_control(s, MECHANIC_ROOT);
}
};
// Frozen Orb Spell =========================================================
struct frozen_orb_bolt_t : public frost_mage_spell_t {
frozen_orb_bolt_t(mage_t *p) :
frost_mage_spell_t("frozen_orb_bolt", p, p->find_spell(84721)) {
aoe = -1;
background = true;
base_multiplier *= 1.0 + p->talents.lonely_winter->effectN(1).percent();
chills = true;
}
virtual bool init_finished() override {
proc_fof = p()->get_proc("Fingers of Frost from Frozen Orb Tick");
return frost_mage_spell_t::init_finished();
}
virtual void execute() override {
frost_mage_spell_t::execute();
if (hit_any_target) {
double fof_proc_chance = p()->spec.fingers_of_frost->effectN(1).percent();
fof_proc_chance += p()->sets->set(MAGE_FROST, T19, B4)->effectN(1).percent();
fof_proc_chance *= 1.0 + p()->talents.frozen_touch->effectN(1).percent();
trigger_fof(fof_proc_chance);
}
}
};
struct frozen_orb_t : public frost_mage_spell_t {
bool ice_time;
ice_time_nova_t *ice_time_nova;
frozen_orb_bolt_t *frozen_orb_bolt;
frozen_orb_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("frozen_orb", p,
p->find_specialization_spell("Frozen Orb")),
ice_time(false),
ice_time_nova(new ice_time_nova_t(p)),
frozen_orb_bolt(new frozen_orb_bolt_t(p)) {
parse_options(options_str);
add_child(frozen_orb_bolt);
add_child(ice_time_nova);
may_miss = false;
may_crit = affected_by.shatter = false;
}
virtual bool init_finished() override {
proc_fof = p()->get_proc("Fingers of Frost from Frozen Orb Initial Impact");
return frost_mage_spell_t::init_finished();
}
virtual timespan_t travel_time() const override {
timespan_t t = frost_mage_spell_t::travel_time();
// Frozen Orb activates after about 0.5 s, even in melee range.
t = std::max(t, timespan_t::from_seconds(0.5));
return t;
}
virtual void execute() override {
frost_mage_spell_t::execute();
if (p()->sets->has_set_bonus(MAGE_FROST, T20, B2)) {
p()->buffs.frozen_mass->trigger();
}
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
player_t *t = s->target;
double x = t->x_position;
double y = t->y_position;
timespan_t ground_aoe_duration = timespan_t::from_seconds(9.5);
p()->ground_aoe_expiration[name_str]
= sim->current_time() + ground_aoe_duration;
if (result_is_hit(s->result)) {
trigger_fof(1.0);
make_event<ground_aoe_event_t>(*sim, p(), ground_aoe_params_t()
.pulse_time(timespan_t::from_seconds(0.5))
.target(t)
.duration(ground_aoe_duration)
.action(frozen_orb_bolt)
.expiration_callback([this, t, x, y]() {
if (ice_time) {
ice_time_nova->set_target(target);
action_state_t *state = ice_time_nova->get_state();
ice_time_nova->snapshot_state(state, ice_time_nova->amount_type(state));
// Make sure Ice Time works correctly with distance targetting, e.g.
// when the target moves out of Frozen Orb.
state->target = t;
state->original_x = x;
state->original_y = y;
ice_time_nova->schedule_execute(state);
}
}), true);
}
}
};
// Glacial Spike Spell ==============================================================
struct glacial_spike_t : public frost_mage_spell_t {
double icicle_damage;
// So that we don't need custom action_state_t to store it.
mutable double icicle_damage_ratio;
glacial_spike_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("glacial_spike", p, p->talents.glacial_spike),
icicle_damage(0.0) {
parse_options(options_str);
parse_effect_data(p->find_spell(228600)->effectN(1));
if (p->talents.splitting_ice->ok()) {
aoe = 1 + p->talents.splitting_ice->effectN(1).base_value();
base_aoe_multiplier *= p->talents.splitting_ice
->effectN(2).percent();
}
calculate_on_impact = true;
track_shatter = true;
}
virtual bool ready() override {
if (p()->buffs.icicles->check() < p()->buffs.icicles->max_stack()) {
return false;
}
return frost_mage_spell_t::ready();
}
virtual void record_data(action_state_t *data) override {
frost_mage_spell_t::record_data(data);
if (icicle_damage_ratio == 0.0)
return;
double amount = data->result_total;
double icicles = amount * icicle_damage_ratio;
double base = amount - icicles;
p()->sample_data.glacial_spike_base->add(base);
p()->sample_data.glacial_spike_icicles->add(icicles);
}
virtual double calculate_impact_direct_amount(action_state_t *s) const override {
double base_amount = frost_mage_spell_t::calculate_impact_direct_amount(s);
double icicle_amount = icicle_damage;
icicle_damage_ratio = 0.0;
// Icicle portion is only affected by target-based damage multipliers.
icicle_amount *= s->target_da_multiplier;
if (s->chain_target > 0)
icicle_amount *= base_aoe_multiplier;
double amount = base_amount + icicle_amount;
s->result_raw = amount;
if (result_is_miss(s->result)) {
s->result_total = 0.0;
return 0.0;
} else {
s->result_total = amount;
if (amount > 0)
icicle_damage_ratio = icicle_amount / amount;
return amount;
}
}
virtual void execute() override {
// Ideally, this would be passed to impact() in action_state_t, but since
// it's pretty much impossible to execute another Glacial Spike before
// the first one impacts, this should be fine.
icicle_damage = 0.0;
int icicle_count = as<int>(p()->icicles.size());
for (int i = 0; i < icicle_count; i++) {
icicle_damage += p()->get_icicle();
}
if (sim->debug) {
sim->out_debug.printf("Add %u icicles to glacial_spike for %f damage",
icicle_count, icicle_damage);
}
frost_mage_spell_t::execute();
p()->buffs.icicles->expire();
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
p()->apply_crowd_control(s, MECHANIC_ROOT);
}
};
// Ice Floes Spell ============================================================
struct ice_floes_t : public mage_spell_t {
ice_floes_t(mage_t *p, const std::string &options_str) :
mage_spell_t("ice_floes", p, p->talents.ice_floes) {
parse_options(options_str);
may_miss = may_crit = harmful = false;
trigger_gcd = timespan_t::zero();
internal_cooldown->duration = data().internal_cooldown();
}
virtual void execute() override {
mage_spell_t::execute();
p()->buffs.ice_floes->trigger();
}
};
// Ice Lance Spell ==========================================================
struct ice_lance_state_t : public mage_spell_state_t {
bool fingers_of_frost;
ice_lance_state_t(action_t *action, player_t *target) :
mage_spell_state_t(action, target),
fingers_of_frost(false) {}
virtual void initialize() override {
mage_spell_state_t::initialize();
fingers_of_frost = false;
}
virtual std::ostringstream &debug_str(std::ostringstream &s) override {
mage_spell_state_t::debug_str(s) << " fingers_of_frost=" << fingers_of_frost;
return s;
}
virtual void copy_state(const action_state_t *s) override {
mage_spell_state_t::copy_state(s);
auto ils = debug_cast<const ice_lance_state_t *>(s);
fingers_of_frost = ils->fingers_of_frost;
}
};
struct ice_lance_t : public frost_mage_spell_t {
proc_source_t *extension_source;
ice_lance_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("ice_lance", p, p->find_specialization_spell("Ice Lance")),
extension_source(nullptr) {
parse_options(options_str);
parse_effect_data(p->find_spell(228598)->effectN(1));
base_multiplier *= 1.0 + p->talents.lonely_winter->effectN(1).percent();
// TODO: Cleave distance for SI seems to be 8 + hitbox size.
if (p->talents.splitting_ice->ok()) {
base_multiplier *= 1.0 + p->talents.splitting_ice
->effectN(3).percent();
aoe = 1 + p->talents.splitting_ice->effectN(1).base_value();
base_aoe_multiplier *= p->talents.splitting_ice
->effectN(2).percent();
}
calculate_on_impact = true;
track_shatter = true;
}
virtual bool init_finished() override {
if (p()->talents.thermal_void->ok() && sim->report_details != 0) {
extension_source = p()->get_proc_source("Shatter/Thermal Void extension", FROZEN_MAX);
}
return frost_mage_spell_t::init_finished();
}
virtual action_state_t *new_state() override { return new ice_lance_state_t(this, target); }
virtual unsigned frozen(const action_state_t *s) const override {
unsigned source = frost_mage_spell_t::frozen(s);
// In game, FoF Ice Lances are implemented using a global flag which determines
// whether to treat the targets as frozen or not. On IL execute, FoF is checked
// and the flag set accordingly.
//
// This works fine under normal circumstances. However, once GCD drops below IL's
// travel time, it's possible to:
//
// a) cast FoF IL, cast non-FoF IL before the first one impacts
// b) cast non-FoF IL, cast FoF IL before the first one impacts
//
// In the a) case, neither Ice Lance gets the extra damage/Shatter bonus, in the
// b) case, both Ice Lances do.
if (p()->bugs) {
if (p()->state.fingers_of_frost_active)
source |= FF_FINGERS_OF_FROST;
} else {
if (debug_cast<const ice_lance_state_t *>(s)->fingers_of_frost)
source |= FF_FINGERS_OF_FROST;
}
return source;
}
virtual void execute() override {
p()->state.fingers_of_frost_active = p()->buffs.fingers_of_frost->up();
frost_mage_spell_t::execute();
p()->buffs.magtheridons_might->trigger();
p()->buffs.fingers_of_frost->decrement();
// Begin casting all Icicles at the target, beginning 0.25 seconds after the
// Ice Lance cast with remaining Icicles launching at intervals of 0.4
// seconds, the latter adjusted by haste. Casting continues until all
// Icicles are gone, including new ones that accumulate while they're being
// fired. If target dies, Icicles stop.
if (!p()->talents.glacial_spike->ok()) {
p()->trigger_icicle(execute_state, true, target);
}
}
virtual void snapshot_state(action_state_t *s, dmg_e rt) override {
frost_mage_spell_t::snapshot_state(s, rt);
debug_cast<ice_lance_state_t *>(s)->fingers_of_frost = p()->buffs.fingers_of_frost->check() != 0;
}
virtual timespan_t travel_time() const override {
timespan_t t = frost_mage_spell_t::travel_time();
if (p()->allow_shimmer_lance && p()->buffs.shimmer->check()) {
double shimmer_distance = p()->talents.shimmer->effectN(1).radius_max();
t = std::max(t - timespan_t::from_seconds(shimmer_distance / travel_speed), timespan_t::zero());
}
return t;
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
if (!result_is_hit(s->result))
return;
bool primary = s->chain_target == 0;
unsigned frozen = debug_cast<mage_spell_state_t *>(s)->frozen;
if (primary && frozen) {
if (p()->talents.thermal_void->ok() && p()->buffs.icy_veins->check()) {
timespan_t tv_extension = p()->talents.thermal_void
->effectN(1).time_value() * 1000;
p()->buffs.icy_veins->extend_duration(p(), tv_extension);
if (extension_source) {
record_shatter_source(s, extension_source);
}
}
if (frozen & FF_FINGERS_OF_FROST
&& frozen & ~FF_FINGERS_OF_FROST) {
p()->procs.fingers_of_frost_wasted->occur();
}
}
if (frozen) {
if (td(s->target)->debuffs.frost_bomb->check()) {
assert(p()->action.frost_bomb_explosion);
p()->action.frost_bomb_explosion->set_target(s->target);
p()->action.frost_bomb_explosion->execute();
}
}
if (primary) {
if (p()->buffs.magtheridons_might->default_chance != 0.0)
p()->benefits.magtheridons_might->update();
}
p()->buffs.arctic_blast->expire(timespan_t::from_seconds(0.5));
}
virtual double action_multiplier() const override {
double am = frost_mage_spell_t::action_multiplier();
am *= 1.0 + p()->buffs.magtheridons_might->check_stack_value();
am *= 1.0 + p()->buffs.arctic_blast->check_value();
return am;
}
virtual double composite_da_multiplier(const action_state_t *s) const override {
double m = frost_mage_spell_t::composite_da_multiplier(s);
if (debug_cast<const mage_spell_state_t *>(s)->frozen) {
m *= 3.0;
}
return m;
}
};
// Ice Nova Spell ==========================================================
struct ice_nova_t : public frost_mage_spell_t {
ice_nova_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("ice_nova", p, p->talents.ice_nova) {
parse_options(options_str);
aoe = -1;
double in_mult = 1.0 + p->talents.ice_nova->effectN(1).percent();
base_multiplier *= in_mult;
base_aoe_multiplier = 1.0 / in_mult;
}
virtual void impact(action_state_t *s) override {
frost_mage_spell_t::impact(s);
p()->apply_crowd_control(s, MECHANIC_ROOT);
}
};
// Icy Veins Spell ==========================================================
struct icy_veins_t : public frost_mage_spell_t {
icy_veins_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("icy_veins", p, p->find_specialization_spell("Icy Veins")) {
parse_options(options_str);
harmful = false;
}
virtual bool init_finished() override {
if (p()->buffs.lady_vashjs_grasp->default_chance != 0.0) {
debug_cast<buffs::lady_vashjs_grasp_t *>(p()->buffs.lady_vashjs_grasp)
->proc_fof = p()->get_proc("Fingers of Frost from Lady Vashj's Grasp");
}
return frost_mage_spell_t::init_finished();
}
virtual void execute() override {
frost_mage_spell_t::execute();
p()->buffs.icy_veins->trigger();
// Refreshing infinite ticking buff doesn't quite work, remove
// LVG manually and then trigger it again.
p()->buffs.lady_vashjs_grasp->expire();
p()->buffs.lady_vashjs_grasp->trigger();
}
};
// Fire Blast Spell ======================================================
struct fire_blast_t : public fire_mage_spell_t {
fire_blast_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("fire_blast", p,
p->find_specialization_spell("Fire Blast")) {
parse_options(options_str);
trigger_gcd = timespan_t::zero();
cooldown->charges = data().charges();
cooldown->charges += p->spec.fire_blast_3->effectN(1).base_value();
cooldown->charges += p->talents.flame_on->effectN(1).base_value();
cooldown->duration = data().charge_cooldown();
cooldown->duration -= 1000 * p->talents.flame_on->effectN(3).time_value();
cooldown->hasted = true;
triggers_hot_streak = true;
triggers_ignite = true;
base_crit += p->spec.fire_blast_2->effectN(1).percent();
}
virtual void execute() override {
fire_mage_spell_t::execute();
// update_ready() assumes the ICD is affected by haste
internal_cooldown->start(data().cooldown());
}
virtual void impact(action_state_t *s) override {
fire_mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
if (s->result == RESULT_CRIT && p()->talents.kindling->ok()) {
p()->cooldowns.combustion
->adjust(-1000 * p()->talents.kindling
->effectN(1).time_value());
}
}
}
};
// Living Bomb Spell ========================================================
struct living_bomb_explosion_t;
struct living_bomb_t;
struct living_bomb_explosion_t : public fire_mage_spell_t {
living_bomb_t *child_lb;
living_bomb_explosion_t(mage_t *p, bool create_dot);
virtual resource_e current_resource() const override;
void impact(action_state_t *s) override;
};
struct living_bomb_t : public fire_mage_spell_t {
living_bomb_explosion_t *explosion;
living_bomb_t(mage_t *p, const std::string &options_str, bool casted);
virtual timespan_t composite_dot_duration(const action_state_t *s) const override;
virtual void last_tick(dot_t *d) override;
virtual void init() override;
};
living_bomb_explosion_t::living_bomb_explosion_t(mage_t *p, bool create_dot) :
fire_mage_spell_t("living_bomb_explosion", p, p->find_spell(44461)),
child_lb(create_dot ? new living_bomb_t(p, "", false) : nullptr) {
aoe = -1;
radius = 10;
background = true;
}
resource_e living_bomb_explosion_t::current_resource() const { return RESOURCE_NONE; }
void living_bomb_explosion_t::impact(action_state_t *s) {
fire_mage_spell_t::impact(s);
if (child_lb && s->chain_target > 0) {
if (sim->debug) {
sim->out_debug.printf(
"%s %s on %s applies %s on %s",
p()->name(), name(), s->action->target->name(),
child_lb->name(), s->target->name());
}
child_lb->set_target(s->target);
child_lb->execute();
}
}
living_bomb_t::living_bomb_t(mage_t *p, const std::string &options_str,
bool casted = true) :
fire_mage_spell_t("living_bomb", p, p->talents.living_bomb),
explosion(new living_bomb_explosion_t(p, casted)) {
parse_options(options_str);
// Why in Azeroth would they put DOT spell data in a separate spell??
const spell_data_t *dot_data = p->find_spell(217694);
dot_duration = dot_data->duration();
for (size_t i = 1; i <= dot_data->effect_count(); i++) {
parse_effect_data(dot_data->effectN(i));
}
cooldown->hasted = true;
hasted_ticks = true;
add_child(explosion);
if (!casted) {
background = true;
base_costs[RESOURCE_MANA] = 0;
}
}
timespan_t living_bomb_t::composite_dot_duration(const action_state_t *s) const {
timespan_t duration = fire_mage_spell_t::composite_dot_duration(s);
return duration * (tick_time(s) / base_tick_time);
}
void living_bomb_t::last_tick(dot_t *d) {
fire_mage_spell_t::last_tick(d);
explosion->set_target(d->target);
explosion->execute();
}
void living_bomb_t::init() {
fire_mage_spell_t::init();
update_flags &= ~STATE_HASTE;
}
// Meteor Spell ===============================================================
// TODO: Have they fixed Meteor's implementation in Legion?
// Implementation details from Celestalon:
// http://blue.mmo-champion.com/topic/318876-warlords-of-draenor-theorycraft-discussion/#post301
// Meteor is split over a number of spell IDs, some of which don't seem to be
// used for anything useful:
// - Meteor (id=153561) is the talent spell, the driver
// - Meteor (id=153564) is the initial impact damage
// - Meteor Burn (id=155158) is the ground effect tick damage
// - Meteor Burn (id=175396) provides the tooltip's burn duration (8 seconds),
// but doesn't match in game where we only see 7 ticks over 7 seconds.
// - Meteor (id=177345) contains the time between cast and impact
// None of these specify the 1 second falling duration given by Celestalon, so
// we're forced to hardcode it.
struct meteor_burn_t : public fire_mage_spell_t {
meteor_burn_t(mage_t *p, int targets, bool legendary) :
fire_mage_spell_t(legendary ? "legendary_meteor_burn" : "meteor_burn",
p, p->find_spell(155158)) {
background = true;
aoe = targets;
spell_power_mod.direct = data().effectN(1).sp_coeff();
spell_power_mod.tick = 0;
dot_duration = timespan_t::zero();
radius = p->find_spell(153564)->effectN(1).radius_max();
ground_aoe = true;
}
// Override damage type because Meteor Burn is considered a DOT
virtual dmg_e amount_type(const action_state_t * /* state */,
bool /* periodic */) const override {
return DMG_OVER_TIME;
}
};
struct meteor_impact_t : public fire_mage_spell_t {
meteor_burn_t *meteor_burn;
timespan_t meteor_burn_duration;
timespan_t meteor_burn_pulse_time;
meteor_impact_t(mage_t *p, meteor_burn_t *meteor_burn, int targets, bool legendary) :
fire_mage_spell_t(legendary ? "legendary_meteor_impact" : "meteor_impact",
p, p->find_spell(153564)),
meteor_burn(meteor_burn),
meteor_burn_duration(p->find_spell(175396)->duration()) {
background = true;
aoe = targets;
split_aoe_damage = true;
triggers_ignite = true;
meteor_burn_pulse_time = meteor_burn->data().effectN(1).period();
// It seems that the 8th tick happens only very rarely in game.
// As of build 25881, 2018-01-22.
if (p->bugs) {
meteor_burn_duration -= meteor_burn_pulse_time;
}
}
virtual timespan_t travel_time() const override {
return timespan_t::from_seconds(1.0);
}
virtual void impact(action_state_t *s) override {
fire_mage_spell_t::impact(s);
p()->ground_aoe_expiration[meteor_burn->name_str]
= sim->current_time() + meteor_burn_duration;
make_event<ground_aoe_event_t>(*sim, p(), ground_aoe_params_t()
.pulse_time(meteor_burn_pulse_time)
.target(s->target)
.duration(meteor_burn_duration)
.action(meteor_burn));
}
};
struct meteor_t : public fire_mage_spell_t {
int targets;
meteor_impact_t *meteor_impact;
timespan_t meteor_delay;
meteor_t(mage_t *p, const std::string &options_str, bool legendary = false) :
fire_mage_spell_t(legendary ? "legendary_meteor" : "meteor",
p, p->find_talent_spell("Meteor", SPEC_NONE, false, !legendary)),
targets(-1),
meteor_delay(p->find_spell(177345)->duration()) {
add_option(opt_int("targets", targets));
parse_options(options_str);
callbacks = false;
meteor_burn_t *meteor_burn = new meteor_burn_t(p, targets, legendary);
meteor_impact = new meteor_impact_t(p, meteor_burn, targets, legendary);
add_child(meteor_impact);
add_child(meteor_burn);
if (legendary) {
background = true;
base_costs[RESOURCE_MANA] = 0;
}
}
virtual timespan_t travel_time() const override {
timespan_t impact_time = meteor_delay * p()->composite_spell_haste();
timespan_t meteor_spawn = impact_time - meteor_impact->travel_time();
meteor_spawn = std::max(timespan_t::zero(), meteor_spawn);
return meteor_spawn;
}
virtual void impact(action_state_t *s) override {
fire_mage_spell_t::impact(s);
meteor_impact->set_target(s->target);
meteor_impact->execute();
}
};
// Mirror Image Spell =========================================================
struct mirror_image_t : public mage_spell_t {
mirror_image_t(mage_t *p, const std::string &options_str) :
mage_spell_t("mirror_image", p, p->find_talent_spell("Mirror Image")) {
parse_options(options_str);
dot_duration = timespan_t::zero();
harmful = false;
}
virtual bool init_finished() override {
std::vector<pet_t *> images = p()->pets.mirror_images;
for (pet_t *image : images) {
if (!image) {
continue;
}
stats->add_child(image->get_stats("arcane_blast"));
stats->add_child(image->get_stats("fireball"));
stats->add_child(image->get_stats("frostbolt"));
}
return mage_spell_t::init_finished();
}
virtual void execute() override {
mage_spell_t::execute();
if (p()->pets.mirror_images[0]) {
for (int i = 0; i < data().effectN(2).base_value(); i++) {
p()->pets.mirror_images[i]->summon(data().duration());
}
}
}
};
// Nether Tempest AoE Spell ===================================================
struct nether_tempest_aoe_t : public arcane_mage_spell_t {
nether_tempest_aoe_t(mage_t *p) :
arcane_mage_spell_t("nether_tempest_aoe", p, p->find_spell(114954)) {
aoe = -1;
background = true;
}
virtual timespan_t travel_time() const override {
return timespan_t::from_seconds(1.3);
}
};
// Nether Tempest Spell =======================================================
struct nether_tempest_t : public arcane_mage_spell_t {
nether_tempest_aoe_t *nether_tempest_aoe;
nether_tempest_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("nether_tempest", p, p->talents.nether_tempest),
nether_tempest_aoe(new nether_tempest_aoe_t(p)) {
parse_options(options_str);
add_child(nether_tempest_aoe);
}
virtual void execute() override {
p()->benefits.arcane_charge.nether_tempest->update();
arcane_mage_spell_t::execute();
if (hit_any_target) {
if (p()->last_bomb_target != nullptr &&
p()->last_bomb_target != execute_state->target) {
td(p()->last_bomb_target)->dots.nether_tempest->cancel();
}
p()->last_bomb_target = execute_state->target;
}
}
virtual void tick(dot_t *d) override {
arcane_mage_spell_t::tick(d);
action_state_t *aoe_state = nether_tempest_aoe->get_state(d->state);
aoe_state->target = d->target;
nether_tempest_aoe->schedule_execute(aoe_state);
}
virtual double composite_persistent_multiplier(const action_state_t *state) const override {
double m = arcane_mage_spell_t::composite_persistent_multiplier(state);
m *= arcane_charge_damage_bonus();
return m;
}
};
// Phoenix Flames Spell ======================================================
struct phoenix_flames_splash_t : public fire_mage_spell_t {
phoenix_flames_splash_t(mage_t *p) :
fire_mage_spell_t("phoenix_flames_splash", p, p->find_spell(257542)) {
aoe = -1;
background = true;
triggers_ignite = true;
// Phoenix Flames always crits
base_crit = 1.0;
}
virtual void impact(action_state_t *s) override {
// PF cleave does not impact main target
if (s->chain_target == 0) {
return;
}
fire_mage_spell_t::impact(s);
}
virtual double action_multiplier() const override {
double am = fire_mage_spell_t::action_multiplier();
// Phoenix Flames splash deal 25% less damage compared to the
// spell data/tooltip values. As of build 25881, 2018-01-221.
if (p()->bugs)
am *= 0.75;
return am;
}
};
struct phoenix_flames_t : public fire_mage_spell_t {
phoenix_flames_splash_t *phoenix_flames_splash;
bool pyrotex_ignition_cloth;
timespan_t pyrotex_ignition_cloth_reduction;
phoenix_flames_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("phoenix_flames", p, p->talents.phoenix_flames),
phoenix_flames_splash(new phoenix_flames_splash_t(p)),
pyrotex_ignition_cloth(false),
pyrotex_ignition_cloth_reduction(timespan_t::zero()) {
parse_options(options_str);
// Phoenix Flames always crits
base_crit = 1.0;
triggers_hot_streak = true;
triggers_ignite = true;
add_child(phoenix_flames_splash);
}
virtual void execute() override {
fire_mage_spell_t::execute();
if (pyrotex_ignition_cloth) {
p()->cooldowns.combustion
->adjust(-1000 * pyrotex_ignition_cloth_reduction);
}
}
virtual void impact(action_state_t *s) override {
fire_mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
phoenix_flames_splash->set_target(s->target);
phoenix_flames_splash->execute();
}
}
virtual timespan_t travel_time() const override {
timespan_t t = fire_mage_spell_t::travel_time();
return std::min(t, timespan_t::from_seconds(0.75));
}
};
// Pyroblast Spell ============================================================
struct pyroblast_t : public fire_mage_spell_t {
pyroblast_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("pyroblast", p, p->find_specialization_spell("Pyroblast")) {
parse_options(options_str);
triggers_ignite = true;
triggers_hot_streak = true;
}
virtual double action_multiplier() const override {
double am = fire_mage_spell_t::action_multiplier();
if (p()->buffs.kaelthas_ultimate_ability->check() && !benefits_from_hot_streak()) {
am *= 1.0 + p()->buffs.kaelthas_ultimate_ability->data().effectN(1).percent();
}
am *= 1.0 + p()->buffs.critical_massive->value();
return am;
}
virtual action_state_t *new_state() override {
return new ignite_spell_state_t(this, target);
}
virtual timespan_t execute_time() const override {
if (p()->buffs.hot_streak->check()) {
return timespan_t::zero();
}
return fire_mage_spell_t::execute_time();
}
virtual void execute() override {
bool hot_streak = benefits_from_hot_streak(true);
fire_mage_spell_t::execute();
if (p()->buffs.kaelthas_ultimate_ability->check() && !hot_streak) {
p()->buffs.kaelthas_ultimate_ability->expire();
}
if (hot_streak) {
p()->buffs.kaelthas_ultimate_ability->trigger();
}
// Ignition/Critical Massive buffs are removed shortly after Flamestrike/Pyroblast cast.
// In a situation where you're hardcasting FS/PB followed by a Hot Streak FS/FB, both
// spells actually benefit. As of build 25881, 2018-01-22.
p()->buffs.ignition->expire(p()->bugs ? timespan_t::from_millis(15) : timespan_t::zero());
p()->buffs.critical_massive->expire(p()->bugs ? timespan_t::from_millis(15) : timespan_t::zero());
if (hot_streak) {
p()->buffs.hot_streak->expire();
if (p()->talents.pyromaniac->ok()
&& rng().roll(p()->talents.pyromaniac->effectN(1).percent())) {
p()->procs.hot_streak->occur();
p()->procs.hot_streak_pyromaniac->occur();
p()->buffs.hot_streak->trigger();
}
}
}
virtual void snapshot_state(action_state_t *s, dmg_e rt) override {
fire_mage_spell_t::snapshot_state(s, rt);
debug_cast<ignite_spell_state_t *>(s)->hot_streak = benefits_from_hot_streak();
}
virtual timespan_t travel_time() const override {
timespan_t t = fire_mage_spell_t::travel_time();
return std::min(t, timespan_t::from_seconds(0.75));
}
virtual void impact(action_state_t *s) override {
fire_mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
if (s->result == RESULT_CRIT && p()->talents.kindling->ok()) {
p()->cooldowns.combustion
->adjust(-1000 * p()->talents.kindling
->effectN(1).time_value());
}
if (p()->sets->has_set_bonus(MAGE_FIRE, T20, B4) && s->result == RESULT_CRIT) {
p()->buffs.critical_massive->trigger();
}
trigger_infernal_core(s->target);
}
}
virtual double composite_crit_chance() const override {
double c = fire_mage_spell_t::composite_crit_chance();
if (p()->buffs.ignition->up()) {
c += 1.0;
}
return c;
}
virtual double composite_ignite_multiplier(const action_state_t *s) const override {
return debug_cast<const ignite_spell_state_t *>(s)->hot_streak ? 2.0 : 1.0;
}
virtual double composite_target_crit_chance(player_t *target) const override {
double c = fire_mage_spell_t::composite_target_crit_chance(target);
if (firestarter_active(target)) {
c = 1.0;
}
return c;
}
};
// Ray of Frost Spell ===============================================================
struct ray_of_frost_t : public frost_mage_spell_t {
ray_of_frost_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("ray_of_frost", p, p->talents.ray_of_frost) {
parse_options(options_str);
channeled = true;
hasted_ticks = true;
}
virtual void init() override {
frost_mage_spell_t::init();
update_flags |= STATE_HASTE; // Not snapshotted for this spell.
}
virtual void execute() override {
frost_mage_spell_t::execute();
p()->cooldowns.ray_of_frost->reset(false);
// Technically, the "castable duration" buff should be ID:208166
// To keep things simple, we just apply a 0 stack of the damage buff 208141
if (!p()->buffs.ray_of_frost->check()) {
p()->buffs.ray_of_frost->trigger(0);
}
}
virtual timespan_t composite_dot_duration(const action_state_t * /* s */ ) const override {
return data().duration();
}
virtual void tick(dot_t *d) override {
p()->benefits.ray_of_frost->update();
frost_mage_spell_t::tick(d);
p()->buffs.ray_of_frost->bump(1, p()->buffs.ray_of_frost->default_value);
}
virtual double action_multiplier() const override {
double am = frost_mage_spell_t::action_multiplier();
am *= 1.0 + p()->buffs.ray_of_frost->check_stack_value();
return am;
}
};
// Rune of Power Spell ==============================================================
struct rune_of_power_t : public mage_spell_t {
rune_of_power_t(mage_t *p, const std::string &options_str) :
mage_spell_t("rune_of_power", p, p->talents.rune_of_power) {
parse_options(options_str);
harmful = false;
}
virtual void execute() override {
mage_spell_t::execute();
// Assume they're always in it
p()->distance_from_rune = 0;
p()->buffs.rune_of_power->trigger();
}
};
// Scorch Spell =============================================================
struct scorch_t : public fire_mage_spell_t {
bool koralons_burning_touch;
double koralons_burning_touch_threshold;
double koralons_burning_touch_multiplier;
scorch_t(mage_t *p, const std::string &options_str) :
fire_mage_spell_t("scorch", p, p->find_specialization_spell("Scorch")),
koralons_burning_touch(false),
koralons_burning_touch_threshold(0.0),
koralons_burning_touch_multiplier(0.0) {
parse_options(options_str);
triggers_hot_streak = true;
triggers_ignite = true;
}
virtual double action_multiplier() const override {
double am = fire_mage_spell_t::action_multiplier();
if (koralons_burning_touch && (target->health_percentage() <= koralons_burning_touch_threshold)) {
am *= 1.0 + koralons_burning_touch_multiplier;
}
return am;
}
virtual double composite_crit_chance() const override {
double c = fire_mage_spell_t::composite_crit_chance();
if (koralons_burning_touch && (target->health_percentage() <= koralons_burning_touch_threshold)) {
c = 1.0;
}
return c;
}
virtual void impact(action_state_t *s) override {
fire_mage_spell_t::impact(s);
if (p()->talents.frenetic_speed->ok()) {
p()->buffs.frenetic_speed->trigger();
}
}
virtual bool usable_moving() const override { return true; }
};
// Shimmer Spell ============================================================
struct shimmer_t : public mage_spell_t {
shimmer_t(mage_t *p, const std::string &options_str) :
mage_spell_t("shimmer", p, p->talents.shimmer) {
parse_options(options_str);
harmful = false;
ignore_false_positive = true;
base_teleport_distance = data().effectN(1).radius_max();
movement_directionality = MOVEMENT_OMNI;
}
virtual void execute() override {
mage_spell_t::execute();
p()->buffs.shimmer->trigger();
}
};
// Slow Spell ===============================================================
struct slow_t : public arcane_mage_spell_t {
slow_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("slow", p, p->find_specialization_spell("Slow")) {
parse_options(options_str);
ignore_false_positive = true;
triggers_erosion = false;
}
virtual void impact(action_state_t *s) override {
arcane_mage_spell_t::impact(s);
if (result_is_hit(s->result)) {
td(s->target)->debuffs.slow->trigger();
}
}
};
// Supernova Spell ==========================================================
struct supernova_t : public arcane_mage_spell_t {
supernova_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("supernova", p, p->talents.supernova) {
parse_options(options_str);
aoe = -1;
double sn_mult = 1.0 + p->talents.supernova->effectN(1).percent();
base_multiplier *= sn_mult;
base_aoe_multiplier = 1.0 / sn_mult;
}
};
// Summon Water Elemental Spell ====================================================
struct summon_water_elemental_t : public frost_mage_spell_t {
summon_water_elemental_t(mage_t *p, const std::string &options_str) :
frost_mage_spell_t("water_elemental", p, p->find_specialization_spell("Summon Water Elemental")) {
parse_options(options_str);
harmful = false;
ignore_false_positive = true;
// TODO: Why is this not on GCD?
trigger_gcd = timespan_t::zero();
track_cd_waste = false;
}
virtual void execute() override {
frost_mage_spell_t::execute();
p()->pets.water_elemental->summon();
}
virtual bool ready() override {
if (!p()->pets.water_elemental)
return false;
if (!p()->pets.water_elemental->is_sleeping())
return false;
if (p()->talents.lonely_winter->ok())
return false;
return frost_mage_spell_t::ready();
}
};
// Summon Arcane Familiar Spell ===============================================
struct arcane_assault_t : public arcane_mage_spell_t {
arcane_assault_t(mage_t *p)
: arcane_mage_spell_t("arcane_assault", p, p->find_spell(225119)) {
background = true;
}
};
struct summon_arcane_familiar_t : public arcane_mage_spell_t {
summon_arcane_familiar_t(mage_t *p, const std::string &options_str) :
arcane_mage_spell_t("summon_arcane_familiar", p,
p->talents.arcane_familiar) {
parse_options(options_str);
harmful = false;
ignore_false_positive = true;
trigger_gcd = timespan_t::zero();
track_cd_waste = false;
}
virtual void execute() override {
arcane_mage_spell_t::execute();
p()->buffs.arcane_familiar->trigger();
}
virtual bool ready() override {
if (p()->buffs.arcane_familiar->check()) {
return false;
}
return arcane_mage_spell_t::ready();
}
};
// Time Warp Spell ============================================================
struct time_warp_t : public mage_spell_t {
time_warp_t(mage_t *p, const std::string &options_str) :
mage_spell_t("time_warp", p, p->find_class_spell("Time Warp")) {
parse_options(options_str);
harmful = false;
}
virtual void execute() override {
mage_spell_t::execute();
for (size_t i = 0; i < sim->player_non_sleeping_list.size(); ++i) {
player_t *p = sim->player_non_sleeping_list[i];
if (p->buffs.exhaustion->check() || p->is_pet())
continue;
p->buffs.bloodlust->trigger();
p->buffs.exhaustion->trigger();
}
// If Shard of the Exodar is equipped, trigger bloodlust regardless.
if (p()->player_t::buffs.bloodlust->default_chance == 0.0) {
p()->player_t::buffs.bloodlust->trigger(1, buff_t::DEFAULT_VALUE(), 1.0);
}
}
virtual bool ready() override {
// If we have shard of the exodar, we're controlling our own destiny. Overrides don't
// apply to us.
bool shard = p()->player_t::buffs.bloodlust->default_chance == 0.0;
if (!shard && sim->overrides.bloodlust)
return false;
if (!shard && player->buffs.exhaustion->check())
return false;
return mage_spell_t::ready();
}
};
// ============================================================================
// Mage Custom Actions
// ============================================================================
// Arcane Mage "Burn" State Switch Action =====================================
struct start_burn_phase_t : public action_t {
start_burn_phase_t(mage_t *p, const std::string &options_str) :
action_t(ACTION_USE, "start_burn_phase", p) {
parse_options(options_str);
trigger_gcd = timespan_t::zero();
harmful = false;
ignore_false_positive = true;
action_skill = 1;
}
virtual void execute() override {
mage_t *p = debug_cast<mage_t *>(player);
bool success = p->burn_phase.enable(sim->current_time());
if (!success) {
sim->errorf("%s start_burn_phase infinite loop detected (no time passing between executes) at '%s'",
p->name(), signature_str.c_str());
sim->cancel_iteration();
sim->cancel();
return;
}
p->sample_data.burn_initial_mana->add(p->resources.current[RESOURCE_MANA] / p->resources.max[RESOURCE_MANA] * 100);
p->uptime.burn_phase->update(true, sim->current_time());
p->uptime.conserve_phase->update(false, sim->current_time());
}
virtual bool ready() override {
mage_t *p = debug_cast<mage_t *>(player);
if (p->burn_phase.on()) {
return false;
}
return action_t::ready();
}
};
struct stop_burn_phase_t : public action_t {
stop_burn_phase_t(mage_t *p, const std::string &options_str) :
action_t(ACTION_USE, "stop_burn_phase", p) {
parse_options(options_str);
trigger_gcd = timespan_t::zero();
harmful = false;
ignore_false_positive = true;
action_skill = 1;
}
virtual void execute() override {
mage_t *p = debug_cast<mage_t *>(player);
p->sample_data.burn_duration_history->add(p->burn_phase.duration(sim->current_time()).total_seconds());
bool success = p->burn_phase.disable(sim->current_time());
if (!success) {
sim->errorf("%s stop_burn_phase infinite loop detected (no time passing between executes) at '%s'",
p->name(), signature_str.c_str());
sim->cancel_iteration();
sim->cancel();
return;
}
p->uptime.burn_phase->update(false, sim->current_time());
p->uptime.conserve_phase->update(true, sim->current_time());
}
virtual bool ready() override {
mage_t *p = debug_cast<mage_t *>(player);
if (!p->burn_phase.on()) {
return false;
}
return action_t::ready();
}
};
// Unstable Magic =============================================================
struct unstable_magic_explosion_t : public mage_spell_t {
unstable_magic_explosion_t(mage_t *p) :
mage_spell_t("unstable_magic_explosion", p, p->talents.unstable_magic) {
may_miss = may_crit = false;
callbacks = false;
aoe = -1;
background = true;
base_dd_min = base_dd_max = 1.0;
switch (p->specialization()) {
case MAGE_ARCANE:school = SCHOOL_ARCANE;
break;
case MAGE_FIRE:school = SCHOOL_FIRE;
break;
case MAGE_FROST:school = SCHOOL_FROST;
break;
default:
// This shouldn't happen
break;
}
}
virtual void init() override {
mage_spell_t::init();
// disable the snapshot_flags for all multipliers
snapshot_flags &= STATE_NO_MULTIPLIER;
snapshot_flags |= STATE_TGT_MUL_DA;
}
virtual void execute() override {
double mult = data().effectN(4).percent();
base_dd_min *= mult;
base_dd_max *= mult;
mage_spell_t::execute();
}
};
void mage_spell_t::trigger_unstable_magic(action_state_t *s) {
if (!p()->talents.unstable_magic->ok())
return;
assert(p()->action.unstable_magic_explosion);
double um_proc_rate;
switch (p()->specialization()) {
case MAGE_ARCANE:
um_proc_rate = p()->action.unstable_magic_explosion
->data().effectN(1).percent();
break;
case MAGE_FROST:
um_proc_rate = p()->action.unstable_magic_explosion
->data().effectN(2).percent();
break;
case MAGE_FIRE:
um_proc_rate = p()->action.unstable_magic_explosion
->data().effectN(3).percent();
break;
default:um_proc_rate = 0.0;
break;
}
if (p()->rng().roll(um_proc_rate)) {
p()->action.unstable_magic_explosion->set_target(s->target);
p()->action.unstable_magic_explosion->base_dd_min = s->result_total;
p()->action.unstable_magic_explosion->base_dd_max = s->result_total;
p()->action.unstable_magic_explosion->execute();
}
}
// Proxy Freeze action ========================================================
struct freeze_t : public action_t {
action_t *pet_freeze;
freeze_t(mage_t *p, const std::string &options_str) :
action_t(ACTION_OTHER, "freeze", p),
pet_freeze(nullptr) {
parse_options(options_str);
may_miss = may_crit = callbacks = false;
dual = true;
trigger_gcd = timespan_t::zero();
ignore_false_positive = true;
action_skill = 1;
}
virtual bool init_finished() override {
bool ret = action_t::init_finished();
mage_t *m = debug_cast<mage_t *>(player);
if (!m->pets.water_elemental)
return ret;
pet_freeze = m->pets.water_elemental->find_action("freeze");
if (!pet_freeze) {
pet_freeze = new pets::water_elemental::freeze_t(m->pets.water_elemental);
pet_freeze->init();
}
auto water_jet =
dynamic_cast<pets::water_elemental::water_jet_t *>( m->pets.water_elemental->find_action("water_jet"));
if (water_jet) {
water_jet->autocast = false;
}
return ret;
}
virtual void execute() override {
assert(pet_freeze);
pet_freeze->set_target(target);
pet_freeze->execute();
}
virtual bool ready() override {
mage_t *m = debug_cast<mage_t *>(player);
if (!m->pets.water_elemental)
return false;
if (m->pets.water_elemental->is_sleeping())
return false;
if (!pet_freeze)
return false;
if (!pet_freeze->ready())
return false;
return action_t::ready();
}
};
// Proxy cast Water Jet Action ================================================
struct water_jet_t : public action_t {
pets::water_elemental::water_jet_t *action;
water_jet_t(mage_t *p, const std::string &options_str) :
action_t(ACTION_OTHER, "water_jet", p),
action(nullptr) {
parse_options(options_str);
may_miss = may_crit = callbacks = false;
dual = true;
trigger_gcd = timespan_t::zero();
ignore_false_positive = true;
action_skill = 1;
}
virtual bool init_finished() override {
mage_t *m = debug_cast<mage_t *>(player);
if (m->pets.water_elemental) {
action = dynamic_cast<pets::water_elemental::water_jet_t *>( m->pets.water_elemental->find_action("water_jet"));
if (action) {
action->autocast = false;
}
}
return action_t::init_finished();
}
virtual void execute() override {
assert(action);
mage_t *m = debug_cast<mage_t *>(player);
action->queued = true;
// Interrupt existing cast
if (m->pets.water_elemental->executing) {
m->pets.water_elemental->executing->interrupt_action();
}
// Cancel existing (potential) player-ready event ..
if (m->pets.water_elemental->readying) {
event_t::cancel(m->pets.water_elemental->readying);
}
// and schedule a new one immediately.
m->pets.water_elemental->schedule_ready();
}
virtual bool ready() override {
mage_t *m = debug_cast<mage_t *>(player);
if (!m->pets.water_elemental)
return false;
if (m->pets.water_elemental->is_sleeping())
return false;
if (!action)
return false;
// Ensure that the Water Elemental's water_jet is ready. Note that this
// skips the water_jet_t::ready() call, and simply checks the "base" ready
// properties of the spell (most importantly, the cooldown). If normal
// ready() was called, this would always return false, as queued = false,
// before this action executes.
if (!action->pets::water_elemental::water_elemental_spell_t::ready())
return false;
// Don't re-execute if water jet is already queued
if (action->queued)
return false;
return action_t::ready();
}
};
} // namespace actions
namespace events {
struct icicle_event_t : public event_t {
mage_t *mage;
player_t *target;
double damage;
icicle_event_t(mage_t &m, double d, player_t *t, bool first = false) :
event_t(m), mage(&m), target(t), damage(d) {
double cast_time = first ? 0.25 : (0.4 * mage->cache.spell_speed());
schedule(timespan_t::from_seconds(cast_time));
}
virtual const char *name() const override { return "icicle_event"; }
virtual void execute() override {
mage->icicle_event = nullptr;
// If the target of the icicle is dead, stop the chain
if (target->is_sleeping()) {
if (mage->sim->debug)
mage->sim->out_debug.printf("%s icicle use on %s (sleeping target), stopping",
mage->name(), target->name());
return;
}
mage->icicle->set_target(target);
mage->icicle->base_dd_min = damage;
mage->icicle->base_dd_max = damage;
mage->icicle->execute();
mage->buffs.icicles->decrement();
double new_damage = mage->get_icicle();
if (new_damage > 0.0) {
mage->icicle_event = make_event<icicle_event_t>(sim(), *mage, new_damage, target);
if (mage->sim->debug)
mage->sim->out_debug.printf("%s icicle use on %s (chained), damage=%f, total=%u",
mage->name(), target->name(), new_damage, as<unsigned>(mage->icicles.size()));
}
}
};
struct ignite_spread_event_t : public event_t {
mage_t *mage;
static double ignite_bank(dot_t *ignite) {
if (!ignite->is_ticking()) {
return 0.0;
}
auto ignite_state = debug_cast<residual_action::residual_periodic_state_t *>(
ignite->state);
return ignite_state->tick_amount * ignite->ticks_left();
}
static bool ignite_compare(dot_t *a, dot_t *b) {
return ignite_bank(a) > ignite_bank(b);
}
ignite_spread_event_t(mage_t &m, timespan_t delta_time) :
event_t(m, delta_time), mage(&m) {}
virtual const char *name() const override { return "ignite_spread_event"; }
virtual void execute() override {
mage->ignite_spread_event = nullptr;
mage->procs.ignite_spread->occur();
if (mage->sim->log) {
sim().out_log.printf("%s ignite spread event occurs", mage->name());
}
std::vector<player_t *> tl = mage->ignite->target_list();
if (tl.size() == 1) {
return;
}
std::vector<dot_t *> active_ignites;
std::vector<dot_t *> candidates;
// Split ignite targets by whether ignite is ticking
for (size_t i = 0, actors = tl.size(); i < actors; i++) {
player_t *t = tl[i];
dot_t *ignite = t->get_dot("ignite", mage);
if (ignite->is_ticking()) {
active_ignites.push_back(ignite);
} else {
candidates.push_back(ignite);
}
}
// Sort active ignites by descending bank size
std::sort(active_ignites.begin(), active_ignites.end(), ignite_compare);
// Loop over active ignites:
// - Pop smallest ignite for spreading
// - Remove equal sized ignites from tail of spread candidate list
// - Choose random target and execute spread
// - Remove spread destination from candidate list
// - Add spreaded ignite source to candidate list
// This algorithm provides random selection of the spread target, while
// guaranteeing that every source will have a larger ignite bank than the
// destination. It also guarantees that each ignite will spread to a unique
// target. This allows us to avoid N^2 spread validity checks.
while (active_ignites.size() > 0) {
dot_t *source = active_ignites.back();
active_ignites.pop_back();
double source_bank = ignite_bank(source);
if (!candidates.empty()) {
// Skip candidates that have equal ignite bank size to the source
int index = as<int>(candidates.size()) - 1;
while (index >= 0) {
if (ignite_bank(candidates[index]) < source_bank) {
break;
}
index--;
}
if (index < 0) {
// No valid spread targets
continue;
}
// TODO: Filter valid candidates by ignite spread range
// Randomly select spread target from remaining candidates
index = as<int>(floor(mage->rng().real() * index));
dot_t *destination = candidates[index];
if (destination->is_ticking()) {
// TODO: Use benefits to keep track of lost ignite banks
destination->cancel();
mage->procs.ignite_overwrite->occur();
if (mage->sim->log) {
sim().out_log.printf("%s ignite spreads from %s to %s (overwrite)",
mage->name(), source->target->name(),
destination->target->name());
}
} else {
mage->procs.ignite_new_spread->occur();
if (mage->sim->log) {
sim().out_log.printf("%s ignite spreads from %s to %s (new)",
mage->name(), source->target->name(),
destination->target->name());
}
}
source->copy(destination->target, DOT_COPY_CLONE);
// Remove spread destination from candidates
candidates.erase(candidates.begin() + index);
}
// Add spread source to candidates
candidates.push_back(source);
}
// Schedule next spread for 2 seconds later
mage->ignite_spread_event = make_event<events::ignite_spread_event_t>(
sim(), *mage, timespan_t::from_seconds(2.0));
}
};
} // namespace events
// ==========================================================================
// Mage Character Definition
// ==========================================================================
// mage_td_t ================================================================
mage_td_t::mage_td_t(player_t *target, mage_t *mage) :
actor_target_data_t(target, mage),
dots(dots_t()),
debuffs(debuffs_t()) {
dots.conflagration_dot = target->get_dot("conflagration_dot", mage);
dots.ignite = target->get_dot("ignite", mage);
dots.living_bomb = target->get_dot("living_bomb", mage);
dots.nether_tempest = target->get_dot("nether_tempest", mage);
debuffs.erosion = new buffs::erosion_t(this);
debuffs.slow = buff_creator_t(*this, "slow", mage->find_spell(31589));
debuffs.frost_bomb = buff_creator_t(*this, "frost_bomb", mage->talents.frost_bomb);
debuffs.frozen = buff_creator_t(*this, "frozen")
.duration(timespan_t::from_seconds(0.5));
debuffs.water_jet = buff_creator_t(*this, "water_jet", mage->find_spell(135029))
.cd(timespan_t::zero());
debuffs.winters_chill = buff_creator_t(*this, "winters_chill", mage->find_spell(228358))
.chance(mage->spec.brain_freeze_2->ok() ? 1.0 : 0.0);
}
mage_t::mage_t(sim_t *sim, const std::string &name, race_e r) :
player_t(sim, MAGE, name, r),
icicle(nullptr),
icicle_event(nullptr),
ignite(nullptr),
ignite_spread_event(nullptr),
last_bomb_target(nullptr),
distance_from_rune(0.0),
global_cinder_count(0.0),
firestarter_time(timespan_t::zero()),
blessing_of_wisdom_count(0),
allow_shimmer_lance(false),
action(actions_t()),
benefits(benefits_t()),
buffs(buffs_t()),
cooldowns(cooldowns_t()),
gains(gains_t()),
pets(pets_t()),
procs(procs_t()),
sample_data(sample_data_t()),
spec(specializations_t()),
state(state_t()),
talents(talents_list_t()) {
// Cooldowns
cooldowns.combustion = get_cooldown("combustion");
cooldowns.cone_of_cold = get_cooldown("cone_of_cold");
cooldowns.evocation = get_cooldown("evocation");
cooldowns.frost_nova = get_cooldown("frost_nova");
cooldowns.frozen_orb = get_cooldown("frozen_orb");
cooldowns.icy_veins = get_cooldown("icy_veins");
cooldowns.presence_of_mind = get_cooldown("presence_of_mind");
cooldowns.ray_of_frost = get_cooldown("ray_of_frost");
cooldowns.time_warp = get_cooldown("time_warp");
// Options
regen_type = REGEN_DYNAMIC;
regen_caches[CACHE_MASTERY] = true;
talent_points.register_validity_fn([this](const spell_data_t *spell) {
// Soul of the Archmage
if (find_item(151642)) {
// TODO: Ring is giving AF, SI and the new NYI Temporal Flux on beta, probably not intended.
switch (specialization()) {
case MAGE_ARCANE:return spell->id() == 234302; // Temporal Flux
case MAGE_FIRE:return spell->id() == 205029; // Flame On
case MAGE_FROST:return spell->id() == 205030; // Frozen Touch
default:break;
}
}
return false;
});
}
mage_t::~mage_t() {
delete benefits.arcane_charge.arcane_barrage;
delete benefits.arcane_charge.arcane_blast;
delete benefits.arcane_charge.nether_tempest;
delete benefits.magtheridons_might;
delete benefits.ray_of_frost;
delete benefits.zannesu_journey;
delete sample_data.burn_duration_history;
delete sample_data.burn_initial_mana;
delete sample_data.blizzard;
delete sample_data.t20_4pc;
delete sample_data.icy_veins_duration;
}
bool mage_t::apply_crowd_control(const action_state_t *state, spell_mechanic type) {
if (type == MECHANIC_INTERRUPT) {
buffs.sephuzs_secret->trigger();
return true;
}
if (action_t::result_is_hit(state->result)
&& (state->target->is_add() || state->target->level() < sim->max_player_level + 3)) {
buffs.sephuzs_secret->trigger();
if (type == MECHANIC_ROOT) {
get_target_data(state->target)->debuffs.frozen->trigger();
}
return true;
}
return false;
}
// mage_t::create_action ====================================================
action_t *mage_t::create_action(const std::string &name,
const std::string &options_str) {
using namespace actions;
// Arcane
if (name == "arcane_barrage")
return new arcane_barrage_t(this, options_str);
if (name == "arcane_blast")
return new arcane_blast_t(this, options_str);
if (name == "arcane_explosion")
return new arcane_explosion_t(this, options_str);
if (name == "arcane_missiles")
return new arcane_missiles_t(this, options_str);
if (name == "arcane_orb")
return new arcane_orb_t(this, options_str);
if (name == "arcane_power")
return new arcane_power_t(this, options_str);
if (name == "charged_up")
return new charged_up_t(this, options_str);
if (name == "evocation")
return new evocation_t(this, options_str);
if (name == "nether_tempest")
return new nether_tempest_t(this, options_str);
if (name == "presence_of_mind")
return new presence_of_mind_t(this, options_str);
if (name == "slow")
return new slow_t(this, options_str);
if (name == "summon_arcane_familiar")
return new summon_arcane_familiar_t(this, options_str);
if (name == "supernova")
return new supernova_t(this, options_str);
if (name == "start_burn_phase")
return new start_burn_phase_t(this, options_str);
if (name == "stop_burn_phase")
return new stop_burn_phase_t(this, options_str);
// Fire
if (name == "blast_wave")
return new blast_wave_t(this, options_str);
if (name == "cinderstorm")
return new cinderstorm_t(this, options_str);
if (name == "combustion")
return new combustion_t(this, options_str);
if (name == "dragons_breath")
return new dragons_breath_t(this, options_str);
if (name == "fireball")
return new fireball_t(this, options_str);
if (name == "flamestrike")
return new flamestrike_t(this, options_str);
if (name == "fire_blast")
return new fire_blast_t(this, options_str);
if (name == "living_bomb")
return new living_bomb_t(this, options_str);
if (name == "meteor")
return new meteor_t(this, options_str);
if (name == "phoenix_flames")
return new phoenix_flames_t(this, options_str);
if (name == "pyroblast")
return new pyroblast_t(this, options_str);
if (name == "scorch")
return new scorch_t(this, options_str);
// Frost
if (name == "blizzard")
return new blizzard_t(this, options_str);
if (name == "cold_snap")
return new cold_snap_t(this, options_str);
if (name == "comet_storm")
return new comet_storm_t(this, options_str);
if (name == "cone_of_cold")
return new cone_of_cold_t(this, options_str);
if (name == "flurry")
return new flurry_t(this, options_str);
if (name == "frost_bomb")
return new frost_bomb_t(this, options_str);
if (name == "frostbolt")
return new frostbolt_t(this, options_str);
if (name == "frozen_orb")
return new frozen_orb_t(this, options_str);
if (name == "glacial_spike")
return new glacial_spike_t(this, options_str);
if (name == "ice_floes")
return new ice_floes_t(this, options_str);
if (name == "ice_lance")
return new ice_lance_t(this, options_str);
if (name == "ice_nova")
return new ice_nova_t(this, options_str);
if (name == "icy_veins")
return new icy_veins_t(this, options_str);
if (name == "ray_of_frost")
return new ray_of_frost_t(this, options_str);
if (name == "water_elemental")
return new summon_water_elemental_t(this, options_str);
if (name == "freeze")
return new freeze_t(this, options_str);
if (name == "water_jet")
return new water_jet_t(this, options_str);
// Shared spells
if (name == "arcane_intellect")
return new arcane_intellect_t(this, options_str);
if (name == "blink") {
if (talents.shimmer->ok()) {
return new shimmer_t(this, options_str);
} else {
return new blink_t(this, options_str);
}
}
if (name == "counterspell")
return new counterspell_t(this, options_str);
if (name == "frost_nova")
return new frost_nova_t(this, options_str);
if (name == "time_warp")
return new time_warp_t(this, options_str);
// Shared talents
if (name == "mirror_image")
return new mirror_image_t(this, options_str);
if (name == "rune_of_power")
return new rune_of_power_t(this, options_str);
if (name == "shimmer")
return new shimmer_t(this, options_str);
return player_t::create_action(name, options_str);
}
// mage_t::create_actions =====================================================
bool mage_t::create_actions() {
using namespace actions;
if (spec.ignite->ok()) {
ignite = new ignite_t(this);
}
if (spec.icicles->ok()) {
icicle = new icicle_t(this);
}
if (talents.arcane_familiar->ok()) {
action.arcane_assault = new arcane_assault_t(this);
}
if (talents.frost_bomb->ok()) {
action.frost_bomb_explosion = new frost_bomb_explosion_t(this);
}
if (talents.unstable_magic->ok()) {
action.unstable_magic_explosion = new unstable_magic_explosion_t(this);
}
// Global actions for 7.2.5 legendaries.
// TODO: Probably a better idea to construct these in the legendary callbacks?
switch (specialization()) {
case MAGE_ARCANE:action.legendary_arcane_orb = new arcane_orb_t(this, "", true);
break;
case MAGE_FIRE:action.legendary_meteor = new meteor_t(this, "", true);
break;
case MAGE_FROST:action.legendary_comet_storm = new comet_storm_t(this, "", true);
break;
default:break;
}
return player_t::create_actions();
}
// mage_t::create_options =====================================================
void mage_t::create_options() {
add_option(opt_float("global_cinder_count", global_cinder_count));
add_option(opt_timespan("firestarter_time", firestarter_time));
add_option(opt_int("blessing_of_wisdom_count", blessing_of_wisdom_count));
add_option(opt_bool("allow_shimmer_lance", allow_shimmer_lance));
player_t::create_options();
}
// mage_t::create_profile ================================================
std::string mage_t::create_profile(save_e save_type) {
std::string profile = player_t::create_profile(save_type);
if (save_type == SAVE_ALL) {
if (firestarter_time > timespan_t::zero()) {
profile += "firestarter_time=" + util::to_string(firestarter_time.total_seconds()) + "\n";
}
}
return profile;
}
// mage_t::copy_from =====================================================
void mage_t::copy_from(player_t *source) {
player_t::copy_from(source);
mage_t *p = debug_cast<mage_t *>(source);
global_cinder_count = p->global_cinder_count;
firestarter_time = p->firestarter_time;
blessing_of_wisdom_count = p->blessing_of_wisdom_count;
allow_shimmer_lance = p->allow_shimmer_lance;
}
// mage_t::merge =========================================================
void mage_t::merge(player_t &other) {
player_t::merge(other);
mage_t &mage = dynamic_cast<mage_t &>( other );
for (size_t i = 0; i < cooldown_waste_data_list.size(); i++) {
cooldown_waste_data_list[i]->merge(*mage.cooldown_waste_data_list[i]);
}
for (size_t i = 0; i < proc_source_list.size(); i++) {
proc_source_list[i]->merge(*mage.proc_source_list[i]);
}
switch (specialization()) {
case MAGE_ARCANE:sample_data.burn_duration_history->merge(*mage.sample_data.burn_duration_history);
sample_data.burn_initial_mana->merge(*mage.sample_data.burn_initial_mana);
break;
case MAGE_FIRE:break;
case MAGE_FROST:
if (talents.thermal_void->ok()) {
sample_data.icy_veins_duration->merge(*mage.sample_data.icy_veins_duration);
}
break;
default:break;
}
}
// mage_t::analyze =======================================================
void mage_t::analyze(sim_t &s) {
player_t::analyze(s);
range::for_each(cooldown_waste_data_list, std::mem_fn(&cooldown_waste_data_t::analyze));
switch (specialization()) {
case MAGE_ARCANE:sample_data.burn_duration_history->analyze();
sample_data.burn_initial_mana->analyze();
break;
case MAGE_FIRE:break;
case MAGE_FROST:
if (talents.thermal_void->ok()) {
sample_data.icy_veins_duration->analyze();
}
break;
default:break;
}
}
// mage_t::datacollection_begin ===============================================
void mage_t::datacollection_begin() {
player_t::datacollection_begin();
range::for_each(cooldown_waste_data_list, std::mem_fn(&cooldown_waste_data_t::datacollection_begin));
range::for_each(proc_source_list, std::mem_fn(&proc_source_t::datacollection_begin));
}
// mage_t::datacollection_end =================================================
void mage_t::datacollection_end() {
player_t::datacollection_end();
range::for_each(cooldown_waste_data_list, std::mem_fn(&cooldown_waste_data_t::datacollection_end));
range::for_each(proc_source_list, std::mem_fn(&proc_source_t::datacollection_end));
}
// mage_t::create_pets ========================================================
void mage_t::create_pets() {
if (specialization() == MAGE_FROST && !talents.lonely_winter->ok() && find_action("water_elemental")) {
pets.water_elemental = new pets::water_elemental::water_elemental_pet_t(sim, this);
}
if (talents.mirror_image->ok() && find_action("mirror_image")) {
int image_num = talents.mirror_image->effectN(2).base_value();
for (int i = 0; i < image_num; i++) {
pets.mirror_images.push_back(new pets::mirror_image::mirror_image_pet_t(sim, this));
if (i > 0) {
pets.mirror_images[i]->quiet = true;
}
}
}
}
// mage_t::init_spells ========================================================
void mage_t::init_spells() {
player_t::init_spells();
// Talents
// Tier 15
talents.rule_of_threes = find_talent_spell("Rule of Threes");
talents.mana_adept = find_talent_spell("Mana Adept");
talents.arcane_familiar = find_talent_spell("Arcane Familiar");
talents.pyromaniac = find_talent_spell("Pyromaniac");
talents.conflagration = find_talent_spell("Conflagration");
talents.firestarter = find_talent_spell("Firestarter");
talents.ray_of_frost = find_talent_spell("Ray of Frost");
talents.lonely_winter = find_talent_spell("Lonely Winter");
talents.bone_chilling = find_talent_spell("Bone Chilling");
// Tier 30
talents.shimmer = find_talent_spell("Shimmer");
talents.slipstream = find_talent_spell("Slipstream");
talents.blast_wave = find_talent_spell("Blast Wave");
talents.ice_floes = find_talent_spell("Ice Floes");
talents.mana_shield = find_talent_spell("Mana Shield");
talents.blazing_soul = find_talent_spell("Blazing Soul");
talents.glacial_insulation = find_talent_spell("Glacial Insulation");
// Tier 45
talents.mirror_image = find_talent_spell("Mirror Image");
talents.rune_of_power = find_talent_spell("Rune of Power");
talents.incanters_flow = find_talent_spell("Incanter's Flow");
// Tier 60
talents.supernova = find_talent_spell("Supernova");
talents.charged_up = find_talent_spell("Charged Up");
talents.resonance = find_talent_spell("Resonance");
talents.alexstraszas_fury = find_talent_spell("Alexstrasza's Fury");
talents.flame_on = find_talent_spell("Flame On");
talents.phoenix_flames = find_talent_spell("Phoenix Flames");
talents.ice_nova = find_talent_spell("Ice Nova");
talents.frozen_touch = find_talent_spell("Frozen Touch");
talents.splitting_ice = find_talent_spell("Splitting Ice");
// Tier 75
talents.chrono_shift = find_talent_spell("Chrono Shift");
talents.frenetic_speed = find_talent_spell("Frenetic Speed");
talents.frigid_winds = find_talent_spell("Frigid Winds");
talents.ring_of_frost = find_talent_spell("Ring of Frost");
talents.ice_ward = find_talent_spell("Ice Ward");
// Tier 90
talents.nether_tempest = find_talent_spell("Nether Tempest");
talents.living_bomb = find_talent_spell("Living Bomb");
talents.frost_bomb = find_talent_spell("Frost Bomb");
talents.unstable_magic = find_talent_spell("Unstable Magic");
talents.erosion = find_talent_spell("Erosion");
talents.flame_patch = find_talent_spell("Flame Patch");
talents.arctic_gale = find_talent_spell("Arctic Gale");
// Tier 100
talents.overpowered = find_talent_spell("Overpowered");
talents.temporal_flux = find_talent_spell("Temporal Flux");
talents.arcane_orb = find_talent_spell("Arcane Orb");
talents.kindling = find_talent_spell("Kindling");
talents.cinderstorm = find_talent_spell("Cinderstorm");
talents.meteor = find_talent_spell("Meteor");
talents.thermal_void = find_talent_spell("Thermal Void");
talents.glacial_spike = find_talent_spell("Glacial Spike");
talents.comet_storm = find_talent_spell("Comet Storm");
// Spec Spells
spec.arcane_barrage_2 = find_specialization_spell(231564);
spec.arcane_charge = find_spell(36032);
spec.arcane_mage = find_specialization_spell(137021);
spec.clearcasting = find_specialization_spell("Clearcasting");
spec.evocation_2 = find_specialization_spell(231565);
spec.critical_mass = find_specialization_spell("Critical Mass");
spec.critical_mass_2 = find_specialization_spell(231630);
spec.enhanced_pyrotechnics = find_specialization_spell(157642);
spec.fire_blast_2 = find_specialization_spell(231568);
spec.fire_blast_3 = find_specialization_spell(231567);
spec.fire_mage = find_specialization_spell(137019);
spec.hot_streak = find_specialization_spell(195283);
spec.brain_freeze = find_specialization_spell("Brain Freeze");
spec.brain_freeze_2 = find_specialization_spell(231584);
spec.blizzard_2 = find_specialization_spell(236662);
spec.fingers_of_frost = find_specialization_spell("Fingers of Frost");
spec.frost_mage = find_specialization_spell(137020);
spec.shatter = find_specialization_spell("Shatter");
spec.shatter_2 = find_specialization_spell(231582);
// Mastery
spec.savant = find_mastery_spell(MAGE_ARCANE);
spec.ignite = find_mastery_spell(MAGE_FIRE);
spec.icicles = find_mastery_spell(MAGE_FROST);
spec.icicles_driver = find_spell(148012);
}
// mage_t::init_base ========================================================
void mage_t::init_base_stats() {
if (base.distance < 1)
base.distance = 30;
player_t::init_base_stats();
base.spell_power_per_intellect = 1.0;
base.attack_power_per_strength = 0.0;
base.attack_power_per_agility = 0.0;
base.mana_regen_per_second = resources.base[RESOURCE_MANA] * 0.01;
base.mana_regen_per_second *= 1.0 + find_spell(121039)->effectN(1).percent();
}
// mage_t::create_buffs =======================================================
void mage_t::create_buffs() {
player_t::create_buffs();
// Arcane
buffs.arcane_charge = buff_creator_t(this, "arcane_charge", spec.arcane_charge);
buffs.arcane_familiar = buff_creator_t(this, "arcane_familiar", find_spell(210126))
.default_value(find_spell(210126)->effectN(1).percent())
.period(timespan_t::from_seconds(3.0))
.tick_behavior(BUFF_TICK_CLIP)
.tick_time_behavior(BUFF_TICK_TIME_HASTED)
.tick_callback([this](buff_t *, int, const timespan_t &) {
assert(action.arcane_assault);
action.arcane_assault->set_target(target);
action.arcane_assault->execute();
})
.stack_change_callback([this](buff_t *, int, int) { recalculate_resource_max(RESOURCE_MANA); });
buffs.arcane_power = buff_creator_t(this, "arcane_power", find_spell(12042))
.default_value(find_spell(12042)->effectN(1).percent()
+ talents.overpowered->effectN(1).percent());
buffs.chrono_shift = buff_creator_t(this, "chrono_shift", find_spell(236298))
.default_value(find_spell(236298)->effectN(1).percent())
.add_invalidate(CACHE_RUN_SPEED);
buffs.clearcasting = buff_creator_t(this, "clearcasting", find_spell(263725))
.default_value(find_spell(263725)->effectN(1).percent());
buffs.crackling_energy = buff_creator_t(this, "crackling_energy", find_spell(246224))
.default_value(find_spell(246224)->effectN(1).percent());
buffs.expanding_mind = buff_creator_t(this, "expanding_mind", find_spell(253262))
.add_invalidate(CACHE_PLAYER_DAMAGE_MULTIPLIER);
buffs.presence_of_mind = buff_creator_t(this, "presence_of_mind", find_spell(205025))
.cd(timespan_t::zero())
.stack_change_callback([this](buff_t *, int, int cur) {
if (cur == 0)
cooldowns.presence_of_mind->start();
});
buffs.quick_thinker = haste_buff_creator_t(this, "quick_thinker", find_spell(253299))
.default_value(find_spell(253299)->effectN(1).percent())
.chance(sets->set(MAGE_ARCANE, T21, B4)->proc_chance());
buffs.rule_of_threes = buff_creator_t(this, "rule_of_threes", find_spell(264774))
.default_value(find_spell(264774)->effectN(1).percent());
// Fire
buffs.combustion = buff_creator_t(this, "combustion", find_spell(190319))
.cd(timespan_t::zero())
.add_invalidate(CACHE_MASTERY)
.default_value(find_spell(190319)->effectN(1).percent());
buffs.combustion->buff_duration += sets->set(MAGE_FIRE, T21, B2)->effectN(1).time_value();
buffs.critical_massive = buff_creator_t(this, "critical_massive", find_spell(242251))
.default_value(find_spell(242251)->effectN(1).percent());
buffs.enhanced_pyrotechnics = buff_creator_t(this, "enhanced_pyrotechnics", find_spell(157644))
.chance(spec.enhanced_pyrotechnics->ok() ? 1.0 : 0.0)
.default_value(find_spell(157644)->effectN(1).percent()
+ sets->set(MAGE_FIRE, T19, B2)->effectN(1).percent());
buffs.erupting_infernal_core = buff_creator_t(this, "erupting_infernal_core", find_spell(248147));
buffs.frenetic_speed = buff_creator_t(this, "frenetic_speed", find_spell(236060))
.default_value(find_spell(236060)->effectN(1).percent())
.add_invalidate(CACHE_RUN_SPEED);
buffs.ignition = buff_creator_t(this, "ignition", find_spell(246261))
.trigger_spell(sets->set(MAGE_FIRE, T20, B2));
buffs.inferno = buff_creator_t(this, "inferno", find_spell(253220))
.default_value(find_spell(253220)->effectN(1).percent());
buffs.inferno->buff_duration = buffs.combustion->buff_duration;
buffs.heating_up = buff_creator_t(this, "heating_up", find_spell(48107));
buffs.hot_streak = buff_creator_t(this, "hot_streak", find_spell(48108));
buffs.streaking = haste_buff_creator_t(this, "streaking", find_spell(211399))
.default_value(find_spell(211399)->effectN(1).percent());
// Frost
buffs.arctic_blast = buff_creator_t(this, "arctic_blast", find_spell(253257))
.default_value(find_spell(253257)->effectN(1).percent())
.chance(sets->has_set_bonus(MAGE_FROST, T21, B4) ? 1.0 : 0.0);
buffs.brain_freeze = new buffs::brain_freeze_buff_t(this);
buffs.bone_chilling = buff_creator_t(this, "bone_chilling", find_spell(205766))
.default_value(talents.bone_chilling->effectN(1).percent() / 10);
buffs.fingers_of_frost = buff_creator_t(this, "fingers_of_frost", find_spell(44544))
.max_stack(find_spell(44544)->max_stacks());
buffs.frozen_mass = buff_creator_t(this, "frozen_mass", find_spell(242253))
.default_value(find_spell(242253)->effectN(1).percent());
buffs.rage_of_the_frost_wyrm = buff_creator_t(this, "rage_of_the_frost_wyrm", find_spell(248177));
// Buff to track icicles. This does not, however, track the true amount of icicles present.
// Instead, as it does in game, it tracks icicle buff stack count based on the number of *casts*
// of icicle generating spells. icicles are generated on impact, so they are slightly de-synced.
//
// A note about in-game implementation. At first, it might seem that each stack has an independent
// expiration timer, but the timing is a bit off and it just doesn't happen in the cases where
// Icicle buff is incremented but the actual Icicle never created.
//
// Instead, the buff is incremented when:
// * Frostbolt executes
// * Ice Nine creates another Icicle
// * One of the Icicles overflows
//
// It is unclear if the buff is incremented twice or three times when Ice Nine procs and two Icicles
// overflow (combat log doesn't track refreshes for Icicles buff).
//
// The buff is decremented when:
// * One of the Icicles is removed
// - Launched after Ice Lance
// - Launched on overflow
// - Removed as a part of Glacial Spike execute
// - Expired after 60 sec
//
// This explains why some Icicle stacks remain if Glacial Spike executes with 5 Icicle stacks but less
// than 5 actual Icicles.
buffs.icicles = buff_creator_t(this, "icicles", find_spell(205473));
buffs.icy_veins = new buffs::icy_veins_buff_t(this);
buffs.ray_of_frost = new buffs::ray_of_frost_buff_t(this);
// Talents
buffs.ice_floes = buff_creator_t(this, "ice_floes", talents.ice_floes);
buffs.incanters_flow = new buffs::incanters_flow_t(this);
buffs.rune_of_power = buff_creator_t(this, "rune_of_power", find_spell(116014))
.duration(find_spell(116011)->duration())
.default_value(find_spell(116014)->effectN(1).percent());
// Legendary
buffs.lady_vashjs_grasp = new buffs::lady_vashjs_grasp_t(this);
//Misc
// N active GBoWs are modeled by a single buff that gives N times as much mana.
buffs.greater_blessing_of_widsom = make_buff(this, "greater_blessing_of_wisdom", find_spell(203539))
->set_tick_callback([this](buff_t *, int, const timespan_t &) {
resource_gain(RESOURCE_MANA,
resources.max[RESOURCE_MANA] * 0.002 * blessing_of_wisdom_count,
gains.greater_blessing_of_wisdom);
})
->set_period(find_spell(203539)->effectN(2).period())
->set_tick_behavior(BUFF_TICK_CLIP);
buffs.t19_oh_buff =
stat_buff_creator_t(this, "ancient_knowledge", sets->set(specialization(), T19OH, B8)->effectN(1).trigger())
.trigger_spell(sets->set(specialization(), T19OH, B8));
buffs.shimmer = buff_creator_t(this, "shimmer", find_spell(212653));
}
// mage_t::init_gains =======================================================
void mage_t::init_gains() {
player_t::init_gains();
gains.evocation = get_gain("Evocation");
gains.mystic_kilt_of_the_rune_master = get_gain("Mystic Kilt of the Rune Master");
gains.greater_blessing_of_wisdom = get_gain("Greater Blessing of Wisdom");
}
// mage_t::init_procs =======================================================
void mage_t::init_procs() {
player_t::init_procs();
switch (specialization()) {
case MAGE_ARCANE:break;
case MAGE_FROST:procs.brain_freeze_flurry = get_proc("Brain Freeze Flurries cast");
procs.fingers_of_frost_wasted = get_proc("Fingers of Frost wasted due to Winter's Chill");
break;
case MAGE_FIRE:procs.heating_up_generated = get_proc("Heating Up generated");
procs.heating_up_removed = get_proc("Heating Up removed");
procs.heating_up_ib_converted = get_proc("IB conversions of HU");
procs.hot_streak = get_proc("Total Hot Streak procs");
procs.hot_streak_pyromaniac = get_proc("Total Hot Streak procs from Pyromaniac");
procs.hot_streak_spell = get_proc("Hot Streak spells used");
procs.hot_streak_spell_crit = get_proc("Hot Streak spell crits");
procs.hot_streak_spell_crit_wasted = get_proc("Wasted Hot Streak spell crits");
procs.ignite_applied = get_proc("Direct Ignite applications");
procs.ignite_spread = get_proc("Ignites spread");
procs.ignite_new_spread = get_proc("Ignites spread to new targets");
procs.ignite_overwrite = get_proc("Ignites spread to target with existing ignite");
break;
default:
// This shouldn't happen
break;
}
}
// mage_t::init_resources =====================================================
void mage_t::init_resources(bool force) {
player_t::init_resources(force);
// This is the call needed to set max mana at the beginning of the sim.
// If this is called without recalculating max mana afterwards, it will
// overwrite the recalculating done earlier in reset() and cache_invalidate()
// back to default max mana.
if (spec.savant->ok()) {
recalculate_resource_max(RESOURCE_MANA);
}
}
// mage_t::init_benefits ======================================================
void mage_t::init_benefits() {
player_t::init_benefits();
if (specialization() == MAGE_ARCANE) {
benefits.arcane_charge.arcane_barrage =
new buff_stack_benefit_t(buffs.arcane_charge, "Arcane Barrage");
benefits.arcane_charge.arcane_blast =
new buff_stack_benefit_t(buffs.arcane_charge, "Arcane Blast");
if (talents.nether_tempest->ok()) {
benefits.arcane_charge.nether_tempest =
new buff_stack_benefit_t(buffs.arcane_charge, "Nether Tempest");
}
}
if (specialization() == MAGE_FROST) {
if (buffs.magtheridons_might->default_chance != 0.0) {
benefits.magtheridons_might =
new buff_stack_benefit_t(buffs.magtheridons_might, "Ice Lance +");
}
if (talents.ray_of_frost->ok()) {
benefits.ray_of_frost =
new buff_stack_benefit_t(buffs.ray_of_frost, "Ray of Frost Tick +");
}
if (buffs.zannesu_journey->default_chance != 0.0) {
benefits.zannesu_journey =
new buff_stack_benefit_t(buffs.zannesu_journey, "Blizzard +");
}
}
}
// mage_t::init_uptimes =======================================================
void mage_t::init_uptimes() {
player_t::init_uptimes();
switch (specialization()) {
case MAGE_ARCANE:uptime.burn_phase = get_uptime("Burn Phase");
uptime.conserve_phase = get_uptime("Conserve Phase");
sample_data.burn_duration_history = new extended_sample_data_t("Burn duration history", false);
sample_data.burn_initial_mana = new extended_sample_data_t("Burn initial mana", false);
break;
case MAGE_FROST:sample_data.blizzard = new cooldown_reduction_data_t(cooldowns.frozen_orb, "Blizzard");
if (talents.thermal_void->ok()) {
sample_data.icy_veins_duration = new extended_sample_data_t("Icy Veins duration", false);
}
if (talents.glacial_spike->ok()) {
sample_data.glacial_spike_base = get_sample_data("Glacial Spike base damage contribution");
sample_data.glacial_spike_icicles = get_sample_data("Glacial Spike Icicle damage contribution");
}
if (sets->has_set_bonus(MAGE_FROST, T20, B4)) {
sample_data.t20_4pc = new cooldown_reduction_data_t(cooldowns.frozen_orb, "T20 4pc");
}
break;
case MAGE_FIRE:break;
default:break;
}
}
// mage_t::init_actions =====================================================
void mage_t::init_action_list() {
if (!action_list_str.empty()) {
player_t::init_action_list();
return;
}
clear_action_priority_lists();
apl_precombat();
switch (specialization()) {
case MAGE_ARCANE:apl_arcane();
break;
case MAGE_FROST:apl_frost();
break;
case MAGE_FIRE:apl_fire();
break;
default:apl_default(); // DEFAULT
break;
}
// Default
use_default_action_list = true;
player_t::init_action_list();
}
// This method only handles 1 item per call in order to allow the user to add special conditons and placements
// to certain items.
std::string mage_t::get_special_use_items(const std::string &item_name, bool specials) {
std::string actions;
std::string conditions;
// If we're dealing with a special item, find its special conditional for the right spec.
if (specials) {
if (specialization() == MAGE_FIRE) {
if (item_name == "obelisk_of_the_void") {
conditions = "if=cooldown.combustion.remains>50";
}
if (item_name == "horn_of_valor") {
conditions = "if=cooldown.combustion.remains>30";
}
}
}
for (const auto &item : mage_t::player_t::items) {
// This will skip Addon and Enchant-based on-use effects. Addons especially are important to
// skip from the default APLs since they will interfere with the potion timer, which is almost
// always preferred over an Addon.
// Special or not, we need the name and slot
if (item.has_special_effect(SPECIAL_EFFECT_SOURCE_ITEM, SPECIAL_EFFECT_USE) && item_name == item.name_str) {
std::string action_string = "use_item,name=";
action_string += item.name_str;
// If special, we care about special conditions and placement. Else, we only care about placement in the APL.
if (specials) {
action_string += ",";
action_string += conditions;
}
actions = action_string;
}
}
return actions;
}
// Pre-combat Action Priority List============================================
void mage_t::apl_precombat() {
action_priority_list_t *precombat = get_action_priority_list("precombat");
precombat->add_action("flask");
precombat->add_action("food");
precombat->add_action("augmentation");
// Water Elemental
if (specialization() == MAGE_FROST)
precombat->add_action("water_elemental");
if (specialization() == MAGE_ARCANE)
precombat->add_action("summon_arcane_familiar");
// Snapshot Stats
precombat->add_action("snapshot_stats");
// Level 90 talents
precombat->add_talent(this, "Mirror Image");
precombat->add_action("potion");
switch (specialization()) {
case MAGE_ARCANE:precombat->add_action(this, "Arcane Blast");
break;
case MAGE_FIRE:precombat->add_action(this, "Pyroblast");
break;
case MAGE_FROST:precombat->add_action(this, "Frostbolt");
break;
default:break;
}
}
std::string mage_t::default_potion() const {
std::string lvl110_potion =
(specialization() == MAGE_ARCANE) ? "deadly_grace" :
(specialization() == MAGE_FIRE) ? "prolonged_power" :
"prolonged_power";
return (true_level >= 100) ? lvl110_potion :
(true_level >= 90) ? "draenic_intellect" :
(true_level >= 85) ? "jade_serpent" :
(true_level >= 80) ? "volcanic" :
"disabled";
}
std::string mage_t::default_flask() const {
return (true_level >= 100) ? "whispered_pact" :
(true_level >= 90) ? "greater_draenic_intellect_flask" :
(true_level >= 85) ? "warm_sun" :
(true_level >= 80) ? "draconic_mind" :
"disabled";
}
std::string mage_t::default_food() const {
std::string lvl100_food =
(specialization() == MAGE_ARCANE) ? "sleeper_sushi" :
(specialization() == MAGE_FIRE) ? "pickled_eel" :
"salty_squid_roll";
return (true_level > 100) ? "lemon_herb_filet" :
(true_level > 90) ? lvl100_food :
(true_level >= 90) ? "mogu_fish_stew" :
(true_level >= 80) ? "severed_sagefish_head" :
"disabled";
}
std::string mage_t::default_rune() const {
return (true_level >= 110) ? "defiled" :
(true_level >= 100) ? "focus" :
"disabled";
}
// Arcane Mage Action List====================================================
void mage_t::apl_arcane() {
std::vector<std::string> racial_actions = get_racial_actions();
action_priority_list_t *default_list = get_action_priority_list("default");
action_priority_list_t *variables = get_action_priority_list("variables");
action_priority_list_t *build = get_action_priority_list("build");
action_priority_list_t *conserve = get_action_priority_list("conserve");
action_priority_list_t *burn = get_action_priority_list("burn");
default_list->add_action(this, "Counterspell", "if=target.debuff.casting.react", "Interrupt the boss when possible.");
default_list->add_action(this,
"Time Warp",
"if=buff.bloodlust.down&(time=0|(buff.arcane_power.up&(buff.potion.up|!action.potion.usable))|target.time_to_die<=buff.bloodlust.duration)",
"3 different lust usages to support Shard: on pull; during Arcane Power (with potion, preferably); end of fight.");
default_list->add_action("call_action_list,name=variables", "Set variables used throughout the APL.");
default_list->add_action("cancel_buff,name=presence_of_mind,if=active_enemies>1&set_bonus.tier20_2pc",
"AoE scenarios will delay our Presence of Mind cooldown because we'll be using Arcane Explosion instead of Arcane Blast, so we cancel the aura immediately.");
default_list->add_action(mage_t::get_special_use_items("horn_of_valor"));
default_list->add_action(mage_t::get_special_use_items("obelisk_of_the_void"));
default_list->add_action(mage_t::get_special_use_items("mrrgrias_favor"));
default_list->add_action(mage_t::get_special_use_items("pharameres_forbidden_grimoire"));
default_list->add_action(mage_t::get_special_use_items("kiljaedens_burning_wish"));
default_list->add_action(
"call_action_list,name=build,if=buff.arcane_charge.stack<buff.arcane_charge.max_stack&!burn_phase",
"Build Arcane Charges before doing anything else. Burn phase has some specific actions for building Arcane Charges, so we avoid entering this list if currently burning.");
default_list->add_action(
"call_action_list,name=burn,if=(buff.arcane_charge.stack=buff.arcane_charge.max_stack&variable.time_until_burn=0)|burn_phase",
"Enter burn actions if we're ready to burn, or already burning.");
default_list->add_action("call_action_list,name=conserve", "Fallback to conserve rotation.");
variables->add_action("variable,name=arcane_missiles_procs,op=set,value=buff.arcane_missiles.react",
"Track the number of Arcane Missiles procs that we have.");
variables->add_action("variable,name=time_until_burn,op=set,value=cooldown.arcane_power.remains",
"Burn condition #1: Arcane Power has to be available.");
variables->add_action(
"variable,name=time_until_burn,op=max,value=cooldown.evocation.remains-variable.average_burn_length",
"Burn condition #2: Evocation should be up by the time we finish burning. We use the custom variable average_burn_length to help estimate when Evocation will be available.");
variables->add_action(
"variable,name=time_until_burn,op=max,value=cooldown.presence_of_mind.remains,if=set_bonus.tier20_2pc",
"Burn condition #3: 2pt20 grants a damage boost with Presence of Mind usage, so we definitely want to stack that with AP.");
variables->add_action(
"variable,name=time_until_burn,op=max,value=action.rune_of_power.usable_in,if=talent.rune_of_power.enabled",
"Burn condition #4: We need an RoP charge if we've actually taken the talent. Check usable_in to see when we'll be able to cast, and ignore the line if we didn't take the talent.");
variables->add_action(
"variable,name=time_until_burn,op=max,value=cooldown.charged_up.remains,if=talent.charged_up.enabled&set_bonus.tier21_2pc",
"Burn condition #5: Charged Up allows the 2pt21 buff to be extended during our burn phase.");
variables->add_action("variable,name=time_until_burn,op=reset,if=target.time_to_die<variable.average_burn_length",
"Boss is gonna die soon. All the above conditions don't really matter. We're just gonna burn our mana until combat ends.");
build->add_talent(this, "Arcane Orb");
build->add_action(this,
"Arcane Missiles",
"if=active_enemies<3&(variable.arcane_missiles_procs=buff.arcane_missiles.max_stack|(variable.arcane_missiles_procs&mana.pct<=50&buff.arcane_charge.stack=3)),chain=1",
"Use Arcane Missiles at max stacks to avoid munching a proc. Alternatively, we can cast at 3 stacks of Arcane Charge to conserve mana.");
build->add_action(this, "Arcane Explosion", "if=active_enemies>1");
build->add_action(this, "Arcane Blast");
burn->add_action("variable,name=total_burns,op=add,value=1,if=!burn_phase",
"Increment our burn phase counter. Whenever we enter the `burn` actions without being in a burn phase, it means that we are about to start one.");
burn->add_action("start_burn_phase,if=!burn_phase",
"The burn_phase variable is a flag indicating whether or not we are in a burn phase. It is set to 1 (True) with start_burn_phase, and 0 (False) with stop_burn_phase.");
burn->add_action("stop_burn_phase,if=prev_gcd.1.evocation&cooldown.evocation.charges=0&burn_phase_duration>0",
"Evocation is the end of our burn phase, but we check available charges in case of Gravity Spiral. The final burn_phase_duration check is to prevent an infinite loop in SimC.");
burn->add_talent(this,
"Nether Tempest",
"if=refreshable|!ticking",
"Use during pandemic refresh window or if the dot is missing.");
burn->add_action(this, "Mark of Aluneth");
burn->add_talent(this, "Mirror Image");
burn->add_action("lights_judgment,if=buff.arcane_power.down");
burn->add_talent(this,
"Rune of Power",
"if=mana.pct>30|(buff.arcane_power.up|cooldown.arcane_power.up)",
"Prevents using RoP at super low mana.");
burn->add_action(this, "Arcane Power");
for (size_t i = 0; i < racial_actions.size(); i++) {
if (racial_actions[i] == "lights_judgment")
continue; // Handled manually.
burn->add_action(racial_actions[i]);
}
burn->add_action("potion,if=buff.arcane_power.up&(buff.berserking.up|buff.blood_fury.up|!(race.troll|race.orc))",
"For Troll/Orc, it's best to sync potion with their racial buffs.");
burn->add_action("use_items,if=buff.arcane_power.up|target.time_to_die<cooldown.arcane_power.remains",
"Pops any on-use items, e.g., Tarnished Sentinel Medallion.");
burn->add_action(this,
"Arcane Barrage",
"if=set_bonus.tier21_2pc&((set_bonus.tier20_2pc&cooldown.presence_of_mind.up)|(talent.charged_up.enabled&cooldown.charged_up.up))&buff.arcane_charge.stack=buff.arcane_charge.max_stack&buff.expanding_mind.down",
"With 2pt20 or Charged Up we are able to extend the damage buff from 2pt21.");
burn->add_action(this,
"Presence of Mind",
"if=((mana.pct>30|buff.arcane_power.up)&set_bonus.tier20_2pc)|buff.rune_of_power.remains<=buff.presence_of_mind.max_stack*action.arcane_blast.execute_time|buff.arcane_power.remains<=buff.presence_of_mind.max_stack*action.arcane_blast.execute_time",
"With T20, use PoM at start of RoP/AP for damage buff. Without T20, use PoM at end of RoP/AP to cram in two final Arcane Blasts. Includes a mana condition to prevent using PoM at super low mana.");
burn->add_talent(this,
"Charged Up",
"if=buff.arcane_charge.stack<buff.arcane_charge.max_stack",
"Use Charged Up to regain Arcane Charges after dumping to refresh 2pt21 buff.");
burn->add_talent(this, "Arcane Orb");
burn->add_action(this,
"Arcane Barrage",
"if=active_enemies>4&equipped.mantle_of_the_first_kirin_tor&buff.arcane_charge.stack=buff.arcane_charge.max_stack",
"Arcane Barrage has a good chance of launching an Arcane Orb at max Arcane Charge stacks.");
burn->add_action(this,
"Arcane Missiles",
"if=variable.arcane_missiles_procs=buff.arcane_missiles.max_stack&active_enemies<3,chain=1",
"Arcane Missiles are good, but not when there's multiple targets up.");
burn->add_action(this,
"Arcane Blast",
"if=buff.presence_of_mind.up",
"Get PoM back on cooldown as soon as possible.");
burn->add_action(this, "Arcane Explosion", "if=active_enemies>1");
burn->add_action(this, "Arcane Missiles", "if=variable.arcane_missiles_procs>1,chain=1");
burn->add_action(this, "Arcane Blast");
burn->add_action(
"variable,name=average_burn_length,op=set,value=(variable.average_burn_length*variable.total_burns-variable.average_burn_length+burn_phase_duration)%variable.total_burns",
"Now that we're done burning, we can update the average_burn_length with the length of this burn.");
burn->add_action(this,
"Evocation",
"interrupt_if=ticks=2|mana.pct>=85,interrupt_immediate=1",
"That last tick of Evocation is a waste; it's better for us to get back to casting.");
conserve->add_talent(this,
"Mirror Image",
"if=variable.time_until_burn>recharge_time|variable.time_until_burn>target.time_to_die");
conserve->add_action(this, "Mark of Aluneth", "if=mana.pct<85");
conserve->add_action(
"strict_sequence,name=miniburn,if=talent.rune_of_power.enabled&set_bonus.tier20_4pc&variable.time_until_burn>30:rune_of_power:arcane_barrage:presence_of_mind");
conserve->add_talent(this,
"Rune of Power",
"if=full_recharge_time<=execute_time|prev_gcd.1.mark_of_aluneth",
"Use if we're about to cap on stacks, or we just used MoA.");
conserve->add_action(
"strict_sequence,name=abarr_cu_combo,if=talent.charged_up.enabled&cooldown.charged_up.recharge_time<variable.time_until_burn:arcane_barrage:charged_up",
"We want Charged Up for our burn phase to refresh 2pt21 buff, but if we have time to let it recharge we can use it during conserve.");
conserve->add_action(this,
"Arcane Missiles",
"if=variable.arcane_missiles_procs=buff.arcane_missiles.max_stack&active_enemies<3,chain=1",
"Arcane Missiles are good, but not when there's multiple targets up.");
conserve->add_talent(this, "Supernova");
conserve->add_talent(this,
"Nether Tempest",
"if=refreshable|!ticking",
"Use during pandemic refresh window or if the dot is missing.");
conserve->add_action(this,
"Arcane Explosion",
"if=active_enemies>1&(mana.pct>=70-(10*equipped.mystic_kilt_of_the_rune_master))",
"AoE until about 70% mana. We can go a little further with kilt, down to 60% mana.");
conserve->add_action(this,
"Arcane Blast",
"if=mana.pct>=90|buff.rhonins_assaulting_armwraps.up|(buff.rune_of_power.remains>=cast_time&equipped.mystic_kilt_of_the_rune_master)",
"Use Arcane Blast if we have the mana for it or a proc from legendary wrists. With the Kilt we can cast freely.");
conserve->add_action(this, "Arcane Missiles", "if=variable.arcane_missiles_procs,chain=1");
conserve->add_action(this, "Arcane Barrage");
conserve->add_action(this,
"Arcane Explosion",
"if=active_enemies>1",
"The following two lines are here in case Arcane Barrage is on cooldown.");
conserve->add_action(this, "Arcane Blast");
}
// Fire Mage Action List ===================================================================================================
void mage_t::apl_fire() {
std::vector<std::string> racial_actions = get_racial_actions();
action_priority_list_t *default_list = get_action_priority_list("default");
action_priority_list_t *combustion_phase = get_action_priority_list("combustion_phase");
action_priority_list_t *rop_phase = get_action_priority_list("rop_phase");
action_priority_list_t *active_talents = get_action_priority_list("active_talents");
action_priority_list_t *standard = get_action_priority_list("standard_rotation");
default_list->add_action(this, "Counterspell", "if=target.debuff.casting.react");
default_list->add_action(this,
"Time Warp",
"if=(time=0&buff.bloodlust.down)|(buff.bloodlust.down&equipped.132410&(cooldown.combustion.remains<1|target.time_to_die<50))");
default_list->add_talent(this, "Mirror Image", "if=buff.combustion.down");
default_list->add_talent(this,
"Rune of Power",
"if=firestarter.active&action.rune_of_power.charges=2|cooldown.combustion.remains>40&buff.combustion.down&!talent.kindling.enabled|target.time_to_die<11|talent.kindling.enabled&(charges_fractional>1.8|time<40)&cooldown.combustion.remains>40",
"Standard Talent RoP Logic.");
default_list->add_talent(this,
"Rune of Power",
"if=(buff.kaelthas_ultimate_ability.react&(cooldown.combustion.remains>40|action.rune_of_power.charges>1))|(buff.erupting_infernal_core.up&(cooldown.combustion.remains>40|action.rune_of_power.charges>1))",
"RoP use while using Legendary Items.");
default_list->add_action(mage_t::get_special_use_items("horn_of_valor", true));
default_list->add_action(mage_t::get_special_use_items("obelisk_of_the_void", true));
default_list->add_action(mage_t::get_special_use_items("mrrgrias_favor"));
default_list->add_action(mage_t::get_special_use_items("pharameres_forbidden_grimoire"));
default_list->add_action(mage_t::get_special_use_items("kiljaedens_burning_wish"));
default_list->add_action(
"call_action_list,name=combustion_phase,if=cooldown.combustion.remains<=action.rune_of_power.cast_time+(!talent.kindling.enabled*gcd)&(!talent.firestarter.enabled|!firestarter.active|active_enemies>=4|active_enemies>=2&talent.flame_patch.enabled)|buff.combustion.up");
default_list->add_action("call_action_list,name=rop_phase,if=buff.rune_of_power.up&buff.combustion.down");
default_list->add_action("call_action_list,name=standard_rotation");
combustion_phase->add_action("lights_judgment,if=buff.combustion.down");
combustion_phase->add_talent(this, "Rune of Power", "if=buff.combustion.down");
combustion_phase->add_action("call_action_list,name=active_talents");
combustion_phase->add_action(this, "Combustion");
combustion_phase->add_action("potion");
for (size_t i = 0; i < racial_actions.size(); i++) {
if (racial_actions[i] == "lights_judgment")
continue; // Handled manually.
combustion_phase->add_action(racial_actions[i]);
}
combustion_phase->add_action("use_items");
combustion_phase->add_action(mage_t::get_special_use_items("obelisk_of_the_void"));
combustion_phase->add_action(this,
"Flamestrike",
"if=(talent.flame_patch.enabled&active_enemies>2|active_enemies>4)&buff.hot_streak.react");
combustion_phase->add_action(this,
"Pyroblast",
"if=buff.kaelthas_ultimate_ability.react&buff.combustion.remains>execute_time");
combustion_phase->add_action(this, "Pyroblast", "if=buff.hot_streak.react");
combustion_phase->add_action(this, "Fire Blast", "if=buff.heating_up.react");
combustion_phase->add_action(this, "Phoenix Flames");
combustion_phase->add_action(this, "Scorch", "if=buff.combustion.remains>cast_time");
combustion_phase->add_action(this,
"Dragon's Breath",
"if=!buff.hot_streak.react&action.fire_blast.charges<1&action.phoenix_flames.charges<1");
combustion_phase->add_action(this, "Scorch", "if=target.health.pct<=30&equipped.132454");
rop_phase->add_talent(this, "Rune of Power");
rop_phase->add_action(this,
"Flamestrike",
"if=((talent.flame_patch.enabled&active_enemies>1)|active_enemies>3)&buff.hot_streak.react");
rop_phase->add_action(this, "Pyroblast", "if=buff.hot_streak.react");
rop_phase->add_action("call_action_list,name=active_talents");
rop_phase->add_action(this,
"Pyroblast",
"if=buff.kaelthas_ultimate_ability.react&execute_time<buff.kaelthas_ultimate_ability.remains&buff.rune_of_power.remains>cast_time");
rop_phase->add_action(this,
"Fire Blast",
"if=!prev_off_gcd.fire_blast&buff.heating_up.react&firestarter.active&charges_fractional>1.7");
rop_phase->add_action(this,
"Phoenix Flames",
"if=!prev_gcd.1.phoenix_flames&charges_fractional>2.7&firestarter.active");
rop_phase->add_action(this, "Fire Blast", "if=!prev_off_gcd.fire_blast&!firestarter.active");
rop_phase->add_action(this, "Phoenix Flames", "if=!prev_gcd.1.phoenix_flames");
rop_phase->add_action(this, "Scorch", "if=target.health.pct<=30&equipped.132454");
rop_phase->add_action(this, "Dragon's Breath", "if=active_enemies>2");
rop_phase->add_action(this, "Flamestrike", "if=(talent.flame_patch.enabled&active_enemies>2)|active_enemies>5");
rop_phase->add_action(this, "Fireball");
active_talents->add_talent(this,
"Blast Wave",
"if=(buff.combustion.down)|(buff.combustion.up&action.fire_blast.charges<1&action.phoenix_flames.charges<1)");
active_talents->add_talent(this,
"Meteor",
"if=cooldown.combustion.remains>40|(cooldown.combustion.remains>target.time_to_die)|buff.rune_of_power.up|firestarter.active");
active_talents->add_talent(this,
"Cinderstorm",
"if=cooldown.combustion.remains<cast_time&(buff.rune_of_power.up|!talent.rune_of_power.enabled)|cooldown.combustion.remains>10*spell_haste&!buff.combustion.up");
active_talents->add_action(this,
"Dragon's Breath",
"if=equipped.132863|(talent.alexstraszas_fury.enabled&!buff.hot_streak.react)");
active_talents->add_talent(this, "Living Bomb", "if=active_enemies>1&buff.combustion.down");
standard->add_action(this,
"Flamestrike",
"if=((talent.flame_patch.enabled&active_enemies>1)|active_enemies>3)&buff.hot_streak.react");
standard->add_action(this,
"Pyroblast",
"if=buff.hot_streak.react&buff.hot_streak.remains<action.fireball.execute_time");
standard->add_action(this, "Pyroblast", "if=buff.hot_streak.react&firestarter.active&!talent.rune_of_power.enabled");
standard->add_action(this, "Phoenix Flames", "if=charges_fractional>2.7&active_enemies>2");
standard->add_action(this,
"Pyroblast",
"if=buff.hot_streak.react&(!prev_gcd.1.pyroblast|action.pyroblast.in_flight)");
standard->add_action(this, "Pyroblast", "if=buff.hot_streak.react&target.health.pct<=30&equipped.132454");
standard->add_action(this,
"Pyroblast",
"if=buff.kaelthas_ultimate_ability.react&execute_time<buff.kaelthas_ultimate_ability.remains");
standard->add_action("call_action_list,name=active_talents");
standard->add_action(this,
"Fire Blast",
"if=!talent.kindling.enabled&buff.heating_up.react&(!talent.rune_of_power.enabled|charges_fractional>1.4|cooldown.combustion.remains<40)&(3-charges_fractional)*(12*spell_haste)<cooldown.combustion.remains+3|target.time_to_die<4");
standard->add_action(this,
"Fire Blast",
"if=talent.kindling.enabled&buff.heating_up.react&(!talent.rune_of_power.enabled|charges_fractional>1.5|cooldown.combustion.remains<40)&(3-charges_fractional)*(18*spell_haste)<cooldown.combustion.remains+3|target.time_to_die<4");
standard->add_action(this,
"Phoenix Flames",
"if=(buff.combustion.up|buff.rune_of_power.up|buff.incanters_flow.stack>3|talent.mirror_image.enabled)&(4-charges_fractional)*13<cooldown.combustion.remains+5|target.time_to_die<10");
standard->add_action(this,
"Phoenix Flames",
"if=(buff.combustion.up|buff.rune_of_power.up)&(4-charges_fractional)*30<cooldown.combustion.remains+5");
standard->add_action(this, "Phoenix Flames", "if=charges_fractional>2.5&cooldown.combustion.remains>23");
standard->add_action(this, "Flamestrike", "if=(talent.flame_patch.enabled&active_enemies>3)|active_enemies>5");
standard->add_action(this, "Scorch", "if=target.health.pct<=30&equipped.132454");
standard->add_action(this, "Fireball");
standard->add_action(this, "Scorch");
}
// Frost Mage Action List ==============================================================================================================
void mage_t::apl_frost() {
std::vector<std::string> racial_actions = get_racial_actions();
action_priority_list_t *default_list = get_action_priority_list("default");
action_priority_list_t *single = get_action_priority_list("single");
action_priority_list_t *aoe = get_action_priority_list("aoe");
action_priority_list_t *cooldowns = get_action_priority_list("cooldowns");
action_priority_list_t *movement = get_action_priority_list("movement");
default_list->add_action(this, "Counterspell");
default_list->add_action(this,
"Ice Lance",
"if=!buff.fingers_of_frost.react&prev_gcd.1.flurry",
"Free Ice Lance after Flurry. This action has rather high priority to ensure that we don't cast Rune of Power, Ray of Frost, "
"etc. after Flurry and break up the combo. If FoF was already active, we do not lose anything by delaying the Ice Lance.");
default_list->add_action(this,
"Time Warp",
"if=buff.bloodlust.down&(buff.exhaustion.down|equipped.shard_of_the_exodar)&(cooldown.icy_veins.remains<1|target.time_to_die<50)",
"Time Warp is used right at the start. If the actor has Shard of the Exodar, try to synchronize the second Time Warp with "
"Icy Veins. If the target is about to die, use Time Warp regardless.");
default_list->add_action(mage_t::get_special_use_items("horn_of_valor"));
default_list->add_action(mage_t::get_special_use_items("obelisk_of_the_void"));
default_list->add_action(mage_t::get_special_use_items("mrrgrias_favor"));
default_list->add_action(mage_t::get_special_use_items("pharameres_forbidden_grimoire"));
default_list->add_action(mage_t::get_special_use_items("kiljaedens_burning_wish"));
default_list->add_action("call_action_list,name=cooldowns");
default_list->add_action("call_action_list,name=aoe,if=active_enemies>=3");
default_list->add_action("call_action_list,name=single");
single->add_talent(this,
"Ice Nova",
"if=debuff.winters_chill.up",
"In some circumstances, it is possible for both Ice Lance and Ice Nova to benefit from a single Winter's Chill.");
single->add_action(this, "Frostbolt", "if=prev_off_gcd.water_jet");
single->add_action("water_jet,if=prev_gcd.1.frostbolt&buff.fingers_of_frost.stack<3&!buff.brain_freeze.react",
"Basic Water Jet combo. Since Water Jet can only be used if the actor is not casting, we use it right after Frostbolt is executed. "
"At the default distance, Frostbolt travels slightly over 1 s, giving Water Jet enough time to apply the DoT (Water Jet's cast time "
"is 1 s, with haste scaling). The APL then forces another Frostbolt to guarantee getting both FoFs from the Water Jet. This works for "
"most haste values (roughly from 0% to 160%). When changing the default distance, great care must be taken otherwise this action "
"won't produce two FoFs.");
single->add_talent(this,
"Ray of Frost",
"if=buff.icy_veins.up|cooldown.icy_veins.remains>action.ray_of_frost.cooldown&buff.rune_of_power.down");
single->add_action(this,
"Flurry",
"if=prev_gcd.1.ebonbolt|buff.brain_freeze.react&(prev_gcd.1.glacial_spike|prev_gcd.1.frostbolt&(!talent.glacial_spike.enabled"
"|buff.icicles.stack<=4|cooldown.frozen_orb.remains<=10&set_bonus.tier20_2pc))",
"Winter's Chill from Flurry can apply to the spell cast right before (provided the travel time is long enough). This can be "
"exploited to a great effect with Ebonbolt, Glacial Spike (which deal a lot of damage by themselves) and Frostbolt (as a "
"guaranteed way to proc Frozen Veins and Chain Reaction). When using Glacial Spike, it is worth saving a Brain Freeze proc "
"when Glacial Spike is right around the corner (i.e. with 5 Icicles). However, when the actor also has T20 2pc, "
"Glacial Spike is delayed to fit into Frozen Mass, so we do not want to sit on a Brain Freeze proc for too long in that case.");
single->add_action(this, "Frozen Orb", "if=set_bonus.tier20_2pc&buff.fingers_of_frost.react<3",
"With T20 2pc, Frozen Orb should be used as soon as it comes off CD.");
single->add_action(this,
"Blizzard",
"if=cast_time=0&active_enemies>1&buff.fingers_of_frost.react<3",
"Freezing Rain Blizzard. While the normal Blizzard action is usually enough, right after Frozen Orb the actor will be "
"getting a lot of FoFs, which might delay Blizzard to the point where we miss out on Freezing Rain. Therefore, if we are "
"not at a risk of overcapping on FoF, use Blizzard before using Ice Lance.");
single->add_talent(this,
"Frost Bomb",
"if=debuff.frost_bomb.remains<action.ice_lance.travel_time&buff.fingers_of_frost.react");
single->add_action(this, "Ice Lance", "if=buff.fingers_of_frost.react");
single->add_action(this, "Ebonbolt");
single->add_action(this, "Frozen Orb");
single->add_talent(this, "Ice Nova");
single->add_talent(this, "Comet Storm");
single->add_action(this,
"Blizzard",
"if=active_enemies>1|buff.zannesu_journey.stack=5&buff.zannesu_journey.remains>cast_time",
"Against low number of targets, Blizzard is used as a filler. Zann'esu buffed Blizzard is used only at 5 stacks.");
single->add_action(this,
"Frostbolt",
"if=buff.frozen_mass.remains>execute_time+action.glacial_spike.execute_time+action.glacial_spike.travel_time&!buff.brain_freeze.react&"
"talent.glacial_spike.enabled",
"While Frozen Mass is active, we want to fish for Brain Freeze for the next Glacial Spike. Stop when Frozen Mass is about to run out "
"and we wouldn't be able to cast Glacial Spike in time.");
single->add_talent(this,
"Glacial Spike",
"if=cooldown.frozen_orb.remains>10|!set_bonus.tier20_2pc",
"Glacial Spike is generally used as it is available, unless we have T20 2pc. In that case, Glacial Spike is delayed when "
"Frozen Mass is happening soon (in less than 10 s).");
single->add_action(this, "Frostbolt");
single->add_action("call_action_list,name=movement");
single->add_action(this, "Blizzard", "",
"While on the move, use instant Blizzard if available.");
single->add_action(this, "Ice Lance", "",
"Otherwise just use Ice Lance to do at least some damage.");
aoe->add_action(this, "Frostbolt", "if=prev_off_gcd.water_jet");
aoe->add_action(this,
"Frozen Orb",
"",
"Make sure Frozen Orb is used before Blizzard if both are available. This is a small gain with Freezing Rain "
"and on par without.");
aoe->add_action(this, "Blizzard");
aoe->add_talent(this, "Comet Storm");
aoe->add_talent(this, "Ice Nova");
aoe->add_action("water_jet,if=prev_gcd.1.frostbolt&buff.fingers_of_frost.stack<3&!buff.brain_freeze.react");
aoe->add_action(this,
"Flurry",
"if=prev_gcd.1.ebonbolt|buff.brain_freeze.react&(prev_gcd.1.glacial_spike|prev_gcd.1.frostbolt)");
aoe->add_talent(this,
"Frost Bomb",
"if=debuff.frost_bomb.remains<action.ice_lance.travel_time&buff.fingers_of_frost.react");
aoe->add_action(this, "Ice Lance", "if=buff.fingers_of_frost.react");
aoe->add_action(this, "Ebonbolt");
aoe->add_talent(this, "Glacial Spike");
aoe->add_action(this, "Frostbolt");
aoe->add_action("call_action_list,name=movement");
aoe->add_action(this, "Cone of Cold");
aoe->add_action(this, "Ice Lance");
cooldowns->add_talent(this,
"Rune of Power",
"if=cooldown.icy_veins.remains<cast_time|charges_fractional>1.9&cooldown.icy_veins.remains>10|buff.icy_veins.up|"
"target.time_to_die+5<charges_fractional*10",
"Rune of Power is used when going into Icy Veins and while Icy Veins are up. Outside of Icy Veins, use Rune of Power "
"when about to cap on charges or the target is about to die.");
cooldowns->add_action("potion,if=cooldown.icy_veins.remains<1|target.time_to_die<70");
cooldowns->add_action(this, "Icy Veins");
cooldowns->add_talent(this, "Mirror Image");
cooldowns->add_action("use_items");
for (size_t i = 0; i < racial_actions.size(); i++) {
cooldowns->add_action(racial_actions[i]);
}
movement->add_action(this, "Blink", "if=movement.distance>10");
movement->add_talent(this, "Ice Floes", "if=buff.ice_floes.down");
}
// Default Action List ========================================================
void mage_t::apl_default() {
action_priority_list_t *default_list = get_action_priority_list("default");
default_list->add_action("Frostbolt");
}
// mage_t::mana_regen_per_second ==============================================
double mage_t::mana_regen_per_second() const {
double mps = player_t::mana_regen_per_second();
if (spec.savant->ok()) {
mps *= 1.0 + cache.mastery() * spec.savant->effectN(1).mastery_value();
}
return mps;
}
// mage_t::invalidate_cache ===================================================
void mage_t::invalidate_cache(cache_e c) {
player_t::invalidate_cache(c);
switch (c) {
case CACHE_MASTERY:
if (spec.savant->ok()) {
recalculate_resource_max(RESOURCE_MANA);
}
break;
case CACHE_SPELL_CRIT_CHANCE:
// Combustion makes mastery dependent on spell crit chance rating. Thus
// any spell_crit_chance invalidation (which should include any
// spell_crit_rating changes) will also invalidate mastery.
if (specialization() == MAGE_FIRE) {
invalidate_cache(CACHE_MASTERY);
}
break;
default:break;
}
}
// mage_t::recalculate_resource_max ===========================================
void mage_t::recalculate_resource_max(resource_e rt) {
if (rt != RESOURCE_MANA) {
return player_t::recalculate_resource_max(rt);
}
double current_mana = resources.current[rt];
double current_mana_max = resources.max[rt];
double mana_percent = current_mana / current_mana_max;
player_t::recalculate_resource_max(rt);
if (spec.savant->ok()) {
resources.max[rt] *= 1.0 + cache.mastery() * spec.savant->effectN(1).mastery_value();
resources.current[rt] = resources.max[rt] * mana_percent;
if (sim->debug) {
sim->out_debug.printf(
"%s Savant adjusts mana from %.0f/%.0f to %.0f/%.0f",
name(), current_mana, current_mana_max,
resources.current[rt], resources.max[rt]);
}
current_mana = resources.current[rt];
current_mana_max = resources.max[rt];
}
if (talents.arcane_familiar->ok() && buffs.arcane_familiar->check()) {
resources.max[rt] *= 1.0 + buffs.arcane_familiar->check_value();
resources.current[rt] = resources.max[rt] * mana_percent;
if (sim->debug) {
sim->out_debug.printf(
"%s Arcane Familiar adjusts mana from %.0f/%.0f to %.0f/%.0f",
name(), current_mana, current_mana_max,
resources.current[rt], resources.max[rt]);
}
}
}
// mage_t::composite_player_critical_damage_multiplier ===================
double mage_t::composite_player_critical_damage_multiplier(const action_state_t *s) const {
double m = player_t::composite_player_critical_damage_multiplier(s);
m *= 1.0 + buffs.inferno->check_value();
m *= 1.0 + buffs.frozen_mass->check_value();
return m;
}
// mage_t::composite_player_pet_damage_multiplier ============================
double mage_t::composite_player_pet_damage_multiplier(const action_state_t *s) const {
double m = player_t::composite_player_pet_damage_multiplier(s);
if (bugs) {
m *= 1.0 + buffs.rune_of_power->check_value();
m *= 1.0 + buffs.incanters_flow->check_stack_value();
}
return m;
}
// mage_t::composite_player_multiplier =======================================
double mage_t::composite_player_multiplier(school_e school) const {
double m = player_t::composite_player_multiplier(school);
m *= 1.0 + buffs.expanding_mind->check_value();
return m;
}
// mage_t::composite_mastery_rating =============================================
double mage_t::composite_mastery_rating() const {
double m = player_t::composite_mastery_rating();
if (buffs.combustion->check()) {
m += composite_spell_crit_rating() * buffs.combustion->data().effectN(3).percent();
}
return m;
}
// mage_t::composite_spell_crit_rating ===============================================
double mage_t::composite_spell_crit_rating() const {
double cr = player_t::composite_spell_crit_rating();
if (spec.critical_mass->ok()) {
cr *= 1.0 + spec.critical_mass_2->effectN(1).percent();
}
return cr;
}
// mage_t::composite_spell_crit_chance ===============================================
double mage_t::composite_spell_crit_chance() const {
double c = player_t::composite_spell_crit_chance();
if (spec.critical_mass->ok()) {
c += spec.critical_mass->effectN(1).percent();
}
return c;
}
// mage_t::composite_spell_haste ==============================================
double mage_t::composite_spell_haste() const {
double h = player_t::composite_spell_haste();
h /= 1.0 + buffs.icy_veins->check_value();
h /= 1.0 + buffs.streaking->check_value();
h /= 1.0 + buffs.quick_thinker->check_value();
h /= 1.0 + buffs.sephuzs_secret->check_value();
if (buffs.sephuzs_secret->default_chance != 0.0) {
h /= 1.0 + buffs.sephuzs_secret->data().driver()->effectN(3).percent();
}
return h;
}
// mage_t::matching_gear_multiplier =========================================
double mage_t::matching_gear_multiplier(attribute_e attr) const {
if (attr == ATTR_INTELLECT)
return 0.05;
return 0.0;
}
// mage_t::reset ============================================================
void mage_t::reset() {
player_t::reset();
icicles.clear();
event_t::cancel(icicle_event);
event_t::cancel(ignite_spread_event);
if (spec.savant->ok()) {
recalculate_resource_max(RESOURCE_MANA);
}
last_bomb_target = nullptr;
ground_aoe_expiration.clear();
burn_phase.reset();
range::for_each(proc_source_list, std::mem_fn(&proc_source_t::reset));
}
// mage_t::stun =============================================================
void mage_t::stun() {
// FIX ME: override this to handle Blink
player_t::stun();
}
// mage_t::update_movement==================================================
void mage_t::update_movement(timespan_t duration) {
player_t::update_movement(duration);
double yards = duration.total_seconds() * composite_movement_speed();
distance_from_rune += yards;
if (buffs.rune_of_power->check()) {
if (distance_from_rune > talents.rune_of_power->effectN(2).radius()) {
buffs.rune_of_power->expire();
if (sim->debug)
sim->out_debug.printf("%s lost Rune of Power due to moving more than 8 yards away from it.", name());
}
}
}
// mage_t::temporary_movement_modifier ==================================
double mage_t::temporary_movement_modifier() const {
double tmm = player_t::temporary_movement_modifier();
if (buffs.sephuzs_secret->check()) {
tmm = std::max(buffs.sephuzs_secret->data().effectN(1).percent(), tmm);
}
return tmm;
}
// mage_t::passive_movement_modifier ====================================
double mage_t::passive_movement_modifier() const {
double pmm = player_t::passive_movement_modifier();
if (buffs.sephuzs_secret->default_chance != 0.0) {
pmm += buffs.sephuzs_secret->data().driver()->effectN(2).percent();
}
pmm += buffs.chrono_shift->check_value();
pmm += buffs.frenetic_speed->check_value();
return pmm;
}
// mage_t::arise ============================================================
void mage_t::arise() {
player_t::arise();
if (talents.incanters_flow->ok())
buffs.incanters_flow->trigger();
if (blessing_of_wisdom_count > 0) {
buffs.greater_blessing_of_widsom->trigger();
}
if (spec.ignite->ok()) {
timespan_t first_spread = timespan_t::from_seconds(rng().real() * 2.0);
ignite_spread_event = make_event<events::ignite_spread_event_t>(*sim, *this, first_spread);
}
}
void mage_t::combat_begin() {
player_t::combat_begin();
if (specialization() == MAGE_ARCANE) {
uptime.burn_phase->update(false, sim->current_time());
uptime.conserve_phase->update(true, sim->current_time());
}
}
void mage_t::combat_end() {
player_t::combat_end();
if (specialization() == MAGE_ARCANE) {
uptime.burn_phase->update(false, sim->current_time());
uptime.conserve_phase->update(false, sim->current_time());
}
}
// mage_t::create_expression ================================================
expr_t *mage_t::create_expression(action_t *a, const std::string &name_str) {
struct mage_expr_t : public expr_t {
mage_t &mage;
mage_expr_t(const std::string &n, mage_t &m) :
expr_t(n), mage(m) {}
};
// Incanters flow direction
// Evaluates to: 0.0 if IF talent not chosen or IF stack unchanged
// 1.0 if next IF stack increases
// -1.0 if IF stack decreases
if (name_str == "incanters_flow_dir") {
struct incanters_flow_dir_expr_t : public mage_expr_t {
incanters_flow_dir_expr_t(mage_t &m) :
mage_expr_t("incanters_flow_dir", m) {}
virtual double evaluate() override {
if (!mage.talents.incanters_flow->ok())
return 0.0;
buff_t *flow = mage.buffs.incanters_flow;
if (flow->reverse)
return flow->check() == 1 ? 0.0 : -1.0;
else
return flow->check() == 5 ? 0.0 : 1.0;
}
};
return new incanters_flow_dir_expr_t(*this);
}
// Arcane Burn Flag Expression ==============================================
if (name_str == "burn_phase") {
struct burn_phase_expr_t : public mage_expr_t {
burn_phase_expr_t(mage_t &m) :
mage_expr_t("burn_phase", m) {}
virtual double evaluate() override {
return mage.burn_phase.on();
}
};
return new burn_phase_expr_t(*this);
}
if (name_str == "burn_phase_duration") {
struct burn_phase_duration_expr_t : public mage_expr_t {
burn_phase_duration_expr_t(mage_t &m) :
mage_expr_t("burn_phase_duration", m) {}
virtual double evaluate() override {
return mage.burn_phase.duration(mage.sim->current_time())
.total_seconds();
}
};
return new burn_phase_duration_expr_t(*this);
}
// Icicle Expressions =======================================================
if (util::str_compare_ci(name_str, "shooting_icicles")) {
struct sicicles_expr_t : public mage_expr_t {
sicicles_expr_t(mage_t &m) : mage_expr_t("shooting_icicles", m) {}
virtual double evaluate() override { return mage.icicle_event != nullptr; }
};
return new sicicles_expr_t(*this);
}
if (util::str_compare_ci(name_str, "icicles")) {
struct icicles_expr_t : public mage_expr_t {
icicles_expr_t(mage_t &m) : mage_expr_t("icicles", m) {}
virtual double evaluate() override {
if (mage.icicles.empty())
return 0;
else if (mage.sim->current_time() - mage.icicles[0].timestamp < mage.spec.icicles_driver->duration())
return as<double>(mage.icicles.size());
else {
size_t icicles = 0;
for (int i = as<int>(mage.icicles.size() - 1); i >= 0; i--) {
if (mage.sim->current_time() - mage.icicles[i].timestamp >= mage.spec.icicles_driver->duration())
break;
icicles++;
}
return as<double>(icicles);
}
}
};
return new icicles_expr_t(*this);
}
std::vector<std::string> splits = util::string_split(name_str, ".");
// Firestarter expressions ==================================================
if (splits.size() == 2 && util::str_compare_ci(splits[0], "firestarter")) {
enum expr_type_e {
FIRESTARTER_ACTIVE,
FIRESTARTER_REMAINS
};
struct firestarter_expr_t : public mage_expr_t {
action_t *a;
expr_type_e type;
firestarter_expr_t(mage_t &m, const std::string &name, action_t *a, expr_type_e type) :
mage_expr_t(name, m), a(a), type(type) {}
virtual double evaluate() override {
if (!mage.talents.firestarter->ok())
return 0.0;
timespan_t remains;
if (mage.firestarter_time > timespan_t::zero()) {
remains = std::max(timespan_t::zero(), mage.firestarter_time - mage.sim->current_time());
} else {
remains = a->target->time_to_percent(mage.talents.firestarter->effectN(1).base_value());
}
switch (type) {
case FIRESTARTER_ACTIVE:return static_cast<double>( remains > timespan_t::zero());
case FIRESTARTER_REMAINS:return remains.total_seconds();
default:return 0.0;
}
}
};
if (util::str_compare_ci(splits[1], "active")) {
return new firestarter_expr_t(*this, name_str, a, FIRESTARTER_ACTIVE);
} else if (util::str_compare_ci(splits[1], "remains")) {
return new firestarter_expr_t(*this, name_str, a, FIRESTARTER_REMAINS);
} else {
sim->errorf("Player %s firestarer expression: unknown operation '%s'", name(), splits[1].c_str());
}
}
// Ground AoE expressions ===================================================
if (splits.size() == 3 && util::str_compare_ci(splits[0], "ground_aoe")) {
struct ground_aoe_expr_t : public mage_expr_t {
std::string aoe_type;
ground_aoe_expr_t(mage_t &m, const std::string &name_str, const std::string &aoe) :
mage_expr_t(name_str, m),
aoe_type(aoe) {
util::tolower(aoe_type);
}
virtual double evaluate() override {
timespan_t expiration;
auto it = mage.ground_aoe_expiration.find(aoe_type);
if (it != mage.ground_aoe_expiration.end()) {
expiration = it->second;
}
return std::max(0.0, (expiration - mage.sim->current_time()).total_seconds());
}
};
if (util::str_compare_ci(splits[2], "remains")) {
return new ground_aoe_expr_t(*this, name_str, splits[1]);
} else {
sim->errorf("Player %s ground_aoe expression: unknown operation '%s'", name(), splits[2].c_str());
}
}
return player_t::create_expression(a, name_str);
}
// mage_t::convert_hybrid_stat ==============================================
stat_e mage_t::convert_hybrid_stat(stat_e s) const {
// this converts hybrid stats that either morph based on spec or only work
// for certain specs into the appropriate "basic" stats
switch (s) {
case STAT_STR_AGI_INT:
case STAT_AGI_INT:
case STAT_STR_INT:return STAT_INTELLECT;
case STAT_STR_AGI:return STAT_NONE;
case STAT_SPIRIT:return STAT_NONE;
case STAT_BONUS_ARMOR:return STAT_NONE;
default: return s;
}
}
// mage_t::get_icicle =======================================================
double mage_t::get_icicle() {
if (icicles.empty())
return 0.0;
// All Icicles created before the treshold timed out.
timespan_t threshold = sim->current_time() - spec.icicles_driver->duration();
// Find first icicle which did not time out
auto idx = range::find_if(icicles, [threshold](const icicle_tuple_t &t) { return t.timestamp > threshold; });
// Remove all timed out icicles
icicles.erase(icicles.begin(), idx);
if (!icicles.empty()) {
double damage = icicles.front().damage;
icicles.erase(icicles.begin());
return damage;
}
return 0.0;
}
void mage_t::trigger_icicle(const action_state_t *trigger_state, bool chain, player_t *chain_target) {
if (!spec.icicles->ok())
return;
if (icicles.empty())
return;
player_t *icicle_target;
if (chain_target) {
icicle_target = chain_target;
} else {
icicle_target = trigger_state->target;
}
if (chain && !icicle_event) {
double damage = get_icicle();
if (damage == 0.0)
return;
assert(icicle_target);
icicle_event = make_event<events::icicle_event_t>(*sim, *this, damage, icicle_target, true);
if (sim->debug) {
sim->out_debug.printf("%s icicle use on %s%s, damage=%f, total=%u",
name(), icicle_target->name(),
chain ? " (chained)" : "", damage,
as<unsigned>(icicles.size()));
}
} else if (!chain) {
double damage = get_icicle();
if (damage == 0.0)
return;
icicle->set_target(icicle_target);
icicle->base_dd_min = damage;
icicle->base_dd_max = damage;
icicle->execute();
if (sim->debug) {
sim->out_debug.printf("%s icicle use on %s%s, damage=%f, total=%u",
name(), icicle_target->name(),
chain ? " (chained)" : "", damage,
as<unsigned>(icicles.size()));
}
}
}
/* Report Extension Class
* Here you can define class specific report extensions/overrides
*/
class mage_report_t : public player_report_extension_t {
public:
mage_report_t(mage_t &player) :
p(player) {}
void html_customsection_cd_waste(report::sc_html_stream &os) {
if (p.cooldown_waste_data_list.empty())
return;
os << "<div class=\"player-section custom_section\">\n"
<< "<h3 class=\"toggle open\">Cooldown waste</h3>\n"
<< "<div class=\"toggle-content\">\n";
os << "<table class=\"sc\" style=\"margin-top: 5px;\">\n"
<< "<tr>\n"
<< "<th></th>\n"
<< "<th colspan=\"3\">Seconds per Execute</th>\n"
<< "<th colspan=\"3\">Seconds per Iteration</th>\n"
<< "</tr>\n"
<< "<tr>\n"
<< "<th>Ability</th>\n"
<< "<th>Average</th>\n"
<< "<th>Minimum</th>\n"
<< "<th>Maximum</th>\n"
<< "<th>Average</th>\n"
<< "<th>Minimum</th>\n"
<< "<th>Maximum</th>\n"
<< "</tr>\n";
size_t row = 0;
for (const cooldown_waste_data_t *data : p.cooldown_waste_data_list) {
if (!data->active())
continue;
std::string name = data->cd->name_str;
if (action_t *a = p.find_action(name)) {
name = report::action_decorator_t(a).decorate();
}
std::string row_class;
if (++row & 1)
row_class = " class=\"odd\"";
os.format("<tr%s>", row_class.c_str());
os << "<td class=\"left\">" << name << "</td>";
os.format("<td class=\"right\">%.3f</td>", data->normal.mean());
os.format("<td class=\"right\">%.3f</td>", data->normal.min());
os.format("<td class=\"right\">%.3f</td>", data->normal.max());
os.format("<td class=\"right\">%.3f</td>", data->cumulative.mean());
os.format("<td class=\"right\">%.3f</td>", data->cumulative.min());
os.format("<td class=\"right\">%.3f</td>", data->cumulative.max());
os << "</tr>\n";
}
os << "</table>\n";
os << "</div>\n"
<< "</div>\n";
}
void html_customsection_burn_phases(report::sc_html_stream &os) {
os << "<div class=\"player-section custom_section\">\n"
<< "<h3 class=\"toggle open\">Burn Phases</h3>\n"
<< "<div class=\"toggle-content\">\n";
os
<< "<p>Burn phase duration tracks the amount of time spent in each burn phase. This is defined as the time between a "
<< "start_burn_phase and stop_burn_phase action being executed. Note that \"execute\" burn phases, i.e., the "
<< "final burn of a fight, is also included.</p>\n";
os << "<div style=\"display: flex;\">\n"
<< "<table class=\"sc\" style=\"margin-top: 5px;\">\n"
<< "<thead>\n"
<< "<tr>\n"
<< "<th>Burn Phase Duration</th>\n"
<< "</tr>\n"
<< "<tbody>\n";
os.format("<tr><td class=\"left\">Count</td><td>%d</td></tr>\n", p.sample_data.burn_duration_history->count());
os.format("<tr><td class=\"left\">Minimum</td><td>%.3f</td></tr>\n", p.sample_data.burn_duration_history->min());
os.format("<tr><td class=\"left\">5<sup>th</sup> percentile</td><td>%.3f</td></tr>\n",
p.sample_data.burn_duration_history->percentile(0.05));
os.format("<tr><td class=\"left\">Mean</td><td>%.3f</td></tr>\n", p.sample_data.burn_duration_history->mean());
os.format("<tr><td class=\"left\">95<sup>th</sup> percentile</td><td>%.3f</td></tr>\n",
p.sample_data.burn_duration_history->percentile(0.95));
os.format("<tr><td class=\"left\">Max</td><td>%.3f</td></tr>\n", p.sample_data.burn_duration_history->max());
os.format("<tr><td class=\"left\">Variance</td><td>%.3f</td></tr>\n",
p.sample_data.burn_duration_history->variance);
os.format("<tr><td class=\"left\">Mean Variance</td><td>%.3f</td></tr>\n",
p.sample_data.burn_duration_history->mean_variance);
os.format("<tr><td class=\"left\">Mean Std. Dev</td><td>%.3f</td></tr>\n",
p.sample_data.burn_duration_history->mean_std_dev);
os << "</tbody>\n"
<< "</table>\n";
highchart::histogram_chart_t burn_duration_history_chart(highchart::build_id(p, "burn_duration_history"), *p.sim);
if (chart::generate_distribution(
burn_duration_history_chart, &p, p.sample_data.burn_duration_history->distribution, "Burn Duration",
p.sample_data.burn_duration_history->mean(),
p.sample_data.burn_duration_history->min(),
p.sample_data.burn_duration_history->max())) {
burn_duration_history_chart.set("tooltip.headerFormat", "<b>{point.key}</b> s<br/>");
burn_duration_history_chart.set("chart.width", "575");
os << burn_duration_history_chart.to_target_div();
p.sim->add_chart_data(burn_duration_history_chart);
}
os << "</div>\n";
os
<< "<p>Mana at burn start is the mana level recorded (in percentage of total mana) when a start_burn_phase command is executed.</p>\n";
os << "<table class=\"sc\">\n"
<< "<thead>\n"
<< "<tr>\n"
<< "<th>Mana at Burn Start</th>\n"
<< "</tr>\n"
<< "<tbody>\n";
os.format("<tr><td class=\"left\">Count</td><td>%d</td></tr>\n", p.sample_data.burn_initial_mana->count());
os.format("<tr><td class=\"left\">Minimum</td><td>%.3f</td></tr>\n", p.sample_data.burn_initial_mana->min());
os.format("<tr><td class=\"left\">5<sup>th</sup> percentile</td><td>%.3f</td></tr>\n",
p.sample_data.burn_initial_mana->percentile(0.05));
os.format("<tr><td class=\"left\">Mean</td><td>%.3f</td></tr>\n", p.sample_data.burn_initial_mana->mean());
os.format("<tr><td class=\"left\">95<sup>th</sup> percentile</td><td>%.3f</td></tr>\n",
p.sample_data.burn_initial_mana->percentile(0.95));
os.format("<tr><td class=\"left\">Max</td><td>%.3f</td></tr>\n", p.sample_data.burn_initial_mana->max());
os.format("<tr><td class=\"left\">Variance</td><td>%.3f</td></tr>\n", p.sample_data.burn_initial_mana->variance);
os.format("<tr><td class=\"left\">Mean Variance</td><td>%.3f</td></tr>\n",
p.sample_data.burn_initial_mana->mean_variance);
os.format("<tr><td class=\"left\">Mean Std. Dev</td><td>%.3f</td></tr>\n",
p.sample_data.burn_initial_mana->mean_std_dev);
os << "</tbody>\n"
<< "</table>\n";
os << "</div>\n"
<< "</div>\n";
}
void html_customsection_icy_veins(report::sc_html_stream &os) {
os << "<div class=\"player-section custom_section\">\n"
<< "<h3 class=\"toggle open\">Icy Veins</h3>\n"
<< "<div class=\"toggle-content\">\n";
int num_buckets = as<int>(p.sample_data.icy_veins_duration->max() - p.sample_data.icy_veins_duration->min()) + 1;
highchart::histogram_chart_t chart(highchart::build_id(p, "iv_dist"), *p.sim);
p.sample_data.icy_veins_duration->create_histogram(num_buckets);
if (chart::generate_distribution(
chart, &p, p.sample_data.icy_veins_duration->distribution, "Icy Veins Duration",
p.sample_data.icy_veins_duration->mean(), p.sample_data.icy_veins_duration->min(),
p.sample_data.icy_veins_duration->max())) {
chart.set("tooltip.headerFormat", "<b>{point.key}</b> s<br/>");
chart.set("chart.width", std::to_string(80 + num_buckets * 13));
os << chart.to_target_div();
p.sim->add_chart_data(chart);
}
os << "</div>\n"
<< "</div>\n";
}
void html_customsection_shatter(report::sc_html_stream &os) {
if (p.proc_source_list.empty())
return;
os << "<div class=\"player-section custom_section\">\n"
<< "<h3 class=\"toggle open\">Shatter</h3>\n"
<< "<div class=\"toggle-content\">\n";
os << "<table class=\"sc\" style=\"margin-top: 5px;\">\n"
<< "<tr>\n"
<< "<th>Ability</th>\n"
<< "<th>Winter's Chill (utilization)</th>\n"
<< "<th>Fingers of Frost</th>\n"
<< "<th>Other effects</th>\n"
<< "</tr>\n";
double bff = p.procs.brain_freeze_flurry->count.pretty_mean();
size_t row = 0;
for (const proc_source_t *data : p.proc_source_list) {
if (!data->active())
continue;
const std::vector<std::string> splits = util::string_split(data->name_str, "/");
if (splits.size() != 2 || splits[0] != "Shatter")
continue;
std::string name = splits[1];
if (action_t *a = p.find_action(name)) {
name = report::action_decorator_t(a).decorate();
}
std::string row_class;
if (++row & 1)
row_class = " class=\"odd\"";
os.format("<tr%s>", row_class.c_str());
auto format_cell = [bff, &os](double mean, bool wc_util) {
std::string format_str;
format_str += "<td class=\"right\">";
if (mean > 0.0)
format_str += "%.1f";
if (mean > 0.0 && wc_util)
format_str += " (%.1f%%)";
format_str += "</td>";
os.format(format_str.c_str(), mean, bff ? 100.0 * mean / bff : 0.0);
};
assert(data->procs.size() == actions::mage_spell_t::FROZEN_MAX);
os << "<td class=\"left\">" << name << "</td>";
format_cell(data->get(actions::mage_spell_t::FROZEN_WINTERS_CHILL).count.pretty_mean(), true);
format_cell(data->get(actions::mage_spell_t::FROZEN_FINGERS_OF_FROST).count.pretty_mean(), false);
format_cell(data->get(actions::mage_spell_t::FROZEN_ROOT).count.pretty_mean(), false);
os << "</tr>\n";
}
os << "</table>\n";
os << "</div>\n"
<< "</div>\n";
}
virtual void html_customsection(report::sc_html_stream &os) override {
if (p.sim->report_details == 0)
return;
html_customsection_cd_waste(os);
switch (p.specialization()) {
case MAGE_ARCANE:html_customsection_burn_phases(os);
break;
case MAGE_FIRE:break;
case MAGE_FROST:html_customsection_shatter(os);
if (p.talents.thermal_void->ok()) {
html_customsection_icy_veins(os);
}
break;
default:break;
}
}
private:
mage_t &p;
};
// Custom Gear ==============================================================
using namespace unique_gear;
using namespace actions;
// Legion Mage JC Neck
struct sorcerous_fireball_t : public spell_t {
sorcerous_fireball_t(const special_effect_t &effect) :
spell_t("sorcerous_fireball", effect.player, effect.player->find_spell(222305)) {
background = true;
may_crit = true;
hasted_ticks = false;
base_dd_min = base_dd_max = data().effectN(1).average(effect.item);
base_td = data().effectN(2).average(effect.item);
}
virtual double composite_crit_chance() const override { return 0.1; }
virtual double composite_crit_chance_multiplier() const override { return 1.0; }
};
struct sorcerous_frostbolt_t : public spell_t {
sorcerous_frostbolt_t(const special_effect_t &effect) :
spell_t("sorcerous_frostbolt", effect.player, effect.player->find_spell(222320)) {
background = true;
may_crit = true;
base_dd_min = base_dd_max = data().effectN(1).average(effect.item);
}
virtual double composite_crit_chance() const override { return 0.1; }
virtual double composite_crit_chance_multiplier() const override { return 1.0; }
};
struct sorcerous_arcane_blast_t : public spell_t {
sorcerous_arcane_blast_t(const special_effect_t &effect) :
spell_t("sorcerous_arcane_blast", effect.player, effect.player->find_spell(222321)) {
background = true;
may_crit = true;
base_dd_min = base_dd_max = data().effectN(1).average(effect.item);
}
virtual double composite_crit_chance() const override { return 0.1; }
virtual double composite_crit_chance_multiplier() const override { return 1.0; }
};
struct sorcerous_shadowruby_pendant_driver_t : public spell_t {
std::array<spell_t *, 3> sorcerous_spells;
sorcerous_shadowruby_pendant_driver_t(const special_effect_t &effect) :
spell_t("wanton_sorcery", effect.player, effect.player->find_spell(222276)) {
callbacks = harmful = false;
background = quiet = true;
sorcerous_spells[0] = new sorcerous_fireball_t(effect);
sorcerous_spells[1] = new sorcerous_frostbolt_t(effect);
sorcerous_spells[2] = new sorcerous_arcane_blast_t(effect);
}
virtual void execute() override {
spell_t::execute();
auto current_roll = static_cast<unsigned>( rng().range(0, as<double>(sorcerous_spells.size())));
auto spell = sorcerous_spells[current_roll];
spell->set_target(execute_state->target);
spell->execute();
}
};
static void sorcerous_shadowruby_pendant(special_effect_t &effect) {
effect.execute_action = new sorcerous_shadowruby_pendant_driver_t(effect);
}
// Mage Legendary Items
struct sephuzs_secret_t : public class_buff_cb_t<mage_t, haste_buff_t, haste_buff_creator_t> {
sephuzs_secret_t() : super(MAGE, "sephuzs_secret") {}
virtual haste_buff_t *&buff_ptr(const special_effect_t &e) override {
return debug_cast<mage_t *>(e.player)->buffs.sephuzs_secret;
}
virtual haste_buff_creator_t creator(const special_effect_t &e) const override {
return super::creator(e)
.spell(e.trigger())
.cd(e.player->find_spell(226262)->duration())
.default_value(e.trigger()->effectN(2).percent())
.add_invalidate(CACHE_RUN_SPEED);
}
};
struct shard_of_the_exodar_t : public scoped_actor_callback_t<mage_t> {
shard_of_the_exodar_t() : super(MAGE) {}
virtual void manipulate(mage_t *actor, const special_effect_t & /* e */ ) override {
// Disable default Bloodlust and let us handle it in a custom way.
actor->cooldowns.time_warp->charges = 2;
actor->player_t::buffs.bloodlust->default_chance = 0.0;
actor->player_t::buffs.bloodlust->cooldown->duration = timespan_t::zero();
}
};
// Arcane Legendary Items
struct mystic_kilt_of_the_rune_master_t : public scoped_action_callback_t<arcane_barrage_t> {
mystic_kilt_of_the_rune_master_t() : super(MAGE_ARCANE, "arcane_barrage") {}
virtual void manipulate(arcane_barrage_t *action, const special_effect_t &e) override {
action->mystic_kilt_of_the_rune_master_regen = e.driver()->effectN(1).percent();
}
};
struct rhonins_assaulting_armwraps_t : public class_buff_cb_t<mage_t, buff_t, buff_creator_t> {
rhonins_assaulting_armwraps_t() : super(MAGE_ARCANE, "rhonins_assaulting_armwraps") {}
virtual buff_t *&buff_ptr(const special_effect_t &e) override {
return debug_cast<mage_t *>(e.player)->buffs.rhonins_assaulting_armwraps;
}
virtual buff_creator_t creator(const special_effect_t &e) const override {
return super::creator(e)
.spell(e.trigger())
.chance(e.driver()->effectN(1).percent());
}
};
struct cord_of_infinity_t : public class_buff_cb_t<mage_t, buff_t, buff_creator_t> {
// TODO: What does this do in BfA?
cord_of_infinity_t() : super(MAGE_ARCANE, "cord_of_infinity") {}
virtual buff_t *&buff_ptr(const special_effect_t &e) override {
return debug_cast<mage_t *>(e.player)->buffs.cord_of_infinity;
}
virtual buff_creator_t creator(const special_effect_t &e) const override {
return super::creator(e)
.spell(e.trigger())
.default_value(e.trigger()->effectN(1).percent() / 10.0);
}
};
struct gravity_spiral_t : public scoped_actor_callback_t<mage_t> {
gravity_spiral_t() : super(MAGE_ARCANE) {}
virtual void manipulate(mage_t *actor, const special_effect_t &e) override {
actor->cooldowns.evocation->charges += e.driver()->effectN(1).base_value();
}
};
struct mantle_of_the_first_kirin_tor_t : public scoped_action_callback_t<arcane_barrage_t> {
mantle_of_the_first_kirin_tor_t() : super(MAGE_ARCANE, "arcane_barrage") {}
virtual void manipulate(arcane_barrage_t *action, const special_effect_t &e) override {
action->mantle_of_the_first_kirin_tor_chance = e.driver()->effectN(1).percent();
}
};
// Fire Legendary Items
struct koralons_burning_touch_t : public scoped_action_callback_t<scorch_t> {
koralons_burning_touch_t() : super(MAGE_FIRE, "scorch") {}
virtual void manipulate(scorch_t *action, const special_effect_t &e) override {
action->koralons_burning_touch = true;
action->koralons_burning_touch_threshold = e.driver()->effectN(1).base_value();
action->koralons_burning_touch_multiplier = e.driver()->effectN(2).percent();
}
};
struct darcklis_dragonfire_diadem_t : public scoped_action_callback_t<dragons_breath_t> {
darcklis_dragonfire_diadem_t() : super(MAGE_FIRE, "dragons_breath") {}
virtual void manipulate(dragons_breath_t *action, const special_effect_t &e) override {
action->radius += e.driver()->effectN(1).base_value();
action->base_multiplier *= 1.0 + e.driver()->effectN(2).percent();
}
};
struct marquee_bindings_of_the_sun_king_t : public class_buff_cb_t<mage_t, buff_t, buff_creator_t> {
marquee_bindings_of_the_sun_king_t() : super(MAGE_FIRE, "kaelthas_ultimate_ability") {}
virtual buff_t *&buff_ptr(const special_effect_t &e) override {
return debug_cast<mage_t *>(e.player)->buffs.kaelthas_ultimate_ability;
}
virtual buff_creator_t creator(const special_effect_t &e) const override {
return super::creator(e)
.spell(e.player->find_spell(209455))
.chance(e.driver()->effectN(1).percent());
}
};
struct pyrotex_ignition_cloth_t : public scoped_action_callback_t<phoenix_flames_t> {
pyrotex_ignition_cloth_t() : super(MAGE_FIRE, "phoenix_flames") {}
virtual void manipulate(phoenix_flames_t *action, const special_effect_t &e) override {
action->pyrotex_ignition_cloth = true;
action->pyrotex_ignition_cloth_reduction = e.driver()->effectN(1).time_value();
}
};
struct contained_infernal_core_t : public class_buff_cb_t<mage_t, buff_t, buff_creator_t> {
contained_infernal_core_t() : super(MAGE_FIRE, "contained_infernal_core") {}
virtual buff_t *&buff_ptr(const special_effect_t &e) override {
return debug_cast<mage_t *>(e.player)->buffs.contained_infernal_core;
}
virtual buff_creator_t creator(const special_effect_t &e) const override {
return super::creator(e)
.spell(e.player->find_spell(248146));
}
};
// Frost Legendary Items
struct magtheridons_banished_bracers_t : public class_buff_cb_t<mage_t, buff_t, buff_creator_t> {
magtheridons_banished_bracers_t() : super(MAGE_FROST, "magtheridons_might") {}
virtual buff_t *&buff_ptr(const special_effect_t &e) override {
return debug_cast<mage_t *>(e.player)->buffs.magtheridons_might;
}
virtual buff_creator_t creator(const special_effect_t &e) const override {
return super::creator(e)
.spell(e.trigger())
.default_value(e.trigger()->effectN(1).percent());
}
};
struct zannesu_journey_t : public class_buff_cb_t<mage_t, buff_t, buff_creator_t> {
zannesu_journey_t() : super(MAGE_FROST, "zannesu_journey") {}
virtual buff_t *&buff_ptr(const special_effect_t &e) override {
return debug_cast<mage_t *>(e.player)->buffs.zannesu_journey;
}
virtual buff_creator_t creator(const special_effect_t &e) const override {
return super::creator(e)
.spell(e.trigger())
.default_value(e.trigger()->effectN(1).percent());
}
};
struct lady_vashjs_grasp_t : public scoped_actor_callback_t<mage_t> {
lady_vashjs_grasp_t() : super(MAGE_FROST) {}
virtual void manipulate(mage_t *actor, const special_effect_t & /* e */ ) override {
actor->buffs.lady_vashjs_grasp->default_chance = 1.0;
}
};
struct ice_time_t : public scoped_action_callback_t<frozen_orb_t> {
ice_time_t() : super(MAGE_FROST, "frozen_orb") {}
virtual void manipulate(frozen_orb_t *action, const special_effect_t & /* e */ ) override {
action->ice_time = true;
}
};
struct shattered_fragments_of_sindragosa_t : public class_buff_cb_t<mage_t, buff_t, buff_creator_t> {
shattered_fragments_of_sindragosa_t() : super(MAGE_FROST, "shattered_fragments_of_sindragosa") {}
virtual buff_t *&buff_ptr(const special_effect_t &e) override {
return debug_cast<mage_t *>(e.player)->buffs.shattered_fragments_of_sindragosa;
}
virtual buff_creator_t creator(const special_effect_t &e) const override {
return super::creator(e)
.spell(e.player->find_spell(248176));
}
};
// MAGE MODULE INTERFACE ====================================================
struct mage_module_t : public module_t {
public:
mage_module_t() : module_t(MAGE) {}
virtual player_t *create_player(sim_t *sim, const std::string &name, race_e r = RACE_NONE) const override {
auto p = new mage_t(sim, name, r);
p->report_extension = std::unique_ptr<player_report_extension_t>(new mage_report_t(*p));
return p;
}
virtual void static_init() const override {
unique_gear::register_special_effect(209311, cord_of_infinity_t(), true);
unique_gear::register_special_effect(208099, koralons_burning_touch_t());
unique_gear::register_special_effect(214403, magtheridons_banished_bracers_t(), true);
unique_gear::register_special_effect(206397, zannesu_journey_t(), true);
unique_gear::register_special_effect(208146, lady_vashjs_grasp_t());
unique_gear::register_special_effect(208080, rhonins_assaulting_armwraps_t(), true);
unique_gear::register_special_effect(207547, darcklis_dragonfire_diadem_t());
unique_gear::register_special_effect(208051, sephuzs_secret_t(), true);
unique_gear::register_special_effect(207970, shard_of_the_exodar_t());
unique_gear::register_special_effect(209450, marquee_bindings_of_the_sun_king_t(), true);
unique_gear::register_special_effect(209280, mystic_kilt_of_the_rune_master_t());
unique_gear::register_special_effect(222276, sorcerous_shadowruby_pendant);
unique_gear::register_special_effect(235940, pyrotex_ignition_cloth_t());
unique_gear::register_special_effect(235227, ice_time_t());
unique_gear::register_special_effect(235273, gravity_spiral_t());
unique_gear::register_special_effect(248098, mantle_of_the_first_kirin_tor_t());
unique_gear::register_special_effect(248099, contained_infernal_core_t(), true);
unique_gear::register_special_effect(248100, shattered_fragments_of_sindragosa_t(), true);
}
virtual void register_hotfixes() const override {
hotfix::register_spell("Mage", "2017-01-11", "Incorrect spell level for Frozen Orb bolt.", 84721)
.field("spell_level")
.operation(hotfix::HOTFIX_SET)
.modifier(57)
.verification_value(81);
hotfix::register_spell("Mage", "2017-11-06", "Incorrect spell level for Icicles driver.", 148012)
.field("spell_level")
.operation(hotfix::HOTFIX_SET)
.modifier(78)
.verification_value(80);
hotfix::register_spell("Mage", "2017-11-06", "Incorrect spell level for Icicle.", 148022)
.field("spell_level")
.operation(hotfix::HOTFIX_SET)
.modifier(78)
.verification_value(80);
hotfix::register_spell("Mage", "2017-11-08", "Incorrect spell level for Ignite.", 12654)
.field("spell_level")
.operation(hotfix::HOTFIX_SET)
.modifier(78)
.verification_value(99);
hotfix::register_spell("Mage", "2017-03-20", "Manually set Frozen Orb's travel speed.", 84714)
.field("prj_speed")
.operation(hotfix::HOTFIX_SET)
.modifier(20.0)
.verification_value(0.0);
hotfix::register_spell("Mage", "2017-06-21", "Ice Lance is slower than spell data suggests.", 30455)
.field("prj_speed")
.operation(hotfix::HOTFIX_SET)
.modifier(47.0)
.verification_value(50.0);
}
virtual bool valid() const override { return true; }
virtual void init(player_t *) const override {}
virtual void combat_begin(sim_t *) const override {}
virtual void combat_end(sim_t *) const override {}
};
} // UNNAMED NAMESPACE
const module_t *module_t::mage() {
static mage_module_t m;
return &m;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment