Skip to content

Commit

Permalink
Core/UnitAI: Rework creature-controlled player behavior.
Browse files Browse the repository at this point in the history
- Removed hacked control mechanism, use proper PlayerAI instead
- Port old hacky code to new SimpleCharmedPlayerAI class
- Make adjustments to aforementioned code to fix bugs:
    - Properly clean up movement after charm ends
    - Only try to attack a target if charmer is engaged in combat
  • Loading branch information
Treeston committed Feb 23, 2016
1 parent e4dfbb6 commit 2f14664
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 102 deletions.
30 changes: 0 additions & 30 deletions src/server/game/AI/CoreAI/UnitAI.cpp
Expand Up @@ -259,36 +259,6 @@ void UnitAI::FillAISpellInfo()
}
}

//Enable PlayerAI when charmed
void PlayerAI::OnCharmed(bool apply)
{
me->IsAIEnabled = apply;
}

void SimpleCharmedAI::UpdateAI(const uint32 /*diff*/)
{
Creature* charmer = me->GetCharmer()->ToCreature();

//kill self if charm aura has infinite duration
if (charmer->IsInEvadeMode())
{
Unit::AuraEffectList const& auras = me->GetAuraEffectsByType(SPELL_AURA_MOD_CHARM);
for (Unit::AuraEffectList::const_iterator iter = auras.begin(); iter != auras.end(); ++iter)
if ((*iter)->GetCasterGUID() == charmer->GetGUID() && (*iter)->GetBase()->IsPermanent())
{
charmer->Kill(me);
return;
}
}

if (!charmer->IsInCombat())
me->GetMotionMaster()->MoveFollow(charmer, PET_FOLLOW_DIST, me->GetFollowAngle());

Unit* target = me->GetVictim();
if (!target || !charmer->IsValidAttackTarget(target))
AttackStart(charmer->SelectNearestTargetInAttackDistance());
}

SpellTargetSelector::SpellTargetSelector(Unit* caster, uint32 spellId) :
_caster(caster), _spellInfo(sSpellMgr->GetSpellForDifficultyFromSpell(sSpellMgr->GetSpellInfo(spellId), caster))
{
Expand Down
17 changes: 0 additions & 17 deletions src/server/game/AI/CoreAI/UnitAI.h
Expand Up @@ -268,21 +268,4 @@ class UnitAI
UnitAI& operator=(UnitAI const& right) = delete;
};

class PlayerAI : public UnitAI
{
protected:
Player* const me;
public:
explicit PlayerAI(Player* player) : UnitAI((Unit*)player), me(player) { }

void OnCharmed(bool apply) override;
};

class SimpleCharmedAI : public PlayerAI
{
public:
void UpdateAI(uint32 diff) override;
SimpleCharmedAI(Player* player): PlayerAI(player) { }
};

#endif
6 changes: 6 additions & 0 deletions src/server/game/AI/CreatureAI.h
Expand Up @@ -28,6 +28,7 @@ class WorldObject;
class Unit;
class Creature;
class Player;
class PlayerAI;
class SpellInfo;

#define TIME_INTERVAL_LOOK 5000
Expand Down Expand Up @@ -186,6 +187,11 @@ class CreatureAI : public UnitAI

virtual bool CanSeeAlways(WorldObject const* /*obj*/) { return false; }

// Called when a player is charmed by the creature
// If a PlayerAI* is returned, that AI is placed on the player instead of the default charm AI
// Object destruction is handled by Unit::RemoveCharmedBy
virtual PlayerAI* GetAIForCharmedPlayer(Player* /*who*/) { return nullptr; }

// intended for encounter design/debugging. do not use for other purposes. expensive.
int32 VisualizeBoundary(uint32 duration, Unit* owner=nullptr, bool fill=false) const;
virtual bool CheckInRoom();
Expand Down
195 changes: 195 additions & 0 deletions src/server/game/AI/PlayerAI/PlayerAI.cpp
@@ -0,0 +1,195 @@
/*
* Copyright (C) 2016-2016 TrinityCore <http://www.trinitycore.org/>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "PlayerAI.h"
#include "SpellAuras.h"
#include "SpellAuraEffects.h"

enum Spells
{
/* Generic */
SPELL_AUTO_SHOT = 75,
SPELL_SHOOT = 3018,
SPELL_THROW = 2764,
SPELL_SHOOT_WAND = 5019,

/* Priest */
SPELL_SHADOWFORM = 15473,

/* Shaman */
SPELL_STORMSTRIKE = 17364,

/* Druid */
SPELL_MOONKIN_FORM = 24858
};
PlayerAI::PlayerAI(Player* player) : UnitAI(static_cast<Unit*>(player)), me(player), _isRangedAttacker(false)
{
switch (me->getClass())
{
case CLASS_WARRIOR:
_isRangedAttacker = false;
break;
case CLASS_PALADIN:
_isRangedAttacker = false;
break;
case CLASS_HUNTER:
{
// check if we have a ranged weapon equipped
Item const* rangedSlot = me->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED);
if (ItemTemplate const* rangedTemplate = rangedSlot ? rangedSlot->GetTemplate() : nullptr)
if ((1 << rangedTemplate->SubClass) & ITEM_SUBCLASS_MASK_WEAPON_RANGED)
{
_isRangedAttacker = true;
break;
}
_isRangedAttacker = false;
break;
}
case CLASS_ROGUE:
_isRangedAttacker = false;
break;
case CLASS_PRIEST:
_isRangedAttacker = me->HasSpell(SPELL_SHADOWFORM);
break;
case CLASS_DEATH_KNIGHT:
_isRangedAttacker = false;
break;
case CLASS_SHAMAN:
_isRangedAttacker = !me->HasSpell(SPELL_STORMSTRIKE);
break;
case CLASS_MAGE:
_isRangedAttacker = true;
break;
case CLASS_WARLOCK:
_isRangedAttacker = true;
break;
case CLASS_DRUID:
_isRangedAttacker = me->HasAura(SPELL_MOONKIN_FORM);
break;
default:
TC_LOG_WARN("entities.unit", "Possessed player %s (possessed by %s) does not have any recognized class (class = %u).", me->GetGUID().ToString().c_str(), me->GetCharmerGUID().ToString().c_str(), me->getClass());
break;
}
}

void PlayerAI::DoRangedAttackIfReady()
{
if (me->HasUnitState(UNIT_STATE_CASTING))
return;

if (!me->isAttackReady(RANGED_ATTACK))
return;

Unit* victim = me->GetVictim();
if (!victim)
return;

uint32 rangedAttackSpell = 0;

Item const* rangedItem = me->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED);
if (ItemTemplate const* rangedTemplate = rangedItem ? rangedItem->GetTemplate() : nullptr)
{
switch (rangedTemplate->SubClass)
{
case ITEM_SUBCLASS_WEAPON_BOW:
case ITEM_SUBCLASS_WEAPON_GUN:
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
rangedAttackSpell = SPELL_SHOOT;
break;
case ITEM_SUBCLASS_WEAPON_THROWN:
rangedAttackSpell = SPELL_THROW;
break;
case ITEM_SUBCLASS_WEAPON_WAND:
rangedAttackSpell = SPELL_SHOOT_WAND;
break;
}
}

std::cout << "Selected " << rangedAttackSpell << std::endl;

if (!rangedAttackSpell)
return;

me->CastSpell(victim, rangedAttackSpell, TRIGGERED_CAST_DIRECTLY);
me->resetAttackTimer(RANGED_ATTACK);
}

void PlayerAI::DoAutoAttackIfReady()
{
if (IsRangedAttacker())
DoRangedAttackIfReady();
else
DoMeleeAttackIfReady();
}

void SimpleCharmedPlayerAI::UpdateAI(const uint32 /*diff*/)
{
Creature* charmer = me->GetCharmer()->ToCreature();

//kill self if charm aura has infinite duration
if (charmer->IsInEvadeMode())
{
Player::AuraEffectList const& auras = me->GetAuraEffectsByType(SPELL_AURA_MOD_CHARM);
for (Player::AuraEffectList::const_iterator iter = auras.begin(); iter != auras.end(); ++iter)
if ((*iter)->GetCasterGUID() == charmer->GetGUID() && (*iter)->GetBase()->IsPermanent())
{
me->Kill(me);
return;
}
}

if (charmer->IsInCombat())
{
Unit* target = me->GetVictim();
if (!target || !charmer->IsValidAttackTarget(target))
{
target = charmer->SelectNearestTarget();
if (!target)
return;

if (IsRangedAttacker())
AttackStartCaster(target, 28.0f);
else
AttackStart(target);
}
}
else
{
me->AttackStop();
me->CastStop();
me->GetMotionMaster()->MoveFollow(charmer, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);
}

DoAutoAttackIfReady();
}

void SimpleCharmedPlayerAI::OnCharmed(bool apply)
{
if (apply)
{
me->CastStop();
me->AttackStop();
}
else
{
me->CastStop();
me->AttackStop();
// @todo only voluntary movement (don't cancel stuff like death grip or charge mid-animation)
me->GetMotionMaster()->Clear();
me->StopMoving();
}
}
52 changes: 52 additions & 0 deletions src/server/game/AI/PlayerAI/PlayerAI.h
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2016-2016 TrinityCore <http://www.trinitycore.org/>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef TRINITY_PLAYERAI_H
#define TRINITY_PLAYERAI_H

#include "UnitAI.h"
#include "Player.h"
#include "Creature.h"

class PlayerAI : public UnitAI
{
public:
explicit PlayerAI(Player* player);

void OnCharmed(bool /*apply*/) override { } // charm AI application for players is handled by Unit::SetCharmedBy / Unit::RemoveCharmedBy

protected:
Player* const me;
void SetIsRangedAttacker(bool state) { _isRangedAttacker = state; }
bool IsRangedAttacker() const { return _isRangedAttacker; }

void DoRangedAttackIfReady();
void DoAutoAttackIfReady();

private:
bool _isRangedAttacker;
};

class SimpleCharmedPlayerAI : public PlayerAI
{
public:
SimpleCharmedPlayerAI(Player* player) : PlayerAI(player) { }
void UpdateAI(uint32 diff) override;
void OnCharmed(bool apply) override;
};

#endif
1 change: 1 addition & 0 deletions src/server/game/CMakeLists.txt
Expand Up @@ -108,6 +108,7 @@ include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/Addons
${CMAKE_CURRENT_SOURCE_DIR}/AI
${CMAKE_CURRENT_SOURCE_DIR}/AI/CoreAI
${CMAKE_CURRENT_SOURCE_DIR}/AI/PlayerAI
${CMAKE_CURRENT_SOURCE_DIR}/AI/ScriptedAI
${CMAKE_CURRENT_SOURCE_DIR}/AI/SmartScripts
${CMAKE_CURRENT_SOURCE_DIR}/AuctionHouse
Expand Down
2 changes: 1 addition & 1 deletion src/server/game/Entities/Creature/Creature.h
Expand Up @@ -479,7 +479,7 @@ class Creature : public Unit, public GridObject<Creature>, public MapObject
bool AIM_Initialize(CreatureAI* ai = NULL);
void Motion_Initialize();

CreatureAI* AI() const { return (CreatureAI*)i_AI; }
CreatureAI* AI() const { return reinterpret_cast<CreatureAI*>(i_AI); }

bool SetWalk(bool enable) override;
bool SetDisableGravity(bool disable, bool packetOnly = false) override;
Expand Down
39 changes: 1 addition & 38 deletions src/server/game/Entities/Player/Player.cpp
Expand Up @@ -1237,12 +1237,7 @@ void Player::Update(uint32 p_time)

UpdateAfkReport(now);

if (IsCharmed())
if (Unit* charmer = GetCharmer())
if (charmer->GetTypeId() == TYPEID_UNIT && charmer->IsAlive())
UpdateCharmedAI();

if (GetAI() && IsAIEnabled)
if (IsAIEnabled && GetAI())
GetAI()->UpdateAI(p_time);

// Update items that have just a limited lifetime
Expand Down Expand Up @@ -24129,38 +24124,6 @@ bool Player::isTotalImmunity()
return false;
}

void Player::UpdateCharmedAI()
{
//This should only called in Player::Update
Creature* charmer = GetCharmer()->ToCreature();

//kill self if charm aura has infinite duration
if (charmer->IsInEvadeMode())
{
AuraEffectList const& auras = GetAuraEffectsByType(SPELL_AURA_MOD_CHARM);
for (AuraEffectList::const_iterator iter = auras.begin(); iter != auras.end(); ++iter)
if ((*iter)->GetCasterGUID() == charmer->GetGUID() && (*iter)->GetBase()->IsPermanent())
{
charmer->DealDamage(this, GetHealth(), NULL, DIRECT_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, NULL, false);
return;
}
}

if (!charmer->IsInCombat())
GetMotionMaster()->MoveFollow(charmer, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);

Unit* target = GetVictim();
if (!target || !charmer->IsValidAttackTarget(target))
{
target = charmer->SelectNearestTarget();
if (!target)
return;

GetMotionMaster()->MoveChase(target);
Attack(target, true);
}
}

uint32 Player::GetRuneBaseCooldown(uint8 index)
{
uint8 rune = GetBaseRune(index);
Expand Down

0 comments on commit 2f14664

Please sign in to comment.