From b14f4a12e52117fa20da5398f2c9fe8ca4047126 Mon Sep 17 00:00:00 2001 From: Publik Date: Fri, 1 May 2026 23:28:17 -0500 Subject: [PATCH 1/2] [Rogue] adjust fatebound delayed coinflip resolution timing --- engine/class_modules/sc_rogue.cpp | 146 +++++++++++++++++------------- 1 file changed, 82 insertions(+), 64 deletions(-) diff --git a/engine/class_modules/sc_rogue.cpp b/engine/class_modules/sc_rogue.cpp index 926275758ed..c59f4c66b8c 100644 --- a/engine/class_modules/sc_rogue.cpp +++ b/engine/class_modules/sc_rogue.cpp @@ -2221,6 +2221,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 ); @@ -8026,87 +8028,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() ) - { - matching_odds += p()->talent.fatebound.destiny_defined->effectN( 3 ).percent(); - } - if ( p()->buffs.fatebound_lucky_coin->check() ) + 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 ) { - // MIDNIGHT TOCHECK -- Is this additive? - matching_odds += p()->spell.fatebound_lucky_coin_buff->effectN( 5 ).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(); + } } - } - // 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; - } + // 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() ) - { - 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 ) ) + if ( p()->talent.fatebound.controlled_chaos->ok() ) { - // 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 ); + 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 ) ) + { + // 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 ); + } } - } + } ); } template -void actions::rogue_action_t::trigger_fatebound_coinflip( const action_state_t* state, fatebound_t::coinflip_e result, timespan_t delay ) +void actions::rogue_action_t::resolve_fatebound_coinflip( player_t* coin_target, fatebound_t::coinflip_e result ) { - 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::HEADS || result == fatebound_t::coinflip_e::EDGE ) + { + p()->buffs.fatebound_coin_heads->increment(); + if ( result != fatebound_t::coinflip_e::EDGE ) { - p()->buffs.fatebound_coin_heads->increment(); - if ( result != fatebound_t::coinflip_e::EDGE ) - { - p()->buffs.fatebound_coin_tails->expire(); - } + p()->buffs.fatebound_coin_tails->expire(); } - if ( result == fatebound_t::coinflip_e::TAILS || 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 ) { - // 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 ); - } + 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 ); + } + } + if ( p()->talent.fatebound.rush_to_the_inevitable->ok() ) { double energize_normal = p()->specialization() == ROGUE_ASSASSINATION @@ -8126,6 +8128,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 ) { From f16ea2cd3f4e826e7f083a7da6f27abbb4e6d8bb Mon Sep 17 00:00:00 2001 From: Publik Date: Sat, 2 May 2026 12:08:55 -0500 Subject: [PATCH 2/2] address feedback --- engine/class_modules/sc_rogue.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/class_modules/sc_rogue.cpp b/engine/class_modules/sc_rogue.cpp index c59f4c66b8c..79055e8ad8d 100644 --- a/engine/class_modules/sc_rogue.cpp +++ b/engine/class_modules/sc_rogue.cpp @@ -8069,8 +8069,6 @@ void actions::rogue_action_t::trigger_hand_of_fate( const action_state_t* current_is_heads ? fatebound_t::coinflip_e::TAILS : fatebound_t::coinflip_e::HEADS; } - resolve_fatebound_coinflip( coin_target, result ); - if ( p()->talent.fatebound.controlled_chaos->ok() ) { int streak = as( p()->talent.fatebound.controlled_chaos->effectN( 1 ).base_value() ); @@ -8081,6 +8079,8 @@ void actions::rogue_action_t::trigger_hand_of_fate( const action_state_t* trigger_fatebound_coinflip( coin_target, result, 250_ms ); } } + + resolve_fatebound_coinflip( coin_target, result ); } ); }