local class = require("core.class")
local statemachine = require("ai.statemachine")
local locomotion = require("creature.locomotion")
local thunk = require("core.thunk")
local sonAggro = require("behavior.aggroSystem")
local incombatlib = require("behavior.incombat")
local poilib = require("behavior.poi")
local positioning = require("behavior.positioning")
local unaware = require("awareness.unaware")
local tablex = require("core.tablex")
local DL = require("design.DesignerLibrary")
local LD = require("design.LevelDesignLibrary")
local CoreBrain = class.Class("CoreBrain")
local Brain = statemachine.StateMachine.New("CoreBrain")
local state_machine_list = {Brain}
local STATE_NonHostile = Brain:State("NonHostile")
statemachine.AddTags(STATE_NonHostile, "NonHostile")
local STATE_Combat = Brain:State("Combat", incombatlib.InCombat)
statemachine.AddTags(STATE_Combat, "Combat")
local STATE_InAggro = Brain:Action("InAggro")
local STATE_Unaware = Brain:State("Unaware", unaware.Unaware) .. {sixthSenseDistance = 8}
statemachine.AddTags(STATE_Unaware, "Unaware")
local STATE_SonAggro = Brain:State("SonAggro", sonAggro.AggroSystem)
local STATE_UsePOI = poilib.NewPOIState(Brain, "UsePOI")
poilib.AllowPOIFromStates(STATE_Combat, STATE_Unaware, STATE_InAggro, STATE_NonHostile)
local STATE_Fallback = Brain:State("Fallback")
local STATE_Leash = Brain:State("Leash-Retreat")
local STATE_LeashDefend = Brain:State("Leash-Defend")
local player, son, creature_table, customDebugTable, previousAggroState, debugPosResult, playerbb
local cameraTargetSwitch = false
local possessorFlyerPowerLevel = 0
local MusicMath = function(newAmountToAdd)
  local musicnumber = LD.GetEntityVariable("MusicIntensity")
  musicnumber = musicnumber + newAmountToAdd
  return musicnumber
end
local SetupDefaultCreatureAIData = function(ai, global, constants)
  constants.fightPosDataDefault = {}
  constants.fightPosDataDefault.OccupancyRadius = 1.1
  constants.fightPosDataDefault.OccupancyPriority = 0
  constants.fightPosDataDefault.AggressiveMass = 50
  constants.fightPosDataDefault.NonAggressiveMass = 20
  constants.fightPosDataDefault.AggressiveMinDistance = 2
  constants.fightPosDataDefault.AggressiveMaxDistance = 4
  constants.fightPosDataDefault.NonAggressiveMinDistance = 10
  constants.fightPosDataDefault.NonAggressiveMaxDistance = 15
  constants.DefaultZoneSet = "Default"
  constants.DefaultNavBank = "NB_OffensiveBank"
  constants.fightPosDataSonTarget = {}
  constants.fightPosDataSonTarget.OccupancyRadius = 1.2
  constants.fightPosDataSonTarget.OccupancyPriority = 1
  constants.fightPosDataSonTarget.AggressiveMass = 20
  constants.fightPosDataSonTarget.NonAggressiveMass = 10
  constants.fightPosDataSonTarget.AggressiveMinDistance = 5
  constants.fightPosDataSonTarget.AggressiveMaxDistance = 7.5
  constants.fightPosDataSonTarget.NonAggressiveMinDistance = 5
  constants.fightPosDataSonTarget.NonAggressiveMaxDistance = 7.5
  constants.fightPosDataSonTarget.AggressiveMinAngle = -20
  constants.fightPosDataSonTarget.AggressiveMaxAngle = 20
  constants.fightPosDataSonTarget.NonAggressiveMinAngle = -20
  constants.fightPosDataSonTarget.NonAggressiveMaxAngle = 20
  constants.fightPosDataSonTarget.SonOnly = true
  constants.moveOptions = {}
  constants.damageCounterDecayRate = 0
  constants.DisableFacingTarget = false
  constants.NoEvadeOnThrow = true
  constants.maxPowerLevel = 1
  constants.hasElitePower = false
  global.target = nil
  global.currentPOI = nil
  global.fightConstants = {}
  global.powerLevel = "1"
  global.awarenessBroadcastRange = 100
  global.awarenessReceiveRange = 30
  global.targetParams = "FIND_TARGET_PARAMETERS_COMBAT_KRATOS_ONLY"
  global.navData = {}
  global.navData.stopDistance = 0.5
  global.navData.startDistance = 1
  global.navData.isStrafing = true
  global.navData.navSpeedJog = 0
  global.navData.navSpeedWalk = 0
  global.useLeashing = true
  constants.fightConstantsDefault = {}
  constants.fightConstantsDefault.AggressiveRange = 12
  constants.fightConstantsDefault.DesiredRadius = 1
  global.fightConstants = constants.fightConstantsDefault
  global.debugPosStart = nil
  global.debugPosEnd = nil
  global.debugPosDistanceXZ = nil
  global.debugPosDistanceY = nil
  global.debugPosSwitch = true
  constants.returnRadius = 1.25
  constants.maxLeashDist = 10
  constants.fallbackTime = 3.5
end
local GetFlyerSpawnConfigString = function(pLevel)
  if 4 < pLevel then
    return "Flyer_posses_hard"
  end
  return "Flyer_posses_easy"
end
local PowerToString = function(powerLevelInt)
  local powerLevel = "1"
  if powerLevelInt == 1 then
    powerLevel = "1"
  elseif powerLevelInt == 2 then
    powerLevel = "2"
  elseif powerLevelInt == 3 then
    powerLevel = "3"
  elseif powerLevelInt == 4 then
    powerLevel = "4"
  elseif powerLevelInt == 5 then
    powerLevel = "5"
  elseif powerLevelInt == 6 then
    powerLevel = "6"
  elseif powerLevelInt == 7 then
    powerLevel = "7"
  elseif powerLevelInt == 8 then
    powerLevel = "8"
  elseif powerLevelInt == 9 then
    powerLevel = "9"
  elseif powerLevelInt == 10 then
    powerLevel = "10"
  elseif powerLevelInt == 11 then
    powerLevel = "11"
  elseif powerLevelInt == 12 then
    powerLevel = "12"
  elseif powerLevelInt == 13 then
    powerLevel = "13"
  elseif powerLevelInt == 14 then
    powerLevel = "14"
  elseif powerLevelInt == 15 then
    powerLevel = "15"
  elseif powerLevelInt == 15 then
    powerLevel = "16"
  elseif powerLevelInt == 15 then
    powerLevel = "17"
  elseif powerLevelInt == 15 then
    powerLevel = "18"
  elseif powerLevelInt == 15 then
    powerLevel = "19"
  else
    engine.Warning("Attempting to override power level with unsupported value. Increase current max supported from (15) to new value in EnemySpawner.lua")
  end
  return powerLevel
end
local UnpossessSpawnFlyer = function(ai)
  if ai:PickupIsAcquired("PossessedByFlyer") and ai:PickupGetStage("PossessedByFlyer") == 0 then
    local possessPickup = ai:GetName():gsub("%d", "") .. "00_Possessed"
    if ai:PickupIsAcquired(possessPickup) then
      ai:PickupRelinquish(possessPickup)
    end
    if not ai:PickupIsAcquired(possessPickup) then
      ai:PickupIncrementStage("PossessedByFlyer")
      local power = PowerToString(possessorFlyerPowerLevel)
      if game.CHECK_FEATURE("PREEMPT_SPAWN_CONFIG") then
        local charConfig = GetFlyerSpawnConfigString(possessorFlyerPowerLevel)
        local spawnedFlyer = game.AI.Spawn(ai.Level, ai.WorldPosition, ai:GetWorldForward(), "CRT_Flyer10", "BRA_SpawnFromUnpossess", {PowerLevel = power, FlyerType = "Possess"}, charConfig)
        if spawnedFlyer ~= nil and game.Encounters.AddSpawnToEncounter then
          game.Encounters.AddSpawnToEncounter(ai, spawnedFlyer)
        end
      else
        local spawnedFlyer = game.AI.Spawn(ai.Level, ai.WorldPosition, ai:GetWorldForward(), "CRT_Flyer10", "BRA_SpawnFromUnpossess")
        if spawnedFlyer ~= nil then
          spawnedFlyer:CallScript("LuaHook_SetSpawnParamsFromWitch", possessorFlyerPowerLevel, "Possess")
          if game.Encounters.AddSpawnToEncounter then
            game.Encounters.AddSpawnToEncounter(ai, spawnedFlyer)
          end
        end
      end
    end
  end
end
function LuaHook_FlyerUnpossess(ai, data)
  UnpossessSpawnFlyer(ai)
end
local GiveHeroBonusMomentum = function()
  local currentComboMeter
  if player ~= nil then
    local currentWeapon = player:GetCurrentWeapon()
    if player:PickupIsAcquired("MomentumMeter") and player:HasMeter("Momentum") then
      if currentWeapon == "Axe" then
        currentComboMeter = player.MeterGetValue(player, "Momentum")
        player:MeterSetValue("Momentum", currentComboMeter + 2)
        player:CallScript("LuaHook_MomentumAxeIncrease")
      elseif player.Axe.ThrowOutStatus == tweaks.tThrowOutStatus.eThrownWeaponStatus.kTOSSuspended or player.Axe.ThrowOutStatus == tweaks.tThrowOutStatus.eThrownWeaponStatus.kTOSInFlightReturn then
        currentComboMeter = player.MeterGetValue(player, "Momentum")
        player:MeterSetValue("Momentum", currentComboMeter + 2)
        player:CallScript("LuaHook_MomentumAxeIncrease")
      end
    end
    if player:PickupIsAcquired("MomentumBladesMeter") and player:HasMeter("MomentumBlades") and currentWeapon == "Blades" then
      currentComboMeter = player.MeterGetValue(player, "MomentumBlades")
      player:MeterSetValue("MomentumBlades", currentComboMeter + 1)
      player:CallScript("LuaHook_MomentumBladesIncrease")
    end
  end
end
local SetupOverrideCreatureAIData = function(ai, global, constants, pCreatureTable)
  if package.preload[pCreatureTable] == nil then
    return
  end
  if pCreatureTable ~= nil then
    creature_table = pCreatureTable
    print("init in core brain")
    local datatable = require(creature_table)
    for k, v in pairs(datatable.global) do
      if type(v) == "table" then
        for a, b in pairs(v) do
          global[k][a] = b
        end
      else
        global[k] = v
      end
    end
    for k, v in pairs(datatable.constants) do
      if constants[k] ~= nil and type(v) == "table" then
        for a, b in pairs(v) do
          constants[k][a] = b
        end
      else
        constants[k] = v
      end
    end
    package.loaded[pCreatureTable] = nil
    _G[pCreatureTable] = nil
  end
  constants.fallbackDTree = constants.fallbackDTree or global.DTree
end
local AddNewBrainState = function(global, newState, newStateName)
  local state_table = {state = newState, active = false}
  if global.tAdditionalCombatState == nil then
    global.tAdditionalCombatState = {}
  end
  global.tAdditionalCombatState[newStateName] = state_table
end
local SetPowerLevel = function(ai, global, constants, level, elite)
  if level > constants.maxPowerLevel then
    level = constants.maxPowerLevel
  end
  local pickup = "%s_PowerLevel"
  local pickupStage = level - 1
  if (level == 1 or level == 2) and ai:AttributeGetValue("Difficulty") >= 4 then
    ai:PickupAcquire("GiveMeGodOfWar_DifficultyDefenseOffset")
  elseif ai:PickupIsAcquired("GiveMeGodOfWar_DifficultyDefenseOffset") then
    ai:PickupRelinquish("GiveMeGodOfWar_DifficultyDefenseOffset")
  end
  pickup = string.format(pickup, ai:GetName())
  if not game.Pickup.Exists(pickup) then
    print("not power level pickup defined for " .. pickup)
    return
  end
  if ai:PickupIsAcquired(pickup) then
    ai:PickupRelinquish(pickup)
  end
  if elite then
    if ai:PickupIsAcquired(pickup .. "Elite") then
      ai:PickupRelinquish(pickup .. "Elite")
    end
    pickup = pickup .. "Elite"
  end
  if not ai:PickupIsAcquired(pickup) then
    ai:PickupAcquire(pickup)
  end
  ai:PickupSetStage(pickup, pickupStage)
  if elite then
    global.powerLevel = "Elite"
  else
    global.powerLevel = level
  end
end
function CoreBrain:init()
end
function CoreBrain:OnAIPreSpawn(ai)
  if StartConfig ~= nil and StartConfig.NonCombatState ~= nil and (StartConfig.NonCombatState == "Wander" or string.find(StartConfig.NonCombatState, "Patrol") ~= nil) then
    return ""
  end
end
function CoreBrain:OnAIPostSpawn(ai, global, constants)
  local disableStun = false
  if StartConfig ~= nil then
    if StartConfig.SonKillerLevel ~= nil then
      if StartConfig.SonKillerLevel == "Stun" then
        ai:PickupAcquire("SonKiller", 0)
      elseif StartConfig.SonKillerLevel == "Incapacitate" then
        ai:PickupAcquire("SonKiller", 1)
      elseif StartConfig.SonKillerLevel == "Kill" then
        ai:PickupAcquire("SonKiller", 2)
      end
    end
    if StartConfig.PowerLevel ~= nil then
      local elite = false
      local level = 1
      if StartConfig.PowerLevel == "2" then
        level = 2
      elseif StartConfig.PowerLevel == "3" then
        level = 3
      elseif StartConfig.PowerLevel == "4" then
        level = 4
      elseif StartConfig.PowerLevel == "5" then
        level = 5
      elseif StartConfig.PowerLevel == "6" then
        level = 6
      elseif StartConfig.PowerLevel == "7" then
        level = 7
      elseif StartConfig.PowerLevel == "8" then
        level = 8
      elseif StartConfig.PowerLevel == "Elite" and constants.hasElitePower then
        elite = true
      end
      if game.Level.GetVariable("TB_UseDebugAISpawnAtPowerLevel") then
        level = game.Level.GetVariable("TB_DebugAISpawnPowerLevel")
      end
      SetPowerLevel(ai, global, constants, level, elite)
      local overridePowerLevel = not game.CHECK_FEATURE("PREEMPT_SPAWN_CONFIG")
      self:SetPowerlevelOverride(ai, global, constants, level, elite, overridePowerLevel)
    end
    if StartConfig.AggroState ~= nil then
      global.aggroState = string.upper(StartConfig.AggroState)
    end
    if StartConfig.DontDropLoot then
      ai:ForceMove("BRA_SetDontDropLootFlag_On")
    end
    if StartConfig.DropHealth then
      ai:ForceMove("BRA_SetDropHealthFlag_On")
    end
    if StartConfig.DropRage then
      ai:ForceMove("BRA_SetDropRageFlag_On")
    end
    if StartConfig.DisableStun then
      disableStun = true
    end
  end
  if constants.theaterInfluenceSettings ~= nil then
    if constants.theaterInfluenceSettings.coneAngle ~= nil then
      ai:SetInfluenceConeAngle(constants.theaterInfluenceSettings.coneAngle)
    end
    if constants.theaterInfluenceSettings.coneAngle ~= nil then
      ai:SetInfluenceConeAngle(constants.theaterInfluenceSettings.coneAngle)
    end
    if constants.theaterInfluenceSettings.coneLength ~= nil then
      ai:SetInfluenceConeLength(constants.theaterInfluenceSettings.coneLength)
    end
    if constants.theaterInfluenceSettings.coneIntensity ~= nil then
      ai:SetInfluenceConeIntensity(constants.theaterInfluenceSettings.coneIntensity)
    end
    if constants.theaterInfluenceSettings.coneDecayRate ~= nil then
      ai:SetInfluenceConeDecay(constants.theaterInfluenceSettings.coneDecayRate)
    end
    ai:SetInfluenceConeIsEnabled(true)
    if constants.theaterInfluenceSettings.circleRadius ~= nil then
      ai:SetInfluenceCircleRadius(constants.theaterInfluenceSettings.circleRadius)
    end
    if constants.theaterInfluenceSettings.circleIntensity ~= nil then
      ai:SetInfluenceCircleIntensity(constants.theaterInfluenceSettings.circleIntensity)
    end
    if constants.theaterInfluenceSettings.circleDecayRate ~= nil then
      ai:SetInfluenceCircleDecay(constants.theaterInfluenceSettings.circleDecayRate)
    end
    ai:SetInfluenceCircleIsEnabled(true)
  end
  local creatureID = ai:GetID()
  if game.GetNewGamePlus() and game.GetCineNumber() < 60 then
    disableStun = false
  end
  if creatureID ~= DL.HashCreatureID(ai, "MAGNI00") and creatureID ~= DL.HashCreatureID(ai, "MODI00") and disableStun ~= true then
    ai:PickupAcquire("HealthThreshold")
  end
  if creatureID == DL.HashCreatureID(ai, "TROLL00") or creatureID == DL.HashCreatureID(ai, "TROLL10") or creatureID == DL.HashCreatureID(ai, "TROLL20") or creatureID == DL.HashCreatureID(ai, "TROLL30") or creatureID == DL.HashCreatureID(ai, "JOTUNN00") or creatureID == DL.HashCreatureID(ai, "JOTUNN10") or creatureID == DL.HashCreatureID(ai, "JOTUNN20") or creatureID == DL.HashCreatureID(ai, "GOLEM00") or creatureID == DL.HashCreatureID(ai, "GOLEM10") or creatureID == DL.HashCreatureID(ai, "GOLEM20") or creatureID == DL.HashCreatureID(ai, "GOLEM30") then
    ai:PickupAcquire("BlockScorpionToss")
  end
  if creatureID == DL.HashCreatureID(ai, "DARKONEELITE00") or creatureID == DL.HashCreatureID(ai, "DRAGON00") or creatureID == DL.HashCreatureID(ai, "TROLL00") or creatureID == DL.HashCreatureID(ai, "TROLL10") or creatureID == DL.HashCreatureID(ai, "TROLL20") or creatureID == DL.HashCreatureID(ai, "TROLL30") or creatureID == DL.HashCreatureID(ai, "VALKYRIE00") then
    LD.SetEntityVariable("MusicIntensity", MusicMath(30))
  end
  if creatureID == DL.HashCreatureID(ai, "GOLEM00") or creatureID == DL.HashCreatureID(ai, "GOLEM10") or creatureID == DL.HashCreatureID(ai, "GOLEM20") or creatureID == DL.HashCreatureID(ai, "GOLEM30") or creatureID == DL.HashCreatureID(ai, "JOTUNN00") or creatureID == DL.HashCreatureID(ai, "JOTUNN01") or creatureID == DL.HashCreatureID(ai, "JOTUNN02") or creatureID == DL.HashCreatureID(ai, "JOTUNN10") or creatureID == DL.HashCreatureID(ai, "JOTUNN20") or creatureID == DL.HashCreatureID(ai, "WITCH00") or creatureID == DL.HashCreatureID(ai, "WITCH10") or creatureID == DL.HashCreatureID(ai, "WITCH20") or creatureID == DL.HashCreatureID(ai, "WITCH30") or creatureID == DL.HashCreatureID(ai, "WULVER00") or creatureID == DL.HashCreatureID(ai, "WULVER10") then
    LD.SetEntityVariable("MusicIntensity", MusicMath(10))
  end
  if creatureID == DL.HashCreatureID(ai, "BANDIT00") or creatureID == DL.HashCreatureID(ai, "BANDIT02") or creatureID == DL.HashCreatureID(ai, "BANDITHUMAN00") or creatureID == DL.HashCreatureID(ai, "BRAWLER00") or creatureID == DL.HashCreatureID(ai, "DARKONE00") or creatureID == DL.HashCreatureID(ai, "DRAUGR00") or creatureID == DL.HashCreatureID(ai, "FANATIC00") or creatureID == DL.HashCreatureID(ai, "PROJECTION00") or creatureID == DL.HashCreatureID(ai, "PROJECTION10") or creatureID == DL.HashCreatureID(ai, "PROJECTION20") or creatureID == DL.HashCreatureID(ai, "TRAVELER00") or creatureID == DL.HashCreatureID(ai, "TRAVELER10") or creatureID == DL.HashCreatureID(ai, "TRAVELER20") then
    LD.SetEntityVariable("MusicIntensity", MusicMath(3))
  end
  if creatureID == DL.HashCreatureID(ai, "CRAWLER00") or creatureID == DL.HashCreatureID(ai, "CRAWLER10") or creatureID == DL.HashCreatureID(ai, "FENRIR00") or creatureID == DL.HashCreatureID(ai, "FLYER00") or creatureID == DL.HashCreatureID(ai, "FLYER10") or creatureID == DL.HashCreatureID(ai, "WOLF00") or creatureID == DL.HashCreatureID(ai, "WOLF10") then
    LD.SetEntityVariable("MusicIntensity", MusicMath(1))
  end
end
function CoreBrain:OnAICreateLuaState(ai, global, constants)
  player = game.Player.FindPlayer()
  son = game.AI.FindSon()
  locomotion.Install()
  SetupDefaultCreatureAIData(ai, global, constants)
  local ai_name = ai:GetName()
  local CreatureTable = string.format("%s.table", ai_name)
  SetupOverrideCreatureAIData(ai, global, constants, CreatureTable)
  self:SetPoiStates(ai_name)
  global.navBank = constants.DefaultNavBank
  ai:SetNavBank(global.navBank)
  constants.fightKnowledgeInputs = positioning.CreateStandardPositioningInputs(constants.fightPosDataDefault)
  positioning.AddStandardPositioningZone("SonTargeting", constants.fightPosDataSonTarget, constants.fightKnowledgeInputs)
  global.navData.navSpeedWalk = ai:LookupFloatConstant("NAV_SPEED_WALK") or 1.3
  global.navData.navSpeedJog = ai:LookupFloatConstant("NAV_SPEED_JOG") or 2.25
  global.aggroState = "UNAWARE"
  if global.navData.navSpeedWalk > global.navData.navSpeedJog then
    engine.Warning(string.format("%s walk speed > jog speed. (%0.2f > %0.2f)", ai:GetName(), global.navData.navSpeedWalk, global.navData.navSpeedJog))
  end
  self.damageCounter = 0
  self:SetupUnaware(global, constants)
  local name = ai:GetName()
  name = string.upper(name:gsub("%d", ""))
  poilib.SetCharacterType(STATE_UsePOI, name)
  statemachine.StartAll(ai, state_machine_list, ai, global, constants)
end
function CoreBrain:Update(ai, global, constants)
  if engine.IsDebug() then
    playerbb = player:GetBlackboard()
    if playerbb ~= nil and playerbb:Exists("DEBUG_TurnOnPositionDistanceDebug") and playerbb:IsBoolean("DEBUG_TurnOnPositionDistanceDebug") and playerbb:GetBoolean("DEBUG_TurnOnPositionDistanceDebug") == true then
      debugPosResult = DL.PositionDistanceDebug(ai, global.debugPosStart, global.debugPosEnd, global.debugPosSwitch)
      global.debugPosStart = debugPosResult.debugPosStart
      global.debugPosEnd = debugPosResult.debugPosEnd
      global.debugPosDistanceXZ = debugPosResult.debugPosDistanceXZ
      global.debugPosDistanceY = debugPosResult.debugPosDistanceY
      global.debugPosSwitch = debugPosResult.debugPosSwitch
    end
  end
  CoreBrain:UpdateAggroState(ai, global)
  if self.damageCounter > 0 then
    self.damageCounter = self.damageCounter - ai:GetUnitTime() * constants.damageCounterDecayRate
  end
  if cameraTargetSwitch == false and ai:CheckDecision("tweak_Decision_OnCamera_Advanced") == false then
    cameraTargetSwitch = true
  elseif cameraTargetSwitch == true and ai:CheckDecision("tweak_Decision_OnCamera_Advanced") == true then
    cameraTargetSwitch = false
  end
  locomotion.CreateDrivers(ai)
  statemachine.UpdateAll(state_machine_list, ai, global, constants)
end
function CoreBrain:UpdateAggroState(ai, global)
  if (global.aggroState == "INCOMBAT" or global.aggroState == "INAGGRO") and (global.target ~= nil and global.target:OnActiveTraversePath() or game.Player.FindPlayer():HasMarker("QuestGiverInteract")) then
    global.aggroState = "UNAWARE"
  end
  if previousAggroState ~= global.aggroState then
    ai:SetAggroState(global.aggroState)
  end
  previousAggroState = global.aggroState
end
function CoreBrain:CreatureUpdate(ai, global, constants)
  if not ai:DebugIsSelectedCreature() then
    return
  end
  local debugTable = statemachine.GetDebugTable(state_machine_list, ai)
  if debugTable then
    self:DrawDebugTable(debugTable, global, ai)
  end
end
function CoreBrain:OnAIUnleashed(ai, global, constants)
  global.useLeashing = false
  global.aggroState = "INCOMBAT"
  global.leashZone = nil
  global.combatZone = nil
  global.target = player
  ai:SetLeashZone("")
end
function CoreBrain:OnAIReleashed(ai, global, constants, leashZoneName)
  global.useLeashing = true
  global.leashZone = leashZoneName
end
function CoreBrain:SetPoiStates(ai_name)
  if ai_name == "troll10" or ai_name == "troll20" then
    ai_name = "troll00"
  end
  local poiStateName = string.format("%s.POIStates", ai_name)
  if package.preload[poiStateName] == nil then
    return
  end
  local setupPOI = require(poiStateName).SetupPOIStates
  assert(type(setupPOI) == "function", "Invalid POIStates, make sure it follows the standard setup. See Henry if you need help.")
  setupPOI(STATE_UsePOI)
end
function CoreBrain:HasActiveState(global)
  if global.tAdditionalCombatState == nil then
    return nil
  end
  for _, v in pairs(global.tAdditionalCombatState) do
    if v.active == true then
      return v.state
    end
  end
  return nil
end
function CoreBrain:SetupUnaware(global, constants)
  if constants.unawareTargetParams ~= nil then
    STATE_Unaware.unawareTargetParams = constants.unawareTargetParams
  end
  if constants.axeDetectionRange ~= nil then
    STATE_Unaware.axeDetectionRange = constants.axeDetectionRange
  end
  if constants.sixthSenseDistance ~= nil then
    STATE_Unaware.sixthSenseDistance = constants.sixthSenseDistance
  end
end
function CoreBrain:GetPositioning()
  return positioning
end
function CoreBrain:AddStateMachine(pStateMachine)
  table.insert(state_machine_list, pStateMachine)
end
function CoreBrain:AddTimer(pTimer, args)
  local timer = Brain:Timer(pTimer) .. {
    period = args.period,
    autostart = args.autostart
  }
  return timer
end
function CoreBrain:AddBrainAction(pActionName, global)
  local newAction = Brain:Action(pActionName)
  newAction.name = pActionName
  AddNewBrainState(global, newAction, pActionName)
  return newAction
end
function CoreBrain:AddBrainState(pStateName, global)
  local newState = Brain:State(pStateName)
  newState.name = pStateName
  AddNewBrainState(global, newState, pStateName)
  return newState
end
function CoreBrain:AddCustomDebugTable(pDebugTable)
  customDebugTable = pDebugTable
end
function CoreBrain:ActivateCombatState(pStateName, global)
  global.tAdditionalCombatState[pStateName].active = true
end
function CoreBrain:DeactivateCombatState(pStateName, global)
  global.tAdditionalCombatState[pStateName].active = false
end
function CoreBrain:SetDamageCounter(damage)
  self.damageCounter = damage
end
function CoreBrain:TargetSon(ai, global, constants, dTreeParams)
  if global.target == son then
    dTreeParams.DecisionTree = STATE_SonAggro.aggroAttributes.dTree
    constants.fightKnowledgeInputs.CurrentZoneSet = "SonTargeting"
    return true
  end
  return false
end
function CoreBrain:SetTarget(target, global, constants)
  global.target = target
  if target == son then
    STATE_SonAggro.aggroAttributes.currentDamageThreshold = 2
  else
    STATE_SonAggro.aggroAttributes.currentDamageThreshold = 0
  end
end
function CoreBrain:GetDebugTable(ai)
  local debugTable = statemachine.GetDebugTable(state_machine_list, ai)
  return debugTable
end
local Brain_Core = CoreBrain.New()
local OnAIUpdate = function(ai)
  CoreBrain:Update(ai, _G.global, _G.constants)
end
local OnAIPostSpawn = function(ai)
  CoreBrain:OnAIPostSpawn(ai, _G.global, _G.constants)
end
local OnAICreateLuaState = function(ai)
  CoreBrain:OnAICreateLuaState(ai, _G.global, _G.constants)
end
local OnCreatureUpdate = function(ai)
  CoreBrain:CreatureUpdate(ai, _G.global, _G.constants)
end
local OnAIUnleashed = function(ai)
  CoreBrain:OnAIUnleashed(ai, _G.global, _G.constants)
end
local OnAIReleashed = function(ai, leashZoneName)
  CoreBrain:OnAIReleashed(ai, _G.global, _G.constants, leashZoneName)
end
thunk.Install("OnAIUpdate", OnAIUpdate)
thunk.Install("OnAIPostSpawn", OnAIPostSpawn)
thunk.Install("OnAICreateLuaState", OnAICreateLuaState)
thunk.Install("OnCreatureUpdate", OnCreatureUpdate)
thunk.Install("OnAIUnleashed", OnAIUnleashed)
thunk.Install("OnAIReleashed", OnAIReleashed)
function OnAIPreSpawn(ai)
  if CoreBrain:OnAIPreSpawn(ai) == "" then
    return ""
  end
end
function Brain:OnBrainInit(ai, global, constants)
  print("init brain")
  local zones = game.World.FindEntityZones(ai.SpawnPosition)
  for _, zone in ipairs(zones) do
    if string.find(zone, "combatzone") ~= nil then
      global.combatZone = zone
    end
  end
end
function Brain:SelectNextState(ai, global, constants)
  if poilib.IsPOIActive() then
    return STATE_UsePOI
  end
  if global.aggroState == "INAGGRO" then
    return STATE_InAggro
  end
  if global.aggroState == "FALLBACK" then
    return STATE_Fallback
  end
  if global.aggroState == "LEASH" then
    return STATE_Leash
  end
  if global.aggroState == "LEASHDEFEND" then
    return STATE_LeashDefend
  end
  if global.aggroState == "INCOMBAT" then
    global.target = ai:FindTarget(global.targetParams)
    local custom_active_state = CoreBrain:HasActiveState(global)
    if custom_active_state ~= nil then
      return custom_active_state
    end
    if global.target then
      return STATE_Combat
    end
  end
  if global.aggroState == "UNAWARE" or global.aggroState == "UNAWARE_STEALTH" or global.aggroState == "UNAWAREIGNORECOMBAT" or global.aggroState == "UNAWAREIGNORETARGET" then
    return STATE_Unaware
  end
  return STATE_NonHostile
end
function STATE_Unaware:Enter(ai, global, constants)
  game.Audio.SetWwiseSwitch(ai, "Aggro", "Unaware")
  self.PARENT.Enter(self, ai, global, constants)
  ai:AddMarker("STATE_UNAWARE")
end
function STATE_Unaware:Exit(ai, global, constants)
  self.PARENT.Exit(self, ai, global, constants)
  ai:RemoveMarker("STATE_UNAWARE")
end
local STATE_DEF_IDLE, STATE_DEF_COOLDOWN, STATE_DEF_ACTIVE, STATE_DEF_RANGE_ACTIVE, STATE_DEF_ACTIVE_INITIAL_WAIT = 0, 1, 2, 3, 4
local SPECIAL_ATTACK_RANGE = 5
function STATE_Combat:OnBrainInit()
end
function STATE_Combat:Update(ai, global, constants)
  self.PARENT.Update(self, ai, global, constants)
  if global.useLeashing and global.leashZone ~= nil and game.Encounters.IsPlayerInAILeashZone(ai) == false then
    global.aggroState = "LEASH"
    game.Encounters.ReportLeashZoneExit(ai)
    return
  end
end
function STATE_Combat:Enter(ai, global, constants)
  if Brain_Core.OnEnterCombat ~= nil then
    Brain_Core:OnEnterCombat(ai, global, constants)
  end
  game.Encounters.ReportCombatStatusChange(ai, true)
  print(tostring(ai) .. " has just switched to In Combat!")
  game.Audio.SetWwiseSwitch(ai, "Aggro", "InCombat")
  ai:AddMarker("STATE_INCOMBAT")
  self.defenseTimer = 0
  self.currentDefState = STATE_DEF_IDLE
  global.bInCombat = true
  ai:SetNavBank(global.navBank)
  ai:ClearForcedPath()
end
function STATE_Combat:OnUpdateCombat(ai, global, constants, dTreeParams)
  Brain_Core:TargetSon(ai, global, constants, dTreeParams)
  if Brain_Core.OnUpdateCombat ~= nil then
    Brain_Core:OnUpdateCombat(ai, global, constants, dTreeParams)
  end
end
function STATE_Combat:OnUpdateMotion(ai, global, constants, actuatorData, avoidanceArgs)
  local currentDistFromTarget = game.AIUtil.Distance(global.target:GetWorldPosition(), ai:GetWorldPosition())
  global.distToPlayer = currentDistFromTarget
  local desiredDistFromTarget = game.AIUtil.Distance(global.target:GetWorldPosition(), actuatorData.Destination)
  if currentDistFromTarget <= desiredDistFromTarget then
    actuatorData.ApproachSpeed = global.navData.navSpeedJog
  end
  self:UpdateDefensive(ai, global, constants)
  if ai:PickupIsActive("TurretMode") then
    actuatorData.Destination = ai:GetWorldPosition()
    local dist = DL.GetDistanceBetweenTwoObjects(game.Player.FindPlayer(), ai)
    if global.turretModeBreak == true and dist < global.turretModeBreakRange then
      ai:PickupRelinquish("TurretMode")
      if ai.SetDirectAimingModeIsEnabled then
        ai:SetDirectAimingModeIsEnabled(false)
      end
    end
  end
  if Brain_Core.OnUpdateMotion ~= nil then
    Brain_Core:OnUpdateMotion(ai, global, constants, actuatorData, avoidanceArgs)
  end
end
function STATE_Combat:Exit(ai, global, constants)
  if Brain_Core.OnExitCombat ~= nil then
    Brain_Core:OnExitCombat(ai, global, constants)
  end
  game.Encounters.ReportCombatStatusChange(ai, false)
  ai:RemoveMarker("STATE_INCOMBAT")
  global.bInCombat = false
end
function STATE_Combat.Events:OnHitReaction(event, ai, global, constants)
  if Brain_Core.OnHitReaction ~= nil then
    Brain_Core:OnHitReaction(event, ai, global, constants)
  end
  if ai:HasMeter("StunState") then
    ai:MeterSetValue("StunState", ai:MeterGetValue("StunState") + 0.001)
  end
  if ai:PickupIsAcquired("Debuff_Slow") and (string.find(ai:GetName(), "troll") or string.find(ai:GetName(), "jotunn") or ai:PickupIsAcquired("Leashing")) then
    ai:PickupRelinquish("Debuff_Slow")
  end
  if ai:PickupIsAcquired("HealthThreshold") and event.source ~= nil and event.source:GetName() == "heroa00" and ai:PickupGetStage("HealthThreshold") == 2 then
    player:CallScript("LuaHook_Perk_Offense_OnMeleeDamageToWoundedEnemy_RageBurst")
  end
  local myLevel = game.FindLevel("hlee_test_level") or game.FindLevel("TBAICast_Run")
  if myLevel then
    myLevel:CallScript("LuaHook_OnEnemyHit", ai, event)
  end
  if ai:HasHitFlag("HIT_THROWABLE", event.hitFlags) and event.source ~= nil and event.source:GetName() == "hero00a" and ai:HasMarker("React") and ai:CheckDecision("DEC_IS_IN_AIR") and player ~= nil then
    player:CallScript("LuaHook_Perk_Throw_OnDamageAirborneEnemy")
  end
  if event.source ~= nil and event.source:GetName() == "heroa00" and ai:HasMarker("React") and (ai:CheckDecision("DEC_IS_IN_AIR") or ai:HasMarker("React_Wall")) and player ~= nil then
    GiveHeroBonusMomentum()
  end
  if event.source ~= nil and event.source:GetName() == "flyer10" and DL.CheckCreatureContext(event.collisionContext, "FLYER_POSSESS") then
    local flyerGO = event.source:GetAI()
    if ai:HasMarker("PossessableByFlyer") then
      local possessPickup = "%s_Possessed"
      possessPickup = string.format(possessPickup, ai:GetName())
      if not ai:PickupIsAcquired(possessPickup) then
        ai:PickupAcquire(possessPickup)
      end
      if not ai:PickupIsAcquired("PossessedByFlyer") then
        ai:PickupAcquire("PossessedByFlyer")
      end
      if ai:PickupIsAcquired("FlyerPossessionTarget") then
        ai:PickupRelinquish("FlyerPossessionTarget")
      end
    end
    if flyerGO:PickupIsAcquired("Flyer10_PowerLevel") then
      possessorFlyerPowerLevel = flyerGO:PickupGetStage("Flyer10_PowerLevel")
      possessorFlyerPowerLevel = possessorFlyerPowerLevel + 1
    end
  end
  if event.source ~= nil and event.source:GetName() == "son00" and (DL.CheckCreatureContext(event.collisionContext, "ARROW_SHOCK") or DL.CheckCreatureContext(event.collisionContext, "ARROW_SHOCK_CHARGED") or DL.CheckCreatureContext(event.collisionContext, "ARROW_SHOCK_NOREACT") or DL.CheckCreatureContext(event.collisionContext, "ARROW_SHOCK_EXPLOSION") or DL.CheckCreatureContext(event.collisionContext, "ARROW_SHOCK_TICK") or DL.CheckCreatureContext(event.collisionContext, "ARROW_LIGHT") or DL.CheckCreatureContext(event.collisionContext, "ARROW_LIGHT_CHARGED") or DL.CheckCreatureContext(event.collisionContext, "ARROW_LIGHT_EXPLOSION") or DL.CheckCreatureContext(event.collisionContext, "ARROW_NORMAL_COMMAND") or DL.CheckCreatureContext(event.collisionContext, "FLIP")) then
    local currentWeapon = player:GetCurrentWeapon()
    if currentWeapon == "Axe" then
      if player:PickupIsAcquired("MomentumMeter") and player:HasMeter("Momentum") then
        local currentComboValue = player:MeterGetValue("Momentum")
        player:MeterSetValue("Momentum", currentComboValue + 0.5)
        player:CallScript("LuaHook_MomentumAxeIncrease")
      end
    elseif currentWeapon == "Blades" and player:PickupIsAcquired("MomentumBladesMeter") and player:HasMeter("MomentumBlades") then
      local currentComboValue = player:MeterGetValue("MomentumBlades")
      player:MeterSetValue("MomentumBlades", currentComboValue + 0.5)
      player:CallScript("LuaHook_MomentumBladesIncrease")
    end
  end
end
function STATE_Combat.Events:OnBroadcastReaction(event, ai, global, constants)
  if self.currentDefState ~= STATE_DEF_COOLDOWN and DL.CheckCreatureContext(event.broadcastContext, "HERO_SPECIAL_ATTACK") then
    local chance_to_evade = constants.defensiveChance or 0
    local random = math.random()
    if chance_to_evade > random then
      self.defTimer = (constants.defensiveDelay or 0) + (constants.defensiveDelayVariance or 0) * math.random() - (constants.defensiveDelayVariance or 0) / 2
      self.currentDefState = STATE_DEF_ACTIVE_INITIAL_WAIT
    else
      self.defTimer = 5
      self.currentDefState = STATE_DEF_COOLDOWN
    end
  end
  if Brain_Core.OnBroadcastReaction ~= nil then
    Brain_Core:OnBroadcastReaction(event, ai, global, constants)
  end
end
function STATE_Combat:TriggerEvadeMove(ai, global, constants)
  if Brain_Core.OnTriggerDefense then
    Brain_Core:OnTriggerDefense(ai, global, constants)
  else
    ai:TriggerMoveEvent("kLEEvadeSpecialAttack")
  end
  self.defTimer = constants.DefenseCooldown or 1
  self.currentDefState = STATE_DEF_COOLDOWN
end
function STATE_Combat:UpdateDefensive(ai, global, constants)
  local target = ai:GetTargetCreature()
  if self.currentDefState == STATE_DEF_IDLE then
    return
  elseif self.currentDefState == STATE_DEF_ACTIVE_INITIAL_WAIT then
    self.defTimer = self.defTimer - ai:GetFrameTime()
    if self.defTimer <= 0 then
      if not ai:HasMarker("CanEvadeSpecialAttack") or ai:PickupIsSlotUsed("Frost") then
        self.currentDefState = STATE_DEF_IDLE
        return
      end
      if target ~= nil and target:HasMarker("Aiming") == true and not constants.NoEvadeOnThrow then
        self.currentDefState = STATE_DEF_RANGE_ACTIVE
      else
        self.currentDefState = STATE_DEF_ACTIVE
      end
      return
    end
  elseif self.currentDefState == STATE_DEF_ACTIVE then
    if target == nil then
      self.currentDefState = STATE_DEF_IDLE
      return
    elseif target:HasMarker("SpecialAttack") == false then
      self.currentDefState = STATE_DEF_IDLE
      return
    end
    if target ~= nil and global.distToPlayer < (constants.SPECIAL_ATTACK_RANGE or SPECIAL_ATTACK_RANGE) then
      local angleFromPlayer = DL.FrontAngle(target, ai)
      if angleFromPlayer < -60 or 60 < angleFromPlayer then
        self.currentDefState = STATE_DEF_IDLE
        return
      end
      self:TriggerEvadeMove(ai, global, constants)
    end
  elseif self.currentDefState == STATE_DEF_RANGE_ACTIVE then
    if global.distToPlayer < 2.5 or global.distToPlayer > 24 then
      self.currentDefState = STATE_DEF_IDLE
      return
    end
    if target ~= nil and game.AIUtil.IntersectPointCone(ai:GetWorldPosition(), target:GetWorldPosition(), target:GetWorldForward(), 12, 25) then
      self:TriggerEvadeMove(ai, global, constants)
    end
    self.currentDefState = STATE_DEF_COOLDOWN
  elseif self.currentDefState == STATE_DEF_COOLDOWN then
    self.defTimer = self.defTimer - ai:GetFrameTime()
    if self.defTimer <= 0 then
      self.currentDefState = STATE_DEF_IDLE
    end
  end
end
function STATE_InAggro:Enter(seq, ai, global, constants)
  local delay = math.random()
  ai:AddMarker("STATE_INAGGRO")
  seq:At(delay + 0.1, function()
    global.aggroState = "INCOMBAT"
  end)
end
function STATE_InAggro:Exit(ai, global, constants)
  ai:RemoveMarker("STATE_INAGGRO")
end
function CoreBrain:SetSonAggro(pEnable)
  STATE_SonAggro.sonAggroSystemEnabled = pEnable
end
function STATE_SonAggro:init()
  print("son init")
  if creature_table ~= nil then
    local datatable = require(creature_table)
    if datatable.sonAttributes ~= nil then
      for k, v in pairs(datatable.sonAttributes) do
        self.aggroAttributes[k] = v
      end
    end
    if datatable.constants.sonAggroSystemEnabled ~= nil then
      self.sonAggroSystemEnabled = datatable.constants.sonAggroSystemEnabled
    end
  end
end
function STATE_Fallback:OnBrainInit(ai, global, constants)
end
function STATE_Fallback:Enter(ai, global, constants)
end
function STATE_Fallback:Update(ai, global, constants)
end
function STATE_Fallback.Events:OnHitReaction(event, ai, global, constants)
end
function STATE_Fallback.Events:OnBroadcastReaction(event, ai, global, constants)
end
function STATE_Fallback:Exit(ai, global, constants)
end
function STATE_Leash:OnBrainInit(ai, global, constants)
  local spawnerPos = game.NavMesh.ClosestLocationVertically(ai.SpawnPosition) or game.NavMesh.ClosestLocation(ai.SpawnPosition, ai)
  if spawnerPos ~= nil then
    self.fallbackDestination = spawnerPos:FindConnectedLocationInRadius(constants.returnRadius)
  else
    self.fallbackDestination = ai.SpawnPosition
  end
  self.startPos = ai:GetWorldPosition()
  self.distToDest = game.AIUtil.Distance(self.startPos, self.fallbackDestination)
  global.distToPlayer = game.AIUtil.Distance(self.startPos, player:GetWorldPosition())
  self.failCount = 0
end
local Leash_Pause = 0
local Leash_Retreat_Jog = 1
local Leash_Retreat_BackPeddle_Jog = 2
local Leash_Retreat_BackPeddle_Walk = 3
function STATE_Leash:HandleTurretModeSetup()
  self.leashState = Leash_Pause
  self.timer = math.random(3, 6)
  self.arrived = true
end
function STATE_Leash:Roll_ShouldEnterPauseState()
  local rollValue = math.random(0, 1)
  if rollValue <= 0.5 then
    self.leashState = Leash_Pause
  end
end
function STATE_Leash:DetermineRetreatState(ai, global, constants)
  local rollValue = math.random(0, 1)
  if global.distToPlayer ~= nil and self.distToDest ~= nil and global.distToPlayer <= 20 and self.distToDest <= 10 then
    rollValue = 0
  end
  if rollValue < 0 then
    rollValue = math.random(0, 1)
    if rollValue <= 0.75 then
      self.leashState = Leash_Retreat_BackPeddle_Walk
    else
      self.leashState = Leash_Retreat_BackPeddle_Jog
    end
  else
    self.leashState = Leash_Retreat_Jog
  end
end
function STATE_Leash:RandomizeEvalTimer()
  if self.leashState == Leash_Pause then
    self.timer = math.random(1, 3)
  else
    self.timer = math.random(2, 6)
  end
end
function STATE_Leash:Enter(ai, global, constants)
  self.Active = true
  self.startPos = ai:GetWorldPosition()
  self.arrived = false
  self.prevDTree = global.DTree
  global.DTree = constants.fallbackDTree or global.DTree
  if game.AIUtil.DistanceXZ then
    self.distToDest = game.AIUtil.DistanceXZ(self.startPos, self.fallbackDestination)
  else
    self.distToDest = game.AIUtil.Distance(self.startPos, self.fallbackDestination)
  end
  global.distToPlayer = game.AIUtil.Distance(self.startPos, player:GetWorldPosition())
  self.distAtStartPhase = self.distToDest
  self.timer = 0
  self.failCount = 0
  self.leashState = Leash_Retreat_Jog
  self.turretMode = ai:PickupIsActive("TurretMode")
  if self.turretMode then
    self:HandleTurretModeSetup()
  else
    self:Roll_ShouldEnterPauseState()
    if self.leashState ~= Leash_Pause then
      self:DetermineRetreatState(ai, global, constants)
    end
    self:RandomizeEvalTimer()
  end
  ai:AddMarker("DoNotEvaluate")
  ai:AddMarker("Leashing")
end
function STATE_Leash:UpdatePauseState(ai, global, constants)
  local inNavMove = ai:IsInNavigationMove()
  if inNavMove then
    local myPos = ai:GetWorldPosition()
    local actuatorData = {}
    ai:SetFocus(player)
    global.target = player
    self.startPos = myPos
    actuatorData.Position = myPos
    actuatorData.StopDistance = 0.75
    actuatorData.StartDistance = 0.1
    actuatorData.ApproachSpeed = global.navData.navSpeedJog
    if self.turretMode then
      actuatorData.Facing = ai.spawnFacing or (player:GetWorldPosition() - myPos):Normalized()
    else
      actuatorData.Facing = (player:GetWorldPosition() - myPos):Normalized()
    end
    actuatorData.Strafe = false
    locomotion.SetActuator(ai, {
      Destination = actuatorData.Position,
      Facing = actuatorData.Facing,
      Strafe = actuatorData.Strafe,
      Speed = actuatorData.ApproachSpeed ~= nil and actuatorData.ApproachSpeed or "Default",
      StopDistance = actuatorData.StopDistance,
      StartDistance = actuatorData.StartDistance
    })
  end
end
function STATE_Leash:UpdateRetreatState(ai, global, constants)
  local inNavMove = ai:IsInNavigationMove()
  if inNavMove then
    local myPos = ai:GetWorldPosition()
    local actuatorData = {}
    actuatorData.Position = self.fallbackDestination
    actuatorData.StopDistance = 0.75
    actuatorData.StartDistance = 0.1
    actuatorData.ApproachSpeed = global.navData.navSpeedJog
    actuatorData.Strafe = false
    if self.leashState == Leash_Retreat_Jog then
      local facingVector = (self.fallbackDestination - myPos):Normalized()
      actuatorData.Facing = facingVector
      global.target = nil
      ai:SetFocus(facingVector, true)
    end
    locomotion.SetActuator(ai, {
      Destination = actuatorData.Position,
      Facing = actuatorData.Facing,
      Strafe = actuatorData.Strafe,
      Speed = actuatorData.ApproachSpeed ~= nil and actuatorData.ApproachSpeed or "Default",
      StopDistance = actuatorData.StopDistance,
      StartDistance = actuatorData.StartDistance
    })
  end
end
function STATE_Leash:Update(ai, global, constants)
  if global.leashZone == nil then
    global.aggroState = "INAGGRO"
    global.target = player
    STATE_Leash:Exit(ai, global, constants)
    return
  end
  if Brain_Core.OnUpdateLeash ~= nil then
    Brain_Core:OnUpdateLeash(ai, global, constants)
  end
  local playerInBoat = game.Boat.GetPlayerBoat() ~= nil
  if ai:IsInNavigationMove() then
    self.timer = self.timer - ai:GetFrameTime()
  end
  local playerInLeashZone = game.Encounters.IsPlayerInAILeashZone(ai) and not playerInBoat
  if playerInLeashZone and (self.leashState == Leash_Pause or self.leashState == Leash_Retreat_BackPeddle_Jog or self.leashState == Leash_Retreat_BackPeddle_Walk) then
    if self.timer <= 0 then
      global.aggroState = "INAGGRO"
      global.target = player
      ai:SetFocus(player)
      STATE_Leash:Exit(ai, global, constants)
      return
    end
    self.timer = self.timer * 0.5
  end
  if self.leashState == Leash_Pause then
    self:UpdatePauseState(ai, global, constants)
  else
    self:UpdateRetreatState(ai, global, constants)
  end
  if self.timer <= 0 and not self.arrived then
    local oldState = self.leashState
    self.leashState = Leash_Retreat_Jog
    if oldState ~= Leash_Pause then
      local distDiff = self.distToDest - self.distAtStartPhase
      if distDiff * distDiff < 3 then
        self.failCount = self.failCount + 1
      end
      self:Roll_ShouldEnterPauseState()
    end
    if self.leashState ~= Leash_Pause then
      self:DetermineRetreatState(ai, global, constants)
    end
    self.distAtStartPhase = self.distToDest
    self:RandomizeEvalTimer()
  end
  local myPos = ai:GetWorldPosition()
  if game.AIUtil.debugPosDistanceXZ then
    self.distToDest = game.AIUtil.DistanceXZ(myPos, self.fallbackDestination)
  else
    self.distToDest = game.AIUtil.Distance(myPos, self.fallbackDestination)
  end
  if game.AIUtil.VerticalAngle and not self.arrived and self.distToDest < 10 then
    local angle = game.AIUtil.VerticalAngle(myPos, self.fallbackDestination)
    if angle < -45 or 45 < angle then
      self.failCount = 10
    end
  end
  global.distToPlayer = game.AIUtil.Distance(player:GetWorldPosition(), myPos)
  if (self.distToDest <= 2.5 or self.failCount > 1) and not self.arrived then
    if playerInLeashZone then
      global.aggroState = "INAGGRO"
      global.target = player
      self.arrived = true
      ai:SetFocus(player)
      STATE_Leash:Exit(ai, global, constants)
    else
      self.arrived = true
      self.leashState = Leash_Pause
      self.turretMode = true
      self.timer = 2
    end
    return
  end
  if self.arrived and self.timer <= 0 then
    global.aggroState = "UNAWARE"
    global.target = nil
    STATE_Leash:Exit(ai, global, constants)
  end
  if engine.IsDebug() then
    global.leashTimer = self.timer
    global.leashState = self.leashState
  end
end
function STATE_Leash.Events:OnHitReaction(event, ai, global, constants)
  if self.Active and event.source == player then
    if game.Encounters.IsPlayerInAILeashZone(ai) then
      global.aggroState = "INAGGRO"
      global.target = player
      ai:SetFocus(player)
      STATE_Leash:Exit(ai, global, constants)
      print("OnHitReaction event on STATE_Leash - player in leash zone")
      return
    end
    local aiToPlayer = player:GetWorldPosition() - ai:GetWorldPosition()
    local aiFacing = ai:GetWorldForward()
    if aiFacing:Dot(aiToPlayer) > 0 then
      print("OnHitReaction event on STATE_Leash - enemy facing player")
      self.leashState = Leash_Retreat_Jog
      self:RandomizeEvalTimer()
    else
      print("OnHitReaction event on STATE_Leash - enemy facing away from player")
      self.leashState = Leash_Retreat_Jog
      self:RandomizeEvalTimer()
    end
  end
end
function STATE_Leash.Events:OnBroadcastReaction(event, ai, global, constants)
  if self.Active and game.Encounters.IsPlayerInAILeashZone(ai) and DL.CheckCreatureContext(event.broadcastContext, "GRUNT_CRY") then
    DL.BroadcastAggroSecondary(ai, global, constants)
    global.target = player
    global.aggroState = "INAGGRO"
  end
end
function STATE_Leash:Exit(ai, global, constants)
  self.Active = false
  global.DTree = self.prevDTree
  ai:RemoveMarker("DoNotEvaluate")
  ai:RemoveMarker("Leashing")
  ai:ClearFocus()
  if engine.IsDebug() then
    global.leashTimer = nil
    global.leashState = nil
  end
  if ai:PickupIsAcquired("Leashing") then
    ai:PickupRelinquish("Leashing")
  end
end
function STATE_LeashDefend:OnBrainInit(ai, global, constants)
end
function STATE_LeashDefend:Enter(ai, global, constants)
  self.Active = true
  global.target = player
  ai:SetFocus(player)
  self.timer = 2
  self.goal = ai:GetWorldPosition() + (player:GetWorldPosition() - ai:GetWorldPosition()):Normalized() * 2
end
function STATE_LeashDefend:Exit(ai, global, constants)
end
function STATE_LeashDefend:Update(ai, global, constants)
  local playerInBoat = game.Boat.GetPlayerBoat() ~= nil
  local playerInLeashZone = game.Encounters.IsPlayerInAILeashZone(ai) and not playerInBoat
  if playerInLeashZone and self.timer <= 0 then
    global.aggroState = "INCOMBAT"
    STATE_LeashDefend:Exit(ai, global, constants)
    return
  end
  local myPos = ai:GetWorldPosition()
  local playerPos = player:GetWorldPosition()
  global.distToPlayer = game.AIUtil.Distance(playerPos, myPos)
  if global.distToPlayer > 50 and self.timer <= 0 then
    global.aggroState = "UNAWARE"
    STATE_LeashDefend:Exit(ai, global, constants)
    return
  end
  self.timer = self.timer - ai:GetFrameTime()
  ai:SetFocus(player)
  global.target = player
  local actuatorData = {}
  actuatorData.Position = self.goal
  actuatorData.StopDistance = 0.75
  actuatorData.StartDistance = 0.1
  actuatorData.ApproachSpeed = global.navData.navSpeedWalk
  actuatorData.Facing = (playerPos - myPos):Normalized()
  actuatorData.Strafe = false
  locomotion.SetActuator(ai, {
    Destination = actuatorData.Position,
    Facing = actuatorData.Facing,
    Strafe = actuatorData.Strafe,
    Speed = actuatorData.ApproachSpeed ~= nil and actuatorData.ApproachSpeed or "Default",
    StopDistance = actuatorData.StopDistance,
    StartDistance = actuatorData.StartDistance
  })
end
function STATE_UsePOI:OnUpdateAwareness(ai, global, constants)
end
function LuaHook_SetInCombatState(ai)
  if game.Player.FindPlayer():OnActiveTraversePath() or game.Player.FindPlayer():HasMarker("QuestGiverInteract") then
    _G.global.aggroState = "UNAWARE"
    return
  end
  _G.global.aggroState = "INCOMBAT"
  ai:TriggerMoveEvent("kLEAlert")
end
function LuaHook_SetState(ai, newState)
  assert(newState == "INCOMBAT" or newState == "NONHOSTILE" or newState == "UNAWARE", "Incorrect state name passed to SetState. Must be INCOMBAT, NONHOSTILE, UNAWARE")
  if newState == "INCOMBAT" and (game.Player.FindPlayer():OnActiveTraversePath() or game.Player.FindPlayer():HasMarker("QuestGiverInteract")) then
    _G.global.aggroState = "UNAWARE"
    return
  end
  _G.global.aggroState = newState
  if newState == "INCOMBAT" then
    ai:TriggerMoveEvent("kLEAlert")
  end
end
function OnDeath(ai, attacker, deathByDespawn, deathByDeathPlaneOrFalling)
  if deathByDespawn == nil or deathByDespawn == false then
    if deathByDeathPlaneOrFalling ~= true then
      UnpossessSpawnFlyer(ai)
    end
    game.Loot.RollConditionSet("CREATURE_LOOTROLL", "HERO", ai, ai:GetName())
    game.Loot.RollConditionSet("CREATURE_XPROLL", "XP_HOLD", ai, ai:GetName())
    if attacker == player then
      player:CallScript("LuaHook_Perk_OnKill_Any_React")
    end
    ai.SoundEmitters[1]:Start("snd_ux_enemy_killed")
  end
  if STATE_SonAggro.forcedTempAggroNow then
    game.Level.SetVariable("CBT_SonAggro", game.Level.GetVariable("CBT_SonAggro") + 1)
  end
  if son ~= nil then
    engine.SendHook("OnRequestDisengageHook", son, ai)
  end
  local creatureID = ai:GetID()
  if creatureID == DL.HashCreatureID(ai, "DARKONEELITE00") or creatureID == DL.HashCreatureID(ai, "DRAGON00") or creatureID == DL.HashCreatureID(ai, "TROLL00") or creatureID == DL.HashCreatureID(ai, "TROLL10") or creatureID == DL.HashCreatureID(ai, "TROLL20") or creatureID == DL.HashCreatureID(ai, "TROLL30") or creatureID == DL.HashCreatureID(ai, "VALKYRIE00") then
    LD.SetEntityVariable("MusicIntensity", MusicMath(-30))
  end
  if creatureID == DL.HashCreatureID(ai, "GOLEM00") or creatureID == DL.HashCreatureID(ai, "GOLEM10") or creatureID == DL.HashCreatureID(ai, "GOLEM20") or creatureID == DL.HashCreatureID(ai, "GOLEM30") or creatureID == DL.HashCreatureID(ai, "JOTUNN00") or creatureID == DL.HashCreatureID(ai, "JOTUNN01") or creatureID == DL.HashCreatureID(ai, "JOTUNN02") or creatureID == DL.HashCreatureID(ai, "JOTUNN10") or creatureID == DL.HashCreatureID(ai, "JOTUNN20") or creatureID == DL.HashCreatureID(ai, "WITCH00") or creatureID == DL.HashCreatureID(ai, "WITCH10") or creatureID == DL.HashCreatureID(ai, "WITCH20") or creatureID == DL.HashCreatureID(ai, "WITCH30") or creatureID == DL.HashCreatureID(ai, "WULVER00") or creatureID == DL.HashCreatureID(ai, "WULVER10") then
    LD.SetEntityVariable("MusicIntensity", MusicMath(-10))
  end
  if creatureID == DL.HashCreatureID(ai, "BANDIT00") or creatureID == DL.HashCreatureID(ai, "BANDIT02") or creatureID == DL.HashCreatureID(ai, "BANDITHUMAN00") or creatureID == DL.HashCreatureID(ai, "BRAWLER00") or creatureID == DL.HashCreatureID(ai, "DARKONE00") or creatureID == DL.HashCreatureID(ai, "DRAUGR00") or creatureID == DL.HashCreatureID(ai, "FANATIC00") or creatureID == DL.HashCreatureID(ai, "PROJECTION00") or creatureID == DL.HashCreatureID(ai, "PROJECTION10") or creatureID == DL.HashCreatureID(ai, "PROJECTION20") or creatureID == DL.HashCreatureID(ai, "TRAVELER00") or creatureID == DL.HashCreatureID(ai, "TRAVELER10") or creatureID == DL.HashCreatureID(ai, "TRAVELER20") then
    LD.SetEntityVariable("MusicIntensity", MusicMath(-3))
  end
  if creatureID == DL.HashCreatureID(ai, "CRAWLER00") or creatureID == DL.HashCreatureID(ai, "CRAWLER10") or creatureID == DL.HashCreatureID(ai, "FENRIR00") or creatureID == DL.HashCreatureID(ai, "FLYER00") or creatureID == DL.HashCreatureID(ai, "FLYER10") or creatureID == DL.HashCreatureID(ai, "WOLF00") or creatureID == DL.HashCreatureID(ai, "WOLF10") then
    LD.SetEntityVariable("MusicIntensity", MusicMath(-1))
  end
  if Brain_Core.OnDeath ~= nil then
    Brain_Core:OnDeath(ai, attacker, deathByDespawn)
  end
end
function LuaHook_Perk_Special_Kill()
  local playerCreature = game.Player.FindPlayer()
  playerCreature:CallScript("LuaHook_Perk_Special_Kill_Callscript")
end
function LuaHook_Perk_Kill_Freeze()
  local playerCreature = game.Player.FindPlayer()
  playerCreature:CallScript("LuaHook_Perk_Kill_Freeze")
end
function LuaHook_Perk_Throw_WallPin()
  local playerCreature = game.Player.FindPlayer()
  playerCreature:CallScript("LuaHook_Perk_Throw_WallPin")
end
function LuaHook_DebugTogglePossess(ai)
  if ai:HasMarker("PossessableByFlyer") then
    local possessPickup = ai:GetName():gsub("%d", "") .. "00_Possessed"
    if ai:PickupIsAcquired(possessPickup) then
      ai:PickupRelinquish(possessPickup)
    else
      ai:PickupAcquire(possessPickup)
    end
    if not ai:PickupIsAcquired("PossessedByFlyer") then
      ai:PickupAcquire("PossessedByFlyer")
    elseif ai:PickupIsAcquired("PossessedByFlyer") then
      ai:PickupRelinquish("PossessedByFlyer")
    end
  end
end
function LuaHook_DebugToggleElite(ai)
  if ai:HasMarker("CanBeElite") then
    local difficultyPickup = ai:GetName():gsub("%d", "") .. "00_Elite"
    if ai:PickupIsAcquired(difficultyPickup) then
      ai:PickupRelinquish(difficultyPickup)
    else
      ai:PickupAcquire(difficultyPickup)
    end
  end
end
function LuaHook_SetPowerLevel(ai, pLevel)
  engine.Warning("Calling LuaHook_SetPowerLevel in core_brain.lua! Replace this call by setting the power level in the spawner or using the override in the encounter wave element data itself")
end
local PowerLevelConvertToInt = function(pLevelString)
  local pLevelInt
  if pLevelString == "1" then
    pLevelInt = 1
  elseif pLevelString == "2" then
    pLevelInt = 2
  elseif pLevelString == "3" then
    pLevelInt = 3
  elseif pLevelString == "4" then
    pLevelInt = 4
  elseif pLevelString == "5" then
    pLevelInt = 5
  elseif pLevelString == "6" then
    pLevelInt = 6
  elseif pLevelString == "7" then
    pLevelInt = 7
  elseif pLevelString == "8" then
    pLevelInt = 8
  elseif pLevelString == "Elite" then
    pLevelInt = 9
  else
    pLevelInt = pLevelString
  end
  return pLevelInt
end
function LuaHook_IncreasePowerLevel(ai)
  local pLevel = PowerLevelConvertToInt(_G.global.powerLevel)
  if pLevel < _G.constants.maxPowerLevel then
    SetPowerLevel(ai, _G.global, _G.constants, pLevel + 1, false)
    Brain_Core:SetPowerlevelOverride(ai, _G.global, _G.constants, pLevel + 1, false, true)
  end
end
function LuaHook_DecreasePowerLevel(ai)
  local pLevel = PowerLevelConvertToInt(_G.global.powerLevel)
  if 1 < pLevel then
    SetPowerLevel(ai, _G.global, _G.constants, pLevel - 1, false)
    Brain_Core:SetPowerlevelOverride(ai, _G.global, _G.constants, pLevel - 1, false, true)
  end
end
function LuaHookDecision_CheckIfStunIsFull(ai)
  if ai:HasMeter("StunState") then
    return ai:MeterGetValue("StunState") == ai:MeterGetMax("StunState")
  end
  return false
end
function LuaHook_GetPowerLevel(ai, level)
  local powerLevel = _G.global.powerLevel
  if powerLevel == "Elite" then
    powerLevel = 9
  end
  return powerLevel
end
function CoreBrain:SetPowerlevelOverride(ai, global, constants, level, elite, forced)
  if Brain_Core.OnSetPowerlevelOverrideCustom then
    Brain_Core:OnSetPowerlevelOverrideCustom(ai, global, constants, level, elite, forced)
  end
end
function CoreBrain:DrawDebugTable(debugTable, global, ai)
  table.insert(debugTable, {
    "State String:",
    global.aggroState
  })
  if global.LeashDecision ~= nil then
    for k, v in tablex.SortedPairs(global.LeashDecision) do
      table.insert(debugTable, {
        "Leash." .. k,
        v
      })
    end
  end
  table.insert(debugTable, {
    "Distance to Player:",
    global.distToPlayer
  })
  table.insert(debugTable, {
    "Target: ",
    global.target
  })
  table.insert(debugTable, {
    "Target Params: ",
    global.targetParams
  })
  table.insert(debugTable, {
    "Force Target Kratos: ",
    tostring(ai:HasMarker("ForceTargetKratos"))
  })
  table.insert(debugTable, {"--", "--"})
  table.insert(debugTable, {
    "Leash Zone: ",
    global.leashZone
  })
  table.insert(debugTable, {
    "Current Dtree : ",
    global.DTree
  })
  table.insert(debugTable, {
    "Power Level : ",
    global.powerLevel
  })
  table.insert(debugTable, {
    "Hitpoints : ",
    ai:GetHitPoints()
  })
  if customDebugTable ~= nil then
    for _, v in pairs(customDebugTable) do
      table.insert(debugTable, {
        v[1],
        v[2]
      })
    end
  end
  if global.fallbackTimer ~= nil then
    table.insert(debugTable, {
      "Leash-Fallback timer: ",
      global.fallbackTimer
    })
  end
  if global.leashTimer ~= nil then
    table.insert(debugTable, {
      "Leash-eval timer: ",
      global.leashTimer
    })
  end
  if global.leashState ~= nil then
    if global.leashState == Leash_Pause then
      table.insert(debugTable, {
        "Leash-State: Leash_Pause"
      })
    elseif global.leashState == Leash_Retreat_Jog then
      table.insert(debugTable, {
        "Leash-State: Leash_Retreat_Jog"
      })
    elseif global.leashState == Leash_Retreat_BackPeddle_Jog then
      table.insert(debugTable, {
        "Leash-State: Leash_Retreat_BackPeddle_Jog"
      })
    elseif global.leashState == Leash_Retreat_BackPeddle_Walk then
      table.insert(debugTable, {
        "Leash-State: Leash_Retreat_BackPeddle_Walk"
      })
    else
      table.insert(debugTable, {
        "Leash-State: INVALID STATE"
      })
    end
  end
  if global.debugPosEnd ~= nil then
    table.insert(debugTable, {"--", "--"})
    table.insert(debugTable, {
      "-- Position Distance Debug --"
    })
  end
  if global.debugPosStart ~= nil then
    table.insert(debugTable, {
      "Start: ",
      global.debugPosStart
    })
  end
  if global.debugPosEnd ~= nil then
    table.insert(debugTable, {
      "End: ",
      global.debugPosEnd
    })
  end
  if global.debugPosDistanceXZ ~= nil then
    table.insert(debugTable, {
      "Result - XZ: ",
      global.debugPosDistanceXZ
    })
  end
  if global.debugPosDistanceY ~= nil then
    table.insert(debugTable, {
      "Result - Y: ",
      global.debugPosDistanceY
    })
  end
  engine.DrawDebugTable(debugTable)
end
return Brain_Core
