Skip to content
Merged
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
152 changes: 128 additions & 24 deletions engine/class_modules/sc_hunter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ struct natures_ally_pet_t;
struct dire_critter_t;
struct dire_beast_t;
struct dark_hound_t;
struct dark_minion_t;
struct fenryr_t;
struct hati_t;
struct bear_t;
Expand Down Expand Up @@ -404,6 +405,7 @@ struct hunter_t final : public player_t
spawner::pet_spawner_t<pets::natures_ally_pet_t, hunter_t> natures_ally_pet;
spawner::pet_spawner_t<pets::dire_beast_t, hunter_t> dire_beast;
spawner::pet_spawner_t<pets::dark_hound_t, hunter_t> dark_hound;
spawner::pet_spawner_t<pets::dark_minion_t, hunter_t> dark_minion;
spawner::pet_spawner_t<pets::fenryr_t, hunter_t> fenryr;
spawner::pet_spawner_t<pets::hati_t, hunter_t> hati;
spawner::pet_spawner_t<pets::bear_t, hunter_t> bear;
Expand All @@ -413,6 +415,7 @@ struct hunter_t final : public player_t
natures_ally_pet( "natures_ally_pet", p ),
dire_beast( "dire_beast", p ),
dark_hound( "dark_hound", p ),
dark_minion( "dark_minion", p ),
fenryr( "fenryr", p ),
hati( "hati", p ),
bear( "bear", p ),
Expand Down Expand Up @@ -622,7 +625,7 @@ struct hunter_t final : public player_t

struct rppm_t
{
real_ppm_t* shadow_hounds;
real_ppm_t* corpsecaller;
real_ppm_t* shadow_surge;

real_ppm_t* let_fly;
Expand Down Expand Up @@ -996,7 +999,8 @@ struct hunter_t final : public player_t
spell_data_ptr_t soul_drinker;
spell_data_ptr_t bleak_powder;
spell_data_ptr_t bleak_powder_spell;
spell_data_ptr_t corpsecaller; // TODO Not implemented
spell_data_ptr_t corpsecaller;
spell_data_ptr_t corpsecaller_minion_summon;

spell_data_ptr_t ebon_bowstring;
spell_data_ptr_t through_the_eyes;
Expand Down Expand Up @@ -1968,6 +1972,62 @@ struct dark_hound_t final : public hunter_pet_t
void init_spells() override;
};

// ==========================================================================
// Dark Minion (Corpsecaller)
// ==========================================================================

struct dark_minion_t final : public pet_t
{
struct
{
action_t* shoot = nullptr;
action_t* blighted_arrow = nullptr;
} actions;

dark_minion_t( hunter_t* owner, util::string_view n = "dark_minion" )
: pet_t( owner->sim, owner, n, PET_HUNTER, true /* GUARDIAN */, true /* dynamic */ )
{
resource_regeneration = regen_type::DISABLED;
owner_coeff.ap_from_ap = 1;
}

void update_stats() override
{
/* 2026-01-25: Dark Minions only seem to inherit AP and Crit from the player.
TODO reconfirm before launch */
current_pet_stats.attack_power_from_ap = owner->composite_total_attack_power_by_type( owner->default_ap_type() ) * owner_coeff.ap_from_ap;
sim->print_debug( "{} refreshed AP from owner (ap={})", name(), composite_melee_attack_power() );

current_pet_stats.composite_melee_crit = owner->cache.attack_crit_chance();
current_pet_stats.composite_spell_crit = owner->cache.spell_crit_chance();
sim->print_debug( "{} refreshed Critical Strike from owner (crit={})", name(), current_pet_stats.composite_melee_crit, owner->cache.attack_crit_chance() );

this->adjust_dynamic_cooldowns();
}

void init_action_list() override
{
pet_t::init_action_list();

action_priority_list_t* def = get_action_priority_list( "default" );
def->add_action( "shoot" );
}

void arise() override
{
pet_t::arise();

/* 2026-01-25: Dark Minions don't cast Shoot for ~1.25s after they spawn.
Further log data required for more accurate range.
TODO reconfirm before launch */
actions.shoot->cooldown->start( owner->rng().range( 1000_ms, 1500_ms ) );
}

void init_spells() override;

action_t* create_action( util::string_view name, util::string_view options_str ) override;
};

// ==========================================================================
// Dire Critter
// ==========================================================================
Expand Down Expand Up @@ -3558,6 +3618,30 @@ struct potent_mutagen_t : public hunter_pet_attack_t<hunter_main_pet_base_t>
}
};

// Shoot (Dark Minion) =============================================================

struct shoot_t final : public ranged_attack_t
{
shoot_t( dark_minion_t* p ) : ranged_attack_t( "shoot", p, p->find_spell( 1264357 ) )
{
/* 2026-01-25: The pet stands around for a variable amount of time between casts.
Log testing puts it between 350ms and 650ms but longer testing required.
TODO reconfirm before launch */
cooldown->duration = rng().range( 350_ms, 650_ms );
}
};

// Blighted Arrow (Dark Minion) ====================================================

struct blighted_arrow_t final : public ranged_attack_t
{
blighted_arrow_t( pet_t* p ) : ranged_attack_t( "blighted_arrow", p, p->find_spell( 1264364 ) )
{
background = true;
aoe = -1;
}
};

} // end namespace pets::actions

fenryr_td_t::fenryr_td_t( player_t* target, fenryr_t* p ) : actor_target_data_t( target, p ), dots()
Expand Down Expand Up @@ -3687,6 +3771,24 @@ void dark_hound_t::init_spells()
main_hand_attack->school = SCHOOL_SHADOW;
}

void dark_minion_t::init_spells()
{
pet_t::init_spells();

actions.blighted_arrow = new actions::blighted_arrow_t( this );
}

action_t* dark_minion_t::create_action( util::string_view name, util::string_view options_str )
{
if ( name == "shoot" )
{
actions.shoot = new actions::shoot_t( this );
return actions.shoot;
}

return pet_t::create_action( name, options_str );
}

void fenryr_t::init_spells()
{
dire_critter_t::init_spells();
Expand Down Expand Up @@ -4934,27 +5036,18 @@ struct black_arrow_base_t : public kill_shot_base_t
{
struct black_arrow_dot_t : public hunter_ranged_attack_t
{
timespan_t dark_hound_duration;

black_arrow_dot_t( util::string_view n, hunter_t* p ) : hunter_ranged_attack_t( n, p, p->talents.black_arrow_dot )
{
background = dual = true;
hasted_ticks = false;

if ( p->talents.shadow_hounds.ok() )
dark_hound_duration = p->talents.shadow_hounds_summon->duration();
}

void tick( dot_t* d ) override
{
hunter_ranged_attack_t::tick( d );

if ( p()->talents.shadow_hounds.ok() && p()->rppm.shadow_hounds->trigger() )
{
p()->pets.dark_hound.spawn( dark_hound_duration );
if ( !p()->pets.dark_hound.active_pets().empty() )
p()->pets.dark_hound.active_pets().back()->buffs.beast_cleave->trigger( dark_hound_duration );
}
if ( p()->talents.corpsecaller_minion_summon.ok() && p()->rppm.corpsecaller->trigger() )
p()->pets.dark_minion.spawn( p()->talents.corpsecaller_minion_summon->duration() );
}
};

Expand Down Expand Up @@ -6073,6 +6166,9 @@ struct aimed_shot_t : public aimed_shot_base_t
p()->buffs.double_tap->expire();
}

for ( auto pet : p()->pets.dark_minion.active_pets() )
pet->actions.blighted_arrow->execute();

if ( lock_and_loaded )
{
p()->buffs.lock_and_load->decrement();
Expand Down Expand Up @@ -7276,7 +7372,7 @@ struct flamefang_pitch_t : public hunter_spell_t
.target( execute_state->target )
.duration( p()->talents.flamefang_pitch_data->duration() )
// No true pulse time exists in spell data for this spell
.pulse_time( p()->talents.flamefang_pitch_data->effectN( 1 ).time_value() * 1000 )
.pulse_time( timespan_t::from_seconds( p()->talents.flamefang_pitch_data->effectN( 1 ).base_value() ) )
.action( aoe ) );

// 2026-01-18: Grenade Juggler is refunding the unhasted cooldown of a bomb instead of a charge.
Expand Down Expand Up @@ -7558,13 +7654,8 @@ struct kill_command_t: public hunter_spell_t

if ( p()->state.fury_of_the_wyvern_extension < fury_of_the_wyvern.cap )
{
/* 2026-01-19: Extending Wyvern's Cry is entirely bugged and not working.
TODO reconfirm before launch */
if ( !p()->bugs )
{
p()->buffs.wyverns_cry->extend_duration( p(), fury_of_the_wyvern.extension );
p()->state.fury_of_the_wyvern_extension += fury_of_the_wyvern.extension;
}
p()->buffs.wyverns_cry->extend_duration( p(), fury_of_the_wyvern.extension );
p()->state.fury_of_the_wyvern_extension += fury_of_the_wyvern.extension;
}

p()->buffs.natures_ally_3->expire();
Expand Down Expand Up @@ -7927,7 +8018,12 @@ struct trueshot_t : public hunter_spell_t
}

if ( p()->talents.wailing_dead.ok() )
{
if ( p()->talents.corpsecaller_minion_summon.ok() )
p()->pets.dark_minion.spawn( p()->talents.corpsecaller_minion_summon->duration() );

p()->buffs.wailing_arrow->trigger();
}

if ( p()->talents.feathered_frenzy.ok() )
p()->trigger_eagles_mark( target, p()->talents.sentinel.ok(), true );
Expand Down Expand Up @@ -9007,6 +9103,7 @@ void hunter_t::init_spells()
talents.bleak_powder = find_talent_spell( talent_tree::HERO, "Bleak Powder" );
talents.bleak_powder_spell = talents.bleak_powder.ok() ? ( specialization() == HUNTER_MARKSMANSHIP ? find_spell( 467914 ) : find_spell( 472084 ) ) : spell_data_t::not_found();
talents.corpsecaller = find_talent_spell( talent_tree::HERO, "Corpsecaller" );
talents.corpsecaller_minion_summon = specialization() == HUNTER_MARKSMANSHIP && talents.corpsecaller.ok() ? find_spell( 1264345 ) : spell_data_t::not_found();

talents.ebon_bowstring = find_talent_spell( talent_tree::HERO, "Ebon Bowstring" );
talents.wailing_dead = find_talent_spell( talent_tree::HERO, "Wailing Dead" );
Expand Down Expand Up @@ -9735,7 +9832,7 @@ void hunter_t::init_rng()
{
player_t::init_rng();

rppm.shadow_hounds = get_rppm( "Shadow Hounds", talents.shadow_hounds );
rppm.corpsecaller = get_rppm( "Corpsecaller", talents.corpsecaller );
rppm.let_fly = get_rppm( "Let Fly", tier_set.mid_s1_mm_4pc );
}

Expand Down Expand Up @@ -9788,9 +9885,16 @@ void hunter_t::init_action_list()

if ( specialization() == HUNTER_SURVIVAL )
{
const weapon_e group = main_hand_weapon.group();
if ( group != WEAPON_2H && group != WEAPON_1H )
const weapon_e mh_group = main_hand_weapon.group();
if ( mh_group != WEAPON_2H && mh_group != WEAPON_1H && mh_group != WEAPON_DAGGER )
sim->error( "Player {} does not have a proper weapon at the Main Hand slot: {}.", name(), main_hand_weapon.type );

if ( const weapon_e oh_group = off_hand_weapon.group() )
{
if ( oh_group != WEAPON_1H && oh_group != WEAPON_DAGGER )
sim->error( "Player {} does not have a proper weapon at the Off Hand slot: {}.", name(), off_hand_weapon.type );
}

}

if ( action_list_str.empty() )
Expand Down
Loading