mirror of
https://github.com/araxiaonline/AscEmu.git
synced 2026-06-13 03:02:22 -04:00
V811 Decreased performance. Excessive type casting: string -> char * -> string. Consider inspecting third argument of the function sendChatMessage. AIInterface.cpp 3746 V811 Decreased performance. Excessive type casting: string -> char * -> string. Consider inspecting third argument of the function sendChatMessage. AIInterface.cpp 3849 V823 Decreased performance. Object may be created in-place in the 'm_specs[s].talents' container. Consider replacing methods: 'insert' -> 'emplace'. Player.cpp 14462 V823 Decreased performance. Object may be created in-place in the 'mAISpellEmote' container. Consider replacing methods: 'push_back' -> 'emplace_back'. AIInterface.cpp 3736 V836 Expression's value is copied at the 'wayPoint' variable declaration. The variable is never modified. Consider declaring it as a reference. AIInterface.cpp 3381 V836 Expression's value is copied at the 'wayPoint' variable declaration. The variable is never modified. Consider declaring it as a reference. AIInterface.cpp 3429 V836 Expression's value is copied at the 'cd' variable declaration. The variable is never modified. Consider declaring it as a reference. SmsgInitialSpells.h 100
4336 lines
138 KiB
C++
4336 lines
138 KiB
C++
/*
|
|
Copyright (c) 2014-2023 AscEmu Team <http://www.ascemu.org>
|
|
This file is released under the MIT license. See README-MIT for more information.
|
|
*/
|
|
|
|
#ifndef UNIX
|
|
#include <cmath>
|
|
#endif
|
|
|
|
#include "VMapFactory.h"
|
|
#include "MMapManager.h"
|
|
#include "MMapFactory.h"
|
|
#include "Objects/Units/Stats.h"
|
|
#include "Storage/MySQLDataStore.hpp"
|
|
#include "Storage/MySQLStructures.h"
|
|
#include "Map/Management/MapMgr.hpp"
|
|
#include "Management/Faction.h"
|
|
#include "Spell/SpellMgr.hpp"
|
|
#include "Macros/AIInterfaceMacros.hpp"
|
|
#include "Spell/Definitions/SpellCastTargetFlags.hpp"
|
|
#include "Spell/Definitions/SpellRanged.hpp"
|
|
#include "Spell/Definitions/LockTypes.hpp"
|
|
#include "Spell/Definitions/SpellIsFlags.hpp"
|
|
#include "Spell/Definitions/PowerType.hpp"
|
|
#include "Pet.h"
|
|
#include "Spell/Definitions/SpellEffects.hpp"
|
|
#include "Management/ObjectMgr.h"
|
|
#include "Map/AreaBoundary.hpp"
|
|
#include "Map/Maps/MapScriptInterface.h"
|
|
#include "Movement/WaypointManager.h"
|
|
#include "Movement/MovementManager.h"
|
|
#include "Movement/Spline/MoveSplineInit.h"
|
|
#include "Server/Definitions.h"
|
|
#include "Server/Script/CreatureAIScript.h"
|
|
#include "Objects/Units/Creatures/CreatureGroups.h"
|
|
#include "Storage/DBC/DBCStores.h"
|
|
|
|
// Random and guessed values for Internal Spell cast chance
|
|
float spellChanceModifierDispell[12] =
|
|
{
|
|
1.4f, // None
|
|
2.5f, // Magic
|
|
1.8f, // Curse
|
|
1.5f, // Diseas
|
|
1.5f, // Poison
|
|
1.0f, // Stealth
|
|
1.0f, // Invis
|
|
2.8f, // All
|
|
1.0f, // Special
|
|
2.0f, // Frenzy
|
|
1.0f, // Unk
|
|
1.0f, // Unk
|
|
};
|
|
|
|
float spellChanceModifierType[12] =
|
|
{
|
|
1.0f, // None
|
|
0.75f, // Rooted
|
|
0.75f, // Heal
|
|
0.75f, // Stun
|
|
0.50f, // Fear
|
|
0.75f, // Silence
|
|
0.95f, // Curse
|
|
0.95f, // AOE Damage
|
|
0.95f, // Damage
|
|
0.75f, // Summon
|
|
1.0f, // Buff
|
|
1.0f, // Debuff
|
|
};
|
|
|
|
AIInterface::AIInterface()
|
|
:
|
|
m_canRangedAttack(false),
|
|
m_canFlee(false),
|
|
m_FleeHealth(0.0f),
|
|
m_FleeDuration(0),
|
|
m_canCallForHelp(false),
|
|
m_CallForHelpHealth(0.0f),
|
|
|
|
m_AiCurrentAgent(AGENT_NULL),
|
|
m_hasFleed(false),
|
|
m_AlreadyCallAssistance(false),
|
|
m_AlreadySearchedAssistance(false),
|
|
m_isEngaged(false),
|
|
m_reactState(REACT_AGGRESSIVE),
|
|
|
|
mIsCombatDisabled(false),
|
|
mIsMeleeDisabled(false),
|
|
mIsRangedDisabled(false),
|
|
mIsCastDisabled(false),
|
|
mIsTargetingDisabled(false),
|
|
m_lasttargetPosition(0, 0, 0, 0),
|
|
|
|
timed_emotes(nullptr),
|
|
|
|
m_Unit(nullptr),
|
|
m_PetOwner(nullptr),
|
|
m_target(nullptr),
|
|
m_isNeutralGuard(false),
|
|
faction_visibility(0),
|
|
m_is_in_instance(false),
|
|
internalPhase(0),
|
|
|
|
m_boundaryCheckTime(2500),
|
|
_negateBoundary(false),
|
|
|
|
mShowWayPoints(false),
|
|
m_UnitToFollow(nullptr),
|
|
|
|
m_totemspelltimer(0),
|
|
m_totemspelltime(0),
|
|
totemspell(nullptr),
|
|
|
|
canEnterCombat(true),
|
|
timed_emote_expire(0xFFFFFFFF)
|
|
{
|
|
_boundary.clear();
|
|
m_assistTargets.clear();
|
|
|
|
onLoadScripts.clear();
|
|
onCombatStartScripts.clear();
|
|
onAIUpdateScripts.clear();
|
|
onLeaveCombatScripts.clear();
|
|
onDiedScripts.clear();
|
|
onKilledScripts.clear();
|
|
onCallForHelpScripts.clear();
|
|
onDamageTakenScripts.clear();
|
|
onRandomWaypointScripts.clear();
|
|
onFleeScripts.clear();
|
|
|
|
mEmotesOnLoad.clear();
|
|
mEmotesOnCombatStart.clear();
|
|
mEmotesOnLeaveCombat.clear();
|
|
mEmotesOnTargetDied.clear();
|
|
mEmotesOnAIUpdate.clear();
|
|
mEmotesOnDied.clear();
|
|
mEmotesOnDamageTaken.clear();
|
|
mEmotesOnCallForHelp.clear();
|
|
mEmotesOnFlee.clear();
|
|
mEmotesOnRandomWaypoint.clear();
|
|
|
|
mLastCastedSpell = nullptr;
|
|
mCurrentSpellTarget = nullptr;
|
|
setCannotReachTarget(false);
|
|
m_fleeTimer.resetInterval(0);
|
|
mSpellWaitTimer.resetInterval(1500);
|
|
m_cannotReachTimer.resetInterval(500);
|
|
m_noTargetTimer.resetInterval(4000);
|
|
m_updateAssistTimer.resetInterval(1500);
|
|
m_updateTargetTimer.resetInterval(1500);
|
|
};
|
|
|
|
AIInterface::~AIInterface()
|
|
{
|
|
mLastCastedSpell = nullptr;
|
|
mCurrentSpellTarget = nullptr;
|
|
mCreatureAISpells.clear();
|
|
clearBoundary();
|
|
onLoadScripts.clear();
|
|
onCombatStartScripts.clear();
|
|
onAIUpdateScripts.clear();
|
|
onLeaveCombatScripts.clear();
|
|
onDiedScripts.clear();
|
|
onKilledScripts.clear();
|
|
onCallForHelpScripts.clear();
|
|
onDamageTakenScripts.clear();
|
|
onRandomWaypointScripts.clear();
|
|
onFleeScripts.clear();
|
|
|
|
mEmotesOnLoad.clear();
|
|
mEmotesOnCombatStart.clear();
|
|
mEmotesOnLeaveCombat.clear();
|
|
mEmotesOnTargetDied.clear();
|
|
mEmotesOnAIUpdate.clear();
|
|
mEmotesOnDied.clear();
|
|
mEmotesOnDamageTaken.clear();
|
|
mEmotesOnCallForHelp.clear();
|
|
mEmotesOnFlee.clear();
|
|
mEmotesOnRandomWaypoint.clear();
|
|
}
|
|
|
|
void AIInterface::Init(Unit* un, Unit* owner /*= nullptr*/ )
|
|
{
|
|
m_Unit = un;
|
|
m_PetOwner = owner;
|
|
}
|
|
|
|
void AIInterface::initialiseScripts(uint32_t entry)
|
|
{
|
|
// Reset AI Spells
|
|
if (mCreatureAISpells.size())
|
|
{
|
|
for (auto spell : mCreatureAISpells)
|
|
{
|
|
spell->mCooldownTimer.resetInterval(spell->mCooldown);
|
|
spell->setCastCount(0);
|
|
}
|
|
}
|
|
|
|
onLoadScripts.clear();
|
|
onCombatStartScripts.clear();
|
|
onAIUpdateScripts.clear();
|
|
onLeaveCombatScripts.clear();
|
|
onDiedScripts.clear();
|
|
onKilledScripts.clear();
|
|
onCallForHelpScripts.clear();
|
|
onDamageTakenScripts.clear();
|
|
onRandomWaypointScripts.clear();
|
|
onFleeScripts.clear();
|
|
|
|
mEmotesOnLoad.clear();
|
|
mEmotesOnCombatStart.clear();
|
|
mEmotesOnLeaveCombat.clear();
|
|
mEmotesOnTargetDied.clear();
|
|
mEmotesOnAIUpdate.clear();
|
|
mEmotesOnDied.clear();
|
|
mEmotesOnDamageTaken.clear();
|
|
mEmotesOnCallForHelp.clear();
|
|
mEmotesOnFlee.clear();
|
|
mEmotesOnRandomWaypoint.clear();
|
|
|
|
setCanCallForHelp(false);
|
|
setCanFlee(false);
|
|
|
|
auto scripts = sMySQLStore.getCreatureAiScripts(entry);
|
|
|
|
uint32_t spellcountOnCombatStart = 1;
|
|
uint32_t spellcountOnAIUpdate = 1;
|
|
|
|
for (auto aiScripts : *scripts)
|
|
{
|
|
uint8_t eventId = aiScripts.event;
|
|
|
|
// Skip not in Current Difficulty
|
|
if (aiScripts.difficulty != 4 && aiScripts.difficulty != getDifficultyType())
|
|
continue;
|
|
|
|
switch (eventId)
|
|
{
|
|
case onLoad:
|
|
{
|
|
onLoadScripts.push_back(aiScripts);
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnLoad.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onEnterCombat:
|
|
{
|
|
onCombatStartScripts.push_back(aiScripts);
|
|
if (aiScripts.spellId)
|
|
++spellcountOnCombatStart;
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnCombatStart.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onLeaveCombat:
|
|
{
|
|
onLeaveCombatScripts.push_back(aiScripts);
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnLeaveCombat.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onDied:
|
|
{
|
|
onDiedScripts.push_back(aiScripts);
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnDied.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onTargetDied:
|
|
{
|
|
onKilledScripts.push_back(aiScripts);
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnTargetDied.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onAIUpdate:
|
|
{
|
|
onAIUpdateScripts.push_back(aiScripts);
|
|
if (aiScripts.spellId)
|
|
++spellcountOnAIUpdate;
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnAIUpdate.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onCallForHelp:
|
|
{
|
|
onCallForHelpScripts.push_back(aiScripts);
|
|
setCanCallForHelp(true);
|
|
m_CallForHelpHealth = aiScripts.maxHealth;
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnCallForHelp.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onRandomWaypoint:
|
|
{
|
|
onRandomWaypointScripts.push_back(aiScripts);
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnRandomWaypoint.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onDamageTaken:
|
|
{
|
|
onDamageTakenScripts.push_back(aiScripts);
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnDamageTaken.push_back(std::move(message));
|
|
}
|
|
}
|
|
break;
|
|
case onFlee:
|
|
{
|
|
onFleeScripts.push_back(aiScripts);
|
|
setCanFlee(true);
|
|
m_FleeHealth = aiScripts.maxHealth;
|
|
|
|
if (aiScripts.action == actionSendMessage)
|
|
{
|
|
std::shared_ptr<AI_SCRIPT_SENDMESSAGES> message = std::make_shared<AI_SCRIPT_SENDMESSAGES>();
|
|
|
|
message->textId = aiScripts.textId;
|
|
message->canche = aiScripts.chance;
|
|
message->phase = aiScripts.phase;
|
|
message->healthPrecent = aiScripts.maxHealth;
|
|
message->maxCount = aiScripts.maxCount;
|
|
|
|
mEmotesOnFlee.push_back(std::move(message));
|
|
}
|
|
|
|
// Incase we want a custom Flee Timer
|
|
if (aiScripts.misc1)
|
|
m_FleeDuration = aiScripts.misc1;
|
|
else
|
|
m_FleeDuration = 10000;
|
|
}
|
|
break;
|
|
default:
|
|
sLogger.debugFlag(AscEmu::Logging::LF_SCRIPT_MGR, "unhandled event with eventId %u", eventId);
|
|
}
|
|
}
|
|
|
|
// On Combat Start
|
|
for (auto onCombatStartScript : onCombatStartScripts)
|
|
{
|
|
if (onCombatStartScript.action == actionSpell)
|
|
{
|
|
const auto spellInfo = sSpellMgr.getSpellInfo(onCombatStartScript.spellId);
|
|
float castChance;
|
|
|
|
if (spellInfo != nullptr)
|
|
{
|
|
if (onCombatStartScript.chance)
|
|
castChance = onCombatStartScript.chance;
|
|
else
|
|
castChance = ((75.0f / static_cast<float_t>(spellcountOnCombatStart)) * spellChanceModifierDispell[spellInfo->getDispelType()] * spellChanceModifierType[onCombatStartScript.spell_type]);
|
|
|
|
sLogger.debug("spell %u chance %f", onCombatStartScript.spellId, castChance);
|
|
|
|
uint32_t spellCooldown = Util::getRandomUInt(onCombatStartScript.cooldownMin, onCombatStartScript.cooldownMax);
|
|
if (spellCooldown == 0)
|
|
spellCooldown = spellInfo->getSpellDefaultDuration(nullptr);
|
|
|
|
// Create AI Spell
|
|
CreatureAISpells* newAISpell = new CreatureAISpells(spellInfo, castChance, onCombatStartScript.target, spellInfo->getSpellDefaultDuration(nullptr), spellCooldown, false, onCombatStartScript.triggered);
|
|
newAISpell->addDBEmote(onCombatStartScript.textId);
|
|
newAISpell->setMaxCastCount(onCombatStartScript.maxCount);
|
|
newAISpell->scriptType = onCombatStartScript.event;
|
|
newAISpell->spell_type = AI_SpellType(onCombatStartScript.spell_type);
|
|
newAISpell->fromDB = true;
|
|
|
|
// Ready add to our List
|
|
if (!hasAISpell(onCombatStartScript.spellId))
|
|
mCreatureAISpells.push_back(newAISpell);
|
|
}
|
|
else
|
|
sLogger.debug("Tried to Register Creature AI Spell without a valid Spell Id %u", onCombatStartScript.spellId);
|
|
}
|
|
}
|
|
|
|
// On AI Update
|
|
for (auto onAIUpdateScript : onAIUpdateScripts)
|
|
{
|
|
if (onAIUpdateScript.action == actionSpell)
|
|
{
|
|
const auto spellInfo = sSpellMgr.getSpellInfo(onAIUpdateScript.spellId);
|
|
float castChance;
|
|
|
|
if (spellInfo != nullptr)
|
|
{
|
|
if (onAIUpdateScript.chance)
|
|
castChance = onAIUpdateScript.chance;
|
|
else
|
|
castChance = ((75.0f / static_cast<float_t>(spellcountOnAIUpdate)) * spellChanceModifierDispell[spellInfo->getDispelType()] * spellChanceModifierType[onAIUpdateScript.spell_type]);
|
|
|
|
sLogger.debug("spell %u chance %f", onAIUpdateScript.spellId, castChance);
|
|
|
|
uint32_t spellCooldown = Util::getRandomUInt(onAIUpdateScript.cooldownMin, onAIUpdateScript.cooldownMax);
|
|
if (spellCooldown == 0)
|
|
spellCooldown = spellInfo->getSpellDefaultDuration(nullptr);
|
|
|
|
// Create AI Spell
|
|
CreatureAISpells* newAISpell = new CreatureAISpells(spellInfo, castChance, onAIUpdateScript.target, spellInfo->getSpellDefaultDuration(nullptr), spellCooldown, false, onAIUpdateScript.triggered);
|
|
newAISpell->addDBEmote(onAIUpdateScript.textId);
|
|
newAISpell->setMaxCastCount(onAIUpdateScript.maxCount);
|
|
newAISpell->scriptType = onAIUpdateScript.event;
|
|
newAISpell->spell_type = AI_SpellType(onAIUpdateScript.spell_type);
|
|
newAISpell->fromDB = true;
|
|
|
|
if (onAIUpdateScript.maxHealth)
|
|
newAISpell->setMinMaxPercentHp(onAIUpdateScript.minHealth, onAIUpdateScript.maxHealth);
|
|
|
|
// Ready add to our List
|
|
if (!hasAISpell(onAIUpdateScript.spellId))
|
|
mCreatureAISpells.push_back(newAISpell);
|
|
}
|
|
else
|
|
sLogger.debug("Tried to Register Creature AI Spell without a valid Spell Id %u", onAIUpdateScript.spellId);
|
|
}
|
|
}
|
|
|
|
if (mEmotesOnCombatStart.size())
|
|
sLogger.debug("Creature with Entry %u has %i emotes on CombatStart", mEmotesOnCombatStart.size(), getUnit()->getEntry());
|
|
}
|
|
|
|
Unit* AIInterface::getUnit() const
|
|
{
|
|
return m_Unit;
|
|
}
|
|
|
|
Unit* AIInterface::getPetOwner() const
|
|
{
|
|
return m_PetOwner;
|
|
}
|
|
|
|
Unit* AIInterface::getCurrentTarget() const
|
|
{
|
|
return m_target;
|
|
}
|
|
|
|
void AIInterface::handleEvent(uint32_t event, Unit* pUnit, uint32_t misc1)
|
|
{
|
|
if (m_Unit == nullptr)
|
|
return;
|
|
|
|
if (event < NUM_AI_EVENTS && AIEventHandlers[event] != NULL)
|
|
(*this.*AIEventHandlers[event])(pUnit, misc1);
|
|
}
|
|
|
|
bool AIInterface::canUnitEvade(unsigned long time_passed)
|
|
{
|
|
// if we dont have a Valid target go in Evade Mode
|
|
if (!getCurrentTarget() && !getUnit()->isInEvadeMode())
|
|
{
|
|
m_noTargetTimer.updateTimer(time_passed);
|
|
if (m_noTargetTimer.isTimePassed())
|
|
{
|
|
m_noTargetTimer.resetInterval(4000);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// if we cannot reach the Target go in Evade Mode
|
|
if (canNotReachTarget() && !getUnit()->isInEvadeMode())
|
|
{
|
|
m_cannotReachTimer.updateTimer(time_passed);
|
|
if (m_cannotReachTimer.isTimePassed())
|
|
return true;
|
|
}
|
|
|
|
// periodic check to see if the creature has passed an evade boundary
|
|
if (!getUnit()->isInEvadeMode())
|
|
{
|
|
m_boundaryCheckTime.updateTimer(time_passed);
|
|
if (m_boundaryCheckTime.isTimePassed())
|
|
{
|
|
if (checkBoundary())
|
|
{
|
|
m_boundaryCheckTime.resetInterval(2500);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool AIInterface::_enterEvadeMode()
|
|
{
|
|
if (getUnit()->isInEvadeMode())
|
|
return false;
|
|
|
|
//if (getUnit()->isPet())
|
|
// return false; aaron02
|
|
|
|
if (!getUnit()->isAlive())
|
|
{
|
|
engagementOver();
|
|
handleEvent(EVENT_UNITDIED, getUnit(), 0);
|
|
return false;
|
|
}
|
|
|
|
engagementOver();
|
|
|
|
handleEvent(EVENT_LEAVECOMBAT, getUnit(), 0);
|
|
return true;
|
|
}
|
|
|
|
void AIInterface::enterEvadeMode()
|
|
{
|
|
if (!_enterEvadeMode())
|
|
return;
|
|
|
|
setNoCallAssistance(false);
|
|
|
|
// Clear tagger on evade
|
|
// Reset it here instead of engagementOver so it's not called on unit death
|
|
m_Unit->setTaggerGuid(0);
|
|
setCurrentTarget(nullptr);
|
|
|
|
if (m_Unit->isAlive())
|
|
{
|
|
if (getPetOwner())
|
|
{
|
|
if (m_Unit->isPet())
|
|
{
|
|
static_cast<Pet*>(m_Unit)->SetPetAction(PET_ACTION_FOLLOW);
|
|
if (m_Unit->isAlive() && m_Unit->IsInWorld())
|
|
{
|
|
static_cast<Pet*>(m_Unit)->HandleAutoCastEvent(AUTOCAST_EVENT_LEAVE_COMBAT);
|
|
}
|
|
}
|
|
handleEvent(EVENT_FOLLOWOWNER, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
getUnit()->addUnitStateFlag(UNIT_STATE_EVADING);
|
|
getUnit()->getMovementManager()->moveTargetedHome();
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIInterface::Update(unsigned long time_passed)
|
|
{
|
|
if (m_Unit->isPlayer() || m_Unit->getWorldMap() == nullptr)
|
|
return;
|
|
|
|
// Call AIUpdate
|
|
m_Unit->ToCreature()->CallScriptUpdate(time_passed);
|
|
|
|
if (getUnit()->hasUnitStateFlag(UNIT_STATE_FLEEING) || !m_Unit->isAIEnabled())
|
|
return;
|
|
|
|
if (getAllowedToEnterCombat())
|
|
{
|
|
updateTargets(time_passed);
|
|
|
|
// When we dont Have Any Targets do Nothing
|
|
if (!updateTarget())
|
|
return;
|
|
|
|
// Cast a Spell,Do Ranged or simply Melee
|
|
if (m_Unit->isTotem())
|
|
updateTotem(time_passed);
|
|
else
|
|
updateAgent(time_passed);
|
|
}
|
|
else
|
|
{
|
|
// Wipe targets
|
|
// Remove Combat
|
|
if (getUnit()->isInCombat())
|
|
enterEvadeMode();
|
|
}
|
|
|
|
// Timed Emotes
|
|
updateEmotes(time_passed);
|
|
}
|
|
|
|
void AIInterface::UpdateAgent(unsigned long time_passed)
|
|
{
|
|
mSpellWaitTimer.updateTimer(time_passed);
|
|
|
|
// Update Spells
|
|
if (getUnit()->isInCombat())
|
|
{
|
|
// Update Internal Spell Timers
|
|
for (auto spells : mCreatureAISpells)
|
|
{
|
|
if (!getUnit()->isCastingSpell())
|
|
spells->mCooldownTimer.updateTimer(time_passed);
|
|
|
|
spells->mDurationTimer.updateTimer(time_passed);
|
|
}
|
|
|
|
UpdateAISpells();
|
|
}
|
|
|
|
// On AIUpdate Scripts
|
|
for (auto itr = onAIUpdateScripts.begin(); itr != onAIUpdateScripts.end(); ++itr)
|
|
{
|
|
uint8_t actionId = itr->action;
|
|
|
|
switch (actionId)
|
|
{
|
|
case actionPhaseChange:
|
|
if (itr->phase > 0 || itr->phase == internalPhase)
|
|
{
|
|
if (float(getUnit()->getHealthPct()) <= itr->maxHealth && itr->maxCount)
|
|
{
|
|
internalPhase = static_cast<uint8_t>(itr->misc1);
|
|
|
|
itr->maxCount = itr->maxCount - 1U;
|
|
|
|
MySQLStructure::NpcScriptText const* npcScriptText = sMySQLStore.getNpcScriptText(itr->textId);
|
|
if (npcScriptText != nullptr)
|
|
{
|
|
getUnit()->sendChatMessage(npcScriptText->type, LANG_UNIVERSAL, npcScriptText->text);
|
|
|
|
if (npcScriptText->sound != 0)
|
|
getUnit()->PlaySoundToSet(npcScriptText->sound);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnAIUpdate, nullptr);
|
|
}
|
|
|
|
void AIInterface::castAISpell(CreatureAISpells* aiSpell)
|
|
{
|
|
Unit* target = getCurrentTarget();
|
|
switch (aiSpell->mTargetType)
|
|
{
|
|
case TARGET_SELF:
|
|
case TARGET_VARIOUS:
|
|
{
|
|
getUnit()->castSpell(getUnit(), aiSpell->mSpellInfo, aiSpell->mIsTriggered);
|
|
mLastCastedSpell = aiSpell;
|
|
} break;
|
|
case TARGET_ATTACKING:
|
|
{
|
|
getUnit()->castSpell(target, aiSpell->mSpellInfo, aiSpell->mIsTriggered);
|
|
mCurrentSpellTarget = target;
|
|
mLastCastedSpell = aiSpell;
|
|
} break;
|
|
case TARGET_SOURCE:
|
|
getUnit()->castSpellLoc(getUnit()->GetPosition(), aiSpell->mSpellInfo, aiSpell->mIsTriggered);
|
|
mLastCastedSpell = aiSpell;
|
|
break;
|
|
case TARGET_DESTINATION:
|
|
{
|
|
getUnit()->castSpellLoc(target->GetPosition(), aiSpell->mSpellInfo, aiSpell->mIsTriggered);
|
|
mCurrentSpellTarget = target;
|
|
mLastCastedSpell = aiSpell;
|
|
} break;
|
|
case TARGET_RANDOM_FRIEND:
|
|
case TARGET_RANDOM_SINGLE:
|
|
case TARGET_RANDOM_DESTINATION:
|
|
{
|
|
castSpellOnRandomTarget(aiSpell);
|
|
mLastCastedSpell = aiSpell;
|
|
} break;
|
|
case TARGET_CLOSEST:
|
|
{
|
|
mCurrentSpellTarget = getBestUnitTarget(TargetFilter_Closest);
|
|
mLastCastedSpell = aiSpell;
|
|
getUnit()->castSpell(mCurrentSpellTarget, aiSpell->mSpellInfo, aiSpell->mIsTriggered);
|
|
} break;
|
|
case TARGET_FURTHEST:
|
|
{
|
|
mCurrentSpellTarget = getBestUnitTarget(TargetFilter_InRangeOnly, 0.0f, 30.0f);
|
|
mLastCastedSpell = aiSpell;
|
|
getUnit()->castSpell(mCurrentSpellTarget, aiSpell->mSpellInfo, aiSpell->mIsTriggered);
|
|
} break;
|
|
case TARGET_CUSTOM:
|
|
{
|
|
// nos custom target set, no spell cast.
|
|
if (aiSpell->getCustomTarget() != nullptr)
|
|
{
|
|
mCurrentSpellTarget = aiSpell->getCustomTarget();
|
|
mLastCastedSpell = aiSpell;
|
|
getUnit()->castSpell(mCurrentSpellTarget, aiSpell->mSpellInfo, aiSpell->mIsTriggered);
|
|
}
|
|
} break;
|
|
case TARGET_FUNCTION:
|
|
{
|
|
if (aiSpell->getTargetFunction() != nullptr)
|
|
{
|
|
mCurrentSpellTarget = aiSpell->getTargetFunction();
|
|
mLastCastedSpell = aiSpell;
|
|
getUnit()->castSpell(mCurrentSpellTarget, aiSpell->mSpellInfo, aiSpell->mIsTriggered);
|
|
}
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void AIInterface::castAISpell(uint32_t aiSpellId)
|
|
{
|
|
CreatureAISpells* aiSpell = nullptr;
|
|
|
|
// Lets find the stored Spellinfo
|
|
for (auto spell : mCreatureAISpells)
|
|
if (spell->mSpellInfo && spell->mSpellInfo->getId() == aiSpellId)
|
|
aiSpell = spell;
|
|
|
|
// when no valid Spell is found return
|
|
if (!aiSpell)
|
|
return;
|
|
|
|
castAISpell(aiSpell);
|
|
}
|
|
|
|
bool AIInterface::hasAISpell(CreatureAISpells* aiSpell)
|
|
{
|
|
// Lets find the stored Spellinfo
|
|
for (auto spell : mCreatureAISpells)
|
|
if (spell->mSpellInfo && spell->mSpellInfo->getId() == aiSpell->mSpellInfo->getId())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool AIInterface::hasAISpell(uint32_t SpellId)
|
|
{
|
|
// Lets find the stored Spellinfo
|
|
for (auto spell : mCreatureAISpells)
|
|
if (spell->mSpellInfo && spell->mSpellInfo->getId() == SpellId)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void AIInterface::castSpellOnRandomTarget(CreatureAISpells* AiSpell)
|
|
{
|
|
if (AiSpell == nullptr)
|
|
return;
|
|
|
|
// helper for following code
|
|
bool isTargetRandFriend = (AiSpell->mTargetType == TARGET_RANDOM_FRIEND ? true : false);
|
|
|
|
// if we already cast a spell, do not set/cast another one!
|
|
if (!getUnit()->isCastingSpell()
|
|
&& getUnit()->getAIInterface()->getCurrentTarget())
|
|
{
|
|
// set up targets in range by position, relation and hp range
|
|
std::vector<Unit*> possibleUnitTargets;
|
|
|
|
for (const auto& inRangeObject : getUnit()->getInRangeObjectsSet())
|
|
{
|
|
if (((isTargetRandFriend && isFriendly(getUnit(), inRangeObject))
|
|
|| (!isTargetRandFriend && isHostile(getUnit(), inRangeObject) && inRangeObject != getUnit())) && inRangeObject->isCreatureOrPlayer())
|
|
{
|
|
Unit* inRangeTarget = static_cast<Unit*>(inRangeObject);
|
|
|
|
if (
|
|
inRangeTarget->isAlive() && AiSpell->isDistanceInRange(getUnit()->GetDistance2dSq(inRangeTarget))
|
|
&& ((AiSpell->isHpInPercentRange(inRangeTarget->getHealthPct()) && isTargetRandFriend)
|
|
|| (getUnit()->getThreatManager().getThreat(inRangeTarget) > 0 && isHostile(getUnit(), inRangeTarget))))
|
|
{
|
|
possibleUnitTargets.push_back(inRangeTarget);
|
|
}
|
|
}
|
|
}
|
|
|
|
// add us as a friendly target.
|
|
if (AiSpell->isHpInPercentRange(getUnit()->getHealthPct()) && isTargetRandFriend)
|
|
possibleUnitTargets.push_back(getUnit());
|
|
|
|
// no targets in our range for hp range and firendly targets
|
|
if (possibleUnitTargets.empty())
|
|
return;
|
|
|
|
// get a random target
|
|
uint32_t randomIndex = Util::getRandomUInt(0, static_cast<uint32_t>(possibleUnitTargets.size() - 1));
|
|
Unit* randomTarget = possibleUnitTargets[randomIndex];
|
|
|
|
if (randomTarget == nullptr)
|
|
return;
|
|
|
|
switch (AiSpell->mTargetType)
|
|
{
|
|
case TARGET_RANDOM_FRIEND:
|
|
case TARGET_RANDOM_SINGLE:
|
|
{
|
|
getUnit()->castSpell(randomTarget, AiSpell->mSpellInfo, AiSpell->mIsTriggered);
|
|
mCurrentSpellTarget = randomTarget;
|
|
} break;
|
|
case TARGET_RANDOM_DESTINATION:
|
|
getUnit()->castSpellLoc(randomTarget->GetPosition(), AiSpell->mSpellInfo, AiSpell->mIsTriggered);
|
|
break;
|
|
}
|
|
|
|
possibleUnitTargets.clear();
|
|
}
|
|
}
|
|
|
|
void AIInterface::updateEmotes(unsigned long time_passed)
|
|
{
|
|
if (!getUnit()->getThreatManager().getCurrentVictim() && m_Unit->isAlive())
|
|
{
|
|
if (timed_emote_expire <= time_passed) // note that creature might go idle and time_passed might get big next time ...We do not skip emotes because of lost time
|
|
{
|
|
if ((*next_timed_emote)->type == 1) //standstate
|
|
{
|
|
m_Unit->setStandState(static_cast<uint8_t>((*next_timed_emote)->value));
|
|
m_Unit->setEmoteState(0);
|
|
}
|
|
else if ((*next_timed_emote)->type == 2) //emotestate
|
|
{
|
|
m_Unit->setEmoteState((*next_timed_emote)->value);
|
|
m_Unit->setStandState(STANDSTATE_STAND);
|
|
}
|
|
else if ((*next_timed_emote)->type == 3) //oneshot emote
|
|
{
|
|
m_Unit->setEmoteState(0);
|
|
m_Unit->setStandState(STANDSTATE_STAND);
|
|
m_Unit->emote((EmoteType)(*next_timed_emote)->value); // Animation
|
|
}
|
|
|
|
if ((*next_timed_emote)->msg)
|
|
m_Unit->sendChatMessage((*next_timed_emote)->msg_type, (*next_timed_emote)->msg_lang, (*next_timed_emote)->msg);
|
|
|
|
timed_emote_expire = (*next_timed_emote)->expire_after; //should we keep lost time ? I think not
|
|
++next_timed_emote;
|
|
|
|
if (next_timed_emote == timed_emotes->end())
|
|
next_timed_emote = timed_emotes->begin();
|
|
}
|
|
else
|
|
{
|
|
timed_emote_expire -= time_passed;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIInterface::eventAiInterfaceParamsetFinish()
|
|
{
|
|
if (timed_emotes && timed_emotes->begin() != timed_emotes->end())
|
|
{
|
|
next_timed_emote = timed_emotes->begin();
|
|
timed_emote_expire = (*next_timed_emote)->expire_after;
|
|
}
|
|
}
|
|
|
|
void AIInterface::updateTargets(unsigned long time_passed)
|
|
{
|
|
// Do not update target while confused or fleeing
|
|
if (getUnit()->hasUnitStateFlag(UNIT_STATE_CONFUSED | UNIT_STATE_FLEEING))
|
|
return;
|
|
|
|
// Hostile NPCs look for attackable unit every 1.5s when out of combat
|
|
// When in combat they look for friendly units to assist it every 1.5s
|
|
|
|
// Find Target when no Threat List is available
|
|
if (!isEngaged() && hasReactState(REACT_AGGRESSIVE))
|
|
{
|
|
m_updateTargetTimer.updateTimer(time_passed);
|
|
if (m_updateTargetTimer.isTimePassed())
|
|
{
|
|
m_updateTargetTimer.resetInterval(1500);
|
|
findTarget();
|
|
}
|
|
}
|
|
|
|
if (isEngaged())
|
|
{
|
|
m_updateAssistTimer.updateTimer(time_passed);
|
|
|
|
if (canUnitEvade(time_passed))
|
|
enterEvadeMode();
|
|
|
|
// Find Assist Targets to assist us in our Fight
|
|
if (m_updateAssistTimer.isTimePassed())
|
|
{
|
|
m_updateAssistTimer.resetInterval(1500);
|
|
|
|
// find nearby allies
|
|
findAssistance();
|
|
|
|
// Clear Assist Targets
|
|
if (m_assistTargets.size())
|
|
{
|
|
for (auto i = m_assistTargets.begin(); i != m_assistTargets.end();)
|
|
{
|
|
auto i2 = i++;
|
|
if ((*i2) == NULL || (*i2)->event_GetCurrentInstanceId() != m_Unit->event_GetCurrentInstanceId() ||
|
|
!(*i2)->isAlive() || m_Unit->getDistanceSq((*i2)) >= 2500.0f || !(*i2)->getCombatHandler().isInCombat() || !((*i2)->m_phase & m_Unit->m_phase))
|
|
{
|
|
m_assistTargets.erase(i2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// When target is out of Possible Range evade.
|
|
if (getUnit()->IsInWorld())
|
|
{
|
|
if (getCurrentTarget() && getCurrentTarget()->GetMapId() != getUnit()->GetMapId())
|
|
{
|
|
if (canUnitEvade(time_passed))
|
|
enterEvadeMode();
|
|
}
|
|
else if (getCurrentTarget() && getCurrentTarget()->getDistance(getUnit()->GetPosition()) > 50.0f && !getUnit()->getWorldMap()->getBaseMap()->instanceable())
|
|
{
|
|
if (canUnitEvade(time_passed))
|
|
enterEvadeMode();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AIInterface::updateTarget()
|
|
{
|
|
if (!isEngaged())
|
|
return false;
|
|
|
|
if (!getUnit()->isAlive())
|
|
{
|
|
engagementOver();
|
|
return false;
|
|
}
|
|
|
|
if (!hasReactState(REACT_PASSIVE))
|
|
{
|
|
if (Unit* victim = selectTarget())
|
|
{
|
|
if (victim != getCurrentTarget())
|
|
attackStart(victim);
|
|
}
|
|
|
|
return getCurrentTarget() != nullptr;
|
|
}
|
|
else if (!getUnit()->isInCombat())
|
|
{
|
|
enterEvadeMode();
|
|
return false;
|
|
}
|
|
else if (getCurrentTarget())
|
|
{
|
|
attackStop();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void AIInterface::attackStart(Unit* target)
|
|
{
|
|
if (target && doInitialAttack(target, true))
|
|
{
|
|
// Clear distracted state on attacking
|
|
if (getUnit()->hasUnitStateFlag(UNIT_STATE_DISTRACTED))
|
|
{
|
|
getUnit()->removeUnitStateFlag(UNIT_STATE_DISTRACTED);
|
|
getUnit()->getMovementManager()->clear();
|
|
}
|
|
getUnit()->getMovementManager()->moveChase(target);
|
|
}
|
|
}
|
|
|
|
void AIInterface::attackStop()
|
|
{
|
|
if (!getCurrentTarget())
|
|
return;
|
|
|
|
Unit* target = getCurrentTarget();
|
|
|
|
setCurrentTarget(nullptr);
|
|
|
|
// Clear Target
|
|
getUnit()->setTargetGuid(0);
|
|
getUnit()->removeUnitStateFlag(UNIT_STATE_MELEE_ATTACKING);
|
|
setNoCallAssistance(false);
|
|
getUnit()->smsg_AttackStop(target);
|
|
}
|
|
|
|
bool AIInterface::doInitialAttack(Unit* target, bool isMelee)
|
|
{
|
|
// no target
|
|
if (!target)
|
|
return false;
|
|
|
|
// ofcourse we cant Attack ourself
|
|
if (target == getUnit())
|
|
return false;
|
|
|
|
// dead units can not be attack
|
|
if (!getUnit()->isAlive() || !target->IsInWorld() || !target->isAlive())
|
|
return false;
|
|
|
|
// cannot attack while evading
|
|
if (getUnit()->isInEvadeMode())
|
|
return false;
|
|
|
|
// nobody can attack GM when GM Flag is set
|
|
if (target->isPlayer())
|
|
{
|
|
if (target->ToPlayer()->isGMFlagSet())
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (target->ToCreature()->isInEvadeMode() || canNotReachTarget())
|
|
return false;
|
|
}
|
|
|
|
if (getUnit()->hasAuraWithAuraEffect(SPELL_AURA_MOD_UNATTACKABLE))
|
|
getUnit()->removeAllAurasByAuraEffect(SPELL_AURA_MOD_UNATTACKABLE);
|
|
|
|
if (getCurrentTarget())
|
|
{
|
|
if (getCurrentTarget() == target)
|
|
{
|
|
// switch to melee attack from ranged/spell
|
|
if (isMelee)
|
|
{
|
|
if (!getUnit()->hasUnitStateFlag(UNIT_STATE_MELEE_ATTACKING))
|
|
{
|
|
getUnit()->addUnitStateFlag(UNIT_STATE_MELEE_ATTACKING);
|
|
getUnit()->smsg_AttackStart(target);
|
|
return true;
|
|
}
|
|
}
|
|
else if (getUnit()->hasUnitStateFlag(UNIT_STATE_MELEE_ATTACKING))
|
|
{
|
|
getUnit()->removeUnitStateFlag(UNIT_STATE_MELEE_ATTACKING);
|
|
getUnit()->smsg_AttackStop(target);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// switch target
|
|
getUnit()->interruptSpellWithSpellType(CURRENT_MELEE_SPELL);
|
|
if (!isMelee)
|
|
getUnit()->removeUnitStateFlag(UNIT_STATE_MELEE_ATTACKING);
|
|
}
|
|
|
|
setCurrentTarget(target);
|
|
|
|
// Set our target
|
|
getUnit()->setTargetGuid(target->getGuid());
|
|
|
|
if (isMelee)
|
|
getUnit()->addUnitStateFlag(UNIT_STATE_MELEE_ATTACKING);
|
|
|
|
if (!getUnit()->isCharmed())
|
|
{
|
|
onHostileAction(target);
|
|
getUnit()->SendAIReaction();
|
|
callAssistance();
|
|
getUnit()->emote(EMOTE_ONESHOT_NONE);
|
|
}
|
|
|
|
if (isMelee)
|
|
getUnit()->smsg_AttackStart(target);
|
|
|
|
return true;
|
|
}
|
|
|
|
Unit* AIInterface::selectTarget()
|
|
{
|
|
Unit* target = nullptr;
|
|
|
|
if (getUnit()->getThreatManager().canHaveThreatList())
|
|
{
|
|
target = getUnit()->getThreatManager().getCurrentVictim();
|
|
}
|
|
else if (!hasReactState(REACT_PASSIVE))
|
|
{
|
|
// playerpet target selection
|
|
target = getTargetForPet();
|
|
if (!target && getUnit()->isSummon())
|
|
{
|
|
if (Unit* owner = getUnit()->ToSummon()->getUnitOwner())
|
|
{
|
|
if (owner->isInCombat())
|
|
target = owner->getAIInterface()->getTargetForPet();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
if (target && isTargetAcceptable(target) && canOwnerAttackUnit(target))
|
|
return target;
|
|
|
|
auto const& iAuras = getUnit()->getAuraEffectList(SPELL_AURA_MOD_INVISIBILITY);
|
|
if (!iAuras.empty())
|
|
{
|
|
for (auto & aura : iAuras)
|
|
{
|
|
if (aura->getAura()->getMaxDuration() == -1)
|
|
{
|
|
enterEvadeMode();
|
|
break;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
enterEvadeMode();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool AIInterface::isTargetAcceptable(Unit* target)
|
|
{
|
|
if (!target)
|
|
return false;
|
|
|
|
if (!target->IsInWorld())
|
|
return false;
|
|
|
|
// if the target cannot be attacked, the target is not acceptable
|
|
#ifdef FT_VEHICLES
|
|
if (isFriendly(getUnit(), target)
|
|
|| !target->getAIInterface()->isTargetableForAttack(false)
|
|
|| (getUnit()->getVehicle() && (getUnit()->isOnVehicle(target) || getUnit()->getVehicle()->getBase()->isOnVehicle(target))))
|
|
return false;
|
|
#else
|
|
if (isFriendly(getUnit(), target) || !target->getAIInterface()->isTargetableForAttack(false))
|
|
return false;
|
|
#endif
|
|
|
|
if (target->hasUnitStateFlag(UNIT_STATE_DIED))
|
|
{
|
|
#if VERSION_STRING > Classic
|
|
// guards can detect fake death
|
|
if (isGuard() && target->hasUnitFlags2(UNIT_FLAG2_FEIGN_DEATH))
|
|
return true;
|
|
else
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
// if I'm already fighting target, or I'm hostile towards the target, the target is acceptable
|
|
if (isEngagedBy(target) || isHostile(getUnit(), target))
|
|
return true;
|
|
|
|
// if the target's victim is not friendly, or the target is friendly, the target is not acceptable
|
|
return false;
|
|
}
|
|
|
|
bool AIInterface::isTargetableForAttack(bool checkFakeDeath)
|
|
{
|
|
if (!getUnit()->isAlive())
|
|
return false;
|
|
|
|
if (getUnit()->hasUnitFlags(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE))
|
|
return false;
|
|
|
|
if (getUnit()->isPlayer() && getUnit()->ToPlayer()->isGMFlagSet())
|
|
return false;
|
|
|
|
return !getUnit()->hasUnitStateFlag(UNIT_STATE_UNATTACKABLE) && (!checkFakeDeath || !getUnit()->hasUnitStateFlag(UNIT_STATE_DIED));
|
|
}
|
|
|
|
Unit* AIInterface::getTargetForPet()
|
|
{
|
|
if (!isEngaged())
|
|
return nullptr;
|
|
|
|
if (Unit* victim = getCurrentTarget())
|
|
if ((!getUnit()->isPet() && !getUnit()->getPlayerOwnerOrSelf()) || getUnit()->isInCombatWith(victim))
|
|
return victim;
|
|
|
|
uint64_t ownerGuid = getUnit()->getCharmedByGuid() ? getUnit()->getCharmedByGuid() : getUnit()->getCreatedByGuid();
|
|
Unit* owner = getUnit()->getWorldMapUnit(ownerGuid);
|
|
|
|
if (owner && owner->getTargetGuid())
|
|
return owner->getWorldMapUnit(owner->getTargetGuid());
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void AIInterface::updateAgent(uint32_t p_time)
|
|
{
|
|
if (getUnit()->isCreature() && static_cast<Creature*>(getUnit())->GetCreatureProperties()->Type == UNIT_TYPE_CRITTER && static_cast<Creature*>(getUnit())->GetType() != CREATURE_TYPE_GUARDIAN)
|
|
return;
|
|
|
|
if (getUnit()->getWorldMap() == nullptr)
|
|
return;
|
|
|
|
if (getUnit()->isCastingSpell())
|
|
return;
|
|
|
|
if (!getCurrentTarget())
|
|
return;
|
|
|
|
spellEvents.updateEvents(p_time, AGENT_SPELL);
|
|
|
|
if (getUnit()->hasUnitStateFlag(UNIT_STATE_STUNNED | UNIT_STATE_CONFUSED | UNIT_STATE_POLYMORPHED | UNIT_STATE_EVADING))
|
|
return;
|
|
|
|
// Do not update combat if unit is feared
|
|
// but update if the fear is self caused by on low health fleeing
|
|
if (getUnit()->hasUnitStateFlag(UNIT_STATE_FLEEING) && m_fleeTimer.getExpireTime() <= 0)
|
|
return;
|
|
|
|
// Selects Current Agent Type For Unit
|
|
uint32_t spellId = spellEvents.getFinishedEvent();
|
|
selectCurrentAgent(getCurrentTarget(), spellId);
|
|
|
|
// Handle Different Agent Types
|
|
switch (getCurrentAgent())
|
|
{
|
|
case AGENT_MELEE:
|
|
{
|
|
handleAgentMelee();
|
|
} break;
|
|
case AGENT_RANGED:
|
|
{
|
|
handleAgentRanged();
|
|
} break;
|
|
case AGENT_SPELL:
|
|
{
|
|
handleAgentSpell(spellId);
|
|
} break;
|
|
case AGENT_FLEE:
|
|
{
|
|
handleAgentFlee(p_time);
|
|
} break;
|
|
case AGENT_CALLFORHELP:
|
|
{
|
|
handleAgentCallForHelp();
|
|
} break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void AIInterface::handleAgentMelee()
|
|
{
|
|
if (getUnit()->isWithinCombatRange(getCurrentTarget(), getUnit()->getMeleeRange(getCurrentTarget()))) // Target is in Range -> Attack
|
|
{
|
|
bool infront = getUnit()->isInFront(getCurrentTarget());
|
|
|
|
if (!infront) // set InFront
|
|
{
|
|
//prevent mob from rotating while stunned
|
|
if (!getUnit()->isStunned())
|
|
{
|
|
getUnit()->setFacingToObject(getCurrentTarget());
|
|
infront = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Mainhand
|
|
if (getUnit()->isAttackReady(MELEE))
|
|
{
|
|
getUnit()->resetAttackTimer(MELEE);
|
|
if (getUnit()->getOnMeleeSpell() != 0)
|
|
getUnit()->castOnMeleeSpell();
|
|
else
|
|
getUnit()->strike(getCurrentTarget(), MELEE, NULL, 0, 0, 0, false, false);
|
|
|
|
if (getUnit()->canDualWield())
|
|
{
|
|
// NPCs always seem to wait 50% of attack timer before doing attack on other hand
|
|
const auto halfAttackSpeed = static_cast<uint32_t>(getUnit()->getBaseAttackTime(OFFHAND) * getUnit()->getAttackSpeedModifier(OFFHAND) * 0.5f);
|
|
const auto msTime = Util::getMSTime();
|
|
if (getUnit()->getAttackTimer(OFFHAND) < msTime || (getUnit()->getAttackTimer(OFFHAND) - msTime) < halfAttackSpeed)
|
|
getUnit()->setAttackTimer(OFFHAND, halfAttackSpeed);
|
|
}
|
|
}
|
|
|
|
// Offhand
|
|
if (getUnit()->canDualWield() && getUnit()->isAttackReady(OFFHAND))
|
|
{
|
|
getUnit()->resetAttackTimer(OFFHAND);
|
|
getUnit()->strike(getCurrentTarget(), OFFHAND, NULL, 0, 0, 0, false, false);
|
|
|
|
// NPCs always seem to wait 50% of attack timer before doing attack on other hand
|
|
const auto halfAttackSpeed = static_cast<uint32_t>(getUnit()->getBaseAttackTime(MELEE) * getUnit()->getAttackSpeedModifier(MELEE) * 0.5f);
|
|
const auto msTime = Util::getMSTime();
|
|
if (getUnit()->getAttackTimer(MELEE) < msTime || (getUnit()->getAttackTimer(MELEE) - msTime) < halfAttackSpeed)
|
|
getUnit()->setAttackTimer(MELEE, halfAttackSpeed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIInterface::handleAgentRanged()
|
|
{
|
|
float combatReach[2]; // Calculate Combat Reach
|
|
float distance = m_Unit->CalcDistance(getCurrentTarget());
|
|
|
|
combatReach[0] = 8.0f;
|
|
combatReach[1] = 30.0f;
|
|
|
|
if (distance >= combatReach[0] && distance <= combatReach[1]) // Target is in Range -> Attack
|
|
{
|
|
if (m_Unit->isAttackReady(RANGED) && !getUnit()->isInEvadeMode())
|
|
{
|
|
bool infront = m_Unit->isInFront(getCurrentTarget());
|
|
|
|
if (!infront) // set InFront
|
|
{
|
|
//prevent mob from rotating while stunned
|
|
if (!m_Unit->isStunned())
|
|
{
|
|
getUnit()->setFacingToObject(getCurrentTarget());
|
|
infront = true;
|
|
}
|
|
}
|
|
|
|
if (infront)
|
|
{
|
|
m_Unit->setAttackTimer(RANGED, m_Unit->getBaseAttackTime(RANGED));
|
|
SpellInfo const* info = sSpellMgr.getSpellInfo(SPELL_RANGED_GENERAL);
|
|
if (info)
|
|
{
|
|
Spell* sp = sSpellMgr.newSpell(m_Unit, info, false, NULL);
|
|
SpellCastTargets targets(getCurrentTarget()->getGuid());
|
|
sp->prepare(&targets);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIInterface::handleAgentSpell(uint32_t spellId)
|
|
{
|
|
auto AIspell = getSpell(spellId);
|
|
bool canCastSpell = false;
|
|
|
|
if (AIspell->agent == AGENT_SPELL)
|
|
{
|
|
if (AIspell->spellType == STYPE_BUFF)
|
|
{
|
|
// cast the buff at requested percent only if we don't have it already
|
|
if (Util::checkChance(AIspell->procChance))
|
|
{
|
|
if (!m_Unit->hasAurasWithId(AIspell->spell->getId()))
|
|
{
|
|
canCastSpell = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// cast the spell at requested percent.
|
|
if (Util::checkChance(AIspell->procChance))
|
|
{
|
|
//focus/mana requirement
|
|
switch (AIspell->spell->getPowerType())
|
|
{
|
|
case POWER_TYPE_MANA:
|
|
{
|
|
if (m_Unit->getPower(POWER_TYPE_MANA) > AIspell->spell->getManaCost())
|
|
canCastSpell = true;
|
|
}
|
|
break;
|
|
case POWER_TYPE_FOCUS:
|
|
{
|
|
if (m_Unit->getPower(POWER_TYPE_FOCUS) > AIspell->spell->getManaCost())
|
|
canCastSpell = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto maxRange = getSpellEntry(spellId)->getMaxRange();
|
|
if (canCastSpell && (maxRange == 0.0f || getUnit()->isWithinCombatRange(getCurrentTarget(), maxRange)))
|
|
{
|
|
SpellInfo const* spellInfo = getSpellEntry(spellId);
|
|
auto targettype = AIspell->spelltargetType;
|
|
SpellCastTargets targets = setSpellTargets(spellInfo, getCurrentTarget(), targettype);
|
|
|
|
switch (targettype)
|
|
{
|
|
case TTYPE_CASTER:
|
|
case TTYPE_SINGLETARGET:
|
|
{
|
|
castSpell(getUnit(), spellInfo, targets);
|
|
}
|
|
break;
|
|
case TTYPE_SOURCE:
|
|
{
|
|
getUnit()->castSpellLoc(targets.getSource(), spellInfo, true);
|
|
}
|
|
break;
|
|
case TTYPE_DESTINATION:
|
|
{
|
|
getUnit()->castSpellLoc(targets.getDestination(), spellInfo, true);
|
|
}
|
|
break;
|
|
default:
|
|
sLogger.failure("AI Agents: Targettype of AI agent spell %u for creature %u not set", spellInfo->getId(), static_cast<Creature*>(getUnit())->GetCreatureProperties()->Id);
|
|
}
|
|
}
|
|
uint32_t casttime = (GetCastTime(sSpellCastTimesStore.LookupEntry(AIspell->spell->getCastingTimeIndex())) ? GetCastTime(sSpellCastTimesStore.LookupEntry(AIspell->spell->getCastingTimeIndex())) : 500);
|
|
const auto cooldown = static_cast<int32_t>(AIspell->cooldown ? AIspell->cooldown : 500);
|
|
// Delay all Spells by our casttime
|
|
spellEvents.delayAllEvents(casttime, AGENT_SPELL);
|
|
// Re add Spell to scheduler
|
|
spellEvents.addEvent(spellId, cooldown, AGENT_SPELL);
|
|
}
|
|
|
|
void AIInterface::handleAgentFlee(uint32_t p_time)
|
|
{
|
|
if (getUnit()->isInEvadeMode())
|
|
return;
|
|
|
|
m_fleeTimer.updateTimer(p_time);
|
|
if (m_fleeTimer.isTimePassed())
|
|
{
|
|
getUnit()->setControlled(false, UNIT_STATE_FLEEING);
|
|
setCurrentAgent(AGENT_NULL);
|
|
}
|
|
|
|
if (m_hasFleed)
|
|
return;
|
|
|
|
m_fleeTimer.resetInterval(m_FleeDuration);
|
|
|
|
if (m_Unit->IsInWorld() && m_Unit->isCreature() && static_cast<Creature*>(m_Unit)->GetScript())
|
|
static_cast<Creature*>(m_Unit)->GetScript()->OnFlee(getCurrentTarget());
|
|
|
|
getUnit()->setControlled(true, UNIT_STATE_FLEEING);
|
|
|
|
std::string msg = "%s attempts to run away in fear!";
|
|
getUnit()->sendChatMessage(CHAT_MSG_MONSTER_EMOTE, LANG_UNIVERSAL, msg);
|
|
|
|
// On Flee Scripts
|
|
for (auto onFleeScript : onFleeScripts)
|
|
{
|
|
if (onFleeScript.action == actionSpell)
|
|
{
|
|
castAISpell(onFleeScript.spellId);
|
|
}
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnFlee, nullptr);
|
|
|
|
m_hasFleed = true;
|
|
}
|
|
|
|
void AIInterface::handleAgentCallForHelp()
|
|
{
|
|
setNoCallAssistance(true);
|
|
callForHelp(30.0f);
|
|
|
|
if (m_Unit->isCreature())
|
|
{
|
|
// On Call for Help Scripts
|
|
for (auto onCallForHelpScript : onCallForHelpScripts)
|
|
{
|
|
if (onCallForHelpScript.action == actionSpell)
|
|
{
|
|
castAISpell(onCallForHelpScript.spellId);
|
|
}
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnCallForHelp, nullptr);
|
|
}
|
|
|
|
if (m_Unit->IsInWorld() && m_Unit->isCreature() && static_cast<Creature*>(m_Unit)->GetScript())
|
|
static_cast<Creature*>(m_Unit)->GetScript()->OnCallForHelp();
|
|
}
|
|
|
|
void AIInterface::doFleeToGetAssistance()
|
|
{
|
|
if (!getCurrentTarget())
|
|
return;
|
|
|
|
if (getUnit()->getAuraWithAuraEffect(SPELL_AURA_PREVENTS_FLEEING))
|
|
return;
|
|
|
|
// maybe move to Config file
|
|
float radius = 30.0f;
|
|
if (radius > 0)
|
|
{
|
|
Creature* creature = getUnit()->getWorldMap()->getInterface()->getNearestAssistCreatureInCell(getUnit()->ToCreature(), getCurrentTarget(), radius);
|
|
|
|
setNoSearchAssistance(true);
|
|
|
|
if (!creature)
|
|
getUnit()->setControlled(true, UNIT_STATE_FLEEING);
|
|
else
|
|
getUnit()->getMovementManager()->moveSeekAssistance(creature->GetPositionX(), creature->GetPositionY(), creature->GetPositionZ());
|
|
}
|
|
}
|
|
|
|
void AIInterface::callAssistance()
|
|
{
|
|
if (!m_AlreadyCallAssistance && getCurrentTarget() && !getUnit()->isPet() && !getUnit()->getUnitOwner())
|
|
{
|
|
setNoCallAssistance(true);
|
|
|
|
// maybe move to Config file
|
|
float radius = 10.0f;
|
|
|
|
if (radius > 0)
|
|
{
|
|
Creature* creature = getUnit()->getWorldMap()->getInterface()->getNearestAssistCreatureInCell(getUnit()->ToCreature(), getCurrentTarget(), radius);
|
|
|
|
if (creature)
|
|
creature->getAIInterface()->onHostileAction(getCurrentTarget());
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIInterface::findAssistance()
|
|
{
|
|
if (!getUnit()->getWorldMap() || !getCurrentTarget())
|
|
return;
|
|
|
|
for (const auto& itr : getUnit()->getInRangeObjectsSet())
|
|
{
|
|
if (itr->isCreature())
|
|
{
|
|
Creature* helper = itr->ToCreature();
|
|
|
|
float DistToMe = getUnit()->CalcDistance(helper);
|
|
|
|
if (DistToMe <= 25.0f && helper->isInCombat() && !isAlreadyAssisting(helper)) // Also add targets if already in fight
|
|
m_assistTargets.insert(helper);
|
|
|
|
if (DistToMe <= 10.0f && getUnit()->getWorldMap()->getBaseMap()->getMapInfo()->isInstanceMap()) // only Search additional Attackers in Instanced Maps
|
|
{
|
|
if (helper->getAIInterface()->canAssistTo(getUnit(), getCurrentTarget(), false))
|
|
{
|
|
m_assistTargets.insert(helper);
|
|
if (!helper->isInCombat())
|
|
helper->getAIInterface()->onHostileAction(getCurrentTarget());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AIInterface::isAlreadyAssisting(Creature* helper)
|
|
{
|
|
if (m_assistTargets.find(helper) != m_assistTargets.end())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void AIInterface::callForHelp(float radius)
|
|
{
|
|
if (radius <= 0.0f || !isEngaged() || !getUnit()->isAlive() || getUnit()->isPet() || getUnit()->isCharmed())
|
|
return;
|
|
|
|
Unit* target = getUnit()->getThreatManager().getCurrentVictim();
|
|
if (!target)
|
|
target = getUnit()->getThreatManager().getAnyTarget();
|
|
|
|
if (!target)
|
|
return;
|
|
|
|
// todo
|
|
}
|
|
|
|
bool AIInterface::canAssistTo(Unit* u, Unit* enemy, bool checkfaction /*= true*/)
|
|
{
|
|
// is it true?
|
|
if (!hasReactState(REACT_AGGRESSIVE))
|
|
return false;
|
|
|
|
// we don't need help from zombies :)
|
|
if (!getUnit()->isAlive())
|
|
return false;
|
|
|
|
// we cannot assist in evade mode
|
|
if (getUnit()->isInEvadeMode())
|
|
return false;
|
|
|
|
// or if enemy is in evade mode
|
|
if (enemy->getObjectTypeId() == TYPEID_UNIT && enemy->ToCreature()->isInEvadeMode())
|
|
return false;
|
|
|
|
if (getUnit()->hasUnitFlags(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE) || isImmuneToNPC())
|
|
return false;
|
|
|
|
// skip fighting creature
|
|
if (isEngaged())
|
|
return false;
|
|
|
|
// only free creature
|
|
if (getUnit()->getUnitOwner())
|
|
return false;
|
|
|
|
// only from same creature faction
|
|
if (checkfaction)
|
|
{
|
|
if (getUnit()->getFactionTemplate() != u->getFactionTemplate())
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!isFriendly(getUnit(), u))
|
|
return false;
|
|
}
|
|
|
|
// skip non hostile to caster enemy creatures
|
|
if (!isHostile(getUnit(), enemy))
|
|
return false;
|
|
|
|
if (isFriendly(getUnit(), enemy))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void AIInterface::selectCurrentAgent(Unit* target, uint32_t spellid)
|
|
{
|
|
// If mob is currently fleeing
|
|
if (m_fleeTimer.getExpireTime() > 0)
|
|
{
|
|
setCurrentAgent(AGENT_FLEE);
|
|
return;
|
|
}
|
|
|
|
if (this->isMeleeDisabled() && getCurrentAgent() == AGENT_MELEE)
|
|
setCurrentAgent(AGENT_NULL);
|
|
|
|
if (this->isRangedDisabled() && getCurrentAgent() == AGENT_RANGED)
|
|
setCurrentAgent(AGENT_NULL);
|
|
|
|
if (this->isCastDisabled() && getCurrentAgent() == AGENT_SPELL)
|
|
setCurrentAgent(AGENT_NULL);
|
|
|
|
if (target->isAlive() && !getUnit()->isInEvadeMode())
|
|
{
|
|
if (canFlee() && !m_hasFleed && ((static_cast<float>(m_Unit->getHealth()) / static_cast<float>(m_Unit->getMaxHealth()) < m_FleeHealth)))
|
|
{
|
|
setCurrentAgent(AGENT_FLEE);
|
|
return;
|
|
}
|
|
else if (canCallForHelp() && !m_AlreadyCallAssistance && ((static_cast<float>(m_Unit->getHealth()) / static_cast<float>(m_Unit->getMaxHealth()) < m_CallForHelpHealth)))
|
|
{
|
|
setCurrentAgent(AGENT_CALLFORHELP);
|
|
return;
|
|
}
|
|
|
|
if (spellid)
|
|
{
|
|
AI_Spell* m_nextSpell = getSpell(spellid);
|
|
if (m_nextSpell->agent != AGENT_NULL)
|
|
{
|
|
setCurrentAgent(AI_Agent(m_nextSpell->agent));
|
|
}
|
|
else
|
|
{
|
|
setCurrentAgent(AGENT_MELEE);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
setCurrentAgent(AGENT_MELEE);
|
|
}
|
|
|
|
if (getCurrentAgent() == AGENT_RANGED || getCurrentAgent() == AGENT_MELEE)
|
|
{
|
|
if (m_canRangedAttack)
|
|
{
|
|
setCurrentAgent(AGENT_MELEE);
|
|
if (target->isPlayer())
|
|
{
|
|
float dist = m_Unit->getDistanceSq(target);
|
|
if (target->hasUnitMovementFlag(MOVEFLAG_ROOTED) || dist >= 64.0f)
|
|
{
|
|
setCurrentAgent(AGENT_RANGED);
|
|
}
|
|
}
|
|
else if (target->m_canMove == false || m_Unit->getDistanceSq(target) >= 64.0f)
|
|
{
|
|
setCurrentAgent(AGENT_RANGED);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
setCurrentAgent(AGENT_MELEE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIInterface::addSpellToList(AI_Spell* sp)
|
|
{
|
|
if (!sp || !sp->spell)
|
|
return;
|
|
|
|
AI_Spell* sp2 = new AI_Spell;
|
|
memcpy(sp2, sp, sizeof(AI_Spell));
|
|
m_spells.push_back(sp2);
|
|
}
|
|
|
|
void AIInterface::initializeSpells()
|
|
{
|
|
if (m_spells.size())
|
|
{
|
|
for (auto itr = m_spells.begin(); itr != m_spells.end(); ++itr)
|
|
{
|
|
const auto cooldown = static_cast<int32_t>((*itr)->cooldown);
|
|
uint32_t spellid = (*itr)->spell->getId();
|
|
spellEvents.addEvent(spellid, cooldown, AGENT_SPELL);
|
|
}
|
|
}
|
|
}
|
|
|
|
AI_Spell* AIInterface::getSpell(uint32_t entry)
|
|
{
|
|
if (m_spells.size())
|
|
{
|
|
for (const auto& aiSpell : m_spells)
|
|
{
|
|
if (aiSpell->spell->getId() == entry)
|
|
return aiSpell;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void AIInterface::setNextSpell(uint32_t spellId)
|
|
{
|
|
if (getSpell(spellId))
|
|
spellEvents.addEvent(spellId, 1);
|
|
}
|
|
|
|
void AIInterface::removeNextSpell(uint32_t spellId)
|
|
{
|
|
if (getSpell(spellId))
|
|
spellEvents.removeEvent(spellId);
|
|
}
|
|
|
|
void AIInterface::castSpell(Unit* caster, SpellInfo const* spellInfo, SpellCastTargets targets)
|
|
{
|
|
if (spellInfo != nullptr)
|
|
{
|
|
if (isCastDisabled())
|
|
return;
|
|
|
|
sLogger.debugFlag(AscEmu::Logging::LF_SPELL, "AI DEBUG: Unit %u casting spell %u on target " I64FMT " ", caster->getEntry(),
|
|
spellInfo->getId(), targets.getUnitTarget());
|
|
|
|
//i wonder if this will lead to a memory leak :S
|
|
Spell* nspell = sSpellMgr.newSpell(caster, spellInfo, false, nullptr);
|
|
nspell->prepare(&targets);
|
|
}
|
|
else
|
|
{
|
|
sLogger.failure("AIInterface::castSpell tried to cast invalid spell!");
|
|
}
|
|
}
|
|
|
|
SpellInfo const* AIInterface::getSpellEntry(uint32_t spellId)
|
|
{
|
|
const auto spellInfo = sSpellMgr.getSpellInfo(spellId);
|
|
|
|
if (!spellInfo)
|
|
{
|
|
sLogger.failure("WORLD: unknown spell id %i", spellId);
|
|
return NULL;
|
|
}
|
|
|
|
return spellInfo;
|
|
}
|
|
|
|
SpellCastTargets AIInterface::setSpellTargets(SpellInfo const* /*spellInfo*/, Unit* target,uint8_t targettype) const
|
|
{
|
|
SpellCastTargets targets;
|
|
targets.setGameObjectTarget(0);
|
|
targets.setUnitTarget(target ? target->getGuid() : 0);
|
|
targets.setItemTarget(0);
|
|
targets.setSource(m_Unit->GetPosition());
|
|
targets.setDestination(m_Unit->GetPosition());
|
|
|
|
if (targettype == TTYPE_SINGLETARGET)
|
|
{
|
|
targets.setTargetMask(TARGET_FLAG_UNIT);
|
|
}
|
|
else if (targettype == TTYPE_SOURCE)
|
|
{
|
|
targets.setTargetMask(TARGET_FLAG_SOURCE_LOCATION);
|
|
}
|
|
else if (targettype == TTYPE_DESTINATION)
|
|
{
|
|
targets.setTargetMask(TARGET_FLAG_DEST_LOCATION);
|
|
if (target)
|
|
{
|
|
targets.setDestination(target->GetPosition());
|
|
}
|
|
}
|
|
else if (targettype == TTYPE_CASTER)
|
|
{
|
|
targets.setTargetMask(TARGET_FLAG_UNIT);
|
|
targets.setUnitTarget(m_Unit->getGuid());
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
|
|
bool AIInterface::canStartAttack(Unit* target, bool force)
|
|
{
|
|
if (getUnit()->ToCreature()->GetCreatureProperties()->extra_a9_flags & 2)
|
|
return false;
|
|
|
|
// This set of checks is should be done only for creatures
|
|
if ((isImmuneToNPC() && !target->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE))
|
|
|| (isImmuneToPC() && target->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE)))
|
|
return false;
|
|
|
|
// Do not attack non-combat pets
|
|
if (target->ToCreature() && target->ToCreature()->GetCreatureProperties()->Type == UNIT_TYPE_NONCOMBAT_PET)
|
|
return false;
|
|
|
|
// Dont Aggro Flying stuff we cannot reach
|
|
if (!getUnit()->canFly() && (getUnit()->getDistanceZ(target) > 3 + getUnit()->getMeleeRange(target)))
|
|
return false;
|
|
|
|
if (!force)
|
|
{
|
|
if (!isTargetAcceptable(target))
|
|
return false;
|
|
|
|
if (getUnit()->isNeutralToAll() || !getUnit()->IsWithinDistInMap(target, calcAggroRange(target)))
|
|
return false;
|
|
}
|
|
|
|
if (!canOwnerAttackUnit(target))
|
|
return false;
|
|
|
|
if (worldConfig.terrainCollision.isCollisionEnabled)
|
|
{
|
|
return getUnit()->IsWithinLOSInMap(target);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//function is designed to make a quick check on target to decide if we can attack it
|
|
bool AIInterface::canOwnerAttackUnit(Unit* pUnit)
|
|
{
|
|
if (!pUnit->IsInWorld())
|
|
return false;
|
|
|
|
if (!isValidTarget(pUnit))
|
|
return false;
|
|
|
|
if (!pUnit->isInAccessiblePlaceFor(getUnit()->ToCreature()))
|
|
return false;
|
|
|
|
// Script Interface Function
|
|
if (getUnit()->ToCreature()->GetScript())
|
|
{
|
|
if (!getUnit()->ToCreature()->GetScript()->canAttackTarget(pUnit))
|
|
return false;
|
|
}
|
|
|
|
if (getUnit()->isInEvadeMode())
|
|
return false;
|
|
|
|
if (pUnit->ToCreature() && pUnit->ToCreature()->isInEvadeMode())
|
|
return false;
|
|
|
|
// Map Visibility Range but not more than the Distance of 2 Cells
|
|
float distance = std::min<float>(getUnit()->getWorldMap()->getVisibilityRange(), Map::Cell::cellSize * 2);
|
|
|
|
uint64_t ownerGuid = getUnit()->getCharmedByGuid() ? getUnit()->getCharmedByGuid() : getUnit()->getCreatedByGuid();
|
|
if (Unit* unit = getUnit()->getWorldMapUnit(ownerGuid))
|
|
{
|
|
return pUnit->IsWithinDistInMap(unit, distance);
|
|
}
|
|
else
|
|
{
|
|
// include sizes for huge npcs
|
|
distance += getUnit()->getCombatReach() + pUnit->getCombatReach();
|
|
|
|
// to prevent creatures in air ignore attacks because distance is already too high...
|
|
if (getUnit()->ToCreature()->getMovementTemplate().isFlightAllowed())
|
|
return pUnit->isInDist2d(getUnit()->GetSpawnPosition(), distance);
|
|
else
|
|
return pUnit->isInDist(getUnit()->GetSpawnPosition(), distance);
|
|
}
|
|
}
|
|
|
|
bool AIInterface::isValidTarget(Unit* target)
|
|
{
|
|
// can't attack unattackable units
|
|
if (target->hasUnitStateFlag(UNIT_STATE_UNATTACKABLE))
|
|
return false;
|
|
|
|
// can't attack GMs
|
|
if (target->isPlayer() && target->ToPlayer()->isGMFlagSet())
|
|
return false;
|
|
|
|
// Creature should not attack permanently invisible units
|
|
if (target->getInvisibilityLevel(INVIS_FLAG_NEVER_VISIBLE) > 0)
|
|
return false;
|
|
|
|
// can't attack invisible
|
|
if (!getUnit()->canSee(target))
|
|
return false;
|
|
|
|
// can't attack dead
|
|
if (!target->isAlive())
|
|
return false;
|
|
|
|
// can't attack untargetable
|
|
if (target->hasUnitFlags(UNIT_FLAG_NOT_SELECTABLE))
|
|
return false;
|
|
|
|
// check flags
|
|
if (target->hasUnitFlags(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_MOUNTED_TAXI | UNIT_FLAG_IGNORE_CREATURE_COMBAT | UNIT_FLAG_ALIVE))
|
|
return false;
|
|
|
|
// ignore immunity flags when assisting
|
|
if (!getUnit()->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) && target->getAIInterface()->isImmuneToNPC())
|
|
return false;
|
|
|
|
if (!target->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) && getUnit()->getAIInterface()->isImmuneToNPC())
|
|
return false;
|
|
|
|
if (getUnit()->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) && target->getAIInterface()->isImmuneToPC())
|
|
return false;
|
|
|
|
if (target->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) && getUnit()->getAIInterface()->isImmuneToPC())
|
|
return false;
|
|
|
|
// Creature Vs Creature
|
|
if (!getUnit()->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) && !target->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE))
|
|
return isHostile(getUnit(), target) || isHostile(target, getUnit());
|
|
|
|
// Player vs Player, Player vs Creature, Creature vs Player case
|
|
// can't attack friendly targets
|
|
if (isFriendly(getUnit(), target) || isFriendly(target, getUnit()))
|
|
return false;
|
|
|
|
Player* playerAffectingAttacker = getUnit()->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) ? getUnit()->getPlayerOwnerOrSelf() : nullptr;
|
|
Player* playerAffectingTarget = target->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) ? target->getPlayerOwnerOrSelf() : nullptr;
|
|
|
|
// Not all neutral creatures can be attacked (even some unfriendly faction does not react aggresive to you, like Sporaggar)
|
|
if ((playerAffectingAttacker && !playerAffectingTarget) || (!playerAffectingAttacker && playerAffectingTarget))
|
|
{
|
|
Player* player = playerAffectingAttacker ? playerAffectingAttacker : playerAffectingTarget;
|
|
|
|
if (Unit* creature = playerAffectingAttacker ? target : getUnit())
|
|
{
|
|
if (creature->getAIInterface()->isGuard() && player->hasPlayerFlags(PLAYER_FLAG_PVP_GUARD_ATTACKABLE))
|
|
return true;
|
|
|
|
if (DBC::Structures::FactionTemplateEntry const* factionTemplate = creature->m_factionTemplate)
|
|
{
|
|
if (DBC::Structures::FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionTemplate->Faction))
|
|
if (player->isHostileBasedOnReputation(factionEntry))
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Creature* creatureAttacker = getUnit()->ToCreature();
|
|
if (creatureAttacker && (creatureAttacker->GetCreatureProperties()->typeFlags & CREATURE_FLAG1_PARTY_MEMBER))
|
|
return false;
|
|
|
|
if (playerAffectingAttacker && playerAffectingTarget)
|
|
if (playerAffectingAttacker->getDuelPlayer() == playerAffectingTarget && playerAffectingAttacker->getDuelState() == DUEL_STATE_STARTED)
|
|
return true;
|
|
|
|
// PvP case - can't attack when attacker or target are in sanctuary
|
|
if (target->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) && getUnit()->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE) && (target->isSanctuaryFlagSet() || getUnit()->isSanctuaryFlagSet()))
|
|
return false;
|
|
|
|
// additional checks - only PvP case
|
|
if (playerAffectingAttacker && playerAffectingTarget)
|
|
{
|
|
if (playerAffectingTarget->isPvpFlagSet())
|
|
return true;
|
|
|
|
if (playerAffectingAttacker->isFfaPvpFlagSet() && playerAffectingTarget->isFfaPvpFlagSet())
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AIInterface::isValidAssistTarget(Unit* target)
|
|
{
|
|
// can assist to self
|
|
if (getUnit() == target)
|
|
return true;
|
|
|
|
if (target == nullptr)
|
|
return false;
|
|
|
|
// can't assist unattackable units
|
|
if (target->hasUnitStateFlag(UNIT_STATE_UNATTACKABLE))
|
|
return false;
|
|
|
|
// can't assist GMs
|
|
if (target->isPlayer() && target->ToPlayer()->isGMFlagSet())
|
|
return false;
|
|
|
|
// Creature should not assist permanently invisible units
|
|
if (target->getInvisibilityLevel(INVIS_FLAG_NEVER_VISIBLE) > 0)
|
|
return false;
|
|
|
|
// can't assist invisible
|
|
if (!getUnit()->canSee(target))
|
|
return false;
|
|
|
|
#ifdef FT_VEHICLES
|
|
// can't assist own vehicle or passenger
|
|
if (getUnit()->getVehicle())
|
|
{
|
|
if (getUnit()->isOnVehicle(target))
|
|
return false;
|
|
|
|
if (getUnit()->getVehicleBase()->isOnVehicle(target))
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
// can't attack dead
|
|
if (!target->isAlive())
|
|
return false;
|
|
|
|
// can't attack untargetable
|
|
if (target->hasUnitFlags(UNIT_FLAG_NOT_SELECTABLE))
|
|
return false;
|
|
|
|
if (getUnit()->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE))
|
|
{
|
|
if (target->getAIInterface()->isImmuneToPC())
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (target->getAIInterface()->isImmuneToNPC())
|
|
return false;
|
|
}
|
|
|
|
// can't assist non-friendly targets
|
|
if (!isFriendly(getUnit(), target))
|
|
return false;
|
|
|
|
// PvP case
|
|
if (target->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE))
|
|
{
|
|
if (getUnit()->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE))
|
|
{
|
|
Player const* selfPlayerOwner = getUnit()->getPlayerOwnerOrSelf();
|
|
Player const* targetPlayerOwner = target->getPlayerOwnerOrSelf();
|
|
if (selfPlayerOwner && targetPlayerOwner)
|
|
{
|
|
// can't assist player which is dueling someone
|
|
if (selfPlayerOwner != targetPlayerOwner && targetPlayerOwner->getDuelPlayer())
|
|
return false;
|
|
}
|
|
// can't assist player in ffa_pvp zone from outside
|
|
if (target->isFfaPvpFlagSet() && !getUnit()->isFfaPvpFlagSet())
|
|
return false;
|
|
|
|
// can't assist player out of sanctuary from sanctuary if has pvp enabled
|
|
if (target->isPvpFlagSet())
|
|
if (getUnit()->isSanctuaryFlagSet() && !target->isSanctuaryFlagSet())
|
|
return false;
|
|
}
|
|
}
|
|
// PvC case - player can assist creature only if has specific type flags
|
|
else if (getUnit()->hasUnitFlags(UNIT_FLAG_PVP_ATTACKABLE))
|
|
{
|
|
if (!target->isPvpFlagSet())
|
|
if (Creature* creatureTarget = target->ToCreature())
|
|
return ((creatureTarget->GetCreatureProperties()->typeFlags & CREATURE_FLAG1_PARTY_MEMBER) || (creatureTarget->GetCreatureProperties()->typeFlags & CREATURE_FLAG1_AID_PLAYERS));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
float AIInterface::calcCombatRange(Unit* target, bool ranged)
|
|
{
|
|
if (!target)
|
|
return 0.0f;
|
|
|
|
float rang = 0.0f;
|
|
if (ranged)
|
|
rang = 5.0f;
|
|
|
|
float selfreach = m_Unit->getCombatReach();
|
|
float targetradius = target->getModelHalfSize();
|
|
float selfradius = m_Unit->getModelHalfSize();
|
|
|
|
float range = targetradius + selfreach + selfradius + rang;
|
|
|
|
return range;
|
|
}
|
|
|
|
Unit* AIInterface::findTarget()
|
|
{
|
|
// find nearest hostile Target to attack
|
|
if (m_Unit->getWorldMap() == nullptr)
|
|
return nullptr;
|
|
|
|
Unit* target = nullptr;
|
|
Unit* critterTarget = nullptr;
|
|
|
|
float distance = 999999.0f; // that should do it.. :p
|
|
|
|
//target is immune to all form of attacks, cant attack either.
|
|
// not attackable creatures sometimes fight enemies in scripted fights though
|
|
if (m_Unit->hasUnitFlags(UNIT_FLAG_NOT_SELECTABLE | UNIT_FLAG_NON_ATTACKABLE))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Should not look for new target while creature is in these states
|
|
if (getUnit()->hasUnitStateFlag(UNIT_STATE_STUNNED | UNIT_STATE_FLEEING | UNIT_STATE_CONFUSED | UNIT_STATE_POLYMORPHED | UNIT_STATE_EVADING))
|
|
return nullptr;
|
|
|
|
for (const auto& itr2 : m_Unit->getInRangeObjectsSet())
|
|
{
|
|
if (itr2)
|
|
{
|
|
if (!itr2->isCreatureOrPlayer())
|
|
continue;
|
|
|
|
Unit* pUnit = static_cast<Unit*>(itr2);
|
|
|
|
if (!hasReactState(REACT_AGGRESSIVE) || !canStartAttack(pUnit, false))
|
|
continue;
|
|
|
|
//on blizz there is no Z limit check
|
|
float dist = m_Unit->GetDistance2dSq(pUnit);
|
|
|
|
if (pUnit->m_factionTemplate && pUnit->m_factionTemplate->Faction == 28)// only Attack a critter if there is no other Enemy in range
|
|
{
|
|
if (dist < 10.0f)
|
|
critterTarget = pUnit;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (dist > distance) // we want to find the CLOSEST target
|
|
continue;
|
|
|
|
distance = dist;
|
|
target = pUnit;
|
|
}
|
|
}
|
|
|
|
if (!target)
|
|
{
|
|
target = critterTarget;
|
|
}
|
|
|
|
if (target)
|
|
{
|
|
onHostileAction(target);
|
|
|
|
if (const auto targetOwner = target->getUnitOwner())
|
|
onHostileAction(targetOwner);
|
|
}
|
|
return target;
|
|
}
|
|
|
|
float AIInterface::calcAggroRange(Unit* target)
|
|
{
|
|
if (!m_Unit->canSee(target))
|
|
return 0;
|
|
|
|
// WoW Wiki: the minimum radius seems to be 5 yards, while the maximum range is 45 yards
|
|
float maxRadius = 45.0f;
|
|
float minRadius = 5.0f;
|
|
|
|
int32_t levelDifference = getUnit()->getLevel() - target->getLevel();
|
|
|
|
// The aggro radius for creatures with equal level as the player is 20 yards.
|
|
// The combatreach should not get taken into account for the distance so we drop it from the range (see Supremus as expample, its a Big Model with a big CombatReach)
|
|
float baseAggroDistance = 20.0f - getUnit()->getCombatReach();
|
|
|
|
// + - 1 yard for each level difference between player and creature
|
|
float aggroRadius = baseAggroDistance + float(levelDifference);
|
|
|
|
// SPELL_AURA_MOD_DETECT_RANGE
|
|
const auto modDetectRange = static_cast<float_t>(target->getDetectRangeMod(m_Unit->getGuid()));
|
|
aggroRadius += modDetectRange;
|
|
if (target->isPlayer())
|
|
{
|
|
aggroRadius += static_cast<float_t>(dynamic_cast<Player*>(target)->m_detectedRange);
|
|
}
|
|
|
|
// Check to see if the target is a player mining a node
|
|
bool isMining = false;
|
|
if (target->isPlayer())
|
|
{
|
|
if (target->isCastingSpell())
|
|
{
|
|
// If nearby miners weren't spotted already we'll give them a little surprise.
|
|
Spell* sp = target->getCurrentSpell(CURRENT_GENERIC_SPELL);
|
|
if (sp != nullptr && sp->getSpellInfo()->getEffect(0) == SPELL_EFFECT_OPEN_LOCK && sp->getSpellInfo()->getEffectMiscValue(0) == LOCKTYPE_MINING)
|
|
{
|
|
isMining = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the target is of a much higher level the aggro range must be scaled down, unless the target is mining a nearby resource node
|
|
if (levelDifference > 8 && !isMining)
|
|
{
|
|
aggroRadius += aggroRadius * static_cast<float_t>((levelDifference - 8) * 5 / 100);
|
|
}
|
|
|
|
// Multiply by elite value
|
|
if (m_Unit->isCreature() && dynamic_cast<Creature*>(m_Unit)->GetCreatureProperties()->Rank > 0)
|
|
{
|
|
aggroRadius *= static_cast<float_t>(dynamic_cast<Creature*>(m_Unit)->GetCreatureProperties()->Rank) * 1.50f;
|
|
}
|
|
|
|
// The aggro range of creatures with higher levels than the total player level for the expansion should get the maxlevel treatment
|
|
// This makes sure that creatures such as bosses wont have a bigger aggro range than the rest of the npc's
|
|
// The following code is used for blizzlike behaivior such as skippable bosses
|
|
if (getUnit()->getLevel() > worldConfig.player.playerGeneratedInformationByLevelCap)
|
|
aggroRadius = baseAggroDistance + float(worldConfig.player.playerGeneratedInformationByLevelCap - target->getLevel());
|
|
|
|
// Make sure that we wont go over the total range limits
|
|
if (aggroRadius > maxRadius)
|
|
aggroRadius = maxRadius;
|
|
else if (aggroRadius < minRadius)
|
|
aggroRadius = minRadius;
|
|
|
|
return aggroRadius;
|
|
}
|
|
|
|
void AIInterface::updateTotem(uint32_t p_time)
|
|
{
|
|
if (totemspell != nullptr)
|
|
{
|
|
if (p_time >= m_totemspelltimer)
|
|
{
|
|
Spell* pSpell = sSpellMgr.newSpell(m_Unit, totemspell, true, 0);
|
|
Unit* nextTarget = getCurrentTarget();
|
|
if (nextTarget == NULL ||
|
|
(!m_Unit->getWorldMap()->getUnit(nextTarget->getGuid()) ||
|
|
!nextTarget->isAlive() ||
|
|
!(m_Unit->isInRange(nextTarget->GetPosition(), pSpell->getSpellInfo()->custom_base_range_or_radius_sqr)) ||
|
|
!isAttackable(m_Unit, nextTarget, !(pSpell->getSpellInfo()->custom_c_is_flags & SPELL_FLAG_IS_TARGETINGSTEALTHED))
|
|
)
|
|
)
|
|
{
|
|
//we set no target and see if we managed to fid a new one
|
|
m_target = nullptr;
|
|
//something happened to our target, pick another one
|
|
SpellCastTargets targets(0);
|
|
pSpell->GenerateTargets(&targets);
|
|
if (targets.getTargetMask() & TARGET_FLAG_UNIT)
|
|
m_target = getUnit()->getWorldMapUnit(targets.getUnitTarget());
|
|
}
|
|
nextTarget = getCurrentTarget();
|
|
if (nextTarget)
|
|
{
|
|
SpellCastTargets targets(nextTarget->getGuid());
|
|
pSpell->prepare(&targets);
|
|
// need proper cooldown time!
|
|
m_totemspelltimer = m_totemspelltime;
|
|
}
|
|
else
|
|
{
|
|
delete pSpell;
|
|
pSpell = nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_totemspelltimer -= p_time;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sLogger.failure("AIInterface::updateTotem tried to update invalid totemspell");
|
|
}
|
|
}
|
|
|
|
void AIInterface::setImmuneToNPC(bool apply)
|
|
{
|
|
if (apply)
|
|
{
|
|
m_Unit->setUnitFlags(UNIT_FLAG_IGNORE_CREATURE_COMBAT);
|
|
}
|
|
else
|
|
{
|
|
m_Unit->removeUnitFlags(UNIT_FLAG_IGNORE_CREATURE_COMBAT);
|
|
}
|
|
}
|
|
|
|
void AIInterface::setImmuneToPC(bool apply)
|
|
{
|
|
if (apply)
|
|
{
|
|
m_Unit->setUnitFlags(UNIT_FLAG_IGNORE_PLAYER_COMBAT);
|
|
}
|
|
else
|
|
{
|
|
m_Unit->removeUnitFlags(UNIT_FLAG_IGNORE_PLAYER_COMBAT);
|
|
}
|
|
}
|
|
|
|
void AIInterface::onHostileAction(Unit* pUnit, SpellInfo const* spellInfo/* = nullptr*/, bool ignoreThreatRedirects/* = false*/)
|
|
{
|
|
const auto wasEngaged = isEngaged();
|
|
|
|
// Add initial threat
|
|
if (getUnit()->getThreatManager().canHaveThreatList())
|
|
getUnit()->getThreatManager().addThreat(pUnit, 0.0f, spellInfo, true, ignoreThreatRedirects);
|
|
else if (getUnit()->isPet())
|
|
attackStart(pUnit);
|
|
|
|
// Update combat for pure creatures only
|
|
if (getUnit()->getPlayerOwner() == nullptr)
|
|
getUnit()->getCombatHandler().onHostileAction(pUnit);
|
|
|
|
// Let players know that creature has aggroed them
|
|
// Pure creature targets do not need this
|
|
if (pUnit->getPlayerOwnerOrSelf() != nullptr)
|
|
pUnit->getCombatHandler().takeCombatAction(getUnit());
|
|
|
|
// Send hostile action event if unit was already engaged
|
|
// no need to send this if unit just started combat
|
|
if (wasEngaged)
|
|
handleEvent(EVENT_HOSTILEACTION, pUnit, 0);
|
|
}
|
|
|
|
void AIInterface::justEnteredCombat(Unit* pUnit)
|
|
{
|
|
if (!isEngaged() && getUnit()->getThreatManager().canHaveThreatList())
|
|
engagementStart(pUnit);
|
|
// Zyres: this looks pretty wrong - fix it if needed otherwise everything sill start engagement @aaron02
|
|
// else // maybe add more here this part down is for petAI
|
|
// engagementStart(pUnit);
|
|
}
|
|
|
|
void AIInterface::engagementStart(Unit* target)
|
|
{
|
|
if (isEngaged())
|
|
{
|
|
sLogger.debug("AIInterface::justEnteredCombat called but creature is already inCombat");
|
|
return;
|
|
}
|
|
|
|
m_isEngaged = true;
|
|
|
|
atEngagementStart(target);
|
|
}
|
|
|
|
void AIInterface::atEngagementStart(Unit* target)
|
|
{
|
|
// make AI group attack
|
|
if (auto group = sMySQLStore.getSpawnGroupDataBySpawn(getUnit()->ToCreature()->getSpawnId()))
|
|
{
|
|
for (auto members : group->spawns)
|
|
{
|
|
if (members.second && members.second->isAlive() && members.second->IsInWorld())
|
|
members.second->getAIInterface()->onHostileAction(target, nullptr, false);
|
|
}
|
|
}
|
|
|
|
MovementGeneratorType const movetype = getUnit()->getMovementManager()->getCurrentMovementGeneratorType();
|
|
if (movetype == WAYPOINT_MOTION_TYPE || movetype == POINT_MOTION_TYPE)
|
|
getUnit()->SetSpawnLocation(getUnit()->GetPosition());
|
|
|
|
if (CreatureGroup* formation = getUnit()->ToCreature()->getFormation())
|
|
formation->memberEngagingTarget(getUnit()->ToCreature(), target);
|
|
|
|
// find assistance
|
|
findAssistance();
|
|
|
|
setDefaultBoundary();
|
|
|
|
handleEvent(EVENT_ENTERCOMBAT, target, 0);
|
|
}
|
|
|
|
void AIInterface::engagementOver()
|
|
{
|
|
if (!m_isEngaged)
|
|
{
|
|
sLogger.debug("AIInterface::engagementOver called but creature is not inCombat");
|
|
return;
|
|
}
|
|
m_isEngaged = false;
|
|
|
|
atEngagementOver();
|
|
}
|
|
|
|
void AIInterface::atEngagementOver()
|
|
{
|
|
internalPhase = 0;
|
|
spellEvents.resetEvents();
|
|
setUnitToFollow(nullptr);
|
|
setCannotReachTarget(false);
|
|
setNoCallAssistance(false);
|
|
setCurrentTarget(nullptr);
|
|
getUnit()->setTargetGuid(0);
|
|
|
|
m_hasFleed = false;
|
|
m_fleeTimer.resetInterval(0);
|
|
setCurrentAgent(AGENT_NULL);
|
|
|
|
m_Unit->smsg_AttackStop(nullptr);
|
|
|
|
m_Unit->getThreatManager().clearAllThreat();
|
|
m_Unit->getThreatManager().removeMeFromThreatLists();
|
|
|
|
if (!m_disableDynamicBoundary)
|
|
clearBoundary();
|
|
|
|
// Remove Instance Combat
|
|
instanceCombatProgress(false);
|
|
}
|
|
|
|
void AIInterface::setCreatureProtoDifficulty(uint32_t entry)
|
|
{
|
|
if (getDifficultyType() != 0)
|
|
{
|
|
uint32_t creature_difficulty_entry = sMySQLStore.getCreatureDifficulty(entry, getDifficultyType());
|
|
auto properties_difficulty = sMySQLStore.getCreatureProperties(creature_difficulty_entry);
|
|
Creature* creature = static_cast<Creature*>(m_Unit);
|
|
if (properties_difficulty != nullptr)
|
|
{
|
|
if (!properties_difficulty->isTrainingDummy && !getUnit()->isVehicle())
|
|
{
|
|
getUnit()->getAIInterface()->setAllowedToEnterCombat(true);
|
|
}
|
|
|
|
getUnit()->setSpeedRate(TYPE_WALK, properties_difficulty->walk_speed, false);
|
|
getUnit()->setSpeedRate(TYPE_RUN, properties_difficulty->run_speed, false);
|
|
getUnit()->setSpeedRate(TYPE_FLY, properties_difficulty->fly_speed, false);
|
|
|
|
getUnit()->setScale(properties_difficulty->Scale);
|
|
|
|
uint32_t health = properties_difficulty->MinHealth + Util::getRandomUInt(properties_difficulty->MaxHealth - properties_difficulty->MinHealth);
|
|
|
|
getUnit()->setMaxHealth(health);
|
|
getUnit()->setHealth(health);
|
|
getUnit()->setBaseHealth(health);
|
|
|
|
getUnit()->setMaxPower(POWER_TYPE_MANA, properties_difficulty->Mana);
|
|
getUnit()->setBaseMana(properties_difficulty->Mana);
|
|
getUnit()->setPower(POWER_TYPE_MANA, properties_difficulty->Mana);
|
|
|
|
getUnit()->setLevel(properties_difficulty->MinLevel + (Util::getRandomUInt(properties_difficulty->MaxLevel - properties_difficulty->MinLevel)));
|
|
|
|
for (uint8_t i = 0; i < TOTAL_SPELL_SCHOOLS; ++i)
|
|
{
|
|
getUnit()->setResistance(i, properties_difficulty->Resistances[i]);
|
|
}
|
|
|
|
getUnit()->setBaseAttackTime(MELEE, properties_difficulty->AttackTime);
|
|
|
|
getUnit()->setMinDamage(properties_difficulty->MinDamage);
|
|
getUnit()->setMaxDamage(properties_difficulty->MaxDamage);
|
|
|
|
getUnit()->setBaseAttackTime(RANGED, properties_difficulty->RangedAttackTime);
|
|
getUnit()->setMinRangedDamage(properties_difficulty->RangedMinDamage);
|
|
getUnit()->setMaxRangedDamage(properties_difficulty->RangedMaxDamage);
|
|
|
|
|
|
getUnit()->setFaction(properties_difficulty->Faction);
|
|
|
|
if (!(getUnit()->m_factionEntry->RepListId == -1 && getUnit()->m_factionTemplate->HostileMask == 0 && getUnit()->m_factionTemplate->FriendlyMask == 0))
|
|
{
|
|
m_Unit->getAIInterface()->setCanCallForHelp(true);
|
|
}
|
|
|
|
if (properties_difficulty->CanRanged == 1)
|
|
getUnit()->getAIInterface()->m_canRangedAttack = true;
|
|
else
|
|
getUnit()->m_aiInterface->m_canRangedAttack = false;
|
|
|
|
getUnit()->setBoundingRadius(properties_difficulty->BoundingRadius);
|
|
|
|
getUnit()->setCombatReach(properties_difficulty->CombatReach);
|
|
|
|
getUnit()->setNpcFlags(properties_difficulty->NPCFLags);
|
|
|
|
// resistances
|
|
for (uint8_t j = 0; j < TOTAL_SPELL_SCHOOLS; ++j)
|
|
{
|
|
getUnit()->m_baseResistance[j] = getUnit()->getResistance(j);
|
|
}
|
|
|
|
for (uint8_t j = 0; j < STAT_COUNT; ++j)
|
|
{
|
|
getUnit()->m_baseStats[j] = getUnit()->getStat(j);
|
|
}
|
|
|
|
getUnit()->m_baseDamage[0] = getUnit()->getMinDamage();
|
|
getUnit()->m_baseDamage[1] = getUnit()->getMaxDamage();
|
|
getUnit()->m_baseOffhandDamage[0] = getUnit()->getMinOffhandDamage();
|
|
getUnit()->m_baseOffhandDamage[1] = getUnit()->getMaxOffhandDamage();
|
|
getUnit()->m_baseRangedDamage[0] = getUnit()->getMinRangedDamage();
|
|
getUnit()->m_baseRangedDamage[1] = getUnit()->getMaxRangedDamage();
|
|
|
|
creature->BaseAttackType = properties_difficulty->attackSchool;
|
|
|
|
/* // Dont was Used in old AIInterface left the code here if needed at other Date
|
|
if (properties_difficulty->guardtype == GUARDTYPE_CITY)
|
|
getUnit()->getAIInterface()->setGuard(true);
|
|
else
|
|
getUnit()->getAIInterface()->setGuard(false);*/
|
|
|
|
if (properties_difficulty->guardtype == GUARDTYPE_NEUTRAL)
|
|
getUnit()->getAIInterface()->setGuard(true);
|
|
else
|
|
getUnit()->getAIInterface()->setGuard(false);
|
|
|
|
#ifdef FT_VEHICLES
|
|
if (getUnit()->isVehicle())
|
|
{
|
|
getUnit()->createVehicleKit(properties_difficulty->Id, properties_difficulty->vehicleid);
|
|
getUnit()->addNpcFlags(UNIT_NPC_FLAG_SPELLCLICK);
|
|
getUnit()->setAItoUse(false);
|
|
}
|
|
#endif
|
|
|
|
if (properties_difficulty->rooted)
|
|
getUnit()->setMoveRoot(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
uint8_t AIInterface::getDifficultyType()
|
|
{
|
|
uint8_t difficulty_type;
|
|
|
|
InstanceMap* instance = sMapMgr.findInstanceMap(m_Unit->GetInstanceID());
|
|
if (instance != nullptr)
|
|
difficulty_type = instance->getDifficulty();
|
|
else
|
|
difficulty_type = 0; // standard MODE_NORMAL / MODE_NORMAL_10MEN
|
|
|
|
return difficulty_type;
|
|
}
|
|
|
|
void AIInterface::eventForceRedirected(Unit* /*pUnit*/, uint32_t /*misc1*/) { }
|
|
|
|
void AIInterface::eventHostileAction(Unit* /*pUnit*/, uint32_t /*misc1*/)
|
|
{
|
|
// On hostile action, set new default boundary
|
|
setDefaultBoundary();
|
|
}
|
|
|
|
void AIInterface::eventWander(Unit* /*pUnit*/, uint32_t /*misc1*/) { }
|
|
|
|
void AIInterface::eventUnwander(Unit* /*pUnit*/, uint32_t /*misc1*/) { }
|
|
|
|
void AIInterface::eventFear(Unit* pUnit, uint32_t /*misc1*/)
|
|
{
|
|
if (pUnit == nullptr)
|
|
return;
|
|
|
|
if (m_Unit->IsInWorld() && m_Unit->isCreature() && static_cast<Creature*>(m_Unit)->GetScript() != nullptr)
|
|
static_cast<Creature*>(m_Unit)->GetScript()->OnFear(pUnit, 0);
|
|
|
|
getUnit()->setControlled(true, UNIT_STATE_FLEEING);
|
|
}
|
|
|
|
void AIInterface::eventUnfear(Unit* /*pUnit*/, uint32_t /*misc1*/)
|
|
{
|
|
if (getUnit()->isInEvadeMode())
|
|
return;
|
|
|
|
getUnit()->setControlled(false, UNIT_STATE_FLEEING);
|
|
}
|
|
|
|
void AIInterface::eventFollowOwner(Unit* /*pUnit*/, uint32_t /*misc1*/)
|
|
{
|
|
getUnit()->getMovementManager()->clear();
|
|
getUnit()->getMovementManager()->moveFollow(getPetOwner(), PET_FOLLOW_DIST, getUnit()->getFollowAngle());
|
|
}
|
|
|
|
void AIInterface::eventDamageTaken(Unit* pUnit, uint32_t misc1)
|
|
{
|
|
if (getUnit()->isInEvadeMode())
|
|
return;
|
|
|
|
if (pUnit == nullptr)
|
|
return;
|
|
|
|
if (m_Unit->isCreature())
|
|
{
|
|
// On Damage Taken Scripts
|
|
for (auto onDamageTakenScript : onDamageTakenScripts)
|
|
{
|
|
if (onDamageTakenScript.action == actionSpell)
|
|
{
|
|
castAISpell(onDamageTakenScript.spellId);
|
|
}
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnDamageTaken, pUnit);
|
|
}
|
|
|
|
pUnit->removeAllAurasById(24575);
|
|
|
|
if (m_Unit->IsInWorld() && m_Unit->isCreature() && static_cast<Creature*>(m_Unit)->GetScript() != nullptr)
|
|
static_cast<Creature*>(m_Unit)->GetScript()->OnDamageTaken(pUnit, misc1);
|
|
}
|
|
|
|
void AIInterface::eventEnterCombat(Unit* pUnit, uint32_t /*misc1*/)
|
|
{
|
|
if (pUnit == nullptr || pUnit->isDead() || m_Unit->isDead())
|
|
return;
|
|
|
|
/* send the message */
|
|
if (m_Unit->isCreature())
|
|
{
|
|
if (const auto creature = dynamic_cast<Creature*>(m_Unit))
|
|
{
|
|
if (creature->IsInWorld() && m_Unit->isCreature() && creature->GetScript())
|
|
{
|
|
creature->GetScript()->_internalOnCombatStart(pUnit);
|
|
creature->GetScript()->OnCombatStart(pUnit);
|
|
}
|
|
|
|
if (m_Unit->getWorldMap() && m_Unit->getWorldMap()->getScript())
|
|
{
|
|
// set encounter state = InProgress
|
|
uint32_t i = 0;
|
|
for (const auto& boss : m_Unit->getWorldMap()->getScript()->getBosses())
|
|
{
|
|
if (m_Unit->getEntry() == boss.entry)
|
|
if (m_Unit->getWorldMap() && m_Unit->getWorldMap()->getScript())
|
|
m_Unit->getWorldMap()->getScript()->setBossState(i, InProgress);
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (creature->m_spawn && (creature->m_spawn->channel_target_go || creature->m_spawn->channel_target_creature))
|
|
{
|
|
m_Unit->setChannelSpellId(0);
|
|
m_Unit->setChannelObjectGuid(0);
|
|
}
|
|
}
|
|
|
|
// Enter Combat Scripts
|
|
for (auto onCombatStartScript : onCombatStartScripts)
|
|
{
|
|
if (onCombatStartScript.action == actionSpell)
|
|
{
|
|
castAISpell(onCombatStartScript.spellId);
|
|
}
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnCombatStart, pUnit);
|
|
}
|
|
|
|
// Stop the emote - change to fight emote
|
|
m_Unit->setEmoteState(EMOTE_STATE_READY1H);
|
|
|
|
// dismount if mounted
|
|
if (m_Unit->isCreature() && !(static_cast<Creature*>(m_Unit)->GetCreatureProperties()->typeFlags & CREATURE_FLAG1_FIGHT_MOUNTED))
|
|
m_Unit->setMountDisplayId(0);
|
|
|
|
// Instance Combat
|
|
instanceCombatProgress(true);
|
|
|
|
// Init Spells
|
|
initializeSpells();
|
|
|
|
// If the Player is in A Group add all Players to the Threat List
|
|
initGroupThreat(pUnit);
|
|
|
|
// Send attack sound
|
|
if (getUnit()->isCreature())
|
|
getUnit()->SendAIReaction();
|
|
|
|
// Put mob into combat animation. Take out weapons and start to look serious :P
|
|
m_Unit->smsg_AttackStart(pUnit);
|
|
}
|
|
|
|
void AIInterface::instanceCombatProgress(bool activate)
|
|
{
|
|
if (getUnit()->getWorldMap() && getUnit()->getWorldMap()->getBaseMap()->getMapInfo() && getUnit()->getWorldMap()->getBaseMap()->getMapInfo()->isRaid())
|
|
{
|
|
if (getUnit()->isCreature())
|
|
{
|
|
if (static_cast<Creature*>(getUnit())->GetCreatureProperties()->Rank == 3)
|
|
{
|
|
if (activate)
|
|
getUnit()->getWorldMap()->addCombatInProgress(getUnit()->getGuid());
|
|
else
|
|
getUnit()->getWorldMap()->removeCombatInProgress(getUnit()->getGuid());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIInterface::initGroupThreat(Unit* target)
|
|
{
|
|
if (target->isPlayer() && static_cast<Player*>(target)->isInGroup())
|
|
{
|
|
Group* pGroup = static_cast<Player*>(target)->getGroup();
|
|
|
|
pGroup->Lock();
|
|
for (uint32_t i = 0; i < pGroup->GetSubGroupCount(); i++)
|
|
{
|
|
for (GroupMembersSet::iterator itr = pGroup->GetSubGroup(i)->GetGroupMembersBegin(); itr != pGroup->GetSubGroup(i)->GetGroupMembersEnd(); ++itr)
|
|
{
|
|
Player* pGroupGuy = sObjectMgr.GetPlayer((*itr)->guid);
|
|
if (pGroupGuy && pGroupGuy->isAlive() && m_Unit->getWorldMap() == pGroupGuy->getWorldMap() && pGroupGuy->getDistanceSq(target) <= 40 * 40) //50 yards for now. lets see if it works
|
|
{
|
|
m_Unit->getThreatManager().addThreat(pGroupGuy, 0.0f, nullptr, true, true);
|
|
}
|
|
}
|
|
}
|
|
pGroup->Unlock();
|
|
}
|
|
}
|
|
|
|
void AIInterface::eventLeaveCombat(Unit* /*pUnit*/, uint32_t /*misc1*/)
|
|
{
|
|
if (m_Unit->isCreature())
|
|
{
|
|
if (m_Unit->isDead())
|
|
m_Unit->removeAllAuras();
|
|
else
|
|
m_Unit->removeAllNegativeAuras();
|
|
}
|
|
|
|
// restart emote
|
|
if (m_Unit->isCreature())
|
|
{
|
|
Creature* creature = static_cast<Creature*>(m_Unit);
|
|
|
|
if (creature->original_emotestate)
|
|
m_Unit->setEmoteState(creature->original_emotestate);
|
|
else
|
|
m_Unit->setEmoteState(EMOTE_ONESHOT_NONE);
|
|
|
|
if (creature->m_spawn && (creature->m_spawn->channel_target_go || creature->m_spawn->channel_target_creature))
|
|
{
|
|
if (creature->m_spawn->channel_target_go)
|
|
sEventMgr.AddEvent(creature, &Creature::ChannelLinkUpGO, creature->m_spawn->channel_target_go, EVENT_CREATURE_CHANNEL_LINKUP, 1000, 5, 0);
|
|
|
|
if (creature->m_spawn->channel_target_creature)
|
|
sEventMgr.AddEvent(creature, &Creature::ChannelLinkUpCreature, creature->m_spawn->channel_target_creature, EVENT_CREATURE_CHANNEL_LINKUP, 1000, 5, 0);
|
|
}
|
|
|
|
// Leave Combat Scripts
|
|
for (auto onLeaveCombatScript : onLeaveCombatScripts)
|
|
{
|
|
if (onLeaveCombatScript.action == actionSpell)
|
|
{
|
|
castAISpell(onLeaveCombatScript.spellId);
|
|
}
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnLeaveCombat, nullptr);
|
|
}
|
|
|
|
if (m_Unit->isCreature() && m_Unit->isAlive())
|
|
{
|
|
if (m_Unit->getWorldMap() && m_Unit->getWorldMap()->getScript())
|
|
{
|
|
// Reset Instance Data
|
|
// set encounter state back to NotStarted
|
|
uint32_t i = 0;
|
|
for (const auto boss : m_Unit->getWorldMap()->getScript()->getBosses())
|
|
{
|
|
if (m_Unit->getEntry() == boss.entry)
|
|
if (m_Unit->getWorldMap() && m_Unit->getWorldMap()->getScript())
|
|
m_Unit->getWorldMap()->getScript()->setBossState(i, NotStarted);
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// Respawn all Npcs from Current Group if needed
|
|
auto data = sMySQLStore.getSpawnGroupDataBySpawn(getUnit()->ToCreature()->getSpawnId());
|
|
if (data && data->spawnFlags & SPAWFLAG_FLAG_FULLPACK)
|
|
{
|
|
if (!m_Unit->getWorldMap()->isUnloadPending())
|
|
{
|
|
for (auto spawns : data->spawns)
|
|
{
|
|
if (spawns.second && spawns.second->m_spawn && !spawns.second->isAlive())
|
|
spawns.second->Despawn(0, 1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remount if mounted
|
|
Creature* creature = static_cast<Creature*>(m_Unit);
|
|
if (creature->m_spawn)
|
|
m_Unit->setMountDisplayId(creature->m_spawn->MountedDisplayID);
|
|
}
|
|
|
|
initialiseScripts(getUnit()->getEntry());
|
|
|
|
// when this leads to errors remove
|
|
if (m_Unit->getWorldMap() && m_Unit->getWorldMap()->isUnloadPending())
|
|
return;
|
|
|
|
if (m_Unit->IsInWorld() && m_Unit->isCreature() && dynamic_cast<Creature*>(m_Unit)->GetScript())
|
|
{
|
|
dynamic_cast<Creature*>(m_Unit)->GetScript()->_internalOnCombatStop();
|
|
dynamic_cast<Creature*>(m_Unit)->GetScript()->OnCombatStop(getUnit());
|
|
}
|
|
}
|
|
|
|
void AIInterface::eventUnitDied(Unit* pUnit, uint32_t /*misc1*/)
|
|
{
|
|
if (pUnit == nullptr)
|
|
return;
|
|
|
|
pUnit->removeAllNegativeAuras();
|
|
|
|
if (m_Unit->IsInWorld() && m_Unit->isCreature() && dynamic_cast<Creature*>(m_Unit)->GetScript())
|
|
{
|
|
dynamic_cast<Creature*>(m_Unit)->GetScript()->_internalOnDied(pUnit);
|
|
dynamic_cast<Creature*>(m_Unit)->GetScript()->OnDied(pUnit);
|
|
}
|
|
|
|
if (m_Unit->isCreature())
|
|
{
|
|
// Died Scripts
|
|
for (auto onDiedScript : onDiedScripts)
|
|
{
|
|
// Placeholder for further actions
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnDied, pUnit);
|
|
|
|
initialiseScripts(getUnit()->getEntry());
|
|
|
|
// Script Hook for Summon Died
|
|
if (Summon* summon = getUnit()->ToSummon())
|
|
{
|
|
if (Unit* summoner = summon->getUnitOwner())
|
|
{
|
|
if (summoner->ToCreature() && summoner->IsInWorld() && summoner->ToCreature()->GetScript())
|
|
dynamic_cast<Creature*>(summoner)->GetScript()->OnSummonDies(m_Unit->ToCreature(), pUnit);
|
|
}
|
|
}
|
|
|
|
if (m_Unit->getWorldMap() && m_Unit->getWorldMap()->getScript())
|
|
m_Unit->getWorldMap()->getScript()->OnCreatureDeath(dynamic_cast<Creature*>(m_Unit), pUnit);
|
|
}
|
|
|
|
m_Unit->setMountDisplayId(0);
|
|
|
|
InstanceMap* pInstance = nullptr;
|
|
auto unitMapMgr = m_Unit->getWorldMap();
|
|
if (unitMapMgr)
|
|
pInstance = m_Unit->getWorldMap()->getInstance();
|
|
|
|
if (unitMapMgr
|
|
&& m_Unit->isCreature()
|
|
&& !m_Unit->isPet()
|
|
&& pInstance)
|
|
{
|
|
Creature* pCreature = static_cast<Creature*>(m_Unit);
|
|
|
|
if (pInstance->isRaidOrHeroicDungeon())
|
|
{
|
|
if (pCreature->isDungeonBoss())
|
|
pInstance->permBindAllPlayers();
|
|
}
|
|
else
|
|
{
|
|
// the reset time is set but not added to the scheduler
|
|
// until the players leave the instance
|
|
const auto now = Util::getTimeNow();
|
|
time_t resettime = now + 2 * HOUR;
|
|
if (InstanceSaved* save = sInstanceMgr.getInstanceSave(pCreature->GetInstanceID()))
|
|
if (save->getResetTime() < resettime)
|
|
save->setResetTime(resettime);
|
|
}
|
|
|
|
if (m_Unit->getWorldMap() && m_Unit->getWorldMap()->getScript())
|
|
{
|
|
// Set Instance Data
|
|
// set encounter state to Performed
|
|
uint32_t i = 0;
|
|
for (const auto boss : m_Unit->getWorldMap()->getScript()->getBosses())
|
|
{
|
|
if (m_Unit->getEntry() == boss.entry)
|
|
{
|
|
if (m_Unit->getWorldMap() && m_Unit->getWorldMap()->getScript())
|
|
m_Unit->getWorldMap()->getScript()->setBossState(i, Performed);
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// Killed Group checks
|
|
auto spawnGroupData = sMySQLStore.getSpawnGroupDataBySpawn(pCreature->spawnid);
|
|
|
|
// Spawn Group Handling
|
|
if (spawnGroupData && spawnGroupData->groupId)
|
|
{
|
|
bool killed = true;
|
|
for (auto spawns : spawnGroupData->spawns)
|
|
{
|
|
if (!unitMapMgr->getRespawnInfo(SPAWN_TYPE_CREATURE, spawns.first))
|
|
killed = false;
|
|
}
|
|
|
|
if (killed)
|
|
{
|
|
if (unitMapMgr->getScript())
|
|
unitMapMgr->getScript()->OnSpawnGroupKilled(spawnGroupData->groupId);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unitMapMgr && unitMapMgr->getBaseMap()->getMapInfo() && unitMapMgr->getBaseMap()->getMapInfo()->isRaid())
|
|
{
|
|
if (m_Unit->isCreature())
|
|
{
|
|
if (dynamic_cast<Creature*>(m_Unit)->GetCreatureProperties()->Rank == 3)
|
|
{
|
|
unitMapMgr->removeCombatInProgress(m_Unit->getGuid());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIInterface::eventOnLoad()
|
|
{
|
|
// On Load Scripts
|
|
if (onLoadScripts.size())
|
|
{
|
|
for (auto onLoadScript : onLoadScripts)
|
|
{
|
|
switch (onLoadScript.action)
|
|
{
|
|
case actionSpell:
|
|
{
|
|
castAISpell(onLoadScript.spellId);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnLoad, nullptr);
|
|
}
|
|
}
|
|
|
|
void AIInterface::eventOnTargetDied(Object* /*pKiller*/)
|
|
{
|
|
// Killed Scripts
|
|
for (auto onKilledScript : onKilledScripts)
|
|
{
|
|
switch (onKilledScript.action)
|
|
{
|
|
case actionPhaseChange:
|
|
{
|
|
if (onKilledScript.phase > 0 || onKilledScript.phase == internalPhase)
|
|
{
|
|
if (float(getUnit()->getHealthPct()) <= onKilledScript.maxHealth && onKilledScript.maxCount)
|
|
{
|
|
internalPhase = static_cast<uint8_t>(onKilledScript.misc1);
|
|
|
|
onKilledScript.maxCount = onKilledScript.maxCount - 1U;
|
|
|
|
MySQLStructure::NpcScriptText const* npcScriptText = sMySQLStore.getNpcScriptText(onKilledScript.textId);
|
|
if (npcScriptText != nullptr)
|
|
{
|
|
getUnit()->sendChatMessage(npcScriptText->type, LANG_UNIVERSAL, npcScriptText->text);
|
|
|
|
if (npcScriptText->sound != 0)
|
|
getUnit()->PlaySoundToSet(npcScriptText->sound);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case actionSpell:
|
|
{
|
|
castAISpell(onKilledScript.spellId);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Send a Chatmessage from our AI Scripts
|
|
sendStoredText(mEmotesOnTargetDied, nullptr);
|
|
}
|
|
|
|
void AIInterface::setCannotReachTarget(bool cannotReach)
|
|
{
|
|
if (cannotReach == m_cannotReachTarget)
|
|
return;
|
|
m_cannotReachTarget = cannotReach;
|
|
m_cannotReachTimer.resetInterval(5000);
|
|
}
|
|
|
|
void AIInterface::initializeReactState()
|
|
{
|
|
if (getUnit()->isTotem() || getUnit()->isCritter() || getUnit()->isTrainingDummy())
|
|
setReactState(REACT_PASSIVE);
|
|
else
|
|
setReactState(REACT_AGGRESSIVE);
|
|
}
|
|
|
|
bool AIInterface::checkBoundary()
|
|
{
|
|
if (!isInBoundary(getUnit()->GetPosition()))
|
|
{
|
|
enterEvadeMode();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AIInterface::isInBoundary(LocationVector who) const
|
|
{
|
|
if (_boundary.empty())
|
|
return true;
|
|
|
|
return AIInterface::isInBounds(&_boundary, who) != _negateBoundary;
|
|
}
|
|
|
|
bool AIInterface::isInBoundary() const
|
|
{
|
|
if (_boundary.empty())
|
|
return true;
|
|
|
|
return AIInterface::isInBounds(&_boundary, getUnit()->GetPosition()) != _negateBoundary;
|
|
}
|
|
|
|
/*static*/ bool AIInterface::isInBounds(CreatureBoundary const* boundary, LocationVector pos)
|
|
{
|
|
for (AreaBoundary const* areaBoundary : *boundary)
|
|
if (!areaBoundary->isWithinBoundary(pos))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void AIInterface::addBoundary(AreaBoundary const* boundary, bool overrideDefault/* = false*/, bool negateBoundaries /*= false*/)
|
|
{
|
|
if (boundary == nullptr)
|
|
return;
|
|
|
|
if (overrideDefault && !m_disableDynamicBoundary)
|
|
{
|
|
// On first custom boundary, clear existing default/dynamic boundaries
|
|
clearBoundary();
|
|
m_disableDynamicBoundary = true;
|
|
}
|
|
|
|
_boundary.push_back(boundary);
|
|
_negateBoundary = negateBoundaries;
|
|
doImmediateBoundaryCheck();
|
|
}
|
|
|
|
void AIInterface::setDefaultBoundary()
|
|
{
|
|
if (m_disableDynamicBoundary)
|
|
return;
|
|
|
|
// Do net set default boundaries to creatures in raids or dungeons
|
|
// Mobs and bosses will chase players to instance portal unless custom boundaries are set
|
|
if (m_Unit->getWorldMap()->getBaseMap()->getMapInfo()->isInstanceMap())
|
|
return;
|
|
|
|
if (m_Unit->isPet() || m_Unit->isSummon())
|
|
return;
|
|
|
|
// Clear existing boundaries
|
|
clearBoundary();
|
|
|
|
// Default boundary 50 yards
|
|
addBoundary(new CircleBoundary(getUnit()->GetPosition(), 50.0f));
|
|
}
|
|
|
|
void AIInterface::clearBoundary()
|
|
{
|
|
for (auto& boundaryItr : _boundary)
|
|
delete boundaryItr;
|
|
|
|
_boundary.clear();
|
|
}
|
|
|
|
void AIInterface::movementInform(uint32_t type, uint32_t id)
|
|
{
|
|
sendStoredText(mEmotesOnRandomWaypoint, nullptr);
|
|
if (m_Unit->IsInWorld() && m_Unit->isCreature() && static_cast<Creature*>(m_Unit)->GetScript())
|
|
static_cast<Creature*>(m_Unit)->GetScript()->OnReachWP(type, id);
|
|
}
|
|
|
|
void AIInterface::eventChangeFaction(Unit* ForceAttackersToHateThisInstead)
|
|
{
|
|
getUnit()->getThreatManager().removeMeFromThreatLists();
|
|
|
|
//we need a new assist list
|
|
m_assistTargets.clear();
|
|
|
|
//Clear targettable
|
|
if (ForceAttackersToHateThisInstead == nullptr)
|
|
{
|
|
for (const auto& itr : m_Unit->getInRangeObjectsSet())
|
|
{
|
|
if (itr && itr->isCreatureOrPlayer() && static_cast<Unit*>(itr)->getAIInterface())
|
|
{
|
|
static_cast<Unit*>(itr)->getThreatManager().clearThreat(m_Unit);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (const auto& itr : m_Unit->getInRangeObjectsSet())
|
|
{
|
|
if (itr && itr->isCreatureOrPlayer()) //this guy will join me in fight since I'm telling him "sorry i was controlled"
|
|
{
|
|
static_cast<Unit*>(itr)->getThreatManager().addThreat(ForceAttackersToHateThisInstead, 10.0f); //just aping to be bale to hate him in case we got nothing else
|
|
static_cast<Unit*>(itr)->getThreatManager().clearThreat(m_Unit);
|
|
}
|
|
}
|
|
|
|
getUnit()->getThreatManager().addThreat(ForceAttackersToHateThisInstead, 0.0f);
|
|
}
|
|
}
|
|
|
|
bool AIInterface::moveTo(float x, float y, float z, float /*o = 0.0f*/, bool running/*= false*/)
|
|
{
|
|
if (m_Unit->isRooted() || m_Unit->isStunned())
|
|
{
|
|
m_Unit->stopMoving() ; //Just Stop
|
|
return false;
|
|
}
|
|
|
|
MovementMgr::MoveSplineInit init(m_Unit);
|
|
init.MoveTo(x, y, z);
|
|
if (running)
|
|
init.SetWalk(false);
|
|
else
|
|
init.SetWalk(true);
|
|
|
|
m_Unit->getMovementManager()->launchMoveSpline(std::move(init));
|
|
|
|
return true;
|
|
}
|
|
|
|
void AIInterface::calcDestinationAndMove(Unit* target, float dist)
|
|
{
|
|
if (m_Unit->isRooted() || m_Unit->isStunned())
|
|
{
|
|
m_Unit->stopMoving(); //Just Stop
|
|
return;
|
|
}
|
|
|
|
float newx, newy, newz;
|
|
|
|
if (target != nullptr)
|
|
{
|
|
newx = target->GetPositionX();
|
|
newy = target->GetPositionY();
|
|
newz = target->GetPositionZ();
|
|
|
|
//avoid eating bandwidth with useless movement packets when target did not move since last position
|
|
//this will work since it turned into a common myth that when you pull mob you should not move :D
|
|
if (abs(m_lasttargetPosition.x - newx) < MIN_WALK_DISTANCE
|
|
&& abs(m_lasttargetPosition.y - newy) < MIN_WALK_DISTANCE)
|
|
return;
|
|
|
|
m_lasttargetPosition.x = newx;
|
|
m_lasttargetPosition.y = newy;
|
|
|
|
float angle = m_Unit->calcAngle(m_Unit->GetPositionX(), m_Unit->GetPositionY(), newx, newy) * M_PI_FLOAT / 180.0f;
|
|
float x = dist * cosf(angle);
|
|
float y = dist * sinf(angle);
|
|
|
|
if (target->isPlayer() && static_cast<Player*>(target)->isMoving())
|
|
{
|
|
// cater for moving player vector based on orientation
|
|
x -= cosf(target->GetOrientation());
|
|
y -= sinf(target->GetOrientation());
|
|
}
|
|
|
|
newx -= x;
|
|
newy -= y;
|
|
}
|
|
else
|
|
{
|
|
newx = m_Unit->GetPositionX();
|
|
newy = m_Unit->GetPositionY();
|
|
newz = m_Unit->GetPositionZ();
|
|
}
|
|
|
|
MovementMgr::MoveSplineInit init(m_Unit);
|
|
init.MoveTo(newx, newy, newz);
|
|
init.SetWalk(true);
|
|
m_Unit->getMovementManager()->launchMoveSpline(std::move(init));
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// Waypoint functions
|
|
bool AIInterface::hasWayPoints()
|
|
{
|
|
if (getUnit()->ToCreature()->getWaypointPath())
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
uint32_t AIInterface::getCurrentWayPointId()
|
|
{
|
|
return getUnit()->ToCreature()->getCurrentWaypointInfo().first;
|
|
}
|
|
|
|
uint32_t AIInterface::getWayPointsCount()
|
|
{
|
|
if (hasWayPoints() && sWaypointMgr->getPath(getUnit()->ToCreature()->getWaypointPath()))
|
|
return static_cast<uint32_t>(sWaypointMgr->getPath(getUnit()->ToCreature()->getWaypointPath())->nodes.size());
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
void AIInterface::setWayPointToMove(uint32_t waypointId)
|
|
{
|
|
auto _path = sWaypointMgr->getPath(getUnit()->ToCreature()->getWaypointPath());
|
|
WaypointNode const &waypoint = _path->nodes[waypointId];
|
|
|
|
MovementMgr::MoveSplineInit init(getUnit());
|
|
init.MoveTo(waypoint.x, waypoint.y, waypoint.z);
|
|
|
|
//! Accepts angles such as 0.00001 and -0.00001, 0 must be ignored, default value in waypoint table
|
|
if (waypoint.orientation && waypoint.delay)
|
|
init.SetFacing(waypoint.orientation);
|
|
|
|
switch (waypoint.moveType)
|
|
{
|
|
case WAYPOINT_MOVE_TYPE_LAND:
|
|
init.SetAnimation(AnimationTier::Ground);
|
|
break;
|
|
case WAYPOINT_MOVE_TYPE_TAKEOFF:
|
|
init.SetAnimation(AnimationTier::Hover);
|
|
break;
|
|
case WAYPOINT_MOVE_TYPE_RUN:
|
|
init.SetWalk(false);
|
|
break;
|
|
case WAYPOINT_MOVE_TYPE_WALK:
|
|
init.SetWalk(true);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
init.Launch();
|
|
}
|
|
|
|
bool AIInterface::activateShowWayPoints(Player* player, bool /*showBackwards*/)
|
|
{
|
|
if (sWaypointMgr->getPath(getUnit()->ToCreature()->getWaypointPath()) == nullptr)
|
|
return false;
|
|
|
|
auto mWayPointMap = sWaypointMgr->getPath(getUnit()->ToCreature()->getWaypointPath());
|
|
|
|
if (mShowWayPoints == true)
|
|
return false;
|
|
|
|
mShowWayPoints = true;
|
|
|
|
for (auto &wayPoint : mWayPointMap->nodes)
|
|
{
|
|
Creature* targetCreature = static_cast<Creature*>(getUnit());
|
|
|
|
Creature* wpCreature = new Creature((uint64)HIGHGUID_TYPE_WAYPOINT << 32 | wayPoint.id);
|
|
wpCreature->CreateWayPoint(wayPoint.id, player->GetMapId(), wayPoint.x, wayPoint.y, wayPoint.z, 0);
|
|
wpCreature->SetCreatureProperties(targetCreature->GetCreatureProperties());
|
|
wpCreature->setEntry(1);
|
|
wpCreature->setScale(0.5f);
|
|
|
|
const uint32_t displayId = getUnit()->getNativeDisplayId();
|
|
|
|
wpCreature->setDisplayId(displayId);
|
|
|
|
wpCreature->setLevel(wayPoint.id);
|
|
wpCreature->setNpcFlags(UNIT_NPC_FLAG_NONE);
|
|
wpCreature->setFaction(player->getFactionTemplate());
|
|
wpCreature->setMaxHealth(1);
|
|
wpCreature->setHealth(1);
|
|
|
|
ByteBuffer buf(3000);
|
|
uint32_t count = wpCreature->buildCreateUpdateBlockForPlayer(&buf, player);
|
|
player->getUpdateMgr().pushCreationData(&buf, count);
|
|
|
|
wpCreature->setMoveRoot(true);
|
|
|
|
delete wpCreature;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool AIInterface::isShowWayPointsActive()
|
|
{
|
|
return mShowWayPoints;
|
|
}
|
|
|
|
bool AIInterface::hideWayPoints(Player* player)
|
|
{
|
|
auto mWayPointMap = sWaypointMgr->getPath(getUnit()->ToCreature()->getWaypointPath());
|
|
|
|
if (mWayPointMap == nullptr)
|
|
return false;
|
|
|
|
if (mShowWayPoints != true)
|
|
return false;
|
|
|
|
mShowWayPoints = false;
|
|
|
|
for (auto &wayPoint : mWayPointMap->nodes)
|
|
{
|
|
uint64_t guid = ((uint64_t)HIGHGUID_TYPE_WAYPOINT << 32) | wayPoint.id;
|
|
WoWGuid wowguid(guid);
|
|
player->getUpdateMgr().pushOutOfRangeGuid(wowguid);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void AIInterface::waypointStarted(uint32_t nodeId, uint32_t pathId)
|
|
{
|
|
if (getUnit()->ToCreature() && getUnit()->ToCreature()->GetScript())
|
|
getUnit()->ToCreature()->GetScript()->waypointStarted(nodeId, pathId);
|
|
}
|
|
|
|
void AIInterface::waypointReached(uint32_t nodeId, uint32_t pathId)
|
|
{
|
|
if (getUnit()->ToCreature() && getUnit()->ToCreature()->GetScript())
|
|
getUnit()->ToCreature()->GetScript()->waypointReached(nodeId, pathId);
|
|
}
|
|
|
|
void AIInterface::waypointPathEnded(uint32_t nodeId, uint32_t pathId)
|
|
{
|
|
if (getUnit()->ToCreature() && getUnit()->ToCreature()->GetScript())
|
|
getUnit()->ToCreature()->GetScript()->waypointPathEnded(nodeId, pathId);
|
|
}
|
|
|
|
bool AIInterface::canCreatePath(float x, float y, float z)
|
|
{
|
|
//make sure current spline is updated
|
|
MMAP::MMapManager* mmap = MMAP::MMapFactory::createOrGetMMapManager();
|
|
dtNavMesh* nav = const_cast<dtNavMesh*>(mmap->GetNavMesh(m_Unit->GetMapId()));
|
|
dtNavMeshQuery* nav_query = const_cast<dtNavMeshQuery*>(mmap->GetNavMeshQuery(m_Unit->GetMapId(), m_Unit->GetInstanceID()));
|
|
//NavMeshData* nav = CollideInterface.GetNavMesh(m_Unit->GetMapId());
|
|
|
|
if (nav == nullptr)
|
|
return false;
|
|
|
|
float start[VERTEX_SIZE] = { m_Unit->GetPositionY(), m_Unit->GetPositionZ(), m_Unit->GetPositionX() };
|
|
float end[VERTEX_SIZE] = { y, z, x };
|
|
float extents[VERTEX_SIZE] = { 3.0f, 5.0f, 3.0f };
|
|
float closest_point[VERTEX_SIZE] = { 0.0f, 0.0f, 0.0f };
|
|
|
|
dtQueryFilter filter;
|
|
filter.setIncludeFlags(NAV_GROUND | NAV_WATER | NAV_MAGMA_SLIME);
|
|
|
|
dtPolyRef startref, endref;
|
|
|
|
if (dtStatusFailed(nav_query->findNearestPoly(start, extents, &filter, &startref, closest_point)))
|
|
return false;
|
|
|
|
if (dtStatusFailed(nav_query->findNearestPoly(end, extents, &filter, &endref, closest_point)))
|
|
return false;
|
|
|
|
if (startref == 0 || endref == 0)
|
|
return false;
|
|
|
|
dtPolyRef path[256];
|
|
int pathcount;
|
|
|
|
if (dtStatusFailed(nav_query->findPath(startref, endref, start, end, &filter, path, &pathcount, 256)))
|
|
return false;
|
|
|
|
if (pathcount == 0 || path[pathcount - 1] != endref)
|
|
return false;
|
|
|
|
float points[MAX_PATH_LENGTH * 3];
|
|
uint32_t pointcount;
|
|
bool usedoffmesh;
|
|
|
|
if (dtStatusFailed(findSmoothPath(start, end, path, pathcount, points, &pointcount, usedoffmesh, MAX_PATH_LENGTH, nav, nav_query, filter)))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
dtStatus AIInterface::findSmoothPath(const float* startPos, const float* endPos, const dtPolyRef* polyPath, const uint32 polyPathSize, float* smoothPath, uint32_t* smoothPathSize, bool & usedOffmesh, const uint32 maxSmoothPathSize, dtNavMesh* mesh, dtNavMeshQuery* query, dtQueryFilter & filter)
|
|
{
|
|
*smoothPathSize = 0;
|
|
uint32 nsmoothPath = 0;
|
|
usedOffmesh = false;
|
|
|
|
dtPolyRef polys[MAX_PATH_LENGTH];
|
|
memcpy(polys, polyPath, sizeof(dtPolyRef)*polyPathSize);
|
|
uint32 npolys = polyPathSize;
|
|
|
|
float iterPos[VERTEX_SIZE], targetPos[VERTEX_SIZE];
|
|
if (dtStatusFailed(query->closestPointOnPolyBoundary(polys[0], startPos, iterPos)))
|
|
return DT_FAILURE | DT_OUT_OF_MEMORY;
|
|
|
|
if (dtStatusFailed(query->closestPointOnPolyBoundary(polys[npolys - 1], endPos, targetPos)))
|
|
return DT_FAILURE | DT_OUT_OF_MEMORY;
|
|
|
|
dtVcopy(&smoothPath[nsmoothPath * VERTEX_SIZE], iterPos);
|
|
nsmoothPath++;
|
|
|
|
// Move towards target a small advancement at a time until target reached or
|
|
// when ran out of memory to store the path.
|
|
while (npolys && nsmoothPath < maxSmoothPathSize)
|
|
{
|
|
// Find location to steer towards.
|
|
float steerPos[VERTEX_SIZE];
|
|
unsigned char steerPosFlag;
|
|
dtPolyRef steerPosRef = 0;
|
|
|
|
if (!getSteerTarget(iterPos, targetPos, SMOOTH_PATH_SLOP, polys, npolys, steerPos, steerPosFlag, steerPosRef, query))
|
|
break;
|
|
|
|
bool endOfPath = (steerPosFlag & DT_STRAIGHTPATH_END) != 0;
|
|
bool offMeshConnection = (steerPosFlag & DT_STRAIGHTPATH_OFFMESH_CONNECTION) != 0;
|
|
|
|
// Find movement delta.
|
|
float delta[VERTEX_SIZE];
|
|
dtVsub(delta, steerPos, iterPos);
|
|
float len = dtMathSqrtf(dtVdot(delta, delta));
|
|
// If the steer target is end of path or off-mesh link, do not move past the location.
|
|
if ((endOfPath || offMeshConnection) && len < SMOOTH_PATH_STEP_SIZE)
|
|
len = 1.0f;
|
|
else
|
|
len = SMOOTH_PATH_STEP_SIZE / len;
|
|
|
|
float moveTgt[VERTEX_SIZE];
|
|
dtVmad(moveTgt, iterPos, delta, len);
|
|
|
|
// Move
|
|
float result[VERTEX_SIZE];
|
|
const static uint32 MAX_VISIT_POLY = 16;
|
|
dtPolyRef visited[MAX_VISIT_POLY];
|
|
|
|
uint32 nvisited = 0;
|
|
query->moveAlongSurface(polys[0], iterPos, moveTgt, &filter, result, visited, (int*)&nvisited, MAX_VISIT_POLY);
|
|
npolys = fixupCorridor(polys, npolys, MAX_PATH_LENGTH, visited, nvisited);
|
|
|
|
query->getPolyHeight(visited[nvisited - 1], result, &result[1]);
|
|
dtVcopy(iterPos, result);
|
|
|
|
// Handle end of path and off-mesh links when close enough.
|
|
if (endOfPath && inRangeYZX(iterPos, steerPos, SMOOTH_PATH_SLOP, 2.0f))
|
|
{
|
|
// Reached end of path.
|
|
dtVcopy(iterPos, targetPos);
|
|
if (nsmoothPath < maxSmoothPathSize)
|
|
{
|
|
dtVcopy(&smoothPath[nsmoothPath * VERTEX_SIZE], iterPos);
|
|
nsmoothPath++;
|
|
}
|
|
break;
|
|
}
|
|
else if (offMeshConnection && inRangeYZX(iterPos, steerPos, SMOOTH_PATH_SLOP, 2.0f))
|
|
{
|
|
// Reached off-mesh connection.
|
|
usedOffmesh = true;
|
|
|
|
// Advance the path up to and over the off-mesh connection.
|
|
dtPolyRef prevRef = 0;
|
|
dtPolyRef polyRef = polys[0];
|
|
uint32 npos = 0;
|
|
while (npos < npolys && polyRef != steerPosRef)
|
|
{
|
|
prevRef = polyRef;
|
|
polyRef = polys[npos];
|
|
npos++;
|
|
}
|
|
|
|
for (uint32 i = npos; i < npolys; ++i)
|
|
{
|
|
polys[i - npos] = polys[i];
|
|
}
|
|
|
|
npolys -= npos;
|
|
|
|
// Handle the connection.
|
|
float startPos2[VERTEX_SIZE], endPos2[VERTEX_SIZE];
|
|
if (!dtStatusFailed(mesh->getOffMeshConnectionPolyEndPoints(prevRef, polyRef, startPos2, endPos2)))
|
|
{
|
|
if (nsmoothPath < maxSmoothPathSize)
|
|
{
|
|
dtVcopy(&smoothPath[nsmoothPath * VERTEX_SIZE], startPos2);
|
|
nsmoothPath++;
|
|
}
|
|
// Move position at the other side of the off-mesh link.
|
|
dtVcopy(iterPos, endPos2);
|
|
query->getPolyHeight(polys[0], iterPos, &iterPos[1]);
|
|
}
|
|
}
|
|
|
|
// Store results.
|
|
if (nsmoothPath < maxSmoothPathSize)
|
|
{
|
|
dtVcopy(&smoothPath[nsmoothPath * VERTEX_SIZE], iterPos);
|
|
nsmoothPath++;
|
|
}
|
|
}
|
|
|
|
*smoothPathSize = nsmoothPath;
|
|
|
|
// this is most likely loop
|
|
return nsmoothPath < maxSmoothPathSize ? DT_SUCCESS : DT_FAILURE;
|
|
}
|
|
|
|
bool AIInterface::getSteerTarget(const float* startPos, const float* endPos, const float minTargetDist, const dtPolyRef* path, const uint32 pathSize, float* steerPos, unsigned char & steerPosFlag, dtPolyRef & steerPosRef, dtNavMeshQuery* query)
|
|
{
|
|
// Find steer target.
|
|
static const int32_t MAX_STEER_POINTS = 3;
|
|
float steerPath[MAX_STEER_POINTS * VERTEX_SIZE];
|
|
unsigned char steerPathFlags[MAX_STEER_POINTS];
|
|
dtPolyRef steerPathPolys[MAX_STEER_POINTS];
|
|
uint32 nsteerPath = 0;
|
|
dtStatus dtResult = query->findStraightPath(startPos, endPos, path, static_cast<int32_t>(pathSize),
|
|
steerPath, steerPathFlags, steerPathPolys, (int*)&nsteerPath, MAX_STEER_POINTS);
|
|
if (!nsteerPath || dtStatusFailed(dtResult))
|
|
return false;
|
|
|
|
// Find vertex far enough to steer to.
|
|
uint32 ns = 0;
|
|
while (ns < nsteerPath)
|
|
{
|
|
// Stop at Off-Mesh link or when point is further than slop away.
|
|
if ((steerPathFlags[ns] & DT_STRAIGHTPATH_OFFMESH_CONNECTION) || !inRangeYZX(&steerPath[ns * VERTEX_SIZE], startPos, minTargetDist, 1000.0f))
|
|
{
|
|
break;
|
|
}
|
|
|
|
ns++;
|
|
}
|
|
// Failed to find good point to steer to.
|
|
if (ns >= nsteerPath)
|
|
return false;
|
|
|
|
dtVcopy(steerPos, &steerPath[ns * VERTEX_SIZE]);
|
|
steerPos[1] = startPos[1]; // keep Z value
|
|
steerPosFlag = steerPathFlags[ns];
|
|
steerPosRef = steerPathPolys[ns];
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32 AIInterface::fixupCorridor(dtPolyRef* path, const uint32 npath, const uint32 maxPath, const dtPolyRef* visited, const uint32 nvisited)
|
|
{
|
|
int32 furthestPath = -1;
|
|
int32 furthestVisited = -1;
|
|
|
|
// Find furthest common polygon.
|
|
for (auto i = static_cast<int32_t>(npath) - 1; i >= 0; --i)
|
|
{
|
|
bool found = false;
|
|
for (auto j = static_cast<int32_t>(nvisited) - 1; j >= 0; --j)
|
|
{
|
|
if (path[i] == visited[j])
|
|
{
|
|
furthestPath = i;
|
|
furthestVisited = j;
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (found)
|
|
break;
|
|
}
|
|
|
|
// If no intersection found just return current path.
|
|
if (furthestPath == -1 || furthestVisited == -1)
|
|
return npath;
|
|
|
|
// Concatenate paths.
|
|
|
|
// Adjust beginning of the buffer to include the visited.
|
|
uint32 req = nvisited - furthestVisited;
|
|
uint32 orig = uint32(furthestPath + 1) < npath ? furthestPath + 1 : npath;
|
|
uint32 size = npath - orig > 0 ? npath - orig : 0;
|
|
if (req + size > maxPath)
|
|
size = maxPath - req;
|
|
|
|
if (size)
|
|
memmove(path + req, path + orig, size * sizeof(dtPolyRef));
|
|
|
|
// Store visited
|
|
for (uint32 i = 0; i < req; ++i)
|
|
{
|
|
path[i] = visited[(nvisited - 1) - i];
|
|
}
|
|
|
|
return req + size;
|
|
}
|
|
|
|
void CreatureAISpells::setdurationTimer(uint32_t durationTimer)
|
|
{
|
|
mDurationTimer.resetInterval(durationTimer);
|
|
}
|
|
|
|
void CreatureAISpells::setCooldownTimer(uint32_t cooldownTimer)
|
|
{
|
|
mCooldownTimer.resetInterval(cooldownTimer);
|
|
}
|
|
|
|
void CreatureAISpells::addDBEmote(uint32_t textId)
|
|
{
|
|
MySQLStructure::NpcScriptText const* npcScriptText = sMySQLStore.getNpcScriptText(textId);
|
|
if (npcScriptText != nullptr)
|
|
addEmote(npcScriptText->text, npcScriptText->type, npcScriptText->sound);
|
|
else
|
|
sLogger.debug("A script tried to add a spell emote with %u! Id is not available in table npc_script_text.", textId);
|
|
}
|
|
|
|
void CreatureAISpells::addEmote(std::string pText, uint8_t pType, uint32_t pSoundId)
|
|
{
|
|
if (!pText.empty() || pSoundId)
|
|
mAISpellEmote.emplace_back(AISpellEmotes(pText, pType, pSoundId));
|
|
}
|
|
|
|
void CreatureAISpells::sendRandomEmote(Unit* creatureAI)
|
|
{
|
|
if (!mAISpellEmote.empty() && creatureAI != nullptr)
|
|
{
|
|
sLogger.debug("AISpellEmotes::sendRandomEmote() : called");
|
|
|
|
uint32_t randomUInt = (mAISpellEmote.size() > 1) ? Util::getRandomUInt(static_cast<uint32_t>(mAISpellEmote.size() - 1)) : 0;
|
|
creatureAI->sendChatMessage(mAISpellEmote[randomUInt].mType, LANG_UNIVERSAL, mAISpellEmote[randomUInt].mText);
|
|
|
|
if (mAISpellEmote[randomUInt].mSoundId != 0)
|
|
creatureAI->PlaySoundToSet(mAISpellEmote[randomUInt].mSoundId);
|
|
}
|
|
}
|
|
|
|
void CreatureAISpells::setMaxStackCount(uint32_t stackCount)
|
|
{
|
|
mMaxStackCount = stackCount;
|
|
}
|
|
|
|
const uint32_t CreatureAISpells::getMaxStackCount()
|
|
{
|
|
return mMaxStackCount;
|
|
}
|
|
|
|
void CreatureAISpells::setMaxCastCount(uint32_t castCount)
|
|
{
|
|
mMaxCount = castCount;
|
|
}
|
|
|
|
const uint32_t CreatureAISpells::getMaxCastCount()
|
|
{
|
|
return mMaxCount;
|
|
}
|
|
|
|
const uint32_t CreatureAISpells::getCastCount()
|
|
{
|
|
return mCastCount;
|
|
}
|
|
|
|
const bool CreatureAISpells::isDistanceInRange(float targetDistance)
|
|
{
|
|
if (targetDistance >= mMinPositionRangeToCast && targetDistance <= mMaxPositionRangeToCast)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void CreatureAISpells::setMinMaxDistance(float minDistance, float maxDistance)
|
|
{
|
|
mMinPositionRangeToCast = minDistance;
|
|
mMaxPositionRangeToCast = maxDistance;
|
|
}
|
|
|
|
const bool CreatureAISpells::isHpInPercentRange(float targetHp)
|
|
{
|
|
if (targetHp >= mMinHpRangeToCast && targetHp <= mMaxHpRangeToCast)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void CreatureAISpells::setMinMaxPercentHp(float minHp, float maxHp)
|
|
{
|
|
mMinHpRangeToCast = minHp;
|
|
mMaxHpRangeToCast = maxHp;
|
|
}
|
|
|
|
void CreatureAISpells::setAvailableForScriptPhase(std::vector<uint32_t> phaseVector)
|
|
{
|
|
for (const auto& phase : phaseVector)
|
|
{
|
|
mPhaseList.push_back(phase);
|
|
}
|
|
}
|
|
|
|
bool CreatureAISpells::isAvailableForScriptPhase(uint32_t scriptPhase)
|
|
{
|
|
if (mPhaseList.empty())
|
|
return true;
|
|
|
|
for (const auto& availablePhase : mPhaseList)
|
|
{
|
|
if (availablePhase == scriptPhase)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void CreatureAISpells::setAttackStopTimer(uint32_t attackStopTime)
|
|
{
|
|
mAttackStopTimer = attackStopTime;
|
|
}
|
|
|
|
uint32_t CreatureAISpells::getAttackStopTimer()
|
|
{
|
|
return mAttackStopTimer;
|
|
}
|
|
|
|
void CreatureAISpells::setAnnouncement(std::string announcement)
|
|
{
|
|
mAnnouncement = announcement;
|
|
}
|
|
|
|
void CreatureAISpells::sendAnnouncement(Unit* pUnit)
|
|
{
|
|
if (!mAnnouncement.empty() && pUnit != nullptr)
|
|
{
|
|
sLogger.debug("AISpellEmotes::sendAnnouncement() : called");
|
|
|
|
pUnit->sendChatMessage(CHAT_MSG_RAID_BOSS_EMOTE, LANG_UNIVERSAL, mAnnouncement);
|
|
}
|
|
}
|
|
|
|
void CreatureAISpells::setCustomTarget(Unit* targetCreature)
|
|
{
|
|
mCustomTargetCreature = targetCreature;
|
|
}
|
|
|
|
Unit* CreatureAISpells::getCustomTarget()
|
|
{
|
|
return mCustomTargetCreature;
|
|
}
|
|
|
|
CreatureAISpells* AIInterface::addAISpell(uint32_t spellId, float castChance, uint32_t targetType, uint32_t duration /*= 0*/, uint32_t cooldown /*= 0*/, bool forceRemove /*= false*/, bool isTriggered /*= false*/)
|
|
{
|
|
const auto spellInfo = sSpellMgr.getSpellInfo(spellId);
|
|
if (spellInfo != nullptr)
|
|
{
|
|
uint32_t spellDuration = duration * 1000;
|
|
if (spellDuration == 0)
|
|
spellDuration = spellInfo->getSpellDefaultDuration(nullptr);
|
|
|
|
uint32_t spellCooldown = cooldown * 1000;
|
|
if (spellCooldown == 0)
|
|
spellCooldown = spellInfo->getSpellDefaultDuration(nullptr);
|
|
|
|
CreatureAISpells* newAISpell = new CreatureAISpells(spellInfo, castChance, targetType, spellDuration, spellCooldown, forceRemove, isTriggered);
|
|
|
|
mCreatureAISpells.push_back(newAISpell);
|
|
|
|
newAISpell->setdurationTimer(spellDuration);
|
|
newAISpell->setCooldownTimer(spellCooldown);
|
|
|
|
return newAISpell;
|
|
}
|
|
|
|
sLogger.failure("tried to add invalid spell with id %u", spellId);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void AIInterface::UpdateAISpells()
|
|
{
|
|
if (mLastCastedSpell)
|
|
{
|
|
if (!mSpellWaitTimer.isTimePassed())
|
|
{
|
|
// spell has a min/max range
|
|
if (!getUnit()->isCastingSpell() && (mLastCastedSpell->mMaxPositionRangeToCast > 0.0f || mLastCastedSpell->mMinPositionRangeToCast > 0.0f))
|
|
{
|
|
// if we have a current target and spell is not triggered
|
|
if (mCurrentSpellTarget != nullptr && !mLastCastedSpell->mIsTriggered)
|
|
{
|
|
// interrupt spell if we are not in required range
|
|
const float targetDistance = getUnit()->GetPosition().Distance2DSq({ mCurrentSpellTarget->GetPositionX(), mCurrentSpellTarget->GetPositionY() });
|
|
if (!mLastCastedSpell->isDistanceInRange(targetDistance))
|
|
{
|
|
sLogger.debugFlag(AscEmu::Logging::LF_SCRIPT_MGR, "Target outside of spell range (%u)! Min: %f Max: %f, distance to Target: %f", mLastCastedSpell->mSpellInfo->getId(), mLastCastedSpell->mMinPositionRangeToCast, mLastCastedSpell->mMaxPositionRangeToCast, targetDistance);
|
|
getUnit()->interruptSpell();
|
|
mLastCastedSpell = nullptr;
|
|
mCurrentSpellTarget = nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// spell gets not interupted after casttime(duration) so we can send the emote.
|
|
mLastCastedSpell->sendRandomEmote(getUnit());
|
|
|
|
// spell sucessfully castet increase cast Amount
|
|
mLastCastedSpell->increaseCastCount();
|
|
|
|
// override attack stop timer if needed
|
|
if (mLastCastedSpell->getAttackStopTimer() != 0)
|
|
getUnit()->setAttackTimer(MELEE, mLastCastedSpell->getAttackStopTimer());
|
|
|
|
mLastCastedSpell = nullptr;
|
|
mCurrentSpellTarget = nullptr;
|
|
}
|
|
}
|
|
|
|
// cleanup exeeded spells
|
|
for (const auto& AISpell : mCreatureAISpells)
|
|
{
|
|
if (AISpell != nullptr)
|
|
{
|
|
// stop spells and remove aura in case of duration
|
|
if (AISpell->mDurationTimer.isTimePassed() && AISpell->mForceRemoveAura)
|
|
{
|
|
getUnit()->interruptSpell();
|
|
getUnit()->removeAllAurasById(AISpell->mSpellInfo->getId());
|
|
}
|
|
}
|
|
}
|
|
|
|
// cast one spell and check if spell is done (duration)
|
|
if (mSpellWaitTimer.isTimePassed())
|
|
{
|
|
CreatureAISpells* usedSpell = nullptr;
|
|
|
|
float randomChance = Util::getRandomFloat(100.0f);
|
|
|
|
// Shuffle Around our Spells to randomize the cast
|
|
if (mCreatureAISpells.size())
|
|
{
|
|
for (uint16_t i = 0; i < mCreatureAISpells.size() - 1; ++i)
|
|
{
|
|
const auto j = i + rand() % (mCreatureAISpells.size() - i);
|
|
std::swap(mCreatureAISpells[i], mCreatureAISpells[j]);
|
|
}
|
|
}
|
|
|
|
for (const auto& AISpell : mCreatureAISpells)
|
|
{
|
|
if (AISpell != nullptr)
|
|
{
|
|
// not on AIUpdate skip
|
|
if (AISpell->scriptType != onAIUpdate)
|
|
continue;
|
|
|
|
// spell was casted before, check if the wait time is done
|
|
if (!AISpell->mCooldownTimer.isTimePassed())
|
|
continue;
|
|
|
|
// check if creature has Mana/Power required to cast
|
|
if(AISpell->mSpellInfo->getPowerType() == POWER_TYPE_MANA)
|
|
{
|
|
if (AISpell->mSpellInfo->getManaCost() > m_Unit->getPower(POWER_TYPE_MANA))
|
|
continue;
|
|
}
|
|
|
|
if (AISpell->mSpellInfo->getPowerType() == POWER_TYPE_FOCUS)
|
|
{
|
|
if (AISpell->mSpellInfo->getManaCost() > m_Unit->getPower(POWER_TYPE_FOCUS))
|
|
continue;
|
|
}
|
|
|
|
// is bound to a specific phase (all greater than 0) if creature has a scripted AI then use its phase
|
|
if (getUnit()->ToCreature()->GetScript())
|
|
{
|
|
if (!AISpell->isAvailableForScriptPhase(getUnit()->ToCreature()->GetScript()->getScriptPhase()))
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
if (!AISpell->isAvailableForScriptPhase(internalPhase))
|
|
continue;
|
|
}
|
|
|
|
// max Spell cast amount
|
|
if (AISpell->getMaxCastCount() && AISpell->getMaxCastCount() <= AISpell->getCastCount())
|
|
continue;
|
|
|
|
// aura stacking
|
|
if (getUnit()->getAuraCountForId(AISpell->mSpellInfo->getId()) >= AISpell->getMaxStackCount())
|
|
continue;
|
|
|
|
// hp range
|
|
if (!AISpell->isHpInPercentRange(getUnit()->getHealthPct()))
|
|
continue;
|
|
|
|
// Check if spell requires current target
|
|
if (getCurrentTarget() == nullptr && (AISpell->mTargetType == TARGET_ATTACKING || AISpell->mTargetType == TARGET_DESTINATION))
|
|
continue;
|
|
|
|
// no random chance (cast in script)
|
|
if (AISpell->mCastChance == 0.0f)
|
|
continue;
|
|
|
|
// do not cast any spell while stunned/feared/silenced/charmed/confused
|
|
if (getUnit()->hasUnitStateFlag(UNIT_STATE_STUNNED | UNIT_STATE_FLEEING | UNIT_STATE_IN_FLIGHT | UNIT_STATE_CHARMED | UNIT_STATE_CONFUSED))
|
|
break;
|
|
|
|
// random chance for shuffeld array should do the job
|
|
if (randomChance < AISpell->mCastChance)
|
|
{
|
|
usedSpell = AISpell;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// When we dont had a canche to cast then add the default 1.5sec cooldown for the next cycle
|
|
AISpell->setCooldownTimer(1500);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (usedSpell != nullptr)
|
|
{
|
|
Unit* target = getCurrentTarget();
|
|
switch (usedSpell->mTargetType)
|
|
{
|
|
case TARGET_SELF:
|
|
case TARGET_VARIOUS:
|
|
{
|
|
getUnit()->castSpell(getUnit(), usedSpell->mSpellInfo, usedSpell->mIsTriggered);
|
|
mLastCastedSpell = usedSpell;
|
|
} break;
|
|
case TARGET_ATTACKING:
|
|
{
|
|
getUnit()->castSpell(target, usedSpell->mSpellInfo, usedSpell->mIsTriggered);
|
|
mCurrentSpellTarget = target;
|
|
mLastCastedSpell = usedSpell;
|
|
} break;
|
|
case TARGET_DESTINATION:
|
|
{
|
|
getUnit()->castSpellLoc(target->GetPosition(), usedSpell->mSpellInfo, usedSpell->mIsTriggered);
|
|
mCurrentSpellTarget = target;
|
|
mLastCastedSpell = usedSpell;
|
|
} break;
|
|
case TARGET_SOURCE:
|
|
{
|
|
getUnit()->castSpellLoc(getUnit()->GetPosition(), usedSpell->mSpellInfo, usedSpell->mIsTriggered);
|
|
mLastCastedSpell = usedSpell;
|
|
} break;
|
|
case TARGET_RANDOM_FRIEND:
|
|
case TARGET_RANDOM_SINGLE:
|
|
case TARGET_RANDOM_DESTINATION:
|
|
{
|
|
castSpellOnRandomTarget(usedSpell);
|
|
mLastCastedSpell = usedSpell;
|
|
} break;
|
|
// TODO: missing TARGET_CLOSEST and TARGET_FURTHEST
|
|
case TARGET_CUSTOM:
|
|
{
|
|
// nos custom target set, no spell cast.
|
|
if (usedSpell->getCustomTarget() != nullptr)
|
|
{
|
|
mCurrentSpellTarget = usedSpell->getCustomTarget();
|
|
mLastCastedSpell = usedSpell;
|
|
getUnit()->castSpell(mCurrentSpellTarget, usedSpell->mSpellInfo, usedSpell->mIsTriggered);
|
|
}
|
|
} break;
|
|
case TARGET_FUNCTION:
|
|
{
|
|
if (usedSpell->getTargetFunction() != nullptr)
|
|
{
|
|
mCurrentSpellTarget = usedSpell->getTargetFunction();
|
|
mLastCastedSpell = usedSpell;
|
|
getUnit()->castSpell(mCurrentSpellTarget, usedSpell->mSpellInfo, usedSpell->mIsTriggered);
|
|
}
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// send announcements on casttime beginn
|
|
usedSpell->sendAnnouncement(getUnit());
|
|
|
|
uint32_t casttime = (GetCastTime(sSpellCastTimesStore.LookupEntry(usedSpell->mSpellInfo->getCastingTimeIndex())) ? GetCastTime(sSpellCastTimesStore.LookupEntry(usedSpell->mSpellInfo->getCastingTimeIndex())) : 500);
|
|
|
|
// reset cast wait timer
|
|
mSpellWaitTimer.resetInterval(casttime);
|
|
|
|
// reset spell timers to cleanup exceeded spells
|
|
usedSpell->mDurationTimer.resetInterval(usedSpell->mDuration);
|
|
usedSpell->mCooldownTimer.resetInterval(usedSpell->mCooldown);
|
|
}
|
|
}
|
|
}
|
|
|
|
Unit* AIInterface::getBestPlayerTarget(TargetFilter pTargetFilter, float pMinRange, float pMaxRange)
|
|
{
|
|
//Build potential target list
|
|
UnitArray TargetArray;
|
|
for (const auto& PlayerIter : getUnit()->getInRangePlayersSet())
|
|
{
|
|
if (PlayerIter && isValidUnitTarget(PlayerIter, pTargetFilter, pMinRange, pMaxRange))
|
|
TargetArray.push_back(static_cast<Unit*>(PlayerIter));
|
|
}
|
|
|
|
return getBestTargetInArray(TargetArray, pTargetFilter);
|
|
}
|
|
|
|
Unit* AIInterface::getBestUnitTarget(TargetFilter pTargetFilter, float pMinRange, float pMaxRange)
|
|
{
|
|
//potential target list
|
|
UnitArray TargetArray;
|
|
if (pTargetFilter & TargetFilter_Friendly)
|
|
{
|
|
for (const auto& ObjectIter : getUnit()->getInRangeObjectsSet())
|
|
{
|
|
if (ObjectIter && isValidUnitTarget(ObjectIter, pTargetFilter, pMinRange, pMaxRange))
|
|
TargetArray.push_back(static_cast<Unit*>(ObjectIter));
|
|
}
|
|
|
|
if (isValidUnitTarget(getUnit(), pTargetFilter))
|
|
TargetArray.push_back(getUnit()); //add self as possible friendly target
|
|
}
|
|
else
|
|
{
|
|
for (const auto& ObjectIter : getUnit()->getInRangeOppositeFactionSet())
|
|
{
|
|
if (ObjectIter && isValidUnitTarget(ObjectIter, pTargetFilter, pMinRange, pMaxRange))
|
|
TargetArray.push_back(static_cast<Unit*>(ObjectIter));
|
|
}
|
|
}
|
|
|
|
return getBestTargetInArray(TargetArray, pTargetFilter);
|
|
}
|
|
|
|
Unit* AIInterface::getBestTargetInArray(UnitArray & pTargetArray, TargetFilter pTargetFilter)
|
|
{
|
|
//only one possible target, return it
|
|
if (pTargetArray.size() == 1)
|
|
return pTargetArray[0];
|
|
|
|
//closest unit if requested
|
|
if (pTargetFilter & TargetFilter_Closest)
|
|
return getNearestTargetInArray(pTargetArray);
|
|
|
|
//second most hated if requested
|
|
if (pTargetFilter & TargetFilter_SecondMostHated)
|
|
return getSecondMostHatedTargetInArray(pTargetArray);
|
|
|
|
//random unit in array
|
|
return (pTargetArray.size() > 1) ? pTargetArray[Util::getRandomUInt((uint32_t)pTargetArray.size() - 1)] : nullptr;
|
|
}
|
|
|
|
Unit* AIInterface::getNearestTargetInArray(UnitArray& pTargetArray)
|
|
{
|
|
Unit* NearestUnit = nullptr;
|
|
|
|
float Distance, NearestDistance = 99999;
|
|
for (const auto& UnitIter : pTargetArray)
|
|
{
|
|
if (UnitIter != nullptr)
|
|
{
|
|
Distance = getUnit()->CalcDistance(static_cast<Unit*>(UnitIter));
|
|
if (Distance < NearestDistance)
|
|
{
|
|
NearestDistance = Distance;
|
|
NearestUnit = UnitIter;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NearestUnit;
|
|
}
|
|
|
|
Unit* AIInterface::getSecondMostHatedTargetInArray(UnitArray & pTargetArray)
|
|
{
|
|
Unit* MostHatedUnit = nullptr;
|
|
Unit* TargetUnit = nullptr;
|
|
Unit* CurrentTarget = static_cast<Unit*>(getCurrentTarget());
|
|
uint32_t Threat = 0;
|
|
uint32_t HighestThreat = 0;
|
|
|
|
for (const auto& UnitIter : pTargetArray)
|
|
{
|
|
if (UnitIter != nullptr)
|
|
{
|
|
TargetUnit = static_cast<Unit*>(UnitIter);
|
|
if (TargetUnit != CurrentTarget)
|
|
{
|
|
Threat = static_cast<uint32_t>(getUnit()->getThreatManager().getThreat(TargetUnit));
|
|
if (Threat > HighestThreat)
|
|
{
|
|
MostHatedUnit = TargetUnit;
|
|
HighestThreat = Threat;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return MostHatedUnit;
|
|
}
|
|
|
|
bool AIInterface::isValidUnitTarget(Object* pObject, TargetFilter pFilter, float pMinRange, float pMaxRange)
|
|
{
|
|
if (!pObject->isCreatureOrPlayer())
|
|
return false;
|
|
|
|
if (pObject->GetInstanceID() != getUnit()->GetInstanceID())
|
|
return false;
|
|
|
|
Unit* UnitTarget = static_cast<Unit*>(pObject);
|
|
//Skip dead (if required), feign death or invisible targets
|
|
if (pFilter & TargetFilter_Corpse)
|
|
{
|
|
if (UnitTarget->isAlive() || !UnitTarget->isCreature() || static_cast<Creature*>(UnitTarget)->GetCreatureProperties()->Rank == ELITE_WORLDBOSS)
|
|
return false;
|
|
}
|
|
else if (!UnitTarget->isAlive())
|
|
return false;
|
|
|
|
if (UnitTarget->isPlayer() && static_cast<Player*>(UnitTarget)->m_isGmInvisible)
|
|
return false;
|
|
|
|
if (UnitTarget->hasUnitFlags(UNIT_FLAG_FEIGN_DEATH))
|
|
return false;
|
|
|
|
// if we apply target filtering
|
|
if (pFilter != TargetFilter_None)
|
|
{
|
|
// units not on threat list
|
|
if ((pFilter & TargetFilter_Aggroed) && getUnit()->getThreatManager().getThreat(UnitTarget) == 0)
|
|
return false;
|
|
|
|
// current attacking target if requested
|
|
if ((pFilter & TargetFilter_NotCurrent) && UnitTarget == getCurrentTarget())
|
|
return false;
|
|
|
|
// only wounded targets if requested
|
|
if ((pFilter & TargetFilter_Wounded) && UnitTarget->getHealthPct() >= 99)
|
|
return false;
|
|
|
|
// targets not in melee range if requested
|
|
if ((pFilter & TargetFilter_InMeleeRange) && !getUnit()->isWithinCombatRange(UnitTarget, getUnit()->getMeleeRange(UnitTarget)))
|
|
return false;
|
|
|
|
// targets not in strict range if requested
|
|
if ((pFilter & TargetFilter_InRangeOnly) && (pMinRange > 0 || pMaxRange > 0))
|
|
{
|
|
float Range = getUnit()->CalcDistance(UnitTarget);
|
|
if (pMinRange > 0 && Range < pMinRange)
|
|
return false;
|
|
|
|
if (pMaxRange > 0 && Range > pMaxRange)
|
|
return false;
|
|
}
|
|
|
|
// targets not in Line Of Sight if requested
|
|
if ((~pFilter & TargetFilter_IgnoreLineOfSight) && !getUnit()->IsWithinLOSInMap(UnitTarget))
|
|
return false;
|
|
|
|
// hostile/friendly
|
|
if ((~pFilter & TargetFilter_Corpse) && (pFilter & TargetFilter_Friendly))
|
|
{
|
|
if (!UnitTarget->getCombatHandler().isInCombat())
|
|
return false; // not-in-combat targets if friendly
|
|
|
|
if (isHostile(getUnit(), UnitTarget) || getUnit()->getThreatManager().getThreat(UnitTarget) > 0)
|
|
return false;
|
|
}
|
|
|
|
if ((pFilter & TargetFilter_Current) && UnitTarget != getCurrentTarget())
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void AIInterface::sendStoredText(definedEmoteVector store, Unit* target)
|
|
{
|
|
float randomChance = Util::getRandomFloat(100.0f);
|
|
|
|
// Shuffle Around our textIds to randomize it
|
|
if (!store.empty())
|
|
{
|
|
for (uint16_t i = 0; i < store.size() - 1; ++i)
|
|
{
|
|
const auto j = i + rand() % (store.size() - i);
|
|
std::swap(store[i], store[j]);
|
|
}
|
|
|
|
for (auto mEmotes : store)
|
|
{
|
|
if (mEmotes->phase && mEmotes->phase != internalPhase)
|
|
continue;
|
|
|
|
if (mEmotes->healthPrecent && float(getUnit()->getHealthPct()) < mEmotes->healthPrecent)
|
|
continue;
|
|
|
|
if (mEmotes->maxCount && mEmotes->count == mEmotes->maxCount)
|
|
continue;
|
|
|
|
if (randomChance < mEmotes->canche)
|
|
{
|
|
MySQLStructure::NpcScriptText const* npcScriptText = sMySQLStore.getNpcScriptText(mEmotes->textId);
|
|
if (npcScriptText != nullptr)
|
|
{
|
|
getUnit()->sendChatMessage(npcScriptText->type, LANG_UNIVERSAL, npcScriptText->text, target, 0);
|
|
|
|
if (npcScriptText->sound != 0)
|
|
getUnit()->PlaySoundToSet(npcScriptText->sound);
|
|
}
|
|
|
|
++mEmotes->count;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} |