few fixes + added Tankbot

This commit is contained in:
Nico committed 2026-10-01 14:38:53 +02:00
1 parent 3cf7e70283
commit 1785350447
16 files changed
+49600 -3

No files matched your search

+105 -3
View File
@@ -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
+3
View File
@@ -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
+1
View File
@@ -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();
}
@@ -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();
}
@@ -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();
}
@@ -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 =
@@ -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();
}