local timers = require("level.timer")
local class = require("core.class")
local profile = require("core.profile")
local runlistlib = require("core.runlist")
local thunk = require("core.thunk")
local savelib = require("core.save")
local level_run_list = runlistlib.RunList.New()
local active_encounters = {}
_G.Gbl_EncounterList = {}
local unleashUpdateRate = 180
local DEFAULT_LOD_LOW_DISTANCE = 80
local DEFAULT_LOD_HIGH_DISTANCE = 120
local lastPrestreamEncounterHash
thunk.Install("OnUpdate", function(lvl)
  level_run_list:Update()
end)
local Encounter = class.Class("Encounter")
local repopulationTimerDefault = 30
function Encounter:EnableUpdate()
  level_run_list:add(self)
end
function Encounter:DisableUpdate()
  level_run_list:remove(self)
end
function Encounter:AddToTracking()
  if self.completed then
    return
  end
  for i = 1, #active_encounters do
    if active_encounters[i] ~= nil and active_encounters[i].hashid == self.hashid then
      return
    end
  end
  table.insert(active_encounters, self)
end
function Encounter:RemoveFromTracking()
  for i = #active_encounters, 1, -1 do
    if active_encounters[i] == nil then
      table.remove(active_encounters, i)
    elseif active_encounters[i].hashid == self.hashid then
      table.remove(active_encounters, i)
      return
    end
  end
end
function Encounter:CheckForAIQueuedSpawns()
  local queuedSpawn = game.Encounters.GetAIQueuedEncounterSpawn(self.Name)
  if queuedSpawn ~= nil then
    local enemy = queuedSpawn.spawnedEnemy
    local wave = queuedSpawn.wave
    local element = queuedSpawn.element
    self:AddAIRequestedEnemyToEncounter(enemy, wave, element)
  end
end
function Encounter:UpdateEnemyUnleashRequests()
  self.timeUntilUnleashUpdate = self.timeUntilUnleashUpdate - 1
  local leashZoneExitTriggered = game.Encounters.GetLeashZoneExitTriggered(self.Name)
  if self.useUnleashing == true and self.timeUntilUnleashUpdate < 0 and leashZoneExitTriggered then
    if game.Encounters.IsPlayerInEncounterLeashZone(self.Name) == true or game.Combat.GetCombatStatus() == false then
      game.Encounters.ReleashUnleashedEncounterEnemies(self.Name)
      self.unleashList = {}
      self.timeUntilUnleashUpdate = unleashUpdateRate
      return
    end
    local unleashTotalBudgetRemaining = game.Encounters.GetGlobalMaxUnleashedAllowed() - game.Encounters.GetUnleashEncounterEnemyCount()
    if game.Encounters.GetEncounterMinUnleashedOverride then
      local minOverride = game.Encounters.GetEncounterMinUnleashedOverride()
      if -1 < minOverride then
        self.minUnleashCount = minOverride
      end
    end
    if game.Encounters.GetEncounterMaxUnleashedOverride then
      local maxOverride = game.Encounters.GetEncounterMaxUnleashedOverride()
      if -1 < maxOverride then
        self.maxUnleashCount = maxOverride
      end
    end
    if 0 < unleashTotalBudgetRemaining then
      local numUnleashed = #self.unleashList
      if numUnleashed < self.minUnleashCount then
        local numToUnleash = self.minUnleashCount - numUnleashed
        if unleashTotalBudgetRemaining < numToUnleash then
          numToUnleash = unleashTotalBudgetRemaining
        end
        self:UnleashEnemies(nil, numToUnleash)
        unleashTotalBudgetRemaining = unleashTotalBudgetRemaining - numToUnleash
      end
      if 0 < unleashTotalBudgetRemaining and numUnleashed < self.maxUnleashCount and game.Encounters.GetLeashAbuseCount(self.Name) >= game.Encounters.GetLeashAbuseThreshold() then
        self:UnleashEnemies(nil, 1)
      end
    end
    self.timeUntilUnleashUpdate = unleashUpdateRate
  end
end
function Encounter:UpdatePrestreamingRequest()
  local numRequests = #self.prestreamRequests
  if numRequests == 0 then
    return
  end
  if self.repopulateEncounter == true then
    local obj = self.prestreamRequests[1].object
    if obj ~= nil and obj.LuaObjectScript.IsInitialized() then
      local completedListStatus = self:VerifyMustHaveCompletedList()
      local canStart = game.Encounters.CanSpawnRepopulationEncounter(self.Name) and completedListStatus
      if not canStart then
        self.prestreamRequests = {}
        numRequests = 0
      end
    end
  end
  local numToPrestream = 6
  local isInLoadingState = false
  if game.Encounters.IsInLoadingState and game.Encounters.IsInLoadingState() then
    isInLoadingState = true
  end
  for i = numRequests, 1, -1 do
    local request = self.prestreamRequests[i]
    local obj = request.object
    if not (obj ~= nil and obj.LuaObjectScript.IsInitialized()) then
      break
    end
    do
      local success = obj.LuaObjectScript.PrestreamEnemy({
        locator = request.locator,
        spawnWad = request.spawnWad,
        powerLevel = request.powerLevel,
        encounter = self.Name
      })
      if not success then
        break
      end
      numToPrestream = numToPrestream - 1
      table.remove(self.prestreamRequests, i)
      if numToPrestream ~= 0 or isInLoadingState then
      else
        do break end
        goto lbl_90
        break
      end
    end
    ::lbl_90::
  end
end
function Encounter:SetUnleashing(minUnleashCount, maxUnleashCount)
  self.timeUntilUnleashUpdate = 0
  self.useUnleashing = true
  self.minUnleashCount = minUnleashCount
  if maxUnleashCount < minUnleashCount then
    maxUnleashCount = minUnleashCount
  end
  self.maxUnleashCount = maxUnleashCount
  self.unleashList = {}
end
function Encounter:UnleashEnemyHelper(enemy)
  table.insert(self.unleashList, enemy)
end
function Encounter:UnleashEnemies(targetEnemy, numberToRelease)
  local enemiesToRelease = {}
  local playerPosition = game.Player.FindPlayer():GetGameObject():GetWorldPosition()
  for wave = 1, #self.waves do
    if self.waves[wave].waveStarted then
      for i = 1, #self.waves[wave] do
        for j = 1, #self.waves[wave][i].livingEnemies do
          local enemy = self.waves[wave][i].livingEnemies[j]
          local alreadyUnleashed = false
          for ul = 1, #self.unleashList do
            if enemy == self.unleashList[ul] then
              alreadyUnleashed = true
              break
            end
          end
          if not alreadyUnleashed and enemy ~= nil then
            if targetEnemy ~= nil and self.waves[wave][i].livingEnemies[j] == targetEnemy then
              table.insert(enemiesToRelease, 1, {enemy, 0})
            else
              local distToPlayer = playerPosition:Distance(enemy:GetWorldPosition())
              for n = 1, numberToRelease do
                if n <= #enemiesToRelease then
                  if distToPlayer < enemiesToRelease[n][2] then
                    table.insert(enemiesToRelease, n, {enemy, distToPlayer})
                    break
                  end
                else
                  table.insert(enemiesToRelease, n, {
                    enemy,
                    distToPlayer,
                    wave,
                    i,
                    j
                  })
                  break
                end
              end
            end
          end
        end
      end
    end
  end
  for i = 1, #enemiesToRelease do
    if numberToRelease < i then
      break
    end
    game.Encounters.ReportEnemyUnleashed(enemiesToRelease[i][1])
    self:UnleashEnemyHelper(enemiesToRelease[i][1])
  end
end
function Encounter:OnWadLoaded(level, newLevel)
  if not self.completed and self.waitForSpawnWad then
    local wadName = string.gsub(newLevel.Name, "WAD_", "")
    if self:Validate_Wad(wadName) then
      self:Update_Wad_State(wadName, true)
      if self:Check_WadList_Loaded() then
        self.waitForSpawnWad = false
        self:Start()
      end
    end
  end
end
function Encounter:OnWadUnloaded(level, newLevel)
  if self.started and not self.completed then
    if newLevel.Name == self.level.Name then
      self:ForceReset()
    end
    local wadName = string.gsub(newLevel.Name, "WAD_", "")
    if self:Validate_Wad(wadName) then
      if self.startZoneMonitor ~= nil and self.startZoneMonitor.LuaObjectScript == nil then
        self.startZoneMonitor:Start()
      end
      if self.alertZoneMonitor ~= nil and self.alertZoneMonitor.LuaObjectScript == nil then
        self.alertZoneMonitor:Start()
      end
      self:Update_Wad_State(wadName, false)
      self:ForceReset()
    end
  end
end
function Encounter:Validate_Wad(wad)
  if self.spawnWadNames[wad] ~= nil then
    return true
  end
  return false
end
function Encounter:Check_WadList_Loaded()
  for _, v in pairs(self.spawnWadNames) do
    if v == false then
      return false
    end
  end
  return true
end
function Encounter:Update_Wad_State(wad, loaded)
  self.spawnWadNames[wad] = loaded
end
function Encounter:AddDependents()
  for i = 1, #self.mustHaveCompletedList do
    for j = 1, #active_encounters do
      if active_encounters[j] ~= nil and self.mustHaveCompletedList[i] == active_encounters[j].hashid and not active_encounters[j].completed then
        table.insert(active_encounters[j].dependents, self.hashid)
      end
    end
  end
end
local NewEncounter = function(level, Name, argsTable)
  local myEncounter = Encounter.New()
  myEncounter.Name = Name
  if string.len(myEncounter.Name) > 63 then
    engine.Error("Encounter name \"", myEncounter.Name, "\" exceeds max number of characters by ( ", string.len(myEncounter.Name) - 63, " ). Please shorten the name")
  end
  if _G.Gbl_EncounterList[myEncounter.Name] ~= nil then
    engine.Error("Encounter is being created twice or two encounters share the same name! - \"", myEncounter.Name, "\"")
  else
    _G.Gbl_EncounterList[myEncounter.Name] = true
  end
  myEncounter.id = myEncounter.Name
  myEncounter.hashid = engine.Hash(Name)
  myEncounter.level = level
  myEncounter.player = game.Player.FindPlayer()
  myEncounter.player_bboard = myEncounter.player:GetPrivateBlackboard()
  myEncounter.started = false
  myEncounter.alertedEnemies = false
  myEncounter.combatStatusLocked = false
  myEncounter.inCombat = false
  myEncounter.previouslyStarted = false
  myEncounter.completed = false
  myEncounter.autoResetOnReload = false
  myEncounter.loadCheck = true
  myEncounter.waitForSpawnWad = false
  myEncounter.loadHooksInstalled = false
  myEncounter.spawnWadNames = {}
  myEncounter.alertZoneMonitor = nil
  myEncounter.startZoneMonitor = nil
  myEncounter.debugInputAllowed = false
  myEncounter.waves = {}
  myEncounter.queuedData = {
    deque_active = true,
    queued_Calls = {},
    queued_Waves = {}
  }
  myEncounter.availableLocators = {}
  myEncounter.currentWave = 0
  myEncounter.stopped = false
  myEncounter.spawnConditionsRequireUpdatePolling = false
  myEncounter.lookAtMonitor_Start = nil
  myEncounter.OnSpawnCallbacks = nil
  myEncounter.OnDeathCallbacks = nil
  myEncounter.OnStartCallbacks = nil
  myEncounter.OnCompleteCallbacks = nil
  myEncounter.OnCombatStartCallbacks = nil
  myEncounter.useUnleashing = false
  myEncounter.abuseThreshhold = 0
  myEncounter.minUnleashCount = 0
  myEncounter.maxUnleashCount = 0
  myEncounter.unleashList = {}
  myEncounter.timeUntilUnleashUpdate = 0
  myEncounter.leashZone = ""
  myEncounter.lodLow = DEFAULT_LOD_LOW_DISTANCE
  myEncounter.lodHigh = DEFAULT_LOD_HIGH_DISTANCE
  myEncounter.repopulateEncounter = false
  myEncounter.mustHaveCompletedList = {}
  myEncounter.dependents = {}
  myEncounter.prestreamRequests = {}
  if not myEncounter.id then
    engine.Warning("Encounter does not have a name. Encounter state will not be checkpointed!")
  else
    myEncounter:MarkForSave()
  end
  local debugName = myEncounter.Name or "Unnamed Encounter"
  myEncounter.showDebug = engine.VFSInt.New(debugName .. " - Show Debug", 0, 2)
  if argsTable ~= nil then
    if argsTable.DebugX ~= nil then
      myEncounter.debugX = argsTable.DebugX
    end
    if argsTable.DebugY ~= nil then
      myEncounter.debugY = argsTable.DebugY
    end
    if argsTable.ShowDebug ~= nil and argsTable.ShowDebug == true then
      myEncounter.showDebug.value = 1
    end
    if argsTable.AutoResetOnReload then
      myEncounter.autoResetOnReload = argsTable.AutoResetOnReload
    end
    if argsTable.LoadCheck ~= nil then
      myEncounter.loadCheck = argsTable.LoadCheck
    end
    myEncounter.idleMusic = "SND_MX_TRANS_TO_IDLE"
    if argsTable.StartMusic ~= nil then
      myEncounter.startMusic = argsTable.StartMusic
    end
    if argsTable.StopMusic ~= nil then
      myEncounter.stopMusic = argsTable.StopMusic
    end
    if argsTable.CheckpointOnComplete then
      myEncounter.checkpointOnComplete = argsTable.CheckpointOnComplete
    end
    if argsTable.spawnWad ~= nil then
      myEncounter.spawnWad = argsTable.spawnWad
    end
    if argsTable.UnleashMin then
      myEncounter:SetUnleashing(argsTable.UnleashMin, argsTable.UnleashMin)
    end
    if argsTable.UnleashMax then
      myEncounter:SetUnleashing(myEncounter.minUnleashCount, argsTable.UnleashMax)
    end
    if argsTable.LeashZone then
      myEncounter.leashZone = argsTable.LeashZone
    end
    if argsTable.LodLow then
      myEncounter.lodLow = argsTable.LodLow
    end
    if argsTable.LodHigh then
      myEncounter.lodHigh = argsTable.LodHigh
    end
    if argsTable.RepopulationEncounter then
      myEncounter.repopulateEncounter = argsTable.RepopulationEncounter
    end
    if argsTable.MustHaveCompletedList then
      for mhi = 1, #argsTable.MustHaveCompletedList do
        table.insert(myEncounter.mustHaveCompletedList, engine.Hash(argsTable.MustHaveCompletedList[mhi]))
      end
    end
    if argsTable.CheckpointOverrideObject ~= nil then
      if not myEncounter.checkpointOnComplete then
        engine.Warning("Encounter " .. debugName .. " has a CheckpointOverrideObject defined, but CheckpointOnComplete is not set to true.")
      elseif argsTable.CheckpointOverrideObject.IsRefNode then
        myEncounter.checkpointOverrideObject = argsTable.CheckpointOverrideObject.Child
      else
        myEncounter.checkpointOverrideObject = argsTable.CheckpointOverrideObject
      end
    end
  end
  myEncounter:AddToTracking()
  myEncounter:AddDependents()
  myEncounter:EnableUpdate()
  return myEncounter
end
local OnEncounterSaveCallback = function(level, encounter)
  local saveInfo = savelib.GetSaveState(level, "__encounterState", encounter.id)
  saveInfo.completed = encounter.completed and not encounter.repopulateEncounter
  saveInfo.previouslyStarted = encounter.previouslyStarted
end
local OnEncounterLoadCallback = function(level, encounter)
  local saveInfo = savelib.GetSaveState(level, "__encounterState", encounter.id)
  encounter.completed = saveInfo.completed or false
  encounter.previouslyStarted = saveInfo.previouslyStarted or false
  if encounter.repopulateEncounter == true and game.Encounters.CanSpawnRepopulationEncounter(encounter.Name) == true and encounter:VerifyMustHaveCompletedList() == true then
    encounter:EnableAllZoneMonitors()
  end
  if encounter:WasRunning() then
    if encounter.autoResetOnReload then
      encounter:Start()
    end
  elseif encounter.completed then
    encounter:RemoveFromTracking()
    encounter:DisableAllZoneMonitors()
  end
end
function Encounter:MarkForSave()
  savelib.AddSaveObjectCallback(self.level, self, OnEncounterSaveCallback)
  savelib.AddLoadObjectCallback(self.level, self, OnEncounterLoadCallback)
  savelib.CreateSaveState(self.level, "__encounterState", self.id)
end
function Encounter:AddWave(args, queueCall)
  if queueCall == nil then
    queueCall = self.queuedData.deque_active
  end
  if queueCall == true then
    table.insert(self.queuedData.queued_Waves, 1, args)
  else
    local nextWave = {
      requiredWaveCompletion = args.requiredWaveCompletion or 100,
      waveComplete = false,
      waveStarted = false,
      waveStopped = false,
      requireCompletionOfAllPriorWaves = args.requireCompletionOfAllPriorWaves,
      triggerWaveFromScript = args.triggerWaveFromScript or false,
      spawnPattern = args.spawnPattern or "Random",
      markerID = args.markerID or nil,
      pointMarkerID = args.pointMarkerID or nil,
      infiniteSpawning = args.infiniteSpawning or false,
      recurring = args.recurring or false,
      recurringCooldown = args.recurringCooldown or 0,
      timeBetweenWaves = args.timeBetweenWaves or 0,
      timeBeforeNextWave = args.timeBeforeNextWave or 0,
      enableSpawnersOnWaveComplete = args.enableSpawnersOnWaveComplete or false,
      alertedEnemies = false,
      prioritizeSpawnersInRange = args.prioritizeSpawnersInRange or nil,
      prioritizeOnScreenSpawners = args.prioritizeOnScreenSpawners or false,
      prioritizeOffScreenSpawners = args.prioritizeOffScreenSpawners or false,
      requireSpawnersInRange = args.requireSpawnersInRange or nil,
      requireOnScreenSpawners = args.requireOnScreenSpawners or false,
      requireOffScreenSpawners = args.requireOffScreenSpawners or false,
      lastSpawnerUsed = nil,
      powerLevel = args.powerLevel
    }
    if args.spawnWad ~= nil then
      nextWave.spawnWad = args.spawnWad
    end
    if nextWave.requireSpawnersInRange ~= nil or nextWave.requireOnScreenSpawners == true or nextWave.requireOffScreenSpawners == true then
      self.spawnConditionsRequireUpdatePolling = true
    end
    if nextWave.prioritizeSpawnersInRange or nextWave.prioritizeOnScreenSpawners or nextWave.prioritizeOffScreenSpawners then
      nextWave.prioritizedSpawnConditions = true
    end
    for i = 1, #args do
      local nextElement = {}
      if type(args[i].spawners) == "string" then
        local spawnerStringHasAsterisk = string.find(args[i].spawners, "*")
        if spawnerStringHasAsterisk ~= nil then
          nextElement.spawners = self.level:FindGameObjects(args[i].spawners)
        else
          nextElement.spawners = self.level:FindGameObjects(tostring(args[i].spawners .. "*"))
        end
      elseif type(args[i].spawners) == "table" then
        nextElement.spawners = args[i].spawners
      end
      for _, spawner in pairs(nextElement.spawners) do
        if spawner.LuaObjectScript == nil then
          engine.Error("Encounter is trying to use a regular object as a spawner - Object: '", spawner:GetName(), "'. This is most likely due to a naming conflict (", self.level.Name, ").")
        end
      end
      if type(args[i].spawnLocators) == "string" then
        local spawnerStringHasAsterisk = string.find(args[i].spawnLocators, "*")
        if spawnerStringHasAsterisk ~= nil then
          nextElement.spawnLocators = self.level:FindGameObjects(args[i].spawnLocators)
        else
          nextElement.spawnLocators = self.level:FindGameObjects(tostring(args[i].spawnLocators .. "*"))
        end
      elseif type(args[i].spawnLocators) == "table" then
        nextElement.spawnLocators = args[i].spawnLocators
      end
      if nextElement.spawnLocators ~= nil then
        for j = 1, #nextElement.spawnLocators do
          self.availableLocators[nextElement.spawnLocators[j]:GetName()] = true
        end
      end
      nextElement.useSpawnLocatorsOnly = args[i].useSpawnLocatorsOnly or nil
      if type(args[i].maxActive) == "table" then
        nextElement.maxActive = math.random(args[i].maxActive[1], args[i].maxActive[2]) or 1
      elseif type(args[i].maxActive) == "number" then
        nextElement.maxActive = args[i].maxActive or 1
      else
        nextElement.maxActive = 1
      end
      if type(args[i].totalSpawns) == "table" then
        nextElement.totalSpawns = math.random(args[i].totalSpawns[1], args[i].totalSpawns[2]) or 1
      elseif type(args[i].totalSpawns) == "number" then
        nextElement.totalSpawns = args[i].totalSpawns or 1
      else
        nextElement.totalSpawns = 1
      end
      if type(args[i].initialSpawnAmount) == "table" then
        nextElement.initialSpawnAmount = math.random(args[i].initialSpawnAmount[1], args[i].initialSpawnAmount[2]) or 0
      elseif type(args[i].initialSpawnAmount) == "number" then
        nextElement.initialSpawnAmount = args[i].initialSpawnAmount or 0
      else
        nextElement.initialSpawnAmount = 0
      end
      if type(args[i].minActive) == "table" then
        nextElement.minActive = math.random(args[i].minActive[1], args[i].minActive[2]) or 0
      elseif type(args[i].minActive) == "number" then
        nextElement.minActive = args[i].minActive or 0
      else
        nextElement.minActive = 0
      end
      nextElement.initialSpawnCooldownType = args[i].initialSpawnCooldownType
      if nextElement.initialSpawnCooldownType == nil then
        if type(args[i].initialSpawnCooldown) == "table" then
          if #args[i].initialSpawnCooldown == 1 then
            nextElement.initialSpawnCooldownType = "SingleValue"
          elseif #args[i].initialSpawnCooldown == 2 then
            nextElement.initialSpawnCooldownType = "RandomRange"
          else
            nextElement.initialSpawnCooldownType = "Sequential"
          end
        elseif type(args[i].initialSpawnCooldown) == "number" then
          nextElement.initialSpawnCooldownType = "SingleValue"
        end
      end
      nextElement.GetInitialSpawnCooldown = nil
      if nextElement.initialSpawnCooldownType == "SingleValue" then
        if type(args[i].initialSpawnCooldown) == "table" then
          nextElement.initialSpawnCooldown = args[i].initialSpawnCooldown[1] or 0
        elseif type(args[i].initialSpawnCooldown) == "number" then
          nextElement.initialSpawnCooldown = args[i].initialSpawnCooldown or 0
        end
        function nextElement.GetInitialSpawnCooldown()
          return nextElement.initialSpawnCooldown
        end
      elseif nextElement.initialSpawnCooldownType == "RandomRange" then
        nextElement.initialSpawnCooldown = args[i].initialSpawnCooldown or 0
        function nextElement.GetInitialSpawnCooldown()
          return math.random(nextElement.initialSpawnCooldown[1] * 1000, nextElement.initialSpawnCooldown[2] * 1000) / 1000
        end
      elseif nextElement.initialSpawnCooldownType == "Sequential" then
        nextElement.initialSpawnCooldown = args[i].initialSpawnCooldown or 0
        nextElement.initialSpawnCooldownIndex = 0
        function nextElement.GetInitialSpawnCooldown()
          if nextElement.initialSpawnCooldownIndex < #nextElement.initialSpawnCooldown then
            nextElement.initialSpawnCooldownIndex = nextElement.initialSpawnCooldownIndex + 1
          else
            nextElement.initialSpawnCooldownIndex = 1
          end
          return nextElement.initialSpawnCooldown[nextElement.initialSpawnCooldownIndex]
        end
      else
        nextElement.initialSpawnCooldown = 0
      end
      nextElement.spawnCooldownType = args[i].spawnCooldownType
      if nextElement.spawnCooldownType == nil then
        if type(args[i].spawnCooldown) == "table" then
          if #args[i].spawnCooldown == 1 then
            nextElement.spawnCooldownType = "SingleValue"
          elseif #args[i].spawnCooldown == 2 then
            nextElement.spawnCooldownType = "RandomRange"
          else
            nextElement.spawnCooldownType = "Sequential"
          end
        elseif type(args[i].spawnCooldown) == "number" then
          nextElement.spawnCooldownType = "SingleValue"
        end
      end
      nextElement.GetSpawnCooldown = nil
      if nextElement.spawnCooldownType == "SingleValue" then
        if type(args[i].spawnCooldown) == "table" then
          nextElement.spawnCooldown = args[i].spawnCooldown[1] or 0
        elseif type(args[i].spawnCooldown) == "number" then
          nextElement.spawnCooldown = args[i].spawnCooldown or 0
        end
        function nextElement.GetSpawnCooldown()
          return nextElement.spawnCooldown
        end
      elseif nextElement.spawnCooldownType == "RandomRange" then
        nextElement.spawnCooldown = args[i].spawnCooldown or 0
        function nextElement.GetSpawnCooldown()
          return math.random(nextElement.spawnCooldown[1] * 1000, nextElement.spawnCooldown[2] * 1000) / 1000
        end
      elseif nextElement.spawnCooldownType == "Sequential" then
        nextElement.spawnCooldown = args[i].spawnCooldown or 0
        nextElement.spawnCooldownIndex = 0
        function nextElement.GetSpawnCooldown()
          if nextElement.spawnCooldownIndex < #nextElement.spawnCooldown then
            nextElement.spawnCooldownIndex = nextElement.spawnCooldownIndex + 1
          else
            nextElement.spawnCooldownIndex = 1
          end
          return nextElement.spawnCooldown[nextElement.spawnCooldownIndex]
        end
      end
      if args[i].spawnWad ~= nil then
        nextElement.spawnWad = args[i].spawnWad
      end
      if args[i].powerLevel ~= nil then
        nextElement.powerLevel = args[i].powerLevel
      end
      nextElement.markerID = args[i].markerID or nil
      nextElement.pointMarkerID = args[i].pointMarkerID or nil
      nextElement.livingEnemies = {}
      nextElement.numEnemiesAlive = 0
      nextElement.spawnCooldownTimer = nil
      nextElement.enemiesSpawned = 0
      nextElement.enemiesKilled = 0
      nextElement.spawnersAvailable = false
      nextElement.disableSpawnerPostSpawn = args[i].disableSpawnerPostSpawn or false
      if args[i].allowUnleashing == nil then
        args[i].allowUnleashing = self.useUnleashing
      else
        nextElement.allowUnleashing = args[i].allowUnleashing
      end
      nextWave[#nextWave + 1] = nextElement
      if 1 > #nextElement.spawners then
        do
          local spawnerName = ""
          if type(args[i].spawners) == "string" then
            spawnerName = " named '" .. args[i].spawners .. "'"
          end
          engine.Warning("No spawners found" .. spawnerName .. ". Double-check that the name of your spawner(s) match in Maya and the Encounter script!")
        end
      end
    end
    self.waves[#self.waves + 1] = nextWave
    if #self.waves == 1 then
      for i = 1, #nextWave do
        self:PrestreamEnemy(1, i)
      end
    end
  end
end
function Encounter:VerifyMustHaveCompletedList()
  for i = 1, #self.mustHaveCompletedList do
    for j = 1, #active_encounters do
      if active_encounters[j] ~= nil and self.mustHaveCompletedList[i] == active_encounters[j].hashid and not active_encounters[j].completed then
        return false
      end
    end
  end
  return true
end
function Encounter:StartPrestreamingSubsequentWaves()
  local numWaves = #self.waves
  if 1 < numWaves then
    for w = 1, numWaves do
      local elements = self.waves[w]
      for e = 1, #elements do
        self:PrestreamEnemy(w, e)
      end
    end
  end
end
function Encounter:Start()
  if game.IsSpawnEnemiesEnabled() == false then
    self:SetComplete()
    return
  end
  if self.queuedData.deque_active then
    self.queuedData.queued_Calls[#self.queuedData.queued_Calls + 1] = function()
      self:Start()
    end
    return
  end
  local repopulationEncounter = self.repopulateEncounter or false
  local completedListStatus = self:VerifyMustHaveCompletedList()
  local canStart = not self.completed and (self.started == false or self.stopped) and completedListStatus
  if repopulationEncounter then
    canStart = self:IsRunning() == false and game.Encounters.CanSpawnRepopulationEncounter(self.Name) and completedListStatus
  end
  if canStart then
    self:AddToTracking()
    self:StartPrestreamingSubsequentWaves()
    if self.loadCheck then
      self:RunLoadChecks()
    end
    if self.loadHooksInstalled == false then
      self.loadHooksInstalled = true
      thunk.Install("OnWadLoaded", function(...)
        self:OnWadLoaded(...)
      end)
      thunk.Install("OnWadUnloaded", function(...)
        self:OnWadUnloaded(...)
      end)
    end
    self:UpdateSpawnWadStatus()
    if self:Check_WadList_Loaded() == false then
      self.waitForSpawnWad = true
      return
    end
    if self.alertZoneMonitor ~= nil and self.alertZoneMonitor.LuaObjectScript == nil then
      self.alertZoneMonitor:Start()
    end
    if game.Encounters.SetRepopulationTimer then
      game.Encounters.SetRepopulationTimer(repopulationTimerDefault)
      game.Encounters.ReportEncounterStarted(self.Name, self.leashZone, #self.waves, false, self.minUnleashCount, self.maxUnleashCount, self.repopulateEncounter)
    else
      game.Encounters.ReportEncounterStarted(self.Name, self.leashZone, #self.waves, false, self.minUnleashCount, self.maxUnleashCount)
    end
    game.Encounters.ReportEncounterLODRangeValues(self.Name, self.lodLow, self.lodHigh)
    if self.waves[#self.waves].requireCompletionOfAllPriorWaves == nil then
      self.waves[#self.waves].requireCompletionOfAllPriorWaves = true
    end
    if self.lookAtMonitor_Start ~= nil then
      self.lookAtMonitor_Start:Stop()
      self.lookAtMonitor_Start = nil
    end
    self:SetStarted(true)
    if self.stopped then
      local currWave = self.currentWave
      for i = 1, currWave - 1 do
        if self.waves[i].waveStopped then
          self:StartWave(i)
        end
      end
      self:StartWave(currWave)
    else
      self:StartWave(1)
      self:FireOnStartCallbacks()
    end
  end
end
function Encounter:ForceClearEncounter()
  game.Encounters.ReportEncounterForceEnded(self.Name)
  _G.Gbl_EncounterList[self.Name] = nil
end
function Encounter:UpdateSpawnWadStatus()
  if self.spawnWad ~= nil then
    if game.FindLevel(self.spawnWad) == nil then
      self:Update_Wad_State(self.spawnWad, false)
    else
      self:Update_Wad_State(self.spawnWad, true)
    end
  end
  for i = 1, #self.waves do
    if self.waves[i].spawnWad ~= nil then
      if game.FindLevel(self.waves[i].spawnWad) == nil then
        self:Update_Wad_State(self.waves[i].spawnWad, false)
      else
        self:Update_Wad_State(self.waves[i].spawnWad, true)
      end
    end
    for j = 1, #self.waves[i] do
      if self.waves[i][j].spawnWad ~= nil then
        if game.FindLevel(self.waves[i][j].spawnWad) == nil then
          self:Update_Wad_State(self.waves[i][j].spawnWad, false)
        else
          self:Update_Wad_State(self.waves[i][j].spawnWad, true)
        end
      end
    end
  end
end
function Encounter:RunLoadChecks()
  if game.IsSpawnEnemiesEnabled() then
    if self.spawnWad ~= nil then
      game.UI.LoadCheck(self.spawnWad)
    end
    for i = 1, #self.waves do
      if self.waves[i].spawnWad ~= nil then
        game.UI.LoadCheck(self.waves[i].spawnWad)
      end
      for j = 1, #self.waves[i] do
        if self.waves[i][j].spawnWad ~= nil then
          game.UI.LoadCheck(self.waves[i][j].spawnWad)
        end
      end
    end
  end
end
function Encounter:StartAndStoreCheckpoint(args)
  self:Start()
  if args ~= nil then
    if args.OverrideObject.IsRefNode then
      game.World.StoreCheckpoint({
        OverrideObject = args.OverrideObject.Child
      })
    else
      game.World.StoreCheckpoint({
        OverrideObject = args.OverrideObject
      })
    end
  else
    game.World.StoreCheckpoint()
  end
end
function Encounter:GetWaveCompletionPercentage(wave)
  local totalEnemies = 0
  local totalEnemiesKilled = 0
  for i = 1, #self.waves[wave] do
    totalEnemies = totalEnemies + self.waves[wave][i].totalSpawns
    totalEnemiesKilled = totalEnemiesKilled + self.waves[wave][i].enemiesKilled
    if self.waves[wave][i].infiniteSpawning then
      return 0
    end
  end
  if totalEnemies == 0 then
    return 0
  end
  return totalEnemiesKilled / totalEnemies * 100
end
function Encounter:Update()
  if engine.IsDebug() and self.showDebug.value > 0 then
    self:DebugInput()
    self:Debug()
  end
  if self:IsRunning() then
    self:UpdateEncounterCombatState()
    self:UpdateEnemyUnleashRequests()
    self:CheckForAIQueuedSpawns()
    self:UpdateAvailableSpawners()
    self:CheckEdgeCasesForForcedCombatStateEnd()
  end
  if lastPrestreamEncounterHash == nil then
    self:UpdateWaveQueue()
    self:UpdatePrestreamingRequest()
    lastPrestreamEncounterHash = self.hashid
  elseif lastPrestreamEncounterHash == self.hashid then
    lastPrestreamEncounterHash = nil
  end
end
function Encounter:UpdateWaveQueue()
  if #self.queuedData.queued_Waves > 0 then
    self.queuedData.deque_active = true
    self:AddWave(self.queuedData.queued_Waves[#self.queuedData.queued_Waves], false)
    table.remove(self.queuedData.queued_Waves)
  else
    self.queuedData.deque_active = false
    if 0 < #self.queuedData.queued_Calls then
      for i = 1, #self.queuedData.queued_Calls do
        self.queuedData.queued_Calls[i]()
      end
      self.queuedData.queued_Calls = {}
    end
  end
end
function Encounter:CheckEdgeCasesForForcedCombatStateEnd()
  if self.player:OnActiveTraversePath() or self.player:HasMarker("QuestGiverInteract") then
    self:PlayerExitedCombatState()
  end
end
function Encounter:UpdateEncounterCombatState()
  if self.inCombat == false then
    if (self.leashZone == "" or self.leashZone ~= "" and game.Encounters.IsPlayerInEncounterLeashZone(self.Name)) and game.Encounters.GetEnemiesInCombatCount(self.Name) > 0 and game.Combat.GetCombatStatus() == true then
      self.inCombat = true
      self:PlayerEnteredCombatState()
    end
  elseif self.leashZone ~= "" and game.Encounters.IsPlayerInEncounterLeashZone(self.Name) == false then
    self:PlayerExitedCombatState(self.idleMusic)
  end
end
function Encounter:IncrementActiveCombatEncounters()
  local activeCombatEncounters
  if self.player_bboard:Exists("ActiveCombatEncounters") then
    activeCombatEncounters = self.player_bboard:GetNumber("ActiveCombatEncounters")
  else
    activeCombatEncounters = 0
  end
  self.player_bboard:Set("ActiveCombatEncounters", activeCombatEncounters + 1)
  return activeCombatEncounters + 1
end
function Encounter:PlayerEnteredCombatState()
  self:IncrementActiveCombatEncounters()
  game.Combat.TurnOnAndLockCombatStatus()
  self.player:CallScript("CombatEncounterStarted", self.startMusic, self.stopMusic)
  self:FireOnCombatStartCallbacks()
end
function Encounter:DecrementActiveCombatEncounters()
  if self.player_bboard:Exists("ActiveCombatEncounters") then
    local count = self.player_bboard:GetNumber("ActiveCombatEncounters")
    self.player_bboard:Set("ActiveCombatEncounters", count - 1)
    return count - 1
  end
end
function Encounter:PlayerExitedCombatState(endMusicOverride)
  if self.inCombat then
    local gbl_activeEncounters = self:DecrementActiveCombatEncounters()
    if gbl_activeEncounters ~= nil and gbl_activeEncounters <= 0 then
      self.combatStatusLocked = false
      game.Combat.UnlockCombatStatus()
    end
    self.inCombat = false
    self.player:CallScript("CombatEncounterEnded", endMusicOverride)
  end
end
function Encounter:OnCombatStart(fn)
  if self.OnCombatStartCallbacks == nil then
    self.OnCombatStartCallbacks = {}
  end
  self.OnCombatStartCallbacks[#self.OnCombatStartCallbacks + 1] = fn
end
function Encounter:FireOnCombatStartCallbacks()
  if self.OnCombatStartCallbacks ~= nil then
    for _, fn in ipairs(self.OnCombatStartCallbacks) do
      fn()
    end
  end
end
function Encounter:UpdateAvailableSpawners()
  if self.spawnConditionsRequireUpdatePolling then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        if self.waves[i][j].spawnersAvailable == false and self:FindAvailableSpawner(i, j) ~= nil then
          self.waves[i][j].spawnersAvailable = true
          self:CheckNextSpawn(i, j)
        end
      end
    end
  end
end
function Encounter:CheckNextSpawn(wave, element)
  if self:SpawnConditionsMet(wave, element) then
    if self.waves[wave][element].numEnemiesAlive < self.waves[wave][element].minActive then
      self:SpawnEnemy(wave, element)
      return
    end
    self:StartSpawnCooldown(wave, element)
  end
end
function Encounter:SpawnConditionsMet(wave, element)
  if self.waves[wave].waveStarted and (self.waves[wave][element].enemiesSpawned < self.waves[wave][element].totalSpawns or self.waves[wave].infiniteSpawning) and (self.waves[wave][element].enemiesSpawned < self.waves[wave][element].initialSpawnAmount or self.waves[wave][element].enemiesSpawned >= self.waves[wave][element].initialSpawnAmount and self.waves[wave][element].numEnemiesAlive < self.waves[wave][element].maxActive) then
    return true
  end
  return false
end
function Encounter:EnemyKilled(enemy, wave, element)
  for ul = 1, #self.unleashList do
    if enemy == self.unleashList[ul] then
      table.remove(self.unleashList, ul)
      break
    end
  end
  if self.started and not self.completed then
    self.waves[wave][element].enemiesKilled = self.waves[wave][element].enemiesKilled + 1
    self.waves[wave][element].numEnemiesAlive = self.waves[wave][element].numEnemiesAlive - 1
    self:FireOnDeathCallbacks(enemy, wave, element)
    self:CheckNextSpawn(wave, element)
    if not self.waves[wave].infiniteSpawning then
      self:UpdateWaveCompletion(wave)
    end
  end
end
function Encounter:AddAIRequestedEnemyToEncounter(spawnedEnemy, wave, element)
  if wave <= #self.waves and element <= #self.waves[wave] then
    self.waves[wave][element].spawnersAvailable = true
    if self.waves[wave][element].numPostAdded then
      self.waves[wave][element].numPostAdded = self.waves[wave][element].numPostAdded + 1
    else
      self.waves[wave][element].numPostAdded = 1
    end
    self.waves[wave][element].totalSpawns = self.waves[wave][element].totalSpawns + 1
    self.waves[wave][element].enemiesSpawned = self.waves[wave][element].enemiesSpawned + 1
    self.waves[wave][element].numEnemiesAlive = self.waves[wave][element].numEnemiesAlive + 1
    table.insert(self.waves[wave][element].livingEnemies, spawnedEnemy)
    local callbackObject = self.waves[1][1].spawners[1]
    callbackObject.LuaObjectScript.SetupDeathCallbackForEncounter(spawnedEnemy, self, wave, element)
  end
end
function Encounter:PrestreamEnemy(wave, element)
  if not game.CHECK_FEATURE("PRESTREAM_ENEMIES") then
    return
  end
  if game.IsSpawnEnemiesEnabled() == false then
    self:SetComplete()
    return
  end
  local selectedSpawner = self:FindAvailableSpawner(wave, element)
  if selectedSpawner == nil then
    return
  end
  local spawnWad
  if self.waves[wave][element].spawnWad == nil then
    if self.waves[wave].spawnWad == nil then
      if self.spawnWad == nil then
        spawnWad = self.level
      else
        spawnWad = self.spawnWad
      end
    else
      spawnWad = self.waves[wave].spawnWad
    end
  else
    spawnWad = self.waves[wave][element].spawnWad
  end
  local powerLevel = self.waves[wave][element].powerLevel
  if powerLevel == nil then
    powerLevel = self.waves[wave].powerLevel
  end
  if selectedSpawner.LuaObjectScript ~= nil then
    self.waves[wave][element].spawnerToUse = selectedSpawner
    table.insert(self.prestreamRequests, {
      spawnWad = spawnWad,
      powerLevel = powerLevel,
      object = selectedSpawner
    })
  else
    if selectedSpawner.IsRefNode then
      selectedSpawner = selectedSpawner.Child
    end
    local randomValue = math.random(1, #self.waves[wave][element].spawners)
    self.waves[wave][element].spawnerToUse = self.waves[wave][element].spawners[randomValue]
    local spawnerToUse = self.waves[wave][element].spawnerToUse
    table.insert(self.prestreamRequests, {
      locator = selectedSpawner,
      spawnWad = spawnWad,
      powerLevel = powerLevel,
      object = spawnerToUse
    })
  end
end
function Encounter:SpawnEnemy(wave, element)
  if game.IsSpawnEnemiesEnabled() == false then
    self:SetComplete()
  elseif self:SpawnConditionsMet(wave, element) then
    local selectedSpawner = self:FindAvailableSpawner(wave, element)
    if selectedSpawner ~= nil then
      local spawnWad
      if self.waves[wave][element].spawnWad == nil then
        if self.waves[wave].spawnWad == nil then
          if self.spawnWad == nil then
            spawnWad = self.level
          else
            spawnWad = self.spawnWad
          end
        else
          spawnWad = self.waves[wave].spawnWad
        end
      else
        spawnWad = self.waves[wave][element].spawnWad
      end
      local powerLevel = self.waves[wave][element].powerLevel
      if powerLevel == nil then
        powerLevel = self.waves[wave].powerLevel
      end
      if selectedSpawner.LuaObjectScript ~= nil then
        selectedSpawner.LuaObjectScript.SpawnEnemyAsync({
          spawnWad = spawnWad,
          powerLevel = powerLevel,
          encounterInfo = {
            self = self,
            wave = wave,
            element = element
          }
        })
        if self.waves[wave][element].disableSpawnerPostSpawn then
          selectedSpawner.LuaObjectScript.Disable()
        end
      else
        if self.waves[wave][element].disableSpawnerPostSpawn then
          self.availableLocators[selectedSpawner:GetName()] = false
        end
        if selectedSpawner.IsRefNode then
          selectedSpawner = selectedSpawner.Child
        end
        self.waves[wave][element].spawners[math.random(1, #self.waves[wave][element].spawners)].LuaObjectScript.SpawnEnemyAsync({
          locator = selectedSpawner,
          spawnWad = spawnWad,
          powerLevel = powerLevel,
          encounterInfo = {
            self = self,
            wave = wave,
            element = element
          }
        })
      end
      self.waves[wave][element].spawnersAvailable = true
      self.waves[wave][element].enemiesSpawned = self.waves[wave][element].enemiesSpawned + 1
      self.waves[wave][element].numEnemiesAlive = self.waves[wave][element].numEnemiesAlive + 1
    end
  end
end
function Encounter:ForwardDeathCallbackToEncounter(creature, wave, element)
  self:EnemyKilled(creature, wave, element)
end
function Encounter:ForwardSpawnCallbackToEncounter(creature, wave, element, spawner, locator)
  game.Encounters.ReportEncounterEnemySpawned(self.Name, self.currentWave, element, creature)
  local callbackMarkers = {}
  local elementMarker = self.waves[wave][element].markerID or nil
  local elementPointMarker = self.waves[wave][element].pointMarkerID or nil
  local waveMarker = self.waves[wave].markerID or nil
  local wavePointMarker = self.waves[wave].pointMarkerID or nil
  if elementMarker ~= nil and creature:HasMarker(elementMarker) == false then
    creature:AddMarker(elementMarker)
    callbackMarkers[#callbackMarkers + 1] = elementMarker
  end
  if elementPointMarker ~= nil and creature:HasMarker(elementPointMarker) == false then
    creature:AddMarkerPointTest(elementPointMarker)
    callbackMarkers[#callbackMarkers + 1] = elementPointMarker
  end
  if waveMarker ~= nil and creature:HasMarker(waveMarker) == false then
    creature:AddMarker(waveMarker)
    callbackMarkers[#callbackMarkers + 1] = waveMarker
  end
  if wavePointMarker ~= nil and creature:HasMarker(wavePointMarker) == false then
    creature:AddMarkerPointTest(wavePointMarker)
    callbackMarkers[#callbackMarkers + 1] = wavePointMarker
  end
  table.insert(self.waves[wave][element].livingEnemies, creature)
  self:FireOnSpawnCallbacks(creature, wave, element, callbackMarkers, spawner, locator)
  self:CheckNextSpawn(wave, element)
end
function Encounter:InsertEnemy(creature, wave)
  wave = wave or self.currentWave
  if wave == 0 then
    wave = 1
  end
  local newElement = {
    livingEnemies = {creature},
    numEnemiesAlive = 1,
    enemiesSpawned = 1,
    enemiesKilled = 0,
    spawners = {},
    spawnLocators = {},
    totalSpawns = 1,
    maxActive = 1
  }
  self.waves[wave].waveStarted = true
  self.waves[wave].waveComplete = false
  self.waves[wave][#self.waves[wave] + 1] = newElement
  local callbackObject = self.waves[1][1].spawners[1]
  callbackObject.LuaObjectScript.SetupDeathCallbackForEncounter(creature, self, wave, #self.waves[wave])
  game.Encounters.ReportEncounterEnemySpawned(self.Name, wave, #self.waves[wave], creature)
end
local GetLastNumberInString = function(string)
  local lowestNum
  for n in string.gmatch(string, "(%d+)") do
    lowestNum = tonumber(n)
  end
  return lowestNum
end
local CompareSpawnerNamesByLastNumber = function(spawnerA, spawnerB)
  local num1 = GetLastNumberInString(spawnerA:GetName())
  local num2 = GetLastNumberInString(spawnerB:GetName())
  return num1 < num2
end
function Encounter:FindAvailableSpawner(wave, element)
  local availableSpawners = {}
  if not self.waves[wave][element].useSpawnLocatorsOnly then
    for i = 1, #self.waves[wave][element].spawners do
      if self.waves[wave][element].spawners[i].LuaObjectScript.IsEnabled() then
        availableSpawners[#availableSpawners + 1] = self.waves[wave][element].spawners[i]
      end
    end
  end
  if self.waves[wave][element].spawnLocators ~= nil then
    for i = 1, #self.waves[wave][element].spawnLocators do
      local locator = self.waves[wave][element].spawnLocators[i]
      if self.availableLocators[locator:GetName()] == true then
        availableSpawners[#availableSpawners + 1] = locator
      end
    end
  end
  if #availableSpawners == 0 then
    engine.Warning("Trying to spawn enemy but no spawners are available - [ ", self.Name, " || Wave ", wave, " || Element ", element, " ]")
    self.waves[wave][element].spawnersAvailable = false
    return
  end
  if self.spawnConditionsRequireUpdatePolling then
    if self.waves[wave].requireSpawnersInRange then
      for i = #availableSpawners, 1, -1 do
        local playerDistanceToSpawner = game.AIUtil.Distance(self.player, availableSpawners[i])
        if playerDistanceToSpawner > self.waves[wave].requireSpawnersInRange[2] or playerDistanceToSpawner < self.waves[wave].requireSpawnersInRange[1] then
          table.remove(availableSpawners, i)
        end
      end
      if #availableSpawners == 0 then
        self.waves[wave][element].spawnersAvailable = false
        return
      end
    end
    if self.waves[wave].requireOnScreenSpawners then
      for i = #availableSpawners, 1, -1 do
        if game.Camera.GetViewPenetration(availableSpawners[i]:GetWorldPosition(), 0, 0) < 0.01 then
          table.remove(availableSpawners, i)
        end
      end
      if #availableSpawners == 0 then
        self.waves[wave][element].spawnersAvailable = false
        return
      end
    elseif self.waves[wave].requireOffScreenSpawners then
      for i = #availableSpawners, 1, -1 do
        if 0 < game.Camera.GetViewPenetration(availableSpawners[i]:GetWorldPosition(), 0, 0) then
          table.remove(availableSpawners, i)
        end
      end
      if #availableSpawners == 0 then
        self.waves[wave][element].spawnersAvailable = false
        return
      end
    end
  end
  local prioritySpawners
  if self.waves[wave].prioritizedSpawnConditions then
    prioritySpawners = {}
    if self.waves[wave].prioritizeSpawnersInRange ~= nil then
      for i = 1, #availableSpawners do
        local playerDistanceToSpawner = game.AIUtil.Distance(self.player, availableSpawners[i])
        if playerDistanceToSpawner >= self.waves[wave].prioritizeSpawnersInRange[1] and playerDistanceToSpawner <= self.waves[wave].prioritizeSpawnersInRange[2] then
          prioritySpawners[#prioritySpawners + 1] = availableSpawners[i]
        end
      end
    end
    if self.waves[wave].prioritizeOnScreenSpawners then
      for i = 1, #availableSpawners do
        if game.Camera.GetViewPenetration(availableSpawners[i]:GetWorldPosition(), 0, 0) >= 0.1 then
          prioritySpawners[#prioritySpawners + 1] = availableSpawners[i]
        end
      end
    elseif self.waves[wave].prioritizeOffScreenSpawners then
      for i = 1, #availableSpawners do
        if 0 >= game.Camera.GetViewPenetration(availableSpawners[i]:GetWorldPosition(), 0, 0) then
          prioritySpawners[#prioritySpawners + 1] = availableSpawners[i]
        end
      end
    end
    if 0 < #prioritySpawners then
      availableSpawners = {}
      for _, spawner in pairs(prioritySpawners) do
        availableSpawners[#availableSpawners + 1] = spawner
      end
    end
  end
  if 0 < #availableSpawners then
    return self:FindNextSpawnerFromSpawnPattern(wave, availableSpawners)
  end
  self.waves[wave][element].spawnersAvailable = false
end
function Encounter:FindNextSpawnerFromSpawnPattern(WaveNumber, Spawners)
  if self.waves[WaveNumber].spawnPattern == "Random" then
    if 1 < #Spawners then
      for i = #Spawners, 1, -1 do
        if Spawners[i] == self.waves[WaveNumber].lastSpawnerUsed then
          table.remove(Spawners, i)
          break
        end
      end
    end
    local randomSpawner = Spawners[math.random(1, #Spawners)]
    self.waves[WaveNumber].lastSpawnerUsed = randomSpawner
    return randomSpawner
  elseif self.waves[WaveNumber].spawnPattern == "Clumped" then
    if self.waves[WaveNumber].lastSpawnerUsed == nil then
      self.waves[WaveNumber].lastSpawnerUsed = Spawners[math.random(1, #Spawners)]
      return self.waves[WaveNumber].lastSpawnerUsed
    else
      local closestDistance = 1000
      local closestSpawnerToLastSpawner
      for i = 1, #Spawners do
        if Spawners[i] ~= self.waves[WaveNumber].lastSpawnerUsed or #Spawners == 1 then
          local distanceFromLastSpawner = game.AIUtil.Distance(self.waves[WaveNumber].lastSpawnerUsed, Spawners[i])
          if closestDistance > distanceFromLastSpawner then
            closestDistance = distanceFromLastSpawner
            closestSpawnerToLastSpawner = Spawners[i]
          end
        end
      end
      self.waves[WaveNumber].lastSpawnerUsed = closestSpawnerToLastSpawner
      return closestSpawnerToLastSpawner
    end
  elseif self.waves[WaveNumber].spawnPattern == "Ordered" then
    table.sort(Spawners, CompareSpawnerNamesByLastNumber)
    local lastSpawnerNum = GetLastNumberInString(Spawners[#Spawners]:GetName())
    if self.waves[WaveNumber].lastSpawnerUsed == nil or self.waves[WaveNumber].lastSpawnerUsed ~= nil and lastSpawnerNum <= self.waves[WaveNumber].lastSpawnerUsed then
      self.waves[WaveNumber].lastSpawnerUsed = GetLastNumberInString(Spawners[1]:GetName())
      return Spawners[1]
    else
      for i = 1, #Spawners do
        if GetLastNumberInString(Spawners[i]:GetName()) > self.waves[WaveNumber].lastSpawnerUsed then
          self.waves[WaveNumber].lastSpawnerUsed = GetLastNumberInString(Spawners[i]:GetName())
          return Spawners[i]
        end
      end
    end
  end
  return nil
end
function Encounter:StartSpawnCooldown(wave, element)
  if self.waves[wave][element].spawnCooldownTimer == nil then
    if self.waves[wave][element].enemiesSpawned < self.waves[wave][element].initialSpawnAmount then
      local spawnCooldown = self.waves[wave][element].GetInitialSpawnCooldown()
      if spawnCooldown == 0 then
        self:SpawnEnemy(wave, element)
      else
        self.waves[wave][element].spawnCooldownTimer = timers.StartLevelTimer(spawnCooldown, function()
          self.waves[wave][element].spawnCooldownTimer:Stop()
          self.waves[wave][element].spawnCooldownTimer = nil
          self:SpawnEnemy(wave, element)
        end)
      end
    else
      local spawnCooldown = self.waves[wave][element].GetSpawnCooldown()
      if spawnCooldown == 0 then
        self:SpawnEnemy(wave, element)
      else
        self.waves[wave][element].spawnCooldownTimer = timers.StartLevelTimer(spawnCooldown, function()
          self.waves[wave][element].spawnCooldownTimer:Stop()
          self.waves[wave][element].spawnCooldownTimer = nil
          self:SpawnEnemy(wave, element)
        end)
      end
    end
  end
end
function Encounter:StopSpawnTimers(wave, element)
  wave = wave or nil
  element = element or nil
  if wave == nil then
    for i = 1, #self.waves do
      if self.waves[i].timeBeforeNextWaveTimer ~= nil then
        self.waves[i].timeBeforeNextWaveTimer:Stop()
        self.waves[i].timeBeforeNextWaveTimer = nil
      end
      for j = 1, #self.waves[i] do
        if self.waves[i][j].spawnCooldownTimer ~= nil then
          self.waves[i][j].spawnCooldownTimer:Stop()
          self.waves[i][j].spawnCooldownTimer = nil
        end
      end
    end
  else
    if self.waves[wave].timeBeforeNextWaveTimer ~= nil then
      self.waves[wave].timeBeforeNextWaveTimer:Stop()
      self.waves[wave].timeBeforeNextWaveTimer = nil
    end
    if element == nil then
      for i = 1, #self.waves[wave] do
        if self.waves[wave][i].spawnCooldownTimer ~= nil then
          self.waves[wave][i].spawnCooldownTimer:Stop()
          self.waves[wave][i].spawnCooldownTimer = nil
        end
      end
    elseif self.waves[wave][element].spawnCooldownTimer ~= nil then
      self.waves[wave][element].spawnCooldownTimer:Stop()
      self.waves[wave][element].spawnCooldownTimer = nil
    end
  end
end
function Encounter:UpdateWaveCompletion(wave)
  if self.started and not self.completed and self:WaveCompletionCriteriaMet(wave) then
    self.waves[wave].waveComplete = true
    self:FireOnCompleteCallbacks(wave)
    game.Encounters.ReportWaveEnd(self.Name, wave)
    if self.waves[wave].enableSpawnersOnWaveComplete then
      for i = 1, #self.waves[wave] do
        if self.waves[wave][i].spawnLocators ~= nil then
          for j = 1, #self.waves[wave][i].spawnLocators do
            self.availableLocators[self.waves[wave][i].spawnLocators[j]:GetName()] = true
          end
        end
      end
      for i = 1, #self.waves[wave] do
        for j = 1, #self.waves[wave][i].spawners do
          self.waves[wave][i].spawners[j].LuaObjectScript.Enable()
        end
      end
    end
    if self:PriorWaveCriteriaMet(wave) == false then
      return
    end
    if wave < #self.waves and not self:WavesComplete() then
      if self.stopped and wave == self.currentWave then
        self.currentWave = self.currentWave + 1
      else
        local nextWave
        if wave == self.currentWave then
          nextWave = wave + 1
        else
          if not self.waves[self.currentWave].requireCompletionOfAllPriorWaves then
            return
          end
          for i = 1, self.currentWave do
            if not self:WavesComplete(i) then
              return
            end
          end
          wave = self.currentWave
          nextWave = self.currentWave + 1
        end
        if self.waves[nextWave].triggerWaveFromScript then
          return
        end
        if self.waves[wave].timeBetweenWaves > 0 then
          local tempCurrentWave = wave
          local tempNextWave = nextWave
          self.waves[tempCurrentWave].timeBetweenWavesTimer = timers.StartLevelTimer(self.waves[tempCurrentWave].timeBetweenWaves, function()
            if not self:IsRunning(tempNextWave) then
              self:StartWave(tempNextWave)
            end
            self.waves[tempCurrentWave].timeBetweenWavesTimer:Stop()
            self.waves[tempCurrentWave].timeBetweenWavesTimer = nil
          end)
          return
        end
        self:StartWave(nextWave)
      end
    elseif self:WavesComplete() then
      self:EndEncounter()
    end
  end
end
function Encounter:PercentageCriteriaMet(wave)
  if self:GetWaveCompletionPercentage(wave) >= self.waves[wave].requiredWaveCompletion then
    return true
  else
    return false
  end
end
function Encounter:PriorWaveCriteriaMet(wave)
  if not self.waves[wave].requireCompletionOfAllPriorWaves then
    return true
  else
    for i = wave - 1, 1, -1 do
      if self:WavesComplete(i) == false then
        return false
      end
    end
    return true
  end
end
function Encounter:WaveCompletionCriteriaMet(wave)
  if self.waves[wave].waveComplete == true then
    return false
  end
  if self:PercentageCriteriaMet(wave) == false then
    return false
  end
  if self.waves[wave].recurring then
    local currentWave = wave
    timers.StartLevelTimer(self.waves[currentWave].recurringCooldown, function()
      if self.waves[currentWave].recurring then
        self:Restart(currentWave)
      end
    end)
    return false
  end
  return true
end
function Encounter:WavesComplete(wave)
  wave = wave or nil
  if wave == nil then
    for i = 1, #self.waves do
      if self.waves[i].waveComplete == false then
        return false
      end
    end
    return true
  else
    return self.waves[wave].waveComplete
  end
end
function Encounter:WavesStarted(wave)
  wave = wave or nil
  if wave == nil then
    for i = 1, #self.waves do
      if self.waves[i].waveStarted == false then
        return false
      end
    end
    return true
  else
    return self.waves[wave].waveStarted
  end
end
function Encounter:CheckTimeBeforeNextWave(waveNum)
  if waveNum < #self.waves and self.waves[waveNum].timeBeforeNextWave > 0 then
    local tempNextWave = waveNum + 1
    self.waves[waveNum].timeBeforeNextWaveTimer = timers.StartLevelTimer(self.waves[waveNum].timeBeforeNextWave, function()
      if not self:IsRunning(tempNextWave) then
        self:StartWave(tempNextWave)
      end
      if self.waves[waveNum].timeBeforeNextWaveTimer then
        self.waves[waveNum].timeBeforeNextWaveTimer:Stop()
        self.waves[waveNum].timeBeforeNextWaveTimer = nil
      end
    end)
  end
end
function Encounter:IsRunning(wave)
  wave = wave or nil
  if wave == nil then
    return self.started and self.completed == false
  else
    return self.waves[wave] ~= nil and self.waves[wave].waveStarted and not self.waves[wave].waveComplete
  end
end
function Encounter:StartWave(waveNum)
  if self.queuedData.deque_active then
    self.queuedData.queued_Calls[#self.queuedData.queued_Calls + 1] = function()
      self:StartWave(waveNum)
    end
    return
  end
  if self.started and not self.completed then
    self:EnableUpdate()
    if self.stopped then
      self.stopped = false
    end
    if waveNum == nil then
      waveNum = self.currentWave + 1
    end
    if self.waves[waveNum] ~= nil then
      self.waves[waveNum].waveStopped = false
      if self:WavesComplete(waveNum) == false then
        self.waves[waveNum].waveStarted = true
        self.currentWave = waveNum
        self:CheckTimeBeforeNextWave(waveNum)
        self:FireOnStartCallbacks(waveNum)
        game.Encounters.ReportWaveStart(self.Name, self.currentWave, self:GetTotalSpawns(self.currentWave))
        for i = 1, #self.waves[waveNum] do
          self:CheckNextSpawn(waveNum, i)
        end
      end
    end
  end
end
function Encounter:EndEncounter()
  if self.completed == false then
    self.completed = true
    if self.started then
      if game.Encounters.ReportEncounterEndedToDependents then
        local endedParam = {
          name = self.Name,
          dependents = {}
        }
        for i = 1, 3 do
          if i <= #self.dependents then
            table.insert(endedParam.dependents, self.dependents[i])
          else
            table.insert(endedParam.dependents, engine.Hash(""))
          end
        end
        game.Encounters.ReportEncounterEndedToDependents(endedParam.name, endedParam.dependents[1], endedParam.dependents[2], endedParam.dependents[3])
      end
      game.Encounters.ReportEncounterEnd(self.Name)
    end
    self:PlayerExitedCombatState()
    if self.id and game.Combat.HandleEncounterCompleted then
      game.Combat.HandleEncounterCompleted(self.id)
    end
    self:RemoveFromTracking()
    self:DisableUpdate()
    self:DisableAllZoneMonitors()
    self:FireOnCompleteCallbacks()
    if self.checkpointOnComplete then
      game.World.StoreCheckpoint({
        OverrideObject = self.checkpointOverrideObject
      })
    end
  end
end
function Encounter:SetSpawnCooldown(args)
  local nextElement = self.waves[args.wave][args.element]
  local spawnCooldown = args.cooldown
  if nextElement.spawnCooldownType == "SingleValue" then
    if type(spawnCooldown) == "table" then
      nextElement.spawnCooldown = spawnCooldown[1] or 0
    elseif type(spawnCooldown) == "number" then
      nextElement.spawnCooldown = spawnCooldown or 0
    end
    function nextElement.GetSpawnCooldown()
      return nextElement.spawnCooldown
    end
  elseif nextElement.spawnCooldownType == "RandomRange" then
    nextElement.spawnCooldown = spawnCooldown or 0
    function nextElement.GetSpawnCooldown()
      return math.random(nextElement.spawnCooldown[1] * 1000, nextElement.spawnCooldown[2] * 1000) / 1000
    end
  elseif nextElement.spawnCooldownType == "Sequential" then
    nextElement.spawnCooldown = spawnCooldown or 0
    nextElement.spawnCooldownIndex = 0
    function nextElement.GetSpawnCooldown()
      if nextElement.spawnCooldownIndex < #nextElement.spawnCooldown then
        nextElement.spawnCooldownIndex = nextElement.spawnCooldownIndex + 1
      else
        nextElement.spawnCooldownIndex = 1
      end
      return nextElement.spawnCooldown[nextElement.spawnCooldownIndex]
    end
  end
end
function Encounter:SetStarted(value)
  self.started = value
  self.previouslyStarted = value
end
function Encounter:WasRunning()
  return self.previouslyStarted and not self.completed
end
function Encounter:IsComplete()
  return self.completed
end
function Encounter:SetStartZone(zoneOrObj, args)
  if not self.completed or self.repopulateEncounter then
    if type(zoneOrObj) == "string" then
      zoneOrObj = self.level:FindSingleGameObject(zoneOrObj)
    end
    if zoneOrObj == nil then
      engine.Warning("Can't find object '", zoneOrObj, "' in function SetStartZone(object). Level: ", self.level.Name, " Encounter: ", self.Name)
    end
    local EvaluateCineRequirements = function()
      if args then
        local cineNumberReqs = args.cineRequirement
        if cineNumberReqs then
          local cineNumber = game.Level.GetVariable("CompletedCineNumber")
          if type(cineNumberReqs) == "table" then
            if cineNumber > cineNumberReqs[1] and cineNumber <= cineNumberReqs[2] then
              return true
            end
          elseif cineNumber == cineNumberReqs then
            return true
          end
          return false
        end
      end
      return true
    end
    if zoneOrObj:FindSingleGOByName("EntityZoneEventsRoot") ~= nil then
      if self.startZoneMonitor == nil then
        self.startZoneMonitor = zoneOrObj:FindSingleGOByName("EntityZoneEventsRoot")
      end
      self.startZoneMonitor.LuaObjectScript.RegisterEntryCallback(function()
        local zoneEventsScript = self.startZoneMonitor.LuaObjectScript
        zoneEventsScript.SetEntryTriggerLimit(-1)
        zoneEventsScript.SetDisableAfterUse(false)
        zoneEventsScript.SetZoneVisibility(true)
        if EvaluateCineRequirements() then
          self:Start()
        end
      end)
    else
      if self.startZoneMonitor == nil then
        local monitors = require("level.MonitorLibrary")
        self.startZoneMonitor = monitors.CreateEntityZoneMonitor(self.player, zoneOrObj)
      end
      self.startZoneMonitor:OnEnter(function()
        if EvaluateCineRequirements() then
          self:Start()
          self.startZoneMonitor:Stop()
        end
      end)
    end
  end
end
function Encounter:SetAlertZone(zoneOrObj, args)
  if not self.completed or self.repopulateEncounter then
    local wave
    if args and args.wave then
      wave = args.wave
    end
    if type(zoneOrObj) == "string" then
      zoneOrObj = self.level:FindSingleGameObject(zoneOrObj)
    end
    if zoneOrObj == nil then
      engine.Warning("Can't find object '", zoneOrObj, "' in function SetAlertZone(object). Level: ", self.level.Name, " Encounter: ", self.Name)
    end
    if zoneOrObj:FindSingleGOByName("EntityZoneEventsRoot") ~= nil then
      if self.alertZoneMonitor == nil then
        self.alertZoneMonitor = zoneOrObj:FindSingleGOByName("EntityZoneEventsRoot")
      end
      self.alertZoneMonitor.LuaObjectScript.RegisterEntryCallback(function()
        local zoneEventsScript = self.alertZoneMonitor.LuaObjectScript
        zoneEventsScript.SetEntryTriggerLimit(-1)
        zoneEventsScript.SetDisableAfterUse(false)
        zoneEventsScript.SetZoneVisibility(true)
        if wave then
          if self:IsRunning(wave) and not self.waves[wave].alertedEnemies then
            self.waves[wave].alertedEnemies = true
            self:AlertEnemies(wave)
          end
        elseif not self.alertedEnemies then
          self.alertedEnemies = true
          self:AlertEnemies()
        end
      end)
    else
      if self.alertZoneMonitor == nil then
        local monitors = require("level.MonitorLibrary")
        self.alertZoneMonitor = monitors.CreateEntityZoneMonitor(self.player, zoneOrObj)
      end
      self.alertZoneMonitor:OnEnter(function()
        if wave then
          if self:IsRunning(wave) and not self.waves[wave].alertedEnemies then
            self.waves[wave].alertedEnemies = true
            self:AlertEnemies(wave)
            self.alertZoneMonitor:Stop()
          end
        else
          if not self.alertedEnemies then
            self.alertedEnemies = true
            self:AlertEnemies()
          end
          self.alertZoneMonitor:Stop()
        end
      end)
    end
  end
end
function Encounter:SetLocators(locators, wave, element)
  if type(locators) == "string" then
    local spawnerStringHasAsterisk = string.find(locators, "*")
    local locatorString = locators
    if spawnerStringHasAsterisk ~= nil then
      locatorString = locatorString .. "*"
    end
    if wave == nil then
      for i = 1, #self.waves do
        for j = 1, #self.waves[i] do
          self.waves[i][j].spawnLocators = self.level:FindGameObjects(locatorString)
        end
      end
    elseif element == nil then
      for i = 1, #self.waves[wave] do
        self.waves[wave][i].spawnLocators = self.level:FindGameObjects(locatorString)
      end
    else
      self.waves[wave][element].spawnLocators = self.level:FindGameObjects(locatorString)
    end
  elseif type(locators) == "table" then
    if wave == nil then
      for i = 1, #self.waves do
        for j = 1, #self.waves[i] do
          self.waves[i][j].spawnLocators = locators
        end
      end
    elseif element == nil then
      for i = 1, #self.waves[wave] do
        self.waves[wave][i].spawnLocators = locators
      end
    else
      self.waves[wave][element].spawnLocators = locators
    end
  end
  if wave == nil then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        for k = 1, #self.waves[i][j].spawnLocators do
          self.availableLocators[self.waves[i][j].spawnLocators[k]:GetName()] = true
        end
      end
    end
  elseif element == nil then
    for i = 1, #self.waves[wave] do
      for j = 1, #self.waves[wave][i].spawnLocators do
        self.availableLocators[self.waves[wave][i].spawnLocators[j]:GetName()] = true
      end
    end
  else
    for i = 1, #self.waves[wave][element].spawnLocators do
      self.availableLocators[self.waves[wave][element].spawnLocators[i]:GetName()] = true
    end
  end
end
function Encounter:DisableAllZoneMonitors()
  if self.startZoneMonitor ~= nil then
    if self.startZoneMonitor.LuaObjectScript ~= nil then
      self.startZoneMonitor.LuaObjectScript.SetZoneVisibility(false)
    else
      self.startZoneMonitor:Stop()
      self.startZoneMonitor = nil
    end
  end
  if self.alertZoneMonitor ~= nil then
    if self.alertZoneMonitor.LuaObjectScript ~= nil then
      self.alertZoneMonitor.LuaObjectScript.SetZoneVisibility(false)
    else
      self.alertZoneMonitor:Stop()
      self.alertZoneMonitor = nil
    end
  end
end
function Encounter:EnableAllZoneMonitors()
  if self.startZoneMonitor ~= nil then
    local zoneScript = self.startZoneMonitor.LuaObjectScript
    if zoneScript ~= nil then
      zoneScript.SetZoneVisibility(true)
      zoneScript.SetDisableAfterUse(false)
      zoneScript.SetEntryTriggerLimit(-1)
    else
      self.startZoneMonitor:Start()
    end
  end
  if self.alertZoneMonitor ~= nil then
    local zoneScript = self.alertZoneMonitor.LuaObjectScript
    if zoneScript ~= nil then
      zoneScript.SetZoneVisibility(true)
      zoneScript.SetDisableAfterUse(false)
      zoneScript.SetEntryTriggerLimit(-1)
    else
      self.alertZoneMonitor:Start()
    end
  end
end
function Encounter:GetMaxActive(wave, element)
  element = element or 1
  return self.waves[wave][element].maxActive
end
function Encounter:SetMaxActive(newMaxActive, wave, element)
  element = element or 1
  self.waves[wave][element].maxActive = newMaxActive
end
function Encounter:GetMinActive(wave, element)
  element = element or 1
  return self.waves[wave][element].minActive
end
function Encounter:SetMinActive(newMinActive, wave, element)
  element = element or 1
  self.waves[wave][element].minActive = newMinActive
end
function Encounter:GetTotalSpawns(wave, element)
  wave = wave or nil
  element = element or nil
  local totalEnemies = 0
  if wave == nil then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        if self.waves[i][j].infiniteSpawning then
          return -1
        end
        totalEnemies = totalEnemies + self.waves[i][j].totalSpawns
      end
    end
    return totalEnemies
  else
    if element == nil then
      for j = 1, #self.waves[wave] do
        if self.waves[wave][j].infiniteSpawning then
          return -1
        end
        totalEnemies = totalEnemies + self.waves[wave][j].totalSpawns
      end
    else
      if self.waves[wave][element].infiniteSpawning then
        return -1
      end
      totalEnemies = self.waves[wave][element].totalSpawns
    end
    return totalEnemies
  end
end
function Encounter:GetNumEnemiesSpawned(wave, element)
  wave = wave or nil
  element = element or nil
  local totalEnemiesSpawned = 0
  if wave == nil then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        totalEnemiesSpawned = totalEnemiesSpawned + self.waves[i][j].enemiesSpawned
      end
    end
    return totalEnemiesSpawned
  else
    if element == nil then
      for j = 1, #self.waves[wave] do
        totalEnemiesSpawned = totalEnemiesSpawned + self.waves[wave][j].enemiesSpawned
      end
    else
      totalEnemiesSpawned = self.waves[wave][element].enemiesSpawned
    end
    return totalEnemiesSpawned
  end
end
function Encounter:GetNumEnemiesAlive(wave)
  wave = wave or nil
  local totalEnemiesAlive = 0
  if wave == nil then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        totalEnemiesAlive = totalEnemiesAlive + self.waves[i][j].numEnemiesAlive
      end
    end
    return totalEnemiesAlive
  else
    for j = 1, #self.waves[wave] do
      totalEnemiesAlive = totalEnemiesAlive + self.waves[wave][j].numEnemiesAlive
    end
    return totalEnemiesAlive
  end
end
function Encounter:GetNumEnemiesKilled(wave, element)
  wave = wave or nil
  element = element or nil
  local totalEnemiesKilled = 0
  if wave == nil then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        totalEnemiesKilled = totalEnemiesKilled + self.waves[i][j].enemiesKilled
      end
    end
    return totalEnemiesKilled
  else
    if element == nil then
      for j = 1, #self.waves[wave] do
        totalEnemiesKilled = totalEnemiesKilled + self.waves[wave][j].enemiesKilled
      end
    else
      totalEnemiesKilled = self.waves[wave][element].enemiesKilled
    end
    return totalEnemiesKilled
  end
end
function Encounter:GetLastEnemySpawned(wave, element)
  wave = wave or nil
  element = element or 1
  if wave == nil then
    local lastEnemiesSpawned = {}
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        if self.waves[i][j].numEnemiesAlive > 0 then
          table.insert(lastEnemiesSpawned, self.waves[i][j].livingEnemies[self.waves[i][j].numEnemiesAlive])
        end
      end
    end
    return lastEnemiesSpawned
  else
    return self.waves[wave][element].livingEnemies[self.waves[wave][element].numEnemiesAlive]
  end
end
function Encounter:GetActiveEnemies(wave, args)
  wave = wave or nil
  if type(wave) ~= "number" then
    args = wave
    wave = nil
  end
  local activeEnemies = {}
  if wave == nil then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        for k = 1, #self.waves[i][j].livingEnemies do
          if self.waves[i][j].livingEnemies[k] ~= nil then
            if args ~= nil then
              if args.markerID ~= nil and self.waves[i][j].livingEnemies[k]:HasMarker(args.markerID) then
                table.insert(activeEnemies, self.waves[i][j].livingEnemies[k])
              end
            else
              table.insert(activeEnemies, self.waves[i][j].livingEnemies[k])
            end
          end
        end
      end
    end
    return activeEnemies
  else
    for j = 1, #self.waves[wave] do
      for k = 1, #self.waves[wave][j].livingEnemies do
        if self.waves[wave][j].livingEnemies[k] ~= nil then
          if args ~= nil then
            if args.markerID ~= nil and self.waves[wave][j].livingEnemies[k]:HasMarker(args.markerID) then
              table.insert(activeEnemies, self.waves[wave][j].livingEnemies[k])
            end
          else
            table.insert(activeEnemies, self.waves[wave][j].livingEnemies[k])
          end
        end
      end
    end
    return activeEnemies
  end
end
function Encounter:GetPercentEnemiesKilled(wave)
  wave = wave or nil
  local totalEnemiesKilled = 0
  local totalEnemySpawns = 0
  if wave == nil then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        totalEnemySpawns = totalEnemySpawns + self.waves[i][j].totalSpawns
        totalEnemiesKilled = totalEnemiesKilled + self.waves[i][j].enemiesKilled
      end
    end
    return totalEnemiesKilled / totalEnemySpawns * 100
  else
    for i = 1, #self.waves[wave] do
      totalEnemySpawns = totalEnemySpawns + self.waves[wave][i].totalSpawns
      totalEnemiesKilled = totalEnemiesKilled + self.waves[wave][i].enemiesKilled
    end
    return totalEnemiesKilled / totalEnemySpawns * 100
  end
end
function Encounter:GetPercentEnemiesSpawned(wave, element)
  wave = wave or nil
  local totalEnemySpawns = 0
  local totalEnemiesSpawned = 0
  if wave == nil then
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        totalEnemySpawns = totalEnemySpawns + self.waves[i][j].totalSpawns
        totalEnemiesSpawned = totalEnemiesSpawned + self.waves[i][j].enemiesSpawned
      end
    end
    return totalEnemiesSpawned / totalEnemySpawns * 100
  else
    for i = 1, #self.waves[wave] do
      totalEnemySpawns = totalEnemySpawns + self.waves[wave][i].totalSpawns
      totalEnemiesSpawned = totalEnemiesSpawned + self.waves[wave][i].enemiesSpawned
    end
    return totalEnemiesSpawned / totalEnemySpawns * 100
  end
end
function Encounter:DisableSpawners(args)
  local disablingSpawners = {}
  if args.name ~= nil then
    local spawnerStringHasAsterisk = string.find(args.name, "*")
    if spawnerStringHasAsterisk ~= nil then
      disablingSpawners = self.level:FindGameObjects(args.name)
    else
      disablingSpawners = self.level:FindGameObjects(tostring(args.name .. "*"))
    end
  end
  if args.object ~= nil then
    disablingSpawners[#disablingSpawners + 1] = args.object
  end
  for _, spawner in pairs(disablingSpawners) do
    if spawner.LuaObjectScript then
      spawner.LuaObjectScript.Disable()
    elseif self.availableLocators[spawner:GetName()] ~= nil then
      self.availableLocators[spawner:GetName()] = false
    end
  end
end
function Encounter:EnableSpawners(args)
  local enablingSpawners = {}
  if args.name ~= nil then
    local spawnerStringHasAsterisk = string.find(args.name, "*")
    if spawnerStringHasAsterisk ~= nil then
      enablingSpawners = self.level:FindGameObjects(args.name)
    else
      enablingSpawners = self.level:FindGameObjects(tostring(args.name .. "*"))
    end
  end
  if args.object ~= nil then
    enablingSpawners[#enablingSpawners + 1] = args.object
  end
  for _, spawner in pairs(enablingSpawners) do
    if spawner.LuaObjectScript then
      spawner.LuaObjectScript.Enable()
    elseif self.availableLocators[spawner:GetName()] ~= nil then
      self.availableLocators[spawner:GetName()] = true
    end
  end
end
function Encounter:EnableAllSpawners()
  for k, _ in pairs(self.availableLocators) do
    self.availableLocators[k] = true
  end
  for i = 1, #self.waves do
    for j = 1, #self.waves[i] do
      for k = 1, #self.waves[i][j].spawners do
        self.waves[i][j].spawners[k].LuaObjectScript.Enable()
      end
    end
  end
end
function Encounter:OnEnemyDeath(functionToCall, args)
  if self.OnDeathCallbacks == nil then
    self.OnDeathCallbacks = {}
  end
  table.insert(self.OnDeathCallbacks, {CallbackFunction = functionToCall, DeathFilter = args})
end
function Encounter:FireOnDeathCallbacks(deadEnemy, wave, element)
  if self.OnDeathCallbacks ~= nil then
    for _, callbackInfo in ipairs(self.OnDeathCallbacks) do
      local callbackConditionsMet = true
      if callbackInfo.DeathFilter ~= nil then
        if callbackInfo.DeathFilter.wave ~= nil and wave ~= callbackInfo.DeathFilter.wave then
          callbackConditionsMet = false
        end
        if callbackInfo.DeathFilter.element ~= nil and element ~= callbackInfo.DeathFilter.element then
          callbackConditionsMet = false
        end
        if callbackInfo.DeathFilter.markerID ~= nil and deadEnemy:HasMarker(callbackInfo.DeathFilter.markerID) == false then
          callbackConditionsMet = false
        end
        if callbackConditionsMet and callbackInfo.DeathFilter.count ~= nil then
          if callbackInfo.DeathFilter.wave == nil then
            if self:GetNumEnemiesKilled() ~= callbackInfo.DeathFilter.count then
              callbackConditionsMet = false
            end
          elseif callbackInfo.DeathFilter.element == nil then
            if self:GetNumEnemiesKilled(callbackInfo.DeathFilter.wave) ~= callbackInfo.DeathFilter.count then
              callbackConditionsMet = false
            end
          elseif self:GetNumEnemiesKilled(callbackInfo.DeathFilter.wave, callbackInfo.DeathFilter.element) ~= callbackInfo.DeathFilter.count then
            callbackConditionsMet = false
          end
        end
      end
      if callbackConditionsMet then
        callbackInfo.CallbackFunction(deadEnemy)
      end
    end
  end
end
function Encounter:OnEnemySpawn(functionToCall, args)
  if self.OnSpawnCallbacks == nil then
    self.OnSpawnCallbacks = {}
  end
  table.insert(self.OnSpawnCallbacks, {CallbackFunction = functionToCall, SpawnFilter = args})
end
function Encounter:FireOnSpawnCallbacks(spawnedEnemy, wave, element, markers, spawner, locator)
  if self.OnSpawnCallbacks ~= nil then
    for _, callbackInfo in ipairs(self.OnSpawnCallbacks) do
      local callbackConditionsMet = true
      if callbackInfo.SpawnFilter ~= nil then
        if callbackInfo.SpawnFilter.wave ~= nil and wave ~= callbackInfo.SpawnFilter.wave then
          callbackConditionsMet = false
        end
        if callbackInfo.SpawnFilter.element ~= nil and element ~= callbackInfo.SpawnFilter.element then
          callbackConditionsMet = false
        end
        if callbackInfo.SpawnFilter.markerID ~= nil and #callbackInfo.SpawnFilter.markerID > 0 then
          if #markers == 0 and callbackInfo.SpawnFilter.markerID ~= nil then
            callbackConditionsMet = false
          elseif 0 < #markers then
            local foundMarker = false
            for _, mrkr in pairs(markers) do
              if mrkr == callbackInfo.SpawnFilter.markerID then
                foundMarker = true
                break
              end
            end
            if not foundMarker then
              callbackConditionsMet = false
            end
          end
        end
        if callbackInfo.SpawnFilter.count ~= nil then
          if callbackInfo.SpawnFilter.wave == nil then
            if self:GetNumEnemiesSpawned() ~= callbackInfo.SpawnFilter.count then
              callbackConditionsMet = false
            end
          elseif callbackInfo.SpawnFilter.element == nil then
            if self:GetNumEnemiesSpawned(callbackInfo.SpawnFilter.wave) ~= callbackInfo.SpawnFilter.count then
              callbackConditionsMet = false
            end
          elseif self:GetNumEnemiesSpawned(callbackInfo.SpawnFilter.wave, callbackInfo.SpawnFilter.element) ~= callbackInfo.SpawnFilter.count then
            callbackConditionsMet = false
          end
        end
      end
      if callbackConditionsMet then
        callbackInfo.CallbackFunction(spawnedEnemy, spawner, locator)
      end
    end
  end
end
function Encounter:OnComplete(functionToCall, wave)
  if self.OnCompleteCallbacks == nil then
    self.OnCompleteCallbacks = {}
  end
  local index = 0
  if wave ~= nil then
    index = wave
  end
  if self.OnCompleteCallbacks[index] == nil then
    self.OnCompleteCallbacks[index] = {}
  end
  self.OnCompleteCallbacks[index][#self.OnCompleteCallbacks[index] + 1] = functionToCall
end
function Encounter:FireOnCompleteCallbacks(wave)
  local index = 0
  if wave ~= nil then
    index = wave
  end
  if self.OnCompleteCallbacks ~= nil and self.OnCompleteCallbacks[index] ~= nil then
    for i = 1, #self.OnCompleteCallbacks[index] do
      self.OnCompleteCallbacks[index][i]()
    end
  end
end
function Encounter:OnStart(functionToCall, wave)
  if self.OnStartCallbacks == nil then
    self.OnStartCallbacks = {}
  end
  local index = 0
  if wave ~= nil then
    index = wave
  end
  if self.OnStartCallbacks[index] == nil then
    self.OnStartCallbacks[index] = {}
  end
  self.OnStartCallbacks[index][#self.OnStartCallbacks[index] + 1] = functionToCall
end
function Encounter:FireOnStartCallbacks(wave)
  local index = 0
  if wave ~= nil then
    index = wave
  end
  if self.OnStartCallbacks ~= nil and self.OnStartCallbacks[index] ~= nil then
    for i = 1, #self.OnStartCallbacks[index] do
      self.OnStartCallbacks[index][i]()
    end
  end
end
function Encounter:Stop()
  if self.queuedData.deque_active then
    self.queuedData.queued_Calls[#self.queuedData.queued_Calls + 1] = function()
      self:Stop()
    end
    return
  end
  if self.started and not self.completed then
    self.stopped = true
    self:PlayerExitedCombatState(self.idleMusic)
    self:DisableUpdate()
    self:RemoveFromTracking()
    for i = 1, #self.waves do
      if self:IsRunning(i) then
        self.waves[i].waveStopped = true
      end
      self.waves[i].waveStarted = false
      for j = 1, #self.waves[i] do
        self:StopSpawnTimers(i, j)
      end
    end
  end
end
function Encounter:ReportWaveDespawned(wave)
  local waveNum = -1
  if wave ~= nil then
    waveNum = wave
  end
  game.Encounters.ReportWaveDespawned(self.Name, waveNum)
end
function Encounter:Reset(wave)
  if self.queuedData.deque_active then
    self.queuedData.queued_Calls[#self.queuedData.queued_Calls + 1] = function()
      self:Reset(wave)
    end
    return
  end
  wave = wave or nil
  if wave == nil then
    self:PlayerExitedCombatState(self.idleMusic)
    self:SetStarted(false)
    self:DisableUpdate()
    self.completed = false
    self.alertedEnemies = false
    self.currentWave = 0
    for i = 1, #self.waves do
      self.waves[i].alertedEnemies = false
      self.waves[i].waveComplete = false
      self.waves[i].waveStarted = false
      self.waves[i].lastSpawnerUsed = nil
      for j = 1, #self.waves[i] do
        self.waves[i][j].enemiesSpawned = self.waves[i][j].numEnemiesAlive
        if self.waves[i][j].enemiesSpawned == self.waves[i][j].totalSpawns then
          self.waves[i][j].livingEnemies = {}
        end
        self.waves[i][j].enemiesKilled = 0
        if self.waves[i][j].numPostAdded ~= nil and 0 < self.waves[i][j].numPostAdded then
          self.waves[i][j].totalSpawns = self.waves[i][j].totalSpawns - self.waves[i][j].numPostAdded
          self.waves[i][j].numPostAdded = nil
        end
        self:StopSpawnTimers(i, j)
      end
    end
    if self.startZoneMonitor ~= nil then
      if self.startZoneMonitor.LuaObjectScript ~= nil then
        self.startZoneMonitor.LuaObjectScript.Enable()
        self.startZoneMonitor.LuaObjectScript.SetZoneVisibility(true)
      else
        self.startZoneMonitor:Start()
      end
    end
    if self.alertZoneMonitor ~= nil then
      if self.alertZoneMonitor.LuaObjectScript ~= nil then
        self.alertZoneMonitor.LuaObjectScript.Enable()
        self.alertZoneMonitor.LuaObjectScript.SetZoneVisibility(true)
      else
        self.alertZoneMonitor:Start()
      end
    end
  else
    self.waves[wave].lastSpawnerUsed = nil
    self.waves[wave].alertedEnemies = false
    self.waves[wave].waveComplete = false
    self.waves[wave].waveStarted = false
    for j = 1, #self.waves[wave] do
      self.waves[wave][j].enemiesSpawned = self.waves[wave][j].numEnemiesAlive
      if self.waves[wave][j].enemiesSpawned == self.waves[wave][j].totalSpawns then
        self.waves[wave][j].livingEnemies = {}
      end
      self.waves[wave][j].enemiesKilled = 0
      self:StopSpawnTimers(wave, j)
    end
  end
  self:ReportWaveDespawned(wave)
  self:AddToTracking()
end
function Encounter:ForceReset(wave)
  if wave == nil then
    self:Reset()
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        self.waves[i][j].numEnemiesAlive = 0
        self.waves[i][j].enemiesSpawned = 0
        self.waves[i][j].livingEnemies = {}
      end
    end
  else
    self:Reset(wave)
    for j = 1, #self.waves[wave] do
      self.waves[wave][j].numEnemiesAlive = 0
      self.waves[wave][j].enemiesSpawned = 0
      self.waves[wave][j].livingEnemies = {}
    end
  end
  self:ReportWaveDespawned(wave)
end
function Encounter:Restart(wave)
  if self.queuedData.deque_active then
    self.queuedData.queued_Calls[#self.queuedData.queued_Calls + 1] = function()
      self:Restart(wave)
    end
    return
  end
  if wave == nil then
    self:Reset()
    self:Start()
  else
    self:Reset(wave)
    self:StartWave(wave)
  end
end
function Encounter:SetComplete(wave)
  wave = wave or nil
  if wave == nil then
    self:PlayerExitedCombatState()
    self.currentWave = #self.waves
    for i = 1, #self.waves do
      for j = 1, #self.waves[i] do
        self.waves[i][j].numEnemiesAlive = 0
        self.waves[i][j].enemiesSpawned = self.waves[i][j].totalSpawns
        self.waves[i][j].enemiesKilled = self.waves[i][j].totalSpawns
        self.waves[i][j].livingEnemies = {}
        self:StopSpawnTimers(i, j)
      end
      self:UpdateWaveCompletion(i)
    end
  else
    for j = 1, #self.waves[wave] do
      self.waves[wave][j].numEnemiesAlive = 0
      self.waves[wave][j].enemiesSpawned = self.waves[wave][j].totalSpawns
      self.waves[wave][j].enemiesKilled = self.waves[wave][j].totalSpawns
      self.waves[wave][j].livingEnemies = {}
      self:StopSpawnTimers(wave, j)
    end
    self:UpdateWaveCompletion(wave)
  end
  self:ReportWaveDespawned(wave)
end
function Encounter:AlertEnemies(wave)
  if self.queuedData.deque_active then
    self.queuedData.queued_Calls[#self.queuedData.queued_Calls + 1] = function()
      self:AlertEnemies(wave)
    end
    return
  end
  local minTime = 0.25
  local maxTime = 1.5
  local randomFn = function()
    return math.random(minTime * 1000, maxTime * 1000) / 1000
  end
  for k, enemy in ipairs(self:GetActiveEnemies(wave)) do
    if k == 1 then
      if enemy ~= nil then
        enemy:CallScript("LuaHook_SetInCombatState")
      end
    else
      timers.StartLevelTimer(randomFn(), function()
        if enemy ~= nil then
          enemy:CallScript("LuaHook_SetInCombatState")
        end
      end)
    end
  end
end
function Encounter:StartRecurringWaves(wave, cooldown)
  wave = wave or nil
  cooldown = cooldown or 0
  if wave == nil then
    for i = 1, #self.waves do
      self.waves[i].recurring = true
      self.waves[i].recurringCooldown = cooldown
    end
  else
    self.waves[wave].recurring = true
    self.waves[wave].recurringCooldown = cooldown
  end
end
function Encounter:StopRecurringWaves(wave)
  wave = wave or nil
  if wave == nil then
    for i = 1, #self.waves do
      self.waves[i].recurring = false
      self:UpdateWaveCompletion(i)
    end
  else
    self.waves[wave].recurring = false
    self:UpdateWaveCompletion(wave)
  end
end
function Encounter:StartInfiniteSpawning(wave)
  wave = wave or nil
  if wave == nil then
    for i = 1, #self.waves do
      self.waves[i].infiniteSpawning = true
    end
  else
    self.waves[wave].infiniteSpawning = true
  end
end
function Encounter:StopInfiniteSpawning(wave)
  wave = wave or nil
  if wave == nil then
    for i = 1, #self.waves do
      self.waves[i].infiniteSpawning = false
    end
  else
    self.waves[wave].infiniteSpawning = false
  end
end
function Encounter:DespawnEnemies(wave)
  wave = wave or nil
  if wave == nil then
    for i = 1, #self.waves do
      for j = #self.waves[i], 1, -1 do
        for k = #self.waves[i][j].livingEnemies, 1, -1 do
          if self.waves[i][j].livingEnemies[k] ~= nil then
            self.waves[i][j].livingEnemies[k]:Destroy()
            self.waves[i][j].numEnemiesAlive = 0
          end
        end
      end
    end
  else
    for i = #self.waves[wave], 1, -1 do
      for j = #self.waves[wave][i].livingEnemies, 1, -1 do
        if self.waves[wave][i].livingEnemies[j] ~= nil then
          self.waves[wave][i].livingEnemies[j]:Destroy()
          self.waves[wave][i].numEnemiesAlive = 0
        end
      end
    end
  end
  self:ReportWaveDespawned(wave)
end
function Encounter:ResetLivingEnemiesCount()
  for i = 1, #self.waves do
    for j = 1, #self.waves[i] do
      self.waves[i][j].numEnemiesAlive = 0
    end
  end
end
function Encounter:SetShowDebug(bool)
  if bool == true then
    self.showDebug.value = 1
  else
    self.showDebug.value = 0
  end
end
function Encounter:Debug()
  local debugTable = {}
  debugTable.TitleAlpha = 255
  local initialState = "Inactive"
  local startState = "Running..."
  local completeState = "Complete"
  if self.waitForSpawnWad then
    debugTable.Title = self.Name .. " (Waiting for spawn wad)"
    debugTable.TitleColor = engine.Vector.New(51, 204, 255)
  elseif self.completed == false and self.started == false then
    debugTable.Title = self.Name .. " (" .. initialState .. ")"
    debugTable.TitleColor = engine.Vector.New(255, 201, 14)
  elseif self.completed == false and self.started == true then
    debugTable.Title = self.Name .. " (" .. startState .. ")"
    debugTable.TitleColor = engine.Vector.New(27, 116, 25)
  elseif self.completed == true then
    debugTable.Title = self.Name .. " (" .. completeState .. ")"
    debugTable.TitleColor = engine.Vector.New(0, 0, 0)
  else
    debugTable.Title = "Encounter complete: " .. tostring(self.completed)
    debugTable.TitleColor = engine.Vector.New(1, 0, 0)
  end
  if self.spawnWad ~= nil then
    debugTable[#debugTable + 1] = {
      "[Spawn wad - \"" .. self.spawnWad .. "\"]"
    }
  end
  local enemyInfo = "Total: " .. self:GetTotalSpawns() .. "      Spawned: " .. self:GetNumEnemiesSpawned() .. "      Active: " .. #self:GetActiveEnemies() .. "      Killed: " .. self:GetNumEnemiesKilled()
  debugTable[#debugTable + 1] = {enemyInfo}
  debugTable[#debugTable + 1] = {""}
  for i = 1, #self.waves do
    local waveHeader = ""
    if i == self.currentWave and self.waves[self.currentWave].waveComplete == false then
      waveHeader = "* " .. waveHeader
    end
    if self:WavesComplete(i) == false and self:IsRunning(i) == false then
      if self.waves[i].waveStopped then
        initialState = "Paused"
      else
        initialState = "Inactive"
      end
      waveHeader = waveHeader .. "Wave " .. i .. " (" .. initialState .. ")"
    elseif self:WavesComplete(i) == false and self:IsRunning(i) and self:PercentageCriteriaMet(i) == false then
      waveHeader = waveHeader .. "Wave " .. i .. " (" .. startState .. ")"
    else
      waveHeader = waveHeader .. "Wave " .. i .. " (" .. completeState .. ")"
    end
    local additionalInfo = ", " .. tostring(self.waves[i].spawnPattern) .. " spawns"
    if self.waves[i].triggerWaveFromScript then
      additionalInfo = additionalInfo .. ", Triggered from script"
    end
    if self.waves[i].spawnWad ~= nil then
      additionalInfo = additionalInfo .. ", Spawn wad: " .. self.waves[i].spawnWad
    end
    if self.waves[i].recurring then
      additionalInfo = additionalInfo .. ", Recurring"
    elseif self.waves[i].infiniteSpawning then
      additionalInfo = additionalInfo .. ", Infinite spawns"
    end
    if self.waves[i].requiredWaveCompletion < 100 then
      additionalInfo = additionalInfo .. ", " .. tostring(self.waves[i].requiredWaveCompletion) .. "% required"
    end
    if self.waves[i].markerID then
      additionalInfo = additionalInfo .. ", Marker: " .. tostring(self.waves[i].markerID)
    end
    if self.waves[i].requireCompletionOfAllPriorWaves then
      additionalInfo = additionalInfo .. ", Require prior waves"
    end
    debugTable[#debugTable + 1] = {
      waveHeader .. additionalInfo
    }
    if self:WavesComplete(i) == false or self:GetNumEnemiesKilled(i) < self:GetTotalSpawns(i) then
      for j = 1, #self.waves[i] do
        local desiredString = ""
        for k = 1, #self.waves[i][j].spawners do
          if self.waves[i][j].spawners[k].LuaObjectScript ~= nil then
            if string.find(desiredString, self.waves[i][j].spawners[k].LuaObjectScript.GetEnemyType()) == nil then
              if 1 < k then
                desiredString = desiredString .. ", " .. self.waves[i][j].spawners[k].LuaObjectScript.GetEnemyType()
              else
                desiredString = desiredString .. self.waves[i][j].spawners[k].LuaObjectScript.GetEnemyType()
              end
            end
            if self.showDebug.value == 2 then
              local spawnPos = self.waves[i][j].spawners[k].LuaObjectScript.GetSpawnPos()
              local approachPos = self.waves[i][j].spawners[k].LuaObjectScript.GetApproachPos()
              if self.waves[i].spawnPattern == "Ordered" then
                local spawnNum = GetLastNumberInString(self.waves[i][j].spawners[k]:GetName())
                engine.DrawTextInWorld(spawnPos, tostring(spawnNum), 65280, -2)
              end
              if self.waves[i][j].spawners[k].LuaObjectScript.enabled then
                engine.DrawFillSphere(spawnPos, 0.2, 6338410)
                engine.DrawSphere(approachPos, 0.1, 6338410)
                engine.DrawLine(spawnPos, approachPos, 6338410)
              else
                engine.DrawFillSphere(spawnPos, 0.2, 16711680)
                engine.DrawSphere(approachPos, 0.1, 16711680)
                engine.DrawLine(spawnPos, approachPos, 16711680)
              end
            end
          end
          if self.showDebug.value == 2 and self.waves[i][j].spawnLocators ~= nil then
            for n = 1, #self.waves[i][j].spawnLocators do
              local locator = self.waves[i][j].spawnLocators[n]
              local spawnPos = locator.Child:GetWorldPosition()
              local approachPos = locator.Child:GetWorldJointPosition(locator.Child:GetJointIndex("ApproachPoint"))
              if self.availableLocators[locator:GetName()] then
                engine.DrawFillSphere(spawnPos, 0.2, 6338410)
                engine.DrawLine(spawnPos, approachPos, 6338410)
                engine.DrawSphere(approachPos, 0.2, 6338410)
              else
                engine.DrawFillSphere(spawnPos, 0.2, 16711680)
                engine.DrawLine(spawnPos, approachPos, 16711680)
                engine.DrawSphere(approachPos, 0.2, 16711680)
              end
            end
          end
        end
        if desiredString == "" then
          desiredString = "Summoned creature"
        end
        desiredString = string.gsub(desiredString, "CRT_", "")
        local cooldownString = ""
        local timer = self.waves[i][j].spawnCooldownTimer
        if timer ~= nil and timer.running then
          cooldownString = cooldownString .. string.format("%1.1f", timer:GetRemainingTime())
        else
          cooldownString = cooldownString .. "-- "
        end
        debugTable[#debugTable + 1] = {
          "  [" .. desiredString .. "] " .. self.waves[i][j].enemiesSpawned .. "/" .. self.waves[i][j].totalSpawns .. " Spawned (" .. self.waves[i][j].maxActive .. " max) || " .. self.waves[i][j].enemiesKilled .. "/" .. self.waves[i][j].totalSpawns .. " Killed || Next: " .. cooldownString
        }
        if self.waves[i][j].spawnWad ~= nil then
          debugTable[#debugTable + 1] = {
            "\t [Spawn wad - \"" .. self.waves[i][j].spawnWad .. "\"]"
          }
        end
      end
      debugTable[#debugTable + 1] = {""}
    end
    if self.completed == false then
      if 0 < self.waves[i].timeBeforeNextWave and self.waves[i].timeBeforeNextWaveTimer ~= nil then
        debugTable[#debugTable + 1] = {
          "Next wave starts in -  " .. tostring(math.ceil(self.waves[i].timeBeforeNextWave - self.waves[i].timeBeforeNextWaveTimer.time)) .. "s..."
        }
      end
      if 0 < self.waves[i].timeBetweenWaves and self.waves[i].timeBetweenWavesTimer ~= nil then
        debugTable[#debugTable + 1] = {
          "Next wave starts in -  " .. tostring(math.ceil(self.waves[i].timeBetweenWaves - self.waves[i].timeBetweenWavesTimer.time)) .. "s..."
        }
      end
    end
  end
  debugTable.X = self.debugX
  debugTable.Y = self.debugY
  engine.DrawDebugTable(debugTable)
end
function Encounter:DebugInput()
  if self.player.Pad.L1Down and self.player.Pad.L2Down then
    local debugEnabled = self.showDebug.value > 0
    if self.player.Pad.RightDown then
      if self.debugInputAllowed and debugEnabled then
        self.debugInputAllowed = false
        if self.started == false then
          self:Restart()
        elseif self.completed then
          self:Restart()
        else
          self:StartWave(self.currentWave + 1)
        end
      end
    elseif self.player.Pad.LeftDown then
      if self.debugInputAllowed and debugEnabled then
        self.debugInputAllowed = false
        for i = 1, #self.waves do
          if self:IsRunning(i) then
            self:SetComplete(i)
            self:DespawnEnemies()
            break
          end
        end
      end
    elseif self.player.Pad.DownDown then
      if self.debugInputAllowed then
        self.debugInputAllowed = false
        if not (not self.started or self.completed) or self.completed and debugEnabled then
          if debugEnabled then
            self:SetShowDebug(false)
          else
            self:SetShowDebug(true)
          end
        end
      end
    elseif self.player.Pad.DampedTouchPoint.y < -180 then
      if self.debugY == nil then
        self.debugY = 0
      end
      if self.debugY + 3 >= 60 then
        self.debugY = 60
      else
        self.debugY = self.debugY + 3
      end
    elseif self.player.Pad.DampedTouchPoint.y > -60 then
      if self.debugY == nil then
        self.debugY = 0
      end
      if 0 >= self.debugY - 3 then
        self.debugY = 0
      else
        self.debugY = self.debugY - 3
      end
    elseif self.player.Pad.DampedTouchPoint.x > 167 then
      if self.debugX == nil then
        self.debugX = 0
      end
      if self.debugX + 5 >= 250 then
        self.debugX = 250
      else
        self.debugX = self.debugX + 5
      end
    elseif self.player.Pad.DampedTouchPoint.x < 87 then
      if self.debugX == nil then
        self.debugX = 0
      end
      if 0 >= self.debugX - 5 then
        self.debugX = 0
      else
        self.debugX = self.debugX - 5
      end
    else
      self.debugInputAllowed = true
    end
  end
end
local GetSpawnSequenceInfoFromLuaTableAttr = function(go, goAttr)
  return go:GetSpawnSequenceInfoFromLuaTableAttr(goAttr)
end
return profile.WrapLibrary({
  Encounter = Encounter,
  NewEncounter = NewEncounter,
  GetSpawnSequenceInfoFromLuaTableAttr = GetSpawnSequenceInfoFromLuaTableAttr
})
