few fixes + added Tankbot
This commit is contained in:
1 parent
3cf7e70283
commit
1785350447
16 files changed
+49600
-3
No files matched your search
@@ -11522,6 +11522,100 @@ TransportBase* Unit::GetDirectTransport() const
|
||||
return GetTransport();
|
||||
}
|
||||
|
||||
|
||||
// VIANO_GROUP_CONTEXT_V1
|
||||
namespace
|
||||
{
|
||||
class VianoGroupBotInRangeCheck
|
||||
{
|
||||
public:
|
||||
VianoGroupBotInRangeCheck(Unit const* source, Player const* groupOwner, float range)
|
||||
: _source(source), _groupOwner(groupOwner), _range(range) { }
|
||||
|
||||
bool operator()(Unit* unit) const
|
||||
{
|
||||
if (!unit || !unit->IsAlive() || !_source || !_groupOwner)
|
||||
return false;
|
||||
|
||||
if (!_source->IsWithinDistInMap(unit, _range))
|
||||
return false;
|
||||
|
||||
Creature* creature = unit->ToCreature();
|
||||
if (!creature || !creature->IsVianoAdventurer())
|
||||
return false;
|
||||
|
||||
Player* botOwner = creature->GetOwner() ? creature->GetOwner()->ToPlayer() : nullptr;
|
||||
if (!botOwner || !botOwner->IsInMap(_groupOwner))
|
||||
return false;
|
||||
|
||||
Group const* group = _groupOwner->GetGroup();
|
||||
if (!group)
|
||||
return botOwner->GetGUID() == _groupOwner->GetGUID();
|
||||
|
||||
return botOwner->GetGroup() == group
|
||||
&& botOwner->GetSubGroup() == _groupOwner->GetSubGroup();
|
||||
}
|
||||
|
||||
private:
|
||||
Unit const* _source;
|
||||
Player const* _groupOwner;
|
||||
float _range;
|
||||
};
|
||||
|
||||
void AppendNearbyVianoGroupBots(Unit* source, Player* groupOwner, std::list<Unit*>& units)
|
||||
{
|
||||
if (!source || !groupOwner || !source->IsInWorld())
|
||||
return;
|
||||
|
||||
float const range = source->GetVisibilityRange();
|
||||
std::list<Unit*> nearby;
|
||||
VianoGroupBotInRangeCheck check(source, groupOwner, range);
|
||||
Trinity::UnitListSearcher<VianoGroupBotInRangeCheck> searcher(source, nearby, check);
|
||||
Cell::VisitAllObjects(source, searcher, range);
|
||||
|
||||
for (Unit* unit : nearby)
|
||||
if (unit && std::find(units.begin(), units.end(), unit) == units.end())
|
||||
units.push_back(unit);
|
||||
}
|
||||
}
|
||||
|
||||
bool Unit::IsVianoAdventurer() const
|
||||
{
|
||||
Creature const* creature = ToCreature();
|
||||
if (!creature)
|
||||
return false;
|
||||
|
||||
CreatureTemplate const* cinfo = creature->GetCreatureTemplate();
|
||||
if (!cinfo)
|
||||
return false;
|
||||
|
||||
std::string const scriptName = sObjectMgr->GetScriptName(cinfo->ScriptID);
|
||||
return scriptName.rfind("npc_horde_adventurer_", 0) == 0
|
||||
|| scriptName.rfind("npc_alliance_adventurer_", 0) == 0;
|
||||
}
|
||||
|
||||
uint32 Unit::GetVianoEffectiveGroupSize() const
|
||||
{
|
||||
std::list<Unit*> units;
|
||||
const_cast<Unit*>(this)->GetPartyMembers(units);
|
||||
|
||||
uint32 count = 0;
|
||||
std::vector<ObjectGuid> seen;
|
||||
for (Unit* unit : units)
|
||||
{
|
||||
if (!unit || (!unit->IsPlayer() && !unit->IsVianoAdventurer()))
|
||||
continue;
|
||||
|
||||
ObjectGuid guid = unit->GetGUID();
|
||||
if (std::find(seen.begin(), seen.end(), guid) != seen.end())
|
||||
continue;
|
||||
|
||||
seen.push_back(guid);
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool Unit::IsInPartyWith(Unit const* unit) const
|
||||
{
|
||||
if (this == unit)
|
||||
@@ -11564,18 +11658,22 @@ void Unit::GetPartyMembers(std::list<Unit*> &TagUnitMap)
|
||||
{
|
||||
Unit* owner = GetCharmerOrOwnerOrSelf();
|
||||
Group* group = nullptr;
|
||||
Player* groupOwner = nullptr;
|
||||
|
||||
if (owner->GetTypeId() == TYPEID_PLAYER)
|
||||
group = owner->ToPlayer()->GetGroup();
|
||||
{
|
||||
groupOwner = owner->ToPlayer();
|
||||
group = groupOwner->GetGroup();
|
||||
}
|
||||
|
||||
if (group)
|
||||
{
|
||||
uint8 subgroup = owner->ToPlayer()->GetSubGroup();
|
||||
uint8 subgroup = groupOwner->GetSubGroup();
|
||||
|
||||
for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next())
|
||||
{
|
||||
Player* Target = itr->GetSource();
|
||||
|
||||
// IsHostileTo check duel and controlled by enemy
|
||||
if (Target && Target->IsInMap(owner) && Target->GetSubGroup() == subgroup && !IsHostileTo(Target))
|
||||
{
|
||||
if (Target->IsAlive())
|
||||
@@ -11595,6 +11693,9 @@ void Unit::GetPartyMembers(std::list<Unit*> &TagUnitMap)
|
||||
if ((pet == this || IsInMap(pet)) && pet->IsAlive())
|
||||
TagUnitMap.push_back(pet);
|
||||
}
|
||||
|
||||
if (groupOwner)
|
||||
AppendNearbyVianoGroupBots(this, groupOwner, TagUnitMap);
|
||||
}
|
||||
|
||||
bool Unit::IsContestedGuard() const
|
||||
@@ -14047,6 +14148,7 @@ void Unit::ProcessPendingSpellCastRequest()
|
||||
// Process packet provided targets
|
||||
SpellCastTargets targets(caster, _pendingSpellCastRequest->CastRequest);
|
||||
|
||||
|
||||
// The spell cast has been requested by using an item. Handle the cast accordingly.
|
||||
if (_pendingSpellCastRequest->CastItemData.has_value())
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -837,6 +837,9 @@ class TC_GAME_API Unit : public WorldObject
|
||||
bool IsInPartyWith(Unit const* unit) const;
|
||||
bool IsInRaidWith(Unit const* unit) const;
|
||||
void GetPartyMembers(std::list<Unit*> &units);
|
||||
// VIANO_GROUP_CONTEXT_V1
|
||||
bool IsVianoAdventurer() const;
|
||||
uint32 GetVianoEffectiveGroupSize() const;
|
||||
bool IsContestedGuard() const;
|
||||
bool IsInSanctuary() const { return HasByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_SANCTUARY); }
|
||||
bool IsPvP() const { return HasByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_PVP); }
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -315,6 +315,7 @@ class TC_GAME_API Group
|
||||
|
||||
void ChangeMembersGroup(ObjectGuid guid, uint8 group);
|
||||
void SetTargetIcon(uint8 id, ObjectGuid whoGuid, ObjectGuid targetGuid);
|
||||
ObjectGuid GetTargetIcon(uint8 id) const { return id < TARGETICONCOUNT ? m_targetIcons[id] : ObjectGuid::Empty; }
|
||||
void SetGroupMemberFlag(ObjectGuid guid, bool apply, GroupMemberFlags flag);
|
||||
void RemoveUniqueGroupMemberFlag(GroupMemberFlags flag);
|
||||
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* 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 TRINITYCORE_GROUP_H
|
||||
#define TRINITYCORE_GROUP_H
|
||||
|
||||
#include "DBCEnums.h"
|
||||
#include "DatabaseEnvFwd.h"
|
||||
#include "GroupRefManager.h"
|
||||
#include "Loot.h"
|
||||
#include "SharedDefines.h"
|
||||
#include <map>
|
||||
|
||||
class Battlefield;
|
||||
class Battleground;
|
||||
class Creature;
|
||||
class DynamicObject;
|
||||
class InstanceSave;
|
||||
class Map;
|
||||
class Player;
|
||||
class Unit;
|
||||
class WorldObject;
|
||||
class WorldPacket;
|
||||
class WorldSession;
|
||||
|
||||
struct MapEntry;
|
||||
|
||||
#define MAXGROUPSIZE 5
|
||||
#define MAXRAIDSIZE 40
|
||||
#define MAX_RAID_SUBGROUPS MAXRAIDSIZE/MAXGROUPSIZE
|
||||
#define TARGETICONCOUNT 8
|
||||
#define SPELL_RAID_MARKER 84996
|
||||
|
||||
enum RollVote
|
||||
{
|
||||
PASS = 0,
|
||||
NEED = 1,
|
||||
GREED = 2,
|
||||
DISENCHANT = 3,
|
||||
NOT_EMITED_YET = 4,
|
||||
NOT_VALID = 5
|
||||
};
|
||||
|
||||
enum GroupMemberOnlineStatus
|
||||
{
|
||||
MEMBER_STATUS_OFFLINE = 0x0000,
|
||||
MEMBER_STATUS_ONLINE = 0x0001, // Lua_UnitIsConnected
|
||||
MEMBER_STATUS_PVP = 0x0002, // Lua_UnitIsPVP
|
||||
MEMBER_STATUS_DEAD = 0x0004, // Lua_UnitIsDead
|
||||
MEMBER_STATUS_GHOST = 0x0008, // Lua_UnitIsGhost
|
||||
MEMBER_STATUS_PVP_FFA = 0x0010, // Lua_UnitIsPVPFreeForAll
|
||||
MEMBER_STATUS_UNK3 = 0x0020, // used in calls from Lua_GetPlayerMapPosition/Lua_GetBattlefieldFlagPosition
|
||||
MEMBER_STATUS_AFK = 0x0040, // Lua_UnitIsAFK
|
||||
MEMBER_STATUS_DND = 0x0080 // Lua_UnitIsDND
|
||||
};
|
||||
|
||||
enum GroupMemberFlags
|
||||
{
|
||||
MEMBER_FLAG_ASSISTANT = 0x01,
|
||||
MEMBER_FLAG_MAINTANK = 0x02,
|
||||
MEMBER_FLAG_MAINASSIST = 0x04
|
||||
};
|
||||
|
||||
enum GroupMemberAssignment
|
||||
{
|
||||
GROUP_ASSIGN_MAINTANK = 0,
|
||||
GROUP_ASSIGN_MAINASSIST = 1
|
||||
};
|
||||
|
||||
enum GroupFlags
|
||||
{
|
||||
GROUP_FLAG_NONE = 0x000,
|
||||
GROUP_FLAG_FAKE_RAID = 0x001,
|
||||
GROUP_FLAG_RAID = 0x002,
|
||||
GROUP_FLAG_LFG_RESTRICTED = 0x004, // Script_HasLFGRestrictions()
|
||||
GROUP_FLAG_LFG = 0x008,
|
||||
GROUP_FLAG_DESTROYED = 0x010,
|
||||
GROUP_FLAG_ONE_PERSON_PARTY = 0x020, // Script_IsOnePersonParty()
|
||||
GROUP_FLAG_EVERYONE_ASSISTANT = 0x040, // Script_IsEveryoneAssistant()
|
||||
|
||||
GROUP_MASK_BGRAID = GROUP_FLAG_FAKE_RAID | GROUP_FLAG_RAID,
|
||||
};
|
||||
|
||||
enum GroupUpdateFlags
|
||||
{
|
||||
GROUP_UPDATE_FLAG_NONE = 0x00000000, // nothing
|
||||
GROUP_UPDATE_FLAG_STATUS = 0x00000001, // uint16 (GroupMemberStatusFlag)
|
||||
GROUP_UPDATE_FLAG_CUR_HP = 0x00000002, // uint32 (HP)
|
||||
GROUP_UPDATE_FLAG_MAX_HP = 0x00000004, // uint32 (HP)
|
||||
GROUP_UPDATE_FLAG_POWER_TYPE = 0x00000008, // uint8 (PowerType)
|
||||
GROUP_UPDATE_FLAG_CUR_POWER = 0x00000010, // int16 (power value)
|
||||
GROUP_UPDATE_FLAG_MAX_POWER = 0x00000020, // int16 (power value)
|
||||
GROUP_UPDATE_FLAG_LEVEL = 0x00000040, // uint16 (level value)
|
||||
GROUP_UPDATE_FLAG_ZONE = 0x00000080, // uint16 (zone id)
|
||||
GROUP_UPDATE_FLAG_WMO_GROUP_ID = 0x00000100, // int16 (WMOGroupID)
|
||||
GROUP_UPDATE_FLAG_POSITION = 0x00000200, // uint16 (x), uint16 (y), uint16 (z)
|
||||
GROUP_UPDATE_FLAG_AURAS = 0x00000400, // uint8 (unk), uint64 (mask), uint32 (count), for each bit set: uint32 (spell id) + uint16 (AuraFlags) (if has flags Scalable -> 3x int32 (bps))
|
||||
GROUP_UPDATE_FLAG_PET_GUID = 0x00000800, // uint64 (pet guid)
|
||||
GROUP_UPDATE_FLAG_PET_NAME = 0x00001000, // cstring (name, nullptr terminated string)
|
||||
GROUP_UPDATE_FLAG_PET_MODEL_ID = 0x00002000, // uint16 (model id)
|
||||
GROUP_UPDATE_FLAG_PET_CUR_HP = 0x00004000, // uint32 (HP)
|
||||
GROUP_UPDATE_FLAG_PET_MAX_HP = 0x00008000, // uint32 (HP)
|
||||
GROUP_UPDATE_FLAG_PET_POWER_TYPE = 0x00010000, // uint8 (PowerType)
|
||||
GROUP_UPDATE_FLAG_PET_CUR_POWER = 0x00020000, // uint16 (power value)
|
||||
GROUP_UPDATE_FLAG_PET_MAX_POWER = 0x00040000, // uint16 (power value)
|
||||
GROUP_UPDATE_FLAG_PET_AURAS = 0x00080000, // [see GROUP_UPDATE_FLAG_AURAS]
|
||||
GROUP_UPDATE_FLAG_VEHICLE_SEAT = 0x00100000, // int32 (vehicle seat id)
|
||||
GROUP_UPDATE_FLAG_PHASE = 0x00200000, // int32 (unk), uint32 (phase count), for (count) uint16(phaseId)
|
||||
GROUP_UPDATE_FLAG_UNK400000 = 0x00400000,
|
||||
GROUP_UPDATE_FLAG_UNK800000 = 0x00800000,
|
||||
GROUP_UPDATE_FLAG_UNK1000000 = 0x01000000,
|
||||
GROUP_UPDATE_FLAG_UNK2000000 = 0x02000000,
|
||||
GROUP_UPDATE_FLAG_UNK4000000 = 0x04000000,
|
||||
GROUP_UPDATE_FLAG_UNK8000000 = 0x08000000,
|
||||
GROUP_UPDATE_FLAG_UNK10000000 = 0x10000000,
|
||||
GROUP_UPDATE_FLAG_UNK20000000 = 0x20000000,
|
||||
GROUP_UPDATE_FLAG_UNK40000000 = 0x40000000,
|
||||
GROUP_UPDATE_FLAG_UNK80000000 = 0x80000000,
|
||||
|
||||
GROUP_UPDATE_PET = GROUP_UPDATE_FLAG_PET_GUID | GROUP_UPDATE_FLAG_PET_NAME | GROUP_UPDATE_FLAG_PET_MODEL_ID |
|
||||
GROUP_UPDATE_FLAG_PET_CUR_HP | GROUP_UPDATE_FLAG_PET_MAX_HP | GROUP_UPDATE_FLAG_PET_POWER_TYPE |
|
||||
GROUP_UPDATE_FLAG_PET_CUR_POWER | GROUP_UPDATE_FLAG_PET_MAX_POWER | GROUP_UPDATE_FLAG_PET_AURAS, // all pet flags
|
||||
GROUP_UPDATE_FULL = GROUP_UPDATE_FLAG_STATUS | GROUP_UPDATE_FLAG_CUR_HP | GROUP_UPDATE_FLAG_MAX_HP |
|
||||
GROUP_UPDATE_FLAG_POWER_TYPE | GROUP_UPDATE_FLAG_CUR_POWER | GROUP_UPDATE_FLAG_MAX_POWER |
|
||||
GROUP_UPDATE_FLAG_LEVEL | GROUP_UPDATE_FLAG_ZONE | GROUP_UPDATE_FLAG_WMO_GROUP_ID |GROUP_UPDATE_FLAG_POSITION |
|
||||
GROUP_UPDATE_FLAG_AURAS | GROUP_UPDATE_FLAG_PHASE | GROUP_UPDATE_FLAG_UNK400000 | GROUP_UPDATE_FLAG_UNK800000 |
|
||||
GROUP_UPDATE_FLAG_UNK1000000 | GROUP_UPDATE_FLAG_UNK2000000 | GROUP_UPDATE_FLAG_UNK4000000 |
|
||||
GROUP_UPDATE_FLAG_UNK8000000 | GROUP_UPDATE_FLAG_UNK10000000 | GROUP_UPDATE_FLAG_UNK20000000 | GROUP_UPDATE_FLAG_UNK40000000
|
||||
};
|
||||
|
||||
class Roll : public LootValidatorRef
|
||||
{
|
||||
public:
|
||||
Roll(ObjectGuid _guid, LootItem const& li);
|
||||
~Roll();
|
||||
void setLoot(Loot* pLoot);
|
||||
Loot* getLoot();
|
||||
void targetObjectBuildLink() override;
|
||||
|
||||
ObjectGuid itemGUID;
|
||||
uint32 itemid;
|
||||
ItemRandomEnchantmentId itemRandomPropId;
|
||||
uint32 itemRandomSuffix;
|
||||
uint8 itemCount;
|
||||
typedef std::map<ObjectGuid, RollVote> PlayerVote;
|
||||
PlayerVote playerVote; //vote position correspond with player position (in group)
|
||||
uint8 totalPlayersRolling;
|
||||
uint8 totalNeed;
|
||||
uint8 totalGreed;
|
||||
uint8 totalPass;
|
||||
uint8 itemSlot;
|
||||
uint8 rollVoteMask;
|
||||
};
|
||||
|
||||
struct InstanceGroupBind
|
||||
{
|
||||
InstanceSave* save;
|
||||
bool perm;
|
||||
/* permanent InstanceGroupBinds exist if the leader has a permanent
|
||||
PlayerInstanceBind for the same instance. */
|
||||
InstanceGroupBind() : save(nullptr), perm(false) { }
|
||||
};
|
||||
|
||||
struct RaidMarkerInfo
|
||||
{
|
||||
ObjectGuid summonerGuid;
|
||||
ObjectGuid markerGuid;
|
||||
};
|
||||
|
||||
typedef std::list<RaidMarkerInfo> RaidMarkerList;
|
||||
|
||||
/** request member stats checken **/
|
||||
/// @todo uninvite people that not accepted invite
|
||||
class TC_GAME_API Group
|
||||
{
|
||||
public:
|
||||
struct MemberSlot
|
||||
{
|
||||
ObjectGuid guid;
|
||||
std::string name;
|
||||
uint8 group;
|
||||
uint8 flags;
|
||||
uint8 roles;
|
||||
uint32 guildId;
|
||||
};
|
||||
typedef std::list<MemberSlot> MemberSlotList;
|
||||
typedef MemberSlotList::const_iterator member_citerator;
|
||||
|
||||
typedef std::unordered_map< uint32 /*mapId*/, InstanceGroupBind> BoundInstancesMap;
|
||||
|
||||
struct GroupDisenchantInfo
|
||||
{
|
||||
ObjectGuid DisenchanterGUID;
|
||||
uint32 MaxDisenchantSkillLevel;
|
||||
|
||||
void Initialize()
|
||||
{
|
||||
DisenchanterGUID = ObjectGuid::Empty;
|
||||
MaxDisenchantSkillLevel = 0;
|
||||
}
|
||||
};
|
||||
|
||||
protected:
|
||||
typedef MemberSlotList::iterator member_witerator;
|
||||
typedef std::set<Player*> InvitesList;
|
||||
|
||||
typedef std::vector<std::unique_ptr<Roll>> Rolls;
|
||||
|
||||
public:
|
||||
Group();
|
||||
~Group();
|
||||
|
||||
// group manipulation methods
|
||||
bool Create(Player* leader);
|
||||
void LoadGroupFromDB(Field* field);
|
||||
void LoadMemberFromDB(ObjectGuid::LowType guidLow, uint8 memberFlags, uint8 subgroup, uint8 roles);
|
||||
bool AddInvite(Player* player);
|
||||
void RemoveInvite(Player* player);
|
||||
void RemoveAllInvites();
|
||||
bool AddLeaderInvite(Player* player);
|
||||
bool AddMember(Player* player);
|
||||
bool RemoveMember(ObjectGuid guid, const RemoveMethod &method = GROUP_REMOVEMETHOD_DEFAULT, ObjectGuid kicker = ObjectGuid::Empty, char const* reason = nullptr);
|
||||
void ChangeLeader(ObjectGuid guid);
|
||||
static void ConvertLeaderInstancesToGroup(Player* player, Group* group, bool switchLeader);
|
||||
void SetLootMethod(LootMethod method);
|
||||
void SetLooterGuid(ObjectGuid guid);
|
||||
void SetMasterLooterGuid(ObjectGuid guid);
|
||||
void UpdateLooterGuid(WorldObject* pLootedObject, bool ifneed = false);
|
||||
void SetLootThreshold(ItemQualities threshold);
|
||||
void Disband(bool hideDestroy = false);
|
||||
void SetLfgRoles(ObjectGuid guid, uint8 roles);
|
||||
uint8 GetLfgRoles(ObjectGuid guid);
|
||||
void SetEveryoneIsAssistant(bool apply);
|
||||
|
||||
void SetGroupMarkerMask(uint32 mask) { m_markerMask = mask; }
|
||||
void AddGroupMarkerMask(uint32 mask) { m_markerMask |= mask; }
|
||||
void RemoveGroupMarkerMask(uint32 mask) { if (mask == 0x20) m_markerMask = 0x20; m_markerMask &= ~mask; }
|
||||
bool HasMarker(uint32 mask) { return (m_markerMask & mask) != 0; }
|
||||
uint32 GetMarkerMask() { return m_markerMask; }
|
||||
|
||||
DynamicObject* GetRaidMarkerBySpellId(uint32 spell);
|
||||
void AddMarkerToList(ObjectGuid summonerGuid, ObjectGuid markerGuid) { m_raidMarkers.push_back({ summonerGuid, markerGuid }); }
|
||||
void RemoveRaidMarkerFromList(ObjectGuid markerGuid);
|
||||
void RemoveAllMarkerFromList() { m_raidMarkers.clear(); }
|
||||
void RemoveMarker();
|
||||
|
||||
// properties accessories
|
||||
bool IsFull() const;
|
||||
bool isLFGGroup() const;
|
||||
bool isLFRGroup() const;
|
||||
bool isRaidGroup() const;
|
||||
bool isBGGroup() const;
|
||||
bool isBFGroup() const;
|
||||
bool IsCreated() const;
|
||||
ObjectGuid GetLeaderGUID() const;
|
||||
ObjectGuid GetGUID() const;
|
||||
ObjectGuid::LowType GetLowGUID() const;
|
||||
const char * GetLeaderName() const;
|
||||
LootMethod GetLootMethod() const;
|
||||
ObjectGuid GetLooterGuid() const;
|
||||
ObjectGuid GetMasterLooterGuid() const;
|
||||
ItemQualities GetLootThreshold() const;
|
||||
|
||||
uint32 GetDbStoreId() const { return m_dbStoreId; }
|
||||
|
||||
// member manipulation methods
|
||||
bool IsMember(ObjectGuid guid) const;
|
||||
bool IsLeader(ObjectGuid guid) const;
|
||||
ObjectGuid GetMemberGUID(const std::string& name);
|
||||
uint8 GetMemberFlags(ObjectGuid guid) const;
|
||||
bool IsAssistant(ObjectGuid guid) const
|
||||
{
|
||||
return (GetMemberFlags(guid) & MEMBER_FLAG_ASSISTANT) == MEMBER_FLAG_ASSISTANT;
|
||||
}
|
||||
|
||||
Player* GetInvited(ObjectGuid guid) const;
|
||||
Player* GetInvited(const std::string& name) const;
|
||||
|
||||
bool SameSubGroup(ObjectGuid guid1, ObjectGuid guid2) const;
|
||||
bool SameSubGroup(ObjectGuid guid1, MemberSlot const* slot2) const;
|
||||
bool SameSubGroup(Player const* member1, Player const* member2) const;
|
||||
bool HasFreeSlotSubGroup(uint8 subgroup) const;
|
||||
|
||||
MemberSlotList const& GetMemberSlots() const { return m_memberSlots; }
|
||||
GroupReference* GetFirstMember() { return m_memberMgr.getFirst(); }
|
||||
GroupReference const* GetFirstMember() const { return m_memberMgr.getFirst(); }
|
||||
uint32 GetMembersCount() const { return m_memberSlots.size(); }
|
||||
uint32 GetInviteeCount() const { return m_invitees.size(); }
|
||||
GroupFlags GetGroupFlags() const { return m_groupFlags; }
|
||||
|
||||
uint8 GetMemberGroup(ObjectGuid guid) const;
|
||||
|
||||
void ConvertToLFG();
|
||||
void ConvertToLFR();
|
||||
void ConvertToRaid();
|
||||
void ConvertToGroup();
|
||||
|
||||
void SetBattlegroundGroup(Battleground* bg);
|
||||
void SetBattlefieldGroup(Battlefield* bf);
|
||||
GroupJoinBattlegroundResult CanJoinBattlegroundQueue(Battleground const* bgOrTemplate, BattlegroundQueueTypeId bgQueueTypeId, uint32 MinPlayerCount, uint32 MaxPlayerCount, bool isRated, uint32 arenaSlot);
|
||||
|
||||
void ChangeMembersGroup(ObjectGuid guid, uint8 group);
|
||||
void SetTargetIcon(uint8 id, ObjectGuid whoGuid, ObjectGuid targetGuid);
|
||||
void SetGroupMemberFlag(ObjectGuid guid, bool apply, GroupMemberFlags flag);
|
||||
void RemoveUniqueGroupMemberFlag(GroupMemberFlags flag);
|
||||
|
||||
Difficulty GetDifficulty(bool isRaid) const;
|
||||
Difficulty GetDungeonDifficulty() const;
|
||||
Difficulty GetRaidDifficulty() const;
|
||||
void SetDungeonDifficulty(Difficulty difficulty);
|
||||
void SetRaidDifficulty(Difficulty difficulty);
|
||||
bool InCombatToInstance(uint32 instanceId);
|
||||
void ResetInstances(uint8 method, bool isRaid, Player* SendMsgTo);
|
||||
|
||||
// -no description-
|
||||
//void SendInit(WorldSession* session);
|
||||
void SendTargetIconList(WorldSession* session);
|
||||
void SendRaidMarkerUpdate();
|
||||
void SendRaidMarkerUpdateToPlayer(ObjectGuid playerGUID, bool remove = false);
|
||||
void SendUpdate();
|
||||
void SendUpdateToPlayer(ObjectGuid playerGUID, MemberSlot* slot = nullptr);
|
||||
void SendUpdateDestroyGroupToPlayer(Player* player);
|
||||
void UpdatePlayerOutOfRange(Player* player);
|
||||
|
||||
template<class Worker>
|
||||
void BroadcastWorker(Worker& worker)
|
||||
{
|
||||
for (GroupReference* itr = GetFirstMember(); itr != nullptr; itr = itr->next())
|
||||
worker(itr->GetSource());
|
||||
}
|
||||
|
||||
template<class Worker>
|
||||
void BroadcastWorker(Worker const& worker) const
|
||||
{
|
||||
for (GroupReference const* itr = GetFirstMember(); itr != nullptr; itr = itr->next())
|
||||
worker(itr->GetSource());
|
||||
}
|
||||
|
||||
void BroadcastPacket(WorldPacket const* packet, bool ignorePlayersInBGRaid, int group = -1, ObjectGuid ignoredPlayer = ObjectGuid::Empty);
|
||||
void BroadcastAddonMessagePacket(WorldPacket const* packet, const std::string& prefix, bool ignorePlayersInBGRaid, int group = -1, uint64 ignore = 0);
|
||||
void BroadcastReadyCheck(WorldPacket const* packet);
|
||||
void OfflineReadyCheck();
|
||||
|
||||
/*********************************************************/
|
||||
/*** LOOT SYSTEM ***/
|
||||
/*********************************************************/
|
||||
|
||||
bool isRollLootActive() const;
|
||||
void SendLootStartRoll(uint32 CountDown, uint32 mapid, Roll const& r);
|
||||
void SendLootStartRollToPlayer(uint32 countDown, uint32 mapId, Player* p, bool canNeed, Roll const& r);
|
||||
void SendLootRoll(ObjectGuid SourceGuid, ObjectGuid TargetGuid, int32 RollNumber, uint8 RollType, Roll const& r, bool autoPass = false);
|
||||
void SendLootRollWon(ObjectGuid SourceGuid, ObjectGuid TargetGuid, int32 RollNumber, uint8 RollType, Roll const& r);
|
||||
void SendLootAllPassed(Roll const& roll);
|
||||
void SendLooter(Creature* creature, Player* pLooter);
|
||||
void GroupLoot(Loot* loot, WorldObject* pLootedObject);
|
||||
void NeedBeforeGreed(Loot* loot, WorldObject* pLootedObject);
|
||||
void MasterLoot(Loot* loot, WorldObject* pLootedObject);
|
||||
Rolls::iterator GetRoll(ObjectGuid Guid);
|
||||
void CountTheRoll(Rolls::iterator roll, Map* allowedMap);
|
||||
void CountRollVote(ObjectGuid playerGUID, ObjectGuid Guid, uint8 Choise);
|
||||
void EndRoll(Loot* loot, Map* allowedMap);
|
||||
|
||||
// related to disenchant rolls
|
||||
void ResetMaxEnchantingLevel();
|
||||
|
||||
void LinkMember(GroupReference* pRef);
|
||||
void DelinkMember(ObjectGuid guid);
|
||||
|
||||
InstanceGroupBind* BindToInstance(InstanceSave* save, bool permanent, bool load = false);
|
||||
void UnbindInstance(uint32 mapid, uint8 difficulty, bool unload = false);
|
||||
InstanceGroupBind* GetBoundInstance(Player* player);
|
||||
InstanceGroupBind* GetBoundInstance(Map* aMap);
|
||||
InstanceGroupBind* GetBoundInstance(MapEntry const* mapEntry);
|
||||
InstanceGroupBind* GetBoundInstance(Difficulty difficulty, uint32 mapId);
|
||||
BoundInstancesMap& GetBoundInstances(Difficulty difficulty);
|
||||
|
||||
// FG: evil hacks
|
||||
void BroadcastGroupUpdate(void);
|
||||
|
||||
// guild misc
|
||||
bool IsGuildGroupFor(Player* player);
|
||||
uint32 GetMembersCountOfGuild(uint32 guildId);
|
||||
uint32 GetNeededMembersOfSameGuild(uint8 arenaType, Map const* map);
|
||||
bool MemberLevelIsInRange(uint32 levelMin, uint32 levelMax);
|
||||
float GetGuildXpRateForPlayer(Player* player);
|
||||
void UpdateGuildFor(ObjectGuid guid, uint32 guildId);
|
||||
|
||||
protected:
|
||||
bool _setMembersGroup(ObjectGuid guid, uint8 group);
|
||||
void _homebindIfInstance(Player* player);
|
||||
|
||||
void _initRaidSubGroupsCounter();
|
||||
member_citerator _getMemberCSlot(ObjectGuid Guid) const;
|
||||
member_witerator _getMemberWSlot(ObjectGuid Guid);
|
||||
void SubGroupCounterIncrease(uint8 subgroup);
|
||||
void SubGroupCounterDecrease(uint8 subgroup);
|
||||
void ToggleGroupMemberFlag(member_witerator slot, uint8 flag, bool apply);
|
||||
|
||||
MemberSlotList m_memberSlots;
|
||||
GroupRefManager m_memberMgr;
|
||||
InvitesList m_invitees;
|
||||
ObjectGuid m_leaderGuid;
|
||||
std::string m_leaderName;
|
||||
GroupFlags m_groupFlags;
|
||||
uint32 m_markerMask;
|
||||
Difficulty m_dungeonDifficulty;
|
||||
Difficulty m_raidDifficulty;
|
||||
Battleground* m_bgGroup;
|
||||
Battlefield* m_bfGroup;
|
||||
ObjectGuid m_targetIcons[TARGETICONCOUNT];
|
||||
LootMethod m_lootMethod;
|
||||
ItemQualities m_lootThreshold;
|
||||
ObjectGuid m_looterGuid;
|
||||
ObjectGuid m_masterLooterGuid;
|
||||
Rolls RollId;
|
||||
BoundInstancesMap m_boundInstances[MAX_DIFFICULTY];
|
||||
uint8* m_subGroupsCounts;
|
||||
ObjectGuid m_guid;
|
||||
uint32 m_counter; // used only in SMSG_GROUP_LIST
|
||||
GroupDisenchantInfo m_disenchantInfo;
|
||||
uint32 m_dbStoreId; // Represents the ID used in database (Can be reused by other groups if group was disbanded)
|
||||
RaidMarkerList m_raidMarkers;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -7,9 +7,12 @@
|
||||
#include "ScriptedCreature.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Player.h"
|
||||
#include "Group.h"
|
||||
#include "TemporarySummon.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "Log.h"
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace VianoAdventurers
|
||||
{
|
||||
@@ -203,6 +206,7 @@ namespace VianoAdventurers
|
||||
ownerGuid = owner->GetGUID();
|
||||
|
||||
me->SetOwnerGUID(ownerGuid);
|
||||
me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED);
|
||||
me->SetFaction(owner->GetFaction());
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
@@ -507,10 +511,449 @@ namespace VianoAdventurers
|
||||
return new npc_horde_adventurer_meleeAI(creature);
|
||||
}
|
||||
};
|
||||
|
||||
// VIANO_TANK_V1
|
||||
class npc_horde_adventurer_tank : public CreatureScript
|
||||
{
|
||||
public:
|
||||
npc_horde_adventurer_tank() : CreatureScript("npc_horde_adventurer_tank") { }
|
||||
|
||||
struct npc_horde_adventurer_tankAI : public ScriptedAI
|
||||
{
|
||||
npc_horde_adventurer_tankAI(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
static constexpr uint8 SKULL_ICON_ID = 7;
|
||||
static constexpr float AUTO_PICKUP_RADIUS = 10.0f;
|
||||
static constexpr uint32 TANK_UPDATE_INTERVAL = 500;
|
||||
static constexpr uint32 AOE_DAMAGE_INTERVAL = 6000;
|
||||
|
||||
ObjectGuid ownerGuid;
|
||||
uint32 updateTimer = 0;
|
||||
uint32 recoveryTimer = 1000;
|
||||
uint32 aoeDamageTimer = 3000;
|
||||
|
||||
Player* GetOwnerPlayer() const
|
||||
{
|
||||
if (!ownerGuid)
|
||||
return nullptr;
|
||||
|
||||
return ObjectAccessor::GetPlayer(*me, ownerGuid);
|
||||
}
|
||||
|
||||
bool IsValidTankTarget(Unit* unit) const
|
||||
{
|
||||
if (!unit || !unit->IsAlive())
|
||||
return false;
|
||||
|
||||
// Dungeon-role bot: do not turn this into a PvP player hunter.
|
||||
if (!unit->ToCreature())
|
||||
return false;
|
||||
|
||||
return me->IsValidAttackTarget(unit);
|
||||
}
|
||||
|
||||
bool IsActivelyEngagedInCombat() const
|
||||
{
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (victim->IsAlive() && me->IsValidAttackTarget(victim))
|
||||
return true;
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (attacker && attacker->IsAlive() && me->IsValidAttackTarget(attacker))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FollowOwner(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE);
|
||||
}
|
||||
|
||||
void HandleOutOfCombatRecovery(uint32 diff)
|
||||
{
|
||||
if (!me->IsAlive() || IsActivelyEngagedInCombat())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (recoveryTimer > diff)
|
||||
{
|
||||
recoveryTimer -= diff;
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryTimer = 1000;
|
||||
|
||||
uint32 maxHealth = me->GetMaxHealth();
|
||||
if (me->GetHealth() < maxHealth)
|
||||
{
|
||||
uint32 gain = maxHealth / 10;
|
||||
if (!gain)
|
||||
gain = 1;
|
||||
|
||||
uint32 health = me->GetHealth() + gain;
|
||||
me->SetHealth(health > maxHealth ? maxHealth : health);
|
||||
}
|
||||
|
||||
uint32 maxMana = me->GetMaxPower(POWER_MANA);
|
||||
if (maxMana && me->GetPower(POWER_MANA) < maxMana)
|
||||
{
|
||||
uint32 gain = maxMana / 10;
|
||||
if (!gain)
|
||||
gain = 1;
|
||||
|
||||
uint32 mana = me->GetPower(POWER_MANA) + gain;
|
||||
me->SetPower(POWER_MANA, mana > maxMana ? maxMana : mana);
|
||||
}
|
||||
}
|
||||
|
||||
Unit* GetSkullTarget(Player* owner) const
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
Group* group = owner->GetGroup();
|
||||
if (!group)
|
||||
return nullptr;
|
||||
|
||||
ObjectGuid guid = group->GetTargetIcon(SKULL_ICON_ID);
|
||||
if (!guid)
|
||||
return nullptr;
|
||||
|
||||
Unit* target = ObjectAccessor::GetUnit(*me, guid);
|
||||
return IsValidTankTarget(target) ? target : nullptr;
|
||||
}
|
||||
|
||||
Unit* FindGroupAttacker(Player* owner) const
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
std::list<Unit*> groupUnits;
|
||||
owner->GetPartyMembers(groupUnits);
|
||||
|
||||
for (Unit* member : groupUnits)
|
||||
{
|
||||
if (!member || !member->IsAlive() || !member->IsInMap(owner))
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
if (IsValidTankTarget(attacker))
|
||||
return attacker;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Unit* SelectPrimaryTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
// Skull is the only explicit permission for the tank to initiate
|
||||
// a pull. It also overrides the current primary target mid-fight.
|
||||
if (Unit* skull = GetSkullTarget(owner))
|
||||
return skull;
|
||||
|
||||
// Existing combat takes precedence over player selection.
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (IsValidTankTarget(victim))
|
||||
return victim;
|
||||
|
||||
// Pick up anything already attacking the group.
|
||||
if (Unit* attacker = FindGroupAttacker(owner))
|
||||
return attacker;
|
||||
|
||||
// The owner already attacking something is also a valid combat signal.
|
||||
if (Unit* victim = owner->GetVictim())
|
||||
if (IsValidTankTarget(victim))
|
||||
return victim;
|
||||
|
||||
// GetVictim() is not always populated for players.
|
||||
if (owner->IsInCombat())
|
||||
if (Unit* selected = owner->GetSelectedUnit())
|
||||
if (IsValidTankTarget(selected))
|
||||
return selected;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsSafeAutoPickup(Unit* enemy) const
|
||||
{
|
||||
return IsValidTankTarget(enemy)
|
||||
&& me->GetDistance(enemy) <= AUTO_PICKUP_RADIUS
|
||||
&& me->IsWithinLOSInMap(enemy);
|
||||
}
|
||||
|
||||
void ForceTankThreat(Unit* enemy)
|
||||
{
|
||||
if (!IsSafeAutoPickup(enemy))
|
||||
return;
|
||||
|
||||
ThreatManager& threat = enemy->GetThreatManager();
|
||||
|
||||
// Match the current highest threat, then exceed it by 35%.
|
||||
// This gives us taunt-like recovery without permanent Fixate.
|
||||
threat.MatchUnitThreatToHighestThreat(me);
|
||||
|
||||
float current = threat.GetThreat(me, true);
|
||||
float bonus = current * 0.35f;
|
||||
if (bonus < 1.0f)
|
||||
bonus = 1.0f;
|
||||
|
||||
threat.AddThreat(me, bonus, nullptr, true, true);
|
||||
}
|
||||
|
||||
void CollectRelevantEnemyGuids(Player* owner, std::vector<ObjectGuid>& targets) const
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
auto addTarget = [this, &targets](Unit* enemy)
|
||||
{
|
||||
if (!IsSafeAutoPickup(enemy))
|
||||
return;
|
||||
|
||||
ObjectGuid const guid = enemy->GetGUID();
|
||||
if (std::find(targets.begin(), targets.end(), guid) == targets.end())
|
||||
targets.push_back(guid);
|
||||
};
|
||||
|
||||
std::list<Unit*> groupUnits;
|
||||
owner->GetPartyMembers(groupUnits);
|
||||
|
||||
for (Unit* member : groupUnits)
|
||||
{
|
||||
if (!member || !member->IsAlive() || !member->IsInMap(owner))
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
addTarget(attacker);
|
||||
}
|
||||
}
|
||||
|
||||
void RecoverGroupThreat(Player* owner)
|
||||
{
|
||||
if (!owner || !IsActivelyEngagedInCombat())
|
||||
return;
|
||||
|
||||
if (Unit* victim = me->GetVictim())
|
||||
ForceTankThreat(victim);
|
||||
|
||||
std::vector<ObjectGuid> targets;
|
||||
CollectRelevantEnemyGuids(owner, targets);
|
||||
|
||||
for (ObjectGuid const& guid : targets)
|
||||
{
|
||||
if (Unit* enemy = ObjectAccessor::GetUnit(*me, guid))
|
||||
ForceTankThreat(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
void DoTankAoeDamage(Player* owner)
|
||||
{
|
||||
if (!owner || !IsActivelyEngagedInCombat())
|
||||
return;
|
||||
|
||||
uint32 damage = uint32(me->getLevel()) * 2u;
|
||||
if (!damage)
|
||||
damage = 1;
|
||||
|
||||
std::vector<ObjectGuid> targets;
|
||||
CollectRelevantEnemyGuids(owner, targets);
|
||||
|
||||
for (ObjectGuid const& guid : targets)
|
||||
{
|
||||
Unit* enemy = ObjectAccessor::GetUnit(*me, guid);
|
||||
if (!IsSafeAutoPickup(enemy))
|
||||
continue;
|
||||
|
||||
Unit::DealDamage(
|
||||
me,
|
||||
enemy,
|
||||
damage,
|
||||
0,
|
||||
DIRECT_DAMAGE,
|
||||
SPELL_SCHOOL_MASK_NORMAL,
|
||||
nullptr,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeAI() override
|
||||
{
|
||||
ScriptedAI::InitializeAI();
|
||||
|
||||
TempSummon* summon = me->ToTempSummon();
|
||||
if (!summon)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
Player* owner = summoner ? summoner->ToPlayer() : nullptr;
|
||||
if (!owner)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
ownerGuid = owner->GetGUID();
|
||||
|
||||
me->SetOwnerGUID(ownerGuid);
|
||||
me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED);
|
||||
me->SetFaction(owner->GetFaction());
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
|
||||
static constexpr uint32 HORDE_ADVENTURER_MODELS[] =
|
||||
{
|
||||
30063,
|
||||
30064,
|
||||
30065,
|
||||
30066
|
||||
};
|
||||
|
||||
me->SetDisplayId(HORDE_ADVENTURER_MODELS[urand(0, 3)], true);
|
||||
me->SetReactState(REACT_DEFENSIVE);
|
||||
|
||||
FollowOwner(owner);
|
||||
|
||||
TC_LOG_INFO(
|
||||
"scripts",
|
||||
"[VianoTank] initialized entry=%u owner='%s' level=%u health=%u armor=%u",
|
||||
me->GetEntry(),
|
||||
owner->GetName().c_str(),
|
||||
uint32(me->getLevel()),
|
||||
me->GetMaxHealth(),
|
||||
me->GetArmor());
|
||||
}
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
updateTimer = 0;
|
||||
recoveryTimer = 1000;
|
||||
aoeDamageTimer = 3000;
|
||||
|
||||
if (Player* owner = GetOwnerPlayer())
|
||||
if (!IsActivelyEngagedInCombat())
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void MoveInLineOfSight(Unit* /*who*/) override
|
||||
{
|
||||
// No autonomous proximity pulls.
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
HandleOutOfCombatRecovery(diff);
|
||||
|
||||
if (aoeDamageTimer > diff)
|
||||
aoeDamageTimer -= diff;
|
||||
else
|
||||
aoeDamageTimer = 0;
|
||||
|
||||
if (updateTimer > diff)
|
||||
{
|
||||
updateTimer -= diff;
|
||||
|
||||
if (me->GetVictim())
|
||||
DoMeleeAttackIfReady();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateTimer = TANK_UPDATE_INTERVAL;
|
||||
|
||||
Player* owner = GetOwnerPlayer();
|
||||
if (!owner || !owner->IsInWorld() || !me->IsInMap(owner))
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
if (me->getLevel() != owner->getLevel())
|
||||
{
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
}
|
||||
|
||||
bool engaged = IsActivelyEngagedInCombat();
|
||||
|
||||
// Recovery teleport is strictly out-of-combat.
|
||||
if (!engaged && me->GetDistance(owner) > TELEPORT_DISTANCE)
|
||||
{
|
||||
me->GetMotionMaster()->Clear();
|
||||
|
||||
me->NearTeleportTo(
|
||||
owner->GetPositionX() + 1.5f,
|
||||
owner->GetPositionY(),
|
||||
owner->GetPositionZ(),
|
||||
owner->GetOrientation());
|
||||
|
||||
FollowOwner(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* skullTarget = GetSkullTarget(owner);
|
||||
bool const explicitSkullPull = skullTarget && !engaged;
|
||||
Unit* desiredTarget = skullTarget ? skullTarget : SelectPrimaryTarget(owner);
|
||||
|
||||
if (desiredTarget && me->GetVictim() != desiredTarget)
|
||||
{
|
||||
if (explicitSkullPull)
|
||||
{
|
||||
if (!me->IsWithinLOSInMap(desiredTarget))
|
||||
{
|
||||
FollowOwner(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
me->SetInCombatWith(desiredTarget);
|
||||
desiredTarget->SetInCombatWith(me);
|
||||
}
|
||||
|
||||
AttackStart(desiredTarget);
|
||||
}
|
||||
|
||||
engaged = IsActivelyEngagedInCombat();
|
||||
|
||||
if (engaged)
|
||||
{
|
||||
RecoverGroupThreat(owner);
|
||||
|
||||
if (aoeDamageTimer == 0)
|
||||
{
|
||||
aoeDamageTimer = AOE_DAMAGE_INTERVAL;
|
||||
DoTankAoeDamage(owner);
|
||||
}
|
||||
|
||||
if (me->GetVictim())
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
else
|
||||
FollowOwner(owner);
|
||||
}
|
||||
};
|
||||
|
||||
CreatureAI* GetAI(Creature* creature) const override
|
||||
{
|
||||
return new npc_horde_adventurer_tankAI(creature);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
void AddSC_viano_adventurers()
|
||||
{
|
||||
using namespace VianoAdventurers;
|
||||
new npc_horde_adventurer_melee();
|
||||
new npc_horde_adventurer_tank();
|
||||
}
|
||||
+516
@@ -0,0 +1,516 @@
|
||||
/*
|
||||
* Viano custom dungeon adventurers
|
||||
* Initial melee companion implementation.
|
||||
*/
|
||||
|
||||
#include "ScriptMgr.h"
|
||||
#include "ScriptedCreature.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Player.h"
|
||||
#include "TemporarySummon.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "Log.h"
|
||||
|
||||
namespace VianoAdventurers
|
||||
{
|
||||
static constexpr float FOLLOW_DISTANCE = 2.5f;
|
||||
static constexpr float FOLLOW_ANGLE = 3.14159265f;
|
||||
static constexpr float TELEPORT_DISTANCE = 45.0f;
|
||||
static constexpr uint32 UPDATE_INTERVAL = 500;
|
||||
static constexpr uint32 DIAGNOSTIC_INTERVAL = 2000;
|
||||
|
||||
// VIANO_DD_PROFILE_V1_5
|
||||
enum AdventurerSpells : uint32
|
||||
{
|
||||
SPELL_ADVENTURER_MORTAL_STRIKE = 65926,
|
||||
SPELL_ADVENTURER_OVERPOWER = 65924,
|
||||
SPELL_ADVENTURER_BLADESTORM = 65947
|
||||
};
|
||||
|
||||
enum AdventurerEvents : uint32
|
||||
{
|
||||
EVENT_ADVENTURER_MORTAL_STRIKE = 1,
|
||||
EVENT_ADVENTURER_OVERPOWER,
|
||||
EVENT_ADVENTURER_BLADESTORM
|
||||
};
|
||||
|
||||
// VIANO_AI_DIAGNOSTICS_V1
|
||||
|
||||
class npc_horde_adventurer_melee : public CreatureScript
|
||||
{
|
||||
public:
|
||||
npc_horde_adventurer_melee() : CreatureScript("npc_horde_adventurer_melee") { }
|
||||
|
||||
struct npc_horde_adventurer_meleeAI : public ScriptedAI
|
||||
{
|
||||
npc_horde_adventurer_meleeAI(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
ObjectGuid ownerGuid;
|
||||
EventMap combatEvents;
|
||||
uint32 updateTimer = 0;
|
||||
uint32 diagnosticTimer = 0;
|
||||
uint32 recoveryTimer = 1000;
|
||||
Player* GetOwnerPlayer() const
|
||||
{
|
||||
if (!ownerGuid)
|
||||
return nullptr;
|
||||
|
||||
return ObjectAccessor::GetPlayer(*me, ownerGuid);
|
||||
}
|
||||
|
||||
void FollowOwner(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE);
|
||||
}
|
||||
|
||||
void ResetCombatEvents()
|
||||
{
|
||||
combatEvents.Reset();
|
||||
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE, 2 * IN_MILLISECONDS);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER, 5 * IN_MILLISECONDS);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM, 12 * IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
void HandleCombatActions(uint32 diff)
|
||||
{
|
||||
Unit* victim = me->GetVictim();
|
||||
if (!victim || !victim->IsAlive())
|
||||
return;
|
||||
|
||||
combatEvents.Update(diff);
|
||||
|
||||
if (me->HasUnitState(UNIT_STATE_CASTING))
|
||||
return;
|
||||
|
||||
if (uint32 eventId = combatEvents.ExecuteEvent())
|
||||
{
|
||||
switch (eventId)
|
||||
{
|
||||
case EVENT_ADVENTURER_MORTAL_STRIKE:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Mortal Strike: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_MORTAL_STRIKE);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE,
|
||||
urand(6 * IN_MILLISECONDS, 8 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
case EVENT_ADVENTURER_OVERPOWER:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Overpower: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_OVERPOWER);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER,
|
||||
urand(10 * IN_MILLISECONDS, 14 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
case EVENT_ADVENTURER_BLADESTORM:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Bladestorm: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_BLADESTORM);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM,
|
||||
urand(30 * IN_MILLISECONDS, 45 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
void HandleOutOfCombatRecovery(uint32 diff)
|
||||
{
|
||||
if (!me->IsAlive())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keine künstliche Regeneration während eines Kampfes.
|
||||
if (me->IsInCombat())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (recoveryTimer > diff)
|
||||
{
|
||||
recoveryTimer -= diff;
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryTimer = 1000;
|
||||
|
||||
// 10 % der maximalen Lebenspunkte pro Sekunde.
|
||||
uint32 maxHealth = me->GetMaxHealth();
|
||||
|
||||
if (me->GetHealth() < maxHealth)
|
||||
{
|
||||
uint32 healthGain = maxHealth / 10;
|
||||
|
||||
if (healthGain == 0)
|
||||
healthGain = 1;
|
||||
|
||||
uint32 newHealth = me->GetHealth() + healthGain;
|
||||
|
||||
if (newHealth > maxHealth)
|
||||
newHealth = maxHealth;
|
||||
|
||||
me->SetHealth(newHealth);
|
||||
}
|
||||
|
||||
// 10 % des maximalen Manas pro Sekunde.
|
||||
uint32 maxMana = me->GetMaxPower(POWER_MANA);
|
||||
|
||||
if (maxMana > 0 && me->GetPower(POWER_MANA) < maxMana)
|
||||
{
|
||||
uint32 manaGain = maxMana / 10;
|
||||
|
||||
if (manaGain == 0)
|
||||
manaGain = 1;
|
||||
|
||||
uint32 newMana = me->GetPower(POWER_MANA) + manaGain;
|
||||
|
||||
if (newMana > maxMana)
|
||||
newMana = maxMana;
|
||||
|
||||
me->SetPower(POWER_MANA, newMana);
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeAI() override
|
||||
{
|
||||
ScriptedAI::InitializeAI();
|
||||
|
||||
TempSummon* summon = me->ToTempSummon();
|
||||
if (!summon)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
Player* owner = summoner ? summoner->ToPlayer() : nullptr;
|
||||
if (!owner)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
ownerGuid = owner->GetGUID();
|
||||
|
||||
me->SetOwnerGUID(ownerGuid);
|
||||
me->SetFaction(owner->GetFaction());
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
|
||||
|
||||
static constexpr uint32 HORDE_ADVENTURER_MODELS[] =
|
||||
{
|
||||
30063,
|
||||
30064,
|
||||
30065,
|
||||
30066
|
||||
};
|
||||
uint32 displayId = HORDE_ADVENTURER_MODELS[urand(0, 3)];
|
||||
|
||||
me->SetDisplayId(displayId, true);
|
||||
|
||||
TC_LOG_INFO("scripts","[VianoAI] Horde adventurer display selected: %u", displayId);
|
||||
|
||||
// REACT_PASSIVE lets AttackStart() briefly set a victim, but the
|
||||
// core subsequently drops that victim again. We already suppress
|
||||
// autonomous proximity aggro in MoveInLineOfSight(), so DEFENSIVE
|
||||
// keeps an explicitly assigned assist target without making the
|
||||
// adventurer independently pull nearby creatures.
|
||||
me->SetReactState(REACT_DEFENSIVE);
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] initialized me='%s' entry=%u owner='%s' ownerGuid=%s meFaction=%u ownerFaction=%u level=%u",
|
||||
me->GetName().c_str(), me->GetEntry(), owner->GetName().c_str(), ownerGuid.ToString().c_str(),
|
||||
me->GetFaction(), owner->GetFaction(), uint32(me->getLevel()));
|
||||
|
||||
ResetCombatEvents();
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
ResetCombatEvents();
|
||||
|
||||
if (Player* owner = GetOwnerPlayer())
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void MoveInLineOfSight(Unit* /*who*/) override
|
||||
{
|
||||
}
|
||||
|
||||
char const* UnitName(Unit* unit) const
|
||||
{
|
||||
return unit ? unit->GetName().c_str() : "<none>";
|
||||
}
|
||||
|
||||
uint32 UnitEntry(Unit* unit) const
|
||||
{
|
||||
return unit ? unit->GetEntry() : 0;
|
||||
}
|
||||
|
||||
bool IsValidCandidate(Unit* unit) const
|
||||
{
|
||||
return unit && unit->IsAlive() && me->IsValidAttackTarget(unit);
|
||||
}
|
||||
|
||||
void LogDiagnostics(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
Unit* ownerVictim = owner->GetVictim();
|
||||
Unit* selected = owner->GetSelectedUnit();
|
||||
Unit* meVictim = me->GetVictim();
|
||||
|
||||
Unit* firstOwnerAttacker = nullptr;
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
{
|
||||
if (attacker)
|
||||
{
|
||||
firstOwnerAttacker = attacker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Unit* firstMeAttacker = nullptr;
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
{
|
||||
if (attacker)
|
||||
{
|
||||
firstMeAttacker = attacker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] me='%s' entry=%u lvl=%u faction=%u react=%u alive=%u combat=%u hp=%u/%u victim='%s'(%u) attackers=%u distance=%.2f",
|
||||
me->GetName().c_str(), me->GetEntry(), uint32(me->getLevel()), me->GetFaction(), uint32(me->GetReactState()),
|
||||
me->IsAlive() ? 1u : 0u, me->IsInCombat() ? 1u : 0u, me->GetHealth(), me->GetMaxHealth(),
|
||||
UnitName(meVictim), UnitEntry(meVictim), uint32(me->getAttackers().size()), me->GetDistance(owner));
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] owner='%s' lvl=%u faction=%u alive=%u combat=%u victim='%s'(%u) selected='%s'(%u) attackers=%u",
|
||||
owner->GetName().c_str(), uint32(owner->getLevel()), owner->GetFaction(), owner->IsAlive() ? 1u : 0u,
|
||||
owner->IsInCombat() ? 1u : 0u, UnitName(ownerVictim), UnitEntry(ownerVictim), UnitName(selected), UnitEntry(selected),
|
||||
uint32(owner->getAttackers().size()));
|
||||
|
||||
if (ownerVictim)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] ownerVictim='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
ownerVictim->GetName().c_str(), ownerVictim->GetEntry(), ownerVictim->GetFaction(), IsValidCandidate(ownerVictim) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(ownerVictim) ? 1u : 0u, owner->IsValidAttackTarget(ownerVictim) ? 1u : 0u,
|
||||
UnitName(ownerVictim->GetVictim()), UnitEntry(ownerVictim->GetVictim()));
|
||||
|
||||
if (selected)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] selected='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
selected->GetName().c_str(), selected->GetEntry(), selected->GetFaction(), IsValidCandidate(selected) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(selected) ? 1u : 0u, owner->IsValidAttackTarget(selected) ? 1u : 0u,
|
||||
UnitName(selected->GetVictim()), UnitEntry(selected->GetVictim()));
|
||||
|
||||
if (firstOwnerAttacker)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] firstOwnerAttacker='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
firstOwnerAttacker->GetName().c_str(), firstOwnerAttacker->GetEntry(), firstOwnerAttacker->GetFaction(), IsValidCandidate(firstOwnerAttacker) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u, owner->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u,
|
||||
UnitName(firstOwnerAttacker->GetVictim()), UnitEntry(firstOwnerAttacker->GetVictim()));
|
||||
|
||||
if (firstMeAttacker)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] firstMeAttacker='%s' entry=%u faction=%u victim='%s'(%u)",
|
||||
firstMeAttacker->GetName().c_str(), firstMeAttacker->GetEntry(), firstMeAttacker->GetFaction(),
|
||||
UnitName(firstMeAttacker->GetVictim()), UnitEntry(firstMeAttacker->GetVictim()));
|
||||
}
|
||||
|
||||
bool IsActivelyEngagedInCombat() const
|
||||
{
|
||||
// IsInCombat() alone is intentionally not enough here: the Core
|
||||
// may keep the combat flag set after the bot has no live opponent.
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (IsValidCandidate(victim))
|
||||
return true;
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Unit* SelectGroupCombatTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
std::list<Unit*> partyMembers;
|
||||
owner->GetPartyMembers(partyMembers);
|
||||
|
||||
// In Vcore GetPartyMembers() returns living members of the same
|
||||
// party/subgroup on the same map, plus their living guardian pets.
|
||||
// An enemy attacking one of them is therefore already part of the
|
||||
// group's current fight.
|
||||
for (Unit* member : partyMembers)
|
||||
{
|
||||
if (!member || !member->IsAlive())
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return attacker;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Unit* SelectAssistTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
// 1. An active owner victim is an explicit target instruction.
|
||||
if (Unit* target = owner->GetVictim())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
|
||||
// During an already running fight, an explicitly selected living
|
||||
// hostile target may also be treated as an instruction. Merely
|
||||
// selecting an enemy out of combat still must not pull it.
|
||||
if (owner->IsInCombat())
|
||||
{
|
||||
if (Unit* target = owner->GetSelectedUnit())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
}
|
||||
|
||||
// 2. Keep the bot's own valid current victim.
|
||||
if (Unit* target = me->GetVictim())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
|
||||
// 3. Continue the group's fight even if the owner is a healer and
|
||||
// currently targets a friendly tank or another party member.
|
||||
if (Unit* target = SelectGroupCombatTarget(owner))
|
||||
return target;
|
||||
|
||||
// 4. Finally, defend the bot itself if something is attacking it.
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return attacker;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
HandleOutOfCombatRecovery(diff);
|
||||
if (diagnosticTimer > diff)
|
||||
diagnosticTimer -= diff;
|
||||
else
|
||||
diagnosticTimer = 0;
|
||||
|
||||
if (updateTimer > diff)
|
||||
{
|
||||
updateTimer -= diff;
|
||||
|
||||
// VIANO_TARGET_CONTROL_V1_4
|
||||
// Keep the explicitly assigned victim. UpdateVictim() may
|
||||
// select another creature from the threat list.
|
||||
if (me->GetVictim())
|
||||
HandleCombatActions(diff);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateTimer = UPDATE_INTERVAL;
|
||||
|
||||
Player* owner = GetOwnerPlayer();
|
||||
if (!owner || !owner->IsInWorld() || !me->IsInMap(owner))
|
||||
{
|
||||
TC_LOG_INFO("scripts", "[VianoAI] despawn: owner invalid or not on same map (ownerFound=%u)", owner ? 1u : 0u);
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
if (diagnosticTimer == 0)
|
||||
{
|
||||
diagnosticTimer = DIAGNOSTIC_INTERVAL;
|
||||
LogDiagnostics(owner);
|
||||
}
|
||||
|
||||
if (me->getLevel() != owner->getLevel())
|
||||
{
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
}
|
||||
|
||||
if (me->GetDistance(owner) > TELEPORT_DISTANCE && !IsActivelyEngagedInCombat())
|
||||
{
|
||||
// Keep the recovery teleport for geometry/jump problems, even
|
||||
// if the group is already fighting elsewhere. Only the bot's
|
||||
// own active engagement blocks teleporting.
|
||||
me->GetMotionMaster()->Clear();
|
||||
|
||||
me->NearTeleportTo(
|
||||
owner->GetPositionX() + 1.5f,
|
||||
owner->GetPositionY(),
|
||||
owner->GetPositionZ(),
|
||||
owner->GetOrientation()
|
||||
);
|
||||
|
||||
FollowOwner(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* desiredTarget = SelectAssistTarget(owner);
|
||||
|
||||
if (desiredTarget)
|
||||
{
|
||||
if (me->GetVictim() != desiredTarget)
|
||||
{
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] AttackStart requested: target='%s' entry=%u targetFaction=%u valid=%u meCombatBefore=%u",
|
||||
desiredTarget->GetName().c_str(), desiredTarget->GetEntry(), desiredTarget->GetFaction(),
|
||||
me->IsValidAttackTarget(desiredTarget) ? 1u : 0u, me->IsInCombat() ? 1u : 0u);
|
||||
|
||||
AttackStart(desiredTarget);
|
||||
|
||||
Unit* afterVictim = me->GetVictim();
|
||||
TC_LOG_INFO("scripts", "[VianoAI] AttackStart result: meCombatAfter=%u victim='%s'(%u)",
|
||||
me->IsInCombat() ? 1u : 0u, UnitName(afterVictim), UnitEntry(afterVictim));
|
||||
}
|
||||
}
|
||||
else if (!IsActivelyEngagedInCombat())
|
||||
{
|
||||
// A stale Core combat flag must not prevent recovery/following.
|
||||
// We deliberately do not clear the flag here; OOC regeneration
|
||||
// will be handled as a separate change.
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
// VIANO_TARGET_CONTROL_V1_4
|
||||
// Keep the explicitly assigned victim. UpdateVictim() may
|
||||
// select another creature from the threat list.
|
||||
if (me->GetVictim())
|
||||
HandleCombatActions(diff);
|
||||
}
|
||||
};
|
||||
|
||||
CreatureAI* GetAI(Creature* creature) const override
|
||||
{
|
||||
return new npc_horde_adventurer_meleeAI(creature);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void AddSC_viano_adventurers()
|
||||
{
|
||||
using namespace VianoAdventurers;
|
||||
new npc_horde_adventurer_melee();
|
||||
}
|
||||
+956
@@ -0,0 +1,956 @@
|
||||
/*
|
||||
* Viano custom dungeon adventurers
|
||||
* Initial melee companion implementation.
|
||||
*/
|
||||
|
||||
#include "ScriptMgr.h"
|
||||
#include "ScriptedCreature.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Player.h"
|
||||
#include "Group.h"
|
||||
#include "TemporarySummon.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "Log.h"
|
||||
|
||||
namespace VianoAdventurers
|
||||
{
|
||||
static constexpr float FOLLOW_DISTANCE = 2.5f;
|
||||
static constexpr float FOLLOW_ANGLE = 3.14159265f;
|
||||
static constexpr float TELEPORT_DISTANCE = 45.0f;
|
||||
static constexpr uint32 UPDATE_INTERVAL = 500;
|
||||
static constexpr uint32 DIAGNOSTIC_INTERVAL = 2000;
|
||||
|
||||
// VIANO_DD_PROFILE_V1_5
|
||||
enum AdventurerSpells : uint32
|
||||
{
|
||||
SPELL_ADVENTURER_MORTAL_STRIKE = 65926,
|
||||
SPELL_ADVENTURER_OVERPOWER = 65924,
|
||||
SPELL_ADVENTURER_BLADESTORM = 65947
|
||||
};
|
||||
|
||||
enum AdventurerEvents : uint32
|
||||
{
|
||||
EVENT_ADVENTURER_MORTAL_STRIKE = 1,
|
||||
EVENT_ADVENTURER_OVERPOWER,
|
||||
EVENT_ADVENTURER_BLADESTORM
|
||||
};
|
||||
|
||||
// VIANO_AI_DIAGNOSTICS_V1
|
||||
|
||||
class npc_horde_adventurer_melee : public CreatureScript
|
||||
{
|
||||
public:
|
||||
npc_horde_adventurer_melee() : CreatureScript("npc_horde_adventurer_melee") { }
|
||||
|
||||
struct npc_horde_adventurer_meleeAI : public ScriptedAI
|
||||
{
|
||||
npc_horde_adventurer_meleeAI(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
ObjectGuid ownerGuid;
|
||||
EventMap combatEvents;
|
||||
uint32 updateTimer = 0;
|
||||
uint32 diagnosticTimer = 0;
|
||||
uint32 recoveryTimer = 1000;
|
||||
Player* GetOwnerPlayer() const
|
||||
{
|
||||
if (!ownerGuid)
|
||||
return nullptr;
|
||||
|
||||
return ObjectAccessor::GetPlayer(*me, ownerGuid);
|
||||
}
|
||||
|
||||
void FollowOwner(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE);
|
||||
}
|
||||
|
||||
void ResetCombatEvents()
|
||||
{
|
||||
combatEvents.Reset();
|
||||
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE, 2 * IN_MILLISECONDS);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER, 5 * IN_MILLISECONDS);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM, 12 * IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
void HandleCombatActions(uint32 diff)
|
||||
{
|
||||
Unit* victim = me->GetVictim();
|
||||
if (!victim || !victim->IsAlive())
|
||||
return;
|
||||
|
||||
combatEvents.Update(diff);
|
||||
|
||||
if (me->HasUnitState(UNIT_STATE_CASTING))
|
||||
return;
|
||||
|
||||
if (uint32 eventId = combatEvents.ExecuteEvent())
|
||||
{
|
||||
switch (eventId)
|
||||
{
|
||||
case EVENT_ADVENTURER_MORTAL_STRIKE:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Mortal Strike: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_MORTAL_STRIKE);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE,
|
||||
urand(6 * IN_MILLISECONDS, 8 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
case EVENT_ADVENTURER_OVERPOWER:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Overpower: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_OVERPOWER);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER,
|
||||
urand(10 * IN_MILLISECONDS, 14 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
case EVENT_ADVENTURER_BLADESTORM:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Bladestorm: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_BLADESTORM);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM,
|
||||
urand(30 * IN_MILLISECONDS, 45 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
void HandleOutOfCombatRecovery(uint32 diff)
|
||||
{
|
||||
if (!me->IsAlive())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keine künstliche Regeneration während eines Kampfes.
|
||||
if (me->IsInCombat())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (recoveryTimer > diff)
|
||||
{
|
||||
recoveryTimer -= diff;
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryTimer = 1000;
|
||||
|
||||
// 10 % der maximalen Lebenspunkte pro Sekunde.
|
||||
uint32 maxHealth = me->GetMaxHealth();
|
||||
|
||||
if (me->GetHealth() < maxHealth)
|
||||
{
|
||||
uint32 healthGain = maxHealth / 10;
|
||||
|
||||
if (healthGain == 0)
|
||||
healthGain = 1;
|
||||
|
||||
uint32 newHealth = me->GetHealth() + healthGain;
|
||||
|
||||
if (newHealth > maxHealth)
|
||||
newHealth = maxHealth;
|
||||
|
||||
me->SetHealth(newHealth);
|
||||
}
|
||||
|
||||
// 10 % des maximalen Manas pro Sekunde.
|
||||
uint32 maxMana = me->GetMaxPower(POWER_MANA);
|
||||
|
||||
if (maxMana > 0 && me->GetPower(POWER_MANA) < maxMana)
|
||||
{
|
||||
uint32 manaGain = maxMana / 10;
|
||||
|
||||
if (manaGain == 0)
|
||||
manaGain = 1;
|
||||
|
||||
uint32 newMana = me->GetPower(POWER_MANA) + manaGain;
|
||||
|
||||
if (newMana > maxMana)
|
||||
newMana = maxMana;
|
||||
|
||||
me->SetPower(POWER_MANA, newMana);
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeAI() override
|
||||
{
|
||||
ScriptedAI::InitializeAI();
|
||||
|
||||
TempSummon* summon = me->ToTempSummon();
|
||||
if (!summon)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
Player* owner = summoner ? summoner->ToPlayer() : nullptr;
|
||||
if (!owner)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
ownerGuid = owner->GetGUID();
|
||||
|
||||
me->SetOwnerGUID(ownerGuid);
|
||||
me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED);
|
||||
me->SetFaction(owner->GetFaction());
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
|
||||
|
||||
static constexpr uint32 HORDE_ADVENTURER_MODELS[] =
|
||||
{
|
||||
30063,
|
||||
30064,
|
||||
30065,
|
||||
30066
|
||||
};
|
||||
uint32 displayId = HORDE_ADVENTURER_MODELS[urand(0, 3)];
|
||||
|
||||
me->SetDisplayId(displayId, true);
|
||||
|
||||
TC_LOG_INFO("scripts","[VianoAI] Horde adventurer display selected: %u", displayId);
|
||||
|
||||
// REACT_PASSIVE lets AttackStart() briefly set a victim, but the
|
||||
// core subsequently drops that victim again. We already suppress
|
||||
// autonomous proximity aggro in MoveInLineOfSight(), so DEFENSIVE
|
||||
// keeps an explicitly assigned assist target without making the
|
||||
// adventurer independently pull nearby creatures.
|
||||
me->SetReactState(REACT_DEFENSIVE);
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] initialized me='%s' entry=%u owner='%s' ownerGuid=%s meFaction=%u ownerFaction=%u level=%u",
|
||||
me->GetName().c_str(), me->GetEntry(), owner->GetName().c_str(), ownerGuid.ToString().c_str(),
|
||||
me->GetFaction(), owner->GetFaction(), uint32(me->getLevel()));
|
||||
|
||||
ResetCombatEvents();
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
ResetCombatEvents();
|
||||
|
||||
if (Player* owner = GetOwnerPlayer())
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void MoveInLineOfSight(Unit* /*who*/) override
|
||||
{
|
||||
}
|
||||
|
||||
char const* UnitName(Unit* unit) const
|
||||
{
|
||||
return unit ? unit->GetName().c_str() : "<none>";
|
||||
}
|
||||
|
||||
uint32 UnitEntry(Unit* unit) const
|
||||
{
|
||||
return unit ? unit->GetEntry() : 0;
|
||||
}
|
||||
|
||||
bool IsValidCandidate(Unit* unit) const
|
||||
{
|
||||
return unit && unit->IsAlive() && me->IsValidAttackTarget(unit);
|
||||
}
|
||||
|
||||
void LogDiagnostics(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
Unit* ownerVictim = owner->GetVictim();
|
||||
Unit* selected = owner->GetSelectedUnit();
|
||||
Unit* meVictim = me->GetVictim();
|
||||
|
||||
Unit* firstOwnerAttacker = nullptr;
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
{
|
||||
if (attacker)
|
||||
{
|
||||
firstOwnerAttacker = attacker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Unit* firstMeAttacker = nullptr;
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
{
|
||||
if (attacker)
|
||||
{
|
||||
firstMeAttacker = attacker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] me='%s' entry=%u lvl=%u faction=%u react=%u alive=%u combat=%u hp=%u/%u victim='%s'(%u) attackers=%u distance=%.2f",
|
||||
me->GetName().c_str(), me->GetEntry(), uint32(me->getLevel()), me->GetFaction(), uint32(me->GetReactState()),
|
||||
me->IsAlive() ? 1u : 0u, me->IsInCombat() ? 1u : 0u, me->GetHealth(), me->GetMaxHealth(),
|
||||
UnitName(meVictim), UnitEntry(meVictim), uint32(me->getAttackers().size()), me->GetDistance(owner));
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] owner='%s' lvl=%u faction=%u alive=%u combat=%u victim='%s'(%u) selected='%s'(%u) attackers=%u",
|
||||
owner->GetName().c_str(), uint32(owner->getLevel()), owner->GetFaction(), owner->IsAlive() ? 1u : 0u,
|
||||
owner->IsInCombat() ? 1u : 0u, UnitName(ownerVictim), UnitEntry(ownerVictim), UnitName(selected), UnitEntry(selected),
|
||||
uint32(owner->getAttackers().size()));
|
||||
|
||||
if (ownerVictim)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] ownerVictim='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
ownerVictim->GetName().c_str(), ownerVictim->GetEntry(), ownerVictim->GetFaction(), IsValidCandidate(ownerVictim) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(ownerVictim) ? 1u : 0u, owner->IsValidAttackTarget(ownerVictim) ? 1u : 0u,
|
||||
UnitName(ownerVictim->GetVictim()), UnitEntry(ownerVictim->GetVictim()));
|
||||
|
||||
if (selected)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] selected='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
selected->GetName().c_str(), selected->GetEntry(), selected->GetFaction(), IsValidCandidate(selected) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(selected) ? 1u : 0u, owner->IsValidAttackTarget(selected) ? 1u : 0u,
|
||||
UnitName(selected->GetVictim()), UnitEntry(selected->GetVictim()));
|
||||
|
||||
if (firstOwnerAttacker)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] firstOwnerAttacker='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
firstOwnerAttacker->GetName().c_str(), firstOwnerAttacker->GetEntry(), firstOwnerAttacker->GetFaction(), IsValidCandidate(firstOwnerAttacker) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u, owner->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u,
|
||||
UnitName(firstOwnerAttacker->GetVictim()), UnitEntry(firstOwnerAttacker->GetVictim()));
|
||||
|
||||
if (firstMeAttacker)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] firstMeAttacker='%s' entry=%u faction=%u victim='%s'(%u)",
|
||||
firstMeAttacker->GetName().c_str(), firstMeAttacker->GetEntry(), firstMeAttacker->GetFaction(),
|
||||
UnitName(firstMeAttacker->GetVictim()), UnitEntry(firstMeAttacker->GetVictim()));
|
||||
}
|
||||
|
||||
bool IsActivelyEngagedInCombat() const
|
||||
{
|
||||
// IsInCombat() alone is intentionally not enough here: the Core
|
||||
// may keep the combat flag set after the bot has no live opponent.
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (IsValidCandidate(victim))
|
||||
return true;
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Unit* SelectGroupCombatTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
std::list<Unit*> partyMembers;
|
||||
owner->GetPartyMembers(partyMembers);
|
||||
|
||||
// In Vcore GetPartyMembers() returns living members of the same
|
||||
// party/subgroup on the same map, plus their living guardian pets.
|
||||
// An enemy attacking one of them is therefore already part of the
|
||||
// group's current fight.
|
||||
for (Unit* member : partyMembers)
|
||||
{
|
||||
if (!member || !member->IsAlive())
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return attacker;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Unit* SelectAssistTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
// 1. An active owner victim is an explicit target instruction.
|
||||
if (Unit* target = owner->GetVictim())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
|
||||
// During an already running fight, an explicitly selected living
|
||||
// hostile target may also be treated as an instruction. Merely
|
||||
// selecting an enemy out of combat still must not pull it.
|
||||
if (owner->IsInCombat())
|
||||
{
|
||||
if (Unit* target = owner->GetSelectedUnit())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
}
|
||||
|
||||
// 2. Keep the bot's own valid current victim.
|
||||
if (Unit* target = me->GetVictim())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
|
||||
// 3. Continue the group's fight even if the owner is a healer and
|
||||
// currently targets a friendly tank or another party member.
|
||||
if (Unit* target = SelectGroupCombatTarget(owner))
|
||||
return target;
|
||||
|
||||
// 4. Finally, defend the bot itself if something is attacking it.
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return attacker;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
HandleOutOfCombatRecovery(diff);
|
||||
if (diagnosticTimer > diff)
|
||||
diagnosticTimer -= diff;
|
||||
else
|
||||
diagnosticTimer = 0;
|
||||
|
||||
if (updateTimer > diff)
|
||||
{
|
||||
updateTimer -= diff;
|
||||
|
||||
// VIANO_TARGET_CONTROL_V1_4
|
||||
// Keep the explicitly assigned victim. UpdateVictim() may
|
||||
// select another creature from the threat list.
|
||||
if (me->GetVictim())
|
||||
HandleCombatActions(diff);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateTimer = UPDATE_INTERVAL;
|
||||
|
||||
Player* owner = GetOwnerPlayer();
|
||||
if (!owner || !owner->IsInWorld() || !me->IsInMap(owner))
|
||||
{
|
||||
TC_LOG_INFO("scripts", "[VianoAI] despawn: owner invalid or not on same map (ownerFound=%u)", owner ? 1u : 0u);
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
if (diagnosticTimer == 0)
|
||||
{
|
||||
diagnosticTimer = DIAGNOSTIC_INTERVAL;
|
||||
LogDiagnostics(owner);
|
||||
}
|
||||
|
||||
if (me->getLevel() != owner->getLevel())
|
||||
{
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
}
|
||||
|
||||
if (me->GetDistance(owner) > TELEPORT_DISTANCE && !IsActivelyEngagedInCombat())
|
||||
{
|
||||
// Keep the recovery teleport for geometry/jump problems, even
|
||||
// if the group is already fighting elsewhere. Only the bot's
|
||||
// own active engagement blocks teleporting.
|
||||
me->GetMotionMaster()->Clear();
|
||||
|
||||
me->NearTeleportTo(
|
||||
owner->GetPositionX() + 1.5f,
|
||||
owner->GetPositionY(),
|
||||
owner->GetPositionZ(),
|
||||
owner->GetOrientation()
|
||||
);
|
||||
|
||||
FollowOwner(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* desiredTarget = SelectAssistTarget(owner);
|
||||
|
||||
if (desiredTarget)
|
||||
{
|
||||
if (me->GetVictim() != desiredTarget)
|
||||
{
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] AttackStart requested: target='%s' entry=%u targetFaction=%u valid=%u meCombatBefore=%u",
|
||||
desiredTarget->GetName().c_str(), desiredTarget->GetEntry(), desiredTarget->GetFaction(),
|
||||
me->IsValidAttackTarget(desiredTarget) ? 1u : 0u, me->IsInCombat() ? 1u : 0u);
|
||||
|
||||
AttackStart(desiredTarget);
|
||||
|
||||
Unit* afterVictim = me->GetVictim();
|
||||
TC_LOG_INFO("scripts", "[VianoAI] AttackStart result: meCombatAfter=%u victim='%s'(%u)",
|
||||
me->IsInCombat() ? 1u : 0u, UnitName(afterVictim), UnitEntry(afterVictim));
|
||||
}
|
||||
}
|
||||
else if (!IsActivelyEngagedInCombat())
|
||||
{
|
||||
// A stale Core combat flag must not prevent recovery/following.
|
||||
// We deliberately do not clear the flag here; OOC regeneration
|
||||
// will be handled as a separate change.
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
// VIANO_TARGET_CONTROL_V1_4
|
||||
// Keep the explicitly assigned victim. UpdateVictim() may
|
||||
// select another creature from the threat list.
|
||||
if (me->GetVictim())
|
||||
HandleCombatActions(diff);
|
||||
}
|
||||
};
|
||||
|
||||
CreatureAI* GetAI(Creature* creature) const override
|
||||
{
|
||||
return new npc_horde_adventurer_meleeAI(creature);
|
||||
}
|
||||
};
|
||||
|
||||
// VIANO_TANK_V1
|
||||
class npc_horde_adventurer_tank : public CreatureScript
|
||||
{
|
||||
public:
|
||||
npc_horde_adventurer_tank() : CreatureScript("npc_horde_adventurer_tank") { }
|
||||
|
||||
struct npc_horde_adventurer_tankAI : public ScriptedAI
|
||||
{
|
||||
npc_horde_adventurer_tankAI(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
static constexpr uint8 SKULL_ICON_ID = 7;
|
||||
static constexpr float AUTO_PICKUP_RADIUS = 10.0f;
|
||||
static constexpr uint32 TANK_UPDATE_INTERVAL = 500;
|
||||
static constexpr uint32 AOE_DAMAGE_INTERVAL = 6000;
|
||||
|
||||
ObjectGuid ownerGuid;
|
||||
uint32 updateTimer = 0;
|
||||
uint32 recoveryTimer = 1000;
|
||||
uint32 aoeDamageTimer = 3000;
|
||||
|
||||
Player* GetOwnerPlayer() const
|
||||
{
|
||||
if (!ownerGuid)
|
||||
return nullptr;
|
||||
|
||||
return ObjectAccessor::GetPlayer(*me, ownerGuid);
|
||||
}
|
||||
|
||||
bool IsValidTankTarget(Unit* unit) const
|
||||
{
|
||||
if (!unit || !unit->IsAlive())
|
||||
return false;
|
||||
|
||||
// Dungeon-role bot: do not turn this into a PvP player hunter.
|
||||
if (!unit->ToCreature())
|
||||
return false;
|
||||
|
||||
return me->IsValidAttackTarget(unit);
|
||||
}
|
||||
|
||||
bool IsActivelyEngagedInCombat() const
|
||||
{
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (victim->IsAlive() && me->IsValidAttackTarget(victim))
|
||||
return true;
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (attacker && attacker->IsAlive() && me->IsValidAttackTarget(attacker))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FollowOwner(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE);
|
||||
}
|
||||
|
||||
void HandleOutOfCombatRecovery(uint32 diff)
|
||||
{
|
||||
if (!me->IsAlive() || IsActivelyEngagedInCombat())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (recoveryTimer > diff)
|
||||
{
|
||||
recoveryTimer -= diff;
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryTimer = 1000;
|
||||
|
||||
uint32 maxHealth = me->GetMaxHealth();
|
||||
if (me->GetHealth() < maxHealth)
|
||||
{
|
||||
uint32 gain = maxHealth / 10;
|
||||
if (!gain)
|
||||
gain = 1;
|
||||
|
||||
uint32 health = me->GetHealth() + gain;
|
||||
me->SetHealth(health > maxHealth ? maxHealth : health);
|
||||
}
|
||||
|
||||
uint32 maxMana = me->GetMaxPower(POWER_MANA);
|
||||
if (maxMana && me->GetPower(POWER_MANA) < maxMana)
|
||||
{
|
||||
uint32 gain = maxMana / 10;
|
||||
if (!gain)
|
||||
gain = 1;
|
||||
|
||||
uint32 mana = me->GetPower(POWER_MANA) + gain;
|
||||
me->SetPower(POWER_MANA, mana > maxMana ? maxMana : mana);
|
||||
}
|
||||
}
|
||||
|
||||
Unit* GetSkullTarget(Player* owner) const
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
Group* group = owner->GetGroup();
|
||||
if (!group)
|
||||
return nullptr;
|
||||
|
||||
ObjectGuid guid = group->GetTargetIcon(SKULL_ICON_ID);
|
||||
if (!guid)
|
||||
return nullptr;
|
||||
|
||||
Unit* target = ObjectAccessor::GetUnit(*me, guid);
|
||||
return IsValidTankTarget(target) ? target : nullptr;
|
||||
}
|
||||
|
||||
Unit* FindGroupAttacker(Player* owner) const
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
if (Group* group = owner->GetGroup())
|
||||
{
|
||||
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
|
||||
{
|
||||
Player* member = ref->GetSource();
|
||||
if (!member || !member->IsAlive() || !member->IsInMap(owner))
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
if (IsValidTankTarget(attacker))
|
||||
return attacker;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
if (IsValidTankTarget(attacker))
|
||||
return attacker;
|
||||
}
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidTankTarget(attacker))
|
||||
return attacker;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Unit* SelectPrimaryTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
// Skull is the only explicit permission for the tank to initiate
|
||||
// a pull. It also overrides the current primary target mid-fight.
|
||||
if (Unit* skull = GetSkullTarget(owner))
|
||||
return skull;
|
||||
|
||||
// Existing combat takes precedence over player selection.
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (IsValidTankTarget(victim))
|
||||
return victim;
|
||||
|
||||
// Pick up anything already attacking the group.
|
||||
if (Unit* attacker = FindGroupAttacker(owner))
|
||||
return attacker;
|
||||
|
||||
// The owner already attacking something is also a valid combat signal.
|
||||
if (Unit* victim = owner->GetVictim())
|
||||
if (IsValidTankTarget(victim))
|
||||
return victim;
|
||||
|
||||
// GetVictim() is not always populated for players.
|
||||
if (owner->IsInCombat())
|
||||
if (Unit* selected = owner->GetSelectedUnit())
|
||||
if (IsValidTankTarget(selected))
|
||||
return selected;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsSafeAutoPickup(Unit* enemy) const
|
||||
{
|
||||
return IsValidTankTarget(enemy)
|
||||
&& me->GetDistance(enemy) <= AUTO_PICKUP_RADIUS
|
||||
&& me->IsWithinLOSInMap(enemy);
|
||||
}
|
||||
|
||||
void ForceTankThreat(Unit* enemy)
|
||||
{
|
||||
if (!IsSafeAutoPickup(enemy))
|
||||
return;
|
||||
|
||||
ThreatManager& threat = enemy->GetThreatManager();
|
||||
|
||||
// Match the current highest threat, then exceed it by 35%.
|
||||
// This gives us taunt-like recovery without permanent Fixate.
|
||||
threat.MatchUnitThreatToHighestThreat(me);
|
||||
|
||||
float current = threat.GetThreat(me, true);
|
||||
float bonus = current * 0.35f;
|
||||
if (bonus < 1.0f)
|
||||
bonus = 1.0f;
|
||||
|
||||
threat.AddThreat(me, bonus, nullptr, true, true);
|
||||
}
|
||||
|
||||
void RecoverGroupThreat(Player* owner)
|
||||
{
|
||||
if (!owner || !IsActivelyEngagedInCombat())
|
||||
return;
|
||||
|
||||
// Strong single-target threat on the current primary victim.
|
||||
if (Unit* victim = me->GetVictim())
|
||||
ForceTankThreat(victim);
|
||||
|
||||
// AoE pickup: only enemies already attacking real group members,
|
||||
// and only inside the configured 10 yard + LOS safety envelope.
|
||||
if (Group* group = owner->GetGroup())
|
||||
{
|
||||
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
|
||||
{
|
||||
Player* member = ref->GetSource();
|
||||
if (!member || !member->IsAlive() || !member->IsInMap(owner))
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
ForceTankThreat(attacker);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
ForceTankThreat(attacker);
|
||||
}
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
ForceTankThreat(attacker);
|
||||
}
|
||||
|
||||
void DoTankAoeDamage(Player* owner)
|
||||
{
|
||||
if (!owner || !IsActivelyEngagedInCombat())
|
||||
return;
|
||||
|
||||
uint32 damage = uint32(me->getLevel()) * 2u;
|
||||
if (!damage)
|
||||
damage = 1;
|
||||
|
||||
auto hit = [this, damage](Unit* enemy)
|
||||
{
|
||||
if (!IsSafeAutoPickup(enemy))
|
||||
return;
|
||||
|
||||
Unit::DealDamage(
|
||||
me,
|
||||
enemy,
|
||||
damage,
|
||||
0,
|
||||
DIRECT_DAMAGE,
|
||||
SPELL_SCHOOL_MASK_NORMAL,
|
||||
nullptr,
|
||||
false);
|
||||
};
|
||||
|
||||
if (Group* group = owner->GetGroup())
|
||||
{
|
||||
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
|
||||
{
|
||||
Player* member = ref->GetSource();
|
||||
if (!member || !member->IsAlive() || !member->IsInMap(owner))
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
hit(attacker);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
hit(attacker);
|
||||
}
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
hit(attacker);
|
||||
}
|
||||
|
||||
void InitializeAI() override
|
||||
{
|
||||
ScriptedAI::InitializeAI();
|
||||
|
||||
TempSummon* summon = me->ToTempSummon();
|
||||
if (!summon)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
Player* owner = summoner ? summoner->ToPlayer() : nullptr;
|
||||
if (!owner)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
ownerGuid = owner->GetGUID();
|
||||
|
||||
me->SetOwnerGUID(ownerGuid);
|
||||
me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED);
|
||||
me->SetFaction(owner->GetFaction());
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
|
||||
static constexpr uint32 HORDE_ADVENTURER_MODELS[] =
|
||||
{
|
||||
30063,
|
||||
30064,
|
||||
30065,
|
||||
30066
|
||||
};
|
||||
|
||||
me->SetDisplayId(HORDE_ADVENTURER_MODELS[urand(0, 3)], true);
|
||||
me->SetReactState(REACT_DEFENSIVE);
|
||||
|
||||
FollowOwner(owner);
|
||||
|
||||
TC_LOG_INFO(
|
||||
"scripts",
|
||||
"[VianoTank] initialized entry=%u owner='%s' level=%u health=%u armor=%u",
|
||||
me->GetEntry(),
|
||||
owner->GetName().c_str(),
|
||||
uint32(me->getLevel()),
|
||||
me->GetMaxHealth(),
|
||||
me->GetArmor());
|
||||
}
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
updateTimer = 0;
|
||||
recoveryTimer = 1000;
|
||||
aoeDamageTimer = 3000;
|
||||
|
||||
if (Player* owner = GetOwnerPlayer())
|
||||
if (!IsActivelyEngagedInCombat())
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void MoveInLineOfSight(Unit* /*who*/) override
|
||||
{
|
||||
// No autonomous proximity pulls.
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
HandleOutOfCombatRecovery(diff);
|
||||
|
||||
if (aoeDamageTimer > diff)
|
||||
aoeDamageTimer -= diff;
|
||||
else
|
||||
aoeDamageTimer = 0;
|
||||
|
||||
if (updateTimer > diff)
|
||||
{
|
||||
updateTimer -= diff;
|
||||
|
||||
if (me->GetVictim())
|
||||
DoMeleeAttackIfReady();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateTimer = TANK_UPDATE_INTERVAL;
|
||||
|
||||
Player* owner = GetOwnerPlayer();
|
||||
if (!owner || !owner->IsInWorld() || !me->IsInMap(owner))
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
if (me->getLevel() != owner->getLevel())
|
||||
{
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
}
|
||||
|
||||
bool engaged = IsActivelyEngagedInCombat();
|
||||
|
||||
// Recovery teleport is strictly out-of-combat.
|
||||
if (!engaged && me->GetDistance(owner) > TELEPORT_DISTANCE)
|
||||
{
|
||||
me->GetMotionMaster()->Clear();
|
||||
|
||||
me->NearTeleportTo(
|
||||
owner->GetPositionX() + 1.5f,
|
||||
owner->GetPositionY(),
|
||||
owner->GetPositionZ(),
|
||||
owner->GetOrientation());
|
||||
|
||||
FollowOwner(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* desiredTarget = SelectPrimaryTarget(owner);
|
||||
|
||||
if (desiredTarget && me->GetVictim() != desiredTarget)
|
||||
AttackStart(desiredTarget);
|
||||
|
||||
engaged = IsActivelyEngagedInCombat();
|
||||
|
||||
if (engaged)
|
||||
{
|
||||
RecoverGroupThreat(owner);
|
||||
|
||||
if (aoeDamageTimer == 0)
|
||||
{
|
||||
aoeDamageTimer = AOE_DAMAGE_INTERVAL;
|
||||
DoTankAoeDamage(owner);
|
||||
}
|
||||
|
||||
if (me->GetVictim())
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
else
|
||||
FollowOwner(owner);
|
||||
}
|
||||
};
|
||||
|
||||
CreatureAI* GetAI(Creature* creature) const override
|
||||
{
|
||||
return new npc_horde_adventurer_tankAI(creature);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
void AddSC_viano_adventurers()
|
||||
{
|
||||
using namespace VianoAdventurers;
|
||||
new npc_horde_adventurer_melee();
|
||||
new npc_horde_adventurer_tank();
|
||||
}
|
||||
+964
@@ -0,0 +1,964 @@
|
||||
/*
|
||||
* Viano custom dungeon adventurers
|
||||
* Initial melee companion implementation.
|
||||
*/
|
||||
|
||||
#include "ScriptMgr.h"
|
||||
#include "ScriptedCreature.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Player.h"
|
||||
#include "Group.h"
|
||||
#include "TemporarySummon.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "Log.h"
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace VianoAdventurers
|
||||
{
|
||||
static constexpr float FOLLOW_DISTANCE = 2.5f;
|
||||
static constexpr float FOLLOW_ANGLE = 3.14159265f;
|
||||
static constexpr float TELEPORT_DISTANCE = 45.0f;
|
||||
static constexpr uint32 UPDATE_INTERVAL = 500;
|
||||
static constexpr uint32 DIAGNOSTIC_INTERVAL = 2000;
|
||||
|
||||
// VIANO_DD_PROFILE_V1_5
|
||||
enum AdventurerSpells : uint32
|
||||
{
|
||||
SPELL_ADVENTURER_MORTAL_STRIKE = 65926,
|
||||
SPELL_ADVENTURER_OVERPOWER = 65924,
|
||||
SPELL_ADVENTURER_BLADESTORM = 65947
|
||||
};
|
||||
|
||||
enum AdventurerEvents : uint32
|
||||
{
|
||||
EVENT_ADVENTURER_MORTAL_STRIKE = 1,
|
||||
EVENT_ADVENTURER_OVERPOWER,
|
||||
EVENT_ADVENTURER_BLADESTORM
|
||||
};
|
||||
|
||||
// VIANO_AI_DIAGNOSTICS_V1
|
||||
|
||||
class npc_horde_adventurer_melee : public CreatureScript
|
||||
{
|
||||
public:
|
||||
npc_horde_adventurer_melee() : CreatureScript("npc_horde_adventurer_melee") { }
|
||||
|
||||
struct npc_horde_adventurer_meleeAI : public ScriptedAI
|
||||
{
|
||||
npc_horde_adventurer_meleeAI(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
ObjectGuid ownerGuid;
|
||||
EventMap combatEvents;
|
||||
uint32 updateTimer = 0;
|
||||
uint32 diagnosticTimer = 0;
|
||||
uint32 recoveryTimer = 1000;
|
||||
Player* GetOwnerPlayer() const
|
||||
{
|
||||
if (!ownerGuid)
|
||||
return nullptr;
|
||||
|
||||
return ObjectAccessor::GetPlayer(*me, ownerGuid);
|
||||
}
|
||||
|
||||
void FollowOwner(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE);
|
||||
}
|
||||
|
||||
void ResetCombatEvents()
|
||||
{
|
||||
combatEvents.Reset();
|
||||
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE, 2 * IN_MILLISECONDS);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER, 5 * IN_MILLISECONDS);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM, 12 * IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
void HandleCombatActions(uint32 diff)
|
||||
{
|
||||
Unit* victim = me->GetVictim();
|
||||
if (!victim || !victim->IsAlive())
|
||||
return;
|
||||
|
||||
combatEvents.Update(diff);
|
||||
|
||||
if (me->HasUnitState(UNIT_STATE_CASTING))
|
||||
return;
|
||||
|
||||
if (uint32 eventId = combatEvents.ExecuteEvent())
|
||||
{
|
||||
switch (eventId)
|
||||
{
|
||||
case EVENT_ADVENTURER_MORTAL_STRIKE:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Mortal Strike: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_MORTAL_STRIKE);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE,
|
||||
urand(6 * IN_MILLISECONDS, 8 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
case EVENT_ADVENTURER_OVERPOWER:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Overpower: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_OVERPOWER);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER,
|
||||
urand(10 * IN_MILLISECONDS, 14 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
case EVENT_ADVENTURER_BLADESTORM:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Bladestorm: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_BLADESTORM);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM,
|
||||
urand(30 * IN_MILLISECONDS, 45 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
void HandleOutOfCombatRecovery(uint32 diff)
|
||||
{
|
||||
if (!me->IsAlive())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keine künstliche Regeneration während eines Kampfes.
|
||||
if (me->IsInCombat())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (recoveryTimer > diff)
|
||||
{
|
||||
recoveryTimer -= diff;
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryTimer = 1000;
|
||||
|
||||
// 10 % der maximalen Lebenspunkte pro Sekunde.
|
||||
uint32 maxHealth = me->GetMaxHealth();
|
||||
|
||||
if (me->GetHealth() < maxHealth)
|
||||
{
|
||||
uint32 healthGain = maxHealth / 10;
|
||||
|
||||
if (healthGain == 0)
|
||||
healthGain = 1;
|
||||
|
||||
uint32 newHealth = me->GetHealth() + healthGain;
|
||||
|
||||
if (newHealth > maxHealth)
|
||||
newHealth = maxHealth;
|
||||
|
||||
me->SetHealth(newHealth);
|
||||
}
|
||||
|
||||
// 10 % des maximalen Manas pro Sekunde.
|
||||
uint32 maxMana = me->GetMaxPower(POWER_MANA);
|
||||
|
||||
if (maxMana > 0 && me->GetPower(POWER_MANA) < maxMana)
|
||||
{
|
||||
uint32 manaGain = maxMana / 10;
|
||||
|
||||
if (manaGain == 0)
|
||||
manaGain = 1;
|
||||
|
||||
uint32 newMana = me->GetPower(POWER_MANA) + manaGain;
|
||||
|
||||
if (newMana > maxMana)
|
||||
newMana = maxMana;
|
||||
|
||||
me->SetPower(POWER_MANA, newMana);
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeAI() override
|
||||
{
|
||||
ScriptedAI::InitializeAI();
|
||||
|
||||
TempSummon* summon = me->ToTempSummon();
|
||||
if (!summon)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
Player* owner = summoner ? summoner->ToPlayer() : nullptr;
|
||||
if (!owner)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
ownerGuid = owner->GetGUID();
|
||||
|
||||
me->SetOwnerGUID(ownerGuid);
|
||||
me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED);
|
||||
me->SetFaction(owner->GetFaction());
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
|
||||
|
||||
static constexpr uint32 HORDE_ADVENTURER_MODELS[] =
|
||||
{
|
||||
30063,
|
||||
30064,
|
||||
30065,
|
||||
30066
|
||||
};
|
||||
uint32 displayId = HORDE_ADVENTURER_MODELS[urand(0, 3)];
|
||||
|
||||
me->SetDisplayId(displayId, true);
|
||||
|
||||
TC_LOG_INFO("scripts","[VianoAI] Horde adventurer display selected: %u", displayId);
|
||||
|
||||
// REACT_PASSIVE lets AttackStart() briefly set a victim, but the
|
||||
// core subsequently drops that victim again. We already suppress
|
||||
// autonomous proximity aggro in MoveInLineOfSight(), so DEFENSIVE
|
||||
// keeps an explicitly assigned assist target without making the
|
||||
// adventurer independently pull nearby creatures.
|
||||
me->SetReactState(REACT_DEFENSIVE);
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] initialized me='%s' entry=%u owner='%s' ownerGuid=%s meFaction=%u ownerFaction=%u level=%u",
|
||||
me->GetName().c_str(), me->GetEntry(), owner->GetName().c_str(), ownerGuid.ToString().c_str(),
|
||||
me->GetFaction(), owner->GetFaction(), uint32(me->getLevel()));
|
||||
|
||||
ResetCombatEvents();
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
ResetCombatEvents();
|
||||
|
||||
if (Player* owner = GetOwnerPlayer())
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void MoveInLineOfSight(Unit* /*who*/) override
|
||||
{
|
||||
}
|
||||
|
||||
char const* UnitName(Unit* unit) const
|
||||
{
|
||||
return unit ? unit->GetName().c_str() : "<none>";
|
||||
}
|
||||
|
||||
uint32 UnitEntry(Unit* unit) const
|
||||
{
|
||||
return unit ? unit->GetEntry() : 0;
|
||||
}
|
||||
|
||||
bool IsValidCandidate(Unit* unit) const
|
||||
{
|
||||
return unit && unit->IsAlive() && me->IsValidAttackTarget(unit);
|
||||
}
|
||||
|
||||
void LogDiagnostics(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
Unit* ownerVictim = owner->GetVictim();
|
||||
Unit* selected = owner->GetSelectedUnit();
|
||||
Unit* meVictim = me->GetVictim();
|
||||
|
||||
Unit* firstOwnerAttacker = nullptr;
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
{
|
||||
if (attacker)
|
||||
{
|
||||
firstOwnerAttacker = attacker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Unit* firstMeAttacker = nullptr;
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
{
|
||||
if (attacker)
|
||||
{
|
||||
firstMeAttacker = attacker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] me='%s' entry=%u lvl=%u faction=%u react=%u alive=%u combat=%u hp=%u/%u victim='%s'(%u) attackers=%u distance=%.2f",
|
||||
me->GetName().c_str(), me->GetEntry(), uint32(me->getLevel()), me->GetFaction(), uint32(me->GetReactState()),
|
||||
me->IsAlive() ? 1u : 0u, me->IsInCombat() ? 1u : 0u, me->GetHealth(), me->GetMaxHealth(),
|
||||
UnitName(meVictim), UnitEntry(meVictim), uint32(me->getAttackers().size()), me->GetDistance(owner));
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] owner='%s' lvl=%u faction=%u alive=%u combat=%u victim='%s'(%u) selected='%s'(%u) attackers=%u",
|
||||
owner->GetName().c_str(), uint32(owner->getLevel()), owner->GetFaction(), owner->IsAlive() ? 1u : 0u,
|
||||
owner->IsInCombat() ? 1u : 0u, UnitName(ownerVictim), UnitEntry(ownerVictim), UnitName(selected), UnitEntry(selected),
|
||||
uint32(owner->getAttackers().size()));
|
||||
|
||||
if (ownerVictim)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] ownerVictim='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
ownerVictim->GetName().c_str(), ownerVictim->GetEntry(), ownerVictim->GetFaction(), IsValidCandidate(ownerVictim) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(ownerVictim) ? 1u : 0u, owner->IsValidAttackTarget(ownerVictim) ? 1u : 0u,
|
||||
UnitName(ownerVictim->GetVictim()), UnitEntry(ownerVictim->GetVictim()));
|
||||
|
||||
if (selected)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] selected='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
selected->GetName().c_str(), selected->GetEntry(), selected->GetFaction(), IsValidCandidate(selected) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(selected) ? 1u : 0u, owner->IsValidAttackTarget(selected) ? 1u : 0u,
|
||||
UnitName(selected->GetVictim()), UnitEntry(selected->GetVictim()));
|
||||
|
||||
if (firstOwnerAttacker)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] firstOwnerAttacker='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
firstOwnerAttacker->GetName().c_str(), firstOwnerAttacker->GetEntry(), firstOwnerAttacker->GetFaction(), IsValidCandidate(firstOwnerAttacker) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u, owner->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u,
|
||||
UnitName(firstOwnerAttacker->GetVictim()), UnitEntry(firstOwnerAttacker->GetVictim()));
|
||||
|
||||
if (firstMeAttacker)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] firstMeAttacker='%s' entry=%u faction=%u victim='%s'(%u)",
|
||||
firstMeAttacker->GetName().c_str(), firstMeAttacker->GetEntry(), firstMeAttacker->GetFaction(),
|
||||
UnitName(firstMeAttacker->GetVictim()), UnitEntry(firstMeAttacker->GetVictim()));
|
||||
}
|
||||
|
||||
bool IsActivelyEngagedInCombat() const
|
||||
{
|
||||
// IsInCombat() alone is intentionally not enough here: the Core
|
||||
// may keep the combat flag set after the bot has no live opponent.
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (IsValidCandidate(victim))
|
||||
return true;
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Unit* SelectGroupCombatTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
std::list<Unit*> partyMembers;
|
||||
owner->GetPartyMembers(partyMembers);
|
||||
|
||||
// In Vcore GetPartyMembers() returns living members of the same
|
||||
// party/subgroup on the same map, plus their living guardian pets.
|
||||
// An enemy attacking one of them is therefore already part of the
|
||||
// group's current fight.
|
||||
for (Unit* member : partyMembers)
|
||||
{
|
||||
if (!member || !member->IsAlive())
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return attacker;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Unit* SelectAssistTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
// 1. An active owner victim is an explicit target instruction.
|
||||
if (Unit* target = owner->GetVictim())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
|
||||
// During an already running fight, an explicitly selected living
|
||||
// hostile target may also be treated as an instruction. Merely
|
||||
// selecting an enemy out of combat still must not pull it.
|
||||
if (owner->IsInCombat())
|
||||
{
|
||||
if (Unit* target = owner->GetSelectedUnit())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
}
|
||||
|
||||
// 2. Keep the bot's own valid current victim.
|
||||
if (Unit* target = me->GetVictim())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
|
||||
// 3. Continue the group's fight even if the owner is a healer and
|
||||
// currently targets a friendly tank or another party member.
|
||||
if (Unit* target = SelectGroupCombatTarget(owner))
|
||||
return target;
|
||||
|
||||
// 4. Finally, defend the bot itself if something is attacking it.
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return attacker;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
HandleOutOfCombatRecovery(diff);
|
||||
if (diagnosticTimer > diff)
|
||||
diagnosticTimer -= diff;
|
||||
else
|
||||
diagnosticTimer = 0;
|
||||
|
||||
if (updateTimer > diff)
|
||||
{
|
||||
updateTimer -= diff;
|
||||
|
||||
// VIANO_TARGET_CONTROL_V1_4
|
||||
// Keep the explicitly assigned victim. UpdateVictim() may
|
||||
// select another creature from the threat list.
|
||||
if (me->GetVictim())
|
||||
HandleCombatActions(diff);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateTimer = UPDATE_INTERVAL;
|
||||
|
||||
Player* owner = GetOwnerPlayer();
|
||||
if (!owner || !owner->IsInWorld() || !me->IsInMap(owner))
|
||||
{
|
||||
TC_LOG_INFO("scripts", "[VianoAI] despawn: owner invalid or not on same map (ownerFound=%u)", owner ? 1u : 0u);
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
if (diagnosticTimer == 0)
|
||||
{
|
||||
diagnosticTimer = DIAGNOSTIC_INTERVAL;
|
||||
LogDiagnostics(owner);
|
||||
}
|
||||
|
||||
if (me->getLevel() != owner->getLevel())
|
||||
{
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
}
|
||||
|
||||
if (me->GetDistance(owner) > TELEPORT_DISTANCE && !IsActivelyEngagedInCombat())
|
||||
{
|
||||
// Keep the recovery teleport for geometry/jump problems, even
|
||||
// if the group is already fighting elsewhere. Only the bot's
|
||||
// own active engagement blocks teleporting.
|
||||
me->GetMotionMaster()->Clear();
|
||||
|
||||
me->NearTeleportTo(
|
||||
owner->GetPositionX() + 1.5f,
|
||||
owner->GetPositionY(),
|
||||
owner->GetPositionZ(),
|
||||
owner->GetOrientation()
|
||||
);
|
||||
|
||||
FollowOwner(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* desiredTarget = SelectAssistTarget(owner);
|
||||
|
||||
if (desiredTarget)
|
||||
{
|
||||
if (me->GetVictim() != desiredTarget)
|
||||
{
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] AttackStart requested: target='%s' entry=%u targetFaction=%u valid=%u meCombatBefore=%u",
|
||||
desiredTarget->GetName().c_str(), desiredTarget->GetEntry(), desiredTarget->GetFaction(),
|
||||
me->IsValidAttackTarget(desiredTarget) ? 1u : 0u, me->IsInCombat() ? 1u : 0u);
|
||||
|
||||
AttackStart(desiredTarget);
|
||||
|
||||
Unit* afterVictim = me->GetVictim();
|
||||
TC_LOG_INFO("scripts", "[VianoAI] AttackStart result: meCombatAfter=%u victim='%s'(%u)",
|
||||
me->IsInCombat() ? 1u : 0u, UnitName(afterVictim), UnitEntry(afterVictim));
|
||||
}
|
||||
}
|
||||
else if (!IsActivelyEngagedInCombat())
|
||||
{
|
||||
// A stale Core combat flag must not prevent recovery/following.
|
||||
// We deliberately do not clear the flag here; OOC regeneration
|
||||
// will be handled as a separate change.
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
// VIANO_TARGET_CONTROL_V1_4
|
||||
// Keep the explicitly assigned victim. UpdateVictim() may
|
||||
// select another creature from the threat list.
|
||||
if (me->GetVictim())
|
||||
HandleCombatActions(diff);
|
||||
}
|
||||
};
|
||||
|
||||
CreatureAI* GetAI(Creature* creature) const override
|
||||
{
|
||||
return new npc_horde_adventurer_meleeAI(creature);
|
||||
}
|
||||
};
|
||||
|
||||
// VIANO_TANK_V1
|
||||
class npc_horde_adventurer_tank : public CreatureScript
|
||||
{
|
||||
public:
|
||||
npc_horde_adventurer_tank() : CreatureScript("npc_horde_adventurer_tank") { }
|
||||
|
||||
struct npc_horde_adventurer_tankAI : public ScriptedAI
|
||||
{
|
||||
npc_horde_adventurer_tankAI(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
static constexpr uint8 SKULL_ICON_ID = 7;
|
||||
static constexpr float AUTO_PICKUP_RADIUS = 10.0f;
|
||||
static constexpr uint32 TANK_UPDATE_INTERVAL = 500;
|
||||
static constexpr uint32 AOE_DAMAGE_INTERVAL = 6000;
|
||||
|
||||
ObjectGuid ownerGuid;
|
||||
uint32 updateTimer = 0;
|
||||
uint32 recoveryTimer = 1000;
|
||||
uint32 aoeDamageTimer = 3000;
|
||||
|
||||
Player* GetOwnerPlayer() const
|
||||
{
|
||||
if (!ownerGuid)
|
||||
return nullptr;
|
||||
|
||||
return ObjectAccessor::GetPlayer(*me, ownerGuid);
|
||||
}
|
||||
|
||||
bool IsValidTankTarget(Unit* unit) const
|
||||
{
|
||||
if (!unit || !unit->IsAlive())
|
||||
return false;
|
||||
|
||||
// Dungeon-role bot: do not turn this into a PvP player hunter.
|
||||
if (!unit->ToCreature())
|
||||
return false;
|
||||
|
||||
return me->IsValidAttackTarget(unit);
|
||||
}
|
||||
|
||||
bool IsActivelyEngagedInCombat() const
|
||||
{
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (victim->IsAlive() && me->IsValidAttackTarget(victim))
|
||||
return true;
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (attacker && attacker->IsAlive() && me->IsValidAttackTarget(attacker))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FollowOwner(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE);
|
||||
}
|
||||
|
||||
void HandleOutOfCombatRecovery(uint32 diff)
|
||||
{
|
||||
if (!me->IsAlive() || IsActivelyEngagedInCombat())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (recoveryTimer > diff)
|
||||
{
|
||||
recoveryTimer -= diff;
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryTimer = 1000;
|
||||
|
||||
uint32 maxHealth = me->GetMaxHealth();
|
||||
if (me->GetHealth() < maxHealth)
|
||||
{
|
||||
uint32 gain = maxHealth / 10;
|
||||
if (!gain)
|
||||
gain = 1;
|
||||
|
||||
uint32 health = me->GetHealth() + gain;
|
||||
me->SetHealth(health > maxHealth ? maxHealth : health);
|
||||
}
|
||||
|
||||
uint32 maxMana = me->GetMaxPower(POWER_MANA);
|
||||
if (maxMana && me->GetPower(POWER_MANA) < maxMana)
|
||||
{
|
||||
uint32 gain = maxMana / 10;
|
||||
if (!gain)
|
||||
gain = 1;
|
||||
|
||||
uint32 mana = me->GetPower(POWER_MANA) + gain;
|
||||
me->SetPower(POWER_MANA, mana > maxMana ? maxMana : mana);
|
||||
}
|
||||
}
|
||||
|
||||
Unit* GetSkullTarget(Player* owner) const
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
Group* group = owner->GetGroup();
|
||||
if (!group)
|
||||
return nullptr;
|
||||
|
||||
ObjectGuid guid = group->GetTargetIcon(SKULL_ICON_ID);
|
||||
if (!guid)
|
||||
return nullptr;
|
||||
|
||||
Unit* target = ObjectAccessor::GetUnit(*me, guid);
|
||||
return IsValidTankTarget(target) ? target : nullptr;
|
||||
}
|
||||
|
||||
Unit* FindGroupAttacker(Player* owner) const
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
if (Group* group = owner->GetGroup())
|
||||
{
|
||||
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
|
||||
{
|
||||
Player* member = ref->GetSource();
|
||||
if (!member || !member->IsAlive() || !member->IsInMap(owner))
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
if (IsValidTankTarget(attacker))
|
||||
return attacker;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
if (IsValidTankTarget(attacker))
|
||||
return attacker;
|
||||
}
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidTankTarget(attacker))
|
||||
return attacker;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Unit* SelectPrimaryTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
// Skull is the only explicit permission for the tank to initiate
|
||||
// a pull. It also overrides the current primary target mid-fight.
|
||||
if (Unit* skull = GetSkullTarget(owner))
|
||||
return skull;
|
||||
|
||||
// Existing combat takes precedence over player selection.
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (IsValidTankTarget(victim))
|
||||
return victim;
|
||||
|
||||
// Pick up anything already attacking the group.
|
||||
if (Unit* attacker = FindGroupAttacker(owner))
|
||||
return attacker;
|
||||
|
||||
// The owner already attacking something is also a valid combat signal.
|
||||
if (Unit* victim = owner->GetVictim())
|
||||
if (IsValidTankTarget(victim))
|
||||
return victim;
|
||||
|
||||
// GetVictim() is not always populated for players.
|
||||
if (owner->IsInCombat())
|
||||
if (Unit* selected = owner->GetSelectedUnit())
|
||||
if (IsValidTankTarget(selected))
|
||||
return selected;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsSafeAutoPickup(Unit* enemy) const
|
||||
{
|
||||
return IsValidTankTarget(enemy)
|
||||
&& me->GetDistance(enemy) <= AUTO_PICKUP_RADIUS
|
||||
&& me->IsWithinLOSInMap(enemy);
|
||||
}
|
||||
|
||||
void ForceTankThreat(Unit* enemy)
|
||||
{
|
||||
if (!IsSafeAutoPickup(enemy))
|
||||
return;
|
||||
|
||||
ThreatManager& threat = enemy->GetThreatManager();
|
||||
|
||||
// Match the current highest threat, then exceed it by 35%.
|
||||
// This gives us taunt-like recovery without permanent Fixate.
|
||||
threat.MatchUnitThreatToHighestThreat(me);
|
||||
|
||||
float current = threat.GetThreat(me, true);
|
||||
float bonus = current * 0.35f;
|
||||
if (bonus < 1.0f)
|
||||
bonus = 1.0f;
|
||||
|
||||
threat.AddThreat(me, bonus, nullptr, true, true);
|
||||
}
|
||||
|
||||
void CollectRelevantEnemyGuids(Player* owner, std::vector<ObjectGuid>& targets) const
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
auto addTarget = [this, &targets](Unit* enemy)
|
||||
{
|
||||
if (!IsSafeAutoPickup(enemy))
|
||||
return;
|
||||
|
||||
ObjectGuid const guid = enemy->GetGUID();
|
||||
|
||||
if (std::find(targets.begin(), targets.end(), guid) == targets.end())
|
||||
targets.push_back(guid);
|
||||
};
|
||||
|
||||
if (Group* group = owner->GetGroup())
|
||||
{
|
||||
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
|
||||
{
|
||||
Player* member = ref->GetSource();
|
||||
if (!member || !member->IsAlive() || !member->IsInMap(owner))
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
addTarget(attacker);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
addTarget(attacker);
|
||||
}
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
addTarget(attacker);
|
||||
}
|
||||
|
||||
void RecoverGroupThreat(Player* owner)
|
||||
{
|
||||
if (!owner || !IsActivelyEngagedInCombat())
|
||||
return;
|
||||
|
||||
if (Unit* victim = me->GetVictim())
|
||||
ForceTankThreat(victim);
|
||||
|
||||
std::vector<ObjectGuid> targets;
|
||||
CollectRelevantEnemyGuids(owner, targets);
|
||||
|
||||
for (ObjectGuid const& guid : targets)
|
||||
{
|
||||
if (Unit* enemy = ObjectAccessor::GetUnit(*me, guid))
|
||||
ForceTankThreat(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
void DoTankAoeDamage(Player* owner)
|
||||
{
|
||||
if (!owner || !IsActivelyEngagedInCombat())
|
||||
return;
|
||||
|
||||
uint32 damage = uint32(me->getLevel()) * 2u;
|
||||
if (!damage)
|
||||
damage = 1;
|
||||
|
||||
std::vector<ObjectGuid> targets;
|
||||
CollectRelevantEnemyGuids(owner, targets);
|
||||
|
||||
for (ObjectGuid const& guid : targets)
|
||||
{
|
||||
Unit* enemy = ObjectAccessor::GetUnit(*me, guid);
|
||||
if (!IsSafeAutoPickup(enemy))
|
||||
continue;
|
||||
|
||||
Unit::DealDamage(
|
||||
me,
|
||||
enemy,
|
||||
damage,
|
||||
0,
|
||||
DIRECT_DAMAGE,
|
||||
SPELL_SCHOOL_MASK_NORMAL,
|
||||
nullptr,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeAI() override
|
||||
{
|
||||
ScriptedAI::InitializeAI();
|
||||
|
||||
TempSummon* summon = me->ToTempSummon();
|
||||
if (!summon)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
Player* owner = summoner ? summoner->ToPlayer() : nullptr;
|
||||
if (!owner)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
ownerGuid = owner->GetGUID();
|
||||
|
||||
me->SetOwnerGUID(ownerGuid);
|
||||
me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED);
|
||||
me->SetFaction(owner->GetFaction());
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
|
||||
static constexpr uint32 HORDE_ADVENTURER_MODELS[] =
|
||||
{
|
||||
30063,
|
||||
30064,
|
||||
30065,
|
||||
30066
|
||||
};
|
||||
|
||||
me->SetDisplayId(HORDE_ADVENTURER_MODELS[urand(0, 3)], true);
|
||||
me->SetReactState(REACT_DEFENSIVE);
|
||||
|
||||
FollowOwner(owner);
|
||||
|
||||
TC_LOG_INFO(
|
||||
"scripts",
|
||||
"[VianoTank] initialized entry=%u owner='%s' level=%u health=%u armor=%u",
|
||||
me->GetEntry(),
|
||||
owner->GetName().c_str(),
|
||||
uint32(me->getLevel()),
|
||||
me->GetMaxHealth(),
|
||||
me->GetArmor());
|
||||
}
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
updateTimer = 0;
|
||||
recoveryTimer = 1000;
|
||||
aoeDamageTimer = 3000;
|
||||
|
||||
if (Player* owner = GetOwnerPlayer())
|
||||
if (!IsActivelyEngagedInCombat())
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void MoveInLineOfSight(Unit* /*who*/) override
|
||||
{
|
||||
// No autonomous proximity pulls.
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
HandleOutOfCombatRecovery(diff);
|
||||
|
||||
if (aoeDamageTimer > diff)
|
||||
aoeDamageTimer -= diff;
|
||||
else
|
||||
aoeDamageTimer = 0;
|
||||
|
||||
if (updateTimer > diff)
|
||||
{
|
||||
updateTimer -= diff;
|
||||
|
||||
if (me->GetVictim())
|
||||
DoMeleeAttackIfReady();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateTimer = TANK_UPDATE_INTERVAL;
|
||||
|
||||
Player* owner = GetOwnerPlayer();
|
||||
if (!owner || !owner->IsInWorld() || !me->IsInMap(owner))
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
if (me->getLevel() != owner->getLevel())
|
||||
{
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
}
|
||||
|
||||
bool engaged = IsActivelyEngagedInCombat();
|
||||
|
||||
// Recovery teleport is strictly out-of-combat.
|
||||
if (!engaged && me->GetDistance(owner) > TELEPORT_DISTANCE)
|
||||
{
|
||||
me->GetMotionMaster()->Clear();
|
||||
|
||||
me->NearTeleportTo(
|
||||
owner->GetPositionX() + 1.5f,
|
||||
owner->GetPositionY(),
|
||||
owner->GetPositionZ(),
|
||||
owner->GetOrientation());
|
||||
|
||||
FollowOwner(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* desiredTarget = SelectPrimaryTarget(owner);
|
||||
|
||||
if (desiredTarget && me->GetVictim() != desiredTarget)
|
||||
AttackStart(desiredTarget);
|
||||
|
||||
engaged = IsActivelyEngagedInCombat();
|
||||
|
||||
if (engaged)
|
||||
{
|
||||
RecoverGroupThreat(owner);
|
||||
|
||||
if (aoeDamageTimer == 0)
|
||||
{
|
||||
aoeDamageTimer = AOE_DAMAGE_INTERVAL;
|
||||
DoTankAoeDamage(owner);
|
||||
}
|
||||
|
||||
if (me->GetVictim())
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
else
|
||||
FollowOwner(owner);
|
||||
}
|
||||
};
|
||||
|
||||
CreatureAI* GetAI(Creature* creature) const override
|
||||
{
|
||||
return new npc_horde_adventurer_tankAI(creature);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
void AddSC_viano_adventurers()
|
||||
{
|
||||
using namespace VianoAdventurers;
|
||||
new npc_horde_adventurer_melee();
|
||||
new npc_horde_adventurer_tank();
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
/*
|
||||
* Viano custom dungeon adventurers
|
||||
* Initial melee companion implementation.
|
||||
*/
|
||||
|
||||
#include "ScriptMgr.h"
|
||||
#include "ScriptedCreature.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Player.h"
|
||||
#include "TemporarySummon.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "Log.h"
|
||||
|
||||
namespace VianoAdventurers
|
||||
{
|
||||
static constexpr float FOLLOW_DISTANCE = 2.5f;
|
||||
static constexpr float FOLLOW_ANGLE = 3.14159265f;
|
||||
static constexpr float TELEPORT_DISTANCE = 45.0f;
|
||||
static constexpr uint32 UPDATE_INTERVAL = 500;
|
||||
static constexpr uint32 DIAGNOSTIC_INTERVAL = 2000;
|
||||
|
||||
// VIANO_DD_PROFILE_V1_5
|
||||
enum AdventurerSpells : uint32
|
||||
{
|
||||
SPELL_ADVENTURER_MORTAL_STRIKE = 65926,
|
||||
SPELL_ADVENTURER_OVERPOWER = 65924,
|
||||
SPELL_ADVENTURER_BLADESTORM = 65947
|
||||
};
|
||||
|
||||
enum AdventurerEvents : uint32
|
||||
{
|
||||
EVENT_ADVENTURER_MORTAL_STRIKE = 1,
|
||||
EVENT_ADVENTURER_OVERPOWER,
|
||||
EVENT_ADVENTURER_BLADESTORM
|
||||
};
|
||||
|
||||
// VIANO_AI_DIAGNOSTICS_V1
|
||||
|
||||
class npc_horde_adventurer_melee : public CreatureScript
|
||||
{
|
||||
public:
|
||||
npc_horde_adventurer_melee() : CreatureScript("npc_horde_adventurer_melee") { }
|
||||
|
||||
struct npc_horde_adventurer_meleeAI : public ScriptedAI
|
||||
{
|
||||
npc_horde_adventurer_meleeAI(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
ObjectGuid ownerGuid;
|
||||
EventMap combatEvents;
|
||||
uint32 updateTimer = 0;
|
||||
uint32 diagnosticTimer = 0;
|
||||
uint32 recoveryTimer = 1000;
|
||||
Player* GetOwnerPlayer() const
|
||||
{
|
||||
if (!ownerGuid)
|
||||
return nullptr;
|
||||
|
||||
return ObjectAccessor::GetPlayer(*me, ownerGuid);
|
||||
}
|
||||
|
||||
void FollowOwner(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE);
|
||||
}
|
||||
|
||||
void ResetCombatEvents()
|
||||
{
|
||||
combatEvents.Reset();
|
||||
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE, 2 * IN_MILLISECONDS);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER, 5 * IN_MILLISECONDS);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM, 12 * IN_MILLISECONDS);
|
||||
}
|
||||
|
||||
void HandleCombatActions(uint32 diff)
|
||||
{
|
||||
Unit* victim = me->GetVictim();
|
||||
if (!victim || !victim->IsAlive())
|
||||
return;
|
||||
|
||||
combatEvents.Update(diff);
|
||||
|
||||
if (me->HasUnitState(UNIT_STATE_CASTING))
|
||||
return;
|
||||
|
||||
if (uint32 eventId = combatEvents.ExecuteEvent())
|
||||
{
|
||||
switch (eventId)
|
||||
{
|
||||
case EVENT_ADVENTURER_MORTAL_STRIKE:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Mortal Strike: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_MORTAL_STRIKE);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE,
|
||||
urand(6 * IN_MILLISECONDS, 8 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
case EVENT_ADVENTURER_OVERPOWER:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Overpower: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_OVERPOWER);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER,
|
||||
urand(10 * IN_MILLISECONDS, 14 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
case EVENT_ADVENTURER_BLADESTORM:
|
||||
TC_LOG_INFO("scripts", "[VianoAI] cast Bladestorm: target='%s'(%u)",
|
||||
UnitName(victim), UnitEntry(victim));
|
||||
DoCastVictim(SPELL_ADVENTURER_BLADESTORM);
|
||||
combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM,
|
||||
urand(30 * IN_MILLISECONDS, 45 * IN_MILLISECONDS));
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
void HandleOutOfCombatRecovery(uint32 diff)
|
||||
{
|
||||
if (!me->IsAlive())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keine künstliche Regeneration während eines Kampfes.
|
||||
if (me->IsInCombat())
|
||||
{
|
||||
recoveryTimer = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (recoveryTimer > diff)
|
||||
{
|
||||
recoveryTimer -= diff;
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryTimer = 1000;
|
||||
|
||||
// 10 % der maximalen Lebenspunkte pro Sekunde.
|
||||
uint32 maxHealth = me->GetMaxHealth();
|
||||
|
||||
if (me->GetHealth() < maxHealth)
|
||||
{
|
||||
uint32 healthGain = maxHealth / 10;
|
||||
|
||||
if (healthGain == 0)
|
||||
healthGain = 1;
|
||||
|
||||
uint32 newHealth = me->GetHealth() + healthGain;
|
||||
|
||||
if (newHealth > maxHealth)
|
||||
newHealth = maxHealth;
|
||||
|
||||
me->SetHealth(newHealth);
|
||||
}
|
||||
|
||||
// 10 % des maximalen Manas pro Sekunde.
|
||||
uint32 maxMana = me->GetMaxPower(POWER_MANA);
|
||||
|
||||
if (maxMana > 0 && me->GetPower(POWER_MANA) < maxMana)
|
||||
{
|
||||
uint32 manaGain = maxMana / 10;
|
||||
|
||||
if (manaGain == 0)
|
||||
manaGain = 1;
|
||||
|
||||
uint32 newMana = me->GetPower(POWER_MANA) + manaGain;
|
||||
|
||||
if (newMana > maxMana)
|
||||
newMana = maxMana;
|
||||
|
||||
me->SetPower(POWER_MANA, newMana);
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeAI() override
|
||||
{
|
||||
ScriptedAI::InitializeAI();
|
||||
|
||||
TempSummon* summon = me->ToTempSummon();
|
||||
if (!summon)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
Player* owner = summoner ? summoner->ToPlayer() : nullptr;
|
||||
if (!owner)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
ownerGuid = owner->GetGUID();
|
||||
|
||||
me->SetOwnerGUID(ownerGuid);
|
||||
me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED);
|
||||
me->SetFaction(owner->GetFaction());
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
|
||||
|
||||
static constexpr uint32 HORDE_ADVENTURER_MODELS[] =
|
||||
{
|
||||
30063,
|
||||
30064,
|
||||
30065,
|
||||
30066
|
||||
};
|
||||
uint32 displayId = HORDE_ADVENTURER_MODELS[urand(0, 3)];
|
||||
|
||||
me->SetDisplayId(displayId, true);
|
||||
|
||||
TC_LOG_INFO("scripts","[VianoAI] Horde adventurer display selected: %u", displayId);
|
||||
|
||||
// REACT_PASSIVE lets AttackStart() briefly set a victim, but the
|
||||
// core subsequently drops that victim again. We already suppress
|
||||
// autonomous proximity aggro in MoveInLineOfSight(), so DEFENSIVE
|
||||
// keeps an explicitly assigned assist target without making the
|
||||
// adventurer independently pull nearby creatures.
|
||||
me->SetReactState(REACT_DEFENSIVE);
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] initialized me='%s' entry=%u owner='%s' ownerGuid=%s meFaction=%u ownerFaction=%u level=%u",
|
||||
me->GetName().c_str(), me->GetEntry(), owner->GetName().c_str(), ownerGuid.ToString().c_str(),
|
||||
me->GetFaction(), owner->GetFaction(), uint32(me->getLevel()));
|
||||
|
||||
ResetCombatEvents();
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
ResetCombatEvents();
|
||||
|
||||
if (Player* owner = GetOwnerPlayer())
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
void MoveInLineOfSight(Unit* /*who*/) override
|
||||
{
|
||||
}
|
||||
|
||||
char const* UnitName(Unit* unit) const
|
||||
{
|
||||
return unit ? unit->GetName().c_str() : "<none>";
|
||||
}
|
||||
|
||||
uint32 UnitEntry(Unit* unit) const
|
||||
{
|
||||
return unit ? unit->GetEntry() : 0;
|
||||
}
|
||||
|
||||
bool IsValidCandidate(Unit* unit) const
|
||||
{
|
||||
return unit && unit->IsAlive() && me->IsValidAttackTarget(unit);
|
||||
}
|
||||
|
||||
void LogDiagnostics(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return;
|
||||
|
||||
Unit* ownerVictim = owner->GetVictim();
|
||||
Unit* selected = owner->GetSelectedUnit();
|
||||
Unit* meVictim = me->GetVictim();
|
||||
|
||||
Unit* firstOwnerAttacker = nullptr;
|
||||
for (Unit* attacker : owner->getAttackers())
|
||||
{
|
||||
if (attacker)
|
||||
{
|
||||
firstOwnerAttacker = attacker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Unit* firstMeAttacker = nullptr;
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
{
|
||||
if (attacker)
|
||||
{
|
||||
firstMeAttacker = attacker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] me='%s' entry=%u lvl=%u faction=%u react=%u alive=%u combat=%u hp=%u/%u victim='%s'(%u) attackers=%u distance=%.2f",
|
||||
me->GetName().c_str(), me->GetEntry(), uint32(me->getLevel()), me->GetFaction(), uint32(me->GetReactState()),
|
||||
me->IsAlive() ? 1u : 0u, me->IsInCombat() ? 1u : 0u, me->GetHealth(), me->GetMaxHealth(),
|
||||
UnitName(meVictim), UnitEntry(meVictim), uint32(me->getAttackers().size()), me->GetDistance(owner));
|
||||
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] owner='%s' lvl=%u faction=%u alive=%u combat=%u victim='%s'(%u) selected='%s'(%u) attackers=%u",
|
||||
owner->GetName().c_str(), uint32(owner->getLevel()), owner->GetFaction(), owner->IsAlive() ? 1u : 0u,
|
||||
owner->IsInCombat() ? 1u : 0u, UnitName(ownerVictim), UnitEntry(ownerVictim), UnitName(selected), UnitEntry(selected),
|
||||
uint32(owner->getAttackers().size()));
|
||||
|
||||
if (ownerVictim)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] ownerVictim='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
ownerVictim->GetName().c_str(), ownerVictim->GetEntry(), ownerVictim->GetFaction(), IsValidCandidate(ownerVictim) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(ownerVictim) ? 1u : 0u, owner->IsValidAttackTarget(ownerVictim) ? 1u : 0u,
|
||||
UnitName(ownerVictim->GetVictim()), UnitEntry(ownerVictim->GetVictim()));
|
||||
|
||||
if (selected)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] selected='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
selected->GetName().c_str(), selected->GetEntry(), selected->GetFaction(), IsValidCandidate(selected) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(selected) ? 1u : 0u, owner->IsValidAttackTarget(selected) ? 1u : 0u,
|
||||
UnitName(selected->GetVictim()), UnitEntry(selected->GetVictim()));
|
||||
|
||||
if (firstOwnerAttacker)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] firstOwnerAttacker='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
|
||||
firstOwnerAttacker->GetName().c_str(), firstOwnerAttacker->GetEntry(), firstOwnerAttacker->GetFaction(), IsValidCandidate(firstOwnerAttacker) ? 1u : 0u,
|
||||
me->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u, owner->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u,
|
||||
UnitName(firstOwnerAttacker->GetVictim()), UnitEntry(firstOwnerAttacker->GetVictim()));
|
||||
|
||||
if (firstMeAttacker)
|
||||
TC_LOG_INFO("scripts", "[VianoAI] firstMeAttacker='%s' entry=%u faction=%u victim='%s'(%u)",
|
||||
firstMeAttacker->GetName().c_str(), firstMeAttacker->GetEntry(), firstMeAttacker->GetFaction(),
|
||||
UnitName(firstMeAttacker->GetVictim()), UnitEntry(firstMeAttacker->GetVictim()));
|
||||
}
|
||||
|
||||
bool IsActivelyEngagedInCombat() const
|
||||
{
|
||||
// IsInCombat() alone is intentionally not enough here: the Core
|
||||
// may keep the combat flag set after the bot has no live opponent.
|
||||
if (Unit* victim = me->GetVictim())
|
||||
if (IsValidCandidate(victim))
|
||||
return true;
|
||||
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Unit* SelectGroupCombatTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
std::list<Unit*> partyMembers;
|
||||
owner->GetPartyMembers(partyMembers);
|
||||
|
||||
// In Vcore GetPartyMembers() returns living members of the same
|
||||
// party/subgroup on the same map, plus their living guardian pets.
|
||||
// An enemy attacking one of them is therefore already part of the
|
||||
// group's current fight.
|
||||
for (Unit* member : partyMembers)
|
||||
{
|
||||
if (!member || !member->IsAlive())
|
||||
continue;
|
||||
|
||||
for (Unit* attacker : member->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return attacker;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Unit* SelectAssistTarget(Player* owner)
|
||||
{
|
||||
if (!owner)
|
||||
return nullptr;
|
||||
|
||||
// 1. An active owner victim is an explicit target instruction.
|
||||
if (Unit* target = owner->GetVictim())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
|
||||
// During an already running fight, an explicitly selected living
|
||||
// hostile target may also be treated as an instruction. Merely
|
||||
// selecting an enemy out of combat still must not pull it.
|
||||
if (owner->IsInCombat())
|
||||
{
|
||||
if (Unit* target = owner->GetSelectedUnit())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
}
|
||||
|
||||
// 2. Keep the bot's own valid current victim.
|
||||
if (Unit* target = me->GetVictim())
|
||||
if (IsValidCandidate(target))
|
||||
return target;
|
||||
|
||||
// 3. Continue the group's fight even if the owner is a healer and
|
||||
// currently targets a friendly tank or another party member.
|
||||
if (Unit* target = SelectGroupCombatTarget(owner))
|
||||
return target;
|
||||
|
||||
// 4. Finally, defend the bot itself if something is attacking it.
|
||||
for (Unit* attacker : me->getAttackers())
|
||||
if (IsValidCandidate(attacker))
|
||||
return attacker;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
HandleOutOfCombatRecovery(diff);
|
||||
if (diagnosticTimer > diff)
|
||||
diagnosticTimer -= diff;
|
||||
else
|
||||
diagnosticTimer = 0;
|
||||
|
||||
if (updateTimer > diff)
|
||||
{
|
||||
updateTimer -= diff;
|
||||
|
||||
// VIANO_TARGET_CONTROL_V1_4
|
||||
// Keep the explicitly assigned victim. UpdateVictim() may
|
||||
// select another creature from the threat list.
|
||||
if (me->GetVictim())
|
||||
HandleCombatActions(diff);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateTimer = UPDATE_INTERVAL;
|
||||
|
||||
Player* owner = GetOwnerPlayer();
|
||||
if (!owner || !owner->IsInWorld() || !me->IsInMap(owner))
|
||||
{
|
||||
TC_LOG_INFO("scripts", "[VianoAI] despawn: owner invalid or not on same map (ownerFound=%u)", owner ? 1u : 0u);
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
if (diagnosticTimer == 0)
|
||||
{
|
||||
diagnosticTimer = DIAGNOSTIC_INTERVAL;
|
||||
LogDiagnostics(owner);
|
||||
}
|
||||
|
||||
if (me->getLevel() != owner->getLevel())
|
||||
{
|
||||
me->SetLevel(owner->getLevel());
|
||||
me->UpdateLevelDependantStats();
|
||||
}
|
||||
|
||||
if (me->GetDistance(owner) > TELEPORT_DISTANCE && !IsActivelyEngagedInCombat())
|
||||
{
|
||||
// Keep the recovery teleport for geometry/jump problems, even
|
||||
// if the group is already fighting elsewhere. Only the bot's
|
||||
// own active engagement blocks teleporting.
|
||||
me->GetMotionMaster()->Clear();
|
||||
|
||||
me->NearTeleportTo(
|
||||
owner->GetPositionX() + 1.5f,
|
||||
owner->GetPositionY(),
|
||||
owner->GetPositionZ(),
|
||||
owner->GetOrientation()
|
||||
);
|
||||
|
||||
FollowOwner(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
Unit* desiredTarget = SelectAssistTarget(owner);
|
||||
|
||||
if (desiredTarget)
|
||||
{
|
||||
if (me->GetVictim() != desiredTarget)
|
||||
{
|
||||
TC_LOG_INFO("scripts",
|
||||
"[VianoAI] AttackStart requested: target='%s' entry=%u targetFaction=%u valid=%u meCombatBefore=%u",
|
||||
desiredTarget->GetName().c_str(), desiredTarget->GetEntry(), desiredTarget->GetFaction(),
|
||||
me->IsValidAttackTarget(desiredTarget) ? 1u : 0u, me->IsInCombat() ? 1u : 0u);
|
||||
|
||||
AttackStart(desiredTarget);
|
||||
|
||||
Unit* afterVictim = me->GetVictim();
|
||||
TC_LOG_INFO("scripts", "[VianoAI] AttackStart result: meCombatAfter=%u victim='%s'(%u)",
|
||||
me->IsInCombat() ? 1u : 0u, UnitName(afterVictim), UnitEntry(afterVictim));
|
||||
}
|
||||
}
|
||||
else if (!IsActivelyEngagedInCombat())
|
||||
{
|
||||
// A stale Core combat flag must not prevent recovery/following.
|
||||
// We deliberately do not clear the flag here; OOC regeneration
|
||||
// will be handled as a separate change.
|
||||
FollowOwner(owner);
|
||||
}
|
||||
|
||||
// VIANO_TARGET_CONTROL_V1_4
|
||||
// Keep the explicitly assigned victim. UpdateVictim() may
|
||||
// select another creature from the threat list.
|
||||
if (me->GetVictim())
|
||||
HandleCombatActions(diff);
|
||||
}
|
||||
};
|
||||
|
||||
CreatureAI* GetAI(Creature* creature) const override
|
||||
{
|
||||
return new npc_horde_adventurer_meleeAI(creature);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void AddSC_viano_adventurers()
|
||||
{
|
||||
using namespace VianoAdventurers;
|
||||
new npc_horde_adventurer_melee();
|
||||
}
|
||||
@@ -23,6 +23,10 @@
|
||||
|
||||
namespace VianoBotCommands
|
||||
{
|
||||
// VIANO_GROUP_CONTEXT_V1
|
||||
static constexpr bool VIANO_ENFORCE_GROUP_LIMIT = false;
|
||||
static constexpr uint32 VIANO_MAX_EFFECTIVE_GROUP_SIZE = 5;
|
||||
|
||||
static char const* BoolText(bool value)
|
||||
{
|
||||
return value ? "yes" : "no";
|
||||
@@ -364,6 +368,71 @@ namespace VianoBotCommands
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// VIANO_BOT_SUMMON_COMMAND_V1
|
||||
static bool HandleSummon(ChatHandler* handler, char const* args)
|
||||
{
|
||||
Player* player = GetCommandPlayer(handler);
|
||||
if (!player)
|
||||
return true;
|
||||
|
||||
if (VIANO_ENFORCE_GROUP_LIMIT
|
||||
&& player->GetVianoEffectiveGroupSize() >= VIANO_MAX_EFFECTIVE_GROUP_SIZE)
|
||||
{
|
||||
handler->PSendSysMessage(
|
||||
"Viano effective group is full (%u/%u).",
|
||||
player->GetVianoEffectiveGroupSize(),
|
||||
VIANO_MAX_EFFECTIVE_GROUP_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string role = args ? args : "";
|
||||
std::string::size_type first = role.find_first_not_of(" \t");
|
||||
std::string::size_type last = role.find_last_not_of(" \t");
|
||||
|
||||
if (first == std::string::npos)
|
||||
{
|
||||
handler->SendSysMessage("Usage: .viano bot summon melee|tank");
|
||||
return true;
|
||||
}
|
||||
|
||||
role = role.substr(first, last - first + 1);
|
||||
|
||||
uint32 entry = 0;
|
||||
if (role == "melee")
|
||||
entry = 900001;
|
||||
else if (role == "tank")
|
||||
entry = 900002;
|
||||
else
|
||||
{
|
||||
handler->SendSysMessage("Unknown role. Usage: .viano bot summon melee|tank");
|
||||
return true;
|
||||
}
|
||||
|
||||
Creature* summon = player->SummonCreature(
|
||||
entry,
|
||||
player->GetPositionX() + 1.5f,
|
||||
player->GetPositionY(),
|
||||
player->GetPositionZ(),
|
||||
player->GetOrientation(),
|
||||
TEMPSUMMON_MANUAL_DESPAWN,
|
||||
0);
|
||||
|
||||
if (!summon)
|
||||
{
|
||||
handler->PSendSysMessage("Failed to summon Viano bot entry=%u.", entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"Summoned Viano bot role=%s entry=%u guid=%s",
|
||||
role.c_str(),
|
||||
entry,
|
||||
GuidText(summon->GetGUID()).c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class VianoBotCommandScript : public CommandScript
|
||||
{
|
||||
public:
|
||||
@@ -378,6 +447,7 @@ namespace VianoBotCommands
|
||||
{ "combat", rbac::RBAC_PERM_COMMAND_GM, false, &HandleCombat, "" },
|
||||
{ "owner", rbac::RBAC_PERM_COMMAND_GM, false, &HandleOwner, "" },
|
||||
{ "role", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRole, "" },
|
||||
{ "summon", rbac::RBAC_PERM_COMMAND_GM, false, &HandleSummon, "" },
|
||||
};
|
||||
|
||||
static std::vector<ChatCommand> vianoCommands =
|
||||
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* Viano bot GM diagnostics.
|
||||
*
|
||||
* Read-only commands:
|
||||
* .viano bot info
|
||||
* .viano bot relations
|
||||
* .viano bot combat
|
||||
* .viano bot owner
|
||||
* .viano bot role
|
||||
*/
|
||||
|
||||
#include "ScriptMgr.h"
|
||||
#include "Chat.h"
|
||||
#include "Creature.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "RBAC.h"
|
||||
#include "TemporarySummon.h"
|
||||
#include "Unit.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace VianoBotCommands
|
||||
{
|
||||
static char const* BoolText(bool value)
|
||||
{
|
||||
return value ? "yes" : "no";
|
||||
}
|
||||
|
||||
static std::string GuidText(ObjectGuid const& guid)
|
||||
{
|
||||
return guid ? guid.ToString() : std::string("<empty>");
|
||||
}
|
||||
|
||||
static char const* UnitName(Unit const* unit)
|
||||
{
|
||||
return unit ? unit->GetName().c_str() : "<none>";
|
||||
}
|
||||
|
||||
static uint32 UnitEntry(Unit const* unit)
|
||||
{
|
||||
if (Creature const* creature = unit ? unit->ToCreature() : nullptr)
|
||||
return creature->GetEntry();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static std::string GetScriptName(Creature const* creature)
|
||||
{
|
||||
if (!creature)
|
||||
return {};
|
||||
|
||||
if (CreatureTemplate const* creatureTemplate = creature->GetCreatureTemplate())
|
||||
return sObjectMgr->GetScriptName(creatureTemplate->ScriptID);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static bool IsVianoBot(Creature const* creature)
|
||||
{
|
||||
if (!creature)
|
||||
return false;
|
||||
|
||||
std::string const scriptName = GetScriptName(creature);
|
||||
|
||||
return scriptName.rfind("npc_horde_adventurer_", 0) == 0
|
||||
|| scriptName.rfind("npc_alliance_adventurer_", 0) == 0;
|
||||
}
|
||||
|
||||
static Creature* GetSelectedVianoBot(ChatHandler* handler)
|
||||
{
|
||||
if (!handler || !handler->GetSession())
|
||||
return nullptr;
|
||||
|
||||
Player* player = handler->GetSession()->GetPlayer();
|
||||
if (!player)
|
||||
return nullptr;
|
||||
|
||||
Unit* selected = player->GetSelectedUnit();
|
||||
Creature* creature = selected ? selected->ToCreature() : nullptr;
|
||||
|
||||
if (!creature)
|
||||
{
|
||||
handler->SendSysMessage("Select a Viano bot creature first.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!IsVianoBot(creature))
|
||||
{
|
||||
handler->PSendSysMessage(
|
||||
"Selected creature is not a recognized Viano bot. entry=%u script='%s'",
|
||||
creature->GetEntry(), GetScriptName(creature).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return creature;
|
||||
}
|
||||
|
||||
static Player* GetCommandPlayer(ChatHandler* handler)
|
||||
{
|
||||
return handler && handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
|
||||
}
|
||||
|
||||
static Unit* GetResolvedController(Creature* bot)
|
||||
{
|
||||
return bot ? bot->GetCharmerOrOwnerOrCreatorOrSelf() : nullptr;
|
||||
}
|
||||
|
||||
static char const* DetectRole(Creature const* bot)
|
||||
{
|
||||
std::string const scriptName = GetScriptName(bot);
|
||||
|
||||
if (scriptName.find("_melee") != std::string::npos)
|
||||
return "damage-melee";
|
||||
if (scriptName.find("_tank") != std::string::npos)
|
||||
return "tank";
|
||||
if (scriptName.find("_healer") != std::string::npos)
|
||||
return "healer";
|
||||
if (scriptName.find("_ranged") != std::string::npos)
|
||||
return "damage-ranged";
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static void PrintHeader(ChatHandler* handler, Creature* bot, char const* section)
|
||||
{
|
||||
handler->PSendSysMessage(
|
||||
"[VianoBot:%s] name='%s' entry=%u guid=%s script='%s'",
|
||||
section,
|
||||
bot->GetName().c_str(),
|
||||
bot->GetEntry(),
|
||||
GuidText(bot->GetGUID()).c_str(),
|
||||
GetScriptName(bot).c_str());
|
||||
}
|
||||
|
||||
static bool HandleInfo(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Player* gm = GetCommandPlayer(handler);
|
||||
Unit* victim = bot->GetVictim();
|
||||
|
||||
PrintHeader(handler, bot, "info");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"level=%u alive=%s health=%u/%u faction=%u react=%u combat=%s attackers=%u",
|
||||
uint32(bot->getLevel()),
|
||||
BoolText(bot->IsAlive()),
|
||||
bot->GetHealth(),
|
||||
bot->GetMaxHealth(),
|
||||
bot->GetFaction(),
|
||||
uint32(bot->GetReactState()),
|
||||
BoolText(bot->IsInCombat()),
|
||||
uint32(bot->getAttackers().size()));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"unitFlags=0x%08X immuneToPC=%s immuneToNPC=%s selectable=%s",
|
||||
bot->GetUInt32Value(UNIT_FIELD_FLAGS),
|
||||
BoolText(bot->IsImmuneToPC()),
|
||||
BoolText(bot->IsImmuneToNPC()),
|
||||
BoolText(!bot->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"victim='%s' entry=%u victimGuid=%s",
|
||||
UnitName(victim),
|
||||
UnitEntry(victim),
|
||||
victim ? GuidText(victim->GetGUID()).c_str() : "<empty>");
|
||||
|
||||
if (gm)
|
||||
handler->PSendSysMessage(
|
||||
"distanceToGM=%.2f gmAssistTarget=%s gmAttackTarget=%s",
|
||||
bot->GetDistance(gm),
|
||||
BoolText(gm->IsValidAssistTarget(bot)),
|
||||
BoolText(gm->IsValidAttackTarget(bot)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleRelations(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Player* player = GetCommandPlayer(handler);
|
||||
if (!player)
|
||||
return true;
|
||||
|
||||
PrintHeader(handler, bot, "relations");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"player='%s' playerGuid=%s botFaction=%u playerFaction=%u",
|
||||
player->GetName().c_str(),
|
||||
GuidText(player->GetGUID()).c_str(),
|
||||
bot->GetFaction(),
|
||||
player->GetFaction());
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"player->bot: party=%s raid=%s assist=%s attack=%s reaction=%d",
|
||||
BoolText(player->IsInPartyWith(bot)),
|
||||
BoolText(player->IsInRaidWith(bot)),
|
||||
BoolText(player->IsValidAssistTarget(bot)),
|
||||
BoolText(player->IsValidAttackTarget(bot)),
|
||||
int32(player->GetReactionTo(bot)));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"bot->player: party=%s raid=%s assist=%s attack=%s reaction=%d",
|
||||
BoolText(bot->IsInPartyWith(player)),
|
||||
BoolText(bot->IsInRaidWith(player)),
|
||||
BoolText(bot->IsValidAssistTarget(player)),
|
||||
BoolText(bot->IsValidAttackTarget(player)),
|
||||
int32(bot->GetReactionTo(player)));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"bot immuneToPC=%s immuneToNPC=%s unitFlags=0x%08X",
|
||||
BoolText(bot->IsImmuneToPC()),
|
||||
BoolText(bot->IsImmuneToNPC()),
|
||||
bot->GetUInt32Value(UNIT_FIELD_FLAGS));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleCombat(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Player* player = GetCommandPlayer(handler);
|
||||
Unit* victim = bot->GetVictim();
|
||||
|
||||
PrintHeader(handler, bot, "combat");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"botCombat=%s victim='%s' entry=%u attackers=%u casting=%s",
|
||||
BoolText(bot->IsInCombat()),
|
||||
UnitName(victim),
|
||||
UnitEntry(victim),
|
||||
uint32(bot->getAttackers().size()),
|
||||
BoolText(bot->HasUnitState(UNIT_STATE_CASTING)));
|
||||
|
||||
uint32 index = 0;
|
||||
for (Unit* attacker : bot->getAttackers())
|
||||
{
|
||||
if (!attacker)
|
||||
continue;
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"attacker[%u]='%s' entry=%u guid=%s alive=%s victim='%s'(%u)",
|
||||
index++,
|
||||
attacker->GetName().c_str(),
|
||||
UnitEntry(attacker),
|
||||
GuidText(attacker->GetGUID()).c_str(),
|
||||
BoolText(attacker->IsAlive()),
|
||||
UnitName(attacker->GetVictim()),
|
||||
UnitEntry(attacker->GetVictim()));
|
||||
}
|
||||
|
||||
if (index == 0)
|
||||
handler->SendSysMessage("attackers=<none>");
|
||||
|
||||
if (player)
|
||||
{
|
||||
Unit* playerVictim = player->GetVictim();
|
||||
Unit* selected = player->GetSelectedUnit();
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"playerCombat=%s playerVictim='%s'(%u) selected='%s'(%u)",
|
||||
BoolText(player->IsInCombat()),
|
||||
UnitName(playerVictim),
|
||||
UnitEntry(playerVictim),
|
||||
UnitName(selected),
|
||||
UnitEntry(selected));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleOwner(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Unit* resolved = GetResolvedController(bot);
|
||||
Unit* owner = bot->GetOwner();
|
||||
Unit* creator = bot->GetCreatorGUID()
|
||||
? ObjectAccessor::GetUnit(*bot, bot->GetCreatorGUID())
|
||||
: nullptr;
|
||||
Unit* charmer = bot->GetCharmer();
|
||||
|
||||
PrintHeader(handler, bot, "owner");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"OwnerGUID=%s CreatorGUID=%s CharmerGUID=%s",
|
||||
GuidText(bot->GetOwnerGUID()).c_str(),
|
||||
GuidText(bot->GetCreatorGUID()).c_str(),
|
||||
GuidText(bot->GetCharmerGUID()).c_str());
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"owner='%s'(%u) creator='%s'(%u) charmer='%s'(%u)",
|
||||
UnitName(owner), UnitEntry(owner),
|
||||
UnitName(creator), UnitEntry(creator),
|
||||
UnitName(charmer), UnitEntry(charmer));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"resolvedController='%s' entry=%u guid=%s typeId=%u",
|
||||
UnitName(resolved),
|
||||
UnitEntry(resolved),
|
||||
resolved ? GuidText(resolved->GetGUID()).c_str() : "<empty>",
|
||||
resolved ? uint32(resolved->GetTypeId()) : 0u);
|
||||
|
||||
if (TempSummon* summon = bot->ToTempSummon())
|
||||
{
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
handler->PSendSysMessage(
|
||||
"tempSummon=yes summoner='%s' entry=%u guid=%s",
|
||||
UnitName(summoner),
|
||||
UnitEntry(summoner),
|
||||
summoner ? GuidText(summoner->GetGUID()).c_str() : "<empty>");
|
||||
}
|
||||
else
|
||||
handler->SendSysMessage("tempSummon=no");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleRole(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Player* player = GetCommandPlayer(handler);
|
||||
Unit* victim = bot->GetVictim();
|
||||
|
||||
PrintHeader(handler, bot, "role");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"role=%s react=%u level=%u combat=%s victim='%s'(%u)",
|
||||
DetectRole(bot),
|
||||
uint32(bot->GetReactState()),
|
||||
uint32(bot->getLevel()),
|
||||
BoolText(bot->IsInCombat()),
|
||||
UnitName(victim),
|
||||
UnitEntry(victim));
|
||||
|
||||
if (player)
|
||||
{
|
||||
handler->PSendSysMessage(
|
||||
"ownerCandidate='%s' distance=%.2f canAssistBot=%s canAttackBot=%s",
|
||||
player->GetName().c_str(),
|
||||
bot->GetDistance(player),
|
||||
BoolText(player->IsValidAssistTarget(bot)),
|
||||
BoolText(player->IsValidAttackTarget(bot)));
|
||||
}
|
||||
|
||||
handler->SendSysMessage(
|
||||
"role-specific AI state/cooldowns can be added here as tank/healer roles are implemented.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// VIANO_BOT_SUMMON_COMMAND_V1
|
||||
static bool HandleSummon(ChatHandler* handler, char const* args)
|
||||
{
|
||||
Player* player = GetCommandPlayer(handler);
|
||||
if (!player)
|
||||
return true;
|
||||
|
||||
std::string role = args ? args : "";
|
||||
std::string::size_type first = role.find_first_not_of(" \t");
|
||||
std::string::size_type last = role.find_last_not_of(" \t");
|
||||
|
||||
if (first == std::string::npos)
|
||||
{
|
||||
handler->SendSysMessage("Usage: .viano bot summon melee|tank");
|
||||
return true;
|
||||
}
|
||||
|
||||
role = role.substr(first, last - first + 1);
|
||||
|
||||
uint32 entry = 0;
|
||||
if (role == "melee")
|
||||
entry = 900001;
|
||||
else if (role == "tank")
|
||||
entry = 900002;
|
||||
else
|
||||
{
|
||||
handler->SendSysMessage("Unknown role. Usage: .viano bot summon melee|tank");
|
||||
return true;
|
||||
}
|
||||
|
||||
Creature* summon = player->SummonCreature(
|
||||
entry,
|
||||
player->GetPositionX() + 1.5f,
|
||||
player->GetPositionY(),
|
||||
player->GetPositionZ(),
|
||||
player->GetOrientation(),
|
||||
TEMPSUMMON_MANUAL_DESPAWN,
|
||||
0);
|
||||
|
||||
if (!summon)
|
||||
{
|
||||
handler->PSendSysMessage("Failed to summon Viano bot entry=%u.", entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"Summoned Viano bot role=%s entry=%u guid=%s",
|
||||
role.c_str(),
|
||||
entry,
|
||||
GuidText(summon->GetGUID()).c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class VianoBotCommandScript : public CommandScript
|
||||
{
|
||||
public:
|
||||
VianoBotCommandScript() : CommandScript("VianoBotCommandScript") { }
|
||||
|
||||
std::vector<ChatCommand> GetCommands() const override
|
||||
{
|
||||
static std::vector<ChatCommand> botCommands =
|
||||
{
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleInfo, "" },
|
||||
{ "relations", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRelations, "" },
|
||||
{ "combat", rbac::RBAC_PERM_COMMAND_GM, false, &HandleCombat, "" },
|
||||
{ "owner", rbac::RBAC_PERM_COMMAND_GM, false, &HandleOwner, "" },
|
||||
{ "role", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRole, "" },
|
||||
{ "summon", rbac::RBAC_PERM_COMMAND_GM, false, &HandleSummon, "" },
|
||||
};
|
||||
|
||||
static std::vector<ChatCommand> vianoCommands =
|
||||
{
|
||||
{ "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands },
|
||||
};
|
||||
|
||||
static std::vector<ChatCommand> commandTable =
|
||||
{
|
||||
{ "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands },
|
||||
};
|
||||
|
||||
return commandTable;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void AddSC_viano_bot_commands()
|
||||
{
|
||||
new VianoBotCommands::VianoBotCommandScript();
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* Viano bot GM diagnostics.
|
||||
*
|
||||
* Read-only commands:
|
||||
* .viano bot info
|
||||
* .viano bot relations
|
||||
* .viano bot combat
|
||||
* .viano bot owner
|
||||
* .viano bot role
|
||||
*/
|
||||
|
||||
#include "ScriptMgr.h"
|
||||
#include "Chat.h"
|
||||
#include "Creature.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "RBAC.h"
|
||||
#include "TemporarySummon.h"
|
||||
#include "Unit.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace VianoBotCommands
|
||||
{
|
||||
static char const* BoolText(bool value)
|
||||
{
|
||||
return value ? "yes" : "no";
|
||||
}
|
||||
|
||||
static std::string GuidText(ObjectGuid const& guid)
|
||||
{
|
||||
return guid ? guid.ToString() : std::string("<empty>");
|
||||
}
|
||||
|
||||
static char const* UnitName(Unit const* unit)
|
||||
{
|
||||
return unit ? unit->GetName().c_str() : "<none>";
|
||||
}
|
||||
|
||||
static uint32 UnitEntry(Unit const* unit)
|
||||
{
|
||||
if (Creature const* creature = unit ? unit->ToCreature() : nullptr)
|
||||
return creature->GetEntry();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static std::string GetScriptName(Creature const* creature)
|
||||
{
|
||||
if (!creature)
|
||||
return {};
|
||||
|
||||
if (CreatureTemplate const* creatureTemplate = creature->GetCreatureTemplate())
|
||||
return sObjectMgr->GetScriptName(creatureTemplate->ScriptID);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static bool IsVianoBot(Creature const* creature)
|
||||
{
|
||||
if (!creature)
|
||||
return false;
|
||||
|
||||
std::string const scriptName = GetScriptName(creature);
|
||||
|
||||
return scriptName.rfind("npc_horde_adventurer_", 0) == 0
|
||||
|| scriptName.rfind("npc_alliance_adventurer_", 0) == 0;
|
||||
}
|
||||
|
||||
static Creature* GetSelectedVianoBot(ChatHandler* handler)
|
||||
{
|
||||
if (!handler || !handler->GetSession())
|
||||
return nullptr;
|
||||
|
||||
Player* player = handler->GetSession()->GetPlayer();
|
||||
if (!player)
|
||||
return nullptr;
|
||||
|
||||
Unit* selected = player->GetSelectedUnit();
|
||||
Creature* creature = selected ? selected->ToCreature() : nullptr;
|
||||
|
||||
if (!creature)
|
||||
{
|
||||
handler->SendSysMessage("Select a Viano bot creature first.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!IsVianoBot(creature))
|
||||
{
|
||||
handler->PSendSysMessage(
|
||||
"Selected creature is not a recognized Viano bot. entry=%u script='%s'",
|
||||
creature->GetEntry(), GetScriptName(creature).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return creature;
|
||||
}
|
||||
|
||||
static Player* GetCommandPlayer(ChatHandler* handler)
|
||||
{
|
||||
return handler && handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
|
||||
}
|
||||
|
||||
static Unit* GetResolvedController(Creature* bot)
|
||||
{
|
||||
return bot ? bot->GetCharmerOrOwnerOrCreatorOrSelf() : nullptr;
|
||||
}
|
||||
|
||||
static char const* DetectRole(Creature const* bot)
|
||||
{
|
||||
std::string const scriptName = GetScriptName(bot);
|
||||
|
||||
if (scriptName.find("_melee") != std::string::npos)
|
||||
return "damage-melee";
|
||||
if (scriptName.find("_tank") != std::string::npos)
|
||||
return "tank";
|
||||
if (scriptName.find("_healer") != std::string::npos)
|
||||
return "healer";
|
||||
if (scriptName.find("_ranged") != std::string::npos)
|
||||
return "damage-ranged";
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static void PrintHeader(ChatHandler* handler, Creature* bot, char const* section)
|
||||
{
|
||||
handler->PSendSysMessage(
|
||||
"[VianoBot:%s] name='%s' entry=%u guid=%s script='%s'",
|
||||
section,
|
||||
bot->GetName().c_str(),
|
||||
bot->GetEntry(),
|
||||
GuidText(bot->GetGUID()).c_str(),
|
||||
GetScriptName(bot).c_str());
|
||||
}
|
||||
|
||||
static bool HandleInfo(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Player* gm = GetCommandPlayer(handler);
|
||||
Unit* victim = bot->GetVictim();
|
||||
|
||||
PrintHeader(handler, bot, "info");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"level=%u alive=%s health=%u/%u faction=%u react=%u combat=%s attackers=%u",
|
||||
uint32(bot->getLevel()),
|
||||
BoolText(bot->IsAlive()),
|
||||
bot->GetHealth(),
|
||||
bot->GetMaxHealth(),
|
||||
bot->GetFaction(),
|
||||
uint32(bot->GetReactState()),
|
||||
BoolText(bot->IsInCombat()),
|
||||
uint32(bot->getAttackers().size()));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"unitFlags=0x%08X immuneToPC=%s immuneToNPC=%s selectable=%s",
|
||||
bot->GetUInt32Value(UNIT_FIELD_FLAGS),
|
||||
BoolText(bot->IsImmuneToPC()),
|
||||
BoolText(bot->IsImmuneToNPC()),
|
||||
BoolText(!bot->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"victim='%s' entry=%u victimGuid=%s",
|
||||
UnitName(victim),
|
||||
UnitEntry(victim),
|
||||
victim ? GuidText(victim->GetGUID()).c_str() : "<empty>");
|
||||
|
||||
if (gm)
|
||||
handler->PSendSysMessage(
|
||||
"distanceToGM=%.2f gmAssistTarget=%s gmAttackTarget=%s",
|
||||
bot->GetDistance(gm),
|
||||
BoolText(gm->IsValidAssistTarget(bot)),
|
||||
BoolText(gm->IsValidAttackTarget(bot)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleRelations(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Player* player = GetCommandPlayer(handler);
|
||||
if (!player)
|
||||
return true;
|
||||
|
||||
PrintHeader(handler, bot, "relations");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"player='%s' playerGuid=%s botFaction=%u playerFaction=%u",
|
||||
player->GetName().c_str(),
|
||||
GuidText(player->GetGUID()).c_str(),
|
||||
bot->GetFaction(),
|
||||
player->GetFaction());
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"player->bot: party=%s raid=%s assist=%s attack=%s reaction=%d",
|
||||
BoolText(player->IsInPartyWith(bot)),
|
||||
BoolText(player->IsInRaidWith(bot)),
|
||||
BoolText(player->IsValidAssistTarget(bot)),
|
||||
BoolText(player->IsValidAttackTarget(bot)),
|
||||
int32(player->GetReactionTo(bot)));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"bot->player: party=%s raid=%s assist=%s attack=%s reaction=%d",
|
||||
BoolText(bot->IsInPartyWith(player)),
|
||||
BoolText(bot->IsInRaidWith(player)),
|
||||
BoolText(bot->IsValidAssistTarget(player)),
|
||||
BoolText(bot->IsValidAttackTarget(player)),
|
||||
int32(bot->GetReactionTo(player)));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"bot immuneToPC=%s immuneToNPC=%s unitFlags=0x%08X",
|
||||
BoolText(bot->IsImmuneToPC()),
|
||||
BoolText(bot->IsImmuneToNPC()),
|
||||
bot->GetUInt32Value(UNIT_FIELD_FLAGS));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleCombat(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Player* player = GetCommandPlayer(handler);
|
||||
Unit* victim = bot->GetVictim();
|
||||
|
||||
PrintHeader(handler, bot, "combat");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"botCombat=%s victim='%s' entry=%u attackers=%u casting=%s",
|
||||
BoolText(bot->IsInCombat()),
|
||||
UnitName(victim),
|
||||
UnitEntry(victim),
|
||||
uint32(bot->getAttackers().size()),
|
||||
BoolText(bot->HasUnitState(UNIT_STATE_CASTING)));
|
||||
|
||||
uint32 index = 0;
|
||||
for (Unit* attacker : bot->getAttackers())
|
||||
{
|
||||
if (!attacker)
|
||||
continue;
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"attacker[%u]='%s' entry=%u guid=%s alive=%s victim='%s'(%u)",
|
||||
index++,
|
||||
attacker->GetName().c_str(),
|
||||
UnitEntry(attacker),
|
||||
GuidText(attacker->GetGUID()).c_str(),
|
||||
BoolText(attacker->IsAlive()),
|
||||
UnitName(attacker->GetVictim()),
|
||||
UnitEntry(attacker->GetVictim()));
|
||||
}
|
||||
|
||||
if (index == 0)
|
||||
handler->SendSysMessage("attackers=<none>");
|
||||
|
||||
if (player)
|
||||
{
|
||||
Unit* playerVictim = player->GetVictim();
|
||||
Unit* selected = player->GetSelectedUnit();
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"playerCombat=%s playerVictim='%s'(%u) selected='%s'(%u)",
|
||||
BoolText(player->IsInCombat()),
|
||||
UnitName(playerVictim),
|
||||
UnitEntry(playerVictim),
|
||||
UnitName(selected),
|
||||
UnitEntry(selected));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleOwner(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Unit* resolved = GetResolvedController(bot);
|
||||
Unit* owner = bot->GetOwner();
|
||||
Unit* creator = bot->GetCreatorGUID()
|
||||
? ObjectAccessor::GetUnit(*bot, bot->GetCreatorGUID())
|
||||
: nullptr;
|
||||
Unit* charmer = bot->GetCharmer();
|
||||
|
||||
PrintHeader(handler, bot, "owner");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"OwnerGUID=%s CreatorGUID=%s CharmerGUID=%s",
|
||||
GuidText(bot->GetOwnerGUID()).c_str(),
|
||||
GuidText(bot->GetCreatorGUID()).c_str(),
|
||||
GuidText(bot->GetCharmerGUID()).c_str());
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"owner='%s'(%u) creator='%s'(%u) charmer='%s'(%u)",
|
||||
UnitName(owner), UnitEntry(owner),
|
||||
UnitName(creator), UnitEntry(creator),
|
||||
UnitName(charmer), UnitEntry(charmer));
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"resolvedController='%s' entry=%u guid=%s typeId=%u",
|
||||
UnitName(resolved),
|
||||
UnitEntry(resolved),
|
||||
resolved ? GuidText(resolved->GetGUID()).c_str() : "<empty>",
|
||||
resolved ? uint32(resolved->GetTypeId()) : 0u);
|
||||
|
||||
if (TempSummon* summon = bot->ToTempSummon())
|
||||
{
|
||||
Unit* summoner = summon->GetSummoner();
|
||||
handler->PSendSysMessage(
|
||||
"tempSummon=yes summoner='%s' entry=%u guid=%s",
|
||||
UnitName(summoner),
|
||||
UnitEntry(summoner),
|
||||
summoner ? GuidText(summoner->GetGUID()).c_str() : "<empty>");
|
||||
}
|
||||
else
|
||||
handler->SendSysMessage("tempSummon=no");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleRole(ChatHandler* handler, char const* /*args*/)
|
||||
{
|
||||
Creature* bot = GetSelectedVianoBot(handler);
|
||||
if (!bot)
|
||||
return true;
|
||||
|
||||
Player* player = GetCommandPlayer(handler);
|
||||
Unit* victim = bot->GetVictim();
|
||||
|
||||
PrintHeader(handler, bot, "role");
|
||||
|
||||
handler->PSendSysMessage(
|
||||
"role=%s react=%u level=%u combat=%s victim='%s'(%u)",
|
||||
DetectRole(bot),
|
||||
uint32(bot->GetReactState()),
|
||||
uint32(bot->getLevel()),
|
||||
BoolText(bot->IsInCombat()),
|
||||
UnitName(victim),
|
||||
UnitEntry(victim));
|
||||
|
||||
if (player)
|
||||
{
|
||||
handler->PSendSysMessage(
|
||||
"ownerCandidate='%s' distance=%.2f canAssistBot=%s canAttackBot=%s",
|
||||
player->GetName().c_str(),
|
||||
bot->GetDistance(player),
|
||||
BoolText(player->IsValidAssistTarget(bot)),
|
||||
BoolText(player->IsValidAttackTarget(bot)));
|
||||
}
|
||||
|
||||
handler->SendSysMessage(
|
||||
"role-specific AI state/cooldowns can be added here as tank/healer roles are implemented.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class VianoBotCommandScript : public CommandScript
|
||||
{
|
||||
public:
|
||||
VianoBotCommandScript() : CommandScript("VianoBotCommandScript") { }
|
||||
|
||||
std::vector<ChatCommand> GetCommands() const override
|
||||
{
|
||||
static std::vector<ChatCommand> botCommands =
|
||||
{
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleInfo, "" },
|
||||
{ "relations", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRelations, "" },
|
||||
{ "combat", rbac::RBAC_PERM_COMMAND_GM, false, &HandleCombat, "" },
|
||||
{ "owner", rbac::RBAC_PERM_COMMAND_GM, false, &HandleOwner, "" },
|
||||
{ "role", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRole, "" },
|
||||
};
|
||||
|
||||
static std::vector<ChatCommand> vianoCommands =
|
||||
{
|
||||
{ "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands },
|
||||
};
|
||||
|
||||
static std::vector<ChatCommand> commandTable =
|
||||
{
|
||||
{ "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands },
|
||||
};
|
||||
|
||||
return commandTable;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void AddSC_viano_bot_commands()
|
||||
{
|
||||
new VianoBotCommands::VianoBotCommandScript();
|
||||
}
|
||||
Reference in new issue
Block a user