Skip to content

Commit 54b9ffb

Browse files
committed
Add raid_events=shield + helpers for enemy-side absorbs
A new `shield` raid event puts an absorb buff on a target enemy (target=, amount=, school= options plus standard timing controls). The raid event creates a real absorb_buff_t so the shield correctly absorbs damage and influences fight termination — the encounter- modeling use case, analogous to raid_event=vulnerable. When a player damages an enemy with an absorb, the attacker's stats include the absorbed portion as damage dealt (action.cpp), so DPS attribution reflects the rolled damage rather than only the post- absorb HP impact. Adds player_t::has_absorb() and current_absorb_amount() helpers (scoped to enemy actors) plus a target.has_absorb expression for APL queries.
1 parent c7607fd commit 54b9ffb

4 files changed

Lines changed: 141 additions & 3 deletions

File tree

engine/action/action.cpp

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2133,20 +2133,27 @@ void action_t::assess_damage( result_amount_type rt, action_state_t* state )
21332133
// Execute outbound damage assessor pipeline on the state object
21342134
player->assessor_out_damage.execute( rt, state );
21352135

2136+
// When the target is an enemy with absorbs, attacker attribution should
2137+
// reflect pre-absorb damage: the player rolled it; a shield consuming some
2138+
// shouldn't undercount their DPS.
2139+
double credited_amount = state->result_amount;
2140+
if ( state->target && state->target->is_enemy() && state->self_absorb_amount > 0 )
2141+
credited_amount += state->self_absorb_amount;
2142+
21362143
// TODO: Should part of this move to assessing, priority_iteration_damage for example?
21372144
if ( state->result_raw > 0 || result_is_miss( state->result ) )
21382145
{
21392146
if ( sim->fight_style == FIGHT_STYLE_DUNGEON_SLICE || sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE )
21402147
{
21412148
if ( state->target->is_boss() )
21422149
{
2143-
player->priority_iteration_dmg += state->result_amount;
2150+
player->priority_iteration_dmg += credited_amount;
21442151
}
21452152
}
21462153
else if ( state->target == sim->target ||
21472154
( sim->merge_enemy_priority_dmg && state->target->is_boss() ) )
21482155
{
2149-
player->priority_iteration_dmg += state->result_amount;
2156+
player->priority_iteration_dmg += credited_amount;
21502157
}
21512158

21522159
record_data( state );
@@ -2158,7 +2165,11 @@ void action_t::record_data( action_state_t* data )
21582165
if ( !stats )
21592166
return;
21602167

2161-
stats->add_result( data->result_amount, data->result_total, report_amount_type( data ), data->result,
2168+
double credited_amount = data->result_amount;
2169+
if ( data->target && data->target->is_enemy() && data->self_absorb_amount > 0 )
2170+
credited_amount += data->self_absorb_amount;
2171+
2172+
stats->add_result( credited_amount, data->result_total, report_amount_type( data ), data->result,
21622173
( may_block || player->position() != POSITION_BACK ) ? data->block_result : BLOCK_RESULT_UNKNOWN,
21632174
data->target );
21642175
}

engine/player/player.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5888,6 +5888,31 @@ double player_t::composite_player_absorb_received_multiplier() const
58885888
return current.absorb_received_multiplier;
58895889
}
58905890

5891+
bool player_t::has_absorb() const
5892+
{
5893+
// Scoped to enemy actors. Player-side absorb subtypes (fractional, per-event-
5894+
// limited, total-limited) aren't faithfully represented by a naive iteration
5895+
// over absorb_buff_list, so this query intentionally only reports for enemies
5896+
// where raid_event=shield places simple absorb_buff_t entries.
5897+
if ( !is_enemy() )
5898+
return false;
5899+
for ( const absorb_buff_t* ab : absorb_buff_list )
5900+
if ( ab->check() )
5901+
return true;
5902+
return false;
5903+
}
5904+
5905+
double player_t::current_absorb_amount() const
5906+
{
5907+
if ( !is_enemy() )
5908+
return 0;
5909+
double total = 0;
5910+
for ( const absorb_buff_t* ab : absorb_buff_list )
5911+
if ( ab->check() )
5912+
total += ab->current_value;
5913+
return total;
5914+
}
5915+
58915916
double player_t::composite_player_target_crit_chance( player_t* t ) const
58925917
{
58935918
double c = 0.0;
@@ -12090,6 +12115,9 @@ std::unique_ptr<expr_t> player_t::create_expression( util::string_view expressio
1209012115
if ( expression_str == "is_enemy" )
1209112116
return expr_t::create_constant( "is_enemy", is_enemy() );
1209212117

12118+
if ( expression_str == "has_absorb" )
12119+
return make_fn_expr( expression_str, [ this ] { return has_absorb() ? 1.0 : 0.0; } );
12120+
1209312121
if ( expression_str == "attack_haste" )
1209412122
return make_fn_expr( expression_str, [this] { return cache.attack_haste(); } );
1209512123

engine/player/player.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,10 @@ struct player_t : public actor_t
14131413
virtual void assess_damage_imminent_pre_absorb( school_e, result_amount_type, action_state_t* );
14141414
virtual void assess_damage_imminent( school_e, result_amount_type, action_state_t* );
14151415
virtual void do_damage( action_state_t* );
1416+
1417+
bool has_absorb() const;
1418+
double current_absorb_amount() const;
1419+
14161420
virtual void assess_heal( school_e, result_amount_type, action_state_t* );
14171421
virtual void trigger_callbacks( proc_types, proc_types2, action_t* action, action_state_t* state,
14181422
proc_trigger_type_e pt_type = TRIGGER_ACTION );

engine/sim/raid_event.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,99 @@ struct vulnerable_event_t final : public raid_event_t
16671667
}
16681668
};
16691669

1670+
// Shield ===================================================================
1671+
1672+
struct shield_event_t final : public raid_event_t
1673+
{
1674+
double amount;
1675+
std::string school_str;
1676+
player_t* target = nullptr;
1677+
std::string target_str;
1678+
school_e school;
1679+
absorb_buff_t* buff = nullptr;
1680+
// Each shield event owns its own absorb_buff_t so overlapping shields stack;
1681+
// instance_id seeds a unique buff name for buff_t::find lookups.
1682+
int instance_id;
1683+
1684+
shield_event_t( sim_t* s, std::string_view options_str )
1685+
: raid_event_t( s, "shield" ), amount( 0.0 ), school( SCHOOL_NONE )
1686+
{
1687+
// Use the event's position in sim->raid_events for the unique-name id.
1688+
// This is consistent across sim_t clones (each thread parses
1689+
// raid_events_str in the same order) so cross-thread stat merging
1690+
// aggregates correctly. Could be replaced with raid_event_t::internal_id
1691+
// if that becomes a framework-provided field.
1692+
instance_id = static_cast<int>( s->raid_events.size() );
1693+
1694+
add_option( opt_float( "amount", amount ) );
1695+
add_option( opt_string( "school", school_str ) );
1696+
1697+
if ( sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE )
1698+
add_option( opt_string( "target", target_str ) );
1699+
else
1700+
add_option( opt_func( "target", [ this ]( auto, auto, std::string_view v ) { return parse_target( v ); } ) );
1701+
1702+
parse_options( options_str );
1703+
1704+
if ( sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE )
1705+
target_str = "Pull_" + util::to_string( pull ) + "_" + target_str;
1706+
1707+
if ( !school_str.empty() )
1708+
{
1709+
school = util::parse_school_type( school_str );
1710+
if ( school == SCHOOL_NONE )
1711+
throw std::invalid_argument( fmt::format( "Unknown shield raid event school '{}'", school_str ) );
1712+
}
1713+
}
1714+
1715+
bool parse_target( std::string_view value )
1716+
{
1717+
auto it = range::find_if( sim->target_list, [ &value ]( const player_t* t ) {
1718+
return util::str_compare_ci( value, t->name() );
1719+
} );
1720+
1721+
if ( it != sim->target_list.end() )
1722+
{
1723+
target = *it;
1724+
return true;
1725+
}
1726+
sim->error( "Unknown shield raid event target '{}'", value );
1727+
return true;
1728+
}
1729+
1730+
void _start() override
1731+
{
1732+
if ( !buff )
1733+
{
1734+
if ( sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE )
1735+
{
1736+
target = sim->find_player( target_str );
1737+
if ( !target )
1738+
throw std::invalid_argument( fmt::format( "Unknown shield raid event target '{}'", target_str ) );
1739+
}
1740+
else if ( !target )
1741+
{
1742+
target = sim->target;
1743+
}
1744+
1745+
buff = make_buff<absorb_buff_t>( target, fmt::format( "raid_event_shield_{}", instance_id ) );
1746+
if ( school != SCHOOL_NONE )
1747+
buff->set_absorb_school( school );
1748+
}
1749+
1750+
// amount unspecified => unbreakable for the window. infinity sentinel
1751+
// avoids consume()-driven expiry; _finish() drops it on window close.
1752+
double value = amount > 0 ? amount : std::numeric_limits<double>::infinity();
1753+
buff->trigger( 1, value, -1.0, timespan_t::max() );
1754+
}
1755+
1756+
void _finish() override
1757+
{
1758+
if ( buff )
1759+
buff->expire();
1760+
}
1761+
};
1762+
16701763
// Position Switch ==========================================================
16711764

16721765
struct position_event_t : public raid_event_t
@@ -2305,6 +2398,8 @@ std::unique_ptr<raid_event_t> raid_event_t::create( sim_t* sim, util::string_vie
23052398
return std::unique_ptr<raid_event_t>( new stun_event_t( sim, options_str ) );
23062399
if ( name == "vulnerable" )
23072400
return std::unique_ptr<raid_event_t>( new vulnerable_event_t( sim, options_str ) );
2401+
if ( name == "shield" )
2402+
return std::unique_ptr<raid_event_t>( new shield_event_t( sim, options_str ) );
23082403
if ( name == "position_switch" )
23092404
return std::unique_ptr<raid_event_t>( new position_event_t( sim, options_str ) );
23102405
if ( name == "flying" )

0 commit comments

Comments
 (0)