diff --git a/engine/class_modules/sc_rogue.cpp b/engine/class_modules/sc_rogue.cpp index 81a2c4893c7..449d143f9a6 100644 --- a/engine/class_modules/sc_rogue.cpp +++ b/engine/class_modules/sc_rogue.cpp @@ -2224,6 +2224,8 @@ class rogue_action_t : public Base void trigger_doomblade( const action_state_t* ); void trigger_echoing_reprimand( const action_state_t* state ); void trigger_fatal_flourish( const action_state_t* ); + void resolve_fatebound_coinflip( player_t* coin_target, fatebound_t::coinflip_e result ); + void trigger_fatebound_coinflip( player_t* coin_target, fatebound_t::coinflip_e result, timespan_t delay = timespan_t::zero() ); void trigger_fatebound_coinflip( const action_state_t* state, fatebound_t::coinflip_e result, timespan_t delay = timespan_t::zero() ); void trigger_fatebound_edge_case( const action_state_t* state ); void trigger_fazed( const action_state_t* state ); @@ -8029,88 +8031,87 @@ void actions::rogue_action_t::trigger_hand_of_fate( const action_state_t* if ( cast_state( state )->get_combo_points() < p()->talent.fatebound.hand_of_fate->effectN( 1 ).base_value() ) return; - fatebound_t::coinflip_e result; + auto coin_target = state->target->is_enemy() ? state->target : p()->target; + make_event( *p()->sim, current_delay, [ this, coin_target, biased ] { + fatebound_t::coinflip_e result; - // No stacks of either buff or equal stacks of both buffs (thanks to only using edge case) - // Nothing to bias, just flip the coin fairly - if ( p()->buffs.fatebound_coin_tails->total_stack() == p()->buffs.fatebound_coin_heads->total_stack() ) - { - result = p()->rng().roll( 0.5 ) ? fatebound_t::coinflip_e::HEADS : fatebound_t::coinflip_e::TAILS; - } - // Flip the coin, potentially with a bias toward matching the last face flipped - else - { - double matching_odds = 0.5; - if ( biased ) + // No stacks of either buff or equal stacks of both buffs (thanks to only using edge case) + // Nothing to bias, just flip the coin fairly + if ( p()->buffs.fatebound_coin_tails->total_stack() == p()->buffs.fatebound_coin_heads->total_stack() ) { - // TODO: Validate how these stack with the presumed base 50/50 chance and one another - if ( p()->talent.fatebound.mean_streak->ok() ) - { - matching_odds += matching_odds * p()->talent.fatebound.mean_streak->effectN( 1 ).percent(); - } - if ( p()->talent.fatebound.destiny_defined->ok() ) + result = p()->rng().roll( 0.5 ) ? fatebound_t::coinflip_e::HEADS : fatebound_t::coinflip_e::TAILS; + } + // Flip the coin, potentially with a bias toward matching the last face flipped + else + { + double matching_odds = 0.5; + if ( biased ) { - matching_odds += p()->talent.fatebound.destiny_defined->effectN( 3 ).percent(); + // TODO: Validate how these stack with the presumed base 50/50 chance and one another + if ( p()->talent.fatebound.mean_streak->ok() ) + { + matching_odds += matching_odds * p()->talent.fatebound.mean_streak->effectN( 1 ).percent(); + } + if ( p()->talent.fatebound.destiny_defined->ok() ) + { + matching_odds += p()->talent.fatebound.destiny_defined->effectN( 3 ).percent(); + } + if ( p()->buffs.fatebound_lucky_coin->check() ) + { + // MIDNIGHT TOCHECK -- Is this additive? + matching_odds += p()->spell.fatebound_lucky_coin_buff->effectN( 5 ).percent(); + } } - if ( p()->buffs.fatebound_lucky_coin->check() ) + + // TODO: it's an assumption that if you have both buffs (thanks, edge case) the bias prefers the one with more stacks + // (since the last one you hit before the edge case has to be the one with more stacks) + const bool is_match = p()->rng().roll( matching_odds ); + const bool current_is_heads = p()->buffs.fatebound_coin_heads->check() > p()->buffs.fatebound_coin_tails->check(); + result = is_match ? + current_is_heads ? fatebound_t::coinflip_e::HEADS : fatebound_t::coinflip_e::TAILS : + current_is_heads ? fatebound_t::coinflip_e::TAILS : fatebound_t::coinflip_e::HEADS; + } + + if ( p()->talent.fatebound.controlled_chaos->ok() ) + { + int streak = as( p()->talent.fatebound.controlled_chaos->effectN( 1 ).base_value() ); + if ( ( p()->buffs.fatebound_coin_tails->total_stack() >= streak && result == fatebound_t::coinflip_e::HEADS ) || + ( p()->buffs.fatebound_coin_heads->total_stack() >= streak && result == fatebound_t::coinflip_e::TAILS ) ) { - // MIDNIGHT TOCHECK -- Is this additive? - matching_odds += p()->spell.fatebound_lucky_coin_buff->effectN( 5 ).percent(); + // 250ms so it does not coincide with an Overflowing Purse roll at the same time for now + trigger_fatebound_coinflip( coin_target, result, 250_ms ); + p()->procs.controlled_chaos->occur(); } } - // TODO: it's an assumption that if you have both buffs (thanks, edge case) the bias prefers the one with more stacks - // (since the last one you hit before the edge case has to be the one with more stacks) - const bool is_match = p()->rng().roll( matching_odds ); - const bool current_is_heads = p()->buffs.fatebound_coin_heads->check() > p()->buffs.fatebound_coin_tails->check(); - result = is_match ? - current_is_heads ? fatebound_t::coinflip_e::HEADS : fatebound_t::coinflip_e::TAILS : - current_is_heads ? fatebound_t::coinflip_e::TAILS : fatebound_t::coinflip_e::HEADS; - } - - trigger_fatebound_coinflip( state, result, current_delay ); + resolve_fatebound_coinflip( coin_target, result ); + } ); +} - if ( p()->talent.fatebound.controlled_chaos->ok() ) +template +void actions::rogue_action_t::resolve_fatebound_coinflip( player_t* coin_target, fatebound_t::coinflip_e result ) +{ + if ( result == fatebound_t::coinflip_e::HEADS || result == fatebound_t::coinflip_e::EDGE ) { - int streak = as( p()->talent.fatebound.controlled_chaos->effectN( 1 ).base_value() ); - if ( ( p()->buffs.fatebound_coin_tails->total_stack() >= streak && result == fatebound_t::coinflip_e::HEADS ) || - ( p()->buffs.fatebound_coin_heads->total_stack() >= streak && result == fatebound_t::coinflip_e::TAILS ) ) + p()->buffs.fatebound_coin_heads->increment(); + if ( result != fatebound_t::coinflip_e::EDGE ) { - // 250ms so it does not coincide with an Overflowing Purse roll at the same time for now - trigger_fatebound_coinflip( state, result, 250_ms + current_delay ); - p()->procs.controlled_chaos->occur(); + p()->buffs.fatebound_coin_tails->expire(); } } -} - -template -void actions::rogue_action_t::trigger_fatebound_coinflip( const action_state_t* state, fatebound_t::coinflip_e result, timespan_t delay ) -{ - auto coin_target = state->target->is_enemy() ? state->target : p()->target; - make_event( *p()->sim, delay, [ this, coin_target, result, delay ] { - p()->sim->print_log( "{} triggered fatebound coinflip with result '{}' and {} delay", *p(), fatebound_t::coinflip_string( result ), delay ); - if ( result == fatebound_t::coinflip_e::HEADS || result == fatebound_t::coinflip_e::EDGE ) + if ( result == fatebound_t::coinflip_e::TAILS || result == fatebound_t::coinflip_e::EDGE ) + { + // Don't fling tails coins at enemies precombat, since that'll start combat (assume the player knows not to have an enemy targeted) + if ( !ab::is_precombat ) { - p()->buffs.fatebound_coin_heads->increment(); - if ( result != fatebound_t::coinflip_e::EDGE ) - { - p()->buffs.fatebound_coin_tails->expire(); - } + p()->active.fatebound.fatebound_coin_tails->trigger_secondary_action( coin_target ); } - if ( result == fatebound_t::coinflip_e::TAILS || result == fatebound_t::coinflip_e::EDGE ) + if ( result != fatebound_t::coinflip_e::EDGE ) { - // Don't fling tails coins at enemies precombat, since that'll start combat (assume the player knows not to have an enemy targeted) - if ( !ab::is_precombat ) - { - p()->active.fatebound.fatebound_coin_tails->trigger_secondary_action( coin_target ); - } - if ( result != fatebound_t::coinflip_e::EDGE ) - { - // 2026-04-26 -- Logs suggest that the first tails attack is calculated before this fades - p()->buffs.fatebound_coin_heads->expire( !ab::is_precombat ? 1_ms : 0_ms ); - } + // 2026-04-26 -- Logs suggest that the first tails attack is calculated before this fades + p()->buffs.fatebound_coin_heads->expire( !ab::is_precombat ? 1_ms : 0_ms ); } - } ); + } if ( p()->talent.fatebound.rush_to_the_inevitable->ok() ) { @@ -8132,6 +8133,22 @@ void actions::rogue_action_t::trigger_fatebound_coinflip( const action_sta } } +template +void actions::rogue_action_t::trigger_fatebound_coinflip( player_t* coin_target, fatebound_t::coinflip_e result, timespan_t delay ) +{ + make_event( *p()->sim, delay, [ this, coin_target, result, delay ] { + p()->sim->print_log( "{} triggered fatebound coinflip with result '{}' and {} delay", *p(), fatebound_t::coinflip_string( result ), delay ); + resolve_fatebound_coinflip( coin_target, result ); + } ); +} + +template +void actions::rogue_action_t::trigger_fatebound_coinflip( const action_state_t* state, fatebound_t::coinflip_e result, timespan_t delay ) +{ + auto coin_target = state->target->is_enemy() ? state->target : p()->target; + trigger_fatebound_coinflip( coin_target, result, delay ); +} + template void actions::rogue_action_t::trigger_fatebound_edge_case( const action_state_t* state ) {