AI director

Difficulty is two dials

Every spawn table in the game is indexed by two numbers, challenge and resistance. Havoc ranks just pick which row you get, and rank 30 reaches a row normal Auric never touches.

The mission board shows one difficulty, but the director reads two numbers:

  • Challenge (C) picks rows in tension, ramp-up, horde composition, monster count, melee aggression and disabler caps.
  • Resistance (R) picks rows in the specials, horde-timer and patrol-density tables.

A lookup is clamped to the table length, so asking for row 6 of a five-row table returns row 5.

Tables are indexed by challenge or resistancemanagers/difficulty/difficulty_manager.lua:62
DifficultyManager.get_table_entry_by_challenge = function (self, t)
  local challenge = self._challenge

  return t[math.min(#t, challenge)]
end

DifficultyManager.get_challenge = function (self)
  return self._challenge
end

DifficultyManager.get_resistance = function (self)
  return self._resistance
Open managers/difficulty/difficulty_manager.lua at line 62 on GitHub

What each Havoc rank maps to

Havoc rank Challenge Resistance Same as
1-10 3 3 Malice
11-20 4 4 Heresy
21-29 5 4 Damnation
30 5 5 Auric
31-40 5 6 nothing outside Havoc

The bracket code only goes up to R5. Ranks 16-29 add the mutator_increased_difficulty circumstance and ranks 30+ add mutator_highest_difficulty. The second one contains a mutator that adds one more point of resistance. That is why rank 30 plays at R5 and ranks 31-40 play at R6, a column that many specials, horde and patrol tables have but normal play can never reach.

Havoc rank to challenge / resistanceutilities/havoc.lua:118
  if havoc_rank <= 10 then
    difficulty.challenge = 3
    difficulty.resistance = 3
  elseif havoc_rank >= 11 and havoc_rank <= 20 then
    difficulty.challenge = 4
    difficulty.resistance = 4
  elseif havoc_rank >= 21 and havoc_rank <= 30 then
    difficulty.challenge = 5
    difficulty.resistance = 4
  else
    difficulty.challenge = 5
    difficulty.resistance = 5
  end
Open utilities/havoc.lua at line 118 on GitHub
Rank 16+ and 30+ difficulty circumstancesutilities/havoc.lua:89
    increased_difficulty = "mutator_increased_difficulty"
    chosen_circumstances = chosen_circumstances .. ":" .. increased_difficulty
  elseif havoc_rank >= 30 then
    increased_difficulty = "mutator_highest_difficulty"
    chosen_circumstances = chosen_circumstances .. ":" .. increased_difficulty
  end

  local chosen_modifiers = ""
  local current_modifer_level = HavocModifierConfig[havoc_rank]
  local index = 1
Open utilities/havoc.lua at line 89 on GitHub
Rank 30+: +1 resistancesettings/mutator/templates/mutator_modify_pacing_templates.lua:250
  mutator_add_resistance = {
    class = "scripts/managers/mutator/mutators/mutator_modify_pacing",
    init_modify_pacing = {
      modify_resistance = 1,
    },
  },
Open settings/mutator/templates/mutator_modify_pacing_templates.lua at line 250 on GitHub

Rank 30 is a real wall, not a cosmetic step. The specials table jumps from 6 to 8 alive, timers drop from 120-320 to 80-240, the coordinated strike chance doubles to 50%, and quiet phases disappear (see the next chapter). Treat 29 to 30 like moving up a whole difficulty.

On top of these dials, every rank adds a stack of modifiers: tougher elites, more specials alive, faster hordes and so on. The rank calculator shows the exact set for any rank.