/*	EQEMu: Everquest Server Emulator
	Copyright (C) 2001-2004 EQEMu Development Team (http://eqemu.org)

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; version 2 of the License.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY except by those people which sell it, which
	are required to give you total support for your newly bought product;
	without even the implied warranty of MERCHANTABILITY or FITNESS FOR
	A PARTICULAR PURPOSE. See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

#include "../common/global_define.h"
#include "../common/features.h"
#include "../common/rulesys.h"
#include "../common/strings.h"

#include "client.h"
#include "entity.h"
#include "map.h"
#include "mob.h"
#include "npc.h"
#include "quest_parser_collection.h"
#include "string_ids.h"
#include "water_map.h"
#include "fastmath.h"
#include "../common/data_verification.h"

#include "bot.h"
#include "../common/repositories/npc_spells_repository.h"
#include "../common/repositories/npc_spells_entries_repository.h"
#include "../common/repositories/criteria/content_filter_criteria.h"

#include <glm/gtx/projection.hpp>
#include <algorithm>
#include <iostream>
#include <limits>

extern EntityList entity_list;
extern FastMath g_Math;

extern Zone *zone;

#if EQDEBUG >= 12
	#define MobAI_DEBUG_Spells	25
#elif EQDEBUG >= 9
	#define MobAI_DEBUG_Spells	10
#else
	#define MobAI_DEBUG_Spells	-1
#endif

//NOTE: do NOT pass in beneficial and detrimental spell types into the same call here!
bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes, bool bInnates) {
	if (!tar)
		return false;

	if (IsNoCast())
		return false;

	if(AI_HasSpells() == false)
		return false;

	// Rooted mobs were just standing around when tar out of range.
	// Any sane mob would cast if they can.
	bool cast_only_option = (IsRooted() && !CombatRange(tar));

	// innates are always attempted
	if (!cast_only_option && iChance < 100 && !bInnates) {
		if (zone->random.Int(0, 100) >= iChance)
			return false;
	}

	float dist2;

	if (iSpellTypes & SpellType_Escape) {
		dist2 = 0; //DistNoRoot(*this);	//WTF was up with this...
	}
	else
		dist2 = DistanceSquared(m_Position, tar->GetPosition());

	bool checked_los = false;	//we do not check LOS until we are absolutely sure we need to, and we only do it once.

	float manaR = GetManaRatio();
	for (int i = static_cast<int>(AIspells.size()) - 1; i >= 0; i--) {
		if (!IsValidSpell(AIspells[i].spellid)) {
			// this is both to quit early to save cpu and to avoid casting bad spells
			// Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here
			//return false;
			continue;
		}

		if ((AIspells[i].priority == 0 && !bInnates) || (AIspells[i].priority != 0 && bInnates)) {
			// so "innate" spells are special and spammed a bit
			// we define an innate spell as a spell with priority 0
			continue;
		}

		// we reuse these fields for heal overrides
		if (AIspells[i].type != SpellType_Heal && AIspells[i].min_hp != 0 && GetIntHPRatio() < AIspells[i].min_hp)
			continue;

		if (AIspells[i].type != SpellType_Heal && AIspells[i].max_hp != 0 && GetIntHPRatio() > AIspells[i].max_hp)
			continue;

		if (iSpellTypes & AIspells[i].type) {
			// manacost has special values, -1 is no mana cost, -2 is instant cast (no mana)
			int32 mana_cost = AIspells[i].manacost;
			if (mana_cost == -1)
				mana_cost = spells[AIspells[i].spellid].mana;
			else if (mana_cost == -2)
				mana_cost = 0;
			// this is ugly -- ignore distance for hatelist spells, looks like the client is only checking distance for some targettypes in CastSpell,
			// should probably match that eventually. This should be good enough for now I guess ....
			if (
				(
				 (spells[AIspells[i].spellid].target_type == ST_HateList || spells[AIspells[i].spellid].target_type == ST_AETargetHateList) ||
				 (
				  // note: I think this check is actually wrong and we should be checking range instead in all cases, BUT if range is 0, range check is skipped? Works for now
				  (spells[AIspells[i].spellid].target_type==ST_AECaster || spells[AIspells[i].spellid].target_type==ST_AEBard || spells[AIspells[i].spellid].target_type==ST_AEClientV1)
				  && dist2 <= spells[AIspells[i].spellid].aoe_range*spells[AIspells[i].spellid].aoe_range
				 ) ||
				 dist2 <= spells[AIspells[i].spellid].range*spells[AIspells[i].spellid].range
				 )
				&& (mana_cost <= GetMana() || GetMana() == GetMaxMana())
				&& (AIspells[i].time_cancast + (zone->random.Int(0, 4) * 500)) <= Timer::GetCurrentTime() //break up the spelling casting over a period of time.
				) {

				LogAI("Casting: spellid [{}] tar [{}] dist2[[{}]]<=[{}] mana_cost[[{}]]<=[{}] cancast[[{}]]<=[{}] type [{}]",
					AIspells[i].spellid, tar->GetName(), dist2, (spells[AIspells[i].spellid].range * spells[AIspells[i].spellid].range), mana_cost, GetMana(), AIspells[i].time_cancast, Timer::GetCurrentTime(), AIspells[i].type);

				switch (AIspells[i].type) {
					case SpellType_Heal: {
						if (
							(spells[AIspells[i].spellid].target_type == ST_Target || tar == this)
							&& tar->DontHealMeBefore() < Timer::GetCurrentTime()
							&& !(tar->IsPet() && tar->GetOwner()->IsOfClientBot())	//no buffing PC's pets
							) {

							auto hp_ratio = tar->GetIntHPRatio();

							int min_hp = AIspells[i].min_hp; // well 0 is default, so no special case here
							int max_hp = AIspells[i].max_hp ? AIspells[i].max_hp : RuleI(Spells, AI_HealHPPct);

							if (EQ::ValueWithin(hp_ratio, min_hp, max_hp) || (tar->IsClient() && hp_ratio <= 99)) { // not sure about client bit, leaving it
								uint32 tempTime = 0;
								AIDoSpellCast(i, tar, mana_cost, &tempTime);
								tar->SetDontHealMeBefore(tempTime);
								return true;
							}
						}
						break;
					}
					case SpellType_Root: {
						Mob *rootee = GetHateRandom();
						if (rootee && !rootee->IsRooted() && !rootee->IsFeared() && (bInnates || zone->random.Roll(50))
							&& rootee->DontRootMeBefore() < Timer::GetCurrentTime()
							&& rootee->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
							) {
							if(!checked_los) {
								if(!CheckLosFN(rootee))
									return(false);	//cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call
								checked_los = true;
							}
							uint32 tempTime = 0;
							AIDoSpellCast(i, rootee, mana_cost, &tempTime);
							rootee->SetDontRootMeBefore(tempTime);
							return true;
						}
						break;
					}
					case SpellType_Buff: {
						if (
							(spells[AIspells[i].spellid].target_type == ST_Target || tar == this)
							&& tar->DontBuffMeBefore() < Timer::GetCurrentTime()
							&& !tar->IsImmuneToSpell(AIspells[i].spellid, this)
							&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
							&& !(tar->IsPet() && tar->GetOwner()->IsOfClientBot() && this != tar)	//no buffing PC's pets, but they can buff themself
							)
						{
							if(!checked_los) {
								if(!CheckLosFN(tar))
									return(false);
								checked_los = true;
							}
							uint32 tempTime = 0;
							AIDoSpellCast(i, tar, mana_cost, &tempTime);
							tar->SetDontBuffMeBefore(tempTime);
							return true;
						}
						break;
					}

					case SpellType_InCombatBuff: {
						if(bInnates || zone->random.Roll(50)) {
							if (tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0) {
								AIDoSpellCast(i, tar, mana_cost);
								return true;
							}
						}
						break;
					}

					case SpellType_Escape: {
						// If min_hp !=0 then the spell list has specified
						// custom range and we're inside that range if we
						// made it here.
						if (AIspells[i].min_hp != 0 || GetHPRatio() <= (RuleI(NPC, NPCGatePercent))) {
							auto npcSpawnPoint = CastToNPC()->GetSpawnPoint();
							if (!RuleB(NPC, NPCGateNearBind) && DistanceNoZ(m_Position, npcSpawnPoint) < RuleI(NPC, NPCGateDistanceBind)) {
								break;
							} else {
								AIDoSpellCast(i, tar, mana_cost);
								return true;
							}
						}
						break;
					}
					case SpellType_Slow:
					case SpellType_Debuff: {
						Mob * debuffee = GetHateRandom();
						if (debuffee && manaR >= 10 && (bInnates || zone->random.Roll(70)) &&
								debuffee->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0) {
							if (!checked_los) {
								if (!CheckLosFN(debuffee))
									return false;
								checked_los = true;
							}
							AIDoSpellCast(i, debuffee, mana_cost);
							return true;
						}
						break;
					}
					case SpellType_Nuke: {
						if (
							manaR >= 10 && (bInnates || (zone->random.Roll(70)
							&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), false) >= 0)) // saying it's a nuke here, AI shouldn't care too much if overwriting
							) {
							if(!checked_los) {
								if(!CheckLosFN(tar))
									return(false);	//cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call
								checked_los = true;
							}
							AIDoSpellCast(i, tar, mana_cost);
							return true;
						}
						break;
					}
					case SpellType_Dispel: {
						if(bInnates || zone->random.Roll(15))
						{
							if(!checked_los) {
								if(!CheckLosFN(tar))
									return(false);	//cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call
								checked_los = true;
							}
							if(tar->CountDispellableBuffs() > 0)
							{
								AIDoSpellCast(i, tar, mana_cost);
								return true;
							}
						}
						break;
					}
					case SpellType_Mez: {
						if(bInnates || zone->random.Roll(20))
						{
							Mob * mezTar = nullptr;
							mezTar = entity_list.GetTargetForMez(this);

							if(mezTar && mezTar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0)
							{
								AIDoSpellCast(i, mezTar, mana_cost);
								return true;
							}
						}
						break;
					}

					case SpellType_Charm:
					{
						if(!IsPet() && (bInnates || zone->random.Roll(20)))
						{
							Mob * chrmTar = GetHateRandom();
							if(chrmTar && chrmTar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0)
							{
								AIDoSpellCast(i, chrmTar, mana_cost);
								return true;
							}
						}
						break;
					}

					case SpellType_Pet: {
						//keep mobs from recasting pets when they have them.
						if (!IsPet() && !GetPetID() && (bInnates || zone->random.Roll(25))) {
							AIDoSpellCast(i, tar, mana_cost);
							return true;
						}
						break;
					}
					case SpellType_Lifetap: {
						if (GetHPRatio() <= 95
							&& (bInnates || zone->random.Roll(50))
							&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
							) {
							if(!checked_los) {
								if(!CheckLosFN(tar))
									return(false);	//cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call
								checked_los = true;
							}
							AIDoSpellCast(i, tar, mana_cost);
							return true;
						}
						break;
					}
					case SpellType_Snare: {
						if (
							!tar->IsRooted()
							&& (bInnates || zone->random.Roll(50))
							&& tar->DontSnareMeBefore() < Timer::GetCurrentTime()
							&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
							) {
							if(!checked_los) {
								if(!CheckLosFN(tar))
									return(false);	//cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call
								checked_los = true;
							}
							uint32 tempTime = 0;
							AIDoSpellCast(i, tar, mana_cost, &tempTime);
							tar->SetDontSnareMeBefore(tempTime);
							return true;
						}
						break;
					}
					case SpellType_DOT: {
						if (
							(bInnates || zone->random.Roll(60))
							&& tar->DontDotMeBefore() < Timer::GetCurrentTime()
							&& tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0
							) {
							if(!checked_los) {
								if(!CheckLosFN(tar))
									return(false);	//cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call
								checked_los = true;
							}
							uint32 tempTime = 0;
							AIDoSpellCast(i, tar, mana_cost, &tempTime);
							tar->SetDontDotMeBefore(tempTime);
							return true;
						}
						break;
					}
					default: {
						std::cout << "Error: Unknown spell type in AICastSpell. caster:" << GetName() << " type:" << AIspells[i].type << " slot:" << i << std::endl;
						break;
					}
				}
			}
			else {
				LogAI("NotCasting: spellid [{}] tar [{}] dist2[[{}]]<=[{}] mana_cost[[{}]]<=[{}] cancast[[{}]]<=[{}] type [{}]",
					AIspells[i].spellid, tar->GetName(), dist2, (spells[AIspells[i].spellid].range * spells[AIspells[i].spellid].range), mana_cost, GetMana(), AIspells[i].time_cancast, Timer::GetCurrentTime(), AIspells[i].type);
			}
		}
	}
	return false;
}

bool NPC::AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore) {
	LogAI("spellid [{}] tar [{}] mana [{}] Name [{}]", AIspells[i].spellid, tar->GetName(), mana_cost, spells[AIspells[i].spellid].name);
	casting_spell_AIindex = i;

	return CastSpell(AIspells[i].spellid, tar->GetID(), EQ::spells::CastingSlot::Gem2, AIspells[i].manacost == -2 ? 0 : -1, mana_cost, oDontDoAgainBefore, -1, -1, 0, &(AIspells[i].resist_adjust));
}

void Mob::AI_Init()
{
	pAIControlled = false;
	AI_think_timer.reset(nullptr);
	AI_walking_timer.reset(nullptr);
	AI_movement_timer.reset(nullptr);
	AI_target_check_timer.reset(nullptr);
	AI_feign_remember_timer.reset(nullptr);
	AI_scan_area_timer.reset(nullptr);
	AI_check_signal_timer.reset(nullptr);
	AI_scan_door_open_timer.reset(nullptr);

	minLastFightingDelayMoving = RuleI(NPC, LastFightingDelayMovingMin);
	maxLastFightingDelayMoving = RuleI(NPC, LastFightingDelayMovingMax);

	m_dont_heal_me_before  = 0;
	m_dont_buff_me_before  = Timer::GetCurrentTime() + 400;
	m_dont_dot_me_before   = 0;
	m_dont_root_me_before  = 0;
	m_dont_snare_me_before = 0;
	m_dont_cure_me_before  = 0;
}

void NPC::AI_Init()
{
	AIautocastspell_timer.reset(nullptr);
	casting_spell_AIindex = static_cast<uint8>(AIspells.size());

	m_roambox.max_x     = 0;
	m_roambox.max_y     = 0;
	m_roambox.min_x     = 0;
	m_roambox.min_y     = 0;
	m_roambox.distance  = 0;
	m_roambox.dest_x    = 0;
	m_roambox.dest_y    = 0;
	m_roambox.dest_z    = 0;
	m_roambox.delay     = 2500;
	m_roambox.min_delay = 2500;
}

void Client::AI_Init()
{
	minLastFightingDelayMoving = CLIENT_LD_TIMEOUT;
	maxLastFightingDelayMoving = CLIENT_LD_TIMEOUT;
}

void Mob::AI_Start(uint32 iMoveDelay) {
	if (pAIControlled)
		return;

	if (iMoveDelay)
		time_until_can_move = Timer::GetCurrentTime() + iMoveDelay;
	else
		time_until_can_move = 0;

	pAIControlled  = true;
	AI_think_timer = std::make_unique<Timer>(AIthink_duration);
	AI_think_timer->Trigger();

	AI_walking_timer        = std::make_unique<Timer>(0);
	AI_movement_timer       = std::make_unique<Timer>(AImovement_duration);
	AI_target_check_timer   = std::make_unique<Timer>(AItarget_check_duration);
	AI_feign_remember_timer = std::make_unique<Timer>(AIfeignremember_delay);
	AI_scan_door_open_timer = std::make_unique<Timer>(AI_scan_door_open_interval);

	if (GetBodyType() == BodyType::Animal && !RuleB(NPC, AnimalsOpenDoors)) {
		SetCanOpenDoors(false);
	}

	if(!RuleB(Aggro, NPCAggroMaxDistanceEnabled)) {
		hate_list_cleanup_timer.Disable();
	}

	if (CastToNPC()->GetNPCAggro())
		AI_scan_area_timer = std::make_unique<Timer>(RandomTimer(RuleI(NPC, NPCToNPCAggroTimerMin), RuleI(NPC, NPCToNPCAggroTimerMax)));

	AI_check_signal_timer = std::make_unique<Timer>(AI_check_signal_timer_delay);


	if (GetAggroRange() == 0)
		pAggroRange = 70;
	if (GetAssistRange() == 0)
		pAssistRange = 70;

	hate_list.WipeHateList();

	m_Delta = glm::vec4();
	pRunAnimSpeed = 0;
}

void Client::AI_Start(uint32 iMoveDelay) {
	Mob::AI_Start(iMoveDelay);

	if (!pAIControlled)
		return;

	pClientSideTarget = GetTarget() ? GetTarget()->GetID() : 0;
	SendAppearancePacket(AppearanceType::Animation, Animation::Freeze);	// this freezes the client
	SendAppearancePacket(AppearanceType::Linkdead, 1); // Sending LD packet so *LD* appears by the player name when charmed/feared -Kasai
	SetAttackTimer();
	SetFeigned(false);
}

void NPC::AI_Start(uint32 iMoveDelay) {
	Mob::AI_Start(iMoveDelay);
	if (!pAIControlled)
		return;

	if (AIspells.empty()) {
		AIautocastspell_timer = std::make_unique<Timer>(1000);
		AIautocastspell_timer->Disable();
	} else {
		AIautocastspell_timer = std::make_unique<Timer>(500);
		AIautocastspell_timer->Start(RandomTimer(0, 300), false);
	}

	if (NPCTypedata) {
		AI_AddNPCSpells(NPCTypedata->npc_spells_id);
		ProcessSpecialAbilities(NPCTypedata->special_abilities);
		AI_AddNPCSpellsEffects(NPCTypedata->npc_spells_effects_id);
	}

	SendTo(GetX(), GetY(), GetZ());
	SaveGuardSpot(GetPosition());
}

void Mob::AI_Stop() {
	if (!IsAIControlled())
		return;

	pAIControlled = false;

	AI_think_timer.reset(nullptr);
	AI_walking_timer.reset(nullptr);
	AI_movement_timer.reset(nullptr);
	AI_target_check_timer.reset(nullptr);
	AI_scan_area_timer.reset(nullptr);
	AI_feign_remember_timer.reset(nullptr);
	AI_check_signal_timer.reset(nullptr);
	AI_scan_door_open_timer.reset(nullptr);

	hate_list.WipeHateList();
}

void NPC::AI_Stop() {
	Waypoints.clear();
	AIautocastspell_timer.reset(nullptr);
}

void Client::AI_Stop() {
	Mob::AI_Stop();
	MessageString(Chat::Red,PLAYER_REGAIN);

	auto app = new EQApplicationPacket(OP_Charm, sizeof(Charm_Struct));
	Charm_Struct *ps = (Charm_Struct*)app->pBuffer;
	ps->owner_id = 0;
	ps->pet_id = GetID();
	ps->command = 0;
	entity_list.QueueClients(this, app);
	safe_delete(app);

	SetTarget(entity_list.GetMob(pClientSideTarget));
	SendAppearancePacket(AppearanceType::Animation, GetAppearanceValue(GetAppearance()));
	SendAppearancePacket(AppearanceType::Linkdead, 0); // Removing LD packet so *LD* no longer appears by the player name when charmed/feared -Kasai
	if (!auto_attack) {
		attack_timer.Disable();
		attack_dw_timer.Disable();
	}
	if (IsLD())
	{
		Save();
		Disconnect();
	}
}

// only call this on a zone shutdown event
void Mob::AI_ShutDown() {
	attack_timer.Disable();
	attack_dw_timer.Disable();
	ranged_timer.Disable();
	tic_timer.Disable();
	mana_timer.Disable();
	spellend_timer.Disable();
	rewind_timer.Disable();
	bindwound_timer.Disable();
	stunned_timer.Disable();
	spun_timer.Disable();
	bardsong_timer.Disable();
	gravity_timer.Disable();
	viral_timer.Disable();
	flee_timer.Disable();

	for (int sat = 0; sat < SpecialAbility::Max; ++sat) {
		if (SpecialAbilities[sat].timer)
			SpecialAbilities[sat].timer->Disable();
	}
}

//todo: expand the logic here to cover:
//redundant debuffs
//buffing owner
//certain types of det spells that need special behavior.
void Client::AI_SpellCast()
{
	if(!charm_cast_timer.Check())
		return;

	Mob *targ = GetTarget();
	if(!targ)
		return;

	float dist = DistanceSquaredNoZ(m_Position, targ->GetPosition());

	std::vector<uint32> valid_spells;
	std::vector<uint32> slots;

	for(uint32 x = 0; x < 9; ++x)
	{
		uint32 current_spell = m_pp.mem_spells[x];
		if(!IsValidSpell(current_spell))
			continue;

		if(IsBeneficialSpell(current_spell))
		{
			continue;
		}

		if(dist > spells[current_spell].range*spells[current_spell].range)
		{
			continue;
		}

		if(GetMana() < spells[current_spell].mana)
		{
			continue;
		}

		if(IsEffectInSpell(current_spell, SpellEffect::Charm))
		{
			continue;
		}

		if(!GetPTimers().Expired(&database, pTimerSpellStart + current_spell, false))
		{
			continue;
		}

		if(targ->CanBuffStack(current_spell, GetLevel(), true) < 0)
		{
			continue;
		}

		//bard songs cause trouble atm
		if(IsBardSong(current_spell))
			continue;

		valid_spells.push_back(current_spell);
		slots.push_back(x);
	}

	uint32 spell_to_cast = 0xFFFFFFFF;
	EQ::spells::CastingSlot slot_to_use = EQ::spells::CastingSlot::Item;
	if(valid_spells.size() == 1)
	{
		spell_to_cast = valid_spells[0];
		slot_to_use = static_cast<EQ::spells::CastingSlot>(slots[0]);
	}
	else if(valid_spells.empty())
	{
		return;
	}
	else
	{
		uint32 idx = zone->random.Int(0, (valid_spells.size()-1));
		spell_to_cast = valid_spells[idx];
		slot_to_use = static_cast<EQ::spells::CastingSlot>(slots[idx]);
	}

	if(IsMesmerizeSpell(spell_to_cast) || IsFearSpell(spell_to_cast))
	{
		Mob *tar = entity_list.GetTargetForMez(this);
		if(!tar)
		{
			tar = GetTarget();
			if(tar && IsFearSpell(spell_to_cast))
			{
				if(!IsBardSong(spell_to_cast))
				{
					StopNavigation();
				}
				CastSpell(spell_to_cast, tar->GetID(), slot_to_use);
				return;
			}
		}
	}
	else
	{
		Mob *tar = GetTarget();
		if(tar)
		{
			if(!IsBardSong(spell_to_cast))
			{
				StopNavigation();
			}
			CastSpell(spell_to_cast, tar->GetID(), slot_to_use);
			return;
		}
	}


}

void Client::AI_Process()
{

	if (!IsAIControlled())
		return;

	if (!(AI_think_timer->Check() || attack_timer.Check(false)))
		return;

	if (IsCasting())
		return;

	bool engaged = IsEngaged();

	Mob *ow = GetOwner();
	if(!engaged)
	{
		if(ow)
		{
			if(ow->IsEngaged())
			{
				Mob *tar = ow->GetTarget();
				if(tar)
				{
					AddToHateList(tar, 1, 0, false);
				}
			}
		}
	}

	if(!ow)
	{
		if(!IsFeared() && !IsLD())
		{
			BuffFadeByEffect(SpellEffect::Charm);
			return;
		}
	}

	if (RuleB(Combat, EnableFearPathing)) {
		if (currently_fleeing) {

			if (IsRooted()) {
				//make sure everybody knows were not moving, for appearance sake
				if (IsMoving()) {
					FaceTarget();
					StopNavigation();
				}
				//continue on to attack code, ensuring that we execute the engaged code
				engaged = true;
			}
			else {
				if (AI_movement_timer->Check()) {
					// Check if we have reached the last fear point
					if(IsPositionEqualWithinCertainZ(glm::vec3(GetX(), GetY(), GetZ()), m_FearWalkTarget, 5.0f)) {
						CalculateNewFearpoint();
					}
					else {
						RunTo(m_FearWalkTarget.x, m_FearWalkTarget.y, m_FearWalkTarget.z);
					}
				}
				if (RuleB(Character, ProcessFearedProximity) && proximity_timer.Check()) {
					entity_list.ProcessMove(this, glm::vec3(GetX(), GetY(), GetZ()));
					if (RuleB(TaskSystem, EnableTaskSystem) && RuleB(TaskSystem, EnableTaskProximity))
						ProcessTaskProximities(GetX(), GetY(), GetZ());

					m_Proximity = glm::vec3(GetX(), GetY(), GetZ());
				}
				return;
			}

		}
	}

	if (engaged)
	{
		if (IsRooted())
			SetTarget(hate_list.GetClosestEntOnHateList(this));
		else
		{
			if(AI_target_check_timer->Check())
			{
				SetTarget(hate_list.GetMobWithMostHateOnList(this));
			}
		}

		if (!GetTarget())
			return;

		if (GetTarget()->IsCorpse()) {
			RemoveFromHateList(this);
			RemoveFromRampageList(this);
			return;
		}

		if(DivineAura())
			return;

		bool is_combat_range = CombatRange(GetTarget());

		if (is_combat_range) {
			if (IsMoving()) {
				StopNavigation();
			}

			if (charm_class_attacks_timer.Check()) {
				DoClassAttacks(GetTarget());
			}

			if (AI_movement_timer->Check()) {
				if (CalculateHeadingToTarget(GetTarget()->GetX(), GetTarget()->GetY()) !=
				    m_Position.w) {
					FaceTarget();
				}
			}
			if (GetTarget() && !IsStunned() && !IsMezzed() && !GetFeigned() && IsAttackAllowed(GetTarget())) {
				if (attack_timer.Check()) {
					// Should charmed clients not be procing?
					DoAttackRounds(GetTarget(), EQ::invslot::slotPrimary);
				}
			}

			if (CanThisClassDualWield() && GetTarget() && !IsStunned() && !IsMezzed() && !GetFeigned() && IsAttackAllowed(GetTarget())) {
				if (attack_dw_timer.Check()) {
					if (CheckDualWield()) {
						// Should charmed clients not be procing?
						DoAttackRounds(GetTarget(), EQ::invslot::slotSecondary);
					}
				}
			}
		} else {
			if(!IsRooted())
			{
				if(AI_movement_timer->Check())
				{
					RunTo(GetTarget()->GetX(), GetTarget()->GetY(), GetTarget()->GetZ());
				}
			}
			else if(IsMoving())
			{
				FaceTarget();
			}
		}
		AI_SpellCast();
	}
	else
	{
		if(AI_feign_remember_timer->Check()) {
			std::set<uint32>::iterator remembered_feigned_mobid;
			remembered_feigned_mobid = feign_memory_list.begin();
			while (remembered_feigned_mobid != feign_memory_list.end()) {

				Mob* remembered_mob = entity_list.GetMob(*remembered_feigned_mobid);
				if (remembered_mob == nullptr || remembered_mob->IsCorpse()) {
					//they are gone now...
					remembered_feigned_mobid = feign_memory_list.erase(remembered_feigned_mobid);
				} else if (!remembered_mob->GetFeigned()) {
					AddToHateList(remembered_mob,1);
					remembered_feigned_mobid = feign_memory_list.erase(remembered_feigned_mobid);
					break;
				} else {
					//they are still feigned, carry on...
					++remembered_feigned_mobid;
				}
			}
		}

		if (IsPet()) {
			Mob *owner = GetOwner();
			if (owner == nullptr)
				return;

			float dist = DistanceSquared(m_Position, owner->GetPosition());
			if (dist >= 202500) { // >= 450 distance
				Teleport(owner->GetPosition());
			} else if (dist >= 400) { // >=20
				if (AI_movement_timer->Check()) {
					if (dist >= 1225) {
						RunTo(owner->GetX(), owner->GetY(), owner->GetZ());
					}
					else {
						WalkTo(owner->GetX(), owner->GetY(), owner->GetZ());
					}
				}
			} else {
				StopNavigation();
			}
		}
	}
}

void Mob::ProcessForcedMovement()
{
	// we are being pushed, we will hijack this movement timer
	// this also needs to be done before casting to have a chance to interrupt
	// this flag won't be set if the mob can't be pushed (rooted etc)
	if (AI_movement_timer->Check()) {
		bool bPassed = true;
		glm::vec3 normal;

		// no zone map = fucked
		if (zone->HasMap()) {
			// in front
			m_CollisionBox[0].x = m_Position.x + 3.0f * g_Math.FastSin(0.0f);
			m_CollisionBox[0].y = m_Position.y + 3.0f * g_Math.FastCos(0.0f);
			m_CollisionBox[0].z = m_Position.z;

			// 45 right front
			m_CollisionBox[1].x = m_Position.x + 3.0f * g_Math.FastSin(64.0f);
			m_CollisionBox[1].y = m_Position.y + 3.0f * g_Math.FastCos(64.0f);
			m_CollisionBox[1].z = m_Position.z;

			// to right
			m_CollisionBox[2].x = m_Position.x + 3.0f * g_Math.FastSin(128.0f);
			m_CollisionBox[2].y = m_Position.y + 3.0f * g_Math.FastCos(128.0f);
			m_CollisionBox[2].z = m_Position.z;

			// 45 right back
			m_CollisionBox[3].x = m_Position.x + 3.0f * g_Math.FastSin(192.0f);
			m_CollisionBox[3].y = m_Position.y + 3.0f * g_Math.FastCos(192.0f);
			m_CollisionBox[3].z = m_Position.z;

			// behind
			m_CollisionBox[4].x = m_Position.x + 3.0f * g_Math.FastSin(256.0f);
			m_CollisionBox[4].y = m_Position.y + 3.0f * g_Math.FastCos(256.0f);
			m_CollisionBox[4].z = m_Position.z;

			// 45 left back
			m_CollisionBox[5].x = m_Position.x + 3.0f * g_Math.FastSin(320.0f);
			m_CollisionBox[5].y = m_Position.y + 3.0f * g_Math.FastCos(320.0f);
			m_CollisionBox[5].z = m_Position.z;

			// to left
			m_CollisionBox[6].x = m_Position.x + 3.0f * g_Math.FastSin(384.0f);
			m_CollisionBox[6].y = m_Position.y + 3.0f * g_Math.FastCos(384.0f);
			m_CollisionBox[6].z = m_Position.z;

			// 45 left front
			m_CollisionBox[7].x = m_Position.x + 3.0f * g_Math.FastSin(448.0f);
			m_CollisionBox[7].y = m_Position.y + 3.0f * g_Math.FastCos(448.0f);
			m_CollisionBox[7].z = m_Position.z;

			// collision happened, need to move along the wall
			float distance = 0.0f, shortest = std::numeric_limits<float>::infinity();
			glm::vec3 tmp_nrm;
			for (auto &vec : m_CollisionBox) {
				if (zone->zonemap->DoCollisionCheck(vec, vec + m_Delta, tmp_nrm, distance)) {
					bPassed = false; // lets try with new projection next pass
					if (distance < shortest) {
						normal = tmp_nrm;
						shortest = distance;
					}
				}
			}
		}

		if (bPassed) {
			ForcedMovement = 0;
			Teleport(m_Position + m_Delta);
			m_Delta = glm::vec4();
			SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0, true);
			FixZ(); // so we teleport to the ground locally, we want the client to interpolate falling etc
		} else if (--ForcedMovement) {
			if (normal.z < -0.15f) // prevent too much wall climbing. ex. OMM's room in anguish
				normal.z = 0.0f;
			auto proj = glm::proj(static_cast<glm::vec3>(m_Delta), normal);
			m_Delta.x -= proj.x;
			m_Delta.y -= proj.y;
			m_Delta.z -= proj.z;
		} else {
			m_Delta = glm::vec4(); // well, we failed to find a spot to be forced to, lets give up
		}
	}
}

void Mob::AI_Process() {
	if (!IsAIControlled())
		return;

	if (!(AI_think_timer->Check() || attack_timer.Check(false)))
		return;


	if (IsCasting())
		return;

	bool engaged  = IsEngaged();
	bool doranged = false;

	if (!zone->CanDoCombat() || IsPetStop() || IsPetRegroup()) {
		engaged = false;
	}

	if (moving && CanOpenDoors()) {
		if (AI_scan_door_open_timer->Check()) {
			HandleDoorOpen();
		}
	}

	// Begin: Additions for Wiz Fear Code
	//
	if (RuleB(Combat, EnableFearPathing)) {
		if (currently_fleeing) {
			if ((IsRooted() || (IsBlind() && CombatRange(hate_list.GetClosestEntOnHateList(this)))) && !IsPetStop() &&
				!IsPetRegroup()) {
				//make sure everybody knows were not moving, for appearance sake
				if (IsMoving()) {
					FaceTarget();
					StopNavigation();
					moved = false;
				}
				//continue on to attack code, ensuring that we execute the engaged code
				engaged = true;
			}
			else {
				if (AI_movement_timer->Check()) {
					// Check if we have reached the last fear point
					if (DistanceNoZ(glm::vec3(GetX(), GetY(), GetZ()), m_FearWalkTarget) <= 5.0f) {
						// Calculate a new point to run to
						StopNavigation();
						CalculateNewFearpoint();
					}
					RunTo(
						m_FearWalkTarget.x,
						m_FearWalkTarget.y,
						m_FearWalkTarget.z
					);
				}
				return;
			}
		}
	}

	// trigger EVENT_SIGNAL if required
	if (AI_check_signal_timer->Check() && IsNPC()) {
		CastToNPC()->CheckSignal();
	}

	if (engaged) {
		if (IsNPC() && m_z_clip_check_timer.Check()) {
			bool is_moving = IsMoving() && !(IsRooted() || IsStunned() || IsMezzed());
			auto t         = GetTarget();
			if (is_moving && t) {
				float self_z            = GetZ() - GetZOffset();
				float target_z          = t->GetPosition().z - t->GetZOffset();
				bool  can_path_to       = CastToNPC()->CanPathTo(t->GetX(), t->GetY(), t->GetZ());
				bool  within_distance   = DistanceNoZ(GetPosition(), t->GetPosition()) < 75;
				bool  within_z_distance = std::abs(self_z - target_z) >= 25;
				if (within_distance && within_z_distance && !can_path_to) {
					float new_z = FindDestGroundZ(t->GetPosition());
					GMMove(t->GetPosition().x, t->GetPosition().y, new_z + GetZOffset(), t->GetPosition().w, false);
					FaceTarget(t);
				}
			}
		}

		if (!(GetPlayerState() & static_cast<uint32>(PlayerState::Aggressive)))
			SendAddPlayerState(PlayerState::Aggressive);

		// NPCs will forget people after 10 mins of not interacting with them or out of range
		// both of these maybe zone specific, hardcoded for now
		if (hate_list_cleanup_timer.Check()) {
			hate_list.RemoveStaleEntries(600000, static_cast<float>(zone->newzone_data.npc_aggro_max_dist));
			if (hate_list.IsHateListEmpty()) {
				AI_Event_NoLongerEngaged();
				zone->DelAggroMob();
				if (IsNPC() && !RuleB(Aggro, AllowTickPulling))
					ResetAssistCap();
			}
		}
		// we are prevented from getting here if we are blind and don't have a target in range
		// from above, so no extra blind checks needed
		if ((IsRooted() && !GetSpecialAbility(SpecialAbility::IgnoreRootAggroRules)) || IsBlind())
			SetTarget(hate_list.GetClosestEntOnHateList(this));
		else {
			if (AI_target_check_timer->Check()) {
				if (
					IsNPC() &&
					!CastToNPC()->GetSwarmInfo() &&
					(!IsPet() || (HasOwner() && GetOwner()->IsNPC())) &&
					!CastToNPC()->GetNPCAggro()
				) {
					WipeHateList(true); // wipe NPCs from hate list to prevent faction war
				}

				if (IsFocused()) {
					if (!target) {
						SetTarget(hate_list.GetMobWithMostHateOnList(this));
					}
				}
				else {
					if (!ImprovedTaunt())
						SetTarget(hate_list.GetMobWithMostHateOnList(this));
				}

			}
		}

		if (!target)
			return;

		if (target->IsCorpse()) {
			RemoveFromHateList(this);
			RemoveFromRampageList(this);
			return;
		}

		if (target->IsMezzed() && IsPet()) {

			auto pet_owner = GetOwner();
			if (pet_owner && pet_owner->IsClient()) {
				pet_owner->MessageString(Chat::NPCQuestSay, CANNOT_WAKE, GetCleanName(), target->GetCleanName());
			}

			RemoveFromHateList(target);
			return;
		}

		if (IsPet() && GetOwner() && GetOwner()->IsBot() && target == GetOwner())
		{
			// this blocks all pet attacks against owner..bot pet test (copied above check)
			RemoveFromHateList(this);
			return;
		}

		if (DivineAura())
			return;

		ProjectileAttack();

		if (shield_timer.Check()) {
			ShieldAbilityFinish();
		}

		auto npcSpawnPoint = CastToNPC()->GetSpawnPoint();
		if (GetSpecialAbility(SpecialAbility::Tether)) {
			float tether_range = static_cast<float>(GetSpecialAbilityParam(SpecialAbility::Tether, 0));
			tether_range = tether_range > 0.0f ? tether_range * tether_range : pAggroRange * pAggroRange;

			if (DistanceSquaredNoZ(m_Position, npcSpawnPoint) > tether_range) {
				GMMove(npcSpawnPoint.x, npcSpawnPoint.y, npcSpawnPoint.z, npcSpawnPoint.w);
			}
		}
		else if (GetSpecialAbility(SpecialAbility::Leash)) {
			float leash_range = static_cast<float>(GetSpecialAbilityParam(SpecialAbility::Leash, 0));
			leash_range = leash_range > 0.0f ? leash_range * leash_range : pAggroRange * pAggroRange;

			if (DistanceSquaredNoZ(m_Position, npcSpawnPoint) > leash_range) {
				GMMove(npcSpawnPoint.x, npcSpawnPoint.y, npcSpawnPoint.z, npcSpawnPoint.w);
				RestoreHealth();
				BuffFadeAll();
				WipeHateList();
				return;
			}
		}

		StartEnrage();

		bool is_combat_range = CombatRange(target);

		if (is_combat_range) {
			if (IsMoving()) {
				StopNavigation();
			}

			FaceTarget();

			//casting checked above...
			if (target && !IsStunned() && !IsMezzed() && GetAppearance() != eaDead && !IsMeleeDisabled()) {

				//we should check to see if they die mid-attacks, previous
				//crap of checking target for null was not gunna cut it

				//try main hand first
				if (attack_timer.Check()) {
					DoMainHandAttackRounds(target);
					TriggerDefensiveProcs(target, EQ::invslot::slotPrimary, false);

					bool specialed = false; // NPCs can only do one of these a round
					if (GetSpecialAbility(SpecialAbility::Flurry)) {
						int flurry_chance = GetSpecialAbilityParam(SpecialAbility::Flurry, 0);
						flurry_chance = flurry_chance > 0 ? flurry_chance : RuleI(Combat, NPCFlurryChance);

						if (zone->random.Roll(flurry_chance)) {
							ExtraAttackOptions opts;
							int                cur = GetSpecialAbilityParam(SpecialAbility::Flurry, 2);
							if (cur > 0)
								opts.damage_percent = cur / 100.0f;

							cur = GetSpecialAbilityParam(SpecialAbility::Flurry, 3);
							if (cur > 0)
								opts.damage_flat = cur;

							cur = GetSpecialAbilityParam(SpecialAbility::Flurry, 4);
							if (cur > 0)
								opts.armor_pen_percent = cur / 100.0f;

							cur = GetSpecialAbilityParam(SpecialAbility::Flurry, 5);
							if (cur > 0)
								opts.armor_pen_flat = cur;

							cur = GetSpecialAbilityParam(SpecialAbility::Flurry, 6);
							if (cur > 0)
								opts.crit_percent = cur / 100.0f;

							cur = GetSpecialAbilityParam(SpecialAbility::Flurry, 7);
							if (cur > 0)
								opts.crit_flat = cur;

							Flurry(&opts);
							specialed = true;
						}
					}

					if (IsPet() || IsTempPet()) {
						Mob *owner = nullptr;
						owner = GetOwner();

						if (owner) {
							int16 flurry_chance = owner->aabonuses.PetFlurry +
												  owner->spellbonuses.PetFlurry + owner->itembonuses.PetFlurry;

							if (flurry_chance && zone->random.Roll(flurry_chance))
								Flurry(nullptr);
						}
					}


					//SE_PC_Pet_Rampage SPA 464 on pet, chance modifier
					if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) {
						int chance = spellbonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_CHANCE] + itembonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_CHANCE] + aabonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_CHANCE];
						if (chance && zone->random.Roll(chance)) {
							Rampage(nullptr);
						}
					}


					if (GetSpecialAbility(SpecialAbility::Rampage) && !specialed) {
						int rampage_chance = GetSpecialAbilityParam(SpecialAbility::Rampage, 0);
						rampage_chance = rampage_chance > 0 ? rampage_chance : 20;
						if (zone->random.Roll(rampage_chance)) {
							ExtraAttackOptions opts;
							int                cur = GetSpecialAbilityParam(SpecialAbility::Rampage, 3);
							if (cur > 0) {
								opts.damage_flat = cur;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::Rampage, 4);
							if (cur > 0) {
								opts.armor_pen_percent = cur / 100.0f;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::Rampage, 5);
							if (cur > 0) {
								opts.armor_pen_flat = cur;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::Rampage, 6);
							if (cur > 0) {
								opts.crit_percent = cur / 100.0f;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::Rampage, 7);
							if (cur > 0) {
								opts.crit_flat = cur;
							}
							Rampage(&opts);
							specialed = true;
						}
					}

					//SE_PC_Pet_Rampage SPA 465 on pet, chance modifier
					if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) {
						int chance = spellbonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_CHANCE] + itembonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_CHANCE] + aabonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_CHANCE];
						if (chance && zone->random.Roll(chance)) {
							Rampage(nullptr);
						}
					}

					if (GetSpecialAbility(SpecialAbility::AreaRampage) && !specialed) {
						int rampage_chance = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 0);
						rampage_chance = rampage_chance > 0 ? rampage_chance : 20;
						if (zone->random.Roll(rampage_chance)) {
							ExtraAttackOptions opts;
							int                cur = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 3);
							if (cur > 0) {
								opts.damage_flat = cur;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 4);
							if (cur > 0) {
								opts.armor_pen_percent = cur / 100.0f;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 5);
							if (cur > 0) {
								opts.armor_pen_flat = cur;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 6);
							if (cur > 0) {
								opts.crit_percent = cur / 100.0f;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 7);
							if (cur > 0) {
								opts.crit_flat = cur;
							}

							cur = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 8);
							if (cur > 0) {
								opts.range_percent = cur;
							}

							AreaRampage(&opts);
							specialed = true;
						}
					}
				}

				//now off hand
				if (attack_dw_timer.Check() && CanThisClassDualWield())
					DoOffHandAttackRounds(target);

				//now special attacks (kick, etc)
				if (IsNPC())
					CastToNPC()->DoClassAttacks(target);

			}
			AI_EngagedCastCheck();

		}    //end is within combat rangepet
		else {

			// See if we can summon the mob to us
			if (!HateSummon()) {

				//could not summon them, check ranged...
				if (GetSpecialAbility(SpecialAbility::RangedAttack) || HasBowAndArrowEquipped()) {
					doranged = true;
				}

				// Now pursue
				// TODO: Check here for another person on hate list with close hate value
				if (AI_PursueCastCheck()) {
					if (IsCasting() && GetClass() != Class::Bard) {
						StopNavigation();
						FaceTarget();
					}
				}
				// mob/npc waits until call for help complete, others can move
				else if (AI_movement_timer->Check() && target &&
						(GetOwnerID() || IsBot() || IsTempPet() ||
						CastToNPC()->GetCombatEvent())) {
					if (!IsRooted()) {
						LogAIDetail("Pursuing [{}] while engaged", target->GetName());
						RunTo(target->GetX(), target->GetY(), target->GetZ());

					}
					else {
						FaceTarget();
					}
				}
			}
		}
	}
	else {
		if (GetPlayerState() & static_cast<uint32>(PlayerState::Aggressive))
			SendRemovePlayerState(PlayerState::Aggressive);

		if (IsPetStop()) // pet stop won't be engaged, so we will always get here and we want the above branch to execute
			return;

		if (zone->CanDoCombat() && AI_feign_remember_timer->Check()) {
			// 6/14/06
			// Improved Feign Death Memory
			// check to see if any of our previous feigned targets have gotten up.
			std::set<uint32>::iterator remembered_feigned_mobid;
			remembered_feigned_mobid = feign_memory_list.begin();
			while (remembered_feigned_mobid != feign_memory_list.end()) {
				Mob *remembered_mob = entity_list.GetMob(*remembered_feigned_mobid);
				if (remembered_mob == nullptr || remembered_mob->IsCorpse()) {
					//they are gone now...
					remembered_feigned_mobid = feign_memory_list.erase(remembered_feigned_mobid);
				}
				else if (!remembered_mob->GetFeigned()) {
					AddToHateList(remembered_mob, 1);
					remembered_feigned_mobid = feign_memory_list.erase(remembered_feigned_mobid);
					break;
				}
				else {
					//they are still feigned, carry on...
					++remembered_feigned_mobid;
				}
			}
		}
		if (AI_IdleCastCheck()) {
			if (IsCasting() && GetClass() != Class::Bard) {
				StopNavigation();
			}
		}
		else if (zone->CanDoCombat() && IsNPC() && CastToNPC()->GetNPCAggro() && AI_scan_area_timer->Check()) {
			CastToNPC()->DoNpcToNpcAggroScan();
		}
		else if (AI_movement_timer->Check() && !IsRooted()) {
			if (IsPet()) {
				// we're a pet, do as we're told
				switch (m_pet_order) {
					case PetOrder::Follow: {

						Mob *owner = GetOwner();
						if (owner == nullptr) {
							break;
						}

						glm::vec4 pet_owner_position = owner->GetPosition();
						float     distance_to_owner  = DistanceSquared(m_Position, pet_owner_position);
						float     z_distance         = pet_owner_position.z - m_Position.z;

						if (distance_to_owner >= 400 || z_distance > 100) {

							bool running = false;

							/**
							 * Distance: >= 35 (Run if far away)
							 */
							if (distance_to_owner >= 1225) {
								running = true;
							}

							/**
							 * Distance: >= 450 (Snap to owner)
							 */
							if (distance_to_owner >= 202500 || z_distance > 100) {
								if (running) {
									RunTo(pet_owner_position.x, pet_owner_position.y, pet_owner_position.z);
								}
								else {
									WalkTo(pet_owner_position.x, pet_owner_position.y, pet_owner_position.z);
								}
							}
							else {

								if (running) {
									RunTo(pet_owner_position.x, pet_owner_position.y, pet_owner_position.z);
								}
								else {
									WalkTo(pet_owner_position.x, pet_owner_position.y, pet_owner_position.z);
								}
							}
						}
						else {
							StopNavigation();
						}

						break;
					}
					case PetOrder::Sit: {
						SetAppearance(eaSitting, false);
						break;
					}
					case PetOrder::Guard: {
						//only NPCs can guard stuff. (forced by where the guard movement code is in the AI)
						if (IsNPC()) {
							CastToNPC()->NextGuardPosition();
						}
						break;
					}
					case PetOrder::Feign: {
						SetAppearance(eaDead, false);
						break;
					}
				}
				if (IsPetRegroup()) {
					return;
				}
			}
				/* Entity has been assigned another entity to follow */
			else if (GetFollowID()) {
				Mob *follow = entity_list.GetMob(static_cast<uint16>(GetFollowID()));
				if (!follow) {
					SetFollowID(0);
					SetFollowDistance(100);
					SetFollowCanRun(true);
				}
				else {

					float distance        = DistanceSquared(m_Position, follow->GetPosition());
					int   follow_distance = GetFollowDistance();

					/**
					 * Default follow distance is 100
					 */
					if (distance >= follow_distance) {
						bool running = false;
						// maybe we want the NPC to only walk doing follow logic
						if (GetFollowCanRun() && distance >= follow_distance + 150) {
							running = true;
						}

						auto &Goal = follow->GetPosition();

						if (running) {
							RunTo(Goal.x, Goal.y, Goal.z);
						}
						else {
							WalkTo(Goal.x, Goal.y, Goal.z);
						}
					}
					else {
						moved = false;
						StopNavigation();
					}
				}
			}
			else //not a pet, and not following somebody...
			{
				// dont move till a bit after you last fought
				if (time_until_can_move < Timer::GetCurrentTime()) {
					if (IsClient()) {

						/**
						 * LD timer expired, drop out of world
						 */
						if (CastToClient()->IsLD()) {
							CastToClient()->Disconnect();
						}

						return;
					}

					if (IsNPC()) {
						if (RuleB(NPC, SmartLastFightingDelayMoving) && !feign_memory_list.empty()) {
							minLastFightingDelayMoving = 0;
							maxLastFightingDelayMoving = 0;
						}
						/* All normal NPC pathing */
						CastToNPC()->AI_DoMovement();
					}
				}
			}
		}
	}

	if (forget_timer.Check()) {
		forget_timer.Disable();
		entity_list.ClearZoneFeignAggro(this);
	}

	//Do Ranged attack here
	if (doranged) {
		RangedAttack(target);
	}
}

void NPC::AI_DoMovement() {

	float move_speed = GetMovespeed();

	if (move_speed <= 0.0f) {
		return;
	}

	// Roambox logic sets precedence
	if (m_roambox.distance > 0) {
		HandleRoambox();
		return;
	}
	else if (roamer) {
		if (AI_walking_timer->Check()) {
			pause_timer_complete = true;
			AI_walking_timer->Disable();
		}

		int32 gridno = CastToNPC()->GetGrid();

		if (gridno > 0 || cur_wp == EQ::WaypointStatus::QuestControlNoGrid) {
			if (pause_timer_complete == true) { // time to pause at wp is over
				AI_SetupNextWaypoint();
			}    // endif (pause_timer_complete==true)
			else if (!(AI_walking_timer->Enabled())) {    // currently moving
				bool doMove = true;
				if(IsPositionEqual(glm::vec2(m_CurrentWayPoint.x, m_CurrentWayPoint.y), glm::vec2(GetX(), GetY()))) {
					LogAIDetail("We have reached waypoint [{}] ({},{},{}) on grid [{}]",
						cur_wp,
						GetX(),
						GetY(),
						GetZ(),
						GetGrid());

					if (wandertype == GridRandomPath)
					{
						if (cur_wp == patrol)
						{
							// reached our randomly selected destination; force a pause
							if (cur_wp_pause == 0)
							{
								if (Waypoints.size() >= cur_wp && Waypoints[cur_wp].pause)
									cur_wp_pause = Waypoints[cur_wp].pause;
								else if (Waypoints.size() > 0 && Waypoints[0].pause)
									cur_wp_pause = Waypoints[0].pause;
								else
									cur_wp_pause = 38;
							}
							Log(Logs::Detail, Logs::AI, "NPC using wander type GridRandomPath on grid %d at waypoint %d has reached its random destination; pause time is %d", GetGrid(), cur_wp, cur_wp_pause);
						}
						else
							cur_wp_pause = 0; // skipping pauses until destination
					}

					SetWaypointPause();
					if (GetAppearance() != eaStanding) {
						SetAppearance(eaStanding, false);
					}
					if (cur_wp_pause > 0 && m_CurrentWayPoint.w >= 0.0) {
						RotateTo(m_CurrentWayPoint.w);
					}

					if (parse->HasQuestSub(GetNPCTypeID(), EVENT_WAYPOINT_ARRIVE)) {
						parse->EventNPC(EVENT_WAYPOINT_ARRIVE, CastToNPC(), nullptr, std::to_string(cur_wp), 0);
					}

					// No need to move as we are there.  Next loop will
					// take care of normal grids, even at pause 0.
					// We do need to call and setup a wp if we're cur_wp=-2
					// as that is where roamer is unset and we don't want
					// the next trip through to move again based on grid stuff.
					doMove = false;
					if (cur_wp == EQ::WaypointStatus::QuestControlNoGrid) {
						AI_SetupNextWaypoint();
					}

					// wipe feign memory since we reached our first waypoint
					if (cur_wp == 1)
						ClearFeignMemory();

					if (cur_wp_pause == 0) {
						pause_timer_complete = true;
						AI_SetupNextWaypoint();
						doMove = true;
					}
				}

				if (doMove) {    // not at waypoint yet or at 0 pause WP, so keep moving
					NavigateTo(
						m_CurrentWayPoint.x,
						m_CurrentWayPoint.y,
						m_CurrentWayPoint.z
					);

				}
			}
		}        // endif (gridno > 0)
			// handle new quest grid command processing
		else if (gridno < 0) {    // this mob is under quest control
			if (pause_timer_complete == true) { // time to pause has ended
				SetGrid(0 - GetGrid()); // revert to AI control
				LogPathing("Quest pathing is finished. Resuming on grid [{}]", GetGrid());

				SetAppearance(eaStanding, false);

				CalculateNewWaypoint();
			}
		}

	}
	else if (IsGuarding()) {
		bool at_gp = IsPositionEqualWithinCertainZ(m_Position, m_GuardPoint, 15.0f);

		if (at_gp) {

			if (moved) {
				LogAIDetail("Reached guard point ({},{},{})", m_GuardPoint.x, m_GuardPoint.y, m_GuardPoint.z);

				ClearFeignMemory();
				moved = false;
				if (GetTarget() == nullptr || DistanceSquared(m_Position, GetTarget()->GetPosition()) >= 5 * 5) {
					RotateTo(m_GuardPoint.w);
				}
				else {
					FaceTarget(GetTarget());
				}
				SetAppearance(GetGuardPointAnim());
			}
		}
		else {
			NavigateTo(m_GuardPoint.x, m_GuardPoint.y, m_GuardPoint.z);
		}
	}
}

void NPC::AI_SetupNextWaypoint() {
	int32 spawn_id = GetSpawnPointID();
	LinkedListIterator<Spawn2*> iterator(zone->spawn2_list);
	iterator.Reset();
	Spawn2 *found_spawn = nullptr;

	while (iterator.MoreElements())
	{
		Spawn2* cur = iterator.GetData();
		iterator.Advance();
		if (cur->GetID() == spawn_id)
		{
			found_spawn = cur;
			break;
		}
	}

	if (wandertype == GridOneWayRepop && cur_wp == CastToNPC()->GetMaxWp()) {
		CastToNPC()->Depop(true); //depop and restart spawn timer
		if (found_spawn)
			found_spawn->SetNPCPointerNull();
	}
	else if (wandertype == GridOneWayDepop && cur_wp == CastToNPC()->GetMaxWp()) {
		CastToNPC()->Depop(false);//depop without spawn timer
		if (found_spawn)
			found_spawn->SetNPCPointerNull();
	}
	else {
		pause_timer_complete = false;
		LogPathingDetail(
			"[{}] departing waypoint [{}]",
			GetCleanName(),
			cur_wp
		);
		//if we were under quest control (with no grid), we are done now..
		if (cur_wp == EQ::WaypointStatus::QuestControlNoGrid) {
			LogPathing("Non-grid quest mob has reached its quest ordered waypoint. Leaving pathing mode");
			roamer = false;
			cur_wp = 0;
		}

		SetAppearance(eaStanding, false);

		entity_list.OpenDoorsNear(this);

		if (!DistractedFromGrid) {
			if (parse->HasQuestSub(GetNPCTypeID(), EVENT_WAYPOINT_DEPART)) {
				parse->EventNPC(EVENT_WAYPOINT_DEPART, CastToNPC(), nullptr, std::to_string(cur_wp), 0);
			}

			//setup our next waypoint, if we are still on our normal grid
			//remember that the quest event above could have done anything it wanted with our grid
			if (GetGrid() > 0) {
				CastToNPC()->CalculateNewWaypoint();
			}
		}
		else {
			DistractedFromGrid = false;
		}
	}
}

/**
 * @param attacker
 * @param yell_for_help
 */
void Mob::AI_Event_Engaged(Mob *attacker, bool yell_for_help)
{
	if (!IsAIControlled()) {
		return;
	}

	SetAppearance(eaStanding);

	parse->EventBotMerc(EVENT_COMBAT, this, attacker, [&] { return "1"; });

	if (IsNPC()) {
		CastToNPC()->AIautocastspell_timer->Start(300, false);

		if (yell_for_help) {
			if (IsPet()) {
				GetOwner()->AI_Event_Engaged(attacker, yell_for_help);
			}
			else if (!HasAssistAggro() && NPCAssistCap() < RuleI(Combat, NPCAssistCap)) {
				CastToNPC()->AIYellForHelp(this, attacker);
				if (NPCAssistCap() > 0 && !assist_cap_timer.Enabled()) {
					assist_cap_timer.Start(RuleI(Combat, NPCAssistCapTimer));
				}
			}
		}

		if (CastToNPC()->GetGrid() > 0) {
			DistractedFromGrid = true;
		}
		if (attacker && !attacker->IsCorpse()) {
			//Because sometimes the AIYellForHelp triggers another engaged and then immediately a not engaged
			//if the target dies before it goes off
			if (attacker->GetHP() > 0) {
				if (!CastToNPC()->GetCombatEvent() && GetHP() > 0) {
					if (parse->HasQuestSub(GetNPCTypeID(), EVENT_COMBAT)) {
						parse->EventNPC(EVENT_COMBAT, CastToNPC(), attacker, "1", 0);
					}

					if (emoteid) {
						CastToNPC()->DoNPCEmote(EQ::constants::EmoteEventTypes::EnterCombat, emoteid, attacker);
					}

					std::string mob_name = GetCleanName();
					m_combat_record.Start(mob_name);
					CastToNPC()->SetCombatEvent(true);
				}
			}
		}
	}
}

// Note: Hate list may not be actually clear until after this function call completes
void Mob::AI_Event_NoLongerEngaged() {
	if (!IsAIControlled()) {
		return;
	}

	AI_walking_timer->Start(RandomTimer(3000,20000));
	time_until_can_move = Timer::GetCurrentTime();

	if (minLastFightingDelayMoving == maxLastFightingDelayMoving) {
		time_until_can_move += minLastFightingDelayMoving;
	} else {
		time_until_can_move += zone->random.Int(minLastFightingDelayMoving, maxLastFightingDelayMoving);
	}

	StopNavigation();
	ClearRampage();

	if (IsNPC()) {
		SetPrimaryAggro(false);
		SetAssistAggro(false);
		if (
			CastToNPC()->GetCombatEvent() &&
			GetHP() > 0 &&
			entity_list.GetNPCByID(GetID())
		) {
			if (parse->HasQuestSub(GetNPCTypeID(), EVENT_COMBAT)) {
				parse->EventNPC(EVENT_COMBAT, CastToNPC(), nullptr, "0", 0);
			}

			const uint32 emote_id = CastToNPC()->GetEmoteID();
			if (emote_id) {
				CastToNPC()->DoNPCEmote(EQ::constants::EmoteEventTypes::LeaveCombat, emote_id);
			}

			m_combat_record.Stop();
			CastToNPC()->SetCombatEvent(false);
		}
	} else {
		parse->EventBotMerc(EVENT_COMBAT, this, nullptr, [&]() { return "0"; });
	}
}

//this gets called from InterruptSpell() for failure or SpellFinished() for success
void NPC::AI_Event_SpellCastFinished(bool iCastSucceeded, uint16 slot) {
	if (slot == 1) {
		uint32 recovery_time = 0;
		if (iCastSucceeded) {
			if (casting_spell_AIindex < AIspells.size()) {
					recovery_time += spells[AIspells[casting_spell_AIindex].spellid].recovery_time;
					if (AIspells[casting_spell_AIindex].recast_delay >= 0)
					{
						if (AIspells[casting_spell_AIindex].recast_delay < 10000)
							AIspells[casting_spell_AIindex].time_cancast = Timer::GetCurrentTime() + (AIspells[casting_spell_AIindex].recast_delay*1000);
					}
					else
						AIspells[casting_spell_AIindex].time_cancast = Timer::GetCurrentTime() + spells[AIspells[casting_spell_AIindex].spellid].recast_time;
			}
			if (recovery_time < AIautocastspell_timer->GetSetAtTrigger())
				recovery_time = AIautocastspell_timer->GetSetAtTrigger();
			AIautocastspell_timer->Start(recovery_time, false);
		}
		else
			AIautocastspell_timer->Start(AISpellVar.fail_recast, false);
		casting_spell_AIindex = AIspells.size();
	}
}


bool NPC::AI_EngagedCastCheck() {
	if (AIautocastspell_timer->Check(false)) {
		AIautocastspell_timer->Disable();	//prevent the timer from going off AGAIN while we are casting.

		LogAIDetail("Engaged autocast check triggered. Trying to cast healing spells then maybe offensive spells");

		// first try innate (spam) spells
		if(!AICastSpell(GetTarget(), 0, SpellType_Nuke | SpellType_Lifetap | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff | SpellType_Charm | SpellType_Root, true)) {
			// try innate (spam) self targeted spells
			if (!AICastSpell(this, 0, SpellType_InCombatBuff, true)) {
				// try casting a heal or gate
				if (!AICastSpell(this, AISpellVar.engaged_beneficial_self_chance, SpellType_Heal | SpellType_Escape | SpellType_InCombatBuff)) {
					// try casting a heal on nearby
					if (!AICheckCloseBeneficialSpells(this, AISpellVar.engaged_beneficial_other_chance, MobAISpellRange, SpellType_Heal)) {
						//nobody to heal, try some detrimental spells.
						if(!AICastSpell(GetTarget(), AISpellVar.engaged_detrimental_chance, SpellType_Nuke | SpellType_Lifetap | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff | SpellType_Charm | SpellType_Root)) {
							//no spell to cast, try again soon.
							AIautocastspell_timer->Start(RandomTimer(AISpellVar.engaged_no_sp_recast_min, AISpellVar.engaged_no_sp_recast_max), false);
						}
					}
				}
			}
		}
		return(true);
	}

	return(false);
}

bool NPC::AI_PursueCastCheck() {
	if (AIautocastspell_timer->Check(false)) {
		AIautocastspell_timer->Disable();	//prevent the timer from going off AGAIN while we are casting.

		LogAIDetail("Engaged (pursuing) autocast check triggered. Trying to cast offensive spells");
		// checking innate (spam) spells first
		if(!AICastSpell(GetTarget(), AISpellVar.pursue_detrimental_chance, SpellType_Root | SpellType_Nuke | SpellType_Lifetap | SpellType_Snare | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff, true)) {
			if(!AICastSpell(GetTarget(), AISpellVar.pursue_detrimental_chance, SpellType_Root | SpellType_Nuke | SpellType_Lifetap | SpellType_Snare | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff)) {
				//no spell cast, try again soon.
				AIautocastspell_timer->Start(RandomTimer(AISpellVar.pursue_no_sp_recast_min, AISpellVar.pursue_no_sp_recast_max), false);
			} //else, spell casting finishing will reset the timer.
		}
		return(true);
	}
	return(false);
}

bool NPC::AI_IdleCastCheck() {
	if (AIautocastspell_timer->Check(false)) {
		AIautocastspell_timer->Disable();	//prevent the timer from going off AGAIN while we are casting.
		if (!AICastSpell(this, AISpellVar.idle_beneficial_chance, SpellType_Heal | SpellType_Buff | SpellType_Pet)) {
			if(!AICheckCloseBeneficialSpells(this, 33, MobAISpellRange, SpellType_Heal | SpellType_Buff)) {
				//if we didnt cast any spells, our autocast timer just resets to the
				//last duration it was set to... try to put up a more reasonable timer...
				AIautocastspell_timer->Start(RandomTimer(AISpellVar.idle_no_sp_recast_min, AISpellVar.idle_no_sp_recast_max), false);

				LogSpells("Mob [{}] Min [{}] Max [{}]", GetCleanName(), AISpellVar.idle_no_sp_recast_min, AISpellVar.idle_no_sp_recast_max);

			}	//else, spell casting finishing will reset the timer.
		}	//else, spell casting finishing will reset the timer.
		return(true);
	}
	return(false);
}

void Mob::StartEnrage()
{
	// dont continue if already enraged
	if (bEnraged)
		return;

	if(!GetSpecialAbility(SpecialAbility::Enrage))
		return;

	int hp_ratio = GetSpecialAbilityParam(SpecialAbility::Enrage, 0);
	hp_ratio = hp_ratio > 0 ? hp_ratio : RuleI(NPC, StartEnrageValue);
	if(GetHPRatio() > static_cast<float>(hp_ratio)) {
		return;
	}

	if(RuleB(NPC, LiveLikeEnrage) && !((IsPet() && !IsCharmed() && GetOwner() && GetOwner()->IsOfClientBot()) ||
		(CastToNPC()->GetSwarmOwner() && entity_list.GetMob(CastToNPC()->GetSwarmOwner())->IsOfClientBot()))) {
		return;
	}

	Timer *timer = GetSpecialAbilityTimer(SpecialAbility::Enrage);
	if (timer && !timer->Check())
		return;

	int enraged_duration = GetSpecialAbilityParam(SpecialAbility::Enrage, 1);
	enraged_duration = enraged_duration > 0 ? enraged_duration : EnragedDurationTimer;
	StartSpecialAbilityTimer(SpecialAbility::Enrage, enraged_duration);

	// start the timer. need to call IsEnraged frequently since we dont have callback timers :-/
	bEnraged = true;
	entity_list.MessageCloseString(this, true, 200, Chat::NPCEnrage, NPC_ENRAGE_START, GetCleanName());
}

void Mob::ProcessEnrage(){
	if(IsEnraged()){
		Timer *timer = GetSpecialAbilityTimer(SpecialAbility::Enrage);
		if(timer && timer->Check()){
			entity_list.MessageCloseString(this, true, 200, Chat::NPCEnrage, NPC_ENRAGE_END, GetCleanName());

			int enraged_cooldown = GetSpecialAbilityParam(SpecialAbility::Enrage, 2);
			enraged_cooldown = enraged_cooldown > 0 ? enraged_cooldown : EnragedTimer;
			StartSpecialAbilityTimer(SpecialAbility::Enrage, enraged_cooldown);
			bEnraged = false;
		}
	}
}

bool Mob::IsEnraged()
{
	return bEnraged;
}

bool Mob::Flurry(ExtraAttackOptions *opts)
{
	// this is wrong, flurry is extra attacks on the current target
	Mob *target = GetTarget();
	if (target) {
		if (IsPet() || IsTempPet() || IsCharmed() || IsAnimation()) {
			entity_list.MessageCloseString(
				this,
				true,
				200,
				Chat::PetFlurry,
				NPC_FLURRY,
				GetCleanName(),
				target->GetCleanName());
		} else {
			entity_list.MessageCloseString(
				this,
				true,
				200,
				Chat::NPCFlurry,
				NPC_FLURRY,
				GetCleanName(),
				target->GetCleanName());
		}

		int num_attacks = GetSpecialAbilityParam(SpecialAbility::Flurry, 1);
		num_attacks = num_attacks > 0 ? num_attacks : RuleI(Combat, MaxFlurryHits);
		for (int i = 0; i < num_attacks; i++)
			Attack(target, EQ::invslot::slotPrimary, false, false, false, opts);
	}
	return true;
}

bool Mob::AddRampage(Mob *mob)
{
	if (!mob) {
		return false;
	}

	if (!GetSpecialAbility(SpecialAbility::Rampage)) {
		return false;
	}

	for (int i = 0; i < RampageArray.size(); i++) {
		// if Entity ID is already on the list don't add it again
		if (mob->GetID() == RampageArray[i]) {
			return false;
		}
	}
	RampageArray.push_back(mob->GetID());
	return true;
}

void Mob::ClearRampage()
{
	RampageArray.clear();
}

void Mob::RemoveFromRampageList(Mob* mob, bool remove_feigned)
{
	if (!mob) {
		return;
	}

	if (
		IsNPC() &&
		GetSpecialAbility(SpecialAbility::Rampage) &&
		(
			remove_feigned  ||
			mob->IsNPC() ||
			(
				mob->IsClient() &&
				!mob->CastToClient()->GetFeigned()
			)
		)
	) {
		for (int i = 0; i < RampageArray.size(); i++) {
			if (mob->GetID() == RampageArray[i]) {
				RampageArray[i] = 0;
			}
		}
	}
}

bool Mob::Rampage(ExtraAttackOptions *opts)
{
	int index_hit = 0;
	if (IsPet() || IsTempPet() || IsCharmed() || IsAnimation()){
		entity_list.MessageCloseString(this, true, 200, Chat::PetFlurry, NPC_RAMPAGE, GetCleanName());
	} else {
		entity_list.MessageCloseString(this, true, 200, Chat::NPCRampage, NPC_RAMPAGE, GetCleanName());
	}

	int rampage_targets = GetSpecialAbilityParam(SpecialAbility::Rampage, 1);

	if (rampage_targets == 0) { // if set to 0 or not set in the DB
		rampage_targets = RuleI(Combat, DefaultRampageTargets);
	}

	if (rampage_targets > RuleI(Combat, MaxRampageTargets)) {
		rampage_targets = RuleI(Combat, MaxRampageTargets);
	}

	m_specialattacks = eSpecialAttacks::Rampage;
	for (int i = 0; i < RampageArray.size(); i++) {
		if (index_hit >= rampage_targets) {
			break;
		}
		// range is important
		Mob *m_target = entity_list.GetMob(RampageArray[i]);
		if (m_target) {
			if (m_target == GetTarget()) {
				continue;
			}

			if (m_target->IsCorpse()) {
				LogAggroDetail("[{}] is on [{}]'s rampage list", m_target->GetCleanName(), GetCleanName());
				RemoveFromRampageList(m_target, true);
				continue;
			}

			if (DistanceSquaredNoZ(GetPosition(), m_target->GetPosition()) <= NPC_RAMPAGE_RANGE2) {
				ProcessAttackRounds(m_target, opts, true);
				index_hit++;
			}
		}
	}

	if (RuleB(Combat, RampageHitsTarget)) {
		if (index_hit < rampage_targets)
			ProcessAttackRounds(GetTarget(), opts, true);
	} else { // let's do correct behavior here, if they set above rule we can assume they want non-live like behavior
		if (index_hit < rampage_targets) {
			// so we go over in reverse order and skip range check
			// lets do it this way to still support non-live-like >1 rampage targets
			// likely live is just a fall through of the last valid mob
			for (auto i = RampageArray.crbegin(); i != RampageArray.crend(); ++i) {
				if (index_hit >= rampage_targets) {
					break;
				}
				auto m_target = entity_list.GetMob(*i);
				if (m_target) {
					if (m_target == GetTarget()) {
						continue;
					}
					ProcessAttackRounds(m_target, opts, true);
					index_hit++;
				}
			}
		}
	}

	m_specialattacks = eSpecialAttacks::None;

	return true;
}

void Mob::AreaRampage(ExtraAttackOptions *opts)
{
	int index_hit = 0;
	if (IsPet() || IsTempPet() || IsCharmed() || IsAnimation()) { // do not know every pet AA so thought it safer to add this
		entity_list.MessageCloseString(this, true, 200, Chat::PetFlurry, AE_RAMPAGE, GetCleanName());
	} else {
		entity_list.MessageCloseString(this, true, 200, Chat::NPCRampage, AE_RAMPAGE, GetCleanName());
	}

	int rampage_targets = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 1);
	rampage_targets = rampage_targets > 0 ? rampage_targets : -1;
	m_specialattacks = eSpecialAttacks::AERampage;
	index_hit = hate_list.AreaRampage(this, GetTarget(), rampage_targets, opts);

	m_specialattacks = eSpecialAttacks::None;
}

uint32 Mob::GetLevelCon(uint8 mylevel, uint8 iOtherLevel) {

	uint32 conlevel = 0;

	if (RuleB(Character, UseOldConSystem))
	{
		int16 diff = iOtherLevel - mylevel;

		if (diff == 0)
			return ConsiderColor::White;
		else if (diff >= 1 && diff <= 2)
			return ConsiderColor::Yellow;
		else if (diff >= 3)
			return ConsiderColor::Red;

		if (mylevel <= 8)
		{
			if (diff <= -4)
				conlevel = ConsiderColor::Gray;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 9)
		{
			if (diff <= -6)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -4)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 13)
		{
			if (diff <= -7)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -5)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 15)
		{
			if (diff <= -7)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -5)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 17)
		{
			if (diff <= -8)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -6)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 21)
		{
			if (diff <= -9)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -7)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 25)
		{
			if (diff <= -10)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -8)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 29)
		{
			if (diff <= -11)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -9)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 31)
		{
			if (diff <= -12)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -9)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 33)
		{
			if (diff <= -13)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -10)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 37)
		{
			if (diff <= -14)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -11)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 41)
		{
			if (diff <= -16)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -12)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 45)
		{
			if (diff <= -17)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -13)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 49)
		{
			if (diff <= -18)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -14)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 53)
		{
			if (diff <= -19)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -15)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else if (mylevel <= 55)
		{
			if (diff <= -20)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -15)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else
		{
			if (diff <= -21)
				conlevel = ConsiderColor::Gray;
			else if (diff <= -16)
				conlevel = ConsiderColor::LightBlue;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
	}
	else
	{
		int16 diff = iOtherLevel - mylevel;
		uint32 conGrayLvl = mylevel - (int32)((mylevel + 5) / 3);
		uint32 conGreenLvl = mylevel - (int32)((mylevel + 7) / 4);

		if (diff == 0)
			return ConsiderColor::White;
		else if (diff >= 1 && diff <= 3)
			return ConsiderColor::Yellow;
		else if (diff >= 4)
			return ConsiderColor::Red;

		if (mylevel <= 15)
		{
			if (diff <= -6)
				conlevel = ConsiderColor::Gray;
			else
				conlevel = ConsiderColor::DarkBlue;
		}
		else
			if (mylevel <= 20)
			{
				if (iOtherLevel <= conGrayLvl)
					conlevel = ConsiderColor::Gray;
				else
					if (iOtherLevel <= conGreenLvl)
						conlevel = ConsiderColor::Green;
					else
						conlevel = ConsiderColor::DarkBlue;
			}
			else
			{
				if (iOtherLevel <= conGrayLvl)
					conlevel = ConsiderColor::Gray;
				else
					if (iOtherLevel <= conGreenLvl)
						conlevel = ConsiderColor::Green;
					else
						if (diff <= -6)
							conlevel = ConsiderColor::LightBlue;
						else
							conlevel = ConsiderColor::DarkBlue;
			}
	}
	return conlevel;
}

void NPC::CheckSignal() {
	if (!signal_q.empty()) {
		int signal_id = signal_q.front();
		signal_q.pop_front();

		if (parse->HasQuestSub(GetNPCTypeID(), EVENT_SIGNAL)) {
			parse->EventNPC(EVENT_SIGNAL, this, nullptr, std::to_string(signal_id), 0);
		}
	}
}

bool IsSpellInList(DBnpcspells_Struct* spell_list, uint16 iSpellID);
bool IsSpellEffectInList(DBnpcspellseffects_Struct* spelleffect_list, uint16 iSpellEffectID, int32 base_value, int32 limit, int32 max_value);

bool NPC::AI_AddNPCSpells(uint32 iDBSpellsID) {
	// ok, this function should load the list, and the parent list then shove them into the struct and sort
	npc_spells_id = iDBSpellsID;
	AIspells.clear();
	if (iDBSpellsID == 0) {
		AIautocastspell_timer->Disable();
		return false;
	}
	DBnpcspells_Struct* spell_list = content_db.GetNPCSpells(iDBSpellsID);
	if (!spell_list) {
		AIautocastspell_timer->Disable();
		return false;
	}
	DBnpcspells_Struct* parentlist = content_db.GetNPCSpells(spell_list->parent_list);
	std::string debug_msg = StringFormat("Loading NPCSpells onto %s: dbspellsid=%u, level=%u", GetName(), iDBSpellsID, GetLevel());
	if (spell_list) {
		debug_msg.append(StringFormat(" (found, %u), parentlist=%u", spell_list->entries.size(), spell_list->parent_list));
		if (spell_list->parent_list) {
			if (parentlist)
				debug_msg.append(StringFormat(" (found, %u)", parentlist->entries.size()));
			else
				debug_msg.append(" (not found)");
		}
	}
	else {
		debug_msg.append(" (not found)");
	}
	LogAI("[{}]", debug_msg.c_str());

	if (parentlist) {
		for (const auto &iter : parentlist->entries) {
			LogAIDetail("([{}]) [{}]", iter.spellid, spells[iter.spellid].name);
		}
	}
	LogAI("fin (parent list)");
	if (spell_list) {
		for (const auto &iter : spell_list->entries) {
			LogAIDetail("([{}]) [{}]", iter.spellid, spells[iter.spellid].name);
		}
	}
	LogAI("fin (spell list)");

	uint16 attack_proc_spell = -1;
	int8 proc_chance = 3;
	uint16 range_proc_spell = -1;
	int16 rproc_chance = 0;
	uint16 defensive_proc_spell = -1;
	int16 dproc_chance = 0;
	uint32 _fail_recast = 0;
	uint32 _engaged_no_sp_recast_min = 0;
	uint32 _engaged_no_sp_recast_max = 0;
	uint8 _engaged_beneficial_self_chance = 0;
	uint8 _engaged_beneficial_other_chance = 0;
	uint8 _engaged_detrimental_chance = 0;
	uint32 _pursue_no_sp_recast_min = 0;
	uint32 _pursue_no_sp_recast_max = 0;
	uint8 _pursue_detrimental_chance = 0;
	uint32 _idle_no_sp_recast_min = 0;
	uint32 _idle_no_sp_recast_max = 0;
	uint8 _idle_beneficial_chance = 0;

	if (parentlist) {
		attack_proc_spell = parentlist->attack_proc;
		proc_chance = parentlist->proc_chance;
		range_proc_spell = parentlist->range_proc;
		rproc_chance = parentlist->rproc_chance;
		defensive_proc_spell = parentlist->defensive_proc;
		dproc_chance = parentlist->dproc_chance;
		_fail_recast = parentlist->fail_recast;
		_engaged_no_sp_recast_min = parentlist->engaged_no_sp_recast_min;
		_engaged_no_sp_recast_max = parentlist->engaged_no_sp_recast_max;
		_engaged_beneficial_self_chance = parentlist->engaged_beneficial_self_chance;
		_engaged_beneficial_other_chance = parentlist->engaged_beneficial_other_chance;
		_engaged_detrimental_chance = parentlist->engaged_detrimental_chance;
		_pursue_no_sp_recast_min = parentlist->pursue_no_sp_recast_min;
		_pursue_no_sp_recast_max = parentlist->pursue_no_sp_recast_max;
		_pursue_detrimental_chance = parentlist->pursue_detrimental_chance;
		_idle_no_sp_recast_min = parentlist->idle_no_sp_recast_min;
		_idle_no_sp_recast_max = parentlist->idle_no_sp_recast_max;
		_idle_beneficial_chance = parentlist->idle_beneficial_chance;
		for (auto &e : parentlist->entries) {
			if (GetLevel() >= e.minlevel && GetLevel() <= e.maxlevel && e.spellid > 0) {
				if (!IsSpellInList(spell_list, e.spellid))
				{
					AddSpellToNPCList(e.priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.min_hp, e.max_hp);
				}
			}
		}
	}
	if (spell_list->attack_proc >= 0) {
		attack_proc_spell = spell_list->attack_proc;
		proc_chance = spell_list->proc_chance;
	}

	if (spell_list->range_proc >= 0) {
		range_proc_spell = spell_list->range_proc;
		rproc_chance = spell_list->rproc_chance;
	}

	if (spell_list->defensive_proc >= 0) {
		defensive_proc_spell = spell_list->defensive_proc;
		dproc_chance = spell_list->dproc_chance;
	}

	//If any casting variables are defined in the current list, ignore those in the parent list.
	if (spell_list->fail_recast || spell_list->engaged_no_sp_recast_min || spell_list->engaged_no_sp_recast_max
		|| spell_list->engaged_beneficial_self_chance || spell_list->engaged_beneficial_other_chance || spell_list->engaged_detrimental_chance
		|| spell_list->pursue_no_sp_recast_min || spell_list->pursue_no_sp_recast_max || spell_list->pursue_detrimental_chance
		|| spell_list->idle_no_sp_recast_min || spell_list->idle_no_sp_recast_max || spell_list->idle_beneficial_chance) {
		_fail_recast = spell_list->fail_recast;
		_engaged_no_sp_recast_min = spell_list->engaged_no_sp_recast_min;
		_engaged_no_sp_recast_max = spell_list->engaged_no_sp_recast_max;
		_engaged_beneficial_self_chance = spell_list->engaged_beneficial_self_chance;
		_engaged_beneficial_other_chance = spell_list->engaged_beneficial_other_chance;
		_engaged_detrimental_chance = spell_list->engaged_detrimental_chance;
		_pursue_no_sp_recast_min = spell_list->pursue_no_sp_recast_min;
		_pursue_no_sp_recast_max = spell_list->pursue_no_sp_recast_max;
		_pursue_detrimental_chance = spell_list->pursue_detrimental_chance;
		_idle_no_sp_recast_min = spell_list->idle_no_sp_recast_min;
		_idle_no_sp_recast_max = spell_list->idle_no_sp_recast_max;
		_idle_beneficial_chance = spell_list->idle_beneficial_chance;
	}

	for (auto &e : spell_list->entries) {
		if (GetLevel() >= e.minlevel && GetLevel() <= e.maxlevel && e.spellid > 0) {
			AddSpellToNPCList(e.priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.min_hp, e.max_hp);
		}
	}

	std::sort(AIspells.begin(), AIspells.end(), [](const AISpells_Struct& a, const AISpells_Struct& b) {
		return a.priority > b.priority;
	});

	if (IsValidSpell(attack_proc_spell)) {
		AddProcToWeapon(attack_proc_spell, true, proc_chance);

		if(RuleB(Spells, NPCInnateProcOverride))
			innate_proc_spell_id = attack_proc_spell;
	}

	if (IsValidSpell(range_proc_spell))
		AddRangedProc(range_proc_spell, (rproc_chance + 100));

	if (IsValidSpell(defensive_proc_spell))
		AddDefensiveProc(defensive_proc_spell, (dproc_chance + 100));

	//Set AI casting variables

	AISpellVar.fail_recast = (_fail_recast) ? _fail_recast : RuleI(Spells, AI_SpellCastFinishedFailRecast);
	AISpellVar.engaged_no_sp_recast_min = (_engaged_no_sp_recast_min) ? _engaged_no_sp_recast_min : RuleI(Spells, AI_EngagedNoSpellMinRecast);
	AISpellVar.engaged_no_sp_recast_max = (_engaged_no_sp_recast_max) ? _engaged_no_sp_recast_max : RuleI(Spells, AI_EngagedNoSpellMaxRecast);
	AISpellVar.engaged_beneficial_self_chance = (_engaged_beneficial_self_chance) ? _engaged_beneficial_self_chance : RuleI(Spells, AI_EngagedBeneficialSelfChance);
	AISpellVar.engaged_beneficial_other_chance = (_engaged_beneficial_other_chance) ? _engaged_beneficial_other_chance : RuleI(Spells, AI_EngagedBeneficialOtherChance);
	AISpellVar.engaged_detrimental_chance = (_engaged_detrimental_chance) ? _engaged_detrimental_chance : RuleI(Spells, AI_EngagedDetrimentalChance);
	AISpellVar.pursue_no_sp_recast_min = (_pursue_no_sp_recast_min) ? _pursue_no_sp_recast_min : RuleI(Spells, AI_PursueNoSpellMinRecast);
	AISpellVar.pursue_no_sp_recast_max = (_pursue_no_sp_recast_max) ? _pursue_no_sp_recast_max : RuleI(Spells, AI_PursueNoSpellMaxRecast);
	AISpellVar.pursue_detrimental_chance = (_pursue_detrimental_chance) ? _pursue_detrimental_chance : RuleI(Spells, AI_PursueDetrimentalChance);
	AISpellVar.idle_no_sp_recast_min = (_idle_no_sp_recast_min) ? _idle_no_sp_recast_min : RuleI(Spells, AI_IdleNoSpellMinRecast);
	AISpellVar.idle_no_sp_recast_max = (_idle_no_sp_recast_max) ? _idle_no_sp_recast_max : RuleI(Spells, AI_IdleNoSpellMaxRecast);
	AISpellVar.idle_beneficial_chance = (_idle_beneficial_chance) ? _idle_beneficial_chance : RuleI(Spells, AI_IdleBeneficialChance);

	if (AIspells.empty())
		AIautocastspell_timer->Disable();
	else
		AIautocastspell_timer->Trigger();
	return true;
}

bool NPC::AI_AddNPCSpellsEffects(uint32 iDBSpellsEffectsID) {

	npc_spells_effects_id = iDBSpellsEffectsID;
	AIspellsEffects.clear();

	if (iDBSpellsEffectsID == 0)
		return false;

	DBnpcspellseffects_Struct* spell_effects_list = content_db.GetNPCSpellsEffects(iDBSpellsEffectsID);

	if (!spell_effects_list) {
		return false;
	}

	DBnpcspellseffects_Struct* parentlist = content_db.GetNPCSpellsEffects(spell_effects_list->parent_list);

	uint32 i;
	std::string debug_msg = StringFormat("Loading NPCSpellsEffects onto %s: dbspellseffectid=%u", GetName(), iDBSpellsEffectsID);
	if (spell_effects_list) {
		debug_msg.append(StringFormat(" (found, %u), parentlist=%u", spell_effects_list->numentries, spell_effects_list->parent_list));
		if (spell_effects_list->parent_list) {
			if (parentlist)
				debug_msg.append(StringFormat(" (found, %u)", parentlist->numentries));
			else
				debug_msg.append(" (not found)");
		}
	}
	else {
		debug_msg.append(" (not found)");
	}
	LogAI("[{}]", debug_msg.c_str());

	if (parentlist) {
		for (i=0; i<parentlist->numentries; i++) {
			if (GetLevel() >= parentlist->entries[i].minlevel && GetLevel() <= parentlist->entries[i].maxlevel && parentlist->entries[i].spelleffectid > 0) {
				if (!IsSpellEffectInList(spell_effects_list, parentlist->entries[i].spelleffectid, parentlist->entries[i].base_value,
					parentlist->entries[i].limit, parentlist->entries[i].max_value))
				{
				AddSpellEffectToNPCList(parentlist->entries[i].spelleffectid,
						parentlist->entries[i].base_value, parentlist->entries[i].limit,
						parentlist->entries[i].max_value);
				}
			}
		}
	}

	for (i=0; i<spell_effects_list->numentries; i++) {
		if (GetLevel() >= spell_effects_list->entries[i].minlevel && GetLevel() <= spell_effects_list->entries[i].maxlevel && spell_effects_list->entries[i].spelleffectid > 0) {
			AddSpellEffectToNPCList(spell_effects_list->entries[i].spelleffectid,
				spell_effects_list->entries[i].base_value, spell_effects_list->entries[i].limit,
				spell_effects_list->entries[i].max_value);
		}
	}

	return true;
}

void NPC::ApplyAISpellEffects(StatBonuses* newbon)
{
	if (!AI_HasSpellsEffects())
		return;

	for (int i = 0; i < AIspellsEffects.size(); i++)
		ApplySpellsBonuses(0, 0, newbon, 0, 0, 0, -1, 10, true, AIspellsEffects[i].spelleffectid,
				   AIspellsEffects[i].base_value, AIspellsEffects[i].limit, AIspellsEffects[i].max_value);

	return;
}

// adds a spell to the list, taking into account priority and resorting list as needed.
void NPC::AddSpellEffectToNPCList(uint16 iSpellEffectID, int32 base_value, int32 limit, int32 max_value, bool apply_bonus)
{
	if(!iSpellEffectID)
		return;

	HasAISpellEffects = true;
	AISpellsEffects_Struct t;

	t.spelleffectid = iSpellEffectID;
	t.base_value = base_value;
	t.limit = limit;
	t.max_value = max_value;
	AIspellsEffects.push_back(t);

	//we recalculate if applied from quest script.
	if (apply_bonus) {
		CalcBonuses();
	}
}

void NPC::RemoveSpellEffectFromNPCList(uint16 iSpellEffectID, bool apply_bonus)
{
	auto iter = AIspellsEffects.begin();
	while (iter != AIspellsEffects.end())
	{
		if ((*iter).spelleffectid == iSpellEffectID)
		{
			iter = AIspellsEffects.erase(iter);
			continue;
		}
		++iter;
	}

	if (apply_bonus) {
		CalcBonuses();
	}
}

bool NPC::HasAISpellEffect(uint16 spell_effect_id)
{
	for (const auto& spell_effect : AIspellsEffects) {
		if (spell_effect.spelleffectid == spell_effect_id) {
			return true;
		}
	}

	return false;
}

bool IsSpellEffectInList(DBnpcspellseffects_Struct* spelleffect_list, uint16 iSpellEffectID, int32 base_value, int32 limit, int32 max_value) {
	for (uint32 i=0; i < spelleffect_list->numentries; i++) {
		if (spelleffect_list->entries[i].spelleffectid == iSpellEffectID &&  spelleffect_list->entries[i].base_value == base_value
			&& spelleffect_list->entries[i].limit == limit && spelleffect_list->entries[i].max_value == max_value)
			return true;
	}
	return false;
}

bool IsSpellInList(DBnpcspells_Struct* spell_list, uint16 iSpellID) {
	auto it = std::find_if(spell_list->entries.begin(), spell_list->entries.end(),
			       [iSpellID](const DBnpcspells_entries_Struct &a) { return a.spellid == iSpellID; });
	return it != spell_list->entries.end();
}

// adds a spell to the list, taking into account priority and resorting list as needed.
void NPC::AddSpellToNPCList(int16 iPriority, uint16 iSpellID, uint32 iType,
							int16 iManaCost, int32 iRecastDelay, int16 iResistAdjust, int8 min_hp, int8 max_hp)
{

	if(!IsValidSpell(iSpellID))
		return;

	HasAISpell = true;
	AISpells_Struct t;

	t.priority = iPriority;
	t.spellid = iSpellID;
	t.type = iType;
	t.manacost = iManaCost;
	t.recast_delay = iRecastDelay;
	t.time_cancast = 0;
	t.resist_adjust = iResistAdjust;
	t.min_hp = min_hp;
	t.max_hp = max_hp;

	AIspells.push_back(t);

	// If we're going from an empty list, we need to start the timer
	if (AIspells.size() == 1)
		AIautocastspell_timer->Start(RandomTimer(0, 300), false);
}

void NPC::RemoveSpellFromNPCList(uint16 spell_id)
{
	auto iter = AIspells.begin();
	while(iter != AIspells.end())
	{
		if((*iter).spellid == spell_id)
		{
			iter = AIspells.erase(iter);
			continue;
		}
		++iter;
	}
}

void NPC::AISpellsList(Client *c)
{
	if (!c) {
		return;
	}

	if (AIspells.size() > 0) {
		c->Message(
			Chat::White,
			fmt::format(
				"{} has {} AI spells.",
				GetCleanName(),
				AIspells.size()
			).c_str()
		);

		int spell_slot = 1;
		for (const auto& ai_spell : AIspells) {
			c->Message(
				Chat::White,
				fmt::format(
					"Spell {} | Name: {} ({}) Type: {} Mana Cost: {}",
					spell_slot,
					GetSpellName(ai_spell.spellid),
					ai_spell.spellid,
					ai_spell.type,
					ai_spell.manacost
				).c_str()
			);

			c->Message(
				Chat::White,
				fmt::format(
					"Spell {} | Priority: {} Recast Delay: {} Resist Difficulty: {}",
					spell_slot,
					ai_spell.priority,
					ai_spell.recast_delay,
					ai_spell.resist_adjust
				).c_str()
			);

			if (ai_spell.time_cancast) {
				c->Message(
					Chat::White,
					fmt::format(
						"Spell {} | Time Can Cast : {}",
						spell_slot,
						ai_spell.time_cancast
					).c_str()
				);
			}

			if (ai_spell.resist_adjust) {
				c->Message(
					Chat::White,
					fmt::format(
						"Spell {} | Resist Adjust: {}",
						spell_slot,
						ai_spell.resist_adjust
					).c_str()
				);
			}

			if (ai_spell.min_hp || ai_spell.max_hp) {
				c->Message(
					Chat::White,
					fmt::format(
						"Spell {} | Min HP: {} Max HP: {}",
						spell_slot,
						ai_spell.min_hp,
						ai_spell.max_hp
					).c_str()
				);
			}

			spell_slot++;
		}
	}
	else {
		c->Message(
			Chat::White,
			fmt::format(
				"{} has no AI spells.",
				GetCleanName()
			).c_str()
		);
	}

	return;
}

DBnpcspells_Struct *ZoneDatabase::GetNPCSpells(uint32 npc_spells_id)
{
	if (npc_spells_id == 0) {
		return nullptr;
	}

	auto it = npc_spells_cache.find(npc_spells_id);
	if (it != npc_spells_cache.end()) { // it's in the cache, easy =)
		return &it->second;
	}

	if (!npc_spells_loadtried.count(npc_spells_id)) { // no reason to ask the DB again if we have failed once already
		npc_spells_loadtried.insert(npc_spells_id);

		auto ns = NpcSpellsRepository::FindOne(*this, npc_spells_id);
		if (!ns.id) {
			return nullptr;
		}

		DBnpcspells_Struct ss;

		ss.parent_list                     = ns.parent_list;
		ss.attack_proc                     = ns.attack_proc;
		ss.proc_chance                     = ns.proc_chance;
		ss.range_proc                      = ns.range_proc;
		ss.rproc_chance                    = ns.rproc_chance;
		ss.defensive_proc                  = ns.defensive_proc;
		ss.dproc_chance                    = ns.dproc_chance;
		ss.fail_recast                     = ns.fail_recast;
		ss.engaged_no_sp_recast_min        = ns.engaged_no_sp_recast_min;
		ss.engaged_no_sp_recast_max        = ns.engaged_no_sp_recast_max;
		ss.engaged_beneficial_self_chance  = ns.engaged_b_self_chance;
		ss.engaged_beneficial_other_chance = ns.engaged_b_other_chance;
		ss.engaged_detrimental_chance      = ns.engaged_d_chance;
		ss.pursue_no_sp_recast_min         = ns.pursue_no_sp_recast_min;
		ss.pursue_no_sp_recast_max         = ns.pursue_no_sp_recast_max;
		ss.pursue_detrimental_chance       = ns.pursue_d_chance;
		ss.idle_no_sp_recast_min           = ns.idle_no_sp_recast_min;
		ss.idle_no_sp_recast_max           = ns.idle_no_sp_recast_max;
		ss.idle_beneficial_chance          = ns.idle_b_chance;

		auto entries = NpcSpellsEntriesRepository::GetWhere(
			*this,
			fmt::format(
				"npc_spells_id = {} {} ORDER BY minlevel",
				npc_spells_id,
				ContentFilterCriteria::apply()
			)
		);

		for (auto &e: entries) {
			DBnpcspells_entries_Struct se{};

			se.spellid      = e.spellid;
			se.type         = e.type;
			se.minlevel     = e.minlevel;
			se.maxlevel     = e.maxlevel;
			se.manacost     = e.manacost;
			se.recast_delay = e.recast_delay;
			se.priority     = e.priority;
			se.min_hp       = e.min_hp;
			se.max_hp       = e.max_hp;

			// some spell types don't make much since to be priority 0, so fix that
			if (!(se.type & SPELL_TYPES_INNATE) && se.priority == 0) {
				se.priority = 1;
			}

			if (e.resist_adjust) {
				se.resist_adjust = e.resist_adjust;
			}
			else if (IsValidSpell(e.spellid)) {
				se.resist_adjust = spells[e.spellid].resist_difficulty;
			}

			ss.entries.push_back(se);
		}

		npc_spells_cache.emplace(std::make_pair(npc_spells_id, ss));

		return &npc_spells_cache[npc_spells_id];
	}

	return nullptr;
}

DBnpcspellseffects_Struct *ZoneDatabase::GetNPCSpellsEffects(uint32 iDBSpellsEffectsID)
{
	if (iDBSpellsEffectsID == 0)
		return nullptr;

	if (!npc_spellseffects_cache) {
		npc_spellseffects_maxid = GetMaxNPCSpellsEffectsID();
		npc_spellseffects_cache = new DBnpcspellseffects_Struct *[npc_spellseffects_maxid + 1];
		npc_spellseffects_loadtried = new bool[npc_spellseffects_maxid + 1];
		for (uint32 i = 0; i <= npc_spellseffects_maxid; i++) {
			npc_spellseffects_cache[i] = nullptr;
			npc_spellseffects_loadtried[i] = false;
		}
	}

	if (iDBSpellsEffectsID > npc_spellseffects_maxid)
		return nullptr;

	if (npc_spellseffects_cache[iDBSpellsEffectsID]) // it's in the cache, easy =)
		return npc_spellseffects_cache[iDBSpellsEffectsID];

	if (npc_spellseffects_loadtried[iDBSpellsEffectsID])
		return nullptr;

	npc_spellseffects_loadtried[iDBSpellsEffectsID] = true;

	std::string query =
	    StringFormat("SELECT id, parent_list FROM npc_spells_effects WHERE id=%d", iDBSpellsEffectsID);
	auto results = QueryDatabase(query);
	if (!results.Success()) {
		return nullptr;
	}

	if (results.RowCount() != 1)
		return nullptr;

	auto row = results.begin();
	uint32 tmpparent_list = Strings::ToInt(row[1]);

	query = StringFormat("SELECT spell_effect_id, minlevel, "
			     "maxlevel,se_base, se_limit, se_max "
			     "FROM npc_spells_effects_entries "
			     "WHERE npc_spells_effects_id = %d ORDER BY minlevel",
			     iDBSpellsEffectsID);
	results = QueryDatabase(query);
	if (!results.Success())
		return nullptr;

	uint32 tmpSize =
	    sizeof(DBnpcspellseffects_Struct) + (sizeof(DBnpcspellseffects_entries_Struct) * results.RowCount());
	npc_spellseffects_cache[iDBSpellsEffectsID] = (DBnpcspellseffects_Struct *)new uchar[tmpSize];
	memset(npc_spellseffects_cache[iDBSpellsEffectsID], 0, tmpSize);
	npc_spellseffects_cache[iDBSpellsEffectsID]->parent_list = tmpparent_list;
	npc_spellseffects_cache[iDBSpellsEffectsID]->numentries = results.RowCount();

	int entryIndex = 0;
	for (row = results.begin(); row != results.end(); ++row, ++entryIndex) {
		int spell_effect_id = Strings::ToInt(row[0]);
		npc_spellseffects_cache[iDBSpellsEffectsID]->entries[entryIndex].spelleffectid = spell_effect_id;
		npc_spellseffects_cache[iDBSpellsEffectsID]->entries[entryIndex].minlevel = Strings::ToInt(row[1]);
		npc_spellseffects_cache[iDBSpellsEffectsID]->entries[entryIndex].maxlevel = Strings::ToInt(row[2]);
		npc_spellseffects_cache[iDBSpellsEffectsID]->entries[entryIndex].base_value = Strings::ToInt(row[3]);
		npc_spellseffects_cache[iDBSpellsEffectsID]->entries[entryIndex].limit = Strings::ToInt(row[4]);
		npc_spellseffects_cache[iDBSpellsEffectsID]->entries[entryIndex].max_value = Strings::ToInt(row[5]);
	}

	return npc_spellseffects_cache[iDBSpellsEffectsID];
}

uint32 ZoneDatabase::GetMaxNPCSpellsEffectsID() {

	std::string query = "SELECT max(id) FROM npc_spells_effects";
	auto results = QueryDatabase(query);
	if (!results.Success()) {
		return 0;
	}

    if (results.RowCount() != 1)
        return 0;

    auto row = results.begin();
    if (!row[0])
        return 0;

    return Strings::ToInt(row[0]);
}

