Specials, slots and strikes
Each special lives in a slot with its own timer. Havoc adds slots, picks more disablers, and turns coordinated strikes from a rare event into a coin flip.
Slots
The director creates one slot per special allowed alive. Each slot gets a breed picked in advance and its own timer, and the slot timers are pushed a few metres apart so they do not all fire together. When a slot’s timer runs out and fewer specials are alive than the cap, the special spawns already aggroed, out of sight ahead of or behind the team.
Number of special slotsmanagers/pacing/specials_pacing/specials_pacing.lua:214
local max_alive_specials = math.ceil(template.max_alive_specials * self._max_alive_specials_multiplier + self._max_alive_specials_bonus)
local specials_slots = Script.new_array(max_alive_specials)Open managers/pacing/specials_pacing/specials_pacing.lua at line 214 on GitHubThe base cap comes from resistance, and Havoc’s more_alive_specials modifier adds 2 to 6 on top:
| Rank | Base cap | Havoc bonus | Alive at once | Timer per slot |
|---|---|---|---|---|
| 10 | 5 | +2 | 7 | 180-400 |
| 20 | 6 | +3 | 9 | 120-320 |
| 30 | 8 | +5 | 13 | 80-240 |
| 40 | 10 | +6 | 16 | 60-220 |
Havoc specials at resistance 5managers/pacing/specials_pacing/templates/havoc_specials_pacing_template.lua:308
max_alive_specials = 8,
max_spawn_group_offset_range = 6,
move_timer_when_challenge_rating_above = 20,
move_timer_when_challenge_rating_above_delay = 20,
move_timer_when_horde_active = true,
move_timer_when_monster_active = true,
move_timer_when_terror_event_active = true,
num_required_speed_running_checks = 2,
rushing_distance = 40,
spawn_failed_wait_time = 5,
speed_running_check_frequency = 5,
speed_running_required_challenge_rating = 8,
speed_running_required_distance = 10,
travel_distance_spawning = true,
timer_range = {
80,
240,
},
always_update_breeds = ALWAYS_UPDATE_AT_CHALLANGE_RATING_BREEDS,
max_of_same = HIGH_MAX_OF_SAME,
min_distances_from_target = DEFAULT_MIN_DISTANCES_FROM_TARGET,
breeds = DEFAULT_BREEDS,
disabler_target_alone_player_chance = DEFAULT_DISABLER_TARGET_ALONE_PLAYER_CHANCE,
num_allowed_disablers_per_alive_targets = DEFAULT_NUM_ALLOWED_DISABLED_PER_ALIVE_TARGETS,
disabler_override_duration = DEFAULT_DISABLER_OVERRIDE_DURATION,
foreshadow_stingers = DEFAULT_FORESHADOW_STINGERS,
foreshadow_stinger_timers = DEFAULT_FORESHADOW_STINGER_TIMERS,
optional_prefered_spawn_direction = DEFAULT_OPTIONAL_PREFERED_SPAWN_DIRECTION,
optional_mainpath_offset = DEFAULT_OPTIONAL_MAINPATH_OFFSET,
spawn_stingers = DEFAULT_SPAWN_STINGERS,Open managers/pacing/specials_pacing/templates/havoc_specials_pacing_template.lua at line 308 on GitHubHavoc: extra alive specialssettings/havoc_settings.lua:123
more_alive_specials = {
{
add_max_alive_specials = 2,
},
{
add_max_alive_specials = 3,
},
{
add_max_alive_specials = 4,
},
{
add_max_alive_specials = 5,
},
{
add_max_alive_specials = 6,
},Open settings/havoc_settings.lua at line 123 on GitHubThere is also a cap on the same breed alive at once. From R4 it is 2 hounds, 2 trappers, 4 mutants, 3 bombers, 3 grenadiers, 3 snipers and 2 flamers.
Max of the same special (R4+)managers/pacing/specials_pacing/templates/special_pacing_template_settings.lua:177
high_max_of_same = {
chaos_beast_of_nurgle = 1,
chaos_hound = 2,
chaos_plague_ogryn = 2,
chaos_poxwalker_bomber = 3,
chaos_spawn = 2,
cultist_flamer = 2,
cultist_grenadier = 2,
cultist_mutant = 4,
flamer = 2,Open managers/pacing/specials_pacing/templates/special_pacing_template_settings.lua at line 177 on GitHubDisablers per standing player
Hounds, trappers and mutants are disablers. Normally the number allowed alive depends on how many players are still standing, so a lone survivor faces at most one. Havoc replaces that table: at C5 the cap is 4 for any number of standing players, so the clutch player can face four disablers alone.
Havoc disabler cap per standing playermanagers/pacing/specials_pacing/templates/special_pacing_template_settings.lua:219
default_num_allowed_disabled_per_alive_targets = {
{
0,
0,
1,
1,
},
{
0,
1,
1,
2,
},
{
2,
2,
3,
3,
},
{
3,
3,
3,
3,
},
{
4,
4,
4,
4,
},
},Open managers/pacing/specials_pacing/templates/special_pacing_template_settings.lua at line 219 on GitHubCoordinated strikes and surges
When a special dies and no specials are left alive, the director rolls for a coordinated strike. On success, several slots get nearly identical timers, so 7-10 specials (at Havoc R5) arrive within a few seconds of each other. The strike can also start a surge: for 40-80 seconds every special that dies is replaced 5-13 seconds later.
| Strike chance | Surge chance | |
|---|---|---|
| Auric (R5) | 30% | 20% |
| Havoc 30 (R5) | 50% | 40% |
| Havoc 31+ (R6) | 60% | 45% |
Coordinated special strikesmanagers/pacing/specials_pacing/specials_pacing.lua:1199
SpecialsPacing._check_and_activate_coordinated_strike = function (self, template, current_special_slot)
local specials_slots = self._specials_slots
local t = Managers.time:time("gameplay")
if not self._surges_paused and self._coordinated_surge_duration and t <= self._coordinated_surge_duration then
local optional_coordinated_strike = true
local coordinated_surge_timer_range = template.coordinated_surge_timer_range
local min_timer_range, max_timer_range = coordinated_surge_timer_range[1], coordinated_surge_timer_range[2]
local coordinated_surge_timer = math.random_range(min_timer_range, max_timer_range)
self:_setup_specials_slot(specials_slots, current_special_slot, template, self._timer_modifier, nil, coordinated_surge_timer, nil, optional_coordinated_strike)
return true
end
if self._num_spawned_specials > 0 then
return false
end
for i = 1, self._max_alive_specials do
local specials_slot = specials_slots[i]
if specials_slot.coordinated_strike then
return falseOpen managers/pacing/specials_pacing/specials_pacing.lua at line 1199 on GitHubKilling the last special alive is the trigger. On rank 30+ it is a coin flip whether that kill starts a wave of seven to ten more. Before you snipe the last trapper, make sure the team is together, topped up and not mid-horde.
Sounds that warn you
Hounds, trappers and mutants play a stinger a few seconds or metres before they spawn: 5 for hounds and mutants, 4 for trappers. The same sound means the special is about to appear, not that it is already on someone.
Foreshadow stinger lead timesmanagers/pacing/specials_pacing/templates/special_pacing_template_settings.lua:10
special_pacing_template_settings.default_foreshadow_stinger_timers = {
chaos_hound = 5,
cultist_mutant = 5,
renegade_netgunner = 4,
}Open managers/pacing/specials_pacing/templates/special_pacing_template_settings.lua at line 10 on GitHubPunishments for splitting up
Three extra checks inject specials outside the normal timers:
- Rush prevention. If the front player or the last player is more than
rushing_distancefrom the next player (40 m at R5, 35 m at R6), a hound or mutant is sent at them. - Loner prevention (R4+). A player out of coherency for 6-10 seconds (at Havoc R5) gets a hound, mutant or trapper. This only fires while at least three players are standing.
- Speed-run prevention. If the lead player keeps gaining 10+ metres every 5-second check, a disabler is placed ahead of them even past the alive cap.
Rush preventionmanagers/pacing/specials_pacing/specials_pacing.lua:1301
SpecialsPacing._update_rush_prevention = function (self, target_side_id, template, t)
local rush_prevention_breeds = template.rush_prevention_breeds
if not rush_prevention_breeds or t < self._rush_prevention_cooldown then
return
end
if self._num_spawned_specials >= self._effective_max_alive_specials then
return
end
local ahead_unit, ahead_travel_distance = Managers.state.main_path:ahead_unit(target_side_id)
if not ahead_unit then
return
end
local behind_unit, behind_travel_distance = Managers.state.main_path:behind_unit(target_side_id)
local nav_world = self._nav_world
local side_system = Managers.state.extension:system("side_system")
local side = side_system:get_side(target_side_id)
local target_units = side.valid_player_units
local num_target_units = #target_units
local second_ahead_distance, second_behind_distance = 0, math.huge
if num_target_units == 1 then
if not self._extra_scrambler_cooldown or t > self._extra_scrambler_cooldown then
local player_unit = target_units[1]
local unit_data_extension = ScriptUnit.extension(player_unit, "unit_data_system")
local disabled_character_state_component = unit_data_extension:read_component("disabled_character_state")Open managers/pacing/specials_pacing/specials_pacing.lua at line 1301 on GitHubLoner preventionmanagers/pacing/specials_pacing/specials_pacing.lua:1401
SpecialsPacing._update_loner_prevention = function (self, target_side_id, template, t)
local loner_prevention_breeds = template.loner_prevention_breeds
if not loner_prevention_breeds or t < self._loner_prevention_cooldown then
return
end
if self._num_spawned_specials >= self._effective_max_alive_specials then
return
end
local side_system = Managers.state.extension:system("side_system")
local side = side_system:get_side(target_side_id)
local target_units = side.valid_player_units
local num_target_units = #target_units
local num_non_disabled_players = 0
table.clear(ALONE_TARGETS)
for i = 1, num_target_units do
local player_unit = target_units[i]
local unit_data_extension = ScriptUnit.extension(player_unit, "unit_data_system")
local character_state_component = unit_data_extension:read_component("character_state")
local requires_help = PlayerUnitStatus.requires_help(character_state_component)
if not requires_help then
num_non_disabled_players = num_non_disabled_players + 1
end
local coherency_extension = ScriptUnit.has_extension(player_unit, "coherency_system")
local num_units_in_coherency = coherency_extension and coherency_extension:num_units_in_coherency()
if num_units_in_coherency == 1 then
ALONE_TARGETS[#ALONE_TARGETS + 1] = player_unitOpen managers/pacing/specials_pacing/specials_pacing.lua at line 1401 on GitHubThe director actively hunts the player who is alone. Staying in coherency is not only about toughness regen. It is the difference between the normal timers and an extra disabler aimed straight at you.