diff --git a/CMakeCache.txt b/CMakeCache.txt deleted file mode 100644 index 87a102b394..0000000000 --- a/CMakeCache.txt +++ /dev/null @@ -1,79 +0,0 @@ -# This is the CMakeCache file. -# For build in directory: /home/wow_admin/wow_cata_server/source/TrinityCore -# It was generated by CMake: /usr/bin/cmake -# You can edit this file to change values found and used by cmake. -# If you do not want to change any of the values, simply exit the editor. -# If you do want to change a value, simply edit, save, and exit the editor. -# The syntax for the file is as follows: -# KEY:TYPE=VALUE -# KEY is the name of a variable in the cache. -# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. -# VALUE is the current value for the KEY. - -######################## -# EXTERNAL cache entries -######################## - -//Value Computed by CMake. -CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/home/wow_admin/wow_cata_server/source/TrinityCore/CMakeFiles/pkgRedirects - -//Value Computed by CMake -CMAKE_PROJECT_DESCRIPTION:STATIC= - -//Value Computed by CMake -CMAKE_PROJECT_HOMEPAGE_URL:STATIC= - -//Value Computed by CMake -CMAKE_PROJECT_NAME:STATIC=TrinityCore - -//Path to a program. -CMAKE_UNAME:FILEPATH=/usr/bin/uname - -//Value Computed by CMake -TrinityCore_BINARY_DIR:STATIC=/home/wow_admin/wow_cata_server/source/TrinityCore - -//Value Computed by CMake -TrinityCore_IS_TOP_LEVEL:STATIC=ON - -//Value Computed by CMake -TrinityCore_SOURCE_DIR:STATIC=/home/wow_admin/wow_cata_server/source/TrinityCore - - -######################## -# INTERNAL cache entries -######################## - -//This is the directory where this CMakeCache.txt was created -CMAKE_CACHEFILE_DIR:INTERNAL=/home/wow_admin/wow_cata_server/source/TrinityCore -//Major version of cmake used to create the current loaded cache -CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 -//Minor version of cmake used to create the current loaded cache -CMAKE_CACHE_MINOR_VERSION:INTERNAL=28 -//Patch version of cmake used to create the current loaded cache -CMAKE_CACHE_PATCH_VERSION:INTERNAL=3 -//Path to CMake executable. -CMAKE_COMMAND:INTERNAL=/usr/bin/cmake -//Path to cpack program executable. -CMAKE_CPACK_COMMAND:INTERNAL=/usr/bin/cpack -//Path to ctest program executable. -CMAKE_CTEST_COMMAND:INTERNAL=/usr/bin/ctest -//Name of external makefile project generator. -CMAKE_EXTRA_GENERATOR:INTERNAL= -//Name of generator. -CMAKE_GENERATOR:INTERNAL=Unix Makefiles -//Generator instance identifier. -CMAKE_GENERATOR_INSTANCE:INTERNAL= -//Name of generator platform. -CMAKE_GENERATOR_PLATFORM:INTERNAL= -//Name of generator toolset. -CMAKE_GENERATOR_TOOLSET:INTERNAL= -//Source directory with the top level CMakeLists.txt file for this -// project -CMAKE_HOME_DIRECTORY:INTERNAL=/home/wow_admin/wow_cata_server/source/TrinityCore -//number of local generators -CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 -//Platform information initialized -CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 -//Path to CMake installation. -CMAKE_ROOT:INTERNAL=/usr/share/cmake-3.28 - diff --git a/CMakeFiles/cmake.check_cache b/CMakeFiles/cmake.check_cache deleted file mode 100644 index 3dccd73172..0000000000 --- a/CMakeFiles/cmake.check_cache +++ /dev/null @@ -1 +0,0 @@ -# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/src/server/game/Entities/Player/Player.cpp.bak-viano-brewfest-area-debug-20261004-233655 b/src/server/game/Entities/Player/Player.cpp.bak-viano-brewfest-area-debug-20261004-233655 new file mode 100644 index 0000000000..0e791443cb --- /dev/null +++ b/src/server/game/Entities/Player/Player.cpp.bak-viano-brewfest-area-debug-20261004-233655 @@ -0,0 +1,28133 @@ +/* + * 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 . + */ + +#include "Player.h" +#include "AccountMgr.h" +#include "AchievementMgr.h" +#include "Archaeology.h" +#include "ArenaTeam.h" +#include "ArenaTeamMgr.h" +#include "Bag.h" +#include "Battlefield.h" +#include "BattlefieldMgr.h" +#include "Battleground.h" +#include "BattlegroundMgr.h" +#include "BattlegroundScore.h" +#include "CellImpl.h" +#include "Channel.h" +#include "ChannelMgr.h" +#include "CharacterCache.h" +#include "CharacterDatabaseCleaner.h" +#include "CharacterPackets.h" +#include "CharmInfo.h" +#include "Chat.h" +#include "CinematicMgr.h" +#include "CombatPackets.h" +#include "CombatLogPackets.h" +#include "Common.h" +#include "ConditionMgr.h" +#include "CreatureAI.h" +#include "DatabaseEnv.h" +#include "DB2Stores.h" +#include "DisableMgr.h" +#include "Formulas.h" +#include "GameClient.h" +#include "GameEventMgr.h" +#include "GameObjectAI.h" +#include "GameTime.h" +#include "GitRevision.h" +#include "GossipDef.h" +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" +#include "Group.h" +#include "GroupMgr.h" +#include "Guild.h" +#include "GuildMgr.h" +#include "InstanceSaveMgr.h" +#include "InstanceScript.h" +#include "InstancePackets.h" +#include "ItemPackets.h" +#include "KillRewarder.h" +#include "Language.h" +#include "LFGMgr.h" +#include "Log.h" +#include "LootItemStorage.h" +#include "LootMgr.h" +#include "LootPackets.h" +#include "Mail.h" +#include "MapManager.h" +#include "MiscPackets.h" +#include "MotionMaster.h" +#include "MovementStructures.h" +#include "MovementPackets.h" +#include "MovementPacketSender.h" +#include "ObjectAccessor.h" +#include "ObjectMgr.h" +#include "Opcodes.h" +#include "OutdoorPvP.h" +#include "OutdoorPvPMgr.h" +#include "Pet.h" +#include "PetPackets.h" +#include "PetitionMgr.h" +#include "PhasingHandler.h" +#include "PlayerTalentInfo.h" +#include "PoolMgr.h" +#include "QueryCallback.h" +#include "QueryHolder.h" +#include "QuestDef.h" +#include "QuestPackets.h" +#include "QuestPools.h" +#include "Realm.h" +#include "ReputationMgr.h" +#include "SharedDefines.h" +#include "SkillDiscovery.h" +#include "SocialMgr.h" +#include "Spell.h" +#include "SpellAuraEffects.h" +#include "SpellAuras.h" +#include "SpellHistory.h" +#include "SpellMgr.h" +#include "SpellPackets.h" +#include "Stats.h" +#include "StringConvert.h" +#include "TerrainMgr.h" +#include "TicketMgr.h" +#include "TradeData.h" +#include "Trainer.h" +#include "Transport.h" +#include "UpdateData.h" +#include "UpdateFieldFlags.h" +#include "UpdateMask.h" +#include "Util.h" +#include "Vehicle.h" +#include "Weather.h" +#include "WeatherMgr.h" +#include "World.h" +#include "WorldPacket.h" +#include "WorldSession.h" +#include "WorldStateMgr.h" +#include "WorldStatePackets.h" +#include +#include + +// corpse reclaim times +static constexpr uint32 DEATH_EXPIRE_STEP = 5 * MINUTE; +static constexpr uint8 MAX_DEATH_COUNT = 3; +static constexpr std::array CorpseReclaimDelay = { 30, 60, 120 }; + +std::array const MasterySpells = +{ + 0, + 87500, // Warrior + 87494, // Paladin + 87493, // Hunter + 87496, // Rogue + 87495, // Priest + 87492, // Death Knight + 87497, // Shaman + 86467, // Mage + 87498, // Warlock + 0, + 87491, // Druid +}; + +uint64 const MAX_MONEY_AMOUNT = 9999999999ULL; + +Player::Player(WorldSession* session) : Unit(true), + _characterFlags(CHARACTER_FLAG_NONE), _characterFlags2(CHARACTER_FLAG_2_NONE) +{ + m_objectType |= TYPEMASK_PLAYER; + m_objectTypeId = TYPEID_PLAYER; + + m_valuesCount = PLAYER_END; + + m_session = session; + + m_ingametime = 0; + m_sharedQuestId = 0; + m_popupQuestId = 0; + + m_ExtraFlags = 0; + + m_spellModTakingSpell = nullptr; + // m_pad = 0; + + // players always accept + if (!GetSession()->HasPermission(rbac::RBAC_PERM_CAN_FILTER_WHISPERS)) SetAcceptWhispers(true); + + m_comboPoints = 0; + + m_weaponChangeTimer = 0; + + m_zoneUpdateId = uint32(-1); + + m_areaUpdateId = 0; + m_team = 0; + + m_needsZoneUpdate = false; + + m_nextSave = sWorld->getIntConfig(CONFIG_INTERVAL_SAVE); + + m_items.fill(nullptr); + + m_social = nullptr; + + // group is initialized in the reference constructor + SetGroupInvite(nullptr); + m_groupUpdateMask = 0; + m_auraRaidUpdateMask = 0; + m_bPassOnGroupLoot = false; + + duel = nullptr; + + m_GuildIdInvited = 0; + m_ArenaTeamIdInvited = 0; + + m_atLoginFlags = AT_LOGIN_NONE; + + mSemaphoreTeleport_Near = false; + mSemaphoreTeleport_Far = false; + + m_DelayedOperations = 0; + m_bCanDelayTeleport = false; + m_bHasDelayedTeleport = false; + m_teleport_options = TELE_TO_NONE; + + m_trade = nullptr; + + m_cinematic = 0; + m_movie = 0; + + PlayerTalkClass = std::make_unique(GetSession()); + m_currentBuybackSlot = BUYBACK_SLOT_START; + + m_DailyQuestChanged = false; + m_lastDailyQuestTime = 0; + + // Init rune flags + m_runeGraceCooldown.fill(0xFFFFFFFF); + m_lastRuneGraceTimers = {}; + + m_MirrorTimer.fill(DISABLED_MIRROR_TIMER); + + m_MirrorTimerFlags = UNDERWATER_NONE; + m_MirrorTimerFlagsLast = UNDERWATER_NONE; + m_hostileReferenceCheckTimer = 0; + m_drunkTimer = 0; + m_deathTimer = 0; + m_deathExpireTime = 0; + + m_swingErrorMsg = 0; + + for (uint8 j = 0; j < PLAYER_MAX_BATTLEGROUND_QUEUES; ++j) + { + m_bgBattlegroundQueueID[j].bgQueueTypeId = BATTLEGROUND_QUEUE_NONE; + m_bgBattlegroundQueueID[j].invitedToInstance = 0; + } + + m_logintime = GameTime::GetGameTime(); + m_Last_tick = m_logintime; + m_Played_time = {}; + m_WeaponProficiency = 0; + m_ArmorProficiency = 0; + m_canParry = false; + m_canBlock = false; + m_canTitanGrip = false; + m_titanGripPenaltySpellId = 0; + + m_temporaryUnsummonedPetNumber = 0; + // cache for UNIT_CREATED_BY_SPELL to allow + // returning reagents for temporarily removed pets + // when dying/logging out + m_oldpetspell = 0; + m_lastpetnumber = 0; + + ////////////////////Rest System///////////////////// + _restTime = 0; + inn_triggerId = 0; + m_rest_bonus = 0; + _restFlagMask = 0; + ////////////////////Rest System///////////////////// + + m_mailsUpdated = false; + unReadMails = 0; + m_nextMailDelivereTime = 0; + + m_itemUpdateQueueBlocked = false; + + m_stableSlots = 0; + + /////////////////// Instance System ///////////////////// + + m_HomebindTimer = 0; + m_InstanceValid = true; + m_dungeonDifficulty = DUNGEON_DIFFICULTY_NORMAL; + m_raidDifficulty = RAID_DIFFICULTY_10MAN_NORMAL; + m_raidMapDifficulty = RAID_DIFFICULTY_10MAN_NORMAL; + + m_lastPotionId = 0; + _talentMgr = std::make_unique(); + + m_auraBaseFlatMod.fill(0.0f); + m_auraBasePctMod.fill(1.0f); + + m_baseRatingValue = {}; + + m_baseSpellPower = 0; + m_baseManaRegen = 0; + m_baseHealthRegen = 0; + m_spellPenetrationItemMod = 0; + + // Honor System + m_lastHonorUpdateTime = GameTime::GetGameTime(); + + m_IsBGRandomWinner = false; + + // Player summoning + m_summon_expire = 0; + m_summon_instanceId = 0; + + m_recall_instanceId = 0; + + m_seer = this; + + m_homebindMapId = 0; + m_homebindAreaId = 0; + m_homebindX = 0; + m_homebindY = 0; + m_homebindZ = 0; + + m_contestedPvPTimer = 0; + + m_isActive = true; + + m_lastFallTime = 0; + m_lastFallZ = 0; + + m_grantableLevels = 0; + m_fishingSteps = 0; + + m_ControlledByPlayer = true; + + sWorld->IncreasePlayerCount(); + + m_ChampioningFaction = 0; + + isDebugAreaTriggers = false; + + m_WeeklyQuestChanged = false; + + m_MonthlyQuestChanged = false; + + m_SeasonalQuestChanged = false; + + m_LFGRewardStatusChanged = false; + + SetPendingBind(0, 0); + + _activeCheats = CHEAT_NONE; + healthBeforeDuel = 0; + manaBeforeDuel = 0; + + _maxPersonalArenaRate = 0; + + _cinematicMgr = std::make_unique(this); + m_achievementMgr = std::make_unique>(this); + m_reputationMgr = std::make_unique(this); + _hasValidLFGLeavePoint = false; + _archaeology = std::make_unique(this); + m_petScalingSynchTimer.Reset(1000); +} + +Player::~Player() +{ + // it must be unloaded already in PlayerLogout and accessed only for logged player + //m_social = nullptr; + + // Note: buy back item already deleted from DB when player was saved + for (uint8 i = 0; i < PLAYER_SLOTS_COUNT; ++i) + delete m_items[i]; + + //all mailed items should be deleted, also all mail should be deallocated + for (PlayerMails::iterator itr = m_mail.begin(); itr != m_mail.end(); ++itr) + delete *itr; + + for (ItemMap::iterator iter = mMitems.begin(); iter != mMitems.end(); ++iter) + delete iter->second; //if item is duplicated... then server may crash ... but that item should be deallocated + + ClearResurrectRequestData(); + + sWorld->DecreasePlayerCount(); +} + +void Player::CleanupsBeforeDelete(bool finalCleanup) +{ + TradeCancel(false); + DuelComplete(DUEL_INTERRUPTED); + + Unit::CleanupsBeforeDelete(finalCleanup); + + // clean up player-instance binds, may unload some instance saves + for (uint8 i = 0; i < MAX_DIFFICULTY; ++i) + for (BoundInstancesMap::iterator itr = m_boundInstances[i].begin(); itr != m_boundInstances[i].end(); ++itr) + itr->second.save->RemovePlayer(this); +} + +bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo) +{ + //FIXME: outfitId not used in player creating + /// @todo need more checks against packet modifications + + Object::_Create(guidlow, 0, HighGuid::Player); + + m_name = createInfo->Name; + + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(createInfo->Race, createInfo->Class); + if (!info) + { + TC_LOG_INFO("entities.player.cheat", "Player::Create: Possible hacking attempt: Account %u tried to create a character named '%s' with an invalid race/class pair (%u/%u) - refusing to do so.", + GetSession()->GetAccountId(), m_name.c_str(), createInfo->Race, createInfo->Class); + return false; + } + + m_items.fill(nullptr); + + Relocate(info->positionX, info->positionY, info->positionZ, info->orientation); + + ChrClassesEntry const* cEntry = sChrClassesStore.LookupEntry(createInfo->Class); + if (!cEntry) + { + TC_LOG_INFO("entities.player.cheat", "Player::Create: Possible hacking attempt: Account %u tried to create a character named '%s' with an invalid character class (%u) - refusing to do so (wrong DBC-files?)", + GetSession()->GetAccountId(), m_name.c_str(), createInfo->Class); + return false; + } + + SetMap(sMapMgr->CreateMap(info->mapId, this)); + + uint8 powertype = cEntry->DisplayPower; + + SetObjectScale(1.0f); + + SetFactionForRace(createInfo->Race); + + if (!IsValidGender(createInfo->Gender)) + { + TC_LOG_INFO("entities.player.cheat", "Player::Create: Possible hacking attempt: Account %u tried to create a character named '%s' with an invalid gender (%u) - refusing to do so", + GetSession()->GetAccountId(), m_name.c_str(), createInfo->Gender); + return false; + } + + if (!ValidateAppearance(createInfo->Race, createInfo->Class, createInfo->Gender, createInfo->HairStyle, createInfo->HairColor, createInfo->Face, createInfo->FacialHair, createInfo->Skin, true)) + { + TC_LOG_INFO("entities.player.cheat", "Player::Create: Possible hacking attempt: Account %u tried to create a character named '%s' with invalid appearance attributes - refusing to do so", + GetSession()->GetAccountId(), m_name.c_str()); + return false; + } + + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_RACE, createInfo->Race); + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_CLASS, createInfo->Class); + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_GENDER, createInfo->Gender); + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_POWER_TYPE, powertype); + RegisterPowerTypes(); + InitDisplayIds(); + if (sWorld->getIntConfig(CONFIG_GAME_TYPE) == REALM_TYPE_PVP || sWorld->getIntConfig(CONFIG_GAME_TYPE) == REALM_TYPE_RPPVP) + { + SetByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_PVP); + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + } + + SetFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_REGENERATE_POWER); + SetFloatValue(UNIT_FIELD_HOVERHEIGHT, 1.0f); // default for players in 3.0.3 + + SetInt32Value(PLAYER_FIELD_WATCHED_FACTION_INDEX, uint32(-1)); // -1 is default value + + SetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID, createInfo->Skin); + SetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_FACE_ID, createInfo->Face); + SetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_STYLE_ID, createInfo->HairStyle); + SetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID, createInfo->HairColor); + SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_FACIAL_STYLE, createInfo->FacialHair); + SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_REST_STATE, (GetSession()->IsARecruiter() || GetSession()->GetRecruiterId() != 0) ? REST_STATE_RAF_LINKED : REST_STATE_NOT_RAF_LINKED); + SetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_GENDER, createInfo->Gender); + SetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_ARENA_FACTION, 0); + + SetUInt64Value(OBJECT_FIELD_DATA, 0); + SetUInt32Value(PLAYER_GUILDRANK, 0); + SetGuildLevel(0); + SetUInt32Value(PLAYER_GUILD_TIMESTAMP, 0); + + for (int i = 0; i < KNOWN_TITLES_SIZE; ++i) + SetUInt64Value(PLAYER__FIELD_KNOWN_TITLES + i, 0); // 0=disabled + SetUInt32Value(PLAYER_CHOSEN_TITLE, 0); + + SetUInt32Value(PLAYER_FIELD_KILLS, 0); + SetUInt32Value(PLAYER_FIELD_LIFETIME_HONORABLE_KILLS, 0); + + // set starting level + uint32 start_level = getClass() != CLASS_DEATH_KNIGHT + ? sWorld->getIntConfig(CONFIG_START_PLAYER_LEVEL) + : sWorld->getIntConfig(CONFIG_START_DEATH_KNIGHT_PLAYER_LEVEL); + + if (m_session->HasPermission(rbac::RBAC_PERM_USE_START_GM_LEVEL)) + { + uint32 gm_level = sWorld->getIntConfig(CONFIG_START_GM_LEVEL); + if (gm_level > start_level) + start_level = gm_level; + } + + SetUInt32Value(UNIT_FIELD_LEVEL, start_level); + + InitRunes(); + + SetUInt64Value(PLAYER_FIELD_COINAGE, sWorld->getIntConfig(CONFIG_START_PLAYER_MONEY)); + SetCurrency(CURRENCY_TYPE_HONOR_POINTS, sWorld->getIntConfig(CONFIG_CURRENCY_START_HONOR_POINTS)); + SetCurrency(CURRENCY_TYPE_JUSTICE_POINTS, sWorld->getIntConfig(CONFIG_CURRENCY_START_JUSTICE_POINTS)); + SetCurrency(CURRENCY_TYPE_CONQUEST_POINTS, sWorld->getIntConfig(CONFIG_CURRENCY_START_CONQUEST_POINTS)); + + // Played time + m_Last_tick = GameTime::GetGameTime(); + m_Played_time[PLAYED_TIME_TOTAL] = 0; + m_Played_time[PLAYED_TIME_LEVEL] = 0; + + // base stats and related field values + InitStatsForLevel(); + InitTaxiNodesForLevel(); + InitGlyphsForLevel(); + InitTalentForLevel(); + InitializeSkillFields(); + InitPrimaryProfessions(); // to max set before any spell added + + // apply original stats mods before spell loading or item equipment that call before equip _RemoveStatsMods() + UpdateMaxHealth(); // Update max Health (for add bonus from stamina) + SetFullHealth(); + SetFullPower(POWER_MANA); + + // original spells + LearnDefaultSkills(); + LearnCustomSpells(); + + // original action bar + for (PlayerCreateInfoActions::const_iterator action_itr = info->action.begin(); action_itr != info->action.end(); ++action_itr) + addActionButton(action_itr->button, action_itr->action, action_itr->type); + + // original items + if (CharStartOutfitEntry const* oEntry = sDBCManager.GetCharStartOutfitEntry(createInfo->Race, createInfo->Class, createInfo->Gender)) + { + for (int j = 0; j < MAX_OUTFIT_ITEMS; ++j) + { + if (oEntry->ItemID[j] <= 0) + continue; + + uint32 itemId = oEntry->ItemID[j]; + + // just skip, reported in ObjectMgr::LoadItemTemplates + ItemTemplate const* iProto = sObjectMgr->GetItemTemplate(itemId); + if (!iProto) + continue; + + // BuyCount by default + uint32 count = iProto->GetBuyCount(); + + // special amount for food/drink + if (iProto->GetClass() == ITEM_CLASS_CONSUMABLE && iProto->GetSubClass() == ITEM_SUBCLASS_FOOD) + { + if (iProto->Effects.size() >= 1) + { + switch (iProto->Effects[0].Category) + { + case SPELL_CATEGORY_FOOD: // food + count = getClass() == CLASS_DEATH_KNIGHT ? 10 : 4; + break; + case SPELL_CATEGORY_DRINK: // drink + count = 2; + break; + } + } + if (iProto->GetMaxStackSize() < count) + count = iProto->GetMaxStackSize(); + } + StoreNewItemInBestSlots(itemId, count); + } + } + + for (PlayerCreateInfoItems::const_iterator item_id_itr = info->item.begin(); item_id_itr != info->item.end(); ++item_id_itr) + StoreNewItemInBestSlots(item_id_itr->item_id, item_id_itr->item_amount); + + // bags and main-hand weapon must equipped at this moment + // now second pass for not equipped (offhand weapon/shield if it attempt equipped before main-hand weapon) + // or ammo not equipped in special bag + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; i++) + { + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + { + uint16 eDest; + // equip offhand weapon/shield if it attempt equipped before main-hand weapon + InventoryResult msg = CanEquipItem(NULL_SLOT, eDest, pItem, false); + if (msg == EQUIP_ERR_OK) + { + RemoveItem(INVENTORY_SLOT_BAG_0, i, true); + EquipItem(eDest, pItem, true); + } + // move other items to more appropriate slots + else + { + ItemPosCountVec sDest; + msg = CanStoreItem(NULL_BAG, NULL_SLOT, sDest, pItem, false); + if (msg == EQUIP_ERR_OK) + { + RemoveItem(INVENTORY_SLOT_BAG_0, i, true); + StoreItem(sDest, pItem, true); + } + } + } + } + // all item positions resolved + + GetThreatManager().Initialize(); + + return true; +} + +bool Player::StoreNewItemInBestSlots(uint32 titem_id, uint32 titem_amount) +{ + TC_LOG_DEBUG("entities.player.items", "Player::StoreNewItemInBestSlots: Player '%s' (%s) creates initial item (ItemID: %u, Count: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), titem_id, titem_amount); + + // attempt equip by one + while (titem_amount > 0) + { + uint16 eDest; + InventoryResult msg = CanEquipNewItem(NULL_SLOT, eDest, titem_id, false); + if (msg != EQUIP_ERR_OK) + break; + + EquipNewItem(eDest, titem_id, true); + AutoUnequipOffhandIfNeed(); + --titem_amount; + } + + if (titem_amount == 0) + return true; // equipped + + // attempt store + ItemPosCountVec sDest; + // store in main bag to simplify second pass (special bags can be not equipped yet at this moment) + InventoryResult msg = CanStoreNewItem(INVENTORY_SLOT_BAG_0, NULL_SLOT, sDest, titem_id, titem_amount); + if (msg == EQUIP_ERR_OK) + { + StoreNewItem(sDest, titem_id, true, GenerateItemRandomPropertyId(titem_id)); + return true; // stored + } + + // item can't be added + TC_LOG_ERROR("entities.player.items", "Player::StoreNewItemInBestSlots: Player '%s' (%s) can't equip or store initial item (ItemID: %u, Race: %u, Class: %u, InventoryResult: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), titem_id, getRace(), getClass(), msg); + return false; +} + +void Player::SendMirrorTimer(MirrorTimerType Type, uint32 MaxValue, uint32 CurrentValue, int32 Regen) +{ + if (int(MaxValue) == DISABLED_MIRROR_TIMER) + { + if (int(CurrentValue) != DISABLED_MIRROR_TIMER) + StopMirrorTimer(Type); + return; + } + + SendDirectMessage(WorldPackets::Misc::StartMirrorTimer(Type, CurrentValue, MaxValue, Regen, 0, false).Write()); +} + +void Player::StopMirrorTimer(MirrorTimerType Type) +{ + m_MirrorTimer[Type] = DISABLED_MIRROR_TIMER; + SendDirectMessage(WorldPackets::Misc::StopMirrorTimer(Type).Write()); +} + +bool Player::IsImmuneToEnvironmentalDamage() const +{ + // check for GM and death state included in isAttackableByAOE + return !isTargetableForAttack(false); +} + +uint32 Player::EnvironmentalDamage(EnviromentalDamage type, uint32 damage) +{ + if (IsImmuneToEnvironmentalDamage()) + return 0; + + // Absorb, resist some environmental damage type + uint32 absorb = 0; + uint32 resist = 0; + switch (type) + { + case DAMAGE_LAVA: + case DAMAGE_SLIME: + case DAMAGE_FIRE: + { + DamageInfo dmgInfo(this, this, damage, nullptr, type == DAMAGE_SLIME ? SPELL_SCHOOL_MASK_NATURE : SPELL_SCHOOL_MASK_FIRE , DIRECT_DAMAGE, BASE_ATTACK); + Unit::CalcAbsorbResist(dmgInfo); + absorb = dmgInfo.GetAbsorb(); + resist = dmgInfo.GetResist(); + damage = dmgInfo.GetDamage(); + break; + } + default: + break; + } + + Unit::DealDamageMods(this, damage, &absorb); + + WorldPackets::CombatLog::EnvironmentalDamageLog environmentalDamageLog; + environmentalDamageLog.Victim = GetGUID(); + environmentalDamageLog.Type = type != DAMAGE_FALL_TO_VOID ? type : DAMAGE_FALL; + environmentalDamageLog.Amount = damage; + environmentalDamageLog.Absorbed = absorb; + environmentalDamageLog.Resisted = resist; + SendMessageToSet(environmentalDamageLog.Write(), true); + + uint32 final_damage = Unit::DealDamage(this, this, damage, 0, SELF_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, nullptr, false); + + if (!IsAlive()) + { + if (type == DAMAGE_FALL) // DealDamage does not apply item durability loss from self-induced damage. + { + TC_LOG_DEBUG("entities.player", "Player::EnvironmentalDamage: Player '%s' (%s) fall to death, losing 10%% durability", + GetName().c_str(), GetGUID().ToString().c_str()); + DurabilityLossAll(0.10f, false); + // durability lost message + SendDurabilityLoss(this, 10); + } + + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DEATHS_FROM, 1, type); + } + + return final_damage; +} + +int32 Player::getMaxTimer(MirrorTimerType timer) const +{ + switch (timer) + { + case FATIGUE_TIMER: + return MINUTE * IN_MILLISECONDS; + case BREATH_TIMER: + { + if (!IsAlive() || HasAuraType(SPELL_AURA_WATER_BREATHING) || GetSession()->GetSecurity() >= AccountTypes(sWorld->getIntConfig(CONFIG_DISABLE_BREATHING))) + return DISABLED_MIRROR_TIMER; + int32 UnderWaterTime = 3 * MINUTE * IN_MILLISECONDS; + AuraEffectList const& mModWaterBreathing = GetAuraEffectsByType(SPELL_AURA_MOD_WATER_BREATHING); + for (AuraEffectList::const_iterator i = mModWaterBreathing.begin(); i != mModWaterBreathing.end(); ++i) + AddPct(UnderWaterTime, (*i)->GetAmount()); + return UnderWaterTime; + } + case FIRE_TIMER: + { + if (!IsAlive()) + return DISABLED_MIRROR_TIMER; + return 1 * IN_MILLISECONDS; + } + default: + return 0; + } +} + +void Player::UpdateMirrorTimers() +{ + // Desync flags for update on next HandleDrowning + if (m_MirrorTimerFlags) + m_MirrorTimerFlagsLast = ~m_MirrorTimerFlags; +} + +void Player::StopMirrorTimers() +{ + StopMirrorTimer(FATIGUE_TIMER); + StopMirrorTimer(BREATH_TIMER); + StopMirrorTimer(FIRE_TIMER); +} + +bool Player::IsMirrorTimerActive(MirrorTimerType type) const +{ + return m_MirrorTimer[type] == getMaxTimer(type); +} + +void Player::HandleDrowning(uint32 time_diff) +{ + if (!m_MirrorTimerFlags) + return; + + // In water + if (m_MirrorTimerFlags & UNDERWATER_INWATER) + { + // Breath timer not activated - activate it + if (m_MirrorTimer[BREATH_TIMER] == DISABLED_MIRROR_TIMER) + { + m_MirrorTimer[BREATH_TIMER] = getMaxTimer(BREATH_TIMER); + SendMirrorTimer(BREATH_TIMER, m_MirrorTimer[BREATH_TIMER], m_MirrorTimer[BREATH_TIMER], -1); + } + else // If activated - do tick + { + m_MirrorTimer[BREATH_TIMER] -= time_diff; + // Timer limit - need deal damage + if (m_MirrorTimer[BREATH_TIMER] < 0) + { + m_MirrorTimer[BREATH_TIMER] += 1 * IN_MILLISECONDS; + // Calculate and deal damage + /// @todo Check this formula + uint32 damage = GetMaxHealth() / 5 + urand(0, getLevel() - 1); + EnvironmentalDamage(DAMAGE_DROWNING, damage); + } + else if (!(m_MirrorTimerFlagsLast & UNDERWATER_INWATER)) // Update time in client if need + SendMirrorTimer(BREATH_TIMER, getMaxTimer(BREATH_TIMER), m_MirrorTimer[BREATH_TIMER], -1); + } + } + else if (m_MirrorTimer[BREATH_TIMER] != DISABLED_MIRROR_TIMER) // Regen timer + { + int32 UnderWaterTime = getMaxTimer(BREATH_TIMER); + // Need breath regen + m_MirrorTimer[BREATH_TIMER] += 10 * time_diff; + if (m_MirrorTimer[BREATH_TIMER] >= UnderWaterTime || !IsAlive()) + StopMirrorTimer(BREATH_TIMER); + else if (m_MirrorTimerFlagsLast & UNDERWATER_INWATER) + SendMirrorTimer(BREATH_TIMER, UnderWaterTime, m_MirrorTimer[BREATH_TIMER], 10); + } + + // In dark water + if (m_MirrorTimerFlags & UNDERWATER_INDARKWATER) + { + // Fatigue timer not activated - activate it + if (m_MirrorTimer[FATIGUE_TIMER] == DISABLED_MIRROR_TIMER) + { + m_MirrorTimer[FATIGUE_TIMER] = getMaxTimer(FATIGUE_TIMER); + SendMirrorTimer(FATIGUE_TIMER, m_MirrorTimer[FATIGUE_TIMER], m_MirrorTimer[FATIGUE_TIMER], -1); + } + else + { + m_MirrorTimer[FATIGUE_TIMER] -= time_diff; + // Timer limit - need deal damage or teleport ghost to graveyard + if (m_MirrorTimer[FATIGUE_TIMER] < 0) + { + m_MirrorTimer[FATIGUE_TIMER] += 1 * IN_MILLISECONDS; + if (IsAlive()) // Calculate and deal damage + { + uint32 damage = GetMaxHealth() / 5 + urand(0, getLevel() - 1); + EnvironmentalDamage(DAMAGE_EXHAUSTED, damage); + } + else if (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_GHOST)) // Teleport ghost to graveyard + RepopAtGraveyard(); + } + else if (!(m_MirrorTimerFlagsLast & UNDERWATER_INDARKWATER)) + SendMirrorTimer(FATIGUE_TIMER, getMaxTimer(FATIGUE_TIMER), m_MirrorTimer[FATIGUE_TIMER], -1); + } + } + else if (m_MirrorTimer[FATIGUE_TIMER] != DISABLED_MIRROR_TIMER) // Regen timer + { + int32 DarkWaterTime = getMaxTimer(FATIGUE_TIMER); + m_MirrorTimer[FATIGUE_TIMER] += 10 * time_diff; + if (m_MirrorTimer[FATIGUE_TIMER] >= DarkWaterTime || !IsAlive()) + StopMirrorTimer(FATIGUE_TIMER); + else if (m_MirrorTimerFlagsLast & UNDERWATER_INDARKWATER) + SendMirrorTimer(FATIGUE_TIMER, DarkWaterTime, m_MirrorTimer[FATIGUE_TIMER], 10); + } + + if (m_MirrorTimerFlags & (UNDERWATER_INLAVA /*| UNDERWATER_INSLIME*/) && !(_lastLiquid && _lastLiquid->SpellID)) + { + // Breath timer not activated - activate it + if (m_MirrorTimer[FIRE_TIMER] == DISABLED_MIRROR_TIMER) + m_MirrorTimer[FIRE_TIMER] = getMaxTimer(FIRE_TIMER); + else + { + m_MirrorTimer[FIRE_TIMER] -= time_diff; + if (m_MirrorTimer[FIRE_TIMER] < 0) + { + m_MirrorTimer[FIRE_TIMER] += 1 * IN_MILLISECONDS; + // Calculate and deal damage + /// @todo Check this formula + uint32 damage = urand(600, 700); + if (m_MirrorTimerFlags & UNDERWATER_INLAVA) + EnvironmentalDamage(DAMAGE_LAVA, damage); + // need to skip Slime damage in Undercity, + // maybe someone can find better way to handle environmental damage + //else if (m_zoneUpdateId != 1497) + // EnvironmentalDamage(DAMAGE_SLIME, damage); + } + } + } + else + m_MirrorTimer[FIRE_TIMER] = DISABLED_MIRROR_TIMER; + + // Recheck timers flag + m_MirrorTimerFlags &= ~UNDERWATER_EXIST_TIMERS; + for (uint8 i = 0; i < MAX_TIMERS; ++i) + if (m_MirrorTimer[i] != DISABLED_MIRROR_TIMER) + { + m_MirrorTimerFlags |= UNDERWATER_EXIST_TIMERS; + break; + } + m_MirrorTimerFlagsLast = m_MirrorTimerFlags; +} + +///The player sobers by 1% every 9 seconds +void Player::HandleSobering() +{ + m_drunkTimer = 0; + + uint8 currentDrunkValue = GetDrunkValue(); + uint8 drunk = currentDrunkValue ? --currentDrunkValue : 0; + SetDrunkValue(drunk); +} + +DrunkenState Player::GetDrunkenstateByValue(uint8 value) +{ + if (value >= 90) + return DRUNKEN_SMASHED; + if (value >= 50) + return DRUNKEN_DRUNK; + if (value) + return DRUNKEN_TIPSY; + return DRUNKEN_SOBER; +} + +void Player::SetDrunkValue(uint8 newDrunkValue, uint32 itemId /*= 0*/) +{ + bool isSobering = newDrunkValue < GetDrunkValue(); + uint32 oldDrunkenState = Player::GetDrunkenstateByValue(GetDrunkValue()); + if (newDrunkValue > 100) + newDrunkValue = 100; + + // select drunk percent or total SPELL_AURA_MOD_FAKE_INEBRIATE amount, whichever is higher for visibility updates + int32 drunkPercent = std::max(newDrunkValue, GetTotalAuraModifier(SPELL_AURA_MOD_FAKE_INEBRIATE)); + if (drunkPercent) + { + m_invisibilityDetect.AddFlag(INVISIBILITY_DRUNK); + m_invisibilityDetect.SetValue(INVISIBILITY_DRUNK, drunkPercent); + } + else if (!HasAuraType(SPELL_AURA_MOD_FAKE_INEBRIATE) && !newDrunkValue) + m_invisibilityDetect.DelFlag(INVISIBILITY_DRUNK); + + uint32 newDrunkenState = Player::GetDrunkenstateByValue(newDrunkValue); + SetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_INEBRIATION, newDrunkValue); + UpdateObjectVisibility(); + + if (!isSobering) + m_drunkTimer = 0; // reset sobering timer + + if (newDrunkenState == oldDrunkenState) + return; + + + WorldPackets::Misc::CrossedInebriationThreshold data; + data.Guid = GetGUID(); + data.Threshold = newDrunkenState; + data.ItemID = itemId; + + SendMessageToSet(data.Write(), true); +} + +void Player::Update(uint32 p_time) +{ + if (!IsInWorld()) + return; + + // undelivered mail + if (m_nextMailDelivereTime && m_nextMailDelivereTime <= GameTime::GetGameTime()) + { + SendNewMail(); + ++unReadMails; + + // It will be recalculate at mailbox open (for unReadMails important non-0 until mailbox open, it also will be recalculated) + m_nextMailDelivereTime = 0; + } + + // Update cinematic location, if 500ms have passed and we're doing a cinematic now. + _cinematicMgr->m_cinematicDiff += p_time; + if (_cinematicMgr->m_activeCinematicCameraId != 0 && GetMSTimeDiffToNow(_cinematicMgr->m_lastCinematicCheck) > CINEMATIC_UPDATEDIFF) + { + _cinematicMgr->m_lastCinematicCheck = GameTime::GetGameTimeMS(); + _cinematicMgr->UpdateCinematicLocation(p_time); + } + + //used to implement delayed far teleport + SetCanDelayTeleport(true); + Unit::Update(p_time); + SetCanDelayTeleport(false); + + time_t now = GameTime::GetGameTime(); + + UpdatePvPFlag(now); + + UpdateContestedPvP(p_time); + + UpdateDuelFlag(now); + + CheckDuelDistance(now); + + UpdateAfkReport(now); + + Unit::AIUpdateTick(p_time); + + // Once per second, update items that have just a limited lifetime + if (now > m_Last_tick) + { + UpdateItemDuration(uint32(now - m_Last_tick)); + UpdateSoulboundTradeItems(); + } + + // If mute expired, remove it from the DB + if (GetSession()->m_muteTime && GetSession()->m_muteTime < now) + { + GetSession()->m_muteTime = 0; + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_MUTE_TIME); + stmt->setInt64(0, 0); // Set the mute time to 0 + stmt->setString(1, ""); + stmt->setString(2, ""); + stmt->setUInt32(3, GetSession()->GetAccountId()); + LoginDatabase.Execute(stmt); + } + + if (!m_timedquests.empty()) + { + QuestSet::iterator iter = m_timedquests.begin(); + while (iter != m_timedquests.end()) + { + QuestStatusData& q_status = m_QuestStatus[*iter]; + if (q_status.Timer <= p_time) + { + uint32 quest_id = *iter; + ++iter; // current iter will be removed in FailQuest + FailQuest(quest_id); + } + else + { + q_status.Timer -= p_time; + m_QuestStatusSave[*iter] = QUEST_DEFAULT_SAVE_TYPE; + ++iter; + } + } + } + + m_achievementMgr->UpdateTimedAchievementCriteria(Milliseconds(p_time)); + + if (HasUnitState(UNIT_STATE_MELEE_ATTACKING) && !HasUnitState(UNIT_STATE_CASTING)) + { + if (Unit* victim = GetVictim()) + { + // default combat reach 10 + /// @todo add weapon, skill check + + if (isAttackReady(BASE_ATTACK)) + { + if (!IsWithinMeleeRange(victim)) + { + setAttackTimer(BASE_ATTACK, 100); + if (m_swingErrorMsg != 1) // send single time (client auto repeat) + { + SendAttackSwingNotInRange(); + m_swingErrorMsg = 1; + } + } + //120 degrees of radiant range + else if (!IsWithinBoundaryRadius(victim) && !HasInArc(2 * float(M_PI) / 3, victim)) + { + setAttackTimer(BASE_ATTACK, 100); + if (m_swingErrorMsg != 2) // send single time (client auto repeat) + { + SendAttackSwingBadFacingAttack(); + m_swingErrorMsg = 2; + } + } + else + { + m_swingErrorMsg = 0; // reset swing error state + + // prevent base and off attack in same time, delay attack at 0.2 sec + if (haveOffhandWeapon()) + if (getAttackTimer(OFF_ATTACK) < ATTACK_DISPLAY_DELAY) + setAttackTimer(OFF_ATTACK, ATTACK_DISPLAY_DELAY); + + // do attack + AttackerStateUpdate(victim, BASE_ATTACK); + resetAttackTimer(BASE_ATTACK); + } + } + + if (haveOffhandWeapon() && isAttackReady(OFF_ATTACK)) + { + if (!IsWithinMeleeRange(victim)) + setAttackTimer(OFF_ATTACK, 100); + else if (!IsWithinBoundaryRadius(victim) && !HasInArc(2 * float(M_PI) / 3, victim)) + { + setAttackTimer(OFF_ATTACK, 100); + } + else + { + // prevent base and off attack in same time, delay attack at 0.2 sec + if (getAttackTimer(BASE_ATTACK) < ATTACK_DISPLAY_DELAY) + setAttackTimer(BASE_ATTACK, ATTACK_DISPLAY_DELAY); + + // do attack + AttackerStateUpdate(victim, OFF_ATTACK); + resetAttackTimer(OFF_ATTACK); + } + } + + /*Unit* owner = victim->GetOwner(); + Unit* u = owner ? owner : victim; + if (u->IsPvP() && (!duel || duel->opponent != u)) + { + UpdatePvP(true); + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::PvPActive); + }*/ + } + } + + if (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING)) + { + if (roll_chance_i(3) && _restTime > 0) // freeze update + { + time_t currTime = GameTime::GetGameTime(); + time_t timeDiff = currTime - _restTime; + if (timeDiff >= 10) // freeze update + { + _restTime = currTime; + + float bubble = 0.125f * sWorld->getRate(RATE_REST_INGAME); + float extraPerSec = ((float)GetUInt32Value(PLAYER_NEXT_LEVEL_XP) / 72000.0f) * bubble; + + // speed collect rest bonus (section/in hour) + float currRestBonus = GetRestBonus(); + SetRestBonus(currRestBonus + timeDiff * extraPerSec); + } + } + } + + if (m_weaponChangeTimer > 0) + { + if (p_time >= m_weaponChangeTimer) + m_weaponChangeTimer = 0; + else + m_weaponChangeTimer -= p_time; + } + + // Power regeneration update + _powerUpdateTimer -= p_time; + if (IsAlive()) + RegenerateAll(p_time); + + // Reset the power update timer after regeneration happened + if (_powerUpdateTimer <= 0) + _powerUpdateTimer = GetPowerUpdateInterval(); + + if (m_deathState == JUST_DIED) + KillPlayer(); + + if (m_nextSave > 0) + { + if (p_time >= m_nextSave) + { + // m_nextSave reset in SaveToDB call + SaveToDB(); + TC_LOG_DEBUG("entities.player", "Player::Update: Player '%s' (%s) saved", GetName().c_str(), GetGUID().ToString().c_str());; + } + else + m_nextSave -= p_time; + } + + //Handle Water/drowning + HandleDrowning(p_time); + + // Played time + if (now > m_Last_tick) + { + uint32 elapsed = uint32(now - m_Last_tick); + m_Played_time[PLAYED_TIME_TOTAL] += elapsed; // Total played time + m_Played_time[PLAYED_TIME_LEVEL] += elapsed; // Level played time + m_Last_tick = now; + } + + if (GetDrunkValue()) + { + m_drunkTimer += p_time; + if (m_drunkTimer > 9 * IN_MILLISECONDS) + HandleSobering(); + } + + if (HasPendingBind()) + { + if (_pendingBindTimer <= p_time) + { + // Player left the instance + if (_pendingBindId == GetInstanceId()) + BindToInstance(); + SetPendingBind(0, 0); + } + else + _pendingBindTimer -= p_time; + } + + // not auto-free ghost from body in instances + if (m_deathTimer > 0 && !GetMap()->Instanceable() && !HasAuraType(SPELL_AURA_PREVENT_RESURRECTION)) + { + if (p_time >= m_deathTimer) + { + m_deathTimer = 0; + BuildPlayerRepop(); + RepopAtGraveyard(); + } + else + m_deathTimer -= p_time; + } + + UpdateEnchantTime(p_time); + UpdateHomebindTime(p_time); + + if (!_instanceResetTimes.empty()) + { + for (InstanceTimeMap::iterator itr = _instanceResetTimes.begin(); itr != _instanceResetTimes.end();) + { + if (itr->second < now) + _instanceResetTimes.erase(itr++); + else + ++itr; + } + } + + if (getClass() == CLASS_DEATH_KNIGHT) + { + // Update rune timers + for (uint8 i = 0; i < MAX_RUNES; ++i) + { + uint32 timer = GetRuneTimer(i); + + // Don't update timer if rune is disabled + if (GetRuneCooldown(i)) + continue; + + // Timer has began + if (timer < 0xFFFFFFFF) + { + timer += p_time; + SetRuneTimer(i, std::min(uint32(2500), timer)); + } + } + } + + Pet* pet = GetPet(); + if (pet && !pet->IsWithinDistInMap(this, GetMap()->GetVisibilityRange()) && !pet->isPossessed()) + //if (pet && !pet->IsWithinDistInMap(this, GetMap()->GetVisibilityDistance()) && (GetCharmGUID() && (pet->GetGUID() != GetCharmGUID()))) + RemovePet(pet, PET_SAVE_DISMISS, true); + + m_petScalingSynchTimer.Update(p_time); + if (m_petScalingSynchTimer.Passed()) + { + if (pet) + pet->UpdatePetScalingAuras(); + + m_petScalingSynchTimer.Reset(1000); + } + + if (IsAlive()) + { + if (m_hostileReferenceCheckTimer <= p_time) + { + m_hostileReferenceCheckTimer = 15 * IN_MILLISECONDS; + if (!GetMap()->IsDungeon()) + GetCombatManager().EndCombatBeyondRange(GetVisibilityRange(), true); + } + else + m_hostileReferenceCheckTimer -= p_time; + } + + //we should execute delayed teleports only for alive(!) players + //because we don't want player's ghost teleported from graveyard + if (IsHasDelayedTeleport() && IsAlive()) + TeleportTo(m_teleport_dest, m_teleport_options); + +} + + +void Player::Heartbeat() +{ + Unit::Heartbeat(); + + // Group update + SendUpdateToOutOfRangeGroupMembers(); + + // Updating Zone and AreaId. This will also trigger spell_area and phasing related updates + UpdateZoneAndAreaId(); + + // Updating auras which can only be used inside or outside (such as Mounts) + UpdateIndoorsOutdoorsAuras(); + + // Updating the resting state when entering resting places + UpdateTavernRestingState(); +} + +void Player::setDeathState(DeathState s) +{ + uint32 ressSpellId = 0; + + bool cur = IsAlive(); + + if (s == JUST_DIED) + { + if (!cur) + { + TC_LOG_ERROR("entities.player", "Player::setDeathState: Attempt to kill a dead player '%s' (%s)", GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + // drunken state is cleared on death + SetDrunkValue(0); + // lost combo points at any target (targeted combo points clear in Unit::setDeathState) + ClearComboPoints(); + + ClearResurrectRequestData(); + + //FIXME: is pet dismissed at dying or releasing spirit? if second, add setDeathState(DEAD) to HandleRepopRequestOpcode and define pet unsummon here with (s == DEAD) + RemovePet(nullptr, PET_SAVE_DISMISS, true); + + // save value before aura remove in Unit::setDeathState + ressSpellId = GetUInt32Value(PLAYER_SELF_RES_SPELL); + + // passive spell + if (!ressSpellId) + ressSpellId = GetResurrectionSpellId(); + + FailQuestsWithFlag(QUEST_FLAGS_COMPLETION_NO_DEATH); + + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DEATH_AT_MAP, 1); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DEATH, 1); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DEATH_IN_DUNGEON, 1); + + // reset all death criterias + FailAchievementCriteria(AchievementCriteriaFailEvent::Death, 0); + } + + Unit::setDeathState(s); + + // restore resurrection spell id for player after aura remove + if (s == JUST_DIED && cur && ressSpellId) + SetUInt32Value(PLAYER_SELF_RES_SPELL, ressSpellId); + + if (IsAlive() && !cur) + //clear aura case after resurrection by another way (spells will be applied before next death) + SetUInt32Value(PLAYER_SELF_RES_SPELL, 0); +} + +void Player::ToggleAFK() +{ + ToggleFlag(PLAYER_FLAGS, PLAYER_FLAGS_AFK); + + // afk player not allowed in battleground + if (!IsGameMaster() && isAFK() && InBattleground() && !InArena()) + LeaveBattleground(); +} + +void Player::ToggleDND() +{ + ToggleFlag(PLAYER_FLAGS, PLAYER_FLAGS_DND); +} + +uint8 Player::GetChatTag() const +{ + uint8 tag = CHAT_TAG_NONE; + + if (isGMChat()) + tag |= CHAT_TAG_GM; + if (isDND()) + tag |= CHAT_TAG_DND; + if (isAFK()) + tag |= CHAT_TAG_AFK; + if (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_DEVELOPER)) + tag |= CHAT_TAG_DEV; + + return tag; +} + +bool Player::TeleportTo(uint32 mapid, float x, float y, float z, float orientation, TeleportToOptions options /*= TELE_TO_NONE*/, Optional instanceId /*= {}*/) +{ + if (!MapManager::IsValidMapCoord(mapid, x, y, z, orientation)) + { + TC_LOG_ERROR("maps", "Player::TeleportTo: Invalid map (%d) or invalid coordinates (X: %f, Y: %f, Z: %f, O: %f) given when teleporting player '%s' (%s, MapID: %d, X: %f, Y: %f, Z: %f, O: %f).", + mapid, x, y, z, orientation, GetGUID().ToString().c_str(), GetName().c_str(), GetMapId(), GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation()); + return false; + } + + if (!GetSession()->HasPermission(rbac::RBAC_PERM_SKIP_CHECK_DISABLE_MAP) && DisableMgr::IsDisabledFor(DISABLE_TYPE_MAP, mapid, this)) + { + TC_LOG_INFO("entities.player.cheat", "Player::TeleportTo: Player '%s' (%s) tried to enter a forbidden map (MapID: %u)", GetGUID().ToString().c_str(), GetName().c_str(), mapid); + SendTransferAborted(mapid, TRANSFER_ABORT_MAP_NOT_ALLOWED); + return false; + } + + // preparing unsummon pet if lost (we must get pet before teleportation or will not find it later) + Pet* pet = GetPet(); + + MapEntry const* mEntry = sMapStore.LookupEntry(mapid); + + // don't let enter battlegrounds without assigned battleground id (for example through areatrigger)... + // don't let gm level > 1 either + if (!InBattleground() && mEntry->IsBattlegroundOrArena()) + return false; + + // client without expansion support + if (GetSession()->GetExpansion() < mEntry->Expansion()) + { + TC_LOG_DEBUG("maps", "Player '%s' (%s) using client without required expansion tried teleport to non accessible map (MapID: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), mapid); + + if (TransportBase* transport = GetTransport()) + { + transport->RemovePassenger(this); + RepopAtGraveyard(); // teleport to near graveyard if on transport, looks blizz like :) + } + + SendTransferAborted(mapid, TRANSFER_ABORT_INSUF_EXPAN_LVL, mEntry->Expansion()); + + return false; // normal client can't teleport to this map... + } + else + TC_LOG_DEBUG("maps", "Player %s (%s) is being teleported to map (MapID: %u)", GetName().c_str(), GetGUID().ToString().c_str(), mapid); + + if (m_vehicle) + ExitVehicle(); + + // reset movement flags at teleport, because player will continue move with these flags after teleport + SetUnitMovementFlags(GetUnitMovementFlags() & MOVEMENTFLAG_MASK_HAS_PLAYER_STATUS_OPCODE); + m_movementInfo.ResetJump(); + DisableSpline(); + + if (TransportBase* transport = GetTransport()) + { + if (!(options & TELE_TO_NOT_LEAVE_TRANSPORT)) + transport->RemovePassenger(this); + } + + // The player was ported to another map and loses the duel immediately. + // We have to perform this check before the teleport, otherwise the + // ObjectAccessor won't find the flag. + if (duel && GetMapId() != mapid && GetMap()->GetGameObject(GetGuidValue(PLAYER_DUEL_ARBITER))) + DuelComplete(DUEL_FLED); + + if (GetMapId() == mapid && (!instanceId || GetInstanceId() == instanceId)) + { + //lets reset far teleport flag if it wasn't reset during chained teleport + SetSemaphoreTeleportFar(false); + //setup delayed teleport flag + SetDelayedTeleportFlag(IsCanDelayTeleport()); + //if teleport spell is cast in Unit::Update() func + //then we need to delay it until update process will be finished + if (IsHasDelayedTeleport()) + { + SetSemaphoreTeleportNear(true); + //lets save teleport destination for player + m_teleport_dest = WorldLocation(mapid, x, y, z, orientation); + m_teleport_instanceId = {}; + m_teleport_options = options; + return true; + } + + if (!(options & TELE_TO_NOT_UNSUMMON_PET)) + { + //same map, only remove pet if out of range for new position + if (pet && !pet->IsWithinDist3d(x, y, z, GetMap()->GetVisibilityRange())) + UnsummonPetTemporaryIfAny(); + } + + if (!(options & TELE_TO_NOT_LEAVE_COMBAT)) + CombatStop(); + + // this will be used instead of the current location in SaveToDB + m_teleport_dest = WorldLocation(mapid, x, y, z, orientation); + m_teleport_instanceId = {}; + m_teleport_options = options; + SetFallInformation(0, GetPositionZ()); + + // code for finish transfer called in WorldSession::HandleMovementOpcodes() + // at client packet CMSG_MOVE_TELEPORT_ACK + SetSemaphoreTeleportNear(true); + // near teleport, triggering send CMSG_MOVE_TELEPORT_ACK from client at landing + if (!GetSession()->PlayerLogout()) + { + m_teleport_dest.m_positionZ += GetHoverOffset(); + SendTeleportPacket(m_teleport_dest); + } + } + else + { + if (!IsGameMaster()) + { + if (getClass() == CLASS_DEATH_KNIGHT && GetMapId() == 609 && !HasSpell(50977)) + { + SendTransferAborted(mapid, TRANSFER_ABORT_UNIQUE_MESSAGE, AsUnderlyingType(TransferAbortedUniqueMessageArgs::StillWithinTheLichKingsGrasp)); + return false; + } + else if (getRace() == RACE_GOBLIN && GetMapId() == 648 && GetQuestStatus(25265) != QUEST_STATUS_REWARDED) + { + SendTransferAborted(mapid, TRANSFER_ABORT_UNIQUE_MESSAGE, AsUnderlyingType(TransferAbortedUniqueMessageArgs::DestinyOfTheBilgewaterCartelStillUndecided)); + return false; + } + else if (getRace() == RACE_WORGEN && GetMapId() == 654 && GetQuestStatus(26706) != QUEST_STATUS_REWARDED) + { + SendTransferAborted(mapid, TRANSFER_ABORT_UNIQUE_MESSAGE, AsUnderlyingType(TransferAbortedUniqueMessageArgs::FateOfGilneasHasNotBeenDecidedYet)); + return false; + } + } + + // far teleport to another map + Map* oldmap = IsInWorld() ? GetMap() : nullptr; + + // check if we can enter before stopping combat / removing pet / totems / interrupting spells + + // Check enter rights before map getting to avoid creating instance copy for player + // this check not dependent from map instance copy and same for all instance copies of selected map + if (Map::PlayerCannotEnter(mapid, this, false)) + return false; + + //I think this always returns true. Correct me if I am wrong. + // If the map is not created, assume it is possible to enter it. + // It will be created in the WorldPortAck. + //Map* map = sMapMgr->FindBaseNonInstanceMap(mapid); + //if (!map || map->CanEnter(this)) + { + //lets reset near teleport flag if it wasn't reset during chained teleports + SetSemaphoreTeleportNear(false); + //setup delayed teleport flag + SetDelayedTeleportFlag(IsCanDelayTeleport()); + //if teleport spell is cast in Unit::Update() func + //then we need to delay it until update process will be finished + if (IsHasDelayedTeleport()) + { + SetSemaphoreTeleportFar(true); + //lets save teleport destination for player + m_teleport_dest = WorldLocation(mapid, x, y, z, orientation); + m_teleport_instanceId = instanceId; + m_teleport_options = options; + return true; + } + + SetSelection(ObjectGuid::Empty); + + CombatStop(); + + ResetContestedPvP(); + + // remove player from battleground on far teleport (when changing maps) + if (Battleground const* bg = GetBattleground()) + { + // Note: at battleground join battleground id set before teleport + // and we already will found "current" battleground + // just need check that this is targeted map or leave + if (bg->GetMapId() != mapid) + LeaveBattleground(false); // don't teleport to entry point + } + + // remove arena spell coldowns/buffs now to also remove pet's cooldowns before it's temporarily unsummoned + if (mEntry->IsBattleArena() && !IsGameMaster()) + { + RemoveArenaSpellCooldowns(true); + RemoveArenaAuras(); + if (pet) + pet->RemoveArenaAuras(); + } + + // remove pet on map change + if (pet) + UnsummonPetTemporaryIfAny(); + + // remove all dyn objects + RemoveAllDynObjects(); + + // stop spellcasting + // not attempt interrupt teleportation spell at caster teleport + if (!(options & TELE_TO_SPELL)) + if (IsNonMeleeSpellCast(true)) + InterruptNonMeleeSpells(true); + + //remove auras before removing from map... + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Moving | SpellAuraInterruptFlags::Turning); + + if (!GetSession()->PlayerLogout()) + { + // send transfer packets + WorldPackets::Movement::TransferPending transferPending; + transferPending.MapID = mapid; + if (Transport* transport = dynamic_cast(GetTransport())) + { + transferPending.Ship.emplace(); + transferPending.Ship->ID = transport->GetEntry(); + transferPending.Ship->OriginMapID = GetMapId(); + } + + SendDirectMessage(transferPending.Write()); + } + + // remove from old map now + if (oldmap) + oldmap->RemovePlayerFromMap(this, false); + + // players on mount will be dismounted. the speed and height change should not require an ACK and should be applied directly + PurgeAndApplyPendingMovementChanges(false); + + m_teleport_dest = WorldLocation(mapid, x, y, z, orientation); + m_teleport_instanceId = instanceId; + m_teleport_options = options; + SetFallInformation(0, GetPositionZ()); + // if the player is saved before worldportack (at logout for example) + // this will be used instead of the current location in SaveToDB + + if (!GetSession()->PlayerLogout()) + { + WorldPackets::Movement::SuspendToken suspendToken; + suspendToken.SequenceIndex = 1; // to-do implement incremental value + suspendToken.Reason = 1; + SendDirectMessage(suspendToken.Write()); + } + + // move packet sent by client always after far teleport + // code for finish transfer to new map called in WorldSession::HandleMoveWorldportAckOpcode at client packet + SetSemaphoreTeleportFar(true); + } + //else + // return false; + } + return true; +} + +bool Player::TeleportTo(WorldLocation const& loc, TeleportToOptions options /*= TELE_TO_NONE*/, Optional instanceId /*= {}*/) +{ + return TeleportTo(loc.GetMapId(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ(), loc.GetOrientation(), options, instanceId); +} + +bool Player::TeleportToBGEntryPoint() +{ + if (m_bgData.joinPos.m_mapId == MAPID_INVALID) + return false; + + ScheduleDelayedOperation(DELAYED_BG_MOUNT_RESTORE); + ScheduleDelayedOperation(DELAYED_BG_TAXI_RESTORE); + ScheduleDelayedOperation(DELAYED_BG_GROUP_RESTORE); + return TeleportTo(m_bgData.joinPos); +} + +void Player::ProcessDelayedOperations() +{ + if (m_DelayedOperations == 0) + return; + + if (m_DelayedOperations & DELAYED_RESURRECT_PLAYER) + { + ResurrectPlayer(0.0f, false); + + if (GetMaxHealth() > _resurrectionData->Health) + SetHealth(_resurrectionData->Health); + else + SetFullHealth(); + + if (uint32(GetMaxPower(POWER_MANA)) > _resurrectionData->Mana) + SetPower(POWER_MANA, _resurrectionData->Mana); + else + SetPower(POWER_MANA, GetMaxPower(POWER_MANA)); + + SetPower(POWER_RAGE, 0); + SetPower(POWER_ENERGY, GetMaxPower(POWER_ENERGY)); + SetPower(POWER_ECLIPSE, 0); + + if (uint32 aura = _resurrectionData->Aura) + { + CastSpellExtraArgs args(TRIGGERED_FULL_MASK); + args.SetOriginalCaster(_resurrectionData->GUID); + CastSpell(this, aura, args); + } + + SpawnCorpseBones(); + } + + if (m_DelayedOperations & DELAYED_SAVE_PLAYER) + SaveToDB(); + + if (m_DelayedOperations & DELAYED_SPELL_CAST_DESERTER) + CastSpell(this, 26013, true); // Deserter + + if (m_DelayedOperations & DELAYED_BG_MOUNT_RESTORE) + { + if (m_bgData.mountSpell) + { + CastSpell(this, m_bgData.mountSpell, true); + m_bgData.mountSpell = 0; + } + } + + if (m_DelayedOperations & DELAYED_BG_TAXI_RESTORE) + { + if (m_bgData.HasTaxiPath()) + { + m_taxi.AddTaxiDestination(m_bgData.taxiPath[0]); + m_taxi.AddTaxiDestination(m_bgData.taxiPath[1]); + m_bgData.ClearTaxiPath(); + + ContinueTaxiFlight(); + } + } + + if (m_DelayedOperations & DELAYED_BG_GROUP_RESTORE) + { + if (Group* g = GetGroup()) + g->SendUpdateToPlayer(GetGUID()); + } + + //we have executed ALL delayed ops, so clear the flag + m_DelayedOperations = 0; +} + +void Player::AddToWorld() +{ + ///- Do not add/remove the player from the object storage + ///- It will crash when updating the ObjectAccessor + ///- The player should only be added when logging in + Unit::AddToWorld(); + + for (uint8 i = PLAYER_SLOT_START; i < PLAYER_SLOT_END; ++i) + if (m_items[i]) + m_items[i]->AddToWorld(); +} + +void Player::RemoveFromWorld() +{ + // cleanup + if (IsInWorld()) + { + ///- Release charmed creatures, unsummon totems and remove pets/guardians + StopCastingCharm(); + StopCastingBindSight(); + UnsummonPetTemporaryIfAny(); + ClearComboPoints(); + ClearComboPointHolders(); + ObjectGuid lootGuid = GetLootGUID(); + if (!lootGuid.IsEmpty()) + m_session->DoLootRelease(lootGuid); + sOutdoorPvPMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId); + sBattlefieldMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId); + } + + // Remove items from world before self - player must be found in Item::RemoveFromObjectUpdate + for (uint8 i = PLAYER_SLOT_START; i < PLAYER_SLOT_END; ++i) + { + if (m_items[i]) + m_items[i]->RemoveFromWorld(); + } + + ///- Do not add/remove the player from the object storage + ///- It will crash when updating the ObjectAccessor + ///- The player should only be removed when logging out + Unit::RemoveFromWorld(); + + for (ItemMap::iterator iter = mMitems.begin(); iter != mMitems.end(); ++iter) + iter->second->RemoveFromWorld(); + + if (m_uint32Values) + { + if (WorldObject* viewpoint = GetViewpoint()) + { + TC_LOG_ERROR("entities.player", "Player::RemoveFromWorld: Player '%s' (%s) has viewpoint (Entry:%u, Type: %u) when removed from world", + GetName().c_str(), GetGUID().ToString().c_str(), viewpoint->GetEntry(), viewpoint->GetTypeId()); + SetViewpoint(viewpoint, false); + } + } +} + +void Player::SetObjectScale(float scale) +{ + Unit::SetObjectScale(scale); + SetFloatValue(UNIT_FIELD_BOUNDINGRADIUS, scale * DEFAULT_PLAYER_BOUNDING_RADIUS); + SetFloatValue(UNIT_FIELD_COMBATREACH, scale * DEFAULT_PLAYER_COMBAT_REACH); + if (IsInWorld()) + SendMovementSetCollisionHeight(GetCollisionHeight(), UPDATE_COLLISION_HEIGHT_SCALE); +} + +bool Player::IsImmunedToSpellEffect(SpellInfo const* spellInfo, uint32 index, WorldObject const* caster, + bool requireImmunityPurgesEffectAttribute /*= false*/) const +{ + // players are immune to taunt (the aura and the spell effect) + if (spellInfo->Effects[index].IsAura(SPELL_AURA_MOD_TAUNT)) + return true; + if (spellInfo->Effects[index].IsEffect(SPELL_EFFECT_ATTACK_ME)) + return true; + + return Unit::IsImmunedToSpellEffect(spellInfo, index, caster, requireImmunityPurgesEffectAttribute); +} + +void Player::RegenerateAll(uint32 diff) +{ + Regenerate(POWER_ENERGY, diff); + Regenerate(POWER_MANA, diff); + Regenerate(POWER_FOCUS, diff); + Regenerate(POWER_HOLY_POWER, diff); + Regenerate(POWER_RUNIC_POWER, diff); + Regenerate(POWER_RAGE, diff); + Regenerate(POWER_ALTERNATE_POWER, diff); + + // Runes act as cooldowns, and they don't need to send any data + if (getClass() == CLASS_DEATH_KNIGHT) + { + for (uint8 i = 0; i < MAX_RUNES; i += 2) + { + uint8 runeToRegen = i; + RuneType runeType = GetCurrentRune(runeToRegen); + float runeCooldown = GetRuneCooldown(i); + float secondRuneCd = GetRuneCooldown(i + 1); + + // Regenerate second rune of the same type only after first rune is off the cooldown + if (G3D::fuzzyNe(secondRuneCd, 0.0f) && (runeCooldown > secondRuneCd || G3D::fuzzyEq(runeCooldown, 0.0f))) + { + runeToRegen = i + 1; + runeType = GetCurrentRune(runeToRegen); + runeCooldown = secondRuneCd; + } + + // No cooldown, nothing to do + if (G3D::fuzzyEq(runeCooldown, 0.0f)) + continue; + + float cdDiff = GetFloatValue(PLAYER_RUNE_REGEN_1 + AsUnderlyingType(runeType)) * 0.001f * diff; + if (runeCooldown > cdDiff) + SetRuneCooldown(runeToRegen, runeCooldown - cdDiff); + else + SetRuneCooldown(runeToRegen, 0.0f); + } + } +} + +void Player::RegenerateHealth() +{ + // Do not regenerate health when in combat, when not polymorphed or when any regeneration aura allows it + if (IsInCombat() && !IsPolymorphed() && !m_baseHealthRegen && !HasAuraType(SPELL_AURA_MOD_REGEN_DURING_COMBAT) && !HasAuraType(SPELL_AURA_MOD_HEALTH_REGEN_IN_COMBAT)) + return; + + uint32 curValue = GetHealth(); + uint32 maxValue = GetMaxHealth(); + + if (curValue >= maxValue) + return; + + float HealthIncreaseRate = sWorld->getRate(RATE_HEALTH); + float addValue = 0.0f; + + // polymorphed case + if (IsPolymorphed()) + addValue = float(GetMaxHealth()) / 3.0f; + // normal regen case (maybe partly in combat case) + else if (!IsInCombat() || HasAuraType(SPELL_AURA_MOD_REGEN_DURING_COMBAT)) + { + addValue = HealthIncreaseRate; + + if (!IsInCombat()) + { + if (getLevel() < 15) + addValue = (0.20f * ((float)GetMaxHealth()) / getLevel() * HealthIncreaseRate); + else + addValue = 0.015f * ((float)GetMaxHealth()) * HealthIncreaseRate; + + AuraEffectList const& mModHealthRegenPct = GetAuraEffectsByType(SPELL_AURA_MOD_HEALTH_REGEN_PERCENT); + for (AuraEffectList::const_iterator i = mModHealthRegenPct.begin(); i != mModHealthRegenPct.end(); ++i) + AddPct(addValue, (*i)->GetAmount()); + + addValue += GetTotalAuraModifier(SPELL_AURA_MOD_REGEN) * 0.4f; + } + else if (HasAuraType(SPELL_AURA_MOD_REGEN_DURING_COMBAT)) + ApplyPct(addValue, GetTotalAuraModifier(SPELL_AURA_MOD_REGEN_DURING_COMBAT)); + + if (!IsStandState()) + addValue *= 1.5f; + } + + // always regeneration bonus (including combat) + addValue += GetTotalAuraModifier(SPELL_AURA_MOD_HEALTH_REGEN_IN_COMBAT); + addValue += m_baseHealthRegen / 2.5f; + + if (addValue < 0.0f) + addValue = 0.0f; + + ModifyHealth(int32(addValue)); +} + +void Player::ResetAllPowers() +{ + SetFullHealth(); + switch (GetPowerType()) + { + case POWER_MANA: + SetFullPower(POWER_MANA); + break; + case POWER_RAGE: + SetPower(POWER_RAGE, 0); + break; + case POWER_ENERGY: + SetFullPower(POWER_ENERGY); + break; + case POWER_RUNIC_POWER: + SetPower(POWER_RUNIC_POWER, 0); + break; + case POWER_ECLIPSE: + SetPower(POWER_ECLIPSE, 0); + break; + default: + break; + } +} + +bool Player::CanInteractWithQuestGiver(Object* questGiver) const +{ + switch (questGiver->GetTypeId()) + { + case TYPEID_UNIT: + return GetNPCIfCanInteractWith(questGiver->GetGUID(), UNIT_NPC_FLAG_QUESTGIVER) != nullptr; + case TYPEID_GAMEOBJECT: + return GetGameObjectIfCanInteractWith(questGiver->GetGUID(), GAMEOBJECT_TYPE_QUESTGIVER) != nullptr; + case TYPEID_PLAYER: + return IsAlive() && questGiver->ToPlayer()->IsAlive(); + case TYPEID_ITEM: + return IsAlive(); + default: + break; + } + return false; +} + +Creature* Player::GetNPCIfCanInteractWith(ObjectGuid const& guid, uint32 npcflagmask) const +{ + // unit checks + if (!guid) + return nullptr; + + if (!IsInWorld()) + return nullptr; + + if (IsInFlight()) + return nullptr; + + // exist (we need look pets also for some interaction (quest/etc) + Creature* creature = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, guid); + if (!creature) + return nullptr; + + // Deathstate checks + if (!IsAlive() && !(creature->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_GHOST_VISIBLE)) + return nullptr; + + // alive or spirit healer + if (!creature->IsAlive() && !(creature->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_CAN_INTERACT_WHILE_DEAD)) + return nullptr; + + // appropriate npc type + if (npcflagmask && !creature->HasFlag(UNIT_NPC_FLAGS, npcflagmask)) + return nullptr; + + // not allow interaction under control, but allow with own pets + if (creature->GetCharmerGUID()) + return nullptr; + + // not unfriendly/hostile + if (creature->GetReactionTo(this) <= REP_UNFRIENDLY) + return nullptr; + + // not too far, taken from CGGameUI::SetInteractTarget + if (!creature->IsWithinDistInMap(this, creature->GetCombatReach() + 4.0f)) + return nullptr; + + return creature; +} + +GameObject* Player::GetGameObjectIfCanInteractWith(ObjectGuid const& guid) const +{ + if (!guid) + return nullptr; + + if (!IsInWorld()) + return nullptr; + + if (IsInFlight()) + return nullptr; + + // exist + GameObject* go = ObjectAccessor::GetGameObject(*this, guid); + if (!go) + return nullptr; + + if (!go->IsWithinDistInMap(this)) + return nullptr; + + return go; +} + +GameObject* Player::GetGameObjectIfCanInteractWith(ObjectGuid const& guid, GameobjectTypes type) const +{ + GameObject* go = GetGameObjectIfCanInteractWith(guid); + if (!go) + return nullptr; + + if (go->GetGoType() != type) + return nullptr; + + return go; +} + +bool Player::IsInAreaTriggerRadius(AreaTriggerEntry const* trigger) const +{ + if (!trigger) + return false; + + if (GetMapId() != trigger->ContinentID && !GetPhaseShift().HasVisibleMapId(trigger->ContinentID)) + return false; + + if (trigger->PhaseID || trigger->PhaseGroupID || trigger->PhaseUseFlags) + if (!PhasingHandler::InDbPhaseShift(this, trigger->PhaseUseFlags, trigger->PhaseID, trigger->PhaseGroupID)) + return false; + + if (trigger->Radius > 0.f) + { + // if we have radius check it + float dist = GetDistance(trigger->Pos.X, trigger->Pos.Y, trigger->Pos.Z); + if (dist > trigger->Radius) + return false; + } + else + { + Position center(trigger->Pos.X, trigger->Pos.Y, trigger->Pos.Z, trigger->Box_yaw); + if (!IsWithinBox(center, trigger->Box_length / 2.f, trigger->Box_width / 2.f, trigger->Box_height / 2.f)) + return false; + } + + return true; +} + +void Player::SetGameMaster(bool on) +{ + if (on) + { + m_ExtraFlags |= PLAYER_EXTRA_GM_ON; + SetFaction(FACTION_FRIENDLY); + AddCharacterFlag(CHARACTER_FLAG_GM_MODE); + SetFlag(PLAYER_FLAGS, PLAYER_FLAGS_GM); + SetFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_ALLOW_CHEAT_SPELLS); + + if (Pet* pet = GetPet()) + pet->SetFaction(FACTION_FRIENDLY); + + RemoveByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_FFA_PVP); + ResetContestedPvP(); + + CombatStopWithPets(); + + PhasingHandler::SetAlwaysVisible(GetPhaseShift(), true); + m_serverSideVisibilityDetect.SetValue(SERVERSIDE_VISIBILITY_GM, GetSession()->GetSecurity()); + } + else + { + PhasingHandler::SetAlwaysVisible(GetPhaseShift(), false); + + m_ExtraFlags &= ~ PLAYER_EXTRA_GM_ON; + SetFactionForRace(getRace()); + RemoveCharacterFlag(CHARACTER_FLAG_GM_MODE); + RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_GM); + RemoveFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_ALLOW_CHEAT_SPELLS); + + if (Pet* pet = GetPet()) + pet->SetFaction(GetFaction()); + + // restore FFA PvP Server state + if (sWorld->IsFFAPvPRealm()) + SetByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_FFA_PVP); + + // restore FFA PvP area state, remove not allowed for GM mounts + UpdateArea(m_areaUpdateId); + + m_serverSideVisibilityDetect.SetValue(SERVERSIDE_VISIBILITY_GM, SEC_PLAYER); + } + + UpdateObjectVisibility(); +} + +bool Player::CanBeGameMaster() const +{ + return GetSession()->HasPermission(rbac::RBAC_PERM_COMMAND_GM); +} + +void Player::SetGMVisible(bool on) +{ + if (on) + { + m_ExtraFlags &= ~PLAYER_EXTRA_GM_INVISIBLE; //remove flag + m_serverSideVisibility.SetValue(SERVERSIDE_VISIBILITY_GM, SEC_PLAYER); + } + else + { + m_ExtraFlags |= PLAYER_EXTRA_GM_INVISIBLE; //add flag + + SetAcceptWhispers(false); + SetGameMaster(true); + + m_serverSideVisibility.SetValue(SERVERSIDE_VISIBILITY_GM, GetSession()->GetSecurity()); + } + + for (Channel* channel : m_channels) + channel->SetInvisible(this, !on); +} + +bool Player::IsGroupVisibleFor(Player const* p) const +{ + switch (sWorld->getIntConfig(CONFIG_GROUP_VISIBILITY)) + { + default: return IsInSameGroupWith(p); + case 1: return IsInSameRaidWith(p); + case 2: return GetTeam() == p->GetTeam(); + case 3: return false; + } +} + +bool Player::IsInSameGroupWith(Player const* p) const +{ + return p == this || (GetGroup() != nullptr && + GetGroup() == p->GetGroup() && + GetGroup()->SameSubGroup(this, p)); +} + +bool Player::IsInSameRaidWith(Player const* p) const +{ + return p == this || (GetGroup() != nullptr && GetGroup() == p->GetGroup()); +} + +///- If the player is invited, remove him. If the group if then only 1 person, disband the group. +void Player::UninviteFromGroup() +{ + Group* group = GetGroupInvite(); + if (!group) + return; + + group->RemoveInvite(this); + + if (group->IsCreated()) + { + if (group->GetMembersCount() <= 1) // group has just 1 member => disband + group->Disband(true); + } + else + { + if (group->GetInviteeCount() <= 1) + { + group->RemoveAllInvites(); + delete group; + } + } +} + +void Player::RemoveFromGroup(Group* group, ObjectGuid guid, RemoveMethod method /*= GROUP_REMOVEMETHOD_DEFAULT*/, ObjectGuid kicker /*= ObjectGuid::Empty*/, char const* reason /*= nullptr*/) +{ + if (!group) + return; + + group->RemoveMember(guid, method, kicker, reason); +} + +void Player::GiveXP(uint32 xp, Unit* victim, float group_rate) +{ + if (xp < 1) + return; + + if (!IsAlive() && !GetBattlegroundId()) + return; + + if (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_NO_XP_GAIN)) + return; + + if (victim && victim->GetTypeId() == TYPEID_UNIT && !victim->ToCreature()->hasLootRecipient()) + return; + + uint8 level = getLevel(); + + sScriptMgr->OnGivePlayerXP(this, xp, victim); + + // XP to money conversion processed in Player::RewardQuest + if (IsMaxLevel()) + return; + + uint32 bonus_xp; + bool recruitAFriend = GetsRecruitAFriendBonus(true); + + // RaF does NOT stack with rested experience + if (recruitAFriend) + bonus_xp = 2 * xp; // xp + bonus_xp must add up to 3 * xp for RaF; calculation for quests done client-side + else + bonus_xp = victim ? GetXPRestBonus(xp) : 0; // XP resting bonus + + WorldPackets::Character::LogXPGain packet; + packet.Victim = victim ? victim->GetGUID() : ObjectGuid::Empty; + packet.Original = xp + bonus_xp; + packet.Reason = victim ? LOG_XP_REASON_KILL : LOG_XP_REASON_NO_KILL; + packet.Amount = xp; + packet.GroupBonus = group_rate; + packet.ReferAFriendBonusType = recruitAFriend ? 1 : 0; + SendDirectMessage(packet.Write()); + + uint32 curXP = GetUInt32Value(PLAYER_XP); + uint32 nextLvlXP = GetUInt32Value(PLAYER_NEXT_LEVEL_XP); + uint32 newXP = curXP + xp + bonus_xp; + + while (newXP >= nextLvlXP && !IsMaxLevel()) + { + newXP -= nextLvlXP; + + if (!IsMaxLevel()) + GiveLevel(level + 1); + + level = getLevel(); + nextLvlXP = GetUInt32Value(PLAYER_NEXT_LEVEL_XP); + } + + SetUInt32Value(PLAYER_XP, newXP); +} + +// Update player to next level +// Current player experience not update (must be update by caller) +void Player::GiveLevel(uint8 level) +{ + uint8 oldLevel = getLevel(); + if (level == oldLevel) + return; + + if (Guild* guild = GetGuild()) + guild->UpdateMemberData(this, GUILD_MEMBER_DATA_LEVEL, level); + + PlayerLevelInfo info; + sObjectMgr->GetPlayerLevelInfo(getRace(), getClass(), level, &info); + + uint32 basehp = 0, basemana = 0; + sObjectMgr->GetPlayerClassLevelInfo(getClass(), level, basehp, basemana); + + + WorldPackets::Misc::LevelUpInfo packet; + packet.Level = level; + packet.HealthDelta = 0; + + /// @todo find some better solution + // for (int i = 0; i < MAX_STORED_POWERS; ++i) + packet.PowerDelta[0] = int32(basemana) - int32(GetCreateMana()); + packet.PowerDelta[1] = 0; + packet.PowerDelta[2] = 0; + packet.PowerDelta[3] = 0; + packet.PowerDelta[4] = 0; + packet.PowerDelta[5] = 0; + + for (StatType stat : AllPrimaryStats) + packet.StatDelta[AsUnderlyingType(stat)] = int32(info.stats[AsUnderlyingType(stat)]) - GetStats().GetBaseStatValue(stat); + + SendDirectMessage(packet.Write()); + + SetUInt32Value(PLAYER_NEXT_LEVEL_XP, sObjectMgr->GetXPForLevel(level)); + + //update level, max level of skills + m_Played_time[PLAYED_TIME_LEVEL] = 0; // Level Played Time reset + + _ApplyAllLevelScaleItemMods(false); + + SetLevel(level); + + UpdateSkillsForLevel(); + + SetCreateHealth(basehp); + SetCreateMana(basemana); + + // save base values (bonuses already included in stored stats) + for (StatType stat : AllPrimaryStats) + GetStats().SetBaseStatValue(stat, info.stats[AsUnderlyingType(stat)]); + + InitTalentForLevel(); + InitTaxiNodesForLevel(); + InitGlyphsForLevel(); + + UpdateAllRatings(); + UpdateAllResistances(); + + _ApplyAllLevelScaleItemMods(true); // Moved to above SetFullHealth so player will have full health from Heirlooms + + // set current level health and mana/energy to maximum after applying all mods. + SetFullHealth(); + SetFullPower(POWER_MANA); + + // update level to hunter/summon pet + if (Pet* pet = GetPet()) + pet->SynchronizeLevelWithOwner(); + + //Update QuestGivers + SendQuestGiverStatusMultiple(); + + if (MailLevelReward const* mailReward = sObjectMgr->GetMailLevelReward(level, getRaceMask())) + { + /// @todo Poor design of mail system + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + MailDraft(mailReward->mailTemplateId).SendMailTo(trans, this, MailSender(MAIL_CREATURE, mailReward->senderEntry)); + CharacterDatabase.CommitTransaction(trans); + } + + StartAchievementCriteria(AchievementCriteriaStartEvent::ReachLevel, level); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_LEVEL); + + // Refer-A-Friend + if (GetSession()->GetRecruiterId()) + if (level < sWorld->getIntConfig(CONFIG_MAX_RECRUIT_A_FRIEND_BONUS_PLAYER_LEVEL)) + if (level % 2 == 0) + { + ++m_grantableLevels; + + if (!HasByteFlag(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTES_OFFSET_RAF_GRANTABLE_LEVEL, 0x01)) + SetByteFlag(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTES_OFFSET_RAF_GRANTABLE_LEVEL, 0x01); + } + + sScriptMgr->OnPlayerLevelChanged(this, oldLevel); +} + +bool Player::IsMaxLevel() const +{ + return getLevel() >= GetUInt32Value(PLAYER_FIELD_MAX_LEVEL); +} + +void Player::InitTalentForLevel() +{ + uint8 level = getLevel(); + // talents base at level diff (talents = level - 9 but some can be used already) + if (level < 10) + { + // Remove all talent points + if (GetUsedTalentCount() > 0) // Free any used talents + { + ResetTalents(true); + SetFreeTalentPoints(0); + } + } + else + { + if (level < sWorld->getIntConfig(CONFIG_MIN_DUALSPEC_LEVEL) || GetSpecsCount() == 0) + { + SetSpecsCount(1); + SetActiveSpec(0); + } + + uint32 talentPointsForLevel = CalculateTalentsPoints(); + + // if used more that have then reset + if (GetUsedTalentCount() > talentPointsForLevel) + { + if (!GetSession()->HasPermission(rbac::RBAC_PERM_SKIP_CHECK_MORE_TALENTS_THAN_ALLOWED)) + ResetTalents(true); + else + SetFreeTalentPoints(0); + } + // else update amount of free points + else + SetFreeTalentPoints(talentPointsForLevel - GetUsedTalentCount()); + } + + if (!GetSession()->PlayerLoading()) + SendTalentsInfoData(false); // update at client +} + +void Player::InitStatsForLevel(bool reapplyMods) +{ + if (reapplyMods) //reapply stats values only on .reset stats (level) command + _RemoveAllStatBonuses(); + + uint32 basehp = 0, basemana = 0; + sObjectMgr->GetPlayerClassLevelInfo(getClass(), getLevel(), basehp, basemana); + + PlayerLevelInfo info; + sObjectMgr->GetPlayerLevelInfo(getRace(), getClass(), getLevel(), &info); + + uint8 exp_max_lvl = DBCManager::GetMaxLevelForExpansion(GetSession()->GetExpansion()); + uint8 conf_max_lvl = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); + if (exp_max_lvl == DEFAULT_MAX_LEVEL || exp_max_lvl >= conf_max_lvl) + SetUInt32Value(PLAYER_FIELD_MAX_LEVEL, conf_max_lvl); + else + SetUInt32Value(PLAYER_FIELD_MAX_LEVEL, exp_max_lvl); + SetUInt32Value(PLAYER_NEXT_LEVEL_XP, sObjectMgr->GetXPForLevel(getLevel())); + + // reset before any aura state sources (health set/aura apply) + SetUInt32Value(UNIT_FIELD_AURASTATE, 0); + + UpdateSkillsForLevel(); + + // set default cast time multiplier + SetFloatValue(UNIT_MOD_CAST_SPEED, 1.0f); + SetFloatValue(UNIT_MOD_CAST_HASTE, 1.0f); + SetFloatValue(PLAYER_FIELD_MOD_HASTE, 1.0f); + SetFloatValue(PLAYER_FIELD_MOD_RANGED_HASTE, 1.0f); + + // reset size before reapply auras + SetObjectScale(1.0f); + + SetCreateHealth(basehp); + SetCreateMana(basemana); + + // save base values (bonuses already included in stored stats + for (StatType stat : AllPrimaryStats) + GetStats().SetBaseStatValue(stat, info.stats[AsUnderlyingType(stat)]); + + //reset rating fields values + for (uint16 index = PLAYER_FIELD_COMBAT_RATING_1; index < PLAYER_FIELD_COMBAT_RATING_1 + MAX_COMBAT_RATING; ++index) + SetUInt32Value(index, 0); + + SetUInt32Value(PLAYER_FIELD_MOD_HEALING_DONE_POS, 0); + SetFloatValue(PLAYER_FIELD_MOD_HEALING_PCT, 1.0f); + SetFloatValue(PLAYER_FIELD_MOD_HEALING_DONE_PCT, 1.0f); + for (uint8 i = 0; i < 7; ++i) + { + SetUInt32Value(PLAYER_FIELD_MOD_DAMAGE_DONE_NEG+i, 0); + SetUInt32Value(PLAYER_FIELD_MOD_DAMAGE_DONE_POS+i, 0); + SetFloatValue(PLAYER_FIELD_MOD_DAMAGE_DONE_PCT+i, 1.0f); + } + + SetFloatValue(PLAYER_FIELD_MOD_SPELL_POWER_PCT, 1.0f); + SetFloatValue(PLAYER_FIELD_MOD_HASTE_REGEN, 1.0f); + SetFloatValue(PLAYER_FIELD_OVERRIDE_SPELL_POWER_BY_AP_PCT, 0.0f); + + //reset attack power, damage and attack speed fields + SetFloatValue(UNIT_FIELD_BASEATTACKTIME, 2000.0f); + SetFloatValue(UNIT_FIELD_BASEATTACKTIME + 1, 2000.0f); // offhand attack time + SetFloatValue(UNIT_FIELD_RANGEDATTACKTIME, 2000.0f); + + SetFloatValue(UNIT_FIELD_MINDAMAGE, 0.0f); + SetFloatValue(UNIT_FIELD_MAXDAMAGE, 0.0f); + SetFloatValue(UNIT_FIELD_MINOFFHANDDAMAGE, 0.0f); + SetFloatValue(UNIT_FIELD_MAXOFFHANDDAMAGE, 0.0f); + SetFloatValue(UNIT_FIELD_MINRANGEDDAMAGE, 0.0f); + SetFloatValue(UNIT_FIELD_MAXRANGEDDAMAGE, 0.0f); + SetFloatValue(PLAYER_FIELD_WEAPON_DMG_MULTIPLIERS, 1.0f); + + SetInt32Value(UNIT_FIELD_ATTACK_POWER, 0); + SetInt32Value(UNIT_FIELD_ATTACK_POWER_MOD_POS, 0); + SetInt32Value(UNIT_FIELD_ATTACK_POWER_MOD_NEG, 0); + SetFloatValue(UNIT_FIELD_ATTACK_POWER_MULTIPLIER, 0.0f); + SetInt32Value(UNIT_FIELD_RANGED_ATTACK_POWER, 0); + SetInt32Value(UNIT_FIELD_ATTACK_POWER_MOD_POS, 0); + SetInt32Value(UNIT_FIELD_ATTACK_POWER_MOD_NEG, 0); + SetFloatValue(UNIT_FIELD_RANGED_ATTACK_POWER_MULTIPLIER, 0.0f); + + // Base crit values (will be recalculated in UpdateAllStats() at loading and in _ApplyAllStatBonuses() at reset + SetFloatValue(PLAYER_CRIT_PERCENTAGE, 0.0f); + SetFloatValue(PLAYER_OFFHAND_CRIT_PERCENTAGE, 0.0f); + SetFloatValue(PLAYER_RANGED_CRIT_PERCENTAGE, 0.0f); + + // Init spell schools (will be recalculated in UpdateAllStats() at loading and in _ApplyAllStatBonuses() at reset + for (uint8 i = 0; i < 7; ++i) + SetFloatValue(PLAYER_SPELL_CRIT_PERCENTAGE1+i, 0.0f); + + SetFloatValue(PLAYER_PARRY_PERCENTAGE, 0.0f); + SetFloatValue(PLAYER_BLOCK_PERCENTAGE, 0.0f); + + // Static 30% damage blocked + SetUInt32Value(PLAYER_SHIELD_BLOCK, 30); + + // Dodge percentage + SetFloatValue(PLAYER_DODGE_PERCENTAGE, 0.0f); + + SetFloatValue(UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + AsUnderlyingType(SPELL_SCHOOL_NORMAL), 0.0f); + SetFloatValue(UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + AsUnderlyingType(SPELL_SCHOOL_NORMAL), 0.0f); + // set other resistance to original value (0) + for (uint8 i = 1; i < MAX_SPELL_SCHOOL; ++i) + { + SetResistance(SpellSchools(i), 0); + SetFloatValue(UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + i, 0.0f); + SetFloatValue(UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + i, 0.0f); + } + + SetUInt32Value(PLAYER_FIELD_MOD_TARGET_RESISTANCE, 0); + SetUInt32Value(PLAYER_FIELD_MOD_TARGET_PHYSICAL_RESISTANCE, 0); + for (uint8 i = 0; i < MAX_SPELL_SCHOOL; ++i) + { + SetUInt32Value(UNIT_FIELD_POWER_COST_MODIFIER+i, 0); + SetFloatValue(UNIT_FIELD_POWER_COST_MULTIPLIER+i, 0.0f); + } + // Reset no reagent cost field + for (uint8 i = 0; i < 3; ++i) + SetUInt32Value(PLAYER_NO_REAGENT_COST_1 + i, 0); + // Init data for form but skip reapply item mods for form + InitDataForForm(reapplyMods); + + // save new stats + for (uint8 i = POWER_MANA; i < MAX_POWERS; ++i) + SetMaxPower(PowerType(i), GetCreatePowerValue(PowerType(i))); + + // cleanup mounted state (it will set correctly at aura loading if player saved at mount. + SetUInt32Value(UNIT_FIELD_MOUNTDISPLAYID, 0); + + // cleanup unit flags (will be re-applied if need at aura load). + RemoveFlag(UNIT_FIELD_FLAGS, + UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_REMOVE_CLIENT_CONTROL | UNIT_FLAG_NOT_ATTACKABLE_1 | + UNIT_FLAG_IMMUNE_TO_PC | UNIT_FLAG_IMMUNE_TO_NPC | UNIT_FLAG_LOOTING | + UNIT_FLAG_PET_IN_COMBAT | UNIT_FLAG_SILENCED | UNIT_FLAG_PACIFIED | + UNIT_FLAG_STUNNED | UNIT_FLAG_IN_COMBAT | UNIT_FLAG_DISARMED | + UNIT_FLAG_CONFUSED | UNIT_FLAG_FLEEING | UNIT_FLAG_NOT_SELECTABLE | + UNIT_FLAG_SKINNABLE | UNIT_FLAG_MOUNT | UNIT_FLAG_ON_TAXI ); + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); // must be set + + SetFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_REGENERATE_POWER);// must be set + + // cleanup player flags (will be re-applied if need at aura load), to avoid have ghost flag without ghost aura, for example. + RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_AFK | PLAYER_FLAGS_DND | PLAYER_FLAGS_GM | PLAYER_FLAGS_GHOST | PLAYER_FLAGS_DISABLE_CASTING_EXCEPT_ABILITIES | PLAYER_FLAGS_DISABLE_ATTACKING_EXCEPT_ABILITIES); + + RemoveStandFlags(UNIT_STAND_FLAGS_ALL); // one form stealth modified bytes + RemoveByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_FFA_PVP | UNIT_BYTE2_FLAG_SANCTUARY); + + // restore if need some important flags + SetUInt32Value(PLAYER_FIELD_BYTES2, 0); // flags empty by default + + if (reapplyMods) // reapply stats values only on .reset stats (level) command + _ApplyAllStatBonuses(); + + // set current level health and mana/energy to maximum after applying all mods. + SetFullHealth(); + SetFullPower(POWER_MANA); + SetFullPower(POWER_ENERGY); + if (GetPower(POWER_RAGE) > GetMaxPower(POWER_RAGE)) + SetFullPower(POWER_RAGE); + SetFullPower(POWER_FOCUS); + SetPower(POWER_RUNIC_POWER, 0); + + // update level to hunter/summon pet + if (Pet* pet = GetPet()) + pet->SynchronizeLevelWithOwner(); +} + +void Player::SendKnownSpells(bool firstLogin /*= false*/) +{ + WorldPackets::Spells::SendKnownSpells knownSpells; + knownSpells.InitialLogin = firstLogin; + + knownSpells.KnownSpells.reserve(m_spells.size()); + for (PlayerSpellMap::value_type const& spell : m_spells) + { + if (spell.second.state == PLAYERSPELL_REMOVED) + continue; + + if (!spell.second.active || spell.second.disabled) + continue; + + knownSpells.KnownSpells.push_back(spell.first); + + } + + GetSpellHistory()->WriteSpellHistoryEntries(knownSpells.SpellHistoryEntries); + + SendDirectMessage(knownSpells.Write()); +} + +void Player::RemoveMail(uint32 id) +{ + for (PlayerMails::iterator itr = m_mail.begin(); itr != m_mail.end(); ++itr) + { + if ((*itr)->messageID == id) + { + //do not delete item, because Player::removeMail() is called when returning mail to sender. + m_mail.erase(itr); + return; + } + } +} + +void Player::SendMailResult(uint32 mailId, MailResponseType mailAction, MailResponseResult mailError, uint32 equipError, ObjectGuid::LowType item_guid, uint32 item_count) const +{ + WorldPacket data(SMSG_SEND_MAIL_RESULT, (4+4+4+(mailError == MAIL_ERR_EQUIP_ERROR?4:(mailAction == MAIL_ITEM_TAKEN?4+4:0)))); + data << uint32(mailId); + data << uint32(mailAction); + data << uint32(mailError); + if (mailError == MAIL_ERR_EQUIP_ERROR) + data << uint32(equipError); + else if (mailAction == MAIL_ITEM_TAKEN) + { + data << uint32(item_guid); // item guid low? + data << uint32(item_count); // item count? + } + SendDirectMessage(&data); +} + +void Player::SendNewMail() const +{ + // deliver undelivered mail + WorldPacket data(SMSG_RECEIVED_MAIL, 4); + data << (uint32) 0; + SendDirectMessage(&data); +} + +void Player::UpdateNextMailTimeAndUnreads() +{ + // calculate next delivery time (min. from non-delivered mails + // and recalculate unReadMail + time_t cTime = GameTime::GetGameTime(); + m_nextMailDelivereTime = 0; + unReadMails = 0; + for (PlayerMails::iterator itr = m_mail.begin(); itr != m_mail.end(); ++itr) + { + if ((*itr)->deliver_time > cTime) + { + if (!m_nextMailDelivereTime || m_nextMailDelivereTime > (*itr)->deliver_time) + m_nextMailDelivereTime = (*itr)->deliver_time; + } + else if (((*itr)->checked & MAIL_CHECK_MASK_READ) == 0) + ++unReadMails; + } +} + +void Player::AddNewMailDeliverTime(time_t deliver_time) +{ + if (deliver_time <= GameTime::GetGameTime()) // ready now + { + ++unReadMails; + SendNewMail(); + } + else // not ready and no have ready mails + { + if (!m_nextMailDelivereTime || m_nextMailDelivereTime > deliver_time) + m_nextMailDelivereTime = deliver_time; + } +} + +void DeleteSpellFromAllPlayers(uint32 spellId) +{ + CharacterDatabaseStatements stmts[2] = {CHAR_DEL_INVALID_SPELL_SPELLS, CHAR_DEL_INVALID_SPELL_TALENTS}; + for (uint8 i = 0; i < 2; i++) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(stmts[i]); + + stmt->setUInt32(0, spellId); + + CharacterDatabase.Execute(stmt); + } +} + +bool Player::AddTalent(uint32 spellId, uint8 spec, bool learning) +{ + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + { + // do character spell book cleanup (all characters) + if (!IsInWorld() && !learning) // spell load case + { + TC_LOG_ERROR("spells", "Player::AddTalent: Spell (ID: %u) does not exist. Deleting for all characters in `character_spell` and `character_talent`.", spellId); + + DeleteSpellFromAllPlayers(spellId); + } + else + TC_LOG_ERROR("spells", "Player::AddTalent: Spell (ID: %u) does not exist", spellId); + + return false; + } + + if (!SpellMgr::IsSpellValid(spellInfo, this, false)) + { + // do character spell book cleanup (all characters) + if (!IsInWorld() && !learning) // spell load case + { + TC_LOG_ERROR("spells", "Player::AddTalent: Spell (ID: %u) is invalid. Deleting for all characters in `character_spell` and `character_talent`.", spellId); + + DeleteSpellFromAllPlayers(spellId); + } + else + TC_LOG_ERROR("spells", "Player::AddTalent: Spell (ID: %u) is invalid", spellId); + + return false; + } + + PlayerTalentMap::iterator itr = GetTalentMap(spec).find(spellId); + if (itr != GetTalentMap(spec).end()) + itr->second.State = PLAYERSPELL_UNCHANGED; + else if (TalentSpellPos const* talentPos = sDBCManager.GetTalentSpellPos(spellId)) + { + if (TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentPos->talent_id)) + { + for (uint8 rank = 0; rank < MAX_TALENT_RANK; ++rank) + { + // skip learning spell and no rank spell case + uint32 rankSpellId = talentInfo->SpellRank[rank]; + if (!rankSpellId || rankSpellId == spellId) + continue; + + itr = GetTalentMap(spec).find(rankSpellId); + if (itr != GetTalentMap(spec).end()) + itr->second.State = PLAYERSPELL_REMOVED; + } + } + + + PlayerTalent& newtalent = GetTalentMap(spec)[spellId]; + newtalent.State = learning ? PLAYERSPELL_NEW : PLAYERSPELL_UNCHANGED; + newtalent.Spec = spec; + return true; + } + return false; +} + +bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading /*= false*/, uint32 fromSkill /*= 0*/) +{ + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + { + // do character spell book cleanup (all characters) + if (!IsInWorld() && !learning) // spell load case + { + TC_LOG_ERROR("spells", "Player::AddSpell: Spell (ID: %u) does not exist. deleting for all characters in `character_spell` and `character_talent`.", spellId); + + DeleteSpellFromAllPlayers(spellId); + } + else + TC_LOG_ERROR("spells", "Player::AddSpell: Spell (ID: %u) does not exist", spellId); + + return false; + } + + if (!SpellMgr::IsSpellValid(spellInfo, this, false)) + { + // do character spell book cleanup (all characters) + if (!IsInWorld() && !learning) // spell load case + { + TC_LOG_ERROR("spells", "Player::AddSpell: Spell (ID: %u) is invalid. deleting for all characters in `character_spell` and `character_talent`.", spellId); + + DeleteSpellFromAllPlayers(spellId); + } + else + TC_LOG_ERROR("spells", "Player::AddSpell: Spell (ID: %u) is invalid", spellId); + + return false; + } + + PlayerSpellState state = learning ? PLAYERSPELL_NEW : PLAYERSPELL_UNCHANGED; + + bool dependent_set = false; + bool disabled_case = false; + bool superceded_old = false; + + PlayerSpellMap::iterator itr = m_spells.find(spellId); + + // Remove temporary spell if found to prevent conflicts + if (itr != m_spells.end() && itr->second.state == PLAYERSPELL_TEMPORARY) + RemoveTemporarySpell(spellId); + else if (itr != m_spells.end()) + { + uint32 next_active_spell_id = 0; + // fix activate state for non-stackable low rank (and find next spell for !active case) + if (!spellInfo->IsStackableWithRanks() && spellInfo->IsRanked()) + { + if (uint32 next = sSpellMgr->GetNextSpellInChain(spellId)) + { + if (HasSpell(next)) + { + // high rank already known so this must !active + active = false; + next_active_spell_id = next; + } + } + } + + // not do anything if already known in expected state + if (itr->second.state != PLAYERSPELL_REMOVED && itr->second.active == active && + itr->second.dependent == dependent && itr->second.disabled == disabled) + { + if (!IsInWorld() && !learning) // explicitly load from DB and then exist in it already and set correctly + itr->second.state = PLAYERSPELL_UNCHANGED; + + return false; + } + + // dependent spell known as not dependent, overwrite state + if (itr->second.state != PLAYERSPELL_REMOVED && !itr->second.dependent && dependent) + { + itr->second.dependent = dependent; + if (itr->second.state != PLAYERSPELL_NEW) + itr->second.state = PLAYERSPELL_CHANGED; + dependent_set = true; + } + + // update active state for known spell + if (itr->second.active != active && itr->second.state != PLAYERSPELL_REMOVED && !itr->second.disabled) + { + itr->second.active = active; + + if (!IsInWorld() && !learning && !dependent_set) // explicitly load from DB and then exist in it already and set correctly + itr->second.state = PLAYERSPELL_UNCHANGED; + else if (itr->second.state != PLAYERSPELL_NEW) + itr->second.state = PLAYERSPELL_CHANGED; + + if (active) + { + if (spellInfo->IsPassive() && HandlePassiveSpellLearn(spellInfo)) + CastSpell(this, spellId, true); + } + else if (IsInWorld()) + { + if (next_active_spell_id) + SendSupercededSpell(spellId, next_active_spell_id); + else + { + WorldPackets::Spells::UnlearnedSpells unlearnedSpells; + unlearnedSpells.SpellID = spellId; + SendDirectMessage(unlearnedSpells.Write()); + } + } + + return active; // learn (show in spell book if active now) + } + + if (itr->second.disabled != disabled && itr->second.state != PLAYERSPELL_REMOVED) + { + if (itr->second.state != PLAYERSPELL_NEW) + itr->second.state = PLAYERSPELL_CHANGED; + itr->second.disabled = disabled; + + if (disabled) + return false; + + disabled_case = true; + } + else switch (itr->second.state) + { + case PLAYERSPELL_UNCHANGED: // known saved spell + return false; + case PLAYERSPELL_REMOVED: // re-learning removed not saved spell + { + m_spells.erase(itr); + state = PLAYERSPELL_CHANGED; + break; // need re-add + } + default: // known not saved yet spell (new or modified) + { + // can be in case spell loading but learned at some previous spell loading + if (!IsInWorld() && !learning && !dependent_set) + itr->second.state = PLAYERSPELL_UNCHANGED; + + return false; + } + } + } + + if (!disabled_case) // skip new spell adding if spell already known (disabled spells case) + { + // talent: unlearn all other talent ranks (high and low) + if (TalentSpellPos const* talentPos = sDBCManager.GetTalentSpellPos(spellId)) + { + if (TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentPos->talent_id)) + { + for (uint8 rank = 0; rank < MAX_TALENT_RANK; ++rank) + { + // skip learning spell and no rank spell case + uint32 rankSpellId = talentInfo->SpellRank[rank]; + if (!rankSpellId || rankSpellId == spellId) + continue; + + RemoveSpell(rankSpellId, false, false); + } + } + } + // non talent spell: learn low ranks (recursive call) + else if (uint32 prev_spell = sSpellMgr->GetPrevSpellInChain(spellId)) + { + if (!IsInWorld() || disabled) // at spells loading, no output, but allow save + AddSpell(prev_spell, active, true, true, disabled, false, fromSkill); + else // at normal learning + LearnSpell(prev_spell, true, fromSkill); + } + + std::pair inserted = m_spells.emplace(std::piecewise_construct, std::forward_as_tuple(spellId), std::forward_as_tuple()); + PlayerSpell& newspell = inserted.first->second; + // learning a previous rank might have given us this spell already from a skill autolearn, most likely with PLAYERSPELL_NEW state + // we dont want to do double insert if this happened during load from db so we force state to CHANGED, just in case + newspell.state = inserted.second ? state : PLAYERSPELL_CHANGED; + newspell.active = active; + newspell.dependent = dependent; + newspell.disabled = disabled; + + // replace spells in action bars and spellbook to bigger rank if only one spell rank must be accessible + if (newspell.active && !newspell.disabled && !spellInfo->IsStackableWithRanks() && spellInfo->IsRanked()) + { + for (PlayerSpellMap::iterator itr2 = m_spells.begin(); itr2 != m_spells.end(); ++itr2) + { + if (itr2->second.state == PLAYERSPELL_REMOVED) + continue; + + SpellInfo const* i_spellInfo = sSpellMgr->GetSpellInfo(itr2->first); + if (!i_spellInfo) + continue; + + if (spellInfo->IsDifferentRankOf(i_spellInfo)) + { + if (itr2->second.active) + { + if (spellInfo->IsHighRankOf(i_spellInfo)) + { + if (IsInWorld()) // not send spell (re-/over-)learn packets at loading + SendSupercededSpell(itr2->first, spellId); + + // mark old spell as disable (SMSG_SUPERCEDED_SPELL replace it in client by new) + itr2->second.active = false; + if (itr2->second.state != PLAYERSPELL_NEW) + itr2->second.state = PLAYERSPELL_CHANGED; + superceded_old = true; // new spell replace old in action bars and spell book. + } + else + { + if (IsInWorld()) // not send spell (re-/over-)learn packets at loading + SendSupercededSpell(spellId, itr2->first); + + // mark new spell as disable (not learned yet for client and will not learned) + newspell.active = false; + if (newspell.state != PLAYERSPELL_NEW) + newspell.state = PLAYERSPELL_CHANGED; + } + } + } + } + } + + // return false if spell disabled + if (newspell.disabled) + return false; + } + + uint32 talentCost = sDBCManager.GetTalentSpellCost(spellId); + + // cast talents with SPELL_EFFECT_LEARN_SPELL (other dependent spells will learned later as not auto-learned) + // note: all spells with SPELL_EFFECT_LEARN_SPELL isn't passive + if (!loading && talentCost > 0 && spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL)) + { + // ignore stance requirement for talent learn spell (stance set for spell only for client spell description show) + CastSpell(this, spellId, true); + } + // also cast passive spells (including all talents without SPELL_EFFECT_LEARN_SPELL) with additional checks + else if (spellInfo->IsPassive()) + { + if (HandlePassiveSpellLearn(spellInfo)) + CastSpell(this, spellId, true); + } + else if (spellInfo->HasEffect(SPELL_EFFECT_SKILL_STEP)) + { + CastSpell(this, spellId, true); + return false; + } + else if (spellInfo->HasAttribute(SPELL_ATTR1_CAST_WHEN_LEARNED)) + CastSpell(this, spellId, true); + + // update used talent points count + SetUsedTalentCount(GetUsedTalentCount() + talentCost); + + // update free primary prof.points (if any, can be none in case GM .learn prof. learning) + if (uint32 freeProfs = GetFreePrimaryProfessionPoints()) + { + if (spellInfo->IsPrimaryProfessionFirstRank()) + SetFreePrimaryProfessions(freeProfs-1); + } + + SkillLineAbilityMapBounds skill_bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId); + + if (SpellLearnSkillNode const* spellLearnSkill = sSpellMgr->GetSpellLearnSkill(spellId)) + { + // add dependent skills if this spell is not learned from adding skill already + if (spellLearnSkill->skill != fromSkill) + { + uint16 skill_value = GetPureSkillValue(spellLearnSkill->skill); + uint16 skill_max_value = GetPureMaxSkillValue(spellLearnSkill->skill); + + if (skill_value < spellLearnSkill->value) + skill_value = spellLearnSkill->value; + + uint16 new_skill_max_value = spellLearnSkill->maxvalue; + + if (new_skill_max_value == 0) + { + if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(spellLearnSkill->skill, getRace(), getClass())) + { + switch (GetSkillRangeType(rcInfo)) + { + case SKILL_RANGE_LANGUAGE: + skill_value = 300; + new_skill_max_value = 300; + break; + case SKILL_RANGE_LEVEL: + new_skill_max_value = GetMaxSkillValueForLevel(); + break; + case SKILL_RANGE_MONO: + new_skill_max_value = 1; + break; + case SKILL_RANGE_RANK: + { + SkillTiersEntry const* tier = sSkillTiersStore.AssertEntry(rcInfo->SkillTierID); + new_skill_max_value = tier->GetValueForTierIndex(spellLearnSkill->step - 1); + break; + } + default: + break; + } + + if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + skill_value = new_skill_max_value; + } + } + + if (skill_max_value < new_skill_max_value) + skill_max_value = new_skill_max_value; + + SetSkill(spellLearnSkill->skill, spellLearnSkill->step, skill_value, skill_max_value); + } + } + else + { + // not ranked skills + for (SkillLineAbilityMap::const_iterator _spell_idx = skill_bounds.first; _spell_idx != skill_bounds.second; ++_spell_idx) + { + SkillLineEntry const* pSkill = sSkillLineStore.LookupEntry(_spell_idx->second->SkillLine); + if (!pSkill) + continue; + + if (pSkill->ID == fromSkill) + continue; + + // Runeforging special case + if ((_spell_idx->second->GetAcquireMethod() == SkillLineAbilityAcquireMethod::AutomaticCharLevel && !HasSkill(_spell_idx->second->SkillLine)) || ((_spell_idx->second->SkillLine == SKILL_RUNEFORGING) && _spell_idx->second->TrivialSkillLineRankHigh == 0)) + if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(_spell_idx->second->SkillLine, getRace(), getClass())) + LearnDefaultSkill(rcInfo); + } + } + + // learn dependent spells + SpellLearnSpellMapBounds spell_bounds = sSpellMgr->GetSpellLearnSpellMapBounds(spellId); + + for (SpellLearnSpellMap::const_iterator itr2 = spell_bounds.first; itr2 != spell_bounds.second; ++itr2) + { + if (!itr2->second.autoLearned) + { + if (!IsInWorld() || !itr2->second.active) // at spells loading, no output, but allow save + AddSpell(itr2->second.spell, itr2->second.active, true, true, false); + else // at normal learning + LearnSpell(itr2->second.spell, true); + } + } + + if (!GetSession()->PlayerLoading()) + { + // not ranked skills + for (SkillLineAbilityMap::const_iterator _spell_idx = skill_bounds.first; _spell_idx != skill_bounds.second; ++_spell_idx) + { + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LINE, _spell_idx->second->SkillLine); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILLLINE_SPELLS, _spell_idx->second->SkillLine); + } + + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SPELL, spellId); + } + + // return true (for send learn packet) only if spell active (in case ranked spells) and not replace old spell + return active && !disabled && !superceded_old; +} + +void Player::AddTemporarySpell(uint32 spellId) +{ + PlayerSpellMap::iterator itr = m_spells.find(spellId); + // spell already added - do not do anything + if (itr != m_spells.end()) + return; + PlayerSpell* newspell = &m_spells[spellId]; + newspell->state = PLAYERSPELL_TEMPORARY; + newspell->active = true; + newspell->dependent = false; + newspell->disabled = false; +} + +void Player::RemoveTemporarySpell(uint32 spellId) +{ + PlayerSpellMap::iterator itr = m_spells.find(spellId); + // spell already not in list - do not do anything + if (itr == m_spells.end()) + return; + // spell has other state than temporary - do not change it + if (itr->second.state != PLAYERSPELL_TEMPORARY) + return; + m_spells.erase(itr); +} + +bool Player::HandlePassiveSpellLearn(SpellInfo const* spellInfo) +{ + // note: form passives activated with shapeshift spells be implemented by HandleShapeshiftBoosts instead of spell_learn_spell + // talent dependent passives activated at form apply have proper stance data + ShapeshiftForm form = GetShapeshiftForm(); + bool need_cast = (!spellInfo->Stances || (form && (spellInfo->Stances & (UI64LIT(1) << (form - 1)))) || + (!form && spellInfo->HasAttribute(SPELL_ATTR2_ALLOW_WHILE_NOT_SHAPESHIFTED_CASTER_FORM))); + + // Check EquippedItemClass + // passive spells which apply aura and have an item requirement are to be added manually, instead of casted + if (spellInfo->EquippedItemClass >= 0) + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (spellInfo->Effects[i].IsAura()) + { + if (!HasAura(spellInfo->Id) && HasItemFitToSpellRequirements(spellInfo)) + AddAura(spellInfo->Id, this); + return false; + } + } + } + + if (spellInfo->HasAttribute(SPELL_ATTR8_MASTERY_AFFECTS_POINTS)) + need_cast &= IsCurrentSpecMasterySpell(spellInfo); + + //Check CasterAuraStates + return need_cast && (!spellInfo->CasterAuraState || HasAuraState(AuraStateType(spellInfo->CasterAuraState))); +} + +bool Player::IsCurrentSpecMasterySpell(SpellInfo const* spellInfo) const +{ + if (TalentTabEntry const* talentTab = sTalentTabStore.LookupEntry(GetPrimaryTalentTree(GetActiveSpec()))) + return spellInfo->Id == talentTab->MasterySpellID[0] || spellInfo->Id == talentTab->MasterySpellID[1]; + + return false; +} + +void Player::LearnSpell(uint32 spell_id, bool dependent, uint32 fromSkill /*= 0*/) +{ + PlayerSpellMap::iterator itr = m_spells.find(spell_id); + + bool disabled = (itr != m_spells.end()) ? itr->second.disabled : false; + bool active = disabled ? itr->second.active : true; + + bool learning = AddSpell(spell_id, active, true, dependent, false, false, fromSkill); + + // prevent duplicated entires in spell book, also not send if not in world (loading) + if (learning && IsInWorld()) + { + WorldPacket data(SMSG_LEARNED_SPELL, 8); + data << uint32(spell_id); + data << uint32(0); + SendDirectMessage(&data); + } + + // learn all disabled higher ranks and required spells (recursive) + if (disabled) + { + if (uint32 nextSpell = sSpellMgr->GetNextSpellInChain(spell_id)) + { + PlayerSpellMap::iterator iter = m_spells.find(nextSpell); + if (iter != m_spells.end() && iter->second.disabled) + LearnSpell(nextSpell, false, fromSkill); + } + + SpellsRequiringSpellMapBounds spellsRequiringSpell = sSpellMgr->GetSpellsRequiringSpellBounds(spell_id); + for (SpellsRequiringSpellMap::const_iterator itr2 = spellsRequiringSpell.first; itr2 != spellsRequiringSpell.second; ++itr2) + { + PlayerSpellMap::iterator iter2 = m_spells.find(itr2->second); + if (iter2 != m_spells.end() && iter2->second.disabled) + LearnSpell(itr2->second, false, fromSkill); + } + } + + if (CanControlPet(spell_id) && GetPet()) + PetSpellInitialize(); + + if (Guild* guild = GetGuild()) + guild->UpdateMemberData(this, GUILD_MEMBER_DATA_PROFESSIONS, 0); +} + +void Player::RemoveSpell(uint32 spell_id, bool disabled, bool learn_low_rank) +{ + PlayerSpellMap::iterator itr = m_spells.find(spell_id); + if (itr == m_spells.end()) + return; + + if (itr->second.state == PLAYERSPELL_REMOVED || (disabled && itr->second.disabled) || itr->second.state == PLAYERSPELL_TEMPORARY) + return; + + // unlearn non talent higher ranks (recursive) + if (uint32 nextSpell = sSpellMgr->GetNextSpellInChain(spell_id)) + { + if (HasSpell(nextSpell) && !sDBCManager.GetTalentSpellPos(nextSpell)) + RemoveSpell(nextSpell, disabled, false); + } + //unlearn spells dependent from recently removed spells + SpellsRequiringSpellMapBounds spellsRequiringSpell = sSpellMgr->GetSpellsRequiringSpellBounds(spell_id); + for (SpellsRequiringSpellMap::const_iterator itr2 = spellsRequiringSpell.first; itr2 != spellsRequiringSpell.second; ++itr2) + RemoveSpell(itr2->second, disabled); + + // re-search, it can be corrupted in prev loop + itr = m_spells.find(spell_id); + if (itr == m_spells.end()) + return; // already unleared + + bool giveTalentPoints = disabled || !itr->second.disabled; + + bool cur_active = itr->second.active; + bool cur_dependent = itr->second.dependent; + + if (disabled) + { + itr->second.disabled = disabled; + if (itr->second.state != PLAYERSPELL_NEW) + itr->second.state = PLAYERSPELL_CHANGED; + } + else + { + if (itr->second.state == PLAYERSPELL_NEW) + m_spells.erase(itr); + else + itr->second.state = PLAYERSPELL_REMOVED; + } + + RemoveOwnedAura(spell_id, GetGUID()); + + // remove pet auras + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (PetAura const* petSpell = sSpellMgr->GetPetAura(spell_id, i)) + RemovePetAura(petSpell); + + // free talent points + uint32 talentCosts = sDBCManager.GetTalentSpellCost(spell_id); + if (talentCosts > 0 && giveTalentPoints) + { + if (talentCosts < GetUsedTalentCount()) + SetUsedTalentCount(GetUsedTalentCount() - talentCosts); + else + SetUsedTalentCount(0); + } + + // update free primary prof.points (if not overflow setting, can be in case GM use before .learn prof. learning) + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell_id); + if (spellInfo && spellInfo->IsPrimaryProfessionFirstRank()) + { + uint32 freeProfs = GetFreePrimaryProfessionPoints()+1; + if (freeProfs <= sWorld->getIntConfig(CONFIG_MAX_PRIMARY_TRADE_SKILL)) + SetFreePrimaryProfessions(freeProfs); + } + + // remove dependent skill + SpellLearnSkillNode const* spellLearnSkill = sSpellMgr->GetSpellLearnSkill(spell_id); + if (spellLearnSkill) + { + uint32 prev_spell = sSpellMgr->GetPrevSpellInChain(spell_id); + if (!prev_spell) // first rank, remove skill + SetSkill(spellLearnSkill->skill, 0, 0, 0); + else + { + // search prev. skill setting by spell ranks chain + SpellLearnSkillNode const* prevSkill = sSpellMgr->GetSpellLearnSkill(prev_spell); + while (!prevSkill && prev_spell) + { + prev_spell = sSpellMgr->GetPrevSpellInChain(prev_spell); + prevSkill = sSpellMgr->GetSpellLearnSkill(sSpellMgr->GetFirstSpellInChain(prev_spell)); + } + + if (!prevSkill) // not found prev skill setting, remove skill + SetSkill(spellLearnSkill->skill, 0, 0, 0); + else // set to prev. skill setting values + { + uint16 skill_value = GetPureSkillValue(prevSkill->skill); + uint16 skill_max_value = GetPureMaxSkillValue(prevSkill->skill); + + uint16 new_skill_max_value = prevSkill->maxvalue; + + if (new_skill_max_value == 0) + { + if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(prevSkill->skill, getRace(), getClass())) + { + switch (GetSkillRangeType(rcInfo)) + { + case SKILL_RANGE_LANGUAGE: + skill_value = 300; + new_skill_max_value = 300; + break; + case SKILL_RANGE_LEVEL: + new_skill_max_value = GetMaxSkillValueForLevel(); + break; + case SKILL_RANGE_MONO: + new_skill_max_value = 1; + break; + case SKILL_RANGE_RANK: + { + SkillTiersEntry const* tier = sSkillTiersStore.AssertEntry(rcInfo->SkillTierID); + new_skill_max_value = tier->GetValueForTierIndex(prevSkill->step - 1); + break; + } + default: + break; + } + + if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + skill_value = new_skill_max_value; + } + } + else if (skill_value > prevSkill->value) + skill_value = prevSkill->value; + + if (skill_max_value > new_skill_max_value) + skill_max_value = new_skill_max_value; + + if (skill_value > new_skill_max_value) + skill_value = new_skill_max_value; + + SetSkill(prevSkill->skill, prevSkill->step, skill_value, skill_max_value); + } + } + } + + // remove dependent spells + SpellLearnSpellMapBounds spell_bounds = sSpellMgr->GetSpellLearnSpellMapBounds(spell_id); + + for (SpellLearnSpellMap::const_iterator itr2 = spell_bounds.first; itr2 != spell_bounds.second; ++itr2) + RemoveSpell(itr2->second.spell, disabled); + + // activate lesser rank in spellbook/action bar, and cast it if need + bool prev_activate = false; + + if (uint32 prev_id = sSpellMgr->GetPrevSpellInChain(spell_id)) + { + // if talent then lesser rank also talent and need learn + if (talentCosts) + { + // I cannot see why mangos has these lines. + //if (learn_low_rank) + // learnSpell(prev_id, false); + } + // if ranked non-stackable spell: need activate lesser rank and update dendence state + /// No need to check for spellInfo != nullptr here because if cur_active is true, then that means that the spell was already in m_spells, and only valid spells can be pushed there. + else if (cur_active && !spellInfo->IsStackableWithRanks() && spellInfo->IsRanked()) + { + // need manually update dependence state (learn spell ignore like attempts) + PlayerSpellMap::iterator prev_itr = m_spells.find(prev_id); + if (prev_itr != m_spells.end()) + { + if (prev_itr->second.dependent != cur_dependent) + { + prev_itr->second.dependent = cur_dependent; + if (prev_itr->second.state != PLAYERSPELL_NEW) + prev_itr->second.state = PLAYERSPELL_CHANGED; + } + + // now re-learn if need re-activate + if (!prev_itr->second.active && learn_low_rank) + { + if (AddSpell(prev_id, true, false, prev_itr->second.dependent, prev_itr->second.disabled)) + { + // downgrade spell ranks in spellbook and action bar + SendSupercededSpell(spell_id, prev_id); + prev_activate = true; + } + } + } + } + } + + if (spell_id == 46917 && m_canTitanGrip) + { + RemoveAurasDueToSpell(m_titanGripPenaltySpellId); + SetCanTitanGrip(false); + } + + if (m_canDualWield) + { + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell_id); + if (spellInfo->IsPassive()) + { + for (int i = 0; i < MAX_SPELL_EFFECTS; i++) + if (spellInfo->Effects[i].Effect == SPELL_EFFECT_DUAL_WIELD) + { + SetCanDualWield(false); + break; + } + } + } + + if (sWorld->getBoolConfig(CONFIG_OFFHAND_CHECK_AT_SPELL_UNLEARN)) + AutoUnequipOffhandIfNeed(); + + // remove from spell book if not replaced by lesser rank + if (!prev_activate) + { + WorldPackets::Spells::UnlearnedSpells unlearnedSpells; + unlearnedSpells.SpellID = spell_id; + SendDirectMessage(unlearnedSpells.Write()); + } + + if (Guild* guild = GetGuild()) + guild->UpdateMemberData(this, GUILD_MEMBER_DATA_PROFESSIONS, 0); +} + +void Player::RemoveArenaSpellCooldowns(bool removeActivePetCooldowns) +{ + // remove cooldowns on spells that have < 10 min CD + GetSpellHistory()->ResetCooldowns([](SpellHistory::CooldownStorageType::iterator itr) -> bool + { + SpellInfo const* spellInfo = sSpellMgr->AssertSpellInfo(itr->first); + return spellInfo->RecoveryTime < 10 * MINUTE * IN_MILLISECONDS + && spellInfo->CategoryRecoveryTime < 10 * MINUTE * IN_MILLISECONDS + && !spellInfo->HasAttribute(SPELL_ATTR6_DO_NOT_RESET_COOLDOWN_IN_ARENA); + }, true); + + // pet cooldowns + if (removeActivePetCooldowns) + if (Pet* pet = GetPet()) + pet->GetSpellHistory()->ResetAllCooldowns(); +} + +uint32 Player::GetNextResetTalentsCost() const +{ + // The first time reset costs 1 gold + if (GetTalentResetCost() < 1*GOLD) + return 1*GOLD; + // then 5 gold + else if (GetTalentResetCost() < 5*GOLD) + return 5*GOLD; + // After that it increases in increments of 5 gold + else if (GetTalentResetCost() < 10*GOLD) + return 10*GOLD; + else + { + uint64 months = (GameTime::GetGameTime() - GetTalentResetTime())/MONTH; + if (months > 0) + { + // This cost will be reduced by a rate of 5 gold per month + int32 new_cost = int32(GetTalentResetCost() - 5*GOLD*months); + // to a minimum of 10 gold. + return (new_cost < 10*GOLD ? 10*GOLD : new_cost); + } + else + { + // After that it increases in increments of 5 gold + int32 new_cost = GetTalentResetCost() + 5*GOLD; + // until it hits a cap of 50 gold. + if (new_cost > 50*GOLD) + new_cost = 50*GOLD; + return new_cost; + } + } +} + +bool Player::ResetTalents(bool no_cost) +{ + sScriptMgr->OnPlayerTalentsReset(this, no_cost); + + // not need after this call + if (HasAtLoginFlag(AT_LOGIN_RESET_TALENTS)) + RemoveAtLoginFlag(AT_LOGIN_RESET_TALENTS, true); + + uint32 talentPointsForLevel = CalculateTalentsPoints(); + + if (!GetUsedTalentCount()) + { + SetFreeTalentPoints(talentPointsForLevel); + return false; + } + + uint32 cost = 0; + + if (!no_cost && !sWorld->getBoolConfig(CONFIG_NO_RESET_TALENT_COST)) + { + cost = GetNextResetTalentsCost(); + + if (!HasEnoughMoney(uint64(cost))) + { + SendBuyError(BUY_ERR_NOT_ENOUGHT_MONEY, nullptr, 0, 0); + return false; + } + } + + RemovePet(nullptr, PET_SAVE_DISMISS, true); + + for (uint32 talentId = 0; talentId < sTalentStore.GetNumRows(); ++talentId) + { + TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentId); + + if (!talentInfo) + continue; + + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentInfo->TabID); + + if (!talentTabInfo) + continue; + + // unlearn only talents for character class + // some spell learned by one class as normal spells or know at creation but another class learn it as talent, + // to prevent unexpected lost normal learned spell skip another class talents + if ((getClassMask() & talentTabInfo->ClassMask) == 0) + continue; + + for (int8 rank = MAX_TALENT_RANK-1; rank >= 0; --rank) + { + // skip non-existing talent ranks + if (talentInfo->SpellRank[rank] == 0) + continue; + + SpellInfo const* _spellEntry = sSpellMgr->GetSpellInfo(talentInfo->SpellRank[rank]); + if (!_spellEntry) + continue; + + RemoveSpell(talentInfo->SpellRank[rank], true); + + // search for spells that the talent teaches and unlearn them + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (_spellEntry->Effects[i].TriggerSpell > 0 && _spellEntry->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL) + RemoveSpell(_spellEntry->Effects[i].TriggerSpell, true); + + // if this talent rank can be found in the PlayerTalentMap, mark the talent as removed so it gets deleted + PlayerTalentMap::iterator plrTalent = GetTalentMap(GetActiveSpec()).find(talentInfo->SpellRank[rank]); + if (plrTalent != GetTalentMap(GetActiveSpec()).end()) + plrTalent->second.State = PLAYERSPELL_REMOVED; + } + } + + // Remove spec specific spells + uint32 const* talentTabs = sDBCManager.GetTalentTabPages(getClass()); + for (uint32 i = 0; i < MAX_TALENT_TABS; ++i) + { + if (std::vector const* specSpells = sDBCManager.GetTalentTreePrimarySpells(talentTabs[i])) + for (size_t j = 0; j < specSpells->size(); ++j) + RemoveSpell(specSpells->at(j), true); + + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentTabs[i]); + for (uint32 j = 0; j < MAX_MASTERY_SPELLS; ++j) + if (uint32 mastery = talentTabInfo->MasterySpellID[j]) + RemoveAurasDueToSpell(mastery); + } + + + SetPrimaryTalentTree(GetActiveSpec(), 0); + SetFreeTalentPoints(talentPointsForLevel); + + // We consider resetting talents as changing a specialization as well as we now can pick an entirely new primary talent tree which equals changing specs + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags2::ChangeSpec); + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + _SaveTalents(trans); + _SaveSpells(trans); + CharacterDatabase.CommitTransaction(trans); + + if (!no_cost) + { + ModifyMoney(-(int64)cost); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_GOLD_SPENT_FOR_TALENTS, cost); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_NUMBER_OF_TALENT_RESETS, 1); + + SetTalentResetCost(cost); + SetTalentResetTime(GameTime::GetGameTime()); + } + + /* when prev line will dropped use next line + if (Pet* pet = GetPet()) + { + if (pet->getPetType() == HUNTER_PET && !pet->GetCreatureTemplate()->IsTameable(CanTameExoticPets())) + RemovePet(nullptr, PET_SAVE_NOT_IN_SLOT, true); + } + */ + + return true; +} + +Mail* Player::GetMail(uint32 id) +{ + for (PlayerMails::iterator itr = m_mail.begin(); itr != m_mail.end(); ++itr) + if ((*itr)->messageID == id) + return (*itr); + + return nullptr; +} + +void Player::BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) const +{ + if (target == this) + { + for (uint8 i = 0; i < EQUIPMENT_SLOT_END; ++i) + { + if (m_items[i] == nullptr) + continue; + + m_items[i]->BuildCreateUpdateBlockForPlayer(data, target); + } + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < BANK_SLOT_BAG_END; ++i) + { + if (m_items[i] == nullptr) + continue; + + m_items[i]->BuildCreateUpdateBlockForPlayer(data, target); + } + } + + Unit::BuildCreateUpdateBlockForPlayer(data, target); +} + +void Player::DestroyForPlayer(Player* target, bool onDeath /*= false*/) const +{ + Unit::DestroyForPlayer(target, onDeath); + + for (uint8 i = 0; i < EQUIPMENT_SLOT_END; ++i) + { + if (m_items[i] == nullptr) + continue; + + m_items[i]->DestroyForPlayer(target); + } + + if (target == this) + { + for (uint8 i = INVENTORY_SLOT_BAG_START; i < BANK_SLOT_BAG_END; ++i) + { + if (m_items[i] == nullptr) + continue; + + m_items[i]->DestroyForPlayer(target); + } + } +} + +bool Player::HasSpell(uint32 spell) const +{ + PlayerSpellMap::const_iterator itr = m_spells.find(spell); + return (itr != m_spells.end() && itr->second.state != PLAYERSPELL_REMOVED && + !itr->second.disabled); +} + +bool Player::HasTalent(uint32 spell, uint8 spec) const +{ + PlayerTalentMap::const_iterator itr = GetTalentMap(spec).find(spell); + return (itr != GetTalentMap(spec).end() && itr->second.State != PLAYERSPELL_REMOVED); +} + +bool Player::HasActiveSpell(uint32 spell) const +{ + PlayerSpellMap::const_iterator itr = m_spells.find(spell); + return (itr != m_spells.end() && itr->second.state != PLAYERSPELL_REMOVED && + itr->second.active && !itr->second.disabled); +} + +/** + * Deletes a character from the database + * + * The way characters will be deleted is decided based on the config option. + * + * @see Player::DeleteOldCharacters + * + * @param playerguid the low-GUID from the player which should be deleted + * @param accountId the account id from the player + * @param updateRealmChars when this flag is set, the amount of characters on that realm will be updated in the realmlist + * @param deleteFinally if this flag is set, the config option will be ignored and the character will be permanently removed from the database + */ +void Player::DeleteFromDB(ObjectGuid playerguid, uint32 accountId, bool updateRealmChars, bool deleteFinally) +{ + // Avoid realm-update for non-existing account + if (accountId == 0) + updateRealmChars = false; + + // Convert guid to low GUID for CharacterNameData, but also other methods on success + ObjectGuid::LowType guid = playerguid.GetCounter(); + uint32 charDelete_method = sWorld->getIntConfig(CONFIG_CHARDELETE_METHOD); + CharacterCacheEntry const* characterInfo = sCharacterCache->GetCharacterCacheByGuid(playerguid); + std::string name; + if (characterInfo) + name = characterInfo->Name; + + if (deleteFinally) + charDelete_method = CHAR_DELETE_REMOVE; + else if (characterInfo) // To avoid a query, we select loaded data. If it doesn't exist, return. + { + // Define the required variables + uint32 charDelete_minLvl = sWorld->getIntConfig(characterInfo->Class != CLASS_DEATH_KNIGHT ? CONFIG_CHARDELETE_MIN_LEVEL : CONFIG_CHARDELETE_DEATH_KNIGHT_MIN_LEVEL); + + // if we want to finalize the character removal or the character does not meet the level requirement of either heroic or non-heroic settings, + // we set it to mode CHAR_DELETE_REMOVE + if (characterInfo->Level < charDelete_minLvl) + charDelete_method = CHAR_DELETE_REMOVE; + } + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + if (ObjectGuid::LowType guildId = sCharacterCache->GetCharacterGuildIdByGuid(playerguid)) + if (Guild* guild = sGuildMgr->GetGuildById(guildId)) + guild->DeleteMember(trans, playerguid, false, false, true); + + // close player ticket if any + GmTicket* ticket = sTicketMgr->GetTicketByPlayer(playerguid); + if (ticket) + sTicketMgr->CloseTicket(ticket->GetId(), playerguid); + + // remove from arena teams + LeaveAllArenaTeams(playerguid); + + // the player was uninvited already on logout so just remove from group + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_GROUP_MEMBER); + stmt->setUInt32(0, guid); + PreparedQueryResult resultGroup = CharacterDatabase.Query(stmt); + + if (resultGroup) + if (Group* group = sGroupMgr->GetGroupByDbStoreId((*resultGroup)[0].GetUInt32())) + RemoveFromGroup(group, playerguid); + + // Remove signs from petitions (also remove petitions if owner); + RemovePetitionsAndSigns(playerguid, CHARTER_TYPE_ANY); + + switch (charDelete_method) + { + // Completely remove from the database + case CHAR_DELETE_REMOVE: + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_COD_ITEM_MAIL); + stmt->setUInt32(0, guid); + PreparedQueryResult resultMail = CharacterDatabase.Query(stmt); + + if (resultMail) + { + std::unordered_map> itemsByMail; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAILITEMS); + stmt->setUInt32(0, guid); + PreparedQueryResult resultItems = CharacterDatabase.Query(stmt); + + if (resultItems) + { + do + { + Field* fields = resultItems->Fetch(); + uint32 mailId = fields[15].GetUInt32(); + if (Item* mailItem = _LoadMailedItem(playerguid, nullptr, mailId, nullptr, fields)) + itemsByMail[mailId].push_back(mailItem); + + } while (resultItems->NextRow()); + } + + do + { + Field* mailFields = resultMail->Fetch(); + + uint32 mail_id = mailFields[0].GetUInt32(); + uint8 mailType = mailFields[1].GetUInt8(); + uint16 mailTemplateId= mailFields[2].GetUInt16(); + uint32 sender = mailFields[3].GetUInt32(); + std::string subject = mailFields[4].GetString(); + std::string body = mailFields[5].GetString(); + uint64 money = mailFields[6].GetUInt64(); + bool has_items = mailFields[7].GetBool(); + + // We can return mail now + // So firstly delete the old one + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_BY_ID); + stmt->setUInt32(0, mail_id); + trans->Append(stmt); + + // Mail is not from player + if (mailType != MAIL_NORMAL) + { + if (has_items) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEM_BY_ID); + stmt->setUInt32(0, mail_id); + trans->Append(stmt); + } + continue; + } + + MailDraft draft(subject, body); + if (mailTemplateId) + draft = MailDraft(mailTemplateId, false); // items are already included + + auto itemsItr = itemsByMail.find(mail_id); + if (itemsItr != itemsByMail.end()) + { + for (Item* item : itemsItr->second) + draft.AddItem(item); + + // MailDraft will take care of freeing memory + itemsByMail.erase(itemsItr); + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEM_BY_ID); + stmt->setUInt32(0, mail_id); + trans->Append(stmt); + + uint32 pl_account = sCharacterCache->GetCharacterAccountIdByGuid(playerguid); + + draft.AddMoney(money).SendReturnToSender(pl_account, guid, sender, trans); + } + while (resultMail->NextRow()); + } + + // Unsummon and delete for pets in world is not required: player deleted from CLI or character list with not loaded pet. + // NOW we can finally clear other DB data related to character + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_PETS); + stmt->setUInt32(0, guid); + PreparedQueryResult resultPets = CharacterDatabase.Query(stmt); + + if (resultPets) + { + do + { + ObjectGuid::LowType petguidlow = (*resultPets)[0].GetUInt32(); + Pet::DeleteFromDB(petguidlow); + } while (resultPets->NextRow()); + } + + // Delete char from social list of online chars + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_SOCIAL); + stmt->setUInt32(0, guid); + + if (PreparedQueryResult resultFriends = CharacterDatabase.Query(stmt)) + { + do + { + if (Player* playerFriend = ObjectAccessor::FindPlayer(ObjectGuid(HighGuid::Player, 0, (*resultFriends)[0].GetUInt32()))) + { + playerFriend->GetSocial()->RemoveFromSocialList(playerguid, SOCIAL_FLAG_ALL); + sSocialMgr->SendFriendStatus(playerFriend, FRIEND_REMOVED, playerguid); + } + } while (resultFriends->NextRow()); + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_ACCOUNT_DATA); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_DECLINED_NAME); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACTION); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_ARENA_STATS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_AURA); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_BGDATA); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_BATTLEGROUND_RANDOM); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_CURRENCY); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_GIFT); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_HOMEBIND); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INSTANCE); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INVENTORY); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_QUESTSTATUS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_QUESTSTATUS_REWARDED); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_REPUTATION); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SPELL); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SPELL_COOLDOWNS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + if (sWorld->getBoolConfig(CONFIG_DELETE_CHARACTER_TICKET_TRACE)) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_PLAYER_GM_TICKETS_ON_CHAR_DELETION); + stmt->setUInt32(0, guid); + trans->Append(stmt); + } + else + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_GM_TICKETS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE_BY_OWNER); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SOCIAL_BY_FRIEND); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SOCIAL_BY_GUID); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEMS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_PET_BY_OWNER); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_PET_DECLINEDNAME_BY_OWNER); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACHIEVEMENTS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACHIEVEMENT_PROGRESS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_EQUIPMENTSETS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_GUILD_EVENTLOG_BY_PLAYER); + stmt->setUInt32(0, guid); + stmt->setUInt32(1, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_GUILD_BANK_EVENTLOG_BY_PLAYER); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_GLYPHS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_QUESTSTATUS_DAILY); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_QUESTSTATUS_WEEKLY); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_QUESTSTATUS_MONTHLY); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_QUESTSTATUS_SEASONAL); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TALENT); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SKILLS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_STATS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_FISHINGSTEPS); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_REWARDSTATUS_LFG); + stmt->setUInt32(0, guid); + trans->Append(stmt); + + Corpse::DeleteFromDB(playerguid, trans); + break; + } + // The character gets unlinked from the account, the name gets freed up and appears as deleted ingame + case CHAR_DELETE_UNLINK: + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_DELETE_INFO); + stmt->setUInt32(0, guid); + + trans->Append(stmt); + break; + } + default: + TC_LOG_INFO("entities.player.cheat", "Player::DeleteFromDB: Tried to delete player (%s) with unsupported delete method (%u).", + playerguid.ToString().c_str(), charDelete_method); + + if (trans->GetSize() > 0) + CharacterDatabase.CommitTransaction(trans); + return; + } + + CharacterDatabase.CommitTransaction(trans); + + if (updateRealmChars) + sWorld->UpdateRealmCharCount(accountId); + + sCharacterCache->DeleteCharacterCacheEntry(playerguid, name); +} + +/** + * Characters which were kept back in the database after being deleted and are now too old (see config option "CharDelete.KeepDays"), will be completely deleted. + * + * @see Player::DeleteFromDB + */ +void Player::DeleteOldCharacters() +{ + uint32 keepDays = sWorld->getIntConfig(CONFIG_CHARDELETE_KEEP_DAYS); + if (!keepDays) + return; + + Player::DeleteOldCharacters(keepDays); +} + +/** + * Characters which were kept back in the database after being deleted and are older than the specified amount of days, will be completely deleted. + * + * @see Player::DeleteFromDB + * + * @param keepDays overwrite the config option by another amount of days + */ +void Player::DeleteOldCharacters(uint32 keepDays) +{ + TC_LOG_INFO("entities.player", "Player::DeleteOldCharacters: Deleting all characters which have been deleted %u days before...", keepDays); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_OLD_CHARS); + stmt->setUInt32(0, uint32(GameTime::GetGameTime() - time_t(keepDays * DAY))); + PreparedQueryResult result = CharacterDatabase.Query(stmt); + + if (result) + { + TC_LOG_DEBUG("entities.player", "Player::DeleteOldCharacters: Found " UI64FMTD " character(s) to delete", result->GetRowCount()); + do + { + Field* fields = result->Fetch(); + Player::DeleteFromDB(ObjectGuid(HighGuid::Player, fields[0].GetUInt32()), fields[1].GetUInt32(), true, true); + } + while (result->NextRow()); + } +} + +/* Preconditions: + - a resurrectable corpse must not be loaded for the player (only bones) + - the player must be in world +*/ +void Player::BuildPlayerRepop() +{ + WorldPacket data(SMSG_PRE_RESURRECT, GetPackGUID().size()); + data << GetPackGUID(); + SendDirectMessage(&data); + + // If the player has the Wisp racial then cast the Wisp aura on them + if (HasSpell(20585)) + CastSpell(this, 20584, true); + CastSpell(this, 8326, true); + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Release); + + // there must be SMSG.FORCE_RUN_SPEED_CHANGE, SMSG.FORCE_SWIM_SPEED_CHANGE, SMSG.MOVE_WATER_WALK + // there must be SMSG.STOP_MIRROR_TIMER + + // the player cannot have a corpse already on current map, only bones which are not returned by GetCorpse + WorldLocation corpseLocation = GetCorpseLocation(); + if (corpseLocation.GetMapId() == GetMapId()) + { + TC_LOG_ERROR("entities.player", "Player::BuildPlayerRepop: Player '%s' (%s) already has a corpse", GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + // create a corpse and place it at the player's location + Corpse* corpse = CreateCorpse(); + if (!corpse) + { + TC_LOG_ERROR("entities.player", "Player::BuildPlayerRepop: Error creating corpse for player '%s' (%s)", GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + GetMap()->AddToMap(corpse); + + // convert player body to ghost + setDeathState(DEAD); + SetHealth(1); + + SetWaterWalking(true); + if (!GetSession()->isLogingOut() && !HasUnitState(UNIT_STATE_STUNNED)) + SetRooted(false); + + // BG - remove insignia related + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_SKINNABLE); + + int32 corpseReclaimDelay = CalculateCorpseReclaimDelay(); + + if (corpseReclaimDelay >= 0) + SendCorpseReclaimDelay(corpseReclaimDelay); + + // to prevent cheating + corpse->ResetGhostTime(); + + StopMirrorTimers(); //disable timers(bars) + + // OnPlayerRepop hook + sScriptMgr->OnPlayerRepop(this); +} + +void Player::ResurrectPlayer(float restore_percent, bool applySickness) +{ + WorldPackets::Misc::DeathReleaseLoc packet; + packet.MapID = -1; + SendDirectMessage(packet.Write()); + + // speed change, land walk + + // remove death flag + set aura + RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_IS_OUT_OF_BOUNDS); + + // This must be called always even on Players with race != RACE_NIGHTELF in case of faction change + RemoveAurasDueToSpell(20584); // RACE_NIGHTELF speed bonuses + RemoveAurasDueToSpell(8326); // SPELL_AURA_GHOST + + if (GetSession()->IsARecruiter() || (GetSession()->GetRecruiterId() != 0)) + SetFlag(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_REFER_A_FRIEND); + + setDeathState(ALIVE); + + SetWaterWalking(false); + if (!HasUnitState(UNIT_STATE_STUNNED)) + SetRooted(false); + + m_deathTimer = 0; + + // set health/powers (0- will be set in caller) + if (restore_percent > 0.0f) + { + SetHealth(uint32(GetMaxHealth()*restore_percent)); + SetPower(POWER_MANA, uint32(GetMaxPower(POWER_MANA)*restore_percent)); + SetPower(POWER_RAGE, 0); + SetPower(POWER_ENERGY, uint32(GetMaxPower(POWER_ENERGY)*restore_percent)); + SetPower(POWER_FOCUS, uint32(GetMaxPower(POWER_FOCUS)*restore_percent)); + SetPower(POWER_ECLIPSE, 0); + } + + // trigger update zone for alive state zone updates + uint32 newzone, newarea; + GetZoneAndAreaId(newzone, newarea); + UpdateZone(newzone, newarea); + sOutdoorPvPMgr->HandlePlayerResurrects(this, newzone); + + if (InBattleground()) + { + if (Battleground* bg = GetBattleground()) + bg->HandlePlayerResurrect(this); + } + + // update visibility + UpdateObjectVisibility(); + + // recast lost by death auras of any items held in the inventory + CastAllObtainSpells(); + + if (!applySickness) + return; + + //Characters from level 1-10 are not affected by resurrection sickness. + //Characters from level 11-19 will suffer from one minute of sickness + //for each level they are above 10. + //Characters level 20 and up suffer from ten minutes of sickness. + int32 startLevel = sWorld->getIntConfig(CONFIG_DEATH_SICKNESS_LEVEL); + + if (int32(getLevel()) >= startLevel) + { + // set resurrection sickness + CastSpell(this, 15007, true); + + // not full duration + if (int32(getLevel()) < startLevel+9) + { + int32 delta = (int32(getLevel()) - startLevel + 1)*MINUTE; + + if (Aura* aur = GetAura(15007, GetGUID())) + { + aur->SetDuration(delta*IN_MILLISECONDS); + } + } + } +} + +void Player::KillPlayer() +{ + if (IsFlying() && !GetTransport()) + GetMotionMaster()->MoveFall(); + + SetRooted(true); + + StopMirrorTimers(); //disable timers(bars) + + setDeathState(CORPSE); + //SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_IN_PVP); + + SetUInt32Value(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_NONE); + ApplyModFlag(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTE_RELEASE_TIMER, !sMapStore.LookupEntry(GetMapId())->Instanceable() && !HasAuraType(SPELL_AURA_PREVENT_RESURRECTION)); + + // 6 minutes until repop at graveyard + m_deathTimer = 6 * MINUTE * IN_MILLISECONDS; + + UpdateCorpseReclaimDelay(); // dependent at use SetDeathPvP() call before kill + + int32 corpseReclaimDelay = CalculateCorpseReclaimDelay(); + + if (corpseReclaimDelay >= 0) + SendCorpseReclaimDelay(corpseReclaimDelay); + + // don't create corpse at this moment, player might be falling + + // update visibility + UpdateObjectVisibility(); +} + +void Player::OfflineResurrect(ObjectGuid const& guid, CharacterDatabaseTransaction& trans) +{ + Corpse::DeleteFromDB(guid, trans); + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_ADD_AT_LOGIN_FLAG); + stmt->setUInt16(0, AT_LOGIN_RESURRECT); + stmt->setUInt32(1, guid.GetCounter()); + CharacterDatabase.ExecuteOrAppend(trans, stmt); +} + +Corpse* Player::CreateCorpse() +{ + // prevent the existence of 2 corpses for one player + SpawnCorpseBones(); + + uint32 _cfb1, _cfb2; + + Corpse* corpse = new Corpse((m_ExtraFlags & PLAYER_EXTRA_PVP_DEATH) ? CORPSE_RESURRECTABLE_PVP : CORPSE_RESURRECTABLE_PVE); + SetPvPDeath(false); + + if (!corpse->Create(GetMap()->GenerateLowGuid(), this)) + { + delete corpse; + return nullptr; + } + + _corpseLocation.WorldRelocate(*this); + + uint8 skin = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + uint8 face = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_FACE_ID); + uint8 hairstyle = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_STYLE_ID); + uint8 haircolor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + uint8 facialStyle = GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_FACIAL_STYLE); + + _cfb1 = ((0x00) | (getRace() << 8) | (GetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_GENDER) << 16) | (skin << 24)); + _cfb2 = ((face) | (hairstyle << 8) | (haircolor << 16) | (facialStyle << 24)); + + corpse->SetUInt32Value(CORPSE_FIELD_BYTES_1, _cfb1); + corpse->SetUInt32Value(CORPSE_FIELD_BYTES_2, _cfb2); + + uint32 flags = CORPSE_FLAG_UNK2; + + if (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_HIDE_HELM)) + flags |= CORPSE_FLAG_HIDE_HELM; + + if (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_HIDE_CLOAK)) + flags |= CORPSE_FLAG_HIDE_CLOAK; + + if (InBattleground() && !InArena()) + flags |= CORPSE_FLAG_LOOTABLE; // to be able to remove insignia + + corpse->SetUInt32Value(CORPSE_FIELD_FLAGS, flags); + corpse->SetUInt32Value(CORPSE_FIELD_DISPLAY_ID, GetNativeDisplayId()); + + uint32 iDisplayID; + uint32 iIventoryType; + uint32 _cfi; + + for (uint8 i = 0; i < EQUIPMENT_SLOT_END; i++) + { + if (m_items[i]) + { + iDisplayID = m_items[i]->GetDisplayId(); + iIventoryType = m_items[i]->GetTemplate()->GetInventoryType(); + + _cfi = iDisplayID | (iIventoryType << 24); + corpse->SetUInt32Value(CORPSE_FIELD_ITEM + i, _cfi); + } + } + + // register for player, but not show + GetMap()->AddCorpse(corpse); + + // we do not need to save corpses for BG/arenas + if (!GetMap()->IsBattlegroundOrArena()) + corpse->SaveToDB(); + + return corpse; +} + +void Player::SpawnCorpseBones(bool triggerSave /*= true*/) +{ + _corpseLocation.WorldRelocate(); + if (GetMap()->ConvertCorpseToBones(GetGUID())) + if (triggerSave && !GetSession()->PlayerLogoutWithSave()) // at logout we will already store the player + SaveToDB(); // prevent loading as ghost without corpse +} + +Corpse* Player::GetCorpse() const +{ + return GetMap()->GetCorpseByPlayer(GetGUID()); +} + +void Player::DurabilityLossAll(double percent, bool inventory) +{ + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + DurabilityLoss(pItem, percent); + + if (inventory) + { + // bags not have durability + // for (int i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + DurabilityLoss(pItem, percent); + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + if (Item* pItem = GetItemByPos(i, j)) + DurabilityLoss(pItem, percent); + } +} + +void Player::DurabilityLoss(Item* item, double percent) +{ + if (!item) + return; + + uint32 pMaxDurability = item ->GetUInt32Value(ITEM_FIELD_MAXDURABILITY); + + if (!pMaxDurability) + return; + + percent /= GetTotalAuraMultiplier(SPELL_AURA_MOD_DURABILITY_LOSS); + + uint32 pDurabilityLoss = uint32(pMaxDurability*percent); + + if (pDurabilityLoss < 1) + pDurabilityLoss = 1; + + DurabilityPointsLoss(item, pDurabilityLoss); +} + +void Player::DurabilityPointsLossAll(int32 points, bool inventory) +{ + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + DurabilityPointsLoss(pItem, points); + + if (inventory) + { + // bags not have durability + // for (int i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + DurabilityPointsLoss(pItem, points); + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + if (Bag* pBag = (Bag*)GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + if (Item* pItem = GetItemByPos(i, j)) + DurabilityPointsLoss(pItem, points); + } +} + +void Player::DurabilityPointsLoss(Item* item, int32 points) +{ + if (HasAuraType(SPELL_AURA_PREVENT_DURABILITY_LOSS)) + return; + + int32 pMaxDurability = item->GetUInt32Value(ITEM_FIELD_MAXDURABILITY); + int32 pOldDurability = item->GetUInt32Value(ITEM_FIELD_DURABILITY); + int32 pNewDurability = pOldDurability - points; + + if (pNewDurability < 0) + pNewDurability = 0; + else if (pNewDurability > pMaxDurability) + pNewDurability = pMaxDurability; + + if (pOldDurability != pNewDurability) + { + // modify item stats _before_ Durability set to 0 to pass _ApplyItemMods internal check + if (pNewDurability == 0 && pOldDurability > 0 && item->IsEquipped()) + _ApplyItemMods(item, item->GetSlot(), false); + + item->SetUInt32Value(ITEM_FIELD_DURABILITY, pNewDurability); + + // modify item stats _after_ restore durability to pass _ApplyItemMods internal check + if (pNewDurability > 0 && pOldDurability == 0 && item->IsEquipped()) + _ApplyItemMods(item, item->GetSlot(), true); + + item->SetState(ITEM_CHANGED, this); + } +} + +void Player::DurabilityPointLossForEquipSlot(EquipmentSlots slot) +{ + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + DurabilityPointsLoss(pItem, 1); +} + +void Player::DurabilityRepairAll(bool takeCost, float discountMod, bool guildBank) +{ + // Collecting all items that can be repaired and repair costs + std::list> itemRepairCostStore; + + // equipped, backpack, bags itself + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; i++) + if (Item* item = GetItemByPos(((INVENTORY_SLOT_BAG_0 << 8) | i))) + if (uint32 cost = item->CalculateDurabilityRepairCost(discountMod)) + itemRepairCostStore.push_back(std::make_pair(item, cost)); + + + // bank, buyback and keys not repaired + + // items in inventory bags + for (uint8 j = INVENTORY_SLOT_BAG_START; j < INVENTORY_SLOT_BAG_END; j++) + for (uint8 i = 0; i < MAX_BAG_SIZE; i++) + if (Item* item = GetItemByPos(((j << 8) | i))) + if (uint32 cost = item->CalculateDurabilityRepairCost(discountMod)) + itemRepairCostStore.push_back(std::make_pair(item, cost)); + + // Handling a free repair case - just repair every item without taking cost. + if (!takeCost) + { + for (auto const& [item, cost] : itemRepairCostStore) + DurabilityRepair(item->GetPos(), false, 0.f); + + return; + } + + // Handling a free repair case - just repair every item without taking cost. + if (!takeCost) + { + for (auto const& [item, cost] : itemRepairCostStore) + DurabilityRepair(item->GetPos(), false, 0.f); + + return; + } + + if (guildBank) + { + // Handling a repair for guild money case. + // We have to repair items one by one until the guild bank has enough money available for withdrawal or until all items are repaired. + + Guild* guild = GetGuild(); + if (!guild) + return; // silent return, client shouldn't display this button for players without guild. + + uint64 const availableGuildMoney = guild->GetMemberAvailableMoneyForRepairItems(GetGUID()); + if (availableGuildMoney == 0) + return; + + // Sort the items by repair cost from lowest to highest + itemRepairCostStore.sort([](auto const& a, auto const& b) -> bool + { + return a.second < b.second; + }); + + // We must calculate total repair cost and take money once to avoid spam in the guild bank log and reduce number of transactions in the database + uint32 totalCost = 0; + + for (auto const& [item, cost] : itemRepairCostStore) + { + uint64 newTotalCost = totalCost + cost; + if (newTotalCost > availableGuildMoney || newTotalCost > MAX_MONEY_AMOUNT) + break; + + totalCost = static_cast(newTotalCost); + + // Repair item without taking cost. We'll do it later. + DurabilityRepair(item->GetPos(), false, 0.f); + } + + // Take money for repairs from the guild bank + guild->HandleMemberWithdrawMoney(GetSession(), totalCost, true); + } + else + { + // Handling a repair for player's money case. + // Unlike repairing for guild money, in this case we must first check if player has enough money to repair all the items at once. + + uint32 totalCost = 0; + for (auto const& [item, cost] : itemRepairCostStore) + totalCost += cost; + + if (!HasEnoughMoney(uint64(totalCost))) + return; // silent return, client should display error by itself and not send opcode. + + ModifyMoney(-int32(totalCost)); + + // Payment for repair has already been taken, so just repair every item without taking cost. + for (auto const& [item, cost] : itemRepairCostStore) + DurabilityRepair(item->GetPos(), false, 0.f); + } +} + +void Player::DurabilityRepair(uint16 pos, bool takeCost, float discountMod) +{ + Item* item = GetItemByPos(pos); + if (!item) + return; + + if (takeCost) + { + uint32 cost = item->CalculateDurabilityRepairCost(discountMod); + + if (!HasEnoughMoney(uint64(cost))) + { + TC_LOG_DEBUG("entities.player.items", "Player::DurabilityRepair: Player '%s' (%s) has not enough money to repair item", + GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + ModifyMoney(-int32(cost)); + } + + bool isBroken = item->IsBroken(); + + item->SetUInt32Value(ITEM_FIELD_DURABILITY, item->GetUInt32Value(ITEM_FIELD_MAXDURABILITY)); + item->SetState(ITEM_CHANGED, this); + + // reapply mods for total broken and repaired item if equipped + if (IsEquipmentPos(pos) && isBroken) + _ApplyItemMods(item, pos & 255, true); +} + +void Player::RepopAtGraveyard() +{ + // note: this can be called also when the player is alive + // for example from WorldSession::HandleMovementOpcodes + + AreaTableEntry const* zone = sAreaTableStore.LookupEntry(GetAreaId()); + + // Such zones are considered unreachable as a ghost and the player must be automatically revived + if ((!IsAlive() && zone && zone->GetFlags().HasFlag(AreaFlags::NoGhostOnRelease)) || GetTransport() || GetPositionZ() < GetMap()->GetMinHeight(GetPhaseShift(), GetPositionX(), GetPositionY())) + { + ResurrectPlayer(0.5f); + SpawnCorpseBones(); + } + + WorldSafeLocsEntry const* ClosestGrave; + + // Special handle for battleground maps + if (Battleground* bg = GetBattleground()) + ClosestGrave = bg->GetClosestGraveyard(this); + else + { + if (Battlefield* bf = sBattlefieldMgr->GetBattlefieldToZoneId(GetMap(), GetZoneId())) + ClosestGrave = bf->GetClosestGraveyard(this); + else + ClosestGrave = sObjectMgr->GetClosestGraveyard(*this, GetTeam(), this); + } + + // stop countdown until repop + m_deathTimer = 0; + + // if no grave found, stay at the current location + // and don't show spirit healer location + if (ClosestGrave) + { + float const* orientation = sObjectMgr->GetGraveyardOrientation(ClosestGrave->ID); + TeleportTo(ClosestGrave->Continent, ClosestGrave->Loc.X, ClosestGrave->Loc.Y, ClosestGrave->Loc.Z, orientation ? *orientation : GetOrientation()); + if (isDead()) // not send if alive, because it used in TeleportTo() + { + WorldPackets::Misc::DeathReleaseLoc packet; + packet.MapID = ClosestGrave->Continent; + packet.Loc.Pos = { ClosestGrave->Loc.X, ClosestGrave->Loc.Y, ClosestGrave->Loc.Z }; + SendDirectMessage(packet.Write()); + } + } + else if (GetPositionZ() < GetMap()->GetMinHeight(GetPhaseShift(), GetPositionX(), GetPositionY())) + TeleportTo(m_homebindMapId, m_homebindX, m_homebindY, m_homebindZ, GetOrientation()); + + RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_IS_OUT_OF_BOUNDS); +} + +bool Player::CanJoinConstantChannelInZone(ChatChannelsEntry const* channel, AreaTableEntry const* zone) const +{ + if (channel->Flags & CHANNEL_DBC_FLAG_ZONE_DEP && zone->GetFlags().HasFlag(AreaFlags::NoChatChannels)) + return false; + + if ((channel->Flags & CHANNEL_DBC_FLAG_CITY_ONLY) && (!(zone->GetFlags().HasFlag(AreaFlags::AllowTradeChannel)))) + return false; + + if ((channel->Flags & CHANNEL_DBC_FLAG_GUILD_REQ) && GetGuildId()) + return false; + + return true; +} + +void Player::JoinedChannel(Channel* c) +{ + m_channels.push_back(c); +} + +void Player::LeftChannel(Channel* c) +{ + m_channels.remove(c); +} + +void Player::CleanupChannels() +{ + while (!m_channels.empty()) + { + Channel* ch = *m_channels.begin(); + m_channels.erase(m_channels.begin()); // remove from player's channel list + ch->LeaveChannel(this, false); // not send to client, not remove from player's channel list + + // delete channel if empty + if (ChannelMgr* cMgr = ChannelMgr::forTeam(GetTeam())) + { + if (ch->IsConstant()) + cMgr->LeftChannel(ch->GetChannelId(), ch->GetZoneEntry()); + else + cMgr->LeftChannel(ch->GetName()); + } + } + TC_LOG_DEBUG("chat.system", "Player::CleanupChannels: Channels of player '%s' (%s) cleaned up.", GetName().c_str(), GetGUID().ToString().c_str()); +} + +void Player::UpdateLocalChannels(uint32 newZone) +{ + if (GetSession()->PlayerLoading() && !IsBeingTeleportedFar()) + return; // The client handles it automatically after loading, but not after teleporting + + AreaTableEntry const* current_zone = sAreaTableStore.LookupEntry(newZone); + if (!current_zone) + return; + + ChannelMgr* cMgr = ChannelMgr::forTeam(GetTeam()); + if (!cMgr) + return; + + for (uint32 i = 0; i < sChatChannelsStore.GetNumRows(); ++i) + { + ChatChannelsEntry const* channelEntry = sChatChannelsStore.LookupEntry(i); + if (!channelEntry) + continue; + + if (!(channelEntry->Flags & CHANNEL_DBC_FLAG_INITIAL)) + continue; + + Channel* usedChannel = nullptr; + for (Channel* channel : m_channels) + { + if (channel->GetChannelId() == i) + { + usedChannel = channel; + break; + } + } + + Channel* removeChannel = nullptr; + Channel* joinChannel = nullptr; + bool sendRemove = true; + + if (CanJoinConstantChannelInZone(channelEntry, current_zone)) + { + if (!(channelEntry->Flags & CHANNEL_DBC_FLAG_GLOBAL)) + { + if (channelEntry->Flags & CHANNEL_DBC_FLAG_CITY_ONLY && usedChannel) + continue; // Already on the channel, as city channel names are not changing + + joinChannel = cMgr->GetJoinChannel(channelEntry->ID, std::string(), current_zone); + if (usedChannel) + { + if (joinChannel != usedChannel) + { + removeChannel = usedChannel; + sendRemove = false; // Do not send leave channel, it already replaced at client + } + else + joinChannel = nullptr; + } + } + else + joinChannel = cMgr->GetJoinChannel(channelEntry->ID, std::string()); + } + else + removeChannel = usedChannel; + + if (joinChannel) + joinChannel->JoinChannel(this, ""); // Changed Channel: ... or Joined Channel: ... + + if (removeChannel) + { + removeChannel->LeaveChannel(this, sendRemove); // Leave old channel + + LeftChannel(removeChannel); // Remove from player's channel list + cMgr->LeftChannel(removeChannel->GetChannelId(), removeChannel->GetZoneEntry()); // Delete if empty + } + } +} + +void Player::LeaveLFGChannel() +{ + for (JoinedChannelsList::iterator i = m_channels.begin(); i != m_channels.end(); ++i) + { + if ((*i)->IsLFG()) + { + (*i)->LeaveChannel(this); + break; + } + } +} + +void Player::HandleBaseModFlatValue(BaseModGroup modGroup, float amount, bool apply) +{ + if (modGroup >= BASEMOD_END) + { + TC_LOG_ERROR("spells", "Player::HandleBaseModValue: Invalid BaseModGroup/BaseModType (%u/%u) for player '%s' (%s)", + modGroup, FLAT_MOD, GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + m_auraBaseFlatMod[modGroup] += apply ? amount : -amount; + UpdateBaseModGroup(modGroup); +} + +void Player::ApplyBaseModPctValue(BaseModGroup modGroup, float pct) +{ + if (modGroup >= BASEMOD_END) + { + TC_LOG_ERROR("spells", "Player::HandleBaseModValue: Invalid BaseModGroup/BaseModType (%u/%u) for player '%s' (%s)", + modGroup, FLAT_MOD, GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + AddPct(m_auraBasePctMod[modGroup], pct); + UpdateBaseModGroup(modGroup); +} + +void Player::SetBaseModFlatValue(BaseModGroup modGroup, float val) +{ + if (m_auraBaseFlatMod[modGroup] == val) + return; + + m_auraBaseFlatMod[modGroup] = val; + UpdateBaseModGroup(modGroup); +} + +void Player::SetBaseModPctValue(BaseModGroup modGroup, float val) +{ + if (m_auraBasePctMod[modGroup] == val) + return; + + m_auraBasePctMod[modGroup] = val; + UpdateBaseModGroup(modGroup); +} + +void Player::UpdateDamageDoneMods(WeaponAttackType attackType, int32 skipEnchantSlot /*= -1*/) +{ + Unit::UpdateDamageDoneMods(attackType, skipEnchantSlot); + + UnitMods unitMod; + switch (attackType) + { + case BASE_ATTACK: + unitMod = UNIT_MOD_DAMAGE_MAINHAND; + break; + case OFF_ATTACK: + unitMod = UNIT_MOD_DAMAGE_OFFHAND; + break; + case RANGED_ATTACK: + unitMod = UNIT_MOD_DAMAGE_RANGED; + break; + default: + ABORT(); + break; + } + + float amount = 0.0f; + Item* item = GetWeaponForAttack(attackType, true); + if (!item) + return; + + for (uint8 slot = 0; slot < MAX_ENCHANTMENT_SLOT; ++slot) + { + if (skipEnchantSlot == slot) + continue; + + SpellItemEnchantmentEntry const* enchantmentEntry = sSpellItemEnchantmentStore.LookupEntry(item->GetEnchantmentId(EnchantmentSlot(slot))); + if (!enchantmentEntry) + continue; + + for (uint8 i = 0; i < MAX_ITEM_ENCHANTMENT_EFFECTS; ++i) + { + switch (enchantmentEntry->Effect[i]) + { + case ITEM_ENCHANTMENT_TYPE_DAMAGE: + amount += enchantmentEntry->EffectPointsMin[i]; + break; + case ITEM_ENCHANTMENT_TYPE_TOTEM: + if (getClass() == CLASS_SHAMAN) + amount += enchantmentEntry->EffectPointsMin[i] * item->GetTemplate()->GetDelay() / 1000.0f; + break; + default: + break; + } + } + } + + HandleStatFlatModifier(unitMod, TOTAL_VALUE, amount, true); +} + +void Player::UpdateBaseModGroup(BaseModGroup modGroup) +{ + if (!CanModifyStats()) + return; + + switch (modGroup) + { + case CRIT_PERCENTAGE: UpdateCritPercentage(BASE_ATTACK); break; + case RANGED_CRIT_PERCENTAGE: UpdateCritPercentage(RANGED_ATTACK); break; + case OFFHAND_CRIT_PERCENTAGE: UpdateCritPercentage(OFF_ATTACK); break; + default: break; + } +} + +float Player::GetBaseModValue(BaseModGroup modGroup, BaseModType modType) const +{ + if (modGroup >= BASEMOD_END || modType >= MOD_END) + { + TC_LOG_ERROR("spells", "Player::GetBaseModValue: Invalid BaseModGroup/BaseModType (%u/%u) for player '%s' (%s)", + modGroup, modType, GetName().c_str(), GetGUID().ToString().c_str()); + return 0.0f; + } + + return (modType == FLAT_MOD ? m_auraBaseFlatMod[modGroup] : m_auraBasePctMod[modGroup]); +} + +float Player::GetTotalBaseModValue(BaseModGroup modGroup) const +{ + if (modGroup >= BASEMOD_END) + { + TC_LOG_ERROR("spells", "Player::GetTotalBaseModValue: Invalid BaseModGroup (%u) for player '%s' (%s)", + modGroup, GetName().c_str(), GetGUID().ToString().c_str()); + return 0.0f; + } + + return m_auraBaseFlatMod[modGroup] * m_auraBasePctMod[modGroup]; +} + +float Player::GetMeleeCritFromAgility() const +{ + uint8 level = getLevel(); + uint32 pclass = getClass(); + + if (level > GT_MAX_LEVEL) + level = GT_MAX_LEVEL; + + GtChanceToMeleeCritBaseEntry const* critBase = sGtChanceToMeleeCritBaseStore.LookupEntry(pclass-1); + GtChanceToMeleeCritEntry const* critRatio = sGtChanceToMeleeCritStore.LookupEntry((pclass-1)*GT_MAX_LEVEL + level-1); + if (critBase == nullptr || critRatio == nullptr) + return 0.0f; + + float crit = critBase->base + GetStat(StatType::Agility) * critRatio->ratio; + return crit * 100.0f; +} + +void Player::GetDodgeFromAgility(float &diminishing, float &nondiminishing) const +{ + /* + "Patch 4.2.0 (2011-06-28): Death Knights, Paladins, and Warriors no + longer receive any bonus to their chance to dodge from Agility. Their base chance to dodge is now a fixed 5%." + */ + if (getClass() == CLASS_WARRIOR || getClass() == CLASS_PALADIN || getClass() == CLASS_DEATH_KNIGHT) + { + nondiminishing += 5.f; + return; + } + + // Table for base dodge values + std::array const baseDodge = + { + 0.037580f, // Warrior (obsolete since 4.2) + 0.036520f, // Paladin (obsolete since 4.2) + -0.054500f, // Hunter + -0.005900f, // Rogue + 0.031830f, // Priest + 0.036640f, // Death Knight (obsolete since 4.2) + 0.016750f, // Shaman + 0.034575f, // Mage + 0.020350f, // Warlock + 0.0f, // ?? + 0.049510f // Druid + }; + + // Crit to dodge coefficient (Todo: these coefficients are based on previous author's code so they still need to be validated) + std::array const critToDodge = + { + 0.f, // Warrior (obsolete since 4.2) + 0.f, // Paladin (obsolete since 4.2) + 0.74f, // Hunter + 1.33f, // Rogue + 0.67f, // Priest + 0.f, // Death Knight (absolete since 4.2) + 1.07f, // Shaman + 0.66f, // Mage + 0.65f, // Warlock + 0.f, // ?? + 1.32f // Druid + }; + + uint8 level = getLevel(); + uint32 playerClass = getClass(); + + if (level > GT_MAX_LEVEL) + level = GT_MAX_LEVEL; + + // Dodge per agility is proportional to crit per agility, which is available from DBC files + GtChanceToMeleeCritEntry const* dodgeRatio = sGtChanceToMeleeCritStore.LookupEntry((playerClass - 1) * GT_MAX_LEVEL + level - 1); + if (!dodgeRatio || playerClass > MAX_CLASSES) + return; + + int32 baseAgility = GetStats().GetBaseStatValue(StatType::Agility); + int32 bonusAgility = GetStat(StatType::Agility) - baseAgility; + + // calculate diminishing (green in char screen) and non-diminishing (white) contribution + diminishing = 100.0f * bonusAgility * dodgeRatio->ratio * critToDodge[playerClass -1 ]; + nondiminishing = 100.0f * (baseDodge[playerClass - 1] + baseAgility * dodgeRatio->ratio * critToDodge[playerClass -1 ]); +} + +float Player::GetSpellCritFromIntellect() const +{ + uint8 level = getLevel(); + uint32 pclass = getClass(); + + if (level > GT_MAX_LEVEL) + level = GT_MAX_LEVEL; + + GtChanceToSpellCritBaseEntry const* critBase = sGtChanceToSpellCritBaseStore.LookupEntry(pclass - 1); + GtChanceToSpellCritEntry const* critRatio = sGtChanceToSpellCritStore.LookupEntry((pclass - 1) * GT_MAX_LEVEL + level - 1); + if (critBase == nullptr || critRatio == nullptr) + return 0.0f; + + float crit = critBase->base + GetStat(StatType::Intellect) * critRatio->ratio; + return crit * 100.0f; +} + +float Player::GetRatingMultiplier(CombatRating cr) const +{ + uint8 level = getLevel(); + + if (level > GT_MAX_LEVEL) + level = GT_MAX_LEVEL; + + GtCombatRatingsEntry const* Rating = sGtCombatRatingsStore.LookupEntry(cr*GT_MAX_LEVEL+level-1); + // gtOCTClassCombatRatingScalarStore.dbc starts with 1, CombatRating with zero, so cr+1 + GtOCTClassCombatRatingScalarEntry const* classRating = sGtOCTClassCombatRatingScalarStore.LookupEntry((getClass()-1)*GT_MAX_RATING+cr+1); + if (!Rating || !classRating) + return 1.0f; // By default use minimum coefficient (not must be called) + + return classRating->ratio / Rating->ratio; +} + +float Player::GetRatingBonusValue(CombatRating cr) const +{ + float baseResult = float(GetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1 + AsUnderlyingType(cr))) * GetRatingMultiplier(cr); + if (cr != CR_RESILIENCE_PLAYER_DAMAGE_TAKEN) + return baseResult; + return float(1.0f - pow(0.99f, baseResult)) * 100.0f; +} + +float Player::GetExpertiseDodgeOrParryReduction(WeaponAttackType attType) const +{ + switch (attType) + { + case BASE_ATTACK: + return GetUInt32Value(PLAYER_EXPERTISE) / 4.0f; + case OFF_ATTACK: + return GetUInt32Value(PLAYER_OFFHAND_EXPERTISE) / 4.0f; + default: + break; + } + return 0.0f; +} + +void Player::ApplyRatingMod(CombatRating combatRating, int32 value, bool apply) +{ + // explicit affected values + switch (combatRating) + { + case CR_HASTE_MELEE: + { + ApplyAttackTimePercentMod(BASE_ATTACK, m_baseRatingValue[combatRating] * GetRatingMultiplier(combatRating), false); + ApplyAttackTimePercentMod(BASE_ATTACK, (m_baseRatingValue[combatRating] + (apply ? value : -value)) * GetRatingMultiplier(combatRating), true); + ApplyAttackTimePercentMod(OFF_ATTACK, m_baseRatingValue[combatRating] * GetRatingMultiplier(combatRating), false); + ApplyAttackTimePercentMod(OFF_ATTACK, (m_baseRatingValue[combatRating] + (apply ? value : -value)) * GetRatingMultiplier(combatRating), true); + + // Haste Rating affects all three haste types so we only have to apply it once here + ApplyHasteRegenMod(m_baseRatingValue[combatRating] * GetRatingMultiplier(combatRating), false); + ApplyHasteRegenMod((m_baseRatingValue[combatRating] + (apply ? value : -value)) * GetRatingMultiplier(combatRating), true); + break; + } + case CR_HASTE_RANGED: + { + ApplyAttackTimePercentMod(RANGED_ATTACK, m_baseRatingValue[combatRating] * GetRatingMultiplier(combatRating), false); + ApplyAttackTimePercentMod(RANGED_ATTACK, (m_baseRatingValue[combatRating] + (apply ? value : -value)) * GetRatingMultiplier(combatRating), true); + break; + } + case CR_HASTE_SPELL: + { + ApplyCastTimePercentMod(m_baseRatingValue[combatRating] * GetRatingMultiplier(combatRating), false); + ApplyCastTimePercentMod((m_baseRatingValue[combatRating] + (apply ? value : -value)) * GetRatingMultiplier(combatRating), true); + break; + } + default: + break; + } + + m_baseRatingValue[combatRating] +=(apply ? value : -value); + + UpdateRating(combatRating); +} + +void Player::UpdateRating(CombatRating cr) +{ + int32 amount = m_baseRatingValue[cr]; + // Apply bonus from SPELL_AURA_MOD_RATING_FROM_STAT + // stat used stored in miscValueB for this aura + AuraEffectList const& modRatingFromStat = GetAuraEffectsByType(SPELL_AURA_MOD_RATING_FROM_STAT); + for (AuraEffectList::const_iterator i = modRatingFromStat.begin(); i != modRatingFromStat.end(); ++i) + if ((*i)->GetMiscValue() & (1<GetMiscValueB())), (*i)->GetAmount())); + if (amount < 0) + amount = 0; + SetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1 + AsUnderlyingType(cr), uint32(amount)); + + bool affectStats = CanModifyStats(); + + switch (cr) + { + case CR_WEAPON_SKILL: + case CR_DEFENSE_SKILL: + break; + case CR_DODGE: + UpdateDodgePercentage(); + break; + case CR_PARRY: + UpdateParryPercentage(); + break; + case CR_BLOCK: + UpdateBlockPercentage(); + break; + case CR_HIT_MELEE: + UpdateMeleeHitChances(); + break; + case CR_HIT_RANGED: + UpdateRangedHitChances(); + break; + case CR_HIT_SPELL: + UpdateSpellHitChances(); + break; + case CR_CRIT_MELEE: + if (affectStats) + { + UpdateCritPercentage(BASE_ATTACK); + UpdateCritPercentage(OFF_ATTACK); + } + break; + case CR_CRIT_RANGED: + if (affectStats) + UpdateCritPercentage(RANGED_ATTACK); + break; + case CR_CRIT_SPELL: + if (affectStats) + UpdateAllSpellCritChances(); + break; + case CR_HIT_TAKEN_MELEE: // Deprecated since Cataclysm + case CR_HIT_TAKEN_RANGED: // Deprecated since Cataclysm + case CR_HIT_TAKEN_SPELL: // Deprecated since Cataclysm + case CR_RESILIENCE_PLAYER_DAMAGE_TAKEN: + case CR_RESILIENCE_CRIT_TAKEN: + case CR_CRIT_TAKEN_SPELL: // Deprecated since Cataclysm + case CR_ARMOR_PENETRATION: // Deprecated since Cataclysm + break; + case CR_HASTE_MELEE: // Implemented in Player::ApplyRatingMod + case CR_HASTE_RANGED: + case CR_HASTE_SPELL: + break; + case CR_WEAPON_SKILL_MAINHAND: // Implemented in Unit::RollMeleeOutcomeAgainst + case CR_WEAPON_SKILL_OFFHAND: + case CR_WEAPON_SKILL_RANGED: + break; + case CR_EXPERTISE: + if (affectStats) + { + UpdateExpertise(BASE_ATTACK); + UpdateExpertise(OFF_ATTACK); + } + break; + case CR_MASTERY: + UpdateMastery(); + break; + } +} + +void Player::UpdateAllRatings() +{ + for (uint8 cr = 0; cr < MAX_COMBAT_RATING; ++cr) + UpdateRating(CombatRating(cr)); +} + +void Player::SetRegularAttackTime() +{ + for (uint8 i = 0; i < MAX_ATTACK; ++i) + { + Item* tmpitem = GetWeaponForAttack(WeaponAttackType(i), true); + if (tmpitem && !tmpitem->IsBroken()) + { + ItemTemplate const* proto = tmpitem->GetTemplate(); + if (proto->GetDelay()) + SetBaseAttackTime(WeaponAttackType(i), proto->GetDelay()); + } + else + SetBaseAttackTime(WeaponAttackType(i), BASE_ATTACK_TIME); // If there is no weapon reset attack time to base (might have been changed from forms) + } +} + +void Player::StoreRaidMapDifficulty() +{ + m_raidMapDifficulty = GetMap()->GetDifficulty(); +} + +//skill+step, checking for max value +bool Player::UpdateSkill(uint32 skill_id, uint32 step) +{ + if (!skill_id) + return false; + + SkillStatusMap::iterator itr = mSkillStatus.find(skill_id); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED) + return false; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; // itr->second.pos % 2 + + uint16 value = GetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset); + uint16 max = GetUInt16Value(PLAYER_SKILL_MAX_RANK_0 + field, offset); + + if (!max || !value || value >= max) + return false; + + uint32 new_value = value + step; + if (new_value > max) + new_value = max; + + SetSkillRank(itr->second.pos, new_value); + if (itr->second.uState != SKILL_NEW) + itr->second.uState = SKILL_CHANGED; + + UpdateSkillEnchantments(skill_id, value, new_value); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, skill_id); + + if (skill_id == SKILL_ARCHAEOLOGY) + _archaeology->Update(); + + return true; +} + +inline int32 SkillGainChance(Player const* player, uint32 SkillValue, uint32 GrayLevel, uint32 GreenLevel, uint32 YellowLevel) +{ + int32 chance = 0; + if (SkillValue >= GrayLevel) + chance = sWorld->getIntConfig(CONFIG_SKILL_CHANCE_GREY) * 10; + else if (SkillValue >= GreenLevel) + chance = sWorld->getIntConfig(CONFIG_SKILL_CHANCE_GREEN) * 10; + else if (SkillValue >= YellowLevel) + chance = sWorld->getIntConfig(CONFIG_SKILL_CHANCE_YELLOW) * 10; + else + chance = sWorld->getIntConfig(CONFIG_SKILL_CHANCE_ORANGE) * 10; + + // SPELL_AURA_INCREASE_SKILL_GAIN_CHANCE bonus + chance *= player->GetTotalAuraMultiplier(SPELL_AURA_INCREASE_SKILL_GAIN_CHANCE); + + return chance; +} + +bool Player::UpdateCraftSkill(SpellInfo const* spellInfo) +{ + if (spellInfo->HasAttribute(SPELL_ATTR1_NO_SKILL_INCREASE)) + return false; + + TC_LOG_DEBUG("entities.player.skills", "Player::UpdateCraftSkill: Player '%s' (%s), SpellID: %d", + GetName().c_str(), GetGUID().ToString().c_str(), spellInfo->Id); + + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellInfo->Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + { + if (_spell_idx->second->SkillLine) + { + uint32 SkillValue = GetPureSkillValue(_spell_idx->second->SkillLine); + + // Alchemy Discoveries here + if (spellInfo->Mechanic == MECHANIC_DISCOVERY) + { + if (uint32 discoveredSpell = GetSkillDiscoverySpell(_spell_idx->second->SkillLine, spellInfo->Id, this)) + LearnSpell(discoveredSpell, false); + } + + uint32 craft_skill_gain = sWorld->getIntConfig(CONFIG_SKILL_GAIN_CRAFTING); + uint32 craft_skill_points = _spell_idx->second->NumSkillUps; + + if (craft_skill_points) + { + craft_skill_points *= craft_skill_gain; + return UpdateSkillPro(_spell_idx->second->SkillLine, SkillGainChance(this, SkillValue, + _spell_idx->second->TrivialSkillLineRankHigh, + (_spell_idx->second->TrivialSkillLineRankHigh + _spell_idx->second->TrivialSkillLineRankLow) / 2, + _spell_idx->second->TrivialSkillLineRankLow), + craft_skill_points); + } + + return UpdateSkillPro(_spell_idx->second->SkillLine, SkillGainChance(this, SkillValue, + _spell_idx->second->TrivialSkillLineRankHigh, + (_spell_idx->second->TrivialSkillLineRankHigh + _spell_idx->second->TrivialSkillLineRankLow) / 2, + _spell_idx->second->TrivialSkillLineRankLow), + craft_skill_gain); + } + } + return false; +} + +bool Player::UpdateGatherSkill(uint32 SkillId, uint32 SkillValue, uint32 RedLevel, uint32 Multiplicator) +{ + TC_LOG_DEBUG("entities.player.skills", "UpdateGatherSkill(SkillId %d SkillLevel %d RedLevel %d)", SkillId, SkillValue, RedLevel); + + uint32 gathering_skill_gain = sWorld->getIntConfig(CONFIG_SKILL_GAIN_GATHERING); + + // For skinning and Mining chance decrease with level. 1-74 - no decrease, 75-149 - 2 times, 225-299 - 8 times + switch (SkillId) + { + case SKILL_ARCHAEOLOGY: + return UpdateSkillPro(SkillId, ((SkillValue < 50) ? 1000 : 0) * Multiplicator, gathering_skill_gain); + case SKILL_HERBALISM: + case SKILL_LOCKPICKING: + case SKILL_JEWELCRAFTING: + case SKILL_INSCRIPTION: + return UpdateSkillPro(SkillId, SkillGainChance(this, SkillValue, RedLevel+100, RedLevel+50, RedLevel+25)*Multiplicator, gathering_skill_gain); + case SKILL_SKINNING: + if (sWorld->getIntConfig(CONFIG_SKILL_CHANCE_SKINNING_STEPS) == 0) + return UpdateSkillPro(SkillId, SkillGainChance(this, SkillValue, RedLevel+100, RedLevel+50, RedLevel+25)*Multiplicator, gathering_skill_gain); + else + return UpdateSkillPro(SkillId, (SkillGainChance(this, SkillValue, RedLevel+100, RedLevel+50, RedLevel+25)*Multiplicator) >> (SkillValue/sWorld->getIntConfig(CONFIG_SKILL_CHANCE_SKINNING_STEPS)), gathering_skill_gain); + case SKILL_MINING: + if (sWorld->getIntConfig(CONFIG_SKILL_CHANCE_MINING_STEPS) == 0) + return UpdateSkillPro(SkillId, SkillGainChance(this, SkillValue, RedLevel+100, RedLevel+50, RedLevel+25)*Multiplicator, gathering_skill_gain); + else + return UpdateSkillPro(SkillId, (SkillGainChance(this, SkillValue, RedLevel+100, RedLevel+50, RedLevel+25)*Multiplicator) >> (SkillValue/sWorld->getIntConfig(CONFIG_SKILL_CHANCE_MINING_STEPS)), gathering_skill_gain); + } + return false; +} + +uint8 GetFishingStepsNeededToLevelUp(uint32 SkillValue) +{ + // These formulas are guessed to be as close as possible to how the skill difficulty curve for fishing was on Retail. + if (SkillValue < 75) + return 1; + + if (SkillValue <= 300) + return SkillValue / 44; + + return SkillValue / 31; +} + +bool Player::UpdateFishingSkill() +{ + TC_LOG_DEBUG("entities.player.skills", "Player::UpdateFishingSkill: Player '%s' (%s)", GetName().c_str(), GetGUID().ToString().c_str()); + + uint32 SkillValue = GetPureSkillValue(SKILL_FISHING); + + if (SkillValue >= GetMaxSkillValue(SKILL_FISHING)) + return false; + + uint8 stepsNeededToLevelUp = GetFishingStepsNeededToLevelUp(SkillValue); + ++m_fishingSteps; + + if (m_fishingSteps >= stepsNeededToLevelUp) + { + m_fishingSteps = 0; + + uint32 gathering_skill_gain = sWorld->getIntConfig(CONFIG_SKILL_GAIN_GATHERING); + return UpdateSkillPro(SKILL_FISHING, 100*10, gathering_skill_gain); + } + + return false; +} + +void Player::SurveyDigSite() +{ + _archaeology->UseSite(); +} + +void Player::NotifyRequestResearchHistory() +{ + _archaeology->SendResearchHistory(); +} + +bool Player::ArchProjectCompleteable(uint16 projectId) +{ + return _archaeology->ProjectCompleteable(projectId); +} + +bool Player::HasArchProject(uint16 projectId) +{ + return _archaeology->ProjectExists(projectId); +} + +void Player::CompleteArchProject(uint16 projectId) +{ + _archaeology->CompleteProject(projectId); +} + +void Player::SetArchData(ArchData const& data) +{ + _archaeology->SetArchData(data); +} + +bool Player::UpdateSkillPro(uint16 skillId, int32 chance, uint32 step) +{ + // levels sync. with spell requirement for skill levels to learn + // bonus abilities in sSkillLineAbilityStore + // Used only to avoid scan DBC at each skill grow + static uint32 bonusSkillLevels[] = { 75, 150, 225, 300, 375, 450, 525 }; + static const size_t bonusSkillLevelsSize = sizeof(bonusSkillLevels) / sizeof(uint32); + + TC_LOG_DEBUG("entities.player.skills", "UpdateSkillPro(SkillId %d, Chance %3.1f%%)", skillId, chance / 10.0f); + if (!skillId) + return false; + + if (chance <= 0) // speedup in 0 chance case + { + TC_LOG_DEBUG("entities.player.skills", "Player::UpdateSkillPro Chance=%3.1f%% missed", chance / 10.0f); + return false; + } + + SkillStatusMap::iterator itr = mSkillStatus.find(skillId); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED) + return false; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; // itr->second.pos % 2 + + uint16 value = GetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset); + uint16 max = GetUInt16Value(PLAYER_SKILL_MAX_RANK_0 + field, offset); + + if (!max || !value || value >= max) + return false; + + if (irand(1, 1000) > chance) + { + TC_LOG_DEBUG("entities.player.skills", "Player::UpdateSkillPro Chance=%3.1f%% missed", chance / 10.0f); + return false; + } + + uint16 new_value = value + step; + if (new_value > max) + new_value = max; + + SetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset, new_value); + if (itr->second.uState != SKILL_NEW) + itr->second.uState = SKILL_CHANGED; + + for (size_t i = 0; i < bonusSkillLevelsSize; ++i) + { + uint32 bsl = bonusSkillLevels[i]; + if (value < bsl && new_value >= bsl) + { + LearnSkillRewardedSpells(skillId, new_value); + break; + } + } + + UpdateSkillEnchantments(skillId, value, new_value); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, skillId); + if (Guild* guild = GetGuild()) + guild->UpdateMemberData(this, GUILD_MEMBER_DATA_PROFESSIONS, 0); + + TC_LOG_DEBUG("entities.player.skills", "Player::UpdateSkillPro Chance=%3.1f%% taken", chance / 10.0f); + + return true; +} + +void Player::ModifySkillBonus(uint32 skillid, int32 val, bool talent) +{ + SkillStatusMap::const_iterator itr = mSkillStatus.find(skillid); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED) + return; + + uint16 field = itr->second.pos / 2 + (talent ? PLAYER_SKILL_TALENT_0 : PLAYER_SKILL_MODIFIER_0); + uint8 offset = itr->second.pos & 1; // itr->second.pos % 2 + + uint16 bonus = GetUInt16Value(field, offset); + + SetUInt16Value(field, offset, bonus + val); +} + +void Player::UpdateSkillsForLevel() +{ + uint32 maxSkill = GetMaxSkillValueForLevel(); + + for (SkillStatusMap::iterator itr = mSkillStatus.begin(); itr != mSkillStatus.end(); ++itr) + { + if (itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos)) + continue; + + uint32 pskill = itr->first; + SkillRaceClassInfoEntry const* rcEntry = sDBCManager.GetSkillRaceClassInfo(pskill, getRace(), getClass()); + if (!rcEntry) + continue; + + if (GetSkillRangeType(rcEntry) != SKILL_RANGE_LEVEL) + continue; + + if (GetSkillRangeType(rcEntry) == SKILL_RANGE_LEVEL) + { + if (rcEntry->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + SetSkillRank(itr->second.pos, maxSkill); + + SetSkillMaxRank(itr->second.pos, maxSkill); + if (itr->second.uState != SKILL_NEW) + itr->second.uState = SKILL_CHANGED; + } + + // Update level dependent skillline spells + LearnSkillRewardedSpells(rcEntry->SkillID, GetSkillRank(itr->second.pos)); + } +} + +void Player::InitializeSkillFields() +{ + uint32 i = 0; + for (SkillLineEntry const* skillLine : sSkillLineStore) + { + if (sDBCManager.GetSkillRaceClassInfo(skillLine->ID, getRace(), getClass())) + { + SetSkillLineId(i, skillLine->ID); + mSkillStatus.insert(SkillStatusMap::value_type(skillLine->ID, SkillStatusData(i, SKILL_UNCHANGED))); + if (++i >= PLAYER_MAX_SKILLS) + break; + } + } +} + +// This functions sets a skill line value (and adds if doesn't exist yet) +// To "remove" a skill line, set it's values to zero +void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal) +{ + SkillLineEntry const* skillEntry = sSkillLineStore.LookupEntry(id); + if (!skillEntry) + { + TC_LOG_ERROR("misc", "Player::SetSkill: Skill (SkillID: %u) not found in SkillLineStore for player '%s' (%s)", + id, GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + uint16 currVal; + SkillStatusMap::iterator itr = mSkillStatus.find(id); + + auto refreshSkillBonusAuras = [&] + { + // Temporary bonuses + for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL)) + if (effect->GetMiscValue() == int32(id)) + effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true); + + // Permanent bonuses + for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL_TALENT)) + if (effect->GetMiscValue() == int32(id)) + effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true); + }; + + // Handle already stored skills + if (itr != mSkillStatus.end()) + { + currVal = GetSkillRank(itr->second.pos); + + // Activate and update skill line + if (newVal) + { + // if skill value is going down, update enchantments before setting the new value + if (newVal < currVal) + UpdateSkillEnchantments(id, currVal, newVal); + + // update step + SetSkillStep(itr->second.pos, step); + // update value + SetSkillRank(itr->second.pos, newVal); + SetSkillMaxRank(itr->second.pos, maxVal); + + LearnSkillRewardedSpells(id, newVal); + // if skill value is going up, update enchantments after setting the new value + if (newVal > currVal) + { + UpdateSkillEnchantments(id, currVal, newVal); + if (id == SKILL_RIDING) + UpdateMountCapability(); + } + + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id); + + // update skill state + if (itr->second.uState == SKILL_UNCHANGED || itr->second.uState == SKILL_DELETED) + { + if (currVal == 0) // activated skill, mark as new to save into database + { + itr->second.uState = itr->second.uState != SKILL_DELETED + ? SKILL_NEW + : SKILL_CHANGED; // skills marked as SKILL_DELETED already exist in database, mark as changed instead of new + + if (skillEntry->CategoryID == SKILL_CATEGORY_PROFESSION) + { + if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1)) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1, id); + else if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1)) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1, id); + } + + refreshSkillBonusAuras(); + } + else // updated skill, mark as changed to save into database + itr->second.uState = SKILL_CHANGED; + } + + // archaeology handling + if (id == SKILL_ARCHAEOLOGY) + { + if (itr->second.uState == SKILL_NEW) + _archaeology->Learn(); + + _archaeology->Update(); + } + } + else if (currVal && !newVal) // Deactivate skill line + { + //remove enchantments needing this skill + UpdateSkillEnchantments(id, currVal, 0); + // clear skill fields + SetSkillStep(itr->second.pos, 0); + SetSkillRank(itr->second.pos, 0); + SetSkillMaxRank(itr->second.pos, 0); + SetSkillTempBonus(itr->second.pos, 0); + SetSkillPermBonus(itr->second.pos, 0); + + // mark as deleted so the next save will delete the data from the database + itr->second.uState = itr->second.uState != SKILL_NEW + ? SKILL_DELETED + : SKILL_UNCHANGED; // skills marked as SKILL_NEW don't exist in database (this distinction is not neccessary for deletion but for re-learning the same skill before save to db happens) + + // remove all spells that related to this skill + if (std::vector const* skillLineAbilities = sDBCManager.GetSkillLineAbilitiesBySkill(id)) + for (SkillLineAbilityEntry const* skillLineAbility : *skillLineAbilities) + RemoveSpell(sSpellMgr->GetFirstSpellInChain(skillLineAbility->Spell)); + + // Clear profession lines + if (GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1) == id) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1, 0); + else if (GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1) == id) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1, 0); + + // archaeology skill unlearned + if (id == SKILL_ARCHAEOLOGY) + _archaeology->UnLearn(); + } + } + else + { + // We are about to learn a skill that has been added outside of normal circumstances (Game Master command, scripts etc.) + uint8 skillSlot = 0; + + // Find a free skill slot + for (uint32 i = 0; i < PLAYER_MAX_SKILLS; ++i) + { + if (!GetUInt16Value(PLAYER_SKILL_LINEID_0 + i / 2, i & 1)) + { + skillSlot = i; + break; + } + } + + if (!skillSlot) + { + TC_LOG_ERROR("misc", "Tried to add skill %u but player %s (%s) cannot have additional skills", id, GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + if (skillEntry->CategoryID == SKILL_CATEGORY_PROFESSION) + { + if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1)) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1, id); + else if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1)) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1, id); + } + + if (itr == mSkillStatus.end()) + SetSkillLineId(skillSlot, id); + + SetSkillStep(skillSlot, step); + SetSkillRank(skillSlot, newVal); + SetSkillMaxRank(skillSlot, maxVal); + + // apply skill bonuses + SetSkillTempBonus(skillSlot, 0); + SetSkillPermBonus(skillSlot, 0); + + UpdateSkillEnchantments(id, 0, newVal); + + mSkillStatus.insert(SkillStatusMap::value_type(id, SkillStatusData(skillSlot, SKILL_NEW))); + + if (newVal) + { + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id); + + refreshSkillBonusAuras(); + + // archaeology skill learned + if (id == SKILL_ARCHAEOLOGY) + _archaeology->Learn(); + + // Learn all spells for skill + LearnSkillRewardedSpells(id, newVal); + } + } + + if (Guild* guild = GetGuild()) + guild->UpdateMemberData(this, GUILD_MEMBER_DATA_PROFESSIONS, 0); +} + +bool Player::HasSkill(uint32 skill) const +{ + if (!skill) + return false; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + return (itr != mSkillStatus.end() && itr->second.uState != SKILL_DELETED && GetSkillRank(itr->second.pos)); +} + +uint16 Player::GetSkillStep(uint16 skill) const +{ + if (!skill) + return 0; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos)) + return 0; + + return GetUInt16Value(PLAYER_SKILL_STEP_0 + itr->second.pos / 2, itr->second.pos & 1); +} + +uint16 Player::GetSkillValue(uint32 skill) const +{ + if (!skill) + return 0; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos)) + return 0; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; + + int32 result = int32(GetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset)); + result += int32(GetUInt16Value(PLAYER_SKILL_MODIFIER_0 + field, offset)); + result += int32(GetUInt16Value(PLAYER_SKILL_TALENT_0 + field, offset)); + return (result < 0 ? 0 : result); +} + +uint16 Player::GetMaxSkillValue(uint32 skill) const +{ + if (!skill) + return 0; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos)) + return 0; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; + + int32 result = int32(GetUInt16Value(PLAYER_SKILL_MAX_RANK_0 + field, offset)); + result += int32(GetUInt16Value(PLAYER_SKILL_MODIFIER_0 + field, offset)); + result += int32(GetUInt16Value(PLAYER_SKILL_TALENT_0 + field, offset)); + return (result < 0 ? 0 : result); +} + +uint16 Player::GetPureMaxSkillValue(uint32 skill) const +{ + if (!skill) + return 0; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || GetSkillRank(itr->second.pos)) + return 0; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; + + return GetUInt16Value(PLAYER_SKILL_MAX_RANK_0 + field, offset); +} + +uint16 Player::GetBaseSkillValue(uint32 skill) const +{ + if (!skill) + return 0; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos)) + return 0; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; + + int32 result = int32(GetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset)); + result += int32(GetUInt16Value(PLAYER_SKILL_TALENT_0 + field, offset)); + return (result < 0 ? 0 : result); +} + +uint16 Player::GetPureSkillValue(uint32 skill) const +{ + if (!skill) + return 0; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos)) + return 0; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; + + return GetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset); +} + +int16 Player::GetSkillPermBonusValue(uint32 skill) const +{ + if (!skill) + return 0; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos)) + return 0; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; + + return GetUInt16Value(PLAYER_SKILL_TALENT_0 + field, offset); +} + +int16 Player::GetSkillTempBonusValue(uint32 skill) const +{ + if (!skill) + return 0; + + SkillStatusMap::const_iterator itr = mSkillStatus.find(skill); + if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos)) + return 0; + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; + + return GetUInt16Value(PLAYER_SKILL_MODIFIER_0 + field, offset); +} + +void Player::SendActionButtons(uint32 state) const +{ + WorldPackets::Spells::UpdateActionButtons packet; + + if (state != 2) + { + for (auto itr = m_actionButtons.begin(); itr != m_actionButtons.end(); ++itr) + if (itr->second.uState != ACTIONBUTTON_DELETED && itr->first < packet.ActionButtons.size()) + packet.ActionButtons[itr->first] = itr->second.packedData; + } + packet.Reason = state; + + SendDirectMessage(packet.Write()); +} + +bool Player::IsActionButtonDataValid(uint8 button, uint32 action, uint8 type) const +{ + if (button >= MAX_ACTION_BUTTONS) + { + TC_LOG_ERROR("entities.player", "Player::IsActionButtonDataValid: Action %u not added into button %u for player %s (%s): button must be < %u", + action, button, GetName().c_str(), GetGUID().ToString().c_str(), MAX_ACTION_BUTTONS); + return false; + } + + if (action >= MAX_ACTION_BUTTON_ACTION_VALUE) + { + TC_LOG_ERROR("entities.player", "Player::IsActionButtonDataValid: Action %u not added into button %u for player %s (%s): action must be < %u", + action, button, GetName().c_str(), GetGUID().ToString().c_str(), MAX_ACTION_BUTTON_ACTION_VALUE); + return false; + } + + switch (type) + { + case ACTION_BUTTON_SPELL: + if (!sSpellMgr->GetSpellInfo(action)) + { + TC_LOG_DEBUG("entities.player", "Player::IsActionButtonDataValid: Spell action %u not added into button %u for player %s (%s): spell does not exist. This can be due to a character imported from a different expansion", + action, button, GetName().c_str(), GetGUID().ToString().c_str()); + return false; + } + break; + case ACTION_BUTTON_ITEM: + if (!sObjectMgr->GetItemTemplate(action)) + { + TC_LOG_ERROR("entities.player", "Player::IsActionButtonDataValid: Item action %u not added into button %u for player %s (%s): item not exist", + action, button, GetName().c_str(), GetGUID().ToString().c_str()); + return false; + } + break; + case ACTION_BUTTON_C: + case ACTION_BUTTON_CMACRO: + case ACTION_BUTTON_MACRO: + case ACTION_BUTTON_EQSET: + case ACTION_BUTTON_DROPDOWN: + break; + default: + TC_LOG_ERROR("entities.player", "Player::IsActionButtonDataValid: Unknown action type %u", type); + return false; // other cases not checked at this moment + } + + return true; +} + +ActionButton* Player::addActionButton(uint8 button, uint32 action, uint8 type) +{ + if (!IsActionButtonDataValid(button, action, type)) + return nullptr; + + // it create new button (NEW state) if need or return existing + ActionButton& ab = m_actionButtons[button]; + + // set data and update to CHANGED if not NEW + ab.SetActionAndType(action, ActionButtonType(type)); + + TC_LOG_DEBUG("entities.player", "Player::AddActionButton: Player '%s' (%s) added action '%u' (type %u) to button '%u'", + GetName().c_str(), GetGUID().ToString().c_str(), action, type, button); + return &ab; +} + +void Player::removeActionButton(uint8 button) +{ + ActionButtonList::iterator buttonItr = m_actionButtons.find(button); + if (buttonItr == m_actionButtons.end() || buttonItr->second.uState == ACTIONBUTTON_DELETED) + return; + + if (buttonItr->second.uState == ACTIONBUTTON_NEW) + m_actionButtons.erase(buttonItr); // new and not saved + else + buttonItr->second.uState = ACTIONBUTTON_DELETED; // saved, will deleted at next save + + TC_LOG_DEBUG("entities.player", "Player::RemoveActionButton: Player '%s' (%s) removed action button '%u'", + GetName().c_str(), GetGUID().ToString().c_str(), button); +} + +ActionButton const* Player::GetActionButton(uint8 button) +{ + ActionButtonList::iterator buttonItr = m_actionButtons.find(button); + if (buttonItr == m_actionButtons.end() || buttonItr->second.uState == ACTIONBUTTON_DELETED) + return nullptr; + + return &buttonItr->second; +} + +bool Player::UpdatePosition(float x, float y, float z, float orientation, bool teleport) +{ + uint32 oldWmoGroupId = GetWMOGroupId(); + + if (!Unit::UpdatePosition(x, y, z, orientation, teleport)) + return false; + //if (movementInfo.flags & MOVEMENTFLAG_MOVING) + // mover->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Moving); + //if (movementInfo.flags & MOVEMENTFLAG_TURNING) + // mover->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Turning); + + // Update player zone if needed + if (m_needsZoneUpdate) + { + uint32 newZone, newArea; + GetZoneAndAreaId(newZone, newArea); + UpdateZone(newZone, newArea); + m_needsZoneUpdate = false; + } + + // Add group update flags + if (GetGroup()) + { + // Position has changed + SetGroupUpdateFlag(GROUP_UPDATE_FLAG_POSITION); + + // Player is no longer on a WMO that belongs to the same area + if (oldWmoGroupId != GetWMOGroupId()) + SetGroupUpdateFlag(GROUP_UPDATE_FLAG_WMO_GROUP_ID); + } + + CheckAreaExplore(); + return true; +} + +void Player::SendMessageToSetInRange(WorldPacket const* data, float dist, bool self) const +{ + if (self) + SendDirectMessage(data); + + Trinity::MessageDistDeliverer notifier(this, data, dist); + Cell::VisitWorldObjects(this, notifier, dist); +} + +void Player::SendMessageToSetInRange(WorldPacket const* data, float dist, bool self, bool own_team_only) const +{ + if (self) + SendDirectMessage(data); + + Trinity::MessageDistDeliverer notifier(this, data, dist, own_team_only); + Cell::VisitWorldObjects(this, notifier, dist); +} + +void Player::SendMessageToSet(WorldPacket const* data, Player const* skipped_rcvr) const +{ + if (skipped_rcvr != this) + SendDirectMessage(data); + + // we use World::GetMaxVisibleDistance() because i cannot see why not use a distance + // update: replaced by GetMap()->GetVisibilityDistance() + Trinity::MessageDistDeliverer notifier(this, data, GetVisibilityRange(), false, skipped_rcvr); + Cell::VisitWorldObjects(this, notifier, GetVisibilityRange()); +} + +void Player::SendDirectMessage(WorldPacket const* data) const +{ + m_session->SendPacket(data); +} + +void Player::SendCinematicStart(uint32 cinematicId) +{ + WorldPackets::Misc::TriggerCinematic packet; + packet.CinematicID = cinematicId; + SendDirectMessage(packet.Write()); + + if (CinematicSequencesEntry const* sequence = sCinematicSequencesStore.LookupEntry(cinematicId)) + _cinematicMgr->SetActiveCinematicCamera(sequence->Camera[0]); +} + +void Player::SendMovieStart(uint32 movieId) +{ + SetMovie(movieId); + WorldPackets::Misc::TriggerMovie packet; + packet.MovieID = movieId; + SendDirectMessage(packet.Write()); +} + +void Player::CheckAreaExplore() +{ + if (!IsAlive()) + return; + + if (IsInFlight()) + return; + + uint32 const areaId = GetAreaId(); + SetAreaExplored(areaId); +} + +void Player::SetAreaExplored(uint32 areaId) +{ + AreaTableEntry const* areaEntry = sAreaTableStore.LookupEntry(areaId); + + if (!areaEntry) + { + TC_LOG_ERROR("entities.player", "Player '%s' (%s) discovered unknown area (x: %f y: %f z: %f map: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), GetPositionX(), GetPositionY(), GetPositionZ(), GetMapId()); + return; + } + + uint32 offset = areaEntry->AreaBit / 32; + + if (offset >= PLAYER_EXPLORED_ZONES_SIZE) + { + TC_LOG_ERROR("entities.player", "Player::CheckAreaExploreAndOutdoor: Wrong area flag %u in map data for (X: %f Y: %f) point to field PLAYER_EXPLORED_ZONES_1 + %u ( %u must be < %u ).", + areaEntry->AreaBit, GetPositionX(), GetPositionY(), offset, offset, PLAYER_EXPLORED_ZONES_SIZE); + return; + } + + uint32 val = (uint32)(1 << (areaEntry->AreaBit % 32)); + uint32 currFields = GetUInt32Value(PLAYER_EXPLORED_ZONES_1 + offset); + + if (!(currFields & val)) + { + SetUInt32Value(PLAYER_EXPLORED_ZONES_1 + offset, (uint32)(currFields | val)); + + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_EXPLORE_AREA, GetAreaId()); + + if (areaEntry->ExplorationLevel > 0) + { + if (IsMaxLevel()) + SendExplorationExperience(areaId, 0); + else + { + int32 diff = int32(getLevel()) - areaEntry->ExplorationLevel; + uint32 XP; + if (diff < -5) + { + XP = uint32(sObjectMgr->GetBaseXP(getLevel() + 5) * sWorld->getRate(RATE_XP_EXPLORE)); + } + else if (diff > 5) + { + int32 exploration_percent = 100 - ((diff - 5) * 5); + if (exploration_percent < 0) + exploration_percent = 0; + + XP = uint32(sObjectMgr->GetBaseXP(areaEntry->ExplorationLevel) * exploration_percent / 100 * sWorld->getRate(RATE_XP_EXPLORE)); + } + else + { + XP = uint32(sObjectMgr->GetBaseXP(areaEntry->ExplorationLevel) * sWorld->getRate(RATE_XP_EXPLORE)); + } + + if (sWorld->getIntConfig(CONFIG_MIN_DISCOVERED_SCALED_XP_RATIO)) + { + uint32 minScaledXP = uint32(sObjectMgr->GetBaseXP(areaEntry->ExplorationLevel) * sWorld->getRate(RATE_XP_EXPLORE)) * sWorld->getIntConfig(CONFIG_MIN_DISCOVERED_SCALED_XP_RATIO) / 100; + XP = std::max(minScaledXP, XP); + } + + GiveXP(XP, nullptr); + SendExplorationExperience(areaId, XP); + } + TC_LOG_DEBUG("entities.player", "Player '%s' (%s) discovered a new area: %u", GetName().c_str(), GetGUID().ToString().c_str(), areaId); + } + } +} + +void Player::UpdateZoneAndAreaId() +{ + uint32 newzone = 0, newarea = 0; + GetZoneAndAreaId(newzone, newarea); + + if (m_zoneUpdateId != newzone) + UpdateZone(newzone, newarea); // also update area + else + { + // use area updates as well + // needed for free far all arenas for example + if (m_areaUpdateId != newarea) + UpdateArea(newarea); + } +} + +void Player::UpdateIndoorsOutdoorsAuras() +{ + if (sWorld->getBoolConfig(CONFIG_VMAP_INDOOR_CHECK)) + RemoveAurasWithAttribute(IsOutdoors() ? SPELL_ATTR0_ONLY_INDOORS : SPELL_ATTR0_ONLY_OUTDOORS); +} + +void Player::UpdateTavernRestingState() +{ + AreaTriggerEntry const* atEntry = sAreaTriggerStore.LookupEntry(GetInnTriggerId()); + + if (HasRestFlag(REST_FLAG_IN_TAVERN) && (!atEntry || !IsInAreaTriggerRadius(atEntry))) + RemoveRestFlag(REST_FLAG_IN_TAVERN); + else if (!HasRestFlag(REST_FLAG_IN_TAVERN) && IsInAreaTriggerRadius(atEntry)) + SetRestFlag(REST_FLAG_IN_TAVERN); +} + + +uint32 Player::TeamForRace(uint8 race) +{ + if (ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(race)) + { + switch (rEntry->BaseLanguage) + { + case 1: return HORDE; + case 7: return ALLIANCE; + } + TC_LOG_ERROR("entities.player", "Race (%u) has wrong teamid (%u) in DBC: wrong DBC files?", uint32(race), rEntry->BaseLanguage); + } + else + TC_LOG_ERROR("entities.player", "Race (%u) not found in DBC: wrong DBC files?", uint32(race)); + + return ALLIANCE; +} + +void Player::SetFactionForRace(uint8 race) +{ + m_team = TeamForRace(race); + + ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(race); + SetFaction(rEntry ? rEntry->FactionID : 0); +} + +ReputationRank Player::GetReputationRank(uint32 faction) const +{ + FactionEntry const* factionEntry = sFactionStore.LookupEntry(faction); + return GetReputationMgr().GetRank(factionEntry); +} + +// Calculate total reputation percent player gain with quest/creature level +int32 Player::CalculateReputationGain(ReputationSource source, uint32 creatureOrQuestLevel, int32 rep, int32 faction, bool noQuestBonus) +{ + float percent = 100.0f; + + float repMod = noQuestBonus ? 0.0f : float(GetTotalAuraModifier(SPELL_AURA_MOD_REPUTATION_GAIN)); + + // faction specific auras only seem to apply to kills + if (source == REPUTATION_SOURCE_KILL) + repMod += GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_FACTION_REPUTATION_GAIN, faction); + + percent += rep > 0 ? repMod : -repMod; + + float rate; + switch (source) + { + case REPUTATION_SOURCE_KILL: + rate = sWorld->getRate(RATE_REPUTATION_LOWLEVEL_KILL); + break; + case REPUTATION_SOURCE_QUEST: + case REPUTATION_SOURCE_DAILY_QUEST: + case REPUTATION_SOURCE_WEEKLY_QUEST: + case REPUTATION_SOURCE_MONTHLY_QUEST: + case REPUTATION_SOURCE_REPEATABLE_QUEST: + rate = sWorld->getRate(RATE_REPUTATION_LOWLEVEL_QUEST); + break; + case REPUTATION_SOURCE_SPELL: + default: + rate = 1.0f; + break; + } + + if (rate != 1.0f && creatureOrQuestLevel <= Trinity::XP::GetGrayLevel(getLevel())) + percent *= rate; + + if (percent <= 0.0f) + return 0; + + // Multiply result with the faction specific rate + if (RepRewardRate const* repData = sObjectMgr->GetRepRewardRate(faction)) + { + float repRate = 0.0f; + switch (source) + { + case REPUTATION_SOURCE_KILL: + repRate = repData->creatureRate; + break; + case REPUTATION_SOURCE_QUEST: + repRate = repData->questRate; + break; + case REPUTATION_SOURCE_DAILY_QUEST: + repRate = repData->questDailyRate; + break; + case REPUTATION_SOURCE_WEEKLY_QUEST: + repRate = repData->questWeeklyRate; + break; + case REPUTATION_SOURCE_MONTHLY_QUEST: + repRate = repData->questMonthlyRate; + break; + case REPUTATION_SOURCE_REPEATABLE_QUEST: + repRate = repData->questRepeatableRate; + break; + case REPUTATION_SOURCE_SPELL: + repRate = repData->spellRate; + break; + } + + // for custom, a rate of 0.0 will totally disable reputation gain for this faction/type + if (repRate <= 0.0f) + return 0; + + percent *= repRate; + } + + if (source != REPUTATION_SOURCE_SPELL && GetsRecruitAFriendBonus(false)) + percent *= 1.0f + sWorld->getRate(RATE_REPUTATION_RECRUIT_A_FRIEND_BONUS); + + return CalculatePct(rep, percent); +} + +// Calculates how many reputation and currency the player gets uppon killing an creature +void Player::RewardOnKill(Unit* victim, float rate) +{ + if (!victim || victim->GetTypeId() == TYPEID_PLAYER) + return; + + if (victim->ToCreature()->IsReputationGainDisabled()) + return; + + RewardOnKillEntry const* Rew = sObjectMgr->GetRewardOnKillEntry(victim->ToCreature()->GetCreatureTemplate()->Entry); + + if (!Rew) + return; + + uint32 ChampioningFaction = 0; + + if (GetChampioningFaction()) + { + // support for: Championing - http://www.wowwiki.com/Championing + Map const* map = GetMap(); + if (map->IsNonRaidDungeon()) + { + if (LFGDungeonEntry const* dungeon = DBCManager::GetLFGDungeon(map->GetId(), map->GetDifficulty())) + { + // WotLK and Cataclysm dungeons only grant championing reputation when being in max level dungeons and fighting for a expansion related faction + uint8 expansion = GetExpansionForFaction(GetChampioningFaction()); + if (dungeon->Target_level == DBCManager::GetMaxLevelForExpansion(dungeon->ExpansionLevel) && expansion) + { + if (expansion == dungeon->ExpansionLevel) + ChampioningFaction = GetChampioningFaction(); + } + else + { + // Classic Tabards (Alliance and Horde capital reputation) always convert the gained reputation + if (!expansion) + ChampioningFaction = GetChampioningFaction(); + } + } + } + } + + uint32 team = GetTeam(); + + if (Rew->RepFaction1 && (!Rew->TeamDependent || team == ALLIANCE)) + { + if (FactionEntry const* rewFactionEntry1 = sFactionStore.LookupEntry(Rew->RepFaction1)) + { + // Patch 4.3: Wearing Horde or Alliance racial faction tabards now grants reputation in Burning Crusade dungeons as well. + /* + Note: retail tests have shown that burning crusade dungeons are the only kind of dungeons that grant championing and usual reputation + at the same time which is why we duplicate the reward and modify the 2nd + */ + bool secondaryReputationReward1 = (GetExpansionForFaction(rewFactionEntry1->ParentFactionID) == EXPANSION_THE_BURNING_CRUSADE + || GetExpansionForFaction(Rew->RepFaction1) == EXPANSION_THE_BURNING_CRUSADE); + + uint32 factionId1 = (ChampioningFaction && !secondaryReputationReward1) ? ChampioningFaction : Rew->RepFaction1; + if (rewFactionEntry1->Expansion && !ChampioningFaction) + factionId1 = 0; + + int32 donerep1 = CalculateReputationGain(REPUTATION_SOURCE_KILL, victim->getLevel(), Rew->RepValue1, factionId1); + donerep1 = int32(donerep1 * rate); + + FactionEntry const* factionEntry1 = sFactionStore.LookupEntry(factionId1); + uint32 current_reputation_rank1 = GetReputationMgr().GetRank(factionEntry1); + if (factionEntry1) + GetReputationMgr().ModifyReputation(factionEntry1, donerep1, current_reputation_rank1 > Rew->ReputationMaxCap1); + + if (factionEntry1 && secondaryReputationReward1 && ChampioningFaction) + { + FactionEntry const* bonusFactionEntry1 = sFactionStore.LookupEntry(ChampioningFaction); + uint32 current_bonus_reputation_rank1 = GetReputationMgr().GetRank(bonusFactionEntry1); + + if (bonusFactionEntry1) + GetReputationMgr().ModifyReputation(bonusFactionEntry1, donerep1, current_bonus_reputation_rank1 > Rew->ReputationMaxCap1); + } + } + } + + if (Rew->RepFaction2 && (!Rew->TeamDependent || team == HORDE)) + { + if (FactionEntry const* rewFactionEntry2 = sFactionStore.LookupEntry(Rew->RepFaction2)) + { + bool secondaryReputationReward2 = (GetExpansionForFaction(rewFactionEntry2->ParentFactionID) == EXPANSION_THE_BURNING_CRUSADE + || GetExpansionForFaction(Rew->RepFaction1) == EXPANSION_THE_BURNING_CRUSADE); + + uint32 factionId2 = (ChampioningFaction && secondaryReputationReward2) ? ChampioningFaction : Rew->RepFaction2; + if (rewFactionEntry2->Expansion && !ChampioningFaction) + factionId2 = 0; + + int32 donerep2 = CalculateReputationGain(REPUTATION_SOURCE_KILL, victim->getLevel(), Rew->RepValue2, factionId2); + donerep2 = int32(donerep2 * rate); + + FactionEntry const* factionEntry2 = sFactionStore.LookupEntry(factionId2); + uint32 current_reputation_rank2 = GetReputationMgr().GetRank(factionEntry2); + if (factionEntry2) + GetReputationMgr().ModifyReputation(factionEntry2, donerep2, current_reputation_rank2 > Rew->ReputationMaxCap2); + + if (factionEntry2 && secondaryReputationReward2 && ChampioningFaction) + { + FactionEntry const* bonusFactionEntry2 = sFactionStore.LookupEntry(ChampioningFaction); + uint32 current_bonus_reputation_rank2 = GetReputationMgr().GetRank(bonusFactionEntry2); + + if (bonusFactionEntry2) + GetReputationMgr().ModifyReputation(bonusFactionEntry2, donerep2, current_bonus_reputation_rank2 > Rew->ReputationMaxCap1); + } + } + } + + if (Rew->CurrencyId1 && Rew->CurrencyCount1) + { + CurrencyTypesEntry const* currencyId1 = sCurrencyTypesStore.LookupEntry(Rew->CurrencyId1); + if (currencyId1) + ModifyCurrency(Rew->CurrencyId1, Rew->CurrencyCount1); + } + + if (Rew->CurrencyId2 && Rew->CurrencyCount2) + { + CurrencyTypesEntry const* currencyId2 = sCurrencyTypesStore.LookupEntry(Rew->CurrencyId2); + if (currencyId2) + ModifyCurrency(Rew->CurrencyId2, Rew->CurrencyCount2); + } + + if (Rew->CurrencyId3 && Rew->CurrencyCount3) + { + CurrencyTypesEntry const* currencyId3 = sCurrencyTypesStore.LookupEntry(Rew->CurrencyId3); + if (currencyId3) + ModifyCurrency(Rew->CurrencyId3, Rew->CurrencyCount3); + } +} + +// Calculate how many reputation points player gain with the quest +void Player::RewardReputation(Quest const* quest) +{ + for (uint8 i = 0; i < QUEST_REPUTATIONS_COUNT; ++i) + { + if (!quest->RewardFactionId[i]) + continue; + + int32 rep = 0; + bool noQuestBonus = false; + + if (quest->RewardFactionValueIdOverride[i]) + { + rep = quest->RewardFactionValueIdOverride[i] / 100; + noQuestBonus = true; + } + else + { + uint32 row = ((quest->RewardFactionValueId[i] < 0) ? 1 : 0) + 1; + if (QuestFactionRewEntry const* questFactionRewEntry = sQuestFactionRewardStore.LookupEntry(row)) + { + uint32 field = abs(quest->RewardFactionValueId[i]); + rep = questFactionRewEntry->Difficulty[field]; + } + } + + if (!rep) + continue; + + if (quest->IsDaily()) + rep = CalculateReputationGain(REPUTATION_SOURCE_DAILY_QUEST, GetQuestLevel(quest), rep, quest->RewardFactionId[i], noQuestBonus); + else if (quest->IsWeekly()) + rep = CalculateReputationGain(REPUTATION_SOURCE_WEEKLY_QUEST, GetQuestLevel(quest), rep, quest->RewardFactionId[i], noQuestBonus); + else if (quest->IsMonthly()) + rep = CalculateReputationGain(REPUTATION_SOURCE_MONTHLY_QUEST, GetQuestLevel(quest), rep, quest->RewardFactionId[i], noQuestBonus); + else if (quest->IsRepeatable()) + rep = CalculateReputationGain(REPUTATION_SOURCE_REPEATABLE_QUEST, GetQuestLevel(quest), rep, quest->RewardFactionId[i], noQuestBonus); + else + rep = CalculateReputationGain(REPUTATION_SOURCE_QUEST, GetQuestLevel(quest), rep, quest->RewardFactionId[i], noQuestBonus); + + if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(quest->RewardFactionId[i])) + GetReputationMgr().ModifyReputation(factionEntry, rep); + } +} + +void Player::UpdateHonorFields() +{ + /// called when rewarding honor and at each save + time_t now = time_t(GameTime::GetGameTime()); + time_t today = time_t(GameTime::GetGameTime() / DAY) * DAY; + + if (m_lastHonorUpdateTime < today) + { + time_t yesterday = today - DAY; + + uint16 kills_today = PAIR32_LOPART(GetUInt32Value(PLAYER_FIELD_KILLS)); + + // update yesterday's contribution + if (m_lastHonorUpdateTime >= yesterday) + { + // this is the first update today, reset today's contribution + SetUInt32Value(PLAYER_FIELD_KILLS, MAKE_PAIR32(0, kills_today)); + } + else + { + // no honor/kills yesterday or today, reset + SetUInt32Value(PLAYER_FIELD_KILLS, 0); + } + } + + m_lastHonorUpdateTime = now; +} + +///Calculate the amount of honor gained based on the victim +///and the size of the group for which the honor is divided +///An exact honor value can also be given (overriding the calcs) +bool Player::RewardHonor(Unit* victim, uint32 groupsize, int32 honor, bool pvptoken) +{ + // do not reward honor in arenas, but enable onkill spellproc + if (InArena()) + { + if (!victim || victim == this || victim->GetTypeId() != TYPEID_PLAYER) + return false; + + if (GetBGTeam() == victim->ToPlayer()->GetBGTeam()) + return false; + + return true; + } + + // 'Inactive' this aura prevents the player from gaining honor points and battleground Tokenizer + if (HasAura(SPELL_AURA_PLAYER_INACTIVE)) + return false; + + ObjectGuid victim_guid; + uint32 victim_rank = 0; + + // need call before fields update to have chance move yesterday data to appropriate fields before today data change. + UpdateHonorFields(); + + // do not reward honor in arenas, but return true to enable onkill spellproc + if (InBattleground() && GetBattleground() && GetBattleground()->isArena()) + return true; + + // Promote to float for calculations + float honor_f = (float)honor; + + if (honor_f <= 0) + { + if (!victim || victim == this || victim->HasAuraType(SPELL_AURA_NO_PVP_CREDIT)) + return false; + + victim_guid = victim->GetGUID(); + + if (Player* plrVictim = victim->ToPlayer()) + { + if (GetTeam() == plrVictim->GetTeam() && !sWorld->IsFFAPvPRealm()) + return false; + + uint8 k_level = getLevel(); + uint8 k_grey = Trinity::XP::GetGrayLevel(k_level); + uint8 v_level = victim->getLevel(); + + if (v_level <= k_grey && !sWorld->getIntConfig(CONFIG_MIN_CREATURE_SCALED_XP_RATIO)) + return false; + + // PLAYER_CHOSEN_TITLE VALUES DESCRIPTION + // [0] Just name + // [1..14] Alliance honor titles and player name + // [15..28] Horde honor titles and player name + // [29..38] Other title and player name + // [39+] Nothing + uint32 victim_title = victim->GetUInt32Value(PLAYER_CHOSEN_TITLE); + // Get Killer titles, CharTitlesEntry::bit_index + // Ranks: + // title[1..14] -> rank[5..18] + // title[15..28] -> rank[5..18] + // title[other] -> 0 + if (victim_title == 0) + victim_guid.Clear(); // Don't show HK: message, only log. + else if (victim_title < 15) + victim_rank = victim_title + 4; + else if (victim_title < 29) + victim_rank = victim_title - 14 + 4; + else + victim_guid.Clear(); // Don't show HK: message, only log. + + honor_f = std::ceil(Trinity::Honor::hk_honor_at_level_f(k_level) * (v_level - k_grey) / (k_level - k_grey)); + + // count the number of playerkills in one day + ApplyModUInt32Value(PLAYER_FIELD_KILLS, 1, true); + // and those in a lifetime + ApplyModUInt32Value(PLAYER_FIELD_LIFETIME_HONORABLE_KILLS, 1, true); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_EARN_HONORABLE_KILL); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HK_CLASS, victim->getClass()); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HK_RACE, victim->getRace()); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HONORABLE_KILL_AT_AREA, GetAreaId()); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HONORABLE_KILL, 1, 0, 0, victim); + if (Guild* guild = GetGuild()) + guild->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HONORABLE_KILLS_GUILD, 0, 0, 0, victim, this); + } + else + { + if (!victim->ToCreature()->IsRacialLeader()) + return false; + + honor_f = 100.0f; // ??? need more info + victim_rank = 19; // HK: Leader + } + } + + if (victim != nullptr) + { + if (groupsize > 1) + honor_f /= groupsize; + + // apply honor multiplier from aura (not stacking-get highest) + AddPct(honor_f, GetMaxPositiveAuraModifier(SPELL_AURA_MOD_HONOR_GAIN_PCT)); + } + + honor_f *= sWorld->getRate(RATE_HONOR); + // Back to int now + honor = int32(honor_f); + // honor - for show honor points in log + // victim_guid - for show victim name in log + // victim_rank [1..4] HK: + // victim_rank [5..19] HK: + // victim_rank [0, 20+] HK: <> + WorldPackets::Combat::PvPCredit data; + data.Honor = honor; + data.Target = victim_guid; + data.Rank = victim_rank; + + SendDirectMessage(data.Write()); + + // add honor points + ModifyCurrency(CURRENCY_TYPE_HONOR_POINTS, int32(honor)); + + // Add guild XP + if (Guild *guild = GetGuild()) + guild->GiveXP(uint32(float(honor) * sWorld->getRate(RATE_XP_HONOR_EARNED_GUILD_MODIFIER)), this); + + if (InBattleground() && honor > 0) + { + if (Battleground* bg = GetBattleground()) + { + bg->UpdatePlayerScore(this, SCORE_BONUS_HONOR, honor, false); //false: prevent looping + } + } + + if (sWorld->getBoolConfig(CONFIG_PVP_TOKEN_ENABLE) && pvptoken) + { + if (!victim || victim == this || victim->HasAuraType(SPELL_AURA_NO_PVP_CREDIT)) + return true; + + if (victim->GetTypeId() == TYPEID_PLAYER) + { + // Check if allowed to receive it in current map + uint8 MapType = sWorld->getIntConfig(CONFIG_PVP_TOKEN_MAP_TYPE); + if ((MapType == 1 && !InBattleground() && !IsFFAPvP()) + || (MapType == 2 && !IsFFAPvP()) + || (MapType == 3 && !InBattleground())) + return true; + + uint32 itemId = sWorld->getIntConfig(CONFIG_PVP_TOKEN_ID); + int32 count = sWorld->getIntConfig(CONFIG_PVP_TOKEN_COUNT); + + if (AddItem(itemId, count)) + ChatHandler(GetSession()).PSendSysMessage("You have been awarded a token for slaying another player."); + } + } + + return true; +} + +void Player::_LoadCurrency(PreparedQueryResult result) +{ + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + + uint16 currencyID = fields[0].GetUInt16(); + + CurrencyTypesEntry const* currency = sCurrencyTypesStore.LookupEntry(currencyID); + if (!currency) + continue; + + PlayerCurrency cur; + cur.State = PLAYERCURRENCY_UNCHANGED; + cur.Quantity = fields[1].GetUInt32(); + cur.WeeklyQuantity = fields[2].GetUInt32(); + cur.TrackedQuantity = fields[3].GetUInt32(); + cur.Flags = fields[4].GetUInt8(); + + _currencyStorage.insert(PlayerCurrenciesMap::value_type(currencyID, cur)); + + } while (result->NextRow()); +} + +void Player::_SaveCurrency(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt = nullptr; + for (PlayerCurrenciesMap::iterator itr = _currencyStorage.begin(); itr != _currencyStorage.end(); ++itr) + { + CurrencyTypesEntry const* entry = sCurrencyTypesStore.LookupEntry(itr->first); + if (!entry) // should never happen + continue; + + switch (itr->second.State) + { + case PLAYERCURRENCY_NEW: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_PLAYER_CURRENCY); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt16(1, itr->first); + stmt->setUInt32(2, itr->second.Quantity); + stmt->setUInt32(3, itr->second.WeeklyQuantity); + stmt->setUInt32(4, itr->second.TrackedQuantity); + stmt->setUInt8(5, itr->second.Flags); + trans->Append(stmt); + break; + case PLAYERCURRENCY_CHANGED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_PLAYER_CURRENCY); + stmt->setUInt32(0, itr->second.Quantity); + stmt->setUInt32(1, itr->second.WeeklyQuantity); + stmt->setUInt32(2, itr->second.TrackedQuantity); + stmt->setUInt8(3, itr->second.Flags); + stmt->setUInt32(4, GetGUID().GetCounter()); + stmt->setUInt16(5, itr->first); + trans->Append(stmt); + break; + default: + break; + } + + itr->second.State = PLAYERCURRENCY_UNCHANGED; + } +} + +void Player::SendNewCurrency(uint32 id) const +{ + PlayerCurrenciesMap::const_iterator itr = _currencyStorage.find(id); + if (itr == _currencyStorage.end()) + return; + + CurrencyTypesEntry const* entry = sCurrencyTypesStore.LookupEntry(id); + if (!entry) // should never happen + return; + + WorldPackets::Misc::SetupCurrency packet; + WorldPackets::Misc::SetupCurrency::Record record; + record.Type = entry->ID; + record.Quantity = itr->second.Quantity; + record.WeeklyQuantity = itr->second.WeeklyQuantity; + record.MaxWeeklyQuantity = GetCurrencyWeekCap(entry); + record.TrackedQuantity = itr->second.TrackedQuantity; + record.Flags = itr->second.Flags; + + packet.Data.push_back(record); + + GetSession()->SendPacket(packet.Write()); +} + +void Player::SendCurrencies() const +{ + WorldPackets::Misc::SetupCurrency packet; + + for (PlayerCurrenciesMap::const_iterator itr = _currencyStorage.begin(); itr != _currencyStorage.end(); ++itr) + { + CurrencyTypesEntry const* entry = sCurrencyTypesStore.LookupEntry(itr->first); + if (!entry) + continue; + + uint32 precision = (entry->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) ? CURRENCY_PRECISION : 1; + + WorldPackets::Misc::SetupCurrency::Record record; + record.Type = entry->ID; + record.Quantity = itr->second.Quantity / precision; + + if (itr->second.WeeklyQuantity / precision > 0) + record.WeeklyQuantity = itr->second.WeeklyQuantity / precision; + + if (entry->GetFlags().HasFlag(CurrencyTypeFlags::ComputedWeeklyMaximum) || entry->MaxEarnablePerWeek) + record.MaxWeeklyQuantity = GetCurrencyWeekCap(entry) / precision; + + if (entry->GetFlags().HasFlag(CurrencyTypeFlags::TrackQuantity)) + record.TrackedQuantity = itr->second.TrackedQuantity / precision; + + record.Flags = itr->second.Flags; + packet.Data.push_back(record); + } + + SendDirectMessage(packet.Write()); +} + +void Player::SendPvpRewards() const +{ + WorldPackets::Misc::RequestPVPRewardsResponse packet; + packet.ArenaMaxRewardPointsThisWeek = GetCurrencyWeekCap(CURRENCY_TYPE_CONQUEST_META_ARENA, true); + packet.ArenaRewardPointsThisWeek = GetCurrencyOnWeek(CURRENCY_TYPE_CONQUEST_META_ARENA, true); + packet.MaxRewardPointsThisWeek = GetCurrencyWeekCap(CURRENCY_TYPE_CONQUEST_POINTS, true); + packet.RewardPointsThisWeek = GetCurrencyOnWeek(CURRENCY_TYPE_CONQUEST_POINTS, true); + packet.RatedMaxRewardPointsThisWeek = GetCurrencyWeekCap(CURRENCY_TYPE_CONQUEST_META_RBG, true); + packet.RatedRewardPointsThisWeek = GetCurrencyOnWeek(CURRENCY_TYPE_CONQUEST_META_RBG, true); + + SendDirectMessage(packet.Write()); +} + +uint32 Player::GetCurrency(uint32 id, bool usePrecision) const +{ + PlayerCurrenciesMap::const_iterator itr = _currencyStorage.find(id); + if (itr == _currencyStorage.end()) + return 0; + + CurrencyTypesEntry const* currency = sCurrencyTypesStore.LookupEntry(id); + uint32 precision = (usePrecision && currency->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) ? CURRENCY_PRECISION : 1; + + return itr->second.Quantity / precision; +} + +uint32 Player::GetCurrencyOnWeek(uint32 id, bool usePrecision) const +{ + PlayerCurrenciesMap::const_iterator itr = _currencyStorage.find(id); + if (itr == _currencyStorage.end()) + return 0; + + CurrencyTypesEntry const* currency = sCurrencyTypesStore.LookupEntry(id); + uint32 precision = (usePrecision && currency->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) ? CURRENCY_PRECISION : 1; + + return itr->second.WeeklyQuantity / precision; +} + +uint32 Player::GetCurrencyOnSeason(uint32 id) const +{ + PlayerCurrenciesMap::const_iterator itr = _currencyStorage.find(id); + if (itr == _currencyStorage.end()) + return 0; + + return itr->second.TrackedQuantity; +} + +bool Player::HasCurrency(uint32 id, uint32 count) const +{ + PlayerCurrenciesMap::const_iterator itr = _currencyStorage.find(id); + return itr != _currencyStorage.end() && itr->second.Quantity >= count; +} + +void Player::ModifyCurrency(uint32 currencyId, int32 amount, bool supressChatLog /*= false*/, bool ignoreModifiersAndTracking /*= false*/) +{ + if (amount == 0) + return; + + CurrencyTypesEntry const* currency = sCurrencyTypesStore.LookupEntry(currencyId); + if (!currency) + { + TC_LOG_ERROR("player", "Player %s was attempting to modify a invalid currency (Id: %u).", GetGUID().ToString().c_str(), currencyId); + return; + } + + if (!ignoreModifiersAndTracking) + amount *= GetTotalAuraMultiplierByMiscValue(SPELL_AURA_MOD_CURRENCY_GAIN, currencyId); + + PlayerCurrency* playerCurrency = nullptr; + PlayerCurrenciesMap::iterator itr = _currencyStorage.find(currencyId); + if (itr == _currencyStorage.end()) + itr = _currencyStorage.insert(PlayerCurrenciesMap::value_type(currencyId, PlayerCurrency())).first; + + playerCurrency = &itr->second; + + // Weekly cap + uint32 weeklyCap = GetCurrencyWeekCap(currency); + if (!ignoreModifiersAndTracking) // Refunds and commands bypass the weekly limit + if (weeklyCap > 0 && amount > 0 && (playerCurrency->WeeklyQuantity + amount) > weeklyCap) + amount = weeklyCap - playerCurrency->WeeklyQuantity; + + // Total cap + if (currency->MaxQty > 0 && amount > 0 && (playerCurrency->Quantity + amount) > currency->MaxQty) + { + // Patch 4.0.3a (2010-11-23): Justice Points over the 4,000 hard cap will be immediately converted at a ratio of 47s 50c per Justice Point + // Apparently there are no currency type flags for this so we assume that this is a hardcoded behavior + if (currency->ID == CURRENCY_TYPE_JUSTICE_POINTS) + { + int32 surplousAmount = amount - (currency->MaxQty - playerCurrency->Quantity); + if (surplousAmount > 0) + ModifyMoney(static_cast(surplousAmount) * JUSTICE_POINTS_CONVERSION_MONEY / CURRENCY_PRECISION, false); + } + + amount = currency->MaxQty - playerCurrency->Quantity; + } + + // Underflow protection + if (amount < 0 && uint32(std::abs(amount)) > playerCurrency->Quantity) + amount = playerCurrency->Quantity * -1; + + if (amount == 0) + return; + + if (playerCurrency->State != PLAYERCURRENCY_NEW) + playerCurrency->State = PLAYERCURRENCY_CHANGED; + + playerCurrency->Quantity += amount; + + if (!ignoreModifiersAndTracking) + { + if (weeklyCap) + playerCurrency->WeeklyQuantity += amount; + + if (currency->GetFlags().HasFlag(CurrencyTypeFlags::TrackQuantity)) + playerCurrency->TrackedQuantity += amount; + + if (amount > 0) + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_CURRENCY, currencyId, amount); + } + + // Arena and rated battleground have two seperate currency Ids but display them both in a unified currency. + if (currency->CategoryID == CURRENCY_CATEGORY_META_CONQUEST) + { + ModifyCurrency(CURRENCY_TYPE_CONQUEST_POINTS, amount, supressChatLog, ignoreModifiersAndTracking); + return; + } + + uint8 precision = (currency->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) ? CURRENCY_PRECISION : 1; + + WorldPackets::Misc::SetCurrency setCurrency; + setCurrency.Type = currency->ID; + setCurrency.Quantity = playerCurrency->Quantity / precision; + if (uint32 trackedQuantity = playerCurrency->TrackedQuantity / precision) + setCurrency.TrackedQuantity = trackedQuantity; + if (uint32 weeklyQuantity = playerCurrency->WeeklyQuantity / precision) + setCurrency.WeeklyQuantity = weeklyQuantity; + setCurrency.SuppressChatLog = supressChatLog; + + SendDirectMessage(setCurrency.Write()); + + // Archaeology branch activation + if (itr->second.State == PLAYERCURRENCY_NEW && HasSkill(SKILL_ARCHAEOLOGY) && + currency->CategoryID == CURRENCY_CATEGORY_ARCHAEOLOGY) + _archaeology->ActivateBranch(sArchaeologyMgr->Currency2BranchId(currency->ID)); +} + +void Player::SetCurrency(uint32 currencyId, uint32 amount) +{ + PlayerCurrenciesMap::iterator itr = _currencyStorage.find(currencyId); + if (itr == _currencyStorage.end()) + { + PlayerCurrency cur; + cur.State = PLAYERCURRENCY_NEW; + cur.Quantity = amount; + cur.WeeklyQuantity = 0; + cur.TrackedQuantity = 0; + cur.Flags = 0; + _currencyStorage.insert(PlayerCurrenciesMap::value_type(currencyId, cur)); + } +} + +uint32 Player::GetCurrencyWeekCap(uint32 id, bool usePrecision) const +{ + CurrencyTypesEntry const* entry = sCurrencyTypesStore.LookupEntry(id); + if (!entry) + return 0; + + uint32 precision = (usePrecision && entry->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) ? CURRENCY_PRECISION : 1; + + return GetCurrencyWeekCap(entry) / precision; +} + +void Player::ResetCurrencyWeekCap() +{ + for (uint32 arenaSlot = 0; arenaSlot < MAX_ARENA_SLOT; arenaSlot++) + { + if (uint32 arenaTeamId = GetArenaTeamId(arenaSlot)) + { + ArenaTeam* arenaTeam = sArenaTeamMgr->GetArenaTeamById(arenaTeamId); + arenaTeam->FinishWeek(); // set played this week etc values to 0 in memory, too + arenaTeam->SaveToDB(); // save changes + arenaTeam->NotifyStatsChanged(); // notify the players of the changes + } + } + + for (PlayerCurrenciesMap::iterator itr = _currencyStorage.begin(); itr != _currencyStorage.end(); ++itr) + { + itr->second.WeeklyQuantity = 0; + itr->second.State = PLAYERCURRENCY_CHANGED; + } + + WorldPacket data(SMSG_WEEKLY_RESET_CURRENCY, 0); + SendDirectMessage(&data); +} + +uint32 Player::GetCurrencyWeekCap(CurrencyTypesEntry const* currency) const +{ + if (currency->GetFlags().HasFlag(CurrencyTypeFlags::ComputedWeeklyMaximum)) + { + switch (currency->ID) + { + //original conquest not have week cap + case CURRENCY_TYPE_CONQUEST_POINTS: + return std::max(GetCurrencyWeekCap(CURRENCY_TYPE_CONQUEST_META_ARENA, false), GetCurrencyWeekCap(CURRENCY_TYPE_CONQUEST_META_RBG, false)); + case CURRENCY_TYPE_CONQUEST_META_ARENA: + // should add precision mod = 100 + return Trinity::Currency::ConquestRatingCalculator(_maxPersonalArenaRate) * CURRENCY_PRECISION; + case CURRENCY_TYPE_CONQUEST_META_RBG: + // should add precision mod = 100 + return Trinity::Currency::BgConquestRatingCalculator(GetRBGPersonalRating()) * CURRENCY_PRECISION; + default: + return 0; + } + } + + return currency->MaxEarnablePerWeek; +} + +uint32 Player::GetCurrencyTotalCap(CurrencyTypesEntry const* currency) const +{ + uint32 cap = currency->MaxQty; + + switch (currency->ID) + { + case CURRENCY_TYPE_HONOR_POINTS: + { + uint32 honorcap = sWorld->getIntConfig(CONFIG_CURRENCY_MAX_HONOR_POINTS); + if (honorcap > 0) + cap = honorcap; + break; + } + case CURRENCY_TYPE_JUSTICE_POINTS: + { + uint32 justicecap = sWorld->getIntConfig(CONFIG_CURRENCY_MAX_JUSTICE_POINTS); + if (justicecap > 0) + cap = justicecap; + break; + } + } + + return cap; +} + +void Player::UpdateConquestCurrencyCap(uint32 currency) +{ + uint32 currenciesToUpdate[2] = { currency, CURRENCY_TYPE_CONQUEST_POINTS }; + + for (uint32 i = 0; i < 2; ++i) + { + CurrencyTypesEntry const* currencyEntry = sCurrencyTypesStore.LookupEntry(currenciesToUpdate[i]); + if (!currencyEntry) + continue; + + uint32 precision = (currencyEntry->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) ? 100 : 1; + uint32 cap = GetCurrencyWeekCap(currencyEntry); + + WorldPacket packet(SMSG_UPDATE_CURRENCY_WEEK_LIMIT, 8); + packet << uint32(cap / precision); + packet << uint32(currenciesToUpdate[i]); + SendDirectMessage(&packet); + } +} +void Player::InitializePlayerFlags() +{ + uint32 flags = PLAYER_FLAGS_NONE; + + if (HasCharacterFlag(CHARACTER_FLAG_HIDE_CLOAK)) + flags |= PLAYER_FLAGS_HIDE_CLOAK; + + if (HasCharacterFlag(CHARACTER_FLAG_HIDE_HELM)) + flags |= PLAYER_FLAGS_HIDE_HELM; + + if (HasCharacterFlag(CHARACTER_FLAG_GM_MODE)) + flags |= PLAYER_FLAGS_GM; + + if (HasCharacterFlag(CHARACTER_FLAG_GHOST)) + flags |= PLAYER_FLAGS_GHOST; + + if (HasCharacterFlag(CHARACTER_FLAG_RESTING)) + flags |= PLAYER_FLAGS_RESTING; + + if (HasCharacterFlag(CHARACTER_FLAG_PVP_ENABLED)) + flags |= PLAYER_FLAGS_CONTESTED_PVP; + + if (HasCharacterFlag(CHARACTER_FLAG_PVP_DESIRED)) + flags |= PLAYER_FLAGS_IN_PVP; + + if (HasCharacterFlag(CHARACTER_FLAG_2_NO_XP_GAIN)) + flags |= PLAYER_FLAGS_NO_XP_GAIN; + + if (HasCharacterFlag(CHARACTER_FLAG_2_CAN_USE_VOID_STORAGE_FEATURE)) + flags |= PLAYER_FLAGS_VOID_UNLOCKED; + + if (HasCharacterFlag(CHARACTER_FLAG_2_AUTO_DECLINE_GUILD)) + flags |= PLAYER_FLAGS_AUTO_DECLINE_GUILD; + + if (HasCharacterFlag(CHARACTER_FLAG_2_LOW_LEVEL_RAID_ENABLED)) + flags |= PLAYER_FLAGS_LOW_LEVEL_RAID_ENABLED; + + SetUInt32Value(PLAYER_FLAGS, flags); +} + +void Player::SetInGuild(uint32 guildId) +{ + if (guildId) + SetGuidValue(OBJECT_FIELD_DATA, ObjectGuid(HighGuid::Guild, guildId)); + else + SetGuidValue(OBJECT_FIELD_DATA, ObjectGuid::Empty); + + ApplyModFlag(PLAYER_FLAGS, PLAYER_FLAGS_GUILD_LEVEL_ENABLED, guildId != 0 && sWorld->getBoolConfig(CONFIG_GUILD_LEVELING_ENABLED)); + SetUInt16Value(OBJECT_FIELD_TYPE, 1, guildId != 0); + sCharacterCache->UpdateCharacterGuildId(GetGUID(), guildId); +} + +void Player::SetArenaTeamInfoField(uint8 slot, ArenaTeamInfoType type, uint32 value) +{ + SetUInt32Value(PLAYER_FIELD_ARENA_TEAM_INFO_1_1 + (slot * ARENA_TEAM_END) + type, value); + if (type == ARENA_TEAM_PERSONAL_RATING && value > _maxPersonalArenaRate) + { + _maxPersonalArenaRate = value; + UpdateConquestCurrencyCap(CURRENCY_TYPE_CONQUEST_META_ARENA); + } +} + +void Player::SetInArenaTeam(uint32 ArenaTeamId, uint8 slot, uint8 type) +{ + SetArenaTeamInfoField(slot, ARENA_TEAM_ID, ArenaTeamId); + SetArenaTeamInfoField(slot, ARENA_TEAM_TYPE, type); +} + +uint32 Player::GetZoneIdFromDB(ObjectGuid guid) +{ + ObjectGuid::LowType guidLow = guid.GetCounter(); + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_ZONE); + stmt->setUInt32(0, guidLow); + PreparedQueryResult result = CharacterDatabase.Query(stmt); + + if (!result) + return 0; + Field* fields = result->Fetch(); + uint32 zone = fields[0].GetUInt16(); + + if (!zone) + { + // stored zone is zero, use generic and slow zone detection + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_POSITION_XYZ); + stmt->setUInt32(0, guidLow); + result = CharacterDatabase.Query(stmt); + + if (!result) + return 0; + fields = result->Fetch(); + uint32 map = fields[0].GetUInt16(); + float posx = fields[1].GetFloat(); + float posy = fields[2].GetFloat(); + float posz = fields[3].GetFloat(); + + if (!sMapStore.LookupEntry(map)) + return 0; + + zone = sTerrainMgr.GetZoneId(PhasingHandler::GetEmptyPhaseShift(), map, posx, posy, posz); + + if (zone > 0) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_ZONE); + + stmt->setUInt16(0, uint16(zone)); + stmt->setUInt32(1, guidLow); + + CharacterDatabase.Execute(stmt); + } + } + + return zone; +} + +void Player::UpdateArea(uint32 newArea) +{ + // FFA_PVP flags are area and not zone id dependent + // so apply them accordingly + m_areaUpdateId = newArea; + + AreaTableEntry const* area = sAreaTableStore.LookupEntry(newArea); + bool oldFFAPvPArea = pvpInfo.IsInFFAPvPArea; + pvpInfo.IsInFFAPvPArea = area && (area->GetFlags().HasFlag(AreaFlags::FreeForAllPvP)); + UpdatePvPState(true); + + // check if we were in ffa arena and we left + if (oldFFAPvPArea && !pvpInfo.IsInFFAPvPArea) + ValidateAttackersAndOwnTarget(); + + PhasingHandler::OnAreaChange(this); + UpdateAreaDependentAuras(newArea); + + // previously this was in UpdateZone (but after UpdateArea) so nothing will break + pvpInfo.IsInNoPvPArea = false; + if (area && area->IsSanctuary()) // in sanctuary + { + SetByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_SANCTUARY); + pvpInfo.IsInNoPvPArea = true; + if (!duel) + CombatStopWithPets(); + } + else + RemoveByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_SANCTUARY); + + AreaFlags const areaRestFlag = (GetTeam() == ALLIANCE) ? AreaFlags::AllianceResting : AreaFlags::HordeResting; + if (area && area->GetFlags().HasFlag(areaRestFlag)) + SetRestFlag(REST_FLAG_IN_FACTION_AREA); + else + RemoveRestFlag(REST_FLAG_IN_FACTION_AREA); + + UpdateMountCapability(); +} + +void Player::UpdateZone(uint32 newZone, uint32 newArea) +{ + if (!IsInWorld()) + return; + + uint32 const oldZone = m_zoneUpdateId; + m_zoneUpdateId = newZone; + + GetMap()->UpdatePlayerZoneStats(oldZone, newZone); + + // call leave script hooks immedately (before updating flags) + if (oldZone != newZone) + { + sOutdoorPvPMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId); + sBattlefieldMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId); + } + + // group update + if (GetGroup()) + { + SetGroupUpdateFlag(GROUP_UPDATE_FULL); + if (GetPet()) + SetGroupUpdateFlag(GROUP_UPDATE_PET); + } + + // zone changed, so area changed as well, update it + UpdateArea(newArea); + + AreaTableEntry const* zone = sAreaTableStore.LookupEntry(newZone); + if (!zone) + return; + + if (sWorld->getBoolConfig(CONFIG_WEATHER)) + GetMap()->GetOrGenerateZoneDefaultWeather(newZone); + GetMap()->SendZoneDynamicInfo(newZone, this); + + UpdateHostileAreaState(zone); + + if (zone->GetFlags().HasFlag(AreaFlags::LinkedChat)) // Is in a capital city + { + if (!pvpInfo.IsHostile || zone->IsSanctuary()) + SetRestFlag(REST_FLAG_IN_CITY); + + pvpInfo.IsInNoPvPArea = true; + } + else + RemoveRestFlag(REST_FLAG_IN_CITY); // Recently left a capital city + + UpdatePvPState(); + + // remove items with area/map limitations (delete only for alive player to allow back in ghost mode) + // if player resurrected at teleport this will be applied in resurrect code + if (IsAlive()) + DestroyZoneLimitedItem(true, newZone); + + // check some item equip limitations (in result lost CanTitanGrip at talent reset, for example) + AutoUnequipOffhandIfNeed(); + + // recent client version not send leave/join channel packets for built-in local channels + UpdateLocalChannels(newZone); + + if (oldZone != newZone) + UpdateZoneDependentAuras(newZone); + + // call enter script hooks after everyting else has processed + sScriptMgr->OnPlayerUpdateZone(this, newZone, newArea); + if (oldZone != newZone) + { + sOutdoorPvPMgr->HandlePlayerEnterZone(this, newZone); + sBattlefieldMgr->HandlePlayerEnterZone(this, newZone); + SendInitWorldStates(newZone, newArea); // only if really enters to new zone, not just area change, works strange... + if (Guild* guild = GetGuild()) + guild->UpdateMemberData(this, GUILD_MEMBER_DATA_ZONEID, newZone); + } +} + +void Player::UpdateHostileAreaState(AreaTableEntry const* area) +{ + pvpInfo.IsInHostileArea = false; + + if (area->IsSanctuary()) // sanctuary and arena cannot be overriden + pvpInfo.IsInHostileArea = false; + else if (area->GetFlags().HasFlag(AreaFlags::FreeForAllPvP)) + pvpInfo.IsInHostileArea = true; + else + { + if (area) + { + if (InBattleground() || area->GetFlags().HasFlag(AreaFlags::CombatZone) || (area->PvpCombatWorldStateID != -1 && sWorldStateMgr->GetValue(area->PvpCombatWorldStateID, GetMap()) != 0)) + pvpInfo.IsInHostileArea = true; + else if (sWorld->IsPvPRealm() || (area->GetFlags().HasFlag(AreaFlags::EnemiesPvPFlagged))) + { + if (area->GetFlags().HasFlag(AreaFlags::Contested)) + pvpInfo.IsInHostileArea = sWorld->IsPvPRealm(); + else + { + FactionTemplateEntry const* factionTemplate = GetFactionTemplateEntry(); + if (!factionTemplate || factionTemplate->FriendGroup & area->FactionGroupMask) + pvpInfo.IsInHostileArea = false; + else if (factionTemplate->EnemyGroup & area->FactionGroupMask) + pvpInfo.IsInHostileArea = true; + else + pvpInfo.IsInHostileArea = sWorld->IsPvPRealm(); + } + } + } + } + + // Treat players having a quest flagging for PvP as always in hostile area + pvpInfo.IsHostile = pvpInfo.IsInHostileArea || HasPvPForcingQuest(); +} + +//If players are too far away from the duel flag... they lose the duel +void Player::CheckDuelDistance(time_t currTime) +{ + if (!duel) + return; + + ObjectGuid duelFlagGUID = GetGuidValue(PLAYER_DUEL_ARBITER); + GameObject* obj = GetMap()->GetGameObject(duelFlagGUID); + if (!obj) + return; + + if (duel->outOfBound == 0) + { + if (!IsWithinDistInMap(obj, 50)) + { + duel->outOfBound = currTime; + + WorldPacket data(SMSG_DUEL_OUTOFBOUNDS, 0); + SendDirectMessage(&data); + } + } + else + { + if (IsWithinDistInMap(obj, 40)) + { + duel->outOfBound = 0; + + WorldPacket data(SMSG_DUEL_INBOUNDS, 0); + SendDirectMessage(&data); + } + else if (currTime >= (duel->outOfBound+10)) + DuelComplete(DUEL_FLED); + } +} + +bool Player::IsOutdoorPvPActive() const +{ + return IsAlive() && !HasInvisibilityAura() && !HasStealthAura() && IsPvP() && !HasUnitMovementFlag(MOVEMENTFLAG_FLYING) && !IsInFlight(); +} + +void Player::DuelComplete(DuelCompleteType type) +{ + // duel not requested + if (!duel) + return; + + // Check if DuelComplete() has been called already up in the stack and in that case don't do anything else here + if (duel->isCompleted || ASSERT_NOTNULL(duel->opponent->duel)->isCompleted) + return; + + duel->isCompleted = true; + duel->opponent->duel->isCompleted = true; + + TC_LOG_DEBUG("entities.unit", "Player::DuelComplete: Player '%s' (%s), Opponent: '%s' (%s)", + GetName().c_str(), GetGUID().ToString().c_str(), duel->opponent->GetName().c_str(), duel->opponent->GetGUID().ToString().c_str()); + + WorldPacket data(SMSG_DUEL_COMPLETE, (1)); + data << (uint8)((type != DUEL_INTERRUPTED) ? 1 : 0); + SendDirectMessage(&data); + + if (duel->opponent->GetSession()) + duel->opponent->SendDirectMessage(&data); + + if (type != DUEL_INTERRUPTED) + { + data.Initialize(SMSG_DUEL_WINNER, (1+20)); // we guess size + data << uint8(type == DUEL_WON ? 0 : 1); // 0 = just won; 1 = fled + data << duel->opponent->GetName(); + data << GetName(); + SendMessageToSet(&data, true); + } + + sScriptMgr->OnPlayerDuelEnd(duel->opponent, this, type); + + switch (type) + { + case DUEL_FLED: + // if initiator and opponent are on the same team + // or initiator and opponent are not PvP enabled, forcibly stop attacking + if (duel->initiator->GetTeam() == duel->opponent->GetTeam()) + { + duel->initiator->AttackStop(); + duel->opponent->AttackStop(); + } + else + { + if (!duel->initiator->IsPvP()) + duel->initiator->AttackStop(); + if (!duel->opponent->IsPvP()) + duel->opponent->AttackStop(); + } + break; + case DUEL_WON: + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOSE_DUEL, 1); + duel->opponent->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_WIN_DUEL, 1); + + // Credit for quest Death's Challenge + if (getClass() == CLASS_DEATH_KNIGHT && duel->opponent->GetQuestStatus(12733) == QUEST_STATUS_INCOMPLETE) + duel->opponent->CastSpell(duel->opponent, 52994, true); + + // Honor points after duel (the winner) - ImpConfig + if (uint32 amount = sWorld->getIntConfig(CONFIG_HONOR_AFTER_DUEL)) + duel->opponent->RewardHonor(nullptr, 1, amount); + + break; + default: + break; + } + + // Victory emote spell + if (type != DUEL_INTERRUPTED) + duel->opponent->CastSpell(duel->opponent, 52852, true); + + //Remove Duel Flag object + GameObject* obj = GetMap()->GetGameObject(GetGuidValue(PLAYER_DUEL_ARBITER)); + if (obj) + duel->initiator->RemoveGameObject(obj, true); + + /* remove auras */ + AuraApplicationMap &itsAuras = duel->opponent->GetAppliedAuras(); + for (AuraApplicationMap::iterator i = itsAuras.begin(); i != itsAuras.end();) + { + Aura const* aura = i->second->GetBase(); + if (!i->second->IsPositive() && aura->GetCasterGUID() == GetGUID() && aura->GetApplyTime() >= duel->startTime) + duel->opponent->RemoveAura(i); + else + ++i; + } + + AuraApplicationMap &myAuras = GetAppliedAuras(); + for (AuraApplicationMap::iterator i = myAuras.begin(); i != myAuras.end();) + { + Aura const* aura = i->second->GetBase(); + if (!i->second->IsPositive() && aura->GetCasterGUID() == duel->opponent->GetGUID() && aura->GetApplyTime() >= duel->startTime) + RemoveAura(i); + else + ++i; + } + + // cleanup combo points + if (GetComboTarget() == duel->opponent->GetGUID()) + ClearComboPoints(); + else if (GetComboTarget() == duel->opponent->GetPetGUID()) + ClearComboPoints(); + + if (duel->opponent->GetComboTarget() == GetGUID()) + duel->opponent->ClearComboPoints(); + else if (duel->opponent->GetComboTarget() == GetPetGUID()) + duel->opponent->ClearComboPoints(); + + //cleanups + SetGuidValue(PLAYER_DUEL_ARBITER, ObjectGuid::Empty); + SetUInt32Value(PLAYER_DUEL_TEAM, 0); + duel->opponent->SetGuidValue(PLAYER_DUEL_ARBITER, ObjectGuid::Empty); + duel->opponent->SetUInt32Value(PLAYER_DUEL_TEAM, 0); + + delete duel->opponent->duel; + duel->opponent->duel = nullptr; + delete duel; + duel = nullptr; +} + +//---------------------------------------------------------// + +void Player::_ApplyItemMods(Item* item, uint8 slot, bool apply, bool updateItemAuras /*= true*/) +{ + if (slot >= INVENTORY_SLOT_BAG_END || !item) + return; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + return; + + // not apply/remove mods for broken item + if (item->IsBroken()) + return; + + TC_LOG_DEBUG("entities.player.items", "Player::_ApplyItemMods: Applying mods for item %s", item->GetGUID().ToString().c_str()); + + if (proto->GetSocketColor(0)) //only (un)equipping of items with sockets can influence metagems, so no need to waste time with normal items + CorrectMetaGemEnchants(slot, apply); + + _ApplyItemBonuses(proto, slot, apply); + ApplyItemEquipSpell(item, apply); + if (updateItemAuras) + { + ApplyItemDependentAuras(item, apply); + + WeaponAttackType const attackType = Player::GetAttackBySlot(slot); + if (attackType != MAX_ATTACK) + UpdateWeaponDependentAuras(attackType); + } + + ApplyEnchantment(item, apply); + + TC_LOG_DEBUG("entities.player.items", "Player::_ApplyItemMods: completed"); +} + +ScalingStatDistributionEntry const* Player::GetScalingStatDistributionFor(ItemTemplate const& itemTemplate) const +{ + if (!itemTemplate.GetScalingStatDistribution()) + return nullptr; + + return sScalingStatDistributionStore.LookupEntry(itemTemplate.GetScalingStatDistribution()); +} + +ScalingStatValuesEntry const* Player::GetScalingStatValuesFor(ItemTemplate const& itemTemplate) const +{ + if (!itemTemplate.GetStatScalingFactor()) + return nullptr; + + ScalingStatDistributionEntry const* ssd = GetScalingStatDistributionFor(itemTemplate); + if (!ssd) + return nullptr; + + // req. check at equip, but allow use for extended range if range limit max level, set proper level + uint32 const ssd_level = std::min(uint32(getLevel()), ssd->Maxlevel); + return sScalingStatValuesStore.LookupEntry(ssd_level); +} + +void Player::_ApplyItemBonuses(ItemTemplate const* proto, uint8 slot, bool apply, bool only_level_scale /*= false*/) +{ + if (slot >= INVENTORY_SLOT_BAG_END || !proto) + return; + + ScalingStatDistributionEntry const* ssd = GetScalingStatDistributionFor(*proto); + ScalingStatValuesEntry const* ssv = GetScalingStatValuesFor(*proto); + if (only_level_scale && (!ssd || !ssv)) + return; + + for (uint8 i = 0; i < MAX_ITEM_PROTO_STATS; ++i) + { + int32 statID = proto->GetItemStatType(i); + if ((statID < 0 && !ssd) || (statID < 0 && ssd && ssd->StatID[i] < 0)) + continue; + + if (statID < 0 && ssd) + statID = ssd->StatID[i]; + + int32 val = proto->GetStatValue(i, this); + if (!val) + continue; + + switch (statID) + { + case ITEM_MOD_MANA: + HandleStatFlatModifier(UNIT_MOD_MANA, BASE_VALUE, float(val), apply); + break; + case ITEM_MOD_HEALTH: // modify HP + HandleStatFlatModifier(UNIT_MOD_HEALTH, BASE_VALUE, float(val), apply); + break; + case ITEM_MOD_AGILITY: // modify agility + if (apply) + GetStats().AddBaseStatModifier(StatType::Agility, val); + else + GetStats().RemoveBaseStatModifier(StatType::Agility, val); + break; + case ITEM_MOD_STRENGTH: //modify strength + if (apply) + GetStats().AddBaseStatModifier(StatType::Strength, val); + else + GetStats().RemoveBaseStatModifier(StatType::Strength, val); + break; + case ITEM_MOD_INTELLECT: //modify intellect + if (apply) + GetStats().AddBaseStatModifier(StatType::Intellect, val); + else + GetStats().RemoveBaseStatModifier(StatType::Intellect, val); + break; + case ITEM_MOD_SPIRIT: //modify spirit + if (apply) + GetStats().AddBaseStatModifier(StatType::Spirit, val); + else + GetStats().RemoveBaseStatModifier(StatType::Spirit, val); + break; + case ITEM_MOD_STAMINA: //modify stamina + if (apply) + GetStats().AddBaseStatModifier(StatType::Stamina, val); + else + GetStats().RemoveBaseStatModifier(StatType::Stamina, val); + break; + case ITEM_MOD_DEFENSE_SKILL_RATING: + ApplyRatingMod(CR_DEFENSE_SKILL, int32(val), apply); + break; + case ITEM_MOD_DODGE_RATING: + ApplyRatingMod(CR_DODGE, int32(val), apply); + break; + case ITEM_MOD_PARRY_RATING: + ApplyRatingMod(CR_PARRY, int32(val), apply); + break; + case ITEM_MOD_BLOCK_RATING: + ApplyRatingMod(CR_BLOCK, int32(val), apply); + break; + case ITEM_MOD_HIT_MELEE_RATING: + ApplyRatingMod(CR_HIT_MELEE, int32(val), apply); + break; + case ITEM_MOD_HIT_RANGED_RATING: + ApplyRatingMod(CR_HIT_RANGED, int32(val), apply); + break; + case ITEM_MOD_HIT_SPELL_RATING: + ApplyRatingMod(CR_HIT_SPELL, int32(val), apply); + break; + case ITEM_MOD_CRIT_MELEE_RATING: + ApplyRatingMod(CR_CRIT_MELEE, int32(val), apply); + break; + case ITEM_MOD_CRIT_RANGED_RATING: + ApplyRatingMod(CR_CRIT_RANGED, int32(val), apply); + break; + case ITEM_MOD_CRIT_SPELL_RATING: + ApplyRatingMod(CR_CRIT_SPELL, int32(val), apply); + break; + // case ITEM_MOD_HIT_TAKEN_MELEE_RATING: + // ApplyRatingMod(CR_HIT_TAKEN_MELEE, int32(val), apply); + // break; + // case ITEM_MOD_HIT_TAKEN_RANGED_RATING: + // ApplyRatingMod(CR_HIT_TAKEN_RANGED, int32(val), apply); + // break; + // case ITEM_MOD_HIT_TAKEN_SPELL_RATING: + // ApplyRatingMod(CR_HIT_TAKEN_SPELL, int32(val), apply); + // break; + // case ITEM_MOD_CRIT_TAKEN_MELEE_RATING: + // ApplyRatingMod(CR_CRIT_TAKEN_MELEE, int32(val), apply); + // break; + case ITEM_MOD_CRIT_TAKEN_RANGED_RATING: + ApplyRatingMod(CR_RESILIENCE_PLAYER_DAMAGE_TAKEN, int32(val), apply); + break; + // case ITEM_MOD_CRIT_TAKEN_SPELL_RATING: + // ApplyRatingMod(CR_CRIT_TAKEN_SPELL, int32(val), apply); + // break; + case ITEM_MOD_HASTE_MELEE_RATING: + ApplyRatingMod(CR_HASTE_MELEE, int32(val), apply); + break; + case ITEM_MOD_HASTE_RANGED_RATING: + ApplyRatingMod(CR_HASTE_RANGED, int32(val), apply); + break; + case ITEM_MOD_HASTE_SPELL_RATING: + ApplyRatingMod(CR_HASTE_SPELL, int32(val), apply); + break; + case ITEM_MOD_HIT_RATING: + ApplyRatingMod(CR_HIT_MELEE, int32(val), apply); + ApplyRatingMod(CR_HIT_RANGED, int32(val), apply); + ApplyRatingMod(CR_HIT_SPELL, int32(val), apply); + break; + case ITEM_MOD_CRIT_RATING: + ApplyRatingMod(CR_CRIT_MELEE, int32(val), apply); + ApplyRatingMod(CR_CRIT_RANGED, int32(val), apply); + ApplyRatingMod(CR_CRIT_SPELL, int32(val), apply); + break; + // case ITEM_MOD_HIT_TAKEN_RATING: // Unused since 3.3.5 + // ApplyRatingMod(CR_HIT_TAKEN_MELEE, int32(val), apply); + // ApplyRatingMod(CR_HIT_TAKEN_RANGED, int32(val), apply); + // ApplyRatingMod(CR_HIT_TAKEN_SPELL, int32(val), apply); + // break; + // case ITEM_MOD_CRIT_TAKEN_RATING: // Unused since 3.3.5 + // ApplyRatingMod(CR_CRIT_TAKEN_MELEE, int32(val), apply); + // ApplyRatingMod(CR_CRIT_TAKEN_RANGED, int32(val), apply); + // ApplyRatingMod(CR_CRIT_TAKEN_SPELL, int32(val), apply); + // break; + case ITEM_MOD_RESILIENCE_RATING: + ApplyRatingMod(CR_RESILIENCE_PLAYER_DAMAGE_TAKEN, int32(val), apply); + break; + case ITEM_MOD_HASTE_RATING: + ApplyRatingMod(CR_HASTE_MELEE, int32(val), apply); + ApplyRatingMod(CR_HASTE_RANGED, int32(val), apply); + ApplyRatingMod(CR_HASTE_SPELL, int32(val), apply); + break; + case ITEM_MOD_EXPERTISE_RATING: + ApplyRatingMod(CR_EXPERTISE, int32(val), apply); + break; + case ITEM_MOD_ATTACK_POWER: + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER, TOTAL_VALUE, float(val), apply); + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER_RANGED, TOTAL_VALUE, float(val), apply); + break; + case ITEM_MOD_RANGED_ATTACK_POWER: + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER_RANGED, TOTAL_VALUE, float(val), apply); + break; + case ITEM_MOD_MANA_REGENERATION: + ApplyManaRegenBonus(int32(val), apply); + break; + //case ITEM_MOD_ARMOR_PENETRATION_RATING: + // ApplyRatingMod(CR_ARMOR_PENETRATION, int32(val), apply); + // break; + case ITEM_MOD_SPELL_POWER: + ApplySpellPowerBonus(int32(val), apply); + break; + case ITEM_MOD_HEALTH_REGEN: + ApplyHealthRegenBonus(int32(val), apply); + break; + case ITEM_MOD_SPELL_PENETRATION: + ApplySpellPenetrationBonus(val, apply); + break; + case ITEM_MOD_MASTERY_RATING: + ApplyRatingMod(CR_MASTERY, int32(val), apply); + break; + case ITEM_MOD_FIRE_RESISTANCE: + HandleStatFlatModifier(UNIT_MOD_RESISTANCE_FIRE, BASE_VALUE, float(val), apply); + break; + case ITEM_MOD_FROST_RESISTANCE: + HandleStatFlatModifier(UNIT_MOD_RESISTANCE_FROST, BASE_VALUE, float(val), apply); + break; + case ITEM_MOD_HOLY_RESISTANCE: + HandleStatFlatModifier(UNIT_MOD_RESISTANCE_HOLY, BASE_VALUE, float(val), apply); + break; + case ITEM_MOD_SHADOW_RESISTANCE: + HandleStatFlatModifier(UNIT_MOD_RESISTANCE_SHADOW, BASE_VALUE, float(val), apply); + break; + case ITEM_MOD_NATURE_RESISTANCE: + HandleStatFlatModifier(UNIT_MOD_RESISTANCE_NATURE, BASE_VALUE, float(val), apply); + break; + case ITEM_MOD_ARCANE_RESISTANCE: + HandleStatFlatModifier(UNIT_MOD_RESISTANCE_ARCANE, BASE_VALUE, float(val), apply); + break; + case ITEM_MOD_EXTRA_ARMOR: + HandleStatFlatModifier(UNIT_MOD_ARMOR, BASE_VALUE, float(val), apply); + break; + } + } + + // Apply Spell Power from ScalingStatValue if set + if (ssv && proto->GetFlags2() & ITEM_FLAG2_CASTER_WEAPON) + if (int32 spellbonus = int32(ssv->Spellpower)) + ApplySpellPowerBonus(spellbonus, apply); + + // If set ScalingStatValue armor get it or use item armor + uint32 armor = proto->GetEffectiveArmor(this); + if (ssv && proto->GetClass() == ITEM_CLASS_ARMOR) + armor = ssv->GetArmor(proto->GetInventoryType(), proto->GetSubClass() - 1); + + if (armor) + { + UnitModifierFlatType modType = TOTAL_VALUE; + if (proto->GetClass() == ITEM_CLASS_ARMOR) + { + switch (proto->GetSubClass()) + { + case ITEM_SUBCLASS_ARMOR_CLOTH: + case ITEM_SUBCLASS_ARMOR_LEATHER: + case ITEM_SUBCLASS_ARMOR_MAIL: + case ITEM_SUBCLASS_ARMOR_PLATE: + case ITEM_SUBCLASS_ARMOR_SHIELD: + modType = BASE_VALUE; + break; + } + } + HandleStatFlatModifier(UNIT_MOD_ARMOR, modType, float(armor), apply); + } + + // Add armor bonus from ArmorDamageModifier if > 0 + if (proto->GetArmorDamageModifier() > 0) + HandleStatFlatModifier(UNIT_MOD_ARMOR, TOTAL_VALUE, float(proto->GetArmorDamageModifier()), apply); + + WeaponAttackType attType = Player::GetAttackBySlot(slot); + if (attType != MAX_ATTACK) + _ApplyWeaponDamage(slot, proto, apply); +} + +void Player::_ApplyWeaponDamage(uint8 slot, ItemTemplate const* proto, bool apply) +{ + WeaponAttackType attType = Player::GetAttackBySlot(slot); + if (!IsInFeralForm() && apply && !CanUseAttackType(attType)) + return; + + ScalingStatValuesEntry const* ssv = GetScalingStatValuesFor(*proto); + float minDamage, maxDamage, dps; + proto->GetWeaponDamage(this, minDamage, maxDamage, dps); + + // If set dpsMod in ScalingStatValue use it for min (70% from average), max (130% from average) damage + int32 extraDPS = 0; + if (ssv) + { + float damageMultiplier = 0.0f; + extraDPS = ssv->GetDPSAndDamageMultiplier(proto->GetSubClass(), (proto->GetFlags2() & ITEM_FLAG2_CASTER_WEAPON) != 0, &damageMultiplier); + if (extraDPS) + { + float average = extraDPS * proto->GetDelay() / 1000.0f; + minDamage = (1.0f - damageMultiplier) * average; + maxDamage = (1.0f + damageMultiplier) * average; + } + } + + if (apply) + { + if (minDamage > 0.f) + SetBaseWeaponDamage(attType, MINDAMAGE, minDamage); + + if (maxDamage > 0.f) + SetBaseWeaponDamage(attType, MAXDAMAGE, maxDamage); + } + + if (!apply) + { + if (attType == BASE_ATTACK) + { + SetBaseWeaponDamage(BASE_ATTACK, MINDAMAGE, BASE_MINDAMAGE); + SetBaseWeaponDamage(BASE_ATTACK, MAXDAMAGE, BASE_MAXDAMAGE); + } + else + { + SetBaseWeaponDamage(attType, MINDAMAGE, 0.f); + SetBaseWeaponDamage(attType, MAXDAMAGE, 0.f); + } + } + + if (proto->GetDelay() && !IsInFeralForm()) + SetBaseAttackTime(attType, apply ? proto->GetDelay() : BASE_ATTACK_TIME); + + // No need to modify any physical damage for ferals as it is calculated from stats only + if (IsInFeralForm()) + return; + + if (CanModifyStats() && (GetWeaponDamageRange(attType, MAXDAMAGE) || proto->GetDelay())) + UpdateDamagePhysical(attType); +} + +SpellSchoolMask Player::GetMeleeDamageSchoolMask(WeaponAttackType attackType /*= BASE_ATTACK*/) const +{ + if (Item const* weapon = GetWeaponForAttack(attackType, true)) + return SpellSchoolMask(1 << weapon->GetTemplate()->GetDamageType()); + + return SPELL_SCHOOL_MASK_NORMAL; +} + +void Player::CastAllObtainSpells() +{ + for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + ApplyItemObtainSpells(item, true); + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + { + Bag* bag = GetBagByPos(i); + if (!bag) + continue; + + for (uint32 slot = 0; slot < bag->GetBagSize(); ++slot) + if (Item* item = bag->GetItemByPos(slot)) + ApplyItemObtainSpells(item, true); + } +} + +void Player::ApplyItemObtainSpells(Item* item, bool apply) +{ + ItemTemplate const* itemTemplate = item->GetTemplate(); + + for (ItemEffect const& effect : itemTemplate->Effects) + { + if (effect.Trigger != ITEM_SPELLTRIGGER_ON_NO_DELAY_USE) // On obtain trigger + continue; + + int32 spellId = effect.SpellID; + if (!spellId) + continue; + + if (apply) + { + if (!HasAura(spellId)) + CastSpell(this, spellId, item); + } + else + RemoveAurasDueToSpell(spellId); + } +} + +// this one rechecks weapon auras and stores them in BaseModGroup container +// needed for things like axe specialization applying only to axe weapons in case of dual-wield +void Player::UpdateWeaponDependentCritAuras(WeaponAttackType attackType) +{ + BaseModGroup modGroup; + switch (attackType) + { + case BASE_ATTACK: + modGroup = CRIT_PERCENTAGE; + break; + case OFF_ATTACK: + modGroup = OFFHAND_CRIT_PERCENTAGE; + break; + case RANGED_ATTACK: + modGroup = RANGED_CRIT_PERCENTAGE; + break; + default: + ABORT(); + break; + } + + float amount = 0.0f; + amount += GetTotalAuraModifier(SPELL_AURA_MOD_WEAPON_CRIT_PERCENT, std::bind(&Unit::CheckAttackFitToAuraRequirement, this, attackType, std::placeholders::_1)); + + // these auras don't have item requirement (only Combat Expertise in 3.3.5a) + amount += GetTotalAuraModifier(SPELL_AURA_MOD_CRIT_PCT); + + SetBaseModFlatValue(modGroup, amount); +} + +void Player::UpdateAllWeaponDependentCritAuras() +{ + for (uint8 i = BASE_ATTACK; i < MAX_ATTACK; ++i) + UpdateWeaponDependentCritAuras(WeaponAttackType(i)); +} + +void Player::UpdateWeaponDependentAuras(WeaponAttackType attackType) +{ + UpdateWeaponDependentCritAuras(attackType); + UpdateDamageDoneMods(attackType); + UpdateDamagePctDoneMods(attackType); +} + +void Player::ApplyItemDependentAuras(Item* item, bool apply) +{ + if (apply) + { + PlayerSpellMap const& spells = GetSpellMap(); + for (auto itr = spells.begin(); itr != spells.end(); ++itr) + { + if (itr->second.state == PLAYERSPELL_REMOVED || itr->second.disabled) + continue; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr->first); + if (!spellInfo || !spellInfo->IsPassive() || spellInfo->EquippedItemClass < 0) + continue; + + if (!HasAura(itr->first) && HasItemFitToSpellRequirements(spellInfo)) + AddAura(itr->first, this); // no SMSG_SPELL_GO in sniff found + } + } + else + RemoveItemDependentAurasAndCasts(item); +} + +bool Player::CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEffect const* aurEff) const +{ + SpellInfo const* spellInfo = aurEff->GetSpellInfo(); + if (spellInfo->EquippedItemClass == -1) + return true; + + Item* item = GetWeaponForAttack(attackType, true); + if (!item || !item->IsFitToSpellRequirements(spellInfo)) + return false; + + return true; +} + +void Player::ApplyItemEquipSpell(Item* item, bool apply, bool form_change) +{ + if (!item) + return; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + return; + + for (ItemEffect const& effect : proto->Effects) + { + // no spell + if (effect.SpellID <= 0) + continue; + + // wrong triggering type + if (apply && effect.Trigger != ITEM_SPELLTRIGGER_ON_EQUIP) + continue; + + // check if it is valid spell + SpellInfo const* spellproto = sSpellMgr->GetSpellInfo(effect.SpellID); + if (!spellproto) + continue; + + ApplyEquipSpell(spellproto, item, apply, form_change); + } +} + +void Player::ApplyEquipSpell(SpellInfo const* spellInfo, Item* item, bool apply, bool form_change) +{ + if (apply) + { + // Cannot be used in this stance/form + if (spellInfo->CheckShapeshift(GetShapeshiftForm()) != SPELL_CAST_OK) + return; + + if (form_change) // check aura active state from other form + { + AuraApplicationMapBounds range = GetAppliedAuras().equal_range(spellInfo->Id); + for (AuraApplicationMap::const_iterator itr = range.first; itr != range.second; ++itr) + if (!item || itr->second->GetBase()->GetCastItemGUID() == item->GetGUID()) + return; + } + + TC_LOG_DEBUG("entities.player", "Player::ApplyEquipSpell: Player '%s' (%s) cast %s equip spell (ID: %i)", + GetName().c_str(), GetGUID().ToString().c_str(), (item ? "item" : "itemset"), spellInfo->Id); + + CastSpell(this, spellInfo->Id, item); + } + else + { + if (form_change) // check aura compatibility + { + // Cannot be used in this stance/form + if (spellInfo->CheckShapeshift(GetShapeshiftForm()) == SPELL_CAST_OK) + return; // and remove only not compatible at form change + } + + if (item) + RemoveAurasDueToItemSpell(spellInfo->Id, item->GetGUID()); // un-apply all spells, not only at-equipped + else + RemoveAurasDueToSpell(spellInfo->Id); // un-apply spell (item set case) + } +} + +void Player::UpdateEquipSpellsAtFormChange() +{ + for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (m_items[i] && !m_items[i]->IsBroken() && CanUseAttackType(GetAttackBySlot(i))) + { + ApplyItemEquipSpell(m_items[i], false, true); // remove spells that not fit to form + ApplyItemEquipSpell(m_items[i], true, true); // add spells that fit form but not active + } + } + + // item set bonuses not dependent from item broken state + for (std::unique_ptr const& itemEffect : ItemSetEff) + { + if (!itemEffect) + continue; + + for (uint8 i = 0; i < MAX_ITEM_SET_SPELLS; ++i) + { + if (SpellInfo const* spellInfo = itemEffect->spells[i]) + { + ApplyEquipSpell(spellInfo, nullptr, false, true); // remove spells that not fit to form + ApplyEquipSpell(spellInfo, nullptr, true, true); // add spells that fit form but not active + } + } + } +} + +void Player::CastItemCombatSpell(DamageInfo const& damageInfo) +{ + Unit* target = damageInfo.GetVictim(); + if (!target || !target->IsAlive() || target == this) + return; + + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) + { + // If usable, try to cast item spell + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + { + if (!item->IsBroken() && CanUseAttackType(damageInfo.GetAttackType())) + { + if (ItemTemplate const* proto = item->GetTemplate()) + { + // Additional check for weapons + if (proto->GetClass() == ITEM_CLASS_WEAPON) + { + // offhand item cannot proc from main hand hit etc + EquipmentSlots slot; + switch (damageInfo.GetAttackType()) + { + case BASE_ATTACK: + slot = EQUIPMENT_SLOT_MAINHAND; + break; + case OFF_ATTACK: + slot = EQUIPMENT_SLOT_OFFHAND; + break; + case RANGED_ATTACK: + slot = EQUIPMENT_SLOT_RANGED; + break; + default: + slot = EQUIPMENT_SLOT_END; + break; + } + if (slot != i) + continue; + // Check if item is useable (forms or disarm) + if (damageInfo.GetAttackType() == BASE_ATTACK) + if (!IsUseEquipedWeapon(true) && !IsInFeralForm()) + continue; + } + + CastItemCombatSpell(damageInfo, item, proto); + } + } + } + } +} + +void Player::CastItemCombatSpell(DamageInfo const& damageInfo, Item* item, ItemTemplate const* proto) +{ + // Can do effect if any damage done to target + // for done procs allow normal + critical + absorbs by default + bool canTrigger = [&]() + { + if (!(damageInfo.GetHitMask() & (PROC_HIT_NORMAL | PROC_HIT_CRITICAL | PROC_HIT_ABSORB))) + return false; + + if (damageInfo.GetSpellInfo() && damageInfo.GetSpellInfo()->HasAttribute(SPELL_ATTR3_SUPPRESS_CASTER_PROCS)) + return false; + + return true; + }(); + + if (canTrigger) + { + for (ItemEffect const& effect : proto->Effects) + { + // no spell + if (effect.SpellID <= 0) + continue; + + // wrong triggering type + if (effect.Trigger != ITEM_SPELLTRIGGER_CHANCE_ON_HIT) + continue; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect.SpellID); + if (!spellInfo) + { + TC_LOG_ERROR("entities.player.items", "Player::CastItemCombatSpell: Player '%s' (%s) cast unknown item spell (ID: %i)", + GetName().c_str(), GetGUID().ToString().c_str(), effect.SpellID); + continue; + } + + // not allow proc extra attack spell at extra attack + if (m_extraAttacks && spellInfo->HasEffect(SPELL_EFFECT_ADD_EXTRA_ATTACKS)) + return; + + float chance = static_cast(spellInfo->ProcChance); + + if (proto->SpellPPMRate) + { + uint32 WeaponSpeed = GetBaseAttackTime(damageInfo.GetAttackType()); + chance = GetPPMProcChance(WeaponSpeed, proto->SpellPPMRate, spellInfo); + } + else if (chance > 100.0f) + chance = GetWeaponProcChance(); + + if (roll_chance_f(chance) && sScriptMgr->OnCastItemCombatSpell(this, damageInfo.GetVictim(), spellInfo, item)) + CastSpell(damageInfo.GetVictim(), spellInfo->Id, item); + } + } + + // item combat enchantments + for (uint8 e_slot = 0; e_slot < MAX_ENCHANTMENT_SLOT; ++e_slot) + { + if (e_slot > PRISMATIC_ENCHANTMENT_SLOT && e_slot < PROP_ENCHANTMENT_SLOT_0) // not holding enchantment id + continue; + + uint32 enchant_id = item->GetEnchantmentId(EnchantmentSlot(e_slot)); + SpellItemEnchantmentEntry const* pEnchant = sSpellItemEnchantmentStore.LookupEntry(enchant_id); + if (!pEnchant) + continue; + + for (uint8 s = 0; s < MAX_ITEM_ENCHANTMENT_EFFECTS; ++s) + { + if (pEnchant->Effect[s] != ITEM_ENCHANTMENT_TYPE_COMBAT_SPELL) + continue; + + SpellEnchantProcEntry const* entry = sSpellMgr->GetSpellEnchantProcEvent(enchant_id); + if (entry && entry->HitMask) + { + // Check hit/crit/dodge/parry requirement + if ((entry->HitMask & damageInfo.GetHitMask()) == 0) + continue; + } + else + { + // Can do effect if any damage done to target + // for done procs allow normal + critical + absorbs by default + if (!canTrigger) + continue; + } + + // check if enchant procs only on white hits + if (entry && (entry->AttributesMask & ENCHANT_PROC_ATTR_WHITE_HIT) && damageInfo.GetSpellInfo()) + continue; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(pEnchant->EffectArg[s]); + if (!spellInfo) + { + TC_LOG_ERROR("entities.player.items", "Player::CastItemCombatSpell: Player '%s' (%s) cast unknown spell (EnchantID: %u, SpellID: %i), ignoring", + GetName().c_str(), GetGUID().ToString().c_str(), pEnchant->ID, pEnchant->EffectArg[s]); + continue; + } + + float chance = pEnchant->EffectPointsMin[s] != 0 ? float(pEnchant->EffectPointsMin[s]) : GetWeaponProcChance(); + if (entry) + { + if (entry->ProcsPerMinute != 0.f) + chance = GetPPMProcChance(proto->GetDelay(), entry->ProcsPerMinute, spellInfo); + else if (entry->Chance != 0.f) + chance = entry->Chance; + } + + // Apply spell mods + ApplySpellMod(spellInfo, SpellModOp::ProcChance, chance); + + // Shiv has 100% chance to apply the poison + if (FindCurrentSpellBySpellId(5938) && e_slot == TEMP_ENCHANTMENT_SLOT) + chance = 100.0f; + + if (roll_chance_f(chance)) + { + Unit* target = spellInfo->IsPositive() ? this : damageInfo.GetVictim(); + + // reduce effect values if enchant is limited + CastSpellExtraArgs args(item); + if (entry && (entry->AttributesMask & ENCHANT_PROC_ATTR_LIMIT_60) && target->getLevel() > 60) + { + int32 const lvlDifference = target->getLevel() - 60; + int32 const lvlPenaltyFactor = 4; // 4% lost effectiveness per level + + int32 const effectPct = std::max(0, 100 - (lvlDifference * lvlPenaltyFactor)); + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (spellInfo->Effects[i].IsEffect()) + args.AddSpellMod(static_cast(SPELLVALUE_BASE_POINT0 + i), CalculatePct(spellInfo->Effects[i].CalcValue(this), effectPct)); + } + } + + CastSpell(target, spellInfo->Id, args); + } + } + } +} + +void Player::CastItemUseSpell(Item* item, SpellCastTargets const& targets, uint8 castId, uint32 glyphIndex) +{ + ItemTemplate const* proto = item->GetTemplate(); + // special learning case + if (proto->Effects[0].SpellID == 483 || proto->Effects[0].SpellID == 55884) + { + uint32 learn_spell_id = proto->Effects[0].SpellID; + uint32 learning_spell_id = proto->Effects[1].SpellID; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(learn_spell_id); + if (!spellInfo) + { + TC_LOG_ERROR("entities.player", "Player::CastItemUseSpell: Item (Entry: %u) has wrong spell id %u, ignoring.", proto->GetId(), learn_spell_id); + SendEquipError(EQUIP_ERR_INTERNAL_BAG_ERROR, item, nullptr); + return; + } + + Spell* spell = new Spell(this, spellInfo, TRIGGERED_NONE); + spell->m_CastItem = item; + spell->m_cast_count = castId; + spell->SetSpellValue({ SPELLVALUE_BASE_POINT0, static_cast(learning_spell_id) }); + spell->prepare(targets); + return; + } + + for (ItemEffect const& effect : proto->Effects) + { + // no spell + if (effect.SpellID <= 0) + continue; + + // wrong triggering type + if (effect.Trigger != ITEM_SPELLTRIGGER_ON_USE) + continue; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect.SpellID); + if (!spellInfo) + { + TC_LOG_ERROR("entities.player", "Player::CastItemUseSpell: Item (Entry: %u) has wrong spell id %u, ignoring", proto->GetId(), effect.SpellID); + continue; + } + + Spell* spell = new Spell(this, spellInfo, TRIGGERED_NONE); + spell->m_CastItem = item; + spell->m_cast_count = castId; + spell->m_glyphIndex = glyphIndex; + spell->prepare(targets); + return; + } + + // Item enchantments spells cast at use + for (uint8 e_slot = 0; e_slot < MAX_ENCHANTMENT_SLOT; ++e_slot) + { + if (e_slot > PRISMATIC_ENCHANTMENT_SLOT && e_slot < PROP_ENCHANTMENT_SLOT_0) // not holding enchantment id + continue; + + uint32 enchant_id = item->GetEnchantmentId(EnchantmentSlot(e_slot)); + SpellItemEnchantmentEntry const* pEnchant = sSpellItemEnchantmentStore.LookupEntry(enchant_id); + if (!pEnchant) + continue; + for (uint8 s = 0; s < MAX_ITEM_ENCHANTMENT_EFFECTS; ++s) + { + if (pEnchant->Effect[s] != ITEM_ENCHANTMENT_TYPE_USE_SPELL) + continue; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(pEnchant->EffectArg[s]); + if (!spellInfo) + { + TC_LOG_ERROR("entities.player", "Player::CastItemUseSpell: Enchant %i, cast unknown spell %i", pEnchant->ID, pEnchant->EffectArg[s]); + continue; + } + + Spell* spell = new Spell(this, spellInfo, TRIGGERED_NONE); + spell->m_CastItem = item; + spell->m_cast_count = castId; // set count of casts + spell->m_glyphIndex = glyphIndex; // glyph index + spell->prepare(targets); + return; + } + } +} + +void Player::_RemoveAllItemMods() +{ + TC_LOG_DEBUG("entities.player.items", "_RemoveAllItemMods start."); + + for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (m_items[i]) + { + ItemTemplate const* proto = m_items[i]->GetTemplate(); + if (!proto) + continue; + + // item set bonuses not dependent from item broken state + if (proto->GetItemSet()) + RemoveItemsSetItem(this, proto); + + if (m_items[i]->IsBroken() || !CanUseAttackType(GetAttackBySlot(i))) + continue; + + ApplyItemEquipSpell(m_items[i], false); + ApplyEnchantment(m_items[i], false); + } + } + + for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (m_items[i]) + { + if (m_items[i]->IsBroken() || !CanUseAttackType(GetAttackBySlot(i))) + continue; + ItemTemplate const* proto = m_items[i]->GetTemplate(); + if (!proto) + continue; + + ApplyItemDependentAuras(m_items[i], false); + _ApplyItemBonuses(proto, i, false); + } + } + + TC_LOG_DEBUG("entities.player.items", "_RemoveAllItemMods complete."); +} + +void Player::_ApplyAllItemMods() +{ + TC_LOG_DEBUG("entities.player.items", "_ApplyAllItemMods start."); + + for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (m_items[i]) + { + if (m_items[i]->IsBroken() || !CanUseAttackType(GetAttackBySlot(i))) + continue; + + ItemTemplate const* proto = m_items[i]->GetTemplate(); + if (!proto) + continue; + + ApplyItemDependentAuras(m_items[i], true); + _ApplyItemBonuses(proto, i, true); + + WeaponAttackType const attackType = Player::GetAttackBySlot(i); + if (attackType != MAX_ATTACK) + UpdateWeaponDependentAuras(attackType); + + } + } + + for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (m_items[i]) + { + ItemTemplate const* proto = m_items[i]->GetTemplate(); + if (!proto) + continue; + + // item set bonuses not dependent from item broken state + if (proto->GetItemSet()) + AddItemsSetItem(this, m_items[i]); + + if (m_items[i]->IsBroken() || !CanUseAttackType(GetAttackBySlot(i))) + continue; + + ApplyItemEquipSpell(m_items[i], true); + ApplyEnchantment(m_items[i], true); + } + } + + TC_LOG_DEBUG("entities.player.items", "_ApplyAllItemMods complete."); +} + +void Player::_ApplyAllLevelScaleItemMods(bool apply) +{ + for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (m_items[i]) + { + if (m_items[i]->IsBroken() || !CanUseAttackType(GetAttackBySlot(i))) + continue; + + ItemTemplate const* proto = m_items[i]->GetTemplate(); + if (!proto) + continue; + + _ApplyItemBonuses(proto, i, apply, true); + } + } +} + +/* If in a battleground a player dies, and an enemy removes the insignia, the player's bones is lootable + Called by remove insignia spell effect */ +void Player::RemovedInsignia(Player* looterPlr) +{ + // If player is not in battleground and not in worldpvpzone + if (!GetBattlegroundId() && !IsInWorldPvpZone()) + return; + + // If not released spirit, do it ! + if (m_deathTimer > 0) + { + m_deathTimer = 0; + BuildPlayerRepop(); + RepopAtGraveyard(); + } + + _corpseLocation.WorldRelocate(); + + // We have to convert player corpse to bones, not to be able to resurrect there + // SpawnCorpseBones isn't handy, 'cos it saves player while he in BG + Corpse* bones = GetMap()->ConvertCorpseToBones(GetGUID(), true); + if (!bones) + return; + + // Now we must make bones lootable, and send player loot + bones->SetFlag(CORPSE_FIELD_DYNAMIC_FLAGS, CORPSE_DYNFLAG_LOOTABLE); + + // We store the level of our player in the gold field + // We retrieve this information at Player::SendLoot() + bones->loot.gold = getLevel(); + bones->lootRecipient = looterPlr; + looterPlr->SendLoot(bones->GetGUID(), LOOT_INSIGNIA); +} + +void Player::SendLootRelease(ObjectGuid guid) const +{ + WorldPackets::Loot::LootReleaseResponse packet; + packet.LootObj = guid; + SendDirectMessage(packet.Write()); +} + +void Player::SendLoot(ObjectGuid guid, LootType loot_type) +{ + if (ObjectGuid lguid = GetLootGUID()) + m_session->DoLootRelease(lguid); + + Loot* loot; + PermissionTypes permission = ALL_PERMISSION; + + TC_LOG_DEBUG("loot", "Player::SendLoot: Player: '%s' (%s), Loot: %s", + GetName().c_str(), GetGUID().ToString().c_str(), guid.ToString().c_str()); + if (guid.IsGameObject()) + { + GameObject* go = GetMap()->GetGameObject(guid); + auto shouldLootRelease = [this](GameObject* go, LootType lootType) -> bool + { + // not check distance for GO in case owned GO (fishing bobber case, for example) + // And permit out of range GO with no owner in case fishing hole + if (!go) + return true; + + if (lootType == LOOT_SKINNING) + { + // Disarm Trap + if (!go->IsWithinDistInMap(this, 20.f)) + return true; + } + else + { + if (lootType != LOOT_FISHINGHOLE && ((lootType != LOOT_FISHING && lootType != LOOT_FISHING_JUNK) || go->GetOwnerGUID() != GetGUID()) && !go->IsWithinDistInMap(this)) + return true; + + if (lootType == LOOT_CORPSE && go->GetRespawnTime() && go->isSpawnedByDefault()) + return true; + } + + return false; + }; + + if (shouldLootRelease(go, loot_type)) + { + SendLootRelease(guid); + return; + } + + loot = &go->loot; + + // loot was generated and respawntime has passed since then, allow to recreate loot + // to avoid bugs, this rule covers spawned gameobjects only + // Don't allow to regenerate chest loot inside instances and raids, to avoid exploits with duplicate boss loot being given for some encounters + if (go->isSpawnedByDefault() && go->getLootState() == GO_ACTIVATED && !go->loot.isLooted() && !go->GetMap()->Instanceable() && go->GetLootGenerationTime() + go->GetRespawnDelay() < GameTime::GetGameTime()) + go->SetLootState(GO_READY); + + if (go->getLootState() == GO_READY) + { + uint32 lootid = go->GetGOInfo()->GetLootId(); + if (Battleground* bg = GetBattleground()) + if (!bg->CanActivateGO(go->GetEntry(), GetTeam())) + { + SendLootRelease(guid); + return; + } + + if (lootid) + { + loot->clear(); + + Group* group = GetGroup(); + bool groupRules = (group && go->GetGOInfo()->type == GAMEOBJECT_TYPE_CHEST && go->GetGOInfo()->chest.usegrouplootrules); + + // check current RR player and get next if necessary + if (groupRules) + group->UpdateLooterGuid(go, true); + + loot->FillLoot(lootid, LootTemplates_Gameobject, this, !groupRules, false, go->GetLootMode()); + go->SetLootGenerationTime(); + + // get next RR player (for next loot) + if (groupRules && !go->loot.empty()) + group->UpdateLooterGuid(go); + } + + if (go->GetLootMode() > 0) + if (GameObjectTemplateAddon const* addon = go->GetTemplateAddon()) + loot->generateMoneyLoot(addon->mingold, addon->maxgold); + + if (loot_type == LOOT_FISHING) + go->getFishLoot(loot, this); + else if (loot_type == LOOT_FISHING_JUNK) + go->getFishLootJunk(loot, this); + + if (go->GetGOInfo()->type == GAMEOBJECT_TYPE_CHEST && go->GetGOInfo()->chest.usegrouplootrules) + { + if (Group* group = GetGroup()) + { + switch (group->GetLootMethod()) + { + case GROUP_LOOT: + // GroupLoot: rolls items over threshold. Items with quality < threshold, round robin + group->GroupLoot(loot, go); + break; + case NEED_BEFORE_GREED: + group->NeedBeforeGreed(loot, go); + break; + case MASTER_LOOT: + group->MasterLoot(loot, go); + break; + default: + break; + } + } + } + + go->SetLootState(GO_ACTIVATED, this); + } + + if (go->getLootState() == GO_ACTIVATED) + { + if (Group* group = GetGroup()) + { + switch (group->GetLootMethod()) + { + case MASTER_LOOT: + permission = group->GetMasterLooterGuid() == GetGUID() ? MASTER_PERMISSION : RESTRICTED_PERMISSION; + break; + case FREE_FOR_ALL: + permission = ALL_PERMISSION; + break; + case ROUND_ROBIN: + permission = ROUND_ROBIN_PERMISSION; + break; + default: + permission = GROUP_PERMISSION; + break; + } + } + else + permission = ALL_PERMISSION; + } + } + else if (guid.IsItem()) + { + Item* item = GetItemByGuid(guid); + + if (!item) + { + SendLootRelease(guid); + return; + } + + permission = OWNER_PERMISSION; + + loot = &item->loot; + + // If item doesn't already have loot, attempt to load it. If that + // fails then this is first time opening, generate loot + if (!item->m_lootGenerated && !sLootItemStorage->LoadStoredLoot(item, this)) + { + item->m_lootGenerated = true; + loot->clear(); + + switch (loot_type) + { + case LOOT_DISENCHANTING: + loot->FillLoot(item->GetTemplate()->DisenchantID, LootTemplates_Disenchant, this, true); + break; + case LOOT_PROSPECTING: + loot->FillLoot(item->GetEntry(), LootTemplates_Prospecting, this, true); + break; + case LOOT_MILLING: + loot->FillLoot(item->GetEntry(), LootTemplates_Milling, this, true); + break; + default: + loot->generateMoneyLoot(item->GetTemplate()->MinMoneyLoot, item->GetTemplate()->MaxMoneyLoot); + loot->FillLoot(item->GetEntry(), LootTemplates_Item, this, true, loot->gold != 0); + + // Force save the loot and money items that were just rolled + // Also saves the container item ID in Loot struct (not to DB) + if (loot->gold > 0 || loot->unlootedCount > 0) + sLootItemStorage->AddNewStoredLoot(item->GetGUID().GetCounter(), loot, this); + + break; + } + } + } + else if (guid.IsCorpse()) // remove insignia + { + Corpse* bones = ObjectAccessor::GetCorpse(*this, guid); + + if (!bones || !(loot_type == LOOT_CORPSE || loot_type == LOOT_INSIGNIA) || bones->GetType() != CORPSE_BONES) + { + SendLootRelease(guid); + return; + } + + loot = &bones->loot; + + if (loot->loot_type == LOOT_NONE) + { + uint32 pLevel = bones->loot.gold; + bones->loot.clear(); + + // For AV Achievement + if (Battleground* bg = GetBattleground()) + { + if (bg->GetTypeID(true) == BATTLEGROUND_AV) + loot->FillLoot(PLAYER_CORPSE_LOOT_ENTRY, LootTemplates_Creature, this, true); + } + // For wintergrasp Quests + else if (GetZoneId() == AREA_WINTERGRASP) + loot->FillLoot(PLAYER_CORPSE_LOOT_ENTRY, LootTemplates_Creature, this, true); + // For Tol Barad Quests + else if (GetZoneId() == AREA_TOLBARAD) + loot->FillLoot(PLAYER_CORPSE_LOOT_ENTRY, LootTemplates_Creature, this, true); + + // It may need a better formula + // Now it works like this: lvl10: ~6copper, lvl70: ~9silver + bones->loot.gold = uint32(urand(50, 150) * 0.016f * std::pow(float(pLevel) / 5.76f, 2.5f) * sWorld->getRate(RATE_DROP_MONEY)); + } + + if (bones->lootRecipient != this) + permission = NONE_PERMISSION; + else + permission = OWNER_PERMISSION; + } + else + { + Creature* creature = GetMap()->GetCreature(guid); + + // must be in range and creature must be alive for pickpocket and must be dead for another loot + if (!creature || creature->IsAlive() != (loot_type == LOOT_PICKPOCKETING) || !creature->IsWithinDistInMap(this, INTERACTION_DISTANCE)) + { + SendLootRelease(guid); + return; + } + + if (loot_type == LOOT_PICKPOCKETING && IsFriendlyTo(creature)) + { + SendLootRelease(guid); + return; + } + + loot = &creature->loot; + + if (loot_type == LOOT_PICKPOCKETING) + { + if (!loot || loot->loot_type != LOOT_PICKPOCKETING) + { + if (creature->CanGeneratePickPocketLoot()) + { + creature->StartPickPocketRefillTimer(); + loot->clear(); + + if (uint32 lootid = creature->GetCreatureTemplate()->pickpocketLootId) + loot->FillLoot(lootid, LootTemplates_Pickpocketing, this, true); + + // Generate extra money for pick pocket loot + const uint32 a = urand(0, creature->getLevel() / 2); + const uint32 b = urand(0, getLevel() / 2); + loot->gold = uint32(10 * (a + b) * sWorld->getRate(RATE_DROP_MONEY)); + permission = OWNER_PERMISSION; + } + else + { + permission = NONE_PERMISSION; + SendLootError(guid, LOOT_ERROR_ALREADY_PICKPOCKETED); + return; + } + } // else - still has pickpocket loot generated & not fully taken + } + else + { + // exploit fix + if (!creature->HasFlag(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_LOOTABLE)) + { + SendLootError(guid, LOOT_ERROR_DIDNT_KILL); + return; + } + + // the player whose group may loot the corpse + Player* recipient = creature->GetLootRecipient(); + Group* recipientGroup = creature->GetLootRecipientGroup(); + if (!recipient && !recipientGroup) + { + SendLootError(guid, LOOT_ERROR_DIDNT_KILL); + return; + } + + if (loot->loot_type == LOOT_NONE) + { + // for creature, loot is filled when creature is killed. + if (Group* group = creature->GetLootRecipientGroup()) + { + switch (group->GetLootMethod()) + { + case GROUP_LOOT: + // GroupLoot: rolls items over threshold. Items with quality < threshold, round robin + group->GroupLoot(loot, creature); + break; + case NEED_BEFORE_GREED: + group->NeedBeforeGreed(loot, creature); + break; + case MASTER_LOOT: + group->MasterLoot(loot, creature); + break; + default: + break; + } + } + } + + // if loot is already skinning loot then don't do anything else + if (loot->loot_type == LOOT_SKINNING) + { + loot_type = LOOT_SKINNING; + permission = creature->GetLootRecipientGUID() == GetGUID() ? OWNER_PERMISSION : NONE_PERMISSION; + } + else if (loot_type == LOOT_SKINNING) + { + loot->clear(); + loot->FillLoot(creature->GetCreatureTemplate()->SkinLootId, LootTemplates_Skinning, this, true); + permission = OWNER_PERMISSION; + + // Set new loot recipient + creature->SetLootRecipient(this, false); + } + // set group rights only for loot_type != LOOT_SKINNING + else + { + if (creature->GetLootRecipientGroup()) + { + Group* group = GetGroup(); + if (group == creature->GetLootRecipientGroup()) + { + switch (group->GetLootMethod()) + { + case MASTER_LOOT: + permission = group->GetMasterLooterGuid() == GetGUID() ? MASTER_PERMISSION : RESTRICTED_PERMISSION; + break; + case FREE_FOR_ALL: + permission = ALL_PERMISSION; + break; + case ROUND_ROBIN: + permission = ROUND_ROBIN_PERMISSION; + break; + default: + permission = GROUP_PERMISSION; + break; + } + } + else + permission = NONE_PERMISSION; + } + else if (creature->GetLootRecipient() == this) + permission = OWNER_PERMISSION; + else + permission = NONE_PERMISSION; + } + } + } + + // LOOT_INSIGNIA and LOOT_FISHINGHOLE unsupported by client + switch (loot_type) + { + case LOOT_INSIGNIA: loot_type = LOOT_SKINNING; break; + case LOOT_FISHINGHOLE: loot_type = LOOT_FISHING; break; + case LOOT_FISHING_JUNK: loot_type = LOOT_FISHING; break; + default: break; + } + + // need know merged fishing/corpse loot type for achievements + loot->loot_type = loot_type; + + if (permission != NONE_PERMISSION) + { + SetLootGUID(guid); + + WorldPackets::Loot::LootResponse packet; + packet.Owner = guid; + packet.AcquireReason = loot_type; + loot->BuildLootResponse(packet, this, permission); + SendDirectMessage(packet.Write()); + + // add 'this' player as one of the players that are looting 'loot' + loot->AddLooter(GetGUID()); + + if (loot_type == LOOT_CORPSE && !guid.IsItem()) + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_LOOTING); + } + else + SendLootError(GetLootGUID(), LOOT_ERROR_DIDNT_KILL); +} + +void Player::SendLootError(ObjectGuid guid, LootError error) const +{ + WorldPackets::Loot::LootResponse lootResponse; + lootResponse.Owner = guid; + lootResponse.AcquireReason = LOOT_NONE; + lootResponse.FailureReason = error; + SendDirectMessage(lootResponse.Write()); +} + +void Player::SendNotifyLootMoneyRemoved() const +{ + SendDirectMessage(WorldPackets::Loot::CoinRemoved().Write()); +} + +void Player::SendNotifyLootItemRemoved(uint8 lootSlot) const +{ + WorldPackets::Loot::LootRemoved packet; + packet.LootListID = lootSlot; + SendDirectMessage(packet.Write()); +} + +void Player::SendNotifyCurrencyLootRemoved(uint8 lootSlot) +{ + WorldPacket data(SMSG_CURRENCY_LOOT_REMOVED, 1); + data << uint8(lootSlot); + SendDirectMessage(&data); +} + +void Player::SendUpdateWorldState(uint32 variable, uint32 value, bool hidden /*= false*/) const +{ + WorldPackets::WorldState::UpdateWorldState worldstate; + worldstate.VariableID = variable; + worldstate.Value = value; + worldstate.Hidden = hidden; + SendDirectMessage(worldstate.Write()); +} + +void Player::SendInitWorldStates(uint32 zoneId, uint32 areaId) +{ + // data depends on zoneid/mapid... + uint32 mapid = GetMapId(); + + TC_LOG_DEBUG("network", "Sending SMSG_INIT_WORLD_STATES to Map: %u, Zone: %u", mapid, zoneId); + + WorldPackets::WorldState::InitWorldStates packet; + packet.MapID = mapid; + packet.AreaID = zoneId; + packet.SubareaID = areaId; + + sWorldStateMgr->FillInitialWorldStates(packet, GetMap(), areaId); + + SendDirectMessage(packet.Write()); +} + +uint32 Player::GetXPRestBonus(uint32 xp) +{ + uint32 rested_bonus = (uint32)GetRestBonus(); // xp for each rested bonus + + if (rested_bonus > xp) // max rested_bonus == xp or (r+x) = 200% xp + rested_bonus = xp; + + SetRestBonus(GetRestBonus() - rested_bonus); + + TC_LOG_DEBUG("entities.player", "Player::GetXPRestBonus: Player '%s' (%s) gain %u xp (+%u Rested Bonus). Rested points=%f", GetGUID().ToString().c_str(), GetName().c_str(), xp + rested_bonus, rested_bonus, GetRestBonus()); + return rested_bonus; +} + +void Player::SetBindPoint(ObjectGuid guid) const +{ + WorldPackets::Misc::BinderConfirm packet(guid); + SendDirectMessage(packet.Write()); +} + +void Player::SendTalentWipeConfirm(ObjectGuid guid) const +{ + WorldPacket data(MSG_TALENT_WIPE_CONFIRM, 8 + 4); + data << uint64(guid); + uint32 cost = sWorld->getBoolConfig(CONFIG_NO_RESET_TALENT_COST) ? 0 : GetNextResetTalentsCost(); + data << cost; + SendDirectMessage(&data); +} + +void Player::ResetPetTalents() +{ + // This needs another gossip option + NPC text as a confirmation. + // The confirmation gossip listid has the text: "Yes, please do." + Pet* pet = GetPet(); + + if (!pet || pet->getPetType() != HUNTER_PET || pet->m_usedTalentCount == 0) + return; + + CharmInfo* charmInfo = pet->GetCharmInfo(); + if (!charmInfo) + { + TC_LOG_ERROR("entities.player", "Object (GUID: %u TypeId: %u) is considered pet-like, but doesn't have charm info!", pet->GetGUID().GetCounter(), pet->GetTypeId()); + return; + } + pet->resetTalents(); + SendTalentsInfoData(true); +} + +/*********************************************************/ +/*** STORAGE SYSTEM ***/ +/*********************************************************/ + +void Player::SetVirtualItemSlot(uint8 i, Item* item) +{ + ASSERT(i < 3); + if (i < 2 && item) + { + if (!item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) + return; + uint32 charges = item->GetEnchantmentCharges(TEMP_ENCHANTMENT_SLOT); + if (charges == 0) + return; + if (charges > 1) + item->SetEnchantmentCharges(TEMP_ENCHANTMENT_SLOT, charges-1); + else + { + ApplyEnchantment(item, TEMP_ENCHANTMENT_SLOT, false); + item->ClearEnchantment(TEMP_ENCHANTMENT_SLOT); + } + } +} + +void Player::SetSheath(SheathState sheathed) +{ + switch (sheathed) + { + case SHEATH_STATE_UNARMED: // no prepared weapon + SetVirtualItemSlot(0, nullptr); + SetVirtualItemSlot(1, nullptr); + SetVirtualItemSlot(2, nullptr); + break; + case SHEATH_STATE_MELEE: // prepared melee weapon + SetVirtualItemSlot(0, GetWeaponForAttack(BASE_ATTACK, true)); + SetVirtualItemSlot(1, GetWeaponForAttack(OFF_ATTACK, true)); + SetVirtualItemSlot(2, nullptr); + break; + case SHEATH_STATE_RANGED: // prepared ranged weapon + SetVirtualItemSlot(0, nullptr); + SetVirtualItemSlot(1, nullptr); + SetVirtualItemSlot(2, GetWeaponForAttack(RANGED_ATTACK, true)); + break; + default: + SetVirtualItemSlot(0, nullptr); + SetVirtualItemSlot(1, nullptr); + SetVirtualItemSlot(2, nullptr); + break; + } + Unit::SetSheath(sheathed); // this must visualize Sheath changing for other players... +} + +uint8 Player::FindEquipSlot(ItemTemplate const* proto, uint32 slot, bool swap) const +{ + uint8 playerClass = getClass(); + + uint8 slots[4]; + slots[0] = NULL_SLOT; + slots[1] = NULL_SLOT; + slots[2] = NULL_SLOT; + slots[3] = NULL_SLOT; + switch (proto->GetInventoryType()) + { + case INVTYPE_HEAD: + slots[0] = EQUIPMENT_SLOT_HEAD; + break; + case INVTYPE_NECK: + slots[0] = EQUIPMENT_SLOT_NECK; + break; + case INVTYPE_SHOULDERS: + slots[0] = EQUIPMENT_SLOT_SHOULDERS; + break; + case INVTYPE_BODY: + slots[0] = EQUIPMENT_SLOT_BODY; + break; + case INVTYPE_CHEST: + slots[0] = EQUIPMENT_SLOT_CHEST; + break; + case INVTYPE_ROBE: + slots[0] = EQUIPMENT_SLOT_CHEST; + break; + case INVTYPE_WAIST: + slots[0] = EQUIPMENT_SLOT_WAIST; + break; + case INVTYPE_LEGS: + slots[0] = EQUIPMENT_SLOT_LEGS; + break; + case INVTYPE_FEET: + slots[0] = EQUIPMENT_SLOT_FEET; + break; + case INVTYPE_WRISTS: + slots[0] = EQUIPMENT_SLOT_WRISTS; + break; + case INVTYPE_HANDS: + slots[0] = EQUIPMENT_SLOT_HANDS; + break; + case INVTYPE_FINGER: + slots[0] = EQUIPMENT_SLOT_FINGER1; + slots[1] = EQUIPMENT_SLOT_FINGER2; + break; + case INVTYPE_TRINKET: + slots[0] = EQUIPMENT_SLOT_TRINKET1; + slots[1] = EQUIPMENT_SLOT_TRINKET2; + break; + case INVTYPE_CLOAK: + slots[0] = EQUIPMENT_SLOT_BACK; + break; + case INVTYPE_WEAPON: + { + slots[0] = EQUIPMENT_SLOT_MAINHAND; + + // suggest offhand slot only if know dual wielding + // (this will be replace mainhand weapon at auto equip instead unwonted "you don't known dual wielding" ... + if (CanDualWield()) + slots[1] = EQUIPMENT_SLOT_OFFHAND; + break; + } + case INVTYPE_SHIELD: + slots[0] = EQUIPMENT_SLOT_OFFHAND; + break; + case INVTYPE_RANGED: + slots[0] = EQUIPMENT_SLOT_RANGED; + break; + case INVTYPE_2HWEAPON: + slots[0] = EQUIPMENT_SLOT_MAINHAND; + if (Item* mhWeapon = GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND)) + { + if (ItemTemplate const* mhWeaponProto = mhWeapon->GetTemplate()) + { + if (mhWeaponProto->GetSubClass() == ITEM_SUBCLASS_WEAPON_POLEARM || mhWeaponProto->GetSubClass() == ITEM_SUBCLASS_WEAPON_STAFF) + { + const_cast(this)->AutoUnequipOffhandIfNeed(true); + break; + } + } + } + + if (GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND)) + { + if (proto->GetSubClass() == ITEM_SUBCLASS_WEAPON_POLEARM || proto->GetSubClass() == ITEM_SUBCLASS_WEAPON_STAFF) + { + const_cast(this)->AutoUnequipOffhandIfNeed(true); + break; + } + } + if (CanDualWield() && CanTitanGrip() && proto->GetSubClass() != ITEM_SUBCLASS_WEAPON_POLEARM && proto->GetSubClass() != ITEM_SUBCLASS_WEAPON_STAFF) + slots[1] = EQUIPMENT_SLOT_OFFHAND; + break; + case INVTYPE_TABARD: + slots[0] = EQUIPMENT_SLOT_TABARD; + break; + case INVTYPE_WEAPONMAINHAND: + slots[0] = EQUIPMENT_SLOT_MAINHAND; + break; + case INVTYPE_WEAPONOFFHAND: + slots[0] = EQUIPMENT_SLOT_OFFHAND; + break; + case INVTYPE_HOLDABLE: + slots[0] = EQUIPMENT_SLOT_OFFHAND; + break; + case INVTYPE_THROWN: + slots[0] = EQUIPMENT_SLOT_RANGED; + break; + case INVTYPE_RANGEDRIGHT: + slots[0] = EQUIPMENT_SLOT_RANGED; + break; + case INVTYPE_BAG: + slots[0] = INVENTORY_SLOT_BAG_START + 0; + slots[1] = INVENTORY_SLOT_BAG_START + 1; + slots[2] = INVENTORY_SLOT_BAG_START + 2; + slots[3] = INVENTORY_SLOT_BAG_START + 3; + break; + case INVTYPE_RELIC: + { + if (playerClass == CLASS_PALADIN || playerClass == CLASS_DRUID || + playerClass == CLASS_SHAMAN || playerClass == CLASS_DEATH_KNIGHT) + slots[0] = EQUIPMENT_SLOT_RANGED; + break; + } + default: + return NULL_SLOT; + } + + if (slot != NULL_SLOT) + { + if (swap || !GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + for (uint8 i = 0; i < 4; ++i) + if (slots[i] == slot) + return slot; + } + else + { + // search free slot at first + for (uint8 i = 0; i < 4; ++i) + if (slots[i] != NULL_SLOT && !GetItemByPos(INVENTORY_SLOT_BAG_0, slots[i])) + // in case 2hand equipped weapon (without titan grip) offhand slot empty but not free + if (slots[i] != EQUIPMENT_SLOT_OFFHAND || !IsTwoHandUsed()) + return slots[i]; + + // if not found free and can swap return first appropriate from used + for (uint8 i = 0; i < 4; ++i) + if (slots[i] != NULL_SLOT && swap) + return slots[i]; + } + + // no free position + return NULL_SLOT; +} + +InventoryResult Player::CanUnequipItems(uint32 item, uint32 count) const +{ + uint32 tempcount = 0; + + InventoryResult res = EQUIP_ERR_OK; + + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->GetEntry() == item) + { + InventoryResult ires = CanUnequipItem(INVENTORY_SLOT_BAG_0 << 8 | i, false); + if (ires == EQUIP_ERR_OK) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return EQUIP_ERR_OK; + } + else + res = ires; + } + + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->GetEntry() == item) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return EQUIP_ERR_OK; + } + + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); ++j) + if (Item* pItem = GetItemByPos(i, j)) + if (pItem->GetEntry() == item) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return EQUIP_ERR_OK; + } + + // not found req. item count and have unequippable items + return res; +} + +uint32 Player::GetItemCount(uint32 item, bool inBankAlso, Item* skipItem) const +{ + uint32 count = 0; + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem != skipItem && pItem->GetEntry() == item) + count += pItem->GetCount(); + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + count += pBag->GetItemCount(item, skipItem); + + if (skipItem && skipItem->GetTemplate()->GetGemProperties()) + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem != skipItem && pItem->GetTemplate()->GetSocketColor(0)) + count += pItem->GetGemCountWithID(item); + + if (inBankAlso) + { + // checking every item from 39 to 74 (including bank bags) + for (uint8 i = BANK_SLOT_ITEM_START; i < BANK_SLOT_BAG_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem != skipItem && pItem->GetEntry() == item) + count += pItem->GetCount(); + + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + count += pBag->GetItemCount(item, skipItem); + + if (skipItem && skipItem->GetTemplate()->GetGemProperties()) + for (uint8 i = BANK_SLOT_ITEM_START; i < BANK_SLOT_ITEM_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem != skipItem && pItem->GetTemplate()->GetSocketColor(0)) + count += pItem->GetGemCountWithID(item); + } + + return count; +} + +uint32 Player::GetItemCountWithLimitCategory(uint32 limitCategory, Item* skipItem) const +{ + uint32 count = 0; + for (int i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem != skipItem) + if (ItemTemplate const* pProto = pItem->GetTemplate()) + if (pProto->GetItemLimitCategory() == limitCategory) + count += pItem->GetCount(); + + for (int i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + count += pBag->GetItemCountWithLimitCategory(limitCategory, skipItem); + + for (int i = BANK_SLOT_ITEM_START; i < BANK_SLOT_BAG_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem != skipItem) + if (ItemTemplate const* pProto = pItem->GetTemplate()) + if (pProto->GetItemLimitCategory() == limitCategory) + count += pItem->GetCount(); + + for (int i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + count += pBag->GetItemCountWithLimitCategory(limitCategory, skipItem); + + return count; +} + +Item* Player::GetItemByGuid(ObjectGuid guid) const +{ + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->GetGUID() == guid) + return pItem; + + for (int i = BANK_SLOT_ITEM_START; i < BANK_SLOT_BAG_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->GetGUID() == guid) + return pItem; + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); ++j) + if (Item* pItem = pBag->GetItemByPos(j)) + if (pItem->GetGUID() == guid) + return pItem; + + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); ++j) + if (Item* pItem = pBag->GetItemByPos(j)) + if (pItem->GetGUID() == guid) + return pItem; + + return nullptr; +} + +Item* Player::GetItemByPos(uint16 pos) const +{ + uint8 bag = pos >> 8; + uint8 slot = pos & 255; + return GetItemByPos(bag, slot); +} + +Item* Player::GetItemByPos(uint8 bag, uint8 slot) const +{ + if (bag == INVENTORY_SLOT_BAG_0 && slot < BANK_SLOT_BAG_END) + return m_items[slot]; + if (Bag* pBag = GetBagByPos(bag)) + return pBag->GetItemByPos(slot); + return nullptr; +} + +//Does additional check for disarmed weapons +Item* Player::GetUseableItemByPos(uint8 bag, uint8 slot) const +{ + if (!CanUseAttackType(GetAttackBySlot(slot))) + return nullptr; + return GetItemByPos(bag, slot); +} + +Bag* Player::GetBagByPos(uint8 bag) const +{ + if ((bag >= INVENTORY_SLOT_BAG_START && bag < INVENTORY_SLOT_BAG_END) + || (bag >= BANK_SLOT_BAG_START && bag < BANK_SLOT_BAG_END)) + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, bag)) + return item->ToBag(); + return nullptr; +} + +uint32 Player::GetFreeInventorySpace() const +{ + uint32 freeSpace = 0; + + // Check backpack + for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) + { + Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + if (!item) + freeSpace += 1; + } + + // Check bags + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + { + if (Bag* bag = GetBagByPos(i)) + freeSpace += bag->GetFreeSlots(); + } + + return freeSpace; +} + +Item* Player::GetWeaponForAttack(WeaponAttackType attackType, bool useable /*= false*/) const +{ + uint8 slot; + switch (attackType) + { + case BASE_ATTACK: slot = EQUIPMENT_SLOT_MAINHAND; break; + case OFF_ATTACK: slot = EQUIPMENT_SLOT_OFFHAND; break; + case RANGED_ATTACK: slot = EQUIPMENT_SLOT_RANGED; break; + default: return nullptr; + } + + Item* item; + if (useable) + item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, slot); + else + item = GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + if (!item || item->GetTemplate()->GetClass() != ITEM_CLASS_WEAPON) + return nullptr; + + if (!useable) + return item; + + if (item->IsBroken() || IsInFeralForm()) + return nullptr; + + return item; +} + +Item* Player::GetShield(bool useable) const +{ + Item* item = nullptr; + if (useable) + item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + else + item = GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!item || item->GetTemplate()->GetClass() != ITEM_CLASS_ARMOR) + return nullptr; + + if (!useable) + return item; + + if (item->IsBroken()) + return nullptr; + + return item; +} + +WeaponAttackType Player::GetAttackBySlot(uint8 slot) +{ + switch (slot) + { + case EQUIPMENT_SLOT_MAINHAND: return BASE_ATTACK; + case EQUIPMENT_SLOT_OFFHAND: return OFF_ATTACK; + case EQUIPMENT_SLOT_RANGED: return RANGED_ATTACK; + default: return MAX_ATTACK; + } +} + +bool Player::IsInventoryPos(uint8 bag, uint8 slot) +{ + if (bag == INVENTORY_SLOT_BAG_0 && slot == NULL_SLOT) + return true; + if (bag == INVENTORY_SLOT_BAG_0 && (slot >= INVENTORY_SLOT_ITEM_START && slot < INVENTORY_SLOT_ITEM_END)) + return true; + if (bag >= INVENTORY_SLOT_BAG_START && bag < INVENTORY_SLOT_BAG_END) + return true; + return false; +} + +bool Player::IsEquipmentPos(uint8 bag, uint8 slot) +{ + if (bag == INVENTORY_SLOT_BAG_0 && (slot < EQUIPMENT_SLOT_END)) + return true; + if (bag == INVENTORY_SLOT_BAG_0 && (slot >= INVENTORY_SLOT_BAG_START && slot < INVENTORY_SLOT_BAG_END)) + return true; + return false; +} + +bool Player::IsBankPos(uint8 bag, uint8 slot) +{ + if (bag == INVENTORY_SLOT_BAG_0 && (slot >= BANK_SLOT_ITEM_START && slot < BANK_SLOT_ITEM_END)) + return true; + if (bag == INVENTORY_SLOT_BAG_0 && (slot >= BANK_SLOT_BAG_START && slot < BANK_SLOT_BAG_END)) + return true; + if (bag >= BANK_SLOT_BAG_START && bag < BANK_SLOT_BAG_END) + return true; + return false; +} + +bool Player::IsBagPos(uint16 pos) +{ + uint8 bag = pos >> 8; + uint8 slot = pos & 255; + if (bag == INVENTORY_SLOT_BAG_0 && (slot >= INVENTORY_SLOT_BAG_START && slot < INVENTORY_SLOT_BAG_END)) + return true; + if (bag == INVENTORY_SLOT_BAG_0 && (slot >= BANK_SLOT_BAG_START && slot < BANK_SLOT_BAG_END)) + return true; + return false; +} + +bool Player::IsValidPos(uint8 bag, uint8 slot, bool explicit_pos) const +{ + // post selected + if (bag == NULL_BAG && !explicit_pos) + return true; + + if (bag == INVENTORY_SLOT_BAG_0) + { + // any post selected + if (slot == NULL_SLOT && !explicit_pos) + return true; + + // equipment + if (slot < EQUIPMENT_SLOT_END) + return true; + + // bag equip slots + if (slot >= INVENTORY_SLOT_BAG_START && slot < INVENTORY_SLOT_BAG_END) + return true; + + // backpack slots + if (slot >= INVENTORY_SLOT_ITEM_START && slot < INVENTORY_SLOT_ITEM_END) + return true; + + // bank main slots + if (slot >= BANK_SLOT_ITEM_START && slot < BANK_SLOT_ITEM_END) + return true; + + // bank bag slots + if (slot >= BANK_SLOT_BAG_START && slot < BANK_SLOT_BAG_END) + return true; + + return false; + } + + // bag content slots + // bank bag content slots + if (Bag* pBag = GetBagByPos(bag)) + { + // any post selected + if (slot == NULL_SLOT && !explicit_pos) + return true; + + return slot < pBag->GetBagSize(); + } + + // where this? + return false; +} + +bool Player::HasItemCount(uint32 item, uint32 count, bool inBankAlso) const +{ + uint32 tempcount = 0; + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; i++) + { + Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (pItem && pItem->GetEntry() == item && !pItem->IsInTrade()) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return true; + } + } + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + { + if (Bag* pBag = GetBagByPos(i)) + { + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + { + Item* pItem = GetItemByPos(i, j); + if (pItem && pItem->GetEntry() == item && !pItem->IsInTrade()) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return true; + } + } + } + } + + if (inBankAlso) + { + for (uint8 i = BANK_SLOT_ITEM_START; i < BANK_SLOT_BAG_END; i++) + { + Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (pItem && pItem->GetEntry() == item && !pItem->IsInTrade()) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return true; + } + } + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; i++) + { + if (Bag* pBag = GetBagByPos(i)) + { + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + { + Item* pItem = GetItemByPos(i, j); + if (pItem && pItem->GetEntry() == item && !pItem->IsInTrade()) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return true; + } + } + } + } + } + + return false; +} + +bool Player::HasItemOrGemWithIdEquipped(uint32 item, uint32 count, uint8 except_slot) const +{ + uint32 tempcount = 0; + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) + { + if (i == except_slot) + continue; + + Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (pItem && pItem->GetEntry() == item) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return true; + } + } + + ItemTemplate const* pProto = sObjectMgr->GetItemTemplate(item); + if (pProto && pProto->GetGemProperties()) + { + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) + { + if (i == except_slot) + continue; + + Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (pItem && pItem->GetTemplate()->GetSocketColor(0)) + { + tempcount += pItem->GetGemCountWithID(item); + if (tempcount >= count) + return true; + } + } + } + + return false; +} + +bool Player::HasItemWithLimitCategoryEquipped(uint32 limitCategory, uint32 count, uint8 except_slot) const +{ + uint32 tempcount = 0; + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) + { + if (i == except_slot) + continue; + + Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (!pItem) + continue; + + ItemTemplate const* pProto = pItem->GetTemplate(); + if (!pProto) + continue; + + if (pProto->GetItemLimitCategory() == limitCategory) + { + tempcount += pItem->GetCount(); + if (tempcount >= count) + return true; + } + } + + return false; +} + +bool Player::HasGemWithLimitCategoryEquipped(uint32 limitCategory, uint32 count, uint8 except_slot) const +{ + uint32 tempcount = 0; + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) + { + if (i == except_slot) + continue; + + Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (!pItem) + continue; + + ItemTemplate const* pProto = pItem->GetTemplate(); + if (!pProto) + continue; + + if (pProto->GetSocketColor(0) || pItem->GetEnchantmentId(PRISMATIC_ENCHANTMENT_SLOT)) + { + tempcount += pItem->GetGemCountWithLimitCategory(limitCategory); + if (tempcount >= count) + return true; + } + } + + return false; +} + +InventoryResult Player::CanTakeMoreSimilarItems(uint32 entry, uint32 count, Item* pItem, uint32* no_space_count /*= nullptr*/, uint32* offendingItemId /*= nullptr*/) const +{ + ItemTemplate const* pProto = sObjectMgr->GetItemTemplate(entry); + if (!pProto) + { + if (no_space_count) + *no_space_count = count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + + if (pItem && pItem->m_lootGenerated) + return EQUIP_ERR_LOOT_GONE; + + // no maximum + if ((pProto->GetMaxCount() <= 0 && pProto->GetItemLimitCategory() == 0) || pProto->GetMaxCount() == 2147483647) + return EQUIP_ERR_OK; + + if (pProto->GetMaxCount() > 0) + { + uint32 curcount = GetItemCount(pProto->GetId(), true, pItem); + if (curcount + count > uint32(pProto->GetMaxCount())) + { + if (no_space_count) + *no_space_count = count + curcount - pProto->GetMaxCount(); + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + + // check unique-equipped limit + if (pProto->GetItemLimitCategory()) + { + ItemLimitCategoryEntry const* limitEntry = sItemLimitCategoryStore.LookupEntry(pProto->GetItemLimitCategory()); + if (!limitEntry) + { + if (no_space_count) + *no_space_count = count; + return EQUIP_ERR_NOT_EQUIPPABLE; + } + + if (limitEntry->Flags == ITEM_LIMIT_CATEGORY_MODE_HAVE) + { + uint32 curcount = GetItemCountWithLimitCategory(pProto->GetItemLimitCategory(), pItem); + if (curcount + count > uint32(limitEntry->Quantity)) + { + if (no_space_count) + *no_space_count = count + curcount - limitEntry->Quantity; + if (offendingItemId) + *offendingItemId = pProto->GetId(); + return EQUIP_ERR_ITEM_MAX_LIMIT_CATEGORY_COUNT_EXCEEDED_IS; + } + } + } + + return EQUIP_ERR_OK; +} + +InventoryResult Player::CanTakeMoreSimilarItems(Item* pItem, uint32* itemLimitCategory /*= nullptr*/) const +{ + return CanTakeMoreSimilarItems(pItem->GetEntry(), pItem->GetCount(), pItem, nullptr, itemLimitCategory); +} + +InventoryResult Player::CanStoreNewItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, uint32 item, uint32 count, uint32* no_space_count /*= nullptr*/) const +{ + return CanStoreItem(bag, slot, dest, item, count, nullptr, false, no_space_count); +} + +InventoryResult Player::CanStoreItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, Item* pItem, bool swap /*= false*/) const +{ + if (!pItem) + return EQUIP_ERR_ITEM_NOT_FOUND; + uint32 count = pItem->GetCount(); + return CanStoreItem(bag, slot, dest, pItem->GetEntry(), count, pItem, swap, nullptr); +} + +bool Player::HasItemTotemCategory(uint32 TotemCategory) const +{ + Item* item; + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; ++i) + { + item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, i); + if (item && DBCManager::IsTotemCategoryCompatibleWith(item->GetTemplate()->GetTotemCategory(), TotemCategory)) + return true; + } + + Bag* bag; + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + { + bag = GetBagByPos(i); + if (bag) + { + for (uint32 j = 0; j < bag->GetBagSize(); ++j) + { + item = GetUseableItemByPos(i, j); + if (item && sDBCManager.IsTotemCategoryCompatibleWith(item->GetTemplate()->GetTotemCategory(), TotemCategory)) + return true; + } + } + } + + return false; +} + +InventoryResult Player::CanStoreItem_InSpecificSlot(uint8 bag, uint8 slot, ItemPosCountVec &dest, ItemTemplate const* pProto, uint32& count, bool swap, Item* pSrcItem) const +{ + Item* pItem2 = GetItemByPos(bag, slot); + + // ignore move item (this slot will be empty at move) + if (pItem2 == pSrcItem) + pItem2 = nullptr; + + uint32 need_space; + + if (pSrcItem && pSrcItem->IsNotEmptyBag() && !IsBagPos(uint16(bag) << 8 | slot)) + return EQUIP_ERR_DESTROY_NONEMPTY_BAG; + + // empty specific slot - check item fit to slot + if (!pItem2 || swap) + { + if (bag == INVENTORY_SLOT_BAG_0) + { + // prevent cheating + if ((slot >= BUYBACK_SLOT_START && slot < BUYBACK_SLOT_END) || slot >= PLAYER_SLOT_END) + return EQUIP_ERR_WRONG_BAG_TYPE; + } + else + { + Bag* pBag = GetBagByPos(bag); + if (!pBag) + return EQUIP_ERR_WRONG_BAG_TYPE; + + ItemTemplate const* pBagProto = pBag->GetTemplate(); + if (!pBagProto) + return EQUIP_ERR_WRONG_BAG_TYPE; + + if (slot >= pBagProto->GetContainerSlots()) + return EQUIP_ERR_WRONG_BAG_TYPE; + + if (!ItemCanGoIntoBag(pProto, pBagProto)) + return EQUIP_ERR_WRONG_BAG_TYPE; + } + + // non empty stack with space + need_space = pProto->GetMaxStackSize(); + } + // non empty slot, check item type + else + { + // can be merged at least partly + InventoryResult res = pItem2->CanBeMergedPartlyWith(pProto); + if (res != EQUIP_ERR_OK) + return res; + + // free stack space or infinity + need_space = pProto->GetMaxStackSize() - pItem2->GetCount(); + } + + if (need_space > count) + need_space = count; + + ItemPosCount newPosition = ItemPosCount((bag << 8) | slot, need_space); + if (!newPosition.isContainedIn(dest)) + { + dest.push_back(newPosition); + count -= need_space; + } + return EQUIP_ERR_OK; +} + +InventoryResult Player::CanStoreItem_InBag(uint8 bag, ItemPosCountVec &dest, ItemTemplate const* pProto, uint32& count, bool merge, bool non_specialized, Item* pSrcItem, uint8 skip_bag, uint8 skip_slot) const +{ + // skip specific bag already processed in first called CanStoreItem_InBag + if (bag == skip_bag) + return EQUIP_ERR_WRONG_BAG_TYPE; + + // skip non-existing bag or self targeted bag + Bag* pBag = GetBagByPos(bag); + if (!pBag || pBag == pSrcItem) + return EQUIP_ERR_WRONG_BAG_TYPE; + + if (pSrcItem && pSrcItem->IsNotEmptyBag()) + return EQUIP_ERR_DESTROY_NONEMPTY_BAG; + + ItemTemplate const* pBagProto = pBag->GetTemplate(); + if (!pBagProto) + return EQUIP_ERR_WRONG_BAG_TYPE; + + // specialized bag mode or non-specilized + if (non_specialized != (pBagProto->GetClass() == ITEM_CLASS_CONTAINER && pBagProto->GetSubClass() == ITEM_SUBCLASS_CONTAINER)) + return EQUIP_ERR_WRONG_BAG_TYPE; + + if (!ItemCanGoIntoBag(pProto, pBagProto)) + return EQUIP_ERR_WRONG_BAG_TYPE; + + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + { + // skip specific slot already processed in first called CanStoreItem_InSpecificSlot + if (j == skip_slot) + continue; + + Item* pItem2 = GetItemByPos(bag, j); + + // ignore move item (this slot will be empty at move) + if (pItem2 == pSrcItem) + pItem2 = nullptr; + + // if merge skip empty, if !merge skip non-empty + if ((pItem2 != nullptr) != merge) + continue; + + uint32 need_space = pProto->GetMaxStackSize(); + + if (pItem2) + { + // can be merged at least partly + if (pItem2->CanBeMergedPartlyWith(pProto) != EQUIP_ERR_OK) + continue; + + // descrease at current stacksize + need_space -= pItem2->GetCount(); + } + + if (need_space > count) + need_space = count; + + ItemPosCount newPosition = ItemPosCount((bag << 8) | j, need_space); + if (!newPosition.isContainedIn(dest)) + { + dest.push_back(newPosition); + count -= need_space; + + if (count==0) + return EQUIP_ERR_OK; + } + } + return EQUIP_ERR_OK; +} + +InventoryResult Player::CanStoreItem_InInventorySlots(uint8 slot_begin, uint8 slot_end, ItemPosCountVec &dest, ItemTemplate const* pProto, uint32& count, bool merge, Item* pSrcItem, uint8 skip_bag, uint8 skip_slot) const +{ + //this is never called for non-bag slots so we can do this + if (pSrcItem && pSrcItem->IsNotEmptyBag()) + return EQUIP_ERR_DESTROY_NONEMPTY_BAG; + + for (uint32 j = slot_begin; j < slot_end; j++) + { + // skip specific slot already processed in first called CanStoreItem_InSpecificSlot + if (INVENTORY_SLOT_BAG_0 == skip_bag && j == skip_slot) + continue; + + Item* pItem2 = GetItemByPos(INVENTORY_SLOT_BAG_0, j); + + // ignore move item (this slot will be empty at move) + if (pItem2 == pSrcItem) + pItem2 = nullptr; + + // if merge skip empty, if !merge skip non-empty + if ((pItem2 != nullptr) != merge) + continue; + + uint32 need_space = pProto->GetMaxStackSize(); + + if (pItem2) + { + // can be merged at least partly + if (pItem2->CanBeMergedPartlyWith(pProto) != EQUIP_ERR_OK) + continue; + + // descrease at current stacksize + need_space -= pItem2->GetCount(); + } + + if (need_space > count) + need_space = count; + + ItemPosCount newPosition = ItemPosCount((INVENTORY_SLOT_BAG_0 << 8) | j, need_space); + if (!newPosition.isContainedIn(dest)) + { + dest.push_back(newPosition); + count -= need_space; + + if (count==0) + return EQUIP_ERR_OK; + } + } + return EQUIP_ERR_OK; +} + +InventoryResult Player::CanStoreItem(uint8 bag, uint8 slot, ItemPosCountVec &dest, uint32 entry, uint32 count, Item* pItem, bool swap, uint32* no_space_count) const +{ + TC_LOG_DEBUG("entities.player.items", "Player::CanStoreItem: Bag: %u, Slot: %u, Item: %u, Count: %u", bag, slot, entry, count); + + ItemTemplate const* pProto = sObjectMgr->GetItemTemplate(entry); + if (!pProto) + { + if (no_space_count) + *no_space_count = count; + return swap ? EQUIP_ERR_CANT_SWAP : EQUIP_ERR_ITEM_NOT_FOUND; + } + + if (pItem) + { + // item used + if (pItem->m_lootGenerated) + { + if (no_space_count) + *no_space_count = count; + return EQUIP_ERR_LOOT_GONE; + } + + if (pItem->IsBindedNotWith(this)) + { + if (no_space_count) + *no_space_count = count; + return EQUIP_ERR_NOT_OWNER; + } + } + + // check count of items (skip for auto move for same player from bank) + uint32 no_similar_count = 0; // can't store this amount similar items + InventoryResult res = CanTakeMoreSimilarItems(entry, count, pItem, &no_similar_count); + if (res != EQUIP_ERR_OK) + { + if (count == no_similar_count) + { + if (no_space_count) + *no_space_count = no_similar_count; + return res; + } + count -= no_similar_count; + } + + // in specific slot + if (bag != NULL_BAG && slot != NULL_SLOT) + { + res = CanStoreItem_InSpecificSlot(bag, slot, dest, pProto, count, swap, pItem); + if (res != EQUIP_ERR_OK) + { + if (no_space_count) + *no_space_count = count + no_similar_count; + return res; + } + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + + // not specific slot or have space for partly store only in specific slot + + // in specific bag + if (bag != NULL_BAG) + { + // search stack in bag for merge to + if (pProto->GetMaxStackSize() != 1) + { + if (bag == INVENTORY_SLOT_BAG_0) // inventory + { + res = CanStoreItem_InInventorySlots(INVENTORY_SLOT_ITEM_START, INVENTORY_SLOT_ITEM_END, dest, pProto, count, true, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + { + if (no_space_count) + *no_space_count = count + no_similar_count; + return res; + } + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + else // equipped bag + { + // we need check 2 time (specialized/non_specialized), use NULL_BAG to prevent skipping bag + res = CanStoreItem_InBag(bag, dest, pProto, count, true, false, pItem, NULL_BAG, slot); + if (res != EQUIP_ERR_OK) + res = CanStoreItem_InBag(bag, dest, pProto, count, true, true, pItem, NULL_BAG, slot); + + if (res != EQUIP_ERR_OK) + { + if (no_space_count) + *no_space_count = count + no_similar_count; + return res; + } + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + } + + // search free slot in bag for place to + if (bag == INVENTORY_SLOT_BAG_0) // inventory + { + res = CanStoreItem_InInventorySlots(INVENTORY_SLOT_ITEM_START, INVENTORY_SLOT_ITEM_END, dest, pProto, count, false, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + { + if (no_space_count) + *no_space_count = count + no_similar_count; + return res; + } + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + else // equipped bag + { + res = CanStoreItem_InBag(bag, dest, pProto, count, false, false, pItem, NULL_BAG, slot); + if (res != EQUIP_ERR_OK) + res = CanStoreItem_InBag(bag, dest, pProto, count, false, true, pItem, NULL_BAG, slot); + + if (res != EQUIP_ERR_OK) + { + if (no_space_count) + *no_space_count = count + no_similar_count; + return res; + } + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + } + + // not specific bag or have space for partly store only in specific bag + + // search stack for merge to + if (pProto->GetMaxStackSize() != 1) + { + res = CanStoreItem_InInventorySlots(INVENTORY_SLOT_ITEM_START, INVENTORY_SLOT_ITEM_END, dest, pProto, count, true, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + { + if (no_space_count) + *no_space_count = count + no_similar_count; + return res; + } + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + + if (pProto->GetBagFamily()) + { + for (uint32 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + { + res = CanStoreItem_InBag(i, dest, pProto, count, true, false, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + continue; + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + } + + for (uint32 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + { + res = CanStoreItem_InBag(i, dest, pProto, count, true, true, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + continue; + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + } + + // search free slot - special bag case + if (pProto->GetBagFamily()) + { + for (uint32 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + { + res = CanStoreItem_InBag(i, dest, pProto, count, false, false, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + continue; + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + } + + if (pItem && pItem->IsNotEmptyBag()) + return EQUIP_ERR_BAG_IN_BAG; + + // search free slot + uint8 searchSlotStart = INVENTORY_SLOT_ITEM_START; + // new bags can be directly equipped + if (!pItem && pProto->GetClass() == ITEM_CLASS_CONTAINER && pProto->GetSubClass() == ITEM_SUBCLASS_CONTAINER && + (pProto->GetBonding() == BIND_NONE || pProto->GetBonding() == BIND_ON_ACQUIRE)) + searchSlotStart = INVENTORY_SLOT_BAG_START; + + res = CanStoreItem_InInventorySlots(searchSlotStart, INVENTORY_SLOT_ITEM_END, dest, pProto, count, false, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + { + if (no_space_count) + *no_space_count = count + no_similar_count; + return res; + } + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + { + res = CanStoreItem_InBag(i, dest, pProto, count, false, true, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + continue; + + if (count == 0) + { + if (no_similar_count == 0) + return EQUIP_ERR_OK; + + if (no_space_count) + *no_space_count = count + no_similar_count; + return EQUIP_ERR_ITEM_MAX_COUNT; + } + } + + if (no_space_count) + *no_space_count = count + no_similar_count; + + return EQUIP_ERR_INV_FULL; +} + +////////////////////////////////////////////////////////////////////////// +InventoryResult Player::CanStoreItems(Item** items, int count, uint32* offendingItemId) const +{ + Item* item2; + + // fill space table + uint32 inventoryCounts[INVENTORY_SLOT_ITEM_END - INVENTORY_SLOT_ITEM_START]; + uint32 bagCounts[INVENTORY_SLOT_BAG_END - INVENTORY_SLOT_BAG_START][MAX_BAG_SIZE]; + + memset(inventoryCounts, 0, sizeof(uint32) * (INVENTORY_SLOT_ITEM_END - INVENTORY_SLOT_ITEM_START)); + memset(bagCounts, 0, sizeof(uint32) * (INVENTORY_SLOT_BAG_END - INVENTORY_SLOT_BAG_START) * MAX_BAG_SIZE); + + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; i++) + { + item2 = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (item2 && !item2->IsInTrade()) + inventoryCounts[i - INVENTORY_SLOT_ITEM_START] = item2->GetCount(); + } + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + { + item2 = GetItemByPos(i, j); + if (item2 && !item2->IsInTrade()) + bagCounts[i - INVENTORY_SLOT_BAG_START][j] = item2->GetCount(); + } + + // check free space for all items + for (int k = 0; k < count; ++k) + { + Item* item = items[k]; + + // no item + if (!item) + continue; + + TC_LOG_DEBUG("entities.player.items", "Player::CanStoreItems: Player '%s' (%s), Index: %i ItemID: %u, Count: %u", + GetName().c_str(), GetGUID().ToString().c_str(), k + 1, item->GetEntry(), item->GetCount()); + ItemTemplate const* pProto = item->GetTemplate(); + + // strange item + if (!pProto) + return EQUIP_ERR_ITEM_NOT_FOUND; + + // item used + if (item->m_lootGenerated) + return EQUIP_ERR_LOOT_GONE; + + // item it 'bind' + if (item->IsBindedNotWith(this)) + return EQUIP_ERR_NOT_OWNER; + + ItemTemplate const* pBagProto; + + // item is 'one item only' + InventoryResult res = CanTakeMoreSimilarItems(item, offendingItemId); + if (res != EQUIP_ERR_OK) + return res; + + // search stack for merge to + if (pProto->GetMaxStackSize() != 1) + { + bool b_found = false; + + for (int t = INVENTORY_SLOT_ITEM_START; t < INVENTORY_SLOT_ITEM_END; ++t) + { + item2 = GetItemByPos(INVENTORY_SLOT_BAG_0, t); + if (item2 && item2->CanBeMergedPartlyWith(pProto) == EQUIP_ERR_OK && inventoryCounts[t-INVENTORY_SLOT_ITEM_START] + item->GetCount() <= pProto->GetMaxStackSize()) + { + inventoryCounts[t-INVENTORY_SLOT_ITEM_START] += item->GetCount(); + b_found = true; + break; + } + } + if (b_found) + continue; + + for (int t = INVENTORY_SLOT_BAG_START; !b_found && t < INVENTORY_SLOT_BAG_END; ++t) + { + if (Bag* bag = GetBagByPos(t)) + { + if (ItemCanGoIntoBag(item->GetTemplate(), bag->GetTemplate())) + { + for (uint32 j = 0; j < bag->GetBagSize(); j++) + { + item2 = GetItemByPos(t, j); + if (item2 && item2->CanBeMergedPartlyWith(pProto) == EQUIP_ERR_OK && bagCounts[t-INVENTORY_SLOT_BAG_START][j] + item->GetCount() <= pProto->GetMaxStackSize()) + { + bagCounts[t-INVENTORY_SLOT_BAG_START][j] += item->GetCount(); + b_found = true; + break; + } + } + } + } + } + if (b_found) + continue; + } + + // special bag case + if (pProto->GetBagFamily()) + { + bool b_found = false; + + for (int t = INVENTORY_SLOT_BAG_START; !b_found && t < INVENTORY_SLOT_BAG_END; ++t) + { + if (Bag* bag = GetBagByPos(t)) + { + pBagProto = bag->GetTemplate(); + + // not plain container check + if (pBagProto && (pBagProto->GetClass() != ITEM_CLASS_CONTAINER || pBagProto->GetSubClass() != ITEM_SUBCLASS_CONTAINER) && + ItemCanGoIntoBag(pProto, pBagProto)) + { + for (uint32 j = 0; j < bag->GetBagSize(); j++) + { + if (bagCounts[t-INVENTORY_SLOT_BAG_START][j] == 0) + { + bagCounts[t-INVENTORY_SLOT_BAG_START][j] = 1; + b_found = true; + break; + } + } + } + } + } + if (b_found) + continue; + } + + // search free slot + bool b_found = false; + for (int t = INVENTORY_SLOT_ITEM_START; t < INVENTORY_SLOT_ITEM_END; ++t) + { + if (inventoryCounts[t-INVENTORY_SLOT_ITEM_START] == 0) + { + inventoryCounts[t-INVENTORY_SLOT_ITEM_START] = 1; + b_found = true; + break; + } + } + if (b_found) + continue; + + // search free slot in bags + for (uint8 t = INVENTORY_SLOT_BAG_START; !b_found && t < INVENTORY_SLOT_BAG_END; ++t) + { + if (Bag* bag = GetBagByPos(t)) + { + pBagProto = bag->GetTemplate(); + + // special bag already checked + if (pBagProto && (pBagProto->GetClass() != ITEM_CLASS_CONTAINER || pBagProto->GetSubClass() != ITEM_SUBCLASS_CONTAINER)) + continue; + + for (uint32 j = 0; j < bag->GetBagSize(); j++) + { + if (bagCounts[t-INVENTORY_SLOT_BAG_START][j] == 0) + { + bagCounts[t-INVENTORY_SLOT_BAG_START][j] = 1; + b_found = true; + break; + } + } + } + } + + // no free slot found? + if (!b_found) + return EQUIP_ERR_BAG_FULL; + } + + return EQUIP_ERR_OK; +} + +////////////////////////////////////////////////////////////////////////// +InventoryResult Player::CanEquipNewItem(uint8 slot, uint16 &dest, uint32 item, bool swap) const +{ + dest = 0; + Item* pItem = Item::CreateItem(item, 1, this); + if (pItem) + { + InventoryResult result = CanEquipItem(slot, dest, pItem, swap); + delete pItem; + return result; + } + + return EQUIP_ERR_ITEM_NOT_FOUND; +} + +InventoryResult Player::CanEquipItem(uint8 slot, uint16 &dest, Item* pItem, bool swap, bool not_loading) const +{ + dest = 0; + if (pItem) + { + TC_LOG_DEBUG("entities.player.items", "Player::CanEquipItem: Player '%s' (%s), Slot: %u, Item: %u, Count: %u", + GetName().c_str(), GetGUID().ToString().c_str(), slot, pItem->GetEntry(), pItem->GetCount()); + ItemTemplate const* pProto = pItem->GetTemplate(); + if (pProto) + { + // item used + if (pItem->m_lootGenerated) + return EQUIP_ERR_LOOT_GONE; + + if (pItem->IsBindedNotWith(this)) + return EQUIP_ERR_NOT_OWNER; + + // check count of items (skip for auto move for same player from bank) + InventoryResult res = CanTakeMoreSimilarItems(pItem); + if (res != EQUIP_ERR_OK) + return res; + + // check this only in game + if (not_loading) + { + // May be here should be more stronger checks; STUNNED checked + // ROOT, CONFUSED, DISTRACTED, FLEEING this needs to be checked. + if (HasUnitState(UNIT_STATE_STUNNED)) + return EQUIP_ERR_GENERIC_STUNNED; + + if (IsCharmed()) + return EQUIP_ERR_CLIENT_LOCKED_OUT; // @todo is this the correct error? + + // do not allow equipping gear except weapons, offhands, projectiles, relics in + // - combat + // - in-progress arenas + if (!pProto->CanChangeEquipStateInCombat()) + { + if (IsInCombat()) + return EQUIP_ERR_NOT_IN_COMBAT; + + if (Battleground* bg = GetBattleground()) + if (bg->isArena() && bg->GetStatus() == STATUS_IN_PROGRESS) + return EQUIP_ERR_NOT_DURING_ARENA_MATCH; + } + + if (IsInCombat()&& (pProto->GetClass() == ITEM_CLASS_WEAPON || pProto->GetInventoryType() == INVTYPE_RELIC) && m_weaponChangeTimer != 0) + return EQUIP_ERR_CLIENT_LOCKED_OUT; // maybe exist better err + + if (Spell* currentGenericSpell = GetCurrentSpell(CURRENT_GENERIC_SPELL)) + if (!currentGenericSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ALLOW_EQUIP_WHILE_CASTING)) + return EQUIP_ERR_CLIENT_LOCKED_OUT; + + if (Spell* currentChanneledSpell = GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + if (!currentChanneledSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ALLOW_EQUIP_WHILE_CASTING)) + return EQUIP_ERR_CLIENT_LOCKED_OUT; + } + + uint8 eslot = FindEquipSlot(pProto, slot, swap); + if (eslot == NULL_SLOT) + return EQUIP_ERR_NOT_EQUIPPABLE; + + res = CanUseItem(pItem, not_loading); + if (res != EQUIP_ERR_OK) + return res; + + if (!swap && GetItemByPos(INVENTORY_SLOT_BAG_0, eslot)) + return EQUIP_ERR_NO_SLOT_AVAILABLE; + + // if we are swapping 2 equiped items, CanEquipUniqueItem check + // should ignore the item we are trying to swap, and not the + // destination item. CanEquipUniqueItem should ignore destination + // item only when we are swapping weapon from bag + uint8 ignore = uint8(NULL_SLOT); + switch (eslot) + { + case EQUIPMENT_SLOT_MAINHAND: + ignore = EQUIPMENT_SLOT_OFFHAND; + break; + case EQUIPMENT_SLOT_OFFHAND: + ignore = EQUIPMENT_SLOT_MAINHAND; + break; + case EQUIPMENT_SLOT_FINGER1: + ignore = EQUIPMENT_SLOT_FINGER2; + break; + case EQUIPMENT_SLOT_FINGER2: + ignore = EQUIPMENT_SLOT_FINGER1; + break; + case EQUIPMENT_SLOT_TRINKET1: + ignore = EQUIPMENT_SLOT_TRINKET2; + break; + case EQUIPMENT_SLOT_TRINKET2: + ignore = EQUIPMENT_SLOT_TRINKET1; + break; + } + + if (ignore == uint8(NULL_SLOT) || pItem != GetItemByPos(INVENTORY_SLOT_BAG_0, ignore)) + ignore = eslot; + + InventoryResult res2 = CanEquipUniqueItem(pItem, swap ? ignore : uint8(NULL_SLOT)); + if (res2 != EQUIP_ERR_OK) + return res2; + + // check unique-equipped special item classes + if (pProto->GetClass() == ITEM_CLASS_QUIVER) + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Item* pBag = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pBag != pItem) + if (ItemTemplate const* pBagProto = pBag->GetTemplate()) + if (pBagProto->GetClass() == pProto->GetClass() && (!swap || pBag->GetSlot() != eslot)) + return (pBagProto->GetSubClass() == ITEM_SUBCLASS_AMMO_POUCH) + ? EQUIP_ERR_ONLY_ONE_AMMO + : EQUIP_ERR_ONLY_ONE_QUIVER; + + uint32 type = pProto->GetInventoryType(); + + if (eslot == EQUIPMENT_SLOT_OFFHAND) + { + // Do not allow polearm to be equipped in the offhand (rare case for the only 1h polearm 41750) + if (type == INVTYPE_WEAPON && pProto->GetSubClass() == ITEM_SUBCLASS_WEAPON_POLEARM) + return EQUIP_ERR_2HSKILLNOTFOUND; + else if (type == INVTYPE_WEAPON || type == INVTYPE_WEAPONOFFHAND) + { + if (!CanDualWield()) + return EQUIP_ERR_2HSKILLNOTFOUND; + } + else if (type == INVTYPE_2HWEAPON) + { + if (!CanDualWield() || !CanTitanGrip()) + return EQUIP_ERR_2HSKILLNOTFOUND; + } + + if (IsTwoHandUsed()) + return EQUIP_ERR_2HANDED_EQUIPPED; + } + + // equip two-hand weapon case (with possible unequip 2 items) + if (type == INVTYPE_2HWEAPON) + { + if (eslot == EQUIPMENT_SLOT_OFFHAND) + { + if (!CanTitanGrip()) + return EQUIP_ERR_NOT_EQUIPPABLE; + } + else if (eslot != EQUIPMENT_SLOT_MAINHAND) + return EQUIP_ERR_NOT_EQUIPPABLE; + + if (!CanTitanGrip()) + { + // offhand item must can be stored in inventory for offhand item and it also must be unequipped + Item* offItem = GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + ItemPosCountVec off_dest; + if (offItem && (!not_loading || + CanUnequipItem(uint16(INVENTORY_SLOT_BAG_0) << 8 | EQUIPMENT_SLOT_OFFHAND, false) != EQUIP_ERR_OK || + CanStoreItem(NULL_BAG, NULL_SLOT, off_dest, offItem, false) != EQUIP_ERR_OK)) + return swap ? EQUIP_ERR_CANT_SWAP : EQUIP_ERR_INV_FULL; + } + } + dest = ((INVENTORY_SLOT_BAG_0 << 8) | eslot); + return EQUIP_ERR_OK; + } + } + + return !swap ? EQUIP_ERR_ITEM_NOT_FOUND : EQUIP_ERR_CANT_SWAP; +} + +InventoryResult Player::CanUnequipItem(uint16 pos, bool swap) const +{ + // Applied only to equipped items and bank bags + if (!IsEquipmentPos(pos) && !IsBagPos(pos)) + return EQUIP_ERR_OK; + + Item* pItem = GetItemByPos(pos); + + // Applied only to existing equipped item + if (!pItem) + return EQUIP_ERR_OK; + + TC_LOG_DEBUG("entities.player.items", "Player::CanUnequipItem: Player '%s' (%s), Slot: %u, Item: %u, Count: %u", + GetName().c_str(), GetGUID().ToString().c_str(), pos, pItem->GetEntry(), pItem->GetCount()); + + ItemTemplate const* pProto = pItem->GetTemplate(); + if (!pProto) + return EQUIP_ERR_ITEM_NOT_FOUND; + + // item used + if (pItem->m_lootGenerated) + return EQUIP_ERR_LOOT_GONE; + + if (IsCharmed()) + return EQUIP_ERR_CLIENT_LOCKED_OUT; // @todo is this the correct error? + + // do not allow unequipping gear except weapons, offhands, projectiles, relics in + // - combat + // - in-progress arenas + if (!pProto->CanChangeEquipStateInCombat()) + { + if (IsInCombat()) + return EQUIP_ERR_NOT_IN_COMBAT; + + if (Battleground* bg = GetBattleground()) + if (bg->isArena() && bg->GetStatus() == STATUS_IN_PROGRESS) + return EQUIP_ERR_NOT_DURING_ARENA_MATCH; + } + + if (!swap && pItem->IsNotEmptyBag()) + return EQUIP_ERR_DESTROY_NONEMPTY_BAG; + + return EQUIP_ERR_OK; +} + +InventoryResult Player::CanBankItem(uint8 bag, uint8 slot, ItemPosCountVec &dest, Item* pItem, bool swap, bool not_loading) const +{ + if (!pItem) + return swap ? EQUIP_ERR_CANT_SWAP : EQUIP_ERR_ITEM_NOT_FOUND; + + uint32 count = pItem->GetCount(); + + TC_LOG_DEBUG("entities.player.items", "Player::CanBankItem: Player '%s' (%s), Bag: %u, Slot: %u, Item: %u, Count: %u", + GetName().c_str(), GetGUID().ToString().c_str(), bag, slot, pItem->GetEntry(), pItem->GetCount()); + ItemTemplate const* pProto = pItem->GetTemplate(); + if (!pProto) + return swap ? EQUIP_ERR_CANT_SWAP : EQUIP_ERR_ITEM_NOT_FOUND; + + // item used + if (pItem->m_lootGenerated) + return EQUIP_ERR_LOOT_GONE; + + if (pItem->IsBindedNotWith(this)) + return EQUIP_ERR_NOT_OWNER; + + // Currency Tokenizer are not supposed to be swapped out of their hidden bag + if (pItem->IsCurrencyToken()) + { + TC_LOG_INFO("entities.player.cheat", "Possible hacking attempt: Player %s (%s) tried to move token [%s entry: %u] out of the currency bag!", + GetName().c_str(), GetGUID().ToString().c_str(), pItem->GetGUID().ToString().c_str(), pProto->GetId()); + return EQUIP_ERR_CANT_SWAP; + } + + // check count of items (skip for auto move for same player from bank) + InventoryResult res = CanTakeMoreSimilarItems(pItem); + if (res != EQUIP_ERR_OK) + return res; + + // in specific slot + if (bag != NULL_BAG && slot != NULL_SLOT) + { + if (slot >= BANK_SLOT_BAG_START && slot < BANK_SLOT_BAG_END) + { + if (!pItem->IsBag()) + return EQUIP_ERR_WRONG_SLOT; + + if (slot - BANK_SLOT_BAG_START >= GetBankBagSlotCount()) + return EQUIP_ERR_NO_BANK_SLOT; + + res = CanUseItem(pItem, not_loading); + if (res != EQUIP_ERR_OK) + return res; + } + + res = CanStoreItem_InSpecificSlot(bag, slot, dest, pProto, count, swap, pItem); + if (res != EQUIP_ERR_OK) + return res; + + if (count == 0) + return EQUIP_ERR_OK; + } + + // not specific slot or have space for partly store only in specific slot + + // in specific bag + if (bag != NULL_BAG) + { + if (pItem->IsNotEmptyBag()) + return EQUIP_ERR_BAG_IN_BAG; + + // search stack in bag for merge to + if (pProto->GetMaxStackSize() != 1) + { + if (bag == INVENTORY_SLOT_BAG_0) + { + res = CanStoreItem_InInventorySlots(BANK_SLOT_ITEM_START, BANK_SLOT_ITEM_END, dest, pProto, count, true, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + return res; + + if (count == 0) + return EQUIP_ERR_OK; + } + else + { + res = CanStoreItem_InBag(bag, dest, pProto, count, true, false, pItem, NULL_BAG, slot); + if (res != EQUIP_ERR_OK) + res = CanStoreItem_InBag(bag, dest, pProto, count, true, true, pItem, NULL_BAG, slot); + + if (res != EQUIP_ERR_OK) + return res; + + if (count == 0) + return EQUIP_ERR_OK; + } + } + + // search free slot in bag + if (bag == INVENTORY_SLOT_BAG_0) + { + res = CanStoreItem_InInventorySlots(BANK_SLOT_ITEM_START, BANK_SLOT_ITEM_END, dest, pProto, count, false, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + return res; + + if (count == 0) + return EQUIP_ERR_OK; + } + else + { + res = CanStoreItem_InBag(bag, dest, pProto, count, false, false, pItem, NULL_BAG, slot); + if (res != EQUIP_ERR_OK) + res = CanStoreItem_InBag(bag, dest, pProto, count, false, true, pItem, NULL_BAG, slot); + + if (res != EQUIP_ERR_OK) + return res; + + if (count == 0) + return EQUIP_ERR_OK; + } + } + + // not specific bag or have space for partly store only in specific bag + + // search stack for merge to + if (pProto->GetMaxStackSize() != 1) + { + // in slots + res = CanStoreItem_InInventorySlots(BANK_SLOT_ITEM_START, BANK_SLOT_ITEM_END, dest, pProto, count, true, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + return res; + + if (count == 0) + return EQUIP_ERR_OK; + + // in special bags + if (pProto->GetBagFamily()) + { + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; i++) + { + res = CanStoreItem_InBag(i, dest, pProto, count, true, false, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + continue; + + if (count == 0) + return EQUIP_ERR_OK; + } + } + + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; i++) + { + res = CanStoreItem_InBag(i, dest, pProto, count, true, true, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + continue; + + if (count == 0) + return EQUIP_ERR_OK; + } + } + + // search free place in special bag + if (pProto->GetBagFamily()) + { + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; i++) + { + res = CanStoreItem_InBag(i, dest, pProto, count, false, false, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + continue; + + if (count == 0) + return EQUIP_ERR_OK; + } + } + + // search free space + res = CanStoreItem_InInventorySlots(BANK_SLOT_ITEM_START, BANK_SLOT_ITEM_END, dest, pProto, count, false, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + return res; + + if (count == 0) + return EQUIP_ERR_OK; + + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; i++) + { + res = CanStoreItem_InBag(i, dest, pProto, count, false, true, pItem, bag, slot); + if (res != EQUIP_ERR_OK) + continue; + + if (count == 0) + return EQUIP_ERR_OK; + } + return EQUIP_ERR_BANK_FULL; +} + +InventoryResult Player::CanUseItem(Item* pItem, bool not_loading) const +{ + if (pItem) + { + TC_LOG_DEBUG("entities.player.items", "Player::CanUseItem: Player '%s' (%s), Item: %u", + GetName().c_str(), GetGUID().ToString().c_str(), pItem->GetEntry()); + + if (!IsAlive() && not_loading) + return EQUIP_ERR_PLAYER_DEAD; + + //if (isStunned()) + // return EQUIP_ERR_GENERIC_STUNNED; + + ItemTemplate const* pProto = pItem->GetTemplate(); + if (pProto) + { + if (pItem->IsBindedNotWith(this)) + return EQUIP_ERR_NOT_OWNER; + + InventoryResult res = CanUseItem(pProto); + if (res != EQUIP_ERR_OK) + return res; + + if (pItem->GetSkill() != 0) + { + bool allowEquip = false; + uint32 itemSkill = pItem->GetSkill(); + // Armor that is binded to account can "morph" from plate to mail, etc. if skill is not learned yet. + if (pProto->GetQuality() == ITEM_QUALITY_HEIRLOOM && pProto->GetClass() == ITEM_CLASS_ARMOR && !HasSkill(itemSkill)) + { + /// @todo when you right-click already equipped item it throws EQUIP_ERR_PROFICIENCY_NEEDED. + // In fact it's a visual bug, everything works properly... I need sniffs of operations with + // binded to account items from off server. + + switch (getClass()) + { + case CLASS_HUNTER: + case CLASS_SHAMAN: + allowEquip = (itemSkill == SKILL_MAIL); + break; + case CLASS_PALADIN: + case CLASS_WARRIOR: + allowEquip = (itemSkill == SKILL_PLATE_MAIL); + break; + } + } + if (!allowEquip && GetSkillValue(itemSkill) == 0) + return EQUIP_ERR_PROFICIENCY_NEEDED; + } + + if (pProto->GetRequiredReputationFaction() && uint32(GetReputationRank(pProto->GetRequiredReputationFaction())) < pProto->GetRequiredReputationRank()) + return EQUIP_ERR_CANT_EQUIP_REPUTATION; + + return EQUIP_ERR_OK; + } + } + return EQUIP_ERR_ITEM_NOT_FOUND; +} + +InventoryResult Player::CanUseItem(ItemTemplate const* proto) const +{ + // Used by group, function NeedBeforeGreed, to know if a prototype can be used by a player + + if (!proto) + return EQUIP_ERR_ITEM_NOT_FOUND; + + if (((proto->GetFlags2() & ITEM_FLAG2_FACTION_HORDE) && GetTeam() != HORDE) || + (((proto->GetFlags2() & ITEM_FLAG2_FACTION_ALLIANCE) && GetTeam() != ALLIANCE))) + return EQUIP_ERR_CANT_EQUIP_EVER; + + if ((proto->GetAllowableClass() & getClassMask()) == 0 || (proto->GetAllowableRace() & getRaceMask()) == 0) + return EQUIP_ERR_CANT_EQUIP_EVER; + + if (proto->GetRequiredSkill() != 0) + { + if (GetSkillValue(proto->GetRequiredSkill()) == 0) + return EQUIP_ERR_PROFICIENCY_NEEDED; + else if (GetSkillValue(proto->GetRequiredSkill()) < proto->GetRequiredSkillRank()) + return EQUIP_ERR_CANT_EQUIP_SKILL; + } + + if (proto->GetRequiredSpell() != 0 && !HasSpell(proto->GetRequiredSpell())) + return EQUIP_ERR_PROFICIENCY_NEEDED; + + if (getLevel() < proto->GetRequiredLevel()) + return EQUIP_ERR_CANT_EQUIP_LEVEL_I; + + // If World Event is not active, prevent using event dependant items + if (proto->GetHolidayID() && !IsHolidayActive((HolidayIds)proto->GetHolidayID())) + return EQUIP_ERR_CLIENT_LOCKED_OUT; + + // learning (recipes, mounts, pets, etc.) + if (proto->Effects[0].SpellID == 483 || proto->Effects[0].SpellID == 55884) + if (HasSpell(proto->Effects[1].SpellID)) + return EQUIP_ERR_INTERNAL_BAG_ERROR; + + return EQUIP_ERR_OK; +} + +static constexpr std::array ItemWeaponSkills = +{ + SKILL_AXES, SKILL_2H_AXES, SKILL_BOWS, SKILL_GUNS, SKILL_MACES, + SKILL_2H_MACES, SKILL_POLEARMS, SKILL_SWORDS, SKILL_2H_SWORDS, 0, + SKILL_STAVES, 0, 0, SKILL_FIST_WEAPONS, 0, + SKILL_DAGGERS, SKILL_THROWN, SKILL_ASSASSINATION, SKILL_CROSSBOWS, SKILL_WANDS, + SKILL_FISHING +}; + +// Patch 3.3.0 applied additional rules to ensure quality. We will go over them now. +bool Player::CanRollNeedForItem(ItemTemplate const* itemTemplate) const +{ + if (!GetGroup()) + return true; + + if (!itemTemplate) + return false; + + // Race and class requirement checks + if ((itemTemplate->GetAllowableClass() & getClassMask()) == 0 || (itemTemplate->GetAllowableRace() & getRaceMask()) == 0) + return false; + + // Spell requirement check + if (itemTemplate->GetRequiredSpell() != 0 && !HasSpell(itemTemplate->GetRequiredSpell())) + return false; + + // Skill requirement check + if (itemTemplate->GetRequiredSkill() != 0 && (!GetSkillValue(itemTemplate->GetRequiredSkill()) || GetSkillValue(itemTemplate->GetRequiredSkill()) < itemTemplate->GetRequiredSkillRank())) + return false; + + // Profiency check + if (itemTemplate->GetClass() == ITEM_CLASS_WEAPON && GetSkillValue(ItemWeaponSkills[itemTemplate->GetSubClass()]) == 0) + return false; + + // Armor type checks + if (itemTemplate->GetClass() == ITEM_CLASS_ARMOR + && itemTemplate->GetSubClass() > ITEM_SUBCLASS_ARMOR_MISCELLANEOUS + && itemTemplate->GetSubClass() < ITEM_SUBCLASS_ARMOR_BUCKLER + && itemTemplate->GetInventoryType() != INVTYPE_CLOAK) + { + switch (getClass()) + { + // Paladins, Warriors and Death Knights can no longer roll for mail when >= level 40 and can only roll for plate + case CLASS_WARRIOR: + case CLASS_DEATH_KNIGHT: + case CLASS_PALADIN: + if (getLevel() < 40 && itemTemplate->GetSubClass() != ITEM_SUBCLASS_ARMOR_MAIL) + return false; + else if (itemTemplate->GetSubClass() != ITEM_SUBCLASS_ARMOR_PLATE) + return false; + break; + // Hunter and Shamans can no longer roll for leather when >= level 40 and can only roll for mail + case CLASS_HUNTER: + case CLASS_SHAMAN: + if (getLevel() < 40 && itemTemplate->GetSubClass() != ITEM_SUBCLASS_ARMOR_LEATHER) + return false; + else if (itemTemplate->GetSubClass() != ITEM_SUBCLASS_ARMOR_MAIL) + return false; + break; + case CLASS_DRUID: + case CLASS_ROGUE: + if (itemTemplate->GetSubClass() != ITEM_SUBCLASS_ARMOR_LEATHER) + return false; + break; + case CLASS_MAGE: + case CLASS_PRIEST: + case CLASS_WARLOCK: + if (itemTemplate->GetSubClass() != ITEM_SUBCLASS_ARMOR_CLOTH) + return false; + break; + default: + break; + } + } + + // Mount and companion checks. Players that already have these cannot roll need again. + if (itemTemplate->Effects[0].SpellID == 55884) + if (HasSpell(itemTemplate->Effects[1].SpellID)) + return false; + + return true; +} + +// Return stored item (if stored to stack, it can diff. from pItem). And pItem ca be deleted in this case. +Item* Player::StoreNewItem(ItemPosCountVec const& dest, uint32 item, bool update, ItemRandomEnchantmentId const& randomPropertyId /*= {}*/, GuidSet const& allowedLooters) +{ + uint32 count = 0; + for (ItemPosCountVec::const_iterator itr = dest.begin(); itr != dest.end(); ++itr) + count += itr->count; + + Item* pItem = Item::CreateItem(item, count, this); + if (pItem) + { + ItemAddedQuestCheck(item, count); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_RECEIVE_EPIC_ITEM, item, count); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM, item, count); + pItem->SetItemRandomProperties(randomPropertyId); + pItem = StoreItem(dest, pItem, update); + + if (allowedLooters.size() > 1 && pItem->GetTemplate()->GetMaxStackSize() == 1 && pItem->IsSoulBound()) + { + pItem->SetSoulboundTradeable(allowedLooters); + pItem->SetUInt32Value(ITEM_FIELD_CREATE_PLAYED_TIME, GetTotalPlayedTime()); + AddTradeableItem(pItem); + + // save data + std::ostringstream ss; + GuidSet::const_iterator itr = allowedLooters.begin(); + ss << *itr; + for (++itr; itr != allowedLooters.end(); ++itr) + ss << ' ' << itr->GetCounter(); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_ITEM_BOP_TRADE); + stmt->setUInt32(0, pItem->GetGUID().GetCounter()); + stmt->setString(1, ss.str()); + CharacterDatabase.Execute(stmt); + } + } + return pItem; +} + +Item* Player::StoreItem(ItemPosCountVec const& dest, Item* pItem, bool update) +{ + if (!pItem) + return nullptr; + + Item* lastItem = pItem; + for (ItemPosCountVec::const_iterator itr = dest.begin(); itr != dest.end();) + { + uint16 pos = itr->pos; + uint32 count = itr->count; + + ++itr; + + if (itr == dest.end()) + { + lastItem = _StoreItem(pos, pItem, count, false, update); + break; + } + + lastItem = _StoreItem(pos, pItem, count, true, update); + } + return lastItem; +} + +// Return stored item (if stored to stack, it can diff. from pItem). And pItem ca be deleted in this case. +Item* Player::_StoreItem(uint16 pos, Item* pItem, uint32 count, bool clone, bool update) +{ + if (!pItem) + return nullptr; + + uint8 bag = pos >> 8; + uint8 slot = pos & 255; + + TC_LOG_DEBUG("entities.player.items", "Player::_StoreItem: Player '%s' (%s), Bag: %u, Slot: %u, Item: %u (%s), Count: %u", + GetName().c_str(), GetGUID().ToString().c_str(), bag, slot, pItem->GetEntry(), pItem->GetGUID().ToString().c_str(), count); + + Item* pItem2 = GetItemByPos(bag, slot); + + if (!pItem2) + { + if (clone) + pItem = pItem->CloneItem(count, this); + else + pItem->SetCount(count); + + if (!pItem) + return nullptr; + + if (pItem->GetTemplate()->GetBonding() == BIND_ON_ACQUIRE || + pItem->GetTemplate()->GetBonding() == BIND_QUEST || + (pItem->GetTemplate()->GetBonding() == BIND_ON_EQUIP && IsBagPos(pos))) + pItem->SetBinding(true); + + Bag* pBag = (bag == INVENTORY_SLOT_BAG_0) ? nullptr : GetBagByPos(bag); + if (!pBag) + { + m_items[slot] = pItem; + SetGuidValue(PLAYER_FIELD_INV_SLOT_HEAD + (slot * 2), pItem->GetGUID()); + pItem->SetGuidValue(ITEM_FIELD_CONTAINED, GetGUID()); + pItem->SetGuidValue(ITEM_FIELD_OWNER, GetGUID()); + + pItem->SetSlot(slot); + pItem->SetContainer(nullptr); + } + else + pBag->StoreItem(slot, pItem, update); + + if (IsInWorld() && update) + { + pItem->AddToWorld(); + pItem->SendUpdateToPlayer(this); + } + + pItem->SetState(ITEM_CHANGED, this); + if (pBag) + pBag->SetState(ITEM_CHANGED, this); + + AddEnchantmentDurations(pItem); + AddItemDurations(pItem); + + if (bag == INVENTORY_SLOT_BAG_0 || (bag >= INVENTORY_SLOT_BAG_START && bag < INVENTORY_SLOT_BAG_END)) + ApplyItemObtainSpells(pItem, true); + + return pItem; + } + else + { + if (pItem2->GetTemplate()->GetBonding() == BIND_ON_ACQUIRE || + pItem2->GetTemplate()->GetBonding() == BIND_QUEST || + (pItem2->GetTemplate()->GetBonding() == BIND_ON_EQUIP && IsBagPos(pos))) + pItem2->SetBinding(true); + + pItem2->SetCount(pItem2->GetCount() + count); + if (IsInWorld() && update) + pItem2->SendUpdateToPlayer(this); + + if (!clone) + { + // delete item (it not in any slot currently) + if (IsInWorld() && update) + { + pItem->RemoveFromWorld(); + pItem->DestroyForPlayer(this); + } + + RemoveEnchantmentDurations(pItem); + RemoveItemDurations(pItem); + + pItem->SetOwnerGUID(GetGUID()); // prevent error at next SetState in case trade/mail/buy from vendor + pItem->SetNotRefundable(this); + pItem->ClearSoulboundTradeable(this); + RemoveTradeableItem(pItem); + pItem->SetState(ITEM_REMOVED, this); + } + + AddEnchantmentDurations(pItem2); + + pItem2->SetState(ITEM_CHANGED, this); + + if (bag == INVENTORY_SLOT_BAG_0 || (bag >= INVENTORY_SLOT_BAG_START && bag < INVENTORY_SLOT_BAG_END)) + ApplyItemObtainSpells(pItem2, true); + + return pItem2; + } +} + +Item* Player::EquipNewItem(uint16 pos, uint32 item, bool update) +{ + if (Item* pItem = Item::CreateItem(item, 1, this)) + { + ItemAddedQuestCheck(item, 1); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_RECEIVE_EPIC_ITEM, item, 1); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM, item, 1); + return EquipItem(pos, pItem, update); + } + + return nullptr; +} + +Item* Player::EquipItem(uint16 pos, Item* pItem, bool update) +{ + AddEnchantmentDurations(pItem); + AddItemDurations(pItem); + + uint8 bag = pos >> 8; + uint8 slot = pos & 255; + + Item* pItem2 = GetItemByPos(bag, slot); + + if (!pItem2) + { + VisualizeItem(slot, pItem); + + if (IsAlive()) + { + ItemTemplate const* pProto = pItem->GetTemplate(); + + // item set bonuses applied only at equip and removed at unequip, and still active for broken items + if (pProto && pProto->GetItemSet()) + AddItemsSetItem(this, pItem); + + _ApplyItemMods(pItem, slot, true); + + if (pProto && IsInCombat() && (pProto->GetClass() == ITEM_CLASS_WEAPON || pProto->GetInventoryType() == INVTYPE_RELIC) && m_weaponChangeTimer == 0) + { + uint32 cooldownSpell = getClass() == CLASS_ROGUE ? 6123 : 6119; + SpellInfo const* spellProto = sSpellMgr->GetSpellInfo(cooldownSpell); + + if (!spellProto) + TC_LOG_ERROR("entities.player", "Player::EquipItem: Weapon switch cooldown spell %u for player '%s' (%s) couldn't be found in Spell.dbc", + cooldownSpell, GetName().c_str(), GetGUID().ToString().c_str()); + else + { + m_weaponChangeTimer = spellProto->StartRecoveryTime; + + GetSpellHistory()->AddGlobalCooldown(spellProto, m_weaponChangeTimer); + WorldPacket data; + GetSpellHistory()->BuildCooldownPacket(data, SPELL_COOLDOWN_FLAG_INCLUDE_GCD, cooldownSpell, 0); + SendDirectMessage(&data); + } + } + } + + if (IsInWorld() && update) + { + pItem->AddToWorld(); + pItem->SendUpdateToPlayer(this); + } + + ApplyEquipCooldown(pItem); + + // update expertise and armor penetration - passive auras may need it + + if (slot == EQUIPMENT_SLOT_MAINHAND) + UpdateExpertise(BASE_ATTACK); + + else if (slot == EQUIPMENT_SLOT_OFFHAND) + UpdateExpertise(OFF_ATTACK); + + UpdateArmorSpecialization(); + } + else + { + pItem2->SetCount(pItem2->GetCount() + pItem->GetCount()); + if (IsInWorld() && update) + pItem2->SendUpdateToPlayer(this); + + // delete item (it not in any slot currently) + //pItem->DeleteFromDB(); + if (IsInWorld() && update) + { + pItem->RemoveFromWorld(); + pItem->DestroyForPlayer(this); + } + + RemoveEnchantmentDurations(pItem); + RemoveItemDurations(pItem); + + pItem->SetOwnerGUID(GetGUID()); // prevent error at next SetState in case trade/mail/buy from vendor + pItem->SetNotRefundable(this); + pItem->ClearSoulboundTradeable(this); + RemoveTradeableItem(pItem); + pItem->SetState(ITEM_REMOVED, this); + pItem2->SetState(ITEM_CHANGED, this); + + ApplyEquipCooldown(pItem2); + + UpdateArmorSpecialization(); + + return pItem2; + } + + if (slot == EQUIPMENT_SLOT_MAINHAND || slot == EQUIPMENT_SLOT_OFFHAND) + CheckTitanGripPenalty(); + + // only for full equip instead adding to stack + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM, pItem->GetEntry()); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_EQUIP_EPIC_ITEM, slot, pItem->GetEntry()); + if (Guild* guild = GetGuild()) + guild->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_OWN_ITEM, pItem->GetEntry(), 1, 0, nullptr, this); + + return pItem; +} + +void Player::QuickEquipItem(uint16 pos, Item* pItem) +{ + if (pItem) + { + AddEnchantmentDurations(pItem); + AddItemDurations(pItem); + + uint8 slot = pos & 255; + VisualizeItem(slot, pItem); + + if (IsInWorld()) + { + pItem->AddToWorld(); + pItem->SendUpdateToPlayer(this); + } + + if (slot == EQUIPMENT_SLOT_MAINHAND || slot == EQUIPMENT_SLOT_OFFHAND) + CheckTitanGripPenalty(); + + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM, pItem->GetEntry()); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_EQUIP_EPIC_ITEM, slot, pItem->GetEntry()); + } +} + +void Player::SetVisibleItemSlot(uint8 slot, Item* pItem) +{ + if (pItem) + { + SetUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + (slot * 2), pItem->GetVisibleEntry()); + SetUInt16Value(PLAYER_VISIBLE_ITEM_1_ENCHANTMENT + (slot * 2), 0, pItem->GetEnchantmentId(PERM_ENCHANTMENT_SLOT)); + SetUInt16Value(PLAYER_VISIBLE_ITEM_1_ENCHANTMENT + (slot * 2), 1, pItem->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)); + } + else + { + SetUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + (slot * 2), 0); + SetUInt32Value(PLAYER_VISIBLE_ITEM_1_ENCHANTMENT + (slot * 2), 0); + } +} + +void Player::VisualizeItem(uint8 slot, Item* pItem) +{ + if (!pItem) + return; + + // check also BIND_ON_ACQUIRE and BIND_QUEST for .additem or .additemset case by GM (not binded at adding to inventory) + if (pItem->GetTemplate()->GetBonding() == BIND_ON_EQUIP || pItem->GetTemplate()->GetBonding() == BIND_ON_ACQUIRE || pItem->GetTemplate()->GetBonding() == BIND_QUEST) + pItem->SetBinding(true); + + TC_LOG_DEBUG("entities.player.items", "Player::SetVisibleItemSlot: Player '%s' (%s), Slot: %u, Item: %u", + GetName().c_str(), GetGUID().ToString().c_str(), slot, pItem->GetEntry()); + + m_items[slot] = pItem; + SetGuidValue(PLAYER_FIELD_INV_SLOT_HEAD + (slot * 2), pItem->GetGUID()); + pItem->SetGuidValue(ITEM_FIELD_CONTAINED, GetGUID()); + pItem->SetGuidValue(ITEM_FIELD_OWNER, GetGUID()); + pItem->SetSlot(slot); + pItem->SetContainer(nullptr); + + if (slot < EQUIPMENT_SLOT_END) + SetVisibleItemSlot(slot, pItem); + + pItem->SetState(ITEM_CHANGED, this); +} + +Item* Player::BankItem(ItemPosCountVec const& dest, Item* pItem, bool update) +{ + return StoreItem(dest, pItem, update); +} + +void Player::RemoveItem(uint8 bag, uint8 slot, bool update) +{ + // note: removeitem does not actually change the item + // it only takes the item out of storage temporarily + // note2: if removeitem is to be used for delinking + // the item must be removed from the player's updatequeue + + Item* pItem = GetItemByPos(bag, slot); + if (pItem) + { + TC_LOG_DEBUG("entities.player.items", "Player::RemoveItem: Player '%s' (%s), Bag: %u, Slot: %u, Item: %u", + GetName().c_str(), GetGUID().ToString().c_str(), bag, slot, pItem->GetEntry()); + + RemoveEnchantmentDurations(pItem); + RemoveItemDurations(pItem); + RemoveTradeableItem(pItem); + + if (bag == INVENTORY_SLOT_BAG_0) + { + if (slot < INVENTORY_SLOT_BAG_END) + { + // item set bonuses applied only at equip and removed at unequip, and still active for broken items + ItemTemplate const* pProto = ASSERT_NOTNULL(pItem->GetTemplate()); + if (pProto->GetItemSet()) + RemoveItemsSetItem(this, pProto); + + _ApplyItemMods(pItem, slot, false, update); + + if (slot == EQUIPMENT_SLOT_MAINHAND || slot == EQUIPMENT_SLOT_OFFHAND) + UpdateExpertise(OFF_ATTACK); + } + + m_items[slot] = nullptr; + SetGuidValue(PLAYER_FIELD_INV_SLOT_HEAD + (slot * 2), ObjectGuid::Empty); + + if (slot < EQUIPMENT_SLOT_END) + { + SetVisibleItemSlot(slot, nullptr); + if (slot == EQUIPMENT_SLOT_MAINHAND || slot == EQUIPMENT_SLOT_OFFHAND) + CheckTitanGripPenalty(); + + if (slot == EQUIPMENT_SLOT_MAINHAND) + { + // clear main hand only enchantments + for (uint32 enchantSlot = 0; enchantSlot < MAX_ENCHANTMENT_SLOT; ++enchantSlot) + if (SpellItemEnchantmentEntry const* enchantment = sSpellItemEnchantmentStore.LookupEntry(pItem->GetEnchantmentId(EnchantmentSlot(enchantSlot)))) + if (enchantment->GetFlags().HasFlag(SpellItemEnchantmentFlags::MainhandOnly)) + pItem->ClearEnchantment(EnchantmentSlot(enchantSlot)); + } + } + } + else if (Bag* pBag = GetBagByPos(bag)) + pBag->RemoveItem(slot, update); + + pItem->SetGuidValue(ITEM_FIELD_CONTAINED, ObjectGuid::Empty); + // pItem->SetUInt64Value(ITEM_FIELD_OWNER, 0); not clear owner at remove (it will be set at store). This used in mail and auction code + pItem->SetSlot(NULL_SLOT); + UpdateArmorSpecialization(); + if (IsInWorld() && update) + pItem->SendUpdateToPlayer(this); + } +} + +// Common operation need to remove item from inventory without delete in trade, auction, guild bank, mail.... +void Player::MoveItemFromInventory(uint8 bag, uint8 slot, bool update) +{ + if (Item* it = GetItemByPos(bag, slot)) + { + ItemRemovedQuestCheck(it->GetEntry(), it->GetCount()); + RemoveItem(bag, slot, update); + it->SetNotRefundable(this, false); + RemoveItemFromUpdateQueueOf(it, this); + if (it->IsInWorld()) + { + it->RemoveFromWorld(); + it->DestroyForPlayer(this); + } + } +} + +// Common operation need to add item from inventory without delete in trade, guild bank, mail.... +void Player::MoveItemToInventory(ItemPosCountVec const& dest, Item* pItem, bool update, bool in_characterInventoryDB) +{ + // update quest counters + ItemAddedQuestCheck(pItem->GetEntry(), pItem->GetCount()); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_RECEIVE_EPIC_ITEM, pItem->GetEntry(), pItem->GetCount()); + + // store item + Item* pLastItem = StoreItem(dest, pItem, update); + + // only set if not merged to existing stack (pItem can be deleted already but we can compare pointers any way) + if (pLastItem == pItem) + { + // update owner for last item (this can be original item with wrong owner + if (pLastItem->GetOwnerGUID() != GetGUID()) + pLastItem->SetOwnerGUID(GetGUID()); + + // if this original item then it need create record in inventory + // in case trade we already have item in other player inventory + pLastItem->SetState(in_characterInventoryDB ? ITEM_CHANGED : ITEM_NEW, this); + + if (pLastItem->HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE)) + AddTradeableItem(pLastItem); + } +} + +void Player::DestroyItem(uint8 bag, uint8 slot, bool update) +{ + Item* pItem = GetItemByPos(bag, slot); + if (pItem) + { + TC_LOG_DEBUG("entities.player.items", "Player::DestroyItem: Player '%s' (%s), Bag: %u, Slot: %u, Item: %u", + GetName().c_str(), GetGUID().ToString().c_str(), bag, slot, pItem->GetEntry()); + // Also remove all contained items if the item is a bag. + // This if () prevents item saving crashes if the condition for a bag to be empty before being destroyed was bypassed somehow. + if (pItem->IsNotEmptyBag()) + for (uint8 i = 0; i < MAX_BAG_SIZE; ++i) + DestroyItem(slot, i, update); + + if (pItem->HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_WRAPPED)) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_GIFT); + + stmt->setUInt32(0, pItem->GetGUID().GetCounter()); + + CharacterDatabase.Execute(stmt); + } + + RemoveEnchantmentDurations(pItem); + RemoveItemDurations(pItem); + + pItem->SetNotRefundable(this); + pItem->ClearSoulboundTradeable(this); + RemoveTradeableItem(pItem); + + ApplyItemObtainSpells(pItem, false); + + ItemRemovedQuestCheck(pItem->GetEntry(), pItem->GetCount()); + sScriptMgr->OnItemRemove(this, pItem); + + ItemTemplate const* pProto = pItem->GetTemplate(); + if (bag == INVENTORY_SLOT_BAG_0) + { + SetGuidValue(PLAYER_FIELD_INV_SLOT_HEAD + (slot * 2), ObjectGuid::Empty); + + // equipment and equipped bags can have applied bonuses + if (slot < INVENTORY_SLOT_BAG_END) + { + // item set bonuses applied only at equip and removed at unequip, and still active for broken items + if (pProto->GetItemSet()) + RemoveItemsSetItem(this, pProto); + + _ApplyItemMods(pItem, slot, false); + } + + if (slot < EQUIPMENT_SLOT_END) + { + if (slot == EQUIPMENT_SLOT_MAINHAND) + UpdateExpertise(BASE_ATTACK); + else if (slot == EQUIPMENT_SLOT_OFFHAND) + UpdateExpertise(OFF_ATTACK); + + // equipment visual show + SetVisibleItemSlot(slot, nullptr); + } + + m_items[slot] = nullptr; + } + else if (Bag* pBag = GetBagByPos(bag)) + pBag->RemoveItem(slot, update); + + // Delete rolled money / loot from db. + // MUST be done before RemoveFromWorld() or GetTemplate() fails + if (pProto->GetFlags() & ITEM_FLAG_HAS_LOOT) + sLootItemStorage->RemoveStoredLootForContainer(pItem->GetGUID().GetCounter()); + + if (IsInWorld() && update) + { + pItem->RemoveFromWorld(); + pItem->DestroyForPlayer(this); + } + + //pItem->SetOwnerGUID(0); + pItem->SetGuidValue(ITEM_FIELD_CONTAINED, ObjectGuid::Empty); + pItem->SetSlot(NULL_SLOT); + pItem->SetState(ITEM_REMOVED, this); + } +} + +void Player::DestroyItemCount(uint32 itemEntry, uint32 count, bool update, bool unequip_check) +{ + TC_LOG_DEBUG("entities.player.items", "Player::DestroyItemCount: Player '%s' (%s), Item: %u, Count: %u", + GetName().c_str(), GetGUID().ToString().c_str(), itemEntry, count); + uint32 remcount = 0; + + // in inventory + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) + { + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + { + if (item->GetEntry() == itemEntry && !item->IsInTrade()) + { + if (item->GetCount() + remcount <= count) + { + // all items in inventory can unequipped + remcount += item->GetCount(); + DestroyItem(INVENTORY_SLOT_BAG_0, i, update); + + if (remcount >= count) + return; + } + else + { + ItemRemovedQuestCheck(item->GetEntry(), count - remcount); + item->SetCount(item->GetCount() - count + remcount); + if (IsInWorld() && update) + item->SendUpdateToPlayer(this); + item->SetState(ITEM_CHANGED, this); + return; + } + } + } + } + + // in inventory bags + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + { + if (Bag* bag = GetBagByPos(i)) + { + for (uint32 j = 0; j < bag->GetBagSize(); j++) + { + if (Item* item = bag->GetItemByPos(j)) + { + if (item->GetEntry() == itemEntry && !item->IsInTrade()) + { + // all items in bags can be unequipped + if (item->GetCount() + remcount <= count) + { + remcount += item->GetCount(); + DestroyItem(i, j, update); + + if (remcount >= count) + return; + } + else + { + ItemRemovedQuestCheck(item->GetEntry(), count - remcount); + item->SetCount(item->GetCount() - count + remcount); + if (IsInWorld() && update) + item->SendUpdateToPlayer(this); + item->SetState(ITEM_CHANGED, this); + return; + } + } + } + } + } + } + + // in equipment and bag list + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_BAG_END; i++) + { + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + { + if (item->GetEntry() == itemEntry && !item->IsInTrade()) + { + if (item->GetCount() + remcount <= count) + { + if (!unequip_check || CanUnequipItem(INVENTORY_SLOT_BAG_0 << 8 | i, false) == EQUIP_ERR_OK) + { + remcount += item->GetCount(); + DestroyItem(INVENTORY_SLOT_BAG_0, i, update); + + if (remcount >= count) + return; + } + } + else + { + ItemRemovedQuestCheck(item->GetEntry(), count - remcount); + item->SetCount(item->GetCount() - count + remcount); + if (IsInWorld() && update) + item->SendUpdateToPlayer(this); + item->SetState(ITEM_CHANGED, this); + return; + } + } + } + } + + // in bank + for (uint8 i = BANK_SLOT_ITEM_START; i < BANK_SLOT_ITEM_END; i++) + { + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + { + if (item->GetEntry() == itemEntry && !item->IsInTrade()) + { + if (item->GetCount() + remcount <= count) + { + remcount += item->GetCount(); + DestroyItem(INVENTORY_SLOT_BAG_0, i, update); + if (remcount >= count) + return; + } + else + { + ItemRemovedQuestCheck(item->GetEntry(), count - remcount); + item->SetCount(item->GetCount() - count + remcount); + if (IsInWorld() && update) + item->SendUpdateToPlayer(this); + item->SetState(ITEM_CHANGED, this); + return; + } + } + } + } + + // in bank bags + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; i++) + { + if (Bag* bag = GetBagByPos(i)) + { + for (uint32 j = 0; j < bag->GetBagSize(); j++) + { + if (Item* item = bag->GetItemByPos(j)) + { + if (item->GetEntry() == itemEntry && !item->IsInTrade()) + { + // all items in bags can be unequipped + if (item->GetCount() + remcount <= count) + { + remcount += item->GetCount(); + DestroyItem(i, j, update); + + if (remcount >= count) + return; + } + else + { + ItemRemovedQuestCheck(item->GetEntry(), count - remcount); + item->SetCount(item->GetCount() - count + remcount); + if (IsInWorld() && update) + item->SendUpdateToPlayer(this); + item->SetState(ITEM_CHANGED, this); + return; + } + } + } + } + } + } + + // in bank bag list + for (uint8 i = BANK_SLOT_BAG_START; i < BANK_SLOT_BAG_END; i++) + { + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + { + if (item->GetEntry() == itemEntry && !item->IsInTrade()) + { + if (item->GetCount() + remcount <= count) + { + if (!unequip_check || CanUnequipItem(INVENTORY_SLOT_BAG_0 << 8 | i, false) == EQUIP_ERR_OK) + { + remcount += item->GetCount(); + DestroyItem(INVENTORY_SLOT_BAG_0, i, update); + if (remcount >= count) + return; + } + } + else + { + ItemRemovedQuestCheck(item->GetEntry(), count - remcount); + item->SetCount(item->GetCount() - count + remcount); + if (IsInWorld() && update) + item->SendUpdateToPlayer(this); + item->SetState(ITEM_CHANGED, this); + return; + } + } + } + } +} + +void Player::DestroyZoneLimitedItem(bool update, uint32 new_zone) +{ + TC_LOG_DEBUG("entities.player.items", "Player::DestroyZoneLimitedItem: In map %u and area %u for player '%s' (%s)", + GetMapId(), new_zone, GetName().c_str(), GetGUID().ToString().c_str()); + + // in inventory + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->IsLimitedToAnotherMapOrZone(GetMapId(), new_zone)) + DestroyItem(INVENTORY_SLOT_BAG_0, i, update); + + // in inventory bags + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + if (Item* pItem = pBag->GetItemByPos(j)) + if (pItem->IsLimitedToAnotherMapOrZone(GetMapId(), new_zone)) + DestroyItem(i, j, update); + + // in equipment and bag list + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_BAG_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->IsLimitedToAnotherMapOrZone(GetMapId(), new_zone)) + DestroyItem(INVENTORY_SLOT_BAG_0, i, update); +} + +void Player::DestroyConjuredItems(bool update) +{ + // used when entering arena + // destroys all conjured items + TC_LOG_DEBUG("entities.player.items", "Player::DestroyConjuredItems: Player '%s' (%s)", + GetName().c_str(), GetGUID().ToString().c_str()); + + // in inventory + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->IsConjuredConsumable()) + DestroyItem(INVENTORY_SLOT_BAG_0, i, update); + + // in inventory bags + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; i++) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + if (Item* pItem = pBag->GetItemByPos(j)) + if (pItem->IsConjuredConsumable()) + DestroyItem(i, j, update); + + // in equipment and bag list + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_BAG_END; i++) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->IsConjuredConsumable()) + DestroyItem(INVENTORY_SLOT_BAG_0, i, update); +} + +Item* Player::GetItemByEntry(uint32 entry) const +{ + // in inventory + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->GetEntry() == entry) + return pItem; + + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); ++j) + if (Item* pItem = pBag->GetItemByPos(j)) + if (pItem->GetEntry() == entry) + return pItem; + + for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (pItem->GetEntry() == entry) + return pItem; + + return nullptr; +} + +void Player::DestroyItemCount(Item* pItem, uint32 &count, bool update) +{ + if (!pItem) + return; + + TC_LOG_DEBUG("entities.player.items", "Player::DestroyItemCount: Player '%s' (%s), Item (%s, Entry: %u), Count: %u", + GetName().c_str(), GetGUID().ToString().c_str(), pItem->GetGUID().ToString().c_str(), pItem->GetEntry(), count); + + if (pItem->GetCount() <= count) + { + count -= pItem->GetCount(); + + DestroyItem(pItem->GetBagSlot(), pItem->GetSlot(), update); + } + else + { + ItemRemovedQuestCheck(pItem->GetEntry(), count); + pItem->SetCount(pItem->GetCount() - count); + count = 0; + if (IsInWorld() && update) + pItem->SendUpdateToPlayer(this); + pItem->SetState(ITEM_CHANGED, this); + } +} + +void Player::SplitItem(uint16 src, uint16 dst, uint32 count) +{ + uint8 srcbag = src >> 8; + uint8 srcslot = src & 255; + + uint8 dstbag = dst >> 8; + uint8 dstslot = dst & 255; + + Item* pSrcItem = GetItemByPos(srcbag, srcslot); + if (!pSrcItem) + { + SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, pSrcItem, nullptr); + return; + } + + if (pSrcItem->m_lootGenerated) // prevent split looting item (item + { + //best error message found for attempting to split while looting + SendEquipError(EQUIP_ERR_SPLIT_FAILED, pSrcItem, nullptr); + return; + } + + // not let split all items (can be only at cheating) + if (pSrcItem->GetCount() == count) + { + SendEquipError(EQUIP_ERR_SPLIT_FAILED, pSrcItem, nullptr); + return; + } + + // not let split more existing items (can be only at cheating) + if (pSrcItem->GetCount() < count) + { + SendEquipError(EQUIP_ERR_TOO_FEW_TO_SPLIT, pSrcItem, nullptr); + return; + } + + //! If trading + if (TradeData* tradeData = GetTradeData()) + { + //! If current item is in trade window (only possible with packet spoofing - silent return) + if (tradeData->GetTradeSlotForItem(pSrcItem->GetGUID()) != TRADE_SLOT_INVALID) + return; + } + + TC_LOG_DEBUG("entities.player.items", "Player::SplitItem: Player '%s' (%s), Bag: %u, Slot: %u, Item: %u, Count: %u", + GetName().c_str(), GetGUID().ToString().c_str(), dstbag, dstslot, pSrcItem->GetEntry(), count); + Item* pNewItem = pSrcItem->CloneItem(count, this); + if (!pNewItem) + { + SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, pSrcItem, nullptr); + return; + } + + if (IsInventoryPos(dst)) + { + // change item amount before check (for unique max count check) + pSrcItem->SetCount(pSrcItem->GetCount() - count); + + ItemPosCountVec dest; + InventoryResult msg = CanStoreItem(dstbag, dstslot, dest, pNewItem, false); + if (msg != EQUIP_ERR_OK) + { + delete pNewItem; + pSrcItem->SetCount(pSrcItem->GetCount() + count); + SendEquipError(msg, pSrcItem, nullptr); + return; + } + + if (IsInWorld()) + pSrcItem->SendUpdateToPlayer(this); + pSrcItem->SetState(ITEM_CHANGED, this); + StoreItem(dest, pNewItem, true); + } + else if (IsBankPos(dst)) + { + // change item amount before check (for unique max count check) + pSrcItem->SetCount(pSrcItem->GetCount() - count); + + ItemPosCountVec dest; + InventoryResult msg = CanBankItem(dstbag, dstslot, dest, pNewItem, false); + if (msg != EQUIP_ERR_OK) + { + delete pNewItem; + pSrcItem->SetCount(pSrcItem->GetCount() + count); + SendEquipError(msg, pSrcItem, nullptr); + return; + } + + if (IsInWorld()) + pSrcItem->SendUpdateToPlayer(this); + pSrcItem->SetState(ITEM_CHANGED, this); + BankItem(dest, pNewItem, true); + } + else if (IsEquipmentPos(dst)) + { + // change item amount before check (for unique max count check), provide space for splitted items + pSrcItem->SetCount(pSrcItem->GetCount() - count); + + uint16 dest; + InventoryResult msg = CanEquipItem(dstslot, dest, pNewItem, false); + if (msg != EQUIP_ERR_OK) + { + delete pNewItem; + pSrcItem->SetCount(pSrcItem->GetCount() + count); + SendEquipError(msg, pSrcItem, nullptr); + return; + } + + if (IsInWorld()) + pSrcItem->SendUpdateToPlayer(this); + pSrcItem->SetState(ITEM_CHANGED, this); + EquipItem(dest, pNewItem, true); + AutoUnequipOffhandIfNeed(); + } +} + +void Player::SwapItem(uint16 src, uint16 dst) +{ + uint8 srcbag = src >> 8; + uint8 srcslot = src & 255; + + uint8 dstbag = dst >> 8; + uint8 dstslot = dst & 255; + + Item* pSrcItem = GetItemByPos(srcbag, srcslot); + Item* pDstItem = GetItemByPos(dstbag, dstslot); + + if (!pSrcItem) + return; + + TC_LOG_DEBUG("entities.player.items", "Player::SwapItem: Player '%s' (%s), Bag: %u, Slot: %u, Item: %u", + GetName().c_str(), GetGUID().ToString().c_str(), dstbag, dstslot, pSrcItem->GetEntry()); + + if (!IsAlive()) + { + SendEquipError(EQUIP_ERR_PLAYER_DEAD, pSrcItem, pDstItem); + return; + } + + // SRC checks + + // check unequip potability for equipped items and bank bags + if (IsEquipmentPos(src) || IsBagPos(src)) + { + // bags can be swapped with empty bag slots, or with empty bag (items move possibility checked later) + InventoryResult msg = CanUnequipItem(src, !IsBagPos(src) || IsBagPos(dst) || (pDstItem && pDstItem->ToBag() && pDstItem->ToBag()->IsEmpty())); + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, pSrcItem, pDstItem); + return; + } + } + + // prevent put equipped/bank bag in self + if (IsBagPos(src) && srcslot == dstbag) + { + SendEquipError(EQUIP_ERR_BAG_IN_BAG, pSrcItem, pDstItem); + return; + } + + // prevent equipping bag in the same slot from its inside + if (IsBagPos(dst) && srcbag == dstslot) + { + SendEquipError(EQUIP_ERR_CANT_SWAP, pSrcItem, pDstItem); + return; + } + + // DST checks + + if (pDstItem) + { + // check unequip potability for equipped items and bank bags + if (IsEquipmentPos(dst) || IsBagPos(dst)) + { + // bags can be swapped with empty bag slots, or with empty bag (items move possibility checked later) + InventoryResult msg = CanUnequipItem(dst, !IsBagPos(dst) || IsBagPos(src) || (pSrcItem->ToBag() && pSrcItem->ToBag()->IsEmpty())); + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, pSrcItem, pDstItem); + return; + } + } + } + + // NOW this is or item move (swap with empty), or swap with another item (including bags in bag possitions) + // or swap empty bag with another empty or not empty bag (with items exchange) + + // Move case + if (!pDstItem) + { + if (IsInventoryPos(dst)) + { + ItemPosCountVec dest; + InventoryResult msg = CanStoreItem(dstbag, dstslot, dest, pSrcItem, false); + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, pSrcItem, nullptr); + return; + } + + RemoveItem(srcbag, srcslot, true); + StoreItem(dest, pSrcItem, true); + if (IsBankPos(src)) + ItemAddedQuestCheck(pSrcItem->GetEntry(), pSrcItem->GetCount()); + } + else if (IsBankPos(dst)) + { + ItemPosCountVec dest; + InventoryResult msg = CanBankItem(dstbag, dstslot, dest, pSrcItem, false); + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, pSrcItem, nullptr); + return; + } + + RemoveItem(srcbag, srcslot, true); + BankItem(dest, pSrcItem, true); + ItemRemovedQuestCheck(pSrcItem->GetEntry(), pSrcItem->GetCount()); + } + else if (IsEquipmentPos(dst)) + { + uint16 dest; + InventoryResult msg = CanEquipItem(dstslot, dest, pSrcItem, false); + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, pSrcItem, nullptr); + return; + } + + RemoveItem(srcbag, srcslot, true); + EquipItem(dest, pSrcItem, true); + AutoUnequipOffhandIfNeed(); + } + + return; + } + + // attempt merge to / fill target item + if (!pSrcItem->IsBag() && !pDstItem->IsBag()) + { + InventoryResult msg; + ItemPosCountVec sDest; + uint16 eDest = 0; + if (IsInventoryPos(dst)) + msg = CanStoreItem(dstbag, dstslot, sDest, pSrcItem, false); + else if (IsBankPos(dst)) + msg = CanBankItem(dstbag, dstslot, sDest, pSrcItem, false); + else if (IsEquipmentPos(dst)) + msg = CanEquipItem(dstslot, eDest, pSrcItem, false); + else + return; + + // can be merge/fill + if (msg == EQUIP_ERR_OK) + { + if (pSrcItem->GetCount() + pDstItem->GetCount() <= pSrcItem->GetTemplate()->GetMaxStackSize()) + { + RemoveItem(srcbag, srcslot, true); + + if (IsInventoryPos(dst)) + StoreItem(sDest, pSrcItem, true); + else if (IsBankPos(dst)) + BankItem(sDest, pSrcItem, true); + else if (IsEquipmentPos(dst)) + { + EquipItem(eDest, pSrcItem, true); + AutoUnequipOffhandIfNeed(); + } + } + else + { + pSrcItem->SetCount(pSrcItem->GetCount() + pDstItem->GetCount() - pSrcItem->GetTemplate()->GetMaxStackSize()); + pDstItem->SetCount(pSrcItem->GetTemplate()->GetMaxStackSize()); + pSrcItem->SetState(ITEM_CHANGED, this); + pDstItem->SetState(ITEM_CHANGED, this); + if (IsInWorld()) + { + pSrcItem->SendUpdateToPlayer(this); + pDstItem->SendUpdateToPlayer(this); + } + } + SendRefundInfo(pDstItem); + return; + } + } + + // impossible merge/fill, do real swap + InventoryResult msg = EQUIP_ERR_OK; + + // check src->dest move possibility + ItemPosCountVec sDest; + uint16 eDest = 0; + if (IsInventoryPos(dst)) + msg = CanStoreItem(dstbag, dstslot, sDest, pSrcItem, true); + else if (IsBankPos(dst)) + msg = CanBankItem(dstbag, dstslot, sDest, pSrcItem, true); + else if (IsEquipmentPos(dst)) + { + msg = CanEquipItem(dstslot, eDest, pSrcItem, true); + if (msg == EQUIP_ERR_OK) + msg = CanUnequipItem(eDest, true); + } + + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, pSrcItem, pDstItem); + return; + } + + // check dest->src move possibility + ItemPosCountVec sDest2; + uint16 eDest2 = 0; + if (IsInventoryPos(src)) + msg = CanStoreItem(srcbag, srcslot, sDest2, pDstItem, true); + else if (IsBankPos(src)) + msg = CanBankItem(srcbag, srcslot, sDest2, pDstItem, true); + else if (IsEquipmentPos(src)) + { + msg = CanEquipItem(srcslot, eDest2, pDstItem, true); + if (msg == EQUIP_ERR_OK) + msg = CanUnequipItem(eDest2, true); + } + + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, pDstItem, pSrcItem); + return; + } + + // Check bag swap with item exchange (one from empty in not bag possition (equipped (not possible in fact) or store) + if (Bag* srcBag = pSrcItem->ToBag()) + { + if (Bag* dstBag = pDstItem->ToBag()) + { + Bag* emptyBag = nullptr; + Bag* fullBag = nullptr; + if (srcBag->IsEmpty() && !IsBagPos(src)) + { + emptyBag = srcBag; + fullBag = dstBag; + } + else if (dstBag->IsEmpty() && !IsBagPos(dst)) + { + emptyBag = dstBag; + fullBag = srcBag; + } + + // bag swap (with items exchange) case + if (emptyBag && fullBag) + { + ItemTemplate const* emptyProto = emptyBag->GetTemplate(); + + uint32 count = 0; + + for (uint32 i=0; i < fullBag->GetBagSize(); ++i) + { + Item* bagItem = fullBag->GetItemByPos(i); + if (!bagItem) + continue; + + ItemTemplate const* bagItemProto = bagItem->GetTemplate(); + if (!bagItemProto || !ItemCanGoIntoBag(bagItemProto, emptyProto)) + { + // one from items not go to empty target bag + SendEquipError(EQUIP_ERR_BAG_IN_BAG, pSrcItem, pDstItem); + return; + } + + ++count; + } + + if (count > emptyBag->GetBagSize()) + { + // too small targeted bag + SendEquipError(EQUIP_ERR_CANT_SWAP, pSrcItem, pDstItem); + return; + } + + // Items swap + count = 0; // will pos in new bag + for (uint32 i = 0; i< fullBag->GetBagSize(); ++i) + { + Item* bagItem = fullBag->GetItemByPos(i); + if (!bagItem) + continue; + + fullBag->RemoveItem(i, true); + emptyBag->StoreItem(count, bagItem, true); + bagItem->SetState(ITEM_CHANGED, this); + + ++count; + } + } + } + } + + // now do moves, remove... + RemoveItem(dstbag, dstslot, false); + RemoveItem(srcbag, srcslot, false); + + // add to dest + if (IsInventoryPos(dst)) + StoreItem(sDest, pSrcItem, true); + else if (IsBankPos(dst)) + BankItem(sDest, pSrcItem, true); + else if (IsEquipmentPos(dst)) + EquipItem(eDest, pSrcItem, true); + + // add to src + if (IsInventoryPos(src)) + StoreItem(sDest2, pDstItem, true); + else if (IsBankPos(src)) + BankItem(sDest2, pDstItem, true); + else if (IsEquipmentPos(src)) + EquipItem(eDest2, pDstItem, true); + + // if inventory item was moved, check if we can remove dependent auras, because they were not removed in Player::RemoveItem (update was set to false) + // do this after swaps are done, we pass nullptr because both weapons could be swapped and none of them should be ignored + if ((srcbag == INVENTORY_SLOT_BAG_0 && srcslot < INVENTORY_SLOT_BAG_END) || (dstbag == INVENTORY_SLOT_BAG_0 && dstslot < INVENTORY_SLOT_BAG_END)) + ApplyItemDependentAuras((Item*)nullptr, false); + + // if player is moving bags and is looting an item inside this bag + // release the loot + if (GetLootGUID()) + { + bool released = false; + if (IsBagPos(src)) + { + Bag* bag = pSrcItem->ToBag(); + for (uint32 i = 0; i < bag->GetBagSize(); ++i) + { + if (Item* bagItem = bag->GetItemByPos(i)) + { + if (bagItem->GetGUID() == GetLootGUID()) + { + m_session->DoLootRelease(GetLootGUID()); + released = true; // so we don't need to look at dstBag + break; + } + } + } + } + + if (!released && IsBagPos(dst)) + { + Bag* bag = pDstItem->ToBag(); + for (uint32 i = 0; i < bag->GetBagSize(); ++i) + { + if (Item* bagItem = bag->GetItemByPos(i)) + { + if (bagItem->GetGUID() == GetLootGUID()) + { + m_session->DoLootRelease(GetLootGUID()); + released = true; // not realy needed here + break; + } + } + } + } + } + + AutoUnequipOffhandIfNeed(); +} + +void Player::AddItemToBuyBackSlot(Item* pItem) +{ + if (pItem) + { + uint32 slot = m_currentBuybackSlot; + // if current back slot non-empty search oldest or free + if (m_items[slot]) + { + uint32 oldest_time = GetUInt32Value(PLAYER_FIELD_BUYBACK_TIMESTAMP_1); + uint32 oldest_slot = BUYBACK_SLOT_START; + + for (uint32 i = BUYBACK_SLOT_START+1; i < BUYBACK_SLOT_END; ++i) + { + // found empty + if (!m_items[i]) + { + oldest_slot = i; + break; + } + + uint32 i_time = GetUInt32Value(PLAYER_FIELD_BUYBACK_TIMESTAMP_1 + i - BUYBACK_SLOT_START); + + if (oldest_time > i_time) + { + oldest_time = i_time; + oldest_slot = i; + } + } + + // find oldest + slot = oldest_slot; + } + + RemoveItemFromBuyBackSlot(slot, true); + TC_LOG_DEBUG("entities.player.items", "Player::AddItemToBuyBackSlot: Player '%s' (%s), Item: %u, Slot: %u", + GetName().c_str(), GetGUID().ToString().c_str(), pItem->GetEntry(), slot); + + m_items[slot] = pItem; + time_t base = GameTime::GetGameTime(); + uint32 etime = uint32(base - m_logintime + (30 * 3600)); + uint32 eslot = slot - BUYBACK_SLOT_START; + + SetGuidValue(PLAYER_FIELD_VENDORBUYBACK_SLOT_1 + (eslot * 2), pItem->GetGUID()); + if (ItemTemplate const* proto = pItem->GetTemplate()) + SetUInt32Value(PLAYER_FIELD_BUYBACK_PRICE_1 + eslot, proto->GetSellPrice() * pItem->GetCount()); + else + SetUInt32Value(PLAYER_FIELD_BUYBACK_PRICE_1 + eslot, 0); + SetUInt32Value(PLAYER_FIELD_BUYBACK_TIMESTAMP_1 + eslot, (uint32)etime); + + // move to next (for non filled list is move most optimized choice) + if (m_currentBuybackSlot < BUYBACK_SLOT_END - 1) + ++m_currentBuybackSlot; + } +} + +Item* Player::GetItemFromBuyBackSlot(uint32 slot) +{ + TC_LOG_DEBUG("entities.player.items", "Player::GetItemFromBuyBackSlot: Player '%s' (%s), Slot: %u", + GetName().c_str(), GetGUID().ToString().c_str(), slot); + if (slot >= BUYBACK_SLOT_START && slot < BUYBACK_SLOT_END) + return m_items[slot]; + return nullptr; +} + +void Player::RemoveItemFromBuyBackSlot(uint32 slot, bool del) +{ + TC_LOG_DEBUG("entities.player.items", "Player::RemoveItemFromBuyBackSlot: Player '%s' (%s), Slot: %u", + GetName().c_str(), GetGUID().ToString().c_str(), slot); + if (slot >= BUYBACK_SLOT_START && slot < BUYBACK_SLOT_END) + { + Item* pItem = m_items[slot]; + if (pItem) + { + pItem->RemoveFromWorld(); + if (del) + { + if (ItemTemplate const* itemTemplate = pItem->GetTemplate()) + if (itemTemplate->GetFlags() & ITEM_FLAG_HAS_LOOT) + sLootItemStorage->RemoveStoredLootForContainer(pItem->GetGUID().GetCounter()); + + pItem->SetState(ITEM_REMOVED, this); + } + } + + m_items[slot] = nullptr; + + uint32 eslot = slot - BUYBACK_SLOT_START; + SetGuidValue(PLAYER_FIELD_VENDORBUYBACK_SLOT_1 + (eslot * 2), ObjectGuid::Empty); + SetUInt32Value(PLAYER_FIELD_BUYBACK_PRICE_1 + eslot, 0); + SetUInt32Value(PLAYER_FIELD_BUYBACK_TIMESTAMP_1 + eslot, 0); + + // if current backslot is filled set to now free slot + if (m_items[m_currentBuybackSlot]) + m_currentBuybackSlot = slot; + } +} + +void Player::SendEquipError(InventoryResult msg, Item* pItem, Item* pItem2, uint32 itemid) const +{ + WorldPacket data(SMSG_INVENTORY_CHANGE_FAILURE, (msg == EQUIP_ERR_CANT_EQUIP_LEVEL_I ? 22 : 18)); + data << uint8(msg); + + if (msg != EQUIP_ERR_OK) + { + data << uint64(pItem ? pItem->GetGUID() : ObjectGuid::Empty); + data << uint64(pItem2 ? pItem2->GetGUID() : ObjectGuid::Empty); + data << uint8(0); // bag type subclass, used with EQUIP_ERR_EVENT_AUTOEQUIP_BIND_CONFIRM and EQUIP_ERR_ITEM_DOESNT_GO_INTO_BAG2 + + switch (msg) + { + case EQUIP_ERR_CANT_EQUIP_LEVEL_I: + case EQUIP_ERR_PURCHASE_LEVEL_TOO_LOW: + { + ItemTemplate const* proto = pItem ? pItem->GetTemplate() : sObjectMgr->GetItemTemplate(itemid); + data << uint32(proto ? proto->GetRequiredLevel() : 0); + break; + } + case EQUIP_ERR_NO_OUTPUT: // no idea about this one... + { + data << uint64(0); // item guid + data << uint32(0); // slot + data << uint64(0); // container + break; + } + case EQUIP_ERR_ITEM_MAX_LIMIT_CATEGORY_COUNT_EXCEEDED_IS: + case EQUIP_ERR_ITEM_MAX_LIMIT_CATEGORY_SOCKETED_EXCEEDED_IS: + case EQUIP_ERR_ITEM_MAX_LIMIT_CATEGORY_EQUIPPED_EXCEEDED_IS: + { + ItemTemplate const* proto = pItem ? pItem->GetTemplate() : sObjectMgr->GetItemTemplate(itemid); + data << uint32(proto ? proto->GetItemLimitCategory() : 0); + break; + } + default: + break; + } + } + SendDirectMessage(&data); +} + +void Player::SendBuyError(BuyResult msg, Creature* creature, uint32 item, uint32 /*param*/) const +{ + WorldPacket data(SMSG_BUY_FAILED, (8+4+4+1)); + data << uint64(creature ? creature->GetGUID() : ObjectGuid::Empty); + data << uint32(item); + data << uint8(msg); + SendDirectMessage(&data); +} + +void Player::SendSellError(SellResult msg, Creature* creature, ObjectGuid guid) const +{ + WorldPacket data(SMSG_SELL_ITEM, (8+8+1)); // last check 4.3.4 + data << uint64(creature ? creature->GetGUID() : ObjectGuid::Empty); + data << uint64(guid); + data << uint8(msg); + SendDirectMessage(&data); +} + +bool Player::IsUseEquipedWeapon(bool mainhand) const +{ + // disarm applied only to mainhand weapon + return !IsInFeralForm() && (!mainhand || !HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_DISARMED)); +} + +void Player::SetCanTitanGrip(bool value, uint32 penaltySpellId /*= 0*/) +{ + if (value == m_canTitanGrip) + return; + + m_canTitanGrip = value; + m_titanGripPenaltySpellId = penaltySpellId; +} + +void Player::CheckTitanGripPenalty() +{ + if (!CanTitanGrip()) + return; + + bool apply = IsUsingTwoHandedWeaponInOneHand(); + if (apply) + { + if (!HasAura(m_titanGripPenaltySpellId)) + CastSpell((Unit*)nullptr, m_titanGripPenaltySpellId, true); + } + else + RemoveAurasDueToSpell(m_titanGripPenaltySpellId); +} + +bool Player::IsTwoHandUsed() const +{ + Item* mainItem = GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + return mainItem && mainItem->GetTemplate()->GetInventoryType() == INVTYPE_2HWEAPON && !CanTitanGrip(); +} + +bool Player::IsUsingTwoHandedWeaponInOneHand() const +{ + Item* offItem = GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (offItem && offItem->GetTemplate()->GetInventoryType() == INVTYPE_2HWEAPON) + return true; + + Item* mainItem = GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (!mainItem || mainItem->GetTemplate()->GetInventoryType() != INVTYPE_2HWEAPON) + return false; + + if (!offItem) + return false; + + return true; +} + +void Player::TradeCancel(bool sendback, TradeStatus status /*= TRADE_STATUS_TRADE_CANCELED*/) +{ + if (m_trade) + { + Player* trader = m_trade->GetTrader(); + + // send yellow "Trade canceled" message to both traders + if (sendback) + GetSession()->SendCancelTrade(status); + + trader->GetSession()->SendCancelTrade(status); + + // cleanup + delete m_trade; + m_trade = nullptr; + delete trader->m_trade; + trader->m_trade = nullptr; + } +} + +void Player::UpdateSoulboundTradeItems() +{ + // also checks for garbage data + for (GuidUnorderedSet::iterator itr = m_itemSoulboundTradeable.begin(); itr != m_itemSoulboundTradeable.end();) + { + Item* item = GetItemByGuid(*itr); + if (!item || item->GetOwnerGUID() != GetGUID() || item->CheckSoulboundTradeExpire()) + itr = m_itemSoulboundTradeable.erase(itr); + else + ++itr; + } +} + +void Player::AddTradeableItem(Item* item) +{ + m_itemSoulboundTradeable.insert(item->GetGUID()); +} + +void Player::RemoveTradeableItem(Item* item) +{ + m_itemSoulboundTradeable.erase(item->GetGUID()); +} + +void Player::UpdateItemDuration(uint32 time, bool realtimeonly) +{ + if (m_itemDuration.empty()) + return; + + TC_LOG_DEBUG("entities.player.items", "Player::UpdateItemDuration: Player '%s' (%s), Time: %u, RealTimeOnly: %u", + GetName().c_str(), GetGUID().ToString().c_str(), time, realtimeonly); + + for (ItemDurationList::const_iterator itr = m_itemDuration.begin(); itr != m_itemDuration.end();) + { + Item* item = *itr; + ++itr; // current element can be erased in UpdateDuration + + if (!realtimeonly || item->GetTemplate()->GetFlags() & ITEM_FLAG_REAL_DURATION) + item->UpdateDuration(this, time); + } +} + +void Player::UpdateEnchantTime(uint32 time) +{ + for (EnchantDurationList::iterator itr = m_enchantDuration.begin(), next; itr != m_enchantDuration.end(); itr=next) + { + ASSERT(itr->item); + next = itr; + if (!itr->item->GetEnchantmentId(itr->slot)) + { + next = m_enchantDuration.erase(itr); + } + else if (itr->leftduration <= time) + { + ApplyEnchantment(itr->item, itr->slot, false, false); + itr->item->ClearEnchantment(itr->slot); + next = m_enchantDuration.erase(itr); + } + else if (itr->leftduration > time) + { + itr->leftduration -= time; + ++next; + } + } +} + +void Player::AddEnchantmentDurations(Item* item) +{ + for (int x = 0; x < MAX_ENCHANTMENT_SLOT; ++x) + { + if (x > PRISMATIC_ENCHANTMENT_SLOT && x < PROP_ENCHANTMENT_SLOT_0) // not holding enchantment id + continue; + + if (!item->GetEnchantmentId(EnchantmentSlot(x))) + continue; + + uint32 duration = item->GetEnchantmentDuration(EnchantmentSlot(x)); + if (duration > 0) + AddEnchantmentDuration(item, EnchantmentSlot(x), duration); + } +} + +void Player::RemoveEnchantmentDurations(Item* item) +{ + for (EnchantDurationList::iterator itr = m_enchantDuration.begin(); itr != m_enchantDuration.end();) + { + if (itr->item == item) + { + // save duration in item + item->SetEnchantmentDuration(EnchantmentSlot(itr->slot), itr->leftduration, this); + itr = m_enchantDuration.erase(itr); + } + else + ++itr; + } +} + +void Player::RemoveEnchantmentDurationsReferences(Item* item) +{ + for (EnchantDurationList::iterator itr = m_enchantDuration.begin(); itr != m_enchantDuration.end();) + { + if (itr->item == item) + itr = m_enchantDuration.erase(itr); + else + ++itr; + } +} + +void Player::RemoveArenaEnchantments(EnchantmentSlot slot) +{ + // remove enchantments from equipped items first to clean up the m_enchantDuration list + for (EnchantDurationList::iterator itr = m_enchantDuration.begin(), next; itr != m_enchantDuration.end(); itr = next) + { + next = itr; + if (itr->slot == slot) + { + if (itr->item && itr->item->GetEnchantmentId(slot)) + { + // Poisons and DK runes are enchants which are allowed on arenas + if (sSpellMgr->IsArenaAllowedEnchancment(itr->item->GetEnchantmentId(slot))) + { + ++next; + continue; + } + // remove from stats + ApplyEnchantment(itr->item, slot, false, false); + // remove visual + itr->item->ClearEnchantment(slot); + } + // remove from update list + next = m_enchantDuration.erase(itr); + } + else + ++next; + } + + // remove enchants from inventory items + // NOTE: no need to remove these from stats, since these aren't equipped + // in inventory + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) + if (Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (!sSpellMgr->IsArenaAllowedEnchancment(pItem->GetEnchantmentId(slot))) + pItem->ClearEnchantment(slot); + + // in inventory bags + for (uint8 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + if (Bag* pBag = GetBagByPos(i)) + for (uint32 j = 0; j < pBag->GetBagSize(); j++) + if (Item* pItem = pBag->GetItemByPos(j)) + if (!sSpellMgr->IsArenaAllowedEnchancment(pItem->GetEnchantmentId(slot))) + pItem->ClearEnchantment(slot); +} + +// duration == 0 will remove item enchant +void Player::AddEnchantmentDuration(Item* item, EnchantmentSlot slot, uint32 duration) +{ + if (!item) + return; + + if (slot >= MAX_ENCHANTMENT_SLOT) + return; + + for (EnchantDurationList::iterator itr = m_enchantDuration.begin(); itr != m_enchantDuration.end(); ++itr) + { + if (itr->item == item && itr->slot == slot) + { + itr->item->SetEnchantmentDuration(itr->slot, itr->leftduration, this); + m_enchantDuration.erase(itr); + break; + } + } + if (duration > 0) + { + GetSession()->SendItemEnchantTimeUpdate(GetGUID(), item->GetGUID(), slot, uint32(duration/1000)); + m_enchantDuration.push_back(EnchantDuration(item, slot, duration)); + } +} + +void Player::ApplyReforgeEnchantment(Item* item, bool apply) +{ + if (!item || item->IsBroken()) + return; + + ItemReforgeEntry const* reforge = sItemReforgeStore.LookupEntry(item->GetEnchantmentId(REFORGE_ENCHANTMENT_SLOT)); + if (!reforge) + return; + + float removeValue = item->GetReforgableStat(ItemModType(reforge->Source_stat)) * reforge->Source_multiplier; + float addValue = removeValue * reforge->Target_multiplier; + + switch (reforge->Source_stat) + { + case ITEM_MOD_MANA: + HandleStatFlatModifier(UNIT_MOD_MANA, BASE_VALUE, -removeValue, apply); + break; + case ITEM_MOD_HEALTH: + HandleStatFlatModifier(UNIT_MOD_HEALTH, BASE_VALUE, -removeValue, apply); + break; + case ITEM_MOD_AGILITY: + if (apply) + GetStats().RemoveBaseStatModifier(StatType::Agility, removeValue); + else + GetStats().AddBaseStatModifier(StatType::Agility, removeValue); + break; + case ITEM_MOD_STRENGTH: + if (apply) + GetStats().RemoveBaseStatModifier(StatType::Strength, removeValue); + else + GetStats().AddBaseStatModifier(StatType::Strength, removeValue); + break; + case ITEM_MOD_INTELLECT: + if (apply) + GetStats().RemoveBaseStatModifier(StatType::Intellect, removeValue); + else + GetStats().AddBaseStatModifier(StatType::Intellect, removeValue); + break; + case ITEM_MOD_SPIRIT: + if (apply) + GetStats().RemoveBaseStatModifier(StatType::Spirit, removeValue); + else + GetStats().AddBaseStatModifier(StatType::Spirit, removeValue); + break; + case ITEM_MOD_STAMINA: + if (apply) + GetStats().RemoveBaseStatModifier(StatType::Stamina, removeValue); + else + GetStats().AddBaseStatModifier(StatType::Stamina, removeValue); + break; + case ITEM_MOD_DEFENSE_SKILL_RATING: + ApplyRatingMod(CR_DEFENSE_SKILL, -int32(removeValue), apply); + break; + case ITEM_MOD_DODGE_RATING: + ApplyRatingMod(CR_DODGE, -int32(removeValue), apply); + break; + case ITEM_MOD_PARRY_RATING: + ApplyRatingMod(CR_PARRY, -int32(removeValue), apply); + break; + case ITEM_MOD_BLOCK_RATING: + ApplyRatingMod(CR_BLOCK, -int32(removeValue), apply); + break; + case ITEM_MOD_HIT_MELEE_RATING: + ApplyRatingMod(CR_HIT_MELEE, -int32(removeValue), apply); + break; + case ITEM_MOD_HIT_RANGED_RATING: + ApplyRatingMod(CR_HIT_RANGED, -int32(removeValue), apply); + break; + case ITEM_MOD_HIT_SPELL_RATING: + ApplyRatingMod(CR_HIT_SPELL, -int32(removeValue), apply); + break; + case ITEM_MOD_CRIT_MELEE_RATING: + ApplyRatingMod(CR_CRIT_MELEE, -int32(removeValue), apply); + break; + case ITEM_MOD_CRIT_RANGED_RATING: + ApplyRatingMod(CR_CRIT_RANGED, -int32(removeValue), apply); + break; + case ITEM_MOD_CRIT_SPELL_RATING: + ApplyRatingMod(CR_CRIT_SPELL, -int32(removeValue), apply); + break; + case ITEM_MOD_HASTE_SPELL_RATING: + ApplyRatingMod(CR_HASTE_SPELL, -int32(removeValue), apply); + break; + case ITEM_MOD_HIT_RATING: + ApplyRatingMod(CR_HIT_MELEE, -int32(removeValue), apply); + ApplyRatingMod(CR_HIT_RANGED, -int32(removeValue), apply); + ApplyRatingMod(CR_HIT_SPELL, -int32(removeValue), apply); + break; + case ITEM_MOD_CRIT_RATING: + ApplyRatingMod(CR_CRIT_MELEE, -int32(removeValue), apply); + ApplyRatingMod(CR_CRIT_RANGED, -int32(removeValue), apply); + ApplyRatingMod(CR_CRIT_SPELL, -int32(removeValue), apply); + break; + case ITEM_MOD_RESILIENCE_RATING: + ApplyRatingMod(CR_RESILIENCE_PLAYER_DAMAGE_TAKEN, -int32(removeValue), apply); + break; + case ITEM_MOD_HASTE_RATING: + ApplyRatingMod(CR_HASTE_MELEE, -int32(removeValue), apply); + ApplyRatingMod(CR_HASTE_RANGED, -int32(removeValue), apply); + ApplyRatingMod(CR_HASTE_SPELL, -int32(removeValue), apply); + break; + case ITEM_MOD_EXPERTISE_RATING: + ApplyRatingMod(CR_EXPERTISE, -int32(removeValue), apply); + break; + case ITEM_MOD_ATTACK_POWER: + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER, TOTAL_VALUE, -removeValue, apply); + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER_RANGED, TOTAL_VALUE, -removeValue, apply); + break; + case ITEM_MOD_RANGED_ATTACK_POWER: + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER_RANGED, TOTAL_VALUE, -removeValue, apply); + break; + case ITEM_MOD_MANA_REGENERATION: + ApplyManaRegenBonus(-int32(removeValue), apply); + break; + //case ITEM_MOD_ARMOR_PENETRATION_RATING: + // ApplyRatingMod(CR_ARMOR_PENETRATION, -int32(removeValue), apply); + // break; + case ITEM_MOD_SPELL_POWER: + ApplySpellPowerBonus(-int32(removeValue), apply); + break; + case ITEM_MOD_HEALTH_REGEN: + ApplyHealthRegenBonus(-int32(removeValue), apply); + break; + case ITEM_MOD_SPELL_PENETRATION: + ApplyModInt32Value(PLAYER_FIELD_MOD_TARGET_RESISTANCE, -int32(removeValue), apply); + m_spellPenetrationItemMod += apply ? -int32(removeValue) : int32(removeValue); + break; + case ITEM_MOD_BLOCK_VALUE: + HandleBaseModFlatValue(SHIELD_BLOCK_VALUE, -removeValue, apply); + break; + case ITEM_MOD_MASTERY_RATING: + ApplyRatingMod(CR_MASTERY, -int32(removeValue), apply); + break; + case ITEM_MOD_EXTRA_ARMOR: + HandleStatFlatModifier(UNIT_MOD_ARMOR, BASE_VALUE, -removeValue, apply); + break; + } + + switch (reforge->Target_stat) + { + case ITEM_MOD_MANA: + HandleStatFlatModifier(UNIT_MOD_MANA, BASE_VALUE, addValue, apply); + break; + case ITEM_MOD_HEALTH: + HandleStatFlatModifier(UNIT_MOD_HEALTH, BASE_VALUE, addValue, apply); + break; + case ITEM_MOD_AGILITY: + if (apply) + GetStats().AddBaseStatModifier(StatType::Agility, addValue); + else + GetStats().RemoveBaseStatModifier(StatType::Agility, addValue); + break; + case ITEM_MOD_STRENGTH: + if (apply) + GetStats().AddBaseStatModifier(StatType::Strength, addValue); + else + GetStats().RemoveBaseStatModifier(StatType::Strength, addValue); + break; + case ITEM_MOD_INTELLECT: + if (apply) + GetStats().AddBaseStatModifier(StatType::Intellect, addValue); + else + GetStats().RemoveBaseStatModifier(StatType::Intellect, addValue); + break; + case ITEM_MOD_SPIRIT: + if (apply) + GetStats().AddBaseStatModifier(StatType::Spirit, addValue); + else + GetStats().RemoveBaseStatModifier(StatType::Spirit, addValue); + break; + case ITEM_MOD_STAMINA: + if (apply) + GetStats().AddBaseStatModifier(StatType::Stamina, addValue); + else + GetStats().RemoveBaseStatModifier(StatType::Stamina, addValue); + break; + case ITEM_MOD_DEFENSE_SKILL_RATING: + ApplyRatingMod(CR_DEFENSE_SKILL, int32(addValue), apply); + break; + case ITEM_MOD_DODGE_RATING: + ApplyRatingMod(CR_DODGE, int32(addValue), apply); + break; + case ITEM_MOD_PARRY_RATING: + ApplyRatingMod(CR_PARRY, int32(addValue), apply); + break; + case ITEM_MOD_BLOCK_RATING: + ApplyRatingMod(CR_BLOCK, int32(addValue), apply); + break; + case ITEM_MOD_HIT_MELEE_RATING: + ApplyRatingMod(CR_HIT_MELEE, int32(addValue), apply); + break; + case ITEM_MOD_HIT_RANGED_RATING: + ApplyRatingMod(CR_HIT_RANGED, int32(addValue), apply); + break; + case ITEM_MOD_HIT_SPELL_RATING: + ApplyRatingMod(CR_HIT_SPELL, int32(addValue), apply); + break; + case ITEM_MOD_CRIT_MELEE_RATING: + ApplyRatingMod(CR_CRIT_MELEE, int32(addValue), apply); + break; + case ITEM_MOD_CRIT_RANGED_RATING: + ApplyRatingMod(CR_CRIT_RANGED, int32(addValue), apply); + break; + case ITEM_MOD_CRIT_SPELL_RATING: + ApplyRatingMod(CR_CRIT_SPELL, int32(addValue), apply); + break; + case ITEM_MOD_HASTE_SPELL_RATING: + ApplyRatingMod(CR_HASTE_SPELL, int32(addValue), apply); + break; + case ITEM_MOD_HIT_RATING: + ApplyRatingMod(CR_HIT_MELEE, int32(addValue), apply); + ApplyRatingMod(CR_HIT_RANGED, int32(addValue), apply); + ApplyRatingMod(CR_HIT_SPELL, int32(addValue), apply); + break; + case ITEM_MOD_CRIT_RATING: + ApplyRatingMod(CR_CRIT_MELEE, int32(addValue), apply); + ApplyRatingMod(CR_CRIT_RANGED, int32(addValue), apply); + ApplyRatingMod(CR_CRIT_SPELL, int32(addValue), apply); + break; + case ITEM_MOD_RESILIENCE_RATING: + ApplyRatingMod(CR_RESILIENCE_PLAYER_DAMAGE_TAKEN, int32(addValue), apply); + break; + case ITEM_MOD_HASTE_RATING: + ApplyRatingMod(CR_HASTE_MELEE, int32(addValue), apply); + ApplyRatingMod(CR_HASTE_RANGED, int32(addValue), apply); + ApplyRatingMod(CR_HASTE_SPELL, int32(addValue), apply); + break; + case ITEM_MOD_EXPERTISE_RATING: + ApplyRatingMod(CR_EXPERTISE, int32(addValue), apply); + break; + case ITEM_MOD_ATTACK_POWER: + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER, TOTAL_VALUE, addValue, apply); + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER_RANGED, TOTAL_VALUE, addValue, apply); + break; + case ITEM_MOD_RANGED_ATTACK_POWER: + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER_RANGED, TOTAL_VALUE, addValue, apply); + break; + case ITEM_MOD_MANA_REGENERATION: + ApplyManaRegenBonus(int32(addValue), apply); + break; + //case ITEM_MOD_ARMOR_PENETRATION_RATING: + // ApplyRatingMod(CR_ARMOR_PENETRATION, int32(addValue), apply); + // break; + case ITEM_MOD_SPELL_POWER: + ApplySpellPowerBonus(int32(addValue), apply); + break; + case ITEM_MOD_HEALTH_REGEN: + ApplyHealthRegenBonus(int32(addValue), apply); + break; + case ITEM_MOD_SPELL_PENETRATION: + ApplyModInt32Value(PLAYER_FIELD_MOD_TARGET_RESISTANCE, int32(addValue), apply); + m_spellPenetrationItemMod += apply ? int32(addValue) : -int32(addValue); + break; + case ITEM_MOD_BLOCK_VALUE: + HandleBaseModFlatValue(SHIELD_BLOCK_VALUE, addValue, apply); + break; + case ITEM_MOD_MASTERY_RATING: + ApplyRatingMod(CR_MASTERY, int32(addValue), apply); + break; + case ITEM_MOD_EXTRA_ARMOR: + HandleStatFlatModifier(UNIT_MOD_ARMOR, BASE_VALUE, addValue, apply); + break; + } +} + +void Player::ApplyEnchantment(Item* item, bool apply) +{ + for (uint32 slot = 0; slot < MAX_ENCHANTMENT_SLOT; ++slot) + { + // Apply reforge as last enchant + if (slot == REFORGE_ENCHANTMENT_SLOT) + continue; + + ApplyEnchantment(item, EnchantmentSlot(slot), apply); + } + + ApplyEnchantment(item, REFORGE_ENCHANTMENT_SLOT, apply); +} + +void Player::ApplyEnchantment(Item* item, EnchantmentSlot slot, bool apply, bool apply_dur, bool ignore_condition) +{ + if (!item || !item->IsEquipped()) + return; + + if (slot >= MAX_ENCHANTMENT_SLOT) + return; + + if (slot == TRANSMOGRIFY_ENCHANTMENT_SLOT) + return; + + if (slot == REFORGE_ENCHANTMENT_SLOT) + { + ApplyReforgeEnchantment(item, apply); + return; + } + + uint32 enchant_id = item->GetEnchantmentId(slot); + if (!enchant_id) + return; + + SpellItemEnchantmentEntry const* pEnchant = sSpellItemEnchantmentStore.LookupEntry(enchant_id); + if (!pEnchant) + return; + + if (!ignore_condition && pEnchant->Condition_ID && !EnchantmentFitsRequirements(pEnchant->Condition_ID, -1)) + return; + + if (pEnchant->MinLevel > getLevel()) + return; + + if (pEnchant->RequiredSkillID > 0 && pEnchant->RequiredSkillRank > GetSkillValue(pEnchant->RequiredSkillID)) + return; + + // Cogwheel gems dont have requirement data set in SpellItemEnchantment.dbc, but they do have it in Item-sparse.db2 + if (ItemTemplate const* gem = sObjectMgr->GetItemTemplate(pEnchant->Src_itemID)) + if (gem->GetRequiredSkill() && GetSkillValue(gem->GetRequiredSkill()) < gem->GetRequiredSkillRank()) + return; + + // If we're dealing with a gem inside a prismatic socket we need to check the prismatic socket requirements + // rather than the gem requirements itself. If the socket has no color it is a prismatic socket. + if ((slot == SOCK_ENCHANTMENT_SLOT || slot == SOCK_ENCHANTMENT_SLOT_2 || slot == SOCK_ENCHANTMENT_SLOT_3) + && !item->GetTemplate()->GetSocketColor(slot-SOCK_ENCHANTMENT_SLOT)) + { + // Check if the requirements for the prismatic socket are met before applying the gem stats + SpellItemEnchantmentEntry const* pPrismaticEnchant = sSpellItemEnchantmentStore.LookupEntry(item->GetEnchantmentId(PRISMATIC_ENCHANTMENT_SLOT)); + if (!pPrismaticEnchant || (pPrismaticEnchant->RequiredSkillID > 0 && pPrismaticEnchant->RequiredSkillRank > GetSkillValue(pPrismaticEnchant->RequiredSkillID))) + return; + } + + if (!item->IsBroken()) + { + for (uint8 s = 0; s < MAX_ITEM_ENCHANTMENT_EFFECTS; ++s) + { + uint32 enchant_display_type = pEnchant->Effect[s]; + uint32 enchant_amount = pEnchant->EffectPointsMin[s]; + uint32 enchant_spell_id = pEnchant->EffectArg[s]; + + switch (enchant_display_type) + { + case ITEM_ENCHANTMENT_TYPE_NONE: + break; + case ITEM_ENCHANTMENT_TYPE_COMBAT_SPELL: + // processed in Player::CastItemCombatSpell + break; + case ITEM_ENCHANTMENT_TYPE_DAMAGE: + { + WeaponAttackType const attackType = Player::GetAttackBySlot(item->GetSlot()); + if (attackType != MAX_ATTACK) + UpdateDamageDoneMods(attackType, apply ? -1 : slot); + break; + } + case ITEM_ENCHANTMENT_TYPE_EQUIP_SPELL: + if (enchant_spell_id) + { + if (apply) + { + int32 basepoints = 0; + // Random Property Exist - try found basepoints for spell (basepoints depends from item suffix factor) + if (item->GetItemRandomPropertyId() < 0) + { + ItemRandomSuffixEntry const* item_rand = sItemRandomSuffixStore.LookupEntry(abs(item->GetItemRandomPropertyId())); + if (item_rand) + { + // Search enchant_amount + for (int k = 0; k < 5; ++k) + { + if (item_rand->Enchantment[k] == enchant_id) + { + basepoints = int32((item_rand->AllocationPct[k] * item->GetItemSuffixFactor()) / 10000); + break; + } + } + } + } + + CastSpellExtraArgs args(item); + // Cast custom spell vs all equal basepoints got from enchant_amount + if (basepoints) + for (uint32 i = 0; i < MAX_SPELL_EFFECTS; ++i) + args.AddSpellMod(SpellValueMod(SPELLVALUE_BASE_POINT0 + i), basepoints); + + CastSpell(this, enchant_spell_id, args); + } + else + RemoveAurasDueToItemSpell(enchant_spell_id, item->GetGUID()); + } + break; + case ITEM_ENCHANTMENT_TYPE_RESISTANCE: + if (!enchant_amount) + { + ItemRandomSuffixEntry const* item_rand = sItemRandomSuffixStore.LookupEntry(abs(item->GetItemRandomPropertyId())); + if (item_rand) + { + for (int k = 0; k < MAX_ITEM_ENCHANTMENT_EFFECTS; ++k) + { + if (item_rand->Enchantment[k] == enchant_id) + { + enchant_amount = uint32((item_rand->AllocationPct[k] * item->GetItemSuffixFactor()) / 10000); + break; + } + } + } + } + + HandleStatFlatModifier(UnitMods(UNIT_MOD_RESISTANCE_START + enchant_spell_id), TOTAL_VALUE, float(enchant_amount), apply); + break; + case ITEM_ENCHANTMENT_TYPE_STAT: + { + if (!enchant_amount) + { + ItemRandomSuffixEntry const* item_rand_suffix = sItemRandomSuffixStore.LookupEntry(abs(item->GetItemRandomPropertyId())); + if (item_rand_suffix) + { + for (int k = 0; k < 5; ++k) + { + if (item_rand_suffix->Enchantment[k] == enchant_id) + { + enchant_amount = uint32((item_rand_suffix->AllocationPct[k] * item->GetItemSuffixFactor()) / 10000); + break; + } + } + } + } + + TC_LOG_DEBUG("entities.player.items", "Adding %u to stat nb %u", enchant_amount, enchant_spell_id); + switch (enchant_spell_id) + { + case ITEM_MOD_MANA: + TC_LOG_DEBUG("entities.player.items", "+ %u MANA", enchant_amount); + HandleStatFlatModifier(UNIT_MOD_MANA, BASE_VALUE, float(enchant_amount), apply); + break; + case ITEM_MOD_HEALTH: + TC_LOG_DEBUG("entities.player.items", "+ %u HEALTH", enchant_amount); + HandleStatFlatModifier(UNIT_MOD_HEALTH, BASE_VALUE, float(enchant_amount), apply); + break; + case ITEM_MOD_AGILITY: + TC_LOG_DEBUG("entities.player.items", "+ %u AGILITY", enchant_amount); + if (apply) + GetStats().AddBaseStatModifier(StatType::Agility, enchant_amount); + else + GetStats().RemoveBaseStatModifier(StatType::Agility, enchant_amount); + break; + case ITEM_MOD_STRENGTH: + TC_LOG_DEBUG("entities.player.items", "+ %u STRENGTH", enchant_amount); + if (apply) + GetStats().AddBaseStatModifier(StatType::Strength, enchant_amount); + else + GetStats().RemoveBaseStatModifier(StatType::Strength, enchant_amount); + break; + case ITEM_MOD_INTELLECT: + TC_LOG_DEBUG("entities.player.items", "+ %u INTELLECT", enchant_amount); + if (apply) + GetStats().AddBaseStatModifier(StatType::Intellect, enchant_amount); + else + GetStats().RemoveBaseStatModifier(StatType::Intellect, enchant_amount); + break; + case ITEM_MOD_SPIRIT: + TC_LOG_DEBUG("entities.player.items", "+ %u SPIRIT", enchant_amount); + if (apply) + GetStats().AddBaseStatModifier(StatType::Spirit, enchant_amount); + else + GetStats().RemoveBaseStatModifier(StatType::Spirit, enchant_amount); + break; + case ITEM_MOD_STAMINA: + TC_LOG_DEBUG("entities.player.items", "+ %u STAMINA", enchant_amount); + if (apply) + GetStats().AddBaseStatModifier(StatType::Stamina, enchant_amount); + else + GetStats().RemoveBaseStatModifier(StatType::Stamina, enchant_amount); + break; + case ITEM_MOD_DEFENSE_SKILL_RATING: + ApplyRatingMod(CR_DEFENSE_SKILL, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u DEFENCE", enchant_amount); + break; + case ITEM_MOD_DODGE_RATING: + ApplyRatingMod(CR_DODGE, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u DODGE", enchant_amount); + break; + case ITEM_MOD_PARRY_RATING: + ApplyRatingMod(CR_PARRY, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u PARRY", enchant_amount); + break; + case ITEM_MOD_BLOCK_RATING: + ApplyRatingMod(CR_BLOCK, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u SHIELD_BLOCK", enchant_amount); + break; + case ITEM_MOD_HIT_MELEE_RATING: + ApplyRatingMod(CR_HIT_MELEE, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u MELEE_HIT", enchant_amount); + break; + case ITEM_MOD_HIT_RANGED_RATING: + ApplyRatingMod(CR_HIT_RANGED, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u RANGED_HIT", enchant_amount); + break; + case ITEM_MOD_HIT_SPELL_RATING: + ApplyRatingMod(CR_HIT_SPELL, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u SPELL_HIT", enchant_amount); + break; + case ITEM_MOD_CRIT_MELEE_RATING: + ApplyRatingMod(CR_CRIT_MELEE, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u MELEE_CRIT", enchant_amount); + break; + case ITEM_MOD_CRIT_RANGED_RATING: + ApplyRatingMod(CR_CRIT_RANGED, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u RANGED_CRIT", enchant_amount); + break; + case ITEM_MOD_CRIT_SPELL_RATING: + ApplyRatingMod(CR_CRIT_SPELL, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u SPELL_CRIT", enchant_amount); + break; + // Values from ITEM_STAT_MELEE_HA_RATING to ITEM_MOD_HASTE_RANGED_RATING are never used + // in Enchantments + // case ITEM_MOD_HIT_TAKEN_MELEE_RATING: + // ApplyRatingMod(CR_HIT_TAKEN_MELEE, enchant_amount, apply); + // break; + // case ITEM_MOD_HIT_TAKEN_RANGED_RATING: + // ApplyRatingMod(CR_HIT_TAKEN_RANGED, enchant_amount, apply); + // break; + // case ITEM_MOD_HIT_TAKEN_SPELL_RATING: + // ApplyRatingMod(CR_HIT_TAKEN_SPELL, enchant_amount, apply); + // break; + // case ITEM_MOD_CRIT_TAKEN_MELEE_RATING: + // ApplyRatingMod(CR_CRIT_TAKEN_MELEE, enchant_amount, apply); + // break; + // case ITEM_MOD_CRIT_TAKEN_RANGED_RATING: + // ApplyRatingMod(CR_CRIT_TAKEN_RANGED, enchant_amount, apply); + // break; + // case ITEM_MOD_CRIT_TAKEN_SPELL_RATING: + // ApplyRatingMod(CR_CRIT_TAKEN_SPELL, enchant_amount, apply); + // break; + // case ITEM_MOD_HASTE_MELEE_RATING: + // ApplyRatingMod(CR_HASTE_MELEE, enchant_amount, apply); + // break; + // case ITEM_MOD_HASTE_RANGED_RATING: + // ApplyRatingMod(CR_HASTE_RANGED, enchant_amount, apply); + // break; + case ITEM_MOD_HASTE_SPELL_RATING: + ApplyRatingMod(CR_HASTE_SPELL, enchant_amount, apply); + break; + case ITEM_MOD_HIT_RATING: + ApplyRatingMod(CR_HIT_MELEE, enchant_amount, apply); + ApplyRatingMod(CR_HIT_RANGED, enchant_amount, apply); + ApplyRatingMod(CR_HIT_SPELL, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u HIT", enchant_amount); + break; + case ITEM_MOD_CRIT_RATING: + ApplyRatingMod(CR_CRIT_MELEE, enchant_amount, apply); + ApplyRatingMod(CR_CRIT_RANGED, enchant_amount, apply); + ApplyRatingMod(CR_CRIT_SPELL, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u CRITICAL", enchant_amount); + break; + // case ITEM_MOD_HIT_TAKEN_RATING: // Unused since 3.3.5 + // ApplyRatingMod(CR_HIT_TAKEN_MELEE, enchant_amount, apply); + // ApplyRatingMod(CR_HIT_TAKEN_RANGED, enchant_amount, apply); + // ApplyRatingMod(CR_HIT_TAKEN_SPELL, enchant_amount, apply); + // break; + // case ITEM_MOD_CRIT_TAKEN_RATING: // Unused since 3.3.5 + // ApplyRatingMod(CR_CRIT_TAKEN_MELEE, enchant_amount, apply); + // ApplyRatingMod(CR_CRIT_TAKEN_RANGED, enchant_amount, apply); + // ApplyRatingMod(CR_CRIT_TAKEN_SPELL, enchant_amount, apply); + // break; + case ITEM_MOD_RESILIENCE_RATING: + ApplyRatingMod(CR_RESILIENCE_PLAYER_DAMAGE_TAKEN, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u RESILIENCE", enchant_amount); + break; + case ITEM_MOD_HASTE_RATING: + ApplyRatingMod(CR_HASTE_MELEE, enchant_amount, apply); + ApplyRatingMod(CR_HASTE_RANGED, enchant_amount, apply); + ApplyRatingMod(CR_HASTE_SPELL, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u HASTE", enchant_amount); + break; + case ITEM_MOD_EXPERTISE_RATING: + ApplyRatingMod(CR_EXPERTISE, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u EXPERTISE", enchant_amount); + break; + case ITEM_MOD_ATTACK_POWER: + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER, TOTAL_VALUE, float(enchant_amount), apply); + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER_RANGED, TOTAL_VALUE, float(enchant_amount), apply); + TC_LOG_DEBUG("entities.player.items", "+ %u ATTACK_POWER", enchant_amount); + break; + case ITEM_MOD_RANGED_ATTACK_POWER: + HandleStatFlatModifier(UNIT_MOD_ATTACK_POWER_RANGED, TOTAL_VALUE, float(enchant_amount), apply); + TC_LOG_DEBUG("entities.player.items", "+ %u RANGED_ATTACK_POWER", enchant_amount); + break; + case ITEM_MOD_MANA_REGENERATION: + ApplyManaRegenBonus(enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u MANA_REGENERATION", enchant_amount); + break; + // case ITEM_MOD_ARMOR_PENETRATION_RATING: + // ApplyRatingMod(CR_ARMOR_PENETRATION, enchant_amount, apply); + // TC_LOG_DEBUG("entities.player.items", "+ %u ARMOR PENETRATION", enchant_amount); + // break; + case ITEM_MOD_SPELL_POWER: + ApplySpellPowerBonus(enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u SPELL_POWER", enchant_amount); + break; + case ITEM_MOD_HEALTH_REGEN: + ApplyHealthRegenBonus(enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u HEALTH_REGENERATION", enchant_amount); + break; + case ITEM_MOD_SPELL_PENETRATION: + ApplySpellPenetrationBonus(enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u SPELL_PENETRATION", enchant_amount); + break; + case ITEM_MOD_BLOCK_VALUE: + HandleBaseModFlatValue(SHIELD_BLOCK_VALUE, float(enchant_amount), apply); + TC_LOG_DEBUG("entities.player.items", "+ %u BLOCK_VALUE", enchant_amount); + break; + case ITEM_MOD_MASTERY_RATING: + ApplyRatingMod(CR_MASTERY, enchant_amount, apply); + TC_LOG_DEBUG("entities.player.items", "+ %u MASTERY", enchant_amount); + break; + case ITEM_MOD_EXTRA_ARMOR: + HandleStatFlatModifier(UNIT_MOD_ARMOR, BASE_VALUE, float(enchant_amount), apply); + TC_LOG_DEBUG("entities.player.items", "+ %u EXTRA_ARMOR", enchant_amount); + break; + default: + break; + } + break; + } + case ITEM_ENCHANTMENT_TYPE_TOTEM: // Shaman Rockbiter Weapon + { + WeaponAttackType const attackType = Player::GetAttackBySlot(item->GetSlot()); + if (attackType != MAX_ATTACK) + UpdateDamageDoneMods(attackType, apply ? -1 : slot); + break; + } + case ITEM_ENCHANTMENT_TYPE_USE_SPELL: + // processed in Player::CastItemUseSpell + break; + case ITEM_ENCHANTMENT_TYPE_PRISMATIC_SOCKET: + // nothing do.. + break; + default: + TC_LOG_ERROR("entities.player", "Player::ApplyEnchantment: Unknown item enchantment (ID: %u, DisplayType: %u) for player '%s' (%s)", + enchant_id, enchant_display_type, GetName().c_str(), GetGUID().ToString().c_str()); + break; + } + } + } + + // visualize enchantment at player and equipped items + if (slot == PERM_ENCHANTMENT_SLOT) + SetUInt16Value(PLAYER_VISIBLE_ITEM_1_ENCHANTMENT + (item->GetSlot() * 2), 0, apply ? item->GetEnchantmentId(slot) : 0); + + if (slot == TEMP_ENCHANTMENT_SLOT) + SetUInt16Value(PLAYER_VISIBLE_ITEM_1_ENCHANTMENT + (item->GetSlot() * 2), 1, apply ? item->GetEnchantmentId(slot) : 0); + + if (apply_dur) + { + if (apply) + { + // set duration + uint32 duration = item->GetEnchantmentDuration(slot); + if (duration > 0) + AddEnchantmentDuration(item, slot, duration); + } + else + { + // duration == 0 will remove EnchantDuration + AddEnchantmentDuration(item, slot, 0); + } + } +} + +void Player::UpdateSkillEnchantments(uint16 skill_id, uint16 curr_value, uint16 new_value) +{ + for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (m_items[i]) + { + for (uint8 slot = 0; slot < MAX_ENCHANTMENT_SLOT; ++slot) + { + if (slot > PRISMATIC_ENCHANTMENT_SLOT && slot < PROP_ENCHANTMENT_SLOT_0) // not holding enchantment id + continue; + + uint32 ench_id = m_items[i]->GetEnchantmentId(EnchantmentSlot(slot)); + if (!ench_id) + continue; + + SpellItemEnchantmentEntry const* Enchant = sSpellItemEnchantmentStore.LookupEntry(ench_id); + if (!Enchant) + return; + + if (Enchant->RequiredSkillID == skill_id) + { + // Checks if the enchantment needs to be applied or removed + if (curr_value < Enchant->RequiredSkillRank && new_value >= Enchant->RequiredSkillRank) + ApplyEnchantment(m_items[i], EnchantmentSlot(slot), true); + else if (new_value < Enchant->RequiredSkillRank && curr_value >= Enchant->RequiredSkillRank) + ApplyEnchantment(m_items[i], EnchantmentSlot(slot), false); + } + + // If we're dealing with a gem inside a prismatic socket we need to check the prismatic socket requirements + // rather than the gem requirements itself. If the socket has no color it is a prismatic socket. + if ((slot == SOCK_ENCHANTMENT_SLOT || slot == SOCK_ENCHANTMENT_SLOT_2 || slot == SOCK_ENCHANTMENT_SLOT_3) + && !m_items[i]->GetTemplate()->GetSocketColor(slot-SOCK_ENCHANTMENT_SLOT)) + { + SpellItemEnchantmentEntry const* pPrismaticEnchant = sSpellItemEnchantmentStore.LookupEntry(m_items[i]->GetEnchantmentId(PRISMATIC_ENCHANTMENT_SLOT)); + + if (pPrismaticEnchant && pPrismaticEnchant->RequiredSkillID == skill_id) + { + if (curr_value < pPrismaticEnchant->RequiredSkillRank && new_value >= pPrismaticEnchant->RequiredSkillRank) + ApplyEnchantment(m_items[i], EnchantmentSlot(slot), true); + else if (new_value < pPrismaticEnchant->RequiredSkillRank && curr_value >= pPrismaticEnchant->RequiredSkillRank) + ApplyEnchantment(m_items[i], EnchantmentSlot(slot), false); + } + } + } + } + } +} + +void Player::SendEnchantmentDurations() +{ + for (EnchantDurationList::const_iterator itr = m_enchantDuration.begin(); itr != m_enchantDuration.end(); ++itr) + GetSession()->SendItemEnchantTimeUpdate(GetGUID(), itr->item->GetGUID(), itr->slot, uint32(itr->leftduration) / 1000); +} + +void Player::SendItemDurations() +{ + for (ItemDurationList::const_iterator itr = m_itemDuration.begin(); itr != m_itemDuration.end(); ++itr) + (*itr)->SendTimeUpdate(this); +} + +void Player::SendNewItem(Item* item, uint32 count, bool received, bool created, bool broadcast, bool sendChatMessage) +{ + if (!item) // prevent crash + return; + + // last check 2.0.10 + WorldPacket data(SMSG_ITEM_PUSH_RESULT, (8+4+4+4+1+4+4+4+4+4)); + data << uint64(GetGUID()); // player GUID + data << uint32(received); // 0=looted, 1=from npc + data << uint32(created); // 0=received, 1=created + data << uint32(sendChatMessage); // bool print message to chat + data << uint8(item->GetBagSlot()); // bagslot + // item slot, but when added to stack: 0xFFFFFFFF + data << uint32((item->GetCount() == count) ? item->GetSlot() : -1); + data << uint32(item->GetEntry()); // item id + data << uint32(item->GetItemSuffixFactor()); // SuffixFactor + data << int32(item->GetItemRandomPropertyId()); // random item property id + data << uint32(count); // count of items + data << uint32(GetItemCount(item->GetEntry())); // count of items in inventory + + if (broadcast && GetGroup()) + GetGroup()->BroadcastPacket(&data, true); + else + SendDirectMessage(&data); +} + +/*********************************************************/ +/*** GOSSIP SYSTEM ***/ +/*********************************************************/ + +void Player::PrepareGossipMenu(WorldObject* source, uint32 menuId /*= 0*/, bool showQuests /*= false*/) +{ + PlayerTalkClass->ClearMenus(); + PlayerTalkClass->GetGossipMenu().SetMenuId(menuId); + + GossipMenuItemsMapBounds menuItemBounds = sObjectMgr->GetGossipMenuItemsMapBounds(menuId); + + // if default menuId and no menu options exist for this, use options from default options + if (menuItemBounds.first == menuItemBounds.second && menuId == GetGossipMenuForSource(source)) + menuItemBounds = sObjectMgr->GetGossipMenuItemsMapBounds(0); + + uint32 npcflags = 0; + + if (source->GetTypeId() == TYPEID_UNIT) + { + npcflags = source->GetUInt32Value(UNIT_NPC_FLAGS); + if (showQuests && npcflags & UNIT_NPC_FLAG_QUESTGIVER) + PrepareQuestMenu(source->GetGUID()); + } + else if (source->GetTypeId() == TYPEID_GAMEOBJECT) + if (showQuests && source->ToGameObject()->GetGoType() == GAMEOBJECT_TYPE_QUESTGIVER) + PrepareQuestMenu(source->GetGUID()); + + for (GossipMenuItemsContainer::const_iterator itr = menuItemBounds.first; itr != menuItemBounds.second; ++itr) + { + bool canTalk = true; + if (!sConditionMgr->IsObjectMeetToConditions(this, source, itr->second.Conditions)) + continue; + + if (Creature* creature = source->ToCreature()) + { + if (!(itr->second.OptionNpcFlag & npcflags)) + continue; + + switch (itr->second.OptionType) + { + case GOSSIP_OPTION_SPIRITHEALER: + if (!isDead()) + canTalk = false; + break; + case GOSSIP_OPTION_LEARNDUALSPEC: + case GOSSIP_OPTION_DUALSPEC_INFO: + if (!(GetSpecsCount() == 1 && creature->CanResetTalents(this) && !(getLevel() < sWorld->getIntConfig(CONFIG_MIN_DUALSPEC_LEVEL)))) + canTalk = false; + break; + case GOSSIP_OPTION_UNLEARNTALENTS: + if (!creature->CanResetTalents(this)) + canTalk = false; + break; + case GOSSIP_OPTION_UNLEARNPETTALENTS: + if (!GetPet() || GetPet()->getPetType() != HUNTER_PET || GetPet()->m_spells.size() <= 1 || !creature->CanResetTalents(this)) + canTalk = false; + break; + case GOSSIP_OPTION_TAXIVENDOR: + if (GetSession()->SendLearnNewTaxiNode(creature)) + return; + break; + case GOSSIP_OPTION_BATTLEFIELD: + if (!creature->isCanInteractWithBattleMaster(this, false)) + canTalk = false; + break; + case GOSSIP_OPTION_STABLEPET: + if (getClass() != CLASS_HUNTER) + canTalk = false; + break; + case GOSSIP_OPTION_ARMORER: + case GOSSIP_OPTION_QUESTGIVER: + canTalk = false; + break; + case GOSSIP_OPTION_TRAINER: + if (creature->GetCreatureTemplate()->trainer_class && !creature->IsClassTrainerOf(this)) + canTalk = false; + break; + case GOSSIP_OPTION_GOSSIP: + case GOSSIP_OPTION_SPIRITGUIDE: + case GOSSIP_OPTION_INNKEEPER: + case GOSSIP_OPTION_BANKER: + case GOSSIP_OPTION_PETITIONER: + case GOSSIP_OPTION_TABARDDESIGNER: + case GOSSIP_OPTION_AUCTIONEER: + case GOSSIP_OPTION_VENDOR: + break; // no checks + case GOSSIP_OPTION_OUTDOORPVP: + if (!sOutdoorPvPMgr->CanTalkTo(this, creature, itr->second)) + canTalk = false; + break; + default: + TC_LOG_ERROR("sql.sql", "Creature entry %u has unknown gossip option %u for menu %u.", creature->GetEntry(), itr->second.OptionType, itr->second.MenuID); + canTalk = false; + break; + } + } + else if (GameObject* go = source->ToGameObject()) + { + switch (itr->second.OptionType) + { + case GOSSIP_OPTION_GOSSIP: + if (go->GetGoType() != GAMEOBJECT_TYPE_QUESTGIVER && go->GetGoType() != GAMEOBJECT_TYPE_GOOBER) + canTalk = false; + break; + default: + canTalk = false; + break; + } + } + + if (canTalk) + { + std::string strOptionText, strBoxText; + BroadcastText const* optionBroadcastText = sObjectMgr->GetBroadcastText(itr->second.OptionBroadcastTextID); + BroadcastText const* boxBroadcastText = sObjectMgr->GetBroadcastText(itr->second.BoxBroadcastTextID); + LocaleConstant locale = GetSession()->GetSessionDbLocaleIndex(); + + if (optionBroadcastText) + strOptionText = optionBroadcastText->GetText(locale, getGender()); + else + strOptionText = itr->second.OptionText; + + if (boxBroadcastText) + strBoxText = boxBroadcastText->GetText(locale, getGender()); + else + strBoxText = itr->second.BoxText; + + if (locale != DEFAULT_LOCALE) + { + if (!optionBroadcastText) + { + /// Find localizations from database. + if (GossipMenuItemsLocale const* gossipMenuLocale = sObjectMgr->GetGossipMenuItemsLocale(menuId, itr->second.OptionID)) + ObjectMgr::GetLocaleString(gossipMenuLocale->OptionText, locale, strOptionText); + } + + if (!boxBroadcastText) + { + /// Find localizations from database. + if (GossipMenuItemsLocale const* gossipMenuLocale = sObjectMgr->GetGossipMenuItemsLocale(menuId, itr->second.OptionID)) + ObjectMgr::GetLocaleString(gossipMenuLocale->BoxText, locale, strBoxText); + } + } + + PlayerTalkClass->GetGossipMenu().AddMenuItem(itr->second.OptionID, itr->second.OptionIcon, strOptionText, 0, itr->second.OptionType, strBoxText, itr->second.BoxMoney, itr->second.BoxCoded); + PlayerTalkClass->GetGossipMenu().AddGossipMenuItemData(itr->second.OptionID, itr->second.ActionMenuID, itr->second.ActionPoiID); + } + } +} + +void Player::SendPreparedGossip(WorldObject* source) +{ + if (!source) + return; + + if (source->GetTypeId() == TYPEID_UNIT) + { + // in case no gossip flag and quest menu not empty, open quest menu (client expect gossip menu with this flag) + if (!source->ToCreature()->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_GOSSIP) && !PlayerTalkClass->GetQuestMenu().Empty()) + { + SendPreparedQuest(source->GetGUID()); + return; + } + } + else if (source->GetTypeId() == TYPEID_GAMEOBJECT) + { + // probably need to find a better way here + if (!PlayerTalkClass->GetGossipMenu().GetMenuId() && !PlayerTalkClass->GetQuestMenu().Empty()) + { + SendPreparedQuest(source->GetGUID()); + return; + } + } + + // in case non empty gossip menu (that not included quests list size) show it + // (quest entries from quest menu will be included in list) + + uint32 textId = GetGossipTextId(source); + + if (uint32 menuId = PlayerTalkClass->GetGossipMenu().GetMenuId()) + textId = GetGossipTextId(menuId, source); + + PlayerTalkClass->SendGossipMenu(textId, source->GetGUID()); +} + +void Player::OnGossipSelect(WorldObject* source, uint32 gossipListId, uint32 menuId) +{ + GossipMenu& gossipMenu = PlayerTalkClass->GetGossipMenu(); + + // if not same, then something funky is going on + if (menuId != gossipMenu.GetMenuId()) + return; + + GossipMenuItem const* item = gossipMenu.GetItem(gossipListId); + if (!item) + return; + + uint32 gossipOptionId = item->OptionType; + ObjectGuid guid = source->GetGUID(); + + if (source->GetTypeId() == TYPEID_GAMEOBJECT) + { + if (gossipOptionId > GOSSIP_OPTION_QUESTGIVER) + { + TC_LOG_ERROR("entities.player", "Player '%s' (%s) requests invalid gossip option for GameObject (Entry: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), source->GetEntry()); + return; + } + } + + GossipMenuItemData const* menuItemData = gossipMenu.GetItemData(gossipListId); + if (!menuItemData) + return; + + int32 cost = int32(item->BoxMoney); + if (!HasEnoughMoney(int64(cost))) + { + SendBuyError(BUY_ERR_NOT_ENOUGHT_MONEY, nullptr, 0, 0); + PlayerTalkClass->SendCloseGossip(); + return; + } + + switch (gossipOptionId) + { + case GOSSIP_OPTION_GOSSIP: + case GOSSIP_OPTION_DUALSPEC_INFO: + { + if (menuItemData->GossipActionPoi) + PlayerTalkClass->SendPointOfInterest(menuItemData->GossipActionPoi); + + if (menuItemData->GossipActionMenuId) + { + PrepareGossipMenu(source, menuItemData->GossipActionMenuId); + SendPreparedGossip(source); + } + + break; + } + case GOSSIP_OPTION_OUTDOORPVP: + sOutdoorPvPMgr->HandleGossipOption(this, source->ToCreature(), gossipListId); + break; + case GOSSIP_OPTION_SPIRITHEALER: + if (isDead()) + source->ToCreature()->CastSpell(source->ToCreature(), 17251, GetGUID()); + break; + case GOSSIP_OPTION_QUESTGIVER: + PrepareQuestMenu(guid); + SendPreparedQuest(guid); + break; + case GOSSIP_OPTION_VENDOR: + case GOSSIP_OPTION_ARMORER: + GetSession()->SendListInventory(guid); + break; + case GOSSIP_OPTION_STABLEPET: + GetSession()->SendStablePet(guid); + break; + case GOSSIP_OPTION_TRAINER: + GetSession()->SendTrainerList(source->ToCreature(), sObjectMgr->GetCreatureTrainerForGossipOption(source->GetEntry(), menuId, gossipListId)); + break; + case GOSSIP_OPTION_LEARNDUALSPEC: + if (GetSpecsCount() == 1 && getLevel() >= sWorld->getIntConfig(CONFIG_MIN_DUALSPEC_LEVEL)) + { + // Cast spells that teach dual spec + // Both are also ImplicitTarget self and must be cast by player + CastSpell(this, 63680, GetGUID()); + CastSpell(this, 63624, GetGUID()); + + PrepareGossipMenu(source, menuItemData->GossipActionMenuId); + SendPreparedGossip(source); + } + break; + case GOSSIP_OPTION_UNLEARNTALENTS: + PlayerTalkClass->SendCloseGossip(); + SendTalentWipeConfirm(guid); + break; + case GOSSIP_OPTION_UNLEARNPETTALENTS: + PlayerTalkClass->SendCloseGossip(); + ResetPetTalents(); + break; + case GOSSIP_OPTION_TAXIVENDOR: + GetSession()->SendTaxiMenu(source->ToCreature()); + break; + case GOSSIP_OPTION_INNKEEPER: + PlayerTalkClass->SendCloseGossip(); + SetBindPoint(guid); + break; + case GOSSIP_OPTION_BANKER: + GetSession()->SendShowBank(guid); + break; + case GOSSIP_OPTION_PETITIONER: + PlayerTalkClass->SendCloseGossip(); + GetSession()->SendPetitionShowList(guid); + break; + case GOSSIP_OPTION_TABARDDESIGNER: + PlayerTalkClass->SendCloseGossip(); + GetSession()->SendTabardVendorActivate(guid); + break; + case GOSSIP_OPTION_AUCTIONEER: + GetSession()->SendAuctionHello(guid, source->ToCreature()); + break; + case GOSSIP_OPTION_SPIRITGUIDE: + PrepareGossipMenu(source); + SendPreparedGossip(source); + break; + case GOSSIP_OPTION_BATTLEFIELD: + { + BattlegroundTypeId bgTypeId = sBattlegroundMgr->GetBattleMasterBG(source->GetEntry()); + + if (bgTypeId == BATTLEGROUND_TYPE_NONE) + { + TC_LOG_ERROR("entities.player", "Player '%s' (%s) requested battlegroundlist from an invalid creature (%s)", + GetName().c_str(), GetGUID().ToString().c_str(), source->GetGUID().ToString().c_str()); + return; + } + + sBattlegroundMgr->SendBattlegroundList(source->GetGUID(), this, BattlegroundTypeId(bgTypeId)); + break; + } + } + + ModifyMoney(-cost); +} + +uint32 Player::GetGossipTextId(WorldObject* source) +{ + if (!source) + return DEFAULT_GOSSIP_MESSAGE; + + return GetGossipTextId(GetGossipMenuForSource(source), source); +} + +uint32 Player::GetGossipTextId(uint32 menuId, WorldObject* source) +{ + uint32 textId = DEFAULT_GOSSIP_MESSAGE; + + if (!menuId) + return textId; + + GossipMenusMapBounds menuBounds = sObjectMgr->GetGossipMenusMapBounds(menuId); + + for (GossipMenusContainer::const_iterator itr = menuBounds.first; itr != menuBounds.second; ++itr) + { + // continue if only checks menuid instead of text + if (!itr->second.TextID) + continue; + + if (sConditionMgr->IsObjectMeetToConditions(this, source, itr->second.Conditions)) + textId = itr->second.TextID; + } + + return textId; +} + +uint32 Player::GetGossipMenuForSource(WorldObject* source) +{ + switch (source->GetTypeId()) + { + case TYPEID_UNIT: + { + uint32 menuIdToShow = source->ToCreature()->GetGossipMenuId(); + + // if menu id is set by script + if (menuIdToShow) + return menuIdToShow; + + // otherwise pick from db based on conditions + for (uint32 menuId : source->ToCreature()->GetCreatureTemplate()->GossipMenuIds) + { + GossipMenusMapBounds menuBounds = sObjectMgr->GetGossipMenusMapBounds(menuId); + + for (GossipMenusContainer::const_iterator itr = menuBounds.first; itr != menuBounds.second; ++itr) + { + if (!sConditionMgr->IsObjectMeetToConditions(this, source, itr->second.Conditions)) + continue; + + menuIdToShow = menuId; + } + } + return menuIdToShow; + } + case TYPEID_GAMEOBJECT: + return source->ToGameObject()->GetGOInfo()->GetGossipMenuId(); + default: + break; + } + + return 0; +} + +/*********************************************************/ +/*** QUEST SYSTEM ***/ +/*********************************************************/ + +void Player::PrepareQuestMenu(ObjectGuid guid) +{ + QuestRelationResult objectQR; + QuestRelationResult objectQIR; + + // pets also can have quests + Creature* creature = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, guid); + if (creature) + { + objectQR = sObjectMgr->GetCreatureQuestRelations(creature->GetEntry()); + objectQIR = sObjectMgr->GetCreatureQuestInvolvedRelations(creature->GetEntry()); + } + else + { + //we should obtain map pointer from GetMap() in 99% of cases. Special case + //only for quests which cast teleport spells on player + Map* _map = IsInWorld() ? GetMap() : sMapMgr->FindMap(GetMapId(), GetInstanceId()); + ASSERT(_map); + GameObject* gameObject = _map->GetGameObject(guid); + if (gameObject) + { + objectQR = sObjectMgr->GetGOQuestRelations(gameObject->GetEntry()); + objectQIR = sObjectMgr->GetGOQuestInvolvedRelations(gameObject->GetEntry()); + } + else + return; + } + + QuestMenu &qm = PlayerTalkClass->GetQuestMenu(); + qm.ClearMenu(); + + for (uint32 quest_id : objectQIR) + { + QuestStatus status = GetQuestStatus(quest_id); + if (status == QUEST_STATUS_COMPLETE) + qm.AddMenuItem(quest_id, 4); + else if (status == QUEST_STATUS_INCOMPLETE) + qm.AddMenuItem(quest_id, 4); + //else if (status == QUEST_STATUS_AVAILABLE) + // qm.AddMenuItem(quest_id, 2); + } + + for (uint32 quest_id : objectQR) + { + Quest const* quest = sObjectMgr->GetQuestTemplate(quest_id); + if (!quest) + continue; + + if (!CanTakeQuest(quest, false)) + continue; + + if (quest->IsTurnIn() && (!quest->IsRepeatable() || quest->IsDaily() || quest->IsWeekly() || quest->IsMonthly())) + qm.AddMenuItem(quest_id, 0); + else if (quest->IsTurnIn()) + qm.AddMenuItem(quest_id, 4); + else if (GetQuestStatus(quest_id) == QUEST_STATUS_NONE) + qm.AddMenuItem(quest_id, 2); + } +} + +void Player::SendPreparedQuest(ObjectGuid guid) +{ + QuestMenu& questMenu = PlayerTalkClass->GetQuestMenu(); + if (questMenu.Empty()) + return; + + // single element case + if (questMenu.GetMenuItemCount() == 1) + { + QuestMenuItem const& qmi0 = questMenu.GetItem(0); + uint32 questId = qmi0.QuestId; + + // Auto open -- maybe also should verify there is no greeting + if (Quest const* quest = sObjectMgr->GetQuestTemplate(questId)) + { + if (qmi0.QuestIcon == 4) + PlayerTalkClass->SendQuestGiverRequestItems(quest, guid, CanRewardQuest(quest, false), true); + // Send completable on repeatable and autoCompletable quest if player don't have quest + /// @todo verify if check for !quest->IsDaily() is really correct (possibly not) + else + { + Object* object = ObjectAccessor::GetObjectByTypeMask(*this, guid, TYPEMASK_UNIT | TYPEMASK_GAMEOBJECT | TYPEMASK_ITEM); + if (!object || (!object->hasQuest(questId) && !object->hasInvolvedQuest(questId))) + { + PlayerTalkClass->SendCloseGossip(); + return; + } + + if (quest->IsAutoAccept() && CanAddQuest(quest, true) && CanTakeQuest(quest, true)) + AddQuestAndCheckCompletion(quest, object); + + if (quest->IsTurnIn() && quest->IsRepeatable() && !quest->IsDailyOrWeekly()) + PlayerTalkClass->SendQuestGiverRequestItems(quest, guid, CanCompleteRepeatableQuest(quest), true); + else + { + SetPopupQuestId(0); + PlayerTalkClass->SendQuestGiverQuestDetails(quest, guid, true, false); + } + } + } + } + // multiple entries + else + { + QEmote qe; + qe._Delay = 0; + qe._Emote = 0; + std::string title = ""; + + // need pet case for some quests + Creature* creature = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, guid); + if (creature) + { + uint32 textid = GetGossipTextId(creature); + GossipText const* gossiptext = sObjectMgr->GetGossipText(textid); + if (!gossiptext) + { + qe._Delay = 0; //TEXTEMOTE_MESSAGE; //zyg: player emote + qe._Emote = 0; //TEXTEMOTE_HELLO; //zyg: NPC emote + title.clear(); + } + else + { + qe = gossiptext->Options[0].Emotes[0]; + + if (!gossiptext->Options[0].Text_0.empty()) + { + title = gossiptext->Options[0].Text_0; + + LocaleConstant localeConstant = GetSession()->GetSessionDbLocaleIndex(); + if (localeConstant != LOCALE_enUS) + if (NpcTextLocale const* nl = sObjectMgr->GetNpcTextLocale(textid)) + ObjectMgr::GetLocaleString(nl->Text_0[0], localeConstant, title); + } + else + { + title = gossiptext->Options[0].Text_1; + + LocaleConstant localeConstant = GetSession()->GetSessionDbLocaleIndex(); + if (localeConstant != LOCALE_enUS) + if (NpcTextLocale const* nl = sObjectMgr->GetNpcTextLocale(textid)) + ObjectMgr::GetLocaleString(nl->Text_1[0], localeConstant, title); + } + } + } + PlayerTalkClass->SendQuestGiverQuestList(qe, title, guid); + } +} + +bool Player::IsActiveQuest(uint32 quest_id) const +{ + return m_QuestStatus.find(quest_id) != m_QuestStatus.end(); +} + +Quest const* Player::GetNextQuest(Object const* questGiver, Quest const* quest) const +{ + uint32 nextQuestID = quest->GetNextQuestInChain(); + if (!nextQuestID) + return nullptr; + + if (questGiver == this) + { + if (!quest->HasFlag(QUEST_FLAGS_AUTO_COMPLETE)) + return nullptr; + + return sObjectMgr->GetQuestTemplate(nextQuestID); + } + + //we should obtain map pointer from GetMap() in 99% of cases. Special case + //only for quests which cast teleport spells on player + if (WorldObject const* worldObjectQuestGiver = dynamic_cast(questGiver)) + if (!IsInMap(worldObjectQuestGiver)) + return nullptr; + + if (questGiver && !questGiver->hasQuest(nextQuestID)) + return nullptr; + + return sObjectMgr->GetQuestTemplate(nextQuestID); +} + +bool Player::CanSeeStartQuest(Quest const* quest) const +{ + if (!DisableMgr::IsDisabledFor(DISABLE_TYPE_QUEST, quest->GetQuestId(), this) && SatisfyQuestClass(quest, false) && SatisfyQuestRace(quest, false) && + SatisfyQuestSkill(quest, false) && SatisfyQuestExclusiveGroup(quest, false) && SatisfyQuestReputation(quest, false) && + SatisfyQuestDependentQuests(quest, false) && + SatisfyQuestDay(quest, false) && SatisfyQuestWeek(quest, false) && + SatisfyQuestMonth(quest, false) && SatisfyQuestSeasonal(quest, false)) + { + return getLevel() + sWorld->getIntConfig(CONFIG_QUEST_HIGH_LEVEL_HIDE_DIFF) >= quest->GetMinLevel(); + } + + return false; +} + +bool Player::CanTakeQuest(Quest const* quest, bool msg) const +{ + return !DisableMgr::IsDisabledFor(DISABLE_TYPE_QUEST, quest->GetQuestId(), this) + && SatisfyQuestStatus(quest, msg) && SatisfyQuestExclusiveGroup(quest, msg) + && SatisfyQuestClass(quest, msg) && SatisfyQuestRace(quest, msg) && SatisfyQuestLevel(quest, msg) + && SatisfyQuestSkill(quest, msg) && SatisfyQuestReputation(quest, msg) + && SatisfyQuestDependentQuests(quest, msg) && SatisfyQuestTimed(quest, msg) + && SatisfyQuestDay(quest, msg) && SatisfyQuestWeek(quest, msg) + && SatisfyQuestMonth(quest, msg) && SatisfyQuestSeasonal(quest, msg) + && SatisfyQuestConditions(quest, msg); +} + +bool Player::CanAddQuest(Quest const* quest, bool msg) const +{ + if (!SatisfyQuestLog(msg)) + return false; + + uint32 srcitem = quest->GetSrcItemId(); + if (srcitem > 0) + { + uint32 count = quest->GetSrcItemCount(); + ItemPosCountVec dest; + InventoryResult msg2 = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, srcitem, count); + + // player already have max number (in most case 1) source item, no additional item needed and quest can be added. + if (msg2 == EQUIP_ERR_ITEM_MAX_COUNT) + return true; + if (msg2 != EQUIP_ERR_OK) + { + SendEquipError(msg2, nullptr, nullptr, srcitem); + return false; + } + } + return true; +} + +bool Player::CanCompleteQuest(uint32 quest_id) +{ + if (quest_id) + { + Quest const* qInfo = sObjectMgr->GetQuestTemplate(quest_id); + if (!qInfo) + return false; + + if (!qInfo->IsRepeatable() && GetQuestRewardStatus(quest_id)) + return false; // not allow re-complete quest + + // auto complete quest + if (qInfo->IsTurnIn() && CanTakeQuest(qInfo, false)) + return true; + + QuestStatusMap::iterator itr = m_QuestStatus.find(quest_id); + if (itr == m_QuestStatus.end()) + return false; + + QuestStatusData &q_status = itr->second; + + if (q_status.Status == QUEST_STATUS_INCOMPLETE) + { + if (qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_DELIVER)) + { + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++) + { + if (qInfo->RequiredItemCount[i]!= 0 && q_status.ItemCount[i] < qInfo->RequiredItemCount[i]) + return false; + } + } + + if (qInfo->HasSpecialFlag(QuestSpecialFlags(QUEST_SPECIAL_FLAGS_KILL | QUEST_SPECIAL_FLAGS_CAST | QUEST_SPECIAL_FLAGS_SPEAKTO))) + { + for (uint8 i = 0; i < QUEST_OBJECTIVES_COUNT; i++) + { + if (qInfo->RequiredNpcOrGo[i] == 0) + continue; + + if (qInfo->RequiredNpcOrGoCount[i] != 0 && q_status.CreatureOrGOCount[i] < qInfo->RequiredNpcOrGoCount[i]) + return false; + } + } + + if (qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_PLAYER_KILL)) + if (qInfo->GetPlayersSlain() != 0 && q_status.PlayerCount < qInfo->GetPlayersSlain()) + return false; + + if ((qInfo->HasFlag(QUEST_FLAGS_COMPLETION_EVENT) || qInfo->HasFlag(QUEST_FLAGS_COMPLETION_AREA_TRIGGER)) && !q_status.Explored) + return false; + + if (qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED) && q_status.Timer == 0) + return false; + + int32 rewardMoney = qInfo->GetMoneyReward(this); + if (rewardMoney < 0) + if (!HasEnoughMoney(-int64(rewardMoney))) + return false; + + uint32 repFacId = qInfo->GetRepObjectiveFaction(); + if (repFacId && GetReputationMgr().GetReputation(repFacId) < qInfo->GetRepObjectiveValue()) + return false; + + return true; + } + } + return false; +} + +bool Player::CanCompleteRepeatableQuest(Quest const* quest) +{ + // Solve problem that player don't have the quest and try complete it. + // if repeatable she must be able to complete event if player don't have it. + // Seem that all repeatable quest are DELIVER Flag so, no need to add more. + if (!CanTakeQuest(quest, false)) + return false; + + if (quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_DELIVER)) + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++) + if (quest->RequiredItemId[i] && quest->RequiredItemCount[i] && !HasItemCount(quest->RequiredItemId[i], quest->RequiredItemCount[i])) + return false; + + if (!CanRewardQuest(quest, false)) + return false; + + return true; +} + +bool Player::CanRewardQuest(Quest const* quest, bool msg) +{ + // not auto complete quest and not completed quest (only cheating case, then ignore without message) + if (!quest->IsDFQuest() && !quest->IsTurnIn() && GetQuestStatus(quest->GetQuestId()) != QUEST_STATUS_COMPLETE) + return false; + + // daily quest can't be rewarded (25 daily quest already completed) + if (!SatisfyQuestDay(quest, true) || !SatisfyQuestWeek(quest, true) || !SatisfyQuestMonth(quest, true) || !SatisfyQuestSeasonal(quest, true)) + return false; + + // rewarded and not repeatable quest (only cheating case, then ignore without message) + if (GetQuestRewardStatus(quest->GetQuestId())) + return false; + + // prevent receive reward with quest items in bank + if (quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_DELIVER)) + { + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++) + { + if (quest->RequiredItemCount[i]!= 0 && + GetItemCount(quest->RequiredItemId[i]) < quest->RequiredItemCount[i]) + { + if (msg) + SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr, quest->RequiredItemId[i]); + return false; + } + } + } + + for (uint8 i = 0; i < QUEST_REQUIRED_CURRENCY_COUNT; i++) + if (quest->RequiredCurrencyId[i] && !HasCurrency(quest->RequiredCurrencyId[i], quest->RequiredCurrencyCount[i])) + return false; + + // prevent receive reward with low money and GetRewOrReqMoney() < 0 + int32 rewardMoney = quest->GetMoneyReward(this); + if (rewardMoney < 0) + if (!HasEnoughMoney(-int64(rewardMoney))) + return false; + + // dungeon finder quests cannot be rewarded when hit weekly currency limit + if (quest->IsDFQuest()) + { + for (uint8 i = 0; i < QUEST_REWARD_CURRENCY_COUNT; i++) + { + if (CurrencyTypesEntry const* currency = sCurrencyTypesStore.LookupEntry(quest->RewardCurrencyId[i])) + { + uint32 weekCap = GetCurrencyWeekCap(currency); + if (weekCap > 0 && GetCurrencyOnWeek(quest->RewardCurrencyId[i], false) >= weekCap) + return false; + } + } + } + + return true; +} + +void Player::AddQuestAndCheckCompletion(Quest const* quest, Object* questGiver) +{ + AddQuest(quest, questGiver); + + if (CanCompleteQuest(quest->GetQuestId())) + CompleteQuest(quest->GetQuestId()); + + if (!questGiver) + return; + + switch (questGiver->GetTypeId()) + { + case TYPEID_UNIT: + PlayerTalkClass->ClearMenus(); + questGiver->ToCreature()->AI()->QuestAccept(this, quest); + break; + case TYPEID_ITEM: + case TYPEID_CONTAINER: + { + Item* item = static_cast(questGiver); + sScriptMgr->OnQuestAccept(this, item, quest); + + // There are two cases where the source item is not destroyed when the quest is accepted: + // - It is required to finish the quest, and is an unique item + // - It is the same item present in the source item field (item that would be given on quest accept) + bool destroyItem = true; + + for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) + { + if (quest->RequiredItemId[i] == item->GetEntry() && item->GetTemplate()->GetMaxCount() > 0) + { + destroyItem = false; + break; + } + } + + if (quest->GetSrcItemId() == item->GetEntry()) + destroyItem = false; + + if (destroyItem) + DestroyItem(item->GetBagSlot(), item->GetSlot(), true); + + break; + } + case TYPEID_GAMEOBJECT: + PlayerTalkClass->ClearMenus(); + questGiver->ToGameObject()->AI()->QuestAccept(this, quest); + break; + default: + break; + } +} + +bool Player::CanRewardQuest(Quest const* quest, uint32 reward, bool msg) +{ + // prevent receive reward with quest items in bank or for not completed quest + if (!CanRewardQuest(quest, msg)) + return false; + + ItemPosCountVec dest; + if (quest->GetRewChoiceItemsCount() > 0) + { + if (quest->RewardChoiceItemId[reward]) + { + InventoryResult res = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, quest->RewardChoiceItemId[reward], quest->RewardChoiceItemCount[reward]); + if (res != EQUIP_ERR_OK) + { + if (msg) + SendQuestFailed(quest->GetQuestId(), res); + + return false; + } + } + } + + if (quest->GetRewItemsCount() > 0) + { + for (uint32 i = 0; i < quest->GetRewItemsCount(); ++i) + { + if (quest->RewardItemId[i]) + { + InventoryResult res = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, quest->RewardItemId[i], quest->RewardItemIdCount[i]); + if (res != EQUIP_ERR_OK) + { + if (msg) + SendQuestFailed(quest->GetQuestId(), res); + + return false; + } + } + } + } + + return true; +} + +void Player::AddQuest(Quest const* quest, Object* questGiver) +{ + uint16 log_slot = FindQuestSlot(0); + + if (log_slot >= MAX_QUEST_LOG_SIZE) // Player does not have any free slot in the quest log + return; + + uint32 quest_id = quest->GetQuestId(); + + // if not exist then created with set uState == NEW and rewarded=false + QuestStatusData& questStatusData = m_QuestStatus[quest_id]; + + // check for repeatable quests status reset + questStatusData.Status = QUEST_STATUS_INCOMPLETE; + questStatusData.Explored = false; + + if (quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_DELIVER)) + { + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) + questStatusData.ItemCount[i] = 0; + } + + if (quest->HasSpecialFlag(QuestSpecialFlags(QUEST_SPECIAL_FLAGS_KILL | QUEST_SPECIAL_FLAGS_CAST | QUEST_SPECIAL_FLAGS_SPEAKTO))) + { + for (uint8 i = 0; i < QUEST_OBJECTIVES_COUNT; ++i) + questStatusData.CreatureOrGOCount[i] = 0; + } + + if (quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_PLAYER_KILL)) + questStatusData.PlayerCount = 0; + + GiveQuestSourceItem(quest); + AdjustQuestReqItemCount(quest, questStatusData); + + if (quest->GetRepObjectiveFaction()) + if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(quest->GetRepObjectiveFaction())) + GetReputationMgr().SetVisible(factionEntry); + + if (quest->GetRepObjectiveFaction2()) + if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(quest->GetRepObjectiveFaction2())) + GetReputationMgr().SetVisible(factionEntry); + + uint32 qtime = 0; + if (quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED)) + { + uint32 timeAllowed = quest->GetTimeAllowed(); + + // shared timed quest + if (questGiver && questGiver->GetTypeId() == TYPEID_PLAYER) + timeAllowed = questGiver->ToPlayer()->getQuestStatusMap()[quest_id].Timer / IN_MILLISECONDS; + + AddTimedQuest(quest_id); + questStatusData.Timer = timeAllowed * IN_MILLISECONDS; + qtime = static_cast(GameTime::GetGameTime()) + timeAllowed; + } + else + questStatusData.Timer = 0; + + if (quest->HasFlag(QUEST_FLAGS_FLAGS_PVP)) + { + pvpInfo.IsHostile = true; + UpdatePvPState(); + } + + if (quest->GetSrcSpell() > 0) + { + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(quest->GetSrcSpell()); + Unit* caster = this; + if (questGiver && questGiver->isType(TYPEMASK_UNIT) && !quest->HasFlag(QUEST_FLAGS_PLAYER_CAST_ACCEPT) && !spellInfo->HasTargetType(TARGET_UNIT_CASTER) && !spellInfo->HasTargetType(TARGET_DEST_CASTER_SUMMON)) + if (Unit* unit = questGiver->ToUnit()) + caster = unit; + + caster->CastSpell(this, spellInfo->Id, CastSpellExtraArgs(TRIGGERED_FULL_MASK)); + } + + SetQuestSlot(log_slot, quest_id, qtime); + + m_QuestStatusSave[quest_id] = QUEST_DEFAULT_SAVE_TYPE; + + StartAchievementCriteria(AchievementCriteriaStartEvent::AcceptQuest, quest_id); + + SendQuestUpdate(quest_id); + + SendQuestGiverStatusMultiple(); + + if (sWorld->getBoolConfig(CONFIG_QUEST_ENABLE_QUEST_TRACKER)) // check if Quest Tracker is enabled + { + // prepare Quest Tracker data + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_QUEST_TRACK); + stmt->setUInt32(0, quest_id); + stmt->setUInt32(1, GetGUID().GetCounter()); + stmt->setString(2, GitRevision::GetHash()); + stmt->setString(3, GitRevision::GetDate()); + + // add to Quest Tracker + CharacterDatabase.Execute(stmt); + } + + sScriptMgr->OnQuestStatusChange(this, quest_id); +} + +void Player::CompleteQuest(uint32 quest_id) +{ + if (quest_id) + { + SetQuestStatus(quest_id, QUEST_STATUS_COMPLETE); + + uint16 log_slot = FindQuestSlot(quest_id); + if (log_slot < MAX_QUEST_LOG_SIZE) + SetQuestSlotState(log_slot, QUEST_STATE_COMPLETE); + + if (Quest const* qInfo = sObjectMgr->GetQuestTemplate(quest_id)) + if (qInfo->HasFlag(QUEST_FLAGS_TRACKING_EVENT)) + RewardQuest(qInfo, 0, this, false); + } + + if (sWorld->getBoolConfig(CONFIG_QUEST_ENABLE_QUEST_TRACKER)) // check if Quest Tracker is enabled + { + // prepare Quest Tracker datas + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_QUEST_TRACK_COMPLETE_TIME); + stmt->setUInt32(0, quest_id); + stmt->setUInt32(1, GetGUID().GetCounter()); + + // add to Quest Tracker + CharacterDatabase.Execute(stmt); + } +} + +void Player::IncompleteQuest(uint32 quest_id) +{ + if (quest_id) + { + SetQuestStatus(quest_id, QUEST_STATUS_INCOMPLETE); + + uint16 log_slot = FindQuestSlot(quest_id); + if (log_slot < MAX_QUEST_LOG_SIZE) RemoveQuestSlotState(log_slot, QUEST_STATE_COMPLETE); + } +} +int32 Player::GetQuestMoneyReward(Quest const* quest) const +{ + int32 moneyReward = quest->GetMoneyReward(this) * sWorld->getRate(RATE_MONEY_QUEST); + if (moneyReward < 0) + return moneyReward; + + if (IsMaxLevel()) + moneyReward += quest->GetRewMoneyMaxLevel(this) * sWorld->getRate(RATE_MONEY_MAX_LEVEL_QUEST); + + return moneyReward; +} + +uint32 Player::GetQuestXPReward(Quest const* quest) +{ + bool rewarded = IsQuestRewarded(quest->GetQuestId()) && !quest->IsDFQuest(); + + // Not give XP in case already completed once repeatable quest + if (rewarded) + return 0; + + uint32 XP = quest->XPValue(this) * sWorld->getRate(RATE_XP_QUEST); + + // handle SPELL_AURA_MOD_XP_QUEST_PCT auras + Unit::AuraEffectList const& ModXPPctAuras = GetAuraEffectsByType(SPELL_AURA_MOD_XP_QUEST_PCT); + for (Unit::AuraEffectList::const_iterator i = ModXPPctAuras.begin(); i != ModXPPctAuras.end(); ++i) + AddPct(XP, (*i)->GetAmount()); + + return XP; +} + +void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, bool announce) +{ + //this THING should be here to protect code from quest, which cast on player far teleport as a reward + //should work fine, cause far teleport will be executed in Player::Update() + SetCanDelayTeleport(true); + + uint32 quest_id = quest->GetQuestId(); + + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) + { + if (quest->RequiredItemId[i]) + { + uint32 amountToDestroy = quest->RequiredItemCount[i]; + if (quest->HasFlag(QUEST_FLAGS_REMOVE_SURPLUS_ITEMS)) + amountToDestroy = std::numeric_limits::max(); + + DestroyItemCount(quest->RequiredItemId[i], amountToDestroy, true); + } + } + + for (uint8 i = 0; i < QUEST_REQUIRED_CURRENCY_COUNT; ++i) + if (quest->RequiredCurrencyId[i]) + ModifyCurrency(quest->RequiredCurrencyId[i], -int32(quest->RequiredCurrencyCount[i])); + + for (uint8 i = 0; i < QUEST_SOURCE_ITEM_IDS_COUNT; ++i) + { + if (ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(quest->ItemDrop[i])) + { + if (quest->ItemDropQuantity[i] > 0 && itemTemplate->GetBonding() == BIND_QUEST && !quest->IsRepeatable() && !HasQuestForItem(quest->ItemDrop[i], quest_id)) + DestroyItemCount(quest->ItemDrop[i], 9999, true); + else + DestroyItemCount(quest->ItemDrop[i], quest->ItemDropQuantity[i], true); + } + } + + RemoveTimedQuest(quest_id); + + if (quest->GetRewItemsCount() > 0) + { + for (uint32 i = 0; i < quest->GetRewItemsCount(); ++i) + { + if (uint32 itemId = quest->RewardItemId[i]) + { + ItemPosCountVec dest; + if (CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemId, quest->RewardItemIdCount[i]) == EQUIP_ERR_OK) + { + Item* item = StoreNewItem(dest, itemId, true, GenerateItemRandomPropertyId(itemId)); + SendNewItem(item, quest->RewardItemIdCount[i], true, false, false, false); + } + else if (quest->IsDFQuest()) + SendItemRetrievalMail(itemId, quest->RewardItemIdCount[i]); + } + } + } + + for (uint8 i = 0; i < QUEST_REWARD_CURRENCY_COUNT; ++i) + { + if (quest->RewardCurrencyId[i]) + { + uint32 rewardCurrencyCount = quest->RewardCurrencyCount[i]; + + CurrencyTypesEntry const* currency = sCurrencyTypesStore.LookupEntry(quest->RewardCurrencyId[i]); + if (currency->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) + rewardCurrencyCount = rewardCurrencyCount * CURRENCY_PRECISION; + + ModifyCurrency(quest->RewardCurrencyId[i], rewardCurrencyCount, !quest->IsDFQuest()); + } + } + + if (uint32 skill = quest->GetRewardSkillId()) + UpdateSkillPro(skill, 1000, quest->GetRewardSkillPoints()); + + if (quest->GetRewChoiceItemsCount() > 0) + { + if (uint32 itemId = quest->RewardChoiceItemId[reward]) + { + ItemPosCountVec dest; + if (CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemId, quest->RewardChoiceItemCount[reward]) == EQUIP_ERR_OK) + { + Item* item = StoreNewItem(dest, itemId, true, GenerateItemRandomPropertyId(itemId)); + SendNewItem(item, quest->RewardChoiceItemCount[reward], true, false, false, false); + } + } + } + + uint16 log_slot = FindQuestSlot(quest_id); + if (log_slot < MAX_QUEST_LOG_SIZE) + SetQuestSlot(log_slot, 0); + + uint32 XP = GetQuestXPReward(quest); + + if (!IsMaxLevel()) + GiveXP(XP, nullptr); + + if (Guild* guild = GetGuild()) + { + uint32 _xp = XP; + uint32 guildRep = std::max(uint32(1), uint32(_xp / 450)); + guild->GiveXP(uint32(_xp * sWorld->getRate(RATE_XP_QUEST) * sWorld->getRate(RATE_XP_QUEST_GUILD_MODIFIER)), this); + guild->GiveReputation(guildRep, this); + guild->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUESTS_GUILD, 0, 0, 0, nullptr, this); + } + + int32 moneyRew = GetQuestMoneyReward(quest); + if (moneyRew) + { + ModifyMoney(moneyRew); + + if (moneyRew > 0) + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_MONEY_FROM_QUEST_REWARD, uint32(moneyRew)); + } + + // honor reward + if (uint32 honor = quest->CalculateHonorGain(getLevel())) + RewardHonor(nullptr, 0, honor); + + // title reward + if (quest->GetCharTitleId()) + { + if (CharTitlesEntry const* titleEntry = sCharTitlesStore.LookupEntry(quest->GetCharTitleId())) + SetTitle(titleEntry); + } + + if (uint32 talents = quest->GetBonusTalents()) + { + AddQuestRewardedTalentCount(talents); + InitTalentForLevel(); + } + + if (uint32 skill = quest->GetRewardSkillId()) + { + if (HasSkill(skill) && (GetSkillValue(skill) + quest->GetRewardSkillPoints()) <= GetMaxSkillValue(skill)) + SetSkill(skill, GetSkillStep(skill), GetSkillValue(skill) + quest->GetRewardSkillPoints(), GetMaxSkillValue(skill)); + } + + // Send reward mail + if (uint32 mail_template_id = quest->GetRewMailTemplateId()) + { + /// @todo Poor design of mail system + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + if (uint32 questMailSender = quest->GetRewMailSenderEntry()) + MailDraft(mail_template_id).SendMailTo(trans, this, questMailSender, MAIL_CHECK_MASK_HAS_BODY, quest->GetRewMailDelaySecs()); + else + MailDraft(mail_template_id).SendMailTo(trans, this, questGiver, MAIL_CHECK_MASK_HAS_BODY, quest->GetRewMailDelaySecs()); + CharacterDatabase.CommitTransaction(trans); + } + + if (quest->IsDaily() && !quest->IsDFQuest()) + { + SetDailyQuestStatus(quest_id); + if (quest->IsDaily()) + { + StartAchievementCriteria(AchievementCriteriaStartEvent::CompleteDailyQuest, 0); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_DAILY_QUEST, quest_id); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_DAILY_QUEST_DAILY, quest_id); + } + } + else if (quest->IsWeekly()) + SetWeeklyQuestStatus(quest_id); + else if (quest->IsMonthly()) + SetMonthlyQuestStatus(quest_id); + else if (quest->IsSeasonal()) + SetSeasonalQuestStatus(quest_id); + + RemoveActiveQuest(quest_id, false); + if (quest->CanIncreaseRewardedQuestCounters()) + SetRewardedQuest(quest_id); + + if (announce) + SendQuestReward(quest, questGiver ? questGiver->ToCreature() : nullptr, XP); + + RewardReputation(quest); + + // cast spells after mark quest complete (some spells have quest completed state requirements in spell_area data) + if (quest->GetRewSpellCast() > 0) + { + SpellInfo const* spellInfo = ASSERT_NOTNULL(sSpellMgr->GetSpellInfo(quest->GetRewSpellCast())); + if (questGiver->isType(TYPEMASK_UNIT) && !spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL) && !spellInfo->HasEffect(SPELL_EFFECT_CREATE_ITEM) && !spellInfo->IsSelfCast() && !quest->HasFlag(QUEST_FLAGS_PLAYER_CAST_COMPLETE)) + { + if (Creature* creature = GetMap()->GetCreature(questGiver->GetGUID())) + creature->CastSpell(this, quest->GetRewSpellCast(), true); + } + else + CastSpell(this, quest->GetRewSpellCast(), true); + } + else if (quest->GetRewSpell() > 0) + { + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(quest->GetRewSpell()); + if (questGiver->isType(TYPEMASK_UNIT) && !spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL) && !spellInfo->HasEffect(SPELL_EFFECT_CREATE_ITEM) && !spellInfo->IsSelfCast() && !quest->HasFlag(QUEST_FLAGS_PLAYER_CAST_COMPLETE)) + { + if (Creature* creature = GetMap()->GetCreature(questGiver->GetGUID())) + creature->CastSpell(this, quest->GetRewSpell(), true); + } + else + CastSpell(this, quest->GetRewSpell(), true); + } + + if (quest->GetZoneOrSort() > 0) + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUESTS_IN_ZONE, quest->GetZoneOrSort()); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST_COUNT); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUEST, quest->GetQuestId()); + + // make full db save + SaveToDB(false); + + if (quest->HasFlag(QUEST_FLAGS_FLAGS_PVP)) + { + pvpInfo.IsHostile = pvpInfo.IsInHostileArea || HasPvPForcingQuest(); + UpdatePvPState(); + } + + SendQuestUpdate(quest_id); + + bool updateVisibility = false; + if (quest->HasFlag(QUEST_FLAGS_UPDATE_PHASESHIFT)) + updateVisibility = PhasingHandler::OnConditionChange(this, false); + + //Update QuestGivers + SendQuestGiverStatusMultiple(); + + //lets remove flag for delayed teleports + SetCanDelayTeleport(false); + + if (questGiver && questGiver->isType(TYPEMASK_UNIT | TYPEMASK_GAMEOBJECT)) + { + //For AutoSubmition was added plr case there as it almost same exclute AI script cases. + // Send next quest + if (Quest const* nextQuest = GetNextQuest(questGiver, quest)) + { + // Only send the quest to the player if the conditions are met + if (CanTakeQuest(nextQuest, false)) + { + if (nextQuest->IsAutoAccept() && CanAddQuest(nextQuest, true)) + AddQuestAndCheckCompletion(nextQuest, questGiver); + + PlayerTalkClass->SendQuestGiverQuestDetails(nextQuest, questGiver->GetGUID(), true, false); + } + } + + PlayerTalkClass->ClearMenus(); + if (Creature* creatureQGiver = questGiver->ToCreature()) + creatureQGiver->AI()->QuestReward(this, quest, reward); + else if (GameObject* goQGiver = questGiver->ToGameObject()) + goQGiver->AI()->QuestReward(this, quest, reward); + } + + sScriptMgr->OnQuestStatusChange(this, quest_id); + + if (updateVisibility) + UpdateObjectVisibility(); +} + +void Player::SetRewardedQuest(uint32 quest_id) +{ + m_RewardedQuests.insert(quest_id); + m_RewardedQuestsSave[quest_id] = QUEST_DEFAULT_SAVE_TYPE; +} + +void Player::FailQuest(uint32 questId) +{ + if (Quest const* quest = sObjectMgr->GetQuestTemplate(questId)) + { + QuestStatus qStatus = GetQuestStatus(questId); + + // we can only fail incomplete quest or... + if (qStatus != QUEST_STATUS_INCOMPLETE) + { + // completed timed quest with no requirements + if (qStatus != QUEST_STATUS_COMPLETE || !quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED) || !quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_COMPLETED_AT_START)) + return; + } + + SetQuestStatus(questId, QUEST_STATUS_FAILED); + + uint16 log_slot = FindQuestSlot(questId); + + if (log_slot < MAX_QUEST_LOG_SIZE) + { + SetQuestSlotTimer(log_slot, 1); + SetQuestSlotState(log_slot, QUEST_STATE_FAIL); + } + + if (quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED)) + { + QuestStatusData& q_status = m_QuestStatus[questId]; + + RemoveTimedQuest(questId); + q_status.Timer = 0; + + SendQuestTimerFailed(questId); + } + else + SendQuestFailed(questId); + + // Destroy quest items on quest failure. + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) + if (ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(quest->RequiredItemId[i])) + if (quest->RequiredItemCount[i] > 0 && itemTemplate->GetBonding() == BIND_QUEST) + DestroyItemCount(quest->RequiredItemId[i], 9999, true); + + for (uint8 i = 0; i < QUEST_SOURCE_ITEM_IDS_COUNT; ++i) + if (ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(quest->ItemDrop[i])) + if (quest->ItemDropQuantity[i] > 0 && itemTemplate->GetBonding() == BIND_QUEST) + DestroyItemCount(quest->ItemDrop[i], 9999, true); + } +} + + +void Player::FailQuestsWithFlag(QuestFlags flag) +{ + for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) + { + uint32 questId = GetQuestSlotQuestId(slot); + if (!questId) + continue; + + if (Quest const* quest = sObjectMgr->GetQuestTemplate(questId)) + if (quest->HasFlag(flag)) + FailQuest(questId); + } +} + +void Player::AbandonQuest(uint32 questId) +{ + if (Quest const* quest = sObjectMgr->GetQuestTemplate(questId)) + { + // Destroy quest items on quest abandon. + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) + if (ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(quest->RequiredItemId[i])) + if (quest->RequiredItemCount[i] > 0 && itemTemplate->GetBonding() == BIND_QUEST) + DestroyItemCount(quest->RequiredItemId[i], 9999, true); + + for (uint8 i = 0; i < QUEST_SOURCE_ITEM_IDS_COUNT; ++i) + if (ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(quest->ItemDrop[i])) + if (quest->ItemDropQuantity[i] > 0 && itemTemplate->GetBonding() == BIND_QUEST) + DestroyItemCount(quest->ItemDrop[i], 9999, true); + + // If Quest has a Abandon Spell cast it now + if (quest->GetQuestAbandonSpell() != 0) + CastSpell(this, quest->GetQuestAbandonSpell(), true); + } +} + +bool Player::SatisfyQuestSkill(Quest const* qInfo, bool msg) const +{ + uint32 skill = qInfo->GetRequiredSkill(); + + // skip 0 case RequiredSkill + if (skill == 0) + return true; + + // check skill value + if (GetSkillValue(skill) < qInfo->GetRequiredSkillValue()) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestSkill: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't have the required skill value.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + + return false; + } + + return true; +} + +bool Player::SatisfyQuestLevel(Quest const* qInfo, bool msg) const +{ + if (getLevel() < qInfo->GetMinLevel()) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_QUEST_FAILED_LOW_LEVEL); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestLevel: Sent INVALIDREASON_QUEST_FAILED_LOW_LEVEL (QuestID: %u) because player '%s' (%s) doesn't have the required (min) level.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + return false; + } + + if (qInfo->GetMaxLevel() > 0 && getLevel() > qInfo->GetMaxLevel()) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); // There doesn't seem to be a specific response for too high player level + TC_LOG_DEBUG("misc", "Player::SatisfyQuestLevel: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't have the required (max) level.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + return false; + } + return true; +} + +bool Player::SatisfyQuestLog(bool msg) const +{ + // exist free slot + if (FindQuestSlot(0) < MAX_QUEST_LOG_SIZE) + return true; + + if (msg) + { + WorldPackets::Quest::QuestLogFull packet; + SendDirectMessage(packet.Write()); + } + return false; +} + +bool Player::SatisfyQuestDependentQuests(Quest const* qInfo, bool msg) const +{ + return SatisfyQuestPreviousQuest(qInfo, msg) && SatisfyQuestDependentPreviousQuests(qInfo, msg) && + SatisfyQuestBreadcrumbQuest(qInfo, msg) && SatisfyQuestDependentBreadcrumbQuests(qInfo, msg); +} + +bool Player::SatisfyQuestPreviousQuest(Quest const* qInfo, bool msg) const +{ + // No previous quest (might be first quest in a series) + if (!qInfo->GetPrevQuestId()) + return true; + + uint32 prevId = std::abs(qInfo->GetPrevQuestId()); + // If positive previous quest rewarded, return true + if (qInfo->GetPrevQuestId() > 0 && m_RewardedQuests.count(prevId) > 0) + return true; + + // If negative previous quest active, return true + if (qInfo->GetPrevQuestId() < 0 && GetQuestStatus(prevId) == QUEST_STATUS_INCOMPLETE) + return true; + + // Has positive prev. quest in non-rewarded state + // and negative prev. quest in non-active state + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestPreviousQuest: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't have required quest %u.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str(), prevId); + } + + return false; +} + + +bool Player::SatisfyQuestDependentPreviousQuests(Quest const* qInfo, bool msg) const +{ + // No previous quest (might be first quest in a series) + if (qInfo->DependentPreviousQuests.empty()) + return true; + + for (uint32 prevId : qInfo->DependentPreviousQuests) + { + // checked in startup + Quest const* questInfo = sObjectMgr->GetQuestTemplate(prevId); + ASSERT(questInfo); + + // If any of the previous quests completed, return true + if (IsQuestRewarded(prevId)) + { + // skip one-from-all exclusive group + if (questInfo->GetExclusiveGroup() >= 0) + return true; + + // each-from-all exclusive group (< 0) + // can be start if only all quests in prev quest exclusive group completed and rewarded + auto bounds = sObjectMgr->GetExclusiveQuestGroupBounds(questInfo->GetExclusiveGroup()); + for (auto itr = bounds.first; itr != bounds.second; ++itr) + { + // skip checked quest id, only state of other quests in group is interesting + uint32 exclusiveQuestId = itr->second; + if (exclusiveQuestId == prevId) + continue; + + // alternative quest from group also must be completed and rewarded (reported) + if (!IsQuestRewarded(exclusiveQuestId)) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestDependentPreviousQuests: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't have the required quest (1).", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + + return false; + } + } + + return true; + } + } + + // Has only prev. quests in non-rewarded state + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestDependentPreviousQuests: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't have required quest (2).", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + + return false; +} + +bool Player::SatisfyQuestBreadcrumbQuest(Quest const* qInfo, bool msg) const +{ + uint32 breadcrumbTargetQuestId = std::abs(qInfo->GetBreadcrumbForQuestId()); + + //If this is not a breadcrumb quest. + if (!breadcrumbTargetQuestId) + return true; + + // If the target quest is not available + if (!CanTakeQuest(sObjectMgr->GetQuestTemplate(breadcrumbTargetQuestId), false)) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestBreadcrumbQuest: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because target quest (QuestID: %u) is not available to player '%s' (%s).", + qInfo->GetQuestId(), breadcrumbTargetQuestId, GetName().c_str(), GetGUID().ToString().c_str()); + } + + return false; + } + + return true; +} + +bool Player::SatisfyQuestDependentBreadcrumbQuests(Quest const* qInfo, bool msg) const +{ + for (uint32 breadcrumbQuestId : qInfo->DependentBreadcrumbQuests) + { + QuestStatus status = GetQuestStatus(breadcrumbQuestId); + // If any of the breadcrumb quests are in the quest log, return false. + if (status == QUEST_STATUS_INCOMPLETE || status == QUEST_STATUS_COMPLETE || status == QUEST_STATUS_FAILED) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestDependentBreadcrumbQuests: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) has a breadcrumb quest towards this quest in the quest log.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + + return false; + } + } + return true; +} + +bool Player::SatisfyQuestClass(Quest const* qInfo, bool msg) const +{ + uint32 reqClass = qInfo->GetAllowableClasses(); + + if (reqClass == 0) + return true; + + if ((reqClass & getClassMask()) == 0) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestClass: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't have required class.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + + return false; + } + + return true; +} + +bool Player::SatisfyQuestRace(Quest const* qInfo, bool msg) const +{ + uint32 reqraces = qInfo->GetAllowableRaces(); + if (reqraces == 0) + return true; + if ((reqraces & getRaceMask()) == 0) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_QUEST_FAILED_WRONG_RACE); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestRace: Sent INVALIDREASON_QUEST_FAILED_WRONG_RACE (QuestID: %u) because player '%s' (%s) doesn't have required race.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + + } + return false; + } + return true; +} + +bool Player::SatisfyQuestReputation(Quest const* qInfo, bool msg) const +{ + uint32 fIdMin = qInfo->GetRequiredMinRepFaction(); //Min required rep + if (fIdMin && GetReputationMgr().GetReputation(fIdMin) < qInfo->GetRequiredMinRepValue()) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestReputation: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't required reputation (min).", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + return false; + } + + uint32 fIdMax = qInfo->GetRequiredMaxRepFaction(); //Max required rep + if (fIdMax && GetReputationMgr().GetReputation(fIdMax) >= qInfo->GetRequiredMaxRepValue()) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "SatisfyQuestReputation: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't required reputation (max).", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + return false; + } + + // ReputationObjective2 does not seem to be an objective requirement but a requirement + // to be able to accept the quest + uint32 fIdObj = qInfo->GetRepObjectiveFaction2(); + if (fIdObj && GetReputationMgr().GetReputation(fIdObj) >= qInfo->GetRepObjectiveValue2()) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "SatisfyQuestReputation: Sent INVALIDREASON_DONT_HAVE_REQ (questId: %u) because player does not have the required reputation (ReputationObjective2).", qInfo->GetQuestId()); + } + return false; + } + + return true; +} + +bool Player::SatisfyQuestStatus(Quest const* qInfo, bool msg) const +{ + if (GetQuestStatus(qInfo->GetQuestId()) == QUEST_STATUS_REWARDED) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_QUEST_ALREADY_DONE); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestStatus: Sent QUEST_STATUS_REWARDED (QuestID: %u) because player '%s' (%s) quest status is already REWARDED.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + return false; + } + + if (GetQuestStatus(qInfo->GetQuestId()) != QUEST_STATUS_NONE) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_QUEST_ALREADY_ON); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestStatus: Sent INVALIDREASON_QUEST_ALREADY_ON (QuestID: %u) because player '%s' (%s) quest status is not NONE.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + return false; + } + return true; +} + +bool Player::SatisfyQuestConditions(Quest const* qInfo, bool msg) const +{ + if (!sConditionMgr->IsObjectMeetingNotGroupedConditions(CONDITION_SOURCE_TYPE_QUEST_AVAILABLE, qInfo->GetQuestId(), const_cast(this))) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestConditions: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) doesn't meet conditions.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + TC_LOG_DEBUG("condition", "Player::SatisfyQuestConditions: conditions not met for quest %u", qInfo->GetQuestId()); + return false; + } + return true; +} + +bool Player::SatisfyQuestTimed(Quest const* qInfo, bool msg) const +{ + if (!m_timedquests.empty() && qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED)) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_QUEST_ONLY_ONE_TIMED); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestTimed: Sent INVALIDREASON_QUEST_ONLY_ONE_TIMED (QuestID: %u) because player '%s' (%s) is already on a timed quest.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + return false; + } + return true; +} + +bool Player::SatisfyQuestExclusiveGroup(Quest const* qInfo, bool msg) const +{ + // non positive exclusive group, if > 0 then can be start if any other quest in exclusive group already started/completed + if (qInfo->GetExclusiveGroup() <= 0) + return true; + + auto bounds = sObjectMgr->GetExclusiveQuestGroupBounds(qInfo->GetExclusiveGroup()); + for (auto itr = bounds.first; itr != bounds.second; ++itr) + { + uint32 exclude_Id = itr->second; + + // skip checked quest id, only state of other quests in group is interesting + if (exclude_Id == qInfo->GetQuestId()) + continue; + + // not allow have daily quest if daily quest from exclusive group already recently completed + Quest const* Nquest = sObjectMgr->GetQuestTemplate(exclude_Id); + ASSERT(Nquest); + if (!SatisfyQuestDay(Nquest, false) || !SatisfyQuestWeek(Nquest, false) || !SatisfyQuestSeasonal(Nquest, false)) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestExclusiveGroup: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) already did daily quests in exclusive group.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + + return false; + } + + // alternative quest already started or completed - but don't check rewarded states if both are repeatable + if (GetQuestStatus(exclude_Id) != QUEST_STATUS_NONE || (!(qInfo->IsRepeatable() && Nquest->IsRepeatable()) && GetQuestRewardStatus(exclude_Id))) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DONT_HAVE_REQ); + TC_LOG_DEBUG("misc", "Player::SatisfyQuestExclusiveGroup: Sent INVALIDREASON_DONT_HAVE_REQ (QuestID: %u) because player '%s' (%s) already did quest in exclusive group.", + qInfo->GetQuestId(), GetName().c_str(), GetGUID().ToString().c_str()); + } + return false; + } + } + return true; +} + +bool Player::SatisfyQuestDay(Quest const* qInfo, bool msg) const +{ + if (!qInfo->IsDaily() && !qInfo->IsDFQuest()) + return true; + + bool have_slot = false; + for (uint32 quest_daily_idx = 0; quest_daily_idx < PLAYER_MAX_DAILY_QUESTS; ++quest_daily_idx) + { + uint32 id = GetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1+quest_daily_idx); + if (qInfo->GetQuestId() == id) + return false; + + if (!id) + have_slot = true; + } + + if (!have_slot) + { + if (msg) + { + SendCanTakeQuestResponse(INVALIDREASON_DAILY_QUESTS_REMAINING); + TC_LOG_DEBUG("misc", "SatisfyQuestDay: Sent INVALIDREASON_DAILY_QUESTS_REMAINING (questId: %u) because player already did all possible quests today.", qInfo->GetQuestId()); + } + return false; + } + + return true; +} + +bool Player::SatisfyQuestWeek(Quest const* qInfo, bool /*msg*/) const +{ + if (!qInfo->IsWeekly() || m_weeklyquests.empty()) + return true; + + // if not found in cooldown list + return m_weeklyquests.find(qInfo->GetQuestId()) == m_weeklyquests.end(); +} + +bool Player::SatisfyQuestSeasonal(Quest const* qInfo, bool /*msg*/) const +{ + if (!qInfo->IsSeasonal() || m_seasonalquests.empty()) + return true; + + auto itr = m_seasonalquests.find(qInfo->GetEventIdForQuest()); + if (itr == m_seasonalquests.end() || itr->second.empty()) + return true; + + // if not found in cooldown list + return itr->second.find(qInfo->GetQuestId()) == itr->second.end(); +} + +bool Player::SatisfyQuestMonth(Quest const* qInfo, bool /*msg*/) const +{ + if (!qInfo->IsMonthly() || m_monthlyquests.empty()) + return true; + + // if not found in cooldown list + return m_monthlyquests.find(qInfo->GetQuestId()) == m_monthlyquests.end(); +} + +bool Player::GiveQuestSourceItem(Quest const* quest) +{ + uint32 srcitem = quest->GetSrcItemId(); + if (srcitem > 0) + { + // Don't give source item if it is the same item used to start the quest + ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(srcitem); + if (quest->GetQuestId() == itemTemplate->GetStartQuest()) + return true; + + uint32 count = quest->GetSrcItemCount(); + if (count <= 0) + count = 1; + + ItemPosCountVec dest; + InventoryResult msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, srcitem, count); + if (msg == EQUIP_ERR_OK) + { + Item* item = StoreNewItem(dest, srcitem, true); + SendNewItem(item, count, true, false); + return true; + } + // player already have max amount required item, just report success + else if (msg == EQUIP_ERR_ITEM_MAX_COUNT) + return true; + + SendEquipError(msg, nullptr, nullptr, srcitem); + return false; + } + + return true; +} + +bool Player::TakeQuestSourceItem(uint32 questId, bool msg) +{ + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (quest) + { + uint32 srcItemId = quest->GetSrcItemId(); + ItemTemplate const* item = sObjectMgr->GetItemTemplate(srcItemId); + + if (srcItemId > 0) + { + uint32 count = quest->GetSrcItemCount(); + if (count <= 0) + count = 1; + + // There are two cases where the source item is not destroyed: + // - Item cannot be unequipped (example: non-empty bags) + // - The source item is the item that started the quest, so the player is supposed to keep it (otherwise it was already destroyed in AddQuestAndCheckCompletion()) + InventoryResult res = CanUnequipItems(srcItemId, count); + if (res != EQUIP_ERR_OK) + { + if (msg) + SendEquipError(res, nullptr, nullptr, srcItemId); + return false; + } + + ASSERT(item); + if (item->GetStartQuest() != questId) + DestroyItemCount(srcItemId, count, true, true); + } + } + + return true; +} + +bool Player::GetQuestRewardStatus(uint32 quest_id) const +{ + Quest const* qInfo = sObjectMgr->GetQuestTemplate(quest_id); + if (qInfo) + { + if (qInfo->IsSeasonal() && !qInfo->IsRepeatable()) + return !SatisfyQuestSeasonal(qInfo, false); + + // for repeatable quests: rewarded field is set after first reward only to prevent getting XP more than once + if (!qInfo->IsRepeatable()) + return IsQuestRewarded(quest_id); + + return false; + } + return false; +} + +QuestStatus Player::GetQuestStatus(uint32 quest_id) const +{ + if (quest_id) + { + QuestStatusMap::const_iterator itr = m_QuestStatus.find(quest_id); + if (itr != m_QuestStatus.end()) + return itr->second.Status; + + if (GetQuestRewardStatus(quest_id)) + return QUEST_STATUS_REWARDED; + } + return QUEST_STATUS_NONE; +} + +bool Player::CanShareQuest(uint32 quest_id) const +{ + Quest const* qInfo = sObjectMgr->GetQuestTemplate(quest_id); + if (qInfo && qInfo->HasFlag(QUEST_FLAGS_SHARABLE)) + { + QuestStatusMap::const_iterator itr = m_QuestStatus.find(quest_id); + if (itr != m_QuestStatus.end()) + { + // in pool and not currently available (wintergrasp weekly, dalaran weekly) - can't share + if (sQuestPoolMgr->IsQuestActive(quest_id)) + { + SendPushToPartyResponse(this, QUEST_PARTY_MSG_CANT_BE_SHARED_TODAY); + return false; + } + + return true; + } + } + return false; +} + +void Player::SetQuestStatus(uint32 questId, QuestStatus status, bool update /*= true*/) +{ + if (Quest const* quest = sObjectMgr->GetQuestTemplate(questId)) + { + m_QuestStatus[questId].Status = status; + + if (!quest->IsTurnIn()) + m_QuestStatusSave[questId] = QUEST_DEFAULT_SAVE_TYPE; + } + + if (update) + SendQuestUpdate(questId); + + sScriptMgr->OnQuestStatusChange(this, questId); +} + +void Player::RemoveActiveQuest(uint32 questId, bool update /*= true*/) +{ + QuestStatusMap::iterator itr = m_QuestStatus.find(questId); + if (itr != m_QuestStatus.end()) + { + m_QuestStatus.erase(itr); + m_QuestStatusSave[questId] = QUEST_DELETE_SAVE_TYPE; + } + + if (update) + SendQuestUpdate(questId); +} + +void Player::RemoveRewardedQuest(uint32 questId, bool update /*= true*/) +{ + RewardedQuestSet::iterator rewItr = m_RewardedQuests.find(questId); + if (rewItr != m_RewardedQuests.end()) + { + m_RewardedQuests.erase(rewItr); + m_RewardedQuestsSave[questId] = QUEST_FORCE_DELETE_SAVE_TYPE; + } + + // Remove seasonal quest also + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questId); + if (qInfo->IsSeasonal()) + { + uint16 eventId = qInfo->GetEventIdForQuest(); + if (m_seasonalquests.find(eventId) != m_seasonalquests.end()) + { + m_seasonalquests[eventId].erase(questId); + m_SeasonalQuestChanged = true; + } + } + + if (update) + SendQuestUpdate(questId); +} + +void Player::SendQuestUpdate(uint32 questId) +{ + SpellAreaForQuestMapBounds saBounds = sSpellMgr->GetSpellAreaForQuestMapBounds(questId); + + if (saBounds.first != saBounds.second) + { + std::set aurasToRemove, aurasToCast; + uint32 zone = 0, area = 0; + GetZoneAndAreaId(zone, area); + + for (SpellAreaForQuestMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) + { + if (itr->second->flags & SPELL_AREA_FLAG_AUTOREMOVE && !itr->second->IsFitToRequirements(this, zone, area)) + aurasToRemove.insert(itr->second->spellId); + else if (itr->second->flags & SPELL_AREA_FLAG_AUTOCAST) + aurasToCast.insert(itr->second->spellId); + } + + // Auras matching the requirements will be inside the aurasToCast container. + // Auras not matching the requirements may prevent using auras matching the requirements. + // aurasToCast will erase conflicting auras in aurasToRemove container to handle spells used by multiple quests. + + for (auto itr = aurasToRemove.begin(); itr != aurasToRemove.end();) + { + bool auraRemoved = false; + + for (const auto i : aurasToCast) + { + if (*itr == i) + { + itr = aurasToRemove.erase(itr); + auraRemoved = true; + break; + } + } + + if (!auraRemoved) + ++itr; + } + + for (auto spellId : aurasToCast) + if (!HasAura(spellId)) + CastSpell(this, spellId, true); + + for (auto spellId : aurasToRemove) + RemoveAurasDueToSpell(spellId); + } + + UpdateVisibleGameobjectsOrSpellClicks(); +} + +bool Player::SatisfyFirstLFGReward(uint32 dungeonId, uint8 maxRewCount) const +{ + LFGRewardStatusMap::const_iterator lfgdungeon = m_lfgrewardstatus.find(dungeonId); + if (lfgdungeon != m_lfgrewardstatus.end()) + return lfgdungeon->second.CompletionsThisPeriod && lfgdungeon->second.CompletionsThisPeriod < maxRewCount; + + return true; +} + +uint8 Player::GetFirstRewardCountForDungeonId(uint32 dungeonId) +{ + LFGRewardStatusMap::const_iterator lfgdungeon = m_lfgrewardstatus.find(dungeonId); + if (lfgdungeon != m_lfgrewardstatus.end()) + return lfgdungeon->second.CompletionsThisPeriod; + + return 0; +} + +QuestGiverStatus Player::GetQuestDialogStatus(Object* questgiver) +{ + QuestRelationResult qr, qir; + + switch (questgiver->GetTypeId()) + { + case TYPEID_GAMEOBJECT: + { + if (auto questStatus = questgiver->ToGameObject()->AI()->GetDialogStatus(this)) + return *questStatus; + qr = sObjectMgr->GetGOQuestRelations(questgiver->GetEntry()); + qir = sObjectMgr->GetGOQuestInvolvedRelations(questgiver->GetEntry()); + break; + } + case TYPEID_UNIT: + { + if (auto questStatus = questgiver->ToCreature()->AI()->GetDialogStatus(this)) + return *questStatus; + qr = sObjectMgr->GetCreatureQuestRelations(questgiver->GetEntry()); + qir = sObjectMgr->GetCreatureQuestInvolvedRelations(questgiver->GetEntry()); + break; + } + default: + // it's impossible, but check + TC_LOG_ERROR("entities.player.quest", "GetQuestDialogStatus called for unexpected type %u", questgiver->GetTypeId()); + return DIALOG_STATUS_NONE; + } + + QuestGiverStatus result = DIALOG_STATUS_NONE; + + for (uint32 questId : qir) + { + QuestGiverStatus result2 = DIALOG_STATUS_NONE; + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + continue; + + QuestStatus status = GetQuestStatus(questId); + if (status == QUEST_STATUS_COMPLETE && !GetQuestRewardStatus(questId)) + result2 = DIALOG_STATUS_REWARD; + else if (status == QUEST_STATUS_INCOMPLETE) + result2 = DIALOG_STATUS_INCOMPLETE; + + if (quest->IsTurnIn() && CanTakeQuest(quest, false) && quest->IsRepeatable() && !quest->IsDailyOrWeekly()) + result2 = DIALOG_STATUS_REWARD_REP; + + if (result2 > result) + result = result2; + } + + for (uint32 questId : qr) + { + QuestGiverStatus result2 = DIALOG_STATUS_NONE; + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + continue; + + if (!sConditionMgr->IsObjectMeetingNotGroupedConditions(CONDITION_SOURCE_TYPE_QUEST_AVAILABLE, quest->GetQuestId(), this)) + continue; + + QuestStatus status = GetQuestStatus(questId); + if (status == QUEST_STATUS_NONE) + { + if (CanSeeStartQuest(quest)) + { + if (SatisfyQuestLevel(quest, false)) + { + bool isNotLowLevelQuest = getLevel() <= (GetQuestLevel(quest) + sWorld->getIntConfig(CONFIG_QUEST_LOW_LEVEL_HIDE_DIFF)); + if (quest->IsRepeatable()) + { + if (quest->IsDaily()) + { + if (isNotLowLevelQuest) + result2 = DIALOG_STATUS_AVAILABLE_REP; + else + result2 = DIALOG_STATUS_LOW_LEVEL_AVAILABLE_REP; + } + else if (quest->IsWeekly() || quest->IsMonthly()) + { + if (isNotLowLevelQuest) + result2 = DIALOG_STATUS_AVAILABLE; + else + result2 = DIALOG_STATUS_LOW_LEVEL_AVAILABLE; + } + else if (quest->IsTurnIn()) + { + if (isNotLowLevelQuest) + result2 = DIALOG_STATUS_REWARD_REP; + else + result2 = DIALOG_STATUS_LOW_LEVEL_REWARD_REP; + } + else + { + if (isNotLowLevelQuest) + result2 = DIALOG_STATUS_AVAILABLE; + else + result2 = DIALOG_STATUS_LOW_LEVEL_AVAILABLE; + } + } + else if (isNotLowLevelQuest) + result2 = DIALOG_STATUS_AVAILABLE; + else + result2 = DIALOG_STATUS_LOW_LEVEL_AVAILABLE; + } + else + result2 = DIALOG_STATUS_UNAVAILABLE; + } + } + + if (result2 > result) + result = result2; + } + + return result; +} + +// not used in Trinity, but used in scripting code +uint16 Player::GetReqKillOrCastCurrentCount(uint32 quest_id, int32 entry) const +{ + Quest const* qInfo = sObjectMgr->GetQuestTemplate(quest_id); + if (!qInfo) + return 0; + + auto itr = m_QuestStatus.find(quest_id); + if (itr != m_QuestStatus.end()) + for (uint8 j = 0; j < QUEST_OBJECTIVES_COUNT; ++j) + if (qInfo->RequiredNpcOrGo[j] == entry) + return itr->second.CreatureOrGOCount[j]; + + return 0; +} + +void Player::AdjustQuestReqItemCount(Quest const* quest, QuestStatusData& questStatusData) +{ + if (quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_DELIVER)) + { + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) + { + uint32 reqitemcount = quest->RequiredItemCount[i]; + if (reqitemcount != 0) + { + uint32 curitemcount = GetItemCount(quest->RequiredItemId[i], true); + + questStatusData.ItemCount[i] = std::min(curitemcount, reqitemcount); + m_QuestStatusSave[quest->GetQuestId()] = QUEST_DEFAULT_SAVE_TYPE; + } + } + } +} + +uint16 Player::FindQuestSlot(uint32 quest_id) const +{ + for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + if (GetQuestSlotQuestId(i) == quest_id) + return i; + + return MAX_QUEST_LOG_SIZE; +} + +uint32 Player::GetQuestSlotQuestId(uint16 slot) const +{ + return GetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_ID_OFFSET); +} + +uint32 Player::GetQuestSlotState(uint16 slot) const +{ + return GetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_STATE_OFFSET); +} + +uint16 Player::GetQuestSlotCounter(uint16 slot, uint8 counter) const +{ + return (uint16)(GetUInt64Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_COUNTS_OFFSET) >> (counter * 16)); +} + +uint32 Player::GetQuestSlotTime(uint16 slot) const +{ + return GetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_TIME_OFFSET); +} + +void Player::SetQuestSlot(uint16 slot, uint32 quest_id, uint32 timer /*= 0*/) +{ + SetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_ID_OFFSET, quest_id); + SetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_STATE_OFFSET, 0); + SetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_COUNTS_OFFSET, 0); + SetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_COUNTS_OFFSET + 1, 0); + SetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_TIME_OFFSET, timer); +} + +void Player::SetQuestSlotCounter(uint16 slot, uint8 counter, uint16 count) +{ + uint64 val = GetUInt64Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_COUNTS_OFFSET); + val &= ~((uint64)0xFFFF << (counter * 16)); + val |= ((uint64)count << (counter * 16)); + SetUInt64Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_COUNTS_OFFSET, val); +} + +void Player::SetQuestSlotState(uint16 slot, uint32 state) +{ + SetFlag(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_STATE_OFFSET, state); +} + +void Player::RemoveQuestSlotState(uint16 slot, uint32 state) +{ + RemoveFlag(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_STATE_OFFSET, state); +} + +void Player::SetQuestSlotTimer(uint16 slot, uint32 timer) +{ + SetUInt32Value(PLAYER_QUEST_LOG_1_1 + slot * MAX_QUEST_OFFSET + QUEST_TIME_OFFSET, timer); +} + +void Player::SwapQuestSlot(uint16 slot1, uint16 slot2) +{ + for (int i = 0; i < MAX_QUEST_OFFSET; ++i) + { + uint32 temp1 = GetUInt32Value(PLAYER_QUEST_LOG_1_1 + MAX_QUEST_OFFSET * slot1 + i); + uint32 temp2 = GetUInt32Value(PLAYER_QUEST_LOG_1_1 + MAX_QUEST_OFFSET * slot2 + i); + + SetUInt32Value(PLAYER_QUEST_LOG_1_1 + MAX_QUEST_OFFSET * slot1 + i, temp2); + SetUInt32Value(PLAYER_QUEST_LOG_1_1 + MAX_QUEST_OFFSET * slot2 + i, temp1); + } +} + +void Player::AreaExploredOrEventHappens(uint32 questId) +{ + if (questId) + { + if (Quest const* qInfo = sObjectMgr->GetQuestTemplate(questId)) + { + uint16 log_slot = FindQuestSlot(questId); + if (log_slot < MAX_QUEST_LOG_SIZE) + { + QuestStatusData& q_status = m_QuestStatus[questId]; + + // Dont complete failed quest + if (!q_status.Explored && q_status.Status != QUEST_STATUS_FAILED) + { + q_status.Explored = true; + m_QuestStatusSave[questId] = QUEST_DEFAULT_SAVE_TYPE; + + SendQuestComplete(qInfo); + } + } + if (CanCompleteQuest(questId)) + CompleteQuest(questId); + } + } +} + +//not used in Trinityd, function for external script library +void Player::GroupEventHappens(uint32 questId, WorldObject const* pEventObject) +{ + if (Group* group = GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* player = itr->GetSource(); + + // for any leave or dead (with not released body) group member at appropriate distance + if (player && player->IsAtGroupRewardDistance(pEventObject) && !player->GetCorpse()) + player->AreaExploredOrEventHappens(questId); + } + } + else + AreaExploredOrEventHappens(questId); +} + +void Player::ItemAddedQuestCheck(uint32 entry, uint32 count) +{ + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (questid == 0) + continue; + + QuestStatusData& q_status = m_QuestStatus[questid]; + + if (q_status.Status != QUEST_STATUS_INCOMPLETE) + continue; + + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid); + if (!qInfo || !qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_DELIVER)) + continue; + + for (uint8 j = 0; j < QUEST_ITEM_OBJECTIVES_COUNT; ++j) + { + uint32 reqitem = qInfo->RequiredItemId[j]; + if (reqitem == entry) + { + uint32 reqitemcount = qInfo->RequiredItemCount[j]; + uint16 curitemcount = q_status.ItemCount[j]; + if (curitemcount < reqitemcount) + { + q_status.ItemCount[j] = std::min(q_status.ItemCount[j] + count, reqitemcount); + m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE; + } + if (CanCompleteQuest(questid)) + CompleteQuest(questid); + } + } + } + UpdateVisibleGameobjectsOrSpellClicks(); +} + +void Player::ItemRemovedQuestCheck(uint32 entry, uint32 count) +{ + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (!questid) + continue; + + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid); + if (!qInfo) + continue; + + if (!qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_DELIVER)) + continue; + + for (uint8 j = 0; j < QUEST_ITEM_OBJECTIVES_COUNT; ++j) + { + uint32 reqitem = qInfo->RequiredItemId[j]; + if (reqitem == entry) + { + QuestStatusData& q_status = m_QuestStatus[questid]; + + uint32 reqItemCount = qInfo->RequiredItemCount[j]; + uint16 questStatusItemCount = q_status.ItemCount[j]; + uint16 newItemCount = (count > questStatusItemCount) ? 0 : questStatusItemCount - count; + + if (questStatusItemCount >= reqItemCount) // we may have more than what the status shows, we don't need reduce by count + newItemCount = GetItemCount(entry, false); + + newItemCount = std::min(newItemCount, reqItemCount); + if (newItemCount != q_status.ItemCount[j]) + { + q_status.ItemCount[j] = newItemCount; + m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE; + IncompleteQuest(questid); + } + } + } + } + UpdateVisibleGameobjectsOrSpellClicks(); +} + +void Player::KilledMonster(CreatureTemplate const* cInfo, ObjectGuid guid) +{ + ASSERT(cInfo); + + if (cInfo->Entry) + KilledMonsterCredit(cInfo->Entry, guid); + + for (uint8 i = 0; i < MAX_KILL_CREDIT; ++i) + if (cInfo->KillCredit[i]) + KilledMonsterCredit(cInfo->KillCredit[i], ObjectGuid::Empty); +} + +void Player::KilledMonsterCredit(uint32 entry, ObjectGuid guid /*= ObjectGuid::Empty*/) +{ + uint16 addkillcount = 1; + uint32 real_entry = entry; + Creature* killed = nullptr; + if (guid) + { + killed = GetMap()->GetCreature(guid); + if (killed && killed->GetEntry()) + real_entry = killed->GetEntry(); + } + + StartAchievementCriteria(AchievementCriteriaStartEvent::KillNPC, real_entry); // MUST BE CALLED FIRST + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE, real_entry, addkillcount, 0, killed); + + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (!questid) + continue; + + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid); + if (!qInfo) + continue; + // just if !ingroup || !noraidgroup || raidgroup + QuestStatusData& q_status = m_QuestStatus[questid]; + if (q_status.Status == QUEST_STATUS_INCOMPLETE && (!GetGroup() || !GetGroup()->isRaidGroup() || qInfo->IsAllowedInRaid(GetMap()->GetDifficulty()))) + { + if (qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_KILL) /*&& !qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_CAST)*/) + { + for (uint8 j = 0; j < QUEST_OBJECTIVES_COUNT; ++j) + { + // skip GO activate objective or none + if (qInfo->RequiredNpcOrGo[j] <= 0) + continue; + + uint32 reqkill = qInfo->RequiredNpcOrGo[j]; + + if (reqkill == real_entry) + { + uint32 reqkillcount = qInfo->RequiredNpcOrGoCount[j]; + uint16 curkillcount = q_status.CreatureOrGOCount[j]; + if (curkillcount < reqkillcount) + { + uint16 newCount = curkillcount + addkillcount; + +q_status.CreatureOrGOCount[j] = newCount; + +m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE; + +uint16 log_slot = FindQuestSlot(questid); +if (log_slot < MAX_QUEST_LOG_SIZE) + SetQuestSlotCounter(log_slot, j, newCount); + +sScriptMgr->OnQuestObjectiveCredit( + this, + questid, + j, + qInfo->RequiredNpcOrGo[j], + curkillcount, + newCount, + guid); + +SendQuestUpdateAddCredit(qInfo, guid, j, newCount); + } + if (CanCompleteQuest(questid)) + CompleteQuest(questid); + + // same objective target can be in many active quests, but not in 2 objectives for single quest (code optimization). + break; + } + } + } + } + } +} + +void Player::KilledPlayerCredit(uint16 count) +{ + StartAchievementCriteria(AchievementCriteriaStartEvent::KillPlayer, 0); + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (!questid) + continue; + + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid); + if (!qInfo) + continue; + // just if !ingroup || !noraidgroup || raidgroup + QuestStatusData& q_status = m_QuestStatus[questid]; + if (q_status.Status == QUEST_STATUS_INCOMPLETE && (!GetGroup() || !GetGroup()->isRaidGroup() || qInfo->IsAllowedInRaid(GetMap()->GetDifficulty()))) + { + // PvP Killing quest require player to be in same zone as quest zone (only 2 quests so no doubt) + if (qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_PLAYER_KILL) && GetZoneId() == static_cast(qInfo->GetZoneOrSort())) + { + KilledPlayerCreditForQuest(count, qInfo); + break; // there is only one quest per zone + } + } + } +} + +void Player::KilledPlayerCreditForQuest(uint16 count, Quest const* quest) +{ + uint32 const questId = quest->GetQuestId(); + auto it = m_QuestStatus.find(questId); + if (it == m_QuestStatus.end()) + return; + QuestStatusData& questStatus = it->second; + + uint16 curKill = questStatus.PlayerCount; + uint32 reqKill = quest->GetPlayersSlain(); + + if (curKill < reqKill) + { + count = std::min(reqKill - curKill, count); + questStatus.PlayerCount = curKill + count; + + m_QuestStatusSave[quest->GetQuestId()] = QUEST_DEFAULT_SAVE_TYPE; + + uint16 log_slot = FindQuestSlot(quest->GetQuestId()); + if (log_slot < MAX_QUEST_LOG_SIZE) + SetQuestSlotCounter(log_slot, QUEST_PVP_KILL_SLOT, curKill + count); + + SendQuestUpdateAddPlayer(quest, curKill + count); + } + + if (CanCompleteQuest(questId)) + CompleteQuest(questId); +} + +void Player::KillCreditGO(uint32 entry, ObjectGuid guid) +{ + uint16 addCastCount = 1; + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (!questid) + continue; + + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid); + if (!qInfo) + continue; + + QuestStatusData& q_status = m_QuestStatus[questid]; + + if (q_status.Status == QUEST_STATUS_INCOMPLETE) + { + if (qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_CAST) /*&& !qInfo->HasSpecialFlag(QUEST_SPECIAL_FLAGS_KILL)*/) + { + for (uint8 j = 0; j < QUEST_OBJECTIVES_COUNT; ++j) + { + uint32 reqTarget = 0; + + // GO activate objective + if (qInfo->RequiredNpcOrGo[j] < 0) + // checked at quest_template loading + reqTarget = - qInfo->RequiredNpcOrGo[j]; + + // other not this creature/GO related objectives + if (reqTarget != entry) + continue; + + uint32 reqCastCount = qInfo->RequiredNpcOrGoCount[j]; + uint16 curCastCount = q_status.CreatureOrGOCount[j]; + if (curCastCount < reqCastCount) +{ + uint16 newCount = curCastCount + addCastCount; + + q_status.CreatureOrGOCount[j] = newCount; + + m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE; + + uint16 log_slot = FindQuestSlot(questid); + if (log_slot < MAX_QUEST_LOG_SIZE) + SetQuestSlotCounter(log_slot, j, newCount); + + sScriptMgr->OnQuestObjectiveCredit( + this, + questid, + j, + qInfo->RequiredNpcOrGo[j], + curCastCount, + newCount, + guid); + + SendQuestUpdateAddCredit(qInfo, guid, j, newCount); +} + + if (CanCompleteQuest(questid)) + CompleteQuest(questid); + + // same objective target can be in many active quests, but not in 2 objectives for single quest (code optimization). + break; + } + } + } + } +} + +void Player::TalkedToCreature(uint32 entry, ObjectGuid guid) +{ + uint16 addTalkCount = 1; + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (!questid) + continue; + + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid); + if (!qInfo) + continue; + + QuestStatusData& q_status = m_QuestStatus[questid]; + + if (q_status.Status == QUEST_STATUS_INCOMPLETE) + { + if (qInfo->HasSpecialFlag(QuestSpecialFlags(QUEST_SPECIAL_FLAGS_KILL | QUEST_SPECIAL_FLAGS_CAST | QUEST_SPECIAL_FLAGS_SPEAKTO))) + { + for (uint8 j = 0; j < QUEST_OBJECTIVES_COUNT; ++j) + { + // skip gameobject objectives + if (qInfo->RequiredNpcOrGo[j] < 0) + continue; + + uint32 reqTarget = 0; + + if (qInfo->RequiredNpcOrGo[j] > 0) // creature activate objectives + // checked at quest_template loading + reqTarget = qInfo->RequiredNpcOrGo[j]; + else + continue; + + if (reqTarget == entry) + { + uint32 reqTalkCount = qInfo->RequiredNpcOrGoCount[j]; + uint16 curTalkCount = q_status.CreatureOrGOCount[j]; + if (curTalkCount < reqTalkCount) + { + uint16 newCount = curTalkCount + addTalkCount; + +q_status.CreatureOrGOCount[j] = newCount; + +m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE; + +uint16 log_slot = FindQuestSlot(questid); +if (log_slot < MAX_QUEST_LOG_SIZE) + SetQuestSlotCounter(log_slot, j, newCount); + +sScriptMgr->OnQuestObjectiveCredit( + this, + questid, + j, + qInfo->RequiredNpcOrGo[j], + curTalkCount, + newCount, + guid); + +SendQuestUpdateAddCredit(qInfo, guid, j, newCount); + } + if (CanCompleteQuest(questid)) + CompleteQuest(questid); + + // same objective target can be in many active quests, but not in 2 objectives for single quest (code optimization). + continue; + } + } + } + } + } +} + +void Player::MoneyChanged(uint64 value) +{ + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (!questid) + continue; + + Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid); + int32 requiredMoney = qInfo ? qInfo->GetMoneyReward(this) : 0; + if (qInfo && requiredMoney < 0) + { + QuestStatusData& q_status = m_QuestStatus[questid]; + + if (q_status.Status == QUEST_STATUS_INCOMPLETE) + { + if (int64(value) >= -int64(requiredMoney)) + { + if (CanCompleteQuest(questid)) + CompleteQuest(questid); + } + } + else if (q_status.Status == QUEST_STATUS_COMPLETE) + { + if (int64(value) < -int64(requiredMoney)) + IncompleteQuest(questid); + } + } + } +} + +void Player::ReputationChanged(FactionEntry const* factionEntry) +{ + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + if (uint32 questid = GetQuestSlotQuestId(i)) + { + if (Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid)) + { + if (qInfo->GetRepObjectiveFaction() == factionEntry->ID) + { + QuestStatusData& q_status = m_QuestStatus[questid]; + if (q_status.Status == QUEST_STATUS_INCOMPLETE) + { + if (GetReputationMgr().GetReputation(factionEntry) >= qInfo->GetRepObjectiveValue()) + if (CanCompleteQuest(questid)) + CompleteQuest(questid); + } + else if (q_status.Status == QUEST_STATUS_COMPLETE) + { + if (GetReputationMgr().GetReputation(factionEntry) < qInfo->GetRepObjectiveValue()) + IncompleteQuest(questid); + } + } + } + } + } +} + +void Player::ReputationChanged2(FactionEntry const* factionEntry) +{ + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + if (uint32 questid = GetQuestSlotQuestId(i)) + { + if (Quest const* qInfo = sObjectMgr->GetQuestTemplate(questid)) + { + if (qInfo->GetRepObjectiveFaction2() == factionEntry->ID) + { + QuestStatusData& q_status = m_QuestStatus[questid]; + if (q_status.Status == QUEST_STATUS_INCOMPLETE) + { + if (GetReputationMgr().GetReputation(factionEntry) >= qInfo->GetRepObjectiveValue2()) + if (CanCompleteQuest(questid)) + CompleteQuest(questid); + } + else if (q_status.Status == QUEST_STATUS_COMPLETE) + { + if (GetReputationMgr().GetReputation(factionEntry) < qInfo->GetRepObjectiveValue2()) + IncompleteQuest(questid); + } + } + } + } + } +} + +bool Player::HasQuestForItem(uint32 itemid, uint32 excludeQuestId /* 0 */, bool turnIn /* false */) const +{ + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (questid == 0 || questid == excludeQuestId) + continue; + + QuestStatusMap::const_iterator qs_itr = m_QuestStatus.find(questid); + if (qs_itr == m_QuestStatus.end()) + continue; + + QuestStatusData const& q_status = qs_itr->second; + + if ((q_status.Status == QUEST_STATUS_INCOMPLETE) || (turnIn && q_status.Status == QUEST_STATUS_COMPLETE)) + { + Quest const* qinfo = sObjectMgr->GetQuestTemplate(questid); + if (!qinfo) + continue; + + // hide quest if player is in raid-group and quest is no raid quest + if (GetGroup() && GetGroup()->isRaidGroup() && !qinfo->IsAllowedInRaid(GetMap()->GetDifficulty())) + if (!InBattleground()) //there are two ways.. we can make every bg-quest a raidquest, or add this code here.. i don't know if this can be exploited by other quests, but i think all other quests depend on a specific area.. but keep this in mind, if something strange happens later + continue; + + // There should be no mixed ReqItem/ReqSource drop + // This part for ReqItem drop + for (uint8 j = 0; j < QUEST_ITEM_OBJECTIVES_COUNT; ++j) + { + if ((itemid == qinfo->RequiredItemId[j] && q_status.ItemCount[j] < qinfo->RequiredItemCount[j]) || (turnIn && q_status.ItemCount[j] >= qinfo->RequiredItemCount[j])) + return true; + } + // This part - for ReqSource + for (uint8 j = 0; j < QUEST_SOURCE_ITEM_IDS_COUNT; ++j) + { + // examined item is a source item + if (qinfo->ItemDrop[j] == itemid) + { + ItemTemplate const* pProto = sObjectMgr->GetItemTemplate(itemid); + + uint32 ownedCount = GetItemCount(itemid, true); + // 'unique' item + if ((pProto->GetMaxCount() && ownedCount < pProto->GetMaxCount()) || (turnIn && ownedCount >= pProto->GetMaxCount())) + return true; + + // allows custom amount drop when not 0 + if (qinfo->ItemDropQuantity[j]) + { + if (ownedCount < qinfo->ItemDropQuantity[j] || turnIn ) + return true; + } else if (ownedCount < pProto->GetMaxStackSize()) + return true; + } + } + } + } + return false; +} + +void Player::SendQuestComplete(Quest const* quest) const +{ + if (quest) + { + WorldPackets::Quest::QuestUpdateComplete packet; + packet.QuestID = quest->GetQuestId(); + SendDirectMessage(packet.Write()); + } +} + +void Player::SendQuestReward(Quest const* quest, Creature const* questGiver, uint32 xp) +{ + uint32 questId = quest->GetQuestId(); + sGameEventMgr->HandleQuestComplete(questId); + + WorldPackets::Quest::QuestGiverQuestComplete packet; + packet.QuestID = questId; + packet.MoneyReward = GetQuestMoneyReward(quest); + packet.XPReward = IsMaxLevel() ? 0 : xp; + packet.NumSkillUpsReward = quest->GetRewardSkillPoints(); + packet.TalentReward = quest->GetBonusTalents(); + packet.SkillLineIDReward = quest->GetRewardSkillId(); + + if (questGiver) + { + if (questGiver->IsGossip()) + packet.LaunchGossip = quest->HasFlag(QUEST_FLAGS_LAUNCH_GOSSIP_COMPLETE); + + else if (quest->GetNextQuestInChain() && !quest->HasFlag(QUEST_FLAGS_AUTO_COMPLETE)) + if (Quest const* rewardQuest = sObjectMgr->GetQuestTemplate(quest->GetNextQuestInChain())) + packet.UseQuestReward = CanTakeQuest(rewardQuest, false); + } + + SendDirectMessage(packet.Write()); +} + +void Player::SendQuestFailed(uint32 questId, InventoryResult reason) const +{ + if (questId) + { + WorldPackets::Quest::QuestGiverQuestFailed packet; + packet.QuestID = questId; + packet.Reason = reason; + SendDirectMessage(packet.Write()); + } +} + +void Player::SendQuestTimerFailed(uint32 questId) const +{ + if (questId) + { + WorldPackets::Quest::QuestUpdateFailedTimer packet; + packet.QuestID = questId; + SendDirectMessage(packet.Write()); + } +} + +void Player::SendCanTakeQuestResponse(QuestFailedReason reason) const +{ + WorldPackets::Quest::QuestGiverInvalidQuest packet; + packet.Reason = reason; + SendDirectMessage(packet.Write()); +} + +void Player::SendQuestConfirmAccept(Quest const* quest, Player* receiver) const +{ + if (!receiver) + return; + + WorldPackets::Quest::QuestConfirmAcceptResponse packet; + + packet.QuestTitle = quest->GetTitle(); + uint32 questID = quest->GetQuestId(); + + LocaleConstant localeConstant = receiver->GetSession()->GetSessionDbLocaleIndex(); + if (localeConstant != LOCALE_enUS) + if (QuestLocale const* pLocale = sObjectMgr->GetQuestLocale(questID)) + ObjectMgr::GetLocaleString(pLocale->Title, localeConstant, packet.QuestTitle); + + packet.QuestID = questID; + packet.InitiatedBy = GetGUID(); + + receiver->GetSession()->SendPacket(packet.Write()); +} + +void Player::SendPushToPartyResponse(Player const* player, uint8 msg) const +{ + if (player) + { + WorldPackets::Quest::QuestPushResultResponse packet; + packet.SenderGUID = player->GetGUID(); + packet.Result = msg; + SendDirectMessage(packet.Write()); + } +} + +void Player::SendQuestUpdateAddCredit(Quest const* quest, ObjectGuid guid, uint32 creatureOrGOIdx, uint16 count) +{ + int32 entry = quest->RequiredNpcOrGo[creatureOrGOIdx]; + if (entry < 0) + // client expected gameobject template id in form (id|0x80000000) + entry = (-entry) | 0x80000000; + + WorldPackets::Quest::QuestUpdateAddCredit packet; + packet.VictimGUID = guid; + packet.QuestID = quest->GetQuestId(); + packet.ObjectID = entry; + packet.Count = count; + packet.Required = quest->RequiredNpcOrGoCount[creatureOrGOIdx]; + SendDirectMessage(packet.Write()); +} + +void Player::SendQuestUpdateAddPlayer(Quest const* quest, uint16 newCount) +{ + WorldPackets::Quest::QuestUpdateAddPvPCredit packet; + packet.QuestID = quest->GetQuestId(); + packet.Count = newCount; + packet.Required = quest->GetPlayersSlain(); + SendDirectMessage(packet.Write()); +} + +void Player::SendQuestGiverStatusMultiple() +{ + WorldPackets::Quest::QuestGiverStatusMultiple response; + + for (auto itr = m_clientGUIDs.begin(); itr != m_clientGUIDs.end(); ++itr) + { + if (itr->IsAnyTypeCreature()) + { + // need also pet quests case support + Creature* questgiver = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, *itr); + if (!questgiver || questgiver->IsHostileTo(this)) + continue; + if (!questgiver->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_QUESTGIVER)) + continue; + + response.QuestGiver.emplace_back(questgiver->GetGUID(), GetQuestDialogStatus(questgiver)); + } + else if (itr->IsGameObject()) + { + GameObject* questgiver = GetMap()->GetGameObject(*itr); + if (!questgiver || questgiver->GetGoType() != GAMEOBJECT_TYPE_QUESTGIVER) + continue; + + response.QuestGiver.emplace_back(questgiver->GetGUID(), GetQuestDialogStatus(questgiver)); + } + } + + SendDirectMessage(response.Write()); +} + +bool Player::HasPvPForcingQuest() const +{ + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questId = GetQuestSlotQuestId(i); + if (questId == 0) + continue; + + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + continue; + + if (quest->HasFlag(QUEST_FLAGS_FLAGS_PVP)) + return true; + } + + return false; +} + +/*********************************************************/ +/*** LOAD SYSTEM ***/ +/*********************************************************/ + +void Player::Initialize(ObjectGuid::LowType guid) +{ + Object::_Create(guid, 0, HighGuid::Player); +} + +void Player::_LoadDeclinedNames(PreparedQueryResult result) +{ + if (!result) + return; + + m_declinedname = std::make_unique(); + for (uint8 i = 0; i < MAX_DECLINED_NAME_CASES; ++i) + m_declinedname->name[i] = (*result)[i].GetString(); +} + +void Player::_LoadArenaTeamInfo(PreparedQueryResult result) +{ + // arenateamid, played_week, played_season, personal_rating + memset((void*)&m_uint32Values[PLAYER_FIELD_ARENA_TEAM_INFO_1_1], 0, sizeof(uint32) * MAX_ARENA_SLOT * ARENA_TEAM_END); + + uint16 personalRatingCache[] = {0, 0, 0}; + + if (result) + { + do + { + Field* fields = result->Fetch(); + + uint32 arenaTeamId = fields[0].GetUInt32(); + + ArenaTeam* arenaTeam = sArenaTeamMgr->GetArenaTeamById(arenaTeamId); + if (!arenaTeam) + { + TC_LOG_ERROR("entities.player", "Player::_LoadArenaTeamInfo: couldn't load arenateam %u", arenaTeamId); + continue; + } + + uint8 arenaSlot = arenaTeam->GetSlot(); + + personalRatingCache[arenaSlot] = fields[4].GetUInt16(); + + SetArenaTeamInfoField(arenaSlot, ARENA_TEAM_ID, arenaTeamId); + SetArenaTeamInfoField(arenaSlot, ARENA_TEAM_TYPE, arenaTeam->GetType()); + SetArenaTeamInfoField(arenaSlot, ARENA_TEAM_MEMBER, (arenaTeam->GetCaptain() == GetGUID()) ? 0 : 1); + SetArenaTeamInfoField(arenaSlot, ARENA_TEAM_GAMES_WEEK, uint32(fields[1].GetUInt16())); + SetArenaTeamInfoField(arenaSlot, ARENA_TEAM_GAMES_SEASON, uint32(fields[2].GetUInt16())); + SetArenaTeamInfoField(arenaSlot, ARENA_TEAM_WINS_SEASON, uint32(fields[3].GetUInt16())); + } + while (result->NextRow()); + } + + for (uint8 slot = 0; slot <= 2; ++slot) + { + SetArenaTeamInfoField(slot, ARENA_TEAM_PERSONAL_RATING, uint32(personalRatingCache[slot])); + } +} + +void Player::_LoadEquipmentSets(PreparedQueryResult result) +{ + // SetPQuery(PLAYER_LOGIN_QUERY_LOADEQUIPMENTSETS, "SELECT setguid, setindex, name, iconname, item0, item1, item2, item3, item4, item5, item6, item7, item8, item9, item10, item11, item12, item13, item14, item15, item16, item17, item18 FROM character_equipmentsets WHERE guid = '%u' ORDER BY setindex", GUID_LOPART(m_guid)); + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + EquipmentSetInfo eqSet; + + eqSet.Data.Guid = fields[0].GetUInt64(); + eqSet.Data.SetID = fields[1].GetUInt8(); + eqSet.Data.SetName = fields[2].GetString(); + eqSet.Data.SetIcon = fields[3].GetString(); + eqSet.Data.IgnoreMask = fields[4].GetUInt32(); + eqSet.State = EQUIPMENT_SET_UNCHANGED; + + for (uint32 i = 0; i < EQUIPMENT_SLOT_END; ++i) + if (ObjectGuid::LowType guid = fields[5 + i].GetUInt32()) + eqSet.Data.Pieces[i] = ObjectGuid::Create(guid); + + // client limit + if (eqSet.Data.SetID >= MAX_EQUIPMENT_SET_INDEX) + continue; + + _equipmentSets[eqSet.Data.Guid] = eqSet; + } while (result->NextRow()); +} + +void Player::_LoadBGData(PreparedQueryResult result) +{ + if (!result) + return; + + Field* fields = result->Fetch(); + // Expecting only one row + // 0 1 2 3 4 5 6 7 8 9 + // SELECT instanceId, team, joinX, joinY, joinZ, joinO, joinMapId, taxiStart, taxiEnd, mountSpell FROM character_battleground_data WHERE guid = ? + + m_bgData.bgInstanceID = fields[0].GetUInt32(); + m_bgData.bgTeam = fields[1].GetUInt16(); + m_bgData.joinPos = WorldLocation(fields[6].GetUInt16(), // Map + fields[2].GetFloat(), // X + fields[3].GetFloat(), // Y + fields[4].GetFloat(), // Z + fields[5].GetFloat()); // Orientation + m_bgData.taxiPath[0] = fields[7].GetUInt32(); + m_bgData.taxiPath[1] = fields[8].GetUInt32(); + m_bgData.mountSpell = fields[9].GetUInt32(); +} + +bool Player::LoadPositionFromDB(uint32& mapid, float& x, float& y, float& z, float& o, bool& in_flight, ObjectGuid guid) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_POSITION); + stmt->setUInt32(0, guid.GetCounter()); + PreparedQueryResult result = CharacterDatabase.Query(stmt); + + if (!result) + return false; + + Field* fields = result->Fetch(); + + x = fields[0].GetFloat(); + y = fields[1].GetFloat(); + z = fields[2].GetFloat(); + o = fields[3].GetFloat(); + mapid = fields[4].GetUInt16(); + in_flight = !fields[5].GetString().empty(); + + return true; +} + +void Player::SetHomebind(WorldLocation const& loc, uint32 areaId) +{ + loc.GetPosition(m_homebindX, m_homebindY, m_homebindZ); + m_homebindMapId = loc.GetMapId(); + m_homebindAreaId = areaId; + + // update sql homebind + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_PLAYER_HOMEBIND); + stmt->setUInt16(0, m_homebindMapId); + stmt->setUInt16(1, m_homebindAreaId); + stmt->setFloat (2, m_homebindX); + stmt->setFloat (3, m_homebindY); + stmt->setFloat (4, m_homebindZ); + stmt->setUInt32(5, GetGUID().GetCounter()); + CharacterDatabase.Execute(stmt); +} + +void Player::SendBindPointUpdate() const +{ + WorldPackets::Misc::BindPointUpdate packet; + packet.BindPosition = Position(m_homebindX, m_homebindY, m_homebindZ); + packet.BindMapID = m_homebindMapId; + packet.BindAreaID = m_homebindAreaId; + SendDirectMessage(packet.Write()); +} + +bool Player::IsLoading() const +{ + return GetSession()->PlayerLoading(); +} + +bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& holder) +{ + //// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + //QueryResult* result = CharacterDatabase.PQuery("SELECT guid, account, name, race, class, gender, level, xp, money, skin, face, hairStyle, hairColor, facialStyle, bankSlots, restState, " + // 16, 17 18 19 20 21 22 23 24 25 26 27 28 29 30 + //" characterFlags, characterFlags2, position_x, position_y, position_z, map, orientation, taximask, cinematic, totaltime, leveltime, rest_bonus, logout_time, is_logout_resting, resettalents_cost, " + // 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 + //"resettalents_time, talentTree, trans_x, trans_y, trans_z, trans_o, transguid, extra_flags, stable_slots, at_login, zone, online, death_expire_time, taxi_path, instance_mode_mask, " + // 46 47 48 49 50 51 + //"totalKills, todayKills, yesterdayKills, chosenTitle, watchedFaction, drunk, " + // 52 53 54 55 56 57 58 59 60 61 62 + //"health, power1, power2, power3, power4, power5, instance_id, speccount, activespec, exploredZones, equipmentCache, " + // 63 64 65 66 + //"knownTitles, actionBars, grantableLevels, fishing_steps FROM characters WHERE guid = '%u'", guid); + + PreparedQueryResult result = holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_FROM); + if (!result) + { + std::string name = ""; + sCharacterCache->GetCharacterNameByGuid(guid, name); + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player '%s' (%s) not found in table `characters`, can't load. ", name.c_str(), guid.ToString().c_str()); + return false; + } + + Field* fields = result->Fetch(); + + uint32 dbAccountId = fields[1].GetUInt32(); + + // check if the character's account in the db and the logged in account match. + // player should be able to load/delete character only with correct account! + if (dbAccountId != GetSession()->GetAccountId()) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) loading from wrong account (is: %u, should be: %u)", guid.ToString().c_str(), GetSession()->GetAccountId(), dbAccountId); + return false; + } + + if (holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_BANNED)) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) is banned, can't load.", guid.ToString().c_str()); + return false; + } + + Object::_Create(guid.GetCounter(), 0, HighGuid::Player); + + m_name = fields[2].GetString(); + + // check name limitations + if (ObjectMgr::CheckPlayerName(m_name, GetSession()->GetSessionDbcLocale()) != CHAR_NAME_SUCCESS || + (!GetSession()->HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHARACTER_CREATION_RESERVEDNAME) && sObjectMgr->IsReservedName(m_name))) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_ADD_AT_LOGIN_FLAG); + stmt->setUInt16(0, uint16(AT_LOGIN_RENAME)); + stmt->setUInt32(1, guid.GetCounter()); + CharacterDatabase.Execute(stmt); + return false; + } + + // overwrite possible wrong/corrupted guid + SetGuidValue(OBJECT_FIELD_GUID, guid); + + uint8 gender = fields[5].GetUInt8(); + if (!IsValidGender(gender)) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) has wrong gender (%u), can't load.", guid.ToString().c_str(), gender); + return false; + } + + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_RACE, fields[3].GetUInt8()); + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_CLASS, fields[4].GetUInt8()); + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_GENDER, gender); + + // check if race/class combination is valid + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(), getClass()); + if (!info) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) has wrong race/class (%u/%u), can't load.", guid.ToString().c_str(), getRace(), getClass()); + return false; + } + + SetUInt32Value(UNIT_FIELD_LEVEL, fields[6].GetUInt8()); + SetUInt32Value(PLAYER_XP, fields[7].GetUInt32()); + + if (!_LoadIntoDataField(fields[61].GetString(), PLAYER_EXPLORED_ZONES_1, PLAYER_EXPLORED_ZONES_SIZE)) + TC_LOG_WARN("entities.player.loading", "Player::LoadFromDB: Player (%s) has invalid exploredzones data (%s). Forcing partial load.", guid.ToString().c_str(), fields[61].GetCString()); + + if (!_LoadIntoDataField(fields[63].GetString(), PLAYER__FIELD_KNOWN_TITLES, KNOWN_TITLES_SIZE * 2)) + TC_LOG_WARN("entities.player.loading", "Player::LoadFromDB: Player (%s) has invalid knowntitles mask (%s). Forcing partial load.", guid.ToString().c_str(), fields[63].GetCString()); + + SetObjectScale(1.0f); + SetFloatValue(UNIT_FIELD_HOVERHEIGHT, 1.0f); + + // load achievements before anything else to prevent multiple gains for the same achievement/criteria on every loading (as loading does call UpdateAchievementCriteria) + m_achievementMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_ACHIEVEMENTS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_CRITERIA_PROGRESS)); + + uint64 money = fields[8].GetUInt64(); + if (money > MAX_MONEY_AMOUNT) + money = MAX_MONEY_AMOUNT; + SetMoney(money); + + SetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID, fields[9].GetUInt8()); + SetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_FACE_ID, fields[10].GetUInt8()); + SetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_STYLE_ID, fields[11].GetUInt8()); + SetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID, fields[12].GetUInt8()); + SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_FACIAL_STYLE, fields[13].GetUInt8()); + SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS, fields[14].GetUInt8()); + SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_REST_STATE, fields[15].GetUInt8()); + SetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_GENDER, fields[5].GetUInt8()); + SetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_INEBRIATION, fields[51].GetUInt8()); + + if (!ValidateAppearance( + fields[3].GetUInt8(), // race + fields[4].GetUInt8(), // class + gender, + GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_STYLE_ID), // hair type + GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID), //hair color + GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_FACE_ID), // face + GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_FACIAL_STYLE), // facial hair + GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID))) // skin color + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) has wrong Appearance values (Hair/Skin/Color), can't load.", guid.ToString().c_str()); + return false; + } + + _characterFlags = static_cast(fields[16].GetUInt32()); + _characterFlags2 = static_cast(fields[17].GetUInt32()); + + InitializePlayerFlags(); + + SetInt32Value(PLAYER_FIELD_WATCHED_FACTION_INDEX, fields[50].GetUInt32()); + + // set which actionbars the client has active - DO NOT REMOVE EVER AGAIN (can be changed though, if it does change fieldwise) + SetByteValue(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTES_OFFSET_ACTION_BAR_TOGGLES, fields[64].GetUInt8()); + + m_fishingSteps = fields[66].GetUInt8(); + + InitDisplayIds(); + + // cleanup inventory related item value fields (it will be filled correctly in _LoadInventory) + for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + { + SetGuidValue(PLAYER_FIELD_INV_SLOT_HEAD + (slot * 2), ObjectGuid::Empty); + SetVisibleItemSlot(slot, nullptr); + + delete m_items[slot]; + m_items[slot] = nullptr; + } + + TC_LOG_DEBUG("entities.player.loading", "Player::LoadFromDB: Load Basic value of player '%s' is: ", m_name.c_str()); + outDebugValues(); + + //Need to call it to initialize m_team (m_team can be calculated from race) + //Other way is to saves m_team into characters table. + SetFactionForRace(getRace()); + + // load home bind and check in same time class/race pair, it used later for restore broken positions + if (!_LoadHomeBind(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_HOME_BIND))) + return false; + + InitializeSkillFields(); + InitPrimaryProfessions(); // to max set before any spell loaded + + // init saved position, and fix it later if problematic + ObjectGuid::LowType transLowGUID = fields[37].GetUInt32(); + + Relocate(fields[18].GetFloat(), fields[19].GetFloat(), fields[20].GetFloat(), fields[22].GetFloat()); + + uint32 mapId = fields[21].GetUInt16(); + uint32 instanceId = fields[58].GetUInt32(); + + uint32 dungeonDiff = fields[45].GetUInt8() & 0x0F; + if (dungeonDiff >= MAX_DUNGEON_DIFFICULTY) + dungeonDiff = DUNGEON_DIFFICULTY_NORMAL; + uint32 raidDiff = (fields[45].GetUInt8() >> 4) & 0x0F; + if (raidDiff >= MAX_RAID_DIFFICULTY) + raidDiff = RAID_DIFFICULTY_10MAN_NORMAL; + SetDungeonDifficulty(Difficulty(dungeonDiff)); // may be changed in _LoadGroup + SetRaidDifficulty(Difficulty(raidDiff)); // may be changed in _LoadGroup + + std::string taxi_nodes = fields[44].GetString(); + +#define RelocateToHomebind(){ mapId = m_homebindMapId; instanceId = 0; Relocate(m_homebindX, m_homebindY, m_homebindZ); } + + _LoadGroup(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GROUP)); + + _LoadArenaTeamInfo(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_ARENA_INFO)); + + // check arena teams integrity + for (uint32 arena_slot = 0; arena_slot < MAX_ARENA_SLOT; ++arena_slot) + { + uint32 arena_team_id = GetArenaTeamId(arena_slot); + if (!arena_team_id) + continue; + + if (ArenaTeam* at = sArenaTeamMgr->GetArenaTeamById(arena_team_id)) + if (at->IsMember(GetGUID())) + continue; + + // arena team not exist or not member, cleanup fields + for (int j = 0; j < 6; ++j) + SetArenaTeamInfoField(arena_slot, ArenaTeamInfoType(j), 0); + } + + _LoadCurrency(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_CURRENCY)); + SetUInt32Value(PLAYER_FIELD_LIFETIME_HONORABLE_KILLS, fields[46].GetUInt32()); + SetUInt16Value(PLAYER_FIELD_KILLS, 0, fields[47].GetUInt16()); + SetUInt16Value(PLAYER_FIELD_KILLS, 1, fields[48].GetUInt16()); + + _LoadBoundInstances(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_BOUND_INSTANCES)); + _LoadInstanceTimeRestrictions(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_INSTANCE_LOCK_TIMES)); + _LoadBGData(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_BG_DATA)); + + GetSession()->SetPlayer(this); + MapEntry const* mapEntry = sMapStore.LookupEntry(mapId); + Map* map = nullptr; + bool player_at_bg = false; + if (!mapEntry || !IsPositionValid()) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) has invalid coordinates (MapId: %u X: %f Y: %f Z: %f O: %f). Teleport to default race/class locations.", + guid.ToString().c_str(), mapId, GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation()); + RelocateToHomebind(); + } + // Player was saved in Arena or Bg + else if (mapEntry->IsBattlegroundOrArena()) + { + Battleground* currentBg = nullptr; + if (m_bgData.bgInstanceID) //saved in Battleground + currentBg = sBattlegroundMgr->GetBattleground(m_bgData.bgInstanceID, BATTLEGROUND_TYPE_NONE); + + player_at_bg = currentBg && currentBg->IsPlayerInBattleground(GetGUID()); + + if (player_at_bg && currentBg->GetStatus() != STATUS_WAIT_LEAVE) + { + map = currentBg->GetBgMap(); + + BattlegroundQueueTypeId bgQueueTypeId = sBattlegroundMgr->BGQueueTypeId(currentBg->GetTypeID(), currentBg->GetArenaType()); + AddBattlegroundQueueId(bgQueueTypeId); + + m_bgData.bgTypeID = currentBg->GetTypeID(); + + //join player to battleground group + currentBg->EventPlayerLoggedIn(this); + + SetInviteForBattlegroundQueueType(bgQueueTypeId, currentBg->GetInstanceID()); + } + // Bg was not found - go to Entry Point + else + { + // leave bg + if (player_at_bg) + { + player_at_bg = false; + currentBg->RemovePlayerAtLeave(GetGUID(), false, true); + } + + // Do not look for instance if bg not found + WorldLocation const& _loc = GetBattlegroundEntryPoint(); + mapId = _loc.GetMapId(); + instanceId = 0; + + // Db field type is type int16, so it can never be MAPID_INVALID + //if (mapId == MAPID_INVALID) -- code kept for reference + if (int16(mapId) == int16(-1)) // Battleground Entry Point not found (???) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) was in BG in database, but BG was not found and entry point was invalid! Teleport to default race/class locations.", + guid.ToString().c_str()); + RelocateToHomebind(); + } + else + Relocate(&_loc); + + // We are not in BG anymore + m_bgData.bgInstanceID = 0; + } + } + // currently we do not support transport in bg + else if (transLowGUID) + { + ObjectGuid transGUID(HighGuid::Mo_Transport, transLowGUID); + + Transport* transport = nullptr; + if (Map* transportMap = sMapMgr->CreateMap(mapId, this, instanceId)) + { + if (Transport* transportOnMap = transportMap->GetTransport(transGUID)) + { + if (transportOnMap->GetExpectedMapId() != mapId) + { + mapId = transportOnMap->GetExpectedMapId(); + instanceId = 0; + transportMap = sMapMgr->CreateMap(mapId, this, instanceId); + if (transportMap) + transport = transportMap->GetTransport(transGUID); + } + else + transport = transportOnMap; + } + } + + if (transport) + { + float x = fields[33].GetFloat(), y = fields[34].GetFloat(), z = fields[35].GetFloat(), o = fields[36].GetFloat(); + m_movementInfo.transport.pos.Relocate(x, y, z, o); + transport->CalculatePassengerPosition(x, y, z, &o); + + if (!Trinity::IsValidMapCoord(x, y, z, o) || + // transport size limited + std::fabs(m_movementInfo.transport.pos.GetPositionX()) > 250.0f || + std::fabs(m_movementInfo.transport.pos.GetPositionY()) > 250.0f || + std::fabs(m_movementInfo.transport.pos.GetPositionZ()) > 250.0f) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) has invalid transport coordinates (X: %f Y: %f Z: %f O: %f). Teleport to bind location.", + guid.ToString().c_str(), x, y, z, o); + + m_movementInfo.transport.Reset(); + + RelocateToHomebind(); + } + else + { + Relocate(x, y, z, o); + mapId = transport->GetMapId(); + + transport->AddPassenger(this); + } + } + else + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) has problems with transport guid (%u). Teleport to bind location.", + guid.ToString().c_str(), transLowGUID); + + RelocateToHomebind(); + } + } + // currently we do not support taxi in instance + else if (!taxi_nodes.empty()) + { + instanceId = 0; + + // Not finish taxi flight path + if (m_bgData.HasTaxiPath()) + { + for (int i = 0; i < 2; ++i) + m_taxi.AddTaxiDestination(m_bgData.taxiPath[i]); + } + else if (!m_taxi.LoadTaxiDestinationsFromString(taxi_nodes, GetTeam())) + { + // problems with taxi path loading + TaxiNodesEntry const* nodeEntry = nullptr; + if (uint32 node_id = m_taxi.GetTaxiSource()) + nodeEntry = sTaxiNodesStore.LookupEntry(node_id); + + if (!nodeEntry) // don't know taxi start node, teleport to homebind + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) has wrong data in taxi destination list, teleport to homebind.", GetGUID().ToString().c_str()); + RelocateToHomebind(); + } + else // has start node, teleport to it + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) has too short taxi destination list, teleport to original node.", GetGUID().ToString().c_str()); + mapId = nodeEntry->ContinentID; + Relocate(nodeEntry->Pos.X, nodeEntry->Pos.Y, nodeEntry->Pos.Z, 0.0f); + } + m_taxi.ClearTaxiDestinations(); + } + + if (uint32 node_id = m_taxi.GetTaxiSource()) + { + // save source node as recall coord to prevent recall and fall from sky + TaxiNodesEntry const* nodeEntry = sTaxiNodesStore.LookupEntry(node_id); + if (nodeEntry && nodeEntry->ContinentID == GetMapId()) + { + ASSERT(nodeEntry); // checked in m_taxi.LoadTaxiDestinationsFromString + mapId = nodeEntry->ContinentID; + Relocate(nodeEntry->Pos.X, nodeEntry->Pos.Y, nodeEntry->Pos.Z, 0.0f); + } + + // flight will started later + } + } + + // Map could be changed before + mapEntry = sMapStore.LookupEntry(mapId); + // client without expansion support + if (mapEntry) + { + if (GetSession()->GetExpansion() < mapEntry->Expansion()) + { + TC_LOG_DEBUG("entities.player.loading.loading", "Player::LoadFromDB: Player '%s' (%s) using client without required expansion tried login at non accessible map %u", + GetName().c_str(), GetGUID().ToString().c_str(), mapId); + RelocateToHomebind(); + } + + // fix crash (because of if (Map* map = _FindMap(instanceId)) in MapInstanced::CreateInstance) + if (instanceId) + if (InstanceSave* save = GetInstanceSave(mapId, mapEntry->IsRaid())) + if (save->GetInstanceId() != instanceId) + instanceId = 0; + } + + // NOW player must have valid map + // load the player's map here if it's not already loaded + if (!map) + map = sMapMgr->CreateMap(mapId, this, instanceId); + AreaTriggerStruct const* areaTrigger = nullptr; + bool check = false; + + if (!map) + { + areaTrigger = sObjectMgr->GetGoBackTrigger(mapId); + check = true; + } + else if (map->IsDungeon()) // if map is dungeon... + { + if (Map::EnterState denyReason = ((InstanceMap*)map)->CannotEnter(this)) // ... and can't enter map, then look for entry point. + { + switch (denyReason) + { + case Map::CANNOT_ENTER_DIFFICULTY_UNAVAILABLE: + SendTransferAborted(map->GetId(), TRANSFER_ABORT_DIFFICULTY, map->GetDifficulty()); + break; + case Map::CANNOT_ENTER_INSTANCE_BIND_MISMATCH: + SendTransferAborted(map->GetId(), TRANSFER_ABORT_LOCKED_TO_DIFFERENT_INSTANCE); + break; + case Map::CANNOT_ENTER_TOO_MANY_INSTANCES: + SendTransferAborted(map->GetId(), TRANSFER_ABORT_TOO_MANY_INSTANCES); + break; + case Map::CANNOT_ENTER_MAX_PLAYERS: + SendTransferAborted(map->GetId(), TRANSFER_ABORT_MAX_PLAYERS); + break; + case Map::CANNOT_ENTER_ZONE_IN_COMBAT: + SendTransferAborted(map->GetId(), TRANSFER_ABORT_ZONE_IN_COMBAT); + break; + default: + break; + } + areaTrigger = sObjectMgr->GetGoBackTrigger(mapId); + check = true; + } + else if (instanceId && !sInstanceSaveMgr->GetInstanceSave(instanceId)) // ... and instance is reseted then look for entrance. + { + areaTrigger = sObjectMgr->GetMapEntranceTrigger(mapId); + check = true; + } + } + + if (check) // in case of special event when creating map... + { + if (areaTrigger) // ... if we have an areatrigger, then relocate to new map/coordinates. + { + Relocate(areaTrigger->target_X, areaTrigger->target_Y, areaTrigger->target_Z, GetOrientation()); + if (mapId != areaTrigger->target_mapId) + { + mapId = areaTrigger->target_mapId; + map = sMapMgr->CreateMap(mapId, this); + } + } + else + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player '%s' (%s) Map: %u, X: %f, Y: %f, Z: %f, O: %f. Areatrigger not found.", + m_name.c_str(), guid.ToString().c_str(), mapId, GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation()); + RelocateToHomebind(); + map = nullptr; + } + } + + if (!map) + { + mapId = info->mapId; + Relocate(info->positionX, info->positionY, info->positionZ, 0.0f); + map = sMapMgr->CreateMap(mapId, this); + if (!map) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player '%s' (%s) Map: %u, X: %f, Y: %f, Z: %f, O: %f. Invalid default map coordinates or instance couldn't be created.", + m_name.c_str(), guid.ToString().c_str(), mapId, GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation()); + return false; + } + } + + SetMap(map); + StoreRaidMapDifficulty(); + + // now that map position is determined, check instance validity + if (!CheckInstanceValidity(true) && !IsInstanceLoginGameMasterException()) + m_InstanceValid = false; + + if (player_at_bg) + map->ToBattlegroundMap()->GetBG()->AddPlayer(this); + + // randomize first save time in range [CONFIG_INTERVAL_SAVE] around [CONFIG_INTERVAL_SAVE] + // this must help in case next save after mass player load after server startup + m_nextSave = urand(m_nextSave / 2, m_nextSave * 3 / 2); + + SaveRecallPosition(); + + time_t now = GameTime::GetGameTime(); + time_t logoutTime = time_t(fields[28].GetUInt32()); + + // since last logout (in seconds) + uint32 time_diff = uint32(now - logoutTime); //uint64 is excessive for a time_diff in seconds.. uint32 allows for 136~ year difference. + + // set value, including drunk invisibility detection + // calculate sobering. after 15 minutes logged out, the player will be sober again + uint8 newDrunkValue = 0; + if (time_diff < uint32(GetDrunkValue()) * 9) + newDrunkValue = GetDrunkValue() - time_diff / 9; + + SetDrunkValue(newDrunkValue); + + m_cinematic = fields[24].GetUInt8(); + m_Played_time[PLAYED_TIME_TOTAL]= fields[25].GetUInt32(); + m_Played_time[PLAYED_TIME_LEVEL]= fields[26].GetUInt32(); + + SetTalentResetCost(fields[30].GetUInt32()); + SetTalentResetTime(time_t(fields[31].GetUInt32())); + + SetSpecsCount(fields[59].GetUInt8()); + SetActiveSpec(fields[60].GetUInt8()); + + // sanity check + if (GetSpecsCount() > MAX_TALENT_SPECS || GetActiveSpec() > MAX_TALENT_SPEC || GetSpecsCount() < MIN_TALENT_SPECS) + { + SetActiveSpec(0); + TC_LOG_ERROR("entities.player.loading", "Player %s(GUID: %u) has SpecCount = %u and ActiveSpec = %u.", GetName().c_str(), GetGUID().GetCounter(), GetSpecsCount(), GetActiveSpec()); + } + + // Only load selected specializations, learning mastery spells requires this + std::vector talentTrees = Trinity::Tokenize(fields[32].GetStringView(), ' ', false); + for (uint8 i = 0; i < MAX_TALENT_SPECS; ++i) + { + if (i >= talentTrees.size()) + break; + + Optional talentTree = Trinity::StringTo(talentTrees[i]); + if (talentTree && sTalentTabStore.LookupEntry(*talentTree)) + SetPrimaryTalentTree(i, *talentTree); + } + + if (!m_taxi.LoadTaxiMask(fields[23].GetString())) // must be before InitTaxiNodesForLevel + TC_LOG_WARN("entities.player.loading", "Player::LoadFromDB: Player (%s) has invalid taximask (%s) in DB. Forced partial load.", GetGUID().ToString().c_str(), fields[23].GetString().c_str()); + + uint32 extraflags = fields[38].GetUInt16(); + + m_stableSlots = fields[39].GetUInt8(); + if (m_stableSlots > MAX_PET_STABLES) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player (%s) can't have more stable slots than %u, but has %u in DB", + GetGUID().ToString().c_str(), MAX_PET_STABLES, uint32(m_stableSlots)); + m_stableSlots = MAX_PET_STABLES; + } + + m_atLoginFlags = fields[40].GetUInt16(); + + if (HasAtLoginFlag(AT_LOGIN_RENAME)) + { + TC_LOG_INFO("entities.player.cheat", "Player::LoadFromDB: Player (%s) tried to login while forced to rename, can't load.'", GetGUID().ToString().c_str()); + return false; + } + + // Honor system + // Update Honor kills data + m_lastHonorUpdateTime = logoutTime; + UpdateHonorFields(); + + m_deathExpireTime = time_t(fields[43].GetUInt32()); + + if (m_deathExpireTime > now + MAX_DEATH_COUNT * DEATH_EXPIRE_STEP) + m_deathExpireTime = now + MAX_DEATH_COUNT * DEATH_EXPIRE_STEP - 1; + + // clear channel spell data (if saved at channel spell casting) + SetChannelObjectGuid(ObjectGuid::Empty); + SetUInt32Value(UNIT_CHANNEL_SPELL, 0); + + // clear charm/summon related fields + SetOwnerGUID(ObjectGuid::Empty); + SetGuidValue(UNIT_FIELD_CHARMEDBY, ObjectGuid::Empty); + SetGuidValue(UNIT_FIELD_CHARM, ObjectGuid::Empty); + SetGuidValue(UNIT_FIELD_SUMMON, ObjectGuid::Empty); + SetGuidValue(PLAYER_FARSIGHT, ObjectGuid::Empty); + SetCreatorGUID(ObjectGuid::Empty); + + RemoveFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_FORCE_MOVEMENT); + + // reset some aura modifiers before aura apply + SetUInt32Value(PLAYER_TRACK_CREATURES, 0); + SetUInt32Value(PLAYER_TRACK_RESOURCES, 0); + + // make sure the unit is considered out of combat for proper loading + ClearInCombat(); + + // make sure the unit is considered not in duel for proper loading + SetGuidValue(PLAYER_DUEL_ARBITER, ObjectGuid::Empty); + SetUInt32Value(PLAYER_DUEL_TEAM, 0); + + RegisterPowerTypes(); + UpdateDisplayPower(); + + // reset stats before loading any modifiers + InitStatsForLevel(); + InitGlyphsForLevel(); + InitTaxiNodesForLevel(); + InitRunes(); + + // rest bonus can only be calculated after InitStatsForLevel() + m_rest_bonus = fields[27].GetFloat(); + + if (time_diff > 0) + { + //speed collect rest bonus in offline, in logout, far from tavern, city (section/in hour) + float bubble0 = 0.031f; + //speed collect rest bonus in offline, in logout, in tavern, city (section/in hour) + float bubble1 = 0.125f; + float bubble = fields[29].GetUInt8() > 0 + ? bubble1*sWorld->getRate(RATE_REST_OFFLINE_IN_TAVERN_OR_CITY) + : bubble0*sWorld->getRate(RATE_REST_OFFLINE_IN_WILDERNESS); + + SetRestBonus(GetRestBonus() + time_diff*((float)GetUInt32Value(PLAYER_NEXT_LEVEL_XP) / 72000)*bubble); + } + + // load skills after InitStatsForLevel because it triggering aura apply also + _LoadSkills(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_SKILLS)); + UpdateSkillsForLevel(); //update skills after load, to make sure they are correctly update at player load + + // apply original stats mods before spell loading or item equipment that call before equip _RemoveStatsMods() + + //mails are loaded only when needed ;-) - when player in game click on mailbox. + //_LoadMail(); + + _LoadTalents(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_TALENTS)); + _LoadSpells(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_SPELLS)); + + _LoadGlyphs(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GLYPHS)); + _LoadAuras(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_AURAS), time_diff); + _LoadGlyphAuras(); + // add ghost flag (must be after aura load: PLAYER_FLAGS_GHOST set in aura) + if (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_GHOST)) + m_deathState = DEAD; + + // after spell load, learn rewarded spell if need also + _LoadQuestStatus(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS)); + _LoadQuestStatusRewarded(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS_REW)); + _LoadDailyQuestStatus(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_DAILY_QUEST_STATUS)); + _LoadWeeklyQuestStatus(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_WEEKLY_QUEST_STATUS)); + _LoadSeasonalQuestStatus(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_SEASONAL_QUEST_STATUS)); + _LoadMonthlyQuestStatus(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MONTHLY_QUEST_STATUS)); + _LoadLFGRewardStatus(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_LFG_REWARD_STATUS)); + _LoadRandomBGStatus(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_RANDOM_BG)); + + // after spell and quest load + InitTalentForLevel(); + LearnDefaultSkills(); + LearnCustomSpells(); + + // must be before inventory (some items required reputation check) + m_reputationMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_REPUTATION)); + + _LoadInventory(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_INVENTORY), time_diff); + + if (IsVoidStorageUnlocked()) + _LoadVoidStorage(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_VOID_STORAGE)); + + // update items with duration and realtime + UpdateItemDuration(time_diff, true); + + _LoadActions(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_ACTIONS)); + + // unread mails and next delivery time, actual mails not loaded + _LoadMail(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAILS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS)); + + m_social = sSocialMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_SOCIAL_LIST), GetGUID()); + + // check PLAYER_CHOSEN_TITLE compatibility with PLAYER__FIELD_KNOWN_TITLES + // note: PLAYER__FIELD_KNOWN_TITLES updated at quest status loaded + uint32 curTitle = fields[49].GetUInt32(); + if (curTitle && !HasTitle(curTitle)) + curTitle = 0; + + SetUInt32Value(PLAYER_CHOSEN_TITLE, curTitle); + + // has to be called after last Relocate() in Player::LoadFromDB + SetFallInformation(0, GetPositionZ()); + + GetSpellHistory()->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_SPELL_COOLDOWNS)); + + uint32 savedHealth = fields[52].GetUInt32(); + if (!savedHealth) + m_deathState = CORPSE; + + // Spell code allow apply any auras to dead character in load time in aura/spell/item loading + // Do now before stats re-calculation cleanup for ghost state unexpected auras + if (!IsAlive()) + RemoveAllAurasOnDeath(); + else + RemoveAllAurasRequiringDeadTarget(); + + //apply all stat bonuses from items and auras + SetCanModifyStats(true); + UpdateAllRatings(); + UpdateAllResistances(); + + // restore remembered power/health values (but not more max values) + SetHealth(savedHealth); + uint32 loadedPowers = 0; + for (uint32 i = 0; i < MAX_POWERS; ++i) + { + if (GetPowerIndex(PowerType(i)) != MAX_POWERS) + { + uint32 savedPower = fields[53 + loadedPowers].GetUInt32(); + uint32 maxPower = GetUInt32Value(UNIT_FIELD_MAXPOWER1 + loadedPowers); + SetPower(PowerType(i), (savedPower > maxPower) ? maxPower : savedPower); + if (++loadedPowers >= MAX_POWERS_PER_CLASS) + break; + } + } + + for (; loadedPowers < MAX_POWERS_PER_CLASS; ++loadedPowers) + SetUInt32Value(UNIT_FIELD_POWER1 + loadedPowers, 0); + + SetPower(POWER_ECLIPSE, 0); + + // Verify loaded talent specializations + for (uint8 i = 0; i < MAX_TALENT_SPECS; ++i) + { + if (i >= talentTrees.size()) + break; + + Optional talentTree = Trinity::StringTo(talentTrees[i]); + if (talentTree) + if (*talentTree > 0 && !sTalentTabStore.LookupEntry(*talentTree) && i == GetActiveSpec()) + SetAtLoginFlag(AT_LOGIN_RESET_TALENTS); // invalid tree, reset talents + } + + // We neeed to update the armor specialization after the talenttree was set + UpdateArmorSpecialization(); + + TC_LOG_DEBUG("entities.player.loading", "The value of player %s after load item and aura is: ", m_name.c_str()); + outDebugValues(); + + // GM state + if (GetSession()->HasPermission(rbac::RBAC_PERM_RESTORE_SAVED_GM_STATE)) + { + switch (sWorld->getIntConfig(CONFIG_GM_LOGIN_STATE)) + { + default: + case 0: break; // disable + case 1: SetGameMaster(true); break; // enable + case 2: // save state + if (extraflags & PLAYER_EXTRA_GM_ON) + SetGameMaster(true); + break; + } + + switch (sWorld->getIntConfig(CONFIG_GM_VISIBLE_STATE)) + { + default: + case 0: SetGMVisible(false); break; // invisible + case 1: break; // visible + case 2: // save state + if (extraflags & PLAYER_EXTRA_GM_INVISIBLE) + SetGMVisible(false); + break; + } + + switch (sWorld->getIntConfig(CONFIG_GM_CHAT)) + { + default: + case 0: break; // disable + case 1: SetGMChat(true); break; // enable + case 2: // save state + if (extraflags & PLAYER_EXTRA_GM_CHAT) + SetGMChat(true); + break; + } + + switch (sWorld->getIntConfig(CONFIG_GM_WHISPERING_TO)) + { + default: + case 0: break; // disable + case 1: SetAcceptWhispers(true); break; // enable + case 2: // save state + if (extraflags & PLAYER_EXTRA_ACCEPT_WHISPERS) + SetAcceptWhispers(true); + break; + } + } + + // RaF stuff. + m_grantableLevels = fields[65].GetUInt8(); + if (GetSession()->IsARecruiter() || (GetSession()->GetRecruiterId() != 0)) + SetFlag(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_REFER_A_FRIEND); + + if (m_grantableLevels > 0) + SetByteValue(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTES_OFFSET_RAF_GRANTABLE_LEVEL, 0x01); + + _LoadDeclinedNames(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_DECLINED_NAMES)); + + m_achievementMgr->CheckAllAchievementCriteria(this); + + _LoadEquipmentSets(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_EQUIPMENT_SETS)); + + _LoadCUFProfiles(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_CUF_PROFILES)); + + return true; +} + +void Player::_LoadCUFProfiles(PreparedQueryResult result) +{ + if (!result) + return; + + do + { + // SELECT id, name, frameHeight, frameWidth, sortBy, healthText, boolOptions, unk146, unk147, unk148, unk150, unk152, unk154 FROM character_cuf_profiles WHERE guid = ? + Field* fields = result->Fetch(); + + uint8 id = fields[0].GetUInt8(); + std::string name = fields[1].GetString(); + uint16 frameHeight = fields[2].GetUInt16(); + uint16 frameWidth = fields[3].GetUInt16(); + uint8 sortBy = fields[4].GetUInt8(); + uint8 healthText = fields[5].GetUInt8(); + uint32 boolOptions = fields[6].GetUInt32(); + uint8 topPoint = fields[7].GetUInt8(); + uint8 bottomPoint = fields[8].GetUInt8(); + uint8 leftPoint = fields[9].GetUInt8(); + uint16 topOffset = fields[10].GetUInt16(); + uint16 bottomOffset = fields[11].GetUInt16(); + uint16 leftOffset = fields[12].GetUInt16(); + + if (id > MAX_CUF_PROFILES) + { + TC_LOG_ERROR("entities.player", "Player::_LoadCUFProfiles - Player (GUID: %u, name: %s) has an CUF profile with invalid id (id: %u), max is %i.", GetGUID().GetCounter(), GetName().c_str(), id, MAX_CUF_PROFILES); + continue; + } + + _CUFProfiles[id] = Trinity::make_unique(name, frameHeight, frameWidth, sortBy, healthText, boolOptions, topPoint, bottomPoint, leftPoint, topOffset, bottomOffset, leftOffset); + } + while (result->NextRow()); +} + +bool Player::isAllowedToLoot(Creature const* creature) +{ + if (!creature->isDead() || !creature->IsDamageEnoughForLootingAndReward()) + return false; + + if (HasPendingBind()) + return false; + + Loot const* loot = &creature->loot; + if (loot->isLooted()) // nothing to loot or everything looted. + return false; + if (!loot->hasItemForAll() && !loot->hasItemFor(this)) // no loot in creature for this player + return false; + + if (loot->loot_type == LOOT_SKINNING) + return creature->GetLootRecipientGUID() == GetGUID(); + + Group* thisGroup = GetGroup(); + if (!thisGroup) + return this == creature->GetLootRecipient(); + else if (thisGroup != creature->GetLootRecipientGroup()) + return false; + + switch (thisGroup->GetLootMethod()) + { + case MASTER_LOOT: + case FREE_FOR_ALL: + return true; + case ROUND_ROBIN: + // may only loot if the player is the loot roundrobin player + // or if there are free/quest/conditional item for the player + if (loot->roundRobinPlayer.IsEmpty() || loot->roundRobinPlayer == GetGUID()) + return true; + + return loot->hasItemFor(this); + case GROUP_LOOT: + case NEED_BEFORE_GREED: + // may only loot if the player is the loot roundrobin player + // or item over threshold (so roll(s) can be launched) + // or if there are free/quest/conditional item for the player + if (loot->roundRobinPlayer.IsEmpty() || loot->roundRobinPlayer == GetGUID()) + return true; + + if (loot->hasOverThresholdItem()) + return true; + + return loot->hasItemFor(this); + } + + return false; +} + +void Player::_LoadActions(PreparedQueryResult result) +{ + m_actionButtons.clear(); + + if (result) + { + do + { + Field* fields = result->Fetch(); + uint8 button = fields[0].GetUInt8(); + uint32 action = fields[1].GetUInt32(); + uint8 type = fields[2].GetUInt8(); + + if (ActionButton* ab = addActionButton(button, action, type)) + ab->uState = ACTIONBUTTON_UNCHANGED; + else + { + TC_LOG_DEBUG("entities.player", "Player::_LoadActions: Player '%s' (%s) has an invalid action button (Button: %u, Action: %u, Type: %u). It will be deleted at next save. This can be due to a player changing their talents.", + GetName().c_str(), GetGUID().ToString().c_str(), button, action, type); + + // Will be deleted in DB at next save (it can create data until save but marked as deleted). + m_actionButtons[button].uState = ACTIONBUTTON_DELETED; + } + } while (result->NextRow()); + } +} + +void Player::_LoadAuras(PreparedQueryResult result, uint32 timediff) +{ + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadAuras: Loading auras for %s", GetGUID().ToString().c_str()); + + /* 0 1 2 3 4 5 6 7 8 9 10 11 + QueryResult* result = CharacterDatabase.PQuery("SELECT casterGuid, itemGuid, spell, effectMask, recalculateMask, stackCount, amount0, amount1, amount2, base_amount0, base_amount1, base_amount2, + 12 13 14 15 16 + maxDuration, remainTime, remainCharges, critChance, applyResilience FROM character_aura WHERE guid = '%u'", GetGUID().GetCounter()); + */ + + if (result) + { + do + { + Field* fields = result->Fetch(); + int32 damage[3]; + int32 baseDamage[3]; + ObjectGuid caster_guid(fields[0].GetUInt64()); + ObjectGuid itemGuid(fields[1].GetUInt64()); + uint32 spellid = fields[2].GetUInt32(); + uint8 effmask = fields[3].GetUInt8(); + uint8 recalculatemask = fields[4].GetUInt8(); + uint8 stackcount = fields[5].GetUInt8(); + damage[0] = fields[6].GetInt32(); + damage[1] = fields[7].GetInt32(); + damage[2] = fields[8].GetInt32(); + baseDamage[0] = fields[9].GetInt32(); + baseDamage[1] = fields[10].GetInt32(); + baseDamage[2] = fields[11].GetInt32(); + int32 maxduration = fields[12].GetInt32(); + int32 remaintime = fields[13].GetInt32(); + uint8 remaincharges = fields[14].GetUInt8(); + float critChance = fields[15].GetFloat(); + bool applyResilience = fields[16].GetBool(); + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellid); + if (!spellInfo) + { + TC_LOG_ERROR("entities.player", "Player::_LoadAuras: Player '%s' (%s) has an invalid aura (SpellID: %u), ignoring.", + GetName().c_str(), GetGUID().ToString().c_str(), spellid); + continue; + } + + // negative effects should continue counting down after logout + if (remaintime != -1 && ((!spellInfo->IsPositive() && spellInfo->Id != 15007) || spellInfo->HasAttribute(SPELL_ATTR4_AURA_EXPIRES_OFFLINE))) // Resurrection sickness should not fade while logged out + { + if (remaintime/IN_MILLISECONDS <= int32(timediff)) + continue; + + remaintime -= timediff*IN_MILLISECONDS; + } + + // prevent wrong values of remaincharges + if (spellInfo->ProcCharges) + { + // we have no control over the order of applying auras and modifiers allow auras + // to have more charges than value in SpellInfo + if (remaincharges <= 0/* || remaincharges > spellproto->procCharges*/) + remaincharges = spellInfo->ProcCharges; + } + else + remaincharges = 0; + + AuraCreateInfo createInfo(spellInfo, effmask, this); + createInfo + .SetCasterGUID(caster_guid) + .SetCastItemGUID(itemGuid) + .SetBaseAmount(baseDamage); + + if (Aura* aura = Aura::TryCreate(createInfo)) + { + if (!aura->CanBeSaved()) + { + aura->Remove(); + continue; + } + + aura->SetLoadedState(maxduration, remaintime, remaincharges, stackcount, recalculatemask, critChance, applyResilience, &damage[0]); + aura->ApplyForTargets(); + TC_LOG_DEBUG("entities.player", "Player::_LoadAuras: Added aura (SpellID: %u, EffectMask: %u) to player '%s (%s)", + spellInfo->Id, effmask, GetName().c_str(), GetGUID().ToString().c_str()); + } + } + while (result->NextRow()); + } +} + +void Player::_LoadGlyphAuras() +{ + for (uint8 i = 0; i < MAX_GLYPH_SLOT_INDEX; ++i) + { + if (uint32 glyph = GetGlyph(GetActiveSpec(), i)) + { + if (GlyphPropertiesEntry const* gp = sGlyphPropertiesStore.LookupEntry(glyph)) + { + if (GlyphSlotEntry const* gs = sGlyphSlotStore.LookupEntry(GetGlyphSlot(i))) + { + if (gp->GlyphSlotFlags == gs->Type) + { + CastSpell(this, gp->SpellID, true); + continue; + } + else + TC_LOG_ERROR("entities.player", "Player::_LoadGlyphAuras: Player '%s' (%s) has glyph with typeflags %u in slot with typeflags %u, removing.", GetName().c_str(), GetGUID().ToString().c_str(), gp->GlyphSlotFlags, gs->Type); + } + else + TC_LOG_ERROR("entities.player", "Player::_LoadGlyphAuras: Player '%s' (%s) has not existing glyph slot entry %u on index %u", GetName().c_str(), GetGUID().ToString().c_str(), GetGlyphSlot(i), i); + } + else + TC_LOG_ERROR("entities.player", "Player::_LoadGlyphAuras: Player '%s' (%s) has not existing glyph entry %u on index %u", GetName().c_str(), GetGUID().ToString().c_str(), glyph, i); + + // On any error remove glyph + SetGlyph(i, 0); + } + } +} + +void Player::LoadCorpse(PreparedQueryResult result) +{ + if (IsAlive() || HasAtLoginFlag(AT_LOGIN_RESURRECT)) + SpawnCorpseBones(false); + + if (!IsAlive()) + { + if (HasAtLoginFlag(AT_LOGIN_RESURRECT)) + ResurrectPlayer(0.5f); + else if (result) + { + Field* fields = result->Fetch(); + _corpseLocation.WorldRelocate(fields[0].GetUInt16(), fields[1].GetFloat(), fields[2].GetFloat(), fields[3].GetFloat(), fields[4].GetFloat()); + ApplyModByteFlag(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTES_OFFSET_FLAGS, PLAYER_FIELD_BYTE_RELEASE_TIMER, !sMapStore.LookupEntry(_corpseLocation.GetMapId())->Instanceable()); + } + } + + RemoveAtLoginFlag(AT_LOGIN_RESURRECT); +} + +void Player::_LoadInventory(PreparedQueryResult result, uint32 timeDiff) +{ + //QueryResult* result = CharacterDatabase.PQuery("SELECT data, text, bag, slot, item, item_template FROM character_inventory JOIN item_instance ON character_inventory.item = item_instance.guid WHERE character_inventory.guid = '%u' ORDER BY bag, slot", GetGUID().GetCounter()); + //NOTE: the "order by `bag`" is important because it makes sure + //the bagMap is filled before items in the bags are loaded + //NOTE2: the "order by `slot`" is needed because mainhand weapons are (wrongly?) + //expected to be equipped before offhand items (@todo fixme) + + if (result) + { + uint32 zoneId = GetZoneId(); + + std::map bagMap; // fast guid lookup for bags + std::map invalidBagMap; // fast guid lookup for bags + std::list problematicItems; + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + // Prevent items from being added to the queue while loading + m_itemUpdateQueueBlocked = true; + do + { + Field* fields = result->Fetch(); + if (Item* item = _LoadItem(trans, zoneId, timeDiff, fields)) + { + ObjectGuid::LowType bagGuid = fields[12].GetUInt32(); + uint8 slot = fields[13].GetUInt8(); + + InventoryResult err = EQUIP_ERR_OK; + // Item is not in bag + if (!bagGuid) + { + item->SetContainer(nullptr); + item->SetSlot(slot); + + if (IsInventoryPos(INVENTORY_SLOT_BAG_0, slot)) + { + ItemPosCountVec dest; + err = CanStoreItem(INVENTORY_SLOT_BAG_0, slot, dest, item, false); + if (err == EQUIP_ERR_OK) + item = StoreItem(dest, item, true); + } + else if (IsEquipmentPos(INVENTORY_SLOT_BAG_0, slot)) + { + uint16 dest; + err = CanEquipItem(slot, dest, item, false, false); + if (err == EQUIP_ERR_OK) + QuickEquipItem(dest, item); + } + else if (IsBankPos(INVENTORY_SLOT_BAG_0, slot)) + { + ItemPosCountVec dest; + err = CanBankItem(INVENTORY_SLOT_BAG_0, slot, dest, item, false, false); + if (err == EQUIP_ERR_OK) + item = BankItem(dest, item, true); + } + + // Remember bags that may contain items in them + if (err == EQUIP_ERR_OK) + { + if (IsBagPos(item->GetPos())) + if (Bag* pBag = item->ToBag()) + bagMap[item->GetGUID().GetCounter()] = pBag; + } + else + if (IsBagPos(item->GetPos())) + if (item->IsBag()) + invalidBagMap[item->GetGUID().GetCounter()] = item; + } + else + { + item->SetSlot(NULL_SLOT); + // Item is in the bag, find the bag + std::map::iterator itr = bagMap.find(bagGuid); + if (itr != bagMap.end()) + { + ItemPosCountVec dest; + err = CanStoreItem(itr->second->GetSlot(), slot, dest, item); + if (err == EQUIP_ERR_OK) + item = StoreItem(dest, item, true); + } + else if (invalidBagMap.find(bagGuid) != invalidBagMap.end()) + { + std::map::iterator invalidBagItr = invalidBagMap.find(bagGuid); + if (std::find(problematicItems.begin(), problematicItems.end(), invalidBagItr->second) != problematicItems.end()) + err = EQUIP_ERR_INTERNAL_BAG_ERROR; + } + else + { + TC_LOG_ERROR("entities.player", "Player::_LoadInventory: Player '%s' (%s) has item (%s, entry: %u) which doesnt have a valid bag (Bag %u, slot: %u). Possible cheat?", + GetName().c_str(), GetGUID().ToString().c_str(), item->GetGUID().ToString().c_str(), item->GetEntry(), bagGuid, slot); + item->DeleteFromInventoryDB(trans); + delete item; + continue; + } + + } + + // Item's state may have changed after storing + if (err == EQUIP_ERR_OK) + item->SetState(ITEM_UNCHANGED, this); + else + { + TC_LOG_ERROR("entities.player", "Player::_LoadInventory: Player '%s' (%s) has item (%s, entry: %u) which can't be loaded into inventory (Bag %u, slot: %u) by reason %u. Item will be sent by mail.", + GetName().c_str(), GetGUID().ToString().c_str(), item->GetGUID().ToString().c_str(), item->GetEntry(), bagGuid, slot, uint32(err)); + item->DeleteFromInventoryDB(trans); + problematicItems.push_back(item); + } + } + } while (result->NextRow()); + + m_itemUpdateQueueBlocked = false; + + // Send problematic items by mail + while (!problematicItems.empty()) + { + std::string subject = GetSession()->GetTrinityString(LANG_NOT_EQUIPPED_ITEM); + + MailDraft draft(subject, "There were problems with equipping item(s)."); + for (uint8 i = 0; !problematicItems.empty() && i < MAX_MAIL_ITEMS; ++i) + { + draft.AddItem(problematicItems.front()); + problematicItems.pop_front(); + } + draft.SendMailTo(trans, this, MailSender(this, MAIL_STATIONERY_GM), MAIL_CHECK_MASK_COPIED); + } + CharacterDatabase.CommitTransaction(trans); + } + //if (IsAlive()) + _ApplyAllItemMods(); +} + +void Player::_LoadVoidStorage(PreparedQueryResult result) +{ + if (!result) + return; + + do + { + // SELECT itemid, itemEntry, slot, creatorGuid FROM character_void_storage WHERE playerGuid = ? + Field* fields = result->Fetch(); + + uint64 itemId = fields[0].GetUInt64(); + uint32 itemEntry = fields[1].GetUInt32(); + uint8 slot = fields[2].GetUInt8(); + ObjectGuid creatorGuid(HighGuid::Player, fields[3].GetUInt32()); + ItemRandomEnchantmentId randomProperty(ItemRandomEnchantmentType(fields[4].GetUInt8()), fields[5].GetUInt32()); + uint32 suffixFactor = fields[6].GetUInt32(); + + if (!itemId) + { + TC_LOG_ERROR("entities.player", "Player::_LoadVoidStorage - Player (GUID: %u, name: %s) has an item with an invalid id (item id: " UI64FMTD ", entry: %u).", GetGUID().GetCounter(), GetName().c_str(), itemId, itemEntry); + continue; + } + + if (!sObjectMgr->GetItemTemplate(itemEntry)) + { + TC_LOG_ERROR("entities.player", "Player::_LoadVoidStorage - Player (GUID: %u, name: %s) has an item with an invalid entry (item id: " UI64FMTD ", entry: %u).", GetGUID().GetCounter(), GetName().c_str(), itemId, itemEntry); + continue; + } + + if (slot >= VOID_STORAGE_MAX_SLOT) + { + TC_LOG_ERROR("entities.player", "Player::_LoadVoidStorage - Player (GUID: %u, name: %s) has an item with an invalid slot (item id: " UI64FMTD ", entry: %u, slot: %u).", GetGUID().GetCounter(), GetName().c_str(), itemId, itemEntry, slot); + continue; + } + + std::string name; + if (creatorGuid && !sCharacterCache->GetCharacterNameByGuid(creatorGuid, name)) + { + TC_LOG_ERROR("entities.player", "Player::_LoadVoidStorage - Player (GUID: %u, name: %s) has an item with an invalid creator guid, set to 0 (item id: " UI64FMTD ", entry: %u, creator: %s).", GetGUID().GetCounter(), GetName().c_str(), itemId, itemEntry, creatorGuid.ToString().c_str()); + creatorGuid.Clear(); + } + + _voidStorageItems[slot] = std::make_unique(itemId, itemEntry, creatorGuid, randomProperty, suffixFactor); + } + while (result->NextRow()); +} + +Item* Player::_LoadItem(CharacterDatabaseTransaction& trans, uint32 zoneId, uint32 timeDiff, Field* fields) +{ + Item* item = nullptr; + ObjectGuid::LowType itemGuid = fields[14].GetUInt32(); + uint32 itemEntry = fields[15].GetUInt32(); + if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemEntry)) + { + bool remove = false; + item = NewItemOrBag(proto); + if (item->LoadFromDB(itemGuid, GetGUID(), fields, itemEntry)) + { + CharacterDatabasePreparedStatement* stmt; + + // Do not allow to have item limited to another map/zone in alive state + if (IsAlive() && item->IsLimitedToAnotherMapOrZone(GetMapId(), zoneId)) + { + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadInventory: player (GUID: %u, name: '%s', map: %u) has item (GUID: %u, entry: %u) limited to another map (%u). Deleting item.", + GetGUID().GetCounter(), GetName().c_str(), GetMapId(), item->GetGUID().GetCounter(), item->GetEntry(), zoneId); + remove = true; + } + // "Conjured items disappear if you are logged out for more than 15 minutes" + else if (timeDiff > 15 * MINUTE && proto->GetFlags() & ITEM_FLAG_CONJURED) + { + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadInventory: player (GUID: %u, name: '%s', diff: %u) has conjured item (GUID: %u, entry: %u) with expired lifetime (15 minutes). Deleting item.", + GetGUID().GetCounter(), GetName().c_str(), timeDiff, item->GetGUID().GetCounter(), item->GetEntry()); + remove = true; + } + else if (item->HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_REFUNDABLE)) + { + if (item->GetPlayedTime(this) > (2 * HOUR)) + { + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadInventory: player (GUID: %u, name: '%s') has item (GUID: %u, entry: %u) with expired refund time (%u). Deleting refund data and removing refundable flag.", + GetGUID().GetCounter(), GetName().c_str(), item->GetGUID().GetCounter(), item->GetEntry(), item->GetPlayedTime(this)); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_REFUND_INSTANCE); + stmt->setUInt32(0, item->GetGUID().GetCounter()); + trans->Append(stmt); + + item->RemoveFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_REFUNDABLE); + } + else + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_ITEM_REFUNDS); + stmt->setUInt32(0, item->GetGUID().GetCounter()); + stmt->setUInt32(1, GetGUID().GetCounter()); + if (PreparedQueryResult result = CharacterDatabase.Query(stmt)) + { + item->SetRefundRecipient((*result)[0].GetUInt32()); + item->SetPaidMoney((*result)[1].GetUInt32()); + item->SetPaidExtendedCost((*result)[2].GetUInt16()); + AddRefundReference(item->GetGUID()); + } + else + { + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadInventory: player (GUID: %u, name: '%s') has item (GUID: %u, entry: %u) with refundable flags, but without data in item_refund_instance. Removing flag.", + GetGUID().GetCounter(), GetName().c_str(), item->GetGUID().GetCounter(), item->GetEntry()); + item->RemoveFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_REFUNDABLE); + } + } + } + else if (item->HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE)) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_ITEM_BOP_TRADE); + stmt->setUInt32(0, item->GetGUID().GetCounter()); + if (PreparedQueryResult result = CharacterDatabase.Query(stmt)) + { + GuidSet looters; + for (std::string_view guidStr : Trinity::Tokenize((*result)[0].GetStringView(), ' ', false)) + { + if (Optional guid = Trinity::StringTo(guidStr)) + looters.insert(ObjectGuid::Create(*guid)); + else + TC_LOG_WARN("entities.player.loading", "Player::_LoadInventory: invalid item_soulbound_trade_data GUID '%s' for item %s. Skipped.", std::string(guidStr).c_str(), item->GetGUID().ToString().c_str()); + } + + if (looters.size() > 1 && item->GetTemplate()->GetMaxStackSize() == 1 && item->IsSoulBound()) + { + item->SetSoulboundTradeable(looters); + AddTradeableItem(item); + } + else + item->ClearSoulboundTradeable(this); + } + else + { + TC_LOG_WARN("entities.player.loading", "Player::_LoadInventory: player (GUID: %u, name: '%s') has item (GUID: %u, entry: %u) with ITEM_FIELD_FLAG_BOP_TRADEABLE flag, but without data in item_soulbound_trade_data. Removing flag.", + GetGUID().GetCounter(), GetName().c_str(), item->GetGUID().GetCounter(), item->GetEntry()); + item->RemoveFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE); + } + } + else if (proto->GetHolidayID()) + { + remove = true; + GameEventMgr::GameEventDataMap const& events = sGameEventMgr->GetEventMap(); + GameEventMgr::ActiveEvents const& activeEventsList = sGameEventMgr->GetActiveEventList(); + for (GameEventMgr::ActiveEvents::const_iterator itr = activeEventsList.begin(); itr != activeEventsList.end(); ++itr) + { + if (uint32(events[*itr].holiday_id) == proto->GetHolidayID()) + { + remove = false; + break; + } + } + } + } + else + { + TC_LOG_ERROR("entities.player", "Player::_LoadInventory: player (GUID: %u, name: '%s') has a broken item (GUID: %u, entry: %u) in inventory. Deleting item.", + GetGUID().GetCounter(), GetName().c_str(), itemGuid, itemEntry); + remove = true; + } + // Remove item from inventory if necessary + if (remove) + { + Item::DeleteFromInventoryDB(trans, itemGuid); + item->FSetState(ITEM_REMOVED); + item->SaveToDB(trans); // it also deletes item object! + item = nullptr; + } + } + else + { + TC_LOG_ERROR("entities.player", "Player::_LoadInventory: player (GUID: %u, name: '%s') has an unknown item (entry: %u) in inventory. Deleting item.", + GetGUID().GetCounter(), GetName().c_str(), itemEntry); + Item::DeleteFromInventoryDB(trans, itemGuid); + Item::DeleteFromDB(trans, itemGuid); + } + return item; +} + +// load mailed item which should receive current player +Item* Player::_LoadMailedItem(ObjectGuid const& playerGuid, Player* player, uint32 mailId, Mail* mail, Field* fields) +{ + ObjectGuid::LowType itemGuid = fields[12].GetUInt32(); + uint32 itemEntry = fields[13].GetUInt32(); + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemEntry); + if (!proto) + { + TC_LOG_ERROR("entities.player", "Player '%s' (%s) has unknown item in mailed items (GUID: %u, Entry: %u) in mail (%u), deleted.", + player ? player->GetName().c_str() : "", playerGuid.ToString().c_str(), itemGuid, itemEntry, mailId); + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM); + stmt->setUInt32(0, itemGuid); + trans->Append(stmt); + + Item::DeleteFromDB(trans, itemGuid); + + CharacterDatabase.CommitTransaction(trans); + return nullptr; + } + + Item* item = NewItemOrBag(proto); + + ObjectGuid ownerGuid = fields[14].GetUInt32() ? ObjectGuid::Create(fields[14].GetUInt32()) : ObjectGuid::Empty; + if (!item->LoadFromDB(itemGuid, ownerGuid, fields, itemEntry)) + { + TC_LOG_ERROR("entities.player", "Player::_LoadMailedItems: Item (GUID: %u) in mail (%u) doesn't exist, deleted from mail.", itemGuid, mailId); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEM); + stmt->setUInt32(0, itemGuid); + CharacterDatabase.Execute(stmt); + + item->FSetState(ITEM_REMOVED); + + CharacterDatabaseTransaction temp = CharacterDatabaseTransaction(nullptr); + item->SaveToDB(temp); // it also deletes item object ! + return nullptr; + } + + if (mail) + mail->AddItem(itemGuid, itemEntry); + + if (player) + player->AddMItem(item); + + return item; +} + +void Player::_LoadMail(PreparedQueryResult mailsResult, PreparedQueryResult mailItemsResult) +{ + m_mail.clear(); + + std::unordered_map mailById; + + if (mailsResult) + { + do + { + Field* fields = mailsResult->Fetch(); + Mail* m = new Mail; + + m->messageID = fields[0].GetUInt32(); + m->messageType = fields[1].GetUInt8(); + m->sender = fields[2].GetUInt32(); + m->receiver = fields[3].GetUInt32(); + m->subject = fields[4].GetString(); + m->body = fields[5].GetString(); + m->expire_time = time_t(fields[6].GetUInt32()); + m->deliver_time = time_t(fields[7].GetUInt32()); + m->money = fields[8].GetUInt64(); + m->COD = fields[9].GetUInt64(); + m->checked = fields[10].GetUInt8(); + m->stationery = fields[11].GetUInt8(); + m->mailTemplateId = fields[12].GetInt16(); + + if (m->mailTemplateId && !sMailTemplateStore.LookupEntry(m->mailTemplateId)) + { + TC_LOG_ERROR("entities.player", "Player::_LoadMail: Mail (%u) has nonexistent MailTemplateId (%u), remove at load", m->messageID, m->mailTemplateId); + m->mailTemplateId = 0; + } + + m->state = MAIL_STATE_UNCHANGED; + m_mail.push_back(m); + mailById[m->messageID] = m; + } + while (mailsResult->NextRow()); + } + + if (mailItemsResult) + { + do + { + Field* fields = mailItemsResult->Fetch(); + uint32 mailId = fields[15].GetUInt32(); + _LoadMailedItem(GetGUID(), this, mailId, mailById[mailId], fields); + } while (mailItemsResult->NextRow()); + } + + UpdateNextMailTimeAndUnreads(); +} + +void Player::LoadPet() +{ + //fixme: the pet should still be loaded if the player is not in world + // just not added to the map + if (IsInWorld()) + { + Pet* pet = new Pet(this); + if (!pet->LoadPetData(this, 0, 0, true)) + delete pet; + } +} + +void Player::LoadPetsFromDB(PreparedQueryResult result) +{ + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + + std::unique_ptr playerPetData = std::make_unique(); + + uint8 slot = fields[7].GetUInt8(); + uint32 petId = fields[0].GetUInt32(); + + if (slot > PET_SLOT_LAST) + { + TC_LOG_ERROR("sql.sql", "Player::LoadPetsFromDB: bad slot %u for pet %u!", slot, petId); + continue; + } + + playerPetData->PetId = petId; + playerPetData->CreatureId = fields[1].GetUInt32(); + playerPetData->Owner = fields[2].GetUInt64(); + playerPetData->DisplayId = fields[3].GetUInt32(); + playerPetData->Petlevel = fields[4].GetUInt16(); + playerPetData->PetExp = fields[5].GetUInt32(); + playerPetData->Reactstate = ReactStates(fields[6].GetUInt8()); + playerPetData->Slot = slot; + playerPetData->Name = fields[8].GetString(); + playerPetData->Renamed = fields[9].GetBool(); + playerPetData->Active = fields[10].GetBool(); + playerPetData->SavedHealth = fields[11].GetUInt32(); + playerPetData->SavedMana = fields[12].GetUInt32(); + playerPetData->Actionbar = fields[13].GetString(); + playerPetData->Timediff = fields[14].GetUInt32(); + playerPetData->SummonSpellId = fields[15].GetUInt32(); + playerPetData->Type = PetType(fields[16].GetUInt8()); + + PlayerPetDataStore.push_back(std::move(playerPetData)); + + } while (result->NextRow()); +} + +PlayerPetData* Player::GetPlayerPetDataById(uint32 petId) +{ + for (std::unique_ptr const& petData : PlayerPetDataStore) + { + if (petData->PetId == petId) + return petData.get(); + } + + return nullptr; +} + +PlayerPetData* Player::GetPlayerPetDataBySlot(uint8 slot) +{ + for (std::unique_ptr const& petData : PlayerPetDataStore) + { + if (petData->Slot == slot) + return petData.get(); + } + + return nullptr; +} + +PlayerPetData* Player::GetPlayerPetDataByCreatureId(uint32 creatureId) +{ + for (std::unique_ptr const& petData : PlayerPetDataStore) + { + if (petData->CreatureId == creatureId) + return petData.get(); + } + + return nullptr; +} + +PlayerPetData* Player::GetPlayerPetDataCurrent() +{ + for (std::unique_ptr const& petData : PlayerPetDataStore) + { + if (petData->Active) + return petData.get(); + } + + return nullptr; +} + +Optional Player::GetFirstUnusedActivePetSlot() +{ + std::set unusedActiveSlot = { 0, 1, 2, 3, 4 }; //unfiltered + + for (std::unique_ptr const& petData : PlayerPetDataStore) + { + if (unusedActiveSlot.find(petData->Slot) != unusedActiveSlot.end()) + unusedActiveSlot.erase(petData->Slot); + } + + if (!unusedActiveSlot.empty()) + return *unusedActiveSlot.begin(); + + return Optional{}; +} + +Optional Player::GetFirstUnusedPetSlot() +{ + std::set unusedSlot; + + for (uint8 i = 0; i < PET_SLOT_LAST; i++) // 4 MAX StableSlots (256 theoretically) + unusedSlot.insert(i); + + for (std::unique_ptr const& petData : PlayerPetDataStore) + { + if (unusedSlot.find(petData->Slot) != unusedSlot.end()) + unusedSlot.erase(petData->Slot); + } + + if (!unusedSlot.empty()) + return *unusedSlot.begin(); + + return Optional{}; +} + +void Player::DeleteFromPlayerPetDataStore(uint32 petNumber) +{ + for (uint8 i = 0; i < PlayerPetDataStore.size(); ++i) + { + if (PlayerPetDataStore[i]->PetId == petNumber) + PlayerPetDataStore.erase(PlayerPetDataStore.begin() + (i--)); + } +} + +void Player::AddToPlayerPetDataStore(std::unique_ptr playerPetData) +{ + PlayerPetDataStore.push_back(std::move(playerPetData)); +} + +void Player::_LoadQuestStatus(PreparedQueryResult result) +{ + uint16 slot = 0; + + //// 0 1 2 3 4 5 6 7 8 9 10 + //QueryResult* result = CharacterDatabase.PQuery("SELECT quest, status, explored, timer, mobcount1, mobcount2, mobcount3, mobcount4, itemcount1, itemcount2, itemcount3, + // 11 12 13 14 + // itemcount4, itemcount5, itemcount6, playercount FROM character_queststatus WHERE guid = '%u'", GetGUID().GetCounter()); + + if (result) + { + do + { + Field* fields = result->Fetch(); + + uint32 quest_id = fields[0].GetUInt32(); + // used to be new, no delete? + Quest const* quest = sObjectMgr->GetQuestTemplate(quest_id); + if (quest) + { + // find or create + QuestStatusData& questStatusData = m_QuestStatus[quest_id]; + + uint8 qstatus = fields[1].GetUInt8(); + if (qstatus < MAX_QUEST_STATUS) + questStatusData.Status = QuestStatus(qstatus); + else + { + questStatusData.Status = QUEST_STATUS_INCOMPLETE; + TC_LOG_ERROR("entities.player", "Player::_LoadQuestStatus: Player '%s' (%s) has invalid quest %d status (%u), replaced by QUEST_STATUS_INCOMPLETE(3).", ++ GetName().c_str(), GetGUID().ToString().c_str(), quest_id, qstatus); + } + + questStatusData.Explored = (fields[2].GetUInt8() > 0); + + time_t quest_time = time_t(fields[3].GetUInt32()); + + if (quest->HasSpecialFlag(QUEST_SPECIAL_FLAGS_TIMED) && !GetQuestRewardStatus(quest_id)) + { + AddTimedQuest(quest_id); + + if (quest_time <= GameTime::GetGameTime()) + questStatusData.Timer = 1; + else + questStatusData.Timer = uint32((quest_time - GameTime::GetGameTime()) * IN_MILLISECONDS); + } + else + quest_time = 0; + + for (uint32 i = 0; i < QUEST_OBJECTIVES_COUNT; ++i) + questStatusData.CreatureOrGOCount[i] = fields[4 + i].GetUInt16(); + + for (uint32 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) + questStatusData.ItemCount[i] = fields[8 + i].GetUInt16(); + + questStatusData.PlayerCount = fields[14].GetUInt16(); + + // add to quest log + if (slot < MAX_QUEST_LOG_SIZE && questStatusData.Status != QUEST_STATUS_NONE) + { + SetQuestSlot(slot, quest_id, uint32(quest_time)); // cast can't be helped + + if (questStatusData.Status == QUEST_STATUS_COMPLETE) + SetQuestSlotState(slot, QUEST_STATE_COMPLETE); + else if (questStatusData.Status == QUEST_STATUS_FAILED) + SetQuestSlotState(slot, QUEST_STATE_FAIL); + + for (uint8 idx = 0; idx < QUEST_OBJECTIVES_COUNT; ++idx) + if (questStatusData.CreatureOrGOCount[idx]) + SetQuestSlotCounter(slot, idx, questStatusData.CreatureOrGOCount[idx]); + + if (questStatusData.PlayerCount) + SetQuestSlotCounter(slot, QUEST_PVP_KILL_SLOT, questStatusData.PlayerCount); + + ++slot; + } + + + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadQuestStatus: Quest status is {%u} for quest {%u} for player (%s)", questStatusData.Status, quest_id, GetGUID().ToString().c_str()); + } + } + while (result->NextRow()); + } + + // clear quest log tail + for (uint16 i = slot; i < MAX_QUEST_LOG_SIZE; ++i) + SetQuestSlot(i, 0); +} + +void Player::_LoadQuestStatusRewarded(PreparedQueryResult result) +{ + // SELECT quest FROM character_queststatus_rewarded WHERE guid = ? + + if (result) + { + do + { + Field* fields = result->Fetch(); + + uint32 quest_id = fields[0].GetUInt32(); + // used to be new, no delete? + Quest const* quest = sObjectMgr->GetQuestTemplate(quest_id); + if (quest) + { + // learn rewarded spell if unknown + LearnQuestRewardedSpells(quest); + + // set rewarded title if any + if (quest->GetCharTitleId()) + { + if (CharTitlesEntry const* titleEntry = sCharTitlesStore.LookupEntry(quest->GetCharTitleId())) + SetTitle(titleEntry); + } + + if (uint32 talents = quest->GetBonusTalents()) + AddQuestRewardedTalentCount(talents); + + if (quest->CanIncreaseRewardedQuestCounters()) + m_RewardedQuests.insert(quest_id); + } + } + while (result->NextRow()); + } +} + +void Player::_LoadDailyQuestStatus(PreparedQueryResult result) +{ + for (uint32 quest_daily_idx = 0; quest_daily_idx < PLAYER_MAX_DAILY_QUESTS; ++quest_daily_idx) + SetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1+quest_daily_idx, 0); + + //QueryResult* result = CharacterDatabase.PQuery("SELECT quest, time FROM character_queststatus_daily WHERE guid = '%u'", GetGUID().GetCounter()); + + if (result) + { + uint32 quest_daily_idx = 0; + + do + { + Field* fields = result->Fetch(); + + if (quest_daily_idx >= PLAYER_MAX_DAILY_QUESTS) // max amount with exist data in query + { + TC_LOG_ERROR("entities.player", "Player (GUID: %u) has more than 25 daily quest records in `charcter_queststatus_daily`", GetGUID().GetCounter()); + break; + } + + uint32 quest_id = fields[0].GetUInt32(); + + // save _any_ from daily quest times (it must be after last reset anyway) + m_lastDailyQuestTime = time_t(fields[1].GetUInt32()); + + Quest const* quest = sObjectMgr->GetQuestTemplate(quest_id); + if (!quest) + continue; + + SetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1+quest_daily_idx, quest_id); + ++quest_daily_idx; + + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadDailyQuestStatus: Loaded daily quest cooldown (QuestID: %u) for player '%s' (%s)", + quest_id, GetName().c_str(), GetGUID().ToString().c_str()); + } + while (result->NextRow()); + } + + m_DailyQuestChanged = false; +} + +void Player::_LoadWeeklyQuestStatus(PreparedQueryResult result) +{ + m_weeklyquests.clear(); + + if (result) + { + do + { + Field* fields = result->Fetch(); + uint32 quest_id = fields[0].GetUInt32(); + Quest const* quest = sObjectMgr->GetQuestTemplate(quest_id); + if (!quest) + continue; + + m_weeklyquests.insert(quest_id); + + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadWeeklyQuestStatus: Loaded weekly quest cooldown (QuestID: %u) for player '%s' (%s)", + quest_id, GetName().c_str(), GetGUID().ToString().c_str()); + } + while (result->NextRow()); + } + + m_WeeklyQuestChanged = false; +} + +void Player::_LoadSeasonalQuestStatus(PreparedQueryResult result) +{ + m_seasonalquests.clear(); + + if (result) + { + do + { + Field* fields = result->Fetch(); + uint32 quest_id = fields[0].GetUInt32(); + uint32 event_id = fields[1].GetUInt32(); + uint32 completedTime = fields[2].GetInt64(); + Quest const* quest = sObjectMgr->GetQuestTemplate(quest_id); + if (!quest) + continue; + + m_seasonalquests[event_id][quest_id] = completedTime; + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadSeasonalQuestStatus: Loaded seasonal quest cooldown (QuestID: %u) for player '%s' (%s)", + quest_id, GetName().c_str(), GetGUID().ToString().c_str()); + } + while (result->NextRow()); + } + + m_SeasonalQuestChanged = false; +} + +void Player::_LoadMonthlyQuestStatus(PreparedQueryResult result) +{ + m_monthlyquests.clear(); + + if (result) + { + do + { + Field* fields = result->Fetch(); + uint32 quest_id = fields[0].GetUInt32(); + Quest const* quest = sObjectMgr->GetQuestTemplate(quest_id); + if (!quest) + continue; + + m_monthlyquests.insert(quest_id); + TC_LOG_DEBUG("entities.player.loading", "Player::_LoadMonthlyQuestStatus: Loaded monthly quest cooldown (QuestID: %u) for player '%s' (%s)", + quest_id, GetName().c_str(), GetGUID().ToString().c_str()); + } + while (result->NextRow()); + } + + m_MonthlyQuestChanged = false; +} + +void Player::_LoadLFGRewardStatus(PreparedQueryResult result) +{ + m_lfgrewardstatus.clear(); + + if (result) + { + do + { + Field* fields = result->Fetch(); + uint32 dungeon_id = fields[0].GetUInt16(); + uint8 reward_count = fields[1].GetUInt8(); + uint8 daily_reset = fields[2].GetUInt8(); + + m_lfgrewardstatus[dungeon_id] = LFGRewardInfo(reward_count, daily_reset); + TC_LOG_DEBUG("entities.player.loading", "Player::_LFGQuestStatus: Loaded LFG quest first reward cooldown (DungeonID: %u) for player '%s' (%s)", + dungeon_id, GetName().c_str(), GetGUID().ToString().c_str()); + } while (result->NextRow()); + } + + m_LFGRewardStatusChanged = false; +} + +void Player::_LoadSpells(PreparedQueryResult result) +{ + //QueryResult* result = CharacterDatabase.PQuery("SELECT spell, active, disabled FROM character_spell WHERE guid = '%u'", GetGUID().GetCounter()); + + if (result) + { + do + AddSpell((*result)[0].GetUInt32(), (*result)[1].GetBool(), false, false, (*result)[2].GetBool(), true); + while (result->NextRow()); + } +} + +void Player::_LoadGroup(PreparedQueryResult result) +{ + //QueryResult* result = CharacterDatabase.PQuery("SELECT guid FROM group_member WHERE memberGuid=%u", GetGUID().GetCounter()); + if (result) + { + if (Group* group = sGroupMgr->GetGroupByDbStoreId((*result)[0].GetUInt32())) + { + if (group->IsLeader(GetGUID())) + SetFlag(PLAYER_FLAGS, PLAYER_FLAGS_GROUP_LEADER); + + uint8 subgroup = group->GetMemberGroup(GetGUID()); + SetGroup(group, subgroup); + if (getLevel() >= LEVELREQUIREMENT_HEROIC) + { + // the group leader may change the instance difficulty while the player is offline + SetDungeonDifficulty(group->GetDungeonDifficulty()); + SetRaidDifficulty(group->GetRaidDifficulty()); + } + } + } + + if (!GetGroup() || !GetGroup()->IsLeader(GetGUID())) + RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_GROUP_LEADER); +} + +void Player::_LoadBoundInstances(PreparedQueryResult result) +{ + for (uint8 i = 0; i < MAX_DIFFICULTY; ++i) + m_boundInstances[i].clear(); + + Group* group = GetGroup(); + + // 0 1 2 3 4 5 + // SELECT id, permanent, map, difficulty, extendState, resettime FROM character_instance LEFT JOIN instance ON instance = id WHERE guid = ? + if (result) + { + do + { + Field* fields = result->Fetch(); + + bool perm = fields[1].GetBool(); + uint32 mapId = fields[2].GetUInt16(); + uint32 instanceId = fields[0].GetUInt32(); + uint8 difficulty = fields[3].GetUInt8(); + BindExtensionState extendState = BindExtensionState(fields[4].GetUInt8()); + + time_t resetTime = time_t(fields[5].GetUInt64()); + // the resettime for normal instances is only saved when the InstanceSave is unloaded + // so the value read from the DB may be wrong here but only if the InstanceSave is loaded + // and in that case it is not used + + bool deleteInstance = false; + + MapEntry const* mapEntry = sMapStore.LookupEntry(mapId); + std::string mapname = mapEntry ? mapEntry->MapName : "Unknown"; + + if (!mapEntry || !mapEntry->IsDungeon()) + { + TC_LOG_ERROR("entities.player", "Player::_LoadBoundInstances: Player '%s' (%s) has bind to not existed or not dungeon map %d (%s)", + GetName().c_str(), GetGUID().ToString().c_str(), mapId, mapname.c_str()); + deleteInstance = true; + } + else if (difficulty >= MAX_DIFFICULTY) + { + TC_LOG_ERROR("entities.player", "Player::_LoadBoundInstances: player '%s' (%s) has bind to not existed difficulty %d instance for map %u (%s)", + GetName().c_str(), GetGUID().ToString().c_str(), difficulty, mapId, mapname.c_str()); + deleteInstance = true; + } + else + { + MapDifficulty const* mapDiff = sDBCManager.GetMapDifficultyData(mapId, Difficulty(difficulty)); + if (!mapDiff) + { + TC_LOG_ERROR("entities.player", "Player::_LoadBoundInstances: player '%s' (%s) has bind to not existed difficulty %d instance for map %u (%s)", + GetName().c_str(), GetGUID().ToString().c_str(), difficulty, mapId, mapname.c_str()); + deleteInstance = true; + } + else if (!perm && group) + { + TC_LOG_ERROR("entities.player", "Player::_LoadBoundInstances: player '%s' (%s) is in group %s but has a non-permanent character bind to map %d (%s), %d, %d", + GetName().c_str(), GetGUID().ToString().c_str(), group->GetGUID().ToString().c_str(), mapId, mapname.c_str(), instanceId, difficulty); + deleteInstance = true; + } + } + + if (deleteInstance) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INSTANCE_BY_INSTANCE_GUID); + + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, instanceId); + + CharacterDatabase.Execute(stmt); + + continue; + } + + // since non permanent binds are always solo bind, they can always be reset + if (InstanceSave* save = sInstanceSaveMgr->AddInstanceSave(mapId, instanceId, Difficulty(difficulty), resetTime, !perm, true)) + BindToInstance(save, perm, extendState, true); + } + while (result->NextRow()); + } +} + +InstancePlayerBind* Player::GetBoundInstance(uint32 mapid, Difficulty difficulty, bool withExpired) +{ + // some instances only have one difficulty + MapDifficulty const* mapDiff = sDBCManager.GetDownscaledMapDifficultyData(mapid, difficulty); + if (!mapDiff) + return nullptr; + + BoundInstancesMap::iterator itr = m_boundInstances[difficulty].find(mapid); + if (itr != m_boundInstances[difficulty].end()) + if (itr->second.extendState || withExpired) + return &itr->second; + return nullptr; +} + +InstancePlayerBind const* Player::GetBoundInstance(uint32 mapid, Difficulty difficulty, bool withExpired) const +{ + // some instances only have one difficulty + MapDifficulty const* mapDiff = sDBCManager.GetDownscaledMapDifficultyData(mapid, difficulty); + if (!mapDiff) + return nullptr; + + BoundInstancesMap::const_iterator itr = m_boundInstances[difficulty].find(mapid); + if (itr != m_boundInstances[difficulty].end()) + if (itr->second.extendState || withExpired) + return &itr->second; + return nullptr; +} + +InstanceSave* Player::GetInstanceSave(uint32 mapid, bool raid) +{ + InstancePlayerBind* pBind = GetBoundInstance(mapid, GetDifficulty(raid)); + InstanceSave* pSave = pBind ? pBind->save : nullptr; + if (!pBind || !pBind->perm) + if (Group* group = GetGroup()) + if (InstanceGroupBind* groupBind = group->GetBoundInstance(GetDifficulty(raid), mapid)) + pSave = groupBind->save; + + return pSave; +} + +void Player::UnbindInstance(uint32 mapid, Difficulty difficulty, bool unload) +{ + BoundInstancesMap::iterator itr = m_boundInstances[difficulty].find(mapid); + if (itr != m_boundInstances[difficulty].end()) + UnbindInstance(itr, difficulty, unload); +} + +void Player::UnbindInstance(BoundInstancesMap::iterator &itr, Difficulty difficulty, bool unload) +{ + if (itr != m_boundInstances[difficulty].end()) + { + if (!unload) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INSTANCE_BY_INSTANCE_GUID); + + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, itr->second.save->GetInstanceId()); + + CharacterDatabase.Execute(stmt); + } + + if (itr->second.perm) + GetSession()->SendCalendarRaidLockout(itr->second.save, false); + + itr->second.save->RemovePlayer(this); // save can become invalid + m_boundInstances[difficulty].erase(itr++); + } +} + +InstancePlayerBind* Player::BindToInstance(InstanceSave* save, bool permanent, BindExtensionState extendState, bool load) +{ + if (save) + { + InstancePlayerBind& bind = m_boundInstances[save->GetDifficulty()][save->GetMapId()]; + if (extendState == EXTEND_STATE_KEEP) // special flag, keep the player's current extend state when updating for new boss down + { + if (save == bind.save) + extendState = bind.extendState; + else + extendState = EXTEND_STATE_NORMAL; + } + if (!load) + { + if (bind.save) + { + // update the save when the group kills a boss + if (permanent != bind.perm || save != bind.save || extendState != bind.extendState) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_INSTANCE); + + stmt->setUInt32(0, save->GetInstanceId()); + stmt->setBool(1, permanent); + stmt->setUInt8(2, extendState); + stmt->setUInt32(3, GetGUID().GetCounter()); + stmt->setUInt32(4, bind.save->GetInstanceId()); + + CharacterDatabase.Execute(stmt); + } + } + else + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_INSTANCE); + + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, save->GetInstanceId()); + stmt->setBool(2, permanent); + stmt->setUInt8(3, extendState); + + CharacterDatabase.Execute(stmt); + } + } + + if (bind.save != save) + { + if (bind.save) + bind.save->RemovePlayer(this); + save->AddPlayer(this); + } + + if (permanent) + save->SetCanReset(false); + + bind.save = save; + bind.perm = permanent; + bind.extendState = extendState; + if (!load) + TC_LOG_DEBUG("maps", "Player::BindToInstance: Player '%s' (%s) is now bound to map (ID: %d, Instance: %d, Difficulty: %d)", + GetName().c_str(), GetGUID().ToString().c_str(), save->GetMapId(), save->GetInstanceId(), save->GetDifficulty()); + sScriptMgr->OnPlayerBindToInstance(this, save->GetDifficulty(), save->GetMapId(), permanent, extendState); + return &bind; + } + + return nullptr; +} + +void Player::BindToInstance() +{ + InstanceSave* mapSave = sInstanceSaveMgr->GetInstanceSave(_pendingBindId); + if (!mapSave) //it seems sometimes mapSave is nullptr, but I did not check why + return; + + WorldPackets::Instance::InstanceSaveCreated instanceSaveCreated; + SendDirectMessage(instanceSaveCreated.Write()); + if (!IsGameMaster()) + { + BindToInstance(mapSave, true, EXTEND_STATE_KEEP); + GetSession()->SendCalendarRaidLockout(mapSave, true); + } +} + +void Player::SetPendingBind(uint32 instanceId, uint32 bindTimer) +{ + _pendingBindId = instanceId; + _pendingBindTimer = bindTimer; +} + +void Player::SendRaidInfo() +{ + WorldPackets::Instance::InstanceInfo instanceInfo; + + time_t now = GameTime::GetGameTime(); + + for (uint8 i = 0; i < MAX_DIFFICULTY; ++i) + { + for (BoundInstancesMap::iterator itr = m_boundInstances[i].begin(); itr != m_boundInstances[i].end(); ++itr) + { + InstancePlayerBind const& bind = itr->second; + if (bind.perm) + { + InstanceSave* save = bind.save; + + WorldPackets::Instance::InstanceLock lockInfos; + + Difficulty difficulty = save->GetDifficulty(); + lockInfos.Heroic = (difficulty == RAID_DIFFICULTY_10MAN_HEROIC || difficulty == RAID_DIFFICULTY_25MAN_HEROIC); + + if (lockInfos.Heroic) + lockInfos.Difficulty = difficulty == RAID_DIFFICULTY_10MAN_HEROIC ? RAID_DIFFICULTY_10MAN_NORMAL : RAID_DIFFICULTY_25MAN_NORMAL; + else + lockInfos.Difficulty = difficulty; + + lockInfos.InstanceID = save->GetInstanceId(); + lockInfos.MapID = save->GetMapId(); + if (bind.extendState != EXTEND_STATE_EXTENDED) + lockInfos.TimeRemaining = save->GetResetTime() - now; + else + lockInfos.TimeRemaining = sInstanceSaveMgr->GetSubsequentResetTime(save->GetMapId(), save->GetDifficulty(), save->GetResetTime()) - now; + + if (Map* map = sMapMgr->FindMap(save->GetMapId(), save->GetInstanceId())) + if (InstanceScript* instanceScript = ((InstanceMap*)map)->GetInstanceScript()) + lockInfos.CompletedMask = instanceScript->GetCompletedEncounterMask(); + + lockInfos.Locked = bind.extendState != EXTEND_STATE_EXPIRED; + lockInfos.Extended = bind.extendState == EXTEND_STATE_EXTENDED; + + instanceInfo.LockList.push_back(lockInfos); + } + } + } + + SendDirectMessage(instanceInfo.Write()); +} + +/* +- called on every successful teleportation to a map +*/ +void Player::SendSavedInstances() +{ + bool hasBeenSaved = false; + + for (uint8 i = 0; i < MAX_DIFFICULTY; ++i) + { + for (BoundInstancesMap::iterator itr = m_boundInstances[i].begin(); itr != m_boundInstances[i].end(); ++itr) + { + if (itr->second.perm) // only permanent binds are sent + { + hasBeenSaved = true; + break; + } + } + } + + // Send opcode SMSG_UPDATE_INSTANCE_OWNERSHIP. true or false means, whether you have current raid/heroic instances + WorldPackets::Instance::UpdateInstanceOwnership updateInstanceOwnership; + updateInstanceOwnership.IOwnInstance = hasBeenSaved; + SendDirectMessage(updateInstanceOwnership.Write()); + + if (!hasBeenSaved) + return; + + for (uint8 i = 0; i < MAX_DIFFICULTY; ++i) + { + for (BoundInstancesMap::iterator itr = m_boundInstances[i].begin(); itr != m_boundInstances[i].end(); ++itr) + { + if (itr->second.perm) + { + WorldPackets::Instance::UpdateLastInstance packet; + packet.MapID = itr->second.save->GetMapId(); + SendDirectMessage(packet.Write()); + } + } + } +} + +bool Player::Satisfy(AccessRequirement const* ar, uint32 target_map, bool report) +{ + if (!IsGameMaster() && ar) + { + uint8 LevelMin = 0; + uint8 LevelMax = 0; + + MapEntry const* mapEntry = sMapStore.LookupEntry(target_map); + if (!mapEntry) + return false; + + if (DisableMgr::IsDisabledFor(DISABLE_TYPE_MAP, target_map, this)) + { + GetSession()->SendAreaTriggerMessage("%s", GetSession()->GetTrinityString(LANG_INSTANCE_CLOSED)); + return false; + } + + if (!sWorld->getBoolConfig(CONFIG_INSTANCE_IGNORE_LEVEL)) + { + if (ar->levelMin && getLevel() < ar->levelMin) + LevelMin = ar->levelMin; + if (ar->levelMax && getLevel() > ar->levelMax) + LevelMax = ar->levelMax; + } + + uint32 missingItem = 0; + if (ar->item) + { + if (!HasItemCount(ar->item) && + (!ar->item2 || !HasItemCount(ar->item2))) + missingItem = ar->item; + } + else if (ar->item2 && !HasItemCount(ar->item2)) + missingItem = ar->item2; + + uint32 missingQuest = 0; + if (GetTeam() == ALLIANCE && ar->quest_A && !GetQuestRewardStatus(ar->quest_A)) + missingQuest = ar->quest_A; + else if (GetTeam() == HORDE && ar->quest_H && !GetQuestRewardStatus(ar->quest_H)) + missingQuest = ar->quest_H; + + uint32 missingAchievement = 0; + Player* leader = this; + ObjectGuid leaderGuid = GetGroup() ? GetGroup()->GetLeaderGUID() : GetGUID(); + if (leaderGuid != GetGUID()) + leader = ObjectAccessor::FindPlayer(leaderGuid); + + if (ar->achievement) + if (!leader || !leader->HasAchieved(ar->achievement)) + missingAchievement = ar->achievement; + + bool heroicExclusive = ar->heroicExclusive; + + Difficulty target_difficulty = GetDifficulty(mapEntry->IsRaid()); + MapDifficulty const* mapDiff = sDBCManager.GetDownscaledMapDifficultyData(target_map, target_difficulty); + if (!mapDiff) + return false; + + if (LevelMin || LevelMax || missingItem || missingQuest || missingAchievement || heroicExclusive) + { + if (report && mapDiff->hasErrorMessage) + SendTransferAborted(target_map, TRANSFER_ABORT_DIFFICULTY, target_difficulty); + return false; + } + } + return true; +} + +bool Player::IsInstanceLoginGameMasterException() const +{ + if (CanBeGameMaster()) + { + ChatHandler(GetSession()).SendSysMessage(LANG_INSTANCE_LOGIN_GAMEMASTER_EXCEPTION); + return true; + } + else + return false; +} + +bool Player::CheckInstanceValidity(bool /*isLogin*/) +{ + // game masters' instances are always valid + if (IsGameMaster()) + return true; + + // non-instances are always valid + Map* map = FindMap(); + if (!map || !map->IsDungeon()) + return true; + + // raid instances require the player to be in a raid group to be valid + if (map->IsRaid() && !sWorld->getBoolConfig(CONFIG_INSTANCE_IGNORE_RAID)) + if (!GetGroup() || !GetGroup()->isRaidGroup()) + return false; + + if (Group* group = GetGroup()) + { + // check if player's group is bound to this instance + InstanceGroupBind* bind = group->GetBoundInstance(map->GetDifficulty(), map->GetId()); + if (!bind || !bind->save || bind->save->GetInstanceId() != map->GetInstanceId()) + return false; + + Map::PlayerList const& players = map->GetPlayers(); + if (!players.isEmpty()) + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + if (Player* otherPlayer = it->GetSource()) + { + if (otherPlayer->IsGameMaster()) + continue; + if (!otherPlayer->m_InstanceValid) // ignore players that currently have a homebind timer active + continue; + if (group != otherPlayer->GetGroup()) + return false; + + Map::PlayerList const& players = map->GetPlayers(); + if (!players.isEmpty()) + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + if (Player* otherPlayer = it->GetSource()) + { + if (otherPlayer->IsGameMaster()) + continue; + if (!otherPlayer->m_InstanceValid) // ignore players that currently have a homebind timer active + continue; + if (group != otherPlayer->GetGroup()) + return false; + } + } + } + } + } + else + { + // instance is invalid if we are not grouped and there are other players + if (map->GetPlayersCountExceptGMs() > 1) + return false; + + // check if the player is bound to this instance + InstancePlayerBind* bind = GetBoundInstance(map->GetId(), map->GetDifficulty()); + if (!bind || !bind->save || bind->save->GetInstanceId() != map->GetInstanceId()) + return false; + } + + return true; +} + +bool Player::CheckInstanceCount(uint32 instanceId) const +{ + if (_instanceResetTimes.size() < sWorld->getIntConfig(CONFIG_MAX_INSTANCES_PER_HOUR)) + return true; + return _instanceResetTimes.find(instanceId) != _instanceResetTimes.end(); +} + +void Player::AddInstanceEnterTime(uint32 instanceId, time_t enterTime) +{ + if (_instanceResetTimes.find(instanceId) == _instanceResetTimes.end()) + _instanceResetTimes.insert(InstanceTimeMap::value_type(instanceId, enterTime + HOUR)); +} + +bool Player::_LoadHomeBind(PreparedQueryResult result) +{ + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(), getClass()); + if (!info) + { + TC_LOG_ERROR("entities.player", "Player (Name %s) has incorrect race/class (%u/%u) pair. Can't be loaded.", + GetName().c_str(), uint32(getRace()), uint32(getClass())); + return false; + } + + bool ok = false; + // SELECT mapId, zoneId, posX, posY, posZ FROM character_homebind WHERE guid = ? + if (result) + { + Field* fields = result->Fetch(); + + m_homebindMapId = fields[0].GetUInt16(); + m_homebindAreaId = fields[1].GetUInt16(); + m_homebindX = fields[2].GetFloat(); + m_homebindY = fields[3].GetFloat(); + m_homebindZ = fields[4].GetFloat(); + + MapEntry const* bindMapEntry = sMapStore.LookupEntry(m_homebindMapId); + + // accept saved data only for valid position (and non instanceable), and accessable + if (MapManager::IsValidMapCoord(m_homebindMapId, m_homebindX, m_homebindY, m_homebindZ) && + !bindMapEntry->Instanceable() && GetSession()->GetExpansion() >= bindMapEntry->Expansion()) + ok = true; + else + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_HOMEBIND); + stmt->setUInt32(0, GetGUID().GetCounter()); + CharacterDatabase.Execute(stmt); + } + } + + if (!ok) + { + m_homebindMapId = info->mapId; + m_homebindAreaId = info->areaId; + m_homebindX = info->positionX; + m_homebindY = info->positionY; + m_homebindZ = info->positionZ; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_PLAYER_HOMEBIND); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt16(1, m_homebindMapId); + stmt->setUInt16(2, m_homebindAreaId); + stmt->setFloat (3, m_homebindX); + stmt->setFloat (4, m_homebindY); + stmt->setFloat (5, m_homebindZ); + CharacterDatabase.Execute(stmt); + } + + TC_LOG_DEBUG("entities.player", "Player::_LoadHomeBind: Setting home position (MapID: %u, AreaID: %u, X: %f, Y: %f, Z: %f) of player '%s' (%s)", + m_homebindMapId, m_homebindAreaId, m_homebindX, m_homebindY, m_homebindZ, GetName().c_str(), GetGUID().ToString().c_str()); + + return true; +} + +/*********************************************************/ +/*** SAVE SYSTEM ***/ +/*********************************************************/ + +void Player::SaveToDB(bool create /*=false*/) +{ + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + SaveToDB(trans, create); + + CharacterDatabase.CommitTransaction(trans); +} + +void Player::SaveToDB(CharacterDatabaseTransaction trans, bool create /* = false */) +{ + // delay auto save at any saves (manual, in code, or autosave) + m_nextSave = sWorld->getIntConfig(CONFIG_INTERVAL_SAVE); + + //lets allow only players in world to be saved + if (IsBeingTeleportedFar()) + { + ScheduleDelayedOperation(DELAYED_SAVE_PLAYER); + return; + } + + // first save/honor gain after midnight will also update the player's honor fields + UpdateHonorFields(); + + TC_LOG_DEBUG("entities.unit", "Player::SaveToDB: The value of player %s at save: ", m_name.c_str()); + outDebugValues(); + + if (!create) + sScriptMgr->OnPlayerSave(this); + + CharacterDatabasePreparedStatement* stmt = nullptr; + uint8 index = 0; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_FISHINGSTEPS); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + auto finiteAlways = [](float f) { return std::isfinite(f) ? f : 0.0f; }; + + if (create) + { + //! Insert query + /// @todo: Filter out more redundant fields that can take their default value at player create + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER); + stmt->setUInt32(index++, GetGUID().GetCounter()); + stmt->setUInt32(index++, GetSession()->GetAccountId()); + stmt->setString(index++, GetName()); + stmt->setUInt8(index++, getRace()); + stmt->setUInt8(index++, getClass()); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_GENDER)); // save gender from PLAYER_BYTES_3, UNIT_BYTES_0 changes with every transform effect + stmt->setUInt8(index++, getLevel()); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_XP)); + stmt->setUInt64(index++, GetMoney()); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_FACE_ID)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_STYLE_ID)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_FACIAL_STYLE)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_REST_STATE)); + stmt->setUInt32(index++, GetCharacterFlags().AsUnderlyingType()); + stmt->setUInt32(index++, GetCharacterFlags2().AsUnderlyingType()); + stmt->setUInt16(index++, (uint16)GetMapId()); + stmt->setUInt32(index++, (uint32)GetInstanceId()); + stmt->setUInt8(index++, (uint8(GetDungeonDifficulty()) | uint8(GetRaidDifficulty()) << 4)); + stmt->setFloat(index++, finiteAlways(GetPositionX())); + stmt->setFloat(index++, finiteAlways(GetPositionY())); + stmt->setFloat(index++, finiteAlways(GetPositionZ())); + stmt->setFloat(index++, finiteAlways(GetOrientation())); + stmt->setFloat(index++, finiteAlways(GetTransOffsetX())); + stmt->setFloat(index++, finiteAlways(GetTransOffsetY())); + stmt->setFloat(index++, finiteAlways(GetTransOffsetZ())); + stmt->setFloat(index++, finiteAlways(GetTransOffsetO())); + ObjectGuid::LowType transLowGUID = 0; + if (Transport* transport = dynamic_cast(GetTransport())) + transLowGUID = transport->GetGUID().GetCounter(); + stmt->setUInt32(index++, transLowGUID); + + std::ostringstream ss; + ss << m_taxi; + stmt->setString(index++, ss.str()); + stmt->setUInt8(index++, m_cinematic); + stmt->setUInt32(index++, m_Played_time[PLAYED_TIME_TOTAL]); + stmt->setUInt32(index++, m_Played_time[PLAYED_TIME_LEVEL]); + stmt->setFloat(index++, finiteAlways(m_rest_bonus)); + stmt->setUInt32(index++, uint32(GameTime::GetGameTime())); + stmt->setUInt8(index++, (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) ? 1 : 0)); + //save, far from tavern/city + //save, but in tavern/city + stmt->setUInt32(index++, GetTalentResetCost()); + stmt->setUInt32(index++, GetTalentResetTime()); + + ss.str(""); + for (uint8 i = 0; i < MAX_TALENT_SPECS; ++i) + ss << GetPrimaryTalentTree(i) << " "; + stmt->setString(index++, ss.str()); + stmt->setUInt16(index++, (uint16)m_ExtraFlags); + stmt->setUInt8(index++, m_stableSlots); + stmt->setUInt16(index++, (uint16)m_atLoginFlags); + stmt->setUInt16(index++, GetZoneId()); + stmt->setUInt32(index++, uint32(m_deathExpireTime)); + + ss.str(""); + ss << m_taxi.SaveTaxiDestinationsToString(); + + stmt->setString(index++, ss.str()); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_FIELD_LIFETIME_HONORABLE_KILLS)); + stmt->setUInt16(index++, GetUInt16Value(PLAYER_FIELD_KILLS, 0)); + stmt->setUInt16(index++, GetUInt16Value(PLAYER_FIELD_KILLS, 1)); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_CHOSEN_TITLE)); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_FIELD_WATCHED_FACTION_INDEX)); + stmt->setUInt8(index++, GetDrunkValue()); + stmt->setUInt32(index++, GetHealth()); + + uint32 storedPowers = 0; + for (uint32 i = 0; i < MAX_POWERS; ++i) + { + if (GetPowerIndex(PowerType(i)) != MAX_POWERS) + { + stmt->setUInt32(index++, GetUInt32Value(UNIT_FIELD_POWER1 + storedPowers)); + if (++storedPowers >= MAX_POWERS_PER_CLASS) + break; + } + } + + for (; storedPowers < MAX_POWERS_PER_CLASS; ++storedPowers) + stmt->setUInt32(index++, 0); + + stmt->setUInt32(index++, GetSession()->GetLatency()); + + stmt->setUInt8(index++, GetSpecsCount()); + stmt->setUInt8(index++, GetActiveSpec()); + + ss.str(""); + for (uint32 i = 0; i < PLAYER_EXPLORED_ZONES_SIZE; ++i) + ss << GetUInt32Value(PLAYER_EXPLORED_ZONES_1 + i) << ' '; + stmt->setString(index++, ss.str()); + + ss.str(""); + // cache equipment... + for (uint32 i = 0; i < EQUIPMENT_SLOT_END * 2; ++i) + ss << GetUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + i) << ' '; + + // ...and bags for enum opcode + for (uint32 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + ss << item->GetEntry(); + else + ss << '0'; + ss << " 0 "; + } + stmt->setString(index++, ss.str()); + + ss.str(""); + for (uint32 i = 0; i < KNOWN_TITLES_SIZE*2; ++i) + ss << GetUInt32Value(PLAYER__FIELD_KNOWN_TITLES + i) << ' '; + + stmt->setString(index++, ss.str()); + stmt->setUInt8(index++, GetByteValue(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTES_OFFSET_ACTION_BAR_TOGGLES)); + stmt->setUInt32(index++, m_grantableLevels); + stmt->setUInt32(index++, GetAchievementPoints()); + } + else + { + // Update query + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHARACTER); + stmt->setString(index++, GetName()); + stmt->setUInt8(index++, getRace()); + stmt->setUInt8(index++, getClass()); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_GENDER)); // save gender from PLAYER_BYTES_3, UNIT_BYTES_0 changes with every transform effect + stmt->setUInt8(index++, getLevel()); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_XP)); + stmt->setUInt64(index++, GetMoney()); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_FACE_ID)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_STYLE_ID)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_FACIAL_STYLE)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS)); + stmt->setUInt8(index++, GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_REST_STATE)); + stmt->setUInt32(index++, GetCharacterFlags().AsUnderlyingType()); + stmt->setUInt32(index++, GetCharacterFlags2().AsUnderlyingType()); + + if (!IsBeingTeleported()) + { + stmt->setUInt16(index++, (uint16)GetMapId()); + stmt->setUInt32(index++, (uint32)GetInstanceId()); + stmt->setUInt8(index++, (uint8(GetDungeonDifficulty()) | uint8(GetRaidDifficulty()) << 4)); + stmt->setFloat(index++, finiteAlways(GetPositionX())); + stmt->setFloat(index++, finiteAlways(GetPositionY())); + stmt->setFloat(index++, finiteAlways(GetPositionZ())); + stmt->setFloat(index++, finiteAlways(GetOrientation())); + } + else + { + stmt->setUInt16(index++, (uint16)GetTeleportDest().GetMapId()); + stmt->setUInt32(index++, (uint32)0); + stmt->setUInt8(index++, (uint8(GetDungeonDifficulty()) | uint8(GetRaidDifficulty()) << 4)); + stmt->setFloat(index++, finiteAlways(GetTeleportDest().GetPositionX())); + stmt->setFloat(index++, finiteAlways(GetTeleportDest().GetPositionY())); + stmt->setFloat(index++, finiteAlways(GetTeleportDest().GetPositionZ())); + stmt->setFloat(index++, finiteAlways(GetTeleportDest().GetOrientation())); + } + + stmt->setFloat(index++, finiteAlways(GetTransOffsetX())); + stmt->setFloat(index++, finiteAlways(GetTransOffsetY())); + stmt->setFloat(index++, finiteAlways(GetTransOffsetZ())); + stmt->setFloat(index++, finiteAlways(GetTransOffsetO())); + ObjectGuid::LowType transLowGUID = 0; + if (Transport* transport = dynamic_cast(GetTransport())) + transLowGUID = transport->GetGUID().GetCounter(); + stmt->setUInt32(index++, transLowGUID); + + std::ostringstream ss; + ss << m_taxi; + stmt->setString(index++, ss.str()); + stmt->setUInt8(index++, m_cinematic); + stmt->setUInt32(index++, m_Played_time[PLAYED_TIME_TOTAL]); + stmt->setUInt32(index++, m_Played_time[PLAYED_TIME_LEVEL]); + stmt->setFloat(index++, finiteAlways(m_rest_bonus)); + stmt->setUInt32(index++, uint32(GameTime::GetGameTime())); + stmt->setUInt8(index++, (HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) ? 1 : 0)); + //save, far from tavern/city + //save, but in tavern/city + stmt->setUInt32(index++, GetTalentResetCost()); + stmt->setUInt32(index++, GetTalentResetTime()); + + ss.str(""); + for (uint8 i = 0; i < MAX_TALENT_SPECS; ++i) + ss << GetPrimaryTalentTree(i) << " "; + stmt->setString(index++, ss.str()); + stmt->setUInt16(index++, (uint16)m_ExtraFlags); + stmt->setUInt8(index++, m_stableSlots); + stmt->setUInt16(index++, (uint16)m_atLoginFlags); + stmt->setUInt16(index++, GetZoneId()); + stmt->setUInt32(index++, uint32(m_deathExpireTime)); + + ss.str(""); + ss << m_taxi.SaveTaxiDestinationsToString(); + + stmt->setString(index++, ss.str()); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_FIELD_LIFETIME_HONORABLE_KILLS)); + stmt->setUInt16(index++, GetUInt16Value(PLAYER_FIELD_KILLS, 0)); + stmt->setUInt16(index++, GetUInt16Value(PLAYER_FIELD_KILLS, 1)); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_CHOSEN_TITLE)); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_FIELD_WATCHED_FACTION_INDEX)); + stmt->setUInt8(index++, GetDrunkValue()); + stmt->setUInt32(index++, GetHealth()); + + uint32 storedPowers = 0; + for (uint32 i = 0; i < MAX_POWERS; ++i) + { + if (GetPowerIndex(PowerType(i)) != MAX_POWERS) + { + stmt->setUInt32(index++, GetUInt32Value(UNIT_FIELD_POWER1 + storedPowers)); + if (++storedPowers >= MAX_POWERS_PER_CLASS) + break; + } + } + + for (; storedPowers < MAX_POWERS_PER_CLASS; ++storedPowers) + stmt->setUInt32(index++, 0); + + stmt->setUInt32(index++, GetSession()->GetLatency()); + + stmt->setUInt8(index++, GetSpecsCount()); + stmt->setUInt8(index++, GetActiveSpec()); + + ss.str(""); + for (uint32 i = 0; i < PLAYER_EXPLORED_ZONES_SIZE; ++i) + ss << GetUInt32Value(PLAYER_EXPLORED_ZONES_1 + i) << ' '; + stmt->setString(index++, ss.str()); + + ss.str(""); + // cache equipment... + for (uint32 i = 0; i < EQUIPMENT_SLOT_END * 2; ++i) + ss << GetUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + i) << ' '; + + // ...and bags for enum opcode + for (uint32 i = INVENTORY_SLOT_BAG_START; i < INVENTORY_SLOT_BAG_END; ++i) + { + if (Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i)) + ss << item->GetEntry(); + else + ss << '0'; + ss << " 0 "; + } + + stmt->setString(index++, ss.str()); + + ss.str(""); + for (uint32 i = 0; i < KNOWN_TITLES_SIZE*2; ++i) + ss << GetUInt32Value(PLAYER__FIELD_KNOWN_TITLES + i) << ' '; + + stmt->setString(index++, ss.str()); + stmt->setUInt8(index++, GetByteValue(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTES_OFFSET_ACTION_BAR_TOGGLES)); + stmt->setUInt32(index++, m_grantableLevels); + stmt->setUInt32(index++, GetAchievementPoints()); + + stmt->setUInt8(index++, IsInWorld() && !GetSession()->PlayerLogout() ? 1 : 0); + // Index + stmt->setUInt32(index++, GetGUID().GetCounter()); + } + + trans->Append(stmt); + + if (m_fishingSteps != 0) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_FISHINGSTEPS); + index = 0; + stmt->setUInt32(index++, GetGUID().GetCounter()); + stmt->setUInt32(index++, m_fishingSteps); + trans->Append(stmt); + } + + if (m_mailsUpdated) //save mails only when needed + _SaveMail(trans); + + _SaveBGData(trans); + _SaveInventory(trans); + _SaveVoidStorage(trans); + _SaveQuestStatus(trans); + _SaveDailyQuestStatus(trans); + _SaveWeeklyQuestStatus(trans); + _SaveSeasonalQuestStatus(trans); + _SaveMonthlyQuestStatus(trans); + _SaveLFGRewardStatus(trans); + _SaveTalents(trans); + _SaveSpells(trans); + GetSpellHistory()->SaveToDB(trans); + _SaveActions(trans); + _SaveAuras(trans); + _SaveSkills(trans); + m_achievementMgr->SaveToDB(trans); + m_reputationMgr->SaveToDB(trans); + _SaveEquipmentSets(trans); + GetSession()->SaveTutorialsData(trans); // changed only while character in game + _SaveGlyphs(trans); + _SaveInstanceTimeRestrictions(trans); + _SaveCurrency(trans); + _SaveCUFProfiles(trans); + + // check if stats should only be saved on logout + // save stats can be out of transaction + if (m_session->isLogingOut() || !sWorld->getBoolConfig(CONFIG_STATS_SAVE_ONLY_ON_LOGOUT)) + _SaveStats(trans); + + // save pet (hunter pet level and experience and all type pets health/mana). + if (Pet* pet = GetPet()) + pet->SavePetToDB(PET_SAVE_CURRENT_STATE); +} + +// fast save function for item/money cheating preventing - save only inventory and money state +void Player::SaveInventoryAndGoldToDB(CharacterDatabaseTransaction& trans) +{ + _SaveInventory(trans); + _SaveCurrency(trans); + SaveGoldToDB(trans); +} + +void Player::SaveGoldToDB(CharacterDatabaseTransaction& trans) const +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_MONEY); + stmt->setUInt64(0, GetMoney()); + stmt->setUInt32(1, GetGUID().GetCounter()); + trans->Append(stmt); +} + +void Player::_SaveActions(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt; + + for (ActionButtonList::iterator itr = m_actionButtons.begin(); itr != m_actionButtons.end();) + { + switch (itr->second.uState) + { + case ACTIONBUTTON_NEW: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_ACTION); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt8(1, GetActiveSpec()); + stmt->setUInt8(2, itr->first); + stmt->setUInt32(3, itr->second.GetAction()); + stmt->setUInt8(4, uint8(itr->second.GetType())); + trans->Append(stmt); + + itr->second.uState = ACTIONBUTTON_UNCHANGED; + ++itr; + break; + case ACTIONBUTTON_CHANGED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_ACTION); + stmt->setUInt32(0, itr->second.GetAction()); + stmt->setUInt8(1, uint8(itr->second.GetType())); + stmt->setUInt32(2, GetGUID().GetCounter()); + stmt->setUInt8(3, itr->first); + stmt->setUInt8(4, GetActiveSpec()); + trans->Append(stmt); + + itr->second.uState = ACTIONBUTTON_UNCHANGED; + ++itr; + break; + case ACTIONBUTTON_DELETED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACTION_BY_BUTTON_SPEC); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt8(1, itr->first); + stmt->setUInt8(2, GetActiveSpec()); + trans->Append(stmt); + + m_actionButtons.erase(itr++); + break; + default: + ++itr; + break; + } + } +} + +void Player::_SaveAuras(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_AURA); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + for (AuraMap::const_iterator itr = m_ownedAuras.begin(); itr != m_ownedAuras.end(); ++itr) + { + if (!itr->second->CanBeSaved()) + continue; + + Aura* aura = itr->second; + + int32 damage[MAX_SPELL_EFFECTS]; + int32 baseDamage[MAX_SPELL_EFFECTS]; + uint8 effMask = 0; + uint8 recalculateMask = 0; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (AuraEffect const* effect = aura->GetEffect(i)) + { + baseDamage[i] = effect->GetBaseAmount(); + damage[i] = effect->GetAmount(); + effMask |= 1 << i; + if (effect->CanBeRecalculated()) + recalculateMask |= 1 << i; + } + else + { + baseDamage[i] = 0; + damage[i] = 0; + } + } + + uint8 index = 0; + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_AURA); + stmt->setUInt32(index++, GetGUID().GetCounter()); + stmt->setUInt64(index++, itr->second->GetCasterGUID().GetRawValue()); + stmt->setUInt64(index++, itr->second->GetCastItemGUID().GetRawValue()); + stmt->setUInt32(index++, itr->second->GetId()); + stmt->setUInt8(index++, effMask); + stmt->setUInt8(index++, recalculateMask); + stmt->setUInt8(index++, itr->second->GetStackAmount()); + stmt->setInt32(index++, damage[0]); + stmt->setInt32(index++, damage[1]); + stmt->setInt32(index++, damage[2]); + stmt->setInt32(index++, baseDamage[0]); + stmt->setInt32(index++, baseDamage[1]); + stmt->setInt32(index++, baseDamage[2]); + stmt->setInt32(index++, itr->second->GetMaxDuration()); + stmt->setInt32(index++, itr->second->GetDuration()); + stmt->setUInt8(index++, itr->second->GetCharges()); + stmt->setFloat(index++, itr->second->GetCritChance()); + stmt->setBool(index++, itr->second->CanApplyResilience()); + trans->Append(stmt); + } +} + +void Player::_SaveInventory(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt; + // force items in buyback slots to new state + // and remove those that aren't already + for (uint8 i = BUYBACK_SLOT_START; i < BUYBACK_SLOT_END; ++i) + { + Item* item = m_items[i]; + if (!item) + continue; + + if (item->GetState() == ITEM_NEW) + { + if (ItemTemplate const* itemTemplate = item->GetTemplate()) + if (itemTemplate->GetFlags() & ITEM_FLAG_HAS_LOOT) + sLootItemStorage->RemoveStoredLootForContainer(item->GetGUID().GetCounter()); + + continue; + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INVENTORY_BY_ITEM); + stmt->setUInt32(0, item->GetGUID().GetCounter()); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE); + stmt->setUInt32(0, item->GetGUID().GetCounter()); + trans->Append(stmt); + m_items[i]->FSetState(ITEM_NEW); + + if (ItemTemplate const* itemTemplate = item->GetTemplate()) + if (itemTemplate->GetFlags() & ITEM_FLAG_HAS_LOOT) + sLootItemStorage->RemoveStoredLootForContainer(item->GetGUID().GetCounter()); + } + + // Updated played time for refundable items. We don't do this in Player::Update because there's simply no need for it, + // the client auto counts down in real time after having received the initial played time on the first + // SMSG_ITEM_REFUND_INFO_RESPONSE packet. + // Item::UpdatePlayedTime is only called when needed, which is in DB saves, and item refund info requests. + GuidSet::iterator i_next; + for (GuidSet::iterator itr = m_refundableItems.begin(); itr!= m_refundableItems.end(); itr = i_next) + { + // use copy iterator because itr may be invalid after operations in this loop + i_next = itr; + ++i_next; + + Item* iPtr = GetItemByGuid(*itr); + if (iPtr) + { + iPtr->UpdatePlayedTime(this); + continue; + } + else + { + TC_LOG_ERROR("entities.player", "Player::_SaveInventory: Can't find item (%s) in refundable storage for player '%s' (%s), removing.", + itr->ToString().c_str(), GetName().c_str(), GetGUID().ToString().c_str()); + m_refundableItems.erase(itr); + } + } + + // update enchantment durations + for (EnchantDurationList::iterator itr = m_enchantDuration.begin(); itr != m_enchantDuration.end(); ++itr) + itr->item->SetEnchantmentDuration(itr->slot, itr->leftduration, this); + + // if no changes + if (m_itemUpdateQueue.empty()) + return; + + ObjectGuid::LowType lowGuid = GetGUID().GetCounter(); + for (size_t i = 0; i < m_itemUpdateQueue.size(); ++i) + { + Item* item = m_itemUpdateQueue[i]; + if (!item) + continue; + + Bag* container = item->GetContainer(); + ObjectGuid::LowType bag_guid = container ? container->GetGUID().GetCounter() : 0; + + if (item->GetState() != ITEM_REMOVED) + { + Item* test = GetItemByPos(item->GetBagSlot(), item->GetSlot()); + if (test == nullptr) + { + ObjectGuid::LowType bagTestGUID = 0; + if (Item* test2 = GetItemByPos(INVENTORY_SLOT_BAG_0, item->GetBagSlot())) + bagTestGUID = test2->GetGUID().GetCounter(); + + TC_LOG_ERROR("entities.player", "Player::_SaveInventory: Player '%s' (%s) has incorrect values (Bag: %u, Slot: %u) for the item (%s, State: %d). The player doesn't have an item at that position.", + GetName().c_str(), GetGUID().ToString().c_str(), item->GetBagSlot(), item->GetSlot(), item->GetGUID().ToString().c_str(), (int32)item->GetState()); + // according to the test that was just performed nothing should be in this slot, delete + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INVENTORY_BY_BAG_SLOT); + stmt->setUInt32(0, bagTestGUID); + stmt->setUInt8(1, item->GetSlot()); + stmt->setUInt32(2, lowGuid); + trans->Append(stmt); + + RemoveTradeableItem(item); + RemoveEnchantmentDurationsReferences(item); + RemoveItemDurations(item); + + // also THIS item should be somewhere else, cheat attempt + item->FSetState(ITEM_REMOVED); // we are IN updateQueue right now, can't use SetState which modifies the queue + DeleteRefundReference(item->GetGUID()); + // don't skip, let the switch delete it + //continue; + } + else if (test != item) + { + TC_LOG_ERROR("entities.player", "Player::_SaveInventory: Player '%s' (%s) has incorrect values (Bag: %u, Slot: %u) for the item (%s). %s is there instead!", + GetName().c_str(), GetGUID().ToString().c_str(), item->GetBagSlot(), item->GetSlot(), item->GetGUID().ToString().c_str(), test->GetGUID().ToString().c_str()); + // save all changes to the item... + if (item->GetState() != ITEM_NEW) // only for existing items, no duplicates + item->SaveToDB(trans); + // ...but do not save position in inventory + continue; + } + } + + switch (item->GetState()) + { + case ITEM_NEW: + case ITEM_CHANGED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_INVENTORY_ITEM); + stmt->setUInt32(0, lowGuid); + stmt->setUInt32(1, bag_guid); + stmt->setUInt8 (2, item->GetSlot()); + stmt->setUInt32(3, item->GetGUID().GetCounter()); + trans->Append(stmt); + break; + case ITEM_REMOVED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_INVENTORY_BY_ITEM); + stmt->setUInt32(0, item->GetGUID().GetCounter()); + trans->Append(stmt); + break; + case ITEM_UNCHANGED: + break; + } + + item->SaveToDB(trans); // item have unchanged inventory record and can be save standalone + } + m_itemUpdateQueue.clear(); +} + +void Player::_SaveVoidStorage(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt = nullptr; + uint32 lowGuid = GetGUID().GetCounter(); + + for (size_t i = 0; i < _voidStorageItems.size(); ++i) + { + if (!_voidStorageItems[i]) // unused item + { + // DELETE FROM void_storage WHERE slot = ? AND playerGuid = ? + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_VOID_STORAGE_ITEM_BY_SLOT); + stmt->setUInt8(0, i); + stmt->setUInt32(1, lowGuid); + } + else + { + // REPLACE INTO character_inventory (itemId, playerGuid, itemEntry, slot, creatorGuid) VALUES (?, ?, ?, ?, ?) + stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CHAR_VOID_STORAGE_ITEM); + stmt->setUInt64(0, _voidStorageItems[i]->ItemId); + stmt->setUInt32(1, lowGuid); + stmt->setUInt32(2, _voidStorageItems[i]->ItemEntry); + stmt->setUInt8(3, i); + stmt->setUInt32(4, _voidStorageItems[i]->CreatorGuid.GetCounter()); + stmt->setUInt8(5, uint8(_voidStorageItems[i]->ItemRandomPropertyId.Type)); + stmt->setUInt32(6, _voidStorageItems[i]->ItemRandomPropertyId.Id); + stmt->setUInt32(7, _voidStorageItems[i]->ItemSuffixFactor); + } + + trans->Append(stmt); + } +} + +void Player::_SaveCUFProfiles(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt = nullptr; + uint32 lowGuid = GetGUID().GetCounter(); + + for (uint8 i = 0; i < MAX_CUF_PROFILES; ++i) + { + if (!_CUFProfiles[i]) // unused profile + { + // DELETE FROM character_cuf_profiles WHERE guid = ? and id = ? + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_CUF_PROFILES); + stmt->setUInt32(0, lowGuid); + stmt->setUInt8(1, i); + } + else + { + // REPLACE INTO character_cuf_profiles (guid, id, name, frameHeight, frameWidth, sortBy, healthText, boolOptions, unk146, unk147, unk148, unk150, unk152, unk154) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CHAR_CUF_PROFILES); + stmt->setUInt32(0, lowGuid); + stmt->setUInt8(1, i); + stmt->setString(2, _CUFProfiles[i]->ProfileName); + stmt->setUInt16(3, _CUFProfiles[i]->FrameHeight); + stmt->setUInt16(4, _CUFProfiles[i]->FrameWidth); + stmt->setUInt8(5, _CUFProfiles[i]->SortBy); + stmt->setUInt8(6, _CUFProfiles[i]->HealthText); + stmt->setUInt32(7, _CUFProfiles[i]->BoolOptions.to_ulong()); // 27 of 32 fields used, fits in an int + stmt->setUInt8(8, _CUFProfiles[i]->TopPoint); + stmt->setUInt8(9, _CUFProfiles[i]->BottomPoint); + stmt->setUInt8(10, _CUFProfiles[i]->LeftPoint); + stmt->setUInt16(11, _CUFProfiles[i]->TopOffset); + stmt->setUInt16(12, _CUFProfiles[i]->BottomOffset); + stmt->setUInt16(13, _CUFProfiles[i]->LeftOffset); + } + + trans->Append(stmt); + } +} + + +void Player::_SaveMail(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt; + + for (PlayerMails::iterator itr = m_mail.begin(); itr != m_mail.end(); ++itr) + { + Mail* m = (*itr); + if (m->state == MAIL_STATE_CHANGED) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_MAIL); + stmt->setUInt8(0, uint8(m->HasItems() ? 1 : 0)); + stmt->setUInt32(1, uint32(m->expire_time)); + stmt->setUInt32(2, uint32(m->deliver_time)); + stmt->setUInt64(3, m->money); + stmt->setUInt64(4, m->COD); + stmt->setUInt8(5, uint8(m->checked)); + stmt->setUInt32(6, m->messageID); + + trans->Append(stmt); + + if (!m->removedItems.empty()) + { + for (std::vector::iterator itr2 = m->removedItems.begin(); itr2 != m->removedItems.end(); ++itr2) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEM); + stmt->setUInt32(0, *itr2); + trans->Append(stmt); + } + m->removedItems.clear(); + } + m->state = MAIL_STATE_UNCHANGED; + } + else if (m->state == MAIL_STATE_DELETED) + { + if (m->HasItems()) + { + for (MailItemInfoVec::iterator itr2 = m->items.begin(); itr2 != m->items.end(); ++itr2) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE); + stmt->setUInt32(0, itr2->item_guid); + trans->Append(stmt); + } + } + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_BY_ID); + stmt->setUInt32(0, m->messageID); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_ITEM_BY_ID); + stmt->setUInt32(0, m->messageID); + trans->Append(stmt); + } + } + + //deallocate deleted mails... + for (PlayerMails::iterator itr = m_mail.begin(); itr != m_mail.end();) + { + if ((*itr)->state == MAIL_STATE_DELETED) + { + Mail* m = *itr; + m_mail.erase(itr); + delete m; + itr = m_mail.begin(); + } + else + ++itr; + } + + m_mailsUpdated = false; +} + +void Player::_SaveQuestStatus(CharacterDatabaseTransaction& trans) +{ + bool isTransaction = bool(trans); + if (!isTransaction) + trans = CharacterDatabase.BeginTransaction(); + + QuestStatusSaveMap::iterator saveItr; + QuestStatusMap::iterator statusItr; + CharacterDatabasePreparedStatement* stmt; + + bool keepAbandoned = !(sWorld->GetCleaningFlags() & CharacterDatabaseCleaner::CLEANING_FLAG_QUESTSTATUS); + + for (saveItr = m_QuestStatusSave.begin(); saveItr != m_QuestStatusSave.end(); ++saveItr) + { + if (saveItr->second == QUEST_DEFAULT_SAVE_TYPE) + { + statusItr = m_QuestStatus.find(saveItr->first); + if (statusItr != m_QuestStatus.end() && (keepAbandoned || statusItr->second.Status != QUEST_STATUS_NONE)) + { + uint8 index = 0; + stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CHAR_QUESTSTATUS); + + stmt->setUInt32(index++, GetGUID().GetCounter()); + stmt->setUInt32(index++, statusItr->first); + stmt->setUInt8(index++, uint8(statusItr->second.Status)); + stmt->setBool(index++, statusItr->second.Explored); + stmt->setUInt32(index++, uint32(statusItr->second.Timer / IN_MILLISECONDS+ GameTime::GetGameTime())); + + for (uint8 i = 0; i < QUEST_OBJECTIVES_COUNT; i++) + stmt->setUInt16(index++, statusItr->second.CreatureOrGOCount[i]); + + for (uint8 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++) + stmt->setUInt16(index++, statusItr->second.ItemCount[i]); + + stmt->setUInt16(index, statusItr->second.PlayerCount); + trans->Append(stmt); + } + } + else + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_QUESTSTATUS_BY_QUEST); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, saveItr->first); + trans->Append(stmt); + } + } + + m_QuestStatusSave.clear(); + + for (saveItr = m_RewardedQuestsSave.begin(); saveItr != m_RewardedQuestsSave.end(); ++saveItr) + { + if (saveItr->second == QUEST_DEFAULT_SAVE_TYPE) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_QUESTSTATUS_REWARDED); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, saveItr->first); + trans->Append(stmt); + + } + else if (saveItr->second == QUEST_FORCE_DELETE_SAVE_TYPE || !keepAbandoned) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_QUESTSTATUS_REWARDED_BY_QUEST); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, saveItr->first); + trans->Append(stmt); + } + } + + m_RewardedQuestsSave.clear(); + + if (!isTransaction) + CharacterDatabase.CommitTransaction(trans); +} + +void Player::_SaveDailyQuestStatus(CharacterDatabaseTransaction& trans) +{ + if (!m_DailyQuestChanged) + return; + + m_DailyQuestChanged = false; + + // save last daily quest time for all quests: we need only mostly reset time for reset check anyway + + // we don't need transactions here. + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_QUESTSTATUS_DAILY); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + for (uint32 quest_daily_idx = 0; quest_daily_idx < PLAYER_MAX_DAILY_QUESTS; ++quest_daily_idx) + { + if (GetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1+quest_daily_idx)) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_QUESTSTATUS_DAILY); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, GetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1+quest_daily_idx)); + stmt->setUInt64(2, uint64(m_lastDailyQuestTime)); + trans->Append(stmt); + } + } +} + +void Player::_SaveWeeklyQuestStatus(CharacterDatabaseTransaction& trans) +{ + if (!m_WeeklyQuestChanged || m_weeklyquests.empty()) + return; + + // we don't need transactions here. + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_QUESTSTATUS_WEEKLY); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + for (QuestSet::const_iterator iter = m_weeklyquests.begin(); iter != m_weeklyquests.end(); ++iter) + { + uint32 questId = *iter; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_QUESTSTATUS_WEEKLY); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, questId); + trans->Append(stmt); + } + + m_WeeklyQuestChanged = false; +} + +void Player::_SaveSeasonalQuestStatus(CharacterDatabaseTransaction& trans) +{ + if (!m_SeasonalQuestChanged) + return; + + // we don't need transactions here. + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_QUESTSTATUS_SEASONAL); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + m_SeasonalQuestChanged = false; + + if (m_seasonalquests.empty()) + return; + + for (SeasonalQuestMapByEvent::const_iterator iter = m_seasonalquests.begin(); iter != m_seasonalquests.end(); ++iter) + { + uint16 eventId = iter->first; + + for (SeasonalQuestMapByQuest::const_iterator itr = iter->second.begin(); itr != iter->second.end(); ++itr) + { + uint32 questId = itr->first; + time_t completedTime = itr->second; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_QUESTSTATUS_SEASONAL); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, questId); + stmt->setUInt32(2, eventId); + stmt->setInt64(3, completedTime); + trans->Append(stmt); + } + } +} + +void Player::_SaveMonthlyQuestStatus(CharacterDatabaseTransaction& trans) +{ + if (!m_MonthlyQuestChanged || m_monthlyquests.empty()) + return; + + // we don't need transactions here. + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_QUESTSTATUS_MONTHLY); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + for (QuestSet::const_iterator iter = m_monthlyquests.begin(); iter != m_monthlyquests.end(); ++iter) + { + uint32 questId = *iter; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_QUESTSTATUS_MONTHLY); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, questId); + trans->Append(stmt); + } + + m_MonthlyQuestChanged = false; +} + +void Player::_SaveLFGRewardStatus(CharacterDatabaseTransaction& trans) +{ + if (!m_LFGRewardStatusChanged || m_lfgrewardstatus.empty()) + return; + + // we don't need transactions here. + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_REWARDSTATUS_LFG); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + for (LFGRewardStatusMap::const_iterator itr = m_lfgrewardstatus.begin(); itr != m_lfgrewardstatus.end(); ++itr) + { + uint32 dungeonId = itr->first; + uint8 rewardCount = itr->second.CompletionsThisPeriod; + uint8 dailyReset = itr->second.IsDaily; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_REWARDSTATUS_LFG); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, dungeonId); + stmt->setUInt8(2, rewardCount); + stmt->setUInt8(3, dailyReset); + trans->Append(stmt); + } + + m_LFGRewardStatusChanged = false; +} + +void Player::_SaveSkills(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt; + // we don't need transactions here. + for (SkillStatusMap::iterator itr = mSkillStatus.begin(); itr != mSkillStatus.end();) + { + if (itr->second.uState == SKILL_UNCHANGED) + { + ++itr; + continue; + } + + uint16 field = itr->second.pos / 2; + uint8 offset = itr->second.pos & 1; + + uint16 value = GetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset); + uint16 max = GetUInt16Value(PLAYER_SKILL_MAX_RANK_0 + field, offset); + + switch (itr->second.uState) + { + case SKILL_NEW: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_SKILLS); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt16(1, uint16(itr->first)); + stmt->setUInt16(2, value); + stmt->setUInt16(3, max); + trans->Append(stmt); + break; + case SKILL_CHANGED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_SKILLS); + stmt->setUInt16(0, value); + stmt->setUInt16(1, max); + stmt->setUInt32(2, GetGUID().GetCounter()); + stmt->setUInt16(3, uint16(itr->first)); + trans->Append(stmt); + break; + case SKILL_DELETED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SKILL_BY_SKILL); + stmt->setUInt64(0, GetGUID().GetCounter()); + stmt->setUInt16(1, uint16(itr->first)); + trans->Append(stmt); + break; + default: + break; + } + + itr->second.uState = SKILL_UNCHANGED; + ++itr; + } +} + +void Player::_SaveSpells(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt; + + for (PlayerSpellMap::iterator itr = m_spells.begin(); itr != m_spells.end();) + { + if (itr->second.state == PLAYERSPELL_REMOVED || itr->second.state == PLAYERSPELL_CHANGED) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_SPELL_BY_SPELL); + stmt->setUInt32(0, itr->first); + stmt->setUInt32(1, GetGUID().GetCounter()); + trans->Append(stmt); + } + + // add only changed/new not dependent spells + if (!itr->second.dependent && (itr->second.state == PLAYERSPELL_NEW || itr->second.state == PLAYERSPELL_CHANGED)) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_SPELL); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, itr->first); + stmt->setBool(2, itr->second.active); + stmt->setBool(3, itr->second.disabled); + trans->Append(stmt); + } + + if (itr->second.state == PLAYERSPELL_REMOVED) + { + itr = m_spells.erase(itr); + continue; + } + + if (itr->second.state != PLAYERSPELL_TEMPORARY) + itr->second.state = PLAYERSPELL_UNCHANGED; + + ++itr; + } +} + +// save player stats -- only for external usage +// real stats will be recalculated on player login +void Player::_SaveStats(CharacterDatabaseTransaction& trans) const +{ + // check if stat saving is enabled and if char level is high enough + if (!sWorld->getIntConfig(CONFIG_MIN_LEVEL_STAT_SAVE) || getLevel() < sWorld->getIntConfig(CONFIG_MIN_LEVEL_STAT_SAVE)) + return; + + CharacterDatabasePreparedStatement* stmt; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_STATS); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + uint8 index = 0; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_STATS); + stmt->setUInt32(index++, GetGUID().GetCounter()); + stmt->setUInt32(index++, GetMaxHealth()); + + for (uint8 i = 0; i < MAX_POWERS_PER_CLASS; ++i) + stmt->setUInt32(index++, GetMaxPower(PowerType(i))); + + for (StatType stat : AllPrimaryStats) + stmt->setUInt32(index++, GetStat(stat)); + + for (int i = 0; i < MAX_SPELL_SCHOOL; ++i) + stmt->setUInt32(index++, GetResistance(SpellSchools(i))); + + stmt->setFloat(index++, GetFloatValue(PLAYER_BLOCK_PERCENTAGE)); + stmt->setFloat(index++, GetFloatValue(PLAYER_DODGE_PERCENTAGE)); + stmt->setFloat(index++, GetFloatValue(PLAYER_PARRY_PERCENTAGE)); + stmt->setFloat(index++, GetFloatValue(PLAYER_CRIT_PERCENTAGE)); + stmt->setFloat(index++, GetFloatValue(PLAYER_RANGED_CRIT_PERCENTAGE)); + stmt->setFloat(index++, GetFloatValue(PLAYER_SPELL_CRIT_PERCENTAGE1)); + stmt->setUInt32(index++, GetUInt32Value(UNIT_FIELD_ATTACK_POWER)); + stmt->setUInt32(index++, GetUInt32Value(UNIT_FIELD_RANGED_ATTACK_POWER)); + stmt->setUInt32(index++, GetBaseSpellPowerBonus()); + stmt->setUInt32(index++, GetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1 + AsUnderlyingType(CR_RESILIENCE_PLAYER_DAMAGE_TAKEN))); + + trans->Append(stmt); +} + +void Player::outDebugValues() const +{ + if (!sLog->ShouldLog("entities.unit", LOG_LEVEL_DEBUG)) + return; + + TC_LOG_DEBUG("entities.unit", "HP is: \t\t\t%u\t\tMP is: \t\t\t%u", GetMaxHealth(), GetMaxPower(POWER_MANA)); + TC_LOG_DEBUG("entities.unit", "AGILITY is: \t\t%i\t\tSTRENGTH is: \t\t%i", GetStat(StatType::Agility), GetStat(StatType::Strength)); + TC_LOG_DEBUG("entities.unit", "INTELLECT is: \t\t%i\t\tSPIRIT is: \t\t%i", GetStat(StatType::Intellect), GetStat(StatType::Spirit)); + TC_LOG_DEBUG("entities.unit", "STAMINA is: \t\t%i", GetStat(StatType::Stamina)); + TC_LOG_DEBUG("entities.unit", "Armor is: \t\t%u\t\tBlock is: \t\t%f", GetArmor(), GetFloatValue(PLAYER_BLOCK_PERCENTAGE)); + TC_LOG_DEBUG("entities.unit", "HolyRes is: \t\t%u\t\tFireRes is: \t\t%u", GetResistance(SPELL_SCHOOL_HOLY), GetResistance(SPELL_SCHOOL_FIRE)); + TC_LOG_DEBUG("entities.unit", "NatureRes is: \t\t%u\t\tFrostRes is: \t\t%u", GetResistance(SPELL_SCHOOL_NATURE), GetResistance(SPELL_SCHOOL_FROST)); + TC_LOG_DEBUG("entities.unit", "ShadowRes is: \t\t%u\t\tArcaneRes is: \t\t%u", GetResistance(SPELL_SCHOOL_SHADOW), GetResistance(SPELL_SCHOOL_ARCANE)); + TC_LOG_DEBUG("entities.unit", "MIN_DAMAGE is: \t\t%f\tMAX_DAMAGE is: \t\t%f", GetFloatValue(UNIT_FIELD_MINDAMAGE), GetFloatValue(UNIT_FIELD_MAXDAMAGE)); + TC_LOG_DEBUG("entities.unit", "MIN_OFFHAND_DAMAGE is: \t%f\tMAX_OFFHAND_DAMAGE is: \t%f", GetFloatValue(UNIT_FIELD_MINOFFHANDDAMAGE), GetFloatValue(UNIT_FIELD_MAXOFFHANDDAMAGE)); + TC_LOG_DEBUG("entities.unit", "MIN_RANGED_DAMAGE is: \t%f\tMAX_RANGED_DAMAGE is: \t%f", GetFloatValue(UNIT_FIELD_MINRANGEDDAMAGE), GetFloatValue(UNIT_FIELD_MAXRANGEDDAMAGE)); + TC_LOG_DEBUG("entities.unit", "ATTACK_TIME is: \t%u\t\tRANGE_ATTACK_TIME is: \t%u", GetBaseAttackTime(BASE_ATTACK), GetBaseAttackTime(RANGED_ATTACK)); +} + +/*********************************************************/ +/*** FLOOD FILTER SYSTEM ***/ +/*********************************************************/ + +void Player::UpdateSpeakTime(ChatFloodThrottle::Index index) +{ + // ignore chat spam protection for GMs in any mode + if (GetSession()->HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHAT_SPAM)) + return; + + uint32 limit; + uint32 delay; + switch (index) + { + case ChatFloodThrottle::REGULAR: + limit = sWorld->getIntConfig(CONFIG_CHATFLOOD_MESSAGE_COUNT); + delay = sWorld->getIntConfig(CONFIG_CHATFLOOD_MESSAGE_DELAY); + break; + case ChatFloodThrottle::ADDON: + limit = sWorld->getIntConfig(CONFIG_CHATFLOOD_ADDON_MESSAGE_COUNT); + delay = sWorld->getIntConfig(CONFIG_CHATFLOOD_ADDON_MESSAGE_DELAY); + break; + default: + return; + } + + time_t current = GameTime::GetGameTime(); + if (m_chatFloodData[index].Time > current) + { + if (!limit) + return; + + ++m_chatFloodData[index].Count; + if (m_chatFloodData[index].Count >= limit) + { + // prevent overwrite mute time, if message send just before mutes set, for example. + time_t new_mute = current + sWorld->getIntConfig(CONFIG_CHATFLOOD_MUTE_TIME); + if (GetSession()->m_muteTime < new_mute) + GetSession()->m_muteTime = new_mute; + + m_chatFloodData[index].Count = 0; + } + } + else + m_chatFloodData[index].Count = 1; + + m_chatFloodData[index].Time = current + delay; +} + +/*********************************************************/ +/*** LOW LEVEL FUNCTIONS:Notifiers ***/ +/*********************************************************/ + +void Player::SendAttackSwingNotInRange() const +{ + WorldPacket data(SMSG_ATTACKSWING_NOTINRANGE, 0); + SendDirectMessage(&data); +} + +void Player::SavePositionInDB(WorldLocation const& loc, uint16 zoneId, ObjectGuid guid, CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHARACTER_POSITION); + + stmt->setFloat(0, loc.GetPositionX()); + stmt->setFloat(1, loc.GetPositionY()); + stmt->setFloat(2, loc.GetPositionZ()); + stmt->setFloat(3, loc.GetOrientation()); + stmt->setUInt16(4, uint16(loc.GetMapId())); + stmt->setUInt16(5, zoneId); + stmt->setUInt32(6, guid.GetCounter()); + + CharacterDatabase.ExecuteOrAppend(trans, stmt); +} + +void Player::Customize(CharacterCustomizeInfo const* customizeInfo, CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_GENDER_AND_APPEARANCE); + + stmt->setUInt8(0, customizeInfo->Gender); + stmt->setUInt8(1, customizeInfo->Skin); + stmt->setUInt8(2, customizeInfo->Face); + stmt->setUInt8(3, customizeInfo->HairStyle); + stmt->setUInt8(4, customizeInfo->HairColor); + stmt->setUInt8(5, customizeInfo->FacialHair); + stmt->setUInt32(6, customizeInfo->Guid.GetCounter()); + + CharacterDatabase.ExecuteOrAppend(trans, stmt); +} + +void Player::SendAttackSwingDeadTarget() const +{ + WorldPacket data(SMSG_ATTACKSWING_DEADTARGET, 0); + SendDirectMessage(&data); +} + +void Player::SendAttackSwingCantAttack() const +{ + WorldPacket data(SMSG_ATTACKSWING_CANT_ATTACK, 0); + SendDirectMessage(&data); +} + +void Player::SendAttackSwingCancelAttack() const +{ + WorldPacket data(SMSG_CANCEL_COMBAT, 0); + SendDirectMessage(&data); +} + +void Player::SendAttackSwingBadFacingAttack() const +{ + WorldPacket data(SMSG_ATTACKSWING_BADFACING, 0); + SendDirectMessage(&data); +} + +void Player::SendAutoRepeatCancel(Unit* target) +{ + WorldPacket data(SMSG_CANCEL_AUTO_REPEAT, target->GetPackGUID().size()); + data << target->GetPackGUID(); // may be it's target guid + SendMessageToSet(&data, true); +} + +void Player::SendExplorationExperience(uint32 Area, uint32 Experience) const +{ + WorldPacket data(SMSG_EXPLORATION_EXPERIENCE, 8); + data << uint32(Area); + data << uint32(Experience); + SendDirectMessage(&data); +} + +void Player::SendDungeonDifficulty(bool IsInGroup) const +{ + uint8 val = 0x00000001; + WorldPacket data(MSG_SET_DUNGEON_DIFFICULTY, 12); + data << (uint32)GetDungeonDifficulty(); + data << uint32(val); + data << uint32(IsInGroup); + SendDirectMessage(&data); +} + +void Player::SendRaidDifficulty(bool IsInGroup, int32 forcedDifficulty) const +{ + uint8 val = 0x00000001; + WorldPacket data(MSG_SET_RAID_DIFFICULTY, 12); + data << uint32(forcedDifficulty == -1 ? GetRaidDifficulty() : forcedDifficulty); + data << uint32(val); + data << uint32(IsInGroup); + SendDirectMessage(&data); +} + +void Player::SendResetFailedNotify(uint32 /*mapid*/) const +{ + WorldPacket data(SMSG_RESET_FAILED_NOTIFY, 4); + SendDirectMessage(&data); +} + +/// Reset all solo instances and optionally send a message on success for each +void Player::ResetInstances(uint8 method, bool isRaid) +{ + // method can be INSTANCE_RESET_ALL, INSTANCE_RESET_CHANGE_DIFFICULTY, INSTANCE_RESET_GROUP_JOIN + + // we assume that when the difficulty changes, all instances that can be reset will be + Difficulty diff = GetDifficulty(isRaid); + + for (BoundInstancesMap::iterator itr = m_boundInstances[diff].begin(); itr != m_boundInstances[diff].end();) + { + InstanceSave* p = itr->second.save; + MapEntry const* entry = sMapStore.LookupEntry(itr->first); + if (!entry || entry->IsRaid() != isRaid || !p->CanReset()) + { + ++itr; + continue; + } + + if (method == INSTANCE_RESET_ALL) + { + // the "reset all instances" method can only reset normal maps + if (entry->MapType == MAP_RAID || diff == DUNGEON_DIFFICULTY_HEROIC) + { + ++itr; + continue; + } + } + + // if the map is loaded, reset it + Map* map = sMapMgr->FindMap(p->GetMapId(), p->GetInstanceId()); + if (map && map->IsDungeon()) + if (!map->ToInstanceMap()->Reset(method)) + { + ++itr; + continue; + } + + // since this is a solo instance there should not be any players inside + if (method == INSTANCE_RESET_ALL || method == INSTANCE_RESET_CHANGE_DIFFICULTY) + SendResetInstanceSuccess(p->GetMapId()); + + p->DeleteFromDB(); + m_boundInstances[diff].erase(itr++); + + // the following should remove the instance save from the manager and delete it as well + p->RemovePlayer(this); + } +} + +void Player::SendResetInstanceSuccess(uint32 MapId) const +{ + WorldPacket data(SMSG_INSTANCE_RESET, 4); + data << uint32(MapId); + SendDirectMessage(&data); +} + +void Player::SendResetInstanceFailed(uint32 reason, uint32 MapId) const +{ + /*reasons for instance reset failure: + // 0: There are players inside the instance. + // 1: There are players offline in your party. + // 2>: There are players in your party attempting to zone into an instance. + */ + WorldPacket data(SMSG_INSTANCE_RESET_FAILED, 8); + data << uint32(reason); + data << uint32(MapId); + SendDirectMessage(&data); +} + +/*********************************************************/ +/*** Update timers ***/ +/*********************************************************/ + +///checks the 15 afk reports per 5 minutes limit +void Player::UpdateAfkReport(time_t currTime) +{ + if (m_bgData.bgAfkReportedTimer <= currTime) + { + m_bgData.bgAfkReportedCount = 0; + m_bgData.bgAfkReportedTimer = currTime+5*MINUTE; + } +} + +void Player::SetContestedPvP(Player* attackedPlayer) +{ + if (attackedPlayer && (attackedPlayer == this || (duel && duel->opponent == attackedPlayer))) + return; + + SetContestedPvPTimer(30000); + if (!HasUnitState(UNIT_STATE_ATTACK_PLAYER)) + { + AddUnitState(UNIT_STATE_ATTACK_PLAYER); + SetFlag(PLAYER_FLAGS, PLAYER_FLAGS_CONTESTED_PVP); + // call MoveInLineOfSight for nearby contested guards + Trinity::AIRelocationNotifier notifier(*this); + Cell::VisitWorldObjects(this, notifier, GetVisibilityRange()); + } + for (Unit* unit : m_Controlled) + { + if (!unit->HasUnitState(UNIT_STATE_ATTACK_PLAYER)) + { + unit->AddUnitState(UNIT_STATE_ATTACK_PLAYER); + Trinity::AIRelocationNotifier notifier(*unit); + Cell::VisitWorldObjects(this, notifier, GetVisibilityRange()); + } + } +} + +void Player::UpdateContestedPvP(uint32 diff) +{ + if (!m_contestedPvPTimer || IsInCombat()) + return; + + if (m_contestedPvPTimer <= diff) + ResetContestedPvP(); + else + m_contestedPvPTimer -= diff; +} + +void Player::ResetContestedPvP() +{ + ClearUnitState(UNIT_STATE_ATTACK_PLAYER); + RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_CONTESTED_PVP); + m_contestedPvPTimer = 0; +} + +void Player::UpdatePvPFlag(time_t currTime) +{ + if (!IsPvP()) + return; + + if (!pvpInfo.EndTimer || currTime < (pvpInfo.EndTimer + 300) || pvpInfo.IsHostile) + return; + + UpdatePvP(false); +} + +void Player::UpdateDuelFlag(time_t currTime) +{ + if (!duel || duel->startTimer == 0 ||currTime < duel->startTimer + 3) + return; + + sScriptMgr->OnPlayerDuelStart(this, duel->opponent); + + SetUInt32Value(PLAYER_DUEL_TEAM, 1); + duel->opponent->SetUInt32Value(PLAYER_DUEL_TEAM, 2); + + duel->startTimer = 0; + duel->startTime = currTime; + duel->opponent->duel->startTimer = 0; + duel->opponent->duel->startTime = currTime; +} + +Pet* Player::GetPet() const +{ + if (ObjectGuid pet_guid = GetPetGUID()) + { + if (!pet_guid.IsPet()) + return nullptr; + + Pet* pet = ObjectAccessor::GetPet(*this, pet_guid); + + if (!pet) + return nullptr; + + if (IsInWorld()) + return pet; + + // there may be a guardian in this slot + //TC_LOG_ERROR("entities.player", "Player::GetPet: Pet %u does not exist.", GUID_LOPART(pet_guid)); + //const_cast(this)->SetPetGUID(0); + } + + return nullptr; +} + +void Player::RemovePet(Pet* pet, PetSaveMode mode, bool returnreagent) +{ + if (!pet) + pet = GetPet(); + + if (pet) + { + TC_LOG_DEBUG("entities.pet", "Player::RemovePet: Player '%s' (%s), Pet (Entry: %u, Mode: %u, ReturnReagent: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), pet->GetEntry(), mode, returnreagent); + + if (pet->m_removed) + return; + } + + if (returnreagent && (pet || m_temporaryUnsummonedPetNumber) && !InBattleground()) + { + //returning of reagents only for players, so best done here + uint32 spellId = pet ? pet->GetUInt32Value(UNIT_CREATED_BY_SPELL) : m_oldpetspell; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + + if (spellInfo) + { + for (uint32 i = 0; i < MAX_SPELL_REAGENTS; ++i) + { + if (spellInfo->Reagent[i] > 0) + { + ItemPosCountVec dest; //for succubus, voidwalker, felhunter and felguard credit soulshard when despawn reason other than death (out of range, logout) + InventoryResult msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, spellInfo->Reagent[i], spellInfo->ReagentCount[i]); + if (msg == EQUIP_ERR_OK) + { + Item* item = StoreNewItem(dest, spellInfo->Reagent[i], true); + if (IsInWorld()) + SendNewItem(item, spellInfo->ReagentCount[i], true, false); + } + } + } + } + m_temporaryUnsummonedPetNumber = 0; + } + + if (!pet || pet->GetOwnerOrCreatorGUID() != GetGUID()) + return; + + pet->CombatStop(); + + if (returnreagent) + { + switch (pet->GetEntry()) + { + //warlock pets except imp are removed(?) when logging out + case 1860: + case 1863: + case 417: + case 17252: + mode = PET_SAVE_DISMISS; + break; + } + } + + pet->SavePetToDB(mode); + + SetMinion(pet, false); + + pet->AddObjectToRemoveList(); + pet->m_removed = true; + + if (pet->isControlled()) + { + WorldPacket data(SMSG_PET_SPELLS, 8); + data << uint64(0); + SendDirectMessage(&data); + + if (GetGroup()) + SetGroupUpdateFlag(GROUP_UPDATE_PET); + + if (mode == PET_SAVE_DISMISS && getClass() == CLASS_WARLOCK) + { + if (CreatureDisplayInfoEntry const* creatureDisplay = sCreatureDisplayInfoStore.LookupEntry(pet->GetDisplayId())) + { + WorldPackets::Pet::PetDismissSound dismissSound; + dismissSound.ModelID = creatureDisplay->ModelID; + dismissSound.ModelPosition = pet->GetPosition(); + pet->SendMessageToSet(dismissSound.Write(), false); + } + } + } +} + +void Player::AddPetAura(PetAura const* petSpell) +{ + m_petAuras.insert(petSpell); + if (Pet* pet = GetPet()) + pet->CastPetAura(petSpell); +} + +void Player::RemovePetAura(PetAura const* petSpell) +{ + m_petAuras.erase(petSpell); + if (Pet* pet = GetPet()) + pet->RemoveAurasDueToSpell(petSpell->GetAura(pet->GetEntry())); +} + +void Player::StopCastingCharm() +{ + Unit* charm = GetCharmed(); + if (!charm) + return; + + if (charm->GetTypeId() == TYPEID_UNIT) + { + if (charm->ToCreature()->HasUnitTypeMask(UNIT_MASK_PUPPET)) + static_cast(charm)->UnSummon(); + else if (charm->IsVehicle()) + { + ExitVehicle(); + + // Temporary for issue https://github.com/TrinityCore/TrinityCore/issues/24876 + if (!GetCharmedGUID().IsEmpty() && !charm->HasAuraTypeWithCaster(SPELL_AURA_CONTROL_VEHICLE, GetGUID())) + { + TC_LOG_FATAL("entities.player", "Player::StopCastingCharm Player '%s' (%s) is not able to uncharm vehicle (%s) because of missing SPELL_AURA_CONTROL_VEHICLE", + GetName().c_str(), GetGUID().ToString().c_str(), GetCharmedGUID().ToString().c_str()); + + // attempt to recover from missing HandleAuraControlVehicle unapply handling + // THIS IS A HACK, NEED TO FIND HOW IS IT EVEN POSSBLE TO NOT HAVE THE AURA + _ExitVehicle(); + } + } + } + if (GetCharmedGUID()) + charm->RemoveCharmAuras(); + + if (GetCharmedGUID()) + { + TC_LOG_FATAL("entities.player", "Player::StopCastingCharm: Player '%s' (%s) is not able to uncharm unit (%s)", GetName().c_str(), GetGUID().ToString().c_str(), GetCharmedGUID().ToString().c_str()); + if (!charm->GetCharmerGUID().IsEmpty()) + { + TC_LOG_FATAL("entities.player", "Player::StopCastingCharm: Charmed unit has charmer %s", charm->GetCharmerGUID().ToString().c_str()); + ABORT(); + } + + SetCharm(charm, false); + } +} + +void Player::Say(std::string const& text, Language language, WorldObject const* /*= nullptr*/) +{ + std::string _text(text); + sScriptMgr->OnPlayerChat(this, CHAT_MSG_SAY, language, _text); + + WorldPacket data; + ChatHandler::BuildChatPacket(data, CHAT_MSG_SAY, language, this, this, _text); + SendMessageToSetInRange(&data, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY), true); +} + +void Player::Say(uint32 textId, WorldObject const* target /*= nullptr*/) +{ + Talk(textId, CHAT_MSG_SAY, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY), target); +} + +void Player::Yell(std::string const& text, Language language, WorldObject const* /*= nullptr*/) +{ + std::string _text(text); + sScriptMgr->OnPlayerChat(this, CHAT_MSG_YELL, language, _text); + + WorldPacket data; + ChatHandler::BuildChatPacket(data, CHAT_MSG_YELL, language, this, this, _text); + SendMessageToSetInRange(&data, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_YELL), true); +} + +void Player::Yell(uint32 textId, WorldObject const* target /*= nullptr*/) +{ + Talk(textId, CHAT_MSG_YELL, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_YELL), target); +} + +void Player::TextEmote(std::string const& text, WorldObject const* /*= nullptr*/, bool /*= false*/) +{ + std::string _text(text); + sScriptMgr->OnPlayerChat(this, CHAT_MSG_EMOTE, LANG_UNIVERSAL, _text); + + WorldPacket data; + ChatHandler::BuildChatPacket(data, CHAT_MSG_EMOTE, LANG_UNIVERSAL, this, this, _text); + SendMessageToSetInRange(&data, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_TEXTEMOTE), true, !GetSession()->HasPermission(rbac::RBAC_PERM_TWO_SIDE_INTERACTION_CHAT)); +} + +void Player::TextEmote(uint32 textId, WorldObject const* target /*= nullptr*/, bool /*isBossEmote = false*/) +{ + Talk(textId, CHAT_MSG_EMOTE, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_TEXTEMOTE), target); +} + +void Player::WhisperAddon(std::string const& text, const std::string& prefix, Player* receiver) +{ + std::string _text(text); + sScriptMgr->OnPlayerChat(this, CHAT_MSG_WHISPER, uint32(LANG_ADDON), _text, receiver); + + if (!receiver->GetSession()->IsAddonRegistered(prefix)) + return; + + WorldPacket data; + ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER, LANG_ADDON, this, this, text, 0, "", DEFAULT_LOCALE, prefix); + receiver->SendDirectMessage(&data); +} + +void Player::Whisper(std::string const& text, Language language, Player* target, bool isBossWhisper) +{ + ASSERT(target); + + bool isAddonMessage = language == LANG_ADDON; + + if (!isAddonMessage) // if not addon data + language = LANG_UNIVERSAL; // whispers should always be readable + + std::string _text(text); + sScriptMgr->OnPlayerChat(this, isBossWhisper ? CHAT_MSG_RAID_BOSS_WHISPER : CHAT_MSG_WHISPER, language, _text, target); + + WorldPacket data; + ChatHandler::BuildChatPacket(data, isBossWhisper ? CHAT_MSG_RAID_BOSS_WHISPER : CHAT_MSG_WHISPER, Language(language), this, this, _text); + target->SendDirectMessage(&data); + + // rest stuff shouldn't happen in case of addon message + if (isAddonMessage) + return; + + ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER_INFORM, Language(language), target, target, _text); + SendDirectMessage(&data); + + if (!isAcceptWhispers() && !IsGameMaster() && !target->IsGameMaster()) + { + SetAcceptWhispers(true); + ChatHandler(GetSession()).SendSysMessage(LANG_COMMAND_WHISPERON); + } + + // announce afk or dnd message + if (target->isAFK()) + ChatHandler(GetSession()).PSendSysMessage(LANG_PLAYER_AFK, target->GetName().c_str(), target->autoReplyMsg.c_str()); + else if (target->isDND()) + ChatHandler(GetSession()).PSendSysMessage(LANG_PLAYER_DND, target->GetName().c_str(), target->autoReplyMsg.c_str()); +} + +void Player::Whisper(uint32 textId, Player* target, bool isBossWhisper) +{ + if (!target) + return; + + BroadcastText const* bct = sObjectMgr->GetBroadcastText(textId); + if (!bct) + { + TC_LOG_ERROR("entities.unit", "WorldObject::MonsterWhisper: `broadcast_text` was not %u found", textId); + return; + } + + LocaleConstant locale = target->GetSession()->GetSessionDbLocaleIndex(); + WorldPacket data; + ChatHandler::BuildChatPacket(data, isBossWhisper ? CHAT_MSG_RAID_BOSS_WHISPER : CHAT_MSG_WHISPER, LANG_UNIVERSAL, this, target, bct->GetText(locale, getGender()), 0, "", locale); + target->SendDirectMessage(&data); +} + +Item* Player::GetMItem(uint32 id) +{ + ItemMap::const_iterator itr = mMitems.find(id); + return itr != mMitems.end() ? itr->second : nullptr; +} + +void Player::AddMItem(Item* it) +{ + ASSERT(it); + //ASSERT deleted, because items can be added before loading + mMitems[it->GetGUID().GetCounter()] = it; +} + +bool Player::RemoveMItem(uint32 id) +{ + return mMitems.erase(id) ? true : false; +} + +void Player::SendOnCancelExpectedVehicleRideAura() const +{ + WorldPacket data(SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA, 0); + SendDirectMessage(&data); +} + +bool Player::CanControlPet(uint32 spellId) const +{ + // Hunters and Warlocks cannot control their pets until they learned their spell + if (spellId) + { + switch (spellId) + { + case 93321: // Control Pet + return getClass() == CLASS_HUNTER; + case 93375: // Control Demon + return getClass() == CLASS_WARLOCK; + } + } + else + { + if (getClass() == CLASS_HUNTER && !HasAura(93321)) + return false; + if (getClass() == CLASS_WARLOCK && !HasAura(93375)) + return false; + } + + return true; +} + +void Player::PetSpellInitialize() +{ + // Do not send the pet action bar when Hunters or Warlocks cannot control their pet + if (!CanControlPet()) + return; + + Pet* pet = GetPet(); + + if (!pet) + return; + + CharmInfo* charmInfo = pet->GetCharmInfo(); + + WorldPacket data(SMSG_PET_SPELLS, 8 + 2 + 4 + 4 + 4 * MAX_UNIT_ACTION_BAR_INDEX + 1 + 1); + data << uint64(pet->GetGUID()); + data << uint16(pet->GetCreatureTemplate()->family); // creature family (required for pet talents) + data << uint32(pet->GetDuration()); + data << uint8(pet->GetReactState()); + data << uint8(charmInfo->GetCommandState()); + data << uint16(0); // Flags, mostly unknown + + TC_LOG_DEBUG("entities.pet", "Player::PetspellInitialize: Creating spellgroups for summoned pet"); + + // action bar loop + charmInfo->BuildActionBar(&data); + + size_t spellsCountPos = data.wpos(); + + // spells count + uint8 addlist = 0; + data << uint8(addlist); // placeholder + + if (pet->IsPermanentPetFor(this)) + { + // spells loop + for (PetSpellMap::iterator itr = pet->m_spells.begin(); itr != pet->m_spells.end(); ++itr) + { + if (itr->second.state == PETSPELL_REMOVED) + continue; + + // Do not send this spells, they are used indirectly + if (sSpellMgr->GetSpellInfo(itr->first)->HasAttribute(SPELL_ATTR4_NOT_IN_SPELLBOOK)) + continue; + + data << uint32(MAKE_UNIT_ACTION_BUTTON(itr->first, itr->second.active)); + ++addlist; + } + } + + data.put(spellsCountPos, addlist); + + //Cooldowns + pet->GetSpellHistory()->WritePacket(data); + + SendDirectMessage(&data); +} + +void Player::PossessSpellInitialize() +{ + Unit* charm = GetCharmed(); + if (!charm) + return; + + CharmInfo* charmInfo = charm->GetCharmInfo(); + + if (!charmInfo) + { + TC_LOG_ERROR("entities.player", "Player::PossessSpellInitialize: charm (%s) has no charminfo!", charm->GetGUID().ToString().c_str()); + return; + } + + WorldPacket data(SMSG_PET_SPELLS, 8+2+4+4+4*MAX_UNIT_ACTION_BAR_INDEX+1+1); + data << uint64(charm->GetGUID()); + data << uint16(0); + data << uint32(0); + data << uint32(0); + + charmInfo->BuildActionBar(&data); + + data << uint8(0); // spells count + data << uint8(0); // cooldowns count + + SendDirectMessage(&data); +} + +void Player::VehicleSpellInitialize() +{ + Creature* vehicle = GetVehicleCreatureBase(); + if (!vehicle) + return; + + uint8 cooldownCount = vehicle->GetSpellHistory()->GetCooldownsSizeForPacket(); + + WorldPacket data(SMSG_PET_SPELLS, 8 + 2 + 4 + 4 + 4 * 10 + 1 + 1 + cooldownCount * (4 + 2 + 4 + 4)); + data << uint64(vehicle->GetGUID()); // Guid + data << uint16(0); // Pet Family (0 for all vehicles) + data << uint32(vehicle->IsSummon() ? vehicle->ToTempSummon()->GetTimer() : 0); // Duration + // The following three segments are read by the client as one uint32 + data << uint8(vehicle->GetReactState()); // React State + data << uint8(0); // Command State + data << uint16(0x800); // DisableActions (set for all vehicles) + + for (uint32 i = 0; i < MAX_CREATURE_SPELLS; ++i) + { + uint32 spellId = vehicle->m_spells[i]; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo || spellInfo->HasAttribute(SPELL_ATTR5_NOT_AVAILABLE_WHILE_CHARMED)) + { + data << uint16(0) << uint8(0) << uint8(i+8); + continue; + } + + if (!sConditionMgr->IsObjectMeetingVehicleSpellConditions(vehicle->GetEntry(), spellId, this, vehicle)) + { + TC_LOG_DEBUG("condition", "Player::VehicleSpellInitialize: Player '%s' (%s) doesn't meet conditions for vehicle (Entry: %u, Spell: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), vehicle->ToCreature()->GetEntry(), spellId); + data << uint16(0) << uint8(0) << uint8(i+8); + continue; + } + + if (spellInfo->IsPassive()) + vehicle->CastSpell(vehicle, spellId, true); + + data << uint32(MAKE_UNIT_ACTION_BUTTON(spellId, i+8)); + } + + for (uint32 i = MAX_CREATURE_SPELLS; i < MAX_SPELL_CONTROL_BAR; ++i) + data << uint32(0); + + data << uint8(0); // Auras? + + // Cooldowns + vehicle->GetSpellHistory()->WritePacket(data); + SendDirectMessage(&data); +} + +void Player::CharmSpellInitialize() +{ + Unit* charm = GetFirstControlled(); + if (!charm) + return; + + CharmInfo* charmInfo = charm->GetCharmInfo(); + if (!charmInfo) + { + TC_LOG_ERROR("entities.player", "Player::CharmSpellInitialize(): Player '%s' (%s) has a charm (%s) but no no charminfo!", + GetName().c_str(), GetGUID().ToString().c_str(), charm->GetGUID().ToString().c_str()); + return; + } + + uint8 addlist = 0; + if (charm->GetTypeId() != TYPEID_PLAYER) + { + //CreatureInfo const* cinfo = charm->ToCreature()->GetCreatureTemplate(); + //if (cinfo && cinfo->type == CREATURE_TYPE_DEMON && getClass() == CLASS_WARLOCK) + { + for (uint32 i = 0; i < MAX_SPELL_CHARM; ++i) + if (charmInfo->GetCharmSpell(i)->GetAction()) + ++addlist; + } + } + + WorldPacket data(SMSG_PET_SPELLS, 8+2+4+4+4*MAX_UNIT_ACTION_BAR_INDEX+1+4*addlist+1); + data << uint64(charm->GetGUID()); + data << uint16(0); + data << uint32(0); + + if (charm->GetTypeId() != TYPEID_PLAYER) + data << uint8(charm->ToCreature()->GetReactState()) << uint8(charmInfo->GetCommandState()) << uint16(0); + else + data << uint32(0); + + charmInfo->BuildActionBar(&data); + + data << uint8(addlist); + + if (addlist) + { + for (uint32 i = 0; i < MAX_SPELL_CHARM; ++i) + { + CharmSpellInfo* cspell = charmInfo->GetCharmSpell(i); + if (cspell->GetAction()) + data << uint32(cspell->packedData); + } + } + + data << uint8(0); // cooldowns count + + SendDirectMessage(&data); +} + +void Player::SendRemoveControlBar() const +{ + WorldPacket data(SMSG_PET_SPELLS, 8); + data << uint64(0); + SendDirectMessage(&data); +} + +bool Player::IsAffectedBySpellmod(SpellInfo const* spellInfo, SpellModifier const* mod, Spell* spell) +{ + if (!mod || !spellInfo) + return false; + + // First time this aura applies a mod to us and is out of charges + if (spell && mod->ownerAura->IsUsingCharges() && !mod->ownerAura->GetCharges() && !spell->m_appliedMods.count(mod->ownerAura)) + return false; + + switch (mod->op) + { + case SpellModOp::Duration: // +duration to infinite duration spells making them limited + if (spellInfo->GetDuration() == -1) + return false; + break; + case SpellModOp::CritChance: // mod crit to spells that can't crit + if (!spellInfo->HasAttribute(SPELL_ATTR0_CU_CAN_CRIT)) + return false; + break; + case SpellModOp::PointsIndex0: // check if spell has any effect at that index + case SpellModOp::Points: + if (!spellInfo->Effects[EFFECT_0].IsEffect()) + return false; + break; + case SpellModOp::PointsIndex1: // check if spell has any effect at that index + if (!spellInfo->Effects[EFFECT_1].IsEffect()) + return false; + break; + case SpellModOp::PointsIndex2: // check if spell has any effect at that index + if (!spellInfo->Effects[EFFECT_2].IsEffect()) + return false; + break; + default: + break; + } + + return spellInfo->IsAffectedBySpellMod(mod); +} + +template +void Player::GetSpellModValues(SpellInfo const* spellInfo, SpellModOp op, Spell* spell, T base, int32* flat, float* pct) const +{ + ASSERT(flat && pct); + + *flat = 0; + *pct = 1.0f; + + auto spellModOpBegin = std::ranges::lower_bound(m_spellMods, op, std::ranges::less(), &SpellModifier::op); + if (spellModOpBegin == m_spellMods.end() || (*spellModOpBegin)->op != op) + return; + + // Drop charges for triggering spells instead of triggered ones + if (m_spellModTakingSpell) + spell = m_spellModTakingSpell; + + auto spellModTypeBegin = [&](SpellModType type) + { + auto typeBegin = spellModOpBegin; + auto end = m_spellMods.end(); + while (typeBegin != end && (*typeBegin)->op == op) + { + if ((*typeBegin)->type == type) + return typeBegin; + + ++typeBegin; + } + + return end; + }; + + // end of our iterable range will be when we reach a spellmod with different op or type than expected + // we can do this because m_spellMods is sorted by op and type + auto spellModTypeEnd = [&](SpellModType type) + { + struct EndSentinel + { + bool operator==(std::vector::const_iterator const& itr) const + { + return itr == end || (*itr)->op != op || (*itr)->type != type; + } + + std::vector::const_iterator end; + SpellModOp op; + SpellModType type; + }; + return EndSentinel{ .end = m_spellMods.end(), .op = op, .type = type }; + }; + + auto spellModTypeRange = [&](SpellModType type) { return Trinity::IteratorPair(spellModTypeBegin(type), spellModTypeEnd(type)); }; + + switch (op) + { + // special case, if a mod makes spell instant, only consume that mod + case SpellModOp::ChangeCastTime: + { + SpellModifier* modInstantSpell = nullptr; + for (SpellModifier* mod : spellModTypeRange(SPELLMOD_PCT)) + { + if (!IsAffectedBySpellmod(spellInfo, mod, spell)) + continue; + + if (base < T(10000) && static_cast(mod)->value <= -100) + { + modInstantSpell = mod; + break; + } + } + + if (modInstantSpell) + { + Player::ApplyModToSpell(modInstantSpell, spell); + *pct = 0.0f; + return; + } + break; + } + // special case if two mods apply 100% critical chance, only consume one + case SpellModOp::CritChance: + { + SpellModifier* modCritical = nullptr; + for (SpellModifier* mod : spellModTypeRange(SPELLMOD_FLAT)) + { + if (!IsAffectedBySpellmod(spellInfo, mod, spell)) + continue; + + if (static_cast(mod)->value >= 100) + { + modCritical = mod; + break; + } + } + + if (modCritical) + { + Player::ApplyModToSpell(modCritical, spell); + *flat = 100; + return; + } + break; + } + default: + break; + } + + for (SpellModifier* mod : spellModTypeRange(SPELLMOD_FLAT)) + { + if (!IsAffectedBySpellmod(spellInfo, mod, spell)) + continue; + + int32 value = static_cast(mod)->value; + if (value == 0) + continue; + + *flat += value; + Player::ApplyModToSpell(mod, spell); + } + + for (SpellModifier* mod : spellModTypeRange(SPELLMOD_PCT)) + { + if (!IsAffectedBySpellmod(spellInfo, mod, spell)) + continue; + + // skip percent mods for null basevalue (most important for spell mods with charges) + if (base + *flat == T(0)) + continue; + + int32 value = static_cast(mod)->value; + if (value == 0) + continue; + + // special case (skip > 10sec spell casts for instant cast setting) + if (op == SpellModOp::ChangeCastTime) + { + if (base >= T(10000) && value <= -100) + continue; + } + + *pct += CalculatePct(1.0f, value); + Player::ApplyModToSpell(mod, spell); + } +} + +template TC_GAME_API void Player::GetSpellModValues(SpellInfo const* spellInfo, SpellModOp op, Spell* spell, int32 base, int32* flat, float* pct) const; +template TC_GAME_API void Player::GetSpellModValues(SpellInfo const* spellInfo, SpellModOp op, Spell* spell, uint32 base, int32* flat, float* pct) const; +template TC_GAME_API void Player::GetSpellModValues(SpellInfo const* spellInfo, SpellModOp op, Spell* spell, float base, int32* flat, float* pct) const; +template TC_GAME_API void Player::GetSpellModValues(SpellInfo const* spellInfo, SpellModOp op, Spell* spell, double base, int32* flat, float* pct) const; + +template +void Player::ApplySpellMod(SpellInfo const* spellInfo, SpellModOp op, T& basevalue, Spell* spell /*= nullptr*/) const +{ + float totalmul = 1.0f; + int32 totalflat = 0; + + GetSpellModValues(spellInfo, op, spell, basevalue, &totalflat, &totalmul); + + basevalue = T(double(basevalue + totalflat) * totalmul); +} + +template TC_GAME_API void Player::ApplySpellMod(SpellInfo const* spellInfo, SpellModOp op, int32& basevalue, Spell* spell) const; +template TC_GAME_API void Player::ApplySpellMod(SpellInfo const* spellInfo, SpellModOp op, uint32& basevalue, Spell* spell) const; +template TC_GAME_API void Player::ApplySpellMod(SpellInfo const* spellInfo, SpellModOp op, float& basevalue, Spell* spell) const; +template TC_GAME_API void Player::ApplySpellMod(SpellInfo const* spellInfo, SpellModOp op, double& basevalue, Spell* spell) const; + +void Player::AddSpellMod(SpellModifier* mod, bool apply) +{ + TC_LOG_DEBUG("spells", "Player::AddSpellMod: Player '%s' (%s), SpellID: %d", GetName().c_str(), GetGUID().ToString().c_str(), mod->spellId); + + /// First, manipulate our spellmodifier container + if (apply) + m_spellMods.insert(mod); + else + m_spellMods.erase(mod); + + /// Now, send spellmodifier packet + switch (mod->type) + { + case SPELLMOD_FLAT: + case SPELLMOD_PCT: + if (!IsLoading()) + { + OpcodeServer opcode = (mod->type == SPELLMOD_FLAT) ? SMSG_SET_FLAT_SPELL_MODIFIER : SMSG_SET_PCT_SPELL_MODIFIER; + + WorldPackets::Spells::SetSpellModifier packet(opcode); + + /// @todo Implement sending of bulk modifiers instead of single + packet.Modifiers.resize(1); + WorldPackets::Spells::SpellModifier& spellModifier = packet.Modifiers[0]; + + spellModifier.ModIndex = AsUnderlyingType(mod->op); + + boost::dynamic_bitset mask; + mask.resize(96); + + boost::from_block_range( + &static_cast(mod)->mask[0], + &static_cast(mod)->mask[0] + 3, + mask); + + for (std::size_t classIndex = mask.find_first(); classIndex != decltype(mask)::npos; classIndex = mask.find_next(classIndex)) + { + WorldPackets::Spells::SpellModifierData& modData = spellModifier.ModifierData.emplace_back(); + if (mod->type == SPELLMOD_FLAT) + { + modData.ModifierValue = 0; + auto itr = std::ranges::lower_bound(m_spellMods, std::make_pair(mod->op, SPELLMOD_FLAT), std::ranges::less(), [](SpellModifier const* sm) { return std::make_pair(sm->op, sm->type); }); + while (itr != m_spellMods.end() && (*itr)->op == mod->op && (*itr)->type == SPELLMOD_FLAT) + { + SpellModifierByClassMask const* spellMod = static_cast(*itr++); + if (spellMod->mask[classIndex / 32] & (1u << (classIndex % 32))) + modData.ModifierValue += spellMod->value; + } + } + else + { + modData.ModifierValue = 0; + auto itr = std::ranges::lower_bound(m_spellMods, std::make_pair(mod->op, SPELLMOD_PCT), std::ranges::less(), [](SpellModifier const* sm) { return std::make_pair(sm->op, sm->type); }); + while (itr != m_spellMods.end() && (*itr)->op == mod->op && (*itr)->type == SPELLMOD_PCT) + { + SpellModifierByClassMask const* spellMod = static_cast(*itr++); + if (spellMod->mask[classIndex / 32] & (1u << (classIndex % 32))) + modData.ModifierValue += spellMod->value; + } + } + + modData.ClassIndex = classIndex; + } + + SendDirectMessage(packet.Write()); + } + break; + default: + break; + } +} + +void Player::ApplyModToSpell(SpellModifier* mod, Spell* spell) +{ + if (!spell) + return; + + // don't do anything with no charges + if (mod->ownerAura->IsUsingCharges() && !mod->ownerAura->GetCharges()) + return; + + // register inside spell, proc system uses this to drop charges + spell->m_appliedMods.insert(mod->ownerAura); +} + +void Player::SetSpellModTakingSpell(Spell* spell, bool apply) +{ + if (apply && m_spellModTakingSpell != nullptr) + return; + + if (!apply && (!m_spellModTakingSpell || m_spellModTakingSpell != spell)) + return; + + m_spellModTakingSpell = apply ? spell : nullptr; +} + +void Player::SendSpellModifiers() const +{ + auto getOrCreateModifierData = [](std::vector& datas, uint8 classIndex, float defaultValue) -> float& + { + auto itr = std::ranges::find(datas, classIndex, &WorldPackets::Spells::SpellModifierData::ClassIndex); + if (itr != datas.end()) + return itr->ModifierValue; + + WorldPackets::Spells::SpellModifierData& data = datas.emplace_back(); + data.ModifierValue = defaultValue; + data.ClassIndex = classIndex; + return data.ModifierValue; + }; + + boost::dynamic_bitset mask; + mask.resize(96); + + WorldPackets::Spells::SetSpellModifier flatMods(SMSG_SET_FLAT_SPELL_MODIFIER); + WorldPackets::Spells::SetSpellModifier pctMods(SMSG_SET_PCT_SPELL_MODIFIER); + + WorldPackets::Spells::SpellModifier* flatModifier = nullptr; + WorldPackets::Spells::SpellModifier* pctModifier = nullptr; + + for (SpellModifier const* mod : m_spellMods) + { + if (mod->type != SPELLMOD_FLAT && mod->type != SPELLMOD_PCT) + continue; + + switch (mod->type) + { + case SPELLMOD_FLAT: + if (!flatModifier || flatModifier->ModIndex != uint8(mod->op)) + { + flatModifier = &flatMods.Modifiers.emplace_back(); + flatModifier->ModIndex = uint8(mod->op); + } + boost::from_block_range(&static_cast(mod)->mask[0], &static_cast(mod)->mask[0] + 3, mask); + for (std::size_t classIndex = mask.find_first(); classIndex != decltype(mask)::npos; classIndex = mask.find_next(classIndex)) + { + float& modifierValue = getOrCreateModifierData(flatModifier->ModifierData, classIndex, 0); + modifierValue += static_cast(mod)->value; + } + break; + case SPELLMOD_PCT: + if (!pctModifier || pctModifier->ModIndex != uint8(mod->op)) + { + pctModifier = &pctMods.Modifiers.emplace_back(); + pctModifier->ModIndex = uint8(mod->op); + } + boost::from_block_range(&static_cast(mod)->mask[0], &static_cast(mod)->mask[0] + 3, mask); + for (std::size_t classIndex = mask.find_first(); classIndex != decltype(mask)::npos; classIndex = mask.find_next(classIndex)) + { + float& modifierValue = getOrCreateModifierData(pctModifier->ModifierData, classIndex, 0); + modifierValue += static_cast(mod)->value; + } + break; + default: + break; + } + } + + if (!flatMods.Modifiers.empty()) + SendDirectMessage(flatMods.Write()); + + if (!pctMods.Modifiers.empty()) + SendDirectMessage(pctMods.Write()); +} + +// send Proficiency +void Player::SendProficiency(ItemClass itemClass, uint32 itemSubclassMask) const +{ + WorldPackets::Item::SetProficiency packet; + packet.ProficiencyClass = itemClass; + packet.ProficiencyMask = itemSubclassMask; + SendDirectMessage(packet.Write()); +} + +void Player::RemovePetitionsAndSigns(ObjectGuid guid, CharterTypes type) +{ + sPetitionMgr->RemoveSignaturesBySignerAndType(guid, type); + sPetitionMgr->RemovePetitionsByOwnerAndType(guid, type); +} + +void Player::LeaveAllArenaTeams(ObjectGuid guid) +{ + CharacterCacheEntry const* characterInfo = sCharacterCache->GetCharacterCacheByGuid(guid); + if (!characterInfo) + return; + + for (uint8 i = 0; i < MAX_ARENA_SLOT; ++i) + { + uint32 arenaTeamId = characterInfo->ArenaTeamId[i]; + if (arenaTeamId != 0) + { + ArenaTeam* arenaTeam = sArenaTeamMgr->GetArenaTeamById(arenaTeamId); + if (arenaTeam) + arenaTeam->DelMember(guid, true); + } + } +} + +void Player::SetRestBonus(float rest_bonus_new) +{ + // Prevent resting on max level + if (IsMaxLevel()) + rest_bonus_new = 0; + + if (rest_bonus_new < 0) + rest_bonus_new = 0; + + float rest_bonus_max = (float)GetUInt32Value(PLAYER_NEXT_LEVEL_XP)*1.5f/2; + + if (rest_bonus_new > rest_bonus_max) + m_rest_bonus = rest_bonus_max; + else + m_rest_bonus = rest_bonus_new; + + // update data for client + if ((GetsRecruitAFriendBonus(true) && (GetSession()->IsARecruiter() || GetSession()->GetRecruiterId() != 0))) + SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_REST_STATE, REST_STATE_RAF_LINKED); + else + { + if (m_rest_bonus > 10) + SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_REST_STATE, REST_STATE_RESTED); + else if (m_rest_bonus <= 1) + SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_REST_STATE, REST_STATE_NOT_RAF_LINKED); + } + + //RestTickUpdate + SetUInt32Value(PLAYER_REST_STATE_EXPERIENCE, uint32(m_rest_bonus)); +} + +bool Player::ActivateTaxiPathTo(std::vector const& nodes, Creature* npc /*= nullptr*/, uint32 spellid /*= 0*/) +{ + if (nodes.size() < 2) + return false; + + // not let cheating with start flight in time of logout process || while in combat || has type state: stunned || has type state: root + if (GetSession()->isLogingOut() || IsInCombat() || HasUnitState(UNIT_STATE_STUNNED) || HasUnitState(UNIT_STATE_ROOT)) + { + GetSession()->SendActivateTaxiReply(ERR_TAXIPLAYERBUSY); + return false; + } + + if (HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_REMOVE_CLIENT_CONTROL)) + return false; + + // taximaster case + if (npc) + { + // not let cheating with start flight mounted + if (IsMounted()) + { + GetSession()->SendActivateTaxiReply(ERR_TAXIPLAYERALREADYMOUNTED); + return false; + } + + if (IsInDisallowedMountForm()) + { + GetSession()->SendActivateTaxiReply(ERR_TAXIPLAYERSHAPESHIFTED); + return false; + } + + // not let cheating with start flight in time of logout process || if casting not finished || while in combat || if not use Spell's with EffectSendTaxi + if (IsNonMeleeSpellCast(false)) + { + GetSession()->SendActivateTaxiReply(ERR_TAXIPLAYERBUSY); + return false; + } + } + // cast case or scripted call case + else + { + RemoveAurasByType(SPELL_AURA_MOUNTED); + + if (IsInDisallowedMountForm()) + RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT); + + if (Spell* spell = GetCurrentSpell(CURRENT_GENERIC_SPELL)) + if (spell->m_spellInfo->Id != spellid) + InterruptSpell(CURRENT_GENERIC_SPELL, false); + + InterruptSpell(CURRENT_AUTOREPEAT_SPELL, false); + + if (Spell* spell = GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + if (spell->m_spellInfo->Id != spellid) + InterruptSpell(CURRENT_CHANNELED_SPELL, true); + } + + uint32 sourcenode = nodes[0]; + + // starting node too far away (cheat?) + TaxiNodesEntry const* node = sTaxiNodesStore.LookupEntry(sourcenode); + if (!node) + { + GetSession()->SendActivateTaxiReply(ERR_TAXINOSUCHPATH); + return false; + } + + // Prepare to flight start now + + // stop combat at start taxi flight if any + CombatStop(); + + StopCastingCharm(); + StopCastingBindSight(); + ExitVehicle(); + + // stop trade (client cancel trade at taxi map open but cheating tools can be used for reopen it) + TradeCancel(true); + + // clean not finished taxi path if any + m_taxi.ClearTaxiDestinations(); + + // 0 element current node + m_taxi.AddTaxiDestination(sourcenode); + + // fill destinations path tail + uint32 sourcepath = 0; + uint32 totalcost = 0; + uint32 firstcost = 0; + + uint32 prevnode = sourcenode; + uint32 lastnode; + + for (uint32 i = 1; i < nodes.size(); ++i) + { + uint32 path, cost; + + lastnode = nodes[i]; + sObjectMgr->GetTaxiPath(prevnode, lastnode, path, cost); + + if (!path) + { + m_taxi.ClearTaxiDestinations(); + return false; + } + + totalcost += cost; + if (i == 1) + firstcost = cost; + + if (prevnode == sourcenode) + sourcepath = path; + + m_taxi.AddTaxiDestination(lastnode); + + prevnode = lastnode; + } + + // get mount model (in case non taximaster (npc == nullptr) allow more wide lookup) + // + // Hack-Fix for Alliance not being able to use Acherus taxi. There is + // only one mount ID for both sides. Probably not good to use 315 in case DBC nodes + // change but I couldn't find a suitable alternative. OK to use class because only DK + // can use this taxi. + uint32 mount_display_id = sObjectMgr->GetTaxiMountDisplayId(sourcenode, GetTeam(), npc == nullptr || (sourcenode == 315 && getClass() == CLASS_DEATH_KNIGHT)); + + // in spell case allow 0 model + if ((mount_display_id == 0 && spellid == 0) || sourcepath == 0) + { + GetSession()->SendActivateTaxiReply(ERR_TAXIUNSPECIFIEDSERVERERROR); + m_taxi.ClearTaxiDestinations(); + return false; + } + + uint64 money = GetMoney(); + + if (npc) + { + float discount = GetReputationPriceDiscount(npc); + totalcost = uint32(ceil(totalcost * discount)); + firstcost = uint32(round(firstcost * discount)); + } + + if (money < totalcost) + { + GetSession()->SendActivateTaxiReply(ERR_TAXINOTENOUGHMONEY); + m_taxi.ClearTaxiDestinations(); + return false; + } + + //Checks and preparations done, DO FLIGHT + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_FLIGHT_PATHS_TAKEN, 1); + + // prevent stealth flight + //RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Interacting); + + if (sWorld->getBoolConfig(CONFIG_INSTANT_TAXI)) + { + TaxiNodesEntry const* lastPathNode = sTaxiNodesStore.LookupEntry(nodes[nodes.size()-1]); + ASSERT(lastPathNode); + m_taxi.ClearTaxiDestinations(); + ModifyMoney(-(int64)totalcost); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_GOLD_SPENT_FOR_TRAVELLING, totalcost); + TeleportTo(lastPathNode->ContinentID, lastPathNode->Pos.X, lastPathNode->Pos.Y, lastPathNode->Pos.Z, GetOrientation()); + return false; + } + else + { + ModifyMoney(-(int64)firstcost); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_GOLD_SPENT_FOR_TRAVELLING, firstcost); + GetSession()->SendActivateTaxiReply(ERR_TAXIOK); + GetSession()->SendDoFlight(mount_display_id, sourcepath); + } + return true; +} + +bool Player::ActivateTaxiPathTo(uint32 taxi_path_id, uint32 spellid /*= 0*/) +{ + TaxiPathEntry const* entry = sTaxiPathStore.LookupEntry(taxi_path_id); + if (!entry) + return false; + + std::vector nodes; + + nodes.resize(2); + nodes[0] = entry->FromTaxiNode; + nodes[1] = entry->ToTaxiNode; + + return ActivateTaxiPathTo(nodes, nullptr, spellid); +} + +void Player::CleanupAfterTaxiFlight() +{ + m_taxi.ClearTaxiDestinations(); // not destinations, clear source node + Dismount(); + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_REMOVE_CLIENT_CONTROL | UNIT_FLAG_ON_TAXI); +} + +void Player::ContinueTaxiFlight() const +{ + uint32 sourceNode = m_taxi.GetTaxiSource(); + if (!sourceNode) + return; + + TC_LOG_DEBUG("entities.unit", "Player::ContinueTaxiFlight: Restart %s taxi flight", GetGUID().ToString().c_str()); + + uint32 mountDisplayId = sObjectMgr->GetTaxiMountDisplayId(sourceNode, GetTeam(), true); + if (!mountDisplayId) + return; + + uint32 path = m_taxi.GetCurrentTaxiPath(); + + // search appropriate start path node + uint32 startNode = 0; + + TaxiPathNodeList const& nodeList = sTaxiPathNodesByPath[path]; + + float distPrev; + float distNext = GetExactDistSq(nodeList[0]->Loc.X, nodeList[0]->Loc.Y, nodeList[0]->Loc.Z); + + for (uint32 i = 1; i < nodeList.size(); ++i) + { + TaxiPathNodeEntry const* node = nodeList[i]; + TaxiPathNodeEntry const* prevNode = nodeList[i-1]; + + // skip nodes at another map + if (node->ContinentID != GetMapId()) + continue; + + distPrev = distNext; + + distNext = GetExactDistSq(node->Loc.X, node->Loc.Y, node->Loc.Z); + + float distNodes = + (node->Loc.X - prevNode->Loc.X)*(node->Loc.X - prevNode->Loc.X) + + (node->Loc.Y - prevNode->Loc.Y)*(node->Loc.Y - prevNode->Loc.Y) + + (node->Loc.Z - prevNode->Loc.Z)*(node->Loc.Z - prevNode->Loc.Z); + + if (distNext + distPrev < distNodes) + { + startNode = i; + break; + } + } + + GetSession()->SendDoFlight(mountDisplayId, path, startNode); +} + +void Player::SendTaxiNodeStatusMultiple() +{ + for (auto itr = m_clientGUIDs.begin(); itr != m_clientGUIDs.end(); ++itr) + { + if (!itr->IsCreature()) + continue; + Creature* creature = ObjectAccessor::GetCreature(*this, *itr); + if (!creature || creature->IsHostileTo(this)) + continue; + if (!creature->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_FLIGHTMASTER)) + continue; + uint32 nearestNode = sObjectMgr->GetNearestTaxiNode(creature->GetPositionX(), creature->GetPositionY(), creature->GetPositionZ(), creature->GetMapId(), GetTeam()); + if (!nearestNode) + continue; + WorldPacket data(SMSG_TAXINODE_STATUS, 9); + data << *itr; + data << uint8(m_taxi.IsTaximaskNodeKnown(nearestNode) ? 1 : 2); + SendDirectMessage(&data); + } +} + +void Player::InitDataForForm(bool reapplyMods) +{ + ShapeshiftForm form = GetShapeshiftForm(); + + SpellShapeshiftFormEntry const* ssEntry = sSpellShapeshiftFormStore.LookupEntry(form); + if (ssEntry && ssEntry->CombatRoundTime) + { + SetBaseAttackTime(BASE_ATTACK, ssEntry->CombatRoundTime); + SetBaseAttackTime(OFF_ATTACK, ssEntry->CombatRoundTime); + SetBaseAttackTime(RANGED_ATTACK, BASE_ATTACK_TIME); + } + else + SetRegularAttackTime(); + + UpdateDisplayPower(); + + // update auras at form change, ignore this at mods reapply (.reset stats/etc) when form not change. + if (!reapplyMods) + UpdateEquipSpellsAtFormChange(); + + UpdateAttackPowerAndDamage(); + UpdateAttackPowerAndDamage(true); +} + +void Player::InitDisplayIds() +{ + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(), getClass()); + if (!info) + { + TC_LOG_ERROR("entities.player", "Player::InitDisplayIds: Player '%s' (%s) has incorrect race/class pair. Can't init display ids.", GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + uint8 gender = GetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_GENDER); + switch (gender) + { + case GENDER_FEMALE: + SetDisplayId(info->displayId_f, true); + break; + case GENDER_MALE: + SetDisplayId(info->displayId_m, true); + break; + default: + TC_LOG_ERROR("entities.player", "Player::InitDisplayIds: Player '%s' (%s) has invalid gender %u", GetName().c_str(), GetGUID().ToString().c_str(), gender); + } +} + +inline bool Player::_StoreOrEquipNewItem(uint32 vendorslot, uint32 item, uint32 count, uint8 bag, uint8 slot, int64 price, ItemTemplate const* pProto, Creature* pVendor, VendorItem const* crItem, bool bStore) +{ + uint32 stacks = count / pProto->GetBuyCount(); + ItemPosCountVec vDest; + uint16 uiDest = 0; + InventoryResult msg = bStore ? + CanStoreNewItem(bag, slot, vDest, item, count) : + CanEquipNewItem(slot, uiDest, item, false); + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, nullptr, nullptr, item); + return false; + } + + ModifyMoney(-price); + + if (crItem->ExtendedCost) // case for new honor system + { + ItemExtendedCostEntry const* iece = sItemExtendedCostStore.LookupEntry(crItem->ExtendedCost); + ASSERT(iece); + for (int i = 0; i < MAX_ITEM_EXT_COST_CURRENCIES; ++i) + { + if (iece->RequiredItem[i]) + DestroyItemCount(iece->RequiredItem[i], iece->RequiredItemCount[i] * stacks, true); + } + + for (int i = 0; i < MAX_ITEM_EXT_COST_CURRENCIES; ++i) + { + if (iece->RequirementFlags & (ITEM_EXT_COST_CURRENCY_REQ_IS_SEASON_EARNED_1 << i)) + continue; + + if (iece->RequiredCurrency[i]) + ModifyCurrency(iece->RequiredCurrency[i], -int32(iece->RequiredCurrencyCount[i] * stacks), true, true); + } + } + + Item* it = bStore ? + StoreNewItem(vDest, item, true, GenerateItemRandomPropertyId(item)) : + EquipNewItem(uiDest, item, true); + if (it) + { + uint32 new_count = pVendor->UpdateVendorItemCurrentCount(crItem, count); + + WorldPacket data(SMSG_BUY_ITEM, (8+4+4+4)); + data << uint64(pVendor->GetGUID()); + data << uint32(vendorslot + 1); // numbered from 1 at client + data << int32(crItem->maxcount > 0 ? new_count : 0xFFFFFFFF); + data << uint32(count); + SendDirectMessage(&data); + SendNewItem(it, count, true, false, false); + + if (!bStore) + AutoUnequipOffhandIfNeed(); + + if (pProto->GetFlags() & ITEM_FLAG_ITEM_PURCHASE_RECORD && crItem->ExtendedCost && pProto->GetMaxStackSize() == 1) + { + it->SetFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_REFUNDABLE); + it->SetRefundRecipient(GetGUID().GetCounter()); + it->SetPaidMoney(uint32(price)); + it->SetPaidExtendedCost(crItem->ExtendedCost); + it->SaveRefundDataToDB(); + AddRefundReference(it->GetGUID()); + } + } + return true; +} + +bool Player::BuyCurrencyFromVendorSlot(ObjectGuid vendorGuid, uint32 vendorSlot, uint32 currency, uint32 count) +{ + // cheating attempt + if (count < 1) count = 1; + + if (!IsAlive()) + return false; + + CurrencyTypesEntry const* proto = sCurrencyTypesStore.LookupEntry(currency); + if (!proto) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, nullptr, currency, 0); + return false; + } + + Creature* creature = GetNPCIfCanInteractWith(vendorGuid, UNIT_NPC_FLAG_VENDOR); + if (!creature) + { + TC_LOG_DEBUG("network", "WORLD: BuyCurrencyFromVendorSlot - %s not found or you can't interact with him.", vendorGuid.ToString().c_str()); + SendBuyError(BUY_ERR_DISTANCE_TOO_FAR, nullptr, currency, 0); + return false; + } + + VendorItemData const* vItems = creature->GetVendorItems(); + if (!vItems || vItems->Empty()) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, currency, 0); + return false; + } + + if (vendorSlot >= vItems->GetItemCount()) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, currency, 0); + return false; + } + + VendorItem const* crItem = vItems->GetItem(vendorSlot); + // store diff item (cheating) + if (!crItem || crItem->item != currency || crItem->Type != ITEM_VENDOR_TYPE_CURRENCY) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, currency, 0); + return false; + } + + if (count % crItem->maxcount) + { + SendEquipError(EQUIP_ERR_CANT_BUY_QUANTITY, nullptr, nullptr); + return false; + } + + uint32 stacks = count / crItem->maxcount; + ItemExtendedCostEntry const* iece = nullptr; + if (crItem->ExtendedCost) + { + iece = sItemExtendedCostStore.LookupEntry(crItem->ExtendedCost); + if (!iece) + { + TC_LOG_ERROR("entities.player", "Currency %u have wrong ExtendedCost field value %u", currency, crItem->ExtendedCost); + return false; + } + + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_ITEMS; ++i) + { + if (iece->RequiredItem[i] && !HasItemCount(iece->RequiredItem[i], (iece->RequiredItemCount[i] * stacks))) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); + return false; + } + } + + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_CURRENCIES; ++i) + { + if (!iece->RequiredCurrency[i]) + continue; + + CurrencyTypesEntry const* entry = sCurrencyTypesStore.LookupEntry(iece->RequiredCurrency[i]); + if (!entry) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, currency, 0); // Find correct error + return false; + } + + if (iece->RequirementFlags & (ITEM_EXT_COST_CURRENCY_REQ_IS_SEASON_EARNED_1 << i)) + { + // Not implemented + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); // Find correct error + return false; + } + else if (!HasCurrency(iece->RequiredCurrency[i], (iece->RequiredCurrencyCount[i] * stacks))) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); // Find correct error + return false; + } + } + + // check for personal arena rating requirement + if (GetMaxPersonalArenaRatingRequirement(iece->RequiredArenaSlot) < iece->RequiredPersonalArenaRating) + { + // probably not the proper equip err + SendEquipError(EQUIP_ERR_CANT_EQUIP_RANK, nullptr, nullptr); + return false; + } + + if (iece->RequiredFactionId && uint32(GetReputationRank(iece->RequiredFactionId)) < iece->RequiredFactionStanding) + { + SendBuyError(BUY_ERR_REPUTATION_REQUIRE, creature, currency, 0); + return false; + } + + if (iece->RequirementFlags & ITEM_EXT_COST_FLAG_REQUIRE_GUILD && !GetGuildId()) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); // Find correct error + return false; + } + + if (iece->RequiredGuildLevel && iece->RequiredGuildLevel < GetGuildLevel()) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); // Find correct error + return false; + } + + if (iece->RequiredAchievement && !HasAchieved(iece->RequiredAchievement)) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); // Find correct error + return false; + } + } + else // currencies have no price defined, can only be bought with ExtendedCost + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, nullptr, currency, 0); + return false; + } + + ModifyCurrency(currency, count, true, true); + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_ITEMS; ++i) + { + if (!iece->RequiredItem[i]) + continue; + + DestroyItemCount(iece->RequiredItem[i], iece->RequiredItemCount[i] * stacks, true); + } + + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_CURRENCIES; ++i) + { + if (!iece->RequiredCurrency[i]) + continue; + + if (iece->RequirementFlags & (ITEM_EXT_COST_CURRENCY_REQ_IS_SEASON_EARNED_1 << i)) + continue; + + ModifyCurrency(iece->RequiredCurrency[i], -int32(iece->RequiredCurrencyCount[i]) * stacks, true, true); + } + + return true; +} + +// Return true is the bought item has a max count to force refresh of window by caller +bool Player::BuyItemFromVendorSlot(ObjectGuid vendorguid, uint32 vendorslot, uint32 item, uint32 count, uint8 bag, uint8 slot) +{ + // cheating attempt + if (count < 1) count = 1; + + // cheating attempt + if (slot > MAX_BAG_SIZE && slot != NULL_SLOT) + return false; + + if (!IsAlive()) + return false; + + ItemTemplate const* pProto = sObjectMgr->GetItemTemplate(item); + if (!pProto) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, nullptr, item, 0); + return false; + } + + if (!(pProto->GetAllowableClass() & getClassMask()) && pProto->GetBonding() == BIND_ON_ACQUIRE && !IsGameMaster()) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, nullptr, item, 0); + return false; + } + + if (!IsGameMaster() && ((pProto->GetFlags2() & ITEM_FLAG2_FACTION_HORDE && GetTeam() == ALLIANCE) || (pProto->GetFlags2() == ITEM_FLAG2_FACTION_ALLIANCE && GetTeam() == HORDE))) + return false; + + Creature* creature = GetNPCIfCanInteractWith(vendorguid, UNIT_NPC_FLAG_VENDOR); + if (!creature) + { + TC_LOG_DEBUG("network", "Player::BuyItemFromVendorSlot: Vendor (%s) not found or player '%s' (%s) can't interact with him.", + vendorguid.ToString().c_str(), GetName().c_str(), GetGUID().ToString().c_str()); + SendBuyError(BUY_ERR_DISTANCE_TOO_FAR, nullptr, item, 0); + return false; + } + + if (!sConditionMgr->IsObjectMeetingVendorItemConditions(creature->GetEntry(), item, this, creature)) + { + TC_LOG_DEBUG("condition", "Player::BuyItemFromVendorSlot: Player '%s' (%s) doesn't meed conditions for creature (Entry: %u, Item: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), creature->GetEntry(), item); + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, item, 0); + return false; + } + + VendorItemData const* vItems = creature->GetVendorItems(); + if (!vItems || vItems->Empty()) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, item, 0); + return false; + } + + if (vendorslot >= vItems->GetItemCount()) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, item, 0); + return false; + } + + VendorItem const* crItem = vItems->GetItem(vendorslot); + // store diff item (cheating) + if (!crItem || crItem->item != item) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, item, 0); + return false; + } + + if (PlayerConditionEntry const* playerCondition = sPlayerConditionStore.LookupEntry(crItem->PlayerConditionId)) + { + if (!ConditionMgr::IsPlayerMeetingCondition(this, playerCondition)) + { + SendEquipError(EQUIP_ERR_ITEM_LOCKED, nullptr, nullptr); + return false; + } + } + + // check current item amount if it limited + if (crItem->maxcount != 0) + { + if (creature->GetVendorItemCurrentCount(crItem) < count) + { + SendBuyError(BUY_ERR_ITEM_ALREADY_SOLD, creature, item, 0); + return false; + } + } + + if (pProto->GetRequiredReputationFaction() && (uint32(GetReputationRank(pProto->GetRequiredReputationFaction())) < pProto->GetRequiredReputationRank())) + { + SendBuyError(BUY_ERR_REPUTATION_REQUIRE, creature, item, 0); + return false; + } + + if (crItem->ExtendedCost) + { + // Can only buy full stacks for extended cost + if (count % pProto->GetBuyCount()) + { + SendEquipError(EQUIP_ERR_CANT_BUY_QUANTITY, nullptr, nullptr); + return false; + } + + uint32 stacks = count / pProto->GetBuyCount(); + ItemExtendedCostEntry const* iece = sItemExtendedCostStore.LookupEntry(crItem->ExtendedCost); + if (!iece) + { + TC_LOG_ERROR("entities.player", "Player::BuyItemFromVendorSlot: Item %u has wrong ExtendedCost field value %u", pProto->GetId(), crItem->ExtendedCost); + return false; + } + + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_ITEMS; ++i) + { + if (iece->RequiredItem[i] && !HasItemCount(iece->RequiredItem[i], iece->RequiredItemCount[i] * stacks)) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); + return false; + } + } + + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_CURRENCIES; ++i) + { + if (!iece->RequiredCurrency[i]) + continue; + + CurrencyTypesEntry const* entry = sCurrencyTypesStore.LookupEntry(iece->RequiredCurrency[i]); + if (!entry) + { + SendBuyError(BUY_ERR_CANT_FIND_ITEM, creature, item, 0); + return false; + } + + if (iece->RequirementFlags & (ITEM_EXT_COST_CURRENCY_REQ_IS_SEASON_EARNED_1 << i)) + { + if (GetCurrencyOnSeason(entry->ID) < iece->RequiredCurrencyCount[0]) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, NULL, NULL); + return false; + } + + if (!HasCurrency(iece->RequiredCurrency[1], iece->RequiredCurrencyCount[1] * stacks)) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, NULL, NULL); + return false; + } + } + else if (!HasCurrency(iece->RequiredCurrency[i], iece->RequiredCurrencyCount[i] * stacks)) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); + return false; + } + } + + // check for personal arena rating requirement + if (GetMaxPersonalArenaRatingRequirement(iece->RequiredArenaSlot) < iece->RequiredPersonalArenaRating) + { + // probably not the proper equip err + SendEquipError(EQUIP_ERR_CANT_EQUIP_RANK, nullptr, nullptr); + return false; + } + + if (iece->RequiredFactionId && uint32(GetReputationRank(iece->RequiredFactionId)) < iece->RequiredFactionStanding) + { + SendBuyError(BUY_ERR_REPUTATION_REQUIRE, creature, item, 0); + return false; + } + + if (iece->RequirementFlags & ITEM_EXT_COST_FLAG_REQUIRE_GUILD && !GetGuildId()) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); // Find correct error + return false; + } + + if (iece->RequiredGuildLevel && iece->RequiredGuildLevel < GetGuildLevel()) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); // Find correct error + return false; + } + + if (iece->RequiredAchievement && !HasAchieved(iece->RequiredAchievement)) + { + SendEquipError(EQUIP_ERR_VENDOR_MISSING_TURNINS, nullptr, nullptr); // Find correct error + return false; + } + } + + uint64 price = 0; + if (crItem->IsGoldRequired(pProto) && pProto->GetBuyPrice() > 0) //Assume price cannot be negative (do not know why it is int32) + { + double buyPricePerItem = double(pProto->GetBuyPrice()) / pProto->GetBuyCount(); + uint32 maxCount = MAX_MONEY_AMOUNT / buyPricePerItem; + if (count > maxCount) + { + TC_LOG_INFO("entities.player.cheat", "Player::BuyItemFromVendorSlot: Player '%s' (%s) tried to buy item (ItemID: %u, Count: %u), causing overflow", + GetName().c_str(), GetGUID().ToString().c_str(), pProto->GetId(), count); + count = maxCount; + } + price = uint64(buyPricePerItem * count); //it should not exceed MAX_MONEY_AMOUNT + + // reputation discount + price = uint64(floor(price * GetReputationPriceDiscount(creature))); + + if (int32 priceMod = GetTotalAuraModifier(SPELL_AURA_MOD_VENDOR_ITEMS_PRICES)) + price -= CalculatePct(price, priceMod); + + if (!HasEnoughMoney(price)) + { + SendBuyError(BUY_ERR_NOT_ENOUGHT_MONEY, creature, item, 0); + return false; + } + } + + if ((bag == NULL_BAG && slot == NULL_SLOT) || IsInventoryPos(bag, slot)) + { + if (!_StoreOrEquipNewItem(vendorslot, item, count, bag, slot, price, pProto, creature, crItem, true)) + return false; + } + else if (IsEquipmentPos(bag, slot)) + { + if (count != 1) + { + SendEquipError(EQUIP_ERR_NOT_EQUIPPABLE, nullptr, nullptr); + return false; + } + if (!_StoreOrEquipNewItem(vendorslot, item, count, bag, slot, price, pProto, creature, crItem, false)) + return false; + } + else + { + SendEquipError(EQUIP_ERR_WRONG_SLOT, nullptr, nullptr); + return false; + } + + if (crItem->maxcount != 0) // bought + { + if (pProto->GetQuality() > ITEM_QUALITY_EPIC || (pProto->GetQuality() == ITEM_QUALITY_EPIC && pProto->GetBaseItemLevel() >= MinNewsItemLevel[sWorld->getIntConfig(CONFIG_EXPANSION)])) + if (Guild* guild = GetGuild()) + guild->AddGuildNews(GUILD_NEWS_ITEM_PURCHASED, GetGUID(), 0, item); + return true; + } + + return false; +} + +uint32 Player::GetMaxPersonalArenaRatingRequirement(uint32 minarenaslot) const +{ + // returns the maximal personal arena rating that can be used to purchase items requiring this condition + // the personal rating of the arena team must match the required limit as well + // so return max[in arenateams](min(personalrating[teamtype], teamrating[teamtype])) + uint32 max_personal_rating = 0; + for (uint8 i = minarenaslot; i < MAX_ARENA_SLOT; ++i) + { + if (ArenaTeam* at = sArenaTeamMgr->GetArenaTeamById(GetArenaTeamId(i))) + { + uint32 p_rating = GetArenaPersonalRating(i); + uint32 t_rating = at->GetRating(); + p_rating = p_rating < t_rating ? p_rating : t_rating; + if (max_personal_rating < p_rating) + max_personal_rating = p_rating; + } + } + return max_personal_rating; +} + +void Player::UpdateHomebindTime(uint32 time) +{ + // GMs never get homebind timer online + if (m_InstanceValid || IsGameMaster()) + { + if (m_HomebindTimer) // instance valid, but timer not reset + SendRaidGroupOnlyMessage(RAID_GROUP_ERR_NONE, 0); + + // instance is valid, reset homebind timer + m_HomebindTimer = 0; + } + else if (m_HomebindTimer > 0) + { + if (time >= m_HomebindTimer) + { + // teleport to nearest graveyard + RepopAtGraveyard(); + } + else + m_HomebindTimer -= time; + } + else + { + // instance is invalid, start homebind timer + m_HomebindTimer = 60000; + // send message to player + SendRaidGroupOnlyMessage(RAID_GROUP_ERR_REQUIREMENTS_UNMATCH, m_HomebindTimer); + TC_LOG_DEBUG("maps", "Player::UpdateHomebindTime: Player '%s' (%s) will be teleported to homebind in 60 seconds", + GetName().c_str(), GetGUID().ToString().c_str()); + } +} + +void Player::UpdatePvPState(bool onlyFFA) +{ + /// @todo should we always synchronize UNIT_FIELD_BYTES_2, 1 of controller and controlled? + // no, we shouldn't, those are checked for affecting player by client + if (!pvpInfo.IsInNoPvPArea && !IsGameMaster() + && (pvpInfo.IsInFFAPvPArea || sWorld->IsFFAPvPRealm())) + { + if (!IsFFAPvP()) + { + SetByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_FFA_PVP); + for (ControlList::iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) + (*itr)->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_FFA_PVP); + } + } + else if (IsFFAPvP()) + { + RemoveByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_FFA_PVP); + for (ControlList::iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) + (*itr)->RemoveByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_FFA_PVP); + } + + if (onlyFFA) + return; + + if (pvpInfo.IsHostile) // in hostile area + { + if (!IsPvP() || pvpInfo.EndTimer) + UpdatePvP(true, true); + } + else // in friendly area + { + if (IsPvP() && !HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_IN_PVP) && !pvpInfo.EndTimer) + pvpInfo.EndTimer = GameTime::GetGameTime(); // start toggle-off + } +} + +void Player::SetPvP(bool state) +{ + Unit::SetPvP(state); + for (ControlList::iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) + (*itr)->SetPvP(state); +} + +void Player::UpdatePvP(bool state, bool _override) +{ + if (!state || _override) + { + SetPvP(state); + pvpInfo.EndTimer = 0; + } + else + { + pvpInfo.EndTimer = GameTime::GetGameTime(); + SetPvP(state); + } +} + +void Player::UpdatePotionCooldown(Spell* spell) +{ + // no potion used i combat or still in combat + if (!m_lastPotionId || IsInCombat()) + return; + + // Call not from spell cast, send cooldown event for item spells if no in combat + if (!spell) + { + // spell/item pair let set proper cooldown (except non-existing charged spell cooldown spellmods for potions) + if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(m_lastPotionId)) + for (ItemEffect const& effect : proto->Effects) + if (effect.SpellID && effect.Trigger== ITEM_SPELLTRIGGER_ON_USE) + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect.SpellID)) + GetSpellHistory()->SendCooldownEvent(spellInfo, m_lastPotionId); + } + // from spell cases (m_lastPotionId set in Spell::SendSpellCooldown) + else + { + if (spell->IsIgnoringCooldowns()) + return; + else + GetSpellHistory()->SendCooldownEvent(spell->m_spellInfo, m_lastPotionId, spell); + } + + m_lastPotionId = 0; +} + +void Player::SetResurrectRequestData(WorldObject const* caster, uint32 health, uint32 mana, uint32 appliedAura) +{ + ASSERT(!IsResurrectRequested()); + _resurrectionData = std::make_unique(); + _resurrectionData->GUID = caster->GetGUID(); + _resurrectionData->Location.WorldRelocate(*caster); + _resurrectionData->Health = health; + _resurrectionData->Mana = mana; + _resurrectionData->Aura = appliedAura; +} + //slot to be excluded while counting +bool Player::EnchantmentFitsRequirements(uint32 enchantmentcondition, int8 slot) const +{ + if (!enchantmentcondition) + return true; + + SpellItemEnchantmentConditionEntry const* Condition = sSpellItemEnchantmentConditionStore.LookupEntry(enchantmentcondition); + + if (!Condition) + return true; + + uint8 curcount[4] = {0, 0, 0, 0}; + + //counting current equipped gem colors + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) + { + if (i == slot) + continue; + Item* pItem2 = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (pItem2 && !pItem2->IsBroken() && pItem2->GetTemplate()->GetSocketColor(0)) + { + for (uint32 enchant_slot = SOCK_ENCHANTMENT_SLOT; enchant_slot < SOCK_ENCHANTMENT_SLOT+3; ++enchant_slot) + { + uint32 enchant_id = pItem2->GetEnchantmentId(EnchantmentSlot(enchant_slot)); + if (!enchant_id) + continue; + + SpellItemEnchantmentEntry const* enchantEntry = sSpellItemEnchantmentStore.LookupEntry(enchant_id); + if (!enchantEntry) + continue; + + uint32 gemid = enchantEntry->Src_itemID; + if (!gemid) + continue; + + ItemTemplate const* gemProto = sObjectMgr->GetItemTemplate(gemid); + if (!gemProto) + continue; + + GemPropertiesEntry const* gemProperty = sGemPropertiesStore.LookupEntry(gemProto->GetGemProperties()); + if (!gemProperty) + continue; + + uint8 GemColor = gemProperty->Type; + + for (uint8 b = 0, tmpcolormask = 1; b < 4; b++, tmpcolormask <<= 1) + { + if (tmpcolormask & GemColor) + ++curcount[b]; + } + } + } + } + + bool activate = true; + + for (uint8 i = 0; i < 3; i++) + { + if (!Condition->Color[i]) + continue; + + uint32 _cur_gem = curcount[Condition->Color[i] - 1]; + + // if have use them as count, else use from Condition + uint32 _cmp_gem = Condition->CompareColor[i] ? curcount[Condition->CompareColor[i] - 1]: Condition->Value[i]; + + switch (Condition->Comparator[i]) + { + case 2: // requires less than ( || ) gems + activate &= (_cur_gem < _cmp_gem) ? true : false; + break; + case 3: // requires more than ( || ) gems + activate &= (_cur_gem > _cmp_gem) ? true : false; + break; + case 5: // requires at least than ( || ) gems + activate &= (_cur_gem >= _cmp_gem) ? true : false; + break; + } + } + + TC_LOG_DEBUG("entities.player.items", "Player::EnchantmentFitsRequirements: Checking Condition %u, there are %u Meta Gems, %u Red Gems, %u Yellow Gems and %u Blue Gems, Activate:%s", + enchantmentcondition, curcount[0], curcount[1], curcount[2], curcount[3], activate ? "yes" : "no"); + + return activate; +} + +void Player::CorrectMetaGemEnchants(uint8 exceptslot, bool apply) +{ + //cycle all equipped items + for (uint32 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + { + //enchants for the slot being socketed are handled by Player::ApplyItemMods + if (slot == exceptslot) + continue; + + Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + + if (!pItem || !pItem->GetTemplate()->GetSocketColor(0)) + continue; + + for (uint32 enchant_slot = SOCK_ENCHANTMENT_SLOT; enchant_slot < SOCK_ENCHANTMENT_SLOT+3; ++enchant_slot) + { + uint32 enchant_id = pItem->GetEnchantmentId(EnchantmentSlot(enchant_slot)); + if (!enchant_id) + continue; + + SpellItemEnchantmentEntry const* enchantEntry = sSpellItemEnchantmentStore.LookupEntry(enchant_id); + if (!enchantEntry) + continue; + + uint32 condition = enchantEntry->Condition_ID; + if (condition) + { + //was enchant active with/without item? + bool wasactive = EnchantmentFitsRequirements(condition, apply ? exceptslot : -1); + //should it now be? + if (wasactive ^ EnchantmentFitsRequirements(condition, apply ? -1 : exceptslot)) + { + // ignore item gem conditions + //if state changed, (dis)apply enchant + ApplyEnchantment(pItem, EnchantmentSlot(enchant_slot), !wasactive, true, true); + } + } + } + } +} + + //if false -> then toggled off if was on| if true -> toggled on if was off AND meets requirements +void Player::ToggleMetaGemsActive(uint8 exceptslot, bool apply) +{ + //cycle all equipped items + for (int slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + { + //enchants for the slot being socketed are handled by WorldSession::HandleSocketOpcode(WorldPacket& recvData) + if (slot == exceptslot) + continue; + + Item* pItem = GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + + if (!pItem || !pItem->GetTemplate()->GetSocketColor(0)) //if item has no sockets or no item is equipped go to next item + continue; + + //cycle all (gem)enchants + for (uint32 enchant_slot = SOCK_ENCHANTMENT_SLOT; enchant_slot < SOCK_ENCHANTMENT_SLOT+3; ++enchant_slot) + { + uint32 enchant_id = pItem->GetEnchantmentId(EnchantmentSlot(enchant_slot)); + if (!enchant_id) //if no enchant go to next enchant(slot) + continue; + + SpellItemEnchantmentEntry const* enchantEntry = sSpellItemEnchantmentStore.LookupEntry(enchant_id); + if (!enchantEntry) + continue; + + //only metagems to be (de)activated, so only enchants with condition + uint32 condition = enchantEntry->Condition_ID; + if (condition) + ApplyEnchantment(pItem, EnchantmentSlot(enchant_slot), apply); + } + } +} + +void Player::SetBattlegroundEntryPoint() +{ + // Taxi path store + if (!m_taxi.empty()) + { + m_bgData.mountSpell = 0; + m_bgData.taxiPath[0] = m_taxi.GetTaxiSource(); + m_bgData.taxiPath[1] = m_taxi.GetTaxiDestination(); + + // On taxi we don't need check for dungeon + m_bgData.joinPos = WorldLocation(GetMapId(), GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation()); + } + else + { + m_bgData.ClearTaxiPath(); + + // Mount spell id storing + if (IsMounted()) + { + AuraEffectList const& auras = GetAuraEffectsByType(SPELL_AURA_MOUNTED); + if (!auras.empty()) + m_bgData.mountSpell = (*auras.begin())->GetId(); + } + else + m_bgData.mountSpell = 0; + + // If map is dungeon find linked graveyard + if (GetMap()->IsDungeon()) + { + if (const WorldSafeLocsEntry* entry = sObjectMgr->GetClosestGraveyard(*this, GetTeam(), this)) + m_bgData.joinPos = WorldLocation(entry->Continent, entry->Loc.X, entry->Loc.Y, entry->Loc.Z, 0.0f); + else + TC_LOG_ERROR("entities.player", "Player::SetBattlegroundEntryPoint: Dungeon (MapID: %u) has no linked graveyard, setting home location as entry point.", GetMapId()); + } + // If new entry point is not BG or arena set it + else if (!GetMap()->IsBattlegroundOrArena()) + m_bgData.joinPos = WorldLocation(GetMapId(), GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation()); + } + + if (m_bgData.joinPos.m_mapId == MAPID_INVALID) // In error cases use homebind position + m_bgData.joinPos = WorldLocation(m_homebindMapId, m_homebindX, m_homebindY, m_homebindZ, 0.0f); +} + +void Player::GetLFGLeavePoint(Position* pos) +{ + if (pos) + { + pos->Relocate(m_bgData.leavePos.m_positionX, m_bgData.leavePos.m_positionY, + m_bgData.leavePos.m_positionZ, m_bgData.leavePos.GetOrientation()); + _hasValidLFGLeavePoint = false; + } +} + +bool Player::HasValidLFGLeavePoint(uint32 mapid) +{ + return _hasValidLFGLeavePoint && MapManager::IsValidMapCoord(mapid, m_bgData.leavePos.m_positionX, m_bgData.leavePos.m_positionY, + m_bgData.leavePos.m_positionZ, m_bgData.leavePos.GetOrientation()); +} + +void Player::SetLFGLeavePoint() +{ + m_bgData.leavePos = WorldLocation(GetMapId(), GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation()); + _hasValidLFGLeavePoint = true; +} + +void Player::SetBGTeam(uint32 team) +{ + m_bgData.bgTeam = team; + SetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_ARENA_FACTION, uint8(team == ALLIANCE ? 1 : 0)); +} + +uint32 Player::GetBGTeam() const +{ + return m_bgData.bgTeam ? m_bgData.bgTeam : GetTeam(); +} + +void Player::LeaveBattleground(bool teleportToEntryPoint) +{ + if (Battleground* bg = GetBattleground()) + { + bg->RemovePlayerAtLeave(GetGUID(), teleportToEntryPoint, true); + + // call after remove to be sure that player resurrected for correct cast + if (bg->isBattleground() && !IsGameMaster() && sWorld->getBoolConfig(CONFIG_BATTLEGROUND_CAST_DESERTER)) + { + if (bg->GetStatus() == STATUS_IN_PROGRESS || bg->GetStatus() == STATUS_WAIT_JOIN) + { + //lets check if player was teleported from BG and schedule delayed Deserter spell cast + if (IsBeingTeleportedFar()) + { + ScheduleDelayedOperation(DELAYED_SPELL_CAST_DESERTER); + return; + } + + CastSpell(this, 26013, true); // Deserter + } + } + + // track if player leaves the BG while inside it + if (bg->isBattleground() && sWorld->getBoolConfig(CONFIG_BATTLEGROUND_TRACK_DESERTERS) && + (bg->GetStatus() == STATUS_IN_PROGRESS || bg->GetStatus() == STATUS_WAIT_JOIN)) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_DESERTER_TRACK); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt8(1, BG_DESERTION_TYPE_LEAVE_BG); + CharacterDatabase.Execute(stmt); + } + } +} + +bool Player::CanJoinToBattleground(Battleground const* bg) const +{ + // check Deserter debuff + if (HasAura(26013)) + return false; + + if (bg->isArena() && !GetSession()->HasPermission(rbac::RBAC_PERM_JOIN_ARENAS)) + return false; + + if (bg->IsRandom() && !GetSession()->HasPermission(rbac::RBAC_PERM_JOIN_RANDOM_BG)) + return false; + + if (!GetSession()->HasPermission(rbac::RBAC_PERM_JOIN_NORMAL_BG)) + return false; + + return true; +} + +bool Player::CanReportAfkDueToLimit() +{ + // a player can complain about 15 people per 5 minutes + if (m_bgData.bgAfkReportedCount++ >= 15) + return false; + + return true; +} + +///This player has been blamed to be inactive in a battleground +void Player::ReportedAfkBy(Player* reporter) +{ + Battleground* bg = GetBattleground(); + // Battleground also must be in progress! + if (!bg || bg != reporter->GetBattleground() || GetTeam() != reporter->GetTeam() || bg->GetStatus() != STATUS_IN_PROGRESS) + return; + + // check if player has 'Idle' or 'Inactive' debuff + if (m_bgData.bgAfkReporter.find(reporter->GetGUID().GetCounter()) == m_bgData.bgAfkReporter.end() && !HasAura(43680) && !HasAura(43681) && reporter->CanReportAfkDueToLimit()) + { + m_bgData.bgAfkReporter.insert(reporter->GetGUID().GetCounter()); + // by default 3 players have to complain to apply debuff + if (m_bgData.bgAfkReporter.size() >= sWorld->getIntConfig(CONFIG_BATTLEGROUND_REPORT_AFK)) + { + // cast 'Idle' spell + CastSpell(this, 43680, true); + m_bgData.bgAfkReporter.clear(); + } + } +} + +WorldLocation Player::GetStartPosition() const +{ + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(), getClass()); + ASSERT(info); + uint32 mapId = info->mapId; + if (getClass() == CLASS_DEATH_KNIGHT && HasSpell(50977)) + mapId = 0; + return WorldLocation(mapId, info->positionX, info->positionY, info->positionZ, 0); +} + +bool Player::HaveAtClient(Object const* u) const +{ + return u == this || m_clientGUIDs.find(u->GetGUID()) != m_clientGUIDs.end(); +} + +bool Player::IsNeverVisible() const +{ + if (Unit::IsNeverVisible()) + return true; + + if (GetSession()->PlayerLogout() || GetSession()->PlayerLoading()) + return true; + + return false; +} + +bool Player::CanAlwaysSee(WorldObject const* obj) const +{ + // Always can see self + if (GetCharmedOrSelf() == obj) + return true; + + if (ObjectGuid guid = GetGuidValue(PLAYER_FARSIGHT)) + if (obj->GetGUID() == guid) + return true; + + return false; +} + +bool Player::IsAlwaysDetectableFor(WorldObject const* seer) const +{ + if (Unit::IsAlwaysDetectableFor(seer)) + return true; + + if (Player const* seerPlayer = seer->ToPlayer()) + if (IsGroupVisibleFor(seerPlayer)) + return !(seerPlayer->duel && seerPlayer->duel->startTime != 0 && seerPlayer->duel->opponent == this); + + return false; +} + +bool Player::IsVisibleGloballyFor(Player const* u) const +{ + if (!u) + return false; + + // Always can see self + if (u == this) + return true; + + // Visible units, always are visible for all players + if (IsVisible()) + return true; + + // GMs are visible for higher gms (or players are visible for gms) + if (!AccountMgr::IsPlayerAccount(u->GetSession()->GetSecurity())) + return GetSession()->GetSecurity() <= u->GetSession()->GetSecurity(); + + // non faction visibility non-breakable for non-GMs + return false; +} + +template +inline void UpdateVisibilityOf_helper(GuidUnorderedSet& s64, T* target, std::set& /*v*/) +{ + s64.insert(target->GetGUID()); +} + +template<> +inline void UpdateVisibilityOf_helper(GuidUnorderedSet& s64, Creature* target, std::set& v) +{ + s64.insert(target->GetGUID()); + v.insert(target); +} + +template<> +inline void UpdateVisibilityOf_helper(GuidUnorderedSet& s64, Player* target, std::set& v) +{ + s64.insert(target->GetGUID()); + v.insert(target); +} + +template +inline void BeforeVisibilityDestroy(T* /*t*/, Player* /*p*/) { } + +template<> +inline void BeforeVisibilityDestroy(Creature* t, Player* p) +{ + if (p->GetPetGUID() == t->GetGUID() && t->IsPet()) + t->ToPet()->Remove(PET_SAVE_DISMISS, true); +} + +void Player::UpdateVisibilityOf(WorldObject* target) +{ + if (HaveAtClient(target)) + { + if (!CanSeeOrDetect(target, false, true)) + { + if (target->GetTypeId() == TYPEID_UNIT) + BeforeVisibilityDestroy(target->ToCreature(), this); + + if (!target->IsDestroyedObject()) + target->SendOutOfRangeForPlayer(this); + else + target->DestroyForPlayer(this); + + m_clientGUIDs.erase(target->GetGUID()); + + #ifdef TRINITY_DEBUG + TC_LOG_DEBUG("maps", "Object %u (Type: %u) out of range for player %u. Distance = %f", target->GetGUID().GetCounter(), target->GetTypeId(), GetGUID().GetCounter(), GetDistance(target)); + #endif + } + } + else + { + if (CanSeeOrDetect(target, false, true)) + { + target->SendUpdateToPlayer(this); + m_clientGUIDs.insert(target->GetGUID()); + + #ifdef TRINITY_DEBUG + TC_LOG_DEBUG("maps", "Object %u (Type: %u) is visible now for player %u. Distance = %f", target->GetGUID().GetCounter(), target->GetTypeId(), GetGUID().GetCounter(), GetDistance(target)); + #endif + + // target aura duration for caster show only if target exist at caster client + // send data at target visibility change (adding to client) + if (target->isType(TYPEMASK_UNIT)) + SendInitialVisiblePackets(static_cast(target)); + } + } +} + +void Player::UpdateTriggerVisibility() +{ + if (m_clientGUIDs.empty()) + return; + + if (!IsInWorld()) + return; + + UpdateData udata(GetMapId()); + WorldPacket packet; + for (auto itr = m_clientGUIDs.begin(); itr != m_clientGUIDs.end(); ++itr) + { + if (itr->IsCreatureOrVehicle()) + { + Creature* creature = GetMap()->GetCreature(*itr); + // Update fields of triggers, transformed units or unselectable units (values dependent on GM state) + if (!creature || (!creature->IsTrigger() && !creature->HasAuraType(SPELL_AURA_TRANSFORM) && !creature->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE))) + continue; + + creature->SetFieldNotifyFlag(UF_FLAG_PUBLIC); + creature->BuildValuesUpdateBlockForPlayer(&udata, this); + creature->RemoveFieldNotifyFlag(UF_FLAG_PUBLIC); + } + else if (itr->IsGameObject()) + { + GameObject* go = GetMap()->GetGameObject(*itr); + if (!go) + continue; + + go->SetFieldNotifyFlag(UF_FLAG_PUBLIC); + go->BuildValuesUpdateBlockForPlayer(&udata, this); + go->RemoveFieldNotifyFlag(UF_FLAG_PUBLIC); + } + } + + if (!udata.HasData()) + return; + + udata.BuildPacket(&packet); + SendDirectMessage(&packet); +} + +void Player::SendInitialVisiblePackets(Unit* target) const +{ + SendAurasForTarget(target); + if (target->IsAlive()) + { + if (target->HasUnitState(UNIT_STATE_MELEE_ATTACKING) && target->GetVictim()) + target->SendMeleeAttackStart(target->GetVictim()); + } +} + +template +void Player::UpdateVisibilityOf(T* target, UpdateData& data, std::set& visibleNow) +{ + if (HaveAtClient(target)) + { + if (!CanSeeOrDetect(target, false, true)) + { + BeforeVisibilityDestroy(target, this); + + if (!target->IsDestroyedObject()) + target->BuildOutOfRangeUpdateBlock(&data); + else + target->DestroyForPlayer(this); + + m_clientGUIDs.erase(target->GetGUID()); + + #ifdef TRINITY_DEBUG + TC_LOG_DEBUG("maps", "Object %u (Type: %u, Entry: %u) is out of range for player %u. Distance = %f", target->GetGUID().GetCounter(), target->GetTypeId(), target->GetEntry(), GetGUID().GetCounter(), GetDistance(target)); + #endif + } + } + else + { + if (CanSeeOrDetect(target, false, true)) + { + target->BuildCreateUpdateBlockForPlayer(&data, this); + UpdateVisibilityOf_helper(m_clientGUIDs, target, visibleNow); + + #ifdef TRINITY_DEBUG + TC_LOG_DEBUG("maps", "Object %u (Type: %u, Entry: %u) is visible now for player %u. Distance = %f", target->GetGUID().GetCounter(), target->GetTypeId(), target->GetEntry(), GetGUID().GetCounter(), GetDistance(target)); + #endif + } + } +} + +template void Player::UpdateVisibilityOf(Player* target, UpdateData& data, std::set& visibleNow); +template void Player::UpdateVisibilityOf(Creature* target, UpdateData& data, std::set& visibleNow); +template void Player::UpdateVisibilityOf(Corpse* target, UpdateData& data, std::set& visibleNow); +template void Player::UpdateVisibilityOf(GameObject* target, UpdateData& data, std::set& visibleNow); +template void Player::UpdateVisibilityOf(DynamicObject* target, UpdateData& data, std::set& visibleNow); +template void Player::UpdateVisibilityOf(AreaTrigger* target, UpdateData& data, std::set& visibleNow); + +void Player::UpdateObjectVisibility(bool forced) +{ + // Prevent updating visibility if player is not in world (example: LoadFromDB sets drunkstate which updates invisibility while player is not in map) + if (!IsInWorld()) + return; + + if (!forced) + AddToNotify(NOTIFY_VISIBILITY_CHANGED); + else + { + Unit::UpdateObjectVisibility(true); + UpdateVisibilityForPlayer(); + } +} + +void Player::UpdateVisibilityForPlayer() +{ + // updates visibility of all objects around point of view for current player + Trinity::VisibleNotifier notifier(*this); + Cell::VisitAllObjects(m_seer, notifier, GetSightRange()); + notifier.SendToSelf(); // send gathered data +} + +void Player::InitPrimaryProfessions() +{ + SetFreePrimaryProfessions(sWorld->getIntConfig(CONFIG_MAX_PRIMARY_TRADE_SKILL)); +} + +bool Player::ModifyMoney(int64 amount, bool sendError /*= true*/) +{ + if (!amount) + return true; + + sScriptMgr->OnPlayerMoneyChanged(this, amount); + + if (amount < 0) + SetMoney(GetMoney() > uint64(-amount) ? GetMoney() + amount : 0); + else + { + if (GetMoney() <= MAX_MONEY_AMOUNT - static_cast(amount)) + SetMoney(GetMoney() + amount); + else + { + sScriptMgr->OnPlayerMoneyLimit(this, amount); + + if (sendError) + SendEquipError(EQUIP_ERR_TOO_MUCH_GOLD, nullptr, nullptr); + return false; + } + } + + return true; +} + +bool Player::HasEnoughMoney(int64 amount) const +{ + if (amount > 0) + return (GetMoney() >= (uint64) amount); + return true; +} + +void Player::SetMoney(uint64 value) +{ + SetUInt64Value(PLAYER_FIELD_COINAGE, value); + MoneyChanged(value); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_GOLD_VALUE_OWNED); +} + +bool Player::IsQuestRewarded(uint32 quest_id) const +{ + return m_RewardedQuests.find(quest_id) != m_RewardedQuests.end(); +} + +Unit* Player::GetSelectedUnit() const +{ + if (ObjectGuid selectionGUID = GetTarget()) + return ObjectAccessor::GetUnit(*this, selectionGUID); + return nullptr; +} + +Player* Player::GetSelectedPlayer() const +{ + if (ObjectGuid selectionGUID = GetTarget()) + return ObjectAccessor::FindConnectedPlayer(selectionGUID); + return nullptr; +} + +bool Player::IsInGroup(ObjectGuid groupGuid) const +{ + if (Group const* group = GetGroup()) + if (group->GetGUID() == groupGuid) + return true; + + if (Group const* group = GetOriginalGroup()) + if (group->GetGUID() == groupGuid) + return true; + + return false; +} + +void Player::SetGroup(Group* group, int8 subgroup) +{ + if (group == nullptr) + m_group.unlink(); + else + { + // never use SetGroup without a subgroup unless you specify nullptr for group + ASSERT(subgroup >= 0); + m_group.link(group, this); + m_group.setSubGroup((uint8)subgroup); + } + + UpdateObjectVisibility(false); +} + +void Player::SendInitialPacketsBeforeAddToMap(bool firstLogin /*= false*/) +{ + /// SMSG_CONTROL_UPDATE + SetClientControl(this, true); + + /// SMSG_BIND_POINT_UPDATE + SendBindPointUpdate(); + + /// SMSG_WORLD_SERVER_INFO + WorldPackets::Misc::WorldServerInfo worldServerInfo; + worldServerInfo.IsTournamentRealm = 0; /// @todo + // worldServerInfo.RestrictedAccountMaxLevel; /// @todo + // worldServerInfo.RestrictedAccountMaxMoney; /// @todo + worldServerInfo.DifficultyID = GetMap()->GetDifficulty(); + worldServerInfo.WeeklyReset = sWorld->GetNextWeeklyQuestsResetTime() - WEEK; + SendDirectMessage(worldServerInfo.Write()); + + /// SMSG_SET_PROFICIENCY + SendProficiency(ITEM_CLASS_WEAPON, m_WeaponProficiency); + SendProficiency(ITEM_CLASS_ARMOR, m_ArmorProficiency); + + /// SMSG_SEND_KNOWN_SPELLS + SendKnownSpells(firstLogin); + + /// SMSG_SEND_UNLEARN_SPELLS + SendDirectMessage(WorldPackets::Spells::SendUnlearnSpells().Write()); + + /// SMSG_UPDATE_TALENT_DATA + SendTalentsInfoData(false); + + /// SMSG_UPDATE_ACTION_BUTTONS + SendInitialActionButtons(); + + /// SMSG_INITIALIZE_FACTIONS + m_reputationMgr->SendInitialReputations(); + + /// MSG_SET_DUNGEON_DIFFICULTY + // Handled in WorldSession::HandlePlayerLogin + + /// SMSG_ACCOUNT_DATA_TIMES + // Handled in WorldSession::HandlePlayerLogin + + /// Pass 'this' as argument because we're not stored in ObjectAccessor yet + GetSocial()->SendSocialList(this, SOCIAL_FLAG_ALL); + + /// SMSG_MOTD + // Handled in WorldSession::HandlePlayerLogin + + /// SMSG_FEATURE_SYSTEM_STATUS + // Handled in WorldSession::HandlePlayerLogin + + /// SMSG_RESYNC_RUNES + if (getClass() == CLASS_DEATH_KNIGHT) + { + // Initialize rune cooldowns + ResyncRunes(); + } + + // Spell modifiers + SendSpellModifiers(); + + /// SMSG_SET_FORCED_REACTIONS + GetReputationMgr().SendForceReactions(); + + /// SMSG_SETUP_CURRENCY + SendCurrencies(); + + /// SMSG_WEEKLY_SPELL_USAGE + + /// SMSG_ALL_ACHIEVEMENT_DATA + m_achievementMgr->SendAllAchievementData(this); + + /// SMSG_LOGIN_SET_TIME_SPEED + static float const TimeSpeed = 0.016666667535901069f; + WorldPackets::Misc::LoginSetTimeSpeed loginSetTimeSpeed; + loginSetTimeSpeed.NewSpeed = TimeSpeed; + loginSetTimeSpeed.GameTime = GameTime::GetGameTime(); + loginSetTimeSpeed.GameTimeHolidayOffset = 0; /// @todo + SendDirectMessage(loginSetTimeSpeed.Write()); + + /// SMSG_EQUIPMENT_SET_LIST + SendEquipmentSetList(); + + // SMSG_TALENTS_INFO x 2 for pet (unspent points and talents in separate packets...) + // SMSG_PET_GUIDS + // SMSG_UPDATE_WORLD_STATE + // SMSG_POWER_UPDATE + + GetGameClient()->SetMovedUnit(this, true); +} + +void Player::SendInitialPacketsAfterAddToMap() +{ + UpdateVisibilityForPlayer(); + + // update zone + uint32 newzone, newarea; + GetZoneAndAreaId(newzone, newarea); + UpdateZone(newzone, newarea); // also calls SendInitWorldStates and SendZoneDynamicInfo + + GetSession()->ResetTimeSync(); + GetSession()->SendTimeSync(); + + GetSession()->SendLoadCUFProfiles(); + + CastSpell(this, 836, true); // LOGINEFFECT + + // set some aura effects that send packet to player client after add player to map + // SendMessageToSet not send it to player not it map, only for aura that not changed anything at re-apply + // same auras state lost at far teleport, send it one more time in this case also + static const AuraType auratypes[] = + { + SPELL_AURA_MOD_FEAR, SPELL_AURA_TRANSFORM, SPELL_AURA_WATER_WALK, + SPELL_AURA_FEATHER_FALL, SPELL_AURA_HOVER, SPELL_AURA_SAFE_FALL, + SPELL_AURA_FLY, SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, SPELL_AURA_NONE + }; + for (AuraType const* itr = &auratypes[0]; itr && itr[0] != SPELL_AURA_NONE; ++itr) + { + Unit::AuraEffectList const& auraList = GetAuraEffectsByType(*itr); + if (!auraList.empty()) + auraList.front()->HandleEffect(this, AURA_EFFECT_HANDLE_SEND_FOR_CLIENT, true); + } + + if (HasAuraType(SPELL_AURA_MOD_STUN)) + SetRooted(true); + + // manual send package (have code in HandleEffect(this, AURA_EFFECT_HANDLE_SEND_FOR_CLIENT, true); that must not be re-applied. + if (HasAuraType(SPELL_AURA_MOD_ROOT)) + SetRooted(true, true); + + // only send the packet for converted runes instead of converting them again + if (HasAuraType(SPELL_AURA_CONVERT_RUNE)) + SendConvertedRunes(); + + SendAurasForTarget(this); + SendEnchantmentDurations(); // must be after add to map + SendItemDurations(); // must be after add to map + SendQuestGiverStatusMultiple(); + SendTaxiNodeStatusMultiple(); + + // raid downscaling - send difficulty to player + if (GetMap()->IsRaid()) + { + if (GetMap()->GetDifficulty() != GetRaidDifficulty()) + { + StoreRaidMapDifficulty(); + SendRaidDifficulty(GetGroup() != nullptr, GetStoredRaidDifficulty()); + } + } + else if (GetRaidDifficulty() != GetStoredRaidDifficulty()) + SendRaidDifficulty(GetGroup() != nullptr); + + if (GetPlayerSharingQuest()) + { + if (Quest const* quest = sObjectMgr->GetQuestTemplate(GetSharedQuestID())) + { + SetPopupQuestId(0); + PlayerTalkClass->SendQuestGiverQuestDetails(quest, GetGUID(), true, false); + } + else + ClearQuestSharingInfo(); + } + + PhasingHandler::OnMapChange(this); +} + +void Player::SendUpdateToOutOfRangeGroupMembers() +{ + if (m_groupUpdateMask == GROUP_UPDATE_FLAG_NONE) + return; + + if (Group* group = GetGroup()) + group->UpdatePlayerOutOfRange(this); + + m_groupUpdateMask = GROUP_UPDATE_FLAG_NONE; + m_auraRaidUpdateMask = 0; + if (Pet* pet = GetPet()) + pet->ResetAuraUpdateMaskForRaid(); +} + +void Player::SendTransferAborted(uint32 mapid, TransferAbortReason reason, uint8 arg) const +{ + WorldPackets::Movement::TransferAborted packet; + packet.MapID = mapid; + packet.TransfertAbort = reason; // transfer abort reason + packet.Arg = arg; + SendDirectMessage(packet.Write()); +} + +void Player::SendInstanceResetWarning(uint32 mapid, Difficulty difficulty, uint32 time, bool welcome) const +{ + // type of warning, based on the time remaining until reset + uint32 type; + if (welcome) + type = RAID_INSTANCE_WELCOME; + else if (time > 21600) + type = RAID_INSTANCE_WELCOME; + else if (time > 3600) + type = RAID_INSTANCE_WARNING_HOURS; + else if (time > 300) + type = RAID_INSTANCE_WARNING_MIN; + else + type = RAID_INSTANCE_WARNING_MIN_SOON; + + WorldPackets::Instance::RaidInstanceMessage raidInstanceMessage; + raidInstanceMessage.Type = type; + raidInstanceMessage.MapID = mapid; + raidInstanceMessage.Difficulty = difficulty; + raidInstanceMessage.TimeLeft = time; + if (InstancePlayerBind const* bind = GetBoundInstance(mapid, difficulty)) + raidInstanceMessage.Locked = bind->perm; + else + raidInstanceMessage.Locked = false; + raidInstanceMessage.Extended = false; + SendDirectMessage(raidInstanceMessage.Write()); +} + +void Player::ApplyEquipCooldown(Item* pItem) +{ + if (pItem->GetTemplate()->GetFlags() & ITEM_FLAG_NO_EQUIP_COOLDOWN) + return; + + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + for (ItemEffect const& effect : pItem->GetTemplate()->Effects) + { + // no spell + if (effect.SpellID <= 0) + continue; + + // apply proc cooldown to equip auras if we have any + if (effect.Trigger == ITEM_SPELLTRIGGER_ON_EQUIP) + { + SpellProcEntry const* procEntry = sSpellMgr->GetSpellProcEntry(effect.SpellID); + if (!procEntry) + continue; + + if (Aura* itemAura = GetAura(effect.SpellID, GetGUID(), pItem->GetGUID())) + itemAura->AddProcCooldown(procEntry, now); + continue; + } + + // wrong triggering type (note: ITEM_SPELLTRIGGER_ON_NO_DELAY_USE not have cooldown) + if (effect.Trigger != ITEM_SPELLTRIGGER_ON_USE) + continue; + + // Don't replace longer cooldowns by equip cooldown if we have any. + if (GetSpellHistory()->GetRemainingCooldown(sSpellMgr->AssertSpellInfo(effect.SpellID)) > 30 * IN_MILLISECONDS) + continue; + + GetSpellHistory()->AddCooldown(effect.SpellID, pItem->GetEntry(), std::chrono::seconds(30)); + + WorldPacket data(SMSG_ITEM_COOLDOWN, 8 + 4); + data << uint64(pItem->GetGUID()); + data << uint32(effect.SpellID); + SendDirectMessage(&data); + } +} + +void Player::ResetSpells(bool myClassOnly) +{ + // not need after this call + if (HasAtLoginFlag(AT_LOGIN_RESET_SPELLS)) + RemoveAtLoginFlag(AT_LOGIN_RESET_SPELLS, true); + + // make full copy of map (spells removed and marked as deleted at another spell remove + // and we can't use original map for safe iterative with visit each spell at loop end + PlayerSpellMap smap = GetSpellMap(); + + uint32 family; + + if (myClassOnly) + { + ChrClassesEntry const* clsEntry = sChrClassesStore.LookupEntry(getClass()); + if (!clsEntry) + return; + family = clsEntry->SpellClassSet; + + for (PlayerSpellMap::const_iterator iter = smap.begin(); iter != smap.end(); ++iter) + { + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(iter->first); + if (!spellInfo) + continue; + + // skip server-side/triggered spells + if (spellInfo->SpellLevel == 0) + continue; + + // skip wrong class/race skills + if (!IsSpellFitByClassAndRace(spellInfo->Id)) + continue; + + // skip other spell families + if (spellInfo->SpellFamilyName != family) + continue; + + // skip spells with first rank learned as talent (and all talents then also) + uint32 firstRank = spellInfo->GetFirstRankSpell()->Id; + if (sDBCManager.GetTalentSpellCost(firstRank) > 0) + continue; + + // skip broken spells + if (!SpellMgr::IsSpellValid(spellInfo, this, false)) + continue; + } + } + else + for (PlayerSpellMap::const_iterator iter = smap.begin(); iter != smap.end(); ++iter) + RemoveSpell(iter->first, false, false); // only iter->first can be accessed, object by iter->second can be deleted already + + LearnDefaultSkills(); + LearnCustomSpells(); + LearnQuestRewardedSpells(); +} + +void Player::LearnCustomSpells() +{ + if (!sWorld->getBoolConfig(CONFIG_START_ALL_SPELLS)) + return; + + // learn default race/class spells + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(), getClass()); + ASSERT(info); + for (PlayerCreateInfoSpells::const_iterator itr = info->customSpells.begin(); itr != info->customSpells.end(); ++itr) + { + uint32 tspell = *itr; + TC_LOG_DEBUG("entities.player.loading", "Player::LearnCustomSpells: Player '%s' (%s, Class: %u Race: %u): Adding initial spell (SpellID: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), uint32(getClass()), uint32(getRace()), tspell); + if (!IsInWorld()) // will send in INITIAL_SPELLS in list anyway at map add + AddSpell(tspell, true, true, true, false); + else // but send in normal spell in game learn case + LearnSpell(tspell, true); + } +} + +void Player::LearnDefaultSkills() +{ + // learn default race/class skills + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(), getClass()); + ASSERT(info); + for (PlayerCreateInfoSkills::const_iterator itr = info->skills.begin(); itr != info->skills.end(); ++itr) + { + SkillRaceClassInfoEntry const* rcInfo = *itr; + if (HasSkill(rcInfo->SkillID)) + continue; + + if (rcInfo->MinLevel > getLevel()) + continue; + + LearnDefaultSkill(rcInfo); + } +} + +void Player::LearnDefaultSkill(SkillRaceClassInfoEntry const* rcInfo) +{ + uint16 skillId = rcInfo->SkillID; + switch (GetSkillRangeType(rcInfo)) + { + case SKILL_RANGE_LANGUAGE: + SetSkill(skillId, 0, 300, 300); + break; + case SKILL_RANGE_LEVEL: + { + uint16 skillValue = 1; + uint16 maxValue = GetMaxSkillValueForLevel(); + if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + skillValue = maxValue; + else if (getClass() == CLASS_DEATH_KNIGHT) + skillValue = std::min(std::max({ 1, uint16((getLevel() - 1) * 5) }), maxValue); + else if (skillId == SKILL_FIST_WEAPONS) + skillValue = std::max(1, GetSkillValue(SKILL_UNARMED)); + else if (skillId == SKILL_LOCKPICKING) + skillValue = std::max(1, GetSkillValue(SKILL_LOCKPICKING)); + + SetSkill(skillId, 0, skillValue, maxValue); + break; + } + case SKILL_RANGE_MONO: + SetSkill(skillId, 0, 1, 1); + break; + case SKILL_RANGE_RANK: + { + SkillTiersEntry const* tier = sSkillTiersStore.LookupEntry(rcInfo->SkillTierID); + uint16 maxValue = tier->GetValueForTierIndex(0); + uint16 skillValue = 1; + if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE) + skillValue = maxValue; + else if (getClass() == CLASS_DEATH_KNIGHT) + skillValue = std::min(std::max(uint16(1), uint16((getLevel() - 1) * 5)), maxValue); + + SetSkill(skillId, 1, skillValue, maxValue); + break; + } + default: + break; + } +} + +void Player::LearnQuestRewardedSpells(Quest const* quest) +{ + int32 spell_id = quest->GetRewSpellCast(); + uint32 src_spell_id = quest->GetSrcSpell(); + + // skip quests without rewarded spell + if (!spell_id) + return; + + // if RewSpellCast = -1 we remove aura do to SrcSpell from player. + if (spell_id == -1 && src_spell_id) + { + RemoveAurasDueToSpell(src_spell_id); + return; + } + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell_id); + if (!spellInfo) + return; + + // check learned spells state + bool found = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (spellInfo->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL && !HasSpell(spellInfo->Effects[i].TriggerSpell)) + { + found = true; + break; + } + } + + // skip quests with not teaching spell or already known spell + if (!found) + return; + + uint32 learned_0 = spellInfo->Effects[0].TriggerSpell; + if (!HasSpell(learned_0)) + { + found = false; + SkillLineAbilityMapBounds skills = sSpellMgr->GetSkillLineAbilityMapBounds(learned_0); + for (auto skillItr = skills.first; skillItr != skills.second; ++skillItr) + { + if (skillItr->second->GetAcquireMethod() == SkillLineAbilityAcquireMethod::LearnedOrAutomaticCharLevel) + { + found = true; + break; + } + } + + if (!found) + return; + } + + CastSpell(this, spell_id, true); +} + +void Player::LearnQuestRewardedSpells() +{ + // learn spells received from quest completing + for (RewardedQuestSet::const_iterator itr = m_RewardedQuests.begin(); itr != m_RewardedQuests.end(); ++itr) + { + Quest const* quest = sObjectMgr->GetQuestTemplate(*itr); + if (!quest) + continue; + + LearnQuestRewardedSpells(quest); + } +} + +void Player::LearnSkillRewardedSpells(uint32 skillId, uint32 skillValue) +{ + uint32 raceMask = getRaceMask(); + uint32 classMask = getClassMask(); + std::vector const* skillLineAbilities = sDBCManager.GetSkillLineAbilitiesBySkill(skillId); + if (!skillLineAbilities) + return; + + for (SkillLineAbilityEntry const* ability : *skillLineAbilities) + { + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(ability->Spell); + if (!spellInfo) + continue; + + switch (ability->GetAcquireMethod()) + { + case SkillLineAbilityAcquireMethod::AutomaticSkillRank: + case SkillLineAbilityAcquireMethod::AutomaticCharLevel: + break; + default: + continue; + } + + // Check race if set + if (ability->RaceMask && !(ability->RaceMask & raceMask)) + continue; + + // Check class if set + if (ability->ClassMask && !(ability->ClassMask & classMask)) + continue; + + // Check level, skip class spells if not high enough + uint32 requiredLevel = std::max(spellInfo->SpellLevel, spellInfo->BaseLevel); + if (requiredLevel > getLevel()) + continue; + + // need unlearn spell + if (skillValue < ability->MinSkillLineRank && ability->GetAcquireMethod() == SkillLineAbilityAcquireMethod::AutomaticSkillRank) + RemoveSpell(ability->Spell); + + // need learn + else if (!IsInWorld()) + AddSpell(ability->Spell, true, true, true, false, false, ability->SkillLine); + else + LearnSpell(ability->Spell, true, ability->SkillLine); + } +} + +uint32 Player::GetSkillRank(uint8 pos) const +{ + uint16 field = pos / 2; + uint8 offset = pos & 1; + + return int32(GetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset)); +} + +void Player::SetSkillLineId(uint8 pos, uint16 value) +{ + uint16 field = pos / 2; + uint8 offset = pos & 1; + SetUInt16Value(PLAYER_SKILL_LINEID_0 + field, offset, value); +} + +void Player::SetSkillStep(uint8 pos, uint16 value) +{ + uint16 field = pos / 2; + uint8 offset = pos & 1; + SetUInt16Value(PLAYER_SKILL_STEP_0 + field, offset, value); +} +void Player::SetSkillRank(uint8 pos, uint16 value) +{ + uint16 field = pos / 2; + uint8 offset = pos & 1; + SetUInt16Value(PLAYER_SKILL_RANK_0 + field, offset, value); +} + +void Player::SetSkillMaxRank(uint8 pos, uint16 value) +{ + uint16 field = pos / 2; + uint8 offset = pos & 1; + SetUInt16Value(PLAYER_SKILL_MAX_RANK_0 + field, offset, value); +} + +void Player::SetSkillTempBonus(uint8 pos, uint16 value) +{ + uint16 field = pos / 2; + uint8 offset = pos & 1; + SetUInt16Value(PLAYER_SKILL_MODIFIER_0 + field, offset, value); +} + +void Player::SetSkillPermBonus(uint8 pos, uint16 value) +{ + uint16 field = pos / 2; + uint8 offset = pos & 1; + SetUInt16Value(PLAYER_SKILL_TALENT_0 + field, offset, value); +} + +void Player::SendAurasForTarget(Unit* target) const +{ + if (!target || target->GetVisibleAuras()->empty()) // speedup things + return; + + /*! Blizz sends certain movement packets sometimes even before CreateObject + These movement packets are usually found in SMSG_COMPRESSED_MOVES + */ + if (target->HasAuraType(SPELL_AURA_FEATHER_FALL)) + target->SetFeatherFall(true); + + if (target->HasAuraType(SPELL_AURA_WATER_WALK)) + target->SetWaterWalking(true); + + if (target->HasAuraType(SPELL_AURA_HOVER)) + target->SetHover(true); + + Unit::VisibleAuraMap const* visibleAuras = target->GetVisibleAuras(); + + WorldPackets::Spells::AuraUpdateAll update; + update.UnitGUID = target->GetGUID(); + update.Auras.reserve(visibleAuras->size()); + + for (Unit::VisibleAuraMap::const_iterator itr = visibleAuras->begin(); itr != visibleAuras->end(); ++itr) + { + WorldPackets::Spells::AuraInfo auraInfo; + AuraApplication* auraApp = itr->second; + auraApp->BuildUpdatePacket(auraInfo, false); + update.Auras.push_back(auraInfo); + } + GetSession()->SendPacket(update.Write()); +} + +void Player::SetDailyQuestStatus(uint32 quest_id) +{ + if (sObjectMgr->GetQuestTemplate(quest_id)) + { + for (uint32 quest_daily_idx = 0; quest_daily_idx < PLAYER_MAX_DAILY_QUESTS; ++quest_daily_idx) + { + if (!GetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1 + quest_daily_idx)) + { + SetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1 + quest_daily_idx, quest_id); + m_lastDailyQuestTime = GameTime::GetGameTime(); // last daily quest time + m_DailyQuestChanged = true; + break; + } + } + } +} + +bool Player::IsDailyQuestDone(uint32 quest_id) +{ + bool found = false; + if (sObjectMgr->GetQuestTemplate(quest_id)) + { + for (uint32 quest_daily_idx = 0; quest_daily_idx < PLAYER_MAX_DAILY_QUESTS; ++quest_daily_idx) + { + if (GetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1 + quest_daily_idx) == quest_id) + { + found = true; + break; + } + } + } + + return found; +} + +void Player::SetWeeklyQuestStatus(uint32 quest_id) +{ + m_weeklyquests.insert(quest_id); + m_WeeklyQuestChanged = true; +} + +void Player::SetSeasonalQuestStatus(uint32 quest_id) +{ + Quest const* quest = sObjectMgr->GetQuestTemplate(quest_id); + if (!quest) + return; + + m_seasonalquests[quest->GetEventIdForQuest()][quest_id] = GameTime::GetGameTime(); + m_SeasonalQuestChanged = true; +} + +void Player::SetMonthlyQuestStatus(uint32 quest_id) +{ + m_monthlyquests.insert(quest_id); + m_MonthlyQuestChanged = true; +} + +void Player::SetLFGRewardStatus(uint32 dungeon_id, bool daily_reset) +{ + LFGRewardStatusMap::iterator lfgdungeon = m_lfgrewardstatus.find(dungeon_id); + + if (lfgdungeon != m_lfgrewardstatus.end()) + lfgdungeon->second.CompletionsThisPeriod++; + else + m_lfgrewardstatus[dungeon_id] = LFGRewardInfo(1, daily_reset); + + m_LFGRewardStatusChanged = true; +} + +void Player::ResetDailyQuestStatus() +{ + for (uint32 quest_daily_idx = 0; quest_daily_idx < PLAYER_MAX_DAILY_QUESTS; ++quest_daily_idx) + SetUInt32Value(PLAYER_FIELD_DAILY_QUESTS_1+quest_daily_idx, 0); + + // DB data deleted in caller + m_DailyQuestChanged = false; + m_lastDailyQuestTime = 0; +} + +void Player::ResetWeeklyQuestStatus() +{ + if (m_weeklyquests.empty()) + return; + + m_weeklyquests.clear(); + // DB data deleted in caller + m_WeeklyQuestChanged = false; +} + +void Player::ResetSeasonalQuestStatus(uint16 event_id, time_t eventStartTime) +{ + // DB data deleted in caller + m_SeasonalQuestChanged = false; + + auto eventItr = m_seasonalquests.find(event_id); + if (eventItr == m_seasonalquests.end()) + return; + + if (eventItr->second.empty()) + return; + + for (auto questItr = eventItr->second.begin(); questItr != eventItr->second.end();) + { + if (questItr->second < eventStartTime) + questItr = eventItr->second.erase(questItr); + else + ++questItr; + } + + if (eventItr->second.empty()) + m_seasonalquests.erase(eventItr); +} + +void Player::ResetMonthlyQuestStatus() +{ + if (m_monthlyquests.empty()) + return; + + m_monthlyquests.clear(); + // DB data deleted in caller + m_MonthlyQuestChanged = false; +} + +void Player::ResetWeeklyLFGRewardStatus() +{ + for (auto itr = m_lfgrewardstatus.begin(); itr != m_lfgrewardstatus.end();) + { + if (!itr->second.IsDaily) + { + itr = m_lfgrewardstatus.erase(itr); + continue; + } + itr++; + } + + // DB data deleted in caller + m_LFGRewardStatusChanged = false; +} + +void Player::ResetDailyLFGRewardStatus() +{ + for (auto itr = m_lfgrewardstatus.begin(); itr != m_lfgrewardstatus.end();) + { + if (itr->second.IsDaily) + { + itr = m_lfgrewardstatus.erase(itr); + continue; + } + itr++; + } + + // DB data deleted in caller + m_LFGRewardStatusChanged = false; +} + +Battleground* Player::GetBattleground() const +{ + if (GetBattlegroundId() == 0) + return nullptr; + + return sBattlegroundMgr->GetBattleground(GetBattlegroundId(), m_bgData.bgTypeID); +} + +uint32 Player::GetBattlegroundQueueJoinTime(uint32 bgTypeId) const +{ + std::map::const_iterator itr = m_bgData.bgQueuesJoinedTime.find(bgTypeId); + if (itr != m_bgData.bgQueuesJoinedTime.end()) + return itr->second; + + return GameTime::GetGameTimeMS(); +} + +void Player::AddBattlegroundQueueJoinTime(uint32 bgTypeId, uint32 joinTime) +{ + m_bgData.bgQueuesJoinedTime[bgTypeId] = joinTime; +} + +void Player::RemoveBattlegroundQueueJoinTime(uint32 bgTypeId) +{ + std::map::const_iterator itr = m_bgData.bgQueuesJoinedTime.find(bgTypeId); + if (itr != m_bgData.bgQueuesJoinedTime.end()) + m_bgData.bgQueuesJoinedTime.erase(itr->second); +} + +bool Player::InBattlegroundQueue() const +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (m_bgBattlegroundQueueID[i].bgQueueTypeId != BATTLEGROUND_QUEUE_NONE) + return true; + return false; +} + +BattlegroundQueueTypeId Player::GetBattlegroundQueueTypeId(uint32 index) const +{ + return m_bgBattlegroundQueueID[index].bgQueueTypeId; +} + +uint32 Player::GetBattlegroundQueueIndex(BattlegroundQueueTypeId bgQueueTypeId) const +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (m_bgBattlegroundQueueID[i].bgQueueTypeId == bgQueueTypeId) + return i; + return PLAYER_MAX_BATTLEGROUND_QUEUES; +} + +bool Player::IsInvitedForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (m_bgBattlegroundQueueID[i].bgQueueTypeId == bgQueueTypeId) + return m_bgBattlegroundQueueID[i].invitedToInstance != 0; + return false; +} + +bool Player::InBattlegroundQueueForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const +{ + return GetBattlegroundQueueIndex(bgQueueTypeId) < PLAYER_MAX_BATTLEGROUND_QUEUES; +} + +void Player::SetBattlegroundId(uint32 val, BattlegroundTypeId bgTypeId) +{ + m_bgData.bgInstanceID = val; + m_bgData.bgTypeID = bgTypeId; +} + +uint32 Player::AddBattlegroundQueueId(BattlegroundQueueTypeId val) +{ + for (uint8 i=0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + { + if (m_bgBattlegroundQueueID[i].bgQueueTypeId == BATTLEGROUND_QUEUE_NONE || m_bgBattlegroundQueueID[i].bgQueueTypeId == val) + { + m_bgBattlegroundQueueID[i].bgQueueTypeId = val; + m_bgBattlegroundQueueID[i].invitedToInstance = 0; + return i; + } + } + return PLAYER_MAX_BATTLEGROUND_QUEUES; +} + +bool Player::HasFreeBattlegroundQueueId() const +{ + for (uint8 i=0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (m_bgBattlegroundQueueID[i].bgQueueTypeId == BATTLEGROUND_QUEUE_NONE) + return true; + return false; +} + +void Player::RemoveBattlegroundQueueId(BattlegroundQueueTypeId val) +{ + for (uint8 i=0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + { + if (m_bgBattlegroundQueueID[i].bgQueueTypeId == val) + { + m_bgBattlegroundQueueID[i].bgQueueTypeId = BATTLEGROUND_QUEUE_NONE; + m_bgBattlegroundQueueID[i].invitedToInstance = 0; + return; + } + } +} + +void Player::SetInviteForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId, uint32 instanceId) +{ + for (uint8 i=0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (m_bgBattlegroundQueueID[i].bgQueueTypeId == bgQueueTypeId) + m_bgBattlegroundQueueID[i].invitedToInstance = instanceId; +} + +bool Player::IsInvitedForBattlegroundInstance(uint32 instanceId) const +{ + for (uint8 i=0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (m_bgBattlegroundQueueID[i].invitedToInstance == instanceId) + return true; + return false; +} + +bool Player::InArena() const +{ + Battleground* bg = GetBattleground(); + if (!bg || !bg->isArena()) + return false; + + return true; +} + +bool Player::GetBGAccessByLevel(BattlegroundTypeId bgTypeId) const +{ + // get a template bg instead of running one + Battleground* bg = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); + if (!bg) + return false; + + // limit check leel to dbc compatible level range + uint32 level = getLevel(); + if (level > DEFAULT_MAX_LEVEL) + level = DEFAULT_MAX_LEVEL; + + if (level < bg->GetMinLevel() || level > bg->GetMaxLevel()) + return false; + + return true; +} + +float Player::GetReputationPriceDiscount(Creature const* creature) const +{ + ChrRacesEntry const* raceEntry = sChrRacesStore.LookupEntry(getRace()); + if (raceEntry->Flags & 0x100) + return 0.8f; + + FactionTemplateEntry const* vendor_faction = creature->GetFactionTemplateEntry(); + if (!vendor_faction || !vendor_faction->Faction) + return 1.0f; + + ReputationRank rank = GetReputationRank(vendor_faction->Faction); + if (rank <= REP_NEUTRAL) + return 1.0f; + + return 1.0f - 0.05f* (rank - REP_NEUTRAL); +} + +Player* Player::GetTrader() const +{ + return m_trade ? m_trade->GetTrader() : nullptr; +} + +bool Player::IsSpellFitByClassAndRace(uint32 spell_id) const +{ + uint32 racemask = getRaceMask(); + uint32 classmask = getClassMask(); + + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spell_id); + if (bounds.first == bounds.second) + return true; + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + { + // skip wrong race skills + if (_spell_idx->second->RaceMask && (_spell_idx->second->RaceMask & racemask) == 0) + continue; + + // skip wrong class skills + if (_spell_idx->second->ClassMask && (_spell_idx->second->ClassMask & classmask) == 0) + continue; + + // skip wrong class and race skill saved in SkillRaceClassInfo.dbc + if (!sDBCManager.GetSkillRaceClassInfo(_spell_idx->second->SkillLine, getRace(), getClass())) + continue; + + return true; + } + + return false; +} + +bool Player::HasQuestForGO(int32 GOId) const +{ + for (uint8 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questid = GetQuestSlotQuestId(i); + if (questid == 0) + continue; + + QuestStatusMap::const_iterator qs_itr = m_QuestStatus.find(questid); + if (qs_itr == m_QuestStatus.end()) + continue; + + QuestStatusData const& qs = qs_itr->second; + + if (qs.Status == QUEST_STATUS_INCOMPLETE) + { + Quest const* qinfo = sObjectMgr->GetQuestTemplate(questid); + if (!qinfo) + continue; + + if (GetGroup() && GetGroup()->isRaidGroup() && !qinfo->IsAllowedInRaid(GetMap()->GetDifficulty())) + continue; + + for (uint8 j = 0; j < QUEST_OBJECTIVES_COUNT; ++j) + { + if (qinfo->RequiredNpcOrGo[j] >= 0) //skip non GO case + continue; + + if ((-1)*GOId == qinfo->RequiredNpcOrGo[j] && qs.CreatureOrGOCount[j] < qinfo->RequiredNpcOrGoCount[j]) + return true; + } + } + } + return false; +} + +void Player::UpdateVisibleGameobjectsOrSpellClicks() +{ + if (m_clientGUIDs.empty()) + return; + + UpdateData udata(GetMapId()); + WorldPacket packet; + for (auto itr = m_clientGUIDs.begin(); itr != m_clientGUIDs.end(); ++itr) + { + if (itr->IsGameObject()) + { + if (GameObject* obj = ObjectAccessor::GetGameObject(*this, *itr)) + obj->BuildValuesUpdateBlockForPlayer(&udata, this); + } + else if (itr->IsCreatureOrVehicle()) + { + Creature* obj = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, *itr); + if (!obj) + continue; + + // check if this unit requires quest specific flags + if (!obj->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_SPELLCLICK)) + continue; + + SpellClickInfoMapBounds clickPair = sObjectMgr->GetSpellClickInfoMapBounds(obj->GetEntry()); + for (SpellClickInfoContainer::const_iterator _itr = clickPair.first; _itr != clickPair.second; ++_itr) + { + if (sConditionMgr->GetConditionsForSpellClickEvent(obj->GetEntry(), _itr->second.spellId)) + { + obj->BuildValuesUpdateBlockForPlayer(&udata, this); + break; + } + } + } + } + udata.BuildPacket(&packet); + SendDirectMessage(&packet); +} + +bool Player::HasSummonPending() const +{ + return m_summon_expire >= GameTime::GetGameTime(); +} + +void Player::SendSummonRequestFrom(Unit* summoner) +{ + if (!summoner) + return; + + // Player already has active summon request + if (HasSummonPending()) + return; + + // Evil Twin (ignore player summon, but hide this for summoner) + if (HasAura(23445)) + return; + + m_summon_expire = GameTime::GetGameTime() + MAX_PLAYER_SUMMON_DELAY; + m_summon_location.WorldRelocate(*summoner); + m_summon_instanceId = summoner->GetInstanceId(); + + WorldPacket data(SMSG_SUMMON_REQUEST, 8 + 4 + 4); + data << uint64(summoner->GetGUID()); // summoner guid + data << uint32(summoner->GetZoneId()); // summoner zone + data << uint32(MAX_PLAYER_SUMMON_DELAY*IN_MILLISECONDS); // auto decline after msecs + SendDirectMessage(&data); +} + +void Player::SummonIfPossible(bool agree) +{ + if (!agree) + { + m_summon_expire = 0; + return; + } + + // expire and auto declined + if (m_summon_expire < GameTime::GetGameTime()) + return; + + // stop taxi flight at summon + if (IsInFlight()) + { + GetMotionMaster()->MovementExpired(); + CleanupAfterTaxiFlight(); + } + + // drop flag at summon + // this code can be reached only when GM is summoning player who carries flag, because player should be immune to summoning spells when he carries flag + if (Battleground* bg = GetBattleground()) + bg->EventPlayerDroppedFlag(this); + + m_summon_expire = 0; + + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_ACCEPTED_SUMMONINGS, 1); + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Summon); + + TeleportTo(m_summon_location, TELE_TO_NONE, m_summon_instanceId); +} + +void Player::RemoveItemDurations(Item* item) +{ + for (ItemDurationList::iterator itr = m_itemDuration.begin(); itr != m_itemDuration.end(); ++itr) + { + if (*itr == item) + { + m_itemDuration.erase(itr); + break; + } + } +} + +void Player::AddItemDurations(Item* item) +{ + if (item->GetUInt32Value(ITEM_FIELD_DURATION)) + { + m_itemDuration.push_back(item); + item->SendTimeUpdate(this); + } +} + +void Player::AutoUnequipOffhandIfNeed(bool force /*= false*/) +{ + Item* offItem = GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!offItem) + return; + + // unequip offhand weapon if player doesn't have dual wield anymore + if (!CanDualWield() && (offItem->GetTemplate()->GetInventoryType() == INVTYPE_WEAPONOFFHAND || offItem->GetTemplate()->GetInventoryType() == INVTYPE_WEAPON)) + force = true; + + // need unequip offhand for 2h-weapon without TitanGrip (in any from hands) + if (!force && (CanTitanGrip() || (offItem->GetTemplate()->GetInventoryType() != INVTYPE_2HWEAPON && !IsTwoHandUsed()))) + return; + + ItemPosCountVec off_dest; + if (CanStoreItem(NULL_BAG, NULL_SLOT, off_dest, offItem, false) == EQUIP_ERR_OK) + { + RemoveItem(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND, true); + StoreItem(off_dest, offItem, true); + } + else + { + MoveItemFromInventory(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND, true); + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + offItem->DeleteFromInventoryDB(trans); // deletes item from character's inventory + offItem->SaveToDB(trans); // recursive and not have transaction guard into self, item not in inventory and can be save standalone + + std::string subject = GetSession()->GetTrinityString(LANG_NOT_EQUIPPED_ITEM); + MailDraft(subject, "There were problems with equipping one or several items").AddItem(offItem).SendMailTo(trans, this, MailSender(this, MAIL_STATIONERY_GM), MAIL_CHECK_MASK_COPIED); + + CharacterDatabase.CommitTransaction(trans); + } +} + +OutdoorPvP* Player::GetOutdoorPvP() const +{ + return sOutdoorPvPMgr->GetOutdoorPvPToZoneId(GetMap(), GetZoneId()); +} + +bool Player::HasItemFitToSpellRequirements(SpellInfo const* spellInfo, Item const* ignoreItem) const +{ + if (spellInfo->EquippedItemClass < 0) + return true; + + // scan other equipped items for same requirements (mostly 2 daggers/etc) + // for optimize check 2 used cases only + switch (spellInfo->EquippedItemClass) + { + case ITEM_CLASS_WEAPON: + { + for (uint8 i = EQUIPMENT_SLOT_MAINHAND; i < EQUIPMENT_SLOT_TABARD; ++i) + if (Item* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (item != ignoreItem && item->IsFitToSpellRequirements(spellInfo)) + return true; + break; + } + case ITEM_CLASS_ARMOR: + { + // most used check: shield only + if (spellInfo->EquippedItemSubClassMask & ((1 << ITEM_SUBCLASS_ARMOR_BUCKLER) | (1 << ITEM_SUBCLASS_ARMOR_SHIELD))) + { + if (Item* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND)) + if (item != ignoreItem && item->IsFitToSpellRequirements(spellInfo)) + return true; + + // special check to filter things like Shield Wall, the aura is not permanent and must stay even without required item + if (!spellInfo->IsPassive()) + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (spellInfo->Effects[i].IsAura()) + return true; + } + } + + // tabard not have dependent spells + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_MAINHAND; ++i) + if (Item* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, i)) + if (item != ignoreItem && item->IsFitToSpellRequirements(spellInfo)) + return true; + + // ranged slot can have some armor subclasses + if (Item* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) + if (item != ignoreItem && item->IsFitToSpellRequirements(spellInfo)) + return true; + break; + } + default: + TC_LOG_ERROR("entities.player", "Player::HasItemFitToSpellRequirements: Not handled spell requirement for item class %u", spellInfo->EquippedItemClass); + break; + } + + return false; +} + +bool Player::HasAllItemsToFitToSpellRequirements(SpellInfo const* spellInfo) +{ + uint8 count = 0; + + for (uint8 slot = EQUIPMENT_SLOT_START; slot <= EQUIPMENT_SLOT_HANDS; ++slot) + if (Item* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, slot)) + if (item->IsFitToSpellRequirements(spellInfo)) + ++count; + + if (count >= 8) + return true; + + return false; +} + +bool Player::CanNoReagentCast(SpellInfo const* spellInfo) const +{ + // don't take reagents for spells with SPELL_ATTR5_NO_REAGENT_WHILE_PREP + if (spellInfo->HasAttribute(SPELL_ATTR5_NO_REAGENT_COST_WITH_AURA) && + HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PREPARATION)) + return true; + + // Check no reagent use mask + flag96 noReagentMask; + noReagentMask[0] = GetUInt32Value(PLAYER_NO_REAGENT_COST_1); + noReagentMask[1] = GetUInt32Value(PLAYER_NO_REAGENT_COST_1+1); + noReagentMask[2] = GetUInt32Value(PLAYER_NO_REAGENT_COST_1+2); + if (spellInfo->SpellFamilyFlags & noReagentMask) + return true; + + return false; +} + +void Player::RemoveItemDependentAurasAndCasts(Item* pItem) +{ + for (AuraMap::iterator itr = m_ownedAuras.begin(); itr != m_ownedAuras.end();) + { + Aura* aura = itr->second; + + // skip not self applied auras + SpellInfo const* spellInfo = aura->GetSpellInfo(); + if (aura->GetCasterGUID() != GetGUID()) + { + ++itr; + continue; + } + + // skip if not item dependent or have alternative item + if (HasItemFitToSpellRequirements(spellInfo, pItem)) + { + ++itr; + continue; + } + + // no alt item, remove aura, restart check + RemoveOwnedAura(itr); + } + + // currently cast spells can be dependent from item + for (uint32 i = 0; i < CURRENT_MAX_SPELL; ++i) + if (Spell* spell = GetCurrentSpell(CurrentSpellTypes(i))) + if (!HasItemFitToSpellRequirements(spell->m_spellInfo, pItem)) + InterruptSpell(CurrentSpellTypes(i), false); +} + +uint32 Player::GetResurrectionSpellId() +{ + // search priceless resurrection possibilities + uint32 prio = 0; + uint32 spell_id = 0; + AuraEffectList const& dummyAuras = GetAuraEffectsByType(SPELL_AURA_DUMMY); + for (AuraEffectList::const_iterator itr = dummyAuras.begin(); itr != dummyAuras.end(); ++itr) + { + // Soulstone Resurrection // prio: 3 (max, non death persistent) + if (prio < 2 && (*itr)->GetSpellInfo()->SpellVisual[0] == 99 && (*itr)->GetSpellInfo()->SpellIconID == 92) + { + switch ((*itr)->GetId()) + { + case 20707: spell_id = 3026; break; // rank 1 + case 20762: spell_id = 20758; break; // rank 2 + case 20763: spell_id = 20759; break; // rank 3 + case 20764: spell_id = 20760; break; // rank 4 + case 20765: spell_id = 20761; break; // rank 5 + case 27239: spell_id = 27240; break; // rank 6 + case 47883: spell_id = 47882; break; // rank 7 + default: + TC_LOG_ERROR("entities.player", "Unhandled spell %u: S.Resurrection", (*itr)->GetId()); + continue; + } + + prio = 3; + } + // Twisting Nether // prio: 2 (max) + else if ((*itr)->GetId() == 23701 && roll_chance_i(10)) + { + prio = 2; + spell_id = 23700; + } + } + + // Reincarnation (passive spell) // prio: 1 // Glyph of Renewed Life + if (prio < 1 && HasSpell(20608) && !GetSpellHistory()->HasCooldown(21169) && (HasAura(58059) || HasItemCount(17030))) + spell_id = 21169; + + return spell_id; +} + +// Used in triggers for check "Only to targets that grant experience or honor" req +bool Player::isHonorOrXPTarget(Unit const* victim) const +{ + uint8 v_level = victim->getLevel(); + uint8 k_grey = Trinity::XP::GetGrayLevel(getLevel()); + + // Victim level less gray level + if (v_level <= k_grey) + return false; + + if (Creature const* creature = victim->ToCreature()) + { + if (creature->IsCritter() || creature->IsTotem()) + return false; + } + return true; +} + +bool Player::GetsRecruitAFriendBonus(bool forXP) +{ + bool recruitAFriend = false; + if (getLevel() <= sWorld->getIntConfig(CONFIG_MAX_RECRUIT_A_FRIEND_BONUS_PLAYER_LEVEL) || !forXP) + { + if (Group* group = GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* player = itr->GetSource(); + if (!player) + continue; + + if (!player->IsAtRecruitAFriendDistance(this)) + continue; // member (alive or dead) or his corpse at req. distance + + if (forXP) + { + // level must be allowed to get RaF bonus + if (player->getLevel() > sWorld->getIntConfig(CONFIG_MAX_RECRUIT_A_FRIEND_BONUS_PLAYER_LEVEL)) + continue; + + // level difference must be small enough to get RaF bonus, UNLESS we are lower level + if (player->getLevel() < getLevel()) + if (uint8(getLevel() - player->getLevel()) > sWorld->getIntConfig(CONFIG_MAX_RECRUIT_A_FRIEND_BONUS_PLAYER_LEVEL_DIFFERENCE)) + continue; + } + + bool ARecruitedB = (player->GetSession()->GetRecruiterId() == GetSession()->GetAccountId()); + bool BRecruitedA = (GetSession()->GetRecruiterId() == player->GetSession()->GetAccountId()); + if (ARecruitedB || BRecruitedA) + { + recruitAFriend = true; + break; + } + } + } + } + return recruitAFriend; +} + +void Player::RewardPlayerAndGroupAtKill(Unit* victim, bool isBattleGround) +{ + KillRewarder(this, victim, isBattleGround).Reward(); +} + +void Player::RewardPlayerAndGroupAtEvent(uint32 creature_id, WorldObject* pRewardSource) +{ + if (!pRewardSource) + return; + + ObjectGuid creature_guid = (pRewardSource->GetTypeId() == TYPEID_UNIT) ? pRewardSource->GetGUID() : ObjectGuid::Empty; + + // prepare data for near group iteration + if (Group* group = GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* player = itr->GetSource(); + if (!player) + continue; + + if (!player->IsAtGroupRewardDistance(pRewardSource)) + continue; // member (alive or dead) or his corpse at req. distance + + // quest objectives updated only for alive group member or dead but with not released body + if (player->IsAlive()|| !player->GetCorpse()) + player->KilledMonsterCredit(creature_id, creature_guid); + } + } + else // if (!group) + KilledMonsterCredit(creature_id, creature_guid); +} + +bool Player::IsAtGroupRewardDistance(WorldObject const* pRewardSource) const +{ + if (!pRewardSource || !IsInMap(pRewardSource)) + return false; + + WorldObject const* player = GetCorpse(); + if (!player || IsAlive()) + player = this; + + if (pRewardSource->GetMap()->IsDungeon()) + return true; + + return pRewardSource->GetDistance(player) <= sWorld->getFloatConfig(CONFIG_GROUP_XP_DISTANCE); +} + +bool Player::IsAtRecruitAFriendDistance(WorldObject const* pOther) const +{ + if (!pOther || !IsInMap(pOther)) + return false; + + WorldObject const* player = GetCorpse(); + if (!player || IsAlive()) + player = this; + + return pOther->GetDistance(player) <= sWorld->getFloatConfig(CONFIG_MAX_RECRUIT_A_FRIEND_DISTANCE); +} + +void Player::ResurrectUsingRequestData() +{ + // Teleport before resurrecting by player, otherwise the player might get attacked from creatures near his corpse + float x, y, z, o; + _resurrectionData->Location.GetPosition(x, y, z, o); + TeleportTo(_resurrectionData->Location.GetMapId(), x, y, z, o); + + if (IsBeingTeleported()) + { + ScheduleDelayedOperation(DELAYED_RESURRECT_PLAYER); + return; + } + + uint32 resurrectHealth = _resurrectionData->Health; + uint32 resurrectMana = _resurrectionData->Mana; + + ResurrectPlayer(0.0f, false); + + SetHealth(resurrectHealth); + SetPower(POWER_MANA, resurrectMana); + + SetPower(POWER_RAGE, 0); + SetFullPower(POWER_ENERGY); + SetFullPower(POWER_FOCUS); + SetPower(POWER_ECLIPSE, 0); + + if (uint32 aura = _resurrectionData->Aura) + { + CastSpellExtraArgs args(TRIGGERED_FULL_MASK); + args.SetOriginalCaster(_resurrectionData->GUID); + CastSpell(this, aura, args); + } + + SpawnCorpseBones(); +} + +void Player::SetClientControl(Unit* target, bool allowMove) +{ + // a player can never client control nothing + ASSERT(target); + + // don't allow possession to be overridden + if (target->HasUnitState(UNIT_STATE_CHARMED) && (GetGUID() != target->GetCharmerGUID())) + { + TC_LOG_ERROR("entities.player", "Player '%s' attempt to client control '%s', which is charmed by GUID %s", + GetName().c_str(), target->GetName().c_str(), target->GetCharmerGUID().ToString().c_str()); + return; + } + + // still affected by some aura that shouldn't allow control, only allow on last such aura to be removed + if (target->HasUnitState(UNIT_STATE_FLEEING | UNIT_STATE_CONFUSED)) + allowMove = false; + + WorldPacket data(SMSG_CLIENT_CONTROL_UPDATE, target->GetPackGUID().size()+1); + data << target->GetPackGUID(); + data << uint8(allowMove ? 1 : 0); + SendDirectMessage(&data); + + WorldObject* viewpoint = GetViewpoint(); + if (!viewpoint) + viewpoint = this; + if (target != viewpoint) + { + if (viewpoint != this) + SetViewpoint(viewpoint, false); + if (target != this) + SetViewpoint(target, true); + } + + GetGameClient()->SetMovedUnit(target, allowMove); + + if (allowMove) + SendDirectMessage(WorldPackets::Movement::MoveSetActiveMover(target->GetGUID()).Write()); +} + +void Player::UpdateZoneDependentAuras(uint32 newZone) +{ + // Some spells applied at enter into zone (with subzones), aura removed in UpdateAreaDependentAuras that called always at zone->area update + SpellAreaForAreaMapBounds saBounds = sSpellMgr->GetSpellAreaForAreaMapBounds(newZone); + for (SpellAreaForAreaMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) + if (itr->second->flags & SPELL_AREA_FLAG_AUTOCAST && itr->second->IsFitToRequirements(this, newZone, 0)) + if (!HasAura(itr->second->spellId)) + CastSpell(this, itr->second->spellId, true); +} + +void Player::UpdateAreaDependentAuras(uint32 newArea) +{ + // remove auras from spells with area limitations + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + // use m_zoneUpdateId for speed: UpdateArea called from UpdateZone or instead UpdateZone in both cases m_zoneUpdateId up-to-date + if (iter->second->GetSpellInfo()->CheckLocation(GetMapId(), m_zoneUpdateId, newArea, this) != SPELL_CAST_OK) + RemoveOwnedAura(iter); + else + ++iter; + } + + // UpdateZoneDependentAuras takes care of zone specific auras so skip excessive casting if the area Id is the same as its parent. + if (m_zoneUpdateId == newArea) + return; + + // some auras applied at subzone enter + SpellAreaForAreaMapBounds saBounds = sSpellMgr->GetSpellAreaForAreaMapBounds(newArea); + for (SpellAreaForAreaMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) + if (itr->second->flags & SPELL_AREA_FLAG_AUTOCAST && itr->second->IsFitToRequirements(this, m_zoneUpdateId, newArea)) + if (!HasAura(itr->second->spellId)) + CastSpell(this, itr->second->spellId, true); +} + +uint32 Player::GetCorpseReclaimDelay(bool pvp) const +{ + if (pvp) + { + if (!sWorld->getBoolConfig(CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVP)) + return CorpseReclaimDelay[0]; + } + else if (!sWorld->getBoolConfig(CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVE)) + return 0; + + time_t now = GameTime::GetGameTime(); + // 0..2 full period + // should be ceil(x)-1 but not floor(x) + uint64 count = (now < m_deathExpireTime - 1) ? (m_deathExpireTime - 1 - now) / DEATH_EXPIRE_STEP : 0; + return CorpseReclaimDelay[count]; +} + +void Player::UpdateCorpseReclaimDelay() +{ + bool pvp = (m_ExtraFlags & PLAYER_EXTRA_PVP_DEATH) != 0; + + if ((pvp && !sWorld->getBoolConfig(CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVP)) || + (!pvp && !sWorld->getBoolConfig(CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVE))) + return; + + time_t now = GameTime::GetGameTime(); + + if (now < m_deathExpireTime) + { + // full and partly periods 1..3 + uint64 count = (m_deathExpireTime - now) / DEATH_EXPIRE_STEP + 1; + + if (count < MAX_DEATH_COUNT) + m_deathExpireTime = now+(count + 1) * DEATH_EXPIRE_STEP; + else + m_deathExpireTime = now + MAX_DEATH_COUNT*DEATH_EXPIRE_STEP; + } + else + m_deathExpireTime = now + DEATH_EXPIRE_STEP; +} + +int32 Player::CalculateCorpseReclaimDelay(bool load) const +{ + Corpse* corpse = GetCorpse(); + + if (load && !corpse) + return -1; + + bool pvp = corpse ? corpse->GetType() == CORPSE_RESURRECTABLE_PVP : (m_ExtraFlags & PLAYER_EXTRA_PVP_DEATH) != 0; + + uint32 delay; + + if (load) + { + if (corpse->GetGhostTime() > m_deathExpireTime) + return -1; + + uint64 count = 0; + + if ((pvp && sWorld->getBoolConfig(CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVP)) || + (!pvp && sWorld->getBoolConfig(CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVE))) + { + count = (m_deathExpireTime - corpse->GetGhostTime()) / DEATH_EXPIRE_STEP; + + if (count >= MAX_DEATH_COUNT) + count = MAX_DEATH_COUNT - 1; + } + + time_t expected_time = corpse->GetGhostTime() + CorpseReclaimDelay[count]; + time_t now = GameTime::GetGameTime(); + + if (now >= expected_time) + return -1; + + delay = expected_time - now; + } + else + delay = GetCorpseReclaimDelay(pvp); + + return delay * IN_MILLISECONDS; +} + +void Player::SendCorpseReclaimDelay(uint32 delay) const +{ + WorldPackets::Misc::CorpseReclaimDelay packet; + packet.Remaining = delay; + SendDirectMessage(packet.Write()); +} + +Player* Player::GetNextRandomRaidMember(float radius) +{ + Group* group = GetGroup(); + if (!group) + return nullptr; + + std::vector nearMembers; + nearMembers.reserve(group->GetMembersCount()); + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* Target = itr->GetSource(); + + // IsHostileTo check duel and controlled by enemy + if (Target && Target != this && IsWithinDistInMap(Target, radius) && + !Target->HasInvisibilityAura() && !IsHostileTo(Target)) + nearMembers.push_back(Target); + } + + if (nearMembers.empty()) + return nullptr; + + uint32 randTarget = urand(0, nearMembers.size()-1); + return nearMembers[randTarget]; +} + +PartyResult Player::CanUninviteFromGroup(ObjectGuid guidMember) const +{ + Group const* grp = GetGroup(); + if (!grp) + return ERR_NOT_IN_GROUP; + + if (grp->isLFGGroup()) + { + ObjectGuid gguid = grp->GetGUID(); + if (!sLFGMgr->GetKicksLeft(gguid)) + return ERR_PARTY_LFG_BOOT_LIMIT; + + lfg::LfgState state = sLFGMgr->GetState(gguid); + if (sLFGMgr->IsVoteKickActive(gguid)) + return ERR_PARTY_LFG_BOOT_IN_PROGRESS; + + if (grp->GetMembersCount() <= lfg::LFG_GROUP_KICK_VOTES_NEEDED) + return ERR_PARTY_LFG_BOOT_TOO_FEW_PLAYERS; + + if (state == lfg::LFG_STATE_FINISHED_DUNGEON) + return ERR_PARTY_LFG_BOOT_DUNGEON_COMPLETE; + + if (grp->isRollLootActive()) + return ERR_PARTY_LFG_BOOT_LOOT_ROLLS; + + /// @todo Should also be sent when anyone has recently left combat, with an aprox ~5 seconds timer. + for (GroupReference const* itr = grp->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (itr->GetSource() && itr->GetSource()->IsInMap(this) && itr->GetSource()->IsInCombat()) + return ERR_PARTY_LFG_BOOT_IN_COMBAT; + + /* Missing support for these types + return ERR_PARTY_LFG_BOOT_COOLDOWN_S; + return ERR_PARTY_LFG_BOOT_NOT_ELIGIBLE_S; + */ + } + else + { + if (!grp->IsLeader(GetGUID()) && !grp->IsAssistant(GetGUID())) + return ERR_NOT_LEADER; + + if (InBattleground()) + return ERR_INVITE_RESTRICTED; + + if (grp->IsLeader(guidMember)) + return ERR_NOT_LEADER; + } + + return ERR_PARTY_RESULT_OK; +} + +bool Player::isUsingLfg() const +{ + return sLFGMgr->GetState(GetGUID()) != lfg::LFG_STATE_NONE; +} + +bool Player::inRandomLfgDungeon() const +{ + if (sLFGMgr->selectedRandomLfgDungeon(GetGUID())) + { + Map const* map = GetMap(); + return sLFGMgr->inLfgDungeonMap(GetGUID(), map->GetId(), map->GetDifficulty()); + } + + return false; +} + +void Player::SetBattlegroundOrBattlefieldRaid(Group* group, int8 subgroup) +{ + //we must move references from m_group to m_originalGroup + SetOriginalGroup(GetGroup(), GetSubGroup()); + + m_group.unlink(); + m_group.link(group, this); + m_group.setSubGroup((uint8)subgroup); +} + +void Player::RemoveFromBattlegroundOrBattlefieldRaid() +{ + //remove existing reference + m_group.unlink(); + if (Group* group = GetOriginalGroup()) + { + m_group.link(group, this); + m_group.setSubGroup(GetOriginalSubGroup()); + } + SetOriginalGroup(nullptr); +} + +void Player::SetOriginalGroup(Group* group, int8 subgroup) +{ + if (group == nullptr) + m_originalGroup.unlink(); + else + { + // never use SetOriginalGroup without a subgroup unless you specify nullptr for group + ASSERT(subgroup >= 0); + m_originalGroup.link(group, this); + m_originalGroup.setSubGroup((uint8)subgroup); + } +} + +void Player::ProcessTerrainStatusUpdate(ZLiquidStatus oldLiquidStatus, Optional const& newLiquidData) +{ + // process liquid auras using generic unit code + Unit::ProcessTerrainStatusUpdate(oldLiquidStatus, newLiquidData); + + // player specific logic for mirror timers + if (GetLiquidStatus() && newLiquidData) + { + // Breath bar state (under water in any liquid type) + if (newLiquidData->type_flags.HasFlag(map_liquidHeaderTypeFlags::AllLiquids)) + { + if (GetLiquidStatus() & LIQUID_MAP_UNDER_WATER) + m_MirrorTimerFlags |= UNDERWATER_INWATER; + else + m_MirrorTimerFlags &= ~UNDERWATER_INWATER; + } + + // Fatigue bar state (if not on flight path or transport) + if (newLiquidData->type_flags.HasFlag(map_liquidHeaderTypeFlags::DarkWater) && !IsInFlight() && !GetTransport()) + m_MirrorTimerFlags |= UNDERWATER_INDARKWATER; + else + m_MirrorTimerFlags &= ~UNDERWATER_INDARKWATER; + + // Lava state (any contact) + if (newLiquidData->type_flags.HasFlag(map_liquidHeaderTypeFlags::Magma)) + { + if (GetLiquidStatus() & MAP_LIQUID_STATUS_IN_CONTACT) + m_MirrorTimerFlags |= UNDERWATER_INLAVA; + else + m_MirrorTimerFlags &= ~UNDERWATER_INLAVA; + } + + // Slime state (any contact) + if (newLiquidData->type_flags.HasFlag(map_liquidHeaderTypeFlags::Slime)) + { + if (GetLiquidStatus() & MAP_LIQUID_STATUS_IN_CONTACT) + m_MirrorTimerFlags |= UNDERWATER_INSLIME; + else + m_MirrorTimerFlags &= ~UNDERWATER_INSLIME; + } + } + else + m_MirrorTimerFlags &= ~(UNDERWATER_INWATER | UNDERWATER_INLAVA | UNDERWATER_INSLIME | UNDERWATER_INDARKWATER); + +} + +void Player::AtExitCombat() +{ + Unit::AtExitCombat(); + UpdatePotionCooldown(); + + if (getClass() == CLASS_DEATH_KNIGHT) + for (uint8 i = 0; i < MAX_RUNES; ++i) + { + SetRuneTimer(i, 0xFFFFFFFF); + SetLastRuneGraceTimer(i, 0); + } +} + +void Player::SetCanParry(bool value) +{ + if (m_canParry == value) + return; + + m_canParry = value; + UpdateParryPercentage(); +} + +void Player::SetCanBlock(bool value) +{ + if (m_canBlock == value) + return; + + m_canBlock = value; + UpdateBlockPercentage(); +} + +bool ItemPosCount::isContainedIn(std::vector const& vec) const +{ + for (ItemPosCountVec::const_iterator itr = vec.begin(); itr != vec.end(); ++itr) + if (itr->pos == pos) + return true; + return false; +} + +void Player::StopCastingBindSight() const +{ + if (WorldObject* target = GetViewpoint()) + { + if (target->isType(TYPEMASK_UNIT)) + { + static_cast(target)->RemoveAurasByType(SPELL_AURA_BIND_SIGHT, GetGUID()); + static_cast(target)->RemoveAurasByType(SPELL_AURA_MOD_POSSESS, GetGUID()); + } + } +} + +void Player::SetViewpoint(WorldObject* target, bool apply) +{ + if (apply) + { + TC_LOG_DEBUG("maps", "Player::CreateViewpoint: Player '%s' (%s) creates seer (Entry: %u, TypeId: %u).", + GetName().c_str(), GetGUID().ToString().c_str(), target->GetEntry(), target->GetTypeId()); + + if (!AddGuidValue(PLAYER_FARSIGHT, target->GetGUID())) + { + TC_LOG_FATAL("entities.player", "Player::CreateViewpoint: Player '%s' (%s) cannot add new viewpoint!", GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + // farsight dynobj or puppet may be very far away + UpdateVisibilityOf(target); + + if (target->isType(TYPEMASK_UNIT) && target != GetVehicleBase()) + static_cast(target)->AddPlayerToVision(this); + SetSeer(target); + } + else + { + TC_LOG_DEBUG("maps", "Player::CreateViewpoint: Player %s removed seer", GetName().c_str()); + + if (!RemoveGuidValue(PLAYER_FARSIGHT, target->GetGUID())) + { + TC_LOG_FATAL("entities.player", "Player::CreateViewpoint: Player '%s' (%s) cannot remove current viewpoint!", GetName().c_str(), GetGUID().ToString().c_str()); + return; + } + + if (target->isType(TYPEMASK_UNIT) && target != GetVehicleBase()) + static_cast(target)->RemovePlayerFromVision(this); + + //must immediately set seer back otherwise may crash + SetSeer(this); + + //WorldPacket data(SMSG_CLEAR_FAR_SIGHT_IMMEDIATE, 0); + //SendDirectMessage(&data); + } +} + +WorldObject* Player::GetViewpoint() const +{ + if (ObjectGuid guid = GetGuidValue(PLAYER_FARSIGHT)) + return static_cast(ObjectAccessor::GetObjectByTypeMask(*this, guid, TYPEMASK_SEER)); + return nullptr; +} + +bool Player::CanUseBattlegroundObject(GameObject* gameobject) const +{ + // It is possible to call this method with a null pointer, only skipping faction check. + if (gameobject) + { + FactionTemplateEntry const* playerFaction = GetFactionTemplateEntry(); + FactionTemplateEntry const* faction = sFactionTemplateStore.LookupEntry(gameobject->GetFaction()); + + if (playerFaction && faction && !playerFaction->IsFriendlyTo(faction)) + return false; + } + + // BUG: sometimes when player clicks on flag in AB - client won't send gameobject_use, only gameobject_report_use packet + // Note: Mount, stealth and invisibility will be removed when used + return (!isTotalImmune() && // Damage immune + !HasAura(SPELL_RECENTLY_DROPPED_FLAG) && // Still has recently held flag debuff + IsAlive()); // Alive +} + +bool Player::CanCaptureTowerPoint() const +{ + return (!HasStealthAura() && // not stealthed + !HasInvisibilityAura() && // not invisible + IsAlive()); // live player +} + +uint32 Player::GetBarberShopCost(uint8 newhairstyle, uint8 newhaircolor, uint8 newfacialhair, BarberShopStyleEntry const* newSkin) const +{ + uint8 level = getLevel(); + + if (level > GT_MAX_LEVEL) + level = GT_MAX_LEVEL; // max level in this dbc + + uint8 hairstyle = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_STYLE_ID); + uint8 haircolor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + uint8 facialhair = GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_FACIAL_STYLE); + uint8 skincolor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + + if ((hairstyle == newhairstyle) && (haircolor == newhaircolor) && (facialhair == newfacialhair) && (!newSkin || (newSkin->Data == skincolor))) + return 0; + + GtBarberShopCostBaseEntry const* bsc = sGtBarberShopCostBaseStore.LookupEntry(level - 1); + + if (!bsc) // shouldn't happen + return 0xFFFFFFFF; + + float cost = 0; + + if (hairstyle != newhairstyle) + cost += bsc->cost; // full price + + if ((haircolor != newhaircolor) && (hairstyle == newhairstyle)) + cost += bsc->cost * 0.5f; // +1/2 of price + + if (facialhair != newfacialhair) + cost += bsc->cost * 0.75f; // +3/4 of price + + if (newSkin && skincolor != newSkin->Data) + cost += bsc->cost * 0.75f; // +5/6 of price + + return uint32(cost); +} + +void Player::InitGlyphsForLevel() +{ + uint32 slot = 0; + for (uint32 i = 0; i < sGlyphSlotStore.GetNumRows() && slot < MAX_GLYPH_SLOT_INDEX; ++i) + if (GlyphSlotEntry const* gs = sGlyphSlotStore.LookupEntry(i)) + SetGlyphSlot(slot++, gs->ID); + + uint8 level = getLevel(); + uint32 slotMask = 0; + + if (level >= 25) + slotMask |= 0x01 | 0x02 | 0x40; + if (level >= 50) + slotMask |= 0x04 | 0x08 | 0x80; + if (level >= 75) + slotMask |= 0x10 | 0x20 | 0x100; + + SetUInt32Value(PLAYER_GLYPHS_ENABLED, slotMask); +} + +void Player::SetGlyph(uint8 slot, uint32 glyph) +{ + _talentMgr->SpecInfo[GetActiveSpec()].Glyphs[slot] = glyph; + SetUInt32Value(PLAYER_FIELD_GLYPHS_1 + slot, glyph); +} + +uint32 Player::GetGlyph(uint8 spec, uint8 slot) const +{ + return _talentMgr->SpecInfo[spec].Glyphs[slot]; +} + +PlayerTalentMap const& Player::GetTalentMap(uint8 spec) const +{ + return _talentMgr->SpecInfo[spec].Talents; +} +PlayerTalentMap& Player::GetTalentMap(uint8 spec) +{ + return _talentMgr->SpecInfo[spec].Talents; +} + +bool Player::isTotalImmune() const +{ + AuraEffectList const& immune = GetAuraEffectsByType(SPELL_AURA_SCHOOL_IMMUNITY); + + uint32 immuneMask = 0; + for (AuraEffectList::const_iterator itr = immune.begin(); itr != immune.end(); ++itr) + { + immuneMask |= (*itr)->GetMiscValue(); + if (immuneMask & SPELL_SCHOOL_MASK_ALL) // total immunity + return true; + } + return false; +} + +bool Player::HasTitle(uint32 bitIndex) const +{ + // bitIndex is zero-based inside PLAYER__FIELD_KNOWN_TITLES. + // MAX_TITLE_INDEX is the total number of available bits (valid range: [0, MAX_TITLE_INDEX)). + if (bitIndex >= MAX_TITLE_INDEX) + return false; + + uint32 fieldIndexOffset = bitIndex / 32; + uint32 flag = 1 << (bitIndex % 32); + return HasFlag(PLAYER__FIELD_KNOWN_TITLES + fieldIndexOffset, flag); +} + +bool Player::HasTitle(CharTitlesEntry const* title) const +{ + return HasTitle(title->Mask_ID); +} + +void Player::SetTitle(CharTitlesEntry const* title, bool lost) +{ + uint32 fieldIndexOffset = title->Mask_ID / 32; + uint32 flag = 1 << (title->Mask_ID % 32); + + if (lost) + { + if (!HasFlag(PLAYER__FIELD_KNOWN_TITLES + fieldIndexOffset, flag)) + return; + + RemoveFlag(PLAYER__FIELD_KNOWN_TITLES + fieldIndexOffset, flag); + } + else + { + if (HasFlag(PLAYER__FIELD_KNOWN_TITLES + fieldIndexOffset, flag)) + return; + + SetFlag(PLAYER__FIELD_KNOWN_TITLES + fieldIndexOffset, flag); + } + + WorldPacket data(SMSG_TITLE_EARNED, 4 + 4); + data << uint32(title->Mask_ID); + data << uint32(lost ? 0 : 1); // 1 - earned, 0 - lost + SendDirectMessage(&data); +} + +bool Player::isTotalImmunity() const +{ + AuraEffectList const& immune = GetAuraEffectsByType(SPELL_AURA_SCHOOL_IMMUNITY); + + for (AuraEffectList::const_iterator itr = immune.begin(); itr != immune.end(); ++itr) + { + if (((*itr)->GetMiscValue() & SPELL_SCHOOL_MASK_ALL) !=0) // total immunity + { + return true; + } + if (((*itr)->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL) !=0) // physical damage immunity + { + for (AuraEffectList::const_iterator i = immune.begin(); i != immune.end(); ++i) + { + if (((*i)->GetMiscValue() & SPELL_SCHOOL_MASK_MAGIC) !=0) // magic immunity + { + return true; + } + } + } + } + return false; +} + +void Runes::SetRuneState(uint8 index, bool set /*= true*/) +{ + if (set) + RuneState |= (1 << index); // usable + else + RuneState &= ~(1 << index); // on cooldown +} + +void Player::ResyncRunes() const +{ + WorldPackets::Spells::ResyncRunes packet; + for (uint8 i = 0; i < MAX_RUNES; ++i) + { + WorldPackets::Spells::ResyncRune resyncRune; + resyncRune.RuneType = AsUnderlyingType(GetCurrentRune(i)); + resyncRune.Cooldown = uint8((1.0f - GetRuneCooldown(i)) * uint32(255)); + packet.Runes.emplace_back(resyncRune); + } + + SendDirectMessage(packet.Write()); +} + +void Player::AddRunePower(uint8 mask) const +{ + WorldPackets::Spells::AddRunePower packet; + packet.AddedRunesMask = mask; // mask (0x00-0x3F probably) + SendDirectMessage(packet.Write()); +} + +static constexpr RuneType runeSlotTypes[MAX_RUNES] = +{ + /*0*/ RuneType::Blood, + /*1*/ RuneType::Blood, + /*2*/ RuneType::Unholy, + /*3*/ RuneType::Unholy, + /*4*/ RuneType::Frost, + /*5*/ RuneType::Frost +}; + +void Player::InitRunes() +{ + if (getClass() != CLASS_DEATH_KNIGHT) + return; + + m_runes = std::make_unique(); + m_runes->RuneState = 0; + m_runes->LastUsedRune = RuneType::Blood; + m_runes->LastUsedRuneMask = 0; + + for (uint8 i = 0; i < MAX_RUNES; ++i) + { + SetBaseRune(i, runeSlotTypes[i]); // init base types + SetCurrentRune(i, runeSlotTypes[i]); // init current types + SetRuneCooldown(i, 0.0f); // reset cooldowns + SetRuneConvertAura(i, nullptr, SPELL_AURA_NONE, nullptr); + m_runes->SetRuneState(i); + } + + for (uint8 i = 0; i < AsUnderlyingType(RuneType::Max); ++i) + SetFloatValue(PLAYER_RUNE_REGEN_1 + i, 0.1f); // set a base regen timer equal to 10 sec +} + +bool Player::IsRuneFullyDepleted(uint8 index) const +{ + return G3D::fuzzyEq(GetRuneCooldown(index), RUNE_BASE_COOLDOWN); +} + +bool Player::HasFullyDepletedRune(RuneType runeType) const +{ + if (!m_runes) + return false; + + for (uint8 i = 0; i < MAX_RUNES; ++i) + { + if (GetBaseRune(i) != runeType) + continue; + + if (IsRuneFullyDepleted(i)) + return true; + } + + return false; +} + +void Player::SetRuneConvertAura(uint8 index, AuraEffect const* aura, AuraType auraType, SpellInfo const* spellInfo) +{ + m_runes->_Runes[index].ConvertAura = aura; + m_runes->_Runes[index].ConvertAuraType = auraType; + m_runes->_Runes[index].ConvertAuraInfo = spellInfo; +} + +void Player::AddRuneByAuraEffect(uint8 index, RuneType newType, AuraEffect const* aura, AuraType auraType, SpellInfo const* spellInfo) +{ + SetRuneConvertAura(index, aura, auraType, spellInfo); + ConvertRune(index, newType); +} + +void Player::SetRuneCooldown(uint8 index, float cooldown) +{ + m_runes->_Runes[index].Cooldown = cooldown; + m_runes->SetRuneState(index, (cooldown == 0) ? true : false); +} + +void Player::RemoveRunesByAuraEffect(AuraEffect const* aura) +{ + for (uint8 i = 0; i < MAX_RUNES; ++i) + { + if (m_runes->_Runes[i].ConvertAura == aura) + { + ConvertRune(i, GetBaseRune(i)); + SetRuneConvertAura(i, nullptr, SPELL_AURA_NONE, nullptr); + } + } +} + +void Player::RestoreBaseRune(uint8 index) +{ + SpellInfo const* spellInfo = m_runes->_Runes[index].ConvertAuraInfo; + AuraType type = m_runes->_Runes[index].ConvertAuraType; + // If rune was converted by a non-pasive aura that still active we should keep it converted + if (spellInfo) + { + if (!(spellInfo->Attributes & SPELL_ATTR0_PASSIVE)) + return; + + // Don't even convert aura for passive convertion + if (spellInfo->IsPassive() && type == SPELL_AURA_CONVERT_RUNE) + return; + } + + ConvertRune(index, GetBaseRune(index)); + SetRuneConvertAura(index, nullptr, SPELL_AURA_NONE, nullptr); +} + +void Player::ConvertRune(uint8 index, RuneType newType) +{ + SetCurrentRune(index, newType); + + WorldPackets::Spells::ConvertRune packet; + packet.Index = index; + packet.Rune = AsUnderlyingType(newType); + + SendDirectMessage(packet.Write()); +} + +void Player::SendConvertedRunes() +{ + if (!m_runes) + return; + + for (uint8 i = 0; i < MAX_RUNES; ++i) + { + if (GetBaseRune(i) != GetCurrentRune(i)) + { + WorldPackets::Spells::ConvertRune convertRune; + convertRune.Index = i; + convertRune.Rune = AsUnderlyingType(GetCurrentRune(i)); + SendDirectMessage(convertRune.Write()); + } + } +} + +void Player::AutoStoreLoot(uint8 bag, uint8 slot, uint32 loot_id, LootStore const& store, bool broadcast) +{ + Loot loot; + loot.FillLoot (loot_id, store, this, true); + + uint32 max_slot = loot.GetMaxSlotInLootFor(this); + for (uint32 i = 0; i < max_slot; ++i) + { + LootItem* lootItem = loot.LootItemInSlot(i, this); + + ItemPosCountVec dest; + InventoryResult msg = CanStoreNewItem(bag, slot, dest, lootItem->itemid, lootItem->count); + if (msg != EQUIP_ERR_OK && slot != NULL_SLOT) + msg = CanStoreNewItem(bag, NULL_SLOT, dest, lootItem->itemid, lootItem->count); + if (msg != EQUIP_ERR_OK && bag != NULL_BAG) + msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, lootItem->itemid, lootItem->count); + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, nullptr, nullptr, lootItem->itemid); + continue; + } + + Item* pItem = StoreNewItem(dest, lootItem->itemid, true, lootItem->randomPropertyId); + SendNewItem(pItem, lootItem->count, false, false, broadcast); + } +} + +void Player::StoreLootItem(ObjectGuid lootWorldObjectGuid, uint8 lootSlot, Loot* loot, GameObject* go) +{ + NotNormalLootItem* qitem = nullptr; + NotNormalLootItem* ffaitem = nullptr; + NotNormalLootItem* conditem = nullptr; + + LootItem* item = loot->LootItemInSlot(lootSlot, this, &qitem, &ffaitem, &conditem); + + if (!item) + { + SendEquipError(EQUIP_ERR_LOOT_GONE, nullptr, nullptr); + return; + } + + if (!item->AllowedForPlayer(this)) + { + SendLootRelease(GetLootGUID()); + return; + } + + // questitems use the blocked field for other purposes + if (!qitem && item->is_blocked) + { + SendLootRelease(GetLootGUID()); + return; + } + + // dont allow protected item to be looted by someone else + if (!item->rollWinnerGUID.IsEmpty() && item->rollWinnerGUID != GetGUID()) + { + SendLootRelease(GetLootGUID()); + return; + } + + ItemPosCountVec dest; + InventoryResult msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, item->itemid, item->count); + if (msg == EQUIP_ERR_OK) + { + GuidSet looters = item->GetAllowedLooters(); + Item* newitem = StoreNewItem(dest, item->itemid, true, item->randomPropertyId, looters); + + if (qitem) + { + qitem->is_looted = true; + //freeforall is 1 if everyone's supposed to get the quest item. + if (item->freeforall || loot->GetPlayerQuestItems().size() == 1) + SendNotifyLootItemRemoved(lootSlot); + else + loot->NotifyQuestItemRemoved(qitem->index); + } + else + { + if (ffaitem) + { + //freeforall case, notify only one player of the removal + ffaitem->is_looted = true; + SendNotifyLootItemRemoved(lootSlot); + } + else + { + //not freeforall, notify everyone + if (conditem) + conditem->is_looted = true; + loot->NotifyItemRemoved(lootSlot); + } + } + + //if only one person is supposed to loot the item, then set it to looted + if (!item->freeforall) + item->is_looted = true; + + --loot->unlootedCount; + + if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(item->itemid)) + if ((proto->GetQuality() > ITEM_QUALITY_EPIC // always log legendary items + || (proto->GetQuality() == ITEM_QUALITY_EPIC && proto->GetBaseItemLevel() >= MinNewsItemLevel[sWorld->getIntConfig(CONFIG_EXPANSION)])) // log epic items when within item level range + && !proto->GetDuration()) // log items only when they are permanent (skipping legendary items from Kael'thas encounter for example) + if (Guild* guild = GetGuild()) + guild->AddGuildNews(GUILD_NEWS_ITEM_LOOTED, GetGUID(), 0, item->itemid); + + SendNewItem(newitem, uint32(item->count), false, false, true); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_ITEM, item->itemid, item->count); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_TYPE, item->itemid, item->count, loot->loot_type); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_EPIC_ITEM, item->itemid, item->count); + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_CATCH_FROM_POOL, item->itemid, item->count, 0, nullptr, go); + + // LootItem is being removed (looted) from the container, delete it from the DB. + if (lootWorldObjectGuid.IsItem() && loot->loot_type == LOOT_CORPSE) + sLootItemStorage->RemoveStoredLootItemForContainer(lootWorldObjectGuid.GetCounter(), item->itemid, item->count, item->itemIndex); + + } + else + SendEquipError(msg, nullptr, nullptr, item->itemid); +} + +uint32 Player::CalculateTalentsPoints() const +{ + // this dbc file has entries only up to level 100 + NumTalentsAtLevelEntry const* count = sNumTalentsAtLevelStore.LookupEntry(std::min(getLevel(), 100)); + if (!count) + return 0; + + float baseForLevel = count->NumberOfTalents; + + if (getClass() != CLASS_DEATH_KNIGHT || GetMapId() != 609) + return uint32(baseForLevel * sWorld->getRate(RATE_TALENT)); + + // Death Knight starting level + // hardcoded here - number of quest awarded talents is equal to number of talents any other class would have at level 55 + if (getLevel() < 55) + return 0; + + NumTalentsAtLevelEntry const* dkBase = sNumTalentsAtLevelStore.LookupEntry(55); + if (!dkBase) + return 0; + + float talentPointsForLevel = count->NumberOfTalents - dkBase->NumberOfTalents; + talentPointsForLevel += float(GetQuestRewardedTalentCount()); + + if (talentPointsForLevel > baseForLevel) + talentPointsForLevel = baseForLevel; + + return uint32(talentPointsForLevel * sWorld->getRate(RATE_TALENT)); +} + +void Player::LearnSpellHighestRank(uint32 spellid) +{ + LearnSpell(spellid, false); + + if (uint32 next = sSpellMgr->GetNextSpellInChain(spellid)) + LearnSpellHighestRank(next); +} + +void Player::_LoadSkills(PreparedQueryResult result) +{ + // 0 1 2 + // SetPQuery(PLAYER_LOGIN_QUERY_LOADSKILLS, "SELECT skill, value, max FROM character_skills WHERE guid = '%u'", GUID_LOPART(m_guid)); + + uint8 professionCount = 0; + std::unordered_map loadedSkillValues; + if (result) + { + do + { + if (mSkillStatus.size() >= PLAYER_MAX_SKILLS) // client limit + { + TC_LOG_ERROR("entities.player", "Player::_LoadSkills: Player '%s' (%s) has more than %u skills.", + GetName().c_str(), GetGUID().ToString().c_str(), PLAYER_MAX_SKILLS); + break; + } + + Field* fields = result->Fetch(); + uint16 skill = fields[0].GetUInt16(); + uint16 value = fields[1].GetUInt16(); + uint16 max = fields[2].GetUInt16(); + + SkillRaceClassInfoEntry const* rcEntry = sDBCManager.GetSkillRaceClassInfo(skill, getRace(), getClass()); + if (!rcEntry) + { + TC_LOG_ERROR("entities.player", "Player::_LoadSkills: Player '%s' (%s, Race: %u, Class: %u) has forbidden skill %u for his race/class combination", + GetName().c_str(), GetGUID().ToString().c_str(), uint32(getRace()), uint32(getClass()), skill); + + mSkillStatus.insert(SkillStatusMap::value_type(skill, SkillStatusData(mSkillStatus.size(), SKILL_DELETED))); + continue; + } + + // set fixed skill ranges + switch (GetSkillRangeType(rcEntry)) + { + case SKILL_RANGE_LANGUAGE: // 300..300 + value = max = 300; + break; + case SKILL_RANGE_MONO: // 1..1, grey monolite bar + value = max = 1; + break; + case SKILL_RANGE_LEVEL: + max = GetMaxSkillValueForLevel(); + break; + default: + break; + } + + auto skillItr = mSkillStatus.find(skill); + if (skillItr == mSkillStatus.end()) + skillItr = mSkillStatus.insert(SkillStatusMap::value_type(skill, SkillStatusData(mSkillStatus.size(), SKILL_UNCHANGED))).first; + + uint16 step = 0; + + if (SkillLineEntry const* skillLine = sSkillLineStore.LookupEntry(rcEntry->SkillID)) + { + if (skillLine->CategoryID == SKILL_CATEGORY_SECONDARY) + step = max / 75; + + if (skillLine->CategoryID == SKILL_CATEGORY_PROFESSION) + { + step = max / 75; + + if (professionCount < 2) + SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + professionCount++, skill); + } + } + + SetSkillLineId(skillItr->second.pos, skill); + SetSkillStep(skillItr->second.pos, step); + SetSkillRank(skillItr->second.pos, value); + SetSkillMaxRank(skillItr->second.pos, max); + SetSkillTempBonus(skillItr->second.pos, 0); + SetSkillPermBonus(skillItr->second.pos, 0); + + loadedSkillValues[skill] = value; + + if (skill == SKILL_ARCHAEOLOGY) + _archaeology->Initialize(); + } + while (result->NextRow()); + } + + // Learn skill rewarded spells after all skills have been loaded to prevent learning a skill from them before its loaded with proper value from DB + for (auto& skill : loadedSkillValues) + LearnSkillRewardedSpells(skill.first, skill.second); + + if (HasSkill(SKILL_FIST_WEAPONS)) + SetSkill(SKILL_FIST_WEAPONS, 0, GetSkillValue(SKILL_UNARMED), GetMaxSkillValueForLevel()); +} + +InventoryResult Player::CanEquipUniqueItem(Item* pItem, uint8 eslot, uint32 limit_count) const +{ + ItemTemplate const* pProto = pItem->GetTemplate(); + + // proto based limitations + if (InventoryResult res = CanEquipUniqueItem(pProto, eslot, limit_count)) + return res; + + // check unique-equipped on gems + for (uint32 enchant_slot = SOCK_ENCHANTMENT_SLOT; enchant_slot < SOCK_ENCHANTMENT_SLOT+3; ++enchant_slot) + { + uint32 enchant_id = pItem->GetEnchantmentId(EnchantmentSlot(enchant_slot)); + if (!enchant_id) + continue; + SpellItemEnchantmentEntry const* enchantEntry = sSpellItemEnchantmentStore.LookupEntry(enchant_id); + if (!enchantEntry) + continue; + + ItemTemplate const* pGem = sObjectMgr->GetItemTemplate(enchantEntry->Src_itemID); + if (!pGem) + continue; + + // include for check equip another gems with same limit category for not equipped item (and then not counted) + uint32 gem_limit_count = !pItem->IsEquipped() && pGem->GetItemLimitCategory() + ? pItem->GetGemCountWithLimitCategory(pGem->GetItemLimitCategory()) : 1; + + if (InventoryResult res = CanEquipUniqueItem(pGem, eslot, gem_limit_count)) + return res; + } + + return EQUIP_ERR_OK; +} + +InventoryResult Player::CanEquipUniqueItem(ItemTemplate const* itemProto, uint8 except_slot, uint32 limit_count) const +{ + // check unique-equipped on item + if (itemProto->GetFlags() & ITEM_FLAG_UNIQUE_EQUIPPABLE) + { + // there is an equip limit on this item + if (HasItemOrGemWithIdEquipped(itemProto->GetId(), 1, except_slot)) + return EQUIP_ERR_ITEM_UNIQUE_EQUIPPABLE; + } + + // check unique-equipped limit + if (itemProto->GetItemLimitCategory()) + { + ItemLimitCategoryEntry const* limitEntry = sItemLimitCategoryStore.LookupEntry(itemProto->GetItemLimitCategory()); + if (!limitEntry) + return EQUIP_ERR_NOT_EQUIPPABLE; + + // NOTE: limitEntry->mode not checked because if item have have-limit then it applied and to equip case + + if (limit_count > limitEntry->Quantity) + return EQUIP_ERR_ITEM_MAX_LIMIT_CATEGORY_EQUIPPED_EXCEEDED_IS; + + // there is an equip limit on this item + if (HasItemWithLimitCategoryEquipped(itemProto->GetItemLimitCategory(), limitEntry->Quantity - limit_count + 1, except_slot)) + return EQUIP_ERR_ITEM_MAX_LIMIT_CATEGORY_EQUIPPED_EXCEEDED_IS; + else if (HasGemWithLimitCategoryEquipped(itemProto->GetItemLimitCategory(), limitEntry->Quantity - limit_count + 1, except_slot)) + return EQUIP_ERR_ITEM_MAX_COUNT_EQUIPPED_SOCKETED; + } + + return EQUIP_ERR_OK; +} + +void Player::SetFallInformation(uint32 time, float z) +{ + m_lastFallTime = time; + m_lastFallZ = z; +} + +void Player::HandleFall(MovementInfo const& movementInfo) +{ + // calculate total z distance of the fall + float z_diff = m_lastFallZ - movementInfo.pos.GetPositionZ(); + //TC_LOG_DEBUG("misc", "zDiff = %f", z_diff); + + //Players with low fall distance, Feather Fall or physical immunity (charges used) are ignored + // 14.57 can be calculated by resolving damageperc formula below to 0 + if (z_diff >= 14.57f && !isDead() && !IsGameMaster() && + !HasAuraType(SPELL_AURA_HOVER) && !HasAuraType(SPELL_AURA_FEATHER_FALL) && + !HasAuraType(SPELL_AURA_FLY) && !IsImmunedToDamage(SPELL_SCHOOL_MASK_NORMAL)) + { + //Safe fall, fall height reduction + int32 safe_fall = GetTotalAuraModifier(SPELL_AURA_SAFE_FALL); + + float damageperc = 0.018f*(z_diff-safe_fall)-0.2426f; + + if (damageperc > 0) + { + uint32 damage = (uint32)(damageperc * GetMaxHealth()*sWorld->getRate(RATE_DAMAGE_FALL)); + + if (GetCommandStatus(CHEAT_GOD)) + damage = 0; + + float height = movementInfo.pos.m_positionZ; + UpdateGroundPositionZ(movementInfo.pos.m_positionX, movementInfo.pos.m_positionY, height); + + if (damage > 0) + { + //Prevent fall damage from being more than the player maximum health + if (damage > GetMaxHealth()) + damage = GetMaxHealth(); + + // Gust of Wind + if (HasAura(43621)) + damage = GetMaxHealth()/2; + + uint32 original_health = GetHealth(); + uint32 final_damage = EnvironmentalDamage(DAMAGE_FALL, damage); + + // recheck alive, might have died of EnvironmentalDamage, avoid cases when player die in fact like Spirit of Redemption case + if (IsAlive() && final_damage < original_health) + UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_FALL_WITHOUT_DYING, uint32(z_diff*100)); + } + + //Z given by moveinfo, LastZ, FallTime, WaterZ, MapZ, Damage, Safefall reduction + TC_LOG_DEBUG("entities.player", "FALLDAMAGE z=%f sz=%f pZ=%f FallTime=%d mZ=%f damage=%d SF=%d", movementInfo.pos.GetPositionZ(), height, GetPositionZ(), movementInfo.jump.fallTime, height, damage, safe_fall); + } + } +} + +bool Player::SetDisableGravity(bool disable, bool /*updateAnimTier = true*/) +{ + if (disable == IsGravityDisabled()) + return false; + + if (IsMovedByClient() && IsInWorld()) + MovementPacketSender::SendMovementFlagChangeToMover(this, MOVEMENTFLAG_DISABLE_GRAVITY, disable); + + Unit::SetDisableGravity(disable, false); + + return true; +} + +bool Player::SetCanFly(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_CAN_FLY)) + return false; + + if (IsMovedByClient() && IsInWorld()) + MovementPacketSender::SendMovementFlagChangeToMover(this, MOVEMENTFLAG_CAN_FLY, enable); + + Unit::SetCanFly(enable); + + return true; +} + +bool Player::SetCanTransitionBetweenSwimAndFly(bool enable) +{ + if (enable == HasExtraUnitMovementFlag(MOVEMENTFLAG2_CAN_SWIM_TO_FLY_TRANS)) + return false; + + if (IsMovedByClient() && IsInWorld()) + MovementPacketSender::SendMovementFlagChangeToMover(this, MOVEMENTFLAG2_CAN_SWIM_TO_FLY_TRANS, enable); + + Unit::SetCanTransitionBetweenSwimAndFly(enable); + + return true; +} + +void Player::SendMovementSetCollisionHeight(float height, UpdateCollisionHeightReason reason) +{ + if (IsMovedByClient() && IsInWorld()) + MovementPacketSender::SendHeightChangeToMover(this, height, reason); +} + +void Player::ResetAchievements() +{ + m_achievementMgr->Reset(); +} + +void Player::SendRespondInspectAchievements(Player* player) const +{ + m_achievementMgr->SendAchievementInfo(player); +} + +uint32 Player::GetAchievementPoints() const +{ + return m_achievementMgr->GetAchievementPoints(); +} + +bool Player::HasAchieved(uint32 achievementId) const +{ + return m_achievementMgr->HasAchieved(achievementId); +} + +void Player::StartAchievementCriteria(AchievementCriteriaStartEvent startEvent, uint32 entry, Milliseconds timeLost/* = Milliseconds::zero()*/) +{ + m_achievementMgr->StartAchievementCriteria(startEvent, entry, timeLost); +} + +void Player::FailAchievementCriteria(AchievementCriteriaFailEvent failEvent, int32 failAsset) +{ + m_achievementMgr->FailAchievementCriteria(failEvent, failAsset); +} + +void Player::UpdateAchievementCriteria(AchievementCriteriaTypes type, uint64 miscValue1 /*= 0*/, uint64 miscValue2 /*= 0*/, uint64 miscValue3 /*= 0*/, WorldObject* ref /*= nullptr*/, GameObject* go /*= nullptr*/) +{ + m_achievementMgr->UpdateAchievementCriteria(type, miscValue1, miscValue2, miscValue3, ref, this); + Guild* guild = sGuildMgr->GetGuildById(GetGuildId()); + if (!guild) + return; + + // Update only individual achievement criteria here, otherwise we may get multiple updates + // from a single boss kill + if (sAchievementMgr->IsGroupCriteriaType(type)) + return; + + guild->UpdateAchievementCriteria(type, miscValue1, miscValue2, miscValue3, ref, this, go); +} + +void Player::CompletedAchievement(AchievementEntry const* entry) +{ + m_achievementMgr->CompletedAchievement(entry, this); +} + +bool Player::LearnTalent(uint32 talentId, uint32 talentRank) +{ + if (talentId == 0 || talentRank >= MAX_TALENT_RANK) + return false; + + uint32 freeTalentPoints = GetFreeTalentPoints(); + if (freeTalentPoints == 0) + return false; + + TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentId); + if (!talentInfo) + return false; + + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentInfo->TabID); + if (!talentTabInfo) + return false; + + // prevent learn talent for different class (cheating) + if ((getClassMask() & talentTabInfo->ClassMask) == 0) + return false; + + // find current max talent rank (0~5) + uint8 curtalent_maxrank = 0; // 0 = not learned any rank + for (int8 rank = MAX_TALENT_RANK-1; rank >= 0; --rank) + { + if (talentInfo->SpellRank[rank] && HasSpell(talentInfo->SpellRank[rank])) + { + curtalent_maxrank = (rank + 1); + break; + } + } + + // we already have same or higher talent rank learned + if (curtalent_maxrank >= (talentRank + 1)) + return false; + + // check if we have enough talent points + if (freeTalentPoints < (talentRank - curtalent_maxrank + 1)) + return false; + + // Check if it requires another talent + if (talentInfo->PrereqTalent[0] > 0) + { + if (TalentEntry const* depTalentInfo = sTalentStore.LookupEntry(talentInfo->PrereqTalent[0])) + { + bool hasEnoughRank = false; + for (uint8 rank = talentInfo->PrereqRank[0]; rank < MAX_TALENT_RANK; rank++) + { + if (depTalentInfo->SpellRank[rank] != 0) + if (HasSpell(depTalentInfo->SpellRank[rank])) + hasEnoughRank = true; + } + if (!hasEnoughRank) + return false; + } + } + + // Find out how many points we have in this field + uint32 spentPoints = 0; + uint32 primaryTreeTalents = 0; + uint32 primaryTreeId = GetPrimaryTalentTree(GetActiveSpec()); + bool isMainTree = primaryTreeId == talentInfo->TabID || !primaryTreeId; + + if (talentInfo->TierID > 0 || !isMainTree) + { + for (uint32 i = 0; i < sTalentStore.GetNumRows(); i++) // Loop through all talents. + { + if (TalentEntry const* tmpTalent = sTalentStore.LookupEntry(i)) // Someday, someone needs to revamp the way talents are tracked + { + for (uint8 rank = 0; rank < MAX_TALENT_RANK; ++rank) + { + if (tmpTalent->SpellRank[rank] != 0) + { + if (HasSpell(tmpTalent->SpellRank[rank])) + { + if (tmpTalent->TabID == talentInfo->TabID) + spentPoints += (rank + 1); + if (tmpTalent->TabID == primaryTreeId) + primaryTreeTalents += (rank + 1); + } + } + } + } + } + } + + // not have required min points spent in talent tree + if (spentPoints < (talentInfo->TierID * MAX_TALENT_RANK)) + return false; + + // player has not spent 31 talents in main tree before attempting to learn other tree's talents + if (!isMainTree && primaryTreeTalents < REQ_PRIMARY_TREE_TALENTS) + return false; + + // spell not set in talent.dbc + uint32 spellid = talentInfo->SpellRank[talentRank]; + if (spellid == 0) + { + TC_LOG_ERROR("entities.player", "Player::LearnTalent: Talent.dbc has no spellInfo for talent: %u (spell id = 0)", talentId); + return false; + } + + // already known + if (HasSpell(spellid)) + return false; + + // learn! (other talent ranks will unlearned at learning) + LearnSpell(spellid, false); + AddTalent(spellid, GetActiveSpec(), true); + + TC_LOG_DEBUG("entities.player", "TalentID: %u Rank: %u Spell: %u Spec: %u\n", talentId, talentRank, spellid, GetActiveSpec()); + + // update free talent points + SetFreeTalentPoints(freeTalentPoints - (talentRank - curtalent_maxrank + 1)); + return true; +} + +void Player::LearnPetTalent(ObjectGuid petGuid, uint32 talentId, uint32 talentRank) +{ + Pet* pet = GetPet(); + + if (!pet) + return; + + if (petGuid != pet->GetGUID()) + return; + + uint32 CurTalentPoints = pet->GetFreeTalentPoints(); + + if (CurTalentPoints == 0) + return; + + if (talentRank >= MAX_PET_TALENT_RANK) + return; + + TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentId); + + if (!talentInfo) + return; + + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentInfo->TabID); + + if (!talentTabInfo) + return; + + CreatureTemplate const* ci = pet->GetCreatureTemplate(); + + if (!ci) + return; + + CreatureFamilyEntry const* pet_family = sCreatureFamilyStore.LookupEntry(ci->family); + + if (!pet_family) + return; + + if (pet_family->PetTalentType < 0) // not hunter pet + return; + + // prevent learn talent for different family (cheating) + if (!((1 << pet_family->PetTalentType) & talentTabInfo->CategoryEnumID)) + return; + + // find current max talent rank (0~5) + uint8 curtalent_maxrank = 0; // 0 = not learned any rank + for (int8 rank = MAX_TALENT_RANK-1; rank >= 0; --rank) + { + if (talentInfo->SpellRank[rank] && pet->HasSpell(talentInfo->SpellRank[rank])) + { + curtalent_maxrank = (rank + 1); + break; + } + } + + // we already have same or higher talent rank learned + if (curtalent_maxrank >= (talentRank + 1)) + return; + + // check if we have enough talent points + if (CurTalentPoints < (talentRank - curtalent_maxrank + 1)) + return; + + // Check if it requires another talent + if (talentInfo->PrereqTalent[0] > 0) + { + if (TalentEntry const* depTalentInfo = sTalentStore.LookupEntry(talentInfo->PrereqTalent[0])) + { + bool hasEnoughRank = false; + for (uint8 rank = talentInfo->PrereqRank[0]; rank < MAX_TALENT_RANK; rank++) + { + if (depTalentInfo->SpellRank[rank] != 0) + if (pet->HasSpell(depTalentInfo->SpellRank[rank])) + hasEnoughRank = true; + } + if (!hasEnoughRank) + return; + } + } + + // Find out how many points we have in this field + uint32 spentPoints = 0; + + uint32 tTab = talentInfo->TabID; + if (talentInfo->TierID > 0) + { + uint32 numRows = sTalentStore.GetNumRows(); + for (uint32 i = 0; i < numRows; ++i) // Loop through all talents. + { + // Someday, someone needs to revamp + TalentEntry const* tmpTalent = sTalentStore.LookupEntry(i); + if (tmpTalent) // the way talents are tracked + { + if (tmpTalent->TabID == tTab) + { + for (uint8 rank = 0; rank < MAX_TALENT_RANK; rank++) + { + if (tmpTalent->SpellRank[rank] != 0) + { + if (pet->HasSpell(tmpTalent->SpellRank[rank])) + { + spentPoints += (rank + 1); + } + } + } + } + } + } + } + + // not have required min points spent in talent tree + if (spentPoints < (talentInfo->TierID * MAX_PET_TALENT_RANK)) + return; + + // spell not set in talent.dbc + uint32 spellid = talentInfo->SpellRank[talentRank]; + if (spellid == 0) + { + TC_LOG_ERROR("entities.player", "Talent.dbc contains talent: %u Rank: %u spell id = 0", talentId, talentRank); + return; + } + + // already known + if (pet->HasSpell(spellid)) + return; + + // learn! (other talent ranks will unlearned at learning) + pet->learnSpell(spellid); + TC_LOG_DEBUG("entities.player", "PetTalentID: %u Rank: %u Spell: %u\n", talentId, talentRank, spellid); + + // update free talent points + pet->SetFreeTalentPoints(CurTalentPoints - (talentRank - curtalent_maxrank + 1)); +} + +bool Player::LearnPrimaryTalentSpecialization(uint8 talentTabIndex) +{ + if (talentTabIndex >= MAX_TALENT_TABS) + return false; + + if (GetPrimaryTalentTree(GetActiveSpec()) != 0) + return false; + + uint32 const* talentTabs = sDBCManager.GetTalentTabPages(getClass()); + if (!talentTabs) + return false; + + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentTabs[talentTabIndex]); + if (!talentTabInfo) + return false; + + SetPrimaryTalentTree(GetActiveSpec(), talentTabInfo->ID); + + // Learn specialization spells and mastery + if (std::vector const* specSpells = sDBCManager.GetTalentTreePrimarySpells(talentTabInfo->ID)) + for (uint32 spellId : *specSpells) + LearnSpell(spellId, false); + + if (CanUseMastery()) + for (uint8 i = 0; i < MAX_MASTERY_SPELLS; ++i) + if (SpellInfo const* masterySpell = sSpellMgr->GetSpellInfo(talentTabInfo->MasterySpellID[i])) + if (masterySpell->IsPassive() && HandlePassiveSpellLearn(masterySpell)) + CastSpell(this, masterySpell->Id, true); + + return true; +} + +void Player::AddKnownCurrency(uint32 itemId) +{ + if (CurrencyTypesEntry const* ctEntry = sCurrencyTypesStore.LookupEntry(itemId)) + SetFlag64(0, (1LL << (ctEntry->ID-1))); +} + +void Player::UpdateFallInformationIfNeed(MovementInfo const& minfo, uint16 opcode) +{ + if (m_lastFallTime >= minfo.jump.fallTime || m_lastFallZ <= minfo.pos.GetPositionZ() || opcode == MSG_MOVE_FALL_LAND) + SetFallInformation(minfo.jump.fallTime, minfo.pos.GetPositionZ()); +} + +void Player::UnsummonPetTemporaryIfAny() +{ + Pet* pet = GetPet(); + if (!pet) + return; + + if (!m_temporaryUnsummonedPetNumber && pet->isControlled() && !pet->isTemporarySummoned()) + { + m_temporaryUnsummonedPetNumber = pet->GetCharmInfo()->GetPetNumber(); + m_oldpetspell = pet->GetUInt32Value(UNIT_CREATED_BY_SPELL); + } + + RemovePet(pet, PET_SAVE_TEMP_UNSUMMON); +} + +void Player::ResummonPetTemporaryUnSummonedIfAny() +{ + if (!m_temporaryUnsummonedPetNumber) + return; + + // not resummon in not appropriate state + if (IsPetNeedBeTemporaryUnsummoned()) + return; + + if (GetPetGUID()) + return; + + Pet* NewPet = new Pet(this); + if (!NewPet->LoadPetData(this, 0, m_temporaryUnsummonedPetNumber, false)) + delete NewPet; + + m_temporaryUnsummonedPetNumber = 0; +} + +bool Player::IsPetNeedBeTemporaryUnsummoned() const +{ + return !IsInWorld() || !IsAlive() || IsMounted() /*+in flight*/; +} + +bool Player::CanSeeSpellClickOn(Creature const* c) const +{ + if (!c->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_SPELLCLICK)) + return false; + + SpellClickInfoMapBounds clickPair = sObjectMgr->GetSpellClickInfoMapBounds(c->GetEntry()); + if (clickPair.first == clickPair.second) + return true; + + for (SpellClickInfoContainer::const_iterator itr = clickPair.first; itr != clickPair.second; ++itr) + { + if (!itr->second.IsFitToRequirements(this, c)) + return false; + + if (sConditionMgr->IsObjectMeetingSpellClickConditions(c->GetEntry(), itr->second.spellId, const_cast(this), const_cast(c))) + return true; + } + + return false; +} + +uint8 Player::GetExpansionForFaction(uint32 faction) const +{ + FactionEntry const* baseFaction = sFactionStore.LookupEntry(faction); + if (!baseFaction || !baseFaction->ParentFactionID) + return 0; + + FactionEntry const* expansionFaction = sFactionStore.LookupEntry(baseFaction->ParentFactionID); + if (!expansionFaction) + return 0; + + return expansionFaction->Expansion; +} + +void Player::BuildPlayerTalentsInfoData(WorldPacket* data) +{ + *data << uint32(GetFreeTalentPoints()); // unspentTalentPoints + *data << uint8(GetSpecsCount()); // talent group count (0, 1 or 2) + *data << uint8(GetActiveSpec()); // talent group index (0 or 1) + + if (GetSpecsCount()) + { + if (GetSpecsCount() > MAX_TALENT_SPECS) + SetSpecsCount(MAX_TALENT_SPECS); + + // loop through all specs (only 1 for now) + for (uint8 specIdx = 0; specIdx < GetSpecsCount(); ++specIdx) + { + *data << uint32(GetPrimaryTalentTree(specIdx)); + uint8 talentIdCount = 0; + size_t pos = data->wpos(); + *data << uint8(talentIdCount); // [PH], talentIdCount + + // find class talent tabs (all players have 3 talent tabs) + uint32 const* talentTabIds = sDBCManager.GetTalentTabPages(getClass()); + + for (uint8 i = 0; i < MAX_TALENT_TABS; ++i) + { + uint32 talentTabId = talentTabIds[i]; + + for (uint32 talentId = 0; talentId < sTalentStore.GetNumRows(); ++talentId) + { + TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentId); + if (!talentInfo) + continue; + + // skip another tab talents + if (talentInfo->TabID != talentTabId) + continue; + + // find max talent rank (0~4) + int8 curtalent_maxrank = -1; + for (int8 rank = MAX_TALENT_RANK-1; rank >= 0; --rank) + { + if (talentInfo->SpellRank[rank] && HasTalent(talentInfo->SpellRank[rank], specIdx)) + { + curtalent_maxrank = rank; + break; + } + } + + // not learned talent + if (curtalent_maxrank < 0) + continue; + + *data << uint32(talentInfo->ID); // Talent.dbc + *data << uint8(curtalent_maxrank); // talentMaxRank (0-4) + + ++talentIdCount; + } + } + + data->put(pos, talentIdCount); // put real count + + *data << uint8(MAX_GLYPH_SLOT_INDEX); // glyphs count + + for (uint8 i = 0; i < MAX_GLYPH_SLOT_INDEX; ++i) + *data << uint16(GetGlyph(specIdx, i)); // GlyphProperties.dbc + } + } +} + +void Player::BuildPetTalentsInfoData(WorldPacket* data) +{ + uint32 unspentTalentPoints = 0; + size_t pointsPos = data->wpos(); + *data << uint32(unspentTalentPoints); // [PH], unspentTalentPoints + + uint8 talentIdCount = 0; + size_t countPos = data->wpos(); + *data << uint8(talentIdCount); // [PH], talentIdCount + + Pet* pet = GetPet(); + if (!pet) + return; + + unspentTalentPoints = pet->GetFreeTalentPoints(); + + data->put(pointsPos, unspentTalentPoints); // put real points + + CreatureTemplate const* ci = pet->GetCreatureTemplate(); + if (!ci) + return; + + CreatureFamilyEntry const* pet_family = sCreatureFamilyStore.LookupEntry(ci->family); + if (!pet_family || pet_family->PetTalentType < 0) + return; + + for (uint32 talentTabId = 1; talentTabId < sTalentTabStore.GetNumRows(); ++talentTabId) + { + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentTabId); + if (!talentTabInfo) + continue; + + if (!((1 << pet_family->PetTalentType) & talentTabInfo->CategoryEnumID)) + continue; + + for (uint32 talentId = 0; talentId < sTalentStore.GetNumRows(); ++talentId) + { + TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentId); + if (!talentInfo) + continue; + + // skip another tab talents + if (talentInfo->TabID != talentTabId) + continue; + + // find max talent rank (0~4) + int8 curtalent_maxrank = -1; + for (int8 rank = MAX_TALENT_RANK-1; rank >= 0; --rank) + { + if (talentInfo->SpellRank[rank] && pet->HasSpell(talentInfo->SpellRank[rank])) + { + curtalent_maxrank = rank; + break; + } + } + + // not learned talent + if (curtalent_maxrank < 0) + continue; + + *data << uint32(talentInfo->ID); // Talent.dbc + *data << uint8(curtalent_maxrank); // talentMaxRank (0-4) + + ++talentIdCount; + } + + data->put(countPos, talentIdCount); // put real count + + break; + } +} + +void Player::SendTalentsInfoData(bool pet) +{ + WorldPacket data(SMSG_TALENTS_INFO, 50); + data << uint8(pet ? 1 : 0); + if (pet) + BuildPetTalentsInfoData(&data); + else + BuildPlayerTalentsInfoData(&data); + SendDirectMessage(&data); +} + +void Player::BuildEnchantmentsInfoData(WorldPacket* data) +{ + uint32 slotUsedMask = 0; + size_t slotUsedMaskPos = data->wpos(); + *data << uint32(slotUsedMask); // slotUsedMask < 0x80000 + + for (uint32 i = 0; i < EQUIPMENT_SLOT_END; ++i) + { + Item* item = GetItemByPos(INVENTORY_SLOT_BAG_0, i); + + if (!item) + continue; + + slotUsedMask |= (1 << i); + + *data << uint32(item->GetEntry()); // item entry + + uint16 enchantmentMask = 0; + size_t enchantmentMaskPos = data->wpos(); + *data << uint16(enchantmentMask); // enchantmentMask < 0x1000 + + for (uint32 j = 0; j < MAX_ENCHANTMENT_SLOT; ++j) + { + uint32 enchId = item->GetEnchantmentId(EnchantmentSlot(j)); + + if (!enchId) + continue; + + enchantmentMask |= (1 << j); + + *data << uint16(enchId); + } + + data->put(enchantmentMaskPos, enchantmentMask); + + *data << uint16(item->GetItemRandomPropertyId()); + *data << item->GetGuidValue(ITEM_FIELD_CREATOR).WriteAsPacked(); + *data << uint32(item->GetItemSuffixFactor()); + } + + data->put(slotUsedMaskPos, slotUsedMask); +} + +void Player::SendEquipmentSetList() +{ + uint32 count = 0; + WorldPacket data(SMSG_EQUIPMENT_SET_LIST, 1000); // guess size + size_t count_pos = data.wpos(); + data << uint32(count); // count placeholder + for (EquipmentSetContainer::value_type const& eqSet : _equipmentSets) + { + if (eqSet.second.State == EQUIPMENT_SET_DELETED) + continue; + + data.appendPackGUID(eqSet.first); + data << uint32(eqSet.second.Data.SetID); + data << eqSet.second.Data.SetName; + data << eqSet.second.Data.SetIcon; + for (uint32 i = 0; i < EQUIPMENT_SLOT_END; ++i) + { + // ignored slots stored in IgnoreMask, client wants "1" as raw GUID, so no HighGuid::Item + if (eqSet.second.Data.IgnoreMask & (1 << i)) + data.appendPackGUID(uint64(1)); + else + data.appendPackGUID(eqSet.second.Data.Pieces[i]); + } + + ++count; // client have limit but it checked at loading and set + } + data.put(count_pos, count); + SendDirectMessage(&data); +} + +void Player::SetEquipmentSet(EquipmentSetInfo::EquipmentSetData const& eqSet) +{ + if (eqSet.Guid != 0) + { + // something wrong... + auto itr = _equipmentSets.find(eqSet.Guid); + if (itr == _equipmentSets.end() || itr->second.Data.Guid != eqSet.Guid) + { + TC_LOG_ERROR("entities.player", "Player::SetEquipmentSet: Player '%s' (%s) tried to save nonexistent equipment set " UI64FMTD " (index: %u)", + GetName().c_str(), GetGUID().ToString().c_str(), eqSet.Guid, eqSet.SetID); + return; + } + } + + uint64 setGuid = (eqSet.Guid != 0) ? eqSet.Guid : sObjectMgr->GenerateEquipmentSetGuid(); + + EquipmentSetInfo& eqSlot = _equipmentSets[setGuid]; + eqSlot.Data = eqSet; + + if (eqSet.Guid == 0) + { + eqSlot.Data.Guid = setGuid; + + WorldPacket data(SMSG_EQUIPMENT_SET_SAVED, 4 + 1); + data << uint32(eqSlot.Data.SetID); + data.appendPackGUID(eqSlot.Data.Guid); + SendDirectMessage(&data); + } + + eqSlot.State = (eqSlot.State == EQUIPMENT_SET_NEW ? EQUIPMENT_SET_NEW : EQUIPMENT_SET_CHANGED); +} + +void Player::_SaveEquipmentSets(CharacterDatabaseTransaction& trans) +{ + for (EquipmentSetContainer::iterator itr = _equipmentSets.begin(); itr != _equipmentSets.end();) + { + EquipmentSetInfo& eqSet = itr->second; + CharacterDatabasePreparedStatement* stmt; + uint8 j = 0; + switch (eqSet.State) + { + case EQUIPMENT_SET_UNCHANGED: + ++itr; + break; // nothing do + case EQUIPMENT_SET_CHANGED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_EQUIP_SET); + stmt->setString(j++, eqSet.Data.SetName); + stmt->setString(j++, eqSet.Data.SetIcon); + stmt->setUInt32(j++, eqSet.Data.IgnoreMask); + for (uint8 i = 0; i < EQUIPMENT_SLOT_END; ++i) + stmt->setUInt32(j++, eqSet.Data.Pieces[i].GetCounter()); + stmt->setUInt32(j++, GetGUID().GetCounter()); + stmt->setUInt64(j++, eqSet.Data.Guid); + stmt->setUInt32(j, eqSet.Data.SetID); + trans->Append(stmt); + eqSet.State = EQUIPMENT_SET_UNCHANGED; + ++itr; + break; + case EQUIPMENT_SET_NEW: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_EQUIP_SET); + stmt->setUInt32(j++, GetGUID().GetCounter()); + stmt->setUInt64(j++, eqSet.Data.Guid); + stmt->setUInt32(j++, eqSet.Data.SetID); + stmt->setString(j++, eqSet.Data.SetName); + stmt->setString(j++, eqSet.Data.SetIcon); + stmt->setUInt32(j++, eqSet.Data.IgnoreMask); + for (uint8 i = 0; i < EQUIPMENT_SLOT_END; ++i) + stmt->setUInt32(j++, eqSet.Data.Pieces[i].GetCounter()); + trans->Append(stmt); + eqSet.State = EQUIPMENT_SET_UNCHANGED; + ++itr; + break; + case EQUIPMENT_SET_DELETED: + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_EQUIP_SET); + stmt->setUInt64(0, eqSet.Data.Guid); + trans->Append(stmt); + itr = _equipmentSets.erase(itr); + break; + } + } +} + +void Player::_SaveBGData(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_BGDATA); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + /* guid, bgInstanceID, bgTeam, x, y, z, o, map, taxi[0], taxi[1], mountSpell */ + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_PLAYER_BGDATA); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, m_bgData.bgInstanceID); + stmt->setUInt16(2, m_bgData.bgTeam); + stmt->setFloat (3, m_bgData.joinPos.GetPositionX()); + stmt->setFloat (4, m_bgData.joinPos.GetPositionY()); + stmt->setFloat (5, m_bgData.joinPos.GetPositionZ()); + stmt->setFloat (6, m_bgData.joinPos.GetOrientation()); + stmt->setUInt16(7, m_bgData.joinPos.GetMapId()); + stmt->setUInt16(8, m_bgData.taxiPath[0]); + stmt->setUInt16(9, m_bgData.taxiPath[1]); + stmt->setUInt16(10, m_bgData.mountSpell); + trans->Append(stmt); +} + +void Player::DeleteEquipmentSet(uint64 setGuid) +{ + for (auto itr = _equipmentSets.begin(); itr != _equipmentSets.end();) + { + if (itr->second.Data.Guid == setGuid) + { + if (itr->second.State == EQUIPMENT_SET_NEW) + itr = _equipmentSets.erase(itr); + else + itr->second.State = EQUIPMENT_SET_DELETED; + break; + } + ++itr; + } +} + +void Player::RemoveAtLoginFlag(AtLoginFlags flags, bool persist /*= false*/) +{ + m_atLoginFlags &= ~flags; + + if (persist) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_REM_AT_LOGIN_FLAG); + + stmt->setUInt16(0, uint16(flags)); + stmt->setUInt32(1, GetGUID().GetCounter()); + + CharacterDatabase.Execute(stmt); + } +} + +void Player::ResetMap() +{ + // this may be called during Map::Update + // after decrement+unlink, ++m_mapRefIter will continue correctly + // when the first element of the list is being removed + // nocheck_prev will return the padding element of the RefManager + // instead of nullptr in the case of prev + GetMap()->UpdateIteratorBack(this); + Unit::ResetMap(); + GetMapRef().unlink(); +} + +void Player::SetMap(Map* map) +{ + Unit::SetMap(map); + m_mapRef.link(map, this); +} + +void Player::_LoadGlyphs(PreparedQueryResult result) +{ + // SELECT talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6 from character_glyphs WHERE guid = '%u' + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + + uint8 spec = fields[0].GetUInt8(); + if (spec >= GetSpecsCount()) + continue; + + for (uint8 i = 0; i < MAX_GLYPH_SLOT_INDEX; ++i) + _talentMgr->SpecInfo[spec].Glyphs[i] = fields[i + 1].GetUInt16(); + } + while (result->NextRow()); +} + +void Player::_SaveGlyphs(CharacterDatabaseTransaction& trans) const +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_GLYPHS); + stmt->setUInt32(0, GetGUID().GetCounter()); + trans->Append(stmt); + + + for (uint8 spec = 0; spec < GetSpecsCount(); ++spec) + { + uint8 index = 0; + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_GLYPHS); + stmt->setUInt32(index++, GetGUID().GetCounter()); + + stmt->setUInt8(index++, spec); + + for (uint8 i = 0; i < MAX_GLYPH_SLOT_INDEX; ++i) + stmt->setUInt16(index++, uint16(GetGlyph(spec, i))); + + trans->Append(stmt); + } +} + +void Player::_LoadTalents(PreparedQueryResult result) +{ + // SetPQuery(PLAYER_LOGIN_QUERY_LOADTALENTS, "SELECT spell, talentGroup FROM character_talent WHERE guid = '%u'", GUID_LOPART(m_guid)); + if (result) + { + do + AddTalent((*result)[0].GetUInt32(), (*result)[1].GetUInt8(), false); + while (result->NextRow()); + } +} + +void Player::_SaveTalents(CharacterDatabaseTransaction& trans) +{ + CharacterDatabasePreparedStatement* stmt; + + for (uint8 i = 0; i < MAX_TALENT_SPECS; ++i) + { + for (PlayerTalentMap::iterator itr = GetTalentMap(i).begin(); itr != GetTalentMap(i).end();) + { + if (itr->second.State == PLAYERSPELL_REMOVED || itr->second.State == PLAYERSPELL_CHANGED) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TALENT_BY_SPELL_SPEC); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, itr->first); + stmt->setUInt8(2, itr->second.Spec); + trans->Append(stmt); + } + + if (itr->second.State == PLAYERSPELL_NEW || itr->second.State == PLAYERSPELL_CHANGED) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_TALENT); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt32(1, itr->first); + stmt->setUInt8(2, itr->second.Spec); + trans->Append(stmt); + } + + if (itr->second.State == PLAYERSPELL_REMOVED) + { + GetTalentMap(i).erase(itr++); + } + else + { + itr->second.State = PLAYERSPELL_UNCHANGED; + ++itr; + } + } + } +} + +void Player::UpdateSpecCount(uint8 count) +{ + uint32 curCount = GetSpecsCount(); + if (curCount == count) + return; + + if (GetActiveSpec() >= count) + ActivateSpec(0); + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + CharacterDatabasePreparedStatement* stmt; + + // Copy spec data + if (count > curCount) + { + _SaveActions(trans); // make sure the button list is cleaned up + for (ActionButtonList::iterator itr = m_actionButtons.begin(); itr != m_actionButtons.end(); ++itr) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_ACTION); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt8(1, 1); + stmt->setUInt8(2, itr->first); + stmt->setUInt32(3, itr->second.GetAction()); + stmt->setUInt8(4, uint8(itr->second.GetType())); + trans->Append(stmt); + } + } + // Delete spec data for removed spec. + else if (count < curCount) + { + _SaveActions(trans); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACTION_EXCEPT_SPEC); + stmt->setUInt8(0, GetActiveSpec()); + stmt->setUInt32(1, GetGUID().GetCounter()); + trans->Append(stmt); + + } + + CharacterDatabase.CommitTransaction(trans); + + SetSpecsCount(count); + + SendTalentsInfoData(false); +} + +void Player::ActivateSpec(uint8 spec) +{ + if (GetActiveSpec() == spec) + return; + + if (spec > GetSpecsCount()) + return; + + if (IsNonMeleeSpellCast(false)) + InterruptNonMeleeSpells(false); + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + _SaveActions(trans); + CharacterDatabase.CommitTransaction(trans); + + // TO-DO: We need more research to know what happens with warlock's reagent + if (Pet* pet = GetPet()) + RemovePet(pet, PET_SAVE_DISMISS); + + ClearAllReactives(); + UnsummonAllTotems(); + ExitVehicle(); + RemoveAllControlled(); + + // remove limited target auras at other targets + AuraList& scAuras = GetSingleCastAuras(); + for (AuraList::iterator iter = scAuras.begin(); iter != scAuras.end();) + { + Aura* aura = *iter; + if (aura->GetUnitOwner() != this) + { + aura->Remove(); + iter = scAuras.begin(); + } + else + ++iter; + } + + /*RemoveAllAurasOnDeath(); + if (GetPet()) + GetPet()->RemoveAllAurasOnDeath();*/ + + //RemoveAllAuras(GetGUID(), nullptr, false, true); // removes too many auras + //ExitVehicle(); // should be impossible to switch specs from inside a vehicle.. + + // Let client clear his current Actions + SendActionButtons(2); + // m_actionButtons.clear() is called in the next _LoadActionButtons + for (uint32 talentId = 0; talentId < sTalentStore.GetNumRows(); ++talentId) + { + TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentId); + + if (!talentInfo) + continue; + + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentInfo->TabID); + + if (!talentTabInfo) + continue; + + // unlearn only talents for character class + // some spell learned by one class as normal spells or know at creation but another class learn it as talent, + // to prevent unexpected lost normal learned spell skip another class talents + if ((getClassMask() & talentTabInfo->ClassMask) == 0) + continue; + + for (int8 rank = MAX_TALENT_RANK-1; rank >= 0; --rank) + { + // skip non-existing talent ranks + if (talentInfo->SpellRank[rank] == 0) + continue; + RemoveSpell(talentInfo->SpellRank[rank], true); // removes the talent, and all dependant, learned, and chained spells.. + if (SpellInfo const* _spellEntry = sSpellMgr->GetSpellInfo(talentInfo->SpellRank[rank])) + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) // search through the SpellInfo for valid trigger spells + if (_spellEntry->Effects[i].TriggerSpell > 0 && _spellEntry->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL) + RemoveSpell(_spellEntry->Effects[i].TriggerSpell, true); // and remove any spells that the talent teaches + // if this talent rank can be found in the PlayerTalentMap, mark the talent as removed so it gets deleted + //PlayerTalentMap::iterator plrTalent = m_talents[m_activeSpec]->find(talentInfo->RankID[rank]); + //if (plrTalent != m_talents[m_activeSpec]->end()) + // plrTalent->second->state = PLAYERSPELL_REMOVED; + } + } + + // Remove spec specific spells + for (uint32 i = 0; i < MAX_TALENT_TABS; ++i) + { + uint32 const* talentTabs = sDBCManager.GetTalentTabPages(getClass()); + std::vector const* specSpells = sDBCManager.GetTalentTreePrimarySpells(talentTabs[i]); + if (specSpells) + for (size_t i = 0; i < specSpells->size(); ++i) + RemoveSpell(specSpells->at(i), true); + + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentTabs[i]); + for (uint32 i = 0; i < MAX_MASTERY_SPELLS; ++i) + if (uint32 mastery = talentTabInfo->MasterySpellID[i]) + RemoveSpell(mastery, true); + } + + // set glyphs + for (uint8 slot = 0; slot < MAX_GLYPH_SLOT_INDEX; ++slot) + // remove secondary glyph + if (uint32 oldglyph = GetGlyph(GetActiveSpec(), slot)) + if (GlyphPropertiesEntry const* old_gp = sGlyphPropertiesStore.LookupEntry(oldglyph)) + RemoveAurasDueToSpell(old_gp->SpellID); + + SetActiveSpec(spec); + uint32 spentTalents = 0; + + for (uint32 talentId = 0; talentId < sTalentStore.GetNumRows(); ++talentId) + { + TalentEntry const* talentInfo = sTalentStore.LookupEntry(talentId); + + if (!talentInfo) + continue; + + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentInfo->TabID); + + if (!talentTabInfo) + continue; + + // learn only talents for character class + if ((getClassMask() & talentTabInfo->ClassMask) == 0) + continue; + + // learn highest talent rank that exists in newly activated spec + for (int8 rank = MAX_TALENT_RANK-1; rank >= 0; --rank) + { + // skip non-existing talent ranks + if (talentInfo->SpellRank[rank] == 0) + continue; + // if the talent can be found in the newly activated PlayerTalentMap + if (HasTalent(talentInfo->SpellRank[rank], GetActiveSpec())) + { + LearnSpell(talentInfo->SpellRank[rank], false); // add the talent to the PlayerSpellMap + spentTalents += (rank + 1); // increment the spentTalents count + } + } + } + + std::vector const* specSpells = sDBCManager.GetTalentTreePrimarySpells(GetPrimaryTalentTree(GetActiveSpec())); + if (specSpells) + for (size_t i = 0; i < specSpells->size(); ++i) + LearnSpell(specSpells->at(i), false); + + if (CanUseMastery()) + if (TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(GetPrimaryTalentTree(GetActiveSpec()))) + for (uint32 i = 0; i < MAX_MASTERY_SPELLS; ++i) + if (uint32 mastery = talentTabInfo->MasterySpellID[i]) + LearnSpell(mastery, false); + + // set glyphs + for (uint8 slot = 0; slot < MAX_GLYPH_SLOT_INDEX; ++slot) + { + uint32 glyph = GetGlyph(GetActiveSpec(), slot); + + // apply primary glyph + if (glyph) + if (GlyphPropertiesEntry const* gp = sGlyphPropertiesStore.LookupEntry(glyph)) + CastSpell(this, gp->SpellID, true); + + SetGlyph(slot, glyph); + } + + SetUsedTalentCount(spentTalents); + InitTalentForLevel(); + + // load them asynchronously + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS_SPEC); + stmt->setUInt32(0, GetGUID().GetCounter()); + stmt->setUInt8(1, GetActiveSpec()); + + WorldSession* mySess = GetSession(); + mySess->GetQueryProcessor().AddCallback(CharacterDatabase.AsyncQuery(stmt) + .WithPreparedCallback([mySess](PreparedQueryResult result) + { + // safe callback, we can't pass this pointer directly + // in case player logs out before db response (player would be deleted in that case) + if (Player* thisPlayer = mySess->GetPlayer()) + thisPlayer->LoadActions(result); + })); + } + + PowerType pw = GetPowerType(); + if (pw != POWER_MANA) + SetPower(POWER_MANA, 0); // Mana must be 0 even if it isn't the active power type. + + SetPower(pw, 0); + + Unit::AuraEffectList const& shapeshiftAuras = GetAuraEffectsByType(SPELL_AURA_MOD_SHAPESHIFT); + for (AuraEffect* aurEff : shapeshiftAuras) + { + aurEff->HandleShapeshiftBoosts(this, false); + aurEff->HandleShapeshiftBoosts(this, true); + } + + if (!sTalentTabStore.LookupEntry(GetPrimaryTalentTree(GetActiveSpec()))) + ResetTalents(true); +} + +void Player::LoadActions(PreparedQueryResult result) +{ + if (result) + _LoadActions(result); + + SendActionButtons(1); +} + +void Player::SetReputation(uint32 factionentry, uint32 value) +{ + GetReputationMgr().SetReputation(sFactionStore.LookupEntry(factionentry), value); +} +uint32 Player::GetReputation(uint32 factionentry) const +{ + return GetReputationMgr().GetReputation(sFactionStore.LookupEntry(factionentry)); +} + +std::string Player::GetGuildName() const +{ + return GetGuildId() ? sGuildMgr->GetGuildById(GetGuildId())->GetName() : ""; +} + +void Player::SendDuelCountdown(uint32 counter) +{ + WorldPacket data(SMSG_DUEL_COUNTDOWN, 4); + data << uint32(counter); // seconds + SendDirectMessage(&data); +} + +void Player::AddRefundReference(ObjectGuid it) +{ + m_refundableItems.insert(it); +} + +void Player::DeleteRefundReference(ObjectGuid it) +{ + GuidSet::iterator itr = m_refundableItems.find(it); + if (itr != m_refundableItems.end()) + m_refundableItems.erase(itr); +} + +void Player::SendRefundInfo(Item* item) +{ + // This function call unsets ITEM_FLAGS_REFUNDABLE if played time is over 2 hours. + item->UpdatePlayedTime(this); + + if (!item->HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_REFUNDABLE)) + { + TC_LOG_DEBUG("entities.player.items", "Item refund: item not refundable!"); + return; + } + + if (GetGUID().GetCounter() != item->GetRefundRecipient()) // Formerly refundable item got traded + { + TC_LOG_DEBUG("entities.player.items", "Item refund: item was traded!"); + item->SetNotRefundable(this); + return; + } + + ItemExtendedCostEntry const* iece = sItemExtendedCostStore.LookupEntry(item->GetPaidExtendedCost()); + if (!iece) + { + TC_LOG_DEBUG("entities.player.items", "Item refund: cannot find extendedcost data."); + return; + } + + ObjectGuid guid = item->GetGUID(); + WorldPacket data(SMSG_ITEM_REFUND_INFO_RESPONSE, 8+4+4+4+4*4+4*4+4+4); + data.WriteBit(guid[3]); + data.WriteBit(guid[5]); + data.WriteBit(guid[7]); + data.WriteBit(guid[6]); + data.WriteBit(guid[2]); + data.WriteBit(guid[4]); + data.WriteBit(guid[0]); + data.WriteBit(guid[1]); + data.FlushBits(); + + data.WriteByteSeq(guid[7]); + data << uint32(std::max(0, 2 * HOUR * IN_MILLISECONDS - item->GetPlayedTime(this) * IN_MILLISECONDS)); // Refund Time Left + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_ITEMS; ++i) // item cost data + { + data << uint32(iece->RequiredItemCount[i]); + data << uint32(iece->RequiredItem[i]); + } + + data.WriteByteSeq(guid[6]); + data.WriteByteSeq(guid[4]); + data.WriteByteSeq(guid[3]); + data.WriteByteSeq(guid[2]); + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_CURRENCIES; ++i) // currency cost data + { + if (iece->RequirementFlags & (ITEM_EXT_COST_CURRENCY_REQ_IS_SEASON_EARNED_1 << i)) + { + data << uint32(0); + data << uint32(0); + continue; + } + + CurrencyTypesEntry const* currencyType = sCurrencyTypesStore.LookupEntry(iece->RequiredCurrency[i]); + uint32 precision = (currencyType && currencyType->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) ? CURRENCY_PRECISION : 1; + + data << uint32(iece->RequiredCurrencyCount[i] / precision); + data << uint32(iece->RequiredCurrency[i]); + } + + data.WriteByteSeq(guid[1]); + data.WriteByteSeq(guid[5]); + data << uint32(0); + data.WriteByteSeq(guid[0]); + data << uint32(item->GetPaidMoney()); // money cost + SendDirectMessage(&data); +} + +bool Player::AddItem(uint32 itemId, uint32 count) +{ + uint32 noSpaceForCount = 0; + ItemPosCountVec dest; + InventoryResult msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemId, count, &noSpaceForCount); + if (msg != EQUIP_ERR_OK) + count -= noSpaceForCount; + + if (count == 0 || dest.empty()) + { + /// @todo Send to mailbox if no space + ChatHandler(GetSession()).PSendSysMessage("You don't have any space in your bags."); + return false; + } + + Item* item = StoreNewItem(dest, itemId, true, GenerateItemRandomPropertyId(itemId)); + if (item) + SendNewItem(item, count, true, false); + else + return false; + return true; +} + +void Player::SendItemRefundResult(Item* item, ItemExtendedCostEntry const* iece, uint8 error) +{ + ObjectGuid guid = item->GetGUID(); + WorldPacket data(SMSG_ITEM_REFUND_RESULT, 1 + 1 + 8 + 4*8 + 4 + 4*8 + 1); + data.WriteBit(guid[4]); + data.WriteBit(guid[5]); + data.WriteBit(guid[1]); + data.WriteBit(guid[6]); + data.WriteBit(guid[7]); + data.WriteBit(guid[0]); + data.WriteBit(guid[3]); + data.WriteBit(guid[2]); + data.WriteBit(!error); + data.WriteBit(item->GetPaidMoney() > 0); + data.FlushBits(); + if (!error) + { + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_CURRENCIES; ++i) + { + if (iece->RequirementFlags & (ITEM_EXT_COST_CURRENCY_REQ_IS_SEASON_EARNED_1 << i)) + { + data << uint32(0); + data << uint32(0); + continue; + } + + CurrencyTypesEntry const* currencyType = sCurrencyTypesStore.LookupEntry(iece->RequiredCurrency[i]); + uint32 precision = (currencyType && currencyType->GetFlags().HasFlag(CurrencyTypeFlags::Uses1To100ScalarForDisplay)) ? CURRENCY_PRECISION : 1; + + data << uint32(iece->RequiredCurrencyCount[i] / precision); + data << uint32(iece->RequiredCurrency[i]); + } + + data << uint32(item->GetPaidMoney()); // money cost + + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_ITEMS; ++i) // item cost data + { + data << uint32(iece->RequiredItemCount[i]); + data << uint32(iece->RequiredItem[i]); + } + } + + data.WriteByteSeq(guid[0]); + data.WriteByteSeq(guid[3]); + data.WriteByteSeq(guid[1]); + data.WriteByteSeq(guid[6]); + data.WriteByteSeq(guid[4]); + data.WriteByteSeq(guid[2]); + data.WriteByteSeq(guid[7]); + data.WriteByteSeq(guid[5]); + + data << uint8(error); // error code + SendDirectMessage(&data); +} + +void Player::RefundItem(Item* item) +{ + if (!item->HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_REFUNDABLE)) + { + TC_LOG_DEBUG("entities.player.items", "Item refund: item not refundable!"); + return; + } + + if (item->IsRefundExpired(this)) // item refund has expired + { + item->SetNotRefundable(this); + SendItemRefundResult(item, nullptr, 10); + return; + } + + if (GetGUID().GetCounter() != item->GetRefundRecipient()) // Formerly refundable item got traded + { + TC_LOG_DEBUG("entities.player.items", "Item refund: item was traded!"); + item->SetNotRefundable(this); + return; + } + + ItemExtendedCostEntry const* iece = sItemExtendedCostStore.LookupEntry(item->GetPaidExtendedCost()); + if (!iece) + { + TC_LOG_DEBUG("entities.player.items", "Item refund: cannot find extendedcost data."); + return; + } + + bool store_error = false; + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_ITEMS; ++i) + { + uint32 count = iece->RequiredItemCount[i]; + uint32 itemid = iece->RequiredItem[i]; + + if (count && itemid) + { + ItemPosCountVec dest; + InventoryResult msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemid, count); + if (msg != EQUIP_ERR_OK) + { + store_error = true; + break; + } + } + } + + if (store_error) + { + SendItemRefundResult(item, iece, 10); + return; + } + + SendItemRefundResult(item, iece, 0); + + uint32 moneyRefund = item->GetPaidMoney(); // item-> will be invalidated in DestroyItem + + // Save all relevant data to DB to prevent desynchronisation exploits + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + // Delete any references to the refund data + item->SetNotRefundable(this, true, &trans); + + // Destroy item + DestroyItem(item->GetBagSlot(), item->GetSlot(), true); + + // Grant back extendedcost items + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_ITEMS; ++i) + { + uint32 count = iece->RequiredItemCount[i]; + uint32 itemid = iece->RequiredItem[i]; + if (count && itemid) + { + ItemPosCountVec dest; + InventoryResult msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemid, count); + ASSERT(msg == EQUIP_ERR_OK); /// Already checked before + Item* it = StoreNewItem(dest, itemid, true); + SendNewItem(it, count, true, false, true); + } + } + + // Grant back currencies + for (uint8 i = 0; i < MAX_ITEM_EXT_COST_CURRENCIES; ++i) + { + if (iece->RequirementFlags & (ITEM_EXT_COST_CURRENCY_REQ_IS_SEASON_EARNED_1 << i)) + continue; + + uint32 count = iece->RequiredCurrencyCount[i]; + uint32 currencyid = iece->RequiredCurrency[i]; + if (iece->RequirementFlags & (ITEM_EXT_COST_CURRENCY_REQ_IS_SEASON_EARNED_1 << i)) + continue; + + if (count && currencyid) + ModifyCurrency(currencyid, count, true, true); + } + + // Grant back money + if (moneyRefund) + ModifyMoney(int64(moneyRefund)); // Saved in SaveInventoryAndGoldToDB + + SaveInventoryAndGoldToDB(trans); + + CharacterDatabase.CommitTransaction(trans); +} + +void Player::SendItemRetrievalMail(uint32 itemEntry, uint32 count) +{ + MailSender sender(MAIL_CREATURE, 34337 /* The Postmaster */); + MailDraft draft("Recovered Item", "We recovered a lost item in the twisting nether and noted that it was yours.$B$BPlease find said object enclosed."); // This is the text used in Cataclysm, it probably wasn't changed. + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + if (Item* item = Item::CreateItem(itemEntry, count, nullptr)) + { + item->SaveToDB(trans); + draft.AddItem(item); + } + + draft.SendMailTo(trans, MailReceiver(this, GetGUID().GetCounter()), sender); + CharacterDatabase.CommitTransaction(trans); +} + +void Player::SetRandomWinner(bool isWinner) +{ + m_IsBGRandomWinner = isWinner; + if (m_IsBGRandomWinner) + { + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_BATTLEGROUND_RANDOM); + + stmt->setUInt32(0, GetGUID().GetCounter()); + + CharacterDatabase.Execute(stmt); + } +} + +void Player::_LoadRandomBGStatus(PreparedQueryResult result) +{ + //QueryResult result = CharacterDatabase.PQuery("SELECT guid FROM character_battleground_random WHERE guid = '%u'", GetGUID().GetCounter()); + + if (result) + m_IsBGRandomWinner = true; +} + +float Player::GetAverageItemLevel() const +{ + auto getEffectiveItemLevel = [](uint32 itemLevel, uint32* pValue, uint32* pMinValue)->void + { + if (pMinValue != nullptr) + { + if (itemLevel <= *pValue) + { + if (itemLevel >= *pMinValue) + *pMinValue = itemLevel; + } + else + { + *pMinValue = *pValue; + *pValue = itemLevel; + } + } + else + { + if (*pValue > itemLevel) + itemLevel = *pValue; + + *pValue = itemLevel; + } + }; + + uint32 itemLevelPerInvType[MAX_INVTYPE] = { 0 }; + + uint32 fingerItemLevel = 0; + uint32 trinketItemLevel = 0; + uint32 weaponItemLevel = 0; + uint32 weapon2HItemLevel = 0; + + for (uint8 i = EQUIPMENT_SLOT_START; i < EQUIPMENT_SLOT_END; ++i) + { + const ItemTemplate* itemTemplate = m_items[i] == nullptr ? nullptr : m_items[i]->GetTemplate(); + if (itemTemplate == nullptr) + continue; + + uint32* pEffective = nullptr; + switch (itemTemplate->GetInventoryType()) + { + case INVTYPE_FINGER: pEffective = &fingerItemLevel; break; + case INVTYPE_TRINKET: pEffective = &trinketItemLevel; break; + case INVTYPE_WEAPON: pEffective = &weaponItemLevel; break; + case INVTYPE_2HWEAPON: pEffective = &weapon2HItemLevel; break; + } + + getEffectiveItemLevel(static_cast(itemTemplate->GetBaseItemLevel()), &itemLevelPerInvType[itemTemplate->GetInventoryType()], pEffective); + } + + uint32 maxRangedThrownRelicRangedRight = std::max( + { + itemLevelPerInvType[INVTYPE_RANGED], + itemLevelPerInvType[INVTYPE_RANGEDRIGHT], + itemLevelPerInvType[INVTYPE_RELIC], + itemLevelPerInvType[INVTYPE_THROWN] + }); + + uint32 offHandItemLevel = 0; + uint32 mainHandItemLevel = 0; + + if (getClass() == CLASS_WARRIOR || getClass() == CLASS_ROGUE || getClass() == CLASS_SHAMAN || getClass() == CLASS_HUNTER || getClass() == CLASS_DEATH_KNIGHT) + { + uint8 const weaponSorterSize = 6; + std::array weaponSorter; + weaponSorter[0] = itemLevelPerInvType[INVTYPE_WEAPONMAINHAND]; + + weaponSorter[1] = std::max( + { + itemLevelPerInvType[INVTYPE_SHIELD], + itemLevelPerInvType[INVTYPE_WEAPONOFFHAND], + itemLevelPerInvType[INVTYPE_HOLDABLE] + }); + + weaponSorter[2] = itemLevelPerInvType[INVTYPE_WEAPON]; + weaponSorter[3] = weaponItemLevel; + + if (getClass() == CLASS_WARRIOR) + { + weaponSorter[4] = itemLevelPerInvType[INVTYPE_2HWEAPON]; + weaponSorter[5] = weapon2HItemLevel; + } + else + { + weaponSorter[4] = 0; + weaponSorter[5] = 0; + } + + std::sort(weaponSorter.begin(), weaponSorter.end(), [](uint32 lhs, uint32 rhs)->bool + { + return rhs < lhs; + }); + + offHandItemLevel = weaponSorter[1]; + mainHandItemLevel = weaponSorter[0]; + } + else + { + mainHandItemLevel = std::max(itemLevelPerInvType[INVTYPE_WEAPON], itemLevelPerInvType[INVTYPE_WEAPONMAINHAND]); + offHandItemLevel = std::max(itemLevelPerInvType[INVTYPE_SHIELD], itemLevelPerInvType[INVTYPE_HOLDABLE]); + } + + uint32 twoHandedWeapon = itemLevelPerInvType[INVTYPE_2HWEAPON]; + itemLevelPerInvType[INVTYPE_NON_EQUIP] = 0; + itemLevelPerInvType[INVTYPE_BODY] = 0; + itemLevelPerInvType[INVTYPE_RANGED] = 0; + itemLevelPerInvType[INVTYPE_THROWN] = 0; + itemLevelPerInvType[INVTYPE_AMMO] = 0; + itemLevelPerInvType[INVTYPE_RANGEDRIGHT] = 0; + itemLevelPerInvType[INVTYPE_RELIC] = 0; + itemLevelPerInvType[INVTYPE_2HWEAPON] = 0; + itemLevelPerInvType[INVTYPE_WEAPON] = 0; + itemLevelPerInvType[INVTYPE_SHIELD] = 0; + itemLevelPerInvType[INVTYPE_WEAPONMAINHAND] = 0; + itemLevelPerInvType[INVTYPE_WEAPONOFFHAND] = 0; + itemLevelPerInvType[INVTYPE_HOLDABLE] = 0; + itemLevelPerInvType[INVTYPE_BAG] = 0; + itemLevelPerInvType[INVTYPE_TABARD] = 0; + itemLevelPerInvType[INVTYPE_CHEST] = std::max(itemLevelPerInvType[INVTYPE_CHEST], itemLevelPerInvType[INVTYPE_ROBE]); + itemLevelPerInvType[INVTYPE_ROBE] = 0; + + uint32 itemLevelAccumulated = 0; + for (uint8 i = 0; i < MAX_INVTYPE; ++i) + itemLevelAccumulated += itemLevelPerInvType[i]; + + itemLevelAccumulated += fingerItemLevel + trinketItemLevel + maxRangedThrownRelicRangedRight; + uint32 finalSum = mainHandItemLevel + itemLevelAccumulated + offHandItemLevel; + float oneHandingScore = finalSum / 17.0f; // One handing + float twoHandingScore = (twoHandedWeapon + itemLevelAccumulated) / 16.0f; // two handing + + if (oneHandingScore <= twoHandingScore) + return twoHandingScore; + + return oneHandingScore; +} + +void Player::_LoadInstanceTimeRestrictions(PreparedQueryResult result) +{ + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + _instanceResetTimes.insert(InstanceTimeMap::value_type(fields[0].GetUInt32(), fields[1].GetUInt64())); + } while (result->NextRow()); +} + +void Player::_SaveInstanceTimeRestrictions(CharacterDatabaseTransaction& trans) +{ + if (_instanceResetTimes.empty()) + return; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ACCOUNT_INSTANCE_LOCK_TIMES); + stmt->setUInt32(0, GetSession()->GetAccountId()); + trans->Append(stmt); + + for (InstanceTimeMap::const_iterator itr = _instanceResetTimes.begin(); itr != _instanceResetTimes.end(); ++itr) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_ACCOUNT_INSTANCE_LOCK_TIMES); + stmt->setUInt32(0, GetSession()->GetAccountId()); + stmt->setUInt32(1, itr->first); + stmt->setUInt64(2, itr->second); + trans->Append(stmt); + } +} + +bool Player::IsInWhisperWhiteList(ObjectGuid guid) +{ + for (GuidList::const_iterator itr = WhisperList.begin(); itr != WhisperList.end(); ++itr) + if (*itr == guid) + return true; + + return false; +} + +void Player::UpdateArmorSpecialization() +{ + // Remove Auras with SPELL_ATTR8_ARMOR_SPECIALIZATION + for (AuraApplicationMap::iterator itr = m_appliedAuras.begin(); itr != m_appliedAuras.end();) + { + SpellInfo const* spell = itr->second->GetBase()->GetSpellInfo(); + if (spell->AttributesEx8 & SPELL_ATTR8_REQUIRES_EQUIPPED_INV_TYPES) + RemoveAura(itr); + else + ++itr; + } + + for (uint8 id = 0; id < MAX_ARMOR_SPECIALIZATION_IDS; id++) + { + if (HasSpell(ArmorSpecializationIds[id])) + { + CastSpell(this, ArmorSpecializationIds[id], TRIGGERED_FULL_MASK); + break; + } + } +} + +uint8 Player::GetNextVoidStorageFreeSlot() const +{ + for (size_t i = 0; i < _voidStorageItems.size(); ++i) + if (!_voidStorageItems[i]) // unused item + return i; + + return VOID_STORAGE_MAX_SLOT; +} + +uint8 Player::GetNumOfVoidStorageFreeSlots() const +{ + uint8 count = 0; + + for (size_t i = 0; i < _voidStorageItems.size(); ++i) + if (!_voidStorageItems[i]) + count++; + + return count; +} + +uint8 Player::AddVoidStorageItem(VoidStorageItem const& item) +{ + int8 slot = GetNextVoidStorageFreeSlot(); + + if (slot >= VOID_STORAGE_MAX_SLOT) + { + GetSession()->SendVoidStorageTransferResult(VOID_TRANSFER_ERROR_FULL); + return 255; + } + + _voidStorageItems[slot] = std::make_unique(item.ItemId, item.ItemEntry, + item.CreatorGuid, item.ItemRandomPropertyId, item.ItemSuffixFactor); + return slot; +} + +void Player::AddVoidStorageItemAtSlot(uint8 slot, const VoidStorageItem& item) +{ + if (slot >= VOID_STORAGE_MAX_SLOT) + { + GetSession()->SendVoidStorageTransferResult(VOID_TRANSFER_ERROR_FULL); + return; + } + + if (_voidStorageItems[slot]) + { + TC_LOG_ERROR("misc", "Player::AddVoidStorageItemAtSlot - Player (GUID: %u, name: %s) tried to add an item to an used slot (item id: " UI64FMTD ", entry: %u, slot: %u).", GetGUID().GetCounter(), GetName().c_str(), _voidStorageItems[slot]->ItemId, _voidStorageItems[slot]->ItemEntry, slot); + GetSession()->SendVoidStorageTransferResult(VOID_TRANSFER_ERROR_INTERNAL_ERROR_1); + return; + } + + _voidStorageItems[slot] = std::make_unique(item.ItemId, item.ItemId, + item.CreatorGuid, item.ItemRandomPropertyId, item.ItemSuffixFactor); +} + +void Player::DeleteVoidStorageItem(uint8 slot) +{ + if (slot >= VOID_STORAGE_MAX_SLOT) + { + GetSession()->SendVoidStorageTransferResult(VOID_TRANSFER_ERROR_INTERNAL_ERROR_1); + return; + } + + _voidStorageItems[slot] = nullptr; +} + +bool Player::SwapVoidStorageItem(uint8 oldSlot, uint8 newSlot) +{ + if (oldSlot >= VOID_STORAGE_MAX_SLOT || newSlot >= VOID_STORAGE_MAX_SLOT || oldSlot == newSlot) + return false; + + std::swap(_voidStorageItems[newSlot], _voidStorageItems[oldSlot]); + return true; +} + +VoidStorageItem* Player::GetVoidStorageItem(uint8 slot) const +{ + if (slot >= VOID_STORAGE_MAX_SLOT) + { + GetSession()->SendVoidStorageTransferResult(VOID_TRANSFER_ERROR_INTERNAL_ERROR_1); + return nullptr; + } + + return _voidStorageItems[slot].get(); +} + +VoidStorageItem* Player::GetVoidStorageItem(uint64 id, uint8& slot) const +{ + for (size_t i = 0; i < _voidStorageItems.size(); ++i) + { + if (_voidStorageItems[i] && _voidStorageItems[i]->ItemId == id) + { + slot = i; + return _voidStorageItems[i].get(); + } + } + + return nullptr; +} + +std::string Player::GetMapAreaAndZoneString() const +{ + uint32 areaId = GetAreaId(); + std::string areaName = "Unknown"; + std::string zoneName = "Unknown"; + if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(areaId)) + { + int locale = GetSession()->GetSessionDbcLocale(); + areaName = area->AreaName[locale]; + if (area->GetFlags().HasFlag(AreaFlags::IsSubzone)) + if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(area->ParentAreaID)) + zoneName = zone->AreaName[GetSession()->GetSessionDbcLocale()]; + } + + std::ostringstream str; + str << "Map: " << GetMapId() << " (" << (FindMap() ? FindMap()->GetMapName() : "Unknown") << ") Area: " << areaId << " (" << areaName.c_str() << ") Zone: " << zoneName.c_str(); + return str.str(); +} + +std::string Player::GetCoordsMapAreaAndZoneString() const +{ + std::ostringstream str; + str << Position::ToString() << " " << GetMapAreaAndZoneString(); + return str.str(); +} + +void Player::UnlockVoidStorage() +{ + AddCharacterFlag(CHARACTER_FLAG_2_CAN_USE_VOID_STORAGE_FEATURE); + SetFlag(PLAYER_FLAGS, PLAYER_FLAGS_VOID_UNLOCKED); +} +void Player::LockVoidStorage() +{ + RemoveCharacterFlag(CHARACTER_FLAG_2_CAN_USE_VOID_STORAGE_FEATURE); + RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_VOID_UNLOCKED); +} + +Guild* Player::GetGuild() +{ + uint32 guildId = GetGuildId(); + return guildId ? sGuildMgr->GetGuildById(guildId) : nullptr; +} + +Pet* Player::SummonPet(uint32 entry, float x, float y, float z, float ang, PetType petType, uint32 duration) +{ + Pet* pet = new Pet(this, petType); + + pet->Relocate(x, y, z, ang); + if (!pet->IsPositionValid()) + { + TC_LOG_ERROR("misc", "Player::SummonPet: Pet (%s, Entry: %d) not summoned. Suggested coordinates aren't valid (X: %f Y: %f)", pet->GetGUID().ToString().c_str(), pet->GetEntry(), pet->GetPositionX(), pet->GetPositionY()); + delete pet; + return nullptr; + } + + bool hasPetData = pet->LoadPetData(this, entry); + + if (!hasPetData && petType == HUNTER_PET) + { + delete pet; + return nullptr; + } + + if (!hasPetData) + { + Map* map = GetMap(); + uint32 pet_number = sObjectMgr->GeneratePetNumber(); + if (!pet->Create(map->GenerateLowGuid(), map, entry, pet_number)) + { + TC_LOG_ERROR("misc", "Player::SummonPet: No such creature entry %u", entry); + delete pet; + return nullptr; + } + + // generate new name for summon pet + std::string new_name = sObjectMgr->GeneratePetName(entry); + if (!new_name.empty()) + pet->SetName(new_name); + + pet->SetCreatorGUID(GetGUID()); + pet->SetUInt32Value(UNIT_FIELD_FACTIONTEMPLATE, GetFaction()); + + pet->SetUInt64Value(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_NONE); + pet->SetUInt32Value(UNIT_FIELD_BYTES_1, 0); + pet->InitStatsForLevel(getLevel()); + pet->SetReactState(REACT_ASSIST); + + SetMinion(pet, true); + + switch (petType) + { + case SUMMON_PET: + pet->GetCharmInfo()->SetPetNumber(pet_number, true); + pet->SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_CLASS, CLASS_MAGE); + pet->SetUInt32Value(UNIT_FIELD_PETEXPERIENCE, 0); + pet->SetUInt32Value(UNIT_FIELD_PETNEXTLEVELEXP, 1000); + pet->SetUInt32Value(UNIT_FIELD_PET_NAME_TIMESTAMP, uint32(GameTime::GetGameTime())); // cast can't be helped in this case + break; + default: + break; + } + + map->AddToMap(pet->ToCreature()); + + switch (petType) + { + case SUMMON_PET: + pet->InitPetCreateSpells(); + pet->SavePetToDB(PET_SAVE_NEW_PET); + PetSpellInitialize(); + GetSession()->SendPetAdded(pet->GetSlot(), pet->GetCharmInfo()->GetPetNumber(), pet->GetEntry(), pet->getLevel(), pet->GetName()); + break; + default: + break; + } + + // Update all stats after we have applied pet scaling auras to make sure we have all fields initialized properly + pet->SetFullHealth(); + pet->SetPower(POWER_MANA, pet->GetMaxPower(POWER_MANA)); + + if (pet->IsHunterPet()) + pet->CastSpell(pet, SPELL_PET_ENERGIZE, true); + else if (pet->IsPetGhoul()) + { + pet->CastSpell(pet, SPELL_PET_RISEN_GHOUL_SPAWN_IN, true); + pet->CastSpell(pet, SPELL_PET_RISEN_GHOUL_SELF_STUN, true); + } + } + + PhasingHandler::InheritPhaseShift(pet, this); + + if (duration > 0) + pet->SetDuration(duration); + + //ObjectAccessor::UpdateObjectVisibility(pet); + + return pet; +} + +bool Player::CanUseMastery() const +{ + return HasSpell(MasterySpells[getClass()]); +} + +void Player::ValidateMovementInfo(MovementInfo* mi) +{ + //! Anti-cheat checks. Please keep them in seperate if () blocks to maintain a clear overview. + //! Might be subject to latency, so just remove improper flags. + #ifdef TRINITY_DEBUG + #define REMOVE_VIOLATING_FLAGS(check, maskToRemove) \ + { \ + if (check) \ + { \ + TC_LOG_DEBUG("entities.unit", "Player::ValidateMovementInfo: Violation of MovementFlags found (%s). " \ + "MovementFlags: %u, MovementFlags2: %u for player %s. Mask %u will be removed.", \ + STRINGIZE(check), mi->GetMovementFlags(), mi->GetExtraMovementFlags(), GetGUID().ToString().c_str(), maskToRemove); \ + mi->RemoveMovementFlag((maskToRemove)); \ + } \ + } + #else + #define REMOVE_VIOLATING_FLAGS(check, maskToRemove) \ + if (check) \ + mi->RemoveMovementFlag((maskToRemove)); + #endif + + if (mi->guid.IsEmpty()) + { + TC_LOG_ERROR("entities.unit", "WorldSession::ReadMovementInfo: mi->guid is empty"); + return; + } + + Unit* mover = GetCharmedOrSelf()->GetGUID() == mi->guid ? GetCharmedOrSelf() : ObjectAccessor::GetUnit(*GetCharmedOrSelf(), mi->guid); + if (!mover) + { + TC_LOG_ERROR("entities.unit", "WorldSession::ReadMovementInfo: If the server allows the unit (GUID %s) to be moved by the client of player %s, the unit should still exist!", + mi->guid.ToString().c_str(), + GetCharmedOrSelf()->GetGUID().ToString().c_str()); + return; + } + + if (!GetCharmedOrSelf()->GetVehicleBase() || !(GetCharmedOrSelf()->GetVehicle()->GetVehicleInfo()->Flags & VEHICLE_FLAG_FIXED_POSITION)) + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_ROOT), MOVEMENTFLAG_ROOT); + + /*! This must be a packet spoofing attempt. MOVEMENTFLAG_ROOT sent from the client is not valid + in conjunction with any of the moving movement flags such as MOVEMENTFLAG_FORWARD. + It will freeze clients that receive this player's movement info. + */ + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_ROOT) && mi->HasMovementFlag(MOVEMENTFLAG_MASK_MOVING), + MOVEMENTFLAG_MASK_MOVING); + + //! Cannot hover without SPELL_AURA_HOVER + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_HOVER) && !mover->HasAuraType(SPELL_AURA_HOVER), + MOVEMENTFLAG_HOVER); + + //! Cannot ascend and descend at the same time + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_ASCENDING) && mi->HasMovementFlag(MOVEMENTFLAG_DESCENDING), + MOVEMENTFLAG_ASCENDING | MOVEMENTFLAG_DESCENDING); + + //! Cannot move left and right at the same time + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_LEFT) && mi->HasMovementFlag(MOVEMENTFLAG_RIGHT), + MOVEMENTFLAG_LEFT | MOVEMENTFLAG_RIGHT); + + //! Cannot strafe left and right at the same time + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_STRAFE_LEFT) && mi->HasMovementFlag(MOVEMENTFLAG_STRAFE_RIGHT), + MOVEMENTFLAG_STRAFE_LEFT | MOVEMENTFLAG_STRAFE_RIGHT); + + //! Cannot pitch up and down at the same time + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_PITCH_UP) && mi->HasMovementFlag(MOVEMENTFLAG_PITCH_DOWN), + MOVEMENTFLAG_PITCH_UP | MOVEMENTFLAG_PITCH_DOWN); + + //! Cannot move forwards and backwards at the same time + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_FORWARD) && mi->HasMovementFlag(MOVEMENTFLAG_BACKWARD), + MOVEMENTFLAG_FORWARD | MOVEMENTFLAG_BACKWARD); + + //! Cannot walk on water without SPELL_AURA_WATER_WALK except for ghosts + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_WATERWALKING) && + !mover->HasAuraType(SPELL_AURA_WATER_WALK) && + !mover->HasAuraType(SPELL_AURA_GHOST), + MOVEMENTFLAG_WATERWALKING); + + //! Cannot feather fall without SPELL_AURA_FEATHER_FALL + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_FALLING_SLOW) && !GetCharmedOrSelf()->HasAuraType(SPELL_AURA_FEATHER_FALL), + MOVEMENTFLAG_FALLING_SLOW); + + /*! Cannot fly if no fly auras present. Exception is being a GM. + Note that we check for account level instead of Player::IsGameMaster() because in some + situations it may be feasable to use .gm fly on as a GM without having .gm on, + e.g. aerial combat. + */ + + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_FLYING | MOVEMENTFLAG_CAN_FLY) && GetSession()->GetSecurity() == SEC_PLAYER && + !mover->HasAuraType(SPELL_AURA_FLY) && + !mover->HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED), + MOVEMENTFLAG_FLYING | MOVEMENTFLAG_CAN_FLY); + + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY | MOVEMENTFLAG_CAN_FLY) && mi->HasMovementFlag(MOVEMENTFLAG_FALLING), + MOVEMENTFLAG_FALLING); + + REMOVE_VIOLATING_FLAGS(mi->HasMovementFlag(MOVEMENTFLAG_SPLINE_ELEVATION) && G3D::fuzzyEq(mi->splineElevation, 0.0f), MOVEMENTFLAG_SPLINE_ELEVATION); + + // Client first checks if spline elevation != 0, then verifies flag presence + if (G3D::fuzzyNe(mi->splineElevation, 0.0f)) + mi->AddMovementFlag(MOVEMENTFLAG_SPLINE_ELEVATION); + + #undef REMOVE_VIOLATING_FLAGS +} + +void Player::ReadMovementInfo(WorldPacket& data, MovementInfo* mi, Movement::ExtraMovementStatusElement* extras /*= nullptr*/) +{ + MovementStatusElements const* sequence = GetMovementStatusElementsSequence(static_cast(data.GetOpcode())); + if (!sequence) + { + TC_LOG_ERROR("network", "Player::ReadMovementInfo: No movement sequence found for opcode %s", GetOpcodeNameForLogging(static_cast(data.GetOpcode())).c_str()); + return; + } + + bool hasMovementFlags = false; + bool hasMovementFlags2 = false; + bool hasTimestamp = false; + bool hasOrientation = false; + bool hasTransportData = false; + bool hasTransportTime2 = false; + bool hasTransportVehicleId = false; + bool hasPitch = false; + bool hasFallData = false; + bool hasFallDirection = false; + bool hasSplineElevation = false; + + ObjectGuid guid; + ObjectGuid tguid; + + for (; *sequence != MSEEnd; ++sequence) + { + MovementStatusElements const& element = *sequence; + + switch (element) + { + case MSEHasGuidByte0: + case MSEHasGuidByte1: + case MSEHasGuidByte2: + case MSEHasGuidByte3: + case MSEHasGuidByte4: + case MSEHasGuidByte5: + case MSEHasGuidByte6: + case MSEHasGuidByte7: + guid[element - MSEHasGuidByte0] = data.ReadBit(); + break; + case MSEHasTransportGuidByte0: + case MSEHasTransportGuidByte1: + case MSEHasTransportGuidByte2: + case MSEHasTransportGuidByte3: + case MSEHasTransportGuidByte4: + case MSEHasTransportGuidByte5: + case MSEHasTransportGuidByte6: + case MSEHasTransportGuidByte7: + if (hasTransportData) + tguid[element - MSEHasTransportGuidByte0] = data.ReadBit(); + break; + case MSEGuidByte0: + case MSEGuidByte1: + case MSEGuidByte2: + case MSEGuidByte3: + case MSEGuidByte4: + case MSEGuidByte5: + case MSEGuidByte6: + case MSEGuidByte7: + data.ReadByteSeq(guid[element - MSEGuidByte0]); + break; + case MSETransportGuidByte0: + case MSETransportGuidByte1: + case MSETransportGuidByte2: + case MSETransportGuidByte3: + case MSETransportGuidByte4: + case MSETransportGuidByte5: + case MSETransportGuidByte6: + case MSETransportGuidByte7: + if (hasTransportData) + data.ReadByteSeq(tguid[element - MSETransportGuidByte0]); + break; + case MSEHasMovementFlags: + hasMovementFlags = !data.ReadBit(); + break; + case MSEHasMovementFlags2: + hasMovementFlags2 = !data.ReadBit(); + break; + case MSEHasTimestamp: + hasTimestamp = !data.ReadBit(); + break; + case MSEHasOrientation: + hasOrientation = !data.ReadBit(); + break; + case MSEHasTransportData: + hasTransportData = data.ReadBit(); + break; + case MSEHasTransportTime2: + if (hasTransportData) + hasTransportTime2 = data.ReadBit(); + break; + case MSEHasVehicleId: + if (hasTransportData) + hasTransportVehicleId = data.ReadBit(); + break; + case MSEHasPitch: + hasPitch = !data.ReadBit(); + break; + case MSEHasFallData: + hasFallData = data.ReadBit(); + break; + case MSEHasFallDirection: + if (hasFallData) + hasFallDirection = data.ReadBit(); + break; + case MSEHasSplineElevation: + hasSplineElevation = !data.ReadBit(); + break; + case MSEHasSpline: + data.ReadBit(); + break; + case MSEHasHeightChangeFailed: + data.ReadBit(); + break; + case MSEMovementFlags: + if (hasMovementFlags) + mi->flags = data.ReadBits(30); + break; + case MSEMovementFlags2: + if (hasMovementFlags2) + mi->flags2 = data.ReadBits(12); + break; + case MSETimestamp: + if (hasTimestamp) + data >> mi->time; + break; + case MSEPositionX: + data >> mi->pos.m_positionX; + break; + case MSEPositionY: + data >> mi->pos.m_positionY; + break; + case MSEPositionZ: + data >> mi->pos.m_positionZ; + break; + case MSEOrientation: + if (hasOrientation) + mi->pos.SetOrientation(data.read()); + break; + case MSETransportPositionX: + if (hasTransportData) + data >> mi->transport.pos.m_positionX; + break; + case MSETransportPositionY: + if (hasTransportData) + data >> mi->transport.pos.m_positionY; + break; + case MSETransportPositionZ: + if (hasTransportData) + data >> mi->transport.pos.m_positionZ; + break; + case MSETransportOrientation: + if (hasTransportData) + mi->transport.pos.SetOrientation(data.read()); + break; + case MSETransportSeat: + if (hasTransportData) + data >> mi->transport.seat; + break; + case MSETransportTime: + if (hasTransportData) + data >> mi->transport.time; + break; + case MSETransportTime2: + if (hasTransportData && hasTransportTime2) + data >> mi->transport.time2; + break; + case MSETransportVehicleId: + if (hasTransportData && hasTransportVehicleId) + data >> mi->transport.vehicleId; + break; + case MSEPitch: + if (hasPitch) + mi->pitch = G3D::wrap(data.read(), float(-M_PI), float(M_PI)); + break; + case MSEFallTime: + if (hasFallData) + data >> mi->jump.fallTime; + break; + case MSEFallVerticalSpeed: + if (hasFallData) + data >> mi->jump.zspeed; + break; + case MSEFallCosAngle: + if (hasFallData && hasFallDirection) + data >> mi->jump.cosAngle; + break; + case MSEFallSinAngle: + if (hasFallData && hasFallDirection) + data >> mi->jump.sinAngle; + break; + case MSEFallHorizontalSpeed: + if (hasFallData && hasFallDirection) + data >> mi->jump.xyspeed; + break; + case MSESplineElevation: + if (hasSplineElevation) + data >> mi->splineElevation; + break; + case MSECounter: + data >> mi->movementCounter; + break; + case MSEZeroBit: + case MSEOneBit: + data.ReadBit(); + break; + case MSEExtraElement: + extras->ReadNextElement(data); + break; + default: + ASSERT(Movement::PrintInvalidSequenceElement(element, __FUNCTION__)); + break; + } + } + + mi->guid = guid; + mi->transport.guid = tguid; + + ValidateMovementInfo(mi); +} + +void Player::SendSupercededSpell(uint32 oldSpell, uint32 newSpell) const +{ + WorldPackets::Spells::SupercededSpells packet; + packet.SpellID = newSpell; + packet.Superceded = oldSpell; + SendDirectMessage(packet.Write()); +} + +bool Player::ValidateAppearance(uint8 race, uint8 class_, uint8 gender, uint8 hairID, uint8 hairColor, uint8 faceID, uint8 facialStyle, uint8 skinColor, bool create /*=false*/) +{ + // Check skin color + // For Skin type is always 0 + if (CharSectionsEntry const* entry = sDBCManager.GetCharSectionEntry(race, SECTION_TYPE_SKIN, gender, 0, skinColor)) + { // Skin Color defined as Face color, too, we check skin & face in one pass + if (CharSectionsEntry const* entry2 = sDBCManager.GetCharSectionEntry(race, SECTION_TYPE_FACE, gender, faceID, skinColor)) + { + // Check DeathKnight exclusive + if (((entry->Flags & SECTION_FLAG_DEATH_KNIGHT) || (entry2->Flags & SECTION_FLAG_DEATH_KNIGHT)) && class_ != CLASS_DEATH_KNIGHT) + return false; + if (create && !((entry->Flags & SECTION_FLAG_PLAYER) && (entry2->Flags & SECTION_FLAG_PLAYER))) + return false; + } + else + return false; + } + else + return false; + + // These combinations don't have an entry of Type SECTION_TYPE_FACIAL_HAIR, exclude them from that check + bool excludeCheck = (race == RACE_TAUREN) || (race == RACE_DRAENEI) || (gender == GENDER_FEMALE && race != RACE_NIGHTELF && race != RACE_UNDEAD_PLAYER); + + // Check Hair + if (CharSectionsEntry const* entry = sDBCManager.GetCharSectionEntry(race, SECTION_TYPE_HAIR, gender, hairID, hairColor)) + { + if ((entry->Flags & SECTION_FLAG_DEATH_KNIGHT) && class_ != CLASS_DEATH_KNIGHT) + return false; + if (create && !(entry->Flags & SECTION_FLAG_PLAYER)) + return false; + + if (!excludeCheck) + { + if (CharSectionsEntry const* entry2 = sDBCManager.GetCharSectionEntry(race, SECTION_TYPE_FACIAL_HAIR, gender, facialStyle, hairColor)) + { + if ((entry2->Flags & SECTION_FLAG_DEATH_KNIGHT) && class_ != CLASS_DEATH_KNIGHT) + return false; + if (create && !(entry2->Flags & SECTION_FLAG_PLAYER)) + return false; + } + else + return false; + } + else + { + // @TODO: Bound checking for facialStyle ID (used clientside for markings, tauren beard, etc.) + // Not present in DBC + } + } + else + return false; + + return true; +} + +struct CategoryCooldownInfo +{ + CategoryCooldownInfo(uint32 category, int32 cooldown) + : Category(category), ModCooldown(cooldown) { } + + uint32 Category = 0; ///< SpellCategory Id + int32 ModCooldown = 0; ///< Reduced Cooldown in ms +}; + +void Player::SendSpellCategoryCooldowns() const +{ + WorldPackets::Spells::CategoryCooldown cooldowns; + + Unit::AuraEffectList const& categoryCooldownAuras = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_CATEGORY_COOLDOWN); + for (AuraEffect* aurEff : categoryCooldownAuras) + { + uint32 categoryId = aurEff->GetMiscValue(); + auto cItr = std::find_if(cooldowns.CategoryCooldowns.begin(), cooldowns.CategoryCooldowns.end(), + [categoryId](WorldPackets::Spells::CategoryCooldown::CategoryCooldownInfo const& cooldown) + { + return cooldown.Category == categoryId; + }); + + if (cItr == cooldowns.CategoryCooldowns.end()) + cooldowns.CategoryCooldowns.emplace_back(categoryId, -aurEff->GetAmount()); + else + cItr->ModCooldown -= aurEff->GetAmount(); + } + + SendDirectMessage(cooldowns.Write()); +} + +void Player::SendRaidGroupOnlyMessage(RaidGroupReason reason, int32 delay) const +{ + WorldPackets::Instance::RaidGroupOnly raidGroupOnly; + raidGroupOnly.Delay = delay; + raidGroupOnly.Reason = reason; + + SendDirectMessage(raidGroupOnly.Write()); +} + +uint32 Player::GetFreeTalentPoints() const +{ + return _talentMgr->FreeTalentPoints; +} + +void Player::SetFreeTalentPoints(uint32 points) +{ + _talentMgr->FreeTalentPoints = points; +} + +uint32 Player::GetUsedTalentCount() const +{ + return _talentMgr->UsedTalentCount; +} + +void Player::SetUsedTalentCount(uint32 talents) +{ + _talentMgr->UsedTalentCount = talents; +} + +uint32 Player::GetQuestRewardedTalentCount() const +{ + return _talentMgr->QuestRewardedTalentCount; +} + +void Player::AddQuestRewardedTalentCount(uint32 points) +{ + _talentMgr->QuestRewardedTalentCount += points; +} + +uint32 Player::GetTalentResetCost() const +{ + return _talentMgr->ResetTalentsCost; +} + +void Player::SetTalentResetCost(uint32 cost) +{ + _talentMgr->ResetTalentsCost = cost; +} + +uint32 Player::GetTalentResetTime() const +{ + return _talentMgr->ResetTalentsTime; +} + +void Player::SetTalentResetTime(time_t time_) +{ + _talentMgr->ResetTalentsTime = time_; +} +uint32 Player::GetPrimaryTalentTree(uint8 spec) const +{ + return _talentMgr->SpecInfo[spec].TalentTree; +} + +void Player::SetPrimaryTalentTree(uint8 spec, uint32 tree) +{ + _talentMgr->SpecInfo[spec].TalentTree = tree; + UpdateArmorSpecialization(); +} + +uint8 Player::GetActiveSpec() const +{ + return _talentMgr->ActiveSpec; +} + +void Player::SetActiveSpec(uint8 spec) +{ + _talentMgr->ActiveSpec = spec; + UpdateArmorSpecialization(); + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags2::ChangeSpec); +} + +uint8 Player::GetSpecsCount() const +{ + return _talentMgr->SpecsCount; +} + +void Player::SetSpecsCount(uint8 count) +{ + _talentMgr->SpecsCount = count; +} + +void Player::SetRestFlag(RestFlag restFlag) +{ + uint32 oldRestMask = _restFlagMask; + _restFlagMask |= restFlag; + + if (!oldRestMask && _restFlagMask) // only set flag/time on the first rest state + { + _restTime = GameTime::GetGameTime(); + AddCharacterFlag(CHARACTER_FLAG_RESTING); + SetFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING); + } +} + +void Player::RemoveRestFlag(RestFlag restFlag) +{ + uint32 oldRestMask = _restFlagMask; + _restFlagMask &= ~restFlag; + + if (oldRestMask && !_restFlagMask) // only remove flag/time on the last rest state remove + { + _restTime = 0; + RemoveCharacterFlag(CHARACTER_FLAG_RESTING); + RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING); + } +} + +uint32 Player::DoRandomRoll(uint32 minimum, uint32 maximum) +{ + ASSERT(maximum <= 10000); + + uint32 roll = urand(minimum, maximum); + + WorldPacket data(MSG_RANDOM_ROLL, 4 + 4 + 4 + 8); + data << uint32(minimum); + data << uint32(maximum); + data << uint32(roll); + data << GetGUID(); + if (Group* group = GetGroup()) + group->BroadcastPacket(&data, false); + else + SendDirectMessage(&data); + + return roll; +} + +void Player::RemoveSocial() +{ + sSocialMgr->RemovePlayerSocial(GetGUID()); + m_social = nullptr; +} + +void Player::SendTamePetFailure(PetTameFailureReason reason) +{ + WorldPacket data(SMSG_PET_TAME_FAILURE, 1); + data << uint8(reason); + SendDirectMessage(&data); +} + +GameClient* Player::GetGameClient() const +{ + return GetSession()->GetGameClient(); +} diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 89a08f3841..25c900d9ef 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1694,6 +1694,37 @@ class TC_GAME_API Player : public Unit, public GridObject void UninviteFromGroup(); static void RemoveFromGroup(Group* group, ObjectGuid guid, RemoveMethod method = GROUP_REMOVEMETHOD_DEFAULT, ObjectGuid kicker = ObjectGuid::Empty, char const* reason = nullptr); void RemoveFromGroup(RemoveMethod method = GROUP_REMOVEMETHOD_DEFAULT) { RemoveFromGroup(GetGroup(), GetGUID(), method); } + + // VIANO_VIRTUAL_TARGET_ICONS_V1 + void SetVianoTargetIcon(uint8 id, ObjectGuid guid) + { + if (id >= _vianoTargetIcons.size()) + return; + + if (guid) + { + for (ObjectGuid& existing : _vianoTargetIcons) + if (existing == guid) + existing.Clear(); + } + + _vianoTargetIcons[id] = guid; + } + + ObjectGuid GetVianoTargetIcon(uint8 id) const + { + return id < _vianoTargetIcons.size() ? _vianoTargetIcons[id] : ObjectGuid::Empty; + } + + void ClearVianoTargetIcon(ObjectGuid guid) + { + if (!guid) + return; + + for (ObjectGuid& existing : _vianoTargetIcons) + if (existing == guid) + existing.Clear(); + } void SendUpdateToOutOfRangeGroupMembers(); void SetInGuild(uint32 guildId); @@ -2609,6 +2640,9 @@ class TC_GAME_API Player : public Unit, public GridObject JoinedChannelsList m_channels; + // VIANO_VIRTUAL_TARGET_ICONS_V1 + std::array _vianoTargetIcons{}; + uint8 m_cinematic; uint32 m_movie; diff --git a/src/server/game/Entities/Player/Player.h.bak-viano-virtual-icons-aura-v1-20261001-174000 b/src/server/game/Entities/Player/Player.h.bak-viano-virtual-icons-aura-v1-20261001-174000 new file mode 100644 index 0000000000..89a08f3841 --- /dev/null +++ b/src/server/game/Entities/Player/Player.h.bak-viano-virtual-icons-aura-v1-20261001-174000 @@ -0,0 +1,2776 @@ +/* + * 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 . + */ + +#ifndef _PLAYER_H +#define _PLAYER_H + +#include "Unit.h" +#include "CUFProfile.h" +#include "DatabaseEnvFwd.h" +#include "DBCEnums.h" +#include "EnumFlag.h" +#include "EquipmentSet.h" +#include "GroupReference.h" +#include "ItemDefines.h" +#include "ItemEnchantmentMgr.h" +#include "MapReference.h" +#include "PetDefines.h" +#include "PlayerTaxi.h" +#include "QuestDef.h" + +struct AccessRequirement; +struct AchievementEntry; +struct AreaTableEntry; +struct AreaTriggerEntry; +struct ArchData; +struct BarberShopStyleEntry; +struct CharacterCustomizeInfo; +struct CharTitlesEntry; +struct ChatChannelsEntry; +struct CreatureTemplate; +struct CurrencyTypesEntry; +struct FactionEntry; +struct ItemExtendedCostEntry; +struct ItemSetEffect; +struct ItemTemplate; +struct Loot; +struct Mail; +struct PlayerTalent; +struct PlayerTalentInfo; +struct ScalingStatDistributionEntry; +struct ScalingStatValuesEntry; +struct SkillRaceClassInfoEntry; +struct TrainerSpell; +struct VendorItem; +struct GuildMemberProfessionData; + +template +class AchievementMgr; + +class Archaeology; +class Bag; +class Battleground; +class CinematicMgr; +class Channel; +class CharacterCreateInfo; +class Creature; +class DynamicObject; +class GameClient; +class Group; +class Guild; +class Item; +class LootStore; +class OutdoorPvP; +class Pet; +class PetAura; +class PlayerAI; +class PlayerMenu; +class PlayerSocial; +class ReputationMgr; +class SpellCastTargets; +class TradeData; + +enum CharacterFlags : int32; +enum CharacterFlags2 : int32; +enum InventoryType : uint8; +enum ItemClass : uint8; +enum LootError : uint8; +enum LootType : uint8; +enum UpdateCollisionHeightReason : uint8; +enum ZLiquidStatus : uint32; + +typedef std::deque PlayerMails; + +constexpr uint8 PLAYER_MAX_SKILLS = 128; +constexpr uint8 PLAYER_MAX_DAILY_QUESTS = 25; +constexpr uint8 PLAYER_EXPLORED_ZONES_SIZE = 156; + +enum SpellModType : uint8 +{ + SPELLMOD_FLAT = 0, // SPELL_AURA_ADD_FLAT_MODIFIER + SPELLMOD_PCT = 1, // SPELL_AURA_ADD_PCT_MODIFIER + SPELLMOD_END +}; + +// 2^n values, Player::m_isunderwater is a bitmask. These are Trinity internal values, they are never send to any client +enum PlayerUnderwaterState +{ + UNDERWATER_NONE = 0x00, + UNDERWATER_INWATER = 0x01, // terrain type is water and player is afflicted by it + UNDERWATER_INLAVA = 0x02, // terrain type is lava and player is afflicted by it + UNDERWATER_INSLIME = 0x04, // terrain type is lava and player is afflicted by it + UNDERWATER_INDARKWATER = 0x08, // terrain type is dark water and player is afflicted by it + + UNDERWATER_EXIST_TIMERS = 0x10 +}; + +enum BuyBankSlotResult +{ + ERR_BANKSLOT_FAILED_TOO_MANY = 0, + ERR_BANKSLOT_INSUFFICIENT_FUNDS = 1, + ERR_BANKSLOT_NOTBANKER = 2, + ERR_BANKSLOT_OK = 3 +}; + +enum PlayerSpellState : uint8 +{ + PLAYERSPELL_UNCHANGED = 0, + PLAYERSPELL_CHANGED = 1, + PLAYERSPELL_NEW = 2, + PLAYERSPELL_REMOVED = 3, + PLAYERSPELL_TEMPORARY = 4 +}; + +struct PlayerSpell +{ + PlayerSpellState state : 8; + bool active : 1; // show in spellbook + bool dependent : 1; // learned as result another spell learn, skill grow, quest reward, etc + bool disabled : 1; // first rank has been learned in result talent learn but currently talent unlearned, save max learned ranks +}; + +struct PlayerTalent +{ + PlayerSpellState State; + uint8 Spec; +}; + +extern std::array const MasterySpells; + +enum TalentTree // talent tabs +{ + TALENT_TREE_WARRIOR_ARMS = 746, + TALENT_TREE_WARRIOR_FURY = 815, + TALENT_TREE_WARRIOR_PROTECTION = 845, + TALENT_TREE_PALADIN_HOLY = 831, + TALENT_TREE_PALADIN_PROTECTION = 839, + TALENT_TREE_PALADIN_RETRIBUTION = 855, + TALENT_TREE_HUNTER_BEAST_MASTERY = 811, + TALENT_TREE_HUNTER_MARKSMANSHIP = 807, + TALENT_TREE_HUNTER_SURVIVAL = 809, + TALENT_TREE_ROGUE_ASSASSINATION = 182, + TALENT_TREE_ROGUE_COMBAT = 181, + TALENT_TREE_ROGUE_SUBTLETY = 183, + TALENT_TREE_PRIEST_DISCIPLINE = 760, + TALENT_TREE_PRIEST_HOLY = 813, + TALENT_TREE_PRIEST_SHADOW = 795, + TALENT_TREE_DEATH_KNIGHT_BLOOD = 398, + TALENT_TREE_DEATH_KNIGHT_FROST = 399, + TALENT_TREE_DEATH_KNIGHT_UNHOLY = 400, + TALENT_TREE_SHAMAN_ELEMENTAL = 261, + TALENT_TREE_SHAMAN_ENHANCEMENT = 263, + TALENT_TREE_SHAMAN_RESTORATION = 262, + TALENT_TREE_MAGE_ARCANE = 799, + TALENT_TREE_MAGE_FIRE = 851, + TALENT_TREE_MAGE_FROST = 823, + TALENT_TREE_WARLOCK_AFFLICTION = 871, + TALENT_TREE_WARLOCK_DEMONOLOGY = 867, + TALENT_TREE_WARLOCK_DESTRUCTION = 865, + TALENT_TREE_DRUID_BALANCE = 752, + TALENT_TREE_DRUID_FERAL_COMBAT = 750, + TALENT_TREE_DRUID_RESTORATION = 748 +}; + +constexpr uint8 MAX_ARMOR_SPECIALIZATION_IDS = 7; +constexpr std::array ArmorSpecializationIds= { 86530, 86531, 86529, 86528, 86525, 86524, 86526 }; + +// Spell modifier (used for modify other spells) +struct SpellModifier +{ + SpellModifier(Aura* _ownerAura) : op(SpellModOp::HealingAndDamage), type(SPELLMOD_FLAT), spellId(0), ownerAura(_ownerAura) { } + virtual ~SpellModifier() = default; + + SpellModOp op; + SpellModType type; + + uint32 spellId; + Aura* const ownerAura; +}; + +struct SpellModifierByClassMask : SpellModifier +{ + SpellModifierByClassMask(Aura* _ownerAura) : SpellModifier(_ownerAura), value(0), mask() { } + + int32 value; + flag96 mask; +}; + +struct SpellModifierCompare +{ + bool operator()(SpellModifier const* left, SpellModifier const* right) const + { + // first sort by SpellModOp + if (left->op != right->op) + return left->op < right->op; + + // then by type (flat/pct) + if (left->type != right->type) + return left->type < right->type; + + return left < right; + } +}; + + +enum PlayerCurrencyState +{ + PLAYERCURRENCY_UNCHANGED = 0, + PLAYERCURRENCY_CHANGED = 1, + PLAYERCURRENCY_NEW = 2, + PLAYERCURRENCY_REMOVED = 3 //not removed just set count == 0 +}; + +struct PlayerCurrency +{ + PlayerCurrency() : State(PLAYERCURRENCY_NEW), Quantity(0), WeeklyQuantity(0), TrackedQuantity(0), Flags(0) { } + + PlayerCurrencyState State; + uint32 Quantity; + uint32 WeeklyQuantity; + uint32 TrackedQuantity; + uint8 Flags; +}; + +typedef std::unordered_map PlayerTalentMap; +typedef std::unordered_map PlayerSpellMap; +typedef Trinity::Containers::FlatSet SpellModContainer; +typedef std::unordered_map PlayerCurrenciesMap; + +typedef std::unordered_map InstanceTimeMap; + +enum ActionButtonUpdateState +{ + ACTIONBUTTON_UNCHANGED = 0, + ACTIONBUTTON_CHANGED = 1, + ACTIONBUTTON_NEW = 2, + ACTIONBUTTON_DELETED = 3 +}; + +enum ActionButtonType +{ + ACTION_BUTTON_SPELL = 0x00, + ACTION_BUTTON_C = 0x01, // click? + ACTION_BUTTON_EQSET = 0x20, + ACTION_BUTTON_DROPDOWN = 0x30, + ACTION_BUTTON_MACRO = 0x40, + ACTION_BUTTON_CMACRO = ACTION_BUTTON_C | ACTION_BUTTON_MACRO, + ACTION_BUTTON_ITEM = 0x80 +}; + +enum ReputationSource +{ + REPUTATION_SOURCE_KILL, + REPUTATION_SOURCE_QUEST, + REPUTATION_SOURCE_DAILY_QUEST, + REPUTATION_SOURCE_WEEKLY_QUEST, + REPUTATION_SOURCE_MONTHLY_QUEST, + REPUTATION_SOURCE_REPEATABLE_QUEST, + REPUTATION_SOURCE_SPELL +}; + +constexpr uint32 ACTION_BUTTON_ACTION(uint32 x) { return x & 0x00FFFFFF; } +constexpr uint32 ACTION_BUTTON_TYPE(uint32 x) { return (x & 0xFF000000) >> 24; } +constexpr uint32 MAX_ACTION_BUTTON_ACTION_VALUE = 0x00FFFFFF + 1; + +struct ActionButton +{ + ActionButton() : packedData(0), uState(ACTIONBUTTON_NEW) { } + + uint32 packedData; + ActionButtonUpdateState uState; + + // helpers + ActionButtonType GetType() const { return ActionButtonType(ACTION_BUTTON_TYPE(packedData)); } + uint32 GetAction() const { return ACTION_BUTTON_ACTION(packedData); } + void SetActionAndType(uint32 action, ActionButtonType type) + { + uint32 newData = action | (uint32(type) << 24); + if (newData != packedData || uState == ACTIONBUTTON_DELETED) + { + packedData = newData; + if (uState != ACTIONBUTTON_NEW) + uState = ACTIONBUTTON_CHANGED; + } + } +}; + +constexpr uint8 MAX_ACTION_BUTTONS = 144; //checked in 3.2.0 + +typedef std::map ActionButtonList; + +struct PvPInfo +{ + PvPInfo() : IsHostile(false), IsInHostileArea(false), IsInNoPvPArea(false), IsInFFAPvPArea(false), EndTimer(0) { } + + bool IsHostile; + bool IsInHostileArea; ///> Marks if player is in an area which forces PvP flag + bool IsInNoPvPArea; ///> Marks if player is in a sanctuary or friendly capital city + bool IsInFFAPvPArea; ///> Marks if player is in an FFAPvP area (such as Gurubashi Arena) + time_t EndTimer; ///> Time when player unflags himself for PvP (flag removed after 5 minutes) +}; + +struct DuelInfo +{ + DuelInfo() : initiator(nullptr), opponent(nullptr), startTimer(0), startTime(0), outOfBound(0), isMounted(false), isCompleted(false) { } + + Player* initiator; + Player* opponent; + time_t startTimer; + time_t startTime; + time_t outOfBound; + bool isMounted; + bool isCompleted; +}; + +struct Areas +{ + uint32 areaID; + uint32 areaFlag; + float x1; + float x2; + float y1; + float y2; +}; + +constexpr uint8 MAX_RUNES = 6; +constexpr float RUNE_BASE_COOLDOWN = 1.0f; + +enum class RuneType : uint8 +{ + + Blood = 0, + Unholy = 1, + Frost = 2, + Death = 3, + Max = 4 +}; + +struct RuneInfo +{ + RuneType BaseRune; + RuneType CurrentRune; + float Cooldown; + AuraEffect const* ConvertAura; + AuraType ConvertAuraType; + SpellInfo const* ConvertAuraInfo; +}; + +struct Runes +{ + RuneInfo _Runes[MAX_RUNES]; + uint8 RuneState; // mask of available runes + RuneType LastUsedRune; + uint8 LastUsedRuneMask; + + void SetRuneState(uint8 index, bool set = true); +}; + +struct EnchantDuration +{ + EnchantDuration() : item(nullptr), slot(MAX_ENCHANTMENT_SLOT), leftduration(0) { } + EnchantDuration(Item* _item, EnchantmentSlot _slot, uint32 _leftduration) : item(_item), slot(_slot), + leftduration(_leftduration){ ASSERT(item); } + + Item* item; + EnchantmentSlot slot; + uint32 leftduration; +}; + +typedef std::list EnchantDurationList; +typedef std::list ItemDurationList; + +enum DrunkenState +{ + DRUNKEN_SOBER = 0, + DRUNKEN_TIPSY = 1, + DRUNKEN_DRUNK = 2, + DRUNKEN_SMASHED = 3 +}; + +constexpr uint8 MAX_DRUNKEN = 4; + +enum PlayerFlags +{ + PLAYER_FLAGS_NONE = 0x00000000, + PLAYER_FLAGS_GROUP_LEADER = 0x00000001, + PLAYER_FLAGS_AFK = 0x00000002, + PLAYER_FLAGS_DND = 0x00000004, + PLAYER_FLAGS_GM = 0x00000008, + PLAYER_FLAGS_GHOST = 0x00000010, + PLAYER_FLAGS_RESTING = 0x00000020, + PLAYER_FLAGS_VOICE_CHAT = 0x00000040, + PLAYER_FLAGS_UNK7 = 0x00000080, // pre-3.0.3 PLAYER_FLAGS_FFA_PVP flag for FFA PVP state + PLAYER_FLAGS_CONTESTED_PVP = 0x00000100, // Player has been involved in a PvP combat and will be attacked by contested guards + PLAYER_FLAGS_IN_PVP = 0x00000200, + PLAYER_FLAGS_HIDE_HELM = 0x00000400, + PLAYER_FLAGS_HIDE_CLOAK = 0x00000800, + PLAYER_FLAGS_PLAYED_LONG_TIME = 0x00001000, // played long time + PLAYER_FLAGS_PLAYED_TOO_LONG = 0x00002000, // played too long time + PLAYER_FLAGS_IS_OUT_OF_BOUNDS = 0x00004000, + PLAYER_FLAGS_DEVELOPER = 0x00008000, // prefix for something? + PLAYER_FLAGS_LOW_LEVEL_RAID_ENABLED = 0x00010000, // pre-3.0.3 PLAYER_FLAGS_SANCTUARY flag for player entered sanctuary + PLAYER_FLAGS_TAXI_BENCHMARK = 0x00020000, // taxi benchmark mode (on/off) (2.0.1) + PLAYER_FLAGS_PVP_TIMER = 0x00040000, // 3.0.2, pvp timer active (after you disable pvp manually) + PLAYER_FLAGS_UBER = 0x00080000, + PLAYER_FLAGS_UNK20 = 0x00100000, + PLAYER_FLAGS_UNK21 = 0x00200000, + PLAYER_FLAGS_COMMENTATOR2 = 0x00400000, + PLAYER_FLAGS_DISABLE_CASTING_EXCEPT_ABILITIES = 0x00800000, // disables casting any ability except for the ones specified in the aura effect's SpellClassMask + PLAYER_FLAGS_DISABLE_ATTACKING_EXCEPT_ABILITIES = 0x01000000, // disables all attacks including ability casts except for the ones specified in the aura effect's SpellClassMask + PLAYER_FLAGS_NO_XP_GAIN = 0x02000000, + PLAYER_FLAGS_UNK26 = 0x04000000, + PLAYER_FLAGS_AUTO_DECLINE_GUILD = 0x08000000, // Automatically declines guild invites + PLAYER_FLAGS_GUILD_LEVEL_ENABLED = 0x10000000, // Lua_GetGuildLevelEnabled() - enables guild leveling related UI + PLAYER_FLAGS_VOID_UNLOCKED = 0x20000000, // void storage + PLAYER_FLAGS_TIMEWALKING = 0x40000000, + PLAYER_FLAGS_COMMENTATOR_CAMERA = 0x80000000 +}; + +enum PlayerBytesOffsets +{ + PLAYER_BYTES_OFFSET_SKIN_ID = 0, + PLAYER_BYTES_OFFSET_FACE_ID = 1, + PLAYER_BYTES_OFFSET_HAIR_STYLE_ID = 2, + PLAYER_BYTES_OFFSET_HAIR_COLOR_ID = 3 +}; + +enum PlayerBytes2Offsets +{ + PLAYER_BYTES_2_OFFSET_FACIAL_STYLE = 0, + PLAYER_BYTES_2_OFFSET_PARTY_TYPE = 1, + PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS = 2, + PLAYER_BYTES_2_OFFSET_REST_STATE = 3 +}; + +enum PlayerBytes3Offsets +{ + PLAYER_BYTES_3_OFFSET_GENDER = 0, + PLAYER_BYTES_3_OFFSET_INEBRIATION = 1, + PLAYER_BYTES_3_OFFSET_PVP_TITLE = 2, + PLAYER_BYTES_3_OFFSET_ARENA_FACTION = 3 +}; + +enum PlayerFieldBytesOffsets +{ + PLAYER_FIELD_BYTES_OFFSET_FLAGS = 0, + PLAYER_FIELD_BYTES_OFFSET_RAF_GRANTABLE_LEVEL = 1, + PLAYER_FIELD_BYTES_OFFSET_ACTION_BAR_TOGGLES = 2, + PLAYER_FIELD_BYTES_OFFSET_LIFETIME_MAX_PVP_RANK = 3 +}; + +enum PlayerFieldBytes2Offsets +{ + PLAYER_FIELD_BYTES_2_OFFSET_OVERRIDE_SPELLS_ID = 0, // uint16! + PLAYER_FIELD_BYTES_2_OFFSET_IGNORE_POWER_REGEN_PREDICTION_MASK = 2, + PLAYER_FIELD_BYTES_2_OFFSET_AURA_VISION = 3 +}; + +static_assert((PLAYER_FIELD_BYTES_2_OFFSET_OVERRIDE_SPELLS_ID & 1) == 0, "PLAYER_FIELD_BYTES_2_OFFSET_OVERRIDE_SPELLS_ID must be aligned to 2 byte boundary"); + +constexpr uint8 PLAYER_BYTES_2_OVERRIDE_SPELLS_UINT16_OFFSET = PLAYER_FIELD_BYTES_2_OFFSET_OVERRIDE_SPELLS_ID / 2; + +// KNOWN_TITLES is stored in 4 uint64 values, so we can represent 256 title bits (4 * 64). +constexpr uint8 KNOWN_TITLES_SIZE = 4; +constexpr uint32 MAX_TITLE_INDEX = KNOWN_TITLES_SIZE * 64; + +// used in PLAYER_FIELD_BYTES values +enum PlayerFieldByteFlags +{ + PLAYER_FIELD_BYTE_TRACK_STEALTHED = 0x00000002, + PLAYER_FIELD_BYTE_RELEASE_TIMER = 0x00000008, // Display time till auto release spirit + PLAYER_FIELD_BYTE_NO_RELEASE_WINDOW = 0x00000010 // Display no "release spirit" window at all +}; + +// used in PLAYER_FIELD_BYTES2 values +enum PlayerFieldByte2Flags +{ + PLAYER_FIELD_BYTE2_NONE = 0x00, + PLAYER_FIELD_BYTE2_STEALTH = 0x20, + PLAYER_FIELD_BYTE2_INVISIBILITY_GLOW = 0x40 +}; + +enum MirrorTimerType +{ + FATIGUE_TIMER = 0, + BREATH_TIMER = 1, + FIRE_TIMER = 2 // feign death +}; + +constexpr uint8 MAX_TIMERS = 3; +constexpr int32 DISABLED_MIRROR_TIMER = -1; + +// 2^n values +enum PlayerExtraFlags +{ + // gm abilities + PLAYER_EXTRA_GM_ON = 0x0001, + PLAYER_EXTRA_ACCEPT_WHISPERS = 0x0004, + PLAYER_EXTRA_TAXICHEAT = 0x0008, + PLAYER_EXTRA_GM_INVISIBLE = 0x0010, + PLAYER_EXTRA_GM_CHAT = 0x0020, // Show GM badge in chat messages + + // other states + PLAYER_EXTRA_PVP_DEATH = 0x0100 // store PvP death status until corpse creating. +}; + +// 2^n values +enum AtLoginFlags +{ + AT_LOGIN_NONE = 0x000, + AT_LOGIN_RENAME = 0x001, + AT_LOGIN_RESET_SPELLS = 0x002, + AT_LOGIN_RESET_TALENTS = 0x004, + AT_LOGIN_CUSTOMIZE = 0x008, + AT_LOGIN_RESET_PET_TALENTS = 0x010, + AT_LOGIN_FIRST = 0x020, + AT_LOGIN_CHANGE_FACTION = 0x040, + AT_LOGIN_CHANGE_RACE = 0x080, + AT_LOGIN_RESURRECT = 0x100 +}; + +typedef std::map QuestStatusMap; +typedef std::set RewardedQuestSet; + +enum QuestSaveType +{ + QUEST_DEFAULT_SAVE_TYPE = 0, + QUEST_DELETE_SAVE_TYPE, + QUEST_FORCE_DELETE_SAVE_TYPE +}; + +// quest +typedef std::map QuestStatusSaveMap; + + +enum QuestSlotOffsets +{ + QUEST_ID_OFFSET = 0, + QUEST_STATE_OFFSET = 1, + QUEST_COUNTS_OFFSET = 2, + QUEST_TIME_OFFSET = 4 +}; + +constexpr uint8 MAX_QUEST_OFFSET = 5; + +enum QuestSlotStateMask +{ + QUEST_STATE_NONE = 0x0000, + QUEST_STATE_COMPLETE = 0x0001, + QUEST_STATE_FAIL = 0x0002 +}; + +enum SkillUpdateState +{ + SKILL_UNCHANGED = 0, + SKILL_CHANGED = 1, + SKILL_NEW = 2, + SKILL_DELETED = 3 +}; + +struct SkillStatusData +{ + SkillStatusData(uint8 _pos, SkillUpdateState _uState) : pos(_pos), uState(_uState) + { + } + uint8 pos; + SkillUpdateState uState; +}; + +typedef std::unordered_map SkillStatusMap; + +class Quest; +class Spell; +class Item; +class WorldSession; + +enum PlayerSlots +{ + // first slot for item stored (in any way in player m_items data) + PLAYER_SLOT_START = 0, + // last+1 slot for item stored (in any way in player m_items data) + PLAYER_SLOT_END = 86, + PLAYER_SLOTS_COUNT = (PLAYER_SLOT_END - PLAYER_SLOT_START) +}; + +constexpr uint8 INVENTORY_SLOT_BAG_0 = 255; + +enum EquipmentSlots : uint8 // 19 slots +{ + EQUIPMENT_SLOT_START = 0, + EQUIPMENT_SLOT_HEAD = 0, + EQUIPMENT_SLOT_NECK = 1, + EQUIPMENT_SLOT_SHOULDERS = 2, + EQUIPMENT_SLOT_BODY = 3, + EQUIPMENT_SLOT_CHEST = 4, + EQUIPMENT_SLOT_WAIST = 5, + EQUIPMENT_SLOT_LEGS = 6, + EQUIPMENT_SLOT_FEET = 7, + EQUIPMENT_SLOT_WRISTS = 8, + EQUIPMENT_SLOT_HANDS = 9, + EQUIPMENT_SLOT_FINGER1 = 10, + EQUIPMENT_SLOT_FINGER2 = 11, + EQUIPMENT_SLOT_TRINKET1 = 12, + EQUIPMENT_SLOT_TRINKET2 = 13, + EQUIPMENT_SLOT_BACK = 14, + EQUIPMENT_SLOT_MAINHAND = 15, + EQUIPMENT_SLOT_OFFHAND = 16, + EQUIPMENT_SLOT_RANGED = 17, + EQUIPMENT_SLOT_TABARD = 18, + EQUIPMENT_SLOT_END = 19 +}; + +enum InventorySlots : uint8 // 4 slots +{ + INVENTORY_SLOT_BAG_START = 19, + INVENTORY_SLOT_BAG_END = 23 +}; + +enum InventoryPackSlots : uint8 // 16 slots +{ + INVENTORY_SLOT_ITEM_START = 23, + INVENTORY_SLOT_ITEM_END = 39 +}; + +enum BankItemSlots // 28 slots +{ + BANK_SLOT_ITEM_START = 39, + BANK_SLOT_ITEM_END = 67 +}; + +enum BankBagSlots // 7 slots +{ + BANK_SLOT_BAG_START = 67, + BANK_SLOT_BAG_END = 74 +}; + +enum BuyBackSlots // 12 slots +{ + // stored in m_buybackitems + BUYBACK_SLOT_START = 74, + BUYBACK_SLOT_END = 86 +}; + +struct ItemPosCount +{ + ItemPosCount(uint16 _pos, uint32 _count) : pos(_pos), count(_count) { } + bool isContainedIn(std::vector const& vec) const; + uint16 pos; + uint32 count; +}; +typedef std::vector ItemPosCountVec; + +constexpr uint32 JUSTICE_POINTS_CONVERSION_MONEY = 4750; + +enum TransferAbortReason +{ + TRANSFER_ABORT_NONE = 0x00, + TRANSFER_ABORT_ERROR = 0x01, + TRANSFER_ABORT_MAX_PLAYERS = 0x02, // Transfer Aborted: instance is full + TRANSFER_ABORT_NOT_FOUND = 0x03, // Transfer Aborted: instance not found + TRANSFER_ABORT_TOO_MANY_INSTANCES = 0x04, // You have entered too many instances recently. + TRANSFER_ABORT_ZONE_IN_COMBAT = 0x06, // Unable to zone in while an encounter is in progress. + TRANSFER_ABORT_INSUF_EXPAN_LVL = 0x07, // You must have expansion installed to access this area. + TRANSFER_ABORT_DIFFICULTY = 0x08, // difficulty mode is not available for %s. + TRANSFER_ABORT_UNIQUE_MESSAGE = 0x09, // Until you've escaped TLK's grasp, you cannot leave this place! + TRANSFER_ABORT_TOO_MANY_REALM_INSTANCES = 0x0A, // Additional instances cannot be launched, please try again later. + TRANSFER_ABORT_NEED_GROUP = 0x0B, // 3.1 + TRANSFER_ABORT_NOT_FOUND1 = 0x0C, // 3.1 + TRANSFER_ABORT_NOT_FOUND2 = 0x0D, // 3.1 + TRANSFER_ABORT_NOT_FOUND3 = 0x0E, // 3.2 + TRANSFER_ABORT_REALM_ONLY = 0x0F, // All players on party must be from the same realm. + TRANSFER_ABORT_MAP_NOT_ALLOWED = 0x10, // Map can't be entered at this time. + TRANSFER_ABORT_LOCKED_TO_DIFFERENT_INSTANCE = 0x12, // 4.2.2 + TRANSFER_ABORT_ALREADY_COMPLETED_ENCOUNTER = 0x13 // 4.2.2 +}; + +enum class TransferAbortedUniqueMessageArgs : uint8 +{ + None = 0, + StillWithinTheLichKingsGrasp = 1, + DestinyOfTheBilgewaterCartelStillUndecided = 2, + FateOfGilneasHasNotBeenDecidedYet = 3 +}; + +enum InstanceResetWarningType +{ + RAID_INSTANCE_WARNING_HOURS = 1, // WARNING! %s is scheduled to reset in %d hour(s). + RAID_INSTANCE_WARNING_MIN = 2, // WARNING! %s is scheduled to reset in %d minute(s)! + RAID_INSTANCE_WARNING_MIN_SOON = 3, // WARNING! %s is scheduled to reset in %d minute(s). Please exit the zone or you will be returned to your bind location! + RAID_INSTANCE_WELCOME = 4, // Welcome to %s. This raid instance is scheduled to reset in %s. + RAID_INSTANCE_EXPIRED = 5 +}; + +// PLAYER_FIELD_ARENA_TEAM_INFO_1_1 offsets +enum ArenaTeamInfoType +{ + ARENA_TEAM_ID = 0, + ARENA_TEAM_TYPE = 1, // new in 3.2 - team type? + ARENA_TEAM_MEMBER = 2, // 0 - captain, 1 - member + ARENA_TEAM_GAMES_WEEK = 3, + ARENA_TEAM_GAMES_SEASON = 4, + ARENA_TEAM_WINS_SEASON = 5, + ARENA_TEAM_PERSONAL_RATING = 6, + ARENA_TEAM_END = 7 +}; + +class InstanceSave; + +enum RestFlag +{ + REST_FLAG_IN_TAVERN = 0x1, + REST_FLAG_IN_CITY = 0x2, + REST_FLAG_IN_FACTION_AREA = 0x4 +}; + +enum TeleportToOptions +{ + TELE_TO_NONE = 0x00, + TELE_TO_GM_MODE = 0x01, + TELE_TO_NOT_LEAVE_TRANSPORT = 0x02, + TELE_TO_NOT_LEAVE_COMBAT = 0x04, + TELE_TO_NOT_UNSUMMON_PET = 0x08, + TELE_TO_SPELL = 0x10 +}; + +DEFINE_ENUM_FLAG(TeleportToOptions); + +/// Type of environmental damages +enum EnviromentalDamage +{ + DAMAGE_EXHAUSTED = 0, + DAMAGE_DROWNING = 1, + DAMAGE_FALL = 2, + DAMAGE_LAVA = 3, + DAMAGE_SLIME = 4, + DAMAGE_FIRE = 5, + DAMAGE_FALL_TO_VOID = 6 // custom case for fall without durability loss +}; + +enum PlayerChatTag +{ + CHAT_TAG_NONE = 0x00, + CHAT_TAG_AFK = 0x01, + CHAT_TAG_DND = 0x02, + CHAT_TAG_GM = 0x04, + CHAT_TAG_COM = 0x08, // Commentator + CHAT_TAG_DEV = 0x10, + CHAT_TAG_BOSS_SOUND = 0x20, // Plays "RaidBossEmoteWarning" sound on raid boss emote/whisper + CHAT_TAG_MOBILE = 0x40 +}; + +enum PlayedTimeIndex +{ + PLAYED_TIME_TOTAL = 0, + PLAYED_TIME_LEVEL = 1 +}; + +constexpr uint8 MAX_PLAYED_TIME_INDEX = 2; + +// used at player loading query list preparing, and later result selection +enum PlayerLoginQueryIndex +{ + PLAYER_LOGIN_QUERY_LOAD_FROM = 0, + PLAYER_LOGIN_QUERY_LOAD_GROUP = 1, + PLAYER_LOGIN_QUERY_LOAD_BOUND_INSTANCES = 2, + PLAYER_LOGIN_QUERY_LOAD_AURAS = 3, + PLAYER_LOGIN_QUERY_LOAD_SPELLS = 4, + PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS = 5, + PLAYER_LOGIN_QUERY_LOAD_DAILY_QUEST_STATUS = 6, + PLAYER_LOGIN_QUERY_LOAD_REPUTATION = 7, + PLAYER_LOGIN_QUERY_LOAD_INVENTORY = 8, + PLAYER_LOGIN_QUERY_LOAD_ACTIONS = 9, + PLAYER_LOGIN_QUERY_LOAD_MAILS = 10, + PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS = 11, + PLAYER_LOGIN_QUERY_LOAD_SOCIAL_LIST = 12, + PLAYER_LOGIN_QUERY_LOAD_HOME_BIND = 13, + PLAYER_LOGIN_QUERY_LOAD_SPELL_COOLDOWNS = 14, + PLAYER_LOGIN_QUERY_LOAD_DECLINED_NAMES = 15, + PLAYER_LOGIN_QUERY_LOAD_GUILD = 16, + PLAYER_LOGIN_QUERY_LOAD_ARENA_INFO = 17, + PLAYER_LOGIN_QUERY_LOAD_ACHIEVEMENTS = 18, + PLAYER_LOGIN_QUERY_LOAD_CRITERIA_PROGRESS = 19, + PLAYER_LOGIN_QUERY_LOAD_EQUIPMENT_SETS = 20, + PLAYER_LOGIN_QUERY_LOAD_BG_DATA = 21, + PLAYER_LOGIN_QUERY_LOAD_GLYPHS = 22, + PLAYER_LOGIN_QUERY_LOAD_TALENTS = 23, + PLAYER_LOGIN_QUERY_LOAD_ACCOUNT_DATA = 24, + PLAYER_LOGIN_QUERY_LOAD_SKILLS = 25, + PLAYER_LOGIN_QUERY_LOAD_WEEKLY_QUEST_STATUS = 26, + PLAYER_LOGIN_QUERY_LOAD_RANDOM_BG = 27, + PLAYER_LOGIN_QUERY_LOAD_BANNED = 28, + PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS_REW = 29, + PLAYER_LOGIN_QUERY_LOAD_INSTANCE_LOCK_TIMES = 30, + PLAYER_LOGIN_QUERY_LOAD_SEASONAL_QUEST_STATUS = 31, + PLAYER_LOGIN_QUERY_LOAD_MONTHLY_QUEST_STATUS = 32, + PLAYER_LOGIN_QUERY_LOAD_VOID_STORAGE = 33, + PLAYER_LOGIN_QUERY_LOAD_CURRENCY = 34, + PLAYER_LOGIN_QUERY_LOAD_CUF_PROFILES = 35, + PLAYER_LOGIN_QUERY_LOAD_CORPSE_LOCATION = 36, + PLAYER_LOGIN_QUERY_LOAD_ALL_PETS = 37, + PLAYER_LOGIN_QUERY_LOAD_LFG_REWARD_STATUS = 38, + MAX_PLAYER_LOGIN_QUERY +}; + +enum PlayerDelayedOperations +{ + DELAYED_SAVE_PLAYER = 0x01, + DELAYED_RESURRECT_PLAYER = 0x02, + DELAYED_SPELL_CAST_DESERTER = 0x04, + DELAYED_BG_MOUNT_RESTORE = 0x08, ///< Flag to restore mount state after teleport from BG + DELAYED_BG_TAXI_RESTORE = 0x10, ///< Flag to restore taxi state after teleport from BG + DELAYED_BG_GROUP_RESTORE = 0x20, ///< Flag to restore group state after teleport from BG + DELAYED_END +}; + +// Player summoning auto-decline time (in secs) +constexpr uint32 MAX_PLAYER_SUMMON_DELAY= 2 * MINUTE; +// Maximum money amount : 2^31 - 1 +TC_GAME_API extern uint64 const MAX_MONEY_AMOUNT; + +enum BindExtensionState +{ + EXTEND_STATE_EXPIRED = 0, + EXTEND_STATE_NORMAL = 1, + EXTEND_STATE_EXTENDED = 2, + EXTEND_STATE_KEEP = 255 // special state: keep current save type +}; +struct InstancePlayerBind +{ + InstanceSave* save; + /* permanent PlayerInstanceBinds are created in Raid/Heroic instances for players + that aren't already permanently bound when they are inside when a boss is killed + or when they enter an instance that the group leader is permanently bound to. */ + bool perm; + /* extend state listing: + EXPIRED - doesn't affect anything unless manually re-extended by player + NORMAL - standard state + EXTENDED - won't be promoted to EXPIRED at next reset period, will instead be promoted to NORMAL */ + BindExtensionState extendState; + + InstancePlayerBind() : save(nullptr), perm(false), extendState(EXTEND_STATE_NORMAL) { } +}; + +enum CharDeleteMethod +{ + CHAR_DELETE_REMOVE = 0, // Completely remove from the database + CHAR_DELETE_UNLINK = 1 // The character gets unlinked from the account, + // the name gets freed up and appears as deleted ingame +}; + +enum ReferAFriendError +{ + ERR_REFER_A_FRIEND_NONE = 0x00, + ERR_REFER_A_FRIEND_NOT_REFERRED_BY = 0x01, + ERR_REFER_A_FRIEND_TARGET_TOO_HIGH = 0x02, + ERR_REFER_A_FRIEND_INSUFFICIENT_GRANTABLE_LEVELS = 0x03, + ERR_REFER_A_FRIEND_TOO_FAR = 0x04, + ERR_REFER_A_FRIEND_DIFFERENT_FACTION = 0x05, + ERR_REFER_A_FRIEND_NOT_NOW = 0x06, + ERR_REFER_A_FRIEND_GRANT_LEVEL_MAX_I = 0x07, + ERR_REFER_A_FRIEND_NO_TARGET = 0x08, + ERR_REFER_A_FRIEND_NOT_IN_GROUP = 0x09, + ERR_REFER_A_FRIEND_SUMMON_LEVEL_MAX_I = 0x0A, + ERR_REFER_A_FRIEND_SUMMON_COOLDOWN = 0x0B, + ERR_REFER_A_FRIEND_INSUF_EXPAN_LVL = 0x0C, + ERR_REFER_A_FRIEND_SUMMON_OFFLINE_S = 0x0D +}; + +enum PlayerRestState : uint8 +{ + REST_STATE_RESTED = 0x01, + REST_STATE_NOT_RAF_LINKED = 0x02, + REST_STATE_RAF_LINKED = 0x06 +}; + +enum PlayerCommandStates +{ + CHEAT_NONE = 0x00, + CHEAT_GOD = 0x01, + CHEAT_CASTTIME = 0x02, + CHEAT_COOLDOWN = 0x04, + CHEAT_POWER = 0x08, + CHEAT_WATERWALK = 0x10 +}; + +enum PlayerLogXPReason : uint8 +{ + LOG_XP_REASON_KILL = 0, + LOG_XP_REASON_NO_KILL = 1 +}; + +struct PlayerPetData +{ + uint32 PetId; + uint32 CreatureId; + uint64 Owner; + uint32 DisplayId; + uint32 Petlevel; + uint32 PetExp; + ReactStates Reactstate; + uint8 Slot; + std::string Name; + bool Renamed; + bool Active; + uint32 SavedHealth; + uint32 SavedMana; + std::string Actionbar; + uint32 Timediff; + uint32 SummonSpellId; + PetType Type; +}; + +class Player; + +/// Holder for Battleground data +struct BGData +{ + BGData() : bgInstanceID(0), bgTypeID(BATTLEGROUND_TYPE_NONE), bgAfkReportedCount(0), bgAfkReportedTimer(0), + bgTeam(0), mountSpell(0) { bgQueuesJoinedTime.clear(); ClearTaxiPath(); } + + uint32 bgInstanceID; ///< This variable is set to bg->m_InstanceID, + /// when player is teleported to BG - (it is battleground's GUID) + BattlegroundTypeId bgTypeID; + + std::map bgQueuesJoinedTime; + + std::set bgAfkReporter; + uint8 bgAfkReportedCount; + time_t bgAfkReportedTimer; + + uint32 bgTeam; ///< What side the player will be added to + + uint32 mountSpell; + uint32 taxiPath[2]; + + WorldLocation joinPos; ///< From where player entered BG + WorldLocation leavePos; + + void ClearTaxiPath() { taxiPath[0] = taxiPath[1] = 0; } + bool HasTaxiPath() const { return taxiPath[0] && taxiPath[1]; } +}; + +struct VoidStorageItem +{ + VoidStorageItem() : ItemId(0), ItemEntry(0), ItemRandomPropertyId(), ItemSuffixFactor(0) { } + VoidStorageItem(uint64 id, uint32 entry, ObjectGuid const& creator, ItemRandomEnchantmentId randomPropertyId, uint32 suffixFactor) + : ItemId(id), ItemEntry(entry), CreatorGuid(creator), ItemRandomPropertyId(randomPropertyId), + ItemSuffixFactor(suffixFactor) { } + + uint64 ItemId; + uint32 ItemEntry; + ObjectGuid CreatorGuid; + ItemRandomEnchantmentId ItemRandomPropertyId; + uint32 ItemSuffixFactor; +}; + +struct ResurrectionData +{ + ObjectGuid GUID; + WorldLocation Location; + uint32 Health; + uint32 Mana; + uint32 Aura; +}; + +class TC_GAME_API Player : public Unit, public GridObject +{ + friend class WorldSession; + friend class CinematicMgr; + friend void AddItemToUpdateQueueOf(Item* item, Player* player); + friend void RemoveItemFromUpdateQueueOf(Item* item, Player* player); + public: + explicit Player(WorldSession* session); + ~Player(); + + PlayerAI* AI() const { return reinterpret_cast(GetAI()); } + + void CleanupsBeforeDelete(bool finalCleanup = true) override; + + void AddToWorld() override; + void RemoveFromWorld() override; + + void SetObjectScale(float scale) override; + + bool TeleportTo(uint32 mapid, float x, float y, float z, float orientation, TeleportToOptions options = TELE_TO_NONE, Optional instanceId = {}); + bool TeleportTo(WorldLocation const& loc, TeleportToOptions options = TELE_TO_NONE, Optional instanceId = {}); + bool TeleportToBGEntryPoint(); + + bool HasSummonPending() const; + void SendSummonRequestFrom(Unit* summoner); + void SummonIfPossible(bool agree); + + bool Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo); + + void Update(uint32 time) override; + + void Heartbeat() override; + + bool IsImmunedToSpellEffect(SpellInfo const* spellInfo, uint32 index, WorldObject const* caster, bool requireImmunityPurgesEffectAttribute = false) const override; + + bool IsInAreaTriggerRadius(AreaTriggerEntry const* trigger) const; + + void SendInitialPacketsBeforeAddToMap(bool firstLogin = false); + void SendInitialPacketsAfterAddToMap(); + void SendSupercededSpell(uint32 oldSpell, uint32 newSpell) const; + void SendTransferAborted(uint32 mapid, TransferAbortReason reason, uint8 arg = 0) const; + void SendInstanceResetWarning(uint32 mapid, Difficulty difficulty, uint32 time, bool welcome) const; + + bool CanInteractWithQuestGiver(Object* questGiver) const; + Creature* GetNPCIfCanInteractWith(ObjectGuid const& guid, uint32 npcflagmask) const; + GameObject* GetGameObjectIfCanInteractWith(ObjectGuid const& guid) const; + GameObject* GetGameObjectIfCanInteractWith(ObjectGuid const& guid, GameobjectTypes type) const; + + void ToggleAFK(); + void ToggleDND(); + bool isAFK() const { return HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_AFK); } + bool isDND() const { return HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_DND); } + uint8 GetChatTag() const; + std::string autoReplyMsg; + + uint32 GetBarberShopCost(uint8 newhairstyle, uint8 newhaircolor, uint8 newfacialhair, BarberShopStyleEntry const* newSkin = nullptr) const; + + PlayerSocial* GetSocial() { return m_social; } + void RemoveSocial(); + + void SendTamePetFailure(PetTameFailureReason reason); + + PlayerTaxi m_taxi; + void InitTaxiNodesForLevel() { m_taxi.InitTaxiNodesForLevel(getRace(), getClass(), getLevel()); } + bool ActivateTaxiPathTo(std::vector const& nodes, Creature* npc = nullptr, uint32 spellid = 0); + bool ActivateTaxiPathTo(uint32 taxi_path_id, uint32 spellid = 0); + void CleanupAfterTaxiFlight(); + void ContinueTaxiFlight() const; + void SendTaxiNodeStatusMultiple(); + // mount_id can be used in scripting calls + bool isAcceptWhispers() const { return (m_ExtraFlags & PLAYER_EXTRA_ACCEPT_WHISPERS) != 0; } + void SetAcceptWhispers(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_ACCEPT_WHISPERS; else m_ExtraFlags &= ~PLAYER_EXTRA_ACCEPT_WHISPERS; } + bool IsGameMaster() const { return (m_ExtraFlags & PLAYER_EXTRA_GM_ON) != 0; } + bool CanBeGameMaster() const; + void SetGameMaster(bool on); + bool isGMChat() const { return (m_ExtraFlags & PLAYER_EXTRA_GM_CHAT) != 0; } + void SetGMChat(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_GM_CHAT; else m_ExtraFlags &= ~PLAYER_EXTRA_GM_CHAT; } + bool isTaxiCheater() const { return (m_ExtraFlags & PLAYER_EXTRA_TAXICHEAT) != 0; } + void SetTaxiCheater(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_TAXICHEAT; else m_ExtraFlags &= ~PLAYER_EXTRA_TAXICHEAT; } + bool isGMVisible() const { return !(m_ExtraFlags & PLAYER_EXTRA_GM_INVISIBLE); } + void SetGMVisible(bool on); + void SetPvPDeath(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_PVP_DEATH; else m_ExtraFlags &= ~PLAYER_EXTRA_PVP_DEATH; } + + void GiveXP(uint32 xp, Unit* victim, float group_rate=1.0f); + void GiveLevel(uint8 level); + bool IsMaxLevel() const; + + void InitStatsForLevel(bool reapplyMods = false); + + // .cheat command related + bool GetCommandStatus(uint32 command) const { return (_activeCheats & command) != 0; } + void SetCommandStatusOn(uint32 command) { _activeCheats |= command; } + void SetCommandStatusOff(uint32 command) { _activeCheats &= ~command; } + + // Played Time Stuff + time_t m_logintime; + time_t m_Last_tick; + std::array m_Played_time; + uint32 GetTotalPlayedTime() const { return m_Played_time[PLAYED_TIME_TOTAL]; } + uint32 GetLevelPlayedTime() const { return m_Played_time[PLAYED_TIME_LEVEL]; } + + void setDeathState(DeathState s) override; // overwrite Unit::setDeathState + + float GetRestBonus() const { return m_rest_bonus; } + void SetRestBonus(float rest_bonus_new); + + bool HasRestFlag(RestFlag restFlag) const { return (_restFlagMask & restFlag) != 0; } + void SetRestFlag(RestFlag restFlag); + void RemoveRestFlag(RestFlag restFlag); + + uint32 GetXPRestBonus(uint32 xp); + uint32 GetInnTriggerId() const { return inn_triggerId; } + void SetInnTriggerID(uint32 id) { inn_triggerId = id; } + + Pet* GetPet() const; + Pet* SummonPet(uint32 entry, float x, float y, float z, float ang, PetType petType, uint32 despwtime); + void RemovePet(Pet* pet, PetSaveMode mode, bool returnreagent = false); + + // pet auras + std::unordered_set m_petAuras; + void AddPetAura(PetAura const* petSpell); + void RemovePetAura(PetAura const* petSpell); + + /// Handles said message in regular chat based on declared language and in config pre-defined Range. + void Say(std::string const& text, Language language, WorldObject const* = nullptr) override; + void Say(uint32 textId, WorldObject const* target = nullptr) override; + /// Handles yelled message in regular chat based on declared language and in config pre-defined Range. + void Yell(std::string const& text, Language language, WorldObject const* = nullptr) override; + void Yell(uint32 textId, WorldObject const* target = nullptr) override; + /// Outputs an universal text which is supposed to be an action. + void TextEmote(std::string const& text, WorldObject const* = nullptr, bool = false) override; + void TextEmote(uint32 textId, WorldObject const* target = nullptr, bool isBossEmote = false) override; + /// Handles whispers from Addons and players based on sender, receiver's guid and language. + void Whisper(std::string const& text, Language language, Player* receiver, bool = false) override; + void Whisper(uint32 textId, Player* target, bool isBossWhisper = false) override; + void WhisperAddon(std::string const& text, std::string const& prefix, Player* receiver); + + /*********************************************************/ + /*** STORAGE SYSTEM ***/ + /*********************************************************/ + + void SetVirtualItemSlot(uint8 i, Item* item); + void SetSheath(SheathState sheathed) override; // overwrite Unit version + uint8 FindEquipSlot(ItemTemplate const* proto, uint32 slot, bool swap) const; + uint32 GetItemCount(uint32 item, bool inBankAlso = false, Item* skipItem = nullptr) const; + uint32 GetItemCountWithLimitCategory(uint32 limitCategory, Item* skipItem = nullptr) const; + Item* GetItemByGuid(ObjectGuid guid) const; + Item* GetItemByEntry(uint32 entry) const; + Item* GetItemByPos(uint16 pos) const; + Item* GetItemByPos(uint8 bag, uint8 slot) const; + Item* GetUseableItemByPos(uint8 bag, uint8 slot) const; + Bag* GetBagByPos(uint8 slot) const; + uint32 GetFreeInventorySpace() const; + Item* GetWeaponForAttack(WeaponAttackType attackType, bool useable = false) const; + Item* GetShield(bool useable = false) const; + static WeaponAttackType GetAttackBySlot(uint8 slot); // MAX_ATTACK if not weapon slot + std::vector& GetItemUpdateQueue() { return m_itemUpdateQueue; } + static bool IsInventoryPos(uint16 pos) { return IsInventoryPos(pos >> 8, pos & 255); } + static bool IsInventoryPos(uint8 bag, uint8 slot); + static bool IsEquipmentPos(uint16 pos) { return IsEquipmentPos(pos >> 8, pos & 255); } + static bool IsEquipmentPos(uint8 bag, uint8 slot); + static bool IsBagPos(uint16 pos); + static bool IsBankPos(uint16 pos) { return IsBankPos(pos >> 8, pos & 255); } + static bool IsBankPos(uint8 bag, uint8 slot); + bool IsValidPos(uint16 pos, bool explicit_pos) const { return IsValidPos(pos >> 8, pos & 255, explicit_pos); } + bool IsValidPos(uint8 bag, uint8 slot, bool explicit_pos) const; + uint8 GetBankBagSlotCount() const { return GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS); } + void SetBankBagSlotCount(uint8 count) { SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS, count); } + bool HasItemCount(uint32 item, uint32 count = 1, bool inBankAlso = false) const; + bool HasItemFitToSpellRequirements(SpellInfo const* spellInfo, Item const* ignoreItem = nullptr) const; + bool HasAllItemsToFitToSpellRequirements(SpellInfo const* spellInfo); + bool CanNoReagentCast(SpellInfo const* spellInfo) const; + bool HasItemOrGemWithIdEquipped(uint32 item, uint32 count, uint8 except_slot = NULL_SLOT) const; + bool HasItemWithLimitCategoryEquipped(uint32 limitCategory, uint32 count, uint8 except_slot = NULL_SLOT) const; + bool HasGemWithLimitCategoryEquipped(uint32 limitCategory, uint32 count, uint8 except_slot = NULL_SLOT) const; + InventoryResult CanTakeMoreSimilarItems(Item* pItem, uint32* offendingItemId = nullptr) const; + InventoryResult CanTakeMoreSimilarItems(uint32 entry, uint32 count, uint32* offendingItemId = nullptr) const { return CanTakeMoreSimilarItems(entry, count, nullptr, nullptr, offendingItemId); } + InventoryResult CanStoreNewItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, uint32 item, uint32 count, uint32* no_space_count = nullptr) const; + InventoryResult CanStoreItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, Item* pItem, bool swap = false) const; + InventoryResult CanStoreItems(Item** items, int count, uint32* offendingItemId) const; + InventoryResult CanEquipNewItem(uint8 slot, uint16& dest, uint32 item, bool swap) const; + InventoryResult CanEquipItem(uint8 slot, uint16& dest, Item* pItem, bool swap, bool not_loading = true) const; + + InventoryResult CanEquipUniqueItem(Item* pItem, uint8 except_slot = NULL_SLOT, uint32 limit_count = 1) const; + InventoryResult CanEquipUniqueItem(ItemTemplate const* itemProto, uint8 except_slot = NULL_SLOT, uint32 limit_count = 1) const; + InventoryResult CanUnequipItems(uint32 item, uint32 count) const; + InventoryResult CanUnequipItem(uint16 src, bool swap) const; + InventoryResult CanBankItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, Item* pItem, bool swap, bool not_loading = true) const; + InventoryResult CanUseItem(Item* pItem, bool not_loading = true) const; + bool HasItemTotemCategory(uint32 TotemCategory) const; + InventoryResult CanUseItem(ItemTemplate const* pItem) const; + bool CanRollNeedForItem(ItemTemplate const* itemTemplate) const; + Item* StoreNewItem(ItemPosCountVec const& pos, uint32 item, bool update, ItemRandomEnchantmentId const& randomPropertyId = {}, GuidSet const& allowedLooters = GuidSet()); + Item* StoreItem(ItemPosCountVec const& pos, Item* pItem, bool update); + Item* EquipNewItem(uint16 pos, uint32 item, bool update); + Item* EquipItem(uint16 pos, Item* pItem, bool update); + void AutoUnequipOffhandIfNeed(bool force = false); + bool StoreNewItemInBestSlots(uint32 item_id, uint32 item_count); + void AutoStoreLoot(uint8 bag, uint8 slot, uint32 loot_id, LootStore const& store, bool broadcast = false); + void AutoStoreLoot(uint32 loot_id, LootStore const& store, bool broadcast = false) { AutoStoreLoot(NULL_BAG, NULL_SLOT, loot_id, store, broadcast); } + void StoreLootItem(ObjectGuid lootWorldObjectGuid, uint8 lootSlot, Loot* loot, GameObject* go = nullptr); + + InventoryResult CanTakeMoreSimilarItems(uint32 entry, uint32 count, Item* pItem, uint32* no_space_count = nullptr, uint32* offendingItemId = nullptr) const; + InventoryResult CanStoreItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, uint32 entry, uint32 count, Item* pItem = nullptr, bool swap = false, uint32* no_space_count = nullptr) const; + + void AddRefundReference(ObjectGuid it); + void DeleteRefundReference(ObjectGuid it); + + /// send initialization of new currency for client + void SendNewCurrency(uint32 id) const; + /// send full data about all currencies to client + void SendCurrencies() const; + /// send conquest currency points and their cap week/arena + void SendPvpRewards() const; + /// return count of currency witch has plr + uint32 GetCurrency(uint32 id, bool usePrecision) const; + /// return count of currency gaind on current week + uint32 GetCurrencyOnWeek(uint32 id, bool usePrecision) const; + /// return count of earned currency on current season + uint32 GetCurrencyOnSeason(uint32 id) const; + /// return week cap by currency id + uint32 GetCurrencyWeekCap(uint32 id, bool usePrecision) const; + /// return presence related currency + bool HasCurrency(uint32 id, uint32 count) const; + /// initialize currency count for custom initialization at create character + void SetCurrency(uint32 currencyId, uint32 amount); + void ResetCurrencyWeekCap(); + void ModifyCurrency(uint32 currencyId, int32 amount, bool supressChatLog = false, bool ignoreModifiersAndTracking = false); + + void ApplyEquipCooldown(Item* pItem); + void QuickEquipItem(uint16 pos, Item* pItem); + void VisualizeItem(uint8 slot, Item* pItem); + void SetVisibleItemSlot(uint8 slot, Item* pItem); + Item* BankItem(ItemPosCountVec const& dest, Item* pItem, bool update); + void RemoveItem(uint8 bag, uint8 slot, bool update); + void MoveItemFromInventory(uint8 bag, uint8 slot, bool update); + // in trade, auction, guild bank, mail.... + void MoveItemToInventory(ItemPosCountVec const& dest, Item* pItem, bool update, bool in_characterInventoryDB = false); + // in trade, guild bank, mail.... + void RemoveItemDependentAurasAndCasts(Item* pItem); + void DestroyItem(uint8 bag, uint8 slot, bool update); + void DestroyItemCount(uint32 item, uint32 count, bool update, bool unequip_check = false); + void DestroyItemCount(Item* item, uint32& count, bool update); + void DestroyConjuredItems(bool update); + void DestroyZoneLimitedItem(bool update, uint32 new_zone); + void SplitItem(uint16 src, uint16 dst, uint32 count); + void SwapItem(uint16 src, uint16 dst); + void AddItemToBuyBackSlot(Item* pItem); + Item* GetItemFromBuyBackSlot(uint32 slot); + void RemoveItemFromBuyBackSlot(uint32 slot, bool del); + void SendEquipError(InventoryResult msg, Item* pItem, Item* pItem2 = nullptr, uint32 itemid = 0) const; + void SendBuyError(BuyResult msg, Creature* creature, uint32 item, uint32 param) const; + void SendSellError(SellResult msg, Creature* creature, ObjectGuid guid) const; + void AddWeaponProficiency(uint32 newflag) { m_WeaponProficiency |= newflag; } + void AddArmorProficiency(uint32 newflag) { m_ArmorProficiency |= newflag; } + uint32 GetWeaponProficiency() const { return m_WeaponProficiency; } + uint32 GetArmorProficiency() const { return m_ArmorProficiency; } + bool IsUseEquipedWeapon(bool mainhand) const; + bool IsTwoHandUsed() const; + bool IsUsingTwoHandedWeaponInOneHand() const; + void SendNewItem(Item* item, uint32 count, bool received, bool created, bool broadcast = false, bool sendChatMessage = true); + bool BuyItemFromVendorSlot(ObjectGuid vendorguid, uint32 vendorslot, uint32 item, uint32 count, uint8 bag, uint8 slot); + bool BuyCurrencyFromVendorSlot(ObjectGuid vendorGuid, uint32 vendorSlot, uint32 currency, uint32 count); + bool _StoreOrEquipNewItem(uint32 vendorslot, uint32 item, uint32 count, uint8 bag, uint8 slot, int64 price, ItemTemplate const* pProto, Creature* pVendor, VendorItem const* crItem, bool bStore); + + float GetReputationPriceDiscount(Creature const* creature) const; + + Player* GetTrader() const; + TradeData* GetTradeData() const { return m_trade; } + void TradeCancel(bool sendback, TradeStatus status = TRADE_STATUS_TRADE_CANCELED); + + CinematicMgr* GetCinematicMgr() const { return _cinematicMgr.get(); } + + void UpdateEnchantTime(uint32 time); + void UpdateSoulboundTradeItems(); + void AddTradeableItem(Item* item); + void RemoveTradeableItem(Item* item); + void UpdateItemDuration(uint32 time, bool realtimeonly = false); + void AddEnchantmentDurations(Item* item); + void RemoveEnchantmentDurations(Item* item); + void RemoveEnchantmentDurationsReferences(Item* item); + void RemoveArenaEnchantments(EnchantmentSlot slot); + void AddEnchantmentDuration(Item* item, EnchantmentSlot slot, uint32 duration); + void ApplyEnchantment(Item* item, EnchantmentSlot slot, bool apply, bool apply_dur = true, bool ignore_condition = false); + void ApplyEnchantment(Item* item, bool apply); + void ApplyReforgeEnchantment(Item* item, bool apply); + void UpdateSkillEnchantments(uint16 skill_id, uint16 curr_value, uint16 new_value); + void SendEnchantmentDurations(); + void BuildEnchantmentsInfoData(WorldPacket* data); + void AddItemDurations(Item* item); + void RemoveItemDurations(Item* item); + void SendItemDurations(); + void LoadCorpse(PreparedQueryResult result); + void LoadPet(); + void LoadPetsFromDB(PreparedQueryResult result); + + bool AddItem(uint32 itemId, uint32 count); + + uint32 m_stableSlots; + + /*********************************************************/ + /*** GOSSIP SYSTEM ***/ + /*********************************************************/ + + void PrepareGossipMenu(WorldObject* source, uint32 menuId = 0, bool showQuests = false); + void SendPreparedGossip(WorldObject* source); + void OnGossipSelect(WorldObject* source, uint32 gossipListId, uint32 menuId); + + uint32 GetGossipTextId(uint32 menuId, WorldObject* source); + uint32 GetGossipTextId(WorldObject* source); + uint32 GetGossipMenuForSource(WorldObject* source); + + /*********************************************************/ + /*** QUEST SYSTEM ***/ + /*********************************************************/ + + int32 GetQuestLevel(Quest const* quest) const { return quest && (quest->GetQuestLevel() > 0) ? quest->GetQuestLevel() : getLevel(); } + + void PrepareQuestMenu(ObjectGuid guid); + void SendPreparedQuest(ObjectGuid guid); + bool IsActiveQuest(uint32 quest_id) const; + Quest const* GetNextQuest(Object const* questGiver, Quest const* quest) const; + bool CanSeeStartQuest(Quest const* quest) const; + bool CanTakeQuest(Quest const* quest, bool msg) const; + bool CanAddQuest(Quest const* quest, bool msg) const; + bool CanCompleteQuest(uint32 quest_id); + bool CanCompleteRepeatableQuest(Quest const* quest); + bool CanRewardQuest(Quest const* quest, bool msg); + bool CanRewardQuest(Quest const* quest, uint32 reward, bool msg); + void AddQuestAndCheckCompletion(Quest const* quest, Object* questGiver); + void AddQuest(Quest const* quest, Object* questGiver); + void AbandonQuest(uint32 quest_id); + void CompleteQuest(uint32 quest_id); + void IncompleteQuest(uint32 quest_id); + int32 GetQuestMoneyReward(Quest const* quest) const; + uint32 GetQuestXPReward(Quest const* quest); + void RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, bool announce = true); + void SetRewardedQuest(uint32 quest_id); + void FailQuest(uint32 quest_id); + void FailQuestsWithFlag(QuestFlags flag); + bool SatisfyQuestSkill(Quest const* qInfo, bool msg) const; + bool SatisfyQuestLevel(Quest const* qInfo, bool msg) const; + bool SatisfyQuestLog(bool msg) const; + bool SatisfyQuestDependentQuests(Quest const* qInfo, bool msg) const; + bool SatisfyQuestPreviousQuest(Quest const* qInfo, bool msg) const; + bool SatisfyQuestDependentPreviousQuests(Quest const* qInfo, bool msg) const; + bool SatisfyQuestBreadcrumbQuest(Quest const* qInfo, bool msg) const; + bool SatisfyQuestDependentBreadcrumbQuests(Quest const* qInfo, bool msg) const; + bool SatisfyQuestClass(Quest const* qInfo, bool msg) const; + bool SatisfyQuestRace(Quest const* qInfo, bool msg) const; + bool SatisfyQuestReputation(Quest const* qInfo, bool msg) const; + bool SatisfyQuestStatus(Quest const* qInfo, bool msg) const; + bool SatisfyQuestConditions(Quest const* qInfo, bool msg) const; + bool SatisfyQuestTimed(Quest const* qInfo, bool msg) const; + bool SatisfyQuestExclusiveGroup(Quest const* qInfo, bool msg) const; + bool SatisfyQuestDay(Quest const* qInfo, bool msg) const; + bool SatisfyQuestWeek(Quest const* qInfo, bool msg) const; + bool SatisfyQuestMonth(Quest const* qInfo, bool msg) const; + bool SatisfyQuestSeasonal(Quest const* qInfo, bool msg) const; + bool GiveQuestSourceItem(Quest const* quest); + bool TakeQuestSourceItem(uint32 questId, bool msg); + bool GetQuestRewardStatus(uint32 quest_id) const; + QuestStatus GetQuestStatus(uint32 quest_id) const; + void SetQuestStatus(uint32 questId, QuestStatus status, bool update = true); + void RemoveActiveQuest(uint32 questId, bool update = true); + void RemoveRewardedQuest(uint32 questId, bool update = true); + void SendQuestUpdate(uint32 questId); + QuestGiverStatus GetQuestDialogStatus(Object* questGiver); + bool SatisfyFirstLFGReward(uint32 dungeonId, uint8 maxRewCount) const; + uint8 GetFirstRewardCountForDungeonId(uint32 dungeon); + + void SetDailyQuestStatus(uint32 quest_id); + bool IsDailyQuestDone(uint32 quest_id); + void SetWeeklyQuestStatus(uint32 quest_id); + void SetMonthlyQuestStatus(uint32 quest_id); + void SetSeasonalQuestStatus(uint32 quest_id); + void SetLFGRewardStatus(uint32 dungeon_id, bool daily_reset); + void ResetDailyQuestStatus(); + void ResetWeeklyQuestStatus(); + void ResetMonthlyQuestStatus(); + void ResetSeasonalQuestStatus(uint16 event_id, time_t eventStartTime); + void ResetWeeklyLFGRewardStatus(); + void ResetDailyLFGRewardStatus(); + + uint16 FindQuestSlot(uint32 quest_id) const; + uint32 GetQuestSlotQuestId(uint16 slot) const; + uint32 GetQuestSlotState(uint16 slot) const; + uint16 GetQuestSlotCounter(uint16 slot, uint8 counter) const; + uint32 GetQuestSlotTime(uint16 slot) const; + void SetQuestSlot(uint16 slot, uint32 quest_id, uint32 timer = 0); + void SetQuestSlotCounter(uint16 slot, uint8 counter, uint16 count); + void SetQuestSlotState(uint16 slot, uint32 state); + void RemoveQuestSlotState(uint16 slot, uint32 state); + void SetQuestSlotTimer(uint16 slot, uint32 timer); + void SwapQuestSlot(uint16 slot1, uint16 slot2); + + uint16 GetReqKillOrCastCurrentCount(uint32 quest_id, int32 entry) const; + void AreaExploredOrEventHappens(uint32 questId); + void GroupEventHappens(uint32 questId, WorldObject const* pEventObject); + void ItemAddedQuestCheck(uint32 entry, uint32 count); + void ItemRemovedQuestCheck(uint32 entry, uint32 count); + void KilledMonster(CreatureTemplate const* cInfo, ObjectGuid guid); + void KilledMonsterCredit(uint32 entry, ObjectGuid guid = ObjectGuid::Empty); + void KilledPlayerCredit(uint16 count = 1); + void KilledPlayerCreditForQuest(uint16 count, Quest const* quest); + void KillCreditGO(uint32 entry, ObjectGuid guid = ObjectGuid::Empty); + void TalkedToCreature(uint32 entry, ObjectGuid guid); + void MoneyChanged(uint64 value); + void ReputationChanged(FactionEntry const* factionEntry); + void ReputationChanged2(FactionEntry const* factionEntry); + bool HasQuestForItem(uint32 itemId, uint32 excludeQuestId = 0, bool turnIn = false) const; + bool HasQuestForGO(int32 goId) const; + void UpdateVisibleGameobjectsOrSpellClicks(); + bool CanShareQuest(uint32 questId) const; + + void SendQuestComplete(Quest const* quest) const; + void SendQuestReward(Quest const* quest, Creature const* questGiver, uint32 xp); + void SendQuestFailed(uint32 questId, InventoryResult reason = EQUIP_ERR_OK) const; + void SendQuestTimerFailed(uint32 questId) const; + void SendCanTakeQuestResponse(QuestFailedReason reason) const; + void SendQuestConfirmAccept(Quest const* quest, Player* receiver) const; + void SendPushToPartyResponse(Player const* player, uint8 msg) const; + void SendQuestUpdateAddCredit(Quest const* quest, ObjectGuid guid, uint32 creatureOrGOIdx, uint16 count); + void SendQuestUpdateAddPlayer(Quest const* quest, uint16 newCount); + void SendQuestGiverStatusMultiple(); + + uint32 GetSharedQuestID() const { return m_sharedQuestId; } + ObjectGuid GetPlayerSharingQuest() const { return m_playerSharingQuest; } + void SetQuestSharingInfo(ObjectGuid guid, uint32 id) { m_playerSharingQuest = guid; m_sharedQuestId = id; } + void ClearQuestSharingInfo() { m_playerSharingQuest = ObjectGuid::Empty; m_sharedQuestId = 0; } + + // Returns the ID of the next available quest in chain of the most recently rewarded quest that has been rewarded via UI popup + uint32 GetPopupQuestId() const { return m_popupQuestId; } + void SetPopupQuestId(uint32 questId) { m_popupQuestId = questId; } + + uint32 GetInGameTime() const { return m_ingametime; } + void SetInGameTime(uint32 time) { m_ingametime = time; } + + void AddTimedQuest(uint32 questId) { m_timedquests.insert(questId); } + void RemoveTimedQuest(uint32 questId) { m_timedquests.erase(questId); } + + void SaveCUFProfile(uint8 id, std::nullptr_t) { _CUFProfiles[id] = nullptr; } ///> Empties a CUF profile at position 0-4 + void SaveCUFProfile(uint8 id, std::unique_ptr profile) { _CUFProfiles[id] = std::move(profile); } ///> Replaces a CUF profile at position 0-4 + CUFProfile* GetCUFProfile(uint8 id) const { return _CUFProfiles[id].get(); } ///> Retrieves a CUF profile at position 0-4 + uint8 GetCUFProfilesCount() const + { + uint8 count = 0; + for (uint8 i = 0; i < MAX_CUF_PROFILES; ++i) + if (_CUFProfiles[i]) + ++count; + return count; + } + + bool HasPvPForcingQuest() const; + + /*********************************************************/ + /*** LOAD SYSTEM ***/ + /*********************************************************/ + + bool LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& holder); + bool IsLoading() const override; + + void Initialize(ObjectGuid::LowType guid); + static uint32 GetZoneIdFromDB(ObjectGuid guid); + static bool LoadPositionFromDB(uint32& mapid, float& x, float& y, float& z, float& o, bool& in_flight, ObjectGuid guid); + + static bool IsValidGender(uint8 Gender) { return Gender <= GENDER_FEMALE; } + static bool ValidateAppearance(uint8 race, uint8 class_, uint8 gender, uint8 hairID, uint8 hairColor, uint8 faceID, uint8 facialHair, uint8 skinColor, bool create = false); + static bool IsValidClass(uint8 Class) { return ((1 << (Class - 1)) & CLASSMASK_ALL_PLAYABLE) != 0; } + static bool IsValidRace(uint8 Race) { return ((1 << (Race - 1)) & RACEMASK_ALL_PLAYABLE) != 0; } + + /*********************************************************/ + /*** SAVE SYSTEM ***/ + /*********************************************************/ + + void SaveToDB(bool create = false); + void SaveToDB(CharacterDatabaseTransaction trans, bool create = false); + void SaveInventoryAndGoldToDB(CharacterDatabaseTransaction& trans); // fast save function for item/money cheating preventing + void SaveGoldToDB(CharacterDatabaseTransaction& trans) const; + + static void Customize(CharacterCustomizeInfo const* customizeInfo, CharacterDatabaseTransaction& trans); + static void SavePositionInDB(WorldLocation const& loc, uint16 zoneId, ObjectGuid guid, CharacterDatabaseTransaction& trans); + + static void DeleteFromDB(ObjectGuid playerguid, uint32 accountId, bool updateRealmChars = true, bool deleteFinally = false); + static void DeleteOldCharacters(); + static void DeleteOldCharacters(uint32 keepDays); + + bool m_mailsUpdated; + + void SetBindPoint(ObjectGuid guid) const; + void SendTalentWipeConfirm(ObjectGuid guid) const; + void ResetPetTalents(); + void RegenerateAll(uint32 diff); + void RegenerateHealth() override; + void setWeaponChangeTimer(uint32 time) { m_weaponChangeTimer = time;} + + uint64 GetMoney() const { return GetUInt64Value(PLAYER_FIELD_COINAGE); } + bool ModifyMoney(int64 amount, bool sendError = true); + bool HasEnoughMoney(uint64 amount) const { return (GetMoney() >= amount); } + bool HasEnoughMoney(int64 amount) const; + void SetMoney(uint64 value); + + RewardedQuestSet const& getRewardedQuests() const { return m_RewardedQuests; } + QuestStatusMap& getQuestStatusMap() { return m_QuestStatus; } + + size_t GetRewardedQuestCount() const { return m_RewardedQuests.size(); } + bool IsQuestRewarded(uint32 quest_id) const; + + Unit* GetSelectedUnit() const; + Player* GetSelectedPlayer() const; + + void SetTarget(ObjectGuid /*guid*/) override { } /// Used for serverside target changes, does not apply to players + void SetSelection(ObjectGuid guid) { SetGuidValue(UNIT_FIELD_TARGET, guid); } + + void SendMailResult(uint32 mailId, MailResponseType mailAction, MailResponseResult mailError, uint32 equipError = 0, ObjectGuid::LowType item_guid = 0, uint32 item_count = 0) const; + void SendNewMail() const; + void UpdateNextMailTimeAndUnreads(); + void AddNewMailDeliverTime(time_t deliver_time); + + void RemoveMail(uint32 id); + + void AddMail(Mail* mail) { m_mail.push_front(mail);}// for call from WorldSession::SendMailTo + uint32 GetMailSize() { return m_mail.size();} + Mail* GetMail(uint32 id); + + PlayerMails::iterator GetMailBegin() { return m_mail.begin();} + PlayerMails::iterator GetMailEnd() { return m_mail.end();} + + void SendItemRetrievalMail(uint32 itemEntry, uint32 count); // Item retrieval mails sent by The Postmaster (34337), used in multiple places. + + /*********************************************************/ + /*** MAILED ITEMS SYSTEM ***/ + /*********************************************************/ + + uint8 unReadMails; + time_t m_nextMailDelivereTime; + + typedef std::unordered_map ItemMap; + + ItemMap mMitems; //template defined in objectmgr.cpp + + Item* GetMItem(uint32 id); + void AddMItem(Item* it); + bool RemoveMItem(uint32 id); + + void SendOnCancelExpectedVehicleRideAura() const; + bool CanControlPet(uint32 spellId = 0) const; + void PetSpellInitialize(); + void CharmSpellInitialize(); + void PossessSpellInitialize(); + void VehicleSpellInitialize(); + void SendRemoveControlBar() const; + bool HasSpell(uint32 spell) const override; + bool HasActiveSpell(uint32 spell) const; // show in spellbook + bool IsSpellFitByClassAndRace(uint32 spell_id) const; + bool HandlePassiveSpellLearn(SpellInfo const* spellInfo); + bool IsCurrentSpecMasterySpell(SpellInfo const* spellInfo) const; + + void SendProficiency(ItemClass itemClass, uint32 itemSubclassMask) const; + void SendKnownSpells(bool firstLogin = false); + bool AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading = false, uint32 fromSkill = 0); + void LearnSpell(uint32 spell_id, bool dependent, uint32 fromSkill = 0); + void RemoveSpell(uint32 spell_id, bool disabled = false, bool learn_low_rank = true); + void ResetSpells(bool myClassOnly = false); + void LearnCustomSpells(); + void LearnDefaultSkills(); + void LearnDefaultSkill(SkillRaceClassInfoEntry const* rcInfo); + void LearnQuestRewardedSpells(); + void LearnQuestRewardedSpells(Quest const* quest); + void LearnSpellHighestRank(uint32 spellid); + void AddTemporarySpell(uint32 spellId); + void RemoveTemporarySpell(uint32 spellId); + void SetReputation(uint32 factionentry, uint32 value); + uint32 GetReputation(uint32 factionentry) const; + std::string GetGuildName() const; + void SendSpellCategoryCooldowns() const; + void SendRaidGroupOnlyMessage(RaidGroupReason reason, int32 delay) const; + + // Talents + uint32 GetFreeTalentPoints() const; + void SetFreeTalentPoints(uint32 points); + uint32 GetUsedTalentCount() const; + void SetUsedTalentCount(uint32 talents); + uint32 GetQuestRewardedTalentCount() const; + void AddQuestRewardedTalentCount(uint32 points); + uint32 GetTalentResetCost() const; + void SetTalentResetCost(uint32 cost); + uint32 GetTalentResetTime() const; + void SetTalentResetTime(time_t time_); + uint32 GetPrimaryTalentTree(uint8 spec) const; + void SetPrimaryTalentTree(uint8 spec, uint32 tree); + uint8 GetActiveSpec() const; + void SetActiveSpec(uint8 spec); + uint8 GetSpecsCount() const; + void SetSpecsCount(uint8 count); + bool ResetTalents(bool no_cost = false); + uint32 GetNextResetTalentsCost() const; + void InitTalentForLevel(); + void BuildPlayerTalentsInfoData(WorldPacket* data); + void BuildPetTalentsInfoData(WorldPacket* data); + void SendTalentsInfoData(bool pet); + bool LearnTalent(uint32 talentId, uint32 talentRank); + void LearnPetTalent(ObjectGuid petGuid, uint32 talentId, uint32 talentRank); + bool LearnPrimaryTalentSpecialization(uint8 talentTabIndex); + bool AddTalent(uint32 spellId, uint8 spec, bool learning); + bool HasTalent(uint32 spell_id, uint8 spec) const; + uint32 CalculateTalentsPoints() const; + + // Armor Specializaion + void UpdateArmorSpecialization(); + + // Dual Spec + void UpdateSpecCount(uint8 count); + void ActivateSpec(uint8 spec); + void LoadActions(PreparedQueryResult result); + + void InitGlyphsForLevel(); + void SetGlyphSlot(uint8 slot, uint32 slottype) { SetUInt32Value(PLAYER_FIELD_GLYPH_SLOTS_1 + slot, slottype); } + + uint32 GetGlyphSlot(uint8 slot) const { return GetUInt32Value(PLAYER_FIELD_GLYPH_SLOTS_1 + slot); } + void SetGlyph(uint8 slot, uint32 glyph); + uint32 GetGlyph(uint8 spec, uint8 slot) const; + + PlayerTalentMap const& GetTalentMap(uint8 spec) const; + PlayerTalentMap& GetTalentMap(uint8 spec); + ActionButtonList const& GetActionButtons() const { return m_actionButtons; } + + uint32 GetFreePrimaryProfessionPoints() const { return GetUInt32Value(PLAYER_CHARACTER_POINTS); } + void SetFreePrimaryProfessions(uint16 profs) { SetUInt32Value(PLAYER_CHARACTER_POINTS, profs); } + void InitPrimaryProfessions(); + + PlayerSpellMap const& GetSpellMap() const { return m_spells; } + PlayerSpellMap & GetSpellMap() { return m_spells; } + + void AddSpellMod(SpellModifier* mod, bool apply); + static bool IsAffectedBySpellmod(SpellInfo const* spellInfo, SpellModifier const* mod, Spell* spell = nullptr); + template + void GetSpellModValues(SpellInfo const* spellInfo, SpellModOp op, Spell* spell, T base, int32* flat, float* pct) const; + template + void ApplySpellMod(SpellInfo const* spellInfo, SpellModOp op, T& basevalue, Spell* spell = nullptr) const; + static void ApplyModToSpell(SpellModifier* mod, Spell* spell); + void SetSpellModTakingSpell(Spell* spell, bool apply); + void SendSpellModifiers() const; + + void RemoveArenaSpellCooldowns(bool removeActivePetCooldowns = false); + uint32 GetLastPotionId() const { return m_lastPotionId; } + void SetLastPotionId(uint32 item_id) { m_lastPotionId = item_id; } + void UpdatePotionCooldown(Spell* spell = nullptr); + + void SetResurrectRequestData(WorldObject const* caster, uint32 health, uint32 mana, uint32 appliedAura); + void ClearResurrectRequestData() { _resurrectionData.reset(); } + + bool IsResurrectRequestedBy(ObjectGuid const& guid) const + { + if (!IsResurrectRequested()) + return false; + + return _resurrectionData->GUID == guid; + } + + bool IsResurrectRequested() const { return _resurrectionData != nullptr; } + void ResurrectUsingRequestData(); + + uint8 getCinematic() const { return m_cinematic; } + void setCinematic(uint8 cine) { m_cinematic = cine; } + + uint32 GetMovie() const { return m_movie; } + void SetMovie(uint32 movie) { m_movie = movie; } + + ActionButton* addActionButton(uint8 button, uint32 action, uint8 type); + void removeActionButton(uint8 button); + ActionButton const* GetActionButton(uint8 button); + void SendInitialActionButtons() const { SendActionButtons(0); } + void SendActionButtons(uint32 state) const; + bool IsActionButtonDataValid(uint8 button, uint32 action, uint8 type) const; + + PvPInfo pvpInfo; + void UpdatePvPState(bool onlyFFA = false); + void SetPvP(bool state) override; + void UpdatePvP(bool state, bool override = false); + void UpdateZone(uint32 newZone, uint32 newArea); + void UpdateArea(uint32 newArea); + void UpdateHostileAreaState(AreaTableEntry const* area); + void SetNeedsZoneUpdate(bool needsUpdate) { m_needsZoneUpdate = needsUpdate; } + + void UpdateZoneDependentAuras(uint32 zone_id); // zones + void UpdateAreaDependentAuras(uint32 area_id); // subzones + + void UpdateAfkReport(time_t currTime); + void UpdatePvPFlag(time_t currTime); + void SetContestedPvP(Player* attackedPlayer = nullptr); + void UpdateContestedPvP(uint32 currTime); + void SetContestedPvPTimer(uint32 newTime) {m_contestedPvPTimer = newTime;} + void ResetContestedPvP(); + + /// @todo: maybe move UpdateDuelFlag+DuelComplete to independent DuelHandler + DuelInfo* duel; + void UpdateDuelFlag(time_t currTime); + void CheckDuelDistance(time_t currTime); + void DuelComplete(DuelCompleteType type); + void SendDuelCountdown(uint32 counter); + + bool IsGroupVisibleFor(Player const* p) const; + bool IsInSameGroupWith(Player const* p) const; + bool IsInSameRaidWith(Player const* p) const; + void UninviteFromGroup(); + static void RemoveFromGroup(Group* group, ObjectGuid guid, RemoveMethod method = GROUP_REMOVEMETHOD_DEFAULT, ObjectGuid kicker = ObjectGuid::Empty, char const* reason = nullptr); + void RemoveFromGroup(RemoveMethod method = GROUP_REMOVEMETHOD_DEFAULT) { RemoveFromGroup(GetGroup(), GetGUID(), method); } + void SendUpdateToOutOfRangeGroupMembers(); + + void SetInGuild(uint32 guildId); + void SetGuildRank(uint8 rankId) { SetUInt32Value(PLAYER_GUILDRANK, rankId); } + uint8 GetGuildRank() const { return uint8(GetUInt32Value(PLAYER_GUILDRANK)); } + void SetGuildLevel(uint32 level) { SetUInt32Value(PLAYER_GUILDLEVEL, level); } + uint32 GetGuildLevel() { return GetUInt32Value(PLAYER_GUILDLEVEL); } + void SetGuildIdInvited(uint32 GuildId) { m_GuildIdInvited = GuildId; } + uint32 GetGuildId() const { return GetUInt32Value(OBJECT_FIELD_DATA); /* return only lower part */ } + Guild* GetGuild(); + int GetGuildIdInvited() const { return m_GuildIdInvited; } + static void RemovePetitionsAndSigns(ObjectGuid guid, CharterTypes type); + + // Arena Team + void SetInArenaTeam(uint32 ArenaTeamId, uint8 slot, uint8 type); + void SetArenaTeamInfoField(uint8 slot, ArenaTeamInfoType type, uint32 value); + static void LeaveAllArenaTeams(ObjectGuid guid); + uint32 GetArenaTeamId(uint8 slot) const { return GetUInt32Value(PLAYER_FIELD_ARENA_TEAM_INFO_1_1 + (slot * ARENA_TEAM_END) + ARENA_TEAM_ID); } + uint32 GetArenaPersonalRating(uint8 slot) const { return GetUInt32Value(PLAYER_FIELD_ARENA_TEAM_INFO_1_1 + (slot * ARENA_TEAM_END) + ARENA_TEAM_PERSONAL_RATING); } + void SetArenaTeamIdInvited(uint32 ArenaTeamId) { m_ArenaTeamIdInvited = ArenaTeamId; } + uint32 GetArenaTeamIdInvited() const { return m_ArenaTeamIdInvited; } + uint32 GetRBGPersonalRating() const { return 0; } + + Difficulty GetDifficulty(bool isRaid) const { return isRaid ? m_raidDifficulty : m_dungeonDifficulty; } + Difficulty GetDungeonDifficulty() const { return m_dungeonDifficulty; } + Difficulty GetRaidDifficulty() const { return m_raidDifficulty; } + Difficulty GetStoredRaidDifficulty() const { return m_raidMapDifficulty; } // only for use in difficulty packet after exiting to raid map + void SetDungeonDifficulty(Difficulty dungeon_difficulty) { m_dungeonDifficulty = dungeon_difficulty; } + void SetRaidDifficulty(Difficulty raid_difficulty) { m_raidDifficulty = raid_difficulty; } + void StoreRaidMapDifficulty(); + + bool UpdateSkill(uint32 skill_id, uint32 step); + bool UpdateSkillPro(uint16 skillId, int32 chance, uint32 step); + + bool UpdateCraftSkill(SpellInfo const* spellInfo); + bool UpdateGatherSkill(uint32 SkillId, uint32 SkillValue, uint32 RedLevel, uint32 Multiplicator = 1); + bool UpdateFishingSkill(); + + void SurveyDigSite(); + void NotifyRequestResearchHistory(); + bool ArchProjectCompleteable(uint16 projectId); + bool HasArchProject(uint16 projectId); + void CompleteArchProject(uint16 projectId); + void SetArchData(ArchData const& data); + + float GetHealthBonusFromStamina(); + float GetManaBonusFromIntellect(); + + void ApplySpellPenetrationBonus(int32 amount, bool apply); + void UpdateResistances(uint32 school) override; + void UpdateArmor() override; + void UpdateMaxHealth() override; + void UpdateMaxPower(PowerType power) override; + uint32 GetPowerIndex(PowerType power) const override; + void UpdateAttackPowerAndDamage(bool ranged = false) override; + void ApplySpellPowerBonus(int32 amount, bool apply); + void UpdateSpellDamageAndHealingBonus(); + void UpdateSpellHealingPercentDone(); + void UpdateSpellHealingPercentTaken(); + void ApplyRatingMod(CombatRating cr, int32 value, bool apply); + void UpdateRating(CombatRating cr); + void UpdateAllRatings(); + void UpdateMastery(); + bool CanUseMastery() const; + + void CalculateMinMaxDamage(WeaponAttackType attType, bool normalized, bool addTotalPct, float& minDamage, float& maxDamage) const override; + + void RecalculateRating(CombatRating cr) { ApplyRatingMod(cr, 0, true);} + float GetMeleeCritFromAgility() const; + void GetDodgeFromAgility(float &diminishing, float &nondiminishing) const; + float GetSpellCritFromIntellect() const; + float GetRatingMultiplier(CombatRating cr) const; + float GetRatingBonusValue(CombatRating cr) const; + + /// Returns base spellpower bonus from spellpower stat on items, without spellpower from intellect stat + uint32 GetBaseSpellPowerBonus() const { return m_baseSpellPower; } + int32 GetSpellPenetrationItemMod() const { return m_spellPenetrationItemMod; } + + bool CanApplyResilience() const override { return true; } + + float GetExpertiseDodgeOrParryReduction(WeaponAttackType attType) const; + void UpdateBlockPercentage(); + void UpdateCritPercentage(WeaponAttackType attType); + void UpdateAllCritPercentages(); + void UpdateParryPercentage(); + void UpdateDodgePercentage(); + void UpdateMeleeHitChances(); + void UpdateRangedHitChances(); + void UpdateSpellHitChances(); + void UpdateHitChances(); + + void UpdateAllSpellCritChances(); + void UpdateSpellCritChance(uint32 school); + void UpdateExpertise(WeaponAttackType attType); + void ApplyManaRegenBonus(int32 amount, bool apply); + void ApplyHealthRegenBonus(int32 amount, bool apply); + uint32 GetRuneTimer(uint8 index) const { return m_runeGraceCooldown[index]; } + void SetRuneTimer(uint8 index, uint32 timer) { m_runeGraceCooldown[index] = timer; } + uint32 GetLastRuneGraceTimer(uint8 index) const { return m_lastRuneGraceTimers[index]; } + void SetLastRuneGraceTimer(uint8 index, uint32 timer) { m_lastRuneGraceTimers[index] = timer; } + + ObjectGuid GetLootGUID() const { return m_lootGuid; } + void SetLootGUID(ObjectGuid guid) { m_lootGuid = guid; } + + void RemovedInsignia(Player* looterPlr); + + WorldSession* GetSession() const { return m_session; } + GameClient* GetGameClient() const; + + void BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) const override; + void DestroyForPlayer(Player* target, bool onDeath = false) const override; + + // notifiers + void SendAttackSwingCantAttack() const; + void SendAttackSwingCancelAttack() const; + void SendAttackSwingDeadTarget() const; + void SendAttackSwingNotInRange() const; + void SendAttackSwingBadFacingAttack() const; + void SendAutoRepeatCancel(Unit* target); + void SendExplorationExperience(uint32 Area, uint32 Experience) const; + + void SendDungeonDifficulty(bool IsInGroup) const; + void SendRaidDifficulty(bool IsInGroup, int32 forcedDifficulty = -1) const; + void ResetInstances(uint8 method, bool isRaid); + void SendResetInstanceSuccess(uint32 MapId) const; + void SendResetInstanceFailed(uint32 reason, uint32 MapId) const; + void SendResetFailedNotify(uint32 mapid) const; + + bool UpdatePosition(float x, float y, float z, float orientation, bool teleport = false) override; + bool UpdatePosition(Position const& pos, bool teleport = false) override { return UpdatePosition(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), pos.GetOrientation(), teleport); } + void ProcessTerrainStatusUpdate(ZLiquidStatus oldLiquidStatus, Optional const& newLiquidData) override; + void AtExitCombat() override; + + void SendMessageToSet(WorldPacket const* data, bool self) const override { SendMessageToSetInRange(data, GetVisibilityRange(), self); } + void SendMessageToSetInRange(WorldPacket const* data, float dist, bool self) const override; + void SendMessageToSetInRange(WorldPacket const* data, float dist, bool self, bool own_team_only) const; + void SendMessageToSet(WorldPacket const* data, Player const* skipped_rcvr) const override; + + Corpse* GetCorpse() const; + void SpawnCorpseBones(bool triggerSave = true); + Corpse* CreateCorpse(); + void KillPlayer(); + static void OfflineResurrect(ObjectGuid const& guid, CharacterDatabaseTransaction& trans); + bool HasCorpse() const { return _corpseLocation.GetMapId() != MAPID_INVALID; } + WorldLocation GetCorpseLocation() const { return _corpseLocation; } + uint32 GetResurrectionSpellId(); + void ResurrectPlayer(float restore_percent, bool applySickness = false); + void BuildPlayerRepop(); + void RepopAtGraveyard(); + + void DurabilityLossAll(double percent, bool inventory); + void DurabilityLoss(Item* item, double percent); + void DurabilityPointsLossAll(int32 points, bool inventory); + void DurabilityPointsLoss(Item* item, int32 points); + void DurabilityPointLossForEquipSlot(EquipmentSlots slot); + void DurabilityRepairAll(bool takeCost, float discountMod, bool guildBank); + void DurabilityRepair(uint16 pos, bool takeCost, float discountMod); + + void UpdateMirrorTimers(); + void StopMirrorTimers(); + bool IsMirrorTimerActive(MirrorTimerType type) const; + + bool CanJoinConstantChannelInZone(ChatChannelsEntry const* channel, AreaTableEntry const* zone) const; + + void JoinedChannel(Channel* c); + void LeftChannel(Channel* c); + void CleanupChannels(); + void UpdateLocalChannels(uint32 newZone); + void LeaveLFGChannel(); + + typedef std::list JoinedChannelsList; + JoinedChannelsList const& GetJoinedChannels() const { return m_channels; } + + void InitializeSkillFields(); + void SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal); + uint16 GetMaxSkillValue(uint32 skill) const; // max + perm. bonus + temp bonus + uint16 GetPureMaxSkillValue(uint32 skill) const; // max + uint16 GetSkillValue(uint32 skill) const; // skill value + perm. bonus + temp bonus + uint16 GetBaseSkillValue(uint32 skill) const; // skill value + perm. bonus + uint16 GetPureSkillValue(uint32 skill) const; // skill value + int16 GetSkillPermBonusValue(uint32 skill) const; + int16 GetSkillTempBonusValue(uint32 skill) const; + uint16 GetSkillStep(uint16 skill) const; // 0...6 + bool HasSkill(uint32 skill) const; + void LearnSkillRewardedSpells(uint32 skillId, uint32 skillValue); + + uint32 GetSkillRank(uint8 pos) const; + void SetSkillLineId(uint8 pos, uint16 value); + void SetSkillStep(uint8 pos, uint16 value); + void SetSkillRank(uint8 pos, uint16 value); + void SetSkillMaxRank(uint8 pos, uint16 value); + void SetSkillTempBonus(uint8 pos, uint16 value); + void SetSkillPermBonus(uint8 pos, uint16 value); + + WorldLocation& GetTeleportDest() { return m_teleport_dest; } + Optional GetTeleportDestInstanceId() const { return m_teleport_instanceId; } + bool IsBeingTeleported() const { return mSemaphoreTeleport_Near || mSemaphoreTeleport_Far; } + bool IsBeingTeleportedNear() const { return mSemaphoreTeleport_Near; } + bool IsBeingTeleportedFar() const { return mSemaphoreTeleport_Far; } + void SetSemaphoreTeleportNear(bool semphsetting) { mSemaphoreTeleport_Near = semphsetting; } + void SetSemaphoreTeleportFar(bool semphsetting) { mSemaphoreTeleport_Far = semphsetting; } + void ProcessDelayedOperations(); + + void CheckAreaExplore(); + void SetAreaExplored(uint32 areaId); + + // These methods are used to periodically update certain area and aura based mechanics used in Heartbeat and Movement + void UpdateZoneAndAreaId(); + void UpdateIndoorsOutdoorsAuras(); + void UpdateTavernRestingState(); + + static uint32 TeamForRace(uint8 race); + uint32 GetTeam() const { return m_team; } + TeamId GetTeamId() const { return m_team == ALLIANCE ? TEAM_ALLIANCE : TEAM_HORDE; } + void SetFactionForRace(uint8 race); + + void InitDisplayIds(); + + bool IsAtGroupRewardDistance(WorldObject const* pRewardSource) const; + bool IsAtRecruitAFriendDistance(WorldObject const* pOther) const; + void RewardPlayerAndGroupAtKill(Unit* victim, bool isBattleGround); + void RewardPlayerAndGroupAtEvent(uint32 creature_id, WorldObject* pRewardSource); + bool isHonorOrXPTarget(Unit const* victim) const; + + bool GetsRecruitAFriendBonus(bool forXP); + uint8 GetGrantableLevels() const { return m_grantableLevels; } + void SetGrantableLevels(uint8 val) { m_grantableLevels = val; } + + ReputationMgr& GetReputationMgr() { return *m_reputationMgr; } + ReputationMgr const& GetReputationMgr() const { return *m_reputationMgr; } + ReputationRank GetReputationRank(uint32 faction_id) const; + void RewardOnKill(Unit* victim, float rate); + void RewardReputation(Quest const* quest); + + int32 CalculateReputationGain(ReputationSource source, uint32 creatureOrQuestLevel, int32 rep, int32 faction, bool noQuestBonus = false); + + void UpdateSkillsForLevel(); + void ModifySkillBonus(uint32 skillid, int32 val, bool talent); + + void SetLastSoulburnSpell(SpellInfo const* spell) { m_lastSoulburnSpell = spell; } + SpellInfo const* GetLastSoulburnSpell() const { return m_lastSoulburnSpell; } + + /*********************************************************/ + /*** PVP SYSTEM ***/ + /*********************************************************/ + // TODO: Properly implement correncies as of Cataclysm + void UpdateHonorFields(); + bool RewardHonor(Unit* victim, uint32 groupsize, int32 honor = -1, bool pvptoken = false); + uint32 GetMaxPersonalArenaRatingRequirement(uint32 minarenaslot) const; + + // duel health and mana reset methods + void SaveHealthBeforeDuel() { healthBeforeDuel = GetHealth(); } + void SaveManaBeforeDuel() { manaBeforeDuel = GetPower(POWER_MANA); } + void RestoreHealthAfterDuel() { SetHealth(healthBeforeDuel); } + void RestoreManaAfterDuel() { SetPower(POWER_MANA, manaBeforeDuel); } + + //End of PvP System + + void SetDrunkValue(uint8 newDrunkValue, uint32 itemId = 0); + uint8 GetDrunkValue() const { return GetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_INEBRIATION); } + static DrunkenState GetDrunkenstateByValue(uint8 value); + + uint32 GetDeathTimer() const { return m_deathTimer; } + uint32 GetCorpseReclaimDelay(bool pvp) const; + void UpdateCorpseReclaimDelay(); + int32 CalculateCorpseReclaimDelay(bool load = false) const; + void SendCorpseReclaimDelay(uint32 delay) const; + + uint32 GetBlockPercent() const override { return GetUInt32Value(PLAYER_SHIELD_BLOCK); } + bool CanParry() const { return m_canParry; } + void SetCanParry(bool value); + bool CanBlock() const { return m_canBlock; } + void SetCanBlock(bool value); + bool CanTitanGrip() const { return m_canTitanGrip; } + void SetCanTitanGrip(bool value, uint32 penaltySpellId = 0); + void CheckTitanGripPenalty(); + bool CanTameExoticPets() const { return IsGameMaster() || HasAuraType(SPELL_AURA_ALLOW_TAME_PET_TYPE); } + + void SetRegularAttackTime(); + + void HandleBaseModFlatValue(BaseModGroup modGroup, float amount, bool apply); + void ApplyBaseModPctValue(BaseModGroup modGroup, float pct); + + void SetBaseModFlatValue(BaseModGroup modGroup, float val); + void SetBaseModPctValue(BaseModGroup modGroup, float val); + + void UpdateDamageDoneMods(WeaponAttackType attackType, int32 skipEnchantSlot = -1) override; + void UpdateBaseModGroup(BaseModGroup modGroup); + + float GetBaseModValue(BaseModGroup modGroup, BaseModType modType) const; + float GetTotalBaseModValue(BaseModGroup modGroup) const; + + void _ApplyAllStatBonuses(); + void _RemoveAllStatBonuses(); + + void ResetAllPowers(); + + SpellSchoolMask GetMeleeDamageSchoolMask(WeaponAttackType attackType = BASE_ATTACK) const override; + + void CastAllObtainSpells(); + void ApplyItemObtainSpells(Item* item, bool apply); + + void UpdateWeaponDependentCritAuras(WeaponAttackType attackType); + void UpdateAllWeaponDependentCritAuras(); + + void UpdateWeaponDependentAuras(WeaponAttackType attackType); + void ApplyItemDependentAuras(Item* item, bool apply); + + bool CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEffect const* aurEff) const override; + + void _ApplyItemMods(Item* item, uint8 slot, bool apply, bool updateItemAuras = true); + void _RemoveAllItemMods(); + void _ApplyAllItemMods(); + void _ApplyAllLevelScaleItemMods(bool apply); + ScalingStatDistributionEntry const* GetScalingStatDistributionFor(ItemTemplate const& itemTemplate) const; + ScalingStatValuesEntry const* GetScalingStatValuesFor(ItemTemplate const& itemTemplate) const; + void _ApplyItemBonuses(ItemTemplate const* proto, uint8 slot, bool apply, bool only_level_scale = false); + void _ApplyWeaponDamage(uint8 slot, ItemTemplate const* proto, bool apply); + bool EnchantmentFitsRequirements(uint32 enchantmentcondition, int8 slot) const; + void ToggleMetaGemsActive(uint8 exceptslot, bool apply); + void CorrectMetaGemEnchants(uint8 slot, bool apply); + void InitDataForForm(bool reapplyMods = false); + + void ApplyItemEquipSpell(Item* item, bool apply, bool form_change = false); + void ApplyEquipSpell(SpellInfo const* spellInfo, Item* item, bool apply, bool form_change = false); + void UpdateEquipSpellsAtFormChange(); + void CastItemCombatSpell(DamageInfo const& damageInfo); + void CastItemCombatSpell(DamageInfo const& damageInfo, Item* item, ItemTemplate const* proto); + void CastItemUseSpell(Item* item, SpellCastTargets const& targets, uint8 castId, uint32 glyphIndex); + + void SendEquipmentSetList(); + void SetEquipmentSet(EquipmentSetInfo::EquipmentSetData const& eqset); + void DeleteEquipmentSet(uint64 setGuid); + + void SendInitWorldStates(uint32 zone, uint32 area); + void SendUpdateWorldState(uint32 variable, uint32 value, bool hidden = false) const; + void SendDirectMessage(WorldPacket const* data) const; + + void SendAurasForTarget(Unit* target) const; + + std::unique_ptr PlayerTalkClass; + std::vector> ItemSetEff; + + void SendLoot(ObjectGuid guid, LootType loot_type); + void SendLootError(ObjectGuid guid, LootError error) const; + void SendLootRelease(ObjectGuid guid) const; + void SendNotifyLootItemRemoved(uint8 lootSlot) const; + void SendNotifyCurrencyLootRemoved(uint8 lootSlot); + void SendNotifyLootMoneyRemoved() const; + + /*********************************************************/ + /*** BATTLEGROUND SYSTEM ***/ + /*********************************************************/ + + bool InBattleground() const { return m_bgData.bgInstanceID != 0; } + bool InArena() const; + uint32 GetBattlegroundId() const { return m_bgData.bgInstanceID; } + BattlegroundTypeId GetBattlegroundTypeId() const { return m_bgData.bgTypeID; } + Battleground* GetBattleground() const; + + uint32 GetBattlegroundQueueJoinTime(uint32 bgTypeId) const; + void AddBattlegroundQueueJoinTime(uint32 bgTypeId, uint32 joinTime); + void RemoveBattlegroundQueueJoinTime(uint32 bgTypeId); + + bool InBattlegroundQueue() const; + + BattlegroundQueueTypeId GetBattlegroundQueueTypeId(uint32 index) const; + uint32 GetBattlegroundQueueIndex(BattlegroundQueueTypeId bgQueueTypeId) const; + bool IsInvitedForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const; + bool InBattlegroundQueueForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const; + + void SetBattlegroundId(uint32 val, BattlegroundTypeId bgTypeId); + uint32 AddBattlegroundQueueId(BattlegroundQueueTypeId val); + bool HasFreeBattlegroundQueueId() const; + void RemoveBattlegroundQueueId(BattlegroundQueueTypeId val); + void SetInviteForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId, uint32 instanceId); + bool IsInvitedForBattlegroundInstance(uint32 instanceId) const; + WorldLocation const& GetBattlegroundEntryPoint() const { return m_bgData.joinPos; } + void SetBattlegroundEntryPoint(); + + void SetBGTeam(uint32 team); + uint32 GetBGTeam() const; + + void LeaveBattleground(bool teleportToEntryPoint = true); + bool CanJoinToBattleground(Battleground const* bg) const; + bool CanReportAfkDueToLimit(); + void ReportedAfkBy(Player* reporter); + void ClearAfkReports() { m_bgData.bgAfkReporter.clear(); } + + bool GetBGAccessByLevel(BattlegroundTypeId bgTypeId) const; + bool isTotalImmunity() const; + bool CanUseBattlegroundObject(GameObject* gameobject) const; + bool isTotalImmune() const; + bool CanCaptureTowerPoint() const; + + bool GetRandomWinner() const { return m_IsBGRandomWinner; } + void SetRandomWinner(bool isWinner); + + /*********************************************************/ + /*** OUTDOOR PVP SYSTEM ***/ + /*********************************************************/ + + OutdoorPvP* GetOutdoorPvP() const; + // returns true if the player is in active state for outdoor pvp objective capturing, false otherwise + bool IsOutdoorPvPActive() const; + + /*********************************************************/ + /*** ENVIROMENTAL SYSTEM ***/ + /*********************************************************/ + + bool IsImmuneToEnvironmentalDamage() const; + uint32 EnvironmentalDamage(EnviromentalDamage type, uint32 damage); + + /*********************************************************/ + /*** FLOOD FILTER SYSTEM ***/ + /*********************************************************/ + + struct ChatFloodThrottle + { + enum Index + { + REGULAR = 0, + ADDON = 1, + MAX + }; + + time_t Time = 0; + uint32 Count = 0; + }; + + void UpdateSpeakTime(ChatFloodThrottle::Index index); + + /*********************************************************/ + /*** VARIOUS SYSTEMS ***/ + /*********************************************************/ + void UpdateFallInformationIfNeed(MovementInfo const& minfo, uint16 opcode); + + WorldObject* m_seer; + void SetFallInformation(uint32 time, float z); + void HandleFall(MovementInfo const& movementInfo); + bool SetDisableGravity(bool disable, bool updateAnimTier = true) override; + bool SetCanFly(bool enable) override; + bool SetCanTransitionBetweenSwimAndFly(bool enable) override; + void SendMovementSetCollisionHeight(float height, UpdateCollisionHeightReason reason); + + void SetClientControl(Unit* target, bool allowMove); + + void SetSeer(WorldObject* target) { m_seer = target; } + void SetViewpoint(WorldObject* target, bool apply); + WorldObject* GetViewpoint() const; + void StopCastingCharm(); + void StopCastingBindSight() const; + + uint32 GetSaveTimer() const { return m_nextSave; } + void SetSaveTimer(uint32 timer) { m_nextSave = timer; } + + void SaveRecallPosition() + { + m_recall_location.WorldRelocate(*this); + m_recall_instanceId = GetInstanceId(); + } + void Recall() { TeleportTo(m_recall_location, TELE_TO_NONE, m_recall_instanceId); } + + void SetHomebind(WorldLocation const& loc, uint32 areaId); + void SendBindPointUpdate() const; + + // Homebind coordinates + uint32 m_homebindMapId; + uint16 m_homebindAreaId; + float m_homebindX; + float m_homebindY; + float m_homebindZ; + + WorldLocation GetStartPosition() const; + + // currently visible objects at player client + GuidUnorderedSet m_clientGUIDs; + GuidUnorderedSet m_visibleTransports; + + bool HaveAtClient(Object const* u) const; + + bool IsNeverVisible() const override; + + bool IsVisibleGloballyFor(Player const* player) const; + + void SendInitialVisiblePackets(Unit* target) const; + void UpdateObjectVisibility(bool forced = true) override; + void UpdateVisibilityForPlayer(); + void UpdateVisibilityOf(WorldObject* target); + void UpdateTriggerVisibility(); + + template + void UpdateVisibilityOf(T* target, UpdateData& data, std::set& visibleNow); + + uint8 m_forced_speed_changes[MAX_MOVE_TYPE]; + + bool HasAtLoginFlag(AtLoginFlags f) const { return (m_atLoginFlags & f) != 0; } + void SetAtLoginFlag(AtLoginFlags f) { m_atLoginFlags |= f; } + void RemoveAtLoginFlag(AtLoginFlags flags, bool persist = false); + + bool isUsingLfg() const; + bool inRandomLfgDungeon() const; + + void GetLFGLeavePoint(Position* pos); + bool HasValidLFGLeavePoint(uint32 mapid); + void SetLFGLeavePoint(); + + // Temporarily removed pet cache + uint32 GetTemporaryUnsummonedPetNumber() const { return m_temporaryUnsummonedPetNumber; } + void SetTemporaryUnsummonedPetNumber(uint32 petnumber) { m_temporaryUnsummonedPetNumber = petnumber; } + void UnsummonPetTemporaryIfAny(); + void ResummonPetTemporaryUnSummonedIfAny(); + bool IsPetNeedBeTemporaryUnsummoned() const; + + void SendCinematicStart(uint32 cinematicId); + void SendMovieStart(uint32 movieId); + + uint32 DoRandomRoll(uint32 minimum, uint32 maximum); + + /*********************************************************/ + /*** INSTANCE SYSTEM ***/ + /*********************************************************/ + + typedef std::unordered_map< uint32 /*mapId*/, InstancePlayerBind > BoundInstancesMap; + + void UpdateHomebindTime(uint32 time); + + uint32 m_HomebindTimer; + bool m_InstanceValid; + // permanent binds and solo binds by difficulty + BoundInstancesMap m_boundInstances[MAX_DIFFICULTY]; + InstancePlayerBind* GetBoundInstance(uint32 mapid, Difficulty difficulty, bool withExpired = false); + InstancePlayerBind const* GetBoundInstance(uint32 mapid, Difficulty difficulty, bool withExpired = false) const; + BoundInstancesMap& GetBoundInstances(Difficulty difficulty) { return m_boundInstances[difficulty]; } + InstanceSave* GetInstanceSave(uint32 mapid, bool raid); + void UnbindInstance(uint32 mapid, Difficulty difficulty, bool unload = false); + void UnbindInstance(BoundInstancesMap::iterator &itr, Difficulty difficulty, bool unload = false); + InstancePlayerBind* BindToInstance(InstanceSave* save, bool permanent, BindExtensionState extendState = EXTEND_STATE_NORMAL, bool load = false); + void BindToInstance(); + void SetPendingBind(uint32 instanceId, uint32 bindTimer); + bool HasPendingBind() const { return _pendingBindId > 0; } + void SendRaidInfo(); + void SendSavedInstances(); + bool Satisfy(AccessRequirement const* ar, uint32 target_map, bool report = false); + bool CheckInstanceValidity(bool /*isLogin*/); + bool CheckInstanceCount(uint32 instanceId) const; + void AddInstanceEnterTime(uint32 instanceId, time_t enterTime); + + // last used pet number (for BG's) + uint32 GetLastPetNumber() const { return m_lastpetnumber; } + void SetLastPetNumber(uint32 petnumber) { m_lastpetnumber = petnumber; } + + /*********************************************************/ + /*** GROUP SYSTEM ***/ + /*********************************************************/ + + bool IsInGroup(ObjectGuid groupGuid) const; + Group* GetGroupInvite() const { return m_groupInvite; } + void SetGroupInvite(Group* group) { m_groupInvite = group; } + Group* GetGroup() { return m_group.getTarget(); } + Group const* GetGroup() const { return const_cast(m_group.getTarget()); } + GroupReference& GetGroupRef() { return m_group; } + void SetGroup(Group* group, int8 subgroup = -1); + uint8 GetSubGroup() const { return m_group.getSubGroup(); } + uint32 GetGroupUpdateFlag() const { return m_groupUpdateMask; } + void SetGroupUpdateFlag(uint32 flag) { m_groupUpdateMask |= flag; } + uint64 GetAuraUpdateMaskForRaid() const { return m_auraRaidUpdateMask; } + void SetAuraUpdateMaskForRaid(uint8 slot) { m_auraRaidUpdateMask |= (uint64(1) << slot); } + Player* GetNextRandomRaidMember(float radius); + PartyResult CanUninviteFromGroup(ObjectGuid guidMember = ObjectGuid::Empty) const; + + // Battleground / Battlefield Group System + void SetBattlegroundOrBattlefieldRaid(Group* group, int8 subgroup = -1); + void RemoveFromBattlegroundOrBattlefieldRaid(); + Group* GetOriginalGroup() const { return m_originalGroup.getTarget(); } + GroupReference& GetOriginalGroupRef() { return m_originalGroup; } + uint8 GetOriginalSubGroup() const { return m_originalGroup.getSubGroup(); } + void SetOriginalGroup(Group* group, int8 subgroup = -1); + + void SetPassOnGroupLoot(bool bPassOnGroupLoot) { m_bPassOnGroupLoot = bPassOnGroupLoot; } + bool GetPassOnGroupLoot() const { return m_bPassOnGroupLoot; } + + MapReference &GetMapRef() { return m_mapRef; } + + // Set map to player and add reference + void SetMap(Map* map) override; + void ResetMap() override; + + bool isAllowedToLoot(Creature const* creature); + + DeclinedName const* GetDeclinedNames() const { return m_declinedname.get(); } + + // Runes + uint8 GetRunesState() const { return m_runes->RuneState; } + RuneType GetBaseRune(uint8 index) const { return RuneType(m_runes->_Runes[index].BaseRune); } + RuneType GetCurrentRune(uint8 index) const { return RuneType(m_runes->_Runes[index].CurrentRune); } + float GetRuneCooldown(uint8 index) const { return m_runes->_Runes[index].Cooldown; } + bool IsRuneFullyDepleted(uint8 index) const; + /// + /// Checks if there is a fully depleted rune. A rune is considered fully depleted when it's on cooldown and isn't recovering yet ('waiting' for another rune to finish regenerating) + /// + /// The kind of base rune that is being checked (Blood, Unholy, Frost). Death Runes are no base rune and can't be checked with this method. Use the underlying base rune instead. + /// true when there is a fully depleted rune + bool HasFullyDepletedRune(RuneType runeType) const; + RuneType GetLastUsedRune() { return m_runes->LastUsedRune; } + uint8 GetLastUsedRuneMask() { return m_runes->LastUsedRuneMask; } + void ClearLastUsedRuneMask() { m_runes->LastUsedRuneMask = 0; } + void SetLastUsedRune(RuneType type) { m_runes->LastUsedRune = type; } + void SetLastUsedRuneIndex(uint8 index) { m_runes->LastUsedRuneMask |= (1 << index); } + void SetBaseRune(uint8 index, RuneType baseRune) { m_runes->_Runes[index].BaseRune = baseRune; } + void SetCurrentRune(uint8 index, RuneType currentRune) { m_runes->_Runes[index].CurrentRune = currentRune; } + void SetRuneConvertAura(uint8 index, AuraEffect const* aura, AuraType auraType, SpellInfo const* spellInfo); + void AddRuneByAuraEffect(uint8 index, RuneType newType, AuraEffect const* aura, AuraType auraType, SpellInfo const* spellInfo); + void SetRuneCooldown(uint8 index, float cooldown); + void RemoveRunesByAuraEffect(AuraEffect const* aura); + void RestoreBaseRune(uint8 index); + void ConvertRune(uint8 index, RuneType newType); + void SendConvertedRunes(); + void ResyncRunes() const; + void AddRunePower(uint8 mask) const; + void InitRunes(); + + void SendRespondInspectAchievements(Player* player) const; + uint32 GetAchievementPoints() const; + bool HasAchieved(uint32 achievementId) const; + void ResetAchievements(); + void FailAchievementCriteria(AchievementCriteriaFailEvent condition, int32 failAsset); + void UpdateAchievementCriteria(AchievementCriteriaTypes type, uint64 miscValue1 = 0, uint64 miscValue2 = 0, uint64 miscValue3 = 0, WorldObject* ref = nullptr, GameObject* go = nullptr); + void StartAchievementCriteria(AchievementCriteriaStartEvent startEvent, uint32 entry, Milliseconds timeLost = Milliseconds::zero()); + void CompletedAchievement(AchievementEntry const* entry); + + bool HasTitle(uint32 bitIndex) const; + bool HasTitle(CharTitlesEntry const* title) const; + void SetTitle(CharTitlesEntry const* title, bool lost = false); + + //bool isActiveObject() const { return true; } + bool CanSeeSpellClickOn(Creature const* creature) const; + + uint32 GetChampioningFaction() const { return m_ChampioningFaction; } + void SetChampioningFaction(uint32 faction) { m_ChampioningFaction = faction; } + uint8 GetExpansionForFaction(uint32 factionId) const; + + Spell* m_spellModTakingSpell; + + float GetAverageItemLevel() const; + bool isDebugAreaTriggers; + + void ClearWhisperWhiteList() { WhisperList.clear(); } + void AddWhisperWhiteList(ObjectGuid guid) { WhisperList.push_back(guid); } + bool IsInWhisperWhiteList(ObjectGuid guid); + void RemoveFromWhisperWhiteList(ObjectGuid guid) { WhisperList.remove(guid); } + + void ReadMovementInfo(WorldPacket& data, MovementInfo* mi, Movement::ExtraMovementStatusElement* extras = nullptr); + + /*! These methods send different packets to the client in apply and unapply case. + These methods are only sent to the current unit. + */ + + void ValidateMovementInfo(MovementInfo* mi); + + bool CanFly() const override { return m_movementInfo.HasMovementFlag(MOVEMENTFLAG_CAN_FLY); } + bool CanEnterWater() const override { return true; } + + std::string GetMapAreaAndZoneString() const; + std::string GetCoordsMapAreaAndZoneString() const; + + // Void Storage + bool IsVoidStorageUnlocked() const { return HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_VOID_UNLOCKED); } + void UnlockVoidStorage(); + void LockVoidStorage(); + uint8 GetNextVoidStorageFreeSlot() const; + uint8 GetNumOfVoidStorageFreeSlots() const; + uint8 AddVoidStorageItem(VoidStorageItem const& item); + void AddVoidStorageItemAtSlot(uint8 slot, const VoidStorageItem& item); + void DeleteVoidStorageItem(uint8 slot); + bool SwapVoidStorageItem(uint8 oldSlot, uint8 newSlot); + VoidStorageItem* GetVoidStorageItem(uint8 slot) const; + VoidStorageItem* GetVoidStorageItem(uint64 id, uint8& slot) const; + + // Pets + PlayerPetData* GetPlayerPetDataById(uint32 petId); + PlayerPetData* GetPlayerPetDataBySlot(uint8 slot); + PlayerPetData* GetPlayerPetDataByCreatureId(uint32 creatureId); + PlayerPetData* GetPlayerPetDataCurrent(); + Optional GetFirstUnusedActivePetSlot(); + Optional GetFirstUnusedPetSlot(); + void DeleteFromPlayerPetDataStore(uint32 petNumber); + void AddToPlayerPetDataStore(std::unique_ptr playerPetData); + + protected: + // Gamemaster whisper whitelist + GuidList WhisperList; + uint32 m_contestedPvPTimer; + + /*********************************************************/ + /*** BATTLEGROUND SYSTEM ***/ + /*********************************************************/ + + /* + this is an array of BG queues (BgTypeIDs) in which is player + */ + struct BgBattlegroundQueueID_Rec + { + BattlegroundQueueTypeId bgQueueTypeId; + uint32 invitedToInstance; + }; + + BgBattlegroundQueueID_Rec m_bgBattlegroundQueueID[PLAYER_MAX_BATTLEGROUND_QUEUES]; + BGData m_bgData; + + bool m_IsBGRandomWinner; + + /*********************************************************/ + /*** QUEST SYSTEM ***/ + /*********************************************************/ + + //We allow only one timed quest active at the same time. Below can then be simple value instead of set. + typedef std::set QuestSet; + typedef std::unordered_map SeasonalQuestMapByQuest; + typedef std::unordered_map SeasonalQuestMapByEvent; + + struct LFGRewardInfo + { + LFGRewardInfo(uint8 _completionsThisPeriod = 0, bool _isDaily = false) : + CompletionsThisPeriod(_completionsThisPeriod), IsDaily(_isDaily) { } + + uint8 CompletionsThisPeriod; + bool IsDaily; + }; + + typedef std::unordered_map LFGRewardStatusMap; + + QuestSet m_timedquests; + QuestSet m_weeklyquests; + QuestSet m_monthlyquests; + SeasonalQuestMapByEvent m_seasonalquests; + LFGRewardStatusMap m_lfgrewardstatus; + + ObjectGuid m_playerSharingQuest; + uint32 m_sharedQuestId; + uint32 m_popupQuestId; + uint32 m_ingametime; + + /*********************************************************/ + /*** LOAD SYSTEM ***/ + /*********************************************************/ + + void _LoadActions(PreparedQueryResult result); + void _LoadAuras(PreparedQueryResult result, uint32 timediff); + void _LoadGlyphAuras(); + void _LoadBoundInstances(PreparedQueryResult result); + void _LoadInventory(PreparedQueryResult result, uint32 timeDiff); + void _LoadVoidStorage(PreparedQueryResult result); + void _LoadMail(PreparedQueryResult mailsResult, PreparedQueryResult mailItemsResult); + static Item* _LoadMailedItem(ObjectGuid const& playerGuid, Player* player, uint32 mailId, Mail* mail, Field* fields); + void _LoadQuestStatus(PreparedQueryResult result); + void _LoadQuestStatusRewarded(PreparedQueryResult result); + void _LoadDailyQuestStatus(PreparedQueryResult result); + void _LoadWeeklyQuestStatus(PreparedQueryResult result); + void _LoadMonthlyQuestStatus(PreparedQueryResult result); + void _LoadSeasonalQuestStatus(PreparedQueryResult result); + void _LoadRandomBGStatus(PreparedQueryResult result); + void _LoadGroup(PreparedQueryResult result); + void _LoadSkills(PreparedQueryResult result); + void _LoadSpells(PreparedQueryResult result); + bool _LoadHomeBind(PreparedQueryResult result); + void _LoadDeclinedNames(PreparedQueryResult result); + void _LoadArenaTeamInfo(PreparedQueryResult result); + void _LoadEquipmentSets(PreparedQueryResult result); + void _LoadBGData(PreparedQueryResult result); + void _LoadGlyphs(PreparedQueryResult result); + void _LoadTalents(PreparedQueryResult result); + void _LoadInstanceTimeRestrictions(PreparedQueryResult result); + void _LoadCurrency(PreparedQueryResult result); + void _LoadCUFProfiles(PreparedQueryResult result); + void _LoadLFGRewardStatus(PreparedQueryResult result); + + /*********************************************************/ + /*** SAVE SYSTEM ***/ + /*********************************************************/ + + void _SaveActions(CharacterDatabaseTransaction& trans); + void _SaveAuras(CharacterDatabaseTransaction& trans); + void _SaveInventory(CharacterDatabaseTransaction& trans); + void _SaveVoidStorage(CharacterDatabaseTransaction& trans); + void _SaveMail(CharacterDatabaseTransaction& trans); + void _SaveQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveDailyQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveWeeklyQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveMonthlyQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveSeasonalQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveSkills(CharacterDatabaseTransaction& trans); + void _SaveSpells(CharacterDatabaseTransaction& trans); + void _SaveEquipmentSets(CharacterDatabaseTransaction& trans); + void _SaveBGData(CharacterDatabaseTransaction& trans); + void _SaveGlyphs(CharacterDatabaseTransaction& trans) const; + void _SaveTalents(CharacterDatabaseTransaction& trans); + void _SaveStats(CharacterDatabaseTransaction& trans) const; + void _SaveInstanceTimeRestrictions(CharacterDatabaseTransaction& trans); + void _SaveCurrency(CharacterDatabaseTransaction& trans); + void _SaveCUFProfiles(CharacterDatabaseTransaction& trans); + void _SaveLFGRewardStatus(CharacterDatabaseTransaction& trans); + + /*********************************************************/ + /*** ENVIRONMENTAL SYSTEM ***/ + /*********************************************************/ + void HandleSobering(); + void SendMirrorTimer(MirrorTimerType Type, uint32 MaxValue, uint32 CurrentValue, int32 Regen); + void StopMirrorTimer(MirrorTimerType Type); + void HandleDrowning(uint32 time_diff); + int32 getMaxTimer(MirrorTimerType timer) const; + + /*********************************************************/ + /*** HONOR SYSTEM ***/ + /*********************************************************/ + time_t m_lastHonorUpdateTime; + + void outDebugValues() const; + ObjectGuid m_lootGuid; + + uint32 m_team; + uint32 m_nextSave; + std::array m_chatFloodData; + Difficulty m_dungeonDifficulty; + Difficulty m_raidDifficulty; + Difficulty m_raidMapDifficulty; + + uint32 m_atLoginFlags; + + std::array m_items; + uint32 m_currentBuybackSlot; + + PlayerCurrenciesMap _currencyStorage; + + /** + * @name GetCurrencyWeekCap + * @brief return week cap for selected currency + + * @param CurrencyTypesEntry for which to retrieve weekly cap + */ + uint32 GetCurrencyWeekCap(CurrencyTypesEntry const* currency) const; + + /* + * @name GetCurrencyTotalCap + * @brief return total cap for selected currency + + * @param CurrencyTypesEntry for which to retrieve total cap + */ + uint32 GetCurrencyTotalCap(CurrencyTypesEntry const* currency) const; + + /// Updates weekly conquest point cap (dynamic cap) + void UpdateConquestCurrencyCap(uint32 currency); + + std::array, VOID_STORAGE_MAX_SLOT> _voidStorageItems; + + std::vector m_itemUpdateQueue; + bool m_itemUpdateQueueBlocked; + + uint32 m_ExtraFlags; + + private: + EnumFlag _characterFlags; + EnumFlag _characterFlags2; + public: + EnumFlag GetCharacterFlags() const { return _characterFlags; } + void AddCharacterFlag(CharacterFlags flag) { _characterFlags |= flag; } + void RemoveCharacterFlag(CharacterFlags flag) { _characterFlags &= ~flag; } + bool HasCharacterFlag(CharacterFlags flag) const { return _characterFlags.HasFlag(flag); } + + EnumFlag GetCharacterFlags2() const { return _characterFlags2; } + void AddCharacterFlag(CharacterFlags2 flag) { _characterFlags2 |= flag; } + void RemoveCharacterFlag(CharacterFlags2 flag) { _characterFlags2 &= ~flag; } + bool HasCharacterFlag(CharacterFlags2 flag) const { return _characterFlags2.HasFlag(flag); } + + void InitializePlayerFlags(); + + protected: + QuestStatusMap m_QuestStatus; + QuestStatusSaveMap m_QuestStatusSave; + + RewardedQuestSet m_RewardedQuests; + QuestStatusSaveMap m_RewardedQuestsSave; + + SkillStatusMap mSkillStatus; + + uint32 m_GuildIdInvited; + uint32 m_ArenaTeamIdInvited; + + PlayerMails m_mail; + PlayerSpellMap m_spells; + uint32 m_lastPotionId; // last used health/mana potion in combat, that block next potion use + + std::unique_ptr _talentMgr; + + ActionButtonList m_actionButtons; + + std::array m_auraBaseFlatMod; + std::array m_auraBasePctMod; + std::array m_baseRatingValue; + uint32 m_baseSpellPower; + uint32 m_baseManaRegen; + uint32 m_baseHealthRegen; + int32 m_spellPenetrationItemMod; + + SpellModContainer m_spellMods; + + EnchantDurationList m_enchantDuration; + ItemDurationList m_itemDuration; + GuidUnorderedSet m_itemSoulboundTradeable; + + std::unique_ptr _resurrectionData; + WorldSession* m_session; + + JoinedChannelsList m_channels; + + uint8 m_cinematic; + uint32 m_movie; + + TradeData* m_trade; + + bool m_DailyQuestChanged; + bool m_WeeklyQuestChanged; + bool m_MonthlyQuestChanged; + bool m_SeasonalQuestChanged; + bool m_LFGRewardStatusChanged; + time_t m_lastDailyQuestTime; + + uint32 m_hostileReferenceCheckTimer; + uint32 m_drunkTimer; + uint32 m_weaponChangeTimer; + + uint32 m_zoneUpdateId; + uint32 m_areaUpdateId; + + uint32 m_deathTimer; + time_t m_deathExpireTime; + + uint32 m_WeaponProficiency; + uint32 m_ArmorProficiency; + bool m_canParry; + bool m_canBlock; + bool m_canTitanGrip; + uint32 m_titanGripPenaltySpellId; + uint8 m_swingErrorMsg; + + ////////////////////Rest System///////////////////// + time_t _restTime; + uint32 inn_triggerId; + float m_rest_bonus; + uint32 _restFlagMask; + ////////////////////Rest System///////////////////// + + // Social + PlayerSocial* m_social; + + // Groups + GroupReference m_group; + GroupReference m_originalGroup; + Group* m_groupInvite; + uint32 m_groupUpdateMask; + uint64 m_auraRaidUpdateMask; + bool m_bPassOnGroupLoot; + + // last used pet number (for BG's) + uint32 m_lastpetnumber; + + // Player summoning + time_t m_summon_expire; + WorldLocation m_summon_location; + uint32 m_summon_instanceId; + + // Recall position + WorldLocation m_recall_location; + uint32 m_recall_instanceId; + + std::unique_ptr m_declinedname; + std::unique_ptr m_runes; + EquipmentSetContainer _equipmentSets; + + bool CanAlwaysSee(WorldObject const* obj) const override; + + bool IsAlwaysDetectableFor(WorldObject const* seer) const override; + + uint8 m_grantableLevels; + + uint8 m_fishingSteps; + + bool m_needsZoneUpdate; + + std::array, MAX_CUF_PROFILES> _CUFProfiles; + + SpellInfo const* m_lastSoulburnSpell; + + private: + // internal common parts for CanStore/StoreItem functions + InventoryResult CanStoreItem_InSpecificSlot(uint8 bag, uint8 slot, ItemPosCountVec& dest, ItemTemplate const* pProto, uint32& count, bool swap, Item* pSrcItem) const; + InventoryResult CanStoreItem_InBag(uint8 bag, ItemPosCountVec& dest, ItemTemplate const* pProto, uint32& count, bool merge, bool non_specialized, Item* pSrcItem, uint8 skip_bag, uint8 skip_slot) const; + InventoryResult CanStoreItem_InInventorySlots(uint8 slot_begin, uint8 slot_end, ItemPosCountVec& dest, ItemTemplate const* pProto, uint32& count, bool merge, Item* pSrcItem, uint8 skip_bag, uint8 skip_slot) const; + Item* _StoreItem(uint16 pos, Item* pItem, uint32 count, bool clone, bool update); + Item* _LoadItem(CharacterDatabaseTransaction& trans, uint32 zoneId, uint32 timeDiff, Field* fields); + + std::unique_ptr _cinematicMgr; + + GuidSet m_refundableItems; + void SendRefundInfo(Item* item); + void RefundItem(Item* item); + void SendItemRefundResult(Item* item, ItemExtendedCostEntry const* iece, uint8 error); + + // know currencies are not removed at any point (0 displayed) + void AddKnownCurrency(uint32 itemId); + + void AdjustQuestReqItemCount(Quest const* quest, QuestStatusData& questStatusData); + + bool IsCanDelayTeleport() const { return m_bCanDelayTeleport; } + void SetCanDelayTeleport(bool setting) { m_bCanDelayTeleport = setting; } + bool IsHasDelayedTeleport() const { return m_bHasDelayedTeleport; } + void SetDelayedTeleportFlag(bool setting) { m_bHasDelayedTeleport = setting; } + void ScheduleDelayedOperation(uint32 operation) { if (operation < DELAYED_END) m_DelayedOperations |= operation; } + + bool IsInstanceLoginGameMasterException() const; + + MapReference m_mapRef; + + uint32 m_lastFallTime; + float m_lastFallZ; + + std::array m_MirrorTimer; + uint8 m_MirrorTimerFlags; + uint8 m_MirrorTimerFlagsLast; + + // Rune type / Rune timer + std::array m_runeGraceCooldown; + std::array m_lastRuneGraceTimers; + + // Current teleport data + WorldLocation m_teleport_dest; + Optional m_teleport_instanceId; + TeleportToOptions m_teleport_options; + bool mSemaphoreTeleport_Near; + bool mSemaphoreTeleport_Far; + + uint32 m_DelayedOperations; + bool m_bCanDelayTeleport; + bool m_bHasDelayedTeleport; + + // Temporary removed pet cache + uint32 m_temporaryUnsummonedPetNumber; + uint32 m_oldpetspell; + + std::unique_ptr> m_achievementMgr; + std::unique_ptr m_reputationMgr; + + uint32 m_ChampioningFaction; + + InstanceTimeMap _instanceResetTimes; + uint32 _pendingBindId; + uint32 _pendingBindTimer; + + uint32 _activeCheats; + uint32 _maxPersonalArenaRate; + + bool _hasValidLFGLeavePoint; + + // variables to save health and mana before duel and restore them after duel + uint32 healthBeforeDuel; + uint32 manaBeforeDuel; + + WorldLocation _corpseLocation; + + std::unique_ptr _archaeology; + + std::vector> PlayerPetDataStore; + + TimeTrackerSmall m_petScalingSynchTimer; +}; + +TC_GAME_API void AddItemsSetItem(Player* player, Item* item); +TC_GAME_API void RemoveItemsSetItem(Player* player, ItemTemplate const* proto); + +#endif diff --git a/src/server/game/Entities/Player/Player.h.bak-vianomod-v1-20261001-195036 b/src/server/game/Entities/Player/Player.h.bak-vianomod-v1-20261001-195036 new file mode 100644 index 0000000000..e1d9efa94e --- /dev/null +++ b/src/server/game/Entities/Player/Player.h.bak-vianomod-v1-20261001-195036 @@ -0,0 +1,2800 @@ +/* + * 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 . + */ + +#ifndef _PLAYER_H +#define _PLAYER_H + +#include "Unit.h" +#include "CUFProfile.h" +#include "DatabaseEnvFwd.h" +#include "DBCEnums.h" +#include "EnumFlag.h" +#include "EquipmentSet.h" +#include "GroupReference.h" +#include "ItemDefines.h" +#include "ItemEnchantmentMgr.h" +#include "MapReference.h" +#include "PetDefines.h" +#include "PlayerTaxi.h" +#include "QuestDef.h" + +struct AccessRequirement; +struct AchievementEntry; +struct AreaTableEntry; +struct AreaTriggerEntry; +struct ArchData; +struct BarberShopStyleEntry; +struct CharacterCustomizeInfo; +struct CharTitlesEntry; +struct ChatChannelsEntry; +struct CreatureTemplate; +struct CurrencyTypesEntry; +struct FactionEntry; +struct ItemExtendedCostEntry; +struct ItemSetEffect; +struct ItemTemplate; +struct Loot; +struct Mail; +struct PlayerTalent; +struct PlayerTalentInfo; +struct ScalingStatDistributionEntry; +struct ScalingStatValuesEntry; +struct SkillRaceClassInfoEntry; +struct TrainerSpell; +struct VendorItem; +struct GuildMemberProfessionData; + +template +class AchievementMgr; + +class Archaeology; +class Bag; +class Battleground; +class CinematicMgr; +class Channel; +class CharacterCreateInfo; +class Creature; +class DynamicObject; +class GameClient; +class Group; +class Guild; +class Item; +class LootStore; +class OutdoorPvP; +class Pet; +class PetAura; +class PlayerAI; +class PlayerMenu; +class PlayerSocial; +class ReputationMgr; +class SpellCastTargets; +class TradeData; + +enum CharacterFlags : int32; +enum CharacterFlags2 : int32; +enum InventoryType : uint8; +enum ItemClass : uint8; +enum LootError : uint8; +enum LootType : uint8; +enum UpdateCollisionHeightReason : uint8; +enum ZLiquidStatus : uint32; + +typedef std::deque PlayerMails; + +constexpr uint8 PLAYER_MAX_SKILLS = 128; +constexpr uint8 PLAYER_MAX_DAILY_QUESTS = 25; +constexpr uint8 PLAYER_EXPLORED_ZONES_SIZE = 156; + +enum SpellModType : uint8 +{ + SPELLMOD_FLAT = 0, // SPELL_AURA_ADD_FLAT_MODIFIER + SPELLMOD_PCT = 1, // SPELL_AURA_ADD_PCT_MODIFIER + SPELLMOD_END +}; + +// 2^n values, Player::m_isunderwater is a bitmask. These are Trinity internal values, they are never send to any client +enum PlayerUnderwaterState +{ + UNDERWATER_NONE = 0x00, + UNDERWATER_INWATER = 0x01, // terrain type is water and player is afflicted by it + UNDERWATER_INLAVA = 0x02, // terrain type is lava and player is afflicted by it + UNDERWATER_INSLIME = 0x04, // terrain type is lava and player is afflicted by it + UNDERWATER_INDARKWATER = 0x08, // terrain type is dark water and player is afflicted by it + + UNDERWATER_EXIST_TIMERS = 0x10 +}; + +enum BuyBankSlotResult +{ + ERR_BANKSLOT_FAILED_TOO_MANY = 0, + ERR_BANKSLOT_INSUFFICIENT_FUNDS = 1, + ERR_BANKSLOT_NOTBANKER = 2, + ERR_BANKSLOT_OK = 3 +}; + +enum PlayerSpellState : uint8 +{ + PLAYERSPELL_UNCHANGED = 0, + PLAYERSPELL_CHANGED = 1, + PLAYERSPELL_NEW = 2, + PLAYERSPELL_REMOVED = 3, + PLAYERSPELL_TEMPORARY = 4 +}; + +struct PlayerSpell +{ + PlayerSpellState state : 8; + bool active : 1; // show in spellbook + bool dependent : 1; // learned as result another spell learn, skill grow, quest reward, etc + bool disabled : 1; // first rank has been learned in result talent learn but currently talent unlearned, save max learned ranks +}; + +struct PlayerTalent +{ + PlayerSpellState State; + uint8 Spec; +}; + +extern std::array const MasterySpells; + +enum TalentTree // talent tabs +{ + TALENT_TREE_WARRIOR_ARMS = 746, + TALENT_TREE_WARRIOR_FURY = 815, + TALENT_TREE_WARRIOR_PROTECTION = 845, + TALENT_TREE_PALADIN_HOLY = 831, + TALENT_TREE_PALADIN_PROTECTION = 839, + TALENT_TREE_PALADIN_RETRIBUTION = 855, + TALENT_TREE_HUNTER_BEAST_MASTERY = 811, + TALENT_TREE_HUNTER_MARKSMANSHIP = 807, + TALENT_TREE_HUNTER_SURVIVAL = 809, + TALENT_TREE_ROGUE_ASSASSINATION = 182, + TALENT_TREE_ROGUE_COMBAT = 181, + TALENT_TREE_ROGUE_SUBTLETY = 183, + TALENT_TREE_PRIEST_DISCIPLINE = 760, + TALENT_TREE_PRIEST_HOLY = 813, + TALENT_TREE_PRIEST_SHADOW = 795, + TALENT_TREE_DEATH_KNIGHT_BLOOD = 398, + TALENT_TREE_DEATH_KNIGHT_FROST = 399, + TALENT_TREE_DEATH_KNIGHT_UNHOLY = 400, + TALENT_TREE_SHAMAN_ELEMENTAL = 261, + TALENT_TREE_SHAMAN_ENHANCEMENT = 263, + TALENT_TREE_SHAMAN_RESTORATION = 262, + TALENT_TREE_MAGE_ARCANE = 799, + TALENT_TREE_MAGE_FIRE = 851, + TALENT_TREE_MAGE_FROST = 823, + TALENT_TREE_WARLOCK_AFFLICTION = 871, + TALENT_TREE_WARLOCK_DEMONOLOGY = 867, + TALENT_TREE_WARLOCK_DESTRUCTION = 865, + TALENT_TREE_DRUID_BALANCE = 752, + TALENT_TREE_DRUID_FERAL_COMBAT = 750, + TALENT_TREE_DRUID_RESTORATION = 748 +}; + +constexpr uint8 MAX_ARMOR_SPECIALIZATION_IDS = 7; +constexpr std::array ArmorSpecializationIds= { 86530, 86531, 86529, 86528, 86525, 86524, 86526 }; + +// Spell modifier (used for modify other spells) +struct SpellModifier +{ + SpellModifier(Aura* _ownerAura) : op(SpellModOp::HealingAndDamage), type(SPELLMOD_FLAT), spellId(0), ownerAura(_ownerAura) { } + virtual ~SpellModifier() = default; + + SpellModOp op; + SpellModType type; + + uint32 spellId; + Aura* const ownerAura; +}; + +struct SpellModifierByClassMask : SpellModifier +{ + SpellModifierByClassMask(Aura* _ownerAura) : SpellModifier(_ownerAura), value(0), mask() { } + + int32 value; + flag96 mask; +}; + +struct SpellModifierCompare +{ + bool operator()(SpellModifier const* left, SpellModifier const* right) const + { + // first sort by SpellModOp + if (left->op != right->op) + return left->op < right->op; + + // then by type (flat/pct) + if (left->type != right->type) + return left->type < right->type; + + return left < right; + } +}; + + +enum PlayerCurrencyState +{ + PLAYERCURRENCY_UNCHANGED = 0, + PLAYERCURRENCY_CHANGED = 1, + PLAYERCURRENCY_NEW = 2, + PLAYERCURRENCY_REMOVED = 3 //not removed just set count == 0 +}; + +struct PlayerCurrency +{ + PlayerCurrency() : State(PLAYERCURRENCY_NEW), Quantity(0), WeeklyQuantity(0), TrackedQuantity(0), Flags(0) { } + + PlayerCurrencyState State; + uint32 Quantity; + uint32 WeeklyQuantity; + uint32 TrackedQuantity; + uint8 Flags; +}; + +typedef std::unordered_map PlayerTalentMap; +typedef std::unordered_map PlayerSpellMap; +typedef Trinity::Containers::FlatSet SpellModContainer; +typedef std::unordered_map PlayerCurrenciesMap; + +typedef std::unordered_map InstanceTimeMap; + +enum ActionButtonUpdateState +{ + ACTIONBUTTON_UNCHANGED = 0, + ACTIONBUTTON_CHANGED = 1, + ACTIONBUTTON_NEW = 2, + ACTIONBUTTON_DELETED = 3 +}; + +enum ActionButtonType +{ + ACTION_BUTTON_SPELL = 0x00, + ACTION_BUTTON_C = 0x01, // click? + ACTION_BUTTON_EQSET = 0x20, + ACTION_BUTTON_DROPDOWN = 0x30, + ACTION_BUTTON_MACRO = 0x40, + ACTION_BUTTON_CMACRO = ACTION_BUTTON_C | ACTION_BUTTON_MACRO, + ACTION_BUTTON_ITEM = 0x80 +}; + +enum ReputationSource +{ + REPUTATION_SOURCE_KILL, + REPUTATION_SOURCE_QUEST, + REPUTATION_SOURCE_DAILY_QUEST, + REPUTATION_SOURCE_WEEKLY_QUEST, + REPUTATION_SOURCE_MONTHLY_QUEST, + REPUTATION_SOURCE_REPEATABLE_QUEST, + REPUTATION_SOURCE_SPELL +}; + +constexpr uint32 ACTION_BUTTON_ACTION(uint32 x) { return x & 0x00FFFFFF; } +constexpr uint32 ACTION_BUTTON_TYPE(uint32 x) { return (x & 0xFF000000) >> 24; } +constexpr uint32 MAX_ACTION_BUTTON_ACTION_VALUE = 0x00FFFFFF + 1; + +struct ActionButton +{ + ActionButton() : packedData(0), uState(ACTIONBUTTON_NEW) { } + + uint32 packedData; + ActionButtonUpdateState uState; + + // helpers + ActionButtonType GetType() const { return ActionButtonType(ACTION_BUTTON_TYPE(packedData)); } + uint32 GetAction() const { return ACTION_BUTTON_ACTION(packedData); } + void SetActionAndType(uint32 action, ActionButtonType type) + { + uint32 newData = action | (uint32(type) << 24); + if (newData != packedData || uState == ACTIONBUTTON_DELETED) + { + packedData = newData; + if (uState != ACTIONBUTTON_NEW) + uState = ACTIONBUTTON_CHANGED; + } + } +}; + +constexpr uint8 MAX_ACTION_BUTTONS = 144; //checked in 3.2.0 + +typedef std::map ActionButtonList; + +struct PvPInfo +{ + PvPInfo() : IsHostile(false), IsInHostileArea(false), IsInNoPvPArea(false), IsInFFAPvPArea(false), EndTimer(0) { } + + bool IsHostile; + bool IsInHostileArea; ///> Marks if player is in an area which forces PvP flag + bool IsInNoPvPArea; ///> Marks if player is in a sanctuary or friendly capital city + bool IsInFFAPvPArea; ///> Marks if player is in an FFAPvP area (such as Gurubashi Arena) + time_t EndTimer; ///> Time when player unflags himself for PvP (flag removed after 5 minutes) +}; + +struct DuelInfo +{ + DuelInfo() : initiator(nullptr), opponent(nullptr), startTimer(0), startTime(0), outOfBound(0), isMounted(false), isCompleted(false) { } + + Player* initiator; + Player* opponent; + time_t startTimer; + time_t startTime; + time_t outOfBound; + bool isMounted; + bool isCompleted; +}; + +struct Areas +{ + uint32 areaID; + uint32 areaFlag; + float x1; + float x2; + float y1; + float y2; +}; + +constexpr uint8 MAX_RUNES = 6; +constexpr float RUNE_BASE_COOLDOWN = 1.0f; + +enum class RuneType : uint8 +{ + + Blood = 0, + Unholy = 1, + Frost = 2, + Death = 3, + Max = 4 +}; + +struct RuneInfo +{ + RuneType BaseRune; + RuneType CurrentRune; + float Cooldown; + AuraEffect const* ConvertAura; + AuraType ConvertAuraType; + SpellInfo const* ConvertAuraInfo; +}; + +struct Runes +{ + RuneInfo _Runes[MAX_RUNES]; + uint8 RuneState; // mask of available runes + RuneType LastUsedRune; + uint8 LastUsedRuneMask; + + void SetRuneState(uint8 index, bool set = true); +}; + +struct EnchantDuration +{ + EnchantDuration() : item(nullptr), slot(MAX_ENCHANTMENT_SLOT), leftduration(0) { } + EnchantDuration(Item* _item, EnchantmentSlot _slot, uint32 _leftduration) : item(_item), slot(_slot), + leftduration(_leftduration){ ASSERT(item); } + + Item* item; + EnchantmentSlot slot; + uint32 leftduration; +}; + +typedef std::list EnchantDurationList; +typedef std::list ItemDurationList; + +enum DrunkenState +{ + DRUNKEN_SOBER = 0, + DRUNKEN_TIPSY = 1, + DRUNKEN_DRUNK = 2, + DRUNKEN_SMASHED = 3 +}; + +constexpr uint8 MAX_DRUNKEN = 4; + +enum PlayerFlags +{ + PLAYER_FLAGS_NONE = 0x00000000, + PLAYER_FLAGS_GROUP_LEADER = 0x00000001, + PLAYER_FLAGS_AFK = 0x00000002, + PLAYER_FLAGS_DND = 0x00000004, + PLAYER_FLAGS_GM = 0x00000008, + PLAYER_FLAGS_GHOST = 0x00000010, + PLAYER_FLAGS_RESTING = 0x00000020, + PLAYER_FLAGS_VOICE_CHAT = 0x00000040, + PLAYER_FLAGS_UNK7 = 0x00000080, // pre-3.0.3 PLAYER_FLAGS_FFA_PVP flag for FFA PVP state + PLAYER_FLAGS_CONTESTED_PVP = 0x00000100, // Player has been involved in a PvP combat and will be attacked by contested guards + PLAYER_FLAGS_IN_PVP = 0x00000200, + PLAYER_FLAGS_HIDE_HELM = 0x00000400, + PLAYER_FLAGS_HIDE_CLOAK = 0x00000800, + PLAYER_FLAGS_PLAYED_LONG_TIME = 0x00001000, // played long time + PLAYER_FLAGS_PLAYED_TOO_LONG = 0x00002000, // played too long time + PLAYER_FLAGS_IS_OUT_OF_BOUNDS = 0x00004000, + PLAYER_FLAGS_DEVELOPER = 0x00008000, // prefix for something? + PLAYER_FLAGS_LOW_LEVEL_RAID_ENABLED = 0x00010000, // pre-3.0.3 PLAYER_FLAGS_SANCTUARY flag for player entered sanctuary + PLAYER_FLAGS_TAXI_BENCHMARK = 0x00020000, // taxi benchmark mode (on/off) (2.0.1) + PLAYER_FLAGS_PVP_TIMER = 0x00040000, // 3.0.2, pvp timer active (after you disable pvp manually) + PLAYER_FLAGS_UBER = 0x00080000, + PLAYER_FLAGS_UNK20 = 0x00100000, + PLAYER_FLAGS_UNK21 = 0x00200000, + PLAYER_FLAGS_COMMENTATOR2 = 0x00400000, + PLAYER_FLAGS_DISABLE_CASTING_EXCEPT_ABILITIES = 0x00800000, // disables casting any ability except for the ones specified in the aura effect's SpellClassMask + PLAYER_FLAGS_DISABLE_ATTACKING_EXCEPT_ABILITIES = 0x01000000, // disables all attacks including ability casts except for the ones specified in the aura effect's SpellClassMask + PLAYER_FLAGS_NO_XP_GAIN = 0x02000000, + PLAYER_FLAGS_UNK26 = 0x04000000, + PLAYER_FLAGS_AUTO_DECLINE_GUILD = 0x08000000, // Automatically declines guild invites + PLAYER_FLAGS_GUILD_LEVEL_ENABLED = 0x10000000, // Lua_GetGuildLevelEnabled() - enables guild leveling related UI + PLAYER_FLAGS_VOID_UNLOCKED = 0x20000000, // void storage + PLAYER_FLAGS_TIMEWALKING = 0x40000000, + PLAYER_FLAGS_COMMENTATOR_CAMERA = 0x80000000 +}; + +enum PlayerBytesOffsets +{ + PLAYER_BYTES_OFFSET_SKIN_ID = 0, + PLAYER_BYTES_OFFSET_FACE_ID = 1, + PLAYER_BYTES_OFFSET_HAIR_STYLE_ID = 2, + PLAYER_BYTES_OFFSET_HAIR_COLOR_ID = 3 +}; + +enum PlayerBytes2Offsets +{ + PLAYER_BYTES_2_OFFSET_FACIAL_STYLE = 0, + PLAYER_BYTES_2_OFFSET_PARTY_TYPE = 1, + PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS = 2, + PLAYER_BYTES_2_OFFSET_REST_STATE = 3 +}; + +enum PlayerBytes3Offsets +{ + PLAYER_BYTES_3_OFFSET_GENDER = 0, + PLAYER_BYTES_3_OFFSET_INEBRIATION = 1, + PLAYER_BYTES_3_OFFSET_PVP_TITLE = 2, + PLAYER_BYTES_3_OFFSET_ARENA_FACTION = 3 +}; + +enum PlayerFieldBytesOffsets +{ + PLAYER_FIELD_BYTES_OFFSET_FLAGS = 0, + PLAYER_FIELD_BYTES_OFFSET_RAF_GRANTABLE_LEVEL = 1, + PLAYER_FIELD_BYTES_OFFSET_ACTION_BAR_TOGGLES = 2, + PLAYER_FIELD_BYTES_OFFSET_LIFETIME_MAX_PVP_RANK = 3 +}; + +enum PlayerFieldBytes2Offsets +{ + PLAYER_FIELD_BYTES_2_OFFSET_OVERRIDE_SPELLS_ID = 0, // uint16! + PLAYER_FIELD_BYTES_2_OFFSET_IGNORE_POWER_REGEN_PREDICTION_MASK = 2, + PLAYER_FIELD_BYTES_2_OFFSET_AURA_VISION = 3 +}; + +static_assert((PLAYER_FIELD_BYTES_2_OFFSET_OVERRIDE_SPELLS_ID & 1) == 0, "PLAYER_FIELD_BYTES_2_OFFSET_OVERRIDE_SPELLS_ID must be aligned to 2 byte boundary"); + +constexpr uint8 PLAYER_BYTES_2_OVERRIDE_SPELLS_UINT16_OFFSET = PLAYER_FIELD_BYTES_2_OFFSET_OVERRIDE_SPELLS_ID / 2; + +// KNOWN_TITLES is stored in 4 uint64 values, so we can represent 256 title bits (4 * 64). +constexpr uint8 KNOWN_TITLES_SIZE = 4; +constexpr uint32 MAX_TITLE_INDEX = KNOWN_TITLES_SIZE * 64; + +// used in PLAYER_FIELD_BYTES values +enum PlayerFieldByteFlags +{ + PLAYER_FIELD_BYTE_TRACK_STEALTHED = 0x00000002, + PLAYER_FIELD_BYTE_RELEASE_TIMER = 0x00000008, // Display time till auto release spirit + PLAYER_FIELD_BYTE_NO_RELEASE_WINDOW = 0x00000010 // Display no "release spirit" window at all +}; + +// used in PLAYER_FIELD_BYTES2 values +enum PlayerFieldByte2Flags +{ + PLAYER_FIELD_BYTE2_NONE = 0x00, + PLAYER_FIELD_BYTE2_STEALTH = 0x20, + PLAYER_FIELD_BYTE2_INVISIBILITY_GLOW = 0x40 +}; + +enum MirrorTimerType +{ + FATIGUE_TIMER = 0, + BREATH_TIMER = 1, + FIRE_TIMER = 2 // feign death +}; + +constexpr uint8 MAX_TIMERS = 3; +constexpr int32 DISABLED_MIRROR_TIMER = -1; + +// 2^n values +enum PlayerExtraFlags +{ + // gm abilities + PLAYER_EXTRA_GM_ON = 0x0001, + PLAYER_EXTRA_ACCEPT_WHISPERS = 0x0004, + PLAYER_EXTRA_TAXICHEAT = 0x0008, + PLAYER_EXTRA_GM_INVISIBLE = 0x0010, + PLAYER_EXTRA_GM_CHAT = 0x0020, // Show GM badge in chat messages + + // other states + PLAYER_EXTRA_PVP_DEATH = 0x0100 // store PvP death status until corpse creating. +}; + +// 2^n values +enum AtLoginFlags +{ + AT_LOGIN_NONE = 0x000, + AT_LOGIN_RENAME = 0x001, + AT_LOGIN_RESET_SPELLS = 0x002, + AT_LOGIN_RESET_TALENTS = 0x004, + AT_LOGIN_CUSTOMIZE = 0x008, + AT_LOGIN_RESET_PET_TALENTS = 0x010, + AT_LOGIN_FIRST = 0x020, + AT_LOGIN_CHANGE_FACTION = 0x040, + AT_LOGIN_CHANGE_RACE = 0x080, + AT_LOGIN_RESURRECT = 0x100 +}; + +typedef std::map QuestStatusMap; +typedef std::set RewardedQuestSet; + +enum QuestSaveType +{ + QUEST_DEFAULT_SAVE_TYPE = 0, + QUEST_DELETE_SAVE_TYPE, + QUEST_FORCE_DELETE_SAVE_TYPE +}; + +// quest +typedef std::map QuestStatusSaveMap; + + +enum QuestSlotOffsets +{ + QUEST_ID_OFFSET = 0, + QUEST_STATE_OFFSET = 1, + QUEST_COUNTS_OFFSET = 2, + QUEST_TIME_OFFSET = 4 +}; + +constexpr uint8 MAX_QUEST_OFFSET = 5; + +enum QuestSlotStateMask +{ + QUEST_STATE_NONE = 0x0000, + QUEST_STATE_COMPLETE = 0x0001, + QUEST_STATE_FAIL = 0x0002 +}; + +enum SkillUpdateState +{ + SKILL_UNCHANGED = 0, + SKILL_CHANGED = 1, + SKILL_NEW = 2, + SKILL_DELETED = 3 +}; + +struct SkillStatusData +{ + SkillStatusData(uint8 _pos, SkillUpdateState _uState) : pos(_pos), uState(_uState) + { + } + uint8 pos; + SkillUpdateState uState; +}; + +typedef std::unordered_map SkillStatusMap; + +class Quest; +class Spell; +class Item; +class WorldSession; + +enum PlayerSlots +{ + // first slot for item stored (in any way in player m_items data) + PLAYER_SLOT_START = 0, + // last+1 slot for item stored (in any way in player m_items data) + PLAYER_SLOT_END = 86, + PLAYER_SLOTS_COUNT = (PLAYER_SLOT_END - PLAYER_SLOT_START) +}; + +constexpr uint8 INVENTORY_SLOT_BAG_0 = 255; + +enum EquipmentSlots : uint8 // 19 slots +{ + EQUIPMENT_SLOT_START = 0, + EQUIPMENT_SLOT_HEAD = 0, + EQUIPMENT_SLOT_NECK = 1, + EQUIPMENT_SLOT_SHOULDERS = 2, + EQUIPMENT_SLOT_BODY = 3, + EQUIPMENT_SLOT_CHEST = 4, + EQUIPMENT_SLOT_WAIST = 5, + EQUIPMENT_SLOT_LEGS = 6, + EQUIPMENT_SLOT_FEET = 7, + EQUIPMENT_SLOT_WRISTS = 8, + EQUIPMENT_SLOT_HANDS = 9, + EQUIPMENT_SLOT_FINGER1 = 10, + EQUIPMENT_SLOT_FINGER2 = 11, + EQUIPMENT_SLOT_TRINKET1 = 12, + EQUIPMENT_SLOT_TRINKET2 = 13, + EQUIPMENT_SLOT_BACK = 14, + EQUIPMENT_SLOT_MAINHAND = 15, + EQUIPMENT_SLOT_OFFHAND = 16, + EQUIPMENT_SLOT_RANGED = 17, + EQUIPMENT_SLOT_TABARD = 18, + EQUIPMENT_SLOT_END = 19 +}; + +enum InventorySlots : uint8 // 4 slots +{ + INVENTORY_SLOT_BAG_START = 19, + INVENTORY_SLOT_BAG_END = 23 +}; + +enum InventoryPackSlots : uint8 // 16 slots +{ + INVENTORY_SLOT_ITEM_START = 23, + INVENTORY_SLOT_ITEM_END = 39 +}; + +enum BankItemSlots // 28 slots +{ + BANK_SLOT_ITEM_START = 39, + BANK_SLOT_ITEM_END = 67 +}; + +enum BankBagSlots // 7 slots +{ + BANK_SLOT_BAG_START = 67, + BANK_SLOT_BAG_END = 74 +}; + +enum BuyBackSlots // 12 slots +{ + // stored in m_buybackitems + BUYBACK_SLOT_START = 74, + BUYBACK_SLOT_END = 86 +}; + +struct ItemPosCount +{ + ItemPosCount(uint16 _pos, uint32 _count) : pos(_pos), count(_count) { } + bool isContainedIn(std::vector const& vec) const; + uint16 pos; + uint32 count; +}; +typedef std::vector ItemPosCountVec; + +constexpr uint32 JUSTICE_POINTS_CONVERSION_MONEY = 4750; + +enum TransferAbortReason +{ + TRANSFER_ABORT_NONE = 0x00, + TRANSFER_ABORT_ERROR = 0x01, + TRANSFER_ABORT_MAX_PLAYERS = 0x02, // Transfer Aborted: instance is full + TRANSFER_ABORT_NOT_FOUND = 0x03, // Transfer Aborted: instance not found + TRANSFER_ABORT_TOO_MANY_INSTANCES = 0x04, // You have entered too many instances recently. + TRANSFER_ABORT_ZONE_IN_COMBAT = 0x06, // Unable to zone in while an encounter is in progress. + TRANSFER_ABORT_INSUF_EXPAN_LVL = 0x07, // You must have expansion installed to access this area. + TRANSFER_ABORT_DIFFICULTY = 0x08, // difficulty mode is not available for %s. + TRANSFER_ABORT_UNIQUE_MESSAGE = 0x09, // Until you've escaped TLK's grasp, you cannot leave this place! + TRANSFER_ABORT_TOO_MANY_REALM_INSTANCES = 0x0A, // Additional instances cannot be launched, please try again later. + TRANSFER_ABORT_NEED_GROUP = 0x0B, // 3.1 + TRANSFER_ABORT_NOT_FOUND1 = 0x0C, // 3.1 + TRANSFER_ABORT_NOT_FOUND2 = 0x0D, // 3.1 + TRANSFER_ABORT_NOT_FOUND3 = 0x0E, // 3.2 + TRANSFER_ABORT_REALM_ONLY = 0x0F, // All players on party must be from the same realm. + TRANSFER_ABORT_MAP_NOT_ALLOWED = 0x10, // Map can't be entered at this time. + TRANSFER_ABORT_LOCKED_TO_DIFFERENT_INSTANCE = 0x12, // 4.2.2 + TRANSFER_ABORT_ALREADY_COMPLETED_ENCOUNTER = 0x13 // 4.2.2 +}; + +enum class TransferAbortedUniqueMessageArgs : uint8 +{ + None = 0, + StillWithinTheLichKingsGrasp = 1, + DestinyOfTheBilgewaterCartelStillUndecided = 2, + FateOfGilneasHasNotBeenDecidedYet = 3 +}; + +enum InstanceResetWarningType +{ + RAID_INSTANCE_WARNING_HOURS = 1, // WARNING! %s is scheduled to reset in %d hour(s). + RAID_INSTANCE_WARNING_MIN = 2, // WARNING! %s is scheduled to reset in %d minute(s)! + RAID_INSTANCE_WARNING_MIN_SOON = 3, // WARNING! %s is scheduled to reset in %d minute(s). Please exit the zone or you will be returned to your bind location! + RAID_INSTANCE_WELCOME = 4, // Welcome to %s. This raid instance is scheduled to reset in %s. + RAID_INSTANCE_EXPIRED = 5 +}; + +// PLAYER_FIELD_ARENA_TEAM_INFO_1_1 offsets +enum ArenaTeamInfoType +{ + ARENA_TEAM_ID = 0, + ARENA_TEAM_TYPE = 1, // new in 3.2 - team type? + ARENA_TEAM_MEMBER = 2, // 0 - captain, 1 - member + ARENA_TEAM_GAMES_WEEK = 3, + ARENA_TEAM_GAMES_SEASON = 4, + ARENA_TEAM_WINS_SEASON = 5, + ARENA_TEAM_PERSONAL_RATING = 6, + ARENA_TEAM_END = 7 +}; + +class InstanceSave; + +enum RestFlag +{ + REST_FLAG_IN_TAVERN = 0x1, + REST_FLAG_IN_CITY = 0x2, + REST_FLAG_IN_FACTION_AREA = 0x4 +}; + +enum TeleportToOptions +{ + TELE_TO_NONE = 0x00, + TELE_TO_GM_MODE = 0x01, + TELE_TO_NOT_LEAVE_TRANSPORT = 0x02, + TELE_TO_NOT_LEAVE_COMBAT = 0x04, + TELE_TO_NOT_UNSUMMON_PET = 0x08, + TELE_TO_SPELL = 0x10 +}; + +DEFINE_ENUM_FLAG(TeleportToOptions); + +/// Type of environmental damages +enum EnviromentalDamage +{ + DAMAGE_EXHAUSTED = 0, + DAMAGE_DROWNING = 1, + DAMAGE_FALL = 2, + DAMAGE_LAVA = 3, + DAMAGE_SLIME = 4, + DAMAGE_FIRE = 5, + DAMAGE_FALL_TO_VOID = 6 // custom case for fall without durability loss +}; + +enum PlayerChatTag +{ + CHAT_TAG_NONE = 0x00, + CHAT_TAG_AFK = 0x01, + CHAT_TAG_DND = 0x02, + CHAT_TAG_GM = 0x04, + CHAT_TAG_COM = 0x08, // Commentator + CHAT_TAG_DEV = 0x10, + CHAT_TAG_BOSS_SOUND = 0x20, // Plays "RaidBossEmoteWarning" sound on raid boss emote/whisper + CHAT_TAG_MOBILE = 0x40 +}; + +enum PlayedTimeIndex +{ + PLAYED_TIME_TOTAL = 0, + PLAYED_TIME_LEVEL = 1 +}; + +constexpr uint8 MAX_PLAYED_TIME_INDEX = 2; + +// used at player loading query list preparing, and later result selection +enum PlayerLoginQueryIndex +{ + PLAYER_LOGIN_QUERY_LOAD_FROM = 0, + PLAYER_LOGIN_QUERY_LOAD_GROUP = 1, + PLAYER_LOGIN_QUERY_LOAD_BOUND_INSTANCES = 2, + PLAYER_LOGIN_QUERY_LOAD_AURAS = 3, + PLAYER_LOGIN_QUERY_LOAD_SPELLS = 4, + PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS = 5, + PLAYER_LOGIN_QUERY_LOAD_DAILY_QUEST_STATUS = 6, + PLAYER_LOGIN_QUERY_LOAD_REPUTATION = 7, + PLAYER_LOGIN_QUERY_LOAD_INVENTORY = 8, + PLAYER_LOGIN_QUERY_LOAD_ACTIONS = 9, + PLAYER_LOGIN_QUERY_LOAD_MAILS = 10, + PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS = 11, + PLAYER_LOGIN_QUERY_LOAD_SOCIAL_LIST = 12, + PLAYER_LOGIN_QUERY_LOAD_HOME_BIND = 13, + PLAYER_LOGIN_QUERY_LOAD_SPELL_COOLDOWNS = 14, + PLAYER_LOGIN_QUERY_LOAD_DECLINED_NAMES = 15, + PLAYER_LOGIN_QUERY_LOAD_GUILD = 16, + PLAYER_LOGIN_QUERY_LOAD_ARENA_INFO = 17, + PLAYER_LOGIN_QUERY_LOAD_ACHIEVEMENTS = 18, + PLAYER_LOGIN_QUERY_LOAD_CRITERIA_PROGRESS = 19, + PLAYER_LOGIN_QUERY_LOAD_EQUIPMENT_SETS = 20, + PLAYER_LOGIN_QUERY_LOAD_BG_DATA = 21, + PLAYER_LOGIN_QUERY_LOAD_GLYPHS = 22, + PLAYER_LOGIN_QUERY_LOAD_TALENTS = 23, + PLAYER_LOGIN_QUERY_LOAD_ACCOUNT_DATA = 24, + PLAYER_LOGIN_QUERY_LOAD_SKILLS = 25, + PLAYER_LOGIN_QUERY_LOAD_WEEKLY_QUEST_STATUS = 26, + PLAYER_LOGIN_QUERY_LOAD_RANDOM_BG = 27, + PLAYER_LOGIN_QUERY_LOAD_BANNED = 28, + PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS_REW = 29, + PLAYER_LOGIN_QUERY_LOAD_INSTANCE_LOCK_TIMES = 30, + PLAYER_LOGIN_QUERY_LOAD_SEASONAL_QUEST_STATUS = 31, + PLAYER_LOGIN_QUERY_LOAD_MONTHLY_QUEST_STATUS = 32, + PLAYER_LOGIN_QUERY_LOAD_VOID_STORAGE = 33, + PLAYER_LOGIN_QUERY_LOAD_CURRENCY = 34, + PLAYER_LOGIN_QUERY_LOAD_CUF_PROFILES = 35, + PLAYER_LOGIN_QUERY_LOAD_CORPSE_LOCATION = 36, + PLAYER_LOGIN_QUERY_LOAD_ALL_PETS = 37, + PLAYER_LOGIN_QUERY_LOAD_LFG_REWARD_STATUS = 38, + MAX_PLAYER_LOGIN_QUERY +}; + +enum PlayerDelayedOperations +{ + DELAYED_SAVE_PLAYER = 0x01, + DELAYED_RESURRECT_PLAYER = 0x02, + DELAYED_SPELL_CAST_DESERTER = 0x04, + DELAYED_BG_MOUNT_RESTORE = 0x08, ///< Flag to restore mount state after teleport from BG + DELAYED_BG_TAXI_RESTORE = 0x10, ///< Flag to restore taxi state after teleport from BG + DELAYED_BG_GROUP_RESTORE = 0x20, ///< Flag to restore group state after teleport from BG + DELAYED_END +}; + +// Player summoning auto-decline time (in secs) +constexpr uint32 MAX_PLAYER_SUMMON_DELAY= 2 * MINUTE; +// Maximum money amount : 2^31 - 1 +TC_GAME_API extern uint64 const MAX_MONEY_AMOUNT; + +enum BindExtensionState +{ + EXTEND_STATE_EXPIRED = 0, + EXTEND_STATE_NORMAL = 1, + EXTEND_STATE_EXTENDED = 2, + EXTEND_STATE_KEEP = 255 // special state: keep current save type +}; +struct InstancePlayerBind +{ + InstanceSave* save; + /* permanent PlayerInstanceBinds are created in Raid/Heroic instances for players + that aren't already permanently bound when they are inside when a boss is killed + or when they enter an instance that the group leader is permanently bound to. */ + bool perm; + /* extend state listing: + EXPIRED - doesn't affect anything unless manually re-extended by player + NORMAL - standard state + EXTENDED - won't be promoted to EXPIRED at next reset period, will instead be promoted to NORMAL */ + BindExtensionState extendState; + + InstancePlayerBind() : save(nullptr), perm(false), extendState(EXTEND_STATE_NORMAL) { } +}; + +enum CharDeleteMethod +{ + CHAR_DELETE_REMOVE = 0, // Completely remove from the database + CHAR_DELETE_UNLINK = 1 // The character gets unlinked from the account, + // the name gets freed up and appears as deleted ingame +}; + +enum ReferAFriendError +{ + ERR_REFER_A_FRIEND_NONE = 0x00, + ERR_REFER_A_FRIEND_NOT_REFERRED_BY = 0x01, + ERR_REFER_A_FRIEND_TARGET_TOO_HIGH = 0x02, + ERR_REFER_A_FRIEND_INSUFFICIENT_GRANTABLE_LEVELS = 0x03, + ERR_REFER_A_FRIEND_TOO_FAR = 0x04, + ERR_REFER_A_FRIEND_DIFFERENT_FACTION = 0x05, + ERR_REFER_A_FRIEND_NOT_NOW = 0x06, + ERR_REFER_A_FRIEND_GRANT_LEVEL_MAX_I = 0x07, + ERR_REFER_A_FRIEND_NO_TARGET = 0x08, + ERR_REFER_A_FRIEND_NOT_IN_GROUP = 0x09, + ERR_REFER_A_FRIEND_SUMMON_LEVEL_MAX_I = 0x0A, + ERR_REFER_A_FRIEND_SUMMON_COOLDOWN = 0x0B, + ERR_REFER_A_FRIEND_INSUF_EXPAN_LVL = 0x0C, + ERR_REFER_A_FRIEND_SUMMON_OFFLINE_S = 0x0D +}; + +enum PlayerRestState : uint8 +{ + REST_STATE_RESTED = 0x01, + REST_STATE_NOT_RAF_LINKED = 0x02, + REST_STATE_RAF_LINKED = 0x06 +}; + +enum PlayerCommandStates +{ + CHEAT_NONE = 0x00, + CHEAT_GOD = 0x01, + CHEAT_CASTTIME = 0x02, + CHEAT_COOLDOWN = 0x04, + CHEAT_POWER = 0x08, + CHEAT_WATERWALK = 0x10 +}; + +enum PlayerLogXPReason : uint8 +{ + LOG_XP_REASON_KILL = 0, + LOG_XP_REASON_NO_KILL = 1 +}; + +struct PlayerPetData +{ + uint32 PetId; + uint32 CreatureId; + uint64 Owner; + uint32 DisplayId; + uint32 Petlevel; + uint32 PetExp; + ReactStates Reactstate; + uint8 Slot; + std::string Name; + bool Renamed; + bool Active; + uint32 SavedHealth; + uint32 SavedMana; + std::string Actionbar; + uint32 Timediff; + uint32 SummonSpellId; + PetType Type; +}; + +class Player; + +/// Holder for Battleground data +struct BGData +{ + BGData() : bgInstanceID(0), bgTypeID(BATTLEGROUND_TYPE_NONE), bgAfkReportedCount(0), bgAfkReportedTimer(0), + bgTeam(0), mountSpell(0) { bgQueuesJoinedTime.clear(); ClearTaxiPath(); } + + uint32 bgInstanceID; ///< This variable is set to bg->m_InstanceID, + /// when player is teleported to BG - (it is battleground's GUID) + BattlegroundTypeId bgTypeID; + + std::map bgQueuesJoinedTime; + + std::set bgAfkReporter; + uint8 bgAfkReportedCount; + time_t bgAfkReportedTimer; + + uint32 bgTeam; ///< What side the player will be added to + + uint32 mountSpell; + uint32 taxiPath[2]; + + WorldLocation joinPos; ///< From where player entered BG + WorldLocation leavePos; + + void ClearTaxiPath() { taxiPath[0] = taxiPath[1] = 0; } + bool HasTaxiPath() const { return taxiPath[0] && taxiPath[1]; } +}; + +struct VoidStorageItem +{ + VoidStorageItem() : ItemId(0), ItemEntry(0), ItemRandomPropertyId(), ItemSuffixFactor(0) { } + VoidStorageItem(uint64 id, uint32 entry, ObjectGuid const& creator, ItemRandomEnchantmentId randomPropertyId, uint32 suffixFactor) + : ItemId(id), ItemEntry(entry), CreatorGuid(creator), ItemRandomPropertyId(randomPropertyId), + ItemSuffixFactor(suffixFactor) { } + + uint64 ItemId; + uint32 ItemEntry; + ObjectGuid CreatorGuid; + ItemRandomEnchantmentId ItemRandomPropertyId; + uint32 ItemSuffixFactor; +}; + +struct ResurrectionData +{ + ObjectGuid GUID; + WorldLocation Location; + uint32 Health; + uint32 Mana; + uint32 Aura; +}; + +class TC_GAME_API Player : public Unit, public GridObject +{ + friend class WorldSession; + friend class CinematicMgr; + friend void AddItemToUpdateQueueOf(Item* item, Player* player); + friend void RemoveItemFromUpdateQueueOf(Item* item, Player* player); + public: + explicit Player(WorldSession* session); + ~Player(); + + PlayerAI* AI() const { return reinterpret_cast(GetAI()); } + + void CleanupsBeforeDelete(bool finalCleanup = true) override; + + void AddToWorld() override; + void RemoveFromWorld() override; + + void SetObjectScale(float scale) override; + + bool TeleportTo(uint32 mapid, float x, float y, float z, float orientation, TeleportToOptions options = TELE_TO_NONE, Optional instanceId = {}); + bool TeleportTo(WorldLocation const& loc, TeleportToOptions options = TELE_TO_NONE, Optional instanceId = {}); + bool TeleportToBGEntryPoint(); + + bool HasSummonPending() const; + void SendSummonRequestFrom(Unit* summoner); + void SummonIfPossible(bool agree); + + bool Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo); + + void Update(uint32 time) override; + + void Heartbeat() override; + + bool IsImmunedToSpellEffect(SpellInfo const* spellInfo, uint32 index, WorldObject const* caster, bool requireImmunityPurgesEffectAttribute = false) const override; + + bool IsInAreaTriggerRadius(AreaTriggerEntry const* trigger) const; + + void SendInitialPacketsBeforeAddToMap(bool firstLogin = false); + void SendInitialPacketsAfterAddToMap(); + void SendSupercededSpell(uint32 oldSpell, uint32 newSpell) const; + void SendTransferAborted(uint32 mapid, TransferAbortReason reason, uint8 arg = 0) const; + void SendInstanceResetWarning(uint32 mapid, Difficulty difficulty, uint32 time, bool welcome) const; + + bool CanInteractWithQuestGiver(Object* questGiver) const; + Creature* GetNPCIfCanInteractWith(ObjectGuid const& guid, uint32 npcflagmask) const; + GameObject* GetGameObjectIfCanInteractWith(ObjectGuid const& guid) const; + GameObject* GetGameObjectIfCanInteractWith(ObjectGuid const& guid, GameobjectTypes type) const; + + void ToggleAFK(); + void ToggleDND(); + bool isAFK() const { return HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_AFK); } + bool isDND() const { return HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_DND); } + uint8 GetChatTag() const; + std::string autoReplyMsg; + + uint32 GetBarberShopCost(uint8 newhairstyle, uint8 newhaircolor, uint8 newfacialhair, BarberShopStyleEntry const* newSkin = nullptr) const; + + PlayerSocial* GetSocial() { return m_social; } + void RemoveSocial(); + + void SendTamePetFailure(PetTameFailureReason reason); + + PlayerTaxi m_taxi; + void InitTaxiNodesForLevel() { m_taxi.InitTaxiNodesForLevel(getRace(), getClass(), getLevel()); } + bool ActivateTaxiPathTo(std::vector const& nodes, Creature* npc = nullptr, uint32 spellid = 0); + bool ActivateTaxiPathTo(uint32 taxi_path_id, uint32 spellid = 0); + void CleanupAfterTaxiFlight(); + void ContinueTaxiFlight() const; + void SendTaxiNodeStatusMultiple(); + // mount_id can be used in scripting calls + bool isAcceptWhispers() const { return (m_ExtraFlags & PLAYER_EXTRA_ACCEPT_WHISPERS) != 0; } + void SetAcceptWhispers(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_ACCEPT_WHISPERS; else m_ExtraFlags &= ~PLAYER_EXTRA_ACCEPT_WHISPERS; } + bool IsGameMaster() const { return (m_ExtraFlags & PLAYER_EXTRA_GM_ON) != 0; } + bool CanBeGameMaster() const; + void SetGameMaster(bool on); + bool isGMChat() const { return (m_ExtraFlags & PLAYER_EXTRA_GM_CHAT) != 0; } + void SetGMChat(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_GM_CHAT; else m_ExtraFlags &= ~PLAYER_EXTRA_GM_CHAT; } + bool isTaxiCheater() const { return (m_ExtraFlags & PLAYER_EXTRA_TAXICHEAT) != 0; } + void SetTaxiCheater(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_TAXICHEAT; else m_ExtraFlags &= ~PLAYER_EXTRA_TAXICHEAT; } + bool isGMVisible() const { return !(m_ExtraFlags & PLAYER_EXTRA_GM_INVISIBLE); } + void SetGMVisible(bool on); + void SetPvPDeath(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_PVP_DEATH; else m_ExtraFlags &= ~PLAYER_EXTRA_PVP_DEATH; } + + void GiveXP(uint32 xp, Unit* victim, float group_rate=1.0f); + void GiveLevel(uint8 level); + bool IsMaxLevel() const; + + void InitStatsForLevel(bool reapplyMods = false); + + // .cheat command related + bool GetCommandStatus(uint32 command) const { return (_activeCheats & command) != 0; } + void SetCommandStatusOn(uint32 command) { _activeCheats |= command; } + void SetCommandStatusOff(uint32 command) { _activeCheats &= ~command; } + + // Played Time Stuff + time_t m_logintime; + time_t m_Last_tick; + std::array m_Played_time; + uint32 GetTotalPlayedTime() const { return m_Played_time[PLAYED_TIME_TOTAL]; } + uint32 GetLevelPlayedTime() const { return m_Played_time[PLAYED_TIME_LEVEL]; } + + void setDeathState(DeathState s) override; // overwrite Unit::setDeathState + + float GetRestBonus() const { return m_rest_bonus; } + void SetRestBonus(float rest_bonus_new); + + bool HasRestFlag(RestFlag restFlag) const { return (_restFlagMask & restFlag) != 0; } + void SetRestFlag(RestFlag restFlag); + void RemoveRestFlag(RestFlag restFlag); + + uint32 GetXPRestBonus(uint32 xp); + uint32 GetInnTriggerId() const { return inn_triggerId; } + void SetInnTriggerID(uint32 id) { inn_triggerId = id; } + + Pet* GetPet() const; + Pet* SummonPet(uint32 entry, float x, float y, float z, float ang, PetType petType, uint32 despwtime); + void RemovePet(Pet* pet, PetSaveMode mode, bool returnreagent = false); + + // pet auras + std::unordered_set m_petAuras; + void AddPetAura(PetAura const* petSpell); + void RemovePetAura(PetAura const* petSpell); + + /// Handles said message in regular chat based on declared language and in config pre-defined Range. + void Say(std::string const& text, Language language, WorldObject const* = nullptr) override; + void Say(uint32 textId, WorldObject const* target = nullptr) override; + /// Handles yelled message in regular chat based on declared language and in config pre-defined Range. + void Yell(std::string const& text, Language language, WorldObject const* = nullptr) override; + void Yell(uint32 textId, WorldObject const* target = nullptr) override; + /// Outputs an universal text which is supposed to be an action. + void TextEmote(std::string const& text, WorldObject const* = nullptr, bool = false) override; + void TextEmote(uint32 textId, WorldObject const* target = nullptr, bool isBossEmote = false) override; + /// Handles whispers from Addons and players based on sender, receiver's guid and language. + void Whisper(std::string const& text, Language language, Player* receiver, bool = false) override; + void Whisper(uint32 textId, Player* target, bool isBossWhisper = false) override; + void WhisperAddon(std::string const& text, std::string const& prefix, Player* receiver); + + /*********************************************************/ + /*** STORAGE SYSTEM ***/ + /*********************************************************/ + + void SetVirtualItemSlot(uint8 i, Item* item); + void SetSheath(SheathState sheathed) override; // overwrite Unit version + uint8 FindEquipSlot(ItemTemplate const* proto, uint32 slot, bool swap) const; + uint32 GetItemCount(uint32 item, bool inBankAlso = false, Item* skipItem = nullptr) const; + uint32 GetItemCountWithLimitCategory(uint32 limitCategory, Item* skipItem = nullptr) const; + Item* GetItemByGuid(ObjectGuid guid) const; + Item* GetItemByEntry(uint32 entry) const; + Item* GetItemByPos(uint16 pos) const; + Item* GetItemByPos(uint8 bag, uint8 slot) const; + Item* GetUseableItemByPos(uint8 bag, uint8 slot) const; + Bag* GetBagByPos(uint8 slot) const; + uint32 GetFreeInventorySpace() const; + Item* GetWeaponForAttack(WeaponAttackType attackType, bool useable = false) const; + Item* GetShield(bool useable = false) const; + static WeaponAttackType GetAttackBySlot(uint8 slot); // MAX_ATTACK if not weapon slot + std::vector& GetItemUpdateQueue() { return m_itemUpdateQueue; } + static bool IsInventoryPos(uint16 pos) { return IsInventoryPos(pos >> 8, pos & 255); } + static bool IsInventoryPos(uint8 bag, uint8 slot); + static bool IsEquipmentPos(uint16 pos) { return IsEquipmentPos(pos >> 8, pos & 255); } + static bool IsEquipmentPos(uint8 bag, uint8 slot); + static bool IsBagPos(uint16 pos); + static bool IsBankPos(uint16 pos) { return IsBankPos(pos >> 8, pos & 255); } + static bool IsBankPos(uint8 bag, uint8 slot); + bool IsValidPos(uint16 pos, bool explicit_pos) const { return IsValidPos(pos >> 8, pos & 255, explicit_pos); } + bool IsValidPos(uint8 bag, uint8 slot, bool explicit_pos) const; + uint8 GetBankBagSlotCount() const { return GetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS); } + void SetBankBagSlotCount(uint8 count) { SetByteValue(PLAYER_BYTES_2, PLAYER_BYTES_2_OFFSET_BANK_BAG_SLOTS, count); } + bool HasItemCount(uint32 item, uint32 count = 1, bool inBankAlso = false) const; + bool HasItemFitToSpellRequirements(SpellInfo const* spellInfo, Item const* ignoreItem = nullptr) const; + bool HasAllItemsToFitToSpellRequirements(SpellInfo const* spellInfo); + bool CanNoReagentCast(SpellInfo const* spellInfo) const; + bool HasItemOrGemWithIdEquipped(uint32 item, uint32 count, uint8 except_slot = NULL_SLOT) const; + bool HasItemWithLimitCategoryEquipped(uint32 limitCategory, uint32 count, uint8 except_slot = NULL_SLOT) const; + bool HasGemWithLimitCategoryEquipped(uint32 limitCategory, uint32 count, uint8 except_slot = NULL_SLOT) const; + InventoryResult CanTakeMoreSimilarItems(Item* pItem, uint32* offendingItemId = nullptr) const; + InventoryResult CanTakeMoreSimilarItems(uint32 entry, uint32 count, uint32* offendingItemId = nullptr) const { return CanTakeMoreSimilarItems(entry, count, nullptr, nullptr, offendingItemId); } + InventoryResult CanStoreNewItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, uint32 item, uint32 count, uint32* no_space_count = nullptr) const; + InventoryResult CanStoreItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, Item* pItem, bool swap = false) const; + InventoryResult CanStoreItems(Item** items, int count, uint32* offendingItemId) const; + InventoryResult CanEquipNewItem(uint8 slot, uint16& dest, uint32 item, bool swap) const; + InventoryResult CanEquipItem(uint8 slot, uint16& dest, Item* pItem, bool swap, bool not_loading = true) const; + + InventoryResult CanEquipUniqueItem(Item* pItem, uint8 except_slot = NULL_SLOT, uint32 limit_count = 1) const; + InventoryResult CanEquipUniqueItem(ItemTemplate const* itemProto, uint8 except_slot = NULL_SLOT, uint32 limit_count = 1) const; + InventoryResult CanUnequipItems(uint32 item, uint32 count) const; + InventoryResult CanUnequipItem(uint16 src, bool swap) const; + InventoryResult CanBankItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, Item* pItem, bool swap, bool not_loading = true) const; + InventoryResult CanUseItem(Item* pItem, bool not_loading = true) const; + bool HasItemTotemCategory(uint32 TotemCategory) const; + InventoryResult CanUseItem(ItemTemplate const* pItem) const; + bool CanRollNeedForItem(ItemTemplate const* itemTemplate) const; + Item* StoreNewItem(ItemPosCountVec const& pos, uint32 item, bool update, ItemRandomEnchantmentId const& randomPropertyId = {}, GuidSet const& allowedLooters = GuidSet()); + Item* StoreItem(ItemPosCountVec const& pos, Item* pItem, bool update); + Item* EquipNewItem(uint16 pos, uint32 item, bool update); + Item* EquipItem(uint16 pos, Item* pItem, bool update); + void AutoUnequipOffhandIfNeed(bool force = false); + bool StoreNewItemInBestSlots(uint32 item_id, uint32 item_count); + void AutoStoreLoot(uint8 bag, uint8 slot, uint32 loot_id, LootStore const& store, bool broadcast = false); + void AutoStoreLoot(uint32 loot_id, LootStore const& store, bool broadcast = false) { AutoStoreLoot(NULL_BAG, NULL_SLOT, loot_id, store, broadcast); } + void StoreLootItem(ObjectGuid lootWorldObjectGuid, uint8 lootSlot, Loot* loot, GameObject* go = nullptr); + + InventoryResult CanTakeMoreSimilarItems(uint32 entry, uint32 count, Item* pItem, uint32* no_space_count = nullptr, uint32* offendingItemId = nullptr) const; + InventoryResult CanStoreItem(uint8 bag, uint8 slot, ItemPosCountVec& dest, uint32 entry, uint32 count, Item* pItem = nullptr, bool swap = false, uint32* no_space_count = nullptr) const; + + void AddRefundReference(ObjectGuid it); + void DeleteRefundReference(ObjectGuid it); + + /// send initialization of new currency for client + void SendNewCurrency(uint32 id) const; + /// send full data about all currencies to client + void SendCurrencies() const; + /// send conquest currency points and their cap week/arena + void SendPvpRewards() const; + /// return count of currency witch has plr + uint32 GetCurrency(uint32 id, bool usePrecision) const; + /// return count of currency gaind on current week + uint32 GetCurrencyOnWeek(uint32 id, bool usePrecision) const; + /// return count of earned currency on current season + uint32 GetCurrencyOnSeason(uint32 id) const; + /// return week cap by currency id + uint32 GetCurrencyWeekCap(uint32 id, bool usePrecision) const; + /// return presence related currency + bool HasCurrency(uint32 id, uint32 count) const; + /// initialize currency count for custom initialization at create character + void SetCurrency(uint32 currencyId, uint32 amount); + void ResetCurrencyWeekCap(); + void ModifyCurrency(uint32 currencyId, int32 amount, bool supressChatLog = false, bool ignoreModifiersAndTracking = false); + + void ApplyEquipCooldown(Item* pItem); + void QuickEquipItem(uint16 pos, Item* pItem); + void VisualizeItem(uint8 slot, Item* pItem); + void SetVisibleItemSlot(uint8 slot, Item* pItem); + Item* BankItem(ItemPosCountVec const& dest, Item* pItem, bool update); + void RemoveItem(uint8 bag, uint8 slot, bool update); + void MoveItemFromInventory(uint8 bag, uint8 slot, bool update); + // in trade, auction, guild bank, mail.... + void MoveItemToInventory(ItemPosCountVec const& dest, Item* pItem, bool update, bool in_characterInventoryDB = false); + // in trade, guild bank, mail.... + void RemoveItemDependentAurasAndCasts(Item* pItem); + void DestroyItem(uint8 bag, uint8 slot, bool update); + void DestroyItemCount(uint32 item, uint32 count, bool update, bool unequip_check = false); + void DestroyItemCount(Item* item, uint32& count, bool update); + void DestroyConjuredItems(bool update); + void DestroyZoneLimitedItem(bool update, uint32 new_zone); + void SplitItem(uint16 src, uint16 dst, uint32 count); + void SwapItem(uint16 src, uint16 dst); + void AddItemToBuyBackSlot(Item* pItem); + Item* GetItemFromBuyBackSlot(uint32 slot); + void RemoveItemFromBuyBackSlot(uint32 slot, bool del); + void SendEquipError(InventoryResult msg, Item* pItem, Item* pItem2 = nullptr, uint32 itemid = 0) const; + void SendBuyError(BuyResult msg, Creature* creature, uint32 item, uint32 param) const; + void SendSellError(SellResult msg, Creature* creature, ObjectGuid guid) const; + void AddWeaponProficiency(uint32 newflag) { m_WeaponProficiency |= newflag; } + void AddArmorProficiency(uint32 newflag) { m_ArmorProficiency |= newflag; } + uint32 GetWeaponProficiency() const { return m_WeaponProficiency; } + uint32 GetArmorProficiency() const { return m_ArmorProficiency; } + bool IsUseEquipedWeapon(bool mainhand) const; + bool IsTwoHandUsed() const; + bool IsUsingTwoHandedWeaponInOneHand() const; + void SendNewItem(Item* item, uint32 count, bool received, bool created, bool broadcast = false, bool sendChatMessage = true); + bool BuyItemFromVendorSlot(ObjectGuid vendorguid, uint32 vendorslot, uint32 item, uint32 count, uint8 bag, uint8 slot); + bool BuyCurrencyFromVendorSlot(ObjectGuid vendorGuid, uint32 vendorSlot, uint32 currency, uint32 count); + bool _StoreOrEquipNewItem(uint32 vendorslot, uint32 item, uint32 count, uint8 bag, uint8 slot, int64 price, ItemTemplate const* pProto, Creature* pVendor, VendorItem const* crItem, bool bStore); + + float GetReputationPriceDiscount(Creature const* creature) const; + + Player* GetTrader() const; + TradeData* GetTradeData() const { return m_trade; } + void TradeCancel(bool sendback, TradeStatus status = TRADE_STATUS_TRADE_CANCELED); + + CinematicMgr* GetCinematicMgr() const { return _cinematicMgr.get(); } + + void UpdateEnchantTime(uint32 time); + void UpdateSoulboundTradeItems(); + void AddTradeableItem(Item* item); + void RemoveTradeableItem(Item* item); + void UpdateItemDuration(uint32 time, bool realtimeonly = false); + void AddEnchantmentDurations(Item* item); + void RemoveEnchantmentDurations(Item* item); + void RemoveEnchantmentDurationsReferences(Item* item); + void RemoveArenaEnchantments(EnchantmentSlot slot); + void AddEnchantmentDuration(Item* item, EnchantmentSlot slot, uint32 duration); + void ApplyEnchantment(Item* item, EnchantmentSlot slot, bool apply, bool apply_dur = true, bool ignore_condition = false); + void ApplyEnchantment(Item* item, bool apply); + void ApplyReforgeEnchantment(Item* item, bool apply); + void UpdateSkillEnchantments(uint16 skill_id, uint16 curr_value, uint16 new_value); + void SendEnchantmentDurations(); + void BuildEnchantmentsInfoData(WorldPacket* data); + void AddItemDurations(Item* item); + void RemoveItemDurations(Item* item); + void SendItemDurations(); + void LoadCorpse(PreparedQueryResult result); + void LoadPet(); + void LoadPetsFromDB(PreparedQueryResult result); + + bool AddItem(uint32 itemId, uint32 count); + + uint32 m_stableSlots; + + /*********************************************************/ + /*** GOSSIP SYSTEM ***/ + /*********************************************************/ + + void PrepareGossipMenu(WorldObject* source, uint32 menuId = 0, bool showQuests = false); + void SendPreparedGossip(WorldObject* source); + void OnGossipSelect(WorldObject* source, uint32 gossipListId, uint32 menuId); + + uint32 GetGossipTextId(uint32 menuId, WorldObject* source); + uint32 GetGossipTextId(WorldObject* source); + uint32 GetGossipMenuForSource(WorldObject* source); + + /*********************************************************/ + /*** QUEST SYSTEM ***/ + /*********************************************************/ + + int32 GetQuestLevel(Quest const* quest) const { return quest && (quest->GetQuestLevel() > 0) ? quest->GetQuestLevel() : getLevel(); } + + void PrepareQuestMenu(ObjectGuid guid); + void SendPreparedQuest(ObjectGuid guid); + bool IsActiveQuest(uint32 quest_id) const; + Quest const* GetNextQuest(Object const* questGiver, Quest const* quest) const; + bool CanSeeStartQuest(Quest const* quest) const; + bool CanTakeQuest(Quest const* quest, bool msg) const; + bool CanAddQuest(Quest const* quest, bool msg) const; + bool CanCompleteQuest(uint32 quest_id); + bool CanCompleteRepeatableQuest(Quest const* quest); + bool CanRewardQuest(Quest const* quest, bool msg); + bool CanRewardQuest(Quest const* quest, uint32 reward, bool msg); + void AddQuestAndCheckCompletion(Quest const* quest, Object* questGiver); + void AddQuest(Quest const* quest, Object* questGiver); + void AbandonQuest(uint32 quest_id); + void CompleteQuest(uint32 quest_id); + void IncompleteQuest(uint32 quest_id); + int32 GetQuestMoneyReward(Quest const* quest) const; + uint32 GetQuestXPReward(Quest const* quest); + void RewardQuest(Quest const* quest, uint32 reward, Object* questGiver, bool announce = true); + void SetRewardedQuest(uint32 quest_id); + void FailQuest(uint32 quest_id); + void FailQuestsWithFlag(QuestFlags flag); + bool SatisfyQuestSkill(Quest const* qInfo, bool msg) const; + bool SatisfyQuestLevel(Quest const* qInfo, bool msg) const; + bool SatisfyQuestLog(bool msg) const; + bool SatisfyQuestDependentQuests(Quest const* qInfo, bool msg) const; + bool SatisfyQuestPreviousQuest(Quest const* qInfo, bool msg) const; + bool SatisfyQuestDependentPreviousQuests(Quest const* qInfo, bool msg) const; + bool SatisfyQuestBreadcrumbQuest(Quest const* qInfo, bool msg) const; + bool SatisfyQuestDependentBreadcrumbQuests(Quest const* qInfo, bool msg) const; + bool SatisfyQuestClass(Quest const* qInfo, bool msg) const; + bool SatisfyQuestRace(Quest const* qInfo, bool msg) const; + bool SatisfyQuestReputation(Quest const* qInfo, bool msg) const; + bool SatisfyQuestStatus(Quest const* qInfo, bool msg) const; + bool SatisfyQuestConditions(Quest const* qInfo, bool msg) const; + bool SatisfyQuestTimed(Quest const* qInfo, bool msg) const; + bool SatisfyQuestExclusiveGroup(Quest const* qInfo, bool msg) const; + bool SatisfyQuestDay(Quest const* qInfo, bool msg) const; + bool SatisfyQuestWeek(Quest const* qInfo, bool msg) const; + bool SatisfyQuestMonth(Quest const* qInfo, bool msg) const; + bool SatisfyQuestSeasonal(Quest const* qInfo, bool msg) const; + bool GiveQuestSourceItem(Quest const* quest); + bool TakeQuestSourceItem(uint32 questId, bool msg); + bool GetQuestRewardStatus(uint32 quest_id) const; + QuestStatus GetQuestStatus(uint32 quest_id) const; + void SetQuestStatus(uint32 questId, QuestStatus status, bool update = true); + void RemoveActiveQuest(uint32 questId, bool update = true); + void RemoveRewardedQuest(uint32 questId, bool update = true); + void SendQuestUpdate(uint32 questId); + QuestGiverStatus GetQuestDialogStatus(Object* questGiver); + bool SatisfyFirstLFGReward(uint32 dungeonId, uint8 maxRewCount) const; + uint8 GetFirstRewardCountForDungeonId(uint32 dungeon); + + void SetDailyQuestStatus(uint32 quest_id); + bool IsDailyQuestDone(uint32 quest_id); + void SetWeeklyQuestStatus(uint32 quest_id); + void SetMonthlyQuestStatus(uint32 quest_id); + void SetSeasonalQuestStatus(uint32 quest_id); + void SetLFGRewardStatus(uint32 dungeon_id, bool daily_reset); + void ResetDailyQuestStatus(); + void ResetWeeklyQuestStatus(); + void ResetMonthlyQuestStatus(); + void ResetSeasonalQuestStatus(uint16 event_id, time_t eventStartTime); + void ResetWeeklyLFGRewardStatus(); + void ResetDailyLFGRewardStatus(); + + uint16 FindQuestSlot(uint32 quest_id) const; + uint32 GetQuestSlotQuestId(uint16 slot) const; + uint32 GetQuestSlotState(uint16 slot) const; + uint16 GetQuestSlotCounter(uint16 slot, uint8 counter) const; + uint32 GetQuestSlotTime(uint16 slot) const; + void SetQuestSlot(uint16 slot, uint32 quest_id, uint32 timer = 0); + void SetQuestSlotCounter(uint16 slot, uint8 counter, uint16 count); + void SetQuestSlotState(uint16 slot, uint32 state); + void RemoveQuestSlotState(uint16 slot, uint32 state); + void SetQuestSlotTimer(uint16 slot, uint32 timer); + void SwapQuestSlot(uint16 slot1, uint16 slot2); + + uint16 GetReqKillOrCastCurrentCount(uint32 quest_id, int32 entry) const; + void AreaExploredOrEventHappens(uint32 questId); + void GroupEventHappens(uint32 questId, WorldObject const* pEventObject); + void ItemAddedQuestCheck(uint32 entry, uint32 count); + void ItemRemovedQuestCheck(uint32 entry, uint32 count); + void KilledMonster(CreatureTemplate const* cInfo, ObjectGuid guid); + void KilledMonsterCredit(uint32 entry, ObjectGuid guid = ObjectGuid::Empty); + void KilledPlayerCredit(uint16 count = 1); + void KilledPlayerCreditForQuest(uint16 count, Quest const* quest); + void KillCreditGO(uint32 entry, ObjectGuid guid = ObjectGuid::Empty); + void TalkedToCreature(uint32 entry, ObjectGuid guid); + void MoneyChanged(uint64 value); + void ReputationChanged(FactionEntry const* factionEntry); + void ReputationChanged2(FactionEntry const* factionEntry); + bool HasQuestForItem(uint32 itemId, uint32 excludeQuestId = 0, bool turnIn = false) const; + bool HasQuestForGO(int32 goId) const; + void UpdateVisibleGameobjectsOrSpellClicks(); + bool CanShareQuest(uint32 questId) const; + + void SendQuestComplete(Quest const* quest) const; + void SendQuestReward(Quest const* quest, Creature const* questGiver, uint32 xp); + void SendQuestFailed(uint32 questId, InventoryResult reason = EQUIP_ERR_OK) const; + void SendQuestTimerFailed(uint32 questId) const; + void SendCanTakeQuestResponse(QuestFailedReason reason) const; + void SendQuestConfirmAccept(Quest const* quest, Player* receiver) const; + void SendPushToPartyResponse(Player const* player, uint8 msg) const; + void SendQuestUpdateAddCredit(Quest const* quest, ObjectGuid guid, uint32 creatureOrGOIdx, uint16 count); + void SendQuestUpdateAddPlayer(Quest const* quest, uint16 newCount); + void SendQuestGiverStatusMultiple(); + + uint32 GetSharedQuestID() const { return m_sharedQuestId; } + ObjectGuid GetPlayerSharingQuest() const { return m_playerSharingQuest; } + void SetQuestSharingInfo(ObjectGuid guid, uint32 id) { m_playerSharingQuest = guid; m_sharedQuestId = id; } + void ClearQuestSharingInfo() { m_playerSharingQuest = ObjectGuid::Empty; m_sharedQuestId = 0; } + + // Returns the ID of the next available quest in chain of the most recently rewarded quest that has been rewarded via UI popup + uint32 GetPopupQuestId() const { return m_popupQuestId; } + void SetPopupQuestId(uint32 questId) { m_popupQuestId = questId; } + + uint32 GetInGameTime() const { return m_ingametime; } + void SetInGameTime(uint32 time) { m_ingametime = time; } + + void AddTimedQuest(uint32 questId) { m_timedquests.insert(questId); } + void RemoveTimedQuest(uint32 questId) { m_timedquests.erase(questId); } + + void SaveCUFProfile(uint8 id, std::nullptr_t) { _CUFProfiles[id] = nullptr; } ///> Empties a CUF profile at position 0-4 + void SaveCUFProfile(uint8 id, std::unique_ptr profile) { _CUFProfiles[id] = std::move(profile); } ///> Replaces a CUF profile at position 0-4 + CUFProfile* GetCUFProfile(uint8 id) const { return _CUFProfiles[id].get(); } ///> Retrieves a CUF profile at position 0-4 + uint8 GetCUFProfilesCount() const + { + uint8 count = 0; + for (uint8 i = 0; i < MAX_CUF_PROFILES; ++i) + if (_CUFProfiles[i]) + ++count; + return count; + } + + bool HasPvPForcingQuest() const; + + /*********************************************************/ + /*** LOAD SYSTEM ***/ + /*********************************************************/ + + bool LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& holder); + bool IsLoading() const override; + + void Initialize(ObjectGuid::LowType guid); + static uint32 GetZoneIdFromDB(ObjectGuid guid); + static bool LoadPositionFromDB(uint32& mapid, float& x, float& y, float& z, float& o, bool& in_flight, ObjectGuid guid); + + static bool IsValidGender(uint8 Gender) { return Gender <= GENDER_FEMALE; } + static bool ValidateAppearance(uint8 race, uint8 class_, uint8 gender, uint8 hairID, uint8 hairColor, uint8 faceID, uint8 facialHair, uint8 skinColor, bool create = false); + static bool IsValidClass(uint8 Class) { return ((1 << (Class - 1)) & CLASSMASK_ALL_PLAYABLE) != 0; } + static bool IsValidRace(uint8 Race) { return ((1 << (Race - 1)) & RACEMASK_ALL_PLAYABLE) != 0; } + + /*********************************************************/ + /*** SAVE SYSTEM ***/ + /*********************************************************/ + + void SaveToDB(bool create = false); + void SaveToDB(CharacterDatabaseTransaction trans, bool create = false); + void SaveInventoryAndGoldToDB(CharacterDatabaseTransaction& trans); // fast save function for item/money cheating preventing + void SaveGoldToDB(CharacterDatabaseTransaction& trans) const; + + static void Customize(CharacterCustomizeInfo const* customizeInfo, CharacterDatabaseTransaction& trans); + static void SavePositionInDB(WorldLocation const& loc, uint16 zoneId, ObjectGuid guid, CharacterDatabaseTransaction& trans); + + static void DeleteFromDB(ObjectGuid playerguid, uint32 accountId, bool updateRealmChars = true, bool deleteFinally = false); + static void DeleteOldCharacters(); + static void DeleteOldCharacters(uint32 keepDays); + + bool m_mailsUpdated; + + void SetBindPoint(ObjectGuid guid) const; + void SendTalentWipeConfirm(ObjectGuid guid) const; + void ResetPetTalents(); + void RegenerateAll(uint32 diff); + void RegenerateHealth() override; + void setWeaponChangeTimer(uint32 time) { m_weaponChangeTimer = time;} + + uint64 GetMoney() const { return GetUInt64Value(PLAYER_FIELD_COINAGE); } + bool ModifyMoney(int64 amount, bool sendError = true); + bool HasEnoughMoney(uint64 amount) const { return (GetMoney() >= amount); } + bool HasEnoughMoney(int64 amount) const; + void SetMoney(uint64 value); + + RewardedQuestSet const& getRewardedQuests() const { return m_RewardedQuests; } + QuestStatusMap& getQuestStatusMap() { return m_QuestStatus; } + + size_t GetRewardedQuestCount() const { return m_RewardedQuests.size(); } + bool IsQuestRewarded(uint32 quest_id) const; + + Unit* GetSelectedUnit() const; + Player* GetSelectedPlayer() const; + + void SetTarget(ObjectGuid /*guid*/) override { } /// Used for serverside target changes, does not apply to players + void SetSelection(ObjectGuid guid) { SetGuidValue(UNIT_FIELD_TARGET, guid); } + + void SendMailResult(uint32 mailId, MailResponseType mailAction, MailResponseResult mailError, uint32 equipError = 0, ObjectGuid::LowType item_guid = 0, uint32 item_count = 0) const; + void SendNewMail() const; + void UpdateNextMailTimeAndUnreads(); + void AddNewMailDeliverTime(time_t deliver_time); + + void RemoveMail(uint32 id); + + void AddMail(Mail* mail) { m_mail.push_front(mail);}// for call from WorldSession::SendMailTo + uint32 GetMailSize() { return m_mail.size();} + Mail* GetMail(uint32 id); + + PlayerMails::iterator GetMailBegin() { return m_mail.begin();} + PlayerMails::iterator GetMailEnd() { return m_mail.end();} + + void SendItemRetrievalMail(uint32 itemEntry, uint32 count); // Item retrieval mails sent by The Postmaster (34337), used in multiple places. + + /*********************************************************/ + /*** MAILED ITEMS SYSTEM ***/ + /*********************************************************/ + + uint8 unReadMails; + time_t m_nextMailDelivereTime; + + typedef std::unordered_map ItemMap; + + ItemMap mMitems; //template defined in objectmgr.cpp + + Item* GetMItem(uint32 id); + void AddMItem(Item* it); + bool RemoveMItem(uint32 id); + + void SendOnCancelExpectedVehicleRideAura() const; + bool CanControlPet(uint32 spellId = 0) const; + void PetSpellInitialize(); + void CharmSpellInitialize(); + void PossessSpellInitialize(); + void VehicleSpellInitialize(); + void SendRemoveControlBar() const; + bool HasSpell(uint32 spell) const override; + bool HasActiveSpell(uint32 spell) const; // show in spellbook + bool IsSpellFitByClassAndRace(uint32 spell_id) const; + bool HandlePassiveSpellLearn(SpellInfo const* spellInfo); + bool IsCurrentSpecMasterySpell(SpellInfo const* spellInfo) const; + + void SendProficiency(ItemClass itemClass, uint32 itemSubclassMask) const; + void SendKnownSpells(bool firstLogin = false); + bool AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading = false, uint32 fromSkill = 0); + void LearnSpell(uint32 spell_id, bool dependent, uint32 fromSkill = 0); + void RemoveSpell(uint32 spell_id, bool disabled = false, bool learn_low_rank = true); + void ResetSpells(bool myClassOnly = false); + void LearnCustomSpells(); + void LearnDefaultSkills(); + void LearnDefaultSkill(SkillRaceClassInfoEntry const* rcInfo); + void LearnQuestRewardedSpells(); + void LearnQuestRewardedSpells(Quest const* quest); + void LearnSpellHighestRank(uint32 spellid); + void AddTemporarySpell(uint32 spellId); + void RemoveTemporarySpell(uint32 spellId); + void SetReputation(uint32 factionentry, uint32 value); + uint32 GetReputation(uint32 factionentry) const; + std::string GetGuildName() const; + void SendSpellCategoryCooldowns() const; + void SendRaidGroupOnlyMessage(RaidGroupReason reason, int32 delay) const; + + // Talents + uint32 GetFreeTalentPoints() const; + void SetFreeTalentPoints(uint32 points); + uint32 GetUsedTalentCount() const; + void SetUsedTalentCount(uint32 talents); + uint32 GetQuestRewardedTalentCount() const; + void AddQuestRewardedTalentCount(uint32 points); + uint32 GetTalentResetCost() const; + void SetTalentResetCost(uint32 cost); + uint32 GetTalentResetTime() const; + void SetTalentResetTime(time_t time_); + uint32 GetPrimaryTalentTree(uint8 spec) const; + void SetPrimaryTalentTree(uint8 spec, uint32 tree); + uint8 GetActiveSpec() const; + void SetActiveSpec(uint8 spec); + uint8 GetSpecsCount() const; + void SetSpecsCount(uint8 count); + bool ResetTalents(bool no_cost = false); + uint32 GetNextResetTalentsCost() const; + void InitTalentForLevel(); + void BuildPlayerTalentsInfoData(WorldPacket* data); + void BuildPetTalentsInfoData(WorldPacket* data); + void SendTalentsInfoData(bool pet); + bool LearnTalent(uint32 talentId, uint32 talentRank); + void LearnPetTalent(ObjectGuid petGuid, uint32 talentId, uint32 talentRank); + bool LearnPrimaryTalentSpecialization(uint8 talentTabIndex); + bool AddTalent(uint32 spellId, uint8 spec, bool learning); + bool HasTalent(uint32 spell_id, uint8 spec) const; + uint32 CalculateTalentsPoints() const; + + // Armor Specializaion + void UpdateArmorSpecialization(); + + // Dual Spec + void UpdateSpecCount(uint8 count); + void ActivateSpec(uint8 spec); + void LoadActions(PreparedQueryResult result); + + void InitGlyphsForLevel(); + void SetGlyphSlot(uint8 slot, uint32 slottype) { SetUInt32Value(PLAYER_FIELD_GLYPH_SLOTS_1 + slot, slottype); } + + uint32 GetGlyphSlot(uint8 slot) const { return GetUInt32Value(PLAYER_FIELD_GLYPH_SLOTS_1 + slot); } + void SetGlyph(uint8 slot, uint32 glyph); + uint32 GetGlyph(uint8 spec, uint8 slot) const; + + PlayerTalentMap const& GetTalentMap(uint8 spec) const; + PlayerTalentMap& GetTalentMap(uint8 spec); + ActionButtonList const& GetActionButtons() const { return m_actionButtons; } + + uint32 GetFreePrimaryProfessionPoints() const { return GetUInt32Value(PLAYER_CHARACTER_POINTS); } + void SetFreePrimaryProfessions(uint16 profs) { SetUInt32Value(PLAYER_CHARACTER_POINTS, profs); } + void InitPrimaryProfessions(); + + PlayerSpellMap const& GetSpellMap() const { return m_spells; } + PlayerSpellMap & GetSpellMap() { return m_spells; } + + void AddSpellMod(SpellModifier* mod, bool apply); + static bool IsAffectedBySpellmod(SpellInfo const* spellInfo, SpellModifier const* mod, Spell* spell = nullptr); + template + void GetSpellModValues(SpellInfo const* spellInfo, SpellModOp op, Spell* spell, T base, int32* flat, float* pct) const; + template + void ApplySpellMod(SpellInfo const* spellInfo, SpellModOp op, T& basevalue, Spell* spell = nullptr) const; + static void ApplyModToSpell(SpellModifier* mod, Spell* spell); + void SetSpellModTakingSpell(Spell* spell, bool apply); + void SendSpellModifiers() const; + + void RemoveArenaSpellCooldowns(bool removeActivePetCooldowns = false); + uint32 GetLastPotionId() const { return m_lastPotionId; } + void SetLastPotionId(uint32 item_id) { m_lastPotionId = item_id; } + void UpdatePotionCooldown(Spell* spell = nullptr); + + void SetResurrectRequestData(WorldObject const* caster, uint32 health, uint32 mana, uint32 appliedAura); + void ClearResurrectRequestData() { _resurrectionData.reset(); } + + bool IsResurrectRequestedBy(ObjectGuid const& guid) const + { + if (!IsResurrectRequested()) + return false; + + return _resurrectionData->GUID == guid; + } + + bool IsResurrectRequested() const { return _resurrectionData != nullptr; } + void ResurrectUsingRequestData(); + + uint8 getCinematic() const { return m_cinematic; } + void setCinematic(uint8 cine) { m_cinematic = cine; } + + uint32 GetMovie() const { return m_movie; } + void SetMovie(uint32 movie) { m_movie = movie; } + + ActionButton* addActionButton(uint8 button, uint32 action, uint8 type); + void removeActionButton(uint8 button); + ActionButton const* GetActionButton(uint8 button); + void SendInitialActionButtons() const { SendActionButtons(0); } + void SendActionButtons(uint32 state) const; + bool IsActionButtonDataValid(uint8 button, uint32 action, uint8 type) const; + + PvPInfo pvpInfo; + void UpdatePvPState(bool onlyFFA = false); + void SetPvP(bool state) override; + void UpdatePvP(bool state, bool override = false); + void UpdateZone(uint32 newZone, uint32 newArea); + void UpdateArea(uint32 newArea); + void UpdateHostileAreaState(AreaTableEntry const* area); + void SetNeedsZoneUpdate(bool needsUpdate) { m_needsZoneUpdate = needsUpdate; } + + void UpdateZoneDependentAuras(uint32 zone_id); // zones + void UpdateAreaDependentAuras(uint32 area_id); // subzones + + void UpdateAfkReport(time_t currTime); + void UpdatePvPFlag(time_t currTime); + void SetContestedPvP(Player* attackedPlayer = nullptr); + void UpdateContestedPvP(uint32 currTime); + void SetContestedPvPTimer(uint32 newTime) {m_contestedPvPTimer = newTime;} + void ResetContestedPvP(); + + /// @todo: maybe move UpdateDuelFlag+DuelComplete to independent DuelHandler + DuelInfo* duel; + void UpdateDuelFlag(time_t currTime); + void CheckDuelDistance(time_t currTime); + void DuelComplete(DuelCompleteType type); + void SendDuelCountdown(uint32 counter); + + bool IsGroupVisibleFor(Player const* p) const; + bool IsInSameGroupWith(Player const* p) const; + bool IsInSameRaidWith(Player const* p) const; + void UninviteFromGroup(); + static void RemoveFromGroup(Group* group, ObjectGuid guid, RemoveMethod method = GROUP_REMOVEMETHOD_DEFAULT, ObjectGuid kicker = ObjectGuid::Empty, char const* reason = nullptr); + void RemoveFromGroup(RemoveMethod method = GROUP_REMOVEMETHOD_DEFAULT) { RemoveFromGroup(GetGroup(), GetGUID(), method); } + + // VIANO_VIRTUAL_TARGET_ICONS_V1 + void SetVianoTargetIcon(uint8 id, ObjectGuid guid) + { + if (id >= _vianoTargetIcons.size()) + return; + + if (guid) + { + for (ObjectGuid& existing : _vianoTargetIcons) + if (existing == guid) + existing.Clear(); + } + + _vianoTargetIcons[id] = guid; + } + + ObjectGuid GetVianoTargetIcon(uint8 id) const + { + return id < _vianoTargetIcons.size() ? _vianoTargetIcons[id] : ObjectGuid::Empty; + } + void SendUpdateToOutOfRangeGroupMembers(); + + void SetInGuild(uint32 guildId); + void SetGuildRank(uint8 rankId) { SetUInt32Value(PLAYER_GUILDRANK, rankId); } + uint8 GetGuildRank() const { return uint8(GetUInt32Value(PLAYER_GUILDRANK)); } + void SetGuildLevel(uint32 level) { SetUInt32Value(PLAYER_GUILDLEVEL, level); } + uint32 GetGuildLevel() { return GetUInt32Value(PLAYER_GUILDLEVEL); } + void SetGuildIdInvited(uint32 GuildId) { m_GuildIdInvited = GuildId; } + uint32 GetGuildId() const { return GetUInt32Value(OBJECT_FIELD_DATA); /* return only lower part */ } + Guild* GetGuild(); + int GetGuildIdInvited() const { return m_GuildIdInvited; } + static void RemovePetitionsAndSigns(ObjectGuid guid, CharterTypes type); + + // Arena Team + void SetInArenaTeam(uint32 ArenaTeamId, uint8 slot, uint8 type); + void SetArenaTeamInfoField(uint8 slot, ArenaTeamInfoType type, uint32 value); + static void LeaveAllArenaTeams(ObjectGuid guid); + uint32 GetArenaTeamId(uint8 slot) const { return GetUInt32Value(PLAYER_FIELD_ARENA_TEAM_INFO_1_1 + (slot * ARENA_TEAM_END) + ARENA_TEAM_ID); } + uint32 GetArenaPersonalRating(uint8 slot) const { return GetUInt32Value(PLAYER_FIELD_ARENA_TEAM_INFO_1_1 + (slot * ARENA_TEAM_END) + ARENA_TEAM_PERSONAL_RATING); } + void SetArenaTeamIdInvited(uint32 ArenaTeamId) { m_ArenaTeamIdInvited = ArenaTeamId; } + uint32 GetArenaTeamIdInvited() const { return m_ArenaTeamIdInvited; } + uint32 GetRBGPersonalRating() const { return 0; } + + Difficulty GetDifficulty(bool isRaid) const { return isRaid ? m_raidDifficulty : m_dungeonDifficulty; } + Difficulty GetDungeonDifficulty() const { return m_dungeonDifficulty; } + Difficulty GetRaidDifficulty() const { return m_raidDifficulty; } + Difficulty GetStoredRaidDifficulty() const { return m_raidMapDifficulty; } // only for use in difficulty packet after exiting to raid map + void SetDungeonDifficulty(Difficulty dungeon_difficulty) { m_dungeonDifficulty = dungeon_difficulty; } + void SetRaidDifficulty(Difficulty raid_difficulty) { m_raidDifficulty = raid_difficulty; } + void StoreRaidMapDifficulty(); + + bool UpdateSkill(uint32 skill_id, uint32 step); + bool UpdateSkillPro(uint16 skillId, int32 chance, uint32 step); + + bool UpdateCraftSkill(SpellInfo const* spellInfo); + bool UpdateGatherSkill(uint32 SkillId, uint32 SkillValue, uint32 RedLevel, uint32 Multiplicator = 1); + bool UpdateFishingSkill(); + + void SurveyDigSite(); + void NotifyRequestResearchHistory(); + bool ArchProjectCompleteable(uint16 projectId); + bool HasArchProject(uint16 projectId); + void CompleteArchProject(uint16 projectId); + void SetArchData(ArchData const& data); + + float GetHealthBonusFromStamina(); + float GetManaBonusFromIntellect(); + + void ApplySpellPenetrationBonus(int32 amount, bool apply); + void UpdateResistances(uint32 school) override; + void UpdateArmor() override; + void UpdateMaxHealth() override; + void UpdateMaxPower(PowerType power) override; + uint32 GetPowerIndex(PowerType power) const override; + void UpdateAttackPowerAndDamage(bool ranged = false) override; + void ApplySpellPowerBonus(int32 amount, bool apply); + void UpdateSpellDamageAndHealingBonus(); + void UpdateSpellHealingPercentDone(); + void UpdateSpellHealingPercentTaken(); + void ApplyRatingMod(CombatRating cr, int32 value, bool apply); + void UpdateRating(CombatRating cr); + void UpdateAllRatings(); + void UpdateMastery(); + bool CanUseMastery() const; + + void CalculateMinMaxDamage(WeaponAttackType attType, bool normalized, bool addTotalPct, float& minDamage, float& maxDamage) const override; + + void RecalculateRating(CombatRating cr) { ApplyRatingMod(cr, 0, true);} + float GetMeleeCritFromAgility() const; + void GetDodgeFromAgility(float &diminishing, float &nondiminishing) const; + float GetSpellCritFromIntellect() const; + float GetRatingMultiplier(CombatRating cr) const; + float GetRatingBonusValue(CombatRating cr) const; + + /// Returns base spellpower bonus from spellpower stat on items, without spellpower from intellect stat + uint32 GetBaseSpellPowerBonus() const { return m_baseSpellPower; } + int32 GetSpellPenetrationItemMod() const { return m_spellPenetrationItemMod; } + + bool CanApplyResilience() const override { return true; } + + float GetExpertiseDodgeOrParryReduction(WeaponAttackType attType) const; + void UpdateBlockPercentage(); + void UpdateCritPercentage(WeaponAttackType attType); + void UpdateAllCritPercentages(); + void UpdateParryPercentage(); + void UpdateDodgePercentage(); + void UpdateMeleeHitChances(); + void UpdateRangedHitChances(); + void UpdateSpellHitChances(); + void UpdateHitChances(); + + void UpdateAllSpellCritChances(); + void UpdateSpellCritChance(uint32 school); + void UpdateExpertise(WeaponAttackType attType); + void ApplyManaRegenBonus(int32 amount, bool apply); + void ApplyHealthRegenBonus(int32 amount, bool apply); + uint32 GetRuneTimer(uint8 index) const { return m_runeGraceCooldown[index]; } + void SetRuneTimer(uint8 index, uint32 timer) { m_runeGraceCooldown[index] = timer; } + uint32 GetLastRuneGraceTimer(uint8 index) const { return m_lastRuneGraceTimers[index]; } + void SetLastRuneGraceTimer(uint8 index, uint32 timer) { m_lastRuneGraceTimers[index] = timer; } + + ObjectGuid GetLootGUID() const { return m_lootGuid; } + void SetLootGUID(ObjectGuid guid) { m_lootGuid = guid; } + + void RemovedInsignia(Player* looterPlr); + + WorldSession* GetSession() const { return m_session; } + GameClient* GetGameClient() const; + + void BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) const override; + void DestroyForPlayer(Player* target, bool onDeath = false) const override; + + // notifiers + void SendAttackSwingCantAttack() const; + void SendAttackSwingCancelAttack() const; + void SendAttackSwingDeadTarget() const; + void SendAttackSwingNotInRange() const; + void SendAttackSwingBadFacingAttack() const; + void SendAutoRepeatCancel(Unit* target); + void SendExplorationExperience(uint32 Area, uint32 Experience) const; + + void SendDungeonDifficulty(bool IsInGroup) const; + void SendRaidDifficulty(bool IsInGroup, int32 forcedDifficulty = -1) const; + void ResetInstances(uint8 method, bool isRaid); + void SendResetInstanceSuccess(uint32 MapId) const; + void SendResetInstanceFailed(uint32 reason, uint32 MapId) const; + void SendResetFailedNotify(uint32 mapid) const; + + bool UpdatePosition(float x, float y, float z, float orientation, bool teleport = false) override; + bool UpdatePosition(Position const& pos, bool teleport = false) override { return UpdatePosition(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), pos.GetOrientation(), teleport); } + void ProcessTerrainStatusUpdate(ZLiquidStatus oldLiquidStatus, Optional const& newLiquidData) override; + void AtExitCombat() override; + + void SendMessageToSet(WorldPacket const* data, bool self) const override { SendMessageToSetInRange(data, GetVisibilityRange(), self); } + void SendMessageToSetInRange(WorldPacket const* data, float dist, bool self) const override; + void SendMessageToSetInRange(WorldPacket const* data, float dist, bool self, bool own_team_only) const; + void SendMessageToSet(WorldPacket const* data, Player const* skipped_rcvr) const override; + + Corpse* GetCorpse() const; + void SpawnCorpseBones(bool triggerSave = true); + Corpse* CreateCorpse(); + void KillPlayer(); + static void OfflineResurrect(ObjectGuid const& guid, CharacterDatabaseTransaction& trans); + bool HasCorpse() const { return _corpseLocation.GetMapId() != MAPID_INVALID; } + WorldLocation GetCorpseLocation() const { return _corpseLocation; } + uint32 GetResurrectionSpellId(); + void ResurrectPlayer(float restore_percent, bool applySickness = false); + void BuildPlayerRepop(); + void RepopAtGraveyard(); + + void DurabilityLossAll(double percent, bool inventory); + void DurabilityLoss(Item* item, double percent); + void DurabilityPointsLossAll(int32 points, bool inventory); + void DurabilityPointsLoss(Item* item, int32 points); + void DurabilityPointLossForEquipSlot(EquipmentSlots slot); + void DurabilityRepairAll(bool takeCost, float discountMod, bool guildBank); + void DurabilityRepair(uint16 pos, bool takeCost, float discountMod); + + void UpdateMirrorTimers(); + void StopMirrorTimers(); + bool IsMirrorTimerActive(MirrorTimerType type) const; + + bool CanJoinConstantChannelInZone(ChatChannelsEntry const* channel, AreaTableEntry const* zone) const; + + void JoinedChannel(Channel* c); + void LeftChannel(Channel* c); + void CleanupChannels(); + void UpdateLocalChannels(uint32 newZone); + void LeaveLFGChannel(); + + typedef std::list JoinedChannelsList; + JoinedChannelsList const& GetJoinedChannels() const { return m_channels; } + + void InitializeSkillFields(); + void SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal); + uint16 GetMaxSkillValue(uint32 skill) const; // max + perm. bonus + temp bonus + uint16 GetPureMaxSkillValue(uint32 skill) const; // max + uint16 GetSkillValue(uint32 skill) const; // skill value + perm. bonus + temp bonus + uint16 GetBaseSkillValue(uint32 skill) const; // skill value + perm. bonus + uint16 GetPureSkillValue(uint32 skill) const; // skill value + int16 GetSkillPermBonusValue(uint32 skill) const; + int16 GetSkillTempBonusValue(uint32 skill) const; + uint16 GetSkillStep(uint16 skill) const; // 0...6 + bool HasSkill(uint32 skill) const; + void LearnSkillRewardedSpells(uint32 skillId, uint32 skillValue); + + uint32 GetSkillRank(uint8 pos) const; + void SetSkillLineId(uint8 pos, uint16 value); + void SetSkillStep(uint8 pos, uint16 value); + void SetSkillRank(uint8 pos, uint16 value); + void SetSkillMaxRank(uint8 pos, uint16 value); + void SetSkillTempBonus(uint8 pos, uint16 value); + void SetSkillPermBonus(uint8 pos, uint16 value); + + WorldLocation& GetTeleportDest() { return m_teleport_dest; } + Optional GetTeleportDestInstanceId() const { return m_teleport_instanceId; } + bool IsBeingTeleported() const { return mSemaphoreTeleport_Near || mSemaphoreTeleport_Far; } + bool IsBeingTeleportedNear() const { return mSemaphoreTeleport_Near; } + bool IsBeingTeleportedFar() const { return mSemaphoreTeleport_Far; } + void SetSemaphoreTeleportNear(bool semphsetting) { mSemaphoreTeleport_Near = semphsetting; } + void SetSemaphoreTeleportFar(bool semphsetting) { mSemaphoreTeleport_Far = semphsetting; } + void ProcessDelayedOperations(); + + void CheckAreaExplore(); + void SetAreaExplored(uint32 areaId); + + // These methods are used to periodically update certain area and aura based mechanics used in Heartbeat and Movement + void UpdateZoneAndAreaId(); + void UpdateIndoorsOutdoorsAuras(); + void UpdateTavernRestingState(); + + static uint32 TeamForRace(uint8 race); + uint32 GetTeam() const { return m_team; } + TeamId GetTeamId() const { return m_team == ALLIANCE ? TEAM_ALLIANCE : TEAM_HORDE; } + void SetFactionForRace(uint8 race); + + void InitDisplayIds(); + + bool IsAtGroupRewardDistance(WorldObject const* pRewardSource) const; + bool IsAtRecruitAFriendDistance(WorldObject const* pOther) const; + void RewardPlayerAndGroupAtKill(Unit* victim, bool isBattleGround); + void RewardPlayerAndGroupAtEvent(uint32 creature_id, WorldObject* pRewardSource); + bool isHonorOrXPTarget(Unit const* victim) const; + + bool GetsRecruitAFriendBonus(bool forXP); + uint8 GetGrantableLevels() const { return m_grantableLevels; } + void SetGrantableLevels(uint8 val) { m_grantableLevels = val; } + + ReputationMgr& GetReputationMgr() { return *m_reputationMgr; } + ReputationMgr const& GetReputationMgr() const { return *m_reputationMgr; } + ReputationRank GetReputationRank(uint32 faction_id) const; + void RewardOnKill(Unit* victim, float rate); + void RewardReputation(Quest const* quest); + + int32 CalculateReputationGain(ReputationSource source, uint32 creatureOrQuestLevel, int32 rep, int32 faction, bool noQuestBonus = false); + + void UpdateSkillsForLevel(); + void ModifySkillBonus(uint32 skillid, int32 val, bool talent); + + void SetLastSoulburnSpell(SpellInfo const* spell) { m_lastSoulburnSpell = spell; } + SpellInfo const* GetLastSoulburnSpell() const { return m_lastSoulburnSpell; } + + /*********************************************************/ + /*** PVP SYSTEM ***/ + /*********************************************************/ + // TODO: Properly implement correncies as of Cataclysm + void UpdateHonorFields(); + bool RewardHonor(Unit* victim, uint32 groupsize, int32 honor = -1, bool pvptoken = false); + uint32 GetMaxPersonalArenaRatingRequirement(uint32 minarenaslot) const; + + // duel health and mana reset methods + void SaveHealthBeforeDuel() { healthBeforeDuel = GetHealth(); } + void SaveManaBeforeDuel() { manaBeforeDuel = GetPower(POWER_MANA); } + void RestoreHealthAfterDuel() { SetHealth(healthBeforeDuel); } + void RestoreManaAfterDuel() { SetPower(POWER_MANA, manaBeforeDuel); } + + //End of PvP System + + void SetDrunkValue(uint8 newDrunkValue, uint32 itemId = 0); + uint8 GetDrunkValue() const { return GetByteValue(PLAYER_BYTES_3, PLAYER_BYTES_3_OFFSET_INEBRIATION); } + static DrunkenState GetDrunkenstateByValue(uint8 value); + + uint32 GetDeathTimer() const { return m_deathTimer; } + uint32 GetCorpseReclaimDelay(bool pvp) const; + void UpdateCorpseReclaimDelay(); + int32 CalculateCorpseReclaimDelay(bool load = false) const; + void SendCorpseReclaimDelay(uint32 delay) const; + + uint32 GetBlockPercent() const override { return GetUInt32Value(PLAYER_SHIELD_BLOCK); } + bool CanParry() const { return m_canParry; } + void SetCanParry(bool value); + bool CanBlock() const { return m_canBlock; } + void SetCanBlock(bool value); + bool CanTitanGrip() const { return m_canTitanGrip; } + void SetCanTitanGrip(bool value, uint32 penaltySpellId = 0); + void CheckTitanGripPenalty(); + bool CanTameExoticPets() const { return IsGameMaster() || HasAuraType(SPELL_AURA_ALLOW_TAME_PET_TYPE); } + + void SetRegularAttackTime(); + + void HandleBaseModFlatValue(BaseModGroup modGroup, float amount, bool apply); + void ApplyBaseModPctValue(BaseModGroup modGroup, float pct); + + void SetBaseModFlatValue(BaseModGroup modGroup, float val); + void SetBaseModPctValue(BaseModGroup modGroup, float val); + + void UpdateDamageDoneMods(WeaponAttackType attackType, int32 skipEnchantSlot = -1) override; + void UpdateBaseModGroup(BaseModGroup modGroup); + + float GetBaseModValue(BaseModGroup modGroup, BaseModType modType) const; + float GetTotalBaseModValue(BaseModGroup modGroup) const; + + void _ApplyAllStatBonuses(); + void _RemoveAllStatBonuses(); + + void ResetAllPowers(); + + SpellSchoolMask GetMeleeDamageSchoolMask(WeaponAttackType attackType = BASE_ATTACK) const override; + + void CastAllObtainSpells(); + void ApplyItemObtainSpells(Item* item, bool apply); + + void UpdateWeaponDependentCritAuras(WeaponAttackType attackType); + void UpdateAllWeaponDependentCritAuras(); + + void UpdateWeaponDependentAuras(WeaponAttackType attackType); + void ApplyItemDependentAuras(Item* item, bool apply); + + bool CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEffect const* aurEff) const override; + + void _ApplyItemMods(Item* item, uint8 slot, bool apply, bool updateItemAuras = true); + void _RemoveAllItemMods(); + void _ApplyAllItemMods(); + void _ApplyAllLevelScaleItemMods(bool apply); + ScalingStatDistributionEntry const* GetScalingStatDistributionFor(ItemTemplate const& itemTemplate) const; + ScalingStatValuesEntry const* GetScalingStatValuesFor(ItemTemplate const& itemTemplate) const; + void _ApplyItemBonuses(ItemTemplate const* proto, uint8 slot, bool apply, bool only_level_scale = false); + void _ApplyWeaponDamage(uint8 slot, ItemTemplate const* proto, bool apply); + bool EnchantmentFitsRequirements(uint32 enchantmentcondition, int8 slot) const; + void ToggleMetaGemsActive(uint8 exceptslot, bool apply); + void CorrectMetaGemEnchants(uint8 slot, bool apply); + void InitDataForForm(bool reapplyMods = false); + + void ApplyItemEquipSpell(Item* item, bool apply, bool form_change = false); + void ApplyEquipSpell(SpellInfo const* spellInfo, Item* item, bool apply, bool form_change = false); + void UpdateEquipSpellsAtFormChange(); + void CastItemCombatSpell(DamageInfo const& damageInfo); + void CastItemCombatSpell(DamageInfo const& damageInfo, Item* item, ItemTemplate const* proto); + void CastItemUseSpell(Item* item, SpellCastTargets const& targets, uint8 castId, uint32 glyphIndex); + + void SendEquipmentSetList(); + void SetEquipmentSet(EquipmentSetInfo::EquipmentSetData const& eqset); + void DeleteEquipmentSet(uint64 setGuid); + + void SendInitWorldStates(uint32 zone, uint32 area); + void SendUpdateWorldState(uint32 variable, uint32 value, bool hidden = false) const; + void SendDirectMessage(WorldPacket const* data) const; + + void SendAurasForTarget(Unit* target) const; + + std::unique_ptr PlayerTalkClass; + std::vector> ItemSetEff; + + void SendLoot(ObjectGuid guid, LootType loot_type); + void SendLootError(ObjectGuid guid, LootError error) const; + void SendLootRelease(ObjectGuid guid) const; + void SendNotifyLootItemRemoved(uint8 lootSlot) const; + void SendNotifyCurrencyLootRemoved(uint8 lootSlot); + void SendNotifyLootMoneyRemoved() const; + + /*********************************************************/ + /*** BATTLEGROUND SYSTEM ***/ + /*********************************************************/ + + bool InBattleground() const { return m_bgData.bgInstanceID != 0; } + bool InArena() const; + uint32 GetBattlegroundId() const { return m_bgData.bgInstanceID; } + BattlegroundTypeId GetBattlegroundTypeId() const { return m_bgData.bgTypeID; } + Battleground* GetBattleground() const; + + uint32 GetBattlegroundQueueJoinTime(uint32 bgTypeId) const; + void AddBattlegroundQueueJoinTime(uint32 bgTypeId, uint32 joinTime); + void RemoveBattlegroundQueueJoinTime(uint32 bgTypeId); + + bool InBattlegroundQueue() const; + + BattlegroundQueueTypeId GetBattlegroundQueueTypeId(uint32 index) const; + uint32 GetBattlegroundQueueIndex(BattlegroundQueueTypeId bgQueueTypeId) const; + bool IsInvitedForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const; + bool InBattlegroundQueueForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const; + + void SetBattlegroundId(uint32 val, BattlegroundTypeId bgTypeId); + uint32 AddBattlegroundQueueId(BattlegroundQueueTypeId val); + bool HasFreeBattlegroundQueueId() const; + void RemoveBattlegroundQueueId(BattlegroundQueueTypeId val); + void SetInviteForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId, uint32 instanceId); + bool IsInvitedForBattlegroundInstance(uint32 instanceId) const; + WorldLocation const& GetBattlegroundEntryPoint() const { return m_bgData.joinPos; } + void SetBattlegroundEntryPoint(); + + void SetBGTeam(uint32 team); + uint32 GetBGTeam() const; + + void LeaveBattleground(bool teleportToEntryPoint = true); + bool CanJoinToBattleground(Battleground const* bg) const; + bool CanReportAfkDueToLimit(); + void ReportedAfkBy(Player* reporter); + void ClearAfkReports() { m_bgData.bgAfkReporter.clear(); } + + bool GetBGAccessByLevel(BattlegroundTypeId bgTypeId) const; + bool isTotalImmunity() const; + bool CanUseBattlegroundObject(GameObject* gameobject) const; + bool isTotalImmune() const; + bool CanCaptureTowerPoint() const; + + bool GetRandomWinner() const { return m_IsBGRandomWinner; } + void SetRandomWinner(bool isWinner); + + /*********************************************************/ + /*** OUTDOOR PVP SYSTEM ***/ + /*********************************************************/ + + OutdoorPvP* GetOutdoorPvP() const; + // returns true if the player is in active state for outdoor pvp objective capturing, false otherwise + bool IsOutdoorPvPActive() const; + + /*********************************************************/ + /*** ENVIROMENTAL SYSTEM ***/ + /*********************************************************/ + + bool IsImmuneToEnvironmentalDamage() const; + uint32 EnvironmentalDamage(EnviromentalDamage type, uint32 damage); + + /*********************************************************/ + /*** FLOOD FILTER SYSTEM ***/ + /*********************************************************/ + + struct ChatFloodThrottle + { + enum Index + { + REGULAR = 0, + ADDON = 1, + MAX + }; + + time_t Time = 0; + uint32 Count = 0; + }; + + void UpdateSpeakTime(ChatFloodThrottle::Index index); + + /*********************************************************/ + /*** VARIOUS SYSTEMS ***/ + /*********************************************************/ + void UpdateFallInformationIfNeed(MovementInfo const& minfo, uint16 opcode); + + WorldObject* m_seer; + void SetFallInformation(uint32 time, float z); + void HandleFall(MovementInfo const& movementInfo); + bool SetDisableGravity(bool disable, bool updateAnimTier = true) override; + bool SetCanFly(bool enable) override; + bool SetCanTransitionBetweenSwimAndFly(bool enable) override; + void SendMovementSetCollisionHeight(float height, UpdateCollisionHeightReason reason); + + void SetClientControl(Unit* target, bool allowMove); + + void SetSeer(WorldObject* target) { m_seer = target; } + void SetViewpoint(WorldObject* target, bool apply); + WorldObject* GetViewpoint() const; + void StopCastingCharm(); + void StopCastingBindSight() const; + + uint32 GetSaveTimer() const { return m_nextSave; } + void SetSaveTimer(uint32 timer) { m_nextSave = timer; } + + void SaveRecallPosition() + { + m_recall_location.WorldRelocate(*this); + m_recall_instanceId = GetInstanceId(); + } + void Recall() { TeleportTo(m_recall_location, TELE_TO_NONE, m_recall_instanceId); } + + void SetHomebind(WorldLocation const& loc, uint32 areaId); + void SendBindPointUpdate() const; + + // Homebind coordinates + uint32 m_homebindMapId; + uint16 m_homebindAreaId; + float m_homebindX; + float m_homebindY; + float m_homebindZ; + + WorldLocation GetStartPosition() const; + + // currently visible objects at player client + GuidUnorderedSet m_clientGUIDs; + GuidUnorderedSet m_visibleTransports; + + bool HaveAtClient(Object const* u) const; + + bool IsNeverVisible() const override; + + bool IsVisibleGloballyFor(Player const* player) const; + + void SendInitialVisiblePackets(Unit* target) const; + void UpdateObjectVisibility(bool forced = true) override; + void UpdateVisibilityForPlayer(); + void UpdateVisibilityOf(WorldObject* target); + void UpdateTriggerVisibility(); + + template + void UpdateVisibilityOf(T* target, UpdateData& data, std::set& visibleNow); + + uint8 m_forced_speed_changes[MAX_MOVE_TYPE]; + + bool HasAtLoginFlag(AtLoginFlags f) const { return (m_atLoginFlags & f) != 0; } + void SetAtLoginFlag(AtLoginFlags f) { m_atLoginFlags |= f; } + void RemoveAtLoginFlag(AtLoginFlags flags, bool persist = false); + + bool isUsingLfg() const; + bool inRandomLfgDungeon() const; + + void GetLFGLeavePoint(Position* pos); + bool HasValidLFGLeavePoint(uint32 mapid); + void SetLFGLeavePoint(); + + // Temporarily removed pet cache + uint32 GetTemporaryUnsummonedPetNumber() const { return m_temporaryUnsummonedPetNumber; } + void SetTemporaryUnsummonedPetNumber(uint32 petnumber) { m_temporaryUnsummonedPetNumber = petnumber; } + void UnsummonPetTemporaryIfAny(); + void ResummonPetTemporaryUnSummonedIfAny(); + bool IsPetNeedBeTemporaryUnsummoned() const; + + void SendCinematicStart(uint32 cinematicId); + void SendMovieStart(uint32 movieId); + + uint32 DoRandomRoll(uint32 minimum, uint32 maximum); + + /*********************************************************/ + /*** INSTANCE SYSTEM ***/ + /*********************************************************/ + + typedef std::unordered_map< uint32 /*mapId*/, InstancePlayerBind > BoundInstancesMap; + + void UpdateHomebindTime(uint32 time); + + uint32 m_HomebindTimer; + bool m_InstanceValid; + // permanent binds and solo binds by difficulty + BoundInstancesMap m_boundInstances[MAX_DIFFICULTY]; + InstancePlayerBind* GetBoundInstance(uint32 mapid, Difficulty difficulty, bool withExpired = false); + InstancePlayerBind const* GetBoundInstance(uint32 mapid, Difficulty difficulty, bool withExpired = false) const; + BoundInstancesMap& GetBoundInstances(Difficulty difficulty) { return m_boundInstances[difficulty]; } + InstanceSave* GetInstanceSave(uint32 mapid, bool raid); + void UnbindInstance(uint32 mapid, Difficulty difficulty, bool unload = false); + void UnbindInstance(BoundInstancesMap::iterator &itr, Difficulty difficulty, bool unload = false); + InstancePlayerBind* BindToInstance(InstanceSave* save, bool permanent, BindExtensionState extendState = EXTEND_STATE_NORMAL, bool load = false); + void BindToInstance(); + void SetPendingBind(uint32 instanceId, uint32 bindTimer); + bool HasPendingBind() const { return _pendingBindId > 0; } + void SendRaidInfo(); + void SendSavedInstances(); + bool Satisfy(AccessRequirement const* ar, uint32 target_map, bool report = false); + bool CheckInstanceValidity(bool /*isLogin*/); + bool CheckInstanceCount(uint32 instanceId) const; + void AddInstanceEnterTime(uint32 instanceId, time_t enterTime); + + // last used pet number (for BG's) + uint32 GetLastPetNumber() const { return m_lastpetnumber; } + void SetLastPetNumber(uint32 petnumber) { m_lastpetnumber = petnumber; } + + /*********************************************************/ + /*** GROUP SYSTEM ***/ + /*********************************************************/ + + bool IsInGroup(ObjectGuid groupGuid) const; + Group* GetGroupInvite() const { return m_groupInvite; } + void SetGroupInvite(Group* group) { m_groupInvite = group; } + Group* GetGroup() { return m_group.getTarget(); } + Group const* GetGroup() const { return const_cast(m_group.getTarget()); } + GroupReference& GetGroupRef() { return m_group; } + void SetGroup(Group* group, int8 subgroup = -1); + uint8 GetSubGroup() const { return m_group.getSubGroup(); } + uint32 GetGroupUpdateFlag() const { return m_groupUpdateMask; } + void SetGroupUpdateFlag(uint32 flag) { m_groupUpdateMask |= flag; } + uint64 GetAuraUpdateMaskForRaid() const { return m_auraRaidUpdateMask; } + void SetAuraUpdateMaskForRaid(uint8 slot) { m_auraRaidUpdateMask |= (uint64(1) << slot); } + Player* GetNextRandomRaidMember(float radius); + PartyResult CanUninviteFromGroup(ObjectGuid guidMember = ObjectGuid::Empty) const; + + // Battleground / Battlefield Group System + void SetBattlegroundOrBattlefieldRaid(Group* group, int8 subgroup = -1); + void RemoveFromBattlegroundOrBattlefieldRaid(); + Group* GetOriginalGroup() const { return m_originalGroup.getTarget(); } + GroupReference& GetOriginalGroupRef() { return m_originalGroup; } + uint8 GetOriginalSubGroup() const { return m_originalGroup.getSubGroup(); } + void SetOriginalGroup(Group* group, int8 subgroup = -1); + + void SetPassOnGroupLoot(bool bPassOnGroupLoot) { m_bPassOnGroupLoot = bPassOnGroupLoot; } + bool GetPassOnGroupLoot() const { return m_bPassOnGroupLoot; } + + MapReference &GetMapRef() { return m_mapRef; } + + // Set map to player and add reference + void SetMap(Map* map) override; + void ResetMap() override; + + bool isAllowedToLoot(Creature const* creature); + + DeclinedName const* GetDeclinedNames() const { return m_declinedname.get(); } + + // Runes + uint8 GetRunesState() const { return m_runes->RuneState; } + RuneType GetBaseRune(uint8 index) const { return RuneType(m_runes->_Runes[index].BaseRune); } + RuneType GetCurrentRune(uint8 index) const { return RuneType(m_runes->_Runes[index].CurrentRune); } + float GetRuneCooldown(uint8 index) const { return m_runes->_Runes[index].Cooldown; } + bool IsRuneFullyDepleted(uint8 index) const; + /// + /// Checks if there is a fully depleted rune. A rune is considered fully depleted when it's on cooldown and isn't recovering yet ('waiting' for another rune to finish regenerating) + /// + /// The kind of base rune that is being checked (Blood, Unholy, Frost). Death Runes are no base rune and can't be checked with this method. Use the underlying base rune instead. + /// true when there is a fully depleted rune + bool HasFullyDepletedRune(RuneType runeType) const; + RuneType GetLastUsedRune() { return m_runes->LastUsedRune; } + uint8 GetLastUsedRuneMask() { return m_runes->LastUsedRuneMask; } + void ClearLastUsedRuneMask() { m_runes->LastUsedRuneMask = 0; } + void SetLastUsedRune(RuneType type) { m_runes->LastUsedRune = type; } + void SetLastUsedRuneIndex(uint8 index) { m_runes->LastUsedRuneMask |= (1 << index); } + void SetBaseRune(uint8 index, RuneType baseRune) { m_runes->_Runes[index].BaseRune = baseRune; } + void SetCurrentRune(uint8 index, RuneType currentRune) { m_runes->_Runes[index].CurrentRune = currentRune; } + void SetRuneConvertAura(uint8 index, AuraEffect const* aura, AuraType auraType, SpellInfo const* spellInfo); + void AddRuneByAuraEffect(uint8 index, RuneType newType, AuraEffect const* aura, AuraType auraType, SpellInfo const* spellInfo); + void SetRuneCooldown(uint8 index, float cooldown); + void RemoveRunesByAuraEffect(AuraEffect const* aura); + void RestoreBaseRune(uint8 index); + void ConvertRune(uint8 index, RuneType newType); + void SendConvertedRunes(); + void ResyncRunes() const; + void AddRunePower(uint8 mask) const; + void InitRunes(); + + void SendRespondInspectAchievements(Player* player) const; + uint32 GetAchievementPoints() const; + bool HasAchieved(uint32 achievementId) const; + void ResetAchievements(); + void FailAchievementCriteria(AchievementCriteriaFailEvent condition, int32 failAsset); + void UpdateAchievementCriteria(AchievementCriteriaTypes type, uint64 miscValue1 = 0, uint64 miscValue2 = 0, uint64 miscValue3 = 0, WorldObject* ref = nullptr, GameObject* go = nullptr); + void StartAchievementCriteria(AchievementCriteriaStartEvent startEvent, uint32 entry, Milliseconds timeLost = Milliseconds::zero()); + void CompletedAchievement(AchievementEntry const* entry); + + bool HasTitle(uint32 bitIndex) const; + bool HasTitle(CharTitlesEntry const* title) const; + void SetTitle(CharTitlesEntry const* title, bool lost = false); + + //bool isActiveObject() const { return true; } + bool CanSeeSpellClickOn(Creature const* creature) const; + + uint32 GetChampioningFaction() const { return m_ChampioningFaction; } + void SetChampioningFaction(uint32 faction) { m_ChampioningFaction = faction; } + uint8 GetExpansionForFaction(uint32 factionId) const; + + Spell* m_spellModTakingSpell; + + float GetAverageItemLevel() const; + bool isDebugAreaTriggers; + + void ClearWhisperWhiteList() { WhisperList.clear(); } + void AddWhisperWhiteList(ObjectGuid guid) { WhisperList.push_back(guid); } + bool IsInWhisperWhiteList(ObjectGuid guid); + void RemoveFromWhisperWhiteList(ObjectGuid guid) { WhisperList.remove(guid); } + + void ReadMovementInfo(WorldPacket& data, MovementInfo* mi, Movement::ExtraMovementStatusElement* extras = nullptr); + + /*! These methods send different packets to the client in apply and unapply case. + These methods are only sent to the current unit. + */ + + void ValidateMovementInfo(MovementInfo* mi); + + bool CanFly() const override { return m_movementInfo.HasMovementFlag(MOVEMENTFLAG_CAN_FLY); } + bool CanEnterWater() const override { return true; } + + std::string GetMapAreaAndZoneString() const; + std::string GetCoordsMapAreaAndZoneString() const; + + // Void Storage + bool IsVoidStorageUnlocked() const { return HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_VOID_UNLOCKED); } + void UnlockVoidStorage(); + void LockVoidStorage(); + uint8 GetNextVoidStorageFreeSlot() const; + uint8 GetNumOfVoidStorageFreeSlots() const; + uint8 AddVoidStorageItem(VoidStorageItem const& item); + void AddVoidStorageItemAtSlot(uint8 slot, const VoidStorageItem& item); + void DeleteVoidStorageItem(uint8 slot); + bool SwapVoidStorageItem(uint8 oldSlot, uint8 newSlot); + VoidStorageItem* GetVoidStorageItem(uint8 slot) const; + VoidStorageItem* GetVoidStorageItem(uint64 id, uint8& slot) const; + + // Pets + PlayerPetData* GetPlayerPetDataById(uint32 petId); + PlayerPetData* GetPlayerPetDataBySlot(uint8 slot); + PlayerPetData* GetPlayerPetDataByCreatureId(uint32 creatureId); + PlayerPetData* GetPlayerPetDataCurrent(); + Optional GetFirstUnusedActivePetSlot(); + Optional GetFirstUnusedPetSlot(); + void DeleteFromPlayerPetDataStore(uint32 petNumber); + void AddToPlayerPetDataStore(std::unique_ptr playerPetData); + + protected: + // Gamemaster whisper whitelist + GuidList WhisperList; + uint32 m_contestedPvPTimer; + + /*********************************************************/ + /*** BATTLEGROUND SYSTEM ***/ + /*********************************************************/ + + /* + this is an array of BG queues (BgTypeIDs) in which is player + */ + struct BgBattlegroundQueueID_Rec + { + BattlegroundQueueTypeId bgQueueTypeId; + uint32 invitedToInstance; + }; + + BgBattlegroundQueueID_Rec m_bgBattlegroundQueueID[PLAYER_MAX_BATTLEGROUND_QUEUES]; + BGData m_bgData; + + bool m_IsBGRandomWinner; + + /*********************************************************/ + /*** QUEST SYSTEM ***/ + /*********************************************************/ + + //We allow only one timed quest active at the same time. Below can then be simple value instead of set. + typedef std::set QuestSet; + typedef std::unordered_map SeasonalQuestMapByQuest; + typedef std::unordered_map SeasonalQuestMapByEvent; + + struct LFGRewardInfo + { + LFGRewardInfo(uint8 _completionsThisPeriod = 0, bool _isDaily = false) : + CompletionsThisPeriod(_completionsThisPeriod), IsDaily(_isDaily) { } + + uint8 CompletionsThisPeriod; + bool IsDaily; + }; + + typedef std::unordered_map LFGRewardStatusMap; + + QuestSet m_timedquests; + QuestSet m_weeklyquests; + QuestSet m_monthlyquests; + SeasonalQuestMapByEvent m_seasonalquests; + LFGRewardStatusMap m_lfgrewardstatus; + + ObjectGuid m_playerSharingQuest; + uint32 m_sharedQuestId; + uint32 m_popupQuestId; + uint32 m_ingametime; + + /*********************************************************/ + /*** LOAD SYSTEM ***/ + /*********************************************************/ + + void _LoadActions(PreparedQueryResult result); + void _LoadAuras(PreparedQueryResult result, uint32 timediff); + void _LoadGlyphAuras(); + void _LoadBoundInstances(PreparedQueryResult result); + void _LoadInventory(PreparedQueryResult result, uint32 timeDiff); + void _LoadVoidStorage(PreparedQueryResult result); + void _LoadMail(PreparedQueryResult mailsResult, PreparedQueryResult mailItemsResult); + static Item* _LoadMailedItem(ObjectGuid const& playerGuid, Player* player, uint32 mailId, Mail* mail, Field* fields); + void _LoadQuestStatus(PreparedQueryResult result); + void _LoadQuestStatusRewarded(PreparedQueryResult result); + void _LoadDailyQuestStatus(PreparedQueryResult result); + void _LoadWeeklyQuestStatus(PreparedQueryResult result); + void _LoadMonthlyQuestStatus(PreparedQueryResult result); + void _LoadSeasonalQuestStatus(PreparedQueryResult result); + void _LoadRandomBGStatus(PreparedQueryResult result); + void _LoadGroup(PreparedQueryResult result); + void _LoadSkills(PreparedQueryResult result); + void _LoadSpells(PreparedQueryResult result); + bool _LoadHomeBind(PreparedQueryResult result); + void _LoadDeclinedNames(PreparedQueryResult result); + void _LoadArenaTeamInfo(PreparedQueryResult result); + void _LoadEquipmentSets(PreparedQueryResult result); + void _LoadBGData(PreparedQueryResult result); + void _LoadGlyphs(PreparedQueryResult result); + void _LoadTalents(PreparedQueryResult result); + void _LoadInstanceTimeRestrictions(PreparedQueryResult result); + void _LoadCurrency(PreparedQueryResult result); + void _LoadCUFProfiles(PreparedQueryResult result); + void _LoadLFGRewardStatus(PreparedQueryResult result); + + /*********************************************************/ + /*** SAVE SYSTEM ***/ + /*********************************************************/ + + void _SaveActions(CharacterDatabaseTransaction& trans); + void _SaveAuras(CharacterDatabaseTransaction& trans); + void _SaveInventory(CharacterDatabaseTransaction& trans); + void _SaveVoidStorage(CharacterDatabaseTransaction& trans); + void _SaveMail(CharacterDatabaseTransaction& trans); + void _SaveQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveDailyQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveWeeklyQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveMonthlyQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveSeasonalQuestStatus(CharacterDatabaseTransaction& trans); + void _SaveSkills(CharacterDatabaseTransaction& trans); + void _SaveSpells(CharacterDatabaseTransaction& trans); + void _SaveEquipmentSets(CharacterDatabaseTransaction& trans); + void _SaveBGData(CharacterDatabaseTransaction& trans); + void _SaveGlyphs(CharacterDatabaseTransaction& trans) const; + void _SaveTalents(CharacterDatabaseTransaction& trans); + void _SaveStats(CharacterDatabaseTransaction& trans) const; + void _SaveInstanceTimeRestrictions(CharacterDatabaseTransaction& trans); + void _SaveCurrency(CharacterDatabaseTransaction& trans); + void _SaveCUFProfiles(CharacterDatabaseTransaction& trans); + void _SaveLFGRewardStatus(CharacterDatabaseTransaction& trans); + + /*********************************************************/ + /*** ENVIRONMENTAL SYSTEM ***/ + /*********************************************************/ + void HandleSobering(); + void SendMirrorTimer(MirrorTimerType Type, uint32 MaxValue, uint32 CurrentValue, int32 Regen); + void StopMirrorTimer(MirrorTimerType Type); + void HandleDrowning(uint32 time_diff); + int32 getMaxTimer(MirrorTimerType timer) const; + + /*********************************************************/ + /*** HONOR SYSTEM ***/ + /*********************************************************/ + time_t m_lastHonorUpdateTime; + + void outDebugValues() const; + ObjectGuid m_lootGuid; + + uint32 m_team; + uint32 m_nextSave; + std::array m_chatFloodData; + Difficulty m_dungeonDifficulty; + Difficulty m_raidDifficulty; + Difficulty m_raidMapDifficulty; + + uint32 m_atLoginFlags; + + std::array m_items; + uint32 m_currentBuybackSlot; + + PlayerCurrenciesMap _currencyStorage; + + /** + * @name GetCurrencyWeekCap + * @brief return week cap for selected currency + + * @param CurrencyTypesEntry for which to retrieve weekly cap + */ + uint32 GetCurrencyWeekCap(CurrencyTypesEntry const* currency) const; + + /* + * @name GetCurrencyTotalCap + * @brief return total cap for selected currency + + * @param CurrencyTypesEntry for which to retrieve total cap + */ + uint32 GetCurrencyTotalCap(CurrencyTypesEntry const* currency) const; + + /// Updates weekly conquest point cap (dynamic cap) + void UpdateConquestCurrencyCap(uint32 currency); + + std::array, VOID_STORAGE_MAX_SLOT> _voidStorageItems; + + std::vector m_itemUpdateQueue; + bool m_itemUpdateQueueBlocked; + + uint32 m_ExtraFlags; + + private: + EnumFlag _characterFlags; + EnumFlag _characterFlags2; + public: + EnumFlag GetCharacterFlags() const { return _characterFlags; } + void AddCharacterFlag(CharacterFlags flag) { _characterFlags |= flag; } + void RemoveCharacterFlag(CharacterFlags flag) { _characterFlags &= ~flag; } + bool HasCharacterFlag(CharacterFlags flag) const { return _characterFlags.HasFlag(flag); } + + EnumFlag GetCharacterFlags2() const { return _characterFlags2; } + void AddCharacterFlag(CharacterFlags2 flag) { _characterFlags2 |= flag; } + void RemoveCharacterFlag(CharacterFlags2 flag) { _characterFlags2 &= ~flag; } + bool HasCharacterFlag(CharacterFlags2 flag) const { return _characterFlags2.HasFlag(flag); } + + void InitializePlayerFlags(); + + protected: + QuestStatusMap m_QuestStatus; + QuestStatusSaveMap m_QuestStatusSave; + + RewardedQuestSet m_RewardedQuests; + QuestStatusSaveMap m_RewardedQuestsSave; + + SkillStatusMap mSkillStatus; + + uint32 m_GuildIdInvited; + uint32 m_ArenaTeamIdInvited; + + PlayerMails m_mail; + PlayerSpellMap m_spells; + uint32 m_lastPotionId; // last used health/mana potion in combat, that block next potion use + + std::unique_ptr _talentMgr; + + ActionButtonList m_actionButtons; + + std::array m_auraBaseFlatMod; + std::array m_auraBasePctMod; + std::array m_baseRatingValue; + uint32 m_baseSpellPower; + uint32 m_baseManaRegen; + uint32 m_baseHealthRegen; + int32 m_spellPenetrationItemMod; + + SpellModContainer m_spellMods; + + EnchantDurationList m_enchantDuration; + ItemDurationList m_itemDuration; + GuidUnorderedSet m_itemSoulboundTradeable; + + std::unique_ptr _resurrectionData; + WorldSession* m_session; + + JoinedChannelsList m_channels; + + // VIANO_VIRTUAL_TARGET_ICONS_V1 + std::array _vianoTargetIcons{}; + + uint8 m_cinematic; + uint32 m_movie; + + TradeData* m_trade; + + bool m_DailyQuestChanged; + bool m_WeeklyQuestChanged; + bool m_MonthlyQuestChanged; + bool m_SeasonalQuestChanged; + bool m_LFGRewardStatusChanged; + time_t m_lastDailyQuestTime; + + uint32 m_hostileReferenceCheckTimer; + uint32 m_drunkTimer; + uint32 m_weaponChangeTimer; + + uint32 m_zoneUpdateId; + uint32 m_areaUpdateId; + + uint32 m_deathTimer; + time_t m_deathExpireTime; + + uint32 m_WeaponProficiency; + uint32 m_ArmorProficiency; + bool m_canParry; + bool m_canBlock; + bool m_canTitanGrip; + uint32 m_titanGripPenaltySpellId; + uint8 m_swingErrorMsg; + + ////////////////////Rest System///////////////////// + time_t _restTime; + uint32 inn_triggerId; + float m_rest_bonus; + uint32 _restFlagMask; + ////////////////////Rest System///////////////////// + + // Social + PlayerSocial* m_social; + + // Groups + GroupReference m_group; + GroupReference m_originalGroup; + Group* m_groupInvite; + uint32 m_groupUpdateMask; + uint64 m_auraRaidUpdateMask; + bool m_bPassOnGroupLoot; + + // last used pet number (for BG's) + uint32 m_lastpetnumber; + + // Player summoning + time_t m_summon_expire; + WorldLocation m_summon_location; + uint32 m_summon_instanceId; + + // Recall position + WorldLocation m_recall_location; + uint32 m_recall_instanceId; + + std::unique_ptr m_declinedname; + std::unique_ptr m_runes; + EquipmentSetContainer _equipmentSets; + + bool CanAlwaysSee(WorldObject const* obj) const override; + + bool IsAlwaysDetectableFor(WorldObject const* seer) const override; + + uint8 m_grantableLevels; + + uint8 m_fishingSteps; + + bool m_needsZoneUpdate; + + std::array, MAX_CUF_PROFILES> _CUFProfiles; + + SpellInfo const* m_lastSoulburnSpell; + + private: + // internal common parts for CanStore/StoreItem functions + InventoryResult CanStoreItem_InSpecificSlot(uint8 bag, uint8 slot, ItemPosCountVec& dest, ItemTemplate const* pProto, uint32& count, bool swap, Item* pSrcItem) const; + InventoryResult CanStoreItem_InBag(uint8 bag, ItemPosCountVec& dest, ItemTemplate const* pProto, uint32& count, bool merge, bool non_specialized, Item* pSrcItem, uint8 skip_bag, uint8 skip_slot) const; + InventoryResult CanStoreItem_InInventorySlots(uint8 slot_begin, uint8 slot_end, ItemPosCountVec& dest, ItemTemplate const* pProto, uint32& count, bool merge, Item* pSrcItem, uint8 skip_bag, uint8 skip_slot) const; + Item* _StoreItem(uint16 pos, Item* pItem, uint32 count, bool clone, bool update); + Item* _LoadItem(CharacterDatabaseTransaction& trans, uint32 zoneId, uint32 timeDiff, Field* fields); + + std::unique_ptr _cinematicMgr; + + GuidSet m_refundableItems; + void SendRefundInfo(Item* item); + void RefundItem(Item* item); + void SendItemRefundResult(Item* item, ItemExtendedCostEntry const* iece, uint8 error); + + // know currencies are not removed at any point (0 displayed) + void AddKnownCurrency(uint32 itemId); + + void AdjustQuestReqItemCount(Quest const* quest, QuestStatusData& questStatusData); + + bool IsCanDelayTeleport() const { return m_bCanDelayTeleport; } + void SetCanDelayTeleport(bool setting) { m_bCanDelayTeleport = setting; } + bool IsHasDelayedTeleport() const { return m_bHasDelayedTeleport; } + void SetDelayedTeleportFlag(bool setting) { m_bHasDelayedTeleport = setting; } + void ScheduleDelayedOperation(uint32 operation) { if (operation < DELAYED_END) m_DelayedOperations |= operation; } + + bool IsInstanceLoginGameMasterException() const; + + MapReference m_mapRef; + + uint32 m_lastFallTime; + float m_lastFallZ; + + std::array m_MirrorTimer; + uint8 m_MirrorTimerFlags; + uint8 m_MirrorTimerFlagsLast; + + // Rune type / Rune timer + std::array m_runeGraceCooldown; + std::array m_lastRuneGraceTimers; + + // Current teleport data + WorldLocation m_teleport_dest; + Optional m_teleport_instanceId; + TeleportToOptions m_teleport_options; + bool mSemaphoreTeleport_Near; + bool mSemaphoreTeleport_Far; + + uint32 m_DelayedOperations; + bool m_bCanDelayTeleport; + bool m_bHasDelayedTeleport; + + // Temporary removed pet cache + uint32 m_temporaryUnsummonedPetNumber; + uint32 m_oldpetspell; + + std::unique_ptr> m_achievementMgr; + std::unique_ptr m_reputationMgr; + + uint32 m_ChampioningFaction; + + InstanceTimeMap _instanceResetTimes; + uint32 _pendingBindId; + uint32 _pendingBindTimer; + + uint32 _activeCheats; + uint32 _maxPersonalArenaRate; + + bool _hasValidLFGLeavePoint; + + // variables to save health and mana before duel and restore them after duel + uint32 healthBeforeDuel; + uint32 manaBeforeDuel; + + WorldLocation _corpseLocation; + + std::unique_ptr _archaeology; + + std::vector> PlayerPetDataStore; + + TimeTrackerSmall m_petScalingSynchTimer; +}; + +TC_GAME_API void AddItemsSetItem(Player* player, Item* item); +TC_GAME_API void RemoveItemsSetItem(Player* player, ItemTemplate const* proto); + +#endif diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 37ac95aaca..33d7df4fad 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -3121,6 +3121,11 @@ void Unit::_AddAura(UnitAura* aura, Unit* caster) // aura application effects are handled separately to prevent aura list corruption AuraApplication* Unit::_CreateAuraApplication(Aura* aura, uint8 effMask) { + // VIANO_AURA_DEBUG_APPLICATION_V1 + if (aura && aura->GetId() == 465 && IsVianoAdventurer()) + TC_LOG_INFO("scripts", "[VianoAuraDebug] stage=CreateAuraApplication spell=%u target=%s", + aura->GetId(), GetGUID().ToString().c_str()); + // can't apply aura on unit which is going to be deleted - to not create a memory leak ASSERT(!m_cleanupDone); @@ -7964,6 +7969,10 @@ void Unit::UpdateMountCapability() AuraEffectList mounts = GetAuraEffectsByType(SPELL_AURA_MOUNTED); for (AuraEffect* aurEff : mounts) { + // Quest/event mounts without a MountType do not use MountCapability. + if (!aurEff->GetMiscValueB()) + continue; + aurEff->RecalculateAmount(); if (!aurEff->GetAmount()) aurEff->GetBase()->Remove(); diff --git a/src/server/game/Entities/Unit/Unit.cpp.bak-viano-mountcap-fix-20261005-092322 b/src/server/game/Entities/Unit/Unit.cpp.bak-viano-mountcap-fix-20261005-092322 new file mode 100644 index 0000000000..a79b375dd7 --- /dev/null +++ b/src/server/game/Entities/Unit/Unit.cpp.bak-viano-mountcap-fix-20261005-092322 @@ -0,0 +1,14387 @@ +/* + * 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 . + */ + +#include "Unit.h" +#include "AbstractPursuer.h" +#include "Archaeology.h" +#include "Battlefield.h" +#include "BattlefieldMgr.h" +#include "Battleground.h" +#include "BattlegroundScore.h" +#include "CellImpl.h" +#include "CharacterCache.h" +#include "CharmInfo.h" +#include "Chat.h" +#include "ChatTextBuilder.h" +#include "ChatPackets.h" +#include "CombatLogPackets.h" +#include "CombatPackets.h" +#include "Common.h" +#include "ConditionMgr.h" +#include "Containers.h" +#include "CreatureAI.h" +#include "CreatureAIImpl.h" +#include "CreatureGroups.h" +#include "Formulas.h" +#include "GameClient.h" +#include "GameTime.h" +#include "GridNotifiersImpl.h" +#include "Group.h" +#include "InstanceSaveMgr.h" +#include "InstanceScript.h" +#include "Item.h" +#include "ListUtils.h" +#include "Log.h" +#include "LootMgr.h" +#include "LootPackets.h" +#include "MiscPackets.h" +#include "MotionMaster.h" +#include "MovementGenerator.h" +#include "MovementPackets.h" +#include "MovementPacketBuilder.h" +#include "MovementStructures.h" +#include "MoveSpline.h" +#include "MoveSplineInit.h" +#include "MovementPacketSender.h" +#include "ObjectAccessor.h" +#include "ObjectMgr.h" +#include "Opcodes.h" +#include "OutdoorPvP.h" +#include "PassiveAI.h" +#include "Pet.h" +#include "PetPackets.h" +#include "PetAI.h" +#include "PhasingHandler.h" +#include "Player.h" +#include "PlayerAI.h" +#include "QuestDef.h" +#include "ReputationMgr.h" +#include "ScheduledChangeAI.h" +#include "Spell.h" +#include "SpellAuraEffects.h" +#include "SpellAuras.h" +#include "SpellCastRequest.h" +#include "SpellHistory.h" +#include "SpellInfo.h" +#include "SpellMgr.h" +#include "SpellPackets.h" +#include "Stats.h" +#include "TemporarySummon.h" +#include "Totem.h" +#include "Transport.h" +#include "UnitAI.h" +#include "UpdateFieldFlags.h" +#include "Util.h" +#include "Vehicle.h" +#include "VehiclePackets.h" +#include "World.h" +#include "WorldPacket.h" +#include "WorldSession.h" +#include +#include + +float baseMoveSpeed[MAX_MOVE_TYPE] = +{ + 2.5f, // MOVE_WALK + 7.0f, // MOVE_RUN + 4.5f, // MOVE_RUN_BACK + 4.722222f, // MOVE_SWIM + 2.5f, // MOVE_SWIM_BACK + 3.141594f, // MOVE_TURN_RATE + 7.0f, // MOVE_FLIGHT + 4.5f, // MOVE_FLIGHT_BACK + 3.14f // MOVE_PITCH_RATE +}; + +float playerBaseMoveSpeed[MAX_MOVE_TYPE] = +{ + 2.5f, // MOVE_WALK + 7.0f, // MOVE_RUN + 4.5f, // MOVE_RUN_BACK + 4.722222f, // MOVE_SWIM + 2.5f, // MOVE_SWIM_BACK + 3.141594f, // MOVE_TURN_RATE + 7.0f, // MOVE_FLIGHT + 4.5f, // MOVE_FLIGHT_BACK + 3.14f // MOVE_PITCH_RATE +}; + +DamageInfo::DamageInfo(Unit* attacker, Unit* victim, uint32 damage, SpellInfo const* spellInfo, SpellSchoolMask schoolMask, DamageEffectType damageType, WeaponAttackType attackType) + : m_attacker(attacker), m_victim(victim), m_damage(damage), m_spellInfo(spellInfo), m_schoolMask(schoolMask), m_damageType(damageType), m_attackType(attackType), + m_absorb(0), m_resist(0), m_block(0), m_hitMask(PROC_HIT_NONE) +{ +} + +DamageInfo::DamageInfo(CalcDamageInfo const& dmgInfo) + : m_attacker(dmgInfo.Attacker), m_victim(dmgInfo.Target), m_damage(dmgInfo.Damage), m_spellInfo(nullptr), m_schoolMask(SpellSchoolMask(dmgInfo.DamageSchoolMask)), + m_damageType(DIRECT_DAMAGE), m_attackType(dmgInfo.AttackType), m_absorb(dmgInfo.Absorb), m_resist(dmgInfo.Resist), m_block(dmgInfo.Blocked), m_hitMask(PROC_HIT_NONE) +{ + switch (dmgInfo.TargetState) + { + case VICTIMSTATE_IS_IMMUNE: + m_hitMask |= PROC_HIT_IMMUNE; + break; + case VICTIMSTATE_BLOCKS: + m_hitMask |= PROC_HIT_FULL_BLOCK; + break; + } + + if (dmgInfo.HitInfo & (HITINFO_PARTIAL_ABSORB | HITINFO_FULL_ABSORB)) + m_hitMask |= PROC_HIT_ABSORB; + + if (dmgInfo.HitInfo & HITINFO_FULL_RESIST) + m_hitMask |= PROC_HIT_FULL_RESIST; + + if (m_block) + m_hitMask |= PROC_HIT_BLOCK; + + bool const damageNullified = (dmgInfo.HitInfo & (HITINFO_FULL_ABSORB | HITINFO_FULL_RESIST)) != 0 || + (m_hitMask & (PROC_HIT_IMMUNE | PROC_HIT_FULL_BLOCK)) != 0; + switch (dmgInfo.HitOutCome) + { + case MELEE_HIT_MISS: + m_hitMask |= PROC_HIT_MISS; + break; + case MELEE_HIT_DODGE: + m_hitMask |= PROC_HIT_DODGE; + break; + case MELEE_HIT_PARRY: + m_hitMask |= PROC_HIT_PARRY; + break; + case MELEE_HIT_EVADE: + m_hitMask |= PROC_HIT_EVADE; + break; + case MELEE_HIT_BLOCK: + case MELEE_HIT_CRUSHING: + case MELEE_HIT_GLANCING: + case MELEE_HIT_NORMAL: + if (!damageNullified) + m_hitMask |= PROC_HIT_NORMAL; + break; + case MELEE_HIT_CRIT: + if (!damageNullified) + m_hitMask |= PROC_HIT_CRITICAL; + break; + } +} + +DamageInfo::DamageInfo(SpellNonMeleeDamage const& spellNonMeleeDamage, DamageEffectType damageType, WeaponAttackType attackType, ProcFlagsHit hitMask) + : m_attacker(spellNonMeleeDamage.attacker), m_victim(spellNonMeleeDamage.target), m_damage(spellNonMeleeDamage.damage), + m_spellInfo(sSpellMgr->GetSpellInfo(spellNonMeleeDamage.SpellID)), m_schoolMask(SpellSchoolMask(spellNonMeleeDamage.schoolMask)), m_damageType(damageType), + m_attackType(attackType), m_absorb(spellNonMeleeDamage.absorb), m_resist(spellNonMeleeDamage.resist), m_block(spellNonMeleeDamage.blocked), m_hitMask(hitMask) +{ + if (spellNonMeleeDamage.blocked) + m_hitMask |= PROC_HIT_BLOCK; + if (spellNonMeleeDamage.absorb) + m_hitMask |= PROC_HIT_ABSORB; +} + +void DamageInfo::ModifyDamage(int32 amount) +{ + amount = std::max(amount, -static_cast(GetDamage())); + m_damage += amount; +} + +void DamageInfo::AbsorbDamage(uint32 amount) +{ + amount = std::min(amount, GetDamage()); + m_absorb += amount; + m_damage -= amount; + m_hitMask |= PROC_HIT_ABSORB; +} + +void DamageInfo::ResistDamage(uint32 amount) +{ + amount = std::min(amount, GetDamage()); + m_resist += amount; + m_damage -= amount; + if (!m_damage) + { + m_hitMask |= PROC_HIT_FULL_RESIST; + m_hitMask &= ~(PROC_HIT_NORMAL | PROC_HIT_CRITICAL); + } +} + +void DamageInfo::BlockDamage(uint32 amount) +{ + amount = std::min(amount, GetDamage()); + m_block += amount; + m_damage -= amount; + m_hitMask |= PROC_HIT_BLOCK; + if (!m_damage) + { + m_hitMask |= PROC_HIT_FULL_BLOCK; + m_hitMask &= ~(PROC_HIT_NORMAL | PROC_HIT_CRITICAL); + } +} + +ProcFlagsHit DamageInfo::GetHitMask() const +{ + return m_hitMask; +} + +HealInfo::HealInfo(Unit* healer, Unit* target, uint32 heal, SpellInfo const* spellInfo, SpellSchoolMask schoolMask) + : _healer(healer), _target(target), _heal(heal), _effectiveHeal(0), _absorb(0), _spellInfo(spellInfo), _schoolMask(schoolMask), _hitMask(0) +{ +} + +void HealInfo::AbsorbHeal(uint32 amount) +{ + amount = std::min(amount, GetHeal()); + _absorb += amount; + _heal -= amount; + amount = std::min(amount, GetEffectiveHeal()); + _effectiveHeal -= amount; + _hitMask |= PROC_HIT_ABSORB; +} + +uint32 HealInfo::GetHitMask() const +{ + return _hitMask; +} + +ProcEventInfo::ProcEventInfo(Unit* actor, Unit* actionTarget, Unit* procTarget, + ProcFlags typeMask, ProcFlagsSpellType spellTypeMask, + ProcFlagsSpellPhase spellPhaseMask, ProcFlagsHit hitMask, + Spell* spell, DamageInfo* damageInfo, + HealInfo* healInfo) : + _actor(actor), _actionTarget(actionTarget), _procTarget(procTarget), + _typeMask(typeMask), _spellTypeMask(spellTypeMask), + _spellPhaseMask(spellPhaseMask), _hitMask(hitMask), _spell(spell), + _damageInfo(damageInfo), _healInfo(healInfo) +{ } + +SpellInfo const* ProcEventInfo::GetSpellInfo() const +{ + if (_spell) + return _spell->GetSpellInfo(); + if (_damageInfo) + return _damageInfo->GetSpellInfo(); + if (_healInfo) + return _healInfo->GetSpellInfo(); + return nullptr; +} + +SpellSchoolMask ProcEventInfo::GetSchoolMask() const +{ + if (_spell) + return _spell->GetSpellInfo()->GetSchoolMask(); + if (_damageInfo) + return _damageInfo->GetSchoolMask(); + if (_healInfo) + return _healInfo->GetSchoolMask(); + return SPELL_SCHOOL_MASK_NONE; +} + +DispelableAura::DispelableAura(Aura* aura, int32 dispelChance, uint8 dispelCharges) : + _aura(aura), _chance(dispelChance), _charges(dispelCharges) +{ +} + +DispelableAura::~DispelableAura() = default; + +bool DispelableAura::RollDispel() const +{ + return roll_chance_i(_chance); +} + +Unit::Unit(bool isWorldObject) : + WorldObject(isWorldObject), m_lastSanctuaryTime(0), + LastCharmerGUID(), m_ControlledByPlayer(false), movespline(std::make_unique()), + m_procDeep(0), m_procChainLength(0), m_removedAurasCount(0), + m_interruptMask(SpellAuraInterruptFlags::None), m_interruptMask2(SpellAuraInterruptFlags2::None), + m_charmer(nullptr), m_charmed(nullptr), + i_motionMaster(std::make_unique(this)), m_vehicle(nullptr), + m_unitTypeMask(UNIT_MASK_NONE), m_isEngaged(false), m_combatManager(this), m_threatManager(this), + i_AI(nullptr), m_aiLocked(false), m_spellHistory(std::make_unique(this)), + _stats(std::make_unique(this)) +{ + m_objectType |= TYPEMASK_UNIT; + m_objectTypeId = TYPEID_UNIT; + + m_updateFlag.MovementUpdate = true; + + m_baseAttackSpeed = { }; + m_attackTimer = { }; + m_modAttackSpeedPct.fill(1.0f); + + m_extraAttacks = 0; + m_canDualWield = false; + + m_movementCounter = 0; + + m_state = 0; + m_deathState = ALIVE; + + m_currentSpells.fill(nullptr); + + m_auraUpdateIterator = m_ownedAuras.end(); + + m_transform = 0; + m_canModifyStats = false; + + for (uint8 i = 0; i < UNIT_MOD_END; ++i) + { + m_auraFlatModifiersGroup[i][BASE_VALUE] = 0.0f; + m_auraFlatModifiersGroup[i][TOTAL_VALUE] = 0.0f; + m_auraPctModifiersGroup[i][BASE_PCT] = 1.0f; + m_auraPctModifiersGroup[i][TOTAL_PCT] = 1.0f; + } + // implement 50% base damage from offhand + m_auraPctModifiersGroup[UNIT_MOD_DAMAGE_OFFHAND][TOTAL_PCT] = 0.5f; + + for (uint8 i = 0; i < MAX_ATTACK; ++i) + { + m_weaponDamage[i][MINDAMAGE] = BASE_MINDAMAGE; + m_weaponDamage[i][MAXDAMAGE] = BASE_MAXDAMAGE; + } + + m_attacking = nullptr; + m_modMeleeHitChance = 0.0f; + m_modRangedHitChance = 0.0f; + m_modSpellHitChance = 0.0f; + m_baseSpellCritChance = 5; + + m_speed_rate.fill(1.0f); + _gameClientMovingMe = nullptr; + + // remove aurastates allowing special moves + m_reactiveTimer = { }; + m_cleanupDone = false; + m_duringRemoveFromWorld = false; + + m_serverSideVisibility.SetValue(SERVERSIDE_VISIBILITY_GHOST, GHOST_VISIBILITY_ALIVE); + + _lastLiquid = nullptr; + + _powerBarId = 0; + + _powerUpdateTimer = GetPowerUpdateInterval(); + _healthRegenerationTimer = UNIT_HEALTH_REGENERATION_INTERVAL; + + _powerFraction.fill(0.0f); + _usedPowerTypes.fill(MAX_POWERS); + + _oldFactionId = 0; + _isWalkingBeforeCharm = false; +} + +//////////////////////////////////////////////////////////// +// Methods of class Unit +Unit::~Unit() +{ + // set current spells as deletable + for (size_t i = 0; i < m_currentSpells.size(); ++i) + { + if (m_currentSpells[i]) + { + m_currentSpells[i]->SetReferencedFromCurrent(false); + m_currentSpells[i] = nullptr; + } + } + + m_Events.KillAllEvents(true); + + _DeleteRemovedAuras(); + + ASSERT(!m_duringRemoveFromWorld); + ASSERT(!m_attacking); + ASSERT(m_attackers.empty()); + ASSERT(m_sharedVision.empty()); + ASSERT(m_Controlled.empty()); + ASSERT(m_appliedAuras.empty()); + ASSERT(m_ownedAuras.empty()); + ASSERT(m_removedAuras.empty()); + ASSERT(m_gameObj.empty()); + ASSERT(m_dynObj.empty()); + ASSERT(!_gameClientMovingMe || _gameClientMovingMe->GetBasePlayer() == this); +} + +void Unit::Update(uint32 p_time) +{ + // WARNING! Order of execution here is important, do not change. + // Spells must be processed with event system BEFORE they go to _UpdateSpells. + // Or else we may have some SPELL_STATE_FINISHED spells stalled in pointers, that is bad. + WorldObject::Update(p_time); + + CheckPendingMovementAcks(); + + if (!IsInWorld()) + return; + + // Update server controlled facing for standing units + if (movespline->Finalized() && !isMoving()) + { + // Prioritize tracking channeled spells above unit targets if they should both be present at the same time. + Spell const* channeledSpell = GetCurrentSpell(CURRENT_CHANNELED_SPELL); + if (channeledSpell && channeledSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR1_TRACK_TARGET_IN_CHANNEL)) + { + ObjectGuid const channelGuid = GetChannelObjectGuid(); + if (!channelGuid.IsEmpty() && channelGuid != GetGUID()) + if (WorldObject const* objectTarget = ObjectAccessor::GetWorldObject(*this, channelGuid)) + SetOrientationTowards(objectTarget); + } + else if (IsCreature() && !HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED)) + { + ObjectGuid const targetGuid = GetTarget(); + if (!targetGuid.IsEmpty() && targetGuid != GetGUID()) + if (WorldObject const* objectTarget = ObjectAccessor::GetWorldObject(*this, targetGuid)) + SetOrientationTowards(objectTarget); + } + } + + _UpdateSpells(p_time); + ProcessPendingSpellCastRequest(); + + // If this is set during update SetCantProc(false) call is missing somewhere in the code + // Having this would prevent spells from being proced, so let's crash + ASSERT(!m_procDeep); + + m_combatManager.Update(p_time); + + auto spellPausesCombatTimer = [&](CurrentSpellTypes type) + { + return GetCurrentSpell(type) && GetCurrentSpell(type)->GetSpellInfo()->HasAttribute(SPELL_ATTR6_DELAY_COMBAT_TIMER_DURING_CAST); + }; + + if (!spellPausesCombatTimer(CURRENT_GENERIC_SPELL) && !spellPausesCombatTimer(CURRENT_CHANNELED_SPELL)) + { + if (uint32 base_att = getAttackTimer(BASE_ATTACK)) + setAttackTimer(BASE_ATTACK, (p_time >= base_att ? 0 : base_att - p_time)); + if (uint32 ranged_att = getAttackTimer(RANGED_ATTACK)) + setAttackTimer(RANGED_ATTACK, (p_time >= ranged_att ? 0 : ranged_att - p_time)); + if (uint32 off_att = getAttackTimer(OFF_ATTACK)) + setAttackTimer(OFF_ATTACK, (p_time >= off_att ? 0 : off_att - p_time)); + } + + // update abilities available only for fraction of time + UpdateReactives(p_time); + + if (IsAlive()) + { + ModifyAuraState(AURA_STATE_HEALTHLESS_20_PERCENT, HealthBelowPct(20)); + ModifyAuraState(AURA_STATE_HEALTHLESS_35_PERCENT, HealthBelowPct(35)); + ModifyAuraState(AURA_STATE_HEALTH_ABOVE_75_PERCENT, HealthAbovePct(75)); + } + + UpdateSplineMovement(p_time); + i_motionMaster->UpdateMotion(p_time); + + if (!i_AI && (GetTypeId() != TYPEID_PLAYER || IsCharmed())) + UpdateCharmAI(); + + // Wait with the aura interrupts until we have updated our movement generators and position6 + if (IsPlayer()) + InterruptMovementBasedAuras(); + else if (!movespline->Finalized()) + InterruptMovementBasedAuras(); + + // All position info based actions have been executed, reset info + _positionUpdateInfo.Reset(); + + // Update Health Regeneration + _healthRegenerationTimer -= p_time; + if (_healthRegenerationTimer <= 0) + { + RegenerateHealth(); + _healthRegenerationTimer += UNIT_HEALTH_REGENERATION_INTERVAL; + } + + if (HasScheduledAIChange() && (GetTypeId() != TYPEID_PLAYER || (IsCharmed() && GetCharmerGUID().IsCreature()))) + UpdateCharmAI(); + RefreshAI(); +} + +void Unit::Heartbeat() +{ + WorldObject::Heartbeat(); + + // SMSG_FLIGHT_SPLINE_SYNC for cyclic splines + SendFlightSplineSyncUpdate(); + + // Trigger heartbeat procs and generic aura behavior such as food emotes and invoking aura script hooks + TriggerAuraHeartbeat(); +} + +void Unit::TriggerAuraHeartbeat() +{ + for (auto const& [_, auraApplication] : m_appliedAuras) + auraApplication->GetBase()->Heartbeat(); + + Unit::ProcSkillsAndAuras(this, nullptr, PROC_FLAG_HEARTBEAT, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); +} + +bool Unit::haveOffhandWeapon() const +{ + if (Player const* player = ToPlayer()) + return player->GetWeaponForAttack(OFF_ATTACK, true) != nullptr; + + return CanDualWield(); +} + +void Unit::MonsterMoveWithSpeed(float x, float y, float z, float speed, bool generatePath, bool forceDestination) +{ + Movement::MoveSplineInit init(this); + init.MoveTo(x, y, z, generatePath, forceDestination); + init.SetVelocity(speed); + GetMotionMaster()->LaunchMoveSpline(std::move(init), 0, MOTION_SLOT_ACTIVE, POINT_MOTION_TYPE); +} + +void Unit::UpdateSplineMovement(uint32 t_diff) +{ + if (movespline->Finalized()) + return; + + movespline->updateState(t_diff); + bool arrived = movespline->Finalized(); + + if (arrived) + { + DisableSpline(); + if (Optional animTier = movespline->GetAnimation()) + SetAnimTier(*animTier); + } + + UpdateSplinePosition(); +} + +void Unit::UpdateSplinePosition() +{ + Movement::Location loc = movespline->ComputePosition(); + + if (movespline->onTransport) + { + Position& pos = m_movementInfo.transport.pos; + pos.m_positionX = loc.x; + pos.m_positionY = loc.y; + pos.m_positionZ = loc.z; + pos.SetOrientation(loc.orientation); + + if (TransportBase* transport = GetDirectTransport()) + transport->CalculatePassengerPosition(loc.x, loc.y, loc.z, &loc.orientation); + else + return; + } + + if (HasUnitState(UNIT_STATE_CANNOT_TURN)) + loc.orientation = GetOrientation(); + + UpdatePosition(loc.x, loc.y, loc.z, loc.orientation); +} + +void Unit::SendFlightSplineSyncUpdate() +{ + if (!movespline->isCyclic() || movespline->Finalized()) + return; + + WorldPackets::Movement::FlightSplineSync flightSplineSync; + flightSplineSync.Guid = GetGUID(); + flightSplineSync.SplineDist = float(movespline->timePassed()) / movespline->Duration(); + SendMessageToSet(flightSplineSync.Write(), true); +} + +void Unit::InterruptMovementBasedAuras() +{ + // TODO: Check if orientation transport offset changed instead of only global orientation + if (_positionUpdateInfo.Turned) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Turning); + + if (_positionUpdateInfo.Relocated && !GetVehicle()) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Moving); +} + +void Unit::DisableSpline() +{ + m_movementInfo.RemoveMovementFlag(MOVEMENTFLAG_FORWARD); + movespline->_Interrupt(); +} + +void Unit::resetAttackTimer(WeaponAttackType type) +{ + m_attackTimer[type] = uint32(GetBaseAttackTime(type) * m_modAttackSpeedPct[type]); +} + +bool Unit::IsWithinCombatRange(Unit const* obj, float dist2compare) const +{ + if (!obj || !IsInMap(obj) || !IsInPhase(obj)) + return false; + + float dx = GetPositionX() - obj->GetPositionX(); + float dy = GetPositionY() - obj->GetPositionY(); + float dz = GetPositionZ() - obj->GetPositionZ(); + float distsq = dx * dx + dy * dy + dz * dz; + + float sizefactor = GetCombatReach() + obj->GetCombatReach(); + float maxdist = dist2compare + sizefactor; + + return distsq < maxdist * maxdist; +} + +bool Unit::IsWithinMeleeRangeAt(Position const& pos, Unit const* obj) const +{ + if (!obj || !IsInMap(obj) || !IsInPhase(obj)) + return false; + + float dx = pos.GetPositionX() - obj->GetPositionX(); + float dy = pos.GetPositionY() - obj->GetPositionY(); + float dz = GetPositionZ() - obj->GetPositionZ(); + + float distsq = dx*dx + dy*dy + dz*dz; + + float maxdist = GetMeleeRange(obj); + + return distsq <= maxdist * maxdist; +} + +float Unit::GetMeleeRange(Unit const* target) const +{ + float range = GetCombatReach() + target->GetCombatReach() + 1.3333334f; + return std::max(range, NOMINAL_MELEE_RANGE); +} + +bool Unit::IsWithinBoundaryRadius(const Unit* obj) const +{ + if (!obj || !IsInMap(obj) || !IsInPhase(obj)) + return false; + + float objBoundaryRadius = std::max(obj->GetBoundaryRadius(), MIN_MELEE_REACH); + + return IsInDist(obj, objBoundaryRadius); +} + +AuraApplication* Unit::GetVisibleAura(uint8 slot) const +{ + VisibleAuraMap::const_iterator itr = m_visibleAuras.find(slot); + if (itr != m_visibleAuras.end()) + return itr->second; + return 0; +} + +void Unit::SetVisibleAura(uint8 slot, AuraApplication * aur) +{ + m_visibleAuras[slot]=aur; + UpdateAuraForGroup(slot); +} + +void Unit::RemoveVisibleAura(uint8 slot) +{ + m_visibleAuras.erase(slot); + UpdateAuraForGroup(slot); +} + +void Unit::UpdateInterruptMask() +{ + m_interruptMask = SpellAuraInterruptFlags::None; + m_interruptMask2 = SpellAuraInterruptFlags2::None; + for (AuraApplication const* aurApp : m_interruptableAuras) + { + m_interruptMask |= aurApp->GetBase()->GetSpellInfo()->AuraInterruptFlags; + m_interruptMask2 |= aurApp->GetBase()->GetSpellInfo()->AuraInterruptFlags2; + } + + if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL]) + { + if (spell->getState() == SPELL_STATE_CHANNELING) + { + m_interruptMask |= spell->m_spellInfo->ChannelInterruptFlags; + m_interruptMask2 |= spell->m_spellInfo->ChannelInterruptFlags2; + } + } +} + +bool Unit::HasAuraTypeWithFamilyFlags(AuraType auraType, uint32 familyName, uint32 familyFlags) const +{ + if (!HasAuraType(auraType)) + return false; + AuraEffectList const& auras = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator itr = auras.begin(); itr != auras.end(); ++itr) + if (SpellInfo const* iterSpellProto = (*itr)->GetSpellInfo()) + if (iterSpellProto->SpellFamilyName == familyName && iterSpellProto->SpellFamilyFlags[0] & familyFlags) + return true; + return false; +} + +bool Unit::HasBreakableByDamageAuraType(AuraType type, uint32 excludeAura) const +{ + AuraEffectList const& auras = GetAuraEffectsByType(type); + for (AuraEffectList::const_iterator itr = auras.begin(); itr != auras.end(); ++itr) + if ((!excludeAura || excludeAura != (*itr)->GetSpellInfo()->Id) && //Avoid self interrupt of channeled Crowd Control spells like Seduction + (*itr)->GetSpellInfo()->HasAuraInterruptFlag(SpellAuraInterruptFlags::Damage)) + return true; + return false; +} + +bool Unit::HasBreakableByDamageCrowdControlAura(Unit* excludeCasterChannel) const +{ + uint32 excludeAura = 0; + if (Spell* currentChanneledSpell = excludeCasterChannel ? excludeCasterChannel->GetCurrentSpell(CURRENT_CHANNELED_SPELL) : nullptr) + excludeAura = currentChanneledSpell->GetSpellInfo()->Id; //Avoid self interrupt of channeled Crowd Control spells like Seduction + + return ( HasBreakableByDamageAuraType(SPELL_AURA_MOD_CONFUSE, excludeAura) + || HasBreakableByDamageAuraType(SPELL_AURA_MOD_FEAR, excludeAura) + || HasBreakableByDamageAuraType(SPELL_AURA_MOD_STUN, excludeAura) + || HasBreakableByDamageAuraType(SPELL_AURA_MOD_ROOT, excludeAura) + || HasBreakableByDamageAuraType(SPELL_AURA_TRANSFORM, excludeAura)); +} + +/*static*/ void Unit::DealDamageMods(Unit const* victim, uint32& damage, uint32* absorb) +{ + if (!victim || !victim->IsAlive() || victim->HasUnitState(UNIT_STATE_IN_FLIGHT) || (victim->GetTypeId() == TYPEID_UNIT && victim->ToCreature()->IsEvadingAttacks())) + { + if (absorb) + *absorb += damage; + damage = 0; + } +} + +/*static*/ uint32 Unit::DealDamage(Unit* attacker, Unit* victim, uint32 damage, uint32 unmitigatedDamage, DamageEffectType damagetype, SpellSchoolMask damageSchoolMask, SpellInfo const* spellProto, bool durabilityLoss) +{ + // Sparring Checks + if (Creature* target = victim->ToCreature()) + { + if (attacker && attacker->IsCreature() && !attacker->IsCharmedOwnedByPlayerOrPlayer()) + { + if (target->GetNoNpcDamageBelowPctHealthValue() != 0.0f) + { + if (damage >= target->GetHealth()) // First check: if we have a sparring limit we will never allow creatures to kill the sparring victim + damage = target->GetHealth() - 1; + else if (target->GetHealthPct() <= target->GetNoNpcDamageBelowPctHealthValue()) // Second check: stop incomming damage when we have surpassed the health limit + damage = 0; + } + } + } + + if (UnitAI* victimAI = victim->GetAI()) + victimAI->DamageTaken(attacker, damage); + + if (UnitAI* attackerAI = attacker ? attacker->GetAI() : nullptr) + attackerAI->DamageDealt(victim, damage, damagetype); + + // Hook for OnDamage Event + sScriptMgr->OnDamage(attacker, victim, damage); + + if (victim->GetTypeId() == TYPEID_PLAYER) + { + // Signal to pets that their owner was attacked - except when DOT. + if (attacker != victim && damagetype != DOT) + { + for (Unit* controlled : victim->m_Controlled) + if (Creature* cControlled = controlled->ToCreature()) + if (CreatureAI* controlledAI = cControlled->AI()) + controlledAI->OwnerAttackedBy(attacker); + } + + if (victim->ToPlayer()->GetCommandStatus(CHEAT_GOD)) + return 0; + } + + if (damagetype != NODAMAGE) + { + // interrupting auras with SpellAuraInterruptFlags::Damage before checking !damage (absorbed damage breaks that type of auras) + if (spellProto) + { + if (!spellProto->HasAttribute(SPELL_ATTR4_REACTIVE_DAMAGE_PROC)) + victim->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Damage, spellProto); + } + else + victim->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Damage); + + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + AuraEffectList vCopyDamageCopy(victim->GetAuraEffectsByType(SPELL_AURA_SHARE_DAMAGE_PCT)); + // copy damage to casters of this aura + for (AuraEffectList::iterator i = vCopyDamageCopy.begin(); i != vCopyDamageCopy.end(); ++i) + { + // Check if aura was removed during iteration - we don't need to work on such auras + if (!((*i)->GetBase()->IsAppliedOnTarget(victim->GetGUID()))) + continue; + // check damage school mask + if (((*i)->GetMiscValue() & damageSchoolMask) == 0) + continue; + + Unit* shareDamageTarget = (*i)->GetCaster(); + if (!shareDamageTarget) + continue; + SpellInfo const* spell = (*i)->GetSpellInfo(); + + uint32 share = CalculatePct(damage, (*i)->GetAmount()); + + /// @todo check packets if damage is done by victim, or by attacker of victim + Unit::DealDamageMods(shareDamageTarget, share, nullptr); + Unit::DealDamage(attacker, shareDamageTarget, share, 0, NODAMAGE, spell->GetSchoolMask(), spell, false); + } + } + + uint32 health = victim->GetHealth(); + + // duel ends when player has 1 or less hp + bool duel_hasEnded = false; + bool duel_wasMounted = false; + if (victim->GetTypeId() == TYPEID_PLAYER && victim->ToPlayer()->duel && damage >= (health-1)) + { + if (!attacker) + return 0; + + // prevent kill only if killed in duel and killed by opponent or opponent controlled creature + if (victim->ToPlayer()->duel->opponent == attacker->GetControllingPlayer()) + damage = health - 1; + + duel_hasEnded = true; + } + else if (victim->IsCreature() && victim != attacker && damage >= health && victim->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_UNKILLABLE)) + { + damage = health - 1; + } + else if (victim->IsVehicle() && damage >= (health-1) && victim->GetCharmer() && victim->GetCharmer()->GetTypeId() == TYPEID_PLAYER) + { + Player* victimRider = victim->GetCharmer()->ToPlayer(); + + if (victimRider && victimRider->duel && victimRider->duel->isMounted) + { + if (!attacker) + return 0; + + // prevent kill only if killed in duel and killed by opponent or opponent controlled creature + if (victimRider->duel->opponent == attacker->GetControllingPlayer()) + damage = health - 1; + + duel_wasMounted = true; + duel_hasEnded = true; + } + } + + if (attacker && attacker != victim) + { + if (Player* killer = attacker->ToPlayer()) + { + // in bg, count dmg if victim is also a player + if (victim->GetTypeId() == TYPEID_PLAYER && !(spellProto && spellProto->HasAttribute(SPELL_ATTR7_DO_NOT_COUNT_FOR_PVP_SCOREBOARD))) + if (Battleground* bg = killer->GetBattleground()) + bg->UpdatePlayerScore(killer, SCORE_DAMAGE_DONE, damage); + + killer->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DAMAGE_DONE, health > damage ? damage : health, 0, 0, victim); + killer->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_HIT_DEALT, damage); + } + } + + if (victim->GetTypeId() == TYPEID_PLAYER) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_HIT_RECEIVED, damage); + else if (!victim->IsControlledByPlayer() || victim->IsVehicle()) + { + if (!victim->ToCreature()->hasLootRecipient()) + victim->ToCreature()->SetLootRecipient(attacker); + + if (!attacker || attacker->IsControlledByPlayer() || (attacker->ToTempSummon() && attacker->ToTempSummon()->GetSummoner() && attacker->ToTempSummon()->GetSummoner()->GetTypeId() == TYPEID_PLAYER)) + victim->ToCreature()->LowerPlayerDamageReq(health < damage ? health : damage); + } + + if (health <= damage) + { + if (victim->GetTypeId() == TYPEID_PLAYER && victim != attacker) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_TOTAL_DAMAGE_RECEIVED, health); + + Unit::Kill(attacker, victim, durabilityLoss); + } + else + { + if (victim->GetTypeId() == TYPEID_PLAYER) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_TOTAL_DAMAGE_RECEIVED, damage); + + victim->ModifyHealth(-(int32)damage); + + if (damagetype == DIRECT_DAMAGE || damagetype == SPELL_DIRECT_DAMAGE) + victim->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::NonPeriodicDamage, spellProto); + + if (victim->GetTypeId() != TYPEID_PLAYER) + { + // Part of Evade mechanics. DoT's and Thorns / Retribution Aura do not contribute to this + if (damagetype != DOT && damage > 0 && !victim->GetOwnerGUID().IsPlayer() && (!spellProto || !spellProto->HasAura(SPELL_AURA_DAMAGE_SHIELD))) + victim->ToCreature()->SetLastDamagedTime(GameTime::GetGameTime() + MAX_AGGRO_RESET_TIME); + + if (attacker && (!spellProto || !spellProto->HasAttribute(SPELL_ATTR4_NO_HARMFUL_THREAT))) + victim->GetThreatManager().AddThreat(attacker, float(damage), spellProto); + } + else // victim is a player + { + // random durability for items (HIT TAKEN) + if (roll_chance_f(sWorld->getRate(RATE_DURABILITY_LOSS_DAMAGE))) + { + EquipmentSlots slot = EquipmentSlots(urand(0, EQUIPMENT_SLOT_END-1)); + victim->ToPlayer()->DurabilityPointLossForEquipSlot(slot); + } + } + + // Rage from damage received + if (attacker != victim && victim->GetPowerType() == POWER_RAGE) + { + uint32 rageBaseReward = CalcDamageTakenRageGain(victim, std::max(damage, unmitigatedDamage)); + victim->RewardRage(rageBaseReward, false); + } + + if (attacker && attacker->GetTypeId() == TYPEID_PLAYER) + { + // random durability for items (HIT DONE) + if (roll_chance_f(sWorld->getRate(RATE_DURABILITY_LOSS_DAMAGE))) + { + EquipmentSlots slot = EquipmentSlots(urand(0, EQUIPMENT_SLOT_END-1)); + attacker->ToPlayer()->DurabilityPointLossForEquipSlot(slot); + } + } + + if (damagetype != NODAMAGE && damagetype != DOT) + { + if (victim != attacker && (!spellProto || !(spellProto->HasAttribute(SPELL_ATTR6_NO_PUSHBACK) || spellProto->HasAttribute(SPELL_ATTR7_DONT_CAUSE_SPELL_PUSHBACK) || spellProto->HasAttribute(SPELL_ATTR3_TREAT_AS_PERIODIC)))) + { + if (Spell* spell = victim->m_currentSpells[CURRENT_GENERIC_SPELL]) + { + if (spell->getState() == SPELL_STATE_PREPARING) + { + auto isCastInterrupted = [&]() + { + if ((victim->IsPlayer() && spell->m_spellInfo->InterruptFlags.HasFlag(SpellInterruptFlags::DamageCancelsPlayerOnly))) + return true; + + return false; + }; + + auto isCastDelayed = [&]() + { + if (!damage) + return false; + + if ((victim->IsPlayer() && spell->m_spellInfo->InterruptFlags.HasFlag(SpellInterruptFlags::DamagePushbackPlayerOnly))) + return true; + + return false; + }; + + if (isCastInterrupted()) + victim->InterruptNonMeleeSpells(false); + else if (isCastDelayed()) + spell->Delayed(); + } + } + + if (damage && victim->IsPlayer()) + if (Spell* spell = victim->m_currentSpells[CURRENT_CHANNELED_SPELL]) + if (spell->getState() == SPELL_STATE_CHANNELING && spell->m_spellInfo->HasChannelInterruptFlag(SpellAuraInterruptFlags::DamageChannelDuration)) + spell->DelayedChannel(); + } + } + + // last damage from duel opponent + if (duel_hasEnded) + { + Player* he = duel_wasMounted ? victim->GetCharmer()->ToPlayer() : victim->ToPlayer(); + + ASSERT(he && he->duel); + + if (duel_wasMounted) // In this case victim == mount + victim->SetHealth(1); + else + he->SetHealth(1); + + he->duel->opponent->CombatStopWithPets(true); + he->CombatStopWithPets(true); + + he->CastSpell(he, 7267, true); // beg + he->DuelComplete(DUEL_WON); + } + } + + return damage; +} + +void Unit::CastStop(uint32 except_spellid) +{ + for (uint32 i = CURRENT_FIRST_NON_MELEE_SPELL; i < CURRENT_MAX_SPELL; i++) + if (m_currentSpells[i] && m_currentSpells[i]->m_spellInfo->Id != except_spellid) + InterruptSpell(CurrentSpellTypes(i), false); +} + +void Unit::CalculateSpellDamageTaken(SpellNonMeleeDamage* damageInfo, int32 damage, SpellInfo const* spellInfo, WeaponAttackType attackType, bool crit, bool blocked /*= false*/) +{ + if (damage < 0) + return; + + Unit* victim = damageInfo->target; + if (!victim || !victim->IsAlive()) + return; + + SpellSchoolMask damageSchoolMask = SpellSchoolMask(damageInfo->schoolMask); + + // Spells with SPELL_ATTR4_IGNORE_DAMAGE_TAKEN_MODIFIERS ignore resilience because their damage is based off another spell's damage. + if (!spellInfo->HasAttribute(SPELL_ATTR4_IGNORE_DAMAGE_TAKEN_MODIFIERS)) + { + if (Unit::IsDamageReducedByArmor(damageSchoolMask, spellInfo)) + damage = Unit::CalcArmorReducedDamage(this, victim, damage, spellInfo, attackType); + + // Per-school calc + switch (spellInfo->DmgClass) + { + // Melee and Ranged Spells + case SPELL_DAMAGE_CLASS_RANGED: + case SPELL_DAMAGE_CLASS_MELEE: + { + if (crit) + { + damageInfo->HitInfo |= SPELL_HIT_TYPE_CRIT; + + // Calculate crit bonus + uint32 crit_bonus = damage; + // Apply crit_damage bonus for melee spells + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellInfo, SpellModOp::CritDamageAndHealing, crit_bonus); + damage += crit_bonus; + + // Apply SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_DAMAGE or SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_DAMAGE + float critPctDamageMod = 0.0f; + if (attackType == RANGED_ATTACK) + critPctDamageMod += victim->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_DAMAGE); + else + critPctDamageMod += victim->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_DAMAGE); + + // Increase crit damage from SPELL_AURA_MOD_CRIT_DAMAGE_BONUS + critPctDamageMod += (GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_CRIT_DAMAGE_BONUS, spellInfo->GetSchoolMask()) - 1.0f) * 100; + + if (critPctDamageMod != 0) + AddPct(damage, critPctDamageMod); + } + + // Spell weapon based damage CAN BE crit & blocked at same time + if (blocked) + { + // double blocked amount if block is critical + uint32 value = victim->GetBlockPercent(); + if (victim->IsBlockCritical()) + value *= 2; // double blocked percent + + damageInfo->blocked = CalculatePct(damage, value); + if (damage <= int32(damageInfo->blocked)) + { + damageInfo->blocked = uint32(damage); + damageInfo->fullBlock = true; + } + damage -= damageInfo->blocked; + } + + if (CanApplyResilience()) + Unit::ApplyResilience(victim, &damage); + break; + } + // Magical Attacks + case SPELL_DAMAGE_CLASS_NONE: + case SPELL_DAMAGE_CLASS_MAGIC: + { + // If crit add critical bonus + if (crit) + { + damageInfo->HitInfo |= SPELL_HIT_TYPE_CRIT; + damage = Unit::SpellCriticalDamageBonus(this, spellInfo, damage); + } + + if (CanApplyResilience()) + Unit::ApplyResilience(victim, &damage); + break; + + } + default: + break; + } + } + + // Script Hook For CalculateSpellDamageTaken -- Allow scripts to change the Damage post class mitigation calculations + sScriptMgr->ModifySpellDamageTaken(damageInfo->target, damageInfo->attacker, damage); + + // Calculate absorb resist + if (damage < 0) + damage = 0; + + damageInfo->damage = damage; + DamageInfo dmgInfo(*damageInfo, SPELL_DIRECT_DAMAGE, BASE_ATTACK, PROC_HIT_NONE); + Unit::CalcAbsorbResist(dmgInfo); + damageInfo->absorb = dmgInfo.GetAbsorb(); + damageInfo->resist = dmgInfo.GetResist(); + + if (damageInfo->absorb) + damageInfo->HitInfo |= (damageInfo->damage - damageInfo->absorb == 0 ? HITINFO_FULL_ABSORB : HITINFO_PARTIAL_ABSORB); + + if (damageInfo->resist) + damageInfo->HitInfo |= (damageInfo->damage - damageInfo->resist == 0 ? HITINFO_FULL_RESIST : HITINFO_PARTIAL_RESIST); + + damageInfo->damage = dmgInfo.GetDamage(); +} + +void Unit::DealSpellDamage(SpellNonMeleeDamage* damageInfo, bool durabilityLoss) +{ + if (!damageInfo) + return; + + Unit* victim = damageInfo->target; + if (!victim) + return; + + if (!victim->IsAlive() || victim->HasUnitState(UNIT_STATE_IN_FLIGHT) || (victim->GetTypeId() == TYPEID_UNIT && victim->ToCreature()->IsEvadingAttacks())) + return; + + SpellInfo const* spellProto = sSpellMgr->GetSpellInfo(damageInfo->SpellID); + if (spellProto == nullptr) + { + TC_LOG_DEBUG("entities.unit", "Unit::DealSpellDamage has wrong damageInfo->SpellID: %u", damageInfo->SpellID); + return; + } + + // Call default DealDamage + Unit::DealDamage(this, victim, damageInfo->damage, damageInfo->unmitigatedDamage, SPELL_DIRECT_DAMAGE, SpellSchoolMask(damageInfo->schoolMask), spellProto, durabilityLoss); +} + +/*static*/ uint32 Unit::CalcMeleeAttackRageGain(Unit const* attacker, Unit const* victim, WeaponAttackType attType) +{ + if (!attacker || !victim || (attType != BASE_ATTACK && attType != OFF_ATTACK)) + return 0; + + uint32 rage = uint32(attacker->GetBaseAttackTime(attType) / 1000.f * 6.5f); + if (attType == OFF_ATTACK) + rage /= 2; + + // Players below level 10 start with a 50% rage gain reduction that slowly diminishes + if (attacker->IsPlayer() && attacker->getLevel() < 10) + rage -= (rage / 2.f) * (1.0f - (attacker->getLevel() / 10.f)); + + rage *= 10; + + AddPct(rage, attacker->GetTotalAuraModifier(SPELL_AURA_MOD_RAGE_FROM_DAMAGE_DEALT)); + + return rage; +} + +// The minimum amount a damaging hit should award +constexpr uint32 MIN_RAGE_AMOUNT = 10; + +/*static*/ uint32 Unit::CalcDamageTakenRageGain(Unit const* victim, uint32 damage) +{ + if (!victim || damage == 0) + return 0; + + double multiplier = static_cast(damage) / victim->GetMaxHealth(); + + // This formula is based on sampling multiple sniff data at multiple level ranges + // There has never been any leak and the only info we have is that the amount is based on max HP and damage taken and that there is a hidden constant + return std::max(MIN_RAGE_AMOUNT, std::ceil(400 * multiplier)); +} + +/// @todo for melee need create structure as in +void Unit::CalculateMeleeDamage(Unit* victim, CalcDamageInfo* damageInfo, WeaponAttackType attackType /*= BASE_ATTACK*/) +{ + damageInfo->Attacker = this; + damageInfo->Target = victim; + + damageInfo->DamageSchoolMask = GetMeleeDamageSchoolMask(attackType); + damageInfo->Damage = 0; + damageInfo->OriginalDamage = 0; + damageInfo->Absorb = 0; + damageInfo->Resist = 0; + + damageInfo->Blocked = 0; + damageInfo->HitInfo = 0; + damageInfo->TargetState = 0; + + damageInfo->AttackType = attackType; + damageInfo->ProcAttacker = PROC_FLAG_NONE; + damageInfo->ProcVictim = PROC_FLAG_NONE; + damageInfo->UnmitigatedDamage = 0; + damageInfo->HitOutCome = MELEE_HIT_EVADE; + damageInfo->RageGained = 0; + + if (!victim) + return; + + if (!IsAlive() || !victim->IsAlive()) + return; + + // Select HitInfo/procAttacker/procVictim flag based on attack type + switch (attackType) + { + case BASE_ATTACK: + damageInfo->ProcAttacker = PROC_FLAG_DEAL_MELEE_SWING | PROC_FLAG_MAIN_HAND_WEAPON_SWING; + damageInfo->ProcVictim = PROC_FLAG_TAKE_MELEE_SWING; + break; + case OFF_ATTACK: + damageInfo->ProcAttacker = PROC_FLAG_DEAL_MELEE_SWING | PROC_FLAG_OFF_HAND_WEAPON_SWING; + damageInfo->ProcVictim = PROC_FLAG_TAKE_MELEE_SWING; + damageInfo->HitInfo = HITINFO_OFFHAND; + break; + default: + return; + } + + // Physical Immune check + if (damageInfo->Target->IsImmunedToDamage(SpellSchoolMask(damageInfo->DamageSchoolMask))) + { + damageInfo->HitInfo |= HITINFO_NORMALSWING; + damageInfo->TargetState = VICTIMSTATE_IS_IMMUNE; + + damageInfo->Damage = 0; + damageInfo->UnmitigatedDamage = 0; + return; + } + + uint32 damage = 0; + damage += CalculateDamage(damageInfo->AttackType, false, true); + // Add melee damage bonus + damage = MeleeDamageBonusDone(damageInfo->Target, damage, damageInfo->AttackType, DIRECT_DAMAGE, nullptr, MECHANIC_NONE, SpellSchoolMask(damageInfo->DamageSchoolMask)); + damage = damageInfo->Target->MeleeDamageBonusTaken(this, damage, damageInfo->AttackType, nullptr, SpellSchoolMask(damageInfo->DamageSchoolMask)); + + // Script Hook For CalculateMeleeDamage -- Allow scripts to change the Damage pre class mitigation calculations + sScriptMgr->ModifyMeleeDamage(damageInfo->Target, damageInfo->Attacker, damage); + + // Calculate armor reduction + if (Unit::IsDamageReducedByArmor((SpellSchoolMask)(damageInfo->DamageSchoolMask))) + damageInfo->Damage = Unit::CalcArmorReducedDamage(damageInfo->Attacker, damageInfo->Target, damage, nullptr, damageInfo->AttackType); + else + damageInfo->Damage = damage; + + // Store unmitigated damage to reward rage later + damageInfo->UnmitigatedDamage = damage; + + damageInfo->HitOutCome = RollMeleeOutcomeAgainst(damageInfo->Target, damageInfo->AttackType); + + switch (damageInfo->HitOutCome) + { + case MELEE_HIT_EVADE: + damageInfo->HitInfo |= HITINFO_MISS | HITINFO_SWINGNOHITSOUND; + damageInfo->TargetState = VICTIMSTATE_EVADES; + damageInfo->OriginalDamage = damageInfo->Damage; + + damageInfo->Damage = 0; + return; + case MELEE_HIT_MISS: + damageInfo->HitInfo |= HITINFO_MISS; + damageInfo->TargetState = VICTIMSTATE_INTACT; + damageInfo->OriginalDamage = damageInfo->Damage; + + damageInfo->Damage = 0; + damageInfo->UnmitigatedDamage = 0; + break; + case MELEE_HIT_NORMAL: + damageInfo->TargetState = VICTIMSTATE_HIT; + damageInfo->OriginalDamage = damageInfo->Damage; + break; + case MELEE_HIT_CRIT: + { + damageInfo->HitInfo |= HITINFO_CRITICALHIT; + damageInfo->TargetState = VICTIMSTATE_HIT; + + // Crit bonus calc + damageInfo->Damage *= 2; + float mod = 0.0f; + // Apply SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_DAMAGE or SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_DAMAGE + if (damageInfo->AttackType == RANGED_ATTACK) + mod += damageInfo->Target->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_DAMAGE); + else + mod += damageInfo->Target->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_DAMAGE); + + // Increase crit damage from SPELL_AURA_MOD_CRIT_DAMAGE_BONUS + mod += (GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_CRIT_DAMAGE_BONUS, damageInfo->DamageSchoolMask) - 1.0f) * 100; + + if (mod != 0) + AddPct(damageInfo->Damage, mod); + + damageInfo->OriginalDamage = damageInfo->Damage; + break; + } + case MELEE_HIT_PARRY: + damageInfo->TargetState = VICTIMSTATE_PARRY; + damageInfo->OriginalDamage = damageInfo->Damage; + damageInfo->Damage = 0; + break; + case MELEE_HIT_DODGE: + damageInfo->TargetState = VICTIMSTATE_DODGE; + damageInfo->OriginalDamage = damageInfo->Damage; + damageInfo->Damage = 0; + break; + case MELEE_HIT_BLOCK: + damageInfo->TargetState = VICTIMSTATE_HIT; + damageInfo->HitInfo |= HITINFO_BLOCK; + // 30% damage blocked, double blocked amount if block is critical + damageInfo->Blocked = CalculatePct(damageInfo->Damage, damageInfo->Target->GetBlockPercent()); + if (damageInfo->Target->IsBlockCritical()) + damageInfo->Blocked *= 2; + + damageInfo->OriginalDamage = damageInfo->Damage; + damageInfo->Damage -= damageInfo->Blocked; + break; + case MELEE_HIT_GLANCING: + { + damageInfo->HitInfo |= HITINFO_GLANCING; + damageInfo->TargetState = VICTIMSTATE_HIT; + int32 leveldif = int32(victim->getLevel()) - int32(getLevel()); + if (leveldif > 3) + leveldif = 3; + + damageInfo->OriginalDamage = damageInfo->Damage; + float reducePercent = 1.f - leveldif * 0.1f; + damageInfo->Damage = uint32(reducePercent * damageInfo->Damage); + break; + } + case MELEE_HIT_CRUSHING: + damageInfo->HitInfo |= HITINFO_CRUSHING; + damageInfo->TargetState = VICTIMSTATE_HIT; + // 150% normal damage + damageInfo->Damage += (damageInfo->Damage / 2); + damageInfo->OriginalDamage = damageInfo->Damage; + break; + default: + break; + } + + // Always apply HITINFO_AFFECTS_VICTIM in case its not a miss + if (!(damageInfo->HitInfo & HITINFO_MISS)) + damageInfo->HitInfo |= HITINFO_AFFECTS_VICTIM; + + int32 resilienceReduction = damageInfo->Damage; + if (CanApplyResilience()) + Unit::ApplyResilience(victim, &resilienceReduction); + resilienceReduction = damageInfo->Damage - resilienceReduction; + damageInfo->Damage -= resilienceReduction; + + // Calculate absorb resist + if (int32(damageInfo->Damage) > 0) + { + damageInfo->ProcVictim |= PROC_FLAG_TAKE_ANY_DAMAGE; + // Calculate absorb & resists + DamageInfo dmgInfo(*damageInfo); + Unit::CalcAbsorbResist(dmgInfo); + damageInfo->Absorb = dmgInfo.GetAbsorb(); + damageInfo->Resist = dmgInfo.GetResist(); + + if (damageInfo->Absorb) + damageInfo->HitInfo |= (damageInfo->Damage - damageInfo->Absorb == 0 ? HITINFO_FULL_ABSORB : HITINFO_PARTIAL_ABSORB); + + if (damageInfo->Resist) + damageInfo->HitInfo |= (damageInfo->Damage - damageInfo->Resist == 0 ? HITINFO_FULL_RESIST : HITINFO_PARTIAL_RESIST); + + damageInfo->Damage = dmgInfo.GetDamage(); + } + else // Impossible get negative result but.... + damageInfo->Damage = 0; +} + +void Unit::DealMeleeDamage(CalcDamageInfo* damageInfo, bool durabilityLoss) +{ + Unit* victim = damageInfo->Target; + + if (!victim->IsAlive() || victim->HasUnitState(UNIT_STATE_IN_FLIGHT) || (victim->GetTypeId() == TYPEID_UNIT && victim->ToCreature()->IsEvadingAttacks())) + return; + + if (damageInfo->TargetState == VICTIMSTATE_PARRY && + (GetTypeId() != TYPEID_UNIT || (ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_PARRY_HASTEN) == 0)) + { + // Get attack timers + float offtime = float(victim->getAttackTimer(OFF_ATTACK)); + float basetime = float(victim->getAttackTimer(BASE_ATTACK)); + // Reduce attack time + if (victim->haveOffhandWeapon() && offtime < basetime) + { + float percent20 = victim->GetBaseAttackTime(OFF_ATTACK) * 0.20f; + float percent60 = 3.0f * percent20; + if (offtime > percent20 && offtime <= percent60) + victim->setAttackTimer(OFF_ATTACK, uint32(percent20)); + else if (offtime > percent60) + { + offtime -= 2.0f * percent20; + victim->setAttackTimer(OFF_ATTACK, uint32(offtime)); + } + } + else + { + float percent20 = victim->GetBaseAttackTime(BASE_ATTACK) * 0.20f; + float percent60 = 3.0f * percent20; + if (basetime > percent20 && basetime <= percent60) + victim->setAttackTimer(BASE_ATTACK, uint32(percent20)); + else if (basetime > percent60) + { + basetime -= 2.0f * percent20; + victim->setAttackTimer(BASE_ATTACK, uint32(basetime)); + } + } + } + + // Call default DealDamage + Unit::DealDamage(this, victim, damageInfo->Damage, damageInfo->UnmitigatedDamage, DIRECT_DAMAGE, SpellSchoolMask(damageInfo->DamageSchoolMask), nullptr, durabilityLoss); + + // If this is a creature and it attacks from behind it has a probability to daze it's victim + if ((damageInfo->HitOutCome == MELEE_HIT_CRIT || damageInfo->HitOutCome == MELEE_HIT_CRUSHING || damageInfo->HitOutCome == MELEE_HIT_NORMAL || damageInfo->HitOutCome == MELEE_HIT_GLANCING) && + IsCreature() && !ToCreature()->IsControlledByPlayer() && !ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_CANNOT_DAZE) && + !victim->HasInArc(float(M_PI), this) && (victim->IsPlayer() || (victim->IsCreature() && !victim->ToCreature()->isWorldBoss())) && !victim->IsVehicle()) + { + // 20% base chance + float chance = 20.0f; + + // there is a newbie protection, at level 10 just 7% base chance; assuming linear function + if (victim->getLevel() < 30) + chance = 0.65f * victim->getLevel() + 0.5f; + + uint32 const victimDefense = victim->GetMaxSkillValueForLevel(this); + uint32 const attackerMeleeSkill = GetMaxSkillValueForLevel(); + + chance *= attackerMeleeSkill / float(victimDefense) * 0.16f; + + // -probability is between 0% and 40% + RoundToInterval(chance, 0.0f, 40.0f); + if (roll_chance_f(chance)) + CastSpell(victim, 1604 /*SPELL_DAZED*/, true); + } + + if (GetTypeId() == TYPEID_PLAYER) + { + DamageInfo dmgInfo(*damageInfo); + ToPlayer()->CastItemCombatSpell(dmgInfo); + } + + // Do effect if any damage done to target + if (damageInfo->Damage) + { + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + AuraEffectList vDamageShieldsCopy(victim->GetAuraEffectsByType(SPELL_AURA_DAMAGE_SHIELD)); + for (AuraEffect const* aurEff : vDamageShieldsCopy) + { + SpellInfo const* spellInfo = aurEff->GetSpellInfo(); + + // Damage shield can be resisted... + SpellMissInfo missInfo = victim->SpellHitResult(this, spellInfo, false); + if (missInfo != SPELL_MISS_NONE) + { + victim->SendSpellMiss(this, spellInfo->Id, missInfo); + continue; + } + + // ...or immuned + if (IsImmunedToDamage(spellInfo)) + { + victim->SendSpellDamageImmune(this, spellInfo->Id); + continue; + } + + uint32 damage = aurEff->GetAmount(); + if (Unit* caster = aurEff->GetCaster()) + { + damage = caster->SpellDamageBonusDone(this, spellInfo, damage, SPELL_DIRECT_DAMAGE, aurEff->GetEffIndex()); + damage = SpellDamageBonusTaken(caster, spellInfo, damage, SPELL_DIRECT_DAMAGE); + } + + DamageInfo dmgInfo(this, victim, damage, spellInfo, spellInfo->GetSchoolMask(), SPELL_DIRECT_DAMAGE, BASE_ATTACK); + victim->CalcAbsorbResist(dmgInfo); + damage = dmgInfo.GetDamage(); + Unit::DealDamageMods(victim, damage, nullptr); + + /// @todo Move this to a packet handler + WorldPackets::CombatLog::SpellDamageShield damageShield; + damageShield.Attacker = victim->GetGUID(); + damageShield.Defender = GetGUID(); + damageShield.SpellID = spellInfo->Id; + damageShield.TotalDamage = damage; + damageShield.OverKill = std::max(int32(damage) - int32(GetHealth()), 0); + damageShield.SchoolMask = spellInfo->SchoolMask; + damageShield.LogAbsorbed = dmgInfo.GetAbsorb(); + victim->SendMessageToSet(damageShield.Write(), true); + + Unit::DealDamage(victim, this, damage, 0, SPELL_DIRECT_DAMAGE, spellInfo->GetSchoolMask(), spellInfo, true); + } + } +} + +void Unit::HandleEmoteCommand(uint32 emoteId) +{ + WorldPackets::Chat::Emote emote; + emote.Guid = GetGUID(); + emote.EmoteID = emoteId; + + SendMessageToSet(emote.Write(), true); +} + +/*static*/ bool Unit::IsDamageReducedByArmor(SpellSchoolMask schoolMask, SpellInfo const* spellInfo /*= nullptr*/) +{ + // only physical spells damage gets reduced by armor + if ((schoolMask & SPELL_SCHOOL_MASK_NORMAL) == 0) + return false; + + return !spellInfo || !spellInfo->HasAttribute(SPELL_ATTR0_CU_IGNORE_ARMOR); +} + +// Calculation taken from Paperdollframe.lua (15595) +static float GetArmorReduction(float armor, uint8 attackerLevel) +{ + float levelModifier = attackerLevel; + if (levelModifier > 80) + levelModifier = levelModifier + (4.5f * (levelModifier - 59)) + (20 * (levelModifier - 80)); + else if (levelModifier > 59) + levelModifier = levelModifier + (4.5f * (levelModifier - 59)); + + float temp = 0.1f * armor / (8.5f * levelModifier + 40); + temp = temp / (1 + temp); + + if (temp > 0.75f) + return 75.f; + + if (temp < 0.f) + return 0.f; + + return temp * 100; +} + +/*static*/ uint32 Unit::CalcArmorReducedDamage(Unit const* attacker, Unit* victim, const uint32 damage, SpellInfo const* spellInfo, WeaponAttackType /*attackType*/, uint8 attackerLevel /*= 0*/) +{ + float armor = float(victim->GetArmor()); + + if (attacker) + { + // bypass enemy armor by SPELL_AURA_BYPASS_ARMOR_FOR_CASTER + int32 armorBypassPct = victim->GetTotalAuraModifier(SPELL_AURA_BYPASS_ARMOR_FOR_CASTER, [&](AuraEffect const* aurEff) + { + return aurEff->GetCasterGUID() == attacker->GetGUID(); + }); + + armor = CalculatePct(armor, 100 - std::min(armorBypassPct, 100)); + + // Ignore enemy armor by SPELL_AURA_MOD_TARGET_RESISTANCE aura + armor += attacker->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_TARGET_RESISTANCE, SPELL_SCHOOL_MASK_NORMAL); + + if (spellInfo) + if (Player* modOwner = attacker->GetSpellModOwner()) + modOwner->ApplySpellMod(spellInfo, SpellModOp::TargetResistance, armor); + + for (AuraEffect const* aurEff : attacker->GetAuraEffectsByType(SPELL_AURA_MOD_IGNORE_TARGET_RESIST)) + { + if (aurEff->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL) + armor = std::floor(AddPct(armor, -aurEff->GetAmount())); + } + + if (armor < 0.0f) + armor = 0.0f; + } + + float armorReduction = 100.f - GetArmorReduction(armor, attacker ? attacker->getLevel() : attackerLevel); + return std::max(std::ceil(damage * armorReduction / 100.f), 0); +} + +/*static*/ uint32 Unit::CalcSpellResistedDamage(Unit const* attacker, Unit* victim, uint32 damage, SpellSchoolMask schoolMask, SpellInfo const* spellInfo) +{ + // Magic damage, check for resists + if (!(schoolMask & SPELL_SCHOOL_MASK_MAGIC)) + return 0; + + // Npcs can have holy resistance + if ((schoolMask & SPELL_SCHOOL_MASK_HOLY) && victim->GetTypeId() != TYPEID_UNIT) + return 0; + + float const averageResist = Unit::CalculateAverageResistReduction(attacker, schoolMask, victim, spellInfo); + + float discreteResistProbability[11] = { }; + if (averageResist <= 0.1f) + { + discreteResistProbability[0] = 1.0f - 7.5f * averageResist; + discreteResistProbability[1] = 5.0f * averageResist; + discreteResistProbability[2] = 2.5f * averageResist; + } + else + { + for (uint32 i = 0; i < 11; ++i) + discreteResistProbability[i] = std::max(0.5f - 2.5f * std::fabs(0.1f * i - averageResist), 0.0f); + } + + float roll = rand_norm(); + float probabilitySum = 0.0f; + + uint32 resistance = 0; + for (; resistance < 11; ++resistance) + if (roll < (probabilitySum += discreteResistProbability[resistance])) + break; + + float damageResisted = damage * resistance / 10.f; + if (damageResisted > 0.0f) // if any damage was resisted + { + if (attacker) + { + int32 ignoredResistance = attacker->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_IGNORE_TARGET_RESIST, schoolMask); + ignoredResistance = std::min(ignoredResistance, 100); + ApplyPct(damageResisted, 100 - ignoredResistance); + + // Spells with melee and magic school mask, decide whether resistance or armor absorb is higher + if (spellInfo && spellInfo->HasAttribute(SPELL_ATTR0_CU_SCHOOLMASK_NORMAL_WITH_MAGIC)) + { + uint32 damageAfterArmor = Unit::CalcArmorReducedDamage(attacker, victim, damage, spellInfo, spellInfo->GetAttackType()); + float armorReduction = damage - damageAfterArmor; + + // pick the lower one, the weakest resistance counts + damageResisted = std::min(damageResisted, armorReduction); + } + } + } + + damageResisted = std::max(damageResisted, 0.f); + return uint32(damageResisted); +} + +/*static*/ float Unit::CalculateAverageResistReduction(WorldObject const* caster, SpellSchoolMask schoolMask, Unit const* victim, SpellInfo const* spellInfo) +{ + float victimResistance = float(victim->GetResistance(schoolMask)); + if (caster) + { + // pets inherit 100% of masters penetration + if (Player const* player = caster->GetSpellModOwner()) + { + victimResistance += float(player->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_TARGET_RESISTANCE, schoolMask)); + victimResistance -= float(player->GetSpellPenetrationItemMod()); + } + else if (Unit const* unitCaster = caster->ToUnit()) + victimResistance += float(unitCaster->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_TARGET_RESISTANCE, schoolMask)); + } + + // holy resistance exists in pve and comes from level difference, ignore template values + if (schoolMask & SPELL_SCHOOL_MASK_HOLY) + victimResistance = 0.0f; + + // Chaos Bolt exception, ignore all target resistances (unknown attribute?) + if (spellInfo && spellInfo->SpellFamilyName == SPELLFAMILY_WARLOCK && spellInfo->SpellIconID == 3178) + victimResistance = 0.0f; + + victimResistance = std::max(victimResistance, 0.0f); + + // level-based resistance does not apply to binary spells, and cannot be overcome by spell penetration + // gameobject caster -- should it have level based resistance? + if (caster && caster->GetTypeId() != TYPEID_GAMEOBJECT && (!spellInfo || !spellInfo->HasAttribute(SPELL_ATTR0_CU_BINARY_SPELL))) + victimResistance += std::max((float(victim->getLevelForTarget(caster)) - float(caster->getLevelForTarget(victim))) * 5.0f, 0.0f); + + static uint32 const BOSS_LEVEL = 83; + static float const BOSS_RESISTANCE_CONSTANT = 510.0f; + uint32 level = victim->getLevel(); + float resistanceConstant = 0.0f; + + if (level == BOSS_LEVEL) + resistanceConstant = BOSS_RESISTANCE_CONSTANT; + else + resistanceConstant = level * 5.0f; + + return victimResistance / (victimResistance + resistanceConstant); +} + +/*static*/ void Unit::CalcAbsorbResist(DamageInfo& damageInfo) +{ + if (!damageInfo.GetVictim() || !damageInfo.GetVictim()->IsAlive() || !damageInfo.GetDamage()) + return; + + uint32 resistedDamage = Unit::CalcSpellResistedDamage(damageInfo.GetAttacker(), damageInfo.GetVictim(), damageInfo.GetDamage(), damageInfo.GetSchoolMask(), damageInfo.GetSpellInfo()); + damageInfo.ResistDamage(resistedDamage); + + // Ignore Absorption Auras + float auraAbsorbMod = 0.f; + if (Unit* attacker = damageInfo.GetAttacker()) + { + auraAbsorbMod = attacker->GetMaxPositiveAuraModifierByMiscMask(SPELL_AURA_MOD_TARGET_ABSORB_SCHOOL, damageInfo.GetSchoolMask()); + auraAbsorbMod = std::max(auraAbsorbMod, static_cast(attacker->GetMaxPositiveAuraModifier(SPELL_AURA_MOD_TARGET_ABILITY_ABSORB_SCHOOL, [&damageInfo](AuraEffect const* aurEff) -> bool + { + if (!(aurEff->GetMiscValue() & damageInfo.GetSchoolMask())) + return false; + + if (!aurEff->IsAffectingSpell(damageInfo.GetSpellInfo())) + return false; + + return true; + }))); + } + + RoundToInterval(auraAbsorbMod, 0.0f, 100.0f); + + int32 absorbIgnoringDamage = CalculatePct(damageInfo.GetDamage(), auraAbsorbMod); + + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + AuraEffectList vSchoolAbsorbCopy(damageInfo.GetVictim()->GetAuraEffectsByType(SPELL_AURA_SCHOOL_ABSORB)); + vSchoolAbsorbCopy.sort(Trinity::AbsorbAuraOrderPred()); + + // absorb without mana cost + for (AuraEffectList::iterator itr = vSchoolAbsorbCopy.begin(); (itr != vSchoolAbsorbCopy.end()) && (damageInfo.GetDamage() > 0); ++itr) + { + AuraEffect* absorbAurEff = *itr; + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication const* aurApp = absorbAurEff->GetBase()->GetApplicationOfTarget(damageInfo.GetVictim()->GetGUID()); + if (!aurApp) + continue; + if (!(absorbAurEff->GetMiscValue() & damageInfo.GetSchoolMask())) + continue; + + // get amount which can be still absorbed by the aura + int32 currentAbsorb = absorbAurEff->GetAmount(); + // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety + if (currentAbsorb < 0) + currentAbsorb = 0; + + if (!absorbAurEff->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE)) + damageInfo.ModifyDamage(-absorbIgnoringDamage); + + uint32 tempAbsorb = uint32(currentAbsorb); + + bool defaultPrevented = false; + + absorbAurEff->GetBase()->CallScriptEffectAbsorbHandlers(absorbAurEff, aurApp, damageInfo, tempAbsorb, defaultPrevented); + currentAbsorb = tempAbsorb; + + if (!defaultPrevented) + { + + // absorb must be smaller than the damage itself + currentAbsorb = RoundToInterval(currentAbsorb, 0, int32(damageInfo.GetDamage())); + + damageInfo.AbsorbDamage(currentAbsorb); + + tempAbsorb = currentAbsorb; + absorbAurEff->GetBase()->CallScriptEffectAfterAbsorbHandlers(absorbAurEff, aurApp, damageInfo, tempAbsorb); + + // Check if our aura is using amount to count damage + if (absorbAurEff->GetAmount() >= 0) + { + // Reduce shield amount + absorbAurEff->SetAmount(absorbAurEff->GetAmount() - currentAbsorb); + // Aura cannot absorb anything more - remove it + if (absorbAurEff->GetAmount() <= 0) + absorbAurEff->GetBase()->Remove(AuraRemoveFlags::ByEnemySpell); + } + } + + if (!absorbAurEff->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE)) + damageInfo.ModifyDamage(absorbIgnoringDamage); + } + + // absorb by mana cost + AuraEffectList vManaShieldCopy(damageInfo.GetVictim()->GetAuraEffectsByType(SPELL_AURA_MANA_SHIELD)); + for (AuraEffectList::const_iterator itr = vManaShieldCopy.begin(); (itr != vManaShieldCopy.end()) && (damageInfo.GetDamage() > 0); ++itr) + { + AuraEffect* absorbAurEff = *itr; + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication const* aurApp = absorbAurEff->GetBase()->GetApplicationOfTarget(damageInfo.GetVictim()->GetGUID()); + if (!aurApp) + continue; + // check damage school mask + if (!(absorbAurEff->GetMiscValue() & damageInfo.GetSchoolMask())) + continue; + + // get amount which can be still absorbed by the aura + int32 currentAbsorb = absorbAurEff->GetAmount(); + // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety + if (currentAbsorb < 0) + currentAbsorb = 0; + + if (!absorbAurEff->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE)) + damageInfo.ModifyDamage(-absorbIgnoringDamage); + + uint32 tempAbsorb = currentAbsorb; + + bool defaultPrevented = false; + + absorbAurEff->GetBase()->CallScriptEffectManaShieldHandlers(absorbAurEff, aurApp, damageInfo, tempAbsorb, defaultPrevented); + currentAbsorb = tempAbsorb; + + if (!defaultPrevented) + { + + // absorb must be smaller than the damage itself + currentAbsorb = RoundToInterval(currentAbsorb, 0, int32(damageInfo.GetDamage())); + + int32 manaReduction = currentAbsorb; + + // lower absorb amount by talents + if (float manaMultiplier = absorbAurEff->GetSpellInfo()->Effects[absorbAurEff->GetEffIndex()].CalcValueMultiplier(absorbAurEff->GetCaster())) + manaReduction = int32(float(manaReduction) * manaMultiplier); + + int32 manaTaken = -damageInfo.GetVictim()->ModifyPower(POWER_MANA, -manaReduction); + + // take case when mana has ended up into account + currentAbsorb = currentAbsorb ? int32(float(currentAbsorb) * (float(manaTaken) / float(manaReduction))) : 0; + + damageInfo.AbsorbDamage(currentAbsorb); + + tempAbsorb = currentAbsorb; + absorbAurEff->GetBase()->CallScriptEffectAfterManaShieldHandlers(absorbAurEff, aurApp, damageInfo, tempAbsorb); + + // Check if our aura is using amount to count damage + if (absorbAurEff->GetAmount() >= 0) + { + absorbAurEff->SetAmount(absorbAurEff->GetAmount() - currentAbsorb); + if ((absorbAurEff->GetAmount() <= 0)) + absorbAurEff->GetBase()->Remove(AuraRemoveFlags::ByEnemySpell); + } + } + + if (!absorbAurEff->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE)) + damageInfo.ModifyDamage(absorbIgnoringDamage); + } + + // split damage auras - only when not damaging self + if (damageInfo.GetVictim() != damageInfo.GetAttacker()) + { + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + AuraEffectList vSplitDamagePctCopy(damageInfo.GetVictim()->GetAuraEffectsByType(SPELL_AURA_SPLIT_DAMAGE_PCT)); + for (AuraEffectList::iterator itr = vSplitDamagePctCopy.begin(); itr != vSplitDamagePctCopy.end() && damageInfo.GetDamage() > 0; ++itr) + { + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication const* aurApp = (*itr)->GetBase()->GetApplicationOfTarget(damageInfo.GetVictim()->GetGUID()); + if (!aurApp) + continue; + + // check damage school mask + if (!((*itr)->GetMiscValue() & damageInfo.GetSchoolMask())) + continue; + + // Damage can be splitted only if aura has an alive caster + Unit* caster = (*itr)->GetCaster(); + if (!caster || (caster == damageInfo.GetVictim()) || !caster->IsInWorld() || !caster->IsAlive()) + continue; + + uint32 splitDamage = CalculatePct(damageInfo.GetDamage(), (*itr)->GetAmount()); + + (*itr)->GetBase()->CallScriptEffectSplitHandlers((*itr), aurApp, damageInfo, splitDamage); + + // absorb must be smaller than the damage itself + splitDamage = RoundToInterval(splitDamage, uint32(0), uint32(damageInfo.GetDamage())); + + damageInfo.AbsorbDamage(splitDamage); + + // check if caster is immune to damage + if (caster->IsImmunedToDamage(damageInfo.GetSchoolMask())) + { + damageInfo.GetVictim()->SendSpellMiss(caster, (*itr)->GetSpellInfo()->Id, SPELL_MISS_IMMUNE); + continue; + } + + uint32 split_absorb = 0; + Unit::DealDamageMods(caster, splitDamage, &split_absorb); + + if (Unit* attacker = damageInfo.GetAttacker()) + { + // Sparring Checks + if (Creature* target = damageInfo.GetVictim()->ToCreature()) + if (attacker->IsCreature() && !attacker->IsCharmedOwnedByPlayerOrPlayer()) + if (target->GetNoNpcDamageBelowPctHealthValue() != 0.0f) + if (target->GetHealthPct() <= target->GetNoNpcDamageBelowPctHealthValue()) + damageInfo.ModifyDamage(damageInfo.GetDamage() * -1); + + attacker->SendSpellNonMeleeDamageLog(caster, (*itr)->GetSpellInfo()->Id, splitDamage, damageInfo.GetSchoolMask(), split_absorb, 0, damageInfo.GetDamageType() == DOT, 0, false, true); + } + + Unit::DealDamage(damageInfo.GetAttacker(), caster, splitDamage, 0, DIRECT_DAMAGE, damageInfo.GetSchoolMask(), (*itr)->GetSpellInfo(), false); + + // break 'Fear' and similar auras + Unit::ProcSkillsAndAuras(damageInfo.GetAttacker(), caster, PROC_FLAG_NONE, PROC_FLAG_TAKE_HARMFUL_SPELL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, PROC_HIT_NONE, nullptr, &damageInfo, nullptr); + } + } +} + +/*static*/ void Unit::CalcHealAbsorb(HealInfo& healInfo) +{ + if (!healInfo.GetHeal()) + return; + + int32 const healing = static_cast(healInfo.GetHeal()); + int32 absorbAmount = 0; + + // Need remove expired auras after + bool existExpired = false; + + // absorb without mana cost + AuraEffectList const& vHealAbsorb = healInfo.GetTarget()->GetAuraEffectsByType(SPELL_AURA_SCHOOL_HEAL_ABSORB); + for (AuraEffectList::const_iterator i = vHealAbsorb.begin(); i != vHealAbsorb.end() && absorbAmount <= healing; ++i) + { + if (!((*i)->GetMiscValue() & healInfo.GetSpellInfo()->SchoolMask)) + continue; + + // Max Amount can be absorbed by this aura + int32 currentAbsorb = (*i)->GetAmount(); + + // Found empty aura (impossible but..) + if (currentAbsorb <= 0) + { + existExpired = true; + continue; + } + + // currentAbsorb - damage can be absorbed by shield + // If need absorb less damage + if (healing < currentAbsorb + absorbAmount) + currentAbsorb = healing - absorbAmount; + + absorbAmount += currentAbsorb; + + // Reduce shield amount + (*i)->SetAmount((*i)->GetAmount() - currentAbsorb); + // Need remove it later + if ((*i)->GetAmount() <= 0) + existExpired = true; + } + + // Remove all expired absorb auras + if (existExpired) + { + for (AuraEffectList::const_iterator i = vHealAbsorb.begin(); i != vHealAbsorb.end();) + { + AuraEffect* auraEff = *i; + ++i; + if (auraEff->GetAmount() <= 0) + { + uint32 removedAuras = healInfo.GetTarget()->m_removedAurasCount; + auraEff->GetBase()->Remove(AuraRemoveFlags::ByEnemySpell); + if (removedAuras + 1 < healInfo.GetTarget()->m_removedAurasCount) + i = vHealAbsorb.begin(); + } + } + } + + if (absorbAmount > 0) + healInfo.AbsorbHeal(absorbAmount); +} + +void Unit::AttackerStateUpdate(Unit* victim, WeaponAttackType attType, bool extra) +{ + if (HasUnitState(UNIT_STATE_CANNOT_AUTOATTACK) || HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PACIFIED)) + return; + + if (HasUnitState(UNIT_STATE_CASTING)) + { + Spell* channeledSpell = GetCurrentSpell(CURRENT_CHANNELED_SPELL); + if (!channeledSpell || !channeledSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR5_ALLOW_ACTIONS_DURING_CHANNEL)) + return; + } + + if (!victim->IsAlive()) + return; + + if ((attType == BASE_ATTACK || attType == OFF_ATTACK) && !IsWithinLOSInMap(victim)) + return; + + AtTargetAttacked(victim, true); + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Attacking); + + if (attType != BASE_ATTACK && attType != OFF_ATTACK) + return; // ignore ranged case + + // melee attack spell cast at main hand attack only - no normal melee dmg dealt + if (attType == BASE_ATTACK && m_currentSpells[CURRENT_MELEE_SPELL] && !extra) + m_currentSpells[CURRENT_MELEE_SPELL]->cast(); + else + { + // attack can be redirected to another target + victim = GetMeleeHitRedirectTarget(victim); + + AuraEffectList const& meleeAttackOverrides = GetAuraEffectsByType(SPELL_AURA_OVERRIDE_AUTOATTACK_WITH_MELEE_SPELL); + AuraEffect const* meleeAttackAuraEffect = nullptr; + uint32 meleeAttackSpellId = 0; + if (attType == BASE_ATTACK) + { + if (!meleeAttackOverrides.empty()) + { + meleeAttackAuraEffect = meleeAttackOverrides.front(); + meleeAttackSpellId = meleeAttackAuraEffect->GetSpellInfo()->Effects[meleeAttackAuraEffect->GetEffIndex()].TriggerSpell; + } + } + else + { + auto itr = std::find_if(meleeAttackOverrides.begin(), meleeAttackOverrides.end(), [&](AuraEffect const* aurEff) + { + return aurEff->GetAmount() != 0; + }); + + if (itr != meleeAttackOverrides.end()) + { + meleeAttackAuraEffect = *itr; + meleeAttackSpellId = meleeAttackAuraEffect->GetAmount(); + } + } + + if (!meleeAttackAuraEffect) + { + CalcDamageInfo damageInfo; + CalculateMeleeDamage(victim, &damageInfo, attType); + // Send log damage message to client + Unit::DealDamageMods(victim, damageInfo.Damage, &damageInfo.Absorb); + + // Sparring Checks + if (Creature* target = victim->ToCreature()) + if (IsCreature() && !IsCharmedOwnedByPlayerOrPlayer()) + if (target->GetNoNpcDamageBelowPctHealthValue() != 0.0f) + if (target->GetHealthPct() <= target->GetNoNpcDamageBelowPctHealthValue()) + damageInfo.HitInfo |= HITINFO_FAKE_DAMAGE; + + // Rage reward + if (this != victim && damageInfo.HitOutCome != MELEE_HIT_MISS && GetPowerType() == POWER_RAGE) + { + if (uint32 rageReward = CalcMeleeAttackRageGain(this, victim, attType)) + { + damageInfo.HitInfo |= HITINFO_RAGE_GAIN; + damageInfo.RageGained = RewardRage(rageReward, true); + } + } + + SendAttackStateUpdate(&damageInfo); + + DealMeleeDamage(&damageInfo, true); + + DamageInfo dmgInfo(damageInfo); + Unit::ProcSkillsAndAuras(damageInfo.Attacker, damageInfo.Target, damageInfo.ProcAttacker, damageInfo.ProcVictim, PROC_SPELL_TYPE_NONE, PROC_SPELL_PHASE_NONE, dmgInfo.GetHitMask(), nullptr, &dmgInfo, nullptr); + + TC_LOG_DEBUG("entities.unit", "AttackerStateUpdate: %s attacked %s for %u dmg, absorbed %u, blocked %u, resisted %u.", + GetGUID().ToString().c_str(), victim->GetGUID().ToString().c_str(), damageInfo.Damage, damageInfo.Absorb, damageInfo.Blocked, damageInfo.Resist); + } + else + { + CastSpell(victim, meleeAttackSpellId, meleeAttackAuraEffect); + + uint32 hitInfo = HITINFO_AFFECTS_VICTIM | HITINFO_NO_ANIMATION; + if (attType == OFF_ATTACK) + hitInfo |= HITINFO_OFFHAND; + + SendAttackStateUpdate(hitInfo, victim, 0, GetMeleeDamageSchoolMask(), 0, 0, 0, VICTIMSTATE_HIT, 0); + } + } +} + +void Unit::HandleProcExtraAttackFor(Unit* victim) +{ + while (m_extraAttacks) + { + AttackerStateUpdate(victim, BASE_ATTACK, true); + --m_extraAttacks; + } +} + +MeleeHitOutcome Unit::RollMeleeOutcomeAgainst(const Unit* victim, WeaponAttackType attType) const +{ + if (victim->IsCreature() && victim->ToCreature()->IsInEvadeMode()) + return MELEE_HIT_EVADE; + + int32 const attackerMaxSkillValueForLevel = GetMaxSkillValueForLevel(victim); + int32 const victimMaxSkillValueForLevel = victim->GetMaxSkillValueForLevel(this); + + // Miss chance based on melee + int32 miss_chance = int32(MeleeSpellMissChance(victim, attType) * 100.f); + + // Critical hit chance + int32 crit_chance = int32(GetUnitCriticalChanceAgainst(attType, victim) * 100.0f); + + // stunned target cannot dodge and this is check in GetUnitDodgeChance() (returned 0 in this case) + int32 dodge_chance = int32(GetUnitDodgeChance(attType, victim) * 100.0f); + int32 block_chance = int32(GetUnitBlockChance(victim) * 100.0f); + int32 parry_chance = int32(GetUnitParryChance(attType, victim) * 100.0f); + + + // melee attack table implementation + // outcome priority: + // 1. > 2. > 3. > 4. > 5. > 6. > 7. > 8. + // MISS > DODGE > PARRY > GLANCING > BLOCK > CRIT > CRUSHING > HIT + + int32 sum = 0, tmp = 0; + int32 roll = urand(0, 9999); + + float arcAngle = static_cast(M_PI); + if (victim->IsCreature() && victim->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_5_240_DEGREE_BACK_ARC)) + arcAngle -= static_cast(M_PI / 3); + + // check if attack comes from behind, nobody can parry or block if attacker is behind + bool canParryOrBlock = victim->HasInArc(arcAngle, this) || victim->HasAuraType(SPELL_AURA_IGNORE_HIT_DIRECTION); + + // only creatures can dodge if attacker is behind + bool canDodge = victim->GetTypeId() != TYPEID_PLAYER || canParryOrBlock; + + // if victim is casting or cc'd it can't avoid attacks + if (victim->IsNonMeleeSpellCast(false) || victim->HasUnitState(UNIT_STATE_CONTROLLED)) + { + canDodge = false; + canParryOrBlock = false; + } + + // 1. MISS + tmp = miss_chance; + if (tmp > 0 && roll < (sum += tmp)) + return MELEE_HIT_MISS; + + // always crit against a sitting target (except 0 crit chance) + if (victim->IsPlayer() && crit_chance > 0 && !victim->IsStandState()) + return MELEE_HIT_CRIT; + + // 2. DODGE + if (canDodge) + { + tmp = dodge_chance; + if (tmp > 0 // check if unit _can_ dodge + && roll < (sum += tmp)) + return MELEE_HIT_DODGE; + } + + // 3. PARRY + if (canParryOrBlock) + { + tmp = parry_chance; + if (tmp > 0 // check if unit _can_ parry + && roll < (sum += tmp)) + return MELEE_HIT_PARRY; + } + + // 4. GLANCING + // Max 40% chance to score a glancing blow against mobs of the same or higher level (only players and pets, not for ranged weapons). + if ((GetTypeId() == TYPEID_PLAYER || IsPet()) && + victim->GetTypeId() != TYPEID_PLAYER && !victim->IsPet() && + getLevel() <= victim->getLevelForTarget(this)) + { + // cap possible value (with bonuses > max skill) + int32 skill = attackerMaxSkillValueForLevel; + + // against boss-level targets - 24% chance of 25% average damage reduction (damage reduction range : 20-30%) + // against level 82 elites - 18% chance of 15% average damage reduction (damage reduction range : 10-20%) + tmp = 600 + (victimMaxSkillValueForLevel - skill) * 120; + tmp = std::min(tmp, 4000); + if (tmp > 0 && roll < (sum += tmp)) + return MELEE_HIT_GLANCING; + } + + // 5. BLOCK + if (canParryOrBlock) + { + tmp = block_chance; + if (tmp > 0 // check if unit _can_ block + && roll < (sum += tmp)) + return MELEE_HIT_BLOCK; + } + + // 6.CRIT + tmp = crit_chance; + if (tmp > 0 && roll < (sum += tmp)) + return MELEE_HIT_CRIT; + + // 7. CRUSHING + // mobs can score crushing blows if they're 4 or more levels above victim + if (getLevelForTarget(victim) >= victim->getLevelForTarget(this) + 4 && + // can be from by creature (if can) or from controlled player that considered as creature + !IsControlledByPlayer() && + !(IsCreature() && ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_CRUSH)) + { + // when their weapon skill is 15 or more above victim's defense skill + tmp = victimMaxSkillValueForLevel; + // tmp = mob's level * 5 - player's current defense skill + tmp = attackerMaxSkillValueForLevel - tmp; + // minimum of 20 points diff (4 levels difference) + tmp = std::max(tmp, 20); + + // add 2% chance per lacking skill point, min. is 15% + tmp = tmp * 200 - 1500; + if (roll < (sum += tmp)) + return MELEE_HIT_CRUSHING; + } + + // 8. HIT + return MELEE_HIT_NORMAL; +} + +uint32 Unit::CalculateDamage(WeaponAttackType attType, bool normalized, bool addTotalPct) const +{ + float minDamage = 0.0f; + float maxDamage = 0.0f; + + if (normalized || !addTotalPct) + CalculateMinMaxDamage(attType, normalized, addTotalPct, minDamage, maxDamage); + else + { + switch (attType) + { + case RANGED_ATTACK: + minDamage = GetFloatValue(UNIT_FIELD_MINRANGEDDAMAGE); + maxDamage = GetFloatValue(UNIT_FIELD_MAXRANGEDDAMAGE); + break; + case BASE_ATTACK: + minDamage = GetFloatValue(UNIT_FIELD_MINDAMAGE); + maxDamage = GetFloatValue(UNIT_FIELD_MAXDAMAGE); + break; + case OFF_ATTACK: + minDamage = GetFloatValue(UNIT_FIELD_MINOFFHANDDAMAGE); + maxDamage = GetFloatValue(UNIT_FIELD_MAXOFFHANDDAMAGE); + break; + default: + break; + } + } + + minDamage = std::max(0.f, minDamage); + maxDamage = std::max(0.f, maxDamage); + + if (minDamage > maxDamage) + std::swap(minDamage, maxDamage); + + return urand(uint32(minDamage), uint32(maxDamage)); +} + +void Unit::SendMeleeAttackStart(Unit* victim) +{ + WorldPackets::Combat::AttackStart packet; + packet.Attacker = GetGUID(); + packet.Victim = victim->GetGUID(); + SendMessageToSet(packet.Write(), IsPlayer()); +} + +void Unit::SendMeleeAttackStop(Unit* victim) +{ + SendMessageToSet(WorldPackets::Combat::SAttackStop(this, victim).Write(), IsPlayer()); + + if (victim) + TC_LOG_DEBUG("entities.unit", "%s %u stopped attacking %s %u", (GetTypeId() == TYPEID_PLAYER ? "Player" : "Creature"), GetGUID().GetCounter(), (victim->GetTypeId() == TYPEID_PLAYER ? "player" : "creature"), victim->GetGUID().GetCounter()); + else + TC_LOG_DEBUG("entities.unit", "%s %u stopped attacking", (GetTypeId() == TYPEID_PLAYER ? "Player" : "Creature"), GetGUID().GetCounter()); +} + +bool Unit::IsBlockCritical() const +{ + if (roll_chance_i(GetTotalAuraModifier(SPELL_AURA_MOD_BLOCK_CRIT_CHANCE))) + return true; + return false; +} + +int32 Unit::GetMechanicResistChance(SpellInfo const* spellInfo) const +{ + if (!spellInfo) + return 0; + + int32 resistMech = 0; + for (uint8 eff = 0; eff < MAX_SPELL_EFFECTS; ++eff) + { + if (!spellInfo->Effects[eff].IsEffect()) + break; + + int32 effectMech = spellInfo->GetEffectMechanic(eff); + if (effectMech) + { + int32 temp = GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_MECHANIC_RESISTANCE, effectMech); + if (resistMech < temp) + resistMech = temp; + } + } + return resistMech; +} + +bool Unit::CanUseAttackType(uint8 attacktype) const +{ + switch (attacktype) + { + case BASE_ATTACK: + return !HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_DISARMED); + case OFF_ATTACK: + return !HasFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_DISARM_OFFHAND); + case RANGED_ATTACK: + return !HasFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_DISARM_RANGED); + default: + return true; + } +} + +// Melee based spells hit result calculations +SpellMissInfo Unit::MeleeSpellHitResult(Unit* victim, SpellInfo const* spellInfo) const +{ + if (spellInfo->HasAttribute(SPELL_ATTR3_NO_AVOIDANCE)) + return SPELL_MISS_NONE; + + WeaponAttackType attType = BASE_ATTACK; + + // Check damage class instead of attack type to correctly handle judgements + // - they are meele, but can't be dodged/parried/deflected because of ranged dmg class + if (spellInfo->DmgClass == SPELL_DAMAGE_CLASS_RANGED) + attType = RANGED_ATTACK; + + uint32 roll = urand(0, 9999); + uint32 missChance = uint32(MeleeSpellMissChance(victim, attType, spellInfo) * 100.0f); + + // Roll miss + uint32 tmp = missChance; + if (roll < tmp) + return SPELL_MISS_MISS; + + // Chance resist mechanic + int32 resist_chance = victim->GetMechanicResistChance(spellInfo) * 100; + tmp += resist_chance; + if (roll < tmp) + return SPELL_MISS_RESIST; + + // Same spells cannot be parried/dodged + if (spellInfo->HasAttribute(SPELL_ATTR0_NO_ACTIVE_DEFENSE)) + return SPELL_MISS_NONE; + + bool canDodge = !spellInfo->HasAttribute(SPELL_ATTR7_NO_ATTACK_DODGE); + bool canParry = !spellInfo->HasAttribute(SPELL_ATTR7_NO_ATTACK_PARRY); + bool canBlock = !spellInfo->HasAttribute(SPELL_ATTR8_NO_ATTACK_BLOCK); + + // if victim is casting or cc'd it can't avoid attacks + bool canUseDefenseWhileChanneling = victim->GetCurrentSpell(CURRENT_CHANNELED_SPELL) ? victim->GetCurrentSpell(CURRENT_CHANNELED_SPELL)->GetSpellInfo()->HasAttribute(SPELL_ATTR10_ALLOW_DEFENSE_WHILE_CHANNELING) : false; + bool canUseDefenseWhileCasting = victim->GetCurrentSpell(CURRENT_GENERIC_SPELL) ? victim->GetCurrentSpell(CURRENT_GENERIC_SPELL)->GetSpellInfo()->HasAttribute(SPELL_ATTR10_ALLOW_DEFENSE_WHILE_CASTING) : false; + if ((victim->IsNonMeleeSpellCast(false) || victim->HasUnitState(UNIT_STATE_CONTROLLED)) && !canUseDefenseWhileChanneling && !canUseDefenseWhileCasting) + { + canDodge = false; + canParry = false; + canBlock = false; + } + + // Ranged attacks can only miss, resist and deflect and get blocked + if (attType == RANGED_ATTACK) + { + canDodge = false; + canParry = false; + + // only if in front + if (!victim->HasUnitState(UNIT_STATE_CONTROLLED) && (victim->HasInArc(float(M_PI), this) || victim->HasAuraType(SPELL_AURA_IGNORE_HIT_DIRECTION))) + { + int32 deflect_chance = victim->GetTotalAuraModifier(SPELL_AURA_DEFLECT_SPELLS) * 100; + tmp += deflect_chance; + if (roll < tmp) + return SPELL_MISS_DEFLECT; + } + } + + // Check for attack from behind + if (!victim->HasInArc(float(M_PI), this)) + { + if (!victim->HasAuraType(SPELL_AURA_IGNORE_HIT_DIRECTION)) + { + // Can't dodge from behind in PvP (but its possible in PvE) + if (victim->GetTypeId() == TYPEID_PLAYER) + canDodge = false; + // Can't parry or block + canParry = false; + canBlock = false; + } + else // Only deterrence as of 3.3.5 + { + if (spellInfo->HasAttribute(SPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET)) + canParry = false; + } + } + + // Ignore combat result aura + AuraEffectList const& ignore = GetAuraEffectsByType(SPELL_AURA_IGNORE_COMBAT_RESULT); + for (AuraEffect const* aurEff : ignore) + { + if (!aurEff->IsAffectingSpell(spellInfo)) + continue; + + switch (aurEff->GetMiscValue()) + { + case MELEE_HIT_DODGE: + canDodge = false; + break; + case MELEE_HIT_BLOCK: + canBlock = false; + break; + case MELEE_HIT_PARRY: + canParry = false; + break; + default: + TC_LOG_DEBUG("entities.unit", "Spell %u SPELL_AURA_IGNORE_COMBAT_RESULT has unhandled state %d", aurEff->GetId(), aurEff->GetMiscValue()); + break; + } + } + + if (canDodge) + { + // Roll dodge + int32 dodgeChance = int32(GetUnitDodgeChance(attType, victim) * 100.0f); + if (dodgeChance < 0) + dodgeChance = 0; + + if (roll < (tmp += dodgeChance)) + return SPELL_MISS_DODGE; + } + + if (canParry) + { + // Roll parry + int32 parryChance = int32(GetUnitParryChance(attType, victim) * 100.0f); + if (parryChance < 0) + parryChance = 0; + + tmp += parryChance; + if (roll < tmp) + return SPELL_MISS_PARRY; + } + + if (canBlock) + { + int32 blockChance = int32(GetUnitBlockChance(victim) * 100.0f); + if (blockChance < 0) + blockChance = 0; + tmp += blockChance; + + if (roll < tmp) + return SPELL_MISS_BLOCK; + } + + return SPELL_MISS_NONE; +} + +float Unit::GetUnitDodgeChance(WeaponAttackType attType, Unit const* victim) const +{ + int32 attackerSkill = GetMaxSkillValueForLevel(nullptr); + int32 defenderSkill = victim->GetMaxSkillValueForLevel(nullptr); + int32 skillDifference = defenderSkill - attackerSkill; + + float chance = 0.0f; + + if (victim->IsPlayer()) + { + chance += victim->GetFloatValue(PLAYER_DODGE_PERCENTAGE); + chance += skillDifference * 0.04f; + } + else + { + if (!victim->IsTotem()) + { + chance += victim->GetTotalAuraModifier(SPELL_AURA_MOD_DODGE_PERCENT); + if (skillDifference <= 10) + chance = 5.f + skillDifference * 0.1f; + else + chance = 6.f + (skillDifference - 10) * 0.1f; + } + } + + // Reduce enemy dodge chance by SPELL_AURA_MOD_COMBAT_RESULT_CHANCE + chance += GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_COMBAT_RESULT_CHANCE, VICTIMSTATE_DODGE); + + // reduce dodge by SPELL_AURA_MOD_ENEMY_DODGE + chance += GetTotalAuraModifier(SPELL_AURA_MOD_ENEMY_DODGE); + + if (IsPlayer()) + chance -= ToPlayer()->GetExpertiseDodgeOrParryReduction(attType); + else + chance -= GetTotalAuraModifier(SPELL_AURA_MOD_EXPERTISE) / 4.0f; + + return std::clamp(chance, 0.f, 100.f); +} + +float Unit::GetUnitParryChance(WeaponAttackType attType, Unit const* victim) const +{ + int32 attackerSkill = GetMaxSkillValueForLevel(nullptr); + int32 defenderSkill = victim->GetMaxSkillValueForLevel(nullptr); + int32 skillDifference = defenderSkill - attackerSkill; + + float chance = 0.0f; + + if (Player const* playerVictim = victim->ToPlayer()) + { + if (playerVictim->CanParry()) + { + Item* tmpitem = playerVictim->GetWeaponForAttack(BASE_ATTACK, true); + if (!tmpitem) + tmpitem = playerVictim->GetWeaponForAttack(OFF_ATTACK, true); + + if (tmpitem) + chance = playerVictim->GetFloatValue(PLAYER_PARRY_PERCENTAGE); + + chance += skillDifference * 0.04f; + } + } + else + { + // Allow parries for creatures only if it's not a totem, does have a virtual item equipped and does not have CREATURE_FLAG_EXTRA_NO_PARRY + if (!victim->IsTotem() && (victim->GetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID + 0) || victim->GetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID + 1)) && + !(victim->ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_PARRY)) + { + chance += victim->GetTotalAuraModifier(SPELL_AURA_MOD_PARRY_PERCENT); + if (skillDifference <= 10) + chance = 5.f + skillDifference * 0.1f; + else + chance = 6.f + (skillDifference - 10) * 0.1f; + } + } + + if (IsPlayer()) + chance -= ToPlayer()->GetExpertiseDodgeOrParryReduction(attType); + else + chance -= GetTotalAuraModifier(SPELL_AURA_MOD_EXPERTISE) / 4.0f; + + return std::max(chance, 0.0f); +} + +float Unit::GetMeleeMissChance(Unit const* victim) const +{ + float chance = 0.f; + + // As of patch 4.0.1, weapon skills have been deprecated so we can go for plain defense skill values + uint16 attackerSkill = GetMaxSkillValueForLevel(nullptr); + uint16 defenderSkill = victim->GetMaxSkillValueForLevel(nullptr); + int32 skillDifference = defenderSkill - attackerSkill; + + if (victim->IsPlayer()) + { + chance = 5.f; + if (skillDifference > 0) + chance += skillDifference * 0.04f; + else if (skillDifference < 0) + chance -= skillDifference * 0.02f; + } + else + { + if (skillDifference <= 10) + chance = 5.f + skillDifference * 0.1f; + else + chance = 6.f + (skillDifference - 10) * 0.4f; + } + + return std::clamp(chance, 0.f, 100.f); +} + +float Unit::GetUnitBlockChance(Unit const* victim) const +{ + float chance = 0.f; + + if (Player const* playerVictim = victim->ToPlayer()) + { + if (playerVictim->CanBlock()) + { + Item* tmpitem = playerVictim->GetUseableItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (tmpitem && !tmpitem->IsBroken()) + chance = playerVictim->GetFloatValue(PLAYER_BLOCK_PERCENTAGE); + } + } + else + { + if (!victim->IsTotem() && !(victim->ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_BLOCK)) + { + chance = 5.0f; + chance += victim->GetTotalAuraModifier(SPELL_AURA_MOD_BLOCK_PERCENT); + } + } + + return std::max(chance, 0.0f); +} + +float Unit::GetUnitCriticalChanceDone(WeaponAttackType attackType) const +{ + float chance = 0.f; + if (GetTypeId() == TYPEID_PLAYER) + { + switch (attackType) + { + case BASE_ATTACK: + chance = GetFloatValue(PLAYER_CRIT_PERCENTAGE); + break; + case OFF_ATTACK: + chance = GetFloatValue(PLAYER_OFFHAND_CRIT_PERCENTAGE); + break; + case RANGED_ATTACK: + chance = GetFloatValue(PLAYER_RANGED_CRIT_PERCENTAGE); + break; + // Just for good manner + default: + chance = 0.0f; + break; + } + } + else + { + if (!(ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_CRIT)) + { + chance = 5.0f; + chance += GetTotalAuraModifier(SPELL_AURA_MOD_WEAPON_CRIT_PERCENT); + chance += GetTotalAuraModifier(SPELL_AURA_MOD_CRIT_PCT); + } + } + + return chance; +} + +float Unit::GetUnitCriticalChanceTaken(WeaponAttackType attackType, float critDone) const +{ + float chance = critDone; + + // flat aura mods + if (attackType == RANGED_ATTACK) + chance += GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_CHANCE); + else + chance += GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_CHANCE); + + // applied after resilience + chance += GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE); + + return std::max(chance, 0.f); +} + +float Unit::GetUnitCriticalChanceAgainst(WeaponAttackType attackType, Unit const* victim) const +{ + float chance = GetUnitCriticalChanceDone(attackType); + return victim->GetUnitCriticalChanceTaken(attackType, chance); +} + +void Unit::_DeleteRemovedAuras() +{ + while (!m_removedAuras.empty()) + { + delete m_removedAuras.front(); + m_removedAuras.pop_front(); + } + + m_removedAurasCount = 0; +} + +void Unit::_UpdateSpells(uint32 time) +{ + m_spellHistory->Update(); + + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL]) + _UpdateAutoRepeatSpell(); + + // remove finished spells from current pointers + for (Spell*& spell : m_currentSpells) + { + if (spell && spell->getState() == SPELL_STATE_FINISHED) + { + spell->SetReferencedFromCurrent(false); + spell = nullptr; // remove pointer + } + } + + // m_auraUpdateIterator can be updated in indirect called code at aura remove to skip next planned to update but removed auras + for (m_auraUpdateIterator = m_ownedAuras.begin(); m_auraUpdateIterator != m_ownedAuras.end();) + { + Aura* i_aura = m_auraUpdateIterator->second; + ++m_auraUpdateIterator; // need shift to next for allow update if need into aura update + i_aura->UpdateOwner(time, this); + } + + // remove expired auras - do that after updates(used in scripts?) + for (AuraMap::iterator i = m_ownedAuras.begin(); i != m_ownedAuras.end();) + { + if (i->second->IsExpired()) + RemoveOwnedAura(i, AuraRemoveFlags::Expired); + else + ++i; + } + + for (VisibleAuraMap::iterator itr = m_visibleAuras.begin(); itr != m_visibleAuras.end(); ++itr) + if (itr->second->IsNeedClientUpdate()) + itr->second->ClientUpdate(); + + _DeleteRemovedAuras(); + + if (!m_gameObj.empty()) + { + GameObjectList::iterator itr; + for (itr = m_gameObj.begin(); itr != m_gameObj.end();) + { + if (!(*itr)->isSpawned()) + { + (*itr)->SetOwnerGUID(ObjectGuid::Empty); + (*itr)->SetRespawnTime(0); + (*itr)->Delete(); + m_gameObj.erase(itr++); + } + else + ++itr; + } + } +} + +void Unit::_UpdateAutoRepeatSpell() +{ + SpellInfo const* autoRepeatSpellInfo = m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->m_spellInfo; + + // check "realtime" interrupts + // don't cancel spells which are affected by a SPELL_AURA_CAST_WHILE_WALKING effect + if ((isMoving() && m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->CheckMovement() != SPELL_CAST_OK) || IsNonMeleeSpellCast(false, false, true, autoRepeatSpellInfo->Id == 75)) + { + // cancel wand shoot + if (autoRepeatSpellInfo->Id != 75) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL); + return; + } + + // castroutine + if (isAttackReady(RANGED_ATTACK) && m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->getState() != SPELL_STATE_PREPARING) + { + // Check if able to cast + SpellCastResult result = m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->CheckCast(true); + if (result != SPELL_CAST_OK) + { + if (autoRepeatSpellInfo->Id != 75) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL); + else if (GetTypeId() == TYPEID_PLAYER) + Spell::SendCastResult(ToPlayer(), autoRepeatSpellInfo, 1, result); + + return; + } + + // we want to shoot + Spell* spell = new Spell(this, autoRepeatSpellInfo, TRIGGERED_IGNORE_GCD); + spell->prepare(m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->m_targets); + } +} + +void Unit::SetCurrentCastSpell(Spell* spell) +{ + ASSERT(spell); // nullptr may be never passed here, use InterruptSpell or InterruptNonMeleeSpells + + CurrentSpellTypes CSpellType = spell->GetCurrentContainer(); + + if (spell == m_currentSpells[CSpellType]) // avoid breaking self + return; + + // special breakage effects: + switch (CSpellType) + { + case CURRENT_GENERIC_SPELL: + { + // generic spells always break channeled not delayed spells + if (m_currentSpells[CURRENT_CHANNELED_SPELL] && !m_currentSpells[CURRENT_CHANNELED_SPELL]->GetSpellInfo()->HasAttribute(SPELL_ATTR5_ALLOW_ACTIONS_DURING_CHANNEL)) + InterruptSpell(CURRENT_CHANNELED_SPELL, false); + + // autorepeat breaking + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL]) + { + // break autorepeat if not Auto Shot + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->GetSpellInfo()->Id != 75) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL); + } + if (spell->GetCastTime() > 0) + AddUnitState(UNIT_STATE_CASTING); + + break; + } + case CURRENT_CHANNELED_SPELL: + { + // channel spells always break generic non-delayed and any channeled spells + InterruptSpell(CURRENT_GENERIC_SPELL, false); + InterruptSpell(CURRENT_CHANNELED_SPELL); + + // it also does break autorepeat if not Auto Shot + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL] && + m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->GetSpellInfo()->Id != 75) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL); + AddUnitState(UNIT_STATE_CASTING); + + break; + } + case CURRENT_AUTOREPEAT_SPELL: + { + if (m_currentSpells[CSpellType] && m_currentSpells[CSpellType]->getState() == SPELL_STATE_IDLE) + m_currentSpells[CSpellType]->setState(SPELL_STATE_FINISHED); + + // only Auto Shoot does not break anything + if (spell->GetSpellInfo()->Id != 75) + { + // generic autorepeats break generic non-delayed and channeled non-delayed spells + InterruptSpell(CURRENT_GENERIC_SPELL, false); + InterruptSpell(CURRENT_CHANNELED_SPELL, false); + } + break; + } + default: + break; // other spell types don't break anything now + } + + // current spell (if it is still here) may be safely deleted now + if (m_currentSpells[CSpellType]) + m_currentSpells[CSpellType]->SetReferencedFromCurrent(false); + + // set new current spell + m_currentSpells[CSpellType] = spell; + spell->SetReferencedFromCurrent(true); + + spell->m_selfContainer = &(m_currentSpells[spell->GetCurrentContainer()]); +} + +void Unit::InterruptSpell(CurrentSpellTypes spellType, bool withDelayed, bool withInstant) +{ + Spell* spell = m_currentSpells[spellType]; + if (!spell) + return; + + if ((withDelayed || spell->getState() != SPELL_STATE_LAUNCHED) && + (withInstant || spell->GetCastTime() > 0 || spell->getState() == SPELL_STATE_CHANNELING)) + { + // for example, do not let self-stun aura interrupt itself + if (!spell->IsInterruptable()) + return; + + // send autorepeat cancel message for autorepeat spells + if (spellType == CURRENT_AUTOREPEAT_SPELL) + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->SendAutoRepeatCancel(this); + + if (spell->getState() != SPELL_STATE_FINISHED) + spell->cancel(); + else + { + m_currentSpells[spellType] = nullptr; + spell->SetReferencedFromCurrent(false); + } + + if (GetTypeId() == TYPEID_UNIT && IsAIEnabled()) + ToCreature()->AI()->OnSpellCastFinished(spell->GetSpellInfo(), SPELL_FINISHED_CANCELED); + } +} + +void Unit::FinishSpell(CurrentSpellTypes spellType, bool ok /*= true*/) +{ + Spell* spell = m_currentSpells[spellType]; + if (!spell) + return; + + if (spellType == CURRENT_CHANNELED_SPELL) + spell->SendChannelUpdate(0); + + spell->finish(ok); +} + +bool Unit::IsNonMeleeSpellCast(bool withDelayed, bool skipChanneled, bool skipAutorepeat, bool isAutoshoot, bool skipInstant) const +{ + // We don't do loop here to explicitly show that melee spell is excluded. + // Maybe later some special spells will be excluded too. + + // generic spells are cast when they are not finished and not delayed + if (m_currentSpells[CURRENT_GENERIC_SPELL] && + (m_currentSpells[CURRENT_GENERIC_SPELL]->getState() != SPELL_STATE_FINISHED) && + (withDelayed || m_currentSpells[CURRENT_GENERIC_SPELL]->getState() != SPELL_STATE_LAUNCHED)) + { + if (!skipInstant || m_currentSpells[CURRENT_GENERIC_SPELL]->GetCastTime()) + { + if (!isAutoshoot || !(m_currentSpells[CURRENT_GENERIC_SPELL]->m_spellInfo->HasAttribute(SPELL_ATTR2_DO_NOT_RESET_COMBAT_TIMERS))) + return true; + } + } + // channeled spells may be delayed, but they are still considered cast + if (!skipChanneled && m_currentSpells[CURRENT_CHANNELED_SPELL] && + (m_currentSpells[CURRENT_CHANNELED_SPELL]->getState() != SPELL_STATE_FINISHED)) + { + if (!isAutoshoot || !(m_currentSpells[CURRENT_CHANNELED_SPELL]->m_spellInfo->HasAttribute(SPELL_ATTR2_DO_NOT_RESET_COMBAT_TIMERS))) + return true; + } + // autorepeat spells may be finished or delayed, but they are still considered cast + if (!skipAutorepeat && m_currentSpells[CURRENT_AUTOREPEAT_SPELL]) + return true; + + return false; +} + +void Unit::InterruptNonMeleeSpells(bool withDelayed, uint32 spell_id, bool withInstant /* = true */) +{ + // generic spells are interrupted if they are not finished or delayed + if (m_currentSpells[CURRENT_GENERIC_SPELL] && (!spell_id || m_currentSpells[CURRENT_GENERIC_SPELL]->m_spellInfo->Id == spell_id)) + InterruptSpell(CURRENT_GENERIC_SPELL, withDelayed, withInstant); + + // autorepeat spells are interrupted if they are not finished or delayed + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL] && (!spell_id || m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->m_spellInfo->Id == spell_id)) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL, withDelayed, withInstant); + + // channeled spells are interrupted if they are not finished, even if they are delayed + if (m_currentSpells[CURRENT_CHANNELED_SPELL] && (!spell_id || m_currentSpells[CURRENT_CHANNELED_SPELL]->m_spellInfo->Id == spell_id)) + InterruptSpell(CURRENT_CHANNELED_SPELL, true, true); +} + +Spell* Unit::FindCurrentSpellBySpellId(uint32 spell_id) const +{ + for (Spell* spell : m_currentSpells) + if (spell && spell->m_spellInfo->Id == spell_id) + return spell; + return nullptr; +} + +int32 Unit::GetCurrentSpellCastTime(uint32 spell_id) const +{ + if (Spell const* spell = FindCurrentSpellBySpellId(spell_id)) + return spell->GetCastTime(); + return 0; +} + +bool Unit::IsMovementPreventedByCasting() const +{ + // can always move when not casting + if (!HasUnitState(UNIT_STATE_CASTING)) + return false; + + if (Spell* spell = m_currentSpells[CURRENT_GENERIC_SPELL]) + if (HasAuraTypeWithAffectMask(SPELL_AURA_CAST_WHILE_WALKING, spell->GetSpellInfo()) || spell->getState() == SPELL_STATE_FINISHED || + !spell->m_spellInfo->InterruptFlags.HasFlag(SpellInterruptFlags::Movement)) + return false; + + // channeled spells during channel stage (after the initial cast timer) allow movement with a specific spell attribute + if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL]) + if (spell->getState() != SPELL_STATE_FINISHED && spell->IsChannelActive()) + if (spell->GetSpellInfo()->IsMoveAllowedChannel() || HasAuraTypeWithAffectMask(SPELL_AURA_CAST_WHILE_WALKING, spell->GetSpellInfo())) + return false; + + // prohibit movement for all other spell casts + return true; +} + +bool Unit::isInFrontInMap(Unit const* target, float distance, float arc) const +{ + return IsWithinDistInMap(target, distance) && HasInArc(arc, target); +} + +bool Unit::isInBackInMap(Unit const* target, float distance, float arc) const +{ + return IsWithinDistInMap(target, distance) && !HasInArc(2 * float(M_PI) - arc, target); +} + +bool Unit::isInAccessiblePlaceFor(Creature const* c) const +{ + // Aquatic creatures are not allowed to leave liquids + if (!IsInWater() && c->IsAquatic()) + return false; + + // Underwater special case. Some creatures may not go below liquid surfaces + if (IsUnderWater() && c->CannotPenetrateWater()) + return false; + + // Water checks + if (IsInWater() && !c->CanEnterWater()) + return false; + + // Some creatures are tied to the ocean floor and cannot chase swimming targets. + if (!IsOnOceanFloor() && c->IsUnderWater() && c->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_CANT_SWIM)) + return false; + + return true; +} + +bool Unit::IsInWater() const +{ + return GetLiquidStatus() & (LIQUID_MAP_IN_WATER | LIQUID_MAP_UNDER_WATER); +} + +bool Unit::IsUnderWater() const +{ + return GetLiquidStatus() & LIQUID_MAP_UNDER_WATER; +} + +bool Unit::IsOnOceanFloor() const +{ + return GetLiquidStatus() & LIQUID_MAP_OCEAN_FLOOR; +} + +void Unit::ProcessPositionDataChanged(PositionFullTerrainStatus const& data) +{ + ZLiquidStatus oldLiquidStatus = GetLiquidStatus(); + WorldObject::ProcessPositionDataChanged(data); + ProcessTerrainStatusUpdate(oldLiquidStatus, data.liquidInfo); + + if (oldLiquidStatus != GetLiquidStatus()) + UpdateMountCapability(); +} + +void Unit::ProcessTerrainStatusUpdate(ZLiquidStatus /*oldLiquidStatus*/, Optional const& newLiquidData) +{ + if (!IsControlledByPlayer()) + return; + + // remove appropriate auras if we are swimming/not swimming respectively + if (IsInWater()) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::UnderWater); + else + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::AboveWater); + + // liquid aura handling + LiquidTypeEntry const* curLiquid = nullptr; + if (IsInWater() && newLiquidData) + curLiquid = sLiquidTypeStore.LookupEntry(newLiquidData->entry); + if (curLiquid != _lastLiquid) + { + if (_lastLiquid && _lastLiquid->SpellID) + RemoveAurasDueToSpell(_lastLiquid->SpellID); + Player* player = GetCharmerOrOwnerPlayerOrPlayerItself(); + + // Set _lastLiquid before casting liquid spell to avoid infinite loops + _lastLiquid = curLiquid; + + if (curLiquid && curLiquid->SpellID && (!player || !player->IsGameMaster())) + CastSpell(this, curLiquid->SpellID, true); + } +} +void Unit::DeMorph() +{ + SetDisplayId(GetNativeDisplayId()); +} + +Aura* Unit::_TryStackingOrRefreshingExistingAura(AuraCreateInfo& createInfo) +{ + ASSERT(createInfo.CasterGUID || createInfo.Caster); + + // Check if these can stack anyway + if (!createInfo.CasterGUID && !createInfo.GetSpellInfo()->IsStackableOnOneSlotWithDifferentCasters()) + createInfo.CasterGUID = createInfo.Caster->GetGUID(); + + // world gameobjects can't own auras and they send empty casterguid + // checked on sniffs with spell 22247 + if (createInfo.CasterGUID.IsGameObject()) + { + createInfo.Caster = nullptr; + createInfo.CasterGUID.Clear(); + } + + // passive and Incanter's Absorption and auras with different type can stack with themselves any number of times + if (!createInfo.GetSpellInfo()->IsMultiSlotAura()) + { + // check if cast item changed + ObjectGuid castItemGUID = createInfo.CastItemGUID; + + // find current aura from spell and change it's stackamount, or refresh it's duration + if (Aura* foundAura = GetOwnedAura(createInfo.GetSpellInfo()->Id, createInfo.GetSpellInfo()->IsStackableOnOneSlotWithDifferentCasters() ? ObjectGuid::Empty : createInfo.CasterGUID, createInfo.GetSpellInfo()->HasAttribute(SPELL_ATTR0_CU_ENCHANT_PROC) ? castItemGUID : ObjectGuid::Empty)) + { + // effect masks do not match + // extremely rare case + // let's just recreate aura + if (createInfo.GetAuraEffectMask() != foundAura->GetEffectMask()) + return nullptr; + + // update basepoints with new values - effect amount will be recalculated in ModStackAmount + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!foundAura->HasEffect(i)) + continue; + + int32 bp = foundAura->GetSpellInfo()->Effects[i].BasePoints; + if (createInfo.BaseAmount) + bp = *(createInfo.BaseAmount + i); + + int32* oldBP = const_cast(&(foundAura->GetEffect(i)->m_baseAmount)); + *oldBP = bp; + } + + // correct cast item guid if needed + if (castItemGUID != foundAura->GetCastItemGUID()) + { + ObjectGuid* oldGUID = const_cast(&foundAura->m_castItemGuid); + *oldGUID = castItemGUID; + } + + // try to increase stack amount + foundAura->ModStackAmount(1, AuraRemoveFlags::ByDefault, createInfo.ResetPeriodicTimer); + return foundAura; + } + } + + return nullptr; +} + +void Unit::_AddAura(UnitAura* aura, Unit* caster) +{ + ASSERT(!m_cleanupDone); + m_ownedAuras.emplace(aura->GetId(), aura); + + _RemoveNoStackAurasDueToAura(aura, true); + + if (aura->IsRemoved()) + return; + + aura->SetIsSingleTarget(caster && aura->GetSpellInfo()->IsSingleTarget()); + if (aura->IsSingleTarget()) + { + ASSERT((IsInWorld() && !IsDuringRemoveFromWorld()) || aura->GetCasterGUID() == GetGUID()); + /* @HACK: Player is not in world during loading auras. + * Single target auras are not saved or loaded from database + * but may be created as a result of aura links. + */ + + std::vector aurasSharingLimit; + // remove other single target auras + for (Aura* scAura : caster->GetSingleCastAuras()) + if (scAura->IsSingleTargetWith(aura)) + aurasSharingLimit.push_back(scAura); + + // register single target aura + caster->GetSingleCastAuras().push_front(aura); + + uint32 maxOtherAuras = aura->GetSpellInfo()->MaxAffectedTargets - 1; + while (aurasSharingLimit.size() > maxOtherAuras) + { + aurasSharingLimit.back()->Remove(); + aurasSharingLimit.pop_back(); + } + } +} + +// creates aura application instance and registers it in lists +// aura application effects are handled separately to prevent aura list corruption +AuraApplication* Unit::_CreateAuraApplication(Aura* aura, uint8 effMask) +{ + // VIANO_AURA_DEBUG_APPLICATION_V1 + if (aura && aura->GetId() == 465 && IsVianoAdventurer()) + TC_LOG_INFO("scripts", "[VianoAuraDebug] stage=CreateAuraApplication spell=%u target=%s", + aura->GetId(), GetGUID().ToString().c_str()); + + // can't apply aura on unit which is going to be deleted - to not create a memory leak + ASSERT(!m_cleanupDone); + + // just return if the aura has been already removed + // this can happen if OnEffectHitTarget() script hook killed the unit or the aura owner (which can be different) + if (aura->IsRemoved()) + { + TC_LOG_ERROR("spells", "Unit::_CreateAuraApplication() called with a removed aura. Check if OnEffectHitTarget() is triggering any spell with apply aura effect (that's not allowed!)"); + return nullptr; + } + + // aura mustn't be already applied on target + ASSERT (!aura->IsAppliedOnTarget(GetGUID()) && "Unit::_CreateAuraApplication: aura musn't be applied on target"); + + SpellInfo const* aurSpellInfo = aura->GetSpellInfo(); + uint32 aurId = aurSpellInfo->Id; + + // ghost spell check, allow apply any auras at player loading in ghost mode (will be cleanup after load) + if (!IsAlive() && !aurSpellInfo->IsDeathPersistent() && + (GetTypeId() != TYPEID_PLAYER || !ToPlayer()->GetSession()->PlayerLoading())) + return nullptr; + + Unit* caster = aura->GetCaster(); + + AuraApplication * aurApp = new AuraApplication(this, caster, aura, effMask); + m_appliedAuras.insert(AuraApplicationMap::value_type(aurId, aurApp)); + + if (aurSpellInfo->HasAnyAuraInterruptFlag()) + { + m_interruptableAuras.push_front(aurApp); + AddInterruptMask(aurSpellInfo->AuraInterruptFlags, aurSpellInfo->AuraInterruptFlags2); + } + + if (AuraStateType aState = aura->GetSpellInfo()->GetAuraState()) + m_auraStateAuras.insert(AuraStateAurasMap::value_type(aState, aurApp)); + + aura->_ApplyForTarget(this, caster, aurApp); + return aurApp; +} + +void Unit::_ApplyAuraEffect(Aura* aura, uint8 effIndex) +{ + ASSERT(aura); + ASSERT(aura->HasEffect(effIndex)); + AuraApplication * aurApp = aura->GetApplicationOfTarget(GetGUID()); + ASSERT(aurApp); + if (!aurApp->GetEffectMask()) + _ApplyAura(aurApp, 1 << effIndex); + else + aurApp->_HandleEffect(effIndex, true); +} + +// handles effects of aura application +// should be done after registering aura in lists +void Unit::_ApplyAura(AuraApplication* aurApp, uint8 effMask) +{ + Aura* aura = aurApp->GetBase(); + + _RemoveNoStackAurasDueToAura(aura, false); + + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + // Update target aura state flag + if (AuraStateType aState = aura->GetSpellInfo()->GetAuraState()) + ModifyAuraState(aState, true); + + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + // Sitdown on apply aura req seated + if (aura->GetSpellInfo()->HasAuraInterruptFlag(SpellAuraInterruptFlags::Standing) && !IsSitState()) + SetStandState(UNIT_STAND_STATE_SIT); + + Unit* caster = aura->GetCaster(); + + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + aura->HandleAuraSpecificMods(aurApp, caster, true, false); + + // apply effects of the aura + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (effMask & 1 << i && !aurApp->GetRemoveMode().HasAnyFlag()) + aurApp->_HandleEffect(i, true); + } + + if (Player* player = ToPlayer()) + { + if (sConditionMgr->IsSpellUsedInSpellClickConditions(aurApp->GetBase()->GetId())) + player->UpdateVisibleGameobjectsOrSpellClicks(); + + player->FailAchievementCriteria(AchievementCriteriaFailEvent::GainAura, aurApp->GetBase()->GetId()); + player->StartAchievementCriteria(AchievementCriteriaStartEvent::GainAura, aurApp->GetBase()->GetId()); + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET2, aurApp->GetBase()->GetId()); + } +} + +// removes aura application from lists and unapplies effects +void Unit::_UnapplyAura(AuraApplicationMap::iterator& i, AuraRemoveFlags removeMode) +{ + AuraApplication * aurApp = i->second; + ASSERT(aurApp); + ASSERT(!aurApp->GetRemoveMode().HasAnyFlag()); + ASSERT(aurApp->GetTarget() == this); + + aurApp->SetRemoveMode(removeMode); + Aura* aura = aurApp->GetBase(); + TC_LOG_DEBUG("spells", "Aura %u now is remove mode %u", aura->GetId(), static_cast(removeMode)); + + // dead loop is killing the server probably + ASSERT(m_removedAurasCount < 0xFFFFFFFF); + + ++m_removedAurasCount; + + Unit* caster = aura->GetCaster(); + + // Remove all pointers from lists here to prevent possible pointer invalidation on spellcast/auraapply/auraremove + m_appliedAuras.erase(i); + + if (aura->GetSpellInfo()->HasAnyAuraInterruptFlag()) + { + Trinity::Containers::Lists::RemoveUnique(m_interruptableAuras, aurApp); + UpdateInterruptMask(); + } + + bool auraStateFound = false; + AuraStateType auraState = aura->GetSpellInfo()->GetAuraState(); + if (auraState) + { + bool canBreak = false; + // Get mask of all aurastates from remaining auras + for (AuraStateAurasMap::iterator itr = m_auraStateAuras.lower_bound(auraState); itr != m_auraStateAuras.upper_bound(auraState) && !(auraStateFound && canBreak);) + { + if (itr->second == aurApp) + { + m_auraStateAuras.erase(itr); + itr = m_auraStateAuras.lower_bound(auraState); + canBreak = true; + continue; + } + auraStateFound = true; + ++itr; + } + } + + aurApp->_Remove(); + aura->_UnapplyForTarget(this, caster, aurApp); + + // remove effects of the spell - needs to be done after removing aura from lists + for (uint8 itr = 0; itr < MAX_SPELL_EFFECTS; ++itr) + { + if (aurApp->HasEffect(itr)) + aurApp->_HandleEffect(itr, false); + } + + // all effect mustn't be applied + ASSERT(!aurApp->GetEffectMask()); + + // Remove totem at next update if totem loses its aura + if (aurApp->GetRemoveMode().HasFlag(AuraRemoveFlags::Expired) && GetTypeId() == TYPEID_UNIT && IsTotem()) + { + if (ToTotem()->GetSpell() == aura->GetId() && ToTotem()->GetTotemType() == TOTEM_PASSIVE) + ToTotem()->setDeathState(JUST_DIED); + } + + // Remove aurastates only if were not found + if (!auraStateFound) + ModifyAuraState(auraState, false); + + aura->HandleAuraSpecificMods(aurApp, caster, false, false); + + if (Player* player = ToPlayer()) + { + if (sConditionMgr->IsSpellUsedInSpellClickConditions(aurApp->GetBase()->GetId())) + player->UpdateVisibleGameobjectsOrSpellClicks(); + + player->FailAchievementCriteria(AchievementCriteriaFailEvent::LoseAura, aurApp->GetBase()->GetId()); + } + + i = m_appliedAuras.begin(); +} + +void Unit::_UnapplyAura(AuraApplication* aurApp, AuraRemoveFlags removeMode) +{ + // aura can be removed from unit only if it's applied on it, shouldn't happen + ASSERT(aurApp->GetBase()->GetApplicationOfTarget(GetGUID()) == aurApp); + + uint32 spellId = aurApp->GetBase()->GetId(); + AuraApplicationMapBoundsNonConst range = m_appliedAuras.equal_range(spellId); + + for (AuraApplicationMap::iterator iter = range.first; iter != range.second;) + { + if (iter->second == aurApp) + { + _UnapplyAura(iter, removeMode); + return; + } + else + ++iter; + } + ABORT(); +} + +void Unit::_RemoveNoStackAurasDueToAura(Aura* aura, bool owned) +{ + SpellInfo const* spellProto = aura->GetSpellInfo(); + + // passive spell special case (only non stackable with ranks) + if (spellProto->IsPassiveStackableWithRanks()) + return; + + if (!IsHighestExclusiveAura(aura)) + { + aura->Remove(); + return; + } + + if (owned) + RemoveOwnedAuras([aura](Aura const* ownedAura) { return !aura->CanStackWith(ownedAura); }, AuraRemoveFlags::ByDefault); + else + RemoveAppliedAuras([aura](AuraApplication const* appliedAura) { return !aura->CanStackWith(appliedAura->GetBase()); }, AuraRemoveFlags::ByDefault); +} + +void Unit::_RegisterAuraEffect(AuraEffect* aurEff, bool apply) +{ + if (apply) + { + m_modAuras[aurEff->GetAuraType()].push_front(aurEff); + if (Player* player = ToPlayer()) + { + player->StartAchievementCriteria(AchievementCriteriaStartEvent::GainAuraEffect, aurEff->GetAuraType()); + player->FailAchievementCriteria(AchievementCriteriaFailEvent::GainAuraEffect, aurEff->GetAuraType()); + } + } + else + Trinity::Containers::Lists::RemoveUnique(m_modAuras[aurEff->GetAuraType()], aurEff); +} + +// All aura base removes should go through this function! +void Unit::RemoveOwnedAura(AuraMap::iterator& i, AuraRemoveFlags removeMode) +{ + Aura* aura = i->second; + ASSERT(!aura->IsRemoved()); + + // if unit currently update aura list then make safe update iterator shift to next + if (m_auraUpdateIterator == i) + ++m_auraUpdateIterator; + + m_ownedAuras.erase(i); + m_removedAuras.push_front(aura); + + // Unregister single target aura + if (aura->IsSingleTarget()) + aura->UnregisterSingleTarget(); + + aura->_Remove(removeMode); + + i = m_ownedAuras.begin(); +} + +void Unit::RemoveOwnedAura(uint32 spellId, ObjectGuid casterGUID, uint8 reqEffMask, AuraRemoveFlags removeMode) +{ + for (AuraMap::iterator itr = m_ownedAuras.lower_bound(spellId); itr != m_ownedAuras.upper_bound(spellId);) + if (((itr->second->GetEffectMask() & reqEffMask) == reqEffMask) && (!casterGUID || itr->second->GetCasterGUID() == casterGUID)) + { + RemoveOwnedAura(itr, removeMode); + itr = m_ownedAuras.lower_bound(spellId); + } + else + ++itr; +} + +void Unit::RemoveOwnedAura(Aura* aura, AuraRemoveFlags removeMode) +{ + if (aura->IsRemoved()) + return; + + ASSERT(aura->GetOwner() == this); + + if (removeMode == AuraRemoveFlags::None) + { + TC_LOG_ERROR("spells", "Unit::RemoveOwnedAura() called with unallowed removeMode AuraRemoveFlags::None, spellId %u", aura->GetId()); + return; + } + + uint32 spellId = aura->GetId(); + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + + for (AuraMap::iterator itr = range.first; itr != range.second; ++itr) + { + if (itr->second == aura) + { + RemoveOwnedAura(itr, removeMode); + return; + } + } + + ABORT(); +} + +Aura* Unit::GetOwnedAura(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask, Aura* except) const +{ + AuraMapBounds range = m_ownedAuras.equal_range(spellId); + for (AuraMap::const_iterator itr = range.first; itr != range.second; ++itr) + { + if (((itr->second->GetEffectMask() & reqEffMask) == reqEffMask) + && (!casterGUID || itr->second->GetCasterGUID() == casterGUID) + && (!itemCasterGUID || itr->second->GetCastItemGUID() == itemCasterGUID) + && (!except || except != itr->second)) + { + return itr->second; + } + } + return nullptr; +} + +void Unit::RemoveAura(AuraApplicationMap::iterator &i, AuraRemoveFlags mode) +{ + AuraApplication * aurApp = i->second; + // Do not remove aura which is already being removed + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + Aura* aura = aurApp->GetBase(); + _UnapplyAura(i, mode); + // Remove aura - for Area and Target auras + if (aura->GetOwner() == this) + aura->Remove(mode); +} + +void Unit::RemoveAura(uint32 spellId, ObjectGuid caster, uint8 reqEffMask, AuraRemoveFlags removeMode) +{ + AuraApplicationMapBoundsNonConst range = m_appliedAuras.equal_range(spellId); + for (AuraApplicationMap::iterator iter = range.first; iter != range.second;) + { + Aura const* aura = iter->second->GetBase(); + if (((aura->GetEffectMask() & reqEffMask) == reqEffMask) + && (!caster || aura->GetCasterGUID() == caster)) + { + RemoveAura(iter, removeMode); + return; + } + else + ++iter; + } +} + +void Unit::RemoveAura(AuraApplication * aurApp, AuraRemoveFlags mode) +{ + // we've special situation here, RemoveAura called while during aura removal + // this kind of call is needed only when aura effect removal handler + // or event triggered by it expects to remove + // not yet removed effects of an aura + if (aurApp->GetRemoveMode().HasAnyFlag()) + { + // remove remaining effects of an aura + for (uint8 itr = 0; itr < MAX_SPELL_EFFECTS; ++itr) + { + if (aurApp->HasEffect(itr)) + aurApp->_HandleEffect(itr, false); + } + return; + } + // no need to remove + if (aurApp->GetBase()->GetApplicationOfTarget(GetGUID()) != aurApp || aurApp->GetBase()->IsRemoved()) + return; + + uint32 spellId = aurApp->GetBase()->GetId(); + AuraApplicationMapBoundsNonConst range = m_appliedAuras.equal_range(spellId); + + for (AuraApplicationMap::iterator iter = range.first; iter != range.second;) + { + if (aurApp == iter->second) + { + RemoveAura(iter, mode); + return; + } + else + ++iter; + } +} + +void Unit::RemoveAura(Aura* aura, AuraRemoveFlags mode) +{ + if (aura->IsRemoved()) + return; + if (AuraApplication * aurApp = aura->GetApplicationOfTarget(GetGUID())) + RemoveAura(aurApp, mode); +} + +void Unit::RemoveAppliedAuras(std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + if (check(iter->second)) + { + RemoveAura(iter, removeMode); + continue; + } + ++iter; + } +} + +void Unit::RemoveOwnedAuras(std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + if (check(iter->second)) + { + RemoveOwnedAura(iter, removeMode); + continue; + } + ++iter; + } +} + +void Unit::RemoveAppliedAuras(uint32 spellId, std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.lower_bound(spellId); iter != m_appliedAuras.upper_bound(spellId);) + { + if (check(iter->second)) + { + RemoveAura(iter, removeMode); + iter = m_appliedAuras.lower_bound(spellId); + continue; + } + ++iter; + } +} + +void Unit::RemoveOwnedAuras(uint32 spellId, std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraMap::iterator iter = m_ownedAuras.lower_bound(spellId); iter != m_ownedAuras.upper_bound(spellId);) + { + if (check(iter->second)) + { + RemoveOwnedAura(iter, removeMode); + iter = m_ownedAuras.lower_bound(spellId); + continue; + } + ++iter; + } +} + +void Unit::RemoveAurasByType(AuraType auraType, std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraEffectList::iterator iter = m_modAuras[auraType].begin(); iter != m_modAuras[auraType].end();) + { + Aura* aura = (*iter)->GetBase(); + AuraApplication * aurApp = aura->GetApplicationOfTarget(GetGUID()); + ASSERT(aurApp); + + ++iter; + if (check(aurApp)) + { + uint32 removedAuras = m_removedAurasCount; + RemoveAura(aurApp, removeMode); + if (m_removedAurasCount > removedAuras + 1) + iter = m_modAuras[auraType].begin(); + } + } +} + +void Unit::RemoveAurasDueToSpell(uint32 spellId, ObjectGuid casterGUID, uint8 reqEffMask, AuraRemoveFlags removeMode) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.lower_bound(spellId); iter != m_appliedAuras.upper_bound(spellId);) + { + Aura const* aura = iter->second->GetBase(); + if (((aura->GetEffectMask() & reqEffMask) == reqEffMask) + && (!casterGUID || aura->GetCasterGUID() == casterGUID)) + { + RemoveAura(iter, removeMode); + iter = m_appliedAuras.lower_bound(spellId); + } + else + ++iter; + } +} + +void Unit::RemoveAuraFromStack(uint32 spellId, ObjectGuid casterGUID, AuraRemoveFlags removeMode) +{ + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + for (AuraMap::iterator iter = range.first; iter != range.second;) + { + Aura* aura = iter->second; + if ((aura->GetType() == UNIT_AURA_TYPE) + && (!casterGUID || aura->GetCasterGUID() == casterGUID)) + { + aura->ModStackAmount(-1, removeMode); + return; + } + else + ++iter; + } +} + +void Unit::RemoveAurasDueToSpellByDispel(uint32 spellId, uint32 dispellerSpellId, ObjectGuid casterGUID, WorldObject* dispeller, uint8 chargesRemoved/*= 1*/) +{ + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + for (AuraMap::iterator iter = range.first; iter != range.second;) + { + Aura* aura = iter->second; + if (aura->GetCasterGUID() == casterGUID) + { + DispelInfo dispelInfo(dispeller, dispellerSpellId, chargesRemoved); + + // Call OnDispel hook on AuraScript + aura->CallScriptDispel(&dispelInfo); + + if (aura->GetSpellInfo()->HasAttribute(SPELL_ATTR7_DISPEL_REMOVES_CHARGES)) + aura->ModCharges(-dispelInfo.GetRemovedCharges(), AuraRemoveFlags::ByEnemySpell); + else + aura->ModStackAmount(-dispelInfo.GetRemovedCharges(), AuraRemoveFlags::ByEnemySpell); + + // Call AfterDispel hook on AuraScript + aura->CallScriptAfterDispel(&dispelInfo); + + return; + } + else + ++iter; + } +} + +void Unit::RemoveAurasDueToSpellBySteal(uint32 spellId, ObjectGuid casterGUID, WorldObject* stealer, int32 stolenCharges /*= 1*/) +{ + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + for (AuraMap::iterator iter = range.first; iter != range.second;) + { + Aura* aura = iter->second; + if (aura->GetCasterGUID() == casterGUID) + { + int32 damage[MAX_SPELL_EFFECTS] = { }; + int32 baseDamage[MAX_SPELL_EFFECTS] = { }; + uint8 effMask = 0; + uint8 recalculateMask = 0; + Unit* caster = aura->GetCaster(); + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (aura->GetEffect(i)) + { + baseDamage[i] = aura->GetEffect(i)->GetBaseAmount(); + damage[i] = aura->GetEffect(i)->GetAmount(); + effMask |= (1<GetEffect(i)->CanBeRecalculated()) + recalculateMask |= (1<GetSpellInfo()->HasAttribute(SPELL_ATTR7_DISPEL_REMOVES_CHARGES); + // Cast duration to unsigned to prevent permanent aura's such as Righteous Fury being permanently added to caster + uint32 dur = std::min(2u * MINUTE * IN_MILLISECONDS, uint32(aura->GetDuration())); + + if (Unit* unitStealer = stealer->ToUnit()) + { + if (Aura* oldAura = unitStealer->GetAura(aura->GetId(), aura->GetCasterGUID())) + { + if (stealCharge) + oldAura->ModCharges(stolenCharges); + else + oldAura->ModStackAmount(stolenCharges); + oldAura->SetDuration(int32(dur)); + } + else + { + // single target state must be removed before aura creation to preserve existing single target aura + if (aura->IsSingleTarget()) + aura->UnregisterSingleTarget(); + + AuraCreateInfo createInfo(aura->GetSpellInfo(), effMask, stealer); + createInfo + .SetCasterGUID(aura->GetCasterGUID()) + .SetBaseAmount(baseDamage); + + if (Aura* newAura = Aura::TryRefreshStackOrCreate(createInfo)) + { + // created aura must not be single target aura,, so stealer won't loose it on recast + if (newAura->IsSingleTarget()) + { + newAura->UnregisterSingleTarget(); + // bring back single target aura status to the old aura + aura->SetIsSingleTarget(true); + caster->GetSingleCastAuras().push_front(aura); + } + // FIXME: using aura->GetMaxDuration() maybe not blizzlike but it fixes stealing of spells like Innervate + newAura->SetLoadedState(aura->GetMaxDuration(), int32(dur), stealCharge ? stolenCharges : aura->GetCharges(), stolenCharges, recalculateMask, aura->GetCritChance(), aura->CanApplyResilience(), &damage[0]); + newAura->ApplyForTargets(); + } + } + } + + if (stealCharge) + aura->ModCharges(-stolenCharges, AuraRemoveFlags::ByEnemySpell); + else + aura->ModStackAmount(-stolenCharges, AuraRemoveFlags::ByEnemySpell); + + return; + } + else + ++iter; + } +} + +void Unit::RemoveAurasDueToItemSpell(uint32 spellId, ObjectGuid castItemGuid) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.lower_bound(spellId); iter != m_appliedAuras.upper_bound(spellId);) + { + if (iter->second->GetBase()->GetCastItemGUID() == castItemGuid) + { + RemoveAura(iter); + iter = m_appliedAuras.lower_bound(spellId); + } + else + ++iter; + } +} + +void Unit::RemoveAurasByType(AuraType auraType, ObjectGuid casterGUID, Aura* except, bool negative, bool positive) +{ + for (AuraEffectList::iterator iter = m_modAuras[auraType].begin(); iter != m_modAuras[auraType].end();) + { + Aura* aura = (*iter)->GetBase(); + AuraApplication * aurApp = aura->GetApplicationOfTarget(GetGUID()); + ASSERT(aurApp); + + ++iter; + if (aura != except && (!casterGUID || aura->GetCasterGUID() == casterGUID) + && ((negative && !aurApp->IsPositive()) || (positive && aurApp->IsPositive()))) + { + uint32 removedAuras = m_removedAurasCount; + RemoveAura(aurApp); + if (m_removedAurasCount > removedAuras + 1) + iter = m_modAuras[auraType].begin(); + } + } +} + +void Unit::RemoveAurasWithAttribute(uint32 flags) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + SpellInfo const* spell = iter->second->GetBase()->GetSpellInfo(); + if (spell->Attributes & flags) + RemoveAura(iter); + else + ++iter; + } +} + +void Unit::RemoveNotOwnSingleTargetAuras(bool onPhaseChange /*= false*/) +{ + // single target auras from other casters + // Iterate m_ownedAuras - aura is marked as single target in Unit::AddAura (and pushed to m_ownedAuras). + // m_appliedAuras will NOT contain the aura before first Unit::Update after adding it to m_ownedAuras. + // Quickly removing such an aura will lead to it not being unregistered from caster's single cast auras container + // leading to assertion failures if the aura was cast on a player that can + // (and is changing map at the point where this function is called). + // Such situation occurs when player is logging in inside an instance and fails the entry check for any reason. + // The aura that was loaded from db (indirectly, via linked casts) gets removed before it has a chance + // to register in m_appliedAuras + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura const* aura = iter->second; + + if (aura->GetCasterGUID() != GetGUID() && aura->IsSingleTarget()) + { + if (!onPhaseChange) + RemoveOwnedAura(iter); + else + { + Unit* caster = aura->GetCaster(); + if (!caster || !caster->IsInPhase(this)) + RemoveOwnedAura(iter); + else + ++iter; + } + } + else + ++iter; + } + + // single target auras at other targets + AuraList& scAuras = GetSingleCastAuras(); + for (AuraList::iterator iter = scAuras.begin(); iter != scAuras.end();) + { + Aura* aura = *iter; + if (aura->GetUnitOwner() != this && (!onPhaseChange || !aura->GetUnitOwner()->IsInPhase(this))) + { + aura->Remove(); + iter = scAuras.begin(); + } + else + ++iter; + } +} + +template +bool IsInterruptFlagIgnoredForSpell(InterruptFlag /*flag*/, Unit const* /*unit*/, SpellInfo const* /*auraSpellInfo*/, SpellInfo const* /*interruptSource*/) +{ + return false; +} + +bool IsInterruptFlagIgnoredForSpell(SpellAuraInterruptFlags flag, Unit const* unit, SpellInfo const* auraSpellInfo, SpellInfo const* interruptSource) +{ + switch (flag) + { + case SpellAuraInterruptFlags::Moving: + return unit->HasAuraTypeWithAffectMask(SPELL_AURA_CAST_WHILE_WALKING, auraSpellInfo); + case SpellAuraInterruptFlags::Action: + case SpellAuraInterruptFlags::ActionDelayed: + if (interruptSource) + { + if (interruptSource->HasAttribute(SPELL_ATTR1_ALLOW_WHILE_STEALTHED) && auraSpellInfo->Dispel == DISPEL_STEALTH) + return true; + + if (interruptSource->HasAttribute(SPELL_ATTR2_ALLOW_WHILE_INVISIBLE) && auraSpellInfo->Dispel == DISPEL_INVISIBILITY) + return true; + } + break; + default: + break; + } + + return false; +} + +template +void Unit::RemoveAurasWithInterruptFlags(InterruptFlags flag, SpellInfo const* source) +{ + if (!HasInterruptFlag(flag)) + return; + + // interrupt auras + for (AuraApplicationList::iterator iter = m_interruptableAuras.begin(); iter != m_interruptableAuras.end();) + { + Aura* aura = (*iter)->GetBase(); + ++iter; + if (aura->GetSpellInfo()->HasAuraInterruptFlag(flag) + && (!source || aura->GetId() != source->Id) + && !IsInterruptFlagIgnoredForSpell(flag, this, aura->GetSpellInfo(), source)) + { + uint32 removedAuras = m_removedAurasCount; + RemoveAura(aura); + if (m_removedAurasCount > removedAuras + 1) + iter = m_interruptableAuras.begin(); + } + } + + // interrupt channeled spell + if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL]) + if (spell->getState() == SPELL_STATE_CHANNELING + && spell->GetSpellInfo()->HasChannelInterruptFlag(flag) + && (!source || spell->GetSpellInfo()->Id != source->Id) + && !IsInterruptFlagIgnoredForSpell(flag, this, spell->GetSpellInfo(), source)) + InterruptNonMeleeSpells(false, 0, true); + + UpdateInterruptMask(); +} + +template TC_GAME_API void Unit::RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags flag, SpellInfo const* source); +template TC_GAME_API void Unit::RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags2 flag, SpellInfo const* source); + +void Unit::RemoveAurasWithFamily(SpellFamilyNames family, uint32 familyFlag1, uint32 familyFlag2, uint32 familyFlag3, ObjectGuid casterGUID) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (!casterGUID || aura->GetCasterGUID() == casterGUID) + { + SpellInfo const* spell = aura->GetSpellInfo(); + if (spell->SpellFamilyName == uint32(family) && spell->SpellFamilyFlags.HasFlag(familyFlag1, familyFlag2, familyFlag3)) + { + RemoveAura(iter); + continue; + } + } + ++iter; + } +} + +void Unit::RemoveMovementImpairingAuras(bool withRoot) +{ + if (withRoot) + RemoveAurasWithMechanic(1 << MECHANIC_ROOT, AuraRemoveFlags::ByDefault, 0, true); + + RemoveAurasWithMechanic(1 << MECHANIC_ROOT, AuraRemoveFlags::ByDefault, 0, false); +} + +void Unit::RemoveAurasWithMechanic(uint32 mechanicMaskToRemove, AuraRemoveFlags removeMode, uint32 exceptSpellId, bool withEffectMechanics) +{ + std::vector aurasToUpdateTargets; + RemoveAppliedAuras([=, &aurasToUpdateTargets](AuraApplication const* aurApp) + { + Aura* aura = aurApp->GetBase(); + if (exceptSpellId && aura->GetId() == exceptSpellId) + return false; + + uint32 appliedMechanicMask = aura->GetSpellInfo()->GetSpellMechanicMaskByEffectMask(aurApp->GetEffectMask()); + if (!(appliedMechanicMask & mechanicMaskToRemove)) + return false; + + // spell mechanic matches required mask for removal + if ((1 << aura->GetSpellInfo()->Mechanic) & mechanicMaskToRemove || withEffectMechanics) + return true; + + // effect mechanic matches required mask for removal - don't remove, only update targets + aurasToUpdateTargets.push_back(aura); + return false; + }, removeMode); + + for (Aura* aura : aurasToUpdateTargets) + { + aura->UpdateTargetMap(aura->GetCaster()); + + // Fully remove the aura if all effects were removed + if (!aura->IsPassive() && aura->GetOwner() == this && !aura->GetApplicationOfTarget(GetGUID())) + aura->Remove(removeMode); + } +} + +void Unit::RemoveAurasByShapeShift() +{ + uint32 mechanic_mask = (1 << MECHANIC_SNARE) | (1 << MECHANIC_ROOT); + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if ((aura->GetSpellInfo()->GetAllEffectsMechanicMask() & mechanic_mask) && !aura->GetSpellInfo()->HasAttribute(SPELL_ATTR0_CU_AURA_CC)) + { + RemoveAura(iter); + continue; + } + ++iter; + } +} + +void Unit::RemoveAreaAurasDueToLeaveWorld() +{ + // make sure that all area auras not applied on self are removed - prevent access to deleted pointer later + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + ++iter; + Aura::ApplicationMap const& appMap = aura->GetApplicationMap(); + for (Aura::ApplicationMap::const_iterator itr = appMap.begin(); itr!= appMap.end();) + { + AuraApplication * aurApp = itr->second; + ++itr; + Unit* target = aurApp->GetTarget(); + if (target == this) + continue; + target->RemoveAura(aurApp); + // things linked on aura remove may apply new area aura - so start from the beginning + iter = m_ownedAuras.begin(); + } + } + + // remove area auras owned by others + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + if (iter->second->GetBase()->GetOwner() != this) + { + RemoveAura(iter); + } + else + ++iter; + } +} + +void Unit::RemoveAllAuras() +{ + // this may be a dead loop if some events on aura remove will continiously apply aura on remove + // we want to have all auras removed, so use your brain when linking events + for (uint32 counter = 0; !m_appliedAuras.empty() || !m_ownedAuras.empty(); counter++) + { + AuraApplicationMap::iterator aurAppIter; + for (aurAppIter = m_appliedAuras.begin(); aurAppIter != m_appliedAuras.end();) + _UnapplyAura(aurAppIter, AuraRemoveFlags::ByDefault); + + AuraMap::iterator aurIter; + for (aurIter = m_ownedAuras.begin(); aurIter != m_ownedAuras.end();) + RemoveOwnedAura(aurIter); + + const int maxIteration = 50; + // give this loop a few tries, if there are still auras then log as much information as possible + if (counter >= maxIteration) + { + std::stringstream sstr; + sstr << "Unit::RemoveAllAuras() iterated " << maxIteration << " times already but there are still " + << m_appliedAuras.size() << " m_appliedAuras and " << m_ownedAuras.size() << " m_ownedAuras.\n"; + + TC_LOG_ERROR("entities.unit", "%s", sstr.str().c_str()); + ASSERT(false); + + break; + } + } +} + +void Unit::RemoveArenaAuras() +{ + // in join, remove positive buffs, on end, remove negative + // used to remove positive visible auras in arenas + RemoveAppliedAuras([](AuraApplication const* aurApp) + { + Aura const* aura = aurApp->GetBase(); + return (!aura->GetSpellInfo()->HasAttribute(SPELL_ATTR4_ALLOW_ENTERING_ARENA) // don't remove stances, shadowform, pally/hunter auras + && !aura->IsPassive() // don't remove passive auras + && (aurApp->IsPositive() || !aura->GetSpellInfo()->HasAttribute(SPELL_ATTR3_ALLOW_AURA_WHILE_DEAD))) || // not negative death persistent auras + aura->GetSpellInfo()->HasAttribute(SPELL_ATTR5_REMOVE_ENTERING_ARENA); // special marker, always remove + }); +} + +void Unit::RemoveAurasOnEvade() +{ + if (IsCharmedOwnedByPlayerOrPlayer()) // if it is a player owned creature it should not remove the aura + return; + + // don't remove vehicle auras, passengers aren't supposed to drop off the vehicle + // don't remove clone caster on evade (to be verified) + auto evadeAuraCheck = [](Aura const* aura) + { + if (aura->HasEffectType(SPELL_AURA_CONTROL_VEHICLE)) + return false; + + if (aura->HasEffectType(SPELL_AURA_CLONE_CASTER)) + return false; + + if (aura->GetSpellInfo()->HasAttribute(SPELL_ATTR1_AURA_STAYS_AFTER_COMBAT)) + return false; + + return true; + }; + + auto evadeAuraApplicationCheck = [&evadeAuraCheck](AuraApplication const* aurApp) + { + return evadeAuraCheck(aurApp->GetBase()); + }; + + RemoveAppliedAuras(evadeAuraApplicationCheck); + RemoveOwnedAuras(evadeAuraCheck); +} + +void Unit::RemoveAllAurasOnDeath() +{ + // used just after dieing to remove all visible auras + // and disable the mods for the passive ones + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (!aura->IsPassive() && !aura->IsDeathPersistent()) + _UnapplyAura(iter, AuraRemoveFlags::ByDeath); + else + ++iter; + } + + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (!aura->IsPassive() && !aura->IsDeathPersistent()) + RemoveOwnedAura(iter, AuraRemoveFlags::ByDeath); + else + ++iter; + } +} + +void Unit::RemoveAllAurasRequiringDeadTarget() +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (!aura->IsPassive() && aura->GetSpellInfo()->IsRequiringDeadTarget()) + _UnapplyAura(iter, AuraRemoveFlags::ByDefault); + else + ++iter; + } + + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (!aura->IsPassive() && aura->GetSpellInfo()->IsRequiringDeadTarget()) + RemoveOwnedAura(iter, AuraRemoveFlags::ByDefault); + else + ++iter; + } +} + +void Unit::RemoveAllAurasExceptType(AuraType type) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (aura->GetSpellInfo()->HasAura(type)) + ++iter; + else + _UnapplyAura(iter, AuraRemoveFlags::ByDefault); + } + + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (aura->GetSpellInfo()->HasAura(type)) + ++iter; + else + RemoveOwnedAura(iter, AuraRemoveFlags::ByDefault); + } +} + +void Unit::RemoveAllAurasExceptType(AuraType type1, AuraType type2) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (aura->GetSpellInfo()->HasAura(type1) || aura->GetSpellInfo()->HasAura(type2)) + ++iter; + else + _UnapplyAura(iter, AuraRemoveFlags::ByDefault); + } + + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (aura->GetSpellInfo()->HasAura(type1) || aura->GetSpellInfo()->HasAura(type2)) + ++iter; + else + RemoveOwnedAura(iter, AuraRemoveFlags::ByDefault); + } +} + +void Unit::RemoveAllGroupBuffsFromCaster(ObjectGuid casterGUID) +{ + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (aura->GetCasterGUID() == casterGUID && aura->GetSpellInfo()->IsGroupBuff()) + { + RemoveOwnedAura(iter); + continue; + } + ++iter; + } +} + +void Unit::DelayOwnedAuras(uint32 spellId, ObjectGuid caster, int32 delaytime) +{ + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + for (; range.first != range.second; ++range.first) + { + Aura* aura = range.first->second; + if (!caster || aura->GetCasterGUID() == caster) + { + if (aura->GetDuration() < delaytime) + aura->SetDuration(0); + else + aura->SetDuration(aura->GetDuration() - delaytime); + + // update for out of range group members (on 1 slot use) + aura->SetNeedClientUpdateForTargets(); + } + } +} + +void Unit::_RemoveAllAuraStatMods() +{ + for (AuraApplicationMap::iterator i = m_appliedAuras.begin(); i != m_appliedAuras.end(); ++i) + (*i).second->GetBase()->HandleAllEffects(i->second, AURA_EFFECT_HANDLE_STAT, false); +} + +void Unit::_ApplyAllAuraStatMods() +{ + for (AuraApplicationMap::iterator i = m_appliedAuras.begin(); i != m_appliedAuras.end(); ++i) + (*i).second->GetBase()->HandleAllEffects(i->second, AURA_EFFECT_HANDLE_STAT, true); +} + +bool Unit::HasSingleCastAuraOfSpell(uint32 spellId) const +{ + AuraList const& scAuras = GetSingleCastAuras(); + for (Aura const* aura : scAuras) + if (aura->GetSpellInfo()->Id == spellId) + return true; + + return false; +} + +AuraEffect* Unit::GetAuraEffect(uint32 spellId, uint8 effIndex, ObjectGuid caster) const +{ + AuraApplicationMapBounds range = m_appliedAuras.equal_range(spellId); + for (AuraApplicationMap::const_iterator itr = range.first; itr != range.second; ++itr) + { + if (itr->second->HasEffect(effIndex) + && (!caster || itr->second->GetBase()->GetCasterGUID() == caster)) + { + return itr->second->GetBase()->GetEffect(effIndex); + } + } + return nullptr; +} + +AuraEffect* Unit::GetAuraEffectOfRankedSpell(uint32 spellId, uint8 effIndex, ObjectGuid caster) const +{ + uint32 rankSpell = sSpellMgr->GetFirstSpellInChain(spellId); + while (rankSpell) + { + if (AuraEffect* aurEff = GetAuraEffect(rankSpell, effIndex, caster)) + return aurEff; + rankSpell = sSpellMgr->GetNextSpellInChain(rankSpell); + } + return nullptr; +} + +AuraEffect* Unit::GetAuraEffect(AuraType type, SpellFamilyNames name, uint32 iconId, uint8 effIndex) const +{ + AuraEffectList const& auras = GetAuraEffectsByType(type); + for (Unit::AuraEffectList::const_iterator itr = auras.begin(); itr != auras.end(); ++itr) + { + if (effIndex != (*itr)->GetEffIndex()) + continue; + SpellInfo const* spell = (*itr)->GetSpellInfo(); + if (spell->SpellIconID == iconId && spell->SpellFamilyName == uint32(name) && !spell->SpellFamilyFlags) + return *itr; + } + return nullptr; +} + +AuraEffect* Unit::GetAuraEffect(AuraType type, SpellFamilyNames family, uint32 familyFlag1, uint32 familyFlag2, uint32 familyFlag3, ObjectGuid casterGUID) const +{ + AuraEffectList const& auras = GetAuraEffectsByType(type); + for (AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + SpellInfo const* spell = (*i)->GetSpellInfo(); + if (spell->SpellFamilyName == uint32(family) && spell->SpellFamilyFlags.HasFlag(familyFlag1, familyFlag2, familyFlag3)) + { + if (casterGUID && (*i)->GetCasterGUID() != casterGUID) + continue; + return (*i); + } + } + return nullptr; +} + +AuraEffect* Unit::GetDummyAuraEffect(SpellFamilyNames name, uint32 iconId, uint8 effIndex) const +{ + return GetAuraEffect(SPELL_AURA_DUMMY, name, iconId, effIndex); +} + +AuraApplication * Unit::GetAuraApplication(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask, AuraApplication * except) const +{ + AuraApplicationMapBounds range = m_appliedAuras.equal_range(spellId); + for (; range.first != range.second; ++range.first) + { + AuraApplication* app = range.first->second; + Aura const* aura = app->GetBase(); + + if (((aura->GetEffectMask() & reqEffMask) == reqEffMask) + && (!casterGUID || aura->GetCasterGUID() == casterGUID) + && (!itemCasterGUID || aura->GetCastItemGUID() == itemCasterGUID) + && (!except || except != app)) + { + return app; + } + } + return nullptr; +} + +Aura* Unit::GetAura(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask) const +{ + AuraApplication * aurApp = GetAuraApplication(spellId, casterGUID, itemCasterGUID, reqEffMask); + return aurApp ? aurApp->GetBase() : nullptr; +} + +AuraApplication * Unit::GetAuraApplicationOfRankedSpell(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask, AuraApplication* except) const +{ + uint32 rankSpell = sSpellMgr->GetFirstSpellInChain(spellId); + while (rankSpell) + { + if (AuraApplication * aurApp = GetAuraApplication(rankSpell, casterGUID, itemCasterGUID, reqEffMask, except)) + return aurApp; + rankSpell = sSpellMgr->GetNextSpellInChain(rankSpell); + } + return nullptr; +} + +Aura* Unit::GetAuraOfRankedSpell(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask) const +{ + AuraApplication * aurApp = GetAuraApplicationOfRankedSpell(spellId, casterGUID, itemCasterGUID, reqEffMask); + return aurApp ? aurApp->GetBase() : nullptr; +} + +void Unit::GetDispellableAuraList(WorldObject const* caster, uint32 dispelMask, DispelChargesList& dispelList, bool isReflect /*= false*/) const +{ + // we should not be able to dispel diseases if the target is affected by unholy blight + if (dispelMask & (1 << DISPEL_DISEASE) && HasAura(50536)) + dispelMask &= ~(1 << DISPEL_DISEASE); + + AuraMap const& auras = GetOwnedAuras(); + for (auto itr = auras.begin(); itr != auras.end(); ++itr) + { + Aura* aura = itr->second; + AuraApplication const* aurApp = aura->GetApplicationOfTarget(GetGUID()); + if (!aurApp) + continue; + + // don't try to remove passive auras + if (aura->IsPassive()) + continue; + + if (aura->GetSpellInfo()->GetDispelMask() & dispelMask) + { + // do not remove positive auras if friendly target + // negative auras if non-friendly + // unless we're reflecting (dispeller eliminates one of it's benefitial buffs) + if (isReflect != (aurApp->IsPositive() == IsFriendlyTo(caster))) + continue; + + // 2.4.3 Patch Notes: "Dispel effects will no longer attempt to remove effects that have 100% dispel resistance." + int32 chance = aura->CalcDispelChance(this, !IsFriendlyTo(caster)); + if (!chance) + continue; + + // The charges / stack amounts don't count towards the total number of auras that can be dispelled. + // Ie: A dispel on a target with 5 stacks of Winters Chill and a Polymorph has 1 / (1 + 1) -> 50% chance to dispell + // Polymorph instead of 1 / (5 + 1) -> 16%. + bool const dispelCharges = aura->GetSpellInfo()->HasAttribute(SPELL_ATTR7_DISPEL_REMOVES_CHARGES); + uint8 charges = dispelCharges ? aura->GetCharges() : aura->GetStackAmount(); + if (charges > 0) + dispelList.emplace_back(aura, chance, charges); + } + } +} + +bool Unit::HasAuraEffect(uint32 spellId, uint8 effIndex, ObjectGuid caster) const +{ + AuraApplicationMapBounds range = m_appliedAuras.equal_range(spellId); + for (AuraApplicationMap::const_iterator itr = range.first; itr != range.second; ++itr) + { + if (itr->second->HasEffect(effIndex) + && (!caster || itr->second->GetBase()->GetCasterGUID() == caster)) + { + return true; + } + } + return false; +} + +uint32 Unit::GetAuraCount(uint32 spellId) const +{ + uint32 count = 0; + AuraApplicationMapBounds range = m_appliedAuras.equal_range(spellId); + + for (AuraApplicationMap::const_iterator itr = range.first; itr != range.second; ++itr) + { + if (itr->second->GetBase()->GetStackAmount() == 0) + ++count; + else + count += (uint32)itr->second->GetBase()->GetStackAmount(); + } + + return count; +} + +bool Unit::HasAura(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask) const +{ + if (GetAuraApplication(spellId, casterGUID, itemCasterGUID, reqEffMask)) + return true; + return false; +} + +bool Unit::HasAuraType(AuraType auraType) const +{ + return (!m_modAuras[auraType].empty()); +} + +bool Unit::HasAuraTypeWithCaster(AuraType auraType, ObjectGuid caster) const +{ + for (AuraEffect const* eff : GetAuraEffectsByType(auraType)) + if (caster == eff->GetCasterGUID()) + return true; + return false; +} + +bool Unit::HasAuraTypeWithMiscvalue(AuraType auraType, int32 miscvalue) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator i = mTotalAuraList.begin(); i != mTotalAuraList.end(); ++i) + if (miscvalue == (*i)->GetMiscValue()) + return true; + return false; +} + +bool Unit::HasAuraTypeWithAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator i = mTotalAuraList.begin(); i != mTotalAuraList.end(); ++i) + if ((*i)->IsAffectingSpell(affectedSpell)) + return true; + return false; +} + +bool Unit::HasAuraTypeWithValue(AuraType auraType, int32 value) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator i = mTotalAuraList.begin(); i != mTotalAuraList.end(); ++i) + if (value == (*i)->GetAmount()) + return true; + return false; +} + +template +bool Unit::HasNegativeAuraWithInterruptFlag(InterruptFlags flag, ObjectGuid guid) const +{ + if (!HasInterruptFlag(flag)) + return false; + + for (AuraApplicationList::const_iterator iter = m_interruptableAuras.begin(); iter != m_interruptableAuras.end(); ++iter) + { + if (!(*iter)->IsPositive() && (*iter)->GetBase()->GetSpellInfo()->HasAuraInterruptFlag(flag) && (!guid || (*iter)->GetBase()->GetCasterGUID() == guid)) + return true; + } + return false; +} + +bool Unit::HasAuraWithMechanic(uint32 mechanicMask) const +{ + for (AuraApplicationMap::const_iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end(); ++iter) + { + SpellInfo const* spellInfo = iter->second->GetBase()->GetSpellInfo(); + if (spellInfo->Mechanic && (mechanicMask & (1 << spellInfo->Mechanic))) + return true; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (iter->second->HasEffect(i) && spellInfo->Effects[i].Effect && spellInfo->Effects[i].Mechanic) + if (mechanicMask & (1 << spellInfo->Effects[i].Mechanic)) + return true; + } + + return false; +} + +bool Unit::HasStrongerAuraWithDR(SpellInfo const* auraSpellInfo, Unit* caster, bool triggered) const +{ + DiminishingGroup diminishGroup = auraSpellInfo->GetDiminishingReturnsGroupForSpell(triggered); + DiminishingLevels level = GetDiminishing(diminishGroup); + for (auto itr = m_appliedAuras.begin(); itr != m_appliedAuras.end(); ++itr) + { + SpellInfo const* spellInfo = itr->second->GetBase()->GetSpellInfo(); + if (spellInfo->GetDiminishingReturnsGroupForSpell(triggered) != diminishGroup) + continue; + + int32 existingDuration = itr->second->GetBase()->GetDuration(); + int32 newDuration = auraSpellInfo->GetMaxDuration(); + ApplyDiminishingToDuration(auraSpellInfo, triggered, newDuration, caster, level); + if (newDuration > 0 && newDuration < existingDuration) + return true; + } + + return false; +} + +AuraEffect* Unit::IsScriptOverriden(SpellInfo const* spell, int32 script) const +{ + AuraEffectList const& auras = GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS); + for (AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + if ((*i)->GetMiscValue() == script) + if ((*i)->IsAffectingSpell(spell)) + return (*i); + } + return nullptr; +} + +uint32 Unit::GetDiseasesByCaster(ObjectGuid casterGUID, bool remove) +{ + static const AuraType diseaseAuraTypes[] = + { + SPELL_AURA_PERIODIC_DAMAGE, // Frost Fever and Blood Plague + SPELL_AURA_LINKED // Crypt Fever and Ebon Plague + }; + + // Burning Blood - Blood DK T12 Bonus + // "Causes owning Death Knight's abilities to treat the target as if it had 2 diseases active." + if (HasAura(98957, casterGUID)) + return 2; + + uint32 diseases = 0; + for (AuraType aType : diseaseAuraTypes) + { + for (auto itr = m_modAuras[aType].begin(); itr != m_modAuras[aType].end();) + { + // Get auras with disease dispel type by caster + if ((*itr)->GetSpellInfo()->Dispel == DISPEL_DISEASE + && (*itr)->GetCasterGUID() == casterGUID) + { + ++diseases; + + if (remove) + { + RemoveAura((*itr)->GetId(), (*itr)->GetCasterGUID()); + itr = m_modAuras[aType].begin(); + continue; + } + } + ++itr; + } + } + return diseases; +} + +uint32 Unit::GetDoTsByCaster(ObjectGuid casterGUID) const +{ + static const AuraType diseaseAuraTypes[] = + { + SPELL_AURA_PERIODIC_DAMAGE, + SPELL_AURA_PERIODIC_DAMAGE_PERCENT, + SPELL_AURA_NONE + }; + + uint32 dots = 0; + for (AuraType const* itr = &diseaseAuraTypes[0]; itr && itr[0] != SPELL_AURA_NONE; ++itr) + { + Unit::AuraEffectList const& auras = GetAuraEffectsByType(*itr); + for (AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + // Get auras by caster + if ((*i)->GetCasterGUID() == casterGUID) + ++dots; + } + } + return dots; +} + +int32 Unit::GetTotalAuraModifier(AuraType auraType, std::function const& predicate) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + if (mTotalAuraList.empty()) + return 0; + + std::map sameEffectSpellGroup; + int32 modifier = 0; + + for (AuraEffect const* aurEff : mTotalAuraList) + { + if (predicate(aurEff)) + { + // Check if the Aura Effect has a the Same Effect Stack Rule and if so, use the highest amount of that SpellGroup + // If the Aura Effect does not have this Stack Rule, it returns false so we can add to the multiplier as usual + if (!sSpellMgr->AddSameEffectStackRuleSpellGroups(aurEff->GetSpellInfo(), static_cast(auraType), aurEff->GetAmount(), sameEffectSpellGroup)) + modifier += aurEff->GetAmount(); + } + } + + // Add the highest of the Same Effect Stack Rule SpellGroups to the accumulator + for (auto itr = sameEffectSpellGroup.begin(); itr != sameEffectSpellGroup.end(); ++itr) + modifier += itr->second; + + return modifier; +} + +float Unit::GetTotalAuraMultiplier(AuraType auraType, std::function const& predicate) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + if (mTotalAuraList.empty()) + return 1.0f; + + std::map sameEffectSpellGroup; + float multiplier = 1.0f; + + for (AuraEffect const* aurEff : mTotalAuraList) + { + if (predicate(aurEff)) + { + // Check if the Aura Effect has a the Same Effect Stack Rule and if so, use the highest amount of that SpellGroup + // If the Aura Effect does not have this Stack Rule, it returns false so we can add to the multiplier as usual + if (!sSpellMgr->AddSameEffectStackRuleSpellGroups(aurEff->GetSpellInfo(), static_cast(auraType), aurEff->GetAmount(), sameEffectSpellGroup)) + AddPct(multiplier, aurEff->GetAmount()); + } + } + + // Add the highest of the Same Effect Stack Rule SpellGroups to the multiplier + for (auto itr = sameEffectSpellGroup.begin(); itr != sameEffectSpellGroup.end(); ++itr) + AddPct(multiplier, itr->second); + + return multiplier; +} + +int32 Unit::GetMaxPositiveAuraModifier(AuraType auraType, std::function const& predicate) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + if (mTotalAuraList.empty()) + return 0; + + int32 modifier = 0; + for (AuraEffect const* aurEff : mTotalAuraList) + { + if (predicate(aurEff)) + modifier = std::max(modifier, aurEff->GetAmount()); + } + + return modifier; +} + +int32 Unit::GetMaxNegativeAuraModifier(AuraType auraType, std::function const& predicate) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + if (mTotalAuraList.empty()) + return 0; + + int32 modifier = 0; + for (AuraEffect const* aurEff : mTotalAuraList) + { + if (predicate(aurEff)) + modifier = std::min(modifier, aurEff->GetAmount()); + } + + return modifier; +} + +int32 Unit::GetTotalAuraModifier(AuraType auraType) const +{ + return GetTotalAuraModifier(auraType, [](AuraEffect const* /*aurEff*/) { return true; }); +} + +float Unit::GetTotalAuraMultiplier(AuraType auraType) const +{ + return GetTotalAuraMultiplier(auraType, [](AuraEffect const* /*aurEff*/) { return true; }); +} + +int32 Unit::GetMaxPositiveAuraModifier(AuraType auraType) const +{ + return GetMaxPositiveAuraModifier(auraType, [](AuraEffect const* /*aurEff*/) { return true; }); +} + +int32 Unit::GetMaxNegativeAuraModifier(AuraType auraType) const +{ + return GetMaxNegativeAuraModifier(auraType, [](AuraEffect const* /*aurEff*/) { return true; }); +} + +int32 Unit::GetTotalAuraModifierByMiscMask(AuraType auraType, uint32 miscMask) const +{ + return GetTotalAuraModifier(auraType, [miscMask](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & miscMask) != 0) + return true; + return false; + }); +} + +float Unit::GetTotalAuraMultiplierByMiscMask(AuraType auraType, uint32 miscMask) const +{ + return GetTotalAuraMultiplier(auraType, [miscMask](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & miscMask) != 0) + return true; + return false; + }); +} + +int32 Unit::GetMaxPositiveAuraModifierByMiscMask(AuraType auraType, uint32 miscMask, AuraEffect const* except /*= nullptr*/) const +{ + return GetMaxPositiveAuraModifier(auraType, [miscMask, except](AuraEffect const* aurEff) -> bool + { + if (except != aurEff && (aurEff->GetMiscValue() & miscMask) != 0) + return true; + return false; + }); +} + +int32 Unit::GetMaxNegativeAuraModifierByMiscMask(AuraType auraType, uint32 miscMask) const +{ + return GetMaxNegativeAuraModifier(auraType, [miscMask](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & miscMask) != 0) + return true; + return false; + }); +} + +int32 Unit::GetTotalAuraModifierByMiscValue(AuraType auraType, int32 miscValue) const +{ + return GetTotalAuraModifier(auraType, [miscValue](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetMiscValue() == miscValue) + return true; + return false; + }); +} + +float Unit::GetTotalAuraMultiplierByMiscValue(AuraType auraType, int32 miscValue) const +{ + return GetTotalAuraMultiplier(auraType, [miscValue](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetMiscValue() == miscValue) + return true; + return false; + }); +} + +int32 Unit::GetMaxPositiveAuraModifierByMiscValue(AuraType auraType, int32 miscValue) const +{ + return GetMaxPositiveAuraModifier(auraType, [miscValue](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetMiscValue() == miscValue) + return true; + return false; + }); +} + +int32 Unit::GetMaxNegativeAuraModifierByMiscValue(AuraType auraType, int32 miscValue) const +{ + return GetMaxNegativeAuraModifier(auraType, [miscValue](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetMiscValue() == miscValue) + return true; + return false; + }); +} + +int32 Unit::GetTotalAuraModifierByAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + return GetTotalAuraModifier(auraType, [affectedSpell](AuraEffect const* aurEff) -> bool + { + if (aurEff->IsAffectingSpell(affectedSpell)) + return true; + return false; + }); +} + +float Unit::GetTotalAuraMultiplierByAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + return GetTotalAuraMultiplier(auraType, [affectedSpell](AuraEffect const* aurEff) -> bool + { + if (aurEff->IsAffectingSpell(affectedSpell)) + return true; + return false; + }); +} + +int32 Unit::GetMaxPositiveAuraModifierByAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + return GetMaxPositiveAuraModifier(auraType, [affectedSpell](AuraEffect const* aurEff) -> bool + { + if (aurEff->IsAffectingSpell(affectedSpell)) + return true; + return false; + }); +} + +int32 Unit::GetMaxNegativeAuraModifierByAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + return GetMaxNegativeAuraModifier(auraType, [affectedSpell](AuraEffect const* aurEff) -> bool + { + if (aurEff->IsAffectingSpell(affectedSpell)) + return true; + return false; + }); +} + +void Unit::UpdateResistanceBuffModsMod(SpellSchools school) +{ + float modPos = 0.0f; + float modNeg = 0.0f; + + // these auras are always positive + modPos = GetMaxPositiveAuraModifierByMiscMask(SPELL_AURA_MOD_RESISTANCE_EXCLUSIVE, 1 << school); + modPos += GetTotalAuraModifier(SPELL_AURA_MOD_RESISTANCE, [school](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & (1 << school)) && aurEff->GetAmount() > 0) + return true; + return false; + }); + + modNeg = GetTotalAuraModifier(SPELL_AURA_MOD_RESISTANCE, [school](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & (1 << school)) && aurEff->GetAmount() < 0) + return true; + return false; + }); + + float factor = GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_RESISTANCE_PCT, 1 << school); + modPos *= factor; + modNeg *= factor; + + SetFloatValue(UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + AsUnderlyingType(school), modPos); + SetFloatValue(UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + AsUnderlyingType(school), modNeg); +} + +void Unit::_RegisterDynObject(DynamicObject* dynObj) +{ + m_dynObj.push_back(dynObj); + if (GetTypeId() == TYPEID_UNIT && IsAIEnabled()) + ToCreature()->AI()->JustRegisteredDynObject(dynObj); +} + +void Unit::_UnregisterDynObject(DynamicObject* dynObj) +{ + m_dynObj.remove(dynObj); + if (GetTypeId() == TYPEID_UNIT && IsAIEnabled()) + ToCreature()->AI()->JustUnregisteredDynObject(dynObj); +} + +DynamicObject* Unit::GetDynObject(uint32 spellId) const +{ + std::vector dynamicobjects = GetDynObjects(spellId); + return dynamicobjects.empty() ? nullptr : dynamicobjects.front(); +} + +std::vector Unit::GetDynObjects(uint32 spellId) const +{ + std::vector dynamicobjects; + for (DynObjectList::const_iterator i = m_dynObj.begin(); i != m_dynObj.end(); ++i) + if ((*i)->GetSpellId() == spellId) + dynamicobjects.push_back(*i); + + return dynamicobjects; +} + +void Unit::RemoveDynObject(uint32 spellId) +{ + if (m_dynObj.empty()) + return; + for (DynObjectList::iterator i = m_dynObj.begin(); i != m_dynObj.end();) + { + DynamicObject* dynObj = *i; + if (dynObj->GetSpellId() == spellId) + { + dynObj->Remove(); + i = m_dynObj.begin(); + } + else + ++i; + } +} + +void Unit::RemoveAllDynObjects() +{ + while (!m_dynObj.empty()) + m_dynObj.front()->Remove(); +} + +GameObject* Unit::GetGameObject(uint32 spellId) const +{ + std::vector gameobjects = GetGameObjects(spellId); + return gameobjects.empty() ? nullptr : gameobjects.front(); +} + +std::vector Unit::GetGameObjects(uint32 spellId) const +{ + std::vector gameobjects; + for (GameObjectList::const_iterator i = m_gameObj.begin(); i != m_gameObj.end(); ++i) + if ((*i)->GetSpellId() == spellId) + gameobjects.push_back(*i); + + return gameobjects; +} + +void Unit::AddGameObject(GameObject* gameObj) +{ + if (!gameObj || gameObj->GetOwnerGUID()) + return; + + m_gameObj.push_back(gameObj); + gameObj->SetOwnerGUID(GetGUID()); + + if (gameObj->GetSpellId()) + { + SpellInfo const* createBySpell = sSpellMgr->GetSpellInfo(gameObj->GetSpellId()); + // Need disable spell use for owner + if (createBySpell && createBySpell->IsCooldownStartedOnEvent()) + // note: item based cooldowns and cooldown spell mods with charges ignored (unknown existing cases) + GetSpellHistory()->StartCooldown(createBySpell, 0, nullptr, true); + } + + if (GetTypeId() == TYPEID_UNIT && ToCreature()->IsAIEnabled()) + ToCreature()->AI()->JustSummonedGameobject(gameObj); +} + +void Unit::RemoveGameObject(GameObject* gameObj, bool del) +{ + if (!gameObj || gameObj->GetOwnerGUID() != GetGUID()) + return; + + gameObj->SetOwnerGUID(ObjectGuid::Empty); + + for (uint8 i = 0; i < MAX_GAMEOBJECT_SLOT; ++i) + { + if (m_ObjectSlot[i] == gameObj->GetGUID()) + { + m_ObjectSlot[i].Clear(); + break; + } + } + + // GO created by some spell + if (uint32 spellid = gameObj->GetSpellId()) + { + RemoveAurasDueToSpell(spellid); + + SpellInfo const* createBySpell = sSpellMgr->GetSpellInfo(spellid); + // Need activate spell use for owner + if (createBySpell && createBySpell->IsCooldownStartedOnEvent()) + // note: item based cooldowns and cooldown spell mods with charges ignored (unknown existing cases) + GetSpellHistory()->SendCooldownEvent(createBySpell); + } + + m_gameObj.remove(gameObj); + + if (GetTypeId() == TYPEID_UNIT && ToCreature()->IsAIEnabled()) + ToCreature()->AI()->SummonedGameobjectDespawn(gameObj); + + if (del) + { + gameObj->SetRespawnTime(0); + gameObj->Delete(); + } +} + +void Unit::RemoveGameObject(uint32 spellid, bool del) +{ + if (m_gameObj.empty()) + return; + GameObjectList::iterator i, next; + for (i = m_gameObj.begin(); i != m_gameObj.end(); i = next) + { + next = i; + if (spellid == 0 || (*i)->GetSpellId() == spellid) + { + (*i)->SetOwnerGUID(ObjectGuid::Empty); + if (del) + { + (*i)->SetRespawnTime(0); + (*i)->Delete(); + } + + next = m_gameObj.erase(i); + } + else + ++next; + } +} + +void Unit::RemoveAllGameObjects() +{ + // remove references to unit + while (!m_gameObj.empty()) + { + GameObjectList::iterator i = m_gameObj.begin(); + (*i)->SetOwnerGUID(ObjectGuid::Empty); + (*i)->SetRespawnTime(0); + (*i)->Delete(); + m_gameObj.erase(i); + } +} + +void Unit::SendSpellNonMeleeDamageLog(SpellNonMeleeDamage* log) +{ + WorldPacket data(SMSG_SPELLNONMELEEDAMAGELOG, (16+4+4+4+1+4+4+1+1+4+4+1)); // we guess size + data << log->target->GetPackGUID(); + data << log->attacker->GetPackGUID(); + data << uint32(log->SpellID); + data << uint32(log->damage); // damage amount + int32 overkill = log->damage - log->target->GetHealth(); + data << uint32(overkill > 0 ? overkill : 0); // overkill + data << uint8 (log->schoolMask); // damage school + data << uint32(log->absorb); // AbsorbedDamage + data << uint32(log->resist); // resist + data << uint8 (log->periodicLog); // if 1, then client show spell name (example: %s's ranged shot hit %s for %u school or %s suffers %u school damage from %s's spell_name + data << uint8 (log->unused); // unused + data << uint32(log->blocked); // blocked + data << uint32(log->HitInfo); + data << uint8(log->HitInfo & (SPELL_HIT_TYPE_CRIT_DEBUG | SPELL_HIT_TYPE_HIT_DEBUG | SPELL_HIT_TYPE_ATTACK_TABLE_DEBUG)); + //if (log->HitInfo & SPELL_HIT_TYPE_CRIT_DEBUG) + //{ + // data << float(log->CritRoll); + // data << float(log->CritNeeded); + //} + //if (log->HitInfo & SPELL_HIT_TYPE_HIT_DEBUG) + //{ + // data << float(log->HitRoll); + // data << float(log->HitNeeded); + //} + //if (log->HitInfo & SPELL_HIT_TYPE_ATTACK_TABLE_DEBUG) + //{ + // data << float(log->MissChance); + // data << float(log->DodgeChance); + // data << float(log->ParryChance); + // data << float(log->BlockChance); + // data << float(log->GlanceChance); + // data << float(log->CrushChance); + //} + SendMessageToSet(&data, true); +} + +void Unit::SendSpellNonMeleeDamageLog(Unit* target, uint32 spellID, uint32 damage, SpellSchoolMask damageSchoolMask, uint32 absorbedDamage, uint32 resist, bool isPeriodic, uint32 blocked, bool criticalHit, bool split) +{ + SpellNonMeleeDamage log(this, target, spellID, damageSchoolMask); + log.damage = damage - absorbedDamage - resist - blocked; + log.absorb = absorbedDamage; + log.resist = resist; + log.periodicLog = isPeriodic; + log.blocked = blocked; + log.HitInfo = 0; + if (criticalHit) + log.HitInfo |= SPELL_HIT_TYPE_CRIT; + if (split) + log.HitInfo |= SPELL_HIT_TYPE_SPLIT; + SendSpellNonMeleeDamageLog(&log); +} + +/*static*/ void Unit::ProcSkillsAndAuras(Unit* actor, Unit* actionTarget, ProcFlags typeMaskActor, ProcFlags typeMaskActionTarget, + ProcFlagsSpellType spellTypeMask, ProcFlagsSpellPhase spellPhaseMask, ProcFlagsHit hitMask, Spell* spell, DamageInfo* damageInfo, HealInfo* healInfo) +{ + static constexpr int32 ProcChainHardLimit = 10; + if (spell && spell->GetProcChainLength() >= ProcChainHardLimit) + { + TC_LOG_ERROR("spells.aura.effect", "Unit::ProcSkillsAndAuras: Possible infinite proc loop detected, current triggering spell %u", spell->GetSpellInfo()->Id); + return; + } + + WeaponAttackType attType = damageInfo ? damageInfo->GetAttackType() : BASE_ATTACK; + SpellInfo const* spellInfo = [&]() -> SpellInfo const* + { + if (spell) + return spell->GetSpellInfo(); + if (damageInfo) + return damageInfo->GetSpellInfo(); + if (healInfo) + return healInfo->GetSpellInfo(); + return nullptr; + }(); + + if (typeMaskActor && actor && !(spellInfo && spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_CASTER_PROCS))) + actor->ProcSkillsAndReactives(false, actionTarget, typeMaskActor, hitMask, attType); + + if (typeMaskActionTarget && actionTarget && !(spellInfo && spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS))) + actionTarget->ProcSkillsAndReactives(true, actor, typeMaskActionTarget, hitMask, attType); + + if (actor) + actor->TriggerAurasProcOnEvent(actionTarget, typeMaskActor, typeMaskActionTarget, spellTypeMask, spellPhaseMask, hitMask, spell, damageInfo, healInfo); +} + +void Unit::SendPeriodicAuraLog(SpellPeriodicAuraLogInfo* pInfo) +{ + AuraEffect const* aura = pInfo->auraEff; + + WorldPackets::CombatLog::SpellPeriodicAuraLog periodicAuraLog; + periodicAuraLog.TargetGUID = GetGUID(); + periodicAuraLog.CasterGUID = aura->GetCasterGUID(); + periodicAuraLog.SpellID = aura->GetId(); + + WorldPackets::CombatLog::PeriodicAuraLogEffect& logEffect = periodicAuraLog.Entries.emplace_back(); + logEffect.Effect = aura->GetAuraType(); + logEffect.Amount = pInfo->damage; + logEffect.OverHealOrKill = pInfo->overDamage; + logEffect.Resisted = pInfo->resist; + logEffect.Crit = pInfo->critical; + + switch (logEffect.Effect) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + logEffect.AbsorbedOrAmplitude = pInfo->absorb; + logEffect.SchoolMaskOrPower = aura->GetSpellInfo()->GetSchoolMask(); + break; + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_OBS_MOD_HEALTH: + logEffect.AbsorbedOrAmplitude = pInfo->absorb; + break; + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_PERIODIC_ENERGIZE: + logEffect.SchoolMaskOrPower = aura->GetMiscValue(); + break; + case SPELL_AURA_PERIODIC_MANA_LEECH: + logEffect.SchoolMaskOrPower = aura->GetMiscValue(); + logEffect.AbsorbedOrAmplitude = pInfo->multiplier; + break; + default: + TC_LOG_ERROR("entities.unit", "Unit::SendPeriodicAuraLog: unknown aura %u", uint32(aura->GetAuraType())); + return; + } + + SendMessageToSet(periodicAuraLog.Write(), true); +} + +void Unit::SendSpellDamageResist(Unit* target, uint32 spellId) +{ + WorldPacket data(SMSG_PROCRESIST, 8+8+4+1); + data << uint64(GetGUID()); + data << uint64(target->GetGUID()); + data << uint32(spellId); + data << uint8(0); // bool - log format: 0-default, 1-debug + SendMessageToSet(&data, true); +} + +void Unit::SendSpellDamageImmune(Unit* target, uint32 spellId) +{ + WorldPacket data(SMSG_SPELLORDAMAGE_IMMUNE, 8+8+4+1); + data << uint64(GetGUID()); + data << uint64(target->GetGUID()); + data << uint32(spellId); + data << uint8(0); // bool - log format: 0-default, 1-debug + SendMessageToSet(&data, true); +} + +void Unit::SendAttackStateUpdate(CalcDamageInfo* damageInfo) +{ + WorldPackets::CombatLog::AttackerStateUpdate packet; + packet.HitInfo = damageInfo->HitInfo; + packet.AttackerGUID = damageInfo->Attacker->GetGUID(); + packet.VictimGUID = damageInfo->Target->GetGUID(); + packet.Damage = damageInfo->Damage; + int32 overkill = damageInfo->Damage - damageInfo->Target->GetHealth(); + packet.OverDamage = (overkill < 0 ? -1 : overkill); + + if (damageInfo->Damage) + { + packet.SubDmg.emplace(); + packet.SubDmg->SchoolMask = damageInfo->DamageSchoolMask; // School of sub damage + packet.SubDmg->FDamage = damageInfo->Damage; // sub damage + packet.SubDmg->Damage = damageInfo->Damage; // Sub Damage + packet.SubDmg->Absorbed = damageInfo->Absorb; + packet.SubDmg->Resisted = damageInfo->Resist; + } + + packet.VictimState = damageInfo->TargetState; + packet.BlockAmount = damageInfo->Blocked; + packet.RageGained = damageInfo->RageGained; + + SendMessageToSet(packet.Write(), true); +} + +void Unit::SendAttackStateUpdate(uint32 HitInfo, Unit* target, uint8 /*SwingType*/, SpellSchoolMask damageSchoolMask, uint32 Damage, uint32 AbsorbDamage, uint32 Resist, VictimState TargetState, uint32 BlockedAmount) +{ + CalcDamageInfo dmgInfo; + dmgInfo.HitInfo = HitInfo; + dmgInfo.Attacker = this; + dmgInfo.Target = target; + dmgInfo.Damage = Damage - AbsorbDamage - Resist - BlockedAmount; + dmgInfo.DamageSchoolMask = damageSchoolMask; + dmgInfo.Absorb = AbsorbDamage; + dmgInfo.Resist = Resist; + dmgInfo.TargetState = TargetState; + dmgInfo.Blocked = BlockedAmount; + dmgInfo.RageGained = 0; + SendAttackStateUpdate(&dmgInfo); +} + +void Unit::SetPowerType(PowerType power) +{ + if (GetPowerType() == power) + return; + + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_POWER_TYPE, power); + + // Creatures can swap out their power type at index 0 so we do keep track of the new power type here + if (IsCreature()) + { + _usedPowerTypes[GetPowerIndex(power)] = power; + UpdatePowerRegeneration(power); + } + + if (GetTypeId() == TYPEID_PLAYER) + { + if (ToPlayer()->GetGroup()) + ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_POWER_TYPE); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_POWER_TYPE); + } + } + + // Update max power + UpdateMaxPower(power); + + // Update current power + switch (power) + { + case POWER_MANA: // Keep the same (druid form switching...) + case POWER_ENERGY: + break; + case POWER_RAGE: // Reset to zero + SetPower(POWER_RAGE, 0); + break; + case POWER_FOCUS: // Make it full + SetFullPower(power); + break; + default: + break; + } +} + +PowerType Unit::CalculateDisplayPowerType() const +{ + PowerType displayPower = POWER_MANA; + switch (GetShapeshiftForm()) + { + case FORM_GHOUL: + case FORM_CAT: + displayPower = POWER_ENERGY; + break; + case FORM_BEAR: + displayPower = POWER_RAGE; + break; + case FORM_TRAVEL: + case FORM_GHOSTWOLF: + displayPower = POWER_MANA; + break; + default: + { + ChrClassesEntry const* cEntry = sChrClassesStore.LookupEntry(getClass()); + if (cEntry && cEntry->DisplayPower < MAX_POWERS) + displayPower = PowerType(cEntry->DisplayPower); + + if (Vehicle* vehicle = GetVehicleKit()) + { + if (PowerDisplayEntry const* powerDisplay = sPowerDisplayStore.LookupEntry(vehicle->GetVehicleInfo()->PowerDisplayID[0])) // To-do: 4.x has 3 power display id fields. + displayPower = PowerType(powerDisplay->ActualType); + else if (getClass() == CLASS_ROGUE) + displayPower = POWER_ENERGY; + } + else if (Pet const* pet = ToPet()) + { + if (pet->getPetType() == HUNTER_PET) // Hunter pets have focus + displayPower = POWER_FOCUS; + else if (pet->IsPetGhoul() || pet->IsRisenAlly()) // DK pets have energy + displayPower = POWER_ENERGY; + } + break; + } + } + + return displayPower; +} + +void Unit::UpdateDisplayPower() +{ + SetPowerType(CalculateDisplayPowerType()); +} + +void Unit::SetSheath(SheathState sheathed) +{ + SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_SHEATH_STATE, sheathed); + if (sheathed == SHEATH_STATE_UNARMED) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Sheathing); +} + +void Unit::_addAttacker(Unit* pAttacker) +{ + m_attackers.insert(pAttacker); +} + +void Unit::_removeAttacker(Unit* pAttacker) +{ + m_attackers.erase(pAttacker); +} + +Unit* Unit::getAttackerForHelper() const // If someone wants to help, who to give them +{ + if (!IsEngaged()) + return nullptr; + + if (Unit* victim = GetVictim()) + if ((!IsPet() && !IsCharmerOrSelfPlayer()) || IsInCombatWith(victim)) + return victim; + + CombatManager const& mgr = GetCombatManager(); + // pick arbitrary targets; our pvp combat > owner's pvp combat > our pve combat > owner's pve combat + Unit* owner = GetCharmerOrOwner(); + if (mgr.HasPvPCombat()) + return mgr.GetPvPCombatRefs().begin()->second->GetOther(this); + if (owner && (owner->GetCombatManager().HasPvPCombat())) + return owner->GetCombatManager().GetPvPCombatRefs().begin()->second->GetOther(owner); + if (mgr.HasPvECombat()) + return mgr.GetPvECombatRefs().begin()->second->GetOther(this); + if (owner && (owner->GetCombatManager().HasPvECombat())) + return owner->GetCombatManager().GetPvECombatRefs().begin()->second->GetOther(owner); + return nullptr; +} + +bool Unit::Attack(Unit* victim, bool meleeAttack) +{ + if (!victim || victim == this) + return false; + + // dead units can neither attack nor be attacked + if (!IsAlive() || !victim->IsInWorld() || !victim->IsAlive()) + return false; + + // player cannot attack in mount state + if (GetTypeId() == TYPEID_PLAYER && IsMounted()) + return false; + + Creature* creature = ToCreature(); + if (creature) + { + // creatures cannot attack while evading + if (creature->IsInEvadeMode()) + return false; + + if (!creature->CanMelee()) + meleeAttack = false; + } + + if (HasAuraType(SPELL_AURA_DISABLE_ATTACKING_EXCEPT_ABILITIES)) + return false; + + // nobody can attack GM in GM-mode + if (victim->GetTypeId() == TYPEID_PLAYER) + { + if (victim->ToPlayer()->IsGameMaster()) + return false; + } + else + { + if (victim->ToCreature()->IsEvadingAttacks()) + return false; + } + + if (m_attacking) + { + if (m_attacking == victim) + { + // switch to melee attack from ranged/magic + if (meleeAttack) + { + if (!HasUnitState(UNIT_STATE_MELEE_ATTACKING)) + { + AddUnitState(UNIT_STATE_MELEE_ATTACKING); + SendMeleeAttackStart(victim); + return true; + } + } + else if (HasUnitState(UNIT_STATE_MELEE_ATTACKING)) + { + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + SendMeleeAttackStop(victim); + return true; + } + return false; + } + + // switch target + InterruptSpell(CURRENT_MELEE_SPELL); + if (!meleeAttack) + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + } + + if (m_attacking) + m_attacking->_removeAttacker(this); + + m_attacking = victim; + m_attacking->_addAttacker(this); + + // Set our target + SetTarget(victim->GetGUID()); + + if (meleeAttack) + AddUnitState(UNIT_STATE_MELEE_ATTACKING); + + // set position before any AI calls/assistance + //if (GetTypeId() == TYPEID_UNIT) + // ToCreature()->SetCombatStartPosition(GetPositionX(), GetPositionY(), GetPositionZ()); + + if (creature && !IsControlledByPlayer()) + { + EngageWithTarget(victim); // ensure that anything we're attacking has threat + creature->CallAssistance(); + + // Remove emote state - will be restored on creature reset + SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + } + + // delay offhand weapon attack by 50% of the base attack time + if (haveOffhandWeapon() && GetTypeId() != TYPEID_PLAYER) + setAttackTimer(OFF_ATTACK, std::max(getAttackTimer(OFF_ATTACK), getAttackTimer(BASE_ATTACK) + uint32(CalculatePct(GetFloatValue(UNIT_FIELD_BASEATTACKTIME), 50)))); + + + if (meleeAttack) + SendMeleeAttackStart(victim); + + // Let the pet know we've started attacking someting. Handles melee attacks only + // Spells such as auto-shot and others handled in WorldSession::HandleCastSpellOpcode + if (GetTypeId() == TYPEID_PLAYER) + { + for (Unit* controlled : m_Controlled) + if (Creature* cControlled = controlled->ToCreature()) + if (CreatureAI* controlledAI = cControlled->AI()) + controlledAI->OwnerAttacked(victim); + } + + return true; +} + +bool Unit::AttackStop() +{ + if (!m_attacking) + return false; + + Unit* victim = m_attacking; + + m_attacking->_removeAttacker(this); + m_attacking = nullptr; + + // Clear our target + SetTarget(ObjectGuid::Empty); + + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + + InterruptSpell(CURRENT_MELEE_SPELL); + + // reset only at real combat stop + if (Creature* creature = ToCreature()) + creature->SetNoCallAssistance(false); + + SendMeleeAttackStop(victim); + + return true; +} + +void Unit::ValidateAttackersAndOwnTarget() +{ + // iterate attackers + UnitVector toRemove; + AttackerSet const& attackers = getAttackers(); + for (Unit* attacker : attackers) + if (!attacker->IsValidAttackTarget(this)) + toRemove.push_back(attacker); + + for (Unit* attacker : toRemove) + attacker->AttackStop(); + + // remove our own victim + if (Unit* victim = GetVictim()) + if (!IsValidAttackTarget(victim)) + AttackStop(); +} + +void Unit::CombatStop(bool includingCast, bool mutualPvP, bool (*unitFilter)(Unit const* otherUnit)) +{ + if (includingCast && IsNonMeleeSpellCast(false)) + InterruptNonMeleeSpells(false); + + AttackStop(); + + if (!unitFilter) + RemoveAllAttackers(); + else + { + std::vector attackersToRemove; + attackersToRemove.reserve(m_attackers.size()); + std::copy_if(m_attackers.begin(), m_attackers.end(), std::back_inserter(attackersToRemove), unitFilter); + + for (Unit* attacker : attackersToRemove) + attacker->AttackStop(); + } + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->SendAttackSwingCancelAttack(); // melee and ranged forced attack cancel + + m_combatManager.EndAllPvECombat(unitFilter); + if (mutualPvP) + m_combatManager.EndAllPvPCombat(unitFilter); + else // vanish and brethren are weird + m_combatManager.SuppressPvPCombat(unitFilter); +} + +void Unit::CombatStopWithPets(bool includingCast) +{ + CombatStop(includingCast); + + for (Unit* minion : m_Controlled) + minion->CombatStop(includingCast); +} + +bool Unit::isAttackingPlayer() const +{ + if (HasUnitState(UNIT_STATE_ATTACK_PLAYER)) + return true; + + for (ControlList::const_iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) + if ((*itr)->isAttackingPlayer()) + return true; + + for (uint8 i = 0; i < MAX_SUMMON_SLOT; ++i) + if (m_SummonSlot[i]) + if (Creature* summon = GetMap()->GetCreature(m_SummonSlot[i])) + if (summon->isAttackingPlayer()) + return true; + + return false; +} + +void Unit::RemoveAllAttackers() +{ + while (!m_attackers.empty()) + { + AttackerSet::iterator iter = m_attackers.begin(); + if (!(*iter)->AttackStop()) + { + TC_LOG_ERROR("entities.unit", "WORLD: Unit has an attacker that isn't attacking it!"); + m_attackers.erase(iter); + } + } +} + +void Unit::ModifyAuraState(AuraStateType flag, bool apply) +{ + if (apply) + { + if (!HasFlag(UNIT_FIELD_AURASTATE, 1<<(flag-1))) + { + SetFlag(UNIT_FIELD_AURASTATE, 1<<(flag-1)); + if (GetTypeId() == TYPEID_PLAYER) + { + PlayerSpellMap const& sp_list = ToPlayer()->GetSpellMap(); + for (PlayerSpellMap::const_iterator itr = sp_list.begin(); itr != sp_list.end(); ++itr) + { + if (itr->second.state == PLAYERSPELL_REMOVED || itr->second.disabled) + continue; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr->first); + if (!spellInfo || !spellInfo->IsPassive()) + continue; + if (spellInfo->CasterAuraState == uint32(flag)) + CastSpell(this, itr->first, true); + } + } + else if (Pet* pet = ToCreature()->ToPet()) + { + for (PetSpellMap::const_iterator itr = pet->m_spells.begin(); itr != pet->m_spells.end(); ++itr) + { + if (itr->second.state == PETSPELL_REMOVED) + continue; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr->first); + if (!spellInfo || !spellInfo->IsPassive()) + continue; + if (spellInfo->CasterAuraState == uint32(flag)) + CastSpell(this, itr->first, true); + } + } + } + } + else + { + if (HasFlag(UNIT_FIELD_AURASTATE, 1<<(flag-1))) + { + RemoveFlag(UNIT_FIELD_AURASTATE, 1<<(flag-1)); + + Unit::AuraApplicationMap& tAuras = GetAppliedAuras(); + for (Unit::AuraApplicationMap::iterator itr = tAuras.begin(); itr != tAuras.end();) + { + SpellInfo const* spellProto = itr->second->GetBase()->GetSpellInfo(); + if (itr->second->GetBase()->GetCasterGUID() == GetGUID() && spellProto->CasterAuraState == uint32(flag) && (spellProto->IsPassive() || flag != AURA_STATE_ENRAGE)) + RemoveAura(itr); + else + ++itr; + } + } + } +} + +uint32 Unit::BuildAuraStateUpdateForTarget(Unit* target) const +{ + uint32 auraStates = GetUInt32Value(UNIT_FIELD_AURASTATE) &~(PER_CASTER_AURA_STATE_MASK); + for (AuraStateAurasMap::const_iterator itr = m_auraStateAuras.begin(); itr != m_auraStateAuras.end(); ++itr) + if ((1 << (itr->first - 1)) & PER_CASTER_AURA_STATE_MASK) + if (itr->second->GetBase()->GetCasterGUID() == target->GetGUID()) + auraStates |= (1 << (itr->first - 1)); + + return auraStates; +} + +bool Unit::HasAuraState(AuraStateType flag, SpellInfo const* spellProto, Unit const* Caster) const +{ + if (Caster) + { + if (spellProto) + { + AuraEffectList const& stateAuras = Caster->GetAuraEffectsByType(SPELL_AURA_ABILITY_IGNORE_AURASTATE); + for (AuraEffect const* aurEff : stateAuras) + if (aurEff->IsAffectingSpell(spellProto)) + return true; + } + + // Check per caster aura state + // If aura with aurastate by caster not found return false + if ((1 << (flag - 1)) & PER_CASTER_AURA_STATE_MASK) + { + AuraStateAurasMapBounds range = m_auraStateAuras.equal_range(flag); + for (AuraStateAurasMap::const_iterator itr = range.first; itr != range.second; ++itr) + if (itr->second->GetBase()->GetCasterGUID() == Caster->GetGUID()) + return true; + return false; + } + } + + return HasFlag(UNIT_FIELD_AURASTATE, 1 << (flag - 1)); +} + +void Unit::SetOwnerGUID(ObjectGuid owner) +{ + if (GetOwnerGUID() == owner) + return; + + SetGuidValue(UNIT_FIELD_SUMMONEDBY, owner); + if (!owner) + return; + + // Update owner dependent fields + Player* player = ObjectAccessor::GetPlayer(*this, owner); + if (!player || !player->HaveAtClient(this)) // if player cannot see this unit yet, he will receive needed data with create object + return; + + SetFieldNotifyFlag(UF_FLAG_OWNER); + + UpdateData udata(GetMapId()); + WorldPacket packet; + BuildValuesUpdateBlockForPlayer(&udata, player); + udata.BuildPacket(&packet); + player->SendDirectMessage(&packet); + + RemoveFieldNotifyFlag(UF_FLAG_OWNER); +} + +Player* Unit::GetControllingPlayer() const +{ + if (ObjectGuid guid = GetCharmerOrOwnerGUID()) + { + if (Unit* master = ObjectAccessor::GetUnit(*this, guid)) + return master->GetControllingPlayer(); + return nullptr; + } + else + return const_cast(ToPlayer()); +} + +Minion* Unit::GetFirstMinion() const +{ + if (ObjectGuid pet_guid = GetMinionGUID()) + { + if (Creature* pet = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, pet_guid)) + if (pet->HasUnitTypeMask(UNIT_MASK_MINION)) + return (Minion*)pet; + + TC_LOG_ERROR("entities.unit", "Unit::GetFirstMinion: Minion %s not exist.", pet_guid.ToString().c_str()); + const_cast(this)->SetMinionGUID(ObjectGuid::Empty); + } + + return nullptr; +} + +Guardian* Unit::GetGuardianPet() const +{ + if (ObjectGuid pet_guid = GetPetGUID()) + { + if (Creature* pet = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, pet_guid)) + if (pet->HasUnitTypeMask(UNIT_MASK_GUARDIAN)) + return (Guardian*)pet; + + TC_LOG_FATAL("entities.unit", "Unit::GetGuardianPet: Guardian %s not exist.", pet_guid.ToString().c_str()); + const_cast(this)->SetPetGUID(ObjectGuid::Empty); + } + + return nullptr; +} + +void Unit::SetMinion(Minion* minion, bool apply) +{ + if (!minion) + { + TC_LOG_ERROR("entities.unit", "Unit::SetMinion: Unit %s tried to reference a non existing minion", GetGUID().ToString().c_str()); + return; + } + + TC_LOG_DEBUG("entities.unit", "SetMinion %u for %u, apply %u", minion->GetEntry(), GetEntry(), apply); + + if (apply) + { + if (minion->GetOwnerGUID()) + { + TC_LOG_FATAL("entities.unit", "SetMinion: Minion %u is not the minion of owner %u", minion->GetEntry(), GetEntry()); + return; + } + + minion->SetOwnerGUID(GetGUID()); + + m_Controlled.insert(minion); + + if (GetTypeId() == TYPEID_PLAYER) + { + minion->m_ControlledByPlayer = true; + minion->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + } + + // Can only have one pet. If a new one is summoned, dismiss the old one. + if (minion->IsGuardianPet()) + { + if (Guardian* oldPet = GetGuardianPet()) + { + if (oldPet != minion && (oldPet->IsPet() || minion->IsPet() || oldPet->GetEntry() != minion->GetEntry())) + { + // remove existing minion pet + if (oldPet->IsPet()) + ((Pet*)oldPet)->Remove(PET_SAVE_DISMISS); + else + oldPet->UnSummon(); + SetPetGUID(minion->GetGUID()); + SetMinionGUID(ObjectGuid::Empty); + } + } + else + { + SetPetGUID(minion->GetGUID()); + SetMinionGUID(ObjectGuid::Empty); + } + } + + if (minion->HasUnitTypeMask(UNIT_MASK_CONTROLABLE_GUARDIAN)) + AddGuidValue(UNIT_FIELD_SUMMON, minion->GetGUID()); + + if (minion->m_Properties && SummonTitle(minion->m_Properties->Title) == SummonTitle::Companion) + SetCritterGUID(minion->GetGUID()); + + // PvP, FFAPvP + minion->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, GetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG)); + + // Send infinity cooldown - client does that automatically but after relog cooldown needs to be set again + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(minion->GetUInt32Value(UNIT_CREATED_BY_SPELL)); + + if (spellInfo && spellInfo->IsCooldownStartedOnEvent()) + GetSpellHistory()->StartCooldown(spellInfo, 0, nullptr, true); + } + else + { + if (minion->GetOwnerGUID() != GetGUID()) + { + TC_LOG_FATAL("entities.unit", "SetMinion: Minion %u is not the minion of owner %u", minion->GetEntry(), GetEntry()); + return; + } + + if (m_Controlled.find(minion) != m_Controlled.end()) + m_Controlled.erase(minion); + + if (minion->m_Properties && SummonTitle(minion->m_Properties->Title) == SummonTitle::Companion) + if (GetCritterGUID() == minion->GetGUID()) + SetCritterGUID(ObjectGuid::Empty); + + if (minion->IsGuardianPet()) + { + if (GetPetGUID() == minion->GetGUID()) + SetPetGUID(ObjectGuid::Empty); + } + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(minion->GetUInt32Value(UNIT_CREATED_BY_SPELL)); + // Remove infinity cooldown + if (spellInfo && (spellInfo->IsCooldownStartedOnEvent())) + GetSpellHistory()->SendCooldownEvent(spellInfo); + + //if (minion->HasUnitTypeMask(UNIT_MASK_GUARDIAN)) + { + if (RemoveGuidValue(UNIT_FIELD_SUMMON, minion->GetGUID())) + { + // Check if there is another minion + for (Unit const* controlled : m_Controlled) + { + // do not use this check, creature do not have charm guid + //if (GetCharmedGUID() == (*itr)->GetGUID()) + if (GetGUID() == controlled->GetCharmerGUID()) + continue; + + //ASSERT((*itr)->GetOwnerGUID() == GetGUID()); + if (controlled->GetOwnerOrCreatorGUID() != GetGUID()) + { + OutDebugInfo(); + controlled->OutDebugInfo(); + ABORT(); + } + ASSERT(controlled->IsCreature()); + + if (!controlled->HasUnitTypeMask(UNIT_MASK_CONTROLABLE_GUARDIAN)) + continue; + + if (AddGuidValue(UNIT_FIELD_SUMMON, controlled->GetGUID())) + { + // show another pet bar if there is no charm bar + if (IsPlayer() && !GetCharmedGUID()) + { + if (controlled->IsPet()) + ToPlayer()->PetSpellInitialize(); + else + ToPlayer()->CharmSpellInitialize(); + } + } + break; + } + } + } + } + UpdatePetCombatState(); +} + +void Unit::GetAllMinionsByEntry(std::list& Minions, uint32 entry) +{ + for (Unit::ControlList::iterator itr = m_Controlled.begin(); itr != m_Controlled.end();) + { + Unit* unit = *itr; + ++itr; + if (unit->GetEntry() == entry && unit->GetTypeId() == TYPEID_UNIT + && unit->IsSummon()) // minion, actually + Minions.push_back(unit->ToCreature()); + } +} + +void Unit::RemoveAllMinionsByEntry(uint32 entry) +{ + for (Unit::ControlList::iterator itr = m_Controlled.begin(); itr != m_Controlled.end();) + { + Unit* unit = *itr; + ++itr; + if (unit->GetEntry() == entry && unit->GetTypeId() == TYPEID_UNIT + && unit->IsSummon()) // minion, actually + unit->ToTempSummon()->UnSummon(); + // i think this is safe because i have never heard that a despawned minion will trigger a same minion + } +} + +void Unit::SetCharm(Unit* charm, bool apply) +{ + if (apply) + { + if (GetTypeId() == TYPEID_PLAYER) + { + ASSERT(AddGuidValue(UNIT_FIELD_CHARM, charm->GetGUID()), + "Player %s is trying to charm unit %u, but it already has a charmed unit %s", GetName().c_str(), charm->GetEntry(), GetCharmedGUID().ToString().c_str()); + m_charmed = charm; + + charm->m_ControlledByPlayer = true; + /// @todo maybe we can use this flag to check if controlled by player + charm->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + } + else + charm->m_ControlledByPlayer = false; + + // PvP, FFAPvP + charm->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, GetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG)); + + ASSERT(charm->AddGuidValue(UNIT_FIELD_CHARMEDBY, GetGUID()), + "Unit %u is being charmed, but it already has a charmer %s", charm->GetEntry(), charm->GetCharmerGUID().ToString().c_str()); + charm->m_charmer = this; + + _isWalkingBeforeCharm = charm->IsWalking(); + if (_isWalkingBeforeCharm) + charm->SetWalk(false); + + m_Controlled.insert(charm); + } + else + { + charm->ClearUnitState(UNIT_STATE_CHARMED); + + if (GetTypeId() == TYPEID_PLAYER) + { + ASSERT(RemoveGuidValue(UNIT_FIELD_CHARM, charm->GetGUID()), + "Player %s is trying to uncharm unit %u, but it has another charmed unit %s", GetName().c_str(), charm->GetEntry(), GetCharmedGUID().ToString().c_str()); + m_charmed = nullptr; + } + + ASSERT(charm->RemoveGuidValue(UNIT_FIELD_CHARMEDBY, GetGUID()), + "Unit %u is being uncharmed, but it has another charmer %s", charm->GetEntry(), charm->GetCharmerGUID().ToString().c_str()); + charm->m_charmer = nullptr; + + if (charm->GetTypeId() == TYPEID_PLAYER) + { + charm->m_ControlledByPlayer = true; + charm->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + charm->ToPlayer()->UpdatePvPState(); + } + else if (Player* player = charm->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + charm->m_ControlledByPlayer = true; + charm->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + charm->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, player->GetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG)); + } + else + { + charm->m_ControlledByPlayer = false; + charm->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + charm->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, 0); + } + + if (charm->IsWalking() != _isWalkingBeforeCharm) + charm->SetWalk(_isWalkingBeforeCharm); + + if (charm->GetTypeId() == TYPEID_PLAYER + || !charm->ToCreature()->HasUnitTypeMask(UNIT_MASK_MINION) + || charm->GetOwnerOrCreatorGUID() != GetGUID()) + { + m_Controlled.erase(charm); + } + } + UpdatePetCombatState(); +} + +/*static*/ void Unit::DealHeal(HealInfo& healInfo) +{ + int32 gain = 0; + Unit* healer = healInfo.GetHealer(); + Unit* victim = healInfo.GetTarget(); + uint32 addhealth = healInfo.GetHeal(); + + if (healer) + { + if (UnitAI* victimAI = victim->GetAI()) + victimAI->HealReceived(healer, addhealth); + + if (UnitAI* healerAI = healer->GetAI()) + healerAI->HealDone(victim, addhealth); + } + + if (addhealth) + gain = victim->ModifyHealth(int32(addhealth)); + + // Hook for OnHeal Event + sScriptMgr->OnHeal(healer, victim, (uint32&)gain); + + Unit* unit = healer; + if (healer && healer->GetTypeId() == TYPEID_UNIT && healer->IsTotem()) + unit = healer->GetOwner(); + + if (unit) + { + if (Player* player = unit->ToPlayer()) + { + if (!healInfo.GetSpellInfo() || !healInfo.GetSpellInfo()->HasAttribute(SPELL_ATTR7_DO_NOT_COUNT_FOR_PVP_SCOREBOARD)) + if (Battleground* bg = player->GetBattleground()) + bg->UpdatePlayerScore(player, SCORE_HEALING_DONE, gain); + + // use the actual gain, as the overheal shall not be counted, skip gain 0 (it ignored anyway in to criteria) + if (gain) + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HEALING_DONE, gain, 0, 0, victim); + + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_HEAL_CAST, addhealth); + } + } + + if (Player* player = victim->ToPlayer()) + { + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_TOTAL_HEALING_RECEIVED, gain); + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_HEALING_RECEIVED, addhealth); + } + + if (gain) + healInfo.SetEffectiveHeal(gain > 0 ? static_cast(gain) : 0UL); +} + +bool Unit::IsMagnet() const +{ + // Grounding Totem + if (GetUInt32Value(UNIT_CREATED_BY_SPELL) == 8177) /// @todo: find a more generic solution + return true; + + return false; +} + +Unit* Unit::GetMeleeHitRedirectTarget(Unit* victim, SpellInfo const* spellInfo) +{ + AuraEffectList const& hitTriggerAuras = victim->GetAuraEffectsByType(SPELL_AURA_ADD_CASTER_HIT_TRIGGER); + for (AuraEffectList::const_iterator i = hitTriggerAuras.begin(); i != hitTriggerAuras.end(); ++i) + { + if (Unit* magnet = (*i)->GetBase()->GetCaster()) + if (IsValidAttackTarget(magnet, spellInfo) && magnet->IsWithinLOSInMap(this) + && (!spellInfo || (spellInfo->CheckExplicitTarget(this, magnet) == SPELL_CAST_OK + && spellInfo->CheckTarget(this, magnet, false) == SPELL_CAST_OK))) + if (roll_chance_i((*i)->GetAmount())) + { + (*i)->GetBase()->DropCharge(AuraRemoveFlags::Expired); + return magnet; + } + } + return victim; +} + +Unit* Unit::GetFirstControlled() const +{ + // Sequence: charmed, pet, other guardians + Unit* unit = GetCharmed(); + if (!unit) + if (ObjectGuid guid = GetMinionGUID()) + unit = ObjectAccessor::GetUnit(*this, guid); + + return unit; +} + +void Unit::RemoveAllControlled() +{ + // possessed pet and vehicle + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->StopCastingCharm(); + + while (!m_Controlled.empty()) + { + Unit* target = *m_Controlled.begin(); + m_Controlled.erase(m_Controlled.begin()); + if (target->GetCharmerGUID() == GetGUID()) + target->RemoveCharmAuras(); + else if (target->GetOwnerOrCreatorGUID() == GetGUID() && target->IsSummon()) + target->ToTempSummon()->UnSummon(); + else + TC_LOG_ERROR("entities.unit", "Unit %u is trying to release unit %u which is neither charmed nor owned by it", GetEntry(), target->GetEntry()); + } + if (GetPetGUID()) + TC_LOG_FATAL("entities.unit", "Unit %u is not able to release its pet %s", GetEntry(), GetPetGUID().ToString().c_str()); + if (GetMinionGUID()) + TC_LOG_FATAL("entities.unit", "Unit %u is not able to release its minion %s", GetEntry(), GetMinionGUID().ToString().c_str()); + if (GetCharmedGUID()) + TC_LOG_FATAL("entities.unit", "Unit %u is not able to release its charm %s", GetEntry(), GetCharmedGUID().ToString().c_str()); + if (!IsPet()) // pets don't use the flag for this + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT); // m_controlled is now empty, so we know none of our minions are in combat +} + +bool Unit::isPossessedByPlayer() const +{ + return HasUnitState(UNIT_STATE_POSSESSED) && GetCharmerGUID().IsPlayer(); +} + +bool Unit::isPossessing(Unit* u) const +{ + return u->isPossessed() && GetCharmedGUID() == u->GetGUID(); +} + +bool Unit::isPossessing() const +{ + if (Unit* u = GetCharmed()) + return u->isPossessed(); + else + return false; +} + +Unit* Unit::GetCharmerOrSelf() const +{ + if (IsCharmed()) + return GetCharmer(); + else + return const_cast(this); +} + +Unit* Unit::GetNextRandomRaidMemberOrPet(float radius) +{ + Player* player = nullptr; + if (GetTypeId() == TYPEID_PLAYER) + player = ToPlayer(); + // Should we enable this also for charmed units? + else if (GetTypeId() == TYPEID_UNIT && IsPet()) + player = GetOwner()->ToPlayer(); + + if (!player) + return nullptr; + Group* group = player->GetGroup(); + // When there is no group check pet presence + if (!group) + { + // We are pet now, return owner + if (player != this) + return IsWithinDistInMap(player, radius) ? player : nullptr; + Unit* pet = GetGuardianPet(); + // No pet, no group, nothing to return + if (!pet) + return nullptr; + // We are owner now, return pet + return IsWithinDistInMap(pet, radius) ? pet : nullptr; + } + + std::vector nearMembers; + // reserve place for players and pets because resizing vector every unit push is unefficient (vector is reallocated then) + nearMembers.reserve(group->GetMembersCount() * 2); + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (Player* Target = itr->GetSource()) + { + // IsHostileTo check duel and controlled by enemy + if (Target != this && IsWithinDistInMap(Target, radius) && Target->IsAlive() && !IsHostileTo(Target)) + nearMembers.push_back(Target); + + // Push player's pet to vector + if (Unit* pet = Target->GetGuardianPet()) + if (pet != this && IsWithinDistInMap(pet, radius) && pet->IsAlive() && !IsHostileTo(pet)) + nearMembers.push_back(pet); + } + + if (nearMembers.empty()) + return nullptr; + + uint32 randTarget = urand(0, nearMembers.size()-1); + return nearMembers[randTarget]; +} + +// only called in Player::SetSeer +// so move it to Player? +void Unit::AddPlayerToVision(Player* player) +{ + if (m_sharedVision.empty()) + { + setActive(true); + SetWorldObject(true); + } + m_sharedVision.push_back(player); +} + +// only called in Player::SetSeer +void Unit::RemovePlayerFromVision(Player* player) +{ + m_sharedVision.remove(player); + if (m_sharedVision.empty()) + { + setActive(false); + SetWorldObject(false); + } +} + +void Unit::RemoveBindSightAuras() +{ + RemoveAurasByType(SPELL_AURA_BIND_SIGHT); +} + +void Unit::RemoveCharmAuras() +{ + RemoveAurasByType(SPELL_AURA_MOD_CHARM); + RemoveAurasByType(SPELL_AURA_MOD_POSSESS); + RemoveAurasByType(SPELL_AURA_AOE_CHARM); +} + +void Unit::UnsummonAllTotems() +{ + for (uint8 i = 0; i < MAX_SUMMON_SLOT; ++i) + { + if (!m_SummonSlot[i]) + continue; + + if (Creature* OldTotem = GetMap()->GetCreature(m_SummonSlot[i])) + if (OldTotem->IsSummon()) + OldTotem->ToTempSummon()->UnSummon(); + } +} + +void Unit::SendHealSpellLog(HealInfo& healInfo, bool critical /*= false*/) +{ + // we guess size + WorldPacket data(SMSG_SPELLHEALLOG, 8 + 8 + 4 + 4 + 4 + 4 + 1 + 1); + data << healInfo.GetTarget()->GetPackGUID(); + data << healInfo.GetHealer()->GetPackGUID(); + data << uint32(healInfo.GetSpellInfo()->Id); + data << uint32(healInfo.GetHeal()); + data << uint32(healInfo.GetHeal() - healInfo.GetEffectiveHeal()); + data << uint32(healInfo.GetAbsorb()); // Absorb amount + data << uint8(critical ? 1 : 0); + data << uint8(0); // unused + SendMessageToSet(&data, true); +} + +int32 Unit::HealBySpell(HealInfo& healInfo, bool critical /*= false*/) +{ + // calculate heal absorb and reduce healing + Unit::CalcHealAbsorb(healInfo); + Unit::DealHeal(healInfo); + SendHealSpellLog(healInfo, critical); + return healInfo.GetEffectiveHeal(); +} + +void Unit::SendEnergizeSpellLog(Unit* victim, uint32 spellId, int32 damage, PowerType powerType) +{ + WorldPackets::CombatLog::SpellEnergizeLog packet; + packet.TargetGUID = victim->GetGUID(); + packet.CasterGUID = GetGUID(); + packet.SpellID = spellId; + packet.Amount = damage; + packet.Type = powerType; + + SendMessageToSet(packet.Write(), true); +} + +void Unit::EnergizeBySpell(Unit* victim, uint32 spellId, int32 damage, PowerType powerType) +{ + if (SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId)) + EnergizeBySpell(victim, info, damage, powerType); +} + +void Unit::EnergizeBySpell(Unit* victim, SpellInfo const* spellInfo, int32 damage, PowerType powerType) +{ + victim->ModifyPower(powerType, damage, false); + victim->GetThreatManager().ForwardThreatForAssistingMe(this, float(damage) / 2, spellInfo, true); + SendEnergizeSpellLog(victim, spellInfo->Id, damage, powerType); +} + +int32 Unit::SpellDamageBonusDone(Unit* victim, SpellInfo const* spellProto, int32 pdamage, DamageEffectType damagetype, uint8 effIndex, uint32 stack /*= 1*/, Spell* spell /*= nullptr*/, AuraEffect const* aurEff /*= nullptr*/) const +{ + if (!spellProto || !victim) + return pdamage; + + int32 DoneTotal = 0; + float DoneTotalMod = 1.0f; + + auto callDamageScript = [&](int32& dmg, int32& flatMod, float& pctMod) + { + if (spell) + spell->CallScriptCalcDamageHandlers(victim, dmg, flatMod, pctMod); + else if (aurEff) + aurEff->GetBase()->CallScriptCalcDamageAndHealingHandlers(aurEff, aurEff->GetBase()->GetApplicationOfTarget(victim->GetGUID()), victim, dmg, flatMod, pctMod); + }; + + // Some spells don't benefit from done mods + if (damagetype == DIRECT_DAMAGE || spellProto->HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS)) + { + callDamageScript(pdamage, DoneTotal, DoneTotalMod); + return int32(std::max(float(pdamage + DoneTotal) * DoneTotalMod, 0.0f)); + } + + // Totems get their bonus damage from their owner + if (IsCreature() && IsTotem()) + if (Unit* creator = ObjectAccessor::GetUnit(*this, GetCreatorGUID())) + return creator->SpellDamageBonusDone(victim, spellProto, pdamage, damagetype, effIndex, stack, spell, aurEff); + + DoneTotalMod = SpellDamagePctDone(victim, spellProto, damagetype); + + // done scripted mod (take it from owner) + Unit const* owner = GetOwner() ? GetOwner() : this; + DoneTotal += owner->GetTotalAuraModifier(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS, [spellProto](AuraEffect const* aurEff) -> bool + { + if (!aurEff->IsAffectingSpell(spellProto)) + return false; + + switch (aurEff->GetMiscValue()) + { + case 4418: // Increased Shock Damage + case 4554: // Increased Lightning Damage + case 4555: // Improved Moonfire + case 5142: // Increased Lightning Damage + case 5147: // Improved Consecration / Libram of Resurgence + case 5148: // Idol of the Shooting Star + case 6008: // Increased Lightning Damage + case 8627: // Totem of Hex + return true; + default: + break; + } + + return false; + }); + + // Some spells don't benefit from pct done mods + if (!spellProto->HasAttribute(SPELL_ATTR6_IGNORE_CASTER_DAMAGE_MODIFIERS)) + DoneTotal += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_FLAT_SPELL_DAMAGE_VERSUS, victim->GetCreatureTypeMask()); + + // Done fixed damage bonus auras + int32 DoneAdvertisedBenefit = SpellBaseDamageBonusDone(spellProto->GetSchoolMask(), true); + // modify spell power by victim's SPELL_AURA_MOD_DAMAGE_TAKEN auras (eg Amplify/Dampen Magic) + DoneAdvertisedBenefit += GetTotalAuraModifier(SPELL_AURA_MOD_DAMAGE_TAKEN, [spellProto](AuraEffect const* aurEff) + { + if (spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if ((aurEff->GetMiscValue() & spellProto->GetSchoolMask()) != 0) + return true; + + return false; + }); + + // Custom scripted damage + switch (spellProto->SpellFamilyName) + { + case SPELLFAMILY_HUNTER: + // Hunter Pet basic attacks (RAP * 0.4 * 0.2) + if (spellProto->SpellFamilyFlags[0] & 0x00080000) + DoneTotal += (owner->GetTotalAttackPowerValue(RANGED_ATTACK) * 0.4f) * 0.2f; + + // Kill Command + if (spellProto->SpellFamilyFlags[1] & 0x00000800) + DoneTotal += owner->GetTotalAttackPowerValue(RANGED_ATTACK) * 0.516f; + break; + case SPELLFAMILY_WARLOCK: + // Warlock Minions inherit 50% of their owner's spell power + if (IsMinion() && GetOwner()) + DoneAdvertisedBenefit = owner->SpellBaseDamageBonusDone(spellProto->GetSchoolMask(), true) * 0.5f; + break; + case SPELLFAMILY_MAGE: + // Mage Water Elementals inherit 40% of their owner's spell power + if (IsMinion() && GetOwner()) + DoneAdvertisedBenefit = owner->SpellBaseDamageBonusDone(spellProto->GetSchoolMask(), true) * 0.4f; + break; + default: + break; + } + + if (spellProto->BonusCoefficientFromAP > 0.0f) + { + float ApCoeffMod = spellProto->BonusCoefficientFromAP; + if (Player* modOwner = GetSpellModOwner()) + { + ApCoeffMod *= 100.0f; + modOwner->ApplySpellMod(spellProto, SpellModOp::BonusCoefficient, ApCoeffMod); + ApCoeffMod /= 100.0f; + } + + WeaponAttackType const attType = [&]() + { + if ((spellProto->IsRangedWeaponSpell() && spellProto->DmgClass != SPELL_DAMAGE_CLASS_MELEE)) + return RANGED_ATTACK; + + if (spellProto->HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON) && !spellProto->HasAttribute(SPELL_ATTR3_REQUIRES_MAIN_HAND_WEAPON)) + return OFF_ATTACK; + + return BASE_ATTACK; + }(); + + float APbonus = float(victim->GetTotalAuraModifier(attType != RANGED_ATTACK ? SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS : SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS)); + APbonus += GetTotalAttackPowerValue(attType); + DoneTotal += int32(stack * ApCoeffMod * APbonus); + } + + // Default calculation + if (DoneAdvertisedBenefit) + { + float coeff = spellProto->Effects[effIndex].BonusMultiplier * spellProto->GetSpellScalingMultiplier(getLevel(), false); + if (Player* modOwner = GetSpellModOwner()) + { + coeff *= 100.0f; + modOwner->ApplySpellMod(spellProto, SpellModOp::BonusCoefficient, coeff); + coeff /= 100.0f; + } + + DoneTotal += int32(DoneAdvertisedBenefit * coeff * stack); + } + + callDamageScript(pdamage, DoneTotal, DoneTotalMod); + + float tmpDamage = float(pdamage + DoneTotal) * DoneTotalMod; + + // apply spellmod to Done damage (flat and pct) + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, damagetype == DOT ? SpellModOp::PeriodicHealingAndDamage : SpellModOp::HealingAndDamage, tmpDamage); + + return int32(std::max(tmpDamage, 0.0f)); +} + +float Unit::SpellDamagePctDone(Unit* victim, SpellInfo const* spellProto, DamageEffectType damagetype) const +{ + if (!spellProto || !victim || damagetype == DIRECT_DAMAGE) + return 1.0f; + + // Some spells don't benefit from done mods + if (spellProto->HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS)) + return 1.0f; + + // Some spells don't benefit from pct done mods + if (spellProto->HasAttribute(SPELL_ATTR6_IGNORE_CASTER_DAMAGE_MODIFIERS)) + return 1.0f; + + // For totems pct done mods are calculated when its calculation is run on the player in SpellDamageBonusDone. + if (GetTypeId() == TYPEID_UNIT && IsTotem()) + return 1.0f; + + // Done total percent damage auras + float DoneTotalMod = 1.0f; + + // Pet damage? + if (GetTypeId() == TYPEID_UNIT && !IsPet()) + DoneTotalMod *= ToCreature()->GetSpellDamageMod(ToCreature()->GetCreatureTemplate()->rank); + + float maxModDamagePercentSchool = 0.0f; + if (GetTypeId() == TYPEID_PLAYER) + { + for (uint32 i = 0; i < MAX_SPELL_SCHOOL; ++i) + if (spellProto->GetSchoolMask() & (1 << i)) + maxModDamagePercentSchool = std::max(maxModDamagePercentSchool, GetFloatValue(PLAYER_FIELD_MOD_DAMAGE_DONE_PCT + i)); + } + else + maxModDamagePercentSchool = GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_PERCENT_DONE, spellProto->GetSchoolMask()); + + DoneTotalMod *= maxModDamagePercentSchool; + + uint32 creatureTypeMask = victim->GetCreatureTypeMask(); + + DoneTotalMod *= GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_DONE_VERSUS, creatureTypeMask); + + // bonus against aurastate + DoneTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_DONE_VERSUS_AURASTATE, [victim](AuraEffect const* aurEff) -> bool + { + if (victim->HasAuraState(static_cast(aurEff->GetMiscValue()))) + return true; + return false; + }); + + // Add SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC percent bonus + AddPct(DoneTotalMod, GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC, spellProto->Mechanic)); + + // done scripted mod (take it from owner) + Unit const* owner = GetOwner() ? GetOwner() : this; + AuraEffectList const& mOverrideClassScript = owner->GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS); + for (AuraEffectList::const_iterator i = mOverrideClassScript.begin(); i != mOverrideClassScript.end(); ++i) + { + if (!(*i)->IsAffectingSpell(spellProto)) + continue; + + switch ((*i)->GetMiscValue()) + { + case 4920: // Molten Fury + case 4919: + { + if (victim->HasAuraState(AURA_STATE_HEALTHLESS_35_PERCENT, spellProto, this)) + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + } + case 6917: // Death's Embrace damage effect + case 6926: + case 6928: + { + // Health at 25% or less (25% stored at effect 2 of the spell) + if (victim->HealthBelowPct(CalculateSpellDamage(this, (*i)->GetSpellInfo(), EFFECT_2))) + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + } + case 6916: // Death's Embrace heal effect + case 6925: + case 6927: + if (HealthBelowPct(CalculateSpellDamage(this, (*i)->GetSpellInfo(), EFFECT_2))) + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + case 4992: // Soul Siphon + case 4993: + { + // EFFECT_0 contains the bonus amount per application, EFFECT_1 contains the maximum bonus amount + int32 bonusPerApplication = CalculateSpellDamage(this, (*i)->GetSpellInfo(), EFFECT_0); + int32 pctBonus = 0; + for (auto const& itr : victim->GetAppliedAuras()) + { + Aura const* aura = itr.second->GetBase(); + if (aura->GetCasterGUID() != GetGUID() || !aura->GetSpellInfo()->IsAbilityOfSkillType(SKILL_AFFLICTION)) + continue; + + pctBonus += bonusPerApplication; + } + + pctBonus = std::min(pctBonus, (*i)->GetAmount()); + AddPct(DoneTotalMod, pctBonus); + break; + } + case 5481: // Starfire Bonus + { + if (victim->GetAuraEffect(SPELL_AURA_PERIODIC_DAMAGE, SPELLFAMILY_DRUID, 0x200002, 0, 0)) + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + } + } + } + + // Add SPELL_AURA_MOD_DAMAGE_FROM_MANA percent bonus + if (HasAuraType(SPELL_AURA_MOD_DAMAGE_FROM_MANA) && GetMaxPower(POWER_MANA)) + { + float totalBonus = owner->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_FROM_MANA, spellProto->GetSchoolMask()) - 1.f; + totalBonus *= ((float)GetPower(POWER_MANA) / GetMaxPower(POWER_MANA)); + if (totalBonus != 0.f) + DoneTotalMod += DoneTotalMod * totalBonus; + } + + // Custom scripted damage + switch (spellProto->SpellFamilyName) + { + case SPELLFAMILY_MAGE: + // Mastery: Frostburn + if (AuraEffect* aurEff = GetAuraEffect(76613, 0)) + if (victim->HasAuraState(AURA_STATE_FROZEN)) + if (aurEff->IsAffectingSpell(spellProto)) + AddPct(DoneTotalMod, aurEff->GetAmount()); + + // Ice Lance + if (spellProto->SpellIconID == 186) + { + if (victim->HasAuraState(AURA_STATE_FROZEN, spellProto, this) + || GetAuraEffect(SPELL_AURA_ABILITY_IGNORE_AURASTATE, SPELLFAMILY_MAGE, 0, 0, 0x0000000A, GetGUID())) + DoneTotalMod *= 2.0f; + } + + // Fingers of Frost + if (spellProto->SpellFamilyFlags[0] & 0x00020000) + if (GetAuraEffect(SPELL_AURA_ABILITY_IGNORE_AURASTATE, SPELLFAMILY_MAGE, 0, 0, 0x0000000A, GetGUID())) + AddPct(DoneTotalMod, 25.0f); + + // Torment the weak + if (spellProto->GetSchoolMask() & SPELL_SCHOOL_MASK_ARCANE) + { + if (victim->HasAuraWithMechanic((1<GetSpellInfo()->SpellIconID == 2215) + { + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + } + } + } + } + break; + case SPELLFAMILY_PRIEST: + // Smite + if (spellProto->SpellFamilyFlags[0] & 0x80) + { + // Glyph of Smite + if (AuraEffect* aurEff = GetAuraEffect(55692, 0)) + if (victim->GetAuraEffect(SPELL_AURA_PERIODIC_DAMAGE, SPELLFAMILY_PRIEST, 0x100000, 0, 0, GetGUID())) + AddPct(DoneTotalMod, aurEff->GetAmount()); + } + break; + case SPELLFAMILY_DEATHKNIGHT: + // Sigil of the Vengeful Heart + if (spellProto->SpellFamilyFlags[0] & 0x2000) + if (AuraEffect* aurEff = GetAuraEffect(64962, EFFECT_1)) + DoneTotalMod += aurEff->GetAmount(); + break; + } + + return DoneTotalMod; +} + +int32 Unit::SpellDamageBonusTaken(Unit* caster, SpellInfo const* spellProto, int32 pdamage, DamageEffectType damagetype) const +{ + if (!spellProto || damagetype == DIRECT_DAMAGE) + return pdamage; + + float TakenTotalMod = 1.0f; + + // Mod damage from spell mechanic + if (uint32 mechanicMask = spellProto->GetAllEffectsMechanicMask()) + { + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT, [mechanicMask, spellProto](AuraEffect const* aurEff) + { + if (spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if (mechanicMask & uint32(1 << (aurEff->GetMiscValue()))) + return true; + return false; + }); + } + + //.. taken pct: dummy auras + AuraEffectList const& mDummyAuras = GetAuraEffectsByType(SPELL_AURA_DUMMY); + for (AuraEffectList::const_iterator i = mDummyAuras.begin(); i != mDummyAuras.end(); ++i) + { + switch ((*i)->GetSpellInfo()->SpellIconID) + { + // Cheat Death + case 2109: + if ((*i)->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL) + AddPct(TakenTotalMod, (*i)->GetAmount()); + break; + } + } + + // Spells with SPELL_ATTR4_IGNORE_DAMAGE_TAKEN_MODIFIERS should only benefit from mechanic damage mod auras. + if (!spellProto->HasAttribute(SPELL_ATTR4_IGNORE_DAMAGE_TAKEN_MODIFIERS)) + { + // from positive and negative SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN + // multiplicative bonus, for example Dispersion + Shadowform (0.10*0.85=0.085) + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN, [spellProto](AuraEffect const* aurEff) + { + if (spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if ((aurEff->GetMiscValue() & spellProto->GetSchoolMask()) != 0) + return true; + + return false; + }); + + // From caster spells + if (caster) + { + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_FROM_CASTER, [caster, spellProto](AuraEffect const* aurEff) + { + if (spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if (aurEff->GetCasterGUID() == caster->GetGUID() && aurEff->IsAffectingSpell(spellProto)) + return true; + return false; + }); + } + } + + // Sanctified Wrath (bypass damage reduction) + if (caster && TakenTotalMod < 1.0f) + { + float damageReduction = 1.0f - TakenTotalMod; + Unit::AuraEffectList const& casterIgnoreResist = caster->GetAuraEffectsByType(SPELL_AURA_MOD_IGNORE_TARGET_RESIST); + for (AuraEffect const* aurEff : casterIgnoreResist) + { + if (!(aurEff->GetMiscValue() & spellProto->GetSchoolMask())) + continue; + + AddPct(damageReduction, -aurEff->GetAmount()); + } + + TakenTotalMod = 1.0f - damageReduction; + } + + float tmpDamage = pdamage * TakenTotalMod; + return int32(std::max(tmpDamage, 0.0f)); +} + +int32 Unit::SpellBaseDamageBonusDone(SpellSchoolMask schoolMask, bool withSpellPowerPctMod /*= false*/) const +{ + if (IsPlayer()) + { + float overrideSP = GetFloatValue(PLAYER_FIELD_OVERRIDE_SPELL_POWER_BY_AP_PCT); + if (overrideSP > 0.0f) + return int32(CalculatePct(GetTotalAttackPowerValue(BASE_ATTACK), overrideSP) + 0.5f); + } + + int32 DoneAdvertisedBenefit = GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_DAMAGE_DONE, schoolMask); + + if (Player const* player = ToPlayer()) + { + // Base value + DoneAdvertisedBenefit += player->GetBaseSpellPowerBonus(); + + // Check if we are ever using mana - PaperDollFrame.lua + if (GetPowerIndex(POWER_MANA) != MAX_POWERS) + DoneAdvertisedBenefit += std::max(0, int32(GetStat(StatType::Intellect)) - 10); // spellpower from intellect + + // Spell Power bonus from SPELL_AURA_MOD_SPELL_POWER_PCT + if (withSpellPowerPctMod) + DoneAdvertisedBenefit *= player->GetFloatValue(PLAYER_FIELD_MOD_SPELL_POWER_PCT); + + // Damage bonus from stats + AuraEffectList const& mDamageDoneOfStatPercent = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT); + for (AuraEffect const* aurEff : mDamageDoneOfStatPercent) + { + if ((aurEff->GetMiscValue() & schoolMask) != 0) + { + // stat used stored in miscValueB for this aura + StatType const usedStat = static_cast(aurEff->GetMiscValueB()); + DoneAdvertisedBenefit += static_cast(CalculatePct(GetStat(usedStat), aurEff->GetAmount())); + } + } + + // ... and attack power + DoneAdvertisedBenefit += static_cast(CalculatePct(GetTotalAttackPowerValue(BASE_ATTACK), GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_SPELL_DAMAGE_OF_ATTACK_POWER, schoolMask))); + } + + return DoneAdvertisedBenefit; +} + +float Unit::SpellCritChanceDone(SpellInfo const* spellInfo, SpellSchoolMask schoolMask, WeaponAttackType attackType /*= BASE_ATTACK*/, bool isPeriodic /*= false*/) const +{ + //! Mobs can't crit with spells. (Except player controlled) + if (GetTypeId() == TYPEID_UNIT && !GetSpellModOwner()) + return 0.0f; + + // not critting spell + if (!isPeriodic && !spellInfo->HasAttribute(SPELL_ATTR0_CU_CAN_CRIT)) + return 0.0f; + + float crit_chance = 0.0f; + switch (spellInfo->DmgClass) + { + case SPELL_DAMAGE_CLASS_MAGIC: + { + if (schoolMask & SPELL_SCHOOL_MASK_NORMAL) + crit_chance = 0.0f; + // For other schools + else if (GetTypeId() == TYPEID_PLAYER) + crit_chance = GetFloatValue(PLAYER_SPELL_CRIT_PERCENTAGE1 + uint32(GetFirstSchoolInMask(schoolMask))); + else + { + crit_chance = (float)m_baseSpellCritChance; + crit_chance += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL, schoolMask); + } + break; + } + case SPELL_DAMAGE_CLASS_MELEE: + case SPELL_DAMAGE_CLASS_RANGED: + { + crit_chance += GetUnitCriticalChanceDone(attackType); + crit_chance += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL, schoolMask); + break; + } + case SPELL_DAMAGE_CLASS_NONE: + default: + return 0.0f; + } + // percent done + // only players use intelligence for critical chance computations + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellInfo, SpellModOp::CritChance, crit_chance); + + return std::max(crit_chance, 0.0f); +} + +float Unit::SpellCritChanceTaken(Unit const* caster, SpellInfo const* spellInfo, SpellSchoolMask schoolMask, float doneChance, WeaponAttackType attackType /*= BASE_ATTACK*/, bool isPeriodic /*= false*/) const +{ + // not critting spell + if (!isPeriodic && !spellInfo->HasAttribute(SPELL_ATTR0_CU_CAN_CRIT)) + return 0.0f; + + float crit_chance = doneChance; + switch (spellInfo->DmgClass) + { + case SPELL_DAMAGE_CLASS_MAGIC: + { + // taken + if (!spellInfo->IsPositive()) + { + // Modify critical chance by victim SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE + crit_chance += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE, schoolMask); + + // Modify critical chance by victim SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE + // applied after resilience + crit_chance += GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE); + } + + // scripted (increase crit chance ... against ... target by x% + if (caster) + { + AuraEffectList const& mOverrideClassScript = caster->GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS); + for (AuraEffect const* aurEff : mOverrideClassScript) + { + if (!aurEff->IsAffectingSpell(spellInfo)) + continue; + + switch (aurEff->GetMiscValue()) + { + case 911: // Shatter + if (HasAuraState(AURA_STATE_FROZEN, spellInfo, caster)) + crit_chance *= (1 * aurEff->GetSpellInfo()->GetRank()); + else if (spellInfo->SpellFamilyFlags[0] & 0x00020000 || spellInfo->SpellFamilyFlags[1] & 0x00100000) + if (GetAuraEffect(SPELL_AURA_ABILITY_IGNORE_AURASTATE, SPELLFAMILY_MAGE, 0, 0, 0x0000000A, caster->GetGUID())) + crit_chance *= (1 * aurEff->GetSpellInfo()->GetRank()); + break; + case 57470: // Renewed Hope + case 57472: + if (HasAura(6788) || HasAura(47930)) + crit_chance += aurEff->GetAmount(); + break; + default: + break; + } + } + // Custom crit by class + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_MAGE: + // Glyph of Fire Blast + if (spellInfo->SpellFamilyFlags[0] == 0x2 && spellInfo->SpellIconID == 12) + if (HasAuraWithMechanic((1 << MECHANIC_STUN) | (1 << MECHANIC_KNOCKOUT))) + if (AuraEffect const* aurEff = caster->GetAuraEffect(56369, EFFECT_0)) + crit_chance += aurEff->GetAmount(); + break; + case SPELLFAMILY_ROGUE: + // Shiv-applied poisons can't crit + if (caster->FindCurrentSpellBySpellId(5938)) + crit_chance = 0.0f; + break; + case SPELLFAMILY_PALADIN: + // Flash of light + if (spellInfo->SpellFamilyFlags[0] & 0x40000000) + { + // Sacred Shield + if (AuraEffect const* aura = GetAuraEffect(58597, 1, GetGUID())) + crit_chance += aura->GetAmount(); + break; + } + // Exorcism + else if (spellInfo->GetCategory() == 19) + { + if (GetCreatureTypeMask() & CREATURE_TYPEMASK_DEMON_OR_UNDEAD) + return 100.0f; + break; + } + + // Last Word + if (AuraEffect const* effect = caster->GetDummyAuraEffect(SPELLFAMILY_PALADIN, 2139, EFFECT_0)) + if (effect->IsAffectingSpell(spellInfo)) + if (HasAuraState(AURA_STATE_HEALTHLESS_35_PERCENT)) + crit_chance += effect->GetAmount(); + break; + case SPELLFAMILY_SHAMAN: + // Lava Burst + if ((spellInfo->SpellFamilyFlags[1] & 0x1000) != 0 || (spellInfo->SpellFamilyFlags[2] & 0x400000) != 0) + { + if (GetAuraEffect(SPELL_AURA_PERIODIC_DAMAGE, SPELLFAMILY_SHAMAN, 0x10000000, 0, 0, caster->GetGUID())) + if (GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE) > -100) + return 100.0f; + break; + } + break; + case SPELLFAMILY_WARLOCK: + // Searing Pain - Soulburn bonus + if (spellInfo->SpellFamilyFlags[0] & 0x00000100) + if (HasAura(74434, GetGUID())) + return 100.0f; + + // Improved Searing Pain + if (AuraEffect const* effect = caster->GetDummyAuraEffect(SPELLFAMILY_WARLOCK, 816, EFFECT_0)) + if (effect->IsAffectingSpell(spellInfo)) + if (GetHealthPct() <= 25.f) + crit_chance += effect->GetAmount(); + break; + } + + // Spell crit suppression + if (GetTypeId() == TYPEID_UNIT) + { + int32 const levelDiff = static_cast(getLevelForTarget(caster)) - caster->getLevel(); + if (levelDiff > 0) + crit_chance -= levelDiff * 0.7f; + } + } + break; + } + case SPELL_DAMAGE_CLASS_MELEE: + { + // Custom crit by class + if (caster) + { + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_DRUID: + // Rend and Tear - bonus crit chance for Ferocious Bite on bleeding targets + if (spellInfo->SpellFamilyFlags[0] & 0x00800000 + && spellInfo->SpellIconID == 1680 + && HasAuraState(AURA_STATE_BLEEDING)) + { + if (AuraEffect const* rendAndTear = caster->GetDummyAuraEffect(SPELLFAMILY_DRUID, 2859, 1)) + crit_chance += rendAndTear->GetAmount(); + break; + } + + // Predatory Strikes + if (spellInfo->SpellFamilyFlags[0] & 0x00010000) + if (AuraEffect const* critAuraEff = caster->GetDummyAuraEffect(SPELLFAMILY_DRUID, 1563, EFFECT_0)) + if (AuraEffect const* healthAuraEff = caster->GetDummyAuraEffect(SPELLFAMILY_DRUID, 1563, EFFECT_1)) + if (GetHealthPct() >= healthAuraEff->GetAmount()) + crit_chance += critAuraEff->GetAmount(); + break; + case SPELLFAMILY_HUNTER: + // Kill Command - Improved Kill Command + if (spellInfo->SpellFamilyFlags[1] & 0x00000800) + if (Player* player = caster->GetSpellModOwner()) + if (AuraEffect const* improvedKillCommand = player->GetDummyAuraEffect(SPELLFAMILY_HUNTER, 2221, EFFECT_0)) + crit_chance += improvedKillCommand->GetAmount(); + break; + } + } + [[fallthrough]]; // Calculate critical strike chance for both Ranged and Melee spells + } + case SPELL_DAMAGE_CLASS_RANGED: + if (caster) + { + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_HUNTER: + { + // Careful Aim + if (GetHealthPct() >= 90) + { + switch (spellInfo->Id) + { + case 56641: // Steady Shot + case 77767: // Cobra Shot + case 19434: // Aimed Shot + case 82928: // Aimed Shot! + if (AuraEffect* aurEff = caster->GetDummyAuraEffect(SPELLFAMILY_HUNTER, 2222, 0)) + crit_chance += aurEff->GetAmount(); + break; + default: + break; + } + } + break; + } + } + crit_chance = GetUnitCriticalChanceTaken(attackType, crit_chance); + } + break; + case SPELL_DAMAGE_CLASS_NONE: + default: + return 0.f; + } + + // for this types the bonus was already added in GetUnitCriticalChance, do not add twice + if (caster) + { + crit_chance += GetTotalAuraModifier(SPELL_AURA_MOD_CRIT_CHANCE_FOR_CASTER, [caster, spellInfo](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetCasterGUID() == caster->GetGUID() && aurEff->IsAffectingSpell(spellInfo)) + return true; + return false; + }); + } + + return std::max(crit_chance, 0.0f); +} + +/*static*/ uint32 Unit::SpellCriticalDamageBonus(Unit const* caster, SpellInfo const* spellProto, uint32 damage, bool periodicDamage /*= false*/) +{ + // Calculate critical bonus + int32 crit_bonus = damage; + float crit_mod = 0.0f; + + switch (spellProto->DmgClass) + { + // Spells which are either marked as ranged or melee damage, will deal 100% additional damage + case SPELL_DAMAGE_CLASS_MELEE: + crit_bonus += damage; + break; + case SPELL_DAMAGE_CLASS_RANGED: + // Ranged spells which have magic periodic damage effect on the other hand, only deal 50% damage, just like spells + // This is a very specific case that is currently only known for Hunter spells, such as Serpent Sting and Black Arrow + if (periodicDamage && (spellProto->GetSchoolMask() & SPELL_SCHOOL_MASK_MAGIC) != 0) + crit_bonus += damage / 2; + else + crit_bonus += damage; + break; + default: + // Magic spells will simply deal 50% additional crit damage + crit_bonus += damage / 2; + break; + } + + if (caster) + { + crit_mod += (caster->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_CRIT_DAMAGE_BONUS, spellProto->GetSchoolMask()) - 1.0f) * 100; + + if (crit_bonus != 0) + AddPct(crit_bonus, crit_mod); + + crit_bonus -= damage; + + if (damage > uint32(crit_bonus)) + { + // adds additional damage to critBonus (from talents) + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, SpellModOp::CritDamageAndHealing, crit_bonus); + } + + crit_bonus += damage; + } + + return crit_bonus; +} + +/*static*/ uint32 Unit::SpellCriticalHealingBonus(Unit const* caster, uint32 damage) +{ + // As of Patch 4.2.0 (2011-06-28): all healing spells now crit for 200% instead of 150% + damage *= 2; + + if (caster) + damage = int32(float(damage) * caster->GetTotalAuraMultiplier(SPELL_AURA_MOD_CRITICAL_HEALING_AMOUNT)); + + return damage; +} + +int32 Unit::SpellHealingBonusDone(Unit* victim, SpellInfo const* spellProto, int32 healamount, DamageEffectType damagetype, uint8 effIndex, uint32 stack /*= 1*/, Spell* spell /*= nullptr*/, AuraEffect const* aurEff /*= nullptr*/) const +{ + // For totems get healing bonus from owner (statue isn't totem in fact) + if (GetTypeId() == TYPEID_UNIT && IsTotem()) + if (Unit* creator = ObjectAccessor::GetUnit(*this, GetCreatorGUID())) + return creator->SpellHealingBonusDone(victim, spellProto, healamount, damagetype, effIndex, stack, spell, aurEff); + + // Some spells don't benefit from done mods + if (spellProto->HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS)) + return healamount; + + // No bonus healing for potion spells + if (spellProto->SpellFamilyName == SPELLFAMILY_POTION) + return healamount; + + int32 DoneTotal = 0; + float DoneTotalMod = SpellHealingPctDone(victim, spellProto); + + // done scripted mod (take it from owner) + Unit const* owner = GetOwner() ? GetOwner() : this; + for (AuraEffect const* overrideClassScripts : owner->GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS)) + { + if (!overrideClassScripts->IsAffectingSpell(spellProto)) + continue; + + switch (overrideClassScripts->GetMiscValue()) + { + case 4415: // Increased Rejuvenation Healing + case 4953: + DoneTotal += overrideClassScripts->GetAmount(); + break; + default: + break; + } + } + + // Done fixed damage bonus auras + int32 DoneAdvertisedBenefit = SpellBaseHealingBonusDone(spellProto->GetSchoolMask(), true); + // modify spell power by victim's SPELL_AURA_MOD_HEALING auras (eg Amplify/Dampen Magic) + DoneAdvertisedBenefit += victim->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_HEALING, spellProto->GetSchoolMask()); + + // Pets just add their bonus damage to their spell damage + // note that their spell damage is just gain of their own auras + if (HasUnitTypeMask(UNIT_MASK_GUARDIAN)) + DoneAdvertisedBenefit += static_cast(this)->GetBonusDamage(); + + + // Check for table values + if (spellProto->BonusCoefficientFromAP > 0.0f) + { + WeaponAttackType const attType = (spellProto->IsRangedWeaponSpell() && spellProto->DmgClass != SPELL_DAMAGE_CLASS_MELEE) ? RANGED_ATTACK : BASE_ATTACK; + float APbonus = float(victim->GetTotalAuraModifier(attType == BASE_ATTACK ? SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS : SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS)); + APbonus += GetTotalAttackPowerValue(attType); + + DoneTotal += int32(spellProto->BonusCoefficientFromAP * stack * APbonus); + } + + // Default calculation + if (DoneAdvertisedBenefit) + { + float coeff = spellProto->Effects[effIndex].BonusMultiplier * spellProto->GetSpellScalingMultiplier(getLevel(), false); + if (Player* modOwner = GetSpellModOwner()) + { + coeff *= 100.0f; + modOwner->ApplySpellMod(spellProto, SpellModOp::BonusCoefficient, coeff); + coeff /= 100.0f; + } + + DoneTotal += int32(DoneAdvertisedBenefit * coeff * stack); + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (spellProto->Effects[i].ApplyAuraName) + { + // Bonus healing does not apply to these spells + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_HEALTH_FUNNEL: + DoneTotal = 0; + break; + } + if (spellProto->Effects[i].Effect == SPELL_EFFECT_HEALTH_LEECH) + DoneTotal = 0; + } + + if (spell) + spell->CallScriptCalcHealingHandlers(victim, healamount, DoneTotal, DoneTotalMod); + else if (aurEff) + aurEff->GetBase()->CallScriptCalcDamageAndHealingHandlers(aurEff, aurEff->GetBase()->GetApplicationOfTarget(victim->GetGUID()), victim, healamount, DoneTotal, DoneTotalMod); + + float heal = float(healamount + DoneTotal) * DoneTotalMod; + + + // apply spellmod to Done amount + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, damagetype == DOT ? SpellModOp::PeriodicHealingAndDamage : SpellModOp::HealingAndDamage, heal); + + return int32(std::max(heal, 0.0f)); +} + +float Unit::SpellHealingPctDone(Unit* victim, SpellInfo const* spellProto) const +{ + // For totems pct done mods are calculated when its calculation is run on the player in SpellHealingBonusDone. + if (GetTypeId() == TYPEID_UNIT && IsTotem()) + return 1.0f; + + // No bonus healing for potion spells + if (spellProto->SpellFamilyName == SPELLFAMILY_POTION) + return 1.0f; + + float DoneTotalMod = 1.0f; + + // Healing done percent + if (IsPlayer()) + DoneTotalMod *= GetFloatValue(PLAYER_FIELD_MOD_HEALING_DONE_PCT); + else + DoneTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_HEALING_DONE_PERCENT); + + // done scripted mod (take it from owner) + Unit const* owner = GetOwner() ? GetOwner() : this; + for (AuraEffect const* overrideClassScripts : owner->GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS)) + { + if (!overrideClassScripts->IsAffectingSpell(spellProto)) + continue; + + switch (overrideClassScripts->GetMiscValue()) + { + case 21: // Test of Faith + case 6935: + case 6918: + if (victim->HealthBelowPct(50)) + AddPct(DoneTotalMod, overrideClassScripts->GetAmount()); + break; + case 8477: // Nourish Heal Boost + { + int32 modPercent = 0; + for (AuraEffect const* periodicHeal : victim->GetAuraEffectsByType(SPELL_AURA_PERIODIC_HEAL)) + { + if (periodicHeal->GetCasterGUID() != GetGUID() || periodicHeal->GetSpellInfo()->SpellFamilyName != SPELLFAMILY_DRUID) + continue; + + if (periodicHeal->GetSpellInfo()->SpellFamilyFlags.HasFlag(0x10 | 0x40, 0x10 | 0x4000000, 0)) + modPercent += overrideClassScripts->GetAmount() * periodicHeal->GetBase()->GetStackAmount(); + } + AddPct(DoneTotalMod, modPercent); + break; + } + default: + break; + } + } + + // Custom scripted damage + switch (spellProto->SpellFamilyName) + { + case SPELLFAMILY_SHAMAN: + { + // Nature's Blessing + if (AuraEffect const* blessing = GetDummyAuraEffect(SPELLFAMILY_SHAMAN, 2012, EFFECT_0)) + if (blessing->IsAffectingSpell(spellProto) && victim->HasAura(974, GetGUID())) + AddPct(DoneTotalMod, blessing->GetAmount()); + break; + } + default: + break; + } + + float healthPctDiff = 100.f - victim->GetHealthPct(); + for (AuraEffect const* healingDonePctVsTargetHealth : owner->GetAuraEffectsByType(SPELL_AURA_MOD_HEALING_DONE_PCT_VERSUS_TARGET_HEALTH)) + { + if (!healingDonePctVsTargetHealth->IsAffectingSpell(spellProto)) + continue; + + AddPct(DoneTotalMod, CalculatePct(float(healingDonePctVsTargetHealth->GetAmount()), healthPctDiff)); + } + + return DoneTotalMod; +} + +int32 Unit::SpellHealingBonusTaken(Unit* caster, SpellInfo const* spellProto, int32 healamount, DamageEffectType damagetype) const +{ + float TakenTotalMod = 1.0f; + + // Healing taken percent + if (IsPlayer()) + TakenTotalMod *= GetFloatValue(PLAYER_FIELD_MOD_HEALING_PCT); + else + { + float minval = float(GetMaxNegativeAuraModifier(SPELL_AURA_MOD_HEALING_PCT)); + if (minval) + AddPct(TakenTotalMod, minval); + + float maxval = float(GetMaxPositiveAuraModifier(SPELL_AURA_MOD_HEALING_PCT)); + if (maxval) + AddPct(TakenTotalMod, maxval); + } + + // Tenacity increase healing % taken + if (AuraEffect const* Tenacity = GetAuraEffect(58549, 0)) + AddPct(TakenTotalMod, Tenacity->GetAmount()); + + // Unused in Cataclysm (15595) + if (damagetype == DOT) + { + // Healing over time taken percent + float minval_hot = float(GetMaxNegativeAuraModifier(SPELL_AURA_259)); + if (minval_hot) + AddPct(TakenTotalMod, minval_hot); + + float maxval_hot = float(GetMaxPositiveAuraModifier(SPELL_AURA_259)); + if (maxval_hot) + AddPct(TakenTotalMod, maxval_hot); + } + + if (caster) + { + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_HEALING_RECEIVED, [caster, spellProto](AuraEffect const* aurEff) -> bool + { + if (caster->GetGUID() == aurEff->GetCasterGUID() && aurEff->IsAffectingSpell(spellProto)) + return true; + return false; + }); + } + + float heal = healamount * TakenTotalMod; + + return int32(std::max(heal, 0.0f)); +} + +int32 Unit::SpellBaseHealingBonusDone(SpellSchoolMask schoolMask, bool withSpellPowerPctMod /*= false*/) const +{ + if (IsPlayer()) + { + float overrideSP = GetFloatValue(PLAYER_FIELD_OVERRIDE_SPELL_POWER_BY_AP_PCT); + if (overrideSP > 0.0f) + return int32(CalculatePct(GetTotalAttackPowerValue(BASE_ATTACK), overrideSP) + 0.5f); + } + + int32 advertisedBenefit = GetTotalAuraModifier(SPELL_AURA_MOD_HEALING_DONE, [schoolMask](AuraEffect const* aurEff) -> bool + { + if (!aurEff->GetMiscValue() || (aurEff->GetMiscValue() & schoolMask) != 0) + return true; + return false; + }); + + // Healing bonus of spirit, intellect and strength + if (Player const* player = ToPlayer()) + { + // Base value + advertisedBenefit += player->GetBaseSpellPowerBonus(); + + // Check if we are ever using mana - PaperDollFrame.lua + if (GetPowerIndex(POWER_MANA) != MAX_POWERS) + advertisedBenefit += std::max(0, int32(GetStat(StatType::Intellect)) - 10); // spellpower from intellect + + // Spell Power bonus from SPELL_AURA_MOD_SPELL_POWER_PCT + if (withSpellPowerPctMod) + advertisedBenefit *= player->GetFloatValue(PLAYER_FIELD_MOD_SPELL_POWER_PCT); + + // Healing bonus from stats + AuraEffectList const& mHealingDoneOfStatPercent = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT); + for (AuraEffectList::const_iterator i = mHealingDoneOfStatPercent.begin(); i != mHealingDoneOfStatPercent.end(); ++i) + { + // stat used dependent from misc value (stat index) + StatType usedStat = StatType((*i)->GetSpellInfo()->Effects[(*i)->GetEffIndex()].MiscValue); + advertisedBenefit += int32(CalculatePct(GetStat(usedStat), (*i)->GetAmount())); + } + + // ... and attack power + advertisedBenefit += CalculatePct(GetTotalAttackPowerValue(BASE_ATTACK), GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_SPELL_HEALING_OF_ATTACK_POWER, schoolMask)); + } + return advertisedBenefit; +} + +bool Unit::IsImmunedToDamage(SpellSchoolMask schoolMask) const +{ + if (schoolMask == SPELL_SCHOOL_MASK_NONE) + return false; + + // If m_immuneToSchool type contain this school type, IMMUNE damage. + uint32 schoolImmunityMask = GetSchoolImmunityMask(); + if ((schoolImmunityMask & schoolMask) == schoolMask) // We need to be immune to all types + return true; + + // If m_immuneToDamage type contain magic, IMMUNE damage. + uint32 damageImmunityMask = GetDamageImmunityMask(); + if ((damageImmunityMask & schoolMask) == schoolMask) // We need to be immune to all types + return true; + + return false; +} + +bool Unit::IsImmunedToDamage(SpellInfo const* spellInfo) const +{ + if (!spellInfo) + return false; + + // for example 40175 + if (spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES) && spellInfo->HasAttribute(SPELL_ATTR3_ALWAYS_HIT)) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) || spellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + return false; + + if (uint32 schoolMask = spellInfo->GetSchoolMask()) + { + // If m_immuneToSchool type contain this school type, IMMUNE damage. + uint32 schoolImmunityMask = 0; + SpellImmuneContainer const& schoolList = m_spellImmune[IMMUNITY_SCHOOL]; + for (auto itr = schoolList.begin(); itr != schoolList.end(); ++itr) + if ((itr->first & schoolMask) && !spellInfo->CanPierceImmuneAura(sSpellMgr->GetSpellInfo(itr->second))) + schoolImmunityMask |= itr->first; + + // // We need to be immune to all types + if ((schoolImmunityMask & schoolMask) == schoolMask) + return true; + + // If m_immuneToDamage type contain magic, IMMUNE damage. + uint32 damageImmunityMask = GetDamageImmunityMask(); + if ((damageImmunityMask & schoolMask) == schoolMask) // We need to be immune to all types + return true; + } + + return false; +} + +bool Unit::IsImmunedToSpell(SpellInfo const* spellInfo, WorldObject const* caster, bool requireImmunityPurgesEffectAttribute /*= false*/) const +{ + if (!spellInfo) + return false; + + auto hasImmunity = [requireImmunityPurgesEffectAttribute](SpellImmuneContainer const& container, uint32 key) + { + Trinity::IteratorPair range = Trinity::Containers::MapEqualRange(container, key); + if (!requireImmunityPurgesEffectAttribute) + return range.begin() != range.end(); + + return std::any_of(range.begin(), range.end(), [](SpellImmuneContainer::value_type const& entry) + { + if (SpellInfo const* immunitySourceSpell = sSpellMgr->GetSpellInfo(entry.second)) + if (immunitySourceSpell->HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + return true; + + return false; + }); + }; + + // Single spell immunity. + SpellImmuneContainer const& idList = m_spellImmune[IMMUNITY_ID]; + if (hasImmunity(idList, spellInfo->Id)) + return true; + + if (spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + if (uint32 dispel = spellInfo->Dispel) + { + SpellImmuneContainer const& dispelList = m_spellImmune[IMMUNITY_DISPEL]; + if (hasImmunity(dispelList, dispel)) + return true; + } + + // Spells that don't have effectMechanics. + if (uint32 mechanic = spellInfo->Mechanic) + { + SpellImmuneContainer const& mechanicList = m_spellImmune[IMMUNITY_MECHANIC]; + if (hasImmunity(mechanicList, mechanic)) + return true; + } + + bool immuneToAllEffects = true; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // State/effect immunities applied by aura expect full spell immunity + // Ignore effects with mechanic, they are supposed to be checked separately + if (!spellInfo->Effects[i].IsEffect()) + continue; + + if (!IsImmunedToSpellEffect(spellInfo, i, caster, requireImmunityPurgesEffectAttribute)) + { + immuneToAllEffects = false; + break; + } + if (spellInfo->HasAttribute(SPELL_ATTR4_NO_PARTIAL_IMMUNITY)) + return true; + } + + if (immuneToAllEffects) //Return immune only if the target is immune to all spell effects. + return true; + + if (uint32 schoolMask = spellInfo->GetSchoolMask()) + { + uint32 schoolImmunityMask = 0; + SpellImmuneContainer const& schoolList = m_spellImmune[IMMUNITY_SCHOOL]; + for (auto itr = schoolList.begin(); itr != schoolList.end(); ++itr) + { + if ((itr->first & schoolMask) == 0) + continue; + + SpellInfo const* immuneSpellInfo = sSpellMgr->GetSpellInfo(itr->second); + if (requireImmunityPurgesEffectAttribute) + if (!immuneSpellInfo || !immuneSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + continue; + + // Consider the school immune if any of these conditions are not satisfied. + // In case of no immuneSpellInfo, ignore that condition and check only the other conditions + if ((immuneSpellInfo && !immuneSpellInfo->IsPositive()) || !spellInfo->IsPositive() || !caster || !IsFriendlyTo(caster)) + if (!spellInfo->CanPierceImmuneAura(immuneSpellInfo)) + schoolImmunityMask |= itr->first; + } + + if ((schoolImmunityMask & schoolMask) == schoolMask) + return true; + } + + return false; +} + +uint32 Unit::GetSchoolImmunityMask() const +{ + uint32 mask = 0; + SpellImmuneContainer const& schoolList = m_spellImmune[IMMUNITY_SCHOOL]; + for (auto itr = schoolList.begin(); itr != schoolList.end(); ++itr) + mask |= itr->first; + + return mask; +} + +uint32 Unit::GetDamageImmunityMask() const +{ + uint32 mask = 0; + SpellImmuneContainer const& damageList = m_spellImmune[IMMUNITY_DAMAGE]; + for (auto itr = damageList.begin(); itr != damageList.end(); ++itr) + mask |= itr->first; + + return mask; +} + +uint32 Unit::GetMechanicImmunityMask() const +{ + uint32 mask = 0; + SpellImmuneContainer const& mechanicList = m_spellImmune[IMMUNITY_MECHANIC]; + for (auto itr = mechanicList.begin(); itr != mechanicList.end(); ++itr) + mask |= (1 << itr->first); + + return mask; +} + +bool Unit::IsImmunedToSpellEffect(SpellInfo const* spellInfo, uint32 index, WorldObject const* caster, + bool requireImmunityPurgesEffectAttribute /*= false*/) const +{ + if (!spellInfo || !spellInfo->Effects[index].IsEffect()) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + auto hasImmunity = [requireImmunityPurgesEffectAttribute](SpellImmuneContainer const& container, uint32 key) + { + Trinity::IteratorPair range = Trinity::Containers::MapEqualRange(container, key); + if (!requireImmunityPurgesEffectAttribute) + return range.begin() != range.end(); + + return std::any_of(range.begin(), range.end(), [](SpellImmuneContainer::value_type const& entry) + { + if (SpellInfo const* immunitySourceSpell = sSpellMgr->GetSpellInfo(entry.second)) + if (immunitySourceSpell->HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + return true; + + return false; + }); + }; + + + // If m_immuneToEffect type contain this effect type, IMMUNE effect. + uint32 effect = spellInfo->Effects[index].Effect; + SpellImmuneContainer const& effectList = m_spellImmune[IMMUNITY_EFFECT]; + if (hasImmunity(effectList, effect)) + return true; + + if (uint32 mechanic = spellInfo->Effects[index].Mechanic) + { + SpellImmuneContainer const& mechanicList = m_spellImmune[IMMUNITY_MECHANIC]; + if (hasImmunity(mechanicList, mechanic)) + return true; + } + + if (!spellInfo->HasAttribute(SPELL_ATTR3_ALWAYS_HIT)) + { + if (uint32 aura = spellInfo->Effects[index].ApplyAuraName) + { + SpellImmuneContainer const& list = m_spellImmune[IMMUNITY_STATE]; + if (hasImmunity(list, aura)) + return true; + + if (!spellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + // Check for immune to application of harmful magical effects + AuraEffectList const& immuneAuraApply = GetAuraEffectsByType(SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL); + for (AuraEffectList::const_iterator iter = immuneAuraApply.begin(); iter != immuneAuraApply.end(); ++iter) + if (((*iter)->GetMiscValue() & spellInfo->GetSchoolMask()) && // Check school + ((caster && !IsFriendlyTo(caster)) || !spellInfo->IsPositiveEffect(index))) // Harmful + return true; + } + } + } + + return false; +} + +int32 Unit::MeleeDamageBonusDone(Unit* victim, int32 damage, WeaponAttackType attType, DamageEffectType damagetype, SpellInfo const* spellProto /*= nullptr*/, Mechanics mechanic /*= MECHANIC_NONE*/, SpellSchoolMask damageSchoolMask /*= SPELL_SCHOOL_MASK_NORMAL*/, Spell* spell /*= nullptr*/, AuraEffect const* aurEff /*= nullptr*/) +{ + if (!victim || damage == 0) + return 0; + + uint32 creatureTypeMask = victim->GetCreatureTypeMask(); + + // Done fixed damage bonus auras + int32 DoneFlatBenefit = 0; + + // ..done + DoneFlatBenefit += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_DAMAGE_DONE_CREATURE, creatureTypeMask); + + // ..done + // SPELL_AURA_MOD_DAMAGE_DONE included in weapon damage + + // ..done (base at attack power for marked target and base at attack power for creature type) + int32 APbonus = 0; + + if (attType == RANGED_ATTACK) + { + APbonus += victim->GetTotalAuraModifier(SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS); + + // ..done (base at attack power and creature type) + APbonus += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_RANGED_ATTACK_POWER_VERSUS, creatureTypeMask); + } + else + { + APbonus += victim->GetTotalAuraModifier(SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS); + + // ..done (base at attack power and creature type) + APbonus += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_MELEE_ATTACK_POWER_VERSUS, creatureTypeMask); + } + + if (APbonus != 0) // Can be negative + { + bool const normalized = spellProto && spellProto->HasEffect(SPELL_EFFECT_NORMALIZED_WEAPON_DMG); + DoneFlatBenefit += int32(APbonus / 14.0f * GetAPMultiplier(attType, normalized)); + } + + // Done total percent damage auras + float DoneTotalMod = 1.0f; + + SpellSchoolMask schoolMask = spellProto ? spellProto->GetSchoolMask() : damageSchoolMask; + + if (!(schoolMask & SPELL_SCHOOL_MASK_NORMAL)) + { + // Some spells don't benefit from pct done mods + // mods for SPELL_SCHOOL_MASK_NORMAL are already factored in base melee damage calculation + if (!spellProto || !spellProto->HasAttribute(SPELL_ATTR6_IGNORE_CASTER_DAMAGE_MODIFIERS)) + { + float maxModDamagePercentSchool = 0.0f; + if (GetTypeId() == TYPEID_PLAYER) + { + for (uint32 i = SPELL_SCHOOL_HOLY; i < MAX_SPELL_SCHOOL; ++i) + if (schoolMask & (1 << i)) + maxModDamagePercentSchool = std::max(maxModDamagePercentSchool, GetFloatValue(PLAYER_FIELD_MOD_DAMAGE_DONE_PCT + i)); + } + else if (spellProto) + maxModDamagePercentSchool = GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_PERCENT_DONE, spellProto->GetSchoolMask()); + + DoneTotalMod *= maxModDamagePercentSchool; + } + } + + DoneTotalMod *= GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_DONE_VERSUS, creatureTypeMask); + + // bonus against aurastate + DoneTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_DONE_VERSUS_AURASTATE, [victim](AuraEffect const* aurEff) -> bool + { + if (victim->HasAuraState(AuraStateType(aurEff->GetMiscValue()))) + return true; + return false; + }); + + // Add SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC percent bonus + // Add SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC percent bonus + if (mechanic != MECHANIC_NONE) + AddPct(DoneTotalMod, GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC, mechanic)); + else if (spellProto && spellProto->Mechanic) + AddPct(DoneTotalMod, GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC, spellProto->Mechanic)); + + if (!spellProto) + { + // Add SPELL_AURA_MOD_AUTOATTACK_DAMAGE percent bonus + AddPct(DoneTotalMod, GetTotalAuraModifier(SPELL_AURA_MOD_AUTOATTACK_DAMAGE)); + + // Add SPELL_AURA_MOD_MELEE_DAMAGE_FROM_CASTER percent bonus + // This aura name is a bit misleading but it's actually only suposed to increase melee auto attack damage + AddPct(DoneTotalMod, victim->GetTotalAuraMultiplier(SPELL_AURA_MOD_MELEE_DAMAGE_FROM_CASTER, [this](AuraEffect const* effect)->bool + { + return effect->GetCasterGUID() == GetGUID(); + })); + } + + if (spell) + spell->CallScriptCalcDamageHandlers(victim, damage, DoneFlatBenefit, DoneTotalMod); + else if (aurEff) + aurEff->GetBase()->CallScriptCalcDamageAndHealingHandlers(aurEff, aurEff->GetBase()->GetApplicationOfTarget(victim->GetGUID()), victim, damage, DoneFlatBenefit, DoneTotalMod); + + float damageF = float(damage + DoneFlatBenefit) * DoneTotalMod; + + // apply spellmod to Done damage + if (spellProto) + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, damagetype == DOT ? SpellModOp::PeriodicHealingAndDamage : SpellModOp::HealingAndDamage, damageF); + + // bonus result can be negative + return int32(std::max(damageF, 0.0f)); +} + +int32 Unit::MeleeDamageBonusTaken(Unit* attacker, int32 pdamage, WeaponAttackType attType, SpellInfo const* spellProto /*= nullptr*/, SpellSchoolMask damageSchoolMask /*= SPELL_SCHOOL_MASK_NORMAL*/) +{ + if (pdamage == 0) + return 0; + + int32 TakenFlatBenefit = 0; + + uint32 meleeDamageSchoolMask = spellProto ? spellProto->GetSchoolMask() : attacker->GetMeleeDamageSchoolMask(); + + // ..taken + TakenFlatBenefit += GetTotalAuraModifier(SPELL_AURA_MOD_DAMAGE_TAKEN, [meleeDamageSchoolMask, spellProto](AuraEffect const* aurEff) + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if ((aurEff->GetMiscValue() & meleeDamageSchoolMask) != 0) + return true; + + return false; + }); + + AuraType modDamageTaken = SPELL_AURA_MOD_RANGED_DAMAGE_TAKEN; + if (attType != RANGED_ATTACK) + modDamageTaken = SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN; + + TakenFlatBenefit += GetTotalAuraModifier(modDamageTaken, [spellProto](AuraEffect const* aurEff) + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + return true; + }); + + if ((TakenFlatBenefit < 0) && (pdamage < -TakenFlatBenefit)) + return 0; + + // Taken total percent damage auras + float TakenTotalMod = 1.0f; + + // ..taken + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN, [meleeDamageSchoolMask, spellProto](AuraEffect const* aurEff) + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if ((aurEff->GetMiscValue() & meleeDamageSchoolMask) != 0) + return true; + + return false; + }); + + // .. taken pct (special attacks) + if (spellProto) + { + // From caster spells + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_FROM_CASTER, [attacker, spellProto](AuraEffect const* aurEff) -> bool + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if (aurEff->GetCasterGUID() == attacker->GetGUID() && aurEff->IsAffectingSpell(spellProto)) + return true; + return false; + }); + + // Mod damage from spell mechanic + uint32 mechanicMask = spellProto->GetAllEffectsMechanicMask(); + + // Shred, Maul - "Effects which increase Bleed damage also increase Shred damage" + if (spellProto->SpellFamilyName == SPELLFAMILY_DRUID && spellProto->SpellFamilyFlags[0] & 0x00008800) + mechanicMask |= (1 << MECHANIC_BLEED); + + if (mechanicMask) + { + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT, [mechanicMask, spellProto](AuraEffect const* aurEff) -> bool + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if (mechanicMask & uint32(1 << (aurEff->GetMiscValue()))) + return true; + return false; + }); + } + } + + // .. taken pct: dummy auras + AuraEffectList const& mDummyAuras = GetAuraEffectsByType(SPELL_AURA_DUMMY); + for (AuraEffectList::const_iterator i = mDummyAuras.begin(); i != mDummyAuras.end(); ++i) + { + switch ((*i)->GetSpellInfo()->SpellIconID) + { + // Cheat Death + case 2109: + if ((*i)->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL) + { + if (Player* player = ToPlayer()) + { + float mod = player->GetRatingBonusValue(CR_RESILIENCE_PLAYER_DAMAGE_TAKEN) * (-8.0f); + AddPct(TakenTotalMod, std::max(mod, float((*i)->GetAmount()))); + } + } + break; + } + } + + + AuraType modDamageTakenPct = SPELL_AURA_MOD_RANGED_DAMAGE_TAKEN_PCT; + if (attType != RANGED_ATTACK) + modDamageTakenPct = SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN_PCT; + + TakenTotalMod *= GetTotalAuraMultiplier(modDamageTakenPct, [spellProto](AuraEffect const* aurEff) + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + return true; + }); + + // Sanctified Wrath (bypass damage reduction) + if (TakenTotalMod < 1.0f) + { + SpellSchoolMask const attackSchoolMask = spellProto ? spellProto->GetSchoolMask() : damageSchoolMask; + + float damageReduction = 1.0f - TakenTotalMod; + Unit::AuraEffectList const& casterIgnoreResist = attacker->GetAuraEffectsByType(SPELL_AURA_MOD_IGNORE_TARGET_RESIST); + for (AuraEffect const* aurEff : casterIgnoreResist) + { + if (!(aurEff->GetMiscValue() & attackSchoolMask)) + continue; + + AddPct(damageReduction, -aurEff->GetAmount()); + } + + TakenTotalMod = 1.0f - damageReduction; + } + + return int32(std::max(std::ceil(static_cast(pdamage + TakenFlatBenefit) * TakenTotalMod), 0.0f)); +} + +void Unit::ApplySpellImmune(uint32 spellId, uint32 op, uint32 type, bool apply) +{ + if (apply) + m_spellImmune[op].emplace(type, spellId); + else + { + auto bounds = m_spellImmune[op].equal_range(type); + for (auto itr = bounds.first; itr != bounds.second;) + { + if (itr->second == spellId) + itr = m_spellImmune[op].erase(itr); + else + ++itr; + } + } +} + +float Unit::GetWeaponProcChance() const +{ + // normalized proc chance for weapon attack speed + // (odd formula...) + if (isAttackReady(BASE_ATTACK)) + return (GetBaseAttackTime(BASE_ATTACK) * 1.8f / 1000.0f); + else if (haveOffhandWeapon() && isAttackReady(OFF_ATTACK)) + return (GetBaseAttackTime(OFF_ATTACK) * 1.6f / 1000.0f); + return 0; +} + +float Unit::GetPPMProcChance(uint32 WeaponSpeed, float PPM, SpellInfo const* spellProto) const +{ + // proc per minute chance calculation + if (PPM <= 0) + return 0.0f; + + // Apply chance modifer aura + if (spellProto) + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, SpellModOp::ProcFrequency, PPM); + + return std::floor((WeaponSpeed * PPM) / 600.0f); // result is chance in percents (probability = Speed_in_sec * (PPM / 60)) +} + +void Unit::Mount(uint32 mount, uint32 VehicleId, uint32 creatureEntry) +{ + if (mount) + SetUInt32Value(UNIT_FIELD_MOUNTDISPLAYID, mount); + + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_MOUNT); + + if (Player* player = ToPlayer()) + { + // mount as a vehicle + if (VehicleId) + { + if (CreateVehicleKit(VehicleId, creatureEntry)) + { + player->SendOnCancelExpectedVehicleRideAura(); + + // mounts can also have accessories + GetVehicleKit()->InstallAllAccessories(false); + } + } + + // unsummon pet + Pet* pet = player->GetPet(); + if (pet) + { + Battleground* bg = ToPlayer()->GetBattleground(); + // don't unsummon pet in arena but SetFlag UNIT_FLAG_STUNNED to disable pet's interface + if (bg && bg->isArena()) + pet->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + else + player->UnsummonPetTemporaryIfAny(); + } + + // if we have charmed npc, stun him also (everywhere) + if (Unit* charm = player->GetCharmed()) + if (charm->GetTypeId() == TYPEID_UNIT) + charm->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + + if (player->IsInWorld()) + player->SendMovementSetCollisionHeight(player->GetCollisionHeight(), UPDATE_COLLISION_HEIGHT_MOUNT); + } + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Mount); +} + +void Unit::Dismount() +{ + if (!IsMounted()) + return; + + SetUInt32Value(UNIT_FIELD_MOUNTDISPLAYID, 0); + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_MOUNT); + + if (Player* thisPlayer = ToPlayer()) + thisPlayer->SendMovementSetCollisionHeight(thisPlayer->GetCollisionHeight(), UPDATE_COLLISION_HEIGHT_MOUNT); + + WorldPackets::Misc::Dismount packet; + packet.Guid = GetGUID(); + SendMessageToSet(packet.Write(), IsPlayer()); + + // dismount as a vehicle + if (GetTypeId() == TYPEID_PLAYER && GetVehicleKit()) + RemoveVehicleKit(); + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Dismount); + + // only resummon old pet if the player is already added to a map + // this prevents adding a pet to a not created map which would otherwise cause a crash + // (it could probably happen when logging in after a previous crash) + if (Player* player = ToPlayer()) + { + if (Pet* pPet = player->GetPet()) + { + if (pPet->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED) && !pPet->HasUnitState(UNIT_STATE_STUNNED)) + pPet->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + } + else + player->ResummonPetTemporaryUnSummonedIfAny(); + + // if we have charmed npc, remove stun also + if (Unit* charm = player->GetCharmed()) + if (charm->GetTypeId() == TYPEID_UNIT && charm->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED) && !charm->HasUnitState(UNIT_STATE_STUNNED)) + charm->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + } +} + +MountCapabilityEntry const* Unit::GetMountCapability(uint32 mountType) const +{ + if (!mountType) + return nullptr; + + MountTypeEntry const* mountTypeEntry = sMountTypeStore.LookupEntry(mountType); + if (!mountTypeEntry) + return nullptr; + + + uint32 areaId = GetAreaId(); + uint32 ridingSkill = 5000; + EnumFlag mountFlags = AreaMountFlags::None; + bool isUnderwater = false; + bool isInWater = false; + + if (GetTypeId() == TYPEID_PLAYER) + ridingSkill = ToPlayer()->GetSkillValue(SKILL_RIDING); + + if (HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS)) + { + for (AuraEffect const* auraEffect : GetAuraEffectsByType(SPELL_AURA_MOUNT_RESTRICTIONS)) + mountFlags |= AreaMountFlags(auraEffect->GetMiscValue()); + } + else if (AreaTableEntry const* areaTable = sAreaTableStore.LookupEntry(areaId)) + mountFlags = areaTable->GetMountFlags(); + + ZLiquidStatus liquidStatus = GetLiquidStatus(); + isUnderwater = (liquidStatus & LIQUID_MAP_UNDER_WATER) != 0; + isInWater = (liquidStatus & LIQUID_MAP_IN_WATER) != 0; + + for (uint32 i = MAX_MOUNT_CAPABILITIES; i > 0; --i) + { + MountCapabilityEntry const* mountCapability = sMountCapabilityStore.LookupEntry(mountTypeEntry->Capability[i - 1]); + if (!mountCapability) + continue; + + if (ridingSkill < mountCapability->ReqRidingSkill) + continue; + + if (!(mountCapability->Flags & MOUNT_CAPABIILTY_FLAG_IGNORE_RESTRICTIONS)) + { + if (mountCapability->Flags & MOUNT_CAPABILITY_FLAG_GROUND && !(mountFlags.HasFlag(AreaMountFlags::AllowGroundMounts))) + continue; + if (mountCapability->Flags & MOUNT_CAPABILITY_FLAG_FLYING && !(mountFlags.HasFlag(AreaMountFlags::AllowFlyingMounts))) + continue; + if (mountCapability->Flags & MOUNT_CAPABILITY_FLAG_FLOAT && !(mountFlags.HasFlag(AreaMountFlags::AllowSurfaceSwimmingMounts))) + continue; + if (mountCapability->Flags & MOUNT_CAPABILITY_FLAG_UNDERWATER && !(mountFlags.HasFlag(AreaMountFlags::AllowUnderwaterSwimmingMounts))) + continue; + } + + // Do not allow underwater restricted capabilities when not underwater + if (!(mountCapability->Flags & MOUNT_CAPABILITY_FLAG_UNDERWATER) && isUnderwater) + continue; + + // Do not allow water surface restricted capabilities when not on water surface + if (!(mountCapability->Flags & MOUNT_CAPABILITY_FLAG_FLOAT) && isInWater) + continue; + + // Do not allow liquid restricted mounts on ground or in air + if (!(mountCapability->Flags & (MOUNT_CAPABILITY_FLAG_GROUND | MOUNT_CAPABILITY_FLAG_FLYING)) && !isUnderwater && !isInWater) + continue; + + if (mountCapability->ReqMapID != -1 && + int32(GetMapId()) != mountCapability->ReqMapID && + GetMap()->GetEntry()->ParentMapID != mountCapability->ReqMapID) + continue; + + if (mountCapability->ReqAreaID && !DBCManager::IsInArea(areaId, mountCapability->ReqAreaID)) + continue; + + if (mountCapability->ReqSpellAuraID && !HasAura(mountCapability->ReqSpellAuraID)) + continue; + + if (mountCapability->ReqSpellKnownID && !HasSpell(mountCapability->ReqSpellKnownID)) + continue; + + return mountCapability; + } + + return nullptr; +} + +void Unit::UpdateMountCapability() +{ + AuraEffectList mounts = GetAuraEffectsByType(SPELL_AURA_MOUNTED); + for (AuraEffect* aurEff : mounts) + { + aurEff->RecalculateAmount(); + if (!aurEff->GetAmount()) + aurEff->GetBase()->Remove(); + else if (MountCapabilityEntry const* capability = sMountCapabilityStore.LookupEntry(aurEff->GetAmount())) // aura may get removed by interrupt flag, reapply + if (!HasAura(capability->ModSpellAuraID)) + CastSpell(this, capability->ModSpellAuraID, aurEff); + } +} + +bool Unit::IsServiceProvider() const +{ + return HasFlag(UNIT_NPC_FLAGS, + UNIT_NPC_FLAG_VENDOR | UNIT_NPC_FLAG_TRAINER | UNIT_NPC_FLAG_FLIGHTMASTER | + UNIT_NPC_FLAG_PETITIONER | UNIT_NPC_FLAG_BATTLEMASTER | UNIT_NPC_FLAG_BANKER | + UNIT_NPC_FLAG_INNKEEPER | UNIT_NPC_FLAG_SPIRITHEALER | + UNIT_NPC_FLAG_SPIRITGUIDE | UNIT_NPC_FLAG_TABARDDESIGNER | UNIT_NPC_FLAG_AUCTIONEER); +} + +void Unit::EngageWithTarget(Unit* enemy) +{ + if (!enemy) + return; + + if (CanHaveThreatList()) + m_threatManager.AddThreat(enemy, 0.0f, nullptr, true, true); + else + SetInCombatWith(enemy); +} + +void Unit::SetImmuneToAll(bool apply, bool keepCombat) +{ + if (apply) + { + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC | UNIT_FLAG_IMMUNE_TO_NPC); + ValidateAttackersAndOwnTarget(); + if (!keepCombat) + m_combatManager.EndAllCombat(); + } + else + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC | UNIT_FLAG_IMMUNE_TO_NPC); +} + +void Unit::SetImmuneToPC(bool apply, bool keepCombat) +{ + if (apply) + { + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC); + ValidateAttackersAndOwnTarget(); + if (!keepCombat) + { + std::list toEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (pair.second->GetOther(this)->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (auto const& pair : m_combatManager.GetPvPCombatRefs()) + if (pair.second->GetOther(this)->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (CombatReference* ref : toEnd) + ref->EndCombat(); + } + } + else + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC); +} + +void Unit::SetImmuneToNPC(bool apply, bool keepCombat) +{ + if (apply) + { + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_NPC); + ValidateAttackersAndOwnTarget(); + if (!keepCombat) + { + std::list toEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (!pair.second->GetOther(this)->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (auto const& pair : m_combatManager.GetPvPCombatRefs()) + if (!pair.second->GetOther(this)->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (CombatReference* ref : toEnd) + ref->EndCombat(); + } + } + else + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_NPC); +} + +bool Unit::IsThreatened() const +{ + return !m_threatManager.IsThreatListEmpty(); +} + +bool Unit::isTargetableForAttack(bool checkFakeDeath) const +{ + if (!IsAlive()) + return false; + + if (HasFlag(UNIT_FIELD_FLAGS, + UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE)) + return false; + + if (GetTypeId() == TYPEID_PLAYER && ToPlayer()->IsGameMaster()) + return false; + + return !HasUnitState(UNIT_STATE_UNATTACKABLE) && (!checkFakeDeath || !HasUnitState(UNIT_STATE_DIED)); +} + +int32 Unit::ModifyHealth(int32 dVal) +{ + int32 gain = 0; + + if (dVal == 0) + return 0; + + int32 curHealth = (int32)GetHealth(); + + int32 val = dVal + curHealth; + if (val <= 0) + { + SetHealth(0); + return -curHealth; + } + + int32 maxHealth = (int32)GetMaxHealth(); + + if (val < maxHealth) + { + SetHealth(val); + gain = val - curHealth; + } + else if (curHealth != maxHealth) + { + SetHealth(maxHealth); + gain = maxHealth - curHealth; + } + + return gain; +} + +int32 Unit::GetHealthGain(int32 dVal) +{ + int32 gain = 0; + + if (dVal == 0) + return 0; + + int32 curHealth = (int32)GetHealth(); + + int32 val = dVal + curHealth; + if (val <= 0) + { + return -curHealth; + } + + int32 maxHealth = (int32)GetMaxHealth(); + + if (val < maxHealth) + gain = dVal; + else if (curHealth != maxHealth) + gain = maxHealth - curHealth; + + return gain; +} + +// returns negative amount on power reduction +int32 Unit::ModifyPower(PowerType power, int32 dVal, bool withPowerUpdate /*= true*/) +{ + int32 gain = 0; + + if (dVal == 0) + return 0; + + int32 curPower = GetPower(power); + + int32 val = dVal + curPower; + if (val <= GetMinPower(power)) + { + SetPower(power, GetMinPower(power), withPowerUpdate); + return -curPower; + } + + int32 maxPower = GetMaxPower(power); + + if (val < maxPower) + { + SetPower(power, val, withPowerUpdate); + gain = val - curPower; + } + else if (curPower != maxPower) + { + SetPower(power, maxPower, withPowerUpdate); + gain = maxPower - curPower; + } + + return gain; +} + +// Based on client function +float Unit::GetPowerRegen(PowerType powerType, bool isInCombat) const +{ + uint32 powerSlot = MAX_POWERS; + float totalRegeneration = 0.f; + double result = DBCManager::GetBasePowerRegen(powerType, isInCombat, _powerBarId); + float combatRegeneration = result; + + powerSlot = GetPowerIndex(powerType); + if (powerSlot != MAX_POWERS) + { + uint32 powerRegenIndex = isInCombat ? UNIT_FIELD_POWER_REGEN_INTERRUPTED_FLAT_MODIFIER : UNIT_FIELD_POWER_REGEN_FLAT_MODIFIER; + totalRegeneration = GetFloatValue(powerRegenIndex + powerSlot) + combatRegeneration; + result = totalRegeneration; + } + else + totalRegeneration = result; + + if (IsPlayer() && DBCManager::IsPowerTypeAffectedByHaste(powerType)) + { + float hasteRegen = GetFloatValue(PLAYER_FIELD_MOD_HASTE_REGEN); + if (G3D::fuzzyNe(hasteRegen, 0.f)) + result = totalRegeneration / hasteRegen; + } + + return result; +} + +bool Unit::IsAlwaysVisibleFor(WorldObject const* seer) const +{ + if (WorldObject::IsAlwaysVisibleFor(seer)) + return true; + + // Always seen by owner + if (ObjectGuid guid = GetCharmerOrOwnerGUID()) + if (seer->GetGUID() == guid) + return true; + + if (Player const* seerPlayer = seer->ToPlayer()) + if (Unit* owner = GetOwner()) + if (Player* ownerPlayer = owner->ToPlayer()) + if (ownerPlayer->IsGroupVisibleFor(seerPlayer)) + return true; + + return false; +} + +bool Unit::IsAlwaysDetectableFor(WorldObject const* seer) const +{ + if (WorldObject::IsAlwaysDetectableFor(seer)) + return true; + + if (HasAuraTypeWithCaster(SPELL_AURA_MOD_STALKED, seer->GetGUID())) + return true; + + return false; +} + +bool Unit::IsVisible() const +{ + return (m_serverSideVisibility.GetValue(SERVERSIDE_VISIBILITY_GM) > SEC_PLAYER) ? false : true; +} + +void Unit::SetVisible(bool x) +{ + if (!x) + m_serverSideVisibility.SetValue(SERVERSIDE_VISIBILITY_GM, SEC_GAMEMASTER); + else + m_serverSideVisibility.SetValue(SERVERSIDE_VISIBILITY_GM, SEC_PLAYER); + + UpdateObjectVisibility(); +} + +void Unit::UpdateSpeed(UnitMoveType mtype) +{ + int32 main_speed_mod = 0; + float stack_bonus = 1.0f; + float non_stack_bonus = 1.0f; + + switch (mtype) + { + // Only apply debuffs + case MOVE_FLIGHT_BACK: + case MOVE_RUN_BACK: + case MOVE_SWIM_BACK: + break; + case MOVE_WALK: + return; + case MOVE_RUN: + { + if (IsMounted()) // Use on mount auras + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED); + stack_bonus = GetTotalAuraMultiplier(SPELL_AURA_MOD_MOUNTED_SPEED_ALWAYS); + non_stack_bonus += GetMaxPositiveAuraModifier(SPELL_AURA_MOD_MOUNTED_SPEED_NOT_STACK) / 100.0f; + } + else + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_SPEED); + stack_bonus = GetTotalAuraMultiplier(SPELL_AURA_MOD_SPEED_ALWAYS); + non_stack_bonus += GetMaxPositiveAuraModifier(SPELL_AURA_MOD_SPEED_NOT_STACK) / 100.0f; + } + break; + } + case MOVE_SWIM: + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_SWIM_SPEED); + break; + } + case MOVE_FLIGHT: + { + if (GetTypeId() == TYPEID_UNIT && IsControlledByPlayer()) // not sure if good for pet + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_VEHICLE_FLIGHT_SPEED); + stack_bonus = GetTotalAuraMultiplier(SPELL_AURA_MOD_VEHICLE_SPEED_ALWAYS); + + // for some spells this mod is applied on vehicle owner + int32 owner_speed_mod = 0; + + if (Unit* owner = GetCharmer()) + owner_speed_mod = owner->GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_VEHICLE_FLIGHT_SPEED); + + main_speed_mod = std::max(main_speed_mod, owner_speed_mod); + } + else if (IsMounted()) + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED); + stack_bonus = GetTotalAuraMultiplier(SPELL_AURA_MOD_MOUNTED_FLIGHT_SPEED_ALWAYS); + } + else // Use not mount (shapeshift for example) auras (should stack) + main_speed_mod = GetTotalAuraModifier(SPELL_AURA_MOD_INCREASE_FLIGHT_SPEED) + GetTotalAuraModifier(SPELL_AURA_MOD_INCREASE_VEHICLE_FLIGHT_SPEED); + + non_stack_bonus += GetMaxPositiveAuraModifier(SPELL_AURA_MOD_FLIGHT_SPEED_NOT_STACK) / 100.0f; + + // Update speed for vehicle if available + if (GetTypeId() == TYPEID_PLAYER && GetVehicle()) + GetVehicleBase()->UpdateSpeed(MOVE_FLIGHT); + break; + } + default: + TC_LOG_ERROR("entities.unit", "Unit::UpdateSpeed: Unsupported move type (%d)", mtype); + return; + } + + // now we ready for speed calculation + float speed = std::max(non_stack_bonus, stack_bonus); + if (main_speed_mod) + AddPct(speed, main_speed_mod); + + switch (mtype) + { + case MOVE_RUN: + case MOVE_SWIM: + case MOVE_FLIGHT: + { + // Set creature speed rate + if (IsCreature()) + { + CreatureMovementInfo const& movementInfo = ToCreature()->GetCreatureMovementInfo(); + speed *= movementInfo.HasRunSpeedOverriden ? (movementInfo.RunSpeed / baseMoveSpeed[MOVE_RUN]) : ToCreature()->GetCreatureTemplate()->speed_run; // at this point, MOVE_WALK is never reached + } + + // Normalize speed by 191 aura SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED if need + /// @todo possible affect only on MOVE_RUN + if (int32 normalization = GetMaxPositiveAuraModifier(SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED)) + { + if (Creature* creature = ToCreature()) + { + uint32 immuneMask = creature->GetCreatureTemplate()->MechanicImmuneMask; + if (immuneMask & (1 << (MECHANIC_SNARE - 1)) || immuneMask & (1 << (MECHANIC_DAZE - 1))) + break; + } + + // Use speed from aura + float max_speed = normalization / (IsControlledByPlayer() ? playerBaseMoveSpeed[mtype] : baseMoveSpeed[mtype]); + if (speed > max_speed) + speed = max_speed; + } + break; + } + default: + break; + } + + // Apply strongest slow aura mod to speed + int32 slow = GetMaxNegativeAuraModifier(SPELL_AURA_MOD_DECREASE_SPEED); + if (slow) + AddPct(speed, slow); + + if (float minSpeedMod = (float)GetMaxPositiveAuraModifier(SPELL_AURA_MOD_MINIMUM_SPEED)) + { + float baseMinSpeed = 1.0f; + if (!GetOwnerOrCreatorGUID().IsPlayer() && !IsHunterPet() && IsCreature()) + { + CreatureMovementInfo const& movementInfo = ToCreature()->GetCreatureMovementInfo(); + baseMinSpeed = movementInfo.HasRunSpeedOverriden ? (movementInfo.RunSpeed / baseMoveSpeed[MOVE_RUN]) : ToCreature()->GetCreatureTemplate()->speed_run; + } + + float min_speed = CalculatePct(baseMinSpeed, minSpeedMod); + if (speed < min_speed) + speed = min_speed; + } + + SetSpeedRate(mtype, speed); +} + +float Unit::GetSpeed(UnitMoveType mtype) const +{ + return m_speed_rate[mtype]*(IsControlledByPlayer() ? playerBaseMoveSpeed[mtype] : baseMoveSpeed[mtype]); +} + +void Unit::SetSpeed(UnitMoveType mtype, float newValue) +{ + SetSpeedRate(mtype, newValue / (IsControlledByPlayer() ? playerBaseMoveSpeed[mtype] : baseMoveSpeed[mtype])); +} + +void Unit::SetSpeedRate(UnitMoveType mtype, float rate) +{ + if (rate < 0) + rate = 0.0f; + + // Update speed only on change + if (m_speed_rate[mtype] == rate) + return; + + + float newSpeedFlat = rate * (IsControlledByPlayer() ? playerBaseMoveSpeed[mtype] : baseMoveSpeed[mtype]); + if (IsMovedByClient() && IsInWorld()) + { + MovementPacketSender::SendSpeedChangeToMover(this, mtype, newSpeedFlat); + SetSpeedRateReal(mtype, rate); + } + else if (IsMovedByClient() && !IsInWorld()) // (1) + SetSpeedRateReal(mtype, rate); + else // <=> if(!IsMovedByPlayer()) + { + SetSpeedRateReal(mtype, rate); + MovementPacketSender::SendSpeedChangeToAll(this, mtype, newSpeedFlat); + } + + // explaination of (1): + // If the player is not in the world yet, it won't reply to the packets requiring an ack. And once the player is in the world, next time a movement + // packet which requires an ack is sent to the client (change of speed for example), the client is kicked from the + // server on the ground that it should have replied to the first packet first. That line is a hacky fix + // in the sense that it doesn't work like that in retail since buffs are applied only after the player has been + // initialized in the world. cf description of PR #18771 +} + +void Unit::SetSpeedRateReal(UnitMoveType mtype, float rate) +{ + if (!IsInCombat() && ToPlayer()) + if (Pet* pet = ToPlayer()->GetPet()) + pet->SetSpeedRate(mtype, rate); + + m_speed_rate[mtype] = rate; + PropagateSpeedChange(); +} + +void Unit::FollowTarget(Unit* target) +{ + if (!target) + { + TC_LOG_ERROR("entities.unit", "Unit::FollowTarget: Unit (%s) tried to follow a non-existant target.", GetGUID().ToString().c_str()); + return; + } + + // Determine follow configuration + bool joinFormation = false; // unit will follow its target in a generated formation shape and catches up to its target + bool catchUpToTarget = false; // unit will allign to the target speed and catches up to the target automatically + bool faceTarget = false; // unit will face its target with every spline + float distance = DEFAULT_FOLLOW_DISTANCE_PET; + + if (TempSummon* summon = ToTempSummon()) + { + if (SummonPropertiesEntry const* properties = summon->m_Properties) + { + // Allied summons, pet summons join a formation unless the following exceptions are being met. + if (properties->Control == SUMMON_CATEGORY_ALLY || properties->Control == SUMMON_CATEGORY_PET) + joinFormation = true; + + // Companion minipets will always be able to catch up to their target + if (properties->Slot == SUMMON_SLOT_MINIPET) + { + joinFormation = false; + catchUpToTarget = true; + faceTarget = true; + distance = DEFAULT_FOLLOW_DISTANCE; + } + + // Quest npcs follow their target outside of formations + if (properties->Slot == SUMMON_SLOT_QUEST) + { + joinFormation = false; + distance = DEFAULT_FOLLOW_DISTANCE; + } + } + + // Pets and minions alwys move in a formation of their target + if (summon->IsPet()) + joinFormation = true; + } + else if (IsCharmed()) + joinFormation = true; + + // Unit is already following its target + if (joinFormation && target->HasFormationFollower(this)) + return; + + GetMotionMaster()->MoveFollow(target, distance, DEFAULT_FOLLOW_ANGLE, joinFormation, catchUpToTarget, faceTarget); +} + +void Unit::RemoveFormationFollower(Unit* follower) +{ + for (FormationFollowerGUIDContainer::const_iterator itr = _formationFollowers.begin(); itr != _formationFollowers.end();) + { + ObjectGuid guid = *itr; + if (Unit* followingUnit = ObjectAccessor::GetUnit(*this, guid)) + { + if (followingUnit == follower) + itr = _formationFollowers.erase(itr); + else + ++itr; + } + else + itr = _formationFollowers.erase(itr); + } +} + +bool Unit::HasFormationFollower(Unit* follower) const +{ + for (ObjectGuid guid : _formationFollowers) + if (Unit* followingUnit = ObjectAccessor::GetUnit(*this, guid)) + if (followingUnit == follower) + return true; + + return false; +} + +void Unit::RemoveAllPursuers() +{ + for (uint8 i = 0; i < AsUnderlyingType(PursuingType::Max); ++i) + { + while (!_unitsPursuingMe[i].empty()) + (*_unitsPursuingMe[i].begin())->SetTarget(PursuingType(i), nullptr); + } +} + +void Unit::setDeathState(DeathState s) +{ + // Death state needs to be updated before RemoveAllAurasOnDeath() is called, to prevent entering combat + m_deathState = s; + + if (s != ALIVE && s != JUST_RESPAWNED) + { + CombatStop(); + ClearComboPointHolders(); // any combo points pointed to unit lost at it death + + if (IsNonMeleeSpellCast(false)) + InterruptNonMeleeSpells(false); + + ExitVehicle(); // Exit vehicle before calling RemoveAllControlled + // vehicles use special type of charm that is not removed by the next function + // triggering an assert + UnsummonAllTotems(); + RemoveAllControlled(); + RemoveAllAurasOnDeath(); + } + + if (s == JUST_DIED) + { + // remove aurastates allowing special moves + ClearAllReactives(); + ClearDiminishings(); + CancelPendingCastRequest(); + if (IsInWorld()) + { + // Only clear MotionMaster for entities that exists in world + // Avoids crashes in the following conditions : + // * Using 'call pet' on dead pets + // * Using 'call stabled pet' + // * Logging in with dead pets + GetMotionMaster()->Clear(false); + GetMotionMaster()->MoveIdle(); + } + StopMoving(); + DisableSpline(); + // without this when removing IncreaseMaxHealth aura player may stuck with 1 hp + // do not why since in IncreaseMaxHealth currenthealth is checked + SetHealth(0); + SetPower(GetPowerType(), 0); + SetUInt32Value(UNIT_NPC_EMOTESTATE, 0); + + // players in instance don't have ZoneScript, but they have InstanceScript + if (ZoneScript* zoneScript = GetZoneScript() ? GetZoneScript() : GetInstanceScript()) + zoneScript->OnUnitDeath(this); + } + else if (s == JUST_RESPAWNED) + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_SKINNABLE); // clear skinnable for creature and player (at battleground) +} + +//====================================================================== + +void Unit::AtEnterCombat() +{ + if (Spell* spell = m_currentSpells[CURRENT_GENERIC_SPELL]) + if (spell->getState() == SPELL_STATE_PREPARING + && spell->m_spellInfo->HasAttribute(SPELL_ATTR0_NOT_IN_COMBAT_ONLY_PEACEFUL) + && spell->m_spellInfo->InterruptFlags.HasFlag(SpellInterruptFlags::Combat) + && !spell->m_spellInfo->HasAura(SPELL_AURA_MOUNTED)) // for some reason mounts are not interrupted by entering combat even with both interrupt flag and attribute + InterruptSpell(CURRENT_GENERIC_SPELL, false, false); + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::EnteringCombat); + ProcSkillsAndAuras(this, nullptr, PROC_FLAG_ENTER_COMBAT, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); +} + +void Unit::AtExitCombat() +{ + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::LeavingCombat); +} + +void Unit::AtTargetAttacked(Unit* target, bool canInitialAggro) +{ + if (!target->IsEngaged() && !canInitialAggro) + return; + target->EngageWithTarget(this); + if (Unit* targetOwner = target->GetCharmerOrOwner()) + targetOwner->EngageWithTarget(this); + + Player* myPlayerOwner = GetCharmerOrOwnerPlayerOrPlayerItself(); + Player* targetPlayerOwner = target->GetCharmerOrOwnerPlayerOrPlayerItself(); + if (myPlayerOwner && targetPlayerOwner && !(myPlayerOwner->duel && myPlayerOwner->duel->opponent == targetPlayerOwner)) + { + myPlayerOwner->UpdatePvP(true); + myPlayerOwner->SetContestedPvP(targetPlayerOwner); + myPlayerOwner->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::PvPActive); + } +} + +void Unit::UpdatePetCombatState() +{ + ASSERT(!IsPet()); // player pets do not use UNIT_FLAG_PET_IN_COMBAT for this purpose - but player pets should also never have minions of their own to call this + + // UNIT_FLAG_PET_IN_COMBAT is set by default when the summoner is in combat by himself + if (IsInCombat()) + return; + + bool state = false; + for (Unit* minion : m_Controlled) + if (minion->IsInCombat()) + { + state = true; + break; + } + + if (state) + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT); + else + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT); +} + +//====================================================================== + +DiminishingLevels Unit::GetDiminishing(DiminishingGroup group) const +{ + DiminishingReturn const& diminish = m_Diminishing[group]; + if (!diminish.hitCount) + return DIMINISHING_LEVEL_1; + + // If last spell was cast more than 15 seconds ago - reset level + if (!diminish.stack && GetMSTimeDiffToNow(diminish.hitTime) > 15000) + return DIMINISHING_LEVEL_1; + + return DiminishingLevels(diminish.hitCount); +} + +void Unit::IncrDiminishing(SpellInfo const* auraSpellInfo, bool triggered) +{ + DiminishingGroup group = auraSpellInfo->GetDiminishingReturnsGroupForSpell(triggered); + uint32 currentLevel = GetDiminishing(group); + uint32 const maxLevel = auraSpellInfo->GetDiminishingReturnsMaxLevel(triggered); + + DiminishingReturn& diminish = m_Diminishing[group]; + if (currentLevel < maxLevel) + diminish.hitCount = currentLevel + 1; +} + +bool Unit::ApplyDiminishingToDuration(SpellInfo const* auraSpellInfo, bool triggered, int32& duration, WorldObject* caster, DiminishingLevels previousLevel) const +{ + DiminishingGroup const group = auraSpellInfo->GetDiminishingReturnsGroupForSpell(triggered); + if (duration == -1 || group == DIMINISHING_NONE) + return true; + + int32 const limitDuration = auraSpellInfo->GetDiminishingReturnsLimitDuration(triggered); + + // test pet/charm masters instead pets/charmeds + Unit const* targetOwner = GetCharmerOrOwner(); + Unit const* casterOwner = caster->GetCharmerOrOwner(); + + // Duration of crowd control abilities on pvp target is limited by 10 sec. (2.2.0) + if (limitDuration > 0 && duration > limitDuration) + { + Unit const* target = targetOwner ? targetOwner : this; + WorldObject const* source = casterOwner ? casterOwner : caster; + + if (target->IsAffectedByDiminishingReturns() && source->GetTypeId() == TYPEID_PLAYER) + duration = limitDuration; + } + + float mod = 1.0f; + if (group == DIMINISHING_TAUNT) + { + if (GetTypeId() == TYPEID_UNIT && (ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_TAUNT_DIMINISH)) + { + DiminishingLevels diminish = previousLevel; + switch (diminish) + { + case DIMINISHING_LEVEL_1: break; + case DIMINISHING_LEVEL_2: mod = 0.65f; break; + case DIMINISHING_LEVEL_3: mod = 0.4225f; break; + case DIMINISHING_LEVEL_4: mod = 0.274625f; break; + case DIMINISHING_LEVEL_TAUNT_IMMUNE: mod = 0.0f; break; + default: break; + } + } + } + // Some diminishings applies to mobs too (for example, Stun) + else if (auraSpellInfo->GetDiminishingReturnsGroupType(triggered) == DRTYPE_ALL || + (auraSpellInfo->GetDiminishingReturnsGroupType(triggered) == DRTYPE_PLAYER && + (targetOwner ? targetOwner->IsAffectedByDiminishingReturns() : IsAffectedByDiminishingReturns()))) + { + DiminishingLevels diminish = previousLevel; + switch (diminish) + { + case DIMINISHING_LEVEL_1: break; + case DIMINISHING_LEVEL_2: mod = 0.5f; break; + case DIMINISHING_LEVEL_3: mod = 0.25f; break; + case DIMINISHING_LEVEL_IMMUNE: mod = 0.0f; break; + default: break; + } + } + + duration = int32(duration * mod); + return (duration != 0); +} + +void Unit::ApplyDiminishingAura(DiminishingGroup group, bool apply) +{ + // Checking for existing in the table + DiminishingReturn& diminish = m_Diminishing[group]; + + if (apply) + ++diminish.stack; + else if (diminish.stack) + { + --diminish.stack; + + // Remember time after last aura from group removed + if (!diminish.stack) + diminish.hitTime = getMSTime(); + } +} + +void Unit::ClearDiminishings() +{ + for (DiminishingReturn& dim : m_Diminishing) + dim.Clear(); +} + +uint32 Unit::GetCreatureType() const +{ + if (GetTypeId() == TYPEID_PLAYER) + { + ShapeshiftForm form = GetShapeshiftForm(); + SpellShapeshiftFormEntry const* ssEntry = sSpellShapeshiftFormStore.LookupEntry(form); + if (ssEntry && ssEntry->CreatureType > 0) + return ssEntry->CreatureType; + else + return CREATURE_TYPE_HUMANOID; + } + else + return ToCreature()->GetCreatureTemplate()->type; +} + +uint32 Unit::GetCreatureTypeMask() const +{ + uint32 creatureType = GetCreatureType(); + return (creatureType >= 1) ? (1 << (creatureType - 1)) : 0; +} + +void Unit::SetShapeshiftForm(ShapeshiftForm form) +{ + SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_SHAPESHIFT_FORM, form); +} + +bool Unit::IsInFeralForm() const +{ + ShapeshiftForm form = GetShapeshiftForm(); + return form == FORM_CAT || form == FORM_BEAR; +} + +bool Unit::IsInDisallowedMountForm() const +{ + if (ShapeshiftForm form = GetShapeshiftForm()) + { + SpellShapeshiftFormEntry const* shapeshift = sSpellShapeshiftFormStore.LookupEntry(form); + if (!shapeshift) + return true; + + if (!shapeshift->GetFlags().HasFlag(SpellShapeshiftFormFlags::Stance)) + return true; + } + + if (GetDisplayId() == GetNativeDisplayId()) + return false; + + CreatureDisplayInfoEntry const* display = sCreatureDisplayInfoStore.LookupEntry(GetDisplayId()); + if (!display) + return true; + + CreatureDisplayInfoExtraEntry const* displayExtra = sCreatureDisplayInfoExtraStore.LookupEntry(display->ExtendedDisplayInfoID); + if (!displayExtra) + return true; + + CreatureModelDataEntry const* model = sCreatureModelDataStore.LookupEntry(display->ModelID); + ChrRacesEntry const* race = sChrRacesStore.LookupEntry(displayExtra->DisplayRaceID); + + if (model && !(model->Flags & 0x80)) + if (race && !(race->Flags & 0x4)) + return true; + + return false; +} + +/*####################################### +######## ######## +######## STAT SYSTEM ######## +######## ######## +#######################################*/ + +void Unit::HandleStatFlatModifier(UnitMods unitMod, UnitModifierFlatType modifierType, float amount, bool apply) +{ + if (unitMod >= UNIT_MOD_END || modifierType >= MODIFIER_TYPE_FLAT_END) + { + TC_LOG_ERROR("entities.unit", "ERROR in HandleStatFlatModifier(): non-existing UnitMods or wrong UnitModifierType!"); + return; + } + + if (!amount) + return; + + switch (modifierType) + { + case BASE_VALUE: + case TOTAL_VALUE: + m_auraFlatModifiersGroup[unitMod][modifierType] += apply ? amount : -amount; + break; + default: + break; + } + + UpdateUnitMod(unitMod); +} + +void Unit::ApplyStatPctModifier(UnitMods unitMod, UnitModifierPctType modifierType, float pct) +{ + if (unitMod >= UNIT_MOD_END || modifierType >= MODIFIER_TYPE_PCT_END) + { + TC_LOG_ERROR("entities.unit", "ERROR in ApplyStatPctModifier(): non-existing UnitMods or wrong UnitModifierType!"); + return; + } + + if (!pct) + return; + + switch (modifierType) + { + case BASE_PCT: + case TOTAL_PCT: + AddPct(m_auraPctModifiersGroup[unitMod][modifierType], pct); + break; + default: + break; + } + + UpdateUnitMod(unitMod); +} + +void Unit::SetStatFlatModifier(UnitMods unitMod, UnitModifierFlatType modifierType, float val) +{ + if (m_auraFlatModifiersGroup[unitMod][modifierType] == val) + return; + + m_auraFlatModifiersGroup[unitMod][modifierType] = val; + UpdateUnitMod(unitMod); +} + +void Unit::SetStatPctModifier(UnitMods unitMod, UnitModifierPctType modifierType, float val) +{ + if (m_auraPctModifiersGroup[unitMod][modifierType] == val) + return; + + m_auraPctModifiersGroup[unitMod][modifierType] = val; + UpdateUnitMod(unitMod); +} + +float Unit::GetFlatModifierValue(UnitMods unitMod, UnitModifierFlatType modifierType) const +{ + if (unitMod >= UNIT_MOD_END || modifierType >= MODIFIER_TYPE_FLAT_END) + { + TC_LOG_ERROR("entities.unit", "attempt to access non-existing modifier value from UnitMods!"); + return 0.0f; + } + + return m_auraFlatModifiersGroup[unitMod][modifierType]; +} + +float Unit::GetPctModifierValue(UnitMods unitMod, UnitModifierPctType modifierType) const +{ + if (unitMod >= UNIT_MOD_END || modifierType >= MODIFIER_TYPE_PCT_END) + { + TC_LOG_ERROR("entities.unit", "attempt to access non-existing modifier value from UnitMods!"); + return 0.0f; + } + + return m_auraPctModifiersGroup[unitMod][modifierType]; +} + +void Unit::UpdateUnitMod(UnitMods unitMod) +{ + if (!CanModifyStats()) + return; + + switch (unitMod) + { + case UNIT_MOD_ARMOR: + UpdateArmor(); + break; + case UNIT_MOD_HEALTH: + UpdateMaxHealth(); + break; + case UNIT_MOD_MANA: + case UNIT_MOD_RAGE: + case UNIT_MOD_FOCUS: + case UNIT_MOD_ENERGY: + case UNIT_MOD_RUNE: + case UNIT_MOD_RUNIC_POWER: + UpdateMaxPower(GetPowerTypeByAuraGroup(unitMod)); + break; + case UNIT_MOD_RESISTANCE_HOLY: + case UNIT_MOD_RESISTANCE_FIRE: + case UNIT_MOD_RESISTANCE_NATURE: + case UNIT_MOD_RESISTANCE_FROST: + case UNIT_MOD_RESISTANCE_SHADOW: + case UNIT_MOD_RESISTANCE_ARCANE: + UpdateResistances(GetSpellSchoolByAuraGroup(unitMod)); + break; + case UNIT_MOD_ATTACK_POWER: + UpdateAttackPowerAndDamage(); + break; + case UNIT_MOD_ATTACK_POWER_RANGED: + UpdateAttackPowerAndDamage(true); + break; + case UNIT_MOD_DAMAGE_MAINHAND: + UpdateDamagePhysical(BASE_ATTACK); + break; + case UNIT_MOD_DAMAGE_OFFHAND: + UpdateDamagePhysical(OFF_ATTACK); + break; + case UNIT_MOD_DAMAGE_RANGED: + UpdateDamagePhysical(RANGED_ATTACK); + break; + default: + break; + } +} + +void Unit::UpdateDamageDoneMods(WeaponAttackType attackType, int32 /*skipEnchantSlot = -1*/) +{ + UnitMods unitMod; + switch (attackType) + { + case BASE_ATTACK: + unitMod = UNIT_MOD_DAMAGE_MAINHAND; + break; + case OFF_ATTACK: + unitMod = UNIT_MOD_DAMAGE_OFFHAND; + break; + case RANGED_ATTACK: + unitMod = UNIT_MOD_DAMAGE_RANGED; + break; + default: + ABORT(); + break; + } + + float amount = GetTotalAuraModifier(SPELL_AURA_MOD_DAMAGE_DONE, [&](AuraEffect const* aurEff) -> bool + { + if (!(aurEff->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL)) + return false; + + return CheckAttackFitToAuraRequirement(attackType, aurEff); + }); + + SetStatFlatModifier(unitMod, TOTAL_VALUE, amount); +} + +void Unit::UpdateAllDamageDoneMods() +{ + for (uint8 i = BASE_ATTACK; i < MAX_ATTACK; ++i) + UpdateDamageDoneMods(WeaponAttackType(i)); +} + +void Unit::UpdateDamagePctDoneMods(WeaponAttackType attackType) +{ + float factor; + UnitMods unitMod; + switch (attackType) + { + case BASE_ATTACK: + factor = 1.0f; + unitMod = UNIT_MOD_DAMAGE_MAINHAND; + break; + case OFF_ATTACK: + // off hand has 50% penalty + factor = 0.5f; + unitMod = UNIT_MOD_DAMAGE_OFFHAND; + break; + case RANGED_ATTACK: + factor = 1.0f; + unitMod = UNIT_MOD_DAMAGE_RANGED; + break; + default: + ABORT(); + break; + } + + factor *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_PERCENT_DONE, [attackType, this](AuraEffect const* aurEff) -> bool + { + if (!(aurEff->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL)) + return false; + + return CheckAttackFitToAuraRequirement(attackType, aurEff); + }); + + if (attackType == OFF_ATTACK) + factor *= GetTotalAuraMultiplier(SPELL_AURA_MOD_OFFHAND_DAMAGE_PCT, std::bind(&Unit::CheckAttackFitToAuraRequirement, this, attackType, std::placeholders::_1)); + + SetStatPctModifier(unitMod, TOTAL_PCT, factor); +} + +void Unit::UpdateAllDamagePctDoneMods() +{ + for (uint8 i = BASE_ATTACK; i < MAX_ATTACK; ++i) + UpdateDamagePctDoneMods(WeaponAttackType(i)); +} + +float Unit::GetTotalAuraModValue(UnitMods unitMod) const +{ + if (unitMod >= UNIT_MOD_END) + { + TC_LOG_ERROR("entities.unit", "attempt to access non-existing UnitMods in GetTotalAuraModValue()!"); + return 0.0f; + } + + float value = GetFlatModifierValue(unitMod, BASE_VALUE); + value *= GetPctModifierValue(unitMod, BASE_PCT); + value += GetFlatModifierValue(unitMod, TOTAL_VALUE); + value *= GetPctModifierValue(unitMod, TOTAL_PCT); + + return value; +} + +SpellSchools Unit::GetSpellSchoolByAuraGroup(UnitMods unitMod) const +{ + SpellSchools school = SPELL_SCHOOL_NORMAL; + + switch (unitMod) + { + case UNIT_MOD_RESISTANCE_HOLY: school = SPELL_SCHOOL_HOLY; break; + case UNIT_MOD_RESISTANCE_FIRE: school = SPELL_SCHOOL_FIRE; break; + case UNIT_MOD_RESISTANCE_NATURE: school = SPELL_SCHOOL_NATURE; break; + case UNIT_MOD_RESISTANCE_FROST: school = SPELL_SCHOOL_FROST; break; + case UNIT_MOD_RESISTANCE_SHADOW: school = SPELL_SCHOOL_SHADOW; break; + case UNIT_MOD_RESISTANCE_ARCANE: school = SPELL_SCHOOL_ARCANE; break; + + default: + break; + } + + return school; +} + +PowerType Unit::GetPowerTypeByAuraGroup(UnitMods unitMod) const +{ + switch (unitMod) + { + case UNIT_MOD_RAGE: return POWER_RAGE; + case UNIT_MOD_FOCUS: return POWER_FOCUS; + case UNIT_MOD_ENERGY: return POWER_ENERGY; + case UNIT_MOD_RUNE: return POWER_RUNE; + case UNIT_MOD_RUNIC_POWER: return POWER_RUNIC_POWER; + default: + case UNIT_MOD_MANA: return POWER_MANA; + } +} + +float Unit::GetTotalAttackPowerValue(WeaponAttackType attType) const +{ + if (attType == RANGED_ATTACK) + { + int32 ap = GetInt32Value(UNIT_FIELD_RANGED_ATTACK_POWER) + + GetInt32Value(UNIT_FIELD_RANGED_ATTACK_POWER_MOD_POS) + - GetInt32Value(UNIT_FIELD_RANGED_ATTACK_POWER_MOD_NEG); + + if (ap < 0) + return 0.0f; + return ap * (1.0f + GetFloatValue(UNIT_FIELD_RANGED_ATTACK_POWER_MULTIPLIER)); + } + else + { + int32 ap = GetInt32Value(UNIT_FIELD_ATTACK_POWER) + + GetInt32Value(UNIT_FIELD_ATTACK_POWER_MOD_POS) + - GetInt32Value(UNIT_FIELD_ATTACK_POWER_MOD_NEG); + + if (ap < 0) + return 0.0f; + return ap * (1.0f + GetFloatValue(UNIT_FIELD_ATTACK_POWER_MULTIPLIER)); + } +} + +float Unit::GetWeaponDamageRange(WeaponAttackType attType, WeaponDamageRange type) const +{ + if (attType == OFF_ATTACK && !haveOffhandWeapon()) + return 0.0f; + + return m_weaponDamage[attType][type]; +} + +bool Unit::CanFreeMove() const +{ + return !HasUnitState(UNIT_STATE_CONFUSED | UNIT_STATE_FLEEING | UNIT_STATE_IN_FLIGHT | + UNIT_STATE_ROOT | UNIT_STATE_STUNNED | UNIT_STATE_DISTRACTED) && GetOwnerOrCreatorGUID().IsEmpty(); +} + +void Unit::SetLevel(uint8 lvl) +{ + SetUInt32Value(UNIT_FIELD_LEVEL, lvl); + + if (Player* player = ToPlayer()) + { + // group update + if (player->GetGroup()) + player->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_LEVEL); + + sCharacterCache->UpdateCharacterLevel(GetGUID(), lvl); + } +} + +void Unit::SetHealth(uint32 val) +{ + if (getDeathState() == JUST_DIED || getDeathState() == CORPSE) + val = 0; + else if (GetTypeId() == TYPEID_PLAYER && getDeathState() == DEAD) + val = 1; + else + { + uint32 maxHealth = GetMaxHealth(); + if (maxHealth < val) + val = maxHealth; + } + + SetUInt32Value(UNIT_FIELD_HEALTH, val); + + // group update + if (Player* player = ToPlayer()) + { + if (player->GetGroup()) + player->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_CUR_HP); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_CUR_HP); + } + } +} + +void Unit::SetMaxHealth(uint32 val) +{ + if (!val) + val = 1; + + uint32 health = GetHealth(); + SetUInt32Value(UNIT_FIELD_MAXHEALTH, val); + + // group update + if (GetTypeId() == TYPEID_PLAYER) + { + if (ToPlayer()->GetGroup()) + ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_MAX_HP); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_MAX_HP); + } + } + + if (val < health) + SetHealth(val); +} + +int32 Unit::GetPower(PowerType power) const +{ + uint32 powerIndex = GetPowerIndex(power); + if (powerIndex == MAX_POWERS) + return 0; + + return GetUInt32Value(UNIT_FIELD_POWER1 + powerIndex); +} + +int32 Unit::GetMaxPower(PowerType power) const +{ + uint32 powerIndex = GetPowerIndex(power); + if (powerIndex == MAX_POWERS) + return 0; + + return GetInt32Value(UNIT_FIELD_MAXPOWER1 + powerIndex); +} + +void Unit::SetPower(PowerType power, int32 val, bool withPowerUpdate /*= true*/) +{ + uint32 powerIndex = GetPowerIndex(power); + if (powerIndex == MAX_POWERS) + return; + + int32 maxPower = int32(GetMaxPower(power)); + if (maxPower < val) + val = maxPower; + + SetInt32Value(UNIT_FIELD_POWER1 + powerIndex, val); + + if (IsInWorld() && withPowerUpdate) + { + WorldPackets::Combat::PowerUpdate packet; + packet.Guid = GetGUID(); + packet.Powers.emplace_back(val, power); + SendMessageToSet(packet.Write(), IsPlayer()); + } + + // group update + if (Player* player = ToPlayer()) + { + if (player->GetGroup()) + player->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_CUR_POWER); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_CUR_POWER); + } + } +} + +void Unit::SetMaxPower(PowerType power, int32 val) +{ + uint32 powerIndex = GetPowerIndex(power); + if (powerIndex == MAX_POWERS) + return; + + int32 cur_power = GetPower(power); + SetInt32Value(UNIT_FIELD_MAXPOWER1 + powerIndex, val); + + // group update + if (GetTypeId() == TYPEID_PLAYER) + { + if (ToPlayer()->GetGroup()) + ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_MAX_POWER); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_MAX_POWER); + } + } + + if (val < cur_power) + SetPower(power, val); +} + +void Unit::Regenerate(PowerType powerType, uint32 diff) +{ + uint32 maxValue = GetMaxPower(powerType); + if (!maxValue) + return; + + if (!HasFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_REGENERATE_POWER)) + return; + + uint32 curValue = GetPower(powerType); + + // Block power regeneration + if (HasAuraTypeWithValue(SPELL_AURA_PREVENT_REGENERATE_POWER, powerType) || HasAuraType(SPELL_AURA_INTERRUPT_REGEN)) + return; + + // Skip regeneration for power type we cannot have + uint32 powerIndex = GetPowerIndex(powerType); + if (powerIndex == MAX_POWERS) + return; + + float addValue = GetPowerRegen(powerType, IsInCombat()) * (diff / float(IN_MILLISECONDS)); + + // Apply config values + switch (powerType) + { + case POWER_MANA: + addValue *= sWorld->getRate(RATE_POWER_MANA); + break; + case POWER_RAGE: + addValue *= sWorld->getRate(RATE_POWER_RAGE_LOSS); + break; + case POWER_RUNIC_POWER: + addValue *= sWorld->getRate(RATE_POWER_RUNICPOWER_LOSS); + break; + case POWER_FOCUS: + addValue *= sWorld->getRate(RATE_POWER_FOCUS); + break; + case POWER_ENERGY: + addValue *= sWorld->getRate(RATE_POWER_ENERGY); + break; + default: + break; + } + + if (addValue < 0.0f) + { + if (curValue == 0) + return; + } + else if (addValue > 0.0f) + { + if (curValue == maxValue) + return; + } + else + return; + + addValue += _powerFraction[powerIndex]; + uint32 integerValue = uint32(std::fabs(addValue)); + + if (addValue < 0.0f) + { + if (curValue > integerValue) + { + curValue -= integerValue; + _powerFraction[powerIndex] = addValue + integerValue; + } + else + { + curValue = 0; + _powerFraction[powerIndex] = 0; + } + } + else + { + curValue += integerValue; + + if (curValue > maxValue) + { + curValue = maxValue; + _powerFraction[powerIndex] = 0; + } + else + _powerFraction[powerIndex] = addValue - integerValue; + } + + // Players update their powers in 2 seconds intervals, creatures in 1 second ones + if (_powerUpdateTimer <= 0) + SetPower(powerType, curValue); + else + UpdateUInt32Value(UNIT_FIELD_POWER1 + powerIndex, curValue); +} + +void Unit::RegisterPowerTypes() +{ + for (uint8 i = POWER_MANA; i < MAX_POWERS; ++i) + { + uint32 powerIndex = GetPowerIndex(PowerType(i)); + if (powerIndex == MAX_POWERS || powerIndex == MAX_POWERS_PER_CLASS) + continue; + + _usedPowerTypes[powerIndex] = static_cast(i); + } +} + +void Unit::AIUpdateTick(uint32 diff) +{ + if (UnitAI* ai = GetAI()) + { + m_aiLocked = true; + ai->UpdateAI(diff); + m_aiLocked = false; + } +} + +void Unit::PushAI(UnitAI* newAI) +{ + i_AIs.emplace(newAI); +} + +void Unit::SetAI(UnitAI* newAI) +{ + PushAI(newAI); + RefreshAI(); +} + +bool Unit::PopAI() +{ + if (!i_AIs.empty()) + { + i_AIs.pop(); + return true; + } + else + return false; +} + +void Unit::RefreshAI() +{ + ASSERT(!m_aiLocked, "Tried to change current AI during UpdateAI()"); + if (i_AIs.empty()) + i_AI = nullptr; + else + i_AI = i_AIs.top(); +} + +void Unit::ScheduleAIChange() +{ + bool const charmed = IsCharmed(); + + if (charmed) + PushAI(GetScheduledChangeAI()); + else + { + RestoreDisabledAI(); + PushAI(GetScheduledChangeAI()); //This could actually be PopAI() to get the previous AI but it's required atm to trigger UpdateCharmAI() + } +} + +void Unit::RestoreDisabledAI() +{ + // Keep popping the stack until we either reach the bottom or find a valid AI + while (PopAI()) + if (GetTopAI() && dynamic_cast(GetTopAI()) == nullptr) + return; +} + +UnitAI* Unit::GetScheduledChangeAI() +{ + if (Creature* creature = ToCreature()) + return new ScheduledChangeAI(creature); + else + return nullptr; +} + +bool Unit::HasScheduledAIChange() const +{ + if (UnitAI* ai = GetAI()) + return dynamic_cast(ai) != nullptr; + else + return true; +} + +void Unit::AddToWorld() +{ + WorldObject::AddToWorld(); + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::EnterWorld); +} + +void Unit::RemoveFromWorld() +{ + // cleanup + ASSERT(GetGUID()); + + if (IsInWorld()) + { + m_duringRemoveFromWorld = true; + if (UnitAI* ai = GetAI()) + ai->LeavingWorld(); + + if (IsVehicle()) + RemoveVehicleKit(true); + + RemoveCharmAuras(); + RemoveBindSightAuras(); + RemoveNotOwnSingleTargetAuras(); + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::LeaveWorld); + + RemoveAllGameObjects(); + RemoveAllDynObjects(); + + ExitVehicle(); // Remove applied auras with SPELL_AURA_CONTROL_VEHICLE + UnsummonAllTotems(); + RemoveAllControlled(); + + RemoveAreaAurasDueToLeaveWorld(); + + // clear idle movement slot to finalize follow movement to unregister formation targets + GetMotionMaster()->Clear(MOTION_SLOT_IDLE); + RemoveAllPursuers(); + + if (IsCharmed()) + RemoveCharmedBy(nullptr); + + ASSERT(!GetCharmedGUID(), "Unit %u has charmed guid when removed from world", GetEntry()); + ASSERT(!GetCharmerGUID(), "Unit %u has charmer guid when removed from world", GetEntry()); + + if (Unit* owner = GetOwner()) + { + if (owner->m_Controlled.find(this) != owner->m_Controlled.end()) + { + TC_LOG_FATAL("entities.unit", "Unit %u is in controlled list of %u when removed from world", GetEntry(), owner->GetEntry()); + ABORT(); + } + } + + WorldObject::RemoveFromWorld(); + m_duringRemoveFromWorld = false; + } +} + +void Unit::CleanupBeforeRemoveFromMap(bool finalCleanup) +{ + // This needs to be before RemoveFromWorld to make GetCaster() return a valid pointer on aura removal + InterruptNonMeleeSpells(true); + + if (IsInWorld()) + RemoveFromWorld(); + else + { + // cleanup that must happen even if not in world + if (IsVehicle()) + RemoveVehicleKit(true); + } + + ASSERT(GetGUID()); + + // A unit may be in removelist and not in world, but it is still in grid + // and may have some references during delete + RemoveAllAuras(); + RemoveAllGameObjects(); + + if (finalCleanup) + m_cleanupDone = true; + + CombatStop(); + ClearComboPointHolders(); +} + +void Unit::CleanupsBeforeDelete(bool finalCleanup) +{ + CleanupBeforeRemoveFromMap(finalCleanup); + + WorldObject::CleanupsBeforeDelete(finalCleanup); +} + +void Unit::UpdateCharmAI() +{ + if (IsCharmed()) + { + UnitAI* newAI = nullptr; + if (IsPlayer()) + { + if (Unit* charmer = GetCharmer()) + { + // first, we check if the creature's own AI specifies an override playerai for its owned players + if (Creature* creatureCharmer = charmer->ToCreature()) + { + if (CreatureAI* charmerAI = creatureCharmer->AI()) + newAI = charmerAI->GetAIForCharmedPlayer(ToPlayer()); + } + else + TC_LOG_ERROR("misc", "Attempt to assign charm AI to player %s who is charmed by non-creature %s.", GetGUID().ToString().c_str(), GetCharmerGUID().ToString().c_str()); + } + if (!newAI) // otherwise, we default to the generic one + newAI = new SimpleCharmedPlayerAI(ToPlayer()); + } + else + { + ASSERT(IsCreature()); + if (isPossessed() || IsVehicle()) + newAI = new PossessedAI(ToCreature()); + else + newAI = new PetAI(ToCreature()); + } + + ASSERT(newAI); + SetAI(newAI); + newAI->OnCharmed(true); + } + else + { + RestoreDisabledAI(); + // Hack: this is required because we want to call OnCharmed(true) on the restored AI + RefreshAI(); + if (UnitAI* ai = GetAI()) + ai->OnCharmed(true); + } +} + +CharmInfo* Unit::InitCharmInfo() +{ + if (!m_charmInfo) + m_charmInfo = std::make_unique(this); + + return m_charmInfo.get(); +} + +void Unit::DeleteCharmInfo() +{ + if (!m_charmInfo) + return; + + m_charmInfo->RestoreState(); + m_charmInfo.reset(); +} + +bool Unit::isFrozen() const +{ + return HasAuraState(AURA_STATE_FROZEN); +} + +ProcFlagsHit createProcHitMask(SpellNonMeleeDamage* damageInfo, SpellMissInfo missCondition) +{ + ProcFlagsHit hitMask = PROC_HIT_NONE; + // Check victim state + if (missCondition != SPELL_MISS_NONE) + { + switch (missCondition) + { + case SPELL_MISS_MISS: + hitMask |= PROC_HIT_MISS; + break; + case SPELL_MISS_DODGE: + hitMask |= PROC_HIT_DODGE; + break; + case SPELL_MISS_PARRY: + hitMask |= PROC_HIT_PARRY; + break; + case SPELL_MISS_BLOCK: + // spells can't be partially blocked (it's damage can though) + hitMask |= PROC_HIT_BLOCK | PROC_HIT_FULL_BLOCK; + break; + case SPELL_MISS_EVADE: + hitMask |= PROC_HIT_EVADE; + break; + case SPELL_MISS_IMMUNE: + case SPELL_MISS_IMMUNE2: + hitMask |= PROC_HIT_IMMUNE; + break; + case SPELL_MISS_DEFLECT: + hitMask |= PROC_HIT_DEFLECT; + break; + case SPELL_MISS_ABSORB: + hitMask |= PROC_HIT_ABSORB; + break; + case SPELL_MISS_REFLECT: + hitMask |= PROC_HIT_REFLECT; + break; + case SPELL_MISS_RESIST: + hitMask |= PROC_HIT_FULL_RESIST; + break; + default: + break; + } + } + else + { + // On block + if (damageInfo->blocked) + { + hitMask |= PROC_HIT_BLOCK; + if (damageInfo->fullBlock) + hitMask |= PROC_HIT_FULL_BLOCK; + } + // On absorb + if (damageInfo->absorb) + hitMask |= PROC_HIT_ABSORB; + + // Don't set hit/crit hitMask if damage is nullified + bool const damageNullified = (damageInfo->HitInfo & (HITINFO_FULL_ABSORB | HITINFO_FULL_RESIST)) != 0 || (hitMask & PROC_HIT_FULL_BLOCK) != 0; + if (!damageNullified) + { + // On crit + if (damageInfo->HitInfo & SPELL_HIT_TYPE_CRIT) + hitMask |= PROC_HIT_CRITICAL; + else + hitMask |= PROC_HIT_NORMAL; + } + else if ((damageInfo->HitInfo & HITINFO_FULL_RESIST) != 0) + hitMask |= PROC_HIT_FULL_RESIST; + } + + return hitMask; +} + +void Unit::ProcSkillsAndReactives(bool isVictim, Unit* procTarget, ProcFlags typeMask, ProcFlagsHit hitMask, WeaponAttackType /*attType*/) +{ + // Player is loaded now - do not allow passive spell casts to proc + if (GetTypeId() == TYPEID_PLAYER && ToPlayer()->GetSession()->PlayerLoading()) + return; + + // For melee/ranged based attack need update skills and set some Aura states if victim present + if (typeMask & MELEE_BASED_TRIGGER_MASK && procTarget) + { + // If exist crit/parry/dodge/block need update aura state (for victim and attacker) + if (hitMask & (PROC_HIT_CRITICAL | PROC_HIT_PARRY | PROC_HIT_DODGE | PROC_HIT_BLOCK)) + { + // for victim + if (isVictim) + { + // Skip checks for Death Knights when in Blood Presence (Rune Strike) + if (getClass() == CLASS_DEATH_KNIGHT && hitMask & (PROC_HIT_DODGE | PROC_HIT_PARRY | PROC_HIT_BLOCK)) + if (GetAuraEffect(SPELL_AURA_MOD_BASE_RESISTANCE_PCT, SPELLFAMILY_DEATHKNIGHT, 0x800000, 0, 0, GetGUID())) + return; + + // if victim and dodge attack + if (hitMask & PROC_HIT_DODGE) + { + // Update AURA_STATE on dodge + if (getClass() != CLASS_ROGUE) // skip Rogue Riposte + { + ModifyAuraState(AURA_STATE_DEFENSE, true); + StartReactiveTimer(REACTIVE_DEFENSE); + } + } + // if victim and parry attack + if (hitMask & PROC_HIT_PARRY) + { + // For Hunters only Counterattack (skip Mongoose bite) + if (getClass() == CLASS_HUNTER) + { + ModifyAuraState(AURA_STATE_HUNTER_PARRY, true); + StartReactiveTimer(REACTIVE_HUNTER_PARRY); + } + else + { + ModifyAuraState(AURA_STATE_DEFENSE, true); + StartReactiveTimer(REACTIVE_DEFENSE); + } + } + // if and victim block attack + if (hitMask & PROC_HIT_BLOCK) + { + ModifyAuraState(AURA_STATE_DEFENSE, true); + StartReactiveTimer(REACTIVE_DEFENSE); + } + } + else // For attacker + { + // Overpower on victim dodge + if ((hitMask & PROC_HIT_DODGE) && GetTypeId() == TYPEID_PLAYER && getClass() == CLASS_WARRIOR) + { + ToPlayer()->AddComboPoints(procTarget, 1); + StartReactiveTimer(REACTIVE_OVERPOWER); + } + } + } + } +} + +void Unit::GetProcAurasTriggeredOnEvent(AuraApplicationProcContainer& aurasTriggeringProc, AuraApplicationList* procAuras, ProcEventInfo& eventInfo) +{ + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + + auto processAuraApplication = [&](AuraApplication* aurApp) + { + if (uint32 procEffectMask = aurApp->GetBase()->GetProcEffectMask(aurApp, eventInfo, now)) + { + aurApp->GetBase()->PrepareProcToTrigger(aurApp, eventInfo, now); + aurasTriggeringProc.emplace_back(procEffectMask, aurApp); + } + else + { + if (aurApp->GetBase()->GetSpellInfo()->HasAttribute(SPELL_ATTR0_PROC_FAILURE_BURNS_CHARGE)) + { + if (SpellProcEntry const* procEntry = sSpellMgr->GetSpellProcEntry(aurApp->GetBase()->GetSpellInfo()->Id)) + { + aurApp->GetBase()->PrepareProcChargeDrop(procEntry, eventInfo); + aurasTriggeringProc.emplace_back(0, aurApp); + } + } + + if (aurApp->GetBase()->GetSpellInfo()->HasAttribute(SPELL_ATTR2_PROC_COOLDOWN_ON_FAILURE)) + if (SpellProcEntry const* procEntry = sSpellMgr->GetSpellProcEntry(aurApp->GetBase()->GetSpellInfo()->Id)) + aurApp->GetBase()->AddProcCooldown(procEntry, now); + } + }; + + // use provided list of auras which can proc + if (procAuras) + { + for (AuraApplication* aurApp : *procAuras) + { + ASSERT(aurApp->GetTarget() == this); + processAuraApplication(aurApp); + } + } + // or generate one on our own + else + { + for (AuraApplicationMap::iterator itr = GetAppliedAuras().begin(); itr != GetAppliedAuras().end(); ++itr) + processAuraApplication(itr->second); + } +} + +void Unit::TriggerAurasProcOnEvent(Unit* actionTarget, ProcFlags typeMaskActor, ProcFlags typeMaskActionTarget, ProcFlagsSpellType spellTypeMask, + ProcFlagsSpellPhase spellPhaseMask, ProcFlagsHit hitMask, Spell* spell, DamageInfo* damageInfo, HealInfo* healInfo) +{ + // prepare data for self trigger + ProcEventInfo myProcEventInfo(this, actionTarget, actionTarget, typeMaskActor, spellTypeMask, spellPhaseMask, hitMask, spell, damageInfo, healInfo); + if (typeMaskActor) + { + AuraApplicationProcContainer myAurasTriggeringProc; + GetProcAurasTriggeredOnEvent(myAurasTriggeringProc, nullptr, myProcEventInfo); + + // needed for example for Cobra Strikes, pet does the attack, but aura is on owner + if (Player* modOwner = GetSpellModOwner()) + { + if (modOwner != this && spell) + { + AuraApplicationList modAuras; + for (auto itr = modOwner->GetAppliedAuras().begin(); itr != modOwner->GetAppliedAuras().end(); ++itr) + { + if (spell->m_appliedMods.count(itr->second->GetBase()) != 0) + modAuras.push_front(itr->second); + } + modOwner->GetProcAurasTriggeredOnEvent(myAurasTriggeringProc, &modAuras, myProcEventInfo); + } + } + TriggerAurasProcOnEvent(myProcEventInfo, myAurasTriggeringProc); + } + + // prepare data for target trigger + ProcEventInfo targetProcEventInfo(this, actionTarget, this, typeMaskActionTarget, spellTypeMask, spellPhaseMask, hitMask, spell, damageInfo, healInfo); + if (typeMaskActionTarget && actionTarget) + { + AuraApplicationProcContainer targetAurasTriggeringProc; + actionTarget->GetProcAurasTriggeredOnEvent(targetAurasTriggeringProc, nullptr, targetProcEventInfo); + actionTarget->TriggerAurasProcOnEvent(targetProcEventInfo, targetAurasTriggeringProc); + } +} + +void Unit::TriggerAurasProcOnEvent(ProcEventInfo& eventInfo, AuraApplicationProcContainer& aurasTriggeringProc) +{ + Spell const* triggeringSpell = eventInfo.GetProcSpell(); + bool const disableProcs = triggeringSpell && triggeringSpell->IsProcDisabled(); + + int32 oldProcChainLength = std::exchange(m_procChainLength, std::max(m_procChainLength + 1, triggeringSpell ? triggeringSpell->GetProcChainLength() : 0)); + + if (disableProcs) + SetCantProc(true); + + for (auto const& [procEffectMask, aurApp] : aurasTriggeringProc) + { + if (aurApp->GetRemoveMode().HasAnyFlag()) + continue; + + aurApp->GetBase()->TriggerProcOnEvent(procEffectMask, aurApp, eventInfo); + } + + if (disableProcs) + SetCantProc(false); + + m_procChainLength = oldProcChainLength; +} + +///----------Pet responses methods----------------- +void Unit::SendPetActionFeedback(uint8 response, uint32 spellId /*=0*/) +{ + Unit* owner = GetOwner(); + if (!owner || owner->GetTypeId() != TYPEID_PLAYER) + return; + + WorldPackets::Pet::PetActionFeedback actionFeedback; + actionFeedback.Response = response; + actionFeedback.SpellID = spellId; + owner->ToPlayer()->SendDirectMessage(actionFeedback.Write()); +} + +void Unit::SendPetActionSound(int32 action) +{ + Unit* owner = GetOwner(); + if (!owner || owner->GetTypeId() != TYPEID_PLAYER) + return; + + WorldPackets::Pet::PetActionSound actionSound; + actionSound.UnitGUID = GetGUID(); + actionSound.Action = action; + owner->ToPlayer()->SendDirectMessage(actionSound.Write()); +} + +void Unit::SendPetAIReaction(ObjectGuid guid) +{ + Unit* owner = GetOwner(); + if (!owner || owner->GetTypeId() != TYPEID_PLAYER) + return; + + WorldPackets::Combat::AIReaction packet; + packet.UnitGUID = guid; + packet.Reaction = AI_REACTION_HOSTILE; + + owner->ToPlayer()->SendDirectMessage(packet.Write()); +} + +///----------End of Pet responses methods---------- + +void Unit::PropagateSpeedChange() +{ + GetMotionMaster()->PropagateSpeedChange(); +} + +void Unit::StopMoving() +{ + ClearUnitState(UNIT_STATE_MOVING); + + // not need send any packets if not in world or not moving + if (!IsInWorld() || movespline->Finalized()) + return; + + // Update position now since Stop does not start a new movement that can be updated later + if (movespline->HasStarted()) + UpdateSplinePosition(); + Movement::MoveSplineInit init(this); + init.Stop(); +} + +void Unit::PauseMovement(uint32 timer/* = 0*/, uint8 slot/* = 0*/, bool forced/* = true*/) +{ + if (IsInvalidMovementSlot(slot)) + return; + + if (MovementGenerator* movementGenerator = GetMotionMaster()->GetMotionSlot(MovementSlot(slot))) + movementGenerator->Pause(timer); + + if (forced && GetMotionMaster()->GetCurrentSlot() == MovementSlot(slot)) + StopMoving(); +} + +void Unit::ResumeMovement(uint32 timer/* = 0*/, uint8 slot/* = 0*/) +{ + if (IsInvalidMovementSlot(slot)) + return; + + if (MovementGenerator* movementGenerator = GetMotionMaster()->GetMotionSlot(MovementSlot(slot))) + movementGenerator->Resume(timer); +} + +bool Unit::IsSitState() const +{ + uint8 s = GetStandState(); + return + s == UNIT_STAND_STATE_SIT_CHAIR || s == UNIT_STAND_STATE_SIT_LOW_CHAIR || + s == UNIT_STAND_STATE_SIT_MEDIUM_CHAIR || s == UNIT_STAND_STATE_SIT_HIGH_CHAIR || + s == UNIT_STAND_STATE_SIT; +} + +bool Unit::IsStandState() const +{ + uint8 s = GetStandState(); + return !IsSitState() && s != UNIT_STAND_STATE_SLEEP && s != UNIT_STAND_STATE_KNEEL; +} + +void Unit::SetStandState(uint8 state) +{ + SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, state); + + if (IsStandState()) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Standing); + + if (IsPlayer()) + { + WorldPackets::Misc::StandStateUpdate packet; + packet.State = state; + ToPlayer()->SendDirectMessage(packet.Write()); + } +} + +void Unit::SetAnimTier(AnimTier animTier, bool notifyClient /*= true*/) +{ + SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_ANIM_TIER, AsUnderlyingType(animTier)); + + if (notifyClient) + { + WorldPackets::Misc::SetAnimTier setAnimTier; + setAnimTier.Unit = GetGUID(); + setAnimTier.Tier = AsUnderlyingType(animTier); + SendMessageToSet(setAnimTier.Write(), true); + } +} + +bool Unit::IsPolymorphed() const +{ + uint32 transformId = getTransForm(); + if (!transformId) + return false; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(transformId); + if (!spellInfo) + return false; + + return spellInfo->GetSpellSpecific() == SPELL_SPECIFIC_MAGE_POLYMORPH; +} + +void Unit::RecalculateObjectScale() +{ + int32 scaleAuras = GetTotalAuraModifier(SPELL_AURA_MOD_SCALE) + GetTotalAuraModifier(SPELL_AURA_MOD_SCALE_2); + float scale = GetNativeObjectScale() + CalculatePct(1.0f, scaleAuras); + float scaleMin = GetTypeId() == TYPEID_PLAYER ? 0.1 : 0.01; + SetObjectScale(std::max(scale, scaleMin)); +} + +void Unit::SetDisplayId(uint32 modelId, bool setNative /*= false*/) +{ + SetUInt32Value(UNIT_FIELD_DISPLAYID, modelId); + // Set Gender by modelId + if (CreatureModelInfo const* minfo = sObjectMgr->GetCreatureModelInfo(modelId)) + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_GENDER, minfo->gender); + + if (setNative) + SetUInt32Value(UNIT_FIELD_NATIVEDISPLAYID, modelId); +} + +void Unit::RestoreDisplayId() +{ + AuraEffect* handledAura = nullptr; + // try to receive model from transform auras + AuraEffectList const& transforms = GetAuraEffectsByType(SPELL_AURA_TRANSFORM); + if (!transforms.empty()) + { + // iterate over already applied transform auras - from newest to oldest + for (auto i = transforms.begin(); i != transforms.end(); ++i) + { + if (AuraApplication const* aurApp = (*i)->GetBase()->GetApplicationOfTarget(GetGUID())) + { + if (!handledAura) + handledAura = (*i); + // prefer negative auras + if (!aurApp->IsPositive()) + { + handledAura = (*i); + break; + } + } + } + } + + AuraEffectList const& shapeshiftAura = GetAuraEffectsByType(SPELL_AURA_MOD_SHAPESHIFT); + + // transform aura was found + if (handledAura) + { + handledAura->HandleEffect(this, AURA_EFFECT_HANDLE_SEND_FOR_CLIENT, true); + return; + } + else if (!shapeshiftAura.empty()) // we've found shapeshift + { + // only one such aura possible at a time + if (uint32 modelId = GetModelForForm(GetShapeshiftForm(), shapeshiftAura.front()->GetId())) + { + SetDisplayId(modelId); + return; + } + } + + // no auras found - set modelid to default + SetDisplayId(GetNativeDisplayId()); +} + +void Unit::SendComboPoints() +{ + if (!IsPlayer()) + return; + + if (Unit* combotarget = ObjectAccessor::GetUnit(*this, m_comboTarget)) + { + WorldPacket data; + Player* movingMe = GetCharmerOrSelfPlayer(); + if (movingMe != this) + { + data.Initialize(SMSG_PET_UPDATE_COMBO_POINTS, movingMe->GetPackGUID().size() + combotarget->GetPackGUID().size() + 1); + data << movingMe->GetPackGUID(); + } + else + data.Initialize(SMSG_UPDATE_COMBO_POINTS, combotarget->GetPackGUID().size() + 1); + data << combotarget->GetPackGUID(); + data << uint8(m_comboPoints); + ToPlayer()->SendDirectMessage(&data); + } +} + +void Unit::AddComboPoints(Unit* target, int8 count, Spell* spell) +{ + if (!count) + return; + + int8* comboPoints = spell ? &spell->m_comboPointGain : &m_comboPoints; + + // without combo points lost (duration checked in aura) + RemoveAurasByType(SPELL_AURA_RETAIN_COMBO_POINTS); + + if (target->GetGUID() == m_comboTarget) + *comboPoints += count; + else + { + if (m_comboTarget) + if (Unit* target2 = ObjectAccessor::GetUnit(*this, m_comboTarget)) + target2->RemoveComboPointHolder(GetGUID()); + + // Spells will always add value to m_comboPoints eventualy, so it must be cleared first + if (spell) + m_comboPoints = 0; + + m_comboTarget = target->GetGUID(); + *comboPoints = count; + + target->AddComboPointHolder(GetGUID()); + } + + if (*comboPoints > 5) + *comboPoints = 5; + else if (*comboPoints < 0) + *comboPoints = 0; + + if (!spell) + SendComboPoints(); +} + +void Unit::GainSpellComboPoints(int8 count) +{ + if (!count) + return; + + m_comboPoints += count; + if (m_comboPoints > 5) m_comboPoints = 5; + else if (m_comboPoints < 0) m_comboPoints = 0; + + SendComboPoints(); +} + +void Unit::ClearComboPoints() +{ + if (!m_comboTarget) + return; + + // without combopoints lost (duration checked in aura) + RemoveAurasByType(SPELL_AURA_RETAIN_COMBO_POINTS); + + m_comboPoints = 0; + + SendComboPoints(); + + if (Unit* target = ObjectAccessor::GetUnit(*this, m_comboTarget)) + target->RemoveComboPointHolder(GetGUID()); + + m_comboTarget.Clear(); +} + +void Unit::ClearComboPointHolders() +{ + while (!m_ComboPointHolders.empty()) + { + ObjectGuid guid = *m_ComboPointHolders.begin(); + + Player* player = ObjectAccessor::GetPlayer(*this, guid); + if (player && player->GetComboTarget() == GetGUID()) // recheck for safe + player->ClearComboPoints(); // remove also guid from m_ComboPointHolders; + else + m_ComboPointHolders.erase(guid); // or remove manually + } +} + +void Unit::ClearAllReactives() +{ + for (uint8 i = 0; i < MAX_REACTIVE; ++i) + m_reactiveTimer[i] = 0; + + if (HasAuraState(AURA_STATE_DEFENSE)) + ModifyAuraState(AURA_STATE_DEFENSE, false); + if (getClass() == CLASS_HUNTER && HasAuraState(AURA_STATE_HUNTER_PARRY)) + ModifyAuraState(AURA_STATE_HUNTER_PARRY, false); + if (getClass() == CLASS_WARRIOR && GetTypeId() == TYPEID_PLAYER) + ToPlayer()->ClearComboPoints(); +} + +void Unit::UpdateReactives(uint32 p_time) +{ + for (uint8 i = 0; i < MAX_REACTIVE; ++i) + { + ReactiveType reactive = ReactiveType(i); + + if (!m_reactiveTimer[reactive]) + continue; + + if (m_reactiveTimer[reactive] <= p_time) + { + m_reactiveTimer[reactive] = 0; + + switch (reactive) + { + case REACTIVE_DEFENSE: + if (HasAuraState(AURA_STATE_DEFENSE)) + ModifyAuraState(AURA_STATE_DEFENSE, false); + break; + case REACTIVE_HUNTER_PARRY: + if (getClass() == CLASS_HUNTER && HasAuraState(AURA_STATE_HUNTER_PARRY)) + ModifyAuraState(AURA_STATE_HUNTER_PARRY, false); + break; + case REACTIVE_OVERPOWER: + if (getClass() == CLASS_WARRIOR && GetTypeId() == TYPEID_PLAYER) + ToPlayer()->ClearComboPoints(); + break; + } + } + else + { + m_reactiveTimer[reactive] -= p_time; + } + } +} + +Unit* Unit::SelectNearbyTarget(Unit* exclude, float dist) const +{ + std::list targets; + Trinity::AnyUnfriendlyUnitInObjectRangeCheck u_check(this, this, dist); + Trinity::UnitListSearcher searcher(this, targets, u_check); + Cell::VisitAllObjects(this, searcher, dist); + + // remove current target + if (GetVictim()) + targets.remove(GetVictim()); + + if (exclude) + targets.remove(exclude); + + // remove not LoS targets + for (std::list::iterator tIter = targets.begin(); tIter != targets.end();) + { + if (!IsWithinLOSInMap(*tIter) || (*tIter)->IsTotem() || (*tIter)->IsSpiritService() || (*tIter)->IsCritter()) + targets.erase(tIter++); + else + ++tIter; + } + + // no appropriate targets + if (targets.empty()) + return nullptr; + + // select random + return Trinity::Containers::SelectRandomContainerElement(targets); +} + +void ApplyPercentModFloatVar(float& var, float val, bool apply) +{ + var *= (apply ? (100.0f + val) / 100.0f : 100.0f / (100.0f + val)); +} + +uint32 Unit::GetBaseAttackTime(WeaponAttackType att) const +{ + return m_baseAttackSpeed[att]; +} + +void Unit::SetBaseAttackTime(WeaponAttackType att, uint32 val) +{ + m_baseAttackSpeed[att] = val; + UpdateAttackTimeField(att); +} + +void Unit::UpdateAttackTimeField(WeaponAttackType att) +{ + switch (att) + { + case BASE_ATTACK: + case OFF_ATTACK: + SetFloatValue(UNIT_FIELD_BASEATTACKTIME + AsUnderlyingType(att), uint32(m_baseAttackSpeed[att] * m_modAttackSpeedPct[att])); + break; + case RANGED_ATTACK: + SetFloatValue(UNIT_FIELD_RANGEDATTACKTIME, uint32(m_baseAttackSpeed[RANGED_ATTACK] * m_modAttackSpeedPct[RANGED_ATTACK])); + break; + default: + break; + } +} + +void Unit::ApplyAttackTimePercentMod(WeaponAttackType att, float val, bool apply) +{ + float amount = GetFloatValue(UNIT_FIELD_BASEATTACKTIME + AsUnderlyingType(att)); + float hasteAmount = GetTypeId() == TYPEID_PLAYER ? GetFloatValue(PLAYER_FIELD_MOD_HASTE) : 0.f; + float rangedHasteAmount = GetTypeId() == TYPEID_PLAYER ? GetFloatValue(PLAYER_FIELD_MOD_RANGED_HASTE) : 0.f; + + float remainingTimePct = float(m_attackTimer[att]) / (m_baseAttackSpeed[att] * m_modAttackSpeedPct[att]); + if (val > 0.f) + { + ApplyPercentModFloatVar(m_modAttackSpeedPct[att], val, !apply); + ApplyPercentModFloatVar(amount, val, !apply); + + if (GetTypeId() == TYPEID_PLAYER) + { + if (att == BASE_ATTACK) + ApplyPercentModFloatVar(hasteAmount, val, !apply); + else if (att == RANGED_ATTACK) + ApplyPercentModFloatVar(rangedHasteAmount, val, !apply); + } + } + else + { + ApplyPercentModFloatVar(m_modAttackSpeedPct[att], -val, apply); + ApplyPercentModFloatVar(amount, -val, apply); + + if (GetTypeId() == TYPEID_PLAYER) + { + if (att == BASE_ATTACK) + ApplyPercentModFloatVar(hasteAmount, -val, apply); + else if (att == RANGED_ATTACK) + ApplyPercentModFloatVar(rangedHasteAmount, -val, apply); + } + } + + SetFloatValue(UNIT_FIELD_BASEATTACKTIME + AsUnderlyingType(att), amount); + + if (GetTypeId() == TYPEID_PLAYER) + { + SetFloatValue(PLAYER_FIELD_MOD_HASTE, hasteAmount); + SetFloatValue(PLAYER_FIELD_MOD_RANGED_HASTE, rangedHasteAmount); + } + + UpdateAttackTimeField(att); + m_attackTimer[att] = uint32(m_baseAttackSpeed[att] * m_modAttackSpeedPct[att] * remainingTimePct); +} + +void Unit::ApplyHasteRegenMod(float val, bool apply) +{ + if (!IsPlayer()) + return; + + float amount = GetFloatValue(PLAYER_FIELD_MOD_HASTE_REGEN); + + if (val > 0.f) + ApplyPercentModFloatVar(amount, val, !apply); + else + ApplyPercentModFloatVar(amount, -val, apply); + + SetFloatValue(PLAYER_FIELD_MOD_HASTE_REGEN, amount); + + for (PowerType powerType : GetUsedPowerTypes()) + { + if (powerType == MAX_POWERS || GetPowerIndex(powerType) == MAX_POWERS) + continue; + + if (DBCManager::IsPowerTypeAffectedByHaste(powerType)) + UpdatePowerRegeneration(powerType); + } + + if (IsPlayer() && getClass() == CLASS_DEATH_KNIGHT) + UpdatePowerRegeneration(POWER_RUNE); +} + +void Unit::ApplyCastTimePercentMod(float val, bool apply, bool withCastHaste /*= true*/, bool withCastSpeed /*= true*/) +{ + float speedAmount = GetFloatValue(UNIT_MOD_CAST_SPEED); + float hasteAmount = GetFloatValue(UNIT_MOD_CAST_HASTE); + + if (val > 0.f) + { + if (withCastSpeed) + ApplyPercentModFloatVar(speedAmount, val, !apply); + if (withCastHaste) + ApplyPercentModFloatVar(hasteAmount, val, !apply); + } + else + { + if (withCastSpeed) + ApplyPercentModFloatVar(speedAmount, -val, apply); + if (withCastHaste) + ApplyPercentModFloatVar(hasteAmount, -val, apply); + } + + SetFloatValue(UNIT_MOD_CAST_SPEED, speedAmount); + SetFloatValue(UNIT_MOD_CAST_HASTE, hasteAmount); +} + +uint32 Unit::GetCastingTimeForBonus(SpellInfo const* spellProto, DamageEffectType damagetype, uint32 CastingTime) const +{ + // Not apply this to creature cast spells with casttime == 0 + if (CastingTime == 0 && GetTypeId() == TYPEID_UNIT && !IsPet()) + return 3500; + + if (CastingTime > 7000) CastingTime = 7000; + if (CastingTime < 1500) CastingTime = 1500; + + if (damagetype == DOT && !spellProto->IsChanneled()) + CastingTime = 3500; + + int32 overTime = 0; + uint8 effects = 0; + bool DirectDamage = false; + bool AreaEffect = false; + + for (uint32 i = 0; i < MAX_SPELL_EFFECTS; i++) + { + switch (spellProto->Effects[i].Effect) + { + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_POWER_BURN: + case SPELL_EFFECT_HEAL: + DirectDamage = true; + break; + case SPELL_EFFECT_APPLY_AURA: + switch (spellProto->Effects[i].ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_PERIODIC_LEECH: + if (spellProto->GetDuration()) + overTime = spellProto->GetDuration(); + break; + default: + // -5% per additional effect + ++effects; + break; + } + break; + default: + break; + } + + if (spellProto->Effects[i].IsTargetingArea()) + AreaEffect = true; + } + + // Combined Spells with Both Over Time and Direct Damage + if (overTime > 0 && CastingTime > 0 && DirectDamage) + { + // mainly for DoTs which are 3500 here otherwise + uint32 OriginalCastTime = spellProto->CalcCastTime(getLevel()); + if (OriginalCastTime > 7000) OriginalCastTime = 7000; + if (OriginalCastTime < 1500) OriginalCastTime = 1500; + // Portion to Over Time + float PtOT = (overTime / 15000.0f) / ((overTime / 15000.0f) + (OriginalCastTime / 3500.0f)); + + if (damagetype == DOT) + CastingTime = uint32(CastingTime * PtOT); + else if (PtOT < 1.0f) + CastingTime = uint32(CastingTime * (1 - PtOT)); + else + CastingTime = 0; + } + + // Area Effect Spells receive only half of bonus + if (AreaEffect) + CastingTime /= 2; + + // 50% for damage and healing spells for leech spells from damage bonus and 0% from healing + for (uint8 j = 0; j < MAX_SPELL_EFFECTS; ++j) + { + if (spellProto->Effects[j].Effect == SPELL_EFFECT_HEALTH_LEECH || + (spellProto->Effects[j].Effect == SPELL_EFFECT_APPLY_AURA && spellProto->Effects[j].ApplyAuraName == SPELL_AURA_PERIODIC_LEECH)) + { + CastingTime /= 2; + break; + } + } + + // -5% of total per any additional effect + for (uint8 i = 0; i < effects; ++i) + CastingTime *= 0.95f; + + return CastingTime; +} + +void Unit::UpdateAuraForGroup(uint8 slot) +{ + if (slot >= MAX_AURAS) // slot not found, return + return; + if (Player* player = ToPlayer()) + { + if (player->GetGroup()) + { + player->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_AURAS); + player->SetAuraUpdateMaskForRaid(slot); + } + } + else if (GetTypeId() == TYPEID_UNIT && IsPet()) + { + Pet* pet = ((Pet*)this); + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + { + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_AURAS); + pet->SetAuraUpdateMaskForRaid(slot); + } + } + } +} + +void Unit::SetCantProc(bool apply) +{ + if (apply) + ++m_procDeep; + else + { + ASSERT(m_procDeep); + --m_procDeep; + } +} + +float Unit::CalculateDefaultCoefficient(SpellInfo const* spellInfo, DamageEffectType damagetype) const +{ + // Damage over Time spells bonus calculation + float DotFactor = 1.0f; + if (damagetype == DOT) + { + + int32 DotDuration = spellInfo->GetDuration(); + if (!spellInfo->IsChanneled() && DotDuration > 0) + DotFactor = DotDuration / 15000.0f; + + if (uint32 DotTicks = spellInfo->GetMaxTicks()) + DotFactor /= DotTicks; + } + + int32 CastingTime = spellInfo->IsChanneled() ? spellInfo->GetDuration() : spellInfo->CalcCastTime(getLevel()); + // Distribute Damage over multiple effects, reduce by AoE + CastingTime = GetCastingTimeForBonus(spellInfo, damagetype, CastingTime); + + // As wowwiki says: C = (Cast Time / 3.5) + return (CastingTime / 3500.0f) * DotFactor; +} + +float Unit::GetAPMultiplier(WeaponAttackType attType, bool normalized) const +{ + if (GetTypeId() != TYPEID_PLAYER || (IsInFeralForm() && !normalized)) + return GetBaseAttackTime(attType) / 1000.0f; + + Item* weapon = ToPlayer()->GetWeaponForAttack(attType, true); + if (!weapon) + return 2.0f; + + if (!normalized) + return weapon->GetTemplate()->GetDelay() / 1000.0f; + + switch (weapon->GetTemplate()->GetSubClass()) + { + case ITEM_SUBCLASS_WEAPON_AXE2: + case ITEM_SUBCLASS_WEAPON_MACE2: + case ITEM_SUBCLASS_WEAPON_POLEARM: + case ITEM_SUBCLASS_WEAPON_SWORD2: + case ITEM_SUBCLASS_WEAPON_STAFF: + case ITEM_SUBCLASS_WEAPON_FISHING_POLE: + return 3.3f; + case ITEM_SUBCLASS_WEAPON_BOW: + case ITEM_SUBCLASS_WEAPON_GUN: + case ITEM_SUBCLASS_WEAPON_CROSSBOW: + case ITEM_SUBCLASS_WEAPON_THROWN: + return 2.8f; + case ITEM_SUBCLASS_WEAPON_AXE: + case ITEM_SUBCLASS_WEAPON_MACE: + case ITEM_SUBCLASS_WEAPON_SWORD: + case ITEM_SUBCLASS_WEAPON_EXOTIC: + case ITEM_SUBCLASS_WEAPON_EXOTIC2: + case ITEM_SUBCLASS_WEAPON_FIST_WEAPON: + return 2.4f; + case ITEM_SUBCLASS_WEAPON_DAGGER: + return 1.7f; + default: + return weapon->GetTemplate()->GetDelay() / 1000.0f; + } +} + +Pet* Unit::CreateTamedPetFrom(Creature* creatureTarget, uint32 spell_id) +{ + if (GetTypeId() != TYPEID_PLAYER) + return nullptr; + + Pet* pet = new Pet(ToPlayer(), HUNTER_PET); + + if (!pet->CreateBaseAtCreature(creatureTarget)) + { + delete pet; + return nullptr; + } + + uint8 level = getLevel(); + + InitTamedPet(pet, level, spell_id); + + return pet; +} + +Pet* Unit::CreateTamedPetFrom(uint32 creatureEntry, uint32 spell_id) +{ + if (GetTypeId() != TYPEID_PLAYER) + return nullptr; + + CreatureTemplate const* creatureInfo = sObjectMgr->GetCreatureTemplate(creatureEntry); + if (!creatureInfo) + return nullptr; + + Pet* pet = new Pet(ToPlayer(), HUNTER_PET); + + if (!pet->CreateBaseAtCreatureInfo(creatureInfo, this) || !InitTamedPet(pet, getLevel(), spell_id)) + { + delete pet; + return nullptr; + } + + return pet; +} + +bool Unit::InitTamedPet(Pet* pet, uint8 level, uint32 spell_id) +{ + pet->SetCreatorGUID(GetGUID()); + pet->SetFaction(GetFaction()); + pet->SetUInt32Value(UNIT_CREATED_BY_SPELL, spell_id); + + if (GetTypeId() == TYPEID_PLAYER) + pet->SetUInt32Value(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + + if (!pet->InitStatsForLevel(level)) + { + TC_LOG_ERROR("entities.unit", "Pet::InitStatsForLevel() failed for creature (Entry: %u)!", pet->GetEntry()); + return false; + } + + PhasingHandler::InheritPhaseShift(pet, this); + + pet->GetCharmInfo()->SetPetNumber(sObjectMgr->GeneratePetNumber(), true); + // this enables pet details window (Shift+P) + pet->InitPetCreateSpells(); + //pet->InitLevelupSpellsForLevel(); + pet->SetFullHealth(); + pet->CastSpell(pet, 99289, true); // Energize + pet->SetReactState(REACT_ASSIST); + return true; +} + +void Unit::SendDurabilityLoss(Player* receiver, uint32 percent) +{ + WorldPackets::Misc::DurabilityDamageDeath packet; + packet.Percent = percent; + receiver->SendDirectMessage(packet.Write()); +} + +void Unit::PlayOneShotAnimKitId(uint16 animKitId) +{ + WorldPackets::Misc::PlayOneShotAnimKit packet; + packet.Unit = GetGUID(); + packet.AnimKitID = animKitId; + SendMessageToSet(packet.Write(), true); +} + +/*static*/ void Unit::Kill(Unit* attacker, Unit* victim, bool durabilityLoss /*= true*/) +{ + // Prevent killing unit twice (and giving reward from kill twice) + if (!victim->GetHealth()) + return; + + if (attacker && !attacker->IsInMap(victim)) + attacker = nullptr; + + // find player: owner of controlled `this` or `this` itself maybe + Player* player = nullptr; + if (attacker) + player = attacker->GetCharmerOrOwnerPlayerOrPlayerItself(); + + Creature* creature = victim->ToCreature(); + + bool isRewardAllowed = true; + if (creature) + { + isRewardAllowed = creature->IsDamageEnoughForLootingAndReward(); + if (!isRewardAllowed) + creature->SetLootRecipient(nullptr); + } + + if (isRewardAllowed && creature) + { + if (Player* lootRecipient = creature->GetLootRecipient()) + { + // Loot recipient can be in a different map + if (!creature->IsInMap(lootRecipient)) + { + if (Group* group = creature->GetLootRecipientGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !creature->IsInMap(member)) + continue; + + player = member; + break; + } + } + } + else + player = creature->GetLootRecipient(); + } + } + + // Exploit fix + if (creature && creature->IsPet() && creature->GetOwnerOrCreatorGUID().IsPlayer()) + isRewardAllowed = false; + + // Reward player, his pets, and group/raid members + // call kill spell proc event (before real die and combat stop to triggering auras removed at death/combat stop) + if (isRewardAllowed && player && player != victim) + { + WorldPacket data(SMSG_PARTYKILLLOG, (8+8)); // send event PARTY_KILL + data << uint64(player->GetGUID()); // player with killing blow + data << uint64(victim->GetGUID()); // victim + + Player* looter = player; + Group* group = player->GetGroup(); + bool hasLooterGuid = false; + + if (group) + { + group->BroadcastPacket(&data, group->GetMemberGroup(player->GetGUID()) != 0); + + if (creature && !creature->IsLootDisabled()) + { + group->UpdateLooterGuid(creature, true); + if (group->GetLooterGuid()) + { + looter = ObjectAccessor::FindPlayer(group->GetLooterGuid()); + if (looter) + { + hasLooterGuid = true; + creature->SetLootRecipient(looter); // update creature loot recipient to the allowed looter. + } + } + } + } + else + { + player->SendDirectMessage(&data); + + if (creature && !creature->IsLootDisabled()) + { + WorldPackets::Loot::LootList lootList; + lootList.Owner = creature->GetGUID(); + lootList.RoundRobinWinner = player->GetGUID(); + + player->SendMessageToSet(lootList.Write(), false); + } + } + + // Generate loot before updating looter + if (creature && !creature->IsLootDisabled()) + { + Loot* loot = &creature->loot; + loot->clear(); + + if (uint32 lootid = creature->GetCreatureTemplate()->lootid) + loot->FillLoot(lootid, LootTemplates_Creature, looter, false, false, creature->GetLootMode()); + + if (creature->GetLootMode() > 0) + loot->generateMoneyLoot(creature->GetCreatureTemplate()->mingold, creature->GetCreatureTemplate()->maxgold); + + if (group) + { + if (hasLooterGuid) + group->SendLooter(creature, looter); + else + group->SendLooter(creature, nullptr); + + // Update round robin looter only if the creature had loot + if (!loot->empty()) + group->UpdateLooterGuid(creature); + } + } + + player->RewardPlayerAndGroupAtKill(victim, false); + } + + // Do KILL and KILLED procs. KILL proc is called only for the unit who landed the killing blow (and its owner - for pets and totems) regardless of who tapped the victim + if (attacker && (attacker->IsPet() || attacker->IsTotem())) + { + // proc only once for victim + if (Unit* owner = attacker->GetOwner()) + Unit::ProcSkillsAndAuras(owner, victim, PROC_FLAG_KILL, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); + } + + if (!victim->IsCritter()) + Unit::ProcSkillsAndAuras(attacker, victim, PROC_FLAG_KILL, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); + + // Proc auras on death - must be before aura/combat remove + Unit::ProcSkillsAndAuras(victim, victim, PROC_FLAG_NONE, PROC_FLAG_DEATH, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); + + // update get killing blow achievements, must be done before setDeathState to be able to require auras on target + // and before Spirit of Redemption as it also removes auras + if (attacker) + if (Player* killerPlayer = attacker->GetCharmerOrOwnerPlayerOrPlayerItself()) + killerPlayer->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_GET_KILLING_BLOWS, 1, 0, 0, victim); + + TC_LOG_DEBUG("entities.unit", "SET JUST_DIED"); + victim->setDeathState(JUST_DIED); + + // Inform pets (if any) when player kills target) + // MUST come after victim->setDeathState(JUST_DIED); or pet next target + // selection will get stuck on same target and break pet react state + if (player) + { + Pet* pet = player->GetPet(); + if (pet && pet->IsAlive() && pet->isControlled()) + { + if (pet->IsAIEnabled()) + pet->AI()->KilledUnit(victim); + else + TC_LOG_ERROR("entities.unit", "Pet doesn't have any AI in Unit::Kill(). %s", pet->GetGUID().ToString().c_str()); + } + } + + // 10% durability loss on death + if (Player* plrVictim = victim->ToPlayer()) + { + // remember victim PvP death for corpse type and corpse reclaim delay + // at original death (not at SpiritOfRedemtionTalent timeout) + plrVictim->SetPvPDeath(player != nullptr); + + // only if not player and not controlled by player pet. And not at BG + if ((durabilityLoss && !player && !victim->ToPlayer()->InBattleground()) || (player && sWorld->getBoolConfig(CONFIG_DURABILITY_LOSS_IN_PVP))) + { + double baseLoss = sWorld->getRate(RATE_DURABILITY_LOSS_ON_DEATH); + uint32 loss = uint32(baseLoss - (baseLoss * plrVictim->GetTotalAuraMultiplier(SPELL_AURA_MOD_DURABILITY_LOSS))); + TC_LOG_DEBUG("entities.unit", "We are dead, losing %u percent durability", loss); + // Durability loss is calculated more accurately again for each item in Player::DurabilityLoss + plrVictim->DurabilityLossAll(baseLoss, false); + // durability lost message + plrVictim->SendDurabilityLoss(plrVictim, loss); + } + // Call KilledUnit for creatures + if (attacker && attacker->IsCreature() && attacker->IsAIEnabled()) + attacker->ToCreature()->AI()->KilledUnit(victim); + + // last damage from non duel opponent or opponent controlled creature + if (plrVictim->duel) + { + plrVictim->duel->opponent->CombatStopWithPets(true); + plrVictim->CombatStopWithPets(true); + plrVictim->DuelComplete(DUEL_INTERRUPTED); + } + } + else // creature died + { + TC_LOG_DEBUG("entities.unit", "DealDamageNotPlayer"); + + if (creature && !creature->IsPet()) + { + // must be after setDeathState which resets dynamic flags + if (!creature->loot.isLooted()) + creature->SetFlag(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_LOOTABLE); + else + creature->AllLootRemovedFromCorpse(); + } + + // Call KilledUnit for creatures, this needs to be called after the lootable flag is set + if (attacker && attacker->IsCreature() && attacker->IsAIEnabled()) + attacker->ToCreature()->AI()->KilledUnit(victim); + + // Call creature just died function + if (CreatureAI* ai = creature->AI()) + ai->JustDied(attacker); + + if (TempSummon* summon = creature->ToTempSummon()) + if (Unit* summoner = summon->GetSummoner()) + if (summoner->ToCreature() && summoner->IsAIEnabled()) + summoner->ToCreature()->AI()->SummonedCreatureDies(creature, attacker); + + // Dungeon specific stuff, only applies to players killing creatures + if (creature->GetInstanceId()) + { + Map* instanceMap = creature->GetMap(); + /// @todo do instance binding anyway if the charmer/owner is offline + + if (instanceMap->IsDungeon() && ((attacker && attacker->GetCharmerOrOwnerPlayerOrPlayerItself()) || attacker == victim)) + { + if (instanceMap->IsRaidOrHeroicDungeon()) + { + if (creature->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_INSTANCE_BIND) + instanceMap->ToInstanceMap()->PermBindAllPlayers(); + } + else + { + // the reset time is set but not added to the scheduler + // until the players leave the instance + time_t resettime = GameTime::GetGameTime() + 2 * HOUR; + if (InstanceSave* save = sInstanceSaveMgr->GetInstanceSave(creature->GetInstanceId())) + if (save->GetResetTime() < resettime) + save->SetResetTime(resettime); + } + } + } + } + + // outdoor pvp things, do these after setting the death state, else the player activity notify won't work... doh... + // handle player kill only if not suicide (spirit of redemption for example) + if (player && attacker != victim) + { + if (OutdoorPvP* pvp = player->GetOutdoorPvP()) + pvp->HandleKill(player, victim); + + if (Battlefield* bf = sBattlefieldMgr->GetBattlefieldToZoneId(player->GetMap(), player->GetZoneId())) + bf->HandleKill(player, victim); + } + + //if (victim->GetTypeId() == TYPEID_PLAYER) + // if (OutdoorPvP* pvp = victim->ToPlayer()->GetOutdoorPvP()) + // pvp->HandlePlayerActivityChangedpVictim->ToPlayer(); + + // battleground things (do this at the end, so the death state flag will be properly set to handle in the bg->handlekill) + if (player && player->InBattleground()) + { + if (Battleground* bg = player->GetBattleground()) + { + if (Player* playerVictim = victim->ToPlayer()) + bg->HandleKillPlayer(playerVictim, player); + else + bg->HandleKillUnit(victim->ToCreature(), player); + } + } + + // achievement stuff + if (attacker && victim->GetTypeId() == TYPEID_PLAYER) + { + if (attacker->GetTypeId() == TYPEID_UNIT) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_CREATURE, attacker->GetEntry()); + else if (attacker->GetTypeId() == TYPEID_PLAYER && victim != attacker) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_PLAYER, 1, attacker->ToPlayer()->GetTeam()); + } + + // Hook for OnPVPKill Event + if (attacker) + { + if (Player* killerPlr = attacker->ToPlayer()) + { + if (Player* killedPlr = victim->ToPlayer()) + sScriptMgr->OnPVPKill(killerPlr, killedPlr); + else if (Creature* killedCre = victim->ToCreature()) + sScriptMgr->OnCreatureKill(killerPlr, killedCre); + } + else if (Creature* killerCre = attacker->ToCreature()) + { + if (Player* killed = victim->ToPlayer()) + sScriptMgr->OnPlayerKilledByCreature(killerCre, killed); + } + } +} + +void Unit::SetControlled(bool apply, UnitState state) +{ + if (apply) + { + if (HasUnitState(state)) + return; + + if (state & UNIT_STATE_CONTROLLED) + CastStop(); + + AddUnitState(state); + switch (state) + { + case UNIT_STATE_STUNNED: + SetStunned(true); + break; + case UNIT_STATE_ROOT: + if (!HasUnitState(UNIT_STATE_STUNNED)) + SetRooted(true); + break; + case UNIT_STATE_CONFUSED: + if (!HasUnitState(UNIT_STATE_STUNNED)) + { + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + SendMeleeAttackStop(); + // SendAutoRepeatCancel ? + SetConfused(true); + } + break; + case UNIT_STATE_FLEEING: + if (!HasUnitState(UNIT_STATE_STUNNED | UNIT_STATE_CONFUSED)) + { + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + SendMeleeAttackStop(); + // SendAutoRepeatCancel ? + SetFeared(true); + } + break; + default: + break; + } + } + else + { + switch (state) + { + case UNIT_STATE_STUNNED: + if (HasAuraType(SPELL_AURA_MOD_STUN)) + return; + + ClearUnitState(state); + SetStunned(false); + break; + case UNIT_STATE_ROOT: + if (HasAuraType(SPELL_AURA_MOD_ROOT) || GetVehicle() || (IsCreature() && ToCreature()->IsSessile())) + return; + + ClearUnitState(state); + SetRooted(false); + break; + case UNIT_STATE_CONFUSED: + if (HasAuraType(SPELL_AURA_MOD_CONFUSE)) + return; + + ClearUnitState(state); + SetConfused(false); + break; + case UNIT_STATE_FLEEING: + if (HasAuraType(SPELL_AURA_MOD_FEAR)) + return; + + ClearUnitState(state); + SetFeared(false); + break; + default: + return; + } + + ApplyControlStatesIfNeeded(); + } +} + +void Unit::ApplyControlStatesIfNeeded() +{ + // Unit States might have been already cleared but auras still present. I need to check with HasAuraType + if (HasUnitState(UNIT_STATE_STUNNED) || HasAuraType(SPELL_AURA_MOD_STUN)) + SetStunned(true); + + if (HasUnitState(UNIT_STATE_ROOT) || HasAuraType(SPELL_AURA_MOD_ROOT)) + SetRooted(true); + + if (HasUnitState(UNIT_STATE_CONFUSED) || HasAuraType(SPELL_AURA_MOD_CONFUSE)) + SetConfused(true); + + if (HasUnitState(UNIT_STATE_FLEEING) || HasAuraType(SPELL_AURA_MOD_FEAR)) + SetFeared(true); +} + +void Unit::SetStunned(bool apply) +{ + if (apply) + { + SetTarget(ObjectGuid::Empty); + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + + // Do not stop movement when the unit is affected by parabolic spline movement (knockbacks, pulls, scripted leaps etc.) + if (!movespline->isParabolic()) + StopMoving(); + + if (GetTypeId() == TYPEID_PLAYER) + SetStandState(UNIT_STAND_STATE_STAND); + SetRooted(true); + + CastStop(); + } + else + { + if (IsAlive() && GetVictim()) + SetTarget(EnsureVictim()->GetGUID()); + + // don't remove UNIT_FLAG_STUNNED for pet when owner is mounted (disabled pet's interface) + Unit* owner = GetCharmerOrOwner(); + if (!owner || owner->GetTypeId() != TYPEID_PLAYER || !owner->ToPlayer()->IsMounted()) + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + + if (!HasUnitState(UNIT_STATE_ROOT)) // prevent moving if it also has root effect + SetRooted(false); + } +} + +void Unit::SetRooted(bool apply, bool packetOnly /*= false*/) +{ + if (!packetOnly) + { + if (apply) + { + // MOVEMENTFLAG_ROOT cannot be used in conjunction with MOVEMENTFLAG_MASK_MOVING (tested 3.3.5a) + // this will freeze clients. That's why we remove MOVEMENTFLAG_MASK_MOVING before + // setting MOVEMENTFLAG_ROOT + RemoveUnitMovementFlag(MOVEMENTFLAG_MASK_MOVING); + AddUnitMovementFlag(MOVEMENTFLAG_ROOT); + + // Do not stop movement when the unit is affected by parabolic spline movement (knockbacks, pulls, scripted leaps etc.) + if (!movespline->isParabolic()) + StopMoving(); + } + else + RemoveUnitMovementFlag(MOVEMENTFLAG_ROOT); + } + + if (apply) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_ROOT, SMSG_MOVE_ROOT, SMSG_MOVE_ROOT).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_UNROOT, SMSG_MOVE_UNROOT, SMSG_MOVE_UNROOT).Send(); +} + +void Unit::SetFeared(bool apply) +{ + if (apply) + { + // block control to real player in control (eg charmer) + if (GetCharmerOrSelfPlayer()) + GetCharmerOrSelfPlayer()->SetClientControl(this, false); + + SetTarget(ObjectGuid::Empty); + + Unit* caster = nullptr; + Unit::AuraEffectList const& fearAuras = GetAuraEffectsByType(SPELL_AURA_MOD_FEAR); + if (!fearAuras.empty()) + caster = ObjectAccessor::GetUnit(*this, fearAuras.front()->GetCasterGUID()); + if (!caster) + caster = getAttackerForHelper(); + GetMotionMaster()->MoveFleeing(caster, fearAuras.empty() ? sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_FLEE_DELAY) : 0); // caster == nullptr processed in MoveFleeing + } + else + { + if (IsAlive()) + { + if (GetMotionMaster()->GetCurrentMovementGeneratorType() == FLEEING_MOTION_TYPE) + GetMotionMaster()->MovementExpired(); + if (GetVictim()) + SetTarget(EnsureVictim()->GetGUID()); + } + + // allow control to real player in control (eg charmer) + if (GetCharmerOrSelfPlayer()) + GetCharmerOrSelfPlayer()->SetClientControl(this, true); + } +} + +void Unit::SetConfused(bool apply) +{ + if (apply) + { + // block control to real player in control (eg charmer) + if (GetCharmerOrSelfPlayer()) + GetCharmerOrSelfPlayer()->SetClientControl(this, false); + + SetTarget(ObjectGuid::Empty); + GetMotionMaster()->MoveConfused(); + } + else + { + if (IsAlive()) + { + if (GetMotionMaster()->GetCurrentMovementGeneratorType() == CONFUSED_MOTION_TYPE) + GetMotionMaster()->MovementExpired(); + if (GetVictim()) + SetTarget(EnsureVictim()->GetGUID()); + } + + // allow control to real player in control (eg charmer) + if (GetCharmerOrSelfPlayer()) + GetCharmerOrSelfPlayer()->SetClientControl(this, true); + } +} + +bool Unit::SetCharmedBy(Unit* charmer, CharmType type, AuraApplication const* aurApp) +{ + if (!charmer) + return false; + + // dismount players when charmed + if (GetTypeId() == TYPEID_PLAYER) + RemoveAurasByType(SPELL_AURA_MOUNTED); + + if (charmer->GetTypeId() == TYPEID_PLAYER) + charmer->RemoveAurasByType(SPELL_AURA_MOUNTED); + + ASSERT(type != CHARM_TYPE_POSSESS || charmer->GetTypeId() == TYPEID_PLAYER); + ASSERT((type == CHARM_TYPE_VEHICLE) == (GetVehicleKit() && GetVehicleKit()->IsControllableVehicle())); + + TC_LOG_DEBUG("entities.unit", "SetCharmedBy: charmer %u (GUID %u), charmed %u (GUID %u), type %u.", charmer->GetEntry(), charmer->GetGUID().GetCounter(), GetEntry(), GetGUID().GetCounter(), uint32(type)); + + if (this == charmer) + { + TC_LOG_FATAL("entities.unit", "Unit::SetCharmedBy: Unit %u (GUID %u) is trying to charm itself!", GetEntry(), GetGUID().GetCounter()); + return false; + } + + //if (HasUnitState(UNIT_STATE_UNATTACKABLE)) + // return false; + + if (GetTypeId() == TYPEID_PLAYER && ToPlayer()->GetTransport()) + { + TC_LOG_FATAL("entities.unit", "Unit::SetCharmedBy: Player on transport is trying to charm %u (GUID %u)", GetEntry(), GetGUID().GetCounter()); + return false; + } + + // Already charmed + if (GetCharmerGUID()) + { + TC_LOG_FATAL("entities.unit", "Unit::SetCharmedBy: %u (GUID %u) has already been charmed but %u (GUID %u) is trying to charm it!", GetEntry(), GetGUID().GetCounter(), charmer->GetEntry(), charmer->GetGUID().GetCounter()); + return false; + } + + CastStop(); + CombatStop(); /// @todo CombatStop(true) may cause crash (interrupt spells) + + Player* playerCharmer = charmer->ToPlayer(); + + // Charmer stop charming + if (playerCharmer) + { + playerCharmer->StopCastingCharm(); + playerCharmer->StopCastingBindSight(); + } + + // Charmed stop charming + if (GetTypeId() == TYPEID_PLAYER) + { + ToPlayer()->StopCastingCharm(); + ToPlayer()->StopCastingBindSight(); + } + + // StopCastingCharm may remove a possessed pet? + if (!IsInWorld()) + { + TC_LOG_FATAL("entities.unit", "Unit::SetCharmedBy: %u (GUID %u) is not in world but %u (GUID %u) is trying to charm it!", GetEntry(), GetGUID().GetCounter(), charmer->GetEntry(), charmer->GetGUID().GetCounter()); + return false; + } + + // charm is set by aura, and aura effect remove handler was called during apply handler execution + // prevent undefined behaviour + if (aurApp && aurApp->GetRemoveMode().HasAnyFlag()) + { + // properly clean up charm changes up to this point to avoid leaving the unit in partially charmed state + SetFaction(_oldFactionId); + GetMotionMaster()->InitDefault(); + charmer->SetCharm(this, false); + return false; + } + + _oldFactionId = GetFaction(); + SetFaction(charmer->GetFaction()); + + // Set charmed + charmer->SetCharm(this, true); + + if (GetTypeId() == TYPEID_UNIT) + { + GetMotionMaster()->Clear(MOTION_SLOT_ACTIVE); + PauseMovement(0, MOTION_SLOT_IDLE); + StopMoving(); + } + else if (Player* player = ToPlayer()) + { + if (player->isAFK()) + player->ToggleAFK(); + + player->SetClientControl(this, false); + } + + // charm is set by aura, and aura effect remove handler was called during apply handler execution + // prevent undefined behaviour + if (aurApp && aurApp->GetRemoveMode().HasAnyFlag()) + return false; + + // Pets already have a properly initialized CharmInfo, don't overwrite it. + if (type != CHARM_TYPE_VEHICLE && !GetCharmInfo()) + { + if (CharmInfo* charmInfo = InitCharmInfo()) + { + if (type == CHARM_TYPE_POSSESS) + charmInfo->InitPossessCreateSpells(); + else + charmInfo->InitCharmCreateSpells(); + } + } + + if (playerCharmer) + { + switch (type) + { + case CHARM_TYPE_VEHICLE: + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED); + playerCharmer->SetClientControl(this, true); + playerCharmer->VehicleSpellInitialize(); + break; + case CHARM_TYPE_POSSESS: + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED); + charmer->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_REMOVE_CLIENT_CONTROL); + playerCharmer->SetClientControl(this, true); + playerCharmer->PossessSpellInitialize(); + AddUnitState(UNIT_STATE_POSSESSED); + break; + case CHARM_TYPE_CHARM: + if (GetTypeId() == TYPEID_UNIT && charmer->getClass() == CLASS_WARLOCK) + { + CreatureTemplate const* cinfo = ToCreature()->GetCreatureTemplate(); + if (cinfo && cinfo->type == CREATURE_TYPE_DEMON) + { + // to prevent client crash + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_CLASS, (uint8)CLASS_MAGE); + + // just to enable stat window + if (GetCharmInfo()) + GetCharmInfo()->SetPetNumber(sObjectMgr->GeneratePetNumber(), true); + + // if charmed two demons the same session, the 2nd gets the 1st one's name + SetUInt32Value(UNIT_FIELD_PET_NAME_TIMESTAMP, uint32(GameTime::GetGameTime())); // cast can't be helped + } + } + playerCharmer->CharmSpellInitialize(); + break; + default: + case CHARM_TYPE_CONVERT: + break; + } + } + + AddUnitState(UNIT_STATE_CHARMED); + + if ((GetTypeId() != TYPEID_PLAYER) || (charmer->GetTypeId() != TYPEID_PLAYER)) + { + // AI will schedule its own change if appropriate + if (UnitAI* ai = GetAI()) + ai->OnCharmed(false); + else + ScheduleAIChange(); + } + return true; +} + +void Unit::RemoveCharmedBy(Unit* charmer) +{ + if (!IsCharmed()) + return; + + if (charmer) + ASSERT(charmer == GetCharmer()); + else + charmer = GetCharmer(); + + ASSERT(charmer); + + CharmType type; + if (HasUnitState(UNIT_STATE_POSSESSED)) + type = CHARM_TYPE_POSSESS; + else if (charmer && charmer->IsOnVehicle(this)) + type = CHARM_TYPE_VEHICLE; + else + type = CHARM_TYPE_CHARM; + + CastStop(); + AttackStop(); + + if (_oldFactionId) + { + SetFaction(_oldFactionId); + _oldFactionId = 0; + } + else + RestoreFaction(); + + GetMotionMaster()->InitDefault(); + + // Vehicle should not attack its passenger after he exists the seat + if (type != CHARM_TYPE_VEHICLE) + LastCharmerGUID = charmer->GetGUID(); + + ASSERT(type != CHARM_TYPE_POSSESS || charmer->GetTypeId() == TYPEID_PLAYER); + ASSERT(type != CHARM_TYPE_VEHICLE || (GetTypeId() == TYPEID_UNIT && IsVehicle())); + + charmer->SetCharm(this, false); + m_combatManager.RevalidateCombat(); + + Player* playerCharmer = charmer->ToPlayer(); + + if (playerCharmer) + { + switch (type) + { + case CHARM_TYPE_VEHICLE: + playerCharmer->SetClientControl(this, false); + playerCharmer->SetClientControl(charmer, true); + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED); + break; + case CHARM_TYPE_POSSESS: + ClearUnitState(UNIT_STATE_POSSESSED); + playerCharmer->SetClientControl(this, false); + playerCharmer->SetClientControl(charmer, true); + charmer->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_REMOVE_CLIENT_CONTROL); + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED); + break; + case CHARM_TYPE_CHARM: + if (GetTypeId() == TYPEID_UNIT && charmer->getClass() == CLASS_WARLOCK) + { + CreatureTemplate const* cinfo = ToCreature()->GetCreatureTemplate(); + if (cinfo && cinfo->type == CREATURE_TYPE_DEMON) + { + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_CLASS, uint8(cinfo->unit_class)); + if (GetCharmInfo()) + GetCharmInfo()->SetPetNumber(0, true); + else + TC_LOG_ERROR("entities.unit", "Aura::HandleModCharm: %s has a charm aura but no charm info!", GetGUID().ToString().c_str()); + } + } + break; + case CHARM_TYPE_CONVERT: + break; + } + } + + if (Player* player = ToPlayer()) + player->SetClientControl(this, true); + + EngageWithTarget(charmer); + + if (playerCharmer && this != charmer->GetFirstControlled()) + playerCharmer->SendRemoveControlBar(); + + // a guardian should always have charminfo + if (!IsGuardian()) + DeleteCharmInfo(); + + // reset confused movement for example + ApplyControlStatesIfNeeded(); + + if (GetTypeId() != TYPEID_PLAYER || charmer->GetTypeId() == TYPEID_UNIT) + { + if (UnitAI* charmedAI = GetAI()) + charmedAI->OnCharmed(false); // AI will potentially schedule a charm ai update + else + ScheduleAIChange(); + } +} + +void Unit::RestoreFaction() +{ + if (HasAuraType(SPELL_AURA_MOD_FACTION)) + { + SetFaction(GetAuraEffectsByType(SPELL_AURA_MOD_FACTION).front()->GetMiscValue()); + return; + } + + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->SetFactionForRace(getRace()); + else + { + if (HasUnitTypeMask(UNIT_MASK_MINION)) + { + if (Unit* owner = GetOwner()) + { + SetFaction(owner->GetFaction()); + return; + } + } + + if (CreatureTemplate const* cinfo = ToCreature()->GetCreatureTemplate()) // normal creature + SetFaction(cinfo->faction); + } +} + +bool Unit::CreateVehicleKit(uint32 id, uint32 creatureEntry, bool loading /*= false*/) +{ + VehicleEntry const* vehInfo = sVehicleStore.LookupEntry(id); + if (!vehInfo) + return false; + + m_vehicleKit = std::make_unique(this, vehInfo, creatureEntry); + m_updateFlag.Vehicle = true; + m_unitTypeMask |= UNIT_MASK_VEHICLE; + + if (!loading) + SendSetVehicleRecId(id); + + return true; +} + +void Unit::RemoveVehicleKit(bool onRemoveFromWorld /*= false*/) +{ + if (!m_vehicleKit) + return; + + if (!onRemoveFromWorld) + SendSetVehicleRecId(0); + + m_vehicleKit->Uninstall(); + m_vehicleKit.reset(); + + m_updateFlag.Vehicle = false; + m_unitTypeMask &= ~UNIT_MASK_VEHICLE; + RemoveFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_SPELLCLICK | UNIT_NPC_FLAG_PLAYER_VEHICLE); +} + +bool Unit::IsOnVehicle(Unit const* vehicle) const +{ + return m_vehicle && m_vehicle == vehicle->GetVehicleKit(); +} + +Unit* Unit::GetVehicleBase() const +{ + return m_vehicle ? m_vehicle->GetBase() : nullptr; +} + +Creature* Unit::GetVehicleCreatureBase() const +{ + if (Unit* veh = GetVehicleBase()) + if (Creature* c = veh->ToCreature()) + return c; + + return nullptr; +} + +ObjectGuid Unit::GetTransGUID() const +{ + if (GetVehicle()) + return GetVehicleBase()->GetGUID(); + if (GetTransport()) + return GetTransport()->GetTransportGUID(); + + return ObjectGuid::Empty; +} + +TransportBase* Unit::GetDirectTransport() const +{ + if (Vehicle* veh = GetVehicle()) + return veh; + 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& units) + { + if (!source || !groupOwner || !source->IsInWorld()) + return; + + float const range = source->GetVisibilityRange(); + std::list nearby; + VianoGroupBotInRangeCheck check(source, groupOwner, range); + Trinity::UnitListSearcher 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 units; + const_cast(this)->GetPartyMembers(units); + + uint32 count = 0; + std::vector 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) + return true; + + Unit const* u1 = GetCharmerOrOwnerOrCreatorOrSelf(); + Unit const* u2 = unit->GetCharmerOrOwnerOrCreatorOrSelf(); + if (u1 == u2) + return true; + + if (u1->GetTypeId() == TYPEID_PLAYER && u2->GetTypeId() == TYPEID_PLAYER) + return u1->ToPlayer()->IsInSameGroupWith(u2->ToPlayer()); + else if ((u2->GetTypeId() == TYPEID_PLAYER && u1->GetTypeId() == TYPEID_UNIT && u1->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_TREAT_AS_RAID_UNIT_FOR_HELPFUL_SPELLS)) || + (u1->GetTypeId() == TYPEID_PLAYER && u2->GetTypeId() == TYPEID_UNIT && u2->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_TREAT_AS_RAID_UNIT_FOR_HELPFUL_SPELLS))) + return true; + + return u1->GetTypeId() == TYPEID_UNIT && u2->GetTypeId() == TYPEID_UNIT && u1->GetFaction() == u2->GetFaction(); +} + +bool Unit::IsInRaidWith(Unit const* unit) const +{ + if (this == unit) + return true; + + Unit const* u1 = GetCharmerOrOwnerOrCreatorOrSelf(); + Unit const* u2 = unit->GetCharmerOrOwnerOrCreatorOrSelf(); + if (u1 == u2) + return true; + + if (u1->GetTypeId() == TYPEID_PLAYER && u2->GetTypeId() == TYPEID_PLAYER) + return u1->ToPlayer()->IsInSameRaidWith(u2->ToPlayer()); + else if ((u2->GetTypeId() == TYPEID_PLAYER && u1->GetTypeId() == TYPEID_UNIT && u1->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_TREAT_AS_RAID_UNIT_FOR_HELPFUL_SPELLS)) || + (u1->GetTypeId() == TYPEID_PLAYER && u2->GetTypeId() == TYPEID_UNIT && u2->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_TREAT_AS_RAID_UNIT_FOR_HELPFUL_SPELLS))) + return true; + + return u1->GetTypeId() == TYPEID_UNIT && u2->GetTypeId() == TYPEID_UNIT && u1->GetFaction() == u2->GetFaction(); +} + +void Unit::GetPartyMembers(std::list &TagUnitMap) +{ + Unit* owner = GetCharmerOrOwnerOrSelf(); + Group* group = nullptr; + Player* groupOwner = nullptr; + + if (owner->GetTypeId() == TYPEID_PLAYER) + { + groupOwner = owner->ToPlayer(); + group = groupOwner->GetGroup(); + } + + if (group) + { + uint8 subgroup = groupOwner->GetSubGroup(); + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* Target = itr->GetSource(); + + if (Target && Target->IsInMap(owner) && Target->GetSubGroup() == subgroup && !IsHostileTo(Target)) + { + if (Target->IsAlive()) + TagUnitMap.push_back(Target); + + if (Guardian* pet = Target->GetGuardianPet()) + if (pet->IsAlive()) + TagUnitMap.push_back(pet); + } + } + } + else + { + if ((owner == this || IsInMap(owner)) && owner->IsAlive()) + TagUnitMap.push_back(owner); + if (Guardian* pet = owner->GetGuardianPet()) + if ((pet == this || IsInMap(pet)) && pet->IsAlive()) + TagUnitMap.push_back(pet); + } + + if (groupOwner) + AppendNearbyVianoGroupBots(this, groupOwner, TagUnitMap); +} + +bool Unit::IsContestedGuard() const +{ + if (FactionTemplateEntry const* entry = GetFactionTemplateEntry()) + return entry->IsHostileToPvpActivePlayers(); + + return false; +} + +void Unit::SetPvP(bool state) +{ + if (state) + SetByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_PVP); + else + RemoveByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_PVP); +} + +Aura* Unit::AddAura(uint32 spellId, Unit* target) +{ + if (!target) + return nullptr; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + return nullptr; + + return AddAura(spellInfo, MAX_EFFECT_MASK, target); +} + +Aura* Unit::AddAura(SpellInfo const* spellInfo, uint8 effMask, Unit* target) +{ + if (!spellInfo) + return nullptr; + + if (!target->IsAlive() && !spellInfo->IsPassive() && !spellInfo->HasAttribute(SPELL_ATTR2_ALLOW_DEAD_TARGET)) + return nullptr; + + if (target->IsImmunedToSpell(spellInfo, this)) + return nullptr; + + for (uint32 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!(effMask & (1 << i))) + continue; + + if (target->IsImmunedToSpellEffect(spellInfo, i, this)) + effMask &= ~(1 << i); + } + + if (!effMask) + return nullptr; + + AuraCreateInfo createInfo(spellInfo, effMask, target); + createInfo.SetCaster(this); + + if (Aura* aura = Aura::TryRefreshStackOrCreate(createInfo)) + { + aura->ApplyForTargets(); + return aura; + } + return nullptr; +} + +void Unit::SetAuraStack(uint32 spellId, Unit* target, uint32 stack) +{ + Aura* aura = target->GetAura(spellId, GetGUID()); + if (!aura) + aura = AddAura(spellId, target); + if (aura && stack) + aura->SetStackAmount(stack); +} + +void Unit::SendPlaySpellVisualKit(uint32 id, uint32 type, uint32 duration) const +{ + if (!sSpellVisualKitStore.LookupEntry(id)) + return; + + WorldPackets::Spells::PlaySpellVisualKit packet; + packet.Unit = GetGUID(); + packet.KitRecID = id; // SpellVisualKit.dbc index + packet.KitType = type; + packet.Duration = duration; + SendMessageToSet(packet.Write(), IsPlayer()); +} + +void Unit::SendPlaySpellVisual(uint32 spellVisualId, Unit const* target /*= nullptr*/, Optional targetPosition /*= {}*/, float travelSpeed /*= 0.f*/, uint16 missReason /*= 0*/, uint16 reflectStatus /*= 0*/, bool speedAsTime /*= false*/) const +{ + if (!sSpellVisualStore.LookupEntry(spellVisualId)) + return; + + WorldPackets::Spells::PlaySpellVisual packet; + packet.SpellVisualID = spellVisualId; + packet.Source = GetGUID(); + if (target) + packet.Target = target->GetGUID(); + + if (targetPosition) + packet.TargetPosition = *targetPosition; + + packet.TravelSpeed = travelSpeed; + packet.MissReason = missReason; + packet.ReflectStatus = reflectStatus; + packet.SpeedAsTime = speedAsTime; + + SendMessageToSet(packet.Write(), true); +} + +void Unit::CancelSpellMissiles(uint32 spellId, bool reverseMissile /*= false*/) +{ + bool hasMissile = false; + for (auto& itr : m_Events.GetEvents()) + { + if (Spell const* spell = Spell::ExtractSpellFromEvent(itr.second)) + { + if (spell->GetSpellInfo()->Id == spellId) + { + if (!itr.second->IsAbortScheduled()) + { + itr.second->ScheduleAbort(); + hasMissile = true; + } + } + } + } + + if (hasMissile) + { + WorldPackets::Spells::MissileCancel packet; + packet.OwnerGUID = GetGUID(); + packet.SpellID = spellId; + packet.Reverse = reverseMissile; + SendMessageToSet(packet.Write(), false); + } +} + +bool Unit::CanApplyResilience() const +{ + return !IsVehicle() && GetOwnerGUID().IsPlayer(); +} + +/*static*/ void Unit::ApplyResilience(Unit const* victim, int32* damage) +{ + // player mounted on multi-passenger mount is also classified as vehicle + if (victim->IsVehicle() && victim->GetTypeId() != TYPEID_PLAYER) + return; + + Unit const* target = nullptr; + if (victim->GetTypeId() == TYPEID_PLAYER) + target = victim; + else // victim->GetTypeId() == TYPEID_UNIT + { + if (Unit* owner = victim->GetOwner()) + if (owner->GetTypeId() == TYPEID_PLAYER) + target = owner; + } + + if (!target) + return; + + *damage -= target->GetDamageReduction(*damage); +} + +int32 Unit::CalculateAOEAvoidance(int32 damage, uint32 schoolMask, bool npcCaster) const +{ + damage = int32(float(damage) * GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_AOE_DAMAGE_AVOIDANCE, schoolMask)); + if (npcCaster) + damage = int32(float(damage) * GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_CREATURE_AOE_DAMAGE_AVOIDANCE, schoolMask)); + + return damage; +} + +// Melee based spells can be miss, parry or dodge on this step +// Crit or block - determined on damage calculation phase! (and can be both in some time) +float Unit::MeleeSpellMissChance(Unit const* victim, WeaponAttackType attType, SpellInfo const* spellInfo /*= nullptr*/) const +{ + if (spellInfo && spellInfo->HasAttribute(SPELL_ATTR7_NO_ATTACK_MISS)) + return 0.f; + + // Calculate miss chance + float missChance = GetMeleeMissChance(victim); + + if (!spellInfo && haveOffhandWeapon()) + missChance += 19.f; + + // Spellmod from SpellModOp::HitChance + float resistMissChance = 100.0f; + if (spellInfo) + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellInfo, SpellModOp::HitChance, resistMissChance); + + missChance -= resistMissChance - 100.0f; + + if (attType == RANGED_ATTACK) + missChance -= m_modRangedHitChance; + else + missChance -= m_modMeleeHitChance; + + // miss chance from auras after calculating skill based miss + missChance -= GetTotalAuraModifier(SPELL_AURA_MOD_HIT_CHANCE); + if (attType == RANGED_ATTACK) + missChance -= victim->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_RANGED_HIT_CHANCE); + else + missChance -= victim->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_MELEE_HIT_CHANCE); + + return std::max(missChance, 0.f); +} + +void Unit::OnPhaseChange() +{ + if (!IsInWorld()) + return; + + // Inform player groups that a member's phase has changed so the next PartyMemberState packet will update the phasing accordingly + if (IsPlayer() && ToPlayer()->GetGroup()) + ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PHASE); +} + +void Unit::UpdateObjectVisibility(bool forced) +{ + if (!forced) + AddToNotify(NOTIFY_VISIBILITY_CHANGED); + else + { + WorldObject::UpdateObjectVisibility(true); + // call MoveInLineOfSight for nearby creatures + Trinity::AIRelocationNotifier notifier(*this); + Cell::VisitAllObjects(this, notifier, GetVisibilityRange()); + } +} + +void Unit::SendMoveKnockBack(Player* player, float speedXY, float speedZ, float vcos, float vsin) +{ + WorldPackets::Movement::MoveKnockBack moveKnockBack; + moveKnockBack.MoverGUID = GetGUID(); + moveKnockBack.SequenceIndex = m_movementCounter++; + moveKnockBack.Speeds.HorzSpeed = speedXY; + moveKnockBack.Speeds.VertSpeed = speedZ; + moveKnockBack.Direction = Position(vcos, vsin); + player->SendDirectMessage(moveKnockBack.Write()); +} + +void Unit::KnockbackFrom(float x, float y, float speedXY, float speedZ) +{ + if (IsMovedByServer()) + GetMotionMaster()->MoveKnockbackFrom(x, y, speedXY, speedZ); + else + { + float vcos, vsin; + GetSinCos(x, y, vsin, vcos); + SendMoveKnockBack(GetGameClientMovingMe()->GetBasePlayer(), speedXY, -speedZ, vcos, vsin); + + if (HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) || HasAuraType(SPELL_AURA_FLY)) + SetCanFly(true); + } +} + +float Unit::GetCombatRatingReduction(CombatRating cr) const +{ + if (Player const* player = ToPlayer()) + return player->GetRatingBonusValue(cr); + // Player's pet get resilience from owner + else if (IsPet() && GetOwner()) + if (Player* owner = GetOwner()->ToPlayer()) + return owner->GetRatingBonusValue(cr); + + return 0.0f; +} + +uint32 Unit::GetCombatRatingDamageReduction(CombatRating cr, float rate, float cap, uint32 damage) const +{ + float percent = std::min(GetCombatRatingReduction(cr) * rate, cap); + return CalculatePct(damage, percent); +} + +uint32 Unit::GetModelForForm(ShapeshiftForm form, uint32 spellId) const +{ + // Hardcoded cases + switch (spellId) + { + case 7090: // Bear Form + return 29414; + case 35200: // Roc Form + return 4877; + default: + break; + } + + if (GetTypeId() == TYPEID_PLAYER) + { + switch (form) + { + case FORM_CAT: + // Based on Hair color + if (getRace() == RACE_NIGHTELF) + { + uint8 hairColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + switch (hairColor) + { + case 7: // Violet + case 8: + return 29405; + case 3: // Light Blue + return 29406; + case 0: // Green + case 1: // Light Green + case 2: // Dark Green + return 29407; + case 4: // White + return 29408; + default: // original - Dark Blue + return 892; + } + } + else if (getRace() == RACE_TROLL) + { + uint8 hairColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + switch (hairColor) + { + case 0: // Red + case 1: + return 33668; + case 2: // Yellow + case 3: + return 33667; + case 4: // Blue + case 5: + case 6: + return 33666; + case 7: // Purple + case 10: + return 33665; + default: // original - white + return 33669; + } + } + else if (getRace() == RACE_WORGEN) + { + // Based on Skin color + uint8 skinColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + // Male + if (getGender() == GENDER_MALE) + { + switch (skinColor) + { + case 1: // Brown + return 33662; + case 2: // Black + case 7: + return 33661; + case 4: // yellow + return 33664; + case 3: // White + case 5: + return 33663; + default: // original - Gray + return 33660; + } + } + // Female + else + { + switch (skinColor) + { + case 5: // Brown + case 6: + return 33662; + case 7: // Black + case 8: + return 33661; + case 3: // yellow + case 4: + return 33664; + case 2: // White + return 33663; + default: // original - Gray + return 33660; + } + } + } + // Based on Skin color + else if (getRace() == RACE_TAUREN) + { + uint8 skinColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + // Male + if (getGender() == GENDER_MALE) + { + switch (skinColor) + { + case 12: // White + case 13: + case 14: + case 18: // Completly White + return 29409; + case 9: // Light Brown + case 10: + case 11: + return 29410; + case 6: // Brown + case 7: + case 8: + return 29411; + case 0: // Dark + case 1: + case 2: + case 3: // Dark Grey + case 4: + case 5: + return 29412; + default: // original - Grey + return 8571; + } + } + // Female + else + { + switch (skinColor) + { + case 10: // White + return 29409; + case 6: // Light Brown + case 7: + return 29410; + case 4: // Brown + case 5: + return 29411; + case 0: // Dark + case 1: + case 2: + case 3: + return 29412; + default: // original - Grey + return 8571; + } + } + } + else if (Player::TeamForRace(getRace()) == ALLIANCE) + return 892; + else + return 8571; + case FORM_BEAR: + // Based on Hair color + if (getRace() == RACE_NIGHTELF) + { + uint8 hairColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + switch (hairColor) + { + case 0: // Green + case 1: // Light Green + case 2: // Dark Green + return 29413; // 29415? + case 6: // Dark Blue + return 29414; + case 4: // White + return 29416; + case 3: // Light Blue + return 29417; + default: // original - Violet + return 2281; + } + } + else if (getRace() == RACE_TROLL) + { + uint8 hairColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + switch (hairColor) + { + case 0: // Red + case 1: + return 33657; + case 2: // Yellow + case 3: + return 33659; + case 7: // Purple + case 10: + return 33656; + case 8: // White + case 9: + case 11: + case 12: + return 33658; + default: // original - Blue + return 33655; + } + } + else if (getRace() == RACE_WORGEN) + { + // Based on Skin color + uint8 skinColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + // Male + if (getGender() == GENDER_MALE) + { + switch (skinColor) + { + case 1: // Brown + return 33653; + case 2: // Black + case 7: + return 33651; + case 4: // Yellow + return 33652; + case 3: // White + case 5: + return 33654; + default: // original - Gray + return 33650; + } + } + // Female + else + { + switch (skinColor) + { + case 5: // Brown + case 6: + return 33652; + case 7: // Black + case 8: + return 33651; + case 3: // yellow + case 4: + return 33654; + case 2: // White + return 33653; + default: // original - Gray + return 33650; + } + } + } + // Based on Skin color + else if (getRace() == RACE_TAUREN) + { + uint8 skinColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + // Male + if (getGender() == GENDER_MALE) + { + switch (skinColor) + { + case 0: // Dark (Black) + case 1: + case 2: + return 29418; + case 3: // White + case 4: + case 5: + case 12: + case 13: + case 14: + return 29419; + case 9: // Light Brown/Grey + case 10: + case 11: + case 15: + case 16: + case 17: + return 29420; + case 18: // Completly White + return 29421; + default: // original - Brown + return 2289; + } + } + // Female + else + { + switch (skinColor) + { + case 0: // Dark (Black) + case 1: + return 29418; + case 2: // White + case 3: + return 29419; + case 6: // Light Brown/Grey + case 7: + case 8: + case 9: + return 29420; + case 10: // Completly White + return 29421; + default: // original - Brown + return 2289; + } + } + } + else if (Player::TeamForRace(getRace()) == ALLIANCE) + return 2281; + else + return 2289; + case FORM_FLIGHT: + if (Player::TeamForRace(getRace()) == ALLIANCE) + return 20857; + return 20872; + case FORM_FLIGHT_EPIC: + if (Player::TeamForRace(getRace()) == ALLIANCE) + return (getRace() == RACE_WORGEN ? 37729 : 21243); + if (getRace() == RACE_TROLL) + return 37730; + return 21244; + case FORM_MOONKIN: + if (getRace() == RACE_TROLL) + return 37174; + if (getRace() == RACE_WORGEN) + return 37173; + return 15374; + case FORM_GHOSTWOLF: + if (HasAura(58135)) //! Glyph of Arctic Wolf + return 27312; + return 4613; + default: + break; + } + } + + uint32 modelid = 0; + SpellShapeshiftFormEntry const* formEntry = sSpellShapeshiftFormStore.LookupEntry(form); + if (formEntry && formEntry->CreatureDisplayID[0]) + { + // Take the alliance modelid as default + if (GetTypeId() != TYPEID_PLAYER) + return formEntry->CreatureDisplayID[0]; + else + { + if (Player::TeamForRace(getRace()) == ALLIANCE) + modelid = formEntry->CreatureDisplayID[0]; + else + modelid = formEntry->CreatureDisplayID[1]; + + // If the player is horde but there are no values for the horde modelid - take the alliance modelid + if (!modelid && Player::TeamForRace(getRace()) == HORDE) + modelid = formEntry->CreatureDisplayID[0]; + } + } + + return modelid; +} + +uint32 Unit::GetModelForTotem(PlayerTotemType totemType) +{ + switch (getRace()) + { + case RACE_ORC: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 30758; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 30757; + case SUMMON_TYPE_TOTEM_WATER: // water + return 30759; + case SUMMON_TYPE_TOTEM_AIR: // air + return 30756; + } + break; + } + case RACE_DWARF: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 30754; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 30753; + case SUMMON_TYPE_TOTEM_WATER: // water + return 30755; + case SUMMON_TYPE_TOTEM_AIR: // air + return 30736; + } + break; + } + case RACE_TROLL: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 30762; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 30761; + case SUMMON_TYPE_TOTEM_WATER: // water + return 30763; + case SUMMON_TYPE_TOTEM_AIR: // air + return 30760; + } + break; + } + case RACE_TAUREN: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 4589; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 4588; + case SUMMON_TYPE_TOTEM_WATER: // water + return 4587; + case SUMMON_TYPE_TOTEM_AIR: // air + return 4590; + } + break; + } + case RACE_DRAENEI: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 19074; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 19073; + case SUMMON_TYPE_TOTEM_WATER: // water + return 19075; + case SUMMON_TYPE_TOTEM_AIR: // air + return 19071; + } + break; + } + case RACE_GOBLIN: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 30783; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 30782; + case SUMMON_TYPE_TOTEM_WATER: // water + return 30784; + case SUMMON_TYPE_TOTEM_AIR: // air + return 30781; + } + break; + } + } + return 0; +} + +void Unit::JumpTo(float speedXY, float speedZ, bool forward, Optional dest) +{ + float angle = forward ? 0 : float(M_PI); + if (dest) + angle += GetRelativeAngle(*dest); + + if (GetTypeId() == TYPEID_UNIT) + GetMotionMaster()->MoveJumpTo(angle, speedXY, speedZ); + else + { + float vcos = std::cos(angle + GetOrientation()); + float vsin = std::sin(angle + GetOrientation()); + SendMoveKnockBack(ToPlayer(), speedXY, -speedZ, vcos, vsin); + } +} + +void Unit::JumpTo(WorldObject* obj, float speedZ, bool withOrientation) +{ + float x, y, z; + obj->GetContactPoint(this, x, y, z); + float speedXY = GetExactDist2d(x, y) * 10.0f / speedZ; + GetMotionMaster()->MoveJump(x, y, z, GetAngle(obj), speedXY, speedZ, EVENT_JUMP, withOrientation); +} + +bool Unit::HandleSpellClick(Unit* clicker, int8 seatId) +{ + bool result = false; + uint32 spellClickEntry = GetVehicleKit() ? GetVehicleKit()->GetCreatureEntry() : GetEntry(); + TriggerCastFlags const flags = GetVehicleKit() ? TRIGGERED_IGNORE_CASTER_MOUNTED_OR_ON_VEHICLE : TRIGGERED_NONE; + + SpellClickInfoMapBounds clickPair = sObjectMgr->GetSpellClickInfoMapBounds(spellClickEntry); + for (SpellClickInfoContainer::const_iterator itr = clickPair.first; itr != clickPair.second; ++itr) + { + //! First check simple relations from clicker to clickee + if (!itr->second.IsFitToRequirements(clicker, this)) + continue; + + //! Check database conditions + if (!sConditionMgr->IsObjectMeetingSpellClickConditions(spellClickEntry, itr->second.spellId, clicker, this)) + continue; + + Unit* caster = (itr->second.castFlags & NPC_CLICK_CAST_CASTER_CLICKER) ? clicker : this; + Unit* target = (itr->second.castFlags & NPC_CLICK_CAST_TARGET_CLICKER) ? clicker : this; + ObjectGuid origCasterGUID = (itr->second.castFlags & NPC_CLICK_CAST_ORIG_CASTER_OWNER) ? GetOwnerOrCreatorGUID() : clicker->GetGUID(); + + SpellInfo const* spellEntry = sSpellMgr->GetSpellInfo(itr->second.spellId); + // if (!spellEntry) should be checked at npc_spellclick load + + if (seatId > -1) + { + uint8 i = 0; + bool valid = false; + while (i < MAX_SPELL_EFFECTS && !valid) + { + if (spellEntry->Effects[i].ApplyAuraName == SPELL_AURA_CONTROL_VEHICLE) + { + valid = true; + break; + } + ++i; + } + + if (!valid) + { + TC_LOG_ERROR("sql.sql", "Spell %u specified in npc_spellclick_spells is not a valid vehicle enter aura!", itr->second.spellId); + continue; + } + + if (IsInMap(caster)) + { + CastSpellExtraArgs args(flags); + args.OriginalCaster = origCasterGUID; + args.AddSpellMod(SpellValueMod(SPELLVALUE_BASE_POINT0 + i), seatId + 1); + caster->CastSpell(target, itr->second.spellId, args); + } + else // This can happen during Player::_LoadAuras + { + int32 bp[MAX_SPELL_EFFECTS] = { }; + for (uint32 j = 0; j < MAX_SPELL_EFFECTS; ++j) + bp[j] = spellEntry->Effects[j].BasePoints; + + bp[i] = seatId; + + AuraCreateInfo createInfo(spellEntry, MAX_EFFECT_MASK, this); + createInfo + .SetCaster(clicker) + .SetBaseAmount(bp) + .SetCasterGUID(origCasterGUID); + + Aura::TryRefreshStackOrCreate(createInfo); + } + } + else + { + if (IsInMap(caster)) + caster->CastSpell(target, spellEntry->Id, CastSpellExtraArgs().SetOriginalCaster(origCasterGUID)); + else + { + AuraCreateInfo createInfo(spellEntry, MAX_EFFECT_MASK, this); + createInfo + .SetCaster(clicker) + .SetCasterGUID(origCasterGUID); + + Aura::TryRefreshStackOrCreate(createInfo); + } + } + + result = true; + } + + Creature* creature = ToCreature(); + if (creature && creature->IsAIEnabled()) + creature->AI()->OnSpellClick(clicker, result); + + return result; +} + +void Unit::EnterVehicle(Unit* base, int8 seatId) +{ + CastSpellExtraArgs args(TRIGGERED_IGNORE_CASTER_MOUNTED_OR_ON_VEHICLE); + args.AddSpellBP0(seatId + 1); + CastSpell(base, VEHICLE_SPELL_RIDE_HARDCODED, args); +} + +void Unit::_EnterVehicle(Vehicle* vehicle, int8 seatId, AuraApplication const* aurApp) +{ + // Must be called only from aura handler + ASSERT(aurApp); + + if (!IsAlive() || GetVehicleKit() == vehicle || vehicle->GetBase()->IsOnVehicle(this)) + return; + + if (m_vehicle) + { + if (m_vehicle != vehicle) + { + TC_LOG_DEBUG("entities.vehicle", "EnterVehicle: %u exit %u and enter %u.", GetEntry(), m_vehicle->GetBase()->GetEntry(), vehicle->GetBase()->GetEntry()); + ExitVehicle(); + } + else if (seatId >= 0 && seatId == GetTransSeat()) + return; + else + { + //Exit the current vehicle because unit will reenter in a new seat. + m_vehicle->GetBase()->RemoveAurasByType(SPELL_AURA_CONTROL_VEHICLE, GetGUID(), aurApp->GetBase()); + } + } + + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + if (Player* player = ToPlayer()) + { + if (vehicle->GetBase()->GetTypeId() == TYPEID_PLAYER && player->IsInCombat()) + { + vehicle->GetBase()->RemoveAura(const_cast(aurApp)); + return; + } + + if (vehicle->GetBase()->GetTypeId() == TYPEID_UNIT) + { + // If a player entered a vehicle that is part of a formation, remove it from said formation + if (CreatureGroup* creatureGroup = vehicle->GetBase()->ToCreature()->GetFormation()) + creatureGroup->RemoveMember(vehicle->GetBase()->ToCreature()); + } + } + + // If vehicle flag for fixed position set (cannons), or if the following hardcoded units, then set state rooted + // 30236 | Argent Cannon + // 39759 | Tankbuster Cannon + if ((vehicle->GetVehicleInfo()->Flags & VEHICLE_FLAG_FIXED_POSITION) || vehicle->GetBase()->GetEntry() == 30236 || vehicle->GetBase()->GetEntry() == 39759) + SetControlled(true, UNIT_STATE_ROOT); + + ASSERT(!m_vehicle); + (void)vehicle->AddVehiclePassenger(this, seatId); +} + +void Unit::ChangeSeat(int8 seatId, bool next) +{ + if (!m_vehicle) + return; + + // Don't change if current and new seat are identical + if (seatId == GetTransSeat()) + return; + + SeatMap::const_iterator seat = (seatId < 0 ? m_vehicle->GetNextEmptySeat(GetTransSeat(), next) : m_vehicle->Seats.find(seatId)); + // The second part of the check will only return true if seatId >= 0. @Vehicle::GetNextEmptySeat makes sure of that. + if (seat == m_vehicle->Seats.end() || !seat->second.IsEmpty()) + return; + + AuraEffect* rideVehicleEffect = nullptr; + AuraEffectList const& vehicleAuras = m_vehicle->GetBase()->GetAuraEffectsByType(SPELL_AURA_CONTROL_VEHICLE); + for (AuraEffectList::const_iterator itr = vehicleAuras.begin(); itr != vehicleAuras.end(); ++itr) + { + if ((*itr)->GetCasterGUID() != GetGUID()) + continue; + + // Make sure there is only one ride vehicle aura on target cast by the unit changing seat + ASSERT(!rideVehicleEffect); + rideVehicleEffect = *itr; + } + + // Unit riding a vehicle must always have control vehicle aura on target + ASSERT(rideVehicleEffect); + + rideVehicleEffect->ChangeAmount(seat->first + 1); +} + +void Unit::ExitVehicle(Position const* /*exitPosition*/) +{ + //! This function can be called at upper level code to initialize an exit from the passenger's side. + if (!m_vehicle) + return; + + GetVehicleBase()->RemoveAurasByType(SPELL_AURA_CONTROL_VEHICLE, GetGUID()); + //! The following call would not even be executed successfully as the + //! SPELL_AURA_CONTROL_VEHICLE unapply handler already calls _ExitVehicle without + //! specifying an exitposition. The subsequent call below would return on if (!m_vehicle). + /*_ExitVehicle(exitPosition);*/ + //! To do: + //! We need to allow SPELL_AURA_CONTROL_VEHICLE unapply handlers in spellscripts + //! to specify exit coordinates and either store those per passenger, or we need to + //! init spline movement based on those coordinates in unapply handlers, and + //! relocate exiting passengers based on Unit::moveSpline data. Either way, + //! Coming Soon(TM) +} + +void Unit::_ExitVehicle(Position const* exitPosition) +{ + /// It's possible m_vehicle is nullptr, when this function is called indirectly from @VehicleJoinEvent::Abort. + /// In that case it was not possible to add the passenger to the vehicle. The vehicle aura has already been removed + /// from the target in the aforementioned function and we don't need to do anything else at this point. + if (!m_vehicle) + return; + + // This should be done before dismiss, because there may be some aura removal + VehicleSeatAddon const* seatAddon = m_vehicle->GetSeatAddonForSeatOfPassenger(this); + Vehicle* vehicle = m_vehicle->RemovePassenger(this); + + if (!vehicle) + { + TC_LOG_ERROR("entities.vehicle", "RemovePassenger() couldn't remove current unit from vehicle."); + return; + } + + Player* player = ToPlayer(); + + // If the player is on mounted duel and exits the mount, he should immediatly lose the duel + if (player && player->duel && player->duel->isMounted) + player->DuelComplete(DUEL_FLED); + + SetControlled(false, UNIT_STATE_ROOT); // SMSG_MOVE_FORCE_UNROOT, ~MOVEMENTFLAG_ROOT + + AddUnitState(UNIT_STATE_MOVE); + + if (player) + player->SetFallInformation(0, GetPositionZ()); + + Position pos; + // If we ask for a specific exit position, use that one. Otherwise allow scripts to modify it + if (exitPosition) + pos = *exitPosition; + else + { + // Set exit position to vehicle position and use the current orientation + pos = vehicle->GetBase()->GetPosition(); + pos.SetOrientation(GetOrientation()); + + // Change exit position based on seat entry + if (seatAddon) + { + if (seatAddon->ExitParameter == VehicleExitParameters::VehicleExitParamOffset) + pos.RelocateOffset({ seatAddon->ExitParameterX, seatAddon->ExitParameterY, seatAddon->ExitParameterZ, seatAddon->ExitParameterO }); + else if (seatAddon->ExitParameter == VehicleExitParameters::VehicleExitParamDest) + pos.Relocate({ seatAddon->ExitParameterX, seatAddon->ExitParameterY, seatAddon->ExitParameterZ, seatAddon->ExitParameterO }); + } + } + + float height = pos.GetPositionZ() + vehicle->GetBase()->GetCollisionHeight(); + + Movement::MoveSplineInit init(this); + + // Creatures without inhabit type air should begin falling after exiting the vehicle + if (GetTypeId() == TYPEID_UNIT && !CanFly() && height > GetMap()->GetWaterOrGroundLevel(GetPhaseShift(), pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ() + vehicle->GetBase()->GetCollisionHeight(), &height)) + init.SetFall(); + + init.MoveTo(pos.GetPositionX(), pos.GetPositionY(), height, false); + init.SetFacing(pos.GetOrientation()); + init.SetTransportExit(); + GetMotionMaster()->LaunchMoveSpline(std::move(init), EVENT_VEHICLE_EXIT, MOTION_SLOT_CONTROLLED); + + if (player) + player->ResummonPetTemporaryUnSummonedIfAny(); + + if (vehicle->GetBase()->HasUnitTypeMask(UNIT_MASK_MINION) && vehicle->GetBase()->GetTypeId() == TYPEID_UNIT) + if (((Minion*)vehicle->GetBase())->GetOwner() == this) + vehicle->GetBase()->ToCreature()->DespawnOrUnsummon(1); + + if (HasUnitTypeMask(UNIT_MASK_ACCESSORY)) + { + // Vehicle just died, we die too + if (vehicle->GetBase()->getDeathState() == JUST_DIED) + setDeathState(JUST_DIED); + // If for other reason we as minion are exiting the vehicle (ejected, master dismounted) - unsummon + else + ToTempSummon()->UnSummon(2000); // Approximation + } +} + +bool Unit::IsFalling() const +{ + return m_movementInfo.HasMovementFlag(MOVEMENTFLAG_FALLING | MOVEMENTFLAG_FALLING_FAR) || movespline->isFalling(); +} + +bool Unit::CanSwim() const +{ + // Mirror client behavior, if this method returns false then client will not use swimming animation and for players will apply gravity as if there was no water + if (HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_CANT_SWIM)) + return false; + if (HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) // is player + return true; + if (HasFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_AI_WILL_ONLY_SWIM_IF_TARGET_SWIMS)) + return false; + if (HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT)) + return true; + return HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_RENAME | UNIT_FLAG_CAN_SWIM); +} + +void Unit::NearTeleportTo(Position const& pos, bool casting /*= false*/) +{ + DisableSpline(); + if (GetTypeId() == TYPEID_PLAYER) + { + WorldLocation target(GetMapId(), pos); + ToPlayer()->TeleportTo(target, TELE_TO_NOT_LEAVE_TRANSPORT | TELE_TO_NOT_LEAVE_COMBAT | TELE_TO_NOT_UNSUMMON_PET | (casting ? TELE_TO_SPELL : TELE_TO_NONE)); + } + else + { + SendTeleportPacket(pos); + UpdatePosition(pos, true); + UpdateObjectVisibility(); + } +} + +void Unit::WriteMovementInfo(WorldPacket& data, Movement::ExtraMovementStatusElement* extras /*= nullptr*/, uint32* movementCounter /*= nullptr*/) +{ + MovementInfo const& mi = m_movementInfo; + + bool hasMovementFlags = GetUnitMovementFlags() != 0; + bool hasMovementFlags2 = GetExtraUnitMovementFlags() != 0; + bool hasTimestamp = true; + bool hasOrientation = !G3D::fuzzyEq(GetOrientation(), 0.0f); + bool hasTransportData = GetTransGUID() != 0; + bool hasSpline = IsSplineEnabled(); + + bool hasTransportTime2 = hasTransportData && m_movementInfo.transport.time2 != 0; + bool hasTransportVehicleId = hasTransportData && m_movementInfo.transport.vehicleId != 0; + bool hasPitch = HasUnitMovementFlag(MovementFlags(MOVEMENTFLAG_SWIMMING | MOVEMENTFLAG_FLYING)) || HasExtraUnitMovementFlag(MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING); + bool hasFallDirection = HasUnitMovementFlag(MOVEMENTFLAG_FALLING); + bool hasFallData = hasFallDirection || m_movementInfo.jump.fallTime != 0; + bool hasSplineElevation = HasUnitMovementFlag(MOVEMENTFLAG_SPLINE_ELEVATION); + + MovementStatusElements const* sequence = GetMovementStatusElementsSequence(static_cast(data.GetOpcode())); + if (!sequence) + { + TC_LOG_ERROR("network", "Unit::WriteMovementInfo: No movement sequence found for opcode %s", GetOpcodeNameForLogging(static_cast(data.GetOpcode())).c_str()); + return; + } + + ObjectGuid guid = GetGUID(); + ObjectGuid tguid = hasTransportData ? GetTransGUID() : ObjectGuid::Empty; + + for (; *sequence != MSEEnd; ++sequence) + { + MovementStatusElements const& element = *sequence; + + switch (element) + { + case MSEHasGuidByte0: + case MSEHasGuidByte1: + case MSEHasGuidByte2: + case MSEHasGuidByte3: + case MSEHasGuidByte4: + case MSEHasGuidByte5: + case MSEHasGuidByte6: + case MSEHasGuidByte7: + data.WriteBit(guid[element - MSEHasGuidByte0]); + break; + case MSEHasTransportGuidByte0: + case MSEHasTransportGuidByte1: + case MSEHasTransportGuidByte2: + case MSEHasTransportGuidByte3: + case MSEHasTransportGuidByte4: + case MSEHasTransportGuidByte5: + case MSEHasTransportGuidByte6: + case MSEHasTransportGuidByte7: + if (hasTransportData) + data.WriteBit(tguid[element - MSEHasTransportGuidByte0]); + break; + case MSEGuidByte0: + case MSEGuidByte1: + case MSEGuidByte2: + case MSEGuidByte3: + case MSEGuidByte4: + case MSEGuidByte5: + case MSEGuidByte6: + case MSEGuidByte7: + data.WriteByteSeq(guid[element - MSEGuidByte0]); + break; + case MSETransportGuidByte0: + case MSETransportGuidByte1: + case MSETransportGuidByte2: + case MSETransportGuidByte3: + case MSETransportGuidByte4: + case MSETransportGuidByte5: + case MSETransportGuidByte6: + case MSETransportGuidByte7: + if (hasTransportData) + data.WriteByteSeq(tguid[element - MSETransportGuidByte0]); + break; + case MSEHasMovementFlags: + data.WriteBit(!hasMovementFlags); + break; + case MSEHasMovementFlags2: + data.WriteBit(!hasMovementFlags2); + break; + case MSEHasTimestamp: + data.WriteBit(!hasTimestamp); + break; + case MSEHasOrientation: + data.WriteBit(!hasOrientation); + break; + case MSEHasTransportData: + data.WriteBit(hasTransportData); + break; + case MSEHasTransportTime2: + if (hasTransportData) + data.WriteBit(hasTransportTime2); + break; + case MSEHasVehicleId: + if (hasTransportData) + data.WriteBit(hasTransportVehicleId); + break; + case MSEHasPitch: + data.WriteBit(!hasPitch); + break; + case MSEHasFallData: + data.WriteBit(hasFallData); + break; + case MSEHasFallDirection: + if (hasFallData) + data.WriteBit(hasFallDirection); + break; + case MSEHasSplineElevation: + data.WriteBit(!hasSplineElevation); + break; + case MSEHasSpline: + data.WriteBit(hasSpline); + break; + case MSEHasHeightChangeFailed: + data.WriteBit(mi.HasHeightChangeFailed()); + break; + case MSEMovementFlags: + if (hasMovementFlags) + data.WriteBits(GetUnitMovementFlags(), 30); + break; + case MSEMovementFlags2: + if (hasMovementFlags2) + data.WriteBits(GetExtraUnitMovementFlags(), 12); + break; + case MSETimestamp: + if (hasTimestamp) + data << mi.time; + break; + case MSEPositionX: + data << GetPositionX(); + break; + case MSEPositionY: + data << GetPositionY(); + break; + case MSEPositionZ: + data << GetPositionZ(); + break; + case MSEOrientation: + if (hasOrientation) + data << GetOrientation(); + break; + case MSETransportPositionX: + if (hasTransportData) + data << GetTransOffsetX(); + break; + case MSETransportPositionY: + if (hasTransportData) + data << GetTransOffsetY(); + break; + case MSETransportPositionZ: + if (hasTransportData) + data << GetTransOffsetZ(); + break; + case MSETransportOrientation: + if (hasTransportData) + data << GetTransOffsetO(); + break; + case MSETransportSeat: + if (hasTransportData) + data << GetTransSeat(); + break; + case MSETransportTime: + if (hasTransportData) + data << GetTransTime(); + break; + case MSETransportTime2: + if (hasTransportData && hasTransportTime2) + data << mi.transport.time2; + break; + case MSETransportVehicleId: + if (hasTransportData && hasTransportVehicleId) + data << mi.transport.vehicleId; + break; + case MSEPitch: + if (hasPitch) + data << mi.pitch; + break; + case MSEFallTime: + if (hasFallData) + data << mi.jump.fallTime; + break; + case MSEFallVerticalSpeed: + if (hasFallData) + data << mi.jump.zspeed; + break; + case MSEFallCosAngle: + if (hasFallData && hasFallDirection) + data << mi.jump.cosAngle; + break; + case MSEFallSinAngle: + if (hasFallData && hasFallDirection) + data << mi.jump.sinAngle; + break; + case MSEFallHorizontalSpeed: + if (hasFallData && hasFallDirection) + data << mi.jump.xyspeed; + break; + case MSESplineElevation: + if (hasSplineElevation) + data << mi.splineElevation; + break; + case MSECounter: + data << uint32(movementCounter ? *movementCounter : 0); + break; + case MSEZeroBit: + data.WriteBit(0); + break; + case MSEOneBit: + data.WriteBit(1); + break; + case MSEFlushBits: + data.FlushBits(); + break; + case MSEExtraElement: + extras->WriteNextElement(data); + break; + default: + ASSERT(Movement::PrintInvalidSequenceElement(element, __FUNCTION__)); + break; + } + } +} + +void Unit::SendTeleportPacket(Position const& pos) +{ + // SMSG_MOVE_UPDATE_TELEPORT is sent to nearby players to signal the teleport + // MSG_MOVE_TELEPORT is sent to self in order to trigger MSG_MOVE_TELEPORT_ACK and update the position server side + if (IsMovedByClient()) + { + Player* playerMover = GetGameClientMovingMe()->GetBasePlayer(); + float x, y, z, o; + pos.GetPosition(x, y, z, o); + if (TransportBase* transportBase = GetDirectTransport()) + transportBase->CalculatePassengerOffset(x, y, z, &o); + + WorldPackets::Movement::MoveTeleport moveTeleport; + moveTeleport.MoverGUID = GetGUID(); + moveTeleport.Pos = Position(x, y, z); + if (GetTransGUID()) + moveTeleport.TransportGUID = GetTransGUID(); + moveTeleport.Facing = o; + moveTeleport.SequenceIndex = m_movementCounter++; + playerMover->SendDirectMessage(moveTeleport.Write()); + } + else + { + // This is the only packet sent for creatures which contains MovementInfo structure + // we do not update m_movementInfo for creatures so it needs to be done manually here + WorldPackets::Movement::MoveUpdateTeleport moveUpdateTeleport; + moveUpdateTeleport.Status = &m_movementInfo; + moveUpdateTeleport.Status->guid = GetGUID(); + moveUpdateTeleport.Status->pos.Relocate(pos); + moveUpdateTeleport.Status->time = GameTime::GetGameTimeMS(); + SendMessageToSet(moveUpdateTeleport.Write(), false); + } +} + +bool Unit::UpdatePosition(float x, float y, float z, float orientation, bool teleport) +{ + // prevent crash when a bad coord is sent by the client + if (!Trinity::IsValidMapCoord(x, y, z, orientation)) + { + TC_LOG_DEBUG("entities.unit", "Unit::UpdatePosition(%f, %f, %f) .. bad coordinates!", x, y, z); + return false; + } + + // Check if angular distance changed + bool const turn = G3D::fuzzyGt(M_PI - fabs(fabs(GetOrientation() - orientation) - M_PI), 0.0f); + + // G3D::fuzzyEq won't help here, in some cases magnitudes differ by a little more than G3D::eps, but should be considered equal + bool const relocated = (teleport || + std::fabs(GetPositionX() - x) > 0.001f || + std::fabs(GetPositionY() - y) > 0.001f || + std::fabs(GetPositionZ() - z) > 0.001f); + + if (relocated) + { + // move and update visible state if need + if (GetTypeId() == TYPEID_PLAYER) + GetMap()->PlayerRelocation(ToPlayer(), x, y, z, orientation); + else + GetMap()->CreatureRelocation(ToCreature(), x, y, z, orientation); + } + else if (turn) + UpdateOrientation(orientation); + + _positionUpdateInfo.Relocated = relocated; + _positionUpdateInfo.Turned = turn; + + return (relocated || turn); +} + +bool Unit::UpdatePosition(Position const& pos, bool teleport) +{ + return UpdatePosition(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), pos.GetOrientation(), teleport); +} + +//! Only server-side orientation update, does not broadcast to client +void Unit::UpdateOrientation(float orientation) +{ + SetOrientation(orientation); + if (IsVehicle()) + GetVehicleKit()->RelocatePassengers(); +} + +//! Only server-side height update, does not broadcast to client +void Unit::UpdateHeight(float newZ) +{ + Relocate(GetPositionX(), GetPositionY(), newZ); + if (IsVehicle()) + GetVehicleKit()->RelocatePassengers(); +} + +void Unit::ClearPendingMovementChangeForType(MovementChangeType changeType) +{ + m_pendingMovementChanges.erase(changeType); +} + +void Unit::AssignPendingMovementChange(MovementChangeType changeType, PlayerMovementPendingChange&& newChange) +{ + m_pendingMovementChanges[changeType] = newChange; +} + +PlayerMovementPendingChange const* Unit::GetPendingMovementChange(MovementChangeType changeType) const +{ + return Trinity::Containers::MapGetValuePtr(m_pendingMovementChanges, changeType); +} + +void Unit::CheckPendingMovementAcks() +{ + if (sWorld->getIntConfig(CONFIG_PENDING_MOVE_CHANGES_TIMEOUT) == 0) + return; + + if (!HasPendingMovementChange()) + return; + + for (std::pair const& pendingChange : m_pendingMovementChanges) + { + if (GameTime::GetGameTimeMS() > pendingChange.second.time + sWorld->getIntConfig(CONFIG_PENDING_MOVE_CHANGES_TIMEOUT)) + { + /* + when players are teleported from one corner of a map to an other (example: from Dragonblight to the entrance of Naxxramas, both in the same map: Northend), + is it done through what is called a 'near' teleport. A near teleport always involve teleporting a player from one point to an other in the same map, even if + the distance is huge. When that distance is big enough, a loading screen appears on the client side. During that time, the client loads the surrounding zone + of the new location (and everything it contains). The problem is that, as long as the client hasn't finished loading the new zone, it will NOT ack the near + teleport. So if the server sends a near teleport order at a certain time and the client takes 20s to load the new zone (let's imagine a very slow computer), + even with zero latency, the server will receive an ack from the client only after 20s. + + For this reason and because the current implementation is simple (you dear reader, feel free to improve it if you can), we will just ignore checking for + near teleport acks (for now. @todo). + */ + if (pendingChange.second.movementChangeType == MovementChangeType::TELEPORT) + return; + + GameClient* controller = GetGameClientMovingMe(); + controller->GetWorldSession()->KickPlayer(); + TC_LOG_INFO("cheat", "Unit::CheckPendingMovementAcks: Player GUID: %s took too long to acknowledge a movement change. He was therefore kicked.", controller->GetBasePlayer()->GetGUID().ToString().c_str()); + break; + } + } +} + +void Unit::PurgeAndApplyPendingMovementChanges(bool informObservers /* = true */) +{ + for (std::pair const& pendingChange : m_pendingMovementChanges) + { + float speedFlat = pendingChange.second.newValue; + MovementChangeType changeType = pendingChange.second.movementChangeType; + Optional moveType; + switch (changeType) + { + case MovementChangeType::SPEED_CHANGE_WALK: moveType = MOVE_WALK; break; + case MovementChangeType::SPEED_CHANGE_RUN: moveType = MOVE_RUN; break; + case MovementChangeType::SPEED_CHANGE_RUN_BACK: moveType = MOVE_RUN_BACK; break; + case MovementChangeType::SPEED_CHANGE_SWIM: moveType = MOVE_SWIM; break; + case MovementChangeType::SPEED_CHANGE_SWIM_BACK: moveType = MOVE_SWIM_BACK; break; + case MovementChangeType::RATE_CHANGE_TURN: moveType = MOVE_TURN_RATE; break; + case MovementChangeType::SPEED_CHANGE_FLIGHT_SPEED: moveType = MOVE_FLIGHT; break; + case MovementChangeType::SPEED_CHANGE_FLIGHT_BACK_SPEED: moveType = MOVE_FLIGHT_BACK; break; + case MovementChangeType::RATE_CHANGE_PITCH: moveType = MOVE_PITCH_RATE; break; + case MovementChangeType::GRAVITY_DISABLE: + Unit::SetDisableGravity(pendingChange.second.apply); + break; + case MovementChangeType::SET_CAN_FLY: + Unit::SetCanFly(pendingChange.second.apply); + break; + case MovementChangeType::SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY: + Unit::SetCanTransitionBetweenSwimAndFly(pendingChange.second.apply); + break; + case MovementChangeType::SET_COLLISION_HGT: + break; + default: + ASSERT(false); + return; + } + + if (moveType.has_value()) + { + float newSpeedRate = speedFlat / (IsControlledByPlayer() ? playerBaseMoveSpeed[*moveType] : baseMoveSpeed[*moveType]); + SetSpeedRateReal(*moveType, newSpeedRate); + + if (informObservers) + MovementPacketSender::SendSpeedChangeToObservers(this, *moveType, speedFlat); + } + else if (informObservers) + { + switch (changeType) + { + case MovementChangeType::GRAVITY_DISABLE: + case MovementChangeType::SET_CAN_FLY: + case MovementChangeType::SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY: + MovementPacketSender::SendMovementFlagChangeToObservers(this); + break; + default: + break; + } + } + } + + m_pendingMovementChanges.clear(); +} + +PlayerMovementPendingChange::PlayerMovementPendingChange() +{ + time = GameTime::GetGameTimeMS(); +} + +// baseRage means damage taken when attacker = false +int32 Unit::RewardRage(uint32 baseRage, bool asAttacker) +{ + float addRage = baseRage; + + addRage *= sWorld->getRate(RATE_POWER_RAGE_INCOME); + return ModifyPower(POWER_RAGE, static_cast(addRage), !asAttacker); +} + +void Unit::StopAttackFaction(uint32 faction_id) +{ + if (Unit* victim = GetVictim()) + { + if (victim->GetFactionTemplateEntry()->Faction == faction_id) + { + AttackStop(); + if (IsNonMeleeSpellCast(false)) + InterruptNonMeleeSpells(false); + + // melee and ranged forced attack cancel + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->SendAttackSwingCancelAttack(); + } + } + + AttackerSet const& attackers = getAttackers(); + for (AttackerSet::const_iterator itr = attackers.begin(); itr != attackers.end();) + { + if ((*itr)->GetFactionTemplateEntry()->Faction == faction_id) + { + (*itr)->AttackStop(); + itr = attackers.begin(); + } + else + ++itr; + } + + std::vector refsToEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (pair.second->GetOther(this)->GetFactionTemplateEntry()->Faction == faction_id) + refsToEnd.push_back(pair.second); + for (CombatReference* ref : refsToEnd) + ref->EndCombat(); + + for (Unit* minion : m_Controlled) + minion->StopAttackFaction(faction_id); +} + +void Unit::OutDebugInfo() const +{ + TC_LOG_ERROR("entities.unit", "Unit::OutDebugInfo"); + TC_LOG_DEBUG("entities.unit", "%s name %s", GetGUID().ToString().c_str(), GetName().c_str()); + TC_LOG_DEBUG("entities.unit", "Owner %s, Minion %s, Charmer %s, Charmed %s", GetOwnerOrCreatorGUID().ToString().c_str(), GetMinionGUID().ToString().c_str(), GetCharmerGUID().ToString().c_str(), GetCharmedGUID().ToString().c_str()); + TC_LOG_DEBUG("entities.unit", "In world %u, unit type mask %u", (uint32)(IsInWorld() ? 1 : 0), m_unitTypeMask); + if (IsInWorld()) + TC_LOG_DEBUG("entities.unit", "Mapid %u", GetMapId()); + + std::ostringstream o; + o << "Summon Slot: "; + for (uint32 i = 0; i < MAX_SUMMON_SLOT; ++i) + o << m_SummonSlot[i].ToString() << ", "; + + TC_LOG_DEBUG("entities.unit", "%s", o.str().c_str()); + o.str(""); + + o << "Controlled List: "; + for (ControlList::const_iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) + o << (*itr)->GetGUID().ToString() << ", "; + TC_LOG_DEBUG("entities.unit", "%s", o.str().c_str()); + o.str(""); + + o << "Aura List: "; + for (AuraApplicationMap::const_iterator itr = m_appliedAuras.begin(); itr != m_appliedAuras.end(); ++itr) + o << itr->first << ", "; + TC_LOG_DEBUG("entities.unit", "%s", o.str().c_str()); + o.str(""); + + if (IsVehicle()) + { + o << "Passenger List: "; + for (SeatMap::iterator itr = GetVehicleKit()->Seats.begin(); itr != GetVehicleKit()->Seats.end(); ++itr) + if (Unit* passenger = ObjectAccessor::GetUnit(*GetVehicleBase(), itr->second.Passenger.Guid)) + o << passenger->GetGUID().ToString() << ", "; + TC_LOG_DEBUG("entities.unit", "%s", o.str().c_str()); + } + + if (GetVehicle()) + TC_LOG_DEBUG("entities.unit", "On vehicle %u.", GetVehicleBase()->GetEntry()); +} + +void Unit::SendClearTarget() +{ + WorldPackets::Combat::BreakTarget packet; + packet.UnitGUID = GetGUID(); + SendMessageToSet(packet.Write(), false); +} + +uint32 Unit::GetResistance(SpellSchoolMask mask) const +{ + int32 resist = -1; + for (int32 i = SPELL_SCHOOL_NORMAL; i < MAX_SPELL_SCHOOL; ++i) + if (mask & (1 << i) && (resist < 0 || resist > int32(GetResistance(SpellSchools(i))))) + resist = int32(GetResistance(SpellSchools(i))); + + // resist value will never be negative here + return uint32(resist); +} + +void CharmInfo::SetIsCommandAttack(bool val) +{ + _isCommandAttack = val; +} + +bool CharmInfo::IsCommandAttack() +{ + return _isCommandAttack; +} + +void CharmInfo::SetIsCommandFollow(bool val) +{ + _isCommandFollow = val; +} + +bool CharmInfo::IsCommandFollow() +{ + return _isCommandFollow; +} + +void CharmInfo::SaveStayPosition() +{ + //! At this point a new spline destination is enabled because of Unit::StopMoving() + G3D::Vector3 stayPos = _unit->movespline->FinalDestination(); + + if (_unit->movespline->onTransport) + if (TransportBase* transport = _unit->GetDirectTransport()) + transport->CalculatePassengerPosition(stayPos.x, stayPos.y, stayPos.z); + + _stayX = stayPos.x; + _stayY = stayPos.y; + _stayZ = stayPos.z; +} + +void CharmInfo::GetStayPosition(float &x, float &y, float &z) +{ + x = _stayX; + y = _stayY; + z = _stayZ; +} + +void CharmInfo::SetIsAtStay(bool val) +{ + _isAtStay = val; +} + +bool CharmInfo::IsAtStay() +{ + return _isAtStay; +} + +void CharmInfo::SetIsFollowing(bool val) +{ + _isFollowing = val; +} + +bool CharmInfo::IsFollowing() +{ + return _isFollowing; +} + +void CharmInfo::SetIsReturning(bool val) +{ + _isReturning = val; +} + +bool CharmInfo::IsReturning() +{ + return _isReturning; +} + +void Unit::SetOrientationTowards(WorldObject const* target) +{ + SetOrientation(GetAngle(target)); +} + +void Unit::SetFacingTo(float ori, bool force) +{ + // do not face when already moving + if (!force && (!IsStopped() || !movespline->Finalized())) + return; + + Movement::MoveSplineInit init(this); + init.MoveTo(GetPositionX(), GetPositionY(), GetPositionZ(), false); + if (GetTransport()) + init.DisableTransportPathTransformations(); // It makes no sense to target global orientation + init.SetFacing(ori); + init.Launch(); + UpdateSplineMovement(1); +} + +void Unit::SetFacingToObject(WorldObject const* object, bool force) +{ + // do not face when already moving + if (!force && (!IsStopped() || !movespline->Finalized())) + return; + + /// @todo figure out under what conditions creature will move towards object instead of facing it where it currently is. + Movement::MoveSplineInit init(this); + init.MoveTo(GetPositionX(), GetPositionY(), GetPositionZ(), false); + init.SetFacing(GetAngle(object)); // when on transport, GetAngle will still return global coordinates (and angle) that needs transforming + init.Launch(); + UpdateSplineMovement(1); +} + +void Unit::SetFacingToPoint(Position const& point, bool force) +{ + // do not face when already moving + if (!force && (!IsStopped() || !movespline->Finalized())) + return; + + /// @todo figure out under what conditions creature will move towards object instead of facing it where it currently is. + Movement::MoveSplineInit init(this); + init.MoveTo(GetPositionX(), GetPositionY(), GetPositionZ(), false); + if (GetTransport()) + init.DisableTransportPathTransformations(); // It makes no sense to target global orientation + init.SetFacing(point.GetPositionX(), point.GetPositionY(), point.GetPositionZ()); + init.Launch(); + UpdateSplineMovement(1); +} + +bool Unit::SetWalk(bool enable) +{ + if (enable == IsWalking()) + return false; + + if (enable) + AddUnitMovementFlag(MOVEMENTFLAG_WALKING); + else + RemoveUnitMovementFlag(MOVEMENTFLAG_WALKING); + + ///@ TODO: Find proper opcode for walk mode setting in player mind controlling a player case + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_WALK_MODE, SMSG_SPLINE_MOVE_SET_WALK_MODE).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_RUN_MODE, SMSG_SPLINE_MOVE_SET_RUN_MODE).Send(); + + return true; +} + +bool Unit::SetDisableGravity(bool disable, bool /*updateAnimTier = true*/) +{ + if (disable == IsGravityDisabled()) + return false; + + if (disable) + { + AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING | MOVEMENTFLAG_SPLINE_ELEVATION); + SetFall(false); + } + else + { + RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + if (!HasUnitMovementFlag(MOVEMENTFLAG_CAN_FLY)) + SetFall(true); + } + + if (IsAlive()) + { + if (IsGravityDisabled() || IsHovering()) + SetPlayHoverAnim(true); + else + SetPlayHoverAnim(false); + } + else if (IsPlayer()) // To update player who dies while flying/hovering + SetPlayHoverAnim(false, false); + + return true; +} + +bool Unit::SetFall(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_FALLING)) + return false; + + if (enable) + { + AddUnitMovementFlag(MOVEMENTFLAG_FALLING); + m_movementInfo.SetFallTime(0); + } + else + RemoveUnitMovementFlag(MOVEMENTFLAG_FALLING | MOVEMENTFLAG_FALLING_FAR); + + return true; +} + +bool Unit::SetSwim(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING)) + return false; + + if (enable) + AddUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + else + RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_START_SWIM, NULL_OPCODE).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_STOP_SWIM, NULL_OPCODE).Send(); + + return true; +} + +bool Unit::SetCanFly(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_CAN_FLY)) + return false; + + if (enable) + { + AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); + RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING | MOVEMENTFLAG_SPLINE_ELEVATION); + SetFall(false); + } + else + { + if (IsFlying() && !IsGravityDisabled()) + SetFall(true); + RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY | MOVEMENTFLAG_MASK_MOVING_FLY); + } + + return true; +} + +bool Unit::SetCanTransitionBetweenSwimAndFly(bool enable) +{ + if (enable == HasExtraUnitMovementFlag(MOVEMENTFLAG2_CAN_SWIM_TO_FLY_TRANS)) + return false; + + if (enable) + AddExtraUnitMovementFlag(MOVEMENTFLAG2_CAN_SWIM_TO_FLY_TRANS); + else + RemoveExtraUnitMovementFlag(MOVEMENTFLAG2_CAN_SWIM_TO_FLY_TRANS); + + return true; +} + +bool Unit::SetWaterWalking(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING)) + return false; + + if (enable) + AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + else + RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_WATER_WALK, SMSG_MOVE_WATER_WALK).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_LAND_WALK, SMSG_MOVE_LAND_WALK).Send(); + + return true; +} + +bool Unit::SetFeatherFall(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_FALLING_SLOW)) + return false; + + if (enable) + AddUnitMovementFlag(MOVEMENTFLAG_FALLING_SLOW); + else + RemoveUnitMovementFlag(MOVEMENTFLAG_FALLING_SLOW); + + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_FEATHER_FALL, SMSG_MOVE_FEATHER_FALL).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_NORMAL_FALL, SMSG_MOVE_NORMAL_FALL).Send(); + + return true; +} + +bool Unit::SetHover(bool enable, bool /*updateAnimTier = true*/) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_HOVER)) + return false; + + float hoverHeight = GetFloatValue(UNIT_FIELD_HOVERHEIGHT); + + if (enable) + { + //! No need to check height on ascent + AddUnitMovementFlag(MOVEMENTFLAG_HOVER); + if (hoverHeight && GetPositionZ() - GetFloorZ() < hoverHeight) + UpdateHeight(GetPositionZ() + hoverHeight); + } + else + { + RemoveUnitMovementFlag(MOVEMENTFLAG_HOVER); + //! Dying creatures will MoveFall from setDeathState + if (hoverHeight && (!isDying() || GetTypeId() != TYPEID_UNIT)) + { + float newZ = std::max(GetFloorZ(), GetPositionZ() - hoverHeight); + UpdateAllowedPositionZ(GetPositionX(), GetPositionY(), newZ); + UpdateHeight(newZ); + } + } + + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_HOVER, SMSG_MOVE_SET_HOVER).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_UNSET_HOVER, SMSG_MOVE_UNSET_HOVER).Send(); + + if (IsAlive()) + { + if (IsGravityDisabled() || IsHovering()) + SetPlayHoverAnim(true); + else + SetPlayHoverAnim(false); + } + else if (IsPlayer()) // To update player who dies while flying/hovering + SetPlayHoverAnim(false, false); + + return true; +} + +void Unit::SendSetVehicleRecId(uint32 vehicleId) +{ + WorldPackets::Vehicle::SetVehicleRecID setVehicleRec; + setVehicleRec.VehicleGUID = GetGUID(); + setVehicleRec.VehicleRecID = vehicleId; + SendMessageToSet(setVehicleRec.Write(), true); +} + +void Unit::SetPlayHoverAnim(bool enable, bool sendUpdate /*= true*/) +{ + if (IsPlayingHoverAnim() == enable) + return; + + m_updateFlag.PlayerHoverAnim = enable; + + if (!sendUpdate) + return; + + WorldPackets::Misc::SetPlayHoverAnim packet; + packet.UnitGUID = GetGUID(); + packet.PlayHoverAnim = enable; + SendMessageToSet(packet.Write(), true); +} + +bool Unit::IsSplineEnabled() const +{ + return movespline->Initialized() && !movespline->Finalized(); +} + +void Unit::BuildValuesUpdate(uint8 updateType, ByteBuffer* data, Player* target) const +{ + if (!target) + return; + + ByteBuffer fieldBuffer; + + uint32 valCount = m_valuesCount; + + uint32* flags = UnitUpdateFieldFlags; + uint32 visibleFlag = UF_FLAG_PUBLIC; + + if (target == this) + visibleFlag |= UF_FLAG_PRIVATE; + else if (GetTypeId() == TYPEID_PLAYER) + valCount = PLAYER_END_NOT_SELF; + + UpdateMaskPacketBuilder updateMask(valCount); + + Player* plr = GetCharmerOrOwnerPlayerOrPlayerItself(); + if (GetOwnerGUID() == target->GetGUID()) + visibleFlag |= UF_FLAG_OWNER; + + if (HasFlag(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_SPECIALINFO)) + if (HasAuraTypeWithCaster(SPELL_AURA_EMPATHY, target->GetGUID())) + visibleFlag |= UF_FLAG_SPECIAL_INFO; + + if (plr && plr->IsInSameRaidWith(target)) + visibleFlag |= UF_FLAG_PARTY_MEMBER; + + if (IsCreature()) + visibleFlag |= UF_FLAG_UNIT_ALL; + + Creature const* creature = ToCreature(); + for (uint16 index = 0; index < valCount; ++index) + { + if (_fieldNotifyFlags & flags[index] || + ((flags[index] & visibleFlag) & UF_FLAG_SPECIAL_INFO) || + ((updateType == UPDATETYPE_VALUES ? _changesMask.GetBit(index) : m_uint32Values[index]) && (flags[index] & visibleFlag)) || + (index == UNIT_FIELD_AURASTATE && HasFlag(UNIT_FIELD_AURASTATE, PER_CASTER_AURA_STATE_MASK))) + { + updateMask.SetBit(index); + + if (index == UNIT_NPC_FLAGS) + { + uint32 appendValue = m_uint32Values[UNIT_NPC_FLAGS]; + + if (creature) + { + if (!target->CanSeeSpellClickOn(creature)) + appendValue &= ~UNIT_NPC_FLAG_SPELLCLICK; + + if (!creature->IsClassTrainerOf(target)) + appendValue &= ~UNIT_NPC_FLAG_TRAINER_CLASS; + } + + fieldBuffer << uint32(appendValue); + } + else if (index == UNIT_FIELD_AURASTATE) + { + // Check per caster aura states to not enable using a spell in client if specified aura is not by target + fieldBuffer << BuildAuraStateUpdateForTarget(target); + } + // FIXME: Some values at server stored in float format but must be sent to client in uint32 format + else if (index >= UNIT_FIELD_BASEATTACKTIME && index <= UNIT_FIELD_RANGEDATTACKTIME) + { + // convert from float to uint32 and send + fieldBuffer << uint32(m_floatValues[index] < 0 ? 0 : m_floatValues[index]); + } + // there are some float values which may be negative or can't get negative due to other checks + else if ((index >= UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE && index <= (UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 6)) || + (index >= UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE && index <= (UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 6))) + { + fieldBuffer << uint32(m_floatValues[index]); + } + // Gamemasters should be always able to select units - remove not selectable flag + else if (index == UNIT_FIELD_FLAGS) + { + uint32 appendValue = m_uint32Values[UNIT_FIELD_FLAGS]; + if (target->IsGameMaster()) + appendValue &= ~UNIT_FLAG_NOT_SELECTABLE; + + fieldBuffer << uint32(appendValue); + } + // use modelid_a if not gm, _h if gm for CREATURE_FLAG_EXTRA_TRIGGER creatures + else if (index == UNIT_FIELD_DISPLAYID) + { + uint32 displayId = m_uint32Values[UNIT_FIELD_DISPLAYID]; + if (creature) + { + CreatureTemplate const* cinfo = creature->GetCreatureTemplate(); + + // this also applies for transform auras + if (SpellInfo const* transform = sSpellMgr->GetSpellInfo(getTransForm())) + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (transform->Effects[i].IsAura(SPELL_AURA_TRANSFORM)) + if (CreatureTemplate const* transformInfo = sObjectMgr->GetCreatureTemplate(transform->Effects[i].MiscValue)) + { + cinfo = transformInfo; + break; + } + + if (cinfo->flags_extra & CREATURE_FLAG_EXTRA_TRIGGER) + if (target->IsGameMaster()) + displayId = cinfo->GetFirstVisibleModel()->CreatureDisplayID; + } + + fieldBuffer << uint32(displayId); + } + // hide lootable animation for unallowed players + else if (index == UNIT_DYNAMIC_FLAGS) + { + uint32 dynamicFlags = m_uint32Values[UNIT_DYNAMIC_FLAGS] & ~(UNIT_DYNFLAG_TAPPED | UNIT_DYNFLAG_TAPPED_BY_PLAYER); + + if (creature) + { + if (creature->hasLootRecipient()) + { + dynamicFlags |= UNIT_DYNFLAG_TAPPED; + if (creature->isTappedBy(target)) + dynamicFlags |= UNIT_DYNFLAG_TAPPED_BY_PLAYER; + } + + if (!target->isAllowedToLoot(creature)) + dynamicFlags &= ~UNIT_DYNFLAG_LOOTABLE; + } + + // unit UNIT_DYNFLAG_TRACK_UNIT should only be sent to caster of SPELL_AURA_MOD_STALKED auras + if (dynamicFlags & UNIT_DYNFLAG_TRACK_UNIT) + if (!HasAuraTypeWithCaster(SPELL_AURA_MOD_STALKED, target->GetGUID())) + dynamicFlags &= ~UNIT_DYNFLAG_TRACK_UNIT; + + fieldBuffer << dynamicFlags; + } + // FG: pretend that OTHER players in own group are friendly ("blue") + else if (index == UNIT_FIELD_BYTES_2 || index == UNIT_FIELD_FACTIONTEMPLATE) + { + if (IsControlledByPlayer() && target != this && sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GROUP) && IsInRaidWith(target)) + { + FactionTemplateEntry const* ft1 = GetFactionTemplateEntry(); + FactionTemplateEntry const* ft2 = target->GetFactionTemplateEntry(); + if (ft1 && ft2 && !ft1->IsFriendlyTo(ft2)) + { + if (index == UNIT_FIELD_BYTES_2) + // Allow targetting opposite faction in party when enabled in config + fieldBuffer << (m_uint32Values[UNIT_FIELD_BYTES_2] & ((UNIT_BYTE2_FLAG_SANCTUARY /*| UNIT_BYTE2_FLAG_AURAS | UNIT_BYTE2_FLAG_UNK5*/) << 8)); // this flag is at uint8 offset 1 !! + else + // pretend that all other HOSTILE players have own faction, to allow follow, heal, rezz (trade wont work) + fieldBuffer << uint32(target->GetFaction()); + } + else + fieldBuffer << m_uint32Values[index]; + } + else + fieldBuffer << m_uint32Values[index]; + } + else + { + // send in current format (float as float, uint32 as uint32) + fieldBuffer << m_uint32Values[index]; + } + } + } + + updateMask.AppendToPacket(data); + data->append(fieldBuffer); +} + +void Unit::DestroyForPlayer(Player* target, bool /*onDeath = false*/) const +{ + if (Battleground* bg = target->GetBattleground()) + { + if (bg->isArena()) + { + WorldPacket data(SMSG_ARENA_UNIT_DESTROYED, 8); + data << GetGUID(); + target->SendDirectMessage(&data); + } + } + + WorldObject::DestroyForPlayer(target); +} + +int32 Unit::GetHighestExclusiveSameEffectSpellGroupValue(AuraEffect const* aurEff, AuraType auraType, bool checkMiscValue /*= false*/, int32 miscValue /*= 0*/) const +{ + int32 val = 0; + SpellSpellGroupMapBounds spellGroup = sSpellMgr->GetSpellSpellGroupMapBounds(aurEff->GetSpellInfo()->GetFirstRankSpell()->Id); + for (SpellSpellGroupMap::const_iterator itr = spellGroup.first; itr != spellGroup.second ; ++itr) + { + if (sSpellMgr->GetSpellGroupStackRule(itr->second) == SPELL_GROUP_STACK_RULE_EXCLUSIVE_SAME_EFFECT) + { + AuraEffectList const& auraEffList = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator auraItr = auraEffList.begin(); auraItr != auraEffList.end(); ++auraItr) + { + if (aurEff != (*auraItr) && (!checkMiscValue || (*auraItr)->GetMiscValue() == miscValue) && + sSpellMgr->IsSpellMemberOfSpellGroup((*auraItr)->GetSpellInfo()->Id, itr->second)) + { + // absolute value only + if (abs(val) < abs((*auraItr)->GetAmount())) + val = (*auraItr)->GetAmount(); + } + } + } + } + return val; +} + +bool Unit::IsHighestExclusiveAura(Aura const* aura, bool removeOtherAuraApplications /*= false*/) +{ + for (uint32 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (AuraEffect const* aurEff = aura->GetEffect(i)) + if (!IsHighestExclusiveAuraEffect(aura->GetSpellInfo(), aurEff->GetAuraType(), aurEff->GetAmount(), aura->GetEffectMask(), removeOtherAuraApplications)) + return false; + + return true; +} + +bool Unit::IsHighestExclusiveAuraEffect(SpellInfo const* spellInfo, AuraType auraType, int32 effectAmount, uint8 auraEffectMask, bool removeOtherAuraApplications /*= false*/) +{ + AuraEffectList const& auras = GetAuraEffectsByType(auraType); + for (Unit::AuraEffectList::const_iterator itr = auras.begin(); itr != auras.end();) + { + AuraEffect const* existingAurEff = (*itr); + ++itr; + + if (sSpellMgr->CheckSpellGroupStackRules(spellInfo, existingAurEff->GetSpellInfo()) == SPELL_GROUP_STACK_RULE_EXCLUSIVE_HIGHEST) + { + int32 diff = abs(effectAmount) - abs(existingAurEff->GetAmount()); + if (!diff) + for (int32 i = 0; i < MAX_SPELL_EFFECTS; ++i) + diff += int32((auraEffectMask & (1 << i)) >> i) - int32((existingAurEff->GetBase()->GetEffectMask() & (1 << i)) >> i); + + if (diff > 0) + { + Aura const* base = existingAurEff->GetBase(); + // no removing of area auras from the original owner, as that completely cancels them + if (removeOtherAuraApplications && (!base->IsArea() || base->GetOwner() != this)) + { + if (AuraApplication* aurApp = existingAurEff->GetBase()->GetApplicationOfTarget(GetGUID())) + { + bool hasMoreThanOneEffect = base->HasMoreThanOneEffectForType(auraType); + uint32 removedAuras = m_removedAurasCount; + RemoveAura(aurApp); + if (hasMoreThanOneEffect || m_removedAurasCount > removedAuras + 1) + itr = auras.begin(); + } + } + } + else if (diff < 0) + return false; + } + } + + return true; +} + +void Unit::Talk(std::string const& text, ChatMsg msgType, Language language, float textRange, WorldObject const* target) +{ + Trinity::CustomChatTextBuilder builder(this, msgType, text, language, target); + Trinity::LocalizedPacketDo localizer(builder); + Trinity::PlayerDistWorker > worker(this, textRange, localizer); + Cell::VisitWorldObjects(this, worker, textRange); +} + +void Unit::Say(std::string const& text, Language language, WorldObject const* target /*= nullptr*/) +{ + Talk(text, CHAT_MSG_MONSTER_SAY, language, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY), target); +} + +void Unit::Yell(std::string const& text, Language language, WorldObject const* target /*= nullptr*/) +{ + Talk(text, CHAT_MSG_MONSTER_YELL, language, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_YELL), target); +} + +void Unit::TextEmote(std::string const& text, WorldObject const* target /*= nullptr*/, bool isBossEmote /*= false*/) +{ + Talk(text, isBossEmote ? CHAT_MSG_RAID_BOSS_EMOTE : CHAT_MSG_MONSTER_EMOTE, LANG_UNIVERSAL, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_TEXTEMOTE), target); +} + +void Unit::Whisper(std::string const& text, Language language, Player* target, bool isBossWhisper /*= false*/) +{ + if (!target) + return; + + LocaleConstant locale = target->GetSession()->GetSessionDbLocaleIndex(); + WorldPacket data; + ChatHandler::BuildChatPacket(data, isBossWhisper ? CHAT_MSG_RAID_BOSS_WHISPER : CHAT_MSG_MONSTER_WHISPER, language, this, target, text, 0, "", locale); + target->SendDirectMessage(&data); +} + +void Unit::Talk(uint32 textId, ChatMsg msgType, float textRange, WorldObject const* target) +{ + if (!sObjectMgr->GetBroadcastText(textId)) + { + TC_LOG_ERROR("entities.unit", "WorldObject::MonsterText: `broadcast_text` (ID: %u) was not found", textId); + return; + } + + Trinity::BroadcastTextBuilder builder(this, msgType, textId, getGender(), target); + Trinity::LocalizedPacketDo localizer(builder); + Trinity::PlayerDistWorker > worker(this, textRange, localizer); + Cell::VisitWorldObjects(this, worker, textRange); +} + +void Unit::Say(uint32 textId, WorldObject const* target /*= nullptr*/) +{ + Talk(textId, CHAT_MSG_MONSTER_SAY, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY), target); +} + +void Unit::Yell(uint32 textId, WorldObject const* target /*= nullptr*/) +{ + Talk(textId, CHAT_MSG_MONSTER_YELL, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_YELL), target); +} + +void Unit::TextEmote(uint32 textId, WorldObject const* target /*= nullptr*/, bool isBossEmote /*= false*/) +{ + Talk(textId, isBossEmote ? CHAT_MSG_RAID_BOSS_EMOTE : CHAT_MSG_MONSTER_EMOTE, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_TEXTEMOTE), target); +} + +void Unit::Whisper(uint32 textId, Player* target, bool isBossWhisper /*= false*/) +{ + if (!target) + return; + + BroadcastText const* bct = sObjectMgr->GetBroadcastText(textId); + if (!bct) + { + TC_LOG_ERROR("entities.unit", "WorldObject::MonsterWhisper: `broadcast_text` was not %u found", textId); + return; + } + + LocaleConstant locale = target->GetSession()->GetSessionDbLocaleIndex(); + WorldPacket data; + ChatHandler::BuildChatPacket(data, isBossWhisper ? CHAT_MSG_RAID_BOSS_WHISPER : CHAT_MSG_MONSTER_WHISPER, LANG_UNIVERSAL, this, target, bct->GetText(locale, getGender()), 0, "", locale); + target->SendDirectMessage(&data); +} + +// Returns collisionheight of the unit. If it is 0, it returns DEFAULT_COLLISION_HEIGHT. +float Unit::GetCollisionHeight() const +{ + float scaleMod = GetObjectScale(); // 99% sure about this + + if (IsMounted()) + { + if (CreatureDisplayInfoEntry const* mountDisplayInfo = sCreatureDisplayInfoStore.LookupEntry(GetUInt32Value(UNIT_FIELD_MOUNTDISPLAYID))) + { + if (CreatureModelDataEntry const* mountModelData = sCreatureModelDataStore.LookupEntry(mountDisplayInfo->ModelID)) + { + CreatureDisplayInfoEntry const* displayInfo = sCreatureDisplayInfoStore.AssertEntry(GetNativeDisplayId()); + CreatureModelDataEntry const* modelData = sCreatureModelDataStore.AssertEntry(displayInfo->ModelID); + float const collisionHeight = scaleMod * ((mountModelData->MountHeight * mountDisplayInfo->CreatureModelScale) + (modelData->CollisionHeight * modelData->ModelScale * displayInfo->CreatureModelScale * 0.5f)); + return collisionHeight == 0.0f ? DEFAULT_COLLISION_HEIGHT : collisionHeight; + } + } + } + + //! Dismounting case - use basic default model data + CreatureDisplayInfoEntry const* displayInfo = sCreatureDisplayInfoStore.AssertEntry(GetNativeDisplayId()); + CreatureModelDataEntry const* modelData = sCreatureModelDataStore.AssertEntry(displayInfo->ModelID); + + float const collisionHeight = scaleMod * modelData->CollisionHeight * modelData->ModelScale * displayInfo->CreatureModelScale; + return collisionHeight == 0.0f ? DEFAULT_COLLISION_HEIGHT : collisionHeight; +} + +SpellInfo const* Unit::GetCastSpellInfo(SpellInfo const* spellInfo, TriggerCastFlags& triggerFlag) const +{ + Unit::AuraEffectList const& swaps = GetAuraEffectsByType(SPELL_AURA_OVERRIDE_ACTIONBAR_SPELLS); + Unit::AuraEffectList const& swaps2 = GetAuraEffectsByType(SPELL_AURA_OVERRIDE_ACTIONBAR_SPELLS_TRIGGERED); + + for (AuraEffect const* auraEffect : swaps) + { + if (uint32(auraEffect->GetMiscValue()) == spellInfo->Id || auraEffect->IsAffectingSpell(spellInfo)) + { + if (SpellInfo const* newInfo = sSpellMgr->GetSpellInfo(auraEffect->GetAmount())) + { + if (auraEffect->GetSpellInfo()->HasAttribute(SPELL_ATTR8_IGNORE_SPELLCAST_OVERRIDE_COST)) + triggerFlag |= TRIGGERED_IGNORE_POWER_COST; + else + triggerFlag &= ~TRIGGERED_IGNORE_POWER_COST; + + return newInfo; + } + } + } + + for (AuraEffect const* auraEffect : swaps2) + { + if (uint32(auraEffect->GetMiscValue()) == spellInfo->Id || auraEffect->IsAffectingSpell(spellInfo)) + { + if (SpellInfo const* newInfo = sSpellMgr->GetSpellInfo(auraEffect->GetAmount())) + { + if (auraEffect->GetSpellInfo()->HasAttribute(SPELL_ATTR8_IGNORE_SPELLCAST_OVERRIDE_COST)) + triggerFlag |= TRIGGERED_IGNORE_POWER_COST; + else + triggerFlag &= ~TRIGGERED_IGNORE_POWER_COST; + + return newInfo; + } + } + } + + return spellInfo; +} + +bool Unit::CanExecutePendingSpellCastRequest(SpellInfo const* spellInfo) const +{ + // Generic and melee spells have to wait, channeled spells can be processed immediately. + if (!GetCurrentSpell(CURRENT_CHANNELED_SPELL) && HasUnitState(UNIT_STATE_CASTING)) + return false; + + // Waiting for the global cooldown to expire + if (GetSpellHistory()->GetRemainingGlobalCooldown(spellInfo) > 0) + return false; + + return true; +} + +void Unit::RequestSpellCast(std::unique_ptr castRequest, SpellInfo const* spellInfo) +{ + // We are overriding an already existing spell cast request so inform the client that the old cast is being replaced + if (_pendingSpellCastRequest) + CancelPendingCastRequest(); + + _pendingSpellCastRequest = std::move(castRequest); + + // If we can process the cast request right now, do it. + if (CanExecutePendingSpellCastRequest(spellInfo)) + ProcessPendingSpellCastRequest(); +} + +void Unit::CancelPendingCastRequest() +{ + if (!_pendingSpellCastRequest) + return; + + // We have to inform the client that the cast has been canceled to avoid the cast button to stay highlightened + if (IsPlayer()) + { + // @todo: convert to packet class + WorldPacket data(SMSG_CAST_FAILED, 1 + 4 + 1); + data << uint8(_pendingSpellCastRequest->CastRequest.CastID); + data << uint32(_pendingSpellCastRequest->CastRequest.SpellID); + data << uint8(SPELL_FAILED_DONT_REPORT); + ToPlayer()->SendDirectMessage(&data); + } + + _pendingSpellCastRequest = nullptr; +} + +// A spell can be queued up within 400 milliseconds before global cooldown expires or the cast finishes +static constexpr uint16 const SPELL_QUEUE_TIME_WINDOW = 400; + +bool Unit::CanRequestSpellCast(SpellInfo const* spellInfo) const +{ + if (GetSpellHistory()->GetRemainingGlobalCooldown(spellInfo) > SPELL_QUEUE_TIME_WINDOW) + return false; + + for (CurrentSpellTypes spellSlot : { CURRENT_MELEE_SPELL, CURRENT_GENERIC_SPELL }) + if (Spell const* spell = GetCurrentSpell(spellSlot)) + if (spell->GetRemainingCastTime() > SPELL_QUEUE_TIME_WINDOW) + return false; + + return true; +} + +void Unit::ProcessPendingSpellCastRequest() +{ + if (!_pendingSpellCastRequest) + return; + + // Sanity check. If the player requested an invalid spell cast, just skip the request. + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(_pendingSpellCastRequest->CastRequest.SpellID); + if (!spellInfo) + { + _pendingSpellCastRequest = nullptr; + return; + } + + // Waiting for ongoing casts to finish and the global cooldown to expire. + if (!CanExecutePendingSpellCastRequest(spellInfo)) + return; + + // Spell can be executed now. Check data and prepare cast. + Unit* caster = this; + + // 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()) + { + ProcessItemCast(*_pendingSpellCastRequest, targets); + _pendingSpellCastRequest = nullptr; + return; + } + + if (caster->IsCreature() && !caster->ToCreature()->HasSpell(spellInfo->Id)) + { + // If the vehicle creature does not have the spell but it allows the passenger to cast own spells + // change caster to player and let him cast + + Unit* charmer = caster->GetCharmer(); + if (charmer && (!charmer->IsOnVehicle(this) || spellInfo->CheckVehicle(charmer) != SPELL_CAST_OK)) + return; + else if (charmer) + caster = charmer; + else + { + TC_LOG_ERROR("spells", "Creature %s tried to allow its charmer to cast spell %u but there is no charmer.", caster->GetGUID().ToString().c_str(), spellInfo->Id); + return; + } + } + + TriggerCastFlags triggerFlag = TRIGGERED_NONE; + + if (caster->GetTypeId() == TYPEID_PLAYER && !caster->ToPlayer()->HasActiveSpell(spellInfo->Id) && !spellInfo->HasAttribute(SPELL_ATTR8_SKIP_IS_KNOWN_CHECK) && + !caster->ToPlayer()->HasArchProject(static_cast(spellInfo->ResearchProjectId))) + { + bool allow = false; + + // allow casting of unknown spells for special lock cases + if (GameObject *go = targets.GetGOTarget()) + if (go->GetSpellForLock(caster->ToPlayer()) == spellInfo) + allow = true; + + // TODO: Preparation for #23204 + // allow casting of spells triggered by clientside periodic trigger auras + /* + if (caster->HasAuraTypeWithTriggerSpell(SPELL_AURA_PERIODIC_TRIGGER_SPELL_FROM_CLIENT, spellId)) + { + allow = true; + triggerFlag = TRIGGERED_FULL_MASK; + } + */ + + if (!allow) + { + _pendingSpellCastRequest = nullptr; + return; + } + } + + // Check possible spell cast overrides + spellInfo = caster->GetCastSpellInfo(spellInfo, triggerFlag); + + // can't use our own spells when we're in possession of another unit, + if (caster->isPossessing()) + { + _pendingSpellCastRequest = nullptr; + return; + } + + // Client is resending autoshot cast opcode when other spell is cast during shoot rotation + // Skip it to prevent "interrupt" message + // Also check targets! target may have changed and we need to interrupt current spell + if (spellInfo->IsAutoRepeatRangedSpell()) + { + if (Spell* spell = caster->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL)) + { + if (spell->m_spellInfo == spellInfo && spell->m_targets.GetUnitTargetGUID() == targets.GetUnitTargetGUID()) + { + _pendingSpellCastRequest.reset(); + return; + } + } + } + + // auto-selection buff level base at target level (in spellInfo) + // TODO: is this even necessary? client already seems to send correct rank for "standard" buffs + if (spellInfo->IsPositive()) + { + if (Unit* target = targets.GetUnitTarget()) + { + SpellInfo const* actualSpellInfo = spellInfo->GetAuraRankForLevel(target->getLevel()); + + // if rank not found then function return NULL but in explicit cast case original spell can be cast and later failed with appropriate error message + if (actualSpellInfo) + spellInfo = actualSpellInfo; + } + } + + if (!_pendingSpellCastRequest->CastRequest.Weight.empty()) + { + ArchData archaeologyCastData; + for (WorldPackets::Spells::SpellWeight const& weight : _pendingSpellCastRequest->CastRequest.Weight) + { + switch (weight.Type) + { + case 1: // Currency + archaeologyCastData.FragId = weight.ID; + archaeologyCastData.FragCount = weight.Quantity; + break; + case 2: // Item + archaeologyCastData.KeyId = weight.ID; + archaeologyCastData.KeyCount = weight.Quantity; + break; + default: + break; + } + } + + if (Player* player = caster->ToPlayer()) + player->SetArchData(archaeologyCastData); + } + + Spell* spell = new Spell(caster, spellInfo, triggerFlag); + spell->m_cast_count = _pendingSpellCastRequest->CastRequest.CastID; // set count of casts + spell->m_glyphIndex = _pendingSpellCastRequest->CastRequest.Misc; + spell->prepare(targets); + + _pendingSpellCastRequest.reset(); +} + +void Unit::ProcessItemCast(PendingSpellCastRequest const& castRequest, SpellCastTargets const& targets) +{ + Player* player = ToPlayer(); + if (!player || !castRequest.CastItemData.has_value()) + return; + + Item* item = player->GetUseableItemByPos(castRequest.CastItemData->BagSlot, castRequest.CastItemData->Slot); + if (!item) + { + player->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr); + return; + } + + if (item->GetGUID() != castRequest.CastItemData->CastItem) + { + player->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr); + return; + } + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + { + player->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, item, nullptr); + return; + } + + // some item classes can be used only in equipped state + if (proto->GetInventoryType() != INVTYPE_NON_EQUIP && !item->IsEquipped()) + { + player->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, item, nullptr); + return; + } + + InventoryResult msg = player->CanUseItem(item); + if (msg != EQUIP_ERR_OK) + { + player->SendEquipError(msg, item, nullptr); + return; + } + + // only allow conjured consumable, bandage, poisons (all should have the 2^21 item flag set in DB) + if (proto->GetClass() == ITEM_CLASS_CONSUMABLE && !(proto->GetFlags() & ITEM_FLAG_IGNORE_DEFAULT_ARENA_RESTRICTIONS) && player->InArena()) + { + player->SendEquipError(EQUIP_ERR_NOT_DURING_ARENA_MATCH, item, nullptr); + return; + } + + // don't allow items banned in arena + if ((proto->GetFlags() & ITEM_FLAG_NOT_USEABLE_IN_ARENA) && player->InArena()) + { + player->SendEquipError(EQUIP_ERR_NOT_DURING_ARENA_MATCH, item, nullptr); + return; + } + + if (player->IsInCombat()) + { + for (ItemEffect const& effect : proto->Effects) + { + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect.SpellID)) + { + if (!spellInfo->CanBeUsedInCombat()) + { + player->SendEquipError(EQUIP_ERR_NOT_IN_COMBAT, item, nullptr); + return; + } + } + } + } + + // check also BIND_ON_ACQUIRE and BIND_QUEST for .additem or .additemset case by GM (not binded at adding to inventory) + if (item->GetTemplate()->GetBonding() == BIND_ON_USE || item->GetTemplate()->GetBonding() == BIND_ON_ACQUIRE || item->GetTemplate()->GetBonding() == BIND_QUEST) + { + if (!item->IsSoulBound()) + { + item->SetState(ITEM_CHANGED, player); + item->SetBinding(true); + } + } + + player->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::ItemUse); + + // Note: If script stop casting it must send appropriate data to client to prevent stuck item in gray state. + if (!sScriptMgr->OnItemUse(player, item, targets)) + { + // no script or script not process request by self + player->CastItemUseSpell(item, targets, castRequest.CastRequest.CastID, castRequest.CastRequest.Misc); + } +} + +void Unit::SetGameClientMovingMe(GameClient* gameClientMovingMe) +{ + _gameClientMovingMe = gameClientMovingMe; +} + +Stats& Unit::GetStats() const +{ + return *_stats; +} + +int32 Unit::GetStat(StatType statType) const +{ + if (statType < StatType::Strength) + return 0; + + return GetInt32Value(UNIT_FIELD_STAT0 + AsUnderlyingType(statType)); +} diff --git a/src/server/game/Entities/Unit/Unit.cpp.bak-vianomod-v1-20261001-195036 b/src/server/game/Entities/Unit/Unit.cpp.bak-vianomod-v1-20261001-195036 new file mode 100644 index 0000000000..37ac95aaca --- /dev/null +++ b/src/server/game/Entities/Unit/Unit.cpp.bak-vianomod-v1-20261001-195036 @@ -0,0 +1,14382 @@ +/* + * 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 . + */ + +#include "Unit.h" +#include "AbstractPursuer.h" +#include "Archaeology.h" +#include "Battlefield.h" +#include "BattlefieldMgr.h" +#include "Battleground.h" +#include "BattlegroundScore.h" +#include "CellImpl.h" +#include "CharacterCache.h" +#include "CharmInfo.h" +#include "Chat.h" +#include "ChatTextBuilder.h" +#include "ChatPackets.h" +#include "CombatLogPackets.h" +#include "CombatPackets.h" +#include "Common.h" +#include "ConditionMgr.h" +#include "Containers.h" +#include "CreatureAI.h" +#include "CreatureAIImpl.h" +#include "CreatureGroups.h" +#include "Formulas.h" +#include "GameClient.h" +#include "GameTime.h" +#include "GridNotifiersImpl.h" +#include "Group.h" +#include "InstanceSaveMgr.h" +#include "InstanceScript.h" +#include "Item.h" +#include "ListUtils.h" +#include "Log.h" +#include "LootMgr.h" +#include "LootPackets.h" +#include "MiscPackets.h" +#include "MotionMaster.h" +#include "MovementGenerator.h" +#include "MovementPackets.h" +#include "MovementPacketBuilder.h" +#include "MovementStructures.h" +#include "MoveSpline.h" +#include "MoveSplineInit.h" +#include "MovementPacketSender.h" +#include "ObjectAccessor.h" +#include "ObjectMgr.h" +#include "Opcodes.h" +#include "OutdoorPvP.h" +#include "PassiveAI.h" +#include "Pet.h" +#include "PetPackets.h" +#include "PetAI.h" +#include "PhasingHandler.h" +#include "Player.h" +#include "PlayerAI.h" +#include "QuestDef.h" +#include "ReputationMgr.h" +#include "ScheduledChangeAI.h" +#include "Spell.h" +#include "SpellAuraEffects.h" +#include "SpellAuras.h" +#include "SpellCastRequest.h" +#include "SpellHistory.h" +#include "SpellInfo.h" +#include "SpellMgr.h" +#include "SpellPackets.h" +#include "Stats.h" +#include "TemporarySummon.h" +#include "Totem.h" +#include "Transport.h" +#include "UnitAI.h" +#include "UpdateFieldFlags.h" +#include "Util.h" +#include "Vehicle.h" +#include "VehiclePackets.h" +#include "World.h" +#include "WorldPacket.h" +#include "WorldSession.h" +#include +#include + +float baseMoveSpeed[MAX_MOVE_TYPE] = +{ + 2.5f, // MOVE_WALK + 7.0f, // MOVE_RUN + 4.5f, // MOVE_RUN_BACK + 4.722222f, // MOVE_SWIM + 2.5f, // MOVE_SWIM_BACK + 3.141594f, // MOVE_TURN_RATE + 7.0f, // MOVE_FLIGHT + 4.5f, // MOVE_FLIGHT_BACK + 3.14f // MOVE_PITCH_RATE +}; + +float playerBaseMoveSpeed[MAX_MOVE_TYPE] = +{ + 2.5f, // MOVE_WALK + 7.0f, // MOVE_RUN + 4.5f, // MOVE_RUN_BACK + 4.722222f, // MOVE_SWIM + 2.5f, // MOVE_SWIM_BACK + 3.141594f, // MOVE_TURN_RATE + 7.0f, // MOVE_FLIGHT + 4.5f, // MOVE_FLIGHT_BACK + 3.14f // MOVE_PITCH_RATE +}; + +DamageInfo::DamageInfo(Unit* attacker, Unit* victim, uint32 damage, SpellInfo const* spellInfo, SpellSchoolMask schoolMask, DamageEffectType damageType, WeaponAttackType attackType) + : m_attacker(attacker), m_victim(victim), m_damage(damage), m_spellInfo(spellInfo), m_schoolMask(schoolMask), m_damageType(damageType), m_attackType(attackType), + m_absorb(0), m_resist(0), m_block(0), m_hitMask(PROC_HIT_NONE) +{ +} + +DamageInfo::DamageInfo(CalcDamageInfo const& dmgInfo) + : m_attacker(dmgInfo.Attacker), m_victim(dmgInfo.Target), m_damage(dmgInfo.Damage), m_spellInfo(nullptr), m_schoolMask(SpellSchoolMask(dmgInfo.DamageSchoolMask)), + m_damageType(DIRECT_DAMAGE), m_attackType(dmgInfo.AttackType), m_absorb(dmgInfo.Absorb), m_resist(dmgInfo.Resist), m_block(dmgInfo.Blocked), m_hitMask(PROC_HIT_NONE) +{ + switch (dmgInfo.TargetState) + { + case VICTIMSTATE_IS_IMMUNE: + m_hitMask |= PROC_HIT_IMMUNE; + break; + case VICTIMSTATE_BLOCKS: + m_hitMask |= PROC_HIT_FULL_BLOCK; + break; + } + + if (dmgInfo.HitInfo & (HITINFO_PARTIAL_ABSORB | HITINFO_FULL_ABSORB)) + m_hitMask |= PROC_HIT_ABSORB; + + if (dmgInfo.HitInfo & HITINFO_FULL_RESIST) + m_hitMask |= PROC_HIT_FULL_RESIST; + + if (m_block) + m_hitMask |= PROC_HIT_BLOCK; + + bool const damageNullified = (dmgInfo.HitInfo & (HITINFO_FULL_ABSORB | HITINFO_FULL_RESIST)) != 0 || + (m_hitMask & (PROC_HIT_IMMUNE | PROC_HIT_FULL_BLOCK)) != 0; + switch (dmgInfo.HitOutCome) + { + case MELEE_HIT_MISS: + m_hitMask |= PROC_HIT_MISS; + break; + case MELEE_HIT_DODGE: + m_hitMask |= PROC_HIT_DODGE; + break; + case MELEE_HIT_PARRY: + m_hitMask |= PROC_HIT_PARRY; + break; + case MELEE_HIT_EVADE: + m_hitMask |= PROC_HIT_EVADE; + break; + case MELEE_HIT_BLOCK: + case MELEE_HIT_CRUSHING: + case MELEE_HIT_GLANCING: + case MELEE_HIT_NORMAL: + if (!damageNullified) + m_hitMask |= PROC_HIT_NORMAL; + break; + case MELEE_HIT_CRIT: + if (!damageNullified) + m_hitMask |= PROC_HIT_CRITICAL; + break; + } +} + +DamageInfo::DamageInfo(SpellNonMeleeDamage const& spellNonMeleeDamage, DamageEffectType damageType, WeaponAttackType attackType, ProcFlagsHit hitMask) + : m_attacker(spellNonMeleeDamage.attacker), m_victim(spellNonMeleeDamage.target), m_damage(spellNonMeleeDamage.damage), + m_spellInfo(sSpellMgr->GetSpellInfo(spellNonMeleeDamage.SpellID)), m_schoolMask(SpellSchoolMask(spellNonMeleeDamage.schoolMask)), m_damageType(damageType), + m_attackType(attackType), m_absorb(spellNonMeleeDamage.absorb), m_resist(spellNonMeleeDamage.resist), m_block(spellNonMeleeDamage.blocked), m_hitMask(hitMask) +{ + if (spellNonMeleeDamage.blocked) + m_hitMask |= PROC_HIT_BLOCK; + if (spellNonMeleeDamage.absorb) + m_hitMask |= PROC_HIT_ABSORB; +} + +void DamageInfo::ModifyDamage(int32 amount) +{ + amount = std::max(amount, -static_cast(GetDamage())); + m_damage += amount; +} + +void DamageInfo::AbsorbDamage(uint32 amount) +{ + amount = std::min(amount, GetDamage()); + m_absorb += amount; + m_damage -= amount; + m_hitMask |= PROC_HIT_ABSORB; +} + +void DamageInfo::ResistDamage(uint32 amount) +{ + amount = std::min(amount, GetDamage()); + m_resist += amount; + m_damage -= amount; + if (!m_damage) + { + m_hitMask |= PROC_HIT_FULL_RESIST; + m_hitMask &= ~(PROC_HIT_NORMAL | PROC_HIT_CRITICAL); + } +} + +void DamageInfo::BlockDamage(uint32 amount) +{ + amount = std::min(amount, GetDamage()); + m_block += amount; + m_damage -= amount; + m_hitMask |= PROC_HIT_BLOCK; + if (!m_damage) + { + m_hitMask |= PROC_HIT_FULL_BLOCK; + m_hitMask &= ~(PROC_HIT_NORMAL | PROC_HIT_CRITICAL); + } +} + +ProcFlagsHit DamageInfo::GetHitMask() const +{ + return m_hitMask; +} + +HealInfo::HealInfo(Unit* healer, Unit* target, uint32 heal, SpellInfo const* spellInfo, SpellSchoolMask schoolMask) + : _healer(healer), _target(target), _heal(heal), _effectiveHeal(0), _absorb(0), _spellInfo(spellInfo), _schoolMask(schoolMask), _hitMask(0) +{ +} + +void HealInfo::AbsorbHeal(uint32 amount) +{ + amount = std::min(amount, GetHeal()); + _absorb += amount; + _heal -= amount; + amount = std::min(amount, GetEffectiveHeal()); + _effectiveHeal -= amount; + _hitMask |= PROC_HIT_ABSORB; +} + +uint32 HealInfo::GetHitMask() const +{ + return _hitMask; +} + +ProcEventInfo::ProcEventInfo(Unit* actor, Unit* actionTarget, Unit* procTarget, + ProcFlags typeMask, ProcFlagsSpellType spellTypeMask, + ProcFlagsSpellPhase spellPhaseMask, ProcFlagsHit hitMask, + Spell* spell, DamageInfo* damageInfo, + HealInfo* healInfo) : + _actor(actor), _actionTarget(actionTarget), _procTarget(procTarget), + _typeMask(typeMask), _spellTypeMask(spellTypeMask), + _spellPhaseMask(spellPhaseMask), _hitMask(hitMask), _spell(spell), + _damageInfo(damageInfo), _healInfo(healInfo) +{ } + +SpellInfo const* ProcEventInfo::GetSpellInfo() const +{ + if (_spell) + return _spell->GetSpellInfo(); + if (_damageInfo) + return _damageInfo->GetSpellInfo(); + if (_healInfo) + return _healInfo->GetSpellInfo(); + return nullptr; +} + +SpellSchoolMask ProcEventInfo::GetSchoolMask() const +{ + if (_spell) + return _spell->GetSpellInfo()->GetSchoolMask(); + if (_damageInfo) + return _damageInfo->GetSchoolMask(); + if (_healInfo) + return _healInfo->GetSchoolMask(); + return SPELL_SCHOOL_MASK_NONE; +} + +DispelableAura::DispelableAura(Aura* aura, int32 dispelChance, uint8 dispelCharges) : + _aura(aura), _chance(dispelChance), _charges(dispelCharges) +{ +} + +DispelableAura::~DispelableAura() = default; + +bool DispelableAura::RollDispel() const +{ + return roll_chance_i(_chance); +} + +Unit::Unit(bool isWorldObject) : + WorldObject(isWorldObject), m_lastSanctuaryTime(0), + LastCharmerGUID(), m_ControlledByPlayer(false), movespline(std::make_unique()), + m_procDeep(0), m_procChainLength(0), m_removedAurasCount(0), + m_interruptMask(SpellAuraInterruptFlags::None), m_interruptMask2(SpellAuraInterruptFlags2::None), + m_charmer(nullptr), m_charmed(nullptr), + i_motionMaster(std::make_unique(this)), m_vehicle(nullptr), + m_unitTypeMask(UNIT_MASK_NONE), m_isEngaged(false), m_combatManager(this), m_threatManager(this), + i_AI(nullptr), m_aiLocked(false), m_spellHistory(std::make_unique(this)), + _stats(std::make_unique(this)) +{ + m_objectType |= TYPEMASK_UNIT; + m_objectTypeId = TYPEID_UNIT; + + m_updateFlag.MovementUpdate = true; + + m_baseAttackSpeed = { }; + m_attackTimer = { }; + m_modAttackSpeedPct.fill(1.0f); + + m_extraAttacks = 0; + m_canDualWield = false; + + m_movementCounter = 0; + + m_state = 0; + m_deathState = ALIVE; + + m_currentSpells.fill(nullptr); + + m_auraUpdateIterator = m_ownedAuras.end(); + + m_transform = 0; + m_canModifyStats = false; + + for (uint8 i = 0; i < UNIT_MOD_END; ++i) + { + m_auraFlatModifiersGroup[i][BASE_VALUE] = 0.0f; + m_auraFlatModifiersGroup[i][TOTAL_VALUE] = 0.0f; + m_auraPctModifiersGroup[i][BASE_PCT] = 1.0f; + m_auraPctModifiersGroup[i][TOTAL_PCT] = 1.0f; + } + // implement 50% base damage from offhand + m_auraPctModifiersGroup[UNIT_MOD_DAMAGE_OFFHAND][TOTAL_PCT] = 0.5f; + + for (uint8 i = 0; i < MAX_ATTACK; ++i) + { + m_weaponDamage[i][MINDAMAGE] = BASE_MINDAMAGE; + m_weaponDamage[i][MAXDAMAGE] = BASE_MAXDAMAGE; + } + + m_attacking = nullptr; + m_modMeleeHitChance = 0.0f; + m_modRangedHitChance = 0.0f; + m_modSpellHitChance = 0.0f; + m_baseSpellCritChance = 5; + + m_speed_rate.fill(1.0f); + _gameClientMovingMe = nullptr; + + // remove aurastates allowing special moves + m_reactiveTimer = { }; + m_cleanupDone = false; + m_duringRemoveFromWorld = false; + + m_serverSideVisibility.SetValue(SERVERSIDE_VISIBILITY_GHOST, GHOST_VISIBILITY_ALIVE); + + _lastLiquid = nullptr; + + _powerBarId = 0; + + _powerUpdateTimer = GetPowerUpdateInterval(); + _healthRegenerationTimer = UNIT_HEALTH_REGENERATION_INTERVAL; + + _powerFraction.fill(0.0f); + _usedPowerTypes.fill(MAX_POWERS); + + _oldFactionId = 0; + _isWalkingBeforeCharm = false; +} + +//////////////////////////////////////////////////////////// +// Methods of class Unit +Unit::~Unit() +{ + // set current spells as deletable + for (size_t i = 0; i < m_currentSpells.size(); ++i) + { + if (m_currentSpells[i]) + { + m_currentSpells[i]->SetReferencedFromCurrent(false); + m_currentSpells[i] = nullptr; + } + } + + m_Events.KillAllEvents(true); + + _DeleteRemovedAuras(); + + ASSERT(!m_duringRemoveFromWorld); + ASSERT(!m_attacking); + ASSERT(m_attackers.empty()); + ASSERT(m_sharedVision.empty()); + ASSERT(m_Controlled.empty()); + ASSERT(m_appliedAuras.empty()); + ASSERT(m_ownedAuras.empty()); + ASSERT(m_removedAuras.empty()); + ASSERT(m_gameObj.empty()); + ASSERT(m_dynObj.empty()); + ASSERT(!_gameClientMovingMe || _gameClientMovingMe->GetBasePlayer() == this); +} + +void Unit::Update(uint32 p_time) +{ + // WARNING! Order of execution here is important, do not change. + // Spells must be processed with event system BEFORE they go to _UpdateSpells. + // Or else we may have some SPELL_STATE_FINISHED spells stalled in pointers, that is bad. + WorldObject::Update(p_time); + + CheckPendingMovementAcks(); + + if (!IsInWorld()) + return; + + // Update server controlled facing for standing units + if (movespline->Finalized() && !isMoving()) + { + // Prioritize tracking channeled spells above unit targets if they should both be present at the same time. + Spell const* channeledSpell = GetCurrentSpell(CURRENT_CHANNELED_SPELL); + if (channeledSpell && channeledSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR1_TRACK_TARGET_IN_CHANNEL)) + { + ObjectGuid const channelGuid = GetChannelObjectGuid(); + if (!channelGuid.IsEmpty() && channelGuid != GetGUID()) + if (WorldObject const* objectTarget = ObjectAccessor::GetWorldObject(*this, channelGuid)) + SetOrientationTowards(objectTarget); + } + else if (IsCreature() && !HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED)) + { + ObjectGuid const targetGuid = GetTarget(); + if (!targetGuid.IsEmpty() && targetGuid != GetGUID()) + if (WorldObject const* objectTarget = ObjectAccessor::GetWorldObject(*this, targetGuid)) + SetOrientationTowards(objectTarget); + } + } + + _UpdateSpells(p_time); + ProcessPendingSpellCastRequest(); + + // If this is set during update SetCantProc(false) call is missing somewhere in the code + // Having this would prevent spells from being proced, so let's crash + ASSERT(!m_procDeep); + + m_combatManager.Update(p_time); + + auto spellPausesCombatTimer = [&](CurrentSpellTypes type) + { + return GetCurrentSpell(type) && GetCurrentSpell(type)->GetSpellInfo()->HasAttribute(SPELL_ATTR6_DELAY_COMBAT_TIMER_DURING_CAST); + }; + + if (!spellPausesCombatTimer(CURRENT_GENERIC_SPELL) && !spellPausesCombatTimer(CURRENT_CHANNELED_SPELL)) + { + if (uint32 base_att = getAttackTimer(BASE_ATTACK)) + setAttackTimer(BASE_ATTACK, (p_time >= base_att ? 0 : base_att - p_time)); + if (uint32 ranged_att = getAttackTimer(RANGED_ATTACK)) + setAttackTimer(RANGED_ATTACK, (p_time >= ranged_att ? 0 : ranged_att - p_time)); + if (uint32 off_att = getAttackTimer(OFF_ATTACK)) + setAttackTimer(OFF_ATTACK, (p_time >= off_att ? 0 : off_att - p_time)); + } + + // update abilities available only for fraction of time + UpdateReactives(p_time); + + if (IsAlive()) + { + ModifyAuraState(AURA_STATE_HEALTHLESS_20_PERCENT, HealthBelowPct(20)); + ModifyAuraState(AURA_STATE_HEALTHLESS_35_PERCENT, HealthBelowPct(35)); + ModifyAuraState(AURA_STATE_HEALTH_ABOVE_75_PERCENT, HealthAbovePct(75)); + } + + UpdateSplineMovement(p_time); + i_motionMaster->UpdateMotion(p_time); + + if (!i_AI && (GetTypeId() != TYPEID_PLAYER || IsCharmed())) + UpdateCharmAI(); + + // Wait with the aura interrupts until we have updated our movement generators and position6 + if (IsPlayer()) + InterruptMovementBasedAuras(); + else if (!movespline->Finalized()) + InterruptMovementBasedAuras(); + + // All position info based actions have been executed, reset info + _positionUpdateInfo.Reset(); + + // Update Health Regeneration + _healthRegenerationTimer -= p_time; + if (_healthRegenerationTimer <= 0) + { + RegenerateHealth(); + _healthRegenerationTimer += UNIT_HEALTH_REGENERATION_INTERVAL; + } + + if (HasScheduledAIChange() && (GetTypeId() != TYPEID_PLAYER || (IsCharmed() && GetCharmerGUID().IsCreature()))) + UpdateCharmAI(); + RefreshAI(); +} + +void Unit::Heartbeat() +{ + WorldObject::Heartbeat(); + + // SMSG_FLIGHT_SPLINE_SYNC for cyclic splines + SendFlightSplineSyncUpdate(); + + // Trigger heartbeat procs and generic aura behavior such as food emotes and invoking aura script hooks + TriggerAuraHeartbeat(); +} + +void Unit::TriggerAuraHeartbeat() +{ + for (auto const& [_, auraApplication] : m_appliedAuras) + auraApplication->GetBase()->Heartbeat(); + + Unit::ProcSkillsAndAuras(this, nullptr, PROC_FLAG_HEARTBEAT, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); +} + +bool Unit::haveOffhandWeapon() const +{ + if (Player const* player = ToPlayer()) + return player->GetWeaponForAttack(OFF_ATTACK, true) != nullptr; + + return CanDualWield(); +} + +void Unit::MonsterMoveWithSpeed(float x, float y, float z, float speed, bool generatePath, bool forceDestination) +{ + Movement::MoveSplineInit init(this); + init.MoveTo(x, y, z, generatePath, forceDestination); + init.SetVelocity(speed); + GetMotionMaster()->LaunchMoveSpline(std::move(init), 0, MOTION_SLOT_ACTIVE, POINT_MOTION_TYPE); +} + +void Unit::UpdateSplineMovement(uint32 t_diff) +{ + if (movespline->Finalized()) + return; + + movespline->updateState(t_diff); + bool arrived = movespline->Finalized(); + + if (arrived) + { + DisableSpline(); + if (Optional animTier = movespline->GetAnimation()) + SetAnimTier(*animTier); + } + + UpdateSplinePosition(); +} + +void Unit::UpdateSplinePosition() +{ + Movement::Location loc = movespline->ComputePosition(); + + if (movespline->onTransport) + { + Position& pos = m_movementInfo.transport.pos; + pos.m_positionX = loc.x; + pos.m_positionY = loc.y; + pos.m_positionZ = loc.z; + pos.SetOrientation(loc.orientation); + + if (TransportBase* transport = GetDirectTransport()) + transport->CalculatePassengerPosition(loc.x, loc.y, loc.z, &loc.orientation); + else + return; + } + + if (HasUnitState(UNIT_STATE_CANNOT_TURN)) + loc.orientation = GetOrientation(); + + UpdatePosition(loc.x, loc.y, loc.z, loc.orientation); +} + +void Unit::SendFlightSplineSyncUpdate() +{ + if (!movespline->isCyclic() || movespline->Finalized()) + return; + + WorldPackets::Movement::FlightSplineSync flightSplineSync; + flightSplineSync.Guid = GetGUID(); + flightSplineSync.SplineDist = float(movespline->timePassed()) / movespline->Duration(); + SendMessageToSet(flightSplineSync.Write(), true); +} + +void Unit::InterruptMovementBasedAuras() +{ + // TODO: Check if orientation transport offset changed instead of only global orientation + if (_positionUpdateInfo.Turned) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Turning); + + if (_positionUpdateInfo.Relocated && !GetVehicle()) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Moving); +} + +void Unit::DisableSpline() +{ + m_movementInfo.RemoveMovementFlag(MOVEMENTFLAG_FORWARD); + movespline->_Interrupt(); +} + +void Unit::resetAttackTimer(WeaponAttackType type) +{ + m_attackTimer[type] = uint32(GetBaseAttackTime(type) * m_modAttackSpeedPct[type]); +} + +bool Unit::IsWithinCombatRange(Unit const* obj, float dist2compare) const +{ + if (!obj || !IsInMap(obj) || !IsInPhase(obj)) + return false; + + float dx = GetPositionX() - obj->GetPositionX(); + float dy = GetPositionY() - obj->GetPositionY(); + float dz = GetPositionZ() - obj->GetPositionZ(); + float distsq = dx * dx + dy * dy + dz * dz; + + float sizefactor = GetCombatReach() + obj->GetCombatReach(); + float maxdist = dist2compare + sizefactor; + + return distsq < maxdist * maxdist; +} + +bool Unit::IsWithinMeleeRangeAt(Position const& pos, Unit const* obj) const +{ + if (!obj || !IsInMap(obj) || !IsInPhase(obj)) + return false; + + float dx = pos.GetPositionX() - obj->GetPositionX(); + float dy = pos.GetPositionY() - obj->GetPositionY(); + float dz = GetPositionZ() - obj->GetPositionZ(); + + float distsq = dx*dx + dy*dy + dz*dz; + + float maxdist = GetMeleeRange(obj); + + return distsq <= maxdist * maxdist; +} + +float Unit::GetMeleeRange(Unit const* target) const +{ + float range = GetCombatReach() + target->GetCombatReach() + 1.3333334f; + return std::max(range, NOMINAL_MELEE_RANGE); +} + +bool Unit::IsWithinBoundaryRadius(const Unit* obj) const +{ + if (!obj || !IsInMap(obj) || !IsInPhase(obj)) + return false; + + float objBoundaryRadius = std::max(obj->GetBoundaryRadius(), MIN_MELEE_REACH); + + return IsInDist(obj, objBoundaryRadius); +} + +AuraApplication* Unit::GetVisibleAura(uint8 slot) const +{ + VisibleAuraMap::const_iterator itr = m_visibleAuras.find(slot); + if (itr != m_visibleAuras.end()) + return itr->second; + return 0; +} + +void Unit::SetVisibleAura(uint8 slot, AuraApplication * aur) +{ + m_visibleAuras[slot]=aur; + UpdateAuraForGroup(slot); +} + +void Unit::RemoveVisibleAura(uint8 slot) +{ + m_visibleAuras.erase(slot); + UpdateAuraForGroup(slot); +} + +void Unit::UpdateInterruptMask() +{ + m_interruptMask = SpellAuraInterruptFlags::None; + m_interruptMask2 = SpellAuraInterruptFlags2::None; + for (AuraApplication const* aurApp : m_interruptableAuras) + { + m_interruptMask |= aurApp->GetBase()->GetSpellInfo()->AuraInterruptFlags; + m_interruptMask2 |= aurApp->GetBase()->GetSpellInfo()->AuraInterruptFlags2; + } + + if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL]) + { + if (spell->getState() == SPELL_STATE_CHANNELING) + { + m_interruptMask |= spell->m_spellInfo->ChannelInterruptFlags; + m_interruptMask2 |= spell->m_spellInfo->ChannelInterruptFlags2; + } + } +} + +bool Unit::HasAuraTypeWithFamilyFlags(AuraType auraType, uint32 familyName, uint32 familyFlags) const +{ + if (!HasAuraType(auraType)) + return false; + AuraEffectList const& auras = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator itr = auras.begin(); itr != auras.end(); ++itr) + if (SpellInfo const* iterSpellProto = (*itr)->GetSpellInfo()) + if (iterSpellProto->SpellFamilyName == familyName && iterSpellProto->SpellFamilyFlags[0] & familyFlags) + return true; + return false; +} + +bool Unit::HasBreakableByDamageAuraType(AuraType type, uint32 excludeAura) const +{ + AuraEffectList const& auras = GetAuraEffectsByType(type); + for (AuraEffectList::const_iterator itr = auras.begin(); itr != auras.end(); ++itr) + if ((!excludeAura || excludeAura != (*itr)->GetSpellInfo()->Id) && //Avoid self interrupt of channeled Crowd Control spells like Seduction + (*itr)->GetSpellInfo()->HasAuraInterruptFlag(SpellAuraInterruptFlags::Damage)) + return true; + return false; +} + +bool Unit::HasBreakableByDamageCrowdControlAura(Unit* excludeCasterChannel) const +{ + uint32 excludeAura = 0; + if (Spell* currentChanneledSpell = excludeCasterChannel ? excludeCasterChannel->GetCurrentSpell(CURRENT_CHANNELED_SPELL) : nullptr) + excludeAura = currentChanneledSpell->GetSpellInfo()->Id; //Avoid self interrupt of channeled Crowd Control spells like Seduction + + return ( HasBreakableByDamageAuraType(SPELL_AURA_MOD_CONFUSE, excludeAura) + || HasBreakableByDamageAuraType(SPELL_AURA_MOD_FEAR, excludeAura) + || HasBreakableByDamageAuraType(SPELL_AURA_MOD_STUN, excludeAura) + || HasBreakableByDamageAuraType(SPELL_AURA_MOD_ROOT, excludeAura) + || HasBreakableByDamageAuraType(SPELL_AURA_TRANSFORM, excludeAura)); +} + +/*static*/ void Unit::DealDamageMods(Unit const* victim, uint32& damage, uint32* absorb) +{ + if (!victim || !victim->IsAlive() || victim->HasUnitState(UNIT_STATE_IN_FLIGHT) || (victim->GetTypeId() == TYPEID_UNIT && victim->ToCreature()->IsEvadingAttacks())) + { + if (absorb) + *absorb += damage; + damage = 0; + } +} + +/*static*/ uint32 Unit::DealDamage(Unit* attacker, Unit* victim, uint32 damage, uint32 unmitigatedDamage, DamageEffectType damagetype, SpellSchoolMask damageSchoolMask, SpellInfo const* spellProto, bool durabilityLoss) +{ + // Sparring Checks + if (Creature* target = victim->ToCreature()) + { + if (attacker && attacker->IsCreature() && !attacker->IsCharmedOwnedByPlayerOrPlayer()) + { + if (target->GetNoNpcDamageBelowPctHealthValue() != 0.0f) + { + if (damage >= target->GetHealth()) // First check: if we have a sparring limit we will never allow creatures to kill the sparring victim + damage = target->GetHealth() - 1; + else if (target->GetHealthPct() <= target->GetNoNpcDamageBelowPctHealthValue()) // Second check: stop incomming damage when we have surpassed the health limit + damage = 0; + } + } + } + + if (UnitAI* victimAI = victim->GetAI()) + victimAI->DamageTaken(attacker, damage); + + if (UnitAI* attackerAI = attacker ? attacker->GetAI() : nullptr) + attackerAI->DamageDealt(victim, damage, damagetype); + + // Hook for OnDamage Event + sScriptMgr->OnDamage(attacker, victim, damage); + + if (victim->GetTypeId() == TYPEID_PLAYER) + { + // Signal to pets that their owner was attacked - except when DOT. + if (attacker != victim && damagetype != DOT) + { + for (Unit* controlled : victim->m_Controlled) + if (Creature* cControlled = controlled->ToCreature()) + if (CreatureAI* controlledAI = cControlled->AI()) + controlledAI->OwnerAttackedBy(attacker); + } + + if (victim->ToPlayer()->GetCommandStatus(CHEAT_GOD)) + return 0; + } + + if (damagetype != NODAMAGE) + { + // interrupting auras with SpellAuraInterruptFlags::Damage before checking !damage (absorbed damage breaks that type of auras) + if (spellProto) + { + if (!spellProto->HasAttribute(SPELL_ATTR4_REACTIVE_DAMAGE_PROC)) + victim->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Damage, spellProto); + } + else + victim->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Damage); + + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + AuraEffectList vCopyDamageCopy(victim->GetAuraEffectsByType(SPELL_AURA_SHARE_DAMAGE_PCT)); + // copy damage to casters of this aura + for (AuraEffectList::iterator i = vCopyDamageCopy.begin(); i != vCopyDamageCopy.end(); ++i) + { + // Check if aura was removed during iteration - we don't need to work on such auras + if (!((*i)->GetBase()->IsAppliedOnTarget(victim->GetGUID()))) + continue; + // check damage school mask + if (((*i)->GetMiscValue() & damageSchoolMask) == 0) + continue; + + Unit* shareDamageTarget = (*i)->GetCaster(); + if (!shareDamageTarget) + continue; + SpellInfo const* spell = (*i)->GetSpellInfo(); + + uint32 share = CalculatePct(damage, (*i)->GetAmount()); + + /// @todo check packets if damage is done by victim, or by attacker of victim + Unit::DealDamageMods(shareDamageTarget, share, nullptr); + Unit::DealDamage(attacker, shareDamageTarget, share, 0, NODAMAGE, spell->GetSchoolMask(), spell, false); + } + } + + uint32 health = victim->GetHealth(); + + // duel ends when player has 1 or less hp + bool duel_hasEnded = false; + bool duel_wasMounted = false; + if (victim->GetTypeId() == TYPEID_PLAYER && victim->ToPlayer()->duel && damage >= (health-1)) + { + if (!attacker) + return 0; + + // prevent kill only if killed in duel and killed by opponent or opponent controlled creature + if (victim->ToPlayer()->duel->opponent == attacker->GetControllingPlayer()) + damage = health - 1; + + duel_hasEnded = true; + } + else if (victim->IsCreature() && victim != attacker && damage >= health && victim->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_UNKILLABLE)) + { + damage = health - 1; + } + else if (victim->IsVehicle() && damage >= (health-1) && victim->GetCharmer() && victim->GetCharmer()->GetTypeId() == TYPEID_PLAYER) + { + Player* victimRider = victim->GetCharmer()->ToPlayer(); + + if (victimRider && victimRider->duel && victimRider->duel->isMounted) + { + if (!attacker) + return 0; + + // prevent kill only if killed in duel and killed by opponent or opponent controlled creature + if (victimRider->duel->opponent == attacker->GetControllingPlayer()) + damage = health - 1; + + duel_wasMounted = true; + duel_hasEnded = true; + } + } + + if (attacker && attacker != victim) + { + if (Player* killer = attacker->ToPlayer()) + { + // in bg, count dmg if victim is also a player + if (victim->GetTypeId() == TYPEID_PLAYER && !(spellProto && spellProto->HasAttribute(SPELL_ATTR7_DO_NOT_COUNT_FOR_PVP_SCOREBOARD))) + if (Battleground* bg = killer->GetBattleground()) + bg->UpdatePlayerScore(killer, SCORE_DAMAGE_DONE, damage); + + killer->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DAMAGE_DONE, health > damage ? damage : health, 0, 0, victim); + killer->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_HIT_DEALT, damage); + } + } + + if (victim->GetTypeId() == TYPEID_PLAYER) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_HIT_RECEIVED, damage); + else if (!victim->IsControlledByPlayer() || victim->IsVehicle()) + { + if (!victim->ToCreature()->hasLootRecipient()) + victim->ToCreature()->SetLootRecipient(attacker); + + if (!attacker || attacker->IsControlledByPlayer() || (attacker->ToTempSummon() && attacker->ToTempSummon()->GetSummoner() && attacker->ToTempSummon()->GetSummoner()->GetTypeId() == TYPEID_PLAYER)) + victim->ToCreature()->LowerPlayerDamageReq(health < damage ? health : damage); + } + + if (health <= damage) + { + if (victim->GetTypeId() == TYPEID_PLAYER && victim != attacker) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_TOTAL_DAMAGE_RECEIVED, health); + + Unit::Kill(attacker, victim, durabilityLoss); + } + else + { + if (victim->GetTypeId() == TYPEID_PLAYER) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_TOTAL_DAMAGE_RECEIVED, damage); + + victim->ModifyHealth(-(int32)damage); + + if (damagetype == DIRECT_DAMAGE || damagetype == SPELL_DIRECT_DAMAGE) + victim->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::NonPeriodicDamage, spellProto); + + if (victim->GetTypeId() != TYPEID_PLAYER) + { + // Part of Evade mechanics. DoT's and Thorns / Retribution Aura do not contribute to this + if (damagetype != DOT && damage > 0 && !victim->GetOwnerGUID().IsPlayer() && (!spellProto || !spellProto->HasAura(SPELL_AURA_DAMAGE_SHIELD))) + victim->ToCreature()->SetLastDamagedTime(GameTime::GetGameTime() + MAX_AGGRO_RESET_TIME); + + if (attacker && (!spellProto || !spellProto->HasAttribute(SPELL_ATTR4_NO_HARMFUL_THREAT))) + victim->GetThreatManager().AddThreat(attacker, float(damage), spellProto); + } + else // victim is a player + { + // random durability for items (HIT TAKEN) + if (roll_chance_f(sWorld->getRate(RATE_DURABILITY_LOSS_DAMAGE))) + { + EquipmentSlots slot = EquipmentSlots(urand(0, EQUIPMENT_SLOT_END-1)); + victim->ToPlayer()->DurabilityPointLossForEquipSlot(slot); + } + } + + // Rage from damage received + if (attacker != victim && victim->GetPowerType() == POWER_RAGE) + { + uint32 rageBaseReward = CalcDamageTakenRageGain(victim, std::max(damage, unmitigatedDamage)); + victim->RewardRage(rageBaseReward, false); + } + + if (attacker && attacker->GetTypeId() == TYPEID_PLAYER) + { + // random durability for items (HIT DONE) + if (roll_chance_f(sWorld->getRate(RATE_DURABILITY_LOSS_DAMAGE))) + { + EquipmentSlots slot = EquipmentSlots(urand(0, EQUIPMENT_SLOT_END-1)); + attacker->ToPlayer()->DurabilityPointLossForEquipSlot(slot); + } + } + + if (damagetype != NODAMAGE && damagetype != DOT) + { + if (victim != attacker && (!spellProto || !(spellProto->HasAttribute(SPELL_ATTR6_NO_PUSHBACK) || spellProto->HasAttribute(SPELL_ATTR7_DONT_CAUSE_SPELL_PUSHBACK) || spellProto->HasAttribute(SPELL_ATTR3_TREAT_AS_PERIODIC)))) + { + if (Spell* spell = victim->m_currentSpells[CURRENT_GENERIC_SPELL]) + { + if (spell->getState() == SPELL_STATE_PREPARING) + { + auto isCastInterrupted = [&]() + { + if ((victim->IsPlayer() && spell->m_spellInfo->InterruptFlags.HasFlag(SpellInterruptFlags::DamageCancelsPlayerOnly))) + return true; + + return false; + }; + + auto isCastDelayed = [&]() + { + if (!damage) + return false; + + if ((victim->IsPlayer() && spell->m_spellInfo->InterruptFlags.HasFlag(SpellInterruptFlags::DamagePushbackPlayerOnly))) + return true; + + return false; + }; + + if (isCastInterrupted()) + victim->InterruptNonMeleeSpells(false); + else if (isCastDelayed()) + spell->Delayed(); + } + } + + if (damage && victim->IsPlayer()) + if (Spell* spell = victim->m_currentSpells[CURRENT_CHANNELED_SPELL]) + if (spell->getState() == SPELL_STATE_CHANNELING && spell->m_spellInfo->HasChannelInterruptFlag(SpellAuraInterruptFlags::DamageChannelDuration)) + spell->DelayedChannel(); + } + } + + // last damage from duel opponent + if (duel_hasEnded) + { + Player* he = duel_wasMounted ? victim->GetCharmer()->ToPlayer() : victim->ToPlayer(); + + ASSERT(he && he->duel); + + if (duel_wasMounted) // In this case victim == mount + victim->SetHealth(1); + else + he->SetHealth(1); + + he->duel->opponent->CombatStopWithPets(true); + he->CombatStopWithPets(true); + + he->CastSpell(he, 7267, true); // beg + he->DuelComplete(DUEL_WON); + } + } + + return damage; +} + +void Unit::CastStop(uint32 except_spellid) +{ + for (uint32 i = CURRENT_FIRST_NON_MELEE_SPELL; i < CURRENT_MAX_SPELL; i++) + if (m_currentSpells[i] && m_currentSpells[i]->m_spellInfo->Id != except_spellid) + InterruptSpell(CurrentSpellTypes(i), false); +} + +void Unit::CalculateSpellDamageTaken(SpellNonMeleeDamage* damageInfo, int32 damage, SpellInfo const* spellInfo, WeaponAttackType attackType, bool crit, bool blocked /*= false*/) +{ + if (damage < 0) + return; + + Unit* victim = damageInfo->target; + if (!victim || !victim->IsAlive()) + return; + + SpellSchoolMask damageSchoolMask = SpellSchoolMask(damageInfo->schoolMask); + + // Spells with SPELL_ATTR4_IGNORE_DAMAGE_TAKEN_MODIFIERS ignore resilience because their damage is based off another spell's damage. + if (!spellInfo->HasAttribute(SPELL_ATTR4_IGNORE_DAMAGE_TAKEN_MODIFIERS)) + { + if (Unit::IsDamageReducedByArmor(damageSchoolMask, spellInfo)) + damage = Unit::CalcArmorReducedDamage(this, victim, damage, spellInfo, attackType); + + // Per-school calc + switch (spellInfo->DmgClass) + { + // Melee and Ranged Spells + case SPELL_DAMAGE_CLASS_RANGED: + case SPELL_DAMAGE_CLASS_MELEE: + { + if (crit) + { + damageInfo->HitInfo |= SPELL_HIT_TYPE_CRIT; + + // Calculate crit bonus + uint32 crit_bonus = damage; + // Apply crit_damage bonus for melee spells + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellInfo, SpellModOp::CritDamageAndHealing, crit_bonus); + damage += crit_bonus; + + // Apply SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_DAMAGE or SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_DAMAGE + float critPctDamageMod = 0.0f; + if (attackType == RANGED_ATTACK) + critPctDamageMod += victim->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_DAMAGE); + else + critPctDamageMod += victim->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_DAMAGE); + + // Increase crit damage from SPELL_AURA_MOD_CRIT_DAMAGE_BONUS + critPctDamageMod += (GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_CRIT_DAMAGE_BONUS, spellInfo->GetSchoolMask()) - 1.0f) * 100; + + if (critPctDamageMod != 0) + AddPct(damage, critPctDamageMod); + } + + // Spell weapon based damage CAN BE crit & blocked at same time + if (blocked) + { + // double blocked amount if block is critical + uint32 value = victim->GetBlockPercent(); + if (victim->IsBlockCritical()) + value *= 2; // double blocked percent + + damageInfo->blocked = CalculatePct(damage, value); + if (damage <= int32(damageInfo->blocked)) + { + damageInfo->blocked = uint32(damage); + damageInfo->fullBlock = true; + } + damage -= damageInfo->blocked; + } + + if (CanApplyResilience()) + Unit::ApplyResilience(victim, &damage); + break; + } + // Magical Attacks + case SPELL_DAMAGE_CLASS_NONE: + case SPELL_DAMAGE_CLASS_MAGIC: + { + // If crit add critical bonus + if (crit) + { + damageInfo->HitInfo |= SPELL_HIT_TYPE_CRIT; + damage = Unit::SpellCriticalDamageBonus(this, spellInfo, damage); + } + + if (CanApplyResilience()) + Unit::ApplyResilience(victim, &damage); + break; + + } + default: + break; + } + } + + // Script Hook For CalculateSpellDamageTaken -- Allow scripts to change the Damage post class mitigation calculations + sScriptMgr->ModifySpellDamageTaken(damageInfo->target, damageInfo->attacker, damage); + + // Calculate absorb resist + if (damage < 0) + damage = 0; + + damageInfo->damage = damage; + DamageInfo dmgInfo(*damageInfo, SPELL_DIRECT_DAMAGE, BASE_ATTACK, PROC_HIT_NONE); + Unit::CalcAbsorbResist(dmgInfo); + damageInfo->absorb = dmgInfo.GetAbsorb(); + damageInfo->resist = dmgInfo.GetResist(); + + if (damageInfo->absorb) + damageInfo->HitInfo |= (damageInfo->damage - damageInfo->absorb == 0 ? HITINFO_FULL_ABSORB : HITINFO_PARTIAL_ABSORB); + + if (damageInfo->resist) + damageInfo->HitInfo |= (damageInfo->damage - damageInfo->resist == 0 ? HITINFO_FULL_RESIST : HITINFO_PARTIAL_RESIST); + + damageInfo->damage = dmgInfo.GetDamage(); +} + +void Unit::DealSpellDamage(SpellNonMeleeDamage* damageInfo, bool durabilityLoss) +{ + if (!damageInfo) + return; + + Unit* victim = damageInfo->target; + if (!victim) + return; + + if (!victim->IsAlive() || victim->HasUnitState(UNIT_STATE_IN_FLIGHT) || (victim->GetTypeId() == TYPEID_UNIT && victim->ToCreature()->IsEvadingAttacks())) + return; + + SpellInfo const* spellProto = sSpellMgr->GetSpellInfo(damageInfo->SpellID); + if (spellProto == nullptr) + { + TC_LOG_DEBUG("entities.unit", "Unit::DealSpellDamage has wrong damageInfo->SpellID: %u", damageInfo->SpellID); + return; + } + + // Call default DealDamage + Unit::DealDamage(this, victim, damageInfo->damage, damageInfo->unmitigatedDamage, SPELL_DIRECT_DAMAGE, SpellSchoolMask(damageInfo->schoolMask), spellProto, durabilityLoss); +} + +/*static*/ uint32 Unit::CalcMeleeAttackRageGain(Unit const* attacker, Unit const* victim, WeaponAttackType attType) +{ + if (!attacker || !victim || (attType != BASE_ATTACK && attType != OFF_ATTACK)) + return 0; + + uint32 rage = uint32(attacker->GetBaseAttackTime(attType) / 1000.f * 6.5f); + if (attType == OFF_ATTACK) + rage /= 2; + + // Players below level 10 start with a 50% rage gain reduction that slowly diminishes + if (attacker->IsPlayer() && attacker->getLevel() < 10) + rage -= (rage / 2.f) * (1.0f - (attacker->getLevel() / 10.f)); + + rage *= 10; + + AddPct(rage, attacker->GetTotalAuraModifier(SPELL_AURA_MOD_RAGE_FROM_DAMAGE_DEALT)); + + return rage; +} + +// The minimum amount a damaging hit should award +constexpr uint32 MIN_RAGE_AMOUNT = 10; + +/*static*/ uint32 Unit::CalcDamageTakenRageGain(Unit const* victim, uint32 damage) +{ + if (!victim || damage == 0) + return 0; + + double multiplier = static_cast(damage) / victim->GetMaxHealth(); + + // This formula is based on sampling multiple sniff data at multiple level ranges + // There has never been any leak and the only info we have is that the amount is based on max HP and damage taken and that there is a hidden constant + return std::max(MIN_RAGE_AMOUNT, std::ceil(400 * multiplier)); +} + +/// @todo for melee need create structure as in +void Unit::CalculateMeleeDamage(Unit* victim, CalcDamageInfo* damageInfo, WeaponAttackType attackType /*= BASE_ATTACK*/) +{ + damageInfo->Attacker = this; + damageInfo->Target = victim; + + damageInfo->DamageSchoolMask = GetMeleeDamageSchoolMask(attackType); + damageInfo->Damage = 0; + damageInfo->OriginalDamage = 0; + damageInfo->Absorb = 0; + damageInfo->Resist = 0; + + damageInfo->Blocked = 0; + damageInfo->HitInfo = 0; + damageInfo->TargetState = 0; + + damageInfo->AttackType = attackType; + damageInfo->ProcAttacker = PROC_FLAG_NONE; + damageInfo->ProcVictim = PROC_FLAG_NONE; + damageInfo->UnmitigatedDamage = 0; + damageInfo->HitOutCome = MELEE_HIT_EVADE; + damageInfo->RageGained = 0; + + if (!victim) + return; + + if (!IsAlive() || !victim->IsAlive()) + return; + + // Select HitInfo/procAttacker/procVictim flag based on attack type + switch (attackType) + { + case BASE_ATTACK: + damageInfo->ProcAttacker = PROC_FLAG_DEAL_MELEE_SWING | PROC_FLAG_MAIN_HAND_WEAPON_SWING; + damageInfo->ProcVictim = PROC_FLAG_TAKE_MELEE_SWING; + break; + case OFF_ATTACK: + damageInfo->ProcAttacker = PROC_FLAG_DEAL_MELEE_SWING | PROC_FLAG_OFF_HAND_WEAPON_SWING; + damageInfo->ProcVictim = PROC_FLAG_TAKE_MELEE_SWING; + damageInfo->HitInfo = HITINFO_OFFHAND; + break; + default: + return; + } + + // Physical Immune check + if (damageInfo->Target->IsImmunedToDamage(SpellSchoolMask(damageInfo->DamageSchoolMask))) + { + damageInfo->HitInfo |= HITINFO_NORMALSWING; + damageInfo->TargetState = VICTIMSTATE_IS_IMMUNE; + + damageInfo->Damage = 0; + damageInfo->UnmitigatedDamage = 0; + return; + } + + uint32 damage = 0; + damage += CalculateDamage(damageInfo->AttackType, false, true); + // Add melee damage bonus + damage = MeleeDamageBonusDone(damageInfo->Target, damage, damageInfo->AttackType, DIRECT_DAMAGE, nullptr, MECHANIC_NONE, SpellSchoolMask(damageInfo->DamageSchoolMask)); + damage = damageInfo->Target->MeleeDamageBonusTaken(this, damage, damageInfo->AttackType, nullptr, SpellSchoolMask(damageInfo->DamageSchoolMask)); + + // Script Hook For CalculateMeleeDamage -- Allow scripts to change the Damage pre class mitigation calculations + sScriptMgr->ModifyMeleeDamage(damageInfo->Target, damageInfo->Attacker, damage); + + // Calculate armor reduction + if (Unit::IsDamageReducedByArmor((SpellSchoolMask)(damageInfo->DamageSchoolMask))) + damageInfo->Damage = Unit::CalcArmorReducedDamage(damageInfo->Attacker, damageInfo->Target, damage, nullptr, damageInfo->AttackType); + else + damageInfo->Damage = damage; + + // Store unmitigated damage to reward rage later + damageInfo->UnmitigatedDamage = damage; + + damageInfo->HitOutCome = RollMeleeOutcomeAgainst(damageInfo->Target, damageInfo->AttackType); + + switch (damageInfo->HitOutCome) + { + case MELEE_HIT_EVADE: + damageInfo->HitInfo |= HITINFO_MISS | HITINFO_SWINGNOHITSOUND; + damageInfo->TargetState = VICTIMSTATE_EVADES; + damageInfo->OriginalDamage = damageInfo->Damage; + + damageInfo->Damage = 0; + return; + case MELEE_HIT_MISS: + damageInfo->HitInfo |= HITINFO_MISS; + damageInfo->TargetState = VICTIMSTATE_INTACT; + damageInfo->OriginalDamage = damageInfo->Damage; + + damageInfo->Damage = 0; + damageInfo->UnmitigatedDamage = 0; + break; + case MELEE_HIT_NORMAL: + damageInfo->TargetState = VICTIMSTATE_HIT; + damageInfo->OriginalDamage = damageInfo->Damage; + break; + case MELEE_HIT_CRIT: + { + damageInfo->HitInfo |= HITINFO_CRITICALHIT; + damageInfo->TargetState = VICTIMSTATE_HIT; + + // Crit bonus calc + damageInfo->Damage *= 2; + float mod = 0.0f; + // Apply SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_DAMAGE or SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_DAMAGE + if (damageInfo->AttackType == RANGED_ATTACK) + mod += damageInfo->Target->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_DAMAGE); + else + mod += damageInfo->Target->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_DAMAGE); + + // Increase crit damage from SPELL_AURA_MOD_CRIT_DAMAGE_BONUS + mod += (GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_CRIT_DAMAGE_BONUS, damageInfo->DamageSchoolMask) - 1.0f) * 100; + + if (mod != 0) + AddPct(damageInfo->Damage, mod); + + damageInfo->OriginalDamage = damageInfo->Damage; + break; + } + case MELEE_HIT_PARRY: + damageInfo->TargetState = VICTIMSTATE_PARRY; + damageInfo->OriginalDamage = damageInfo->Damage; + damageInfo->Damage = 0; + break; + case MELEE_HIT_DODGE: + damageInfo->TargetState = VICTIMSTATE_DODGE; + damageInfo->OriginalDamage = damageInfo->Damage; + damageInfo->Damage = 0; + break; + case MELEE_HIT_BLOCK: + damageInfo->TargetState = VICTIMSTATE_HIT; + damageInfo->HitInfo |= HITINFO_BLOCK; + // 30% damage blocked, double blocked amount if block is critical + damageInfo->Blocked = CalculatePct(damageInfo->Damage, damageInfo->Target->GetBlockPercent()); + if (damageInfo->Target->IsBlockCritical()) + damageInfo->Blocked *= 2; + + damageInfo->OriginalDamage = damageInfo->Damage; + damageInfo->Damage -= damageInfo->Blocked; + break; + case MELEE_HIT_GLANCING: + { + damageInfo->HitInfo |= HITINFO_GLANCING; + damageInfo->TargetState = VICTIMSTATE_HIT; + int32 leveldif = int32(victim->getLevel()) - int32(getLevel()); + if (leveldif > 3) + leveldif = 3; + + damageInfo->OriginalDamage = damageInfo->Damage; + float reducePercent = 1.f - leveldif * 0.1f; + damageInfo->Damage = uint32(reducePercent * damageInfo->Damage); + break; + } + case MELEE_HIT_CRUSHING: + damageInfo->HitInfo |= HITINFO_CRUSHING; + damageInfo->TargetState = VICTIMSTATE_HIT; + // 150% normal damage + damageInfo->Damage += (damageInfo->Damage / 2); + damageInfo->OriginalDamage = damageInfo->Damage; + break; + default: + break; + } + + // Always apply HITINFO_AFFECTS_VICTIM in case its not a miss + if (!(damageInfo->HitInfo & HITINFO_MISS)) + damageInfo->HitInfo |= HITINFO_AFFECTS_VICTIM; + + int32 resilienceReduction = damageInfo->Damage; + if (CanApplyResilience()) + Unit::ApplyResilience(victim, &resilienceReduction); + resilienceReduction = damageInfo->Damage - resilienceReduction; + damageInfo->Damage -= resilienceReduction; + + // Calculate absorb resist + if (int32(damageInfo->Damage) > 0) + { + damageInfo->ProcVictim |= PROC_FLAG_TAKE_ANY_DAMAGE; + // Calculate absorb & resists + DamageInfo dmgInfo(*damageInfo); + Unit::CalcAbsorbResist(dmgInfo); + damageInfo->Absorb = dmgInfo.GetAbsorb(); + damageInfo->Resist = dmgInfo.GetResist(); + + if (damageInfo->Absorb) + damageInfo->HitInfo |= (damageInfo->Damage - damageInfo->Absorb == 0 ? HITINFO_FULL_ABSORB : HITINFO_PARTIAL_ABSORB); + + if (damageInfo->Resist) + damageInfo->HitInfo |= (damageInfo->Damage - damageInfo->Resist == 0 ? HITINFO_FULL_RESIST : HITINFO_PARTIAL_RESIST); + + damageInfo->Damage = dmgInfo.GetDamage(); + } + else // Impossible get negative result but.... + damageInfo->Damage = 0; +} + +void Unit::DealMeleeDamage(CalcDamageInfo* damageInfo, bool durabilityLoss) +{ + Unit* victim = damageInfo->Target; + + if (!victim->IsAlive() || victim->HasUnitState(UNIT_STATE_IN_FLIGHT) || (victim->GetTypeId() == TYPEID_UNIT && victim->ToCreature()->IsEvadingAttacks())) + return; + + if (damageInfo->TargetState == VICTIMSTATE_PARRY && + (GetTypeId() != TYPEID_UNIT || (ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_PARRY_HASTEN) == 0)) + { + // Get attack timers + float offtime = float(victim->getAttackTimer(OFF_ATTACK)); + float basetime = float(victim->getAttackTimer(BASE_ATTACK)); + // Reduce attack time + if (victim->haveOffhandWeapon() && offtime < basetime) + { + float percent20 = victim->GetBaseAttackTime(OFF_ATTACK) * 0.20f; + float percent60 = 3.0f * percent20; + if (offtime > percent20 && offtime <= percent60) + victim->setAttackTimer(OFF_ATTACK, uint32(percent20)); + else if (offtime > percent60) + { + offtime -= 2.0f * percent20; + victim->setAttackTimer(OFF_ATTACK, uint32(offtime)); + } + } + else + { + float percent20 = victim->GetBaseAttackTime(BASE_ATTACK) * 0.20f; + float percent60 = 3.0f * percent20; + if (basetime > percent20 && basetime <= percent60) + victim->setAttackTimer(BASE_ATTACK, uint32(percent20)); + else if (basetime > percent60) + { + basetime -= 2.0f * percent20; + victim->setAttackTimer(BASE_ATTACK, uint32(basetime)); + } + } + } + + // Call default DealDamage + Unit::DealDamage(this, victim, damageInfo->Damage, damageInfo->UnmitigatedDamage, DIRECT_DAMAGE, SpellSchoolMask(damageInfo->DamageSchoolMask), nullptr, durabilityLoss); + + // If this is a creature and it attacks from behind it has a probability to daze it's victim + if ((damageInfo->HitOutCome == MELEE_HIT_CRIT || damageInfo->HitOutCome == MELEE_HIT_CRUSHING || damageInfo->HitOutCome == MELEE_HIT_NORMAL || damageInfo->HitOutCome == MELEE_HIT_GLANCING) && + IsCreature() && !ToCreature()->IsControlledByPlayer() && !ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_CANNOT_DAZE) && + !victim->HasInArc(float(M_PI), this) && (victim->IsPlayer() || (victim->IsCreature() && !victim->ToCreature()->isWorldBoss())) && !victim->IsVehicle()) + { + // 20% base chance + float chance = 20.0f; + + // there is a newbie protection, at level 10 just 7% base chance; assuming linear function + if (victim->getLevel() < 30) + chance = 0.65f * victim->getLevel() + 0.5f; + + uint32 const victimDefense = victim->GetMaxSkillValueForLevel(this); + uint32 const attackerMeleeSkill = GetMaxSkillValueForLevel(); + + chance *= attackerMeleeSkill / float(victimDefense) * 0.16f; + + // -probability is between 0% and 40% + RoundToInterval(chance, 0.0f, 40.0f); + if (roll_chance_f(chance)) + CastSpell(victim, 1604 /*SPELL_DAZED*/, true); + } + + if (GetTypeId() == TYPEID_PLAYER) + { + DamageInfo dmgInfo(*damageInfo); + ToPlayer()->CastItemCombatSpell(dmgInfo); + } + + // Do effect if any damage done to target + if (damageInfo->Damage) + { + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + AuraEffectList vDamageShieldsCopy(victim->GetAuraEffectsByType(SPELL_AURA_DAMAGE_SHIELD)); + for (AuraEffect const* aurEff : vDamageShieldsCopy) + { + SpellInfo const* spellInfo = aurEff->GetSpellInfo(); + + // Damage shield can be resisted... + SpellMissInfo missInfo = victim->SpellHitResult(this, spellInfo, false); + if (missInfo != SPELL_MISS_NONE) + { + victim->SendSpellMiss(this, spellInfo->Id, missInfo); + continue; + } + + // ...or immuned + if (IsImmunedToDamage(spellInfo)) + { + victim->SendSpellDamageImmune(this, spellInfo->Id); + continue; + } + + uint32 damage = aurEff->GetAmount(); + if (Unit* caster = aurEff->GetCaster()) + { + damage = caster->SpellDamageBonusDone(this, spellInfo, damage, SPELL_DIRECT_DAMAGE, aurEff->GetEffIndex()); + damage = SpellDamageBonusTaken(caster, spellInfo, damage, SPELL_DIRECT_DAMAGE); + } + + DamageInfo dmgInfo(this, victim, damage, spellInfo, spellInfo->GetSchoolMask(), SPELL_DIRECT_DAMAGE, BASE_ATTACK); + victim->CalcAbsorbResist(dmgInfo); + damage = dmgInfo.GetDamage(); + Unit::DealDamageMods(victim, damage, nullptr); + + /// @todo Move this to a packet handler + WorldPackets::CombatLog::SpellDamageShield damageShield; + damageShield.Attacker = victim->GetGUID(); + damageShield.Defender = GetGUID(); + damageShield.SpellID = spellInfo->Id; + damageShield.TotalDamage = damage; + damageShield.OverKill = std::max(int32(damage) - int32(GetHealth()), 0); + damageShield.SchoolMask = spellInfo->SchoolMask; + damageShield.LogAbsorbed = dmgInfo.GetAbsorb(); + victim->SendMessageToSet(damageShield.Write(), true); + + Unit::DealDamage(victim, this, damage, 0, SPELL_DIRECT_DAMAGE, spellInfo->GetSchoolMask(), spellInfo, true); + } + } +} + +void Unit::HandleEmoteCommand(uint32 emoteId) +{ + WorldPackets::Chat::Emote emote; + emote.Guid = GetGUID(); + emote.EmoteID = emoteId; + + SendMessageToSet(emote.Write(), true); +} + +/*static*/ bool Unit::IsDamageReducedByArmor(SpellSchoolMask schoolMask, SpellInfo const* spellInfo /*= nullptr*/) +{ + // only physical spells damage gets reduced by armor + if ((schoolMask & SPELL_SCHOOL_MASK_NORMAL) == 0) + return false; + + return !spellInfo || !spellInfo->HasAttribute(SPELL_ATTR0_CU_IGNORE_ARMOR); +} + +// Calculation taken from Paperdollframe.lua (15595) +static float GetArmorReduction(float armor, uint8 attackerLevel) +{ + float levelModifier = attackerLevel; + if (levelModifier > 80) + levelModifier = levelModifier + (4.5f * (levelModifier - 59)) + (20 * (levelModifier - 80)); + else if (levelModifier > 59) + levelModifier = levelModifier + (4.5f * (levelModifier - 59)); + + float temp = 0.1f * armor / (8.5f * levelModifier + 40); + temp = temp / (1 + temp); + + if (temp > 0.75f) + return 75.f; + + if (temp < 0.f) + return 0.f; + + return temp * 100; +} + +/*static*/ uint32 Unit::CalcArmorReducedDamage(Unit const* attacker, Unit* victim, const uint32 damage, SpellInfo const* spellInfo, WeaponAttackType /*attackType*/, uint8 attackerLevel /*= 0*/) +{ + float armor = float(victim->GetArmor()); + + if (attacker) + { + // bypass enemy armor by SPELL_AURA_BYPASS_ARMOR_FOR_CASTER + int32 armorBypassPct = victim->GetTotalAuraModifier(SPELL_AURA_BYPASS_ARMOR_FOR_CASTER, [&](AuraEffect const* aurEff) + { + return aurEff->GetCasterGUID() == attacker->GetGUID(); + }); + + armor = CalculatePct(armor, 100 - std::min(armorBypassPct, 100)); + + // Ignore enemy armor by SPELL_AURA_MOD_TARGET_RESISTANCE aura + armor += attacker->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_TARGET_RESISTANCE, SPELL_SCHOOL_MASK_NORMAL); + + if (spellInfo) + if (Player* modOwner = attacker->GetSpellModOwner()) + modOwner->ApplySpellMod(spellInfo, SpellModOp::TargetResistance, armor); + + for (AuraEffect const* aurEff : attacker->GetAuraEffectsByType(SPELL_AURA_MOD_IGNORE_TARGET_RESIST)) + { + if (aurEff->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL) + armor = std::floor(AddPct(armor, -aurEff->GetAmount())); + } + + if (armor < 0.0f) + armor = 0.0f; + } + + float armorReduction = 100.f - GetArmorReduction(armor, attacker ? attacker->getLevel() : attackerLevel); + return std::max(std::ceil(damage * armorReduction / 100.f), 0); +} + +/*static*/ uint32 Unit::CalcSpellResistedDamage(Unit const* attacker, Unit* victim, uint32 damage, SpellSchoolMask schoolMask, SpellInfo const* spellInfo) +{ + // Magic damage, check for resists + if (!(schoolMask & SPELL_SCHOOL_MASK_MAGIC)) + return 0; + + // Npcs can have holy resistance + if ((schoolMask & SPELL_SCHOOL_MASK_HOLY) && victim->GetTypeId() != TYPEID_UNIT) + return 0; + + float const averageResist = Unit::CalculateAverageResistReduction(attacker, schoolMask, victim, spellInfo); + + float discreteResistProbability[11] = { }; + if (averageResist <= 0.1f) + { + discreteResistProbability[0] = 1.0f - 7.5f * averageResist; + discreteResistProbability[1] = 5.0f * averageResist; + discreteResistProbability[2] = 2.5f * averageResist; + } + else + { + for (uint32 i = 0; i < 11; ++i) + discreteResistProbability[i] = std::max(0.5f - 2.5f * std::fabs(0.1f * i - averageResist), 0.0f); + } + + float roll = rand_norm(); + float probabilitySum = 0.0f; + + uint32 resistance = 0; + for (; resistance < 11; ++resistance) + if (roll < (probabilitySum += discreteResistProbability[resistance])) + break; + + float damageResisted = damage * resistance / 10.f; + if (damageResisted > 0.0f) // if any damage was resisted + { + if (attacker) + { + int32 ignoredResistance = attacker->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_IGNORE_TARGET_RESIST, schoolMask); + ignoredResistance = std::min(ignoredResistance, 100); + ApplyPct(damageResisted, 100 - ignoredResistance); + + // Spells with melee and magic school mask, decide whether resistance or armor absorb is higher + if (spellInfo && spellInfo->HasAttribute(SPELL_ATTR0_CU_SCHOOLMASK_NORMAL_WITH_MAGIC)) + { + uint32 damageAfterArmor = Unit::CalcArmorReducedDamage(attacker, victim, damage, spellInfo, spellInfo->GetAttackType()); + float armorReduction = damage - damageAfterArmor; + + // pick the lower one, the weakest resistance counts + damageResisted = std::min(damageResisted, armorReduction); + } + } + } + + damageResisted = std::max(damageResisted, 0.f); + return uint32(damageResisted); +} + +/*static*/ float Unit::CalculateAverageResistReduction(WorldObject const* caster, SpellSchoolMask schoolMask, Unit const* victim, SpellInfo const* spellInfo) +{ + float victimResistance = float(victim->GetResistance(schoolMask)); + if (caster) + { + // pets inherit 100% of masters penetration + if (Player const* player = caster->GetSpellModOwner()) + { + victimResistance += float(player->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_TARGET_RESISTANCE, schoolMask)); + victimResistance -= float(player->GetSpellPenetrationItemMod()); + } + else if (Unit const* unitCaster = caster->ToUnit()) + victimResistance += float(unitCaster->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_TARGET_RESISTANCE, schoolMask)); + } + + // holy resistance exists in pve and comes from level difference, ignore template values + if (schoolMask & SPELL_SCHOOL_MASK_HOLY) + victimResistance = 0.0f; + + // Chaos Bolt exception, ignore all target resistances (unknown attribute?) + if (spellInfo && spellInfo->SpellFamilyName == SPELLFAMILY_WARLOCK && spellInfo->SpellIconID == 3178) + victimResistance = 0.0f; + + victimResistance = std::max(victimResistance, 0.0f); + + // level-based resistance does not apply to binary spells, and cannot be overcome by spell penetration + // gameobject caster -- should it have level based resistance? + if (caster && caster->GetTypeId() != TYPEID_GAMEOBJECT && (!spellInfo || !spellInfo->HasAttribute(SPELL_ATTR0_CU_BINARY_SPELL))) + victimResistance += std::max((float(victim->getLevelForTarget(caster)) - float(caster->getLevelForTarget(victim))) * 5.0f, 0.0f); + + static uint32 const BOSS_LEVEL = 83; + static float const BOSS_RESISTANCE_CONSTANT = 510.0f; + uint32 level = victim->getLevel(); + float resistanceConstant = 0.0f; + + if (level == BOSS_LEVEL) + resistanceConstant = BOSS_RESISTANCE_CONSTANT; + else + resistanceConstant = level * 5.0f; + + return victimResistance / (victimResistance + resistanceConstant); +} + +/*static*/ void Unit::CalcAbsorbResist(DamageInfo& damageInfo) +{ + if (!damageInfo.GetVictim() || !damageInfo.GetVictim()->IsAlive() || !damageInfo.GetDamage()) + return; + + uint32 resistedDamage = Unit::CalcSpellResistedDamage(damageInfo.GetAttacker(), damageInfo.GetVictim(), damageInfo.GetDamage(), damageInfo.GetSchoolMask(), damageInfo.GetSpellInfo()); + damageInfo.ResistDamage(resistedDamage); + + // Ignore Absorption Auras + float auraAbsorbMod = 0.f; + if (Unit* attacker = damageInfo.GetAttacker()) + { + auraAbsorbMod = attacker->GetMaxPositiveAuraModifierByMiscMask(SPELL_AURA_MOD_TARGET_ABSORB_SCHOOL, damageInfo.GetSchoolMask()); + auraAbsorbMod = std::max(auraAbsorbMod, static_cast(attacker->GetMaxPositiveAuraModifier(SPELL_AURA_MOD_TARGET_ABILITY_ABSORB_SCHOOL, [&damageInfo](AuraEffect const* aurEff) -> bool + { + if (!(aurEff->GetMiscValue() & damageInfo.GetSchoolMask())) + return false; + + if (!aurEff->IsAffectingSpell(damageInfo.GetSpellInfo())) + return false; + + return true; + }))); + } + + RoundToInterval(auraAbsorbMod, 0.0f, 100.0f); + + int32 absorbIgnoringDamage = CalculatePct(damageInfo.GetDamage(), auraAbsorbMod); + + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + AuraEffectList vSchoolAbsorbCopy(damageInfo.GetVictim()->GetAuraEffectsByType(SPELL_AURA_SCHOOL_ABSORB)); + vSchoolAbsorbCopy.sort(Trinity::AbsorbAuraOrderPred()); + + // absorb without mana cost + for (AuraEffectList::iterator itr = vSchoolAbsorbCopy.begin(); (itr != vSchoolAbsorbCopy.end()) && (damageInfo.GetDamage() > 0); ++itr) + { + AuraEffect* absorbAurEff = *itr; + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication const* aurApp = absorbAurEff->GetBase()->GetApplicationOfTarget(damageInfo.GetVictim()->GetGUID()); + if (!aurApp) + continue; + if (!(absorbAurEff->GetMiscValue() & damageInfo.GetSchoolMask())) + continue; + + // get amount which can be still absorbed by the aura + int32 currentAbsorb = absorbAurEff->GetAmount(); + // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety + if (currentAbsorb < 0) + currentAbsorb = 0; + + if (!absorbAurEff->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE)) + damageInfo.ModifyDamage(-absorbIgnoringDamage); + + uint32 tempAbsorb = uint32(currentAbsorb); + + bool defaultPrevented = false; + + absorbAurEff->GetBase()->CallScriptEffectAbsorbHandlers(absorbAurEff, aurApp, damageInfo, tempAbsorb, defaultPrevented); + currentAbsorb = tempAbsorb; + + if (!defaultPrevented) + { + + // absorb must be smaller than the damage itself + currentAbsorb = RoundToInterval(currentAbsorb, 0, int32(damageInfo.GetDamage())); + + damageInfo.AbsorbDamage(currentAbsorb); + + tempAbsorb = currentAbsorb; + absorbAurEff->GetBase()->CallScriptEffectAfterAbsorbHandlers(absorbAurEff, aurApp, damageInfo, tempAbsorb); + + // Check if our aura is using amount to count damage + if (absorbAurEff->GetAmount() >= 0) + { + // Reduce shield amount + absorbAurEff->SetAmount(absorbAurEff->GetAmount() - currentAbsorb); + // Aura cannot absorb anything more - remove it + if (absorbAurEff->GetAmount() <= 0) + absorbAurEff->GetBase()->Remove(AuraRemoveFlags::ByEnemySpell); + } + } + + if (!absorbAurEff->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE)) + damageInfo.ModifyDamage(absorbIgnoringDamage); + } + + // absorb by mana cost + AuraEffectList vManaShieldCopy(damageInfo.GetVictim()->GetAuraEffectsByType(SPELL_AURA_MANA_SHIELD)); + for (AuraEffectList::const_iterator itr = vManaShieldCopy.begin(); (itr != vManaShieldCopy.end()) && (damageInfo.GetDamage() > 0); ++itr) + { + AuraEffect* absorbAurEff = *itr; + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication const* aurApp = absorbAurEff->GetBase()->GetApplicationOfTarget(damageInfo.GetVictim()->GetGUID()); + if (!aurApp) + continue; + // check damage school mask + if (!(absorbAurEff->GetMiscValue() & damageInfo.GetSchoolMask())) + continue; + + // get amount which can be still absorbed by the aura + int32 currentAbsorb = absorbAurEff->GetAmount(); + // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety + if (currentAbsorb < 0) + currentAbsorb = 0; + + if (!absorbAurEff->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE)) + damageInfo.ModifyDamage(-absorbIgnoringDamage); + + uint32 tempAbsorb = currentAbsorb; + + bool defaultPrevented = false; + + absorbAurEff->GetBase()->CallScriptEffectManaShieldHandlers(absorbAurEff, aurApp, damageInfo, tempAbsorb, defaultPrevented); + currentAbsorb = tempAbsorb; + + if (!defaultPrevented) + { + + // absorb must be smaller than the damage itself + currentAbsorb = RoundToInterval(currentAbsorb, 0, int32(damageInfo.GetDamage())); + + int32 manaReduction = currentAbsorb; + + // lower absorb amount by talents + if (float manaMultiplier = absorbAurEff->GetSpellInfo()->Effects[absorbAurEff->GetEffIndex()].CalcValueMultiplier(absorbAurEff->GetCaster())) + manaReduction = int32(float(manaReduction) * manaMultiplier); + + int32 manaTaken = -damageInfo.GetVictim()->ModifyPower(POWER_MANA, -manaReduction); + + // take case when mana has ended up into account + currentAbsorb = currentAbsorb ? int32(float(currentAbsorb) * (float(manaTaken) / float(manaReduction))) : 0; + + damageInfo.AbsorbDamage(currentAbsorb); + + tempAbsorb = currentAbsorb; + absorbAurEff->GetBase()->CallScriptEffectAfterManaShieldHandlers(absorbAurEff, aurApp, damageInfo, tempAbsorb); + + // Check if our aura is using amount to count damage + if (absorbAurEff->GetAmount() >= 0) + { + absorbAurEff->SetAmount(absorbAurEff->GetAmount() - currentAbsorb); + if ((absorbAurEff->GetAmount() <= 0)) + absorbAurEff->GetBase()->Remove(AuraRemoveFlags::ByEnemySpell); + } + } + + if (!absorbAurEff->GetSpellInfo()->HasAttribute(SPELL_ATTR6_ABSORB_CANNOT_BE_IGNORE)) + damageInfo.ModifyDamage(absorbIgnoringDamage); + } + + // split damage auras - only when not damaging self + if (damageInfo.GetVictim() != damageInfo.GetAttacker()) + { + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + AuraEffectList vSplitDamagePctCopy(damageInfo.GetVictim()->GetAuraEffectsByType(SPELL_AURA_SPLIT_DAMAGE_PCT)); + for (AuraEffectList::iterator itr = vSplitDamagePctCopy.begin(); itr != vSplitDamagePctCopy.end() && damageInfo.GetDamage() > 0; ++itr) + { + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication const* aurApp = (*itr)->GetBase()->GetApplicationOfTarget(damageInfo.GetVictim()->GetGUID()); + if (!aurApp) + continue; + + // check damage school mask + if (!((*itr)->GetMiscValue() & damageInfo.GetSchoolMask())) + continue; + + // Damage can be splitted only if aura has an alive caster + Unit* caster = (*itr)->GetCaster(); + if (!caster || (caster == damageInfo.GetVictim()) || !caster->IsInWorld() || !caster->IsAlive()) + continue; + + uint32 splitDamage = CalculatePct(damageInfo.GetDamage(), (*itr)->GetAmount()); + + (*itr)->GetBase()->CallScriptEffectSplitHandlers((*itr), aurApp, damageInfo, splitDamage); + + // absorb must be smaller than the damage itself + splitDamage = RoundToInterval(splitDamage, uint32(0), uint32(damageInfo.GetDamage())); + + damageInfo.AbsorbDamage(splitDamage); + + // check if caster is immune to damage + if (caster->IsImmunedToDamage(damageInfo.GetSchoolMask())) + { + damageInfo.GetVictim()->SendSpellMiss(caster, (*itr)->GetSpellInfo()->Id, SPELL_MISS_IMMUNE); + continue; + } + + uint32 split_absorb = 0; + Unit::DealDamageMods(caster, splitDamage, &split_absorb); + + if (Unit* attacker = damageInfo.GetAttacker()) + { + // Sparring Checks + if (Creature* target = damageInfo.GetVictim()->ToCreature()) + if (attacker->IsCreature() && !attacker->IsCharmedOwnedByPlayerOrPlayer()) + if (target->GetNoNpcDamageBelowPctHealthValue() != 0.0f) + if (target->GetHealthPct() <= target->GetNoNpcDamageBelowPctHealthValue()) + damageInfo.ModifyDamage(damageInfo.GetDamage() * -1); + + attacker->SendSpellNonMeleeDamageLog(caster, (*itr)->GetSpellInfo()->Id, splitDamage, damageInfo.GetSchoolMask(), split_absorb, 0, damageInfo.GetDamageType() == DOT, 0, false, true); + } + + Unit::DealDamage(damageInfo.GetAttacker(), caster, splitDamage, 0, DIRECT_DAMAGE, damageInfo.GetSchoolMask(), (*itr)->GetSpellInfo(), false); + + // break 'Fear' and similar auras + Unit::ProcSkillsAndAuras(damageInfo.GetAttacker(), caster, PROC_FLAG_NONE, PROC_FLAG_TAKE_HARMFUL_SPELL, PROC_SPELL_TYPE_DAMAGE, PROC_SPELL_PHASE_HIT, PROC_HIT_NONE, nullptr, &damageInfo, nullptr); + } + } +} + +/*static*/ void Unit::CalcHealAbsorb(HealInfo& healInfo) +{ + if (!healInfo.GetHeal()) + return; + + int32 const healing = static_cast(healInfo.GetHeal()); + int32 absorbAmount = 0; + + // Need remove expired auras after + bool existExpired = false; + + // absorb without mana cost + AuraEffectList const& vHealAbsorb = healInfo.GetTarget()->GetAuraEffectsByType(SPELL_AURA_SCHOOL_HEAL_ABSORB); + for (AuraEffectList::const_iterator i = vHealAbsorb.begin(); i != vHealAbsorb.end() && absorbAmount <= healing; ++i) + { + if (!((*i)->GetMiscValue() & healInfo.GetSpellInfo()->SchoolMask)) + continue; + + // Max Amount can be absorbed by this aura + int32 currentAbsorb = (*i)->GetAmount(); + + // Found empty aura (impossible but..) + if (currentAbsorb <= 0) + { + existExpired = true; + continue; + } + + // currentAbsorb - damage can be absorbed by shield + // If need absorb less damage + if (healing < currentAbsorb + absorbAmount) + currentAbsorb = healing - absorbAmount; + + absorbAmount += currentAbsorb; + + // Reduce shield amount + (*i)->SetAmount((*i)->GetAmount() - currentAbsorb); + // Need remove it later + if ((*i)->GetAmount() <= 0) + existExpired = true; + } + + // Remove all expired absorb auras + if (existExpired) + { + for (AuraEffectList::const_iterator i = vHealAbsorb.begin(); i != vHealAbsorb.end();) + { + AuraEffect* auraEff = *i; + ++i; + if (auraEff->GetAmount() <= 0) + { + uint32 removedAuras = healInfo.GetTarget()->m_removedAurasCount; + auraEff->GetBase()->Remove(AuraRemoveFlags::ByEnemySpell); + if (removedAuras + 1 < healInfo.GetTarget()->m_removedAurasCount) + i = vHealAbsorb.begin(); + } + } + } + + if (absorbAmount > 0) + healInfo.AbsorbHeal(absorbAmount); +} + +void Unit::AttackerStateUpdate(Unit* victim, WeaponAttackType attType, bool extra) +{ + if (HasUnitState(UNIT_STATE_CANNOT_AUTOATTACK) || HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PACIFIED)) + return; + + if (HasUnitState(UNIT_STATE_CASTING)) + { + Spell* channeledSpell = GetCurrentSpell(CURRENT_CHANNELED_SPELL); + if (!channeledSpell || !channeledSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR5_ALLOW_ACTIONS_DURING_CHANNEL)) + return; + } + + if (!victim->IsAlive()) + return; + + if ((attType == BASE_ATTACK || attType == OFF_ATTACK) && !IsWithinLOSInMap(victim)) + return; + + AtTargetAttacked(victim, true); + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Attacking); + + if (attType != BASE_ATTACK && attType != OFF_ATTACK) + return; // ignore ranged case + + // melee attack spell cast at main hand attack only - no normal melee dmg dealt + if (attType == BASE_ATTACK && m_currentSpells[CURRENT_MELEE_SPELL] && !extra) + m_currentSpells[CURRENT_MELEE_SPELL]->cast(); + else + { + // attack can be redirected to another target + victim = GetMeleeHitRedirectTarget(victim); + + AuraEffectList const& meleeAttackOverrides = GetAuraEffectsByType(SPELL_AURA_OVERRIDE_AUTOATTACK_WITH_MELEE_SPELL); + AuraEffect const* meleeAttackAuraEffect = nullptr; + uint32 meleeAttackSpellId = 0; + if (attType == BASE_ATTACK) + { + if (!meleeAttackOverrides.empty()) + { + meleeAttackAuraEffect = meleeAttackOverrides.front(); + meleeAttackSpellId = meleeAttackAuraEffect->GetSpellInfo()->Effects[meleeAttackAuraEffect->GetEffIndex()].TriggerSpell; + } + } + else + { + auto itr = std::find_if(meleeAttackOverrides.begin(), meleeAttackOverrides.end(), [&](AuraEffect const* aurEff) + { + return aurEff->GetAmount() != 0; + }); + + if (itr != meleeAttackOverrides.end()) + { + meleeAttackAuraEffect = *itr; + meleeAttackSpellId = meleeAttackAuraEffect->GetAmount(); + } + } + + if (!meleeAttackAuraEffect) + { + CalcDamageInfo damageInfo; + CalculateMeleeDamage(victim, &damageInfo, attType); + // Send log damage message to client + Unit::DealDamageMods(victim, damageInfo.Damage, &damageInfo.Absorb); + + // Sparring Checks + if (Creature* target = victim->ToCreature()) + if (IsCreature() && !IsCharmedOwnedByPlayerOrPlayer()) + if (target->GetNoNpcDamageBelowPctHealthValue() != 0.0f) + if (target->GetHealthPct() <= target->GetNoNpcDamageBelowPctHealthValue()) + damageInfo.HitInfo |= HITINFO_FAKE_DAMAGE; + + // Rage reward + if (this != victim && damageInfo.HitOutCome != MELEE_HIT_MISS && GetPowerType() == POWER_RAGE) + { + if (uint32 rageReward = CalcMeleeAttackRageGain(this, victim, attType)) + { + damageInfo.HitInfo |= HITINFO_RAGE_GAIN; + damageInfo.RageGained = RewardRage(rageReward, true); + } + } + + SendAttackStateUpdate(&damageInfo); + + DealMeleeDamage(&damageInfo, true); + + DamageInfo dmgInfo(damageInfo); + Unit::ProcSkillsAndAuras(damageInfo.Attacker, damageInfo.Target, damageInfo.ProcAttacker, damageInfo.ProcVictim, PROC_SPELL_TYPE_NONE, PROC_SPELL_PHASE_NONE, dmgInfo.GetHitMask(), nullptr, &dmgInfo, nullptr); + + TC_LOG_DEBUG("entities.unit", "AttackerStateUpdate: %s attacked %s for %u dmg, absorbed %u, blocked %u, resisted %u.", + GetGUID().ToString().c_str(), victim->GetGUID().ToString().c_str(), damageInfo.Damage, damageInfo.Absorb, damageInfo.Blocked, damageInfo.Resist); + } + else + { + CastSpell(victim, meleeAttackSpellId, meleeAttackAuraEffect); + + uint32 hitInfo = HITINFO_AFFECTS_VICTIM | HITINFO_NO_ANIMATION; + if (attType == OFF_ATTACK) + hitInfo |= HITINFO_OFFHAND; + + SendAttackStateUpdate(hitInfo, victim, 0, GetMeleeDamageSchoolMask(), 0, 0, 0, VICTIMSTATE_HIT, 0); + } + } +} + +void Unit::HandleProcExtraAttackFor(Unit* victim) +{ + while (m_extraAttacks) + { + AttackerStateUpdate(victim, BASE_ATTACK, true); + --m_extraAttacks; + } +} + +MeleeHitOutcome Unit::RollMeleeOutcomeAgainst(const Unit* victim, WeaponAttackType attType) const +{ + if (victim->IsCreature() && victim->ToCreature()->IsInEvadeMode()) + return MELEE_HIT_EVADE; + + int32 const attackerMaxSkillValueForLevel = GetMaxSkillValueForLevel(victim); + int32 const victimMaxSkillValueForLevel = victim->GetMaxSkillValueForLevel(this); + + // Miss chance based on melee + int32 miss_chance = int32(MeleeSpellMissChance(victim, attType) * 100.f); + + // Critical hit chance + int32 crit_chance = int32(GetUnitCriticalChanceAgainst(attType, victim) * 100.0f); + + // stunned target cannot dodge and this is check in GetUnitDodgeChance() (returned 0 in this case) + int32 dodge_chance = int32(GetUnitDodgeChance(attType, victim) * 100.0f); + int32 block_chance = int32(GetUnitBlockChance(victim) * 100.0f); + int32 parry_chance = int32(GetUnitParryChance(attType, victim) * 100.0f); + + + // melee attack table implementation + // outcome priority: + // 1. > 2. > 3. > 4. > 5. > 6. > 7. > 8. + // MISS > DODGE > PARRY > GLANCING > BLOCK > CRIT > CRUSHING > HIT + + int32 sum = 0, tmp = 0; + int32 roll = urand(0, 9999); + + float arcAngle = static_cast(M_PI); + if (victim->IsCreature() && victim->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_5_240_DEGREE_BACK_ARC)) + arcAngle -= static_cast(M_PI / 3); + + // check if attack comes from behind, nobody can parry or block if attacker is behind + bool canParryOrBlock = victim->HasInArc(arcAngle, this) || victim->HasAuraType(SPELL_AURA_IGNORE_HIT_DIRECTION); + + // only creatures can dodge if attacker is behind + bool canDodge = victim->GetTypeId() != TYPEID_PLAYER || canParryOrBlock; + + // if victim is casting or cc'd it can't avoid attacks + if (victim->IsNonMeleeSpellCast(false) || victim->HasUnitState(UNIT_STATE_CONTROLLED)) + { + canDodge = false; + canParryOrBlock = false; + } + + // 1. MISS + tmp = miss_chance; + if (tmp > 0 && roll < (sum += tmp)) + return MELEE_HIT_MISS; + + // always crit against a sitting target (except 0 crit chance) + if (victim->IsPlayer() && crit_chance > 0 && !victim->IsStandState()) + return MELEE_HIT_CRIT; + + // 2. DODGE + if (canDodge) + { + tmp = dodge_chance; + if (tmp > 0 // check if unit _can_ dodge + && roll < (sum += tmp)) + return MELEE_HIT_DODGE; + } + + // 3. PARRY + if (canParryOrBlock) + { + tmp = parry_chance; + if (tmp > 0 // check if unit _can_ parry + && roll < (sum += tmp)) + return MELEE_HIT_PARRY; + } + + // 4. GLANCING + // Max 40% chance to score a glancing blow against mobs of the same or higher level (only players and pets, not for ranged weapons). + if ((GetTypeId() == TYPEID_PLAYER || IsPet()) && + victim->GetTypeId() != TYPEID_PLAYER && !victim->IsPet() && + getLevel() <= victim->getLevelForTarget(this)) + { + // cap possible value (with bonuses > max skill) + int32 skill = attackerMaxSkillValueForLevel; + + // against boss-level targets - 24% chance of 25% average damage reduction (damage reduction range : 20-30%) + // against level 82 elites - 18% chance of 15% average damage reduction (damage reduction range : 10-20%) + tmp = 600 + (victimMaxSkillValueForLevel - skill) * 120; + tmp = std::min(tmp, 4000); + if (tmp > 0 && roll < (sum += tmp)) + return MELEE_HIT_GLANCING; + } + + // 5. BLOCK + if (canParryOrBlock) + { + tmp = block_chance; + if (tmp > 0 // check if unit _can_ block + && roll < (sum += tmp)) + return MELEE_HIT_BLOCK; + } + + // 6.CRIT + tmp = crit_chance; + if (tmp > 0 && roll < (sum += tmp)) + return MELEE_HIT_CRIT; + + // 7. CRUSHING + // mobs can score crushing blows if they're 4 or more levels above victim + if (getLevelForTarget(victim) >= victim->getLevelForTarget(this) + 4 && + // can be from by creature (if can) or from controlled player that considered as creature + !IsControlledByPlayer() && + !(IsCreature() && ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_CRUSH)) + { + // when their weapon skill is 15 or more above victim's defense skill + tmp = victimMaxSkillValueForLevel; + // tmp = mob's level * 5 - player's current defense skill + tmp = attackerMaxSkillValueForLevel - tmp; + // minimum of 20 points diff (4 levels difference) + tmp = std::max(tmp, 20); + + // add 2% chance per lacking skill point, min. is 15% + tmp = tmp * 200 - 1500; + if (roll < (sum += tmp)) + return MELEE_HIT_CRUSHING; + } + + // 8. HIT + return MELEE_HIT_NORMAL; +} + +uint32 Unit::CalculateDamage(WeaponAttackType attType, bool normalized, bool addTotalPct) const +{ + float minDamage = 0.0f; + float maxDamage = 0.0f; + + if (normalized || !addTotalPct) + CalculateMinMaxDamage(attType, normalized, addTotalPct, minDamage, maxDamage); + else + { + switch (attType) + { + case RANGED_ATTACK: + minDamage = GetFloatValue(UNIT_FIELD_MINRANGEDDAMAGE); + maxDamage = GetFloatValue(UNIT_FIELD_MAXRANGEDDAMAGE); + break; + case BASE_ATTACK: + minDamage = GetFloatValue(UNIT_FIELD_MINDAMAGE); + maxDamage = GetFloatValue(UNIT_FIELD_MAXDAMAGE); + break; + case OFF_ATTACK: + minDamage = GetFloatValue(UNIT_FIELD_MINOFFHANDDAMAGE); + maxDamage = GetFloatValue(UNIT_FIELD_MAXOFFHANDDAMAGE); + break; + default: + break; + } + } + + minDamage = std::max(0.f, minDamage); + maxDamage = std::max(0.f, maxDamage); + + if (minDamage > maxDamage) + std::swap(minDamage, maxDamage); + + return urand(uint32(minDamage), uint32(maxDamage)); +} + +void Unit::SendMeleeAttackStart(Unit* victim) +{ + WorldPackets::Combat::AttackStart packet; + packet.Attacker = GetGUID(); + packet.Victim = victim->GetGUID(); + SendMessageToSet(packet.Write(), IsPlayer()); +} + +void Unit::SendMeleeAttackStop(Unit* victim) +{ + SendMessageToSet(WorldPackets::Combat::SAttackStop(this, victim).Write(), IsPlayer()); + + if (victim) + TC_LOG_DEBUG("entities.unit", "%s %u stopped attacking %s %u", (GetTypeId() == TYPEID_PLAYER ? "Player" : "Creature"), GetGUID().GetCounter(), (victim->GetTypeId() == TYPEID_PLAYER ? "player" : "creature"), victim->GetGUID().GetCounter()); + else + TC_LOG_DEBUG("entities.unit", "%s %u stopped attacking", (GetTypeId() == TYPEID_PLAYER ? "Player" : "Creature"), GetGUID().GetCounter()); +} + +bool Unit::IsBlockCritical() const +{ + if (roll_chance_i(GetTotalAuraModifier(SPELL_AURA_MOD_BLOCK_CRIT_CHANCE))) + return true; + return false; +} + +int32 Unit::GetMechanicResistChance(SpellInfo const* spellInfo) const +{ + if (!spellInfo) + return 0; + + int32 resistMech = 0; + for (uint8 eff = 0; eff < MAX_SPELL_EFFECTS; ++eff) + { + if (!spellInfo->Effects[eff].IsEffect()) + break; + + int32 effectMech = spellInfo->GetEffectMechanic(eff); + if (effectMech) + { + int32 temp = GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_MECHANIC_RESISTANCE, effectMech); + if (resistMech < temp) + resistMech = temp; + } + } + return resistMech; +} + +bool Unit::CanUseAttackType(uint8 attacktype) const +{ + switch (attacktype) + { + case BASE_ATTACK: + return !HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_DISARMED); + case OFF_ATTACK: + return !HasFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_DISARM_OFFHAND); + case RANGED_ATTACK: + return !HasFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_DISARM_RANGED); + default: + return true; + } +} + +// Melee based spells hit result calculations +SpellMissInfo Unit::MeleeSpellHitResult(Unit* victim, SpellInfo const* spellInfo) const +{ + if (spellInfo->HasAttribute(SPELL_ATTR3_NO_AVOIDANCE)) + return SPELL_MISS_NONE; + + WeaponAttackType attType = BASE_ATTACK; + + // Check damage class instead of attack type to correctly handle judgements + // - they are meele, but can't be dodged/parried/deflected because of ranged dmg class + if (spellInfo->DmgClass == SPELL_DAMAGE_CLASS_RANGED) + attType = RANGED_ATTACK; + + uint32 roll = urand(0, 9999); + uint32 missChance = uint32(MeleeSpellMissChance(victim, attType, spellInfo) * 100.0f); + + // Roll miss + uint32 tmp = missChance; + if (roll < tmp) + return SPELL_MISS_MISS; + + // Chance resist mechanic + int32 resist_chance = victim->GetMechanicResistChance(spellInfo) * 100; + tmp += resist_chance; + if (roll < tmp) + return SPELL_MISS_RESIST; + + // Same spells cannot be parried/dodged + if (spellInfo->HasAttribute(SPELL_ATTR0_NO_ACTIVE_DEFENSE)) + return SPELL_MISS_NONE; + + bool canDodge = !spellInfo->HasAttribute(SPELL_ATTR7_NO_ATTACK_DODGE); + bool canParry = !spellInfo->HasAttribute(SPELL_ATTR7_NO_ATTACK_PARRY); + bool canBlock = !spellInfo->HasAttribute(SPELL_ATTR8_NO_ATTACK_BLOCK); + + // if victim is casting or cc'd it can't avoid attacks + bool canUseDefenseWhileChanneling = victim->GetCurrentSpell(CURRENT_CHANNELED_SPELL) ? victim->GetCurrentSpell(CURRENT_CHANNELED_SPELL)->GetSpellInfo()->HasAttribute(SPELL_ATTR10_ALLOW_DEFENSE_WHILE_CHANNELING) : false; + bool canUseDefenseWhileCasting = victim->GetCurrentSpell(CURRENT_GENERIC_SPELL) ? victim->GetCurrentSpell(CURRENT_GENERIC_SPELL)->GetSpellInfo()->HasAttribute(SPELL_ATTR10_ALLOW_DEFENSE_WHILE_CASTING) : false; + if ((victim->IsNonMeleeSpellCast(false) || victim->HasUnitState(UNIT_STATE_CONTROLLED)) && !canUseDefenseWhileChanneling && !canUseDefenseWhileCasting) + { + canDodge = false; + canParry = false; + canBlock = false; + } + + // Ranged attacks can only miss, resist and deflect and get blocked + if (attType == RANGED_ATTACK) + { + canDodge = false; + canParry = false; + + // only if in front + if (!victim->HasUnitState(UNIT_STATE_CONTROLLED) && (victim->HasInArc(float(M_PI), this) || victim->HasAuraType(SPELL_AURA_IGNORE_HIT_DIRECTION))) + { + int32 deflect_chance = victim->GetTotalAuraModifier(SPELL_AURA_DEFLECT_SPELLS) * 100; + tmp += deflect_chance; + if (roll < tmp) + return SPELL_MISS_DEFLECT; + } + } + + // Check for attack from behind + if (!victim->HasInArc(float(M_PI), this)) + { + if (!victim->HasAuraType(SPELL_AURA_IGNORE_HIT_DIRECTION)) + { + // Can't dodge from behind in PvP (but its possible in PvE) + if (victim->GetTypeId() == TYPEID_PLAYER) + canDodge = false; + // Can't parry or block + canParry = false; + canBlock = false; + } + else // Only deterrence as of 3.3.5 + { + if (spellInfo->HasAttribute(SPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET)) + canParry = false; + } + } + + // Ignore combat result aura + AuraEffectList const& ignore = GetAuraEffectsByType(SPELL_AURA_IGNORE_COMBAT_RESULT); + for (AuraEffect const* aurEff : ignore) + { + if (!aurEff->IsAffectingSpell(spellInfo)) + continue; + + switch (aurEff->GetMiscValue()) + { + case MELEE_HIT_DODGE: + canDodge = false; + break; + case MELEE_HIT_BLOCK: + canBlock = false; + break; + case MELEE_HIT_PARRY: + canParry = false; + break; + default: + TC_LOG_DEBUG("entities.unit", "Spell %u SPELL_AURA_IGNORE_COMBAT_RESULT has unhandled state %d", aurEff->GetId(), aurEff->GetMiscValue()); + break; + } + } + + if (canDodge) + { + // Roll dodge + int32 dodgeChance = int32(GetUnitDodgeChance(attType, victim) * 100.0f); + if (dodgeChance < 0) + dodgeChance = 0; + + if (roll < (tmp += dodgeChance)) + return SPELL_MISS_DODGE; + } + + if (canParry) + { + // Roll parry + int32 parryChance = int32(GetUnitParryChance(attType, victim) * 100.0f); + if (parryChance < 0) + parryChance = 0; + + tmp += parryChance; + if (roll < tmp) + return SPELL_MISS_PARRY; + } + + if (canBlock) + { + int32 blockChance = int32(GetUnitBlockChance(victim) * 100.0f); + if (blockChance < 0) + blockChance = 0; + tmp += blockChance; + + if (roll < tmp) + return SPELL_MISS_BLOCK; + } + + return SPELL_MISS_NONE; +} + +float Unit::GetUnitDodgeChance(WeaponAttackType attType, Unit const* victim) const +{ + int32 attackerSkill = GetMaxSkillValueForLevel(nullptr); + int32 defenderSkill = victim->GetMaxSkillValueForLevel(nullptr); + int32 skillDifference = defenderSkill - attackerSkill; + + float chance = 0.0f; + + if (victim->IsPlayer()) + { + chance += victim->GetFloatValue(PLAYER_DODGE_PERCENTAGE); + chance += skillDifference * 0.04f; + } + else + { + if (!victim->IsTotem()) + { + chance += victim->GetTotalAuraModifier(SPELL_AURA_MOD_DODGE_PERCENT); + if (skillDifference <= 10) + chance = 5.f + skillDifference * 0.1f; + else + chance = 6.f + (skillDifference - 10) * 0.1f; + } + } + + // Reduce enemy dodge chance by SPELL_AURA_MOD_COMBAT_RESULT_CHANCE + chance += GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_COMBAT_RESULT_CHANCE, VICTIMSTATE_DODGE); + + // reduce dodge by SPELL_AURA_MOD_ENEMY_DODGE + chance += GetTotalAuraModifier(SPELL_AURA_MOD_ENEMY_DODGE); + + if (IsPlayer()) + chance -= ToPlayer()->GetExpertiseDodgeOrParryReduction(attType); + else + chance -= GetTotalAuraModifier(SPELL_AURA_MOD_EXPERTISE) / 4.0f; + + return std::clamp(chance, 0.f, 100.f); +} + +float Unit::GetUnitParryChance(WeaponAttackType attType, Unit const* victim) const +{ + int32 attackerSkill = GetMaxSkillValueForLevel(nullptr); + int32 defenderSkill = victim->GetMaxSkillValueForLevel(nullptr); + int32 skillDifference = defenderSkill - attackerSkill; + + float chance = 0.0f; + + if (Player const* playerVictim = victim->ToPlayer()) + { + if (playerVictim->CanParry()) + { + Item* tmpitem = playerVictim->GetWeaponForAttack(BASE_ATTACK, true); + if (!tmpitem) + tmpitem = playerVictim->GetWeaponForAttack(OFF_ATTACK, true); + + if (tmpitem) + chance = playerVictim->GetFloatValue(PLAYER_PARRY_PERCENTAGE); + + chance += skillDifference * 0.04f; + } + } + else + { + // Allow parries for creatures only if it's not a totem, does have a virtual item equipped and does not have CREATURE_FLAG_EXTRA_NO_PARRY + if (!victim->IsTotem() && (victim->GetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID + 0) || victim->GetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID + 1)) && + !(victim->ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_PARRY)) + { + chance += victim->GetTotalAuraModifier(SPELL_AURA_MOD_PARRY_PERCENT); + if (skillDifference <= 10) + chance = 5.f + skillDifference * 0.1f; + else + chance = 6.f + (skillDifference - 10) * 0.1f; + } + } + + if (IsPlayer()) + chance -= ToPlayer()->GetExpertiseDodgeOrParryReduction(attType); + else + chance -= GetTotalAuraModifier(SPELL_AURA_MOD_EXPERTISE) / 4.0f; + + return std::max(chance, 0.0f); +} + +float Unit::GetMeleeMissChance(Unit const* victim) const +{ + float chance = 0.f; + + // As of patch 4.0.1, weapon skills have been deprecated so we can go for plain defense skill values + uint16 attackerSkill = GetMaxSkillValueForLevel(nullptr); + uint16 defenderSkill = victim->GetMaxSkillValueForLevel(nullptr); + int32 skillDifference = defenderSkill - attackerSkill; + + if (victim->IsPlayer()) + { + chance = 5.f; + if (skillDifference > 0) + chance += skillDifference * 0.04f; + else if (skillDifference < 0) + chance -= skillDifference * 0.02f; + } + else + { + if (skillDifference <= 10) + chance = 5.f + skillDifference * 0.1f; + else + chance = 6.f + (skillDifference - 10) * 0.4f; + } + + return std::clamp(chance, 0.f, 100.f); +} + +float Unit::GetUnitBlockChance(Unit const* victim) const +{ + float chance = 0.f; + + if (Player const* playerVictim = victim->ToPlayer()) + { + if (playerVictim->CanBlock()) + { + Item* tmpitem = playerVictim->GetUseableItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (tmpitem && !tmpitem->IsBroken()) + chance = playerVictim->GetFloatValue(PLAYER_BLOCK_PERCENTAGE); + } + } + else + { + if (!victim->IsTotem() && !(victim->ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_BLOCK)) + { + chance = 5.0f; + chance += victim->GetTotalAuraModifier(SPELL_AURA_MOD_BLOCK_PERCENT); + } + } + + return std::max(chance, 0.0f); +} + +float Unit::GetUnitCriticalChanceDone(WeaponAttackType attackType) const +{ + float chance = 0.f; + if (GetTypeId() == TYPEID_PLAYER) + { + switch (attackType) + { + case BASE_ATTACK: + chance = GetFloatValue(PLAYER_CRIT_PERCENTAGE); + break; + case OFF_ATTACK: + chance = GetFloatValue(PLAYER_OFFHAND_CRIT_PERCENTAGE); + break; + case RANGED_ATTACK: + chance = GetFloatValue(PLAYER_RANGED_CRIT_PERCENTAGE); + break; + // Just for good manner + default: + chance = 0.0f; + break; + } + } + else + { + if (!(ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_NO_CRIT)) + { + chance = 5.0f; + chance += GetTotalAuraModifier(SPELL_AURA_MOD_WEAPON_CRIT_PERCENT); + chance += GetTotalAuraModifier(SPELL_AURA_MOD_CRIT_PCT); + } + } + + return chance; +} + +float Unit::GetUnitCriticalChanceTaken(WeaponAttackType attackType, float critDone) const +{ + float chance = critDone; + + // flat aura mods + if (attackType == RANGED_ATTACK) + chance += GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_CHANCE); + else + chance += GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_CHANCE); + + // applied after resilience + chance += GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE); + + return std::max(chance, 0.f); +} + +float Unit::GetUnitCriticalChanceAgainst(WeaponAttackType attackType, Unit const* victim) const +{ + float chance = GetUnitCriticalChanceDone(attackType); + return victim->GetUnitCriticalChanceTaken(attackType, chance); +} + +void Unit::_DeleteRemovedAuras() +{ + while (!m_removedAuras.empty()) + { + delete m_removedAuras.front(); + m_removedAuras.pop_front(); + } + + m_removedAurasCount = 0; +} + +void Unit::_UpdateSpells(uint32 time) +{ + m_spellHistory->Update(); + + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL]) + _UpdateAutoRepeatSpell(); + + // remove finished spells from current pointers + for (Spell*& spell : m_currentSpells) + { + if (spell && spell->getState() == SPELL_STATE_FINISHED) + { + spell->SetReferencedFromCurrent(false); + spell = nullptr; // remove pointer + } + } + + // m_auraUpdateIterator can be updated in indirect called code at aura remove to skip next planned to update but removed auras + for (m_auraUpdateIterator = m_ownedAuras.begin(); m_auraUpdateIterator != m_ownedAuras.end();) + { + Aura* i_aura = m_auraUpdateIterator->second; + ++m_auraUpdateIterator; // need shift to next for allow update if need into aura update + i_aura->UpdateOwner(time, this); + } + + // remove expired auras - do that after updates(used in scripts?) + for (AuraMap::iterator i = m_ownedAuras.begin(); i != m_ownedAuras.end();) + { + if (i->second->IsExpired()) + RemoveOwnedAura(i, AuraRemoveFlags::Expired); + else + ++i; + } + + for (VisibleAuraMap::iterator itr = m_visibleAuras.begin(); itr != m_visibleAuras.end(); ++itr) + if (itr->second->IsNeedClientUpdate()) + itr->second->ClientUpdate(); + + _DeleteRemovedAuras(); + + if (!m_gameObj.empty()) + { + GameObjectList::iterator itr; + for (itr = m_gameObj.begin(); itr != m_gameObj.end();) + { + if (!(*itr)->isSpawned()) + { + (*itr)->SetOwnerGUID(ObjectGuid::Empty); + (*itr)->SetRespawnTime(0); + (*itr)->Delete(); + m_gameObj.erase(itr++); + } + else + ++itr; + } + } +} + +void Unit::_UpdateAutoRepeatSpell() +{ + SpellInfo const* autoRepeatSpellInfo = m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->m_spellInfo; + + // check "realtime" interrupts + // don't cancel spells which are affected by a SPELL_AURA_CAST_WHILE_WALKING effect + if ((isMoving() && m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->CheckMovement() != SPELL_CAST_OK) || IsNonMeleeSpellCast(false, false, true, autoRepeatSpellInfo->Id == 75)) + { + // cancel wand shoot + if (autoRepeatSpellInfo->Id != 75) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL); + return; + } + + // castroutine + if (isAttackReady(RANGED_ATTACK) && m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->getState() != SPELL_STATE_PREPARING) + { + // Check if able to cast + SpellCastResult result = m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->CheckCast(true); + if (result != SPELL_CAST_OK) + { + if (autoRepeatSpellInfo->Id != 75) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL); + else if (GetTypeId() == TYPEID_PLAYER) + Spell::SendCastResult(ToPlayer(), autoRepeatSpellInfo, 1, result); + + return; + } + + // we want to shoot + Spell* spell = new Spell(this, autoRepeatSpellInfo, TRIGGERED_IGNORE_GCD); + spell->prepare(m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->m_targets); + } +} + +void Unit::SetCurrentCastSpell(Spell* spell) +{ + ASSERT(spell); // nullptr may be never passed here, use InterruptSpell or InterruptNonMeleeSpells + + CurrentSpellTypes CSpellType = spell->GetCurrentContainer(); + + if (spell == m_currentSpells[CSpellType]) // avoid breaking self + return; + + // special breakage effects: + switch (CSpellType) + { + case CURRENT_GENERIC_SPELL: + { + // generic spells always break channeled not delayed spells + if (m_currentSpells[CURRENT_CHANNELED_SPELL] && !m_currentSpells[CURRENT_CHANNELED_SPELL]->GetSpellInfo()->HasAttribute(SPELL_ATTR5_ALLOW_ACTIONS_DURING_CHANNEL)) + InterruptSpell(CURRENT_CHANNELED_SPELL, false); + + // autorepeat breaking + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL]) + { + // break autorepeat if not Auto Shot + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->GetSpellInfo()->Id != 75) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL); + } + if (spell->GetCastTime() > 0) + AddUnitState(UNIT_STATE_CASTING); + + break; + } + case CURRENT_CHANNELED_SPELL: + { + // channel spells always break generic non-delayed and any channeled spells + InterruptSpell(CURRENT_GENERIC_SPELL, false); + InterruptSpell(CURRENT_CHANNELED_SPELL); + + // it also does break autorepeat if not Auto Shot + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL] && + m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->GetSpellInfo()->Id != 75) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL); + AddUnitState(UNIT_STATE_CASTING); + + break; + } + case CURRENT_AUTOREPEAT_SPELL: + { + if (m_currentSpells[CSpellType] && m_currentSpells[CSpellType]->getState() == SPELL_STATE_IDLE) + m_currentSpells[CSpellType]->setState(SPELL_STATE_FINISHED); + + // only Auto Shoot does not break anything + if (spell->GetSpellInfo()->Id != 75) + { + // generic autorepeats break generic non-delayed and channeled non-delayed spells + InterruptSpell(CURRENT_GENERIC_SPELL, false); + InterruptSpell(CURRENT_CHANNELED_SPELL, false); + } + break; + } + default: + break; // other spell types don't break anything now + } + + // current spell (if it is still here) may be safely deleted now + if (m_currentSpells[CSpellType]) + m_currentSpells[CSpellType]->SetReferencedFromCurrent(false); + + // set new current spell + m_currentSpells[CSpellType] = spell; + spell->SetReferencedFromCurrent(true); + + spell->m_selfContainer = &(m_currentSpells[spell->GetCurrentContainer()]); +} + +void Unit::InterruptSpell(CurrentSpellTypes spellType, bool withDelayed, bool withInstant) +{ + Spell* spell = m_currentSpells[spellType]; + if (!spell) + return; + + if ((withDelayed || spell->getState() != SPELL_STATE_LAUNCHED) && + (withInstant || spell->GetCastTime() > 0 || spell->getState() == SPELL_STATE_CHANNELING)) + { + // for example, do not let self-stun aura interrupt itself + if (!spell->IsInterruptable()) + return; + + // send autorepeat cancel message for autorepeat spells + if (spellType == CURRENT_AUTOREPEAT_SPELL) + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->SendAutoRepeatCancel(this); + + if (spell->getState() != SPELL_STATE_FINISHED) + spell->cancel(); + else + { + m_currentSpells[spellType] = nullptr; + spell->SetReferencedFromCurrent(false); + } + + if (GetTypeId() == TYPEID_UNIT && IsAIEnabled()) + ToCreature()->AI()->OnSpellCastFinished(spell->GetSpellInfo(), SPELL_FINISHED_CANCELED); + } +} + +void Unit::FinishSpell(CurrentSpellTypes spellType, bool ok /*= true*/) +{ + Spell* spell = m_currentSpells[spellType]; + if (!spell) + return; + + if (spellType == CURRENT_CHANNELED_SPELL) + spell->SendChannelUpdate(0); + + spell->finish(ok); +} + +bool Unit::IsNonMeleeSpellCast(bool withDelayed, bool skipChanneled, bool skipAutorepeat, bool isAutoshoot, bool skipInstant) const +{ + // We don't do loop here to explicitly show that melee spell is excluded. + // Maybe later some special spells will be excluded too. + + // generic spells are cast when they are not finished and not delayed + if (m_currentSpells[CURRENT_GENERIC_SPELL] && + (m_currentSpells[CURRENT_GENERIC_SPELL]->getState() != SPELL_STATE_FINISHED) && + (withDelayed || m_currentSpells[CURRENT_GENERIC_SPELL]->getState() != SPELL_STATE_LAUNCHED)) + { + if (!skipInstant || m_currentSpells[CURRENT_GENERIC_SPELL]->GetCastTime()) + { + if (!isAutoshoot || !(m_currentSpells[CURRENT_GENERIC_SPELL]->m_spellInfo->HasAttribute(SPELL_ATTR2_DO_NOT_RESET_COMBAT_TIMERS))) + return true; + } + } + // channeled spells may be delayed, but they are still considered cast + if (!skipChanneled && m_currentSpells[CURRENT_CHANNELED_SPELL] && + (m_currentSpells[CURRENT_CHANNELED_SPELL]->getState() != SPELL_STATE_FINISHED)) + { + if (!isAutoshoot || !(m_currentSpells[CURRENT_CHANNELED_SPELL]->m_spellInfo->HasAttribute(SPELL_ATTR2_DO_NOT_RESET_COMBAT_TIMERS))) + return true; + } + // autorepeat spells may be finished or delayed, but they are still considered cast + if (!skipAutorepeat && m_currentSpells[CURRENT_AUTOREPEAT_SPELL]) + return true; + + return false; +} + +void Unit::InterruptNonMeleeSpells(bool withDelayed, uint32 spell_id, bool withInstant /* = true */) +{ + // generic spells are interrupted if they are not finished or delayed + if (m_currentSpells[CURRENT_GENERIC_SPELL] && (!spell_id || m_currentSpells[CURRENT_GENERIC_SPELL]->m_spellInfo->Id == spell_id)) + InterruptSpell(CURRENT_GENERIC_SPELL, withDelayed, withInstant); + + // autorepeat spells are interrupted if they are not finished or delayed + if (m_currentSpells[CURRENT_AUTOREPEAT_SPELL] && (!spell_id || m_currentSpells[CURRENT_AUTOREPEAT_SPELL]->m_spellInfo->Id == spell_id)) + InterruptSpell(CURRENT_AUTOREPEAT_SPELL, withDelayed, withInstant); + + // channeled spells are interrupted if they are not finished, even if they are delayed + if (m_currentSpells[CURRENT_CHANNELED_SPELL] && (!spell_id || m_currentSpells[CURRENT_CHANNELED_SPELL]->m_spellInfo->Id == spell_id)) + InterruptSpell(CURRENT_CHANNELED_SPELL, true, true); +} + +Spell* Unit::FindCurrentSpellBySpellId(uint32 spell_id) const +{ + for (Spell* spell : m_currentSpells) + if (spell && spell->m_spellInfo->Id == spell_id) + return spell; + return nullptr; +} + +int32 Unit::GetCurrentSpellCastTime(uint32 spell_id) const +{ + if (Spell const* spell = FindCurrentSpellBySpellId(spell_id)) + return spell->GetCastTime(); + return 0; +} + +bool Unit::IsMovementPreventedByCasting() const +{ + // can always move when not casting + if (!HasUnitState(UNIT_STATE_CASTING)) + return false; + + if (Spell* spell = m_currentSpells[CURRENT_GENERIC_SPELL]) + if (HasAuraTypeWithAffectMask(SPELL_AURA_CAST_WHILE_WALKING, spell->GetSpellInfo()) || spell->getState() == SPELL_STATE_FINISHED || + !spell->m_spellInfo->InterruptFlags.HasFlag(SpellInterruptFlags::Movement)) + return false; + + // channeled spells during channel stage (after the initial cast timer) allow movement with a specific spell attribute + if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL]) + if (spell->getState() != SPELL_STATE_FINISHED && spell->IsChannelActive()) + if (spell->GetSpellInfo()->IsMoveAllowedChannel() || HasAuraTypeWithAffectMask(SPELL_AURA_CAST_WHILE_WALKING, spell->GetSpellInfo())) + return false; + + // prohibit movement for all other spell casts + return true; +} + +bool Unit::isInFrontInMap(Unit const* target, float distance, float arc) const +{ + return IsWithinDistInMap(target, distance) && HasInArc(arc, target); +} + +bool Unit::isInBackInMap(Unit const* target, float distance, float arc) const +{ + return IsWithinDistInMap(target, distance) && !HasInArc(2 * float(M_PI) - arc, target); +} + +bool Unit::isInAccessiblePlaceFor(Creature const* c) const +{ + // Aquatic creatures are not allowed to leave liquids + if (!IsInWater() && c->IsAquatic()) + return false; + + // Underwater special case. Some creatures may not go below liquid surfaces + if (IsUnderWater() && c->CannotPenetrateWater()) + return false; + + // Water checks + if (IsInWater() && !c->CanEnterWater()) + return false; + + // Some creatures are tied to the ocean floor and cannot chase swimming targets. + if (!IsOnOceanFloor() && c->IsUnderWater() && c->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_CANT_SWIM)) + return false; + + return true; +} + +bool Unit::IsInWater() const +{ + return GetLiquidStatus() & (LIQUID_MAP_IN_WATER | LIQUID_MAP_UNDER_WATER); +} + +bool Unit::IsUnderWater() const +{ + return GetLiquidStatus() & LIQUID_MAP_UNDER_WATER; +} + +bool Unit::IsOnOceanFloor() const +{ + return GetLiquidStatus() & LIQUID_MAP_OCEAN_FLOOR; +} + +void Unit::ProcessPositionDataChanged(PositionFullTerrainStatus const& data) +{ + ZLiquidStatus oldLiquidStatus = GetLiquidStatus(); + WorldObject::ProcessPositionDataChanged(data); + ProcessTerrainStatusUpdate(oldLiquidStatus, data.liquidInfo); + + if (oldLiquidStatus != GetLiquidStatus()) + UpdateMountCapability(); +} + +void Unit::ProcessTerrainStatusUpdate(ZLiquidStatus /*oldLiquidStatus*/, Optional const& newLiquidData) +{ + if (!IsControlledByPlayer()) + return; + + // remove appropriate auras if we are swimming/not swimming respectively + if (IsInWater()) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::UnderWater); + else + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::AboveWater); + + // liquid aura handling + LiquidTypeEntry const* curLiquid = nullptr; + if (IsInWater() && newLiquidData) + curLiquid = sLiquidTypeStore.LookupEntry(newLiquidData->entry); + if (curLiquid != _lastLiquid) + { + if (_lastLiquid && _lastLiquid->SpellID) + RemoveAurasDueToSpell(_lastLiquid->SpellID); + Player* player = GetCharmerOrOwnerPlayerOrPlayerItself(); + + // Set _lastLiquid before casting liquid spell to avoid infinite loops + _lastLiquid = curLiquid; + + if (curLiquid && curLiquid->SpellID && (!player || !player->IsGameMaster())) + CastSpell(this, curLiquid->SpellID, true); + } +} +void Unit::DeMorph() +{ + SetDisplayId(GetNativeDisplayId()); +} + +Aura* Unit::_TryStackingOrRefreshingExistingAura(AuraCreateInfo& createInfo) +{ + ASSERT(createInfo.CasterGUID || createInfo.Caster); + + // Check if these can stack anyway + if (!createInfo.CasterGUID && !createInfo.GetSpellInfo()->IsStackableOnOneSlotWithDifferentCasters()) + createInfo.CasterGUID = createInfo.Caster->GetGUID(); + + // world gameobjects can't own auras and they send empty casterguid + // checked on sniffs with spell 22247 + if (createInfo.CasterGUID.IsGameObject()) + { + createInfo.Caster = nullptr; + createInfo.CasterGUID.Clear(); + } + + // passive and Incanter's Absorption and auras with different type can stack with themselves any number of times + if (!createInfo.GetSpellInfo()->IsMultiSlotAura()) + { + // check if cast item changed + ObjectGuid castItemGUID = createInfo.CastItemGUID; + + // find current aura from spell and change it's stackamount, or refresh it's duration + if (Aura* foundAura = GetOwnedAura(createInfo.GetSpellInfo()->Id, createInfo.GetSpellInfo()->IsStackableOnOneSlotWithDifferentCasters() ? ObjectGuid::Empty : createInfo.CasterGUID, createInfo.GetSpellInfo()->HasAttribute(SPELL_ATTR0_CU_ENCHANT_PROC) ? castItemGUID : ObjectGuid::Empty)) + { + // effect masks do not match + // extremely rare case + // let's just recreate aura + if (createInfo.GetAuraEffectMask() != foundAura->GetEffectMask()) + return nullptr; + + // update basepoints with new values - effect amount will be recalculated in ModStackAmount + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!foundAura->HasEffect(i)) + continue; + + int32 bp = foundAura->GetSpellInfo()->Effects[i].BasePoints; + if (createInfo.BaseAmount) + bp = *(createInfo.BaseAmount + i); + + int32* oldBP = const_cast(&(foundAura->GetEffect(i)->m_baseAmount)); + *oldBP = bp; + } + + // correct cast item guid if needed + if (castItemGUID != foundAura->GetCastItemGUID()) + { + ObjectGuid* oldGUID = const_cast(&foundAura->m_castItemGuid); + *oldGUID = castItemGUID; + } + + // try to increase stack amount + foundAura->ModStackAmount(1, AuraRemoveFlags::ByDefault, createInfo.ResetPeriodicTimer); + return foundAura; + } + } + + return nullptr; +} + +void Unit::_AddAura(UnitAura* aura, Unit* caster) +{ + ASSERT(!m_cleanupDone); + m_ownedAuras.emplace(aura->GetId(), aura); + + _RemoveNoStackAurasDueToAura(aura, true); + + if (aura->IsRemoved()) + return; + + aura->SetIsSingleTarget(caster && aura->GetSpellInfo()->IsSingleTarget()); + if (aura->IsSingleTarget()) + { + ASSERT((IsInWorld() && !IsDuringRemoveFromWorld()) || aura->GetCasterGUID() == GetGUID()); + /* @HACK: Player is not in world during loading auras. + * Single target auras are not saved or loaded from database + * but may be created as a result of aura links. + */ + + std::vector aurasSharingLimit; + // remove other single target auras + for (Aura* scAura : caster->GetSingleCastAuras()) + if (scAura->IsSingleTargetWith(aura)) + aurasSharingLimit.push_back(scAura); + + // register single target aura + caster->GetSingleCastAuras().push_front(aura); + + uint32 maxOtherAuras = aura->GetSpellInfo()->MaxAffectedTargets - 1; + while (aurasSharingLimit.size() > maxOtherAuras) + { + aurasSharingLimit.back()->Remove(); + aurasSharingLimit.pop_back(); + } + } +} + +// creates aura application instance and registers it in lists +// aura application effects are handled separately to prevent aura list corruption +AuraApplication* Unit::_CreateAuraApplication(Aura* aura, uint8 effMask) +{ + // can't apply aura on unit which is going to be deleted - to not create a memory leak + ASSERT(!m_cleanupDone); + + // just return if the aura has been already removed + // this can happen if OnEffectHitTarget() script hook killed the unit or the aura owner (which can be different) + if (aura->IsRemoved()) + { + TC_LOG_ERROR("spells", "Unit::_CreateAuraApplication() called with a removed aura. Check if OnEffectHitTarget() is triggering any spell with apply aura effect (that's not allowed!)"); + return nullptr; + } + + // aura mustn't be already applied on target + ASSERT (!aura->IsAppliedOnTarget(GetGUID()) && "Unit::_CreateAuraApplication: aura musn't be applied on target"); + + SpellInfo const* aurSpellInfo = aura->GetSpellInfo(); + uint32 aurId = aurSpellInfo->Id; + + // ghost spell check, allow apply any auras at player loading in ghost mode (will be cleanup after load) + if (!IsAlive() && !aurSpellInfo->IsDeathPersistent() && + (GetTypeId() != TYPEID_PLAYER || !ToPlayer()->GetSession()->PlayerLoading())) + return nullptr; + + Unit* caster = aura->GetCaster(); + + AuraApplication * aurApp = new AuraApplication(this, caster, aura, effMask); + m_appliedAuras.insert(AuraApplicationMap::value_type(aurId, aurApp)); + + if (aurSpellInfo->HasAnyAuraInterruptFlag()) + { + m_interruptableAuras.push_front(aurApp); + AddInterruptMask(aurSpellInfo->AuraInterruptFlags, aurSpellInfo->AuraInterruptFlags2); + } + + if (AuraStateType aState = aura->GetSpellInfo()->GetAuraState()) + m_auraStateAuras.insert(AuraStateAurasMap::value_type(aState, aurApp)); + + aura->_ApplyForTarget(this, caster, aurApp); + return aurApp; +} + +void Unit::_ApplyAuraEffect(Aura* aura, uint8 effIndex) +{ + ASSERT(aura); + ASSERT(aura->HasEffect(effIndex)); + AuraApplication * aurApp = aura->GetApplicationOfTarget(GetGUID()); + ASSERT(aurApp); + if (!aurApp->GetEffectMask()) + _ApplyAura(aurApp, 1 << effIndex); + else + aurApp->_HandleEffect(effIndex, true); +} + +// handles effects of aura application +// should be done after registering aura in lists +void Unit::_ApplyAura(AuraApplication* aurApp, uint8 effMask) +{ + Aura* aura = aurApp->GetBase(); + + _RemoveNoStackAurasDueToAura(aura, false); + + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + // Update target aura state flag + if (AuraStateType aState = aura->GetSpellInfo()->GetAuraState()) + ModifyAuraState(aState, true); + + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + // Sitdown on apply aura req seated + if (aura->GetSpellInfo()->HasAuraInterruptFlag(SpellAuraInterruptFlags::Standing) && !IsSitState()) + SetStandState(UNIT_STAND_STATE_SIT); + + Unit* caster = aura->GetCaster(); + + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + aura->HandleAuraSpecificMods(aurApp, caster, true, false); + + // apply effects of the aura + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (effMask & 1 << i && !aurApp->GetRemoveMode().HasAnyFlag()) + aurApp->_HandleEffect(i, true); + } + + if (Player* player = ToPlayer()) + { + if (sConditionMgr->IsSpellUsedInSpellClickConditions(aurApp->GetBase()->GetId())) + player->UpdateVisibleGameobjectsOrSpellClicks(); + + player->FailAchievementCriteria(AchievementCriteriaFailEvent::GainAura, aurApp->GetBase()->GetId()); + player->StartAchievementCriteria(AchievementCriteriaStartEvent::GainAura, aurApp->GetBase()->GetId()); + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET2, aurApp->GetBase()->GetId()); + } +} + +// removes aura application from lists and unapplies effects +void Unit::_UnapplyAura(AuraApplicationMap::iterator& i, AuraRemoveFlags removeMode) +{ + AuraApplication * aurApp = i->second; + ASSERT(aurApp); + ASSERT(!aurApp->GetRemoveMode().HasAnyFlag()); + ASSERT(aurApp->GetTarget() == this); + + aurApp->SetRemoveMode(removeMode); + Aura* aura = aurApp->GetBase(); + TC_LOG_DEBUG("spells", "Aura %u now is remove mode %u", aura->GetId(), static_cast(removeMode)); + + // dead loop is killing the server probably + ASSERT(m_removedAurasCount < 0xFFFFFFFF); + + ++m_removedAurasCount; + + Unit* caster = aura->GetCaster(); + + // Remove all pointers from lists here to prevent possible pointer invalidation on spellcast/auraapply/auraremove + m_appliedAuras.erase(i); + + if (aura->GetSpellInfo()->HasAnyAuraInterruptFlag()) + { + Trinity::Containers::Lists::RemoveUnique(m_interruptableAuras, aurApp); + UpdateInterruptMask(); + } + + bool auraStateFound = false; + AuraStateType auraState = aura->GetSpellInfo()->GetAuraState(); + if (auraState) + { + bool canBreak = false; + // Get mask of all aurastates from remaining auras + for (AuraStateAurasMap::iterator itr = m_auraStateAuras.lower_bound(auraState); itr != m_auraStateAuras.upper_bound(auraState) && !(auraStateFound && canBreak);) + { + if (itr->second == aurApp) + { + m_auraStateAuras.erase(itr); + itr = m_auraStateAuras.lower_bound(auraState); + canBreak = true; + continue; + } + auraStateFound = true; + ++itr; + } + } + + aurApp->_Remove(); + aura->_UnapplyForTarget(this, caster, aurApp); + + // remove effects of the spell - needs to be done after removing aura from lists + for (uint8 itr = 0; itr < MAX_SPELL_EFFECTS; ++itr) + { + if (aurApp->HasEffect(itr)) + aurApp->_HandleEffect(itr, false); + } + + // all effect mustn't be applied + ASSERT(!aurApp->GetEffectMask()); + + // Remove totem at next update if totem loses its aura + if (aurApp->GetRemoveMode().HasFlag(AuraRemoveFlags::Expired) && GetTypeId() == TYPEID_UNIT && IsTotem()) + { + if (ToTotem()->GetSpell() == aura->GetId() && ToTotem()->GetTotemType() == TOTEM_PASSIVE) + ToTotem()->setDeathState(JUST_DIED); + } + + // Remove aurastates only if were not found + if (!auraStateFound) + ModifyAuraState(auraState, false); + + aura->HandleAuraSpecificMods(aurApp, caster, false, false); + + if (Player* player = ToPlayer()) + { + if (sConditionMgr->IsSpellUsedInSpellClickConditions(aurApp->GetBase()->GetId())) + player->UpdateVisibleGameobjectsOrSpellClicks(); + + player->FailAchievementCriteria(AchievementCriteriaFailEvent::LoseAura, aurApp->GetBase()->GetId()); + } + + i = m_appliedAuras.begin(); +} + +void Unit::_UnapplyAura(AuraApplication* aurApp, AuraRemoveFlags removeMode) +{ + // aura can be removed from unit only if it's applied on it, shouldn't happen + ASSERT(aurApp->GetBase()->GetApplicationOfTarget(GetGUID()) == aurApp); + + uint32 spellId = aurApp->GetBase()->GetId(); + AuraApplicationMapBoundsNonConst range = m_appliedAuras.equal_range(spellId); + + for (AuraApplicationMap::iterator iter = range.first; iter != range.second;) + { + if (iter->second == aurApp) + { + _UnapplyAura(iter, removeMode); + return; + } + else + ++iter; + } + ABORT(); +} + +void Unit::_RemoveNoStackAurasDueToAura(Aura* aura, bool owned) +{ + SpellInfo const* spellProto = aura->GetSpellInfo(); + + // passive spell special case (only non stackable with ranks) + if (spellProto->IsPassiveStackableWithRanks()) + return; + + if (!IsHighestExclusiveAura(aura)) + { + aura->Remove(); + return; + } + + if (owned) + RemoveOwnedAuras([aura](Aura const* ownedAura) { return !aura->CanStackWith(ownedAura); }, AuraRemoveFlags::ByDefault); + else + RemoveAppliedAuras([aura](AuraApplication const* appliedAura) { return !aura->CanStackWith(appliedAura->GetBase()); }, AuraRemoveFlags::ByDefault); +} + +void Unit::_RegisterAuraEffect(AuraEffect* aurEff, bool apply) +{ + if (apply) + { + m_modAuras[aurEff->GetAuraType()].push_front(aurEff); + if (Player* player = ToPlayer()) + { + player->StartAchievementCriteria(AchievementCriteriaStartEvent::GainAuraEffect, aurEff->GetAuraType()); + player->FailAchievementCriteria(AchievementCriteriaFailEvent::GainAuraEffect, aurEff->GetAuraType()); + } + } + else + Trinity::Containers::Lists::RemoveUnique(m_modAuras[aurEff->GetAuraType()], aurEff); +} + +// All aura base removes should go through this function! +void Unit::RemoveOwnedAura(AuraMap::iterator& i, AuraRemoveFlags removeMode) +{ + Aura* aura = i->second; + ASSERT(!aura->IsRemoved()); + + // if unit currently update aura list then make safe update iterator shift to next + if (m_auraUpdateIterator == i) + ++m_auraUpdateIterator; + + m_ownedAuras.erase(i); + m_removedAuras.push_front(aura); + + // Unregister single target aura + if (aura->IsSingleTarget()) + aura->UnregisterSingleTarget(); + + aura->_Remove(removeMode); + + i = m_ownedAuras.begin(); +} + +void Unit::RemoveOwnedAura(uint32 spellId, ObjectGuid casterGUID, uint8 reqEffMask, AuraRemoveFlags removeMode) +{ + for (AuraMap::iterator itr = m_ownedAuras.lower_bound(spellId); itr != m_ownedAuras.upper_bound(spellId);) + if (((itr->second->GetEffectMask() & reqEffMask) == reqEffMask) && (!casterGUID || itr->second->GetCasterGUID() == casterGUID)) + { + RemoveOwnedAura(itr, removeMode); + itr = m_ownedAuras.lower_bound(spellId); + } + else + ++itr; +} + +void Unit::RemoveOwnedAura(Aura* aura, AuraRemoveFlags removeMode) +{ + if (aura->IsRemoved()) + return; + + ASSERT(aura->GetOwner() == this); + + if (removeMode == AuraRemoveFlags::None) + { + TC_LOG_ERROR("spells", "Unit::RemoveOwnedAura() called with unallowed removeMode AuraRemoveFlags::None, spellId %u", aura->GetId()); + return; + } + + uint32 spellId = aura->GetId(); + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + + for (AuraMap::iterator itr = range.first; itr != range.second; ++itr) + { + if (itr->second == aura) + { + RemoveOwnedAura(itr, removeMode); + return; + } + } + + ABORT(); +} + +Aura* Unit::GetOwnedAura(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask, Aura* except) const +{ + AuraMapBounds range = m_ownedAuras.equal_range(spellId); + for (AuraMap::const_iterator itr = range.first; itr != range.second; ++itr) + { + if (((itr->second->GetEffectMask() & reqEffMask) == reqEffMask) + && (!casterGUID || itr->second->GetCasterGUID() == casterGUID) + && (!itemCasterGUID || itr->second->GetCastItemGUID() == itemCasterGUID) + && (!except || except != itr->second)) + { + return itr->second; + } + } + return nullptr; +} + +void Unit::RemoveAura(AuraApplicationMap::iterator &i, AuraRemoveFlags mode) +{ + AuraApplication * aurApp = i->second; + // Do not remove aura which is already being removed + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + Aura* aura = aurApp->GetBase(); + _UnapplyAura(i, mode); + // Remove aura - for Area and Target auras + if (aura->GetOwner() == this) + aura->Remove(mode); +} + +void Unit::RemoveAura(uint32 spellId, ObjectGuid caster, uint8 reqEffMask, AuraRemoveFlags removeMode) +{ + AuraApplicationMapBoundsNonConst range = m_appliedAuras.equal_range(spellId); + for (AuraApplicationMap::iterator iter = range.first; iter != range.second;) + { + Aura const* aura = iter->second->GetBase(); + if (((aura->GetEffectMask() & reqEffMask) == reqEffMask) + && (!caster || aura->GetCasterGUID() == caster)) + { + RemoveAura(iter, removeMode); + return; + } + else + ++iter; + } +} + +void Unit::RemoveAura(AuraApplication * aurApp, AuraRemoveFlags mode) +{ + // we've special situation here, RemoveAura called while during aura removal + // this kind of call is needed only when aura effect removal handler + // or event triggered by it expects to remove + // not yet removed effects of an aura + if (aurApp->GetRemoveMode().HasAnyFlag()) + { + // remove remaining effects of an aura + for (uint8 itr = 0; itr < MAX_SPELL_EFFECTS; ++itr) + { + if (aurApp->HasEffect(itr)) + aurApp->_HandleEffect(itr, false); + } + return; + } + // no need to remove + if (aurApp->GetBase()->GetApplicationOfTarget(GetGUID()) != aurApp || aurApp->GetBase()->IsRemoved()) + return; + + uint32 spellId = aurApp->GetBase()->GetId(); + AuraApplicationMapBoundsNonConst range = m_appliedAuras.equal_range(spellId); + + for (AuraApplicationMap::iterator iter = range.first; iter != range.second;) + { + if (aurApp == iter->second) + { + RemoveAura(iter, mode); + return; + } + else + ++iter; + } +} + +void Unit::RemoveAura(Aura* aura, AuraRemoveFlags mode) +{ + if (aura->IsRemoved()) + return; + if (AuraApplication * aurApp = aura->GetApplicationOfTarget(GetGUID())) + RemoveAura(aurApp, mode); +} + +void Unit::RemoveAppliedAuras(std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + if (check(iter->second)) + { + RemoveAura(iter, removeMode); + continue; + } + ++iter; + } +} + +void Unit::RemoveOwnedAuras(std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + if (check(iter->second)) + { + RemoveOwnedAura(iter, removeMode); + continue; + } + ++iter; + } +} + +void Unit::RemoveAppliedAuras(uint32 spellId, std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.lower_bound(spellId); iter != m_appliedAuras.upper_bound(spellId);) + { + if (check(iter->second)) + { + RemoveAura(iter, removeMode); + iter = m_appliedAuras.lower_bound(spellId); + continue; + } + ++iter; + } +} + +void Unit::RemoveOwnedAuras(uint32 spellId, std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraMap::iterator iter = m_ownedAuras.lower_bound(spellId); iter != m_ownedAuras.upper_bound(spellId);) + { + if (check(iter->second)) + { + RemoveOwnedAura(iter, removeMode); + iter = m_ownedAuras.lower_bound(spellId); + continue; + } + ++iter; + } +} + +void Unit::RemoveAurasByType(AuraType auraType, std::function const& check, AuraRemoveFlags removeMode /* = AuraRemoveFlags::ByDefault*/) +{ + for (AuraEffectList::iterator iter = m_modAuras[auraType].begin(); iter != m_modAuras[auraType].end();) + { + Aura* aura = (*iter)->GetBase(); + AuraApplication * aurApp = aura->GetApplicationOfTarget(GetGUID()); + ASSERT(aurApp); + + ++iter; + if (check(aurApp)) + { + uint32 removedAuras = m_removedAurasCount; + RemoveAura(aurApp, removeMode); + if (m_removedAurasCount > removedAuras + 1) + iter = m_modAuras[auraType].begin(); + } + } +} + +void Unit::RemoveAurasDueToSpell(uint32 spellId, ObjectGuid casterGUID, uint8 reqEffMask, AuraRemoveFlags removeMode) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.lower_bound(spellId); iter != m_appliedAuras.upper_bound(spellId);) + { + Aura const* aura = iter->second->GetBase(); + if (((aura->GetEffectMask() & reqEffMask) == reqEffMask) + && (!casterGUID || aura->GetCasterGUID() == casterGUID)) + { + RemoveAura(iter, removeMode); + iter = m_appliedAuras.lower_bound(spellId); + } + else + ++iter; + } +} + +void Unit::RemoveAuraFromStack(uint32 spellId, ObjectGuid casterGUID, AuraRemoveFlags removeMode) +{ + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + for (AuraMap::iterator iter = range.first; iter != range.second;) + { + Aura* aura = iter->second; + if ((aura->GetType() == UNIT_AURA_TYPE) + && (!casterGUID || aura->GetCasterGUID() == casterGUID)) + { + aura->ModStackAmount(-1, removeMode); + return; + } + else + ++iter; + } +} + +void Unit::RemoveAurasDueToSpellByDispel(uint32 spellId, uint32 dispellerSpellId, ObjectGuid casterGUID, WorldObject* dispeller, uint8 chargesRemoved/*= 1*/) +{ + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + for (AuraMap::iterator iter = range.first; iter != range.second;) + { + Aura* aura = iter->second; + if (aura->GetCasterGUID() == casterGUID) + { + DispelInfo dispelInfo(dispeller, dispellerSpellId, chargesRemoved); + + // Call OnDispel hook on AuraScript + aura->CallScriptDispel(&dispelInfo); + + if (aura->GetSpellInfo()->HasAttribute(SPELL_ATTR7_DISPEL_REMOVES_CHARGES)) + aura->ModCharges(-dispelInfo.GetRemovedCharges(), AuraRemoveFlags::ByEnemySpell); + else + aura->ModStackAmount(-dispelInfo.GetRemovedCharges(), AuraRemoveFlags::ByEnemySpell); + + // Call AfterDispel hook on AuraScript + aura->CallScriptAfterDispel(&dispelInfo); + + return; + } + else + ++iter; + } +} + +void Unit::RemoveAurasDueToSpellBySteal(uint32 spellId, ObjectGuid casterGUID, WorldObject* stealer, int32 stolenCharges /*= 1*/) +{ + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + for (AuraMap::iterator iter = range.first; iter != range.second;) + { + Aura* aura = iter->second; + if (aura->GetCasterGUID() == casterGUID) + { + int32 damage[MAX_SPELL_EFFECTS] = { }; + int32 baseDamage[MAX_SPELL_EFFECTS] = { }; + uint8 effMask = 0; + uint8 recalculateMask = 0; + Unit* caster = aura->GetCaster(); + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (aura->GetEffect(i)) + { + baseDamage[i] = aura->GetEffect(i)->GetBaseAmount(); + damage[i] = aura->GetEffect(i)->GetAmount(); + effMask |= (1<GetEffect(i)->CanBeRecalculated()) + recalculateMask |= (1<GetSpellInfo()->HasAttribute(SPELL_ATTR7_DISPEL_REMOVES_CHARGES); + // Cast duration to unsigned to prevent permanent aura's such as Righteous Fury being permanently added to caster + uint32 dur = std::min(2u * MINUTE * IN_MILLISECONDS, uint32(aura->GetDuration())); + + if (Unit* unitStealer = stealer->ToUnit()) + { + if (Aura* oldAura = unitStealer->GetAura(aura->GetId(), aura->GetCasterGUID())) + { + if (stealCharge) + oldAura->ModCharges(stolenCharges); + else + oldAura->ModStackAmount(stolenCharges); + oldAura->SetDuration(int32(dur)); + } + else + { + // single target state must be removed before aura creation to preserve existing single target aura + if (aura->IsSingleTarget()) + aura->UnregisterSingleTarget(); + + AuraCreateInfo createInfo(aura->GetSpellInfo(), effMask, stealer); + createInfo + .SetCasterGUID(aura->GetCasterGUID()) + .SetBaseAmount(baseDamage); + + if (Aura* newAura = Aura::TryRefreshStackOrCreate(createInfo)) + { + // created aura must not be single target aura,, so stealer won't loose it on recast + if (newAura->IsSingleTarget()) + { + newAura->UnregisterSingleTarget(); + // bring back single target aura status to the old aura + aura->SetIsSingleTarget(true); + caster->GetSingleCastAuras().push_front(aura); + } + // FIXME: using aura->GetMaxDuration() maybe not blizzlike but it fixes stealing of spells like Innervate + newAura->SetLoadedState(aura->GetMaxDuration(), int32(dur), stealCharge ? stolenCharges : aura->GetCharges(), stolenCharges, recalculateMask, aura->GetCritChance(), aura->CanApplyResilience(), &damage[0]); + newAura->ApplyForTargets(); + } + } + } + + if (stealCharge) + aura->ModCharges(-stolenCharges, AuraRemoveFlags::ByEnemySpell); + else + aura->ModStackAmount(-stolenCharges, AuraRemoveFlags::ByEnemySpell); + + return; + } + else + ++iter; + } +} + +void Unit::RemoveAurasDueToItemSpell(uint32 spellId, ObjectGuid castItemGuid) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.lower_bound(spellId); iter != m_appliedAuras.upper_bound(spellId);) + { + if (iter->second->GetBase()->GetCastItemGUID() == castItemGuid) + { + RemoveAura(iter); + iter = m_appliedAuras.lower_bound(spellId); + } + else + ++iter; + } +} + +void Unit::RemoveAurasByType(AuraType auraType, ObjectGuid casterGUID, Aura* except, bool negative, bool positive) +{ + for (AuraEffectList::iterator iter = m_modAuras[auraType].begin(); iter != m_modAuras[auraType].end();) + { + Aura* aura = (*iter)->GetBase(); + AuraApplication * aurApp = aura->GetApplicationOfTarget(GetGUID()); + ASSERT(aurApp); + + ++iter; + if (aura != except && (!casterGUID || aura->GetCasterGUID() == casterGUID) + && ((negative && !aurApp->IsPositive()) || (positive && aurApp->IsPositive()))) + { + uint32 removedAuras = m_removedAurasCount; + RemoveAura(aurApp); + if (m_removedAurasCount > removedAuras + 1) + iter = m_modAuras[auraType].begin(); + } + } +} + +void Unit::RemoveAurasWithAttribute(uint32 flags) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + SpellInfo const* spell = iter->second->GetBase()->GetSpellInfo(); + if (spell->Attributes & flags) + RemoveAura(iter); + else + ++iter; + } +} + +void Unit::RemoveNotOwnSingleTargetAuras(bool onPhaseChange /*= false*/) +{ + // single target auras from other casters + // Iterate m_ownedAuras - aura is marked as single target in Unit::AddAura (and pushed to m_ownedAuras). + // m_appliedAuras will NOT contain the aura before first Unit::Update after adding it to m_ownedAuras. + // Quickly removing such an aura will lead to it not being unregistered from caster's single cast auras container + // leading to assertion failures if the aura was cast on a player that can + // (and is changing map at the point where this function is called). + // Such situation occurs when player is logging in inside an instance and fails the entry check for any reason. + // The aura that was loaded from db (indirectly, via linked casts) gets removed before it has a chance + // to register in m_appliedAuras + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura const* aura = iter->second; + + if (aura->GetCasterGUID() != GetGUID() && aura->IsSingleTarget()) + { + if (!onPhaseChange) + RemoveOwnedAura(iter); + else + { + Unit* caster = aura->GetCaster(); + if (!caster || !caster->IsInPhase(this)) + RemoveOwnedAura(iter); + else + ++iter; + } + } + else + ++iter; + } + + // single target auras at other targets + AuraList& scAuras = GetSingleCastAuras(); + for (AuraList::iterator iter = scAuras.begin(); iter != scAuras.end();) + { + Aura* aura = *iter; + if (aura->GetUnitOwner() != this && (!onPhaseChange || !aura->GetUnitOwner()->IsInPhase(this))) + { + aura->Remove(); + iter = scAuras.begin(); + } + else + ++iter; + } +} + +template +bool IsInterruptFlagIgnoredForSpell(InterruptFlag /*flag*/, Unit const* /*unit*/, SpellInfo const* /*auraSpellInfo*/, SpellInfo const* /*interruptSource*/) +{ + return false; +} + +bool IsInterruptFlagIgnoredForSpell(SpellAuraInterruptFlags flag, Unit const* unit, SpellInfo const* auraSpellInfo, SpellInfo const* interruptSource) +{ + switch (flag) + { + case SpellAuraInterruptFlags::Moving: + return unit->HasAuraTypeWithAffectMask(SPELL_AURA_CAST_WHILE_WALKING, auraSpellInfo); + case SpellAuraInterruptFlags::Action: + case SpellAuraInterruptFlags::ActionDelayed: + if (interruptSource) + { + if (interruptSource->HasAttribute(SPELL_ATTR1_ALLOW_WHILE_STEALTHED) && auraSpellInfo->Dispel == DISPEL_STEALTH) + return true; + + if (interruptSource->HasAttribute(SPELL_ATTR2_ALLOW_WHILE_INVISIBLE) && auraSpellInfo->Dispel == DISPEL_INVISIBILITY) + return true; + } + break; + default: + break; + } + + return false; +} + +template +void Unit::RemoveAurasWithInterruptFlags(InterruptFlags flag, SpellInfo const* source) +{ + if (!HasInterruptFlag(flag)) + return; + + // interrupt auras + for (AuraApplicationList::iterator iter = m_interruptableAuras.begin(); iter != m_interruptableAuras.end();) + { + Aura* aura = (*iter)->GetBase(); + ++iter; + if (aura->GetSpellInfo()->HasAuraInterruptFlag(flag) + && (!source || aura->GetId() != source->Id) + && !IsInterruptFlagIgnoredForSpell(flag, this, aura->GetSpellInfo(), source)) + { + uint32 removedAuras = m_removedAurasCount; + RemoveAura(aura); + if (m_removedAurasCount > removedAuras + 1) + iter = m_interruptableAuras.begin(); + } + } + + // interrupt channeled spell + if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL]) + if (spell->getState() == SPELL_STATE_CHANNELING + && spell->GetSpellInfo()->HasChannelInterruptFlag(flag) + && (!source || spell->GetSpellInfo()->Id != source->Id) + && !IsInterruptFlagIgnoredForSpell(flag, this, spell->GetSpellInfo(), source)) + InterruptNonMeleeSpells(false, 0, true); + + UpdateInterruptMask(); +} + +template TC_GAME_API void Unit::RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags flag, SpellInfo const* source); +template TC_GAME_API void Unit::RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags2 flag, SpellInfo const* source); + +void Unit::RemoveAurasWithFamily(SpellFamilyNames family, uint32 familyFlag1, uint32 familyFlag2, uint32 familyFlag3, ObjectGuid casterGUID) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (!casterGUID || aura->GetCasterGUID() == casterGUID) + { + SpellInfo const* spell = aura->GetSpellInfo(); + if (spell->SpellFamilyName == uint32(family) && spell->SpellFamilyFlags.HasFlag(familyFlag1, familyFlag2, familyFlag3)) + { + RemoveAura(iter); + continue; + } + } + ++iter; + } +} + +void Unit::RemoveMovementImpairingAuras(bool withRoot) +{ + if (withRoot) + RemoveAurasWithMechanic(1 << MECHANIC_ROOT, AuraRemoveFlags::ByDefault, 0, true); + + RemoveAurasWithMechanic(1 << MECHANIC_ROOT, AuraRemoveFlags::ByDefault, 0, false); +} + +void Unit::RemoveAurasWithMechanic(uint32 mechanicMaskToRemove, AuraRemoveFlags removeMode, uint32 exceptSpellId, bool withEffectMechanics) +{ + std::vector aurasToUpdateTargets; + RemoveAppliedAuras([=, &aurasToUpdateTargets](AuraApplication const* aurApp) + { + Aura* aura = aurApp->GetBase(); + if (exceptSpellId && aura->GetId() == exceptSpellId) + return false; + + uint32 appliedMechanicMask = aura->GetSpellInfo()->GetSpellMechanicMaskByEffectMask(aurApp->GetEffectMask()); + if (!(appliedMechanicMask & mechanicMaskToRemove)) + return false; + + // spell mechanic matches required mask for removal + if ((1 << aura->GetSpellInfo()->Mechanic) & mechanicMaskToRemove || withEffectMechanics) + return true; + + // effect mechanic matches required mask for removal - don't remove, only update targets + aurasToUpdateTargets.push_back(aura); + return false; + }, removeMode); + + for (Aura* aura : aurasToUpdateTargets) + { + aura->UpdateTargetMap(aura->GetCaster()); + + // Fully remove the aura if all effects were removed + if (!aura->IsPassive() && aura->GetOwner() == this && !aura->GetApplicationOfTarget(GetGUID())) + aura->Remove(removeMode); + } +} + +void Unit::RemoveAurasByShapeShift() +{ + uint32 mechanic_mask = (1 << MECHANIC_SNARE) | (1 << MECHANIC_ROOT); + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if ((aura->GetSpellInfo()->GetAllEffectsMechanicMask() & mechanic_mask) && !aura->GetSpellInfo()->HasAttribute(SPELL_ATTR0_CU_AURA_CC)) + { + RemoveAura(iter); + continue; + } + ++iter; + } +} + +void Unit::RemoveAreaAurasDueToLeaveWorld() +{ + // make sure that all area auras not applied on self are removed - prevent access to deleted pointer later + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + ++iter; + Aura::ApplicationMap const& appMap = aura->GetApplicationMap(); + for (Aura::ApplicationMap::const_iterator itr = appMap.begin(); itr!= appMap.end();) + { + AuraApplication * aurApp = itr->second; + ++itr; + Unit* target = aurApp->GetTarget(); + if (target == this) + continue; + target->RemoveAura(aurApp); + // things linked on aura remove may apply new area aura - so start from the beginning + iter = m_ownedAuras.begin(); + } + } + + // remove area auras owned by others + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + if (iter->second->GetBase()->GetOwner() != this) + { + RemoveAura(iter); + } + else + ++iter; + } +} + +void Unit::RemoveAllAuras() +{ + // this may be a dead loop if some events on aura remove will continiously apply aura on remove + // we want to have all auras removed, so use your brain when linking events + for (uint32 counter = 0; !m_appliedAuras.empty() || !m_ownedAuras.empty(); counter++) + { + AuraApplicationMap::iterator aurAppIter; + for (aurAppIter = m_appliedAuras.begin(); aurAppIter != m_appliedAuras.end();) + _UnapplyAura(aurAppIter, AuraRemoveFlags::ByDefault); + + AuraMap::iterator aurIter; + for (aurIter = m_ownedAuras.begin(); aurIter != m_ownedAuras.end();) + RemoveOwnedAura(aurIter); + + const int maxIteration = 50; + // give this loop a few tries, if there are still auras then log as much information as possible + if (counter >= maxIteration) + { + std::stringstream sstr; + sstr << "Unit::RemoveAllAuras() iterated " << maxIteration << " times already but there are still " + << m_appliedAuras.size() << " m_appliedAuras and " << m_ownedAuras.size() << " m_ownedAuras.\n"; + + TC_LOG_ERROR("entities.unit", "%s", sstr.str().c_str()); + ASSERT(false); + + break; + } + } +} + +void Unit::RemoveArenaAuras() +{ + // in join, remove positive buffs, on end, remove negative + // used to remove positive visible auras in arenas + RemoveAppliedAuras([](AuraApplication const* aurApp) + { + Aura const* aura = aurApp->GetBase(); + return (!aura->GetSpellInfo()->HasAttribute(SPELL_ATTR4_ALLOW_ENTERING_ARENA) // don't remove stances, shadowform, pally/hunter auras + && !aura->IsPassive() // don't remove passive auras + && (aurApp->IsPositive() || !aura->GetSpellInfo()->HasAttribute(SPELL_ATTR3_ALLOW_AURA_WHILE_DEAD))) || // not negative death persistent auras + aura->GetSpellInfo()->HasAttribute(SPELL_ATTR5_REMOVE_ENTERING_ARENA); // special marker, always remove + }); +} + +void Unit::RemoveAurasOnEvade() +{ + if (IsCharmedOwnedByPlayerOrPlayer()) // if it is a player owned creature it should not remove the aura + return; + + // don't remove vehicle auras, passengers aren't supposed to drop off the vehicle + // don't remove clone caster on evade (to be verified) + auto evadeAuraCheck = [](Aura const* aura) + { + if (aura->HasEffectType(SPELL_AURA_CONTROL_VEHICLE)) + return false; + + if (aura->HasEffectType(SPELL_AURA_CLONE_CASTER)) + return false; + + if (aura->GetSpellInfo()->HasAttribute(SPELL_ATTR1_AURA_STAYS_AFTER_COMBAT)) + return false; + + return true; + }; + + auto evadeAuraApplicationCheck = [&evadeAuraCheck](AuraApplication const* aurApp) + { + return evadeAuraCheck(aurApp->GetBase()); + }; + + RemoveAppliedAuras(evadeAuraApplicationCheck); + RemoveOwnedAuras(evadeAuraCheck); +} + +void Unit::RemoveAllAurasOnDeath() +{ + // used just after dieing to remove all visible auras + // and disable the mods for the passive ones + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (!aura->IsPassive() && !aura->IsDeathPersistent()) + _UnapplyAura(iter, AuraRemoveFlags::ByDeath); + else + ++iter; + } + + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (!aura->IsPassive() && !aura->IsDeathPersistent()) + RemoveOwnedAura(iter, AuraRemoveFlags::ByDeath); + else + ++iter; + } +} + +void Unit::RemoveAllAurasRequiringDeadTarget() +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (!aura->IsPassive() && aura->GetSpellInfo()->IsRequiringDeadTarget()) + _UnapplyAura(iter, AuraRemoveFlags::ByDefault); + else + ++iter; + } + + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (!aura->IsPassive() && aura->GetSpellInfo()->IsRequiringDeadTarget()) + RemoveOwnedAura(iter, AuraRemoveFlags::ByDefault); + else + ++iter; + } +} + +void Unit::RemoveAllAurasExceptType(AuraType type) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (aura->GetSpellInfo()->HasAura(type)) + ++iter; + else + _UnapplyAura(iter, AuraRemoveFlags::ByDefault); + } + + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (aura->GetSpellInfo()->HasAura(type)) + ++iter; + else + RemoveOwnedAura(iter, AuraRemoveFlags::ByDefault); + } +} + +void Unit::RemoveAllAurasExceptType(AuraType type1, AuraType type2) +{ + for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) + { + Aura const* aura = iter->second->GetBase(); + if (aura->GetSpellInfo()->HasAura(type1) || aura->GetSpellInfo()->HasAura(type2)) + ++iter; + else + _UnapplyAura(iter, AuraRemoveFlags::ByDefault); + } + + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (aura->GetSpellInfo()->HasAura(type1) || aura->GetSpellInfo()->HasAura(type2)) + ++iter; + else + RemoveOwnedAura(iter, AuraRemoveFlags::ByDefault); + } +} + +void Unit::RemoveAllGroupBuffsFromCaster(ObjectGuid casterGUID) +{ + for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) + { + Aura* aura = iter->second; + if (aura->GetCasterGUID() == casterGUID && aura->GetSpellInfo()->IsGroupBuff()) + { + RemoveOwnedAura(iter); + continue; + } + ++iter; + } +} + +void Unit::DelayOwnedAuras(uint32 spellId, ObjectGuid caster, int32 delaytime) +{ + AuraMapBoundsNonConst range = m_ownedAuras.equal_range(spellId); + for (; range.first != range.second; ++range.first) + { + Aura* aura = range.first->second; + if (!caster || aura->GetCasterGUID() == caster) + { + if (aura->GetDuration() < delaytime) + aura->SetDuration(0); + else + aura->SetDuration(aura->GetDuration() - delaytime); + + // update for out of range group members (on 1 slot use) + aura->SetNeedClientUpdateForTargets(); + } + } +} + +void Unit::_RemoveAllAuraStatMods() +{ + for (AuraApplicationMap::iterator i = m_appliedAuras.begin(); i != m_appliedAuras.end(); ++i) + (*i).second->GetBase()->HandleAllEffects(i->second, AURA_EFFECT_HANDLE_STAT, false); +} + +void Unit::_ApplyAllAuraStatMods() +{ + for (AuraApplicationMap::iterator i = m_appliedAuras.begin(); i != m_appliedAuras.end(); ++i) + (*i).second->GetBase()->HandleAllEffects(i->second, AURA_EFFECT_HANDLE_STAT, true); +} + +bool Unit::HasSingleCastAuraOfSpell(uint32 spellId) const +{ + AuraList const& scAuras = GetSingleCastAuras(); + for (Aura const* aura : scAuras) + if (aura->GetSpellInfo()->Id == spellId) + return true; + + return false; +} + +AuraEffect* Unit::GetAuraEffect(uint32 spellId, uint8 effIndex, ObjectGuid caster) const +{ + AuraApplicationMapBounds range = m_appliedAuras.equal_range(spellId); + for (AuraApplicationMap::const_iterator itr = range.first; itr != range.second; ++itr) + { + if (itr->second->HasEffect(effIndex) + && (!caster || itr->second->GetBase()->GetCasterGUID() == caster)) + { + return itr->second->GetBase()->GetEffect(effIndex); + } + } + return nullptr; +} + +AuraEffect* Unit::GetAuraEffectOfRankedSpell(uint32 spellId, uint8 effIndex, ObjectGuid caster) const +{ + uint32 rankSpell = sSpellMgr->GetFirstSpellInChain(spellId); + while (rankSpell) + { + if (AuraEffect* aurEff = GetAuraEffect(rankSpell, effIndex, caster)) + return aurEff; + rankSpell = sSpellMgr->GetNextSpellInChain(rankSpell); + } + return nullptr; +} + +AuraEffect* Unit::GetAuraEffect(AuraType type, SpellFamilyNames name, uint32 iconId, uint8 effIndex) const +{ + AuraEffectList const& auras = GetAuraEffectsByType(type); + for (Unit::AuraEffectList::const_iterator itr = auras.begin(); itr != auras.end(); ++itr) + { + if (effIndex != (*itr)->GetEffIndex()) + continue; + SpellInfo const* spell = (*itr)->GetSpellInfo(); + if (spell->SpellIconID == iconId && spell->SpellFamilyName == uint32(name) && !spell->SpellFamilyFlags) + return *itr; + } + return nullptr; +} + +AuraEffect* Unit::GetAuraEffect(AuraType type, SpellFamilyNames family, uint32 familyFlag1, uint32 familyFlag2, uint32 familyFlag3, ObjectGuid casterGUID) const +{ + AuraEffectList const& auras = GetAuraEffectsByType(type); + for (AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + SpellInfo const* spell = (*i)->GetSpellInfo(); + if (spell->SpellFamilyName == uint32(family) && spell->SpellFamilyFlags.HasFlag(familyFlag1, familyFlag2, familyFlag3)) + { + if (casterGUID && (*i)->GetCasterGUID() != casterGUID) + continue; + return (*i); + } + } + return nullptr; +} + +AuraEffect* Unit::GetDummyAuraEffect(SpellFamilyNames name, uint32 iconId, uint8 effIndex) const +{ + return GetAuraEffect(SPELL_AURA_DUMMY, name, iconId, effIndex); +} + +AuraApplication * Unit::GetAuraApplication(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask, AuraApplication * except) const +{ + AuraApplicationMapBounds range = m_appliedAuras.equal_range(spellId); + for (; range.first != range.second; ++range.first) + { + AuraApplication* app = range.first->second; + Aura const* aura = app->GetBase(); + + if (((aura->GetEffectMask() & reqEffMask) == reqEffMask) + && (!casterGUID || aura->GetCasterGUID() == casterGUID) + && (!itemCasterGUID || aura->GetCastItemGUID() == itemCasterGUID) + && (!except || except != app)) + { + return app; + } + } + return nullptr; +} + +Aura* Unit::GetAura(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask) const +{ + AuraApplication * aurApp = GetAuraApplication(spellId, casterGUID, itemCasterGUID, reqEffMask); + return aurApp ? aurApp->GetBase() : nullptr; +} + +AuraApplication * Unit::GetAuraApplicationOfRankedSpell(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask, AuraApplication* except) const +{ + uint32 rankSpell = sSpellMgr->GetFirstSpellInChain(spellId); + while (rankSpell) + { + if (AuraApplication * aurApp = GetAuraApplication(rankSpell, casterGUID, itemCasterGUID, reqEffMask, except)) + return aurApp; + rankSpell = sSpellMgr->GetNextSpellInChain(rankSpell); + } + return nullptr; +} + +Aura* Unit::GetAuraOfRankedSpell(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask) const +{ + AuraApplication * aurApp = GetAuraApplicationOfRankedSpell(spellId, casterGUID, itemCasterGUID, reqEffMask); + return aurApp ? aurApp->GetBase() : nullptr; +} + +void Unit::GetDispellableAuraList(WorldObject const* caster, uint32 dispelMask, DispelChargesList& dispelList, bool isReflect /*= false*/) const +{ + // we should not be able to dispel diseases if the target is affected by unholy blight + if (dispelMask & (1 << DISPEL_DISEASE) && HasAura(50536)) + dispelMask &= ~(1 << DISPEL_DISEASE); + + AuraMap const& auras = GetOwnedAuras(); + for (auto itr = auras.begin(); itr != auras.end(); ++itr) + { + Aura* aura = itr->second; + AuraApplication const* aurApp = aura->GetApplicationOfTarget(GetGUID()); + if (!aurApp) + continue; + + // don't try to remove passive auras + if (aura->IsPassive()) + continue; + + if (aura->GetSpellInfo()->GetDispelMask() & dispelMask) + { + // do not remove positive auras if friendly target + // negative auras if non-friendly + // unless we're reflecting (dispeller eliminates one of it's benefitial buffs) + if (isReflect != (aurApp->IsPositive() == IsFriendlyTo(caster))) + continue; + + // 2.4.3 Patch Notes: "Dispel effects will no longer attempt to remove effects that have 100% dispel resistance." + int32 chance = aura->CalcDispelChance(this, !IsFriendlyTo(caster)); + if (!chance) + continue; + + // The charges / stack amounts don't count towards the total number of auras that can be dispelled. + // Ie: A dispel on a target with 5 stacks of Winters Chill and a Polymorph has 1 / (1 + 1) -> 50% chance to dispell + // Polymorph instead of 1 / (5 + 1) -> 16%. + bool const dispelCharges = aura->GetSpellInfo()->HasAttribute(SPELL_ATTR7_DISPEL_REMOVES_CHARGES); + uint8 charges = dispelCharges ? aura->GetCharges() : aura->GetStackAmount(); + if (charges > 0) + dispelList.emplace_back(aura, chance, charges); + } + } +} + +bool Unit::HasAuraEffect(uint32 spellId, uint8 effIndex, ObjectGuid caster) const +{ + AuraApplicationMapBounds range = m_appliedAuras.equal_range(spellId); + for (AuraApplicationMap::const_iterator itr = range.first; itr != range.second; ++itr) + { + if (itr->second->HasEffect(effIndex) + && (!caster || itr->second->GetBase()->GetCasterGUID() == caster)) + { + return true; + } + } + return false; +} + +uint32 Unit::GetAuraCount(uint32 spellId) const +{ + uint32 count = 0; + AuraApplicationMapBounds range = m_appliedAuras.equal_range(spellId); + + for (AuraApplicationMap::const_iterator itr = range.first; itr != range.second; ++itr) + { + if (itr->second->GetBase()->GetStackAmount() == 0) + ++count; + else + count += (uint32)itr->second->GetBase()->GetStackAmount(); + } + + return count; +} + +bool Unit::HasAura(uint32 spellId, ObjectGuid casterGUID, ObjectGuid itemCasterGUID, uint8 reqEffMask) const +{ + if (GetAuraApplication(spellId, casterGUID, itemCasterGUID, reqEffMask)) + return true; + return false; +} + +bool Unit::HasAuraType(AuraType auraType) const +{ + return (!m_modAuras[auraType].empty()); +} + +bool Unit::HasAuraTypeWithCaster(AuraType auraType, ObjectGuid caster) const +{ + for (AuraEffect const* eff : GetAuraEffectsByType(auraType)) + if (caster == eff->GetCasterGUID()) + return true; + return false; +} + +bool Unit::HasAuraTypeWithMiscvalue(AuraType auraType, int32 miscvalue) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator i = mTotalAuraList.begin(); i != mTotalAuraList.end(); ++i) + if (miscvalue == (*i)->GetMiscValue()) + return true; + return false; +} + +bool Unit::HasAuraTypeWithAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator i = mTotalAuraList.begin(); i != mTotalAuraList.end(); ++i) + if ((*i)->IsAffectingSpell(affectedSpell)) + return true; + return false; +} + +bool Unit::HasAuraTypeWithValue(AuraType auraType, int32 value) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator i = mTotalAuraList.begin(); i != mTotalAuraList.end(); ++i) + if (value == (*i)->GetAmount()) + return true; + return false; +} + +template +bool Unit::HasNegativeAuraWithInterruptFlag(InterruptFlags flag, ObjectGuid guid) const +{ + if (!HasInterruptFlag(flag)) + return false; + + for (AuraApplicationList::const_iterator iter = m_interruptableAuras.begin(); iter != m_interruptableAuras.end(); ++iter) + { + if (!(*iter)->IsPositive() && (*iter)->GetBase()->GetSpellInfo()->HasAuraInterruptFlag(flag) && (!guid || (*iter)->GetBase()->GetCasterGUID() == guid)) + return true; + } + return false; +} + +bool Unit::HasAuraWithMechanic(uint32 mechanicMask) const +{ + for (AuraApplicationMap::const_iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end(); ++iter) + { + SpellInfo const* spellInfo = iter->second->GetBase()->GetSpellInfo(); + if (spellInfo->Mechanic && (mechanicMask & (1 << spellInfo->Mechanic))) + return true; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (iter->second->HasEffect(i) && spellInfo->Effects[i].Effect && spellInfo->Effects[i].Mechanic) + if (mechanicMask & (1 << spellInfo->Effects[i].Mechanic)) + return true; + } + + return false; +} + +bool Unit::HasStrongerAuraWithDR(SpellInfo const* auraSpellInfo, Unit* caster, bool triggered) const +{ + DiminishingGroup diminishGroup = auraSpellInfo->GetDiminishingReturnsGroupForSpell(triggered); + DiminishingLevels level = GetDiminishing(diminishGroup); + for (auto itr = m_appliedAuras.begin(); itr != m_appliedAuras.end(); ++itr) + { + SpellInfo const* spellInfo = itr->second->GetBase()->GetSpellInfo(); + if (spellInfo->GetDiminishingReturnsGroupForSpell(triggered) != diminishGroup) + continue; + + int32 existingDuration = itr->second->GetBase()->GetDuration(); + int32 newDuration = auraSpellInfo->GetMaxDuration(); + ApplyDiminishingToDuration(auraSpellInfo, triggered, newDuration, caster, level); + if (newDuration > 0 && newDuration < existingDuration) + return true; + } + + return false; +} + +AuraEffect* Unit::IsScriptOverriden(SpellInfo const* spell, int32 script) const +{ + AuraEffectList const& auras = GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS); + for (AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + if ((*i)->GetMiscValue() == script) + if ((*i)->IsAffectingSpell(spell)) + return (*i); + } + return nullptr; +} + +uint32 Unit::GetDiseasesByCaster(ObjectGuid casterGUID, bool remove) +{ + static const AuraType diseaseAuraTypes[] = + { + SPELL_AURA_PERIODIC_DAMAGE, // Frost Fever and Blood Plague + SPELL_AURA_LINKED // Crypt Fever and Ebon Plague + }; + + // Burning Blood - Blood DK T12 Bonus + // "Causes owning Death Knight's abilities to treat the target as if it had 2 diseases active." + if (HasAura(98957, casterGUID)) + return 2; + + uint32 diseases = 0; + for (AuraType aType : diseaseAuraTypes) + { + for (auto itr = m_modAuras[aType].begin(); itr != m_modAuras[aType].end();) + { + // Get auras with disease dispel type by caster + if ((*itr)->GetSpellInfo()->Dispel == DISPEL_DISEASE + && (*itr)->GetCasterGUID() == casterGUID) + { + ++diseases; + + if (remove) + { + RemoveAura((*itr)->GetId(), (*itr)->GetCasterGUID()); + itr = m_modAuras[aType].begin(); + continue; + } + } + ++itr; + } + } + return diseases; +} + +uint32 Unit::GetDoTsByCaster(ObjectGuid casterGUID) const +{ + static const AuraType diseaseAuraTypes[] = + { + SPELL_AURA_PERIODIC_DAMAGE, + SPELL_AURA_PERIODIC_DAMAGE_PERCENT, + SPELL_AURA_NONE + }; + + uint32 dots = 0; + for (AuraType const* itr = &diseaseAuraTypes[0]; itr && itr[0] != SPELL_AURA_NONE; ++itr) + { + Unit::AuraEffectList const& auras = GetAuraEffectsByType(*itr); + for (AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + // Get auras by caster + if ((*i)->GetCasterGUID() == casterGUID) + ++dots; + } + } + return dots; +} + +int32 Unit::GetTotalAuraModifier(AuraType auraType, std::function const& predicate) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + if (mTotalAuraList.empty()) + return 0; + + std::map sameEffectSpellGroup; + int32 modifier = 0; + + for (AuraEffect const* aurEff : mTotalAuraList) + { + if (predicate(aurEff)) + { + // Check if the Aura Effect has a the Same Effect Stack Rule and if so, use the highest amount of that SpellGroup + // If the Aura Effect does not have this Stack Rule, it returns false so we can add to the multiplier as usual + if (!sSpellMgr->AddSameEffectStackRuleSpellGroups(aurEff->GetSpellInfo(), static_cast(auraType), aurEff->GetAmount(), sameEffectSpellGroup)) + modifier += aurEff->GetAmount(); + } + } + + // Add the highest of the Same Effect Stack Rule SpellGroups to the accumulator + for (auto itr = sameEffectSpellGroup.begin(); itr != sameEffectSpellGroup.end(); ++itr) + modifier += itr->second; + + return modifier; +} + +float Unit::GetTotalAuraMultiplier(AuraType auraType, std::function const& predicate) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + if (mTotalAuraList.empty()) + return 1.0f; + + std::map sameEffectSpellGroup; + float multiplier = 1.0f; + + for (AuraEffect const* aurEff : mTotalAuraList) + { + if (predicate(aurEff)) + { + // Check if the Aura Effect has a the Same Effect Stack Rule and if so, use the highest amount of that SpellGroup + // If the Aura Effect does not have this Stack Rule, it returns false so we can add to the multiplier as usual + if (!sSpellMgr->AddSameEffectStackRuleSpellGroups(aurEff->GetSpellInfo(), static_cast(auraType), aurEff->GetAmount(), sameEffectSpellGroup)) + AddPct(multiplier, aurEff->GetAmount()); + } + } + + // Add the highest of the Same Effect Stack Rule SpellGroups to the multiplier + for (auto itr = sameEffectSpellGroup.begin(); itr != sameEffectSpellGroup.end(); ++itr) + AddPct(multiplier, itr->second); + + return multiplier; +} + +int32 Unit::GetMaxPositiveAuraModifier(AuraType auraType, std::function const& predicate) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + if (mTotalAuraList.empty()) + return 0; + + int32 modifier = 0; + for (AuraEffect const* aurEff : mTotalAuraList) + { + if (predicate(aurEff)) + modifier = std::max(modifier, aurEff->GetAmount()); + } + + return modifier; +} + +int32 Unit::GetMaxNegativeAuraModifier(AuraType auraType, std::function const& predicate) const +{ + AuraEffectList const& mTotalAuraList = GetAuraEffectsByType(auraType); + if (mTotalAuraList.empty()) + return 0; + + int32 modifier = 0; + for (AuraEffect const* aurEff : mTotalAuraList) + { + if (predicate(aurEff)) + modifier = std::min(modifier, aurEff->GetAmount()); + } + + return modifier; +} + +int32 Unit::GetTotalAuraModifier(AuraType auraType) const +{ + return GetTotalAuraModifier(auraType, [](AuraEffect const* /*aurEff*/) { return true; }); +} + +float Unit::GetTotalAuraMultiplier(AuraType auraType) const +{ + return GetTotalAuraMultiplier(auraType, [](AuraEffect const* /*aurEff*/) { return true; }); +} + +int32 Unit::GetMaxPositiveAuraModifier(AuraType auraType) const +{ + return GetMaxPositiveAuraModifier(auraType, [](AuraEffect const* /*aurEff*/) { return true; }); +} + +int32 Unit::GetMaxNegativeAuraModifier(AuraType auraType) const +{ + return GetMaxNegativeAuraModifier(auraType, [](AuraEffect const* /*aurEff*/) { return true; }); +} + +int32 Unit::GetTotalAuraModifierByMiscMask(AuraType auraType, uint32 miscMask) const +{ + return GetTotalAuraModifier(auraType, [miscMask](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & miscMask) != 0) + return true; + return false; + }); +} + +float Unit::GetTotalAuraMultiplierByMiscMask(AuraType auraType, uint32 miscMask) const +{ + return GetTotalAuraMultiplier(auraType, [miscMask](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & miscMask) != 0) + return true; + return false; + }); +} + +int32 Unit::GetMaxPositiveAuraModifierByMiscMask(AuraType auraType, uint32 miscMask, AuraEffect const* except /*= nullptr*/) const +{ + return GetMaxPositiveAuraModifier(auraType, [miscMask, except](AuraEffect const* aurEff) -> bool + { + if (except != aurEff && (aurEff->GetMiscValue() & miscMask) != 0) + return true; + return false; + }); +} + +int32 Unit::GetMaxNegativeAuraModifierByMiscMask(AuraType auraType, uint32 miscMask) const +{ + return GetMaxNegativeAuraModifier(auraType, [miscMask](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & miscMask) != 0) + return true; + return false; + }); +} + +int32 Unit::GetTotalAuraModifierByMiscValue(AuraType auraType, int32 miscValue) const +{ + return GetTotalAuraModifier(auraType, [miscValue](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetMiscValue() == miscValue) + return true; + return false; + }); +} + +float Unit::GetTotalAuraMultiplierByMiscValue(AuraType auraType, int32 miscValue) const +{ + return GetTotalAuraMultiplier(auraType, [miscValue](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetMiscValue() == miscValue) + return true; + return false; + }); +} + +int32 Unit::GetMaxPositiveAuraModifierByMiscValue(AuraType auraType, int32 miscValue) const +{ + return GetMaxPositiveAuraModifier(auraType, [miscValue](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetMiscValue() == miscValue) + return true; + return false; + }); +} + +int32 Unit::GetMaxNegativeAuraModifierByMiscValue(AuraType auraType, int32 miscValue) const +{ + return GetMaxNegativeAuraModifier(auraType, [miscValue](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetMiscValue() == miscValue) + return true; + return false; + }); +} + +int32 Unit::GetTotalAuraModifierByAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + return GetTotalAuraModifier(auraType, [affectedSpell](AuraEffect const* aurEff) -> bool + { + if (aurEff->IsAffectingSpell(affectedSpell)) + return true; + return false; + }); +} + +float Unit::GetTotalAuraMultiplierByAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + return GetTotalAuraMultiplier(auraType, [affectedSpell](AuraEffect const* aurEff) -> bool + { + if (aurEff->IsAffectingSpell(affectedSpell)) + return true; + return false; + }); +} + +int32 Unit::GetMaxPositiveAuraModifierByAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + return GetMaxPositiveAuraModifier(auraType, [affectedSpell](AuraEffect const* aurEff) -> bool + { + if (aurEff->IsAffectingSpell(affectedSpell)) + return true; + return false; + }); +} + +int32 Unit::GetMaxNegativeAuraModifierByAffectMask(AuraType auraType, SpellInfo const* affectedSpell) const +{ + return GetMaxNegativeAuraModifier(auraType, [affectedSpell](AuraEffect const* aurEff) -> bool + { + if (aurEff->IsAffectingSpell(affectedSpell)) + return true; + return false; + }); +} + +void Unit::UpdateResistanceBuffModsMod(SpellSchools school) +{ + float modPos = 0.0f; + float modNeg = 0.0f; + + // these auras are always positive + modPos = GetMaxPositiveAuraModifierByMiscMask(SPELL_AURA_MOD_RESISTANCE_EXCLUSIVE, 1 << school); + modPos += GetTotalAuraModifier(SPELL_AURA_MOD_RESISTANCE, [school](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & (1 << school)) && aurEff->GetAmount() > 0) + return true; + return false; + }); + + modNeg = GetTotalAuraModifier(SPELL_AURA_MOD_RESISTANCE, [school](AuraEffect const* aurEff) -> bool + { + if ((aurEff->GetMiscValue() & (1 << school)) && aurEff->GetAmount() < 0) + return true; + return false; + }); + + float factor = GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_RESISTANCE_PCT, 1 << school); + modPos *= factor; + modNeg *= factor; + + SetFloatValue(UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + AsUnderlyingType(school), modPos); + SetFloatValue(UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + AsUnderlyingType(school), modNeg); +} + +void Unit::_RegisterDynObject(DynamicObject* dynObj) +{ + m_dynObj.push_back(dynObj); + if (GetTypeId() == TYPEID_UNIT && IsAIEnabled()) + ToCreature()->AI()->JustRegisteredDynObject(dynObj); +} + +void Unit::_UnregisterDynObject(DynamicObject* dynObj) +{ + m_dynObj.remove(dynObj); + if (GetTypeId() == TYPEID_UNIT && IsAIEnabled()) + ToCreature()->AI()->JustUnregisteredDynObject(dynObj); +} + +DynamicObject* Unit::GetDynObject(uint32 spellId) const +{ + std::vector dynamicobjects = GetDynObjects(spellId); + return dynamicobjects.empty() ? nullptr : dynamicobjects.front(); +} + +std::vector Unit::GetDynObjects(uint32 spellId) const +{ + std::vector dynamicobjects; + for (DynObjectList::const_iterator i = m_dynObj.begin(); i != m_dynObj.end(); ++i) + if ((*i)->GetSpellId() == spellId) + dynamicobjects.push_back(*i); + + return dynamicobjects; +} + +void Unit::RemoveDynObject(uint32 spellId) +{ + if (m_dynObj.empty()) + return; + for (DynObjectList::iterator i = m_dynObj.begin(); i != m_dynObj.end();) + { + DynamicObject* dynObj = *i; + if (dynObj->GetSpellId() == spellId) + { + dynObj->Remove(); + i = m_dynObj.begin(); + } + else + ++i; + } +} + +void Unit::RemoveAllDynObjects() +{ + while (!m_dynObj.empty()) + m_dynObj.front()->Remove(); +} + +GameObject* Unit::GetGameObject(uint32 spellId) const +{ + std::vector gameobjects = GetGameObjects(spellId); + return gameobjects.empty() ? nullptr : gameobjects.front(); +} + +std::vector Unit::GetGameObjects(uint32 spellId) const +{ + std::vector gameobjects; + for (GameObjectList::const_iterator i = m_gameObj.begin(); i != m_gameObj.end(); ++i) + if ((*i)->GetSpellId() == spellId) + gameobjects.push_back(*i); + + return gameobjects; +} + +void Unit::AddGameObject(GameObject* gameObj) +{ + if (!gameObj || gameObj->GetOwnerGUID()) + return; + + m_gameObj.push_back(gameObj); + gameObj->SetOwnerGUID(GetGUID()); + + if (gameObj->GetSpellId()) + { + SpellInfo const* createBySpell = sSpellMgr->GetSpellInfo(gameObj->GetSpellId()); + // Need disable spell use for owner + if (createBySpell && createBySpell->IsCooldownStartedOnEvent()) + // note: item based cooldowns and cooldown spell mods with charges ignored (unknown existing cases) + GetSpellHistory()->StartCooldown(createBySpell, 0, nullptr, true); + } + + if (GetTypeId() == TYPEID_UNIT && ToCreature()->IsAIEnabled()) + ToCreature()->AI()->JustSummonedGameobject(gameObj); +} + +void Unit::RemoveGameObject(GameObject* gameObj, bool del) +{ + if (!gameObj || gameObj->GetOwnerGUID() != GetGUID()) + return; + + gameObj->SetOwnerGUID(ObjectGuid::Empty); + + for (uint8 i = 0; i < MAX_GAMEOBJECT_SLOT; ++i) + { + if (m_ObjectSlot[i] == gameObj->GetGUID()) + { + m_ObjectSlot[i].Clear(); + break; + } + } + + // GO created by some spell + if (uint32 spellid = gameObj->GetSpellId()) + { + RemoveAurasDueToSpell(spellid); + + SpellInfo const* createBySpell = sSpellMgr->GetSpellInfo(spellid); + // Need activate spell use for owner + if (createBySpell && createBySpell->IsCooldownStartedOnEvent()) + // note: item based cooldowns and cooldown spell mods with charges ignored (unknown existing cases) + GetSpellHistory()->SendCooldownEvent(createBySpell); + } + + m_gameObj.remove(gameObj); + + if (GetTypeId() == TYPEID_UNIT && ToCreature()->IsAIEnabled()) + ToCreature()->AI()->SummonedGameobjectDespawn(gameObj); + + if (del) + { + gameObj->SetRespawnTime(0); + gameObj->Delete(); + } +} + +void Unit::RemoveGameObject(uint32 spellid, bool del) +{ + if (m_gameObj.empty()) + return; + GameObjectList::iterator i, next; + for (i = m_gameObj.begin(); i != m_gameObj.end(); i = next) + { + next = i; + if (spellid == 0 || (*i)->GetSpellId() == spellid) + { + (*i)->SetOwnerGUID(ObjectGuid::Empty); + if (del) + { + (*i)->SetRespawnTime(0); + (*i)->Delete(); + } + + next = m_gameObj.erase(i); + } + else + ++next; + } +} + +void Unit::RemoveAllGameObjects() +{ + // remove references to unit + while (!m_gameObj.empty()) + { + GameObjectList::iterator i = m_gameObj.begin(); + (*i)->SetOwnerGUID(ObjectGuid::Empty); + (*i)->SetRespawnTime(0); + (*i)->Delete(); + m_gameObj.erase(i); + } +} + +void Unit::SendSpellNonMeleeDamageLog(SpellNonMeleeDamage* log) +{ + WorldPacket data(SMSG_SPELLNONMELEEDAMAGELOG, (16+4+4+4+1+4+4+1+1+4+4+1)); // we guess size + data << log->target->GetPackGUID(); + data << log->attacker->GetPackGUID(); + data << uint32(log->SpellID); + data << uint32(log->damage); // damage amount + int32 overkill = log->damage - log->target->GetHealth(); + data << uint32(overkill > 0 ? overkill : 0); // overkill + data << uint8 (log->schoolMask); // damage school + data << uint32(log->absorb); // AbsorbedDamage + data << uint32(log->resist); // resist + data << uint8 (log->periodicLog); // if 1, then client show spell name (example: %s's ranged shot hit %s for %u school or %s suffers %u school damage from %s's spell_name + data << uint8 (log->unused); // unused + data << uint32(log->blocked); // blocked + data << uint32(log->HitInfo); + data << uint8(log->HitInfo & (SPELL_HIT_TYPE_CRIT_DEBUG | SPELL_HIT_TYPE_HIT_DEBUG | SPELL_HIT_TYPE_ATTACK_TABLE_DEBUG)); + //if (log->HitInfo & SPELL_HIT_TYPE_CRIT_DEBUG) + //{ + // data << float(log->CritRoll); + // data << float(log->CritNeeded); + //} + //if (log->HitInfo & SPELL_HIT_TYPE_HIT_DEBUG) + //{ + // data << float(log->HitRoll); + // data << float(log->HitNeeded); + //} + //if (log->HitInfo & SPELL_HIT_TYPE_ATTACK_TABLE_DEBUG) + //{ + // data << float(log->MissChance); + // data << float(log->DodgeChance); + // data << float(log->ParryChance); + // data << float(log->BlockChance); + // data << float(log->GlanceChance); + // data << float(log->CrushChance); + //} + SendMessageToSet(&data, true); +} + +void Unit::SendSpellNonMeleeDamageLog(Unit* target, uint32 spellID, uint32 damage, SpellSchoolMask damageSchoolMask, uint32 absorbedDamage, uint32 resist, bool isPeriodic, uint32 blocked, bool criticalHit, bool split) +{ + SpellNonMeleeDamage log(this, target, spellID, damageSchoolMask); + log.damage = damage - absorbedDamage - resist - blocked; + log.absorb = absorbedDamage; + log.resist = resist; + log.periodicLog = isPeriodic; + log.blocked = blocked; + log.HitInfo = 0; + if (criticalHit) + log.HitInfo |= SPELL_HIT_TYPE_CRIT; + if (split) + log.HitInfo |= SPELL_HIT_TYPE_SPLIT; + SendSpellNonMeleeDamageLog(&log); +} + +/*static*/ void Unit::ProcSkillsAndAuras(Unit* actor, Unit* actionTarget, ProcFlags typeMaskActor, ProcFlags typeMaskActionTarget, + ProcFlagsSpellType spellTypeMask, ProcFlagsSpellPhase spellPhaseMask, ProcFlagsHit hitMask, Spell* spell, DamageInfo* damageInfo, HealInfo* healInfo) +{ + static constexpr int32 ProcChainHardLimit = 10; + if (spell && spell->GetProcChainLength() >= ProcChainHardLimit) + { + TC_LOG_ERROR("spells.aura.effect", "Unit::ProcSkillsAndAuras: Possible infinite proc loop detected, current triggering spell %u", spell->GetSpellInfo()->Id); + return; + } + + WeaponAttackType attType = damageInfo ? damageInfo->GetAttackType() : BASE_ATTACK; + SpellInfo const* spellInfo = [&]() -> SpellInfo const* + { + if (spell) + return spell->GetSpellInfo(); + if (damageInfo) + return damageInfo->GetSpellInfo(); + if (healInfo) + return healInfo->GetSpellInfo(); + return nullptr; + }(); + + if (typeMaskActor && actor && !(spellInfo && spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_CASTER_PROCS))) + actor->ProcSkillsAndReactives(false, actionTarget, typeMaskActor, hitMask, attType); + + if (typeMaskActionTarget && actionTarget && !(spellInfo && spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS))) + actionTarget->ProcSkillsAndReactives(true, actor, typeMaskActionTarget, hitMask, attType); + + if (actor) + actor->TriggerAurasProcOnEvent(actionTarget, typeMaskActor, typeMaskActionTarget, spellTypeMask, spellPhaseMask, hitMask, spell, damageInfo, healInfo); +} + +void Unit::SendPeriodicAuraLog(SpellPeriodicAuraLogInfo* pInfo) +{ + AuraEffect const* aura = pInfo->auraEff; + + WorldPackets::CombatLog::SpellPeriodicAuraLog periodicAuraLog; + periodicAuraLog.TargetGUID = GetGUID(); + periodicAuraLog.CasterGUID = aura->GetCasterGUID(); + periodicAuraLog.SpellID = aura->GetId(); + + WorldPackets::CombatLog::PeriodicAuraLogEffect& logEffect = periodicAuraLog.Entries.emplace_back(); + logEffect.Effect = aura->GetAuraType(); + logEffect.Amount = pInfo->damage; + logEffect.OverHealOrKill = pInfo->overDamage; + logEffect.Resisted = pInfo->resist; + logEffect.Crit = pInfo->critical; + + switch (logEffect.Effect) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + logEffect.AbsorbedOrAmplitude = pInfo->absorb; + logEffect.SchoolMaskOrPower = aura->GetSpellInfo()->GetSchoolMask(); + break; + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_OBS_MOD_HEALTH: + logEffect.AbsorbedOrAmplitude = pInfo->absorb; + break; + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_PERIODIC_ENERGIZE: + logEffect.SchoolMaskOrPower = aura->GetMiscValue(); + break; + case SPELL_AURA_PERIODIC_MANA_LEECH: + logEffect.SchoolMaskOrPower = aura->GetMiscValue(); + logEffect.AbsorbedOrAmplitude = pInfo->multiplier; + break; + default: + TC_LOG_ERROR("entities.unit", "Unit::SendPeriodicAuraLog: unknown aura %u", uint32(aura->GetAuraType())); + return; + } + + SendMessageToSet(periodicAuraLog.Write(), true); +} + +void Unit::SendSpellDamageResist(Unit* target, uint32 spellId) +{ + WorldPacket data(SMSG_PROCRESIST, 8+8+4+1); + data << uint64(GetGUID()); + data << uint64(target->GetGUID()); + data << uint32(spellId); + data << uint8(0); // bool - log format: 0-default, 1-debug + SendMessageToSet(&data, true); +} + +void Unit::SendSpellDamageImmune(Unit* target, uint32 spellId) +{ + WorldPacket data(SMSG_SPELLORDAMAGE_IMMUNE, 8+8+4+1); + data << uint64(GetGUID()); + data << uint64(target->GetGUID()); + data << uint32(spellId); + data << uint8(0); // bool - log format: 0-default, 1-debug + SendMessageToSet(&data, true); +} + +void Unit::SendAttackStateUpdate(CalcDamageInfo* damageInfo) +{ + WorldPackets::CombatLog::AttackerStateUpdate packet; + packet.HitInfo = damageInfo->HitInfo; + packet.AttackerGUID = damageInfo->Attacker->GetGUID(); + packet.VictimGUID = damageInfo->Target->GetGUID(); + packet.Damage = damageInfo->Damage; + int32 overkill = damageInfo->Damage - damageInfo->Target->GetHealth(); + packet.OverDamage = (overkill < 0 ? -1 : overkill); + + if (damageInfo->Damage) + { + packet.SubDmg.emplace(); + packet.SubDmg->SchoolMask = damageInfo->DamageSchoolMask; // School of sub damage + packet.SubDmg->FDamage = damageInfo->Damage; // sub damage + packet.SubDmg->Damage = damageInfo->Damage; // Sub Damage + packet.SubDmg->Absorbed = damageInfo->Absorb; + packet.SubDmg->Resisted = damageInfo->Resist; + } + + packet.VictimState = damageInfo->TargetState; + packet.BlockAmount = damageInfo->Blocked; + packet.RageGained = damageInfo->RageGained; + + SendMessageToSet(packet.Write(), true); +} + +void Unit::SendAttackStateUpdate(uint32 HitInfo, Unit* target, uint8 /*SwingType*/, SpellSchoolMask damageSchoolMask, uint32 Damage, uint32 AbsorbDamage, uint32 Resist, VictimState TargetState, uint32 BlockedAmount) +{ + CalcDamageInfo dmgInfo; + dmgInfo.HitInfo = HitInfo; + dmgInfo.Attacker = this; + dmgInfo.Target = target; + dmgInfo.Damage = Damage - AbsorbDamage - Resist - BlockedAmount; + dmgInfo.DamageSchoolMask = damageSchoolMask; + dmgInfo.Absorb = AbsorbDamage; + dmgInfo.Resist = Resist; + dmgInfo.TargetState = TargetState; + dmgInfo.Blocked = BlockedAmount; + dmgInfo.RageGained = 0; + SendAttackStateUpdate(&dmgInfo); +} + +void Unit::SetPowerType(PowerType power) +{ + if (GetPowerType() == power) + return; + + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_POWER_TYPE, power); + + // Creatures can swap out their power type at index 0 so we do keep track of the new power type here + if (IsCreature()) + { + _usedPowerTypes[GetPowerIndex(power)] = power; + UpdatePowerRegeneration(power); + } + + if (GetTypeId() == TYPEID_PLAYER) + { + if (ToPlayer()->GetGroup()) + ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_POWER_TYPE); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_POWER_TYPE); + } + } + + // Update max power + UpdateMaxPower(power); + + // Update current power + switch (power) + { + case POWER_MANA: // Keep the same (druid form switching...) + case POWER_ENERGY: + break; + case POWER_RAGE: // Reset to zero + SetPower(POWER_RAGE, 0); + break; + case POWER_FOCUS: // Make it full + SetFullPower(power); + break; + default: + break; + } +} + +PowerType Unit::CalculateDisplayPowerType() const +{ + PowerType displayPower = POWER_MANA; + switch (GetShapeshiftForm()) + { + case FORM_GHOUL: + case FORM_CAT: + displayPower = POWER_ENERGY; + break; + case FORM_BEAR: + displayPower = POWER_RAGE; + break; + case FORM_TRAVEL: + case FORM_GHOSTWOLF: + displayPower = POWER_MANA; + break; + default: + { + ChrClassesEntry const* cEntry = sChrClassesStore.LookupEntry(getClass()); + if (cEntry && cEntry->DisplayPower < MAX_POWERS) + displayPower = PowerType(cEntry->DisplayPower); + + if (Vehicle* vehicle = GetVehicleKit()) + { + if (PowerDisplayEntry const* powerDisplay = sPowerDisplayStore.LookupEntry(vehicle->GetVehicleInfo()->PowerDisplayID[0])) // To-do: 4.x has 3 power display id fields. + displayPower = PowerType(powerDisplay->ActualType); + else if (getClass() == CLASS_ROGUE) + displayPower = POWER_ENERGY; + } + else if (Pet const* pet = ToPet()) + { + if (pet->getPetType() == HUNTER_PET) // Hunter pets have focus + displayPower = POWER_FOCUS; + else if (pet->IsPetGhoul() || pet->IsRisenAlly()) // DK pets have energy + displayPower = POWER_ENERGY; + } + break; + } + } + + return displayPower; +} + +void Unit::UpdateDisplayPower() +{ + SetPowerType(CalculateDisplayPowerType()); +} + +void Unit::SetSheath(SheathState sheathed) +{ + SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_SHEATH_STATE, sheathed); + if (sheathed == SHEATH_STATE_UNARMED) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Sheathing); +} + +void Unit::_addAttacker(Unit* pAttacker) +{ + m_attackers.insert(pAttacker); +} + +void Unit::_removeAttacker(Unit* pAttacker) +{ + m_attackers.erase(pAttacker); +} + +Unit* Unit::getAttackerForHelper() const // If someone wants to help, who to give them +{ + if (!IsEngaged()) + return nullptr; + + if (Unit* victim = GetVictim()) + if ((!IsPet() && !IsCharmerOrSelfPlayer()) || IsInCombatWith(victim)) + return victim; + + CombatManager const& mgr = GetCombatManager(); + // pick arbitrary targets; our pvp combat > owner's pvp combat > our pve combat > owner's pve combat + Unit* owner = GetCharmerOrOwner(); + if (mgr.HasPvPCombat()) + return mgr.GetPvPCombatRefs().begin()->second->GetOther(this); + if (owner && (owner->GetCombatManager().HasPvPCombat())) + return owner->GetCombatManager().GetPvPCombatRefs().begin()->second->GetOther(owner); + if (mgr.HasPvECombat()) + return mgr.GetPvECombatRefs().begin()->second->GetOther(this); + if (owner && (owner->GetCombatManager().HasPvECombat())) + return owner->GetCombatManager().GetPvECombatRefs().begin()->second->GetOther(owner); + return nullptr; +} + +bool Unit::Attack(Unit* victim, bool meleeAttack) +{ + if (!victim || victim == this) + return false; + + // dead units can neither attack nor be attacked + if (!IsAlive() || !victim->IsInWorld() || !victim->IsAlive()) + return false; + + // player cannot attack in mount state + if (GetTypeId() == TYPEID_PLAYER && IsMounted()) + return false; + + Creature* creature = ToCreature(); + if (creature) + { + // creatures cannot attack while evading + if (creature->IsInEvadeMode()) + return false; + + if (!creature->CanMelee()) + meleeAttack = false; + } + + if (HasAuraType(SPELL_AURA_DISABLE_ATTACKING_EXCEPT_ABILITIES)) + return false; + + // nobody can attack GM in GM-mode + if (victim->GetTypeId() == TYPEID_PLAYER) + { + if (victim->ToPlayer()->IsGameMaster()) + return false; + } + else + { + if (victim->ToCreature()->IsEvadingAttacks()) + return false; + } + + if (m_attacking) + { + if (m_attacking == victim) + { + // switch to melee attack from ranged/magic + if (meleeAttack) + { + if (!HasUnitState(UNIT_STATE_MELEE_ATTACKING)) + { + AddUnitState(UNIT_STATE_MELEE_ATTACKING); + SendMeleeAttackStart(victim); + return true; + } + } + else if (HasUnitState(UNIT_STATE_MELEE_ATTACKING)) + { + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + SendMeleeAttackStop(victim); + return true; + } + return false; + } + + // switch target + InterruptSpell(CURRENT_MELEE_SPELL); + if (!meleeAttack) + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + } + + if (m_attacking) + m_attacking->_removeAttacker(this); + + m_attacking = victim; + m_attacking->_addAttacker(this); + + // Set our target + SetTarget(victim->GetGUID()); + + if (meleeAttack) + AddUnitState(UNIT_STATE_MELEE_ATTACKING); + + // set position before any AI calls/assistance + //if (GetTypeId() == TYPEID_UNIT) + // ToCreature()->SetCombatStartPosition(GetPositionX(), GetPositionY(), GetPositionZ()); + + if (creature && !IsControlledByPlayer()) + { + EngageWithTarget(victim); // ensure that anything we're attacking has threat + creature->CallAssistance(); + + // Remove emote state - will be restored on creature reset + SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + } + + // delay offhand weapon attack by 50% of the base attack time + if (haveOffhandWeapon() && GetTypeId() != TYPEID_PLAYER) + setAttackTimer(OFF_ATTACK, std::max(getAttackTimer(OFF_ATTACK), getAttackTimer(BASE_ATTACK) + uint32(CalculatePct(GetFloatValue(UNIT_FIELD_BASEATTACKTIME), 50)))); + + + if (meleeAttack) + SendMeleeAttackStart(victim); + + // Let the pet know we've started attacking someting. Handles melee attacks only + // Spells such as auto-shot and others handled in WorldSession::HandleCastSpellOpcode + if (GetTypeId() == TYPEID_PLAYER) + { + for (Unit* controlled : m_Controlled) + if (Creature* cControlled = controlled->ToCreature()) + if (CreatureAI* controlledAI = cControlled->AI()) + controlledAI->OwnerAttacked(victim); + } + + return true; +} + +bool Unit::AttackStop() +{ + if (!m_attacking) + return false; + + Unit* victim = m_attacking; + + m_attacking->_removeAttacker(this); + m_attacking = nullptr; + + // Clear our target + SetTarget(ObjectGuid::Empty); + + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + + InterruptSpell(CURRENT_MELEE_SPELL); + + // reset only at real combat stop + if (Creature* creature = ToCreature()) + creature->SetNoCallAssistance(false); + + SendMeleeAttackStop(victim); + + return true; +} + +void Unit::ValidateAttackersAndOwnTarget() +{ + // iterate attackers + UnitVector toRemove; + AttackerSet const& attackers = getAttackers(); + for (Unit* attacker : attackers) + if (!attacker->IsValidAttackTarget(this)) + toRemove.push_back(attacker); + + for (Unit* attacker : toRemove) + attacker->AttackStop(); + + // remove our own victim + if (Unit* victim = GetVictim()) + if (!IsValidAttackTarget(victim)) + AttackStop(); +} + +void Unit::CombatStop(bool includingCast, bool mutualPvP, bool (*unitFilter)(Unit const* otherUnit)) +{ + if (includingCast && IsNonMeleeSpellCast(false)) + InterruptNonMeleeSpells(false); + + AttackStop(); + + if (!unitFilter) + RemoveAllAttackers(); + else + { + std::vector attackersToRemove; + attackersToRemove.reserve(m_attackers.size()); + std::copy_if(m_attackers.begin(), m_attackers.end(), std::back_inserter(attackersToRemove), unitFilter); + + for (Unit* attacker : attackersToRemove) + attacker->AttackStop(); + } + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->SendAttackSwingCancelAttack(); // melee and ranged forced attack cancel + + m_combatManager.EndAllPvECombat(unitFilter); + if (mutualPvP) + m_combatManager.EndAllPvPCombat(unitFilter); + else // vanish and brethren are weird + m_combatManager.SuppressPvPCombat(unitFilter); +} + +void Unit::CombatStopWithPets(bool includingCast) +{ + CombatStop(includingCast); + + for (Unit* minion : m_Controlled) + minion->CombatStop(includingCast); +} + +bool Unit::isAttackingPlayer() const +{ + if (HasUnitState(UNIT_STATE_ATTACK_PLAYER)) + return true; + + for (ControlList::const_iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) + if ((*itr)->isAttackingPlayer()) + return true; + + for (uint8 i = 0; i < MAX_SUMMON_SLOT; ++i) + if (m_SummonSlot[i]) + if (Creature* summon = GetMap()->GetCreature(m_SummonSlot[i])) + if (summon->isAttackingPlayer()) + return true; + + return false; +} + +void Unit::RemoveAllAttackers() +{ + while (!m_attackers.empty()) + { + AttackerSet::iterator iter = m_attackers.begin(); + if (!(*iter)->AttackStop()) + { + TC_LOG_ERROR("entities.unit", "WORLD: Unit has an attacker that isn't attacking it!"); + m_attackers.erase(iter); + } + } +} + +void Unit::ModifyAuraState(AuraStateType flag, bool apply) +{ + if (apply) + { + if (!HasFlag(UNIT_FIELD_AURASTATE, 1<<(flag-1))) + { + SetFlag(UNIT_FIELD_AURASTATE, 1<<(flag-1)); + if (GetTypeId() == TYPEID_PLAYER) + { + PlayerSpellMap const& sp_list = ToPlayer()->GetSpellMap(); + for (PlayerSpellMap::const_iterator itr = sp_list.begin(); itr != sp_list.end(); ++itr) + { + if (itr->second.state == PLAYERSPELL_REMOVED || itr->second.disabled) + continue; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr->first); + if (!spellInfo || !spellInfo->IsPassive()) + continue; + if (spellInfo->CasterAuraState == uint32(flag)) + CastSpell(this, itr->first, true); + } + } + else if (Pet* pet = ToCreature()->ToPet()) + { + for (PetSpellMap::const_iterator itr = pet->m_spells.begin(); itr != pet->m_spells.end(); ++itr) + { + if (itr->second.state == PETSPELL_REMOVED) + continue; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr->first); + if (!spellInfo || !spellInfo->IsPassive()) + continue; + if (spellInfo->CasterAuraState == uint32(flag)) + CastSpell(this, itr->first, true); + } + } + } + } + else + { + if (HasFlag(UNIT_FIELD_AURASTATE, 1<<(flag-1))) + { + RemoveFlag(UNIT_FIELD_AURASTATE, 1<<(flag-1)); + + Unit::AuraApplicationMap& tAuras = GetAppliedAuras(); + for (Unit::AuraApplicationMap::iterator itr = tAuras.begin(); itr != tAuras.end();) + { + SpellInfo const* spellProto = itr->second->GetBase()->GetSpellInfo(); + if (itr->second->GetBase()->GetCasterGUID() == GetGUID() && spellProto->CasterAuraState == uint32(flag) && (spellProto->IsPassive() || flag != AURA_STATE_ENRAGE)) + RemoveAura(itr); + else + ++itr; + } + } + } +} + +uint32 Unit::BuildAuraStateUpdateForTarget(Unit* target) const +{ + uint32 auraStates = GetUInt32Value(UNIT_FIELD_AURASTATE) &~(PER_CASTER_AURA_STATE_MASK); + for (AuraStateAurasMap::const_iterator itr = m_auraStateAuras.begin(); itr != m_auraStateAuras.end(); ++itr) + if ((1 << (itr->first - 1)) & PER_CASTER_AURA_STATE_MASK) + if (itr->second->GetBase()->GetCasterGUID() == target->GetGUID()) + auraStates |= (1 << (itr->first - 1)); + + return auraStates; +} + +bool Unit::HasAuraState(AuraStateType flag, SpellInfo const* spellProto, Unit const* Caster) const +{ + if (Caster) + { + if (spellProto) + { + AuraEffectList const& stateAuras = Caster->GetAuraEffectsByType(SPELL_AURA_ABILITY_IGNORE_AURASTATE); + for (AuraEffect const* aurEff : stateAuras) + if (aurEff->IsAffectingSpell(spellProto)) + return true; + } + + // Check per caster aura state + // If aura with aurastate by caster not found return false + if ((1 << (flag - 1)) & PER_CASTER_AURA_STATE_MASK) + { + AuraStateAurasMapBounds range = m_auraStateAuras.equal_range(flag); + for (AuraStateAurasMap::const_iterator itr = range.first; itr != range.second; ++itr) + if (itr->second->GetBase()->GetCasterGUID() == Caster->GetGUID()) + return true; + return false; + } + } + + return HasFlag(UNIT_FIELD_AURASTATE, 1 << (flag - 1)); +} + +void Unit::SetOwnerGUID(ObjectGuid owner) +{ + if (GetOwnerGUID() == owner) + return; + + SetGuidValue(UNIT_FIELD_SUMMONEDBY, owner); + if (!owner) + return; + + // Update owner dependent fields + Player* player = ObjectAccessor::GetPlayer(*this, owner); + if (!player || !player->HaveAtClient(this)) // if player cannot see this unit yet, he will receive needed data with create object + return; + + SetFieldNotifyFlag(UF_FLAG_OWNER); + + UpdateData udata(GetMapId()); + WorldPacket packet; + BuildValuesUpdateBlockForPlayer(&udata, player); + udata.BuildPacket(&packet); + player->SendDirectMessage(&packet); + + RemoveFieldNotifyFlag(UF_FLAG_OWNER); +} + +Player* Unit::GetControllingPlayer() const +{ + if (ObjectGuid guid = GetCharmerOrOwnerGUID()) + { + if (Unit* master = ObjectAccessor::GetUnit(*this, guid)) + return master->GetControllingPlayer(); + return nullptr; + } + else + return const_cast(ToPlayer()); +} + +Minion* Unit::GetFirstMinion() const +{ + if (ObjectGuid pet_guid = GetMinionGUID()) + { + if (Creature* pet = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, pet_guid)) + if (pet->HasUnitTypeMask(UNIT_MASK_MINION)) + return (Minion*)pet; + + TC_LOG_ERROR("entities.unit", "Unit::GetFirstMinion: Minion %s not exist.", pet_guid.ToString().c_str()); + const_cast(this)->SetMinionGUID(ObjectGuid::Empty); + } + + return nullptr; +} + +Guardian* Unit::GetGuardianPet() const +{ + if (ObjectGuid pet_guid = GetPetGUID()) + { + if (Creature* pet = ObjectAccessor::GetCreatureOrPetOrVehicle(*this, pet_guid)) + if (pet->HasUnitTypeMask(UNIT_MASK_GUARDIAN)) + return (Guardian*)pet; + + TC_LOG_FATAL("entities.unit", "Unit::GetGuardianPet: Guardian %s not exist.", pet_guid.ToString().c_str()); + const_cast(this)->SetPetGUID(ObjectGuid::Empty); + } + + return nullptr; +} + +void Unit::SetMinion(Minion* minion, bool apply) +{ + if (!minion) + { + TC_LOG_ERROR("entities.unit", "Unit::SetMinion: Unit %s tried to reference a non existing minion", GetGUID().ToString().c_str()); + return; + } + + TC_LOG_DEBUG("entities.unit", "SetMinion %u for %u, apply %u", minion->GetEntry(), GetEntry(), apply); + + if (apply) + { + if (minion->GetOwnerGUID()) + { + TC_LOG_FATAL("entities.unit", "SetMinion: Minion %u is not the minion of owner %u", minion->GetEntry(), GetEntry()); + return; + } + + minion->SetOwnerGUID(GetGUID()); + + m_Controlled.insert(minion); + + if (GetTypeId() == TYPEID_PLAYER) + { + minion->m_ControlledByPlayer = true; + minion->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + } + + // Can only have one pet. If a new one is summoned, dismiss the old one. + if (minion->IsGuardianPet()) + { + if (Guardian* oldPet = GetGuardianPet()) + { + if (oldPet != minion && (oldPet->IsPet() || minion->IsPet() || oldPet->GetEntry() != minion->GetEntry())) + { + // remove existing minion pet + if (oldPet->IsPet()) + ((Pet*)oldPet)->Remove(PET_SAVE_DISMISS); + else + oldPet->UnSummon(); + SetPetGUID(minion->GetGUID()); + SetMinionGUID(ObjectGuid::Empty); + } + } + else + { + SetPetGUID(minion->GetGUID()); + SetMinionGUID(ObjectGuid::Empty); + } + } + + if (minion->HasUnitTypeMask(UNIT_MASK_CONTROLABLE_GUARDIAN)) + AddGuidValue(UNIT_FIELD_SUMMON, minion->GetGUID()); + + if (minion->m_Properties && SummonTitle(minion->m_Properties->Title) == SummonTitle::Companion) + SetCritterGUID(minion->GetGUID()); + + // PvP, FFAPvP + minion->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, GetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG)); + + // Send infinity cooldown - client does that automatically but after relog cooldown needs to be set again + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(minion->GetUInt32Value(UNIT_CREATED_BY_SPELL)); + + if (spellInfo && spellInfo->IsCooldownStartedOnEvent()) + GetSpellHistory()->StartCooldown(spellInfo, 0, nullptr, true); + } + else + { + if (minion->GetOwnerGUID() != GetGUID()) + { + TC_LOG_FATAL("entities.unit", "SetMinion: Minion %u is not the minion of owner %u", minion->GetEntry(), GetEntry()); + return; + } + + if (m_Controlled.find(minion) != m_Controlled.end()) + m_Controlled.erase(minion); + + if (minion->m_Properties && SummonTitle(minion->m_Properties->Title) == SummonTitle::Companion) + if (GetCritterGUID() == minion->GetGUID()) + SetCritterGUID(ObjectGuid::Empty); + + if (minion->IsGuardianPet()) + { + if (GetPetGUID() == minion->GetGUID()) + SetPetGUID(ObjectGuid::Empty); + } + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(minion->GetUInt32Value(UNIT_CREATED_BY_SPELL)); + // Remove infinity cooldown + if (spellInfo && (spellInfo->IsCooldownStartedOnEvent())) + GetSpellHistory()->SendCooldownEvent(spellInfo); + + //if (minion->HasUnitTypeMask(UNIT_MASK_GUARDIAN)) + { + if (RemoveGuidValue(UNIT_FIELD_SUMMON, minion->GetGUID())) + { + // Check if there is another minion + for (Unit const* controlled : m_Controlled) + { + // do not use this check, creature do not have charm guid + //if (GetCharmedGUID() == (*itr)->GetGUID()) + if (GetGUID() == controlled->GetCharmerGUID()) + continue; + + //ASSERT((*itr)->GetOwnerGUID() == GetGUID()); + if (controlled->GetOwnerOrCreatorGUID() != GetGUID()) + { + OutDebugInfo(); + controlled->OutDebugInfo(); + ABORT(); + } + ASSERT(controlled->IsCreature()); + + if (!controlled->HasUnitTypeMask(UNIT_MASK_CONTROLABLE_GUARDIAN)) + continue; + + if (AddGuidValue(UNIT_FIELD_SUMMON, controlled->GetGUID())) + { + // show another pet bar if there is no charm bar + if (IsPlayer() && !GetCharmedGUID()) + { + if (controlled->IsPet()) + ToPlayer()->PetSpellInitialize(); + else + ToPlayer()->CharmSpellInitialize(); + } + } + break; + } + } + } + } + UpdatePetCombatState(); +} + +void Unit::GetAllMinionsByEntry(std::list& Minions, uint32 entry) +{ + for (Unit::ControlList::iterator itr = m_Controlled.begin(); itr != m_Controlled.end();) + { + Unit* unit = *itr; + ++itr; + if (unit->GetEntry() == entry && unit->GetTypeId() == TYPEID_UNIT + && unit->IsSummon()) // minion, actually + Minions.push_back(unit->ToCreature()); + } +} + +void Unit::RemoveAllMinionsByEntry(uint32 entry) +{ + for (Unit::ControlList::iterator itr = m_Controlled.begin(); itr != m_Controlled.end();) + { + Unit* unit = *itr; + ++itr; + if (unit->GetEntry() == entry && unit->GetTypeId() == TYPEID_UNIT + && unit->IsSummon()) // minion, actually + unit->ToTempSummon()->UnSummon(); + // i think this is safe because i have never heard that a despawned minion will trigger a same minion + } +} + +void Unit::SetCharm(Unit* charm, bool apply) +{ + if (apply) + { + if (GetTypeId() == TYPEID_PLAYER) + { + ASSERT(AddGuidValue(UNIT_FIELD_CHARM, charm->GetGUID()), + "Player %s is trying to charm unit %u, but it already has a charmed unit %s", GetName().c_str(), charm->GetEntry(), GetCharmedGUID().ToString().c_str()); + m_charmed = charm; + + charm->m_ControlledByPlayer = true; + /// @todo maybe we can use this flag to check if controlled by player + charm->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + } + else + charm->m_ControlledByPlayer = false; + + // PvP, FFAPvP + charm->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, GetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG)); + + ASSERT(charm->AddGuidValue(UNIT_FIELD_CHARMEDBY, GetGUID()), + "Unit %u is being charmed, but it already has a charmer %s", charm->GetEntry(), charm->GetCharmerGUID().ToString().c_str()); + charm->m_charmer = this; + + _isWalkingBeforeCharm = charm->IsWalking(); + if (_isWalkingBeforeCharm) + charm->SetWalk(false); + + m_Controlled.insert(charm); + } + else + { + charm->ClearUnitState(UNIT_STATE_CHARMED); + + if (GetTypeId() == TYPEID_PLAYER) + { + ASSERT(RemoveGuidValue(UNIT_FIELD_CHARM, charm->GetGUID()), + "Player %s is trying to uncharm unit %u, but it has another charmed unit %s", GetName().c_str(), charm->GetEntry(), GetCharmedGUID().ToString().c_str()); + m_charmed = nullptr; + } + + ASSERT(charm->RemoveGuidValue(UNIT_FIELD_CHARMEDBY, GetGUID()), + "Unit %u is being uncharmed, but it has another charmer %s", charm->GetEntry(), charm->GetCharmerGUID().ToString().c_str()); + charm->m_charmer = nullptr; + + if (charm->GetTypeId() == TYPEID_PLAYER) + { + charm->m_ControlledByPlayer = true; + charm->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + charm->ToPlayer()->UpdatePvPState(); + } + else if (Player* player = charm->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + charm->m_ControlledByPlayer = true; + charm->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + charm->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, player->GetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG)); + } + else + { + charm->m_ControlledByPlayer = false; + charm->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + charm->SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, 0); + } + + if (charm->IsWalking() != _isWalkingBeforeCharm) + charm->SetWalk(_isWalkingBeforeCharm); + + if (charm->GetTypeId() == TYPEID_PLAYER + || !charm->ToCreature()->HasUnitTypeMask(UNIT_MASK_MINION) + || charm->GetOwnerOrCreatorGUID() != GetGUID()) + { + m_Controlled.erase(charm); + } + } + UpdatePetCombatState(); +} + +/*static*/ void Unit::DealHeal(HealInfo& healInfo) +{ + int32 gain = 0; + Unit* healer = healInfo.GetHealer(); + Unit* victim = healInfo.GetTarget(); + uint32 addhealth = healInfo.GetHeal(); + + if (healer) + { + if (UnitAI* victimAI = victim->GetAI()) + victimAI->HealReceived(healer, addhealth); + + if (UnitAI* healerAI = healer->GetAI()) + healerAI->HealDone(victim, addhealth); + } + + if (addhealth) + gain = victim->ModifyHealth(int32(addhealth)); + + // Hook for OnHeal Event + sScriptMgr->OnHeal(healer, victim, (uint32&)gain); + + Unit* unit = healer; + if (healer && healer->GetTypeId() == TYPEID_UNIT && healer->IsTotem()) + unit = healer->GetOwner(); + + if (unit) + { + if (Player* player = unit->ToPlayer()) + { + if (!healInfo.GetSpellInfo() || !healInfo.GetSpellInfo()->HasAttribute(SPELL_ATTR7_DO_NOT_COUNT_FOR_PVP_SCOREBOARD)) + if (Battleground* bg = player->GetBattleground()) + bg->UpdatePlayerScore(player, SCORE_HEALING_DONE, gain); + + // use the actual gain, as the overheal shall not be counted, skip gain 0 (it ignored anyway in to criteria) + if (gain) + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HEALING_DONE, gain, 0, 0, victim); + + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_HEAL_CAST, addhealth); + } + } + + if (Player* player = victim->ToPlayer()) + { + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_TOTAL_HEALING_RECEIVED, gain); + player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_HEALING_RECEIVED, addhealth); + } + + if (gain) + healInfo.SetEffectiveHeal(gain > 0 ? static_cast(gain) : 0UL); +} + +bool Unit::IsMagnet() const +{ + // Grounding Totem + if (GetUInt32Value(UNIT_CREATED_BY_SPELL) == 8177) /// @todo: find a more generic solution + return true; + + return false; +} + +Unit* Unit::GetMeleeHitRedirectTarget(Unit* victim, SpellInfo const* spellInfo) +{ + AuraEffectList const& hitTriggerAuras = victim->GetAuraEffectsByType(SPELL_AURA_ADD_CASTER_HIT_TRIGGER); + for (AuraEffectList::const_iterator i = hitTriggerAuras.begin(); i != hitTriggerAuras.end(); ++i) + { + if (Unit* magnet = (*i)->GetBase()->GetCaster()) + if (IsValidAttackTarget(magnet, spellInfo) && magnet->IsWithinLOSInMap(this) + && (!spellInfo || (spellInfo->CheckExplicitTarget(this, magnet) == SPELL_CAST_OK + && spellInfo->CheckTarget(this, magnet, false) == SPELL_CAST_OK))) + if (roll_chance_i((*i)->GetAmount())) + { + (*i)->GetBase()->DropCharge(AuraRemoveFlags::Expired); + return magnet; + } + } + return victim; +} + +Unit* Unit::GetFirstControlled() const +{ + // Sequence: charmed, pet, other guardians + Unit* unit = GetCharmed(); + if (!unit) + if (ObjectGuid guid = GetMinionGUID()) + unit = ObjectAccessor::GetUnit(*this, guid); + + return unit; +} + +void Unit::RemoveAllControlled() +{ + // possessed pet and vehicle + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->StopCastingCharm(); + + while (!m_Controlled.empty()) + { + Unit* target = *m_Controlled.begin(); + m_Controlled.erase(m_Controlled.begin()); + if (target->GetCharmerGUID() == GetGUID()) + target->RemoveCharmAuras(); + else if (target->GetOwnerOrCreatorGUID() == GetGUID() && target->IsSummon()) + target->ToTempSummon()->UnSummon(); + else + TC_LOG_ERROR("entities.unit", "Unit %u is trying to release unit %u which is neither charmed nor owned by it", GetEntry(), target->GetEntry()); + } + if (GetPetGUID()) + TC_LOG_FATAL("entities.unit", "Unit %u is not able to release its pet %s", GetEntry(), GetPetGUID().ToString().c_str()); + if (GetMinionGUID()) + TC_LOG_FATAL("entities.unit", "Unit %u is not able to release its minion %s", GetEntry(), GetMinionGUID().ToString().c_str()); + if (GetCharmedGUID()) + TC_LOG_FATAL("entities.unit", "Unit %u is not able to release its charm %s", GetEntry(), GetCharmedGUID().ToString().c_str()); + if (!IsPet()) // pets don't use the flag for this + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT); // m_controlled is now empty, so we know none of our minions are in combat +} + +bool Unit::isPossessedByPlayer() const +{ + return HasUnitState(UNIT_STATE_POSSESSED) && GetCharmerGUID().IsPlayer(); +} + +bool Unit::isPossessing(Unit* u) const +{ + return u->isPossessed() && GetCharmedGUID() == u->GetGUID(); +} + +bool Unit::isPossessing() const +{ + if (Unit* u = GetCharmed()) + return u->isPossessed(); + else + return false; +} + +Unit* Unit::GetCharmerOrSelf() const +{ + if (IsCharmed()) + return GetCharmer(); + else + return const_cast(this); +} + +Unit* Unit::GetNextRandomRaidMemberOrPet(float radius) +{ + Player* player = nullptr; + if (GetTypeId() == TYPEID_PLAYER) + player = ToPlayer(); + // Should we enable this also for charmed units? + else if (GetTypeId() == TYPEID_UNIT && IsPet()) + player = GetOwner()->ToPlayer(); + + if (!player) + return nullptr; + Group* group = player->GetGroup(); + // When there is no group check pet presence + if (!group) + { + // We are pet now, return owner + if (player != this) + return IsWithinDistInMap(player, radius) ? player : nullptr; + Unit* pet = GetGuardianPet(); + // No pet, no group, nothing to return + if (!pet) + return nullptr; + // We are owner now, return pet + return IsWithinDistInMap(pet, radius) ? pet : nullptr; + } + + std::vector nearMembers; + // reserve place for players and pets because resizing vector every unit push is unefficient (vector is reallocated then) + nearMembers.reserve(group->GetMembersCount() * 2); + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (Player* Target = itr->GetSource()) + { + // IsHostileTo check duel and controlled by enemy + if (Target != this && IsWithinDistInMap(Target, radius) && Target->IsAlive() && !IsHostileTo(Target)) + nearMembers.push_back(Target); + + // Push player's pet to vector + if (Unit* pet = Target->GetGuardianPet()) + if (pet != this && IsWithinDistInMap(pet, radius) && pet->IsAlive() && !IsHostileTo(pet)) + nearMembers.push_back(pet); + } + + if (nearMembers.empty()) + return nullptr; + + uint32 randTarget = urand(0, nearMembers.size()-1); + return nearMembers[randTarget]; +} + +// only called in Player::SetSeer +// so move it to Player? +void Unit::AddPlayerToVision(Player* player) +{ + if (m_sharedVision.empty()) + { + setActive(true); + SetWorldObject(true); + } + m_sharedVision.push_back(player); +} + +// only called in Player::SetSeer +void Unit::RemovePlayerFromVision(Player* player) +{ + m_sharedVision.remove(player); + if (m_sharedVision.empty()) + { + setActive(false); + SetWorldObject(false); + } +} + +void Unit::RemoveBindSightAuras() +{ + RemoveAurasByType(SPELL_AURA_BIND_SIGHT); +} + +void Unit::RemoveCharmAuras() +{ + RemoveAurasByType(SPELL_AURA_MOD_CHARM); + RemoveAurasByType(SPELL_AURA_MOD_POSSESS); + RemoveAurasByType(SPELL_AURA_AOE_CHARM); +} + +void Unit::UnsummonAllTotems() +{ + for (uint8 i = 0; i < MAX_SUMMON_SLOT; ++i) + { + if (!m_SummonSlot[i]) + continue; + + if (Creature* OldTotem = GetMap()->GetCreature(m_SummonSlot[i])) + if (OldTotem->IsSummon()) + OldTotem->ToTempSummon()->UnSummon(); + } +} + +void Unit::SendHealSpellLog(HealInfo& healInfo, bool critical /*= false*/) +{ + // we guess size + WorldPacket data(SMSG_SPELLHEALLOG, 8 + 8 + 4 + 4 + 4 + 4 + 1 + 1); + data << healInfo.GetTarget()->GetPackGUID(); + data << healInfo.GetHealer()->GetPackGUID(); + data << uint32(healInfo.GetSpellInfo()->Id); + data << uint32(healInfo.GetHeal()); + data << uint32(healInfo.GetHeal() - healInfo.GetEffectiveHeal()); + data << uint32(healInfo.GetAbsorb()); // Absorb amount + data << uint8(critical ? 1 : 0); + data << uint8(0); // unused + SendMessageToSet(&data, true); +} + +int32 Unit::HealBySpell(HealInfo& healInfo, bool critical /*= false*/) +{ + // calculate heal absorb and reduce healing + Unit::CalcHealAbsorb(healInfo); + Unit::DealHeal(healInfo); + SendHealSpellLog(healInfo, critical); + return healInfo.GetEffectiveHeal(); +} + +void Unit::SendEnergizeSpellLog(Unit* victim, uint32 spellId, int32 damage, PowerType powerType) +{ + WorldPackets::CombatLog::SpellEnergizeLog packet; + packet.TargetGUID = victim->GetGUID(); + packet.CasterGUID = GetGUID(); + packet.SpellID = spellId; + packet.Amount = damage; + packet.Type = powerType; + + SendMessageToSet(packet.Write(), true); +} + +void Unit::EnergizeBySpell(Unit* victim, uint32 spellId, int32 damage, PowerType powerType) +{ + if (SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId)) + EnergizeBySpell(victim, info, damage, powerType); +} + +void Unit::EnergizeBySpell(Unit* victim, SpellInfo const* spellInfo, int32 damage, PowerType powerType) +{ + victim->ModifyPower(powerType, damage, false); + victim->GetThreatManager().ForwardThreatForAssistingMe(this, float(damage) / 2, spellInfo, true); + SendEnergizeSpellLog(victim, spellInfo->Id, damage, powerType); +} + +int32 Unit::SpellDamageBonusDone(Unit* victim, SpellInfo const* spellProto, int32 pdamage, DamageEffectType damagetype, uint8 effIndex, uint32 stack /*= 1*/, Spell* spell /*= nullptr*/, AuraEffect const* aurEff /*= nullptr*/) const +{ + if (!spellProto || !victim) + return pdamage; + + int32 DoneTotal = 0; + float DoneTotalMod = 1.0f; + + auto callDamageScript = [&](int32& dmg, int32& flatMod, float& pctMod) + { + if (spell) + spell->CallScriptCalcDamageHandlers(victim, dmg, flatMod, pctMod); + else if (aurEff) + aurEff->GetBase()->CallScriptCalcDamageAndHealingHandlers(aurEff, aurEff->GetBase()->GetApplicationOfTarget(victim->GetGUID()), victim, dmg, flatMod, pctMod); + }; + + // Some spells don't benefit from done mods + if (damagetype == DIRECT_DAMAGE || spellProto->HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS)) + { + callDamageScript(pdamage, DoneTotal, DoneTotalMod); + return int32(std::max(float(pdamage + DoneTotal) * DoneTotalMod, 0.0f)); + } + + // Totems get their bonus damage from their owner + if (IsCreature() && IsTotem()) + if (Unit* creator = ObjectAccessor::GetUnit(*this, GetCreatorGUID())) + return creator->SpellDamageBonusDone(victim, spellProto, pdamage, damagetype, effIndex, stack, spell, aurEff); + + DoneTotalMod = SpellDamagePctDone(victim, spellProto, damagetype); + + // done scripted mod (take it from owner) + Unit const* owner = GetOwner() ? GetOwner() : this; + DoneTotal += owner->GetTotalAuraModifier(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS, [spellProto](AuraEffect const* aurEff) -> bool + { + if (!aurEff->IsAffectingSpell(spellProto)) + return false; + + switch (aurEff->GetMiscValue()) + { + case 4418: // Increased Shock Damage + case 4554: // Increased Lightning Damage + case 4555: // Improved Moonfire + case 5142: // Increased Lightning Damage + case 5147: // Improved Consecration / Libram of Resurgence + case 5148: // Idol of the Shooting Star + case 6008: // Increased Lightning Damage + case 8627: // Totem of Hex + return true; + default: + break; + } + + return false; + }); + + // Some spells don't benefit from pct done mods + if (!spellProto->HasAttribute(SPELL_ATTR6_IGNORE_CASTER_DAMAGE_MODIFIERS)) + DoneTotal += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_FLAT_SPELL_DAMAGE_VERSUS, victim->GetCreatureTypeMask()); + + // Done fixed damage bonus auras + int32 DoneAdvertisedBenefit = SpellBaseDamageBonusDone(spellProto->GetSchoolMask(), true); + // modify spell power by victim's SPELL_AURA_MOD_DAMAGE_TAKEN auras (eg Amplify/Dampen Magic) + DoneAdvertisedBenefit += GetTotalAuraModifier(SPELL_AURA_MOD_DAMAGE_TAKEN, [spellProto](AuraEffect const* aurEff) + { + if (spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if ((aurEff->GetMiscValue() & spellProto->GetSchoolMask()) != 0) + return true; + + return false; + }); + + // Custom scripted damage + switch (spellProto->SpellFamilyName) + { + case SPELLFAMILY_HUNTER: + // Hunter Pet basic attacks (RAP * 0.4 * 0.2) + if (spellProto->SpellFamilyFlags[0] & 0x00080000) + DoneTotal += (owner->GetTotalAttackPowerValue(RANGED_ATTACK) * 0.4f) * 0.2f; + + // Kill Command + if (spellProto->SpellFamilyFlags[1] & 0x00000800) + DoneTotal += owner->GetTotalAttackPowerValue(RANGED_ATTACK) * 0.516f; + break; + case SPELLFAMILY_WARLOCK: + // Warlock Minions inherit 50% of their owner's spell power + if (IsMinion() && GetOwner()) + DoneAdvertisedBenefit = owner->SpellBaseDamageBonusDone(spellProto->GetSchoolMask(), true) * 0.5f; + break; + case SPELLFAMILY_MAGE: + // Mage Water Elementals inherit 40% of their owner's spell power + if (IsMinion() && GetOwner()) + DoneAdvertisedBenefit = owner->SpellBaseDamageBonusDone(spellProto->GetSchoolMask(), true) * 0.4f; + break; + default: + break; + } + + if (spellProto->BonusCoefficientFromAP > 0.0f) + { + float ApCoeffMod = spellProto->BonusCoefficientFromAP; + if (Player* modOwner = GetSpellModOwner()) + { + ApCoeffMod *= 100.0f; + modOwner->ApplySpellMod(spellProto, SpellModOp::BonusCoefficient, ApCoeffMod); + ApCoeffMod /= 100.0f; + } + + WeaponAttackType const attType = [&]() + { + if ((spellProto->IsRangedWeaponSpell() && spellProto->DmgClass != SPELL_DAMAGE_CLASS_MELEE)) + return RANGED_ATTACK; + + if (spellProto->HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON) && !spellProto->HasAttribute(SPELL_ATTR3_REQUIRES_MAIN_HAND_WEAPON)) + return OFF_ATTACK; + + return BASE_ATTACK; + }(); + + float APbonus = float(victim->GetTotalAuraModifier(attType != RANGED_ATTACK ? SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS : SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS)); + APbonus += GetTotalAttackPowerValue(attType); + DoneTotal += int32(stack * ApCoeffMod * APbonus); + } + + // Default calculation + if (DoneAdvertisedBenefit) + { + float coeff = spellProto->Effects[effIndex].BonusMultiplier * spellProto->GetSpellScalingMultiplier(getLevel(), false); + if (Player* modOwner = GetSpellModOwner()) + { + coeff *= 100.0f; + modOwner->ApplySpellMod(spellProto, SpellModOp::BonusCoefficient, coeff); + coeff /= 100.0f; + } + + DoneTotal += int32(DoneAdvertisedBenefit * coeff * stack); + } + + callDamageScript(pdamage, DoneTotal, DoneTotalMod); + + float tmpDamage = float(pdamage + DoneTotal) * DoneTotalMod; + + // apply spellmod to Done damage (flat and pct) + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, damagetype == DOT ? SpellModOp::PeriodicHealingAndDamage : SpellModOp::HealingAndDamage, tmpDamage); + + return int32(std::max(tmpDamage, 0.0f)); +} + +float Unit::SpellDamagePctDone(Unit* victim, SpellInfo const* spellProto, DamageEffectType damagetype) const +{ + if (!spellProto || !victim || damagetype == DIRECT_DAMAGE) + return 1.0f; + + // Some spells don't benefit from done mods + if (spellProto->HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS)) + return 1.0f; + + // Some spells don't benefit from pct done mods + if (spellProto->HasAttribute(SPELL_ATTR6_IGNORE_CASTER_DAMAGE_MODIFIERS)) + return 1.0f; + + // For totems pct done mods are calculated when its calculation is run on the player in SpellDamageBonusDone. + if (GetTypeId() == TYPEID_UNIT && IsTotem()) + return 1.0f; + + // Done total percent damage auras + float DoneTotalMod = 1.0f; + + // Pet damage? + if (GetTypeId() == TYPEID_UNIT && !IsPet()) + DoneTotalMod *= ToCreature()->GetSpellDamageMod(ToCreature()->GetCreatureTemplate()->rank); + + float maxModDamagePercentSchool = 0.0f; + if (GetTypeId() == TYPEID_PLAYER) + { + for (uint32 i = 0; i < MAX_SPELL_SCHOOL; ++i) + if (spellProto->GetSchoolMask() & (1 << i)) + maxModDamagePercentSchool = std::max(maxModDamagePercentSchool, GetFloatValue(PLAYER_FIELD_MOD_DAMAGE_DONE_PCT + i)); + } + else + maxModDamagePercentSchool = GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_PERCENT_DONE, spellProto->GetSchoolMask()); + + DoneTotalMod *= maxModDamagePercentSchool; + + uint32 creatureTypeMask = victim->GetCreatureTypeMask(); + + DoneTotalMod *= GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_DONE_VERSUS, creatureTypeMask); + + // bonus against aurastate + DoneTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_DONE_VERSUS_AURASTATE, [victim](AuraEffect const* aurEff) -> bool + { + if (victim->HasAuraState(static_cast(aurEff->GetMiscValue()))) + return true; + return false; + }); + + // Add SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC percent bonus + AddPct(DoneTotalMod, GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC, spellProto->Mechanic)); + + // done scripted mod (take it from owner) + Unit const* owner = GetOwner() ? GetOwner() : this; + AuraEffectList const& mOverrideClassScript = owner->GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS); + for (AuraEffectList::const_iterator i = mOverrideClassScript.begin(); i != mOverrideClassScript.end(); ++i) + { + if (!(*i)->IsAffectingSpell(spellProto)) + continue; + + switch ((*i)->GetMiscValue()) + { + case 4920: // Molten Fury + case 4919: + { + if (victim->HasAuraState(AURA_STATE_HEALTHLESS_35_PERCENT, spellProto, this)) + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + } + case 6917: // Death's Embrace damage effect + case 6926: + case 6928: + { + // Health at 25% or less (25% stored at effect 2 of the spell) + if (victim->HealthBelowPct(CalculateSpellDamage(this, (*i)->GetSpellInfo(), EFFECT_2))) + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + } + case 6916: // Death's Embrace heal effect + case 6925: + case 6927: + if (HealthBelowPct(CalculateSpellDamage(this, (*i)->GetSpellInfo(), EFFECT_2))) + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + case 4992: // Soul Siphon + case 4993: + { + // EFFECT_0 contains the bonus amount per application, EFFECT_1 contains the maximum bonus amount + int32 bonusPerApplication = CalculateSpellDamage(this, (*i)->GetSpellInfo(), EFFECT_0); + int32 pctBonus = 0; + for (auto const& itr : victim->GetAppliedAuras()) + { + Aura const* aura = itr.second->GetBase(); + if (aura->GetCasterGUID() != GetGUID() || !aura->GetSpellInfo()->IsAbilityOfSkillType(SKILL_AFFLICTION)) + continue; + + pctBonus += bonusPerApplication; + } + + pctBonus = std::min(pctBonus, (*i)->GetAmount()); + AddPct(DoneTotalMod, pctBonus); + break; + } + case 5481: // Starfire Bonus + { + if (victim->GetAuraEffect(SPELL_AURA_PERIODIC_DAMAGE, SPELLFAMILY_DRUID, 0x200002, 0, 0)) + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + } + } + } + + // Add SPELL_AURA_MOD_DAMAGE_FROM_MANA percent bonus + if (HasAuraType(SPELL_AURA_MOD_DAMAGE_FROM_MANA) && GetMaxPower(POWER_MANA)) + { + float totalBonus = owner->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_FROM_MANA, spellProto->GetSchoolMask()) - 1.f; + totalBonus *= ((float)GetPower(POWER_MANA) / GetMaxPower(POWER_MANA)); + if (totalBonus != 0.f) + DoneTotalMod += DoneTotalMod * totalBonus; + } + + // Custom scripted damage + switch (spellProto->SpellFamilyName) + { + case SPELLFAMILY_MAGE: + // Mastery: Frostburn + if (AuraEffect* aurEff = GetAuraEffect(76613, 0)) + if (victim->HasAuraState(AURA_STATE_FROZEN)) + if (aurEff->IsAffectingSpell(spellProto)) + AddPct(DoneTotalMod, aurEff->GetAmount()); + + // Ice Lance + if (spellProto->SpellIconID == 186) + { + if (victim->HasAuraState(AURA_STATE_FROZEN, spellProto, this) + || GetAuraEffect(SPELL_AURA_ABILITY_IGNORE_AURASTATE, SPELLFAMILY_MAGE, 0, 0, 0x0000000A, GetGUID())) + DoneTotalMod *= 2.0f; + } + + // Fingers of Frost + if (spellProto->SpellFamilyFlags[0] & 0x00020000) + if (GetAuraEffect(SPELL_AURA_ABILITY_IGNORE_AURASTATE, SPELLFAMILY_MAGE, 0, 0, 0x0000000A, GetGUID())) + AddPct(DoneTotalMod, 25.0f); + + // Torment the weak + if (spellProto->GetSchoolMask() & SPELL_SCHOOL_MASK_ARCANE) + { + if (victim->HasAuraWithMechanic((1<GetSpellInfo()->SpellIconID == 2215) + { + AddPct(DoneTotalMod, (*i)->GetAmount()); + break; + } + } + } + } + break; + case SPELLFAMILY_PRIEST: + // Smite + if (spellProto->SpellFamilyFlags[0] & 0x80) + { + // Glyph of Smite + if (AuraEffect* aurEff = GetAuraEffect(55692, 0)) + if (victim->GetAuraEffect(SPELL_AURA_PERIODIC_DAMAGE, SPELLFAMILY_PRIEST, 0x100000, 0, 0, GetGUID())) + AddPct(DoneTotalMod, aurEff->GetAmount()); + } + break; + case SPELLFAMILY_DEATHKNIGHT: + // Sigil of the Vengeful Heart + if (spellProto->SpellFamilyFlags[0] & 0x2000) + if (AuraEffect* aurEff = GetAuraEffect(64962, EFFECT_1)) + DoneTotalMod += aurEff->GetAmount(); + break; + } + + return DoneTotalMod; +} + +int32 Unit::SpellDamageBonusTaken(Unit* caster, SpellInfo const* spellProto, int32 pdamage, DamageEffectType damagetype) const +{ + if (!spellProto || damagetype == DIRECT_DAMAGE) + return pdamage; + + float TakenTotalMod = 1.0f; + + // Mod damage from spell mechanic + if (uint32 mechanicMask = spellProto->GetAllEffectsMechanicMask()) + { + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT, [mechanicMask, spellProto](AuraEffect const* aurEff) + { + if (spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if (mechanicMask & uint32(1 << (aurEff->GetMiscValue()))) + return true; + return false; + }); + } + + //.. taken pct: dummy auras + AuraEffectList const& mDummyAuras = GetAuraEffectsByType(SPELL_AURA_DUMMY); + for (AuraEffectList::const_iterator i = mDummyAuras.begin(); i != mDummyAuras.end(); ++i) + { + switch ((*i)->GetSpellInfo()->SpellIconID) + { + // Cheat Death + case 2109: + if ((*i)->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL) + AddPct(TakenTotalMod, (*i)->GetAmount()); + break; + } + } + + // Spells with SPELL_ATTR4_IGNORE_DAMAGE_TAKEN_MODIFIERS should only benefit from mechanic damage mod auras. + if (!spellProto->HasAttribute(SPELL_ATTR4_IGNORE_DAMAGE_TAKEN_MODIFIERS)) + { + // from positive and negative SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN + // multiplicative bonus, for example Dispersion + Shadowform (0.10*0.85=0.085) + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN, [spellProto](AuraEffect const* aurEff) + { + if (spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if ((aurEff->GetMiscValue() & spellProto->GetSchoolMask()) != 0) + return true; + + return false; + }); + + // From caster spells + if (caster) + { + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_FROM_CASTER, [caster, spellProto](AuraEffect const* aurEff) + { + if (spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if (aurEff->GetCasterGUID() == caster->GetGUID() && aurEff->IsAffectingSpell(spellProto)) + return true; + return false; + }); + } + } + + // Sanctified Wrath (bypass damage reduction) + if (caster && TakenTotalMod < 1.0f) + { + float damageReduction = 1.0f - TakenTotalMod; + Unit::AuraEffectList const& casterIgnoreResist = caster->GetAuraEffectsByType(SPELL_AURA_MOD_IGNORE_TARGET_RESIST); + for (AuraEffect const* aurEff : casterIgnoreResist) + { + if (!(aurEff->GetMiscValue() & spellProto->GetSchoolMask())) + continue; + + AddPct(damageReduction, -aurEff->GetAmount()); + } + + TakenTotalMod = 1.0f - damageReduction; + } + + float tmpDamage = pdamage * TakenTotalMod; + return int32(std::max(tmpDamage, 0.0f)); +} + +int32 Unit::SpellBaseDamageBonusDone(SpellSchoolMask schoolMask, bool withSpellPowerPctMod /*= false*/) const +{ + if (IsPlayer()) + { + float overrideSP = GetFloatValue(PLAYER_FIELD_OVERRIDE_SPELL_POWER_BY_AP_PCT); + if (overrideSP > 0.0f) + return int32(CalculatePct(GetTotalAttackPowerValue(BASE_ATTACK), overrideSP) + 0.5f); + } + + int32 DoneAdvertisedBenefit = GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_DAMAGE_DONE, schoolMask); + + if (Player const* player = ToPlayer()) + { + // Base value + DoneAdvertisedBenefit += player->GetBaseSpellPowerBonus(); + + // Check if we are ever using mana - PaperDollFrame.lua + if (GetPowerIndex(POWER_MANA) != MAX_POWERS) + DoneAdvertisedBenefit += std::max(0, int32(GetStat(StatType::Intellect)) - 10); // spellpower from intellect + + // Spell Power bonus from SPELL_AURA_MOD_SPELL_POWER_PCT + if (withSpellPowerPctMod) + DoneAdvertisedBenefit *= player->GetFloatValue(PLAYER_FIELD_MOD_SPELL_POWER_PCT); + + // Damage bonus from stats + AuraEffectList const& mDamageDoneOfStatPercent = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT); + for (AuraEffect const* aurEff : mDamageDoneOfStatPercent) + { + if ((aurEff->GetMiscValue() & schoolMask) != 0) + { + // stat used stored in miscValueB for this aura + StatType const usedStat = static_cast(aurEff->GetMiscValueB()); + DoneAdvertisedBenefit += static_cast(CalculatePct(GetStat(usedStat), aurEff->GetAmount())); + } + } + + // ... and attack power + DoneAdvertisedBenefit += static_cast(CalculatePct(GetTotalAttackPowerValue(BASE_ATTACK), GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_SPELL_DAMAGE_OF_ATTACK_POWER, schoolMask))); + } + + return DoneAdvertisedBenefit; +} + +float Unit::SpellCritChanceDone(SpellInfo const* spellInfo, SpellSchoolMask schoolMask, WeaponAttackType attackType /*= BASE_ATTACK*/, bool isPeriodic /*= false*/) const +{ + //! Mobs can't crit with spells. (Except player controlled) + if (GetTypeId() == TYPEID_UNIT && !GetSpellModOwner()) + return 0.0f; + + // not critting spell + if (!isPeriodic && !spellInfo->HasAttribute(SPELL_ATTR0_CU_CAN_CRIT)) + return 0.0f; + + float crit_chance = 0.0f; + switch (spellInfo->DmgClass) + { + case SPELL_DAMAGE_CLASS_MAGIC: + { + if (schoolMask & SPELL_SCHOOL_MASK_NORMAL) + crit_chance = 0.0f; + // For other schools + else if (GetTypeId() == TYPEID_PLAYER) + crit_chance = GetFloatValue(PLAYER_SPELL_CRIT_PERCENTAGE1 + uint32(GetFirstSchoolInMask(schoolMask))); + else + { + crit_chance = (float)m_baseSpellCritChance; + crit_chance += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL, schoolMask); + } + break; + } + case SPELL_DAMAGE_CLASS_MELEE: + case SPELL_DAMAGE_CLASS_RANGED: + { + crit_chance += GetUnitCriticalChanceDone(attackType); + crit_chance += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL, schoolMask); + break; + } + case SPELL_DAMAGE_CLASS_NONE: + default: + return 0.0f; + } + // percent done + // only players use intelligence for critical chance computations + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellInfo, SpellModOp::CritChance, crit_chance); + + return std::max(crit_chance, 0.0f); +} + +float Unit::SpellCritChanceTaken(Unit const* caster, SpellInfo const* spellInfo, SpellSchoolMask schoolMask, float doneChance, WeaponAttackType attackType /*= BASE_ATTACK*/, bool isPeriodic /*= false*/) const +{ + // not critting spell + if (!isPeriodic && !spellInfo->HasAttribute(SPELL_ATTR0_CU_CAN_CRIT)) + return 0.0f; + + float crit_chance = doneChance; + switch (spellInfo->DmgClass) + { + case SPELL_DAMAGE_CLASS_MAGIC: + { + // taken + if (!spellInfo->IsPositive()) + { + // Modify critical chance by victim SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE + crit_chance += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE, schoolMask); + + // Modify critical chance by victim SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE + // applied after resilience + crit_chance += GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE); + } + + // scripted (increase crit chance ... against ... target by x% + if (caster) + { + AuraEffectList const& mOverrideClassScript = caster->GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS); + for (AuraEffect const* aurEff : mOverrideClassScript) + { + if (!aurEff->IsAffectingSpell(spellInfo)) + continue; + + switch (aurEff->GetMiscValue()) + { + case 911: // Shatter + if (HasAuraState(AURA_STATE_FROZEN, spellInfo, caster)) + crit_chance *= (1 * aurEff->GetSpellInfo()->GetRank()); + else if (spellInfo->SpellFamilyFlags[0] & 0x00020000 || spellInfo->SpellFamilyFlags[1] & 0x00100000) + if (GetAuraEffect(SPELL_AURA_ABILITY_IGNORE_AURASTATE, SPELLFAMILY_MAGE, 0, 0, 0x0000000A, caster->GetGUID())) + crit_chance *= (1 * aurEff->GetSpellInfo()->GetRank()); + break; + case 57470: // Renewed Hope + case 57472: + if (HasAura(6788) || HasAura(47930)) + crit_chance += aurEff->GetAmount(); + break; + default: + break; + } + } + // Custom crit by class + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_MAGE: + // Glyph of Fire Blast + if (spellInfo->SpellFamilyFlags[0] == 0x2 && spellInfo->SpellIconID == 12) + if (HasAuraWithMechanic((1 << MECHANIC_STUN) | (1 << MECHANIC_KNOCKOUT))) + if (AuraEffect const* aurEff = caster->GetAuraEffect(56369, EFFECT_0)) + crit_chance += aurEff->GetAmount(); + break; + case SPELLFAMILY_ROGUE: + // Shiv-applied poisons can't crit + if (caster->FindCurrentSpellBySpellId(5938)) + crit_chance = 0.0f; + break; + case SPELLFAMILY_PALADIN: + // Flash of light + if (spellInfo->SpellFamilyFlags[0] & 0x40000000) + { + // Sacred Shield + if (AuraEffect const* aura = GetAuraEffect(58597, 1, GetGUID())) + crit_chance += aura->GetAmount(); + break; + } + // Exorcism + else if (spellInfo->GetCategory() == 19) + { + if (GetCreatureTypeMask() & CREATURE_TYPEMASK_DEMON_OR_UNDEAD) + return 100.0f; + break; + } + + // Last Word + if (AuraEffect const* effect = caster->GetDummyAuraEffect(SPELLFAMILY_PALADIN, 2139, EFFECT_0)) + if (effect->IsAffectingSpell(spellInfo)) + if (HasAuraState(AURA_STATE_HEALTHLESS_35_PERCENT)) + crit_chance += effect->GetAmount(); + break; + case SPELLFAMILY_SHAMAN: + // Lava Burst + if ((spellInfo->SpellFamilyFlags[1] & 0x1000) != 0 || (spellInfo->SpellFamilyFlags[2] & 0x400000) != 0) + { + if (GetAuraEffect(SPELL_AURA_PERIODIC_DAMAGE, SPELLFAMILY_SHAMAN, 0x10000000, 0, 0, caster->GetGUID())) + if (GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE) > -100) + return 100.0f; + break; + } + break; + case SPELLFAMILY_WARLOCK: + // Searing Pain - Soulburn bonus + if (spellInfo->SpellFamilyFlags[0] & 0x00000100) + if (HasAura(74434, GetGUID())) + return 100.0f; + + // Improved Searing Pain + if (AuraEffect const* effect = caster->GetDummyAuraEffect(SPELLFAMILY_WARLOCK, 816, EFFECT_0)) + if (effect->IsAffectingSpell(spellInfo)) + if (GetHealthPct() <= 25.f) + crit_chance += effect->GetAmount(); + break; + } + + // Spell crit suppression + if (GetTypeId() == TYPEID_UNIT) + { + int32 const levelDiff = static_cast(getLevelForTarget(caster)) - caster->getLevel(); + if (levelDiff > 0) + crit_chance -= levelDiff * 0.7f; + } + } + break; + } + case SPELL_DAMAGE_CLASS_MELEE: + { + // Custom crit by class + if (caster) + { + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_DRUID: + // Rend and Tear - bonus crit chance for Ferocious Bite on bleeding targets + if (spellInfo->SpellFamilyFlags[0] & 0x00800000 + && spellInfo->SpellIconID == 1680 + && HasAuraState(AURA_STATE_BLEEDING)) + { + if (AuraEffect const* rendAndTear = caster->GetDummyAuraEffect(SPELLFAMILY_DRUID, 2859, 1)) + crit_chance += rendAndTear->GetAmount(); + break; + } + + // Predatory Strikes + if (spellInfo->SpellFamilyFlags[0] & 0x00010000) + if (AuraEffect const* critAuraEff = caster->GetDummyAuraEffect(SPELLFAMILY_DRUID, 1563, EFFECT_0)) + if (AuraEffect const* healthAuraEff = caster->GetDummyAuraEffect(SPELLFAMILY_DRUID, 1563, EFFECT_1)) + if (GetHealthPct() >= healthAuraEff->GetAmount()) + crit_chance += critAuraEff->GetAmount(); + break; + case SPELLFAMILY_HUNTER: + // Kill Command - Improved Kill Command + if (spellInfo->SpellFamilyFlags[1] & 0x00000800) + if (Player* player = caster->GetSpellModOwner()) + if (AuraEffect const* improvedKillCommand = player->GetDummyAuraEffect(SPELLFAMILY_HUNTER, 2221, EFFECT_0)) + crit_chance += improvedKillCommand->GetAmount(); + break; + } + } + [[fallthrough]]; // Calculate critical strike chance for both Ranged and Melee spells + } + case SPELL_DAMAGE_CLASS_RANGED: + if (caster) + { + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_HUNTER: + { + // Careful Aim + if (GetHealthPct() >= 90) + { + switch (spellInfo->Id) + { + case 56641: // Steady Shot + case 77767: // Cobra Shot + case 19434: // Aimed Shot + case 82928: // Aimed Shot! + if (AuraEffect* aurEff = caster->GetDummyAuraEffect(SPELLFAMILY_HUNTER, 2222, 0)) + crit_chance += aurEff->GetAmount(); + break; + default: + break; + } + } + break; + } + } + crit_chance = GetUnitCriticalChanceTaken(attackType, crit_chance); + } + break; + case SPELL_DAMAGE_CLASS_NONE: + default: + return 0.f; + } + + // for this types the bonus was already added in GetUnitCriticalChance, do not add twice + if (caster) + { + crit_chance += GetTotalAuraModifier(SPELL_AURA_MOD_CRIT_CHANCE_FOR_CASTER, [caster, spellInfo](AuraEffect const* aurEff) -> bool + { + if (aurEff->GetCasterGUID() == caster->GetGUID() && aurEff->IsAffectingSpell(spellInfo)) + return true; + return false; + }); + } + + return std::max(crit_chance, 0.0f); +} + +/*static*/ uint32 Unit::SpellCriticalDamageBonus(Unit const* caster, SpellInfo const* spellProto, uint32 damage, bool periodicDamage /*= false*/) +{ + // Calculate critical bonus + int32 crit_bonus = damage; + float crit_mod = 0.0f; + + switch (spellProto->DmgClass) + { + // Spells which are either marked as ranged or melee damage, will deal 100% additional damage + case SPELL_DAMAGE_CLASS_MELEE: + crit_bonus += damage; + break; + case SPELL_DAMAGE_CLASS_RANGED: + // Ranged spells which have magic periodic damage effect on the other hand, only deal 50% damage, just like spells + // This is a very specific case that is currently only known for Hunter spells, such as Serpent Sting and Black Arrow + if (periodicDamage && (spellProto->GetSchoolMask() & SPELL_SCHOOL_MASK_MAGIC) != 0) + crit_bonus += damage / 2; + else + crit_bonus += damage; + break; + default: + // Magic spells will simply deal 50% additional crit damage + crit_bonus += damage / 2; + break; + } + + if (caster) + { + crit_mod += (caster->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_CRIT_DAMAGE_BONUS, spellProto->GetSchoolMask()) - 1.0f) * 100; + + if (crit_bonus != 0) + AddPct(crit_bonus, crit_mod); + + crit_bonus -= damage; + + if (damage > uint32(crit_bonus)) + { + // adds additional damage to critBonus (from talents) + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, SpellModOp::CritDamageAndHealing, crit_bonus); + } + + crit_bonus += damage; + } + + return crit_bonus; +} + +/*static*/ uint32 Unit::SpellCriticalHealingBonus(Unit const* caster, uint32 damage) +{ + // As of Patch 4.2.0 (2011-06-28): all healing spells now crit for 200% instead of 150% + damage *= 2; + + if (caster) + damage = int32(float(damage) * caster->GetTotalAuraMultiplier(SPELL_AURA_MOD_CRITICAL_HEALING_AMOUNT)); + + return damage; +} + +int32 Unit::SpellHealingBonusDone(Unit* victim, SpellInfo const* spellProto, int32 healamount, DamageEffectType damagetype, uint8 effIndex, uint32 stack /*= 1*/, Spell* spell /*= nullptr*/, AuraEffect const* aurEff /*= nullptr*/) const +{ + // For totems get healing bonus from owner (statue isn't totem in fact) + if (GetTypeId() == TYPEID_UNIT && IsTotem()) + if (Unit* creator = ObjectAccessor::GetUnit(*this, GetCreatorGUID())) + return creator->SpellHealingBonusDone(victim, spellProto, healamount, damagetype, effIndex, stack, spell, aurEff); + + // Some spells don't benefit from done mods + if (spellProto->HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS)) + return healamount; + + // No bonus healing for potion spells + if (spellProto->SpellFamilyName == SPELLFAMILY_POTION) + return healamount; + + int32 DoneTotal = 0; + float DoneTotalMod = SpellHealingPctDone(victim, spellProto); + + // done scripted mod (take it from owner) + Unit const* owner = GetOwner() ? GetOwner() : this; + for (AuraEffect const* overrideClassScripts : owner->GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS)) + { + if (!overrideClassScripts->IsAffectingSpell(spellProto)) + continue; + + switch (overrideClassScripts->GetMiscValue()) + { + case 4415: // Increased Rejuvenation Healing + case 4953: + DoneTotal += overrideClassScripts->GetAmount(); + break; + default: + break; + } + } + + // Done fixed damage bonus auras + int32 DoneAdvertisedBenefit = SpellBaseHealingBonusDone(spellProto->GetSchoolMask(), true); + // modify spell power by victim's SPELL_AURA_MOD_HEALING auras (eg Amplify/Dampen Magic) + DoneAdvertisedBenefit += victim->GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_HEALING, spellProto->GetSchoolMask()); + + // Pets just add their bonus damage to their spell damage + // note that their spell damage is just gain of their own auras + if (HasUnitTypeMask(UNIT_MASK_GUARDIAN)) + DoneAdvertisedBenefit += static_cast(this)->GetBonusDamage(); + + + // Check for table values + if (spellProto->BonusCoefficientFromAP > 0.0f) + { + WeaponAttackType const attType = (spellProto->IsRangedWeaponSpell() && spellProto->DmgClass != SPELL_DAMAGE_CLASS_MELEE) ? RANGED_ATTACK : BASE_ATTACK; + float APbonus = float(victim->GetTotalAuraModifier(attType == BASE_ATTACK ? SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS : SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS)); + APbonus += GetTotalAttackPowerValue(attType); + + DoneTotal += int32(spellProto->BonusCoefficientFromAP * stack * APbonus); + } + + // Default calculation + if (DoneAdvertisedBenefit) + { + float coeff = spellProto->Effects[effIndex].BonusMultiplier * spellProto->GetSpellScalingMultiplier(getLevel(), false); + if (Player* modOwner = GetSpellModOwner()) + { + coeff *= 100.0f; + modOwner->ApplySpellMod(spellProto, SpellModOp::BonusCoefficient, coeff); + coeff /= 100.0f; + } + + DoneTotal += int32(DoneAdvertisedBenefit * coeff * stack); + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (spellProto->Effects[i].ApplyAuraName) + { + // Bonus healing does not apply to these spells + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_HEALTH_FUNNEL: + DoneTotal = 0; + break; + } + if (spellProto->Effects[i].Effect == SPELL_EFFECT_HEALTH_LEECH) + DoneTotal = 0; + } + + if (spell) + spell->CallScriptCalcHealingHandlers(victim, healamount, DoneTotal, DoneTotalMod); + else if (aurEff) + aurEff->GetBase()->CallScriptCalcDamageAndHealingHandlers(aurEff, aurEff->GetBase()->GetApplicationOfTarget(victim->GetGUID()), victim, healamount, DoneTotal, DoneTotalMod); + + float heal = float(healamount + DoneTotal) * DoneTotalMod; + + + // apply spellmod to Done amount + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, damagetype == DOT ? SpellModOp::PeriodicHealingAndDamage : SpellModOp::HealingAndDamage, heal); + + return int32(std::max(heal, 0.0f)); +} + +float Unit::SpellHealingPctDone(Unit* victim, SpellInfo const* spellProto) const +{ + // For totems pct done mods are calculated when its calculation is run on the player in SpellHealingBonusDone. + if (GetTypeId() == TYPEID_UNIT && IsTotem()) + return 1.0f; + + // No bonus healing for potion spells + if (spellProto->SpellFamilyName == SPELLFAMILY_POTION) + return 1.0f; + + float DoneTotalMod = 1.0f; + + // Healing done percent + if (IsPlayer()) + DoneTotalMod *= GetFloatValue(PLAYER_FIELD_MOD_HEALING_DONE_PCT); + else + DoneTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_HEALING_DONE_PERCENT); + + // done scripted mod (take it from owner) + Unit const* owner = GetOwner() ? GetOwner() : this; + for (AuraEffect const* overrideClassScripts : owner->GetAuraEffectsByType(SPELL_AURA_OVERRIDE_CLASS_SCRIPTS)) + { + if (!overrideClassScripts->IsAffectingSpell(spellProto)) + continue; + + switch (overrideClassScripts->GetMiscValue()) + { + case 21: // Test of Faith + case 6935: + case 6918: + if (victim->HealthBelowPct(50)) + AddPct(DoneTotalMod, overrideClassScripts->GetAmount()); + break; + case 8477: // Nourish Heal Boost + { + int32 modPercent = 0; + for (AuraEffect const* periodicHeal : victim->GetAuraEffectsByType(SPELL_AURA_PERIODIC_HEAL)) + { + if (periodicHeal->GetCasterGUID() != GetGUID() || periodicHeal->GetSpellInfo()->SpellFamilyName != SPELLFAMILY_DRUID) + continue; + + if (periodicHeal->GetSpellInfo()->SpellFamilyFlags.HasFlag(0x10 | 0x40, 0x10 | 0x4000000, 0)) + modPercent += overrideClassScripts->GetAmount() * periodicHeal->GetBase()->GetStackAmount(); + } + AddPct(DoneTotalMod, modPercent); + break; + } + default: + break; + } + } + + // Custom scripted damage + switch (spellProto->SpellFamilyName) + { + case SPELLFAMILY_SHAMAN: + { + // Nature's Blessing + if (AuraEffect const* blessing = GetDummyAuraEffect(SPELLFAMILY_SHAMAN, 2012, EFFECT_0)) + if (blessing->IsAffectingSpell(spellProto) && victim->HasAura(974, GetGUID())) + AddPct(DoneTotalMod, blessing->GetAmount()); + break; + } + default: + break; + } + + float healthPctDiff = 100.f - victim->GetHealthPct(); + for (AuraEffect const* healingDonePctVsTargetHealth : owner->GetAuraEffectsByType(SPELL_AURA_MOD_HEALING_DONE_PCT_VERSUS_TARGET_HEALTH)) + { + if (!healingDonePctVsTargetHealth->IsAffectingSpell(spellProto)) + continue; + + AddPct(DoneTotalMod, CalculatePct(float(healingDonePctVsTargetHealth->GetAmount()), healthPctDiff)); + } + + return DoneTotalMod; +} + +int32 Unit::SpellHealingBonusTaken(Unit* caster, SpellInfo const* spellProto, int32 healamount, DamageEffectType damagetype) const +{ + float TakenTotalMod = 1.0f; + + // Healing taken percent + if (IsPlayer()) + TakenTotalMod *= GetFloatValue(PLAYER_FIELD_MOD_HEALING_PCT); + else + { + float minval = float(GetMaxNegativeAuraModifier(SPELL_AURA_MOD_HEALING_PCT)); + if (minval) + AddPct(TakenTotalMod, minval); + + float maxval = float(GetMaxPositiveAuraModifier(SPELL_AURA_MOD_HEALING_PCT)); + if (maxval) + AddPct(TakenTotalMod, maxval); + } + + // Tenacity increase healing % taken + if (AuraEffect const* Tenacity = GetAuraEffect(58549, 0)) + AddPct(TakenTotalMod, Tenacity->GetAmount()); + + // Unused in Cataclysm (15595) + if (damagetype == DOT) + { + // Healing over time taken percent + float minval_hot = float(GetMaxNegativeAuraModifier(SPELL_AURA_259)); + if (minval_hot) + AddPct(TakenTotalMod, minval_hot); + + float maxval_hot = float(GetMaxPositiveAuraModifier(SPELL_AURA_259)); + if (maxval_hot) + AddPct(TakenTotalMod, maxval_hot); + } + + if (caster) + { + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_HEALING_RECEIVED, [caster, spellProto](AuraEffect const* aurEff) -> bool + { + if (caster->GetGUID() == aurEff->GetCasterGUID() && aurEff->IsAffectingSpell(spellProto)) + return true; + return false; + }); + } + + float heal = healamount * TakenTotalMod; + + return int32(std::max(heal, 0.0f)); +} + +int32 Unit::SpellBaseHealingBonusDone(SpellSchoolMask schoolMask, bool withSpellPowerPctMod /*= false*/) const +{ + if (IsPlayer()) + { + float overrideSP = GetFloatValue(PLAYER_FIELD_OVERRIDE_SPELL_POWER_BY_AP_PCT); + if (overrideSP > 0.0f) + return int32(CalculatePct(GetTotalAttackPowerValue(BASE_ATTACK), overrideSP) + 0.5f); + } + + int32 advertisedBenefit = GetTotalAuraModifier(SPELL_AURA_MOD_HEALING_DONE, [schoolMask](AuraEffect const* aurEff) -> bool + { + if (!aurEff->GetMiscValue() || (aurEff->GetMiscValue() & schoolMask) != 0) + return true; + return false; + }); + + // Healing bonus of spirit, intellect and strength + if (Player const* player = ToPlayer()) + { + // Base value + advertisedBenefit += player->GetBaseSpellPowerBonus(); + + // Check if we are ever using mana - PaperDollFrame.lua + if (GetPowerIndex(POWER_MANA) != MAX_POWERS) + advertisedBenefit += std::max(0, int32(GetStat(StatType::Intellect)) - 10); // spellpower from intellect + + // Spell Power bonus from SPELL_AURA_MOD_SPELL_POWER_PCT + if (withSpellPowerPctMod) + advertisedBenefit *= player->GetFloatValue(PLAYER_FIELD_MOD_SPELL_POWER_PCT); + + // Healing bonus from stats + AuraEffectList const& mHealingDoneOfStatPercent = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT); + for (AuraEffectList::const_iterator i = mHealingDoneOfStatPercent.begin(); i != mHealingDoneOfStatPercent.end(); ++i) + { + // stat used dependent from misc value (stat index) + StatType usedStat = StatType((*i)->GetSpellInfo()->Effects[(*i)->GetEffIndex()].MiscValue); + advertisedBenefit += int32(CalculatePct(GetStat(usedStat), (*i)->GetAmount())); + } + + // ... and attack power + advertisedBenefit += CalculatePct(GetTotalAttackPowerValue(BASE_ATTACK), GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_SPELL_HEALING_OF_ATTACK_POWER, schoolMask)); + } + return advertisedBenefit; +} + +bool Unit::IsImmunedToDamage(SpellSchoolMask schoolMask) const +{ + if (schoolMask == SPELL_SCHOOL_MASK_NONE) + return false; + + // If m_immuneToSchool type contain this school type, IMMUNE damage. + uint32 schoolImmunityMask = GetSchoolImmunityMask(); + if ((schoolImmunityMask & schoolMask) == schoolMask) // We need to be immune to all types + return true; + + // If m_immuneToDamage type contain magic, IMMUNE damage. + uint32 damageImmunityMask = GetDamageImmunityMask(); + if ((damageImmunityMask & schoolMask) == schoolMask) // We need to be immune to all types + return true; + + return false; +} + +bool Unit::IsImmunedToDamage(SpellInfo const* spellInfo) const +{ + if (!spellInfo) + return false; + + // for example 40175 + if (spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES) && spellInfo->HasAttribute(SPELL_ATTR3_ALWAYS_HIT)) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) || spellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + return false; + + if (uint32 schoolMask = spellInfo->GetSchoolMask()) + { + // If m_immuneToSchool type contain this school type, IMMUNE damage. + uint32 schoolImmunityMask = 0; + SpellImmuneContainer const& schoolList = m_spellImmune[IMMUNITY_SCHOOL]; + for (auto itr = schoolList.begin(); itr != schoolList.end(); ++itr) + if ((itr->first & schoolMask) && !spellInfo->CanPierceImmuneAura(sSpellMgr->GetSpellInfo(itr->second))) + schoolImmunityMask |= itr->first; + + // // We need to be immune to all types + if ((schoolImmunityMask & schoolMask) == schoolMask) + return true; + + // If m_immuneToDamage type contain magic, IMMUNE damage. + uint32 damageImmunityMask = GetDamageImmunityMask(); + if ((damageImmunityMask & schoolMask) == schoolMask) // We need to be immune to all types + return true; + } + + return false; +} + +bool Unit::IsImmunedToSpell(SpellInfo const* spellInfo, WorldObject const* caster, bool requireImmunityPurgesEffectAttribute /*= false*/) const +{ + if (!spellInfo) + return false; + + auto hasImmunity = [requireImmunityPurgesEffectAttribute](SpellImmuneContainer const& container, uint32 key) + { + Trinity::IteratorPair range = Trinity::Containers::MapEqualRange(container, key); + if (!requireImmunityPurgesEffectAttribute) + return range.begin() != range.end(); + + return std::any_of(range.begin(), range.end(), [](SpellImmuneContainer::value_type const& entry) + { + if (SpellInfo const* immunitySourceSpell = sSpellMgr->GetSpellInfo(entry.second)) + if (immunitySourceSpell->HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + return true; + + return false; + }); + }; + + // Single spell immunity. + SpellImmuneContainer const& idList = m_spellImmune[IMMUNITY_ID]; + if (hasImmunity(idList, spellInfo->Id)) + return true; + + if (spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + if (uint32 dispel = spellInfo->Dispel) + { + SpellImmuneContainer const& dispelList = m_spellImmune[IMMUNITY_DISPEL]; + if (hasImmunity(dispelList, dispel)) + return true; + } + + // Spells that don't have effectMechanics. + if (uint32 mechanic = spellInfo->Mechanic) + { + SpellImmuneContainer const& mechanicList = m_spellImmune[IMMUNITY_MECHANIC]; + if (hasImmunity(mechanicList, mechanic)) + return true; + } + + bool immuneToAllEffects = true; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // State/effect immunities applied by aura expect full spell immunity + // Ignore effects with mechanic, they are supposed to be checked separately + if (!spellInfo->Effects[i].IsEffect()) + continue; + + if (!IsImmunedToSpellEffect(spellInfo, i, caster, requireImmunityPurgesEffectAttribute)) + { + immuneToAllEffects = false; + break; + } + if (spellInfo->HasAttribute(SPELL_ATTR4_NO_PARTIAL_IMMUNITY)) + return true; + } + + if (immuneToAllEffects) //Return immune only if the target is immune to all spell effects. + return true; + + if (uint32 schoolMask = spellInfo->GetSchoolMask()) + { + uint32 schoolImmunityMask = 0; + SpellImmuneContainer const& schoolList = m_spellImmune[IMMUNITY_SCHOOL]; + for (auto itr = schoolList.begin(); itr != schoolList.end(); ++itr) + { + if ((itr->first & schoolMask) == 0) + continue; + + SpellInfo const* immuneSpellInfo = sSpellMgr->GetSpellInfo(itr->second); + if (requireImmunityPurgesEffectAttribute) + if (!immuneSpellInfo || !immuneSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + continue; + + // Consider the school immune if any of these conditions are not satisfied. + // In case of no immuneSpellInfo, ignore that condition and check only the other conditions + if ((immuneSpellInfo && !immuneSpellInfo->IsPositive()) || !spellInfo->IsPositive() || !caster || !IsFriendlyTo(caster)) + if (!spellInfo->CanPierceImmuneAura(immuneSpellInfo)) + schoolImmunityMask |= itr->first; + } + + if ((schoolImmunityMask & schoolMask) == schoolMask) + return true; + } + + return false; +} + +uint32 Unit::GetSchoolImmunityMask() const +{ + uint32 mask = 0; + SpellImmuneContainer const& schoolList = m_spellImmune[IMMUNITY_SCHOOL]; + for (auto itr = schoolList.begin(); itr != schoolList.end(); ++itr) + mask |= itr->first; + + return mask; +} + +uint32 Unit::GetDamageImmunityMask() const +{ + uint32 mask = 0; + SpellImmuneContainer const& damageList = m_spellImmune[IMMUNITY_DAMAGE]; + for (auto itr = damageList.begin(); itr != damageList.end(); ++itr) + mask |= itr->first; + + return mask; +} + +uint32 Unit::GetMechanicImmunityMask() const +{ + uint32 mask = 0; + SpellImmuneContainer const& mechanicList = m_spellImmune[IMMUNITY_MECHANIC]; + for (auto itr = mechanicList.begin(); itr != mechanicList.end(); ++itr) + mask |= (1 << itr->first); + + return mask; +} + +bool Unit::IsImmunedToSpellEffect(SpellInfo const* spellInfo, uint32 index, WorldObject const* caster, + bool requireImmunityPurgesEffectAttribute /*= false*/) const +{ + if (!spellInfo || !spellInfo->Effects[index].IsEffect()) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + auto hasImmunity = [requireImmunityPurgesEffectAttribute](SpellImmuneContainer const& container, uint32 key) + { + Trinity::IteratorPair range = Trinity::Containers::MapEqualRange(container, key); + if (!requireImmunityPurgesEffectAttribute) + return range.begin() != range.end(); + + return std::any_of(range.begin(), range.end(), [](SpellImmuneContainer::value_type const& entry) + { + if (SpellInfo const* immunitySourceSpell = sSpellMgr->GetSpellInfo(entry.second)) + if (immunitySourceSpell->HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + return true; + + return false; + }); + }; + + + // If m_immuneToEffect type contain this effect type, IMMUNE effect. + uint32 effect = spellInfo->Effects[index].Effect; + SpellImmuneContainer const& effectList = m_spellImmune[IMMUNITY_EFFECT]; + if (hasImmunity(effectList, effect)) + return true; + + if (uint32 mechanic = spellInfo->Effects[index].Mechanic) + { + SpellImmuneContainer const& mechanicList = m_spellImmune[IMMUNITY_MECHANIC]; + if (hasImmunity(mechanicList, mechanic)) + return true; + } + + if (!spellInfo->HasAttribute(SPELL_ATTR3_ALWAYS_HIT)) + { + if (uint32 aura = spellInfo->Effects[index].ApplyAuraName) + { + SpellImmuneContainer const& list = m_spellImmune[IMMUNITY_STATE]; + if (hasImmunity(list, aura)) + return true; + + if (!spellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + // Check for immune to application of harmful magical effects + AuraEffectList const& immuneAuraApply = GetAuraEffectsByType(SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL); + for (AuraEffectList::const_iterator iter = immuneAuraApply.begin(); iter != immuneAuraApply.end(); ++iter) + if (((*iter)->GetMiscValue() & spellInfo->GetSchoolMask()) && // Check school + ((caster && !IsFriendlyTo(caster)) || !spellInfo->IsPositiveEffect(index))) // Harmful + return true; + } + } + } + + return false; +} + +int32 Unit::MeleeDamageBonusDone(Unit* victim, int32 damage, WeaponAttackType attType, DamageEffectType damagetype, SpellInfo const* spellProto /*= nullptr*/, Mechanics mechanic /*= MECHANIC_NONE*/, SpellSchoolMask damageSchoolMask /*= SPELL_SCHOOL_MASK_NORMAL*/, Spell* spell /*= nullptr*/, AuraEffect const* aurEff /*= nullptr*/) +{ + if (!victim || damage == 0) + return 0; + + uint32 creatureTypeMask = victim->GetCreatureTypeMask(); + + // Done fixed damage bonus auras + int32 DoneFlatBenefit = 0; + + // ..done + DoneFlatBenefit += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_DAMAGE_DONE_CREATURE, creatureTypeMask); + + // ..done + // SPELL_AURA_MOD_DAMAGE_DONE included in weapon damage + + // ..done (base at attack power for marked target and base at attack power for creature type) + int32 APbonus = 0; + + if (attType == RANGED_ATTACK) + { + APbonus += victim->GetTotalAuraModifier(SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS); + + // ..done (base at attack power and creature type) + APbonus += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_RANGED_ATTACK_POWER_VERSUS, creatureTypeMask); + } + else + { + APbonus += victim->GetTotalAuraModifier(SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS); + + // ..done (base at attack power and creature type) + APbonus += GetTotalAuraModifierByMiscMask(SPELL_AURA_MOD_MELEE_ATTACK_POWER_VERSUS, creatureTypeMask); + } + + if (APbonus != 0) // Can be negative + { + bool const normalized = spellProto && spellProto->HasEffect(SPELL_EFFECT_NORMALIZED_WEAPON_DMG); + DoneFlatBenefit += int32(APbonus / 14.0f * GetAPMultiplier(attType, normalized)); + } + + // Done total percent damage auras + float DoneTotalMod = 1.0f; + + SpellSchoolMask schoolMask = spellProto ? spellProto->GetSchoolMask() : damageSchoolMask; + + if (!(schoolMask & SPELL_SCHOOL_MASK_NORMAL)) + { + // Some spells don't benefit from pct done mods + // mods for SPELL_SCHOOL_MASK_NORMAL are already factored in base melee damage calculation + if (!spellProto || !spellProto->HasAttribute(SPELL_ATTR6_IGNORE_CASTER_DAMAGE_MODIFIERS)) + { + float maxModDamagePercentSchool = 0.0f; + if (GetTypeId() == TYPEID_PLAYER) + { + for (uint32 i = SPELL_SCHOOL_HOLY; i < MAX_SPELL_SCHOOL; ++i) + if (schoolMask & (1 << i)) + maxModDamagePercentSchool = std::max(maxModDamagePercentSchool, GetFloatValue(PLAYER_FIELD_MOD_DAMAGE_DONE_PCT + i)); + } + else if (spellProto) + maxModDamagePercentSchool = GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_PERCENT_DONE, spellProto->GetSchoolMask()); + + DoneTotalMod *= maxModDamagePercentSchool; + } + } + + DoneTotalMod *= GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_DAMAGE_DONE_VERSUS, creatureTypeMask); + + // bonus against aurastate + DoneTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_DONE_VERSUS_AURASTATE, [victim](AuraEffect const* aurEff) -> bool + { + if (victim->HasAuraState(AuraStateType(aurEff->GetMiscValue()))) + return true; + return false; + }); + + // Add SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC percent bonus + // Add SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC percent bonus + if (mechanic != MECHANIC_NONE) + AddPct(DoneTotalMod, GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC, mechanic)); + else if (spellProto && spellProto->Mechanic) + AddPct(DoneTotalMod, GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC, spellProto->Mechanic)); + + if (!spellProto) + { + // Add SPELL_AURA_MOD_AUTOATTACK_DAMAGE percent bonus + AddPct(DoneTotalMod, GetTotalAuraModifier(SPELL_AURA_MOD_AUTOATTACK_DAMAGE)); + + // Add SPELL_AURA_MOD_MELEE_DAMAGE_FROM_CASTER percent bonus + // This aura name is a bit misleading but it's actually only suposed to increase melee auto attack damage + AddPct(DoneTotalMod, victim->GetTotalAuraMultiplier(SPELL_AURA_MOD_MELEE_DAMAGE_FROM_CASTER, [this](AuraEffect const* effect)->bool + { + return effect->GetCasterGUID() == GetGUID(); + })); + } + + if (spell) + spell->CallScriptCalcDamageHandlers(victim, damage, DoneFlatBenefit, DoneTotalMod); + else if (aurEff) + aurEff->GetBase()->CallScriptCalcDamageAndHealingHandlers(aurEff, aurEff->GetBase()->GetApplicationOfTarget(victim->GetGUID()), victim, damage, DoneFlatBenefit, DoneTotalMod); + + float damageF = float(damage + DoneFlatBenefit) * DoneTotalMod; + + // apply spellmod to Done damage + if (spellProto) + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, damagetype == DOT ? SpellModOp::PeriodicHealingAndDamage : SpellModOp::HealingAndDamage, damageF); + + // bonus result can be negative + return int32(std::max(damageF, 0.0f)); +} + +int32 Unit::MeleeDamageBonusTaken(Unit* attacker, int32 pdamage, WeaponAttackType attType, SpellInfo const* spellProto /*= nullptr*/, SpellSchoolMask damageSchoolMask /*= SPELL_SCHOOL_MASK_NORMAL*/) +{ + if (pdamage == 0) + return 0; + + int32 TakenFlatBenefit = 0; + + uint32 meleeDamageSchoolMask = spellProto ? spellProto->GetSchoolMask() : attacker->GetMeleeDamageSchoolMask(); + + // ..taken + TakenFlatBenefit += GetTotalAuraModifier(SPELL_AURA_MOD_DAMAGE_TAKEN, [meleeDamageSchoolMask, spellProto](AuraEffect const* aurEff) + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if ((aurEff->GetMiscValue() & meleeDamageSchoolMask) != 0) + return true; + + return false; + }); + + AuraType modDamageTaken = SPELL_AURA_MOD_RANGED_DAMAGE_TAKEN; + if (attType != RANGED_ATTACK) + modDamageTaken = SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN; + + TakenFlatBenefit += GetTotalAuraModifier(modDamageTaken, [spellProto](AuraEffect const* aurEff) + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + return true; + }); + + if ((TakenFlatBenefit < 0) && (pdamage < -TakenFlatBenefit)) + return 0; + + // Taken total percent damage auras + float TakenTotalMod = 1.0f; + + // ..taken + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN, [meleeDamageSchoolMask, spellProto](AuraEffect const* aurEff) + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if ((aurEff->GetMiscValue() & meleeDamageSchoolMask) != 0) + return true; + + return false; + }); + + // .. taken pct (special attacks) + if (spellProto) + { + // From caster spells + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_FROM_CASTER, [attacker, spellProto](AuraEffect const* aurEff) -> bool + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if (aurEff->GetCasterGUID() == attacker->GetGUID() && aurEff->IsAffectingSpell(spellProto)) + return true; + return false; + }); + + // Mod damage from spell mechanic + uint32 mechanicMask = spellProto->GetAllEffectsMechanicMask(); + + // Shred, Maul - "Effects which increase Bleed damage also increase Shred damage" + if (spellProto->SpellFamilyName == SPELLFAMILY_DRUID && spellProto->SpellFamilyFlags[0] & 0x00008800) + mechanicMask |= (1 << MECHANIC_BLEED); + + if (mechanicMask) + { + TakenTotalMod *= GetTotalAuraMultiplier(SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT, [mechanicMask, spellProto](AuraEffect const* aurEff) -> bool + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + if (mechanicMask & uint32(1 << (aurEff->GetMiscValue()))) + return true; + return false; + }); + } + } + + // .. taken pct: dummy auras + AuraEffectList const& mDummyAuras = GetAuraEffectsByType(SPELL_AURA_DUMMY); + for (AuraEffectList::const_iterator i = mDummyAuras.begin(); i != mDummyAuras.end(); ++i) + { + switch ((*i)->GetSpellInfo()->SpellIconID) + { + // Cheat Death + case 2109: + if ((*i)->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL) + { + if (Player* player = ToPlayer()) + { + float mod = player->GetRatingBonusValue(CR_RESILIENCE_PLAYER_DAMAGE_TAKEN) * (-8.0f); + AddPct(TakenTotalMod, std::max(mod, float((*i)->GetAmount()))); + } + } + break; + } + } + + + AuraType modDamageTakenPct = SPELL_AURA_MOD_RANGED_DAMAGE_TAKEN_PCT; + if (attType != RANGED_ATTACK) + modDamageTakenPct = SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN_PCT; + + TakenTotalMod *= GetTotalAuraMultiplier(modDamageTakenPct, [spellProto](AuraEffect const* aurEff) + { + if (spellProto && spellProto->HasAttribute(SPELL_ATTR10_IGNORE_POSITIVE_DAMAGE_TAKEN_MODS) && aurEff->GetAmount() > 0) + return false; + + return true; + }); + + // Sanctified Wrath (bypass damage reduction) + if (TakenTotalMod < 1.0f) + { + SpellSchoolMask const attackSchoolMask = spellProto ? spellProto->GetSchoolMask() : damageSchoolMask; + + float damageReduction = 1.0f - TakenTotalMod; + Unit::AuraEffectList const& casterIgnoreResist = attacker->GetAuraEffectsByType(SPELL_AURA_MOD_IGNORE_TARGET_RESIST); + for (AuraEffect const* aurEff : casterIgnoreResist) + { + if (!(aurEff->GetMiscValue() & attackSchoolMask)) + continue; + + AddPct(damageReduction, -aurEff->GetAmount()); + } + + TakenTotalMod = 1.0f - damageReduction; + } + + return int32(std::max(std::ceil(static_cast(pdamage + TakenFlatBenefit) * TakenTotalMod), 0.0f)); +} + +void Unit::ApplySpellImmune(uint32 spellId, uint32 op, uint32 type, bool apply) +{ + if (apply) + m_spellImmune[op].emplace(type, spellId); + else + { + auto bounds = m_spellImmune[op].equal_range(type); + for (auto itr = bounds.first; itr != bounds.second;) + { + if (itr->second == spellId) + itr = m_spellImmune[op].erase(itr); + else + ++itr; + } + } +} + +float Unit::GetWeaponProcChance() const +{ + // normalized proc chance for weapon attack speed + // (odd formula...) + if (isAttackReady(BASE_ATTACK)) + return (GetBaseAttackTime(BASE_ATTACK) * 1.8f / 1000.0f); + else if (haveOffhandWeapon() && isAttackReady(OFF_ATTACK)) + return (GetBaseAttackTime(OFF_ATTACK) * 1.6f / 1000.0f); + return 0; +} + +float Unit::GetPPMProcChance(uint32 WeaponSpeed, float PPM, SpellInfo const* spellProto) const +{ + // proc per minute chance calculation + if (PPM <= 0) + return 0.0f; + + // Apply chance modifer aura + if (spellProto) + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellProto, SpellModOp::ProcFrequency, PPM); + + return std::floor((WeaponSpeed * PPM) / 600.0f); // result is chance in percents (probability = Speed_in_sec * (PPM / 60)) +} + +void Unit::Mount(uint32 mount, uint32 VehicleId, uint32 creatureEntry) +{ + if (mount) + SetUInt32Value(UNIT_FIELD_MOUNTDISPLAYID, mount); + + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_MOUNT); + + if (Player* player = ToPlayer()) + { + // mount as a vehicle + if (VehicleId) + { + if (CreateVehicleKit(VehicleId, creatureEntry)) + { + player->SendOnCancelExpectedVehicleRideAura(); + + // mounts can also have accessories + GetVehicleKit()->InstallAllAccessories(false); + } + } + + // unsummon pet + Pet* pet = player->GetPet(); + if (pet) + { + Battleground* bg = ToPlayer()->GetBattleground(); + // don't unsummon pet in arena but SetFlag UNIT_FLAG_STUNNED to disable pet's interface + if (bg && bg->isArena()) + pet->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + else + player->UnsummonPetTemporaryIfAny(); + } + + // if we have charmed npc, stun him also (everywhere) + if (Unit* charm = player->GetCharmed()) + if (charm->GetTypeId() == TYPEID_UNIT) + charm->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + + if (player->IsInWorld()) + player->SendMovementSetCollisionHeight(player->GetCollisionHeight(), UPDATE_COLLISION_HEIGHT_MOUNT); + } + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Mount); +} + +void Unit::Dismount() +{ + if (!IsMounted()) + return; + + SetUInt32Value(UNIT_FIELD_MOUNTDISPLAYID, 0); + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_MOUNT); + + if (Player* thisPlayer = ToPlayer()) + thisPlayer->SendMovementSetCollisionHeight(thisPlayer->GetCollisionHeight(), UPDATE_COLLISION_HEIGHT_MOUNT); + + WorldPackets::Misc::Dismount packet; + packet.Guid = GetGUID(); + SendMessageToSet(packet.Write(), IsPlayer()); + + // dismount as a vehicle + if (GetTypeId() == TYPEID_PLAYER && GetVehicleKit()) + RemoveVehicleKit(); + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Dismount); + + // only resummon old pet if the player is already added to a map + // this prevents adding a pet to a not created map which would otherwise cause a crash + // (it could probably happen when logging in after a previous crash) + if (Player* player = ToPlayer()) + { + if (Pet* pPet = player->GetPet()) + { + if (pPet->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED) && !pPet->HasUnitState(UNIT_STATE_STUNNED)) + pPet->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + } + else + player->ResummonPetTemporaryUnSummonedIfAny(); + + // if we have charmed npc, remove stun also + if (Unit* charm = player->GetCharmed()) + if (charm->GetTypeId() == TYPEID_UNIT && charm->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED) && !charm->HasUnitState(UNIT_STATE_STUNNED)) + charm->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + } +} + +MountCapabilityEntry const* Unit::GetMountCapability(uint32 mountType) const +{ + if (!mountType) + return nullptr; + + MountTypeEntry const* mountTypeEntry = sMountTypeStore.LookupEntry(mountType); + if (!mountTypeEntry) + return nullptr; + + + uint32 areaId = GetAreaId(); + uint32 ridingSkill = 5000; + EnumFlag mountFlags = AreaMountFlags::None; + bool isUnderwater = false; + bool isInWater = false; + + if (GetTypeId() == TYPEID_PLAYER) + ridingSkill = ToPlayer()->GetSkillValue(SKILL_RIDING); + + if (HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS)) + { + for (AuraEffect const* auraEffect : GetAuraEffectsByType(SPELL_AURA_MOUNT_RESTRICTIONS)) + mountFlags |= AreaMountFlags(auraEffect->GetMiscValue()); + } + else if (AreaTableEntry const* areaTable = sAreaTableStore.LookupEntry(areaId)) + mountFlags = areaTable->GetMountFlags(); + + ZLiquidStatus liquidStatus = GetLiquidStatus(); + isUnderwater = (liquidStatus & LIQUID_MAP_UNDER_WATER) != 0; + isInWater = (liquidStatus & LIQUID_MAP_IN_WATER) != 0; + + for (uint32 i = MAX_MOUNT_CAPABILITIES; i > 0; --i) + { + MountCapabilityEntry const* mountCapability = sMountCapabilityStore.LookupEntry(mountTypeEntry->Capability[i - 1]); + if (!mountCapability) + continue; + + if (ridingSkill < mountCapability->ReqRidingSkill) + continue; + + if (!(mountCapability->Flags & MOUNT_CAPABIILTY_FLAG_IGNORE_RESTRICTIONS)) + { + if (mountCapability->Flags & MOUNT_CAPABILITY_FLAG_GROUND && !(mountFlags.HasFlag(AreaMountFlags::AllowGroundMounts))) + continue; + if (mountCapability->Flags & MOUNT_CAPABILITY_FLAG_FLYING && !(mountFlags.HasFlag(AreaMountFlags::AllowFlyingMounts))) + continue; + if (mountCapability->Flags & MOUNT_CAPABILITY_FLAG_FLOAT && !(mountFlags.HasFlag(AreaMountFlags::AllowSurfaceSwimmingMounts))) + continue; + if (mountCapability->Flags & MOUNT_CAPABILITY_FLAG_UNDERWATER && !(mountFlags.HasFlag(AreaMountFlags::AllowUnderwaterSwimmingMounts))) + continue; + } + + // Do not allow underwater restricted capabilities when not underwater + if (!(mountCapability->Flags & MOUNT_CAPABILITY_FLAG_UNDERWATER) && isUnderwater) + continue; + + // Do not allow water surface restricted capabilities when not on water surface + if (!(mountCapability->Flags & MOUNT_CAPABILITY_FLAG_FLOAT) && isInWater) + continue; + + // Do not allow liquid restricted mounts on ground or in air + if (!(mountCapability->Flags & (MOUNT_CAPABILITY_FLAG_GROUND | MOUNT_CAPABILITY_FLAG_FLYING)) && !isUnderwater && !isInWater) + continue; + + if (mountCapability->ReqMapID != -1 && + int32(GetMapId()) != mountCapability->ReqMapID && + GetMap()->GetEntry()->ParentMapID != mountCapability->ReqMapID) + continue; + + if (mountCapability->ReqAreaID && !DBCManager::IsInArea(areaId, mountCapability->ReqAreaID)) + continue; + + if (mountCapability->ReqSpellAuraID && !HasAura(mountCapability->ReqSpellAuraID)) + continue; + + if (mountCapability->ReqSpellKnownID && !HasSpell(mountCapability->ReqSpellKnownID)) + continue; + + return mountCapability; + } + + return nullptr; +} + +void Unit::UpdateMountCapability() +{ + AuraEffectList mounts = GetAuraEffectsByType(SPELL_AURA_MOUNTED); + for (AuraEffect* aurEff : mounts) + { + aurEff->RecalculateAmount(); + if (!aurEff->GetAmount()) + aurEff->GetBase()->Remove(); + else if (MountCapabilityEntry const* capability = sMountCapabilityStore.LookupEntry(aurEff->GetAmount())) // aura may get removed by interrupt flag, reapply + if (!HasAura(capability->ModSpellAuraID)) + CastSpell(this, capability->ModSpellAuraID, aurEff); + } +} + +bool Unit::IsServiceProvider() const +{ + return HasFlag(UNIT_NPC_FLAGS, + UNIT_NPC_FLAG_VENDOR | UNIT_NPC_FLAG_TRAINER | UNIT_NPC_FLAG_FLIGHTMASTER | + UNIT_NPC_FLAG_PETITIONER | UNIT_NPC_FLAG_BATTLEMASTER | UNIT_NPC_FLAG_BANKER | + UNIT_NPC_FLAG_INNKEEPER | UNIT_NPC_FLAG_SPIRITHEALER | + UNIT_NPC_FLAG_SPIRITGUIDE | UNIT_NPC_FLAG_TABARDDESIGNER | UNIT_NPC_FLAG_AUCTIONEER); +} + +void Unit::EngageWithTarget(Unit* enemy) +{ + if (!enemy) + return; + + if (CanHaveThreatList()) + m_threatManager.AddThreat(enemy, 0.0f, nullptr, true, true); + else + SetInCombatWith(enemy); +} + +void Unit::SetImmuneToAll(bool apply, bool keepCombat) +{ + if (apply) + { + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC | UNIT_FLAG_IMMUNE_TO_NPC); + ValidateAttackersAndOwnTarget(); + if (!keepCombat) + m_combatManager.EndAllCombat(); + } + else + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC | UNIT_FLAG_IMMUNE_TO_NPC); +} + +void Unit::SetImmuneToPC(bool apply, bool keepCombat) +{ + if (apply) + { + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC); + ValidateAttackersAndOwnTarget(); + if (!keepCombat) + { + std::list toEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (pair.second->GetOther(this)->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (auto const& pair : m_combatManager.GetPvPCombatRefs()) + if (pair.second->GetOther(this)->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (CombatReference* ref : toEnd) + ref->EndCombat(); + } + } + else + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC); +} + +void Unit::SetImmuneToNPC(bool apply, bool keepCombat) +{ + if (apply) + { + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_NPC); + ValidateAttackersAndOwnTarget(); + if (!keepCombat) + { + std::list toEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (!pair.second->GetOther(this)->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (auto const& pair : m_combatManager.GetPvPCombatRefs()) + if (!pair.second->GetOther(this)->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (CombatReference* ref : toEnd) + ref->EndCombat(); + } + } + else + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_NPC); +} + +bool Unit::IsThreatened() const +{ + return !m_threatManager.IsThreatListEmpty(); +} + +bool Unit::isTargetableForAttack(bool checkFakeDeath) const +{ + if (!IsAlive()) + return false; + + if (HasFlag(UNIT_FIELD_FLAGS, + UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE)) + return false; + + if (GetTypeId() == TYPEID_PLAYER && ToPlayer()->IsGameMaster()) + return false; + + return !HasUnitState(UNIT_STATE_UNATTACKABLE) && (!checkFakeDeath || !HasUnitState(UNIT_STATE_DIED)); +} + +int32 Unit::ModifyHealth(int32 dVal) +{ + int32 gain = 0; + + if (dVal == 0) + return 0; + + int32 curHealth = (int32)GetHealth(); + + int32 val = dVal + curHealth; + if (val <= 0) + { + SetHealth(0); + return -curHealth; + } + + int32 maxHealth = (int32)GetMaxHealth(); + + if (val < maxHealth) + { + SetHealth(val); + gain = val - curHealth; + } + else if (curHealth != maxHealth) + { + SetHealth(maxHealth); + gain = maxHealth - curHealth; + } + + return gain; +} + +int32 Unit::GetHealthGain(int32 dVal) +{ + int32 gain = 0; + + if (dVal == 0) + return 0; + + int32 curHealth = (int32)GetHealth(); + + int32 val = dVal + curHealth; + if (val <= 0) + { + return -curHealth; + } + + int32 maxHealth = (int32)GetMaxHealth(); + + if (val < maxHealth) + gain = dVal; + else if (curHealth != maxHealth) + gain = maxHealth - curHealth; + + return gain; +} + +// returns negative amount on power reduction +int32 Unit::ModifyPower(PowerType power, int32 dVal, bool withPowerUpdate /*= true*/) +{ + int32 gain = 0; + + if (dVal == 0) + return 0; + + int32 curPower = GetPower(power); + + int32 val = dVal + curPower; + if (val <= GetMinPower(power)) + { + SetPower(power, GetMinPower(power), withPowerUpdate); + return -curPower; + } + + int32 maxPower = GetMaxPower(power); + + if (val < maxPower) + { + SetPower(power, val, withPowerUpdate); + gain = val - curPower; + } + else if (curPower != maxPower) + { + SetPower(power, maxPower, withPowerUpdate); + gain = maxPower - curPower; + } + + return gain; +} + +// Based on client function +float Unit::GetPowerRegen(PowerType powerType, bool isInCombat) const +{ + uint32 powerSlot = MAX_POWERS; + float totalRegeneration = 0.f; + double result = DBCManager::GetBasePowerRegen(powerType, isInCombat, _powerBarId); + float combatRegeneration = result; + + powerSlot = GetPowerIndex(powerType); + if (powerSlot != MAX_POWERS) + { + uint32 powerRegenIndex = isInCombat ? UNIT_FIELD_POWER_REGEN_INTERRUPTED_FLAT_MODIFIER : UNIT_FIELD_POWER_REGEN_FLAT_MODIFIER; + totalRegeneration = GetFloatValue(powerRegenIndex + powerSlot) + combatRegeneration; + result = totalRegeneration; + } + else + totalRegeneration = result; + + if (IsPlayer() && DBCManager::IsPowerTypeAffectedByHaste(powerType)) + { + float hasteRegen = GetFloatValue(PLAYER_FIELD_MOD_HASTE_REGEN); + if (G3D::fuzzyNe(hasteRegen, 0.f)) + result = totalRegeneration / hasteRegen; + } + + return result; +} + +bool Unit::IsAlwaysVisibleFor(WorldObject const* seer) const +{ + if (WorldObject::IsAlwaysVisibleFor(seer)) + return true; + + // Always seen by owner + if (ObjectGuid guid = GetCharmerOrOwnerGUID()) + if (seer->GetGUID() == guid) + return true; + + if (Player const* seerPlayer = seer->ToPlayer()) + if (Unit* owner = GetOwner()) + if (Player* ownerPlayer = owner->ToPlayer()) + if (ownerPlayer->IsGroupVisibleFor(seerPlayer)) + return true; + + return false; +} + +bool Unit::IsAlwaysDetectableFor(WorldObject const* seer) const +{ + if (WorldObject::IsAlwaysDetectableFor(seer)) + return true; + + if (HasAuraTypeWithCaster(SPELL_AURA_MOD_STALKED, seer->GetGUID())) + return true; + + return false; +} + +bool Unit::IsVisible() const +{ + return (m_serverSideVisibility.GetValue(SERVERSIDE_VISIBILITY_GM) > SEC_PLAYER) ? false : true; +} + +void Unit::SetVisible(bool x) +{ + if (!x) + m_serverSideVisibility.SetValue(SERVERSIDE_VISIBILITY_GM, SEC_GAMEMASTER); + else + m_serverSideVisibility.SetValue(SERVERSIDE_VISIBILITY_GM, SEC_PLAYER); + + UpdateObjectVisibility(); +} + +void Unit::UpdateSpeed(UnitMoveType mtype) +{ + int32 main_speed_mod = 0; + float stack_bonus = 1.0f; + float non_stack_bonus = 1.0f; + + switch (mtype) + { + // Only apply debuffs + case MOVE_FLIGHT_BACK: + case MOVE_RUN_BACK: + case MOVE_SWIM_BACK: + break; + case MOVE_WALK: + return; + case MOVE_RUN: + { + if (IsMounted()) // Use on mount auras + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED); + stack_bonus = GetTotalAuraMultiplier(SPELL_AURA_MOD_MOUNTED_SPEED_ALWAYS); + non_stack_bonus += GetMaxPositiveAuraModifier(SPELL_AURA_MOD_MOUNTED_SPEED_NOT_STACK) / 100.0f; + } + else + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_SPEED); + stack_bonus = GetTotalAuraMultiplier(SPELL_AURA_MOD_SPEED_ALWAYS); + non_stack_bonus += GetMaxPositiveAuraModifier(SPELL_AURA_MOD_SPEED_NOT_STACK) / 100.0f; + } + break; + } + case MOVE_SWIM: + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_SWIM_SPEED); + break; + } + case MOVE_FLIGHT: + { + if (GetTypeId() == TYPEID_UNIT && IsControlledByPlayer()) // not sure if good for pet + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_VEHICLE_FLIGHT_SPEED); + stack_bonus = GetTotalAuraMultiplier(SPELL_AURA_MOD_VEHICLE_SPEED_ALWAYS); + + // for some spells this mod is applied on vehicle owner + int32 owner_speed_mod = 0; + + if (Unit* owner = GetCharmer()) + owner_speed_mod = owner->GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_VEHICLE_FLIGHT_SPEED); + + main_speed_mod = std::max(main_speed_mod, owner_speed_mod); + } + else if (IsMounted()) + { + main_speed_mod = GetMaxPositiveAuraModifier(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED); + stack_bonus = GetTotalAuraMultiplier(SPELL_AURA_MOD_MOUNTED_FLIGHT_SPEED_ALWAYS); + } + else // Use not mount (shapeshift for example) auras (should stack) + main_speed_mod = GetTotalAuraModifier(SPELL_AURA_MOD_INCREASE_FLIGHT_SPEED) + GetTotalAuraModifier(SPELL_AURA_MOD_INCREASE_VEHICLE_FLIGHT_SPEED); + + non_stack_bonus += GetMaxPositiveAuraModifier(SPELL_AURA_MOD_FLIGHT_SPEED_NOT_STACK) / 100.0f; + + // Update speed for vehicle if available + if (GetTypeId() == TYPEID_PLAYER && GetVehicle()) + GetVehicleBase()->UpdateSpeed(MOVE_FLIGHT); + break; + } + default: + TC_LOG_ERROR("entities.unit", "Unit::UpdateSpeed: Unsupported move type (%d)", mtype); + return; + } + + // now we ready for speed calculation + float speed = std::max(non_stack_bonus, stack_bonus); + if (main_speed_mod) + AddPct(speed, main_speed_mod); + + switch (mtype) + { + case MOVE_RUN: + case MOVE_SWIM: + case MOVE_FLIGHT: + { + // Set creature speed rate + if (IsCreature()) + { + CreatureMovementInfo const& movementInfo = ToCreature()->GetCreatureMovementInfo(); + speed *= movementInfo.HasRunSpeedOverriden ? (movementInfo.RunSpeed / baseMoveSpeed[MOVE_RUN]) : ToCreature()->GetCreatureTemplate()->speed_run; // at this point, MOVE_WALK is never reached + } + + // Normalize speed by 191 aura SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED if need + /// @todo possible affect only on MOVE_RUN + if (int32 normalization = GetMaxPositiveAuraModifier(SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED)) + { + if (Creature* creature = ToCreature()) + { + uint32 immuneMask = creature->GetCreatureTemplate()->MechanicImmuneMask; + if (immuneMask & (1 << (MECHANIC_SNARE - 1)) || immuneMask & (1 << (MECHANIC_DAZE - 1))) + break; + } + + // Use speed from aura + float max_speed = normalization / (IsControlledByPlayer() ? playerBaseMoveSpeed[mtype] : baseMoveSpeed[mtype]); + if (speed > max_speed) + speed = max_speed; + } + break; + } + default: + break; + } + + // Apply strongest slow aura mod to speed + int32 slow = GetMaxNegativeAuraModifier(SPELL_AURA_MOD_DECREASE_SPEED); + if (slow) + AddPct(speed, slow); + + if (float minSpeedMod = (float)GetMaxPositiveAuraModifier(SPELL_AURA_MOD_MINIMUM_SPEED)) + { + float baseMinSpeed = 1.0f; + if (!GetOwnerOrCreatorGUID().IsPlayer() && !IsHunterPet() && IsCreature()) + { + CreatureMovementInfo const& movementInfo = ToCreature()->GetCreatureMovementInfo(); + baseMinSpeed = movementInfo.HasRunSpeedOverriden ? (movementInfo.RunSpeed / baseMoveSpeed[MOVE_RUN]) : ToCreature()->GetCreatureTemplate()->speed_run; + } + + float min_speed = CalculatePct(baseMinSpeed, minSpeedMod); + if (speed < min_speed) + speed = min_speed; + } + + SetSpeedRate(mtype, speed); +} + +float Unit::GetSpeed(UnitMoveType mtype) const +{ + return m_speed_rate[mtype]*(IsControlledByPlayer() ? playerBaseMoveSpeed[mtype] : baseMoveSpeed[mtype]); +} + +void Unit::SetSpeed(UnitMoveType mtype, float newValue) +{ + SetSpeedRate(mtype, newValue / (IsControlledByPlayer() ? playerBaseMoveSpeed[mtype] : baseMoveSpeed[mtype])); +} + +void Unit::SetSpeedRate(UnitMoveType mtype, float rate) +{ + if (rate < 0) + rate = 0.0f; + + // Update speed only on change + if (m_speed_rate[mtype] == rate) + return; + + + float newSpeedFlat = rate * (IsControlledByPlayer() ? playerBaseMoveSpeed[mtype] : baseMoveSpeed[mtype]); + if (IsMovedByClient() && IsInWorld()) + { + MovementPacketSender::SendSpeedChangeToMover(this, mtype, newSpeedFlat); + SetSpeedRateReal(mtype, rate); + } + else if (IsMovedByClient() && !IsInWorld()) // (1) + SetSpeedRateReal(mtype, rate); + else // <=> if(!IsMovedByPlayer()) + { + SetSpeedRateReal(mtype, rate); + MovementPacketSender::SendSpeedChangeToAll(this, mtype, newSpeedFlat); + } + + // explaination of (1): + // If the player is not in the world yet, it won't reply to the packets requiring an ack. And once the player is in the world, next time a movement + // packet which requires an ack is sent to the client (change of speed for example), the client is kicked from the + // server on the ground that it should have replied to the first packet first. That line is a hacky fix + // in the sense that it doesn't work like that in retail since buffs are applied only after the player has been + // initialized in the world. cf description of PR #18771 +} + +void Unit::SetSpeedRateReal(UnitMoveType mtype, float rate) +{ + if (!IsInCombat() && ToPlayer()) + if (Pet* pet = ToPlayer()->GetPet()) + pet->SetSpeedRate(mtype, rate); + + m_speed_rate[mtype] = rate; + PropagateSpeedChange(); +} + +void Unit::FollowTarget(Unit* target) +{ + if (!target) + { + TC_LOG_ERROR("entities.unit", "Unit::FollowTarget: Unit (%s) tried to follow a non-existant target.", GetGUID().ToString().c_str()); + return; + } + + // Determine follow configuration + bool joinFormation = false; // unit will follow its target in a generated formation shape and catches up to its target + bool catchUpToTarget = false; // unit will allign to the target speed and catches up to the target automatically + bool faceTarget = false; // unit will face its target with every spline + float distance = DEFAULT_FOLLOW_DISTANCE_PET; + + if (TempSummon* summon = ToTempSummon()) + { + if (SummonPropertiesEntry const* properties = summon->m_Properties) + { + // Allied summons, pet summons join a formation unless the following exceptions are being met. + if (properties->Control == SUMMON_CATEGORY_ALLY || properties->Control == SUMMON_CATEGORY_PET) + joinFormation = true; + + // Companion minipets will always be able to catch up to their target + if (properties->Slot == SUMMON_SLOT_MINIPET) + { + joinFormation = false; + catchUpToTarget = true; + faceTarget = true; + distance = DEFAULT_FOLLOW_DISTANCE; + } + + // Quest npcs follow their target outside of formations + if (properties->Slot == SUMMON_SLOT_QUEST) + { + joinFormation = false; + distance = DEFAULT_FOLLOW_DISTANCE; + } + } + + // Pets and minions alwys move in a formation of their target + if (summon->IsPet()) + joinFormation = true; + } + else if (IsCharmed()) + joinFormation = true; + + // Unit is already following its target + if (joinFormation && target->HasFormationFollower(this)) + return; + + GetMotionMaster()->MoveFollow(target, distance, DEFAULT_FOLLOW_ANGLE, joinFormation, catchUpToTarget, faceTarget); +} + +void Unit::RemoveFormationFollower(Unit* follower) +{ + for (FormationFollowerGUIDContainer::const_iterator itr = _formationFollowers.begin(); itr != _formationFollowers.end();) + { + ObjectGuid guid = *itr; + if (Unit* followingUnit = ObjectAccessor::GetUnit(*this, guid)) + { + if (followingUnit == follower) + itr = _formationFollowers.erase(itr); + else + ++itr; + } + else + itr = _formationFollowers.erase(itr); + } +} + +bool Unit::HasFormationFollower(Unit* follower) const +{ + for (ObjectGuid guid : _formationFollowers) + if (Unit* followingUnit = ObjectAccessor::GetUnit(*this, guid)) + if (followingUnit == follower) + return true; + + return false; +} + +void Unit::RemoveAllPursuers() +{ + for (uint8 i = 0; i < AsUnderlyingType(PursuingType::Max); ++i) + { + while (!_unitsPursuingMe[i].empty()) + (*_unitsPursuingMe[i].begin())->SetTarget(PursuingType(i), nullptr); + } +} + +void Unit::setDeathState(DeathState s) +{ + // Death state needs to be updated before RemoveAllAurasOnDeath() is called, to prevent entering combat + m_deathState = s; + + if (s != ALIVE && s != JUST_RESPAWNED) + { + CombatStop(); + ClearComboPointHolders(); // any combo points pointed to unit lost at it death + + if (IsNonMeleeSpellCast(false)) + InterruptNonMeleeSpells(false); + + ExitVehicle(); // Exit vehicle before calling RemoveAllControlled + // vehicles use special type of charm that is not removed by the next function + // triggering an assert + UnsummonAllTotems(); + RemoveAllControlled(); + RemoveAllAurasOnDeath(); + } + + if (s == JUST_DIED) + { + // remove aurastates allowing special moves + ClearAllReactives(); + ClearDiminishings(); + CancelPendingCastRequest(); + if (IsInWorld()) + { + // Only clear MotionMaster for entities that exists in world + // Avoids crashes in the following conditions : + // * Using 'call pet' on dead pets + // * Using 'call stabled pet' + // * Logging in with dead pets + GetMotionMaster()->Clear(false); + GetMotionMaster()->MoveIdle(); + } + StopMoving(); + DisableSpline(); + // without this when removing IncreaseMaxHealth aura player may stuck with 1 hp + // do not why since in IncreaseMaxHealth currenthealth is checked + SetHealth(0); + SetPower(GetPowerType(), 0); + SetUInt32Value(UNIT_NPC_EMOTESTATE, 0); + + // players in instance don't have ZoneScript, but they have InstanceScript + if (ZoneScript* zoneScript = GetZoneScript() ? GetZoneScript() : GetInstanceScript()) + zoneScript->OnUnitDeath(this); + } + else if (s == JUST_RESPAWNED) + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_SKINNABLE); // clear skinnable for creature and player (at battleground) +} + +//====================================================================== + +void Unit::AtEnterCombat() +{ + if (Spell* spell = m_currentSpells[CURRENT_GENERIC_SPELL]) + if (spell->getState() == SPELL_STATE_PREPARING + && spell->m_spellInfo->HasAttribute(SPELL_ATTR0_NOT_IN_COMBAT_ONLY_PEACEFUL) + && spell->m_spellInfo->InterruptFlags.HasFlag(SpellInterruptFlags::Combat) + && !spell->m_spellInfo->HasAura(SPELL_AURA_MOUNTED)) // for some reason mounts are not interrupted by entering combat even with both interrupt flag and attribute + InterruptSpell(CURRENT_GENERIC_SPELL, false, false); + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::EnteringCombat); + ProcSkillsAndAuras(this, nullptr, PROC_FLAG_ENTER_COMBAT, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); +} + +void Unit::AtExitCombat() +{ + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::LeavingCombat); +} + +void Unit::AtTargetAttacked(Unit* target, bool canInitialAggro) +{ + if (!target->IsEngaged() && !canInitialAggro) + return; + target->EngageWithTarget(this); + if (Unit* targetOwner = target->GetCharmerOrOwner()) + targetOwner->EngageWithTarget(this); + + Player* myPlayerOwner = GetCharmerOrOwnerPlayerOrPlayerItself(); + Player* targetPlayerOwner = target->GetCharmerOrOwnerPlayerOrPlayerItself(); + if (myPlayerOwner && targetPlayerOwner && !(myPlayerOwner->duel && myPlayerOwner->duel->opponent == targetPlayerOwner)) + { + myPlayerOwner->UpdatePvP(true); + myPlayerOwner->SetContestedPvP(targetPlayerOwner); + myPlayerOwner->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::PvPActive); + } +} + +void Unit::UpdatePetCombatState() +{ + ASSERT(!IsPet()); // player pets do not use UNIT_FLAG_PET_IN_COMBAT for this purpose - but player pets should also never have minions of their own to call this + + // UNIT_FLAG_PET_IN_COMBAT is set by default when the summoner is in combat by himself + if (IsInCombat()) + return; + + bool state = false; + for (Unit* minion : m_Controlled) + if (minion->IsInCombat()) + { + state = true; + break; + } + + if (state) + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT); + else + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT); +} + +//====================================================================== + +DiminishingLevels Unit::GetDiminishing(DiminishingGroup group) const +{ + DiminishingReturn const& diminish = m_Diminishing[group]; + if (!diminish.hitCount) + return DIMINISHING_LEVEL_1; + + // If last spell was cast more than 15 seconds ago - reset level + if (!diminish.stack && GetMSTimeDiffToNow(diminish.hitTime) > 15000) + return DIMINISHING_LEVEL_1; + + return DiminishingLevels(diminish.hitCount); +} + +void Unit::IncrDiminishing(SpellInfo const* auraSpellInfo, bool triggered) +{ + DiminishingGroup group = auraSpellInfo->GetDiminishingReturnsGroupForSpell(triggered); + uint32 currentLevel = GetDiminishing(group); + uint32 const maxLevel = auraSpellInfo->GetDiminishingReturnsMaxLevel(triggered); + + DiminishingReturn& diminish = m_Diminishing[group]; + if (currentLevel < maxLevel) + diminish.hitCount = currentLevel + 1; +} + +bool Unit::ApplyDiminishingToDuration(SpellInfo const* auraSpellInfo, bool triggered, int32& duration, WorldObject* caster, DiminishingLevels previousLevel) const +{ + DiminishingGroup const group = auraSpellInfo->GetDiminishingReturnsGroupForSpell(triggered); + if (duration == -1 || group == DIMINISHING_NONE) + return true; + + int32 const limitDuration = auraSpellInfo->GetDiminishingReturnsLimitDuration(triggered); + + // test pet/charm masters instead pets/charmeds + Unit const* targetOwner = GetCharmerOrOwner(); + Unit const* casterOwner = caster->GetCharmerOrOwner(); + + // Duration of crowd control abilities on pvp target is limited by 10 sec. (2.2.0) + if (limitDuration > 0 && duration > limitDuration) + { + Unit const* target = targetOwner ? targetOwner : this; + WorldObject const* source = casterOwner ? casterOwner : caster; + + if (target->IsAffectedByDiminishingReturns() && source->GetTypeId() == TYPEID_PLAYER) + duration = limitDuration; + } + + float mod = 1.0f; + if (group == DIMINISHING_TAUNT) + { + if (GetTypeId() == TYPEID_UNIT && (ToCreature()->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_TAUNT_DIMINISH)) + { + DiminishingLevels diminish = previousLevel; + switch (diminish) + { + case DIMINISHING_LEVEL_1: break; + case DIMINISHING_LEVEL_2: mod = 0.65f; break; + case DIMINISHING_LEVEL_3: mod = 0.4225f; break; + case DIMINISHING_LEVEL_4: mod = 0.274625f; break; + case DIMINISHING_LEVEL_TAUNT_IMMUNE: mod = 0.0f; break; + default: break; + } + } + } + // Some diminishings applies to mobs too (for example, Stun) + else if (auraSpellInfo->GetDiminishingReturnsGroupType(triggered) == DRTYPE_ALL || + (auraSpellInfo->GetDiminishingReturnsGroupType(triggered) == DRTYPE_PLAYER && + (targetOwner ? targetOwner->IsAffectedByDiminishingReturns() : IsAffectedByDiminishingReturns()))) + { + DiminishingLevels diminish = previousLevel; + switch (diminish) + { + case DIMINISHING_LEVEL_1: break; + case DIMINISHING_LEVEL_2: mod = 0.5f; break; + case DIMINISHING_LEVEL_3: mod = 0.25f; break; + case DIMINISHING_LEVEL_IMMUNE: mod = 0.0f; break; + default: break; + } + } + + duration = int32(duration * mod); + return (duration != 0); +} + +void Unit::ApplyDiminishingAura(DiminishingGroup group, bool apply) +{ + // Checking for existing in the table + DiminishingReturn& diminish = m_Diminishing[group]; + + if (apply) + ++diminish.stack; + else if (diminish.stack) + { + --diminish.stack; + + // Remember time after last aura from group removed + if (!diminish.stack) + diminish.hitTime = getMSTime(); + } +} + +void Unit::ClearDiminishings() +{ + for (DiminishingReturn& dim : m_Diminishing) + dim.Clear(); +} + +uint32 Unit::GetCreatureType() const +{ + if (GetTypeId() == TYPEID_PLAYER) + { + ShapeshiftForm form = GetShapeshiftForm(); + SpellShapeshiftFormEntry const* ssEntry = sSpellShapeshiftFormStore.LookupEntry(form); + if (ssEntry && ssEntry->CreatureType > 0) + return ssEntry->CreatureType; + else + return CREATURE_TYPE_HUMANOID; + } + else + return ToCreature()->GetCreatureTemplate()->type; +} + +uint32 Unit::GetCreatureTypeMask() const +{ + uint32 creatureType = GetCreatureType(); + return (creatureType >= 1) ? (1 << (creatureType - 1)) : 0; +} + +void Unit::SetShapeshiftForm(ShapeshiftForm form) +{ + SetByteValue(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_SHAPESHIFT_FORM, form); +} + +bool Unit::IsInFeralForm() const +{ + ShapeshiftForm form = GetShapeshiftForm(); + return form == FORM_CAT || form == FORM_BEAR; +} + +bool Unit::IsInDisallowedMountForm() const +{ + if (ShapeshiftForm form = GetShapeshiftForm()) + { + SpellShapeshiftFormEntry const* shapeshift = sSpellShapeshiftFormStore.LookupEntry(form); + if (!shapeshift) + return true; + + if (!shapeshift->GetFlags().HasFlag(SpellShapeshiftFormFlags::Stance)) + return true; + } + + if (GetDisplayId() == GetNativeDisplayId()) + return false; + + CreatureDisplayInfoEntry const* display = sCreatureDisplayInfoStore.LookupEntry(GetDisplayId()); + if (!display) + return true; + + CreatureDisplayInfoExtraEntry const* displayExtra = sCreatureDisplayInfoExtraStore.LookupEntry(display->ExtendedDisplayInfoID); + if (!displayExtra) + return true; + + CreatureModelDataEntry const* model = sCreatureModelDataStore.LookupEntry(display->ModelID); + ChrRacesEntry const* race = sChrRacesStore.LookupEntry(displayExtra->DisplayRaceID); + + if (model && !(model->Flags & 0x80)) + if (race && !(race->Flags & 0x4)) + return true; + + return false; +} + +/*####################################### +######## ######## +######## STAT SYSTEM ######## +######## ######## +#######################################*/ + +void Unit::HandleStatFlatModifier(UnitMods unitMod, UnitModifierFlatType modifierType, float amount, bool apply) +{ + if (unitMod >= UNIT_MOD_END || modifierType >= MODIFIER_TYPE_FLAT_END) + { + TC_LOG_ERROR("entities.unit", "ERROR in HandleStatFlatModifier(): non-existing UnitMods or wrong UnitModifierType!"); + return; + } + + if (!amount) + return; + + switch (modifierType) + { + case BASE_VALUE: + case TOTAL_VALUE: + m_auraFlatModifiersGroup[unitMod][modifierType] += apply ? amount : -amount; + break; + default: + break; + } + + UpdateUnitMod(unitMod); +} + +void Unit::ApplyStatPctModifier(UnitMods unitMod, UnitModifierPctType modifierType, float pct) +{ + if (unitMod >= UNIT_MOD_END || modifierType >= MODIFIER_TYPE_PCT_END) + { + TC_LOG_ERROR("entities.unit", "ERROR in ApplyStatPctModifier(): non-existing UnitMods or wrong UnitModifierType!"); + return; + } + + if (!pct) + return; + + switch (modifierType) + { + case BASE_PCT: + case TOTAL_PCT: + AddPct(m_auraPctModifiersGroup[unitMod][modifierType], pct); + break; + default: + break; + } + + UpdateUnitMod(unitMod); +} + +void Unit::SetStatFlatModifier(UnitMods unitMod, UnitModifierFlatType modifierType, float val) +{ + if (m_auraFlatModifiersGroup[unitMod][modifierType] == val) + return; + + m_auraFlatModifiersGroup[unitMod][modifierType] = val; + UpdateUnitMod(unitMod); +} + +void Unit::SetStatPctModifier(UnitMods unitMod, UnitModifierPctType modifierType, float val) +{ + if (m_auraPctModifiersGroup[unitMod][modifierType] == val) + return; + + m_auraPctModifiersGroup[unitMod][modifierType] = val; + UpdateUnitMod(unitMod); +} + +float Unit::GetFlatModifierValue(UnitMods unitMod, UnitModifierFlatType modifierType) const +{ + if (unitMod >= UNIT_MOD_END || modifierType >= MODIFIER_TYPE_FLAT_END) + { + TC_LOG_ERROR("entities.unit", "attempt to access non-existing modifier value from UnitMods!"); + return 0.0f; + } + + return m_auraFlatModifiersGroup[unitMod][modifierType]; +} + +float Unit::GetPctModifierValue(UnitMods unitMod, UnitModifierPctType modifierType) const +{ + if (unitMod >= UNIT_MOD_END || modifierType >= MODIFIER_TYPE_PCT_END) + { + TC_LOG_ERROR("entities.unit", "attempt to access non-existing modifier value from UnitMods!"); + return 0.0f; + } + + return m_auraPctModifiersGroup[unitMod][modifierType]; +} + +void Unit::UpdateUnitMod(UnitMods unitMod) +{ + if (!CanModifyStats()) + return; + + switch (unitMod) + { + case UNIT_MOD_ARMOR: + UpdateArmor(); + break; + case UNIT_MOD_HEALTH: + UpdateMaxHealth(); + break; + case UNIT_MOD_MANA: + case UNIT_MOD_RAGE: + case UNIT_MOD_FOCUS: + case UNIT_MOD_ENERGY: + case UNIT_MOD_RUNE: + case UNIT_MOD_RUNIC_POWER: + UpdateMaxPower(GetPowerTypeByAuraGroup(unitMod)); + break; + case UNIT_MOD_RESISTANCE_HOLY: + case UNIT_MOD_RESISTANCE_FIRE: + case UNIT_MOD_RESISTANCE_NATURE: + case UNIT_MOD_RESISTANCE_FROST: + case UNIT_MOD_RESISTANCE_SHADOW: + case UNIT_MOD_RESISTANCE_ARCANE: + UpdateResistances(GetSpellSchoolByAuraGroup(unitMod)); + break; + case UNIT_MOD_ATTACK_POWER: + UpdateAttackPowerAndDamage(); + break; + case UNIT_MOD_ATTACK_POWER_RANGED: + UpdateAttackPowerAndDamage(true); + break; + case UNIT_MOD_DAMAGE_MAINHAND: + UpdateDamagePhysical(BASE_ATTACK); + break; + case UNIT_MOD_DAMAGE_OFFHAND: + UpdateDamagePhysical(OFF_ATTACK); + break; + case UNIT_MOD_DAMAGE_RANGED: + UpdateDamagePhysical(RANGED_ATTACK); + break; + default: + break; + } +} + +void Unit::UpdateDamageDoneMods(WeaponAttackType attackType, int32 /*skipEnchantSlot = -1*/) +{ + UnitMods unitMod; + switch (attackType) + { + case BASE_ATTACK: + unitMod = UNIT_MOD_DAMAGE_MAINHAND; + break; + case OFF_ATTACK: + unitMod = UNIT_MOD_DAMAGE_OFFHAND; + break; + case RANGED_ATTACK: + unitMod = UNIT_MOD_DAMAGE_RANGED; + break; + default: + ABORT(); + break; + } + + float amount = GetTotalAuraModifier(SPELL_AURA_MOD_DAMAGE_DONE, [&](AuraEffect const* aurEff) -> bool + { + if (!(aurEff->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL)) + return false; + + return CheckAttackFitToAuraRequirement(attackType, aurEff); + }); + + SetStatFlatModifier(unitMod, TOTAL_VALUE, amount); +} + +void Unit::UpdateAllDamageDoneMods() +{ + for (uint8 i = BASE_ATTACK; i < MAX_ATTACK; ++i) + UpdateDamageDoneMods(WeaponAttackType(i)); +} + +void Unit::UpdateDamagePctDoneMods(WeaponAttackType attackType) +{ + float factor; + UnitMods unitMod; + switch (attackType) + { + case BASE_ATTACK: + factor = 1.0f; + unitMod = UNIT_MOD_DAMAGE_MAINHAND; + break; + case OFF_ATTACK: + // off hand has 50% penalty + factor = 0.5f; + unitMod = UNIT_MOD_DAMAGE_OFFHAND; + break; + case RANGED_ATTACK: + factor = 1.0f; + unitMod = UNIT_MOD_DAMAGE_RANGED; + break; + default: + ABORT(); + break; + } + + factor *= GetTotalAuraMultiplier(SPELL_AURA_MOD_DAMAGE_PERCENT_DONE, [attackType, this](AuraEffect const* aurEff) -> bool + { + if (!(aurEff->GetMiscValue() & SPELL_SCHOOL_MASK_NORMAL)) + return false; + + return CheckAttackFitToAuraRequirement(attackType, aurEff); + }); + + if (attackType == OFF_ATTACK) + factor *= GetTotalAuraMultiplier(SPELL_AURA_MOD_OFFHAND_DAMAGE_PCT, std::bind(&Unit::CheckAttackFitToAuraRequirement, this, attackType, std::placeholders::_1)); + + SetStatPctModifier(unitMod, TOTAL_PCT, factor); +} + +void Unit::UpdateAllDamagePctDoneMods() +{ + for (uint8 i = BASE_ATTACK; i < MAX_ATTACK; ++i) + UpdateDamagePctDoneMods(WeaponAttackType(i)); +} + +float Unit::GetTotalAuraModValue(UnitMods unitMod) const +{ + if (unitMod >= UNIT_MOD_END) + { + TC_LOG_ERROR("entities.unit", "attempt to access non-existing UnitMods in GetTotalAuraModValue()!"); + return 0.0f; + } + + float value = GetFlatModifierValue(unitMod, BASE_VALUE); + value *= GetPctModifierValue(unitMod, BASE_PCT); + value += GetFlatModifierValue(unitMod, TOTAL_VALUE); + value *= GetPctModifierValue(unitMod, TOTAL_PCT); + + return value; +} + +SpellSchools Unit::GetSpellSchoolByAuraGroup(UnitMods unitMod) const +{ + SpellSchools school = SPELL_SCHOOL_NORMAL; + + switch (unitMod) + { + case UNIT_MOD_RESISTANCE_HOLY: school = SPELL_SCHOOL_HOLY; break; + case UNIT_MOD_RESISTANCE_FIRE: school = SPELL_SCHOOL_FIRE; break; + case UNIT_MOD_RESISTANCE_NATURE: school = SPELL_SCHOOL_NATURE; break; + case UNIT_MOD_RESISTANCE_FROST: school = SPELL_SCHOOL_FROST; break; + case UNIT_MOD_RESISTANCE_SHADOW: school = SPELL_SCHOOL_SHADOW; break; + case UNIT_MOD_RESISTANCE_ARCANE: school = SPELL_SCHOOL_ARCANE; break; + + default: + break; + } + + return school; +} + +PowerType Unit::GetPowerTypeByAuraGroup(UnitMods unitMod) const +{ + switch (unitMod) + { + case UNIT_MOD_RAGE: return POWER_RAGE; + case UNIT_MOD_FOCUS: return POWER_FOCUS; + case UNIT_MOD_ENERGY: return POWER_ENERGY; + case UNIT_MOD_RUNE: return POWER_RUNE; + case UNIT_MOD_RUNIC_POWER: return POWER_RUNIC_POWER; + default: + case UNIT_MOD_MANA: return POWER_MANA; + } +} + +float Unit::GetTotalAttackPowerValue(WeaponAttackType attType) const +{ + if (attType == RANGED_ATTACK) + { + int32 ap = GetInt32Value(UNIT_FIELD_RANGED_ATTACK_POWER) + + GetInt32Value(UNIT_FIELD_RANGED_ATTACK_POWER_MOD_POS) + - GetInt32Value(UNIT_FIELD_RANGED_ATTACK_POWER_MOD_NEG); + + if (ap < 0) + return 0.0f; + return ap * (1.0f + GetFloatValue(UNIT_FIELD_RANGED_ATTACK_POWER_MULTIPLIER)); + } + else + { + int32 ap = GetInt32Value(UNIT_FIELD_ATTACK_POWER) + + GetInt32Value(UNIT_FIELD_ATTACK_POWER_MOD_POS) + - GetInt32Value(UNIT_FIELD_ATTACK_POWER_MOD_NEG); + + if (ap < 0) + return 0.0f; + return ap * (1.0f + GetFloatValue(UNIT_FIELD_ATTACK_POWER_MULTIPLIER)); + } +} + +float Unit::GetWeaponDamageRange(WeaponAttackType attType, WeaponDamageRange type) const +{ + if (attType == OFF_ATTACK && !haveOffhandWeapon()) + return 0.0f; + + return m_weaponDamage[attType][type]; +} + +bool Unit::CanFreeMove() const +{ + return !HasUnitState(UNIT_STATE_CONFUSED | UNIT_STATE_FLEEING | UNIT_STATE_IN_FLIGHT | + UNIT_STATE_ROOT | UNIT_STATE_STUNNED | UNIT_STATE_DISTRACTED) && GetOwnerOrCreatorGUID().IsEmpty(); +} + +void Unit::SetLevel(uint8 lvl) +{ + SetUInt32Value(UNIT_FIELD_LEVEL, lvl); + + if (Player* player = ToPlayer()) + { + // group update + if (player->GetGroup()) + player->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_LEVEL); + + sCharacterCache->UpdateCharacterLevel(GetGUID(), lvl); + } +} + +void Unit::SetHealth(uint32 val) +{ + if (getDeathState() == JUST_DIED || getDeathState() == CORPSE) + val = 0; + else if (GetTypeId() == TYPEID_PLAYER && getDeathState() == DEAD) + val = 1; + else + { + uint32 maxHealth = GetMaxHealth(); + if (maxHealth < val) + val = maxHealth; + } + + SetUInt32Value(UNIT_FIELD_HEALTH, val); + + // group update + if (Player* player = ToPlayer()) + { + if (player->GetGroup()) + player->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_CUR_HP); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_CUR_HP); + } + } +} + +void Unit::SetMaxHealth(uint32 val) +{ + if (!val) + val = 1; + + uint32 health = GetHealth(); + SetUInt32Value(UNIT_FIELD_MAXHEALTH, val); + + // group update + if (GetTypeId() == TYPEID_PLAYER) + { + if (ToPlayer()->GetGroup()) + ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_MAX_HP); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_MAX_HP); + } + } + + if (val < health) + SetHealth(val); +} + +int32 Unit::GetPower(PowerType power) const +{ + uint32 powerIndex = GetPowerIndex(power); + if (powerIndex == MAX_POWERS) + return 0; + + return GetUInt32Value(UNIT_FIELD_POWER1 + powerIndex); +} + +int32 Unit::GetMaxPower(PowerType power) const +{ + uint32 powerIndex = GetPowerIndex(power); + if (powerIndex == MAX_POWERS) + return 0; + + return GetInt32Value(UNIT_FIELD_MAXPOWER1 + powerIndex); +} + +void Unit::SetPower(PowerType power, int32 val, bool withPowerUpdate /*= true*/) +{ + uint32 powerIndex = GetPowerIndex(power); + if (powerIndex == MAX_POWERS) + return; + + int32 maxPower = int32(GetMaxPower(power)); + if (maxPower < val) + val = maxPower; + + SetInt32Value(UNIT_FIELD_POWER1 + powerIndex, val); + + if (IsInWorld() && withPowerUpdate) + { + WorldPackets::Combat::PowerUpdate packet; + packet.Guid = GetGUID(); + packet.Powers.emplace_back(val, power); + SendMessageToSet(packet.Write(), IsPlayer()); + } + + // group update + if (Player* player = ToPlayer()) + { + if (player->GetGroup()) + player->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_CUR_POWER); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_CUR_POWER); + } + } +} + +void Unit::SetMaxPower(PowerType power, int32 val) +{ + uint32 powerIndex = GetPowerIndex(power); + if (powerIndex == MAX_POWERS) + return; + + int32 cur_power = GetPower(power); + SetInt32Value(UNIT_FIELD_MAXPOWER1 + powerIndex, val); + + // group update + if (GetTypeId() == TYPEID_PLAYER) + { + if (ToPlayer()->GetGroup()) + ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_MAX_POWER); + } + else if (Pet* pet = ToCreature()->ToPet()) + { + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_MAX_POWER); + } + } + + if (val < cur_power) + SetPower(power, val); +} + +void Unit::Regenerate(PowerType powerType, uint32 diff) +{ + uint32 maxValue = GetMaxPower(powerType); + if (!maxValue) + return; + + if (!HasFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_REGENERATE_POWER)) + return; + + uint32 curValue = GetPower(powerType); + + // Block power regeneration + if (HasAuraTypeWithValue(SPELL_AURA_PREVENT_REGENERATE_POWER, powerType) || HasAuraType(SPELL_AURA_INTERRUPT_REGEN)) + return; + + // Skip regeneration for power type we cannot have + uint32 powerIndex = GetPowerIndex(powerType); + if (powerIndex == MAX_POWERS) + return; + + float addValue = GetPowerRegen(powerType, IsInCombat()) * (diff / float(IN_MILLISECONDS)); + + // Apply config values + switch (powerType) + { + case POWER_MANA: + addValue *= sWorld->getRate(RATE_POWER_MANA); + break; + case POWER_RAGE: + addValue *= sWorld->getRate(RATE_POWER_RAGE_LOSS); + break; + case POWER_RUNIC_POWER: + addValue *= sWorld->getRate(RATE_POWER_RUNICPOWER_LOSS); + break; + case POWER_FOCUS: + addValue *= sWorld->getRate(RATE_POWER_FOCUS); + break; + case POWER_ENERGY: + addValue *= sWorld->getRate(RATE_POWER_ENERGY); + break; + default: + break; + } + + if (addValue < 0.0f) + { + if (curValue == 0) + return; + } + else if (addValue > 0.0f) + { + if (curValue == maxValue) + return; + } + else + return; + + addValue += _powerFraction[powerIndex]; + uint32 integerValue = uint32(std::fabs(addValue)); + + if (addValue < 0.0f) + { + if (curValue > integerValue) + { + curValue -= integerValue; + _powerFraction[powerIndex] = addValue + integerValue; + } + else + { + curValue = 0; + _powerFraction[powerIndex] = 0; + } + } + else + { + curValue += integerValue; + + if (curValue > maxValue) + { + curValue = maxValue; + _powerFraction[powerIndex] = 0; + } + else + _powerFraction[powerIndex] = addValue - integerValue; + } + + // Players update their powers in 2 seconds intervals, creatures in 1 second ones + if (_powerUpdateTimer <= 0) + SetPower(powerType, curValue); + else + UpdateUInt32Value(UNIT_FIELD_POWER1 + powerIndex, curValue); +} + +void Unit::RegisterPowerTypes() +{ + for (uint8 i = POWER_MANA; i < MAX_POWERS; ++i) + { + uint32 powerIndex = GetPowerIndex(PowerType(i)); + if (powerIndex == MAX_POWERS || powerIndex == MAX_POWERS_PER_CLASS) + continue; + + _usedPowerTypes[powerIndex] = static_cast(i); + } +} + +void Unit::AIUpdateTick(uint32 diff) +{ + if (UnitAI* ai = GetAI()) + { + m_aiLocked = true; + ai->UpdateAI(diff); + m_aiLocked = false; + } +} + +void Unit::PushAI(UnitAI* newAI) +{ + i_AIs.emplace(newAI); +} + +void Unit::SetAI(UnitAI* newAI) +{ + PushAI(newAI); + RefreshAI(); +} + +bool Unit::PopAI() +{ + if (!i_AIs.empty()) + { + i_AIs.pop(); + return true; + } + else + return false; +} + +void Unit::RefreshAI() +{ + ASSERT(!m_aiLocked, "Tried to change current AI during UpdateAI()"); + if (i_AIs.empty()) + i_AI = nullptr; + else + i_AI = i_AIs.top(); +} + +void Unit::ScheduleAIChange() +{ + bool const charmed = IsCharmed(); + + if (charmed) + PushAI(GetScheduledChangeAI()); + else + { + RestoreDisabledAI(); + PushAI(GetScheduledChangeAI()); //This could actually be PopAI() to get the previous AI but it's required atm to trigger UpdateCharmAI() + } +} + +void Unit::RestoreDisabledAI() +{ + // Keep popping the stack until we either reach the bottom or find a valid AI + while (PopAI()) + if (GetTopAI() && dynamic_cast(GetTopAI()) == nullptr) + return; +} + +UnitAI* Unit::GetScheduledChangeAI() +{ + if (Creature* creature = ToCreature()) + return new ScheduledChangeAI(creature); + else + return nullptr; +} + +bool Unit::HasScheduledAIChange() const +{ + if (UnitAI* ai = GetAI()) + return dynamic_cast(ai) != nullptr; + else + return true; +} + +void Unit::AddToWorld() +{ + WorldObject::AddToWorld(); + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::EnterWorld); +} + +void Unit::RemoveFromWorld() +{ + // cleanup + ASSERT(GetGUID()); + + if (IsInWorld()) + { + m_duringRemoveFromWorld = true; + if (UnitAI* ai = GetAI()) + ai->LeavingWorld(); + + if (IsVehicle()) + RemoveVehicleKit(true); + + RemoveCharmAuras(); + RemoveBindSightAuras(); + RemoveNotOwnSingleTargetAuras(); + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::LeaveWorld); + + RemoveAllGameObjects(); + RemoveAllDynObjects(); + + ExitVehicle(); // Remove applied auras with SPELL_AURA_CONTROL_VEHICLE + UnsummonAllTotems(); + RemoveAllControlled(); + + RemoveAreaAurasDueToLeaveWorld(); + + // clear idle movement slot to finalize follow movement to unregister formation targets + GetMotionMaster()->Clear(MOTION_SLOT_IDLE); + RemoveAllPursuers(); + + if (IsCharmed()) + RemoveCharmedBy(nullptr); + + ASSERT(!GetCharmedGUID(), "Unit %u has charmed guid when removed from world", GetEntry()); + ASSERT(!GetCharmerGUID(), "Unit %u has charmer guid when removed from world", GetEntry()); + + if (Unit* owner = GetOwner()) + { + if (owner->m_Controlled.find(this) != owner->m_Controlled.end()) + { + TC_LOG_FATAL("entities.unit", "Unit %u is in controlled list of %u when removed from world", GetEntry(), owner->GetEntry()); + ABORT(); + } + } + + WorldObject::RemoveFromWorld(); + m_duringRemoveFromWorld = false; + } +} + +void Unit::CleanupBeforeRemoveFromMap(bool finalCleanup) +{ + // This needs to be before RemoveFromWorld to make GetCaster() return a valid pointer on aura removal + InterruptNonMeleeSpells(true); + + if (IsInWorld()) + RemoveFromWorld(); + else + { + // cleanup that must happen even if not in world + if (IsVehicle()) + RemoveVehicleKit(true); + } + + ASSERT(GetGUID()); + + // A unit may be in removelist and not in world, but it is still in grid + // and may have some references during delete + RemoveAllAuras(); + RemoveAllGameObjects(); + + if (finalCleanup) + m_cleanupDone = true; + + CombatStop(); + ClearComboPointHolders(); +} + +void Unit::CleanupsBeforeDelete(bool finalCleanup) +{ + CleanupBeforeRemoveFromMap(finalCleanup); + + WorldObject::CleanupsBeforeDelete(finalCleanup); +} + +void Unit::UpdateCharmAI() +{ + if (IsCharmed()) + { + UnitAI* newAI = nullptr; + if (IsPlayer()) + { + if (Unit* charmer = GetCharmer()) + { + // first, we check if the creature's own AI specifies an override playerai for its owned players + if (Creature* creatureCharmer = charmer->ToCreature()) + { + if (CreatureAI* charmerAI = creatureCharmer->AI()) + newAI = charmerAI->GetAIForCharmedPlayer(ToPlayer()); + } + else + TC_LOG_ERROR("misc", "Attempt to assign charm AI to player %s who is charmed by non-creature %s.", GetGUID().ToString().c_str(), GetCharmerGUID().ToString().c_str()); + } + if (!newAI) // otherwise, we default to the generic one + newAI = new SimpleCharmedPlayerAI(ToPlayer()); + } + else + { + ASSERT(IsCreature()); + if (isPossessed() || IsVehicle()) + newAI = new PossessedAI(ToCreature()); + else + newAI = new PetAI(ToCreature()); + } + + ASSERT(newAI); + SetAI(newAI); + newAI->OnCharmed(true); + } + else + { + RestoreDisabledAI(); + // Hack: this is required because we want to call OnCharmed(true) on the restored AI + RefreshAI(); + if (UnitAI* ai = GetAI()) + ai->OnCharmed(true); + } +} + +CharmInfo* Unit::InitCharmInfo() +{ + if (!m_charmInfo) + m_charmInfo = std::make_unique(this); + + return m_charmInfo.get(); +} + +void Unit::DeleteCharmInfo() +{ + if (!m_charmInfo) + return; + + m_charmInfo->RestoreState(); + m_charmInfo.reset(); +} + +bool Unit::isFrozen() const +{ + return HasAuraState(AURA_STATE_FROZEN); +} + +ProcFlagsHit createProcHitMask(SpellNonMeleeDamage* damageInfo, SpellMissInfo missCondition) +{ + ProcFlagsHit hitMask = PROC_HIT_NONE; + // Check victim state + if (missCondition != SPELL_MISS_NONE) + { + switch (missCondition) + { + case SPELL_MISS_MISS: + hitMask |= PROC_HIT_MISS; + break; + case SPELL_MISS_DODGE: + hitMask |= PROC_HIT_DODGE; + break; + case SPELL_MISS_PARRY: + hitMask |= PROC_HIT_PARRY; + break; + case SPELL_MISS_BLOCK: + // spells can't be partially blocked (it's damage can though) + hitMask |= PROC_HIT_BLOCK | PROC_HIT_FULL_BLOCK; + break; + case SPELL_MISS_EVADE: + hitMask |= PROC_HIT_EVADE; + break; + case SPELL_MISS_IMMUNE: + case SPELL_MISS_IMMUNE2: + hitMask |= PROC_HIT_IMMUNE; + break; + case SPELL_MISS_DEFLECT: + hitMask |= PROC_HIT_DEFLECT; + break; + case SPELL_MISS_ABSORB: + hitMask |= PROC_HIT_ABSORB; + break; + case SPELL_MISS_REFLECT: + hitMask |= PROC_HIT_REFLECT; + break; + case SPELL_MISS_RESIST: + hitMask |= PROC_HIT_FULL_RESIST; + break; + default: + break; + } + } + else + { + // On block + if (damageInfo->blocked) + { + hitMask |= PROC_HIT_BLOCK; + if (damageInfo->fullBlock) + hitMask |= PROC_HIT_FULL_BLOCK; + } + // On absorb + if (damageInfo->absorb) + hitMask |= PROC_HIT_ABSORB; + + // Don't set hit/crit hitMask if damage is nullified + bool const damageNullified = (damageInfo->HitInfo & (HITINFO_FULL_ABSORB | HITINFO_FULL_RESIST)) != 0 || (hitMask & PROC_HIT_FULL_BLOCK) != 0; + if (!damageNullified) + { + // On crit + if (damageInfo->HitInfo & SPELL_HIT_TYPE_CRIT) + hitMask |= PROC_HIT_CRITICAL; + else + hitMask |= PROC_HIT_NORMAL; + } + else if ((damageInfo->HitInfo & HITINFO_FULL_RESIST) != 0) + hitMask |= PROC_HIT_FULL_RESIST; + } + + return hitMask; +} + +void Unit::ProcSkillsAndReactives(bool isVictim, Unit* procTarget, ProcFlags typeMask, ProcFlagsHit hitMask, WeaponAttackType /*attType*/) +{ + // Player is loaded now - do not allow passive spell casts to proc + if (GetTypeId() == TYPEID_PLAYER && ToPlayer()->GetSession()->PlayerLoading()) + return; + + // For melee/ranged based attack need update skills and set some Aura states if victim present + if (typeMask & MELEE_BASED_TRIGGER_MASK && procTarget) + { + // If exist crit/parry/dodge/block need update aura state (for victim and attacker) + if (hitMask & (PROC_HIT_CRITICAL | PROC_HIT_PARRY | PROC_HIT_DODGE | PROC_HIT_BLOCK)) + { + // for victim + if (isVictim) + { + // Skip checks for Death Knights when in Blood Presence (Rune Strike) + if (getClass() == CLASS_DEATH_KNIGHT && hitMask & (PROC_HIT_DODGE | PROC_HIT_PARRY | PROC_HIT_BLOCK)) + if (GetAuraEffect(SPELL_AURA_MOD_BASE_RESISTANCE_PCT, SPELLFAMILY_DEATHKNIGHT, 0x800000, 0, 0, GetGUID())) + return; + + // if victim and dodge attack + if (hitMask & PROC_HIT_DODGE) + { + // Update AURA_STATE on dodge + if (getClass() != CLASS_ROGUE) // skip Rogue Riposte + { + ModifyAuraState(AURA_STATE_DEFENSE, true); + StartReactiveTimer(REACTIVE_DEFENSE); + } + } + // if victim and parry attack + if (hitMask & PROC_HIT_PARRY) + { + // For Hunters only Counterattack (skip Mongoose bite) + if (getClass() == CLASS_HUNTER) + { + ModifyAuraState(AURA_STATE_HUNTER_PARRY, true); + StartReactiveTimer(REACTIVE_HUNTER_PARRY); + } + else + { + ModifyAuraState(AURA_STATE_DEFENSE, true); + StartReactiveTimer(REACTIVE_DEFENSE); + } + } + // if and victim block attack + if (hitMask & PROC_HIT_BLOCK) + { + ModifyAuraState(AURA_STATE_DEFENSE, true); + StartReactiveTimer(REACTIVE_DEFENSE); + } + } + else // For attacker + { + // Overpower on victim dodge + if ((hitMask & PROC_HIT_DODGE) && GetTypeId() == TYPEID_PLAYER && getClass() == CLASS_WARRIOR) + { + ToPlayer()->AddComboPoints(procTarget, 1); + StartReactiveTimer(REACTIVE_OVERPOWER); + } + } + } + } +} + +void Unit::GetProcAurasTriggeredOnEvent(AuraApplicationProcContainer& aurasTriggeringProc, AuraApplicationList* procAuras, ProcEventInfo& eventInfo) +{ + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + + auto processAuraApplication = [&](AuraApplication* aurApp) + { + if (uint32 procEffectMask = aurApp->GetBase()->GetProcEffectMask(aurApp, eventInfo, now)) + { + aurApp->GetBase()->PrepareProcToTrigger(aurApp, eventInfo, now); + aurasTriggeringProc.emplace_back(procEffectMask, aurApp); + } + else + { + if (aurApp->GetBase()->GetSpellInfo()->HasAttribute(SPELL_ATTR0_PROC_FAILURE_BURNS_CHARGE)) + { + if (SpellProcEntry const* procEntry = sSpellMgr->GetSpellProcEntry(aurApp->GetBase()->GetSpellInfo()->Id)) + { + aurApp->GetBase()->PrepareProcChargeDrop(procEntry, eventInfo); + aurasTriggeringProc.emplace_back(0, aurApp); + } + } + + if (aurApp->GetBase()->GetSpellInfo()->HasAttribute(SPELL_ATTR2_PROC_COOLDOWN_ON_FAILURE)) + if (SpellProcEntry const* procEntry = sSpellMgr->GetSpellProcEntry(aurApp->GetBase()->GetSpellInfo()->Id)) + aurApp->GetBase()->AddProcCooldown(procEntry, now); + } + }; + + // use provided list of auras which can proc + if (procAuras) + { + for (AuraApplication* aurApp : *procAuras) + { + ASSERT(aurApp->GetTarget() == this); + processAuraApplication(aurApp); + } + } + // or generate one on our own + else + { + for (AuraApplicationMap::iterator itr = GetAppliedAuras().begin(); itr != GetAppliedAuras().end(); ++itr) + processAuraApplication(itr->second); + } +} + +void Unit::TriggerAurasProcOnEvent(Unit* actionTarget, ProcFlags typeMaskActor, ProcFlags typeMaskActionTarget, ProcFlagsSpellType spellTypeMask, + ProcFlagsSpellPhase spellPhaseMask, ProcFlagsHit hitMask, Spell* spell, DamageInfo* damageInfo, HealInfo* healInfo) +{ + // prepare data for self trigger + ProcEventInfo myProcEventInfo(this, actionTarget, actionTarget, typeMaskActor, spellTypeMask, spellPhaseMask, hitMask, spell, damageInfo, healInfo); + if (typeMaskActor) + { + AuraApplicationProcContainer myAurasTriggeringProc; + GetProcAurasTriggeredOnEvent(myAurasTriggeringProc, nullptr, myProcEventInfo); + + // needed for example for Cobra Strikes, pet does the attack, but aura is on owner + if (Player* modOwner = GetSpellModOwner()) + { + if (modOwner != this && spell) + { + AuraApplicationList modAuras; + for (auto itr = modOwner->GetAppliedAuras().begin(); itr != modOwner->GetAppliedAuras().end(); ++itr) + { + if (spell->m_appliedMods.count(itr->second->GetBase()) != 0) + modAuras.push_front(itr->second); + } + modOwner->GetProcAurasTriggeredOnEvent(myAurasTriggeringProc, &modAuras, myProcEventInfo); + } + } + TriggerAurasProcOnEvent(myProcEventInfo, myAurasTriggeringProc); + } + + // prepare data for target trigger + ProcEventInfo targetProcEventInfo(this, actionTarget, this, typeMaskActionTarget, spellTypeMask, spellPhaseMask, hitMask, spell, damageInfo, healInfo); + if (typeMaskActionTarget && actionTarget) + { + AuraApplicationProcContainer targetAurasTriggeringProc; + actionTarget->GetProcAurasTriggeredOnEvent(targetAurasTriggeringProc, nullptr, targetProcEventInfo); + actionTarget->TriggerAurasProcOnEvent(targetProcEventInfo, targetAurasTriggeringProc); + } +} + +void Unit::TriggerAurasProcOnEvent(ProcEventInfo& eventInfo, AuraApplicationProcContainer& aurasTriggeringProc) +{ + Spell const* triggeringSpell = eventInfo.GetProcSpell(); + bool const disableProcs = triggeringSpell && triggeringSpell->IsProcDisabled(); + + int32 oldProcChainLength = std::exchange(m_procChainLength, std::max(m_procChainLength + 1, triggeringSpell ? triggeringSpell->GetProcChainLength() : 0)); + + if (disableProcs) + SetCantProc(true); + + for (auto const& [procEffectMask, aurApp] : aurasTriggeringProc) + { + if (aurApp->GetRemoveMode().HasAnyFlag()) + continue; + + aurApp->GetBase()->TriggerProcOnEvent(procEffectMask, aurApp, eventInfo); + } + + if (disableProcs) + SetCantProc(false); + + m_procChainLength = oldProcChainLength; +} + +///----------Pet responses methods----------------- +void Unit::SendPetActionFeedback(uint8 response, uint32 spellId /*=0*/) +{ + Unit* owner = GetOwner(); + if (!owner || owner->GetTypeId() != TYPEID_PLAYER) + return; + + WorldPackets::Pet::PetActionFeedback actionFeedback; + actionFeedback.Response = response; + actionFeedback.SpellID = spellId; + owner->ToPlayer()->SendDirectMessage(actionFeedback.Write()); +} + +void Unit::SendPetActionSound(int32 action) +{ + Unit* owner = GetOwner(); + if (!owner || owner->GetTypeId() != TYPEID_PLAYER) + return; + + WorldPackets::Pet::PetActionSound actionSound; + actionSound.UnitGUID = GetGUID(); + actionSound.Action = action; + owner->ToPlayer()->SendDirectMessage(actionSound.Write()); +} + +void Unit::SendPetAIReaction(ObjectGuid guid) +{ + Unit* owner = GetOwner(); + if (!owner || owner->GetTypeId() != TYPEID_PLAYER) + return; + + WorldPackets::Combat::AIReaction packet; + packet.UnitGUID = guid; + packet.Reaction = AI_REACTION_HOSTILE; + + owner->ToPlayer()->SendDirectMessage(packet.Write()); +} + +///----------End of Pet responses methods---------- + +void Unit::PropagateSpeedChange() +{ + GetMotionMaster()->PropagateSpeedChange(); +} + +void Unit::StopMoving() +{ + ClearUnitState(UNIT_STATE_MOVING); + + // not need send any packets if not in world or not moving + if (!IsInWorld() || movespline->Finalized()) + return; + + // Update position now since Stop does not start a new movement that can be updated later + if (movespline->HasStarted()) + UpdateSplinePosition(); + Movement::MoveSplineInit init(this); + init.Stop(); +} + +void Unit::PauseMovement(uint32 timer/* = 0*/, uint8 slot/* = 0*/, bool forced/* = true*/) +{ + if (IsInvalidMovementSlot(slot)) + return; + + if (MovementGenerator* movementGenerator = GetMotionMaster()->GetMotionSlot(MovementSlot(slot))) + movementGenerator->Pause(timer); + + if (forced && GetMotionMaster()->GetCurrentSlot() == MovementSlot(slot)) + StopMoving(); +} + +void Unit::ResumeMovement(uint32 timer/* = 0*/, uint8 slot/* = 0*/) +{ + if (IsInvalidMovementSlot(slot)) + return; + + if (MovementGenerator* movementGenerator = GetMotionMaster()->GetMotionSlot(MovementSlot(slot))) + movementGenerator->Resume(timer); +} + +bool Unit::IsSitState() const +{ + uint8 s = GetStandState(); + return + s == UNIT_STAND_STATE_SIT_CHAIR || s == UNIT_STAND_STATE_SIT_LOW_CHAIR || + s == UNIT_STAND_STATE_SIT_MEDIUM_CHAIR || s == UNIT_STAND_STATE_SIT_HIGH_CHAIR || + s == UNIT_STAND_STATE_SIT; +} + +bool Unit::IsStandState() const +{ + uint8 s = GetStandState(); + return !IsSitState() && s != UNIT_STAND_STATE_SLEEP && s != UNIT_STAND_STATE_KNEEL; +} + +void Unit::SetStandState(uint8 state) +{ + SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, state); + + if (IsStandState()) + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Standing); + + if (IsPlayer()) + { + WorldPackets::Misc::StandStateUpdate packet; + packet.State = state; + ToPlayer()->SendDirectMessage(packet.Write()); + } +} + +void Unit::SetAnimTier(AnimTier animTier, bool notifyClient /*= true*/) +{ + SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_ANIM_TIER, AsUnderlyingType(animTier)); + + if (notifyClient) + { + WorldPackets::Misc::SetAnimTier setAnimTier; + setAnimTier.Unit = GetGUID(); + setAnimTier.Tier = AsUnderlyingType(animTier); + SendMessageToSet(setAnimTier.Write(), true); + } +} + +bool Unit::IsPolymorphed() const +{ + uint32 transformId = getTransForm(); + if (!transformId) + return false; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(transformId); + if (!spellInfo) + return false; + + return spellInfo->GetSpellSpecific() == SPELL_SPECIFIC_MAGE_POLYMORPH; +} + +void Unit::RecalculateObjectScale() +{ + int32 scaleAuras = GetTotalAuraModifier(SPELL_AURA_MOD_SCALE) + GetTotalAuraModifier(SPELL_AURA_MOD_SCALE_2); + float scale = GetNativeObjectScale() + CalculatePct(1.0f, scaleAuras); + float scaleMin = GetTypeId() == TYPEID_PLAYER ? 0.1 : 0.01; + SetObjectScale(std::max(scale, scaleMin)); +} + +void Unit::SetDisplayId(uint32 modelId, bool setNative /*= false*/) +{ + SetUInt32Value(UNIT_FIELD_DISPLAYID, modelId); + // Set Gender by modelId + if (CreatureModelInfo const* minfo = sObjectMgr->GetCreatureModelInfo(modelId)) + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_GENDER, minfo->gender); + + if (setNative) + SetUInt32Value(UNIT_FIELD_NATIVEDISPLAYID, modelId); +} + +void Unit::RestoreDisplayId() +{ + AuraEffect* handledAura = nullptr; + // try to receive model from transform auras + AuraEffectList const& transforms = GetAuraEffectsByType(SPELL_AURA_TRANSFORM); + if (!transforms.empty()) + { + // iterate over already applied transform auras - from newest to oldest + for (auto i = transforms.begin(); i != transforms.end(); ++i) + { + if (AuraApplication const* aurApp = (*i)->GetBase()->GetApplicationOfTarget(GetGUID())) + { + if (!handledAura) + handledAura = (*i); + // prefer negative auras + if (!aurApp->IsPositive()) + { + handledAura = (*i); + break; + } + } + } + } + + AuraEffectList const& shapeshiftAura = GetAuraEffectsByType(SPELL_AURA_MOD_SHAPESHIFT); + + // transform aura was found + if (handledAura) + { + handledAura->HandleEffect(this, AURA_EFFECT_HANDLE_SEND_FOR_CLIENT, true); + return; + } + else if (!shapeshiftAura.empty()) // we've found shapeshift + { + // only one such aura possible at a time + if (uint32 modelId = GetModelForForm(GetShapeshiftForm(), shapeshiftAura.front()->GetId())) + { + SetDisplayId(modelId); + return; + } + } + + // no auras found - set modelid to default + SetDisplayId(GetNativeDisplayId()); +} + +void Unit::SendComboPoints() +{ + if (!IsPlayer()) + return; + + if (Unit* combotarget = ObjectAccessor::GetUnit(*this, m_comboTarget)) + { + WorldPacket data; + Player* movingMe = GetCharmerOrSelfPlayer(); + if (movingMe != this) + { + data.Initialize(SMSG_PET_UPDATE_COMBO_POINTS, movingMe->GetPackGUID().size() + combotarget->GetPackGUID().size() + 1); + data << movingMe->GetPackGUID(); + } + else + data.Initialize(SMSG_UPDATE_COMBO_POINTS, combotarget->GetPackGUID().size() + 1); + data << combotarget->GetPackGUID(); + data << uint8(m_comboPoints); + ToPlayer()->SendDirectMessage(&data); + } +} + +void Unit::AddComboPoints(Unit* target, int8 count, Spell* spell) +{ + if (!count) + return; + + int8* comboPoints = spell ? &spell->m_comboPointGain : &m_comboPoints; + + // without combo points lost (duration checked in aura) + RemoveAurasByType(SPELL_AURA_RETAIN_COMBO_POINTS); + + if (target->GetGUID() == m_comboTarget) + *comboPoints += count; + else + { + if (m_comboTarget) + if (Unit* target2 = ObjectAccessor::GetUnit(*this, m_comboTarget)) + target2->RemoveComboPointHolder(GetGUID()); + + // Spells will always add value to m_comboPoints eventualy, so it must be cleared first + if (spell) + m_comboPoints = 0; + + m_comboTarget = target->GetGUID(); + *comboPoints = count; + + target->AddComboPointHolder(GetGUID()); + } + + if (*comboPoints > 5) + *comboPoints = 5; + else if (*comboPoints < 0) + *comboPoints = 0; + + if (!spell) + SendComboPoints(); +} + +void Unit::GainSpellComboPoints(int8 count) +{ + if (!count) + return; + + m_comboPoints += count; + if (m_comboPoints > 5) m_comboPoints = 5; + else if (m_comboPoints < 0) m_comboPoints = 0; + + SendComboPoints(); +} + +void Unit::ClearComboPoints() +{ + if (!m_comboTarget) + return; + + // without combopoints lost (duration checked in aura) + RemoveAurasByType(SPELL_AURA_RETAIN_COMBO_POINTS); + + m_comboPoints = 0; + + SendComboPoints(); + + if (Unit* target = ObjectAccessor::GetUnit(*this, m_comboTarget)) + target->RemoveComboPointHolder(GetGUID()); + + m_comboTarget.Clear(); +} + +void Unit::ClearComboPointHolders() +{ + while (!m_ComboPointHolders.empty()) + { + ObjectGuid guid = *m_ComboPointHolders.begin(); + + Player* player = ObjectAccessor::GetPlayer(*this, guid); + if (player && player->GetComboTarget() == GetGUID()) // recheck for safe + player->ClearComboPoints(); // remove also guid from m_ComboPointHolders; + else + m_ComboPointHolders.erase(guid); // or remove manually + } +} + +void Unit::ClearAllReactives() +{ + for (uint8 i = 0; i < MAX_REACTIVE; ++i) + m_reactiveTimer[i] = 0; + + if (HasAuraState(AURA_STATE_DEFENSE)) + ModifyAuraState(AURA_STATE_DEFENSE, false); + if (getClass() == CLASS_HUNTER && HasAuraState(AURA_STATE_HUNTER_PARRY)) + ModifyAuraState(AURA_STATE_HUNTER_PARRY, false); + if (getClass() == CLASS_WARRIOR && GetTypeId() == TYPEID_PLAYER) + ToPlayer()->ClearComboPoints(); +} + +void Unit::UpdateReactives(uint32 p_time) +{ + for (uint8 i = 0; i < MAX_REACTIVE; ++i) + { + ReactiveType reactive = ReactiveType(i); + + if (!m_reactiveTimer[reactive]) + continue; + + if (m_reactiveTimer[reactive] <= p_time) + { + m_reactiveTimer[reactive] = 0; + + switch (reactive) + { + case REACTIVE_DEFENSE: + if (HasAuraState(AURA_STATE_DEFENSE)) + ModifyAuraState(AURA_STATE_DEFENSE, false); + break; + case REACTIVE_HUNTER_PARRY: + if (getClass() == CLASS_HUNTER && HasAuraState(AURA_STATE_HUNTER_PARRY)) + ModifyAuraState(AURA_STATE_HUNTER_PARRY, false); + break; + case REACTIVE_OVERPOWER: + if (getClass() == CLASS_WARRIOR && GetTypeId() == TYPEID_PLAYER) + ToPlayer()->ClearComboPoints(); + break; + } + } + else + { + m_reactiveTimer[reactive] -= p_time; + } + } +} + +Unit* Unit::SelectNearbyTarget(Unit* exclude, float dist) const +{ + std::list targets; + Trinity::AnyUnfriendlyUnitInObjectRangeCheck u_check(this, this, dist); + Trinity::UnitListSearcher searcher(this, targets, u_check); + Cell::VisitAllObjects(this, searcher, dist); + + // remove current target + if (GetVictim()) + targets.remove(GetVictim()); + + if (exclude) + targets.remove(exclude); + + // remove not LoS targets + for (std::list::iterator tIter = targets.begin(); tIter != targets.end();) + { + if (!IsWithinLOSInMap(*tIter) || (*tIter)->IsTotem() || (*tIter)->IsSpiritService() || (*tIter)->IsCritter()) + targets.erase(tIter++); + else + ++tIter; + } + + // no appropriate targets + if (targets.empty()) + return nullptr; + + // select random + return Trinity::Containers::SelectRandomContainerElement(targets); +} + +void ApplyPercentModFloatVar(float& var, float val, bool apply) +{ + var *= (apply ? (100.0f + val) / 100.0f : 100.0f / (100.0f + val)); +} + +uint32 Unit::GetBaseAttackTime(WeaponAttackType att) const +{ + return m_baseAttackSpeed[att]; +} + +void Unit::SetBaseAttackTime(WeaponAttackType att, uint32 val) +{ + m_baseAttackSpeed[att] = val; + UpdateAttackTimeField(att); +} + +void Unit::UpdateAttackTimeField(WeaponAttackType att) +{ + switch (att) + { + case BASE_ATTACK: + case OFF_ATTACK: + SetFloatValue(UNIT_FIELD_BASEATTACKTIME + AsUnderlyingType(att), uint32(m_baseAttackSpeed[att] * m_modAttackSpeedPct[att])); + break; + case RANGED_ATTACK: + SetFloatValue(UNIT_FIELD_RANGEDATTACKTIME, uint32(m_baseAttackSpeed[RANGED_ATTACK] * m_modAttackSpeedPct[RANGED_ATTACK])); + break; + default: + break; + } +} + +void Unit::ApplyAttackTimePercentMod(WeaponAttackType att, float val, bool apply) +{ + float amount = GetFloatValue(UNIT_FIELD_BASEATTACKTIME + AsUnderlyingType(att)); + float hasteAmount = GetTypeId() == TYPEID_PLAYER ? GetFloatValue(PLAYER_FIELD_MOD_HASTE) : 0.f; + float rangedHasteAmount = GetTypeId() == TYPEID_PLAYER ? GetFloatValue(PLAYER_FIELD_MOD_RANGED_HASTE) : 0.f; + + float remainingTimePct = float(m_attackTimer[att]) / (m_baseAttackSpeed[att] * m_modAttackSpeedPct[att]); + if (val > 0.f) + { + ApplyPercentModFloatVar(m_modAttackSpeedPct[att], val, !apply); + ApplyPercentModFloatVar(amount, val, !apply); + + if (GetTypeId() == TYPEID_PLAYER) + { + if (att == BASE_ATTACK) + ApplyPercentModFloatVar(hasteAmount, val, !apply); + else if (att == RANGED_ATTACK) + ApplyPercentModFloatVar(rangedHasteAmount, val, !apply); + } + } + else + { + ApplyPercentModFloatVar(m_modAttackSpeedPct[att], -val, apply); + ApplyPercentModFloatVar(amount, -val, apply); + + if (GetTypeId() == TYPEID_PLAYER) + { + if (att == BASE_ATTACK) + ApplyPercentModFloatVar(hasteAmount, -val, apply); + else if (att == RANGED_ATTACK) + ApplyPercentModFloatVar(rangedHasteAmount, -val, apply); + } + } + + SetFloatValue(UNIT_FIELD_BASEATTACKTIME + AsUnderlyingType(att), amount); + + if (GetTypeId() == TYPEID_PLAYER) + { + SetFloatValue(PLAYER_FIELD_MOD_HASTE, hasteAmount); + SetFloatValue(PLAYER_FIELD_MOD_RANGED_HASTE, rangedHasteAmount); + } + + UpdateAttackTimeField(att); + m_attackTimer[att] = uint32(m_baseAttackSpeed[att] * m_modAttackSpeedPct[att] * remainingTimePct); +} + +void Unit::ApplyHasteRegenMod(float val, bool apply) +{ + if (!IsPlayer()) + return; + + float amount = GetFloatValue(PLAYER_FIELD_MOD_HASTE_REGEN); + + if (val > 0.f) + ApplyPercentModFloatVar(amount, val, !apply); + else + ApplyPercentModFloatVar(amount, -val, apply); + + SetFloatValue(PLAYER_FIELD_MOD_HASTE_REGEN, amount); + + for (PowerType powerType : GetUsedPowerTypes()) + { + if (powerType == MAX_POWERS || GetPowerIndex(powerType) == MAX_POWERS) + continue; + + if (DBCManager::IsPowerTypeAffectedByHaste(powerType)) + UpdatePowerRegeneration(powerType); + } + + if (IsPlayer() && getClass() == CLASS_DEATH_KNIGHT) + UpdatePowerRegeneration(POWER_RUNE); +} + +void Unit::ApplyCastTimePercentMod(float val, bool apply, bool withCastHaste /*= true*/, bool withCastSpeed /*= true*/) +{ + float speedAmount = GetFloatValue(UNIT_MOD_CAST_SPEED); + float hasteAmount = GetFloatValue(UNIT_MOD_CAST_HASTE); + + if (val > 0.f) + { + if (withCastSpeed) + ApplyPercentModFloatVar(speedAmount, val, !apply); + if (withCastHaste) + ApplyPercentModFloatVar(hasteAmount, val, !apply); + } + else + { + if (withCastSpeed) + ApplyPercentModFloatVar(speedAmount, -val, apply); + if (withCastHaste) + ApplyPercentModFloatVar(hasteAmount, -val, apply); + } + + SetFloatValue(UNIT_MOD_CAST_SPEED, speedAmount); + SetFloatValue(UNIT_MOD_CAST_HASTE, hasteAmount); +} + +uint32 Unit::GetCastingTimeForBonus(SpellInfo const* spellProto, DamageEffectType damagetype, uint32 CastingTime) const +{ + // Not apply this to creature cast spells with casttime == 0 + if (CastingTime == 0 && GetTypeId() == TYPEID_UNIT && !IsPet()) + return 3500; + + if (CastingTime > 7000) CastingTime = 7000; + if (CastingTime < 1500) CastingTime = 1500; + + if (damagetype == DOT && !spellProto->IsChanneled()) + CastingTime = 3500; + + int32 overTime = 0; + uint8 effects = 0; + bool DirectDamage = false; + bool AreaEffect = false; + + for (uint32 i = 0; i < MAX_SPELL_EFFECTS; i++) + { + switch (spellProto->Effects[i].Effect) + { + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_POWER_BURN: + case SPELL_EFFECT_HEAL: + DirectDamage = true; + break; + case SPELL_EFFECT_APPLY_AURA: + switch (spellProto->Effects[i].ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_PERIODIC_LEECH: + if (spellProto->GetDuration()) + overTime = spellProto->GetDuration(); + break; + default: + // -5% per additional effect + ++effects; + break; + } + break; + default: + break; + } + + if (spellProto->Effects[i].IsTargetingArea()) + AreaEffect = true; + } + + // Combined Spells with Both Over Time and Direct Damage + if (overTime > 0 && CastingTime > 0 && DirectDamage) + { + // mainly for DoTs which are 3500 here otherwise + uint32 OriginalCastTime = spellProto->CalcCastTime(getLevel()); + if (OriginalCastTime > 7000) OriginalCastTime = 7000; + if (OriginalCastTime < 1500) OriginalCastTime = 1500; + // Portion to Over Time + float PtOT = (overTime / 15000.0f) / ((overTime / 15000.0f) + (OriginalCastTime / 3500.0f)); + + if (damagetype == DOT) + CastingTime = uint32(CastingTime * PtOT); + else if (PtOT < 1.0f) + CastingTime = uint32(CastingTime * (1 - PtOT)); + else + CastingTime = 0; + } + + // Area Effect Spells receive only half of bonus + if (AreaEffect) + CastingTime /= 2; + + // 50% for damage and healing spells for leech spells from damage bonus and 0% from healing + for (uint8 j = 0; j < MAX_SPELL_EFFECTS; ++j) + { + if (spellProto->Effects[j].Effect == SPELL_EFFECT_HEALTH_LEECH || + (spellProto->Effects[j].Effect == SPELL_EFFECT_APPLY_AURA && spellProto->Effects[j].ApplyAuraName == SPELL_AURA_PERIODIC_LEECH)) + { + CastingTime /= 2; + break; + } + } + + // -5% of total per any additional effect + for (uint8 i = 0; i < effects; ++i) + CastingTime *= 0.95f; + + return CastingTime; +} + +void Unit::UpdateAuraForGroup(uint8 slot) +{ + if (slot >= MAX_AURAS) // slot not found, return + return; + if (Player* player = ToPlayer()) + { + if (player->GetGroup()) + { + player->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_AURAS); + player->SetAuraUpdateMaskForRaid(slot); + } + } + else if (GetTypeId() == TYPEID_UNIT && IsPet()) + { + Pet* pet = ((Pet*)this); + if (pet->isControlled()) + { + Unit* owner = GetOwner(); + if (owner && (owner->GetTypeId() == TYPEID_PLAYER) && owner->ToPlayer()->GetGroup()) + { + owner->ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_AURAS); + pet->SetAuraUpdateMaskForRaid(slot); + } + } + } +} + +void Unit::SetCantProc(bool apply) +{ + if (apply) + ++m_procDeep; + else + { + ASSERT(m_procDeep); + --m_procDeep; + } +} + +float Unit::CalculateDefaultCoefficient(SpellInfo const* spellInfo, DamageEffectType damagetype) const +{ + // Damage over Time spells bonus calculation + float DotFactor = 1.0f; + if (damagetype == DOT) + { + + int32 DotDuration = spellInfo->GetDuration(); + if (!spellInfo->IsChanneled() && DotDuration > 0) + DotFactor = DotDuration / 15000.0f; + + if (uint32 DotTicks = spellInfo->GetMaxTicks()) + DotFactor /= DotTicks; + } + + int32 CastingTime = spellInfo->IsChanneled() ? spellInfo->GetDuration() : spellInfo->CalcCastTime(getLevel()); + // Distribute Damage over multiple effects, reduce by AoE + CastingTime = GetCastingTimeForBonus(spellInfo, damagetype, CastingTime); + + // As wowwiki says: C = (Cast Time / 3.5) + return (CastingTime / 3500.0f) * DotFactor; +} + +float Unit::GetAPMultiplier(WeaponAttackType attType, bool normalized) const +{ + if (GetTypeId() != TYPEID_PLAYER || (IsInFeralForm() && !normalized)) + return GetBaseAttackTime(attType) / 1000.0f; + + Item* weapon = ToPlayer()->GetWeaponForAttack(attType, true); + if (!weapon) + return 2.0f; + + if (!normalized) + return weapon->GetTemplate()->GetDelay() / 1000.0f; + + switch (weapon->GetTemplate()->GetSubClass()) + { + case ITEM_SUBCLASS_WEAPON_AXE2: + case ITEM_SUBCLASS_WEAPON_MACE2: + case ITEM_SUBCLASS_WEAPON_POLEARM: + case ITEM_SUBCLASS_WEAPON_SWORD2: + case ITEM_SUBCLASS_WEAPON_STAFF: + case ITEM_SUBCLASS_WEAPON_FISHING_POLE: + return 3.3f; + case ITEM_SUBCLASS_WEAPON_BOW: + case ITEM_SUBCLASS_WEAPON_GUN: + case ITEM_SUBCLASS_WEAPON_CROSSBOW: + case ITEM_SUBCLASS_WEAPON_THROWN: + return 2.8f; + case ITEM_SUBCLASS_WEAPON_AXE: + case ITEM_SUBCLASS_WEAPON_MACE: + case ITEM_SUBCLASS_WEAPON_SWORD: + case ITEM_SUBCLASS_WEAPON_EXOTIC: + case ITEM_SUBCLASS_WEAPON_EXOTIC2: + case ITEM_SUBCLASS_WEAPON_FIST_WEAPON: + return 2.4f; + case ITEM_SUBCLASS_WEAPON_DAGGER: + return 1.7f; + default: + return weapon->GetTemplate()->GetDelay() / 1000.0f; + } +} + +Pet* Unit::CreateTamedPetFrom(Creature* creatureTarget, uint32 spell_id) +{ + if (GetTypeId() != TYPEID_PLAYER) + return nullptr; + + Pet* pet = new Pet(ToPlayer(), HUNTER_PET); + + if (!pet->CreateBaseAtCreature(creatureTarget)) + { + delete pet; + return nullptr; + } + + uint8 level = getLevel(); + + InitTamedPet(pet, level, spell_id); + + return pet; +} + +Pet* Unit::CreateTamedPetFrom(uint32 creatureEntry, uint32 spell_id) +{ + if (GetTypeId() != TYPEID_PLAYER) + return nullptr; + + CreatureTemplate const* creatureInfo = sObjectMgr->GetCreatureTemplate(creatureEntry); + if (!creatureInfo) + return nullptr; + + Pet* pet = new Pet(ToPlayer(), HUNTER_PET); + + if (!pet->CreateBaseAtCreatureInfo(creatureInfo, this) || !InitTamedPet(pet, getLevel(), spell_id)) + { + delete pet; + return nullptr; + } + + return pet; +} + +bool Unit::InitTamedPet(Pet* pet, uint8 level, uint32 spell_id) +{ + pet->SetCreatorGUID(GetGUID()); + pet->SetFaction(GetFaction()); + pet->SetUInt32Value(UNIT_CREATED_BY_SPELL, spell_id); + + if (GetTypeId() == TYPEID_PLAYER) + pet->SetUInt32Value(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + + if (!pet->InitStatsForLevel(level)) + { + TC_LOG_ERROR("entities.unit", "Pet::InitStatsForLevel() failed for creature (Entry: %u)!", pet->GetEntry()); + return false; + } + + PhasingHandler::InheritPhaseShift(pet, this); + + pet->GetCharmInfo()->SetPetNumber(sObjectMgr->GeneratePetNumber(), true); + // this enables pet details window (Shift+P) + pet->InitPetCreateSpells(); + //pet->InitLevelupSpellsForLevel(); + pet->SetFullHealth(); + pet->CastSpell(pet, 99289, true); // Energize + pet->SetReactState(REACT_ASSIST); + return true; +} + +void Unit::SendDurabilityLoss(Player* receiver, uint32 percent) +{ + WorldPackets::Misc::DurabilityDamageDeath packet; + packet.Percent = percent; + receiver->SendDirectMessage(packet.Write()); +} + +void Unit::PlayOneShotAnimKitId(uint16 animKitId) +{ + WorldPackets::Misc::PlayOneShotAnimKit packet; + packet.Unit = GetGUID(); + packet.AnimKitID = animKitId; + SendMessageToSet(packet.Write(), true); +} + +/*static*/ void Unit::Kill(Unit* attacker, Unit* victim, bool durabilityLoss /*= true*/) +{ + // Prevent killing unit twice (and giving reward from kill twice) + if (!victim->GetHealth()) + return; + + if (attacker && !attacker->IsInMap(victim)) + attacker = nullptr; + + // find player: owner of controlled `this` or `this` itself maybe + Player* player = nullptr; + if (attacker) + player = attacker->GetCharmerOrOwnerPlayerOrPlayerItself(); + + Creature* creature = victim->ToCreature(); + + bool isRewardAllowed = true; + if (creature) + { + isRewardAllowed = creature->IsDamageEnoughForLootingAndReward(); + if (!isRewardAllowed) + creature->SetLootRecipient(nullptr); + } + + if (isRewardAllowed && creature) + { + if (Player* lootRecipient = creature->GetLootRecipient()) + { + // Loot recipient can be in a different map + if (!creature->IsInMap(lootRecipient)) + { + if (Group* group = creature->GetLootRecipientGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !creature->IsInMap(member)) + continue; + + player = member; + break; + } + } + } + else + player = creature->GetLootRecipient(); + } + } + + // Exploit fix + if (creature && creature->IsPet() && creature->GetOwnerOrCreatorGUID().IsPlayer()) + isRewardAllowed = false; + + // Reward player, his pets, and group/raid members + // call kill spell proc event (before real die and combat stop to triggering auras removed at death/combat stop) + if (isRewardAllowed && player && player != victim) + { + WorldPacket data(SMSG_PARTYKILLLOG, (8+8)); // send event PARTY_KILL + data << uint64(player->GetGUID()); // player with killing blow + data << uint64(victim->GetGUID()); // victim + + Player* looter = player; + Group* group = player->GetGroup(); + bool hasLooterGuid = false; + + if (group) + { + group->BroadcastPacket(&data, group->GetMemberGroup(player->GetGUID()) != 0); + + if (creature && !creature->IsLootDisabled()) + { + group->UpdateLooterGuid(creature, true); + if (group->GetLooterGuid()) + { + looter = ObjectAccessor::FindPlayer(group->GetLooterGuid()); + if (looter) + { + hasLooterGuid = true; + creature->SetLootRecipient(looter); // update creature loot recipient to the allowed looter. + } + } + } + } + else + { + player->SendDirectMessage(&data); + + if (creature && !creature->IsLootDisabled()) + { + WorldPackets::Loot::LootList lootList; + lootList.Owner = creature->GetGUID(); + lootList.RoundRobinWinner = player->GetGUID(); + + player->SendMessageToSet(lootList.Write(), false); + } + } + + // Generate loot before updating looter + if (creature && !creature->IsLootDisabled()) + { + Loot* loot = &creature->loot; + loot->clear(); + + if (uint32 lootid = creature->GetCreatureTemplate()->lootid) + loot->FillLoot(lootid, LootTemplates_Creature, looter, false, false, creature->GetLootMode()); + + if (creature->GetLootMode() > 0) + loot->generateMoneyLoot(creature->GetCreatureTemplate()->mingold, creature->GetCreatureTemplate()->maxgold); + + if (group) + { + if (hasLooterGuid) + group->SendLooter(creature, looter); + else + group->SendLooter(creature, nullptr); + + // Update round robin looter only if the creature had loot + if (!loot->empty()) + group->UpdateLooterGuid(creature); + } + } + + player->RewardPlayerAndGroupAtKill(victim, false); + } + + // Do KILL and KILLED procs. KILL proc is called only for the unit who landed the killing blow (and its owner - for pets and totems) regardless of who tapped the victim + if (attacker && (attacker->IsPet() || attacker->IsTotem())) + { + // proc only once for victim + if (Unit* owner = attacker->GetOwner()) + Unit::ProcSkillsAndAuras(owner, victim, PROC_FLAG_KILL, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); + } + + if (!victim->IsCritter()) + Unit::ProcSkillsAndAuras(attacker, victim, PROC_FLAG_KILL, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); + + // Proc auras on death - must be before aura/combat remove + Unit::ProcSkillsAndAuras(victim, victim, PROC_FLAG_NONE, PROC_FLAG_DEATH, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, nullptr, nullptr, nullptr); + + // update get killing blow achievements, must be done before setDeathState to be able to require auras on target + // and before Spirit of Redemption as it also removes auras + if (attacker) + if (Player* killerPlayer = attacker->GetCharmerOrOwnerPlayerOrPlayerItself()) + killerPlayer->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_GET_KILLING_BLOWS, 1, 0, 0, victim); + + TC_LOG_DEBUG("entities.unit", "SET JUST_DIED"); + victim->setDeathState(JUST_DIED); + + // Inform pets (if any) when player kills target) + // MUST come after victim->setDeathState(JUST_DIED); or pet next target + // selection will get stuck on same target and break pet react state + if (player) + { + Pet* pet = player->GetPet(); + if (pet && pet->IsAlive() && pet->isControlled()) + { + if (pet->IsAIEnabled()) + pet->AI()->KilledUnit(victim); + else + TC_LOG_ERROR("entities.unit", "Pet doesn't have any AI in Unit::Kill(). %s", pet->GetGUID().ToString().c_str()); + } + } + + // 10% durability loss on death + if (Player* plrVictim = victim->ToPlayer()) + { + // remember victim PvP death for corpse type and corpse reclaim delay + // at original death (not at SpiritOfRedemtionTalent timeout) + plrVictim->SetPvPDeath(player != nullptr); + + // only if not player and not controlled by player pet. And not at BG + if ((durabilityLoss && !player && !victim->ToPlayer()->InBattleground()) || (player && sWorld->getBoolConfig(CONFIG_DURABILITY_LOSS_IN_PVP))) + { + double baseLoss = sWorld->getRate(RATE_DURABILITY_LOSS_ON_DEATH); + uint32 loss = uint32(baseLoss - (baseLoss * plrVictim->GetTotalAuraMultiplier(SPELL_AURA_MOD_DURABILITY_LOSS))); + TC_LOG_DEBUG("entities.unit", "We are dead, losing %u percent durability", loss); + // Durability loss is calculated more accurately again for each item in Player::DurabilityLoss + plrVictim->DurabilityLossAll(baseLoss, false); + // durability lost message + plrVictim->SendDurabilityLoss(plrVictim, loss); + } + // Call KilledUnit for creatures + if (attacker && attacker->IsCreature() && attacker->IsAIEnabled()) + attacker->ToCreature()->AI()->KilledUnit(victim); + + // last damage from non duel opponent or opponent controlled creature + if (plrVictim->duel) + { + plrVictim->duel->opponent->CombatStopWithPets(true); + plrVictim->CombatStopWithPets(true); + plrVictim->DuelComplete(DUEL_INTERRUPTED); + } + } + else // creature died + { + TC_LOG_DEBUG("entities.unit", "DealDamageNotPlayer"); + + if (creature && !creature->IsPet()) + { + // must be after setDeathState which resets dynamic flags + if (!creature->loot.isLooted()) + creature->SetFlag(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_LOOTABLE); + else + creature->AllLootRemovedFromCorpse(); + } + + // Call KilledUnit for creatures, this needs to be called after the lootable flag is set + if (attacker && attacker->IsCreature() && attacker->IsAIEnabled()) + attacker->ToCreature()->AI()->KilledUnit(victim); + + // Call creature just died function + if (CreatureAI* ai = creature->AI()) + ai->JustDied(attacker); + + if (TempSummon* summon = creature->ToTempSummon()) + if (Unit* summoner = summon->GetSummoner()) + if (summoner->ToCreature() && summoner->IsAIEnabled()) + summoner->ToCreature()->AI()->SummonedCreatureDies(creature, attacker); + + // Dungeon specific stuff, only applies to players killing creatures + if (creature->GetInstanceId()) + { + Map* instanceMap = creature->GetMap(); + /// @todo do instance binding anyway if the charmer/owner is offline + + if (instanceMap->IsDungeon() && ((attacker && attacker->GetCharmerOrOwnerPlayerOrPlayerItself()) || attacker == victim)) + { + if (instanceMap->IsRaidOrHeroicDungeon()) + { + if (creature->GetCreatureTemplate()->flags_extra & CREATURE_FLAG_EXTRA_INSTANCE_BIND) + instanceMap->ToInstanceMap()->PermBindAllPlayers(); + } + else + { + // the reset time is set but not added to the scheduler + // until the players leave the instance + time_t resettime = GameTime::GetGameTime() + 2 * HOUR; + if (InstanceSave* save = sInstanceSaveMgr->GetInstanceSave(creature->GetInstanceId())) + if (save->GetResetTime() < resettime) + save->SetResetTime(resettime); + } + } + } + } + + // outdoor pvp things, do these after setting the death state, else the player activity notify won't work... doh... + // handle player kill only if not suicide (spirit of redemption for example) + if (player && attacker != victim) + { + if (OutdoorPvP* pvp = player->GetOutdoorPvP()) + pvp->HandleKill(player, victim); + + if (Battlefield* bf = sBattlefieldMgr->GetBattlefieldToZoneId(player->GetMap(), player->GetZoneId())) + bf->HandleKill(player, victim); + } + + //if (victim->GetTypeId() == TYPEID_PLAYER) + // if (OutdoorPvP* pvp = victim->ToPlayer()->GetOutdoorPvP()) + // pvp->HandlePlayerActivityChangedpVictim->ToPlayer(); + + // battleground things (do this at the end, so the death state flag will be properly set to handle in the bg->handlekill) + if (player && player->InBattleground()) + { + if (Battleground* bg = player->GetBattleground()) + { + if (Player* playerVictim = victim->ToPlayer()) + bg->HandleKillPlayer(playerVictim, player); + else + bg->HandleKillUnit(victim->ToCreature(), player); + } + } + + // achievement stuff + if (attacker && victim->GetTypeId() == TYPEID_PLAYER) + { + if (attacker->GetTypeId() == TYPEID_UNIT) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_CREATURE, attacker->GetEntry()); + else if (attacker->GetTypeId() == TYPEID_PLAYER && victim != attacker) + victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_PLAYER, 1, attacker->ToPlayer()->GetTeam()); + } + + // Hook for OnPVPKill Event + if (attacker) + { + if (Player* killerPlr = attacker->ToPlayer()) + { + if (Player* killedPlr = victim->ToPlayer()) + sScriptMgr->OnPVPKill(killerPlr, killedPlr); + else if (Creature* killedCre = victim->ToCreature()) + sScriptMgr->OnCreatureKill(killerPlr, killedCre); + } + else if (Creature* killerCre = attacker->ToCreature()) + { + if (Player* killed = victim->ToPlayer()) + sScriptMgr->OnPlayerKilledByCreature(killerCre, killed); + } + } +} + +void Unit::SetControlled(bool apply, UnitState state) +{ + if (apply) + { + if (HasUnitState(state)) + return; + + if (state & UNIT_STATE_CONTROLLED) + CastStop(); + + AddUnitState(state); + switch (state) + { + case UNIT_STATE_STUNNED: + SetStunned(true); + break; + case UNIT_STATE_ROOT: + if (!HasUnitState(UNIT_STATE_STUNNED)) + SetRooted(true); + break; + case UNIT_STATE_CONFUSED: + if (!HasUnitState(UNIT_STATE_STUNNED)) + { + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + SendMeleeAttackStop(); + // SendAutoRepeatCancel ? + SetConfused(true); + } + break; + case UNIT_STATE_FLEEING: + if (!HasUnitState(UNIT_STATE_STUNNED | UNIT_STATE_CONFUSED)) + { + ClearUnitState(UNIT_STATE_MELEE_ATTACKING); + SendMeleeAttackStop(); + // SendAutoRepeatCancel ? + SetFeared(true); + } + break; + default: + break; + } + } + else + { + switch (state) + { + case UNIT_STATE_STUNNED: + if (HasAuraType(SPELL_AURA_MOD_STUN)) + return; + + ClearUnitState(state); + SetStunned(false); + break; + case UNIT_STATE_ROOT: + if (HasAuraType(SPELL_AURA_MOD_ROOT) || GetVehicle() || (IsCreature() && ToCreature()->IsSessile())) + return; + + ClearUnitState(state); + SetRooted(false); + break; + case UNIT_STATE_CONFUSED: + if (HasAuraType(SPELL_AURA_MOD_CONFUSE)) + return; + + ClearUnitState(state); + SetConfused(false); + break; + case UNIT_STATE_FLEEING: + if (HasAuraType(SPELL_AURA_MOD_FEAR)) + return; + + ClearUnitState(state); + SetFeared(false); + break; + default: + return; + } + + ApplyControlStatesIfNeeded(); + } +} + +void Unit::ApplyControlStatesIfNeeded() +{ + // Unit States might have been already cleared but auras still present. I need to check with HasAuraType + if (HasUnitState(UNIT_STATE_STUNNED) || HasAuraType(SPELL_AURA_MOD_STUN)) + SetStunned(true); + + if (HasUnitState(UNIT_STATE_ROOT) || HasAuraType(SPELL_AURA_MOD_ROOT)) + SetRooted(true); + + if (HasUnitState(UNIT_STATE_CONFUSED) || HasAuraType(SPELL_AURA_MOD_CONFUSE)) + SetConfused(true); + + if (HasUnitState(UNIT_STATE_FLEEING) || HasAuraType(SPELL_AURA_MOD_FEAR)) + SetFeared(true); +} + +void Unit::SetStunned(bool apply) +{ + if (apply) + { + SetTarget(ObjectGuid::Empty); + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + + // Do not stop movement when the unit is affected by parabolic spline movement (knockbacks, pulls, scripted leaps etc.) + if (!movespline->isParabolic()) + StopMoving(); + + if (GetTypeId() == TYPEID_PLAYER) + SetStandState(UNIT_STAND_STATE_STAND); + SetRooted(true); + + CastStop(); + } + else + { + if (IsAlive() && GetVictim()) + SetTarget(EnsureVictim()->GetGUID()); + + // don't remove UNIT_FLAG_STUNNED for pet when owner is mounted (disabled pet's interface) + Unit* owner = GetCharmerOrOwner(); + if (!owner || owner->GetTypeId() != TYPEID_PLAYER || !owner->ToPlayer()->IsMounted()) + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_STUNNED); + + if (!HasUnitState(UNIT_STATE_ROOT)) // prevent moving if it also has root effect + SetRooted(false); + } +} + +void Unit::SetRooted(bool apply, bool packetOnly /*= false*/) +{ + if (!packetOnly) + { + if (apply) + { + // MOVEMENTFLAG_ROOT cannot be used in conjunction with MOVEMENTFLAG_MASK_MOVING (tested 3.3.5a) + // this will freeze clients. That's why we remove MOVEMENTFLAG_MASK_MOVING before + // setting MOVEMENTFLAG_ROOT + RemoveUnitMovementFlag(MOVEMENTFLAG_MASK_MOVING); + AddUnitMovementFlag(MOVEMENTFLAG_ROOT); + + // Do not stop movement when the unit is affected by parabolic spline movement (knockbacks, pulls, scripted leaps etc.) + if (!movespline->isParabolic()) + StopMoving(); + } + else + RemoveUnitMovementFlag(MOVEMENTFLAG_ROOT); + } + + if (apply) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_ROOT, SMSG_MOVE_ROOT, SMSG_MOVE_ROOT).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_UNROOT, SMSG_MOVE_UNROOT, SMSG_MOVE_UNROOT).Send(); +} + +void Unit::SetFeared(bool apply) +{ + if (apply) + { + // block control to real player in control (eg charmer) + if (GetCharmerOrSelfPlayer()) + GetCharmerOrSelfPlayer()->SetClientControl(this, false); + + SetTarget(ObjectGuid::Empty); + + Unit* caster = nullptr; + Unit::AuraEffectList const& fearAuras = GetAuraEffectsByType(SPELL_AURA_MOD_FEAR); + if (!fearAuras.empty()) + caster = ObjectAccessor::GetUnit(*this, fearAuras.front()->GetCasterGUID()); + if (!caster) + caster = getAttackerForHelper(); + GetMotionMaster()->MoveFleeing(caster, fearAuras.empty() ? sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_FLEE_DELAY) : 0); // caster == nullptr processed in MoveFleeing + } + else + { + if (IsAlive()) + { + if (GetMotionMaster()->GetCurrentMovementGeneratorType() == FLEEING_MOTION_TYPE) + GetMotionMaster()->MovementExpired(); + if (GetVictim()) + SetTarget(EnsureVictim()->GetGUID()); + } + + // allow control to real player in control (eg charmer) + if (GetCharmerOrSelfPlayer()) + GetCharmerOrSelfPlayer()->SetClientControl(this, true); + } +} + +void Unit::SetConfused(bool apply) +{ + if (apply) + { + // block control to real player in control (eg charmer) + if (GetCharmerOrSelfPlayer()) + GetCharmerOrSelfPlayer()->SetClientControl(this, false); + + SetTarget(ObjectGuid::Empty); + GetMotionMaster()->MoveConfused(); + } + else + { + if (IsAlive()) + { + if (GetMotionMaster()->GetCurrentMovementGeneratorType() == CONFUSED_MOTION_TYPE) + GetMotionMaster()->MovementExpired(); + if (GetVictim()) + SetTarget(EnsureVictim()->GetGUID()); + } + + // allow control to real player in control (eg charmer) + if (GetCharmerOrSelfPlayer()) + GetCharmerOrSelfPlayer()->SetClientControl(this, true); + } +} + +bool Unit::SetCharmedBy(Unit* charmer, CharmType type, AuraApplication const* aurApp) +{ + if (!charmer) + return false; + + // dismount players when charmed + if (GetTypeId() == TYPEID_PLAYER) + RemoveAurasByType(SPELL_AURA_MOUNTED); + + if (charmer->GetTypeId() == TYPEID_PLAYER) + charmer->RemoveAurasByType(SPELL_AURA_MOUNTED); + + ASSERT(type != CHARM_TYPE_POSSESS || charmer->GetTypeId() == TYPEID_PLAYER); + ASSERT((type == CHARM_TYPE_VEHICLE) == (GetVehicleKit() && GetVehicleKit()->IsControllableVehicle())); + + TC_LOG_DEBUG("entities.unit", "SetCharmedBy: charmer %u (GUID %u), charmed %u (GUID %u), type %u.", charmer->GetEntry(), charmer->GetGUID().GetCounter(), GetEntry(), GetGUID().GetCounter(), uint32(type)); + + if (this == charmer) + { + TC_LOG_FATAL("entities.unit", "Unit::SetCharmedBy: Unit %u (GUID %u) is trying to charm itself!", GetEntry(), GetGUID().GetCounter()); + return false; + } + + //if (HasUnitState(UNIT_STATE_UNATTACKABLE)) + // return false; + + if (GetTypeId() == TYPEID_PLAYER && ToPlayer()->GetTransport()) + { + TC_LOG_FATAL("entities.unit", "Unit::SetCharmedBy: Player on transport is trying to charm %u (GUID %u)", GetEntry(), GetGUID().GetCounter()); + return false; + } + + // Already charmed + if (GetCharmerGUID()) + { + TC_LOG_FATAL("entities.unit", "Unit::SetCharmedBy: %u (GUID %u) has already been charmed but %u (GUID %u) is trying to charm it!", GetEntry(), GetGUID().GetCounter(), charmer->GetEntry(), charmer->GetGUID().GetCounter()); + return false; + } + + CastStop(); + CombatStop(); /// @todo CombatStop(true) may cause crash (interrupt spells) + + Player* playerCharmer = charmer->ToPlayer(); + + // Charmer stop charming + if (playerCharmer) + { + playerCharmer->StopCastingCharm(); + playerCharmer->StopCastingBindSight(); + } + + // Charmed stop charming + if (GetTypeId() == TYPEID_PLAYER) + { + ToPlayer()->StopCastingCharm(); + ToPlayer()->StopCastingBindSight(); + } + + // StopCastingCharm may remove a possessed pet? + if (!IsInWorld()) + { + TC_LOG_FATAL("entities.unit", "Unit::SetCharmedBy: %u (GUID %u) is not in world but %u (GUID %u) is trying to charm it!", GetEntry(), GetGUID().GetCounter(), charmer->GetEntry(), charmer->GetGUID().GetCounter()); + return false; + } + + // charm is set by aura, and aura effect remove handler was called during apply handler execution + // prevent undefined behaviour + if (aurApp && aurApp->GetRemoveMode().HasAnyFlag()) + { + // properly clean up charm changes up to this point to avoid leaving the unit in partially charmed state + SetFaction(_oldFactionId); + GetMotionMaster()->InitDefault(); + charmer->SetCharm(this, false); + return false; + } + + _oldFactionId = GetFaction(); + SetFaction(charmer->GetFaction()); + + // Set charmed + charmer->SetCharm(this, true); + + if (GetTypeId() == TYPEID_UNIT) + { + GetMotionMaster()->Clear(MOTION_SLOT_ACTIVE); + PauseMovement(0, MOTION_SLOT_IDLE); + StopMoving(); + } + else if (Player* player = ToPlayer()) + { + if (player->isAFK()) + player->ToggleAFK(); + + player->SetClientControl(this, false); + } + + // charm is set by aura, and aura effect remove handler was called during apply handler execution + // prevent undefined behaviour + if (aurApp && aurApp->GetRemoveMode().HasAnyFlag()) + return false; + + // Pets already have a properly initialized CharmInfo, don't overwrite it. + if (type != CHARM_TYPE_VEHICLE && !GetCharmInfo()) + { + if (CharmInfo* charmInfo = InitCharmInfo()) + { + if (type == CHARM_TYPE_POSSESS) + charmInfo->InitPossessCreateSpells(); + else + charmInfo->InitCharmCreateSpells(); + } + } + + if (playerCharmer) + { + switch (type) + { + case CHARM_TYPE_VEHICLE: + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED); + playerCharmer->SetClientControl(this, true); + playerCharmer->VehicleSpellInitialize(); + break; + case CHARM_TYPE_POSSESS: + SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED); + charmer->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_REMOVE_CLIENT_CONTROL); + playerCharmer->SetClientControl(this, true); + playerCharmer->PossessSpellInitialize(); + AddUnitState(UNIT_STATE_POSSESSED); + break; + case CHARM_TYPE_CHARM: + if (GetTypeId() == TYPEID_UNIT && charmer->getClass() == CLASS_WARLOCK) + { + CreatureTemplate const* cinfo = ToCreature()->GetCreatureTemplate(); + if (cinfo && cinfo->type == CREATURE_TYPE_DEMON) + { + // to prevent client crash + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_CLASS, (uint8)CLASS_MAGE); + + // just to enable stat window + if (GetCharmInfo()) + GetCharmInfo()->SetPetNumber(sObjectMgr->GeneratePetNumber(), true); + + // if charmed two demons the same session, the 2nd gets the 1st one's name + SetUInt32Value(UNIT_FIELD_PET_NAME_TIMESTAMP, uint32(GameTime::GetGameTime())); // cast can't be helped + } + } + playerCharmer->CharmSpellInitialize(); + break; + default: + case CHARM_TYPE_CONVERT: + break; + } + } + + AddUnitState(UNIT_STATE_CHARMED); + + if ((GetTypeId() != TYPEID_PLAYER) || (charmer->GetTypeId() != TYPEID_PLAYER)) + { + // AI will schedule its own change if appropriate + if (UnitAI* ai = GetAI()) + ai->OnCharmed(false); + else + ScheduleAIChange(); + } + return true; +} + +void Unit::RemoveCharmedBy(Unit* charmer) +{ + if (!IsCharmed()) + return; + + if (charmer) + ASSERT(charmer == GetCharmer()); + else + charmer = GetCharmer(); + + ASSERT(charmer); + + CharmType type; + if (HasUnitState(UNIT_STATE_POSSESSED)) + type = CHARM_TYPE_POSSESS; + else if (charmer && charmer->IsOnVehicle(this)) + type = CHARM_TYPE_VEHICLE; + else + type = CHARM_TYPE_CHARM; + + CastStop(); + AttackStop(); + + if (_oldFactionId) + { + SetFaction(_oldFactionId); + _oldFactionId = 0; + } + else + RestoreFaction(); + + GetMotionMaster()->InitDefault(); + + // Vehicle should not attack its passenger after he exists the seat + if (type != CHARM_TYPE_VEHICLE) + LastCharmerGUID = charmer->GetGUID(); + + ASSERT(type != CHARM_TYPE_POSSESS || charmer->GetTypeId() == TYPEID_PLAYER); + ASSERT(type != CHARM_TYPE_VEHICLE || (GetTypeId() == TYPEID_UNIT && IsVehicle())); + + charmer->SetCharm(this, false); + m_combatManager.RevalidateCombat(); + + Player* playerCharmer = charmer->ToPlayer(); + + if (playerCharmer) + { + switch (type) + { + case CHARM_TYPE_VEHICLE: + playerCharmer->SetClientControl(this, false); + playerCharmer->SetClientControl(charmer, true); + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED); + break; + case CHARM_TYPE_POSSESS: + ClearUnitState(UNIT_STATE_POSSESSED); + playerCharmer->SetClientControl(this, false); + playerCharmer->SetClientControl(charmer, true); + charmer->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_REMOVE_CLIENT_CONTROL); + RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_POSSESSED); + break; + case CHARM_TYPE_CHARM: + if (GetTypeId() == TYPEID_UNIT && charmer->getClass() == CLASS_WARLOCK) + { + CreatureTemplate const* cinfo = ToCreature()->GetCreatureTemplate(); + if (cinfo && cinfo->type == CREATURE_TYPE_DEMON) + { + SetByteValue(UNIT_FIELD_BYTES_0, UNIT_BYTES_0_OFFSET_CLASS, uint8(cinfo->unit_class)); + if (GetCharmInfo()) + GetCharmInfo()->SetPetNumber(0, true); + else + TC_LOG_ERROR("entities.unit", "Aura::HandleModCharm: %s has a charm aura but no charm info!", GetGUID().ToString().c_str()); + } + } + break; + case CHARM_TYPE_CONVERT: + break; + } + } + + if (Player* player = ToPlayer()) + player->SetClientControl(this, true); + + EngageWithTarget(charmer); + + if (playerCharmer && this != charmer->GetFirstControlled()) + playerCharmer->SendRemoveControlBar(); + + // a guardian should always have charminfo + if (!IsGuardian()) + DeleteCharmInfo(); + + // reset confused movement for example + ApplyControlStatesIfNeeded(); + + if (GetTypeId() != TYPEID_PLAYER || charmer->GetTypeId() == TYPEID_UNIT) + { + if (UnitAI* charmedAI = GetAI()) + charmedAI->OnCharmed(false); // AI will potentially schedule a charm ai update + else + ScheduleAIChange(); + } +} + +void Unit::RestoreFaction() +{ + if (HasAuraType(SPELL_AURA_MOD_FACTION)) + { + SetFaction(GetAuraEffectsByType(SPELL_AURA_MOD_FACTION).front()->GetMiscValue()); + return; + } + + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->SetFactionForRace(getRace()); + else + { + if (HasUnitTypeMask(UNIT_MASK_MINION)) + { + if (Unit* owner = GetOwner()) + { + SetFaction(owner->GetFaction()); + return; + } + } + + if (CreatureTemplate const* cinfo = ToCreature()->GetCreatureTemplate()) // normal creature + SetFaction(cinfo->faction); + } +} + +bool Unit::CreateVehicleKit(uint32 id, uint32 creatureEntry, bool loading /*= false*/) +{ + VehicleEntry const* vehInfo = sVehicleStore.LookupEntry(id); + if (!vehInfo) + return false; + + m_vehicleKit = std::make_unique(this, vehInfo, creatureEntry); + m_updateFlag.Vehicle = true; + m_unitTypeMask |= UNIT_MASK_VEHICLE; + + if (!loading) + SendSetVehicleRecId(id); + + return true; +} + +void Unit::RemoveVehicleKit(bool onRemoveFromWorld /*= false*/) +{ + if (!m_vehicleKit) + return; + + if (!onRemoveFromWorld) + SendSetVehicleRecId(0); + + m_vehicleKit->Uninstall(); + m_vehicleKit.reset(); + + m_updateFlag.Vehicle = false; + m_unitTypeMask &= ~UNIT_MASK_VEHICLE; + RemoveFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_SPELLCLICK | UNIT_NPC_FLAG_PLAYER_VEHICLE); +} + +bool Unit::IsOnVehicle(Unit const* vehicle) const +{ + return m_vehicle && m_vehicle == vehicle->GetVehicleKit(); +} + +Unit* Unit::GetVehicleBase() const +{ + return m_vehicle ? m_vehicle->GetBase() : nullptr; +} + +Creature* Unit::GetVehicleCreatureBase() const +{ + if (Unit* veh = GetVehicleBase()) + if (Creature* c = veh->ToCreature()) + return c; + + return nullptr; +} + +ObjectGuid Unit::GetTransGUID() const +{ + if (GetVehicle()) + return GetVehicleBase()->GetGUID(); + if (GetTransport()) + return GetTransport()->GetTransportGUID(); + + return ObjectGuid::Empty; +} + +TransportBase* Unit::GetDirectTransport() const +{ + if (Vehicle* veh = GetVehicle()) + return veh; + 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& units) + { + if (!source || !groupOwner || !source->IsInWorld()) + return; + + float const range = source->GetVisibilityRange(); + std::list nearby; + VianoGroupBotInRangeCheck check(source, groupOwner, range); + Trinity::UnitListSearcher 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 units; + const_cast(this)->GetPartyMembers(units); + + uint32 count = 0; + std::vector 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) + return true; + + Unit const* u1 = GetCharmerOrOwnerOrCreatorOrSelf(); + Unit const* u2 = unit->GetCharmerOrOwnerOrCreatorOrSelf(); + if (u1 == u2) + return true; + + if (u1->GetTypeId() == TYPEID_PLAYER && u2->GetTypeId() == TYPEID_PLAYER) + return u1->ToPlayer()->IsInSameGroupWith(u2->ToPlayer()); + else if ((u2->GetTypeId() == TYPEID_PLAYER && u1->GetTypeId() == TYPEID_UNIT && u1->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_TREAT_AS_RAID_UNIT_FOR_HELPFUL_SPELLS)) || + (u1->GetTypeId() == TYPEID_PLAYER && u2->GetTypeId() == TYPEID_UNIT && u2->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_TREAT_AS_RAID_UNIT_FOR_HELPFUL_SPELLS))) + return true; + + return u1->GetTypeId() == TYPEID_UNIT && u2->GetTypeId() == TYPEID_UNIT && u1->GetFaction() == u2->GetFaction(); +} + +bool Unit::IsInRaidWith(Unit const* unit) const +{ + if (this == unit) + return true; + + Unit const* u1 = GetCharmerOrOwnerOrCreatorOrSelf(); + Unit const* u2 = unit->GetCharmerOrOwnerOrCreatorOrSelf(); + if (u1 == u2) + return true; + + if (u1->GetTypeId() == TYPEID_PLAYER && u2->GetTypeId() == TYPEID_PLAYER) + return u1->ToPlayer()->IsInSameRaidWith(u2->ToPlayer()); + else if ((u2->GetTypeId() == TYPEID_PLAYER && u1->GetTypeId() == TYPEID_UNIT && u1->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_TREAT_AS_RAID_UNIT_FOR_HELPFUL_SPELLS)) || + (u1->GetTypeId() == TYPEID_PLAYER && u2->GetTypeId() == TYPEID_UNIT && u2->ToCreature()->HasStaticFlag(CREATURE_STATIC_FLAG_4_TREAT_AS_RAID_UNIT_FOR_HELPFUL_SPELLS))) + return true; + + return u1->GetTypeId() == TYPEID_UNIT && u2->GetTypeId() == TYPEID_UNIT && u1->GetFaction() == u2->GetFaction(); +} + +void Unit::GetPartyMembers(std::list &TagUnitMap) +{ + Unit* owner = GetCharmerOrOwnerOrSelf(); + Group* group = nullptr; + Player* groupOwner = nullptr; + + if (owner->GetTypeId() == TYPEID_PLAYER) + { + groupOwner = owner->ToPlayer(); + group = groupOwner->GetGroup(); + } + + if (group) + { + uint8 subgroup = groupOwner->GetSubGroup(); + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* Target = itr->GetSource(); + + if (Target && Target->IsInMap(owner) && Target->GetSubGroup() == subgroup && !IsHostileTo(Target)) + { + if (Target->IsAlive()) + TagUnitMap.push_back(Target); + + if (Guardian* pet = Target->GetGuardianPet()) + if (pet->IsAlive()) + TagUnitMap.push_back(pet); + } + } + } + else + { + if ((owner == this || IsInMap(owner)) && owner->IsAlive()) + TagUnitMap.push_back(owner); + if (Guardian* pet = owner->GetGuardianPet()) + if ((pet == this || IsInMap(pet)) && pet->IsAlive()) + TagUnitMap.push_back(pet); + } + + if (groupOwner) + AppendNearbyVianoGroupBots(this, groupOwner, TagUnitMap); +} + +bool Unit::IsContestedGuard() const +{ + if (FactionTemplateEntry const* entry = GetFactionTemplateEntry()) + return entry->IsHostileToPvpActivePlayers(); + + return false; +} + +void Unit::SetPvP(bool state) +{ + if (state) + SetByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_PVP); + else + RemoveByteFlag(UNIT_FIELD_BYTES_2, UNIT_BYTES_2_OFFSET_PVP_FLAG, UNIT_BYTE2_FLAG_PVP); +} + +Aura* Unit::AddAura(uint32 spellId, Unit* target) +{ + if (!target) + return nullptr; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + return nullptr; + + return AddAura(spellInfo, MAX_EFFECT_MASK, target); +} + +Aura* Unit::AddAura(SpellInfo const* spellInfo, uint8 effMask, Unit* target) +{ + if (!spellInfo) + return nullptr; + + if (!target->IsAlive() && !spellInfo->IsPassive() && !spellInfo->HasAttribute(SPELL_ATTR2_ALLOW_DEAD_TARGET)) + return nullptr; + + if (target->IsImmunedToSpell(spellInfo, this)) + return nullptr; + + for (uint32 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!(effMask & (1 << i))) + continue; + + if (target->IsImmunedToSpellEffect(spellInfo, i, this)) + effMask &= ~(1 << i); + } + + if (!effMask) + return nullptr; + + AuraCreateInfo createInfo(spellInfo, effMask, target); + createInfo.SetCaster(this); + + if (Aura* aura = Aura::TryRefreshStackOrCreate(createInfo)) + { + aura->ApplyForTargets(); + return aura; + } + return nullptr; +} + +void Unit::SetAuraStack(uint32 spellId, Unit* target, uint32 stack) +{ + Aura* aura = target->GetAura(spellId, GetGUID()); + if (!aura) + aura = AddAura(spellId, target); + if (aura && stack) + aura->SetStackAmount(stack); +} + +void Unit::SendPlaySpellVisualKit(uint32 id, uint32 type, uint32 duration) const +{ + if (!sSpellVisualKitStore.LookupEntry(id)) + return; + + WorldPackets::Spells::PlaySpellVisualKit packet; + packet.Unit = GetGUID(); + packet.KitRecID = id; // SpellVisualKit.dbc index + packet.KitType = type; + packet.Duration = duration; + SendMessageToSet(packet.Write(), IsPlayer()); +} + +void Unit::SendPlaySpellVisual(uint32 spellVisualId, Unit const* target /*= nullptr*/, Optional targetPosition /*= {}*/, float travelSpeed /*= 0.f*/, uint16 missReason /*= 0*/, uint16 reflectStatus /*= 0*/, bool speedAsTime /*= false*/) const +{ + if (!sSpellVisualStore.LookupEntry(spellVisualId)) + return; + + WorldPackets::Spells::PlaySpellVisual packet; + packet.SpellVisualID = spellVisualId; + packet.Source = GetGUID(); + if (target) + packet.Target = target->GetGUID(); + + if (targetPosition) + packet.TargetPosition = *targetPosition; + + packet.TravelSpeed = travelSpeed; + packet.MissReason = missReason; + packet.ReflectStatus = reflectStatus; + packet.SpeedAsTime = speedAsTime; + + SendMessageToSet(packet.Write(), true); +} + +void Unit::CancelSpellMissiles(uint32 spellId, bool reverseMissile /*= false*/) +{ + bool hasMissile = false; + for (auto& itr : m_Events.GetEvents()) + { + if (Spell const* spell = Spell::ExtractSpellFromEvent(itr.second)) + { + if (spell->GetSpellInfo()->Id == spellId) + { + if (!itr.second->IsAbortScheduled()) + { + itr.second->ScheduleAbort(); + hasMissile = true; + } + } + } + } + + if (hasMissile) + { + WorldPackets::Spells::MissileCancel packet; + packet.OwnerGUID = GetGUID(); + packet.SpellID = spellId; + packet.Reverse = reverseMissile; + SendMessageToSet(packet.Write(), false); + } +} + +bool Unit::CanApplyResilience() const +{ + return !IsVehicle() && GetOwnerGUID().IsPlayer(); +} + +/*static*/ void Unit::ApplyResilience(Unit const* victim, int32* damage) +{ + // player mounted on multi-passenger mount is also classified as vehicle + if (victim->IsVehicle() && victim->GetTypeId() != TYPEID_PLAYER) + return; + + Unit const* target = nullptr; + if (victim->GetTypeId() == TYPEID_PLAYER) + target = victim; + else // victim->GetTypeId() == TYPEID_UNIT + { + if (Unit* owner = victim->GetOwner()) + if (owner->GetTypeId() == TYPEID_PLAYER) + target = owner; + } + + if (!target) + return; + + *damage -= target->GetDamageReduction(*damage); +} + +int32 Unit::CalculateAOEAvoidance(int32 damage, uint32 schoolMask, bool npcCaster) const +{ + damage = int32(float(damage) * GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_AOE_DAMAGE_AVOIDANCE, schoolMask)); + if (npcCaster) + damage = int32(float(damage) * GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_CREATURE_AOE_DAMAGE_AVOIDANCE, schoolMask)); + + return damage; +} + +// Melee based spells can be miss, parry or dodge on this step +// Crit or block - determined on damage calculation phase! (and can be both in some time) +float Unit::MeleeSpellMissChance(Unit const* victim, WeaponAttackType attType, SpellInfo const* spellInfo /*= nullptr*/) const +{ + if (spellInfo && spellInfo->HasAttribute(SPELL_ATTR7_NO_ATTACK_MISS)) + return 0.f; + + // Calculate miss chance + float missChance = GetMeleeMissChance(victim); + + if (!spellInfo && haveOffhandWeapon()) + missChance += 19.f; + + // Spellmod from SpellModOp::HitChance + float resistMissChance = 100.0f; + if (spellInfo) + if (Player* modOwner = GetSpellModOwner()) + modOwner->ApplySpellMod(spellInfo, SpellModOp::HitChance, resistMissChance); + + missChance -= resistMissChance - 100.0f; + + if (attType == RANGED_ATTACK) + missChance -= m_modRangedHitChance; + else + missChance -= m_modMeleeHitChance; + + // miss chance from auras after calculating skill based miss + missChance -= GetTotalAuraModifier(SPELL_AURA_MOD_HIT_CHANCE); + if (attType == RANGED_ATTACK) + missChance -= victim->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_RANGED_HIT_CHANCE); + else + missChance -= victim->GetTotalAuraModifier(SPELL_AURA_MOD_ATTACKER_MELEE_HIT_CHANCE); + + return std::max(missChance, 0.f); +} + +void Unit::OnPhaseChange() +{ + if (!IsInWorld()) + return; + + // Inform player groups that a member's phase has changed so the next PartyMemberState packet will update the phasing accordingly + if (IsPlayer() && ToPlayer()->GetGroup()) + ToPlayer()->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PHASE); +} + +void Unit::UpdateObjectVisibility(bool forced) +{ + if (!forced) + AddToNotify(NOTIFY_VISIBILITY_CHANGED); + else + { + WorldObject::UpdateObjectVisibility(true); + // call MoveInLineOfSight for nearby creatures + Trinity::AIRelocationNotifier notifier(*this); + Cell::VisitAllObjects(this, notifier, GetVisibilityRange()); + } +} + +void Unit::SendMoveKnockBack(Player* player, float speedXY, float speedZ, float vcos, float vsin) +{ + WorldPackets::Movement::MoveKnockBack moveKnockBack; + moveKnockBack.MoverGUID = GetGUID(); + moveKnockBack.SequenceIndex = m_movementCounter++; + moveKnockBack.Speeds.HorzSpeed = speedXY; + moveKnockBack.Speeds.VertSpeed = speedZ; + moveKnockBack.Direction = Position(vcos, vsin); + player->SendDirectMessage(moveKnockBack.Write()); +} + +void Unit::KnockbackFrom(float x, float y, float speedXY, float speedZ) +{ + if (IsMovedByServer()) + GetMotionMaster()->MoveKnockbackFrom(x, y, speedXY, speedZ); + else + { + float vcos, vsin; + GetSinCos(x, y, vsin, vcos); + SendMoveKnockBack(GetGameClientMovingMe()->GetBasePlayer(), speedXY, -speedZ, vcos, vsin); + + if (HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) || HasAuraType(SPELL_AURA_FLY)) + SetCanFly(true); + } +} + +float Unit::GetCombatRatingReduction(CombatRating cr) const +{ + if (Player const* player = ToPlayer()) + return player->GetRatingBonusValue(cr); + // Player's pet get resilience from owner + else if (IsPet() && GetOwner()) + if (Player* owner = GetOwner()->ToPlayer()) + return owner->GetRatingBonusValue(cr); + + return 0.0f; +} + +uint32 Unit::GetCombatRatingDamageReduction(CombatRating cr, float rate, float cap, uint32 damage) const +{ + float percent = std::min(GetCombatRatingReduction(cr) * rate, cap); + return CalculatePct(damage, percent); +} + +uint32 Unit::GetModelForForm(ShapeshiftForm form, uint32 spellId) const +{ + // Hardcoded cases + switch (spellId) + { + case 7090: // Bear Form + return 29414; + case 35200: // Roc Form + return 4877; + default: + break; + } + + if (GetTypeId() == TYPEID_PLAYER) + { + switch (form) + { + case FORM_CAT: + // Based on Hair color + if (getRace() == RACE_NIGHTELF) + { + uint8 hairColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + switch (hairColor) + { + case 7: // Violet + case 8: + return 29405; + case 3: // Light Blue + return 29406; + case 0: // Green + case 1: // Light Green + case 2: // Dark Green + return 29407; + case 4: // White + return 29408; + default: // original - Dark Blue + return 892; + } + } + else if (getRace() == RACE_TROLL) + { + uint8 hairColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + switch (hairColor) + { + case 0: // Red + case 1: + return 33668; + case 2: // Yellow + case 3: + return 33667; + case 4: // Blue + case 5: + case 6: + return 33666; + case 7: // Purple + case 10: + return 33665; + default: // original - white + return 33669; + } + } + else if (getRace() == RACE_WORGEN) + { + // Based on Skin color + uint8 skinColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + // Male + if (getGender() == GENDER_MALE) + { + switch (skinColor) + { + case 1: // Brown + return 33662; + case 2: // Black + case 7: + return 33661; + case 4: // yellow + return 33664; + case 3: // White + case 5: + return 33663; + default: // original - Gray + return 33660; + } + } + // Female + else + { + switch (skinColor) + { + case 5: // Brown + case 6: + return 33662; + case 7: // Black + case 8: + return 33661; + case 3: // yellow + case 4: + return 33664; + case 2: // White + return 33663; + default: // original - Gray + return 33660; + } + } + } + // Based on Skin color + else if (getRace() == RACE_TAUREN) + { + uint8 skinColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + // Male + if (getGender() == GENDER_MALE) + { + switch (skinColor) + { + case 12: // White + case 13: + case 14: + case 18: // Completly White + return 29409; + case 9: // Light Brown + case 10: + case 11: + return 29410; + case 6: // Brown + case 7: + case 8: + return 29411; + case 0: // Dark + case 1: + case 2: + case 3: // Dark Grey + case 4: + case 5: + return 29412; + default: // original - Grey + return 8571; + } + } + // Female + else + { + switch (skinColor) + { + case 10: // White + return 29409; + case 6: // Light Brown + case 7: + return 29410; + case 4: // Brown + case 5: + return 29411; + case 0: // Dark + case 1: + case 2: + case 3: + return 29412; + default: // original - Grey + return 8571; + } + } + } + else if (Player::TeamForRace(getRace()) == ALLIANCE) + return 892; + else + return 8571; + case FORM_BEAR: + // Based on Hair color + if (getRace() == RACE_NIGHTELF) + { + uint8 hairColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + switch (hairColor) + { + case 0: // Green + case 1: // Light Green + case 2: // Dark Green + return 29413; // 29415? + case 6: // Dark Blue + return 29414; + case 4: // White + return 29416; + case 3: // Light Blue + return 29417; + default: // original - Violet + return 2281; + } + } + else if (getRace() == RACE_TROLL) + { + uint8 hairColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_HAIR_COLOR_ID); + switch (hairColor) + { + case 0: // Red + case 1: + return 33657; + case 2: // Yellow + case 3: + return 33659; + case 7: // Purple + case 10: + return 33656; + case 8: // White + case 9: + case 11: + case 12: + return 33658; + default: // original - Blue + return 33655; + } + } + else if (getRace() == RACE_WORGEN) + { + // Based on Skin color + uint8 skinColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + // Male + if (getGender() == GENDER_MALE) + { + switch (skinColor) + { + case 1: // Brown + return 33653; + case 2: // Black + case 7: + return 33651; + case 4: // Yellow + return 33652; + case 3: // White + case 5: + return 33654; + default: // original - Gray + return 33650; + } + } + // Female + else + { + switch (skinColor) + { + case 5: // Brown + case 6: + return 33652; + case 7: // Black + case 8: + return 33651; + case 3: // yellow + case 4: + return 33654; + case 2: // White + return 33653; + default: // original - Gray + return 33650; + } + } + } + // Based on Skin color + else if (getRace() == RACE_TAUREN) + { + uint8 skinColor = GetByteValue(PLAYER_BYTES, PLAYER_BYTES_OFFSET_SKIN_ID); + // Male + if (getGender() == GENDER_MALE) + { + switch (skinColor) + { + case 0: // Dark (Black) + case 1: + case 2: + return 29418; + case 3: // White + case 4: + case 5: + case 12: + case 13: + case 14: + return 29419; + case 9: // Light Brown/Grey + case 10: + case 11: + case 15: + case 16: + case 17: + return 29420; + case 18: // Completly White + return 29421; + default: // original - Brown + return 2289; + } + } + // Female + else + { + switch (skinColor) + { + case 0: // Dark (Black) + case 1: + return 29418; + case 2: // White + case 3: + return 29419; + case 6: // Light Brown/Grey + case 7: + case 8: + case 9: + return 29420; + case 10: // Completly White + return 29421; + default: // original - Brown + return 2289; + } + } + } + else if (Player::TeamForRace(getRace()) == ALLIANCE) + return 2281; + else + return 2289; + case FORM_FLIGHT: + if (Player::TeamForRace(getRace()) == ALLIANCE) + return 20857; + return 20872; + case FORM_FLIGHT_EPIC: + if (Player::TeamForRace(getRace()) == ALLIANCE) + return (getRace() == RACE_WORGEN ? 37729 : 21243); + if (getRace() == RACE_TROLL) + return 37730; + return 21244; + case FORM_MOONKIN: + if (getRace() == RACE_TROLL) + return 37174; + if (getRace() == RACE_WORGEN) + return 37173; + return 15374; + case FORM_GHOSTWOLF: + if (HasAura(58135)) //! Glyph of Arctic Wolf + return 27312; + return 4613; + default: + break; + } + } + + uint32 modelid = 0; + SpellShapeshiftFormEntry const* formEntry = sSpellShapeshiftFormStore.LookupEntry(form); + if (formEntry && formEntry->CreatureDisplayID[0]) + { + // Take the alliance modelid as default + if (GetTypeId() != TYPEID_PLAYER) + return formEntry->CreatureDisplayID[0]; + else + { + if (Player::TeamForRace(getRace()) == ALLIANCE) + modelid = formEntry->CreatureDisplayID[0]; + else + modelid = formEntry->CreatureDisplayID[1]; + + // If the player is horde but there are no values for the horde modelid - take the alliance modelid + if (!modelid && Player::TeamForRace(getRace()) == HORDE) + modelid = formEntry->CreatureDisplayID[0]; + } + } + + return modelid; +} + +uint32 Unit::GetModelForTotem(PlayerTotemType totemType) +{ + switch (getRace()) + { + case RACE_ORC: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 30758; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 30757; + case SUMMON_TYPE_TOTEM_WATER: // water + return 30759; + case SUMMON_TYPE_TOTEM_AIR: // air + return 30756; + } + break; + } + case RACE_DWARF: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 30754; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 30753; + case SUMMON_TYPE_TOTEM_WATER: // water + return 30755; + case SUMMON_TYPE_TOTEM_AIR: // air + return 30736; + } + break; + } + case RACE_TROLL: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 30762; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 30761; + case SUMMON_TYPE_TOTEM_WATER: // water + return 30763; + case SUMMON_TYPE_TOTEM_AIR: // air + return 30760; + } + break; + } + case RACE_TAUREN: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 4589; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 4588; + case SUMMON_TYPE_TOTEM_WATER: // water + return 4587; + case SUMMON_TYPE_TOTEM_AIR: // air + return 4590; + } + break; + } + case RACE_DRAENEI: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 19074; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 19073; + case SUMMON_TYPE_TOTEM_WATER: // water + return 19075; + case SUMMON_TYPE_TOTEM_AIR: // air + return 19071; + } + break; + } + case RACE_GOBLIN: + { + switch (totemType) + { + case SUMMON_TYPE_TOTEM_FIRE: // fire + return 30783; + case SUMMON_TYPE_TOTEM_EARTH: // earth + return 30782; + case SUMMON_TYPE_TOTEM_WATER: // water + return 30784; + case SUMMON_TYPE_TOTEM_AIR: // air + return 30781; + } + break; + } + } + return 0; +} + +void Unit::JumpTo(float speedXY, float speedZ, bool forward, Optional dest) +{ + float angle = forward ? 0 : float(M_PI); + if (dest) + angle += GetRelativeAngle(*dest); + + if (GetTypeId() == TYPEID_UNIT) + GetMotionMaster()->MoveJumpTo(angle, speedXY, speedZ); + else + { + float vcos = std::cos(angle + GetOrientation()); + float vsin = std::sin(angle + GetOrientation()); + SendMoveKnockBack(ToPlayer(), speedXY, -speedZ, vcos, vsin); + } +} + +void Unit::JumpTo(WorldObject* obj, float speedZ, bool withOrientation) +{ + float x, y, z; + obj->GetContactPoint(this, x, y, z); + float speedXY = GetExactDist2d(x, y) * 10.0f / speedZ; + GetMotionMaster()->MoveJump(x, y, z, GetAngle(obj), speedXY, speedZ, EVENT_JUMP, withOrientation); +} + +bool Unit::HandleSpellClick(Unit* clicker, int8 seatId) +{ + bool result = false; + uint32 spellClickEntry = GetVehicleKit() ? GetVehicleKit()->GetCreatureEntry() : GetEntry(); + TriggerCastFlags const flags = GetVehicleKit() ? TRIGGERED_IGNORE_CASTER_MOUNTED_OR_ON_VEHICLE : TRIGGERED_NONE; + + SpellClickInfoMapBounds clickPair = sObjectMgr->GetSpellClickInfoMapBounds(spellClickEntry); + for (SpellClickInfoContainer::const_iterator itr = clickPair.first; itr != clickPair.second; ++itr) + { + //! First check simple relations from clicker to clickee + if (!itr->second.IsFitToRequirements(clicker, this)) + continue; + + //! Check database conditions + if (!sConditionMgr->IsObjectMeetingSpellClickConditions(spellClickEntry, itr->second.spellId, clicker, this)) + continue; + + Unit* caster = (itr->second.castFlags & NPC_CLICK_CAST_CASTER_CLICKER) ? clicker : this; + Unit* target = (itr->second.castFlags & NPC_CLICK_CAST_TARGET_CLICKER) ? clicker : this; + ObjectGuid origCasterGUID = (itr->second.castFlags & NPC_CLICK_CAST_ORIG_CASTER_OWNER) ? GetOwnerOrCreatorGUID() : clicker->GetGUID(); + + SpellInfo const* spellEntry = sSpellMgr->GetSpellInfo(itr->second.spellId); + // if (!spellEntry) should be checked at npc_spellclick load + + if (seatId > -1) + { + uint8 i = 0; + bool valid = false; + while (i < MAX_SPELL_EFFECTS && !valid) + { + if (spellEntry->Effects[i].ApplyAuraName == SPELL_AURA_CONTROL_VEHICLE) + { + valid = true; + break; + } + ++i; + } + + if (!valid) + { + TC_LOG_ERROR("sql.sql", "Spell %u specified in npc_spellclick_spells is not a valid vehicle enter aura!", itr->second.spellId); + continue; + } + + if (IsInMap(caster)) + { + CastSpellExtraArgs args(flags); + args.OriginalCaster = origCasterGUID; + args.AddSpellMod(SpellValueMod(SPELLVALUE_BASE_POINT0 + i), seatId + 1); + caster->CastSpell(target, itr->second.spellId, args); + } + else // This can happen during Player::_LoadAuras + { + int32 bp[MAX_SPELL_EFFECTS] = { }; + for (uint32 j = 0; j < MAX_SPELL_EFFECTS; ++j) + bp[j] = spellEntry->Effects[j].BasePoints; + + bp[i] = seatId; + + AuraCreateInfo createInfo(spellEntry, MAX_EFFECT_MASK, this); + createInfo + .SetCaster(clicker) + .SetBaseAmount(bp) + .SetCasterGUID(origCasterGUID); + + Aura::TryRefreshStackOrCreate(createInfo); + } + } + else + { + if (IsInMap(caster)) + caster->CastSpell(target, spellEntry->Id, CastSpellExtraArgs().SetOriginalCaster(origCasterGUID)); + else + { + AuraCreateInfo createInfo(spellEntry, MAX_EFFECT_MASK, this); + createInfo + .SetCaster(clicker) + .SetCasterGUID(origCasterGUID); + + Aura::TryRefreshStackOrCreate(createInfo); + } + } + + result = true; + } + + Creature* creature = ToCreature(); + if (creature && creature->IsAIEnabled()) + creature->AI()->OnSpellClick(clicker, result); + + return result; +} + +void Unit::EnterVehicle(Unit* base, int8 seatId) +{ + CastSpellExtraArgs args(TRIGGERED_IGNORE_CASTER_MOUNTED_OR_ON_VEHICLE); + args.AddSpellBP0(seatId + 1); + CastSpell(base, VEHICLE_SPELL_RIDE_HARDCODED, args); +} + +void Unit::_EnterVehicle(Vehicle* vehicle, int8 seatId, AuraApplication const* aurApp) +{ + // Must be called only from aura handler + ASSERT(aurApp); + + if (!IsAlive() || GetVehicleKit() == vehicle || vehicle->GetBase()->IsOnVehicle(this)) + return; + + if (m_vehicle) + { + if (m_vehicle != vehicle) + { + TC_LOG_DEBUG("entities.vehicle", "EnterVehicle: %u exit %u and enter %u.", GetEntry(), m_vehicle->GetBase()->GetEntry(), vehicle->GetBase()->GetEntry()); + ExitVehicle(); + } + else if (seatId >= 0 && seatId == GetTransSeat()) + return; + else + { + //Exit the current vehicle because unit will reenter in a new seat. + m_vehicle->GetBase()->RemoveAurasByType(SPELL_AURA_CONTROL_VEHICLE, GetGUID(), aurApp->GetBase()); + } + } + + if (aurApp->GetRemoveMode().HasAnyFlag()) + return; + + if (Player* player = ToPlayer()) + { + if (vehicle->GetBase()->GetTypeId() == TYPEID_PLAYER && player->IsInCombat()) + { + vehicle->GetBase()->RemoveAura(const_cast(aurApp)); + return; + } + + if (vehicle->GetBase()->GetTypeId() == TYPEID_UNIT) + { + // If a player entered a vehicle that is part of a formation, remove it from said formation + if (CreatureGroup* creatureGroup = vehicle->GetBase()->ToCreature()->GetFormation()) + creatureGroup->RemoveMember(vehicle->GetBase()->ToCreature()); + } + } + + // If vehicle flag for fixed position set (cannons), or if the following hardcoded units, then set state rooted + // 30236 | Argent Cannon + // 39759 | Tankbuster Cannon + if ((vehicle->GetVehicleInfo()->Flags & VEHICLE_FLAG_FIXED_POSITION) || vehicle->GetBase()->GetEntry() == 30236 || vehicle->GetBase()->GetEntry() == 39759) + SetControlled(true, UNIT_STATE_ROOT); + + ASSERT(!m_vehicle); + (void)vehicle->AddVehiclePassenger(this, seatId); +} + +void Unit::ChangeSeat(int8 seatId, bool next) +{ + if (!m_vehicle) + return; + + // Don't change if current and new seat are identical + if (seatId == GetTransSeat()) + return; + + SeatMap::const_iterator seat = (seatId < 0 ? m_vehicle->GetNextEmptySeat(GetTransSeat(), next) : m_vehicle->Seats.find(seatId)); + // The second part of the check will only return true if seatId >= 0. @Vehicle::GetNextEmptySeat makes sure of that. + if (seat == m_vehicle->Seats.end() || !seat->second.IsEmpty()) + return; + + AuraEffect* rideVehicleEffect = nullptr; + AuraEffectList const& vehicleAuras = m_vehicle->GetBase()->GetAuraEffectsByType(SPELL_AURA_CONTROL_VEHICLE); + for (AuraEffectList::const_iterator itr = vehicleAuras.begin(); itr != vehicleAuras.end(); ++itr) + { + if ((*itr)->GetCasterGUID() != GetGUID()) + continue; + + // Make sure there is only one ride vehicle aura on target cast by the unit changing seat + ASSERT(!rideVehicleEffect); + rideVehicleEffect = *itr; + } + + // Unit riding a vehicle must always have control vehicle aura on target + ASSERT(rideVehicleEffect); + + rideVehicleEffect->ChangeAmount(seat->first + 1); +} + +void Unit::ExitVehicle(Position const* /*exitPosition*/) +{ + //! This function can be called at upper level code to initialize an exit from the passenger's side. + if (!m_vehicle) + return; + + GetVehicleBase()->RemoveAurasByType(SPELL_AURA_CONTROL_VEHICLE, GetGUID()); + //! The following call would not even be executed successfully as the + //! SPELL_AURA_CONTROL_VEHICLE unapply handler already calls _ExitVehicle without + //! specifying an exitposition. The subsequent call below would return on if (!m_vehicle). + /*_ExitVehicle(exitPosition);*/ + //! To do: + //! We need to allow SPELL_AURA_CONTROL_VEHICLE unapply handlers in spellscripts + //! to specify exit coordinates and either store those per passenger, or we need to + //! init spline movement based on those coordinates in unapply handlers, and + //! relocate exiting passengers based on Unit::moveSpline data. Either way, + //! Coming Soon(TM) +} + +void Unit::_ExitVehicle(Position const* exitPosition) +{ + /// It's possible m_vehicle is nullptr, when this function is called indirectly from @VehicleJoinEvent::Abort. + /// In that case it was not possible to add the passenger to the vehicle. The vehicle aura has already been removed + /// from the target in the aforementioned function and we don't need to do anything else at this point. + if (!m_vehicle) + return; + + // This should be done before dismiss, because there may be some aura removal + VehicleSeatAddon const* seatAddon = m_vehicle->GetSeatAddonForSeatOfPassenger(this); + Vehicle* vehicle = m_vehicle->RemovePassenger(this); + + if (!vehicle) + { + TC_LOG_ERROR("entities.vehicle", "RemovePassenger() couldn't remove current unit from vehicle."); + return; + } + + Player* player = ToPlayer(); + + // If the player is on mounted duel and exits the mount, he should immediatly lose the duel + if (player && player->duel && player->duel->isMounted) + player->DuelComplete(DUEL_FLED); + + SetControlled(false, UNIT_STATE_ROOT); // SMSG_MOVE_FORCE_UNROOT, ~MOVEMENTFLAG_ROOT + + AddUnitState(UNIT_STATE_MOVE); + + if (player) + player->SetFallInformation(0, GetPositionZ()); + + Position pos; + // If we ask for a specific exit position, use that one. Otherwise allow scripts to modify it + if (exitPosition) + pos = *exitPosition; + else + { + // Set exit position to vehicle position and use the current orientation + pos = vehicle->GetBase()->GetPosition(); + pos.SetOrientation(GetOrientation()); + + // Change exit position based on seat entry + if (seatAddon) + { + if (seatAddon->ExitParameter == VehicleExitParameters::VehicleExitParamOffset) + pos.RelocateOffset({ seatAddon->ExitParameterX, seatAddon->ExitParameterY, seatAddon->ExitParameterZ, seatAddon->ExitParameterO }); + else if (seatAddon->ExitParameter == VehicleExitParameters::VehicleExitParamDest) + pos.Relocate({ seatAddon->ExitParameterX, seatAddon->ExitParameterY, seatAddon->ExitParameterZ, seatAddon->ExitParameterO }); + } + } + + float height = pos.GetPositionZ() + vehicle->GetBase()->GetCollisionHeight(); + + Movement::MoveSplineInit init(this); + + // Creatures without inhabit type air should begin falling after exiting the vehicle + if (GetTypeId() == TYPEID_UNIT && !CanFly() && height > GetMap()->GetWaterOrGroundLevel(GetPhaseShift(), pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ() + vehicle->GetBase()->GetCollisionHeight(), &height)) + init.SetFall(); + + init.MoveTo(pos.GetPositionX(), pos.GetPositionY(), height, false); + init.SetFacing(pos.GetOrientation()); + init.SetTransportExit(); + GetMotionMaster()->LaunchMoveSpline(std::move(init), EVENT_VEHICLE_EXIT, MOTION_SLOT_CONTROLLED); + + if (player) + player->ResummonPetTemporaryUnSummonedIfAny(); + + if (vehicle->GetBase()->HasUnitTypeMask(UNIT_MASK_MINION) && vehicle->GetBase()->GetTypeId() == TYPEID_UNIT) + if (((Minion*)vehicle->GetBase())->GetOwner() == this) + vehicle->GetBase()->ToCreature()->DespawnOrUnsummon(1); + + if (HasUnitTypeMask(UNIT_MASK_ACCESSORY)) + { + // Vehicle just died, we die too + if (vehicle->GetBase()->getDeathState() == JUST_DIED) + setDeathState(JUST_DIED); + // If for other reason we as minion are exiting the vehicle (ejected, master dismounted) - unsummon + else + ToTempSummon()->UnSummon(2000); // Approximation + } +} + +bool Unit::IsFalling() const +{ + return m_movementInfo.HasMovementFlag(MOVEMENTFLAG_FALLING | MOVEMENTFLAG_FALLING_FAR) || movespline->isFalling(); +} + +bool Unit::CanSwim() const +{ + // Mirror client behavior, if this method returns false then client will not use swimming animation and for players will apply gravity as if there was no water + if (HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_CANT_SWIM)) + return false; + if (HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) // is player + return true; + if (HasFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_AI_WILL_ONLY_SWIM_IF_TARGET_SWIMS)) + return false; + if (HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT)) + return true; + return HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_RENAME | UNIT_FLAG_CAN_SWIM); +} + +void Unit::NearTeleportTo(Position const& pos, bool casting /*= false*/) +{ + DisableSpline(); + if (GetTypeId() == TYPEID_PLAYER) + { + WorldLocation target(GetMapId(), pos); + ToPlayer()->TeleportTo(target, TELE_TO_NOT_LEAVE_TRANSPORT | TELE_TO_NOT_LEAVE_COMBAT | TELE_TO_NOT_UNSUMMON_PET | (casting ? TELE_TO_SPELL : TELE_TO_NONE)); + } + else + { + SendTeleportPacket(pos); + UpdatePosition(pos, true); + UpdateObjectVisibility(); + } +} + +void Unit::WriteMovementInfo(WorldPacket& data, Movement::ExtraMovementStatusElement* extras /*= nullptr*/, uint32* movementCounter /*= nullptr*/) +{ + MovementInfo const& mi = m_movementInfo; + + bool hasMovementFlags = GetUnitMovementFlags() != 0; + bool hasMovementFlags2 = GetExtraUnitMovementFlags() != 0; + bool hasTimestamp = true; + bool hasOrientation = !G3D::fuzzyEq(GetOrientation(), 0.0f); + bool hasTransportData = GetTransGUID() != 0; + bool hasSpline = IsSplineEnabled(); + + bool hasTransportTime2 = hasTransportData && m_movementInfo.transport.time2 != 0; + bool hasTransportVehicleId = hasTransportData && m_movementInfo.transport.vehicleId != 0; + bool hasPitch = HasUnitMovementFlag(MovementFlags(MOVEMENTFLAG_SWIMMING | MOVEMENTFLAG_FLYING)) || HasExtraUnitMovementFlag(MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING); + bool hasFallDirection = HasUnitMovementFlag(MOVEMENTFLAG_FALLING); + bool hasFallData = hasFallDirection || m_movementInfo.jump.fallTime != 0; + bool hasSplineElevation = HasUnitMovementFlag(MOVEMENTFLAG_SPLINE_ELEVATION); + + MovementStatusElements const* sequence = GetMovementStatusElementsSequence(static_cast(data.GetOpcode())); + if (!sequence) + { + TC_LOG_ERROR("network", "Unit::WriteMovementInfo: No movement sequence found for opcode %s", GetOpcodeNameForLogging(static_cast(data.GetOpcode())).c_str()); + return; + } + + ObjectGuid guid = GetGUID(); + ObjectGuid tguid = hasTransportData ? GetTransGUID() : ObjectGuid::Empty; + + for (; *sequence != MSEEnd; ++sequence) + { + MovementStatusElements const& element = *sequence; + + switch (element) + { + case MSEHasGuidByte0: + case MSEHasGuidByte1: + case MSEHasGuidByte2: + case MSEHasGuidByte3: + case MSEHasGuidByte4: + case MSEHasGuidByte5: + case MSEHasGuidByte6: + case MSEHasGuidByte7: + data.WriteBit(guid[element - MSEHasGuidByte0]); + break; + case MSEHasTransportGuidByte0: + case MSEHasTransportGuidByte1: + case MSEHasTransportGuidByte2: + case MSEHasTransportGuidByte3: + case MSEHasTransportGuidByte4: + case MSEHasTransportGuidByte5: + case MSEHasTransportGuidByte6: + case MSEHasTransportGuidByte7: + if (hasTransportData) + data.WriteBit(tguid[element - MSEHasTransportGuidByte0]); + break; + case MSEGuidByte0: + case MSEGuidByte1: + case MSEGuidByte2: + case MSEGuidByte3: + case MSEGuidByte4: + case MSEGuidByte5: + case MSEGuidByte6: + case MSEGuidByte7: + data.WriteByteSeq(guid[element - MSEGuidByte0]); + break; + case MSETransportGuidByte0: + case MSETransportGuidByte1: + case MSETransportGuidByte2: + case MSETransportGuidByte3: + case MSETransportGuidByte4: + case MSETransportGuidByte5: + case MSETransportGuidByte6: + case MSETransportGuidByte7: + if (hasTransportData) + data.WriteByteSeq(tguid[element - MSETransportGuidByte0]); + break; + case MSEHasMovementFlags: + data.WriteBit(!hasMovementFlags); + break; + case MSEHasMovementFlags2: + data.WriteBit(!hasMovementFlags2); + break; + case MSEHasTimestamp: + data.WriteBit(!hasTimestamp); + break; + case MSEHasOrientation: + data.WriteBit(!hasOrientation); + break; + case MSEHasTransportData: + data.WriteBit(hasTransportData); + break; + case MSEHasTransportTime2: + if (hasTransportData) + data.WriteBit(hasTransportTime2); + break; + case MSEHasVehicleId: + if (hasTransportData) + data.WriteBit(hasTransportVehicleId); + break; + case MSEHasPitch: + data.WriteBit(!hasPitch); + break; + case MSEHasFallData: + data.WriteBit(hasFallData); + break; + case MSEHasFallDirection: + if (hasFallData) + data.WriteBit(hasFallDirection); + break; + case MSEHasSplineElevation: + data.WriteBit(!hasSplineElevation); + break; + case MSEHasSpline: + data.WriteBit(hasSpline); + break; + case MSEHasHeightChangeFailed: + data.WriteBit(mi.HasHeightChangeFailed()); + break; + case MSEMovementFlags: + if (hasMovementFlags) + data.WriteBits(GetUnitMovementFlags(), 30); + break; + case MSEMovementFlags2: + if (hasMovementFlags2) + data.WriteBits(GetExtraUnitMovementFlags(), 12); + break; + case MSETimestamp: + if (hasTimestamp) + data << mi.time; + break; + case MSEPositionX: + data << GetPositionX(); + break; + case MSEPositionY: + data << GetPositionY(); + break; + case MSEPositionZ: + data << GetPositionZ(); + break; + case MSEOrientation: + if (hasOrientation) + data << GetOrientation(); + break; + case MSETransportPositionX: + if (hasTransportData) + data << GetTransOffsetX(); + break; + case MSETransportPositionY: + if (hasTransportData) + data << GetTransOffsetY(); + break; + case MSETransportPositionZ: + if (hasTransportData) + data << GetTransOffsetZ(); + break; + case MSETransportOrientation: + if (hasTransportData) + data << GetTransOffsetO(); + break; + case MSETransportSeat: + if (hasTransportData) + data << GetTransSeat(); + break; + case MSETransportTime: + if (hasTransportData) + data << GetTransTime(); + break; + case MSETransportTime2: + if (hasTransportData && hasTransportTime2) + data << mi.transport.time2; + break; + case MSETransportVehicleId: + if (hasTransportData && hasTransportVehicleId) + data << mi.transport.vehicleId; + break; + case MSEPitch: + if (hasPitch) + data << mi.pitch; + break; + case MSEFallTime: + if (hasFallData) + data << mi.jump.fallTime; + break; + case MSEFallVerticalSpeed: + if (hasFallData) + data << mi.jump.zspeed; + break; + case MSEFallCosAngle: + if (hasFallData && hasFallDirection) + data << mi.jump.cosAngle; + break; + case MSEFallSinAngle: + if (hasFallData && hasFallDirection) + data << mi.jump.sinAngle; + break; + case MSEFallHorizontalSpeed: + if (hasFallData && hasFallDirection) + data << mi.jump.xyspeed; + break; + case MSESplineElevation: + if (hasSplineElevation) + data << mi.splineElevation; + break; + case MSECounter: + data << uint32(movementCounter ? *movementCounter : 0); + break; + case MSEZeroBit: + data.WriteBit(0); + break; + case MSEOneBit: + data.WriteBit(1); + break; + case MSEFlushBits: + data.FlushBits(); + break; + case MSEExtraElement: + extras->WriteNextElement(data); + break; + default: + ASSERT(Movement::PrintInvalidSequenceElement(element, __FUNCTION__)); + break; + } + } +} + +void Unit::SendTeleportPacket(Position const& pos) +{ + // SMSG_MOVE_UPDATE_TELEPORT is sent to nearby players to signal the teleport + // MSG_MOVE_TELEPORT is sent to self in order to trigger MSG_MOVE_TELEPORT_ACK and update the position server side + if (IsMovedByClient()) + { + Player* playerMover = GetGameClientMovingMe()->GetBasePlayer(); + float x, y, z, o; + pos.GetPosition(x, y, z, o); + if (TransportBase* transportBase = GetDirectTransport()) + transportBase->CalculatePassengerOffset(x, y, z, &o); + + WorldPackets::Movement::MoveTeleport moveTeleport; + moveTeleport.MoverGUID = GetGUID(); + moveTeleport.Pos = Position(x, y, z); + if (GetTransGUID()) + moveTeleport.TransportGUID = GetTransGUID(); + moveTeleport.Facing = o; + moveTeleport.SequenceIndex = m_movementCounter++; + playerMover->SendDirectMessage(moveTeleport.Write()); + } + else + { + // This is the only packet sent for creatures which contains MovementInfo structure + // we do not update m_movementInfo for creatures so it needs to be done manually here + WorldPackets::Movement::MoveUpdateTeleport moveUpdateTeleport; + moveUpdateTeleport.Status = &m_movementInfo; + moveUpdateTeleport.Status->guid = GetGUID(); + moveUpdateTeleport.Status->pos.Relocate(pos); + moveUpdateTeleport.Status->time = GameTime::GetGameTimeMS(); + SendMessageToSet(moveUpdateTeleport.Write(), false); + } +} + +bool Unit::UpdatePosition(float x, float y, float z, float orientation, bool teleport) +{ + // prevent crash when a bad coord is sent by the client + if (!Trinity::IsValidMapCoord(x, y, z, orientation)) + { + TC_LOG_DEBUG("entities.unit", "Unit::UpdatePosition(%f, %f, %f) .. bad coordinates!", x, y, z); + return false; + } + + // Check if angular distance changed + bool const turn = G3D::fuzzyGt(M_PI - fabs(fabs(GetOrientation() - orientation) - M_PI), 0.0f); + + // G3D::fuzzyEq won't help here, in some cases magnitudes differ by a little more than G3D::eps, but should be considered equal + bool const relocated = (teleport || + std::fabs(GetPositionX() - x) > 0.001f || + std::fabs(GetPositionY() - y) > 0.001f || + std::fabs(GetPositionZ() - z) > 0.001f); + + if (relocated) + { + // move and update visible state if need + if (GetTypeId() == TYPEID_PLAYER) + GetMap()->PlayerRelocation(ToPlayer(), x, y, z, orientation); + else + GetMap()->CreatureRelocation(ToCreature(), x, y, z, orientation); + } + else if (turn) + UpdateOrientation(orientation); + + _positionUpdateInfo.Relocated = relocated; + _positionUpdateInfo.Turned = turn; + + return (relocated || turn); +} + +bool Unit::UpdatePosition(Position const& pos, bool teleport) +{ + return UpdatePosition(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), pos.GetOrientation(), teleport); +} + +//! Only server-side orientation update, does not broadcast to client +void Unit::UpdateOrientation(float orientation) +{ + SetOrientation(orientation); + if (IsVehicle()) + GetVehicleKit()->RelocatePassengers(); +} + +//! Only server-side height update, does not broadcast to client +void Unit::UpdateHeight(float newZ) +{ + Relocate(GetPositionX(), GetPositionY(), newZ); + if (IsVehicle()) + GetVehicleKit()->RelocatePassengers(); +} + +void Unit::ClearPendingMovementChangeForType(MovementChangeType changeType) +{ + m_pendingMovementChanges.erase(changeType); +} + +void Unit::AssignPendingMovementChange(MovementChangeType changeType, PlayerMovementPendingChange&& newChange) +{ + m_pendingMovementChanges[changeType] = newChange; +} + +PlayerMovementPendingChange const* Unit::GetPendingMovementChange(MovementChangeType changeType) const +{ + return Trinity::Containers::MapGetValuePtr(m_pendingMovementChanges, changeType); +} + +void Unit::CheckPendingMovementAcks() +{ + if (sWorld->getIntConfig(CONFIG_PENDING_MOVE_CHANGES_TIMEOUT) == 0) + return; + + if (!HasPendingMovementChange()) + return; + + for (std::pair const& pendingChange : m_pendingMovementChanges) + { + if (GameTime::GetGameTimeMS() > pendingChange.second.time + sWorld->getIntConfig(CONFIG_PENDING_MOVE_CHANGES_TIMEOUT)) + { + /* + when players are teleported from one corner of a map to an other (example: from Dragonblight to the entrance of Naxxramas, both in the same map: Northend), + is it done through what is called a 'near' teleport. A near teleport always involve teleporting a player from one point to an other in the same map, even if + the distance is huge. When that distance is big enough, a loading screen appears on the client side. During that time, the client loads the surrounding zone + of the new location (and everything it contains). The problem is that, as long as the client hasn't finished loading the new zone, it will NOT ack the near + teleport. So if the server sends a near teleport order at a certain time and the client takes 20s to load the new zone (let's imagine a very slow computer), + even with zero latency, the server will receive an ack from the client only after 20s. + + For this reason and because the current implementation is simple (you dear reader, feel free to improve it if you can), we will just ignore checking for + near teleport acks (for now. @todo). + */ + if (pendingChange.second.movementChangeType == MovementChangeType::TELEPORT) + return; + + GameClient* controller = GetGameClientMovingMe(); + controller->GetWorldSession()->KickPlayer(); + TC_LOG_INFO("cheat", "Unit::CheckPendingMovementAcks: Player GUID: %s took too long to acknowledge a movement change. He was therefore kicked.", controller->GetBasePlayer()->GetGUID().ToString().c_str()); + break; + } + } +} + +void Unit::PurgeAndApplyPendingMovementChanges(bool informObservers /* = true */) +{ + for (std::pair const& pendingChange : m_pendingMovementChanges) + { + float speedFlat = pendingChange.second.newValue; + MovementChangeType changeType = pendingChange.second.movementChangeType; + Optional moveType; + switch (changeType) + { + case MovementChangeType::SPEED_CHANGE_WALK: moveType = MOVE_WALK; break; + case MovementChangeType::SPEED_CHANGE_RUN: moveType = MOVE_RUN; break; + case MovementChangeType::SPEED_CHANGE_RUN_BACK: moveType = MOVE_RUN_BACK; break; + case MovementChangeType::SPEED_CHANGE_SWIM: moveType = MOVE_SWIM; break; + case MovementChangeType::SPEED_CHANGE_SWIM_BACK: moveType = MOVE_SWIM_BACK; break; + case MovementChangeType::RATE_CHANGE_TURN: moveType = MOVE_TURN_RATE; break; + case MovementChangeType::SPEED_CHANGE_FLIGHT_SPEED: moveType = MOVE_FLIGHT; break; + case MovementChangeType::SPEED_CHANGE_FLIGHT_BACK_SPEED: moveType = MOVE_FLIGHT_BACK; break; + case MovementChangeType::RATE_CHANGE_PITCH: moveType = MOVE_PITCH_RATE; break; + case MovementChangeType::GRAVITY_DISABLE: + Unit::SetDisableGravity(pendingChange.second.apply); + break; + case MovementChangeType::SET_CAN_FLY: + Unit::SetCanFly(pendingChange.second.apply); + break; + case MovementChangeType::SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY: + Unit::SetCanTransitionBetweenSwimAndFly(pendingChange.second.apply); + break; + case MovementChangeType::SET_COLLISION_HGT: + break; + default: + ASSERT(false); + return; + } + + if (moveType.has_value()) + { + float newSpeedRate = speedFlat / (IsControlledByPlayer() ? playerBaseMoveSpeed[*moveType] : baseMoveSpeed[*moveType]); + SetSpeedRateReal(*moveType, newSpeedRate); + + if (informObservers) + MovementPacketSender::SendSpeedChangeToObservers(this, *moveType, speedFlat); + } + else if (informObservers) + { + switch (changeType) + { + case MovementChangeType::GRAVITY_DISABLE: + case MovementChangeType::SET_CAN_FLY: + case MovementChangeType::SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY: + MovementPacketSender::SendMovementFlagChangeToObservers(this); + break; + default: + break; + } + } + } + + m_pendingMovementChanges.clear(); +} + +PlayerMovementPendingChange::PlayerMovementPendingChange() +{ + time = GameTime::GetGameTimeMS(); +} + +// baseRage means damage taken when attacker = false +int32 Unit::RewardRage(uint32 baseRage, bool asAttacker) +{ + float addRage = baseRage; + + addRage *= sWorld->getRate(RATE_POWER_RAGE_INCOME); + return ModifyPower(POWER_RAGE, static_cast(addRage), !asAttacker); +} + +void Unit::StopAttackFaction(uint32 faction_id) +{ + if (Unit* victim = GetVictim()) + { + if (victim->GetFactionTemplateEntry()->Faction == faction_id) + { + AttackStop(); + if (IsNonMeleeSpellCast(false)) + InterruptNonMeleeSpells(false); + + // melee and ranged forced attack cancel + if (GetTypeId() == TYPEID_PLAYER) + ToPlayer()->SendAttackSwingCancelAttack(); + } + } + + AttackerSet const& attackers = getAttackers(); + for (AttackerSet::const_iterator itr = attackers.begin(); itr != attackers.end();) + { + if ((*itr)->GetFactionTemplateEntry()->Faction == faction_id) + { + (*itr)->AttackStop(); + itr = attackers.begin(); + } + else + ++itr; + } + + std::vector refsToEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (pair.second->GetOther(this)->GetFactionTemplateEntry()->Faction == faction_id) + refsToEnd.push_back(pair.second); + for (CombatReference* ref : refsToEnd) + ref->EndCombat(); + + for (Unit* minion : m_Controlled) + minion->StopAttackFaction(faction_id); +} + +void Unit::OutDebugInfo() const +{ + TC_LOG_ERROR("entities.unit", "Unit::OutDebugInfo"); + TC_LOG_DEBUG("entities.unit", "%s name %s", GetGUID().ToString().c_str(), GetName().c_str()); + TC_LOG_DEBUG("entities.unit", "Owner %s, Minion %s, Charmer %s, Charmed %s", GetOwnerOrCreatorGUID().ToString().c_str(), GetMinionGUID().ToString().c_str(), GetCharmerGUID().ToString().c_str(), GetCharmedGUID().ToString().c_str()); + TC_LOG_DEBUG("entities.unit", "In world %u, unit type mask %u", (uint32)(IsInWorld() ? 1 : 0), m_unitTypeMask); + if (IsInWorld()) + TC_LOG_DEBUG("entities.unit", "Mapid %u", GetMapId()); + + std::ostringstream o; + o << "Summon Slot: "; + for (uint32 i = 0; i < MAX_SUMMON_SLOT; ++i) + o << m_SummonSlot[i].ToString() << ", "; + + TC_LOG_DEBUG("entities.unit", "%s", o.str().c_str()); + o.str(""); + + o << "Controlled List: "; + for (ControlList::const_iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) + o << (*itr)->GetGUID().ToString() << ", "; + TC_LOG_DEBUG("entities.unit", "%s", o.str().c_str()); + o.str(""); + + o << "Aura List: "; + for (AuraApplicationMap::const_iterator itr = m_appliedAuras.begin(); itr != m_appliedAuras.end(); ++itr) + o << itr->first << ", "; + TC_LOG_DEBUG("entities.unit", "%s", o.str().c_str()); + o.str(""); + + if (IsVehicle()) + { + o << "Passenger List: "; + for (SeatMap::iterator itr = GetVehicleKit()->Seats.begin(); itr != GetVehicleKit()->Seats.end(); ++itr) + if (Unit* passenger = ObjectAccessor::GetUnit(*GetVehicleBase(), itr->second.Passenger.Guid)) + o << passenger->GetGUID().ToString() << ", "; + TC_LOG_DEBUG("entities.unit", "%s", o.str().c_str()); + } + + if (GetVehicle()) + TC_LOG_DEBUG("entities.unit", "On vehicle %u.", GetVehicleBase()->GetEntry()); +} + +void Unit::SendClearTarget() +{ + WorldPackets::Combat::BreakTarget packet; + packet.UnitGUID = GetGUID(); + SendMessageToSet(packet.Write(), false); +} + +uint32 Unit::GetResistance(SpellSchoolMask mask) const +{ + int32 resist = -1; + for (int32 i = SPELL_SCHOOL_NORMAL; i < MAX_SPELL_SCHOOL; ++i) + if (mask & (1 << i) && (resist < 0 || resist > int32(GetResistance(SpellSchools(i))))) + resist = int32(GetResistance(SpellSchools(i))); + + // resist value will never be negative here + return uint32(resist); +} + +void CharmInfo::SetIsCommandAttack(bool val) +{ + _isCommandAttack = val; +} + +bool CharmInfo::IsCommandAttack() +{ + return _isCommandAttack; +} + +void CharmInfo::SetIsCommandFollow(bool val) +{ + _isCommandFollow = val; +} + +bool CharmInfo::IsCommandFollow() +{ + return _isCommandFollow; +} + +void CharmInfo::SaveStayPosition() +{ + //! At this point a new spline destination is enabled because of Unit::StopMoving() + G3D::Vector3 stayPos = _unit->movespline->FinalDestination(); + + if (_unit->movespline->onTransport) + if (TransportBase* transport = _unit->GetDirectTransport()) + transport->CalculatePassengerPosition(stayPos.x, stayPos.y, stayPos.z); + + _stayX = stayPos.x; + _stayY = stayPos.y; + _stayZ = stayPos.z; +} + +void CharmInfo::GetStayPosition(float &x, float &y, float &z) +{ + x = _stayX; + y = _stayY; + z = _stayZ; +} + +void CharmInfo::SetIsAtStay(bool val) +{ + _isAtStay = val; +} + +bool CharmInfo::IsAtStay() +{ + return _isAtStay; +} + +void CharmInfo::SetIsFollowing(bool val) +{ + _isFollowing = val; +} + +bool CharmInfo::IsFollowing() +{ + return _isFollowing; +} + +void CharmInfo::SetIsReturning(bool val) +{ + _isReturning = val; +} + +bool CharmInfo::IsReturning() +{ + return _isReturning; +} + +void Unit::SetOrientationTowards(WorldObject const* target) +{ + SetOrientation(GetAngle(target)); +} + +void Unit::SetFacingTo(float ori, bool force) +{ + // do not face when already moving + if (!force && (!IsStopped() || !movespline->Finalized())) + return; + + Movement::MoveSplineInit init(this); + init.MoveTo(GetPositionX(), GetPositionY(), GetPositionZ(), false); + if (GetTransport()) + init.DisableTransportPathTransformations(); // It makes no sense to target global orientation + init.SetFacing(ori); + init.Launch(); + UpdateSplineMovement(1); +} + +void Unit::SetFacingToObject(WorldObject const* object, bool force) +{ + // do not face when already moving + if (!force && (!IsStopped() || !movespline->Finalized())) + return; + + /// @todo figure out under what conditions creature will move towards object instead of facing it where it currently is. + Movement::MoveSplineInit init(this); + init.MoveTo(GetPositionX(), GetPositionY(), GetPositionZ(), false); + init.SetFacing(GetAngle(object)); // when on transport, GetAngle will still return global coordinates (and angle) that needs transforming + init.Launch(); + UpdateSplineMovement(1); +} + +void Unit::SetFacingToPoint(Position const& point, bool force) +{ + // do not face when already moving + if (!force && (!IsStopped() || !movespline->Finalized())) + return; + + /// @todo figure out under what conditions creature will move towards object instead of facing it where it currently is. + Movement::MoveSplineInit init(this); + init.MoveTo(GetPositionX(), GetPositionY(), GetPositionZ(), false); + if (GetTransport()) + init.DisableTransportPathTransformations(); // It makes no sense to target global orientation + init.SetFacing(point.GetPositionX(), point.GetPositionY(), point.GetPositionZ()); + init.Launch(); + UpdateSplineMovement(1); +} + +bool Unit::SetWalk(bool enable) +{ + if (enable == IsWalking()) + return false; + + if (enable) + AddUnitMovementFlag(MOVEMENTFLAG_WALKING); + else + RemoveUnitMovementFlag(MOVEMENTFLAG_WALKING); + + ///@ TODO: Find proper opcode for walk mode setting in player mind controlling a player case + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_WALK_MODE, SMSG_SPLINE_MOVE_SET_WALK_MODE).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_RUN_MODE, SMSG_SPLINE_MOVE_SET_RUN_MODE).Send(); + + return true; +} + +bool Unit::SetDisableGravity(bool disable, bool /*updateAnimTier = true*/) +{ + if (disable == IsGravityDisabled()) + return false; + + if (disable) + { + AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING | MOVEMENTFLAG_SPLINE_ELEVATION); + SetFall(false); + } + else + { + RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + if (!HasUnitMovementFlag(MOVEMENTFLAG_CAN_FLY)) + SetFall(true); + } + + if (IsAlive()) + { + if (IsGravityDisabled() || IsHovering()) + SetPlayHoverAnim(true); + else + SetPlayHoverAnim(false); + } + else if (IsPlayer()) // To update player who dies while flying/hovering + SetPlayHoverAnim(false, false); + + return true; +} + +bool Unit::SetFall(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_FALLING)) + return false; + + if (enable) + { + AddUnitMovementFlag(MOVEMENTFLAG_FALLING); + m_movementInfo.SetFallTime(0); + } + else + RemoveUnitMovementFlag(MOVEMENTFLAG_FALLING | MOVEMENTFLAG_FALLING_FAR); + + return true; +} + +bool Unit::SetSwim(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING)) + return false; + + if (enable) + AddUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + else + RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_START_SWIM, NULL_OPCODE).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_STOP_SWIM, NULL_OPCODE).Send(); + + return true; +} + +bool Unit::SetCanFly(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_CAN_FLY)) + return false; + + if (enable) + { + AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); + RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING | MOVEMENTFLAG_SPLINE_ELEVATION); + SetFall(false); + } + else + { + if (IsFlying() && !IsGravityDisabled()) + SetFall(true); + RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY | MOVEMENTFLAG_MASK_MOVING_FLY); + } + + return true; +} + +bool Unit::SetCanTransitionBetweenSwimAndFly(bool enable) +{ + if (enable == HasExtraUnitMovementFlag(MOVEMENTFLAG2_CAN_SWIM_TO_FLY_TRANS)) + return false; + + if (enable) + AddExtraUnitMovementFlag(MOVEMENTFLAG2_CAN_SWIM_TO_FLY_TRANS); + else + RemoveExtraUnitMovementFlag(MOVEMENTFLAG2_CAN_SWIM_TO_FLY_TRANS); + + return true; +} + +bool Unit::SetWaterWalking(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING)) + return false; + + if (enable) + AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + else + RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_WATER_WALK, SMSG_MOVE_WATER_WALK).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_LAND_WALK, SMSG_MOVE_LAND_WALK).Send(); + + return true; +} + +bool Unit::SetFeatherFall(bool enable) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_FALLING_SLOW)) + return false; + + if (enable) + AddUnitMovementFlag(MOVEMENTFLAG_FALLING_SLOW); + else + RemoveUnitMovementFlag(MOVEMENTFLAG_FALLING_SLOW); + + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_FEATHER_FALL, SMSG_MOVE_FEATHER_FALL).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_NORMAL_FALL, SMSG_MOVE_NORMAL_FALL).Send(); + + return true; +} + +bool Unit::SetHover(bool enable, bool /*updateAnimTier = true*/) +{ + if (enable == HasUnitMovementFlag(MOVEMENTFLAG_HOVER)) + return false; + + float hoverHeight = GetFloatValue(UNIT_FIELD_HOVERHEIGHT); + + if (enable) + { + //! No need to check height on ascent + AddUnitMovementFlag(MOVEMENTFLAG_HOVER); + if (hoverHeight && GetPositionZ() - GetFloorZ() < hoverHeight) + UpdateHeight(GetPositionZ() + hoverHeight); + } + else + { + RemoveUnitMovementFlag(MOVEMENTFLAG_HOVER); + //! Dying creatures will MoveFall from setDeathState + if (hoverHeight && (!isDying() || GetTypeId() != TYPEID_UNIT)) + { + float newZ = std::max(GetFloorZ(), GetPositionZ() - hoverHeight); + UpdateAllowedPositionZ(GetPositionX(), GetPositionY(), newZ); + UpdateHeight(newZ); + } + } + + if (enable) + Movement::PacketSender(this, SMSG_SPLINE_MOVE_SET_HOVER, SMSG_MOVE_SET_HOVER).Send(); + else + Movement::PacketSender(this, SMSG_SPLINE_MOVE_UNSET_HOVER, SMSG_MOVE_UNSET_HOVER).Send(); + + if (IsAlive()) + { + if (IsGravityDisabled() || IsHovering()) + SetPlayHoverAnim(true); + else + SetPlayHoverAnim(false); + } + else if (IsPlayer()) // To update player who dies while flying/hovering + SetPlayHoverAnim(false, false); + + return true; +} + +void Unit::SendSetVehicleRecId(uint32 vehicleId) +{ + WorldPackets::Vehicle::SetVehicleRecID setVehicleRec; + setVehicleRec.VehicleGUID = GetGUID(); + setVehicleRec.VehicleRecID = vehicleId; + SendMessageToSet(setVehicleRec.Write(), true); +} + +void Unit::SetPlayHoverAnim(bool enable, bool sendUpdate /*= true*/) +{ + if (IsPlayingHoverAnim() == enable) + return; + + m_updateFlag.PlayerHoverAnim = enable; + + if (!sendUpdate) + return; + + WorldPackets::Misc::SetPlayHoverAnim packet; + packet.UnitGUID = GetGUID(); + packet.PlayHoverAnim = enable; + SendMessageToSet(packet.Write(), true); +} + +bool Unit::IsSplineEnabled() const +{ + return movespline->Initialized() && !movespline->Finalized(); +} + +void Unit::BuildValuesUpdate(uint8 updateType, ByteBuffer* data, Player* target) const +{ + if (!target) + return; + + ByteBuffer fieldBuffer; + + uint32 valCount = m_valuesCount; + + uint32* flags = UnitUpdateFieldFlags; + uint32 visibleFlag = UF_FLAG_PUBLIC; + + if (target == this) + visibleFlag |= UF_FLAG_PRIVATE; + else if (GetTypeId() == TYPEID_PLAYER) + valCount = PLAYER_END_NOT_SELF; + + UpdateMaskPacketBuilder updateMask(valCount); + + Player* plr = GetCharmerOrOwnerPlayerOrPlayerItself(); + if (GetOwnerGUID() == target->GetGUID()) + visibleFlag |= UF_FLAG_OWNER; + + if (HasFlag(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_SPECIALINFO)) + if (HasAuraTypeWithCaster(SPELL_AURA_EMPATHY, target->GetGUID())) + visibleFlag |= UF_FLAG_SPECIAL_INFO; + + if (plr && plr->IsInSameRaidWith(target)) + visibleFlag |= UF_FLAG_PARTY_MEMBER; + + if (IsCreature()) + visibleFlag |= UF_FLAG_UNIT_ALL; + + Creature const* creature = ToCreature(); + for (uint16 index = 0; index < valCount; ++index) + { + if (_fieldNotifyFlags & flags[index] || + ((flags[index] & visibleFlag) & UF_FLAG_SPECIAL_INFO) || + ((updateType == UPDATETYPE_VALUES ? _changesMask.GetBit(index) : m_uint32Values[index]) && (flags[index] & visibleFlag)) || + (index == UNIT_FIELD_AURASTATE && HasFlag(UNIT_FIELD_AURASTATE, PER_CASTER_AURA_STATE_MASK))) + { + updateMask.SetBit(index); + + if (index == UNIT_NPC_FLAGS) + { + uint32 appendValue = m_uint32Values[UNIT_NPC_FLAGS]; + + if (creature) + { + if (!target->CanSeeSpellClickOn(creature)) + appendValue &= ~UNIT_NPC_FLAG_SPELLCLICK; + + if (!creature->IsClassTrainerOf(target)) + appendValue &= ~UNIT_NPC_FLAG_TRAINER_CLASS; + } + + fieldBuffer << uint32(appendValue); + } + else if (index == UNIT_FIELD_AURASTATE) + { + // Check per caster aura states to not enable using a spell in client if specified aura is not by target + fieldBuffer << BuildAuraStateUpdateForTarget(target); + } + // FIXME: Some values at server stored in float format but must be sent to client in uint32 format + else if (index >= UNIT_FIELD_BASEATTACKTIME && index <= UNIT_FIELD_RANGEDATTACKTIME) + { + // convert from float to uint32 and send + fieldBuffer << uint32(m_floatValues[index] < 0 ? 0 : m_floatValues[index]); + } + // there are some float values which may be negative or can't get negative due to other checks + else if ((index >= UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE && index <= (UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 6)) || + (index >= UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE && index <= (UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 6))) + { + fieldBuffer << uint32(m_floatValues[index]); + } + // Gamemasters should be always able to select units - remove not selectable flag + else if (index == UNIT_FIELD_FLAGS) + { + uint32 appendValue = m_uint32Values[UNIT_FIELD_FLAGS]; + if (target->IsGameMaster()) + appendValue &= ~UNIT_FLAG_NOT_SELECTABLE; + + fieldBuffer << uint32(appendValue); + } + // use modelid_a if not gm, _h if gm for CREATURE_FLAG_EXTRA_TRIGGER creatures + else if (index == UNIT_FIELD_DISPLAYID) + { + uint32 displayId = m_uint32Values[UNIT_FIELD_DISPLAYID]; + if (creature) + { + CreatureTemplate const* cinfo = creature->GetCreatureTemplate(); + + // this also applies for transform auras + if (SpellInfo const* transform = sSpellMgr->GetSpellInfo(getTransForm())) + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (transform->Effects[i].IsAura(SPELL_AURA_TRANSFORM)) + if (CreatureTemplate const* transformInfo = sObjectMgr->GetCreatureTemplate(transform->Effects[i].MiscValue)) + { + cinfo = transformInfo; + break; + } + + if (cinfo->flags_extra & CREATURE_FLAG_EXTRA_TRIGGER) + if (target->IsGameMaster()) + displayId = cinfo->GetFirstVisibleModel()->CreatureDisplayID; + } + + fieldBuffer << uint32(displayId); + } + // hide lootable animation for unallowed players + else if (index == UNIT_DYNAMIC_FLAGS) + { + uint32 dynamicFlags = m_uint32Values[UNIT_DYNAMIC_FLAGS] & ~(UNIT_DYNFLAG_TAPPED | UNIT_DYNFLAG_TAPPED_BY_PLAYER); + + if (creature) + { + if (creature->hasLootRecipient()) + { + dynamicFlags |= UNIT_DYNFLAG_TAPPED; + if (creature->isTappedBy(target)) + dynamicFlags |= UNIT_DYNFLAG_TAPPED_BY_PLAYER; + } + + if (!target->isAllowedToLoot(creature)) + dynamicFlags &= ~UNIT_DYNFLAG_LOOTABLE; + } + + // unit UNIT_DYNFLAG_TRACK_UNIT should only be sent to caster of SPELL_AURA_MOD_STALKED auras + if (dynamicFlags & UNIT_DYNFLAG_TRACK_UNIT) + if (!HasAuraTypeWithCaster(SPELL_AURA_MOD_STALKED, target->GetGUID())) + dynamicFlags &= ~UNIT_DYNFLAG_TRACK_UNIT; + + fieldBuffer << dynamicFlags; + } + // FG: pretend that OTHER players in own group are friendly ("blue") + else if (index == UNIT_FIELD_BYTES_2 || index == UNIT_FIELD_FACTIONTEMPLATE) + { + if (IsControlledByPlayer() && target != this && sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GROUP) && IsInRaidWith(target)) + { + FactionTemplateEntry const* ft1 = GetFactionTemplateEntry(); + FactionTemplateEntry const* ft2 = target->GetFactionTemplateEntry(); + if (ft1 && ft2 && !ft1->IsFriendlyTo(ft2)) + { + if (index == UNIT_FIELD_BYTES_2) + // Allow targetting opposite faction in party when enabled in config + fieldBuffer << (m_uint32Values[UNIT_FIELD_BYTES_2] & ((UNIT_BYTE2_FLAG_SANCTUARY /*| UNIT_BYTE2_FLAG_AURAS | UNIT_BYTE2_FLAG_UNK5*/) << 8)); // this flag is at uint8 offset 1 !! + else + // pretend that all other HOSTILE players have own faction, to allow follow, heal, rezz (trade wont work) + fieldBuffer << uint32(target->GetFaction()); + } + else + fieldBuffer << m_uint32Values[index]; + } + else + fieldBuffer << m_uint32Values[index]; + } + else + { + // send in current format (float as float, uint32 as uint32) + fieldBuffer << m_uint32Values[index]; + } + } + } + + updateMask.AppendToPacket(data); + data->append(fieldBuffer); +} + +void Unit::DestroyForPlayer(Player* target, bool /*onDeath = false*/) const +{ + if (Battleground* bg = target->GetBattleground()) + { + if (bg->isArena()) + { + WorldPacket data(SMSG_ARENA_UNIT_DESTROYED, 8); + data << GetGUID(); + target->SendDirectMessage(&data); + } + } + + WorldObject::DestroyForPlayer(target); +} + +int32 Unit::GetHighestExclusiveSameEffectSpellGroupValue(AuraEffect const* aurEff, AuraType auraType, bool checkMiscValue /*= false*/, int32 miscValue /*= 0*/) const +{ + int32 val = 0; + SpellSpellGroupMapBounds spellGroup = sSpellMgr->GetSpellSpellGroupMapBounds(aurEff->GetSpellInfo()->GetFirstRankSpell()->Id); + for (SpellSpellGroupMap::const_iterator itr = spellGroup.first; itr != spellGroup.second ; ++itr) + { + if (sSpellMgr->GetSpellGroupStackRule(itr->second) == SPELL_GROUP_STACK_RULE_EXCLUSIVE_SAME_EFFECT) + { + AuraEffectList const& auraEffList = GetAuraEffectsByType(auraType); + for (AuraEffectList::const_iterator auraItr = auraEffList.begin(); auraItr != auraEffList.end(); ++auraItr) + { + if (aurEff != (*auraItr) && (!checkMiscValue || (*auraItr)->GetMiscValue() == miscValue) && + sSpellMgr->IsSpellMemberOfSpellGroup((*auraItr)->GetSpellInfo()->Id, itr->second)) + { + // absolute value only + if (abs(val) < abs((*auraItr)->GetAmount())) + val = (*auraItr)->GetAmount(); + } + } + } + } + return val; +} + +bool Unit::IsHighestExclusiveAura(Aura const* aura, bool removeOtherAuraApplications /*= false*/) +{ + for (uint32 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (AuraEffect const* aurEff = aura->GetEffect(i)) + if (!IsHighestExclusiveAuraEffect(aura->GetSpellInfo(), aurEff->GetAuraType(), aurEff->GetAmount(), aura->GetEffectMask(), removeOtherAuraApplications)) + return false; + + return true; +} + +bool Unit::IsHighestExclusiveAuraEffect(SpellInfo const* spellInfo, AuraType auraType, int32 effectAmount, uint8 auraEffectMask, bool removeOtherAuraApplications /*= false*/) +{ + AuraEffectList const& auras = GetAuraEffectsByType(auraType); + for (Unit::AuraEffectList::const_iterator itr = auras.begin(); itr != auras.end();) + { + AuraEffect const* existingAurEff = (*itr); + ++itr; + + if (sSpellMgr->CheckSpellGroupStackRules(spellInfo, existingAurEff->GetSpellInfo()) == SPELL_GROUP_STACK_RULE_EXCLUSIVE_HIGHEST) + { + int32 diff = abs(effectAmount) - abs(existingAurEff->GetAmount()); + if (!diff) + for (int32 i = 0; i < MAX_SPELL_EFFECTS; ++i) + diff += int32((auraEffectMask & (1 << i)) >> i) - int32((existingAurEff->GetBase()->GetEffectMask() & (1 << i)) >> i); + + if (diff > 0) + { + Aura const* base = existingAurEff->GetBase(); + // no removing of area auras from the original owner, as that completely cancels them + if (removeOtherAuraApplications && (!base->IsArea() || base->GetOwner() != this)) + { + if (AuraApplication* aurApp = existingAurEff->GetBase()->GetApplicationOfTarget(GetGUID())) + { + bool hasMoreThanOneEffect = base->HasMoreThanOneEffectForType(auraType); + uint32 removedAuras = m_removedAurasCount; + RemoveAura(aurApp); + if (hasMoreThanOneEffect || m_removedAurasCount > removedAuras + 1) + itr = auras.begin(); + } + } + } + else if (diff < 0) + return false; + } + } + + return true; +} + +void Unit::Talk(std::string const& text, ChatMsg msgType, Language language, float textRange, WorldObject const* target) +{ + Trinity::CustomChatTextBuilder builder(this, msgType, text, language, target); + Trinity::LocalizedPacketDo localizer(builder); + Trinity::PlayerDistWorker > worker(this, textRange, localizer); + Cell::VisitWorldObjects(this, worker, textRange); +} + +void Unit::Say(std::string const& text, Language language, WorldObject const* target /*= nullptr*/) +{ + Talk(text, CHAT_MSG_MONSTER_SAY, language, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY), target); +} + +void Unit::Yell(std::string const& text, Language language, WorldObject const* target /*= nullptr*/) +{ + Talk(text, CHAT_MSG_MONSTER_YELL, language, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_YELL), target); +} + +void Unit::TextEmote(std::string const& text, WorldObject const* target /*= nullptr*/, bool isBossEmote /*= false*/) +{ + Talk(text, isBossEmote ? CHAT_MSG_RAID_BOSS_EMOTE : CHAT_MSG_MONSTER_EMOTE, LANG_UNIVERSAL, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_TEXTEMOTE), target); +} + +void Unit::Whisper(std::string const& text, Language language, Player* target, bool isBossWhisper /*= false*/) +{ + if (!target) + return; + + LocaleConstant locale = target->GetSession()->GetSessionDbLocaleIndex(); + WorldPacket data; + ChatHandler::BuildChatPacket(data, isBossWhisper ? CHAT_MSG_RAID_BOSS_WHISPER : CHAT_MSG_MONSTER_WHISPER, language, this, target, text, 0, "", locale); + target->SendDirectMessage(&data); +} + +void Unit::Talk(uint32 textId, ChatMsg msgType, float textRange, WorldObject const* target) +{ + if (!sObjectMgr->GetBroadcastText(textId)) + { + TC_LOG_ERROR("entities.unit", "WorldObject::MonsterText: `broadcast_text` (ID: %u) was not found", textId); + return; + } + + Trinity::BroadcastTextBuilder builder(this, msgType, textId, getGender(), target); + Trinity::LocalizedPacketDo localizer(builder); + Trinity::PlayerDistWorker > worker(this, textRange, localizer); + Cell::VisitWorldObjects(this, worker, textRange); +} + +void Unit::Say(uint32 textId, WorldObject const* target /*= nullptr*/) +{ + Talk(textId, CHAT_MSG_MONSTER_SAY, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY), target); +} + +void Unit::Yell(uint32 textId, WorldObject const* target /*= nullptr*/) +{ + Talk(textId, CHAT_MSG_MONSTER_YELL, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_YELL), target); +} + +void Unit::TextEmote(uint32 textId, WorldObject const* target /*= nullptr*/, bool isBossEmote /*= false*/) +{ + Talk(textId, isBossEmote ? CHAT_MSG_RAID_BOSS_EMOTE : CHAT_MSG_MONSTER_EMOTE, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_TEXTEMOTE), target); +} + +void Unit::Whisper(uint32 textId, Player* target, bool isBossWhisper /*= false*/) +{ + if (!target) + return; + + BroadcastText const* bct = sObjectMgr->GetBroadcastText(textId); + if (!bct) + { + TC_LOG_ERROR("entities.unit", "WorldObject::MonsterWhisper: `broadcast_text` was not %u found", textId); + return; + } + + LocaleConstant locale = target->GetSession()->GetSessionDbLocaleIndex(); + WorldPacket data; + ChatHandler::BuildChatPacket(data, isBossWhisper ? CHAT_MSG_RAID_BOSS_WHISPER : CHAT_MSG_MONSTER_WHISPER, LANG_UNIVERSAL, this, target, bct->GetText(locale, getGender()), 0, "", locale); + target->SendDirectMessage(&data); +} + +// Returns collisionheight of the unit. If it is 0, it returns DEFAULT_COLLISION_HEIGHT. +float Unit::GetCollisionHeight() const +{ + float scaleMod = GetObjectScale(); // 99% sure about this + + if (IsMounted()) + { + if (CreatureDisplayInfoEntry const* mountDisplayInfo = sCreatureDisplayInfoStore.LookupEntry(GetUInt32Value(UNIT_FIELD_MOUNTDISPLAYID))) + { + if (CreatureModelDataEntry const* mountModelData = sCreatureModelDataStore.LookupEntry(mountDisplayInfo->ModelID)) + { + CreatureDisplayInfoEntry const* displayInfo = sCreatureDisplayInfoStore.AssertEntry(GetNativeDisplayId()); + CreatureModelDataEntry const* modelData = sCreatureModelDataStore.AssertEntry(displayInfo->ModelID); + float const collisionHeight = scaleMod * ((mountModelData->MountHeight * mountDisplayInfo->CreatureModelScale) + (modelData->CollisionHeight * modelData->ModelScale * displayInfo->CreatureModelScale * 0.5f)); + return collisionHeight == 0.0f ? DEFAULT_COLLISION_HEIGHT : collisionHeight; + } + } + } + + //! Dismounting case - use basic default model data + CreatureDisplayInfoEntry const* displayInfo = sCreatureDisplayInfoStore.AssertEntry(GetNativeDisplayId()); + CreatureModelDataEntry const* modelData = sCreatureModelDataStore.AssertEntry(displayInfo->ModelID); + + float const collisionHeight = scaleMod * modelData->CollisionHeight * modelData->ModelScale * displayInfo->CreatureModelScale; + return collisionHeight == 0.0f ? DEFAULT_COLLISION_HEIGHT : collisionHeight; +} + +SpellInfo const* Unit::GetCastSpellInfo(SpellInfo const* spellInfo, TriggerCastFlags& triggerFlag) const +{ + Unit::AuraEffectList const& swaps = GetAuraEffectsByType(SPELL_AURA_OVERRIDE_ACTIONBAR_SPELLS); + Unit::AuraEffectList const& swaps2 = GetAuraEffectsByType(SPELL_AURA_OVERRIDE_ACTIONBAR_SPELLS_TRIGGERED); + + for (AuraEffect const* auraEffect : swaps) + { + if (uint32(auraEffect->GetMiscValue()) == spellInfo->Id || auraEffect->IsAffectingSpell(spellInfo)) + { + if (SpellInfo const* newInfo = sSpellMgr->GetSpellInfo(auraEffect->GetAmount())) + { + if (auraEffect->GetSpellInfo()->HasAttribute(SPELL_ATTR8_IGNORE_SPELLCAST_OVERRIDE_COST)) + triggerFlag |= TRIGGERED_IGNORE_POWER_COST; + else + triggerFlag &= ~TRIGGERED_IGNORE_POWER_COST; + + return newInfo; + } + } + } + + for (AuraEffect const* auraEffect : swaps2) + { + if (uint32(auraEffect->GetMiscValue()) == spellInfo->Id || auraEffect->IsAffectingSpell(spellInfo)) + { + if (SpellInfo const* newInfo = sSpellMgr->GetSpellInfo(auraEffect->GetAmount())) + { + if (auraEffect->GetSpellInfo()->HasAttribute(SPELL_ATTR8_IGNORE_SPELLCAST_OVERRIDE_COST)) + triggerFlag |= TRIGGERED_IGNORE_POWER_COST; + else + triggerFlag &= ~TRIGGERED_IGNORE_POWER_COST; + + return newInfo; + } + } + } + + return spellInfo; +} + +bool Unit::CanExecutePendingSpellCastRequest(SpellInfo const* spellInfo) const +{ + // Generic and melee spells have to wait, channeled spells can be processed immediately. + if (!GetCurrentSpell(CURRENT_CHANNELED_SPELL) && HasUnitState(UNIT_STATE_CASTING)) + return false; + + // Waiting for the global cooldown to expire + if (GetSpellHistory()->GetRemainingGlobalCooldown(spellInfo) > 0) + return false; + + return true; +} + +void Unit::RequestSpellCast(std::unique_ptr castRequest, SpellInfo const* spellInfo) +{ + // We are overriding an already existing spell cast request so inform the client that the old cast is being replaced + if (_pendingSpellCastRequest) + CancelPendingCastRequest(); + + _pendingSpellCastRequest = std::move(castRequest); + + // If we can process the cast request right now, do it. + if (CanExecutePendingSpellCastRequest(spellInfo)) + ProcessPendingSpellCastRequest(); +} + +void Unit::CancelPendingCastRequest() +{ + if (!_pendingSpellCastRequest) + return; + + // We have to inform the client that the cast has been canceled to avoid the cast button to stay highlightened + if (IsPlayer()) + { + // @todo: convert to packet class + WorldPacket data(SMSG_CAST_FAILED, 1 + 4 + 1); + data << uint8(_pendingSpellCastRequest->CastRequest.CastID); + data << uint32(_pendingSpellCastRequest->CastRequest.SpellID); + data << uint8(SPELL_FAILED_DONT_REPORT); + ToPlayer()->SendDirectMessage(&data); + } + + _pendingSpellCastRequest = nullptr; +} + +// A spell can be queued up within 400 milliseconds before global cooldown expires or the cast finishes +static constexpr uint16 const SPELL_QUEUE_TIME_WINDOW = 400; + +bool Unit::CanRequestSpellCast(SpellInfo const* spellInfo) const +{ + if (GetSpellHistory()->GetRemainingGlobalCooldown(spellInfo) > SPELL_QUEUE_TIME_WINDOW) + return false; + + for (CurrentSpellTypes spellSlot : { CURRENT_MELEE_SPELL, CURRENT_GENERIC_SPELL }) + if (Spell const* spell = GetCurrentSpell(spellSlot)) + if (spell->GetRemainingCastTime() > SPELL_QUEUE_TIME_WINDOW) + return false; + + return true; +} + +void Unit::ProcessPendingSpellCastRequest() +{ + if (!_pendingSpellCastRequest) + return; + + // Sanity check. If the player requested an invalid spell cast, just skip the request. + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(_pendingSpellCastRequest->CastRequest.SpellID); + if (!spellInfo) + { + _pendingSpellCastRequest = nullptr; + return; + } + + // Waiting for ongoing casts to finish and the global cooldown to expire. + if (!CanExecutePendingSpellCastRequest(spellInfo)) + return; + + // Spell can be executed now. Check data and prepare cast. + Unit* caster = this; + + // 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()) + { + ProcessItemCast(*_pendingSpellCastRequest, targets); + _pendingSpellCastRequest = nullptr; + return; + } + + if (caster->IsCreature() && !caster->ToCreature()->HasSpell(spellInfo->Id)) + { + // If the vehicle creature does not have the spell but it allows the passenger to cast own spells + // change caster to player and let him cast + + Unit* charmer = caster->GetCharmer(); + if (charmer && (!charmer->IsOnVehicle(this) || spellInfo->CheckVehicle(charmer) != SPELL_CAST_OK)) + return; + else if (charmer) + caster = charmer; + else + { + TC_LOG_ERROR("spells", "Creature %s tried to allow its charmer to cast spell %u but there is no charmer.", caster->GetGUID().ToString().c_str(), spellInfo->Id); + return; + } + } + + TriggerCastFlags triggerFlag = TRIGGERED_NONE; + + if (caster->GetTypeId() == TYPEID_PLAYER && !caster->ToPlayer()->HasActiveSpell(spellInfo->Id) && !spellInfo->HasAttribute(SPELL_ATTR8_SKIP_IS_KNOWN_CHECK) && + !caster->ToPlayer()->HasArchProject(static_cast(spellInfo->ResearchProjectId))) + { + bool allow = false; + + // allow casting of unknown spells for special lock cases + if (GameObject *go = targets.GetGOTarget()) + if (go->GetSpellForLock(caster->ToPlayer()) == spellInfo) + allow = true; + + // TODO: Preparation for #23204 + // allow casting of spells triggered by clientside periodic trigger auras + /* + if (caster->HasAuraTypeWithTriggerSpell(SPELL_AURA_PERIODIC_TRIGGER_SPELL_FROM_CLIENT, spellId)) + { + allow = true; + triggerFlag = TRIGGERED_FULL_MASK; + } + */ + + if (!allow) + { + _pendingSpellCastRequest = nullptr; + return; + } + } + + // Check possible spell cast overrides + spellInfo = caster->GetCastSpellInfo(spellInfo, triggerFlag); + + // can't use our own spells when we're in possession of another unit, + if (caster->isPossessing()) + { + _pendingSpellCastRequest = nullptr; + return; + } + + // Client is resending autoshot cast opcode when other spell is cast during shoot rotation + // Skip it to prevent "interrupt" message + // Also check targets! target may have changed and we need to interrupt current spell + if (spellInfo->IsAutoRepeatRangedSpell()) + { + if (Spell* spell = caster->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL)) + { + if (spell->m_spellInfo == spellInfo && spell->m_targets.GetUnitTargetGUID() == targets.GetUnitTargetGUID()) + { + _pendingSpellCastRequest.reset(); + return; + } + } + } + + // auto-selection buff level base at target level (in spellInfo) + // TODO: is this even necessary? client already seems to send correct rank for "standard" buffs + if (spellInfo->IsPositive()) + { + if (Unit* target = targets.GetUnitTarget()) + { + SpellInfo const* actualSpellInfo = spellInfo->GetAuraRankForLevel(target->getLevel()); + + // if rank not found then function return NULL but in explicit cast case original spell can be cast and later failed with appropriate error message + if (actualSpellInfo) + spellInfo = actualSpellInfo; + } + } + + if (!_pendingSpellCastRequest->CastRequest.Weight.empty()) + { + ArchData archaeologyCastData; + for (WorldPackets::Spells::SpellWeight const& weight : _pendingSpellCastRequest->CastRequest.Weight) + { + switch (weight.Type) + { + case 1: // Currency + archaeologyCastData.FragId = weight.ID; + archaeologyCastData.FragCount = weight.Quantity; + break; + case 2: // Item + archaeologyCastData.KeyId = weight.ID; + archaeologyCastData.KeyCount = weight.Quantity; + break; + default: + break; + } + } + + if (Player* player = caster->ToPlayer()) + player->SetArchData(archaeologyCastData); + } + + Spell* spell = new Spell(caster, spellInfo, triggerFlag); + spell->m_cast_count = _pendingSpellCastRequest->CastRequest.CastID; // set count of casts + spell->m_glyphIndex = _pendingSpellCastRequest->CastRequest.Misc; + spell->prepare(targets); + + _pendingSpellCastRequest.reset(); +} + +void Unit::ProcessItemCast(PendingSpellCastRequest const& castRequest, SpellCastTargets const& targets) +{ + Player* player = ToPlayer(); + if (!player || !castRequest.CastItemData.has_value()) + return; + + Item* item = player->GetUseableItemByPos(castRequest.CastItemData->BagSlot, castRequest.CastItemData->Slot); + if (!item) + { + player->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr); + return; + } + + if (item->GetGUID() != castRequest.CastItemData->CastItem) + { + player->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr); + return; + } + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + { + player->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, item, nullptr); + return; + } + + // some item classes can be used only in equipped state + if (proto->GetInventoryType() != INVTYPE_NON_EQUIP && !item->IsEquipped()) + { + player->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, item, nullptr); + return; + } + + InventoryResult msg = player->CanUseItem(item); + if (msg != EQUIP_ERR_OK) + { + player->SendEquipError(msg, item, nullptr); + return; + } + + // only allow conjured consumable, bandage, poisons (all should have the 2^21 item flag set in DB) + if (proto->GetClass() == ITEM_CLASS_CONSUMABLE && !(proto->GetFlags() & ITEM_FLAG_IGNORE_DEFAULT_ARENA_RESTRICTIONS) && player->InArena()) + { + player->SendEquipError(EQUIP_ERR_NOT_DURING_ARENA_MATCH, item, nullptr); + return; + } + + // don't allow items banned in arena + if ((proto->GetFlags() & ITEM_FLAG_NOT_USEABLE_IN_ARENA) && player->InArena()) + { + player->SendEquipError(EQUIP_ERR_NOT_DURING_ARENA_MATCH, item, nullptr); + return; + } + + if (player->IsInCombat()) + { + for (ItemEffect const& effect : proto->Effects) + { + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect.SpellID)) + { + if (!spellInfo->CanBeUsedInCombat()) + { + player->SendEquipError(EQUIP_ERR_NOT_IN_COMBAT, item, nullptr); + return; + } + } + } + } + + // check also BIND_ON_ACQUIRE and BIND_QUEST for .additem or .additemset case by GM (not binded at adding to inventory) + if (item->GetTemplate()->GetBonding() == BIND_ON_USE || item->GetTemplate()->GetBonding() == BIND_ON_ACQUIRE || item->GetTemplate()->GetBonding() == BIND_QUEST) + { + if (!item->IsSoulBound()) + { + item->SetState(ITEM_CHANGED, player); + item->SetBinding(true); + } + } + + player->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::ItemUse); + + // Note: If script stop casting it must send appropriate data to client to prevent stuck item in gray state. + if (!sScriptMgr->OnItemUse(player, item, targets)) + { + // no script or script not process request by self + player->CastItemUseSpell(item, targets, castRequest.CastRequest.CastID, castRequest.CastRequest.Misc); + } +} + +void Unit::SetGameClientMovingMe(GameClient* gameClientMovingMe) +{ + _gameClientMovingMe = gameClientMovingMe; +} + +Stats& Unit::GetStats() const +{ + return *_stats; +} + +int32 Unit::GetStat(StatType statType) const +{ + if (statType < StatType::Strength) + return 0; + + return GetInt32Value(UNIT_FIELD_STAT0 + AsUnderlyingType(statType)); +} diff --git a/src/server/game/Handlers/ChatHandler.cpp b/src/server/game/Handlers/ChatHandler.cpp index 70a92268fc..9dec043982 100644 --- a/src/server/game/Handlers/ChatHandler.cpp +++ b/src/server/game/Handlers/ChatHandler.cpp @@ -16,6 +16,8 @@ */ #include "WorldSession.h" + +#include #include "AccountMgr.h" #include "CellImpl.h" #include "Channel.h" @@ -647,6 +649,67 @@ void WorldSession::HandleAddonMessagechatOpcode(WorldPacket& recvData) return; } + // VIANO_MOD_BRIDGE_V1 + // Generic Viano client -> server bridge. First module: virtual raid markers. + if (prefix == "VIANO" && type == CHAT_MSG_WHISPER) + { + auto parseRawGuid = [](std::string const& value, ObjectGuid& guid) -> bool + { + if (value.empty()) + return false; + char* end = nullptr; + uint64 raw = std::strtoull(value.c_str(), &end, 0); + if (!end || *end != '\0' || raw == 0) + return false; + guid.Set(raw); + return true; + }; + + std::vector fields; + std::size_t begin = 0; + while (begin <= message.size()) + { + std::size_t split = message.find('|', begin); + if (split == std::string::npos) + { + fields.push_back(message.substr(begin)); + break; + } + fields.push_back(message.substr(begin, split - begin)); + begin = split + 1; + } + + if (fields.size() == 4 && fields[0] == "RM" && fields[1] == "1") + { + char* end = nullptr; + unsigned long iconValue = std::strtoul(fields[2].c_str(), &end, 10); + ObjectGuid guid; + if (end && *end == '\0' && iconValue < 8 && parseRawGuid(fields[3], guid)) + { + Unit* target = ObjectAccessor::GetUnit(*sender, guid); + if (target && target->IsInMap(sender)) + { + sender->SetVianoTargetIcon(uint8(iconValue), guid); + TC_LOG_DEBUG("scripts", "[VianoMod] marker player=%s icon=%lu target=%s", + sender->GetName().c_str(), iconValue, guid.ToString().c_str()); + } + } + return; + } + + if (fields.size() == 3 && fields[0] == "RC" && fields[1] == "1") + { + ObjectGuid guid; + if (parseRawGuid(fields[2], guid)) + sender->ClearVianoTargetIcon(guid); + return; + } + + TC_LOG_DEBUG("scripts", "[VianoMod] malformed message player=%s payload=%s", + sender->GetName().c_str(), message.c_str()); + return; + } + // Disabled addon channel? if (!sWorld->getBoolConfig(CONFIG_ADDON_CHANNEL)) { diff --git a/src/server/game/Handlers/ChatHandler.cpp.bak-vianomod-v1-20261001-195036 b/src/server/game/Handlers/ChatHandler.cpp.bak-vianomod-v1-20261001-195036 new file mode 100644 index 0000000000..70a92268fc --- /dev/null +++ b/src/server/game/Handlers/ChatHandler.cpp.bak-vianomod-v1-20261001-195036 @@ -0,0 +1,860 @@ +/* + * 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 . + */ + +#include "WorldSession.h" +#include "AccountMgr.h" +#include "CellImpl.h" +#include "Channel.h" +#include "ChannelMgr.h" +#include "Chat.h" +#include "ChatPackets.h" +#include "Common.h" +#include "DatabaseEnv.h" +#include "DBCStores.h" +#include "GameTime.h" +#include "GridNotifiersImpl.h" +#include "Group.h" +#include "Guild.h" +#include "GuildMgr.h" +#include "Language.h" +#include "Log.h" +#include "ObjectAccessor.h" +#include "ObjectMgr.h" +#include "Opcodes.h" +#include "Player.h" +#include "ScriptMgr.h" +#include "SpellAuraEffects.h" +#include "Util.h" +#include "World.h" +#include "WorldPacket.h" + +void WorldSession::HandleMessagechatOpcode(WorldPacket& recvData) +{ + uint32 type = 0; + uint32 lang; + + switch (recvData.GetOpcode()) + { + case CMSG_MESSAGECHAT_SAY: + type = CHAT_MSG_SAY; + break; + case CMSG_MESSAGECHAT_YELL: + type = CHAT_MSG_YELL; + break; + case CMSG_MESSAGECHAT_CHANNEL: + type = CHAT_MSG_CHANNEL; + break; + case CMSG_MESSAGECHAT_WHISPER: + type = CHAT_MSG_WHISPER; + break; + case CMSG_MESSAGECHAT_GUILD: + type = CHAT_MSG_GUILD; + break; + case CMSG_MESSAGECHAT_OFFICER: + type = CHAT_MSG_OFFICER; + break; + case CMSG_MESSAGECHAT_AFK: + type = CHAT_MSG_AFK; + break; + case CMSG_MESSAGECHAT_DND: + type = CHAT_MSG_DND; + break; + case CMSG_MESSAGECHAT_EMOTE: + type = CHAT_MSG_EMOTE; + break; + case CMSG_MESSAGECHAT_PARTY: + type = CHAT_MSG_PARTY; + break; + case CMSG_MESSAGECHAT_RAID: + type = CHAT_MSG_RAID; + break; + case CMSG_MESSAGECHAT_BATTLEGROUND: + type = CHAT_MSG_BATTLEGROUND; + break; + case CMSG_MESSAGECHAT_RAID_WARNING: + type = CHAT_MSG_RAID_WARNING; + break; + default: + TC_LOG_ERROR("network", "HandleMessagechatOpcode : Unknown chat opcode (%u)", recvData.GetOpcode()); + recvData.hexlike(); + return; + } + + if (type >= MAX_CHAT_MSG_TYPE) + { + TC_LOG_ERROR("network", "CHAT: Wrong message type received: %u", type); + recvData.rfinish(); + return; + } + + Player* sender = GetPlayer(); + + //TC_LOG_DEBUG("misc", "CHAT: packet received. type %u, lang %u", type, lang); + + // no language sent with emote packet. + if (type != CHAT_MSG_EMOTE && type != CHAT_MSG_AFK && type != CHAT_MSG_DND) + { + recvData >> lang; + + if (lang == LANG_UNIVERSAL) + { + TC_LOG_INFO("entities.player.cheat", "CMSG_MESSAGECHAT: Possible hacking-attempt: %s tried to send a message in universal language", GetPlayerInfo().c_str()); + SendNotification(LANG_UNKNOWN_LANGUAGE); + recvData.rfinish(); + return; + } + + // prevent talking at unknown language (cheating) + LanguageDesc const* langDesc = GetLanguageDescByID(lang); + if (!langDesc) + { + SendNotification(LANG_UNKNOWN_LANGUAGE); + recvData.rfinish(); + return; + } + + if (langDesc->skill_id != 0 && !sender->HasSkill(langDesc->skill_id)) + { + // also check SPELL_AURA_COMPREHEND_LANGUAGE (client offers option to speak in that language) + Unit::AuraEffectList const& langAuras = sender->GetAuraEffectsByType(SPELL_AURA_COMPREHEND_LANGUAGE); + bool foundAura = false; + for (Unit::AuraEffectList::const_iterator i = langAuras.begin(); i != langAuras.end(); ++i) + { + if ((*i)->GetMiscValue() == int32(lang)) + { + foundAura = true; + break; + } + } + if (!foundAura) + { + SendNotification(LANG_NOT_LEARNED_LANGUAGE); + recvData.rfinish(); + return; + } + } + + if (lang == LANG_ADDON) + { + // LANG_ADDON is only valid for the following message types + switch (type) + { + case CHAT_MSG_PARTY: + case CHAT_MSG_RAID: + case CHAT_MSG_GUILD: + case CHAT_MSG_BATTLEGROUND: + case CHAT_MSG_WHISPER: + // check if addon messages are disabled + if (!sWorld->getBoolConfig(CONFIG_ADDON_CHANNEL)) + { + recvData.rfinish(); + return; + } + break; + default: + TC_LOG_ERROR("network", "Player %s (GUID: %u) sent a chatmessage with an invalid language/message type combination", + GetPlayer()->GetName().c_str(), GetPlayer()->GetGUID().GetCounter()); + + recvData.rfinish(); + return; + } + } + // LANG_ADDON should not be changed nor be affected by flood control + else + { + // send in universal language if player in .gm on mode (ignore spell effects) + if (sender->IsGameMaster()) + lang = LANG_UNIVERSAL; + else + { + // send in universal language in two side iteration allowed mode + if (HasPermission(rbac::RBAC_PERM_TWO_SIDE_INTERACTION_CHAT)) + lang = LANG_UNIVERSAL; + else + { + switch (type) + { + case CHAT_MSG_PARTY: + case CHAT_MSG_PARTY_LEADER: + case CHAT_MSG_RAID: + case CHAT_MSG_RAID_LEADER: + case CHAT_MSG_RAID_WARNING: + // allow two side chat at group channel if two side group allowed + if (sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GROUP)) + lang = LANG_UNIVERSAL; + break; + case CHAT_MSG_GUILD: + case CHAT_MSG_OFFICER: + // allow two side chat at guild channel if two side guild allowed + if (sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD)) + lang = LANG_UNIVERSAL; + break; + } + } + + // but overwrite it by SPELL_AURA_MOD_LANGUAGE auras (only single case used) + Unit::AuraEffectList const& ModLangAuras = sender->GetAuraEffectsByType(SPELL_AURA_MOD_LANGUAGE); + if (!ModLangAuras.empty()) + lang = ModLangAuras.front()->GetMiscValue(); + } + + if (!CanSpeak()) + { + std::string timeStr = secsToTimeString(m_muteTime - GameTime::GetGameTime()); + SendNotification(GetTrinityString(LANG_WAIT_BEFORE_SPEAKING), timeStr.c_str()); + recvData.rfinish(); // Prevent warnings + return; + } + + if (type != CHAT_MSG_AFK && type != CHAT_MSG_DND) + sender->UpdateSpeakTime(Player::ChatFloodThrottle::REGULAR); + } + } + else + lang = LANG_UNIVERSAL; + + if (sender->HasAura(1852) && type != CHAT_MSG_WHISPER) + { + SendNotification(GetTrinityString(LANG_GM_SILENCE), sender->GetName().c_str()); + recvData.rfinish(); + return; + } + + uint32 textLength = 0; + uint32 receiverLength = 0; + std::string to, channel, msg; + bool ignoreChecks = false; + switch (type) + { + case CHAT_MSG_SAY: + case CHAT_MSG_EMOTE: + case CHAT_MSG_YELL: + case CHAT_MSG_PARTY: + case CHAT_MSG_GUILD: + case CHAT_MSG_OFFICER: + case CHAT_MSG_RAID: + case CHAT_MSG_RAID_WARNING: + case CHAT_MSG_BATTLEGROUND: + textLength = recvData.ReadBits(9); + msg = recvData.ReadString(textLength); + break; + case CHAT_MSG_WHISPER: + receiverLength = recvData.ReadBits(10); + textLength = recvData.ReadBits(9); + to = recvData.ReadString(receiverLength); + msg = recvData.ReadString(textLength); + break; + case CHAT_MSG_CHANNEL: + receiverLength = recvData.ReadBits(10); + textLength = recvData.ReadBits(9); + msg = recvData.ReadString(textLength); + channel = recvData.ReadString(receiverLength); + break; + case CHAT_MSG_AFK: + case CHAT_MSG_DND: + textLength = recvData.ReadBits(9); + msg = recvData.ReadString(textLength); + ignoreChecks = true; + break; + } + + if (!ignoreChecks) + { + if (msg.empty()) + return; + + if (lang == LANG_ADDON) + { + if (AddonChannelCommandHandler(this).ParseCommands(msg.c_str())) + return; + } + if (lang != LANG_ADDON) + { + if (ChatHandler(this).ParseCommands(msg.c_str())) + return; + // Strip invisible characters for non-addon messages + if (sWorld->getBoolConfig(CONFIG_CHAT_FAKE_MESSAGE_PREVENTING)) + stripLineInvisibleChars(msg); + + if (sWorld->getIntConfig(CONFIG_CHAT_STRICT_LINK_CHECKING_SEVERITY) && !ChatHandler(this).isValidChatMessage(msg.c_str())) + { + TC_LOG_ERROR("network", "Player %s (GUID: %u) sent a chatmessage with an invalid link: %s", GetPlayer()->GetName().c_str(), + GetPlayer()->GetGUID().GetCounter(), msg.c_str()); + + if (sWorld->getIntConfig(CONFIG_CHAT_STRICT_LINK_CHECKING_KICK)) + KickPlayer(); + + return; + } + } + } + + switch (type) + { + case CHAT_MSG_SAY: + { + // Prevent cheating + if (!sender->IsAlive()) + return; + + if (sender->getLevel() < sWorld->getIntConfig(CONFIG_CHAT_SAY_LEVEL_REQ)) + { + SendNotification(GetTrinityString(LANG_SAY_REQ), sWorld->getIntConfig(CONFIG_CHAT_SAY_LEVEL_REQ)); + return; + } + + sender->Say(msg, Language(lang)); + break; + } + case CHAT_MSG_EMOTE: + { + // Prevent cheating + if (!sender->IsAlive()) + return; + + if (sender->getLevel() < sWorld->getIntConfig(CONFIG_CHAT_EMOTE_LEVEL_REQ)) + { + SendNotification(GetTrinityString(LANG_SAY_REQ), sWorld->getIntConfig(CONFIG_CHAT_EMOTE_LEVEL_REQ)); + return; + } + + sender->TextEmote(msg); + break; + } + case CHAT_MSG_YELL: + { + // Prevent cheating + if (!sender->IsAlive()) + return; + + if (sender->getLevel() < sWorld->getIntConfig(CONFIG_CHAT_YELL_LEVEL_REQ)) + { + SendNotification(GetTrinityString(LANG_SAY_REQ), sWorld->getIntConfig(CONFIG_CHAT_YELL_LEVEL_REQ)); + return; + } + + sender->Yell(msg, Language(lang)); + break; + } + case CHAT_MSG_WHISPER: + { + if (!normalizePlayerName(to)) + { + SendPlayerNotFoundNotice(to); + break; + } + + Player* receiver = ObjectAccessor::FindConnectedPlayerByName(to); + if (!receiver || (lang != LANG_ADDON && !receiver->isAcceptWhispers() && receiver->GetSession()->HasPermission(rbac::RBAC_PERM_CAN_FILTER_WHISPERS) && !receiver->IsInWhisperWhiteList(sender->GetGUID()))) + { + SendPlayerNotFoundNotice(to); + return; + } + if (!sender->IsGameMaster() && sender->getLevel() < sWorld->getIntConfig(CONFIG_CHAT_WHISPER_LEVEL_REQ) && !receiver->IsInWhisperWhiteList(sender->GetGUID())) + { + SendNotification(GetTrinityString(LANG_WHISPER_REQ), sWorld->getIntConfig(CONFIG_CHAT_WHISPER_LEVEL_REQ)); + return; + } + + if (GetPlayer()->GetTeam() != receiver->GetTeam() && !HasPermission(rbac::RBAC_PERM_TWO_SIDE_INTERACTION_CHAT) && !receiver->IsInWhisperWhiteList(sender->GetGUID())) + { + SendWrongFactionNotice(); + return; + } + + if (GetPlayer()->HasAura(1852) && !receiver->IsGameMaster()) + { + SendNotification(GetTrinityString(LANG_GM_SILENCE), GetPlayer()->GetName().c_str()); + return; + } + + // If player is a Gamemaster and doesn't accept whisper, we auto-whitelist every player that the Gamemaster is talking to + // We also do that if a player is under the required level for whispers. + if (receiver->getLevel() < sWorld->getIntConfig(CONFIG_CHAT_WHISPER_LEVEL_REQ) || + (HasPermission(rbac::RBAC_PERM_CAN_FILTER_WHISPERS) && !sender->isAcceptWhispers() && !sender->IsInWhisperWhiteList(receiver->GetGUID()))) + sender->AddWhisperWhiteList(receiver->GetGUID()); + + GetPlayer()->Whisper(msg, Language(lang), receiver); + break; + } + case CHAT_MSG_PARTY: + case CHAT_MSG_PARTY_LEADER: + { + // if player is in battleground, he cannot say to battleground members by /p + Group* group = GetPlayer()->GetOriginalGroup(); + if (!group) + { + group = sender->GetGroup(); + if (!group || group->isBGGroup()) + return; + } + + if (group->IsLeader(GetPlayer()->GetGUID())) + type = CHAT_MSG_PARTY_LEADER; + + sScriptMgr->OnPlayerChat(GetPlayer(), type, lang, msg, group); + + WorldPacket data; + ChatHandler::BuildChatPacket(data, ChatMsg(type), Language(lang), sender, nullptr, msg); + group->BroadcastPacket(&data, false, group->GetMemberGroup(GetPlayer()->GetGUID())); + break; + } + case CHAT_MSG_GUILD: + { + if (GetPlayer()->GetGuildId()) + { + if (Guild* guild = sGuildMgr->GetGuildById(GetPlayer()->GetGuildId())) + { + sScriptMgr->OnPlayerChat(GetPlayer(), type, lang, msg, guild); + + guild->BroadcastToGuild(this, false, msg, lang == LANG_ADDON ? LANG_ADDON : LANG_UNIVERSAL); + } + } + break; + } + case CHAT_MSG_OFFICER: + { + if (GetPlayer()->GetGuildId()) + { + if (Guild* guild = sGuildMgr->GetGuildById(GetPlayer()->GetGuildId())) + { + sScriptMgr->OnPlayerChat(GetPlayer(), type, lang, msg, guild); + + guild->BroadcastToGuild(this, true, msg, lang == LANG_ADDON ? LANG_ADDON : LANG_UNIVERSAL); + } + } + break; + } + case CHAT_MSG_RAID: + case CHAT_MSG_RAID_LEADER: + { + // if player is in battleground, he cannot say to battleground members by /ra + Group* group = GetPlayer()->GetOriginalGroup(); + if (!group) + { + group = GetPlayer()->GetGroup(); + if (!group || group->isBGGroup() || !group->isRaidGroup()) + return; + } + + if (group->IsLeader(GetPlayer()->GetGUID())) + type = CHAT_MSG_RAID_LEADER; + + sScriptMgr->OnPlayerChat(GetPlayer(), type, lang, msg, group); + + WorldPacket data; + ChatHandler::BuildChatPacket(data, ChatMsg(type), Language(lang), sender, nullptr, msg); + group->BroadcastPacket(&data, false); + break; + } + case CHAT_MSG_RAID_WARNING: + { + Group* group = GetPlayer()->GetGroup(); + if (!group || !(group->isRaidGroup() || sWorld->getBoolConfig(CONFIG_CHAT_PARTY_RAID_WARNINGS)) || !(group->IsLeader(GetPlayer()->GetGUID()) || group->IsAssistant(GetPlayer()->GetGUID())) || group->isBGGroup()) + return; + + sScriptMgr->OnPlayerChat(GetPlayer(), type, lang, msg, group); + + WorldPacket data; + //in battleground, raid warning is sent only to players in battleground - code is ok + ChatHandler::BuildChatPacket(data, CHAT_MSG_RAID_WARNING, Language(lang), sender, nullptr, msg); + group->BroadcastPacket(&data, false); + break; + } + case CHAT_MSG_BATTLEGROUND: + case CHAT_MSG_BATTLEGROUND_LEADER: + { + // battleground raid is always in Player->GetGroup(), never in GetOriginalGroup() + Group* group = GetPlayer()->GetGroup(); + if (!group || !group->isBGGroup()) + return; + + if (group->IsLeader(GetPlayer()->GetGUID())) + type = CHAT_MSG_BATTLEGROUND_LEADER; + + sScriptMgr->OnPlayerChat(GetPlayer(), type, lang, msg, group); + + WorldPacket data; + ChatHandler::BuildChatPacket(data, ChatMsg(type), Language(lang), sender, nullptr, msg); + group->BroadcastPacket(&data, false); + break; + } + case CHAT_MSG_CHANNEL: + { + if (!HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHAT_CHANNEL_REQ)) + { + if (sender->getLevel() < sWorld->getIntConfig(CONFIG_CHAT_CHANNEL_LEVEL_REQ)) + { + SendNotification(GetTrinityString(LANG_CHANNEL_REQ), sWorld->getIntConfig(CONFIG_CHAT_CHANNEL_LEVEL_REQ)); + return; + } + } + + if (Channel* chn = ChannelMgr::GetChannelForPlayerByNamePart(channel, sender)) + { + sScriptMgr->OnPlayerChat(sender, type, lang, msg, chn); + chn->Say(sender->GetGUID(), msg, lang); + } + break; + } + case CHAT_MSG_AFK: + { + if (!sender->IsInCombat()) + { + if (sender ->isAFK()) // Already AFK + { + if (msg.empty()) + sender->ToggleAFK(); // Remove AFK + else + sender->autoReplyMsg = msg; // Update message + } + else // New AFK mode + { + sender->autoReplyMsg = msg.empty() ? GetTrinityString(LANG_PLAYER_AFK_DEFAULT) : msg; + + if (sender->isDND()) + sender->ToggleDND(); + + sender->ToggleAFK(); + } + + sScriptMgr->OnPlayerChat(sender, type, lang, msg); + } + break; + } + case CHAT_MSG_DND: + { + if (sender->isDND()) // Already DND + { + if (msg.empty()) + sender->ToggleDND(); // Remove DND + else + sender->autoReplyMsg = msg; // Update message + } + else // New DND mode + { + sender->autoReplyMsg = msg.empty() ? GetTrinityString(LANG_PLAYER_DND_DEFAULT) : msg; + + if (sender->isAFK()) + sender->ToggleAFK(); + + sender->ToggleDND(); + } + + sScriptMgr->OnPlayerChat(sender, type, lang, msg); + break; + } + default: + TC_LOG_ERROR("network", "CHAT: unknown message type %u, lang: %u", type, lang); + break; + } +} + +void WorldSession::HandleAddonMessagechatOpcode(WorldPacket& recvData) +{ + Player* sender = GetPlayer(); + ChatMsg type; + + switch (recvData.GetOpcode()) + { + case CMSG_MESSAGECHAT_ADDON_BATTLEGROUND: + type = CHAT_MSG_BATTLEGROUND; + break; + case CMSG_MESSAGECHAT_ADDON_GUILD: + type = CHAT_MSG_GUILD; + break; + case CMSG_MESSAGECHAT_ADDON_OFFICER: + type = CHAT_MSG_OFFICER; + break; + case CMSG_MESSAGECHAT_ADDON_PARTY: + type = CHAT_MSG_PARTY; + break; + case CMSG_MESSAGECHAT_ADDON_RAID: + type = CHAT_MSG_RAID; + break; + case CMSG_MESSAGECHAT_ADDON_WHISPER: + type = CHAT_MSG_WHISPER; + break; + default: + TC_LOG_ERROR("network", "HandleAddonMessagechatOpcode: Unknown addon chat opcode (%u)", recvData.GetOpcode()); + recvData.hexlike(); + return; + } + + std::string message; + std::string prefix; + std::string targetName; + + switch (type) + { + case CHAT_MSG_WHISPER: + { + uint32 msgLen = recvData.ReadBits(9); + uint32 prefixLen = recvData.ReadBits(5); + uint32 targetLen = recvData.ReadBits(10); + message = recvData.ReadString(msgLen); + prefix = recvData.ReadString(prefixLen); + targetName = recvData.ReadString(targetLen); + break; + } + case CHAT_MSG_RAID: + case CHAT_MSG_BATTLEGROUND: + { + uint32 prefixLen = recvData.ReadBits(5); + uint32 msgLen = recvData.ReadBits(9); + prefix = recvData.ReadString(prefixLen); + message = recvData.ReadString(msgLen); + break; + } + case CHAT_MSG_PARTY: + case CHAT_MSG_OFFICER: + { + uint32 prefixLen = recvData.ReadBits(5); + uint32 msgLen = recvData.ReadBits(9); + message = recvData.ReadString(msgLen); + prefix = recvData.ReadString(prefixLen); + break; + } + case CHAT_MSG_GUILD: + { + uint32 msgLen = recvData.ReadBits(9); + uint32 prefixLen = recvData.ReadBits(5); + message = recvData.ReadString(msgLen); + prefix = recvData.ReadString(prefixLen); + break; + } + default: + break; + } + + if (prefix.empty() || prefix.length() > 16) + { + recvData.rfinish(); + return; + } + + // Disabled addon channel? + if (!sWorld->getBoolConfig(CONFIG_ADDON_CHANNEL)) + { + recvData.rfinish(); + return; + } + + if (!CanSpeak()) + { + recvData.rfinish(); + return; + } + + sender->UpdateSpeakTime(Player::ChatFloodThrottle::ADDON); + + switch (type) + { + case CHAT_MSG_BATTLEGROUND: + { + Group* group = sender->GetGroup(); + if (!group || !group->isBGGroup()) + return; + + WorldPacket data; + ChatHandler::BuildChatPacket(data, type, LANG_ADDON, sender, nullptr, message, 0U, "", DEFAULT_LOCALE, prefix); + group->BroadcastAddonMessagePacket(&data, prefix, false); + break; + } + case CHAT_MSG_GUILD: + case CHAT_MSG_OFFICER: + { + if (sender->GetGuildId()) + if (Guild* guild = sGuildMgr->GetGuildById(sender->GetGuildId())) + guild->BroadcastAddonToGuild(this, type == CHAT_MSG_OFFICER, message, prefix); + break; + } + case CHAT_MSG_WHISPER: + { + if (!normalizePlayerName(targetName)) + break; + Player* receiver = ObjectAccessor::FindPlayerByName(targetName); + if (!receiver) + break; + + sender->WhisperAddon(message, prefix, receiver); + break; + } + // Messages sent to "RAID" while in a party will get delivered to "PARTY" + case CHAT_MSG_PARTY: + case CHAT_MSG_RAID: + { + Group* group = sender->GetGroup(); + if (!group || group->isBGGroup()) + break; + + WorldPacket data; + ChatHandler::BuildChatPacket(data, type, LANG_ADDON, sender, nullptr, message, 0U, "", DEFAULT_LOCALE, prefix); + group->BroadcastAddonMessagePacket(&data, prefix, true, -1, group->GetMemberGroup(sender->GetGUID())); + break; + } + default: + { + TC_LOG_ERROR("misc", "HandleAddonMessagechatOpcode: unknown addon message type %u", type); + break; + } + } +} + +void WorldSession::HandleEmoteOpcode(WorldPacket& recvData) +{ + if (!GetPlayer()->IsAlive() || GetPlayer()->HasUnitState(UNIT_STATE_DIED)) + return; + + uint32 emote; + recvData >> emote; + + // restrict to the only emotes hardcoded in client + if (emote != EMOTE_ONESHOT_NONE && emote != EMOTE_ONESHOT_WAVE) + return; + + sScriptMgr->OnPlayerClearEmote(GetPlayer()); + + if (_player->GetUInt32Value(UNIT_NPC_EMOTESTATE)) + _player->SetUInt32Value(UNIT_NPC_EMOTESTATE, 0); +} + +void WorldSession::HandleSendTextEmoteOpcode(WorldPackets::Chat::SendTextEmote& packet) +{ + if (!GetPlayer()->IsAlive()) + return; + + if (!CanSpeak()) + { + std::string timeStr = secsToTimeString(m_muteTime - GameTime::GetGameTime()); + SendNotification(GetTrinityString(LANG_WAIT_BEFORE_SPEAKING), timeStr.c_str()); + return; + } + + sScriptMgr->OnPlayerTextEmote(GetPlayer(), packet.SoundIndex, packet.EmoteID, packet.Target); + + EmotesTextEntry const* em = sEmotesTextStore.LookupEntry(packet.EmoteID); + if (!em) + return; + + Emote emote = static_cast(em->EmoteID); + + switch (emote) + { + case EMOTE_STATE_SLEEP: + case EMOTE_STATE_SIT: + case EMOTE_STATE_KNEEL: + case EMOTE_ONESHOT_NONE: + break; + case EMOTE_STATE_DANCE: + case EMOTE_STATE_READ: + GetPlayer()->SetUInt32Value(UNIT_NPC_EMOTESTATE, emote); + break; + default: + // Only allow text-emotes for "dead" entities (feign death included) + if (GetPlayer()->HasUnitState(UNIT_STATE_DIED)) + break; + GetPlayer()->HandleEmoteCommand(emote); + break; + } + + Unit* unit = ObjectAccessor::GetUnit(*_player, packet.Target); + + WorldPackets::Chat::STextEmote textEmote; + textEmote.SourceGUID = _player->GetGUID(); + textEmote.EmoteID = packet.EmoteID; + textEmote.SoundIndex = packet.SoundIndex; + if (unit) + textEmote.Target = unit->GetName(); + + _player->SendMessageToSetInRange(textEmote.Write(), sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_TEXTEMOTE), true); + + GetPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE, packet.EmoteID, 0, 0, unit); + + //Send scripted event call + if (unit && unit->GetTypeId() == TYPEID_UNIT && ((Creature*)unit)->AI()) + ((Creature*)unit)->AI()->ReceiveEmote(GetPlayer(), packet.EmoteID); + + if (emote != EMOTE_ONESHOT_NONE) + _player->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::Anim); +} + +void WorldSession::HandleChatIgnoredOpcode(WorldPacket& recvData) +{ + ObjectGuid guid; + uint8 unk; + //TC_LOG_DEBUG("network", "WORLD: Received CMSG_CHAT_IGNORED"); + + recvData >> unk; // probably related to spam reporting + guid[5] = recvData.ReadBit(); + guid[2] = recvData.ReadBit(); + guid[6] = recvData.ReadBit(); + guid[4] = recvData.ReadBit(); + guid[7] = recvData.ReadBit(); + guid[0] = recvData.ReadBit(); + guid[1] = recvData.ReadBit(); + guid[3] = recvData.ReadBit(); + + recvData.ReadByteSeq(guid[0]); + recvData.ReadByteSeq(guid[6]); + recvData.ReadByteSeq(guid[5]); + recvData.ReadByteSeq(guid[1]); + recvData.ReadByteSeq(guid[4]); + recvData.ReadByteSeq(guid[3]); + recvData.ReadByteSeq(guid[7]); + recvData.ReadByteSeq(guid[2]); + + Player* player = ObjectAccessor::FindConnectedPlayer(guid); + if (!player || !player->GetSession()) + return; + + WorldPacket data; + ChatHandler::BuildChatPacket(data, CHAT_MSG_IGNORED, LANG_UNIVERSAL, _player, _player, GetPlayer()->GetName()); + player->SendDirectMessage(&data); +} + +void WorldSession::HandleChannelDeclineInvite(WorldPacket &recvPacket) +{ + TC_LOG_DEBUG("network", "Opcode %u", recvPacket.GetOpcode()); +} + +void WorldSession::SendPlayerNotFoundNotice(std::string const& name) +{ + WorldPacket data(SMSG_CHAT_PLAYER_NOT_FOUND, name.size()+1); + data << name; + SendPacket(&data); +} + +void WorldSession::SendPlayerAmbiguousNotice(std::string const& name) +{ + WorldPacket data(SMSG_CHAT_PLAYER_AMBIGUOUS, name.size()+1); + data << name; + SendPacket(&data); +} + +void WorldSession::SendWrongFactionNotice() +{ + WorldPacket data(SMSG_CHAT_WRONG_FACTION, 0); + SendPacket(&data); +} + +void WorldSession::SendChatRestrictedNotice(ChatRestrictionType restriction) +{ + WorldPacket data(SMSG_CHAT_RESTRICTED, 1); + data << uint8(restriction); + SendPacket(&data); +} diff --git a/src/server/game/Handlers/GroupHandler.cpp b/src/server/game/Handlers/GroupHandler.cpp index 23dec9c218..de4c88e555 100644 --- a/src/server/game/Handlers/GroupHandler.cpp +++ b/src/server/game/Handlers/GroupHandler.cpp @@ -552,13 +552,25 @@ void WorldSession::HandleRaidTargetUpdateOpcode(WorldPacket& recvData) { TC_LOG_DEBUG("network", "WORLD: Received MSG_RAID_TARGET_UPDATE"); - Group* group = GetPlayer()->GetGroup(); - if (!group) - return; - + // VIANO_VIRTUAL_TARGET_ICONS_V1 + // Read the icon byte before requiring a real Trinity Group. The client can + // place raid target icons while Viano provides only virtual bot members. uint8 x; recvData >> x; + Group* group = GetPlayer()->GetGroup(); + if (!group) + { + // Query packet (0xFF) has no GUID payload. + if (x == 0xFF) + return; + + ObjectGuid guid; + recvData >> guid; + + GetPlayer()->SetVianoTargetIcon(x, guid); + return; + } /** error handling **/ /********************/ @@ -581,6 +593,7 @@ void WorldSession::HandleRaidTargetUpdateOpcode(WorldPacket& recvData) return; } + _player->SetVianoTargetIcon(x, guid); group->SetTargetIcon(x, _player->GetGUID(), guid); } } diff --git a/src/server/game/Handlers/GroupHandler.cpp.bak-viano-virtual-icons-aura-v1-20261001-174000 b/src/server/game/Handlers/GroupHandler.cpp.bak-viano-virtual-icons-aura-v1-20261001-174000 new file mode 100644 index 0000000000..23dec9c218 --- /dev/null +++ b/src/server/game/Handlers/GroupHandler.cpp.bak-viano-virtual-icons-aura-v1-20261001-174000 @@ -0,0 +1,874 @@ +/* + * 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 . + */ + +#include "WorldSession.h" +#include "CharacterCache.h" +#include "Common.h" +#include "DatabaseEnv.h" +#include "Group.h" +#include "GroupMgr.h" +#include "Log.h" +#include "ObjectAccessor.h" +#include "ObjectMgr.h" +#include "PartyPackets.h" +#include "Pet.h" +#include "PhasingHandler.h" +#include "Player.h" +#include "SocialMgr.h" +#include "SpellAuraEffects.h" +#include "SpellAuras.h" +#include "Util.h" +#include "Vehicle.h" +#include "World.h" +#include "WorldPacket.h" + +class Aura; + +/* differeces from off: + -you can uninvite yourself - is is useful + -you can accept invitation even if leader went offline +*/ +/* todo: + -group_destroyed msg is sent but not shown + -reduce xp gaining when in raid group + -quest sharing has to be corrected + -FIX sending PartyMemberStats +*/ + +void WorldSession::SendPartyResult(PartyOperation operation, const std::string& member, PartyResult res, uint32 val /* = 0 */) +{ + WorldPackets::Party::PartyCommandResult packet; + packet.Name = member; + packet.Command = uint8(operation); + packet.Result = uint8(res); + packet.ResultData = val; + packet.ResultGUID = ObjectGuid::Empty; + + SendPacket(packet.Write()); +} + +void WorldSession::HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient& packet) +{ + // cheating + if (!normalizePlayerName(packet.TargetName)) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_BAD_PLAYER_NAME_S); + return; + } + + Player* invitingPlayer = GetPlayer(); + Player* invitedPlayer = ObjectAccessor::FindPlayerByName(packet.TargetName); + + // no player + if (!invitedPlayer) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_BAD_PLAYER_NAME_S); + return; + } + + // player trying to invite himself (most likely cheating) + if (invitedPlayer == invitingPlayer) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_BAD_PLAYER_NAME_S); + return; + } + + // restrict invite to GMs + if (!sWorld->getBoolConfig(CONFIG_ALLOW_GM_GROUP) && !invitingPlayer->IsGameMaster() && invitedPlayer->IsGameMaster()) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_BAD_PLAYER_NAME_S); + return; + } + + // can't group with + if (!invitingPlayer->IsGameMaster() && !sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GROUP) && invitingPlayer->GetTeam() != invitedPlayer->GetTeam()) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_PLAYER_WRONG_FACTION); + return; + } + if (invitingPlayer->GetInstanceId() != 0 && invitedPlayer->GetInstanceId() != 0 && invitingPlayer->GetInstanceId() != invitedPlayer->GetInstanceId() && invitingPlayer->GetMapId() == invitedPlayer->GetMapId()) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_TARGET_NOT_IN_INSTANCE_S); + return; + } + // just ignore us + if (invitedPlayer->GetInstanceId() != 0 && invitedPlayer->GetDungeonDifficulty() != invitingPlayer->GetDungeonDifficulty()) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_IGNORING_YOU_S); + return; + } + + if (invitedPlayer->GetSocial()->HasIgnore(invitingPlayer->GetGUID())) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_IGNORING_YOU_S); + return; + } + + if (!invitedPlayer->GetSocial()->HasFriend(invitingPlayer->GetGUID()) && invitingPlayer->getLevel() < sWorld->getIntConfig(CONFIG_PARTY_LEVEL_REQ)) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_INVITE_RESTRICTED); + return; + } + + Group* group = invitingPlayer->GetGroup(); + if (group && group->isBGGroup()) + group = invitingPlayer->GetOriginalGroup(); + if (!group) + group = invitingPlayer->GetGroupInvite(); + + Group* group2 = invitedPlayer->GetGroup(); + if (group2 && group2->isBGGroup()) + group2 = invitedPlayer->GetOriginalGroup(); + // player already in another group or invited + if (group2 || invitedPlayer->GetGroupInvite()) + { + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_ALREADY_IN_GROUP_S); + + if (group2) + { + // tell the player that they were invited but it failed as they were already in a group + WorldPackets::Party::PartyInvite partyInvite; + partyInvite.Initialize(invitingPlayer, packet.ProposedRoles, false); + invitedPlayer->SendDirectMessage(partyInvite.Write()); + } + + return; + } + + if (group) + { + // not have permissions for invite + if (!group->IsLeader(invitingPlayer->GetGUID()) && !group->IsAssistant(invitingPlayer->GetGUID())) + { + if (group->IsCreated()) + SendPartyResult(PARTY_OP_INVITE, "", ERR_NOT_LEADER); + return; + } + // not have place + if (group->IsFull()) + { + SendPartyResult(PARTY_OP_INVITE, "", ERR_GROUP_FULL); + return; + } + } + + // ok, but group not exist, start a new group + // but don't create and save the group to the DB until + // at least one person joins + if (!group) + { + group = new Group(); + // new group: if can't add then delete + if (!group->AddLeaderInvite(invitingPlayer)) + { + delete group; + return; + } + if (!group->AddInvite(invitedPlayer)) + { + group->RemoveAllInvites(); + delete group; + return; + } + } + else + { + // already existed group: if can't add then just leave + if (!group->AddInvite(invitedPlayer)) + { + return; + } + } + + WorldPackets::Party::PartyInvite partyInvite; + partyInvite.Initialize(invitingPlayer, packet.ProposedRoles, true); + invitedPlayer->SendDirectMessage(partyInvite.Write()); + + SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_PARTY_RESULT_OK); +} + +void WorldSession::HandlePartyInviteResponseOpcode(WorldPackets::Party::PartyInviteResponse& packet) +{ + Group* group = GetPlayer()->GetGroupInvite(); + + if (!group) + return; + + if (packet.Accept) + { + // Remove player from invitees in any case + group->RemoveInvite(GetPlayer()); + + if (group->GetLeaderGUID() == GetPlayer()->GetGUID()) + { + TC_LOG_ERROR("network", "HandleGroupAcceptOpcode: player %s(%d) tried to accept an invite to his own group", GetPlayer()->GetName().c_str(), GetPlayer()->GetGUID().GetCounter()); + return; + } + + // Group is full + if (group->IsFull()) + { + SendPartyResult(PARTY_OP_INVITE, "", ERR_GROUP_FULL); + return; + } + + Player* leader = ObjectAccessor::FindPlayer(group->GetLeaderGUID()); + + // Forming a new group, create it + if (!group->IsCreated()) + { + // This can happen if the leader is zoning. To be removed once delayed actions for zoning are implemented + if (!leader) + { + group->RemoveAllInvites(); + return; + } + + // If we're about to create a group there really should be a leader present + ASSERT(leader); + group->RemoveInvite(leader); + group->Create(leader); + sGroupMgr->AddGroup(group); + } + + // Everything is fine, do it, PLAYER'S GROUP IS SET IN ADDMEMBER!!! + if (!group->AddMember(GetPlayer())) + return; + + group->BroadcastGroupUpdate(); + } + else + { + // Remember leader if online (group pointer will be invalid if group gets disbanded) + Player* leader = ObjectAccessor::FindConnectedPlayer(group->GetLeaderGUID()); + + // uninvite, group can be deleted + GetPlayer()->UninviteFromGroup(); + + if (!leader || !leader->GetSession()) + return; + + // report + WorldPackets::Party::GroupDecline decline(GetPlayer()->GetName()); + leader->SendDirectMessage(decline.Write()); + } +} + +void WorldSession::HandleGroupClearMarker(WorldPacket& recv_data) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_CLEAR_RAID_MARKER"); + + uint8 marker; + recv_data >> marker; + + if (Group* group = GetPlayer()->GetGroup()) + { + group->RemoveGroupMarkerMask(1 << marker); + group->SendRaidMarkerUpdate(); + group->RemoveMarker(); + } +} + +void WorldSession::HandleGroupUninviteGuidOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_GROUP_UNINVITE_GUID"); + + ObjectGuid guid; + std::string reason; + recvData >> guid; + recvData >> reason; + + //can't uninvite yourself + if (guid == GetPlayer()->GetGUID()) + { + TC_LOG_ERROR("network", "WorldSession::HandleGroupUninviteGuidOpcode: leader %s(%d) tried to uninvite himself from the group.", + GetPlayer()->GetName().c_str(), GetPlayer()->GetGUID().GetCounter()); + return; + } + + PartyResult res = GetPlayer()->CanUninviteFromGroup(guid); + if (res != ERR_PARTY_RESULT_OK) + { + SendPartyResult(PARTY_OP_UNINVITE, "", res); + return; + } + + Group* grp = GetPlayer()->GetGroup(); + // grp is checked already above in CanUninviteFromGroup() + ASSERT(grp); + + if (grp->IsMember(guid)) + { + Player::RemoveFromGroup(grp, guid, GROUP_REMOVEMETHOD_KICK, GetPlayer()->GetGUID(), reason.c_str()); + return; + } + + if (Player* player = grp->GetInvited(guid)) + { + player->UninviteFromGroup(); + return; + } + + SendPartyResult(PARTY_OP_UNINVITE, "", ERR_TARGET_NOT_IN_GROUP_S); +} + +void WorldSession::HandleGroupUninviteOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_GROUP_UNINVITE"); + + std::string membername; + recvData >> membername; + + // player not found + if (!normalizePlayerName(membername)) + return; + + // can't uninvite yourself + if (GetPlayer()->GetName() == membername) + { + TC_LOG_ERROR("network", "WorldSession::HandleGroupUninviteOpcode: leader %s(%d) tried to uninvite himself from the group.", + GetPlayer()->GetName().c_str(), GetPlayer()->GetGUID().GetCounter()); + return; + } + + PartyResult res = GetPlayer()->CanUninviteFromGroup(); + if (res != ERR_PARTY_RESULT_OK) + { + SendPartyResult(PARTY_OP_UNINVITE, "", res); + return; + } + + Group* grp = GetPlayer()->GetGroup(); + if (!grp) + return; + + if (ObjectGuid guid = grp->GetMemberGUID(membername)) + { + Player::RemoveFromGroup(grp, guid, GROUP_REMOVEMETHOD_KICK, GetPlayer()->GetGUID()); + return; + } + + if (Player* player = grp->GetInvited(membername)) + { + player->UninviteFromGroup(); + return; + } + + SendPartyResult(PARTY_OP_UNINVITE, membername, ERR_TARGET_NOT_IN_GROUP_S); +} + +void WorldSession::HandleGroupSetLeaderOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_GROUP_SET_LEADER"); + + ObjectGuid guid; + recvData >> guid; + + Player* player = ObjectAccessor::FindConnectedPlayer(guid); + Group* group = GetPlayer()->GetGroup(); + + if (!group || !player) + return; + + if (!group->IsLeader(GetPlayer()->GetGUID()) || player->GetGroup() != group) + return; + + // Everything's fine, accepted. + group->ChangeLeader(guid); + group->SendUpdate(); +} + +void WorldSession::HandleSetRoleOpcode(WorldPackets::Party::SetRole& packet) +{ + WorldPackets::Party::RoleChangedInform roleChangedInform; + + Group* group = GetPlayer()->GetGroup(); + uint8 oldRole = group ? group->GetLfgRoles(packet.ChangedUnit) : 0; + if (oldRole == packet.Role) + return; + + roleChangedInform.From = GetPlayer()->GetGUID(); + roleChangedInform.ChangedUnit = packet.ChangedUnit; + roleChangedInform.OldRole = oldRole; + roleChangedInform.NewRole = packet.Role; + + if (group) + { + group->BroadcastPacket(roleChangedInform.Write(), false); + group->SetLfgRoles(packet.ChangedUnit, packet.Role); + } + else + SendPacket(roleChangedInform.Write()); +} + +void WorldSession::HandleGroupDisbandOpcode(WorldPacket& /*recvData*/) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_GROUP_DISBAND"); + + Group* grp = GetPlayer()->GetGroup(); + Group* grpInvite = GetPlayer()->GetGroupInvite(); + if (!grp && !grpInvite) + return; + + if (_player->InBattleground()) + { + SendPartyResult(PARTY_OP_INVITE, "", ERR_INVITE_RESTRICTED); + return; + } + + /** error handling **/ + /********************/ + + // everything's fine, do it + if (grp) + { + SendPartyResult(PARTY_OP_LEAVE, GetPlayer()->GetName(), ERR_PARTY_RESULT_OK); + GetPlayer()->RemoveFromGroup(GROUP_REMOVEMETHOD_LEAVE); + } + else if (grpInvite && grpInvite->GetLeaderGUID() == GetPlayer()->GetGUID()) + { // pending group creation being cancelled + SendPartyResult(PARTY_OP_LEAVE, GetPlayer()->GetName(), ERR_PARTY_RESULT_OK); + grpInvite->Disband(); + } +} + +void WorldSession::HandleLootMethodOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_LOOT_METHOD"); + + uint32 lootMethod; + ObjectGuid lootMaster; + uint32 lootThreshold; + recvData >> lootMethod >> lootMaster >> lootThreshold; + + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + /** error handling **/ + if (!group->IsLeader(GetPlayer()->GetGUID())) + return; + + if (group->isLFGGroup()) + return; + + if (lootMethod > NEED_BEFORE_GREED) + return; + + if (lootThreshold < ITEM_QUALITY_UNCOMMON || lootThreshold > ITEM_QUALITY_ARTIFACT) + return; + + if (lootMethod == MASTER_LOOT && !group->IsMember(lootMaster)) + return; + /********************/ + + // everything's fine, do it + group->SetLootMethod((LootMethod)lootMethod); + group->SetMasterLooterGuid(lootMaster); + group->SetLootThreshold((ItemQualities)lootThreshold); + group->SendUpdate(); +} + +void WorldSession::HandleLootRoll(WorldPacket& recvData) +{ + ObjectGuid guid; + uint32 itemSlot; + uint8 rollType; + recvData >> guid; // guid of the item rolled + recvData >> itemSlot; + recvData >> rollType; // 0: pass, 1: need, 2: greed + + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + group->CountRollVote(GetPlayer()->GetGUID(), guid, rollType); + + switch (rollType) + { + case ROLL_NEED: + GetPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_ROLL_NEED, 1); + break; + case ROLL_GREED: + GetPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_ROLL_GREED, 1); + break; + } +} + +void WorldSession::HandleMinimapPingOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received MSG_MINIMAP_PING"); + + if (!GetPlayer()->GetGroup()) + return; + + float x, y; + recvData >> x; + recvData >> y; + + //TC_LOG_DEBUG("misc", "Received opcode MSG_MINIMAP_PING X: %f, Y: %f", x, y); + + /** error handling **/ + /********************/ + + // everything's fine, do it + WorldPacket data(MSG_MINIMAP_PING, (8+4+4)); + data << uint64(GetPlayer()->GetGUID()); + data << float(x); + data << float(y); + GetPlayer()->GetGroup()->BroadcastPacket(&data, true, -1, GetPlayer()->GetGUID()); +} + +void WorldSession::HandleRandomRollOpcode(WorldPacket& recvData) +{ + uint32 minimum, maximum; + recvData >> minimum; + recvData >> maximum; + + /** error handling **/ + if (minimum > maximum || maximum > 10000) // < 32768 for urand call + return; + /********************/ + + // everything's fine, do it + GetPlayer()->DoRandomRoll(minimum, maximum); +} + +void WorldSession::HandleRaidTargetUpdateOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received MSG_RAID_TARGET_UPDATE"); + + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + uint8 x; + recvData >> x; + + /** error handling **/ + /********************/ + + // everything's fine, do it + if (x == 0xFF) // target icon request + group->SendTargetIconList(this); + else // target icon update + { + if (group->isRaidGroup() && !group->IsLeader(GetPlayer()->GetGUID()) && !group->IsAssistant(GetPlayer()->GetGUID())) + return; + + ObjectGuid guid; + recvData >> guid; + + if (guid.IsPlayer()) + { + Player* target = ObjectAccessor::FindConnectedPlayer(guid); + + if (!target || target->IsHostileTo(GetPlayer())) + return; + } + + group->SetTargetIcon(x, _player->GetGUID(), guid); + } +} + +void WorldSession::HandleGroupRaidConvertOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_GROUP_RAID_CONVERT"); + + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + if (_player->InBattleground() || group->isLFGGroup()) + return; + + // error handling + if (!group->IsLeader(GetPlayer()->GetGUID()) || group->GetMembersCount() < 2) + return; + + // everything's fine, do it (is it 0 (PARTY_OP_INVITE) correct code) + SendPartyResult(PARTY_OP_INVITE, "", ERR_PARTY_RESULT_OK); + + // New 4.x: it is now possible to convert a raid to a group if member count is 5 or less + + bool toRaid; + recvData >> toRaid; + + if (toRaid) + group->ConvertToRaid(); + else + group->ConvertToGroup(); +} + +void WorldSession::HandleGroupRequestJoinUpdates(WorldPacket& /*recvData*/) +{ + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + WorldPacket data(SMSG_REAL_GROUP_UPDATE, 1 + 4 + 8); + data << uint8(group->GetGroupFlags()); + data << uint32(group->GetMembersCount() - 1); + data << uint64(group->GetLeaderGUID()); + SendPacket(&data); +} + +void WorldSession::HandleGroupChangeSubGroupOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_GROUP_CHANGE_SUB_GROUP"); + + // we will get correct pointer for group here, so we don't have to check if group is BG raid + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + std::string name; + uint8 groupNr; + recvData >> name; + recvData >> groupNr; + + if (!normalizePlayerName(name)) + return; + + if (groupNr >= MAX_RAID_SUBGROUPS) + return; + + ObjectGuid senderGuid = GetPlayer()->GetGUID(); + if (!group->IsLeader(senderGuid) && !group->IsAssistant(senderGuid)) + return; + + if (!group->HasFreeSlotSubGroup(groupNr)) + return; + + ObjectGuid guid; + if (Player* movedPlayer = ObjectAccessor::FindConnectedPlayerByName(name)) + guid = movedPlayer->GetGUID(); + else + guid = sCharacterCache->GetCharacterGuidByName(name); + + if (guid.IsEmpty()) + return; + + group->ChangeMembersGroup(guid, groupNr); +} + +void WorldSession::HandleGroupSwapSubGroupOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_GROUP_SWAP_SUB_GROUP"); + std::string name1; + std::string name2; + + recvData >> name1; + recvData >> name2; +} + +void WorldSession::HandleGroupAssistantLeaderOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_GROUP_ASSISTANT_LEADER"); + + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + if (!group->IsLeader(GetPlayer()->GetGUID())) + return; + + ObjectGuid guid; + bool apply; + recvData >> guid; + recvData >> apply; + + group->SetGroupMemberFlag(guid, apply, MEMBER_FLAG_ASSISTANT); +} + +void WorldSession::HandlePartyAssignmentOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received MSG_PARTY_ASSIGNMENT"); + + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + ObjectGuid senderGuid = GetPlayer()->GetGUID(); + if (!group->IsLeader(senderGuid) && !group->IsAssistant(senderGuid)) + return; + + uint8 assignment; + bool apply; + ObjectGuid guid; + recvData >> assignment >> apply; + recvData >> guid; + + switch (assignment) + { + case GROUP_ASSIGN_MAINASSIST: + group->RemoveUniqueGroupMemberFlag(MEMBER_FLAG_MAINASSIST); + group->SetGroupMemberFlag(guid, apply, MEMBER_FLAG_MAINASSIST); + break; + case GROUP_ASSIGN_MAINTANK: + group->RemoveUniqueGroupMemberFlag(MEMBER_FLAG_MAINTANK); // Remove main assist flag from current if any. + group->SetGroupMemberFlag(guid, apply, MEMBER_FLAG_MAINTANK); + break; + default: + break; + } + + group->SendUpdate(); +} + +void WorldSession::HandleRaidReadyCheckOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received MSG_RAID_READY_CHECK"); + + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + if (recvData.empty()) // request + { + /** error handling **/ + if (!group->IsLeader(GetPlayer()->GetGUID()) && !group->IsAssistant(GetPlayer()->GetGUID())) + return; + /********************/ + + // everything's fine, do it + WorldPacket data(MSG_RAID_READY_CHECK, 8); + data << GetPlayer()->GetGUID(); + group->BroadcastPacket(&data, false, -1); + + group->OfflineReadyCheck(); + } + else // answer + { + uint8 state; + recvData >> state; + + // everything's fine, do it + WorldPacket data(MSG_RAID_READY_CHECK_CONFIRM, 9); + data << uint64(GetPlayer()->GetGUID()); + data << uint8(state); + group->BroadcastReadyCheck(&data); + } +} + +void WorldSession::HandleRaidReadyCheckFinishedOpcode(WorldPacket& /*recvData*/) +{ + //Group* group = GetPlayer()->GetGroup(); + //if (!group) + // return; + + //if (!group->IsLeader(GetPlayer()->GetGUID()) && !group->IsAssistant(GetPlayer()->GetGUID())) + // return; + + // Is any reaction need? +} + +/*this procedure handles clients CMSG_REQUEST_PARTY_MEMBER_STATS request*/ +void WorldSession::HandleRequestPartyMemberStatsOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_REQUEST_PARTY_MEMBER_STATS"); + ObjectGuid Guid; + recvData >> Guid; + + Player* player = ObjectAccessor::FindConnectedPlayer(Guid); + if (!player) + { + WorldPackets::Party::PartyMemberState packet(true); + packet.MemberGuid = Guid; + packet.ChangeMask = GROUP_UPDATE_FLAG_STATUS; + packet.MemberStats.Status = MEMBER_STATUS_OFFLINE; + SendPacket(packet.Write()); + return; + } + + WorldPackets::Party::PartyMemberState packet(true); + packet.Initialize(player); + SendPacket(packet.Write()); +} + +void WorldSession::HandleRequestRaidInfoOpcode(WorldPacket& /*recvData*/) +{ + // every time the player checks the character screen + _player->SendRaidInfo(); +} + +void WorldSession::HandleOptOutOfLootOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_OPT_OUT_OF_LOOT"); + + bool passOnLoot; + recvData >> passOnLoot; // 1 always pass, 0 do not pass + + // ignore if player not loaded + if (!GetPlayer()) // needed because STATUS_AUTHED + { + if (passOnLoot) + TC_LOG_ERROR("network", "CMSG_OPT_OUT_OF_LOOT value<>0 for not-loaded character!"); + return; + } + + GetPlayer()->SetPassOnGroupLoot(passOnLoot != 0); +} + +void WorldSession::HandleRolePollBeginOpcode(WorldPacket& recvData) +{ + TC_LOG_DEBUG("network", "WORLD: Received CMSG_ROLE_POLL_BEGIN"); + + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + if (recvData.empty()) + { + if (!group->IsLeader(GetPlayer()->GetGUID()) && !group->IsAssistant(GetPlayer()->GetGUID())) + return; + + ObjectGuid guid = GetPlayer()->GetGUID(); + + WorldPacket data(SMSG_ROLE_POLL_BEGIN, 8); + data.WriteBit(guid[1]); + data.WriteBit(guid[5]); + data.WriteBit(guid[7]); + data.WriteBit(guid[3]); + data.WriteBit(guid[2]); + data.WriteBit(guid[4]); + data.WriteBit(guid[0]); + data.WriteBit(guid[6]); + data.WriteByteSeq(guid[4]); + data.WriteByteSeq(guid[7]); + data.WriteByteSeq(guid[0]); + data.WriteByteSeq(guid[5]); + data.WriteByteSeq(guid[1]); + data.WriteByteSeq(guid[6]); + data.WriteByteSeq(guid[2]); + data.WriteByteSeq(guid[3]); + + GetPlayer()->GetGroup()->BroadcastPacket(&data, true); + } +} + +void WorldSession::HandleSetEveryoneIsAssistant(WorldPackets::Party::SetEveryoneIsAssistant& packet) +{ + Group* group = GetPlayer()->GetGroup(); + if (!group) + return; + + if (!group->IsLeader(GetPlayer()->GetGUID())) + return; + + group->SetEveryoneIsAssistant(packet.EveryoneIsAssistant); +} diff --git a/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.cpp b/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.cpp index 39481cc8ec..26116d253f 100644 --- a/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.cpp +++ b/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.cpp @@ -23,7 +23,9 @@ void GenericMovementGenerator::Initialize(Unit* /*owner*/) { - _duration.Reset(_splineInit.Launch()); + int32 duration = _splineInit.Launch(); + _launched = duration > 0; + _duration.Reset(duration); } bool GenericMovementGenerator::Update(Unit* owner, uint32 diff) @@ -40,7 +42,8 @@ bool GenericMovementGenerator::Update(Unit* owner, uint32 diff) void GenericMovementGenerator::Finalize(Unit* owner) { - MovementInform(owner); + if (_launched) + MovementInform(owner); } void GenericMovementGenerator::MovementInform(Unit* owner) diff --git a/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.cpp.bak-bug15-core-20261001-143921 b/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.cpp.bak-bug15-core-20261001-143921 new file mode 100644 index 0000000000..39481cc8ec --- /dev/null +++ b/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.cpp.bak-bug15-core-20261001-143921 @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +#include "GenericMovementGenerator.h" +#include "Creature.h" +#include "CreatureAI.h" +#include "MoveSpline.h" +#include "Unit.h" + +void GenericMovementGenerator::Initialize(Unit* /*owner*/) +{ + _duration.Reset(_splineInit.Launch()); +} + +bool GenericMovementGenerator::Update(Unit* owner, uint32 diff) +{ + // Cyclic splines never expire, so update the duration only if it's not cyclic + if (!owner->movespline->isCyclic()) + _duration.Update(diff); + + if (_duration.Passed()) + return false; + + return !owner->movespline->Finalized(); +} + +void GenericMovementGenerator::Finalize(Unit* owner) +{ + MovementInform(owner); +} + +void GenericMovementGenerator::MovementInform(Unit* owner) +{ + if (Creature* creature = owner->ToCreature()) + if (creature->AI()) + creature->AI()->MovementInform(_type, _pointId); +} diff --git a/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.h b/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.h index e1689f8341..8c5cf4777d 100644 --- a/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.h +++ b/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.h @@ -29,7 +29,7 @@ enum MovementGeneratorType : uint8; class GenericMovementGenerator : public MovementGenerator { public: - explicit GenericMovementGenerator(Movement::MoveSplineInit&& splineInit, MovementGeneratorType type, uint32 id) : _splineInit(std::move(splineInit)), _type(type), _pointId(id), _duration(0) { } + explicit GenericMovementGenerator(Movement::MoveSplineInit&& splineInit, MovementGeneratorType type, uint32 id) : _splineInit(std::move(splineInit)), _type(type), _pointId(id), _duration(0), _launched(false) { } void Initialize(Unit*) override; void Finalize(Unit*) override; @@ -44,6 +44,7 @@ class GenericMovementGenerator : public MovementGenerator MovementGeneratorType _type; uint32 _pointId; TimeTrackerSmall _duration; + bool _launched; }; #endif diff --git a/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.h.bak-bug15-core-20261001-143921 b/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.h.bak-bug15-core-20261001-143921 new file mode 100644 index 0000000000..e1689f8341 --- /dev/null +++ b/src/server/game/Movement/MovementGenerators/GenericMovementGenerator.h.bak-bug15-core-20261001-143921 @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +#ifndef TRINITY_GENERICMOVEMENTGENERATOR_H +#define TRINITY_GENERICMOVEMENTGENERATOR_H + +#include "MovementGenerator.h" +#include "MoveSplineInit.h" +#include "Timer.h" + +class Unit; + +enum MovementGeneratorType : uint8; + +class GenericMovementGenerator : public MovementGenerator +{ + public: + explicit GenericMovementGenerator(Movement::MoveSplineInit&& splineInit, MovementGeneratorType type, uint32 id) : _splineInit(std::move(splineInit)), _type(type), _pointId(id), _duration(0) { } + + void Initialize(Unit*) override; + void Finalize(Unit*) override; + void Reset(Unit*) override { } + bool Update(Unit*, uint32) override; + MovementGeneratorType GetMovementGeneratorType() const override { return _type; } + + private: + void MovementInform(Unit*); + + Movement::MoveSplineInit _splineInit; + MovementGeneratorType _type; + uint32 _pointId; + TimeTrackerSmall _duration; +}; + +#endif diff --git a/src/server/game/Spells/SpellInfo.cpp b/src/server/game/Spells/SpellInfo.cpp index a0f2c9913d..28b5d013da 100644 --- a/src/server/game/Spells/SpellInfo.cpp +++ b/src/server/game/Spells/SpellInfo.cpp @@ -527,8 +527,10 @@ int32 SpellEffectInfo::CalcValue(WorldObject const* caster /*= nullptr*/, int32 { double comboDamage = CalcPointsPerResource(casterUnit); if (comboDamage != 0.0) - if (uint8 comboPoints = casterUnit->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints()) - value += comboDamage * comboPoints; + if (GameClient* gameClient = casterUnit->GetGameClientMovingMe()) + if (Player* player = gameClient->GetBasePlayer()) + if (uint8 comboPoints = player->GetComboPoints()) + value += comboDamage * comboPoints; } if (_spellInfo->HasAttribute(SPELL_ATTR8_MASTERY_AFFECTS_POINTS)) @@ -1933,6 +1935,12 @@ SpellCastResult SpellInfo::CheckLocation(uint32 map_id, uint32 zone_id, uint32 a SpellCastResult SpellInfo::CheckTarget(WorldObject const* caster, WorldObject const* target, bool implicit /*= true*/) const { + // VIANO_AURA_DEBUG_CHECKTARGET_V1 + Unit const* vianoTarget = target ? target->ToUnit() : nullptr; + if (Id == 465 && vianoTarget && vianoTarget->IsVianoAdventurer()) + TC_LOG_INFO("scripts", "[VianoAuraDebug] stage=CheckTarget spell=%u target=%s controlled=%u", + Id, vianoTarget->GetGUID().ToString().c_str(), vianoTarget->IsControlledByPlayer() ? 1u : 0u); + if (HasAttribute(SPELL_ATTR1_EXCLUDE_CASTER) && caster == target) return SPELL_FAILED_BAD_TARGETS; @@ -2026,7 +2034,12 @@ SpellCastResult SpellInfo::CheckTarget(WorldObject const* caster, WorldObject co if (HasAttribute(SPELL_ATTR3_ONLY_ON_PLAYER)) return SPELL_FAILED_TARGET_NOT_PLAYER; - if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) && unitTarget->IsControlledByPlayer()) + // VIANO_AURA_PLAYER_CONTROLLED_EXCEPTION_V1 + // Viano adventurers keep PLAYER_CONTROLLED for direct helpful targeting, + // but are intentionally eligible for group/raid buffs such as paladin auras. + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) + && unitTarget->IsControlledByPlayer() + && !unitTarget->IsVianoAdventurer()) return SPELL_FAILED_TARGET_IS_PLAYER_CONTROLLED; } else if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER)) diff --git a/src/server/game/Spells/SpellInfo.cpp.bak-viano-login-combopoints-20261006-140145 b/src/server/game/Spells/SpellInfo.cpp.bak-viano-login-combopoints-20261006-140145 new file mode 100644 index 0000000000..79ec3b0e8d --- /dev/null +++ b/src/server/game/Spells/SpellInfo.cpp.bak-viano-login-combopoints-20261006-140145 @@ -0,0 +1,4533 @@ +/* + * 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 . + */ + +#include "SpellInfo.h" +#include "Battleground.h" +#include "Corpse.h" +#include "Creature.h" +#include "DBCStores.h" +#include "GameClient.h" +#include "GameTables.h" +#include "InstanceScript.h" +#include "Item.h" +#include "ItemTemplate.h" +#include "Log.h" +#include "LootMgr.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "Random.h" +#include "Spell.h" +#include "SpellAuraDefines.h" +#include "SpellAuraEffects.h" +#include "SpellMgr.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +uint32 GetTargetFlagMask(SpellTargetObjectTypes objType) +{ + switch (objType) + { + case TARGET_OBJECT_TYPE_DEST: + return TARGET_FLAG_DEST_LOCATION; + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + return TARGET_FLAG_DEST_LOCATION | TARGET_FLAG_UNIT; + case TARGET_OBJECT_TYPE_CORPSE_ALLY: + return TARGET_FLAG_CORPSE_ALLY; + case TARGET_OBJECT_TYPE_CORPSE_ENEMY: + return TARGET_FLAG_CORPSE_ENEMY; + case TARGET_OBJECT_TYPE_CORPSE: + return TARGET_FLAG_CORPSE_ALLY | TARGET_FLAG_CORPSE_ENEMY; + case TARGET_OBJECT_TYPE_UNIT: + return TARGET_FLAG_UNIT; + case TARGET_OBJECT_TYPE_GOBJ: + return TARGET_FLAG_GAMEOBJECT; + case TARGET_OBJECT_TYPE_GOBJ_ITEM: + return TARGET_FLAG_GAMEOBJECT_ITEM; + case TARGET_OBJECT_TYPE_ITEM: + return TARGET_FLAG_ITEM; + case TARGET_OBJECT_TYPE_SRC: + return TARGET_FLAG_SOURCE_LOCATION; + default: + return TARGET_FLAG_NONE; + } +} + +SpellImplicitTargetInfo::SpellImplicitTargetInfo(uint32 target) +{ + _target = Targets(target); +} + +bool SpellImplicitTargetInfo::IsArea() const +{ + return GetSelectionCategory() == TARGET_SELECT_CATEGORY_AREA || GetSelectionCategory() == TARGET_SELECT_CATEGORY_CONE; +} + +SpellTargetSelectionCategories SpellImplicitTargetInfo::GetSelectionCategory() const +{ + return _data[_target].SelectionCategory; +} + +SpellTargetReferenceTypes SpellImplicitTargetInfo::GetReferenceType() const +{ + return _data[_target].ReferenceType; +} + +SpellTargetObjectTypes SpellImplicitTargetInfo::GetObjectType() const +{ + return _data[_target].ObjectType; +} + +SpellTargetCheckTypes SpellImplicitTargetInfo::GetCheckType() const +{ + return _data[_target].SelectionCheckType; +} + +SpellTargetDirectionTypes SpellImplicitTargetInfo::GetDirectionType() const +{ + return _data[_target].DirectionType; +} + +float SpellImplicitTargetInfo::CalcDirectionAngle(SpellEffectInfo const& effectInfo) const +{ + switch (GetDirectionType()) + { + case TARGET_DIR_FRONT: + return 0.0f; + case TARGET_DIR_BACK: + return static_cast(M_PI); + case TARGET_DIR_RIGHT: + return static_cast(-M_PI/2); + case TARGET_DIR_LEFT: + return static_cast(M_PI/2); + case TARGET_DIR_FRONT_RIGHT: + return static_cast(-M_PI/4); + case TARGET_DIR_BACK_RIGHT: + return static_cast(-3*M_PI/4); + case TARGET_DIR_BACK_LEFT: + return static_cast(3*M_PI/4); + case TARGET_DIR_FRONT_LEFT: + return static_cast(M_PI/4); + case TARGET_DIR_RANDOM: + return rand_norm() * static_cast(2 * M_PI); + case TARGET_DIR_SUMMON: + // This direction does alter its angle based on what is being summoned. + // Creatures are being summoned on the left, gameobjects infront + switch (effectInfo.Effect) + { + case SPELL_EFFECT_SUMMON_PET: + case SPELL_EFFECT_SUMMON: + return static_cast(M_PI / 2); + default: + return 0.0f; + } + default: + return 0.0f; + } +} + +Targets SpellImplicitTargetInfo::GetTarget() const +{ + return _target; +} + +uint32 SpellImplicitTargetInfo::GetExplicitTargetMask(bool& srcSet, bool& dstSet) const +{ + uint32 targetMask = 0; + if (GetTarget() == TARGET_DEST_TRAJ) + { + if (!srcSet) + targetMask = TARGET_FLAG_SOURCE_LOCATION; + if (!dstSet) + targetMask |= TARGET_FLAG_DEST_LOCATION; + } + else + { + switch (GetReferenceType()) + { + case TARGET_REFERENCE_TYPE_SRC: + if (srcSet) + break; + targetMask = TARGET_FLAG_SOURCE_LOCATION; + break; + case TARGET_REFERENCE_TYPE_DEST: + if (dstSet) + break; + targetMask = TARGET_FLAG_DEST_LOCATION; + break; + case TARGET_REFERENCE_TYPE_TARGET: + switch (GetObjectType()) + { + case TARGET_OBJECT_TYPE_GOBJ: + targetMask = TARGET_FLAG_GAMEOBJECT; + break; + case TARGET_OBJECT_TYPE_GOBJ_ITEM: + targetMask = TARGET_FLAG_GAMEOBJECT_ITEM; + break; + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + case TARGET_OBJECT_TYPE_UNIT: + case TARGET_OBJECT_TYPE_DEST: + switch (GetCheckType()) + { + case TARGET_CHECK_ENEMY: + targetMask = TARGET_FLAG_UNIT_ENEMY; + break; + case TARGET_CHECK_ALLY: + targetMask = TARGET_FLAG_UNIT_ALLY; + break; + case TARGET_CHECK_PARTY: + targetMask = TARGET_FLAG_UNIT_PARTY; + break; + case TARGET_CHECK_RAID: + targetMask = TARGET_FLAG_UNIT_RAID; + break; + case TARGET_CHECK_PASSENGER: + targetMask = TARGET_FLAG_UNIT_PASSENGER; + break; + case TARGET_CHECK_RAID_CLASS: + [[fallthrough]]; + default: + targetMask = TARGET_FLAG_UNIT; + break; + } + break; + default: + break; + } + break; + default: + break; + } + } + + switch (GetObjectType()) + { + case TARGET_OBJECT_TYPE_SRC: + srcSet = true; + break; + case TARGET_OBJECT_TYPE_DEST: + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + dstSet = true; + break; + default: + break; + } + return targetMask; +} + +SpellImplicitTargetInfo::StaticData SpellImplicitTargetInfo::_data[TOTAL_SPELL_TARGETS] = +{ + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 1 TARGET_UNIT_CASTER + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 2 TARGET_UNIT_NEARBY_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 3 TARGET_UNIT_NEARBY_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 4 TARGET_UNIT_NEARBY_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 5 TARGET_UNIT_PET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 6 TARGET_UNIT_TARGET_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 7 TARGET_UNIT_SRC_AREA_ENTRY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 8 TARGET_UNIT_DEST_AREA_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 9 TARGET_DEST_HOME + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 10 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 11 TARGET_UNIT_SRC_AREA_UNK_11 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 12 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 13 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 14 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 15 TARGET_UNIT_SRC_AREA_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 16 TARGET_UNIT_DEST_AREA_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 17 TARGET_DEST_DB + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 18 TARGET_DEST_CASTER + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 19 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 20 TARGET_UNIT_CASTER_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 21 TARGET_UNIT_TARGET_ALLY + {TARGET_OBJECT_TYPE_SRC, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 22 TARGET_SRC_CASTER + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 23 TARGET_GAMEOBJECT_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 24 TARGET_UNIT_CONE_ENEMY_24 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 25 TARGET_UNIT_TARGET_ANY + {TARGET_OBJECT_TYPE_GOBJ_ITEM, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 26 TARGET_GAMEOBJECT_ITEM_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 27 TARGET_UNIT_MASTER + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 28 TARGET_DEST_DYNOBJ_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 29 TARGET_DEST_DYNOBJ_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 30 TARGET_UNIT_SRC_AREA_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 31 TARGET_UNIT_DEST_AREA_ALLY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_SUMMON }, // 32 TARGET_DEST_CASTER_SUMMON + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 33 TARGET_UNIT_SRC_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 34 TARGET_UNIT_DEST_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 35 TARGET_UNIT_TARGET_PARTY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 36 TARGET_DEST_CASTER_UNK_36 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_LAST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 37 TARGET_UNIT_LASTTARGET_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 38 TARGET_UNIT_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 39 TARGET_DEST_CASTER_FISHING + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 40 TARGET_GAMEOBJECT_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 41 TARGET_DEST_CASTER_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 42 TARGET_DEST_CASTER_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 43 TARGET_DEST_CASTER_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 44 TARGET_DEST_CASTER_FRONT_LEFT + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 45 TARGET_UNIT_TARGET_CHAINHEAL_ALLY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 46 TARGET_DEST_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 47 TARGET_DEST_CASTER_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 48 TARGET_DEST_CASTER_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 49 TARGET_DEST_CASTER_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 50 TARGET_DEST_CASTER_LEFT + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 51 TARGET_GAMEOBJECT_SRC_AREA + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 52 TARGET_GAMEOBJECT_DEST_AREA + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 53 TARGET_DEST_TARGET_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 54 TARGET_UNIT_CONE_180_DEG_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 55 TARGET_DEST_CASTER_FRONT_LEAP + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 56 TARGET_UNIT_CASTER_AREA_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 57 TARGET_UNIT_TARGET_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 58 TARGET_UNIT_NEARBY_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ALLY, TARGET_DIR_FRONT}, // 59 TARGET_UNIT_CONE_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENTRY, TARGET_DIR_FRONT}, // 60 TARGET_UNIT_CONE_ENTRY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID_CLASS, TARGET_DIR_NONE}, // 61 TARGET_UNIT_TARGET_AREA_RAID_CLASS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 62 TARGET_DEST_CASTER_GROUND + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 63 TARGET_DEST_TARGET_ANY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 64 TARGET_DEST_TARGET_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 65 TARGET_DEST_TARGET_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 66 TARGET_DEST_TARGET_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 67 TARGET_DEST_TARGET_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 68 TARGET_DEST_TARGET_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 69 TARGET_DEST_TARGET_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 70 TARGET_DEST_TARGET_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 71 TARGET_DEST_TARGET_FRONT_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 72 TARGET_DEST_CASTER_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 73 TARGET_DEST_CASTER_RADIUS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 74 TARGET_DEST_TARGET_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 75 TARGET_DEST_TARGET_RADIUS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 76 TARGET_DEST_CHANNEL_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 77 TARGET_UNIT_CHANNEL_TARGET + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 78 TARGET_DEST_DEST_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 79 TARGET_DEST_DEST_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 80 TARGET_DEST_DEST_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 81 TARGET_DEST_DEST_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 82 TARGET_DEST_DEST_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 83 TARGET_DEST_DEST_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 84 TARGET_DEST_DEST_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 85 TARGET_DEST_DEST_FRONT_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 86 TARGET_DEST_DEST_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 87 TARGET_DEST_DEST + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 88 TARGET_DEST_DYNOBJ_NONE + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_TRAJ, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 89 TARGET_DEST_TRAJ + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 90 TARGET_UNIT_TARGET_MINIPET + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 91 TARGET_DEST_DEST_RADIUS + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 92 TARGET_UNIT_SUMMONER + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 93 TARGET_CORPSE_SRC_AREA_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 94 TARGET_UNIT_VEHICLE + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_PASSENGER, TARGET_DIR_NONE}, // 95 TARGET_UNIT_TARGET_PASSENGER + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 96 TARGET_UNIT_PASSENGER_0 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 97 TARGET_UNIT_PASSENGER_1 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 98 TARGET_UNIT_PASSENGER_2 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 99 TARGET_UNIT_PASSENGER_3 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 100 TARGET_UNIT_PASSENGER_4 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 101 TARGET_UNIT_PASSENGER_5 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 102 TARGET_UNIT_PASSENGER_6 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 103 TARGET_UNIT_PASSENGER_7 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 104 TARGET_UNIT_CONE_CASTER_TO_DEST_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 105 TARGET_UNIT_CASTER_AND_PASSENGERS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 106 TARGET_DEST_CHANNEL_CASTER + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 107 TARGET_DEST_NEARBY_ENTRY_2 + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 108 TARGET_GAMEOBJECT_CONE_CASTER_TO_DEST_ENEMY + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ALLY, TARGET_DIR_FRONT}, // 109 TARGET_GAMEOBJECT_CONE_CASTER_TO_DEST_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENTRY, TARGET_DIR_FRONT}, // 110 TARGET_UNIT_CONE_CASTER_TO_DEST_ENTRY + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 111 + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 112 TARGET_DEST_TARGET_ANY_2 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 113 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 114 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 115 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 116 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 117 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 118 TARGET_UNIT_TARGET_ALLY_OR_RAID + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 119 TARGET_CORPSE_SRC_AREA_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_SUMMONED, TARGET_DIR_NONE}, // 120 TARGET_UNIT_SELF_AND_SUMMONS + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 121 TARGET_CORPSE_TARGET_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_THREAT, TARGET_DIR_NONE}, // 122 TARGET_UNIT_AREA_THREAT_LIST + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_TAP, TARGET_DIR_NONE}, // 123 TARGET_UNIT_AREA_TAP_LIST + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 124 TARGET_UNIT_TARGET_TAP_LIST + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 125 TARGET_DEST_CASTER_GROUND_2 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE }, // 126 TARGET_UNIT_AREA_ENEMY + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 127 +}; + +SpellEffectInfo::SpellEffectInfo(SpellEntry const* /*spellEntry*/, SpellInfo const* spellInfo, uint8 effIndex, SpellEffectEntry const* _effect) +{ + SpellScalingEntry const* scaling = spellInfo->GetSpellScaling(); + + _spellInfo = spellInfo; + _effIndex = _effect ? _effect->EffectIndex : effIndex; + Effect = _effect ? _effect->Effect : 0; + ApplyAuraName = _effect ? _effect->EffectAura : 0; + AuraPeriod = _effect ? _effect->EffectAuraPeriod : 0; + DieSides = _effect ? _effect->EffectDieSides : 0; + RealPointsPerLevel = _effect ? _effect->EffectRealPointsPerLevel : 0.0f; + BasePoints = _effect ? _effect->EffectBasePoints : 0; + PointsPerComboPoint = _effect ? _effect->EffectPointsPerResource : 0.0f; + Amplitude = _effect ? _effect->EffectAmplitude : 0.0f; + DamageMultiplier = _effect ? _effect->EffectChainAmplitude : 0.0f; + BonusMultiplier = _effect ? _effect->EffectBonusCoefficient : 0.0f; + MiscValue = _effect ? _effect->EffectMiscValue : 0; + MiscValueB = _effect ? _effect->EffectMiscValueB : 0; + Mechanic = Mechanics(_effect ? _effect->EffectMechanic : 0); + TargetA = SpellImplicitTargetInfo(_effect ? _effect->EffectImplicitTargetA : 0); + TargetB = SpellImplicitTargetInfo(_effect ? _effect->EffectImplicitTargetB : 0); + TargetARadiusEntry = _effect && _effect->EffectRadiusIndex ? sSpellRadiusStore.LookupEntry(_effect->EffectRadiusIndex) : nullptr; + TargetBRadiusEntry = _effect && _effect->EffectRadiusMaxIndex ? sSpellRadiusStore.LookupEntry(_effect->EffectRadiusMaxIndex) : nullptr; + ChainTarget = _effect ? _effect->EffectChainTargets : 0; + ItemType = _effect ? _effect->EffectItemType : 0; + TriggerSpell = _effect ? _effect->EffectTriggerSpell : 0; + SpellClassMask = _effect ? _effect->EffectSpellClassMask : flag96(0); + ImplicitTargetConditions = nullptr; + + Scaling.Coefficient = scaling ? scaling->Coefficient[_effIndex] : 0.0f; + Scaling.Variance = scaling ? scaling->Variance[_effIndex] : 0.0f; + Scaling.ComboPointsCoefficient = scaling ? scaling->ComboPointsCoefficient[_effIndex] : 0.0f; +} + +bool SpellEffectInfo::IsEffect() const +{ + return Effect != 0; +} + +bool SpellEffectInfo::IsEffect(SpellEffects effectName) const +{ + return Effect == effectName; +} + +bool SpellEffectInfo::IsAura() const +{ + return (IsUnitOwnedAuraEffect() || Effect == SPELL_EFFECT_PERSISTENT_AREA_AURA) && ApplyAuraName != 0; +} + +bool SpellEffectInfo::IsAura(AuraType aura) const +{ + return IsAura() && ApplyAuraName == aura; +} + +bool SpellEffectInfo::IsTargetingArea() const +{ + return TargetA.IsArea() || TargetB.IsArea(); +} + +bool SpellEffectInfo::IsAreaAuraEffect() const +{ + if (Effect == SPELL_EFFECT_APPLY_AREA_AURA_PARTY || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_FRIEND || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_ENEMY || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_PET || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_OWNER) + return true; + return false; +} + +bool SpellEffectInfo::IsUnitOwnedAuraEffect() const +{ + return IsAreaAuraEffect() || Effect == SPELL_EFFECT_APPLY_AURA || Effect == SPELL_EFFECT_APPLY_AURA_2; +} + +bool SpellEffectInfo::ScalesWithCreatureLevel() const +{ + if (!_spellInfo->HasAttribute(SPELL_ATTR0_SCALES_WITH_CREATURE_LEVEL)) + return false; + + switch (Effect) + { + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_DUMMY: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_HEAL: + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_POWER_BURN: + case SPELL_EFFECT_SCRIPT_EFFECT: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_FORCE_CAST_WITH_VALUE: + case SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE: + case SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE: + return true; + default: + break; + } + + switch (ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_DUMMY: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_DAMAGE_SHIELD: + case SPELL_AURA_PROC_TRIGGER_DAMAGE: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_SCHOOL_ABSORB: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + return true; + default: + break; + } + + return false; +} + +int32 SpellEffectInfo::CalcValue(WorldObject const* caster /*= nullptr*/, int32 const* bp /*= nullptr*/, Unit const* target /*= nullptr*/) const +{ + float basePointsPerLevel = RealPointsPerLevel; + // TODO: this needs to be a float, not rounded + int basePoints = CalcBaseValue(caster, target); + double value = bp ? *bp : basePoints; + + Unit const* casterUnit = nullptr; + if (caster) + casterUnit = caster->ToUnit(); + + if (Scaling.Variance != 0.0f) + { + float delta = fabs(Scaling.Variance * 0.5f); + double valueVariance = frand(-delta, delta); + value += double(basePoints) * valueVariance; + } + else if (DieSides) + { + // roll in a range <1;EffectDieSides> as of patch 3.3.3 + if (DieSides == 1) + value += DieSides; + else + value += (DieSides >= 1) ? irand(1, DieSides) : irand(DieSides, 1); + } + + // base amount modification based on spell lvl vs caster lvl + if (Scaling.Coefficient == 0.0f) + { + if (casterUnit && basePointsPerLevel != 0.0) + { + int32 level = int32(casterUnit->getLevel()); + if (level > int32(_spellInfo->MaxLevel) && _spellInfo->MaxLevel > 0) + level = int32(_spellInfo->MaxLevel); + + // if base level is greater than spell level, reduce by base level (eg. pilgrims foods) + level -= int32(std::max(_spellInfo->BaseLevel, _spellInfo->SpellLevel)); + if (level < 0) + level = 0; + value += level * basePointsPerLevel; + } + } + + // bonus amount from combo points + if (casterUnit && _spellInfo->HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DAMAGE)) + { + double comboDamage = CalcPointsPerResource(casterUnit); + if (comboDamage != 0.0) + if (uint8 comboPoints = casterUnit->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints()) + value += comboDamage * comboPoints; + } + + if (_spellInfo->HasAttribute(SPELL_ATTR8_MASTERY_AFFECTS_POINTS)) + if (Player const* playerCaster = caster ? caster->ToPlayer() : nullptr) + value += playerCaster->GetFloatValue(PLAYER_MASTERY) * BonusMultiplier; + + if (casterUnit) + value = casterUnit->ApplyEffectModifiers(_spellInfo, _effIndex, value); + + return int32(round(value)); +} + +int32 SpellEffectInfo::CalcBaseValue(WorldObject const* caster, Unit const* target) const +{ + if (Scaling.Coefficient != 0.0f) + { + uint32 level = _spellInfo->SpellLevel; + if (target && _spellInfo->HasAttribute(SPELL_ATTR8_USE_TARGETS_LEVEL_FOR_SPELL_SCALING)) + level = target->getLevel(); + else if (caster && caster->IsUnit()) + level = caster->ToUnit()->getLevel(); + + if (_spellInfo->BaseLevel && _spellInfo->HasAttribute(SPELL_ATTR10_USE_SPELL_BASE_LEVEL_FOR_SCALING)) + level = _spellInfo->BaseLevel; + + float value = 0.0f; + + if (level > 0) + { + if (!_spellInfo->Scaling.Class) + return 0; + + value = GameTables::GetSpellScalingValue(level, static_cast(_spellInfo->Scaling.Class > 0 ? _spellInfo->Scaling.Class: (MAX_CLASSES - 1 /*last class*/) - _spellInfo->Scaling.Class)); + value *= _spellInfo->GetSpellScalingMultiplier(level, false); + } + + value *= Scaling.Coefficient; + if (value > 0.0f) + value = std::max(value, 1.0f); + + return int32(round(value)); + } + else + { + float value = BasePoints; + + if (ScalesWithCreatureLevel()) + { + // Some spells use the internal game table to calculate scaling coefficients for creatures + if (Creature const* creatureCaster = Object::ToCreature(caster)) + { + uint8 casterClass = creatureCaster->getClass(); + uint8 expansion = creatureCaster->GetCreatureTemplate()->GetHealthScalingExpansion(); + + float spellDamage = GetGameTableColumnForClass(sNpcDamageByClassGameTable[expansion].GetRow(_spellInfo->SpellLevel), casterClass); + float creatureDamage = GetGameTableColumnForClass(sNpcDamageByClassGameTable[expansion].GetRow(creatureCaster->getLevel()), casterClass); + + if (spellDamage != 0.0f) + value *= creatureDamage / spellDamage; + } + } + + return int32(round(value)); + } +} + +// Based on client function QuestTextParser::Spell_C_GetPointsPerResource (build 15595) +double SpellEffectInfo::CalcPointsPerResource(WorldObject const* caster) const +{ + double value = 0.0; + if (caster && caster->IsUnit()) + { + if (Scaling.Coefficient != 0.0f) + { + if (!_spellInfo->Scaling.Class) + return 0.0; + + int32 scalingClass = _spellInfo->Scaling.Class; + if (scalingClass < 0) + scalingClass = (MAX_CLASSES - 1 /*last class*/) - scalingClass; + + float scaledAmount = GameTables::GetSpellScalingValue(caster->ToUnit()->getLevel(), static_cast(scalingClass)); + scaledAmount *= _spellInfo->GetSpellScalingMultiplier(caster->ToUnit()->getLevel(), false) * Scaling.ComboPointsCoefficient; + value = scaledAmount; + } + } + + if (std::fabs(value) < 0.00000023841858 && PointsPerComboPoint != 0.0f) + return PointsPerComboPoint; + + return value; +} + +float SpellEffectInfo::CalcValueMultiplier(WorldObject* caster, Spell* spell) const +{ + float multiplier = Amplitude; + if (Player* modOwner = (caster ? caster->GetSpellModOwner() : nullptr)) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::Amplitude, multiplier, spell); + return multiplier; +} + +float SpellEffectInfo::CalcDamageMultiplier(WorldObject* caster, Spell* spell) const +{ + float multiplierPercent = DamageMultiplier * 100.0f; + if (Player* modOwner = (caster ? caster->GetSpellModOwner() : nullptr)) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::ChainAmplitude, multiplierPercent, spell); + return multiplierPercent / 100.0f; +} + +bool SpellEffectInfo::HasRadius(SpellTargetIndex targetIndex) const +{ + switch (targetIndex) + { + case SpellTargetIndex::TargetA: + return TargetARadiusEntry != nullptr; + case SpellTargetIndex::TargetB: + return TargetBRadiusEntry != nullptr; + default: + return false; + } +} + +float SpellEffectInfo::CalcRadius(WorldObject* caster /*= nullptr*/, SpellTargetIndex targetIndex /*=SpellTargetIndex::TargetA*/, Spell* spell /*= nullptr*/) const +{ + // TargetA -> TargetARadiusEntry + // TargetB -> TargetBRadiusEntry + // Aura effects have TargetARadiusEntry == TargetBRadiusEntry (mostly) + SpellImplicitTargetInfo target = TargetA; + SpellRadiusEntry const* entry = TargetARadiusEntry; + if (targetIndex == SpellTargetIndex::TargetB && HasRadius(targetIndex)) + { + target = TargetB; + entry = TargetBRadiusEntry; + } + + if (!entry) + return 0.0f; + + float radius = entry->RadiusMin; + + // Random targets use random value between RadiusMin and RadiusMax + // For other cases, client uses RadiusMax if RadiusMin is 0 + if (target.GetTarget() == TARGET_DEST_CASTER_RANDOM || + target.GetTarget() == TARGET_DEST_TARGET_RANDOM || + target.GetTarget() == TARGET_DEST_DEST_RANDOM) + radius += (entry->RadiusMax - radius) * std::sqrt(rand_norm()); + else if (radius == 0.0f) + radius = entry->RadiusMax; + + if (caster) + { + if (Unit* casterUnit = caster->ToUnit()) + radius += entry->RadiusPerLevel * casterUnit->getLevel(); + + radius = std::min(radius, entry->RadiusMax); + + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::Radius, radius, spell); + } + + return radius; +} + +uint32 SpellEffectInfo::GetProvidedTargetMask() const +{ + return GetTargetFlagMask(TargetA.GetObjectType()) | GetTargetFlagMask(TargetB.GetObjectType()); +} + +uint32 SpellEffectInfo::GetMissingTargetMask(bool srcSet /*= false*/, bool dstSet /*= false*/, uint32 mask /*=0*/) const +{ + uint32 effImplicitTargetMask = GetTargetFlagMask(GetUsedTargetObjectType()); + uint32 providedTargetMask = GetProvidedTargetMask() | mask; + + // remove all flags covered by effect target mask + if (providedTargetMask & TARGET_FLAG_UNIT_MASK) + effImplicitTargetMask &= ~(TARGET_FLAG_UNIT_MASK); + if (providedTargetMask & TARGET_FLAG_CORPSE_MASK) + effImplicitTargetMask &= ~(TARGET_FLAG_UNIT_MASK | TARGET_FLAG_CORPSE_MASK); + if (providedTargetMask & TARGET_FLAG_GAMEOBJECT_ITEM) + effImplicitTargetMask &= ~(TARGET_FLAG_GAMEOBJECT_ITEM | TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_ITEM); + if (providedTargetMask & TARGET_FLAG_GAMEOBJECT) + effImplicitTargetMask &= ~(TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_GAMEOBJECT_ITEM); + if (providedTargetMask & TARGET_FLAG_ITEM) + effImplicitTargetMask &= ~(TARGET_FLAG_ITEM | TARGET_FLAG_GAMEOBJECT_ITEM); + if (dstSet || providedTargetMask & TARGET_FLAG_DEST_LOCATION) + effImplicitTargetMask &= ~(TARGET_FLAG_DEST_LOCATION); + if (srcSet || providedTargetMask & TARGET_FLAG_SOURCE_LOCATION) + effImplicitTargetMask &= ~(TARGET_FLAG_SOURCE_LOCATION); + + return effImplicitTargetMask; +} + +SpellEffectImplicitTargetTypes SpellEffectInfo::GetImplicitTargetType() const +{ + return _data[Effect].ImplicitTargetType; +} + +SpellTargetObjectTypes SpellEffectInfo::GetUsedTargetObjectType() const +{ + return _data[Effect].UsedTargetObjectType; +} + +SpellEffectInfo::StaticData SpellEffectInfo::_data[TOTAL_SPELL_EFFECTS] = +{ + // implicit target type used target object type + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 0 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 1 SPELL_EFFECT_INSTAKILL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 2 SPELL_EFFECT_SCHOOL_DAMAGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 3 SPELL_EFFECT_DUMMY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 4 SPELL_EFFECT_PORTAL_TELEPORT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 5 SPELL_EFFECT_TELEPORT_UNITS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 6 SPELL_EFFECT_APPLY_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 7 SPELL_EFFECT_ENVIRONMENTAL_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 8 SPELL_EFFECT_POWER_DRAIN + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 9 SPELL_EFFECT_HEALTH_LEECH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 10 SPELL_EFFECT_HEAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 11 SPELL_EFFECT_BIND + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 12 SPELL_EFFECT_PORTAL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 13 SPELL_EFFECT_RITUAL_BASE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 14 SPELL_EFFECT_RITUAL_SPECIALIZE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 15 SPELL_EFFECT_RITUAL_ACTIVATE_PORTAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 16 SPELL_EFFECT_QUEST_COMPLETE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 17 SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 18 SPELL_EFFECT_RESURRECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 19 SPELL_EFFECT_ADD_EXTRA_ATTACKS + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 20 SPELL_EFFECT_DODGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 21 SPELL_EFFECT_EVADE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 22 SPELL_EFFECT_PARRY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 23 SPELL_EFFECT_BLOCK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 24 SPELL_EFFECT_CREATE_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 25 SPELL_EFFECT_WEAPON + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 26 SPELL_EFFECT_DEFENSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 27 SPELL_EFFECT_PERSISTENT_AREA_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 28 SPELL_EFFECT_SUMMON + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 29 SPELL_EFFECT_LEAP + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 30 SPELL_EFFECT_ENERGIZE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 31 SPELL_EFFECT_WEAPON_PERCENT_DAMAGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 32 SPELL_EFFECT_TRIGGER_MISSILE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ_ITEM}, // 33 SPELL_EFFECT_OPEN_LOCK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 34 SPELL_EFFECT_SUMMON_CHANGE_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 35 SPELL_EFFECT_APPLY_AREA_AURA_PARTY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 36 SPELL_EFFECT_LEARN_SPELL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 37 SPELL_EFFECT_SPELL_DEFENSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 38 SPELL_EFFECT_DISPEL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 39 SPELL_EFFECT_LANGUAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 40 SPELL_EFFECT_DUAL_WIELD + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 41 SPELL_EFFECT_JUMP + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 42 SPELL_EFFECT_JUMP_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 43 SPELL_EFFECT_TELEPORT_UNITS_FACE_CASTER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 44 SPELL_EFFECT_SKILL_STEP + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 45 SPELL_EFFECT_ADD_HONOR + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 46 SPELL_EFFECT_SPAWN + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 47 SPELL_EFFECT_TRADE_SKILL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 48 SPELL_EFFECT_STEALTH + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 49 SPELL_EFFECT_DETECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 50 SPELL_EFFECT_TRANS_DOOR + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 51 SPELL_EFFECT_FORCE_CRITICAL_HIT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 52 SPELL_EFFECT_GUARANTEE_HIT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 53 SPELL_EFFECT_ENCHANT_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 54 SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 55 SPELL_EFFECT_TAMECREATURE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 56 SPELL_EFFECT_SUMMON_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 57 SPELL_EFFECT_LEARN_PET_SPELL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 58 SPELL_EFFECT_WEAPON_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 59 SPELL_EFFECT_CREATE_RANDOM_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 60 SPELL_EFFECT_PROFICIENCY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 61 SPELL_EFFECT_SEND_EVENT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 62 SPELL_EFFECT_POWER_BURN + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 63 SPELL_EFFECT_THREAT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 64 SPELL_EFFECT_TRIGGER_SPELL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 65 SPELL_EFFECT_APPLY_AREA_AURA_RAID + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 66 SPELL_EFFECT_CREATE_MANA_GEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 67 SPELL_EFFECT_HEAL_MAX_HEALTH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 68 SPELL_EFFECT_INTERRUPT_CAST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 69 SPELL_EFFECT_DISTRACT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 70 SPELL_EFFECT_PULL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 71 SPELL_EFFECT_PICKPOCKET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 72 SPELL_EFFECT_ADD_FARSIGHT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 73 SPELL_EFFECT_UNTRAIN_TALENTS + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 74 SPELL_EFFECT_APPLY_GLYPH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 75 SPELL_EFFECT_HEAL_MECHANICAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 76 SPELL_EFFECT_SUMMON_OBJECT_WILD + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 77 SPELL_EFFECT_SCRIPT_EFFECT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 78 SPELL_EFFECT_ATTACK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 79 SPELL_EFFECT_SANCTUARY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 80 SPELL_EFFECT_ADD_COMBO_POINTS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 81 SPELL_EFFECT_CREATE_HOUSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 82 SPELL_EFFECT_BIND_SIGHT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 83 SPELL_EFFECT_DUEL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 84 SPELL_EFFECT_STUCK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 85 SPELL_EFFECT_SUMMON_PLAYER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 86 SPELL_EFFECT_ACTIVATE_OBJECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 87 SPELL_EFFECT_GAMEOBJECT_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 88 SPELL_EFFECT_GAMEOBJECT_REPAIR + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 89 SPELL_EFFECT_GAMEOBJECT_SET_DESTRUCTION_STATE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 90 SPELL_EFFECT_KILL_CREDIT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 91 SPELL_EFFECT_THREAT_ALL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 92 SPELL_EFFECT_ENCHANT_HELD_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 93 SPELL_EFFECT_FORCE_DESELECT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 94 SPELL_EFFECT_SELF_RESURRECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 95 SPELL_EFFECT_SKINNING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 96 SPELL_EFFECT_CHARGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 97 SPELL_EFFECT_CAST_BUTTON + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 98 SPELL_EFFECT_KNOCK_BACK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 99 SPELL_EFFECT_DISENCHANT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 100 SPELL_EFFECT_INEBRIATE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 101 SPELL_EFFECT_FEED_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 102 SPELL_EFFECT_DISMISS_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 103 SPELL_EFFECT_REPUTATION + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 104 SPELL_EFFECT_SUMMON_OBJECT_SLOT1 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 105 SPELL_EFFECT_SUMMON_OBJECT_SLOT2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 106 SPELL_EFFECT_SUMMON_OBJECT_SLOT3 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 107 SPELL_EFFECT_SUMMON_OBJECT_SLOT4 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 108 SPELL_EFFECT_DISPEL_MECHANIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 109 SPELL_EFFECT_SUMMON_DEAD_PET + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 110 SPELL_EFFECT_DESTROY_ALL_TOTEMS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 111 SPELL_EFFECT_DURABILITY_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 112 SPELL_EFFECT_112 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 113 SPELL_EFFECT_RESURRECT_NEW + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 114 SPELL_EFFECT_ATTACK_ME + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 115 SPELL_EFFECT_DURABILITY_DAMAGE_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ENEMY}, // 116 SPELL_EFFECT_SKIN_PLAYER_CORPSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 117 SPELL_EFFECT_SPIRIT_HEAL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 118 SPELL_EFFECT_SKILL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 119 SPELL_EFFECT_APPLY_AREA_AURA_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 120 SPELL_EFFECT_TELEPORT_GRAVEYARD + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 121 SPELL_EFFECT_NORMALIZED_WEAPON_DMG + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 122 SPELL_EFFECT_122 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 123 SPELL_EFFECT_SEND_TAXI + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 124 SPELL_EFFECT_PULL_TOWARDS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 125 SPELL_EFFECT_MODIFY_THREAT_PERCENT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 126 SPELL_EFFECT_STEAL_BENEFICIAL_BUFF + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 127 SPELL_EFFECT_PROSPECTING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 128 SPELL_EFFECT_APPLY_AREA_AURA_FRIEND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 129 SPELL_EFFECT_APPLY_AREA_AURA_ENEMY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 130 SPELL_EFFECT_REDIRECT_THREAT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 131 SPELL_EFFECT_PLAY_SOUND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 132 SPELL_EFFECT_PLAY_MUSIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 133 SPELL_EFFECT_UNLEARN_SPECIALIZATION + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 134 SPELL_EFFECT_KILL_CREDIT2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 135 SPELL_EFFECT_CALL_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 136 SPELL_EFFECT_HEAL_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 137 SPELL_EFFECT_ENERGIZE_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 138 SPELL_EFFECT_LEAP_BACK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 139 SPELL_EFFECT_CLEAR_QUEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 140 SPELL_EFFECT_FORCE_CAST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 141 SPELL_EFFECT_FORCE_CAST_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 142 SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 143 SPELL_EFFECT_APPLY_AREA_AURA_OWNER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 144 SPELL_EFFECT_KNOCK_BACK_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 145 SPELL_EFFECT_PULL_TOWARDS_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 146 SPELL_EFFECT_ACTIVATE_RUNE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 147 SPELL_EFFECT_QUEST_FAIL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 148 SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 149 SPELL_EFFECT_CHARGE_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 150 SPELL_EFFECT_QUEST_START + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 151 SPELL_EFFECT_TRIGGER_SPELL_2 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 152 SPELL_EFFECT_SUMMON_RAF_FRIEND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 153 SPELL_EFFECT_CREATE_TAMED_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 154 SPELL_EFFECT_DISCOVER_TAXI + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 155 SPELL_EFFECT_TITAN_GRIP + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 156 SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 157 SPELL_EFFECT_CREATE_ITEM_2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 158 SPELL_EFFECT_MILLING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 159 SPELL_EFFECT_ALLOW_RENAME_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 160 SPELL_EFFECT_FORCE_CAST_2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 161 SPELL_EFFECT_TALENT_SPEC_COUNT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 162 SPELL_EFFECT_TALENT_SPEC_SELECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 163 SPELL_EFFECT_163 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 164 SPELL_EFFECT_REMOVE_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 165 SPELL_EFFECT_165 + {EFFECT_IMPLICIT_TARGET_CASTER, TARGET_OBJECT_TYPE_UNIT}, // 166 SPELL_EFFECT_GIVE_CURRENCY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 167 SPELL_EFFECT_167 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 168 SPELL_EFFECT_168 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_ITEM}, // 169 SPELL_EFFECT_DESTROY_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 170 SPELL_EFFECT_170 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 171 SPELL_EFFECT_SUMMON_PERSONAL_GAMEOBJECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 172 SPELL_EFFECT_RESURRECT_WITH_AURA + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 173 SPELL_EFFECT_UNLOCK_GUILD_VAULT_TAB + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 174 SPELL_EFFECT_APPLY_AURA_2 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 175 SPELL_EFFECT_175 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 176 SPELL_EFFECT_176 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 177 SPELL_EFFECT_177 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 178 SPELL_EFFECT_178 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 179 SPELL_EFFECT_CREATE_AREATRIGGER + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 180 SPELL_EFFECT_180 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 181 SPELL_EFFECT_181 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 182 SPELL_EFFECT_182 +}; + +SpellInfo::SpellInfo(SpellEntry const* spellEntry, SpellEffectEntry const** effects) +{ + Id = spellEntry->ID; + Attributes = spellEntry->Attributes; + AttributesEx = spellEntry->AttributesEx; + AttributesEx2 = spellEntry->AttributesEx2; + AttributesEx3 = spellEntry->AttributesEx3; + AttributesEx4 = spellEntry->AttributesEx4; + AttributesEx5 = spellEntry->AttributesEx5; + AttributesEx6 = spellEntry->AttributesEx6; + AttributesEx7 = spellEntry->AttributesEx7; + AttributesEx8 = spellEntry->AttributesEx8; + AttributesEx9 = spellEntry->AttributesEx9; + AttributesEx10 = spellEntry->AttributesEx10; + AttributesCu = 0; + CastTimeEntry = spellEntry->CastingTimeIndex ? sSpellCastTimesStore.LookupEntry(spellEntry->CastingTimeIndex) : nullptr; + DurationEntry = spellEntry->DurationIndex ? sSpellDurationStore.LookupEntry(spellEntry->DurationIndex) : nullptr; + PowerType = spellEntry->PowerType; + RangeEntry = spellEntry->RangeIndex ? sSpellRangeStore.LookupEntry(spellEntry->RangeIndex) : nullptr; + Speed = spellEntry->Speed; + + for (uint8 i = 0; i < 2; ++i) + SpellVisual[i] = spellEntry->SpellVisualID[i]; + + SpellIconID = spellEntry->SpellIconID; + ActiveIconID = spellEntry->ActiveIconID; + SpellName = spellEntry->Name; + Rank = spellEntry->NameSubtext; + SchoolMask = spellEntry->SchoolMask; + RuneCostID = spellEntry->RuneCostID; + SpellDifficultyId = spellEntry->Difficulty; + BonusCoefficientFromAP = spellEntry->BonusCoefficient; + SpellScalingId = spellEntry->ScalingID; + SpellAuraOptionsId = spellEntry->AuraOptionsID; + SpellAuraRestrictionsId = spellEntry->AuraRestrictionsID; + SpellCastingRequirementsId = spellEntry->CastingRequirementsID; + SpellCategoriesId = spellEntry->CategoriesID; + SpellClassOptionsId = spellEntry->ClassOptionsID; + SpellCooldownsId = spellEntry->CooldownsID; + SpellEquippedItemsId = spellEntry->EquippedItemsID; + SpellInterruptsId = spellEntry->InterruptsID; + SpellLevelsId = spellEntry->LevelsID; + SpellPowerId = spellEntry->PowerDisplayID; + SpellReagentsId = spellEntry->ReagentsID; + SpellShapeshiftId = spellEntry->ShapeshiftID; + SpellTargetRestrictionsId = spellEntry->TargetRestrictionsID; + SpellTotemsId = spellEntry->TotemsID; + ResearchProjectId = spellEntry->RequiredProjectID; + + // SpellDifficultyEntry + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + Effects[i] = SpellEffectInfo(spellEntry, this, i, effects[i]); + + // SpellScalingEntry + SpellScalingEntry const* _scaling = GetSpellScaling(); + Scaling.CastTimeMin = _scaling ? _scaling->CastTimeMin : 0; + Scaling.CastTimeMax = _scaling ? _scaling->CastTimeMax : 0; + Scaling.CastTimeMaxLevel = _scaling ? _scaling->CastTimeMaxLevel : 0; + Scaling.Class = _scaling ? _scaling->Class : 0; + Scaling.NerfFactor = _scaling ? _scaling->NerfFactor : 0.f; + Scaling.NerfMaxLevel = _scaling ? _scaling->NerfMaxLevel : 0; + + // SpellAuraOptionsEntry + SpellAuraOptionsEntry const* _options = GetSpellAuraOptions(); + ProcFlags = _options ? ::ProcFlags(_options->ProcTypeMask) : PROC_FLAG_NONE; + ProcChance = _options ? _options->ProcChance : 0; + ProcCharges = _options ? _options->ProcCharges : 0; + StackAmount = _options ? _options->CumulativeAura : 0; + + // SpellAuraRestrictionsEntry + SpellAuraRestrictionsEntry const* _aura = GetSpellAuraRestrictions(); + CasterAuraState = _aura ? _aura->CasterAuraState : 0; + TargetAuraState = _aura ? _aura->TargetAuraState : 0; + CasterAuraStateNot = _aura ? _aura->ExcludeCasterAuraState : 0; + TargetAuraStateNot = _aura ? _aura->ExcludeTargetAuraState : 0; + CasterAuraSpell = _aura ? _aura->CasterAuraSpell : 0; + TargetAuraSpell = _aura ? _aura->TargetAuraSpell : 0; + ExcludeCasterAuraSpell = _aura ? _aura->ExcludeCasterAuraSpell : 0; + ExcludeTargetAuraSpell = _aura ? _aura->ExcludeTargetAuraSpell : 0; + + // SpellCastingRequirementsEntry + SpellCastingRequirementsEntry const* _castreq = GetSpellCastingRequirements(); + RequiresSpellFocus = _castreq ? _castreq->RequiresSpellFocus : 0; + FacingCasterFlags = _castreq ? SpellFacingCasterFlags(_castreq->FacingCasterFlags) : SpellFacingCasterFlags::None; + MinFactionId = _castreq ? _castreq->MinFactionID : 0; + MinReputation = _castreq ? _castreq->MinReputation : 0; + AreaGroupId = _castreq ? _castreq->RequiredAreasID : -1; + + // SpellCategoriesEntry + SpellCategoriesEntry const* _categorie = GetSpellCategories(); + CategoryEntry = _categorie ? sSpellCategoryStore.LookupEntry(_categorie->Category) : nullptr; + Dispel = _categorie ? _categorie->DispelType : 0; + Mechanic = _categorie ? _categorie->Mechanic : 0; + StartRecoveryCategory = _categorie ? _categorie->StartRecoveryCategory : 0; + DmgClass = _categorie ? _categorie->DefenseType : 0; + PreventionType = _categorie ? _categorie->PreventionType : 0; + + // SpellClassOptionsEntry + SpellClassOptionsEntry const* _class = GetSpellClassOptions(); + SpellFamilyName = _class ? _class->SpellClassSet : 0; + SpellFamilyFlags = _class ? _class->SpellFamilyMask : flag96(0); + + // SpellCooldownsEntry + SpellCooldownsEntry const* _cooldowns = GetSpellCooldowns(); + RecoveryTime = _cooldowns ? _cooldowns->RecoveryTime : 0; + CategoryRecoveryTime = _cooldowns ? _cooldowns->CategoryRecoveryTime : 0; + StartRecoveryTime = _cooldowns ? _cooldowns->StartRecoveryTime : 0; + + // SpellEquippedItemsEntry + SpellEquippedItemsEntry const* _equipped = GetSpellEquippedItems(); + EquippedItemClass = _equipped ? _equipped->EquippedItemClass : -1; + EquippedItemSubClassMask = _equipped ?_equipped->EquippedItemSubclass : -1; + EquippedItemInventoryTypeMask = _equipped ? _equipped->EquippedItemInvTypes : -1; + + // SpellInterruptsEntry + SpellInterruptsEntry const* _interrupt = GetSpellInterrupts(); + + AuraInterruptFlags = _interrupt ? SpellAuraInterruptFlags(_interrupt->AuraInterruptFlags[0]) : SpellAuraInterruptFlags::None; + AuraInterruptFlags2 = _interrupt? SpellAuraInterruptFlags2(_interrupt->AuraInterruptFlags[1]) : SpellAuraInterruptFlags2::None; + ChannelInterruptFlags = _interrupt ? SpellAuraInterruptFlags(_interrupt->ChannelInterruptFlags[0] ) : SpellAuraInterruptFlags::None; + ChannelInterruptFlags2 = _interrupt ? SpellAuraInterruptFlags2(_interrupt->ChannelInterruptFlags[1]) : SpellAuraInterruptFlags2::None; + InterruptFlags = _interrupt ? SpellInterruptFlags(_interrupt->InterruptFlags) : SpellInterruptFlags::None; + + // SpellLevelsEntry + SpellLevelsEntry const* _levels = GetSpellLevels(); + MaxLevel = _levels ? _levels->MaxLevel : 0; + BaseLevel = _levels ? _levels->BaseLevel : 0; + SpellLevel = _levels ? _levels->SpellLevel : 0; + + // SpellPowerEntry + SpellPowerEntry const* _power = GetSpellPower(); + ManaCost = _power ? _power->ManaCost : 0; + ManaCostPerlevel = _power ? _power->ManaCostPerLevel : 0; + ManaCostPercentage = _power ? _power->PowerCostPct : 0; + ManaCostPercentage2 = _power ? _power->PowerCostPct2 : 0.f; + ManaPerSecond = _power ? _power->ManaPerSecond : 0; + + // SpellReagentsEntry + SpellReagentsEntry const* _reagents = GetSpellReagents(); + for (uint8 i = 0; i < MAX_SPELL_REAGENTS; ++i) + Reagent[i] = _reagents ? _reagents->Reagents[i] : 0; + for (uint8 i = 0; i < MAX_SPELL_REAGENTS; ++i) + ReagentCount[i] = _reagents ? _reagents->ReagentCount[i] : 0; + + // SpellShapeshiftEntry + if (SpellShapeshiftEntry const* _shapeshift = GetSpellShapeshift()) + { + Stances = MAKE_PAIR64(_shapeshift->ShapeshiftMask[0], _shapeshift->ShapeshiftMask[1]); + StancesNot = MAKE_PAIR64(_shapeshift->ShapeshiftExclude[0], _shapeshift->ShapeshiftExclude[1]); + } + else + { + Stances = UI64LIT(0); + StancesNot = UI64LIT(0); + } + + // SpellTargetRestrictionsEntry + SpellTargetRestrictionsEntry const* _target = GetSpellTargetRestrictions(); + ConeAngle = _target ? _target->ConeAngle : 0.f; + Targets = _target ? _target->Targets : 0; + TargetCreatureType = _target ? _target->TargetCreatureType : 0; + MaxAffectedTargets = _target ? _target->MaxTargets : 0; + MaxTargetLevel = _target ? _target->MaxTargetLevel : 0; + + // SpellTotemsEntry + SpellTotemsEntry const* _totem = GetSpellTotems(); + for (uint8 i = 0; i < 2; ++i) + TotemCategory[i] = _totem ? _totem->RequiredTotemCategoryID[i] : 0; + for (uint8 i = 0; i < 2; ++i) + Totem[i] = _totem ? _totem->Totem[i] : 0; + + ChainEntry = nullptr; + ExplicitTargetMask = 0; + + _spellSpecific = SPELL_SPECIFIC_NORMAL; + _auraState = AURA_STATE_NONE; + + _allowedMechanicMask = 0; +} + +SpellInfo::~SpellInfo() +{ + _UnloadImplicitTargetConditionLists(); +} + +uint32 SpellInfo::GetCategory() const +{ + return CategoryEntry ? CategoryEntry->ID : 0; +} + +bool SpellInfo::HasEffect(SpellEffects effect) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect(effect)) + return true; + return false; +} + +bool SpellInfo::HasAura(AuraType aura) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAura(aura)) + return true; + return false; +} + +bool SpellInfo::HasAreaAuraEffect() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAreaAuraEffect()) + return true; + return false; +} + +bool SpellInfo::HasOnlyDamageEffects() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].IsEffect()) + { + switch (Effects[i].Effect) + { + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_HEALTH_LEECH: + continue; + default: + return false; + } + } + } + + return true; +} + +bool SpellInfo::HasTargetType(::Targets target) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect()) + continue; + + if (Effects[i].TargetA.GetTarget() == target || Effects[i].TargetB.GetTarget() == target) + return true; + } + + return false; +} + +bool SpellInfo::CanBeInterrupted(Unit const* interruptTarget, bool ignoreImmunity /*= false*/) const +{ + return HasAttribute(SPELL_ATTR7_NO_UI_NOT_INTERRUPTIBLE) + || HasChannelInterruptFlag(SpellAuraInterruptFlags::Damage | SpellAuraInterruptFlags::EnteringCombat) + || (interruptTarget->IsPlayer() && InterruptFlags.HasFlag(SpellInterruptFlags::DamageCancelsPlayerOnly)) + || ((!(interruptTarget->GetMechanicImmunityMask() & (1 << MECHANIC_INTERRUPT)) || ignoreImmunity) + && PreventionType & SPELL_PREVENTION_TYPE_SILENCE); +} + +bool SpellInfo::HasAnyAuraInterruptFlag() const +{ + return AuraInterruptFlags != SpellAuraInterruptFlags::None || AuraInterruptFlags2 != SpellAuraInterruptFlags2::None; +} + +bool SpellInfo::IsExplicitDiscovery() const +{ + return ((Effects[0].Effect == SPELL_EFFECT_CREATE_RANDOM_ITEM + || Effects[0].Effect == SPELL_EFFECT_CREATE_ITEM_2) + && Effects[1].Effect == SPELL_EFFECT_SCRIPT_EFFECT) + || Id == 64323; +} + +bool SpellInfo::IsLootCrafting() const +{ + return (Effects[0].Effect == SPELL_EFFECT_CREATE_RANDOM_ITEM || + // different random cards from Inscription (121==Virtuoso Inking Set category) r without explicit item + (Effects[0].Effect == SPELL_EFFECT_CREATE_ITEM_2 && + ((TotemCategory[0] != 0 || (Totem[0] != 0 && SpellIconID == 1)) || Effects[0].ItemType == 0))); +} + +bool SpellInfo::IsQuestTame() const +{ + return Effects[0].Effect == SPELL_EFFECT_THREAT && Effects[1].Effect == SPELL_EFFECT_APPLY_AURA && Effects[1].ApplyAuraName == SPELL_AURA_DUMMY; +} + +bool SpellInfo::IsProfessionOrRiding() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsProfessionOrRidingSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsProfession() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsProfessionSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsPrimaryProfession() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsPrimaryProfessionSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsPrimaryProfessionFirstRank() const +{ + return IsPrimaryProfession() && GetRank() == 1; +} + +bool SpellInfo::IsAbilityLearnedWithProfession() const +{ + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + { + SkillLineAbilityEntry const* pAbility = _spell_idx->second; + if (!pAbility || pAbility->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticSkillRank) + continue; + + if (pAbility->MinSkillLineRank > 0) + return true; + } + + return false; +} + +bool SpellInfo::IsAbilityOfSkillType(uint32 skillType) const +{ + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + if (_spell_idx->second->SkillLine == uint32(skillType)) + return true; + + return false; +} + +bool SpellInfo::IsAffectingArea() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && (Effects[i].IsTargetingArea() || Effects[i].IsEffect(SPELL_EFFECT_PERSISTENT_AREA_AURA) || Effects[i].IsAreaAuraEffect())) + return true; + return false; +} + +// checks if spell targets are selected from area, doesn't include spell effects in check (like area wide auras for example) +bool SpellInfo::IsTargetingArea() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].IsTargetingArea()) + return true; + return false; +} + +bool SpellInfo::NeedsExplicitUnitTarget() const +{ + return (GetExplicitTargetMask() & TARGET_FLAG_UNIT_MASK) != 0; +} + +bool SpellInfo::NeedsToBeTriggeredByCaster(SpellInfo const* triggeringSpell) const +{ + if (NeedsExplicitUnitTarget()) + return true; + + /* + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].IsEffect()) + { + if (Effects[i].TargetA.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CHANNEL + || Effects[i].TargetB.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CHANNEL) + return true; + } + } + */ + + if (triggeringSpell->IsChanneled()) + { + uint32 mask = 0; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER && Effects[i].TargetA.GetTarget() != TARGET_DEST_CASTER + && Effects[i].TargetB.GetTarget() != TARGET_UNIT_CASTER && Effects[i].TargetB.GetTarget() != TARGET_DEST_CASTER) + { + mask |= Effects[i].GetProvidedTargetMask(); + } + } + + if (mask & TARGET_FLAG_UNIT_MASK) + return true; + } + + return false; +} + +bool SpellInfo::IsSelfCast() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Effect && Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER) + return false; + return true; +} + +bool SpellInfo::IsPassive() const +{ + return HasAttribute(SPELL_ATTR0_PASSIVE); +} + +bool SpellInfo::IsAutocastable() const +{ + if (IsPassive()) + return false; + if (HasAttribute(SPELL_ATTR1_NO_AUTOCAST_AI)) + return false; + return true; +} + +bool SpellInfo::IsStackableWithRanks() const +{ + if (IsPassive()) + return false; + if (PowerType != POWER_MANA && PowerType != POWER_HEALTH) + return false; + if (IsProfessionOrRiding()) + return false; + + if (IsAbilityLearnedWithProfession()) + return false; + + // All stance spells. if any better way, change it. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (SpellFamilyName) + { + case SPELLFAMILY_PALADIN: + // Paladin aura Spell + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID) + return false; + break; + case SPELLFAMILY_DRUID: + // Druid form Spell + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA && + Effects[i].ApplyAuraName == SPELL_AURA_MOD_SHAPESHIFT) + return false; + break; + } + } + return true; +} + +bool SpellInfo::IsPassiveStackableWithRanks() const +{ + return IsPassive() && !HasEffect(SPELL_EFFECT_APPLY_AURA); +} + +bool SpellInfo::IsMultiSlotAura() const +{ + if (IsPassive() || HasAura(SPELL_AURA_CONTROL_VEHICLE)) + return true; + + switch (Id) + { + case 55849: // Power Spark + case 40075: // Fel Flak Fire + case 44413: // Incanter's Absorption + case 79010: // Point of Vulnerability + case 79920: // Shared Health + return true; + default: + return false; + } +} + +bool SpellInfo::IsStackableOnOneSlotWithDifferentCasters() const +{ + /// TODO: Re-verify meaning of SPELL_ATTR3_STACK_FOR_DIFF_CASTERS and update conditions here + return StackAmount > 1 && !IsChanneled() && !HasAttribute(SPELL_ATTR3_DOT_STACKING_RULE); +} + +bool SpellInfo::IsCooldownStartedOnEvent() const +{ + if (HasAttribute(SPELL_ATTR0_COOLDOWN_ON_EVENT)) + return true; + + return CategoryEntry && CategoryEntry->GetFlags().HasFlag(SpellCategoryFlags::CooldownEventOnLeaveCombat); +} + +bool SpellInfo::IsDeathPersistent() const +{ + return HasAttribute(SPELL_ATTR3_ALLOW_AURA_WHILE_DEAD); +} + +bool SpellInfo::IsRequiringDeadTarget() const +{ + return HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS); +} + +bool SpellInfo::IsAllowingDeadTarget() const +{ + if (HasAttribute(SPELL_ATTR2_ALLOW_DEAD_TARGET) || Targets & (TARGET_FLAG_CORPSE_ALLY | TARGET_FLAG_CORPSE_ENEMY | TARGET_FLAG_UNIT_DEAD)) + return true; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].TargetA.GetTarget() == TARGET_CORPSE_SRC_AREA_RAID) + return true; + + return false; +} + +bool SpellInfo::IsGroupBuff() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (Effects[i].TargetA.GetCheckType()) + { + case TARGET_CHECK_PARTY: + case TARGET_CHECK_RAID: + case TARGET_CHECK_RAID_CLASS: + return true; + default: + break; + } + } + + return false; +} + +bool SpellInfo::CanBeUsedInCombat() const +{ + return !HasAttribute(SPELL_ATTR0_NOT_IN_COMBAT_ONLY_PEACEFUL); +} + +bool SpellInfo::IsPositive() const +{ + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE); +} + +bool SpellInfo::IsPositiveEffect(uint8 effIndex) const +{ + switch (effIndex) + { + default: + case 0: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF0); + case 1: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF1); + case 2: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF2); + } +} + +bool SpellInfo::IsChanneled() const +{ + return HasAttribute(SpellAttr1(SPELL_ATTR1_IS_CHANNELLED | SPELL_ATTR1_IS_SELF_CHANNELLED)); +} + +bool SpellInfo::IsMoveAllowedChannel() const +{ + return IsChanneled() && !ChannelInterruptFlags.HasFlag(SpellAuraInterruptFlags::Moving | SpellAuraInterruptFlags::Turning); +} + +bool SpellInfo::NeedsComboPoints() const +{ + return HasAttribute(SpellAttr1(SPELL_ATTR1_FINISHING_MOVE_DAMAGE | SPELL_ATTR1_FINISHING_MOVE_DURATION)); +} + +bool SpellInfo::IsNextMeleeSwingSpell() const +{ + return HasAttribute(SpellAttr0(SPELL_ATTR0_ON_NEXT_SWING_NO_DAMAGE | SPELL_ATTR0_ON_NEXT_SWING)); +} + +bool SpellInfo::IsRangedWeaponSpell() const +{ + return (EquippedItemClass == ITEM_CLASS_WEAPON) && (EquippedItemSubClassMask & ITEM_SUBCLASS_MASK_WEAPON_RANGED); +} + +bool SpellInfo::IsAutoRepeatRangedSpell() const +{ + return HasAttribute(SPELL_ATTR2_AUTO_REPEAT); +} + +bool SpellInfo::CausesInitialThreat() const +{ + return !HasAttribute(SPELL_ATTR1_NO_THREAT) && !HasAttribute(SPELL_ATTR2_NO_INITIAL_THREAT) && !HasAttribute(SPELL_ATTR4_NO_HARMFUL_THREAT); +} + +WeaponAttackType SpellInfo::GetAttackType() const +{ + WeaponAttackType result; + switch (DmgClass) + { + case SPELL_DAMAGE_CLASS_MELEE: + if (HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + result = OFF_ATTACK; + else + result = BASE_ATTACK; + break; + case SPELL_DAMAGE_CLASS_RANGED: + result = IsRangedWeaponSpell() ? RANGED_ATTACK : BASE_ATTACK; + break; + default: + // Wands + if (IsAutoRepeatRangedSpell()) + result = RANGED_ATTACK; + else + result = BASE_ATTACK; + break; + } + + return result; +} + +bool SpellInfo::IsAffected(uint32 familyName, flag96 const& familyFlags) const +{ + if (!familyName) + return true; + + if (familyName != SpellFamilyName) + return false; + + if (familyFlags && !(familyFlags & SpellFamilyFlags)) + return false; + + return true; +} + +bool SpellInfo::IsAffectedBySpellMods() const +{ + return !HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS); +} + +bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod) const +{ + if (!IsAffectedBySpellMods()) + return false; + + SpellInfo const* affectSpell = sSpellMgr->GetSpellInfo(mod->spellId); + if (!affectSpell) + return false; + + switch (mod->type) + { + case SPELLMOD_FLAT: + case SPELLMOD_PCT: + // TEMP: dont use IsAffected - !familyName and !familyFlags are not valid options for spell mods + // TODO: investigate if the !familyName and !familyFlags conditions are even valid for all other (nonmod) uses of SpellInfo::IsAffected + return affectSpell->SpellFamilyName == SpellFamilyName && static_cast(mod)->mask & SpellFamilyFlags; + default: + break; + } + + return false; +} + +bool SpellInfo::CanPierceImmuneAura(SpellInfo const* auraSpellInfo) const +{ + // aura can't be pierced + if (!auraSpellInfo || auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + // these spells pierce all available spells (Resurrection Sickness for example) + if (HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return true; + + // these spells (Cyclone for example) can pierce all... + if (HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) || HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + // ...but not these (Divine shield, Ice block, Cyclone and Banish for example) + if (auraSpellInfo->Mechanic != MECHANIC_IMMUNE_SHIELD && + auraSpellInfo->Mechanic != MECHANIC_INVULNERABILITY && + (auraSpellInfo->Mechanic != MECHANIC_BANISH || (IsRankOf(auraSpellInfo) && auraSpellInfo->Dispel != DISPEL_NONE))) // Banish shouldn't be immune to itself, but Cyclone should + return true; + } + + // Dispels other auras on immunity, check if this spell makes the unit immune to aura + if (HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT) && CanSpellProvideImmunityAgainstAura(auraSpellInfo)) + return true; + + return false; +} + +bool SpellInfo::CanDispelAura(SpellInfo const* auraSpellInfo) const +{ + // These auras (like Divine Shield) can't be dispelled + if (auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + // These spells (like Mass Dispel) can dispel all auras + if (HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return true; + + // These auras (Cyclone for example) are not dispelable + if ((auraSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) && auraSpellInfo->Mechanic != MECHANIC_NONE) + || auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + return false; + + return true; +} + +bool SpellInfo::IsSingleTarget() const +{ + // all other single target spells have if it has AttributesEx5 + if (HasAttribute(SPELL_ATTR5_LIMIT_N)) + return true; + + return false; +} + +bool SpellInfo::IsAuraExclusiveBySpecificWith(SpellInfo const* spellInfo) const +{ + SpellSpecificType spellSpec1 = GetSpellSpecific(); + SpellSpecificType spellSpec2 = spellInfo->GetSpellSpecific(); + switch (spellSpec1) + { + case SPELL_SPECIFIC_WARLOCK_ARMOR: + case SPELL_SPECIFIC_MAGE_ARMOR: + case SPELL_SPECIFIC_ELEMENTAL_SHIELD: + case SPELL_SPECIFIC_MAGE_POLYMORPH: + case SPELL_SPECIFIC_PRESENCE: + case SPELL_SPECIFIC_CHARM: + case SPELL_SPECIFIC_SCROLL: + case SPELL_SPECIFIC_WARRIOR_ENRAGE: + case SPELL_SPECIFIC_MAGE_ARCANE_BRILLANCE: + case SPELL_SPECIFIC_PRIEST_DIVINE_SPIRIT: + return spellSpec1 == spellSpec2; + case SPELL_SPECIFIC_FOOD: + return spellSpec2 == SPELL_SPECIFIC_FOOD + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + case SPELL_SPECIFIC_DRINK: + return spellSpec2 == SPELL_SPECIFIC_DRINK + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + case SPELL_SPECIFIC_FOOD_AND_DRINK: + return spellSpec2 == SPELL_SPECIFIC_FOOD + || spellSpec2 == SPELL_SPECIFIC_DRINK + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + default: + return false; + } +} + +bool SpellInfo::IsAuraExclusiveBySpecificPerCasterWith(SpellInfo const* spellInfo) const +{ + SpellSpecificType spellSpec = GetSpellSpecific(); + switch (spellSpec) + { + case SPELL_SPECIFIC_SEAL: + case SPELL_SPECIFIC_HAND: + case SPELL_SPECIFIC_AURA: + case SPELL_SPECIFIC_STING: + case SPELL_SPECIFIC_CURSE: + case SPELL_SPECIFIC_BANE: + case SPELL_SPECIFIC_ASPECT: + case SPELL_SPECIFIC_WARLOCK_CORRUPTION: + return spellSpec == spellInfo->GetSpellSpecific(); + default: + return false; + } +} + +SpellCastResult SpellInfo::CheckShapeshift(uint32 form) const +{ + // talents that learn spells can have stance requirements that need ignore + // (this requirement only for client-side stance show in talent description) + if (sDBCManager.GetTalentSpellCost(Id) > 0 && + (Effects[0].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[1].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[2].Effect == SPELL_EFFECT_LEARN_SPELL)) + return SPELL_CAST_OK; + + uint64 stanceMask = (form ? UI64LIT(1) << (form - 1) : 0); + + if (stanceMask & StancesNot) // can explicitly not be cast in this stance + return SPELL_FAILED_NOT_SHAPESHIFT; + + if (stanceMask & Stances) // can explicitly be cast in this stance + return SPELL_CAST_OK; + + bool actAsShifted = false; + SpellShapeshiftFormEntry const* shapeInfo = nullptr; + if (form > 0) + { + shapeInfo = sSpellShapeshiftFormStore.LookupEntry(form); + if (!shapeInfo) + { + TC_LOG_ERROR("spells", "GetErrorAtShapeshiftedCast: unknown shapeshift %u", form); + return SPELL_CAST_OK; + } + actAsShifted = !shapeInfo->GetFlags().HasFlag(SpellShapeshiftFormFlags::Stance); + } + + if (actAsShifted) + { + if (HasAttribute(SPELL_ATTR0_NOT_SHAPESHIFTED) || (shapeInfo && shapeInfo->GetFlags().HasFlag(SpellShapeshiftFormFlags::CanOnlyCastShapeshiftSpells))) // not while shapeshifted + return SPELL_FAILED_NOT_SHAPESHIFT; + else if (Stances != 0) // needs other shapeshift + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + else + { + // needs shapeshift + if (!HasAttribute(SPELL_ATTR2_ALLOW_WHILE_NOT_SHAPESHIFTED_CASTER_FORM) && Stances != 0) + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + + // Check if stance disables cast of not-stance spells + // Example: cannot cast any other spells in zombie or ghoul form + /// @todo Find a way to disable use of these spells clientside + if (shapeInfo && shapeInfo->Flags & 0x400) + { + if (!(stanceMask & Stances)) + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckLocation(uint32 map_id, uint32 zone_id, uint32 area_id, Player const* player) const +{ + // normal case + if (AreaGroupId > 0) + { + bool found = false; + AreaGroupEntry const* groupEntry = sAreaGroupStore.LookupEntry(AreaGroupId); + while (groupEntry) + { + for (uint8 i = 0; i < MAX_GROUP_AREA_IDS; ++i) + if (DBCManager::IsInArea(area_id, groupEntry->AreaID[i])) + found = true; + + if (found || !groupEntry->NextAreaID) + break; + // Try search in next group + groupEntry = sAreaGroupStore.LookupEntry(groupEntry->NextAreaID); + } + + if (!found) + return SPELL_FAILED_INCORRECT_AREA; + } + + // continent limitation (virtual continent) + if (HasAttribute(SPELL_ATTR4_ONLY_FLYING_AREAS)) + { + EnumFlag mountFlags = AreaMountFlags::None; + if (player && player->HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS)) + { + for (AuraEffect const* auraEffect : player->GetAuraEffectsByType(SPELL_AURA_MOUNT_RESTRICTIONS)) + mountFlags |= AreaMountFlags(auraEffect->GetMiscValue()); + } + else if (AreaTableEntry const* areaTable = sAreaTableStore.LookupEntry(area_id)) + mountFlags = areaTable->GetMountFlags(); + + if (!(mountFlags.HasFlag(AreaMountFlags::AllowFlyingMounts))) + return SPELL_FAILED_INCORRECT_AREA; + } + + // raid instance limitation + if (HasAttribute(SPELL_ATTR6_NOT_IN_RAID_INSTANCES)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || mapEntry->IsRaid()) + return SPELL_FAILED_NOT_IN_RAID_INSTANCE; + } + + if (HasAttribute(SPELL_ATTR8_REMOVE_OUTSIDE_DUNGEONS_AND_RAIDS)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || !mapEntry->IsDungeon()) + return SPELL_FAILED_TARGET_NOT_IN_INSTANCE; + } + + if (HasAttribute(SPELL_ATTR8_NOT_IN_BATTLEGROUND)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || mapEntry->IsBattleground()) + return SPELL_FAILED_NOT_IN_BATTLEGROUND; + } + + // DB base check (if non empty then must fit at least single for allow) + SpellAreaMapBounds saBounds = sSpellMgr->GetSpellAreaMapBounds(Id); + if (saBounds.first != saBounds.second) + { + for (SpellAreaMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) + { + if (itr->second.IsFitToRequirements(player, zone_id, area_id)) + return SPELL_CAST_OK; + } + return SPELL_FAILED_INCORRECT_AREA; + } + + // bg spell checks + switch (Id) + { + case 23333: // Horde Flag + case 23335: // Alliance Flag + return (map_id == 489 || map_id == 726) && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + case 34976: // Netherstorm Flag + return map_id == 566 && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + case 2584: // Waiting to Resurrect + case 22011: // Spirit Heal Channel + case 22012: // Spirit Heal + case 24171: // Resurrection Impact Visual + case 42792: // Recently Dropped Flag + case 43681: // Inactive + case 44535: // Spirit Heal (mana) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + return zone_id == AREA_WINTERGRASP || (mapEntry->IsBattleground() && player && player->InBattleground()) ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 44521: // Preparation + { + if (!player) + return SPELL_FAILED_REQUIRES_AREA; + + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + if (!mapEntry->IsBattleground()) + return SPELL_FAILED_REQUIRES_AREA; + + Battleground* bg = player->GetBattleground(); + return bg && bg->GetStatus() == STATUS_WAIT_JOIN ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 32724: // Gold Team (Alliance) + case 32725: // Green Team (Alliance) + case 35774: // Gold Team (Horde) + case 35775: // Green Team (Horde) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + return mapEntry->IsBattleArena() && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 32727: // Arena Preparation + { + if (!player) + return SPELL_FAILED_REQUIRES_AREA; + + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + if (!mapEntry->IsBattleArena()) + return SPELL_FAILED_REQUIRES_AREA; + + Battleground* bg = player->GetBattleground(); + return bg && bg->GetStatus() == STATUS_WAIT_JOIN ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + } + + // aura limitations + if (player) + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsAura()) + continue; + + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_SHAPESHIFT: + { + if (SpellShapeshiftFormEntry const* spellShapeshiftForm = sSpellShapeshiftFormStore.LookupEntry(Effects[i].MiscValue)) + if (uint32 mountType = spellShapeshiftForm->MountTypeID) + if (!player->GetMountCapability(mountType)) + return SPELL_FAILED_NOT_HERE; + break; + } + case SPELL_AURA_MOUNTED: + { + if (Effects[i].MiscValueB && !player->GetMountCapability(Effects[i].MiscValueB)) + return SPELL_FAILED_NOT_HERE; + break; + } + break; + } + } + } + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckTarget(WorldObject const* caster, WorldObject const* target, bool implicit /*= true*/) const +{ + // VIANO_AURA_DEBUG_CHECKTARGET_V1 + Unit const* vianoTarget = target ? target->ToUnit() : nullptr; + if (Id == 465 && vianoTarget && vianoTarget->IsVianoAdventurer()) + TC_LOG_INFO("scripts", "[VianoAuraDebug] stage=CheckTarget spell=%u target=%s controlled=%u", + Id, vianoTarget->GetGUID().ToString().c_str(), vianoTarget->IsControlledByPlayer() ? 1u : 0u); + + if (HasAttribute(SPELL_ATTR1_EXCLUDE_CASTER) && caster == target) + return SPELL_FAILED_BAD_TARGETS; + + // check visibility - ignore stealth for implicit (area) targets + if (!HasAttribute(SPELL_ATTR6_IGNORE_PHASE_SHIFT) && !caster->CanSeeOrDetect(target, implicit)) + return SPELL_FAILED_BAD_TARGETS; + + Unit const* unitTarget = target->ToUnit(); + + /* + * @todo: 4.x - old hold due to an upcoming tempsummon rework + if (HasAttribute(SPELL_ATTR8_ONLY_TARGET_IF_SAME_CREATOR)) + if (caster != target && caster->GetGUID() != target->GetOwnerGUID()) + return SPELL_FAILED_BAD_TARGETS; + */ + + // creature/player specific target checks + if (unitTarget) + { + // spells cannot be cast if target has a pet in combat either + if (HasAttribute(SPELL_ATTR1_ONLY_PEACEFUL_TARGETS) && (unitTarget->IsInCombat() || unitTarget->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT))) + return SPELL_FAILED_TARGET_AFFECTING_COMBAT; + + // only spells with SPELL_ATTR3_ONLY_TARGET_GHOSTS can target ghosts + if (HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS) != unitTarget->HasAuraType(SPELL_AURA_GHOST)) + { + if (HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS)) + return SPELL_FAILED_TARGET_NOT_GHOST; + else + return SPELL_FAILED_BAD_TARGETS; + } + + if (caster != unitTarget) + { + if (caster->GetTypeId() == TYPEID_PLAYER) + { + // Do not allow these spells to target creatures not tapped by us (Banish, Polymorph, many quest spells) + if (HasAttribute(SPELL_ATTR2_CANNOT_CAST_ON_TAPPED)) + if (Creature const* targetCreature = unitTarget->ToCreature()) + if (targetCreature->hasLootRecipient() && !targetCreature->isTappedBy(caster->ToPlayer())) + return SPELL_FAILED_CANT_CAST_ON_TAPPED; + + if (HasAttribute(SPELL_ATTR0_CU_PICKPOCKET)) + { + Creature const* targetCreature = unitTarget->ToCreature(); + if (!targetCreature) + return SPELL_FAILED_BAD_TARGETS; + + if (!LootTemplates_Pickpocketing.HaveLootFor(targetCreature->GetCreatureTemplate()->pickpocketLootId)) + return SPELL_FAILED_TARGET_NO_POCKETS; + } + + // Not allow disarm unarmed player + if (Mechanic == MECHANIC_DISARM) + { + if (unitTarget->GetTypeId() == TYPEID_PLAYER) + { + Player const* player = unitTarget->ToPlayer(); + if (!player->GetWeaponForAttack(BASE_ATTACK) || !player->IsUseEquipedWeapon(true)) + return SPELL_FAILED_TARGET_NO_WEAPONS; + } + else if (!unitTarget->GetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID)) + return SPELL_FAILED_TARGET_NO_WEAPONS; + } + } + } + + if (HasAttribute(SPELL_ATTR8_ONLY_TARGET_OWN_SUMMONS)) + if (!unitTarget->IsSummon() || unitTarget->ToTempSummon()->GetSummonerGUID() != caster->GetGUID()) + return SPELL_FAILED_BAD_TARGETS; + } + // corpse specific target checks + else if (Corpse const* corpseTarget = target->ToCorpse()) + { + // cannot target bare bones + if (corpseTarget->GetType() == CORPSE_BONES) + return SPELL_FAILED_BAD_TARGETS; + // we have to use owner for some checks (aura preventing resurrection for example) + if (Player* owner = ObjectAccessor::FindPlayer(corpseTarget->GetOwnerGUID())) + unitTarget = owner; + // we're not interested in corpses without owner + else + return SPELL_FAILED_BAD_TARGETS; + } + // other types of objects - always valid + else return SPELL_CAST_OK; + + // corpseOwner and unit specific target checks + if (!unitTarget->IsPlayer()) + { + if (HasAttribute(SPELL_ATTR3_ONLY_ON_PLAYER)) + return SPELL_FAILED_TARGET_NOT_PLAYER; + + // VIANO_AURA_PLAYER_CONTROLLED_EXCEPTION_V1 + // Viano adventurers keep PLAYER_CONTROLLED for direct helpful targeting, + // but are intentionally eligible for group/raid buffs such as paladin auras. + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) + && unitTarget->IsControlledByPlayer() + && !unitTarget->IsVianoAdventurer()) + return SPELL_FAILED_TARGET_IS_PLAYER_CONTROLLED; + } + else if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER)) + return SPELL_FAILED_TARGET_IS_PLAYER; + + if (!IsAllowingDeadTarget() && !unitTarget->IsAlive()) + return SPELL_FAILED_TARGETS_DEAD; + + // check this flag only for implicit targets (chain and area), allow to explicitly target units for spells like Shield of Righteousness + if (implicit && HasAttribute(SPELL_ATTR6_DO_NOT_CHAIN_TO_CROWD_CONTROLLED_TARGETS) && !unitTarget->CanFreeMove()) + return SPELL_FAILED_BAD_TARGETS; + + // Do not allow pet or guardian targets if the spell is a raid buff or may not target pets at all + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) && unitTarget->IsCreature() && unitTarget->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + return SPELL_FAILED_BAD_TARGETS; + + // checked in Unit::IsValidAttack/AssistTarget, shouldn't be checked for ENTRY targets + //if (!HasAttribute(SPELL_ATTR6_CAN_TARGET_UNTARGETABLE) && target->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)) + // return SPELL_FAILED_BAD_TARGETS; + + if (!CheckTargetCreatureType(unitTarget)) + { + if (target->GetTypeId() == TYPEID_PLAYER) + return SPELL_FAILED_TARGET_IS_PLAYER; + else + return SPELL_FAILED_BAD_TARGETS; + } + + // check GM mode and GM invisibility - only for player casts (npc casts are controlled by AI) and negative spells + if (unitTarget != caster && (caster->GetAffectingPlayer() || !IsPositive()) && unitTarget->GetTypeId() == TYPEID_PLAYER) + { + if (!unitTarget->ToPlayer()->IsVisible()) + return SPELL_FAILED_BM_OR_INVISGOD; + + if (unitTarget->ToPlayer()->IsGameMaster()) + return SPELL_FAILED_BM_OR_INVISGOD; + } + + // not allow casting on flying player + if (unitTarget->HasUnitState(UNIT_STATE_IN_FLIGHT) && !HasAttribute(SPELL_ATTR0_CU_ALLOW_INFLIGHT_TARGET)) + return SPELL_FAILED_BAD_TARGETS; + + /* TARGET_UNIT_MASTER gets blocked here for passengers, because the whole idea of this check is to + not allow passengers to be implicitly hit by spells, however this target type should be an exception, + if this is left it kills spells that award kill credit from vehicle to master (few spells), + the use of these 2 covers passenger target check, logically, if vehicle cast this to master it should always hit + him, because it would be it's passenger, there's no such case where this gets to fail legitimacy, this problem + cannot be solved from within the check in other way since target type cannot be called for the spell currently + Spell examples: [ID - 52864 Devour Water, ID - 52862 Devour Wind, ID - 49370 Wyrmrest Defender: Destabilize Azure Dragonshrine Effect] */ + if (Unit const* unitCaster = caster->ToUnit()) + { + if (!unitCaster->IsVehicle() && !(unitCaster->GetCharmerOrOwner() == target)) + { + if (TargetAuraState && !unitTarget->HasAuraState(AuraStateType(TargetAuraState), this, unitCaster)) + return SPELL_FAILED_TARGET_AURASTATE; + + if (TargetAuraStateNot && unitTarget->HasAuraState(AuraStateType(TargetAuraStateNot), this, unitCaster)) + return SPELL_FAILED_TARGET_AURASTATE; + } + } + + if (TargetAuraSpell && !unitTarget->HasAura(sSpellMgr->GetSpellIdForDifficulty(TargetAuraSpell, caster))) + return SPELL_FAILED_TARGET_AURASTATE; + + if (ExcludeTargetAuraSpell && unitTarget->HasAura(sSpellMgr->GetSpellIdForDifficulty(ExcludeTargetAuraSpell, caster))) + return SPELL_FAILED_TARGET_AURASTATE; + + if (unitTarget->HasAuraType(SPELL_AURA_PREVENT_RESURRECTION)) + if (HasEffect(SPELL_EFFECT_SELF_RESURRECT) || HasEffect(SPELL_EFFECT_RESURRECT) || HasEffect(SPELL_EFFECT_RESURRECT_NEW)) + return SPELL_FAILED_TARGET_CANNOT_BE_RESURRECTED; + + if (!unitTarget->CheckPrivateObjectOwnerVisibility(caster)) + return SPELL_FAILED_BAD_TARGETS; + + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckExplicitTarget(WorldObject const* caster, WorldObject const* target, Item const* itemTarget) const +{ + uint32 neededTargets = GetExplicitTargetMask(); + if (!target) + { + if (neededTargets & (TARGET_FLAG_UNIT_MASK | TARGET_FLAG_GAMEOBJECT_MASK | TARGET_FLAG_CORPSE_MASK)) + if (!(neededTargets & TARGET_FLAG_GAMEOBJECT_ITEM) || !itemTarget) + return SPELL_FAILED_BAD_TARGETS; + return SPELL_CAST_OK; + } + + if (Unit const* unitTarget = target->ToUnit()) + { + if (neededTargets & (TARGET_FLAG_UNIT_ENEMY | TARGET_FLAG_UNIT_ALLY | TARGET_FLAG_UNIT_RAID | TARGET_FLAG_UNIT_PARTY | TARGET_FLAG_UNIT_MINIPET | TARGET_FLAG_UNIT_PASSENGER)) + { + Unit const* unitCaster = caster->ToUnit(); + if (neededTargets & TARGET_FLAG_UNIT_ENEMY) + if (caster->IsValidAttackTarget(unitTarget, this)) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_ALLY) + || ((neededTargets & TARGET_FLAG_UNIT_PARTY) && unitCaster && unitCaster->IsInPartyWith(unitTarget)) + || ((neededTargets & TARGET_FLAG_UNIT_RAID) && unitCaster && unitCaster->IsInRaidWith(unitTarget))) + if (caster->IsValidAssistTarget(unitTarget, this)) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_MINIPET) && unitCaster) + if (unitTarget->GetGUID() == unitCaster->GetCritterGUID()) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_PASSENGER) && unitCaster) + if (unitTarget->IsOnVehicle(unitCaster)) + return SPELL_CAST_OK; + return SPELL_FAILED_BAD_TARGETS; + } + } + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckVehicle(Unit const* caster) const +{ + // All creatures should be able to cast as passengers freely, restriction and attribute are only for players + if (caster->GetTypeId() != TYPEID_PLAYER) + return SPELL_CAST_OK; + + Vehicle* vehicle = caster->GetVehicle(); + if (vehicle) + { + uint16 checkMask = 0; + for (uint8 effIndex = EFFECT_0; effIndex < MAX_SPELL_EFFECTS; ++effIndex) + { + if (Effects[effIndex].ApplyAuraName == SPELL_AURA_MOD_SHAPESHIFT) + { + SpellShapeshiftFormEntry const* shapeShiftFromEntry = sSpellShapeshiftFormStore.LookupEntry(Effects[effIndex].MiscValue); + if (shapeShiftFromEntry && (shapeShiftFromEntry->Flags & 1) == 0) // unk flag + checkMask |= VEHICLE_SEAT_FLAG_UNCONTROLLED; + break; + } + } + + if (HasAura(SPELL_AURA_MOUNTED)) + checkMask |= VEHICLE_SEAT_FLAG_CAN_CAST_MOUNT_SPELL; + + if (!checkMask) + checkMask = VEHICLE_SEAT_FLAG_CAN_ATTACK; + + VehicleSeatEntry const* vehicleSeat = vehicle->GetSeatForPassenger(caster); + if (!vehicleSeat) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + + if (!HasAttribute(SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE) && !HasAttribute(SPELL_ATTR0_ALLOW_WHILE_MOUNTED) + && (vehicleSeat->Flags & checkMask) != checkMask) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + + // Can only summon uncontrolled minions/guardians when on controlled vehicle + if (vehicleSeat->Flags & (VEHICLE_SEAT_FLAG_CAN_CONTROL | VEHICLE_SEAT_FLAG_UNK2)) + { + for (uint32 i = EFFECT_0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect != SPELL_EFFECT_SUMMON) + continue; + + SummonPropertiesEntry const* props = sSummonPropertiesStore.LookupEntry(Effects[i].MiscValueB); + if (props && props->Control != SUMMON_CATEGORY_WILD) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + } + } + } + + return SPELL_CAST_OK; +} + +bool SpellInfo::CheckTargetCreatureType(Unit const* target) const +{ + // Curse of Doom & Exorcism: not find another way to fix spell target check :/ + if (SpellFamilyName == SPELLFAMILY_WARLOCK && GetCategory() == 1179) + { + // not allow cast at player + if (target->GetTypeId() == TYPEID_PLAYER) + return false; + else + return true; + } + + // if target is magnet (i.e Grounding Totem) the check is skipped + if (target->IsMagnet()) + return true; + + uint32 creatureType = target->GetCreatureTypeMask(); + return !TargetCreatureType || !creatureType || (creatureType & TargetCreatureType); +} + +SpellSchoolMask SpellInfo::GetSchoolMask() const +{ + return SpellSchoolMask(SchoolMask); +} + +uint32 SpellInfo::GetAllEffectsMechanicMask() const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].Mechanic) + mask |= 1 << Effects[i].Mechanic; + return mask; +} + +uint32 SpellInfo::GetEffectMechanicMask(uint8 effIndex) const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + if (Effects[effIndex].IsEffect() && Effects[effIndex].Mechanic) + mask |= 1 << Effects[effIndex].Mechanic; + return mask; +} + +uint32 SpellInfo::GetSpellMechanicMaskByEffectMask(uint32 effectMask) const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if ((effectMask & (1 << i)) && Effects[i].Mechanic) + mask |= 1 << Effects[i].Mechanic; + return mask; +} + +Mechanics SpellInfo::GetEffectMechanic(uint8 effIndex) const +{ + if (Effects[effIndex].IsEffect() && Effects[effIndex].Mechanic) + return Mechanics(Effects[effIndex].Mechanic); + if (Mechanic) + return Mechanics(Mechanic); + return MECHANIC_NONE; +} + +bool SpellInfo::HasAnyEffectMechanic() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Mechanic) + return true; + return false; +} + +uint32 SpellInfo::GetDispelMask() const +{ + return GetDispelMask(DispelType(Dispel)); +} + +uint32 SpellInfo::GetDispelMask(DispelType type) +{ + // If dispel all + if (type == DISPEL_ALL) + return DISPEL_ALL_MASK; + else + return uint32(1 << type); +} + +uint32 SpellInfo::GetExplicitTargetMask() const +{ + return ExplicitTargetMask; +} + +AuraStateType SpellInfo::GetAuraState() const +{ + return _auraState; +} + +void SpellInfo::_LoadAuraState() +{ + _auraState = [this]() -> AuraStateType + { + // Seals + if (GetSpellSpecific() == SPELL_SPECIFIC_SEAL) + return AURA_STATE_JUDGEMENT; + + // Conflagrate aura state on Immolate and Shadowflame + if (SpellFamilyName == SPELLFAMILY_WARLOCK && + // Immolate + ((SpellFamilyFlags[0] & 4) || + // Shadowflame + (SpellFamilyFlags[2] & 2))) + return AURA_STATE_CONFLAGRATE; + + // Faerie Fire (druid versions) + if (SpellFamilyName == SPELLFAMILY_DRUID && SpellFamilyFlags[0] & 0x400) + return AURA_STATE_FAERIE_FIRE; + + // Sting (hunter's pet ability) + if (GetCategory() == 1133) + return AURA_STATE_FAERIE_FIRE; + + // Victorious + if (SpellFamilyName == SPELLFAMILY_WARRIOR && SpellFamilyFlags[1] & 0x00040000) + return AURA_STATE_WARRIOR_VICTORY_RUSH; + + // Swiftmend state on Regrowth & Rejuvenation + if (SpellFamilyName == SPELLFAMILY_DRUID && SpellFamilyFlags[0] & 0x50) + return AURA_STATE_SWIFTMEND; + + // Deadly poison aura state + if (SpellFamilyName == SPELLFAMILY_ROGUE && SpellFamilyFlags[0] & 0x10000) + return AURA_STATE_DEADLY_POISON; + + // Enrage aura state + if (Dispel == DISPEL_ENRAGE) + return AURA_STATE_ENRAGE; + + // Bleeding aura state + if (GetAllEffectsMechanicMask() & 1 << MECHANIC_BLEED) + return AURA_STATE_BLEEDING; + + if (GetSchoolMask() & SPELL_SCHOOL_MASK_FROST) + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAura() && (Effects[i].ApplyAuraName == SPELL_AURA_MOD_STUN + || Effects[i].ApplyAuraName == SPELL_AURA_MOD_ROOT)) + return AURA_STATE_FROZEN; + + switch (Id) + { + case 71465: // Divine Surge + case 50241: // Evasive Charges + return AURA_STATE_UNKNOWN22; + case 9991: // Touch of Zanzil + case 35325: // Glowing Blood + case 35328: // Lambent Blood + case 35329: // Vibrant Blood + case 35331: // Black Blood + case 49163: // Perpetual Instability + return AURA_STATE_FAERIE_FIRE; + default: + break; + } + + return AURA_STATE_NONE; + }(); +} + +SpellSpecificType SpellInfo::GetSpellSpecific() const +{ + return _spellSpecific; +} + +void SpellInfo::_LoadSpellSpecific() +{ + _spellSpecific = [this]()->SpellSpecificType + { + switch (SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + { + // Food / Drinks (mostly) + if (HasAuraInterruptFlag(SpellAuraInterruptFlags::Standing)) + { + bool food = false; + bool drink = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsAura()) + continue; + switch (Effects[i].ApplyAuraName) + { + // Food + case SPELL_AURA_MOD_REGEN: + case SPELL_AURA_OBS_MOD_HEALTH: + food = true; + break; + // Drink + case SPELL_AURA_MOD_POWER_REGEN: + case SPELL_AURA_OBS_MOD_POWER: + drink = true; + break; + default: + break; + } + } + + if (food && drink) + return SPELL_SPECIFIC_FOOD_AND_DRINK; + else if (food) + return SPELL_SPECIFIC_FOOD; + else if (drink) + return SPELL_SPECIFIC_DRINK; + } + // scrolls effects + else + { + SpellInfo const* firstRankSpellInfo = GetFirstRankSpell(); + switch (firstRankSpellInfo->Id) + { + case 8118: // Strength + case 8099: // Stamina + case 8112: // Spirit + case 8096: // Intellect + case 8115: // Agility + case 8091: // Armor + return SPELL_SPECIFIC_SCROLL; + case 12880: // Enrage (Enrage) + case 57518: // Enrage (Wrecking Crew) + return SPELL_SPECIFIC_WARRIOR_ENRAGE; + } + } + break; + } + case SPELLFAMILY_MAGE: + { + // family flags 18(Molten), 25(Frost/Ice), 28(Mage) + if (SpellFamilyFlags[0] & 0x12040000) + return SPELL_SPECIFIC_MAGE_ARMOR; + + // Arcane brillance and Arcane intelect (normal check fails because of flags difference) + if (SpellFamilyFlags[0] & 0x400) + return SPELL_SPECIFIC_MAGE_ARCANE_BRILLANCE; + + if ((SpellFamilyFlags[0] & 0x1000000) && Effects[0].ApplyAuraName == SPELL_AURA_MOD_CONFUSE) + return SPELL_SPECIFIC_MAGE_POLYMORPH; + + break; + } + case SPELLFAMILY_WARRIOR: + { + if (Id == 12292) // Death Wish + return SPELL_SPECIFIC_WARRIOR_ENRAGE; + + break; + } + case SPELLFAMILY_WARLOCK: + { + // Warlock (Bane of Doom | Bane of Agony | Bane of Havoc) + if (Id == 603 || Id == 980 || Id == 80240) + return SPELL_SPECIFIC_BANE; + + // only warlock curses have this + if (Dispel == DISPEL_CURSE) + return SPELL_SPECIFIC_CURSE; + + // Warlock (Demon Armor | Demon Skin | Fel Armor) + if (SpellFamilyFlags[1] & 0x20000020 || SpellFamilyFlags[2] & 0x00000010) + return SPELL_SPECIFIC_WARLOCK_ARMOR; + + //seed of corruption and corruption + if (SpellFamilyFlags[1] & 0x10 || SpellFamilyFlags[0] & 0x2) + return SPELL_SPECIFIC_WARLOCK_CORRUPTION; + break; + } + case SPELLFAMILY_PRIEST: + { + // Divine Spirit and Prayer of Spirit + if (SpellFamilyFlags[0] & 0x20) + return SPELL_SPECIFIC_PRIEST_DIVINE_SPIRIT; + + break; + } + case SPELLFAMILY_HUNTER: + { + // only hunter stings have this + if (Dispel == DISPEL_POISON) + return SPELL_SPECIFIC_STING; + + // only hunter aspects have this (but not all aspects in hunter family) + if (SpellFamilyFlags.HasFlag(0x00380000, 0x00440000, 0x00001010)) + return SPELL_SPECIFIC_ASPECT; + + break; + } + case SPELLFAMILY_PALADIN: + { + // Collection of all the seal family flags. No other paladin spell has any of those. + if (SpellFamilyFlags[1] & 0xA2000800) + return SPELL_SPECIFIC_SEAL; + + if (SpellFamilyFlags[0] & 0x00002190) + return SPELL_SPECIFIC_HAND; + + // only paladin auras have this (for palaldin class family) + if (SpellFamilyFlags[2] & 0x00000020) + return SPELL_SPECIFIC_AURA; + + break; + } + case SPELLFAMILY_SHAMAN: + { + // family flags 10 (Lightning), 42 (Earth), 37 (Water), proc shield from T2 8 pieces bonus + if (SpellFamilyFlags[1] & 0x420 + || SpellFamilyFlags[0] & 0x00000400 + || Id == 23552) + return SPELL_SPECIFIC_ELEMENTAL_SHIELD; + + break; + } + case SPELLFAMILY_DEATHKNIGHT: + if (Id == 48266 || Id == 48263 || Id == 48265) + return SPELL_SPECIFIC_PRESENCE; + break; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA) + { + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_CHARM: + case SPELL_AURA_MOD_POSSESS: + case SPELL_AURA_AOE_CHARM: + return SPELL_SPECIFIC_CHARM; + case SPELL_AURA_TRACK_CREATURES: + /// @workaround For non-stacking tracking spells (We need generic solution) + if (Id == 30645) // Gas Cloud Tracking + return SPELL_SPECIFIC_NORMAL; + [[fallthrough]]; + case SPELL_AURA_TRACK_RESOURCES: + case SPELL_AURA_TRACK_STEALTHED: + return SPELL_SPECIFIC_TRACKER; + } + } + } + + return SPELL_SPECIFIC_NORMAL; + }(); +} + +void SpellInfo::_LoadSpellDiminishInfo() +{ + auto diminishingGroupCompute = [this](bool triggered) -> DiminishingGroup + { + if (IsPositive()) + return DIMINISHING_NONE; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].ApplyAuraName == SPELL_AURA_MOD_TAUNT) + return DIMINISHING_TAUNT; + } + + // Explicit Diminishing Groups + switch (SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + { + // Pet charge effects (Infernal Awakening, Demon Charge) + if (SpellVisual[0] == 2816 && SpellIconID == 15) + return DIMINISHING_CONTROLLED_STUN; + // Frost Tomb + else if (Id == 48400) + return DIMINISHING_NONE; + // Gnaw + else if (Id == 47481) + return DIMINISHING_CONTROLLED_STUN; + // ToC Icehowl Arctic Breath + else if (SpellVisual[0] == 14153) + return DIMINISHING_NONE; + // Black Plague + else if (Id == 64155) + return DIMINISHING_NONE; + // Screams of the Dead (King Ymiron) + else if (Id == 51750) + return DIMINISHING_NONE; + break; + } + // Event spells + case SPELLFAMILY_UNK1: + return DIMINISHING_NONE; + case SPELLFAMILY_MAGE: + { + // Frostbite + if (SpellFamilyFlags[1] & 0x80000000) + return DIMINISHING_ROOT; + // Shattered Barrier + else if (SpellVisual[0] == 12297) + return DIMINISHING_ROOT; + // Deep Freeze + else if (SpellIconID == 2939 && SpellVisual[0] == 9963) + return DIMINISHING_CONTROLLED_STUN; + // Frost Nova / Freeze (Water Elemental) + else if (SpellIconID == 193) + return DIMINISHING_CONTROLLED_ROOT; + // Dragon's Breath + else if (SpellFamilyFlags[0] & 0x800000) + return DIMINISHING_DRAGONS_BREATH; + break; + } + case SPELLFAMILY_WARRIOR: + { + // Hamstring - limit duration to 10s in PvP + if (SpellFamilyFlags[0] & 0x2) + return DIMINISHING_LIMITONLY; + // Charge Stun (own diminishing) + else if (SpellFamilyFlags[0] & 0x01000000) + return DIMINISHING_CHARGE; + break; + } + case SPELLFAMILY_WARLOCK: + { + // Curses/etc + if ((SpellFamilyFlags[0] & 0x80000000) || (SpellFamilyFlags[1] & 0x200)) + return DIMINISHING_LIMITONLY; + // Seduction + else if (SpellFamilyFlags[1] & 0x10000000) + return DIMINISHING_FEAR; + // Sin and Punishment (Priest spell, don't ask) + else if (SpellIconID == 1869) + return DIMINISHING_NONE; + break; + } + case SPELLFAMILY_DRUID: + { + // Pounce - there are two spells which share the same family: the stun effect and the bleed spell. We don't want the bleed to be affected by DR + if ((SpellFamilyFlags[0] & 0x20000) && Id != 9007) + return DIMINISHING_OPENING_STUN; + // Cyclone + else if (SpellFamilyFlags[1] & 0x20) + return DIMINISHING_CYCLONE; + // Entangling Roots + // Nature's Grasp + else if (SpellFamilyFlags[0] & 0x00000200) + return DIMINISHING_CONTROLLED_ROOT; + // Faerie Fire + else if (SpellFamilyFlags[0] & 0x400) + return DIMINISHING_LIMITONLY; + break; + } + case SPELLFAMILY_ROGUE: + { + // Gouge + if (SpellFamilyFlags[0] & 0x8) + return DIMINISHING_DISORIENT; + // Blind + else if (SpellFamilyFlags[0] & 0x1000000) + return DIMINISHING_FEAR; + // Cheap Shot + else if (SpellFamilyFlags[0] & 0x400) + return DIMINISHING_OPENING_STUN; + // Crippling poison - Limit to 10 seconds in PvP (No SpellFamilyFlags) + else if (SpellIconID == 163) + return DIMINISHING_LIMITONLY; + break; + } + case SPELLFAMILY_HUNTER: + { + // Hunter's Mark + if ((SpellFamilyFlags[0] & 0x400) && SpellIconID == 538) + return DIMINISHING_LIMITONLY; + // Scatter Shot (own diminishing) + else if ((SpellFamilyFlags[0] & 0x40000) && SpellIconID == 132) + return DIMINISHING_SCATTER_SHOT; + // Entrapment (own diminishing) + else if (SpellVisual[0] == 7484 && SpellIconID == 20) + return DIMINISHING_ENTRAPMENT; + // Wyvern Sting mechanic is MECHANIC_SLEEP but the diminishing is DIMINISHING_DISORIENT + else if ((SpellFamilyFlags[1] & 0x1000) && SpellIconID == 1721) + return DIMINISHING_DISORIENT; + // Freezing Arrow + else if (SpellFamilyFlags[0] & 0x8) + return DIMINISHING_DISORIENT; + break; + } + case SPELLFAMILY_PALADIN: + { + // Judgement of Justice - limit duration to 10s in PvP + if (SpellFamilyFlags[0] & 0x100000) + return DIMINISHING_LIMITONLY; + // Turn Evil + else if ((SpellFamilyFlags[1] & 0x804000) && SpellIconID == 309) + return DIMINISHING_FEAR; + break; + } + case SPELLFAMILY_SHAMAN: + { + // Storm, Earth and Fire - Earthgrab + if (SpellFamilyFlags[2] & 0x4000) + return DIMINISHING_NONE; + break; + } + case SPELLFAMILY_DEATHKNIGHT: + { + // Hungering Cold (no flags) + if (SpellIconID == 2797) + return DIMINISHING_DISORIENT; + // Mark of Blood + else if ((SpellFamilyFlags[0] & 0x10000000) && SpellIconID == 2285) + return DIMINISHING_LIMITONLY; + break; + } + default: + break; + } + + // Lastly - Set diminishing depending on mechanic + uint32 mechanic = GetAllEffectsMechanicMask(); + if (mechanic & (1 << MECHANIC_CHARM)) + return DIMINISHING_MIND_CONTROL; + if (mechanic & (1 << MECHANIC_SILENCE)) + return DIMINISHING_SILENCE; + if (mechanic & (1 << MECHANIC_SLEEP)) + return DIMINISHING_SLEEP; + if (mechanic & ((1 << MECHANIC_SAPPED) | (1 << MECHANIC_POLYMORPH) | (1 << MECHANIC_SHACKLE))) + return DIMINISHING_DISORIENT; + // Mechanic Knockout, except Blast Wave + if (mechanic & (1 << MECHANIC_KNOCKOUT) && SpellIconID != 292) + return DIMINISHING_DISORIENT; + if (mechanic & (1 << MECHANIC_DISARM)) + return DIMINISHING_DISARM; + if (mechanic & (1 << MECHANIC_FEAR)) + return DIMINISHING_FEAR; + if (mechanic & (1 << MECHANIC_STUN)) + return triggered ? DIMINISHING_STUN : DIMINISHING_CONTROLLED_STUN; + if (mechanic & (1 << MECHANIC_BANISH)) + return DIMINISHING_BANISH; + if (mechanic & (1 << MECHANIC_ROOT)) + return triggered ? DIMINISHING_ROOT : DIMINISHING_CONTROLLED_ROOT; + if (mechanic & (1 << MECHANIC_HORROR)) + return DIMINISHING_HORROR; + + return DIMINISHING_NONE; + }; + + auto diminishingTypeCompute = [](DiminishingGroup group) -> DiminishingReturnsType + { + switch (group) + { + case DIMINISHING_TAUNT: + case DIMINISHING_CONTROLLED_STUN: + case DIMINISHING_STUN: + case DIMINISHING_OPENING_STUN: + case DIMINISHING_CYCLONE: + case DIMINISHING_CHARGE: + return DRTYPE_ALL; + case DIMINISHING_LIMITONLY: + case DIMINISHING_NONE: + return DRTYPE_NONE; + default: + return DRTYPE_PLAYER; + } + }; + + auto diminishingMaxLevelCompute = [](DiminishingGroup group) -> DiminishingLevels + { + switch (group) + { + case DIMINISHING_TAUNT: + return DIMINISHING_LEVEL_TAUNT_IMMUNE; + default: + return DIMINISHING_LEVEL_IMMUNE; + } + }; + + auto diminishingLimitDurationCompute = [this](DiminishingGroup group) -> int32 + { + auto isGroupDurationLimited = [group]() -> bool + { + switch (group) + { + case DIMINISHING_BANISH: + case DIMINISHING_CONTROLLED_STUN: + case DIMINISHING_CONTROLLED_ROOT: + case DIMINISHING_CYCLONE: + case DIMINISHING_DISORIENT: + case DIMINISHING_ENTRAPMENT: + case DIMINISHING_FEAR: + case DIMINISHING_HORROR: + case DIMINISHING_MIND_CONTROL: + case DIMINISHING_OPENING_STUN: + case DIMINISHING_ROOT: + case DIMINISHING_STUN: + case DIMINISHING_SLEEP: + case DIMINISHING_LIMITONLY: + return true; + default: + return false; + } + }; + + if (!isGroupDurationLimited()) + return 0; + + // Explicit diminishing duration + switch (SpellFamilyName) + { + case SPELLFAMILY_DRUID: + { + // Faerie Fire - limit to 40 seconds in PvP (3.1) + if (SpellFamilyFlags[0] & 0x400) + return 40 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_HUNTER: + { + // Wyvern Sting + if (SpellFamilyFlags[1] & 0x1000) + return 6 * IN_MILLISECONDS; + // Hunter's Mark + if (SpellFamilyFlags[0] & 0x400) + return 30 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_PALADIN: + { + // Repentance - limit to 6 seconds in PvP + if (SpellFamilyFlags[0] & 0x4) + return 6 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_WARLOCK: + { + // Banish - limit to 6 seconds in PvP + if (SpellFamilyFlags[1] & 0x8000000) + return 6 * IN_MILLISECONDS; + // Curse of Tongues - limit to 12 seconds in PvP + else if (SpellFamilyFlags[2] & 0x800) + return 12 * IN_MILLISECONDS; + // Curse of Elements - limit to 120 seconds in PvP + else if (SpellFamilyFlags[1] & 0x200) + return 120 * IN_MILLISECONDS; + break; + } + default: + break; + } + + return 8 * IN_MILLISECONDS; + }; + + SpellDiminishInfo triggeredInfo, normalInfo; + triggeredInfo.DiminishGroup = diminishingGroupCompute(true); + triggeredInfo.DiminishReturnType = diminishingTypeCompute(triggeredInfo.DiminishGroup); + triggeredInfo.DiminishMaxLevel = diminishingMaxLevelCompute(triggeredInfo.DiminishGroup); + triggeredInfo.DiminishDurationLimit = diminishingLimitDurationCompute(triggeredInfo.DiminishGroup); + + normalInfo.DiminishGroup = diminishingGroupCompute(false); + normalInfo.DiminishReturnType = diminishingTypeCompute(normalInfo.DiminishGroup); + normalInfo.DiminishMaxLevel = diminishingMaxLevelCompute(normalInfo.DiminishGroup); + normalInfo.DiminishDurationLimit = diminishingLimitDurationCompute(normalInfo.DiminishGroup); + + _diminishInfoTriggered = triggeredInfo; + _diminishInfoNonTriggered = normalInfo; +} + +DiminishingGroup SpellInfo::GetDiminishingReturnsGroupForSpell(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishGroup : _diminishInfoNonTriggered.DiminishGroup; +} + +DiminishingReturnsType SpellInfo::GetDiminishingReturnsGroupType(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishReturnType : _diminishInfoNonTriggered.DiminishReturnType; +} + +DiminishingLevels SpellInfo::GetDiminishingReturnsMaxLevel(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishMaxLevel : _diminishInfoNonTriggered.DiminishMaxLevel; +} + +int32 SpellInfo::GetDiminishingReturnsLimitDuration(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishDurationLimit : _diminishInfoNonTriggered.DiminishDurationLimit; +} + +void SpellInfo::_LoadImmunityInfo() +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + uint32 schoolImmunityMask = 0; + uint32 applyHarmfulAuraImmunityMask = 0; + uint32 mechanicImmunityMask = 0; + uint32 dispelImmunity = 0; + uint32 damageImmunityMask = 0; + + if (!Effects[i].IsEffect()) + continue; + + int32 miscVal = Effects[i].MiscValue; + int32 amount = Effects[i].CalcValue(); + + ImmunityInfo& immuneInfo = _immunityInfo[i]; + + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MECHANIC_IMMUNITY_MASK: + { + switch (miscVal) + { + case 96: // Free Friend, Uncontrollable Frenzy, Warlord's Presence + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + break; + } + case 1615: // Incite Rage, Wolf Spirit, Overload, Lightning Tendrils + { + switch (Id) + { + case 43292: // Incite Rage + case 49172: // Wolf Spirit + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + [[fallthrough]]; + case 61869: // Overload + case 63481: + case 61887: // Lightning Tendrils + case 63486: + mechanicImmunityMask |= (1 << MECHANIC_INTERRUPT) | (1 << MECHANIC_SILENCE); + + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK); + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK_DEST); + break; + default: + break; + } + break; + } + case 679: // Mind Control, Avenging Fury + { + if (Id == 57742) // Avenging Fury + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 1557: // Startling Roar, Warlord Roar, Break Bonds, Stormshield + { + if (Id == 64187) // Stormshield + { + mechanicImmunityMask |= (1 << MECHANIC_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + } + else + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 1614: // Fixate + case 1694: // Fixated, Lightning Tendrils + { + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_ATTACK_ME); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_TAUNT); + break; + } + case 1630: // Fervor, Berserk + { + if (Id == 64112) // Berserk + { + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_ATTACK_ME); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_TAUNT); + } + else + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 477: // Bladestorm + case 1733: // Bladestorm, Killing Spree + { + if (!amount) + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK); + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK_DEST); + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 878: // Whirlwind, Fog of Corruption, Determination + { + if (Id == 66092) // Determination + { + mechanicImmunityMask |= (1 << MECHANIC_SNARE) | (1 << MECHANIC_STUN) + | (1 << MECHANIC_DISORIENTED) | (1 << MECHANIC_FREEZE); + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + } + break; + } + default: + break; + } + + if (immuneInfo.AuraTypeImmune.empty()) + { + if (miscVal & (1 << 10)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + if (miscVal & (1 << 1)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_TRANSFORM); + + // These flag can be recognized wrong: + if (miscVal & (1 << 6)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + if (miscVal & (1 << 0)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + if (miscVal & (1 << 2)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + if (miscVal & (1 << 9)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + if (miscVal & (1 << 7)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DISARM); + } + break; + } + case SPELL_AURA_MECHANIC_IMMUNITY: + { + switch (Id) + { + case 42292: // PvP trinket + case 59752: // Every Man for Himself + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED); + break; + case 34471: // The Beast Within + case 19574: // Bestial Wrath + case 53490: // Bullheaded + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + break; + case 54508: // Demonic Empowerment + mechanicImmunityMask |= (1 << MECHANIC_SNARE) | (1 << MECHANIC_ROOT) | (1 << MECHANIC_STUN); + break; + default: + if (miscVal < 1) + continue; + + mechanicImmunityMask |= 1 << miscVal; + break; + } + break; + } + case SPELL_AURA_EFFECT_IMMUNITY: + { + immuneInfo.SpellEffectImmune.insert(static_cast(miscVal)); + break; + } + case SPELL_AURA_STATE_IMMUNITY: + { + immuneInfo.AuraTypeImmune.insert(static_cast(miscVal)); + break; + } + case SPELL_AURA_SCHOOL_IMMUNITY: + { + schoolImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: + { + applyHarmfulAuraImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_DAMAGE_IMMUNITY: + { + damageImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_DISPEL_IMMUNITY: + { + dispelImmunity = uint32(miscVal); + break; + } + default: + break; + } + + immuneInfo.SchoolImmuneMask = schoolImmunityMask; + immuneInfo.ApplyHarmfulAuraImmuneMask = applyHarmfulAuraImmunityMask; + immuneInfo.MechanicImmuneMask = mechanicImmunityMask; + immuneInfo.DispelImmune = dispelImmunity; + immuneInfo.DamageSchoolMask = damageImmunityMask; + + immuneInfo.AuraTypeImmune.shrink_to_fit(); + immuneInfo.SpellEffectImmune.shrink_to_fit(); + + _allowedMechanicMask |= immuneInfo.MechanicImmuneMask; + } + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_STUNNED)) + { + switch (Id) + { + case 22812: // Barkskin + case 47585: // Dispersion + _allowedMechanicMask |= + (1 << MECHANIC_STUN) | + (1 << MECHANIC_FREEZE) | + (1 << MECHANIC_KNOCKOUT) | + (1 << MECHANIC_SLEEP); + break; + case 49039: // Lichborne, don't allow normal stuns + break; + default: + _allowedMechanicMask |= (1 << MECHANIC_STUN); + break; + } + } + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_CONFUSED)) + _allowedMechanicMask |= (1 << MECHANIC_DISORIENTED); + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_FLEEING)) + { + switch (Id) + { + case 22812: // Barkskin + case 47585: // Dispersion + _allowedMechanicMask |= (1 << MECHANIC_FEAR) | (1 << MECHANIC_HORROR); + break; + default: + _allowedMechanicMask |= (1 << MECHANIC_FEAR); + break; + } + } +} + +void SpellInfo::ApplyAllSpellImmunitiesTo(Unit* target, uint8 effIndex, bool apply) const +{ + ImmunityInfo const& immuneInfo = _immunityInfo[effIndex]; + + if (uint32 schoolImmunity = immuneInfo.SchoolImmuneMask) + { + target->ApplySpellImmune(Id, IMMUNITY_SCHOOL, schoolImmunity, apply); + + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + target->RemoveAppliedAuras([this, schoolImmunity](AuraApplication const* aurApp) -> bool + { + SpellInfo const* auraSpellInfo = aurApp->GetBase()->GetSpellInfo(); + return ((auraSpellInfo->GetSchoolMask() & schoolImmunity) != 0 && // Check for school mask + CanDispelAura(auraSpellInfo) && + (IsPositive() != aurApp->IsPositive()) && // Check spell vs aura possitivity + !auraSpellInfo->IsPassive() && // Don't remove passive auras + auraSpellInfo->Id != Id); // Don't remove self + }); + } + if (apply && schoolImmunity & SPELL_SCHOOL_MASK_NORMAL) + target->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::InvulnerabilityBuff); + } + + if (uint32 mechanicImmunity = immuneInfo.MechanicImmuneMask) + { + for (uint32 i = MECHANIC_CHARM; i < MAX_MECHANIC; ++i) + if (mechanicImmunity & (1 << i)) + target->ApplySpellImmune(Id, IMMUNITY_MECHANIC, i, apply); + + if (HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + if (apply) + target->RemoveAurasWithMechanic(mechanicImmunity, AuraRemoveFlags::ByDefault, Id); + else + { + std::vector aurasToUpdateTargets; + target->RemoveAppliedAuras([mechanicImmunity, &aurasToUpdateTargets](AuraApplication const* aurApp) + { + Aura* aura = aurApp->GetBase(); + if (aura->GetSpellInfo()->GetAllEffectsMechanicMask() & mechanicImmunity) + aurasToUpdateTargets.push_back(aura); + + // only update targets, don't remove anything + return false; + }); + + for (Aura* aura : aurasToUpdateTargets) + aura->UpdateTargetMap(aura->GetCaster()); + } + } + } + + if (uint32 dispelImmunity = immuneInfo.DispelImmune) + { + target->ApplySpellImmune(Id, IMMUNITY_DISPEL, dispelImmunity, apply); + + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + target->RemoveAppliedAuras([dispelImmunity](AuraApplication const* aurApp) -> bool + { + SpellInfo const* spellInfo = aurApp->GetBase()->GetSpellInfo(); + if (spellInfo->Dispel == dispelImmunity) + return true; + + return false; + }); + } + } + + if (uint32 damageImmunity = immuneInfo.DamageSchoolMask) + { + target->ApplySpellImmune(Id, IMMUNITY_DAMAGE, damageImmunity, apply); + + if (apply && damageImmunity & SPELL_SCHOOL_MASK_NORMAL) + target->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::InvulnerabilityBuff); + } + + for (AuraType auraType : immuneInfo.AuraTypeImmune) + { + target->ApplySpellImmune(Id, IMMUNITY_STATE, auraType, apply); + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + target->RemoveAurasByType(auraType, [](AuraApplication const* aurApp) -> bool + { + // if the aura has SPELL_ATTR0_NO_IMMUNITIES, then it cannot be removed by immunity + return !aurApp->GetBase()->GetSpellInfo()->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES); + }); + } + + for (SpellEffects effectType : immuneInfo.SpellEffectImmune) + target->ApplySpellImmune(Id, IMMUNITY_EFFECT, effectType, apply); +} + +bool SpellInfo::CanSpellProvideImmunityAgainstAura(SpellInfo const* auraSpellInfo) const +{ + if (!auraSpellInfo) + return false; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + ImmunityInfo const* immuneInfo = _immunityInfo + i; + + if (!auraSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) && !auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + if (uint32 schoolImmunity = immuneInfo->SchoolImmuneMask) + if ((auraSpellInfo->SchoolMask & schoolImmunity) != 0) + return true; + } + + if (uint32 mechanicImmunity = immuneInfo->MechanicImmuneMask) + if ((mechanicImmunity & (1 << auraSpellInfo->Mechanic)) != 0) + return true; + + if (uint32 dispelImmunity = immuneInfo->DispelImmune) + if (auraSpellInfo->Dispel == dispelImmunity) + return true; + + bool immuneToAllEffects = true; + for (uint8 effIndex = 0; effIndex < MAX_SPELL_EFFECTS; ++effIndex) + { + uint32 effectName = auraSpellInfo->Effects[effIndex].Effect; + if (!effectName) + continue; + + auto spellImmuneItr = immuneInfo->SpellEffectImmune.find(static_cast(effectName)); + if (spellImmuneItr == immuneInfo->SpellEffectImmune.cend()) + { + immuneToAllEffects = false; + break; + } + + if (uint32 mechanic = auraSpellInfo->Effects[effIndex].Mechanic) + { + if (!(immuneInfo->MechanicImmuneMask & (1 << mechanic))) + { + immuneToAllEffects = false; + break; + } + } + + if (!auraSpellInfo->HasAttribute(SPELL_ATTR3_ALWAYS_HIT)) + { + if (uint32 auraName = auraSpellInfo->Effects[effIndex].ApplyAuraName) + { + bool isImmuneToAuraEffectApply = false; + auto auraImmuneItr = immuneInfo->AuraTypeImmune.find(static_cast(auraName)); + if (auraImmuneItr != immuneInfo->AuraTypeImmune.cend()) + isImmuneToAuraEffectApply = true; + + if (!isImmuneToAuraEffectApply && !auraSpellInfo->IsPositiveEffect(effIndex) && !auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + if (uint32 applyHarmfulAuraImmunityMask = immuneInfo->ApplyHarmfulAuraImmuneMask) + if ((auraSpellInfo->GetSchoolMask() & applyHarmfulAuraImmunityMask) != 0) + isImmuneToAuraEffectApply = true; + } + + if (!isImmuneToAuraEffectApply) + { + immuneToAllEffects = false; + break; + } + } + } + } + + if (immuneToAllEffects) + return true; + } + + return false; +} + +// based on client Spell_C::CancelsAuraEffect +bool SpellInfo::SpellCancelsAuraEffect(SpellInfo const* auraSpellInfo, uint8 auraEffIndex) const +{ + if (!HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + return false; + + if (auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect != SPELL_EFFECT_APPLY_AURA && Effects[i].Effect != SPELL_EFFECT_APPLY_AURA_2) + continue; + + uint32 const miscValue = static_cast(Effects[i].MiscValue); + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_STATE_IMMUNITY: + if (miscValue != auraSpellInfo->Effects[auraEffIndex].ApplyAuraName) + continue; + break; + case SPELL_AURA_SCHOOL_IMMUNITY: + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: + if (auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES) || !(auraSpellInfo->SchoolMask & miscValue)) + continue; + break; + case SPELL_AURA_DISPEL_IMMUNITY: + if (miscValue != auraSpellInfo->Dispel) + continue; + break; + case SPELL_AURA_MECHANIC_IMMUNITY: + if (miscValue != auraSpellInfo->Mechanic) + { + if (miscValue != auraSpellInfo->Effects[auraEffIndex].Mechanic) + continue; + } + break; + default: + continue; + } + + return true; + } + + return false; +} + +uint32 SpellInfo::GetAllowedMechanicMask() const +{ + return _allowedMechanicMask; +} + +uint32 SpellInfo::GetMechanicImmunityMask(Unit* caster) const +{ + uint32 casterMechanicImmunityMask = caster->GetMechanicImmunityMask(); + uint32 mechanicImmunityMask = 0; + + if (CanBeInterrupted(caster, true)) + { + if (casterMechanicImmunityMask & (1 << MECHANIC_INTERRUPT)) + mechanicImmunityMask |= (1 << MECHANIC_INTERRUPT); + + if (casterMechanicImmunityMask & (1 << MECHANIC_SILENCE)) + mechanicImmunityMask |= (1 << MECHANIC_SILENCE); + } + + return mechanicImmunityMask; +} + +float SpellInfo::CalculateScaledCoefficient(Unit const* caster, float coefficient) const +{ + if (coefficient == 0.f || !caster || SpellScalingId == 0) + return coefficient; + + return coefficient *= GetSpellScalingMultiplier(caster->getLevel()); +} + +float SpellInfo::GetMinRange(bool positive) const +{ + if (!RangeEntry) + return 0.0f; + if (positive) + return RangeEntry->RangeMin[1]; + return RangeEntry->RangeMin[0]; +} + +float SpellInfo::GetMaxRange(bool positive, WorldObject* caster, Spell* spell) const +{ + if (!RangeEntry) + return 0.0f; + float range; + if (positive) + range = RangeEntry->RangeMax[1]; + else + range = RangeEntry->RangeMax[0]; + if (caster) + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Range, range, spell); + return range; +} + +int32 SpellInfo::GetDuration() const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + return (DurationEntry->Duration == -1) ? -1 : abs(DurationEntry->Duration); +} + +int32 SpellInfo::GetMaxDuration() const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + return (DurationEntry->MaxDuration == -1) ? -1 : abs(DurationEntry->MaxDuration); +} + +float SpellInfo::CalcPeriodicHasteMod(Unit const* caster) const +{ + float hasteMod = 1.f; + if (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC)) + hasteMod = caster->GetFloatValue(UNIT_MOD_CAST_HASTE); + else if (HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC) && caster->IsPlayer()) + hasteMod = caster->GetFloatValue(PLAYER_FIELD_MOD_HASTE); + + return hasteMod; +} + +int32 SpellInfo::CalcDuration(WorldObject const* caster /*= nullptr*/) const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + + int32 duration = GetMaxDuration(); + if (duration == -1) + return -1; + + Unit const* unitCaster = caster ? caster->ToUnit() : nullptr; + if (!unitCaster) + return std::min(GetMaxDuration(), DurationEntry->Duration); + + uint32 level = unitCaster->getLevel(); + if (MaxLevel > 0 && level > MaxLevel) + level = MaxLevel; + if (BaseLevel > 0) + level -= BaseLevel; + + duration = DurationEntry->Duration + DurationEntry->DurationPerLevel * level; + duration = std::min(GetMaxDuration(), duration); + + if (duration == -1) + return -1; + + // Increase duration based on combo points + if (HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DURATION)) + { + if (uint8 comboPoints = unitCaster->IsMovedByClient() ? unitCaster->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints() : 0) + { + if (GetDuration() != GetMaxDuration() && GetDuration() != -1) + duration += int32((GetMaxDuration() - GetDuration()) * comboPoints / 5); + } + } + + if (Player const* modOwner = unitCaster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Duration, duration); + + if (HasAttribute(SPELL_ATTR8_HASTE_AFFECTS_DURATION) || HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC)) + { + SpellEffIndex periodicEffectIndex = EFFECT_0; + bool hasPeriodicEffect = [&]() + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].AuraPeriod) + { + periodicEffectIndex = SpellEffIndex(i); + return true; + } + + return false; + }(); + + if (hasPeriodicEffect && !HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS) && (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC) || HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC))) + { + float hasteMod = CalcPeriodicHasteMod(unitCaster); + if (hasteMod > 0.001f) + { + if (HasAttribute(SPELL_ATTR8_HASTE_AFFECTS_DURATION)) + return int32(duration * hasteMod); + + int32 period = CalcPeriod(caster, periodicEffectIndex); + if (period > 0) + duration = int32(period * (duration / (float)period)); + } + } + } + + return duration; +} + +int32 SpellInfo::CalcPeriod(WorldObject const* caster, SpellEffIndex effIndex, Optional periodOverride /*= {}*/) const +{ + int32 period = 0; + if (periodOverride.has_value() && *periodOverride > 0) + period = *periodOverride; + else + { + if (Effects[effIndex].IsEffect() && Effects[effIndex].AuraPeriod != 0) + period = Effects[effIndex].AuraPeriod; + else + period = 5200; + } + + Unit const* unitCaster = caster ? caster->ToUnit() : nullptr; + if (!unitCaster) + return period; + + if (Player const* modOwner = unitCaster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Period, period); + + if (!HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS) && (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC) || HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC))) + { + float hasteMod = CalcPeriodicHasteMod(unitCaster); + if (hasteMod > 0.001f) + period = ((float)period * hasteMod); + } + + return period; +} + +uint32 SpellInfo::CalcCastTime(uint8 level, Spell* spell /*= nullptr*/) const +{ + int32 castTime = 0; + if (!level && spell && spell->GetCaster()->IsUnit()) + level = spell->GetCaster()->ToUnit()->getLevel(); + + // not all spells have cast time index and this is all is passive abilities + if (level && Scaling.CastTimeMax > 0) + { + castTime = Scaling.CastTimeMax; + if (Scaling.CastTimeMaxLevel > level) + castTime = Scaling.CastTimeMin + int32(level - 1) * (Scaling.CastTimeMax - Scaling.CastTimeMin) / (Scaling.CastTimeMaxLevel - 1); + } + else if (CastTimeEntry) + castTime = CastTimeEntry->Base; + + if (!castTime) + return 0; + + if (spell) + spell->GetCaster()->ModSpellCastTime(this, castTime, spell); + + return (castTime > 0) ? uint32(castTime) : 0; +} + +uint32 SpellInfo::GetMaxTicks() const +{ + uint32 totalTicks = 0; + int32 DotDuration = GetDuration(); + + for (uint8 x = 0; x < MAX_SPELL_EFFECTS; x++) + { + if (Effects[x].Effect == SPELL_EFFECT_APPLY_AURA || Effects[x].Effect == SPELL_EFFECT_APPLY_AURA_2) + { + switch (Effects[x].ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_48: + case SPELL_AURA_POWER_BURN: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_PERIODIC_DUMMY: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + case SPELL_AURA_PERIODIC_HEALTH_FUNNEL: + // skip infinite periodics + if (Effects[x].AuraPeriod > 0 && DotDuration > 0) + { + totalTicks = static_cast(DotDuration) / Effects[x].AuraPeriod; + if (HasAttribute(SPELL_ATTR5_EXTRA_INITIAL_PERIOD)) + ++totalTicks; + } + break; + default: + break; + } + } + } + + return totalTicks; +} + +uint32 SpellInfo::GetRecoveryTime() const +{ + return RecoveryTime > CategoryRecoveryTime ? RecoveryTime : CategoryRecoveryTime; +} + +int32 SpellInfo::CalcPowerCost(WorldObject const* caster, SpellSchoolMask schoolMask, Spell* spell) const +{ + // gameobject casts don't use power + Unit const* unitCaster = caster->ToUnit(); + if (!unitCaster) + return 0; + + // Spell drain all exist power on cast (Only paladin lay of Hands) + if (HasAttribute(SPELL_ATTR1_USE_ALL_MANA)) + { + // If power type - health drain all + if (PowerType == POWER_HEALTH) + return unitCaster->GetHealth(); + // Else drain all power + if (PowerType < MAX_POWERS) + return unitCaster->GetPower(static_cast<::PowerType>(PowerType)); + + TC_LOG_ERROR("spells", "SpellInfo::CalcPowerCost: Unknown power type '%d' in spell %d", PowerType, Id); + return 0; + } + + bool invertSign = false; + int32 powerCost = 0; + + if (SpellPowerId) + { + int32 manaCost = ManaCost; + powerCost = manaCost; + float powerCostPct = ManaCostPercentage2 != 0.f ? ManaCostPercentage2 : (float)ManaCostPercentage; + + if (manaCost < 0 || powerCostPct < 0.f) + { + manaCost = std::abs(manaCost); + powerCostPct = std::fabs(powerCostPct); + invertSign = true; + powerCost = manaCost; + } + + // Base calculation + if (ManaCostPercentage) + { + int32 ressource = 0; + switch (PowerType) + { + // health as power used + case POWER_HEALTH: + ressource = unitCaster->GetCreateHealth(); + break; + case POWER_MANA: + ressource = unitCaster->GetCreateMana(); + break; + case POWER_RAGE: + case POWER_FOCUS: + case POWER_ENERGY: + ressource = unitCaster->GetMaxPower(static_cast<::PowerType>(PowerType)); + break; + case POWER_RUNE: + case POWER_RUNIC_POWER: + TC_LOG_DEBUG("spells", "CalculateManaCost: Not implemented yet!"); + break; + default: + TC_LOG_ERROR("spells", "CalculateManaCost: Unknown power type '%d' in spell %d", PowerType, Id); + return 0; + } + + manaCost += int32((ressource * powerCostPct) / 100); + powerCost = manaCost; + } + + // Scaling + if (SpellScalingId) + powerCost = int32(ceilf(GetSpellScalingMultiplier(unitCaster->getLevel(), true) * (double)powerCost)); + else + { + uint32 manaCostPerLevel = invertSign ? -int32(ManaCostPerlevel) : ManaCostPerlevel; + powerCost = manaCostPerLevel * SpellLevel + manaCost; + } + } + + // Flat mod from caster auras by spell school and power type + Unit::AuraEffectList const& auras = unitCaster->GetAuraEffectsByType(SPELL_AURA_MOD_POWER_COST_SCHOOL); + for (Unit::AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + if (!((*i)->GetMiscValue() & schoolMask)) + continue; + if (!((*i)->GetMiscValueB() & (1 << PowerType))) + continue; + powerCost += (*i)->GetAmount(); + } + + // Shiv - costs 20 + weaponSpeed*10 energy (apply only to non-triggered spell with energy cost) + if (HasAttribute(SPELL_ATTR4_WEAPON_SPEED_COST_SCALING)) + { + uint32 speed = 0; + if (SpellShapeshiftFormEntry const* ss = sSpellShapeshiftFormStore.LookupEntry(unitCaster->GetShapeshiftForm())) + speed = ss->CombatRoundTime; + else + { + WeaponAttackType slot = BASE_ATTACK; + if (!HasAttribute(SPELL_ATTR3_REQUIRES_MAIN_HAND_WEAPON) && HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + slot = OFF_ATTACK; + + speed = unitCaster->GetBaseAttackTime(slot); + } + + powerCost += speed / 100; + } + + // Apply cost mod by spell + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::PowerCost0, powerCost, spell); + + if (!unitCaster->IsControlledByPlayer()) + { + if (HasAttribute(SPELL_ATTR0_SCALES_WITH_CREATURE_LEVEL)) + { + GtNPCManaCostScalerEntry const* spellScaler = sGtNPCManaCostScalerStore.LookupEntry(SpellLevel - 1); + GtNPCManaCostScalerEntry const* casterScaler = sGtNPCManaCostScalerStore.LookupEntry(unitCaster->getLevel() - 1); + if (spellScaler && spellScaler->ratio > 0.f && casterScaler && casterScaler->ratio > 0.f) + powerCost *= casterScaler->ratio / spellScaler->ratio; + } + } + + if (invertSign) + powerCost = -powerCost; + + // PCT mod from user auras by spell school and power type + Unit::AuraEffectList const& aurasPct = unitCaster->GetAuraEffectsByType(SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT); + for (Unit::AuraEffectList::const_iterator i = aurasPct.begin(); i != aurasPct.end(); ++i) + { + if (!((*i)->GetMiscValue() & schoolMask)) + continue; + if (!((*i)->GetMiscValueB() & (1 << PowerType))) + continue; + powerCost += CalculatePct(powerCost, (*i)->GetAmount()); + } + if (powerCost < 0) + powerCost = 0; + return powerCost; +} + +float SpellInfo::GetSpellScalingMultiplier(int32 targetLevel, bool isPowerCostRelated /*= false*/) const +{ + if (targetLevel <= 0 || Scaling.Class == 0) + return 1.f; + + float multiplier = 1.f; + float scalingMultiplier = 1.f; + + if (targetLevel < Scaling.CastTimeMaxLevel && Scaling.CastTimeMax) + { + int32 castTime = Scaling.CastTimeMin + ((targetLevel - 1) * (Scaling.CastTimeMax - Scaling.CastTimeMin)) / (Scaling.CastTimeMaxLevel - 1); + multiplier = castTime / static_cast(Scaling.CastTimeMax); + scalingMultiplier = castTime / static_cast(Scaling.CastTimeMax); + } + + if (!isPowerCostRelated) + { + if (targetLevel < Scaling.NerfMaxLevel) + scalingMultiplier = ((((1.f - Scaling.NerfFactor) * (targetLevel - 1)) / (Scaling.NerfMaxLevel - 1)) + Scaling.NerfFactor) * multiplier; + } + + return scalingMultiplier; +} + +bool SpellInfo::IsRanked() const +{ + return ChainEntry != nullptr; +} + +uint8 SpellInfo::GetRank() const +{ + if (!ChainEntry) + return 1; + return ChainEntry->rank; +} + +SpellInfo const* SpellInfo::GetFirstRankSpell() const +{ + if (!ChainEntry) + return this; + return ChainEntry->first; +} +SpellInfo const* SpellInfo::GetLastRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->last; +} +SpellInfo const* SpellInfo::GetNextRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->next; +} +SpellInfo const* SpellInfo::GetPrevRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->prev; +} + +SpellInfo const* SpellInfo::GetAuraRankForLevel(uint8 level) const +{ + // ignore passive spells + if (IsPassive()) + return this; + + // Client ignores spell with these attributes (sub_53D9D0) + if (HasAttribute(SPELL_ATTR0_AURA_IS_DEBUFF) || HasAttribute(SPELL_ATTR2_ALLOW_LOW_LEVEL_BUFF)) + return this; + + bool needRankSelection = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (IsPositiveEffect(i) && + (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA || + Effects[i].Effect == SPELL_EFFECT_APPLY_AURA_2 || + Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_PARTY || + Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID) && + !Effects[i].Scaling.Coefficient) + { + needRankSelection = true; + break; + } + } + + // not required + if (!needRankSelection) + return this; + + for (SpellInfo const* nextSpellInfo = this; nextSpellInfo != nullptr; nextSpellInfo = nextSpellInfo->GetPrevRankSpell()) + { + // if found appropriate level + if (uint32(level + 10) >= nextSpellInfo->SpellLevel) + return nextSpellInfo; + + // one rank less then + } + + // not found + return nullptr; +} + +bool SpellInfo::IsRankOf(SpellInfo const* spellInfo) const +{ + return GetFirstRankSpell() == spellInfo->GetFirstRankSpell(); +} + +bool SpellInfo::IsDifferentRankOf(SpellInfo const* spellInfo) const +{ + if (Id == spellInfo->Id) + return false; + return IsRankOf(spellInfo); +} + +bool SpellInfo::IsHighRankOf(SpellInfo const* spellInfo) const +{ + if (ChainEntry && spellInfo->ChainEntry) + if (ChainEntry->first == spellInfo->ChainEntry->first) + if (ChainEntry->rank > spellInfo->ChainEntry->rank) + return true; + + return false; +} + +void SpellInfo::_InitializeExplicitTargetMask() +{ + bool srcSet = false; + bool dstSet = false; + uint32 targetMask = Targets; + // prepare target mask using effect target entries + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect()) + continue; + + targetMask |= Effects[i].TargetA.GetExplicitTargetMask(srcSet, dstSet); + targetMask |= Effects[i].TargetB.GetExplicitTargetMask(srcSet, dstSet); + + // add explicit target flags based on spell effects which have EFFECT_IMPLICIT_TARGET_EXPLICIT and no valid target provided + if (Effects[i].GetImplicitTargetType() != EFFECT_IMPLICIT_TARGET_EXPLICIT) + continue; + + // extend explicit target mask only if valid targets for effect could not be provided by target types + uint32 effectTargetMask = Effects[i].GetMissingTargetMask(srcSet, dstSet, targetMask); + + // don't add explicit object/dest flags when spell has no max range + if (GetMaxRange(true) == 0.0f && GetMaxRange(false) == 0.0f) + effectTargetMask &= ~(TARGET_FLAG_UNIT_MASK | TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_CORPSE_MASK | TARGET_FLAG_DEST_LOCATION); + + targetMask |= effectTargetMask; + } + + ExplicitTargetMask = targetMask; +} + +inline bool _isPositiveTarget(SpellInfo const* spellInfo, uint8 effIndex) +{ + if (!spellInfo->Effects[effIndex].IsEffect()) + return true; + + return (spellInfo->Effects[effIndex].TargetA.GetCheckType() != TARGET_CHECK_ENEMY && + spellInfo->Effects[effIndex].TargetB.GetCheckType() != TARGET_CHECK_ENEMY); +} + +bool _isPositiveEffectImpl(SpellInfo const* spellInfo, uint8 effIndex, std::unordered_set>& visited) +{ + if (!spellInfo->Effects[effIndex].IsEffect()) + return true; + + // attribute may be already set in DB + if (!spellInfo->IsPositiveEffect(effIndex)) + return false; + + // passive auras like talents are all positive + if (spellInfo->IsPassive()) + return true; + + // not found a single positive spell with this attribute + if (spellInfo->HasAttribute(SPELL_ATTR0_AURA_IS_DEBUFF)) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR4_AURA_IS_BUFF)) + return true; + + visited.insert({ spellInfo->Id, effIndex }); + + //We need scaling level info for some auras that compute bp 0 or positive but should be debuffs + float bpScalePerLevel = spellInfo->Effects[effIndex].RealPointsPerLevel; + int32 bp = spellInfo->Effects[effIndex].CalcValue(); + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + switch (spellInfo->Id) + { + case 40268: // Spiritual Vengeance, Teron Gorefiend, Black Temple + case 61987: // Avenging Wrath Marker + case 61988: // Divine Shield exclude aura + case 64412: // Phase Punch, Algalon the Observer, Ulduar + case 72410: // Rune of Blood, Saurfang, Icecrown Citadel + case 71204: // Touch of Insignificance, Lady Deathwhisper, Icecrown Citadel + return false; + case 24732: // Bat Costume + case 30877: // Tag Murloc + case 61716: // Rabbit Costume + case 61734: // Noblegarden Bunny + case 62344: // Fists of Stone + case 50344: // Dream Funnel + case 61819: // Manabonked! (item) + case 61834: // Manabonked! (minigob) + case 73523: // Rigor Mortis + return true; + default: + break; + } + break; + case SPELLFAMILY_ROGUE: + switch (spellInfo->Id) + { + // Envenom must be considered as a positive effect even though it deals damage + case 32645: // Envenom + return true; + case 40251: // Shadow of Death, Teron Gorefiend, Black Temple + return false; + default: + break; + } + break; + case SPELLFAMILY_MAGE: + // Arcane Missiles + if (spellInfo->SpellFamilyFlags[0] == 0x00000800) + return false; + break; + case SPELLFAMILY_WARRIOR: + // Slam, Execute + if ((spellInfo->SpellFamilyFlags[0] & 0x20200000) != 0) + return false; + break; + default: + break; + } + + switch (spellInfo->Mechanic) + { + case MECHANIC_IMMUNE_SHIELD: + return true; + default: + break; + } + + // Special case: effects which determine positivity of whole spell + if (spellInfo->HasAttribute(SPELL_ATTR1_AURA_UNIQUE)) // @todo: rename to and handle SPELL_ATTR1_AURA_UNIQUE + { + // check for targets, there seems to be an assortment of dummy triggering spells that should be negative + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (!_isPositiveTarget(spellInfo, i)) + return false; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (spellInfo->Effects[i].Effect) + { + case SPELL_EFFECT_HEAL: + case SPELL_EFFECT_LEARN_SPELL: + case SPELL_EFFECT_SKILL_STEP: + case SPELL_EFFECT_HEAL_PCT: + return true; + case SPELL_EFFECT_INSTAKILL: + if (i != effIndex && // for spells like 38044: instakill effect is negative but auras on target must count as buff + spellInfo->Effects[i].TargetA.GetTarget() == spellInfo->Effects[effIndex].TargetA.GetTarget() && + spellInfo->Effects[i].TargetB.GetTarget() == spellInfo->Effects[effIndex].TargetB.GetTarget()) + return false; + break; + default: + break; + } + + if (spellInfo->Effects[i].IsAura()) + { + switch (spellInfo->Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_STEALTH: + case SPELL_AURA_MOD_UNATTACKABLE: + return true; + case SPELL_AURA_SCHOOL_HEAL_ABSORB: + case SPELL_AURA_EMPATHY: + case SPELL_AURA_MOD_DAMAGE_FROM_CASTER: + case SPELL_AURA_PREVENTS_FLEEING: + return false; + default: + break; + } + } + } + + switch (spellInfo->Effects[effIndex].Effect) + { + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_INSTAKILL: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_STEAL_BENEFICIAL_BUFF: + case SPELL_EFFECT_INTERRUPT_CAST: + case SPELL_EFFECT_PICKPOCKET: + case SPELL_EFFECT_GAMEOBJECT_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE_PCT: + case SPELL_EFFECT_APPLY_AREA_AURA_ENEMY: + case SPELL_EFFECT_TAMECREATURE: + case SPELL_EFFECT_DISTRACT: + return false; + case SPELL_EFFECT_ENERGIZE: + case SPELL_EFFECT_ENERGIZE_PCT: + case SPELL_EFFECT_HEAL_PCT: + case SPELL_EFFECT_HEAL_MAX_HEALTH: + case SPELL_EFFECT_HEAL_MECHANICAL: + return true; + case SPELL_EFFECT_KNOCK_BACK: + case SPELL_EFFECT_CHARGE: + case SPELL_EFFECT_PERSISTENT_AREA_AURA: + case SPELL_EFFECT_ATTACK_ME: + case SPELL_EFFECT_POWER_BURN: + // check targets + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_EFFECT_DISPEL: + // non-positive dispel + switch (spellInfo->Effects[effIndex].MiscValue) + { + case DISPEL_STEALTH: + case DISPEL_INVISIBILITY: + case DISPEL_ENRAGE: + return false; + default: + break; + } + + // also check targets + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_EFFECT_DISPEL_MECHANIC: + if (!_isPositiveTarget(spellInfo, effIndex)) + { + // non-positive mechanic dispel on negative target + switch (spellInfo->Effects[effIndex].MiscValue) + { + case MECHANIC_BANDAGE: + case MECHANIC_SHIELD: + case MECHANIC_MOUNT: + case MECHANIC_INVULNERABILITY: + return false; + default: + break; + } + } + break; + case SPELL_EFFECT_THREAT: + case SPELL_EFFECT_MODIFY_THREAT_PERCENT: + // check targets AND basepoints + if (!_isPositiveTarget(spellInfo, effIndex) && bp > 0) + return false; + break; + default: + break; + } + + if (spellInfo->Effects[effIndex].IsAura()) + { + // non-positive aura use + switch (spellInfo->Effects[effIndex].ApplyAuraName) + { + case SPELL_AURA_MOD_STAT: // dependent from basepoint sign (negative -> negative) + case SPELL_AURA_MOD_SKILL: + case SPELL_AURA_MOD_DODGE_PERCENT: + case SPELL_AURA_MOD_HEALING_DONE: + case SPELL_AURA_MOD_DAMAGE_DONE_CREATURE: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_MOD_CRIT_PCT: + case SPELL_AURA_MOD_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_CRIT_CHANCE: + case SPELL_AURA_MOD_RANGED_HASTE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE_2: + case SPELL_AURA_MOD_CASTING_SPEED_NOT_STACK: + case SPELL_AURA_HASTE_SPELLS: + case SPELL_AURA_HASTE_RANGED: + case SPELL_AURA_MOD_DETECT_RANGE: + case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT: + case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE: + case SPELL_AURA_MOD_INCREASE_SWIM_SPEED: + case SPELL_AURA_MOD_PERCENT_STAT: + case SPELL_AURA_MOD_INCREASE_HEALTH: + case SPELL_AURA_MOD_SPEED_ALWAYS: + if (bp < 0 || bpScalePerLevel < 0) //TODO: What if both are 0? Should it be a buff or debuff? + return false; + break; + case SPELL_AURA_MOD_ATTACKSPEED: // some buffs have negative bp, check both target and bp + case SPELL_AURA_MOD_MELEE_HASTE: + case SPELL_AURA_MOD_DAMAGE_DONE: + case SPELL_AURA_MOD_RESISTANCE: + case SPELL_AURA_MOD_RESISTANCE_PCT: + case SPELL_AURA_MOD_RATING: + case SPELL_AURA_MOD_ATTACK_POWER: + case SPELL_AURA_MOD_RANGED_ATTACK_POWER: + case SPELL_AURA_MOD_DAMAGE_PERCENT_DONE: + case SPELL_AURA_MOD_SPEED_SLOW_ALL: + case SPELL_AURA_MELEE_SLOW: + case SPELL_AURA_MOD_ATTACK_POWER_PCT: + case SPELL_AURA_MOD_HEALING_DONE_PERCENT: + case SPELL_AURA_MOD_HEALING_PCT: + case SPELL_AURA_MOD_DAMAGE_DONE_VERSUS_AURASTATE: + if (!_isPositiveTarget(spellInfo, effIndex) && bp < 0) + return false; + break; + case SPELL_AURA_MOD_DAMAGE_TAKEN: // dependent from basepoint sign (positive -> negative) + case SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN: + case SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN_PCT: + case SPELL_AURA_MOD_POWER_COST_SCHOOL: + case SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT: + case SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT: + case SPELL_AURA_BYPASS_ARMOR_FOR_CASTER: + if (bp > 0) + return false; + break; + case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN: // check targets and basepoints (ex Recklessness) + if (!_isPositiveTarget(spellInfo, effIndex) && bp > 0) + return false; + break; + case SPELL_AURA_MOD_HEALTH_REGEN_PERCENT: // check targets and basepoints (target enemy and negative bp -> negative) + if (!_isPositiveTarget(spellInfo, effIndex) && bp < 0) + return false; + break; + case SPELL_AURA_ADD_TARGET_TRIGGER: + return true; + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + if (!_isPositiveTarget(spellInfo, effIndex)) + { + if (SpellInfo const* spellTriggeredProto = sSpellMgr->GetSpellInfo(spellInfo->Effects[effIndex].TriggerSpell)) + { + // negative targets of main spell return early + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // already seen this + if (visited.count({ spellTriggeredProto->Id, i }) > 0) + continue; + + if (!spellTriggeredProto->Effects[i].Effect) + continue; + + // if non-positive trigger cast targeted to positive target this main cast is non-positive + // this will place this spell auras as debuffs + if (_isPositiveTarget(spellTriggeredProto, i) && !_isPositiveEffectImpl(spellTriggeredProto, i, visited)) + return false; + } + } + } + break; + case SPELL_AURA_PERIODIC_TRIGGER_SPELL: + case SPELL_AURA_MOD_STUN: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_MOD_DECREASE_SPEED: + case SPELL_AURA_MOD_FEAR: + case SPELL_AURA_MOD_TAUNT: + // special auras: they may have non negative target but still need to be marked as debuff + // checked again after all effects (SpellInfo::_InitializeSpellPositivity) + case SPELL_AURA_MOD_PACIFY: + case SPELL_AURA_MOD_PACIFY_SILENCE: + case SPELL_AURA_MOD_DISARM: + case SPELL_AURA_MOD_DISARM_OFFHAND: + case SPELL_AURA_MOD_DISARM_RANGED: + case SPELL_AURA_MOD_CHARM: + case SPELL_AURA_AOE_CHARM: + case SPELL_AURA_MOD_POSSESS: + case SPELL_AURA_MOD_LANGUAGE: + case SPELL_AURA_DAMAGE_SHIELD: + case SPELL_AURA_PROC_TRIGGER_SPELL: + case SPELL_AURA_MOD_ATTACKER_MELEE_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_RANGED_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE: + case SPELL_AURA_DUMMY: + case SPELL_AURA_PERIODIC_DUMMY: + case SPELL_AURA_MOD_HEALING: + case SPELL_AURA_MOD_WEAPON_CRIT_PERCENT: + case SPELL_AURA_POWER_BURN: + case SPELL_AURA_MOD_COOLDOWN: + case SPELL_AURA_MOD_INCREASE_SPEED: + case SPELL_AURA_MOD_PARRY_PERCENT: + case SPELL_AURA_SET_VEHICLE_ID: + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_EFFECT_IMMUNITY: + case SPELL_AURA_OVERRIDE_CLASS_SCRIPTS: + case SPELL_AURA_MOD_SHAPESHIFT: + case SPELL_AURA_MOD_THREAT: + case SPELL_AURA_PROC_TRIGGER_SPELL_WITH_VALUE: + // check target for positive and negative spells + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_AURA_MOD_CONFUSE: + case SPELL_AURA_CHANNEL_DEATH_ITEM: + case SPELL_AURA_MOD_ROOT: + case SPELL_AURA_MOD_SILENCE: + case SPELL_AURA_MOD_DETAUNT: + case SPELL_AURA_GHOST: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_MOD_STALKED: + case SPELL_AURA_PREVENT_RESURRECTION: + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS: + case SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS: + case SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE: + return false; + case SPELL_AURA_MECHANIC_IMMUNITY: + { + // non-positive immunities + switch (spellInfo->Effects[effIndex].MiscValue) + { + case MECHANIC_BANDAGE: + case MECHANIC_SHIELD: + case MECHANIC_MOUNT: + case MECHANIC_INVULNERABILITY: + return false; + default: + break; + } + break; + } + case SPELL_AURA_ADD_FLAT_MODIFIER: // mods + case SPELL_AURA_ADD_PCT_MODIFIER: + { + switch (SpellModOp(spellInfo->Effects[effIndex].MiscValue)) + { + case SpellModOp::ChangeCastTime: // dependent from basepoint sign (positive -> negative) + case SpellModOp::Period: + case SpellModOp::PowerCostOnMiss: + case SpellModOp::StartCooldown: + if (bp > 0) + return false; + break; + case SpellModOp::Cooldown: + case SpellModOp::PowerCost0: + if (!spellInfo->IsPositive() && bp > 0) // dependent on prev effects too (ex Arcane Power) + return false; + break; + case SpellModOp::PointsIndex0: // always positive + case SpellModOp::PointsIndex1: + case SpellModOp::PointsIndex2: + case SpellModOp::Points: + case SpellModOp::Hate: + case SpellModOp::ChainAmplitude: + case SpellModOp::Amplitude: + return true; + case SpellModOp::Duration: + case SpellModOp::CritChance: + case SpellModOp::HealingAndDamage: + case SpellModOp::ChainTargets: + if (!spellInfo->IsPositive() && bp < 0) // dependent on prev effects too + return false; + break; + default: // dependent from basepoint sign (negative -> negative) + if (bp < 0) + return false; + break; + } + break; + } + default: + break; + } + } + // negative spell if triggered spell is negative + if (!spellInfo->Effects[effIndex].ApplyAuraName && spellInfo->Effects[effIndex].TriggerSpell) + { + if (SpellInfo const* spellTriggeredProto = sSpellMgr->GetSpellInfo(spellInfo->Effects[effIndex].TriggerSpell)) + { + // spells with at least one negative effect are considered negative + // some self-applied spells have negative effects but in self casting case negative check ignored. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // already seen this + if (visited.count({ spellTriggeredProto->Id, i }) > 0) + continue; + + if (!spellTriggeredProto->Effects[i].IsEffect()) + continue; + + if (!_isPositiveEffectImpl(spellTriggeredProto, i, visited)) + return false; + } + } + } + + // ok, positive + return true; +} + +void SpellInfo::_InitializeSpellPositivity() +{ + std::unordered_set> visited; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (!_isPositiveEffectImpl(this, i, visited)) + AttributesCu |= (SPELL_ATTR0_CU_NEGATIVE_EFF0 << i); + + // additional checks after effects marked + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect() || !IsPositiveEffect(i)) + continue; + + switch (Effects[i].ApplyAuraName) + { + // has other non positive effect? + // then it should be marked negative if has same target as negative effect (ex 8510, 8511, 8893, 10267) + case SPELL_AURA_DUMMY: + case SPELL_AURA_MOD_STUN: + case SPELL_AURA_MOD_FEAR: + case SPELL_AURA_MOD_TAUNT: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_MOD_ATTACKSPEED: + case SPELL_AURA_MOD_DECREASE_SPEED: + { + for (uint8 j = i + 1; j < MAX_SPELL_EFFECTS; ++j) + if (!IsPositiveEffect(j) && Effects[i].TargetA.GetTarget() == Effects[j].TargetA.GetTarget() && Effects[i].TargetB.GetTarget() == Effects[j].TargetB.GetTarget()) + AttributesCu |= (SPELL_ATTR0_CU_NEGATIVE_EFF0 << i); + + break; + } + default: + break; + } + } +} + +SpellTargetRestrictionsEntry const* SpellInfo::GetSpellTargetRestrictions() const +{ + return SpellTargetRestrictionsId ? sSpellTargetRestrictionsStore.LookupEntry(SpellTargetRestrictionsId) : nullptr; +} + +SpellEquippedItemsEntry const* SpellInfo::GetSpellEquippedItems() const +{ + return SpellEquippedItemsId ? sSpellEquippedItemsStore.LookupEntry(SpellEquippedItemsId) : nullptr; +} + +SpellInterruptsEntry const* SpellInfo::GetSpellInterrupts() const +{ + return SpellInterruptsId ? sSpellInterruptsStore.LookupEntry(SpellInterruptsId) : nullptr; +} + +SpellLevelsEntry const* SpellInfo::GetSpellLevels() const +{ + return SpellLevelsId ? sSpellLevelsStore.LookupEntry(SpellLevelsId) : nullptr; +} + +SpellPowerEntry const* SpellInfo::GetSpellPower() const +{ + return SpellPowerId ? sSpellPowerStore.LookupEntry(SpellPowerId) : nullptr; +} + +SpellReagentsEntry const* SpellInfo::GetSpellReagents() const +{ + return SpellReagentsId ? sSpellReagentsStore.LookupEntry(SpellReagentsId) : nullptr; +} + +SpellScalingEntry const* SpellInfo::GetSpellScaling() const +{ + return SpellScalingId ? sSpellScalingStore.LookupEntry(SpellScalingId) : nullptr; +} + +SpellShapeshiftEntry const* SpellInfo::GetSpellShapeshift() const +{ + return SpellShapeshiftId ? sSpellShapeshiftStore.LookupEntry(SpellShapeshiftId) : nullptr; +} + +SpellTotemsEntry const* SpellInfo::GetSpellTotems() const +{ + return SpellTotemsId ? sSpellTotemsStore.LookupEntry(SpellTotemsId) : nullptr; +} + +SpellAuraOptionsEntry const* SpellInfo::GetSpellAuraOptions() const +{ + return SpellAuraOptionsId ? sSpellAuraOptionsStore.LookupEntry(SpellAuraOptionsId) : nullptr; +} + +SpellAuraRestrictionsEntry const* SpellInfo::GetSpellAuraRestrictions() const +{ + return SpellAuraRestrictionsId ? sSpellAuraRestrictionsStore.LookupEntry(SpellAuraRestrictionsId) : nullptr; +} + +SpellCastingRequirementsEntry const* SpellInfo::GetSpellCastingRequirements() const +{ + return SpellCastingRequirementsId ? sSpellCastingRequirementsStore.LookupEntry(SpellCastingRequirementsId) : nullptr; +} + +SpellCategoriesEntry const* SpellInfo::GetSpellCategories() const +{ + return SpellCategoriesId ? sSpellCategoriesStore.LookupEntry(SpellCategoriesId) : nullptr; +} + +SpellClassOptionsEntry const* SpellInfo::GetSpellClassOptions() const +{ + return SpellClassOptionsId ? sSpellClassOptionsStore.LookupEntry(SpellClassOptionsId) : nullptr; +} + +SpellCooldownsEntry const* SpellInfo::GetSpellCooldowns() const +{ + return SpellCooldownsId ? sSpellCooldownsStore.LookupEntry(SpellCooldownsId) : nullptr; +} + +void SpellInfo::_UnloadImplicitTargetConditionLists() +{ + // find the same instances of ConditionList and delete them. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + ConditionContainer* cur = Effects[i].ImplicitTargetConditions; + if (!cur) + continue; + for (uint8 j = i; j < MAX_SPELL_EFFECTS; ++j) + { + if (Effects[j].ImplicitTargetConditions == cur) + Effects[j].ImplicitTargetConditions = nullptr; + } + delete cur; + } +} diff --git a/src/server/game/Spells/SpellInfo.cpp.bak-viano-virtual-icons-aura-v1-20261001-174000 b/src/server/game/Spells/SpellInfo.cpp.bak-viano-virtual-icons-aura-v1-20261001-174000 new file mode 100644 index 0000000000..a0f2c9913d --- /dev/null +++ b/src/server/game/Spells/SpellInfo.cpp.bak-viano-virtual-icons-aura-v1-20261001-174000 @@ -0,0 +1,4522 @@ +/* + * 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 . + */ + +#include "SpellInfo.h" +#include "Battleground.h" +#include "Corpse.h" +#include "Creature.h" +#include "DBCStores.h" +#include "GameClient.h" +#include "GameTables.h" +#include "InstanceScript.h" +#include "Item.h" +#include "ItemTemplate.h" +#include "Log.h" +#include "LootMgr.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "Random.h" +#include "Spell.h" +#include "SpellAuraDefines.h" +#include "SpellAuraEffects.h" +#include "SpellMgr.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +uint32 GetTargetFlagMask(SpellTargetObjectTypes objType) +{ + switch (objType) + { + case TARGET_OBJECT_TYPE_DEST: + return TARGET_FLAG_DEST_LOCATION; + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + return TARGET_FLAG_DEST_LOCATION | TARGET_FLAG_UNIT; + case TARGET_OBJECT_TYPE_CORPSE_ALLY: + return TARGET_FLAG_CORPSE_ALLY; + case TARGET_OBJECT_TYPE_CORPSE_ENEMY: + return TARGET_FLAG_CORPSE_ENEMY; + case TARGET_OBJECT_TYPE_CORPSE: + return TARGET_FLAG_CORPSE_ALLY | TARGET_FLAG_CORPSE_ENEMY; + case TARGET_OBJECT_TYPE_UNIT: + return TARGET_FLAG_UNIT; + case TARGET_OBJECT_TYPE_GOBJ: + return TARGET_FLAG_GAMEOBJECT; + case TARGET_OBJECT_TYPE_GOBJ_ITEM: + return TARGET_FLAG_GAMEOBJECT_ITEM; + case TARGET_OBJECT_TYPE_ITEM: + return TARGET_FLAG_ITEM; + case TARGET_OBJECT_TYPE_SRC: + return TARGET_FLAG_SOURCE_LOCATION; + default: + return TARGET_FLAG_NONE; + } +} + +SpellImplicitTargetInfo::SpellImplicitTargetInfo(uint32 target) +{ + _target = Targets(target); +} + +bool SpellImplicitTargetInfo::IsArea() const +{ + return GetSelectionCategory() == TARGET_SELECT_CATEGORY_AREA || GetSelectionCategory() == TARGET_SELECT_CATEGORY_CONE; +} + +SpellTargetSelectionCategories SpellImplicitTargetInfo::GetSelectionCategory() const +{ + return _data[_target].SelectionCategory; +} + +SpellTargetReferenceTypes SpellImplicitTargetInfo::GetReferenceType() const +{ + return _data[_target].ReferenceType; +} + +SpellTargetObjectTypes SpellImplicitTargetInfo::GetObjectType() const +{ + return _data[_target].ObjectType; +} + +SpellTargetCheckTypes SpellImplicitTargetInfo::GetCheckType() const +{ + return _data[_target].SelectionCheckType; +} + +SpellTargetDirectionTypes SpellImplicitTargetInfo::GetDirectionType() const +{ + return _data[_target].DirectionType; +} + +float SpellImplicitTargetInfo::CalcDirectionAngle(SpellEffectInfo const& effectInfo) const +{ + switch (GetDirectionType()) + { + case TARGET_DIR_FRONT: + return 0.0f; + case TARGET_DIR_BACK: + return static_cast(M_PI); + case TARGET_DIR_RIGHT: + return static_cast(-M_PI/2); + case TARGET_DIR_LEFT: + return static_cast(M_PI/2); + case TARGET_DIR_FRONT_RIGHT: + return static_cast(-M_PI/4); + case TARGET_DIR_BACK_RIGHT: + return static_cast(-3*M_PI/4); + case TARGET_DIR_BACK_LEFT: + return static_cast(3*M_PI/4); + case TARGET_DIR_FRONT_LEFT: + return static_cast(M_PI/4); + case TARGET_DIR_RANDOM: + return rand_norm() * static_cast(2 * M_PI); + case TARGET_DIR_SUMMON: + // This direction does alter its angle based on what is being summoned. + // Creatures are being summoned on the left, gameobjects infront + switch (effectInfo.Effect) + { + case SPELL_EFFECT_SUMMON_PET: + case SPELL_EFFECT_SUMMON: + return static_cast(M_PI / 2); + default: + return 0.0f; + } + default: + return 0.0f; + } +} + +Targets SpellImplicitTargetInfo::GetTarget() const +{ + return _target; +} + +uint32 SpellImplicitTargetInfo::GetExplicitTargetMask(bool& srcSet, bool& dstSet) const +{ + uint32 targetMask = 0; + if (GetTarget() == TARGET_DEST_TRAJ) + { + if (!srcSet) + targetMask = TARGET_FLAG_SOURCE_LOCATION; + if (!dstSet) + targetMask |= TARGET_FLAG_DEST_LOCATION; + } + else + { + switch (GetReferenceType()) + { + case TARGET_REFERENCE_TYPE_SRC: + if (srcSet) + break; + targetMask = TARGET_FLAG_SOURCE_LOCATION; + break; + case TARGET_REFERENCE_TYPE_DEST: + if (dstSet) + break; + targetMask = TARGET_FLAG_DEST_LOCATION; + break; + case TARGET_REFERENCE_TYPE_TARGET: + switch (GetObjectType()) + { + case TARGET_OBJECT_TYPE_GOBJ: + targetMask = TARGET_FLAG_GAMEOBJECT; + break; + case TARGET_OBJECT_TYPE_GOBJ_ITEM: + targetMask = TARGET_FLAG_GAMEOBJECT_ITEM; + break; + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + case TARGET_OBJECT_TYPE_UNIT: + case TARGET_OBJECT_TYPE_DEST: + switch (GetCheckType()) + { + case TARGET_CHECK_ENEMY: + targetMask = TARGET_FLAG_UNIT_ENEMY; + break; + case TARGET_CHECK_ALLY: + targetMask = TARGET_FLAG_UNIT_ALLY; + break; + case TARGET_CHECK_PARTY: + targetMask = TARGET_FLAG_UNIT_PARTY; + break; + case TARGET_CHECK_RAID: + targetMask = TARGET_FLAG_UNIT_RAID; + break; + case TARGET_CHECK_PASSENGER: + targetMask = TARGET_FLAG_UNIT_PASSENGER; + break; + case TARGET_CHECK_RAID_CLASS: + [[fallthrough]]; + default: + targetMask = TARGET_FLAG_UNIT; + break; + } + break; + default: + break; + } + break; + default: + break; + } + } + + switch (GetObjectType()) + { + case TARGET_OBJECT_TYPE_SRC: + srcSet = true; + break; + case TARGET_OBJECT_TYPE_DEST: + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + dstSet = true; + break; + default: + break; + } + return targetMask; +} + +SpellImplicitTargetInfo::StaticData SpellImplicitTargetInfo::_data[TOTAL_SPELL_TARGETS] = +{ + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 1 TARGET_UNIT_CASTER + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 2 TARGET_UNIT_NEARBY_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 3 TARGET_UNIT_NEARBY_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 4 TARGET_UNIT_NEARBY_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 5 TARGET_UNIT_PET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 6 TARGET_UNIT_TARGET_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 7 TARGET_UNIT_SRC_AREA_ENTRY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 8 TARGET_UNIT_DEST_AREA_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 9 TARGET_DEST_HOME + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 10 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 11 TARGET_UNIT_SRC_AREA_UNK_11 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 12 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 13 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 14 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 15 TARGET_UNIT_SRC_AREA_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 16 TARGET_UNIT_DEST_AREA_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 17 TARGET_DEST_DB + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 18 TARGET_DEST_CASTER + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 19 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 20 TARGET_UNIT_CASTER_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 21 TARGET_UNIT_TARGET_ALLY + {TARGET_OBJECT_TYPE_SRC, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 22 TARGET_SRC_CASTER + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 23 TARGET_GAMEOBJECT_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 24 TARGET_UNIT_CONE_ENEMY_24 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 25 TARGET_UNIT_TARGET_ANY + {TARGET_OBJECT_TYPE_GOBJ_ITEM, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 26 TARGET_GAMEOBJECT_ITEM_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 27 TARGET_UNIT_MASTER + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 28 TARGET_DEST_DYNOBJ_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 29 TARGET_DEST_DYNOBJ_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 30 TARGET_UNIT_SRC_AREA_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 31 TARGET_UNIT_DEST_AREA_ALLY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_SUMMON }, // 32 TARGET_DEST_CASTER_SUMMON + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 33 TARGET_UNIT_SRC_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 34 TARGET_UNIT_DEST_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 35 TARGET_UNIT_TARGET_PARTY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 36 TARGET_DEST_CASTER_UNK_36 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_LAST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 37 TARGET_UNIT_LASTTARGET_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 38 TARGET_UNIT_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 39 TARGET_DEST_CASTER_FISHING + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 40 TARGET_GAMEOBJECT_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 41 TARGET_DEST_CASTER_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 42 TARGET_DEST_CASTER_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 43 TARGET_DEST_CASTER_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 44 TARGET_DEST_CASTER_FRONT_LEFT + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 45 TARGET_UNIT_TARGET_CHAINHEAL_ALLY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 46 TARGET_DEST_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 47 TARGET_DEST_CASTER_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 48 TARGET_DEST_CASTER_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 49 TARGET_DEST_CASTER_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 50 TARGET_DEST_CASTER_LEFT + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 51 TARGET_GAMEOBJECT_SRC_AREA + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 52 TARGET_GAMEOBJECT_DEST_AREA + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 53 TARGET_DEST_TARGET_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 54 TARGET_UNIT_CONE_180_DEG_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 55 TARGET_DEST_CASTER_FRONT_LEAP + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 56 TARGET_UNIT_CASTER_AREA_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 57 TARGET_UNIT_TARGET_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 58 TARGET_UNIT_NEARBY_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ALLY, TARGET_DIR_FRONT}, // 59 TARGET_UNIT_CONE_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENTRY, TARGET_DIR_FRONT}, // 60 TARGET_UNIT_CONE_ENTRY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID_CLASS, TARGET_DIR_NONE}, // 61 TARGET_UNIT_TARGET_AREA_RAID_CLASS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 62 TARGET_DEST_CASTER_GROUND + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 63 TARGET_DEST_TARGET_ANY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 64 TARGET_DEST_TARGET_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 65 TARGET_DEST_TARGET_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 66 TARGET_DEST_TARGET_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 67 TARGET_DEST_TARGET_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 68 TARGET_DEST_TARGET_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 69 TARGET_DEST_TARGET_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 70 TARGET_DEST_TARGET_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 71 TARGET_DEST_TARGET_FRONT_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 72 TARGET_DEST_CASTER_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 73 TARGET_DEST_CASTER_RADIUS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 74 TARGET_DEST_TARGET_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 75 TARGET_DEST_TARGET_RADIUS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 76 TARGET_DEST_CHANNEL_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 77 TARGET_UNIT_CHANNEL_TARGET + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 78 TARGET_DEST_DEST_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 79 TARGET_DEST_DEST_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 80 TARGET_DEST_DEST_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 81 TARGET_DEST_DEST_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 82 TARGET_DEST_DEST_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 83 TARGET_DEST_DEST_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 84 TARGET_DEST_DEST_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 85 TARGET_DEST_DEST_FRONT_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 86 TARGET_DEST_DEST_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 87 TARGET_DEST_DEST + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 88 TARGET_DEST_DYNOBJ_NONE + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_TRAJ, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 89 TARGET_DEST_TRAJ + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 90 TARGET_UNIT_TARGET_MINIPET + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 91 TARGET_DEST_DEST_RADIUS + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 92 TARGET_UNIT_SUMMONER + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 93 TARGET_CORPSE_SRC_AREA_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 94 TARGET_UNIT_VEHICLE + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_PASSENGER, TARGET_DIR_NONE}, // 95 TARGET_UNIT_TARGET_PASSENGER + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 96 TARGET_UNIT_PASSENGER_0 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 97 TARGET_UNIT_PASSENGER_1 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 98 TARGET_UNIT_PASSENGER_2 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 99 TARGET_UNIT_PASSENGER_3 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 100 TARGET_UNIT_PASSENGER_4 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 101 TARGET_UNIT_PASSENGER_5 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 102 TARGET_UNIT_PASSENGER_6 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 103 TARGET_UNIT_PASSENGER_7 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 104 TARGET_UNIT_CONE_CASTER_TO_DEST_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 105 TARGET_UNIT_CASTER_AND_PASSENGERS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 106 TARGET_DEST_CHANNEL_CASTER + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 107 TARGET_DEST_NEARBY_ENTRY_2 + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 108 TARGET_GAMEOBJECT_CONE_CASTER_TO_DEST_ENEMY + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ALLY, TARGET_DIR_FRONT}, // 109 TARGET_GAMEOBJECT_CONE_CASTER_TO_DEST_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENTRY, TARGET_DIR_FRONT}, // 110 TARGET_UNIT_CONE_CASTER_TO_DEST_ENTRY + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 111 + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 112 TARGET_DEST_TARGET_ANY_2 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 113 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 114 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 115 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 116 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 117 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 118 TARGET_UNIT_TARGET_ALLY_OR_RAID + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 119 TARGET_CORPSE_SRC_AREA_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_SUMMONED, TARGET_DIR_NONE}, // 120 TARGET_UNIT_SELF_AND_SUMMONS + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 121 TARGET_CORPSE_TARGET_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_THREAT, TARGET_DIR_NONE}, // 122 TARGET_UNIT_AREA_THREAT_LIST + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_TAP, TARGET_DIR_NONE}, // 123 TARGET_UNIT_AREA_TAP_LIST + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 124 TARGET_UNIT_TARGET_TAP_LIST + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 125 TARGET_DEST_CASTER_GROUND_2 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE }, // 126 TARGET_UNIT_AREA_ENEMY + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 127 +}; + +SpellEffectInfo::SpellEffectInfo(SpellEntry const* /*spellEntry*/, SpellInfo const* spellInfo, uint8 effIndex, SpellEffectEntry const* _effect) +{ + SpellScalingEntry const* scaling = spellInfo->GetSpellScaling(); + + _spellInfo = spellInfo; + _effIndex = _effect ? _effect->EffectIndex : effIndex; + Effect = _effect ? _effect->Effect : 0; + ApplyAuraName = _effect ? _effect->EffectAura : 0; + AuraPeriod = _effect ? _effect->EffectAuraPeriod : 0; + DieSides = _effect ? _effect->EffectDieSides : 0; + RealPointsPerLevel = _effect ? _effect->EffectRealPointsPerLevel : 0.0f; + BasePoints = _effect ? _effect->EffectBasePoints : 0; + PointsPerComboPoint = _effect ? _effect->EffectPointsPerResource : 0.0f; + Amplitude = _effect ? _effect->EffectAmplitude : 0.0f; + DamageMultiplier = _effect ? _effect->EffectChainAmplitude : 0.0f; + BonusMultiplier = _effect ? _effect->EffectBonusCoefficient : 0.0f; + MiscValue = _effect ? _effect->EffectMiscValue : 0; + MiscValueB = _effect ? _effect->EffectMiscValueB : 0; + Mechanic = Mechanics(_effect ? _effect->EffectMechanic : 0); + TargetA = SpellImplicitTargetInfo(_effect ? _effect->EffectImplicitTargetA : 0); + TargetB = SpellImplicitTargetInfo(_effect ? _effect->EffectImplicitTargetB : 0); + TargetARadiusEntry = _effect && _effect->EffectRadiusIndex ? sSpellRadiusStore.LookupEntry(_effect->EffectRadiusIndex) : nullptr; + TargetBRadiusEntry = _effect && _effect->EffectRadiusMaxIndex ? sSpellRadiusStore.LookupEntry(_effect->EffectRadiusMaxIndex) : nullptr; + ChainTarget = _effect ? _effect->EffectChainTargets : 0; + ItemType = _effect ? _effect->EffectItemType : 0; + TriggerSpell = _effect ? _effect->EffectTriggerSpell : 0; + SpellClassMask = _effect ? _effect->EffectSpellClassMask : flag96(0); + ImplicitTargetConditions = nullptr; + + Scaling.Coefficient = scaling ? scaling->Coefficient[_effIndex] : 0.0f; + Scaling.Variance = scaling ? scaling->Variance[_effIndex] : 0.0f; + Scaling.ComboPointsCoefficient = scaling ? scaling->ComboPointsCoefficient[_effIndex] : 0.0f; +} + +bool SpellEffectInfo::IsEffect() const +{ + return Effect != 0; +} + +bool SpellEffectInfo::IsEffect(SpellEffects effectName) const +{ + return Effect == effectName; +} + +bool SpellEffectInfo::IsAura() const +{ + return (IsUnitOwnedAuraEffect() || Effect == SPELL_EFFECT_PERSISTENT_AREA_AURA) && ApplyAuraName != 0; +} + +bool SpellEffectInfo::IsAura(AuraType aura) const +{ + return IsAura() && ApplyAuraName == aura; +} + +bool SpellEffectInfo::IsTargetingArea() const +{ + return TargetA.IsArea() || TargetB.IsArea(); +} + +bool SpellEffectInfo::IsAreaAuraEffect() const +{ + if (Effect == SPELL_EFFECT_APPLY_AREA_AURA_PARTY || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_FRIEND || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_ENEMY || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_PET || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_OWNER) + return true; + return false; +} + +bool SpellEffectInfo::IsUnitOwnedAuraEffect() const +{ + return IsAreaAuraEffect() || Effect == SPELL_EFFECT_APPLY_AURA || Effect == SPELL_EFFECT_APPLY_AURA_2; +} + +bool SpellEffectInfo::ScalesWithCreatureLevel() const +{ + if (!_spellInfo->HasAttribute(SPELL_ATTR0_SCALES_WITH_CREATURE_LEVEL)) + return false; + + switch (Effect) + { + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_DUMMY: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_HEAL: + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_POWER_BURN: + case SPELL_EFFECT_SCRIPT_EFFECT: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_FORCE_CAST_WITH_VALUE: + case SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE: + case SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE: + return true; + default: + break; + } + + switch (ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_DUMMY: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_DAMAGE_SHIELD: + case SPELL_AURA_PROC_TRIGGER_DAMAGE: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_SCHOOL_ABSORB: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + return true; + default: + break; + } + + return false; +} + +int32 SpellEffectInfo::CalcValue(WorldObject const* caster /*= nullptr*/, int32 const* bp /*= nullptr*/, Unit const* target /*= nullptr*/) const +{ + float basePointsPerLevel = RealPointsPerLevel; + // TODO: this needs to be a float, not rounded + int basePoints = CalcBaseValue(caster, target); + double value = bp ? *bp : basePoints; + + Unit const* casterUnit = nullptr; + if (caster) + casterUnit = caster->ToUnit(); + + if (Scaling.Variance != 0.0f) + { + float delta = fabs(Scaling.Variance * 0.5f); + double valueVariance = frand(-delta, delta); + value += double(basePoints) * valueVariance; + } + else if (DieSides) + { + // roll in a range <1;EffectDieSides> as of patch 3.3.3 + if (DieSides == 1) + value += DieSides; + else + value += (DieSides >= 1) ? irand(1, DieSides) : irand(DieSides, 1); + } + + // base amount modification based on spell lvl vs caster lvl + if (Scaling.Coefficient == 0.0f) + { + if (casterUnit && basePointsPerLevel != 0.0) + { + int32 level = int32(casterUnit->getLevel()); + if (level > int32(_spellInfo->MaxLevel) && _spellInfo->MaxLevel > 0) + level = int32(_spellInfo->MaxLevel); + + // if base level is greater than spell level, reduce by base level (eg. pilgrims foods) + level -= int32(std::max(_spellInfo->BaseLevel, _spellInfo->SpellLevel)); + if (level < 0) + level = 0; + value += level * basePointsPerLevel; + } + } + + // bonus amount from combo points + if (casterUnit && _spellInfo->HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DAMAGE)) + { + double comboDamage = CalcPointsPerResource(casterUnit); + if (comboDamage != 0.0) + if (uint8 comboPoints = casterUnit->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints()) + value += comboDamage * comboPoints; + } + + if (_spellInfo->HasAttribute(SPELL_ATTR8_MASTERY_AFFECTS_POINTS)) + if (Player const* playerCaster = caster ? caster->ToPlayer() : nullptr) + value += playerCaster->GetFloatValue(PLAYER_MASTERY) * BonusMultiplier; + + if (casterUnit) + value = casterUnit->ApplyEffectModifiers(_spellInfo, _effIndex, value); + + return int32(round(value)); +} + +int32 SpellEffectInfo::CalcBaseValue(WorldObject const* caster, Unit const* target) const +{ + if (Scaling.Coefficient != 0.0f) + { + uint32 level = _spellInfo->SpellLevel; + if (target && _spellInfo->HasAttribute(SPELL_ATTR8_USE_TARGETS_LEVEL_FOR_SPELL_SCALING)) + level = target->getLevel(); + else if (caster && caster->IsUnit()) + level = caster->ToUnit()->getLevel(); + + if (_spellInfo->BaseLevel && _spellInfo->HasAttribute(SPELL_ATTR10_USE_SPELL_BASE_LEVEL_FOR_SCALING)) + level = _spellInfo->BaseLevel; + + float value = 0.0f; + + if (level > 0) + { + if (!_spellInfo->Scaling.Class) + return 0; + + value = GameTables::GetSpellScalingValue(level, static_cast(_spellInfo->Scaling.Class > 0 ? _spellInfo->Scaling.Class: (MAX_CLASSES - 1 /*last class*/) - _spellInfo->Scaling.Class)); + value *= _spellInfo->GetSpellScalingMultiplier(level, false); + } + + value *= Scaling.Coefficient; + if (value > 0.0f) + value = std::max(value, 1.0f); + + return int32(round(value)); + } + else + { + float value = BasePoints; + + if (ScalesWithCreatureLevel()) + { + // Some spells use the internal game table to calculate scaling coefficients for creatures + if (Creature const* creatureCaster = Object::ToCreature(caster)) + { + uint8 casterClass = creatureCaster->getClass(); + uint8 expansion = creatureCaster->GetCreatureTemplate()->GetHealthScalingExpansion(); + + float spellDamage = GetGameTableColumnForClass(sNpcDamageByClassGameTable[expansion].GetRow(_spellInfo->SpellLevel), casterClass); + float creatureDamage = GetGameTableColumnForClass(sNpcDamageByClassGameTable[expansion].GetRow(creatureCaster->getLevel()), casterClass); + + if (spellDamage != 0.0f) + value *= creatureDamage / spellDamage; + } + } + + return int32(round(value)); + } +} + +// Based on client function QuestTextParser::Spell_C_GetPointsPerResource (build 15595) +double SpellEffectInfo::CalcPointsPerResource(WorldObject const* caster) const +{ + double value = 0.0; + if (caster && caster->IsUnit()) + { + if (Scaling.Coefficient != 0.0f) + { + if (!_spellInfo->Scaling.Class) + return 0.0; + + int32 scalingClass = _spellInfo->Scaling.Class; + if (scalingClass < 0) + scalingClass = (MAX_CLASSES - 1 /*last class*/) - scalingClass; + + float scaledAmount = GameTables::GetSpellScalingValue(caster->ToUnit()->getLevel(), static_cast(scalingClass)); + scaledAmount *= _spellInfo->GetSpellScalingMultiplier(caster->ToUnit()->getLevel(), false) * Scaling.ComboPointsCoefficient; + value = scaledAmount; + } + } + + if (std::fabs(value) < 0.00000023841858 && PointsPerComboPoint != 0.0f) + return PointsPerComboPoint; + + return value; +} + +float SpellEffectInfo::CalcValueMultiplier(WorldObject* caster, Spell* spell) const +{ + float multiplier = Amplitude; + if (Player* modOwner = (caster ? caster->GetSpellModOwner() : nullptr)) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::Amplitude, multiplier, spell); + return multiplier; +} + +float SpellEffectInfo::CalcDamageMultiplier(WorldObject* caster, Spell* spell) const +{ + float multiplierPercent = DamageMultiplier * 100.0f; + if (Player* modOwner = (caster ? caster->GetSpellModOwner() : nullptr)) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::ChainAmplitude, multiplierPercent, spell); + return multiplierPercent / 100.0f; +} + +bool SpellEffectInfo::HasRadius(SpellTargetIndex targetIndex) const +{ + switch (targetIndex) + { + case SpellTargetIndex::TargetA: + return TargetARadiusEntry != nullptr; + case SpellTargetIndex::TargetB: + return TargetBRadiusEntry != nullptr; + default: + return false; + } +} + +float SpellEffectInfo::CalcRadius(WorldObject* caster /*= nullptr*/, SpellTargetIndex targetIndex /*=SpellTargetIndex::TargetA*/, Spell* spell /*= nullptr*/) const +{ + // TargetA -> TargetARadiusEntry + // TargetB -> TargetBRadiusEntry + // Aura effects have TargetARadiusEntry == TargetBRadiusEntry (mostly) + SpellImplicitTargetInfo target = TargetA; + SpellRadiusEntry const* entry = TargetARadiusEntry; + if (targetIndex == SpellTargetIndex::TargetB && HasRadius(targetIndex)) + { + target = TargetB; + entry = TargetBRadiusEntry; + } + + if (!entry) + return 0.0f; + + float radius = entry->RadiusMin; + + // Random targets use random value between RadiusMin and RadiusMax + // For other cases, client uses RadiusMax if RadiusMin is 0 + if (target.GetTarget() == TARGET_DEST_CASTER_RANDOM || + target.GetTarget() == TARGET_DEST_TARGET_RANDOM || + target.GetTarget() == TARGET_DEST_DEST_RANDOM) + radius += (entry->RadiusMax - radius) * std::sqrt(rand_norm()); + else if (radius == 0.0f) + radius = entry->RadiusMax; + + if (caster) + { + if (Unit* casterUnit = caster->ToUnit()) + radius += entry->RadiusPerLevel * casterUnit->getLevel(); + + radius = std::min(radius, entry->RadiusMax); + + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::Radius, radius, spell); + } + + return radius; +} + +uint32 SpellEffectInfo::GetProvidedTargetMask() const +{ + return GetTargetFlagMask(TargetA.GetObjectType()) | GetTargetFlagMask(TargetB.GetObjectType()); +} + +uint32 SpellEffectInfo::GetMissingTargetMask(bool srcSet /*= false*/, bool dstSet /*= false*/, uint32 mask /*=0*/) const +{ + uint32 effImplicitTargetMask = GetTargetFlagMask(GetUsedTargetObjectType()); + uint32 providedTargetMask = GetProvidedTargetMask() | mask; + + // remove all flags covered by effect target mask + if (providedTargetMask & TARGET_FLAG_UNIT_MASK) + effImplicitTargetMask &= ~(TARGET_FLAG_UNIT_MASK); + if (providedTargetMask & TARGET_FLAG_CORPSE_MASK) + effImplicitTargetMask &= ~(TARGET_FLAG_UNIT_MASK | TARGET_FLAG_CORPSE_MASK); + if (providedTargetMask & TARGET_FLAG_GAMEOBJECT_ITEM) + effImplicitTargetMask &= ~(TARGET_FLAG_GAMEOBJECT_ITEM | TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_ITEM); + if (providedTargetMask & TARGET_FLAG_GAMEOBJECT) + effImplicitTargetMask &= ~(TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_GAMEOBJECT_ITEM); + if (providedTargetMask & TARGET_FLAG_ITEM) + effImplicitTargetMask &= ~(TARGET_FLAG_ITEM | TARGET_FLAG_GAMEOBJECT_ITEM); + if (dstSet || providedTargetMask & TARGET_FLAG_DEST_LOCATION) + effImplicitTargetMask &= ~(TARGET_FLAG_DEST_LOCATION); + if (srcSet || providedTargetMask & TARGET_FLAG_SOURCE_LOCATION) + effImplicitTargetMask &= ~(TARGET_FLAG_SOURCE_LOCATION); + + return effImplicitTargetMask; +} + +SpellEffectImplicitTargetTypes SpellEffectInfo::GetImplicitTargetType() const +{ + return _data[Effect].ImplicitTargetType; +} + +SpellTargetObjectTypes SpellEffectInfo::GetUsedTargetObjectType() const +{ + return _data[Effect].UsedTargetObjectType; +} + +SpellEffectInfo::StaticData SpellEffectInfo::_data[TOTAL_SPELL_EFFECTS] = +{ + // implicit target type used target object type + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 0 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 1 SPELL_EFFECT_INSTAKILL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 2 SPELL_EFFECT_SCHOOL_DAMAGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 3 SPELL_EFFECT_DUMMY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 4 SPELL_EFFECT_PORTAL_TELEPORT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 5 SPELL_EFFECT_TELEPORT_UNITS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 6 SPELL_EFFECT_APPLY_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 7 SPELL_EFFECT_ENVIRONMENTAL_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 8 SPELL_EFFECT_POWER_DRAIN + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 9 SPELL_EFFECT_HEALTH_LEECH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 10 SPELL_EFFECT_HEAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 11 SPELL_EFFECT_BIND + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 12 SPELL_EFFECT_PORTAL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 13 SPELL_EFFECT_RITUAL_BASE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 14 SPELL_EFFECT_RITUAL_SPECIALIZE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 15 SPELL_EFFECT_RITUAL_ACTIVATE_PORTAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 16 SPELL_EFFECT_QUEST_COMPLETE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 17 SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 18 SPELL_EFFECT_RESURRECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 19 SPELL_EFFECT_ADD_EXTRA_ATTACKS + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 20 SPELL_EFFECT_DODGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 21 SPELL_EFFECT_EVADE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 22 SPELL_EFFECT_PARRY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 23 SPELL_EFFECT_BLOCK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 24 SPELL_EFFECT_CREATE_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 25 SPELL_EFFECT_WEAPON + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 26 SPELL_EFFECT_DEFENSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 27 SPELL_EFFECT_PERSISTENT_AREA_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 28 SPELL_EFFECT_SUMMON + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 29 SPELL_EFFECT_LEAP + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 30 SPELL_EFFECT_ENERGIZE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 31 SPELL_EFFECT_WEAPON_PERCENT_DAMAGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 32 SPELL_EFFECT_TRIGGER_MISSILE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ_ITEM}, // 33 SPELL_EFFECT_OPEN_LOCK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 34 SPELL_EFFECT_SUMMON_CHANGE_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 35 SPELL_EFFECT_APPLY_AREA_AURA_PARTY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 36 SPELL_EFFECT_LEARN_SPELL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 37 SPELL_EFFECT_SPELL_DEFENSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 38 SPELL_EFFECT_DISPEL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 39 SPELL_EFFECT_LANGUAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 40 SPELL_EFFECT_DUAL_WIELD + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 41 SPELL_EFFECT_JUMP + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 42 SPELL_EFFECT_JUMP_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 43 SPELL_EFFECT_TELEPORT_UNITS_FACE_CASTER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 44 SPELL_EFFECT_SKILL_STEP + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 45 SPELL_EFFECT_ADD_HONOR + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 46 SPELL_EFFECT_SPAWN + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 47 SPELL_EFFECT_TRADE_SKILL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 48 SPELL_EFFECT_STEALTH + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 49 SPELL_EFFECT_DETECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 50 SPELL_EFFECT_TRANS_DOOR + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 51 SPELL_EFFECT_FORCE_CRITICAL_HIT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 52 SPELL_EFFECT_GUARANTEE_HIT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 53 SPELL_EFFECT_ENCHANT_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 54 SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 55 SPELL_EFFECT_TAMECREATURE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 56 SPELL_EFFECT_SUMMON_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 57 SPELL_EFFECT_LEARN_PET_SPELL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 58 SPELL_EFFECT_WEAPON_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 59 SPELL_EFFECT_CREATE_RANDOM_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 60 SPELL_EFFECT_PROFICIENCY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 61 SPELL_EFFECT_SEND_EVENT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 62 SPELL_EFFECT_POWER_BURN + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 63 SPELL_EFFECT_THREAT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 64 SPELL_EFFECT_TRIGGER_SPELL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 65 SPELL_EFFECT_APPLY_AREA_AURA_RAID + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 66 SPELL_EFFECT_CREATE_MANA_GEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 67 SPELL_EFFECT_HEAL_MAX_HEALTH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 68 SPELL_EFFECT_INTERRUPT_CAST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 69 SPELL_EFFECT_DISTRACT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 70 SPELL_EFFECT_PULL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 71 SPELL_EFFECT_PICKPOCKET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 72 SPELL_EFFECT_ADD_FARSIGHT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 73 SPELL_EFFECT_UNTRAIN_TALENTS + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 74 SPELL_EFFECT_APPLY_GLYPH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 75 SPELL_EFFECT_HEAL_MECHANICAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 76 SPELL_EFFECT_SUMMON_OBJECT_WILD + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 77 SPELL_EFFECT_SCRIPT_EFFECT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 78 SPELL_EFFECT_ATTACK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 79 SPELL_EFFECT_SANCTUARY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 80 SPELL_EFFECT_ADD_COMBO_POINTS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 81 SPELL_EFFECT_CREATE_HOUSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 82 SPELL_EFFECT_BIND_SIGHT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 83 SPELL_EFFECT_DUEL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 84 SPELL_EFFECT_STUCK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 85 SPELL_EFFECT_SUMMON_PLAYER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 86 SPELL_EFFECT_ACTIVATE_OBJECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 87 SPELL_EFFECT_GAMEOBJECT_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 88 SPELL_EFFECT_GAMEOBJECT_REPAIR + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 89 SPELL_EFFECT_GAMEOBJECT_SET_DESTRUCTION_STATE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 90 SPELL_EFFECT_KILL_CREDIT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 91 SPELL_EFFECT_THREAT_ALL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 92 SPELL_EFFECT_ENCHANT_HELD_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 93 SPELL_EFFECT_FORCE_DESELECT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 94 SPELL_EFFECT_SELF_RESURRECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 95 SPELL_EFFECT_SKINNING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 96 SPELL_EFFECT_CHARGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 97 SPELL_EFFECT_CAST_BUTTON + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 98 SPELL_EFFECT_KNOCK_BACK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 99 SPELL_EFFECT_DISENCHANT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 100 SPELL_EFFECT_INEBRIATE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 101 SPELL_EFFECT_FEED_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 102 SPELL_EFFECT_DISMISS_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 103 SPELL_EFFECT_REPUTATION + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 104 SPELL_EFFECT_SUMMON_OBJECT_SLOT1 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 105 SPELL_EFFECT_SUMMON_OBJECT_SLOT2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 106 SPELL_EFFECT_SUMMON_OBJECT_SLOT3 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 107 SPELL_EFFECT_SUMMON_OBJECT_SLOT4 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 108 SPELL_EFFECT_DISPEL_MECHANIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 109 SPELL_EFFECT_SUMMON_DEAD_PET + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 110 SPELL_EFFECT_DESTROY_ALL_TOTEMS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 111 SPELL_EFFECT_DURABILITY_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 112 SPELL_EFFECT_112 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 113 SPELL_EFFECT_RESURRECT_NEW + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 114 SPELL_EFFECT_ATTACK_ME + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 115 SPELL_EFFECT_DURABILITY_DAMAGE_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ENEMY}, // 116 SPELL_EFFECT_SKIN_PLAYER_CORPSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 117 SPELL_EFFECT_SPIRIT_HEAL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 118 SPELL_EFFECT_SKILL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 119 SPELL_EFFECT_APPLY_AREA_AURA_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 120 SPELL_EFFECT_TELEPORT_GRAVEYARD + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 121 SPELL_EFFECT_NORMALIZED_WEAPON_DMG + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 122 SPELL_EFFECT_122 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 123 SPELL_EFFECT_SEND_TAXI + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 124 SPELL_EFFECT_PULL_TOWARDS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 125 SPELL_EFFECT_MODIFY_THREAT_PERCENT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 126 SPELL_EFFECT_STEAL_BENEFICIAL_BUFF + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 127 SPELL_EFFECT_PROSPECTING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 128 SPELL_EFFECT_APPLY_AREA_AURA_FRIEND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 129 SPELL_EFFECT_APPLY_AREA_AURA_ENEMY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 130 SPELL_EFFECT_REDIRECT_THREAT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 131 SPELL_EFFECT_PLAY_SOUND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 132 SPELL_EFFECT_PLAY_MUSIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 133 SPELL_EFFECT_UNLEARN_SPECIALIZATION + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 134 SPELL_EFFECT_KILL_CREDIT2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 135 SPELL_EFFECT_CALL_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 136 SPELL_EFFECT_HEAL_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 137 SPELL_EFFECT_ENERGIZE_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 138 SPELL_EFFECT_LEAP_BACK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 139 SPELL_EFFECT_CLEAR_QUEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 140 SPELL_EFFECT_FORCE_CAST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 141 SPELL_EFFECT_FORCE_CAST_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 142 SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 143 SPELL_EFFECT_APPLY_AREA_AURA_OWNER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 144 SPELL_EFFECT_KNOCK_BACK_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 145 SPELL_EFFECT_PULL_TOWARDS_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 146 SPELL_EFFECT_ACTIVATE_RUNE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 147 SPELL_EFFECT_QUEST_FAIL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 148 SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 149 SPELL_EFFECT_CHARGE_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 150 SPELL_EFFECT_QUEST_START + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 151 SPELL_EFFECT_TRIGGER_SPELL_2 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 152 SPELL_EFFECT_SUMMON_RAF_FRIEND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 153 SPELL_EFFECT_CREATE_TAMED_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 154 SPELL_EFFECT_DISCOVER_TAXI + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 155 SPELL_EFFECT_TITAN_GRIP + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 156 SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 157 SPELL_EFFECT_CREATE_ITEM_2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 158 SPELL_EFFECT_MILLING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 159 SPELL_EFFECT_ALLOW_RENAME_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 160 SPELL_EFFECT_FORCE_CAST_2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 161 SPELL_EFFECT_TALENT_SPEC_COUNT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 162 SPELL_EFFECT_TALENT_SPEC_SELECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 163 SPELL_EFFECT_163 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 164 SPELL_EFFECT_REMOVE_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 165 SPELL_EFFECT_165 + {EFFECT_IMPLICIT_TARGET_CASTER, TARGET_OBJECT_TYPE_UNIT}, // 166 SPELL_EFFECT_GIVE_CURRENCY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 167 SPELL_EFFECT_167 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 168 SPELL_EFFECT_168 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_ITEM}, // 169 SPELL_EFFECT_DESTROY_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 170 SPELL_EFFECT_170 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 171 SPELL_EFFECT_SUMMON_PERSONAL_GAMEOBJECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 172 SPELL_EFFECT_RESURRECT_WITH_AURA + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 173 SPELL_EFFECT_UNLOCK_GUILD_VAULT_TAB + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 174 SPELL_EFFECT_APPLY_AURA_2 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 175 SPELL_EFFECT_175 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 176 SPELL_EFFECT_176 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 177 SPELL_EFFECT_177 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 178 SPELL_EFFECT_178 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 179 SPELL_EFFECT_CREATE_AREATRIGGER + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 180 SPELL_EFFECT_180 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 181 SPELL_EFFECT_181 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 182 SPELL_EFFECT_182 +}; + +SpellInfo::SpellInfo(SpellEntry const* spellEntry, SpellEffectEntry const** effects) +{ + Id = spellEntry->ID; + Attributes = spellEntry->Attributes; + AttributesEx = spellEntry->AttributesEx; + AttributesEx2 = spellEntry->AttributesEx2; + AttributesEx3 = spellEntry->AttributesEx3; + AttributesEx4 = spellEntry->AttributesEx4; + AttributesEx5 = spellEntry->AttributesEx5; + AttributesEx6 = spellEntry->AttributesEx6; + AttributesEx7 = spellEntry->AttributesEx7; + AttributesEx8 = spellEntry->AttributesEx8; + AttributesEx9 = spellEntry->AttributesEx9; + AttributesEx10 = spellEntry->AttributesEx10; + AttributesCu = 0; + CastTimeEntry = spellEntry->CastingTimeIndex ? sSpellCastTimesStore.LookupEntry(spellEntry->CastingTimeIndex) : nullptr; + DurationEntry = spellEntry->DurationIndex ? sSpellDurationStore.LookupEntry(spellEntry->DurationIndex) : nullptr; + PowerType = spellEntry->PowerType; + RangeEntry = spellEntry->RangeIndex ? sSpellRangeStore.LookupEntry(spellEntry->RangeIndex) : nullptr; + Speed = spellEntry->Speed; + + for (uint8 i = 0; i < 2; ++i) + SpellVisual[i] = spellEntry->SpellVisualID[i]; + + SpellIconID = spellEntry->SpellIconID; + ActiveIconID = spellEntry->ActiveIconID; + SpellName = spellEntry->Name; + Rank = spellEntry->NameSubtext; + SchoolMask = spellEntry->SchoolMask; + RuneCostID = spellEntry->RuneCostID; + SpellDifficultyId = spellEntry->Difficulty; + BonusCoefficientFromAP = spellEntry->BonusCoefficient; + SpellScalingId = spellEntry->ScalingID; + SpellAuraOptionsId = spellEntry->AuraOptionsID; + SpellAuraRestrictionsId = spellEntry->AuraRestrictionsID; + SpellCastingRequirementsId = spellEntry->CastingRequirementsID; + SpellCategoriesId = spellEntry->CategoriesID; + SpellClassOptionsId = spellEntry->ClassOptionsID; + SpellCooldownsId = spellEntry->CooldownsID; + SpellEquippedItemsId = spellEntry->EquippedItemsID; + SpellInterruptsId = spellEntry->InterruptsID; + SpellLevelsId = spellEntry->LevelsID; + SpellPowerId = spellEntry->PowerDisplayID; + SpellReagentsId = spellEntry->ReagentsID; + SpellShapeshiftId = spellEntry->ShapeshiftID; + SpellTargetRestrictionsId = spellEntry->TargetRestrictionsID; + SpellTotemsId = spellEntry->TotemsID; + ResearchProjectId = spellEntry->RequiredProjectID; + + // SpellDifficultyEntry + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + Effects[i] = SpellEffectInfo(spellEntry, this, i, effects[i]); + + // SpellScalingEntry + SpellScalingEntry const* _scaling = GetSpellScaling(); + Scaling.CastTimeMin = _scaling ? _scaling->CastTimeMin : 0; + Scaling.CastTimeMax = _scaling ? _scaling->CastTimeMax : 0; + Scaling.CastTimeMaxLevel = _scaling ? _scaling->CastTimeMaxLevel : 0; + Scaling.Class = _scaling ? _scaling->Class : 0; + Scaling.NerfFactor = _scaling ? _scaling->NerfFactor : 0.f; + Scaling.NerfMaxLevel = _scaling ? _scaling->NerfMaxLevel : 0; + + // SpellAuraOptionsEntry + SpellAuraOptionsEntry const* _options = GetSpellAuraOptions(); + ProcFlags = _options ? ::ProcFlags(_options->ProcTypeMask) : PROC_FLAG_NONE; + ProcChance = _options ? _options->ProcChance : 0; + ProcCharges = _options ? _options->ProcCharges : 0; + StackAmount = _options ? _options->CumulativeAura : 0; + + // SpellAuraRestrictionsEntry + SpellAuraRestrictionsEntry const* _aura = GetSpellAuraRestrictions(); + CasterAuraState = _aura ? _aura->CasterAuraState : 0; + TargetAuraState = _aura ? _aura->TargetAuraState : 0; + CasterAuraStateNot = _aura ? _aura->ExcludeCasterAuraState : 0; + TargetAuraStateNot = _aura ? _aura->ExcludeTargetAuraState : 0; + CasterAuraSpell = _aura ? _aura->CasterAuraSpell : 0; + TargetAuraSpell = _aura ? _aura->TargetAuraSpell : 0; + ExcludeCasterAuraSpell = _aura ? _aura->ExcludeCasterAuraSpell : 0; + ExcludeTargetAuraSpell = _aura ? _aura->ExcludeTargetAuraSpell : 0; + + // SpellCastingRequirementsEntry + SpellCastingRequirementsEntry const* _castreq = GetSpellCastingRequirements(); + RequiresSpellFocus = _castreq ? _castreq->RequiresSpellFocus : 0; + FacingCasterFlags = _castreq ? SpellFacingCasterFlags(_castreq->FacingCasterFlags) : SpellFacingCasterFlags::None; + MinFactionId = _castreq ? _castreq->MinFactionID : 0; + MinReputation = _castreq ? _castreq->MinReputation : 0; + AreaGroupId = _castreq ? _castreq->RequiredAreasID : -1; + + // SpellCategoriesEntry + SpellCategoriesEntry const* _categorie = GetSpellCategories(); + CategoryEntry = _categorie ? sSpellCategoryStore.LookupEntry(_categorie->Category) : nullptr; + Dispel = _categorie ? _categorie->DispelType : 0; + Mechanic = _categorie ? _categorie->Mechanic : 0; + StartRecoveryCategory = _categorie ? _categorie->StartRecoveryCategory : 0; + DmgClass = _categorie ? _categorie->DefenseType : 0; + PreventionType = _categorie ? _categorie->PreventionType : 0; + + // SpellClassOptionsEntry + SpellClassOptionsEntry const* _class = GetSpellClassOptions(); + SpellFamilyName = _class ? _class->SpellClassSet : 0; + SpellFamilyFlags = _class ? _class->SpellFamilyMask : flag96(0); + + // SpellCooldownsEntry + SpellCooldownsEntry const* _cooldowns = GetSpellCooldowns(); + RecoveryTime = _cooldowns ? _cooldowns->RecoveryTime : 0; + CategoryRecoveryTime = _cooldowns ? _cooldowns->CategoryRecoveryTime : 0; + StartRecoveryTime = _cooldowns ? _cooldowns->StartRecoveryTime : 0; + + // SpellEquippedItemsEntry + SpellEquippedItemsEntry const* _equipped = GetSpellEquippedItems(); + EquippedItemClass = _equipped ? _equipped->EquippedItemClass : -1; + EquippedItemSubClassMask = _equipped ?_equipped->EquippedItemSubclass : -1; + EquippedItemInventoryTypeMask = _equipped ? _equipped->EquippedItemInvTypes : -1; + + // SpellInterruptsEntry + SpellInterruptsEntry const* _interrupt = GetSpellInterrupts(); + + AuraInterruptFlags = _interrupt ? SpellAuraInterruptFlags(_interrupt->AuraInterruptFlags[0]) : SpellAuraInterruptFlags::None; + AuraInterruptFlags2 = _interrupt? SpellAuraInterruptFlags2(_interrupt->AuraInterruptFlags[1]) : SpellAuraInterruptFlags2::None; + ChannelInterruptFlags = _interrupt ? SpellAuraInterruptFlags(_interrupt->ChannelInterruptFlags[0] ) : SpellAuraInterruptFlags::None; + ChannelInterruptFlags2 = _interrupt ? SpellAuraInterruptFlags2(_interrupt->ChannelInterruptFlags[1]) : SpellAuraInterruptFlags2::None; + InterruptFlags = _interrupt ? SpellInterruptFlags(_interrupt->InterruptFlags) : SpellInterruptFlags::None; + + // SpellLevelsEntry + SpellLevelsEntry const* _levels = GetSpellLevels(); + MaxLevel = _levels ? _levels->MaxLevel : 0; + BaseLevel = _levels ? _levels->BaseLevel : 0; + SpellLevel = _levels ? _levels->SpellLevel : 0; + + // SpellPowerEntry + SpellPowerEntry const* _power = GetSpellPower(); + ManaCost = _power ? _power->ManaCost : 0; + ManaCostPerlevel = _power ? _power->ManaCostPerLevel : 0; + ManaCostPercentage = _power ? _power->PowerCostPct : 0; + ManaCostPercentage2 = _power ? _power->PowerCostPct2 : 0.f; + ManaPerSecond = _power ? _power->ManaPerSecond : 0; + + // SpellReagentsEntry + SpellReagentsEntry const* _reagents = GetSpellReagents(); + for (uint8 i = 0; i < MAX_SPELL_REAGENTS; ++i) + Reagent[i] = _reagents ? _reagents->Reagents[i] : 0; + for (uint8 i = 0; i < MAX_SPELL_REAGENTS; ++i) + ReagentCount[i] = _reagents ? _reagents->ReagentCount[i] : 0; + + // SpellShapeshiftEntry + if (SpellShapeshiftEntry const* _shapeshift = GetSpellShapeshift()) + { + Stances = MAKE_PAIR64(_shapeshift->ShapeshiftMask[0], _shapeshift->ShapeshiftMask[1]); + StancesNot = MAKE_PAIR64(_shapeshift->ShapeshiftExclude[0], _shapeshift->ShapeshiftExclude[1]); + } + else + { + Stances = UI64LIT(0); + StancesNot = UI64LIT(0); + } + + // SpellTargetRestrictionsEntry + SpellTargetRestrictionsEntry const* _target = GetSpellTargetRestrictions(); + ConeAngle = _target ? _target->ConeAngle : 0.f; + Targets = _target ? _target->Targets : 0; + TargetCreatureType = _target ? _target->TargetCreatureType : 0; + MaxAffectedTargets = _target ? _target->MaxTargets : 0; + MaxTargetLevel = _target ? _target->MaxTargetLevel : 0; + + // SpellTotemsEntry + SpellTotemsEntry const* _totem = GetSpellTotems(); + for (uint8 i = 0; i < 2; ++i) + TotemCategory[i] = _totem ? _totem->RequiredTotemCategoryID[i] : 0; + for (uint8 i = 0; i < 2; ++i) + Totem[i] = _totem ? _totem->Totem[i] : 0; + + ChainEntry = nullptr; + ExplicitTargetMask = 0; + + _spellSpecific = SPELL_SPECIFIC_NORMAL; + _auraState = AURA_STATE_NONE; + + _allowedMechanicMask = 0; +} + +SpellInfo::~SpellInfo() +{ + _UnloadImplicitTargetConditionLists(); +} + +uint32 SpellInfo::GetCategory() const +{ + return CategoryEntry ? CategoryEntry->ID : 0; +} + +bool SpellInfo::HasEffect(SpellEffects effect) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect(effect)) + return true; + return false; +} + +bool SpellInfo::HasAura(AuraType aura) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAura(aura)) + return true; + return false; +} + +bool SpellInfo::HasAreaAuraEffect() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAreaAuraEffect()) + return true; + return false; +} + +bool SpellInfo::HasOnlyDamageEffects() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].IsEffect()) + { + switch (Effects[i].Effect) + { + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_HEALTH_LEECH: + continue; + default: + return false; + } + } + } + + return true; +} + +bool SpellInfo::HasTargetType(::Targets target) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect()) + continue; + + if (Effects[i].TargetA.GetTarget() == target || Effects[i].TargetB.GetTarget() == target) + return true; + } + + return false; +} + +bool SpellInfo::CanBeInterrupted(Unit const* interruptTarget, bool ignoreImmunity /*= false*/) const +{ + return HasAttribute(SPELL_ATTR7_NO_UI_NOT_INTERRUPTIBLE) + || HasChannelInterruptFlag(SpellAuraInterruptFlags::Damage | SpellAuraInterruptFlags::EnteringCombat) + || (interruptTarget->IsPlayer() && InterruptFlags.HasFlag(SpellInterruptFlags::DamageCancelsPlayerOnly)) + || ((!(interruptTarget->GetMechanicImmunityMask() & (1 << MECHANIC_INTERRUPT)) || ignoreImmunity) + && PreventionType & SPELL_PREVENTION_TYPE_SILENCE); +} + +bool SpellInfo::HasAnyAuraInterruptFlag() const +{ + return AuraInterruptFlags != SpellAuraInterruptFlags::None || AuraInterruptFlags2 != SpellAuraInterruptFlags2::None; +} + +bool SpellInfo::IsExplicitDiscovery() const +{ + return ((Effects[0].Effect == SPELL_EFFECT_CREATE_RANDOM_ITEM + || Effects[0].Effect == SPELL_EFFECT_CREATE_ITEM_2) + && Effects[1].Effect == SPELL_EFFECT_SCRIPT_EFFECT) + || Id == 64323; +} + +bool SpellInfo::IsLootCrafting() const +{ + return (Effects[0].Effect == SPELL_EFFECT_CREATE_RANDOM_ITEM || + // different random cards from Inscription (121==Virtuoso Inking Set category) r without explicit item + (Effects[0].Effect == SPELL_EFFECT_CREATE_ITEM_2 && + ((TotemCategory[0] != 0 || (Totem[0] != 0 && SpellIconID == 1)) || Effects[0].ItemType == 0))); +} + +bool SpellInfo::IsQuestTame() const +{ + return Effects[0].Effect == SPELL_EFFECT_THREAT && Effects[1].Effect == SPELL_EFFECT_APPLY_AURA && Effects[1].ApplyAuraName == SPELL_AURA_DUMMY; +} + +bool SpellInfo::IsProfessionOrRiding() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsProfessionOrRidingSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsProfession() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsProfessionSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsPrimaryProfession() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsPrimaryProfessionSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsPrimaryProfessionFirstRank() const +{ + return IsPrimaryProfession() && GetRank() == 1; +} + +bool SpellInfo::IsAbilityLearnedWithProfession() const +{ + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + { + SkillLineAbilityEntry const* pAbility = _spell_idx->second; + if (!pAbility || pAbility->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticSkillRank) + continue; + + if (pAbility->MinSkillLineRank > 0) + return true; + } + + return false; +} + +bool SpellInfo::IsAbilityOfSkillType(uint32 skillType) const +{ + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + if (_spell_idx->second->SkillLine == uint32(skillType)) + return true; + + return false; +} + +bool SpellInfo::IsAffectingArea() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && (Effects[i].IsTargetingArea() || Effects[i].IsEffect(SPELL_EFFECT_PERSISTENT_AREA_AURA) || Effects[i].IsAreaAuraEffect())) + return true; + return false; +} + +// checks if spell targets are selected from area, doesn't include spell effects in check (like area wide auras for example) +bool SpellInfo::IsTargetingArea() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].IsTargetingArea()) + return true; + return false; +} + +bool SpellInfo::NeedsExplicitUnitTarget() const +{ + return (GetExplicitTargetMask() & TARGET_FLAG_UNIT_MASK) != 0; +} + +bool SpellInfo::NeedsToBeTriggeredByCaster(SpellInfo const* triggeringSpell) const +{ + if (NeedsExplicitUnitTarget()) + return true; + + /* + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].IsEffect()) + { + if (Effects[i].TargetA.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CHANNEL + || Effects[i].TargetB.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CHANNEL) + return true; + } + } + */ + + if (triggeringSpell->IsChanneled()) + { + uint32 mask = 0; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER && Effects[i].TargetA.GetTarget() != TARGET_DEST_CASTER + && Effects[i].TargetB.GetTarget() != TARGET_UNIT_CASTER && Effects[i].TargetB.GetTarget() != TARGET_DEST_CASTER) + { + mask |= Effects[i].GetProvidedTargetMask(); + } + } + + if (mask & TARGET_FLAG_UNIT_MASK) + return true; + } + + return false; +} + +bool SpellInfo::IsSelfCast() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Effect && Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER) + return false; + return true; +} + +bool SpellInfo::IsPassive() const +{ + return HasAttribute(SPELL_ATTR0_PASSIVE); +} + +bool SpellInfo::IsAutocastable() const +{ + if (IsPassive()) + return false; + if (HasAttribute(SPELL_ATTR1_NO_AUTOCAST_AI)) + return false; + return true; +} + +bool SpellInfo::IsStackableWithRanks() const +{ + if (IsPassive()) + return false; + if (PowerType != POWER_MANA && PowerType != POWER_HEALTH) + return false; + if (IsProfessionOrRiding()) + return false; + + if (IsAbilityLearnedWithProfession()) + return false; + + // All stance spells. if any better way, change it. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (SpellFamilyName) + { + case SPELLFAMILY_PALADIN: + // Paladin aura Spell + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID) + return false; + break; + case SPELLFAMILY_DRUID: + // Druid form Spell + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA && + Effects[i].ApplyAuraName == SPELL_AURA_MOD_SHAPESHIFT) + return false; + break; + } + } + return true; +} + +bool SpellInfo::IsPassiveStackableWithRanks() const +{ + return IsPassive() && !HasEffect(SPELL_EFFECT_APPLY_AURA); +} + +bool SpellInfo::IsMultiSlotAura() const +{ + if (IsPassive() || HasAura(SPELL_AURA_CONTROL_VEHICLE)) + return true; + + switch (Id) + { + case 55849: // Power Spark + case 40075: // Fel Flak Fire + case 44413: // Incanter's Absorption + case 79010: // Point of Vulnerability + case 79920: // Shared Health + return true; + default: + return false; + } +} + +bool SpellInfo::IsStackableOnOneSlotWithDifferentCasters() const +{ + /// TODO: Re-verify meaning of SPELL_ATTR3_STACK_FOR_DIFF_CASTERS and update conditions here + return StackAmount > 1 && !IsChanneled() && !HasAttribute(SPELL_ATTR3_DOT_STACKING_RULE); +} + +bool SpellInfo::IsCooldownStartedOnEvent() const +{ + if (HasAttribute(SPELL_ATTR0_COOLDOWN_ON_EVENT)) + return true; + + return CategoryEntry && CategoryEntry->GetFlags().HasFlag(SpellCategoryFlags::CooldownEventOnLeaveCombat); +} + +bool SpellInfo::IsDeathPersistent() const +{ + return HasAttribute(SPELL_ATTR3_ALLOW_AURA_WHILE_DEAD); +} + +bool SpellInfo::IsRequiringDeadTarget() const +{ + return HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS); +} + +bool SpellInfo::IsAllowingDeadTarget() const +{ + if (HasAttribute(SPELL_ATTR2_ALLOW_DEAD_TARGET) || Targets & (TARGET_FLAG_CORPSE_ALLY | TARGET_FLAG_CORPSE_ENEMY | TARGET_FLAG_UNIT_DEAD)) + return true; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].TargetA.GetTarget() == TARGET_CORPSE_SRC_AREA_RAID) + return true; + + return false; +} + +bool SpellInfo::IsGroupBuff() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (Effects[i].TargetA.GetCheckType()) + { + case TARGET_CHECK_PARTY: + case TARGET_CHECK_RAID: + case TARGET_CHECK_RAID_CLASS: + return true; + default: + break; + } + } + + return false; +} + +bool SpellInfo::CanBeUsedInCombat() const +{ + return !HasAttribute(SPELL_ATTR0_NOT_IN_COMBAT_ONLY_PEACEFUL); +} + +bool SpellInfo::IsPositive() const +{ + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE); +} + +bool SpellInfo::IsPositiveEffect(uint8 effIndex) const +{ + switch (effIndex) + { + default: + case 0: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF0); + case 1: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF1); + case 2: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF2); + } +} + +bool SpellInfo::IsChanneled() const +{ + return HasAttribute(SpellAttr1(SPELL_ATTR1_IS_CHANNELLED | SPELL_ATTR1_IS_SELF_CHANNELLED)); +} + +bool SpellInfo::IsMoveAllowedChannel() const +{ + return IsChanneled() && !ChannelInterruptFlags.HasFlag(SpellAuraInterruptFlags::Moving | SpellAuraInterruptFlags::Turning); +} + +bool SpellInfo::NeedsComboPoints() const +{ + return HasAttribute(SpellAttr1(SPELL_ATTR1_FINISHING_MOVE_DAMAGE | SPELL_ATTR1_FINISHING_MOVE_DURATION)); +} + +bool SpellInfo::IsNextMeleeSwingSpell() const +{ + return HasAttribute(SpellAttr0(SPELL_ATTR0_ON_NEXT_SWING_NO_DAMAGE | SPELL_ATTR0_ON_NEXT_SWING)); +} + +bool SpellInfo::IsRangedWeaponSpell() const +{ + return (EquippedItemClass == ITEM_CLASS_WEAPON) && (EquippedItemSubClassMask & ITEM_SUBCLASS_MASK_WEAPON_RANGED); +} + +bool SpellInfo::IsAutoRepeatRangedSpell() const +{ + return HasAttribute(SPELL_ATTR2_AUTO_REPEAT); +} + +bool SpellInfo::CausesInitialThreat() const +{ + return !HasAttribute(SPELL_ATTR1_NO_THREAT) && !HasAttribute(SPELL_ATTR2_NO_INITIAL_THREAT) && !HasAttribute(SPELL_ATTR4_NO_HARMFUL_THREAT); +} + +WeaponAttackType SpellInfo::GetAttackType() const +{ + WeaponAttackType result; + switch (DmgClass) + { + case SPELL_DAMAGE_CLASS_MELEE: + if (HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + result = OFF_ATTACK; + else + result = BASE_ATTACK; + break; + case SPELL_DAMAGE_CLASS_RANGED: + result = IsRangedWeaponSpell() ? RANGED_ATTACK : BASE_ATTACK; + break; + default: + // Wands + if (IsAutoRepeatRangedSpell()) + result = RANGED_ATTACK; + else + result = BASE_ATTACK; + break; + } + + return result; +} + +bool SpellInfo::IsAffected(uint32 familyName, flag96 const& familyFlags) const +{ + if (!familyName) + return true; + + if (familyName != SpellFamilyName) + return false; + + if (familyFlags && !(familyFlags & SpellFamilyFlags)) + return false; + + return true; +} + +bool SpellInfo::IsAffectedBySpellMods() const +{ + return !HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS); +} + +bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod) const +{ + if (!IsAffectedBySpellMods()) + return false; + + SpellInfo const* affectSpell = sSpellMgr->GetSpellInfo(mod->spellId); + if (!affectSpell) + return false; + + switch (mod->type) + { + case SPELLMOD_FLAT: + case SPELLMOD_PCT: + // TEMP: dont use IsAffected - !familyName and !familyFlags are not valid options for spell mods + // TODO: investigate if the !familyName and !familyFlags conditions are even valid for all other (nonmod) uses of SpellInfo::IsAffected + return affectSpell->SpellFamilyName == SpellFamilyName && static_cast(mod)->mask & SpellFamilyFlags; + default: + break; + } + + return false; +} + +bool SpellInfo::CanPierceImmuneAura(SpellInfo const* auraSpellInfo) const +{ + // aura can't be pierced + if (!auraSpellInfo || auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + // these spells pierce all available spells (Resurrection Sickness for example) + if (HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return true; + + // these spells (Cyclone for example) can pierce all... + if (HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) || HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + // ...but not these (Divine shield, Ice block, Cyclone and Banish for example) + if (auraSpellInfo->Mechanic != MECHANIC_IMMUNE_SHIELD && + auraSpellInfo->Mechanic != MECHANIC_INVULNERABILITY && + (auraSpellInfo->Mechanic != MECHANIC_BANISH || (IsRankOf(auraSpellInfo) && auraSpellInfo->Dispel != DISPEL_NONE))) // Banish shouldn't be immune to itself, but Cyclone should + return true; + } + + // Dispels other auras on immunity, check if this spell makes the unit immune to aura + if (HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT) && CanSpellProvideImmunityAgainstAura(auraSpellInfo)) + return true; + + return false; +} + +bool SpellInfo::CanDispelAura(SpellInfo const* auraSpellInfo) const +{ + // These auras (like Divine Shield) can't be dispelled + if (auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + // These spells (like Mass Dispel) can dispel all auras + if (HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return true; + + // These auras (Cyclone for example) are not dispelable + if ((auraSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) && auraSpellInfo->Mechanic != MECHANIC_NONE) + || auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + return false; + + return true; +} + +bool SpellInfo::IsSingleTarget() const +{ + // all other single target spells have if it has AttributesEx5 + if (HasAttribute(SPELL_ATTR5_LIMIT_N)) + return true; + + return false; +} + +bool SpellInfo::IsAuraExclusiveBySpecificWith(SpellInfo const* spellInfo) const +{ + SpellSpecificType spellSpec1 = GetSpellSpecific(); + SpellSpecificType spellSpec2 = spellInfo->GetSpellSpecific(); + switch (spellSpec1) + { + case SPELL_SPECIFIC_WARLOCK_ARMOR: + case SPELL_SPECIFIC_MAGE_ARMOR: + case SPELL_SPECIFIC_ELEMENTAL_SHIELD: + case SPELL_SPECIFIC_MAGE_POLYMORPH: + case SPELL_SPECIFIC_PRESENCE: + case SPELL_SPECIFIC_CHARM: + case SPELL_SPECIFIC_SCROLL: + case SPELL_SPECIFIC_WARRIOR_ENRAGE: + case SPELL_SPECIFIC_MAGE_ARCANE_BRILLANCE: + case SPELL_SPECIFIC_PRIEST_DIVINE_SPIRIT: + return spellSpec1 == spellSpec2; + case SPELL_SPECIFIC_FOOD: + return spellSpec2 == SPELL_SPECIFIC_FOOD + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + case SPELL_SPECIFIC_DRINK: + return spellSpec2 == SPELL_SPECIFIC_DRINK + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + case SPELL_SPECIFIC_FOOD_AND_DRINK: + return spellSpec2 == SPELL_SPECIFIC_FOOD + || spellSpec2 == SPELL_SPECIFIC_DRINK + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + default: + return false; + } +} + +bool SpellInfo::IsAuraExclusiveBySpecificPerCasterWith(SpellInfo const* spellInfo) const +{ + SpellSpecificType spellSpec = GetSpellSpecific(); + switch (spellSpec) + { + case SPELL_SPECIFIC_SEAL: + case SPELL_SPECIFIC_HAND: + case SPELL_SPECIFIC_AURA: + case SPELL_SPECIFIC_STING: + case SPELL_SPECIFIC_CURSE: + case SPELL_SPECIFIC_BANE: + case SPELL_SPECIFIC_ASPECT: + case SPELL_SPECIFIC_WARLOCK_CORRUPTION: + return spellSpec == spellInfo->GetSpellSpecific(); + default: + return false; + } +} + +SpellCastResult SpellInfo::CheckShapeshift(uint32 form) const +{ + // talents that learn spells can have stance requirements that need ignore + // (this requirement only for client-side stance show in talent description) + if (sDBCManager.GetTalentSpellCost(Id) > 0 && + (Effects[0].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[1].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[2].Effect == SPELL_EFFECT_LEARN_SPELL)) + return SPELL_CAST_OK; + + uint64 stanceMask = (form ? UI64LIT(1) << (form - 1) : 0); + + if (stanceMask & StancesNot) // can explicitly not be cast in this stance + return SPELL_FAILED_NOT_SHAPESHIFT; + + if (stanceMask & Stances) // can explicitly be cast in this stance + return SPELL_CAST_OK; + + bool actAsShifted = false; + SpellShapeshiftFormEntry const* shapeInfo = nullptr; + if (form > 0) + { + shapeInfo = sSpellShapeshiftFormStore.LookupEntry(form); + if (!shapeInfo) + { + TC_LOG_ERROR("spells", "GetErrorAtShapeshiftedCast: unknown shapeshift %u", form); + return SPELL_CAST_OK; + } + actAsShifted = !shapeInfo->GetFlags().HasFlag(SpellShapeshiftFormFlags::Stance); + } + + if (actAsShifted) + { + if (HasAttribute(SPELL_ATTR0_NOT_SHAPESHIFTED) || (shapeInfo && shapeInfo->GetFlags().HasFlag(SpellShapeshiftFormFlags::CanOnlyCastShapeshiftSpells))) // not while shapeshifted + return SPELL_FAILED_NOT_SHAPESHIFT; + else if (Stances != 0) // needs other shapeshift + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + else + { + // needs shapeshift + if (!HasAttribute(SPELL_ATTR2_ALLOW_WHILE_NOT_SHAPESHIFTED_CASTER_FORM) && Stances != 0) + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + + // Check if stance disables cast of not-stance spells + // Example: cannot cast any other spells in zombie or ghoul form + /// @todo Find a way to disable use of these spells clientside + if (shapeInfo && shapeInfo->Flags & 0x400) + { + if (!(stanceMask & Stances)) + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckLocation(uint32 map_id, uint32 zone_id, uint32 area_id, Player const* player) const +{ + // normal case + if (AreaGroupId > 0) + { + bool found = false; + AreaGroupEntry const* groupEntry = sAreaGroupStore.LookupEntry(AreaGroupId); + while (groupEntry) + { + for (uint8 i = 0; i < MAX_GROUP_AREA_IDS; ++i) + if (DBCManager::IsInArea(area_id, groupEntry->AreaID[i])) + found = true; + + if (found || !groupEntry->NextAreaID) + break; + // Try search in next group + groupEntry = sAreaGroupStore.LookupEntry(groupEntry->NextAreaID); + } + + if (!found) + return SPELL_FAILED_INCORRECT_AREA; + } + + // continent limitation (virtual continent) + if (HasAttribute(SPELL_ATTR4_ONLY_FLYING_AREAS)) + { + EnumFlag mountFlags = AreaMountFlags::None; + if (player && player->HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS)) + { + for (AuraEffect const* auraEffect : player->GetAuraEffectsByType(SPELL_AURA_MOUNT_RESTRICTIONS)) + mountFlags |= AreaMountFlags(auraEffect->GetMiscValue()); + } + else if (AreaTableEntry const* areaTable = sAreaTableStore.LookupEntry(area_id)) + mountFlags = areaTable->GetMountFlags(); + + if (!(mountFlags.HasFlag(AreaMountFlags::AllowFlyingMounts))) + return SPELL_FAILED_INCORRECT_AREA; + } + + // raid instance limitation + if (HasAttribute(SPELL_ATTR6_NOT_IN_RAID_INSTANCES)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || mapEntry->IsRaid()) + return SPELL_FAILED_NOT_IN_RAID_INSTANCE; + } + + if (HasAttribute(SPELL_ATTR8_REMOVE_OUTSIDE_DUNGEONS_AND_RAIDS)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || !mapEntry->IsDungeon()) + return SPELL_FAILED_TARGET_NOT_IN_INSTANCE; + } + + if (HasAttribute(SPELL_ATTR8_NOT_IN_BATTLEGROUND)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || mapEntry->IsBattleground()) + return SPELL_FAILED_NOT_IN_BATTLEGROUND; + } + + // DB base check (if non empty then must fit at least single for allow) + SpellAreaMapBounds saBounds = sSpellMgr->GetSpellAreaMapBounds(Id); + if (saBounds.first != saBounds.second) + { + for (SpellAreaMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) + { + if (itr->second.IsFitToRequirements(player, zone_id, area_id)) + return SPELL_CAST_OK; + } + return SPELL_FAILED_INCORRECT_AREA; + } + + // bg spell checks + switch (Id) + { + case 23333: // Horde Flag + case 23335: // Alliance Flag + return (map_id == 489 || map_id == 726) && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + case 34976: // Netherstorm Flag + return map_id == 566 && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + case 2584: // Waiting to Resurrect + case 22011: // Spirit Heal Channel + case 22012: // Spirit Heal + case 24171: // Resurrection Impact Visual + case 42792: // Recently Dropped Flag + case 43681: // Inactive + case 44535: // Spirit Heal (mana) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + return zone_id == AREA_WINTERGRASP || (mapEntry->IsBattleground() && player && player->InBattleground()) ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 44521: // Preparation + { + if (!player) + return SPELL_FAILED_REQUIRES_AREA; + + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + if (!mapEntry->IsBattleground()) + return SPELL_FAILED_REQUIRES_AREA; + + Battleground* bg = player->GetBattleground(); + return bg && bg->GetStatus() == STATUS_WAIT_JOIN ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 32724: // Gold Team (Alliance) + case 32725: // Green Team (Alliance) + case 35774: // Gold Team (Horde) + case 35775: // Green Team (Horde) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + return mapEntry->IsBattleArena() && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 32727: // Arena Preparation + { + if (!player) + return SPELL_FAILED_REQUIRES_AREA; + + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + if (!mapEntry->IsBattleArena()) + return SPELL_FAILED_REQUIRES_AREA; + + Battleground* bg = player->GetBattleground(); + return bg && bg->GetStatus() == STATUS_WAIT_JOIN ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + } + + // aura limitations + if (player) + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsAura()) + continue; + + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_SHAPESHIFT: + { + if (SpellShapeshiftFormEntry const* spellShapeshiftForm = sSpellShapeshiftFormStore.LookupEntry(Effects[i].MiscValue)) + if (uint32 mountType = spellShapeshiftForm->MountTypeID) + if (!player->GetMountCapability(mountType)) + return SPELL_FAILED_NOT_HERE; + break; + } + case SPELL_AURA_MOUNTED: + { + if (Effects[i].MiscValueB && !player->GetMountCapability(Effects[i].MiscValueB)) + return SPELL_FAILED_NOT_HERE; + break; + } + break; + } + } + } + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckTarget(WorldObject const* caster, WorldObject const* target, bool implicit /*= true*/) const +{ + if (HasAttribute(SPELL_ATTR1_EXCLUDE_CASTER) && caster == target) + return SPELL_FAILED_BAD_TARGETS; + + // check visibility - ignore stealth for implicit (area) targets + if (!HasAttribute(SPELL_ATTR6_IGNORE_PHASE_SHIFT) && !caster->CanSeeOrDetect(target, implicit)) + return SPELL_FAILED_BAD_TARGETS; + + Unit const* unitTarget = target->ToUnit(); + + /* + * @todo: 4.x - old hold due to an upcoming tempsummon rework + if (HasAttribute(SPELL_ATTR8_ONLY_TARGET_IF_SAME_CREATOR)) + if (caster != target && caster->GetGUID() != target->GetOwnerGUID()) + return SPELL_FAILED_BAD_TARGETS; + */ + + // creature/player specific target checks + if (unitTarget) + { + // spells cannot be cast if target has a pet in combat either + if (HasAttribute(SPELL_ATTR1_ONLY_PEACEFUL_TARGETS) && (unitTarget->IsInCombat() || unitTarget->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT))) + return SPELL_FAILED_TARGET_AFFECTING_COMBAT; + + // only spells with SPELL_ATTR3_ONLY_TARGET_GHOSTS can target ghosts + if (HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS) != unitTarget->HasAuraType(SPELL_AURA_GHOST)) + { + if (HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS)) + return SPELL_FAILED_TARGET_NOT_GHOST; + else + return SPELL_FAILED_BAD_TARGETS; + } + + if (caster != unitTarget) + { + if (caster->GetTypeId() == TYPEID_PLAYER) + { + // Do not allow these spells to target creatures not tapped by us (Banish, Polymorph, many quest spells) + if (HasAttribute(SPELL_ATTR2_CANNOT_CAST_ON_TAPPED)) + if (Creature const* targetCreature = unitTarget->ToCreature()) + if (targetCreature->hasLootRecipient() && !targetCreature->isTappedBy(caster->ToPlayer())) + return SPELL_FAILED_CANT_CAST_ON_TAPPED; + + if (HasAttribute(SPELL_ATTR0_CU_PICKPOCKET)) + { + Creature const* targetCreature = unitTarget->ToCreature(); + if (!targetCreature) + return SPELL_FAILED_BAD_TARGETS; + + if (!LootTemplates_Pickpocketing.HaveLootFor(targetCreature->GetCreatureTemplate()->pickpocketLootId)) + return SPELL_FAILED_TARGET_NO_POCKETS; + } + + // Not allow disarm unarmed player + if (Mechanic == MECHANIC_DISARM) + { + if (unitTarget->GetTypeId() == TYPEID_PLAYER) + { + Player const* player = unitTarget->ToPlayer(); + if (!player->GetWeaponForAttack(BASE_ATTACK) || !player->IsUseEquipedWeapon(true)) + return SPELL_FAILED_TARGET_NO_WEAPONS; + } + else if (!unitTarget->GetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID)) + return SPELL_FAILED_TARGET_NO_WEAPONS; + } + } + } + + if (HasAttribute(SPELL_ATTR8_ONLY_TARGET_OWN_SUMMONS)) + if (!unitTarget->IsSummon() || unitTarget->ToTempSummon()->GetSummonerGUID() != caster->GetGUID()) + return SPELL_FAILED_BAD_TARGETS; + } + // corpse specific target checks + else if (Corpse const* corpseTarget = target->ToCorpse()) + { + // cannot target bare bones + if (corpseTarget->GetType() == CORPSE_BONES) + return SPELL_FAILED_BAD_TARGETS; + // we have to use owner for some checks (aura preventing resurrection for example) + if (Player* owner = ObjectAccessor::FindPlayer(corpseTarget->GetOwnerGUID())) + unitTarget = owner; + // we're not interested in corpses without owner + else + return SPELL_FAILED_BAD_TARGETS; + } + // other types of objects - always valid + else return SPELL_CAST_OK; + + // corpseOwner and unit specific target checks + if (!unitTarget->IsPlayer()) + { + if (HasAttribute(SPELL_ATTR3_ONLY_ON_PLAYER)) + return SPELL_FAILED_TARGET_NOT_PLAYER; + + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) && unitTarget->IsControlledByPlayer()) + return SPELL_FAILED_TARGET_IS_PLAYER_CONTROLLED; + } + else if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER)) + return SPELL_FAILED_TARGET_IS_PLAYER; + + if (!IsAllowingDeadTarget() && !unitTarget->IsAlive()) + return SPELL_FAILED_TARGETS_DEAD; + + // check this flag only for implicit targets (chain and area), allow to explicitly target units for spells like Shield of Righteousness + if (implicit && HasAttribute(SPELL_ATTR6_DO_NOT_CHAIN_TO_CROWD_CONTROLLED_TARGETS) && !unitTarget->CanFreeMove()) + return SPELL_FAILED_BAD_TARGETS; + + // Do not allow pet or guardian targets if the spell is a raid buff or may not target pets at all + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) && unitTarget->IsCreature() && unitTarget->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + return SPELL_FAILED_BAD_TARGETS; + + // checked in Unit::IsValidAttack/AssistTarget, shouldn't be checked for ENTRY targets + //if (!HasAttribute(SPELL_ATTR6_CAN_TARGET_UNTARGETABLE) && target->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)) + // return SPELL_FAILED_BAD_TARGETS; + + if (!CheckTargetCreatureType(unitTarget)) + { + if (target->GetTypeId() == TYPEID_PLAYER) + return SPELL_FAILED_TARGET_IS_PLAYER; + else + return SPELL_FAILED_BAD_TARGETS; + } + + // check GM mode and GM invisibility - only for player casts (npc casts are controlled by AI) and negative spells + if (unitTarget != caster && (caster->GetAffectingPlayer() || !IsPositive()) && unitTarget->GetTypeId() == TYPEID_PLAYER) + { + if (!unitTarget->ToPlayer()->IsVisible()) + return SPELL_FAILED_BM_OR_INVISGOD; + + if (unitTarget->ToPlayer()->IsGameMaster()) + return SPELL_FAILED_BM_OR_INVISGOD; + } + + // not allow casting on flying player + if (unitTarget->HasUnitState(UNIT_STATE_IN_FLIGHT) && !HasAttribute(SPELL_ATTR0_CU_ALLOW_INFLIGHT_TARGET)) + return SPELL_FAILED_BAD_TARGETS; + + /* TARGET_UNIT_MASTER gets blocked here for passengers, because the whole idea of this check is to + not allow passengers to be implicitly hit by spells, however this target type should be an exception, + if this is left it kills spells that award kill credit from vehicle to master (few spells), + the use of these 2 covers passenger target check, logically, if vehicle cast this to master it should always hit + him, because it would be it's passenger, there's no such case where this gets to fail legitimacy, this problem + cannot be solved from within the check in other way since target type cannot be called for the spell currently + Spell examples: [ID - 52864 Devour Water, ID - 52862 Devour Wind, ID - 49370 Wyrmrest Defender: Destabilize Azure Dragonshrine Effect] */ + if (Unit const* unitCaster = caster->ToUnit()) + { + if (!unitCaster->IsVehicle() && !(unitCaster->GetCharmerOrOwner() == target)) + { + if (TargetAuraState && !unitTarget->HasAuraState(AuraStateType(TargetAuraState), this, unitCaster)) + return SPELL_FAILED_TARGET_AURASTATE; + + if (TargetAuraStateNot && unitTarget->HasAuraState(AuraStateType(TargetAuraStateNot), this, unitCaster)) + return SPELL_FAILED_TARGET_AURASTATE; + } + } + + if (TargetAuraSpell && !unitTarget->HasAura(sSpellMgr->GetSpellIdForDifficulty(TargetAuraSpell, caster))) + return SPELL_FAILED_TARGET_AURASTATE; + + if (ExcludeTargetAuraSpell && unitTarget->HasAura(sSpellMgr->GetSpellIdForDifficulty(ExcludeTargetAuraSpell, caster))) + return SPELL_FAILED_TARGET_AURASTATE; + + if (unitTarget->HasAuraType(SPELL_AURA_PREVENT_RESURRECTION)) + if (HasEffect(SPELL_EFFECT_SELF_RESURRECT) || HasEffect(SPELL_EFFECT_RESURRECT) || HasEffect(SPELL_EFFECT_RESURRECT_NEW)) + return SPELL_FAILED_TARGET_CANNOT_BE_RESURRECTED; + + if (!unitTarget->CheckPrivateObjectOwnerVisibility(caster)) + return SPELL_FAILED_BAD_TARGETS; + + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckExplicitTarget(WorldObject const* caster, WorldObject const* target, Item const* itemTarget) const +{ + uint32 neededTargets = GetExplicitTargetMask(); + if (!target) + { + if (neededTargets & (TARGET_FLAG_UNIT_MASK | TARGET_FLAG_GAMEOBJECT_MASK | TARGET_FLAG_CORPSE_MASK)) + if (!(neededTargets & TARGET_FLAG_GAMEOBJECT_ITEM) || !itemTarget) + return SPELL_FAILED_BAD_TARGETS; + return SPELL_CAST_OK; + } + + if (Unit const* unitTarget = target->ToUnit()) + { + if (neededTargets & (TARGET_FLAG_UNIT_ENEMY | TARGET_FLAG_UNIT_ALLY | TARGET_FLAG_UNIT_RAID | TARGET_FLAG_UNIT_PARTY | TARGET_FLAG_UNIT_MINIPET | TARGET_FLAG_UNIT_PASSENGER)) + { + Unit const* unitCaster = caster->ToUnit(); + if (neededTargets & TARGET_FLAG_UNIT_ENEMY) + if (caster->IsValidAttackTarget(unitTarget, this)) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_ALLY) + || ((neededTargets & TARGET_FLAG_UNIT_PARTY) && unitCaster && unitCaster->IsInPartyWith(unitTarget)) + || ((neededTargets & TARGET_FLAG_UNIT_RAID) && unitCaster && unitCaster->IsInRaidWith(unitTarget))) + if (caster->IsValidAssistTarget(unitTarget, this)) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_MINIPET) && unitCaster) + if (unitTarget->GetGUID() == unitCaster->GetCritterGUID()) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_PASSENGER) && unitCaster) + if (unitTarget->IsOnVehicle(unitCaster)) + return SPELL_CAST_OK; + return SPELL_FAILED_BAD_TARGETS; + } + } + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckVehicle(Unit const* caster) const +{ + // All creatures should be able to cast as passengers freely, restriction and attribute are only for players + if (caster->GetTypeId() != TYPEID_PLAYER) + return SPELL_CAST_OK; + + Vehicle* vehicle = caster->GetVehicle(); + if (vehicle) + { + uint16 checkMask = 0; + for (uint8 effIndex = EFFECT_0; effIndex < MAX_SPELL_EFFECTS; ++effIndex) + { + if (Effects[effIndex].ApplyAuraName == SPELL_AURA_MOD_SHAPESHIFT) + { + SpellShapeshiftFormEntry const* shapeShiftFromEntry = sSpellShapeshiftFormStore.LookupEntry(Effects[effIndex].MiscValue); + if (shapeShiftFromEntry && (shapeShiftFromEntry->Flags & 1) == 0) // unk flag + checkMask |= VEHICLE_SEAT_FLAG_UNCONTROLLED; + break; + } + } + + if (HasAura(SPELL_AURA_MOUNTED)) + checkMask |= VEHICLE_SEAT_FLAG_CAN_CAST_MOUNT_SPELL; + + if (!checkMask) + checkMask = VEHICLE_SEAT_FLAG_CAN_ATTACK; + + VehicleSeatEntry const* vehicleSeat = vehicle->GetSeatForPassenger(caster); + if (!vehicleSeat) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + + if (!HasAttribute(SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE) && !HasAttribute(SPELL_ATTR0_ALLOW_WHILE_MOUNTED) + && (vehicleSeat->Flags & checkMask) != checkMask) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + + // Can only summon uncontrolled minions/guardians when on controlled vehicle + if (vehicleSeat->Flags & (VEHICLE_SEAT_FLAG_CAN_CONTROL | VEHICLE_SEAT_FLAG_UNK2)) + { + for (uint32 i = EFFECT_0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect != SPELL_EFFECT_SUMMON) + continue; + + SummonPropertiesEntry const* props = sSummonPropertiesStore.LookupEntry(Effects[i].MiscValueB); + if (props && props->Control != SUMMON_CATEGORY_WILD) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + } + } + } + + return SPELL_CAST_OK; +} + +bool SpellInfo::CheckTargetCreatureType(Unit const* target) const +{ + // Curse of Doom & Exorcism: not find another way to fix spell target check :/ + if (SpellFamilyName == SPELLFAMILY_WARLOCK && GetCategory() == 1179) + { + // not allow cast at player + if (target->GetTypeId() == TYPEID_PLAYER) + return false; + else + return true; + } + + // if target is magnet (i.e Grounding Totem) the check is skipped + if (target->IsMagnet()) + return true; + + uint32 creatureType = target->GetCreatureTypeMask(); + return !TargetCreatureType || !creatureType || (creatureType & TargetCreatureType); +} + +SpellSchoolMask SpellInfo::GetSchoolMask() const +{ + return SpellSchoolMask(SchoolMask); +} + +uint32 SpellInfo::GetAllEffectsMechanicMask() const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].Mechanic) + mask |= 1 << Effects[i].Mechanic; + return mask; +} + +uint32 SpellInfo::GetEffectMechanicMask(uint8 effIndex) const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + if (Effects[effIndex].IsEffect() && Effects[effIndex].Mechanic) + mask |= 1 << Effects[effIndex].Mechanic; + return mask; +} + +uint32 SpellInfo::GetSpellMechanicMaskByEffectMask(uint32 effectMask) const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if ((effectMask & (1 << i)) && Effects[i].Mechanic) + mask |= 1 << Effects[i].Mechanic; + return mask; +} + +Mechanics SpellInfo::GetEffectMechanic(uint8 effIndex) const +{ + if (Effects[effIndex].IsEffect() && Effects[effIndex].Mechanic) + return Mechanics(Effects[effIndex].Mechanic); + if (Mechanic) + return Mechanics(Mechanic); + return MECHANIC_NONE; +} + +bool SpellInfo::HasAnyEffectMechanic() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Mechanic) + return true; + return false; +} + +uint32 SpellInfo::GetDispelMask() const +{ + return GetDispelMask(DispelType(Dispel)); +} + +uint32 SpellInfo::GetDispelMask(DispelType type) +{ + // If dispel all + if (type == DISPEL_ALL) + return DISPEL_ALL_MASK; + else + return uint32(1 << type); +} + +uint32 SpellInfo::GetExplicitTargetMask() const +{ + return ExplicitTargetMask; +} + +AuraStateType SpellInfo::GetAuraState() const +{ + return _auraState; +} + +void SpellInfo::_LoadAuraState() +{ + _auraState = [this]() -> AuraStateType + { + // Seals + if (GetSpellSpecific() == SPELL_SPECIFIC_SEAL) + return AURA_STATE_JUDGEMENT; + + // Conflagrate aura state on Immolate and Shadowflame + if (SpellFamilyName == SPELLFAMILY_WARLOCK && + // Immolate + ((SpellFamilyFlags[0] & 4) || + // Shadowflame + (SpellFamilyFlags[2] & 2))) + return AURA_STATE_CONFLAGRATE; + + // Faerie Fire (druid versions) + if (SpellFamilyName == SPELLFAMILY_DRUID && SpellFamilyFlags[0] & 0x400) + return AURA_STATE_FAERIE_FIRE; + + // Sting (hunter's pet ability) + if (GetCategory() == 1133) + return AURA_STATE_FAERIE_FIRE; + + // Victorious + if (SpellFamilyName == SPELLFAMILY_WARRIOR && SpellFamilyFlags[1] & 0x00040000) + return AURA_STATE_WARRIOR_VICTORY_RUSH; + + // Swiftmend state on Regrowth & Rejuvenation + if (SpellFamilyName == SPELLFAMILY_DRUID && SpellFamilyFlags[0] & 0x50) + return AURA_STATE_SWIFTMEND; + + // Deadly poison aura state + if (SpellFamilyName == SPELLFAMILY_ROGUE && SpellFamilyFlags[0] & 0x10000) + return AURA_STATE_DEADLY_POISON; + + // Enrage aura state + if (Dispel == DISPEL_ENRAGE) + return AURA_STATE_ENRAGE; + + // Bleeding aura state + if (GetAllEffectsMechanicMask() & 1 << MECHANIC_BLEED) + return AURA_STATE_BLEEDING; + + if (GetSchoolMask() & SPELL_SCHOOL_MASK_FROST) + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAura() && (Effects[i].ApplyAuraName == SPELL_AURA_MOD_STUN + || Effects[i].ApplyAuraName == SPELL_AURA_MOD_ROOT)) + return AURA_STATE_FROZEN; + + switch (Id) + { + case 71465: // Divine Surge + case 50241: // Evasive Charges + return AURA_STATE_UNKNOWN22; + case 9991: // Touch of Zanzil + case 35325: // Glowing Blood + case 35328: // Lambent Blood + case 35329: // Vibrant Blood + case 35331: // Black Blood + case 49163: // Perpetual Instability + return AURA_STATE_FAERIE_FIRE; + default: + break; + } + + return AURA_STATE_NONE; + }(); +} + +SpellSpecificType SpellInfo::GetSpellSpecific() const +{ + return _spellSpecific; +} + +void SpellInfo::_LoadSpellSpecific() +{ + _spellSpecific = [this]()->SpellSpecificType + { + switch (SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + { + // Food / Drinks (mostly) + if (HasAuraInterruptFlag(SpellAuraInterruptFlags::Standing)) + { + bool food = false; + bool drink = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsAura()) + continue; + switch (Effects[i].ApplyAuraName) + { + // Food + case SPELL_AURA_MOD_REGEN: + case SPELL_AURA_OBS_MOD_HEALTH: + food = true; + break; + // Drink + case SPELL_AURA_MOD_POWER_REGEN: + case SPELL_AURA_OBS_MOD_POWER: + drink = true; + break; + default: + break; + } + } + + if (food && drink) + return SPELL_SPECIFIC_FOOD_AND_DRINK; + else if (food) + return SPELL_SPECIFIC_FOOD; + else if (drink) + return SPELL_SPECIFIC_DRINK; + } + // scrolls effects + else + { + SpellInfo const* firstRankSpellInfo = GetFirstRankSpell(); + switch (firstRankSpellInfo->Id) + { + case 8118: // Strength + case 8099: // Stamina + case 8112: // Spirit + case 8096: // Intellect + case 8115: // Agility + case 8091: // Armor + return SPELL_SPECIFIC_SCROLL; + case 12880: // Enrage (Enrage) + case 57518: // Enrage (Wrecking Crew) + return SPELL_SPECIFIC_WARRIOR_ENRAGE; + } + } + break; + } + case SPELLFAMILY_MAGE: + { + // family flags 18(Molten), 25(Frost/Ice), 28(Mage) + if (SpellFamilyFlags[0] & 0x12040000) + return SPELL_SPECIFIC_MAGE_ARMOR; + + // Arcane brillance and Arcane intelect (normal check fails because of flags difference) + if (SpellFamilyFlags[0] & 0x400) + return SPELL_SPECIFIC_MAGE_ARCANE_BRILLANCE; + + if ((SpellFamilyFlags[0] & 0x1000000) && Effects[0].ApplyAuraName == SPELL_AURA_MOD_CONFUSE) + return SPELL_SPECIFIC_MAGE_POLYMORPH; + + break; + } + case SPELLFAMILY_WARRIOR: + { + if (Id == 12292) // Death Wish + return SPELL_SPECIFIC_WARRIOR_ENRAGE; + + break; + } + case SPELLFAMILY_WARLOCK: + { + // Warlock (Bane of Doom | Bane of Agony | Bane of Havoc) + if (Id == 603 || Id == 980 || Id == 80240) + return SPELL_SPECIFIC_BANE; + + // only warlock curses have this + if (Dispel == DISPEL_CURSE) + return SPELL_SPECIFIC_CURSE; + + // Warlock (Demon Armor | Demon Skin | Fel Armor) + if (SpellFamilyFlags[1] & 0x20000020 || SpellFamilyFlags[2] & 0x00000010) + return SPELL_SPECIFIC_WARLOCK_ARMOR; + + //seed of corruption and corruption + if (SpellFamilyFlags[1] & 0x10 || SpellFamilyFlags[0] & 0x2) + return SPELL_SPECIFIC_WARLOCK_CORRUPTION; + break; + } + case SPELLFAMILY_PRIEST: + { + // Divine Spirit and Prayer of Spirit + if (SpellFamilyFlags[0] & 0x20) + return SPELL_SPECIFIC_PRIEST_DIVINE_SPIRIT; + + break; + } + case SPELLFAMILY_HUNTER: + { + // only hunter stings have this + if (Dispel == DISPEL_POISON) + return SPELL_SPECIFIC_STING; + + // only hunter aspects have this (but not all aspects in hunter family) + if (SpellFamilyFlags.HasFlag(0x00380000, 0x00440000, 0x00001010)) + return SPELL_SPECIFIC_ASPECT; + + break; + } + case SPELLFAMILY_PALADIN: + { + // Collection of all the seal family flags. No other paladin spell has any of those. + if (SpellFamilyFlags[1] & 0xA2000800) + return SPELL_SPECIFIC_SEAL; + + if (SpellFamilyFlags[0] & 0x00002190) + return SPELL_SPECIFIC_HAND; + + // only paladin auras have this (for palaldin class family) + if (SpellFamilyFlags[2] & 0x00000020) + return SPELL_SPECIFIC_AURA; + + break; + } + case SPELLFAMILY_SHAMAN: + { + // family flags 10 (Lightning), 42 (Earth), 37 (Water), proc shield from T2 8 pieces bonus + if (SpellFamilyFlags[1] & 0x420 + || SpellFamilyFlags[0] & 0x00000400 + || Id == 23552) + return SPELL_SPECIFIC_ELEMENTAL_SHIELD; + + break; + } + case SPELLFAMILY_DEATHKNIGHT: + if (Id == 48266 || Id == 48263 || Id == 48265) + return SPELL_SPECIFIC_PRESENCE; + break; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA) + { + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_CHARM: + case SPELL_AURA_MOD_POSSESS: + case SPELL_AURA_AOE_CHARM: + return SPELL_SPECIFIC_CHARM; + case SPELL_AURA_TRACK_CREATURES: + /// @workaround For non-stacking tracking spells (We need generic solution) + if (Id == 30645) // Gas Cloud Tracking + return SPELL_SPECIFIC_NORMAL; + [[fallthrough]]; + case SPELL_AURA_TRACK_RESOURCES: + case SPELL_AURA_TRACK_STEALTHED: + return SPELL_SPECIFIC_TRACKER; + } + } + } + + return SPELL_SPECIFIC_NORMAL; + }(); +} + +void SpellInfo::_LoadSpellDiminishInfo() +{ + auto diminishingGroupCompute = [this](bool triggered) -> DiminishingGroup + { + if (IsPositive()) + return DIMINISHING_NONE; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].ApplyAuraName == SPELL_AURA_MOD_TAUNT) + return DIMINISHING_TAUNT; + } + + // Explicit Diminishing Groups + switch (SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + { + // Pet charge effects (Infernal Awakening, Demon Charge) + if (SpellVisual[0] == 2816 && SpellIconID == 15) + return DIMINISHING_CONTROLLED_STUN; + // Frost Tomb + else if (Id == 48400) + return DIMINISHING_NONE; + // Gnaw + else if (Id == 47481) + return DIMINISHING_CONTROLLED_STUN; + // ToC Icehowl Arctic Breath + else if (SpellVisual[0] == 14153) + return DIMINISHING_NONE; + // Black Plague + else if (Id == 64155) + return DIMINISHING_NONE; + // Screams of the Dead (King Ymiron) + else if (Id == 51750) + return DIMINISHING_NONE; + break; + } + // Event spells + case SPELLFAMILY_UNK1: + return DIMINISHING_NONE; + case SPELLFAMILY_MAGE: + { + // Frostbite + if (SpellFamilyFlags[1] & 0x80000000) + return DIMINISHING_ROOT; + // Shattered Barrier + else if (SpellVisual[0] == 12297) + return DIMINISHING_ROOT; + // Deep Freeze + else if (SpellIconID == 2939 && SpellVisual[0] == 9963) + return DIMINISHING_CONTROLLED_STUN; + // Frost Nova / Freeze (Water Elemental) + else if (SpellIconID == 193) + return DIMINISHING_CONTROLLED_ROOT; + // Dragon's Breath + else if (SpellFamilyFlags[0] & 0x800000) + return DIMINISHING_DRAGONS_BREATH; + break; + } + case SPELLFAMILY_WARRIOR: + { + // Hamstring - limit duration to 10s in PvP + if (SpellFamilyFlags[0] & 0x2) + return DIMINISHING_LIMITONLY; + // Charge Stun (own diminishing) + else if (SpellFamilyFlags[0] & 0x01000000) + return DIMINISHING_CHARGE; + break; + } + case SPELLFAMILY_WARLOCK: + { + // Curses/etc + if ((SpellFamilyFlags[0] & 0x80000000) || (SpellFamilyFlags[1] & 0x200)) + return DIMINISHING_LIMITONLY; + // Seduction + else if (SpellFamilyFlags[1] & 0x10000000) + return DIMINISHING_FEAR; + // Sin and Punishment (Priest spell, don't ask) + else if (SpellIconID == 1869) + return DIMINISHING_NONE; + break; + } + case SPELLFAMILY_DRUID: + { + // Pounce - there are two spells which share the same family: the stun effect and the bleed spell. We don't want the bleed to be affected by DR + if ((SpellFamilyFlags[0] & 0x20000) && Id != 9007) + return DIMINISHING_OPENING_STUN; + // Cyclone + else if (SpellFamilyFlags[1] & 0x20) + return DIMINISHING_CYCLONE; + // Entangling Roots + // Nature's Grasp + else if (SpellFamilyFlags[0] & 0x00000200) + return DIMINISHING_CONTROLLED_ROOT; + // Faerie Fire + else if (SpellFamilyFlags[0] & 0x400) + return DIMINISHING_LIMITONLY; + break; + } + case SPELLFAMILY_ROGUE: + { + // Gouge + if (SpellFamilyFlags[0] & 0x8) + return DIMINISHING_DISORIENT; + // Blind + else if (SpellFamilyFlags[0] & 0x1000000) + return DIMINISHING_FEAR; + // Cheap Shot + else if (SpellFamilyFlags[0] & 0x400) + return DIMINISHING_OPENING_STUN; + // Crippling poison - Limit to 10 seconds in PvP (No SpellFamilyFlags) + else if (SpellIconID == 163) + return DIMINISHING_LIMITONLY; + break; + } + case SPELLFAMILY_HUNTER: + { + // Hunter's Mark + if ((SpellFamilyFlags[0] & 0x400) && SpellIconID == 538) + return DIMINISHING_LIMITONLY; + // Scatter Shot (own diminishing) + else if ((SpellFamilyFlags[0] & 0x40000) && SpellIconID == 132) + return DIMINISHING_SCATTER_SHOT; + // Entrapment (own diminishing) + else if (SpellVisual[0] == 7484 && SpellIconID == 20) + return DIMINISHING_ENTRAPMENT; + // Wyvern Sting mechanic is MECHANIC_SLEEP but the diminishing is DIMINISHING_DISORIENT + else if ((SpellFamilyFlags[1] & 0x1000) && SpellIconID == 1721) + return DIMINISHING_DISORIENT; + // Freezing Arrow + else if (SpellFamilyFlags[0] & 0x8) + return DIMINISHING_DISORIENT; + break; + } + case SPELLFAMILY_PALADIN: + { + // Judgement of Justice - limit duration to 10s in PvP + if (SpellFamilyFlags[0] & 0x100000) + return DIMINISHING_LIMITONLY; + // Turn Evil + else if ((SpellFamilyFlags[1] & 0x804000) && SpellIconID == 309) + return DIMINISHING_FEAR; + break; + } + case SPELLFAMILY_SHAMAN: + { + // Storm, Earth and Fire - Earthgrab + if (SpellFamilyFlags[2] & 0x4000) + return DIMINISHING_NONE; + break; + } + case SPELLFAMILY_DEATHKNIGHT: + { + // Hungering Cold (no flags) + if (SpellIconID == 2797) + return DIMINISHING_DISORIENT; + // Mark of Blood + else if ((SpellFamilyFlags[0] & 0x10000000) && SpellIconID == 2285) + return DIMINISHING_LIMITONLY; + break; + } + default: + break; + } + + // Lastly - Set diminishing depending on mechanic + uint32 mechanic = GetAllEffectsMechanicMask(); + if (mechanic & (1 << MECHANIC_CHARM)) + return DIMINISHING_MIND_CONTROL; + if (mechanic & (1 << MECHANIC_SILENCE)) + return DIMINISHING_SILENCE; + if (mechanic & (1 << MECHANIC_SLEEP)) + return DIMINISHING_SLEEP; + if (mechanic & ((1 << MECHANIC_SAPPED) | (1 << MECHANIC_POLYMORPH) | (1 << MECHANIC_SHACKLE))) + return DIMINISHING_DISORIENT; + // Mechanic Knockout, except Blast Wave + if (mechanic & (1 << MECHANIC_KNOCKOUT) && SpellIconID != 292) + return DIMINISHING_DISORIENT; + if (mechanic & (1 << MECHANIC_DISARM)) + return DIMINISHING_DISARM; + if (mechanic & (1 << MECHANIC_FEAR)) + return DIMINISHING_FEAR; + if (mechanic & (1 << MECHANIC_STUN)) + return triggered ? DIMINISHING_STUN : DIMINISHING_CONTROLLED_STUN; + if (mechanic & (1 << MECHANIC_BANISH)) + return DIMINISHING_BANISH; + if (mechanic & (1 << MECHANIC_ROOT)) + return triggered ? DIMINISHING_ROOT : DIMINISHING_CONTROLLED_ROOT; + if (mechanic & (1 << MECHANIC_HORROR)) + return DIMINISHING_HORROR; + + return DIMINISHING_NONE; + }; + + auto diminishingTypeCompute = [](DiminishingGroup group) -> DiminishingReturnsType + { + switch (group) + { + case DIMINISHING_TAUNT: + case DIMINISHING_CONTROLLED_STUN: + case DIMINISHING_STUN: + case DIMINISHING_OPENING_STUN: + case DIMINISHING_CYCLONE: + case DIMINISHING_CHARGE: + return DRTYPE_ALL; + case DIMINISHING_LIMITONLY: + case DIMINISHING_NONE: + return DRTYPE_NONE; + default: + return DRTYPE_PLAYER; + } + }; + + auto diminishingMaxLevelCompute = [](DiminishingGroup group) -> DiminishingLevels + { + switch (group) + { + case DIMINISHING_TAUNT: + return DIMINISHING_LEVEL_TAUNT_IMMUNE; + default: + return DIMINISHING_LEVEL_IMMUNE; + } + }; + + auto diminishingLimitDurationCompute = [this](DiminishingGroup group) -> int32 + { + auto isGroupDurationLimited = [group]() -> bool + { + switch (group) + { + case DIMINISHING_BANISH: + case DIMINISHING_CONTROLLED_STUN: + case DIMINISHING_CONTROLLED_ROOT: + case DIMINISHING_CYCLONE: + case DIMINISHING_DISORIENT: + case DIMINISHING_ENTRAPMENT: + case DIMINISHING_FEAR: + case DIMINISHING_HORROR: + case DIMINISHING_MIND_CONTROL: + case DIMINISHING_OPENING_STUN: + case DIMINISHING_ROOT: + case DIMINISHING_STUN: + case DIMINISHING_SLEEP: + case DIMINISHING_LIMITONLY: + return true; + default: + return false; + } + }; + + if (!isGroupDurationLimited()) + return 0; + + // Explicit diminishing duration + switch (SpellFamilyName) + { + case SPELLFAMILY_DRUID: + { + // Faerie Fire - limit to 40 seconds in PvP (3.1) + if (SpellFamilyFlags[0] & 0x400) + return 40 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_HUNTER: + { + // Wyvern Sting + if (SpellFamilyFlags[1] & 0x1000) + return 6 * IN_MILLISECONDS; + // Hunter's Mark + if (SpellFamilyFlags[0] & 0x400) + return 30 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_PALADIN: + { + // Repentance - limit to 6 seconds in PvP + if (SpellFamilyFlags[0] & 0x4) + return 6 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_WARLOCK: + { + // Banish - limit to 6 seconds in PvP + if (SpellFamilyFlags[1] & 0x8000000) + return 6 * IN_MILLISECONDS; + // Curse of Tongues - limit to 12 seconds in PvP + else if (SpellFamilyFlags[2] & 0x800) + return 12 * IN_MILLISECONDS; + // Curse of Elements - limit to 120 seconds in PvP + else if (SpellFamilyFlags[1] & 0x200) + return 120 * IN_MILLISECONDS; + break; + } + default: + break; + } + + return 8 * IN_MILLISECONDS; + }; + + SpellDiminishInfo triggeredInfo, normalInfo; + triggeredInfo.DiminishGroup = diminishingGroupCompute(true); + triggeredInfo.DiminishReturnType = diminishingTypeCompute(triggeredInfo.DiminishGroup); + triggeredInfo.DiminishMaxLevel = diminishingMaxLevelCompute(triggeredInfo.DiminishGroup); + triggeredInfo.DiminishDurationLimit = diminishingLimitDurationCompute(triggeredInfo.DiminishGroup); + + normalInfo.DiminishGroup = diminishingGroupCompute(false); + normalInfo.DiminishReturnType = diminishingTypeCompute(normalInfo.DiminishGroup); + normalInfo.DiminishMaxLevel = diminishingMaxLevelCompute(normalInfo.DiminishGroup); + normalInfo.DiminishDurationLimit = diminishingLimitDurationCompute(normalInfo.DiminishGroup); + + _diminishInfoTriggered = triggeredInfo; + _diminishInfoNonTriggered = normalInfo; +} + +DiminishingGroup SpellInfo::GetDiminishingReturnsGroupForSpell(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishGroup : _diminishInfoNonTriggered.DiminishGroup; +} + +DiminishingReturnsType SpellInfo::GetDiminishingReturnsGroupType(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishReturnType : _diminishInfoNonTriggered.DiminishReturnType; +} + +DiminishingLevels SpellInfo::GetDiminishingReturnsMaxLevel(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishMaxLevel : _diminishInfoNonTriggered.DiminishMaxLevel; +} + +int32 SpellInfo::GetDiminishingReturnsLimitDuration(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishDurationLimit : _diminishInfoNonTriggered.DiminishDurationLimit; +} + +void SpellInfo::_LoadImmunityInfo() +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + uint32 schoolImmunityMask = 0; + uint32 applyHarmfulAuraImmunityMask = 0; + uint32 mechanicImmunityMask = 0; + uint32 dispelImmunity = 0; + uint32 damageImmunityMask = 0; + + if (!Effects[i].IsEffect()) + continue; + + int32 miscVal = Effects[i].MiscValue; + int32 amount = Effects[i].CalcValue(); + + ImmunityInfo& immuneInfo = _immunityInfo[i]; + + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MECHANIC_IMMUNITY_MASK: + { + switch (miscVal) + { + case 96: // Free Friend, Uncontrollable Frenzy, Warlord's Presence + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + break; + } + case 1615: // Incite Rage, Wolf Spirit, Overload, Lightning Tendrils + { + switch (Id) + { + case 43292: // Incite Rage + case 49172: // Wolf Spirit + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + [[fallthrough]]; + case 61869: // Overload + case 63481: + case 61887: // Lightning Tendrils + case 63486: + mechanicImmunityMask |= (1 << MECHANIC_INTERRUPT) | (1 << MECHANIC_SILENCE); + + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK); + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK_DEST); + break; + default: + break; + } + break; + } + case 679: // Mind Control, Avenging Fury + { + if (Id == 57742) // Avenging Fury + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 1557: // Startling Roar, Warlord Roar, Break Bonds, Stormshield + { + if (Id == 64187) // Stormshield + { + mechanicImmunityMask |= (1 << MECHANIC_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + } + else + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 1614: // Fixate + case 1694: // Fixated, Lightning Tendrils + { + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_ATTACK_ME); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_TAUNT); + break; + } + case 1630: // Fervor, Berserk + { + if (Id == 64112) // Berserk + { + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_ATTACK_ME); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_TAUNT); + } + else + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 477: // Bladestorm + case 1733: // Bladestorm, Killing Spree + { + if (!amount) + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK); + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK_DEST); + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 878: // Whirlwind, Fog of Corruption, Determination + { + if (Id == 66092) // Determination + { + mechanicImmunityMask |= (1 << MECHANIC_SNARE) | (1 << MECHANIC_STUN) + | (1 << MECHANIC_DISORIENTED) | (1 << MECHANIC_FREEZE); + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + } + break; + } + default: + break; + } + + if (immuneInfo.AuraTypeImmune.empty()) + { + if (miscVal & (1 << 10)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + if (miscVal & (1 << 1)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_TRANSFORM); + + // These flag can be recognized wrong: + if (miscVal & (1 << 6)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + if (miscVal & (1 << 0)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + if (miscVal & (1 << 2)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + if (miscVal & (1 << 9)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + if (miscVal & (1 << 7)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DISARM); + } + break; + } + case SPELL_AURA_MECHANIC_IMMUNITY: + { + switch (Id) + { + case 42292: // PvP trinket + case 59752: // Every Man for Himself + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED); + break; + case 34471: // The Beast Within + case 19574: // Bestial Wrath + case 53490: // Bullheaded + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + break; + case 54508: // Demonic Empowerment + mechanicImmunityMask |= (1 << MECHANIC_SNARE) | (1 << MECHANIC_ROOT) | (1 << MECHANIC_STUN); + break; + default: + if (miscVal < 1) + continue; + + mechanicImmunityMask |= 1 << miscVal; + break; + } + break; + } + case SPELL_AURA_EFFECT_IMMUNITY: + { + immuneInfo.SpellEffectImmune.insert(static_cast(miscVal)); + break; + } + case SPELL_AURA_STATE_IMMUNITY: + { + immuneInfo.AuraTypeImmune.insert(static_cast(miscVal)); + break; + } + case SPELL_AURA_SCHOOL_IMMUNITY: + { + schoolImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: + { + applyHarmfulAuraImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_DAMAGE_IMMUNITY: + { + damageImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_DISPEL_IMMUNITY: + { + dispelImmunity = uint32(miscVal); + break; + } + default: + break; + } + + immuneInfo.SchoolImmuneMask = schoolImmunityMask; + immuneInfo.ApplyHarmfulAuraImmuneMask = applyHarmfulAuraImmunityMask; + immuneInfo.MechanicImmuneMask = mechanicImmunityMask; + immuneInfo.DispelImmune = dispelImmunity; + immuneInfo.DamageSchoolMask = damageImmunityMask; + + immuneInfo.AuraTypeImmune.shrink_to_fit(); + immuneInfo.SpellEffectImmune.shrink_to_fit(); + + _allowedMechanicMask |= immuneInfo.MechanicImmuneMask; + } + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_STUNNED)) + { + switch (Id) + { + case 22812: // Barkskin + case 47585: // Dispersion + _allowedMechanicMask |= + (1 << MECHANIC_STUN) | + (1 << MECHANIC_FREEZE) | + (1 << MECHANIC_KNOCKOUT) | + (1 << MECHANIC_SLEEP); + break; + case 49039: // Lichborne, don't allow normal stuns + break; + default: + _allowedMechanicMask |= (1 << MECHANIC_STUN); + break; + } + } + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_CONFUSED)) + _allowedMechanicMask |= (1 << MECHANIC_DISORIENTED); + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_FLEEING)) + { + switch (Id) + { + case 22812: // Barkskin + case 47585: // Dispersion + _allowedMechanicMask |= (1 << MECHANIC_FEAR) | (1 << MECHANIC_HORROR); + break; + default: + _allowedMechanicMask |= (1 << MECHANIC_FEAR); + break; + } + } +} + +void SpellInfo::ApplyAllSpellImmunitiesTo(Unit* target, uint8 effIndex, bool apply) const +{ + ImmunityInfo const& immuneInfo = _immunityInfo[effIndex]; + + if (uint32 schoolImmunity = immuneInfo.SchoolImmuneMask) + { + target->ApplySpellImmune(Id, IMMUNITY_SCHOOL, schoolImmunity, apply); + + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + target->RemoveAppliedAuras([this, schoolImmunity](AuraApplication const* aurApp) -> bool + { + SpellInfo const* auraSpellInfo = aurApp->GetBase()->GetSpellInfo(); + return ((auraSpellInfo->GetSchoolMask() & schoolImmunity) != 0 && // Check for school mask + CanDispelAura(auraSpellInfo) && + (IsPositive() != aurApp->IsPositive()) && // Check spell vs aura possitivity + !auraSpellInfo->IsPassive() && // Don't remove passive auras + auraSpellInfo->Id != Id); // Don't remove self + }); + } + if (apply && schoolImmunity & SPELL_SCHOOL_MASK_NORMAL) + target->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::InvulnerabilityBuff); + } + + if (uint32 mechanicImmunity = immuneInfo.MechanicImmuneMask) + { + for (uint32 i = MECHANIC_CHARM; i < MAX_MECHANIC; ++i) + if (mechanicImmunity & (1 << i)) + target->ApplySpellImmune(Id, IMMUNITY_MECHANIC, i, apply); + + if (HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + if (apply) + target->RemoveAurasWithMechanic(mechanicImmunity, AuraRemoveFlags::ByDefault, Id); + else + { + std::vector aurasToUpdateTargets; + target->RemoveAppliedAuras([mechanicImmunity, &aurasToUpdateTargets](AuraApplication const* aurApp) + { + Aura* aura = aurApp->GetBase(); + if (aura->GetSpellInfo()->GetAllEffectsMechanicMask() & mechanicImmunity) + aurasToUpdateTargets.push_back(aura); + + // only update targets, don't remove anything + return false; + }); + + for (Aura* aura : aurasToUpdateTargets) + aura->UpdateTargetMap(aura->GetCaster()); + } + } + } + + if (uint32 dispelImmunity = immuneInfo.DispelImmune) + { + target->ApplySpellImmune(Id, IMMUNITY_DISPEL, dispelImmunity, apply); + + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + target->RemoveAppliedAuras([dispelImmunity](AuraApplication const* aurApp) -> bool + { + SpellInfo const* spellInfo = aurApp->GetBase()->GetSpellInfo(); + if (spellInfo->Dispel == dispelImmunity) + return true; + + return false; + }); + } + } + + if (uint32 damageImmunity = immuneInfo.DamageSchoolMask) + { + target->ApplySpellImmune(Id, IMMUNITY_DAMAGE, damageImmunity, apply); + + if (apply && damageImmunity & SPELL_SCHOOL_MASK_NORMAL) + target->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::InvulnerabilityBuff); + } + + for (AuraType auraType : immuneInfo.AuraTypeImmune) + { + target->ApplySpellImmune(Id, IMMUNITY_STATE, auraType, apply); + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + target->RemoveAurasByType(auraType, [](AuraApplication const* aurApp) -> bool + { + // if the aura has SPELL_ATTR0_NO_IMMUNITIES, then it cannot be removed by immunity + return !aurApp->GetBase()->GetSpellInfo()->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES); + }); + } + + for (SpellEffects effectType : immuneInfo.SpellEffectImmune) + target->ApplySpellImmune(Id, IMMUNITY_EFFECT, effectType, apply); +} + +bool SpellInfo::CanSpellProvideImmunityAgainstAura(SpellInfo const* auraSpellInfo) const +{ + if (!auraSpellInfo) + return false; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + ImmunityInfo const* immuneInfo = _immunityInfo + i; + + if (!auraSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) && !auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + if (uint32 schoolImmunity = immuneInfo->SchoolImmuneMask) + if ((auraSpellInfo->SchoolMask & schoolImmunity) != 0) + return true; + } + + if (uint32 mechanicImmunity = immuneInfo->MechanicImmuneMask) + if ((mechanicImmunity & (1 << auraSpellInfo->Mechanic)) != 0) + return true; + + if (uint32 dispelImmunity = immuneInfo->DispelImmune) + if (auraSpellInfo->Dispel == dispelImmunity) + return true; + + bool immuneToAllEffects = true; + for (uint8 effIndex = 0; effIndex < MAX_SPELL_EFFECTS; ++effIndex) + { + uint32 effectName = auraSpellInfo->Effects[effIndex].Effect; + if (!effectName) + continue; + + auto spellImmuneItr = immuneInfo->SpellEffectImmune.find(static_cast(effectName)); + if (spellImmuneItr == immuneInfo->SpellEffectImmune.cend()) + { + immuneToAllEffects = false; + break; + } + + if (uint32 mechanic = auraSpellInfo->Effects[effIndex].Mechanic) + { + if (!(immuneInfo->MechanicImmuneMask & (1 << mechanic))) + { + immuneToAllEffects = false; + break; + } + } + + if (!auraSpellInfo->HasAttribute(SPELL_ATTR3_ALWAYS_HIT)) + { + if (uint32 auraName = auraSpellInfo->Effects[effIndex].ApplyAuraName) + { + bool isImmuneToAuraEffectApply = false; + auto auraImmuneItr = immuneInfo->AuraTypeImmune.find(static_cast(auraName)); + if (auraImmuneItr != immuneInfo->AuraTypeImmune.cend()) + isImmuneToAuraEffectApply = true; + + if (!isImmuneToAuraEffectApply && !auraSpellInfo->IsPositiveEffect(effIndex) && !auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + if (uint32 applyHarmfulAuraImmunityMask = immuneInfo->ApplyHarmfulAuraImmuneMask) + if ((auraSpellInfo->GetSchoolMask() & applyHarmfulAuraImmunityMask) != 0) + isImmuneToAuraEffectApply = true; + } + + if (!isImmuneToAuraEffectApply) + { + immuneToAllEffects = false; + break; + } + } + } + } + + if (immuneToAllEffects) + return true; + } + + return false; +} + +// based on client Spell_C::CancelsAuraEffect +bool SpellInfo::SpellCancelsAuraEffect(SpellInfo const* auraSpellInfo, uint8 auraEffIndex) const +{ + if (!HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + return false; + + if (auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect != SPELL_EFFECT_APPLY_AURA && Effects[i].Effect != SPELL_EFFECT_APPLY_AURA_2) + continue; + + uint32 const miscValue = static_cast(Effects[i].MiscValue); + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_STATE_IMMUNITY: + if (miscValue != auraSpellInfo->Effects[auraEffIndex].ApplyAuraName) + continue; + break; + case SPELL_AURA_SCHOOL_IMMUNITY: + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: + if (auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES) || !(auraSpellInfo->SchoolMask & miscValue)) + continue; + break; + case SPELL_AURA_DISPEL_IMMUNITY: + if (miscValue != auraSpellInfo->Dispel) + continue; + break; + case SPELL_AURA_MECHANIC_IMMUNITY: + if (miscValue != auraSpellInfo->Mechanic) + { + if (miscValue != auraSpellInfo->Effects[auraEffIndex].Mechanic) + continue; + } + break; + default: + continue; + } + + return true; + } + + return false; +} + +uint32 SpellInfo::GetAllowedMechanicMask() const +{ + return _allowedMechanicMask; +} + +uint32 SpellInfo::GetMechanicImmunityMask(Unit* caster) const +{ + uint32 casterMechanicImmunityMask = caster->GetMechanicImmunityMask(); + uint32 mechanicImmunityMask = 0; + + if (CanBeInterrupted(caster, true)) + { + if (casterMechanicImmunityMask & (1 << MECHANIC_INTERRUPT)) + mechanicImmunityMask |= (1 << MECHANIC_INTERRUPT); + + if (casterMechanicImmunityMask & (1 << MECHANIC_SILENCE)) + mechanicImmunityMask |= (1 << MECHANIC_SILENCE); + } + + return mechanicImmunityMask; +} + +float SpellInfo::CalculateScaledCoefficient(Unit const* caster, float coefficient) const +{ + if (coefficient == 0.f || !caster || SpellScalingId == 0) + return coefficient; + + return coefficient *= GetSpellScalingMultiplier(caster->getLevel()); +} + +float SpellInfo::GetMinRange(bool positive) const +{ + if (!RangeEntry) + return 0.0f; + if (positive) + return RangeEntry->RangeMin[1]; + return RangeEntry->RangeMin[0]; +} + +float SpellInfo::GetMaxRange(bool positive, WorldObject* caster, Spell* spell) const +{ + if (!RangeEntry) + return 0.0f; + float range; + if (positive) + range = RangeEntry->RangeMax[1]; + else + range = RangeEntry->RangeMax[0]; + if (caster) + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Range, range, spell); + return range; +} + +int32 SpellInfo::GetDuration() const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + return (DurationEntry->Duration == -1) ? -1 : abs(DurationEntry->Duration); +} + +int32 SpellInfo::GetMaxDuration() const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + return (DurationEntry->MaxDuration == -1) ? -1 : abs(DurationEntry->MaxDuration); +} + +float SpellInfo::CalcPeriodicHasteMod(Unit const* caster) const +{ + float hasteMod = 1.f; + if (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC)) + hasteMod = caster->GetFloatValue(UNIT_MOD_CAST_HASTE); + else if (HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC) && caster->IsPlayer()) + hasteMod = caster->GetFloatValue(PLAYER_FIELD_MOD_HASTE); + + return hasteMod; +} + +int32 SpellInfo::CalcDuration(WorldObject const* caster /*= nullptr*/) const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + + int32 duration = GetMaxDuration(); + if (duration == -1) + return -1; + + Unit const* unitCaster = caster ? caster->ToUnit() : nullptr; + if (!unitCaster) + return std::min(GetMaxDuration(), DurationEntry->Duration); + + uint32 level = unitCaster->getLevel(); + if (MaxLevel > 0 && level > MaxLevel) + level = MaxLevel; + if (BaseLevel > 0) + level -= BaseLevel; + + duration = DurationEntry->Duration + DurationEntry->DurationPerLevel * level; + duration = std::min(GetMaxDuration(), duration); + + if (duration == -1) + return -1; + + // Increase duration based on combo points + if (HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DURATION)) + { + if (uint8 comboPoints = unitCaster->IsMovedByClient() ? unitCaster->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints() : 0) + { + if (GetDuration() != GetMaxDuration() && GetDuration() != -1) + duration += int32((GetMaxDuration() - GetDuration()) * comboPoints / 5); + } + } + + if (Player const* modOwner = unitCaster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Duration, duration); + + if (HasAttribute(SPELL_ATTR8_HASTE_AFFECTS_DURATION) || HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC)) + { + SpellEffIndex periodicEffectIndex = EFFECT_0; + bool hasPeriodicEffect = [&]() + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].AuraPeriod) + { + periodicEffectIndex = SpellEffIndex(i); + return true; + } + + return false; + }(); + + if (hasPeriodicEffect && !HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS) && (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC) || HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC))) + { + float hasteMod = CalcPeriodicHasteMod(unitCaster); + if (hasteMod > 0.001f) + { + if (HasAttribute(SPELL_ATTR8_HASTE_AFFECTS_DURATION)) + return int32(duration * hasteMod); + + int32 period = CalcPeriod(caster, periodicEffectIndex); + if (period > 0) + duration = int32(period * (duration / (float)period)); + } + } + } + + return duration; +} + +int32 SpellInfo::CalcPeriod(WorldObject const* caster, SpellEffIndex effIndex, Optional periodOverride /*= {}*/) const +{ + int32 period = 0; + if (periodOverride.has_value() && *periodOverride > 0) + period = *periodOverride; + else + { + if (Effects[effIndex].IsEffect() && Effects[effIndex].AuraPeriod != 0) + period = Effects[effIndex].AuraPeriod; + else + period = 5200; + } + + Unit const* unitCaster = caster ? caster->ToUnit() : nullptr; + if (!unitCaster) + return period; + + if (Player const* modOwner = unitCaster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Period, period); + + if (!HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS) && (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC) || HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC))) + { + float hasteMod = CalcPeriodicHasteMod(unitCaster); + if (hasteMod > 0.001f) + period = ((float)period * hasteMod); + } + + return period; +} + +uint32 SpellInfo::CalcCastTime(uint8 level, Spell* spell /*= nullptr*/) const +{ + int32 castTime = 0; + if (!level && spell && spell->GetCaster()->IsUnit()) + level = spell->GetCaster()->ToUnit()->getLevel(); + + // not all spells have cast time index and this is all is passive abilities + if (level && Scaling.CastTimeMax > 0) + { + castTime = Scaling.CastTimeMax; + if (Scaling.CastTimeMaxLevel > level) + castTime = Scaling.CastTimeMin + int32(level - 1) * (Scaling.CastTimeMax - Scaling.CastTimeMin) / (Scaling.CastTimeMaxLevel - 1); + } + else if (CastTimeEntry) + castTime = CastTimeEntry->Base; + + if (!castTime) + return 0; + + if (spell) + spell->GetCaster()->ModSpellCastTime(this, castTime, spell); + + return (castTime > 0) ? uint32(castTime) : 0; +} + +uint32 SpellInfo::GetMaxTicks() const +{ + uint32 totalTicks = 0; + int32 DotDuration = GetDuration(); + + for (uint8 x = 0; x < MAX_SPELL_EFFECTS; x++) + { + if (Effects[x].Effect == SPELL_EFFECT_APPLY_AURA || Effects[x].Effect == SPELL_EFFECT_APPLY_AURA_2) + { + switch (Effects[x].ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_48: + case SPELL_AURA_POWER_BURN: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_PERIODIC_DUMMY: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + case SPELL_AURA_PERIODIC_HEALTH_FUNNEL: + // skip infinite periodics + if (Effects[x].AuraPeriod > 0 && DotDuration > 0) + { + totalTicks = static_cast(DotDuration) / Effects[x].AuraPeriod; + if (HasAttribute(SPELL_ATTR5_EXTRA_INITIAL_PERIOD)) + ++totalTicks; + } + break; + default: + break; + } + } + } + + return totalTicks; +} + +uint32 SpellInfo::GetRecoveryTime() const +{ + return RecoveryTime > CategoryRecoveryTime ? RecoveryTime : CategoryRecoveryTime; +} + +int32 SpellInfo::CalcPowerCost(WorldObject const* caster, SpellSchoolMask schoolMask, Spell* spell) const +{ + // gameobject casts don't use power + Unit const* unitCaster = caster->ToUnit(); + if (!unitCaster) + return 0; + + // Spell drain all exist power on cast (Only paladin lay of Hands) + if (HasAttribute(SPELL_ATTR1_USE_ALL_MANA)) + { + // If power type - health drain all + if (PowerType == POWER_HEALTH) + return unitCaster->GetHealth(); + // Else drain all power + if (PowerType < MAX_POWERS) + return unitCaster->GetPower(static_cast<::PowerType>(PowerType)); + + TC_LOG_ERROR("spells", "SpellInfo::CalcPowerCost: Unknown power type '%d' in spell %d", PowerType, Id); + return 0; + } + + bool invertSign = false; + int32 powerCost = 0; + + if (SpellPowerId) + { + int32 manaCost = ManaCost; + powerCost = manaCost; + float powerCostPct = ManaCostPercentage2 != 0.f ? ManaCostPercentage2 : (float)ManaCostPercentage; + + if (manaCost < 0 || powerCostPct < 0.f) + { + manaCost = std::abs(manaCost); + powerCostPct = std::fabs(powerCostPct); + invertSign = true; + powerCost = manaCost; + } + + // Base calculation + if (ManaCostPercentage) + { + int32 ressource = 0; + switch (PowerType) + { + // health as power used + case POWER_HEALTH: + ressource = unitCaster->GetCreateHealth(); + break; + case POWER_MANA: + ressource = unitCaster->GetCreateMana(); + break; + case POWER_RAGE: + case POWER_FOCUS: + case POWER_ENERGY: + ressource = unitCaster->GetMaxPower(static_cast<::PowerType>(PowerType)); + break; + case POWER_RUNE: + case POWER_RUNIC_POWER: + TC_LOG_DEBUG("spells", "CalculateManaCost: Not implemented yet!"); + break; + default: + TC_LOG_ERROR("spells", "CalculateManaCost: Unknown power type '%d' in spell %d", PowerType, Id); + return 0; + } + + manaCost += int32((ressource * powerCostPct) / 100); + powerCost = manaCost; + } + + // Scaling + if (SpellScalingId) + powerCost = int32(ceilf(GetSpellScalingMultiplier(unitCaster->getLevel(), true) * (double)powerCost)); + else + { + uint32 manaCostPerLevel = invertSign ? -int32(ManaCostPerlevel) : ManaCostPerlevel; + powerCost = manaCostPerLevel * SpellLevel + manaCost; + } + } + + // Flat mod from caster auras by spell school and power type + Unit::AuraEffectList const& auras = unitCaster->GetAuraEffectsByType(SPELL_AURA_MOD_POWER_COST_SCHOOL); + for (Unit::AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + if (!((*i)->GetMiscValue() & schoolMask)) + continue; + if (!((*i)->GetMiscValueB() & (1 << PowerType))) + continue; + powerCost += (*i)->GetAmount(); + } + + // Shiv - costs 20 + weaponSpeed*10 energy (apply only to non-triggered spell with energy cost) + if (HasAttribute(SPELL_ATTR4_WEAPON_SPEED_COST_SCALING)) + { + uint32 speed = 0; + if (SpellShapeshiftFormEntry const* ss = sSpellShapeshiftFormStore.LookupEntry(unitCaster->GetShapeshiftForm())) + speed = ss->CombatRoundTime; + else + { + WeaponAttackType slot = BASE_ATTACK; + if (!HasAttribute(SPELL_ATTR3_REQUIRES_MAIN_HAND_WEAPON) && HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + slot = OFF_ATTACK; + + speed = unitCaster->GetBaseAttackTime(slot); + } + + powerCost += speed / 100; + } + + // Apply cost mod by spell + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::PowerCost0, powerCost, spell); + + if (!unitCaster->IsControlledByPlayer()) + { + if (HasAttribute(SPELL_ATTR0_SCALES_WITH_CREATURE_LEVEL)) + { + GtNPCManaCostScalerEntry const* spellScaler = sGtNPCManaCostScalerStore.LookupEntry(SpellLevel - 1); + GtNPCManaCostScalerEntry const* casterScaler = sGtNPCManaCostScalerStore.LookupEntry(unitCaster->getLevel() - 1); + if (spellScaler && spellScaler->ratio > 0.f && casterScaler && casterScaler->ratio > 0.f) + powerCost *= casterScaler->ratio / spellScaler->ratio; + } + } + + if (invertSign) + powerCost = -powerCost; + + // PCT mod from user auras by spell school and power type + Unit::AuraEffectList const& aurasPct = unitCaster->GetAuraEffectsByType(SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT); + for (Unit::AuraEffectList::const_iterator i = aurasPct.begin(); i != aurasPct.end(); ++i) + { + if (!((*i)->GetMiscValue() & schoolMask)) + continue; + if (!((*i)->GetMiscValueB() & (1 << PowerType))) + continue; + powerCost += CalculatePct(powerCost, (*i)->GetAmount()); + } + if (powerCost < 0) + powerCost = 0; + return powerCost; +} + +float SpellInfo::GetSpellScalingMultiplier(int32 targetLevel, bool isPowerCostRelated /*= false*/) const +{ + if (targetLevel <= 0 || Scaling.Class == 0) + return 1.f; + + float multiplier = 1.f; + float scalingMultiplier = 1.f; + + if (targetLevel < Scaling.CastTimeMaxLevel && Scaling.CastTimeMax) + { + int32 castTime = Scaling.CastTimeMin + ((targetLevel - 1) * (Scaling.CastTimeMax - Scaling.CastTimeMin)) / (Scaling.CastTimeMaxLevel - 1); + multiplier = castTime / static_cast(Scaling.CastTimeMax); + scalingMultiplier = castTime / static_cast(Scaling.CastTimeMax); + } + + if (!isPowerCostRelated) + { + if (targetLevel < Scaling.NerfMaxLevel) + scalingMultiplier = ((((1.f - Scaling.NerfFactor) * (targetLevel - 1)) / (Scaling.NerfMaxLevel - 1)) + Scaling.NerfFactor) * multiplier; + } + + return scalingMultiplier; +} + +bool SpellInfo::IsRanked() const +{ + return ChainEntry != nullptr; +} + +uint8 SpellInfo::GetRank() const +{ + if (!ChainEntry) + return 1; + return ChainEntry->rank; +} + +SpellInfo const* SpellInfo::GetFirstRankSpell() const +{ + if (!ChainEntry) + return this; + return ChainEntry->first; +} +SpellInfo const* SpellInfo::GetLastRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->last; +} +SpellInfo const* SpellInfo::GetNextRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->next; +} +SpellInfo const* SpellInfo::GetPrevRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->prev; +} + +SpellInfo const* SpellInfo::GetAuraRankForLevel(uint8 level) const +{ + // ignore passive spells + if (IsPassive()) + return this; + + // Client ignores spell with these attributes (sub_53D9D0) + if (HasAttribute(SPELL_ATTR0_AURA_IS_DEBUFF) || HasAttribute(SPELL_ATTR2_ALLOW_LOW_LEVEL_BUFF)) + return this; + + bool needRankSelection = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (IsPositiveEffect(i) && + (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA || + Effects[i].Effect == SPELL_EFFECT_APPLY_AURA_2 || + Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_PARTY || + Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID) && + !Effects[i].Scaling.Coefficient) + { + needRankSelection = true; + break; + } + } + + // not required + if (!needRankSelection) + return this; + + for (SpellInfo const* nextSpellInfo = this; nextSpellInfo != nullptr; nextSpellInfo = nextSpellInfo->GetPrevRankSpell()) + { + // if found appropriate level + if (uint32(level + 10) >= nextSpellInfo->SpellLevel) + return nextSpellInfo; + + // one rank less then + } + + // not found + return nullptr; +} + +bool SpellInfo::IsRankOf(SpellInfo const* spellInfo) const +{ + return GetFirstRankSpell() == spellInfo->GetFirstRankSpell(); +} + +bool SpellInfo::IsDifferentRankOf(SpellInfo const* spellInfo) const +{ + if (Id == spellInfo->Id) + return false; + return IsRankOf(spellInfo); +} + +bool SpellInfo::IsHighRankOf(SpellInfo const* spellInfo) const +{ + if (ChainEntry && spellInfo->ChainEntry) + if (ChainEntry->first == spellInfo->ChainEntry->first) + if (ChainEntry->rank > spellInfo->ChainEntry->rank) + return true; + + return false; +} + +void SpellInfo::_InitializeExplicitTargetMask() +{ + bool srcSet = false; + bool dstSet = false; + uint32 targetMask = Targets; + // prepare target mask using effect target entries + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect()) + continue; + + targetMask |= Effects[i].TargetA.GetExplicitTargetMask(srcSet, dstSet); + targetMask |= Effects[i].TargetB.GetExplicitTargetMask(srcSet, dstSet); + + // add explicit target flags based on spell effects which have EFFECT_IMPLICIT_TARGET_EXPLICIT and no valid target provided + if (Effects[i].GetImplicitTargetType() != EFFECT_IMPLICIT_TARGET_EXPLICIT) + continue; + + // extend explicit target mask only if valid targets for effect could not be provided by target types + uint32 effectTargetMask = Effects[i].GetMissingTargetMask(srcSet, dstSet, targetMask); + + // don't add explicit object/dest flags when spell has no max range + if (GetMaxRange(true) == 0.0f && GetMaxRange(false) == 0.0f) + effectTargetMask &= ~(TARGET_FLAG_UNIT_MASK | TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_CORPSE_MASK | TARGET_FLAG_DEST_LOCATION); + + targetMask |= effectTargetMask; + } + + ExplicitTargetMask = targetMask; +} + +inline bool _isPositiveTarget(SpellInfo const* spellInfo, uint8 effIndex) +{ + if (!spellInfo->Effects[effIndex].IsEffect()) + return true; + + return (spellInfo->Effects[effIndex].TargetA.GetCheckType() != TARGET_CHECK_ENEMY && + spellInfo->Effects[effIndex].TargetB.GetCheckType() != TARGET_CHECK_ENEMY); +} + +bool _isPositiveEffectImpl(SpellInfo const* spellInfo, uint8 effIndex, std::unordered_set>& visited) +{ + if (!spellInfo->Effects[effIndex].IsEffect()) + return true; + + // attribute may be already set in DB + if (!spellInfo->IsPositiveEffect(effIndex)) + return false; + + // passive auras like talents are all positive + if (spellInfo->IsPassive()) + return true; + + // not found a single positive spell with this attribute + if (spellInfo->HasAttribute(SPELL_ATTR0_AURA_IS_DEBUFF)) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR4_AURA_IS_BUFF)) + return true; + + visited.insert({ spellInfo->Id, effIndex }); + + //We need scaling level info for some auras that compute bp 0 or positive but should be debuffs + float bpScalePerLevel = spellInfo->Effects[effIndex].RealPointsPerLevel; + int32 bp = spellInfo->Effects[effIndex].CalcValue(); + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + switch (spellInfo->Id) + { + case 40268: // Spiritual Vengeance, Teron Gorefiend, Black Temple + case 61987: // Avenging Wrath Marker + case 61988: // Divine Shield exclude aura + case 64412: // Phase Punch, Algalon the Observer, Ulduar + case 72410: // Rune of Blood, Saurfang, Icecrown Citadel + case 71204: // Touch of Insignificance, Lady Deathwhisper, Icecrown Citadel + return false; + case 24732: // Bat Costume + case 30877: // Tag Murloc + case 61716: // Rabbit Costume + case 61734: // Noblegarden Bunny + case 62344: // Fists of Stone + case 50344: // Dream Funnel + case 61819: // Manabonked! (item) + case 61834: // Manabonked! (minigob) + case 73523: // Rigor Mortis + return true; + default: + break; + } + break; + case SPELLFAMILY_ROGUE: + switch (spellInfo->Id) + { + // Envenom must be considered as a positive effect even though it deals damage + case 32645: // Envenom + return true; + case 40251: // Shadow of Death, Teron Gorefiend, Black Temple + return false; + default: + break; + } + break; + case SPELLFAMILY_MAGE: + // Arcane Missiles + if (spellInfo->SpellFamilyFlags[0] == 0x00000800) + return false; + break; + case SPELLFAMILY_WARRIOR: + // Slam, Execute + if ((spellInfo->SpellFamilyFlags[0] & 0x20200000) != 0) + return false; + break; + default: + break; + } + + switch (spellInfo->Mechanic) + { + case MECHANIC_IMMUNE_SHIELD: + return true; + default: + break; + } + + // Special case: effects which determine positivity of whole spell + if (spellInfo->HasAttribute(SPELL_ATTR1_AURA_UNIQUE)) // @todo: rename to and handle SPELL_ATTR1_AURA_UNIQUE + { + // check for targets, there seems to be an assortment of dummy triggering spells that should be negative + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (!_isPositiveTarget(spellInfo, i)) + return false; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (spellInfo->Effects[i].Effect) + { + case SPELL_EFFECT_HEAL: + case SPELL_EFFECT_LEARN_SPELL: + case SPELL_EFFECT_SKILL_STEP: + case SPELL_EFFECT_HEAL_PCT: + return true; + case SPELL_EFFECT_INSTAKILL: + if (i != effIndex && // for spells like 38044: instakill effect is negative but auras on target must count as buff + spellInfo->Effects[i].TargetA.GetTarget() == spellInfo->Effects[effIndex].TargetA.GetTarget() && + spellInfo->Effects[i].TargetB.GetTarget() == spellInfo->Effects[effIndex].TargetB.GetTarget()) + return false; + break; + default: + break; + } + + if (spellInfo->Effects[i].IsAura()) + { + switch (spellInfo->Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_STEALTH: + case SPELL_AURA_MOD_UNATTACKABLE: + return true; + case SPELL_AURA_SCHOOL_HEAL_ABSORB: + case SPELL_AURA_EMPATHY: + case SPELL_AURA_MOD_DAMAGE_FROM_CASTER: + case SPELL_AURA_PREVENTS_FLEEING: + return false; + default: + break; + } + } + } + + switch (spellInfo->Effects[effIndex].Effect) + { + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_INSTAKILL: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_STEAL_BENEFICIAL_BUFF: + case SPELL_EFFECT_INTERRUPT_CAST: + case SPELL_EFFECT_PICKPOCKET: + case SPELL_EFFECT_GAMEOBJECT_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE_PCT: + case SPELL_EFFECT_APPLY_AREA_AURA_ENEMY: + case SPELL_EFFECT_TAMECREATURE: + case SPELL_EFFECT_DISTRACT: + return false; + case SPELL_EFFECT_ENERGIZE: + case SPELL_EFFECT_ENERGIZE_PCT: + case SPELL_EFFECT_HEAL_PCT: + case SPELL_EFFECT_HEAL_MAX_HEALTH: + case SPELL_EFFECT_HEAL_MECHANICAL: + return true; + case SPELL_EFFECT_KNOCK_BACK: + case SPELL_EFFECT_CHARGE: + case SPELL_EFFECT_PERSISTENT_AREA_AURA: + case SPELL_EFFECT_ATTACK_ME: + case SPELL_EFFECT_POWER_BURN: + // check targets + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_EFFECT_DISPEL: + // non-positive dispel + switch (spellInfo->Effects[effIndex].MiscValue) + { + case DISPEL_STEALTH: + case DISPEL_INVISIBILITY: + case DISPEL_ENRAGE: + return false; + default: + break; + } + + // also check targets + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_EFFECT_DISPEL_MECHANIC: + if (!_isPositiveTarget(spellInfo, effIndex)) + { + // non-positive mechanic dispel on negative target + switch (spellInfo->Effects[effIndex].MiscValue) + { + case MECHANIC_BANDAGE: + case MECHANIC_SHIELD: + case MECHANIC_MOUNT: + case MECHANIC_INVULNERABILITY: + return false; + default: + break; + } + } + break; + case SPELL_EFFECT_THREAT: + case SPELL_EFFECT_MODIFY_THREAT_PERCENT: + // check targets AND basepoints + if (!_isPositiveTarget(spellInfo, effIndex) && bp > 0) + return false; + break; + default: + break; + } + + if (spellInfo->Effects[effIndex].IsAura()) + { + // non-positive aura use + switch (spellInfo->Effects[effIndex].ApplyAuraName) + { + case SPELL_AURA_MOD_STAT: // dependent from basepoint sign (negative -> negative) + case SPELL_AURA_MOD_SKILL: + case SPELL_AURA_MOD_DODGE_PERCENT: + case SPELL_AURA_MOD_HEALING_DONE: + case SPELL_AURA_MOD_DAMAGE_DONE_CREATURE: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_MOD_CRIT_PCT: + case SPELL_AURA_MOD_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_CRIT_CHANCE: + case SPELL_AURA_MOD_RANGED_HASTE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE_2: + case SPELL_AURA_MOD_CASTING_SPEED_NOT_STACK: + case SPELL_AURA_HASTE_SPELLS: + case SPELL_AURA_HASTE_RANGED: + case SPELL_AURA_MOD_DETECT_RANGE: + case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT: + case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE: + case SPELL_AURA_MOD_INCREASE_SWIM_SPEED: + case SPELL_AURA_MOD_PERCENT_STAT: + case SPELL_AURA_MOD_INCREASE_HEALTH: + case SPELL_AURA_MOD_SPEED_ALWAYS: + if (bp < 0 || bpScalePerLevel < 0) //TODO: What if both are 0? Should it be a buff or debuff? + return false; + break; + case SPELL_AURA_MOD_ATTACKSPEED: // some buffs have negative bp, check both target and bp + case SPELL_AURA_MOD_MELEE_HASTE: + case SPELL_AURA_MOD_DAMAGE_DONE: + case SPELL_AURA_MOD_RESISTANCE: + case SPELL_AURA_MOD_RESISTANCE_PCT: + case SPELL_AURA_MOD_RATING: + case SPELL_AURA_MOD_ATTACK_POWER: + case SPELL_AURA_MOD_RANGED_ATTACK_POWER: + case SPELL_AURA_MOD_DAMAGE_PERCENT_DONE: + case SPELL_AURA_MOD_SPEED_SLOW_ALL: + case SPELL_AURA_MELEE_SLOW: + case SPELL_AURA_MOD_ATTACK_POWER_PCT: + case SPELL_AURA_MOD_HEALING_DONE_PERCENT: + case SPELL_AURA_MOD_HEALING_PCT: + case SPELL_AURA_MOD_DAMAGE_DONE_VERSUS_AURASTATE: + if (!_isPositiveTarget(spellInfo, effIndex) && bp < 0) + return false; + break; + case SPELL_AURA_MOD_DAMAGE_TAKEN: // dependent from basepoint sign (positive -> negative) + case SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN: + case SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN_PCT: + case SPELL_AURA_MOD_POWER_COST_SCHOOL: + case SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT: + case SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT: + case SPELL_AURA_BYPASS_ARMOR_FOR_CASTER: + if (bp > 0) + return false; + break; + case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN: // check targets and basepoints (ex Recklessness) + if (!_isPositiveTarget(spellInfo, effIndex) && bp > 0) + return false; + break; + case SPELL_AURA_MOD_HEALTH_REGEN_PERCENT: // check targets and basepoints (target enemy and negative bp -> negative) + if (!_isPositiveTarget(spellInfo, effIndex) && bp < 0) + return false; + break; + case SPELL_AURA_ADD_TARGET_TRIGGER: + return true; + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + if (!_isPositiveTarget(spellInfo, effIndex)) + { + if (SpellInfo const* spellTriggeredProto = sSpellMgr->GetSpellInfo(spellInfo->Effects[effIndex].TriggerSpell)) + { + // negative targets of main spell return early + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // already seen this + if (visited.count({ spellTriggeredProto->Id, i }) > 0) + continue; + + if (!spellTriggeredProto->Effects[i].Effect) + continue; + + // if non-positive trigger cast targeted to positive target this main cast is non-positive + // this will place this spell auras as debuffs + if (_isPositiveTarget(spellTriggeredProto, i) && !_isPositiveEffectImpl(spellTriggeredProto, i, visited)) + return false; + } + } + } + break; + case SPELL_AURA_PERIODIC_TRIGGER_SPELL: + case SPELL_AURA_MOD_STUN: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_MOD_DECREASE_SPEED: + case SPELL_AURA_MOD_FEAR: + case SPELL_AURA_MOD_TAUNT: + // special auras: they may have non negative target but still need to be marked as debuff + // checked again after all effects (SpellInfo::_InitializeSpellPositivity) + case SPELL_AURA_MOD_PACIFY: + case SPELL_AURA_MOD_PACIFY_SILENCE: + case SPELL_AURA_MOD_DISARM: + case SPELL_AURA_MOD_DISARM_OFFHAND: + case SPELL_AURA_MOD_DISARM_RANGED: + case SPELL_AURA_MOD_CHARM: + case SPELL_AURA_AOE_CHARM: + case SPELL_AURA_MOD_POSSESS: + case SPELL_AURA_MOD_LANGUAGE: + case SPELL_AURA_DAMAGE_SHIELD: + case SPELL_AURA_PROC_TRIGGER_SPELL: + case SPELL_AURA_MOD_ATTACKER_MELEE_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_RANGED_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE: + case SPELL_AURA_DUMMY: + case SPELL_AURA_PERIODIC_DUMMY: + case SPELL_AURA_MOD_HEALING: + case SPELL_AURA_MOD_WEAPON_CRIT_PERCENT: + case SPELL_AURA_POWER_BURN: + case SPELL_AURA_MOD_COOLDOWN: + case SPELL_AURA_MOD_INCREASE_SPEED: + case SPELL_AURA_MOD_PARRY_PERCENT: + case SPELL_AURA_SET_VEHICLE_ID: + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_EFFECT_IMMUNITY: + case SPELL_AURA_OVERRIDE_CLASS_SCRIPTS: + case SPELL_AURA_MOD_SHAPESHIFT: + case SPELL_AURA_MOD_THREAT: + case SPELL_AURA_PROC_TRIGGER_SPELL_WITH_VALUE: + // check target for positive and negative spells + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_AURA_MOD_CONFUSE: + case SPELL_AURA_CHANNEL_DEATH_ITEM: + case SPELL_AURA_MOD_ROOT: + case SPELL_AURA_MOD_SILENCE: + case SPELL_AURA_MOD_DETAUNT: + case SPELL_AURA_GHOST: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_MOD_STALKED: + case SPELL_AURA_PREVENT_RESURRECTION: + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS: + case SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS: + case SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE: + return false; + case SPELL_AURA_MECHANIC_IMMUNITY: + { + // non-positive immunities + switch (spellInfo->Effects[effIndex].MiscValue) + { + case MECHANIC_BANDAGE: + case MECHANIC_SHIELD: + case MECHANIC_MOUNT: + case MECHANIC_INVULNERABILITY: + return false; + default: + break; + } + break; + } + case SPELL_AURA_ADD_FLAT_MODIFIER: // mods + case SPELL_AURA_ADD_PCT_MODIFIER: + { + switch (SpellModOp(spellInfo->Effects[effIndex].MiscValue)) + { + case SpellModOp::ChangeCastTime: // dependent from basepoint sign (positive -> negative) + case SpellModOp::Period: + case SpellModOp::PowerCostOnMiss: + case SpellModOp::StartCooldown: + if (bp > 0) + return false; + break; + case SpellModOp::Cooldown: + case SpellModOp::PowerCost0: + if (!spellInfo->IsPositive() && bp > 0) // dependent on prev effects too (ex Arcane Power) + return false; + break; + case SpellModOp::PointsIndex0: // always positive + case SpellModOp::PointsIndex1: + case SpellModOp::PointsIndex2: + case SpellModOp::Points: + case SpellModOp::Hate: + case SpellModOp::ChainAmplitude: + case SpellModOp::Amplitude: + return true; + case SpellModOp::Duration: + case SpellModOp::CritChance: + case SpellModOp::HealingAndDamage: + case SpellModOp::ChainTargets: + if (!spellInfo->IsPositive() && bp < 0) // dependent on prev effects too + return false; + break; + default: // dependent from basepoint sign (negative -> negative) + if (bp < 0) + return false; + break; + } + break; + } + default: + break; + } + } + // negative spell if triggered spell is negative + if (!spellInfo->Effects[effIndex].ApplyAuraName && spellInfo->Effects[effIndex].TriggerSpell) + { + if (SpellInfo const* spellTriggeredProto = sSpellMgr->GetSpellInfo(spellInfo->Effects[effIndex].TriggerSpell)) + { + // spells with at least one negative effect are considered negative + // some self-applied spells have negative effects but in self casting case negative check ignored. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // already seen this + if (visited.count({ spellTriggeredProto->Id, i }) > 0) + continue; + + if (!spellTriggeredProto->Effects[i].IsEffect()) + continue; + + if (!_isPositiveEffectImpl(spellTriggeredProto, i, visited)) + return false; + } + } + } + + // ok, positive + return true; +} + +void SpellInfo::_InitializeSpellPositivity() +{ + std::unordered_set> visited; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (!_isPositiveEffectImpl(this, i, visited)) + AttributesCu |= (SPELL_ATTR0_CU_NEGATIVE_EFF0 << i); + + // additional checks after effects marked + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect() || !IsPositiveEffect(i)) + continue; + + switch (Effects[i].ApplyAuraName) + { + // has other non positive effect? + // then it should be marked negative if has same target as negative effect (ex 8510, 8511, 8893, 10267) + case SPELL_AURA_DUMMY: + case SPELL_AURA_MOD_STUN: + case SPELL_AURA_MOD_FEAR: + case SPELL_AURA_MOD_TAUNT: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_MOD_ATTACKSPEED: + case SPELL_AURA_MOD_DECREASE_SPEED: + { + for (uint8 j = i + 1; j < MAX_SPELL_EFFECTS; ++j) + if (!IsPositiveEffect(j) && Effects[i].TargetA.GetTarget() == Effects[j].TargetA.GetTarget() && Effects[i].TargetB.GetTarget() == Effects[j].TargetB.GetTarget()) + AttributesCu |= (SPELL_ATTR0_CU_NEGATIVE_EFF0 << i); + + break; + } + default: + break; + } + } +} + +SpellTargetRestrictionsEntry const* SpellInfo::GetSpellTargetRestrictions() const +{ + return SpellTargetRestrictionsId ? sSpellTargetRestrictionsStore.LookupEntry(SpellTargetRestrictionsId) : nullptr; +} + +SpellEquippedItemsEntry const* SpellInfo::GetSpellEquippedItems() const +{ + return SpellEquippedItemsId ? sSpellEquippedItemsStore.LookupEntry(SpellEquippedItemsId) : nullptr; +} + +SpellInterruptsEntry const* SpellInfo::GetSpellInterrupts() const +{ + return SpellInterruptsId ? sSpellInterruptsStore.LookupEntry(SpellInterruptsId) : nullptr; +} + +SpellLevelsEntry const* SpellInfo::GetSpellLevels() const +{ + return SpellLevelsId ? sSpellLevelsStore.LookupEntry(SpellLevelsId) : nullptr; +} + +SpellPowerEntry const* SpellInfo::GetSpellPower() const +{ + return SpellPowerId ? sSpellPowerStore.LookupEntry(SpellPowerId) : nullptr; +} + +SpellReagentsEntry const* SpellInfo::GetSpellReagents() const +{ + return SpellReagentsId ? sSpellReagentsStore.LookupEntry(SpellReagentsId) : nullptr; +} + +SpellScalingEntry const* SpellInfo::GetSpellScaling() const +{ + return SpellScalingId ? sSpellScalingStore.LookupEntry(SpellScalingId) : nullptr; +} + +SpellShapeshiftEntry const* SpellInfo::GetSpellShapeshift() const +{ + return SpellShapeshiftId ? sSpellShapeshiftStore.LookupEntry(SpellShapeshiftId) : nullptr; +} + +SpellTotemsEntry const* SpellInfo::GetSpellTotems() const +{ + return SpellTotemsId ? sSpellTotemsStore.LookupEntry(SpellTotemsId) : nullptr; +} + +SpellAuraOptionsEntry const* SpellInfo::GetSpellAuraOptions() const +{ + return SpellAuraOptionsId ? sSpellAuraOptionsStore.LookupEntry(SpellAuraOptionsId) : nullptr; +} + +SpellAuraRestrictionsEntry const* SpellInfo::GetSpellAuraRestrictions() const +{ + return SpellAuraRestrictionsId ? sSpellAuraRestrictionsStore.LookupEntry(SpellAuraRestrictionsId) : nullptr; +} + +SpellCastingRequirementsEntry const* SpellInfo::GetSpellCastingRequirements() const +{ + return SpellCastingRequirementsId ? sSpellCastingRequirementsStore.LookupEntry(SpellCastingRequirementsId) : nullptr; +} + +SpellCategoriesEntry const* SpellInfo::GetSpellCategories() const +{ + return SpellCategoriesId ? sSpellCategoriesStore.LookupEntry(SpellCategoriesId) : nullptr; +} + +SpellClassOptionsEntry const* SpellInfo::GetSpellClassOptions() const +{ + return SpellClassOptionsId ? sSpellClassOptionsStore.LookupEntry(SpellClassOptionsId) : nullptr; +} + +SpellCooldownsEntry const* SpellInfo::GetSpellCooldowns() const +{ + return SpellCooldownsId ? sSpellCooldownsStore.LookupEntry(SpellCooldownsId) : nullptr; +} + +void SpellInfo::_UnloadImplicitTargetConditionLists() +{ + // find the same instances of ConditionList and delete them. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + ConditionContainer* cur = Effects[i].ImplicitTargetConditions; + if (!cur) + continue; + for (uint8 j = i; j < MAX_SPELL_EFFECTS; ++j) + { + if (Effects[j].ImplicitTargetConditions == cur) + Effects[j].ImplicitTargetConditions = nullptr; + } + delete cur; + } +} diff --git a/src/server/game/Spells/SpellInfo.cpp.bak-vianomod-aura-compile-fix2-20261001-202041 b/src/server/game/Spells/SpellInfo.cpp.bak-vianomod-aura-compile-fix2-20261001-202041 new file mode 100644 index 0000000000..8fd56cbc7e --- /dev/null +++ b/src/server/game/Spells/SpellInfo.cpp.bak-vianomod-aura-compile-fix2-20261001-202041 @@ -0,0 +1,4532 @@ +/* + * 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 . + */ + +#include "SpellInfo.h" +#include "Battleground.h" +#include "Corpse.h" +#include "Creature.h" +#include "DBCStores.h" +#include "GameClient.h" +#include "GameTables.h" +#include "InstanceScript.h" +#include "Item.h" +#include "ItemTemplate.h" +#include "Log.h" +#include "LootMgr.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "Random.h" +#include "Spell.h" +#include "SpellAuraDefines.h" +#include "SpellAuraEffects.h" +#include "SpellMgr.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +uint32 GetTargetFlagMask(SpellTargetObjectTypes objType) +{ + switch (objType) + { + case TARGET_OBJECT_TYPE_DEST: + return TARGET_FLAG_DEST_LOCATION; + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + return TARGET_FLAG_DEST_LOCATION | TARGET_FLAG_UNIT; + case TARGET_OBJECT_TYPE_CORPSE_ALLY: + return TARGET_FLAG_CORPSE_ALLY; + case TARGET_OBJECT_TYPE_CORPSE_ENEMY: + return TARGET_FLAG_CORPSE_ENEMY; + case TARGET_OBJECT_TYPE_CORPSE: + return TARGET_FLAG_CORPSE_ALLY | TARGET_FLAG_CORPSE_ENEMY; + case TARGET_OBJECT_TYPE_UNIT: + return TARGET_FLAG_UNIT; + case TARGET_OBJECT_TYPE_GOBJ: + return TARGET_FLAG_GAMEOBJECT; + case TARGET_OBJECT_TYPE_GOBJ_ITEM: + return TARGET_FLAG_GAMEOBJECT_ITEM; + case TARGET_OBJECT_TYPE_ITEM: + return TARGET_FLAG_ITEM; + case TARGET_OBJECT_TYPE_SRC: + return TARGET_FLAG_SOURCE_LOCATION; + default: + return TARGET_FLAG_NONE; + } +} + +SpellImplicitTargetInfo::SpellImplicitTargetInfo(uint32 target) +{ + _target = Targets(target); +} + +bool SpellImplicitTargetInfo::IsArea() const +{ + return GetSelectionCategory() == TARGET_SELECT_CATEGORY_AREA || GetSelectionCategory() == TARGET_SELECT_CATEGORY_CONE; +} + +SpellTargetSelectionCategories SpellImplicitTargetInfo::GetSelectionCategory() const +{ + return _data[_target].SelectionCategory; +} + +SpellTargetReferenceTypes SpellImplicitTargetInfo::GetReferenceType() const +{ + return _data[_target].ReferenceType; +} + +SpellTargetObjectTypes SpellImplicitTargetInfo::GetObjectType() const +{ + return _data[_target].ObjectType; +} + +SpellTargetCheckTypes SpellImplicitTargetInfo::GetCheckType() const +{ + return _data[_target].SelectionCheckType; +} + +SpellTargetDirectionTypes SpellImplicitTargetInfo::GetDirectionType() const +{ + return _data[_target].DirectionType; +} + +float SpellImplicitTargetInfo::CalcDirectionAngle(SpellEffectInfo const& effectInfo) const +{ + switch (GetDirectionType()) + { + case TARGET_DIR_FRONT: + return 0.0f; + case TARGET_DIR_BACK: + return static_cast(M_PI); + case TARGET_DIR_RIGHT: + return static_cast(-M_PI/2); + case TARGET_DIR_LEFT: + return static_cast(M_PI/2); + case TARGET_DIR_FRONT_RIGHT: + return static_cast(-M_PI/4); + case TARGET_DIR_BACK_RIGHT: + return static_cast(-3*M_PI/4); + case TARGET_DIR_BACK_LEFT: + return static_cast(3*M_PI/4); + case TARGET_DIR_FRONT_LEFT: + return static_cast(M_PI/4); + case TARGET_DIR_RANDOM: + return rand_norm() * static_cast(2 * M_PI); + case TARGET_DIR_SUMMON: + // This direction does alter its angle based on what is being summoned. + // Creatures are being summoned on the left, gameobjects infront + switch (effectInfo.Effect) + { + case SPELL_EFFECT_SUMMON_PET: + case SPELL_EFFECT_SUMMON: + return static_cast(M_PI / 2); + default: + return 0.0f; + } + default: + return 0.0f; + } +} + +Targets SpellImplicitTargetInfo::GetTarget() const +{ + return _target; +} + +uint32 SpellImplicitTargetInfo::GetExplicitTargetMask(bool& srcSet, bool& dstSet) const +{ + uint32 targetMask = 0; + if (GetTarget() == TARGET_DEST_TRAJ) + { + if (!srcSet) + targetMask = TARGET_FLAG_SOURCE_LOCATION; + if (!dstSet) + targetMask |= TARGET_FLAG_DEST_LOCATION; + } + else + { + switch (GetReferenceType()) + { + case TARGET_REFERENCE_TYPE_SRC: + if (srcSet) + break; + targetMask = TARGET_FLAG_SOURCE_LOCATION; + break; + case TARGET_REFERENCE_TYPE_DEST: + if (dstSet) + break; + targetMask = TARGET_FLAG_DEST_LOCATION; + break; + case TARGET_REFERENCE_TYPE_TARGET: + switch (GetObjectType()) + { + case TARGET_OBJECT_TYPE_GOBJ: + targetMask = TARGET_FLAG_GAMEOBJECT; + break; + case TARGET_OBJECT_TYPE_GOBJ_ITEM: + targetMask = TARGET_FLAG_GAMEOBJECT_ITEM; + break; + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + case TARGET_OBJECT_TYPE_UNIT: + case TARGET_OBJECT_TYPE_DEST: + switch (GetCheckType()) + { + case TARGET_CHECK_ENEMY: + targetMask = TARGET_FLAG_UNIT_ENEMY; + break; + case TARGET_CHECK_ALLY: + targetMask = TARGET_FLAG_UNIT_ALLY; + break; + case TARGET_CHECK_PARTY: + targetMask = TARGET_FLAG_UNIT_PARTY; + break; + case TARGET_CHECK_RAID: + targetMask = TARGET_FLAG_UNIT_RAID; + break; + case TARGET_CHECK_PASSENGER: + targetMask = TARGET_FLAG_UNIT_PASSENGER; + break; + case TARGET_CHECK_RAID_CLASS: + [[fallthrough]]; + default: + targetMask = TARGET_FLAG_UNIT; + break; + } + break; + default: + break; + } + break; + default: + break; + } + } + + switch (GetObjectType()) + { + case TARGET_OBJECT_TYPE_SRC: + srcSet = true; + break; + case TARGET_OBJECT_TYPE_DEST: + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + dstSet = true; + break; + default: + break; + } + return targetMask; +} + +SpellImplicitTargetInfo::StaticData SpellImplicitTargetInfo::_data[TOTAL_SPELL_TARGETS] = +{ + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 1 TARGET_UNIT_CASTER + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 2 TARGET_UNIT_NEARBY_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 3 TARGET_UNIT_NEARBY_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 4 TARGET_UNIT_NEARBY_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 5 TARGET_UNIT_PET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 6 TARGET_UNIT_TARGET_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 7 TARGET_UNIT_SRC_AREA_ENTRY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 8 TARGET_UNIT_DEST_AREA_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 9 TARGET_DEST_HOME + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 10 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 11 TARGET_UNIT_SRC_AREA_UNK_11 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 12 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 13 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 14 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 15 TARGET_UNIT_SRC_AREA_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 16 TARGET_UNIT_DEST_AREA_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 17 TARGET_DEST_DB + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 18 TARGET_DEST_CASTER + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 19 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 20 TARGET_UNIT_CASTER_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 21 TARGET_UNIT_TARGET_ALLY + {TARGET_OBJECT_TYPE_SRC, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 22 TARGET_SRC_CASTER + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 23 TARGET_GAMEOBJECT_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 24 TARGET_UNIT_CONE_ENEMY_24 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 25 TARGET_UNIT_TARGET_ANY + {TARGET_OBJECT_TYPE_GOBJ_ITEM, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 26 TARGET_GAMEOBJECT_ITEM_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 27 TARGET_UNIT_MASTER + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 28 TARGET_DEST_DYNOBJ_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 29 TARGET_DEST_DYNOBJ_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 30 TARGET_UNIT_SRC_AREA_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 31 TARGET_UNIT_DEST_AREA_ALLY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_SUMMON }, // 32 TARGET_DEST_CASTER_SUMMON + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 33 TARGET_UNIT_SRC_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 34 TARGET_UNIT_DEST_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 35 TARGET_UNIT_TARGET_PARTY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 36 TARGET_DEST_CASTER_UNK_36 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_LAST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 37 TARGET_UNIT_LASTTARGET_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 38 TARGET_UNIT_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 39 TARGET_DEST_CASTER_FISHING + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 40 TARGET_GAMEOBJECT_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 41 TARGET_DEST_CASTER_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 42 TARGET_DEST_CASTER_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 43 TARGET_DEST_CASTER_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 44 TARGET_DEST_CASTER_FRONT_LEFT + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 45 TARGET_UNIT_TARGET_CHAINHEAL_ALLY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 46 TARGET_DEST_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 47 TARGET_DEST_CASTER_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 48 TARGET_DEST_CASTER_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 49 TARGET_DEST_CASTER_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 50 TARGET_DEST_CASTER_LEFT + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 51 TARGET_GAMEOBJECT_SRC_AREA + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 52 TARGET_GAMEOBJECT_DEST_AREA + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 53 TARGET_DEST_TARGET_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 54 TARGET_UNIT_CONE_180_DEG_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 55 TARGET_DEST_CASTER_FRONT_LEAP + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 56 TARGET_UNIT_CASTER_AREA_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 57 TARGET_UNIT_TARGET_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 58 TARGET_UNIT_NEARBY_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ALLY, TARGET_DIR_FRONT}, // 59 TARGET_UNIT_CONE_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENTRY, TARGET_DIR_FRONT}, // 60 TARGET_UNIT_CONE_ENTRY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID_CLASS, TARGET_DIR_NONE}, // 61 TARGET_UNIT_TARGET_AREA_RAID_CLASS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 62 TARGET_DEST_CASTER_GROUND + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 63 TARGET_DEST_TARGET_ANY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 64 TARGET_DEST_TARGET_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 65 TARGET_DEST_TARGET_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 66 TARGET_DEST_TARGET_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 67 TARGET_DEST_TARGET_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 68 TARGET_DEST_TARGET_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 69 TARGET_DEST_TARGET_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 70 TARGET_DEST_TARGET_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 71 TARGET_DEST_TARGET_FRONT_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 72 TARGET_DEST_CASTER_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 73 TARGET_DEST_CASTER_RADIUS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 74 TARGET_DEST_TARGET_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 75 TARGET_DEST_TARGET_RADIUS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 76 TARGET_DEST_CHANNEL_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 77 TARGET_UNIT_CHANNEL_TARGET + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 78 TARGET_DEST_DEST_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 79 TARGET_DEST_DEST_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 80 TARGET_DEST_DEST_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 81 TARGET_DEST_DEST_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 82 TARGET_DEST_DEST_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 83 TARGET_DEST_DEST_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 84 TARGET_DEST_DEST_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 85 TARGET_DEST_DEST_FRONT_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 86 TARGET_DEST_DEST_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 87 TARGET_DEST_DEST + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 88 TARGET_DEST_DYNOBJ_NONE + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_TRAJ, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 89 TARGET_DEST_TRAJ + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 90 TARGET_UNIT_TARGET_MINIPET + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 91 TARGET_DEST_DEST_RADIUS + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 92 TARGET_UNIT_SUMMONER + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 93 TARGET_CORPSE_SRC_AREA_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 94 TARGET_UNIT_VEHICLE + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_PASSENGER, TARGET_DIR_NONE}, // 95 TARGET_UNIT_TARGET_PASSENGER + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 96 TARGET_UNIT_PASSENGER_0 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 97 TARGET_UNIT_PASSENGER_1 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 98 TARGET_UNIT_PASSENGER_2 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 99 TARGET_UNIT_PASSENGER_3 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 100 TARGET_UNIT_PASSENGER_4 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 101 TARGET_UNIT_PASSENGER_5 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 102 TARGET_UNIT_PASSENGER_6 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 103 TARGET_UNIT_PASSENGER_7 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 104 TARGET_UNIT_CONE_CASTER_TO_DEST_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 105 TARGET_UNIT_CASTER_AND_PASSENGERS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 106 TARGET_DEST_CHANNEL_CASTER + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 107 TARGET_DEST_NEARBY_ENTRY_2 + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 108 TARGET_GAMEOBJECT_CONE_CASTER_TO_DEST_ENEMY + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ALLY, TARGET_DIR_FRONT}, // 109 TARGET_GAMEOBJECT_CONE_CASTER_TO_DEST_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENTRY, TARGET_DIR_FRONT}, // 110 TARGET_UNIT_CONE_CASTER_TO_DEST_ENTRY + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 111 + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 112 TARGET_DEST_TARGET_ANY_2 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 113 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 114 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 115 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 116 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 117 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 118 TARGET_UNIT_TARGET_ALLY_OR_RAID + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 119 TARGET_CORPSE_SRC_AREA_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_SUMMONED, TARGET_DIR_NONE}, // 120 TARGET_UNIT_SELF_AND_SUMMONS + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 121 TARGET_CORPSE_TARGET_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_THREAT, TARGET_DIR_NONE}, // 122 TARGET_UNIT_AREA_THREAT_LIST + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_TAP, TARGET_DIR_NONE}, // 123 TARGET_UNIT_AREA_TAP_LIST + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 124 TARGET_UNIT_TARGET_TAP_LIST + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 125 TARGET_DEST_CASTER_GROUND_2 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE }, // 126 TARGET_UNIT_AREA_ENEMY + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 127 +}; + +SpellEffectInfo::SpellEffectInfo(SpellEntry const* /*spellEntry*/, SpellInfo const* spellInfo, uint8 effIndex, SpellEffectEntry const* _effect) +{ + SpellScalingEntry const* scaling = spellInfo->GetSpellScaling(); + + _spellInfo = spellInfo; + _effIndex = _effect ? _effect->EffectIndex : effIndex; + Effect = _effect ? _effect->Effect : 0; + ApplyAuraName = _effect ? _effect->EffectAura : 0; + AuraPeriod = _effect ? _effect->EffectAuraPeriod : 0; + DieSides = _effect ? _effect->EffectDieSides : 0; + RealPointsPerLevel = _effect ? _effect->EffectRealPointsPerLevel : 0.0f; + BasePoints = _effect ? _effect->EffectBasePoints : 0; + PointsPerComboPoint = _effect ? _effect->EffectPointsPerResource : 0.0f; + Amplitude = _effect ? _effect->EffectAmplitude : 0.0f; + DamageMultiplier = _effect ? _effect->EffectChainAmplitude : 0.0f; + BonusMultiplier = _effect ? _effect->EffectBonusCoefficient : 0.0f; + MiscValue = _effect ? _effect->EffectMiscValue : 0; + MiscValueB = _effect ? _effect->EffectMiscValueB : 0; + Mechanic = Mechanics(_effect ? _effect->EffectMechanic : 0); + TargetA = SpellImplicitTargetInfo(_effect ? _effect->EffectImplicitTargetA : 0); + TargetB = SpellImplicitTargetInfo(_effect ? _effect->EffectImplicitTargetB : 0); + TargetARadiusEntry = _effect && _effect->EffectRadiusIndex ? sSpellRadiusStore.LookupEntry(_effect->EffectRadiusIndex) : nullptr; + TargetBRadiusEntry = _effect && _effect->EffectRadiusMaxIndex ? sSpellRadiusStore.LookupEntry(_effect->EffectRadiusMaxIndex) : nullptr; + ChainTarget = _effect ? _effect->EffectChainTargets : 0; + ItemType = _effect ? _effect->EffectItemType : 0; + TriggerSpell = _effect ? _effect->EffectTriggerSpell : 0; + SpellClassMask = _effect ? _effect->EffectSpellClassMask : flag96(0); + ImplicitTargetConditions = nullptr; + + Scaling.Coefficient = scaling ? scaling->Coefficient[_effIndex] : 0.0f; + Scaling.Variance = scaling ? scaling->Variance[_effIndex] : 0.0f; + Scaling.ComboPointsCoefficient = scaling ? scaling->ComboPointsCoefficient[_effIndex] : 0.0f; +} + +bool SpellEffectInfo::IsEffect() const +{ + return Effect != 0; +} + +bool SpellEffectInfo::IsEffect(SpellEffects effectName) const +{ + return Effect == effectName; +} + +bool SpellEffectInfo::IsAura() const +{ + return (IsUnitOwnedAuraEffect() || Effect == SPELL_EFFECT_PERSISTENT_AREA_AURA) && ApplyAuraName != 0; +} + +bool SpellEffectInfo::IsAura(AuraType aura) const +{ + return IsAura() && ApplyAuraName == aura; +} + +bool SpellEffectInfo::IsTargetingArea() const +{ + return TargetA.IsArea() || TargetB.IsArea(); +} + +bool SpellEffectInfo::IsAreaAuraEffect() const +{ + if (Effect == SPELL_EFFECT_APPLY_AREA_AURA_PARTY || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_FRIEND || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_ENEMY || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_PET || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_OWNER) + return true; + return false; +} + +bool SpellEffectInfo::IsUnitOwnedAuraEffect() const +{ + return IsAreaAuraEffect() || Effect == SPELL_EFFECT_APPLY_AURA || Effect == SPELL_EFFECT_APPLY_AURA_2; +} + +bool SpellEffectInfo::ScalesWithCreatureLevel() const +{ + if (!_spellInfo->HasAttribute(SPELL_ATTR0_SCALES_WITH_CREATURE_LEVEL)) + return false; + + switch (Effect) + { + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_DUMMY: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_HEAL: + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_POWER_BURN: + case SPELL_EFFECT_SCRIPT_EFFECT: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_FORCE_CAST_WITH_VALUE: + case SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE: + case SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE: + return true; + default: + break; + } + + switch (ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_DUMMY: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_DAMAGE_SHIELD: + case SPELL_AURA_PROC_TRIGGER_DAMAGE: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_SCHOOL_ABSORB: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + return true; + default: + break; + } + + return false; +} + +int32 SpellEffectInfo::CalcValue(WorldObject const* caster /*= nullptr*/, int32 const* bp /*= nullptr*/, Unit const* target /*= nullptr*/) const +{ + float basePointsPerLevel = RealPointsPerLevel; + // TODO: this needs to be a float, not rounded + int basePoints = CalcBaseValue(caster, target); + double value = bp ? *bp : basePoints; + + Unit const* casterUnit = nullptr; + if (caster) + casterUnit = caster->ToUnit(); + + if (Scaling.Variance != 0.0f) + { + float delta = fabs(Scaling.Variance * 0.5f); + double valueVariance = frand(-delta, delta); + value += double(basePoints) * valueVariance; + } + else if (DieSides) + { + // roll in a range <1;EffectDieSides> as of patch 3.3.3 + if (DieSides == 1) + value += DieSides; + else + value += (DieSides >= 1) ? irand(1, DieSides) : irand(DieSides, 1); + } + + // base amount modification based on spell lvl vs caster lvl + if (Scaling.Coefficient == 0.0f) + { + if (casterUnit && basePointsPerLevel != 0.0) + { + int32 level = int32(casterUnit->getLevel()); + if (level > int32(_spellInfo->MaxLevel) && _spellInfo->MaxLevel > 0) + level = int32(_spellInfo->MaxLevel); + + // if base level is greater than spell level, reduce by base level (eg. pilgrims foods) + level -= int32(std::max(_spellInfo->BaseLevel, _spellInfo->SpellLevel)); + if (level < 0) + level = 0; + value += level * basePointsPerLevel; + } + } + + // bonus amount from combo points + if (casterUnit && _spellInfo->HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DAMAGE)) + { + double comboDamage = CalcPointsPerResource(casterUnit); + if (comboDamage != 0.0) + if (uint8 comboPoints = casterUnit->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints()) + value += comboDamage * comboPoints; + } + + if (_spellInfo->HasAttribute(SPELL_ATTR8_MASTERY_AFFECTS_POINTS)) + if (Player const* playerCaster = caster ? caster->ToPlayer() : nullptr) + value += playerCaster->GetFloatValue(PLAYER_MASTERY) * BonusMultiplier; + + if (casterUnit) + value = casterUnit->ApplyEffectModifiers(_spellInfo, _effIndex, value); + + return int32(round(value)); +} + +int32 SpellEffectInfo::CalcBaseValue(WorldObject const* caster, Unit const* target) const +{ + if (Scaling.Coefficient != 0.0f) + { + uint32 level = _spellInfo->SpellLevel; + if (target && _spellInfo->HasAttribute(SPELL_ATTR8_USE_TARGETS_LEVEL_FOR_SPELL_SCALING)) + level = target->getLevel(); + else if (caster && caster->IsUnit()) + level = caster->ToUnit()->getLevel(); + + if (_spellInfo->BaseLevel && _spellInfo->HasAttribute(SPELL_ATTR10_USE_SPELL_BASE_LEVEL_FOR_SCALING)) + level = _spellInfo->BaseLevel; + + float value = 0.0f; + + if (level > 0) + { + if (!_spellInfo->Scaling.Class) + return 0; + + value = GameTables::GetSpellScalingValue(level, static_cast(_spellInfo->Scaling.Class > 0 ? _spellInfo->Scaling.Class: (MAX_CLASSES - 1 /*last class*/) - _spellInfo->Scaling.Class)); + value *= _spellInfo->GetSpellScalingMultiplier(level, false); + } + + value *= Scaling.Coefficient; + if (value > 0.0f) + value = std::max(value, 1.0f); + + return int32(round(value)); + } + else + { + float value = BasePoints; + + if (ScalesWithCreatureLevel()) + { + // Some spells use the internal game table to calculate scaling coefficients for creatures + if (Creature const* creatureCaster = Object::ToCreature(caster)) + { + uint8 casterClass = creatureCaster->getClass(); + uint8 expansion = creatureCaster->GetCreatureTemplate()->GetHealthScalingExpansion(); + + float spellDamage = GetGameTableColumnForClass(sNpcDamageByClassGameTable[expansion].GetRow(_spellInfo->SpellLevel), casterClass); + float creatureDamage = GetGameTableColumnForClass(sNpcDamageByClassGameTable[expansion].GetRow(creatureCaster->getLevel()), casterClass); + + if (spellDamage != 0.0f) + value *= creatureDamage / spellDamage; + } + } + + return int32(round(value)); + } +} + +// Based on client function QuestTextParser::Spell_C_GetPointsPerResource (build 15595) +double SpellEffectInfo::CalcPointsPerResource(WorldObject const* caster) const +{ + double value = 0.0; + if (caster && caster->IsUnit()) + { + if (Scaling.Coefficient != 0.0f) + { + if (!_spellInfo->Scaling.Class) + return 0.0; + + int32 scalingClass = _spellInfo->Scaling.Class; + if (scalingClass < 0) + scalingClass = (MAX_CLASSES - 1 /*last class*/) - scalingClass; + + float scaledAmount = GameTables::GetSpellScalingValue(caster->ToUnit()->getLevel(), static_cast(scalingClass)); + scaledAmount *= _spellInfo->GetSpellScalingMultiplier(caster->ToUnit()->getLevel(), false) * Scaling.ComboPointsCoefficient; + value = scaledAmount; + } + } + + if (std::fabs(value) < 0.00000023841858 && PointsPerComboPoint != 0.0f) + return PointsPerComboPoint; + + return value; +} + +float SpellEffectInfo::CalcValueMultiplier(WorldObject* caster, Spell* spell) const +{ + float multiplier = Amplitude; + if (Player* modOwner = (caster ? caster->GetSpellModOwner() : nullptr)) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::Amplitude, multiplier, spell); + return multiplier; +} + +float SpellEffectInfo::CalcDamageMultiplier(WorldObject* caster, Spell* spell) const +{ + float multiplierPercent = DamageMultiplier * 100.0f; + if (Player* modOwner = (caster ? caster->GetSpellModOwner() : nullptr)) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::ChainAmplitude, multiplierPercent, spell); + return multiplierPercent / 100.0f; +} + +bool SpellEffectInfo::HasRadius(SpellTargetIndex targetIndex) const +{ + switch (targetIndex) + { + case SpellTargetIndex::TargetA: + return TargetARadiusEntry != nullptr; + case SpellTargetIndex::TargetB: + return TargetBRadiusEntry != nullptr; + default: + return false; + } +} + +float SpellEffectInfo::CalcRadius(WorldObject* caster /*= nullptr*/, SpellTargetIndex targetIndex /*=SpellTargetIndex::TargetA*/, Spell* spell /*= nullptr*/) const +{ + // TargetA -> TargetARadiusEntry + // TargetB -> TargetBRadiusEntry + // Aura effects have TargetARadiusEntry == TargetBRadiusEntry (mostly) + SpellImplicitTargetInfo target = TargetA; + SpellRadiusEntry const* entry = TargetARadiusEntry; + if (targetIndex == SpellTargetIndex::TargetB && HasRadius(targetIndex)) + { + target = TargetB; + entry = TargetBRadiusEntry; + } + + if (!entry) + return 0.0f; + + float radius = entry->RadiusMin; + + // Random targets use random value between RadiusMin and RadiusMax + // For other cases, client uses RadiusMax if RadiusMin is 0 + if (target.GetTarget() == TARGET_DEST_CASTER_RANDOM || + target.GetTarget() == TARGET_DEST_TARGET_RANDOM || + target.GetTarget() == TARGET_DEST_DEST_RANDOM) + radius += (entry->RadiusMax - radius) * std::sqrt(rand_norm()); + else if (radius == 0.0f) + radius = entry->RadiusMax; + + if (caster) + { + if (Unit* casterUnit = caster->ToUnit()) + radius += entry->RadiusPerLevel * casterUnit->getLevel(); + + radius = std::min(radius, entry->RadiusMax); + + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::Radius, radius, spell); + } + + return radius; +} + +uint32 SpellEffectInfo::GetProvidedTargetMask() const +{ + return GetTargetFlagMask(TargetA.GetObjectType()) | GetTargetFlagMask(TargetB.GetObjectType()); +} + +uint32 SpellEffectInfo::GetMissingTargetMask(bool srcSet /*= false*/, bool dstSet /*= false*/, uint32 mask /*=0*/) const +{ + uint32 effImplicitTargetMask = GetTargetFlagMask(GetUsedTargetObjectType()); + uint32 providedTargetMask = GetProvidedTargetMask() | mask; + + // remove all flags covered by effect target mask + if (providedTargetMask & TARGET_FLAG_UNIT_MASK) + effImplicitTargetMask &= ~(TARGET_FLAG_UNIT_MASK); + if (providedTargetMask & TARGET_FLAG_CORPSE_MASK) + effImplicitTargetMask &= ~(TARGET_FLAG_UNIT_MASK | TARGET_FLAG_CORPSE_MASK); + if (providedTargetMask & TARGET_FLAG_GAMEOBJECT_ITEM) + effImplicitTargetMask &= ~(TARGET_FLAG_GAMEOBJECT_ITEM | TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_ITEM); + if (providedTargetMask & TARGET_FLAG_GAMEOBJECT) + effImplicitTargetMask &= ~(TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_GAMEOBJECT_ITEM); + if (providedTargetMask & TARGET_FLAG_ITEM) + effImplicitTargetMask &= ~(TARGET_FLAG_ITEM | TARGET_FLAG_GAMEOBJECT_ITEM); + if (dstSet || providedTargetMask & TARGET_FLAG_DEST_LOCATION) + effImplicitTargetMask &= ~(TARGET_FLAG_DEST_LOCATION); + if (srcSet || providedTargetMask & TARGET_FLAG_SOURCE_LOCATION) + effImplicitTargetMask &= ~(TARGET_FLAG_SOURCE_LOCATION); + + return effImplicitTargetMask; +} + +SpellEffectImplicitTargetTypes SpellEffectInfo::GetImplicitTargetType() const +{ + return _data[Effect].ImplicitTargetType; +} + +SpellTargetObjectTypes SpellEffectInfo::GetUsedTargetObjectType() const +{ + return _data[Effect].UsedTargetObjectType; +} + +SpellEffectInfo::StaticData SpellEffectInfo::_data[TOTAL_SPELL_EFFECTS] = +{ + // implicit target type used target object type + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 0 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 1 SPELL_EFFECT_INSTAKILL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 2 SPELL_EFFECT_SCHOOL_DAMAGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 3 SPELL_EFFECT_DUMMY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 4 SPELL_EFFECT_PORTAL_TELEPORT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 5 SPELL_EFFECT_TELEPORT_UNITS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 6 SPELL_EFFECT_APPLY_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 7 SPELL_EFFECT_ENVIRONMENTAL_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 8 SPELL_EFFECT_POWER_DRAIN + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 9 SPELL_EFFECT_HEALTH_LEECH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 10 SPELL_EFFECT_HEAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 11 SPELL_EFFECT_BIND + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 12 SPELL_EFFECT_PORTAL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 13 SPELL_EFFECT_RITUAL_BASE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 14 SPELL_EFFECT_RITUAL_SPECIALIZE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 15 SPELL_EFFECT_RITUAL_ACTIVATE_PORTAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 16 SPELL_EFFECT_QUEST_COMPLETE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 17 SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 18 SPELL_EFFECT_RESURRECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 19 SPELL_EFFECT_ADD_EXTRA_ATTACKS + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 20 SPELL_EFFECT_DODGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 21 SPELL_EFFECT_EVADE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 22 SPELL_EFFECT_PARRY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 23 SPELL_EFFECT_BLOCK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 24 SPELL_EFFECT_CREATE_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 25 SPELL_EFFECT_WEAPON + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 26 SPELL_EFFECT_DEFENSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 27 SPELL_EFFECT_PERSISTENT_AREA_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 28 SPELL_EFFECT_SUMMON + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 29 SPELL_EFFECT_LEAP + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 30 SPELL_EFFECT_ENERGIZE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 31 SPELL_EFFECT_WEAPON_PERCENT_DAMAGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 32 SPELL_EFFECT_TRIGGER_MISSILE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ_ITEM}, // 33 SPELL_EFFECT_OPEN_LOCK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 34 SPELL_EFFECT_SUMMON_CHANGE_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 35 SPELL_EFFECT_APPLY_AREA_AURA_PARTY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 36 SPELL_EFFECT_LEARN_SPELL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 37 SPELL_EFFECT_SPELL_DEFENSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 38 SPELL_EFFECT_DISPEL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 39 SPELL_EFFECT_LANGUAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 40 SPELL_EFFECT_DUAL_WIELD + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 41 SPELL_EFFECT_JUMP + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 42 SPELL_EFFECT_JUMP_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 43 SPELL_EFFECT_TELEPORT_UNITS_FACE_CASTER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 44 SPELL_EFFECT_SKILL_STEP + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 45 SPELL_EFFECT_ADD_HONOR + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 46 SPELL_EFFECT_SPAWN + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 47 SPELL_EFFECT_TRADE_SKILL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 48 SPELL_EFFECT_STEALTH + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 49 SPELL_EFFECT_DETECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 50 SPELL_EFFECT_TRANS_DOOR + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 51 SPELL_EFFECT_FORCE_CRITICAL_HIT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 52 SPELL_EFFECT_GUARANTEE_HIT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 53 SPELL_EFFECT_ENCHANT_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 54 SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 55 SPELL_EFFECT_TAMECREATURE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 56 SPELL_EFFECT_SUMMON_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 57 SPELL_EFFECT_LEARN_PET_SPELL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 58 SPELL_EFFECT_WEAPON_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 59 SPELL_EFFECT_CREATE_RANDOM_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 60 SPELL_EFFECT_PROFICIENCY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 61 SPELL_EFFECT_SEND_EVENT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 62 SPELL_EFFECT_POWER_BURN + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 63 SPELL_EFFECT_THREAT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 64 SPELL_EFFECT_TRIGGER_SPELL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 65 SPELL_EFFECT_APPLY_AREA_AURA_RAID + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 66 SPELL_EFFECT_CREATE_MANA_GEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 67 SPELL_EFFECT_HEAL_MAX_HEALTH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 68 SPELL_EFFECT_INTERRUPT_CAST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 69 SPELL_EFFECT_DISTRACT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 70 SPELL_EFFECT_PULL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 71 SPELL_EFFECT_PICKPOCKET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 72 SPELL_EFFECT_ADD_FARSIGHT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 73 SPELL_EFFECT_UNTRAIN_TALENTS + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 74 SPELL_EFFECT_APPLY_GLYPH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 75 SPELL_EFFECT_HEAL_MECHANICAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 76 SPELL_EFFECT_SUMMON_OBJECT_WILD + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 77 SPELL_EFFECT_SCRIPT_EFFECT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 78 SPELL_EFFECT_ATTACK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 79 SPELL_EFFECT_SANCTUARY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 80 SPELL_EFFECT_ADD_COMBO_POINTS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 81 SPELL_EFFECT_CREATE_HOUSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 82 SPELL_EFFECT_BIND_SIGHT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 83 SPELL_EFFECT_DUEL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 84 SPELL_EFFECT_STUCK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 85 SPELL_EFFECT_SUMMON_PLAYER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 86 SPELL_EFFECT_ACTIVATE_OBJECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 87 SPELL_EFFECT_GAMEOBJECT_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 88 SPELL_EFFECT_GAMEOBJECT_REPAIR + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 89 SPELL_EFFECT_GAMEOBJECT_SET_DESTRUCTION_STATE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 90 SPELL_EFFECT_KILL_CREDIT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 91 SPELL_EFFECT_THREAT_ALL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 92 SPELL_EFFECT_ENCHANT_HELD_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 93 SPELL_EFFECT_FORCE_DESELECT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 94 SPELL_EFFECT_SELF_RESURRECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 95 SPELL_EFFECT_SKINNING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 96 SPELL_EFFECT_CHARGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 97 SPELL_EFFECT_CAST_BUTTON + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 98 SPELL_EFFECT_KNOCK_BACK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 99 SPELL_EFFECT_DISENCHANT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 100 SPELL_EFFECT_INEBRIATE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 101 SPELL_EFFECT_FEED_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 102 SPELL_EFFECT_DISMISS_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 103 SPELL_EFFECT_REPUTATION + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 104 SPELL_EFFECT_SUMMON_OBJECT_SLOT1 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 105 SPELL_EFFECT_SUMMON_OBJECT_SLOT2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 106 SPELL_EFFECT_SUMMON_OBJECT_SLOT3 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 107 SPELL_EFFECT_SUMMON_OBJECT_SLOT4 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 108 SPELL_EFFECT_DISPEL_MECHANIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 109 SPELL_EFFECT_SUMMON_DEAD_PET + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 110 SPELL_EFFECT_DESTROY_ALL_TOTEMS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 111 SPELL_EFFECT_DURABILITY_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 112 SPELL_EFFECT_112 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 113 SPELL_EFFECT_RESURRECT_NEW + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 114 SPELL_EFFECT_ATTACK_ME + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 115 SPELL_EFFECT_DURABILITY_DAMAGE_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ENEMY}, // 116 SPELL_EFFECT_SKIN_PLAYER_CORPSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 117 SPELL_EFFECT_SPIRIT_HEAL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 118 SPELL_EFFECT_SKILL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 119 SPELL_EFFECT_APPLY_AREA_AURA_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 120 SPELL_EFFECT_TELEPORT_GRAVEYARD + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 121 SPELL_EFFECT_NORMALIZED_WEAPON_DMG + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 122 SPELL_EFFECT_122 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 123 SPELL_EFFECT_SEND_TAXI + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 124 SPELL_EFFECT_PULL_TOWARDS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 125 SPELL_EFFECT_MODIFY_THREAT_PERCENT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 126 SPELL_EFFECT_STEAL_BENEFICIAL_BUFF + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 127 SPELL_EFFECT_PROSPECTING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 128 SPELL_EFFECT_APPLY_AREA_AURA_FRIEND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 129 SPELL_EFFECT_APPLY_AREA_AURA_ENEMY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 130 SPELL_EFFECT_REDIRECT_THREAT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 131 SPELL_EFFECT_PLAY_SOUND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 132 SPELL_EFFECT_PLAY_MUSIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 133 SPELL_EFFECT_UNLEARN_SPECIALIZATION + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 134 SPELL_EFFECT_KILL_CREDIT2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 135 SPELL_EFFECT_CALL_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 136 SPELL_EFFECT_HEAL_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 137 SPELL_EFFECT_ENERGIZE_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 138 SPELL_EFFECT_LEAP_BACK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 139 SPELL_EFFECT_CLEAR_QUEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 140 SPELL_EFFECT_FORCE_CAST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 141 SPELL_EFFECT_FORCE_CAST_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 142 SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 143 SPELL_EFFECT_APPLY_AREA_AURA_OWNER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 144 SPELL_EFFECT_KNOCK_BACK_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 145 SPELL_EFFECT_PULL_TOWARDS_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 146 SPELL_EFFECT_ACTIVATE_RUNE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 147 SPELL_EFFECT_QUEST_FAIL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 148 SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 149 SPELL_EFFECT_CHARGE_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 150 SPELL_EFFECT_QUEST_START + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 151 SPELL_EFFECT_TRIGGER_SPELL_2 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 152 SPELL_EFFECT_SUMMON_RAF_FRIEND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 153 SPELL_EFFECT_CREATE_TAMED_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 154 SPELL_EFFECT_DISCOVER_TAXI + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 155 SPELL_EFFECT_TITAN_GRIP + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 156 SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 157 SPELL_EFFECT_CREATE_ITEM_2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 158 SPELL_EFFECT_MILLING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 159 SPELL_EFFECT_ALLOW_RENAME_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 160 SPELL_EFFECT_FORCE_CAST_2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 161 SPELL_EFFECT_TALENT_SPEC_COUNT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 162 SPELL_EFFECT_TALENT_SPEC_SELECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 163 SPELL_EFFECT_163 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 164 SPELL_EFFECT_REMOVE_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 165 SPELL_EFFECT_165 + {EFFECT_IMPLICIT_TARGET_CASTER, TARGET_OBJECT_TYPE_UNIT}, // 166 SPELL_EFFECT_GIVE_CURRENCY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 167 SPELL_EFFECT_167 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 168 SPELL_EFFECT_168 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_ITEM}, // 169 SPELL_EFFECT_DESTROY_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 170 SPELL_EFFECT_170 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 171 SPELL_EFFECT_SUMMON_PERSONAL_GAMEOBJECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 172 SPELL_EFFECT_RESURRECT_WITH_AURA + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 173 SPELL_EFFECT_UNLOCK_GUILD_VAULT_TAB + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 174 SPELL_EFFECT_APPLY_AURA_2 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 175 SPELL_EFFECT_175 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 176 SPELL_EFFECT_176 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 177 SPELL_EFFECT_177 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 178 SPELL_EFFECT_178 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 179 SPELL_EFFECT_CREATE_AREATRIGGER + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 180 SPELL_EFFECT_180 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 181 SPELL_EFFECT_181 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 182 SPELL_EFFECT_182 +}; + +SpellInfo::SpellInfo(SpellEntry const* spellEntry, SpellEffectEntry const** effects) +{ + Id = spellEntry->ID; + Attributes = spellEntry->Attributes; + AttributesEx = spellEntry->AttributesEx; + AttributesEx2 = spellEntry->AttributesEx2; + AttributesEx3 = spellEntry->AttributesEx3; + AttributesEx4 = spellEntry->AttributesEx4; + AttributesEx5 = spellEntry->AttributesEx5; + AttributesEx6 = spellEntry->AttributesEx6; + AttributesEx7 = spellEntry->AttributesEx7; + AttributesEx8 = spellEntry->AttributesEx8; + AttributesEx9 = spellEntry->AttributesEx9; + AttributesEx10 = spellEntry->AttributesEx10; + AttributesCu = 0; + CastTimeEntry = spellEntry->CastingTimeIndex ? sSpellCastTimesStore.LookupEntry(spellEntry->CastingTimeIndex) : nullptr; + DurationEntry = spellEntry->DurationIndex ? sSpellDurationStore.LookupEntry(spellEntry->DurationIndex) : nullptr; + PowerType = spellEntry->PowerType; + RangeEntry = spellEntry->RangeIndex ? sSpellRangeStore.LookupEntry(spellEntry->RangeIndex) : nullptr; + Speed = spellEntry->Speed; + + for (uint8 i = 0; i < 2; ++i) + SpellVisual[i] = spellEntry->SpellVisualID[i]; + + SpellIconID = spellEntry->SpellIconID; + ActiveIconID = spellEntry->ActiveIconID; + SpellName = spellEntry->Name; + Rank = spellEntry->NameSubtext; + SchoolMask = spellEntry->SchoolMask; + RuneCostID = spellEntry->RuneCostID; + SpellDifficultyId = spellEntry->Difficulty; + BonusCoefficientFromAP = spellEntry->BonusCoefficient; + SpellScalingId = spellEntry->ScalingID; + SpellAuraOptionsId = spellEntry->AuraOptionsID; + SpellAuraRestrictionsId = spellEntry->AuraRestrictionsID; + SpellCastingRequirementsId = spellEntry->CastingRequirementsID; + SpellCategoriesId = spellEntry->CategoriesID; + SpellClassOptionsId = spellEntry->ClassOptionsID; + SpellCooldownsId = spellEntry->CooldownsID; + SpellEquippedItemsId = spellEntry->EquippedItemsID; + SpellInterruptsId = spellEntry->InterruptsID; + SpellLevelsId = spellEntry->LevelsID; + SpellPowerId = spellEntry->PowerDisplayID; + SpellReagentsId = spellEntry->ReagentsID; + SpellShapeshiftId = spellEntry->ShapeshiftID; + SpellTargetRestrictionsId = spellEntry->TargetRestrictionsID; + SpellTotemsId = spellEntry->TotemsID; + ResearchProjectId = spellEntry->RequiredProjectID; + + // SpellDifficultyEntry + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + Effects[i] = SpellEffectInfo(spellEntry, this, i, effects[i]); + + // SpellScalingEntry + SpellScalingEntry const* _scaling = GetSpellScaling(); + Scaling.CastTimeMin = _scaling ? _scaling->CastTimeMin : 0; + Scaling.CastTimeMax = _scaling ? _scaling->CastTimeMax : 0; + Scaling.CastTimeMaxLevel = _scaling ? _scaling->CastTimeMaxLevel : 0; + Scaling.Class = _scaling ? _scaling->Class : 0; + Scaling.NerfFactor = _scaling ? _scaling->NerfFactor : 0.f; + Scaling.NerfMaxLevel = _scaling ? _scaling->NerfMaxLevel : 0; + + // SpellAuraOptionsEntry + SpellAuraOptionsEntry const* _options = GetSpellAuraOptions(); + ProcFlags = _options ? ::ProcFlags(_options->ProcTypeMask) : PROC_FLAG_NONE; + ProcChance = _options ? _options->ProcChance : 0; + ProcCharges = _options ? _options->ProcCharges : 0; + StackAmount = _options ? _options->CumulativeAura : 0; + + // SpellAuraRestrictionsEntry + SpellAuraRestrictionsEntry const* _aura = GetSpellAuraRestrictions(); + CasterAuraState = _aura ? _aura->CasterAuraState : 0; + TargetAuraState = _aura ? _aura->TargetAuraState : 0; + CasterAuraStateNot = _aura ? _aura->ExcludeCasterAuraState : 0; + TargetAuraStateNot = _aura ? _aura->ExcludeTargetAuraState : 0; + CasterAuraSpell = _aura ? _aura->CasterAuraSpell : 0; + TargetAuraSpell = _aura ? _aura->TargetAuraSpell : 0; + ExcludeCasterAuraSpell = _aura ? _aura->ExcludeCasterAuraSpell : 0; + ExcludeTargetAuraSpell = _aura ? _aura->ExcludeTargetAuraSpell : 0; + + // SpellCastingRequirementsEntry + SpellCastingRequirementsEntry const* _castreq = GetSpellCastingRequirements(); + RequiresSpellFocus = _castreq ? _castreq->RequiresSpellFocus : 0; + FacingCasterFlags = _castreq ? SpellFacingCasterFlags(_castreq->FacingCasterFlags) : SpellFacingCasterFlags::None; + MinFactionId = _castreq ? _castreq->MinFactionID : 0; + MinReputation = _castreq ? _castreq->MinReputation : 0; + AreaGroupId = _castreq ? _castreq->RequiredAreasID : -1; + + // SpellCategoriesEntry + SpellCategoriesEntry const* _categorie = GetSpellCategories(); + CategoryEntry = _categorie ? sSpellCategoryStore.LookupEntry(_categorie->Category) : nullptr; + Dispel = _categorie ? _categorie->DispelType : 0; + Mechanic = _categorie ? _categorie->Mechanic : 0; + StartRecoveryCategory = _categorie ? _categorie->StartRecoveryCategory : 0; + DmgClass = _categorie ? _categorie->DefenseType : 0; + PreventionType = _categorie ? _categorie->PreventionType : 0; + + // SpellClassOptionsEntry + SpellClassOptionsEntry const* _class = GetSpellClassOptions(); + SpellFamilyName = _class ? _class->SpellClassSet : 0; + SpellFamilyFlags = _class ? _class->SpellFamilyMask : flag96(0); + + // SpellCooldownsEntry + SpellCooldownsEntry const* _cooldowns = GetSpellCooldowns(); + RecoveryTime = _cooldowns ? _cooldowns->RecoveryTime : 0; + CategoryRecoveryTime = _cooldowns ? _cooldowns->CategoryRecoveryTime : 0; + StartRecoveryTime = _cooldowns ? _cooldowns->StartRecoveryTime : 0; + + // SpellEquippedItemsEntry + SpellEquippedItemsEntry const* _equipped = GetSpellEquippedItems(); + EquippedItemClass = _equipped ? _equipped->EquippedItemClass : -1; + EquippedItemSubClassMask = _equipped ?_equipped->EquippedItemSubclass : -1; + EquippedItemInventoryTypeMask = _equipped ? _equipped->EquippedItemInvTypes : -1; + + // SpellInterruptsEntry + SpellInterruptsEntry const* _interrupt = GetSpellInterrupts(); + + AuraInterruptFlags = _interrupt ? SpellAuraInterruptFlags(_interrupt->AuraInterruptFlags[0]) : SpellAuraInterruptFlags::None; + AuraInterruptFlags2 = _interrupt? SpellAuraInterruptFlags2(_interrupt->AuraInterruptFlags[1]) : SpellAuraInterruptFlags2::None; + ChannelInterruptFlags = _interrupt ? SpellAuraInterruptFlags(_interrupt->ChannelInterruptFlags[0] ) : SpellAuraInterruptFlags::None; + ChannelInterruptFlags2 = _interrupt ? SpellAuraInterruptFlags2(_interrupt->ChannelInterruptFlags[1]) : SpellAuraInterruptFlags2::None; + InterruptFlags = _interrupt ? SpellInterruptFlags(_interrupt->InterruptFlags) : SpellInterruptFlags::None; + + // SpellLevelsEntry + SpellLevelsEntry const* _levels = GetSpellLevels(); + MaxLevel = _levels ? _levels->MaxLevel : 0; + BaseLevel = _levels ? _levels->BaseLevel : 0; + SpellLevel = _levels ? _levels->SpellLevel : 0; + + // SpellPowerEntry + SpellPowerEntry const* _power = GetSpellPower(); + ManaCost = _power ? _power->ManaCost : 0; + ManaCostPerlevel = _power ? _power->ManaCostPerLevel : 0; + ManaCostPercentage = _power ? _power->PowerCostPct : 0; + ManaCostPercentage2 = _power ? _power->PowerCostPct2 : 0.f; + ManaPerSecond = _power ? _power->ManaPerSecond : 0; + + // SpellReagentsEntry + SpellReagentsEntry const* _reagents = GetSpellReagents(); + for (uint8 i = 0; i < MAX_SPELL_REAGENTS; ++i) + Reagent[i] = _reagents ? _reagents->Reagents[i] : 0; + for (uint8 i = 0; i < MAX_SPELL_REAGENTS; ++i) + ReagentCount[i] = _reagents ? _reagents->ReagentCount[i] : 0; + + // SpellShapeshiftEntry + if (SpellShapeshiftEntry const* _shapeshift = GetSpellShapeshift()) + { + Stances = MAKE_PAIR64(_shapeshift->ShapeshiftMask[0], _shapeshift->ShapeshiftMask[1]); + StancesNot = MAKE_PAIR64(_shapeshift->ShapeshiftExclude[0], _shapeshift->ShapeshiftExclude[1]); + } + else + { + Stances = UI64LIT(0); + StancesNot = UI64LIT(0); + } + + // SpellTargetRestrictionsEntry + SpellTargetRestrictionsEntry const* _target = GetSpellTargetRestrictions(); + ConeAngle = _target ? _target->ConeAngle : 0.f; + Targets = _target ? _target->Targets : 0; + TargetCreatureType = _target ? _target->TargetCreatureType : 0; + MaxAffectedTargets = _target ? _target->MaxTargets : 0; + MaxTargetLevel = _target ? _target->MaxTargetLevel : 0; + + // SpellTotemsEntry + SpellTotemsEntry const* _totem = GetSpellTotems(); + for (uint8 i = 0; i < 2; ++i) + TotemCategory[i] = _totem ? _totem->RequiredTotemCategoryID[i] : 0; + for (uint8 i = 0; i < 2; ++i) + Totem[i] = _totem ? _totem->Totem[i] : 0; + + ChainEntry = nullptr; + ExplicitTargetMask = 0; + + _spellSpecific = SPELL_SPECIFIC_NORMAL; + _auraState = AURA_STATE_NONE; + + _allowedMechanicMask = 0; +} + +SpellInfo::~SpellInfo() +{ + _UnloadImplicitTargetConditionLists(); +} + +uint32 SpellInfo::GetCategory() const +{ + return CategoryEntry ? CategoryEntry->ID : 0; +} + +bool SpellInfo::HasEffect(SpellEffects effect) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect(effect)) + return true; + return false; +} + +bool SpellInfo::HasAura(AuraType aura) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAura(aura)) + return true; + return false; +} + +bool SpellInfo::HasAreaAuraEffect() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAreaAuraEffect()) + return true; + return false; +} + +bool SpellInfo::HasOnlyDamageEffects() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].IsEffect()) + { + switch (Effects[i].Effect) + { + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_HEALTH_LEECH: + continue; + default: + return false; + } + } + } + + return true; +} + +bool SpellInfo::HasTargetType(::Targets target) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect()) + continue; + + if (Effects[i].TargetA.GetTarget() == target || Effects[i].TargetB.GetTarget() == target) + return true; + } + + return false; +} + +bool SpellInfo::CanBeInterrupted(Unit const* interruptTarget, bool ignoreImmunity /*= false*/) const +{ + return HasAttribute(SPELL_ATTR7_NO_UI_NOT_INTERRUPTIBLE) + || HasChannelInterruptFlag(SpellAuraInterruptFlags::Damage | SpellAuraInterruptFlags::EnteringCombat) + || (interruptTarget->IsPlayer() && InterruptFlags.HasFlag(SpellInterruptFlags::DamageCancelsPlayerOnly)) + || ((!(interruptTarget->GetMechanicImmunityMask() & (1 << MECHANIC_INTERRUPT)) || ignoreImmunity) + && PreventionType & SPELL_PREVENTION_TYPE_SILENCE); +} + +bool SpellInfo::HasAnyAuraInterruptFlag() const +{ + return AuraInterruptFlags != SpellAuraInterruptFlags::None || AuraInterruptFlags2 != SpellAuraInterruptFlags2::None; +} + +bool SpellInfo::IsExplicitDiscovery() const +{ + return ((Effects[0].Effect == SPELL_EFFECT_CREATE_RANDOM_ITEM + || Effects[0].Effect == SPELL_EFFECT_CREATE_ITEM_2) + && Effects[1].Effect == SPELL_EFFECT_SCRIPT_EFFECT) + || Id == 64323; +} + +bool SpellInfo::IsLootCrafting() const +{ + return (Effects[0].Effect == SPELL_EFFECT_CREATE_RANDOM_ITEM || + // different random cards from Inscription (121==Virtuoso Inking Set category) r without explicit item + (Effects[0].Effect == SPELL_EFFECT_CREATE_ITEM_2 && + ((TotemCategory[0] != 0 || (Totem[0] != 0 && SpellIconID == 1)) || Effects[0].ItemType == 0))); +} + +bool SpellInfo::IsQuestTame() const +{ + return Effects[0].Effect == SPELL_EFFECT_THREAT && Effects[1].Effect == SPELL_EFFECT_APPLY_AURA && Effects[1].ApplyAuraName == SPELL_AURA_DUMMY; +} + +bool SpellInfo::IsProfessionOrRiding() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsProfessionOrRidingSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsProfession() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsProfessionSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsPrimaryProfession() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsPrimaryProfessionSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsPrimaryProfessionFirstRank() const +{ + return IsPrimaryProfession() && GetRank() == 1; +} + +bool SpellInfo::IsAbilityLearnedWithProfession() const +{ + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + { + SkillLineAbilityEntry const* pAbility = _spell_idx->second; + if (!pAbility || pAbility->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticSkillRank) + continue; + + if (pAbility->MinSkillLineRank > 0) + return true; + } + + return false; +} + +bool SpellInfo::IsAbilityOfSkillType(uint32 skillType) const +{ + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + if (_spell_idx->second->SkillLine == uint32(skillType)) + return true; + + return false; +} + +bool SpellInfo::IsAffectingArea() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && (Effects[i].IsTargetingArea() || Effects[i].IsEffect(SPELL_EFFECT_PERSISTENT_AREA_AURA) || Effects[i].IsAreaAuraEffect())) + return true; + return false; +} + +// checks if spell targets are selected from area, doesn't include spell effects in check (like area wide auras for example) +bool SpellInfo::IsTargetingArea() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].IsTargetingArea()) + return true; + return false; +} + +bool SpellInfo::NeedsExplicitUnitTarget() const +{ + return (GetExplicitTargetMask() & TARGET_FLAG_UNIT_MASK) != 0; +} + +bool SpellInfo::NeedsToBeTriggeredByCaster(SpellInfo const* triggeringSpell) const +{ + if (NeedsExplicitUnitTarget()) + return true; + + /* + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].IsEffect()) + { + if (Effects[i].TargetA.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CHANNEL + || Effects[i].TargetB.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CHANNEL) + return true; + } + } + */ + + if (triggeringSpell->IsChanneled()) + { + uint32 mask = 0; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER && Effects[i].TargetA.GetTarget() != TARGET_DEST_CASTER + && Effects[i].TargetB.GetTarget() != TARGET_UNIT_CASTER && Effects[i].TargetB.GetTarget() != TARGET_DEST_CASTER) + { + mask |= Effects[i].GetProvidedTargetMask(); + } + } + + if (mask & TARGET_FLAG_UNIT_MASK) + return true; + } + + return false; +} + +bool SpellInfo::IsSelfCast() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Effect && Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER) + return false; + return true; +} + +bool SpellInfo::IsPassive() const +{ + return HasAttribute(SPELL_ATTR0_PASSIVE); +} + +bool SpellInfo::IsAutocastable() const +{ + if (IsPassive()) + return false; + if (HasAttribute(SPELL_ATTR1_NO_AUTOCAST_AI)) + return false; + return true; +} + +bool SpellInfo::IsStackableWithRanks() const +{ + if (IsPassive()) + return false; + if (PowerType != POWER_MANA && PowerType != POWER_HEALTH) + return false; + if (IsProfessionOrRiding()) + return false; + + if (IsAbilityLearnedWithProfession()) + return false; + + // All stance spells. if any better way, change it. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (SpellFamilyName) + { + case SPELLFAMILY_PALADIN: + // Paladin aura Spell + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID) + return false; + break; + case SPELLFAMILY_DRUID: + // Druid form Spell + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA && + Effects[i].ApplyAuraName == SPELL_AURA_MOD_SHAPESHIFT) + return false; + break; + } + } + return true; +} + +bool SpellInfo::IsPassiveStackableWithRanks() const +{ + return IsPassive() && !HasEffect(SPELL_EFFECT_APPLY_AURA); +} + +bool SpellInfo::IsMultiSlotAura() const +{ + if (IsPassive() || HasAura(SPELL_AURA_CONTROL_VEHICLE)) + return true; + + switch (Id) + { + case 55849: // Power Spark + case 40075: // Fel Flak Fire + case 44413: // Incanter's Absorption + case 79010: // Point of Vulnerability + case 79920: // Shared Health + return true; + default: + return false; + } +} + +bool SpellInfo::IsStackableOnOneSlotWithDifferentCasters() const +{ + /// TODO: Re-verify meaning of SPELL_ATTR3_STACK_FOR_DIFF_CASTERS and update conditions here + return StackAmount > 1 && !IsChanneled() && !HasAttribute(SPELL_ATTR3_DOT_STACKING_RULE); +} + +bool SpellInfo::IsCooldownStartedOnEvent() const +{ + if (HasAttribute(SPELL_ATTR0_COOLDOWN_ON_EVENT)) + return true; + + return CategoryEntry && CategoryEntry->GetFlags().HasFlag(SpellCategoryFlags::CooldownEventOnLeaveCombat); +} + +bool SpellInfo::IsDeathPersistent() const +{ + return HasAttribute(SPELL_ATTR3_ALLOW_AURA_WHILE_DEAD); +} + +bool SpellInfo::IsRequiringDeadTarget() const +{ + return HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS); +} + +bool SpellInfo::IsAllowingDeadTarget() const +{ + if (HasAttribute(SPELL_ATTR2_ALLOW_DEAD_TARGET) || Targets & (TARGET_FLAG_CORPSE_ALLY | TARGET_FLAG_CORPSE_ENEMY | TARGET_FLAG_UNIT_DEAD)) + return true; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].TargetA.GetTarget() == TARGET_CORPSE_SRC_AREA_RAID) + return true; + + return false; +} + +bool SpellInfo::IsGroupBuff() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (Effects[i].TargetA.GetCheckType()) + { + case TARGET_CHECK_PARTY: + case TARGET_CHECK_RAID: + case TARGET_CHECK_RAID_CLASS: + return true; + default: + break; + } + } + + return false; +} + +bool SpellInfo::CanBeUsedInCombat() const +{ + return !HasAttribute(SPELL_ATTR0_NOT_IN_COMBAT_ONLY_PEACEFUL); +} + +bool SpellInfo::IsPositive() const +{ + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE); +} + +bool SpellInfo::IsPositiveEffect(uint8 effIndex) const +{ + switch (effIndex) + { + default: + case 0: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF0); + case 1: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF1); + case 2: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF2); + } +} + +bool SpellInfo::IsChanneled() const +{ + return HasAttribute(SpellAttr1(SPELL_ATTR1_IS_CHANNELLED | SPELL_ATTR1_IS_SELF_CHANNELLED)); +} + +bool SpellInfo::IsMoveAllowedChannel() const +{ + return IsChanneled() && !ChannelInterruptFlags.HasFlag(SpellAuraInterruptFlags::Moving | SpellAuraInterruptFlags::Turning); +} + +bool SpellInfo::NeedsComboPoints() const +{ + return HasAttribute(SpellAttr1(SPELL_ATTR1_FINISHING_MOVE_DAMAGE | SPELL_ATTR1_FINISHING_MOVE_DURATION)); +} + +bool SpellInfo::IsNextMeleeSwingSpell() const +{ + return HasAttribute(SpellAttr0(SPELL_ATTR0_ON_NEXT_SWING_NO_DAMAGE | SPELL_ATTR0_ON_NEXT_SWING)); +} + +bool SpellInfo::IsRangedWeaponSpell() const +{ + return (EquippedItemClass == ITEM_CLASS_WEAPON) && (EquippedItemSubClassMask & ITEM_SUBCLASS_MASK_WEAPON_RANGED); +} + +bool SpellInfo::IsAutoRepeatRangedSpell() const +{ + return HasAttribute(SPELL_ATTR2_AUTO_REPEAT); +} + +bool SpellInfo::CausesInitialThreat() const +{ + return !HasAttribute(SPELL_ATTR1_NO_THREAT) && !HasAttribute(SPELL_ATTR2_NO_INITIAL_THREAT) && !HasAttribute(SPELL_ATTR4_NO_HARMFUL_THREAT); +} + +WeaponAttackType SpellInfo::GetAttackType() const +{ + WeaponAttackType result; + switch (DmgClass) + { + case SPELL_DAMAGE_CLASS_MELEE: + if (HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + result = OFF_ATTACK; + else + result = BASE_ATTACK; + break; + case SPELL_DAMAGE_CLASS_RANGED: + result = IsRangedWeaponSpell() ? RANGED_ATTACK : BASE_ATTACK; + break; + default: + // Wands + if (IsAutoRepeatRangedSpell()) + result = RANGED_ATTACK; + else + result = BASE_ATTACK; + break; + } + + return result; +} + +bool SpellInfo::IsAffected(uint32 familyName, flag96 const& familyFlags) const +{ + if (!familyName) + return true; + + if (familyName != SpellFamilyName) + return false; + + if (familyFlags && !(familyFlags & SpellFamilyFlags)) + return false; + + return true; +} + +bool SpellInfo::IsAffectedBySpellMods() const +{ + return !HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS); +} + +bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod) const +{ + if (!IsAffectedBySpellMods()) + return false; + + SpellInfo const* affectSpell = sSpellMgr->GetSpellInfo(mod->spellId); + if (!affectSpell) + return false; + + switch (mod->type) + { + case SPELLMOD_FLAT: + case SPELLMOD_PCT: + // TEMP: dont use IsAffected - !familyName and !familyFlags are not valid options for spell mods + // TODO: investigate if the !familyName and !familyFlags conditions are even valid for all other (nonmod) uses of SpellInfo::IsAffected + return affectSpell->SpellFamilyName == SpellFamilyName && static_cast(mod)->mask & SpellFamilyFlags; + default: + break; + } + + return false; +} + +bool SpellInfo::CanPierceImmuneAura(SpellInfo const* auraSpellInfo) const +{ + // aura can't be pierced + if (!auraSpellInfo || auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + // these spells pierce all available spells (Resurrection Sickness for example) + if (HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return true; + + // these spells (Cyclone for example) can pierce all... + if (HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) || HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + // ...but not these (Divine shield, Ice block, Cyclone and Banish for example) + if (auraSpellInfo->Mechanic != MECHANIC_IMMUNE_SHIELD && + auraSpellInfo->Mechanic != MECHANIC_INVULNERABILITY && + (auraSpellInfo->Mechanic != MECHANIC_BANISH || (IsRankOf(auraSpellInfo) && auraSpellInfo->Dispel != DISPEL_NONE))) // Banish shouldn't be immune to itself, but Cyclone should + return true; + } + + // Dispels other auras on immunity, check if this spell makes the unit immune to aura + if (HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT) && CanSpellProvideImmunityAgainstAura(auraSpellInfo)) + return true; + + return false; +} + +bool SpellInfo::CanDispelAura(SpellInfo const* auraSpellInfo) const +{ + // These auras (like Divine Shield) can't be dispelled + if (auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + // These spells (like Mass Dispel) can dispel all auras + if (HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return true; + + // These auras (Cyclone for example) are not dispelable + if ((auraSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) && auraSpellInfo->Mechanic != MECHANIC_NONE) + || auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + return false; + + return true; +} + +bool SpellInfo::IsSingleTarget() const +{ + // all other single target spells have if it has AttributesEx5 + if (HasAttribute(SPELL_ATTR5_LIMIT_N)) + return true; + + return false; +} + +bool SpellInfo::IsAuraExclusiveBySpecificWith(SpellInfo const* spellInfo) const +{ + SpellSpecificType spellSpec1 = GetSpellSpecific(); + SpellSpecificType spellSpec2 = spellInfo->GetSpellSpecific(); + switch (spellSpec1) + { + case SPELL_SPECIFIC_WARLOCK_ARMOR: + case SPELL_SPECIFIC_MAGE_ARMOR: + case SPELL_SPECIFIC_ELEMENTAL_SHIELD: + case SPELL_SPECIFIC_MAGE_POLYMORPH: + case SPELL_SPECIFIC_PRESENCE: + case SPELL_SPECIFIC_CHARM: + case SPELL_SPECIFIC_SCROLL: + case SPELL_SPECIFIC_WARRIOR_ENRAGE: + case SPELL_SPECIFIC_MAGE_ARCANE_BRILLANCE: + case SPELL_SPECIFIC_PRIEST_DIVINE_SPIRIT: + return spellSpec1 == spellSpec2; + case SPELL_SPECIFIC_FOOD: + return spellSpec2 == SPELL_SPECIFIC_FOOD + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + case SPELL_SPECIFIC_DRINK: + return spellSpec2 == SPELL_SPECIFIC_DRINK + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + case SPELL_SPECIFIC_FOOD_AND_DRINK: + return spellSpec2 == SPELL_SPECIFIC_FOOD + || spellSpec2 == SPELL_SPECIFIC_DRINK + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + default: + return false; + } +} + +bool SpellInfo::IsAuraExclusiveBySpecificPerCasterWith(SpellInfo const* spellInfo) const +{ + SpellSpecificType spellSpec = GetSpellSpecific(); + switch (spellSpec) + { + case SPELL_SPECIFIC_SEAL: + case SPELL_SPECIFIC_HAND: + case SPELL_SPECIFIC_AURA: + case SPELL_SPECIFIC_STING: + case SPELL_SPECIFIC_CURSE: + case SPELL_SPECIFIC_BANE: + case SPELL_SPECIFIC_ASPECT: + case SPELL_SPECIFIC_WARLOCK_CORRUPTION: + return spellSpec == spellInfo->GetSpellSpecific(); + default: + return false; + } +} + +SpellCastResult SpellInfo::CheckShapeshift(uint32 form) const +{ + // talents that learn spells can have stance requirements that need ignore + // (this requirement only for client-side stance show in talent description) + if (sDBCManager.GetTalentSpellCost(Id) > 0 && + (Effects[0].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[1].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[2].Effect == SPELL_EFFECT_LEARN_SPELL)) + return SPELL_CAST_OK; + + uint64 stanceMask = (form ? UI64LIT(1) << (form - 1) : 0); + + if (stanceMask & StancesNot) // can explicitly not be cast in this stance + return SPELL_FAILED_NOT_SHAPESHIFT; + + if (stanceMask & Stances) // can explicitly be cast in this stance + return SPELL_CAST_OK; + + bool actAsShifted = false; + SpellShapeshiftFormEntry const* shapeInfo = nullptr; + if (form > 0) + { + shapeInfo = sSpellShapeshiftFormStore.LookupEntry(form); + if (!shapeInfo) + { + TC_LOG_ERROR("spells", "GetErrorAtShapeshiftedCast: unknown shapeshift %u", form); + return SPELL_CAST_OK; + } + actAsShifted = !shapeInfo->GetFlags().HasFlag(SpellShapeshiftFormFlags::Stance); + } + + if (actAsShifted) + { + if (HasAttribute(SPELL_ATTR0_NOT_SHAPESHIFTED) || (shapeInfo && shapeInfo->GetFlags().HasFlag(SpellShapeshiftFormFlags::CanOnlyCastShapeshiftSpells))) // not while shapeshifted + return SPELL_FAILED_NOT_SHAPESHIFT; + else if (Stances != 0) // needs other shapeshift + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + else + { + // needs shapeshift + if (!HasAttribute(SPELL_ATTR2_ALLOW_WHILE_NOT_SHAPESHIFTED_CASTER_FORM) && Stances != 0) + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + + // Check if stance disables cast of not-stance spells + // Example: cannot cast any other spells in zombie or ghoul form + /// @todo Find a way to disable use of these spells clientside + if (shapeInfo && shapeInfo->Flags & 0x400) + { + if (!(stanceMask & Stances)) + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckLocation(uint32 map_id, uint32 zone_id, uint32 area_id, Player const* player) const +{ + // normal case + if (AreaGroupId > 0) + { + bool found = false; + AreaGroupEntry const* groupEntry = sAreaGroupStore.LookupEntry(AreaGroupId); + while (groupEntry) + { + for (uint8 i = 0; i < MAX_GROUP_AREA_IDS; ++i) + if (DBCManager::IsInArea(area_id, groupEntry->AreaID[i])) + found = true; + + if (found || !groupEntry->NextAreaID) + break; + // Try search in next group + groupEntry = sAreaGroupStore.LookupEntry(groupEntry->NextAreaID); + } + + if (!found) + return SPELL_FAILED_INCORRECT_AREA; + } + + // continent limitation (virtual continent) + if (HasAttribute(SPELL_ATTR4_ONLY_FLYING_AREAS)) + { + EnumFlag mountFlags = AreaMountFlags::None; + if (player && player->HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS)) + { + for (AuraEffect const* auraEffect : player->GetAuraEffectsByType(SPELL_AURA_MOUNT_RESTRICTIONS)) + mountFlags |= AreaMountFlags(auraEffect->GetMiscValue()); + } + else if (AreaTableEntry const* areaTable = sAreaTableStore.LookupEntry(area_id)) + mountFlags = areaTable->GetMountFlags(); + + if (!(mountFlags.HasFlag(AreaMountFlags::AllowFlyingMounts))) + return SPELL_FAILED_INCORRECT_AREA; + } + + // raid instance limitation + if (HasAttribute(SPELL_ATTR6_NOT_IN_RAID_INSTANCES)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || mapEntry->IsRaid()) + return SPELL_FAILED_NOT_IN_RAID_INSTANCE; + } + + if (HasAttribute(SPELL_ATTR8_REMOVE_OUTSIDE_DUNGEONS_AND_RAIDS)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || !mapEntry->IsDungeon()) + return SPELL_FAILED_TARGET_NOT_IN_INSTANCE; + } + + if (HasAttribute(SPELL_ATTR8_NOT_IN_BATTLEGROUND)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || mapEntry->IsBattleground()) + return SPELL_FAILED_NOT_IN_BATTLEGROUND; + } + + // DB base check (if non empty then must fit at least single for allow) + SpellAreaMapBounds saBounds = sSpellMgr->GetSpellAreaMapBounds(Id); + if (saBounds.first != saBounds.second) + { + for (SpellAreaMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) + { + if (itr->second.IsFitToRequirements(player, zone_id, area_id)) + return SPELL_CAST_OK; + } + return SPELL_FAILED_INCORRECT_AREA; + } + + // bg spell checks + switch (Id) + { + case 23333: // Horde Flag + case 23335: // Alliance Flag + return (map_id == 489 || map_id == 726) && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + case 34976: // Netherstorm Flag + return map_id == 566 && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + case 2584: // Waiting to Resurrect + case 22011: // Spirit Heal Channel + case 22012: // Spirit Heal + case 24171: // Resurrection Impact Visual + case 42792: // Recently Dropped Flag + case 43681: // Inactive + case 44535: // Spirit Heal (mana) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + return zone_id == AREA_WINTERGRASP || (mapEntry->IsBattleground() && player && player->InBattleground()) ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 44521: // Preparation + { + if (!player) + return SPELL_FAILED_REQUIRES_AREA; + + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + if (!mapEntry->IsBattleground()) + return SPELL_FAILED_REQUIRES_AREA; + + Battleground* bg = player->GetBattleground(); + return bg && bg->GetStatus() == STATUS_WAIT_JOIN ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 32724: // Gold Team (Alliance) + case 32725: // Green Team (Alliance) + case 35774: // Gold Team (Horde) + case 35775: // Green Team (Horde) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + return mapEntry->IsBattleArena() && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 32727: // Arena Preparation + { + if (!player) + return SPELL_FAILED_REQUIRES_AREA; + + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + if (!mapEntry->IsBattleArena()) + return SPELL_FAILED_REQUIRES_AREA; + + Battleground* bg = player->GetBattleground(); + return bg && bg->GetStatus() == STATUS_WAIT_JOIN ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + } + + // aura limitations + if (player) + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsAura()) + continue; + + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_SHAPESHIFT: + { + if (SpellShapeshiftFormEntry const* spellShapeshiftForm = sSpellShapeshiftFormStore.LookupEntry(Effects[i].MiscValue)) + if (uint32 mountType = spellShapeshiftForm->MountTypeID) + if (!player->GetMountCapability(mountType)) + return SPELL_FAILED_NOT_HERE; + break; + } + case SPELL_AURA_MOUNTED: + { + if (Effects[i].MiscValueB && !player->GetMountCapability(Effects[i].MiscValueB)) + return SPELL_FAILED_NOT_HERE; + break; + } + break; + } + } + } + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckTarget(WorldObject const* caster, WorldObject const* target, bool implicit /*= true*/) const +{ + // VIANO_AURA_DEBUG_CHECKTARGET_V1 + if (Id == 465 && unitTarget && unitTarget->IsVianoAdventurer()) + TC_LOG_INFO("scripts", "[VianoAuraDebug] stage=CheckTarget spell=%u target=%s controlled=%u", + Id, unitTarget->GetGUID().ToString().c_str(), unitTarget->IsControlledByPlayer() ? 1u : 0u); + + if (HasAttribute(SPELL_ATTR1_EXCLUDE_CASTER) && caster == target) + return SPELL_FAILED_BAD_TARGETS; + + // check visibility - ignore stealth for implicit (area) targets + if (!HasAttribute(SPELL_ATTR6_IGNORE_PHASE_SHIFT) && !caster->CanSeeOrDetect(target, implicit)) + return SPELL_FAILED_BAD_TARGETS; + + Unit const* unitTarget = target->ToUnit(); + + /* + * @todo: 4.x - old hold due to an upcoming tempsummon rework + if (HasAttribute(SPELL_ATTR8_ONLY_TARGET_IF_SAME_CREATOR)) + if (caster != target && caster->GetGUID() != target->GetOwnerGUID()) + return SPELL_FAILED_BAD_TARGETS; + */ + + // creature/player specific target checks + if (unitTarget) + { + // spells cannot be cast if target has a pet in combat either + if (HasAttribute(SPELL_ATTR1_ONLY_PEACEFUL_TARGETS) && (unitTarget->IsInCombat() || unitTarget->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT))) + return SPELL_FAILED_TARGET_AFFECTING_COMBAT; + + // only spells with SPELL_ATTR3_ONLY_TARGET_GHOSTS can target ghosts + if (HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS) != unitTarget->HasAuraType(SPELL_AURA_GHOST)) + { + if (HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS)) + return SPELL_FAILED_TARGET_NOT_GHOST; + else + return SPELL_FAILED_BAD_TARGETS; + } + + if (caster != unitTarget) + { + if (caster->GetTypeId() == TYPEID_PLAYER) + { + // Do not allow these spells to target creatures not tapped by us (Banish, Polymorph, many quest spells) + if (HasAttribute(SPELL_ATTR2_CANNOT_CAST_ON_TAPPED)) + if (Creature const* targetCreature = unitTarget->ToCreature()) + if (targetCreature->hasLootRecipient() && !targetCreature->isTappedBy(caster->ToPlayer())) + return SPELL_FAILED_CANT_CAST_ON_TAPPED; + + if (HasAttribute(SPELL_ATTR0_CU_PICKPOCKET)) + { + Creature const* targetCreature = unitTarget->ToCreature(); + if (!targetCreature) + return SPELL_FAILED_BAD_TARGETS; + + if (!LootTemplates_Pickpocketing.HaveLootFor(targetCreature->GetCreatureTemplate()->pickpocketLootId)) + return SPELL_FAILED_TARGET_NO_POCKETS; + } + + // Not allow disarm unarmed player + if (Mechanic == MECHANIC_DISARM) + { + if (unitTarget->GetTypeId() == TYPEID_PLAYER) + { + Player const* player = unitTarget->ToPlayer(); + if (!player->GetWeaponForAttack(BASE_ATTACK) || !player->IsUseEquipedWeapon(true)) + return SPELL_FAILED_TARGET_NO_WEAPONS; + } + else if (!unitTarget->GetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID)) + return SPELL_FAILED_TARGET_NO_WEAPONS; + } + } + } + + if (HasAttribute(SPELL_ATTR8_ONLY_TARGET_OWN_SUMMONS)) + if (!unitTarget->IsSummon() || unitTarget->ToTempSummon()->GetSummonerGUID() != caster->GetGUID()) + return SPELL_FAILED_BAD_TARGETS; + } + // corpse specific target checks + else if (Corpse const* corpseTarget = target->ToCorpse()) + { + // cannot target bare bones + if (corpseTarget->GetType() == CORPSE_BONES) + return SPELL_FAILED_BAD_TARGETS; + // we have to use owner for some checks (aura preventing resurrection for example) + if (Player* owner = ObjectAccessor::FindPlayer(corpseTarget->GetOwnerGUID())) + unitTarget = owner; + // we're not interested in corpses without owner + else + return SPELL_FAILED_BAD_TARGETS; + } + // other types of objects - always valid + else return SPELL_CAST_OK; + + // corpseOwner and unit specific target checks + if (!unitTarget->IsPlayer()) + { + if (HasAttribute(SPELL_ATTR3_ONLY_ON_PLAYER)) + return SPELL_FAILED_TARGET_NOT_PLAYER; + + // VIANO_AURA_PLAYER_CONTROLLED_EXCEPTION_V1 + // Viano adventurers keep PLAYER_CONTROLLED for direct helpful targeting, + // but are intentionally eligible for group/raid buffs such as paladin auras. + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) + && unitTarget->IsControlledByPlayer() + && !unitTarget->IsVianoAdventurer()) + return SPELL_FAILED_TARGET_IS_PLAYER_CONTROLLED; + } + else if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER)) + return SPELL_FAILED_TARGET_IS_PLAYER; + + if (!IsAllowingDeadTarget() && !unitTarget->IsAlive()) + return SPELL_FAILED_TARGETS_DEAD; + + // check this flag only for implicit targets (chain and area), allow to explicitly target units for spells like Shield of Righteousness + if (implicit && HasAttribute(SPELL_ATTR6_DO_NOT_CHAIN_TO_CROWD_CONTROLLED_TARGETS) && !unitTarget->CanFreeMove()) + return SPELL_FAILED_BAD_TARGETS; + + // Do not allow pet or guardian targets if the spell is a raid buff or may not target pets at all + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) && unitTarget->IsCreature() && unitTarget->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + return SPELL_FAILED_BAD_TARGETS; + + // checked in Unit::IsValidAttack/AssistTarget, shouldn't be checked for ENTRY targets + //if (!HasAttribute(SPELL_ATTR6_CAN_TARGET_UNTARGETABLE) && target->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)) + // return SPELL_FAILED_BAD_TARGETS; + + if (!CheckTargetCreatureType(unitTarget)) + { + if (target->GetTypeId() == TYPEID_PLAYER) + return SPELL_FAILED_TARGET_IS_PLAYER; + else + return SPELL_FAILED_BAD_TARGETS; + } + + // check GM mode and GM invisibility - only for player casts (npc casts are controlled by AI) and negative spells + if (unitTarget != caster && (caster->GetAffectingPlayer() || !IsPositive()) && unitTarget->GetTypeId() == TYPEID_PLAYER) + { + if (!unitTarget->ToPlayer()->IsVisible()) + return SPELL_FAILED_BM_OR_INVISGOD; + + if (unitTarget->ToPlayer()->IsGameMaster()) + return SPELL_FAILED_BM_OR_INVISGOD; + } + + // not allow casting on flying player + if (unitTarget->HasUnitState(UNIT_STATE_IN_FLIGHT) && !HasAttribute(SPELL_ATTR0_CU_ALLOW_INFLIGHT_TARGET)) + return SPELL_FAILED_BAD_TARGETS; + + /* TARGET_UNIT_MASTER gets blocked here for passengers, because the whole idea of this check is to + not allow passengers to be implicitly hit by spells, however this target type should be an exception, + if this is left it kills spells that award kill credit from vehicle to master (few spells), + the use of these 2 covers passenger target check, logically, if vehicle cast this to master it should always hit + him, because it would be it's passenger, there's no such case where this gets to fail legitimacy, this problem + cannot be solved from within the check in other way since target type cannot be called for the spell currently + Spell examples: [ID - 52864 Devour Water, ID - 52862 Devour Wind, ID - 49370 Wyrmrest Defender: Destabilize Azure Dragonshrine Effect] */ + if (Unit const* unitCaster = caster->ToUnit()) + { + if (!unitCaster->IsVehicle() && !(unitCaster->GetCharmerOrOwner() == target)) + { + if (TargetAuraState && !unitTarget->HasAuraState(AuraStateType(TargetAuraState), this, unitCaster)) + return SPELL_FAILED_TARGET_AURASTATE; + + if (TargetAuraStateNot && unitTarget->HasAuraState(AuraStateType(TargetAuraStateNot), this, unitCaster)) + return SPELL_FAILED_TARGET_AURASTATE; + } + } + + if (TargetAuraSpell && !unitTarget->HasAura(sSpellMgr->GetSpellIdForDifficulty(TargetAuraSpell, caster))) + return SPELL_FAILED_TARGET_AURASTATE; + + if (ExcludeTargetAuraSpell && unitTarget->HasAura(sSpellMgr->GetSpellIdForDifficulty(ExcludeTargetAuraSpell, caster))) + return SPELL_FAILED_TARGET_AURASTATE; + + if (unitTarget->HasAuraType(SPELL_AURA_PREVENT_RESURRECTION)) + if (HasEffect(SPELL_EFFECT_SELF_RESURRECT) || HasEffect(SPELL_EFFECT_RESURRECT) || HasEffect(SPELL_EFFECT_RESURRECT_NEW)) + return SPELL_FAILED_TARGET_CANNOT_BE_RESURRECTED; + + if (!unitTarget->CheckPrivateObjectOwnerVisibility(caster)) + return SPELL_FAILED_BAD_TARGETS; + + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckExplicitTarget(WorldObject const* caster, WorldObject const* target, Item const* itemTarget) const +{ + uint32 neededTargets = GetExplicitTargetMask(); + if (!target) + { + if (neededTargets & (TARGET_FLAG_UNIT_MASK | TARGET_FLAG_GAMEOBJECT_MASK | TARGET_FLAG_CORPSE_MASK)) + if (!(neededTargets & TARGET_FLAG_GAMEOBJECT_ITEM) || !itemTarget) + return SPELL_FAILED_BAD_TARGETS; + return SPELL_CAST_OK; + } + + if (Unit const* unitTarget = target->ToUnit()) + { + if (neededTargets & (TARGET_FLAG_UNIT_ENEMY | TARGET_FLAG_UNIT_ALLY | TARGET_FLAG_UNIT_RAID | TARGET_FLAG_UNIT_PARTY | TARGET_FLAG_UNIT_MINIPET | TARGET_FLAG_UNIT_PASSENGER)) + { + Unit const* unitCaster = caster->ToUnit(); + if (neededTargets & TARGET_FLAG_UNIT_ENEMY) + if (caster->IsValidAttackTarget(unitTarget, this)) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_ALLY) + || ((neededTargets & TARGET_FLAG_UNIT_PARTY) && unitCaster && unitCaster->IsInPartyWith(unitTarget)) + || ((neededTargets & TARGET_FLAG_UNIT_RAID) && unitCaster && unitCaster->IsInRaidWith(unitTarget))) + if (caster->IsValidAssistTarget(unitTarget, this)) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_MINIPET) && unitCaster) + if (unitTarget->GetGUID() == unitCaster->GetCritterGUID()) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_PASSENGER) && unitCaster) + if (unitTarget->IsOnVehicle(unitCaster)) + return SPELL_CAST_OK; + return SPELL_FAILED_BAD_TARGETS; + } + } + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckVehicle(Unit const* caster) const +{ + // All creatures should be able to cast as passengers freely, restriction and attribute are only for players + if (caster->GetTypeId() != TYPEID_PLAYER) + return SPELL_CAST_OK; + + Vehicle* vehicle = caster->GetVehicle(); + if (vehicle) + { + uint16 checkMask = 0; + for (uint8 effIndex = EFFECT_0; effIndex < MAX_SPELL_EFFECTS; ++effIndex) + { + if (Effects[effIndex].ApplyAuraName == SPELL_AURA_MOD_SHAPESHIFT) + { + SpellShapeshiftFormEntry const* shapeShiftFromEntry = sSpellShapeshiftFormStore.LookupEntry(Effects[effIndex].MiscValue); + if (shapeShiftFromEntry && (shapeShiftFromEntry->Flags & 1) == 0) // unk flag + checkMask |= VEHICLE_SEAT_FLAG_UNCONTROLLED; + break; + } + } + + if (HasAura(SPELL_AURA_MOUNTED)) + checkMask |= VEHICLE_SEAT_FLAG_CAN_CAST_MOUNT_SPELL; + + if (!checkMask) + checkMask = VEHICLE_SEAT_FLAG_CAN_ATTACK; + + VehicleSeatEntry const* vehicleSeat = vehicle->GetSeatForPassenger(caster); + if (!vehicleSeat) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + + if (!HasAttribute(SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE) && !HasAttribute(SPELL_ATTR0_ALLOW_WHILE_MOUNTED) + && (vehicleSeat->Flags & checkMask) != checkMask) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + + // Can only summon uncontrolled minions/guardians when on controlled vehicle + if (vehicleSeat->Flags & (VEHICLE_SEAT_FLAG_CAN_CONTROL | VEHICLE_SEAT_FLAG_UNK2)) + { + for (uint32 i = EFFECT_0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect != SPELL_EFFECT_SUMMON) + continue; + + SummonPropertiesEntry const* props = sSummonPropertiesStore.LookupEntry(Effects[i].MiscValueB); + if (props && props->Control != SUMMON_CATEGORY_WILD) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + } + } + } + + return SPELL_CAST_OK; +} + +bool SpellInfo::CheckTargetCreatureType(Unit const* target) const +{ + // Curse of Doom & Exorcism: not find another way to fix spell target check :/ + if (SpellFamilyName == SPELLFAMILY_WARLOCK && GetCategory() == 1179) + { + // not allow cast at player + if (target->GetTypeId() == TYPEID_PLAYER) + return false; + else + return true; + } + + // if target is magnet (i.e Grounding Totem) the check is skipped + if (target->IsMagnet()) + return true; + + uint32 creatureType = target->GetCreatureTypeMask(); + return !TargetCreatureType || !creatureType || (creatureType & TargetCreatureType); +} + +SpellSchoolMask SpellInfo::GetSchoolMask() const +{ + return SpellSchoolMask(SchoolMask); +} + +uint32 SpellInfo::GetAllEffectsMechanicMask() const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].Mechanic) + mask |= 1 << Effects[i].Mechanic; + return mask; +} + +uint32 SpellInfo::GetEffectMechanicMask(uint8 effIndex) const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + if (Effects[effIndex].IsEffect() && Effects[effIndex].Mechanic) + mask |= 1 << Effects[effIndex].Mechanic; + return mask; +} + +uint32 SpellInfo::GetSpellMechanicMaskByEffectMask(uint32 effectMask) const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if ((effectMask & (1 << i)) && Effects[i].Mechanic) + mask |= 1 << Effects[i].Mechanic; + return mask; +} + +Mechanics SpellInfo::GetEffectMechanic(uint8 effIndex) const +{ + if (Effects[effIndex].IsEffect() && Effects[effIndex].Mechanic) + return Mechanics(Effects[effIndex].Mechanic); + if (Mechanic) + return Mechanics(Mechanic); + return MECHANIC_NONE; +} + +bool SpellInfo::HasAnyEffectMechanic() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Mechanic) + return true; + return false; +} + +uint32 SpellInfo::GetDispelMask() const +{ + return GetDispelMask(DispelType(Dispel)); +} + +uint32 SpellInfo::GetDispelMask(DispelType type) +{ + // If dispel all + if (type == DISPEL_ALL) + return DISPEL_ALL_MASK; + else + return uint32(1 << type); +} + +uint32 SpellInfo::GetExplicitTargetMask() const +{ + return ExplicitTargetMask; +} + +AuraStateType SpellInfo::GetAuraState() const +{ + return _auraState; +} + +void SpellInfo::_LoadAuraState() +{ + _auraState = [this]() -> AuraStateType + { + // Seals + if (GetSpellSpecific() == SPELL_SPECIFIC_SEAL) + return AURA_STATE_JUDGEMENT; + + // Conflagrate aura state on Immolate and Shadowflame + if (SpellFamilyName == SPELLFAMILY_WARLOCK && + // Immolate + ((SpellFamilyFlags[0] & 4) || + // Shadowflame + (SpellFamilyFlags[2] & 2))) + return AURA_STATE_CONFLAGRATE; + + // Faerie Fire (druid versions) + if (SpellFamilyName == SPELLFAMILY_DRUID && SpellFamilyFlags[0] & 0x400) + return AURA_STATE_FAERIE_FIRE; + + // Sting (hunter's pet ability) + if (GetCategory() == 1133) + return AURA_STATE_FAERIE_FIRE; + + // Victorious + if (SpellFamilyName == SPELLFAMILY_WARRIOR && SpellFamilyFlags[1] & 0x00040000) + return AURA_STATE_WARRIOR_VICTORY_RUSH; + + // Swiftmend state on Regrowth & Rejuvenation + if (SpellFamilyName == SPELLFAMILY_DRUID && SpellFamilyFlags[0] & 0x50) + return AURA_STATE_SWIFTMEND; + + // Deadly poison aura state + if (SpellFamilyName == SPELLFAMILY_ROGUE && SpellFamilyFlags[0] & 0x10000) + return AURA_STATE_DEADLY_POISON; + + // Enrage aura state + if (Dispel == DISPEL_ENRAGE) + return AURA_STATE_ENRAGE; + + // Bleeding aura state + if (GetAllEffectsMechanicMask() & 1 << MECHANIC_BLEED) + return AURA_STATE_BLEEDING; + + if (GetSchoolMask() & SPELL_SCHOOL_MASK_FROST) + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAura() && (Effects[i].ApplyAuraName == SPELL_AURA_MOD_STUN + || Effects[i].ApplyAuraName == SPELL_AURA_MOD_ROOT)) + return AURA_STATE_FROZEN; + + switch (Id) + { + case 71465: // Divine Surge + case 50241: // Evasive Charges + return AURA_STATE_UNKNOWN22; + case 9991: // Touch of Zanzil + case 35325: // Glowing Blood + case 35328: // Lambent Blood + case 35329: // Vibrant Blood + case 35331: // Black Blood + case 49163: // Perpetual Instability + return AURA_STATE_FAERIE_FIRE; + default: + break; + } + + return AURA_STATE_NONE; + }(); +} + +SpellSpecificType SpellInfo::GetSpellSpecific() const +{ + return _spellSpecific; +} + +void SpellInfo::_LoadSpellSpecific() +{ + _spellSpecific = [this]()->SpellSpecificType + { + switch (SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + { + // Food / Drinks (mostly) + if (HasAuraInterruptFlag(SpellAuraInterruptFlags::Standing)) + { + bool food = false; + bool drink = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsAura()) + continue; + switch (Effects[i].ApplyAuraName) + { + // Food + case SPELL_AURA_MOD_REGEN: + case SPELL_AURA_OBS_MOD_HEALTH: + food = true; + break; + // Drink + case SPELL_AURA_MOD_POWER_REGEN: + case SPELL_AURA_OBS_MOD_POWER: + drink = true; + break; + default: + break; + } + } + + if (food && drink) + return SPELL_SPECIFIC_FOOD_AND_DRINK; + else if (food) + return SPELL_SPECIFIC_FOOD; + else if (drink) + return SPELL_SPECIFIC_DRINK; + } + // scrolls effects + else + { + SpellInfo const* firstRankSpellInfo = GetFirstRankSpell(); + switch (firstRankSpellInfo->Id) + { + case 8118: // Strength + case 8099: // Stamina + case 8112: // Spirit + case 8096: // Intellect + case 8115: // Agility + case 8091: // Armor + return SPELL_SPECIFIC_SCROLL; + case 12880: // Enrage (Enrage) + case 57518: // Enrage (Wrecking Crew) + return SPELL_SPECIFIC_WARRIOR_ENRAGE; + } + } + break; + } + case SPELLFAMILY_MAGE: + { + // family flags 18(Molten), 25(Frost/Ice), 28(Mage) + if (SpellFamilyFlags[0] & 0x12040000) + return SPELL_SPECIFIC_MAGE_ARMOR; + + // Arcane brillance and Arcane intelect (normal check fails because of flags difference) + if (SpellFamilyFlags[0] & 0x400) + return SPELL_SPECIFIC_MAGE_ARCANE_BRILLANCE; + + if ((SpellFamilyFlags[0] & 0x1000000) && Effects[0].ApplyAuraName == SPELL_AURA_MOD_CONFUSE) + return SPELL_SPECIFIC_MAGE_POLYMORPH; + + break; + } + case SPELLFAMILY_WARRIOR: + { + if (Id == 12292) // Death Wish + return SPELL_SPECIFIC_WARRIOR_ENRAGE; + + break; + } + case SPELLFAMILY_WARLOCK: + { + // Warlock (Bane of Doom | Bane of Agony | Bane of Havoc) + if (Id == 603 || Id == 980 || Id == 80240) + return SPELL_SPECIFIC_BANE; + + // only warlock curses have this + if (Dispel == DISPEL_CURSE) + return SPELL_SPECIFIC_CURSE; + + // Warlock (Demon Armor | Demon Skin | Fel Armor) + if (SpellFamilyFlags[1] & 0x20000020 || SpellFamilyFlags[2] & 0x00000010) + return SPELL_SPECIFIC_WARLOCK_ARMOR; + + //seed of corruption and corruption + if (SpellFamilyFlags[1] & 0x10 || SpellFamilyFlags[0] & 0x2) + return SPELL_SPECIFIC_WARLOCK_CORRUPTION; + break; + } + case SPELLFAMILY_PRIEST: + { + // Divine Spirit and Prayer of Spirit + if (SpellFamilyFlags[0] & 0x20) + return SPELL_SPECIFIC_PRIEST_DIVINE_SPIRIT; + + break; + } + case SPELLFAMILY_HUNTER: + { + // only hunter stings have this + if (Dispel == DISPEL_POISON) + return SPELL_SPECIFIC_STING; + + // only hunter aspects have this (but not all aspects in hunter family) + if (SpellFamilyFlags.HasFlag(0x00380000, 0x00440000, 0x00001010)) + return SPELL_SPECIFIC_ASPECT; + + break; + } + case SPELLFAMILY_PALADIN: + { + // Collection of all the seal family flags. No other paladin spell has any of those. + if (SpellFamilyFlags[1] & 0xA2000800) + return SPELL_SPECIFIC_SEAL; + + if (SpellFamilyFlags[0] & 0x00002190) + return SPELL_SPECIFIC_HAND; + + // only paladin auras have this (for palaldin class family) + if (SpellFamilyFlags[2] & 0x00000020) + return SPELL_SPECIFIC_AURA; + + break; + } + case SPELLFAMILY_SHAMAN: + { + // family flags 10 (Lightning), 42 (Earth), 37 (Water), proc shield from T2 8 pieces bonus + if (SpellFamilyFlags[1] & 0x420 + || SpellFamilyFlags[0] & 0x00000400 + || Id == 23552) + return SPELL_SPECIFIC_ELEMENTAL_SHIELD; + + break; + } + case SPELLFAMILY_DEATHKNIGHT: + if (Id == 48266 || Id == 48263 || Id == 48265) + return SPELL_SPECIFIC_PRESENCE; + break; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA) + { + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_CHARM: + case SPELL_AURA_MOD_POSSESS: + case SPELL_AURA_AOE_CHARM: + return SPELL_SPECIFIC_CHARM; + case SPELL_AURA_TRACK_CREATURES: + /// @workaround For non-stacking tracking spells (We need generic solution) + if (Id == 30645) // Gas Cloud Tracking + return SPELL_SPECIFIC_NORMAL; + [[fallthrough]]; + case SPELL_AURA_TRACK_RESOURCES: + case SPELL_AURA_TRACK_STEALTHED: + return SPELL_SPECIFIC_TRACKER; + } + } + } + + return SPELL_SPECIFIC_NORMAL; + }(); +} + +void SpellInfo::_LoadSpellDiminishInfo() +{ + auto diminishingGroupCompute = [this](bool triggered) -> DiminishingGroup + { + if (IsPositive()) + return DIMINISHING_NONE; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].ApplyAuraName == SPELL_AURA_MOD_TAUNT) + return DIMINISHING_TAUNT; + } + + // Explicit Diminishing Groups + switch (SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + { + // Pet charge effects (Infernal Awakening, Demon Charge) + if (SpellVisual[0] == 2816 && SpellIconID == 15) + return DIMINISHING_CONTROLLED_STUN; + // Frost Tomb + else if (Id == 48400) + return DIMINISHING_NONE; + // Gnaw + else if (Id == 47481) + return DIMINISHING_CONTROLLED_STUN; + // ToC Icehowl Arctic Breath + else if (SpellVisual[0] == 14153) + return DIMINISHING_NONE; + // Black Plague + else if (Id == 64155) + return DIMINISHING_NONE; + // Screams of the Dead (King Ymiron) + else if (Id == 51750) + return DIMINISHING_NONE; + break; + } + // Event spells + case SPELLFAMILY_UNK1: + return DIMINISHING_NONE; + case SPELLFAMILY_MAGE: + { + // Frostbite + if (SpellFamilyFlags[1] & 0x80000000) + return DIMINISHING_ROOT; + // Shattered Barrier + else if (SpellVisual[0] == 12297) + return DIMINISHING_ROOT; + // Deep Freeze + else if (SpellIconID == 2939 && SpellVisual[0] == 9963) + return DIMINISHING_CONTROLLED_STUN; + // Frost Nova / Freeze (Water Elemental) + else if (SpellIconID == 193) + return DIMINISHING_CONTROLLED_ROOT; + // Dragon's Breath + else if (SpellFamilyFlags[0] & 0x800000) + return DIMINISHING_DRAGONS_BREATH; + break; + } + case SPELLFAMILY_WARRIOR: + { + // Hamstring - limit duration to 10s in PvP + if (SpellFamilyFlags[0] & 0x2) + return DIMINISHING_LIMITONLY; + // Charge Stun (own diminishing) + else if (SpellFamilyFlags[0] & 0x01000000) + return DIMINISHING_CHARGE; + break; + } + case SPELLFAMILY_WARLOCK: + { + // Curses/etc + if ((SpellFamilyFlags[0] & 0x80000000) || (SpellFamilyFlags[1] & 0x200)) + return DIMINISHING_LIMITONLY; + // Seduction + else if (SpellFamilyFlags[1] & 0x10000000) + return DIMINISHING_FEAR; + // Sin and Punishment (Priest spell, don't ask) + else if (SpellIconID == 1869) + return DIMINISHING_NONE; + break; + } + case SPELLFAMILY_DRUID: + { + // Pounce - there are two spells which share the same family: the stun effect and the bleed spell. We don't want the bleed to be affected by DR + if ((SpellFamilyFlags[0] & 0x20000) && Id != 9007) + return DIMINISHING_OPENING_STUN; + // Cyclone + else if (SpellFamilyFlags[1] & 0x20) + return DIMINISHING_CYCLONE; + // Entangling Roots + // Nature's Grasp + else if (SpellFamilyFlags[0] & 0x00000200) + return DIMINISHING_CONTROLLED_ROOT; + // Faerie Fire + else if (SpellFamilyFlags[0] & 0x400) + return DIMINISHING_LIMITONLY; + break; + } + case SPELLFAMILY_ROGUE: + { + // Gouge + if (SpellFamilyFlags[0] & 0x8) + return DIMINISHING_DISORIENT; + // Blind + else if (SpellFamilyFlags[0] & 0x1000000) + return DIMINISHING_FEAR; + // Cheap Shot + else if (SpellFamilyFlags[0] & 0x400) + return DIMINISHING_OPENING_STUN; + // Crippling poison - Limit to 10 seconds in PvP (No SpellFamilyFlags) + else if (SpellIconID == 163) + return DIMINISHING_LIMITONLY; + break; + } + case SPELLFAMILY_HUNTER: + { + // Hunter's Mark + if ((SpellFamilyFlags[0] & 0x400) && SpellIconID == 538) + return DIMINISHING_LIMITONLY; + // Scatter Shot (own diminishing) + else if ((SpellFamilyFlags[0] & 0x40000) && SpellIconID == 132) + return DIMINISHING_SCATTER_SHOT; + // Entrapment (own diminishing) + else if (SpellVisual[0] == 7484 && SpellIconID == 20) + return DIMINISHING_ENTRAPMENT; + // Wyvern Sting mechanic is MECHANIC_SLEEP but the diminishing is DIMINISHING_DISORIENT + else if ((SpellFamilyFlags[1] & 0x1000) && SpellIconID == 1721) + return DIMINISHING_DISORIENT; + // Freezing Arrow + else if (SpellFamilyFlags[0] & 0x8) + return DIMINISHING_DISORIENT; + break; + } + case SPELLFAMILY_PALADIN: + { + // Judgement of Justice - limit duration to 10s in PvP + if (SpellFamilyFlags[0] & 0x100000) + return DIMINISHING_LIMITONLY; + // Turn Evil + else if ((SpellFamilyFlags[1] & 0x804000) && SpellIconID == 309) + return DIMINISHING_FEAR; + break; + } + case SPELLFAMILY_SHAMAN: + { + // Storm, Earth and Fire - Earthgrab + if (SpellFamilyFlags[2] & 0x4000) + return DIMINISHING_NONE; + break; + } + case SPELLFAMILY_DEATHKNIGHT: + { + // Hungering Cold (no flags) + if (SpellIconID == 2797) + return DIMINISHING_DISORIENT; + // Mark of Blood + else if ((SpellFamilyFlags[0] & 0x10000000) && SpellIconID == 2285) + return DIMINISHING_LIMITONLY; + break; + } + default: + break; + } + + // Lastly - Set diminishing depending on mechanic + uint32 mechanic = GetAllEffectsMechanicMask(); + if (mechanic & (1 << MECHANIC_CHARM)) + return DIMINISHING_MIND_CONTROL; + if (mechanic & (1 << MECHANIC_SILENCE)) + return DIMINISHING_SILENCE; + if (mechanic & (1 << MECHANIC_SLEEP)) + return DIMINISHING_SLEEP; + if (mechanic & ((1 << MECHANIC_SAPPED) | (1 << MECHANIC_POLYMORPH) | (1 << MECHANIC_SHACKLE))) + return DIMINISHING_DISORIENT; + // Mechanic Knockout, except Blast Wave + if (mechanic & (1 << MECHANIC_KNOCKOUT) && SpellIconID != 292) + return DIMINISHING_DISORIENT; + if (mechanic & (1 << MECHANIC_DISARM)) + return DIMINISHING_DISARM; + if (mechanic & (1 << MECHANIC_FEAR)) + return DIMINISHING_FEAR; + if (mechanic & (1 << MECHANIC_STUN)) + return triggered ? DIMINISHING_STUN : DIMINISHING_CONTROLLED_STUN; + if (mechanic & (1 << MECHANIC_BANISH)) + return DIMINISHING_BANISH; + if (mechanic & (1 << MECHANIC_ROOT)) + return triggered ? DIMINISHING_ROOT : DIMINISHING_CONTROLLED_ROOT; + if (mechanic & (1 << MECHANIC_HORROR)) + return DIMINISHING_HORROR; + + return DIMINISHING_NONE; + }; + + auto diminishingTypeCompute = [](DiminishingGroup group) -> DiminishingReturnsType + { + switch (group) + { + case DIMINISHING_TAUNT: + case DIMINISHING_CONTROLLED_STUN: + case DIMINISHING_STUN: + case DIMINISHING_OPENING_STUN: + case DIMINISHING_CYCLONE: + case DIMINISHING_CHARGE: + return DRTYPE_ALL; + case DIMINISHING_LIMITONLY: + case DIMINISHING_NONE: + return DRTYPE_NONE; + default: + return DRTYPE_PLAYER; + } + }; + + auto diminishingMaxLevelCompute = [](DiminishingGroup group) -> DiminishingLevels + { + switch (group) + { + case DIMINISHING_TAUNT: + return DIMINISHING_LEVEL_TAUNT_IMMUNE; + default: + return DIMINISHING_LEVEL_IMMUNE; + } + }; + + auto diminishingLimitDurationCompute = [this](DiminishingGroup group) -> int32 + { + auto isGroupDurationLimited = [group]() -> bool + { + switch (group) + { + case DIMINISHING_BANISH: + case DIMINISHING_CONTROLLED_STUN: + case DIMINISHING_CONTROLLED_ROOT: + case DIMINISHING_CYCLONE: + case DIMINISHING_DISORIENT: + case DIMINISHING_ENTRAPMENT: + case DIMINISHING_FEAR: + case DIMINISHING_HORROR: + case DIMINISHING_MIND_CONTROL: + case DIMINISHING_OPENING_STUN: + case DIMINISHING_ROOT: + case DIMINISHING_STUN: + case DIMINISHING_SLEEP: + case DIMINISHING_LIMITONLY: + return true; + default: + return false; + } + }; + + if (!isGroupDurationLimited()) + return 0; + + // Explicit diminishing duration + switch (SpellFamilyName) + { + case SPELLFAMILY_DRUID: + { + // Faerie Fire - limit to 40 seconds in PvP (3.1) + if (SpellFamilyFlags[0] & 0x400) + return 40 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_HUNTER: + { + // Wyvern Sting + if (SpellFamilyFlags[1] & 0x1000) + return 6 * IN_MILLISECONDS; + // Hunter's Mark + if (SpellFamilyFlags[0] & 0x400) + return 30 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_PALADIN: + { + // Repentance - limit to 6 seconds in PvP + if (SpellFamilyFlags[0] & 0x4) + return 6 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_WARLOCK: + { + // Banish - limit to 6 seconds in PvP + if (SpellFamilyFlags[1] & 0x8000000) + return 6 * IN_MILLISECONDS; + // Curse of Tongues - limit to 12 seconds in PvP + else if (SpellFamilyFlags[2] & 0x800) + return 12 * IN_MILLISECONDS; + // Curse of Elements - limit to 120 seconds in PvP + else if (SpellFamilyFlags[1] & 0x200) + return 120 * IN_MILLISECONDS; + break; + } + default: + break; + } + + return 8 * IN_MILLISECONDS; + }; + + SpellDiminishInfo triggeredInfo, normalInfo; + triggeredInfo.DiminishGroup = diminishingGroupCompute(true); + triggeredInfo.DiminishReturnType = diminishingTypeCompute(triggeredInfo.DiminishGroup); + triggeredInfo.DiminishMaxLevel = diminishingMaxLevelCompute(triggeredInfo.DiminishGroup); + triggeredInfo.DiminishDurationLimit = diminishingLimitDurationCompute(triggeredInfo.DiminishGroup); + + normalInfo.DiminishGroup = diminishingGroupCompute(false); + normalInfo.DiminishReturnType = diminishingTypeCompute(normalInfo.DiminishGroup); + normalInfo.DiminishMaxLevel = diminishingMaxLevelCompute(normalInfo.DiminishGroup); + normalInfo.DiminishDurationLimit = diminishingLimitDurationCompute(normalInfo.DiminishGroup); + + _diminishInfoTriggered = triggeredInfo; + _diminishInfoNonTriggered = normalInfo; +} + +DiminishingGroup SpellInfo::GetDiminishingReturnsGroupForSpell(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishGroup : _diminishInfoNonTriggered.DiminishGroup; +} + +DiminishingReturnsType SpellInfo::GetDiminishingReturnsGroupType(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishReturnType : _diminishInfoNonTriggered.DiminishReturnType; +} + +DiminishingLevels SpellInfo::GetDiminishingReturnsMaxLevel(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishMaxLevel : _diminishInfoNonTriggered.DiminishMaxLevel; +} + +int32 SpellInfo::GetDiminishingReturnsLimitDuration(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishDurationLimit : _diminishInfoNonTriggered.DiminishDurationLimit; +} + +void SpellInfo::_LoadImmunityInfo() +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + uint32 schoolImmunityMask = 0; + uint32 applyHarmfulAuraImmunityMask = 0; + uint32 mechanicImmunityMask = 0; + uint32 dispelImmunity = 0; + uint32 damageImmunityMask = 0; + + if (!Effects[i].IsEffect()) + continue; + + int32 miscVal = Effects[i].MiscValue; + int32 amount = Effects[i].CalcValue(); + + ImmunityInfo& immuneInfo = _immunityInfo[i]; + + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MECHANIC_IMMUNITY_MASK: + { + switch (miscVal) + { + case 96: // Free Friend, Uncontrollable Frenzy, Warlord's Presence + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + break; + } + case 1615: // Incite Rage, Wolf Spirit, Overload, Lightning Tendrils + { + switch (Id) + { + case 43292: // Incite Rage + case 49172: // Wolf Spirit + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + [[fallthrough]]; + case 61869: // Overload + case 63481: + case 61887: // Lightning Tendrils + case 63486: + mechanicImmunityMask |= (1 << MECHANIC_INTERRUPT) | (1 << MECHANIC_SILENCE); + + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK); + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK_DEST); + break; + default: + break; + } + break; + } + case 679: // Mind Control, Avenging Fury + { + if (Id == 57742) // Avenging Fury + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 1557: // Startling Roar, Warlord Roar, Break Bonds, Stormshield + { + if (Id == 64187) // Stormshield + { + mechanicImmunityMask |= (1 << MECHANIC_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + } + else + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 1614: // Fixate + case 1694: // Fixated, Lightning Tendrils + { + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_ATTACK_ME); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_TAUNT); + break; + } + case 1630: // Fervor, Berserk + { + if (Id == 64112) // Berserk + { + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_ATTACK_ME); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_TAUNT); + } + else + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 477: // Bladestorm + case 1733: // Bladestorm, Killing Spree + { + if (!amount) + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK); + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK_DEST); + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 878: // Whirlwind, Fog of Corruption, Determination + { + if (Id == 66092) // Determination + { + mechanicImmunityMask |= (1 << MECHANIC_SNARE) | (1 << MECHANIC_STUN) + | (1 << MECHANIC_DISORIENTED) | (1 << MECHANIC_FREEZE); + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + } + break; + } + default: + break; + } + + if (immuneInfo.AuraTypeImmune.empty()) + { + if (miscVal & (1 << 10)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + if (miscVal & (1 << 1)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_TRANSFORM); + + // These flag can be recognized wrong: + if (miscVal & (1 << 6)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + if (miscVal & (1 << 0)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + if (miscVal & (1 << 2)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + if (miscVal & (1 << 9)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + if (miscVal & (1 << 7)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DISARM); + } + break; + } + case SPELL_AURA_MECHANIC_IMMUNITY: + { + switch (Id) + { + case 42292: // PvP trinket + case 59752: // Every Man for Himself + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED); + break; + case 34471: // The Beast Within + case 19574: // Bestial Wrath + case 53490: // Bullheaded + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + break; + case 54508: // Demonic Empowerment + mechanicImmunityMask |= (1 << MECHANIC_SNARE) | (1 << MECHANIC_ROOT) | (1 << MECHANIC_STUN); + break; + default: + if (miscVal < 1) + continue; + + mechanicImmunityMask |= 1 << miscVal; + break; + } + break; + } + case SPELL_AURA_EFFECT_IMMUNITY: + { + immuneInfo.SpellEffectImmune.insert(static_cast(miscVal)); + break; + } + case SPELL_AURA_STATE_IMMUNITY: + { + immuneInfo.AuraTypeImmune.insert(static_cast(miscVal)); + break; + } + case SPELL_AURA_SCHOOL_IMMUNITY: + { + schoolImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: + { + applyHarmfulAuraImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_DAMAGE_IMMUNITY: + { + damageImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_DISPEL_IMMUNITY: + { + dispelImmunity = uint32(miscVal); + break; + } + default: + break; + } + + immuneInfo.SchoolImmuneMask = schoolImmunityMask; + immuneInfo.ApplyHarmfulAuraImmuneMask = applyHarmfulAuraImmunityMask; + immuneInfo.MechanicImmuneMask = mechanicImmunityMask; + immuneInfo.DispelImmune = dispelImmunity; + immuneInfo.DamageSchoolMask = damageImmunityMask; + + immuneInfo.AuraTypeImmune.shrink_to_fit(); + immuneInfo.SpellEffectImmune.shrink_to_fit(); + + _allowedMechanicMask |= immuneInfo.MechanicImmuneMask; + } + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_STUNNED)) + { + switch (Id) + { + case 22812: // Barkskin + case 47585: // Dispersion + _allowedMechanicMask |= + (1 << MECHANIC_STUN) | + (1 << MECHANIC_FREEZE) | + (1 << MECHANIC_KNOCKOUT) | + (1 << MECHANIC_SLEEP); + break; + case 49039: // Lichborne, don't allow normal stuns + break; + default: + _allowedMechanicMask |= (1 << MECHANIC_STUN); + break; + } + } + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_CONFUSED)) + _allowedMechanicMask |= (1 << MECHANIC_DISORIENTED); + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_FLEEING)) + { + switch (Id) + { + case 22812: // Barkskin + case 47585: // Dispersion + _allowedMechanicMask |= (1 << MECHANIC_FEAR) | (1 << MECHANIC_HORROR); + break; + default: + _allowedMechanicMask |= (1 << MECHANIC_FEAR); + break; + } + } +} + +void SpellInfo::ApplyAllSpellImmunitiesTo(Unit* target, uint8 effIndex, bool apply) const +{ + ImmunityInfo const& immuneInfo = _immunityInfo[effIndex]; + + if (uint32 schoolImmunity = immuneInfo.SchoolImmuneMask) + { + target->ApplySpellImmune(Id, IMMUNITY_SCHOOL, schoolImmunity, apply); + + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + target->RemoveAppliedAuras([this, schoolImmunity](AuraApplication const* aurApp) -> bool + { + SpellInfo const* auraSpellInfo = aurApp->GetBase()->GetSpellInfo(); + return ((auraSpellInfo->GetSchoolMask() & schoolImmunity) != 0 && // Check for school mask + CanDispelAura(auraSpellInfo) && + (IsPositive() != aurApp->IsPositive()) && // Check spell vs aura possitivity + !auraSpellInfo->IsPassive() && // Don't remove passive auras + auraSpellInfo->Id != Id); // Don't remove self + }); + } + if (apply && schoolImmunity & SPELL_SCHOOL_MASK_NORMAL) + target->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::InvulnerabilityBuff); + } + + if (uint32 mechanicImmunity = immuneInfo.MechanicImmuneMask) + { + for (uint32 i = MECHANIC_CHARM; i < MAX_MECHANIC; ++i) + if (mechanicImmunity & (1 << i)) + target->ApplySpellImmune(Id, IMMUNITY_MECHANIC, i, apply); + + if (HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + if (apply) + target->RemoveAurasWithMechanic(mechanicImmunity, AuraRemoveFlags::ByDefault, Id); + else + { + std::vector aurasToUpdateTargets; + target->RemoveAppliedAuras([mechanicImmunity, &aurasToUpdateTargets](AuraApplication const* aurApp) + { + Aura* aura = aurApp->GetBase(); + if (aura->GetSpellInfo()->GetAllEffectsMechanicMask() & mechanicImmunity) + aurasToUpdateTargets.push_back(aura); + + // only update targets, don't remove anything + return false; + }); + + for (Aura* aura : aurasToUpdateTargets) + aura->UpdateTargetMap(aura->GetCaster()); + } + } + } + + if (uint32 dispelImmunity = immuneInfo.DispelImmune) + { + target->ApplySpellImmune(Id, IMMUNITY_DISPEL, dispelImmunity, apply); + + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + target->RemoveAppliedAuras([dispelImmunity](AuraApplication const* aurApp) -> bool + { + SpellInfo const* spellInfo = aurApp->GetBase()->GetSpellInfo(); + if (spellInfo->Dispel == dispelImmunity) + return true; + + return false; + }); + } + } + + if (uint32 damageImmunity = immuneInfo.DamageSchoolMask) + { + target->ApplySpellImmune(Id, IMMUNITY_DAMAGE, damageImmunity, apply); + + if (apply && damageImmunity & SPELL_SCHOOL_MASK_NORMAL) + target->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::InvulnerabilityBuff); + } + + for (AuraType auraType : immuneInfo.AuraTypeImmune) + { + target->ApplySpellImmune(Id, IMMUNITY_STATE, auraType, apply); + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + target->RemoveAurasByType(auraType, [](AuraApplication const* aurApp) -> bool + { + // if the aura has SPELL_ATTR0_NO_IMMUNITIES, then it cannot be removed by immunity + return !aurApp->GetBase()->GetSpellInfo()->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES); + }); + } + + for (SpellEffects effectType : immuneInfo.SpellEffectImmune) + target->ApplySpellImmune(Id, IMMUNITY_EFFECT, effectType, apply); +} + +bool SpellInfo::CanSpellProvideImmunityAgainstAura(SpellInfo const* auraSpellInfo) const +{ + if (!auraSpellInfo) + return false; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + ImmunityInfo const* immuneInfo = _immunityInfo + i; + + if (!auraSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) && !auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + if (uint32 schoolImmunity = immuneInfo->SchoolImmuneMask) + if ((auraSpellInfo->SchoolMask & schoolImmunity) != 0) + return true; + } + + if (uint32 mechanicImmunity = immuneInfo->MechanicImmuneMask) + if ((mechanicImmunity & (1 << auraSpellInfo->Mechanic)) != 0) + return true; + + if (uint32 dispelImmunity = immuneInfo->DispelImmune) + if (auraSpellInfo->Dispel == dispelImmunity) + return true; + + bool immuneToAllEffects = true; + for (uint8 effIndex = 0; effIndex < MAX_SPELL_EFFECTS; ++effIndex) + { + uint32 effectName = auraSpellInfo->Effects[effIndex].Effect; + if (!effectName) + continue; + + auto spellImmuneItr = immuneInfo->SpellEffectImmune.find(static_cast(effectName)); + if (spellImmuneItr == immuneInfo->SpellEffectImmune.cend()) + { + immuneToAllEffects = false; + break; + } + + if (uint32 mechanic = auraSpellInfo->Effects[effIndex].Mechanic) + { + if (!(immuneInfo->MechanicImmuneMask & (1 << mechanic))) + { + immuneToAllEffects = false; + break; + } + } + + if (!auraSpellInfo->HasAttribute(SPELL_ATTR3_ALWAYS_HIT)) + { + if (uint32 auraName = auraSpellInfo->Effects[effIndex].ApplyAuraName) + { + bool isImmuneToAuraEffectApply = false; + auto auraImmuneItr = immuneInfo->AuraTypeImmune.find(static_cast(auraName)); + if (auraImmuneItr != immuneInfo->AuraTypeImmune.cend()) + isImmuneToAuraEffectApply = true; + + if (!isImmuneToAuraEffectApply && !auraSpellInfo->IsPositiveEffect(effIndex) && !auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + if (uint32 applyHarmfulAuraImmunityMask = immuneInfo->ApplyHarmfulAuraImmuneMask) + if ((auraSpellInfo->GetSchoolMask() & applyHarmfulAuraImmunityMask) != 0) + isImmuneToAuraEffectApply = true; + } + + if (!isImmuneToAuraEffectApply) + { + immuneToAllEffects = false; + break; + } + } + } + } + + if (immuneToAllEffects) + return true; + } + + return false; +} + +// based on client Spell_C::CancelsAuraEffect +bool SpellInfo::SpellCancelsAuraEffect(SpellInfo const* auraSpellInfo, uint8 auraEffIndex) const +{ + if (!HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + return false; + + if (auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect != SPELL_EFFECT_APPLY_AURA && Effects[i].Effect != SPELL_EFFECT_APPLY_AURA_2) + continue; + + uint32 const miscValue = static_cast(Effects[i].MiscValue); + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_STATE_IMMUNITY: + if (miscValue != auraSpellInfo->Effects[auraEffIndex].ApplyAuraName) + continue; + break; + case SPELL_AURA_SCHOOL_IMMUNITY: + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: + if (auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES) || !(auraSpellInfo->SchoolMask & miscValue)) + continue; + break; + case SPELL_AURA_DISPEL_IMMUNITY: + if (miscValue != auraSpellInfo->Dispel) + continue; + break; + case SPELL_AURA_MECHANIC_IMMUNITY: + if (miscValue != auraSpellInfo->Mechanic) + { + if (miscValue != auraSpellInfo->Effects[auraEffIndex].Mechanic) + continue; + } + break; + default: + continue; + } + + return true; + } + + return false; +} + +uint32 SpellInfo::GetAllowedMechanicMask() const +{ + return _allowedMechanicMask; +} + +uint32 SpellInfo::GetMechanicImmunityMask(Unit* caster) const +{ + uint32 casterMechanicImmunityMask = caster->GetMechanicImmunityMask(); + uint32 mechanicImmunityMask = 0; + + if (CanBeInterrupted(caster, true)) + { + if (casterMechanicImmunityMask & (1 << MECHANIC_INTERRUPT)) + mechanicImmunityMask |= (1 << MECHANIC_INTERRUPT); + + if (casterMechanicImmunityMask & (1 << MECHANIC_SILENCE)) + mechanicImmunityMask |= (1 << MECHANIC_SILENCE); + } + + return mechanicImmunityMask; +} + +float SpellInfo::CalculateScaledCoefficient(Unit const* caster, float coefficient) const +{ + if (coefficient == 0.f || !caster || SpellScalingId == 0) + return coefficient; + + return coefficient *= GetSpellScalingMultiplier(caster->getLevel()); +} + +float SpellInfo::GetMinRange(bool positive) const +{ + if (!RangeEntry) + return 0.0f; + if (positive) + return RangeEntry->RangeMin[1]; + return RangeEntry->RangeMin[0]; +} + +float SpellInfo::GetMaxRange(bool positive, WorldObject* caster, Spell* spell) const +{ + if (!RangeEntry) + return 0.0f; + float range; + if (positive) + range = RangeEntry->RangeMax[1]; + else + range = RangeEntry->RangeMax[0]; + if (caster) + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Range, range, spell); + return range; +} + +int32 SpellInfo::GetDuration() const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + return (DurationEntry->Duration == -1) ? -1 : abs(DurationEntry->Duration); +} + +int32 SpellInfo::GetMaxDuration() const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + return (DurationEntry->MaxDuration == -1) ? -1 : abs(DurationEntry->MaxDuration); +} + +float SpellInfo::CalcPeriodicHasteMod(Unit const* caster) const +{ + float hasteMod = 1.f; + if (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC)) + hasteMod = caster->GetFloatValue(UNIT_MOD_CAST_HASTE); + else if (HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC) && caster->IsPlayer()) + hasteMod = caster->GetFloatValue(PLAYER_FIELD_MOD_HASTE); + + return hasteMod; +} + +int32 SpellInfo::CalcDuration(WorldObject const* caster /*= nullptr*/) const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + + int32 duration = GetMaxDuration(); + if (duration == -1) + return -1; + + Unit const* unitCaster = caster ? caster->ToUnit() : nullptr; + if (!unitCaster) + return std::min(GetMaxDuration(), DurationEntry->Duration); + + uint32 level = unitCaster->getLevel(); + if (MaxLevel > 0 && level > MaxLevel) + level = MaxLevel; + if (BaseLevel > 0) + level -= BaseLevel; + + duration = DurationEntry->Duration + DurationEntry->DurationPerLevel * level; + duration = std::min(GetMaxDuration(), duration); + + if (duration == -1) + return -1; + + // Increase duration based on combo points + if (HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DURATION)) + { + if (uint8 comboPoints = unitCaster->IsMovedByClient() ? unitCaster->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints() : 0) + { + if (GetDuration() != GetMaxDuration() && GetDuration() != -1) + duration += int32((GetMaxDuration() - GetDuration()) * comboPoints / 5); + } + } + + if (Player const* modOwner = unitCaster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Duration, duration); + + if (HasAttribute(SPELL_ATTR8_HASTE_AFFECTS_DURATION) || HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC)) + { + SpellEffIndex periodicEffectIndex = EFFECT_0; + bool hasPeriodicEffect = [&]() + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].AuraPeriod) + { + periodicEffectIndex = SpellEffIndex(i); + return true; + } + + return false; + }(); + + if (hasPeriodicEffect && !HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS) && (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC) || HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC))) + { + float hasteMod = CalcPeriodicHasteMod(unitCaster); + if (hasteMod > 0.001f) + { + if (HasAttribute(SPELL_ATTR8_HASTE_AFFECTS_DURATION)) + return int32(duration * hasteMod); + + int32 period = CalcPeriod(caster, periodicEffectIndex); + if (period > 0) + duration = int32(period * (duration / (float)period)); + } + } + } + + return duration; +} + +int32 SpellInfo::CalcPeriod(WorldObject const* caster, SpellEffIndex effIndex, Optional periodOverride /*= {}*/) const +{ + int32 period = 0; + if (periodOverride.has_value() && *periodOverride > 0) + period = *periodOverride; + else + { + if (Effects[effIndex].IsEffect() && Effects[effIndex].AuraPeriod != 0) + period = Effects[effIndex].AuraPeriod; + else + period = 5200; + } + + Unit const* unitCaster = caster ? caster->ToUnit() : nullptr; + if (!unitCaster) + return period; + + if (Player const* modOwner = unitCaster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Period, period); + + if (!HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS) && (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC) || HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC))) + { + float hasteMod = CalcPeriodicHasteMod(unitCaster); + if (hasteMod > 0.001f) + period = ((float)period * hasteMod); + } + + return period; +} + +uint32 SpellInfo::CalcCastTime(uint8 level, Spell* spell /*= nullptr*/) const +{ + int32 castTime = 0; + if (!level && spell && spell->GetCaster()->IsUnit()) + level = spell->GetCaster()->ToUnit()->getLevel(); + + // not all spells have cast time index and this is all is passive abilities + if (level && Scaling.CastTimeMax > 0) + { + castTime = Scaling.CastTimeMax; + if (Scaling.CastTimeMaxLevel > level) + castTime = Scaling.CastTimeMin + int32(level - 1) * (Scaling.CastTimeMax - Scaling.CastTimeMin) / (Scaling.CastTimeMaxLevel - 1); + } + else if (CastTimeEntry) + castTime = CastTimeEntry->Base; + + if (!castTime) + return 0; + + if (spell) + spell->GetCaster()->ModSpellCastTime(this, castTime, spell); + + return (castTime > 0) ? uint32(castTime) : 0; +} + +uint32 SpellInfo::GetMaxTicks() const +{ + uint32 totalTicks = 0; + int32 DotDuration = GetDuration(); + + for (uint8 x = 0; x < MAX_SPELL_EFFECTS; x++) + { + if (Effects[x].Effect == SPELL_EFFECT_APPLY_AURA || Effects[x].Effect == SPELL_EFFECT_APPLY_AURA_2) + { + switch (Effects[x].ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_48: + case SPELL_AURA_POWER_BURN: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_PERIODIC_DUMMY: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + case SPELL_AURA_PERIODIC_HEALTH_FUNNEL: + // skip infinite periodics + if (Effects[x].AuraPeriod > 0 && DotDuration > 0) + { + totalTicks = static_cast(DotDuration) / Effects[x].AuraPeriod; + if (HasAttribute(SPELL_ATTR5_EXTRA_INITIAL_PERIOD)) + ++totalTicks; + } + break; + default: + break; + } + } + } + + return totalTicks; +} + +uint32 SpellInfo::GetRecoveryTime() const +{ + return RecoveryTime > CategoryRecoveryTime ? RecoveryTime : CategoryRecoveryTime; +} + +int32 SpellInfo::CalcPowerCost(WorldObject const* caster, SpellSchoolMask schoolMask, Spell* spell) const +{ + // gameobject casts don't use power + Unit const* unitCaster = caster->ToUnit(); + if (!unitCaster) + return 0; + + // Spell drain all exist power on cast (Only paladin lay of Hands) + if (HasAttribute(SPELL_ATTR1_USE_ALL_MANA)) + { + // If power type - health drain all + if (PowerType == POWER_HEALTH) + return unitCaster->GetHealth(); + // Else drain all power + if (PowerType < MAX_POWERS) + return unitCaster->GetPower(static_cast<::PowerType>(PowerType)); + + TC_LOG_ERROR("spells", "SpellInfo::CalcPowerCost: Unknown power type '%d' in spell %d", PowerType, Id); + return 0; + } + + bool invertSign = false; + int32 powerCost = 0; + + if (SpellPowerId) + { + int32 manaCost = ManaCost; + powerCost = manaCost; + float powerCostPct = ManaCostPercentage2 != 0.f ? ManaCostPercentage2 : (float)ManaCostPercentage; + + if (manaCost < 0 || powerCostPct < 0.f) + { + manaCost = std::abs(manaCost); + powerCostPct = std::fabs(powerCostPct); + invertSign = true; + powerCost = manaCost; + } + + // Base calculation + if (ManaCostPercentage) + { + int32 ressource = 0; + switch (PowerType) + { + // health as power used + case POWER_HEALTH: + ressource = unitCaster->GetCreateHealth(); + break; + case POWER_MANA: + ressource = unitCaster->GetCreateMana(); + break; + case POWER_RAGE: + case POWER_FOCUS: + case POWER_ENERGY: + ressource = unitCaster->GetMaxPower(static_cast<::PowerType>(PowerType)); + break; + case POWER_RUNE: + case POWER_RUNIC_POWER: + TC_LOG_DEBUG("spells", "CalculateManaCost: Not implemented yet!"); + break; + default: + TC_LOG_ERROR("spells", "CalculateManaCost: Unknown power type '%d' in spell %d", PowerType, Id); + return 0; + } + + manaCost += int32((ressource * powerCostPct) / 100); + powerCost = manaCost; + } + + // Scaling + if (SpellScalingId) + powerCost = int32(ceilf(GetSpellScalingMultiplier(unitCaster->getLevel(), true) * (double)powerCost)); + else + { + uint32 manaCostPerLevel = invertSign ? -int32(ManaCostPerlevel) : ManaCostPerlevel; + powerCost = manaCostPerLevel * SpellLevel + manaCost; + } + } + + // Flat mod from caster auras by spell school and power type + Unit::AuraEffectList const& auras = unitCaster->GetAuraEffectsByType(SPELL_AURA_MOD_POWER_COST_SCHOOL); + for (Unit::AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + if (!((*i)->GetMiscValue() & schoolMask)) + continue; + if (!((*i)->GetMiscValueB() & (1 << PowerType))) + continue; + powerCost += (*i)->GetAmount(); + } + + // Shiv - costs 20 + weaponSpeed*10 energy (apply only to non-triggered spell with energy cost) + if (HasAttribute(SPELL_ATTR4_WEAPON_SPEED_COST_SCALING)) + { + uint32 speed = 0; + if (SpellShapeshiftFormEntry const* ss = sSpellShapeshiftFormStore.LookupEntry(unitCaster->GetShapeshiftForm())) + speed = ss->CombatRoundTime; + else + { + WeaponAttackType slot = BASE_ATTACK; + if (!HasAttribute(SPELL_ATTR3_REQUIRES_MAIN_HAND_WEAPON) && HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + slot = OFF_ATTACK; + + speed = unitCaster->GetBaseAttackTime(slot); + } + + powerCost += speed / 100; + } + + // Apply cost mod by spell + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::PowerCost0, powerCost, spell); + + if (!unitCaster->IsControlledByPlayer()) + { + if (HasAttribute(SPELL_ATTR0_SCALES_WITH_CREATURE_LEVEL)) + { + GtNPCManaCostScalerEntry const* spellScaler = sGtNPCManaCostScalerStore.LookupEntry(SpellLevel - 1); + GtNPCManaCostScalerEntry const* casterScaler = sGtNPCManaCostScalerStore.LookupEntry(unitCaster->getLevel() - 1); + if (spellScaler && spellScaler->ratio > 0.f && casterScaler && casterScaler->ratio > 0.f) + powerCost *= casterScaler->ratio / spellScaler->ratio; + } + } + + if (invertSign) + powerCost = -powerCost; + + // PCT mod from user auras by spell school and power type + Unit::AuraEffectList const& aurasPct = unitCaster->GetAuraEffectsByType(SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT); + for (Unit::AuraEffectList::const_iterator i = aurasPct.begin(); i != aurasPct.end(); ++i) + { + if (!((*i)->GetMiscValue() & schoolMask)) + continue; + if (!((*i)->GetMiscValueB() & (1 << PowerType))) + continue; + powerCost += CalculatePct(powerCost, (*i)->GetAmount()); + } + if (powerCost < 0) + powerCost = 0; + return powerCost; +} + +float SpellInfo::GetSpellScalingMultiplier(int32 targetLevel, bool isPowerCostRelated /*= false*/) const +{ + if (targetLevel <= 0 || Scaling.Class == 0) + return 1.f; + + float multiplier = 1.f; + float scalingMultiplier = 1.f; + + if (targetLevel < Scaling.CastTimeMaxLevel && Scaling.CastTimeMax) + { + int32 castTime = Scaling.CastTimeMin + ((targetLevel - 1) * (Scaling.CastTimeMax - Scaling.CastTimeMin)) / (Scaling.CastTimeMaxLevel - 1); + multiplier = castTime / static_cast(Scaling.CastTimeMax); + scalingMultiplier = castTime / static_cast(Scaling.CastTimeMax); + } + + if (!isPowerCostRelated) + { + if (targetLevel < Scaling.NerfMaxLevel) + scalingMultiplier = ((((1.f - Scaling.NerfFactor) * (targetLevel - 1)) / (Scaling.NerfMaxLevel - 1)) + Scaling.NerfFactor) * multiplier; + } + + return scalingMultiplier; +} + +bool SpellInfo::IsRanked() const +{ + return ChainEntry != nullptr; +} + +uint8 SpellInfo::GetRank() const +{ + if (!ChainEntry) + return 1; + return ChainEntry->rank; +} + +SpellInfo const* SpellInfo::GetFirstRankSpell() const +{ + if (!ChainEntry) + return this; + return ChainEntry->first; +} +SpellInfo const* SpellInfo::GetLastRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->last; +} +SpellInfo const* SpellInfo::GetNextRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->next; +} +SpellInfo const* SpellInfo::GetPrevRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->prev; +} + +SpellInfo const* SpellInfo::GetAuraRankForLevel(uint8 level) const +{ + // ignore passive spells + if (IsPassive()) + return this; + + // Client ignores spell with these attributes (sub_53D9D0) + if (HasAttribute(SPELL_ATTR0_AURA_IS_DEBUFF) || HasAttribute(SPELL_ATTR2_ALLOW_LOW_LEVEL_BUFF)) + return this; + + bool needRankSelection = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (IsPositiveEffect(i) && + (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA || + Effects[i].Effect == SPELL_EFFECT_APPLY_AURA_2 || + Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_PARTY || + Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID) && + !Effects[i].Scaling.Coefficient) + { + needRankSelection = true; + break; + } + } + + // not required + if (!needRankSelection) + return this; + + for (SpellInfo const* nextSpellInfo = this; nextSpellInfo != nullptr; nextSpellInfo = nextSpellInfo->GetPrevRankSpell()) + { + // if found appropriate level + if (uint32(level + 10) >= nextSpellInfo->SpellLevel) + return nextSpellInfo; + + // one rank less then + } + + // not found + return nullptr; +} + +bool SpellInfo::IsRankOf(SpellInfo const* spellInfo) const +{ + return GetFirstRankSpell() == spellInfo->GetFirstRankSpell(); +} + +bool SpellInfo::IsDifferentRankOf(SpellInfo const* spellInfo) const +{ + if (Id == spellInfo->Id) + return false; + return IsRankOf(spellInfo); +} + +bool SpellInfo::IsHighRankOf(SpellInfo const* spellInfo) const +{ + if (ChainEntry && spellInfo->ChainEntry) + if (ChainEntry->first == spellInfo->ChainEntry->first) + if (ChainEntry->rank > spellInfo->ChainEntry->rank) + return true; + + return false; +} + +void SpellInfo::_InitializeExplicitTargetMask() +{ + bool srcSet = false; + bool dstSet = false; + uint32 targetMask = Targets; + // prepare target mask using effect target entries + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect()) + continue; + + targetMask |= Effects[i].TargetA.GetExplicitTargetMask(srcSet, dstSet); + targetMask |= Effects[i].TargetB.GetExplicitTargetMask(srcSet, dstSet); + + // add explicit target flags based on spell effects which have EFFECT_IMPLICIT_TARGET_EXPLICIT and no valid target provided + if (Effects[i].GetImplicitTargetType() != EFFECT_IMPLICIT_TARGET_EXPLICIT) + continue; + + // extend explicit target mask only if valid targets for effect could not be provided by target types + uint32 effectTargetMask = Effects[i].GetMissingTargetMask(srcSet, dstSet, targetMask); + + // don't add explicit object/dest flags when spell has no max range + if (GetMaxRange(true) == 0.0f && GetMaxRange(false) == 0.0f) + effectTargetMask &= ~(TARGET_FLAG_UNIT_MASK | TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_CORPSE_MASK | TARGET_FLAG_DEST_LOCATION); + + targetMask |= effectTargetMask; + } + + ExplicitTargetMask = targetMask; +} + +inline bool _isPositiveTarget(SpellInfo const* spellInfo, uint8 effIndex) +{ + if (!spellInfo->Effects[effIndex].IsEffect()) + return true; + + return (spellInfo->Effects[effIndex].TargetA.GetCheckType() != TARGET_CHECK_ENEMY && + spellInfo->Effects[effIndex].TargetB.GetCheckType() != TARGET_CHECK_ENEMY); +} + +bool _isPositiveEffectImpl(SpellInfo const* spellInfo, uint8 effIndex, std::unordered_set>& visited) +{ + if (!spellInfo->Effects[effIndex].IsEffect()) + return true; + + // attribute may be already set in DB + if (!spellInfo->IsPositiveEffect(effIndex)) + return false; + + // passive auras like talents are all positive + if (spellInfo->IsPassive()) + return true; + + // not found a single positive spell with this attribute + if (spellInfo->HasAttribute(SPELL_ATTR0_AURA_IS_DEBUFF)) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR4_AURA_IS_BUFF)) + return true; + + visited.insert({ spellInfo->Id, effIndex }); + + //We need scaling level info for some auras that compute bp 0 or positive but should be debuffs + float bpScalePerLevel = spellInfo->Effects[effIndex].RealPointsPerLevel; + int32 bp = spellInfo->Effects[effIndex].CalcValue(); + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + switch (spellInfo->Id) + { + case 40268: // Spiritual Vengeance, Teron Gorefiend, Black Temple + case 61987: // Avenging Wrath Marker + case 61988: // Divine Shield exclude aura + case 64412: // Phase Punch, Algalon the Observer, Ulduar + case 72410: // Rune of Blood, Saurfang, Icecrown Citadel + case 71204: // Touch of Insignificance, Lady Deathwhisper, Icecrown Citadel + return false; + case 24732: // Bat Costume + case 30877: // Tag Murloc + case 61716: // Rabbit Costume + case 61734: // Noblegarden Bunny + case 62344: // Fists of Stone + case 50344: // Dream Funnel + case 61819: // Manabonked! (item) + case 61834: // Manabonked! (minigob) + case 73523: // Rigor Mortis + return true; + default: + break; + } + break; + case SPELLFAMILY_ROGUE: + switch (spellInfo->Id) + { + // Envenom must be considered as a positive effect even though it deals damage + case 32645: // Envenom + return true; + case 40251: // Shadow of Death, Teron Gorefiend, Black Temple + return false; + default: + break; + } + break; + case SPELLFAMILY_MAGE: + // Arcane Missiles + if (spellInfo->SpellFamilyFlags[0] == 0x00000800) + return false; + break; + case SPELLFAMILY_WARRIOR: + // Slam, Execute + if ((spellInfo->SpellFamilyFlags[0] & 0x20200000) != 0) + return false; + break; + default: + break; + } + + switch (spellInfo->Mechanic) + { + case MECHANIC_IMMUNE_SHIELD: + return true; + default: + break; + } + + // Special case: effects which determine positivity of whole spell + if (spellInfo->HasAttribute(SPELL_ATTR1_AURA_UNIQUE)) // @todo: rename to and handle SPELL_ATTR1_AURA_UNIQUE + { + // check for targets, there seems to be an assortment of dummy triggering spells that should be negative + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (!_isPositiveTarget(spellInfo, i)) + return false; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (spellInfo->Effects[i].Effect) + { + case SPELL_EFFECT_HEAL: + case SPELL_EFFECT_LEARN_SPELL: + case SPELL_EFFECT_SKILL_STEP: + case SPELL_EFFECT_HEAL_PCT: + return true; + case SPELL_EFFECT_INSTAKILL: + if (i != effIndex && // for spells like 38044: instakill effect is negative but auras on target must count as buff + spellInfo->Effects[i].TargetA.GetTarget() == spellInfo->Effects[effIndex].TargetA.GetTarget() && + spellInfo->Effects[i].TargetB.GetTarget() == spellInfo->Effects[effIndex].TargetB.GetTarget()) + return false; + break; + default: + break; + } + + if (spellInfo->Effects[i].IsAura()) + { + switch (spellInfo->Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_STEALTH: + case SPELL_AURA_MOD_UNATTACKABLE: + return true; + case SPELL_AURA_SCHOOL_HEAL_ABSORB: + case SPELL_AURA_EMPATHY: + case SPELL_AURA_MOD_DAMAGE_FROM_CASTER: + case SPELL_AURA_PREVENTS_FLEEING: + return false; + default: + break; + } + } + } + + switch (spellInfo->Effects[effIndex].Effect) + { + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_INSTAKILL: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_STEAL_BENEFICIAL_BUFF: + case SPELL_EFFECT_INTERRUPT_CAST: + case SPELL_EFFECT_PICKPOCKET: + case SPELL_EFFECT_GAMEOBJECT_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE_PCT: + case SPELL_EFFECT_APPLY_AREA_AURA_ENEMY: + case SPELL_EFFECT_TAMECREATURE: + case SPELL_EFFECT_DISTRACT: + return false; + case SPELL_EFFECT_ENERGIZE: + case SPELL_EFFECT_ENERGIZE_PCT: + case SPELL_EFFECT_HEAL_PCT: + case SPELL_EFFECT_HEAL_MAX_HEALTH: + case SPELL_EFFECT_HEAL_MECHANICAL: + return true; + case SPELL_EFFECT_KNOCK_BACK: + case SPELL_EFFECT_CHARGE: + case SPELL_EFFECT_PERSISTENT_AREA_AURA: + case SPELL_EFFECT_ATTACK_ME: + case SPELL_EFFECT_POWER_BURN: + // check targets + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_EFFECT_DISPEL: + // non-positive dispel + switch (spellInfo->Effects[effIndex].MiscValue) + { + case DISPEL_STEALTH: + case DISPEL_INVISIBILITY: + case DISPEL_ENRAGE: + return false; + default: + break; + } + + // also check targets + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_EFFECT_DISPEL_MECHANIC: + if (!_isPositiveTarget(spellInfo, effIndex)) + { + // non-positive mechanic dispel on negative target + switch (spellInfo->Effects[effIndex].MiscValue) + { + case MECHANIC_BANDAGE: + case MECHANIC_SHIELD: + case MECHANIC_MOUNT: + case MECHANIC_INVULNERABILITY: + return false; + default: + break; + } + } + break; + case SPELL_EFFECT_THREAT: + case SPELL_EFFECT_MODIFY_THREAT_PERCENT: + // check targets AND basepoints + if (!_isPositiveTarget(spellInfo, effIndex) && bp > 0) + return false; + break; + default: + break; + } + + if (spellInfo->Effects[effIndex].IsAura()) + { + // non-positive aura use + switch (spellInfo->Effects[effIndex].ApplyAuraName) + { + case SPELL_AURA_MOD_STAT: // dependent from basepoint sign (negative -> negative) + case SPELL_AURA_MOD_SKILL: + case SPELL_AURA_MOD_DODGE_PERCENT: + case SPELL_AURA_MOD_HEALING_DONE: + case SPELL_AURA_MOD_DAMAGE_DONE_CREATURE: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_MOD_CRIT_PCT: + case SPELL_AURA_MOD_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_CRIT_CHANCE: + case SPELL_AURA_MOD_RANGED_HASTE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE_2: + case SPELL_AURA_MOD_CASTING_SPEED_NOT_STACK: + case SPELL_AURA_HASTE_SPELLS: + case SPELL_AURA_HASTE_RANGED: + case SPELL_AURA_MOD_DETECT_RANGE: + case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT: + case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE: + case SPELL_AURA_MOD_INCREASE_SWIM_SPEED: + case SPELL_AURA_MOD_PERCENT_STAT: + case SPELL_AURA_MOD_INCREASE_HEALTH: + case SPELL_AURA_MOD_SPEED_ALWAYS: + if (bp < 0 || bpScalePerLevel < 0) //TODO: What if both are 0? Should it be a buff or debuff? + return false; + break; + case SPELL_AURA_MOD_ATTACKSPEED: // some buffs have negative bp, check both target and bp + case SPELL_AURA_MOD_MELEE_HASTE: + case SPELL_AURA_MOD_DAMAGE_DONE: + case SPELL_AURA_MOD_RESISTANCE: + case SPELL_AURA_MOD_RESISTANCE_PCT: + case SPELL_AURA_MOD_RATING: + case SPELL_AURA_MOD_ATTACK_POWER: + case SPELL_AURA_MOD_RANGED_ATTACK_POWER: + case SPELL_AURA_MOD_DAMAGE_PERCENT_DONE: + case SPELL_AURA_MOD_SPEED_SLOW_ALL: + case SPELL_AURA_MELEE_SLOW: + case SPELL_AURA_MOD_ATTACK_POWER_PCT: + case SPELL_AURA_MOD_HEALING_DONE_PERCENT: + case SPELL_AURA_MOD_HEALING_PCT: + case SPELL_AURA_MOD_DAMAGE_DONE_VERSUS_AURASTATE: + if (!_isPositiveTarget(spellInfo, effIndex) && bp < 0) + return false; + break; + case SPELL_AURA_MOD_DAMAGE_TAKEN: // dependent from basepoint sign (positive -> negative) + case SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN: + case SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN_PCT: + case SPELL_AURA_MOD_POWER_COST_SCHOOL: + case SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT: + case SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT: + case SPELL_AURA_BYPASS_ARMOR_FOR_CASTER: + if (bp > 0) + return false; + break; + case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN: // check targets and basepoints (ex Recklessness) + if (!_isPositiveTarget(spellInfo, effIndex) && bp > 0) + return false; + break; + case SPELL_AURA_MOD_HEALTH_REGEN_PERCENT: // check targets and basepoints (target enemy and negative bp -> negative) + if (!_isPositiveTarget(spellInfo, effIndex) && bp < 0) + return false; + break; + case SPELL_AURA_ADD_TARGET_TRIGGER: + return true; + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + if (!_isPositiveTarget(spellInfo, effIndex)) + { + if (SpellInfo const* spellTriggeredProto = sSpellMgr->GetSpellInfo(spellInfo->Effects[effIndex].TriggerSpell)) + { + // negative targets of main spell return early + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // already seen this + if (visited.count({ spellTriggeredProto->Id, i }) > 0) + continue; + + if (!spellTriggeredProto->Effects[i].Effect) + continue; + + // if non-positive trigger cast targeted to positive target this main cast is non-positive + // this will place this spell auras as debuffs + if (_isPositiveTarget(spellTriggeredProto, i) && !_isPositiveEffectImpl(spellTriggeredProto, i, visited)) + return false; + } + } + } + break; + case SPELL_AURA_PERIODIC_TRIGGER_SPELL: + case SPELL_AURA_MOD_STUN: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_MOD_DECREASE_SPEED: + case SPELL_AURA_MOD_FEAR: + case SPELL_AURA_MOD_TAUNT: + // special auras: they may have non negative target but still need to be marked as debuff + // checked again after all effects (SpellInfo::_InitializeSpellPositivity) + case SPELL_AURA_MOD_PACIFY: + case SPELL_AURA_MOD_PACIFY_SILENCE: + case SPELL_AURA_MOD_DISARM: + case SPELL_AURA_MOD_DISARM_OFFHAND: + case SPELL_AURA_MOD_DISARM_RANGED: + case SPELL_AURA_MOD_CHARM: + case SPELL_AURA_AOE_CHARM: + case SPELL_AURA_MOD_POSSESS: + case SPELL_AURA_MOD_LANGUAGE: + case SPELL_AURA_DAMAGE_SHIELD: + case SPELL_AURA_PROC_TRIGGER_SPELL: + case SPELL_AURA_MOD_ATTACKER_MELEE_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_RANGED_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE: + case SPELL_AURA_DUMMY: + case SPELL_AURA_PERIODIC_DUMMY: + case SPELL_AURA_MOD_HEALING: + case SPELL_AURA_MOD_WEAPON_CRIT_PERCENT: + case SPELL_AURA_POWER_BURN: + case SPELL_AURA_MOD_COOLDOWN: + case SPELL_AURA_MOD_INCREASE_SPEED: + case SPELL_AURA_MOD_PARRY_PERCENT: + case SPELL_AURA_SET_VEHICLE_ID: + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_EFFECT_IMMUNITY: + case SPELL_AURA_OVERRIDE_CLASS_SCRIPTS: + case SPELL_AURA_MOD_SHAPESHIFT: + case SPELL_AURA_MOD_THREAT: + case SPELL_AURA_PROC_TRIGGER_SPELL_WITH_VALUE: + // check target for positive and negative spells + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_AURA_MOD_CONFUSE: + case SPELL_AURA_CHANNEL_DEATH_ITEM: + case SPELL_AURA_MOD_ROOT: + case SPELL_AURA_MOD_SILENCE: + case SPELL_AURA_MOD_DETAUNT: + case SPELL_AURA_GHOST: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_MOD_STALKED: + case SPELL_AURA_PREVENT_RESURRECTION: + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS: + case SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS: + case SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE: + return false; + case SPELL_AURA_MECHANIC_IMMUNITY: + { + // non-positive immunities + switch (spellInfo->Effects[effIndex].MiscValue) + { + case MECHANIC_BANDAGE: + case MECHANIC_SHIELD: + case MECHANIC_MOUNT: + case MECHANIC_INVULNERABILITY: + return false; + default: + break; + } + break; + } + case SPELL_AURA_ADD_FLAT_MODIFIER: // mods + case SPELL_AURA_ADD_PCT_MODIFIER: + { + switch (SpellModOp(spellInfo->Effects[effIndex].MiscValue)) + { + case SpellModOp::ChangeCastTime: // dependent from basepoint sign (positive -> negative) + case SpellModOp::Period: + case SpellModOp::PowerCostOnMiss: + case SpellModOp::StartCooldown: + if (bp > 0) + return false; + break; + case SpellModOp::Cooldown: + case SpellModOp::PowerCost0: + if (!spellInfo->IsPositive() && bp > 0) // dependent on prev effects too (ex Arcane Power) + return false; + break; + case SpellModOp::PointsIndex0: // always positive + case SpellModOp::PointsIndex1: + case SpellModOp::PointsIndex2: + case SpellModOp::Points: + case SpellModOp::Hate: + case SpellModOp::ChainAmplitude: + case SpellModOp::Amplitude: + return true; + case SpellModOp::Duration: + case SpellModOp::CritChance: + case SpellModOp::HealingAndDamage: + case SpellModOp::ChainTargets: + if (!spellInfo->IsPositive() && bp < 0) // dependent on prev effects too + return false; + break; + default: // dependent from basepoint sign (negative -> negative) + if (bp < 0) + return false; + break; + } + break; + } + default: + break; + } + } + // negative spell if triggered spell is negative + if (!spellInfo->Effects[effIndex].ApplyAuraName && spellInfo->Effects[effIndex].TriggerSpell) + { + if (SpellInfo const* spellTriggeredProto = sSpellMgr->GetSpellInfo(spellInfo->Effects[effIndex].TriggerSpell)) + { + // spells with at least one negative effect are considered negative + // some self-applied spells have negative effects but in self casting case negative check ignored. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // already seen this + if (visited.count({ spellTriggeredProto->Id, i }) > 0) + continue; + + if (!spellTriggeredProto->Effects[i].IsEffect()) + continue; + + if (!_isPositiveEffectImpl(spellTriggeredProto, i, visited)) + return false; + } + } + } + + // ok, positive + return true; +} + +void SpellInfo::_InitializeSpellPositivity() +{ + std::unordered_set> visited; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (!_isPositiveEffectImpl(this, i, visited)) + AttributesCu |= (SPELL_ATTR0_CU_NEGATIVE_EFF0 << i); + + // additional checks after effects marked + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect() || !IsPositiveEffect(i)) + continue; + + switch (Effects[i].ApplyAuraName) + { + // has other non positive effect? + // then it should be marked negative if has same target as negative effect (ex 8510, 8511, 8893, 10267) + case SPELL_AURA_DUMMY: + case SPELL_AURA_MOD_STUN: + case SPELL_AURA_MOD_FEAR: + case SPELL_AURA_MOD_TAUNT: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_MOD_ATTACKSPEED: + case SPELL_AURA_MOD_DECREASE_SPEED: + { + for (uint8 j = i + 1; j < MAX_SPELL_EFFECTS; ++j) + if (!IsPositiveEffect(j) && Effects[i].TargetA.GetTarget() == Effects[j].TargetA.GetTarget() && Effects[i].TargetB.GetTarget() == Effects[j].TargetB.GetTarget()) + AttributesCu |= (SPELL_ATTR0_CU_NEGATIVE_EFF0 << i); + + break; + } + default: + break; + } + } +} + +SpellTargetRestrictionsEntry const* SpellInfo::GetSpellTargetRestrictions() const +{ + return SpellTargetRestrictionsId ? sSpellTargetRestrictionsStore.LookupEntry(SpellTargetRestrictionsId) : nullptr; +} + +SpellEquippedItemsEntry const* SpellInfo::GetSpellEquippedItems() const +{ + return SpellEquippedItemsId ? sSpellEquippedItemsStore.LookupEntry(SpellEquippedItemsId) : nullptr; +} + +SpellInterruptsEntry const* SpellInfo::GetSpellInterrupts() const +{ + return SpellInterruptsId ? sSpellInterruptsStore.LookupEntry(SpellInterruptsId) : nullptr; +} + +SpellLevelsEntry const* SpellInfo::GetSpellLevels() const +{ + return SpellLevelsId ? sSpellLevelsStore.LookupEntry(SpellLevelsId) : nullptr; +} + +SpellPowerEntry const* SpellInfo::GetSpellPower() const +{ + return SpellPowerId ? sSpellPowerStore.LookupEntry(SpellPowerId) : nullptr; +} + +SpellReagentsEntry const* SpellInfo::GetSpellReagents() const +{ + return SpellReagentsId ? sSpellReagentsStore.LookupEntry(SpellReagentsId) : nullptr; +} + +SpellScalingEntry const* SpellInfo::GetSpellScaling() const +{ + return SpellScalingId ? sSpellScalingStore.LookupEntry(SpellScalingId) : nullptr; +} + +SpellShapeshiftEntry const* SpellInfo::GetSpellShapeshift() const +{ + return SpellShapeshiftId ? sSpellShapeshiftStore.LookupEntry(SpellShapeshiftId) : nullptr; +} + +SpellTotemsEntry const* SpellInfo::GetSpellTotems() const +{ + return SpellTotemsId ? sSpellTotemsStore.LookupEntry(SpellTotemsId) : nullptr; +} + +SpellAuraOptionsEntry const* SpellInfo::GetSpellAuraOptions() const +{ + return SpellAuraOptionsId ? sSpellAuraOptionsStore.LookupEntry(SpellAuraOptionsId) : nullptr; +} + +SpellAuraRestrictionsEntry const* SpellInfo::GetSpellAuraRestrictions() const +{ + return SpellAuraRestrictionsId ? sSpellAuraRestrictionsStore.LookupEntry(SpellAuraRestrictionsId) : nullptr; +} + +SpellCastingRequirementsEntry const* SpellInfo::GetSpellCastingRequirements() const +{ + return SpellCastingRequirementsId ? sSpellCastingRequirementsStore.LookupEntry(SpellCastingRequirementsId) : nullptr; +} + +SpellCategoriesEntry const* SpellInfo::GetSpellCategories() const +{ + return SpellCategoriesId ? sSpellCategoriesStore.LookupEntry(SpellCategoriesId) : nullptr; +} + +SpellClassOptionsEntry const* SpellInfo::GetSpellClassOptions() const +{ + return SpellClassOptionsId ? sSpellClassOptionsStore.LookupEntry(SpellClassOptionsId) : nullptr; +} + +SpellCooldownsEntry const* SpellInfo::GetSpellCooldowns() const +{ + return SpellCooldownsId ? sSpellCooldownsStore.LookupEntry(SpellCooldownsId) : nullptr; +} + +void SpellInfo::_UnloadImplicitTargetConditionLists() +{ + // find the same instances of ConditionList and delete them. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + ConditionContainer* cur = Effects[i].ImplicitTargetConditions; + if (!cur) + continue; + for (uint8 j = i; j < MAX_SPELL_EFFECTS; ++j) + { + if (Effects[j].ImplicitTargetConditions == cur) + Effects[j].ImplicitTargetConditions = nullptr; + } + delete cur; + } +} diff --git a/src/server/game/Spells/SpellInfo.cpp.bak-vianomod-v1-20261001-195036 b/src/server/game/Spells/SpellInfo.cpp.bak-vianomod-v1-20261001-195036 new file mode 100644 index 0000000000..4d2aae35fb --- /dev/null +++ b/src/server/game/Spells/SpellInfo.cpp.bak-vianomod-v1-20261001-195036 @@ -0,0 +1,4527 @@ +/* + * 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 . + */ + +#include "SpellInfo.h" +#include "Battleground.h" +#include "Corpse.h" +#include "Creature.h" +#include "DBCStores.h" +#include "GameClient.h" +#include "GameTables.h" +#include "InstanceScript.h" +#include "Item.h" +#include "ItemTemplate.h" +#include "Log.h" +#include "LootMgr.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "Random.h" +#include "Spell.h" +#include "SpellAuraDefines.h" +#include "SpellAuraEffects.h" +#include "SpellMgr.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +uint32 GetTargetFlagMask(SpellTargetObjectTypes objType) +{ + switch (objType) + { + case TARGET_OBJECT_TYPE_DEST: + return TARGET_FLAG_DEST_LOCATION; + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + return TARGET_FLAG_DEST_LOCATION | TARGET_FLAG_UNIT; + case TARGET_OBJECT_TYPE_CORPSE_ALLY: + return TARGET_FLAG_CORPSE_ALLY; + case TARGET_OBJECT_TYPE_CORPSE_ENEMY: + return TARGET_FLAG_CORPSE_ENEMY; + case TARGET_OBJECT_TYPE_CORPSE: + return TARGET_FLAG_CORPSE_ALLY | TARGET_FLAG_CORPSE_ENEMY; + case TARGET_OBJECT_TYPE_UNIT: + return TARGET_FLAG_UNIT; + case TARGET_OBJECT_TYPE_GOBJ: + return TARGET_FLAG_GAMEOBJECT; + case TARGET_OBJECT_TYPE_GOBJ_ITEM: + return TARGET_FLAG_GAMEOBJECT_ITEM; + case TARGET_OBJECT_TYPE_ITEM: + return TARGET_FLAG_ITEM; + case TARGET_OBJECT_TYPE_SRC: + return TARGET_FLAG_SOURCE_LOCATION; + default: + return TARGET_FLAG_NONE; + } +} + +SpellImplicitTargetInfo::SpellImplicitTargetInfo(uint32 target) +{ + _target = Targets(target); +} + +bool SpellImplicitTargetInfo::IsArea() const +{ + return GetSelectionCategory() == TARGET_SELECT_CATEGORY_AREA || GetSelectionCategory() == TARGET_SELECT_CATEGORY_CONE; +} + +SpellTargetSelectionCategories SpellImplicitTargetInfo::GetSelectionCategory() const +{ + return _data[_target].SelectionCategory; +} + +SpellTargetReferenceTypes SpellImplicitTargetInfo::GetReferenceType() const +{ + return _data[_target].ReferenceType; +} + +SpellTargetObjectTypes SpellImplicitTargetInfo::GetObjectType() const +{ + return _data[_target].ObjectType; +} + +SpellTargetCheckTypes SpellImplicitTargetInfo::GetCheckType() const +{ + return _data[_target].SelectionCheckType; +} + +SpellTargetDirectionTypes SpellImplicitTargetInfo::GetDirectionType() const +{ + return _data[_target].DirectionType; +} + +float SpellImplicitTargetInfo::CalcDirectionAngle(SpellEffectInfo const& effectInfo) const +{ + switch (GetDirectionType()) + { + case TARGET_DIR_FRONT: + return 0.0f; + case TARGET_DIR_BACK: + return static_cast(M_PI); + case TARGET_DIR_RIGHT: + return static_cast(-M_PI/2); + case TARGET_DIR_LEFT: + return static_cast(M_PI/2); + case TARGET_DIR_FRONT_RIGHT: + return static_cast(-M_PI/4); + case TARGET_DIR_BACK_RIGHT: + return static_cast(-3*M_PI/4); + case TARGET_DIR_BACK_LEFT: + return static_cast(3*M_PI/4); + case TARGET_DIR_FRONT_LEFT: + return static_cast(M_PI/4); + case TARGET_DIR_RANDOM: + return rand_norm() * static_cast(2 * M_PI); + case TARGET_DIR_SUMMON: + // This direction does alter its angle based on what is being summoned. + // Creatures are being summoned on the left, gameobjects infront + switch (effectInfo.Effect) + { + case SPELL_EFFECT_SUMMON_PET: + case SPELL_EFFECT_SUMMON: + return static_cast(M_PI / 2); + default: + return 0.0f; + } + default: + return 0.0f; + } +} + +Targets SpellImplicitTargetInfo::GetTarget() const +{ + return _target; +} + +uint32 SpellImplicitTargetInfo::GetExplicitTargetMask(bool& srcSet, bool& dstSet) const +{ + uint32 targetMask = 0; + if (GetTarget() == TARGET_DEST_TRAJ) + { + if (!srcSet) + targetMask = TARGET_FLAG_SOURCE_LOCATION; + if (!dstSet) + targetMask |= TARGET_FLAG_DEST_LOCATION; + } + else + { + switch (GetReferenceType()) + { + case TARGET_REFERENCE_TYPE_SRC: + if (srcSet) + break; + targetMask = TARGET_FLAG_SOURCE_LOCATION; + break; + case TARGET_REFERENCE_TYPE_DEST: + if (dstSet) + break; + targetMask = TARGET_FLAG_DEST_LOCATION; + break; + case TARGET_REFERENCE_TYPE_TARGET: + switch (GetObjectType()) + { + case TARGET_OBJECT_TYPE_GOBJ: + targetMask = TARGET_FLAG_GAMEOBJECT; + break; + case TARGET_OBJECT_TYPE_GOBJ_ITEM: + targetMask = TARGET_FLAG_GAMEOBJECT_ITEM; + break; + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + case TARGET_OBJECT_TYPE_UNIT: + case TARGET_OBJECT_TYPE_DEST: + switch (GetCheckType()) + { + case TARGET_CHECK_ENEMY: + targetMask = TARGET_FLAG_UNIT_ENEMY; + break; + case TARGET_CHECK_ALLY: + targetMask = TARGET_FLAG_UNIT_ALLY; + break; + case TARGET_CHECK_PARTY: + targetMask = TARGET_FLAG_UNIT_PARTY; + break; + case TARGET_CHECK_RAID: + targetMask = TARGET_FLAG_UNIT_RAID; + break; + case TARGET_CHECK_PASSENGER: + targetMask = TARGET_FLAG_UNIT_PASSENGER; + break; + case TARGET_CHECK_RAID_CLASS: + [[fallthrough]]; + default: + targetMask = TARGET_FLAG_UNIT; + break; + } + break; + default: + break; + } + break; + default: + break; + } + } + + switch (GetObjectType()) + { + case TARGET_OBJECT_TYPE_SRC: + srcSet = true; + break; + case TARGET_OBJECT_TYPE_DEST: + case TARGET_OBJECT_TYPE_UNIT_AND_DEST: + dstSet = true; + break; + default: + break; + } + return targetMask; +} + +SpellImplicitTargetInfo::StaticData SpellImplicitTargetInfo::_data[TOTAL_SPELL_TARGETS] = +{ + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 1 TARGET_UNIT_CASTER + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 2 TARGET_UNIT_NEARBY_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 3 TARGET_UNIT_NEARBY_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 4 TARGET_UNIT_NEARBY_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 5 TARGET_UNIT_PET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 6 TARGET_UNIT_TARGET_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 7 TARGET_UNIT_SRC_AREA_ENTRY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 8 TARGET_UNIT_DEST_AREA_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 9 TARGET_DEST_HOME + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 10 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 11 TARGET_UNIT_SRC_AREA_UNK_11 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 12 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 13 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 14 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 15 TARGET_UNIT_SRC_AREA_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 16 TARGET_UNIT_DEST_AREA_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 17 TARGET_DEST_DB + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 18 TARGET_DEST_CASTER + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 19 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 20 TARGET_UNIT_CASTER_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 21 TARGET_UNIT_TARGET_ALLY + {TARGET_OBJECT_TYPE_SRC, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 22 TARGET_SRC_CASTER + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 23 TARGET_GAMEOBJECT_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 24 TARGET_UNIT_CONE_ENEMY_24 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 25 TARGET_UNIT_TARGET_ANY + {TARGET_OBJECT_TYPE_GOBJ_ITEM, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 26 TARGET_GAMEOBJECT_ITEM_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 27 TARGET_UNIT_MASTER + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 28 TARGET_DEST_DYNOBJ_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 29 TARGET_DEST_DYNOBJ_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 30 TARGET_UNIT_SRC_AREA_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 31 TARGET_UNIT_DEST_AREA_ALLY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_SUMMON }, // 32 TARGET_DEST_CASTER_SUMMON + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 33 TARGET_UNIT_SRC_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 34 TARGET_UNIT_DEST_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 35 TARGET_UNIT_TARGET_PARTY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 36 TARGET_DEST_CASTER_UNK_36 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_LAST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_PARTY, TARGET_DIR_NONE}, // 37 TARGET_UNIT_LASTTARGET_AREA_PARTY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 38 TARGET_UNIT_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 39 TARGET_DEST_CASTER_FISHING + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 40 TARGET_GAMEOBJECT_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 41 TARGET_DEST_CASTER_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 42 TARGET_DEST_CASTER_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 43 TARGET_DEST_CASTER_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 44 TARGET_DEST_CASTER_FRONT_LEFT + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 45 TARGET_UNIT_TARGET_CHAINHEAL_ALLY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 46 TARGET_DEST_NEARBY_ENTRY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 47 TARGET_DEST_CASTER_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 48 TARGET_DEST_CASTER_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 49 TARGET_DEST_CASTER_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 50 TARGET_DEST_CASTER_LEFT + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 51 TARGET_GAMEOBJECT_SRC_AREA + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 52 TARGET_GAMEOBJECT_DEST_AREA + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 53 TARGET_DEST_TARGET_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 54 TARGET_UNIT_CONE_180_DEG_ENEMY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 55 TARGET_DEST_CASTER_FRONT_LEAP + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 56 TARGET_UNIT_CASTER_AREA_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 57 TARGET_UNIT_TARGET_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 58 TARGET_UNIT_NEARBY_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ALLY, TARGET_DIR_FRONT}, // 59 TARGET_UNIT_CONE_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENTRY, TARGET_DIR_FRONT}, // 60 TARGET_UNIT_CONE_ENTRY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID_CLASS, TARGET_DIR_NONE}, // 61 TARGET_UNIT_TARGET_AREA_RAID_CLASS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 62 TARGET_DEST_CASTER_GROUND + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 63 TARGET_DEST_TARGET_ANY + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 64 TARGET_DEST_TARGET_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 65 TARGET_DEST_TARGET_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 66 TARGET_DEST_TARGET_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 67 TARGET_DEST_TARGET_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 68 TARGET_DEST_TARGET_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 69 TARGET_DEST_TARGET_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 70 TARGET_DEST_TARGET_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 71 TARGET_DEST_TARGET_FRONT_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 72 TARGET_DEST_CASTER_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 73 TARGET_DEST_CASTER_RADIUS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 74 TARGET_DEST_TARGET_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 75 TARGET_DEST_TARGET_RADIUS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 76 TARGET_DEST_CHANNEL_TARGET + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 77 TARGET_UNIT_CHANNEL_TARGET + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT}, // 78 TARGET_DEST_DEST_FRONT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK}, // 79 TARGET_DEST_DEST_BACK + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RIGHT}, // 80 TARGET_DEST_DEST_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_LEFT}, // 81 TARGET_DEST_DEST_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_RIGHT}, // 82 TARGET_DEST_DEST_FRONT_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_RIGHT}, // 83 TARGET_DEST_DEST_BACK_RIGHT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_BACK_LEFT}, // 84 TARGET_DEST_DEST_BACK_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_FRONT_LEFT}, // 85 TARGET_DEST_DEST_FRONT_LEFT + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 86 TARGET_DEST_DEST_RANDOM + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 87 TARGET_DEST_DEST + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 88 TARGET_DEST_DYNOBJ_NONE + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_TRAJ, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 89 TARGET_DEST_TRAJ + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 90 TARGET_UNIT_TARGET_MINIPET + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_RANDOM}, // 91 TARGET_DEST_DEST_RADIUS + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 92 TARGET_UNIT_SUMMONER + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_SRC, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE}, // 93 TARGET_CORPSE_SRC_AREA_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 94 TARGET_UNIT_VEHICLE + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_PASSENGER, TARGET_DIR_NONE}, // 95 TARGET_UNIT_TARGET_PASSENGER + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 96 TARGET_UNIT_PASSENGER_0 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 97 TARGET_UNIT_PASSENGER_1 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 98 TARGET_UNIT_PASSENGER_2 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 99 TARGET_UNIT_PASSENGER_3 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 100 TARGET_UNIT_PASSENGER_4 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 101 TARGET_UNIT_PASSENGER_5 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 102 TARGET_UNIT_PASSENGER_6 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 103 TARGET_UNIT_PASSENGER_7 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 104 TARGET_UNIT_CONE_CASTER_TO_DEST_ENEMY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 105 TARGET_UNIT_CASTER_AND_PASSENGERS + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_CHANNEL, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 106 TARGET_DEST_CHANNEL_CASTER + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_NEARBY, TARGET_CHECK_ENTRY, TARGET_DIR_NONE}, // 107 TARGET_DEST_NEARBY_ENTRY_2 + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENEMY, TARGET_DIR_FRONT}, // 108 TARGET_GAMEOBJECT_CONE_CASTER_TO_DEST_ENEMY + {TARGET_OBJECT_TYPE_GOBJ, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ALLY, TARGET_DIR_FRONT}, // 109 TARGET_GAMEOBJECT_CONE_CASTER_TO_DEST_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_DEST, TARGET_SELECT_CATEGORY_CONE, TARGET_CHECK_ENTRY, TARGET_DIR_FRONT}, // 110 TARGET_UNIT_CONE_CASTER_TO_DEST_ENTRY + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 111 + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_TARGET, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 112 TARGET_DEST_TARGET_ANY_2 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 113 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 114 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 115 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 116 + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 117 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 118 TARGET_UNIT_TARGET_ALLY_OR_RAID + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_RAID, TARGET_DIR_NONE}, // 119 TARGET_CORPSE_SRC_AREA_RAID + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_SUMMONED, TARGET_DIR_NONE}, // 120 TARGET_UNIT_SELF_AND_SUMMONS + {TARGET_OBJECT_TYPE_CORPSE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_ALLY, TARGET_DIR_NONE}, // 121 TARGET_CORPSE_TARGET_ALLY + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_THREAT, TARGET_DIR_NONE}, // 122 TARGET_UNIT_AREA_THREAT_LIST + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_TAP, TARGET_DIR_NONE}, // 123 TARGET_UNIT_AREA_TAP_LIST + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 124 TARGET_UNIT_TARGET_TAP_LIST + {TARGET_OBJECT_TYPE_DEST, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_DEFAULT, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 125 TARGET_DEST_CASTER_GROUND_2 + {TARGET_OBJECT_TYPE_UNIT, TARGET_REFERENCE_TYPE_CASTER, TARGET_SELECT_CATEGORY_AREA, TARGET_CHECK_ENEMY, TARGET_DIR_NONE }, // 126 TARGET_UNIT_AREA_ENEMY + {TARGET_OBJECT_TYPE_NONE, TARGET_REFERENCE_TYPE_NONE, TARGET_SELECT_CATEGORY_NYI, TARGET_CHECK_DEFAULT, TARGET_DIR_NONE}, // 127 +}; + +SpellEffectInfo::SpellEffectInfo(SpellEntry const* /*spellEntry*/, SpellInfo const* spellInfo, uint8 effIndex, SpellEffectEntry const* _effect) +{ + SpellScalingEntry const* scaling = spellInfo->GetSpellScaling(); + + _spellInfo = spellInfo; + _effIndex = _effect ? _effect->EffectIndex : effIndex; + Effect = _effect ? _effect->Effect : 0; + ApplyAuraName = _effect ? _effect->EffectAura : 0; + AuraPeriod = _effect ? _effect->EffectAuraPeriod : 0; + DieSides = _effect ? _effect->EffectDieSides : 0; + RealPointsPerLevel = _effect ? _effect->EffectRealPointsPerLevel : 0.0f; + BasePoints = _effect ? _effect->EffectBasePoints : 0; + PointsPerComboPoint = _effect ? _effect->EffectPointsPerResource : 0.0f; + Amplitude = _effect ? _effect->EffectAmplitude : 0.0f; + DamageMultiplier = _effect ? _effect->EffectChainAmplitude : 0.0f; + BonusMultiplier = _effect ? _effect->EffectBonusCoefficient : 0.0f; + MiscValue = _effect ? _effect->EffectMiscValue : 0; + MiscValueB = _effect ? _effect->EffectMiscValueB : 0; + Mechanic = Mechanics(_effect ? _effect->EffectMechanic : 0); + TargetA = SpellImplicitTargetInfo(_effect ? _effect->EffectImplicitTargetA : 0); + TargetB = SpellImplicitTargetInfo(_effect ? _effect->EffectImplicitTargetB : 0); + TargetARadiusEntry = _effect && _effect->EffectRadiusIndex ? sSpellRadiusStore.LookupEntry(_effect->EffectRadiusIndex) : nullptr; + TargetBRadiusEntry = _effect && _effect->EffectRadiusMaxIndex ? sSpellRadiusStore.LookupEntry(_effect->EffectRadiusMaxIndex) : nullptr; + ChainTarget = _effect ? _effect->EffectChainTargets : 0; + ItemType = _effect ? _effect->EffectItemType : 0; + TriggerSpell = _effect ? _effect->EffectTriggerSpell : 0; + SpellClassMask = _effect ? _effect->EffectSpellClassMask : flag96(0); + ImplicitTargetConditions = nullptr; + + Scaling.Coefficient = scaling ? scaling->Coefficient[_effIndex] : 0.0f; + Scaling.Variance = scaling ? scaling->Variance[_effIndex] : 0.0f; + Scaling.ComboPointsCoefficient = scaling ? scaling->ComboPointsCoefficient[_effIndex] : 0.0f; +} + +bool SpellEffectInfo::IsEffect() const +{ + return Effect != 0; +} + +bool SpellEffectInfo::IsEffect(SpellEffects effectName) const +{ + return Effect == effectName; +} + +bool SpellEffectInfo::IsAura() const +{ + return (IsUnitOwnedAuraEffect() || Effect == SPELL_EFFECT_PERSISTENT_AREA_AURA) && ApplyAuraName != 0; +} + +bool SpellEffectInfo::IsAura(AuraType aura) const +{ + return IsAura() && ApplyAuraName == aura; +} + +bool SpellEffectInfo::IsTargetingArea() const +{ + return TargetA.IsArea() || TargetB.IsArea(); +} + +bool SpellEffectInfo::IsAreaAuraEffect() const +{ + if (Effect == SPELL_EFFECT_APPLY_AREA_AURA_PARTY || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_FRIEND || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_ENEMY || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_PET || + Effect == SPELL_EFFECT_APPLY_AREA_AURA_OWNER) + return true; + return false; +} + +bool SpellEffectInfo::IsUnitOwnedAuraEffect() const +{ + return IsAreaAuraEffect() || Effect == SPELL_EFFECT_APPLY_AURA || Effect == SPELL_EFFECT_APPLY_AURA_2; +} + +bool SpellEffectInfo::ScalesWithCreatureLevel() const +{ + if (!_spellInfo->HasAttribute(SPELL_ATTR0_SCALES_WITH_CREATURE_LEVEL)) + return false; + + switch (Effect) + { + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_DUMMY: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_HEAL: + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_POWER_BURN: + case SPELL_EFFECT_SCRIPT_EFFECT: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_FORCE_CAST_WITH_VALUE: + case SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE: + case SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE: + return true; + default: + break; + } + + switch (ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_DUMMY: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_DAMAGE_SHIELD: + case SPELL_AURA_PROC_TRIGGER_DAMAGE: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_SCHOOL_ABSORB: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + return true; + default: + break; + } + + return false; +} + +int32 SpellEffectInfo::CalcValue(WorldObject const* caster /*= nullptr*/, int32 const* bp /*= nullptr*/, Unit const* target /*= nullptr*/) const +{ + float basePointsPerLevel = RealPointsPerLevel; + // TODO: this needs to be a float, not rounded + int basePoints = CalcBaseValue(caster, target); + double value = bp ? *bp : basePoints; + + Unit const* casterUnit = nullptr; + if (caster) + casterUnit = caster->ToUnit(); + + if (Scaling.Variance != 0.0f) + { + float delta = fabs(Scaling.Variance * 0.5f); + double valueVariance = frand(-delta, delta); + value += double(basePoints) * valueVariance; + } + else if (DieSides) + { + // roll in a range <1;EffectDieSides> as of patch 3.3.3 + if (DieSides == 1) + value += DieSides; + else + value += (DieSides >= 1) ? irand(1, DieSides) : irand(DieSides, 1); + } + + // base amount modification based on spell lvl vs caster lvl + if (Scaling.Coefficient == 0.0f) + { + if (casterUnit && basePointsPerLevel != 0.0) + { + int32 level = int32(casterUnit->getLevel()); + if (level > int32(_spellInfo->MaxLevel) && _spellInfo->MaxLevel > 0) + level = int32(_spellInfo->MaxLevel); + + // if base level is greater than spell level, reduce by base level (eg. pilgrims foods) + level -= int32(std::max(_spellInfo->BaseLevel, _spellInfo->SpellLevel)); + if (level < 0) + level = 0; + value += level * basePointsPerLevel; + } + } + + // bonus amount from combo points + if (casterUnit && _spellInfo->HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DAMAGE)) + { + double comboDamage = CalcPointsPerResource(casterUnit); + if (comboDamage != 0.0) + if (uint8 comboPoints = casterUnit->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints()) + value += comboDamage * comboPoints; + } + + if (_spellInfo->HasAttribute(SPELL_ATTR8_MASTERY_AFFECTS_POINTS)) + if (Player const* playerCaster = caster ? caster->ToPlayer() : nullptr) + value += playerCaster->GetFloatValue(PLAYER_MASTERY) * BonusMultiplier; + + if (casterUnit) + value = casterUnit->ApplyEffectModifiers(_spellInfo, _effIndex, value); + + return int32(round(value)); +} + +int32 SpellEffectInfo::CalcBaseValue(WorldObject const* caster, Unit const* target) const +{ + if (Scaling.Coefficient != 0.0f) + { + uint32 level = _spellInfo->SpellLevel; + if (target && _spellInfo->HasAttribute(SPELL_ATTR8_USE_TARGETS_LEVEL_FOR_SPELL_SCALING)) + level = target->getLevel(); + else if (caster && caster->IsUnit()) + level = caster->ToUnit()->getLevel(); + + if (_spellInfo->BaseLevel && _spellInfo->HasAttribute(SPELL_ATTR10_USE_SPELL_BASE_LEVEL_FOR_SCALING)) + level = _spellInfo->BaseLevel; + + float value = 0.0f; + + if (level > 0) + { + if (!_spellInfo->Scaling.Class) + return 0; + + value = GameTables::GetSpellScalingValue(level, static_cast(_spellInfo->Scaling.Class > 0 ? _spellInfo->Scaling.Class: (MAX_CLASSES - 1 /*last class*/) - _spellInfo->Scaling.Class)); + value *= _spellInfo->GetSpellScalingMultiplier(level, false); + } + + value *= Scaling.Coefficient; + if (value > 0.0f) + value = std::max(value, 1.0f); + + return int32(round(value)); + } + else + { + float value = BasePoints; + + if (ScalesWithCreatureLevel()) + { + // Some spells use the internal game table to calculate scaling coefficients for creatures + if (Creature const* creatureCaster = Object::ToCreature(caster)) + { + uint8 casterClass = creatureCaster->getClass(); + uint8 expansion = creatureCaster->GetCreatureTemplate()->GetHealthScalingExpansion(); + + float spellDamage = GetGameTableColumnForClass(sNpcDamageByClassGameTable[expansion].GetRow(_spellInfo->SpellLevel), casterClass); + float creatureDamage = GetGameTableColumnForClass(sNpcDamageByClassGameTable[expansion].GetRow(creatureCaster->getLevel()), casterClass); + + if (spellDamage != 0.0f) + value *= creatureDamage / spellDamage; + } + } + + return int32(round(value)); + } +} + +// Based on client function QuestTextParser::Spell_C_GetPointsPerResource (build 15595) +double SpellEffectInfo::CalcPointsPerResource(WorldObject const* caster) const +{ + double value = 0.0; + if (caster && caster->IsUnit()) + { + if (Scaling.Coefficient != 0.0f) + { + if (!_spellInfo->Scaling.Class) + return 0.0; + + int32 scalingClass = _spellInfo->Scaling.Class; + if (scalingClass < 0) + scalingClass = (MAX_CLASSES - 1 /*last class*/) - scalingClass; + + float scaledAmount = GameTables::GetSpellScalingValue(caster->ToUnit()->getLevel(), static_cast(scalingClass)); + scaledAmount *= _spellInfo->GetSpellScalingMultiplier(caster->ToUnit()->getLevel(), false) * Scaling.ComboPointsCoefficient; + value = scaledAmount; + } + } + + if (std::fabs(value) < 0.00000023841858 && PointsPerComboPoint != 0.0f) + return PointsPerComboPoint; + + return value; +} + +float SpellEffectInfo::CalcValueMultiplier(WorldObject* caster, Spell* spell) const +{ + float multiplier = Amplitude; + if (Player* modOwner = (caster ? caster->GetSpellModOwner() : nullptr)) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::Amplitude, multiplier, spell); + return multiplier; +} + +float SpellEffectInfo::CalcDamageMultiplier(WorldObject* caster, Spell* spell) const +{ + float multiplierPercent = DamageMultiplier * 100.0f; + if (Player* modOwner = (caster ? caster->GetSpellModOwner() : nullptr)) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::ChainAmplitude, multiplierPercent, spell); + return multiplierPercent / 100.0f; +} + +bool SpellEffectInfo::HasRadius(SpellTargetIndex targetIndex) const +{ + switch (targetIndex) + { + case SpellTargetIndex::TargetA: + return TargetARadiusEntry != nullptr; + case SpellTargetIndex::TargetB: + return TargetBRadiusEntry != nullptr; + default: + return false; + } +} + +float SpellEffectInfo::CalcRadius(WorldObject* caster /*= nullptr*/, SpellTargetIndex targetIndex /*=SpellTargetIndex::TargetA*/, Spell* spell /*= nullptr*/) const +{ + // TargetA -> TargetARadiusEntry + // TargetB -> TargetBRadiusEntry + // Aura effects have TargetARadiusEntry == TargetBRadiusEntry (mostly) + SpellImplicitTargetInfo target = TargetA; + SpellRadiusEntry const* entry = TargetARadiusEntry; + if (targetIndex == SpellTargetIndex::TargetB && HasRadius(targetIndex)) + { + target = TargetB; + entry = TargetBRadiusEntry; + } + + if (!entry) + return 0.0f; + + float radius = entry->RadiusMin; + + // Random targets use random value between RadiusMin and RadiusMax + // For other cases, client uses RadiusMax if RadiusMin is 0 + if (target.GetTarget() == TARGET_DEST_CASTER_RANDOM || + target.GetTarget() == TARGET_DEST_TARGET_RANDOM || + target.GetTarget() == TARGET_DEST_DEST_RANDOM) + radius += (entry->RadiusMax - radius) * std::sqrt(rand_norm()); + else if (radius == 0.0f) + radius = entry->RadiusMax; + + if (caster) + { + if (Unit* casterUnit = caster->ToUnit()) + radius += entry->RadiusPerLevel * casterUnit->getLevel(); + + radius = std::min(radius, entry->RadiusMax); + + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(_spellInfo, SpellModOp::Radius, radius, spell); + } + + return radius; +} + +uint32 SpellEffectInfo::GetProvidedTargetMask() const +{ + return GetTargetFlagMask(TargetA.GetObjectType()) | GetTargetFlagMask(TargetB.GetObjectType()); +} + +uint32 SpellEffectInfo::GetMissingTargetMask(bool srcSet /*= false*/, bool dstSet /*= false*/, uint32 mask /*=0*/) const +{ + uint32 effImplicitTargetMask = GetTargetFlagMask(GetUsedTargetObjectType()); + uint32 providedTargetMask = GetProvidedTargetMask() | mask; + + // remove all flags covered by effect target mask + if (providedTargetMask & TARGET_FLAG_UNIT_MASK) + effImplicitTargetMask &= ~(TARGET_FLAG_UNIT_MASK); + if (providedTargetMask & TARGET_FLAG_CORPSE_MASK) + effImplicitTargetMask &= ~(TARGET_FLAG_UNIT_MASK | TARGET_FLAG_CORPSE_MASK); + if (providedTargetMask & TARGET_FLAG_GAMEOBJECT_ITEM) + effImplicitTargetMask &= ~(TARGET_FLAG_GAMEOBJECT_ITEM | TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_ITEM); + if (providedTargetMask & TARGET_FLAG_GAMEOBJECT) + effImplicitTargetMask &= ~(TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_GAMEOBJECT_ITEM); + if (providedTargetMask & TARGET_FLAG_ITEM) + effImplicitTargetMask &= ~(TARGET_FLAG_ITEM | TARGET_FLAG_GAMEOBJECT_ITEM); + if (dstSet || providedTargetMask & TARGET_FLAG_DEST_LOCATION) + effImplicitTargetMask &= ~(TARGET_FLAG_DEST_LOCATION); + if (srcSet || providedTargetMask & TARGET_FLAG_SOURCE_LOCATION) + effImplicitTargetMask &= ~(TARGET_FLAG_SOURCE_LOCATION); + + return effImplicitTargetMask; +} + +SpellEffectImplicitTargetTypes SpellEffectInfo::GetImplicitTargetType() const +{ + return _data[Effect].ImplicitTargetType; +} + +SpellTargetObjectTypes SpellEffectInfo::GetUsedTargetObjectType() const +{ + return _data[Effect].UsedTargetObjectType; +} + +SpellEffectInfo::StaticData SpellEffectInfo::_data[TOTAL_SPELL_EFFECTS] = +{ + // implicit target type used target object type + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 0 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 1 SPELL_EFFECT_INSTAKILL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 2 SPELL_EFFECT_SCHOOL_DAMAGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 3 SPELL_EFFECT_DUMMY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 4 SPELL_EFFECT_PORTAL_TELEPORT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 5 SPELL_EFFECT_TELEPORT_UNITS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 6 SPELL_EFFECT_APPLY_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 7 SPELL_EFFECT_ENVIRONMENTAL_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 8 SPELL_EFFECT_POWER_DRAIN + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 9 SPELL_EFFECT_HEALTH_LEECH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 10 SPELL_EFFECT_HEAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 11 SPELL_EFFECT_BIND + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 12 SPELL_EFFECT_PORTAL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 13 SPELL_EFFECT_RITUAL_BASE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 14 SPELL_EFFECT_RITUAL_SPECIALIZE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 15 SPELL_EFFECT_RITUAL_ACTIVATE_PORTAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 16 SPELL_EFFECT_QUEST_COMPLETE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 17 SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 18 SPELL_EFFECT_RESURRECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 19 SPELL_EFFECT_ADD_EXTRA_ATTACKS + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 20 SPELL_EFFECT_DODGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 21 SPELL_EFFECT_EVADE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 22 SPELL_EFFECT_PARRY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 23 SPELL_EFFECT_BLOCK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 24 SPELL_EFFECT_CREATE_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 25 SPELL_EFFECT_WEAPON + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 26 SPELL_EFFECT_DEFENSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 27 SPELL_EFFECT_PERSISTENT_AREA_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 28 SPELL_EFFECT_SUMMON + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 29 SPELL_EFFECT_LEAP + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 30 SPELL_EFFECT_ENERGIZE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 31 SPELL_EFFECT_WEAPON_PERCENT_DAMAGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 32 SPELL_EFFECT_TRIGGER_MISSILE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ_ITEM}, // 33 SPELL_EFFECT_OPEN_LOCK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 34 SPELL_EFFECT_SUMMON_CHANGE_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 35 SPELL_EFFECT_APPLY_AREA_AURA_PARTY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 36 SPELL_EFFECT_LEARN_SPELL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 37 SPELL_EFFECT_SPELL_DEFENSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 38 SPELL_EFFECT_DISPEL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 39 SPELL_EFFECT_LANGUAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 40 SPELL_EFFECT_DUAL_WIELD + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 41 SPELL_EFFECT_JUMP + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 42 SPELL_EFFECT_JUMP_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 43 SPELL_EFFECT_TELEPORT_UNITS_FACE_CASTER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 44 SPELL_EFFECT_SKILL_STEP + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 45 SPELL_EFFECT_ADD_HONOR + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 46 SPELL_EFFECT_SPAWN + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 47 SPELL_EFFECT_TRADE_SKILL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 48 SPELL_EFFECT_STEALTH + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 49 SPELL_EFFECT_DETECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 50 SPELL_EFFECT_TRANS_DOOR + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 51 SPELL_EFFECT_FORCE_CRITICAL_HIT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 52 SPELL_EFFECT_GUARANTEE_HIT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 53 SPELL_EFFECT_ENCHANT_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 54 SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 55 SPELL_EFFECT_TAMECREATURE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 56 SPELL_EFFECT_SUMMON_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 57 SPELL_EFFECT_LEARN_PET_SPELL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 58 SPELL_EFFECT_WEAPON_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 59 SPELL_EFFECT_CREATE_RANDOM_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 60 SPELL_EFFECT_PROFICIENCY + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 61 SPELL_EFFECT_SEND_EVENT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 62 SPELL_EFFECT_POWER_BURN + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 63 SPELL_EFFECT_THREAT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 64 SPELL_EFFECT_TRIGGER_SPELL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 65 SPELL_EFFECT_APPLY_AREA_AURA_RAID + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 66 SPELL_EFFECT_CREATE_MANA_GEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 67 SPELL_EFFECT_HEAL_MAX_HEALTH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 68 SPELL_EFFECT_INTERRUPT_CAST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 69 SPELL_EFFECT_DISTRACT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 70 SPELL_EFFECT_PULL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 71 SPELL_EFFECT_PICKPOCKET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 72 SPELL_EFFECT_ADD_FARSIGHT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 73 SPELL_EFFECT_UNTRAIN_TALENTS + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 74 SPELL_EFFECT_APPLY_GLYPH + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 75 SPELL_EFFECT_HEAL_MECHANICAL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 76 SPELL_EFFECT_SUMMON_OBJECT_WILD + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 77 SPELL_EFFECT_SCRIPT_EFFECT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 78 SPELL_EFFECT_ATTACK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 79 SPELL_EFFECT_SANCTUARY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 80 SPELL_EFFECT_ADD_COMBO_POINTS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 81 SPELL_EFFECT_CREATE_HOUSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 82 SPELL_EFFECT_BIND_SIGHT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 83 SPELL_EFFECT_DUEL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 84 SPELL_EFFECT_STUCK + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 85 SPELL_EFFECT_SUMMON_PLAYER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 86 SPELL_EFFECT_ACTIVATE_OBJECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 87 SPELL_EFFECT_GAMEOBJECT_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 88 SPELL_EFFECT_GAMEOBJECT_REPAIR + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_GOBJ}, // 89 SPELL_EFFECT_GAMEOBJECT_SET_DESTRUCTION_STATE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 90 SPELL_EFFECT_KILL_CREDIT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 91 SPELL_EFFECT_THREAT_ALL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 92 SPELL_EFFECT_ENCHANT_HELD_ITEM + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 93 SPELL_EFFECT_FORCE_DESELECT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 94 SPELL_EFFECT_SELF_RESURRECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 95 SPELL_EFFECT_SKINNING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 96 SPELL_EFFECT_CHARGE + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 97 SPELL_EFFECT_CAST_BUTTON + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 98 SPELL_EFFECT_KNOCK_BACK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 99 SPELL_EFFECT_DISENCHANT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 100 SPELL_EFFECT_INEBRIATE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 101 SPELL_EFFECT_FEED_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 102 SPELL_EFFECT_DISMISS_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 103 SPELL_EFFECT_REPUTATION + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 104 SPELL_EFFECT_SUMMON_OBJECT_SLOT1 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 105 SPELL_EFFECT_SUMMON_OBJECT_SLOT2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 106 SPELL_EFFECT_SUMMON_OBJECT_SLOT3 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 107 SPELL_EFFECT_SUMMON_OBJECT_SLOT4 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 108 SPELL_EFFECT_DISPEL_MECHANIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 109 SPELL_EFFECT_SUMMON_DEAD_PET + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 110 SPELL_EFFECT_DESTROY_ALL_TOTEMS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 111 SPELL_EFFECT_DURABILITY_DAMAGE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 112 SPELL_EFFECT_112 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 113 SPELL_EFFECT_RESURRECT_NEW + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 114 SPELL_EFFECT_ATTACK_ME + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 115 SPELL_EFFECT_DURABILITY_DAMAGE_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ENEMY}, // 116 SPELL_EFFECT_SKIN_PLAYER_CORPSE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 117 SPELL_EFFECT_SPIRIT_HEAL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 118 SPELL_EFFECT_SKILL + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 119 SPELL_EFFECT_APPLY_AREA_AURA_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 120 SPELL_EFFECT_TELEPORT_GRAVEYARD + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 121 SPELL_EFFECT_NORMALIZED_WEAPON_DMG + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 122 SPELL_EFFECT_122 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 123 SPELL_EFFECT_SEND_TAXI + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 124 SPELL_EFFECT_PULL_TOWARDS + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 125 SPELL_EFFECT_MODIFY_THREAT_PERCENT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 126 SPELL_EFFECT_STEAL_BENEFICIAL_BUFF + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 127 SPELL_EFFECT_PROSPECTING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 128 SPELL_EFFECT_APPLY_AREA_AURA_FRIEND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 129 SPELL_EFFECT_APPLY_AREA_AURA_ENEMY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 130 SPELL_EFFECT_REDIRECT_THREAT + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 131 SPELL_EFFECT_PLAY_SOUND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 132 SPELL_EFFECT_PLAY_MUSIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 133 SPELL_EFFECT_UNLEARN_SPECIALIZATION + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 134 SPELL_EFFECT_KILL_CREDIT2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 135 SPELL_EFFECT_CALL_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 136 SPELL_EFFECT_HEAL_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 137 SPELL_EFFECT_ENERGIZE_PCT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 138 SPELL_EFFECT_LEAP_BACK + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 139 SPELL_EFFECT_CLEAR_QUEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 140 SPELL_EFFECT_FORCE_CAST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 141 SPELL_EFFECT_FORCE_CAST_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 142 SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 143 SPELL_EFFECT_APPLY_AREA_AURA_OWNER + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 144 SPELL_EFFECT_KNOCK_BACK_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT_AND_DEST}, // 145 SPELL_EFFECT_PULL_TOWARDS_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 146 SPELL_EFFECT_ACTIVATE_RUNE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 147 SPELL_EFFECT_QUEST_FAIL + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 148 SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_DEST}, // 149 SPELL_EFFECT_CHARGE_DEST + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 150 SPELL_EFFECT_QUEST_START + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 151 SPELL_EFFECT_TRIGGER_SPELL_2 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 152 SPELL_EFFECT_SUMMON_RAF_FRIEND + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 153 SPELL_EFFECT_CREATE_TAMED_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 154 SPELL_EFFECT_DISCOVER_TAXI + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 155 SPELL_EFFECT_TITAN_GRIP + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 156 SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 157 SPELL_EFFECT_CREATE_ITEM_2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_ITEM}, // 158 SPELL_EFFECT_MILLING + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 159 SPELL_EFFECT_ALLOW_RENAME_PET + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 160 SPELL_EFFECT_FORCE_CAST_2 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 161 SPELL_EFFECT_TALENT_SPEC_COUNT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 162 SPELL_EFFECT_TALENT_SPEC_SELECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 163 SPELL_EFFECT_163 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 164 SPELL_EFFECT_REMOVE_AURA + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 165 SPELL_EFFECT_165 + {EFFECT_IMPLICIT_TARGET_CASTER, TARGET_OBJECT_TYPE_UNIT}, // 166 SPELL_EFFECT_GIVE_CURRENCY + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 167 SPELL_EFFECT_167 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 168 SPELL_EFFECT_168 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_ITEM}, // 169 SPELL_EFFECT_DESTROY_ITEM + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 170 SPELL_EFFECT_170 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 171 SPELL_EFFECT_SUMMON_PERSONAL_GAMEOBJECT + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_CORPSE_ALLY}, // 172 SPELL_EFFECT_RESURRECT_WITH_AURA + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 173 SPELL_EFFECT_UNLOCK_GUILD_VAULT_TAB + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 174 SPELL_EFFECT_APPLY_AURA_2 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 175 SPELL_EFFECT_175 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 176 SPELL_EFFECT_176 + {EFFECT_IMPLICIT_TARGET_EXPLICIT, TARGET_OBJECT_TYPE_UNIT}, // 177 SPELL_EFFECT_177 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 178 SPELL_EFFECT_178 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_DEST}, // 179 SPELL_EFFECT_CREATE_AREATRIGGER + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 180 SPELL_EFFECT_180 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_NONE}, // 181 SPELL_EFFECT_181 + {EFFECT_IMPLICIT_TARGET_NONE, TARGET_OBJECT_TYPE_UNIT}, // 182 SPELL_EFFECT_182 +}; + +SpellInfo::SpellInfo(SpellEntry const* spellEntry, SpellEffectEntry const** effects) +{ + Id = spellEntry->ID; + Attributes = spellEntry->Attributes; + AttributesEx = spellEntry->AttributesEx; + AttributesEx2 = spellEntry->AttributesEx2; + AttributesEx3 = spellEntry->AttributesEx3; + AttributesEx4 = spellEntry->AttributesEx4; + AttributesEx5 = spellEntry->AttributesEx5; + AttributesEx6 = spellEntry->AttributesEx6; + AttributesEx7 = spellEntry->AttributesEx7; + AttributesEx8 = spellEntry->AttributesEx8; + AttributesEx9 = spellEntry->AttributesEx9; + AttributesEx10 = spellEntry->AttributesEx10; + AttributesCu = 0; + CastTimeEntry = spellEntry->CastingTimeIndex ? sSpellCastTimesStore.LookupEntry(spellEntry->CastingTimeIndex) : nullptr; + DurationEntry = spellEntry->DurationIndex ? sSpellDurationStore.LookupEntry(spellEntry->DurationIndex) : nullptr; + PowerType = spellEntry->PowerType; + RangeEntry = spellEntry->RangeIndex ? sSpellRangeStore.LookupEntry(spellEntry->RangeIndex) : nullptr; + Speed = spellEntry->Speed; + + for (uint8 i = 0; i < 2; ++i) + SpellVisual[i] = spellEntry->SpellVisualID[i]; + + SpellIconID = spellEntry->SpellIconID; + ActiveIconID = spellEntry->ActiveIconID; + SpellName = spellEntry->Name; + Rank = spellEntry->NameSubtext; + SchoolMask = spellEntry->SchoolMask; + RuneCostID = spellEntry->RuneCostID; + SpellDifficultyId = spellEntry->Difficulty; + BonusCoefficientFromAP = spellEntry->BonusCoefficient; + SpellScalingId = spellEntry->ScalingID; + SpellAuraOptionsId = spellEntry->AuraOptionsID; + SpellAuraRestrictionsId = spellEntry->AuraRestrictionsID; + SpellCastingRequirementsId = spellEntry->CastingRequirementsID; + SpellCategoriesId = spellEntry->CategoriesID; + SpellClassOptionsId = spellEntry->ClassOptionsID; + SpellCooldownsId = spellEntry->CooldownsID; + SpellEquippedItemsId = spellEntry->EquippedItemsID; + SpellInterruptsId = spellEntry->InterruptsID; + SpellLevelsId = spellEntry->LevelsID; + SpellPowerId = spellEntry->PowerDisplayID; + SpellReagentsId = spellEntry->ReagentsID; + SpellShapeshiftId = spellEntry->ShapeshiftID; + SpellTargetRestrictionsId = spellEntry->TargetRestrictionsID; + SpellTotemsId = spellEntry->TotemsID; + ResearchProjectId = spellEntry->RequiredProjectID; + + // SpellDifficultyEntry + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + Effects[i] = SpellEffectInfo(spellEntry, this, i, effects[i]); + + // SpellScalingEntry + SpellScalingEntry const* _scaling = GetSpellScaling(); + Scaling.CastTimeMin = _scaling ? _scaling->CastTimeMin : 0; + Scaling.CastTimeMax = _scaling ? _scaling->CastTimeMax : 0; + Scaling.CastTimeMaxLevel = _scaling ? _scaling->CastTimeMaxLevel : 0; + Scaling.Class = _scaling ? _scaling->Class : 0; + Scaling.NerfFactor = _scaling ? _scaling->NerfFactor : 0.f; + Scaling.NerfMaxLevel = _scaling ? _scaling->NerfMaxLevel : 0; + + // SpellAuraOptionsEntry + SpellAuraOptionsEntry const* _options = GetSpellAuraOptions(); + ProcFlags = _options ? ::ProcFlags(_options->ProcTypeMask) : PROC_FLAG_NONE; + ProcChance = _options ? _options->ProcChance : 0; + ProcCharges = _options ? _options->ProcCharges : 0; + StackAmount = _options ? _options->CumulativeAura : 0; + + // SpellAuraRestrictionsEntry + SpellAuraRestrictionsEntry const* _aura = GetSpellAuraRestrictions(); + CasterAuraState = _aura ? _aura->CasterAuraState : 0; + TargetAuraState = _aura ? _aura->TargetAuraState : 0; + CasterAuraStateNot = _aura ? _aura->ExcludeCasterAuraState : 0; + TargetAuraStateNot = _aura ? _aura->ExcludeTargetAuraState : 0; + CasterAuraSpell = _aura ? _aura->CasterAuraSpell : 0; + TargetAuraSpell = _aura ? _aura->TargetAuraSpell : 0; + ExcludeCasterAuraSpell = _aura ? _aura->ExcludeCasterAuraSpell : 0; + ExcludeTargetAuraSpell = _aura ? _aura->ExcludeTargetAuraSpell : 0; + + // SpellCastingRequirementsEntry + SpellCastingRequirementsEntry const* _castreq = GetSpellCastingRequirements(); + RequiresSpellFocus = _castreq ? _castreq->RequiresSpellFocus : 0; + FacingCasterFlags = _castreq ? SpellFacingCasterFlags(_castreq->FacingCasterFlags) : SpellFacingCasterFlags::None; + MinFactionId = _castreq ? _castreq->MinFactionID : 0; + MinReputation = _castreq ? _castreq->MinReputation : 0; + AreaGroupId = _castreq ? _castreq->RequiredAreasID : -1; + + // SpellCategoriesEntry + SpellCategoriesEntry const* _categorie = GetSpellCategories(); + CategoryEntry = _categorie ? sSpellCategoryStore.LookupEntry(_categorie->Category) : nullptr; + Dispel = _categorie ? _categorie->DispelType : 0; + Mechanic = _categorie ? _categorie->Mechanic : 0; + StartRecoveryCategory = _categorie ? _categorie->StartRecoveryCategory : 0; + DmgClass = _categorie ? _categorie->DefenseType : 0; + PreventionType = _categorie ? _categorie->PreventionType : 0; + + // SpellClassOptionsEntry + SpellClassOptionsEntry const* _class = GetSpellClassOptions(); + SpellFamilyName = _class ? _class->SpellClassSet : 0; + SpellFamilyFlags = _class ? _class->SpellFamilyMask : flag96(0); + + // SpellCooldownsEntry + SpellCooldownsEntry const* _cooldowns = GetSpellCooldowns(); + RecoveryTime = _cooldowns ? _cooldowns->RecoveryTime : 0; + CategoryRecoveryTime = _cooldowns ? _cooldowns->CategoryRecoveryTime : 0; + StartRecoveryTime = _cooldowns ? _cooldowns->StartRecoveryTime : 0; + + // SpellEquippedItemsEntry + SpellEquippedItemsEntry const* _equipped = GetSpellEquippedItems(); + EquippedItemClass = _equipped ? _equipped->EquippedItemClass : -1; + EquippedItemSubClassMask = _equipped ?_equipped->EquippedItemSubclass : -1; + EquippedItemInventoryTypeMask = _equipped ? _equipped->EquippedItemInvTypes : -1; + + // SpellInterruptsEntry + SpellInterruptsEntry const* _interrupt = GetSpellInterrupts(); + + AuraInterruptFlags = _interrupt ? SpellAuraInterruptFlags(_interrupt->AuraInterruptFlags[0]) : SpellAuraInterruptFlags::None; + AuraInterruptFlags2 = _interrupt? SpellAuraInterruptFlags2(_interrupt->AuraInterruptFlags[1]) : SpellAuraInterruptFlags2::None; + ChannelInterruptFlags = _interrupt ? SpellAuraInterruptFlags(_interrupt->ChannelInterruptFlags[0] ) : SpellAuraInterruptFlags::None; + ChannelInterruptFlags2 = _interrupt ? SpellAuraInterruptFlags2(_interrupt->ChannelInterruptFlags[1]) : SpellAuraInterruptFlags2::None; + InterruptFlags = _interrupt ? SpellInterruptFlags(_interrupt->InterruptFlags) : SpellInterruptFlags::None; + + // SpellLevelsEntry + SpellLevelsEntry const* _levels = GetSpellLevels(); + MaxLevel = _levels ? _levels->MaxLevel : 0; + BaseLevel = _levels ? _levels->BaseLevel : 0; + SpellLevel = _levels ? _levels->SpellLevel : 0; + + // SpellPowerEntry + SpellPowerEntry const* _power = GetSpellPower(); + ManaCost = _power ? _power->ManaCost : 0; + ManaCostPerlevel = _power ? _power->ManaCostPerLevel : 0; + ManaCostPercentage = _power ? _power->PowerCostPct : 0; + ManaCostPercentage2 = _power ? _power->PowerCostPct2 : 0.f; + ManaPerSecond = _power ? _power->ManaPerSecond : 0; + + // SpellReagentsEntry + SpellReagentsEntry const* _reagents = GetSpellReagents(); + for (uint8 i = 0; i < MAX_SPELL_REAGENTS; ++i) + Reagent[i] = _reagents ? _reagents->Reagents[i] : 0; + for (uint8 i = 0; i < MAX_SPELL_REAGENTS; ++i) + ReagentCount[i] = _reagents ? _reagents->ReagentCount[i] : 0; + + // SpellShapeshiftEntry + if (SpellShapeshiftEntry const* _shapeshift = GetSpellShapeshift()) + { + Stances = MAKE_PAIR64(_shapeshift->ShapeshiftMask[0], _shapeshift->ShapeshiftMask[1]); + StancesNot = MAKE_PAIR64(_shapeshift->ShapeshiftExclude[0], _shapeshift->ShapeshiftExclude[1]); + } + else + { + Stances = UI64LIT(0); + StancesNot = UI64LIT(0); + } + + // SpellTargetRestrictionsEntry + SpellTargetRestrictionsEntry const* _target = GetSpellTargetRestrictions(); + ConeAngle = _target ? _target->ConeAngle : 0.f; + Targets = _target ? _target->Targets : 0; + TargetCreatureType = _target ? _target->TargetCreatureType : 0; + MaxAffectedTargets = _target ? _target->MaxTargets : 0; + MaxTargetLevel = _target ? _target->MaxTargetLevel : 0; + + // SpellTotemsEntry + SpellTotemsEntry const* _totem = GetSpellTotems(); + for (uint8 i = 0; i < 2; ++i) + TotemCategory[i] = _totem ? _totem->RequiredTotemCategoryID[i] : 0; + for (uint8 i = 0; i < 2; ++i) + Totem[i] = _totem ? _totem->Totem[i] : 0; + + ChainEntry = nullptr; + ExplicitTargetMask = 0; + + _spellSpecific = SPELL_SPECIFIC_NORMAL; + _auraState = AURA_STATE_NONE; + + _allowedMechanicMask = 0; +} + +SpellInfo::~SpellInfo() +{ + _UnloadImplicitTargetConditionLists(); +} + +uint32 SpellInfo::GetCategory() const +{ + return CategoryEntry ? CategoryEntry->ID : 0; +} + +bool SpellInfo::HasEffect(SpellEffects effect) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect(effect)) + return true; + return false; +} + +bool SpellInfo::HasAura(AuraType aura) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAura(aura)) + return true; + return false; +} + +bool SpellInfo::HasAreaAuraEffect() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAreaAuraEffect()) + return true; + return false; +} + +bool SpellInfo::HasOnlyDamageEffects() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].IsEffect()) + { + switch (Effects[i].Effect) + { + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_HEALTH_LEECH: + continue; + default: + return false; + } + } + } + + return true; +} + +bool SpellInfo::HasTargetType(::Targets target) const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect()) + continue; + + if (Effects[i].TargetA.GetTarget() == target || Effects[i].TargetB.GetTarget() == target) + return true; + } + + return false; +} + +bool SpellInfo::CanBeInterrupted(Unit const* interruptTarget, bool ignoreImmunity /*= false*/) const +{ + return HasAttribute(SPELL_ATTR7_NO_UI_NOT_INTERRUPTIBLE) + || HasChannelInterruptFlag(SpellAuraInterruptFlags::Damage | SpellAuraInterruptFlags::EnteringCombat) + || (interruptTarget->IsPlayer() && InterruptFlags.HasFlag(SpellInterruptFlags::DamageCancelsPlayerOnly)) + || ((!(interruptTarget->GetMechanicImmunityMask() & (1 << MECHANIC_INTERRUPT)) || ignoreImmunity) + && PreventionType & SPELL_PREVENTION_TYPE_SILENCE); +} + +bool SpellInfo::HasAnyAuraInterruptFlag() const +{ + return AuraInterruptFlags != SpellAuraInterruptFlags::None || AuraInterruptFlags2 != SpellAuraInterruptFlags2::None; +} + +bool SpellInfo::IsExplicitDiscovery() const +{ + return ((Effects[0].Effect == SPELL_EFFECT_CREATE_RANDOM_ITEM + || Effects[0].Effect == SPELL_EFFECT_CREATE_ITEM_2) + && Effects[1].Effect == SPELL_EFFECT_SCRIPT_EFFECT) + || Id == 64323; +} + +bool SpellInfo::IsLootCrafting() const +{ + return (Effects[0].Effect == SPELL_EFFECT_CREATE_RANDOM_ITEM || + // different random cards from Inscription (121==Virtuoso Inking Set category) r without explicit item + (Effects[0].Effect == SPELL_EFFECT_CREATE_ITEM_2 && + ((TotemCategory[0] != 0 || (Totem[0] != 0 && SpellIconID == 1)) || Effects[0].ItemType == 0))); +} + +bool SpellInfo::IsQuestTame() const +{ + return Effects[0].Effect == SPELL_EFFECT_THREAT && Effects[1].Effect == SPELL_EFFECT_APPLY_AURA && Effects[1].ApplyAuraName == SPELL_AURA_DUMMY; +} + +bool SpellInfo::IsProfessionOrRiding() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsProfessionOrRidingSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsProfession() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsProfessionSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsPrimaryProfession() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_SKILL) + { + uint32 skill = Effects[i].MiscValue; + + if (IsPrimaryProfessionSkill(skill)) + return true; + } + } + return false; +} + +bool SpellInfo::IsPrimaryProfessionFirstRank() const +{ + return IsPrimaryProfession() && GetRank() == 1; +} + +bool SpellInfo::IsAbilityLearnedWithProfession() const +{ + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + { + SkillLineAbilityEntry const* pAbility = _spell_idx->second; + if (!pAbility || pAbility->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticSkillRank) + continue; + + if (pAbility->MinSkillLineRank > 0) + return true; + } + + return false; +} + +bool SpellInfo::IsAbilityOfSkillType(uint32 skillType) const +{ + SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(Id); + + for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) + if (_spell_idx->second->SkillLine == uint32(skillType)) + return true; + + return false; +} + +bool SpellInfo::IsAffectingArea() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && (Effects[i].IsTargetingArea() || Effects[i].IsEffect(SPELL_EFFECT_PERSISTENT_AREA_AURA) || Effects[i].IsAreaAuraEffect())) + return true; + return false; +} + +// checks if spell targets are selected from area, doesn't include spell effects in check (like area wide auras for example) +bool SpellInfo::IsTargetingArea() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].IsTargetingArea()) + return true; + return false; +} + +bool SpellInfo::NeedsExplicitUnitTarget() const +{ + return (GetExplicitTargetMask() & TARGET_FLAG_UNIT_MASK) != 0; +} + +bool SpellInfo::NeedsToBeTriggeredByCaster(SpellInfo const* triggeringSpell) const +{ + if (NeedsExplicitUnitTarget()) + return true; + + /* + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].IsEffect()) + { + if (Effects[i].TargetA.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CHANNEL + || Effects[i].TargetB.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CHANNEL) + return true; + } + } + */ + + if (triggeringSpell->IsChanneled()) + { + uint32 mask = 0; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER && Effects[i].TargetA.GetTarget() != TARGET_DEST_CASTER + && Effects[i].TargetB.GetTarget() != TARGET_UNIT_CASTER && Effects[i].TargetB.GetTarget() != TARGET_DEST_CASTER) + { + mask |= Effects[i].GetProvidedTargetMask(); + } + } + + if (mask & TARGET_FLAG_UNIT_MASK) + return true; + } + + return false; +} + +bool SpellInfo::IsSelfCast() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Effect && Effects[i].TargetA.GetTarget() != TARGET_UNIT_CASTER) + return false; + return true; +} + +bool SpellInfo::IsPassive() const +{ + return HasAttribute(SPELL_ATTR0_PASSIVE); +} + +bool SpellInfo::IsAutocastable() const +{ + if (IsPassive()) + return false; + if (HasAttribute(SPELL_ATTR1_NO_AUTOCAST_AI)) + return false; + return true; +} + +bool SpellInfo::IsStackableWithRanks() const +{ + if (IsPassive()) + return false; + if (PowerType != POWER_MANA && PowerType != POWER_HEALTH) + return false; + if (IsProfessionOrRiding()) + return false; + + if (IsAbilityLearnedWithProfession()) + return false; + + // All stance spells. if any better way, change it. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (SpellFamilyName) + { + case SPELLFAMILY_PALADIN: + // Paladin aura Spell + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID) + return false; + break; + case SPELLFAMILY_DRUID: + // Druid form Spell + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA && + Effects[i].ApplyAuraName == SPELL_AURA_MOD_SHAPESHIFT) + return false; + break; + } + } + return true; +} + +bool SpellInfo::IsPassiveStackableWithRanks() const +{ + return IsPassive() && !HasEffect(SPELL_EFFECT_APPLY_AURA); +} + +bool SpellInfo::IsMultiSlotAura() const +{ + if (IsPassive() || HasAura(SPELL_AURA_CONTROL_VEHICLE)) + return true; + + switch (Id) + { + case 55849: // Power Spark + case 40075: // Fel Flak Fire + case 44413: // Incanter's Absorption + case 79010: // Point of Vulnerability + case 79920: // Shared Health + return true; + default: + return false; + } +} + +bool SpellInfo::IsStackableOnOneSlotWithDifferentCasters() const +{ + /// TODO: Re-verify meaning of SPELL_ATTR3_STACK_FOR_DIFF_CASTERS and update conditions here + return StackAmount > 1 && !IsChanneled() && !HasAttribute(SPELL_ATTR3_DOT_STACKING_RULE); +} + +bool SpellInfo::IsCooldownStartedOnEvent() const +{ + if (HasAttribute(SPELL_ATTR0_COOLDOWN_ON_EVENT)) + return true; + + return CategoryEntry && CategoryEntry->GetFlags().HasFlag(SpellCategoryFlags::CooldownEventOnLeaveCombat); +} + +bool SpellInfo::IsDeathPersistent() const +{ + return HasAttribute(SPELL_ATTR3_ALLOW_AURA_WHILE_DEAD); +} + +bool SpellInfo::IsRequiringDeadTarget() const +{ + return HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS); +} + +bool SpellInfo::IsAllowingDeadTarget() const +{ + if (HasAttribute(SPELL_ATTR2_ALLOW_DEAD_TARGET) || Targets & (TARGET_FLAG_CORPSE_ALLY | TARGET_FLAG_CORPSE_ENEMY | TARGET_FLAG_UNIT_DEAD)) + return true; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].TargetA.GetTarget() == TARGET_CORPSE_SRC_AREA_RAID) + return true; + + return false; +} + +bool SpellInfo::IsGroupBuff() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (Effects[i].TargetA.GetCheckType()) + { + case TARGET_CHECK_PARTY: + case TARGET_CHECK_RAID: + case TARGET_CHECK_RAID_CLASS: + return true; + default: + break; + } + } + + return false; +} + +bool SpellInfo::CanBeUsedInCombat() const +{ + return !HasAttribute(SPELL_ATTR0_NOT_IN_COMBAT_ONLY_PEACEFUL); +} + +bool SpellInfo::IsPositive() const +{ + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE); +} + +bool SpellInfo::IsPositiveEffect(uint8 effIndex) const +{ + switch (effIndex) + { + default: + case 0: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF0); + case 1: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF1); + case 2: + return !HasAttribute(SPELL_ATTR0_CU_NEGATIVE_EFF2); + } +} + +bool SpellInfo::IsChanneled() const +{ + return HasAttribute(SpellAttr1(SPELL_ATTR1_IS_CHANNELLED | SPELL_ATTR1_IS_SELF_CHANNELLED)); +} + +bool SpellInfo::IsMoveAllowedChannel() const +{ + return IsChanneled() && !ChannelInterruptFlags.HasFlag(SpellAuraInterruptFlags::Moving | SpellAuraInterruptFlags::Turning); +} + +bool SpellInfo::NeedsComboPoints() const +{ + return HasAttribute(SpellAttr1(SPELL_ATTR1_FINISHING_MOVE_DAMAGE | SPELL_ATTR1_FINISHING_MOVE_DURATION)); +} + +bool SpellInfo::IsNextMeleeSwingSpell() const +{ + return HasAttribute(SpellAttr0(SPELL_ATTR0_ON_NEXT_SWING_NO_DAMAGE | SPELL_ATTR0_ON_NEXT_SWING)); +} + +bool SpellInfo::IsRangedWeaponSpell() const +{ + return (EquippedItemClass == ITEM_CLASS_WEAPON) && (EquippedItemSubClassMask & ITEM_SUBCLASS_MASK_WEAPON_RANGED); +} + +bool SpellInfo::IsAutoRepeatRangedSpell() const +{ + return HasAttribute(SPELL_ATTR2_AUTO_REPEAT); +} + +bool SpellInfo::CausesInitialThreat() const +{ + return !HasAttribute(SPELL_ATTR1_NO_THREAT) && !HasAttribute(SPELL_ATTR2_NO_INITIAL_THREAT) && !HasAttribute(SPELL_ATTR4_NO_HARMFUL_THREAT); +} + +WeaponAttackType SpellInfo::GetAttackType() const +{ + WeaponAttackType result; + switch (DmgClass) + { + case SPELL_DAMAGE_CLASS_MELEE: + if (HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + result = OFF_ATTACK; + else + result = BASE_ATTACK; + break; + case SPELL_DAMAGE_CLASS_RANGED: + result = IsRangedWeaponSpell() ? RANGED_ATTACK : BASE_ATTACK; + break; + default: + // Wands + if (IsAutoRepeatRangedSpell()) + result = RANGED_ATTACK; + else + result = BASE_ATTACK; + break; + } + + return result; +} + +bool SpellInfo::IsAffected(uint32 familyName, flag96 const& familyFlags) const +{ + if (!familyName) + return true; + + if (familyName != SpellFamilyName) + return false; + + if (familyFlags && !(familyFlags & SpellFamilyFlags)) + return false; + + return true; +} + +bool SpellInfo::IsAffectedBySpellMods() const +{ + return !HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS); +} + +bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod) const +{ + if (!IsAffectedBySpellMods()) + return false; + + SpellInfo const* affectSpell = sSpellMgr->GetSpellInfo(mod->spellId); + if (!affectSpell) + return false; + + switch (mod->type) + { + case SPELLMOD_FLAT: + case SPELLMOD_PCT: + // TEMP: dont use IsAffected - !familyName and !familyFlags are not valid options for spell mods + // TODO: investigate if the !familyName and !familyFlags conditions are even valid for all other (nonmod) uses of SpellInfo::IsAffected + return affectSpell->SpellFamilyName == SpellFamilyName && static_cast(mod)->mask & SpellFamilyFlags; + default: + break; + } + + return false; +} + +bool SpellInfo::CanPierceImmuneAura(SpellInfo const* auraSpellInfo) const +{ + // aura can't be pierced + if (!auraSpellInfo || auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + // these spells pierce all available spells (Resurrection Sickness for example) + if (HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return true; + + // these spells (Cyclone for example) can pierce all... + if (HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) || HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + // ...but not these (Divine shield, Ice block, Cyclone and Banish for example) + if (auraSpellInfo->Mechanic != MECHANIC_IMMUNE_SHIELD && + auraSpellInfo->Mechanic != MECHANIC_INVULNERABILITY && + (auraSpellInfo->Mechanic != MECHANIC_BANISH || (IsRankOf(auraSpellInfo) && auraSpellInfo->Dispel != DISPEL_NONE))) // Banish shouldn't be immune to itself, but Cyclone should + return true; + } + + // Dispels other auras on immunity, check if this spell makes the unit immune to aura + if (HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT) && CanSpellProvideImmunityAgainstAura(auraSpellInfo)) + return true; + + return false; +} + +bool SpellInfo::CanDispelAura(SpellInfo const* auraSpellInfo) const +{ + // These auras (like Divine Shield) can't be dispelled + if (auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + // These spells (like Mass Dispel) can dispel all auras + if (HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return true; + + // These auras (Cyclone for example) are not dispelable + if ((auraSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) && auraSpellInfo->Mechanic != MECHANIC_NONE) + || auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + return false; + + return true; +} + +bool SpellInfo::IsSingleTarget() const +{ + // all other single target spells have if it has AttributesEx5 + if (HasAttribute(SPELL_ATTR5_LIMIT_N)) + return true; + + return false; +} + +bool SpellInfo::IsAuraExclusiveBySpecificWith(SpellInfo const* spellInfo) const +{ + SpellSpecificType spellSpec1 = GetSpellSpecific(); + SpellSpecificType spellSpec2 = spellInfo->GetSpellSpecific(); + switch (spellSpec1) + { + case SPELL_SPECIFIC_WARLOCK_ARMOR: + case SPELL_SPECIFIC_MAGE_ARMOR: + case SPELL_SPECIFIC_ELEMENTAL_SHIELD: + case SPELL_SPECIFIC_MAGE_POLYMORPH: + case SPELL_SPECIFIC_PRESENCE: + case SPELL_SPECIFIC_CHARM: + case SPELL_SPECIFIC_SCROLL: + case SPELL_SPECIFIC_WARRIOR_ENRAGE: + case SPELL_SPECIFIC_MAGE_ARCANE_BRILLANCE: + case SPELL_SPECIFIC_PRIEST_DIVINE_SPIRIT: + return spellSpec1 == spellSpec2; + case SPELL_SPECIFIC_FOOD: + return spellSpec2 == SPELL_SPECIFIC_FOOD + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + case SPELL_SPECIFIC_DRINK: + return spellSpec2 == SPELL_SPECIFIC_DRINK + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + case SPELL_SPECIFIC_FOOD_AND_DRINK: + return spellSpec2 == SPELL_SPECIFIC_FOOD + || spellSpec2 == SPELL_SPECIFIC_DRINK + || spellSpec2 == SPELL_SPECIFIC_FOOD_AND_DRINK; + default: + return false; + } +} + +bool SpellInfo::IsAuraExclusiveBySpecificPerCasterWith(SpellInfo const* spellInfo) const +{ + SpellSpecificType spellSpec = GetSpellSpecific(); + switch (spellSpec) + { + case SPELL_SPECIFIC_SEAL: + case SPELL_SPECIFIC_HAND: + case SPELL_SPECIFIC_AURA: + case SPELL_SPECIFIC_STING: + case SPELL_SPECIFIC_CURSE: + case SPELL_SPECIFIC_BANE: + case SPELL_SPECIFIC_ASPECT: + case SPELL_SPECIFIC_WARLOCK_CORRUPTION: + return spellSpec == spellInfo->GetSpellSpecific(); + default: + return false; + } +} + +SpellCastResult SpellInfo::CheckShapeshift(uint32 form) const +{ + // talents that learn spells can have stance requirements that need ignore + // (this requirement only for client-side stance show in talent description) + if (sDBCManager.GetTalentSpellCost(Id) > 0 && + (Effects[0].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[1].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[2].Effect == SPELL_EFFECT_LEARN_SPELL)) + return SPELL_CAST_OK; + + uint64 stanceMask = (form ? UI64LIT(1) << (form - 1) : 0); + + if (stanceMask & StancesNot) // can explicitly not be cast in this stance + return SPELL_FAILED_NOT_SHAPESHIFT; + + if (stanceMask & Stances) // can explicitly be cast in this stance + return SPELL_CAST_OK; + + bool actAsShifted = false; + SpellShapeshiftFormEntry const* shapeInfo = nullptr; + if (form > 0) + { + shapeInfo = sSpellShapeshiftFormStore.LookupEntry(form); + if (!shapeInfo) + { + TC_LOG_ERROR("spells", "GetErrorAtShapeshiftedCast: unknown shapeshift %u", form); + return SPELL_CAST_OK; + } + actAsShifted = !shapeInfo->GetFlags().HasFlag(SpellShapeshiftFormFlags::Stance); + } + + if (actAsShifted) + { + if (HasAttribute(SPELL_ATTR0_NOT_SHAPESHIFTED) || (shapeInfo && shapeInfo->GetFlags().HasFlag(SpellShapeshiftFormFlags::CanOnlyCastShapeshiftSpells))) // not while shapeshifted + return SPELL_FAILED_NOT_SHAPESHIFT; + else if (Stances != 0) // needs other shapeshift + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + else + { + // needs shapeshift + if (!HasAttribute(SPELL_ATTR2_ALLOW_WHILE_NOT_SHAPESHIFTED_CASTER_FORM) && Stances != 0) + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + + // Check if stance disables cast of not-stance spells + // Example: cannot cast any other spells in zombie or ghoul form + /// @todo Find a way to disable use of these spells clientside + if (shapeInfo && shapeInfo->Flags & 0x400) + { + if (!(stanceMask & Stances)) + return SPELL_FAILED_ONLY_SHAPESHIFT; + } + + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckLocation(uint32 map_id, uint32 zone_id, uint32 area_id, Player const* player) const +{ + // normal case + if (AreaGroupId > 0) + { + bool found = false; + AreaGroupEntry const* groupEntry = sAreaGroupStore.LookupEntry(AreaGroupId); + while (groupEntry) + { + for (uint8 i = 0; i < MAX_GROUP_AREA_IDS; ++i) + if (DBCManager::IsInArea(area_id, groupEntry->AreaID[i])) + found = true; + + if (found || !groupEntry->NextAreaID) + break; + // Try search in next group + groupEntry = sAreaGroupStore.LookupEntry(groupEntry->NextAreaID); + } + + if (!found) + return SPELL_FAILED_INCORRECT_AREA; + } + + // continent limitation (virtual continent) + if (HasAttribute(SPELL_ATTR4_ONLY_FLYING_AREAS)) + { + EnumFlag mountFlags = AreaMountFlags::None; + if (player && player->HasAuraType(SPELL_AURA_MOUNT_RESTRICTIONS)) + { + for (AuraEffect const* auraEffect : player->GetAuraEffectsByType(SPELL_AURA_MOUNT_RESTRICTIONS)) + mountFlags |= AreaMountFlags(auraEffect->GetMiscValue()); + } + else if (AreaTableEntry const* areaTable = sAreaTableStore.LookupEntry(area_id)) + mountFlags = areaTable->GetMountFlags(); + + if (!(mountFlags.HasFlag(AreaMountFlags::AllowFlyingMounts))) + return SPELL_FAILED_INCORRECT_AREA; + } + + // raid instance limitation + if (HasAttribute(SPELL_ATTR6_NOT_IN_RAID_INSTANCES)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || mapEntry->IsRaid()) + return SPELL_FAILED_NOT_IN_RAID_INSTANCE; + } + + if (HasAttribute(SPELL_ATTR8_REMOVE_OUTSIDE_DUNGEONS_AND_RAIDS)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || !mapEntry->IsDungeon()) + return SPELL_FAILED_TARGET_NOT_IN_INSTANCE; + } + + if (HasAttribute(SPELL_ATTR8_NOT_IN_BATTLEGROUND)) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry || mapEntry->IsBattleground()) + return SPELL_FAILED_NOT_IN_BATTLEGROUND; + } + + // DB base check (if non empty then must fit at least single for allow) + SpellAreaMapBounds saBounds = sSpellMgr->GetSpellAreaMapBounds(Id); + if (saBounds.first != saBounds.second) + { + for (SpellAreaMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) + { + if (itr->second.IsFitToRequirements(player, zone_id, area_id)) + return SPELL_CAST_OK; + } + return SPELL_FAILED_INCORRECT_AREA; + } + + // bg spell checks + switch (Id) + { + case 23333: // Horde Flag + case 23335: // Alliance Flag + return (map_id == 489 || map_id == 726) && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + case 34976: // Netherstorm Flag + return map_id == 566 && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + case 2584: // Waiting to Resurrect + case 22011: // Spirit Heal Channel + case 22012: // Spirit Heal + case 24171: // Resurrection Impact Visual + case 42792: // Recently Dropped Flag + case 43681: // Inactive + case 44535: // Spirit Heal (mana) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + return zone_id == AREA_WINTERGRASP || (mapEntry->IsBattleground() && player && player->InBattleground()) ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 44521: // Preparation + { + if (!player) + return SPELL_FAILED_REQUIRES_AREA; + + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + if (!mapEntry->IsBattleground()) + return SPELL_FAILED_REQUIRES_AREA; + + Battleground* bg = player->GetBattleground(); + return bg && bg->GetStatus() == STATUS_WAIT_JOIN ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 32724: // Gold Team (Alliance) + case 32725: // Green Team (Alliance) + case 35774: // Gold Team (Horde) + case 35775: // Green Team (Horde) + { + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + return mapEntry->IsBattleArena() && player && player->InBattleground() ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + case 32727: // Arena Preparation + { + if (!player) + return SPELL_FAILED_REQUIRES_AREA; + + MapEntry const* mapEntry = sMapStore.LookupEntry(map_id); + if (!mapEntry) + return SPELL_FAILED_INCORRECT_AREA; + + if (!mapEntry->IsBattleArena()) + return SPELL_FAILED_REQUIRES_AREA; + + Battleground* bg = player->GetBattleground(); + return bg && bg->GetStatus() == STATUS_WAIT_JOIN ? SPELL_CAST_OK : SPELL_FAILED_REQUIRES_AREA; + } + } + + // aura limitations + if (player) + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsAura()) + continue; + + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_SHAPESHIFT: + { + if (SpellShapeshiftFormEntry const* spellShapeshiftForm = sSpellShapeshiftFormStore.LookupEntry(Effects[i].MiscValue)) + if (uint32 mountType = spellShapeshiftForm->MountTypeID) + if (!player->GetMountCapability(mountType)) + return SPELL_FAILED_NOT_HERE; + break; + } + case SPELL_AURA_MOUNTED: + { + if (Effects[i].MiscValueB && !player->GetMountCapability(Effects[i].MiscValueB)) + return SPELL_FAILED_NOT_HERE; + break; + } + break; + } + } + } + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckTarget(WorldObject const* caster, WorldObject const* target, bool implicit /*= true*/) const +{ + if (HasAttribute(SPELL_ATTR1_EXCLUDE_CASTER) && caster == target) + return SPELL_FAILED_BAD_TARGETS; + + // check visibility - ignore stealth for implicit (area) targets + if (!HasAttribute(SPELL_ATTR6_IGNORE_PHASE_SHIFT) && !caster->CanSeeOrDetect(target, implicit)) + return SPELL_FAILED_BAD_TARGETS; + + Unit const* unitTarget = target->ToUnit(); + + /* + * @todo: 4.x - old hold due to an upcoming tempsummon rework + if (HasAttribute(SPELL_ATTR8_ONLY_TARGET_IF_SAME_CREATOR)) + if (caster != target && caster->GetGUID() != target->GetOwnerGUID()) + return SPELL_FAILED_BAD_TARGETS; + */ + + // creature/player specific target checks + if (unitTarget) + { + // spells cannot be cast if target has a pet in combat either + if (HasAttribute(SPELL_ATTR1_ONLY_PEACEFUL_TARGETS) && (unitTarget->IsInCombat() || unitTarget->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT))) + return SPELL_FAILED_TARGET_AFFECTING_COMBAT; + + // only spells with SPELL_ATTR3_ONLY_TARGET_GHOSTS can target ghosts + if (HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS) != unitTarget->HasAuraType(SPELL_AURA_GHOST)) + { + if (HasAttribute(SPELL_ATTR3_ONLY_ON_GHOSTS)) + return SPELL_FAILED_TARGET_NOT_GHOST; + else + return SPELL_FAILED_BAD_TARGETS; + } + + if (caster != unitTarget) + { + if (caster->GetTypeId() == TYPEID_PLAYER) + { + // Do not allow these spells to target creatures not tapped by us (Banish, Polymorph, many quest spells) + if (HasAttribute(SPELL_ATTR2_CANNOT_CAST_ON_TAPPED)) + if (Creature const* targetCreature = unitTarget->ToCreature()) + if (targetCreature->hasLootRecipient() && !targetCreature->isTappedBy(caster->ToPlayer())) + return SPELL_FAILED_CANT_CAST_ON_TAPPED; + + if (HasAttribute(SPELL_ATTR0_CU_PICKPOCKET)) + { + Creature const* targetCreature = unitTarget->ToCreature(); + if (!targetCreature) + return SPELL_FAILED_BAD_TARGETS; + + if (!LootTemplates_Pickpocketing.HaveLootFor(targetCreature->GetCreatureTemplate()->pickpocketLootId)) + return SPELL_FAILED_TARGET_NO_POCKETS; + } + + // Not allow disarm unarmed player + if (Mechanic == MECHANIC_DISARM) + { + if (unitTarget->GetTypeId() == TYPEID_PLAYER) + { + Player const* player = unitTarget->ToPlayer(); + if (!player->GetWeaponForAttack(BASE_ATTACK) || !player->IsUseEquipedWeapon(true)) + return SPELL_FAILED_TARGET_NO_WEAPONS; + } + else if (!unitTarget->GetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID)) + return SPELL_FAILED_TARGET_NO_WEAPONS; + } + } + } + + if (HasAttribute(SPELL_ATTR8_ONLY_TARGET_OWN_SUMMONS)) + if (!unitTarget->IsSummon() || unitTarget->ToTempSummon()->GetSummonerGUID() != caster->GetGUID()) + return SPELL_FAILED_BAD_TARGETS; + } + // corpse specific target checks + else if (Corpse const* corpseTarget = target->ToCorpse()) + { + // cannot target bare bones + if (corpseTarget->GetType() == CORPSE_BONES) + return SPELL_FAILED_BAD_TARGETS; + // we have to use owner for some checks (aura preventing resurrection for example) + if (Player* owner = ObjectAccessor::FindPlayer(corpseTarget->GetOwnerGUID())) + unitTarget = owner; + // we're not interested in corpses without owner + else + return SPELL_FAILED_BAD_TARGETS; + } + // other types of objects - always valid + else return SPELL_CAST_OK; + + // corpseOwner and unit specific target checks + if (!unitTarget->IsPlayer()) + { + if (HasAttribute(SPELL_ATTR3_ONLY_ON_PLAYER)) + return SPELL_FAILED_TARGET_NOT_PLAYER; + + // VIANO_AURA_PLAYER_CONTROLLED_EXCEPTION_V1 + // Viano adventurers keep PLAYER_CONTROLLED for direct helpful targeting, + // but are intentionally eligible for group/raid buffs such as paladin auras. + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) + && unitTarget->IsControlledByPlayer() + && !unitTarget->IsVianoAdventurer()) + return SPELL_FAILED_TARGET_IS_PLAYER_CONTROLLED; + } + else if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER)) + return SPELL_FAILED_TARGET_IS_PLAYER; + + if (!IsAllowingDeadTarget() && !unitTarget->IsAlive()) + return SPELL_FAILED_TARGETS_DEAD; + + // check this flag only for implicit targets (chain and area), allow to explicitly target units for spells like Shield of Righteousness + if (implicit && HasAttribute(SPELL_ATTR6_DO_NOT_CHAIN_TO_CROWD_CONTROLLED_TARGETS) && !unitTarget->CanFreeMove()) + return SPELL_FAILED_BAD_TARGETS; + + // Do not allow pet or guardian targets if the spell is a raid buff or may not target pets at all + if (HasAttribute(SPELL_ATTR5_NOT_ON_PLAYER_CONTROLLED_NPC) && unitTarget->IsCreature() && unitTarget->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED)) + return SPELL_FAILED_BAD_TARGETS; + + // checked in Unit::IsValidAttack/AssistTarget, shouldn't be checked for ENTRY targets + //if (!HasAttribute(SPELL_ATTR6_CAN_TARGET_UNTARGETABLE) && target->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)) + // return SPELL_FAILED_BAD_TARGETS; + + if (!CheckTargetCreatureType(unitTarget)) + { + if (target->GetTypeId() == TYPEID_PLAYER) + return SPELL_FAILED_TARGET_IS_PLAYER; + else + return SPELL_FAILED_BAD_TARGETS; + } + + // check GM mode and GM invisibility - only for player casts (npc casts are controlled by AI) and negative spells + if (unitTarget != caster && (caster->GetAffectingPlayer() || !IsPositive()) && unitTarget->GetTypeId() == TYPEID_PLAYER) + { + if (!unitTarget->ToPlayer()->IsVisible()) + return SPELL_FAILED_BM_OR_INVISGOD; + + if (unitTarget->ToPlayer()->IsGameMaster()) + return SPELL_FAILED_BM_OR_INVISGOD; + } + + // not allow casting on flying player + if (unitTarget->HasUnitState(UNIT_STATE_IN_FLIGHT) && !HasAttribute(SPELL_ATTR0_CU_ALLOW_INFLIGHT_TARGET)) + return SPELL_FAILED_BAD_TARGETS; + + /* TARGET_UNIT_MASTER gets blocked here for passengers, because the whole idea of this check is to + not allow passengers to be implicitly hit by spells, however this target type should be an exception, + if this is left it kills spells that award kill credit from vehicle to master (few spells), + the use of these 2 covers passenger target check, logically, if vehicle cast this to master it should always hit + him, because it would be it's passenger, there's no such case where this gets to fail legitimacy, this problem + cannot be solved from within the check in other way since target type cannot be called for the spell currently + Spell examples: [ID - 52864 Devour Water, ID - 52862 Devour Wind, ID - 49370 Wyrmrest Defender: Destabilize Azure Dragonshrine Effect] */ + if (Unit const* unitCaster = caster->ToUnit()) + { + if (!unitCaster->IsVehicle() && !(unitCaster->GetCharmerOrOwner() == target)) + { + if (TargetAuraState && !unitTarget->HasAuraState(AuraStateType(TargetAuraState), this, unitCaster)) + return SPELL_FAILED_TARGET_AURASTATE; + + if (TargetAuraStateNot && unitTarget->HasAuraState(AuraStateType(TargetAuraStateNot), this, unitCaster)) + return SPELL_FAILED_TARGET_AURASTATE; + } + } + + if (TargetAuraSpell && !unitTarget->HasAura(sSpellMgr->GetSpellIdForDifficulty(TargetAuraSpell, caster))) + return SPELL_FAILED_TARGET_AURASTATE; + + if (ExcludeTargetAuraSpell && unitTarget->HasAura(sSpellMgr->GetSpellIdForDifficulty(ExcludeTargetAuraSpell, caster))) + return SPELL_FAILED_TARGET_AURASTATE; + + if (unitTarget->HasAuraType(SPELL_AURA_PREVENT_RESURRECTION)) + if (HasEffect(SPELL_EFFECT_SELF_RESURRECT) || HasEffect(SPELL_EFFECT_RESURRECT) || HasEffect(SPELL_EFFECT_RESURRECT_NEW)) + return SPELL_FAILED_TARGET_CANNOT_BE_RESURRECTED; + + if (!unitTarget->CheckPrivateObjectOwnerVisibility(caster)) + return SPELL_FAILED_BAD_TARGETS; + + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckExplicitTarget(WorldObject const* caster, WorldObject const* target, Item const* itemTarget) const +{ + uint32 neededTargets = GetExplicitTargetMask(); + if (!target) + { + if (neededTargets & (TARGET_FLAG_UNIT_MASK | TARGET_FLAG_GAMEOBJECT_MASK | TARGET_FLAG_CORPSE_MASK)) + if (!(neededTargets & TARGET_FLAG_GAMEOBJECT_ITEM) || !itemTarget) + return SPELL_FAILED_BAD_TARGETS; + return SPELL_CAST_OK; + } + + if (Unit const* unitTarget = target->ToUnit()) + { + if (neededTargets & (TARGET_FLAG_UNIT_ENEMY | TARGET_FLAG_UNIT_ALLY | TARGET_FLAG_UNIT_RAID | TARGET_FLAG_UNIT_PARTY | TARGET_FLAG_UNIT_MINIPET | TARGET_FLAG_UNIT_PASSENGER)) + { + Unit const* unitCaster = caster->ToUnit(); + if (neededTargets & TARGET_FLAG_UNIT_ENEMY) + if (caster->IsValidAttackTarget(unitTarget, this)) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_ALLY) + || ((neededTargets & TARGET_FLAG_UNIT_PARTY) && unitCaster && unitCaster->IsInPartyWith(unitTarget)) + || ((neededTargets & TARGET_FLAG_UNIT_RAID) && unitCaster && unitCaster->IsInRaidWith(unitTarget))) + if (caster->IsValidAssistTarget(unitTarget, this)) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_MINIPET) && unitCaster) + if (unitTarget->GetGUID() == unitCaster->GetCritterGUID()) + return SPELL_CAST_OK; + if ((neededTargets & TARGET_FLAG_UNIT_PASSENGER) && unitCaster) + if (unitTarget->IsOnVehicle(unitCaster)) + return SPELL_CAST_OK; + return SPELL_FAILED_BAD_TARGETS; + } + } + return SPELL_CAST_OK; +} + +SpellCastResult SpellInfo::CheckVehicle(Unit const* caster) const +{ + // All creatures should be able to cast as passengers freely, restriction and attribute are only for players + if (caster->GetTypeId() != TYPEID_PLAYER) + return SPELL_CAST_OK; + + Vehicle* vehicle = caster->GetVehicle(); + if (vehicle) + { + uint16 checkMask = 0; + for (uint8 effIndex = EFFECT_0; effIndex < MAX_SPELL_EFFECTS; ++effIndex) + { + if (Effects[effIndex].ApplyAuraName == SPELL_AURA_MOD_SHAPESHIFT) + { + SpellShapeshiftFormEntry const* shapeShiftFromEntry = sSpellShapeshiftFormStore.LookupEntry(Effects[effIndex].MiscValue); + if (shapeShiftFromEntry && (shapeShiftFromEntry->Flags & 1) == 0) // unk flag + checkMask |= VEHICLE_SEAT_FLAG_UNCONTROLLED; + break; + } + } + + if (HasAura(SPELL_AURA_MOUNTED)) + checkMask |= VEHICLE_SEAT_FLAG_CAN_CAST_MOUNT_SPELL; + + if (!checkMask) + checkMask = VEHICLE_SEAT_FLAG_CAN_ATTACK; + + VehicleSeatEntry const* vehicleSeat = vehicle->GetSeatForPassenger(caster); + if (!vehicleSeat) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + + if (!HasAttribute(SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE) && !HasAttribute(SPELL_ATTR0_ALLOW_WHILE_MOUNTED) + && (vehicleSeat->Flags & checkMask) != checkMask) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + + // Can only summon uncontrolled minions/guardians when on controlled vehicle + if (vehicleSeat->Flags & (VEHICLE_SEAT_FLAG_CAN_CONTROL | VEHICLE_SEAT_FLAG_UNK2)) + { + for (uint32 i = EFFECT_0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect != SPELL_EFFECT_SUMMON) + continue; + + SummonPropertiesEntry const* props = sSummonPropertiesStore.LookupEntry(Effects[i].MiscValueB); + if (props && props->Control != SUMMON_CATEGORY_WILD) + return SPELL_FAILED_CANT_DO_THAT_RIGHT_NOW; + } + } + } + + return SPELL_CAST_OK; +} + +bool SpellInfo::CheckTargetCreatureType(Unit const* target) const +{ + // Curse of Doom & Exorcism: not find another way to fix spell target check :/ + if (SpellFamilyName == SPELLFAMILY_WARLOCK && GetCategory() == 1179) + { + // not allow cast at player + if (target->GetTypeId() == TYPEID_PLAYER) + return false; + else + return true; + } + + // if target is magnet (i.e Grounding Totem) the check is skipped + if (target->IsMagnet()) + return true; + + uint32 creatureType = target->GetCreatureTypeMask(); + return !TargetCreatureType || !creatureType || (creatureType & TargetCreatureType); +} + +SpellSchoolMask SpellInfo::GetSchoolMask() const +{ + return SpellSchoolMask(SchoolMask); +} + +uint32 SpellInfo::GetAllEffectsMechanicMask() const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].Mechanic) + mask |= 1 << Effects[i].Mechanic; + return mask; +} + +uint32 SpellInfo::GetEffectMechanicMask(uint8 effIndex) const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + if (Effects[effIndex].IsEffect() && Effects[effIndex].Mechanic) + mask |= 1 << Effects[effIndex].Mechanic; + return mask; +} + +uint32 SpellInfo::GetSpellMechanicMaskByEffectMask(uint32 effectMask) const +{ + uint32 mask = 0; + if (Mechanic) + mask |= 1 << Mechanic; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if ((effectMask & (1 << i)) && Effects[i].Mechanic) + mask |= 1 << Effects[i].Mechanic; + return mask; +} + +Mechanics SpellInfo::GetEffectMechanic(uint8 effIndex) const +{ + if (Effects[effIndex].IsEffect() && Effects[effIndex].Mechanic) + return Mechanics(Effects[effIndex].Mechanic); + if (Mechanic) + return Mechanics(Mechanic); + return MECHANIC_NONE; +} + +bool SpellInfo::HasAnyEffectMechanic() const +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].Mechanic) + return true; + return false; +} + +uint32 SpellInfo::GetDispelMask() const +{ + return GetDispelMask(DispelType(Dispel)); +} + +uint32 SpellInfo::GetDispelMask(DispelType type) +{ + // If dispel all + if (type == DISPEL_ALL) + return DISPEL_ALL_MASK; + else + return uint32(1 << type); +} + +uint32 SpellInfo::GetExplicitTargetMask() const +{ + return ExplicitTargetMask; +} + +AuraStateType SpellInfo::GetAuraState() const +{ + return _auraState; +} + +void SpellInfo::_LoadAuraState() +{ + _auraState = [this]() -> AuraStateType + { + // Seals + if (GetSpellSpecific() == SPELL_SPECIFIC_SEAL) + return AURA_STATE_JUDGEMENT; + + // Conflagrate aura state on Immolate and Shadowflame + if (SpellFamilyName == SPELLFAMILY_WARLOCK && + // Immolate + ((SpellFamilyFlags[0] & 4) || + // Shadowflame + (SpellFamilyFlags[2] & 2))) + return AURA_STATE_CONFLAGRATE; + + // Faerie Fire (druid versions) + if (SpellFamilyName == SPELLFAMILY_DRUID && SpellFamilyFlags[0] & 0x400) + return AURA_STATE_FAERIE_FIRE; + + // Sting (hunter's pet ability) + if (GetCategory() == 1133) + return AURA_STATE_FAERIE_FIRE; + + // Victorious + if (SpellFamilyName == SPELLFAMILY_WARRIOR && SpellFamilyFlags[1] & 0x00040000) + return AURA_STATE_WARRIOR_VICTORY_RUSH; + + // Swiftmend state on Regrowth & Rejuvenation + if (SpellFamilyName == SPELLFAMILY_DRUID && SpellFamilyFlags[0] & 0x50) + return AURA_STATE_SWIFTMEND; + + // Deadly poison aura state + if (SpellFamilyName == SPELLFAMILY_ROGUE && SpellFamilyFlags[0] & 0x10000) + return AURA_STATE_DEADLY_POISON; + + // Enrage aura state + if (Dispel == DISPEL_ENRAGE) + return AURA_STATE_ENRAGE; + + // Bleeding aura state + if (GetAllEffectsMechanicMask() & 1 << MECHANIC_BLEED) + return AURA_STATE_BLEEDING; + + if (GetSchoolMask() & SPELL_SCHOOL_MASK_FROST) + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsAura() && (Effects[i].ApplyAuraName == SPELL_AURA_MOD_STUN + || Effects[i].ApplyAuraName == SPELL_AURA_MOD_ROOT)) + return AURA_STATE_FROZEN; + + switch (Id) + { + case 71465: // Divine Surge + case 50241: // Evasive Charges + return AURA_STATE_UNKNOWN22; + case 9991: // Touch of Zanzil + case 35325: // Glowing Blood + case 35328: // Lambent Blood + case 35329: // Vibrant Blood + case 35331: // Black Blood + case 49163: // Perpetual Instability + return AURA_STATE_FAERIE_FIRE; + default: + break; + } + + return AURA_STATE_NONE; + }(); +} + +SpellSpecificType SpellInfo::GetSpellSpecific() const +{ + return _spellSpecific; +} + +void SpellInfo::_LoadSpellSpecific() +{ + _spellSpecific = [this]()->SpellSpecificType + { + switch (SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + { + // Food / Drinks (mostly) + if (HasAuraInterruptFlag(SpellAuraInterruptFlags::Standing)) + { + bool food = false; + bool drink = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsAura()) + continue; + switch (Effects[i].ApplyAuraName) + { + // Food + case SPELL_AURA_MOD_REGEN: + case SPELL_AURA_OBS_MOD_HEALTH: + food = true; + break; + // Drink + case SPELL_AURA_MOD_POWER_REGEN: + case SPELL_AURA_OBS_MOD_POWER: + drink = true; + break; + default: + break; + } + } + + if (food && drink) + return SPELL_SPECIFIC_FOOD_AND_DRINK; + else if (food) + return SPELL_SPECIFIC_FOOD; + else if (drink) + return SPELL_SPECIFIC_DRINK; + } + // scrolls effects + else + { + SpellInfo const* firstRankSpellInfo = GetFirstRankSpell(); + switch (firstRankSpellInfo->Id) + { + case 8118: // Strength + case 8099: // Stamina + case 8112: // Spirit + case 8096: // Intellect + case 8115: // Agility + case 8091: // Armor + return SPELL_SPECIFIC_SCROLL; + case 12880: // Enrage (Enrage) + case 57518: // Enrage (Wrecking Crew) + return SPELL_SPECIFIC_WARRIOR_ENRAGE; + } + } + break; + } + case SPELLFAMILY_MAGE: + { + // family flags 18(Molten), 25(Frost/Ice), 28(Mage) + if (SpellFamilyFlags[0] & 0x12040000) + return SPELL_SPECIFIC_MAGE_ARMOR; + + // Arcane brillance and Arcane intelect (normal check fails because of flags difference) + if (SpellFamilyFlags[0] & 0x400) + return SPELL_SPECIFIC_MAGE_ARCANE_BRILLANCE; + + if ((SpellFamilyFlags[0] & 0x1000000) && Effects[0].ApplyAuraName == SPELL_AURA_MOD_CONFUSE) + return SPELL_SPECIFIC_MAGE_POLYMORPH; + + break; + } + case SPELLFAMILY_WARRIOR: + { + if (Id == 12292) // Death Wish + return SPELL_SPECIFIC_WARRIOR_ENRAGE; + + break; + } + case SPELLFAMILY_WARLOCK: + { + // Warlock (Bane of Doom | Bane of Agony | Bane of Havoc) + if (Id == 603 || Id == 980 || Id == 80240) + return SPELL_SPECIFIC_BANE; + + // only warlock curses have this + if (Dispel == DISPEL_CURSE) + return SPELL_SPECIFIC_CURSE; + + // Warlock (Demon Armor | Demon Skin | Fel Armor) + if (SpellFamilyFlags[1] & 0x20000020 || SpellFamilyFlags[2] & 0x00000010) + return SPELL_SPECIFIC_WARLOCK_ARMOR; + + //seed of corruption and corruption + if (SpellFamilyFlags[1] & 0x10 || SpellFamilyFlags[0] & 0x2) + return SPELL_SPECIFIC_WARLOCK_CORRUPTION; + break; + } + case SPELLFAMILY_PRIEST: + { + // Divine Spirit and Prayer of Spirit + if (SpellFamilyFlags[0] & 0x20) + return SPELL_SPECIFIC_PRIEST_DIVINE_SPIRIT; + + break; + } + case SPELLFAMILY_HUNTER: + { + // only hunter stings have this + if (Dispel == DISPEL_POISON) + return SPELL_SPECIFIC_STING; + + // only hunter aspects have this (but not all aspects in hunter family) + if (SpellFamilyFlags.HasFlag(0x00380000, 0x00440000, 0x00001010)) + return SPELL_SPECIFIC_ASPECT; + + break; + } + case SPELLFAMILY_PALADIN: + { + // Collection of all the seal family flags. No other paladin spell has any of those. + if (SpellFamilyFlags[1] & 0xA2000800) + return SPELL_SPECIFIC_SEAL; + + if (SpellFamilyFlags[0] & 0x00002190) + return SPELL_SPECIFIC_HAND; + + // only paladin auras have this (for palaldin class family) + if (SpellFamilyFlags[2] & 0x00000020) + return SPELL_SPECIFIC_AURA; + + break; + } + case SPELLFAMILY_SHAMAN: + { + // family flags 10 (Lightning), 42 (Earth), 37 (Water), proc shield from T2 8 pieces bonus + if (SpellFamilyFlags[1] & 0x420 + || SpellFamilyFlags[0] & 0x00000400 + || Id == 23552) + return SPELL_SPECIFIC_ELEMENTAL_SHIELD; + + break; + } + case SPELLFAMILY_DEATHKNIGHT: + if (Id == 48266 || Id == 48263 || Id == 48265) + return SPELL_SPECIFIC_PRESENCE; + break; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA) + { + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_CHARM: + case SPELL_AURA_MOD_POSSESS: + case SPELL_AURA_AOE_CHARM: + return SPELL_SPECIFIC_CHARM; + case SPELL_AURA_TRACK_CREATURES: + /// @workaround For non-stacking tracking spells (We need generic solution) + if (Id == 30645) // Gas Cloud Tracking + return SPELL_SPECIFIC_NORMAL; + [[fallthrough]]; + case SPELL_AURA_TRACK_RESOURCES: + case SPELL_AURA_TRACK_STEALTHED: + return SPELL_SPECIFIC_TRACKER; + } + } + } + + return SPELL_SPECIFIC_NORMAL; + }(); +} + +void SpellInfo::_LoadSpellDiminishInfo() +{ + auto diminishingGroupCompute = [this](bool triggered) -> DiminishingGroup + { + if (IsPositive()) + return DIMINISHING_NONE; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].ApplyAuraName == SPELL_AURA_MOD_TAUNT) + return DIMINISHING_TAUNT; + } + + // Explicit Diminishing Groups + switch (SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + { + // Pet charge effects (Infernal Awakening, Demon Charge) + if (SpellVisual[0] == 2816 && SpellIconID == 15) + return DIMINISHING_CONTROLLED_STUN; + // Frost Tomb + else if (Id == 48400) + return DIMINISHING_NONE; + // Gnaw + else if (Id == 47481) + return DIMINISHING_CONTROLLED_STUN; + // ToC Icehowl Arctic Breath + else if (SpellVisual[0] == 14153) + return DIMINISHING_NONE; + // Black Plague + else if (Id == 64155) + return DIMINISHING_NONE; + // Screams of the Dead (King Ymiron) + else if (Id == 51750) + return DIMINISHING_NONE; + break; + } + // Event spells + case SPELLFAMILY_UNK1: + return DIMINISHING_NONE; + case SPELLFAMILY_MAGE: + { + // Frostbite + if (SpellFamilyFlags[1] & 0x80000000) + return DIMINISHING_ROOT; + // Shattered Barrier + else if (SpellVisual[0] == 12297) + return DIMINISHING_ROOT; + // Deep Freeze + else if (SpellIconID == 2939 && SpellVisual[0] == 9963) + return DIMINISHING_CONTROLLED_STUN; + // Frost Nova / Freeze (Water Elemental) + else if (SpellIconID == 193) + return DIMINISHING_CONTROLLED_ROOT; + // Dragon's Breath + else if (SpellFamilyFlags[0] & 0x800000) + return DIMINISHING_DRAGONS_BREATH; + break; + } + case SPELLFAMILY_WARRIOR: + { + // Hamstring - limit duration to 10s in PvP + if (SpellFamilyFlags[0] & 0x2) + return DIMINISHING_LIMITONLY; + // Charge Stun (own diminishing) + else if (SpellFamilyFlags[0] & 0x01000000) + return DIMINISHING_CHARGE; + break; + } + case SPELLFAMILY_WARLOCK: + { + // Curses/etc + if ((SpellFamilyFlags[0] & 0x80000000) || (SpellFamilyFlags[1] & 0x200)) + return DIMINISHING_LIMITONLY; + // Seduction + else if (SpellFamilyFlags[1] & 0x10000000) + return DIMINISHING_FEAR; + // Sin and Punishment (Priest spell, don't ask) + else if (SpellIconID == 1869) + return DIMINISHING_NONE; + break; + } + case SPELLFAMILY_DRUID: + { + // Pounce - there are two spells which share the same family: the stun effect and the bleed spell. We don't want the bleed to be affected by DR + if ((SpellFamilyFlags[0] & 0x20000) && Id != 9007) + return DIMINISHING_OPENING_STUN; + // Cyclone + else if (SpellFamilyFlags[1] & 0x20) + return DIMINISHING_CYCLONE; + // Entangling Roots + // Nature's Grasp + else if (SpellFamilyFlags[0] & 0x00000200) + return DIMINISHING_CONTROLLED_ROOT; + // Faerie Fire + else if (SpellFamilyFlags[0] & 0x400) + return DIMINISHING_LIMITONLY; + break; + } + case SPELLFAMILY_ROGUE: + { + // Gouge + if (SpellFamilyFlags[0] & 0x8) + return DIMINISHING_DISORIENT; + // Blind + else if (SpellFamilyFlags[0] & 0x1000000) + return DIMINISHING_FEAR; + // Cheap Shot + else if (SpellFamilyFlags[0] & 0x400) + return DIMINISHING_OPENING_STUN; + // Crippling poison - Limit to 10 seconds in PvP (No SpellFamilyFlags) + else if (SpellIconID == 163) + return DIMINISHING_LIMITONLY; + break; + } + case SPELLFAMILY_HUNTER: + { + // Hunter's Mark + if ((SpellFamilyFlags[0] & 0x400) && SpellIconID == 538) + return DIMINISHING_LIMITONLY; + // Scatter Shot (own diminishing) + else if ((SpellFamilyFlags[0] & 0x40000) && SpellIconID == 132) + return DIMINISHING_SCATTER_SHOT; + // Entrapment (own diminishing) + else if (SpellVisual[0] == 7484 && SpellIconID == 20) + return DIMINISHING_ENTRAPMENT; + // Wyvern Sting mechanic is MECHANIC_SLEEP but the diminishing is DIMINISHING_DISORIENT + else if ((SpellFamilyFlags[1] & 0x1000) && SpellIconID == 1721) + return DIMINISHING_DISORIENT; + // Freezing Arrow + else if (SpellFamilyFlags[0] & 0x8) + return DIMINISHING_DISORIENT; + break; + } + case SPELLFAMILY_PALADIN: + { + // Judgement of Justice - limit duration to 10s in PvP + if (SpellFamilyFlags[0] & 0x100000) + return DIMINISHING_LIMITONLY; + // Turn Evil + else if ((SpellFamilyFlags[1] & 0x804000) && SpellIconID == 309) + return DIMINISHING_FEAR; + break; + } + case SPELLFAMILY_SHAMAN: + { + // Storm, Earth and Fire - Earthgrab + if (SpellFamilyFlags[2] & 0x4000) + return DIMINISHING_NONE; + break; + } + case SPELLFAMILY_DEATHKNIGHT: + { + // Hungering Cold (no flags) + if (SpellIconID == 2797) + return DIMINISHING_DISORIENT; + // Mark of Blood + else if ((SpellFamilyFlags[0] & 0x10000000) && SpellIconID == 2285) + return DIMINISHING_LIMITONLY; + break; + } + default: + break; + } + + // Lastly - Set diminishing depending on mechanic + uint32 mechanic = GetAllEffectsMechanicMask(); + if (mechanic & (1 << MECHANIC_CHARM)) + return DIMINISHING_MIND_CONTROL; + if (mechanic & (1 << MECHANIC_SILENCE)) + return DIMINISHING_SILENCE; + if (mechanic & (1 << MECHANIC_SLEEP)) + return DIMINISHING_SLEEP; + if (mechanic & ((1 << MECHANIC_SAPPED) | (1 << MECHANIC_POLYMORPH) | (1 << MECHANIC_SHACKLE))) + return DIMINISHING_DISORIENT; + // Mechanic Knockout, except Blast Wave + if (mechanic & (1 << MECHANIC_KNOCKOUT) && SpellIconID != 292) + return DIMINISHING_DISORIENT; + if (mechanic & (1 << MECHANIC_DISARM)) + return DIMINISHING_DISARM; + if (mechanic & (1 << MECHANIC_FEAR)) + return DIMINISHING_FEAR; + if (mechanic & (1 << MECHANIC_STUN)) + return triggered ? DIMINISHING_STUN : DIMINISHING_CONTROLLED_STUN; + if (mechanic & (1 << MECHANIC_BANISH)) + return DIMINISHING_BANISH; + if (mechanic & (1 << MECHANIC_ROOT)) + return triggered ? DIMINISHING_ROOT : DIMINISHING_CONTROLLED_ROOT; + if (mechanic & (1 << MECHANIC_HORROR)) + return DIMINISHING_HORROR; + + return DIMINISHING_NONE; + }; + + auto diminishingTypeCompute = [](DiminishingGroup group) -> DiminishingReturnsType + { + switch (group) + { + case DIMINISHING_TAUNT: + case DIMINISHING_CONTROLLED_STUN: + case DIMINISHING_STUN: + case DIMINISHING_OPENING_STUN: + case DIMINISHING_CYCLONE: + case DIMINISHING_CHARGE: + return DRTYPE_ALL; + case DIMINISHING_LIMITONLY: + case DIMINISHING_NONE: + return DRTYPE_NONE; + default: + return DRTYPE_PLAYER; + } + }; + + auto diminishingMaxLevelCompute = [](DiminishingGroup group) -> DiminishingLevels + { + switch (group) + { + case DIMINISHING_TAUNT: + return DIMINISHING_LEVEL_TAUNT_IMMUNE; + default: + return DIMINISHING_LEVEL_IMMUNE; + } + }; + + auto diminishingLimitDurationCompute = [this](DiminishingGroup group) -> int32 + { + auto isGroupDurationLimited = [group]() -> bool + { + switch (group) + { + case DIMINISHING_BANISH: + case DIMINISHING_CONTROLLED_STUN: + case DIMINISHING_CONTROLLED_ROOT: + case DIMINISHING_CYCLONE: + case DIMINISHING_DISORIENT: + case DIMINISHING_ENTRAPMENT: + case DIMINISHING_FEAR: + case DIMINISHING_HORROR: + case DIMINISHING_MIND_CONTROL: + case DIMINISHING_OPENING_STUN: + case DIMINISHING_ROOT: + case DIMINISHING_STUN: + case DIMINISHING_SLEEP: + case DIMINISHING_LIMITONLY: + return true; + default: + return false; + } + }; + + if (!isGroupDurationLimited()) + return 0; + + // Explicit diminishing duration + switch (SpellFamilyName) + { + case SPELLFAMILY_DRUID: + { + // Faerie Fire - limit to 40 seconds in PvP (3.1) + if (SpellFamilyFlags[0] & 0x400) + return 40 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_HUNTER: + { + // Wyvern Sting + if (SpellFamilyFlags[1] & 0x1000) + return 6 * IN_MILLISECONDS; + // Hunter's Mark + if (SpellFamilyFlags[0] & 0x400) + return 30 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_PALADIN: + { + // Repentance - limit to 6 seconds in PvP + if (SpellFamilyFlags[0] & 0x4) + return 6 * IN_MILLISECONDS; + break; + } + case SPELLFAMILY_WARLOCK: + { + // Banish - limit to 6 seconds in PvP + if (SpellFamilyFlags[1] & 0x8000000) + return 6 * IN_MILLISECONDS; + // Curse of Tongues - limit to 12 seconds in PvP + else if (SpellFamilyFlags[2] & 0x800) + return 12 * IN_MILLISECONDS; + // Curse of Elements - limit to 120 seconds in PvP + else if (SpellFamilyFlags[1] & 0x200) + return 120 * IN_MILLISECONDS; + break; + } + default: + break; + } + + return 8 * IN_MILLISECONDS; + }; + + SpellDiminishInfo triggeredInfo, normalInfo; + triggeredInfo.DiminishGroup = diminishingGroupCompute(true); + triggeredInfo.DiminishReturnType = diminishingTypeCompute(triggeredInfo.DiminishGroup); + triggeredInfo.DiminishMaxLevel = diminishingMaxLevelCompute(triggeredInfo.DiminishGroup); + triggeredInfo.DiminishDurationLimit = diminishingLimitDurationCompute(triggeredInfo.DiminishGroup); + + normalInfo.DiminishGroup = diminishingGroupCompute(false); + normalInfo.DiminishReturnType = diminishingTypeCompute(normalInfo.DiminishGroup); + normalInfo.DiminishMaxLevel = diminishingMaxLevelCompute(normalInfo.DiminishGroup); + normalInfo.DiminishDurationLimit = diminishingLimitDurationCompute(normalInfo.DiminishGroup); + + _diminishInfoTriggered = triggeredInfo; + _diminishInfoNonTriggered = normalInfo; +} + +DiminishingGroup SpellInfo::GetDiminishingReturnsGroupForSpell(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishGroup : _diminishInfoNonTriggered.DiminishGroup; +} + +DiminishingReturnsType SpellInfo::GetDiminishingReturnsGroupType(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishReturnType : _diminishInfoNonTriggered.DiminishReturnType; +} + +DiminishingLevels SpellInfo::GetDiminishingReturnsMaxLevel(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishMaxLevel : _diminishInfoNonTriggered.DiminishMaxLevel; +} + +int32 SpellInfo::GetDiminishingReturnsLimitDuration(bool triggered) const +{ + return triggered ? _diminishInfoTriggered.DiminishDurationLimit : _diminishInfoNonTriggered.DiminishDurationLimit; +} + +void SpellInfo::_LoadImmunityInfo() +{ + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + uint32 schoolImmunityMask = 0; + uint32 applyHarmfulAuraImmunityMask = 0; + uint32 mechanicImmunityMask = 0; + uint32 dispelImmunity = 0; + uint32 damageImmunityMask = 0; + + if (!Effects[i].IsEffect()) + continue; + + int32 miscVal = Effects[i].MiscValue; + int32 amount = Effects[i].CalcValue(); + + ImmunityInfo& immuneInfo = _immunityInfo[i]; + + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_MECHANIC_IMMUNITY_MASK: + { + switch (miscVal) + { + case 96: // Free Friend, Uncontrollable Frenzy, Warlord's Presence + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + break; + } + case 1615: // Incite Rage, Wolf Spirit, Overload, Lightning Tendrils + { + switch (Id) + { + case 43292: // Incite Rage + case 49172: // Wolf Spirit + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + [[fallthrough]]; + case 61869: // Overload + case 63481: + case 61887: // Lightning Tendrils + case 63486: + mechanicImmunityMask |= (1 << MECHANIC_INTERRUPT) | (1 << MECHANIC_SILENCE); + + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK); + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK_DEST); + break; + default: + break; + } + break; + } + case 679: // Mind Control, Avenging Fury + { + if (Id == 57742) // Avenging Fury + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 1557: // Startling Roar, Warlord Roar, Break Bonds, Stormshield + { + if (Id == 64187) // Stormshield + { + mechanicImmunityMask |= (1 << MECHANIC_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + } + else + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 1614: // Fixate + case 1694: // Fixated, Lightning Tendrils + { + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_ATTACK_ME); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_TAUNT); + break; + } + case 1630: // Fervor, Berserk + { + if (Id == 64112) // Berserk + { + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_ATTACK_ME); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_TAUNT); + } + else + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 477: // Bladestorm + case 1733: // Bladestorm, Killing Spree + { + if (!amount) + { + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK); + immuneInfo.SpellEffectImmune.insert(SPELL_EFFECT_KNOCK_BACK_DEST); + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + } + break; + } + case 878: // Whirlwind, Fog of Corruption, Determination + { + if (Id == 66092) // Determination + { + mechanicImmunityMask |= (1 << MECHANIC_SNARE) | (1 << MECHANIC_STUN) + | (1 << MECHANIC_DISORIENTED) | (1 << MECHANIC_FREEZE); + + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + } + break; + } + default: + break; + } + + if (immuneInfo.AuraTypeImmune.empty()) + { + if (miscVal & (1 << 10)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_STUN); + if (miscVal & (1 << 1)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_TRANSFORM); + + // These flag can be recognized wrong: + if (miscVal & (1 << 6)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DECREASE_SPEED); + if (miscVal & (1 << 0)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_ROOT); + if (miscVal & (1 << 2)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_CONFUSE); + if (miscVal & (1 << 9)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_FEAR); + if (miscVal & (1 << 7)) + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_MOD_DISARM); + } + break; + } + case SPELL_AURA_MECHANIC_IMMUNITY: + { + switch (Id) + { + case 42292: // PvP trinket + case 59752: // Every Man for Himself + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + immuneInfo.AuraTypeImmune.insert(SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED); + break; + case 34471: // The Beast Within + case 19574: // Bestial Wrath + case 53490: // Bullheaded + mechanicImmunityMask |= IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK; + break; + case 54508: // Demonic Empowerment + mechanicImmunityMask |= (1 << MECHANIC_SNARE) | (1 << MECHANIC_ROOT) | (1 << MECHANIC_STUN); + break; + default: + if (miscVal < 1) + continue; + + mechanicImmunityMask |= 1 << miscVal; + break; + } + break; + } + case SPELL_AURA_EFFECT_IMMUNITY: + { + immuneInfo.SpellEffectImmune.insert(static_cast(miscVal)); + break; + } + case SPELL_AURA_STATE_IMMUNITY: + { + immuneInfo.AuraTypeImmune.insert(static_cast(miscVal)); + break; + } + case SPELL_AURA_SCHOOL_IMMUNITY: + { + schoolImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: + { + applyHarmfulAuraImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_DAMAGE_IMMUNITY: + { + damageImmunityMask |= uint32(miscVal); + break; + } + case SPELL_AURA_DISPEL_IMMUNITY: + { + dispelImmunity = uint32(miscVal); + break; + } + default: + break; + } + + immuneInfo.SchoolImmuneMask = schoolImmunityMask; + immuneInfo.ApplyHarmfulAuraImmuneMask = applyHarmfulAuraImmunityMask; + immuneInfo.MechanicImmuneMask = mechanicImmunityMask; + immuneInfo.DispelImmune = dispelImmunity; + immuneInfo.DamageSchoolMask = damageImmunityMask; + + immuneInfo.AuraTypeImmune.shrink_to_fit(); + immuneInfo.SpellEffectImmune.shrink_to_fit(); + + _allowedMechanicMask |= immuneInfo.MechanicImmuneMask; + } + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_STUNNED)) + { + switch (Id) + { + case 22812: // Barkskin + case 47585: // Dispersion + _allowedMechanicMask |= + (1 << MECHANIC_STUN) | + (1 << MECHANIC_FREEZE) | + (1 << MECHANIC_KNOCKOUT) | + (1 << MECHANIC_SLEEP); + break; + case 49039: // Lichborne, don't allow normal stuns + break; + default: + _allowedMechanicMask |= (1 << MECHANIC_STUN); + break; + } + } + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_CONFUSED)) + _allowedMechanicMask |= (1 << MECHANIC_DISORIENTED); + + if (HasAttribute(SPELL_ATTR5_ALLOW_WHILE_FLEEING)) + { + switch (Id) + { + case 22812: // Barkskin + case 47585: // Dispersion + _allowedMechanicMask |= (1 << MECHANIC_FEAR) | (1 << MECHANIC_HORROR); + break; + default: + _allowedMechanicMask |= (1 << MECHANIC_FEAR); + break; + } + } +} + +void SpellInfo::ApplyAllSpellImmunitiesTo(Unit* target, uint8 effIndex, bool apply) const +{ + ImmunityInfo const& immuneInfo = _immunityInfo[effIndex]; + + if (uint32 schoolImmunity = immuneInfo.SchoolImmuneMask) + { + target->ApplySpellImmune(Id, IMMUNITY_SCHOOL, schoolImmunity, apply); + + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + target->RemoveAppliedAuras([this, schoolImmunity](AuraApplication const* aurApp) -> bool + { + SpellInfo const* auraSpellInfo = aurApp->GetBase()->GetSpellInfo(); + return ((auraSpellInfo->GetSchoolMask() & schoolImmunity) != 0 && // Check for school mask + CanDispelAura(auraSpellInfo) && + (IsPositive() != aurApp->IsPositive()) && // Check spell vs aura possitivity + !auraSpellInfo->IsPassive() && // Don't remove passive auras + auraSpellInfo->Id != Id); // Don't remove self + }); + } + if (apply && schoolImmunity & SPELL_SCHOOL_MASK_NORMAL) + target->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::InvulnerabilityBuff); + } + + if (uint32 mechanicImmunity = immuneInfo.MechanicImmuneMask) + { + for (uint32 i = MECHANIC_CHARM; i < MAX_MECHANIC; ++i) + if (mechanicImmunity & (1 << i)) + target->ApplySpellImmune(Id, IMMUNITY_MECHANIC, i, apply); + + if (HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + if (apply) + target->RemoveAurasWithMechanic(mechanicImmunity, AuraRemoveFlags::ByDefault, Id); + else + { + std::vector aurasToUpdateTargets; + target->RemoveAppliedAuras([mechanicImmunity, &aurasToUpdateTargets](AuraApplication const* aurApp) + { + Aura* aura = aurApp->GetBase(); + if (aura->GetSpellInfo()->GetAllEffectsMechanicMask() & mechanicImmunity) + aurasToUpdateTargets.push_back(aura); + + // only update targets, don't remove anything + return false; + }); + + for (Aura* aura : aurasToUpdateTargets) + aura->UpdateTargetMap(aura->GetCaster()); + } + } + } + + if (uint32 dispelImmunity = immuneInfo.DispelImmune) + { + target->ApplySpellImmune(Id, IMMUNITY_DISPEL, dispelImmunity, apply); + + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + { + target->RemoveAppliedAuras([dispelImmunity](AuraApplication const* aurApp) -> bool + { + SpellInfo const* spellInfo = aurApp->GetBase()->GetSpellInfo(); + if (spellInfo->Dispel == dispelImmunity) + return true; + + return false; + }); + } + } + + if (uint32 damageImmunity = immuneInfo.DamageSchoolMask) + { + target->ApplySpellImmune(Id, IMMUNITY_DAMAGE, damageImmunity, apply); + + if (apply && damageImmunity & SPELL_SCHOOL_MASK_NORMAL) + target->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::InvulnerabilityBuff); + } + + for (AuraType auraType : immuneInfo.AuraTypeImmune) + { + target->ApplySpellImmune(Id, IMMUNITY_STATE, auraType, apply); + if (apply && HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + target->RemoveAurasByType(auraType, [](AuraApplication const* aurApp) -> bool + { + // if the aura has SPELL_ATTR0_NO_IMMUNITIES, then it cannot be removed by immunity + return !aurApp->GetBase()->GetSpellInfo()->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES); + }); + } + + for (SpellEffects effectType : immuneInfo.SpellEffectImmune) + target->ApplySpellImmune(Id, IMMUNITY_EFFECT, effectType, apply); +} + +bool SpellInfo::CanSpellProvideImmunityAgainstAura(SpellInfo const* auraSpellInfo) const +{ + if (!auraSpellInfo) + return false; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + ImmunityInfo const* immuneInfo = _immunityInfo + i; + + if (!auraSpellInfo->HasAttribute(SPELL_ATTR1_IMMUNITY_TO_HOSTILE_AND_FRIENDLY_EFFECTS) && !auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + if (uint32 schoolImmunity = immuneInfo->SchoolImmuneMask) + if ((auraSpellInfo->SchoolMask & schoolImmunity) != 0) + return true; + } + + if (uint32 mechanicImmunity = immuneInfo->MechanicImmuneMask) + if ((mechanicImmunity & (1 << auraSpellInfo->Mechanic)) != 0) + return true; + + if (uint32 dispelImmunity = immuneInfo->DispelImmune) + if (auraSpellInfo->Dispel == dispelImmunity) + return true; + + bool immuneToAllEffects = true; + for (uint8 effIndex = 0; effIndex < MAX_SPELL_EFFECTS; ++effIndex) + { + uint32 effectName = auraSpellInfo->Effects[effIndex].Effect; + if (!effectName) + continue; + + auto spellImmuneItr = immuneInfo->SpellEffectImmune.find(static_cast(effectName)); + if (spellImmuneItr == immuneInfo->SpellEffectImmune.cend()) + { + immuneToAllEffects = false; + break; + } + + if (uint32 mechanic = auraSpellInfo->Effects[effIndex].Mechanic) + { + if (!(immuneInfo->MechanicImmuneMask & (1 << mechanic))) + { + immuneToAllEffects = false; + break; + } + } + + if (!auraSpellInfo->HasAttribute(SPELL_ATTR3_ALWAYS_HIT)) + { + if (uint32 auraName = auraSpellInfo->Effects[effIndex].ApplyAuraName) + { + bool isImmuneToAuraEffectApply = false; + auto auraImmuneItr = immuneInfo->AuraTypeImmune.find(static_cast(auraName)); + if (auraImmuneItr != immuneInfo->AuraTypeImmune.cend()) + isImmuneToAuraEffectApply = true; + + if (!isImmuneToAuraEffectApply && !auraSpellInfo->IsPositiveEffect(effIndex) && !auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES)) + { + if (uint32 applyHarmfulAuraImmunityMask = immuneInfo->ApplyHarmfulAuraImmuneMask) + if ((auraSpellInfo->GetSchoolMask() & applyHarmfulAuraImmunityMask) != 0) + isImmuneToAuraEffectApply = true; + } + + if (!isImmuneToAuraEffectApply) + { + immuneToAllEffects = false; + break; + } + } + } + } + + if (immuneToAllEffects) + return true; + } + + return false; +} + +// based on client Spell_C::CancelsAuraEffect +bool SpellInfo::SpellCancelsAuraEffect(SpellInfo const* auraSpellInfo, uint8 auraEffIndex) const +{ + if (!HasAttribute(SPELL_ATTR1_IMMUNITY_PURGES_EFFECT)) + return false; + + if (auraSpellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES)) + return false; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (Effects[i].Effect != SPELL_EFFECT_APPLY_AURA && Effects[i].Effect != SPELL_EFFECT_APPLY_AURA_2) + continue; + + uint32 const miscValue = static_cast(Effects[i].MiscValue); + switch (Effects[i].ApplyAuraName) + { + case SPELL_AURA_STATE_IMMUNITY: + if (miscValue != auraSpellInfo->Effects[auraEffIndex].ApplyAuraName) + continue; + break; + case SPELL_AURA_SCHOOL_IMMUNITY: + case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: + if (auraSpellInfo->HasAttribute(SPELL_ATTR2_NO_SCHOOL_IMMUNITIES) || !(auraSpellInfo->SchoolMask & miscValue)) + continue; + break; + case SPELL_AURA_DISPEL_IMMUNITY: + if (miscValue != auraSpellInfo->Dispel) + continue; + break; + case SPELL_AURA_MECHANIC_IMMUNITY: + if (miscValue != auraSpellInfo->Mechanic) + { + if (miscValue != auraSpellInfo->Effects[auraEffIndex].Mechanic) + continue; + } + break; + default: + continue; + } + + return true; + } + + return false; +} + +uint32 SpellInfo::GetAllowedMechanicMask() const +{ + return _allowedMechanicMask; +} + +uint32 SpellInfo::GetMechanicImmunityMask(Unit* caster) const +{ + uint32 casterMechanicImmunityMask = caster->GetMechanicImmunityMask(); + uint32 mechanicImmunityMask = 0; + + if (CanBeInterrupted(caster, true)) + { + if (casterMechanicImmunityMask & (1 << MECHANIC_INTERRUPT)) + mechanicImmunityMask |= (1 << MECHANIC_INTERRUPT); + + if (casterMechanicImmunityMask & (1 << MECHANIC_SILENCE)) + mechanicImmunityMask |= (1 << MECHANIC_SILENCE); + } + + return mechanicImmunityMask; +} + +float SpellInfo::CalculateScaledCoefficient(Unit const* caster, float coefficient) const +{ + if (coefficient == 0.f || !caster || SpellScalingId == 0) + return coefficient; + + return coefficient *= GetSpellScalingMultiplier(caster->getLevel()); +} + +float SpellInfo::GetMinRange(bool positive) const +{ + if (!RangeEntry) + return 0.0f; + if (positive) + return RangeEntry->RangeMin[1]; + return RangeEntry->RangeMin[0]; +} + +float SpellInfo::GetMaxRange(bool positive, WorldObject* caster, Spell* spell) const +{ + if (!RangeEntry) + return 0.0f; + float range; + if (positive) + range = RangeEntry->RangeMax[1]; + else + range = RangeEntry->RangeMax[0]; + if (caster) + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Range, range, spell); + return range; +} + +int32 SpellInfo::GetDuration() const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + return (DurationEntry->Duration == -1) ? -1 : abs(DurationEntry->Duration); +} + +int32 SpellInfo::GetMaxDuration() const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + return (DurationEntry->MaxDuration == -1) ? -1 : abs(DurationEntry->MaxDuration); +} + +float SpellInfo::CalcPeriodicHasteMod(Unit const* caster) const +{ + float hasteMod = 1.f; + if (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC)) + hasteMod = caster->GetFloatValue(UNIT_MOD_CAST_HASTE); + else if (HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC) && caster->IsPlayer()) + hasteMod = caster->GetFloatValue(PLAYER_FIELD_MOD_HASTE); + + return hasteMod; +} + +int32 SpellInfo::CalcDuration(WorldObject const* caster /*= nullptr*/) const +{ + if (!DurationEntry) + return IsPassive() ? -1 : 0; + + int32 duration = GetMaxDuration(); + if (duration == -1) + return -1; + + Unit const* unitCaster = caster ? caster->ToUnit() : nullptr; + if (!unitCaster) + return std::min(GetMaxDuration(), DurationEntry->Duration); + + uint32 level = unitCaster->getLevel(); + if (MaxLevel > 0 && level > MaxLevel) + level = MaxLevel; + if (BaseLevel > 0) + level -= BaseLevel; + + duration = DurationEntry->Duration + DurationEntry->DurationPerLevel * level; + duration = std::min(GetMaxDuration(), duration); + + if (duration == -1) + return -1; + + // Increase duration based on combo points + if (HasAttribute(SPELL_ATTR1_FINISHING_MOVE_DURATION)) + { + if (uint8 comboPoints = unitCaster->IsMovedByClient() ? unitCaster->GetGameClientMovingMe()->GetBasePlayer()->GetComboPoints() : 0) + { + if (GetDuration() != GetMaxDuration() && GetDuration() != -1) + duration += int32((GetMaxDuration() - GetDuration()) * comboPoints / 5); + } + } + + if (Player const* modOwner = unitCaster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Duration, duration); + + if (HasAttribute(SPELL_ATTR8_HASTE_AFFECTS_DURATION) || HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC)) + { + SpellEffIndex periodicEffectIndex = EFFECT_0; + bool hasPeriodicEffect = [&]() + { + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (Effects[i].IsEffect() && Effects[i].AuraPeriod) + { + periodicEffectIndex = SpellEffIndex(i); + return true; + } + + return false; + }(); + + if (hasPeriodicEffect && !HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS) && (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC) || HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC))) + { + float hasteMod = CalcPeriodicHasteMod(unitCaster); + if (hasteMod > 0.001f) + { + if (HasAttribute(SPELL_ATTR8_HASTE_AFFECTS_DURATION)) + return int32(duration * hasteMod); + + int32 period = CalcPeriod(caster, periodicEffectIndex); + if (period > 0) + duration = int32(period * (duration / (float)period)); + } + } + } + + return duration; +} + +int32 SpellInfo::CalcPeriod(WorldObject const* caster, SpellEffIndex effIndex, Optional periodOverride /*= {}*/) const +{ + int32 period = 0; + if (periodOverride.has_value() && *periodOverride > 0) + period = *periodOverride; + else + { + if (Effects[effIndex].IsEffect() && Effects[effIndex].AuraPeriod != 0) + period = Effects[effIndex].AuraPeriod; + else + period = 5200; + } + + Unit const* unitCaster = caster ? caster->ToUnit() : nullptr; + if (!unitCaster) + return period; + + if (Player const* modOwner = unitCaster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::Period, period); + + if (!HasAttribute(SPELL_ATTR3_IGNORE_CASTER_MODIFIERS) && (HasAttribute(SPELL_ATTR5_SPELL_HASTE_AFFECTS_PERIODIC) || HasAttribute(SPELL_ATTR8_MELEE_HASTE_AFFECTS_PERIODIC))) + { + float hasteMod = CalcPeriodicHasteMod(unitCaster); + if (hasteMod > 0.001f) + period = ((float)period * hasteMod); + } + + return period; +} + +uint32 SpellInfo::CalcCastTime(uint8 level, Spell* spell /*= nullptr*/) const +{ + int32 castTime = 0; + if (!level && spell && spell->GetCaster()->IsUnit()) + level = spell->GetCaster()->ToUnit()->getLevel(); + + // not all spells have cast time index and this is all is passive abilities + if (level && Scaling.CastTimeMax > 0) + { + castTime = Scaling.CastTimeMax; + if (Scaling.CastTimeMaxLevel > level) + castTime = Scaling.CastTimeMin + int32(level - 1) * (Scaling.CastTimeMax - Scaling.CastTimeMin) / (Scaling.CastTimeMaxLevel - 1); + } + else if (CastTimeEntry) + castTime = CastTimeEntry->Base; + + if (!castTime) + return 0; + + if (spell) + spell->GetCaster()->ModSpellCastTime(this, castTime, spell); + + return (castTime > 0) ? uint32(castTime) : 0; +} + +uint32 SpellInfo::GetMaxTicks() const +{ + uint32 totalTicks = 0; + int32 DotDuration = GetDuration(); + + for (uint8 x = 0; x < MAX_SPELL_EFFECTS; x++) + { + if (Effects[x].Effect == SPELL_EFFECT_APPLY_AURA || Effects[x].Effect == SPELL_EFFECT_APPLY_AURA_2) + { + switch (Effects[x].ApplyAuraName) + { + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_PERIODIC_HEAL: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_48: + case SPELL_AURA_POWER_BURN: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_PERIODIC_DUMMY: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL: + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + case SPELL_AURA_PERIODIC_HEALTH_FUNNEL: + // skip infinite periodics + if (Effects[x].AuraPeriod > 0 && DotDuration > 0) + { + totalTicks = static_cast(DotDuration) / Effects[x].AuraPeriod; + if (HasAttribute(SPELL_ATTR5_EXTRA_INITIAL_PERIOD)) + ++totalTicks; + } + break; + default: + break; + } + } + } + + return totalTicks; +} + +uint32 SpellInfo::GetRecoveryTime() const +{ + return RecoveryTime > CategoryRecoveryTime ? RecoveryTime : CategoryRecoveryTime; +} + +int32 SpellInfo::CalcPowerCost(WorldObject const* caster, SpellSchoolMask schoolMask, Spell* spell) const +{ + // gameobject casts don't use power + Unit const* unitCaster = caster->ToUnit(); + if (!unitCaster) + return 0; + + // Spell drain all exist power on cast (Only paladin lay of Hands) + if (HasAttribute(SPELL_ATTR1_USE_ALL_MANA)) + { + // If power type - health drain all + if (PowerType == POWER_HEALTH) + return unitCaster->GetHealth(); + // Else drain all power + if (PowerType < MAX_POWERS) + return unitCaster->GetPower(static_cast<::PowerType>(PowerType)); + + TC_LOG_ERROR("spells", "SpellInfo::CalcPowerCost: Unknown power type '%d' in spell %d", PowerType, Id); + return 0; + } + + bool invertSign = false; + int32 powerCost = 0; + + if (SpellPowerId) + { + int32 manaCost = ManaCost; + powerCost = manaCost; + float powerCostPct = ManaCostPercentage2 != 0.f ? ManaCostPercentage2 : (float)ManaCostPercentage; + + if (manaCost < 0 || powerCostPct < 0.f) + { + manaCost = std::abs(manaCost); + powerCostPct = std::fabs(powerCostPct); + invertSign = true; + powerCost = manaCost; + } + + // Base calculation + if (ManaCostPercentage) + { + int32 ressource = 0; + switch (PowerType) + { + // health as power used + case POWER_HEALTH: + ressource = unitCaster->GetCreateHealth(); + break; + case POWER_MANA: + ressource = unitCaster->GetCreateMana(); + break; + case POWER_RAGE: + case POWER_FOCUS: + case POWER_ENERGY: + ressource = unitCaster->GetMaxPower(static_cast<::PowerType>(PowerType)); + break; + case POWER_RUNE: + case POWER_RUNIC_POWER: + TC_LOG_DEBUG("spells", "CalculateManaCost: Not implemented yet!"); + break; + default: + TC_LOG_ERROR("spells", "CalculateManaCost: Unknown power type '%d' in spell %d", PowerType, Id); + return 0; + } + + manaCost += int32((ressource * powerCostPct) / 100); + powerCost = manaCost; + } + + // Scaling + if (SpellScalingId) + powerCost = int32(ceilf(GetSpellScalingMultiplier(unitCaster->getLevel(), true) * (double)powerCost)); + else + { + uint32 manaCostPerLevel = invertSign ? -int32(ManaCostPerlevel) : ManaCostPerlevel; + powerCost = manaCostPerLevel * SpellLevel + manaCost; + } + } + + // Flat mod from caster auras by spell school and power type + Unit::AuraEffectList const& auras = unitCaster->GetAuraEffectsByType(SPELL_AURA_MOD_POWER_COST_SCHOOL); + for (Unit::AuraEffectList::const_iterator i = auras.begin(); i != auras.end(); ++i) + { + if (!((*i)->GetMiscValue() & schoolMask)) + continue; + if (!((*i)->GetMiscValueB() & (1 << PowerType))) + continue; + powerCost += (*i)->GetAmount(); + } + + // Shiv - costs 20 + weaponSpeed*10 energy (apply only to non-triggered spell with energy cost) + if (HasAttribute(SPELL_ATTR4_WEAPON_SPEED_COST_SCALING)) + { + uint32 speed = 0; + if (SpellShapeshiftFormEntry const* ss = sSpellShapeshiftFormStore.LookupEntry(unitCaster->GetShapeshiftForm())) + speed = ss->CombatRoundTime; + else + { + WeaponAttackType slot = BASE_ATTACK; + if (!HasAttribute(SPELL_ATTR3_REQUIRES_MAIN_HAND_WEAPON) && HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + slot = OFF_ATTACK; + + speed = unitCaster->GetBaseAttackTime(slot); + } + + powerCost += speed / 100; + } + + // Apply cost mod by spell + if (Player* modOwner = caster->GetSpellModOwner()) + modOwner->ApplySpellMod(this, SpellModOp::PowerCost0, powerCost, spell); + + if (!unitCaster->IsControlledByPlayer()) + { + if (HasAttribute(SPELL_ATTR0_SCALES_WITH_CREATURE_LEVEL)) + { + GtNPCManaCostScalerEntry const* spellScaler = sGtNPCManaCostScalerStore.LookupEntry(SpellLevel - 1); + GtNPCManaCostScalerEntry const* casterScaler = sGtNPCManaCostScalerStore.LookupEntry(unitCaster->getLevel() - 1); + if (spellScaler && spellScaler->ratio > 0.f && casterScaler && casterScaler->ratio > 0.f) + powerCost *= casterScaler->ratio / spellScaler->ratio; + } + } + + if (invertSign) + powerCost = -powerCost; + + // PCT mod from user auras by spell school and power type + Unit::AuraEffectList const& aurasPct = unitCaster->GetAuraEffectsByType(SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT); + for (Unit::AuraEffectList::const_iterator i = aurasPct.begin(); i != aurasPct.end(); ++i) + { + if (!((*i)->GetMiscValue() & schoolMask)) + continue; + if (!((*i)->GetMiscValueB() & (1 << PowerType))) + continue; + powerCost += CalculatePct(powerCost, (*i)->GetAmount()); + } + if (powerCost < 0) + powerCost = 0; + return powerCost; +} + +float SpellInfo::GetSpellScalingMultiplier(int32 targetLevel, bool isPowerCostRelated /*= false*/) const +{ + if (targetLevel <= 0 || Scaling.Class == 0) + return 1.f; + + float multiplier = 1.f; + float scalingMultiplier = 1.f; + + if (targetLevel < Scaling.CastTimeMaxLevel && Scaling.CastTimeMax) + { + int32 castTime = Scaling.CastTimeMin + ((targetLevel - 1) * (Scaling.CastTimeMax - Scaling.CastTimeMin)) / (Scaling.CastTimeMaxLevel - 1); + multiplier = castTime / static_cast(Scaling.CastTimeMax); + scalingMultiplier = castTime / static_cast(Scaling.CastTimeMax); + } + + if (!isPowerCostRelated) + { + if (targetLevel < Scaling.NerfMaxLevel) + scalingMultiplier = ((((1.f - Scaling.NerfFactor) * (targetLevel - 1)) / (Scaling.NerfMaxLevel - 1)) + Scaling.NerfFactor) * multiplier; + } + + return scalingMultiplier; +} + +bool SpellInfo::IsRanked() const +{ + return ChainEntry != nullptr; +} + +uint8 SpellInfo::GetRank() const +{ + if (!ChainEntry) + return 1; + return ChainEntry->rank; +} + +SpellInfo const* SpellInfo::GetFirstRankSpell() const +{ + if (!ChainEntry) + return this; + return ChainEntry->first; +} +SpellInfo const* SpellInfo::GetLastRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->last; +} +SpellInfo const* SpellInfo::GetNextRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->next; +} +SpellInfo const* SpellInfo::GetPrevRankSpell() const +{ + if (!ChainEntry) + return nullptr; + return ChainEntry->prev; +} + +SpellInfo const* SpellInfo::GetAuraRankForLevel(uint8 level) const +{ + // ignore passive spells + if (IsPassive()) + return this; + + // Client ignores spell with these attributes (sub_53D9D0) + if (HasAttribute(SPELL_ATTR0_AURA_IS_DEBUFF) || HasAttribute(SPELL_ATTR2_ALLOW_LOW_LEVEL_BUFF)) + return this; + + bool needRankSelection = false; + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (IsPositiveEffect(i) && + (Effects[i].Effect == SPELL_EFFECT_APPLY_AURA || + Effects[i].Effect == SPELL_EFFECT_APPLY_AURA_2 || + Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_PARTY || + Effects[i].Effect == SPELL_EFFECT_APPLY_AREA_AURA_RAID) && + !Effects[i].Scaling.Coefficient) + { + needRankSelection = true; + break; + } + } + + // not required + if (!needRankSelection) + return this; + + for (SpellInfo const* nextSpellInfo = this; nextSpellInfo != nullptr; nextSpellInfo = nextSpellInfo->GetPrevRankSpell()) + { + // if found appropriate level + if (uint32(level + 10) >= nextSpellInfo->SpellLevel) + return nextSpellInfo; + + // one rank less then + } + + // not found + return nullptr; +} + +bool SpellInfo::IsRankOf(SpellInfo const* spellInfo) const +{ + return GetFirstRankSpell() == spellInfo->GetFirstRankSpell(); +} + +bool SpellInfo::IsDifferentRankOf(SpellInfo const* spellInfo) const +{ + if (Id == spellInfo->Id) + return false; + return IsRankOf(spellInfo); +} + +bool SpellInfo::IsHighRankOf(SpellInfo const* spellInfo) const +{ + if (ChainEntry && spellInfo->ChainEntry) + if (ChainEntry->first == spellInfo->ChainEntry->first) + if (ChainEntry->rank > spellInfo->ChainEntry->rank) + return true; + + return false; +} + +void SpellInfo::_InitializeExplicitTargetMask() +{ + bool srcSet = false; + bool dstSet = false; + uint32 targetMask = Targets; + // prepare target mask using effect target entries + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect()) + continue; + + targetMask |= Effects[i].TargetA.GetExplicitTargetMask(srcSet, dstSet); + targetMask |= Effects[i].TargetB.GetExplicitTargetMask(srcSet, dstSet); + + // add explicit target flags based on spell effects which have EFFECT_IMPLICIT_TARGET_EXPLICIT and no valid target provided + if (Effects[i].GetImplicitTargetType() != EFFECT_IMPLICIT_TARGET_EXPLICIT) + continue; + + // extend explicit target mask only if valid targets for effect could not be provided by target types + uint32 effectTargetMask = Effects[i].GetMissingTargetMask(srcSet, dstSet, targetMask); + + // don't add explicit object/dest flags when spell has no max range + if (GetMaxRange(true) == 0.0f && GetMaxRange(false) == 0.0f) + effectTargetMask &= ~(TARGET_FLAG_UNIT_MASK | TARGET_FLAG_GAMEOBJECT | TARGET_FLAG_CORPSE_MASK | TARGET_FLAG_DEST_LOCATION); + + targetMask |= effectTargetMask; + } + + ExplicitTargetMask = targetMask; +} + +inline bool _isPositiveTarget(SpellInfo const* spellInfo, uint8 effIndex) +{ + if (!spellInfo->Effects[effIndex].IsEffect()) + return true; + + return (spellInfo->Effects[effIndex].TargetA.GetCheckType() != TARGET_CHECK_ENEMY && + spellInfo->Effects[effIndex].TargetB.GetCheckType() != TARGET_CHECK_ENEMY); +} + +bool _isPositiveEffectImpl(SpellInfo const* spellInfo, uint8 effIndex, std::unordered_set>& visited) +{ + if (!spellInfo->Effects[effIndex].IsEffect()) + return true; + + // attribute may be already set in DB + if (!spellInfo->IsPositiveEffect(effIndex)) + return false; + + // passive auras like talents are all positive + if (spellInfo->IsPassive()) + return true; + + // not found a single positive spell with this attribute + if (spellInfo->HasAttribute(SPELL_ATTR0_AURA_IS_DEBUFF)) + return false; + + if (spellInfo->HasAttribute(SPELL_ATTR4_AURA_IS_BUFF)) + return true; + + visited.insert({ spellInfo->Id, effIndex }); + + //We need scaling level info for some auras that compute bp 0 or positive but should be debuffs + float bpScalePerLevel = spellInfo->Effects[effIndex].RealPointsPerLevel; + int32 bp = spellInfo->Effects[effIndex].CalcValue(); + switch (spellInfo->SpellFamilyName) + { + case SPELLFAMILY_GENERIC: + switch (spellInfo->Id) + { + case 40268: // Spiritual Vengeance, Teron Gorefiend, Black Temple + case 61987: // Avenging Wrath Marker + case 61988: // Divine Shield exclude aura + case 64412: // Phase Punch, Algalon the Observer, Ulduar + case 72410: // Rune of Blood, Saurfang, Icecrown Citadel + case 71204: // Touch of Insignificance, Lady Deathwhisper, Icecrown Citadel + return false; + case 24732: // Bat Costume + case 30877: // Tag Murloc + case 61716: // Rabbit Costume + case 61734: // Noblegarden Bunny + case 62344: // Fists of Stone + case 50344: // Dream Funnel + case 61819: // Manabonked! (item) + case 61834: // Manabonked! (minigob) + case 73523: // Rigor Mortis + return true; + default: + break; + } + break; + case SPELLFAMILY_ROGUE: + switch (spellInfo->Id) + { + // Envenom must be considered as a positive effect even though it deals damage + case 32645: // Envenom + return true; + case 40251: // Shadow of Death, Teron Gorefiend, Black Temple + return false; + default: + break; + } + break; + case SPELLFAMILY_MAGE: + // Arcane Missiles + if (spellInfo->SpellFamilyFlags[0] == 0x00000800) + return false; + break; + case SPELLFAMILY_WARRIOR: + // Slam, Execute + if ((spellInfo->SpellFamilyFlags[0] & 0x20200000) != 0) + return false; + break; + default: + break; + } + + switch (spellInfo->Mechanic) + { + case MECHANIC_IMMUNE_SHIELD: + return true; + default: + break; + } + + // Special case: effects which determine positivity of whole spell + if (spellInfo->HasAttribute(SPELL_ATTR1_AURA_UNIQUE)) // @todo: rename to and handle SPELL_ATTR1_AURA_UNIQUE + { + // check for targets, there seems to be an assortment of dummy triggering spells that should be negative + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (!_isPositiveTarget(spellInfo, i)) + return false; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + switch (spellInfo->Effects[i].Effect) + { + case SPELL_EFFECT_HEAL: + case SPELL_EFFECT_LEARN_SPELL: + case SPELL_EFFECT_SKILL_STEP: + case SPELL_EFFECT_HEAL_PCT: + return true; + case SPELL_EFFECT_INSTAKILL: + if (i != effIndex && // for spells like 38044: instakill effect is negative but auras on target must count as buff + spellInfo->Effects[i].TargetA.GetTarget() == spellInfo->Effects[effIndex].TargetA.GetTarget() && + spellInfo->Effects[i].TargetB.GetTarget() == spellInfo->Effects[effIndex].TargetB.GetTarget()) + return false; + break; + default: + break; + } + + if (spellInfo->Effects[i].IsAura()) + { + switch (spellInfo->Effects[i].ApplyAuraName) + { + case SPELL_AURA_MOD_STEALTH: + case SPELL_AURA_MOD_UNATTACKABLE: + return true; + case SPELL_AURA_SCHOOL_HEAL_ABSORB: + case SPELL_AURA_EMPATHY: + case SPELL_AURA_MOD_DAMAGE_FROM_CASTER: + case SPELL_AURA_PREVENTS_FLEEING: + return false; + default: + break; + } + } + } + + switch (spellInfo->Effects[effIndex].Effect) + { + case SPELL_EFFECT_WEAPON_DAMAGE: + case SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL: + case SPELL_EFFECT_NORMALIZED_WEAPON_DMG: + case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE: + case SPELL_EFFECT_SCHOOL_DAMAGE: + case SPELL_EFFECT_ENVIRONMENTAL_DAMAGE: + case SPELL_EFFECT_HEALTH_LEECH: + case SPELL_EFFECT_INSTAKILL: + case SPELL_EFFECT_POWER_DRAIN: + case SPELL_EFFECT_STEAL_BENEFICIAL_BUFF: + case SPELL_EFFECT_INTERRUPT_CAST: + case SPELL_EFFECT_PICKPOCKET: + case SPELL_EFFECT_GAMEOBJECT_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE: + case SPELL_EFFECT_DURABILITY_DAMAGE_PCT: + case SPELL_EFFECT_APPLY_AREA_AURA_ENEMY: + case SPELL_EFFECT_TAMECREATURE: + case SPELL_EFFECT_DISTRACT: + return false; + case SPELL_EFFECT_ENERGIZE: + case SPELL_EFFECT_ENERGIZE_PCT: + case SPELL_EFFECT_HEAL_PCT: + case SPELL_EFFECT_HEAL_MAX_HEALTH: + case SPELL_EFFECT_HEAL_MECHANICAL: + return true; + case SPELL_EFFECT_KNOCK_BACK: + case SPELL_EFFECT_CHARGE: + case SPELL_EFFECT_PERSISTENT_AREA_AURA: + case SPELL_EFFECT_ATTACK_ME: + case SPELL_EFFECT_POWER_BURN: + // check targets + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_EFFECT_DISPEL: + // non-positive dispel + switch (spellInfo->Effects[effIndex].MiscValue) + { + case DISPEL_STEALTH: + case DISPEL_INVISIBILITY: + case DISPEL_ENRAGE: + return false; + default: + break; + } + + // also check targets + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_EFFECT_DISPEL_MECHANIC: + if (!_isPositiveTarget(spellInfo, effIndex)) + { + // non-positive mechanic dispel on negative target + switch (spellInfo->Effects[effIndex].MiscValue) + { + case MECHANIC_BANDAGE: + case MECHANIC_SHIELD: + case MECHANIC_MOUNT: + case MECHANIC_INVULNERABILITY: + return false; + default: + break; + } + } + break; + case SPELL_EFFECT_THREAT: + case SPELL_EFFECT_MODIFY_THREAT_PERCENT: + // check targets AND basepoints + if (!_isPositiveTarget(spellInfo, effIndex) && bp > 0) + return false; + break; + default: + break; + } + + if (spellInfo->Effects[effIndex].IsAura()) + { + // non-positive aura use + switch (spellInfo->Effects[effIndex].ApplyAuraName) + { + case SPELL_AURA_MOD_STAT: // dependent from basepoint sign (negative -> negative) + case SPELL_AURA_MOD_SKILL: + case SPELL_AURA_MOD_DODGE_PERCENT: + case SPELL_AURA_MOD_HEALING_DONE: + case SPELL_AURA_MOD_DAMAGE_DONE_CREATURE: + case SPELL_AURA_OBS_MOD_HEALTH: + case SPELL_AURA_OBS_MOD_POWER: + case SPELL_AURA_MOD_CRIT_PCT: + case SPELL_AURA_MOD_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_SPELL_CRIT_CHANCE: + case SPELL_AURA_MOD_RANGED_HASTE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE: + case SPELL_AURA_MOD_MELEE_RANGED_HASTE_2: + case SPELL_AURA_MOD_CASTING_SPEED_NOT_STACK: + case SPELL_AURA_HASTE_SPELLS: + case SPELL_AURA_HASTE_RANGED: + case SPELL_AURA_MOD_DETECT_RANGE: + case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT: + case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE: + case SPELL_AURA_MOD_INCREASE_SWIM_SPEED: + case SPELL_AURA_MOD_PERCENT_STAT: + case SPELL_AURA_MOD_INCREASE_HEALTH: + case SPELL_AURA_MOD_SPEED_ALWAYS: + if (bp < 0 || bpScalePerLevel < 0) //TODO: What if both are 0? Should it be a buff or debuff? + return false; + break; + case SPELL_AURA_MOD_ATTACKSPEED: // some buffs have negative bp, check both target and bp + case SPELL_AURA_MOD_MELEE_HASTE: + case SPELL_AURA_MOD_DAMAGE_DONE: + case SPELL_AURA_MOD_RESISTANCE: + case SPELL_AURA_MOD_RESISTANCE_PCT: + case SPELL_AURA_MOD_RATING: + case SPELL_AURA_MOD_ATTACK_POWER: + case SPELL_AURA_MOD_RANGED_ATTACK_POWER: + case SPELL_AURA_MOD_DAMAGE_PERCENT_DONE: + case SPELL_AURA_MOD_SPEED_SLOW_ALL: + case SPELL_AURA_MELEE_SLOW: + case SPELL_AURA_MOD_ATTACK_POWER_PCT: + case SPELL_AURA_MOD_HEALING_DONE_PERCENT: + case SPELL_AURA_MOD_HEALING_PCT: + case SPELL_AURA_MOD_DAMAGE_DONE_VERSUS_AURASTATE: + if (!_isPositiveTarget(spellInfo, effIndex) && bp < 0) + return false; + break; + case SPELL_AURA_MOD_DAMAGE_TAKEN: // dependent from basepoint sign (positive -> negative) + case SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN: + case SPELL_AURA_MOD_MELEE_DAMAGE_TAKEN_PCT: + case SPELL_AURA_MOD_POWER_COST_SCHOOL: + case SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT: + case SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT: + case SPELL_AURA_BYPASS_ARMOR_FOR_CASTER: + if (bp > 0) + return false; + break; + case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN: // check targets and basepoints (ex Recklessness) + if (!_isPositiveTarget(spellInfo, effIndex) && bp > 0) + return false; + break; + case SPELL_AURA_MOD_HEALTH_REGEN_PERCENT: // check targets and basepoints (target enemy and negative bp -> negative) + if (!_isPositiveTarget(spellInfo, effIndex) && bp < 0) + return false; + break; + case SPELL_AURA_ADD_TARGET_TRIGGER: + return true; + case SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE: + if (!_isPositiveTarget(spellInfo, effIndex)) + { + if (SpellInfo const* spellTriggeredProto = sSpellMgr->GetSpellInfo(spellInfo->Effects[effIndex].TriggerSpell)) + { + // negative targets of main spell return early + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // already seen this + if (visited.count({ spellTriggeredProto->Id, i }) > 0) + continue; + + if (!spellTriggeredProto->Effects[i].Effect) + continue; + + // if non-positive trigger cast targeted to positive target this main cast is non-positive + // this will place this spell auras as debuffs + if (_isPositiveTarget(spellTriggeredProto, i) && !_isPositiveEffectImpl(spellTriggeredProto, i, visited)) + return false; + } + } + } + break; + case SPELL_AURA_PERIODIC_TRIGGER_SPELL: + case SPELL_AURA_MOD_STUN: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_MOD_DECREASE_SPEED: + case SPELL_AURA_MOD_FEAR: + case SPELL_AURA_MOD_TAUNT: + // special auras: they may have non negative target but still need to be marked as debuff + // checked again after all effects (SpellInfo::_InitializeSpellPositivity) + case SPELL_AURA_MOD_PACIFY: + case SPELL_AURA_MOD_PACIFY_SILENCE: + case SPELL_AURA_MOD_DISARM: + case SPELL_AURA_MOD_DISARM_OFFHAND: + case SPELL_AURA_MOD_DISARM_RANGED: + case SPELL_AURA_MOD_CHARM: + case SPELL_AURA_AOE_CHARM: + case SPELL_AURA_MOD_POSSESS: + case SPELL_AURA_MOD_LANGUAGE: + case SPELL_AURA_DAMAGE_SHIELD: + case SPELL_AURA_PROC_TRIGGER_SPELL: + case SPELL_AURA_MOD_ATTACKER_MELEE_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_RANGED_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_SPELL_HIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_MELEE_CRIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_RANGED_CRIT_CHANCE: + case SPELL_AURA_MOD_ATTACKER_SPELL_AND_WEAPON_CRIT_CHANCE: + case SPELL_AURA_DUMMY: + case SPELL_AURA_PERIODIC_DUMMY: + case SPELL_AURA_MOD_HEALING: + case SPELL_AURA_MOD_WEAPON_CRIT_PERCENT: + case SPELL_AURA_POWER_BURN: + case SPELL_AURA_MOD_COOLDOWN: + case SPELL_AURA_MOD_INCREASE_SPEED: + case SPELL_AURA_MOD_PARRY_PERCENT: + case SPELL_AURA_SET_VEHICLE_ID: + case SPELL_AURA_PERIODIC_ENERGIZE: + case SPELL_AURA_EFFECT_IMMUNITY: + case SPELL_AURA_OVERRIDE_CLASS_SCRIPTS: + case SPELL_AURA_MOD_SHAPESHIFT: + case SPELL_AURA_MOD_THREAT: + case SPELL_AURA_PROC_TRIGGER_SPELL_WITH_VALUE: + // check target for positive and negative spells + if (!_isPositiveTarget(spellInfo, effIndex)) + return false; + break; + case SPELL_AURA_MOD_CONFUSE: + case SPELL_AURA_CHANNEL_DEATH_ITEM: + case SPELL_AURA_MOD_ROOT: + case SPELL_AURA_MOD_SILENCE: + case SPELL_AURA_MOD_DETAUNT: + case SPELL_AURA_GHOST: + case SPELL_AURA_PERIODIC_LEECH: + case SPELL_AURA_PERIODIC_MANA_LEECH: + case SPELL_AURA_MOD_STALKED: + case SPELL_AURA_PREVENT_RESURRECTION: + case SPELL_AURA_PERIODIC_DAMAGE: + case SPELL_AURA_PERIODIC_DAMAGE_PERCENT: + case SPELL_AURA_MELEE_ATTACK_POWER_ATTACKER_BONUS: + case SPELL_AURA_RANGED_ATTACK_POWER_ATTACKER_BONUS: + case SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE: + return false; + case SPELL_AURA_MECHANIC_IMMUNITY: + { + // non-positive immunities + switch (spellInfo->Effects[effIndex].MiscValue) + { + case MECHANIC_BANDAGE: + case MECHANIC_SHIELD: + case MECHANIC_MOUNT: + case MECHANIC_INVULNERABILITY: + return false; + default: + break; + } + break; + } + case SPELL_AURA_ADD_FLAT_MODIFIER: // mods + case SPELL_AURA_ADD_PCT_MODIFIER: + { + switch (SpellModOp(spellInfo->Effects[effIndex].MiscValue)) + { + case SpellModOp::ChangeCastTime: // dependent from basepoint sign (positive -> negative) + case SpellModOp::Period: + case SpellModOp::PowerCostOnMiss: + case SpellModOp::StartCooldown: + if (bp > 0) + return false; + break; + case SpellModOp::Cooldown: + case SpellModOp::PowerCost0: + if (!spellInfo->IsPositive() && bp > 0) // dependent on prev effects too (ex Arcane Power) + return false; + break; + case SpellModOp::PointsIndex0: // always positive + case SpellModOp::PointsIndex1: + case SpellModOp::PointsIndex2: + case SpellModOp::Points: + case SpellModOp::Hate: + case SpellModOp::ChainAmplitude: + case SpellModOp::Amplitude: + return true; + case SpellModOp::Duration: + case SpellModOp::CritChance: + case SpellModOp::HealingAndDamage: + case SpellModOp::ChainTargets: + if (!spellInfo->IsPositive() && bp < 0) // dependent on prev effects too + return false; + break; + default: // dependent from basepoint sign (negative -> negative) + if (bp < 0) + return false; + break; + } + break; + } + default: + break; + } + } + // negative spell if triggered spell is negative + if (!spellInfo->Effects[effIndex].ApplyAuraName && spellInfo->Effects[effIndex].TriggerSpell) + { + if (SpellInfo const* spellTriggeredProto = sSpellMgr->GetSpellInfo(spellInfo->Effects[effIndex].TriggerSpell)) + { + // spells with at least one negative effect are considered negative + // some self-applied spells have negative effects but in self casting case negative check ignored. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + // already seen this + if (visited.count({ spellTriggeredProto->Id, i }) > 0) + continue; + + if (!spellTriggeredProto->Effects[i].IsEffect()) + continue; + + if (!_isPositiveEffectImpl(spellTriggeredProto, i, visited)) + return false; + } + } + } + + // ok, positive + return true; +} + +void SpellInfo::_InitializeSpellPositivity() +{ + std::unordered_set> visited; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + if (!_isPositiveEffectImpl(this, i, visited)) + AttributesCu |= (SPELL_ATTR0_CU_NEGATIVE_EFF0 << i); + + // additional checks after effects marked + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (!Effects[i].IsEffect() || !IsPositiveEffect(i)) + continue; + + switch (Effects[i].ApplyAuraName) + { + // has other non positive effect? + // then it should be marked negative if has same target as negative effect (ex 8510, 8511, 8893, 10267) + case SPELL_AURA_DUMMY: + case SPELL_AURA_MOD_STUN: + case SPELL_AURA_MOD_FEAR: + case SPELL_AURA_MOD_TAUNT: + case SPELL_AURA_TRANSFORM: + case SPELL_AURA_MOD_ATTACKSPEED: + case SPELL_AURA_MOD_DECREASE_SPEED: + { + for (uint8 j = i + 1; j < MAX_SPELL_EFFECTS; ++j) + if (!IsPositiveEffect(j) && Effects[i].TargetA.GetTarget() == Effects[j].TargetA.GetTarget() && Effects[i].TargetB.GetTarget() == Effects[j].TargetB.GetTarget()) + AttributesCu |= (SPELL_ATTR0_CU_NEGATIVE_EFF0 << i); + + break; + } + default: + break; + } + } +} + +SpellTargetRestrictionsEntry const* SpellInfo::GetSpellTargetRestrictions() const +{ + return SpellTargetRestrictionsId ? sSpellTargetRestrictionsStore.LookupEntry(SpellTargetRestrictionsId) : nullptr; +} + +SpellEquippedItemsEntry const* SpellInfo::GetSpellEquippedItems() const +{ + return SpellEquippedItemsId ? sSpellEquippedItemsStore.LookupEntry(SpellEquippedItemsId) : nullptr; +} + +SpellInterruptsEntry const* SpellInfo::GetSpellInterrupts() const +{ + return SpellInterruptsId ? sSpellInterruptsStore.LookupEntry(SpellInterruptsId) : nullptr; +} + +SpellLevelsEntry const* SpellInfo::GetSpellLevels() const +{ + return SpellLevelsId ? sSpellLevelsStore.LookupEntry(SpellLevelsId) : nullptr; +} + +SpellPowerEntry const* SpellInfo::GetSpellPower() const +{ + return SpellPowerId ? sSpellPowerStore.LookupEntry(SpellPowerId) : nullptr; +} + +SpellReagentsEntry const* SpellInfo::GetSpellReagents() const +{ + return SpellReagentsId ? sSpellReagentsStore.LookupEntry(SpellReagentsId) : nullptr; +} + +SpellScalingEntry const* SpellInfo::GetSpellScaling() const +{ + return SpellScalingId ? sSpellScalingStore.LookupEntry(SpellScalingId) : nullptr; +} + +SpellShapeshiftEntry const* SpellInfo::GetSpellShapeshift() const +{ + return SpellShapeshiftId ? sSpellShapeshiftStore.LookupEntry(SpellShapeshiftId) : nullptr; +} + +SpellTotemsEntry const* SpellInfo::GetSpellTotems() const +{ + return SpellTotemsId ? sSpellTotemsStore.LookupEntry(SpellTotemsId) : nullptr; +} + +SpellAuraOptionsEntry const* SpellInfo::GetSpellAuraOptions() const +{ + return SpellAuraOptionsId ? sSpellAuraOptionsStore.LookupEntry(SpellAuraOptionsId) : nullptr; +} + +SpellAuraRestrictionsEntry const* SpellInfo::GetSpellAuraRestrictions() const +{ + return SpellAuraRestrictionsId ? sSpellAuraRestrictionsStore.LookupEntry(SpellAuraRestrictionsId) : nullptr; +} + +SpellCastingRequirementsEntry const* SpellInfo::GetSpellCastingRequirements() const +{ + return SpellCastingRequirementsId ? sSpellCastingRequirementsStore.LookupEntry(SpellCastingRequirementsId) : nullptr; +} + +SpellCategoriesEntry const* SpellInfo::GetSpellCategories() const +{ + return SpellCategoriesId ? sSpellCategoriesStore.LookupEntry(SpellCategoriesId) : nullptr; +} + +SpellClassOptionsEntry const* SpellInfo::GetSpellClassOptions() const +{ + return SpellClassOptionsId ? sSpellClassOptionsStore.LookupEntry(SpellClassOptionsId) : nullptr; +} + +SpellCooldownsEntry const* SpellInfo::GetSpellCooldowns() const +{ + return SpellCooldownsId ? sSpellCooldownsStore.LookupEntry(SpellCooldownsId) : nullptr; +} + +void SpellInfo::_UnloadImplicitTargetConditionLists() +{ + // find the same instances of ConditionList and delete them. + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + ConditionContainer* cur = Effects[i].ImplicitTargetConditions; + if (!cur) + continue; + for (uint8 j = i; j < MAX_SPELL_EFFECTS; ++j) + { + if (Effects[j].ImplicitTargetConditions == cur) + Effects[j].ImplicitTargetConditions = nullptr; + } + delete cur; + } +} diff --git a/src/server/scripts/Custom/custom_script_loader.cpp.bak-viano-botcommands-20260930-185547 b/src/server/scripts/Custom/custom_script_loader.cpp.bak-viano-botcommands-20260930-185547 deleted file mode 100644 index e79a2ac228..0000000000 --- a/src/server/scripts/Custom/custom_script_loader.cpp.bak-viano-botcommands-20260930-185547 +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 . - */ - -// This is where scripts' loading functions should be declared: - - -// VIANO_ADVENTURERS -void AddSC_viano_adventurers(); - -// Viano Server diagnostics -void AddSC_vcore_diagnostics(); - -// The name of this function should match: -// void Add${NameOfDirectory}Scripts() -void AddCustomScripts() -{ - AddSC_viano_adventurers(); - AddSC_vcore_diagnostics(); - -} diff --git a/src/server/scripts/Custom/vcore_diagnostics.cpp.before_quest_selection b/src/server/scripts/Custom/vcore_diagnostics.cpp.before_quest_selection deleted file mode 100644 index a686421ced..0000000000 --- a/src/server/scripts/Custom/vcore_diagnostics.cpp.before_quest_selection +++ /dev/null @@ -1,2190 +0,0 @@ -/* - * Vcore Diagnostics Server v0.4 - * - * Target: The Cataclysm Preservation Project / Vcore API line. - * - * Provides: - * - short-lived per-player in-memory blackbox - * - persistent tester reproduction sessions - * - server snapshot attached to bug reports - * - * Deliberately does NOT patch gameplay. - */ - -#include "ScriptMgr.h" -#include "Chat.h" -#include "ChatCommand.h" -#include "DatabaseEnv.h" -#include "Group.h" -#include "Player.h" -#include "Spell.h" -#include "WorldSession.h" -#include "RBAC.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace VCoreDiagnostics -{ - using Clock = std::chrono::system_clock; - - static constexpr std::size_t BLACKBOX_MAX_EVENTS = 64; - static constexpr std::chrono::seconds BLACKBOX_MAX_AGE{120}; - - static constexpr std::size_t MAX_PLAYER_REPORT_TEXT_BYTES = 4096; - static constexpr std::size_t MAX_CLIENT_CONTEXT_BYTES = 16384; - static constexpr std::size_t MAX_TESTER_NOTE_BYTES = 4096; - static constexpr std::chrono::seconds PLAYER_REPORT_COOLDOWN{10}; - - static constexpr char ADDON_PROTOCOL_PREFIX[] = "VCORE1|CHUNK|"; - static constexpr std::size_t MAX_ADDON_TRANSPORT_BYTES = 24576; - static constexpr uint32 MAX_ADDON_TRANSPORT_CHUNKS = 128; - static constexpr std::chrono::seconds ADDON_TRANSPORT_TIMEOUT{30}; - - struct BlackboxEvent - { - Clock::time_point time; - std::string type; - std::string dataJson; - }; - - struct PendingAddonPayload - { - char operation = 0; - std::string requestId; - uint32 totalChunks = 0; - uint32 receivedChunks = 0; - std::size_t receivedBytes = 0; - Clock::time_point startedAt{}; - std::vector chunks; - std::vector received; - }; - - struct PlayerDiagnosticsState - { - std::deque blackbox; - std::optional activeReproductionId; - std::optional activeReportId; - std::optional lastPlayerReportAt; - std::optional pendingAddonPayload; - }; - - static std::unordered_map g_states; - - static uint64 PlayerKey(Player const* player) - { - return uint64(player->GetGUID().GetCounter()); - } - - static std::string JsonEscape(std::string const& input) - { - std::ostringstream out; - - for (char c : input) - { - switch (c) - { - case '\\': out << "\\\\"; break; - case '"': out << "\\\""; break; - case '\n': out << "\\n"; break; - case '\r': out << "\\r"; break; - case '\t': out << "\\t"; break; - default: - if (static_cast(c) < 0x20) - out << ' '; - else - out << c; - break; - } - } - - return out.str(); - } - - static int64 UnixMs(Clock::time_point tp) - { - return std::chrono::duration_cast( - tp.time_since_epoch()).count(); - } - - static bool IsPayloadSizeValid(std::string const& value, std::size_t maxBytes) - { - return value.size() <= maxBytes; - } - - static bool LooksLikeJsonObject(std::string const& value) - { - if (value.empty()) - return true; - - std::size_t first = value.find_first_not_of(" \t\r\n"); - std::size_t last = value.find_last_not_of(" \t\r\n"); - - if (first == std::string::npos || last == std::string::npos) - return true; - - return value[first] == '{' && value[last] == '}'; - } - - static bool CanCreatePlayerReport(Player* player) - { - if (!player) - return false; - - auto& state = g_states[PlayerKey(player)]; - - if (!state.lastPlayerReportAt) - return true; - - return Clock::now() - *state.lastPlayerReportAt >= PLAYER_REPORT_COOLDOWN; - } - - static void SendTransportStatus(Player* player, std::string const& text) - { - if (!player || !player->GetSession()) - return; - - ChatHandler(player->GetSession()).PSendSysMessage( - "[Vcore Diagnostics] %s", - text.c_str()); - } - - static bool ParseUnsigned(std::string const& value, uint32& out) - { - if (value.empty()) - return false; - - char* end = nullptr; - unsigned long parsed = std::strtoul(value.c_str(), &end, 10); - - if (!end || *end != '\0') - return false; - - if (parsed > 0xFFFFFFFFul) - return false; - - out = uint32(parsed); - return true; - } - - static bool ParseLengthPrefixedPair( - std::string const& payload, - std::string& first, - std::string& second) - { - std::size_t colon = payload.find(':'); - if (colon == std::string::npos) - return false; - - uint32 firstLength = 0; - if (!ParseUnsigned(payload.substr(0, colon), firstLength)) - return false; - - std::size_t dataStart = colon + 1; - if (firstLength > payload.size() - dataStart) - return false; - - first = payload.substr(dataStart, firstLength); - second = payload.substr(dataStart + firstLength); - return true; - } - - static std::string MakeToken(char const* prefix, Player const* player) - { - std::ostringstream out; - out << prefix << "-" - << player->GetGUID().GetCounter() << "-" - << UnixMs(Clock::now()); - return out.str(); - } - - static void Prune(PlayerDiagnosticsState& state) - { - auto const now = Clock::now(); - - while (!state.blackbox.empty() && - now - state.blackbox.front().time > BLACKBOX_MAX_AGE) - { - state.blackbox.pop_front(); - } - - while (state.blackbox.size() > BLACKBOX_MAX_EVENTS) - state.blackbox.pop_front(); - } - - static std::string TargetSnapshot(Player* player) - { - Unit* selected = player->GetSelectedUnit(); - - if (!selected) - return "null"; - - std::ostringstream out; - out << "{"; - out << "\"guid\":" << selected->GetGUID().GetCounter() << ","; - out << "\"type\":\"" << (selected->ToCreature() ? "creature" : "unit") << "\""; - - if (Creature* creature = selected->ToCreature()) - { - out << ",\"entry\":" << creature->GetEntry(); - out << ",\"spawn_id\":" << creature->GetSpawnId(); - } - - out << "}"; - return out.str(); - } - - static std::string ActiveQuestSnapshot(Player* player) - { - std::ostringstream out; - out << "["; - - bool first = true; - - for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) - { - uint32 questId = player->GetQuestSlotQuestId(slot); - if (!questId) - continue; - - if (!first) - out << ","; - - first = false; - - out << "{"; - out << "\"id\":" << questId << ","; - out << "\"status\":" << uint32(player->GetQuestStatus(questId)); - out << "}"; - } - - out << "]"; - return out.str(); - } - - static std::string BuildServerSnapshot(Player* player) - { - std::ostringstream out; - - out << "{"; - - out << "\"character\":{"; - out << "\"guid\":" << player->GetGUID().GetCounter() << ","; - out << "\"name\":\"" << JsonEscape(player->GetName()) << "\","; - out << "\"account_id\":" << player->GetSession()->GetAccountId() << ","; - out << "\"level\":" << uint32(player->getLevel()) << ","; - out << "\"class\":" << uint32(player->getClass()); - out << "},"; - - out << "\"location\":{"; - out << "\"map_id\":" << player->GetMapId() << ","; - out << "\"zone_id\":" << player->GetZoneId() << ","; - out << "\"area_id\":" << player->GetAreaId() << ","; - out << "\"x\":" << player->GetPositionX() << ","; - out << "\"y\":" << player->GetPositionY() << ","; - out << "\"z\":" << player->GetPositionZ() << ","; - out << "\"o\":" << player->GetOrientation(); - out << "},"; - - out << "\"target\":" << TargetSnapshot(player) << ","; - out << "\"active_quests\":" << ActiveQuestSnapshot(player) << ","; - - out << "\"state\":{"; - out << "\"alive\":" << (player->IsAlive() ? "true" : "false") << ","; - out << "\"in_combat\":" << (player->IsInCombat() ? "true" : "false") << ","; - out << "\"in_group\":" << (player->GetGroup() ? "true" : "false"); - out << "}"; - - out << "}"; - - return out.str(); - } - - static std::string BuildBlackboxJson(Player* player) - { - auto& state = g_states[PlayerKey(player)]; - Prune(state); - - std::ostringstream out; - out << "["; - - bool first = true; - auto const now = Clock::now(); - - for (BlackboxEvent const& event : state.blackbox) - { - if (!first) - out << ","; - - first = false; - - int64 offsetMs = - std::chrono::duration_cast( - event.time - now).count(); - - out << "{"; - out << "\"offset_ms\":" << offsetMs << ","; - out << "\"type\":\"" << JsonEscape(event.type) << "\","; - out << "\"data\":" << (event.dataJson.empty() ? "{}" : event.dataJson); - out << "}"; - } - - out << "]"; - return out.str(); - } - - static void ExecuteSync(std::string sql) - { - WorldDatabase.DirectExecute(sql.c_str()); - } - - static QueryResult QuerySync(std::string const& sql) - { - return WorldDatabase.Query(sql.c_str()); - } - - static std::string HexEncode(std::string const& input) - { - static char const* HEX = "0123456789ABCDEF"; - std::string out; - out.reserve(input.size() * 2); - - for (unsigned char c : input) - { - out.push_back(HEX[(c >> 4) & 0x0F]); - out.push_back(HEX[c & 0x0F]); - } - - return out; - } - - static void SendAddonResponse( - Player* player, - std::string const& requestId, - std::string const& payload) - { - if (!player || !player->GetSession()) - return; - - static constexpr std::size_t RESPONSE_CHUNK_SIZE = 180; - std::size_t total = (payload.size() + RESPONSE_CHUNK_SIZE - 1) / RESPONSE_CHUNK_SIZE; - if (total == 0) - total = 1; - - for (std::size_t i = 0; i < total; ++i) - { - std::size_t first = i * RESPONSE_CHUNK_SIZE; - std::string chunk = payload.substr(first, RESPONSE_CHUNK_SIZE); - - std::ostringstream body; - body << "VCORE2|RESP|" - << requestId << "|" - << (i + 1) << "|" - << total << "|" - << chunk; - - WorldPacket data; - ChatHandler::BuildChatPacket( - data, - CHAT_MSG_WHISPER, - LANG_ADDON, - player, - player, - body.str(), - 0U, - "", - DEFAULT_LOCALE, - "VCORE"); - - player->SendDirectMessage(&data); - } - } - - static void SendAddonError( - Player* player, - std::string const& requestId, - std::string const& code, - std::string const& message) - { - SendAddonResponse( - player, - requestId, - "ERROR|" + code + "|" + HexEncode(message)); - } - - static bool IsSeverityValid(std::string const& severity, bool allowUnknown) - { - return severity == "low" || - severity == "medium" || - severity == "high" || - severity == "critical" || - (allowUnknown && severity == "unknown"); - } - - enum class ReportCreateResult - { - Ok, - InvalidPlayer, - EmptyDescription, - DescriptionTooLarge, - ClientContextTooLarge, - InvalidClientContext, - RateLimited, - DatabaseFailure - }; - - struct ReportCreateResponse - { - ReportCreateResult result = ReportCreateResult::DatabaseFailure; - std::optional reportId; - }; - - static void InsertReproductionEvent( - uint64 reproductionId, - char const* source, - char const* eventType, - std::string dataJson) - { - std::string safeSource = source ? source : "server"; - std::string safeType = eventType ? eventType : "unknown"; - std::string safeData = std::move(dataJson); - - WorldDatabase.EscapeString(safeSource); - WorldDatabase.EscapeString(safeType); - WorldDatabase.EscapeString(safeData); - - std::ostringstream sql; - sql << "INSERT INTO vcore_dev.bug_reproduction_events " - << "(reproduction_id, source, event_type, data_json) VALUES (" - << reproductionId << ", '" - << safeSource << "', '" - << safeType << "', '" - << safeData << "')"; - - ExecuteSync(sql.str()); - } - - static void Record(Player* player, std::string type, std::string dataJson = "{}") - { - if (!player) - return; - - auto& state = g_states[PlayerKey(player)]; - - state.blackbox.push_back( - { Clock::now(), std::move(type), dataJson.empty() ? "{}" : std::move(dataJson) }); - - Prune(state); - - if (state.activeReproductionId) - { - BlackboxEvent const& event = state.blackbox.back(); - - InsertReproductionEvent( - *state.activeReproductionId, - "server", - event.type.c_str(), - event.dataJson); - } - } - - static std::optional ResolveReproductionIdByToken(std::string token) - { - WorldDatabase.EscapeString(token); - - std::ostringstream sql; - sql << "SELECT reproduction_id " - << "FROM vcore_dev.bug_reproductions " - << "WHERE session_token='" << token << "' LIMIT 1"; - - QueryResult result = QuerySync(sql.str()); - - if (!result) - return std::nullopt; - - return (*result)[0].GetUInt64(); - } - - static std::optional ResolveReportIdByToken(std::string token) - { - WorldDatabase.EscapeString(token); - - std::ostringstream sql; - sql << "SELECT report_id " - << "FROM vcore_dev.bug_reports " - << "WHERE report_token='" << token << "' LIMIT 1"; - - QueryResult result = QuerySync(sql.str()); - - if (!result) - return std::nullopt; - - return (*result)[0].GetUInt64(); - } - - static std::optional StartReproductionForReport( - Player* player, - uint64 reportId) - { - if (!player || !reportId) - return std::nullopt; - - auto& state = g_states[PlayerKey(player)]; - - if (state.activeReproductionId) - return state.activeReproductionId; - - std::ostringstream reportQuery; - reportQuery - << "SELECT COALESCE(bug_id,0), status " - << "FROM vcore_dev.bug_reports " - << "WHERE report_id=" << reportId << " LIMIT 1"; - - QueryResult reportResult = QuerySync(reportQuery.str()); - if (!reportResult) - return std::nullopt; - - Field* reportFields = reportResult->Fetch(); - uint64 bugId = reportFields[0].GetUInt64(); - std::string reportStatus = reportFields[1].GetString(); - - if (reportStatus == "closed") - return std::nullopt; - - std::string token = MakeToken("repro", player); - std::string safeToken = token; - std::string safeName = player->GetName(); - - WorldDatabase.EscapeString(safeToken); - WorldDatabase.EscapeString(safeName); - - std::ostringstream sql; - sql << "INSERT INTO vcore_dev.bug_reproductions " - << "(session_token, bug_id, report_id, tester_account_id, tester_character_guid, " - << "tester_character_name, result, map_id, zone_id, area_id, " - << "position_x, position_y, position_z, orientation, started_at, created_at) " - << "VALUES ('" - << safeToken << "', "; - - if (bugId) - sql << bugId; - else - sql << "NULL"; - - sql << ", " - << reportId << ", " - << player->GetSession()->GetAccountId() << ", " - << player->GetGUID().GetCounter() << ", '" - << safeName << "', 'running', " - << player->GetMapId() << ", " - << player->GetZoneId() << ", " - << player->GetAreaId() << ", " - << player->GetPositionX() << ", " - << player->GetPositionY() << ", " - << player->GetPositionZ() << ", " - << player->GetOrientation() - << ", NOW(), NOW())"; - - ExecuteSync(sql.str()); - - auto id = ResolveReproductionIdByToken(token); - if (!id) - return std::nullopt; - - state.activeReproductionId = *id; - state.activeReportId = reportId; - - std::ostringstream updateReport; - updateReport - << "UPDATE vcore_dev.bug_reports SET " - << "status=IF(status='open','investigating',status), " - << "updated_at=NOW() " - << "WHERE report_id=" << reportId; - ExecuteSync(updateReport.str()); - - InsertReproductionEvent( - *id, - "server", - "reproduction_started", - "{\"snapshot\":" + BuildServerSnapshot(player) + "}"); - - return id; - } - - static bool FinishReproduction( - Player* player, - std::string result, - std::string observation) - { - if (!player) - return false; - - auto& state = g_states[PlayerKey(player)]; - - if (!state.activeReproductionId) - return false; - - uint64 id = *state.activeReproductionId; - - InsertReproductionEvent( - id, - "server", - "reproduction_stopped", - "{\"snapshot\":" + BuildServerSnapshot(player) + "}"); - - std::string safeResult = result.empty() ? "unknown" : result; - std::string safeObservation = std::move(observation); - - WorldDatabase.EscapeString(safeResult); - WorldDatabase.EscapeString(safeObservation); - - std::ostringstream sql; - sql << "UPDATE vcore_dev.bug_reproductions SET " - << "result='" << safeResult << "', " - << "observation='" << safeObservation << "', " - << "finished_at=NOW() " - << "WHERE reproduction_id=" << id; - - ExecuteSync(sql.str()); - - state.activeReproductionId.reset(); - state.activeReportId.reset(); - - return true; - } - - static bool AddTesterNote(Player* player, std::string note) - { - if (!player || note.empty()) - return false; - - if (!IsPayloadSizeValid(note, MAX_TESTER_NOTE_BYTES)) - return false; - - auto& state = g_states[PlayerKey(player)]; - - if (!state.activeReproductionId) - return false; - - std::ostringstream data; - data << "{"; - data << "\"text\":\"" << JsonEscape(note) << "\","; - data << "\"snapshot\":" << BuildServerSnapshot(player); - data << "}"; - - InsertReproductionEvent( - *state.activeReproductionId, - "tester", - "tester_note", - data.str()); - - return true; - } - - static ReportCreateResponse CreateBugReport( - Player* player, - std::string reportedSeverity, - std::string description, - std::string clientContextJson = "{}") - { - ReportCreateResponse response; - - if (!player) - { - response.result = ReportCreateResult::InvalidPlayer; - return response; - } - - if (!IsSeverityValid(reportedSeverity, false)) - { - response.result = ReportCreateResult::DatabaseFailure; - return response; - } - - if (description.empty()) - { - response.result = ReportCreateResult::EmptyDescription; - return response; - } - - if (!IsPayloadSizeValid(description, MAX_PLAYER_REPORT_TEXT_BYTES)) - { - response.result = ReportCreateResult::DescriptionTooLarge; - return response; - } - - if (!IsPayloadSizeValid(clientContextJson, MAX_CLIENT_CONTEXT_BYTES)) - { - response.result = ReportCreateResult::ClientContextTooLarge; - return response; - } - - if (!LooksLikeJsonObject(clientContextJson)) - { - response.result = ReportCreateResult::InvalidClientContext; - return response; - } - - if (!CanCreatePlayerReport(player)) - { - response.result = ReportCreateResult::RateLimited; - return response; - } - - if (clientContextJson.empty()) - clientContextJson = "{}"; - - std::string token = MakeToken("report", player); - std::string safeToken = token; - std::string safeSeverity = std::move(reportedSeverity); - std::string safeDescription = std::move(description); - std::string safeName = player->GetName(); - - WorldDatabase.EscapeString(safeToken); - WorldDatabase.EscapeString(safeSeverity); - WorldDatabase.EscapeString(safeDescription); - WorldDatabase.EscapeString(safeName); - - Unit* target = player->GetSelectedUnit(); - - uint64 targetGuid = - target ? uint64(target->GetGUID().GetCounter()) : uint64(0); - - uint32 targetEntry = - target && target->ToCreature() - ? target->ToCreature()->GetEntry() - : uint32(0); - - std::ostringstream context; - context << "{"; - context << "\"client\":" << clientContextJson << ","; - context << "\"server\":" << BuildServerSnapshot(player) << ","; - context << "\"server_blackbox\":" << BuildBlackboxJson(player) << ","; - context << "\"meta\":{"; - context << "\"client_context_trusted\":false,"; - context << "\"server_context_trusted\":true"; - context << "}"; - context << "}"; - - std::string contextJson = context.str(); - WorldDatabase.EscapeString(contextJson); - - std::ostringstream sql; - sql << "INSERT INTO vcore_dev.bug_reports " - << "(report_token, bug_id, source_type, account_id, character_guid, " - << "character_name, reported_severity, status, description, map_id, zone_id, area_id, " - << "position_x, position_y, position_z, orientation, " - << "target_guid, target_entry, target_type, context_json, created_at) " - << "VALUES ('" - << safeToken << "', NULL, 'player', " - << player->GetSession()->GetAccountId() << ", " - << player->GetGUID().GetCounter() << ", '" - << safeName << "', '" - << safeSeverity << "', 'open', '" - << safeDescription << "', " - << player->GetMapId() << ", " - << player->GetZoneId() << ", " - << player->GetAreaId() << ", " - << player->GetPositionX() << ", " - << player->GetPositionY() << ", " - << player->GetPositionZ() << ", " - << player->GetOrientation() << ", "; - - if (targetGuid) - sql << targetGuid; - else - sql << "NULL"; - - sql << ", "; - - if (targetEntry) - sql << targetEntry; - else - sql << "NULL"; - - sql << ", "; - - if (target && target->ToCreature()) - sql << "'creature'"; - else if (target) - sql << "'unit'"; - else - sql << "NULL"; - - sql << ", '" << contextJson << "', NOW())"; - - ExecuteSync(sql.str()); - - auto reportId = ResolveReportIdByToken(token); - if (!reportId) - { - response.result = ReportCreateResult::DatabaseFailure; - return response; - } - - g_states[PlayerKey(player)].lastPlayerReportAt = Clock::now(); - - response.result = ReportCreateResult::Ok; - response.reportId = reportId; - return response; - } - - static ReportCreateResponse SubmitPlayerReport( - Player* player, - std::string reportedSeverity, - std::string description, - std::string clientContextJson) - { - return CreateBugReport( - player, - std::move(reportedSeverity), - std::move(description), - std::move(clientContextJson)); - } - - static std::optional StartTesterReproduction( - Player* player, - uint64 reportId) - { - return StartReproductionForReport(player, reportId); - } - - static bool AddTesterReproductionNote( - Player* player, - std::string note) - { - return AddTesterNote(player, std::move(note)); - } - - static bool FinishTesterReproduction( - Player* player, - std::string result, - std::string observation) - { - return FinishReproduction( - player, - std::move(result), - std::move(observation)); - } - - - static bool HasTesterPermission(Player* player); - - static std::string SeverityOrderSql(char const* column) - { - std::ostringstream sql; - sql << "CASE " << column - << " WHEN 'critical' THEN 1" - << " WHEN 'high' THEN 2" - << " WHEN 'medium' THEN 3" - << " WHEN 'low' THEN 4" - << " ELSE 5 END"; - return sql.str(); - } - - static bool SplitFirst( - std::string const& value, - char delimiter, - std::string& first, - std::string& rest) - { - std::size_t pos = value.find(delimiter); - if (pos == std::string::npos) - return false; - - first = value.substr(0, pos); - rest = value.substr(pos + 1); - return true; - } - - static void SendPermissions(Player* player, std::string const& requestId) - { - bool tester = HasTesterPermission(player); - SendAddonResponse( - player, - requestId, - std::string("PERM|") + (tester ? "1" : "0")); - - auto it = g_states.find(PlayerKey(player)); - if (it != g_states.end() && - it->second.activeReproductionId && - it->second.activeReportId) - { - std::ostringstream response; - response << "SESSION|" - << *it->second.activeReproductionId << "|" - << *it->second.activeReportId; - SendAddonResponse(player, requestId, response.str()); - } - else - { - SendAddonResponse(player, requestId, "SESSION|0|0"); - } - } - - static void SendReportList( - Player* player, - std::string const& requestId, - std::string sort, - uint32 page) - { - if (page == 0) - page = 1; - - if (sort != "newest" && sort != "area" && sort != "severity") - sort = "newest"; - - uint32 offset = (page - 1) * 8; - - std::ostringstream sql; - sql << "SELECT report_id, reported_severity, status, " - << "COALESCE(bug_id,0), COALESCE(zone_id,0), COALESCE(area_id,0), " - << "DATE_FORMAT(created_at,'%Y-%m-%d %H:%i'), " - << "COALESCE(character_name,''), " - << "LEFT(REPLACE(REPLACE(description,'\\r',' '),'\\n',' '),120), " - << "COALESCE(JSON_UNQUOTE(JSON_EXTRACT(context_json,'$.client.zone.zone_text')),'') " - << "FROM vcore_dev.bug_reports "; - - if (sort == "area") - sql << "ORDER BY COALESCE(zone_id,0), COALESCE(area_id,0), report_id DESC "; - else if (sort == "severity") - sql << "ORDER BY " << SeverityOrderSql("reported_severity") << ", report_id DESC "; - else - sql << "ORDER BY report_id DESC "; - - sql << "LIMIT 9 OFFSET " << offset; - - QueryResult result = QuerySync(sql.str()); - - std::ostringstream response; - response << "REPORT_LIST|" << page << "|" << (page > 1 ? 1 : 0) << "|"; - - bool hasNext = false; - std::vector rows; - - if (result) - { - uint32 rowIndex = 0; - do - { - Field* fields = result->Fetch(); - - if (rowIndex >= 8) - { - hasNext = true; - break; - } - - std::ostringstream row; - row << fields[0].GetUInt64() << "|" - << fields[1].GetString() << "|" - << fields[2].GetString() << "|" - << fields[3].GetUInt64() << "|" - << fields[4].GetUInt32() << "|" - << fields[5].GetUInt32() << "|" - << HexEncode(fields[6].GetString()) << "|" - << HexEncode(fields[7].GetString()) << "|" - << HexEncode(fields[8].GetString()) << "|" - << HexEncode(fields[9].GetString()); - - rows.push_back(row.str()); - ++rowIndex; - } while (result->NextRow()); - } - - response << (hasNext ? 1 : 0) << "|"; - for (std::size_t i = 0; i < rows.size(); ++i) - { - if (i) - response << "\n"; - response << rows[i]; - } - - SendAddonResponse(player, requestId, response.str()); - } - - static void SendReportDetail( - Player* player, - std::string const& requestId, - uint64 reportId) - { - std::ostringstream sql; - sql << "SELECT report_id, reported_severity, status, COALESCE(bug_id,0), " - << "COALESCE(zone_id,0), COALESCE(area_id,0), " - << "DATE_FORMAT(created_at,'%Y-%m-%d %H:%i:%s'), " - << "COALESCE(character_name,''), description, " - << "COALESCE(closure_reason,''), COALESCE(closure_note,''), " - << "COALESCE(JSON_UNQUOTE(JSON_EXTRACT(context_json,'$.client.zone.zone_text')),''), " - << "COALESCE(JSON_UNQUOTE(JSON_EXTRACT(context_json,'$.client.zone.subzone_text')),'') " - << "FROM vcore_dev.bug_reports WHERE report_id=" << reportId << " LIMIT 1"; - - QueryResult result = QuerySync(sql.str()); - if (!result) - { - SendAddonError(player, requestId, "REPORT_NOT_FOUND", "Report not found."); - return; - } - - Field* fields = result->Fetch(); - - std::ostringstream response; - response << "REPORT_DETAIL|" - << fields[0].GetUInt64() << "|" - << fields[1].GetString() << "|" - << fields[2].GetString() << "|" - << fields[3].GetUInt64() << "|" - << fields[4].GetUInt32() << "|" - << fields[5].GetUInt32() << "|" - << HexEncode(fields[6].GetString()) << "|" - << HexEncode(fields[7].GetString()) << "|" - << HexEncode(fields[8].GetString()) << "|" - << HexEncode(fields[9].GetString()) << "|" - << HexEncode(fields[10].GetString()) << "|" - << HexEncode(fields[11].GetString()) << "|" - << HexEncode(fields[12].GetString()); - - SendAddonResponse(player, requestId, response.str()); - - std::ostringstream reproSql; - reproSql - << "SELECT reproduction_id, result, " - << "DATE_FORMAT(COALESCE(finished_at,started_at),'%Y-%m-%d %H:%i:%s'), " - << "COALESCE(tester_character_name,''), COALESCE(observation,'') " - << "FROM vcore_dev.bug_reproductions " - << "WHERE report_id=" << reportId << " " - << "ORDER BY reproduction_id DESC LIMIT 12"; - - QueryResult reproResult = QuerySync(reproSql.str()); - std::ostringstream reproResponse; - reproResponse << "REPRO_LIST|" << reportId << "|"; - - bool first = true; - if (reproResult) - { - do - { - Field* r = reproResult->Fetch(); - if (!first) - reproResponse << "\n"; - first = false; - - reproResponse - << r[0].GetUInt64() << "|" - << r[1].GetString() << "|" - << HexEncode(r[2].GetString()) << "|" - << HexEncode(r[3].GetString()) << "|" - << HexEncode(r[4].GetString()); - } while (reproResult->NextRow()); - } - - SendAddonResponse(player, requestId, reproResponse.str()); - } - - static void SendBugList( - Player* player, - std::string const& requestId, - std::string sort, - uint32 page) - { - if (page == 0) - page = 1; - - if (sort != "newest" && sort != "area" && sort != "severity") - sort = "newest"; - - uint32 offset = (page - 1) * 8; - - std::ostringstream sql; - sql << "SELECT bug_id, title, status, severity, category, " - << "COALESCE(primary_zone_id,0), COALESCE(primary_area_id,0), " - << "DATE_FORMAT(updated_at,'%Y-%m-%d %H:%i') " - << "FROM vcore_dev.bugs "; - - if (sort == "area") - sql << "ORDER BY COALESCE(primary_zone_id,0), COALESCE(primary_area_id,0), bug_id DESC "; - else if (sort == "severity") - sql << "ORDER BY " << SeverityOrderSql("severity") << ", bug_id DESC "; - else - sql << "ORDER BY updated_at DESC, bug_id DESC "; - - sql << "LIMIT 9 OFFSET " << offset; - - QueryResult result = QuerySync(sql.str()); - - std::ostringstream response; - response << "BUG_LIST|" << page << "|" << (page > 1 ? 1 : 0) << "|"; - - bool hasNext = false; - std::vector rows; - - if (result) - { - uint32 rowIndex = 0; - do - { - Field* fields = result->Fetch(); - - if (rowIndex >= 8) - { - hasNext = true; - break; - } - - std::ostringstream row; - row << fields[0].GetUInt64() << "|" - << HexEncode(fields[1].GetString()) << "|" - << fields[2].GetString() << "|" - << fields[3].GetString() << "|" - << fields[4].GetString() << "|" - << fields[5].GetUInt32() << "|" - << fields[6].GetUInt32() << "|" - << HexEncode(fields[7].GetString()); - - rows.push_back(row.str()); - ++rowIndex; - } while (result->NextRow()); - } - - response << (hasNext ? 1 : 0) << "|"; - for (std::size_t i = 0; i < rows.size(); ++i) - { - if (i) - response << "\n"; - response << rows[i]; - } - - SendAddonResponse(player, requestId, response.str()); - } - - static void SendBugDetail( - Player* player, - std::string const& requestId, - uint64 bugId) - { - std::ostringstream sql; - sql << "SELECT bug_id, title, status, severity, category, " - << "COALESCE(description,''), COALESCE(primary_zone_id,0), " - << "COALESCE(primary_area_id,0), DATE_FORMAT(updated_at,'%Y-%m-%d %H:%i:%s'), " - << "(SELECT COUNT(*) FROM vcore_dev.bug_reports r WHERE r.bug_id=bugs.bug_id), " - << "(SELECT COUNT(*) FROM vcore_dev.bug_reproductions p WHERE p.bug_id=bugs.bug_id) " - << "FROM vcore_dev.bugs WHERE bug_id=" << bugId << " LIMIT 1"; - - QueryResult result = QuerySync(sql.str()); - if (!result) - { - SendAddonError(player, requestId, "BUG_NOT_FOUND", "Bug not found."); - return; - } - - Field* fields = result->Fetch(); - - std::ostringstream response; - response << "BUG_DETAIL|" - << fields[0].GetUInt64() << "|" - << HexEncode(fields[1].GetString()) << "|" - << fields[2].GetString() << "|" - << fields[3].GetString() << "|" - << fields[4].GetString() << "|" - << HexEncode(fields[5].GetString()) << "|" - << fields[6].GetUInt32() << "|" - << fields[7].GetUInt32() << "|" - << HexEncode(fields[8].GetString()) << "|" - << fields[9].GetUInt64() << "|" - << fields[10].GetUInt64(); - - SendAddonResponse(player, requestId, response.str()); - } - - static std::optional ResolveBugIdByToken(std::string token) - { - WorldDatabase.EscapeString(token); - std::ostringstream sql; - sql << "SELECT bug_id FROM vcore_dev.bugs " - << "WHERE bug_token='" << token << "' LIMIT 1"; - - QueryResult result = QuerySync(sql.str()); - if (!result) - return std::nullopt; - - return (*result)[0].GetUInt64(); - } - - static std::optional CreateBugFromReport( - Player* player, - uint64 reportId, - std::string severity, - std::string title) - { - if (!player || !reportId || !IsSeverityValid(severity, false) || title.empty()) - return std::nullopt; - - std::ostringstream sourceSql; - sourceSql - << "SELECT description, COALESCE(target_type,''), COALESCE(target_entry,0), " - << "COALESCE(map_id,0), COALESCE(zone_id,0), COALESCE(area_id,0), " - << "COALESCE(bug_id,0) " - << "FROM vcore_dev.bug_reports WHERE report_id=" << reportId << " LIMIT 1"; - - QueryResult sourceResult = QuerySync(sourceSql.str()); - if (!sourceResult) - return std::nullopt; - - Field* source = sourceResult->Fetch(); - if (source[6].GetUInt64()) - return source[6].GetUInt64(); - - std::string token = MakeToken("bug", player); - std::string safeToken = token; - std::string safeTitle = std::move(title); - std::string safeSeverity = std::move(severity); - std::string safeDescription = source[0].GetString(); - std::string safeEntityType = source[1].GetString(); - - WorldDatabase.EscapeString(safeToken); - WorldDatabase.EscapeString(safeTitle); - WorldDatabase.EscapeString(safeSeverity); - WorldDatabase.EscapeString(safeDescription); - WorldDatabase.EscapeString(safeEntityType); - - std::ostringstream insert; - insert - << "INSERT INTO vcore_dev.bugs " - << "(bug_token,title,status,severity,category,primary_entity_type,primary_entity_id," - << "description,primary_map_id,primary_zone_id,primary_area_id,created_at,updated_at) " - << "VALUES ('" << safeToken << "','" << safeTitle - << "','new','" << safeSeverity << "','unknown',"; - - if (!safeEntityType.empty()) - insert << "'" << safeEntityType << "'"; - else - insert << "NULL"; - - insert << ","; - - uint64 targetEntry = source[2].GetUInt64(); - if (targetEntry) - insert << targetEntry; - else - insert << "NULL"; - - insert - << ",'" << safeDescription << "'," - << source[3].GetUInt32() << "," - << source[4].GetUInt32() << "," - << source[5].GetUInt32() - << ",NOW(),NOW())"; - - ExecuteSync(insert.str()); - - auto bugId = ResolveBugIdByToken(token); - if (!bugId) - return std::nullopt; - - std::ostringstream link; - link - << "UPDATE vcore_dev.bug_reports SET bug_id=" << *bugId - << ", status='linked', updated_at=NOW() WHERE report_id=" << reportId; - ExecuteSync(link.str()); - - std::ostringstream linkRepros; - linkRepros - << "UPDATE vcore_dev.bug_reproductions SET bug_id=" << *bugId - << " WHERE report_id=" << reportId; - ExecuteSync(linkRepros.str()); - - return bugId; - } - - static bool LinkReportToBug(uint64 reportId, uint64 bugId) - { - std::ostringstream check; - check - << "SELECT bug_id FROM vcore_dev.bugs WHERE bug_id=" << bugId << " LIMIT 1"; - if (!QuerySync(check.str())) - return false; - - std::ostringstream update; - update - << "UPDATE vcore_dev.bug_reports SET bug_id=" << bugId - << ", status='linked', updated_at=NOW() WHERE report_id=" << reportId; - ExecuteSync(update.str()); - - std::ostringstream repro; - repro - << "UPDATE vcore_dev.bug_reproductions SET bug_id=" << bugId - << " WHERE report_id=" << reportId; - ExecuteSync(repro.str()); - - std::ostringstream touch; - touch << "UPDATE vcore_dev.bugs SET updated_at=NOW() WHERE bug_id=" << bugId; - ExecuteSync(touch.str()); - - return true; - } - - static bool CloseReport( - Player* player, - uint64 reportId, - std::string reason, - std::string note) - { - if (!player || !reportId) - return false; - - std::string safeReason = std::move(reason); - std::string safeNote = std::move(note); - WorldDatabase.EscapeString(safeReason); - WorldDatabase.EscapeString(safeNote); - - std::ostringstream sql; - sql - << "UPDATE vcore_dev.bug_reports SET status='closed', " - << "closed_at=NOW(), closed_by_account_id=" - << player->GetSession()->GetAccountId() - << ", closure_reason='" << safeReason - << "', closure_note='" << safeNote - << "', updated_at=NOW() WHERE report_id=" << reportId; - ExecuteSync(sql.str()); - return true; - } - - static bool ReopenReport(uint64 reportId) - { - std::ostringstream sql; - sql - << "UPDATE vcore_dev.bug_reports SET " - << "status=IF(bug_id IS NULL,'open','linked'), " - << "closed_at=NULL, closed_by_account_id=NULL, " - << "closure_reason=NULL, closure_note=NULL, updated_at=NOW() " - << "WHERE report_id=" << reportId; - ExecuteSync(sql.str()); - return true; - } - - static std::optional ResolveDiagnosticRunIdByToken(std::string token) - { - WorldDatabase.EscapeString(token); - - std::ostringstream sql; - sql << "SELECT diagnostic_run_id FROM vcore_dev.diagnostic_runs " - << "WHERE run_token='" << token << "' LIMIT 1"; - - QueryResult result = QuerySync(sql.str()); - if (!result) - return std::nullopt; - - return (*result)[0].GetUInt64(); - } - - static std::optional QueueDiagnosticRun(Player* player, uint64 bugId) - { - if (!player || !bugId) - return std::nullopt; - - std::ostringstream check; - check << "SELECT bug_id FROM vcore_dev.bugs WHERE bug_id=" << bugId << " LIMIT 1"; - if (!QuerySync(check.str())) - return std::nullopt; - - std::string token = MakeToken("diag", player); - std::string safeToken = token; - WorldDatabase.EscapeString(safeToken); - - std::ostringstream insert; - insert - << "INSERT INTO vcore_dev.diagnostic_runs " - << "(run_token,bug_id,requested_by_account_id,requested_by_character_guid,status,created_at) " - << "VALUES ('" << safeToken << "'," << bugId << "," - << player->GetSession()->GetAccountId() << "," - << player->GetGUID().GetCounter() - << ",'queued',NOW())"; - ExecuteSync(insert.str()); - - return ResolveDiagnosticRunIdByToken(token); - } - - - static bool HasTesterPermission(Player* player) - { - return player && - player->GetSession() && - player->GetSession()->HasPermission(rbac::RBAC_PERM_COMMAND_GM); - } - - static void DispatchAddonPayload( - Player* player, - char operation, - std::string const& requestId, - std::string const& payload) - { - if (!player) - return; - - // Ordinary players: permissions query and report creation only. - if (operation == 'P') - { - SendPermissions(player, requestId); - return; - } - - if (operation == 'R') - { - std::string severity; - std::string remainder; - if (!SplitFirst(payload, '|', severity, remainder)) - { - SendAddonError(player, requestId, "BAD_REPORT", "Invalid report payload."); - return; - } - - std::string description; - std::string clientContext; - - if (!ParseLengthPrefixedPair(remainder, description, clientContext)) - { - SendAddonError(player, requestId, "BAD_REPORT", "Invalid report payload."); - return; - } - - ReportCreateResponse response = - SubmitPlayerReport(player, severity, description, clientContext); - - if (response.result == ReportCreateResult::Ok && response.reportId) - { - SendAddonResponse( - player, - requestId, - "REPORT_OK|" + std::to_string(*response.reportId)); - } - else if (response.result == ReportCreateResult::RateLimited) - { - SendAddonError(player, requestId, "COOLDOWN", "Report cooldown active."); - } - else if (response.result == ReportCreateResult::DescriptionTooLarge || - response.result == ReportCreateResult::ClientContextTooLarge) - { - SendAddonError(player, requestId, "TOO_LARGE", "Report payload too large."); - } - else - { - SendAddonError(player, requestId, "REPORT_FAILED", "Report could not be saved."); - } - - return; - } - - if (!HasTesterPermission(player)) - { - SendAddonError( - player, - requestId, - "PERMISSION", - "Insufficient permission for tester/bug management."); - return; - } - - if (operation == 'L') - { - std::string sort; - std::string pageText; - if (!SplitFirst(payload, '|', sort, pageText)) - { - sort = "newest"; - pageText = "1"; - } - - uint32 page = 1; - ParseUnsigned(pageText, page); - SendReportList(player, requestId, sort, page); - return; - } - - if (operation == 'D') - { - uint32 reportId = 0; - if (!ParseUnsigned(payload, reportId) || !reportId) - { - SendAddonError(player, requestId, "BAD_REPORT_ID", "Invalid report ID."); - return; - } - - SendReportDetail(player, requestId, reportId); - return; - } - - if (operation == 'B') - { - std::string sort; - std::string pageText; - if (!SplitFirst(payload, '|', sort, pageText)) - { - sort = "newest"; - pageText = "1"; - } - - uint32 page = 1; - ParseUnsigned(pageText, page); - SendBugList(player, requestId, sort, page); - return; - } - - if (operation == 'G') - { - uint32 bugId = 0; - if (!ParseUnsigned(payload, bugId) || !bugId) - { - SendAddonError(player, requestId, "BAD_BUG_ID", "Invalid bug ID."); - return; - } - - SendBugDetail(player, requestId, bugId); - return; - } - - if (operation == 'S') - { - uint32 reportId = 0; - if (!ParseUnsigned(payload, reportId) || !reportId) - { - SendAddonError(player, requestId, "BAD_REPORT_ID", "Invalid report ID."); - return; - } - - auto reproductionId = - StartTesterReproduction(player, uint64(reportId)); - - if (!reproductionId) - { - SendAddonError( - player, - requestId, - "REPRO_START_FAILED", - "Could not start reproduction. The report may be closed or missing."); - return; - } - - std::ostringstream response; - response << "REPRO_STARTED|" << *reproductionId << "|" << reportId; - SendAddonResponse(player, requestId, response.str()); - return; - } - - if (operation == 'N') - { - if (!AddTesterReproductionNote(player, payload)) - { - SendAddonError( - player, - requestId, - "NOTE_FAILED", - "No active reproduction or invalid note."); - return; - } - - SendAddonResponse(player, requestId, "REPRO_NOTE_OK"); - return; - } - - if (operation == 'F') - { - std::string result; - std::string observation; - - if (!ParseLengthPrefixedPair(payload, result, observation)) - { - SendAddonError(player, requestId, "BAD_FINISH", "Invalid finish payload."); - return; - } - - if (result != "reproduced" && - result != "not_reproduced" && - result != "inconclusive") - { - SendAddonError(player, requestId, "BAD_RESULT", "Invalid reproduction result."); - return; - } - - auto it = g_states.find(PlayerKey(player)); - uint64 reproId = - it != g_states.end() && it->second.activeReproductionId - ? *it->second.activeReproductionId - : 0; - - if (!FinishTesterReproduction(player, result, observation)) - { - SendAddonError(player, requestId, "NO_ACTIVE_REPRO", "No active reproduction."); - return; - } - - SendAddonResponse( - player, - requestId, - "REPRO_FINISHED|" + std::to_string(reproId) + "|" + result); - return; - } - - if (operation == 'A') - { - auto it = g_states.find(PlayerKey(player)); - uint64 reproId = - it != g_states.end() && it->second.activeReproductionId - ? *it->second.activeReproductionId - : 0; - - std::string reason = payload.empty() - ? std::string("aborted by tester") - : payload; - - if (!FinishTesterReproduction(player, "aborted", reason)) - { - SendAddonError(player, requestId, "NO_ACTIVE_REPRO", "No active reproduction."); - return; - } - - SendAddonResponse( - player, - requestId, - "REPRO_FINISHED|" + std::to_string(reproId) + "|aborted"); - return; - } - - if (operation == 'C') - { - std::string reportText; - std::string rest; - if (!SplitFirst(payload, '|', reportText, rest)) - { - SendAddonError(player, requestId, "BAD_CREATE_BUG", "Invalid create-bug payload."); - return; - } - - std::string severity; - std::string title; - if (!SplitFirst(rest, '|', severity, title)) - { - SendAddonError(player, requestId, "BAD_CREATE_BUG", "Invalid create-bug payload."); - return; - } - - uint32 reportId = 0; - if (!ParseUnsigned(reportText, reportId) || !reportId) - { - SendAddonError(player, requestId, "BAD_REPORT_ID", "Invalid report ID."); - return; - } - - auto bugId = CreateBugFromReport(player, reportId, severity, title); - if (!bugId) - { - SendAddonError(player, requestId, "BUG_CREATE_FAILED", "Could not create bug."); - return; - } - - std::ostringstream response; - response << "BUG_CREATED|" << *bugId << "|" << reportId; - SendAddonResponse(player, requestId, response.str()); - return; - } - - if (operation == 'K') - { - std::string reportText; - std::string bugText; - if (!SplitFirst(payload, '|', reportText, bugText)) - { - SendAddonError(player, requestId, "BAD_LINK", "Invalid link payload."); - return; - } - - uint32 reportId = 0; - uint32 bugId = 0; - if (!ParseUnsigned(reportText, reportId) || - !ParseUnsigned(bugText, bugId) || - !reportId || !bugId || - !LinkReportToBug(reportId, bugId)) - { - SendAddonError(player, requestId, "LINK_FAILED", "Could not link report to bug."); - return; - } - - std::ostringstream response; - response << "REPORT_LINKED|" << reportId << "|" << bugId; - SendAddonResponse(player, requestId, response.str()); - return; - } - - if (operation == 'X') - { - std::string reportText; - std::string rest; - if (!SplitFirst(payload, '|', reportText, rest)) - { - SendAddonError(player, requestId, "BAD_CLOSE", "Invalid close payload."); - return; - } - - std::string reason; - std::string note; - if (!SplitFirst(rest, '|', reason, note)) - { - reason = rest; - note.clear(); - } - - uint32 reportId = 0; - if (!ParseUnsigned(reportText, reportId) || - !reportId || - !CloseReport(player, reportId, reason, note)) - { - SendAddonError(player, requestId, "CLOSE_FAILED", "Could not close report."); - return; - } - - SendAddonResponse( - player, - requestId, - "REPORT_CLOSED|" + std::to_string(reportId)); - return; - } - - if (operation == 'O') - { - uint32 reportId = 0; - if (!ParseUnsigned(payload, reportId) || !reportId || !ReopenReport(reportId)) - { - SendAddonError(player, requestId, "REOPEN_FAILED", "Could not reopen report."); - return; - } - - SendAddonResponse( - player, - requestId, - "REPORT_REOPENED|" + std::to_string(reportId)); - return; - } - - if (operation == 'Q') - { - uint32 bugId = 0; - if (!ParseUnsigned(payload, bugId) || !bugId) - { - SendAddonError(player, requestId, "BAD_BUG_ID", "Invalid bug ID."); - return; - } - - auto runId = QueueDiagnosticRun(player, bugId); - if (!runId) - { - SendAddonError(player, requestId, "DIAG_FAILED", "Could not queue diagnostic run."); - return; - } - - std::ostringstream response; - response << "DIAG_QUEUED|" << *runId << "|" << bugId; - SendAddonResponse(player, requestId, response.str()); - return; - } - - SendAddonError(player, requestId, "UNKNOWN_OPERATION", "Unknown AddOn operation."); - } - - static void HandleAddonTransport(Player* player, std::string const& message) - { - if (!player) - return; - - if (message.rfind(ADDON_PROTOCOL_PREFIX, 0) != 0) - return; - - std::string body = message.substr( - std::char_traits::length(ADDON_PROTOCOL_PREFIX)); - - // VCORE1|CHUNK||||| - std::size_t p1 = body.find('|'); - std::size_t p2 = p1 == std::string::npos ? std::string::npos : body.find('|', p1 + 1); - std::size_t p3 = p2 == std::string::npos ? std::string::npos : body.find('|', p2 + 1); - std::size_t p4 = p3 == std::string::npos ? std::string::npos : body.find('|', p3 + 1); - - if (p1 == std::string::npos || - p2 == std::string::npos || - p3 == std::string::npos || - p4 == std::string::npos) - { - SendTransportStatus(player, "Malformed AddOn message."); - return; - } - - std::string opText = body.substr(0, p1); - std::string requestId = body.substr(p1 + 1, p2 - p1 - 1); - std::string indexText = body.substr(p2 + 1, p3 - p2 - 1); - std::string totalText = body.substr(p3 + 1, p4 - p3 - 1); - std::string chunk = body.substr(p4 + 1); - - if (opText.size() != 1 || requestId.empty() || requestId.size() > 32) - { - SendTransportStatus(player, "Malformed AddOn message header."); - return; - } - - uint32 index = 0; - uint32 total = 0; - - if (!ParseUnsigned(indexText, index) || - !ParseUnsigned(totalText, total) || - index == 0 || - total == 0 || - index > total || - total > MAX_ADDON_TRANSPORT_CHUNKS) - { - SendTransportStatus(player, "Invalid AddOn chunk metadata."); - return; - } - - auto& state = g_states[PlayerKey(player)]; - auto const now = Clock::now(); - - if (state.pendingAddonPayload && - now - state.pendingAddonPayload->startedAt > ADDON_TRANSPORT_TIMEOUT) - { - state.pendingAddonPayload.reset(); - } - - char operation = opText[0]; - - if (!state.pendingAddonPayload || - state.pendingAddonPayload->requestId != requestId || - state.pendingAddonPayload->operation != operation || - state.pendingAddonPayload->totalChunks != total) - { - PendingAddonPayload pending; - pending.operation = operation; - pending.requestId = requestId; - pending.totalChunks = total; - pending.startedAt = now; - pending.chunks.resize(total); - pending.received.resize(total, false); - state.pendingAddonPayload = std::move(pending); - } - - PendingAddonPayload& pending = *state.pendingAddonPayload; - std::size_t slot = std::size_t(index - 1); - - if (!pending.received[slot]) - { - if (pending.receivedBytes + chunk.size() > MAX_ADDON_TRANSPORT_BYTES) - { - state.pendingAddonPayload.reset(); - SendTransportStatus(player, "AddOn payload too large."); - return; - } - - pending.chunks[slot] = std::move(chunk); - pending.received[slot] = true; - pending.receivedBytes += pending.chunks[slot].size(); - ++pending.receivedChunks; - } - - if (pending.receivedChunks != pending.totalChunks) - return; - - std::string payload; - payload.reserve(pending.receivedBytes); - - for (std::string const& part : pending.chunks) - payload += part; - - char completedOperation = pending.operation; - std::string completedRequestId = pending.requestId; - state.pendingAddonPayload.reset(); - - DispatchAddonPayload(player, completedOperation, completedRequestId, payload); - } - - class VCoreDiagnosticsPlayerScript : public PlayerScript - { - public: - VCoreDiagnosticsPlayerScript() - : PlayerScript("VCoreDiagnosticsPlayerScript") - { - } - - void OnLogin(Player* player, bool /*firstLogin*/) override - { - g_states.try_emplace(PlayerKey(player)); - Record(player, "login"); - } - - void OnLogout(Player* player) override - { - if (!player) - return; - - auto it = g_states.find(PlayerKey(player)); - - if (it != g_states.end() && it->second.activeReproductionId) - { - FinishReproduction( - player, - "aborted", - "Player logged out while reproduction was active."); - } - - g_states.erase(PlayerKey(player)); - } - - void OnChat( - Player* player, - uint32 type, - uint32 lang, - std::string& msg, - Player* receiver) override - { - // The existing Vcore AddOn whisper path ends up in this PlayerScript - // hook. Restrict diagnostics transport to self-whispers only. - if (!player || - receiver != player || - type != CHAT_MSG_WHISPER || - lang != uint32(LANG_ADDON)) - { - return; - } - - HandleAddonTransport(player, msg); - } - - void OnUpdateZone(Player* player, uint32 newZone, uint32 newArea) override - { - std::ostringstream data; - data << "{\"zone_id\":" << newZone - << ",\"area_id\":" << newArea << "}"; - - Record(player, "zone_changed", data.str()); - } - - void OnMapChanged(Player* player) override - { - std::ostringstream data; - data << "{\"map_id\":" << player->GetMapId() << "}"; - - Record(player, "map_changed", data.str()); - } - - void OnQuestStatusChange(Player* player, uint32 questId) override - { - std::ostringstream data; - data << "{\"quest_id\":" << questId - << ",\"status\":" << uint32(player->GetQuestStatus(questId)) - << "}"; - - Record(player, "quest_status_changed", data.str()); - } - - void OnSpellCast(Player* player, Spell* spell, bool /*skipCheck*/) override - { - if (!spell || !spell->GetSpellInfo()) - return; - - std::ostringstream data; - data << "{\"spell_id\":" << spell->GetSpellInfo()->Id << "}"; - - Record(player, "spell_cast", data.str()); - } - - void OnCreatureKill(Player* killer, Creature* killed) override - { - if (!killer || !killed) - return; - - std::ostringstream data; - data << "{\"entry\":" << killed->GetEntry() - << ",\"guid\":" << killed->GetGUID().GetCounter() - << "}"; - - Record(killer, "creature_kill", data.str()); - } - - void OnPlayerRepop(Player* player) override - { - Record(player, "player_repop"); - } - }; - - class VCoreDiagnosticsCommandScript : public CommandScript - { - public: - VCoreDiagnosticsCommandScript() - : CommandScript("VCoreDiagnosticsCommandScript") - { - } - - std::vector GetCommands() const override - { - static std::vector reproductionCommands = - { - { "start", rbac::RBAC_PERM_COMMAND_GM, false, &HandleReproStart, "" }, - { "stop", rbac::RBAC_PERM_COMMAND_GM, false, &HandleReproStop, "" }, - { "abort", rbac::RBAC_PERM_COMMAND_GM, false, &HandleReproAbort, "" }, - { "note", rbac::RBAC_PERM_COMMAND_GM, false, &HandleReproNote, "" }, - { "status", rbac::RBAC_PERM_COMMAND_GM, false, &HandleReproStatus, "" }, - }; - - static std::vector diagnosticsCommands = - { - { "repro", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", reproductionCommands }, - { "report", rbac::RBAC_PERM_COMMAND_GM, false, &HandleReport, "" }, - }; - - static std::vector commandTable = - { - { "vdiag", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", diagnosticsCommands }, - }; - - return commandTable; - } - - static bool HandleReproStart(ChatHandler* handler, char const* args) - { - if (!args || !*args) - return false; - - char* end = nullptr; - unsigned long long parsed = std::strtoull(args, &end, 10); - - if (end == args || parsed == 0) - return false; - - Player* player = handler->GetSession()->GetPlayer(); - - auto id = StartReproductionForReport(player, uint64(parsed)); - - if (!id) - { - handler->SendSysMessage("Could not start Vcore reproduction."); - return false; - } - - handler->PSendSysMessage( - "Vcore reproduction started from report: REPRO-%llu", - static_cast(*id)); - - return true; - } - - static bool HandleReproStop(ChatHandler* handler, char const* args) - { - Player* player = handler->GetSession()->GetPlayer(); - std::string observation = args ? args : ""; - - if (!FinishReproduction(player, "reproduced", observation)) - { - handler->SendSysMessage("No active Vcore reproduction."); - return false; - } - - handler->SendSysMessage("Vcore reproduction stopped."); - return true; - } - - static bool HandleReproAbort(ChatHandler* handler, char const* args) - { - Player* player = handler->GetSession()->GetPlayer(); - - std::string reason = - (args && *args) - ? std::string(args) - : std::string("aborted by tester"); - - if (!FinishReproduction(player, "aborted", reason)) - { - handler->SendSysMessage("No active Vcore reproduction."); - return false; - } - - handler->SendSysMessage("Vcore reproduction aborted."); - return true; - } - - static bool HandleReproNote(ChatHandler* handler, char const* args) - { - if (!args || !*args) - return false; - - Player* player = handler->GetSession()->GetPlayer(); - - if (!AddTesterNote(player, args)) - { - handler->SendSysMessage("No active Vcore reproduction."); - return false; - } - - handler->SendSysMessage("Vcore tester note stored."); - return true; - } - - static bool HandleReproStatus(ChatHandler* handler, char const* /*args*/) - { - Player* player = handler->GetSession()->GetPlayer(); - - auto it = g_states.find(PlayerKey(player)); - - if (it == g_states.end() || !it->second.activeReproductionId) - { - handler->SendSysMessage("No active Vcore reproduction."); - return true; - } - - handler->PSendSysMessage( - "Active Vcore reproduction: REPRO-%llu", - static_cast(*it->second.activeReproductionId)); - - return true; - } - - static bool HandleReport(ChatHandler* handler, char const* args) - { - if (!args || !*args) - return false; - - Player* player = handler->GetSession()->GetPlayer(); - - ReportCreateResponse response = SubmitPlayerReport(player, "medium", args, "{}"); - - if (response.result != ReportCreateResult::Ok || !response.reportId) - { - switch (response.result) - { - case ReportCreateResult::RateLimited: - handler->SendSysMessage("Vcore report rejected: report cooldown active."); - break; - case ReportCreateResult::DescriptionTooLarge: - handler->SendSysMessage("Vcore report rejected: description too large."); - break; - default: - handler->SendSysMessage("Could not create Vcore bug report."); - break; - } - - return false; - } - - handler->PSendSysMessage( - "Vcore bug report created: REPORT-%llu", - static_cast(*response.reportId)); - - return true; - } - }; -} - -void AddSC_vcore_diagnostics() -{ - new VCoreDiagnostics::VCoreDiagnosticsPlayerScript(); - new VCoreDiagnostics::VCoreDiagnosticsCommandScript(); -} diff --git a/src/server/scripts/Custom/viano_adventurers.cpp b/src/server/scripts/Custom/viano_adventurers.cpp index 6acb067ae1..cbdfdf10f1 100644 --- a/src/server/scripts/Custom/viano_adventurers.cpp +++ b/src/server/scripts/Custom/viano_adventurers.cpp @@ -11,6 +11,13 @@ #include "TemporarySummon.h" #include "MotionMaster.h" #include "Log.h" +#include "SpellInfo.h" +#include "SpellAuras.h" +#include +#include +#include "LFGMgr.h" +#include "LFG.h" +#include "ObjectMgr.h" #include #include @@ -22,6 +29,102 @@ namespace VianoAdventurers static constexpr uint32 UPDATE_INTERVAL = 500; static constexpr uint32 DIAGNOSTIC_INTERVAL = 2000; + + // VIANO_FORMATION_V1 + static bool IsVianoTankUnit(Unit* unit) + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (!creature || !creature->IsVianoAdventurer()) + 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_tank", 0) == 0 + || scriptName.rfind("npc_alliance_adventurer_tank", 0) == 0; + } + + static void GetVianoFollowSlot(Creature* bot, Player* owner, float& distance, float& angle) + { + distance = FOLLOW_DISTANCE; + angle = FOLLOW_ANGLE; + + if (!bot || !owner) + return; + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + std::vector bots; + for (Unit* unit : groupUnits) + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (!creature || !creature->IsVianoAdventurer()) + continue; + if (!creature->IsAlive() || !creature->IsInMap(owner)) + continue; + + bots.push_back(creature); + } + + std::sort(bots.begin(), bots.end(), [](Creature const* left, Creature const* right) + { + return left->GetGUID().GetRawValue() < right->GetGUID().GetRawValue(); + }); + + size_t slot = 0; + bool found = false; + for (size_t i = 0; i < bots.size(); ++i) + { + if (bots[i] == bot) + { + slot = i; + found = true; + break; + } + } + + if (!found) + return; + + struct FollowSlot + { + float distance; + float angle; + }; + + static FollowSlot const baseSlots[] = + { + { 2.7f, 3.14159265f }, + { 3.1f, 2.52f }, + { 3.1f, 3.76f }, + { 4.0f, 2.25f }, + { 4.0f, 4.03f }, + { 4.4f, 2.70f }, + { 4.4f, 3.58f }, + { 4.8f, 3.14159265f } + }; + + if (slot < sizeof(baseSlots) / sizeof(baseSlots[0])) + { + distance = baseSlots[slot].distance; + angle = baseSlots[slot].angle; + return; + } + + // Unlimited development-mode fallback: additional bots form wider rows + // instead of being stacked on one point. + static float const offsets[] = { -0.95f, -0.48f, 0.0f, 0.48f, 0.95f }; + size_t extra = slot - (sizeof(baseSlots) / sizeof(baseSlots[0])); + size_t row = extra / 5; + size_t column = extra % 5; + + distance = 5.4f + float(row) * 1.25f; + angle = 3.14159265f + offsets[column]; + } + // VIANO_DD_PROFILE_V1_5 enum AdventurerSpells : uint32 { @@ -66,7 +169,10 @@ namespace VianoAdventurers if (!owner) return; - me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE); + float followDistance = FOLLOW_DISTANCE; + float followAngle = FOLLOW_ANGLE; + GetVianoFollowSlot(me, owner, followDistance, followAngle); + me->GetMotionMaster()->MoveFollow(owner, followDistance, followAngle); } void ResetCombatEvents() @@ -373,39 +479,156 @@ namespace VianoAdventurers return nullptr; } - Unit* SelectAssistTarget(Player* owner) + 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()) + auto isCombatRelevant = [](Unit* target) -> bool { - if (Unit* target = owner->GetSelectedUnit()) - if (IsValidCandidate(target)) + return target + && target->IsAlive() + && (target->IsInCombat() + || target->GetVictim() + || !target->getAttackers().empty()); + }; + + auto isDeclaredTank = [owner](Unit* unit) -> bool + { + if (!unit) + return false; + + if (Creature* creature = unit->ToCreature()) + { + if (CreatureTemplate const* creatureTemplate = creature->GetCreatureTemplate()) + { + std::string const scriptName = sObjectMgr->GetScriptName(creatureTemplate->ScriptID); + if (scriptName.find("_tank") != std::string::npos) + return true; + } + } + + if (Player* player = unit->ToPlayer()) + { + if (Group* group = owner->GetGroup()) + { + if (group->isLFGGroup() && player->GetGroup() == group) + { + uint8 roles = sLFGMgr->GetRoles(player->GetGUID()); + if (roles & lfg::PLAYER_ROLE_TANK) + return true; + } + } + } + + return false; + }; + + // VIANO_DD_TARGET_V1_1: + // If a declared tank is actively fighting, DDs follow the tank's + // victim. Merely selecting another hostile target must not pull it. + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + Unit* bestTankTarget = nullptr; + uint32 bestTankPressure = 0; + bool declaredTankPresent = false; + + auto considerTank = [&](Unit* tank) + { + if (!tank || !tank->IsAlive() || !tank->IsInMap(owner) || !isDeclaredTank(tank)) + return; + + declaredTankPresent = true; + + Unit* victim = tank->GetVictim(); + if (!victim || !IsValidCandidate(victim) || !isCombatRelevant(victim)) + return; + + // VIANO_DD_TANK_ENGAGEMENT_GATE_V1: + // AttackStart() gives the tank a victim immediately during a + // skull pull. DDs must not interpret that as "tank has taken + // the target" yet. Wait until the enemy is actually attacking + // the tank / appears in the tank's attacker set. + bool tankHasAggro = victim->GetVictim() == tank; + if (!tankHasAggro) + { + for (Unit* attacker : tank->getAttackers()) + { + if (attacker == victim) + { + tankHasAggro = true; + break; + } + } + } + + if (!tankHasAggro) + return; + + uint32 pressure = uint32(tank->getAttackers().size()); + if (!bestTankTarget || pressure > bestTankPressure) + { + bestTankTarget = victim; + bestTankPressure = pressure; + } + }; + + considerTank(owner); + for (Unit* member : groupUnits) + considerTank(member); + + if (bestTankTarget) + return bestTankTarget; + + // If a tank exists but has not actually established the pull yet, + // DDs wait. In particular they must NOT fall through to the + // player's selected skull target merely because the tank just put + // that target into combat. + if (declaredTankPresent) + { + if (Unit* target = me->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(target)) return target; + + if (Unit* target = SelectGroupCombatTarget(owner)) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + for (Unit* attacker : me->getAttackers()) + if (IsValidCandidate(attacker) && isCombatRelevant(attacker)) + return attacker; + + return nullptr; } - // 2. Keep the bot's own valid current victim. - if (Unit* target = me->GetVictim()) - if (IsValidCandidate(target)) + // No declared tank: the owner controls DD focus, but only after + // the target is already genuinely combat-relevant. + if (Unit* target = owner->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(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; + if (Unit* target = owner->GetSelectedUnit()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + // Keep a valid existing bot victim. + if (Unit* target = me->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + // Help the virtual/real group against enemies that are already + // attacking group members. + if (Unit* target = SelectGroupCombatTarget(owner)) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + for (Unit* attacker : owner->getAttackers()) + if (IsValidCandidate(attacker) && isCombatRelevant(attacker)) + return attacker; - // 4. Finally, defend the bot itself if something is attacking it. for (Unit* attacker : me->getAttackers()) - if (IsValidCandidate(attacker)) + if (IsValidCandidate(attacker) && isCombatRelevant(attacker)) return attacker; return nullptr; @@ -570,7 +793,10 @@ namespace VianoAdventurers if (!owner) return; - me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE); + float followDistance = FOLLOW_DISTANCE; + float followAngle = FOLLOW_ANGLE; + GetVianoFollowSlot(me, owner, followDistance, followAngle); + me->GetMotionMaster()->MoveFollow(owner, followDistance, followAngle); } void HandleOutOfCombatRecovery(uint32 diff) @@ -617,13 +843,14 @@ namespace VianoAdventurers if (!owner) return nullptr; - Group* group = owner->GetGroup(); - if (!group) - return nullptr; + ObjectGuid guid; - ObjectGuid guid = group->GetTargetIcon(SKULL_ICON_ID); - if (!guid) - return nullptr; + // A real Trinity Group is authoritative when it exists. + if (Group* group = owner->GetGroup()) guid = group->GetTargetIcon(SKULL_ICON_ID); + + if (!guid) guid = owner->GetVianoTargetIcon(SKULL_ICON_ID); + + if (!guid) return nullptr; Unit* target = ObjectAccessor::GetUnit(*me, guid); return IsValidTankTarget(target) ? target : nullptr; @@ -690,15 +917,72 @@ namespace VianoAdventurers && me->IsWithinLOSInMap(enemy); } + // VIANO_MULTI_TANK_ARBITRATION_V1 + bool IsPreferredTankFor(Player* owner, Unit* enemy) const + { + if (!owner || !enemy) + return false; + + // Never steal an enemy that is already actively tanked by another + // living Viano tank. Normal damage threat still works; only the + // artificial recovery threat is suppressed. + if (Unit* victim = enemy->GetVictim()) + { + if (victim == me) + return false; + + if (victim->IsAlive() && IsVianoTankUnit(victim)) + return false; + } + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + Creature* preferred = nullptr; + float preferredDistance = 0.0f; + + for (Unit* unit : groupUnits) + { + Creature* candidate = unit ? unit->ToCreature() : nullptr; + if (!candidate || !candidate->IsAlive() || !candidate->IsInMap(owner)) + continue; + if (!IsVianoTankUnit(candidate)) + continue; + if (!candidate->IsValidAttackTarget(enemy)) + continue; + if (candidate->GetDistance(enemy) > AUTO_PICKUP_RADIUS) + continue; + if (!candidate->IsWithinLOSInMap(enemy)) + continue; + + float candidateDistance = candidate->GetDistance(enemy); + if (!preferred + || candidateDistance < preferredDistance + || (candidateDistance == preferredDistance + && candidate->GetGUID().GetRawValue() < preferred->GetGUID().GetRawValue())) + { + preferred = candidate; + preferredDistance = candidateDistance; + } + } + + return preferred == me; + } + void ForceTankThreat(Unit* enemy) { if (!IsSafeAutoPickup(enemy)) return; + Player* owner = GetOwnerPlayer(); + if (!owner || !IsPreferredTankFor(owner, 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. + // Recovery only: reach the current highest threat and exceed it. + // Once this tank actually owns the victim, IsPreferredTankFor() + // stops further artificial inflation until aggro is lost again. threat.MatchUnitThreatToHighestThreat(me); float current = threat.GetThreat(me, true); @@ -821,6 +1105,7 @@ namespace VianoAdventurers }; me->SetDisplayId(HORDE_ADVENTURER_MODELS[urand(0, 3)], true); + SetEquipmentSlots(false, 50179, 50794, EQUIP_NO_CHANGE); me->SetReactState(REACT_DEFENSIVE); FollowOwner(owner); @@ -949,6 +1234,446 @@ namespace VianoAdventurers } }; + + // VIANO_HEALER_V1 + class npc_horde_adventurer_healer : public CreatureScript + { + public: + npc_horde_adventurer_healer() : CreatureScript("npc_horde_adventurer_healer") { } + + struct npc_horde_adventurer_healerAI : public ScriptedAI + { + npc_horde_adventurer_healerAI(Creature* creature) : ScriptedAI(creature) { } + + static constexpr uint32 SPELL_HEAL_EFFICIENT = 635; + static constexpr uint32 SPELL_HEAL_STRONG = 82326; + static constexpr uint32 SPELL_HEAL_EMERGENCY = 19750; + static constexpr float HEAL_RANGE = 40.0f; + + ObjectGuid ownerGuid; + uint32 updateTimer = 0; + uint32 recoveryTimer = 1000; + uint32 decisionLockTimer = 0; + + Player* GetOwnerPlayer() const + { + return ownerGuid ? ObjectAccessor::GetPlayer(*me, ownerGuid) : nullptr; + } + + static std::string GetScriptName(Creature const* creature) + { + if (!creature) + return {}; + if (CreatureTemplate const* ct = creature->GetCreatureTemplate()) + return sObjectMgr->GetScriptName(ct->ScriptID); + return {}; + } + + bool IsVianoTank(Unit* unit) const + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + return creature && GetScriptName(creature).find("_tank") != std::string::npos; + } + + bool IsDeclaredPlayerTank(Unit* unit, Player* owner) const + { + Player* player = unit ? unit->ToPlayer() : nullptr; + if (!player || !owner) + return false; + + Group* group = owner->GetGroup(); + if (!group || !group->isLFGGroup() || player->GetGroup() != group) + return false; + + uint8 roles = sLFGMgr->GetRoles(player->GetGUID()); + return (roles & lfg::PLAYER_ROLE_TANK) != 0; + } + + bool IsDeclaredTank(Unit* unit, Player* owner) const + { + return IsVianoTank(unit) || IsDeclaredPlayerTank(unit, owner); + } + + bool IsActiveTank(Unit* unit) const + { + return unit && unit->IsAlive() && !unit->getAttackers().empty(); + } + + uint32 CountRelevantAttackers(Unit* unit) const + { + if (!unit) + return 0; + + uint32 count = 0; + for (Unit* attacker : unit->getAttackers()) + if (attacker && attacker->IsAlive() && attacker->GetVictim() == unit) + ++count; + return count; + } + + bool IsGroupCombatActive(Player* owner) const + { + if (!owner) + return false; + + std::list units; + owner->GetPartyMembers(units); + for (Unit* unit : units) + if (unit && unit->IsAlive() && (!unit->getAttackers().empty() || unit->IsInCombat())) + return true; + return false; + } + + bool IsValidHealTarget(Unit* unit, Player* owner) const + { + return unit + && owner + && unit->IsAlive() + && unit->IsInMap(owner) + && me->GetDistance(unit) <= HEAL_RANGE + && me->IsWithinLOSInMap(unit) + && me->IsValidAssistTarget(unit); + } + + float HealthPct(Unit* unit) const + { + return unit && unit->GetMaxHealth() + ? 100.0f * float(unit->GetHealth()) / float(unit->GetMaxHealth()) + : 100.0f; + } + + int32 HealPriority(Unit* unit, Player* owner) const + { + if (!IsValidHealTarget(unit, owner)) + return -1; + + float hp = HealthPct(unit); + bool declaredTank = IsDeclaredTank(unit, owner); + bool activeTank = IsActiveTank(unit); + uint32 attackers = CountRelevantAttackers(unit); + + if (!declaredTank && !activeTank && hp > 75.0f) + return -1; + if ((declaredTank || activeTank) && hp > 95.0f) + return -1; + + int32 score = int32(100.0f - hp); + if (declaredTank) + score += 45; + if (activeTank) + score += 65; + score += int32(std::min(attackers, 5u)) * 12; + + if (hp <= 35.0f) + score += 120; + else if (hp <= 50.0f) + score += 55; + + return score; + } + + Unit* SelectHealTarget(Player* owner) const + { + std::list units; + owner->GetPartyMembers(units); + + Unit* best = nullptr; + int32 bestScore = -1; + for (Unit* unit : units) + { + int32 score = HealPriority(unit, owner); + if (score > bestScore) + { + best = unit; + bestScore = score; + } + } + return best; + } + + uint32 SelectHealSpell(Unit* target, Player* owner) const + { + float hp = HealthPct(target); + bool tank = IsDeclaredTank(target, owner) || IsActiveTank(target); + + if (hp <= 35.0f || (tank && hp <= 45.0f)) + return SPELL_HEAL_EMERGENCY; + if (hp <= 50.0f || (tank && hp <= 68.0f)) + return SPELL_HEAL_STRONG; + return SPELL_HEAL_EFFICIENT; + } + + bool ManaPolicyAllows(Unit* target, Player* owner, uint32 spellId) const + { + uint32 maxMana = me->GetMaxPower(POWER_MANA); + if (!maxMana) + return true; + + float manaPct = 100.0f * float(me->GetPower(POWER_MANA)) / float(maxMana); + float hp = HealthPct(target); + bool tank = IsDeclaredTank(target, owner) || IsActiveTank(target); + + if (manaPct < 10.0f) + return spellId == SPELL_HEAL_EMERGENCY && hp <= 30.0f; + + if (manaPct < 25.0f) + { + if (spellId == SPELL_HEAL_EMERGENCY) + return true; + if (tank) + return hp <= 70.0f; + return hp <= 45.0f; + } + + return true; + } + + void ConfigureMana(bool refill) + { + // VIANO_HEALER_BALANCE_V1_2 + // Player-like mana pool: level 31 -> 1802 mana. + uint32 desired = 500u + uint32(me->getLevel()) * 42u; + if (desired < 900u) + desired = 900u; + + uint32 oldMax = me->GetMaxPower(POWER_MANA); + uint32 oldMana = me->GetPower(POWER_MANA); + me->SetMaxPower(POWER_MANA, desired); + + if (refill || !oldMax) + me->SetPower(POWER_MANA, desired); + else + me->SetPower(POWER_MANA, uint32(std::min( + uint64(oldMana) * uint64(desired) / uint64(oldMax), uint64(desired)))); + + // Disable generic creature regen; Viano owns healer mana regen. + me->RemoveFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_REGENERATE_POWER); + } + + void FollowOwner(Player* owner) + { + if (!owner) + return; + + uint32 slot = me->GetGUID().GetCounter() % 6u; + static float const distances[6] = { 3.0f, 3.2f, 3.2f, 4.0f, 4.0f, 4.5f }; + static float const angles[6] = { 3.14159265f, 2.55f, 3.73f, 2.80f, 3.48f, 3.14159265f }; + me->GetMotionMaster()->MoveFollow(owner, distances[slot], angles[slot]); + } + + void HandleOutOfCombatRecovery(uint32 diff, Player* owner) + { + if (!me->IsAlive() || !owner) + { + recoveryTimer = 1000; + return; + } + + if (recoveryTimer > diff) + { + recoveryTimer -= diff; + return; + } + + recoveryTimer = 1000; + bool const groupCombat = IsGroupCombatActive(owner); + + if (!groupCombat) + { + uint32 maxHealth = me->GetMaxHealth(); + if (me->GetHealth() < maxHealth) + { + uint32 gain = std::max(1u, maxHealth / 10u); + me->SetHealth(std::min(maxHealth, me->GetHealth() + gain)); + } + } + + uint32 maxMana = me->GetMaxPower(POWER_MANA); + if (maxMana && me->GetPower(POWER_MANA) < maxMana) + { + uint32 divisor = groupCombat ? 100u : 30u; // 1% combat, ~3.3% OOC + uint32 gain = std::max(1u, maxMana / divisor); + me->SetPower(POWER_MANA, + std::min(maxMana, me->GetPower(POWER_MANA) + gain)); + } + } + + void InitializeAI() override + { + ScriptedAI::InitializeAI(); + + TempSummon* summon = me->ToTempSummon(); + Player* owner = summon && summon->GetSummoner() ? summon->GetSummoner()->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(); + ConfigureMana(true); + + static constexpr uint32 MODELS[] = { 30063, 30064, 30065, 30066 }; + me->SetDisplayId(MODELS[urand(0, 3)], true); + SetEquipmentSlots(false, 39426, EQUIP_NO_CHANGE, EQUIP_NO_CHANGE); + me->SetReactState(REACT_PASSIVE); + FollowOwner(owner); + + TC_LOG_INFO("scripts", + "[VianoHealer] initialized entry=%u owner='%s' level=%u hp=%u/%u mana=%u/%u", + me->GetEntry(), owner->GetName().c_str(), uint32(me->getLevel()), + me->GetHealth(), me->GetMaxHealth(), + me->GetPower(POWER_MANA), me->GetMaxPower(POWER_MANA)); + } + + void Reset() override + { + if (Player* owner = GetOwnerPlayer()) + FollowOwner(owner); + } + + void MoveInLineOfSight(Unit* /*who*/) override + { + } + + + // VIANO_HEALER_DISPEL_V1 + bool TryDispelGroupMember(Player* owner) + { + if (!owner || me->HasUnitState(UNIT_STATE_CASTING)) + return false; + + std::list members; + owner->GetPartyMembers(members); + std::vector candidates; + candidates.push_back(owner); + + for (Unit* member : members) + { + if (!member) + continue; + bool duplicate = false; + for (Unit* existing : candidates) + if (existing && existing->GetGUID() == member->GetGUID()) + duplicate = true; + if (!duplicate) + candidates.push_back(member); + } + + // Healing wins if anyone is substantially hurt. + for (Unit* unit : candidates) + if (unit && unit->IsAlive() && unit->IsInMap(me) && unit->GetHealthPct() < 70.0f) + return false; + + uint32 const curseMask = (1u << DISPEL_CURSE); + uint32 const cleanseMask = (1u << DISPEL_MAGIC) | + (1u << DISPEL_DISEASE) | (1u << DISPEL_POISON); + + for (Unit* unit : candidates) + { + if (!unit || !unit->IsAlive() || !unit->IsInMap(me)) + continue; + if (!me->IsWithinDistInMap(unit, 40.0f) || !me->IsWithinLOSInMap(unit)) + continue; + + DispelChargesList curseAuras; + unit->GetDispellableAuraList(me, curseMask, curseAuras); + if (!curseAuras.empty()) + { + DoCast(unit, 2782); // Remove Corruption + return true; + } + + DispelChargesList cleanseAuras; + unit->GetDispellableAuraList(me, cleanseMask, cleanseAuras); + if (!cleanseAuras.empty()) + { + DoCast(unit, 4987); // Cleanse + return true; + } + } + return false; + } + +void UpdateAI(uint32 diff) override + { + if (updateTimer > diff) + { + updateTimer -= diff; + decisionLockTimer = decisionLockTimer > diff ? decisionLockTimer - diff : 0; + HandleOutOfCombatRecovery(diff, GetOwnerPlayer()); + return; + } + + updateTimer = 250; + decisionLockTimer = decisionLockTimer > diff ? decisionLockTimer - diff : 0; + + Player* owner = GetOwnerPlayer(); + + if (owner && TryDispelGroupMember(owner)) + return; + if (!owner || !owner->IsInWorld() || !me->IsInMap(owner)) + { + me->DespawnOrUnsummon(); + return; + } + + HandleOutOfCombatRecovery(diff, owner); + + if (me->getLevel() != owner->getLevel()) + { + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + ConfigureMana(false); + } + + if (me->GetDistance(owner) > 45.0f && !me->HasUnitState(UNIT_STATE_CASTING)) + { + me->GetMotionMaster()->Clear(); + me->NearTeleportTo(owner->GetPositionX() + 2.0f, owner->GetPositionY() + 1.0f, + owner->GetPositionZ(), owner->GetOrientation()); + FollowOwner(owner); + return; + } + + if (me->HasUnitState(UNIT_STATE_CASTING) || decisionLockTimer) + return; + + Unit* target = SelectHealTarget(owner); + if (!target) + { + FollowOwner(owner); + return; + } + + uint32 spellId = SelectHealSpell(target, owner); + if (!ManaPolicyAllows(target, owner, spellId)) + return; + + me->CastSpell(target, spellId); + + TC_LOG_INFO("scripts", + "[VianoHealer] cast spell=%u target='%s' hp=%.1f declaredTank=%u activeTank=%u attackers=%u mana=%u/%u", + spellId, target->GetName().c_str(), HealthPct(target), + IsDeclaredTank(target, owner) ? 1u : 0u, + IsActiveTank(target) ? 1u : 0u, + CountRelevantAttackers(target), + me->GetPower(POWER_MANA), me->GetMaxPower(POWER_MANA)); + + decisionLockTimer = 500; + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_healerAI(creature); + } + }; } void AddSC_viano_adventurers() @@ -956,4 +1681,5 @@ void AddSC_viano_adventurers() using namespace VianoAdventurers; new npc_horde_adventurer_melee(); new npc_horde_adventurer_tank(); + new npc_horde_adventurer_healer(); } diff --git a/src/server/scripts/Custom/viano_adventurers.cpp.bak-groupcombat-20260930-163130 b/src/server/scripts/Custom/viano_adventurers.cpp.bak-groupcombat-20260930-163130 deleted file mode 100644 index cd28a8c62b..0000000000 --- a/src/server/scripts/Custom/viano_adventurers.cpp.bak-groupcombat-20260930-163130 +++ /dev/null @@ -1,477 +0,0 @@ -/* - * 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() : ""; - } - - 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())); - } - - Unit* SelectAssistTarget(Player* owner) -{ - if (!owner) - return nullptr; - - // 1. Ein aktives Ziel des Besitzers ist eine ausdrückliche - // Zielanweisung und darf das aktuelle Bot-Ziel überschreiben. - if (Unit* target = owner->GetVictim()) - if (IsValidCandidate(target)) - return target; - - // GetVictim() ist beim Spieler nicht immer zuverlässig gesetzt. - // Während eines bereits laufenden Kampfes darf deshalb auch ein - // bewusst ausgewähltes lebendes Feindziel als Anweisung gelten. - // Reines Anwählen außerhalb des Kampfes darf weiterhin NICHT pullen. - if (owner->IsInCombat()) - { - if (Unit* target = owner->GetSelectedUnit()) - if (IsValidCandidate(target)) - return target; - } - - // 2. Sobald der Bot selbst einen gültigen Gegner bekämpft, - // hält er an diesem Ziel fest. ESC, eine Leiche als Target oder - // fehlendes Spielertarget beendet den Kampf dadurch nicht mehr. - if (Unit* target = me->GetVictim()) - if (IsValidCandidate(target)) - return target; - - // 3. Falls das bisherige Ziel tot/ungültig ist, helfen wir dem - // Besitzer gegen einen Gegner, der ihn bereits angreift. - for (Unit* attacker : owner->getAttackers()) - if (IsValidCandidate(attacker)) - return attacker; - - // 4. Danach übernimmt der Bot einen Gegner, der ihn selbst angreift. - // Besonders wichtig, wenn sein vorheriges Ziel gerade gestorben ist. - 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) - { - me->CombatStop(true); - 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 (!me->IsInCombat() && !me->GetVictim()) - { - 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(); -} diff --git a/src/server/scripts/Custom/viano_adventurers.cpp.bak-player-controlled-20261001-010613 b/src/server/scripts/Custom/viano_adventurers.cpp.bak-player-controlled-20261001-010613 deleted file mode 100644 index a3032d11e5..0000000000 --- a/src/server/scripts/Custom/viano_adventurers.cpp.bak-player-controlled-20261001-010613 +++ /dev/null @@ -1,516 +0,0 @@ -/* - * 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() : ""; - } - - 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 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(); -} diff --git a/src/server/scripts/Custom/viano_adventurers.cpp.bak-tank-attacker-snapshot-20261001-114726 b/src/server/scripts/Custom/viano_adventurers.cpp.bak-tank-attacker-snapshot-20261001-114726 deleted file mode 100644 index 5d668ad151..0000000000 --- a/src/server/scripts/Custom/viano_adventurers.cpp.bak-tank-attacker-snapshot-20261001-114726 +++ /dev/null @@ -1,956 +0,0 @@ -/* - * 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() : ""; - } - - 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 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(); -} diff --git a/src/server/scripts/Custom/viano_adventurers.cpp.bak-viano-group-context-v1-20261001-134352 b/src/server/scripts/Custom/viano_adventurers.cpp.bak-viano-group-context-v1-20261001-134352 deleted file mode 100644 index 8ce77f0bf8..0000000000 --- a/src/server/scripts/Custom/viano_adventurers.cpp.bak-viano-group-context-v1-20261001-134352 +++ /dev/null @@ -1,964 +0,0 @@ -/* - * 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 -#include - -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() : ""; - } - - 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 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& 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 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 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(); -} diff --git a/src/server/scripts/Custom/viano_adventurers.cpp.bak-viano-tank-v1-20261001-083107 b/src/server/scripts/Custom/viano_adventurers.cpp.bak-viano-tank-v1-20261001-083107 deleted file mode 100644 index 8f4fe95e21..0000000000 --- a/src/server/scripts/Custom/viano_adventurers.cpp.bak-viano-tank-v1-20261001-083107 +++ /dev/null @@ -1,517 +0,0 @@ -/* - * 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() : ""; - } - - 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 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(); -} diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index 458cc25bcf..fe0a143e2d 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -12,9 +12,11 @@ #include "ScriptMgr.h" #include "Chat.h" #include "Creature.h" +#include "Group.h" #include "ObjectAccessor.h" #include "ObjectMgr.h" #include "Player.h" +#include "SpellAuras.h" #include "RBAC.h" #include "TemporarySummon.h" #include "Unit.h" @@ -403,6 +405,8 @@ namespace VianoBotCommands entry = 900001; else if (role == "tank") entry = 900002; + else if (role == "healer") + entry = 900003; else { handler->SendSysMessage("Unknown role. Usage: .viano bot summon melee|tank"); @@ -433,6 +437,140 @@ namespace VianoBotCommands return true; } + + // VIANO_GROUP_DIAGNOSTICS_V1 + static void PrintAuraSummary(ChatHandler* handler, char const* label, Unit* unit) + { + if (!handler || !unit) + return; + + Unit::AuraApplicationMap const& auras = unit->GetAppliedAuras(); + handler->PSendSysMessage("%s appliedAuras=%u", label, uint32(auras.size())); + + uint32 shown = 0; + for (auto const& pair : auras) + { + AuraApplication const* app = pair.second; + if (!app || !app->GetBase()) + continue; + + handler->PSendSysMessage( + "%s aura[%u] spell=%u positive=%s caster=%s", + label, + shown, + app->GetBase()->GetId(), + BoolText(app->IsPositive()), + GuidText(app->GetBase()->GetCasterGUID()).c_str()); + + if (++shown >= 20) + { + handler->SendSysMessage("aura output truncated after 20 entries."); + break; + } + } + } + + static bool HandleGroup(ChatHandler* handler, char const* /*args*/) + { + Creature* bot = GetSelectedVianoBot(handler); + if (!bot) + return true; + + Player* owner = bot->GetOwner() ? bot->GetOwner()->ToPlayer() : nullptr; + if (!owner) + { + handler->SendSysMessage("Selected bot has no resolved player owner."); + return true; + } + + PrintHeader(handler, bot, "group"); + + std::list units; + owner->GetPartyMembers(units); + handler->PSendSysMessage( + "owner='%s' effectiveGroupSize=%u enumeratedUnits=%u", + owner->GetName().c_str(), + owner->GetVianoEffectiveGroupSize(), + uint32(units.size())); + + uint32 index = 0; + for (Unit* unit : units) + { + if (!unit) + continue; + + handler->PSendSysMessage( + "member[%u] name='%s' entry=%u guid=%s player=%s viano=%s alive=%s distance=%.2f", + index++, + UnitName(unit), + UnitEntry(unit), + GuidText(unit->GetGUID()).c_str(), + BoolText(unit->IsPlayer()), + BoolText(unit->IsVianoAdventurer()), + BoolText(unit->IsAlive()), + bot->GetDistance(unit)); + } + + if (Group* group = owner->GetGroup()) + { + handler->SendSysMessage("targetIconSource=real-group"); + for (uint8 icon = 0; icon < TARGETICONCOUNT; ++icon) + { + ObjectGuid guid = group->GetTargetIcon(icon); + Unit* target = guid ? ObjectAccessor::GetUnit(*bot, guid) : nullptr; + handler->PSendSysMessage( + "targetIcon[%u] guid=%s target='%s' entry=%u", + uint32(icon), + GuidText(guid).c_str(), + UnitName(target), + UnitEntry(target)); + } + } + else + { + handler->SendSysMessage("targetIconSource=viano-virtual"); + for (uint8 icon = 0; icon < TARGETICONCOUNT; ++icon) + { + ObjectGuid guid = owner->GetVianoTargetIcon(icon); + Unit* target = guid ? ObjectAccessor::GetUnit(*bot, guid) : nullptr; + handler->PSendSysMessage( + "targetIcon[%u] guid=%s target='%s' entry=%u", + uint32(icon), + GuidText(guid).c_str(), + UnitName(target), + UnitEntry(target)); + } + } + + PrintAuraSummary(handler, "owner", owner); + PrintAuraSummary(handler, "bot", bot); + + uint32 missingShown = 0; + for (auto const& pair : owner->GetAppliedAuras()) + { + AuraApplication const* app = pair.second; + if (!app || !app->GetBase() || !app->IsPositive()) + continue; + + uint32 spellId = app->GetBase()->GetId(); + if (bot->HasAura(spellId)) + continue; + + handler->PSendSysMessage( + "ownerPositiveMissingOnBot spell=%u caster=%s", + spellId, + GuidText(app->GetBase()->GetCasterGUID()).c_str()); + + if (++missingShown >= 20) + { + handler->SendSysMessage("missing-aura output truncated after 20 entries."); + break; + } + } + + return true; + } + class VianoBotCommandScript : public CommandScript { public: @@ -447,6 +585,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, "" }, + { "group", rbac::RBAC_PERM_COMMAND_GM, false, &HandleGroup, "" }, { "summon", rbac::RBAC_PERM_COMMAND_GM, false, &HandleSummon, "" }, }; diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp.bak-rbac-20260930-190232 b/src/server/scripts/Custom/viano_bot_commands.cpp.bak-rbac-20260930-190232 deleted file mode 100644 index c8d3ccd88a..0000000000 --- a/src/server/scripts/Custom/viano_bot_commands.cpp.bak-rbac-20260930-190232 +++ /dev/null @@ -1,400 +0,0 @@ -/* - * 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 "TemporarySummon.h" -#include "Unit.h" - -#include - -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(""); - } - - static char const* UnitName(Unit const* unit) - { - return unit ? unit->GetName().c_str() : ""; - } - - 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() : ""); - - 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="); - - 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() : "", - 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() : ""); - } - 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 GetCommands() const override - { - static std::vector 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 vianoCommands = - { - { "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands }, - }; - - static std::vector commandTable = - { - { "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands }, - }; - - return commandTable; - } - }; -} - -void AddSC_viano_bot_commands() -{ - new VianoBotCommands::VianoBotCommandScript(); -} diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp.bak-viano-group-context-v1-20261001-134352 b/src/server/scripts/Custom/viano_bot_commands.cpp.bak-viano-group-context-v1-20261001-134352 deleted file mode 100644 index d57f99a3bb..0000000000 --- a/src/server/scripts/Custom/viano_bot_commands.cpp.bak-viano-group-context-v1-20261001-134352 +++ /dev/null @@ -1,457 +0,0 @@ -/* - * 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 - -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(""); - } - - static char const* UnitName(Unit const* unit) - { - return unit ? unit->GetName().c_str() : ""; - } - - 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() : ""); - - 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="); - - 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() : "", - 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() : ""); - } - 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 GetCommands() const override - { - static std::vector 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 vianoCommands = - { - { "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands }, - }; - - static std::vector commandTable = - { - { "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands }, - }; - - return commandTable; - } - }; -} - -void AddSC_viano_bot_commands() -{ - new VianoBotCommands::VianoBotCommandScript(); -} diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp.bak-viano-tank-v1-20261001-083107 b/src/server/scripts/Custom/viano_bot_commands.cpp.bak-viano-tank-v1-20261001-083107 deleted file mode 100644 index decd1afe75..0000000000 --- a/src/server/scripts/Custom/viano_bot_commands.cpp.bak-viano-tank-v1-20261001-083107 +++ /dev/null @@ -1,401 +0,0 @@ -/* - * 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 - -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(""); - } - - static char const* UnitName(Unit const* unit) - { - return unit ? unit->GetName().c_str() : ""; - } - - 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() : ""); - - 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="); - - 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() : "", - 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() : ""); - } - 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 GetCommands() const override - { - static std::vector 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 vianoCommands = - { - { "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands }, - }; - - static std::vector commandTable = - { - { "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands }, - }; - - return commandTable; - } - }; -} - -void AddSC_viano_bot_commands() -{ - new VianoBotCommands::VianoBotCommandScript(); -} diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp index 493bb80626..4b89399938 100644 --- a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp @@ -426,6 +426,14 @@ struct npc_greymanes_horse : public VehicleAI { npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { } + void DamageTaken(Unit* /*attacker*/, uint32& damage) override + { + if (me->GetHealth() <= 1) + damage = 0; + else if (damage >= me->GetHealth()) + damage = me->GetHealth() - 1; + } + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override { if (apply && passenger->GetTypeId() == TYPEID_PLAYER) @@ -540,10 +548,76 @@ enum CrowleysHorse static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f }; static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f }; +static Position const CrowleysHorsePath1[] = +{ + { -1686.12f, 1655.64f, 20.6106f }, + { -1668.16f, 1639.43f, 20.6106f }, + { -1666.63f, 1628.86f, 20.5327f }, + { -1670.76f, 1617.43f, 20.6106f }, + { -1693.65f, 1592.64f, 20.6106f }, + { -1708.02f, 1590.55f, 20.6104f }, + { -1719.37f, 1595.65f, 20.6104f }, + { -1720.57f, 1610.14f, 20.6074f }, + { -1711.90f, 1629.70f, 20.6074f }, + { -1698.15f, 1656.48f, 20.6106f }, + { -1696.42f, 1682.01f, 20.5262f }, + { -1684.68f, 1700.12f, 20.4648f }, + { -1658.31f, 1710.31f, 20.5898f }, + { -1625.48f, 1712.30f, 21.8051f }, + { -1592.09f, 1710.71f, 20.5899f } +}; + + +static Position const CrowleysHorsePath2[] = +{ + { -1545.08f, 1684.23f, 20.6099f }, + { -1542.79f, 1656.08f, 20.6099f }, + { -1543.0f, 1640.54f, 20.6099f }, + { -1520.73f, 1618.1f, 20.6105f }, + { -1498.79f, 1621.74f, 20.6105f }, + { -1468.36f, 1630.78f, 20.6105f }, + { -1441.65f, 1627.67f, 20.6105f }, + { -1429.84f, 1616.63f, 20.5898f }, + { -1417.99f, 1586.55f, 20.5898f }, + { -1429.18f, 1548.78f, 20.8485f }, + { -1447.42f, 1532.79f, 20.601f }, + { -1463.1f, 1535.03f, 20.6105f }, + { -1483.04f, 1544.15f, 20.6105f }, + { -1505.16f, 1568.61f, 20.6105f }, + { -1531.6f, 1581.87f, 26.5387f }, + { -1540.59f, 1574.43f, 29.2067f }, +}; + +static Position const CrowleysHorsePath3[] = +{ + { -1512.36f, 1570.78f, 24.0671f }, + { -1498.71f, 1565.43f, 20.8781f }, + { -1487.86f, 1557.16f, 20.85f }, + { -1474.18f, 1542.68f, 20.6105f }, + { -1457.6f, 1532.64f, 20.601f }, + { -1438.01f, 1525.31f, 20.8762f }, + { -1424.2f, 1545.7f, 20.4657f }, + { -1411.29f, 1556.16f, 20.557f }, + { -1414.19f, 1577.65f, 20.5898f }, + { -1427.47f, 1617.3f, 20.5898f }, + { -1445.54f, 1633.9f, 20.6105f }, + { -1470.71f, 1658.21f, 20.6105f }, + { -1491.05f, 1658.74f, 20.6105f }, + { -1515.33f, 1641.18f, 20.6099f }, + { -1526.75f, 1630.37f, 20.6105f }, + { -1528.42f, 1610.71f, 20.6105f }, + { -1532.39f, 1585.36f, 26.538f }, +}; + struct npc_crowleys_horse : public VehicleAI { npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { } + void DamageTaken(Unit* /*attacker*/, uint32& damage) override + { + damage = 0; + } + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override { if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2) @@ -560,11 +634,11 @@ struct npc_crowleys_horse : public VehicleAI void MovementInform(uint32 type, uint32 pointId) override { - if (type == WAYPOINT_MOTION_TYPE && pointId == 14 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) + if (type == EFFECT_MOTION_TYPE && pointId == PATH_ID_CROWLEYS_HORSE_1 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s); - else if (type == WAYPOINT_MOTION_TYPE && pointId == 15 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) + else if (type == EFFECT_MOTION_TYPE && pointId == PATH_ID_CROWLEYS_HORSE_2 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); - else if (type == WAYPOINT_MOTION_TYPE && pointId == 16 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) + else if (type == EFFECT_MOTION_TYPE && pointId == PATH_ID_CROWLEYS_HORSE_3 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); } @@ -582,8 +656,11 @@ struct npc_crowleys_horse : public VehicleAI _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 2s); break; case EVENT_MOVE_PATH_MAIN_1: - me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_1, false); _currentPath = PATH_ID_CROWLEYS_HORSE_1; + me->GetMotionMaster()->MoveSmoothPath( + PATH_ID_CROWLEYS_HORSE_1, + CrowleysHorsePath1, + 15); break; case EVENT_JUMP_OVER_BARRICADES_2: me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f); @@ -591,7 +668,10 @@ struct npc_crowleys_horse : public VehicleAI break; case EVENT_MOVE_PATH_MAIN_2: _currentPath = PATH_ID_CROWLEYS_HORSE_2; - me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_2, false); + me->GetMotionMaster()->MoveSmoothPath( + PATH_ID_CROWLEYS_HORSE_2, + CrowleysHorsePath2, + 16); break; case EVENT_DISMOUNT_PLAYER: { @@ -610,7 +690,10 @@ struct npc_crowleys_horse : public VehicleAI case EVENT_MOVE_OFF_PATH: me->SetControlled(false, UNIT_STATE_ROOT); _currentPath = PATH_ID_CROWLEYS_HORSE_3; - me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_3, false); + me->GetMotionMaster()->MoveSmoothPath( + PATH_ID_CROWLEYS_HORSE_3, + CrowleysHorsePath3, + 17); break; default: break; diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug15-horse-1hp-20261001-152719 b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug15-horse-1hp-20261001-152719 new file mode 100644 index 0000000000..b64cf1d0b1 --- /dev/null +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug15-horse-1hp-20261001-152719 @@ -0,0 +1,701 @@ +/* +* 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 . +*/ + +#include "gilneas.h" +#include "ScriptMgr.h" +#include "CombatAI.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "PassiveAI.h" +#include "PhasingHandler.h" +#include "ScriptedCreature.h" +#include "SpellInfo.h" +#include "SpellScript.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +namespace Gilneas::Chapter1 +{ + +/*###### +## Quest 14098 - Evacuate the Merchant Square +######*/ + +enum FrightenedCitizen +{ + SAY_FRIGHTENED_CITIZEN_RESCUE = 0, + + NPC_EVACUATION_STALKER_FIRST = 35830, + NPC_EVACUATION_STALKER_NEAR = 35010, + NPC_EVACUATION_STALKER_FAR = 35011, + NPC_RAMPAGING_WORGEN = 35660, + + CREDIT_35830 = 35830, + + POINT_STALKER_FIRST = 1, + POINT_STALKER_NEAR = 2, + POINT_STALKER_FAR = 3, + + EVENT_TALK_FRIGHTENED = 1, + EVENT_MOVE_TO_NEAR_STALKER = 2, + EVENT_MOVE_TO_FAR_STALKER = 3, + EVENT_DESPAWN = 4 +}; + +struct npc_frightened_citizen : public PassiveAI +{ + npc_frightened_citizen(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* /*summoner*/) override + { + if (Creature* stalkerNear = me->FindNearestCreature(NPC_EVACUATION_STALKER_FIRST, 20.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FIRST, stalkerNear->GetPosition(), true); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + switch (id) + { + case POINT_STALKER_FIRST: + _events.ScheduleEvent(EVENT_TALK_FRIGHTENED, 1s); + break; + case POINT_STALKER_NEAR: + _events.ScheduleEvent(EVENT_MOVE_TO_FAR_STALKER, 1ms); + break; + case POINT_STALKER_FAR: + _events.ScheduleEvent(EVENT_DESPAWN, 1ms); + break; + default: + break; + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_TALK_FRIGHTENED: + if (Unit* summoner = me->ToTempSummon()->GetSummoner()) + { + if (Player* player = summoner->ToPlayer()) + { + player->KilledMonsterCredit(CREDIT_35830); + Talk(SAY_FRIGHTENED_CITIZEN_RESCUE, summoner); + } + } + _events.ScheduleEvent(EVENT_MOVE_TO_NEAR_STALKER, 2s); + break; + case EVENT_MOVE_TO_NEAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 50.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + else if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_NEAR, 100.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_NEAR, stalker->GetPosition(), true); + break; + case EVENT_MOVE_TO_FAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 500.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + break; + case EVENT_DESPAWN: + me->DespawnOrUnsummon(); + break; + default: + break; + } + } + } +private: + EventMap _events; +}; + +/*###### +## Quest 14154 - By the Skin of his Teeth +######*/ + +enum WorgenRunt +{ + EVENT_JUMP_TO_PRISON = 1, + EVENT_AGGRO_PLAYER = 2, + EVENT_FORCE_DESPAWN = 3, + + PHASE_ROOF = 0, + PHASE_COMBAT = 1, + + WORGEN_ID_ROOF_1 = 0, + WORGEN_ID_ROOF_2 = 1, + WORGEN_ID_ROOF_3 = 2, + WORGEN_ID_ROOF_4 = 3, + WORGEN_ID_ROOF_5 = 4, + WORGEN_ID_ROOF_6 = 5, + WORGEN_ID_ROOF_7 = 6, + + WORGEN_ID_CATHEDRAL_1 = 7, + WORGEN_ID_CATHEDRAL_2 = 8, + WORGEN_ID_CATHEDRAL_3 = 9, + WORGEN_ID_CATHEDRAL_4 = 10, + WORGEN_ID_CATHEDRAL_5 = 11, + WORGEN_ID_CATHEDRAL_6 = 12, + WORGEN_ID_CATHEDRAL_7 = 13, + WORGEN_ID_CATHEDRAL_8 = 14, + WORGEN_ID_CATHEDRAL_9 = 15, + + NPC_WORGEN_RUNT_SPELL = 35188, +}; + +static Position const RuntSpellSummonJumpPos = { -1671.915f, 1446.734f, 52.28712f }; + +struct npc_worgen_runt : public ScriptedAI +{ + npc_worgen_runt(Creature* creature) : ScriptedAI(creature), _worgenID(0), _wayPointCounter(0), _jumped(false) { } + + void JustEngagedWith(Unit* /*who*/) override + { + _events.SetPhase(PHASE_COMBAT); + } + + void IsSummonedBy(Unit* summoner) override + { + me->setActive(true); // we are in a phased and cut off map so we're fine to use that here + _events.SetPhase(PHASE_ROOF); + _events.ScheduleEvent(EVENT_FORCE_DESPAWN, 70s, 0, PHASE_ROOF); + _playerGuid = summoner->GetGUID(); + if (me->GetEntry() == NPC_WORGEN_RUNT_SPELL) + me->GetMotionMaster()->MoveJump(RuntSpellSummonJumpPos, 16.0f, 4.371286f); + } + + void DoAction(int32 action) override + { + _worgenID = action; + switch (_worgenID) // ugly as fuck but needed for individual controls + { + // House roof cases + case WORGEN_ID_ROOF_1: + _wayPointCounter = runtHousePathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize1, worgenRuntHousePath1, runtHousePathSize1); + break; + case WORGEN_ID_ROOF_2: + _wayPointCounter = runtHousePathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize2, worgenRuntHousePath2, runtHousePathSize2); + break; + case WORGEN_ID_ROOF_3: + _wayPointCounter = runtHousePathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize3, worgenRuntHousePath3, runtHousePathSize3); + break; + case WORGEN_ID_ROOF_4: + _wayPointCounter = runtHousePathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize4, worgenRuntHousePath4, runtHousePathSize4); + break; + case WORGEN_ID_ROOF_5: + _wayPointCounter = runtHousePathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize5, worgenRuntHousePath5, runtHousePathSize5); + break; + case WORGEN_ID_ROOF_6: + _wayPointCounter = runtHousePathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize6, worgenRuntHousePath6, runtHousePathSize6); + break; + case WORGEN_ID_ROOF_7: + _wayPointCounter = runtHousePathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize7, worgenRuntHousePath7, runtHousePathSize7); + break; + // Cathedral cases + case WORGEN_ID_CATHEDRAL_1: + _wayPointCounter = runtCathedralPathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize1, worgenRuntCathedralPath1, runtCathedralPathSize1); + break; + case WORGEN_ID_CATHEDRAL_2: + _wayPointCounter = runtCathedralPathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize2, worgenRuntCathedralPath1, runtCathedralPathSize2); + break; + case WORGEN_ID_CATHEDRAL_3: + _wayPointCounter = runtCathedralPathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize3, worgenRuntCathedralPath1, runtCathedralPathSize3); + break; + case WORGEN_ID_CATHEDRAL_4: + _wayPointCounter = runtCathedralPathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize4, worgenRuntCathedralPath1, runtCathedralPathSize4); + break; + case WORGEN_ID_CATHEDRAL_5: + _wayPointCounter = runtCathedralPathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize5, worgenRuntCathedralPath1, runtCathedralPathSize5); + break; + case WORGEN_ID_CATHEDRAL_6: + _wayPointCounter = runtCathedralPathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize6, worgenRuntCathedralPath1, runtCathedralPathSize6); + break; + case WORGEN_ID_CATHEDRAL_7: + _wayPointCounter = runtCathedralPathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize7, worgenRuntCathedralPath1, runtCathedralPathSize7); + break; + case WORGEN_ID_CATHEDRAL_8: + _wayPointCounter = runtCathedralPathSize8; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize8, worgenRuntCathedralPath1, runtCathedralPathSize8); + break; + } + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == _wayPointCounter && !_jumped) + { + _jumped = true; + _events.ScheduleEvent(EVENT_JUMP_TO_PRISON, 1ms); + } + } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim() && !_events.IsInPhase(PHASE_ROOF)) + return; + + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_FORCE_DESPAWN: + me->DespawnOrUnsummon(); + break; + case EVENT_JUMP_TO_PRISON: + me->GetMotionMaster()->MoveJump(worgenRuntJumpPos[_worgenID], 16.0f, _worgenID < WORGEN_ID_CATHEDRAL_1 ? 19.2911f : frand(3.945607f, 4.852813f)); + me->SetHomePosition(worgenRuntJumpPos[_worgenID]); + _events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s); + break; + case EVENT_AGGRO_PLAYER: + { + Creature* crowley = me->FindNearestCreature(35077, 100.0f, true); + Creature* tobias = me->FindNearestCreature(35124, 100.0f, true); + + if (crowley && tobias) + { + if (me->GetDistance(crowley) <= me->GetDistance(tobias)) + AttackStart(crowley); + else + AttackStart(tobias); + } + else if (crowley) + AttackStart(crowley); + else if (tobias) + AttackStart(tobias); + + break; + } + default: + break; + } + } + + DoMeleeAttackIfReady(); + } +private: + uint32 _worgenID; + uint32 _wayPointCounter; + bool _jumped; + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14159 - The Rebel Lord's Arsenal +######*/ + +enum TheRebelLordsArsena +{ + PHASE_ID_SUMMON = 170, + PHASE_ID_WOUND = 171, + NPC_LORNA_CROWLEY = 35378, + NPC_GENERIC_TRIGGER_LAB = 35374, + + SPELL_SHOOT_INSTAKILL = 67593, + SPELL_COSMETIC_ATTACK = 42880, + SPELL_PULL_TO = 67357, + + EVENT_COSMETIC_ATTACK = 1, + EVENT_JUMP_TO_PLAYER, + EVENT_SHOOT_JOSIAH +}; + +static Position const JosiahJumpPos = { -1796.63f, 1427.73f, 12.4624f }; + +struct npc_josiah_avery : public PassiveAI +{ + npc_josiah_avery(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* summoner) override + { + PhasingHandler::AddPhase(me, PHASE_ID_SUMMON, true); + PhasingHandler::AddPhase(me, PHASE_ID_WOUND, true); + _playerGuid = summoner->GetGUID(); + _events.ScheduleEvent(EVENT_COSMETIC_ATTACK, 500ms); + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_COSMETIC_ATTACK: + if (Player* player = ObjectAccessor::GetPlayer(*me, _playerGuid)) + { + DoCast(player, SPELL_COSMETIC_ATTACK); + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + if (Creature* labTrigger = lorna->FindNearestCreature(NPC_GENERIC_TRIGGER_LAB, 5.0f, true)) + labTrigger->CastSpell(player, SPELL_PULL_TO); + + _events.ScheduleEvent(EVENT_JUMP_TO_PLAYER, 1s); + } + break; + case EVENT_JUMP_TO_PLAYER: + me->GetMotionMaster()->MoveJump(JosiahJumpPos, 10.0f, 14.18636f); + _events.ScheduleEvent(EVENT_SHOOT_JOSIAH, 500ms); + break; + case EVENT_SHOOT_JOSIAH: + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + lorna->CastSpell(me, SPELL_SHOOT_INSTAKILL, true); + break; + default: + break; + } + } + } +private: + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14293 - Save Krennan Aranas +######*/ + +static Position const GreymanesHorseJumpPos = { -1676.16f, 1346.19f, 15.1349f }; + +enum GreymanesHorse +{ + SAY_ANNOUNCE_RESCUE = 0, + SAY_RESCUED = 0, + SAY_TRAPPED = 0, + + PATH_KRENNAN_TREE = 0, + PATH_KRENNAN_BACK = 1, + + EVENT_START_PATH_1 = 1, + EVENT_START_PATH_2, + EVENT_JUMP_TO_KRENNAN, + EVENT_ANNOUNCE_RESCUE, + EVENT_DISMOUNT_PLAYER, + + NPC_RESCUED_KRENNAN = 35907, + NPC_TRAPPED_KRENNAN = 35753, +}; + +struct npc_greymanes_horse : public VehicleAI +{ + npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { } + + void DamageTaken(Unit* /*attacker*/, uint32& damage) override + { + damage = 0; + } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_START_PATH_1, 1s); + } + else if (apply && passenger->GetEntry() == NPC_RESCUED_KRENNAN) + _events.ScheduleEvent(EVENT_START_PATH_2, 1s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == pathSize1 && _currentPath == PATH_KRENNAN_TREE) + _events.ScheduleEvent(EVENT_JUMP_TO_KRENNAN, 1ms); + else if (type == EFFECT_MOTION_TYPE && pointId == pathSize2 && _currentPath == PATH_KRENNAN_BACK) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_PATH_1: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_TREE; + me->GetMotionMaster()->MoveSmoothPath(pathSize1, greymanesHorsePath1, pathSize1); + break; + case EVENT_JUMP_TO_KRENNAN: + me->GetMotionMaster()->MoveJump(GreymanesHorseJumpPos, 16.0f, 14.00968f); + _events.ScheduleEvent(EVENT_ANNOUNCE_RESCUE, 2s); + break; + case EVENT_ANNOUNCE_RESCUE: + me->SetControlled(true, UNIT_STATE_ROOT); + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(0)) + { + Talk(SAY_ANNOUNCE_RESCUE, passenger); + if (Creature* krennan = me->FindNearestCreature(NPC_TRAPPED_KRENNAN, 30.0f, true)) + krennan->AI()->Talk(SAY_TRAPPED, passenger); + } + break; + case EVENT_START_PATH_2: + me->SetControlled(true, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_BACK; + me->GetMotionMaster()->MoveSmoothPath(pathSize2, greymanesHorsePath2, pathSize2); + break; + case EVENT_DISMOUNT_PLAYER: + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(1)) + { + if (Unit* player = me->GetVehicleKit()->GetPassenger(0)) + { + if (Creature* krennan = passenger->ToCreature()) + { + std::vector storedAttackers; + + for (Unit* attacker : me->getAttackers()) + storedAttackers.push_back(attacker); + + for (Unit* attacker : storedAttackers) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + player->ExitVehicle(); + krennan->AddUnitState(UNIT_STATE_ROOT); + krennan->ExitVehicle(); + krennan->AI()->Talk(SAY_RESCUED, me); + krennan->DespawnOrUnsummon(7s); + } + } + } + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +/*###### +## Quest 14212 - Sacrifices +######*/ + +enum CrowleysHorse +{ + EVENT_JUMP_OVER_BARRICADES_1 = 1, + EVENT_JUMP_OVER_BARRICADES_2, + EVENT_MOVE_PATH_MAIN_1, + EVENT_MOVE_PATH_MAIN_2, + EVENT_MOVE_OFF_PATH = 6, + + PATH_ID_CROWLEYS_HORSE_1 = 352310, + PATH_ID_CROWLEYS_HORSE_2 = 352311, + PATH_ID_CROWLEYS_HORSE_3 = 444280, + + SPELL_THROW_TORCH = 67063, + NPC_CROWLEYS_HORSE_2 = 44428, +}; + +static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f }; +static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f }; + +struct npc_crowleys_horse : public VehicleAI +{ + npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_1, 2s); + } + else if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_MOVE_OFF_PATH, 2s); + } + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == WAYPOINT_MOTION_TYPE && pointId == 14 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 15 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 16 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_JUMP_OVER_BARRICADES_1: + me->SetControlled(false, UNIT_STATE_ROOT); + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 2s); + break; + case EVENT_MOVE_PATH_MAIN_1: + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_1, false); + _currentPath = PATH_ID_CROWLEYS_HORSE_1; + break; + case EVENT_JUMP_OVER_BARRICADES_2: + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_2, 2s); + break; + case EVENT_MOVE_PATH_MAIN_2: + _currentPath = PATH_ID_CROWLEYS_HORSE_2; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_2, false); + break; + case EVENT_DISMOUNT_PLAYER: + { + std::set attackersCopy = me->getAttackers(); + for (Unit* attacker : attackersCopy) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + me->RemoveAurasDueToSpell(VEHICLE_SPELL_RIDE_HARDCODED); + me->DespawnOrUnsummon(5s); + break; + } + case EVENT_MOVE_OFF_PATH: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_ID_CROWLEYS_HORSE_3; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_3, false); + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +enum GileanCrow +{ + SPELL_GILNEAN_CROW = 93275, + EVENT_APPLY_HOVER_BYTES = 1, + EVENT_FLY_AWAY_1 = 2, + EVENT_FLY_AWAY_2 = 3, + + POINT_NONE = 0, + POINT_CROW_FLIGHT = 1 +}; + +struct npc_gilnean_crow : public PassiveAI +{ + npc_gilnean_crow(Creature* creature) : PassiveAI(creature) { } + + void SpellHit(WorldObject* /*caster*/, SpellInfo const* spell) override + { + if (spell->Id == SPELL_GILNEAN_CROW) + _events.ScheduleEvent(EVENT_APPLY_HOVER_BYTES, 500ms); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == POINT_MOTION_TYPE && pointId == POINT_CROW_FLIGHT) + { + Position pos = me->GetRandomNearPosition(8.0f); + pos.m_positionZ = me->GetPositionZ() + 10.0f; + me->GetMotionMaster()->MovePoint(POINT_NONE, pos); + me->DespawnOrUnsummon(14s); + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_APPLY_HOVER_BYTES: + me->SetStandState(UNIT_STAND_STATE_SIT); + me->SetAnimTier(AnimTier::Submerged); + _events.ScheduleEvent(EVENT_FLY_AWAY_1, 1s); + break; + case EVENT_FLY_AWAY_1: + { + Position pos = me->GetRandomNearPosition(7.0f); + pos.m_positionZ = me->GetPositionZ() + 8.0f; + me->GetMotionMaster()->MovePoint(POINT_CROW_FLIGHT, pos); + break; + } + default: + break; + } + } + } +private: + EventMap _events; +}; +} + +void AddSC_gilneas_chapter_1() +{ + using namespace Gilneas::Chapter1; + RegisterCreatureAI(npc_frightened_citizen); + RegisterCreatureAI(npc_worgen_runt); + RegisterCreatureAI(npc_josiah_avery); + RegisterCreatureAI(npc_greymanes_horse); + RegisterCreatureAI(npc_crowleys_horse); + RegisterCreatureAI(npc_gilnean_crow); +} diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug15-horse-invuln-20261001-150511 b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug15-horse-invuln-20261001-150511 new file mode 100644 index 0000000000..493bb80626 --- /dev/null +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug15-horse-invuln-20261001-150511 @@ -0,0 +1,696 @@ +/* +* 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 . +*/ + +#include "gilneas.h" +#include "ScriptMgr.h" +#include "CombatAI.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "PassiveAI.h" +#include "PhasingHandler.h" +#include "ScriptedCreature.h" +#include "SpellInfo.h" +#include "SpellScript.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +namespace Gilneas::Chapter1 +{ + +/*###### +## Quest 14098 - Evacuate the Merchant Square +######*/ + +enum FrightenedCitizen +{ + SAY_FRIGHTENED_CITIZEN_RESCUE = 0, + + NPC_EVACUATION_STALKER_FIRST = 35830, + NPC_EVACUATION_STALKER_NEAR = 35010, + NPC_EVACUATION_STALKER_FAR = 35011, + NPC_RAMPAGING_WORGEN = 35660, + + CREDIT_35830 = 35830, + + POINT_STALKER_FIRST = 1, + POINT_STALKER_NEAR = 2, + POINT_STALKER_FAR = 3, + + EVENT_TALK_FRIGHTENED = 1, + EVENT_MOVE_TO_NEAR_STALKER = 2, + EVENT_MOVE_TO_FAR_STALKER = 3, + EVENT_DESPAWN = 4 +}; + +struct npc_frightened_citizen : public PassiveAI +{ + npc_frightened_citizen(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* /*summoner*/) override + { + if (Creature* stalkerNear = me->FindNearestCreature(NPC_EVACUATION_STALKER_FIRST, 20.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FIRST, stalkerNear->GetPosition(), true); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + switch (id) + { + case POINT_STALKER_FIRST: + _events.ScheduleEvent(EVENT_TALK_FRIGHTENED, 1s); + break; + case POINT_STALKER_NEAR: + _events.ScheduleEvent(EVENT_MOVE_TO_FAR_STALKER, 1ms); + break; + case POINT_STALKER_FAR: + _events.ScheduleEvent(EVENT_DESPAWN, 1ms); + break; + default: + break; + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_TALK_FRIGHTENED: + if (Unit* summoner = me->ToTempSummon()->GetSummoner()) + { + if (Player* player = summoner->ToPlayer()) + { + player->KilledMonsterCredit(CREDIT_35830); + Talk(SAY_FRIGHTENED_CITIZEN_RESCUE, summoner); + } + } + _events.ScheduleEvent(EVENT_MOVE_TO_NEAR_STALKER, 2s); + break; + case EVENT_MOVE_TO_NEAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 50.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + else if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_NEAR, 100.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_NEAR, stalker->GetPosition(), true); + break; + case EVENT_MOVE_TO_FAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 500.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + break; + case EVENT_DESPAWN: + me->DespawnOrUnsummon(); + break; + default: + break; + } + } + } +private: + EventMap _events; +}; + +/*###### +## Quest 14154 - By the Skin of his Teeth +######*/ + +enum WorgenRunt +{ + EVENT_JUMP_TO_PRISON = 1, + EVENT_AGGRO_PLAYER = 2, + EVENT_FORCE_DESPAWN = 3, + + PHASE_ROOF = 0, + PHASE_COMBAT = 1, + + WORGEN_ID_ROOF_1 = 0, + WORGEN_ID_ROOF_2 = 1, + WORGEN_ID_ROOF_3 = 2, + WORGEN_ID_ROOF_4 = 3, + WORGEN_ID_ROOF_5 = 4, + WORGEN_ID_ROOF_6 = 5, + WORGEN_ID_ROOF_7 = 6, + + WORGEN_ID_CATHEDRAL_1 = 7, + WORGEN_ID_CATHEDRAL_2 = 8, + WORGEN_ID_CATHEDRAL_3 = 9, + WORGEN_ID_CATHEDRAL_4 = 10, + WORGEN_ID_CATHEDRAL_5 = 11, + WORGEN_ID_CATHEDRAL_6 = 12, + WORGEN_ID_CATHEDRAL_7 = 13, + WORGEN_ID_CATHEDRAL_8 = 14, + WORGEN_ID_CATHEDRAL_9 = 15, + + NPC_WORGEN_RUNT_SPELL = 35188, +}; + +static Position const RuntSpellSummonJumpPos = { -1671.915f, 1446.734f, 52.28712f }; + +struct npc_worgen_runt : public ScriptedAI +{ + npc_worgen_runt(Creature* creature) : ScriptedAI(creature), _worgenID(0), _wayPointCounter(0), _jumped(false) { } + + void JustEngagedWith(Unit* /*who*/) override + { + _events.SetPhase(PHASE_COMBAT); + } + + void IsSummonedBy(Unit* summoner) override + { + me->setActive(true); // we are in a phased and cut off map so we're fine to use that here + _events.SetPhase(PHASE_ROOF); + _events.ScheduleEvent(EVENT_FORCE_DESPAWN, 70s, 0, PHASE_ROOF); + _playerGuid = summoner->GetGUID(); + if (me->GetEntry() == NPC_WORGEN_RUNT_SPELL) + me->GetMotionMaster()->MoveJump(RuntSpellSummonJumpPos, 16.0f, 4.371286f); + } + + void DoAction(int32 action) override + { + _worgenID = action; + switch (_worgenID) // ugly as fuck but needed for individual controls + { + // House roof cases + case WORGEN_ID_ROOF_1: + _wayPointCounter = runtHousePathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize1, worgenRuntHousePath1, runtHousePathSize1); + break; + case WORGEN_ID_ROOF_2: + _wayPointCounter = runtHousePathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize2, worgenRuntHousePath2, runtHousePathSize2); + break; + case WORGEN_ID_ROOF_3: + _wayPointCounter = runtHousePathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize3, worgenRuntHousePath3, runtHousePathSize3); + break; + case WORGEN_ID_ROOF_4: + _wayPointCounter = runtHousePathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize4, worgenRuntHousePath4, runtHousePathSize4); + break; + case WORGEN_ID_ROOF_5: + _wayPointCounter = runtHousePathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize5, worgenRuntHousePath5, runtHousePathSize5); + break; + case WORGEN_ID_ROOF_6: + _wayPointCounter = runtHousePathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize6, worgenRuntHousePath6, runtHousePathSize6); + break; + case WORGEN_ID_ROOF_7: + _wayPointCounter = runtHousePathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize7, worgenRuntHousePath7, runtHousePathSize7); + break; + // Cathedral cases + case WORGEN_ID_CATHEDRAL_1: + _wayPointCounter = runtCathedralPathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize1, worgenRuntCathedralPath1, runtCathedralPathSize1); + break; + case WORGEN_ID_CATHEDRAL_2: + _wayPointCounter = runtCathedralPathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize2, worgenRuntCathedralPath1, runtCathedralPathSize2); + break; + case WORGEN_ID_CATHEDRAL_3: + _wayPointCounter = runtCathedralPathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize3, worgenRuntCathedralPath1, runtCathedralPathSize3); + break; + case WORGEN_ID_CATHEDRAL_4: + _wayPointCounter = runtCathedralPathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize4, worgenRuntCathedralPath1, runtCathedralPathSize4); + break; + case WORGEN_ID_CATHEDRAL_5: + _wayPointCounter = runtCathedralPathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize5, worgenRuntCathedralPath1, runtCathedralPathSize5); + break; + case WORGEN_ID_CATHEDRAL_6: + _wayPointCounter = runtCathedralPathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize6, worgenRuntCathedralPath1, runtCathedralPathSize6); + break; + case WORGEN_ID_CATHEDRAL_7: + _wayPointCounter = runtCathedralPathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize7, worgenRuntCathedralPath1, runtCathedralPathSize7); + break; + case WORGEN_ID_CATHEDRAL_8: + _wayPointCounter = runtCathedralPathSize8; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize8, worgenRuntCathedralPath1, runtCathedralPathSize8); + break; + } + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == _wayPointCounter && !_jumped) + { + _jumped = true; + _events.ScheduleEvent(EVENT_JUMP_TO_PRISON, 1ms); + } + } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim() && !_events.IsInPhase(PHASE_ROOF)) + return; + + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_FORCE_DESPAWN: + me->DespawnOrUnsummon(); + break; + case EVENT_JUMP_TO_PRISON: + me->GetMotionMaster()->MoveJump(worgenRuntJumpPos[_worgenID], 16.0f, _worgenID < WORGEN_ID_CATHEDRAL_1 ? 19.2911f : frand(3.945607f, 4.852813f)); + me->SetHomePosition(worgenRuntJumpPos[_worgenID]); + _events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s); + break; + case EVENT_AGGRO_PLAYER: + { + Creature* crowley = me->FindNearestCreature(35077, 100.0f, true); + Creature* tobias = me->FindNearestCreature(35124, 100.0f, true); + + if (crowley && tobias) + { + if (me->GetDistance(crowley) <= me->GetDistance(tobias)) + AttackStart(crowley); + else + AttackStart(tobias); + } + else if (crowley) + AttackStart(crowley); + else if (tobias) + AttackStart(tobias); + + break; + } + default: + break; + } + } + + DoMeleeAttackIfReady(); + } +private: + uint32 _worgenID; + uint32 _wayPointCounter; + bool _jumped; + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14159 - The Rebel Lord's Arsenal +######*/ + +enum TheRebelLordsArsena +{ + PHASE_ID_SUMMON = 170, + PHASE_ID_WOUND = 171, + NPC_LORNA_CROWLEY = 35378, + NPC_GENERIC_TRIGGER_LAB = 35374, + + SPELL_SHOOT_INSTAKILL = 67593, + SPELL_COSMETIC_ATTACK = 42880, + SPELL_PULL_TO = 67357, + + EVENT_COSMETIC_ATTACK = 1, + EVENT_JUMP_TO_PLAYER, + EVENT_SHOOT_JOSIAH +}; + +static Position const JosiahJumpPos = { -1796.63f, 1427.73f, 12.4624f }; + +struct npc_josiah_avery : public PassiveAI +{ + npc_josiah_avery(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* summoner) override + { + PhasingHandler::AddPhase(me, PHASE_ID_SUMMON, true); + PhasingHandler::AddPhase(me, PHASE_ID_WOUND, true); + _playerGuid = summoner->GetGUID(); + _events.ScheduleEvent(EVENT_COSMETIC_ATTACK, 500ms); + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_COSMETIC_ATTACK: + if (Player* player = ObjectAccessor::GetPlayer(*me, _playerGuid)) + { + DoCast(player, SPELL_COSMETIC_ATTACK); + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + if (Creature* labTrigger = lorna->FindNearestCreature(NPC_GENERIC_TRIGGER_LAB, 5.0f, true)) + labTrigger->CastSpell(player, SPELL_PULL_TO); + + _events.ScheduleEvent(EVENT_JUMP_TO_PLAYER, 1s); + } + break; + case EVENT_JUMP_TO_PLAYER: + me->GetMotionMaster()->MoveJump(JosiahJumpPos, 10.0f, 14.18636f); + _events.ScheduleEvent(EVENT_SHOOT_JOSIAH, 500ms); + break; + case EVENT_SHOOT_JOSIAH: + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + lorna->CastSpell(me, SPELL_SHOOT_INSTAKILL, true); + break; + default: + break; + } + } + } +private: + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14293 - Save Krennan Aranas +######*/ + +static Position const GreymanesHorseJumpPos = { -1676.16f, 1346.19f, 15.1349f }; + +enum GreymanesHorse +{ + SAY_ANNOUNCE_RESCUE = 0, + SAY_RESCUED = 0, + SAY_TRAPPED = 0, + + PATH_KRENNAN_TREE = 0, + PATH_KRENNAN_BACK = 1, + + EVENT_START_PATH_1 = 1, + EVENT_START_PATH_2, + EVENT_JUMP_TO_KRENNAN, + EVENT_ANNOUNCE_RESCUE, + EVENT_DISMOUNT_PLAYER, + + NPC_RESCUED_KRENNAN = 35907, + NPC_TRAPPED_KRENNAN = 35753, +}; + +struct npc_greymanes_horse : public VehicleAI +{ + npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_START_PATH_1, 1s); + } + else if (apply && passenger->GetEntry() == NPC_RESCUED_KRENNAN) + _events.ScheduleEvent(EVENT_START_PATH_2, 1s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == pathSize1 && _currentPath == PATH_KRENNAN_TREE) + _events.ScheduleEvent(EVENT_JUMP_TO_KRENNAN, 1ms); + else if (type == EFFECT_MOTION_TYPE && pointId == pathSize2 && _currentPath == PATH_KRENNAN_BACK) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_PATH_1: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_TREE; + me->GetMotionMaster()->MoveSmoothPath(pathSize1, greymanesHorsePath1, pathSize1); + break; + case EVENT_JUMP_TO_KRENNAN: + me->GetMotionMaster()->MoveJump(GreymanesHorseJumpPos, 16.0f, 14.00968f); + _events.ScheduleEvent(EVENT_ANNOUNCE_RESCUE, 2s); + break; + case EVENT_ANNOUNCE_RESCUE: + me->SetControlled(true, UNIT_STATE_ROOT); + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(0)) + { + Talk(SAY_ANNOUNCE_RESCUE, passenger); + if (Creature* krennan = me->FindNearestCreature(NPC_TRAPPED_KRENNAN, 30.0f, true)) + krennan->AI()->Talk(SAY_TRAPPED, passenger); + } + break; + case EVENT_START_PATH_2: + me->SetControlled(true, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_BACK; + me->GetMotionMaster()->MoveSmoothPath(pathSize2, greymanesHorsePath2, pathSize2); + break; + case EVENT_DISMOUNT_PLAYER: + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(1)) + { + if (Unit* player = me->GetVehicleKit()->GetPassenger(0)) + { + if (Creature* krennan = passenger->ToCreature()) + { + std::vector storedAttackers; + + for (Unit* attacker : me->getAttackers()) + storedAttackers.push_back(attacker); + + for (Unit* attacker : storedAttackers) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + player->ExitVehicle(); + krennan->AddUnitState(UNIT_STATE_ROOT); + krennan->ExitVehicle(); + krennan->AI()->Talk(SAY_RESCUED, me); + krennan->DespawnOrUnsummon(7s); + } + } + } + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +/*###### +## Quest 14212 - Sacrifices +######*/ + +enum CrowleysHorse +{ + EVENT_JUMP_OVER_BARRICADES_1 = 1, + EVENT_JUMP_OVER_BARRICADES_2, + EVENT_MOVE_PATH_MAIN_1, + EVENT_MOVE_PATH_MAIN_2, + EVENT_MOVE_OFF_PATH = 6, + + PATH_ID_CROWLEYS_HORSE_1 = 352310, + PATH_ID_CROWLEYS_HORSE_2 = 352311, + PATH_ID_CROWLEYS_HORSE_3 = 444280, + + SPELL_THROW_TORCH = 67063, + NPC_CROWLEYS_HORSE_2 = 44428, +}; + +static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f }; +static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f }; + +struct npc_crowleys_horse : public VehicleAI +{ + npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_1, 2s); + } + else if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_MOVE_OFF_PATH, 2s); + } + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == WAYPOINT_MOTION_TYPE && pointId == 14 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 15 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 16 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_JUMP_OVER_BARRICADES_1: + me->SetControlled(false, UNIT_STATE_ROOT); + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 2s); + break; + case EVENT_MOVE_PATH_MAIN_1: + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_1, false); + _currentPath = PATH_ID_CROWLEYS_HORSE_1; + break; + case EVENT_JUMP_OVER_BARRICADES_2: + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_2, 2s); + break; + case EVENT_MOVE_PATH_MAIN_2: + _currentPath = PATH_ID_CROWLEYS_HORSE_2; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_2, false); + break; + case EVENT_DISMOUNT_PLAYER: + { + std::set attackersCopy = me->getAttackers(); + for (Unit* attacker : attackersCopy) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + me->RemoveAurasDueToSpell(VEHICLE_SPELL_RIDE_HARDCODED); + me->DespawnOrUnsummon(5s); + break; + } + case EVENT_MOVE_OFF_PATH: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_ID_CROWLEYS_HORSE_3; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_3, false); + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +enum GileanCrow +{ + SPELL_GILNEAN_CROW = 93275, + EVENT_APPLY_HOVER_BYTES = 1, + EVENT_FLY_AWAY_1 = 2, + EVENT_FLY_AWAY_2 = 3, + + POINT_NONE = 0, + POINT_CROW_FLIGHT = 1 +}; + +struct npc_gilnean_crow : public PassiveAI +{ + npc_gilnean_crow(Creature* creature) : PassiveAI(creature) { } + + void SpellHit(WorldObject* /*caster*/, SpellInfo const* spell) override + { + if (spell->Id == SPELL_GILNEAN_CROW) + _events.ScheduleEvent(EVENT_APPLY_HOVER_BYTES, 500ms); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == POINT_MOTION_TYPE && pointId == POINT_CROW_FLIGHT) + { + Position pos = me->GetRandomNearPosition(8.0f); + pos.m_positionZ = me->GetPositionZ() + 10.0f; + me->GetMotionMaster()->MovePoint(POINT_NONE, pos); + me->DespawnOrUnsummon(14s); + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_APPLY_HOVER_BYTES: + me->SetStandState(UNIT_STAND_STATE_SIT); + me->SetAnimTier(AnimTier::Submerged); + _events.ScheduleEvent(EVENT_FLY_AWAY_1, 1s); + break; + case EVENT_FLY_AWAY_1: + { + Position pos = me->GetRandomNearPosition(7.0f); + pos.m_positionZ = me->GetPositionZ() + 8.0f; + me->GetMotionMaster()->MovePoint(POINT_CROW_FLIGHT, pos); + break; + } + default: + break; + } + } + } +private: + EventMap _events; +}; +} + +void AddSC_gilneas_chapter_1() +{ + using namespace Gilneas::Chapter1; + RegisterCreatureAI(npc_frightened_citizen); + RegisterCreatureAI(npc_worgen_runt); + RegisterCreatureAI(npc_josiah_avery); + RegisterCreatureAI(npc_greymanes_horse); + RegisterCreatureAI(npc_crowleys_horse); + RegisterCreatureAI(npc_gilnean_crow); +} diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-delaytest-20261001-165224 b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-delaytest-20261001-165224 new file mode 100644 index 0000000000..832d1f2b1a --- /dev/null +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-delaytest-20261001-165224 @@ -0,0 +1,704 @@ +/* +* 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 . +*/ + +#include "gilneas.h" +#include "ScriptMgr.h" +#include "CombatAI.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "PassiveAI.h" +#include "PhasingHandler.h" +#include "ScriptedCreature.h" +#include "SpellInfo.h" +#include "SpellScript.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +namespace Gilneas::Chapter1 +{ + +/*###### +## Quest 14098 - Evacuate the Merchant Square +######*/ + +enum FrightenedCitizen +{ + SAY_FRIGHTENED_CITIZEN_RESCUE = 0, + + NPC_EVACUATION_STALKER_FIRST = 35830, + NPC_EVACUATION_STALKER_NEAR = 35010, + NPC_EVACUATION_STALKER_FAR = 35011, + NPC_RAMPAGING_WORGEN = 35660, + + CREDIT_35830 = 35830, + + POINT_STALKER_FIRST = 1, + POINT_STALKER_NEAR = 2, + POINT_STALKER_FAR = 3, + + EVENT_TALK_FRIGHTENED = 1, + EVENT_MOVE_TO_NEAR_STALKER = 2, + EVENT_MOVE_TO_FAR_STALKER = 3, + EVENT_DESPAWN = 4 +}; + +struct npc_frightened_citizen : public PassiveAI +{ + npc_frightened_citizen(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* /*summoner*/) override + { + if (Creature* stalkerNear = me->FindNearestCreature(NPC_EVACUATION_STALKER_FIRST, 20.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FIRST, stalkerNear->GetPosition(), true); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + switch (id) + { + case POINT_STALKER_FIRST: + _events.ScheduleEvent(EVENT_TALK_FRIGHTENED, 1s); + break; + case POINT_STALKER_NEAR: + _events.ScheduleEvent(EVENT_MOVE_TO_FAR_STALKER, 1ms); + break; + case POINT_STALKER_FAR: + _events.ScheduleEvent(EVENT_DESPAWN, 1ms); + break; + default: + break; + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_TALK_FRIGHTENED: + if (Unit* summoner = me->ToTempSummon()->GetSummoner()) + { + if (Player* player = summoner->ToPlayer()) + { + player->KilledMonsterCredit(CREDIT_35830); + Talk(SAY_FRIGHTENED_CITIZEN_RESCUE, summoner); + } + } + _events.ScheduleEvent(EVENT_MOVE_TO_NEAR_STALKER, 2s); + break; + case EVENT_MOVE_TO_NEAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 50.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + else if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_NEAR, 100.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_NEAR, stalker->GetPosition(), true); + break; + case EVENT_MOVE_TO_FAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 500.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + break; + case EVENT_DESPAWN: + me->DespawnOrUnsummon(); + break; + default: + break; + } + } + } +private: + EventMap _events; +}; + +/*###### +## Quest 14154 - By the Skin of his Teeth +######*/ + +enum WorgenRunt +{ + EVENT_JUMP_TO_PRISON = 1, + EVENT_AGGRO_PLAYER = 2, + EVENT_FORCE_DESPAWN = 3, + + PHASE_ROOF = 0, + PHASE_COMBAT = 1, + + WORGEN_ID_ROOF_1 = 0, + WORGEN_ID_ROOF_2 = 1, + WORGEN_ID_ROOF_3 = 2, + WORGEN_ID_ROOF_4 = 3, + WORGEN_ID_ROOF_5 = 4, + WORGEN_ID_ROOF_6 = 5, + WORGEN_ID_ROOF_7 = 6, + + WORGEN_ID_CATHEDRAL_1 = 7, + WORGEN_ID_CATHEDRAL_2 = 8, + WORGEN_ID_CATHEDRAL_3 = 9, + WORGEN_ID_CATHEDRAL_4 = 10, + WORGEN_ID_CATHEDRAL_5 = 11, + WORGEN_ID_CATHEDRAL_6 = 12, + WORGEN_ID_CATHEDRAL_7 = 13, + WORGEN_ID_CATHEDRAL_8 = 14, + WORGEN_ID_CATHEDRAL_9 = 15, + + NPC_WORGEN_RUNT_SPELL = 35188, +}; + +static Position const RuntSpellSummonJumpPos = { -1671.915f, 1446.734f, 52.28712f }; + +struct npc_worgen_runt : public ScriptedAI +{ + npc_worgen_runt(Creature* creature) : ScriptedAI(creature), _worgenID(0), _wayPointCounter(0), _jumped(false) { } + + void JustEngagedWith(Unit* /*who*/) override + { + _events.SetPhase(PHASE_COMBAT); + } + + void IsSummonedBy(Unit* summoner) override + { + me->setActive(true); // we are in a phased and cut off map so we're fine to use that here + _events.SetPhase(PHASE_ROOF); + _events.ScheduleEvent(EVENT_FORCE_DESPAWN, 70s, 0, PHASE_ROOF); + _playerGuid = summoner->GetGUID(); + if (me->GetEntry() == NPC_WORGEN_RUNT_SPELL) + me->GetMotionMaster()->MoveJump(RuntSpellSummonJumpPos, 16.0f, 4.371286f); + } + + void DoAction(int32 action) override + { + _worgenID = action; + switch (_worgenID) // ugly as fuck but needed for individual controls + { + // House roof cases + case WORGEN_ID_ROOF_1: + _wayPointCounter = runtHousePathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize1, worgenRuntHousePath1, runtHousePathSize1); + break; + case WORGEN_ID_ROOF_2: + _wayPointCounter = runtHousePathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize2, worgenRuntHousePath2, runtHousePathSize2); + break; + case WORGEN_ID_ROOF_3: + _wayPointCounter = runtHousePathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize3, worgenRuntHousePath3, runtHousePathSize3); + break; + case WORGEN_ID_ROOF_4: + _wayPointCounter = runtHousePathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize4, worgenRuntHousePath4, runtHousePathSize4); + break; + case WORGEN_ID_ROOF_5: + _wayPointCounter = runtHousePathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize5, worgenRuntHousePath5, runtHousePathSize5); + break; + case WORGEN_ID_ROOF_6: + _wayPointCounter = runtHousePathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize6, worgenRuntHousePath6, runtHousePathSize6); + break; + case WORGEN_ID_ROOF_7: + _wayPointCounter = runtHousePathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize7, worgenRuntHousePath7, runtHousePathSize7); + break; + // Cathedral cases + case WORGEN_ID_CATHEDRAL_1: + _wayPointCounter = runtCathedralPathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize1, worgenRuntCathedralPath1, runtCathedralPathSize1); + break; + case WORGEN_ID_CATHEDRAL_2: + _wayPointCounter = runtCathedralPathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize2, worgenRuntCathedralPath1, runtCathedralPathSize2); + break; + case WORGEN_ID_CATHEDRAL_3: + _wayPointCounter = runtCathedralPathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize3, worgenRuntCathedralPath1, runtCathedralPathSize3); + break; + case WORGEN_ID_CATHEDRAL_4: + _wayPointCounter = runtCathedralPathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize4, worgenRuntCathedralPath1, runtCathedralPathSize4); + break; + case WORGEN_ID_CATHEDRAL_5: + _wayPointCounter = runtCathedralPathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize5, worgenRuntCathedralPath1, runtCathedralPathSize5); + break; + case WORGEN_ID_CATHEDRAL_6: + _wayPointCounter = runtCathedralPathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize6, worgenRuntCathedralPath1, runtCathedralPathSize6); + break; + case WORGEN_ID_CATHEDRAL_7: + _wayPointCounter = runtCathedralPathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize7, worgenRuntCathedralPath1, runtCathedralPathSize7); + break; + case WORGEN_ID_CATHEDRAL_8: + _wayPointCounter = runtCathedralPathSize8; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize8, worgenRuntCathedralPath1, runtCathedralPathSize8); + break; + } + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == _wayPointCounter && !_jumped) + { + _jumped = true; + _events.ScheduleEvent(EVENT_JUMP_TO_PRISON, 1ms); + } + } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim() && !_events.IsInPhase(PHASE_ROOF)) + return; + + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_FORCE_DESPAWN: + me->DespawnOrUnsummon(); + break; + case EVENT_JUMP_TO_PRISON: + me->GetMotionMaster()->MoveJump(worgenRuntJumpPos[_worgenID], 16.0f, _worgenID < WORGEN_ID_CATHEDRAL_1 ? 19.2911f : frand(3.945607f, 4.852813f)); + me->SetHomePosition(worgenRuntJumpPos[_worgenID]); + _events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s); + break; + case EVENT_AGGRO_PLAYER: + { + Creature* crowley = me->FindNearestCreature(35077, 100.0f, true); + Creature* tobias = me->FindNearestCreature(35124, 100.0f, true); + + if (crowley && tobias) + { + if (me->GetDistance(crowley) <= me->GetDistance(tobias)) + AttackStart(crowley); + else + AttackStart(tobias); + } + else if (crowley) + AttackStart(crowley); + else if (tobias) + AttackStart(tobias); + + break; + } + default: + break; + } + } + + DoMeleeAttackIfReady(); + } +private: + uint32 _worgenID; + uint32 _wayPointCounter; + bool _jumped; + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14159 - The Rebel Lord's Arsenal +######*/ + +enum TheRebelLordsArsena +{ + PHASE_ID_SUMMON = 170, + PHASE_ID_WOUND = 171, + NPC_LORNA_CROWLEY = 35378, + NPC_GENERIC_TRIGGER_LAB = 35374, + + SPELL_SHOOT_INSTAKILL = 67593, + SPELL_COSMETIC_ATTACK = 42880, + SPELL_PULL_TO = 67357, + + EVENT_COSMETIC_ATTACK = 1, + EVENT_JUMP_TO_PLAYER, + EVENT_SHOOT_JOSIAH +}; + +static Position const JosiahJumpPos = { -1796.63f, 1427.73f, 12.4624f }; + +struct npc_josiah_avery : public PassiveAI +{ + npc_josiah_avery(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* summoner) override + { + PhasingHandler::AddPhase(me, PHASE_ID_SUMMON, true); + PhasingHandler::AddPhase(me, PHASE_ID_WOUND, true); + _playerGuid = summoner->GetGUID(); + _events.ScheduleEvent(EVENT_COSMETIC_ATTACK, 500ms); + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_COSMETIC_ATTACK: + if (Player* player = ObjectAccessor::GetPlayer(*me, _playerGuid)) + { + DoCast(player, SPELL_COSMETIC_ATTACK); + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + if (Creature* labTrigger = lorna->FindNearestCreature(NPC_GENERIC_TRIGGER_LAB, 5.0f, true)) + labTrigger->CastSpell(player, SPELL_PULL_TO); + + _events.ScheduleEvent(EVENT_JUMP_TO_PLAYER, 1s); + } + break; + case EVENT_JUMP_TO_PLAYER: + me->GetMotionMaster()->MoveJump(JosiahJumpPos, 10.0f, 14.18636f); + _events.ScheduleEvent(EVENT_SHOOT_JOSIAH, 500ms); + break; + case EVENT_SHOOT_JOSIAH: + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + lorna->CastSpell(me, SPELL_SHOOT_INSTAKILL, true); + break; + default: + break; + } + } + } +private: + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14293 - Save Krennan Aranas +######*/ + +static Position const GreymanesHorseJumpPos = { -1676.16f, 1346.19f, 15.1349f }; + +enum GreymanesHorse +{ + SAY_ANNOUNCE_RESCUE = 0, + SAY_RESCUED = 0, + SAY_TRAPPED = 0, + + PATH_KRENNAN_TREE = 0, + PATH_KRENNAN_BACK = 1, + + EVENT_START_PATH_1 = 1, + EVENT_START_PATH_2, + EVENT_JUMP_TO_KRENNAN, + EVENT_ANNOUNCE_RESCUE, + EVENT_DISMOUNT_PLAYER, + + NPC_RESCUED_KRENNAN = 35907, + NPC_TRAPPED_KRENNAN = 35753, +}; + +struct npc_greymanes_horse : public VehicleAI +{ + npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { } + + void DamageTaken(Unit* /*attacker*/, uint32& damage) override + { + if (me->GetHealth() <= 1) + damage = 0; + else if (damage >= me->GetHealth()) + damage = me->GetHealth() - 1; + } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_START_PATH_1, 1s); + } + else if (apply && passenger->GetEntry() == NPC_RESCUED_KRENNAN) + _events.ScheduleEvent(EVENT_START_PATH_2, 1s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == pathSize1 && _currentPath == PATH_KRENNAN_TREE) + _events.ScheduleEvent(EVENT_JUMP_TO_KRENNAN, 1ms); + else if (type == EFFECT_MOTION_TYPE && pointId == pathSize2 && _currentPath == PATH_KRENNAN_BACK) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_PATH_1: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_TREE; + me->GetMotionMaster()->MoveSmoothPath(pathSize1, greymanesHorsePath1, pathSize1); + break; + case EVENT_JUMP_TO_KRENNAN: + me->GetMotionMaster()->MoveJump(GreymanesHorseJumpPos, 16.0f, 14.00968f); + _events.ScheduleEvent(EVENT_ANNOUNCE_RESCUE, 2s); + break; + case EVENT_ANNOUNCE_RESCUE: + me->SetControlled(true, UNIT_STATE_ROOT); + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(0)) + { + Talk(SAY_ANNOUNCE_RESCUE, passenger); + if (Creature* krennan = me->FindNearestCreature(NPC_TRAPPED_KRENNAN, 30.0f, true)) + krennan->AI()->Talk(SAY_TRAPPED, passenger); + } + break; + case EVENT_START_PATH_2: + me->SetControlled(true, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_BACK; + me->GetMotionMaster()->MoveSmoothPath(pathSize2, greymanesHorsePath2, pathSize2); + break; + case EVENT_DISMOUNT_PLAYER: + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(1)) + { + if (Unit* player = me->GetVehicleKit()->GetPassenger(0)) + { + if (Creature* krennan = passenger->ToCreature()) + { + std::vector storedAttackers; + + for (Unit* attacker : me->getAttackers()) + storedAttackers.push_back(attacker); + + for (Unit* attacker : storedAttackers) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + player->ExitVehicle(); + krennan->AddUnitState(UNIT_STATE_ROOT); + krennan->ExitVehicle(); + krennan->AI()->Talk(SAY_RESCUED, me); + krennan->DespawnOrUnsummon(7s); + } + } + } + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +/*###### +## Quest 14212 - Sacrifices +######*/ + +enum CrowleysHorse +{ + EVENT_JUMP_OVER_BARRICADES_1 = 1, + EVENT_JUMP_OVER_BARRICADES_2, + EVENT_MOVE_PATH_MAIN_1, + EVENT_MOVE_PATH_MAIN_2, + EVENT_MOVE_OFF_PATH = 6, + + PATH_ID_CROWLEYS_HORSE_1 = 352310, + PATH_ID_CROWLEYS_HORSE_2 = 352311, + PATH_ID_CROWLEYS_HORSE_3 = 444280, + + SPELL_THROW_TORCH = 67063, + NPC_CROWLEYS_HORSE_2 = 44428, +}; + +static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f }; +static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f }; + +struct npc_crowleys_horse : public VehicleAI +{ + npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_1, 2s); + } + else if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_MOVE_OFF_PATH, 2s); + } + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == WAYPOINT_MOTION_TYPE && pointId == 14 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 15 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 16 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_JUMP_OVER_BARRICADES_1: + me->SetControlled(false, UNIT_STATE_ROOT); + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 2s); + break; + case EVENT_MOVE_PATH_MAIN_1: + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_1, false); + _currentPath = PATH_ID_CROWLEYS_HORSE_1; + break; + case EVENT_JUMP_OVER_BARRICADES_2: + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_2, 2s); + break; + case EVENT_MOVE_PATH_MAIN_2: + _currentPath = PATH_ID_CROWLEYS_HORSE_2; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_2, false); + break; + case EVENT_DISMOUNT_PLAYER: + { + std::set attackersCopy = me->getAttackers(); + for (Unit* attacker : attackersCopy) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + me->RemoveAurasDueToSpell(VEHICLE_SPELL_RIDE_HARDCODED); + me->DespawnOrUnsummon(5s); + break; + } + case EVENT_MOVE_OFF_PATH: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_ID_CROWLEYS_HORSE_3; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_3, false); + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +enum GileanCrow +{ + SPELL_GILNEAN_CROW = 93275, + EVENT_APPLY_HOVER_BYTES = 1, + EVENT_FLY_AWAY_1 = 2, + EVENT_FLY_AWAY_2 = 3, + + POINT_NONE = 0, + POINT_CROW_FLIGHT = 1 +}; + +struct npc_gilnean_crow : public PassiveAI +{ + npc_gilnean_crow(Creature* creature) : PassiveAI(creature) { } + + void SpellHit(WorldObject* /*caster*/, SpellInfo const* spell) override + { + if (spell->Id == SPELL_GILNEAN_CROW) + _events.ScheduleEvent(EVENT_APPLY_HOVER_BYTES, 500ms); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == POINT_MOTION_TYPE && pointId == POINT_CROW_FLIGHT) + { + Position pos = me->GetRandomNearPosition(8.0f); + pos.m_positionZ = me->GetPositionZ() + 10.0f; + me->GetMotionMaster()->MovePoint(POINT_NONE, pos); + me->DespawnOrUnsummon(14s); + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_APPLY_HOVER_BYTES: + me->SetStandState(UNIT_STAND_STATE_SIT); + me->SetAnimTier(AnimTier::Submerged); + _events.ScheduleEvent(EVENT_FLY_AWAY_1, 1s); + break; + case EVENT_FLY_AWAY_1: + { + Position pos = me->GetRandomNearPosition(7.0f); + pos.m_positionZ = me->GetPositionZ() + 8.0f; + me->GetMotionMaster()->MovePoint(POINT_CROW_FLIGHT, pos); + break; + } + default: + break; + } + } + } +private: + EventMap _events; +}; +} + +void AddSC_gilneas_chapter_1() +{ + using namespace Gilneas::Chapter1; + RegisterCreatureAI(npc_frightened_citizen); + RegisterCreatureAI(npc_worgen_runt); + RegisterCreatureAI(npc_josiah_avery); + RegisterCreatureAI(npc_greymanes_horse); + RegisterCreatureAI(npc_crowleys_horse); + RegisterCreatureAI(npc_gilnean_crow); +} diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-full-smoothpath-20261001-183320 b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-full-smoothpath-20261001-183320 new file mode 100644 index 0000000000..86c027c848 --- /dev/null +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-full-smoothpath-20261001-183320 @@ -0,0 +1,727 @@ +/* +* 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 . +*/ + +#include "gilneas.h" +#include "ScriptMgr.h" +#include "CombatAI.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "PassiveAI.h" +#include "PhasingHandler.h" +#include "ScriptedCreature.h" +#include "SpellInfo.h" +#include "SpellScript.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +namespace Gilneas::Chapter1 +{ + +/*###### +## Quest 14098 - Evacuate the Merchant Square +######*/ + +enum FrightenedCitizen +{ + SAY_FRIGHTENED_CITIZEN_RESCUE = 0, + + NPC_EVACUATION_STALKER_FIRST = 35830, + NPC_EVACUATION_STALKER_NEAR = 35010, + NPC_EVACUATION_STALKER_FAR = 35011, + NPC_RAMPAGING_WORGEN = 35660, + + CREDIT_35830 = 35830, + + POINT_STALKER_FIRST = 1, + POINT_STALKER_NEAR = 2, + POINT_STALKER_FAR = 3, + + EVENT_TALK_FRIGHTENED = 1, + EVENT_MOVE_TO_NEAR_STALKER = 2, + EVENT_MOVE_TO_FAR_STALKER = 3, + EVENT_DESPAWN = 4 +}; + +struct npc_frightened_citizen : public PassiveAI +{ + npc_frightened_citizen(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* /*summoner*/) override + { + if (Creature* stalkerNear = me->FindNearestCreature(NPC_EVACUATION_STALKER_FIRST, 20.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FIRST, stalkerNear->GetPosition(), true); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + switch (id) + { + case POINT_STALKER_FIRST: + _events.ScheduleEvent(EVENT_TALK_FRIGHTENED, 1s); + break; + case POINT_STALKER_NEAR: + _events.ScheduleEvent(EVENT_MOVE_TO_FAR_STALKER, 1ms); + break; + case POINT_STALKER_FAR: + _events.ScheduleEvent(EVENT_DESPAWN, 1ms); + break; + default: + break; + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_TALK_FRIGHTENED: + if (Unit* summoner = me->ToTempSummon()->GetSummoner()) + { + if (Player* player = summoner->ToPlayer()) + { + player->KilledMonsterCredit(CREDIT_35830); + Talk(SAY_FRIGHTENED_CITIZEN_RESCUE, summoner); + } + } + _events.ScheduleEvent(EVENT_MOVE_TO_NEAR_STALKER, 2s); + break; + case EVENT_MOVE_TO_NEAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 50.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + else if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_NEAR, 100.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_NEAR, stalker->GetPosition(), true); + break; + case EVENT_MOVE_TO_FAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 500.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + break; + case EVENT_DESPAWN: + me->DespawnOrUnsummon(); + break; + default: + break; + } + } + } +private: + EventMap _events; +}; + +/*###### +## Quest 14154 - By the Skin of his Teeth +######*/ + +enum WorgenRunt +{ + EVENT_JUMP_TO_PRISON = 1, + EVENT_AGGRO_PLAYER = 2, + EVENT_FORCE_DESPAWN = 3, + + PHASE_ROOF = 0, + PHASE_COMBAT = 1, + + WORGEN_ID_ROOF_1 = 0, + WORGEN_ID_ROOF_2 = 1, + WORGEN_ID_ROOF_3 = 2, + WORGEN_ID_ROOF_4 = 3, + WORGEN_ID_ROOF_5 = 4, + WORGEN_ID_ROOF_6 = 5, + WORGEN_ID_ROOF_7 = 6, + + WORGEN_ID_CATHEDRAL_1 = 7, + WORGEN_ID_CATHEDRAL_2 = 8, + WORGEN_ID_CATHEDRAL_3 = 9, + WORGEN_ID_CATHEDRAL_4 = 10, + WORGEN_ID_CATHEDRAL_5 = 11, + WORGEN_ID_CATHEDRAL_6 = 12, + WORGEN_ID_CATHEDRAL_7 = 13, + WORGEN_ID_CATHEDRAL_8 = 14, + WORGEN_ID_CATHEDRAL_9 = 15, + + NPC_WORGEN_RUNT_SPELL = 35188, +}; + +static Position const RuntSpellSummonJumpPos = { -1671.915f, 1446.734f, 52.28712f }; + +struct npc_worgen_runt : public ScriptedAI +{ + npc_worgen_runt(Creature* creature) : ScriptedAI(creature), _worgenID(0), _wayPointCounter(0), _jumped(false) { } + + void JustEngagedWith(Unit* /*who*/) override + { + _events.SetPhase(PHASE_COMBAT); + } + + void IsSummonedBy(Unit* summoner) override + { + me->setActive(true); // we are in a phased and cut off map so we're fine to use that here + _events.SetPhase(PHASE_ROOF); + _events.ScheduleEvent(EVENT_FORCE_DESPAWN, 70s, 0, PHASE_ROOF); + _playerGuid = summoner->GetGUID(); + if (me->GetEntry() == NPC_WORGEN_RUNT_SPELL) + me->GetMotionMaster()->MoveJump(RuntSpellSummonJumpPos, 16.0f, 4.371286f); + } + + void DoAction(int32 action) override + { + _worgenID = action; + switch (_worgenID) // ugly as fuck but needed for individual controls + { + // House roof cases + case WORGEN_ID_ROOF_1: + _wayPointCounter = runtHousePathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize1, worgenRuntHousePath1, runtHousePathSize1); + break; + case WORGEN_ID_ROOF_2: + _wayPointCounter = runtHousePathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize2, worgenRuntHousePath2, runtHousePathSize2); + break; + case WORGEN_ID_ROOF_3: + _wayPointCounter = runtHousePathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize3, worgenRuntHousePath3, runtHousePathSize3); + break; + case WORGEN_ID_ROOF_4: + _wayPointCounter = runtHousePathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize4, worgenRuntHousePath4, runtHousePathSize4); + break; + case WORGEN_ID_ROOF_5: + _wayPointCounter = runtHousePathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize5, worgenRuntHousePath5, runtHousePathSize5); + break; + case WORGEN_ID_ROOF_6: + _wayPointCounter = runtHousePathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize6, worgenRuntHousePath6, runtHousePathSize6); + break; + case WORGEN_ID_ROOF_7: + _wayPointCounter = runtHousePathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize7, worgenRuntHousePath7, runtHousePathSize7); + break; + // Cathedral cases + case WORGEN_ID_CATHEDRAL_1: + _wayPointCounter = runtCathedralPathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize1, worgenRuntCathedralPath1, runtCathedralPathSize1); + break; + case WORGEN_ID_CATHEDRAL_2: + _wayPointCounter = runtCathedralPathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize2, worgenRuntCathedralPath1, runtCathedralPathSize2); + break; + case WORGEN_ID_CATHEDRAL_3: + _wayPointCounter = runtCathedralPathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize3, worgenRuntCathedralPath1, runtCathedralPathSize3); + break; + case WORGEN_ID_CATHEDRAL_4: + _wayPointCounter = runtCathedralPathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize4, worgenRuntCathedralPath1, runtCathedralPathSize4); + break; + case WORGEN_ID_CATHEDRAL_5: + _wayPointCounter = runtCathedralPathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize5, worgenRuntCathedralPath1, runtCathedralPathSize5); + break; + case WORGEN_ID_CATHEDRAL_6: + _wayPointCounter = runtCathedralPathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize6, worgenRuntCathedralPath1, runtCathedralPathSize6); + break; + case WORGEN_ID_CATHEDRAL_7: + _wayPointCounter = runtCathedralPathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize7, worgenRuntCathedralPath1, runtCathedralPathSize7); + break; + case WORGEN_ID_CATHEDRAL_8: + _wayPointCounter = runtCathedralPathSize8; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize8, worgenRuntCathedralPath1, runtCathedralPathSize8); + break; + } + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == _wayPointCounter && !_jumped) + { + _jumped = true; + _events.ScheduleEvent(EVENT_JUMP_TO_PRISON, 1ms); + } + } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim() && !_events.IsInPhase(PHASE_ROOF)) + return; + + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_FORCE_DESPAWN: + me->DespawnOrUnsummon(); + break; + case EVENT_JUMP_TO_PRISON: + me->GetMotionMaster()->MoveJump(worgenRuntJumpPos[_worgenID], 16.0f, _worgenID < WORGEN_ID_CATHEDRAL_1 ? 19.2911f : frand(3.945607f, 4.852813f)); + me->SetHomePosition(worgenRuntJumpPos[_worgenID]); + _events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s); + break; + case EVENT_AGGRO_PLAYER: + { + Creature* crowley = me->FindNearestCreature(35077, 100.0f, true); + Creature* tobias = me->FindNearestCreature(35124, 100.0f, true); + + if (crowley && tobias) + { + if (me->GetDistance(crowley) <= me->GetDistance(tobias)) + AttackStart(crowley); + else + AttackStart(tobias); + } + else if (crowley) + AttackStart(crowley); + else if (tobias) + AttackStart(tobias); + + break; + } + default: + break; + } + } + + DoMeleeAttackIfReady(); + } +private: + uint32 _worgenID; + uint32 _wayPointCounter; + bool _jumped; + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14159 - The Rebel Lord's Arsenal +######*/ + +enum TheRebelLordsArsena +{ + PHASE_ID_SUMMON = 170, + PHASE_ID_WOUND = 171, + NPC_LORNA_CROWLEY = 35378, + NPC_GENERIC_TRIGGER_LAB = 35374, + + SPELL_SHOOT_INSTAKILL = 67593, + SPELL_COSMETIC_ATTACK = 42880, + SPELL_PULL_TO = 67357, + + EVENT_COSMETIC_ATTACK = 1, + EVENT_JUMP_TO_PLAYER, + EVENT_SHOOT_JOSIAH +}; + +static Position const JosiahJumpPos = { -1796.63f, 1427.73f, 12.4624f }; + +struct npc_josiah_avery : public PassiveAI +{ + npc_josiah_avery(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* summoner) override + { + PhasingHandler::AddPhase(me, PHASE_ID_SUMMON, true); + PhasingHandler::AddPhase(me, PHASE_ID_WOUND, true); + _playerGuid = summoner->GetGUID(); + _events.ScheduleEvent(EVENT_COSMETIC_ATTACK, 500ms); + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_COSMETIC_ATTACK: + if (Player* player = ObjectAccessor::GetPlayer(*me, _playerGuid)) + { + DoCast(player, SPELL_COSMETIC_ATTACK); + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + if (Creature* labTrigger = lorna->FindNearestCreature(NPC_GENERIC_TRIGGER_LAB, 5.0f, true)) + labTrigger->CastSpell(player, SPELL_PULL_TO); + + _events.ScheduleEvent(EVENT_JUMP_TO_PLAYER, 1s); + } + break; + case EVENT_JUMP_TO_PLAYER: + me->GetMotionMaster()->MoveJump(JosiahJumpPos, 10.0f, 14.18636f); + _events.ScheduleEvent(EVENT_SHOOT_JOSIAH, 500ms); + break; + case EVENT_SHOOT_JOSIAH: + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + lorna->CastSpell(me, SPELL_SHOOT_INSTAKILL, true); + break; + default: + break; + } + } + } +private: + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14293 - Save Krennan Aranas +######*/ + +static Position const GreymanesHorseJumpPos = { -1676.16f, 1346.19f, 15.1349f }; + +enum GreymanesHorse +{ + SAY_ANNOUNCE_RESCUE = 0, + SAY_RESCUED = 0, + SAY_TRAPPED = 0, + + PATH_KRENNAN_TREE = 0, + PATH_KRENNAN_BACK = 1, + + EVENT_START_PATH_1 = 1, + EVENT_START_PATH_2, + EVENT_JUMP_TO_KRENNAN, + EVENT_ANNOUNCE_RESCUE, + EVENT_DISMOUNT_PLAYER, + + NPC_RESCUED_KRENNAN = 35907, + NPC_TRAPPED_KRENNAN = 35753, +}; + +struct npc_greymanes_horse : public VehicleAI +{ + npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { } + + void DamageTaken(Unit* /*attacker*/, uint32& damage) override + { + if (me->GetHealth() <= 1) + damage = 0; + else if (damage >= me->GetHealth()) + damage = me->GetHealth() - 1; + } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_START_PATH_1, 1s); + } + else if (apply && passenger->GetEntry() == NPC_RESCUED_KRENNAN) + _events.ScheduleEvent(EVENT_START_PATH_2, 1s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == pathSize1 && _currentPath == PATH_KRENNAN_TREE) + _events.ScheduleEvent(EVENT_JUMP_TO_KRENNAN, 1ms); + else if (type == EFFECT_MOTION_TYPE && pointId == pathSize2 && _currentPath == PATH_KRENNAN_BACK) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_PATH_1: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_TREE; + me->GetMotionMaster()->MoveSmoothPath(pathSize1, greymanesHorsePath1, pathSize1); + break; + case EVENT_JUMP_TO_KRENNAN: + me->GetMotionMaster()->MoveJump(GreymanesHorseJumpPos, 16.0f, 14.00968f); + _events.ScheduleEvent(EVENT_ANNOUNCE_RESCUE, 2s); + break; + case EVENT_ANNOUNCE_RESCUE: + me->SetControlled(true, UNIT_STATE_ROOT); + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(0)) + { + Talk(SAY_ANNOUNCE_RESCUE, passenger); + if (Creature* krennan = me->FindNearestCreature(NPC_TRAPPED_KRENNAN, 30.0f, true)) + krennan->AI()->Talk(SAY_TRAPPED, passenger); + } + break; + case EVENT_START_PATH_2: + me->SetControlled(true, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_BACK; + me->GetMotionMaster()->MoveSmoothPath(pathSize2, greymanesHorsePath2, pathSize2); + break; + case EVENT_DISMOUNT_PLAYER: + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(1)) + { + if (Unit* player = me->GetVehicleKit()->GetPassenger(0)) + { + if (Creature* krennan = passenger->ToCreature()) + { + std::vector storedAttackers; + + for (Unit* attacker : me->getAttackers()) + storedAttackers.push_back(attacker); + + for (Unit* attacker : storedAttackers) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + player->ExitVehicle(); + krennan->AddUnitState(UNIT_STATE_ROOT); + krennan->ExitVehicle(); + krennan->AI()->Talk(SAY_RESCUED, me); + krennan->DespawnOrUnsummon(7s); + } + } + } + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +/*###### +## Quest 14212 - Sacrifices +######*/ + +enum CrowleysHorse +{ + EVENT_JUMP_OVER_BARRICADES_1 = 1, + EVENT_JUMP_OVER_BARRICADES_2, + EVENT_MOVE_PATH_MAIN_1, + EVENT_MOVE_PATH_MAIN_2, + EVENT_MOVE_OFF_PATH = 6, + + PATH_ID_CROWLEYS_HORSE_1 = 352310, + PATH_ID_CROWLEYS_HORSE_2 = 352311, + PATH_ID_CROWLEYS_HORSE_3 = 444280, + + SPELL_THROW_TORCH = 67063, + NPC_CROWLEYS_HORSE_2 = 44428, +}; + +static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f }; +static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f }; + +static Position const CrowleysHorsePath1[] = +{ + { -1686.12f, 1655.64f, 20.6106f }, + { -1668.16f, 1639.43f, 20.6106f }, + { -1666.63f, 1628.86f, 20.5327f }, + { -1670.76f, 1617.43f, 20.6106f }, + { -1693.65f, 1592.64f, 20.6106f }, + { -1708.02f, 1590.55f, 20.6104f }, + { -1719.37f, 1595.65f, 20.6104f }, + { -1720.57f, 1610.14f, 20.6074f }, + { -1711.90f, 1629.70f, 20.6074f }, + { -1698.15f, 1656.48f, 20.6106f }, + { -1696.42f, 1682.01f, 20.5262f }, + { -1684.68f, 1700.12f, 20.4648f }, + { -1658.31f, 1710.31f, 20.5898f }, + { -1625.48f, 1712.30f, 21.8051f }, + { -1592.09f, 1710.71f, 20.5899f } +}; + + +struct npc_crowleys_horse : public VehicleAI +{ + npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_1, 2s); + } + else if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_MOVE_OFF_PATH, 2s); + } + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == PATH_ID_CROWLEYS_HORSE_1 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 15 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 16 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_JUMP_OVER_BARRICADES_1: + me->SetControlled(false, UNIT_STATE_ROOT); + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 2s); + break; + case EVENT_MOVE_PATH_MAIN_1: + _currentPath = PATH_ID_CROWLEYS_HORSE_1; + me->GetMotionMaster()->MoveSmoothPath( + PATH_ID_CROWLEYS_HORSE_1, + CrowleysHorsePath1, + 15); + break; + case EVENT_JUMP_OVER_BARRICADES_2: + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_2, 2s); + break; + case EVENT_MOVE_PATH_MAIN_2: + _currentPath = PATH_ID_CROWLEYS_HORSE_2; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_2, false); + break; + case EVENT_DISMOUNT_PLAYER: + { + std::set attackersCopy = me->getAttackers(); + for (Unit* attacker : attackersCopy) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + me->RemoveAurasDueToSpell(VEHICLE_SPELL_RIDE_HARDCODED); + me->DespawnOrUnsummon(5s); + break; + } + case EVENT_MOVE_OFF_PATH: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_ID_CROWLEYS_HORSE_3; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_3, false); + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +enum GileanCrow +{ + SPELL_GILNEAN_CROW = 93275, + EVENT_APPLY_HOVER_BYTES = 1, + EVENT_FLY_AWAY_1 = 2, + EVENT_FLY_AWAY_2 = 3, + + POINT_NONE = 0, + POINT_CROW_FLIGHT = 1 +}; + +struct npc_gilnean_crow : public PassiveAI +{ + npc_gilnean_crow(Creature* creature) : PassiveAI(creature) { } + + void SpellHit(WorldObject* /*caster*/, SpellInfo const* spell) override + { + if (spell->Id == SPELL_GILNEAN_CROW) + _events.ScheduleEvent(EVENT_APPLY_HOVER_BYTES, 500ms); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == POINT_MOTION_TYPE && pointId == POINT_CROW_FLIGHT) + { + Position pos = me->GetRandomNearPosition(8.0f); + pos.m_positionZ = me->GetPositionZ() + 10.0f; + me->GetMotionMaster()->MovePoint(POINT_NONE, pos); + me->DespawnOrUnsummon(14s); + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_APPLY_HOVER_BYTES: + me->SetStandState(UNIT_STAND_STATE_SIT); + me->SetAnimTier(AnimTier::Submerged); + _events.ScheduleEvent(EVENT_FLY_AWAY_1, 1s); + break; + case EVENT_FLY_AWAY_1: + { + Position pos = me->GetRandomNearPosition(7.0f); + pos.m_positionZ = me->GetPositionZ() + 8.0f; + me->GetMotionMaster()->MovePoint(POINT_CROW_FLIGHT, pos); + break; + } + default: + break; + } + } + } +private: + EventMap _events; +}; +} + +void AddSC_gilneas_chapter_1() +{ + using namespace Gilneas::Chapter1; + RegisterCreatureAI(npc_frightened_citizen); + RegisterCreatureAI(npc_worgen_runt); + RegisterCreatureAI(npc_josiah_avery); + RegisterCreatureAI(npc_greymanes_horse); + RegisterCreatureAI(npc_crowleys_horse); + RegisterCreatureAI(npc_gilnean_crow); +} diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-horse-1hp-20261001-195837 b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-horse-1hp-20261001-195837 new file mode 100644 index 0000000000..3aa11a6b44 --- /dev/null +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-horse-1hp-20261001-195837 @@ -0,0 +1,774 @@ +/* +* 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 . +*/ + +#include "gilneas.h" +#include "ScriptMgr.h" +#include "CombatAI.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "PassiveAI.h" +#include "PhasingHandler.h" +#include "ScriptedCreature.h" +#include "SpellInfo.h" +#include "SpellScript.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +namespace Gilneas::Chapter1 +{ + +/*###### +## Quest 14098 - Evacuate the Merchant Square +######*/ + +enum FrightenedCitizen +{ + SAY_FRIGHTENED_CITIZEN_RESCUE = 0, + + NPC_EVACUATION_STALKER_FIRST = 35830, + NPC_EVACUATION_STALKER_NEAR = 35010, + NPC_EVACUATION_STALKER_FAR = 35011, + NPC_RAMPAGING_WORGEN = 35660, + + CREDIT_35830 = 35830, + + POINT_STALKER_FIRST = 1, + POINT_STALKER_NEAR = 2, + POINT_STALKER_FAR = 3, + + EVENT_TALK_FRIGHTENED = 1, + EVENT_MOVE_TO_NEAR_STALKER = 2, + EVENT_MOVE_TO_FAR_STALKER = 3, + EVENT_DESPAWN = 4 +}; + +struct npc_frightened_citizen : public PassiveAI +{ + npc_frightened_citizen(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* /*summoner*/) override + { + if (Creature* stalkerNear = me->FindNearestCreature(NPC_EVACUATION_STALKER_FIRST, 20.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FIRST, stalkerNear->GetPosition(), true); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + switch (id) + { + case POINT_STALKER_FIRST: + _events.ScheduleEvent(EVENT_TALK_FRIGHTENED, 1s); + break; + case POINT_STALKER_NEAR: + _events.ScheduleEvent(EVENT_MOVE_TO_FAR_STALKER, 1ms); + break; + case POINT_STALKER_FAR: + _events.ScheduleEvent(EVENT_DESPAWN, 1ms); + break; + default: + break; + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_TALK_FRIGHTENED: + if (Unit* summoner = me->ToTempSummon()->GetSummoner()) + { + if (Player* player = summoner->ToPlayer()) + { + player->KilledMonsterCredit(CREDIT_35830); + Talk(SAY_FRIGHTENED_CITIZEN_RESCUE, summoner); + } + } + _events.ScheduleEvent(EVENT_MOVE_TO_NEAR_STALKER, 2s); + break; + case EVENT_MOVE_TO_NEAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 50.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + else if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_NEAR, 100.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_NEAR, stalker->GetPosition(), true); + break; + case EVENT_MOVE_TO_FAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 500.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + break; + case EVENT_DESPAWN: + me->DespawnOrUnsummon(); + break; + default: + break; + } + } + } +private: + EventMap _events; +}; + +/*###### +## Quest 14154 - By the Skin of his Teeth +######*/ + +enum WorgenRunt +{ + EVENT_JUMP_TO_PRISON = 1, + EVENT_AGGRO_PLAYER = 2, + EVENT_FORCE_DESPAWN = 3, + + PHASE_ROOF = 0, + PHASE_COMBAT = 1, + + WORGEN_ID_ROOF_1 = 0, + WORGEN_ID_ROOF_2 = 1, + WORGEN_ID_ROOF_3 = 2, + WORGEN_ID_ROOF_4 = 3, + WORGEN_ID_ROOF_5 = 4, + WORGEN_ID_ROOF_6 = 5, + WORGEN_ID_ROOF_7 = 6, + + WORGEN_ID_CATHEDRAL_1 = 7, + WORGEN_ID_CATHEDRAL_2 = 8, + WORGEN_ID_CATHEDRAL_3 = 9, + WORGEN_ID_CATHEDRAL_4 = 10, + WORGEN_ID_CATHEDRAL_5 = 11, + WORGEN_ID_CATHEDRAL_6 = 12, + WORGEN_ID_CATHEDRAL_7 = 13, + WORGEN_ID_CATHEDRAL_8 = 14, + WORGEN_ID_CATHEDRAL_9 = 15, + + NPC_WORGEN_RUNT_SPELL = 35188, +}; + +static Position const RuntSpellSummonJumpPos = { -1671.915f, 1446.734f, 52.28712f }; + +struct npc_worgen_runt : public ScriptedAI +{ + npc_worgen_runt(Creature* creature) : ScriptedAI(creature), _worgenID(0), _wayPointCounter(0), _jumped(false) { } + + void JustEngagedWith(Unit* /*who*/) override + { + _events.SetPhase(PHASE_COMBAT); + } + + void IsSummonedBy(Unit* summoner) override + { + me->setActive(true); // we are in a phased and cut off map so we're fine to use that here + _events.SetPhase(PHASE_ROOF); + _events.ScheduleEvent(EVENT_FORCE_DESPAWN, 70s, 0, PHASE_ROOF); + _playerGuid = summoner->GetGUID(); + if (me->GetEntry() == NPC_WORGEN_RUNT_SPELL) + me->GetMotionMaster()->MoveJump(RuntSpellSummonJumpPos, 16.0f, 4.371286f); + } + + void DoAction(int32 action) override + { + _worgenID = action; + switch (_worgenID) // ugly as fuck but needed for individual controls + { + // House roof cases + case WORGEN_ID_ROOF_1: + _wayPointCounter = runtHousePathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize1, worgenRuntHousePath1, runtHousePathSize1); + break; + case WORGEN_ID_ROOF_2: + _wayPointCounter = runtHousePathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize2, worgenRuntHousePath2, runtHousePathSize2); + break; + case WORGEN_ID_ROOF_3: + _wayPointCounter = runtHousePathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize3, worgenRuntHousePath3, runtHousePathSize3); + break; + case WORGEN_ID_ROOF_4: + _wayPointCounter = runtHousePathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize4, worgenRuntHousePath4, runtHousePathSize4); + break; + case WORGEN_ID_ROOF_5: + _wayPointCounter = runtHousePathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize5, worgenRuntHousePath5, runtHousePathSize5); + break; + case WORGEN_ID_ROOF_6: + _wayPointCounter = runtHousePathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize6, worgenRuntHousePath6, runtHousePathSize6); + break; + case WORGEN_ID_ROOF_7: + _wayPointCounter = runtHousePathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize7, worgenRuntHousePath7, runtHousePathSize7); + break; + // Cathedral cases + case WORGEN_ID_CATHEDRAL_1: + _wayPointCounter = runtCathedralPathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize1, worgenRuntCathedralPath1, runtCathedralPathSize1); + break; + case WORGEN_ID_CATHEDRAL_2: + _wayPointCounter = runtCathedralPathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize2, worgenRuntCathedralPath1, runtCathedralPathSize2); + break; + case WORGEN_ID_CATHEDRAL_3: + _wayPointCounter = runtCathedralPathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize3, worgenRuntCathedralPath1, runtCathedralPathSize3); + break; + case WORGEN_ID_CATHEDRAL_4: + _wayPointCounter = runtCathedralPathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize4, worgenRuntCathedralPath1, runtCathedralPathSize4); + break; + case WORGEN_ID_CATHEDRAL_5: + _wayPointCounter = runtCathedralPathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize5, worgenRuntCathedralPath1, runtCathedralPathSize5); + break; + case WORGEN_ID_CATHEDRAL_6: + _wayPointCounter = runtCathedralPathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize6, worgenRuntCathedralPath1, runtCathedralPathSize6); + break; + case WORGEN_ID_CATHEDRAL_7: + _wayPointCounter = runtCathedralPathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize7, worgenRuntCathedralPath1, runtCathedralPathSize7); + break; + case WORGEN_ID_CATHEDRAL_8: + _wayPointCounter = runtCathedralPathSize8; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize8, worgenRuntCathedralPath1, runtCathedralPathSize8); + break; + } + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == _wayPointCounter && !_jumped) + { + _jumped = true; + _events.ScheduleEvent(EVENT_JUMP_TO_PRISON, 1ms); + } + } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim() && !_events.IsInPhase(PHASE_ROOF)) + return; + + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_FORCE_DESPAWN: + me->DespawnOrUnsummon(); + break; + case EVENT_JUMP_TO_PRISON: + me->GetMotionMaster()->MoveJump(worgenRuntJumpPos[_worgenID], 16.0f, _worgenID < WORGEN_ID_CATHEDRAL_1 ? 19.2911f : frand(3.945607f, 4.852813f)); + me->SetHomePosition(worgenRuntJumpPos[_worgenID]); + _events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s); + break; + case EVENT_AGGRO_PLAYER: + { + Creature* crowley = me->FindNearestCreature(35077, 100.0f, true); + Creature* tobias = me->FindNearestCreature(35124, 100.0f, true); + + if (crowley && tobias) + { + if (me->GetDistance(crowley) <= me->GetDistance(tobias)) + AttackStart(crowley); + else + AttackStart(tobias); + } + else if (crowley) + AttackStart(crowley); + else if (tobias) + AttackStart(tobias); + + break; + } + default: + break; + } + } + + DoMeleeAttackIfReady(); + } +private: + uint32 _worgenID; + uint32 _wayPointCounter; + bool _jumped; + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14159 - The Rebel Lord's Arsenal +######*/ + +enum TheRebelLordsArsena +{ + PHASE_ID_SUMMON = 170, + PHASE_ID_WOUND = 171, + NPC_LORNA_CROWLEY = 35378, + NPC_GENERIC_TRIGGER_LAB = 35374, + + SPELL_SHOOT_INSTAKILL = 67593, + SPELL_COSMETIC_ATTACK = 42880, + SPELL_PULL_TO = 67357, + + EVENT_COSMETIC_ATTACK = 1, + EVENT_JUMP_TO_PLAYER, + EVENT_SHOOT_JOSIAH +}; + +static Position const JosiahJumpPos = { -1796.63f, 1427.73f, 12.4624f }; + +struct npc_josiah_avery : public PassiveAI +{ + npc_josiah_avery(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* summoner) override + { + PhasingHandler::AddPhase(me, PHASE_ID_SUMMON, true); + PhasingHandler::AddPhase(me, PHASE_ID_WOUND, true); + _playerGuid = summoner->GetGUID(); + _events.ScheduleEvent(EVENT_COSMETIC_ATTACK, 500ms); + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_COSMETIC_ATTACK: + if (Player* player = ObjectAccessor::GetPlayer(*me, _playerGuid)) + { + DoCast(player, SPELL_COSMETIC_ATTACK); + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + if (Creature* labTrigger = lorna->FindNearestCreature(NPC_GENERIC_TRIGGER_LAB, 5.0f, true)) + labTrigger->CastSpell(player, SPELL_PULL_TO); + + _events.ScheduleEvent(EVENT_JUMP_TO_PLAYER, 1s); + } + break; + case EVENT_JUMP_TO_PLAYER: + me->GetMotionMaster()->MoveJump(JosiahJumpPos, 10.0f, 14.18636f); + _events.ScheduleEvent(EVENT_SHOOT_JOSIAH, 500ms); + break; + case EVENT_SHOOT_JOSIAH: + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + lorna->CastSpell(me, SPELL_SHOOT_INSTAKILL, true); + break; + default: + break; + } + } + } +private: + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14293 - Save Krennan Aranas +######*/ + +static Position const GreymanesHorseJumpPos = { -1676.16f, 1346.19f, 15.1349f }; + +enum GreymanesHorse +{ + SAY_ANNOUNCE_RESCUE = 0, + SAY_RESCUED = 0, + SAY_TRAPPED = 0, + + PATH_KRENNAN_TREE = 0, + PATH_KRENNAN_BACK = 1, + + EVENT_START_PATH_1 = 1, + EVENT_START_PATH_2, + EVENT_JUMP_TO_KRENNAN, + EVENT_ANNOUNCE_RESCUE, + EVENT_DISMOUNT_PLAYER, + + NPC_RESCUED_KRENNAN = 35907, + NPC_TRAPPED_KRENNAN = 35753, +}; + +struct npc_greymanes_horse : public VehicleAI +{ + npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { } + + void DamageTaken(Unit* /*attacker*/, uint32& damage) override + { + if (me->GetHealth() <= 1) + damage = 0; + else if (damage >= me->GetHealth()) + damage = me->GetHealth() - 1; + } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_START_PATH_1, 1s); + } + else if (apply && passenger->GetEntry() == NPC_RESCUED_KRENNAN) + _events.ScheduleEvent(EVENT_START_PATH_2, 1s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == pathSize1 && _currentPath == PATH_KRENNAN_TREE) + _events.ScheduleEvent(EVENT_JUMP_TO_KRENNAN, 1ms); + else if (type == EFFECT_MOTION_TYPE && pointId == pathSize2 && _currentPath == PATH_KRENNAN_BACK) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_PATH_1: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_TREE; + me->GetMotionMaster()->MoveSmoothPath(pathSize1, greymanesHorsePath1, pathSize1); + break; + case EVENT_JUMP_TO_KRENNAN: + me->GetMotionMaster()->MoveJump(GreymanesHorseJumpPos, 16.0f, 14.00968f); + _events.ScheduleEvent(EVENT_ANNOUNCE_RESCUE, 2s); + break; + case EVENT_ANNOUNCE_RESCUE: + me->SetControlled(true, UNIT_STATE_ROOT); + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(0)) + { + Talk(SAY_ANNOUNCE_RESCUE, passenger); + if (Creature* krennan = me->FindNearestCreature(NPC_TRAPPED_KRENNAN, 30.0f, true)) + krennan->AI()->Talk(SAY_TRAPPED, passenger); + } + break; + case EVENT_START_PATH_2: + me->SetControlled(true, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_BACK; + me->GetMotionMaster()->MoveSmoothPath(pathSize2, greymanesHorsePath2, pathSize2); + break; + case EVENT_DISMOUNT_PLAYER: + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(1)) + { + if (Unit* player = me->GetVehicleKit()->GetPassenger(0)) + { + if (Creature* krennan = passenger->ToCreature()) + { + std::vector storedAttackers; + + for (Unit* attacker : me->getAttackers()) + storedAttackers.push_back(attacker); + + for (Unit* attacker : storedAttackers) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + player->ExitVehicle(); + krennan->AddUnitState(UNIT_STATE_ROOT); + krennan->ExitVehicle(); + krennan->AI()->Talk(SAY_RESCUED, me); + krennan->DespawnOrUnsummon(7s); + } + } + } + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +/*###### +## Quest 14212 - Sacrifices +######*/ + +enum CrowleysHorse +{ + EVENT_JUMP_OVER_BARRICADES_1 = 1, + EVENT_JUMP_OVER_BARRICADES_2, + EVENT_MOVE_PATH_MAIN_1, + EVENT_MOVE_PATH_MAIN_2, + EVENT_MOVE_OFF_PATH = 6, + + PATH_ID_CROWLEYS_HORSE_1 = 352310, + PATH_ID_CROWLEYS_HORSE_2 = 352311, + PATH_ID_CROWLEYS_HORSE_3 = 444280, + + SPELL_THROW_TORCH = 67063, + NPC_CROWLEYS_HORSE_2 = 44428, +}; + +static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f }; +static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f }; + +static Position const CrowleysHorsePath1[] = +{ + { -1686.12f, 1655.64f, 20.6106f }, + { -1668.16f, 1639.43f, 20.6106f }, + { -1666.63f, 1628.86f, 20.5327f }, + { -1670.76f, 1617.43f, 20.6106f }, + { -1693.65f, 1592.64f, 20.6106f }, + { -1708.02f, 1590.55f, 20.6104f }, + { -1719.37f, 1595.65f, 20.6104f }, + { -1720.57f, 1610.14f, 20.6074f }, + { -1711.90f, 1629.70f, 20.6074f }, + { -1698.15f, 1656.48f, 20.6106f }, + { -1696.42f, 1682.01f, 20.5262f }, + { -1684.68f, 1700.12f, 20.4648f }, + { -1658.31f, 1710.31f, 20.5898f }, + { -1625.48f, 1712.30f, 21.8051f }, + { -1592.09f, 1710.71f, 20.5899f } +}; + + +static Position const CrowleysHorsePath2[] = +{ + { -1545.08f, 1684.23f, 20.6099f }, + { -1542.79f, 1656.08f, 20.6099f }, + { -1543.0f, 1640.54f, 20.6099f }, + { -1520.73f, 1618.1f, 20.6105f }, + { -1498.79f, 1621.74f, 20.6105f }, + { -1468.36f, 1630.78f, 20.6105f }, + { -1441.65f, 1627.67f, 20.6105f }, + { -1429.84f, 1616.63f, 20.5898f }, + { -1417.99f, 1586.55f, 20.5898f }, + { -1429.18f, 1548.78f, 20.8485f }, + { -1447.42f, 1532.79f, 20.601f }, + { -1463.1f, 1535.03f, 20.6105f }, + { -1483.04f, 1544.15f, 20.6105f }, + { -1505.16f, 1568.61f, 20.6105f }, + { -1531.6f, 1581.87f, 26.5387f }, + { -1540.59f, 1574.43f, 29.2067f }, +}; + +static Position const CrowleysHorsePath3[] = +{ + { -1512.36f, 1570.78f, 24.0671f }, + { -1498.71f, 1565.43f, 20.8781f }, + { -1487.86f, 1557.16f, 20.85f }, + { -1474.18f, 1542.68f, 20.6105f }, + { -1457.6f, 1532.64f, 20.601f }, + { -1438.01f, 1525.31f, 20.8762f }, + { -1424.2f, 1545.7f, 20.4657f }, + { -1411.29f, 1556.16f, 20.557f }, + { -1414.19f, 1577.65f, 20.5898f }, + { -1427.47f, 1617.3f, 20.5898f }, + { -1445.54f, 1633.9f, 20.6105f }, + { -1470.71f, 1658.21f, 20.6105f }, + { -1491.05f, 1658.74f, 20.6105f }, + { -1515.33f, 1641.18f, 20.6099f }, + { -1526.75f, 1630.37f, 20.6105f }, + { -1528.42f, 1610.71f, 20.6105f }, + { -1532.39f, 1585.36f, 26.538f }, +}; + +struct npc_crowleys_horse : public VehicleAI +{ + npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_1, 2s); + } + else if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_MOVE_OFF_PATH, 2s); + } + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == PATH_ID_CROWLEYS_HORSE_1 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s); + else if (type == EFFECT_MOTION_TYPE && pointId == PATH_ID_CROWLEYS_HORSE_2 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + else if (type == EFFECT_MOTION_TYPE && pointId == PATH_ID_CROWLEYS_HORSE_3 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_JUMP_OVER_BARRICADES_1: + me->SetControlled(false, UNIT_STATE_ROOT); + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 2s); + break; + case EVENT_MOVE_PATH_MAIN_1: + _currentPath = PATH_ID_CROWLEYS_HORSE_1; + me->GetMotionMaster()->MoveSmoothPath( + PATH_ID_CROWLEYS_HORSE_1, + CrowleysHorsePath1, + 15); + break; + case EVENT_JUMP_OVER_BARRICADES_2: + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_2, 2s); + break; + case EVENT_MOVE_PATH_MAIN_2: + _currentPath = PATH_ID_CROWLEYS_HORSE_2; + me->GetMotionMaster()->MoveSmoothPath( + PATH_ID_CROWLEYS_HORSE_2, + CrowleysHorsePath2, + 16); + break; + case EVENT_DISMOUNT_PLAYER: + { + std::set attackersCopy = me->getAttackers(); + for (Unit* attacker : attackersCopy) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + me->RemoveAurasDueToSpell(VEHICLE_SPELL_RIDE_HARDCODED); + me->DespawnOrUnsummon(5s); + break; + } + case EVENT_MOVE_OFF_PATH: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_ID_CROWLEYS_HORSE_3; + me->GetMotionMaster()->MoveSmoothPath( + PATH_ID_CROWLEYS_HORSE_3, + CrowleysHorsePath3, + 17); + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +enum GileanCrow +{ + SPELL_GILNEAN_CROW = 93275, + EVENT_APPLY_HOVER_BYTES = 1, + EVENT_FLY_AWAY_1 = 2, + EVENT_FLY_AWAY_2 = 3, + + POINT_NONE = 0, + POINT_CROW_FLIGHT = 1 +}; + +struct npc_gilnean_crow : public PassiveAI +{ + npc_gilnean_crow(Creature* creature) : PassiveAI(creature) { } + + void SpellHit(WorldObject* /*caster*/, SpellInfo const* spell) override + { + if (spell->Id == SPELL_GILNEAN_CROW) + _events.ScheduleEvent(EVENT_APPLY_HOVER_BYTES, 500ms); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == POINT_MOTION_TYPE && pointId == POINT_CROW_FLIGHT) + { + Position pos = me->GetRandomNearPosition(8.0f); + pos.m_positionZ = me->GetPositionZ() + 10.0f; + me->GetMotionMaster()->MovePoint(POINT_NONE, pos); + me->DespawnOrUnsummon(14s); + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_APPLY_HOVER_BYTES: + me->SetStandState(UNIT_STAND_STATE_SIT); + me->SetAnimTier(AnimTier::Submerged); + _events.ScheduleEvent(EVENT_FLY_AWAY_1, 1s); + break; + case EVENT_FLY_AWAY_1: + { + Position pos = me->GetRandomNearPosition(7.0f); + pos.m_positionZ = me->GetPositionZ() + 8.0f; + me->GetMotionMaster()->MovePoint(POINT_CROW_FLIGHT, pos); + break; + } + default: + break; + } + } + } +private: + EventMap _events; +}; +} + +void AddSC_gilneas_chapter_1() +{ + using namespace Gilneas::Chapter1; + RegisterCreatureAI(npc_frightened_citizen); + RegisterCreatureAI(npc_worgen_runt); + RegisterCreatureAI(npc_josiah_avery); + RegisterCreatureAI(npc_greymanes_horse); + RegisterCreatureAI(npc_crowleys_horse); + RegisterCreatureAI(npc_gilnean_crow); +} diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-movepoint-test-20261001-180011 b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-movepoint-test-20261001-180011 new file mode 100644 index 0000000000..1845a4fbef --- /dev/null +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-movepoint-test-20261001-180011 @@ -0,0 +1,704 @@ +/* +* 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 . +*/ + +#include "gilneas.h" +#include "ScriptMgr.h" +#include "CombatAI.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "PassiveAI.h" +#include "PhasingHandler.h" +#include "ScriptedCreature.h" +#include "SpellInfo.h" +#include "SpellScript.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +namespace Gilneas::Chapter1 +{ + +/*###### +## Quest 14098 - Evacuate the Merchant Square +######*/ + +enum FrightenedCitizen +{ + SAY_FRIGHTENED_CITIZEN_RESCUE = 0, + + NPC_EVACUATION_STALKER_FIRST = 35830, + NPC_EVACUATION_STALKER_NEAR = 35010, + NPC_EVACUATION_STALKER_FAR = 35011, + NPC_RAMPAGING_WORGEN = 35660, + + CREDIT_35830 = 35830, + + POINT_STALKER_FIRST = 1, + POINT_STALKER_NEAR = 2, + POINT_STALKER_FAR = 3, + + EVENT_TALK_FRIGHTENED = 1, + EVENT_MOVE_TO_NEAR_STALKER = 2, + EVENT_MOVE_TO_FAR_STALKER = 3, + EVENT_DESPAWN = 4 +}; + +struct npc_frightened_citizen : public PassiveAI +{ + npc_frightened_citizen(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* /*summoner*/) override + { + if (Creature* stalkerNear = me->FindNearestCreature(NPC_EVACUATION_STALKER_FIRST, 20.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FIRST, stalkerNear->GetPosition(), true); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + switch (id) + { + case POINT_STALKER_FIRST: + _events.ScheduleEvent(EVENT_TALK_FRIGHTENED, 1s); + break; + case POINT_STALKER_NEAR: + _events.ScheduleEvent(EVENT_MOVE_TO_FAR_STALKER, 1ms); + break; + case POINT_STALKER_FAR: + _events.ScheduleEvent(EVENT_DESPAWN, 1ms); + break; + default: + break; + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_TALK_FRIGHTENED: + if (Unit* summoner = me->ToTempSummon()->GetSummoner()) + { + if (Player* player = summoner->ToPlayer()) + { + player->KilledMonsterCredit(CREDIT_35830); + Talk(SAY_FRIGHTENED_CITIZEN_RESCUE, summoner); + } + } + _events.ScheduleEvent(EVENT_MOVE_TO_NEAR_STALKER, 2s); + break; + case EVENT_MOVE_TO_NEAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 50.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + else if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_NEAR, 100.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_NEAR, stalker->GetPosition(), true); + break; + case EVENT_MOVE_TO_FAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 500.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + break; + case EVENT_DESPAWN: + me->DespawnOrUnsummon(); + break; + default: + break; + } + } + } +private: + EventMap _events; +}; + +/*###### +## Quest 14154 - By the Skin of his Teeth +######*/ + +enum WorgenRunt +{ + EVENT_JUMP_TO_PRISON = 1, + EVENT_AGGRO_PLAYER = 2, + EVENT_FORCE_DESPAWN = 3, + + PHASE_ROOF = 0, + PHASE_COMBAT = 1, + + WORGEN_ID_ROOF_1 = 0, + WORGEN_ID_ROOF_2 = 1, + WORGEN_ID_ROOF_3 = 2, + WORGEN_ID_ROOF_4 = 3, + WORGEN_ID_ROOF_5 = 4, + WORGEN_ID_ROOF_6 = 5, + WORGEN_ID_ROOF_7 = 6, + + WORGEN_ID_CATHEDRAL_1 = 7, + WORGEN_ID_CATHEDRAL_2 = 8, + WORGEN_ID_CATHEDRAL_3 = 9, + WORGEN_ID_CATHEDRAL_4 = 10, + WORGEN_ID_CATHEDRAL_5 = 11, + WORGEN_ID_CATHEDRAL_6 = 12, + WORGEN_ID_CATHEDRAL_7 = 13, + WORGEN_ID_CATHEDRAL_8 = 14, + WORGEN_ID_CATHEDRAL_9 = 15, + + NPC_WORGEN_RUNT_SPELL = 35188, +}; + +static Position const RuntSpellSummonJumpPos = { -1671.915f, 1446.734f, 52.28712f }; + +struct npc_worgen_runt : public ScriptedAI +{ + npc_worgen_runt(Creature* creature) : ScriptedAI(creature), _worgenID(0), _wayPointCounter(0), _jumped(false) { } + + void JustEngagedWith(Unit* /*who*/) override + { + _events.SetPhase(PHASE_COMBAT); + } + + void IsSummonedBy(Unit* summoner) override + { + me->setActive(true); // we are in a phased and cut off map so we're fine to use that here + _events.SetPhase(PHASE_ROOF); + _events.ScheduleEvent(EVENT_FORCE_DESPAWN, 70s, 0, PHASE_ROOF); + _playerGuid = summoner->GetGUID(); + if (me->GetEntry() == NPC_WORGEN_RUNT_SPELL) + me->GetMotionMaster()->MoveJump(RuntSpellSummonJumpPos, 16.0f, 4.371286f); + } + + void DoAction(int32 action) override + { + _worgenID = action; + switch (_worgenID) // ugly as fuck but needed for individual controls + { + // House roof cases + case WORGEN_ID_ROOF_1: + _wayPointCounter = runtHousePathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize1, worgenRuntHousePath1, runtHousePathSize1); + break; + case WORGEN_ID_ROOF_2: + _wayPointCounter = runtHousePathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize2, worgenRuntHousePath2, runtHousePathSize2); + break; + case WORGEN_ID_ROOF_3: + _wayPointCounter = runtHousePathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize3, worgenRuntHousePath3, runtHousePathSize3); + break; + case WORGEN_ID_ROOF_4: + _wayPointCounter = runtHousePathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize4, worgenRuntHousePath4, runtHousePathSize4); + break; + case WORGEN_ID_ROOF_5: + _wayPointCounter = runtHousePathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize5, worgenRuntHousePath5, runtHousePathSize5); + break; + case WORGEN_ID_ROOF_6: + _wayPointCounter = runtHousePathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize6, worgenRuntHousePath6, runtHousePathSize6); + break; + case WORGEN_ID_ROOF_7: + _wayPointCounter = runtHousePathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize7, worgenRuntHousePath7, runtHousePathSize7); + break; + // Cathedral cases + case WORGEN_ID_CATHEDRAL_1: + _wayPointCounter = runtCathedralPathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize1, worgenRuntCathedralPath1, runtCathedralPathSize1); + break; + case WORGEN_ID_CATHEDRAL_2: + _wayPointCounter = runtCathedralPathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize2, worgenRuntCathedralPath1, runtCathedralPathSize2); + break; + case WORGEN_ID_CATHEDRAL_3: + _wayPointCounter = runtCathedralPathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize3, worgenRuntCathedralPath1, runtCathedralPathSize3); + break; + case WORGEN_ID_CATHEDRAL_4: + _wayPointCounter = runtCathedralPathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize4, worgenRuntCathedralPath1, runtCathedralPathSize4); + break; + case WORGEN_ID_CATHEDRAL_5: + _wayPointCounter = runtCathedralPathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize5, worgenRuntCathedralPath1, runtCathedralPathSize5); + break; + case WORGEN_ID_CATHEDRAL_6: + _wayPointCounter = runtCathedralPathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize6, worgenRuntCathedralPath1, runtCathedralPathSize6); + break; + case WORGEN_ID_CATHEDRAL_7: + _wayPointCounter = runtCathedralPathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize7, worgenRuntCathedralPath1, runtCathedralPathSize7); + break; + case WORGEN_ID_CATHEDRAL_8: + _wayPointCounter = runtCathedralPathSize8; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize8, worgenRuntCathedralPath1, runtCathedralPathSize8); + break; + } + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == _wayPointCounter && !_jumped) + { + _jumped = true; + _events.ScheduleEvent(EVENT_JUMP_TO_PRISON, 1ms); + } + } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim() && !_events.IsInPhase(PHASE_ROOF)) + return; + + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_FORCE_DESPAWN: + me->DespawnOrUnsummon(); + break; + case EVENT_JUMP_TO_PRISON: + me->GetMotionMaster()->MoveJump(worgenRuntJumpPos[_worgenID], 16.0f, _worgenID < WORGEN_ID_CATHEDRAL_1 ? 19.2911f : frand(3.945607f, 4.852813f)); + me->SetHomePosition(worgenRuntJumpPos[_worgenID]); + _events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s); + break; + case EVENT_AGGRO_PLAYER: + { + Creature* crowley = me->FindNearestCreature(35077, 100.0f, true); + Creature* tobias = me->FindNearestCreature(35124, 100.0f, true); + + if (crowley && tobias) + { + if (me->GetDistance(crowley) <= me->GetDistance(tobias)) + AttackStart(crowley); + else + AttackStart(tobias); + } + else if (crowley) + AttackStart(crowley); + else if (tobias) + AttackStart(tobias); + + break; + } + default: + break; + } + } + + DoMeleeAttackIfReady(); + } +private: + uint32 _worgenID; + uint32 _wayPointCounter; + bool _jumped; + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14159 - The Rebel Lord's Arsenal +######*/ + +enum TheRebelLordsArsena +{ + PHASE_ID_SUMMON = 170, + PHASE_ID_WOUND = 171, + NPC_LORNA_CROWLEY = 35378, + NPC_GENERIC_TRIGGER_LAB = 35374, + + SPELL_SHOOT_INSTAKILL = 67593, + SPELL_COSMETIC_ATTACK = 42880, + SPELL_PULL_TO = 67357, + + EVENT_COSMETIC_ATTACK = 1, + EVENT_JUMP_TO_PLAYER, + EVENT_SHOOT_JOSIAH +}; + +static Position const JosiahJumpPos = { -1796.63f, 1427.73f, 12.4624f }; + +struct npc_josiah_avery : public PassiveAI +{ + npc_josiah_avery(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* summoner) override + { + PhasingHandler::AddPhase(me, PHASE_ID_SUMMON, true); + PhasingHandler::AddPhase(me, PHASE_ID_WOUND, true); + _playerGuid = summoner->GetGUID(); + _events.ScheduleEvent(EVENT_COSMETIC_ATTACK, 500ms); + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_COSMETIC_ATTACK: + if (Player* player = ObjectAccessor::GetPlayer(*me, _playerGuid)) + { + DoCast(player, SPELL_COSMETIC_ATTACK); + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + if (Creature* labTrigger = lorna->FindNearestCreature(NPC_GENERIC_TRIGGER_LAB, 5.0f, true)) + labTrigger->CastSpell(player, SPELL_PULL_TO); + + _events.ScheduleEvent(EVENT_JUMP_TO_PLAYER, 1s); + } + break; + case EVENT_JUMP_TO_PLAYER: + me->GetMotionMaster()->MoveJump(JosiahJumpPos, 10.0f, 14.18636f); + _events.ScheduleEvent(EVENT_SHOOT_JOSIAH, 500ms); + break; + case EVENT_SHOOT_JOSIAH: + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + lorna->CastSpell(me, SPELL_SHOOT_INSTAKILL, true); + break; + default: + break; + } + } + } +private: + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14293 - Save Krennan Aranas +######*/ + +static Position const GreymanesHorseJumpPos = { -1676.16f, 1346.19f, 15.1349f }; + +enum GreymanesHorse +{ + SAY_ANNOUNCE_RESCUE = 0, + SAY_RESCUED = 0, + SAY_TRAPPED = 0, + + PATH_KRENNAN_TREE = 0, + PATH_KRENNAN_BACK = 1, + + EVENT_START_PATH_1 = 1, + EVENT_START_PATH_2, + EVENT_JUMP_TO_KRENNAN, + EVENT_ANNOUNCE_RESCUE, + EVENT_DISMOUNT_PLAYER, + + NPC_RESCUED_KRENNAN = 35907, + NPC_TRAPPED_KRENNAN = 35753, +}; + +struct npc_greymanes_horse : public VehicleAI +{ + npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { } + + void DamageTaken(Unit* /*attacker*/, uint32& damage) override + { + if (me->GetHealth() <= 1) + damage = 0; + else if (damage >= me->GetHealth()) + damage = me->GetHealth() - 1; + } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_START_PATH_1, 1s); + } + else if (apply && passenger->GetEntry() == NPC_RESCUED_KRENNAN) + _events.ScheduleEvent(EVENT_START_PATH_2, 1s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == pathSize1 && _currentPath == PATH_KRENNAN_TREE) + _events.ScheduleEvent(EVENT_JUMP_TO_KRENNAN, 1ms); + else if (type == EFFECT_MOTION_TYPE && pointId == pathSize2 && _currentPath == PATH_KRENNAN_BACK) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_PATH_1: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_TREE; + me->GetMotionMaster()->MoveSmoothPath(pathSize1, greymanesHorsePath1, pathSize1); + break; + case EVENT_JUMP_TO_KRENNAN: + me->GetMotionMaster()->MoveJump(GreymanesHorseJumpPos, 16.0f, 14.00968f); + _events.ScheduleEvent(EVENT_ANNOUNCE_RESCUE, 2s); + break; + case EVENT_ANNOUNCE_RESCUE: + me->SetControlled(true, UNIT_STATE_ROOT); + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(0)) + { + Talk(SAY_ANNOUNCE_RESCUE, passenger); + if (Creature* krennan = me->FindNearestCreature(NPC_TRAPPED_KRENNAN, 30.0f, true)) + krennan->AI()->Talk(SAY_TRAPPED, passenger); + } + break; + case EVENT_START_PATH_2: + me->SetControlled(true, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_BACK; + me->GetMotionMaster()->MoveSmoothPath(pathSize2, greymanesHorsePath2, pathSize2); + break; + case EVENT_DISMOUNT_PLAYER: + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(1)) + { + if (Unit* player = me->GetVehicleKit()->GetPassenger(0)) + { + if (Creature* krennan = passenger->ToCreature()) + { + std::vector storedAttackers; + + for (Unit* attacker : me->getAttackers()) + storedAttackers.push_back(attacker); + + for (Unit* attacker : storedAttackers) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + player->ExitVehicle(); + krennan->AddUnitState(UNIT_STATE_ROOT); + krennan->ExitVehicle(); + krennan->AI()->Talk(SAY_RESCUED, me); + krennan->DespawnOrUnsummon(7s); + } + } + } + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +/*###### +## Quest 14212 - Sacrifices +######*/ + +enum CrowleysHorse +{ + EVENT_JUMP_OVER_BARRICADES_1 = 1, + EVENT_JUMP_OVER_BARRICADES_2, + EVENT_MOVE_PATH_MAIN_1, + EVENT_MOVE_PATH_MAIN_2, + EVENT_MOVE_OFF_PATH = 6, + + PATH_ID_CROWLEYS_HORSE_1 = 352310, + PATH_ID_CROWLEYS_HORSE_2 = 352311, + PATH_ID_CROWLEYS_HORSE_3 = 444280, + + SPELL_THROW_TORCH = 67063, + NPC_CROWLEYS_HORSE_2 = 44428, +}; + +static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f }; +static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f }; + +struct npc_crowleys_horse : public VehicleAI +{ + npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_1, 2s); + } + else if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_MOVE_OFF_PATH, 2s); + } + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == WAYPOINT_MOTION_TYPE && pointId == 14 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 15 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 16 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_JUMP_OVER_BARRICADES_1: + me->SetControlled(false, UNIT_STATE_ROOT); + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 4s); + break; + case EVENT_MOVE_PATH_MAIN_1: + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_1, false); + _currentPath = PATH_ID_CROWLEYS_HORSE_1; + break; + case EVENT_JUMP_OVER_BARRICADES_2: + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_2, 2s); + break; + case EVENT_MOVE_PATH_MAIN_2: + _currentPath = PATH_ID_CROWLEYS_HORSE_2; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_2, false); + break; + case EVENT_DISMOUNT_PLAYER: + { + std::set attackersCopy = me->getAttackers(); + for (Unit* attacker : attackersCopy) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + me->RemoveAurasDueToSpell(VEHICLE_SPELL_RIDE_HARDCODED); + me->DespawnOrUnsummon(5s); + break; + } + case EVENT_MOVE_OFF_PATH: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_ID_CROWLEYS_HORSE_3; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_3, false); + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +enum GileanCrow +{ + SPELL_GILNEAN_CROW = 93275, + EVENT_APPLY_HOVER_BYTES = 1, + EVENT_FLY_AWAY_1 = 2, + EVENT_FLY_AWAY_2 = 3, + + POINT_NONE = 0, + POINT_CROW_FLIGHT = 1 +}; + +struct npc_gilnean_crow : public PassiveAI +{ + npc_gilnean_crow(Creature* creature) : PassiveAI(creature) { } + + void SpellHit(WorldObject* /*caster*/, SpellInfo const* spell) override + { + if (spell->Id == SPELL_GILNEAN_CROW) + _events.ScheduleEvent(EVENT_APPLY_HOVER_BYTES, 500ms); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == POINT_MOTION_TYPE && pointId == POINT_CROW_FLIGHT) + { + Position pos = me->GetRandomNearPosition(8.0f); + pos.m_positionZ = me->GetPositionZ() + 10.0f; + me->GetMotionMaster()->MovePoint(POINT_NONE, pos); + me->DespawnOrUnsummon(14s); + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_APPLY_HOVER_BYTES: + me->SetStandState(UNIT_STAND_STATE_SIT); + me->SetAnimTier(AnimTier::Submerged); + _events.ScheduleEvent(EVENT_FLY_AWAY_1, 1s); + break; + case EVENT_FLY_AWAY_1: + { + Position pos = me->GetRandomNearPosition(7.0f); + pos.m_positionZ = me->GetPositionZ() + 8.0f; + me->GetMotionMaster()->MovePoint(POINT_CROW_FLIGHT, pos); + break; + } + default: + break; + } + } + } +private: + EventMap _events; +}; +} + +void AddSC_gilneas_chapter_1() +{ + using namespace Gilneas::Chapter1; + RegisterCreatureAI(npc_frightened_citizen); + RegisterCreatureAI(npc_worgen_runt); + RegisterCreatureAI(npc_josiah_avery); + RegisterCreatureAI(npc_greymanes_horse); + RegisterCreatureAI(npc_crowleys_horse); + RegisterCreatureAI(npc_gilnean_crow); +} diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-smoothpath-test-20261001-181316 b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-smoothpath-test-20261001-181316 new file mode 100644 index 0000000000..349ae2a84d --- /dev/null +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug16-smoothpath-test-20261001-181316 @@ -0,0 +1,704 @@ +/* +* 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 . +*/ + +#include "gilneas.h" +#include "ScriptMgr.h" +#include "CombatAI.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "PassiveAI.h" +#include "PhasingHandler.h" +#include "ScriptedCreature.h" +#include "SpellInfo.h" +#include "SpellScript.h" +#include "TemporarySummon.h" +#include "Vehicle.h" + +namespace Gilneas::Chapter1 +{ + +/*###### +## Quest 14098 - Evacuate the Merchant Square +######*/ + +enum FrightenedCitizen +{ + SAY_FRIGHTENED_CITIZEN_RESCUE = 0, + + NPC_EVACUATION_STALKER_FIRST = 35830, + NPC_EVACUATION_STALKER_NEAR = 35010, + NPC_EVACUATION_STALKER_FAR = 35011, + NPC_RAMPAGING_WORGEN = 35660, + + CREDIT_35830 = 35830, + + POINT_STALKER_FIRST = 1, + POINT_STALKER_NEAR = 2, + POINT_STALKER_FAR = 3, + + EVENT_TALK_FRIGHTENED = 1, + EVENT_MOVE_TO_NEAR_STALKER = 2, + EVENT_MOVE_TO_FAR_STALKER = 3, + EVENT_DESPAWN = 4 +}; + +struct npc_frightened_citizen : public PassiveAI +{ + npc_frightened_citizen(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* /*summoner*/) override + { + if (Creature* stalkerNear = me->FindNearestCreature(NPC_EVACUATION_STALKER_FIRST, 20.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FIRST, stalkerNear->GetPosition(), true); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + switch (id) + { + case POINT_STALKER_FIRST: + _events.ScheduleEvent(EVENT_TALK_FRIGHTENED, 1s); + break; + case POINT_STALKER_NEAR: + _events.ScheduleEvent(EVENT_MOVE_TO_FAR_STALKER, 1ms); + break; + case POINT_STALKER_FAR: + _events.ScheduleEvent(EVENT_DESPAWN, 1ms); + break; + default: + break; + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_TALK_FRIGHTENED: + if (Unit* summoner = me->ToTempSummon()->GetSummoner()) + { + if (Player* player = summoner->ToPlayer()) + { + player->KilledMonsterCredit(CREDIT_35830); + Talk(SAY_FRIGHTENED_CITIZEN_RESCUE, summoner); + } + } + _events.ScheduleEvent(EVENT_MOVE_TO_NEAR_STALKER, 2s); + break; + case EVENT_MOVE_TO_NEAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 50.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + else if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_NEAR, 100.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_NEAR, stalker->GetPosition(), true); + break; + case EVENT_MOVE_TO_FAR_STALKER: + if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 500.0f)) + me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true); + break; + case EVENT_DESPAWN: + me->DespawnOrUnsummon(); + break; + default: + break; + } + } + } +private: + EventMap _events; +}; + +/*###### +## Quest 14154 - By the Skin of his Teeth +######*/ + +enum WorgenRunt +{ + EVENT_JUMP_TO_PRISON = 1, + EVENT_AGGRO_PLAYER = 2, + EVENT_FORCE_DESPAWN = 3, + + PHASE_ROOF = 0, + PHASE_COMBAT = 1, + + WORGEN_ID_ROOF_1 = 0, + WORGEN_ID_ROOF_2 = 1, + WORGEN_ID_ROOF_3 = 2, + WORGEN_ID_ROOF_4 = 3, + WORGEN_ID_ROOF_5 = 4, + WORGEN_ID_ROOF_6 = 5, + WORGEN_ID_ROOF_7 = 6, + + WORGEN_ID_CATHEDRAL_1 = 7, + WORGEN_ID_CATHEDRAL_2 = 8, + WORGEN_ID_CATHEDRAL_3 = 9, + WORGEN_ID_CATHEDRAL_4 = 10, + WORGEN_ID_CATHEDRAL_5 = 11, + WORGEN_ID_CATHEDRAL_6 = 12, + WORGEN_ID_CATHEDRAL_7 = 13, + WORGEN_ID_CATHEDRAL_8 = 14, + WORGEN_ID_CATHEDRAL_9 = 15, + + NPC_WORGEN_RUNT_SPELL = 35188, +}; + +static Position const RuntSpellSummonJumpPos = { -1671.915f, 1446.734f, 52.28712f }; + +struct npc_worgen_runt : public ScriptedAI +{ + npc_worgen_runt(Creature* creature) : ScriptedAI(creature), _worgenID(0), _wayPointCounter(0), _jumped(false) { } + + void JustEngagedWith(Unit* /*who*/) override + { + _events.SetPhase(PHASE_COMBAT); + } + + void IsSummonedBy(Unit* summoner) override + { + me->setActive(true); // we are in a phased and cut off map so we're fine to use that here + _events.SetPhase(PHASE_ROOF); + _events.ScheduleEvent(EVENT_FORCE_DESPAWN, 70s, 0, PHASE_ROOF); + _playerGuid = summoner->GetGUID(); + if (me->GetEntry() == NPC_WORGEN_RUNT_SPELL) + me->GetMotionMaster()->MoveJump(RuntSpellSummonJumpPos, 16.0f, 4.371286f); + } + + void DoAction(int32 action) override + { + _worgenID = action; + switch (_worgenID) // ugly as fuck but needed for individual controls + { + // House roof cases + case WORGEN_ID_ROOF_1: + _wayPointCounter = runtHousePathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize1, worgenRuntHousePath1, runtHousePathSize1); + break; + case WORGEN_ID_ROOF_2: + _wayPointCounter = runtHousePathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize2, worgenRuntHousePath2, runtHousePathSize2); + break; + case WORGEN_ID_ROOF_3: + _wayPointCounter = runtHousePathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize3, worgenRuntHousePath3, runtHousePathSize3); + break; + case WORGEN_ID_ROOF_4: + _wayPointCounter = runtHousePathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize4, worgenRuntHousePath4, runtHousePathSize4); + break; + case WORGEN_ID_ROOF_5: + _wayPointCounter = runtHousePathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize5, worgenRuntHousePath5, runtHousePathSize5); + break; + case WORGEN_ID_ROOF_6: + _wayPointCounter = runtHousePathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize6, worgenRuntHousePath6, runtHousePathSize6); + break; + case WORGEN_ID_ROOF_7: + _wayPointCounter = runtHousePathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize7, worgenRuntHousePath7, runtHousePathSize7); + break; + // Cathedral cases + case WORGEN_ID_CATHEDRAL_1: + _wayPointCounter = runtCathedralPathSize1; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize1, worgenRuntCathedralPath1, runtCathedralPathSize1); + break; + case WORGEN_ID_CATHEDRAL_2: + _wayPointCounter = runtCathedralPathSize2; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize2, worgenRuntCathedralPath1, runtCathedralPathSize2); + break; + case WORGEN_ID_CATHEDRAL_3: + _wayPointCounter = runtCathedralPathSize3; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize3, worgenRuntCathedralPath1, runtCathedralPathSize3); + break; + case WORGEN_ID_CATHEDRAL_4: + _wayPointCounter = runtCathedralPathSize4; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize4, worgenRuntCathedralPath1, runtCathedralPathSize4); + break; + case WORGEN_ID_CATHEDRAL_5: + _wayPointCounter = runtCathedralPathSize5; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize5, worgenRuntCathedralPath1, runtCathedralPathSize5); + break; + case WORGEN_ID_CATHEDRAL_6: + _wayPointCounter = runtCathedralPathSize6; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize6, worgenRuntCathedralPath1, runtCathedralPathSize6); + break; + case WORGEN_ID_CATHEDRAL_7: + _wayPointCounter = runtCathedralPathSize7; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize7, worgenRuntCathedralPath1, runtCathedralPathSize7); + break; + case WORGEN_ID_CATHEDRAL_8: + _wayPointCounter = runtCathedralPathSize8; + me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize8, worgenRuntCathedralPath1, runtCathedralPathSize8); + break; + } + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == _wayPointCounter && !_jumped) + { + _jumped = true; + _events.ScheduleEvent(EVENT_JUMP_TO_PRISON, 1ms); + } + } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim() && !_events.IsInPhase(PHASE_ROOF)) + return; + + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_FORCE_DESPAWN: + me->DespawnOrUnsummon(); + break; + case EVENT_JUMP_TO_PRISON: + me->GetMotionMaster()->MoveJump(worgenRuntJumpPos[_worgenID], 16.0f, _worgenID < WORGEN_ID_CATHEDRAL_1 ? 19.2911f : frand(3.945607f, 4.852813f)); + me->SetHomePosition(worgenRuntJumpPos[_worgenID]); + _events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s); + break; + case EVENT_AGGRO_PLAYER: + { + Creature* crowley = me->FindNearestCreature(35077, 100.0f, true); + Creature* tobias = me->FindNearestCreature(35124, 100.0f, true); + + if (crowley && tobias) + { + if (me->GetDistance(crowley) <= me->GetDistance(tobias)) + AttackStart(crowley); + else + AttackStart(tobias); + } + else if (crowley) + AttackStart(crowley); + else if (tobias) + AttackStart(tobias); + + break; + } + default: + break; + } + } + + DoMeleeAttackIfReady(); + } +private: + uint32 _worgenID; + uint32 _wayPointCounter; + bool _jumped; + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14159 - The Rebel Lord's Arsenal +######*/ + +enum TheRebelLordsArsena +{ + PHASE_ID_SUMMON = 170, + PHASE_ID_WOUND = 171, + NPC_LORNA_CROWLEY = 35378, + NPC_GENERIC_TRIGGER_LAB = 35374, + + SPELL_SHOOT_INSTAKILL = 67593, + SPELL_COSMETIC_ATTACK = 42880, + SPELL_PULL_TO = 67357, + + EVENT_COSMETIC_ATTACK = 1, + EVENT_JUMP_TO_PLAYER, + EVENT_SHOOT_JOSIAH +}; + +static Position const JosiahJumpPos = { -1796.63f, 1427.73f, 12.4624f }; + +struct npc_josiah_avery : public PassiveAI +{ + npc_josiah_avery(Creature* creature) : PassiveAI(creature) { } + + void IsSummonedBy(Unit* summoner) override + { + PhasingHandler::AddPhase(me, PHASE_ID_SUMMON, true); + PhasingHandler::AddPhase(me, PHASE_ID_WOUND, true); + _playerGuid = summoner->GetGUID(); + _events.ScheduleEvent(EVENT_COSMETIC_ATTACK, 500ms); + } + + void JustDied(Unit* /*killer*/) override + { + me->DespawnOrUnsummon(5s); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_COSMETIC_ATTACK: + if (Player* player = ObjectAccessor::GetPlayer(*me, _playerGuid)) + { + DoCast(player, SPELL_COSMETIC_ATTACK); + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + if (Creature* labTrigger = lorna->FindNearestCreature(NPC_GENERIC_TRIGGER_LAB, 5.0f, true)) + labTrigger->CastSpell(player, SPELL_PULL_TO); + + _events.ScheduleEvent(EVENT_JUMP_TO_PLAYER, 1s); + } + break; + case EVENT_JUMP_TO_PLAYER: + me->GetMotionMaster()->MoveJump(JosiahJumpPos, 10.0f, 14.18636f); + _events.ScheduleEvent(EVENT_SHOOT_JOSIAH, 500ms); + break; + case EVENT_SHOOT_JOSIAH: + if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true)) + lorna->CastSpell(me, SPELL_SHOOT_INSTAKILL, true); + break; + default: + break; + } + } + } +private: + ObjectGuid _playerGuid; + EventMap _events; +}; + +/*###### +## Quest 14293 - Save Krennan Aranas +######*/ + +static Position const GreymanesHorseJumpPos = { -1676.16f, 1346.19f, 15.1349f }; + +enum GreymanesHorse +{ + SAY_ANNOUNCE_RESCUE = 0, + SAY_RESCUED = 0, + SAY_TRAPPED = 0, + + PATH_KRENNAN_TREE = 0, + PATH_KRENNAN_BACK = 1, + + EVENT_START_PATH_1 = 1, + EVENT_START_PATH_2, + EVENT_JUMP_TO_KRENNAN, + EVENT_ANNOUNCE_RESCUE, + EVENT_DISMOUNT_PLAYER, + + NPC_RESCUED_KRENNAN = 35907, + NPC_TRAPPED_KRENNAN = 35753, +}; + +struct npc_greymanes_horse : public VehicleAI +{ + npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { } + + void DamageTaken(Unit* /*attacker*/, uint32& damage) override + { + if (me->GetHealth() <= 1) + damage = 0; + else if (damage >= me->GetHealth()) + damage = me->GetHealth() - 1; + } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_START_PATH_1, 1s); + } + else if (apply && passenger->GetEntry() == NPC_RESCUED_KRENNAN) + _events.ScheduleEvent(EVENT_START_PATH_2, 1s); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == EFFECT_MOTION_TYPE && pointId == pathSize1 && _currentPath == PATH_KRENNAN_TREE) + _events.ScheduleEvent(EVENT_JUMP_TO_KRENNAN, 1ms); + else if (type == EFFECT_MOTION_TYPE && pointId == pathSize2 && _currentPath == PATH_KRENNAN_BACK) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_PATH_1: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_TREE; + me->GetMotionMaster()->MoveSmoothPath(pathSize1, greymanesHorsePath1, pathSize1); + break; + case EVENT_JUMP_TO_KRENNAN: + me->GetMotionMaster()->MoveJump(GreymanesHorseJumpPos, 16.0f, 14.00968f); + _events.ScheduleEvent(EVENT_ANNOUNCE_RESCUE, 2s); + break; + case EVENT_ANNOUNCE_RESCUE: + me->SetControlled(true, UNIT_STATE_ROOT); + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(0)) + { + Talk(SAY_ANNOUNCE_RESCUE, passenger); + if (Creature* krennan = me->FindNearestCreature(NPC_TRAPPED_KRENNAN, 30.0f, true)) + krennan->AI()->Talk(SAY_TRAPPED, passenger); + } + break; + case EVENT_START_PATH_2: + me->SetControlled(true, UNIT_STATE_ROOT); + _currentPath = PATH_KRENNAN_BACK; + me->GetMotionMaster()->MoveSmoothPath(pathSize2, greymanesHorsePath2, pathSize2); + break; + case EVENT_DISMOUNT_PLAYER: + if (Unit* passenger = me->GetVehicleKit()->GetPassenger(1)) + { + if (Unit* player = me->GetVehicleKit()->GetPassenger(0)) + { + if (Creature* krennan = passenger->ToCreature()) + { + std::vector storedAttackers; + + for (Unit* attacker : me->getAttackers()) + storedAttackers.push_back(attacker); + + for (Unit* attacker : storedAttackers) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + player->ExitVehicle(); + krennan->AddUnitState(UNIT_STATE_ROOT); + krennan->ExitVehicle(); + krennan->AI()->Talk(SAY_RESCUED, me); + krennan->DespawnOrUnsummon(7s); + } + } + } + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +/*###### +## Quest 14212 - Sacrifices +######*/ + +enum CrowleysHorse +{ + EVENT_JUMP_OVER_BARRICADES_1 = 1, + EVENT_JUMP_OVER_BARRICADES_2, + EVENT_MOVE_PATH_MAIN_1, + EVENT_MOVE_PATH_MAIN_2, + EVENT_MOVE_OFF_PATH = 6, + + PATH_ID_CROWLEYS_HORSE_1 = 352310, + PATH_ID_CROWLEYS_HORSE_2 = 352311, + PATH_ID_CROWLEYS_HORSE_3 = 444280, + + SPELL_THROW_TORCH = 67063, + NPC_CROWLEYS_HORSE_2 = 44428, +}; + +static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f }; +static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f }; + +struct npc_crowleys_horse : public VehicleAI +{ + npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { } + + void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override + { + if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_1, 2s); + } + else if (apply && passenger->GetTypeId() == TYPEID_PLAYER) + { + me->SetControlled(true, UNIT_STATE_ROOT); + _events.ScheduleEvent(EVENT_MOVE_OFF_PATH, 2s); + } + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == WAYPOINT_MOTION_TYPE && pointId == 14 && _currentPath == PATH_ID_CROWLEYS_HORSE_1) + _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 15 && _currentPath == PATH_ID_CROWLEYS_HORSE_2) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + else if (type == WAYPOINT_MOTION_TYPE && pointId == 16 && _currentPath == PATH_ID_CROWLEYS_HORSE_3) + _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms); + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_JUMP_OVER_BARRICADES_1: + me->SetControlled(false, UNIT_STATE_ROOT); + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 2s); + break; + case EVENT_MOVE_PATH_MAIN_1: + me->GetMotionMaster()->MovePoint(1001, -1686.12f, 1655.64f, 20.6106f); + _currentPath = PATH_ID_CROWLEYS_HORSE_1; + break; + case EVENT_JUMP_OVER_BARRICADES_2: + me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f); + _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_2, 2s); + break; + case EVENT_MOVE_PATH_MAIN_2: + _currentPath = PATH_ID_CROWLEYS_HORSE_2; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_2, false); + break; + case EVENT_DISMOUNT_PLAYER: + { + std::set attackersCopy = me->getAttackers(); + for (Unit* attacker : attackersCopy) + { + if (Creature* creature = attacker->ToCreature()) + if (creature->IsAIEnabled()) + creature->AI()->EnterEvadeMode(); + } + + me->RemoveAurasDueToSpell(VEHICLE_SPELL_RIDE_HARDCODED); + me->DespawnOrUnsummon(5s); + break; + } + case EVENT_MOVE_OFF_PATH: + me->SetControlled(false, UNIT_STATE_ROOT); + _currentPath = PATH_ID_CROWLEYS_HORSE_3; + me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_3, false); + break; + default: + break; + } + } + } +private: + EventMap _events; + uint32 _currentPath; +}; + +enum GileanCrow +{ + SPELL_GILNEAN_CROW = 93275, + EVENT_APPLY_HOVER_BYTES = 1, + EVENT_FLY_AWAY_1 = 2, + EVENT_FLY_AWAY_2 = 3, + + POINT_NONE = 0, + POINT_CROW_FLIGHT = 1 +}; + +struct npc_gilnean_crow : public PassiveAI +{ + npc_gilnean_crow(Creature* creature) : PassiveAI(creature) { } + + void SpellHit(WorldObject* /*caster*/, SpellInfo const* spell) override + { + if (spell->Id == SPELL_GILNEAN_CROW) + _events.ScheduleEvent(EVENT_APPLY_HOVER_BYTES, 500ms); + } + + void MovementInform(uint32 type, uint32 pointId) override + { + if (type == POINT_MOTION_TYPE && pointId == POINT_CROW_FLIGHT) + { + Position pos = me->GetRandomNearPosition(8.0f); + pos.m_positionZ = me->GetPositionZ() + 10.0f; + me->GetMotionMaster()->MovePoint(POINT_NONE, pos); + me->DespawnOrUnsummon(14s); + } + } + + void UpdateAI(uint32 diff) override + { + _events.Update(diff); + + while (uint32 eventId = _events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_APPLY_HOVER_BYTES: + me->SetStandState(UNIT_STAND_STATE_SIT); + me->SetAnimTier(AnimTier::Submerged); + _events.ScheduleEvent(EVENT_FLY_AWAY_1, 1s); + break; + case EVENT_FLY_AWAY_1: + { + Position pos = me->GetRandomNearPosition(7.0f); + pos.m_positionZ = me->GetPositionZ() + 8.0f; + me->GetMotionMaster()->MovePoint(POINT_CROW_FLIGHT, pos); + break; + } + default: + break; + } + } + } +private: + EventMap _events; +}; +} + +void AddSC_gilneas_chapter_1() +{ + using namespace Gilneas::Chapter1; + RegisterCreatureAI(npc_frightened_citizen); + RegisterCreatureAI(npc_worgen_runt); + RegisterCreatureAI(npc_josiah_avery); + RegisterCreatureAI(npc_greymanes_horse); + RegisterCreatureAI(npc_crowleys_horse); + RegisterCreatureAI(npc_gilnean_crow); +} diff --git a/src/server/scripts/EasternKingdoms/arathi_highlands.cpp b/src/server/scripts/EasternKingdoms/arathi_highlands.cpp new file mode 100644 index 0000000000..24efdb2fbf --- /dev/null +++ b/src/server/scripts/EasternKingdoms/arathi_highlands.cpp @@ -0,0 +1,671 @@ +/* + * Vcore - Arathi Highlands + * Quest 26050: Goggle Boggle + */ + +#include "ScriptMgr.h" +#include "ScriptedCreature.h" +#include "Player.h" +#include "ObjectAccessor.h" +#include "MotionMaster.h" +#include "TemporarySummon.h" + +enum PhizzlethorpeData +{ + QUEST_GOGGLE_BOGGLE = 26050, + + NPC_VENGEFUL_SURGE = 2776, + + POINT_OUTBOUND_1 = 1, + POINT_CAVE_OUTSIDE = 2, + POINT_CAVE_INSIDE = 3, + POINT_STONE = 4, + POINT_RETURN_CAVE_IN = 5, + POINT_RETURN_CAVE_OUT = 6, + POINT_RETURN_1 = 7, + POINT_RETURN_HOME = 8, + + POINT_CAVE_LEFT_1 = 9, + POINT_CAVE_LEFT_2 = 10, + + EVENT_START_MOVING = 1, + EVENT_ENTER_CAVE = 2, + EVENT_START_WORKING = 3, + EVENT_SPAWN_AMBUSH = 4, + EVENT_ALMOST_DONE = 5, + EVENT_BEGIN_RETURN = 6, + EVENT_FINISH_QUEST = 7, + EVENT_CHECK_PLAYER = 8, + EVENT_TIMEOUT = 9 +}; + +static Position const PhizzlethorpeStart = +{ + -2066.46f, -2085.97f, 8.92410f, 0.0f +}; + +static Position const PhizzlethorpePoint1 = +{ + -2078.255371f, -2092.058105f, 9.744357f, 0.0f +}; + +static Position const PhizzlethorpeCaveOutside = +{ + -2072.194580f, -2127.455322f, 19.688131f, 0.0f +}; + +/* + * Point recorded directly behind the cave threshold. + * This prevents MovePoint from trying to cut sideways through + * the cave wall when travelling from outside to the rune stone. + */ +static Position const PhizzlethorpeCaveInside = +{ + -2069.170166f, -2129.804443f, 21.074938f, 5.562022f +}; + +static Position const PhizzlethorpeCaveLeft1 = +{ + -2062.205322f, -2129.687500f, 21.316385f, 5.452067f +}; + +static Position const PhizzlethorpeCaveLeft2 = +{ + -2056.376465f, -2137.908203f, 20.625616f, 5.419080f +}; + +static Position const PhizzlethorpeStone = +{ + -2043.507202f, -2153.291016f, 20.177914f, 0.0f +}; + +static Position const SurgePosition1 = +{ + -2040.5f, -2150.5f, 20.17f, 0.0f +}; + +static Position const SurgePosition2 = +{ + -2046.5f, -2155.5f, 20.17f, 0.0f +}; + +class npc_professor_phizzlethorpe : public CreatureScript +{ +public: + npc_professor_phizzlethorpe() : CreatureScript("npc_professor_phizzlethorpe") { } + + struct npc_professor_phizzlethorpeAI : public ScriptedAI + { + npc_professor_phizzlethorpeAI(Creature* creature) + : ScriptedAI(creature), summons(me) + { + } + + EventMap events; + SummonList summons; + ObjectGuid playerGuid; + + uint8 surgeDeaths = 0; + uint8 surgeCount = 0; + + bool eventActive = false; + bool returning = false; + bool waitingForAmbush = false; + + void ResetWorkEmote() + { + me->SetUInt32Value(UNIT_NPC_EMOTESTATE, 0); + } + + void Reset() override + { + /* + * Combat evade/reset can happen during an active escort. + * Do not destroy the running quest state in that case. + */ + if (eventActive) + return; + + events.Reset(); + summons.DespawnAll(); + + playerGuid.Clear(); + + surgeDeaths = 0; + surgeCount = 0; + + returning = false; + waitingForAmbush = false; + + ResetWorkEmote(); + } + + Player* GetEscortPlayer() + { + if (playerGuid.IsEmpty()) + return nullptr; + + return ObjectAccessor::GetPlayer(*me, playerGuid); + } + + void StartEvent(Player* player) + { + if (!player || eventActive) + return; + + eventActive = true; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid = player->GetGUID(); + + events.Reset(); + summons.DespawnAll(); + + me->CombatStop(true); + ResetWorkEmote(); + + /* + * Outbound trip is deliberately walked. + */ + me->SetWalk(true); + + /* + * creature_text group 0: + * "Ok, $n. Follow me to the cave..." + */ + Talk(0, player); + + /* + * Give the opening line time to breathe before moving. + */ + events.ScheduleEvent(EVENT_START_MOVING, 3000); + + events.ScheduleEvent(EVENT_CHECK_PLAYER, 1000); + events.ScheduleEvent(EVENT_TIMEOUT, 300000); + } + + void AbortEvent(bool failQuest) + { + Player* player = GetEscortPlayer(); + + if (failQuest && player && + player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + { + player->FailQuest(QUEST_GOGGLE_BOGGLE); + } + + events.Reset(); + summons.DespawnAll(); + + ResetWorkEmote(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid.Clear(); + + me->CombatStop(true); + me->GetMotionMaster()->Clear(); + + /* + * Do NOT despawn the professor. + * He is the real questgiver spawn and has a long DB respawn. + */ + me->SetWalk(false); + me->GetMotionMaster()->MoveTargetedHome(); + } + + void AmbushFinished() + { + if (!eventActive || !waitingForAmbush) + return; + + waitingForAmbush = false; + + /* + * Stop the "working on the goggles" animation now that + * the extraction is finished. + */ + ResetWorkEmote(); + + me->CombatStop(true); + + /* + * Small pause after the last attacker dies. + */ + events.ScheduleEvent(EVENT_BEGIN_RETURN, 2000); + } + + void FinishEvent() + { + Player* player = GetEscortPlayer(); + + if (player && + player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + { + /* + * creature_text group 9: + * Doctor Draxlegauge will give you further instructions... + */ + Talk(9, player); + + player->GroupEventHappens(QUEST_GOGGLE_BOGGLE, me); + } + + events.Reset(); + summons.DespawnAll(); + + ResetWorkEmote(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid.Clear(); + + me->CombatStop(true); + me->SetWalk(false); + } + + void QuestAccept(Player* player, Quest const* quest) override + { + if (quest->GetQuestId() == QUEST_GOGGLE_BOGGLE) + StartEvent(player); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE || !eventActive) + return; + + switch (id) + { + case POINT_OUTBOUND_1: + me->GetMotionMaster()->MovePoint( + POINT_CAVE_OUTSIDE, + PhizzlethorpeCaveOutside); + break; + + case POINT_CAVE_OUTSIDE: + { + /* + * creature_text group 1: + * "I discovered this cave on our first day here..." + */ + if (Player* player = GetEscortPlayer()) + Talk(1, player); + + /* + * Five-second pause outside before entering. + */ + events.ScheduleEvent(EVENT_ENTER_CAVE, 5000); + break; + } + + case POINT_CAVE_INSIDE: + me->GetMotionMaster()->MovePoint( + POINT_CAVE_LEFT_1, + PhizzlethorpeCaveLeft1); + break; + + case POINT_CAVE_LEFT_1: + me->GetMotionMaster()->MovePoint( + POINT_CAVE_LEFT_2, + PhizzlethorpeCaveLeft2); + break; + + case POINT_CAVE_LEFT_2: + me->GetMotionMaster()->MovePoint( + POINT_STONE, + PhizzlethorpeStone); + break; + + case POINT_STONE: + { + /* + * creature_text group 2: + * Player is told to defend the professor. + */ + if (Player* player = GetEscortPlayer()) + Talk(2, player); + + /* + * He studies/prepares at the stone for five seconds. + */ + events.ScheduleEvent(EVENT_START_WORKING, 5000); + break; + } + + case POINT_RETURN_CAVE_IN: + me->GetMotionMaster()->MovePoint( + POINT_RETURN_CAVE_OUT, + PhizzlethorpeCaveOutside); + break; + + case POINT_RETURN_CAVE_OUT: + { + /* + * creature_text group 7: + * "Phew! Glad to be back from that creepy cave." + */ + if (Player* player = GetEscortPlayer()) + Talk(7, player); + + me->GetMotionMaster()->MovePoint( + POINT_RETURN_1, + PhizzlethorpePoint1); + break; + } + + case POINT_RETURN_1: + me->GetMotionMaster()->MovePoint( + POINT_RETURN_HOME, + PhizzlethorpeStart); + break; + + case POINT_RETURN_HOME: + { + /* + * creature_text group 8: + * emote about handing the glowing goggles over. + */ + if (Player* player = GetEscortPlayer()) + Talk(8, player); + + /* + * Give the final emote a moment before the last line + * and quest completion. + */ + events.ScheduleEvent(EVENT_FINISH_QUEST, 2000); + break; + } + + default: + break; + } + } + + void JustSummoned(Creature* summon) override + { + summons.Summon(summon); + + if (summon->GetEntry() == NPC_VENGEFUL_SURGE) + { + ++surgeCount; + + /* + * The player is supposed to protect Phizzlethorpe. + */ + summon->AI()->AttackStart(me); + } + } + + void SummonedCreatureDies(Creature* summon, Unit* /*killer*/) override + { + if (!eventActive || + !waitingForAmbush || + summon->GetEntry() != NPC_VENGEFUL_SURGE) + { + return; + } + + ++surgeDeaths; + + if (surgeCount > 0 && surgeDeaths >= surgeCount) + AmbushFinished(); + } + + void JustDied(Unit* /*killer*/) override + { + ResetWorkEmote(); + + if (Player* player = GetEscortPlayer()) + { + if (player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + player->FailQuest(QUEST_GOGGLE_BOGGLE); + } + + events.Reset(); + summons.DespawnAll(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + } + + void UpdateAI(uint32 diff) override + { + events.Update(diff); + + while (uint32 eventId = events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_MOVING: + { + if (!eventActive) + break; + + me->GetMotionMaster()->MovePoint( + POINT_OUTBOUND_1, + PhizzlethorpePoint1); + break; + } + + case EVENT_ENTER_CAVE: + { + if (!eventActive || returning) + break; + + /* + * Newly recorded point immediately behind the cave + * threshold. This forces a sane crossing of the entrance. + */ + me->GetMotionMaster()->MovePoint( + POINT_CAVE_INSIDE, + PhizzlethorpeCaveInside); + break; + } + + case EVENT_START_WORKING: + { + if (!eventActive || returning) + break; + + /* + * creature_text group 3 is the visible emote text: + * "...begins tinkering with the goggles..." + */ + if (Player* player = GetEscortPlayer()) + Talk(3, player); + + /* + * Emote 69 / STATE_USESTANDING. + * Verified ingame for this character/model. + */ + me->SetUInt32Value( + UNIT_NPC_EMOTESTATE, + EMOTE_STATE_USE_STANDING); + + /* + * Let him visibly work for four seconds before + * anything attacks. + */ + events.ScheduleEvent(EVENT_SPAWN_AMBUSH, 4000); + break; + } + + case EVENT_SPAWN_AMBUSH: + { + if (!eventActive || returning) + break; + + waitingForAmbush = true; + surgeDeaths = 0; + surgeCount = 0; + + /* + * creature_text group 4 has two possible help lines. + */ + if (Player* player = GetEscortPlayer()) + Talk(4, player); + + me->SummonCreature( + NPC_VENGEFUL_SURGE, + SurgePosition1, + TEMPSUMMON_CORPSE_TIMED_DESPAWN, + 5000); + + me->SummonCreature( + NPC_VENGEFUL_SURGE, + SurgePosition2, + TEMPSUMMON_CORPSE_TIMED_DESPAWN, + 5000); + + /* + * Mid-event encouragement. + */ + events.ScheduleEvent(EVENT_ALMOST_DONE, 5000); + + /* + * If neither creature could be created, do not leave + * the sole questgiver trapped forever. + */ + if (surgeCount == 0) + AbortEvent(true); + + break; + } + + case EVENT_ALMOST_DONE: + { + if (eventActive && waitingForAmbush) + { + /* + * creature_text group 5: + * "Almost done! Just a little longer!" + */ + if (Player* player = GetEscortPlayer()) + Talk(5, player); + } + + break; + } + + case EVENT_BEGIN_RETURN: + { + if (!eventActive) + break; + + returning = true; + waitingForAmbush = false; + + ResetWorkEmote(); + + /* + * creature_text group 6: + * "I've done it!... Let's get out of here!" + */ + if (Player* player = GetEscortPlayer()) + Talk(6, player); + + /* + * Return trip may be faster than the deliberate + * outbound walk. + */ + me->SetWalk(false); + + me->GetMotionMaster()->MovePoint( + POINT_RETURN_CAVE_IN, + PhizzlethorpeCaveInside); + break; + } + + case EVENT_FINISH_QUEST: + { + if (eventActive) + FinishEvent(); + + break; + } + + case EVENT_CHECK_PLAYER: + { + if (!eventActive) + break; + + Player* player = GetEscortPlayer(); + + /* + * Logout / object no longer available: + * restore professor without artificially failing + * an offline player's quest. + */ + if (!player) + { + AbortEvent(false); + break; + } + + /* + * Player abandoned or otherwise no longer has the + * quest active. + */ + if (player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) != QUEST_STATUS_INCOMPLETE) + { + AbortEvent(false); + break; + } + + /* + * Actual escort failure. + */ + if (!player->IsAlive() || me->GetDistance(player) > 120.0f) + { + AbortEvent(true); + break; + } + + events.ScheduleEvent(EVENT_CHECK_PLAYER, 1000); + break; + } + + case EVENT_TIMEOUT: + { + if (eventActive) + AbortEvent(true); + + break; + } + + default: + break; + } + } + + if (UpdateVictim()) + DoMeleeAttackIfReady(); + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_professor_phizzlethorpeAI(creature); + } +}; + +void AddSC_arathi_highlands() +{ + new npc_professor_phizzlethorpe(); +} diff --git a/src/server/scripts/EasternKingdoms/arathi_highlands.cpp.bak_20261004_104948 b/src/server/scripts/EasternKingdoms/arathi_highlands.cpp.bak_20261004_104948 new file mode 100644 index 0000000000..a34c5ec8d8 --- /dev/null +++ b/src/server/scripts/EasternKingdoms/arathi_highlands.cpp.bak_20261004_104948 @@ -0,0 +1,409 @@ +#include "ScriptMgr.h" +#include "ScriptedCreature.h" +#include "Player.h" +#include "ObjectAccessor.h" +#include "MotionMaster.h" +#include "TemporarySummon.h" + +enum GoggleBoggle +{ + QUEST_GOGGLE_BOGGLE = 26050, + NPC_VENGEFUL_SURGE = 2776, + + POINT_P1 = 1, + POINT_P2 = 2, + POINT_P3 = 3, + POINT_RETURN_P2 = 4, + POINT_RETURN_P1 = 5, + POINT_RETURN_HOME = 6, + + EVENT_CHECK_PLAYER = 1, + EVENT_TIMEOUT = 2 +}; + +Position const PhizzlethorpeP1 = +{ + -2078.255371f, -2092.058105f, 9.744357f, 0.0f +}; + +Position const PhizzlethorpeP2 = +{ + -2072.194580f, -2127.455322f, 19.688131f, 0.0f +}; + +Position const PhizzlethorpeP3 = +{ + -2043.507202f, -2153.291016f, 20.177914f, 0.0f +}; + +Position const PhizzlethorpeHome = +{ + -2066.46f, -2085.97f, 8.92410f, 0.0f +}; + +class npc_professor_phizzlethorpe : public CreatureScript +{ +public: + npc_professor_phizzlethorpe() + : CreatureScript("npc_professor_phizzlethorpe") + { + } + + struct npc_professor_phizzlethorpeAI : public ScriptedAI + { + npc_professor_phizzlethorpeAI(Creature* creature) + : ScriptedAI(creature), summons(me) + { + } + + EventMap events; + SummonList summons; + + ObjectGuid playerGuid; + + uint8 surgeDeaths = 0; + uint8 surgeCount = 0; + + bool eventActive = false; + bool returning = false; + bool waitingForAmbush = false; + + void Reset() override + { + // Do not destroy an active escort merely because combat AI resets. + if (eventActive) + return; + + events.Reset(); + summons.DespawnAll(); + + playerGuid.Clear(); + + surgeDeaths = 0; + surgeCount = 0; + + returning = false; + waitingForAmbush = false; + } + + void StartEvent(Player* player) + { + if (!player || eventActive) + return; + + playerGuid = player->GetGUID(); + + surgeDeaths = 0; + surgeCount = 0; + + eventActive = true; + returning = false; + waitingForAmbush = false; + + events.Reset(); + summons.DespawnAll(); + + // Check regularly whether the player can still continue the event. + events.ScheduleEvent(EVENT_CHECK_PLAYER, 1000); + + // Hard safety timeout: the professor must never remain stuck forever. + events.ScheduleEvent(EVENT_TIMEOUT, 300000); + + me->GetMotionMaster()->MovePoint( + POINT_P1, + PhizzlethorpeP1.GetPositionX(), + PhizzlethorpeP1.GetPositionY(), + PhizzlethorpeP1.GetPositionZ() + ); + } + + void AbortEvent(bool failQuest) + { + if (!eventActive) + return; + + if (failQuest) + { + if (Player* player = ObjectAccessor::GetPlayer(*me, playerGuid)) + { + if (player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + player->FailQuest(QUEST_GOGGLE_BOGGLE); + } + } + + events.Reset(); + summons.DespawnAll(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid.Clear(); + + me->CombatStop(true); + me->GetMotionMaster()->Clear(); + me->GetMotionMaster()->MoveTargetedHome(); + } + + void BeginReturn() + { + if (!eventActive || returning) + return; + + waitingForAmbush = false; + returning = true; + + me->CombatStop(true); + + me->GetMotionMaster()->MovePoint( + POINT_RETURN_P2, + PhizzlethorpeP2.GetPositionX(), + PhizzlethorpeP2.GetPositionY(), + PhizzlethorpeP2.GetPositionZ() + ); + } + + void FinishEvent() + { + if (!eventActive) + return; + + if (Player* player = ObjectAccessor::GetPlayer(*me, playerGuid)) + { + if (player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + player->GroupEventHappens(QUEST_GOGGLE_BOGGLE, me); + } + + events.Reset(); + summons.DespawnAll(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid.Clear(); + } + + void QuestAccept(Player* player, Quest const* quest) override + { + if (quest->GetQuestId() == QUEST_GOGGLE_BOGGLE) + StartEvent(player); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE || !eventActive) + return; + + switch (id) + { + case POINT_P1: + me->GetMotionMaster()->MovePoint( + POINT_P2, + PhizzlethorpeP2.GetPositionX(), + PhizzlethorpeP2.GetPositionY(), + PhizzlethorpeP2.GetPositionZ() + ); + break; + + case POINT_P2: + me->GetMotionMaster()->MovePoint( + POINT_P3, + PhizzlethorpeP3.GetPositionX(), + PhizzlethorpeP3.GetPositionY(), + PhizzlethorpeP3.GetPositionZ() + ); + break; + + case POINT_P3: + { + waitingForAmbush = true; + surgeDeaths = 0; + surgeCount = 0; + + Position surgePos1 = + { + -2040.5f, -2150.5f, 20.17f, 0.0f + }; + + Position surgePos2 = + { + -2046.5f, -2155.5f, 20.17f, 0.0f + }; + + if (TempSummon* surge = me->SummonCreature( + NPC_VENGEFUL_SURGE, + surgePos1, + TEMPSUMMON_CORPSE_TIMED_DESPAWN, + 5000)) + { + surge->AI()->AttackStart(me); + } + + if (TempSummon* surge = me->SummonCreature( + NPC_VENGEFUL_SURGE, + surgePos2, + TEMPSUMMON_CORPSE_TIMED_DESPAWN, + 5000)) + { + surge->AI()->AttackStart(me); + } + + // If neither summon could be created, do not leave the + // professor permanently stuck in the cave. + if (surgeCount == 0) + AbortEvent(true); + + break; + } + + case POINT_RETURN_P2: + me->GetMotionMaster()->MovePoint( + POINT_RETURN_P1, + PhizzlethorpeP1.GetPositionX(), + PhizzlethorpeP1.GetPositionY(), + PhizzlethorpeP1.GetPositionZ() + ); + break; + + case POINT_RETURN_P1: + me->GetMotionMaster()->MovePoint( + POINT_RETURN_HOME, + PhizzlethorpeHome.GetPositionX(), + PhizzlethorpeHome.GetPositionY(), + PhizzlethorpeHome.GetPositionZ() + ); + break; + + case POINT_RETURN_HOME: + FinishEvent(); + break; + + default: + break; + } + } + + void JustSummoned(Creature* summon) override + { + summons.Summon(summon); + + if (summon->GetEntry() == NPC_VENGEFUL_SURGE) + ++surgeCount; + } + + void SummonedCreatureDies(Creature* summon, Unit* /*killer*/) override + { + if (!eventActive || !waitingForAmbush) + return; + + if (!summon || summon->GetEntry() != NPC_VENGEFUL_SURGE) + return; + + ++surgeDeaths; + + if (surgeCount > 0 && surgeDeaths >= surgeCount) + BeginReturn(); + } + + void JustDied(Unit* /*killer*/) override + { + if (Player* player = ObjectAccessor::GetPlayer(*me, playerGuid)) + { + if (player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + player->FailQuest(QUEST_GOGGLE_BOGGLE); + } + + events.Reset(); + summons.DespawnAll(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid.Clear(); + + // The professor is the real DB spawn. His normal 300 second + // creature respawn handles an actual death. + } + + void UpdateAI(uint32 diff) override + { + events.Update(diff); + + while (uint32 eventId = events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_CHECK_PLAYER: + { + if (!eventActive) + break; + + Player* player = ObjectAccessor::GetPlayer(*me, playerGuid); + + // Logout: clean up and return home. The player cannot + // be failed while no Player object exists. + if (!player) + { + AbortEvent(false); + break; + } + + // Quest was abandoned, rewarded or otherwise removed. + if (player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) != QUEST_STATUS_INCOMPLETE) + { + AbortEvent(false); + break; + } + + // Death or excessive distance means a failed escort. + if (!player->IsAlive() || !player->IsWithinDistInMap(me, 120.0f)) + { + AbortEvent(true); + break; + } + + events.ScheduleEvent(EVENT_CHECK_PLAYER, 1000); + break; + } + + case EVENT_TIMEOUT: + { + if (eventActive) + AbortEvent(true); + + break; + } + + default: + break; + } + } + + if (!UpdateVictim()) + return; + + DoMeleeAttackIfReady(); + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_professor_phizzlethorpeAI(creature); + } +}; + +void AddSC_arathi_highlands() +{ + new npc_professor_phizzlethorpe(); +} diff --git a/src/server/scripts/EasternKingdoms/arathi_highlands.cpp.bak_route_20261004_115151 b/src/server/scripts/EasternKingdoms/arathi_highlands.cpp.bak_route_20261004_115151 new file mode 100644 index 0000000000..d4e74e0643 --- /dev/null +++ b/src/server/scripts/EasternKingdoms/arathi_highlands.cpp.bak_route_20261004_115151 @@ -0,0 +1,646 @@ +/* + * Vcore - Arathi Highlands + * Quest 26050: Goggle Boggle + */ + +#include "ScriptMgr.h" +#include "ScriptedCreature.h" +#include "Player.h" +#include "ObjectAccessor.h" +#include "MotionMaster.h" +#include "TemporarySummon.h" + +enum PhizzlethorpeData +{ + QUEST_GOGGLE_BOGGLE = 26050, + + NPC_VENGEFUL_SURGE = 2776, + + POINT_OUTBOUND_1 = 1, + POINT_CAVE_OUTSIDE = 2, + POINT_CAVE_INSIDE = 3, + POINT_STONE = 4, + POINT_RETURN_CAVE_IN = 5, + POINT_RETURN_CAVE_OUT = 6, + POINT_RETURN_1 = 7, + POINT_RETURN_HOME = 8, + + EVENT_START_MOVING = 1, + EVENT_ENTER_CAVE = 2, + EVENT_START_WORKING = 3, + EVENT_SPAWN_AMBUSH = 4, + EVENT_ALMOST_DONE = 5, + EVENT_BEGIN_RETURN = 6, + EVENT_FINISH_QUEST = 7, + EVENT_CHECK_PLAYER = 8, + EVENT_TIMEOUT = 9 +}; + +static Position const PhizzlethorpeStart = +{ + -2066.46f, -2085.97f, 8.92410f, 0.0f +}; + +static Position const PhizzlethorpePoint1 = +{ + -2078.255371f, -2092.058105f, 9.744357f, 0.0f +}; + +static Position const PhizzlethorpeCaveOutside = +{ + -2072.194580f, -2127.455322f, 19.688131f, 0.0f +}; + +/* + * Point recorded directly behind the cave threshold. + * This prevents MovePoint from trying to cut sideways through + * the cave wall when travelling from outside to the rune stone. + */ +static Position const PhizzlethorpeCaveInside = +{ + -2069.125732f, -2129.864502f, 21.102127f, 5.300940f +}; + +static Position const PhizzlethorpeStone = +{ + -2043.507202f, -2153.291016f, 20.177914f, 0.0f +}; + +static Position const SurgePosition1 = +{ + -2040.5f, -2150.5f, 20.17f, 0.0f +}; + +static Position const SurgePosition2 = +{ + -2046.5f, -2155.5f, 20.17f, 0.0f +}; + +class npc_professor_phizzlethorpe : public CreatureScript +{ +public: + npc_professor_phizzlethorpe() : CreatureScript("npc_professor_phizzlethorpe") { } + + struct npc_professor_phizzlethorpeAI : public ScriptedAI + { + npc_professor_phizzlethorpeAI(Creature* creature) + : ScriptedAI(creature), summons(me) + { + } + + EventMap events; + SummonList summons; + ObjectGuid playerGuid; + + uint8 surgeDeaths = 0; + uint8 surgeCount = 0; + + bool eventActive = false; + bool returning = false; + bool waitingForAmbush = false; + + void ResetWorkEmote() + { + me->SetUInt32Value(UNIT_NPC_EMOTESTATE, 0); + } + + void Reset() override + { + /* + * Combat evade/reset can happen during an active escort. + * Do not destroy the running quest state in that case. + */ + if (eventActive) + return; + + events.Reset(); + summons.DespawnAll(); + + playerGuid.Clear(); + + surgeDeaths = 0; + surgeCount = 0; + + returning = false; + waitingForAmbush = false; + + ResetWorkEmote(); + } + + Player* GetEscortPlayer() + { + if (playerGuid.IsEmpty()) + return nullptr; + + return ObjectAccessor::GetPlayer(*me, playerGuid); + } + + void StartEvent(Player* player) + { + if (!player || eventActive) + return; + + eventActive = true; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid = player->GetGUID(); + + events.Reset(); + summons.DespawnAll(); + + me->CombatStop(true); + ResetWorkEmote(); + + /* + * Outbound trip is deliberately walked. + */ + me->SetWalk(true); + + /* + * creature_text group 0: + * "Ok, $n. Follow me to the cave..." + */ + Talk(0, player); + + /* + * Give the opening line time to breathe before moving. + */ + events.ScheduleEvent(EVENT_START_MOVING, 3000); + + events.ScheduleEvent(EVENT_CHECK_PLAYER, 1000); + events.ScheduleEvent(EVENT_TIMEOUT, 300000); + } + + void AbortEvent(bool failQuest) + { + Player* player = GetEscortPlayer(); + + if (failQuest && player && + player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + { + player->FailQuest(QUEST_GOGGLE_BOGGLE); + } + + events.Reset(); + summons.DespawnAll(); + + ResetWorkEmote(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid.Clear(); + + me->CombatStop(true); + me->GetMotionMaster()->Clear(); + + /* + * Do NOT despawn the professor. + * He is the real questgiver spawn and has a long DB respawn. + */ + me->SetWalk(false); + me->GetMotionMaster()->MoveTargetedHome(); + } + + void AmbushFinished() + { + if (!eventActive || !waitingForAmbush) + return; + + waitingForAmbush = false; + + /* + * Stop the "working on the goggles" animation now that + * the extraction is finished. + */ + ResetWorkEmote(); + + me->CombatStop(true); + + /* + * Small pause after the last attacker dies. + */ + events.ScheduleEvent(EVENT_BEGIN_RETURN, 2000); + } + + void FinishEvent() + { + Player* player = GetEscortPlayer(); + + if (player && + player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + { + /* + * creature_text group 9: + * Doctor Draxlegauge will give you further instructions... + */ + Talk(9, player); + + player->GroupEventHappens(QUEST_GOGGLE_BOGGLE, me); + } + + events.Reset(); + summons.DespawnAll(); + + ResetWorkEmote(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + + surgeDeaths = 0; + surgeCount = 0; + + playerGuid.Clear(); + + me->CombatStop(true); + me->SetWalk(false); + } + + void QuestAccept(Player* player, Quest const* quest) override + { + if (quest->GetQuestId() == QUEST_GOGGLE_BOGGLE) + StartEvent(player); + } + + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE || !eventActive) + return; + + switch (id) + { + case POINT_OUTBOUND_1: + me->GetMotionMaster()->MovePoint( + POINT_CAVE_OUTSIDE, + PhizzlethorpeCaveOutside); + break; + + case POINT_CAVE_OUTSIDE: + { + /* + * creature_text group 1: + * "I discovered this cave on our first day here..." + */ + if (Player* player = GetEscortPlayer()) + Talk(1, player); + + /* + * Five-second pause outside before entering. + */ + events.ScheduleEvent(EVENT_ENTER_CAVE, 5000); + break; + } + + case POINT_CAVE_INSIDE: + me->GetMotionMaster()->MovePoint( + POINT_STONE, + PhizzlethorpeStone); + break; + + case POINT_STONE: + { + /* + * creature_text group 2: + * Player is told to defend the professor. + */ + if (Player* player = GetEscortPlayer()) + Talk(2, player); + + /* + * He studies/prepares at the stone for five seconds. + */ + events.ScheduleEvent(EVENT_START_WORKING, 5000); + break; + } + + case POINT_RETURN_CAVE_IN: + me->GetMotionMaster()->MovePoint( + POINT_RETURN_CAVE_OUT, + PhizzlethorpeCaveOutside); + break; + + case POINT_RETURN_CAVE_OUT: + { + /* + * creature_text group 7: + * "Phew! Glad to be back from that creepy cave." + */ + if (Player* player = GetEscortPlayer()) + Talk(7, player); + + me->GetMotionMaster()->MovePoint( + POINT_RETURN_1, + PhizzlethorpePoint1); + break; + } + + case POINT_RETURN_1: + me->GetMotionMaster()->MovePoint( + POINT_RETURN_HOME, + PhizzlethorpeStart); + break; + + case POINT_RETURN_HOME: + { + /* + * creature_text group 8: + * emote about handing the glowing goggles over. + */ + if (Player* player = GetEscortPlayer()) + Talk(8, player); + + /* + * Give the final emote a moment before the last line + * and quest completion. + */ + events.ScheduleEvent(EVENT_FINISH_QUEST, 2000); + break; + } + + default: + break; + } + } + + void JustSummoned(Creature* summon) override + { + summons.Summon(summon); + + if (summon->GetEntry() == NPC_VENGEFUL_SURGE) + { + ++surgeCount; + + /* + * The player is supposed to protect Phizzlethorpe. + */ + summon->AI()->AttackStart(me); + } + } + + void SummonedCreatureDies(Creature* summon, Unit* /*killer*/) override + { + if (!eventActive || + !waitingForAmbush || + summon->GetEntry() != NPC_VENGEFUL_SURGE) + { + return; + } + + ++surgeDeaths; + + if (surgeCount > 0 && surgeDeaths >= surgeCount) + AmbushFinished(); + } + + void JustDied(Unit* /*killer*/) override + { + ResetWorkEmote(); + + if (Player* player = GetEscortPlayer()) + { + if (player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) == QUEST_STATUS_INCOMPLETE) + player->FailQuest(QUEST_GOGGLE_BOGGLE); + } + + events.Reset(); + summons.DespawnAll(); + + eventActive = false; + returning = false; + waitingForAmbush = false; + } + + void UpdateAI(uint32 diff) override + { + events.Update(diff); + + while (uint32 eventId = events.ExecuteEvent()) + { + switch (eventId) + { + case EVENT_START_MOVING: + { + if (!eventActive) + break; + + me->GetMotionMaster()->MovePoint( + POINT_OUTBOUND_1, + PhizzlethorpePoint1); + break; + } + + case EVENT_ENTER_CAVE: + { + if (!eventActive || returning) + break; + + /* + * Newly recorded point immediately behind the cave + * threshold. This forces a sane crossing of the entrance. + */ + me->GetMotionMaster()->MovePoint( + POINT_CAVE_INSIDE, + PhizzlethorpeCaveInside); + break; + } + + case EVENT_START_WORKING: + { + if (!eventActive || returning) + break; + + /* + * creature_text group 3 is the visible emote text: + * "...begins tinkering with the goggles..." + */ + if (Player* player = GetEscortPlayer()) + Talk(3, player); + + /* + * Emote 69 / STATE_USESTANDING. + * Verified ingame for this character/model. + */ + me->SetUInt32Value( + UNIT_NPC_EMOTESTATE, + EMOTE_STATE_USE_STANDING); + + /* + * Let him visibly work for four seconds before + * anything attacks. + */ + events.ScheduleEvent(EVENT_SPAWN_AMBUSH, 4000); + break; + } + + case EVENT_SPAWN_AMBUSH: + { + if (!eventActive || returning) + break; + + waitingForAmbush = true; + surgeDeaths = 0; + surgeCount = 0; + + /* + * creature_text group 4 has two possible help lines. + */ + if (Player* player = GetEscortPlayer()) + Talk(4, player); + + me->SummonCreature( + NPC_VENGEFUL_SURGE, + SurgePosition1, + TEMPSUMMON_CORPSE_TIMED_DESPAWN, + 5000); + + me->SummonCreature( + NPC_VENGEFUL_SURGE, + SurgePosition2, + TEMPSUMMON_CORPSE_TIMED_DESPAWN, + 5000); + + /* + * Mid-event encouragement. + */ + events.ScheduleEvent(EVENT_ALMOST_DONE, 5000); + + /* + * If neither creature could be created, do not leave + * the sole questgiver trapped forever. + */ + if (surgeCount == 0) + AbortEvent(true); + + break; + } + + case EVENT_ALMOST_DONE: + { + if (eventActive && waitingForAmbush) + { + /* + * creature_text group 5: + * "Almost done! Just a little longer!" + */ + if (Player* player = GetEscortPlayer()) + Talk(5, player); + } + + break; + } + + case EVENT_BEGIN_RETURN: + { + if (!eventActive) + break; + + returning = true; + waitingForAmbush = false; + + ResetWorkEmote(); + + /* + * creature_text group 6: + * "I've done it!... Let's get out of here!" + */ + if (Player* player = GetEscortPlayer()) + Talk(6, player); + + /* + * Return trip may be faster than the deliberate + * outbound walk. + */ + me->SetWalk(false); + + me->GetMotionMaster()->MovePoint( + POINT_RETURN_CAVE_IN, + PhizzlethorpeCaveInside); + break; + } + + case EVENT_FINISH_QUEST: + { + if (eventActive) + FinishEvent(); + + break; + } + + case EVENT_CHECK_PLAYER: + { + if (!eventActive) + break; + + Player* player = GetEscortPlayer(); + + /* + * Logout / object no longer available: + * restore professor without artificially failing + * an offline player's quest. + */ + if (!player) + { + AbortEvent(false); + break; + } + + /* + * Player abandoned or otherwise no longer has the + * quest active. + */ + if (player->GetQuestStatus(QUEST_GOGGLE_BOGGLE) != QUEST_STATUS_INCOMPLETE) + { + AbortEvent(false); + break; + } + + /* + * Actual escort failure. + */ + if (!player->IsAlive() || me->GetDistance(player) > 120.0f) + { + AbortEvent(true); + break; + } + + events.ScheduleEvent(EVENT_CHECK_PLAYER, 1000); + break; + } + + case EVENT_TIMEOUT: + { + if (eventActive) + AbortEvent(true); + + break; + } + + default: + break; + } + } + + if (UpdateVictim()) + DoMeleeAttackIfReady(); + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_professor_phizzlethorpeAI(creature); + } +}; + +void AddSC_arathi_highlands() +{ + new npc_professor_phizzlethorpe(); +} diff --git a/src/server/scripts/EasternKingdoms/eastern_kingdoms_script_loader.cpp b/src/server/scripts/EasternKingdoms/eastern_kingdoms_script_loader.cpp index e106480ede..2dd81752f3 100644 --- a/src/server/scripts/EasternKingdoms/eastern_kingdoms_script_loader.cpp +++ b/src/server/scripts/EasternKingdoms/eastern_kingdoms_script_loader.cpp @@ -222,7 +222,7 @@ void AddSC_boss_zanzil(); void AddSC_zulgurub(); void AddSC_instance_zulgurub(); //void AddSC_alterac_mountains(); -//void AddSC_arathi_highlands(); +void AddSC_arathi_highlands(); void AddSC_blasted_lands(); void AddSC_dun_morogh(); void AddSC_dun_morogh_area_coldridge_valley(); @@ -454,7 +454,7 @@ void AddEasternKingdomsScripts() AddSC_zulgurub(); AddSC_instance_zulgurub(); //AddSC_alterac_mountains(); - //AddSC_arathi_highlands(); + AddSC_arathi_highlands(); AddSC_blasted_lands(); AddSC_dun_morogh(); AddSC_dun_morogh_area_coldridge_valley(); diff --git a/src/server/scripts/World/item_scripts.cpp b/src/server/scripts/World/item_scripts.cpp index 294faace10..896de8b8f7 100644 --- a/src/server/scripts/World/item_scripts.cpp +++ b/src/server/scripts/World/item_scripts.cpp @@ -417,11 +417,6 @@ class item_generic_limit_chance_above_60 : public ItemScript # item_horde_adventurer_horn #####*/ -enum HordeAdventurerHorn -{ - NPC_HORDE_ADVENTURER = 900001 -}; - class item_horde_adventurer_horn : public ItemScript { public: @@ -429,35 +424,53 @@ public: bool OnUse(Player* player, Item* item, SpellCastTargets const& /*targets*/) override { - TC_LOG_INFO("scripts", "Horde adventureer horn OnUse triggered by player %s (item %u)", player->GetName().c_str(), item->GetEntry()); - // The horn may not be used during combat. + TC_LOG_INFO( + "scripts", + "[VianoHorn] OnUse player='%s' item=%u", + player->GetName().c_str(), + item->GetEntry()); + + // Horns may not be used during combat. if (player->IsInCombat()) { - player->SendEquipError(EQUIP_ERR_CLIENT_LOCKED_OUT, item, nullptr); + TC_LOG_INFO("scripts", + "[VianoHorn] rejected: player is in combat"); + + player->SendEquipError( + EQUIP_ERR_CLIENT_LOCKED_OUT, + item, + nullptr); + return true; } - // The horn may only be used inside a dungeon instance. + // Horns may only be used inside dungeon instances. if (!player->GetMap()->IsDungeon()) { - player->SendEquipError(EQUIP_ERR_CLIENT_LOCKED_OUT, item, nullptr); + TC_LOG_INFO("scripts", + "[VianoHorn] rejected: player is not inside a dungeon"); + + player->SendEquipError( + EQUIP_ERR_CLIENT_LOCKED_OUT, + item, + nullptr); + return true; } - player->SummonCreature( - NPC_HORDE_ADVENTURER, - player->GetPositionX() + 2.0f, - player->GetPositionY(), - player->GetPositionZ(), - player->GetOrientation(), - TEMPSUMMON_MANUAL_DESPAWN, - 0 - ); + TC_LOG_INFO( + "scripts", + "[VianoHorn] prechecks passed; allowing normal item spell processing"); - return true; + // IMPORTANT: + // false means the ItemScript does not consume/suppress the normal item use. + // The item's SpellID1 (120000-120003) may now run through the regular + // Trinity spell lifecycle. + return false; } -}; -} +}; +}// namespace ItemScripts + void AddSC_item_scripts() {