Triggers
Bosses, rituals and the twins are tied to distances along the route and fire when the player in front crosses them. Knowing where those lines are is most of Havoc 40.
A trigger is a distance, not a place
The director measures progress as travel distance along the main path, the line on every map page. Pre-placed spawns each carry a trigger distance. Every frame the game asks how far the player furthest ahead has travelled, and once that passes a trigger, the spawn happens. Where the other three players are does not matter.
Bosses spawn when the lead player passes their trigger distancemanagers/pacing/monster_pacing/monster_pacing.lua:677
local ahead_target_unit, ahead_travel_distance = Managers.state.main_path:ahead_unit(target_side_id)
if not ahead_target_unit then
return
end
local monsters_allowed = Managers.state.pacing:spawn_type_enabled("monsters")
if monsters_allowed then
local pacing_type = self._pacing_type
if pacing_type == pacing_types.default then
local monsters = self._monsters
local num_monsters = #monsters
for i = 1, num_monsters do
local monster = monsters[i]
local spawn_travel_distance = monster.travel_distance
if spawn_travel_distance <= ahead_travel_distance then
monster = self:_get_captain_faction(monster)
self:_spawn_monster(monster, ahead_target_unit, side_id)
table.remove(monsters, i)
break
end
endOpen managers/pacing/monster_pacing/monster_pacing.lua at line 677 on GitHubThat is the whole reason Havoc teams move as one block. The player who steps over a trigger line decides when the boss arrives, for everyone. Each map page draws the trigger points and lists them in order, so the team can agree where to stop, clear, top up and then step over together.
Bosses, captains and daemonhosts
When the mission loads, the director splits the route into sections and gives each section a handful of possible spawn points. It then picks sections at random and one point in each. A boss appears when the lead player is 40 m short of that point, so it spawns ahead of you rather than behind.
Monsters spawn 40 m aheadsettings/monster/monster_settings.lua:6
spawn_distance = 40,Open settings/monster/monster_settings.lua at line 6 on GitHubThere can never be more progression bosses than sections: the number rolled is clamped to the section count, and each pick removes its section. Every Havoc map in our captures has exactly three boss sections, so a run gets at most three bosses from progression, whatever the Havoc modifiers ask for. Captains use the same spawn points, spaced by distance, and daemonhosts have their own set.
At most one monster per route sectionmanagers/pacing/monster_pacing/monster_pacing.lua:185
if current_num_sections < num_to_spawn then
Log.warning("MonsterPacing", "Requested %s spawns for type %s but only had %s sections. Clamped.", num_to_spawn, spawn_type, current_num_sections)
num_to_spawn = current_num_sections
endOpen managers/pacing/monster_pacing/monster_pacing.lua at line 185 on GitHubThe map shows every possible point, because the game does not tell clients which ones were picked. The red band on the route covers the span of distances where a section’s boss can trigger. Inside a band, expect it at any moment.
Heinous Rituals
With the Heinous Rituals modifier, a ritual (eight chanting cultists summoning a daemonhost) appears at fixed ritual sites. Most maps use five sites per run, Clandestinum Gloriana four and Habs Block three, picked at least 30 m apart where the map allows it. A ritual site appears when the lead player is 55 m short of it, and it always brings a horde with it.
Heinous Rituals: sites per map, 55 m trigger, forced hordesettings/havoc/havoc_mutator_local_settings.lua:527
mutator_havoc_chaos_rituals = {
class = "scripts/managers/mutator/mutators/mutator_spawner",
force_horde_on_spawn = true,
trigger_distance = 55,
spawn_locations = MutatorSpawnerLocationSources.mission_provided_gizmo(),
num_to_spawn_per_mission = {
cm_archives = 5,
cm_habs = 3,
cm_raid = 5,
core_research = 4,
dm_forge = 5,
dm_propaganda = 5,
dm_rise = 5,
dm_stockpile = 5,
fm_armoury = 5,
fm_cargo = 5,
fm_resurgence = 5,
hm_cartel = 5,
hm_complex = 5,
hm_strain = 5,
km_enforcer = 5,
km_heresy = 5,
km_station = 5,
lm_cooling = 5,
lm_rails = 5,
lm_scavenge = 5,
spillway = 5,Open settings/havoc/havoc_mutator_local_settings.lua at line 527 on GitHubThe ritual clock does not start when the ritual appears. It starts at half speed once the lead player gets within 30 m of the site along the route, or anyone stands within 30 m of it. It switches to full speed, nine times faster, within 15 m, or as soon as anyone damages the daemonhost. It never slows back down.
Ritual speed-up distancessettings/breed/breed_actions/chaos/chaos_mutator_daemonhost_actions.lua:98
close_distance_offset = 15,
death_stinger = "wwise/events/minions/play_daemonhost_mutator_death_stinger",
effect_template_name = "chaos_mutator_daemonhost_shield",
far_distance_offset = 30,
half_time_multiplier = 9,Open settings/breed/breed_actions/chaos/chaos_mutator_daemonhost_actions.lua at line 98 on GitHubWhen the ritual clock starts and speeds upextension_systems/behavior/nodes/actions/bt_chaos_mutator_daemonhost_passive_action.lua:522
local within_close_distance = ahead_travel_distance > monster_travel_distance - close_distance_offset or closest_player_distance <= close_distance_offset
if within_close_distance or damage_override then
scratchpad.speed = "full"
return true
end
local within_far_distance = ahead_travel_distance > monster_travel_distance - far_distance_offset or closest_player_distance <= far_distance_offset
if within_far_distance then
scratchpad.speed = "half"
end
Open extension_systems/behavior/nodes/actions/bt_chaos_mutator_daemonhost_passive_action.lua at line 522 on GitHubThe clock is also multiplied by the number of ritualists still chanting, so each one you kill slows the summoning, and killing all of them ends it. While any scripted event is running, the clock stops completely.
The ritual clock: ritualists, terror events, half and full speedextension_systems/behavior/nodes/actions/bt_chaos_mutator_daemonhost_passive_action.lua:317
if multiplier == 0 and HEALTH_ALIVE[unit] then
self:_kill(scratchpad, action_data, unit)
end
if Managers.state.terror_event:num_active_events() > 0 then
return
end
_update_progress_bar(unit, breed, blackboard, scratchpad, action_data, t)
local half_time_multiplier = action_data.half_time_multiplier
local dt = Managers.time:delta_time("gameplay")
local current_speed = scratchpad.speed
if current_speed == "half" then
scratchpad.timer = scratchpad.timer + dt * (multiplier / half_time_multiplier)
scratchpad.health_timer = scratchpad.health_timer + dt * (multiplier / half_time_multiplier)
elseif current_speed == "full" then
scratchpad.timer = scratchpad.timer + dt * multiplier
scratchpad.health_timer = scratchpad.health_timer + dt * multiplier
endOpen extension_systems/behavior/nodes/actions/bt_chaos_mutator_daemonhost_passive_action.lua at line 317 on GitHubSo the advice players give, to stay back until the current fight is over, works because of distance, not luck. Below 30 m of route progress and 30 m of range, the ritual waits for you. Once you commit, commit together, and shoot ritualists before you touch the daemonhost.
The twins
From rank 16 the twins can ambush once per run, at one of a few fixed points per map. Like rituals, they trigger 55 m before their point and bring a horde.
Havoc twins: one ambush, 55 m triggersettings/havoc/havoc_mutator_local_settings.lua:570
mutator_havoc_twins_settings = {
class = "scripts/managers/mutator/mutators/mutator_spawner",
num_to_spawn = 1,
trigger_distance = 55,
spawn_locations = MutatorSpawnerLocationSources.prebaked_mission_locations(),Open settings/havoc/havoc_mutator_local_settings.lua at line 570 on GitHubEvent trigger zones
Scripted events are started differently: by trigger volumes in the level that fire when someone walks in. The volume hands control to the level’s own script, which starts the objective and its terror event. Our capture fired each volume once in a solo run and recorded what it started, and the map pages draw the zones that start an objective or an event.
A trigger volume hands control to the level's scriptextension_systems/trigger/trigger_actions/trigger_action_send_flow.lua:7
TriggerActionSendFlow.local_on_activate = function (self, unit)
TriggerActionSendFlow.super.local_on_activate(self, unit)
local volume_unit = self._volume_unit
Unit.set_flow_variable(volume_unit, "lua_trigger_activating_unit", unit)
Unit.flow_event(volume_unit, "lua_trigger_activated")
endOpen extension_systems/trigger/trigger_actions/trigger_action_send_flow.lua at line 7 on GitHubBefore a trigger line, the front player stops. The team clears what is around, refills ammo and toughness, and then crosses together. After a boss spawns, do not push forward: the next section’s trigger could be close behind it, and Havoc stacks whatever triggers on top of what is already alive.