From 6da84b7bfd9e1fe8d3064f317cd4a15762cc6b01 Mon Sep 17 00:00:00 2001 From: Azortharion Date: Fri, 24 Jul 2026 15:31:34 +0200 Subject: [PATCH] DungeonRoute: opt-in dynamic targeting. dungeon_route_dynamic_targeting=1 attacks the highest-current-hp mob (1s re-check) instead of the spawn-list head, and prioritydps counts damage into that mob instead of BOSS_ tags. Default off, no behavior change unless enabled. --- engine/action/action.cpp | 15 +++++++++++++-- engine/player/player.cpp | 38 ++++++++++++++++++++++++++++++++++++-- engine/sc_enums.hpp | 3 ++- engine/sim/raid_event.cpp | 33 +++++++++++++++++++++++++++++++++ engine/sim/sim.cpp | 5 +++++ engine/sim/sim.hpp | 2 ++ engine/util/util.cpp | 1 + 7 files changed, 92 insertions(+), 5 deletions(-) diff --git a/engine/action/action.cpp b/engine/action/action.cpp index be16cd258f8..0a1def796a9 100644 --- a/engine/action/action.cpp +++ b/engine/action/action.cpp @@ -2130,7 +2130,16 @@ void action_t::assess_damage( result_amount_type rt, action_state_t* state ) { if ( sim->fight_style == FIGHT_STYLE_DUNGEON_SLICE || sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE ) { - if ( state->target->is_boss() ) + if ( sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE && sim->dungeon_route_dynamic_targeting ) + { + // Dynamic targeting: priority damage is damage into the mob currently gating the end of + // the pull (the highest-hp one), regardless of BOSS_ prefixes. + if ( state->target == sim->dungeon_route_priority_target ) + { + player->priority_iteration_dmg += state->result_amount; + } + } + else if ( state->target->is_boss() ) { player->priority_iteration_dmg += state->result_amount; } @@ -5120,8 +5129,10 @@ void action_t::acquire_target( retarget_source event, player_t* /* context */, p } // Don't swap targets if the action's current target is still alive, except in cases - // where the actor has risen (been summoned, start of iteration etc). + // where the actor has risen (been summoned, start of iteration etc), or when dungeon route + // dynamic targeting moves everyone onto the currently-highest-hp mob. if ( event != retarget_source::SELF_ARISE && + !( sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE && sim->dungeon_route_dynamic_targeting ) && target && !target->is_sleeping() && !target->debuffs.invulnerable->check() ) { return; diff --git a/engine/player/player.cpp b/engine/player/player.cpp index baee2a26f89..29059f0618a 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -14657,6 +14657,9 @@ void player_t::acquire_target( retarget_source event, player_t* context ) player_t* candidate_target = nullptr; player_t* first_invuln_target = nullptr; + bool dynamic_targeting = sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE && sim->dungeon_route_dynamic_targeting; + double candidate_hp = -1.0; + // TODO: Fancier system for ( auto enemy : sim->target_non_sleeping_list ) { @@ -14669,8 +14672,39 @@ void player_t::acquire_target( retarget_source event, player_t* context ) continue; } - candidate_target = enemy; - break; + if ( dynamic_targeting ) + { + // Dynamic targeting: always attack the mob that currently has the most hp, since it gates the + // end of the pull. Re-evaluated on arise/demise and on the periodic PRIORITY_CHANGE event. + double enemy_hp = enemy->resources.current[ RESOURCE_HEALTH ]; + if ( enemy_hp > candidate_hp ) + { + candidate_hp = enemy_hp; + candidate_target = enemy; + } + } + else + { + candidate_target = enemy; + break; + } + } + + if ( dynamic_targeting ) + { + // Keep the current target unless the candidate leads it by more than 5%: no two mobs are ever + // exactly tied mid-combat, and per-tick lead flips between mobs dying together would cause + // pointless target flickering. A player swaps on a visible hp lead, not on every frame. + if ( target && target != candidate_target && !target->is_sleeping() && target->is_enemy() && + !( target->debuffs.invulnerable && target->debuffs.invulnerable->check() ) && + candidate_hp <= target->resources.current[ RESOURCE_HEALTH ] * 1.05 ) + { + candidate_target = target; + } + + // Track the current priority mob for the Priority DPS metric. Follows the hysteresis-kept + // choice so the metric always agrees with the target the player is actually prioritizing. + sim->dungeon_route_priority_target = candidate_target; } // Invulnerable targets are currently not in the target_non_sleeping_list, so fall back to diff --git a/engine/sc_enums.hpp b/engine/sc_enums.hpp index 6945f7fbed0..d19a880c9fe 100644 --- a/engine/sc_enums.hpp +++ b/engine/sc_enums.hpp @@ -58,7 +58,8 @@ enum class retarget_source ACTOR_DEMISE, // Any actor demises ACTOR_INVULNERABLE, // Actor becomes invulnerable ACTOR_VULNERABLE, // Actor becomes vulnerable (after becoming invulnerable) - SELF_ARISE // Actor has arisen (no context provided) + SELF_ARISE, // Actor has arisen (no context provided) + PRIORITY_CHANGE // DungeonRoute dynamic targeting: periodic re-check of the highest-hp mob (no context) }; // Misc Constants diff --git a/engine/sim/raid_event.cpp b/engine/sim/raid_event.cpp index e767010c3cf..d81f16da4d4 100644 --- a/engine/sim/raid_event.cpp +++ b/engine/sim/raid_event.cpp @@ -480,6 +480,32 @@ struct pull_event_t final : raid_event_t } }; + // Periodically re-checks which mob is currently the highest-hp one and retargets the players + // onto it (dungeon_route_dynamic_targeting). 1 second approximates a player's tab-retarget + // reaction time. + struct dynamic_retarget_event_t : public event_t + { + pull_event_t* pull; + + dynamic_retarget_event_t( pull_event_t* pull_ ) : event_t( *pull_->sim, 1.0_s ), pull( pull_ ) + { } + + const char* name() const override + { + return "dynamic_retarget_event_t"; + } + + void execute() override + { + pull->retarget_event = nullptr; + + range::for_each( sim().player_non_sleeping_list, + []( player_t* p ) { p->acquire_target( retarget_source::PRIORITY_CHANGE ); } ); + + pull->retarget_event = make_event( sim(), pull ); + } + }; + player_t* master; std::string enemies_str; timespan_t delay; @@ -489,6 +515,7 @@ struct pull_event_t final : raid_event_t bool has_boss; event_t* spawn_event; event_t* redistribute_event; + event_t* retarget_event; extended_sample_data_t real_duration; std::vector> child_events; @@ -513,6 +540,7 @@ struct pull_event_t final : raid_event_t has_boss( false ), spawn_event( nullptr ), redistribute_event( nullptr ), + retarget_event( nullptr ), real_duration( "Pull Length", false ) { add_option( opt_string( "enemies", enemies_str ) ); @@ -738,6 +766,9 @@ struct pull_event_t final : raid_event_t if ( shared_health ) redistribute_event = make_event( *sim, this ); + if ( sim->fight_style == FIGHT_STYLE_DUNGEON_ROUTE && sim->dungeon_route_dynamic_targeting ) + retarget_event = make_event( *sim, this ); + sim->print_log( "Spawned Pull {}: {} mobs with {} total health, {:.1f}s delay from previous", pull, adds.size(), total_health, delay.total_seconds() ); @@ -768,6 +799,7 @@ struct pull_event_t final : raid_event_t saved_duration = timespan_t::from_seconds( length ); event_t::cancel( redistribute_event ); + event_t::cancel( retarget_event ); if ( has_boss ) { @@ -824,6 +856,7 @@ struct pull_event_t final : raid_event_t raid_event->reset(); redistribute_event = nullptr; + retarget_event = nullptr; } double parse_health( util::string_view str ) diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index 4620571d0b9..ac927dfe7d4 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -1467,6 +1467,8 @@ sim_t::sim_t() dungeon_route_simple_dps_members( 0 ), dungeon_route_pct_hp( 0 ), dungeon_route_key_level( 0 ), + dungeon_route_dynamic_targeting( false ), + dungeon_route_priority_target( nullptr ), challenge_mode( false ), scale_itemlevel_down_only( false ), disable_set_bonuses( false ), @@ -1806,6 +1808,8 @@ void sim_t::reset() { print_debug( "Resetting Simulator" ); + dungeon_route_priority_target = nullptr; + if ( deterministic ) seed = rng().reseed(); @@ -3910,6 +3914,7 @@ void sim_t::create_options() add_option( opt_int( "keystone_level", dungeon_route_key_level ) ); add_option( opt_int( "keystone_pct_hp", dungeon_route_pct_hp, 0, 100 ) ); add_option( opt_bool( "dungeon_route_smart_targeting", dungeon_route_smart_targeting ) ); + add_option( opt_bool( "dungeon_route_dynamic_targeting", dungeon_route_dynamic_targeting ) ); add_option( opt_int( "dungeon_route_simple_dps_members", dungeon_route_simple_dps_members, 0, 3 ) ); // Character Creation diff --git a/engine/sim/sim.hpp b/engine/sim/sim.hpp index 4a817aef73e..65344fc7330 100644 --- a/engine/sim/sim.hpp +++ b/engine/sim/sim.hpp @@ -175,6 +175,8 @@ struct sim_t : private sc_thread_t int dungeon_route_simple_dps_members; // fills the sim with up to 3 simplified dps players, just like your lfg weeklies :^) int dungeon_route_pct_hp; // the portion of full mob hp being used to sim an incomplete party int dungeon_route_key_level; // keystone difficulty level + bool dungeon_route_dynamic_targeting; // opt-in: retarget to the currently-highest-hp mob and credit priority damage to it + player_t* dungeon_route_priority_target; // runtime state: the current highest-hp mob when dynamic targeting is active bool challenge_mode; // if active, players will get scaled down to 620 and set bonuses are deactivated bool scale_itemlevel_down_only; // Items below the value of scale_to_itemlevel will not be scaled up. bool disable_set_bonuses; // Disables all set bonuses. diff --git a/engine/util/util.cpp b/engine/util/util.cpp index 8fb46a52719..6ea795097ad 100644 --- a/engine/util/util.cpp +++ b/engine/util/util.cpp @@ -2604,6 +2604,7 @@ const char* util::retarget_event_string( retarget_source event ) case retarget_source::ACTOR_INVULNERABLE: return "actor_invulnerable"; case retarget_source::ACTOR_VULNERABLE: return "actor_vulnnerable"; case retarget_source::SELF_ARISE: return "self_arise"; + case retarget_source::PRIORITY_CHANGE: return "priority_change"; default: return "unknown"; } }