diff --git a/common/emu_constants.h b/common/emu_constants.h index 96806cc7d..06330f8dc 100644 --- a/common/emu_constants.h +++ b/common/emu_constants.h @@ -217,6 +217,25 @@ namespace EQ stanceBurnAE }; + enum BotSpellIDs : int { + Warrior = 3001, + Cleric, + Paladin, + Ranger, + Shadowknight, + Druid, + Monk, + Bard, + Rogue, + Shaman, + Necromancer, + Wizard, + Magician, + Enchanter, + Beastlord, + Berserker + }; + enum GravityBehavior : int8 { Ground, Flying, diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index debf05b4c..c1a1d5925 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -38,6 +38,11 @@ OutF(LogSys, Logs::General, Logs::AI, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogAIModerate(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::AI))\ + OutF(LogSys, Logs::Moderate, Logs::AI, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define LogAIDetail(message, ...) do {\ if (LogSys.IsLogEnabled(Logs::Detail, Logs::AI))\ OutF(LogSys, Logs::Detail, Logs::AI, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ diff --git a/zone/bot.cpp b/zone/bot.cpp index ac3f7d968..631ebbcd1 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -843,7 +843,7 @@ void Bot::GenerateBaseStats() int32 CorruptionResist = _baseCorrup; // pulling fixed values from an auto-increment field is dangerous... - switch(GetClass()) { + switch (GetClass()) { case WARRIOR: BotSpellID = 3001; Strength += 10; @@ -2173,6 +2173,47 @@ bool Bot::Process() return true; } +void Bot::AI_Bot_Start(uint32 iMoveDelay) { + Mob::AI_Start(iMoveDelay); + if (!pAIControlled) { + return; + } + + if (AIBot_spells.empty()) { + AIautocastspell_timer = std::make_unique(1000); + AIautocastspell_timer->Disable(); + } else { + AIautocastspell_timer = std::make_unique(500); + AIautocastspell_timer->Start(RandomTimer(0, 300), false); + } + + if (NPCTypedata) { + AI_AddBotSpells(NPCTypedata->npc_spells_id); + ProcessSpecialAbilities(NPCTypedata->special_abilities); + AI_AddNPCSpellsEffects(NPCTypedata->npc_spells_effects_id); + } + + SendTo(GetX(), GetY(), GetZ()); + SaveGuardSpot(GetPosition()); +} + +void Bot::AI_Bot_Init() +{ + AIautocastspell_timer.reset(nullptr); + casting_spell_AIindex = static_cast(AIBot_spells.size()); + + roambox_max_x = 0; + roambox_max_y = 0; + roambox_min_x = 0; + roambox_min_y = 0; + roambox_distance = 0; + roambox_destination_x = 0; + roambox_destination_y = 0; + roambox_destination_z = 0; + roambox_min_delay = 2500; + roambox_delay = 2500; +} + void Bot::SpellProcess() { if(spellend_timer.Check(false)) { NPC::SpellProcess(); @@ -2259,7 +2300,7 @@ void Bot::BotRangedAttack(Mob* other) { if (spellbonuses.NegateIfCombat) BuffFadeByEffect(SE_NegateIfCombat); - if(hidden || improved_hidden){ + if (hidden || improved_hidden) { hidden = false; improved_hidden = false; EQApplicationPacket* outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); @@ -5161,7 +5202,7 @@ bool Bot::Attack(Mob* other, int Hand, bool FromRiposte, bool IsStrikethrough, b #endif //Live AA - Sinister Strikes *Adds weapon damage bonus to offhand weapon. if (Hand == EQ::invslot::slotSecondary) { - if (aabonuses.SecondaryDmgInc || itembonuses.SecondaryDmgInc || spellbonuses.SecondaryDmgInc){ + if (aabonuses.SecondaryDmgInc || itembonuses.SecondaryDmgInc || spellbonuses.SecondaryDmgInc) { ucDamageBonus = GetWeaponDamageBonus(weapon ? weapon->GetItem() : (const EQ::ItemData*) nullptr); my_hit.min_damage = ucDamageBonus; hate += ucDamageBonus; @@ -6430,9 +6471,9 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { return; } - if(ka_time){ + if (ka_time) { - switch(GetClass()){ + switch (GetClass()) { case SHADOWKNIGHT: { CastSpell(SPELL_NPC_HARM_TOUCH, target->GetID()); knightattack_timer.Start(HarmTouchReuseTime * 1000); @@ -6530,9 +6571,9 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { int level = GetLevel(); int reuse = (TauntReuseTime * 1000); bool did_attack = false; - switch(GetClass()) { + switch (GetClass()) { case WARRIOR: - if(level >= RuleI(Combat, NPCBashKickLevel)){ + if (level >= RuleI(Combat, NPCBashKickLevel)) { bool canBash = false; if ((GetRace() == OGRE || GetRace() == TROLL || GetRace() == BARBARIAN) || (m_inv.GetItem(EQ::invslot::slotSecondary) && m_inv.GetItem(EQ::invslot::slotSecondary)->GetItem()->ItemType == EQ::item::ItemTypeShield) || (m_inv.GetItem(EQ::invslot::slotPrimary) && m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->IsType2HWeapon() && GetAA(aa2HandBash) >= 1)) canBash = true; @@ -6552,7 +6593,7 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { case CLERIC: case SHADOWKNIGHT: case PALADIN: - if(level >= RuleI(Combat, NPCBashKickLevel)){ + if (level >= RuleI(Combat, NPCBashKickLevel)) { if ((GetRace() == OGRE || GetRace() == TROLL || GetRace() == BARBARIAN) || (m_inv.GetItem(EQ::invslot::slotSecondary) && m_inv.GetItem(EQ::invslot::slotSecondary)->GetItem()->ItemType == EQ::item::ItemTypeShield) || (m_inv.GetItem(EQ::invslot::slotPrimary) && m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->IsType2HWeapon() && GetAA(aa2HandBash) >= 1)) skill_to_use = EQ::skills::SkillBash; } @@ -7006,7 +7047,7 @@ int64 Bot::GetActSpellDamage(uint16 spell_id, int64 value, Mob* target) { } else if (GetClass() == WIZARD || (IsMerc() && GetClass() == CASTERDPS)) { - if ((GetLevel() >= RuleI(Spells, WizCritLevel)) && zone->random.Roll(RuleI(Spells, WizCritChance))){ + if ((GetLevel() >= RuleI(Spells, WizCritLevel)) && zone->random.Roll(RuleI(Spells, WizCritChance))) { //Wizard innate critical chance is calculated seperately from spell effect and is not a set ratio. (20-70 is parse confirmed) ratio += zone->random.Int(20,70); Critical = true; @@ -7436,16 +7477,16 @@ bool Bot::CastSpell(uint16 spell_id, uint16 target_id, EQ::spells::CastingSlot s MessageString(Chat::White, MELEE_SILENCE); if(casting_spell_id) - AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); + AI_Bot_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); return false; } } - if(IsDetrimentalSpell(spell_id) && !zone->CanDoCombat()){ + if (IsDetrimentalSpell(spell_id) && !zone->CanDoCombat()) { MessageString(Chat::White, SPELL_WOULDNT_HOLD); if(casting_spell_id) - AI_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); + AI_Bot_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); return false; } @@ -7733,7 +7774,7 @@ bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, EQ::spe if(IsGrouped() && (spellTarget->IsBot() || spellTarget->IsClient()) && RuleB(Bots, GroupBuffing)) { bool noGroupSpell = false; uint16 thespell = spell_id; - for(int i = 0; i < AIspells.size(); i++) { + for (int i = 0; i < AIBot_spells.size(); i++) { int j = BotGetSpells(i); int spelltype = BotGetSpellType(i); bool spellequal = (j == thespell); @@ -7833,7 +7874,7 @@ void Bot::CalcBonuses() { end_regen = CalcEnduranceRegen(); } -int64 Bot::CalcHPRegenCap(){ +int64 Bot::CalcHPRegenCap() { int level = GetLevel(); int64 hpregen_cap = 0; hpregen_cap = (RuleI(Character, ItemHealthRegenCap) + itembonuses.HeroicSTA / 25); @@ -7841,7 +7882,7 @@ int64 Bot::CalcHPRegenCap(){ return (hpregen_cap * RuleI(Character, HPRegenMultiplier) / 100); } -int64 Bot::CalcManaRegenCap(){ +int64 Bot::CalcManaRegenCap() { int64 cap = RuleI(Character, ItemManaRegenCap) + aabonuses.ItemManaRegenCap; switch(GetCasterClass()) { case 'I': @@ -8278,7 +8319,7 @@ int64 Bot::CalcManaRegen() { uint64 Bot::GetClassHPFactor() { uint32 factor; - switch(GetClass()) { + switch (GetClass()) { case BEASTLORD: case BERSERKER: case MONK: @@ -9101,7 +9142,7 @@ void Bot::AddItemBonuses(const EQ::ItemInstance *inst, StatBonuses* newbon, bool } } - if (item->SkillModValue != 0 && item->SkillModType <= EQ::skills::HIGHEST_SKILL){ + if (item->SkillModValue != 0 && item->SkillModType <= EQ::skills::HIGHEST_SKILL) { if ((item->SkillModValue > 0 && newbon->skillmod[item->SkillModType] < item->SkillModValue) || (item->SkillModValue < 0 && newbon->skillmod[item->SkillModType] > item->SkillModValue)) { @@ -9194,7 +9235,7 @@ void Bot::CalcBotStats(bool showtext) { CalcBonuses(); - AI_AddNPCSpells(GetBotSpellID()); + AI_AddBotSpells(GetBotSpellID()); if(showtext) { GetBotOwner()->Message(Chat::Yellow, "%s has been updated.", GetCleanName()); diff --git a/zone/bot.h b/zone/bot.h index 1f1bd1724..650c2e9a0 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -174,9 +174,9 @@ public: virtual bool Save(); virtual void Depop(); void CalcBotStats(bool showtext = true); - uint16 BotGetSpells(int spellslot) { return AIspells[spellslot].spellid; } - uint32 BotGetSpellType(int spellslot) { return AIspells[spellslot].type; } - uint16 BotGetSpellPriority(int spellslot) { return AIspells[spellslot].priority; } + uint16 BotGetSpells(int spellslot) { return AIBot_spells[spellslot].spellid; } + uint32 BotGetSpellType(int spellslot) { return AIBot_spells[spellslot].type; } + uint16 BotGetSpellPriority(int spellslot) { return AIBot_spells[spellslot].priority; } virtual float GetProcChances(float ProcBonus, uint16 hand); virtual int GetHandToHandDamage(void); virtual bool TryFinishingBlow(Mob *defender, int64 &damage); @@ -220,7 +220,6 @@ public: virtual void AddToHateList(Mob* other, int64 hate = 0, int64 damage = 0, bool iYellForHelp = true, bool bFrenzy = false, bool iBuffTic = false, bool pet_command = false); virtual void SetTarget(Mob* mob); virtual void Zone(); - std::vector GetBotSpells() { return AIspells; } bool IsArcheryRange(Mob* target); void ChangeBotArcherWeapons(bool isArcher); void Sit(); @@ -308,6 +307,9 @@ public: void DoEnduranceRegen(); //This Regenerates endurance void DoEnduranceUpkeep(); //does the endurance upkeep + bool AI_AddBotSpells(uint32 iDBSpellsID); + void AddSpellToBotList(int16 iPriority, uint16 iSpellID, uint32 iType, int16 iManaCost, int32 iRecastDelay, int16 iResistAdjust, int8 min_hp, int8 max_hp); + void AI_Bot_Event_SpellCastFinished(bool iCastSucceeded, uint16 slot); // AI Methods virtual bool AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes); virtual bool AI_EngagedCastCheck(); @@ -321,6 +323,10 @@ public: void SetGuardMode(); void SetHoldMode(); + // Bot AI Methods + void AI_Bot_Init(); + void AI_Bot_Start(uint32 iMoveDelay = 0); + // Mob AI Virtual Override Methods virtual void AI_Process(); virtual void AI_Stop(); @@ -637,7 +643,6 @@ private: // Class Members uint32 _botID; uint32 _botOwnerCharacterID; - //uint32 _botSpellID; bool _spawnStatus; Mob* _botOwner; bool _botOrderAttack; @@ -744,6 +749,8 @@ private: static uint8 spell_casting_chances[SPELL_TYPE_COUNT][PLAYER_CLASS_COUNT][EQ::constants::STANCE_TYPE_COUNT][cntHSND]; }; +bool IsSpellInBotList(DBbotspells_Struct* spell_list, uint16 iSpellID); + #endif // BOTS #endif // BOT_H diff --git a/zone/botspellsai.cpp b/zone/botspellsai.cpp index d52ef2b38..b9a8c93ec 100644 --- a/zone/botspellsai.cpp +++ b/zone/botspellsai.cpp @@ -39,7 +39,7 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { return false; if (iChance < 100) { - if (zone->random.Int(0, 100) > iChance){ + if (zone->random.Int(0, 100) > iChance) { return false; } } @@ -87,13 +87,14 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { Mob* addMob = GetFirstIncomingMobToMez(this, botSpell); - if(!addMob){ + if (!addMob) { //Say("!addMob."); - break;} - - if(!(!addMob->IsImmuneToSpell(botSpell.SpellId, this) && addMob->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) break; + } + if (!(!addMob->IsImmuneToSpell(botSpell.SpellId, this) && addMob->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) { + break; + } castedSpell = AIDoSpellCast(botSpell.SpellIndex, addMob, botSpell.ManaCost); if(castedSpell) @@ -135,58 +136,65 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { if(hpr < 35) { botSpell = GetBestBotSpellForFastHeal(this); } - else if(hpr >= 35 && hpr < 70){ - if(GetNumberNeedingHealedInGroup(60, false) >= 3) + else if (hpr >= 35 && hpr < 70) { + if (GetNumberNeedingHealedInGroup(60, false) >= 3) { botSpell = GetBestBotSpellForGroupHeal(this); - - if(botSpell.SpellId == 0) + } + if (botSpell.SpellId == 0) { botSpell = GetBestBotSpellForPercentageHeal(this); + } } - else if(hpr >= 70 && hpr < 95){ - if(GetNumberNeedingHealedInGroup(80, false) >= 3) + else if (hpr >= 70 && hpr < 95) { + if (GetNumberNeedingHealedInGroup(80, false) >= 3) { botSpell = GetBestBotSpellForGroupHealOverTime(this); - - if(hasAggro) + } + if (hasAggro) { botSpell = GetBestBotSpellForPercentageHeal(this); + } } else { - if(!tar->FindType(SE_HealOverTime)) + if (!tar->FindType(SE_HealOverTime)) { botSpell = GetBestBotSpellForHealOverTime(this); + } } } else if ((botClass == CLERIC) || (botClass == DRUID) || (botClass == SHAMAN) || (botClass == PALADIN)) { - if(GetNumberNeedingHealedInGroup(40, true) >= 2){ + if (GetNumberNeedingHealedInGroup(40, true) >= 2) { botSpell = GetBestBotSpellForGroupCompleteHeal(this); - if(botSpell.SpellId == 0) + if (botSpell.SpellId == 0) { botSpell = GetBestBotSpellForGroupHeal(this); - - if(botSpell.SpellId == 0) + } + if (botSpell.SpellId == 0) { botSpell = GetBestBotSpellForGroupHealOverTime(this); - - if(hpr < 40) { - if(botSpell.SpellId == 0) + } + if (hpr < 40) { + if (botSpell.SpellId == 0) { botSpell = GetBestBotSpellForPercentageHeal(this); + } } } - else if(GetNumberNeedingHealedInGroup(60, true) >= 2){ + else if (GetNumberNeedingHealedInGroup(60, true) >= 2) { botSpell = GetBestBotSpellForGroupHeal(this); - if(botSpell.SpellId == 0) + if (botSpell.SpellId == 0) { botSpell = GetBestBotSpellForGroupHealOverTime(this); - - if(hpr < 40) { + } + if (hpr < 40) { if(botSpell.SpellId == 0) botSpell = GetBestBotSpellForPercentageHeal(this); } } - else if(hpr < 40) + else if (hpr < 40) { botSpell = GetBestBotSpellForPercentageHeal(this); - else if(hpr >= 40 && hpr < 75) + } + else if (hpr >= 40 && hpr < 75) { botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + } else { - if(hpr < 90 && !tar->FindType(SE_HealOverTime)) + if (hpr < 90 && !tar->FindType(SE_HealOverTime)) { botSpell = GetBestBotSpellForHealOverTime(this); + } } } else { @@ -213,17 +221,18 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { } //If we're at specified mana % or below, don't heal as hybrid - if(tar->GetHPRatio() <= hpRatioToCast) + if (tar->GetHPRatio() <= hpRatioToCast) { botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + } } - if(botSpell.SpellId == 0) + if (botSpell.SpellId == 0) { botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); - - if(botSpell.SpellId == 0) + } + if (botSpell.SpellId == 0) { botSpell = GetFirstBotSpellForSingleTargetHeal(this); - - if(botSpell.SpellId == 0 && botClass == BARD){ + } + if (botSpell.SpellId == 0 && botClass == BARD) { botSpell = GetFirstBotSpellBySpellType(this, SpellType_Heal); } @@ -257,8 +266,8 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { } }*/ if(botClass != BARD) { - if(IsGroupSpell(botSpell.SpellId)){ - if(HasGroup()) { + if (IsGroupSpell(botSpell.SpellId)) { + if (HasGroup()) { Group *g = GetGroup(); if(g) { @@ -345,14 +354,14 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { } // Put the zone levitate and movement check here since bots are able to bypass the client casting check - if((IsEffectInSpell(selectedBotSpell.SpellId, SE_Levitate) && !zone->CanLevitate()) + if ((IsEffectInSpell(selectedBotSpell.SpellId, SE_Levitate) && !zone->CanLevitate()) || (IsEffectInSpell(selectedBotSpell.SpellId, SE_MovementSpeed) && !zone->CanCastOutdoor())) { - if(botClass != BARD || !IsSpellUsableThisZoneType(selectedBotSpell.SpellId, zone->GetZoneType())){ + if (botClass != BARD || !IsSpellUsableThisZoneType(selectedBotSpell.SpellId, zone->GetZoneType())) { continue; } } - switch(tar->GetArchetype()) + switch (tar->GetArchetype()) { case ARCHETYPE_CASTER: //TODO: probably more caster specific spell effects in here @@ -491,8 +500,8 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { botSpell = GetBestBotSpellForNukeByTargetType(this, ST_Summoned); } - if(botClass == PALADIN || botClass == DRUID || botClass == CLERIC || botClass == ENCHANTER || botClass == WIZARD) { - if(botSpell.SpellId == 0) { + if (botClass == PALADIN || botClass == DRUID || botClass == CLERIC || botClass == ENCHANTER || botClass == WIZARD) { + if (botSpell.SpellId == 0) { uint8 stunChance = (tar->IsCasting() ? 30: 15); if(botClass == PALADIN) @@ -913,29 +922,29 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { break; } case SpellType_Cure: { - if(GetNeedsCured(tar) && (tar->DontCureMeBefore() < Timer::GetCurrentTime()) && !(GetNumberNeedingHealedInGroup(25, false) > 0) && !(GetNumberNeedingHealedInGroup(40, false) > 2)) + if (GetNeedsCured(tar) && (tar->DontCureMeBefore() < Timer::GetCurrentTime()) && !(GetNumberNeedingHealedInGroup(25, false) > 0) && !(GetNumberNeedingHealedInGroup(40, false) > 2)) { botSpell = GetBestBotSpellForCure(this, tar); - if(botSpell.SpellId == 0) + if (botSpell.SpellId == 0) break; uint32 TempDontCureMeBeforeTime = tar->DontCureMeBefore(); castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontCureMeBeforeTime); - if(castedSpell) { - if(botClass != BARD) { - if(IsGroupSpell(botSpell.SpellId)){ + if (castedSpell) { + if (botClass != BARD) { + if (IsGroupSpell(botSpell.SpellId)) { Group *g; - if(HasGroup()) { + if (HasGroup()) { Group *g = GetGroup(); - if(g) { - for( int i = 0; imembers[i] && !g->members[i]->qglobal) { - if(TempDontCureMeBeforeTime != tar->DontCureMeBefore()) + if (g) { + for ( int i = 0; imembers[i] && !g->members[i]->qglobal) { + if (TempDontCureMeBeforeTime != tar->DontCureMeBefore()) g->members[i]->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000); } } @@ -943,8 +952,9 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { } } else { - if(TempDontCureMeBeforeTime != tar->DontCureMeBefore()) + if (TempDontCureMeBeforeTime != tar->DontCureMeBefore()) { tar->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000); + } } } } @@ -1063,7 +1073,7 @@ bool Bot::AIDoSpellCast(uint8 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgain int32 manaCost = mana_cost; if (manaCost == -1) - manaCost = spells[AIspells[i].spellid].mana; + manaCost = spells[AIBot_spells[i].spellid].mana; else if (manaCost == -2) manaCost = 0; @@ -1074,7 +1084,7 @@ bool Bot::AIDoSpellCast(uint8 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgain if(RuleB(Bots, FinishBuffing)) { if(manaCost > hasMana) { // Let's have the bots complete the buff time process - if(AIspells[i].type & SpellType_Buff) { + if(AIBot_spells[i].type & SpellType_Buff) { extraMana = manaCost - hasMana; SetMana(manaCost); } @@ -1083,20 +1093,22 @@ bool Bot::AIDoSpellCast(uint8 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgain float dist2 = 0; - if (AIspells[i].type & SpellType_Escape) { + if (AIBot_spells[i].type & SpellType_Escape) { dist2 = 0; } else dist2 = DistanceSquared(m_Position, tar->GetPosition()); - if (((((spells[AIspells[i].spellid].target_type==ST_GroupTeleport && AIspells[i].type==2) - || spells[AIspells[i].spellid].target_type==ST_AECaster - || spells[AIspells[i].spellid].target_type==ST_Group - || spells[AIspells[i].spellid].target_type==ST_AEBard) - && dist2 <= spells[AIspells[i].spellid].aoe_range*spells[AIspells[i].spellid].aoe_range) - || dist2 <= GetActSpellRange(AIspells[i].spellid, spells[AIspells[i].spellid].range)*GetActSpellRange(AIspells[i].spellid, spells[AIspells[i].spellid].range)) && (mana_cost <= GetMana() || GetMana() == GetMaxMana())) + if (((((spells[AIBot_spells[i].spellid].target_type==ST_GroupTeleport && AIBot_spells[i].type==2) + || spells[AIBot_spells[i].spellid].target_type==ST_AECaster + || spells[AIBot_spells[i].spellid].target_type==ST_Group + || spells[AIBot_spells[i].spellid].target_type==ST_AEBard) + && dist2 <= spells[AIBot_spells[i].spellid].aoe_range*spells[AIBot_spells[i].spellid].aoe_range) + || dist2 <= GetActSpellRange(AIBot_spells[i].spellid, spells[AIBot_spells[i].spellid].range)*GetActSpellRange(AIBot_spells[i].spellid, spells[AIBot_spells[i].spellid].range)) && (mana_cost <= GetMana() || GetMana() == GetMaxMana())) { - result = NPC::AIDoSpellCast(i, tar, mana_cost, oDontDoAgainBefore); - + casting_spell_AIindex = i; + LogAIDetail("Bot::AIDoSpellCast: spellid = [{}], tar = [{}], mana = [{}], Name: [{}]", AIBot_spells[i].spellid, tar->GetName(), mana_cost, spells[AIBot_spells[i].spellid].name); + result = Mob::CastSpell(AIBot_spells[i].spellid, tar->GetID(), EQ::spells::CastingSlot::Gem2, spells[AIBot_spells[i].spellid].cast_time, AIBot_spells[i].manacost == -2 ? 0 : mana_cost, oDontDoAgainBefore, -1, -1, 0, &(AIBot_spells[i].resist_adjust)); + if(IsCasting() && IsSitting()) Stand(); } @@ -1107,20 +1119,20 @@ bool Bot::AIDoSpellCast(uint8 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgain extraMana = false; } else { //handle spell recast and recast timers - //if(GetClass() == BARD && IsGroupSpell(AIspells[i].spellid)) { + //if(GetClass() == BARD && IsGroupSpell(AIBot_spells[i].spellid)) { // // Bard buff songs have been moved to their own npc spell type.. // // Buff stacking is now checked as opposed to manipulating the timer to avoid rapid casting - // //AIspells[i].time_cancast = (spells[AIspells[i].spellid].recast_time > (spells[AIspells[i].spellid].buffduration * 6000)) ? Timer::GetCurrentTime() + spells[AIspells[i].spellid].recast_time : Timer::GetCurrentTime() + spells[AIspells[i].spellid].buffduration * 6000; - // //spellend_timer.Start(spells[AIspells[i].spellid].cast_time); + // //AIBot_spells[i].time_cancast = (spells[AIBot_spells[i].spellid].recast_time > (spells[AIBot_spells[i].spellid].buffduration * 6000)) ? Timer::GetCurrentTime() + spells[AIBot_spells[i].spellid].recast_time : Timer::GetCurrentTime() + spells[AIBot_spells[i].spellid].buffduration * 6000; + // //spellend_timer.Start(spells[AIBot_spells[i].spellid].cast_time); //} //else - // AIspells[i].time_cancast = Timer::GetCurrentTime() + spells[AIspells[i].spellid].recast_time; + // AIBot_spells[i].time_cancast = Timer::GetCurrentTime() + spells[AIBot_spells[i].spellid].recast_time; - AIspells[i].time_cancast = Timer::GetCurrentTime() + spells[AIspells[i].spellid].recast_time; + AIBot_spells[i].time_cancast = Timer::GetCurrentTime() + spells[AIBot_spells[i].spellid].recast_time; - if(spells[AIspells[i].spellid].timer_id > 0) { - SetSpellRecastTimer(spells[AIspells[i].spellid].timer_id, spells[AIspells[i].spellid].recast_time); + if(spells[AIBot_spells[i].spellid].timer_id > 0) { + SetSpellRecastTimer(spells[AIBot_spells[i].spellid].timer_id, spells[AIBot_spells[i].spellid].recast_time); } } @@ -1551,13 +1563,13 @@ bool Bot::AIHealRotation(Mob* tar, bool useFastHeals) { else { botSpell = GetBestBotSpellForPercentageHeal(this); - if(botSpell.SpellId == 0) + if (botSpell.SpellId == 0) { botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); - - if(botSpell.SpellId == 0) + } + if (botSpell.SpellId == 0) { botSpell = GetFirstBotSpellForSingleTargetHeal(this); - - if(botSpell.SpellId == 0){ + } + if (botSpell.SpellId == 0) { botSpell = GetFirstBotSpellBySpellType(this, SpellType_Heal); } } @@ -1593,7 +1605,7 @@ std::list Bot::GetBotSpellsForSpellEffect(Bot* botCaster, int spellEff std::list result; if(botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for (int i = botSpellList.size() - 1; i >= 0; i--) { if (botSpellList[i].spellid <= 0 || botSpellList[i].spellid >= SPDAT_RECORDS) { @@ -1620,7 +1632,7 @@ std::list Bot::GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, std::list result; if(botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for (int i = botSpellList.size() - 1; i >= 0; i--) { if (botSpellList[i].spellid <= 0 || botSpellList[i].spellid >= SPDAT_RECORDS) { @@ -1649,7 +1661,7 @@ std::list Bot::GetBotSpellsBySpellType(Bot* botCaster, uint32 spellTyp std::list result; if(botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for (int i = botSpellList.size() - 1; i >= 0; i--) { if (botSpellList[i].spellid <= 0 || botSpellList[i].spellid >= SPDAT_RECORDS) { @@ -1676,7 +1688,7 @@ std::list Bot::GetPrioritizedBotSpellsBySpellType(Bot* botCa std::list result; if (botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for (int i = botSpellList.size() - 1; i >= 0; i--) { if (botSpellList[i].spellid <= 0 || botSpellList[i].spellid >= SPDAT_RECORDS) { @@ -1711,7 +1723,7 @@ BotSpell Bot::GetFirstBotSpellBySpellType(Bot* botCaster, uint32 spellType) { result.ManaCost = 0; if(botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for (int i = botSpellList.size() - 1; i >= 0; i--) { if (botSpellList[i].spellid <= 0 || botSpellList[i].spellid >= SPDAT_RECORDS) { @@ -1767,7 +1779,7 @@ BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* botCaster) { if(botCaster) { std::list botHoTSpellList = GetBotSpellsForSpellEffect(botCaster, SE_HealOverTime); - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for(std::list::iterator botSpellListItr = botHoTSpellList.begin(); botSpellListItr != botHoTSpellList.end(); ++botSpellListItr) { // Assuming all the spells have been loaded into this list by level and in descending order @@ -1803,7 +1815,7 @@ BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot *botCaster) { result.ManaCost = 0; if(botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for (int i = botSpellList.size() - 1; i >= 0; i--) { if (botSpellList[i].spellid <= 0 || botSpellList[i].spellid >= SPDAT_RECORDS) { @@ -1909,7 +1921,7 @@ BotSpell Bot::GetBestBotSpellForGroupHealOverTime(Bot* botCaster) { if(botCaster) { std::list botHoTSpellList = GetBotSpellsForSpellEffect(botCaster, SE_HealOverTime); - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for(std::list::iterator botSpellListItr = botHoTSpellList.begin(); botSpellListItr != botHoTSpellList.end(); ++botSpellListItr) { // Assuming all the spells have been loaded into this list by level and in descending order @@ -2320,7 +2332,7 @@ BotSpell Bot::GetDebuffBotSpell(Bot* botCaster, Mob *tar) { return result; if(botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for (int i = botSpellList.size() - 1; i >= 0; i--) { if (botSpellList[i].spellid <= 0 || botSpellList[i].spellid >= SPDAT_RECORDS) { @@ -2367,7 +2379,7 @@ BotSpell Bot::GetBestBotSpellForResistDebuff(Bot* botCaster, Mob *tar) { bool needsDiseaseResistDebuff = (tar->GetDR() + level_mod) > 100 ? true: false; if(botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->GetBotSpells(); + std::vector botSpellList = botCaster->AIBot_spells; for (int i = botSpellList.size() - 1; i >= 0; i--) { if (botSpellList[i].spellid <= 0 || botSpellList[i].spellid >= SPDAT_RECORDS) { @@ -2526,8 +2538,8 @@ int32 Bot::GetSpellRecastTimer(Bot *caster, int timer_index) { bool Bot::CheckSpellRecastTimers(Bot *caster, int SpellIndex) { if(caster) { - if(caster->AIspells[SpellIndex].time_cancast < Timer::GetCurrentTime()) { //checks spell recast - if(GetSpellRecastTimer(caster, spells[caster->AIspells[SpellIndex].spellid].timer_id) < Timer::GetCurrentTime()) { //checks for spells on the same timer + if(caster->AIBot_spells[SpellIndex].time_cancast < Timer::GetCurrentTime()) { //checks spell recast + if(GetSpellRecastTimer(caster, spells[caster->AIBot_spells[SpellIndex].spellid].timer_id) < Timer::GetCurrentTime()) { //checks for spells on the same timer return true; //can cast spell } } @@ -2674,4 +2686,344 @@ uint8 Bot::GetChanceToCastBySpellType(uint32 spellType) return database.botdb.GetSpellCastingChance(spell_type_index, class_index, stance_index, type_index); } +bool Bot::AI_AddBotSpells(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; + AIBot_spells.clear(); + if (iDBSpellsID == 0) { + AIautocastspell_timer->Disable(); + return false; + } + DBbotspells_Struct* spell_list = content_db.GetBotSpells(iDBSpellsID); + if (!spell_list) { + AIautocastspell_timer->Disable(); + return false; + } + DBbotspells_Struct* parentlist = content_db.GetBotSpells(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)"); + } + LogAIDetail("[{}]", debug_msg.c_str()); + + if (parentlist) { + for (const auto &iter : parentlist->entries) { + LogAI("([{}]) [{}]", iter.spellid, spells[iter.spellid].name); + } + } + LogAIModerate("fin (parent list)"); + if (spell_list) { + for (const auto &iter : spell_list->entries) { + LogAIDetail("([{}]) [{}]", iter.spellid, spells[iter.spellid].name); + } + } + LogAIModerate("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 (!IsSpellInBotList(spell_list, e.spellid)) + { + AddSpellToBotList(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) { + AddSpellToBotList(e.priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.min_hp, e.max_hp); + } + } + + std::sort(AIBot_spells.begin(), AIBot_spells.end(), [](const BotSpells_Struct& a, const BotSpells_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 (AIBot_spells.empty()) { + AIautocastspell_timer->Disable(); + } else { + AIautocastspell_timer->Trigger(); + } + return true; +} + +bool IsSpellInBotList(DBbotspells_Struct* spell_list, uint16 iSpellID) { + auto it = std::find_if(spell_list->entries.begin(), spell_list->entries.end(), + [iSpellID](const DBbotspells_entries_Struct &a) { return a.spellid == iSpellID; }); + return it != spell_list->entries.end(); +} + +DBbotspells_Struct *ZoneDatabase::GetBotSpells(uint32 iDBSpellsID) +{ + if (iDBSpellsID == 0) { + return nullptr; + } + + auto it = Bot_Spells_Cache.find(iDBSpellsID); + + if (it != Bot_Spells_Cache.end()) { // it's in the cache, easy =) + return &it->second; + } + + if (!Bot_Spells_LoadTried.count(iDBSpellsID)) { // no reason to ask the DB again if we have failed once already + Bot_Spells_LoadTried.insert(iDBSpellsID); + + std::string query = StringFormat("SELECT id, parent_list, attack_proc, proc_chance, " + "range_proc, rproc_chance, defensive_proc, dproc_chance, " + "fail_recast, engaged_no_sp_recast_min, engaged_no_sp_recast_max, " + "engaged_b_self_chance, engaged_b_other_chance, engaged_d_chance, " + "pursue_no_sp_recast_min, pursue_no_sp_recast_max, " + "pursue_d_chance, idle_no_sp_recast_min, idle_no_sp_recast_max, " + "idle_b_chance FROM npc_spells WHERE id=%d", + iDBSpellsID); + auto results = QueryDatabase(query); + if (!results.Success()) { + return nullptr; + } + + if (results.RowCount() != 1) { + return nullptr; + } + + auto row = results.begin(); + DBbotspells_Struct spell_set; + + spell_set.parent_list = atoi(row[1]); + spell_set.attack_proc = atoi(row[2]); + spell_set.proc_chance = atoi(row[3]); + spell_set.range_proc = atoi(row[4]); + spell_set.rproc_chance = atoi(row[5]); + spell_set.defensive_proc = atoi(row[6]); + spell_set.dproc_chance = atoi(row[7]); + spell_set.fail_recast = atoi(row[8]); + spell_set.engaged_no_sp_recast_min = atoi(row[9]); + spell_set.engaged_no_sp_recast_max = atoi(row[10]); + spell_set.engaged_beneficial_self_chance = atoi(row[11]); + spell_set.engaged_beneficial_other_chance = atoi(row[12]); + spell_set.engaged_detrimental_chance = atoi(row[13]); + spell_set.pursue_no_sp_recast_min = atoi(row[14]); + spell_set.pursue_no_sp_recast_max = atoi(row[15]); + spell_set.pursue_detrimental_chance = atoi(row[16]); + spell_set.idle_no_sp_recast_min = atoi(row[17]); + spell_set.idle_no_sp_recast_max = atoi(row[18]); + spell_set.idle_beneficial_chance = atoi(row[19]); + + // pulling fixed values from an auto-increment field is dangerous... + query = StringFormat( + "SELECT spellid, type, minlevel, maxlevel, " + "manacost, recast_delay, priority, min_hp, max_hp, resist_adjust " + "FROM bot_spells_entries " + "WHERE npc_spells_id=%d ORDER BY minlevel", + iDBSpellsID); + results = QueryDatabase(query); + + if (!results.Success()) { + return nullptr; + } + + int entryIndex = 0; + for (row = results.begin(); row != results.end(); ++row, ++entryIndex) { + DBbotspells_entries_Struct entry; + int spell_id = atoi(row[0]); + entry.spellid = spell_id; + entry.type = atoul(row[1]); + entry.minlevel = atoi(row[2]); + entry.maxlevel = atoi(row[3]); + entry.manacost = atoi(row[4]); + entry.recast_delay = atoi(row[5]); + entry.priority = atoi(row[6]); + entry.min_hp = atoi(row[7]); + entry.max_hp = atoi(row[8]); + + // some spell types don't make much since to be priority 0, so fix that + if (!(entry.type & SPELL_TYPES_INNATE) && entry.priority == 0) + entry.priority = 1; + + if (row[9]) { + entry.resist_adjust = atoi(row[9]); + } + else if (IsValidSpell(spell_id)) { + entry.resist_adjust = spells[spell_id].resist_difficulty; + } + spell_set.entries.push_back(entry); + } + + Bot_Spells_Cache.insert(std::make_pair(iDBSpellsID, spell_set)); + + return &Bot_Spells_Cache[iDBSpellsID]; + } + + return nullptr; +} + +// adds a spell to the list, taking into account priority and resorting list as needed. +void Bot::AddSpellToBotList(int16 iPriority, uint16 iSpellID, uint32 iType, + int16 iManaCost, int32 iRecastDelay, int16 iResistAdjust, int8 min_hp, int8 max_hp) +{ + + if(!IsValidSpell(iSpellID)) { + return; + } + + HasAISpell = true; + BotSpells_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; + + AIBot_spells.push_back(t); + + // If we're going from an empty list, we need to start the timer + if (AIBot_spells.empty()) { + AIautocastspell_timer->Start(RandomTimer(0, 300), false); + } +} + +//this gets called from InterruptSpell() for failure or SpellFinished() for success +void Bot::AI_Bot_Event_SpellCastFinished(bool iCastSucceeded, uint16 slot) { + if (slot == 1) { + uint32 recovery_time = 0; + if (iCastSucceeded) { + if (casting_spell_AIindex < AIBot_spells.size()) { + recovery_time += spells[AIBot_spells[casting_spell_AIindex].spellid].recovery_time; + if (AIBot_spells[casting_spell_AIindex].recast_delay >= 0) { + if (AIBot_spells[casting_spell_AIindex].recast_delay < 10000) { + AIBot_spells[casting_spell_AIindex].time_cancast = Timer::GetCurrentTime() + (AIBot_spells[casting_spell_AIindex].recast_delay*1000); + } + } + else { + AIBot_spells[casting_spell_AIindex].time_cancast = Timer::GetCurrentTime() + spells[AIBot_spells[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 = AIBot_spells.size(); + } +} + #endif diff --git a/zone/merc.cpp b/zone/merc.cpp index 687c8068f..c21984773 100644 --- a/zone/merc.cpp +++ b/zone/merc.cpp @@ -2138,11 +2138,11 @@ bool Merc::AICastSpell(int8 iChance, uint32 iSpellTypes) { } else { //check for heal over time. if not present, try it first - if(!tar->FindType(SE_HealOverTime)) { + if (!tar->FindType(SE_HealOverTime)) { selectedMercSpell = GetBestMercSpellForHealOverTime(this); //get regular heal - if(selectedMercSpell.spellid == 0) { + if (selectedMercSpell.spellid == 0) { selectedMercSpell = GetBestMercSpellForRegularSingleTargetHeal(this); } } diff --git a/zone/mob_ai.cpp b/zone/mob_ai.cpp index 2b2227a45..98cbba88b 100644 --- a/zone/mob_ai.cpp +++ b/zone/mob_ai.cpp @@ -2988,16 +2988,9 @@ DBnpcspells_Struct *ZoneDatabase::GetNPCSpells(uint32 iDBSpellsID) query = StringFormat( "SELECT spellid, type, minlevel, maxlevel, " "manacost, recast_delay, priority, min_hp, max_hp, resist_adjust " -#ifdef BOTS - "FROM %s " - "WHERE npc_spells_id=%d ORDER BY minlevel", - (iDBSpellsID >= 3001 && iDBSpellsID <= 3016 ? "bot_spells_entries" : "npc_spells_entries"), - iDBSpellsID); -#else "FROM npc_spells_entries " "WHERE npc_spells_id=%d ORDER BY minlevel", iDBSpellsID); -#endif results = QueryDatabase(query); if (!results.Success()) { diff --git a/zone/npc.cpp b/zone/npc.cpp index da4f5bd41..f8df0f4bc 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -31,6 +31,7 @@ #include "../common/linked_list.h" #include "../common/servertalk.h" #include "../common/say_link.h" +#include "../common/data_verification.h" #include "client.h" #include "entity.h" @@ -42,6 +43,10 @@ #include "water_map.h" #include "npc_scale_manager.h" +#ifdef BOTS +#include "bot.h" +#endif + #include #include #include @@ -309,8 +314,16 @@ NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &posi AISpellVar.idle_no_sp_recast_max = static_cast(RuleI(Spells, AI_IdleNoSpellMaxRecast)); AISpellVar.idle_beneficial_chance = static_cast (RuleI(Spells, AI_IdleBeneficialChance)); - AI_Init(); - AI_Start(); + // It's possible for IsBot() to not be set yet during Bot loading, so have to use an alternative to catch Bots + if (!EQ::ValueWithin(npc_type_data->npc_spells_id, EQ::constants::BotSpellIDs::Warrior, EQ::constants::BotSpellIDs::Berserker)) { + AI_Init(); + AI_Start(); +#ifdef BOTS + } else { + CastToBot()->AI_Bot_Init(); + CastToBot()->AI_Bot_Start(); +#endif + } d_melee_texture1 = npc_type_data->d_melee_texture1; d_melee_texture2 = npc_type_data->d_melee_texture2; diff --git a/zone/npc.h b/zone/npc.h index ab4cb921e..3b236a784 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -68,6 +68,18 @@ struct AISpells_Struct { int8 max_hp; // >0 won't cast if HP is above }; +struct BotSpells_Struct { + uint32 type; // 0 = never, must be one (and only one) of the defined values + int16 spellid; // <= 0 = no spell + int16 manacost; // -1 = use spdat, -2 = no cast time + uint32 time_cancast; // when we can cast this spell next + int32 recast_delay; + int16 priority; + int16 resist_adjust; + int16 min_hp; // >0 won't cast if HP is below + int16 max_hp; // >0 won't cast if HP is above +}; + struct AISpellsEffects_Struct { uint16 spelleffectid; int32 base_value; @@ -585,6 +597,7 @@ protected: uint32* pDontCastBefore_casting_spell; std::vector AIspells; + std::vector AIBot_spells; //Will eventually be moved to Bot Class once Bots are no longer reliant on NPC constructor bool HasAISpell; virtual bool AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes, bool bInnates = false); virtual bool AIDoSpellCast(uint8 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore = 0); diff --git a/zone/zone.cpp b/zone/zone.cpp index 2e1feb58b..51025d707 100755 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -1874,7 +1874,8 @@ bool Zone::Depop(bool StartSpawnTimer) { // clear spell cache database.ClearNPCSpells(); - + database.ClearBotSpells(); + zone->spawn_group_list.ReloadSpawnGroups(); return true; diff --git a/zone/zonedb.h b/zone/zonedb.h index b181da243..3daf0a123 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -107,6 +107,44 @@ struct DBnpcspellseffects_Struct { DBnpcspellseffects_entries_Struct entries[0]; }; +#pragma pack(1) +struct DBbotspells_entries_Struct { + uint16 spellid; + uint8 minlevel; + uint8 maxlevel; + uint32 type; + int16 manacost; + int16 priority; + int32 recast_delay; + int16 resist_adjust; + int8 min_hp; + int8 max_hp; +}; +#pragma pack() + +struct DBbotspells_Struct { + uint32 parent_list; + uint16 attack_proc; + uint8 proc_chance; + uint16 range_proc; + int16 rproc_chance; + uint16 defensive_proc; + int16 dproc_chance; + uint32 fail_recast; + uint32 engaged_no_sp_recast_min; + uint32 engaged_no_sp_recast_max; + uint8 engaged_beneficial_self_chance; + uint8 engaged_beneficial_other_chance; + uint8 engaged_detrimental_chance; + uint32 pursue_no_sp_recast_min; + uint32 pursue_no_sp_recast_max; + uint8 pursue_detrimental_chance; + uint32 idle_no_sp_recast_min; + uint32 idle_no_sp_recast_max; + uint8 idle_beneficial_chance; + std::vector entries; +}; + struct DBTradeskillRecipe_Struct { EQ::skills::SkillType tradeskill; int16 skill_needed; @@ -491,6 +529,10 @@ public: void ClearNPCSpells() { npc_spells_cache.clear(); npc_spells_loadtried.clear(); } const NPCType* LoadNPCTypesData(uint32 id, bool bulk_load = false); + /*Bots */ + DBbotspells_Struct* GetBotSpells(uint32 iDBSpellsID); + void ClearBotSpells() { Bot_Spells_Cache.clear(); Bot_Spells_LoadTried.clear(); } + /* Mercs */ const NPCType* GetMercType(uint32 id, uint16 raceid, uint32 clientlevel); void LoadMercEquipment(Merc *merc); @@ -595,6 +637,8 @@ protected: std::unordered_set npc_spells_loadtried; DBnpcspellseffects_Struct** npc_spellseffects_cache; bool* npc_spellseffects_loadtried; + std::unordered_map Bot_Spells_Cache; + std::unordered_set Bot_Spells_LoadTried; }; extern ZoneDatabase database;