From c23fc4ade75f7b0d01162b4cfb5d2324e2cf2b61 Mon Sep 17 00:00:00 2001 From: Xackery Date: Mon, 15 Nov 2021 20:55:40 -0800 Subject: [PATCH] Added Elixir --- common/ruletypes.h | 6 + zone/CMakeLists.txt | 2 + zone/bot.h | 4 +- zone/botspellsai.cpp | 228 ++++++++++++ zone/elixir.cpp | 804 +++++++++++++++++++++++++++++++++++++++++++ zone/elixir.h | 21 ++ zone/merc.cpp | 156 +++++++++ zone/merc.h | 2 + zone/mob.h | 5 +- zone/spells.cpp | 21 ++ 10 files changed, 1246 insertions(+), 3 deletions(-) create mode 100644 zone/elixir.cpp create mode 100644 zone/elixir.h diff --git a/common/ruletypes.h b/common/ruletypes.h index c2b91c6b5..2f71690a3 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -188,6 +188,9 @@ RULE_INT(Mercs, AggroRadiusPuller, 25, "Determines the distance from which a mer RULE_INT(Mercs, ResurrectRadius, 50, "Determines the distance from which a healer merc will attempt to resurrect a group member's corpse") RULE_INT(Mercs, ScaleRate, 100, "Merc scale factor") RULE_BOOL(Mercs, AllowMercSuspendInCombat, true, "Allow merc suspend in combat") +RULE_BOOL(Mercs, IsMercsElixirEnabled, false, "Override AI with elixir logic") +RULE_INT(Mercs, MercsElixirHealPercent, 90, "Heal allies at this percent health") +RULE_INT(Mercs, MercsElixirAEMinimum, 3, "AE Minimum to trigger AE spells (heals and nukes)") RULE_CATEGORY_END() RULE_CATEGORY(Guild) @@ -599,6 +602,9 @@ RULE_BOOL(Bots, OldRaceRezEffects, false, "Older clients had ID 757 for races wi RULE_BOOL(Bots, ResurrectionSickness, true, "Use Resurrection Sickness based on Resurrection spell cast, set to false to disable Resurrection Sickness.") RULE_INT(Bots, OldResurrectionSicknessSpell, 757, "757 is Default Old Resurrection Sickness Spell") RULE_INT(Bots, ResurrectionSicknessSpell, 756, "756 is Default Resurrection Sickness Spell") +RULE_BOOL(Bots, IsBotsElixirEnabled, false, "Override AI with elixir logic") +RULE_INT(Bots, BotsElixirHealPercent, 90, "Heal allies at this percent health") +RULE_INT(Bots, BotsElixirAEMinimum, 3, "AE Minimum to trigger AE spells (heals and nukes)") RULE_CATEGORY_END() #endif diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index dd4188c25..ba1c68c0d 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -26,6 +26,7 @@ SET(zone_sources dialogue_window.cpp dynamic_zone.cpp effects.cpp + elixir.cpp embparser.cpp embparser_api.cpp embperl.cpp @@ -187,6 +188,7 @@ SET(zone_headers doors.h dialogue_window.h dynamic_zone.h + elixir.h embparser.h embperl.h embxs.h diff --git a/zone/bot.h b/zone/bot.h index 5ed980fe8..3c7dfb415 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -320,7 +320,9 @@ public: uint8 GetStopMeleeLevel() { return _stopMeleeLevel; } void SetStopMeleeLevel(uint8 level); void SetGuardMode(); - void SetHoldMode(); + void SetHoldMode(); + bool ElixirAIDetermineSpellToCast(); + bool ElixirAITryCastSpell(BotSpell botSpell, bool isHeal = false); // Mob AI Virtual Override Methods virtual void AI_Process(); diff --git a/zone/botspellsai.cpp b/zone/botspellsai.cpp index 22eb103c5..e6f4440d0 100644 --- a/zone/botspellsai.cpp +++ b/zone/botspellsai.cpp @@ -1134,6 +1134,14 @@ bool Bot::AI_PursueCastCheck() { AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. + if (RuleB(Bots, IsBotsElixirEnabled)) { + if (ElixirAIDetermineSpellToCast()) { + AIautocastspell_timer->Start(RandomTimer(500, 2000), false); // avg human response is much less than 5 seconds..even for non-combat situations... + return true; + } + return false; + } + LogAI("Bot Engaged (pursuing) autocast check triggered. Trying to cast offensive spells"); if(!AICastSpell(GetTarget(), 100, SpellType_Snare)) { @@ -1166,6 +1174,14 @@ bool Bot::AI_IdleCastCheck() { #endif AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. + if (RuleB(Bots, IsBotsElixirEnabled)) { + if (ElixirAIDetermineSpellToCast()) { + AIautocastspell_timer->Start(RandomTimer(500, 2000), false); // avg human response is much less than 5 seconds..even for non-combat situations... + return true; + } + return false; + } + bool pre_combat = false; Client* test_against = nullptr; @@ -1308,6 +1324,13 @@ bool Bot::AI_EngagedCastCheck() { if (GetTarget() && AIautocastspell_timer->Check(false)) { AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. + if (RuleB(Bots, IsBotsElixirEnabled)) { + if (ElixirAIDetermineSpellToCast()) { + AIautocastspell_timer->Start(RandomTimer(500, 2000), false); // avg human response is much less than 5 seconds..even for non-combat situations... + return true; + } + return false; + } uint8 botClass = GetClass(); EQ::constants::StanceType botStance = GetBotStance(); @@ -2674,4 +2697,209 @@ uint8 Bot::GetChanceToCastBySpellType(uint32 spellType) return database.botdb.GetSpellCastingChance(spell_type_index, class_index, stance_index, type_index); } + + +// ElixirAIDetermineSpellToCast is called during AI bot logics +// It determines by class which spell to cast +bool Bot::ElixirAIDetermineSpellToCast() { + BotSpell selectedBotSpell; + int8 spellAIResult; + Group *grp = GetGroup(); + + if (GetClass() == WARRIOR || GetClass() == SHADOWKNIGHT || GetClass() == PALADIN) { + + /*if(CheckAETaunt()) { + selectedBotSpell = GetBestBotSpellForAETaunt(this); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + Log(Logs::General, Logs::Botenaries, "%s AE Taunting.", GetName()); + return true; + } + } + + if(CheckTaunt()) { + selectedBotSpell = GetBestBotSpellForTaunt(this); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + } + */ + } + + switch (GetClass()) { + case CLERIC: + case PALADIN: + case RANGER: + selectedBotSpell = GetBestBotSpellForGroupHeal(this); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + + selectedBotSpell = GetBestBotSpellForHealOverTime(this); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + + selectedBotSpell = GetBestBotSpellForFastHeal(this); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + + selectedBotSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + + for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (!grp) break; + if (!grp->members[i]) continue; + if (!grp->members[i]->qglobal) continue; + if(!GetNeedsCured(grp->members[i])) continue; + if (grp->members[i]->DontCureMeBefore() > Timer::GetCurrentTime()) continue; + selectedBotSpell = GetBestBotSpellForCure(this, grp->members[i]); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + } + + if (GetManaRatio() > 50) { // healers only offensive or buff at > 50% mana + selectedBotSpell = GetBestBotSpellForNukeByTargetType(this, ST_Target); + if (ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + + auto buffSpells = GetBotSpellsBySpellType(this, SpellType_Buff); + for (auto buffSpell : buffSpells) { + if (!ElixirAITryCastSpell(selectedBotSpell)) continue; + return true; + } + } + return false; + // Pets class will first cast their pet, then buffs + case DRUID: + case MAGICIAN: + case SHADOWKNIGHT: + case SHAMAN: + case NECROMANCER: + case ENCHANTER: + case BEASTLORD: + selectedBotSpell = GetBestBotSpellForGroupHeal(this); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + + selectedBotSpell = GetBestBotSpellForHealOverTime(this); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + + selectedBotSpell = GetBestBotSpellForFastHeal(this); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + + selectedBotSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + + for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (!grp) break; + if (!grp->members[i]) continue; + if (!grp->members[i]->qglobal) continue; + if(!GetNeedsCured(grp->members[i])) continue; + if (grp->members[i]->DontCureMeBefore() > Timer::GetCurrentTime()) continue; + selectedBotSpell = GetBestBotSpellForCure(this, grp->members[i]); + if (ElixirAITryCastSpell(selectedBotSpell, true)) { + return true; + } + } + + if (GetManaRatio() > 50) { // healers only offensive or buff at > 50% mana + selectedBotSpell = GetBestBotSpellForNukeByTargetType(this, ST_Target); + if (ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + + auto buffSpells = GetBotSpellsBySpellType(this, SpellType_Buff); + for (auto buffSpell : buffSpells) { + if (!ElixirAITryCastSpell(selectedBotSpell)) continue; + return true; + } + } + return false; + case WIZARD: // This can eventually be move into the BEASTLORD case handler once pre-combat is fully implemented + if (GetTarget() && HasOrMayGetAggro()) { + selectedBotSpell = GetFirstBotSpellBySpellType(this, SpellType_Escape); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + } + + selectedBotSpell = GetFirstBotSpellBySpellType(this, SpellType_Nuke); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + return false; + case BARD: + if (GetTarget() && HasOrMayGetAggro()) { + selectedBotSpell = GetFirstBotSpellBySpellType(this, SpellType_PreCombatBuffSong); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + } + + selectedBotSpell = GetFirstBotSpellBySpellType(this, SpellType_InCombatBuffSong); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + return false; + default: + if (GetTarget() && HasOrMayGetAggro()) { + selectedBotSpell = GetFirstBotSpellBySpellType(this, SpellType_Escape); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + } + + selectedBotSpell = GetFirstBotSpellBySpellType(this, SpellType_Nuke); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + + selectedBotSpell = GetFirstBotSpellBySpellType(this, SpellType_InCombatBuff); + if (selectedBotSpell.SpellId > 0 && ElixirAITryCastSpell(selectedBotSpell)) { + return true; + } + return false; + } + return false; +} + +// ElixirAITryCastSpell takes a provided spell id and does a spell check to determine if the spell is valid +// Once valid, it will cast on returned mob candidate +bool Bot::ElixirAITryCastSpell(BotSpell botSpell, bool isHeal) { + auto spellID = botSpell.SpellId; + if (spellID == 0) return false; + + Mob* outMob; + auto spellAIResult = ElixirCastSpellCheck(spellID, outMob); + + if (spellAIResult < 0) return false; + + if (spellAIResult == 0) { + AIDoSpellCast(botSpell.SpellIndex, GetTarget(), -1); + if (GetTarget() == this) return true; + if (isHeal) BotGroupSay(this, "Casting %s on %s.", spells[spellID].name, GetTarget()->GetCleanName()); + return true; + } + + if (outMob == nullptr) { + return false; + } + + AIDoSpellCast(botSpell.SpellIndex, outMob, -1); + if (outMob == this) return true; + if (isHeal) BotGroupSay(this, "Casting %s on %s.", spells[spellID].name, outMob->GetCleanName()); + return true; +} #endif diff --git a/zone/elixir.cpp b/zone/elixir.cpp new file mode 100644 index 000000000..7de140d5d --- /dev/null +++ b/zone/elixir.cpp @@ -0,0 +1,804 @@ +#include "../common/rulesys.h" +#include "../common/global_define.h" +#include "../common/eqemu_logsys.h" + +#include "mob.h" +#include "elixir.h" +#include "zone.h" +#include "groups.h" + +extern Zone* zone; + +// ElixirCastSpell determines if a spell can be casted by Mob. +// If 0 is returned, spell is valid and no target changing is required +// If a negative value is returned, an error occured. See elixir.h ELIXIR_ prefix const error lookup of reasons +// If 1 is returned, outMob is set to the suggested mob entity +int8 Mob::ElixirCastSpellCheck(uint16 spellID, Mob *outMob) +{ + int manaCurrent = GetMana(); + int manaMax = GetMaxMana(); + int hpCurrent = GetHP(); + int hpMax = GetMaxHP(); + int endCurrent = GetEndurance(); + int endMax = GetMaxEndurance(); + + int healPercent = 90; + if (IsMerc()) healPercent = RuleI(Mercs, MercsElixirHealPercent); +#ifdef BOTS + if (IsBot()) healPercent = RuleI(Bots, BotsElixirHealPercent); +#endif + if (healPercent < 1) healPercent = 1; + if (healPercent > 99) healPercent = 99; + + int aeMinimum = 3; + if (IsMerc()) aeMinimum = RuleI(Mercs, MercsElixirAEMinimum); +#ifdef BOTS + if (IsBot()) aeMinimum = RuleI(Bots, BotsElixirAEMinimum); +#endif + if (aeMinimum < 1) aeMinimum = 1; + if (aeMinimum > 99) aeMinimum = 99; + + if (IsCorpse()) return ELIXIR_CANNOT_CAST_BAD_STATE; + + bool isHeal = false; + bool isDebuff = false; + bool isBuff = false; + bool isLifetap = false; + bool isMana = false; + bool isCharm = false; + bool isSnare = false; + bool isSow = false; + bool isTaunt = false; + bool isSingleTargetSpell = false; + bool isPetSummon = false; + bool isTransport = false; + bool isGroupSpell = false; + bool isBardSong = false; + bool isMez = false; + bool isLull = false; + long stunDuration = 0; + long damageAmount = 0; + long healAmount = 0; + + int skillID; + + bodyType targetBodyType = BT_NoTarget2; + + const SPDat_Spell_Struct &spDat = spells[spellID]; + + int spellgroup = spDat.type_description_id; + uint32 ticks = spDat.buff_duration; + int targets = spDat.aoe_max_targets; + SpellTargetType targettype = spDat.target_type; + EQ::skills::SkillType skill = spDat.skill; + uint16 recourseID = spDat.recourse_link; + int category = spDat.spell_category; + int subcategory = spDat.effect_description_id; + + uint32 buffCount; + Group *grp = GetGroup(); + Raid *raid = GetRaid(); + + for (int i = 0; i < EFFECT_COUNT; i++) { + if (IsBlankSpellEffect(spellID, i)) continue; + + int attr = spDat.effect_id[i]; + int base = spDat.base_value[i]; + int base2 = spDat.limit_value[i]; + int max = spDat.max_value[i]; + int calc = spDat.formula[i]; + + if (attr == SE_CurrentHP) { //0 + if (max > 0) { //Heal / HoT + if (ticks < 5 && base > 0) { //regen + isHeal = true; + healAmount = base; + } + if (ticks > 0) { + isBuff = true; + } + if (category == 114) { //taps like touch of zlandicar + isLifetap = true; + } + } + if (base < 0 && damageAmount == 0) { + damageAmount = -base; + } + if (max < 0) { //Nuke / DoT + damageAmount = -max; + } + } + + if (attr == SE_ArmorClass || // 1 ac + attr == SE_ATK || //2 attack + attr == SE_STR || //4 str + attr == SE_DEX || //5 dex + attr == SE_AGI || //6 agi + attr == SE_STA || //7 sta + attr == SE_INT || //8 int + attr == SE_WIS //9 wis + ) { + if (base > 0) { //+stat + isBuff = true; + } + if (base < 0) { //-stat + isDebuff = true; + } + } + + if (attr == SE_MovementSpeed) { //3 + if (base > 0) { //+Movement + isBuff = true; + isSow = true; + } + if (base < 0) { //-Movement + isDebuff = true; + isSnare = true; + } + } + + if (attr == SE_CHA) { //10 CHA + if (base > 0 && base < 254) { //+CHA + isBuff = true; + } + if (base < 0) { //-CHA + isDebuff = true; + } + } + + if (attr == SE_AttackSpeed) { //11 attackspeed + if (base > 0) { //+Haste + isBuff = true; + } + if (base < 0) { //-Haste + isDebuff = true; + } + } + + if (attr == SE_CurrentMana) { //15 Mana + isMana = true; + } + if (attr == SE_Lull) { // pacify (lull) + isLull = true; + } + if (attr == SE_Stun) { //21 stun + stunDuration = base2; + if (targettype == ST_AEClientV1 || targettype == ST_AECaster) { // 2 or + isSingleTargetSpell = true; //hack to make ae stuns work + } + } + if (attr == SE_Charm) { //23 charm + isCharm = true; + } + + if (attr == SE_Gate) { //26 Gate + isTransport = true; + } + + if (attr == SE_ChangeFrenzyRad) { //30 frenzy radius reduction (lull) + isLull = true; + } + + if (attr == SE_Mez) { //31 Mesmerization + isMez = true; + } + + if (attr == SE_SummonPet) { //33 Summon Elemental Pet + isPetSummon = true; + } + + if (attr == SE_NecPet) { //71 Summon Skeleton Pet + isPetSummon = true; + } + + if (attr == SE_Teleport) { //83 Transport + isTransport = true; + } + + if (attr == SE_Harmony) { //86 reaction radius reduction (lull) + isLull = true; + } + if (attr == SE_Succor) { //88 Evac + isTransport = true; + } + + if (attr == SE_Familiar) { //108 Summon Familiar + isPetSummon = true; + } + + if (attr == SE_Hate) { //192 taunt + isTaunt = true; + } + + if (attr == SE_SkillAttack) { //193 skill attack + skillID = spDat.skill; + } + } + + if (subcategory == 43 && ticks < 10) { //Health + isHeal = true; + } + + if (category == 126) { //Taps + if (subcategory == 43) { //Health + isLifetap = true; + } + } + + /* + //TODO TargetTypes: + case 40: return "AE PC v2"; + case 25: return "AE Summoned"; + case 24: return "AE Undead"; + case 20: return "Targetted AE Tap"; + case 8: return "Targetted AE"; + case 2: return "AE PC v1"; + case 1: return "Line of Sight"; + */ + + + if (targettype == ST_Group) { // 41 Group v2 + isGroupSpell = true; + } + if (targettype == ST_GroupTeleport) { //3 Group v1 + isGroupSpell = true; + } + + if (isGroupSpell && IsBardSong(spellID)) { + isBardSong = true; + } + + if (targettype == ST_Target) { //5 Single + isSingleTargetSpell = true; + } + + if (GetTarget()) { + targetBodyType = target->bodytype; + } + + if (targettype == ST_Animal) { //9 Animal + isSingleTargetSpell = true; + } + + if (targettype == ST_Undead) { //10 Undead + isSingleTargetSpell = true; + } + + if (targettype == ST_Summoned) { //11 Summoned + isSingleTargetSpell = true; + } + + if (targettype == ST_Tap) { //13 Lifetap + isSingleTargetSpell = true; + } + + if (targettype == ST_Pet) { //14 Pet + isSingleTargetSpell = true; + } + + if (targettype == ST_Corpse) { //15 Corpse + isSingleTargetSpell = true; + } + + if (targettype == ST_Plant) { //16 Plant + isSingleTargetSpell = true; + } + + if (targettype == ST_Giant) { //17 Uber Giants + isSingleTargetSpell = true; + } + + if (targettype == ST_Dragon) { //18 Uber Dragons + isSingleTargetSpell = true; + } + + if (spDat.mana > 0 && manaCurrent < GetActSpellCost(spellID, spDat.mana)) return ELIXIR_NOT_ENOUGH_MANA; + if (spDat.endurance_cost > 0 && endCurrent < spDat.endurance_cost) return ELIXIR_NOT_ENOUGH_ENDURANCE; + + if (isLull) return ELIXIR_LULL_IGNORED; + if (isMez) return ELIXIR_MEZ_IGNORED; + if (isCharm) return ELIXIR_CHARM_IGNORED; + + if (targettype == ST_Animal) { //16 Animal + if (target == nullptr) return ELIXIR_NO_TARGET; + if (targetBodyType != BT_Animal) return ELIXIR_INVALID_TARGET_BODYTYPE; + } + + if (targettype == ST_Undead ) { //10 Undead + if (target == nullptr) return ELIXIR_NO_TARGET; + if (targetBodyType != BT_Undead) return ELIXIR_INVALID_TARGET_BODYTYPE; + } + + if (targettype == ST_Summoned) { //11 Summoned + if (target == nullptr) return ELIXIR_NO_TARGET; + if (targetBodyType != BT_Summoned) return ELIXIR_INVALID_TARGET_BODYTYPE; + } + + if (targettype == ST_Plant) { //Plant + if (target == nullptr) return ELIXIR_NO_TARGET; + if (targetBodyType != BT_Plant) return ELIXIR_INVALID_TARGET_BODYTYPE; + } + + if (isTransport) return ELIXIR_TRANSPORT_IGNORED; + + if (spDat.npc_no_los == 0 && target && isSingleTargetSpell && CheckLosFN(target)) return ELIXIR_NOT_LINE_OF_SIGHT; + + for (int i = 0; i < 4; i++) { // Reagent check + if (spDat.component[i] == 0) continue; + if (spDat.component_count[i] == -1) continue; + if (IsMerc()) continue; //mercs don't have inventory nor require reagents +#ifdef BOTS + if (IsBot()) continue; //bots don't have inventory nor require reagents +#endif + return ELIXIR_COMPONENT_REQUIRED; + //TODO: teach elixir how to check inventory for component in cases it's a client calling this function + } + + //TODO: CasterRequirement logic + //DWORD ReqID = pSpell->CasterRequirementID; + //if (ReqID == 518 && SpawnPctHPs(pChar->pSpawn) > 89) return "not < 90% hp"; + + + if (skillID == EQ::skills::SkillBackstab) { // 8 backstab + if (!target) { + return ELIXIR_NO_TARGET; + } + if (!BehindMob(target)) { + return ELIXIR_NOT_NEEDED; + } + if (!IsWithinSpellRange(target, spDat.range, spellID)) { + return ELIXIR_OUT_OF_RANGE; + } + return 0; + } + + if (recourseID > 0) { //recourse buff attached + const SPDat_Spell_Struct &spDatRecourse = spells[recourseID]; + if (spDatRecourse.buff_duration > 0) { + buffCount = GetMaxTotalSlots(); + for (uint32 i = 0; i < buffCount; i++) { + auto buff = buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.ticsremaining < 2) continue; + if (buff.spellid == recourseID) { + return ELIXIR_ALREADY_HAVE_BUFF; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, recourseID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + return ELIXIR_ALREADY_HAVE_BUFF; + } + } + } + } + + if (ticks > 0 && !IsBeneficialSpell(spellID) && targettype == ST_Target) { // debuff + if (!target) { + return ELIXIR_NO_TARGET; + } + + buffCount = target->GetMaxTotalSlots(); + for (uint32 i = 0; i < buffCount; i++) { + auto buff = target->buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.ticsremaining < 2) continue; + if (buff.spellid == spellID) { + return ELIXIR_ALREADY_HAVE_BUFF; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + return ELIXIR_ALREADY_HAVE_BUFF; + } + } + } + + if (spDat.zone_type == 1 && !zone->CanCastOutdoor()) { + return ELIXIR_ZONETYPE_FAIL; + } + + //TODO: zone_type 2 check (can't cast outdoors indoor only) + + if (IsEffectInSpell(spellID, SE_Levitate) && !zone->CanLevitate()) { + return ELIXIR_ZONETYPE_FAIL; + } + + if (!spDat.can_cast_in_combat) { + if (IsEngaged()) return ELIXIR_CANNOT_USE_IN_COMBAT; + buffCount = target->GetMaxTotalSlots(); + for (uint32 i = 0; i < buffCount; i++) { + auto buff = target->buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (IsDetrimentalSpell(buff.spellid) && buff.ticsremaining > 0 && !DetrimentalSpellAllowsRest(buff.spellid)) { + return ELIXIR_CANNOT_USE_IN_COMBAT; + } + } + } + + if (IsDisciplineBuff(spellID)) { + buffCount = GetMaxTotalSlots(); + for (uint32 i = 0; i < buffCount; i++) { + auto buff = buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.ticsremaining < 2) continue; + if (buff.spellid == spellID) { + return ELIXIR_ALREADY_HAVE_BUFF; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + return ELIXIR_ALREADY_HAVE_BUFF; + } + } + } + + if (isPetSummon && HasPet()) return ELIXIR_ALREADY_HAVE_PET; + + + if (targettype == ST_Pet && isHeal) { + if (!HasPet()) { + return ELIXIR_NO_PET; + } + if (GetPet()->GetHPRatio() <= healPercent) { + return 0; + } + return ELIXIR_NOT_NEEDED; + } + + if (isBuff && targettype == ST_Pet && IsBeneficialSpell(spellID)) { + if (!HasPet()) { + return ELIXIR_NO_PET; + } + + buffCount = GetPet()->GetMaxTotalSlots(); + for (uint32 i = 0; i < buffCount; i++) { + auto buff = GetPet()->buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.ticsremaining < 2) continue; + if (buff.spellid == spellID) { + return ELIXIR_ALREADY_HAVE_BUFF; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + return ELIXIR_ALREADY_HAVE_BUFF; + } + if (!IsWithinSpellRange(GetPet(), spDat.range, spellID)) { + return ELIXIR_OUT_OF_RANGE; + } + return 0; + } + } + + if (isMana && targettype == ST_Self && ticks <= 0 && !isPetSummon) { // self only regen, like harvest, canni + if (stunDuration > 0 && IsEngaged()) { + return ELIXIR_CANNOT_USE_IN_COMBAT; + } + + if (GetManaRatio() > 50) { + return ELIXIR_NOT_NEEDED; + } + + return 0; + } + + if (isLifetap && GetHPRatio() <= healPercent) { + //TODO: check if it's a group recourse lifetap regen so necros can bond heal group + return ELIXIR_NOT_NEEDED; + } + + + if (isGroupSpell && isHeal && ticks == 0) { //self/group instant heals + int groupHealCount = 0; + + float sqDistance = spDat.aoe_range * spDat.aoe_range; + for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (!grp) break; + if (!grp->members[i]) continue; + if (grp->members[i]->GetHPRatio() > healPercent) continue; + if (sqDistance > 0 && DistanceSquaredNoZ(target->GetPosition(), grp->members[i]->GetPosition()) > sqDistance) continue; + groupHealCount++; + } + + if (groupHealCount < aeMinimum) { + return ELIXIR_NOT_NEEDED; + } + return 0; + } + + + if ((targettype == ST_Self || isGroupSpell) && IsBeneficialSpell(spellID)) { //self/group beneficial spell + if (IsEngaged() && !isBardSong) { + return ELIXIR_CANNOT_USE_IN_COMBAT; + } + + if (ticks > 0) { + bool isBuffNeeded = true; + + buffCount = GetMaxTotalSlots(); + for (uint32 i = 0; i < buffCount; i++) { + auto buff = buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.ticsremaining < 2) continue; + if (buff.spellid == spellID) { + isBuffNeeded = false; + break; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + isBuffNeeded = false; + break; + } + } + if (isBuffNeeded) return 0; + if (!isGroupSpell) return ELIXIR_NOT_NEEDED; + + isBuffNeeded = true; + for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { + + if (!grp) break; + if (!grp->members[i]) continue; + buffCount = grp->members[i]->GetMaxTotalSlots(); + for (uint32 i = 0; i < buffCount; i++) { + auto buff = buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.ticsremaining < 2) continue; + if (buff.spellid == spellID) { + isBuffNeeded = false; + break; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + isBuffNeeded = false; + break; + } + } + if (!isBuffNeeded) continue; + outMob = grp->members[i]; + return 1; + } + return ELIXIR_NOT_NEEDED; + } + + return 0; + } + + if (targettype == ST_Target || IsBeneficialSpell(spellID)) { //single target beneficial spell + if (isHeal) { //heal logic + int healIDPercent = 100; + // figure out who is lowest HP party member + if (GetHPRatio() <= healPercent) { + if (ticks == 0) { // instant heal, just apply + outMob = this; + healIDPercent = GetHPRatio(); + } else { // it's a heal buff, check if player already has it + bool isBuffNeeded = true; + for (uint32 i = 0; i < buffCount; i++) { + auto buff = buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.spellid == spellID) { + isBuffNeeded = false; + break; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + isBuffNeeded = false; + break; + } + } + if (isBuffNeeded) { + outMob = this; + healIDPercent = GetHPRatio(); + } + } + } + + for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (!grp) break; + if (!grp->members[i]) continue; + if (grp->members[i]->GetHPRatio() > healPercent) continue; + if (grp->members[i]->GetHPRatio() > healIDPercent) continue; + if (!IsWithinSpellRange(grp->members[i], spDat.range, spellID)) continue; + + if (ticks == 0) { // instant heal, just apply + outMob = grp->members[i]; + healIDPercent = grp->members[i]->GetHPRatio(); + } else { // it's a heal buff, check if player already has it + bool isBuffNeeded = true; + for (uint32 i = 0; i < buffCount; i++) { + auto buff = grp->members[i]->buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.spellid == spellID) { + isBuffNeeded = false; + break; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + isBuffNeeded = false; + break; + } + // TODO: Immune Check + } + if (isBuffNeeded) { + outMob = grp->members[i]; + healIDPercent = GetHPRatio(); + } + } + } + if (!outMob) { + return ELIXIR_NOT_NEEDED; + } + return 1; + } + + // TODO: add exceptions for combat buffs situation + bool isCombatBuff = false; + + if (IsEngaged() && !isCombatBuff) { + return ELIXIR_NOT_NEEDED; + } + + // for the time being, any beneficial non-heal single target spells without a duration are skipped + // later, we need to add in things like necro mana flow, etc + if (ticks == 0) { + return ELIXIR_NOT_NEEDED; + } + + // always self buff first + bool isBuffNeeded = true; + for (uint32 i = 0; i < buffCount; i++) { + auto buff = buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.spellid == spellID) { + isBuffNeeded = false; + break; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + isBuffNeeded = false; + break; + } + // TODO: Immune Check + } + if (isBuffNeeded) { + if (target && target->GetID() == GetID()) { + return 0; + } + outMob = this; + return 1; + } + + for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (!grp) break; + if (!grp->members[i]) continue; + if (!IsWithinSpellRange(grp->members[i], spDat.range, spellID)) continue; + + bool isBuffNeeded = true; + for (uint32 i = 0; i < buffCount; i++) { + auto buff = grp->members[i]->buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.spellid == spellID) { + isBuffNeeded = false; + break; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + isBuffNeeded = false; + break; + } + // TODO: Immune Check + } + if (isBuffNeeded) { + if (target && target->GetID() == grp->members[i]->GetID()) { + return 0; + } + outMob = grp->members[i]; + return 1; + } + } + + return ELIXIR_NOT_NEEDED; + } + + if (damageAmount > 0 && (targettype == ST_AEClientV1 || targettype == ST_AreaClientOnly || targettype == ST_AECaster)) { // PB AE DD + int targetCount = 0; + float sqDistance = spDat.aoe_range * spDat.aoe_range; + auto hates = GetHateList(); + auto iter = hates.begin(); + for (auto iter : hates) { + if (sqDistance > 0 && DistanceSquaredNoZ(GetPosition(), iter->entity_on_hatelist->GetPosition()) > sqDistance) continue; + targetCount++; + } + if (targetCount < aeMinimum) { + return ELIXIR_NOT_NEEDED; + } + + return 0; + } + + + if (damageAmount > 0 && (targettype == ST_TargetAETap || targettype == ST_AETarget)) { // Target AE DD + if (!target) { + return ELIXIR_NO_TARGET; + } + if (!IsWithinSpellRange(target, spDat.range, spellID)) { + return ELIXIR_OUT_OF_RANGE; + } + + int targetCount = 0; + float sqDistance = spDat.aoe_range * spDat.aoe_range; + auto hates = GetHateList(); + auto iter = hates.begin(); + for (auto iter : hates) { + if (sqDistance > 0 && DistanceSquaredNoZ(target->GetPosition(), iter->entity_on_hatelist->GetPosition()) > sqDistance) continue; + targetCount++; + } + if (targetCount < aeMinimum) { + return ELIXIR_NOT_NEEDED; + } + + return 0; + } + + + if (targettype == ST_Target || !IsBeneficialSpell(spellID)) { // single target detrimental spell + if (!hate_list.IsEntOnHateList(target)) { + return ELIXIR_NO_TARGET; + } + + if (target->GetHPRatio() <= 0) { + return ELIXIR_NOT_NEEDED; + } + + if (!IsWithinSpellRange(GetPet(), spDat.range, spellID)) { + return ELIXIR_OUT_OF_RANGE; + } + + if (target->IsMezzed()) { + return ELIXIR_NOT_NEEDED; + } + + if (!IsAttackAllowed(target)) { + return ELIXIR_NOT_NEEDED; + } + + if (ticks == 0) { + if (!target) { + return ELIXIR_NO_TARGET; + } + return 0; + } + + for (uint32 i = 0; i < buffCount; i++) { + auto buff = target->buffs[i]; + if (buff.spellid == SPELL_UNKNOWN) continue; + if (spells[buff.spellid].buff_duration_formula == DF_Permanent) continue; + if (buff.spellid == spellID) { + return ELIXIR_NOT_NEEDED; + } + int stackResult = CheckStackConflict(buff.spellid, buff.casterlevel, spellID, GetLevel(), entity_list.GetMobID(buff.casterid), this, i); + if (stackResult == -1) { + return ELIXIR_NOT_NEEDED; + } + // TODO: Immune Check + } + return 0; + } + + return ELIXIR_UNHANDLED_SPELL; +} + +/* + for (int i = 0; i < MAX_RAID_MEMBERS; i++) { + if (!raid) break; + if (!raid->members[i]) continue; + if (!raid->members[i].member) continue; + //raid->members[i].GroupNumber == gid + } +*/ \ No newline at end of file diff --git a/zone/elixir.h b/zone/elixir.h new file mode 100644 index 000000000..18742bafd --- /dev/null +++ b/zone/elixir.h @@ -0,0 +1,21 @@ +enum ElixirError { + ELIXIR_UNHANDLED_SPELL = -1, + ELIXIR_CANNOT_CAST_BAD_STATE = -2, + ELIXIR_NOT_ENOUGH_MANA = -3, + ELIXIR_LULL_IGNORED = -4, + ELIXIR_MEZ_IGNORED = -5, + ELIXIR_CHARM_IGNORED = -6, + ELIXIR_NO_TARGET = -7, + ELIXIR_INVALID_TARGET_BODYTYPE = -8, + ELIXIR_TRANSPORT_IGNORED = -9, + ELIXIR_NOT_LINE_OF_SIGHT = -10, + ELIXIR_COMPONENT_REQUIRED = -11, + ELIXIR_ALREADY_HAVE_BUFF = -12, + ELIXIR_ZONETYPE_FAIL = -13, + ELIXIR_CANNOT_USE_IN_COMBAT = -14, + ELIXIR_NOT_ENOUGH_ENDURANCE = -15, + ELIXIR_ALREADY_HAVE_PET = -16, + ELIXIR_OUT_OF_RANGE = -17, + ELIXIR_NO_PET = -18, + ELIXIR_NOT_NEEDED = -19, +}; \ No newline at end of file diff --git a/zone/merc.cpp b/zone/merc.cpp index 306c1aeec..8958a5d10 100644 --- a/zone/merc.cpp +++ b/zone/merc.cpp @@ -2020,6 +2020,10 @@ bool Merc::AICastSpell(int8 iChance, uint32 iSpellTypes) { if(!AI_HasSpells()) return false; + if (RuleB(Mercs, IsMercsElixirEnabled)) { + return ElixirAIDetermineSpellToCast(); + } + if (iChance < 100) { if (zone->random.Int(0, 100) > iChance){ return false; @@ -6362,3 +6366,155 @@ uint32 Merc::CalcUpkeepCost(uint32 templateID , uint8 level, uint8 currency_type return cost; } + + +// ElixirAIDetermineSpellToCast is called during Merc::AICastSpell and overrides normal logic +// It determines by class which spell to cast +bool Merc::ElixirAIDetermineSpellToCast() { + MercSpell selectedMercSpell; + int8 spellAIResult; + Group *grp = GetGroup(); + + switch (GetClass()) { + case HEALER: + selectedMercSpell = GetBestMercSpellForGroupHeal(this); + if (ElixirAITryCastSpell(selectedMercSpell, true)) { + return true; + } + + selectedMercSpell = GetBestMercSpellForHealOverTime(this); + if (ElixirAITryCastSpell(selectedMercSpell, true)) { + return true; + } + + selectedMercSpell = GetBestMercSpellForFastHeal(this); + if (ElixirAITryCastSpell(selectedMercSpell, true)) { + return true; + } + + selectedMercSpell = GetBestMercSpellForRegularSingleTargetHeal(this); + if (ElixirAITryCastSpell(selectedMercSpell, true)) { + return true; + } + + for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (!grp) break; + if (!grp->members[i]) continue; + if (!grp->members[i]->qglobal) continue; + if(!GetNeedsCured(grp->members[i])) continue; + if (grp->members[i]->DontCureMeBefore() > Timer::GetCurrentTime()) continue; + selectedMercSpell = GetBestMercSpellForCure(this, grp->members[i]); + if (ElixirAITryCastSpell(selectedMercSpell, true)) { + return true; + } + } + + if (GetManaRatio() > 50) { // healers only offensive or buff at > 50% mana + selectedMercSpell = GetBestMercSpellForStun(this); + if (ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + + selectedMercSpell = GetBestMercSpellForNuke(this); + if (ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + + auto buffSpells = GetMercSpellsBySpellType(this, SpellType_Buff); + for (auto buffSpell : buffSpells) { + if (!ElixirAITryCastSpell(selectedMercSpell)) continue; + return true; + } + } + return false; + case MELEEDPS: + if (GetTarget() && HasOrMayGetAggro()) { + selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_Escape); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + } + + selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_Nuke); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + + selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_InCombatBuff); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + return false; + case TANK: + if(CheckAETaunt()) { + selectedMercSpell = GetBestMercSpellForAETaunt(this); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + Log(Logs::General, Logs::Mercenaries, "%s AE Taunting.", GetName()); + return true; + } + } + + if(CheckTaunt()) { + selectedMercSpell = GetBestMercSpellForTaunt(this); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + } + + selectedMercSpell = GetBestMercSpellForHate(this); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + + selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_Nuke); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + + selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_InCombatBuff); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + return false; + case CASTERDPS: + if (GetTarget() && HasOrMayGetAggro()) { + selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_Escape); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + } + + selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_Nuke); + if (selectedMercSpell.spellid > 0 && ElixirAITryCastSpell(selectedMercSpell)) { + return true; + } + } + return false; +} + +// ElixirAITryCastSpell takes a provided spell id and does a spell check to determine if the spell is valid +// Once valid, it will cast on returned mob candidate +bool Merc::ElixirAITryCastSpell(MercSpell mercSpell, bool isHeal) { + auto spellID = mercSpell.spellid; + if (spellID == 0) return false; + + Mob* outMob; + auto spellAIResult = ElixirCastSpellCheck(spellID, outMob); + + if (spellAIResult < 0) return false; + + if (spellAIResult == 0) { + AIDoSpellCast(spellID, GetTarget(), -1); + if (GetTarget() == this) return true; + if (isHeal) MercGroupSay(this, "Casting %s on %s.", spells[spellID].name, GetTarget()->GetCleanName()); + return true; + } + + if (outMob == nullptr) { + return false; + } + AIDoSpellCast(spellID, outMob, -1); + if (outMob == this) return true; + if (isHeal) MercGroupSay(this, "Casting %s on %s.", spells[spellID].name, outMob->GetCleanName()); + return true; +} \ No newline at end of file diff --git a/zone/merc.h b/zone/merc.h index 44ea7dda9..dd6c736b0 100644 --- a/zone/merc.h +++ b/zone/merc.h @@ -178,6 +178,8 @@ public: bool CheckAETaunt(); bool CheckConfidence(); bool TryHide(); + bool ElixirAIDetermineSpellToCast(); + bool ElixirAITryCastSpell(MercSpell mercSpell, bool isHeal = false); // stat functions virtual void ScaleStats(int scalepercent, bool setmax = false); diff --git a/zone/mob.h b/zone/mob.h index 327f73f9c..1f534524c 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -354,7 +354,8 @@ public: void CalcDestFromHeading(float heading, float distance, float MaxZDiff, float StartX, float StartY, float &dX, float &dY, float &dZ); void BeamDirectional(uint16 spell_id, int16 resist_adjust); void ConeDirectional(uint16 spell_id, int16 resist_adjust); - void TryOnSpellFinished(Mob *caster, Mob *target, uint16 spell_id); + void TryOnSpellFinished(Mob *caster, Mob *target, uint16 spell_id); + bool IsWithinSpellRange(Mob *target, float spellRange, uint16 spellID); //Buff void BuffProcess(); @@ -854,7 +855,7 @@ public: inline bool HasBaseEffectFocus() const { return (spellbonuses.FocusEffects[focusFcBaseEffects] || aabonuses.FocusEffects[focusFcBaseEffects] || itembonuses.FocusEffects[focusFcBaseEffects]); } int32 GetDualWieldingSameDelayWeapons() const { return dw_same_delay; } inline void SetDualWieldingSameDelayWeapons(int32 val) { dw_same_delay = val; } - + int8 ElixirCastSpellCheck(uint16 spellID, Mob* outMob); bool TryDoubleMeleeRoundEffect(); bool GetUseDoubleMeleeRoundDmgBonus() const { return use_double_melee_round_dmg_bonus; } inline void SetUseDoubleMeleeRoundDmgBonus(bool val) { use_double_melee_round_dmg_bonus = val; } diff --git a/zone/spells.cpp b/zone/spells.cpp index b38d23486..01a05c6e9 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -6327,3 +6327,24 @@ int Client::GetNextAvailableDisciplineSlot(int starting_slot) { return -1; // Return -1 if No Slots open } + +// IsWithinRange returns true if target is within range of spell ID casted by mob +bool Mob::IsWithinSpellRange(Mob *target, float spellRange, uint16 spellID) { + float range = GetActSpellRange(spellID, spellRange, false); + if (!target) { + return false; + } + + if (target->GetID() == GetID()) return true; + + float dist2 = DistanceSquared(GetPosition(), target->GetPosition()); + float range2 = spellRange * spellRange; + float min_range2 = spells[spellID].min_range * spells[spellID].min_range; + if(dist2 > range2) { + return false; + } + if (dist2 < min_range2){ + return false; + } + return true; +} \ No newline at end of file