eqemu-server/zone/mob_ai.cpp
Knightly 7ab909ee47 Standardize Licensing
- License was intended to be GPLv3 per earlier commit of GPLv3 LICENSE FILE
- This is confirmed by the inclusion of libraries that are incompatible with GPLv2
- This is also confirmed by KLS and the agreement of KLS's predecessors
- Added GPLv3 license headers to the compilable source files
- Removed Folly licensing in strings.h since the string functions do not match the Folly functions and are standard functions - this must have been left over from previous implementations
- Removed individual contributor license headers since the project has been under the "developer" mantle for many years
- Removed comments on files that were previously automatically generated since they've been manually modified multiple times and there are no automatic scripts referencing them (removed in 2023)
2026-04-01 17:09:57 -07:00

2992 lines
88 KiB
C++

/* EQEmu: EQEmulator
Copyright (C) 2001-2026 EQEmu Development Team
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; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; 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, see <http://www.gnu.org/licenses/>.
*/
#include "mob.h"
#include "common/data_verification.h"
#include "common/features.h"
#include "common/repositories/criteria/content_filter_criteria.h"
#include "common/repositories/npc_spells_entries_repository.h"
#include "common/repositories/npc_spells_repository.h"
#include "common/rulesys.h"
#include "common/strings.h"
#include "zone/bot.h"
#include "zone/client.h"
#include "zone/entity.h"
#include "zone/fastmath.h"
#include "zone/map.h"
#include "zone/npc.h"
#include "zone/quest_parser_collection.h"
#include "zone/string_ids.h"
#include "zone/water_map.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]);
}