Skip to content
Open
Changes from all 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
147 changes: 82 additions & 65 deletions engine/class_modules/sc_rogue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -8029,88 +8031,87 @@ void actions::rogue_action_t<Base>::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<int>( 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 <typename Base>
void actions::rogue_action_t<Base>::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<int>( 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 <typename Base>
void actions::rogue_action_t<Base>::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() )
{
Expand All @@ -8132,6 +8133,22 @@ void actions::rogue_action_t<Base>::trigger_fatebound_coinflip( const action_sta
}
}

template <typename Base>
void actions::rogue_action_t<Base>::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 <typename Base>
void actions::rogue_action_t<Base>::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 <typename Base>
void actions::rogue_action_t<Base>::trigger_fatebound_edge_case( const action_state_t* state )
{
Expand Down