Skip to content

[Mage] Clearcasting BLP + Arcane TWW2 Tier Effect #10303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: thewarwithin
Choose a base branch
from
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 82 additions & 53 deletions engine/class_modules/sc_mage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ struct mage_t final : public player_t
unsigned initial_spellfire_spheres = 5;
arcane_phoenix_rotation arcane_phoenix_rotation_override = arcane_phoenix_rotation::DEFAULT;
bool ice_nova_consumes_winters_chill = true;
double clearcasting_chance = 0.0068;
double illuminated_thoughts_clearcasting_chance = 0.0938;
} options;

// Pets
Expand Down Expand Up @@ -575,6 +577,7 @@ struct mage_t final : public player_t
int embedded_splinters;
int magis_spark_spells;
int intuition_blp_count;
int clearcasting_blp_count;
} state;

struct expression_support_t
Expand Down Expand Up @@ -1026,7 +1029,7 @@ struct mage_t final : public player_t
void trigger_arcane_charge( int stacks = 1 );
bool trigger_brain_freeze( double chance, proc_t* source, timespan_t delay = 0.15_s );
bool trigger_crowd_control( const action_state_t* s, spell_mechanic type, timespan_t adjust = 0_ms );
bool trigger_clearcasting( double chance, timespan_t delay = 0.15_s );
bool trigger_clearcasting( double chance, timespan_t delay = 0.15_s, bool predictable = true, bool guaranteed_jackpot = false );
Copy link
Contributor Author

@pinepinepinepinepine pinepinepinepinepine Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it's better to handle jackpots/tier effects inside of Clearcasting or ToTM; ToTM grants a guaranteed jackpot, either I deal with it within totm's execute, or here. My reasoning was that since jackpots are directly linked with regular Clearcasting procs, I might as well pair them up, but I'm not sure if this is the right call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the need to set up the RPPM for this as I mentioned in another comment, I don't think it makes sense to have the guaranteed triggers in here. I think it's better to just trigger it directly from TotM so that the trigger in here can always roll the RPPM. This way it's also a bit more consistent with how the other specs are handled. While this could cause a double trigger of the jackpot buffs in some cases, as far as I can tell that wouldn't matter, so we don't even need to test if it can happen in game.

Is there any case other than the Time Anomaly trigger where a true 100% chance proc would not be predictable here? It might be better to make it never_predictable = false by default and then use if ( chance >= 1.0 && !never_predictable ) as the condition.

bool trigger_fof( double chance, proc_t* source, int stacks = 1 );
void trigger_icicle( player_t* icicle_target, bool chain = false );
void trigger_icicle_gain( player_t* icicle_target, action_t* icicle_action, double chance = 1.0, timespan_t duration = timespan_t::min() );
Expand Down Expand Up @@ -1924,6 +1927,7 @@ struct mage_spell_t : public spell_t
{
bool chill = false;
bool clearcasting = false;
bool intuition = false;
bool from_the_ashes = false;
bool frostfire_infusion = true;
bool frostfire_mastery = true;
Expand Down Expand Up @@ -2258,25 +2262,39 @@ struct mage_spell_t : public spell_t
// This will prevent for example Arcane Missiles consuming its own Clearcasting proc.
consume_cost_reductions();

if ( p()->spec.clearcasting->ok() && triggers.clearcasting )
if ( triggers.clearcasting && p()->spec.clearcasting->ok() )
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why I changed this. I suppose it just looked neater to me at the time? Sorry.

{
// TODO: implement the hidden BLP
double chance = p()->spec.clearcasting->effectN( 2 ).percent();
chance += p()->talents.illuminated_thoughts->effectN( 1 ).percent();
// Arcane Blast gets an additional 5% chance. Not mentioned in the spell data (or even the description).
constexpr int cc_blp_threshold = 13;
// The tooltip chance present on Clearcasting/Illuminated Thoughts is the total expected outcome of Clearcasting applications, not it's random proc chance.
// Whenever combining both the proc chance and its bad luck protection, the final application rate is equal to its tooltip chance.
double proc_chance = p()->options.clearcasting_chance;
if ( p()->talents.illuminated_thoughts.ok() )
proc_chance = p()->options.illuminated_thoughts_clearcasting_chance;
// Arcane Blast has an unmentioned 5% increase in total expected Clearcasting applications -- same BLP threshold, but higher proc chance.
if ( id == 30451 )
chance += 0.05;
p()->trigger_clearcasting( chance );
{
proc_chance = 0.0938;
if ( p()->talents.illuminated_thoughts.ok() )
proc_chance = 0.1618;
}

// Likely a bug: Arcane Orb procs from Orb Barrage uniquely decrements the BLP, effectively nullifying the incoming increment from Barrage.
if ( name_str == "orb_barrage_arcane_orb" )
Copy link
Contributor Author

@pinepinepinepinepine pinepinepinepinepine Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I should include the use of 'bugs' here. I have no idea if this is intentional by blizz -- if it is, I think it's kinda silly, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should check whether the arcane_orb_t's type == ao_type::ORB_BARRAGE instead of doing a string comparison.

p()->state.clearcasting_blp_count -= 1;
else
p()->state.clearcasting_blp_count += 1;
if ( !background && rng().roll( proc_chance ) || ( p()->state.clearcasting_blp_count >= cc_blp_threshold ) )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something like this would be cleaner here, especially when combined with the change I mentioned above to replace that predictable argument, because unless I'm missing something, we could predict a proc is coming due to BLP.

if ( p()->state.clearcasting_blp_count >= cc_blp_threshold )
  proc_chance = 1.0;
  
if ( p()->trigger_clearcasting( proc_chance ) )
  p()->state.clearcasting_blp_count = 0;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we allow predicting the BLP (I'd assume for both this and intuition), might it be a good idea to make the bad luck protection visible as an actual buff, something like with AA's counter? I'd also assume it'd be nice to have it visible + tangible within the input.

{
p()->trigger_clearcasting( 1.0, 100_ms, false );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 100_ms and not the default 0.15_s? I don't know which is more accurate, but I think it should be consistent in both places.

Copy link
Contributor Author

@pinepinepinepinepine pinepinepinepinepine Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100ms delay for CC gained via the BLP/random proc rate is what I consistently saw in logs. Occasionally, I'd see a bit of a delay greater than 100ms, but I believe it's mostly due to my autoclicker being unsynchronized with spell casts and its following GCD, so it'd desync every now and then. For stuff like TA/Splinterstorm/Soul/ToTM jackpot, it was 0ms, though.

Yeah, I'll go ahead and change it.

edit: I'll re-verify the delay closer, it's confusing me. Just a bit of time.

p()->state.clearcasting_blp_count = 0;
}
}

if ( !background && affected_by.ice_floes && time_to_execute > 0_ms )
p()->buffs.ice_floes->decrement();

if ( triggers.frostfire_mastery && harmful && !background )
trigger_frostfire_mastery();

if ( !background && harmful )
trigger_intuition( false );
}

void impact( action_state_t* s ) override
Expand Down Expand Up @@ -2464,28 +2482,6 @@ struct mage_spell_t : public spell_t

p()->state.trigger_glorious_incandescence = false;
}


// The blp_exclude_initial param controls whether the spell is allowed to start a new BLP "chain"
// by itself. Some spells (orbs from Orb Barrage, AE echoes) only contribute to ongoing chains.
void trigger_intuition( bool blp_exclude_initial )
{
if ( !p()->talents.intuition.ok() || p()->buffs.intuition->check() )
return;

constexpr int blp_threshold = 11;

if ( p()->state.intuition_blp_count > 0 || !blp_exclude_initial )
p()->state.intuition_blp_count += 1;

if ( p()->state.intuition_blp_count >= blp_threshold
|| ( !background && harmful && rng().roll( p()->talents.intuition->effectN( 1 ).percent() ) ) )
{
// Needs to be triggered with a delay so that ABar doesn't eat its own proc
make_event( *sim, [ this ] { p()->buffs.intuition->trigger(); } );
p()->state.intuition_blp_count = 0;
}
}
};

double mage_spell_state_t::composite_crit_chance() const
Expand Down Expand Up @@ -2620,6 +2616,27 @@ struct arcane_mage_spell_t : public mage_spell_t
return 1.0 + p()->buffs.arcane_charge->check() * ( base + mastery );
}

void execute() override
{
mage_spell_t::execute();

if ( p()->talents.intuition.ok() && !p()->buffs.intuition->check() && triggers.intuition )
{
constexpr int blp_threshold = 11;
// Tooltip claims there's a 5% chance to proc Intuition, yet seemingly, it's 1%
constexpr double base_proc_chance = 0.01;

p()->state.intuition_blp_count += 1;
if ( p()->state.intuition_blp_count >= blp_threshold
|| ( !background && rng().roll( base_proc_chance ) ) )
{
// Needs to be triggered with a delay so that ABar doesn't eat its own proc
make_event( *sim, [ this ] { p()->buffs.intuition->trigger(); } );
p()->state.intuition_blp_count = 0;
}
}
}

void impact( action_state_t* s ) override
{
mage_spell_t::impact( s );
Expand Down Expand Up @@ -3430,7 +3447,8 @@ struct arcane_orb_t final : public arcane_mage_spell_t
may_miss = false;
aoe = -1;
cooldown->charges += as<int>( p->talents.charged_orb->effectN( 1 ).base_value() );
triggers.clearcasting = type == ao_type::NORMAL;
triggers.clearcasting = true;
triggers.intuition = true;

std::string_view bolt_name;
switch ( type )
Expand Down Expand Up @@ -3464,9 +3482,6 @@ struct arcane_orb_t final : public arcane_mage_spell_t
{
arcane_mage_spell_t::execute();
p()->trigger_arcane_charge();

if ( background )
trigger_intuition( type == ao_type::ORB_BARRAGE );
}

void impact( action_state_t* s ) override
Expand Down Expand Up @@ -3511,6 +3526,7 @@ struct arcane_barrage_t final : public dematerialize_spell_t
base_aoe_multiplier *= p->talents.arcing_cleave->effectN( 2 ).percent();
affected_by.arcane_debilitation = true;
triggers.clearcasting = true;
triggers.intuition = true;
base_multiplier *= 1.0 + p->sets->set( MAGE_ARCANE, TWW1, B2 )->effectN( 1 ).percent();
glorious_incandescence_charges = as<int>( p->find_spell( 451223 )->effectN( 1 ).base_value() );
arcane_soul_charges = as<int>( p->find_spell( 453413 )->effectN( 1 ).base_value() );
Expand Down Expand Up @@ -3634,6 +3650,7 @@ struct arcane_blast_t final : public dematerialize_spell_t
parse_options( options_str );
affected_by.arcane_debilitation = true;
triggers.clearcasting = true;
triggers.intuition = true;
base_multiplier *= 1.0 + p->talents.consortiums_bauble->effectN( 2 ).percent();
base_multiplier *= 1.0 + p->sets->set( MAGE_ARCANE, TWW1, B2 )->effectN( 1 ).percent();
base_costs[ RESOURCE_MANA ] *= 1.0 + p->talents.consortiums_bauble->effectN( 1 ).percent();
Expand Down Expand Up @@ -3752,6 +3769,7 @@ struct arcane_explosion_t final : public arcane_mage_spell_t
aoe = -1;
affected_by.savant = true;
triggers.clearcasting = type != ae_type::ENERGY_RECON;
triggers.intuition = true;

if ( type == ae_type::NORMAL )
{
Expand Down Expand Up @@ -3779,9 +3797,6 @@ struct arcane_explosion_t final : public arcane_mage_spell_t
p()->buffs.static_cloud->expire();
p()->buffs.static_cloud->trigger();

if ( background )
trigger_intuition( type == ae_type::ECHO );

if ( type == ae_type::ENERGY_RECON )
return;

Expand Down Expand Up @@ -3955,6 +3970,7 @@ struct arcane_missiles_t final : public custom_state_spell_t<arcane_mage_spell_t
// In the game, the tick zero of Arcane Missiles actually happens after 100 ms
tick_zero = channeled = true;
triggers.clearcasting = true;
triggers.intuition = true;
tick_action = get_action<arcane_missiles_tick_t>( "arcane_missiles_tick", p );
cost_reductions = { p->buffs.clearcasting };
if ( p->talents.slipstream.ok() )
Expand Down Expand Up @@ -4100,6 +4116,7 @@ struct arcane_surge_t final : public arcane_mage_spell_t
reduced_aoe_targets = data().effectN( 3 ).base_value();
// TODO 11.1: Applies to Arcane Surge instead of Arcane Orb
base_multiplier *= 1.0 + p->sets->set( MAGE_ARCANE, TWW1, B4 )->effectN( 1 ).percent();
triggers.intuition = true;
}

timespan_t travel_time() const override
Expand Down Expand Up @@ -4575,8 +4592,6 @@ struct fireball_t final : public fire_mage_spell_t
triggers.phoenix_reborn = triggers.unleashed_inferno = TT_MAIN_TARGET;
triggers.ignite = triggers.from_the_ashes = true;
affected_by.unleashed_inferno = affected_by.flame_accelerant = true;
if ( p->bugs && sim->dbc->wowv() < wowv_t{ 11, 1, 5 } )
base_dd_multiplier *= 1.0 + p->talents.master_of_flame->effectN( 3 ).percent();

if ( frostfire )
{
Expand Down Expand Up @@ -6762,6 +6777,7 @@ struct touch_of_the_magi_t final : public arcane_mage_spell_t
{
parse_options( options_str );
triggers.clearcasting = true;
triggers.intuition = true;

if ( data().ok() )
add_child( p->action.touch_of_the_magi_explosion );
Expand All @@ -6773,7 +6789,8 @@ struct touch_of_the_magi_t final : public arcane_mage_spell_t

p()->trigger_arcane_charge( as<int>( data().effectN( 2 ).base_value() ) );
p()->buffs.leydrinker->trigger();
p()->trigger_jackpot( true );
// Clearcastings generated by TWW2's tier effect is independent, allowing ToTM to sometimes apply two applications with one cast.
p()->trigger_clearcasting( p()->sets->has_set_bonus( MAGE_ARCANE, TWW2, B2 ), 0_ms, true, true );
}

void impact( action_state_t* s ) override
Expand Down Expand Up @@ -7500,7 +7517,7 @@ struct time_anomaly_tick_event_t final : public mage_event_t
mage->buffs.arcane_surge->trigger( 1000 * mage->talents.time_anomaly->effectN( 1 ).time_value() );
break;
case TA_CLEARCASTING:
mage->trigger_clearcasting( 1.0, 0_ms );
mage->trigger_clearcasting( 1.0, 0_ms, false );
break;
case TA_COMBUSTION:
mage->buffs.combustion->trigger( 1000 * mage->talents.time_anomaly->effectN( 4 ).time_value() );
Expand Down Expand Up @@ -7585,7 +7602,7 @@ struct splinterstorm_event_t final : public mage_event_t
else
// Doesn't seem to be affected by Illuminated Thoughts.
// TODO: get more data and double check
mage->trigger_clearcasting( mage->talents.splinterstorm->effectN( 4 ).percent(), 0_ms );
mage->trigger_clearcasting( mage->talents.splinterstorm->effectN( 4 ).percent(), 0_ms, false );
}

mage->events.splinterstorm = make_event<splinterstorm_event_t>(
Expand Down Expand Up @@ -7921,6 +7938,8 @@ void mage_t::create_options()
return true;
} ) );
add_option( opt_bool( "mage.ice_nova_consumes_winters_chill", options.ice_nova_consumes_winters_chill ) );
add_option( opt_float( "mage.clearcasting_chance", options.clearcasting_chance));
add_option( opt_float( "mage.illuminated_thoughts_clearcasting_chance", options.illuminated_thoughts_clearcasting_chance));

player_t::create_options();
}
Expand Down Expand Up @@ -8371,7 +8390,7 @@ void mage_t::init_spells()
void mage_t::init_special_effects()
{
auto spell = sets->set( specialization(), TWW2, B2 );
if ( spell->ok() )
if ( spell->ok() && ( specialization() != MAGE_ARCANE ) )
{
auto effect = new special_effect_t( this );
effect->name_str = "mage_jackpot_proc";
Expand Down Expand Up @@ -9488,11 +9507,20 @@ void mage_t::trigger_jackpot( bool guaranteed )
bool has_4pc = sets->has_set_bonus( specialization(), TWW2, B4 );
switch ( specialization() )
{
// TWW2's tier effect for Arcane doesn't randomly generate Clearcasting (has a misleading tooltip). Instead, it solely grants Clarity/AA whenever any source of CC is applied at a 13% chance.
case MAGE_ARCANE:
buffs.clarity->trigger();
trigger_clearcasting( 1.0, 0_ms );
if ( has_4pc )
buffs.aether_attunement->trigger();
if ( guaranteed || rng().roll( 0.13 ) )
Copy link
Contributor Author

@pinepinepinepinepine pinepinepinepinepine Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused as to where it originally (before this) returned Jackpot's original proc rate -- I'm certain it's a 13% chance, but I can't find a mention of it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jackpot effects are normally triggered through the driver aura that gets set up in mage_t::init_special_effects. For Arcane, this aura is 2 RPPM, which when saturated after 3.5 seconds of no trigger attempts comes out to a proc chance of 2 RPPM * 3.5 seconds / 60 seconds = 11.67%. On top of this, RPPM has its own BLP system, which is probably where the 13% average you're seeing comes from (under normal conditions, the BLP averages out to around 13.1% extra procs, which would increase the 11.67% to about 13.2% here).

To roll the RPPM after triggering Clearcasting, we're probably going to need to (1) not create that special effect for Arcane and (2) manually set up the RPPM object for Arcane (see mage_t::init_rng for a couple of other RPPMs we use manually). The RPPM object would then be manually rolled before calling non-guaranteed triggers of the jackpot.

{
buffs.clarity->trigger();
if ( guaranteed )
buffs.clarity->predict();
if ( has_4pc )
{
buffs.aether_attunement->trigger();
if ( guaranteed )
buffs.aether_attunement->predict();
}
}
break;
case MAGE_FIRE:
{
Expand Down Expand Up @@ -9694,7 +9722,7 @@ void mage_t::trigger_splinter( player_t* target, int count )
}
}

bool mage_t::trigger_clearcasting( double chance, timespan_t delay )
bool mage_t::trigger_clearcasting( double chance, timespan_t delay, bool predictable, bool guaranteed_jackpot )
{
if ( specialization() != MAGE_ARCANE )
return false;
Expand All @@ -9706,10 +9734,11 @@ bool mage_t::trigger_clearcasting( double chance, timespan_t delay )
make_event( *sim, delay, [ this ] { buffs.clearcasting->trigger(); } );
else
buffs.clearcasting->trigger();

if ( chance >= 1.0 )
if ( predictable )
buffs.clearcasting->predict();
buffs.big_brained->trigger();

trigger_jackpot( guaranteed_jackpot );
}

return success;
Expand Down