- Merge fix
This commit is contained in:
@@ -29,6 +29,16 @@ namespace Acore
|
||||
template<typename... Args>
|
||||
using FormatString = fmt::format_string<Args...>;
|
||||
|
||||
using FormatStringView = fmt::string_view;
|
||||
|
||||
using FormatArgs = fmt::format_args;
|
||||
|
||||
template<typename... Args>
|
||||
constexpr auto MakeFormatArgs(Args&&... args)
|
||||
{
|
||||
return fmt::make_format_args(args...);
|
||||
}
|
||||
|
||||
/// Default AC string format function.
|
||||
template<typename... Args>
|
||||
inline std::string StringFormat(FormatString<Args...> fmt, Args&&... args)
|
||||
@@ -43,6 +53,47 @@ namespace Acore
|
||||
}
|
||||
}
|
||||
|
||||
/// Format directly to an output iterator.
|
||||
template<typename OutputIt, typename... Args>
|
||||
inline OutputIt StringFormatTo(OutputIt out, FormatString<Args...> fmt, Args&&... args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return fmt::format_to(out, fmt, std::forward<Args>(args)...);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
return fmt::format_to(out, "Wrong format occurred ({}). Fmt string: '{}'", e.what(), fmt.get());
|
||||
}
|
||||
}
|
||||
|
||||
/// Format with pre-built format args.
|
||||
inline std::string StringVFormat(FormatStringView fmt, FormatArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return fmt::vformat(fmt, args);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
return fmt::format("Wrong format occurred ({}). Fmt string: '{}'", e.what(), fmt);
|
||||
}
|
||||
}
|
||||
|
||||
/// Format with pre-built format args directly to an output iterator.
|
||||
template<typename OutputIt>
|
||||
inline OutputIt StringVFormatTo(OutputIt out, FormatStringView fmt, FormatArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return fmt::vformat_to(out, fmt, args);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
return fmt::format_to(out, "Wrong format occurred ({}). Fmt string: '{}'", e.what(), fmt);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the given char pointer is null.
|
||||
inline bool IsFormatEmptyOrNull(char const* fmt)
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
#include <windows.h>
|
||||
#include <iostream>
|
||||
#else
|
||||
#include "Chat.h"
|
||||
#include "ChatCommand.h"
|
||||
@@ -158,21 +159,48 @@ void CliThread()
|
||||
std::string command;
|
||||
|
||||
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
||||
wchar_t commandbuf[256];
|
||||
DWORD charsRead = 0;
|
||||
|
||||
if (ReadConsoleW(hStdIn, commandbuf, sizeof(commandbuf) / sizeof(wchar_t) - 1, &charsRead, nullptr))
|
||||
static bool checkedConsole = false;
|
||||
static bool isRealConsole = false;
|
||||
|
||||
if (!checkedConsole)
|
||||
{
|
||||
if (charsRead > 0)
|
||||
DWORD mode = 0;
|
||||
isRealConsole = GetConsoleMode(hStdIn, &mode);
|
||||
checkedConsole = true;
|
||||
}
|
||||
|
||||
if (isRealConsole)
|
||||
{
|
||||
// ===== Real Windows Console =====
|
||||
wchar_t commandbuf[256];
|
||||
DWORD charsRead = 0;
|
||||
|
||||
if (ReadConsoleW(hStdIn, commandbuf,
|
||||
sizeof(commandbuf) / sizeof(wchar_t) - 1,
|
||||
&charsRead, nullptr))
|
||||
{
|
||||
commandbuf[charsRead] = L'\0';
|
||||
if (!WStrToUtf8(commandbuf, charsRead, command))
|
||||
if (charsRead > 0)
|
||||
{
|
||||
PrintCliPrefix();
|
||||
continue;
|
||||
commandbuf[charsRead] = L'\0';
|
||||
if (!WStrToUtf8(commandbuf, charsRead, command))
|
||||
{
|
||||
PrintCliPrefix();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ===== Redirected input (pipe) =====
|
||||
if (!std::getline(std::cin, command))
|
||||
{
|
||||
World::StopNow(SHUTDOWN_EXIT_CODE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
char* command_str = readline(CLI_PREFIX);
|
||||
::rl_bind_key('\t', ::rl_complete);
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
# CHARACTER DELETE
|
||||
# CHARACTER CREATION
|
||||
# CHARACTER
|
||||
# ACHIEVEMENT
|
||||
# SKILL
|
||||
# STATS
|
||||
# REPUTATION
|
||||
@@ -2227,6 +2228,22 @@ SpellQueue.Window = 400
|
||||
#
|
||||
###################################################################################################
|
||||
|
||||
###################################################################################################
|
||||
# ACHIEVEMENT
|
||||
#
|
||||
# Achievement.RealmFirstKillWindow
|
||||
# Description: Time window (in seconds) for realm first kill achievements after the first
|
||||
# completion. This setting controls whether multiple groups can earn the
|
||||
# realm first kill achievement within a short time window.
|
||||
# Default: 60 - (Allow additional groups to complete within 60 seconds/1 minute)
|
||||
# 0 - (Strict mode, only the first group/player gets the achievement)
|
||||
# 1+ - (Extended window, allow N seconds for additional completions)
|
||||
|
||||
Achievement.RealmFirstKillWindow = 60
|
||||
|
||||
#
|
||||
###################################################################################################
|
||||
|
||||
###################################################################################################
|
||||
# SKILL
|
||||
#
|
||||
|
||||
@@ -724,7 +724,7 @@ bool PetAI::CanAttack(Unit* target, SpellInfo const* spellInfo)
|
||||
return me->GetCharmInfo()->IsCommandAttack();
|
||||
|
||||
// CC - mobs under crowd control can be attacked if owner commanded
|
||||
if (target->HasBreakableByDamageCrowdControlAura() && (!spellInfo || !spellInfo->HasAttribute(SPELL_ATTR4_REACTIVE_DAMAGE_PROC)))
|
||||
if (target->HasBreakableByDamageCrowdControlAura() && (!spellInfo || !spellInfo->HasAttribute(SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS)))
|
||||
return me->GetCharmInfo()->IsCommandAttack();
|
||||
|
||||
// Returning - pets ignore attacks only if owner clicked follow
|
||||
|
||||
@@ -29,8 +29,6 @@ class Unit;
|
||||
class Creature;
|
||||
class Player;
|
||||
class SpellInfo;
|
||||
enum SpellFinishReason : uint8;
|
||||
|
||||
typedef std::vector<AreaBoundary const*> CreatureBoundary;
|
||||
|
||||
#define TIME_INTERVAL_LOOK 5000
|
||||
@@ -147,8 +145,17 @@ public:
|
||||
// Called when spell hits a target
|
||||
virtual void SpellHitTarget(Unit* /*target*/, SpellInfo const* /*spell*/) {}
|
||||
|
||||
// Called when a spell either finishes, interrupts or cancels a spell cast
|
||||
virtual void OnSpellCastFinished(SpellInfo const* /*spell*/, SpellFinishReason /*reason*/) {}
|
||||
// Called when the creature begins casting a spell (has cast time or is channeled)
|
||||
virtual void OnSpellStart(SpellInfo const* /*spell*/) {}
|
||||
|
||||
// Called when the creature successfully executes a spell cast
|
||||
virtual void OnSpellCast(SpellInfo const* /*spell*/) {}
|
||||
|
||||
// Called when a spell cast is interrupted or cancelled
|
||||
virtual void OnSpellFailed(SpellInfo const* /*spell*/) {}
|
||||
|
||||
// Called when a channeled spell finishes its full duration
|
||||
virtual void OnChannelFinished(SpellInfo const* /*spell*/) {}
|
||||
|
||||
// Called when the creature is target of hostile action: swing, hostile spell landed, fear/etc)
|
||||
virtual void AttackedBy(Unit* /*attacker*/) {}
|
||||
|
||||
@@ -269,6 +269,16 @@ void ScriptedAI::DoStopAttack()
|
||||
me->AttackStop();
|
||||
}
|
||||
|
||||
void ScriptedAI::DoRewardPlayersInArea()
|
||||
{
|
||||
me->GetMap()->DoForAllPlayers([&](Player* player)
|
||||
{
|
||||
if (player->GetFaction() != me->GetCreatureTemplate()->faction && !player->IsGameMaster())
|
||||
if (player->GetAreaId() == me->GetAreaId())
|
||||
player->KilledMonsterCredit(me->GetEntry());
|
||||
});
|
||||
}
|
||||
|
||||
void ScriptedAI::DoCastSpell(Unit* target, SpellInfo const* spellInfo, bool triggered)
|
||||
{
|
||||
if (!target || me->IsNonMeleeSpellCast(false))
|
||||
@@ -756,9 +766,26 @@ void BossAI::UpdateAI(uint32 diff)
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
|
||||
void BossAI::OnSpellCastFinished(SpellInfo const* spellInfo, SpellFinishReason reason)
|
||||
void BossAI::OnSpellCast(SpellInfo const* spellInfo)
|
||||
{
|
||||
ScriptedAI::OnSpellCast(spellInfo);
|
||||
_CheckHealthAfterCast();
|
||||
}
|
||||
|
||||
void BossAI::OnChannelFinished(SpellInfo const* spellInfo)
|
||||
{
|
||||
ScriptedAI::OnChannelFinished(spellInfo);
|
||||
_CheckHealthAfterCast();
|
||||
}
|
||||
|
||||
void BossAI::OnSpellFailed(SpellInfo const* spellInfo)
|
||||
{
|
||||
ScriptedAI::OnSpellFailed(spellInfo);
|
||||
_CheckHealthAfterCast();
|
||||
}
|
||||
|
||||
void BossAI::_CheckHealthAfterCast()
|
||||
{
|
||||
ScriptedAI::OnSpellCastFinished(spellInfo, reason);
|
||||
// Check if any health check events are pending (i.e. waiting for the boss to stop casting.
|
||||
if (_nextHealthCheck.IsPending() && me->IsInCombat())
|
||||
{
|
||||
|
||||
@@ -306,6 +306,9 @@ struct ScriptedAI : public CreatureAI
|
||||
//Stop attack of current victim
|
||||
void DoStopAttack();
|
||||
|
||||
//Reward kill credit to all players from the oposing faction in the area (faction leaders)
|
||||
void DoRewardPlayersInArea();
|
||||
|
||||
//Cast spell by spell info
|
||||
void DoCastSpell(Unit* target, SpellInfo const* spellInfo, bool triggered = false);
|
||||
|
||||
@@ -490,7 +493,9 @@ public:
|
||||
|
||||
bool CanRespawn() override;
|
||||
|
||||
void OnSpellCastFinished(SpellInfo const* spell, SpellFinishReason reason) override;
|
||||
void OnSpellCast(SpellInfo const* spell) override;
|
||||
void OnChannelFinished(SpellInfo const* spell) override;
|
||||
void OnSpellFailed(SpellInfo const* spell) override;
|
||||
void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType damagetype, SpellSchoolMask damageSchoolMask) override;
|
||||
void JustSummoned(Creature* summon) override;
|
||||
void SummonedCreatureDespawn(Creature* summon) override;
|
||||
@@ -534,6 +539,7 @@ protected:
|
||||
SummonList summons;
|
||||
|
||||
private:
|
||||
void _CheckHealthAfterCast();
|
||||
uint32 const _bossId;
|
||||
std::list<HealthCheckEventData> _healthCheckEvents;
|
||||
HealthCheckEventData _nextHealthCheck;
|
||||
|
||||
@@ -1250,10 +1250,10 @@ bool SmartAI::IsMainSpellPrevented(SpellInfo const* spellInfo) const
|
||||
return false;
|
||||
}
|
||||
|
||||
void SmartAI::OnSpellCastFinished(SpellInfo const* spell, SpellFinishReason reason)
|
||||
void SmartAI::OnSpellFailed(SpellInfo const* spell)
|
||||
{
|
||||
CreatureAI::OnSpellCastFinished(spell, reason);
|
||||
if (reason == SPELL_FINISHED_CANCELED && _mainSpellId == spell->Id)
|
||||
CreatureAI::OnSpellFailed(spell);
|
||||
if (_mainSpellId == spell->Id)
|
||||
if (_currentRangeMode && IsMainSpellPrevented(spell))
|
||||
SetCurrentRangeMode(false);
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ public:
|
||||
|
||||
bool IsMainSpellPrevented(SpellInfo const* spellInfo) const;
|
||||
|
||||
void OnSpellCastFinished(SpellInfo const* spell, SpellFinishReason reason) override;
|
||||
void OnSpellFailed(SpellInfo const* spell) override;
|
||||
|
||||
private:
|
||||
bool mIsCharmed;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "DBCEnums.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DisableMgr.h"
|
||||
#include "Duration.h"
|
||||
#include "GameEventMgr.h"
|
||||
#include "GameTime.h"
|
||||
#include "GridNotifiersImpl.h"
|
||||
@@ -2592,11 +2593,18 @@ bool AchievementGlobalMgr::IsRealmCompleted(AchievementEntry const* achievement)
|
||||
if (itr->second == SystemTimePoint::max())
|
||||
return true;
|
||||
|
||||
// Allow completing the realm first kill for entire minute after first person did it
|
||||
// Allow completing the realm first kill for configurable time window after first person did it
|
||||
// it may allow more than one group to achieve it (highly unlikely)
|
||||
// but apparently this is how blizz handles it as well
|
||||
if (achievement->flags & ACHIEVEMENT_FLAG_REALM_FIRST_KILL)
|
||||
return (GameTime::GetSystemTime() - itr->second) > 1min;
|
||||
{
|
||||
Seconds windowSeconds = Seconds(sWorld->getIntConfig(CONFIG_ACHIEVEMENT_REALM_FIRST_KILL_WINDOW));
|
||||
|
||||
if (windowSeconds == 0s)
|
||||
return true;
|
||||
|
||||
return (GameTime::GetSystemTime() - itr->second) > windowSeconds;
|
||||
}
|
||||
|
||||
sScriptMgr->SetRealmCompleted(achievement);
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ void Battlefield::PlayerAcceptInviteToWar(Player* player)
|
||||
{
|
||||
player->GetSession()->SendBfEntered(m_BattleId);
|
||||
m_PlayersInWar[player->GetTeamId()].insert(player->GetGUID());
|
||||
m_InvitedPlayers[player->GetTeamId()].erase(player->GetGUID());
|
||||
m_InvitedPlayers[player->GetTeamId(true)].erase(player->GetGUID());
|
||||
|
||||
if (player->isAFK())
|
||||
player->ToggleAFK();
|
||||
|
||||
@@ -361,6 +361,14 @@ public:
|
||||
// Returns the maximum players allowed per team
|
||||
uint32 GetMaxPlayersPerTeam() const { return m_MaxPlayer; }
|
||||
|
||||
/// Returns the set of players waiting in the pre-battle queue (per team, read-only).
|
||||
GuidUnorderedSet const& GetPlayersQueueSet(TeamId teamId) const { return m_PlayersInQueue[teamId]; }
|
||||
/// Returns the map of players invited to join the active war, value is invite expiry
|
||||
/// timestamp (per team, read-only).
|
||||
PlayerTimerMap const& GetInvitedPlayersMap(TeamId teamId) const { return m_InvitedPlayers[teamId]; }
|
||||
/// Returns the set of players actively fighting in the war (per team, read-only).
|
||||
GuidUnorderedSet const& GetPlayersInWarSet(TeamId teamId) const { return m_PlayersInWar[teamId]; }
|
||||
|
||||
void DoPlaySoundToAll(uint32 SoundID);
|
||||
|
||||
void InvitePlayerToQueue(Player* player);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "CreatureAI.h"
|
||||
#include "CreatureAISelector.h"
|
||||
#include "CreatureGroups.h"
|
||||
#include "MoveSpline.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Formulas.h"
|
||||
#include "GameEventMgr.h"
|
||||
@@ -1093,8 +1094,7 @@ bool Creature::AIM_Initialize(CreatureAI* ai)
|
||||
|
||||
UnitAI* oldAI = i_AI;
|
||||
|
||||
// Xinef: called in add to world
|
||||
//Motion_Initialize();
|
||||
Motion_Initialize();
|
||||
|
||||
i_AI = ai ? ai : FactorySelector::SelectAI(this);
|
||||
delete oldAI;
|
||||
@@ -1117,7 +1117,19 @@ void Creature::Motion_Initialize()
|
||||
GetMotionMaster()->Initialize();
|
||||
}
|
||||
else if (m_formation->IsFormed())
|
||||
{
|
||||
// If the leader is already moving, start following immediately
|
||||
// instead of waiting for the next waypoint signal.
|
||||
if (Creature* leader = m_formation->GetLeader())
|
||||
{
|
||||
if (leader->IsAlive() && !leader->movespline->Finalized())
|
||||
{
|
||||
m_formation->LeaderStartedMoving();
|
||||
return;
|
||||
}
|
||||
}
|
||||
GetMotionMaster()->MoveIdle(); //wait the order of leader
|
||||
}
|
||||
else
|
||||
GetMotionMaster()->Initialize();
|
||||
}
|
||||
|
||||
@@ -303,6 +303,11 @@ struct CreatureBaseStats
|
||||
uint32 AttackPower;
|
||||
uint32 RangedAttackPower;
|
||||
float BaseDamage[MAX_EXPANSIONS];
|
||||
uint32 Strength;
|
||||
uint32 Agility;
|
||||
uint32 Stamina;
|
||||
uint32 Intellect;
|
||||
uint32 Spirit;
|
||||
|
||||
// Helpers
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ void CreatureGroup::RemoveMember(Creature* member)
|
||||
{
|
||||
if (m_leader == member)
|
||||
{
|
||||
RemoveFormationMovement();
|
||||
m_leader = nullptr;
|
||||
}
|
||||
|
||||
@@ -333,7 +334,7 @@ void CreatureGroup::FormationReset(bool dismiss, bool initMotionMaster)
|
||||
if (initMotionMaster)
|
||||
{
|
||||
if (dismiss)
|
||||
member->GetMotionMaster()->Initialize();
|
||||
member->GetMotionMaster()->MovementExpiredOnSlot(MOTION_SLOT_IDLE, false);
|
||||
else
|
||||
member->GetMotionMaster()->MoveIdle();
|
||||
|
||||
@@ -344,16 +345,11 @@ void CreatureGroup::FormationReset(bool dismiss, bool initMotionMaster)
|
||||
m_Formed = !dismiss;
|
||||
}
|
||||
|
||||
void CreatureGroup::LeaderMoveTo(float x, float y, float z, uint32 move_type)
|
||||
void CreatureGroup::LeaderStartedMoving()
|
||||
{
|
||||
//! To do: This should probably get its own movement generator or use WaypointMovementGenerator.
|
||||
//! If the leader's path is known, member's path can be plotted as well using formation offsets.
|
||||
if (!m_leader)
|
||||
return;
|
||||
|
||||
float pathDist = m_leader->GetExactDist(x, y, z);
|
||||
float pathAngle = std::atan2(m_leader->GetPositionY() - y, m_leader->GetPositionX() - x);
|
||||
|
||||
for (auto const& itr : m_members)
|
||||
{
|
||||
Creature* member = itr.first;
|
||||
@@ -361,59 +357,39 @@ void CreatureGroup::LeaderMoveTo(float x, float y, float z, uint32 move_type)
|
||||
if (member == m_leader || !member->IsAlive() || member->GetVictim() || !pFormationInfo.HasGroupFlag(std::underlying_type_t<GroupAIFlags>(GroupAIFlags::GROUP_AI_FLAG_FOLLOW_LEADER)))
|
||||
continue;
|
||||
|
||||
// If member is stunned / rooted etc don't allow to move him
|
||||
// Or if charmed/controlled
|
||||
if (member->HasUnitState(UNIT_STATE_NOT_MOVE) || member->isPossessed() || member->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED))
|
||||
continue;
|
||||
|
||||
// Xinef: this should be automatized, if turn angle is greater than PI/2 (90�) we should swap formation angle
|
||||
float followAngle = pFormationInfo.follow_angle;
|
||||
if (static_cast<float>(M_PI) - std::fabs(std::fabs(m_leader->GetOrientation() - pathAngle) - static_cast<float>(M_PI)) > static_cast<float>(M_PI)* 0.5f)
|
||||
{
|
||||
// pussywizard: in both cases should be 2*M_PI - follow_angle
|
||||
// pussywizard: also, GetCurrentWaypointID() returns 0..n-1, while point_1 must be > 0, so +1
|
||||
// pussywizard: db table waypoint_data shouldn't have point id 0 and shouldn't have any gaps for this to work!
|
||||
// if (m_leader->GetCurrentWaypointID()+1 == pFormationInfo->point_1 || m_leader->GetCurrentWaypointID()+1 == itr->second->point_2)
|
||||
followAngle = Position::NormalizeOrientation(pFormationInfo.follow_angle + static_cast<float>(M_PI)); //(2 * M_PI) - itr->second->follow_angle;
|
||||
}
|
||||
|
||||
float const followAngle = pFormationInfo.follow_angle;
|
||||
float const followDist = pFormationInfo.follow_dist;
|
||||
|
||||
float dx = x + std::cos(followAngle + pathAngle) * followDist;
|
||||
float dy = y + std::sin(followAngle + pathAngle) * followDist;
|
||||
float dz = z;
|
||||
if (!member->HasUnitState(UNIT_STATE_FOLLOW_MOVE))
|
||||
member->GetMotionMaster()->MoveFormation(m_leader, followDist, followAngle, pFormationInfo.point_1, pFormationInfo.point_2);
|
||||
}
|
||||
}
|
||||
|
||||
Acore::NormalizeMapCoord(dx);
|
||||
Acore::NormalizeMapCoord(dy);
|
||||
if (move_type < 2)
|
||||
member->UpdateGroundPositionZ(dx, dy, dz);
|
||||
bool CreatureGroup::CanLeaderStartMoving() const
|
||||
{
|
||||
for (auto const& itr : m_members)
|
||||
{
|
||||
if (itr.first && itr.first != m_leader && itr.first->IsAlive())
|
||||
if (itr.first->IsEngaged() || itr.first->IsInEvadeMode())
|
||||
return false;
|
||||
}
|
||||
|
||||
// pussywizard: setting the same movementflags is not enough, spline decides whether leader walks/runs, so spline param is now passed as "run" parameter to this function
|
||||
member->SetUnitMovementFlags(m_leader->GetUnitMovementFlags());
|
||||
switch (move_type)
|
||||
{
|
||||
case WAYPOINT_MOVE_TYPE_WALK:
|
||||
member->AddUnitMovementFlag(MOVEMENTFLAG_WALKING);
|
||||
break;
|
||||
case WAYPOINT_MOVE_TYPE_RUN:
|
||||
member->RemoveUnitMovementFlag(MOVEMENTFLAG_WALKING);
|
||||
break;
|
||||
case WAYPOINT_MOVE_TYPE_LAND:
|
||||
member->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// xinef: if we move members to position without taking care of sizes, we should compare distance without sizes
|
||||
// xinef: change members speed basing on distance - if too far speed up, if too close slow down
|
||||
UnitMoveType const mtype = Movement::SelectSpeedType(member->GetUnitMovementFlags());
|
||||
float const speedRate = m_leader->GetSpeedRate(mtype) * member->GetExactDist(dx, dy, dz) / pathDist;
|
||||
void CreatureGroup::RemoveFormationMovement()
|
||||
{
|
||||
for (auto const& itr : m_members)
|
||||
{
|
||||
Creature* member = itr.first;
|
||||
if (!member || member == m_leader)
|
||||
continue;
|
||||
|
||||
if (speedRate > 0.01f) // don't move if speed rate is too low
|
||||
{
|
||||
member->SetSpeedRate(mtype, speedRate);
|
||||
member->GetMotionMaster()->MovePoint(0, dx, dy, dz);
|
||||
member->SetHomePosition(dx, dy, dz, pathAngle);
|
||||
}
|
||||
if (member->GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_IDLE) == FORMATION_MOTION_TYPE)
|
||||
member->GetMotionMaster()->MovementExpiredOnSlot(MOTION_SLOT_IDLE, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,9 @@ public:
|
||||
void RemoveMember(Creature* member);
|
||||
void FormationReset(bool dismiss, bool initMotionMaster);
|
||||
|
||||
void LeaderMoveTo(float x, float y, float z, uint32 move_type);
|
||||
void LeaderStartedMoving();
|
||||
[[nodiscard]] bool CanLeaderStartMoving() const;
|
||||
void RemoveFormationMovement();
|
||||
void MemberEngagingTarget(Creature* member, Unit* target);
|
||||
Unit* GetNewTargetForMember(Creature* member);
|
||||
void MemberEvaded(Creature* member);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
#include "Unit.h"
|
||||
#include "AbstractFollower.h"
|
||||
#include "AreaDefines.h"
|
||||
#include "ArenaSpectator.h"
|
||||
#include "Battlefield.h"
|
||||
@@ -750,8 +751,7 @@ void Unit::DisableSpline()
|
||||
|
||||
void Unit::resetAttackTimer(WeaponAttackType type)
|
||||
{
|
||||
int32 time = int32(GetAttackTime(type) * m_modAttackSpeedPct[type]);
|
||||
m_attackTimer[type] = std::min(m_attackTimer[type] + time, time);
|
||||
m_attackTimer[type] = int32(GetAttackTime(type) * m_modAttackSpeedPct[type]);
|
||||
}
|
||||
|
||||
bool Unit::IsWithinCombatRange(Unit const* obj, float dist2compare) const
|
||||
@@ -978,7 +978,7 @@ uint32 Unit::DealDamage(Unit* attacker, Unit* victim, uint32 damage, CleanDamage
|
||||
if (attacker && damagetype != DOT && spellProto->DmgClass == SPELL_DAMAGE_CLASS_MELEE && !(spellProto->GetSchoolMask() & SPELL_SCHOOL_MASK_HOLY))
|
||||
attacker->DealDamageShieldDamage(victim);
|
||||
|
||||
if (!spellProto->HasAttribute(SPELL_ATTR4_REACTIVE_DAMAGE_PROC))
|
||||
if (!spellProto->HasAttribute(SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS))
|
||||
victim->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TAKE_DAMAGE, spellProto->Id);
|
||||
}
|
||||
else
|
||||
@@ -2234,7 +2234,7 @@ uint32 Unit::CalcArmorReducedDamage(Unit const* attacker, Unit const* victim, co
|
||||
return uint32(std::ceil(std::max(damage * (1.0f - tmpvalue), 0.0f)));
|
||||
}
|
||||
|
||||
float Unit::GetEffectiveResistChance(Unit const* owner, SpellSchoolMask schoolMask, Unit const* victim)
|
||||
float Unit::GetEffectiveResistChance(Unit const* owner, SpellSchoolMask schoolMask, Unit const* victim, SpellInfo const* spellInfo /*= nullptr*/)
|
||||
{
|
||||
float victimResistance = static_cast<float>(victim->GetResistance(schoolMask));
|
||||
if (owner)
|
||||
@@ -2252,7 +2252,8 @@ float Unit::GetEffectiveResistChance(Unit const* owner, SpellSchoolMask schoolMa
|
||||
}
|
||||
|
||||
victimResistance = std::max(victimResistance, 0.0f);
|
||||
if (owner)
|
||||
|
||||
if (owner && (!spellInfo || !spellInfo->HasAttribute(SPELL_ATTR0_CU_BINARY_SPELL)))
|
||||
victimResistance += std::max(static_cast<float>(victim->GetLevel() - owner->GetLevel()) * 5.0f, 0.0f);
|
||||
|
||||
float level = static_cast<float>(victim->GetLevel());
|
||||
@@ -3555,9 +3556,8 @@ SpellMissInfo Unit::MagicSpellHitResult(Unit* victim, SpellInfo const* spellInfo
|
||||
tmp += victim->GetMaxNegativeAuraModifierByMiscValue(SPELL_AURA_MOD_DEBUFF_RESISTANCE, int32(spellInfo->Dispel)) * 100;
|
||||
}
|
||||
|
||||
// Players resistance for binary spells
|
||||
if (spellInfo->HasAttribute(SPELL_ATTR0_CU_BINARY_SPELL) && (spellInfo->GetSchoolMask() & (SPELL_SCHOOL_MASK_NORMAL | SPELL_SCHOOL_MASK_HOLY)) == 0)
|
||||
tmp += int32(Unit::GetEffectiveResistChance(this, spellInfo->GetSchoolMask(), victim) * 10000.0f); // 100 for spell calculations, and 100 for return value percentage
|
||||
tmp += int32(Unit::GetEffectiveResistChance(this, spellInfo->GetSchoolMask(), victim, spellInfo) * 10000.0f);
|
||||
}
|
||||
|
||||
// Roll chance
|
||||
@@ -4210,7 +4210,7 @@ void Unit::InterruptSpell(CurrentSpellTypes spellType, bool withDelayed, bool wi
|
||||
}
|
||||
|
||||
if (IsCreature() && IsAIEnabled)
|
||||
ToCreature()->AI()->OnSpellCastFinished(spell->GetSpellInfo(), SPELL_FINISHED_CANCELED);
|
||||
ToCreature()->AI()->OnSpellFailed(spell->GetSpellInfo());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5528,6 +5528,12 @@ void Unit::RemoveAreaAurasDueToLeaveWorld()
|
||||
}
|
||||
}
|
||||
|
||||
void Unit::RemoveAllFollowers()
|
||||
{
|
||||
while (!m_followingMe.empty())
|
||||
(*m_followingMe.begin())->SetTarget(nullptr);
|
||||
}
|
||||
|
||||
void Unit::RemoveAllAuras()
|
||||
{
|
||||
// this may be a dead loop if some events on aura remove will continiously apply aura on remove
|
||||
@@ -8973,7 +8979,22 @@ float Unit::SpellDoneCritChance(Unit const* /*victim*/, SpellInfo const* spellPr
|
||||
crit_chance = 0.0f;
|
||||
// For other schools
|
||||
else if (IsPlayer())
|
||||
{
|
||||
crit_chance = GetFloatValue(static_cast<uint16>(PLAYER_SPELL_CRIT_PERCENTAGE1) + GetFirstSchoolInMask(schoolMask));
|
||||
|
||||
// register aura mod, this is needed for Arcane Potency
|
||||
if (Spell* spell = ToPlayer()->m_spellModTakingSpell)
|
||||
{
|
||||
Unit::AuraEffectList const& critAuras = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_CRIT_CHANCE);
|
||||
for (AuraEffect const* aurEff : critAuras)
|
||||
spell->m_appliedMods.insert(aurEff->GetBase());
|
||||
|
||||
Unit::AuraEffectList const& critSchoolAuras = GetAuraEffectsByType(SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL);
|
||||
for (AuraEffect const* aurEff : critSchoolAuras)
|
||||
if (aurEff->GetMiscValue() & schoolMask)
|
||||
spell->m_appliedMods.insert(aurEff->GetBase());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
crit_chance = (float)m_baseSpellCritChance;
|
||||
@@ -12706,6 +12727,8 @@ void Unit::RemoveFromWorld()
|
||||
|
||||
RemoveAreaAurasDueToLeaveWorld();
|
||||
|
||||
RemoveAllFollowers();
|
||||
|
||||
if (GetCharmerGUID())
|
||||
{
|
||||
LOG_FATAL("entities.unit", "Unit {} has charmer guid when removed from world", GetEntry());
|
||||
@@ -14106,7 +14129,7 @@ bool Unit::HandleAuraRaidProcFromCharge(AuraEffect* triggeredByAura)
|
||||
return true;
|
||||
}
|
||||
|
||||
void Unit::Kill(Unit* killer, Unit* victim, bool durabilityLoss, WeaponAttackType attackType, SpellInfo const* spellProto, Spell const* spell /*= nullptr*/)
|
||||
void Unit::Kill(Unit* killer, Unit* victim, bool durabilityLoss, WeaponAttackType attackType, SpellInfo const* /*spellProto*/, Spell const* /*spell*/ /*= nullptr*/)
|
||||
{
|
||||
// Prevent killing unit twice (and giving reward from kill twice)
|
||||
if (!victim->GetHealth())
|
||||
@@ -14220,20 +14243,21 @@ void Unit::Kill(Unit* killer, Unit* victim, bool durabilityLoss, WeaponAttackTyp
|
||||
}
|
||||
|
||||
// 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
|
||||
// Spell context is not passed to avoid the killing spell's triggered status from suppressing nested proc events
|
||||
if (killer && (killer->IsPet() || killer->IsTotem()))
|
||||
if (Unit* owner = killer->GetOwner())
|
||||
{
|
||||
Unit::ProcSkillsAndAuras(owner, victim, PROC_FLAG_KILL, PROC_FLAG_NONE, PROC_EX_NONE, 0, attackType, spellProto, nullptr, -1, spell);
|
||||
Unit::ProcSkillsAndAuras(owner, victim, PROC_FLAG_KILL, PROC_FLAG_NONE, PROC_EX_NONE, 0, attackType, nullptr, nullptr, -1, nullptr);
|
||||
sScriptMgr->OnPlayerCreatureKilledByPet( killer->GetCharmerOrOwnerPlayerOrPlayerItself(), victim->ToCreature());
|
||||
}
|
||||
|
||||
if (killer != victim)
|
||||
{
|
||||
Unit::ProcSkillsAndAuras(killer, victim, killer ? PROC_FLAG_KILL : 0, PROC_FLAG_KILLED, PROC_EX_NONE, 0, attackType, spellProto, nullptr, -1, spell);
|
||||
Unit::ProcSkillsAndAuras(killer, victim, killer ? PROC_FLAG_KILL : 0, PROC_FLAG_KILLED, PROC_EX_NONE, 0, attackType, nullptr, nullptr, -1, nullptr);
|
||||
}
|
||||
|
||||
// Proc auras on death - must be before aura/combat remove
|
||||
Unit::ProcSkillsAndAuras(victim, nullptr, PROC_FLAG_DEATH, PROC_FLAG_NONE, PROC_EX_NONE, 0, attackType, spellProto, nullptr, -1, spell);
|
||||
Unit::ProcSkillsAndAuras(victim, nullptr, PROC_FLAG_DEATH, PROC_FLAG_NONE, PROC_EX_NONE, 0, attackType, nullptr, nullptr, -1, 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
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
|
||||
#include "EnumFlag.h"
|
||||
#include "EventProcessor.h"
|
||||
#include "FollowerRefMgr.h"
|
||||
#include "FollowerReference.h"
|
||||
#include "HostileRefMgr.h"
|
||||
#include "ItemTemplate.h"
|
||||
#include "MotionMaster.h"
|
||||
@@ -74,6 +72,7 @@ class MotionTransport;
|
||||
class Vehicle;
|
||||
class TransportBase;
|
||||
class SpellCastTargets;
|
||||
class AbstractFollower;
|
||||
|
||||
typedef std::list<Unit*> UnitList;
|
||||
typedef std::list< std::pair<Aura*, uint8> > DispelChargesList;
|
||||
@@ -1191,7 +1190,7 @@ public:
|
||||
int32 GetMechanicResistChance(SpellInfo const* spell);
|
||||
[[nodiscard]] uint32 GetResistance(SpellSchoolMask mask) const;
|
||||
[[nodiscard]] uint32 GetResistance(SpellSchools school) const { return GetUInt32Value(static_cast<uint16>(UNIT_FIELD_RESISTANCES) + school); }
|
||||
static float GetEffectiveResistChance(Unit const* owner, SpellSchoolMask schoolMask, Unit const* victim);
|
||||
static float GetEffectiveResistChance(Unit const* owner, SpellSchoolMask schoolMask, Unit const* victim, SpellInfo const* spellInfo = nullptr);
|
||||
|
||||
void SetResistance(SpellSchools school, int32 val) { SetStatInt32Value(static_cast<uint16>(UNIT_FIELD_RESISTANCES) + school, val); }
|
||||
void UpdateResistanceBuffModsMod(SpellSchools school);
|
||||
@@ -1882,9 +1881,10 @@ public:
|
||||
[[nodiscard]] bool IsInDisallowedMountForm() const;
|
||||
|
||||
// Followers
|
||||
void addFollower(FollowerReference* pRef) { m_FollowingRefMgr.insertFirst(pRef); }
|
||||
void removeFollower(FollowerReference* /*pRef*/) { /* nothing to do yet */ }
|
||||
void FollowerAdded(AbstractFollower* f) { m_followingMe.insert(f); }
|
||||
void FollowerRemoved(AbstractFollower* f) { m_followingMe.erase(f); }
|
||||
[[nodiscard]] virtual float GetFollowAngle() const { return static_cast<float>(M_PI / 2); }
|
||||
void RemoveAllFollowers();
|
||||
|
||||
// Pets, guardians, minions...
|
||||
[[nodiscard]] Guardian* GetGuardianPet() const;
|
||||
@@ -2230,7 +2230,7 @@ private:
|
||||
// Manage all Units that are threatened by us
|
||||
HostileRefMgr m_HostileRefMgr;
|
||||
|
||||
FollowerRefMgr m_FollowingRefMgr;
|
||||
std::unordered_set<AbstractFollower*> m_followingMe;
|
||||
|
||||
Unit* m_comboTarget;
|
||||
int8 m_comboPoints;
|
||||
|
||||
@@ -1961,21 +1961,23 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event)
|
||||
bool singleDate = ((holiday->Date[0] >> 24) & 0x1F) == 31; // Events with fixed date within year have - 1
|
||||
|
||||
time_t curTime = GameTime::GetGameTime().count();
|
||||
for (uint8 i = 0; i < MAX_HOLIDAY_DATES && holiday->Date[i]; ++i)
|
||||
|
||||
if (!singleDate)
|
||||
{
|
||||
time_t start = HolidayDateCalculator::FindStartTimeForStage(
|
||||
holiday->Date, MAX_HOLIDAY_DATES, stageOffset, event.Length, curTime);
|
||||
if (start)
|
||||
event.Start = start;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8 i = 0; i < MAX_HOLIDAY_DATES && holiday->Date[i]; ++i)
|
||||
{
|
||||
uint32 date = holiday->Date[i];
|
||||
|
||||
tm timeInfo;
|
||||
if (singleDate)
|
||||
{
|
||||
timeInfo = Acore::Time::TimeBreakdown(curTime);
|
||||
timeInfo.tm_year -= 1; // First try last year (event active through New Year)
|
||||
}
|
||||
else
|
||||
{
|
||||
timeInfo.tm_year = ((date >> 24) & 0x1F) + 100;
|
||||
}
|
||||
tm timeInfo = Acore::Time::TimeBreakdown(curTime);
|
||||
timeInfo.tm_year -= 1; // First try last year (event active through New Year)
|
||||
|
||||
timeInfo.tm_mon = (date >> 20) & 0xF;
|
||||
timeInfo.tm_mday = ((date >> 14) & 0x3F) + 1;
|
||||
@@ -1986,12 +1988,12 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event)
|
||||
|
||||
// try to get next start time (skip past dates)
|
||||
time_t startTime = mktime(&timeInfo);
|
||||
if (curTime < startTime + event.Length * MINUTE)
|
||||
if (curTime < startTime + stageOffset + event.Length * MINUTE)
|
||||
{
|
||||
event.Start = startTime + stageOffset;
|
||||
break;
|
||||
}
|
||||
else if (singleDate)
|
||||
else
|
||||
{
|
||||
tm tmCopy = Acore::Time::TimeBreakdown(curTime);
|
||||
int year = tmCopy.tm_year; // This year
|
||||
@@ -2000,11 +2002,6 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event)
|
||||
event.Start = mktime(&tmCopy) + stageOffset;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// date is due and not a singleDate event, try with next DBC date (dynamically calculated or overridden by game_event.start_time)
|
||||
// if none is found we don't modify start date and use the one in game_event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -580,3 +580,27 @@ std::vector<uint32_t> HolidayDateCalculator::GetDarkmoonFaireDates(int locationO
|
||||
|
||||
return dates;
|
||||
}
|
||||
|
||||
time_t HolidayDateCalculator::FindStartTimeForStage(const uint32_t* packedDates, uint8_t numDates,
|
||||
time_t stageOffset, uint32_t stageLengthMinutes, time_t curTime)
|
||||
{
|
||||
for (uint8_t i = 0; i < numDates && packedDates[i]; ++i)
|
||||
{
|
||||
uint32_t date = packedDates[i];
|
||||
|
||||
std::tm timeInfo = {};
|
||||
timeInfo.tm_year = static_cast<int>(((date >> 24) & 0x1F)) + 100;
|
||||
timeInfo.tm_mon = static_cast<int>((date >> 20) & 0xF);
|
||||
timeInfo.tm_mday = static_cast<int>(((date >> 14) & 0x3F)) + 1;
|
||||
timeInfo.tm_hour = static_cast<int>((date >> 6) & 0x1F);
|
||||
timeInfo.tm_min = static_cast<int>(date & 0x3F);
|
||||
timeInfo.tm_sec = 0;
|
||||
timeInfo.tm_isdst = -1;
|
||||
|
||||
time_t startTime = mktime(&timeInfo);
|
||||
if (curTime < startTime + stageOffset + static_cast<time_t>(stageLengthMinutes) * 60)
|
||||
return startTime + stageOffset;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -100,6 +100,13 @@ public:
|
||||
// Returns packed dates for all occurrences in the year range
|
||||
static std::vector<uint32_t> GetDarkmoonFaireDates(int locationOffset, int startYear, int numYears, int dayOffset = 0);
|
||||
|
||||
// Find the correct stage start time from a list of packed holiday dates.
|
||||
// For multi-stage holidays, stageOffset is the cumulative duration (in seconds) of all prior stages.
|
||||
// stageLengthMinutes is the duration of the current stage in minutes.
|
||||
// Returns the computed stage start time (startTime + stageOffset), or 0 if no valid date found.
|
||||
static time_t FindStartTimeForStage(const uint32_t* packedDates, uint8_t numDates,
|
||||
time_t stageOffset, uint32_t stageLengthMinutes, time_t curTime);
|
||||
|
||||
private:
|
||||
// Julian Date conversions for lunar calculations
|
||||
static double DateToJulianDay(int year, int month, double day);
|
||||
|
||||
@@ -10431,6 +10431,11 @@ CreatureBaseStats const* ObjectMgr::GetCreatureBaseStats(uint8 level, uint8 unit
|
||||
BaseMana = 0;
|
||||
AttackPower = 0;
|
||||
RangedAttackPower = 0;
|
||||
Strength = 0;
|
||||
Agility = 0;
|
||||
Stamina = 0;
|
||||
Intellect = 0;
|
||||
Spirit = 0;
|
||||
}
|
||||
};
|
||||
static const DefaultCreatureBaseStats defStats;
|
||||
@@ -10441,7 +10446,7 @@ void ObjectMgr::LoadCreatureClassLevelStats()
|
||||
{
|
||||
uint32 oldMSTime = getMSTime();
|
||||
|
||||
QueryResult result = WorldDatabase.Query("SELECT level, class, basehp0, basehp1, basehp2, basemana, basearmor, attackpower, rangedattackpower, damage_base, damage_exp1, damage_exp2 FROM creature_classlevelstats");
|
||||
QueryResult result = WorldDatabase.Query("SELECT level, class, basehp0, basehp1, basehp2, basemana, basearmor, attackpower, rangedattackpower, damage_base, damage_exp1, damage_exp2, Strength, Agility, Stamina, Intellect, Spirit FROM creature_classlevelstats");
|
||||
|
||||
if (!result)
|
||||
{
|
||||
@@ -10501,6 +10506,18 @@ void ObjectMgr::LoadCreatureClassLevelStats()
|
||||
stats.AttackPower = fields[7].Get<uint32>();
|
||||
stats.RangedAttackPower = fields[8].Get<uint32>();
|
||||
|
||||
stats.Strength = fields[12].Get<uint32>();
|
||||
stats.Agility = fields[13].Get<uint32>();
|
||||
stats.Stamina = fields[14].Get<uint32>();
|
||||
stats.Intellect = fields[15].Get<uint32>();
|
||||
stats.Spirit = fields[16].Get<uint32>();
|
||||
|
||||
if (!stats.Strength || !stats.Agility || !stats.Stamina || !stats.Intellect || !stats.Spirit)
|
||||
{
|
||||
// Once these attributes are implemented, this should probably be uncommented.
|
||||
// LOG_WARN("server.loading", "Creature base attributes for class {}, level {} are missing!", Class, Level);
|
||||
}
|
||||
|
||||
_creatureBaseStatsStore[MAKE_PAIR16(Level, Class)] = stats;
|
||||
|
||||
++count;
|
||||
|
||||
@@ -96,6 +96,12 @@ void WorldSession::HandleGossipSelectOptionOpcode(WorldPacket& recv_data)
|
||||
|
||||
recv_data >> guid >> menuId >> gossipListId;
|
||||
|
||||
if (!_player->PlayerTalkClass->GetGossipMenu().GetItem(gossipListId))
|
||||
{
|
||||
recv_data.rfinish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_player->PlayerTalkClass->IsGossipOptionCoded(gossipListId))
|
||||
recv_data >> code;
|
||||
|
||||
|
||||
@@ -530,14 +530,16 @@ void WorldSession::HandleCastSpellOpcode(WorldPacket& recvPacket)
|
||||
}
|
||||
|
||||
// auto-selection buff level base at target level (in spellInfo)
|
||||
if (targets.GetUnitTarget())
|
||||
{
|
||||
SpellInfo const* actualSpellInfo = spellInfo->GetAuraRankForLevel(targets.GetUnitTarget()->GetLevel());
|
||||
if (spellInfo->IsPositive())
|
||||
if (Unit* target = targets.GetUnitTarget())
|
||||
if (mover->IsFriendlyTo(target))
|
||||
{
|
||||
SpellInfo const* actualSpellInfo = spellInfo->GetAuraRankForLevel(target->GetLevel());
|
||||
|
||||
// if rank not found then function return nullptr but in explicit cast case original spell can be casted and later failed with appropriate error message
|
||||
if (actualSpellInfo)
|
||||
spellInfo = actualSpellInfo;
|
||||
}
|
||||
// if rank not found then function return nullptr but in explicit cast case original spell can be casted and later failed with appropriate error message
|
||||
if (actualSpellInfo)
|
||||
spellInfo = actualSpellInfo;
|
||||
}
|
||||
|
||||
Spell* spell = new Spell(mover, spellInfo, triggerFlag, ObjectGuid::Empty, false);
|
||||
|
||||
|
||||
@@ -1194,7 +1194,15 @@ enum AcoreStrings
|
||||
|
||||
LANG_RESETALL_HONOR = 5118,
|
||||
LANG_RESETALL_ARENA = 5119,
|
||||
// Room for more strings 5120-9999
|
||||
|
||||
// Battlefield queue display (.bf queue)
|
||||
LANG_BF_QUEUE_HDR_WAR = 5120,
|
||||
LANG_BF_QUEUE_HDR_WAIT = 5121,
|
||||
LANG_BF_QUEUE_TEAM_HDR = 5122,
|
||||
LANG_BF_QUEUE_PLAYER_QUEUE = 5123,
|
||||
LANG_BF_QUEUE_PLAYER_INVITED = 5124,
|
||||
LANG_BF_QUEUE_PLAYER_WAR = 5125,
|
||||
// Room for more strings 5126-9999
|
||||
|
||||
// Level requirement notifications
|
||||
LANG_SAY_REQ = 6604,
|
||||
|
||||
+13
-10
@@ -15,15 +15,18 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _FOLLOWERREFMANAGER
|
||||
#define _FOLLOWERREFMANAGER
|
||||
#include "AbstractFollower.h"
|
||||
#include "Unit.h"
|
||||
|
||||
#include "RefMgr.h"
|
||||
|
||||
class Unit;
|
||||
class TargetedMovementGeneratorBase;
|
||||
|
||||
class FollowerRefMgr : public RefMgr<Unit, TargetedMovementGeneratorBase>
|
||||
void AbstractFollower::SetTarget(Unit* unit)
|
||||
{
|
||||
};
|
||||
#endif
|
||||
if (unit == _target)
|
||||
return;
|
||||
|
||||
if (_target)
|
||||
_target->FollowerRemoved(this);
|
||||
|
||||
_target = unit;
|
||||
if (_target)
|
||||
_target->FollowerAdded(this);
|
||||
}
|
||||
+13
-10
@@ -15,19 +15,22 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _FOLLOWERREFERENCE_H
|
||||
#define _FOLLOWERREFERENCE_H
|
||||
#ifndef ACORE_ABSTRACTFOLLOWER_H
|
||||
#define ACORE_ABSTRACTFOLLOWER_H
|
||||
|
||||
#include "Reference.h"
|
||||
|
||||
class TargetedMovementGeneratorBase;
|
||||
class Unit;
|
||||
|
||||
class FollowerReference : public Reference<Unit, TargetedMovementGeneratorBase>
|
||||
class AbstractFollower
|
||||
{
|
||||
protected:
|
||||
void targetObjectBuildLink() override;
|
||||
void targetObjectDestroyLink() override;
|
||||
void sourceObjectDestroyLink() override;
|
||||
public:
|
||||
explicit AbstractFollower(Unit* target = nullptr) { SetTarget(target); }
|
||||
virtual ~AbstractFollower() { SetTarget(nullptr); }
|
||||
|
||||
void SetTarget(Unit* unit);
|
||||
[[nodiscard]] Unit* GetTarget() const { return _target; }
|
||||
|
||||
private:
|
||||
Unit* _target = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FollowerReference.h"
|
||||
#include "TargetedMovementGenerator.h"
|
||||
#include "Unit.h"
|
||||
|
||||
void FollowerReference::targetObjectBuildLink()
|
||||
{
|
||||
getTarget()->addFollower(this);
|
||||
}
|
||||
|
||||
void FollowerReference::targetObjectDestroyLink()
|
||||
{
|
||||
getTarget()->removeFollower(this);
|
||||
}
|
||||
|
||||
void FollowerReference::sourceObjectDestroyLink()
|
||||
{
|
||||
GetSource()->stopFollowing();
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "CreatureAISelector.h"
|
||||
#include "EscortMovementGenerator.h"
|
||||
#include "FleeingMovementGenerator.h"
|
||||
#include "FormationMovementGenerator.h"
|
||||
#include "GameTime.h"
|
||||
#include "HomeMovementGenerator.h"
|
||||
#include "IdleMovementGenerator.h"
|
||||
@@ -466,6 +467,17 @@ void MotionMaster::MoveFollow(Unit* target, float dist, float angle, MovementSlo
|
||||
}
|
||||
}
|
||||
|
||||
void MotionMaster::MoveFormation(Unit* leader, float dist, float angle, uint32 point1, uint32 point2)
|
||||
{
|
||||
if (!leader || leader == _owner || _owner->HasUnitFlag(UNIT_FLAG_DISABLE_MOVE))
|
||||
return;
|
||||
|
||||
if (!_owner->IsCreature())
|
||||
return;
|
||||
|
||||
Mutate(new FormationMovementGenerator(leader, dist, angle, point1, point2), MOTION_SLOT_IDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The unit will move to a specific point. Doesn't work with UNIT_FLAG_DISABLE_MOVE
|
||||
*
|
||||
|
||||
@@ -55,7 +55,8 @@ enum MovementGeneratorType
|
||||
ROTATE_MOTION_TYPE = 15,
|
||||
EFFECT_MOTION_TYPE = 16,
|
||||
ESCORT_MOTION_TYPE = 17, // xinef: EscortMovementGenerator.h
|
||||
NULL_MOTION_TYPE = 18
|
||||
FORMATION_MOTION_TYPE = 18, // FormationMovementGenerator.h
|
||||
NULL_MOTION_TYPE = 19
|
||||
};
|
||||
|
||||
enum MovementSlot
|
||||
@@ -227,6 +228,7 @@ public:
|
||||
void MoveTargetedHome(bool walk = false);
|
||||
void MoveRandom(float wanderDistance = 0.0f);
|
||||
void MoveFollow(Unit* target, float dist, float angle, MovementSlot slot = MOTION_SLOT_ACTIVE, bool inheritWalkState = true, bool inheritSpeed = true);
|
||||
void MoveFormation(Unit* leader, float dist, float angle, uint32 point1, uint32 point2);
|
||||
void MoveChase(Unit* target, std::optional<ChaseRange> dist = {}, std::optional<ChaseAngle> angle = {});
|
||||
void MoveChase(Unit* target, float dist, float angle) { MoveChase(target, ChaseRange(dist), ChaseAngle(angle)); }
|
||||
void MoveChase(Unit* target, float dist) { MoveChase(target, ChaseRange(dist)); }
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FormationMovementGenerator.h"
|
||||
#include "Creature.h"
|
||||
#include "CreatureAI.h"
|
||||
#include "CreatureGroups.h"
|
||||
#include "MoveSpline.h"
|
||||
#include "MoveSplineInit.h"
|
||||
|
||||
FormationMovementGenerator::FormationMovementGenerator(Unit* leader, float range, float angle, uint32 point1, uint32 point2)
|
||||
: AbstractFollower(leader), _range(range), _angle(angle), _point1(point1), _point2(point2), _lastLeaderSplineID(0),
|
||||
_hasPredictedDestination(false), _isMoving(false), _nextMoveTimer(0)
|
||||
{
|
||||
}
|
||||
|
||||
void FormationMovementGenerator::DoInitialize(Creature* owner)
|
||||
{
|
||||
if (owner->HasUnitState(UNIT_STATE_NOT_MOVE) || owner->IsMovementPreventedByCasting())
|
||||
{
|
||||
owner->StopMoving();
|
||||
return;
|
||||
}
|
||||
|
||||
_nextMoveTimer.Reset(0);
|
||||
}
|
||||
|
||||
void FormationMovementGenerator::DoFinalize(Creature* owner)
|
||||
{
|
||||
owner->ClearUnitState(UNIT_STATE_FOLLOW_MOVE);
|
||||
}
|
||||
|
||||
void FormationMovementGenerator::DoReset(Creature* owner)
|
||||
{
|
||||
DoInitialize(owner);
|
||||
}
|
||||
|
||||
bool FormationMovementGenerator::DoUpdate(Creature* owner, uint32 diff)
|
||||
{
|
||||
Unit* target = GetTarget();
|
||||
if (!owner || !target)
|
||||
return false;
|
||||
|
||||
if (owner->HasUnitState(UNIT_STATE_NOT_MOVE) || owner->IsMovementPreventedByCasting())
|
||||
{
|
||||
owner->StopMoving();
|
||||
_nextMoveTimer.Reset(0);
|
||||
_hasPredictedDestination = false;
|
||||
_isMoving = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target->movespline->Finalized() && target->movespline->GetId() == _lastLeaderSplineID && _hasPredictedDestination)
|
||||
{
|
||||
owner->StopMoving();
|
||||
_nextMoveTimer.Reset(0);
|
||||
_hasPredictedDestination = false;
|
||||
_isMoving = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!owner->movespline->Finalized())
|
||||
owner->SetHomePosition(owner->GetPosition());
|
||||
|
||||
if (!target->movespline->Finalized() && target->movespline->GetId() != _lastLeaderSplineID)
|
||||
{
|
||||
if (_point1 && target->IsCreature())
|
||||
{
|
||||
if (CreatureGroup* formation = target->ToCreature()->GetFormation())
|
||||
{
|
||||
if (Creature* leader = formation->GetLeader())
|
||||
{
|
||||
uint32 currentWaypoint = leader->GetCurrentWaypointID() + 1;
|
||||
if (currentWaypoint == _point1 || currentWaypoint == _point2)
|
||||
_angle = Position::NormalizeOrientation(float(2 * M_PI) - _angle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchMovement(owner, target);
|
||||
_lastLeaderSplineID = target->movespline->GetId();
|
||||
return true;
|
||||
}
|
||||
|
||||
_nextMoveTimer.Update(diff);
|
||||
if (_nextMoveTimer.Passed())
|
||||
{
|
||||
_nextMoveTimer.Reset(FORMATION_MOVEMENT_INTERVAL);
|
||||
|
||||
if (_lastLeaderPosition != target->GetPosition())
|
||||
{
|
||||
LaunchMovement(owner, target);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (_isMoving && owner->movespline->Finalized())
|
||||
{
|
||||
_isMoving = false;
|
||||
owner->SetFacingTo(target->GetOrientation());
|
||||
MovementInform(owner);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FormationMovementGenerator::LaunchMovement(Creature* owner, Unit* target)
|
||||
{
|
||||
float relativeAngle = 0.0f;
|
||||
|
||||
if (!target->movespline->Finalized())
|
||||
{
|
||||
G3D::Vector3 const leaderDestination = target->movespline->CurrentDestination();
|
||||
relativeAngle = target->GetRelativeAngle(leaderDestination.x, leaderDestination.y);
|
||||
}
|
||||
|
||||
Position dest = target->GetPosition();
|
||||
float velocity = 0.0f;
|
||||
|
||||
if (!target->movespline->Finalized())
|
||||
{
|
||||
// Pick up leader's spline velocity
|
||||
velocity = target->movespline->Velocity();
|
||||
|
||||
// Calculate travel distance to get a 1650ms result
|
||||
float travelDist = velocity * 1.65f;
|
||||
target->MovePositionToFirstCollision(dest, travelDist, relativeAngle);
|
||||
target->MovePositionToFirstCollision(dest, _range, _angle + relativeAngle);
|
||||
|
||||
float distance = owner->GetExactDist(dest);
|
||||
float velocityMod = std::min<float>(distance / travelDist, 1.5f);
|
||||
|
||||
velocity *= velocityMod;
|
||||
_hasPredictedDestination = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
target->MovePositionToFirstCollision(dest, _range, _angle + relativeAngle);
|
||||
_hasPredictedDestination = false;
|
||||
}
|
||||
|
||||
if (velocity == 0.0f)
|
||||
velocity = target->GetSpeed(MOVE_WALK);
|
||||
|
||||
Movement::MoveSplineInit init(owner);
|
||||
init.MoveTo(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
init.SetVelocity(velocity);
|
||||
init.Launch();
|
||||
|
||||
_lastLeaderPosition.Relocate(target->GetPosition());
|
||||
_isMoving = true;
|
||||
owner->AddUnitState(UNIT_STATE_FOLLOW_MOVE);
|
||||
}
|
||||
|
||||
void FormationMovementGenerator::MovementInform(Creature* owner)
|
||||
{
|
||||
if (owner->AI())
|
||||
owner->AI()->MovementInform(FORMATION_MOTION_TYPE, 0);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef ACORE_FORMATIONMOVEMENTGENERATOR_H
|
||||
#define ACORE_FORMATIONMOVEMENTGENERATOR_H
|
||||
|
||||
#include "AbstractFollower.h"
|
||||
#include "MovementGenerator.h"
|
||||
#include "Position.h"
|
||||
#include "Timer.h"
|
||||
|
||||
class Creature;
|
||||
|
||||
class FormationMovementGenerator : public MovementGeneratorMedium<Creature, FormationMovementGenerator>, public AbstractFollower
|
||||
{
|
||||
public:
|
||||
explicit FormationMovementGenerator(Unit* leader, float range, float angle, uint32 point1, uint32 point2);
|
||||
|
||||
MovementGeneratorType GetMovementGeneratorType() { return FORMATION_MOTION_TYPE; }
|
||||
|
||||
void DoInitialize(Creature*);
|
||||
void DoFinalize(Creature*);
|
||||
void DoReset(Creature*);
|
||||
bool DoUpdate(Creature*, uint32);
|
||||
|
||||
private:
|
||||
void MovementInform(Creature* owner);
|
||||
void LaunchMovement(Creature* owner, Unit* target);
|
||||
|
||||
static constexpr uint32 FORMATION_MOVEMENT_INTERVAL = 1200;
|
||||
|
||||
float const _range;
|
||||
float _angle;
|
||||
uint32 const _point1;
|
||||
uint32 const _point2;
|
||||
uint32 _lastLeaderSplineID;
|
||||
bool _hasPredictedDestination;
|
||||
bool _isMoving;
|
||||
|
||||
Position _lastLeaderPosition;
|
||||
TimeTracker _nextMoveTimer;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -67,8 +67,8 @@ void RandomMovementGenerator<Creature>::_setRandomLocation(Creature* creature)
|
||||
|
||||
init.SetWalk(walk);
|
||||
init.Launch();
|
||||
if (creature->GetFormation() && creature->GetFormation()->GetLeader() == creature)
|
||||
creature->GetFormation()->LeaderMoveTo(_currDestPosition.GetPositionX(), _currDestPosition.GetPositionY(), _currDestPosition.GetPositionZ(), 0);
|
||||
if (creature->GetFormation() && creature->GetFormation()->GetLeader() == creature && creature->GetFormation()->CanLeaderStartMoving())
|
||||
creature->GetFormation()->LeaderStartedMoving();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,8 +242,8 @@ void RandomMovementGenerator<Creature>::_setRandomLocation(Creature* creature)
|
||||
}
|
||||
|
||||
//Call for creature group update
|
||||
if (creature->GetFormation() && creature->GetFormation()->GetLeader() == creature)
|
||||
creature->GetFormation()->LeaderMoveTo(finalPoint.x, finalPoint.y, finalPoint.z, 0);
|
||||
if (creature->GetFormation() && creature->GetFormation()->GetLeader() == creature && creature->GetFormation()->CanLeaderStartMoving())
|
||||
creature->GetFormation()->LeaderStartedMoving();
|
||||
|
||||
if (sWorld->getBoolConfig(CONFIG_DONT_CACHE_RANDOM_MOVEMENT_PATHS))
|
||||
_preComputedPaths.erase(pathIdx);
|
||||
|
||||
@@ -73,7 +73,7 @@ void ChaseMovementGenerator<T>::SetOffsetAndAngle(std::optional<ChaseRange> dist
|
||||
template<class T>
|
||||
void ChaseMovementGenerator<T>::SetNewTarget(Unit* target)
|
||||
{
|
||||
i_target.link(target, this);
|
||||
SetTarget(target);
|
||||
_lastTargetPosition.reset();
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ void ChaseMovementGenerator<T>::DistanceYourself(T* owner, float distance)
|
||||
i_path = std::make_unique<PathGenerator>(owner);
|
||||
|
||||
float x, y, z;
|
||||
i_target->GetNearPoint(owner, x, y, z, owner->GetBoundaryRadius(), distance, i_target->GetAngle(owner));
|
||||
GetTarget()->GetNearPoint(owner, x, y, z, owner->GetBoundaryRadius(), distance, GetTarget()->GetAngle(owner));
|
||||
if (DispatchSplineToPosition(owner, x, y, z, false, false, 0.f, false, false))
|
||||
{
|
||||
m_currentMode = CHASE_MODE_DISTANCING;
|
||||
@@ -110,7 +110,7 @@ bool ChaseMovementGenerator<T>::DispatchSplineToPosition(T* owner, float x, floa
|
||||
|
||||
// For pets, treat incomplete paths as failures to avoid clipping through geometry
|
||||
// Players and Player-controlled units have more erratic movement, skip failure
|
||||
if (cOwner && (cOwner->IsPet() || cOwner->IsControlledByPlayer()) && !i_target.getTarget()->IsCharmedOwnedByPlayerOrPlayer())
|
||||
if (cOwner && (cOwner->IsPet() || cOwner->IsControlledByPlayer()) && !GetTarget()->IsCharmedOwnedByPlayerOrPlayer())
|
||||
if (pathType & PATHFIND_INCOMPLETE)
|
||||
pathFailed = true;
|
||||
|
||||
@@ -118,7 +118,7 @@ bool ChaseMovementGenerator<T>::DispatchSplineToPosition(T* owner, float x, floa
|
||||
{
|
||||
if (cOwner)
|
||||
{
|
||||
cOwner->SetCannotReachTarget(i_target.getTarget()->GetGUID());
|
||||
cOwner->SetCannotReachTarget(GetTarget()->GetGUID());
|
||||
|
||||
if (cOwner->IsPet() || cOwner->IsControlledByPlayer())
|
||||
cOwner->AttackStop();
|
||||
@@ -142,7 +142,7 @@ bool ChaseMovementGenerator<T>::DispatchSplineToPosition(T* owner, float x, floa
|
||||
Movement::MoveSplineInit init(owner);
|
||||
init.MovebyPath(i_path->GetPath());
|
||||
if (target)
|
||||
init.SetFacing(i_target.getTarget());
|
||||
init.SetFacing(GetTarget());
|
||||
init.SetWalk(walk);
|
||||
init.Launch();
|
||||
|
||||
@@ -152,7 +152,7 @@ bool ChaseMovementGenerator<T>::DispatchSplineToPosition(T* owner, float x, floa
|
||||
template<class T>
|
||||
bool ChaseMovementGenerator<T>::DoUpdate(T* owner, uint32 time_diff)
|
||||
{
|
||||
if (!i_target.isValid() || !i_target->IsInWorld() || !owner->IsInMap(i_target.getTarget()))
|
||||
if (!GetTarget() || !GetTarget()->IsInWorld() || !owner->IsInMap(GetTarget()))
|
||||
return false;
|
||||
|
||||
if (!owner || !owner->IsAlive())
|
||||
@@ -194,11 +194,11 @@ bool ChaseMovementGenerator<T>::DoUpdate(T* owner, uint32 time_diff)
|
||||
|
||||
bool forceDest =
|
||||
//(cOwner && (cOwner->isWorldBoss() || cOwner->IsDungeonBoss())) || // force for all bosses, even not in instances
|
||||
(i_target->IsPlayer() && i_target->ToPlayer()->IsGameMaster()) || // for .npc follow
|
||||
(GetTarget()->IsPlayer() && GetTarget()->ToPlayer()->IsGameMaster()) || // for .npc follow
|
||||
(owner->CanFly())
|
||||
; // closes "bool forceDest", that way it is more appropriate, so we can comment out crap whenever we need to
|
||||
|
||||
Unit* target = i_target.getTarget();
|
||||
Unit* target = GetTarget();
|
||||
|
||||
bool mutualChase = IsMutualChase(owner, target);
|
||||
bool const mutualTarget = target->GetVictim() == owner;
|
||||
@@ -414,7 +414,7 @@ void ChaseMovementGenerator<T>::MovementInform(T* owner)
|
||||
{
|
||||
// Pass back the GUIDLow of the target. If it is pet's owner then PetAI will handle
|
||||
if (CreatureAI* AI = owner->ToCreature()->AI())
|
||||
AI->MovementInform(CHASE_MOTION_TYPE, i_target.getTarget()->GetGUID().GetCounter());
|
||||
AI->MovementInform(CHASE_MOTION_TYPE, GetTarget()->GetGUID().GetCounter());
|
||||
break;
|
||||
}
|
||||
case CHASE_MODE_DISTANCING:
|
||||
@@ -552,14 +552,14 @@ bool FollowMovementGenerator<T>::PositionOkay(Unit* target, bool isPlayerPet, bo
|
||||
template<class T>
|
||||
bool FollowMovementGenerator<T>::DoUpdate(T* owner, uint32 time_diff)
|
||||
{
|
||||
if (!i_target.isValid() || !i_target->IsInWorld() || !owner->IsInMap(i_target.getTarget()))
|
||||
if (!GetTarget() || !GetTarget()->IsInWorld() || !owner->IsInMap(GetTarget()))
|
||||
return false;
|
||||
|
||||
if (!owner || !owner->IsAlive())
|
||||
return false;
|
||||
|
||||
Creature* cOwner = owner->ToCreature();
|
||||
Unit* target = i_target.getTarget();
|
||||
Unit* target = GetTarget();
|
||||
|
||||
// the owner might be unable to move (rooted or casting), or we have lost the target, pause movement
|
||||
if (owner->HasUnitState(UNIT_STATE_NOT_MOVE) || (cOwner && owner->ToCreature()->IsMovementPreventedByCasting()))
|
||||
@@ -580,7 +580,7 @@ bool FollowMovementGenerator<T>::DoUpdate(T* owner, uint32 time_diff)
|
||||
|
||||
bool forceDest =
|
||||
(followingMaster) || // allow pets following their master to cheat while generating paths
|
||||
(i_target->IsPlayer() && i_target->ToPlayer()->IsGameMaster()) // for .npc follow
|
||||
(GetTarget()->IsPlayer() && GetTarget()->ToPlayer()->IsGameMaster()) // for .npc follow
|
||||
; // closes "bool forceDest", that way it is more appropriate, so we can comment out crap whenever we need to
|
||||
|
||||
bool targetIsMoving = false;
|
||||
@@ -717,7 +717,7 @@ void FollowMovementGenerator<T>::MovementInform(T* owner)
|
||||
|
||||
// Pass back the GUIDLow of the target. If it is pet's owner then PetAI will handle
|
||||
if (CreatureAI* AI = owner->ToCreature()->AI())
|
||||
AI->MovementInform(FOLLOW_MOTION_TYPE, i_target.getTarget()->GetGUID().GetCounter());
|
||||
AI->MovementInform(FOLLOW_MOTION_TYPE, GetTarget()->GetGUID().GetCounter());
|
||||
}
|
||||
|
||||
//-----------------------------------------------//
|
||||
|
||||
@@ -18,22 +18,13 @@
|
||||
#ifndef ACORE_TARGETEDMOVEMENTGENERATOR_H
|
||||
#define ACORE_TARGETEDMOVEMENTGENERATOR_H
|
||||
|
||||
#include "FollowerReference.h"
|
||||
#include "AbstractFollower.h"
|
||||
#include "MovementGenerator.h"
|
||||
#include "Optional.h"
|
||||
#include "PathGenerator.h"
|
||||
#include "Timer.h"
|
||||
#include "Unit.h"
|
||||
|
||||
class TargetedMovementGeneratorBase
|
||||
{
|
||||
public:
|
||||
TargetedMovementGeneratorBase(Unit* target) { i_target.link(target, this); }
|
||||
void stopFollowing() { }
|
||||
protected:
|
||||
FollowerReference i_target;
|
||||
};
|
||||
|
||||
enum ChaseMovementMode
|
||||
{
|
||||
CHASE_MODE_NORMAL, // chasing target
|
||||
@@ -43,11 +34,11 @@ enum ChaseMovementMode
|
||||
};
|
||||
|
||||
template<class T>
|
||||
class ChaseMovementGenerator : public MovementGeneratorMedium<T, ChaseMovementGenerator<T>>, public TargetedMovementGeneratorBase
|
||||
class ChaseMovementGenerator : public MovementGeneratorMedium<T, ChaseMovementGenerator<T>>, public AbstractFollower
|
||||
{
|
||||
public:
|
||||
ChaseMovementGenerator(Unit* target, Optional<ChaseRange> range = {}, Optional<ChaseAngle> angle = {})
|
||||
: TargetedMovementGeneratorBase(target), i_leashExtensionTimer(5000), i_path(nullptr), i_recheckDistance(0), i_recalculateTravel(true), _range(range), _angle(angle), m_currentMode(CHASE_MODE_NORMAL) {}
|
||||
: AbstractFollower(target), i_leashExtensionTimer(5000), i_path(nullptr), i_recheckDistance(0), i_recalculateTravel(true), _range(range), _angle(angle), m_currentMode(CHASE_MODE_NORMAL) {}
|
||||
~ChaseMovementGenerator() { }
|
||||
|
||||
MovementGeneratorType GetMovementGeneratorType() { return CHASE_MOTION_TYPE; }
|
||||
@@ -61,7 +52,7 @@ public:
|
||||
bool PositionOkay(T* owner, Unit* target, Optional<float> maxDistance, Optional<ChaseAngle> angle);
|
||||
|
||||
void unitSpeedChanged() { _lastTargetPosition.reset(); }
|
||||
Unit* GetTarget() const { return i_target.getTarget(); }
|
||||
Unit* GetTarget() const { return AbstractFollower::GetTarget(); }
|
||||
|
||||
bool EnableWalking() const { return false; }
|
||||
bool HasLostTarget(Unit* unit) const { return unit->GetVictim() != this->GetTarget(); }
|
||||
@@ -87,11 +78,11 @@ private:
|
||||
};
|
||||
|
||||
template<class T>
|
||||
class FollowMovementGenerator : public MovementGeneratorMedium<T, FollowMovementGenerator<T>>, public TargetedMovementGeneratorBase
|
||||
class FollowMovementGenerator : public MovementGeneratorMedium<T, FollowMovementGenerator<T>>, public AbstractFollower
|
||||
{
|
||||
public:
|
||||
FollowMovementGenerator(Unit* target, float range, ChaseAngle angle, bool inheritWalkState, bool inheritSpeed)
|
||||
: TargetedMovementGeneratorBase(target), i_path(nullptr), i_recheckPredictedDistanceTimer(0), i_recheckPredictedDistance(false), _range(range), _angle(angle), _inheritWalkState(inheritWalkState), _inheritSpeed(inheritSpeed) {}
|
||||
: AbstractFollower(target), i_path(nullptr), i_recheckPredictedDistanceTimer(0), i_recheckPredictedDistance(false), _range(range), _angle(angle), _inheritWalkState(inheritWalkState), _inheritSpeed(inheritSpeed) {}
|
||||
~FollowMovementGenerator() { }
|
||||
|
||||
MovementGeneratorType GetMovementGeneratorType() { return FOLLOW_MOTION_TYPE; }
|
||||
@@ -102,7 +93,7 @@ public:
|
||||
void DoReset(T*);
|
||||
void MovementInform(T*);
|
||||
|
||||
Unit* GetTarget() const { return i_target.getTarget(); }
|
||||
Unit* GetTarget() const { return AbstractFollower::GetTarget(); }
|
||||
|
||||
void unitSpeedChanged() { _lastTargetPosition.reset(); }
|
||||
|
||||
|
||||
@@ -222,8 +222,8 @@ bool WaypointMovementGenerator<Creature>::StartMove(Creature* creature)
|
||||
init.Launch();
|
||||
|
||||
//Call for creature group update
|
||||
if (creature->GetFormation() && creature->GetFormation()->GetLeader() == creature)
|
||||
creature->GetFormation()->LeaderMoveTo(formationDest.x, formationDest.y, formationDest.z, node.move_type);
|
||||
if (creature->GetFormation() && creature->GetFormation()->GetLeader() == creature && creature->GetFormation()->CanLeaderStartMoving())
|
||||
creature->GetFormation()->LeaderStartedMoving();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ namespace Movement
|
||||
time_passed = 0;
|
||||
vertical_acceleration = 0.f;
|
||||
effect_start_time = 0;
|
||||
velocity = args.velocity;
|
||||
|
||||
// Check if its a stop spline
|
||||
if (args.flags.done)
|
||||
@@ -190,7 +191,7 @@ namespace Movement
|
||||
}
|
||||
|
||||
MoveSpline::MoveSpline() : m_Id(0), time_passed(0),
|
||||
vertical_acceleration(0.f), initialOrientation(0.f), effect_start_time(0), point_Idx(0), point_Idx_offset(0),
|
||||
vertical_acceleration(0.f), initialOrientation(0.f), velocity(0.f), effect_start_time(0), point_Idx(0), point_Idx_offset(0),
|
||||
onTransport(false)
|
||||
{
|
||||
splineflags.done = true;
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace Movement
|
||||
//float duration_mod_next;
|
||||
float vertical_acceleration;
|
||||
float initialOrientation;
|
||||
float velocity;
|
||||
int32 effect_start_time;
|
||||
int32 point_Idx;
|
||||
int32 point_Idx_offset;
|
||||
@@ -85,6 +86,7 @@ namespace Movement
|
||||
[[nodiscard]] int32 Duration() const { return spline.length(); }
|
||||
[[nodiscard]] MySpline const& _Spline() const { return spline; }
|
||||
[[nodiscard]] int32 _currentSplineIdx() const { return point_Idx; }
|
||||
[[nodiscard]] float Velocity() const { return velocity; }
|
||||
void _Finalize();
|
||||
void _Interrupt() { splineflags.done = true; }
|
||||
|
||||
|
||||
@@ -59,9 +59,7 @@ bool ScriptMgr::CanPacketSend(WorldSession* session, WorldPacket const& packet)
|
||||
if (ScriptRegistry<ServerScript>::ScriptPointerList.empty())
|
||||
return true;
|
||||
|
||||
WorldPacket copy(packet);
|
||||
|
||||
CALL_ENABLED_BOOLEAN_HOOKS(ServerScript, SERVERHOOK_CAN_PACKET_SEND, !script->CanPacketSend(session, copy));
|
||||
CALL_ENABLED_BOOLEAN_HOOKS(ServerScript, SERVERHOOK_CAN_PACKET_SEND, !script->CanPacketSend(session, packet));
|
||||
}
|
||||
|
||||
bool ScriptMgr::CanPacketReceive(WorldSession* session, WorldPacket const& packet)
|
||||
@@ -69,9 +67,7 @@ bool ScriptMgr::CanPacketReceive(WorldSession* session, WorldPacket const& packe
|
||||
if (ScriptRegistry<ServerScript>::ScriptPointerList.empty())
|
||||
return true;
|
||||
|
||||
WorldPacket copy(packet);
|
||||
|
||||
CALL_ENABLED_BOOLEAN_HOOKS(ServerScript, SERVERHOOK_CAN_PACKET_RECEIVE, !script->CanPacketReceive(session, copy));
|
||||
CALL_ENABLED_BOOLEAN_HOOKS(ServerScript, SERVERHOOK_CAN_PACKET_RECEIVE, !script->CanPacketReceive(session, packet));
|
||||
}
|
||||
|
||||
ServerScript::ServerScript(const char* name, std::vector<uint16> enabledHooks)
|
||||
|
||||
@@ -53,23 +53,24 @@ public:
|
||||
virtual void OnSocketClose(std::shared_ptr<WorldSocket> const& /*socket*/) { }
|
||||
|
||||
/**
|
||||
* @brief This hook called when a packet is sent to a client. The packet object is a copy of the original packet, so reading and modifying it is safe.
|
||||
* @brief This hook is called when a packet is sent to a client.
|
||||
*
|
||||
* @param session Contains information about the WorldSession
|
||||
* @param packet Contains information about the WorldPacket
|
||||
* @return True if you want to continue sending the packet, false if you want to disallow sending the packet
|
||||
*/
|
||||
[[nodiscard]] virtual bool CanPacketSend(WorldSession* /*session*/, WorldPacket& /*packet*/) { return true; }
|
||||
[[nodiscard]] virtual bool CanPacketSend(WorldSession* /*session*/, WorldPacket const& /*packet*/) { return true; }
|
||||
|
||||
/**
|
||||
* @brief Called when a (valid) packet is received by a client. The packet object is a copy of the original packet, so
|
||||
* reading and modifying it is safe. Make sure to check WorldSession pointer before usage, it might be null in case of auth packets
|
||||
* @brief Called when a (valid) packet is received by a client.
|
||||
* Make sure to check WorldSession pointer before usage, it might be null in case of auth packets
|
||||
*
|
||||
* @param session Contains information about the WorldSession
|
||||
* @param packet Contains information about the WorldPacket
|
||||
* @return True if you want to continue receive the packet, false if you want to disallow receive the packet
|
||||
* @return True if you want to continue receiving the packet, false if you want to disallow receiving the packet
|
||||
*/
|
||||
[[nodiscard]] virtual bool CanPacketReceive(WorldSession* /*session*/, WorldPacket& /*packet*/) { return true; }
|
||||
[[nodiscard]] virtual bool CanPacketReceive(WorldSession* /*session*/, WorldPacket const& /*packet*/) { return true; }
|
||||
|
||||
virtual void OnPacketReceived(WorldSession* /*session*/, WorldPacket const& /*packet*/) { }
|
||||
};
|
||||
|
||||
@@ -87,6 +87,7 @@ void ScriptMgr::Initialize()
|
||||
ScriptRegistry<ArenaScript>::InitEnabledHooksIfNeeded(ARENAHOOK_END);
|
||||
ScriptRegistry<ArenaTeamScript>::InitEnabledHooksIfNeeded(ARENATEAMHOOK_END);
|
||||
ScriptRegistry<AuctionHouseScript>::InitEnabledHooksIfNeeded(AUCTIONHOUSEHOOK_END);
|
||||
ScriptRegistry<BattlefieldScript>::InitEnabledHooksIfNeeded(BATTLEFIELDHOOK_END);
|
||||
ScriptRegistry<BGScript>::InitEnabledHooksIfNeeded(ALLBATTLEGROUNDHOOK_END);
|
||||
ScriptRegistry<CommandSC>::InitEnabledHooksIfNeeded(ALLCOMMANDHOOK_END);
|
||||
ScriptRegistry<DatabaseScript>::InitEnabledHooksIfNeeded(DATABASEHOOK_END);
|
||||
|
||||
@@ -1105,14 +1105,12 @@ bool AuraEffect::IsAffectedOnSpell(SpellInfo const* spell) const
|
||||
{
|
||||
if (!spell)
|
||||
return false;
|
||||
// Check family name
|
||||
if (spell->SpellFamilyName != m_spellInfo->SpellFamilyName)
|
||||
|
||||
// Check family name and EffectClassMask
|
||||
if (!spell->IsAffected(m_spellInfo->SpellFamilyName, m_spellInfo->Effects[m_effIndex].SpellClassMask))
|
||||
return false;
|
||||
|
||||
// Check EffectClassMask
|
||||
if (m_spellInfo->Effects[m_effIndex].SpellClassMask & spell->SpellFamilyFlags)
|
||||
return true;
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuraEffect::HasSpellClassMask() const
|
||||
|
||||
@@ -1348,28 +1348,6 @@ void Aura::HandleAuraSpecificMods(AuraApplication const* aurApp, Unit* caster, b
|
||||
}
|
||||
switch (GetId())
|
||||
{
|
||||
case 12536: // Clearcasting
|
||||
case 12043: // Presence of Mind
|
||||
// Arcane Potency
|
||||
if (AuraEffect const* aurEff = caster->GetAuraEffect(SPELL_AURA_DUMMY, SPELLFAMILY_MAGE, 2120, 0))
|
||||
{
|
||||
uint32 spellId = 0;
|
||||
|
||||
switch (aurEff->GetId())
|
||||
{
|
||||
case 31571:
|
||||
spellId = 57529;
|
||||
break;
|
||||
case 31572:
|
||||
spellId = 57531;
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("spells.aura", "Aura::HandleAuraSpecificMods: Unknown rank of Arcane Potency ({}) found", aurEff->GetId());
|
||||
}
|
||||
if (spellId)
|
||||
caster->CastSpell(caster, spellId, true);
|
||||
}
|
||||
break;
|
||||
case 44544: // Fingers of Frost
|
||||
{
|
||||
// See if we already have the indicator aura. If not, create one.
|
||||
|
||||
@@ -64,9 +64,9 @@ public:
|
||||
uint8 GetSlot() const { return _slot; }
|
||||
uint8 GetFlags() const { return _flags; }
|
||||
uint8 GetEffectMask() const { return _flags & (AFLAG_EFF_INDEX_0 | AFLAG_EFF_INDEX_1 | AFLAG_EFF_INDEX_2); }
|
||||
bool HasEffect(uint8 effect) const { ASSERT(effect < MAX_SPELL_EFFECTS); return _flags & (1 << effect); }
|
||||
bool IsPositive() const { return _flags & AFLAG_POSITIVE; }
|
||||
bool IsSelfcasted() const { return _flags & AFLAG_CASTER; }
|
||||
bool HasEffect(uint8 effect) const { ASSERT(effect < MAX_SPELL_EFFECTS); return (_flags & (1 << effect)) != 0; }
|
||||
bool IsPositive() const { return (_flags & AFLAG_POSITIVE) != 0; }
|
||||
bool IsSelfcasted() const { return (_flags & AFLAG_CASTER) != 0; }
|
||||
uint8 GetEffectsToApply() const { return _effectsToApply; }
|
||||
|
||||
void SetRemoveMode(AuraRemoveMode mode) { _removeMode = mode; }
|
||||
|
||||
@@ -1862,7 +1862,7 @@ float tangent(float x)
|
||||
//if (x <= -std::numeric_limits<float>::max()) return -std::numeric_limits<float>::max();
|
||||
if (x < 100000.0f && x > -100000.0f) return x;
|
||||
if (x >= 100000.0f) return 100000.0f;
|
||||
if (x <= 100000.0f) return -100000.0f;
|
||||
if (x <= -100000.0f) return -100000.0f;
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
@@ -2126,6 +2126,10 @@ void Spell::SearchChainTargets(std::list<WorldObject*>& targets, uint32 chainTar
|
||||
break;
|
||||
}
|
||||
|
||||
// per-spell override from spell_jump_distance table
|
||||
if (m_spellInfo->JumpDistance > 0.0f)
|
||||
jumpRadius = m_spellInfo->JumpDistance;
|
||||
|
||||
// chain lightning/heal spells and similar - allow to jump at larger distance and go out of los
|
||||
bool isBouncingFar = (m_spellInfo->HasAttribute(SPELL_ATTR4_BOUNCY_CHAIN_MISSILES)
|
||||
|| m_spellInfo->DmgClass == SPELL_DAMAGE_CLASS_NONE
|
||||
@@ -3255,7 +3259,7 @@ void Spell::DoTriggersOnSpellHit(Unit* unit, uint8 effMask)
|
||||
Aura* aur = unit->GetAura(m_spellInfo->Id, m_caster->GetGUID());
|
||||
_duration = aur ? aur->GetDuration() : -1;
|
||||
}
|
||||
triggeredAur->SetDuration(_duration);
|
||||
triggeredAur->SetDuration(std::max(triggeredAur->GetDuration(), _duration));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3629,6 +3633,12 @@ SpellCastResult Spell::prepare(SpellCastTargets const* targets, AuraEffect const
|
||||
m_caster->SetCurrentCastedSpell(this);
|
||||
SendSpellStart();
|
||||
|
||||
// Call CreatureAI hook OnSpellStart for spells with cast time or channeled spells
|
||||
if (m_casttime > 0 || m_spellInfo->IsChanneled())
|
||||
if (Creature* caster = m_caster->ToCreature())
|
||||
if (caster->IsAIEnabled)
|
||||
caster->AI()->OnSpellStart(GetSpellInfo());
|
||||
|
||||
// set target for proper facing
|
||||
if ((m_casttime || m_spellInfo->IsChanneled()) && !HasTriggeredCastFlag(TRIGGERED_IGNORE_SET_FACING))
|
||||
{
|
||||
@@ -4016,7 +4026,7 @@ void Spell::_cast(bool skipCheck)
|
||||
break;
|
||||
}
|
||||
|
||||
Unit::ProcSkillsAndAuras(m_originalCaster, m_originalCaster, procAttacker, PROC_FLAG_NONE, hitMask, 1, BASE_ATTACK, m_spellInfo, m_triggeredByAuraSpell.spellInfo,
|
||||
Unit::ProcSkillsAndAuras(m_originalCaster, nullptr, procAttacker, PROC_FLAG_NONE, hitMask, 1, BASE_ATTACK, m_spellInfo, m_triggeredByAuraSpell.spellInfo,
|
||||
m_triggeredByAuraSpell.effectIndex, this, nullptr, nullptr, PROC_SPELL_PHASE_CAST);
|
||||
}
|
||||
|
||||
@@ -4052,11 +4062,10 @@ void Spell::_cast(bool skipCheck)
|
||||
|
||||
SetExecutedCurrently(false);
|
||||
|
||||
// Call CreatureAI hook OnSpellCastFinished
|
||||
if (m_originalCaster)
|
||||
if (Creature* caster = m_originalCaster->ToCreature())
|
||||
if (caster->IsAIEnabled)
|
||||
caster->AI()->OnSpellCastFinished(GetSpellInfo(), SPELL_FINISHED_SUCCESSFUL_CAST);
|
||||
// Call CreatureAI hook on successful cast
|
||||
if (Creature* caster = m_caster->ToCreature())
|
||||
if (caster->IsAIEnabled)
|
||||
caster->AI()->OnSpellCast(GetSpellInfo());
|
||||
}
|
||||
|
||||
void Spell::handle_immediate()
|
||||
@@ -4416,7 +4425,7 @@ void Spell::update(uint32 difftime)
|
||||
// We call the hook here instead of in Spell::finish because we only want to call it for completed channeling. Everything else is handled by interrupts
|
||||
if (Creature* creatureCaster = m_caster->ToCreature())
|
||||
if (creatureCaster->IsAIEnabled)
|
||||
creatureCaster->AI()->OnSpellCastFinished(m_spellInfo, SPELL_FINISHED_CHANNELING_COMPLETE);
|
||||
creatureCaster->AI()->OnChannelFinished(m_spellInfo);
|
||||
}
|
||||
// Xinef: Dont update channeled target list on last tick, allow auras to update duration properly
|
||||
// Xinef: Added this strange check because of diffrent update routines for players / creatures
|
||||
@@ -4478,8 +4487,8 @@ void Spell::finish(bool ok)
|
||||
// Xinef: Reset cooldown event in case of fail cast
|
||||
if (m_spellInfo->IsCooldownStartedOnEvent())
|
||||
m_caster->ToPlayer()->SendCooldownEvent(m_spellInfo, 0, 0, false);
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6878,9 +6887,9 @@ SpellCastResult Spell::CheckCasterAuras(bool preventionOnly) const
|
||||
// Barkskin should skip sleep effects, sap and fears
|
||||
if (m_spellInfo->Id == 22812)
|
||||
mask |= 1 << MECHANIC_SAPPED | 1 << MECHANIC_HORROR | 1 << MECHANIC_SLEEP;
|
||||
// Hand of Freedom, can be used while sapped
|
||||
// Hand of Freedom, can be used while sapped and while under fear-mechanic stuns (e.g. Intimidating Shout primary target)
|
||||
if (m_spellInfo->Id == 1044)
|
||||
mask |= 1 << MECHANIC_SAPPED;
|
||||
mask |= (1 << MECHANIC_SAPPED) | (1 << MECHANIC_FEAR);
|
||||
Unit::AuraEffectList const& stunAuras = m_caster->GetAuraEffectsByType(SPELL_AURA_MOD_STUN);
|
||||
for (Unit::AuraEffectList::const_iterator i = stunAuras.begin(); i != stunAuras.end(); ++i)
|
||||
{
|
||||
@@ -6943,9 +6952,9 @@ SpellCastResult Spell::CheckCasterAuras(bool preventionOnly) const
|
||||
// Barkskin should skip sleep effects, sap and fears
|
||||
if (m_spellInfo->Id == 22812)
|
||||
mask |= 1 << MECHANIC_SAPPED | 1 << MECHANIC_HORROR | 1 << MECHANIC_SLEEP;
|
||||
// Hand of Freedom, can be used while sapped
|
||||
// Hand of Freedom, can be used while sapped and while under fear-mechanic stuns (e.g. Intimidating Shout primary target)
|
||||
if (m_spellInfo->Id == 1044)
|
||||
mask |= 1 << MECHANIC_SAPPED;
|
||||
mask |= (1 << MECHANIC_SAPPED) | (1 << MECHANIC_FEAR);
|
||||
|
||||
if (!usableInStun || !(auraInfo->GetAllEffectsMechanicMask() & mask))
|
||||
return SPELL_FAILED_STUNNED;
|
||||
|
||||
@@ -93,13 +93,6 @@ enum SpellRangeFlag
|
||||
SPELL_RANGE_RANGED = 2, //hunter range and ranged weapon
|
||||
};
|
||||
|
||||
enum SpellFinishReason : uint8
|
||||
{
|
||||
SPELL_FINISHED_SUCCESSFUL_CAST = 0, // spell has sucessfully launched
|
||||
SPELL_FINISHED_CANCELED = 1, // spell has been canceled (interrupts)
|
||||
SPELL_FINISHED_CHANNELING_COMPLETE = 2 // spell channeling has been finished
|
||||
};
|
||||
|
||||
struct SpellDestination
|
||||
{
|
||||
SpellDestination();
|
||||
|
||||
@@ -861,6 +861,7 @@ SpellInfo::SpellInfo(SpellEntry const* spellEntry)
|
||||
_isSpellValid = true;
|
||||
_isCritCapable = false;
|
||||
_requireCooldownInfo = false;
|
||||
JumpDistance = 0.0f;
|
||||
}
|
||||
|
||||
SpellInfo::~SpellInfo()
|
||||
@@ -1285,6 +1286,20 @@ bool SpellInfo::IsAutoRepeatRangedSpell() const
|
||||
return AttributesEx2 & SPELL_ATTR2_AUTO_REPEAT;
|
||||
}
|
||||
|
||||
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 !(AttributesEx3 & SPELL_ATTR3_IGNORE_CASTER_MODIFIERS);
|
||||
@@ -1300,23 +1315,12 @@ bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod) const
|
||||
SpellInfo const* affectSpell = sSpellMgr->GetSpellInfo(mod->spellId);
|
||||
|
||||
if (!affectSpell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sScriptMgr->OnIsAffectedBySpellModCheck(affectSpell, this, mod))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// False if affect_spell == nullptr or spellFamily not equal
|
||||
if (affectSpell->SpellFamilyName != SpellFamilyName)
|
||||
return false;
|
||||
|
||||
if (mod->mask && !(mod->mask & SpellFamilyFlags))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
return IsAffected(affectSpell->SpellFamilyName, mod->mask);
|
||||
}
|
||||
|
||||
bool SpellInfo::CanPierceImmuneAura(SpellInfo const* aura) const
|
||||
|
||||
@@ -401,6 +401,7 @@ public:
|
||||
bool _isSpellValid;
|
||||
bool _isCritCapable;
|
||||
bool _requireCooldownInfo;
|
||||
float JumpDistance;
|
||||
|
||||
SpellInfo(SpellEntry const* spellEntry);
|
||||
~SpellInfo();
|
||||
@@ -466,6 +467,8 @@ public:
|
||||
bool IsRangedWeaponSpell() const;
|
||||
bool IsAutoRepeatRangedSpell() const;
|
||||
|
||||
[[nodiscard]] bool IsAffected(uint32 familyName, flag96 const& familyFlags) const;
|
||||
|
||||
bool IsAffectedBySpellMods() const;
|
||||
bool IsAffectedBySpellMod(SpellModifier const* mod) const;
|
||||
|
||||
|
||||
@@ -652,6 +652,19 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
spellInfo->AttributesEx3 |= SPELL_ATTR3_ALWAYS_HIT;
|
||||
});
|
||||
|
||||
// Explosive Trap Effect
|
||||
ApplySpellFix({
|
||||
13812, // Explosive Trap Effect (Rank 1)
|
||||
14314, // Explosive Trap Effect (Rank 2)
|
||||
14315, // Explosive Trap Effect (Rank 3)
|
||||
27026, // Explosive Trap Effect (Rank 4)
|
||||
49064, // Explosive Trap Effect (Rank 5)
|
||||
49065 // Explosive Trap Effect (Rank 6)
|
||||
}, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->DmgClass = SPELL_DAMAGE_CLASS_MAGIC;
|
||||
});
|
||||
|
||||
// Kill Command
|
||||
// Kill Command, Overpower
|
||||
ApplySpellFix({ 34027, 37529 }, [](SpellInfo* spellInfo)
|
||||
@@ -1050,7 +1063,7 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
ApplySpellFix({ 44461, 55361, 55362 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->AttributesEx3 |= SPELL_ATTR3_SUPPRESS_TARGET_PROCS;
|
||||
spellInfo->AttributesEx4 |= SPELL_ATTR4_REACTIVE_DAMAGE_PROC;
|
||||
spellInfo->AttributesEx4 |= SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS;
|
||||
});
|
||||
|
||||
// Evocation
|
||||
@@ -1486,7 +1499,7 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
spellInfo->AttributesEx3 |= SPELL_ATTR3_DOT_STACKING_RULE;
|
||||
});
|
||||
|
||||
// Activate Sunblade Protecto
|
||||
// Activate Sunblade Protector
|
||||
ApplySpellFix({ 46475, 46476 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->RangeEntry = sSpellRangeStore.LookupEntry(14); // 60yd
|
||||
@@ -1533,14 +1546,21 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
spellInfo->AttributesEx6 |= SPELL_ATTR6_IGNORE_PHASE_SHIFT;
|
||||
});
|
||||
|
||||
// Parasitic Shadowfiend
|
||||
// Illidan Stormrage - Parasitic Shadowfiend
|
||||
ApplySpellFix({ 41914 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->Attributes |= SPELL_ATTR0_AURA_IS_DEBUFF;
|
||||
spellInfo->AttributesEx3 |= SPELL_ATTR3_DOT_STACKING_RULE;
|
||||
});
|
||||
|
||||
// Teleport Maiev
|
||||
// Illidan Stormrage - Demon Fire
|
||||
ApplySpellFix({ 40030 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->AttributesEx3 |= SPELL_ATTR3_ALWAYS_HIT;
|
||||
spellInfo->AttributesEx4 &= ~SPELL_ATTR4_NO_CAST_LOG;
|
||||
});
|
||||
|
||||
// Illidan Stormrage - Teleport Maiev
|
||||
ApplySpellFix({ 41221 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->RangeEntry = sSpellRangeStore.LookupEntry(13); // 0-50000yd
|
||||
|
||||
@@ -823,17 +823,10 @@ bool SpellMgr::CanSpellTriggerProcOnEvent(SpellProcEntry const& procEntry, ProcE
|
||||
|
||||
// check spell family name/flags (if set) for spells
|
||||
if (eventInfo.GetTypeMask() & SPELL_PROC_FLAG_MASK)
|
||||
{
|
||||
if (SpellInfo const* eventSpellInfo = eventInfo.GetSpellInfo())
|
||||
{
|
||||
if (procEntry.SpellFamilyName && procEntry.SpellFamilyName != eventSpellInfo->SpellFamilyName)
|
||||
if (!eventSpellInfo->IsAffected(procEntry.SpellFamilyName, procEntry.SpellFamilyMask))
|
||||
return false;
|
||||
|
||||
if (procEntry.SpellFamilyMask && !(procEntry.SpellFamilyMask & eventSpellInfo->SpellFamilyFlags))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// check spell type mask (if set)
|
||||
if (eventInfo.GetTypeMask() & (SPELL_PROC_FLAG_MASK | PERIODIC_PROC_FLAG_MASK))
|
||||
{
|
||||
@@ -1970,7 +1963,8 @@ void SpellMgr::LoadSpellProcs()
|
||||
if (!spellInfo->Effects[i].IsAura())
|
||||
continue;
|
||||
|
||||
if (spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_ADD_PCT_MODIFIER || spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_ADD_FLAT_MODIFIER)
|
||||
if (spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_ADD_PCT_MODIFIER || spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_ADD_FLAT_MODIFIER
|
||||
|| spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_MOD_SPELL_CRIT_CHANCE || spellInfo->Effects[i].ApplyAuraName == SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
@@ -2963,6 +2957,42 @@ void SpellMgr::LoadSpellSpecificAndAuraState()
|
||||
LOG_INFO("server.loading", " ");
|
||||
}
|
||||
|
||||
void SpellMgr::LoadSpellJumpDistances()
|
||||
{
|
||||
uint32 oldMSTime = getMSTime();
|
||||
|
||||
QueryResult result = WorldDatabase.Query("SELECT ID, JumpDistance FROM spell_jump_distance");
|
||||
|
||||
if (!result)
|
||||
{
|
||||
LOG_WARN("server.loading", ">> Loaded 0 spell jump distances. DB table `spell_jump_distance` is empty.");
|
||||
LOG_INFO("server.loading", " ");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32 count = 0;
|
||||
do
|
||||
{
|
||||
Field const* fields = result->Fetch();
|
||||
|
||||
uint32 const spellId = fields[0].Get<uint32>();
|
||||
float const jumpDistance = fields[1].Get<float>();
|
||||
|
||||
SpellInfo* spellInfo = _GetSpellInfo(spellId);
|
||||
if (!spellInfo)
|
||||
{
|
||||
LOG_ERROR("sql.sql", "Table `spell_jump_distance` has wrong spell (spell_id: {}), ignored.", spellId);
|
||||
continue;
|
||||
}
|
||||
|
||||
spellInfo->JumpDistance = jumpDistance;
|
||||
++count;
|
||||
} while (result->NextRow());
|
||||
|
||||
LOG_INFO("server.loading", ">> Loaded {} spell jump distances in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
|
||||
LOG_INFO("server.loading", " ");
|
||||
}
|
||||
|
||||
void SpellMgr::LoadSpellInfoCustomAttributes()
|
||||
{
|
||||
uint32 const oldMSTime = getMSTime();
|
||||
@@ -3230,7 +3260,7 @@ void SpellMgr::LoadSpellInfoCustomAttributes()
|
||||
continue;
|
||||
[[fallthrough]]; /// @todo: Not sure whether the fallthrough was a mistake (forgetting a break) or intended. This should be double-checked.
|
||||
default:
|
||||
if (!(spellInfo->Effects[j].CalcValue() &&
|
||||
if (!(spellInfo->Effects[j].CalcValue() ||
|
||||
((spellInfo->Effects[j].Effect == SPELL_EFFECT_INTERRUPT_CAST || spellInfo->HasAttribute(SPELL_ATTR0_CU_DONT_BREAK_STEALTH)) &&
|
||||
!spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES))))
|
||||
continue;
|
||||
|
||||
@@ -778,6 +778,7 @@ public:
|
||||
void LoadSpellInfoCustomAttributes();
|
||||
void LoadSpellInfoCorrections();
|
||||
void LoadSpellSpecificAndAuraState();
|
||||
void LoadSpellJumpDistances();
|
||||
|
||||
private:
|
||||
SpellDifficultySearcherMap mSpellDifficultySearcherMap;
|
||||
|
||||
@@ -434,6 +434,9 @@ void World::SetInitialWorldSettings()
|
||||
LOG_INFO("server.loading", "Loading SpellInfo Custom Attributes...");
|
||||
sSpellMgr->LoadSpellInfoCustomAttributes();
|
||||
|
||||
LOG_INFO("server.loading", "Loading Spell Jump Distances...");
|
||||
sSpellMgr->LoadSpellJumpDistances();
|
||||
|
||||
LOG_INFO("server.loading", "Loading Player Totem models...");
|
||||
sObjectMgr->LoadPlayerTotemModels();
|
||||
|
||||
|
||||
@@ -674,4 +674,7 @@ void WorldConfig::BuildConfigCache()
|
||||
SetConfigValue<uint32>(CONFIG_SCOURGEINVASION_COUNTER_THIRD, "ScourgeInvasion.CounterThird", 150);
|
||||
|
||||
SetConfigValue<std::string>(CONFIG_NEW_CHAR_STRING, "PlayerStart.String", "");
|
||||
|
||||
// Achievement
|
||||
SetConfigValue<uint32>(CONFIG_ACHIEVEMENT_REALM_FIRST_KILL_WINDOW, "Achievement.RealmFirstKillWindow", 60);
|
||||
}
|
||||
|
||||
@@ -489,6 +489,7 @@ enum ServerConfigs
|
||||
RATE_MISS_CHANCE_MULTIPLIER_TARGET_PLAYER,
|
||||
CONFIG_NEW_CHAR_STRING,
|
||||
CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS,
|
||||
CONFIG_ACHIEVEMENT_REALM_FIRST_KILL_WINDOW,
|
||||
|
||||
MAX_NUM_SERVER_CONFIGS
|
||||
};
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#include "Chat.h"
|
||||
#include "CommandScript.h"
|
||||
#include "Language.h"
|
||||
#include "GameTime.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Player.h"
|
||||
|
||||
using namespace Acore::ChatCommands;
|
||||
|
||||
@@ -35,7 +38,8 @@ public:
|
||||
{ "stop", HandleBattlefieldEnd, SEC_ADMINISTRATOR, Console::Yes },
|
||||
{ "switch", HandleBattlefieldSwitch, SEC_ADMINISTRATOR, Console::Yes },
|
||||
{ "timer", HandleBattlefieldTimer, SEC_ADMINISTRATOR, Console::Yes },
|
||||
{ "enable", HandleBattlefieldEnable, SEC_ADMINISTRATOR, Console::Yes }
|
||||
{ "enable", HandleBattlefieldEnable, SEC_ADMINISTRATOR, Console::Yes },
|
||||
{ "queue", HandleBattlefieldQueue, SEC_GAMEMASTER, Console::Yes }
|
||||
};
|
||||
static ChatCommandTable commandTable =
|
||||
{
|
||||
@@ -167,6 +171,63 @@ public:
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleBattlefieldQueue(ChatHandler* handler, uint32 battleId)
|
||||
{
|
||||
Battlefield* bf = sBattlefieldMgr->GetBattlefieldByBattleId(battleId);
|
||||
|
||||
if (!bf)
|
||||
{
|
||||
handler->SendErrorMessage(LANG_BF_NOT_FOUND, battleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
handler->PSendSysMessage(bf->IsWarTime() ? LANG_BF_QUEUE_HDR_WAR : LANG_BF_QUEUE_HDR_WAIT,
|
||||
battleId, bf->GetTimer() / IN_MILLISECONDS);
|
||||
|
||||
static char const* teamNames[PVP_TEAMS_COUNT] = { "Alliance", "Horde" };
|
||||
|
||||
std::string offlineSuffix = handler->GetAcoreString(LANG_OFFLINE);
|
||||
|
||||
auto nameOf = [offlineSuffix](ObjectGuid guid) -> std::string
|
||||
{
|
||||
if (Player* p = ObjectAccessor::FindPlayer(guid))
|
||||
return p->GetName();
|
||||
return std::to_string(guid.GetCounter()) + offlineSuffix;
|
||||
};
|
||||
|
||||
for (uint8 i = 0; i < PVP_TEAMS_COUNT; ++i)
|
||||
{
|
||||
TeamId team = TeamId(i);
|
||||
|
||||
GuidUnorderedSet const& inQueue = bf->GetPlayersQueueSet(team);
|
||||
PlayerTimerMap const& invited = bf->GetInvitedPlayersMap(team);
|
||||
GuidUnorderedSet const& inWar = bf->GetPlayersInWarSet(team);
|
||||
|
||||
handler->PSendSysMessage(LANG_BF_QUEUE_TEAM_HDR,
|
||||
teamNames[i],
|
||||
static_cast<uint32>(inQueue.size()),
|
||||
static_cast<uint32>(invited.size()),
|
||||
static_cast<uint32>(inWar.size()));
|
||||
|
||||
for (ObjectGuid const& guid : inQueue)
|
||||
handler->PSendSysMessage(LANG_BF_QUEUE_PLAYER_QUEUE, nameOf(guid));
|
||||
|
||||
SystemTimePoint now = GameTime::GetSystemTime();
|
||||
for (auto const& [guid, expiry] : invited)
|
||||
{
|
||||
SystemTimePoint expiryPoint = std::chrono::system_clock::from_time_t(expiry);
|
||||
int32 secsLeft = std::max(int32(0), static_cast<int32>(
|
||||
std::chrono::duration_cast<Seconds>(expiryPoint - now).count()));
|
||||
handler->PSendSysMessage(LANG_BF_QUEUE_PLAYER_INVITED, nameOf(guid), secsLeft);
|
||||
}
|
||||
|
||||
for (ObjectGuid const& guid : inWar)
|
||||
handler->PSendSysMessage(LANG_BF_QUEUE_PLAYER_WAR, nameOf(guid));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
void AddSC_bf_commandscript()
|
||||
|
||||
@@ -620,16 +620,38 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleNpcInfoCommand(ChatHandler* handler)
|
||||
static bool HandleNpcInfoCommand(ChatHandler* handler, Optional<CreatureSpawnId> spawnIdArg)
|
||||
{
|
||||
Creature* target = handler->getSelectedCreature();
|
||||
ObjectGuid::LowType lowGuid = 0;
|
||||
|
||||
if (!target)
|
||||
if (spawnIdArg)
|
||||
lowGuid = *spawnIdArg;
|
||||
|
||||
if (!target && lowGuid)
|
||||
target = handler->GetCreatureFromPlayerMapByDbGuid(lowGuid);
|
||||
|
||||
if (target)
|
||||
return HandleNpcInfoCommandShowCreature(handler, target);
|
||||
|
||||
if (!lowGuid)
|
||||
{
|
||||
handler->SendErrorMessage(LANG_SELECT_CREATURE);
|
||||
return false;
|
||||
}
|
||||
|
||||
CreatureData const* cData = sObjectMgr->GetCreatureData(lowGuid);
|
||||
if (!cData)
|
||||
cData = sObjectMgr->LoadCreatureDataFromDB(lowGuid);
|
||||
if (cData)
|
||||
return HandleNpcInfoCommandShowFromDB(handler, lowGuid, cData);
|
||||
|
||||
handler->SendErrorMessage(LANG_COMMAND_CREATGUIDNOTFOUND, lowGuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool HandleNpcInfoCommandShowCreature(ChatHandler* handler, Creature* target)
|
||||
{
|
||||
CreatureTemplate const* cInfo = target->GetCreatureTemplate();
|
||||
uint32 faction = target->GetFaction();
|
||||
uint32 npcflags = target->GetNpcFlags();
|
||||
@@ -695,6 +717,31 @@ public:
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleNpcInfoCommandShowFromDB(ChatHandler* handler, ObjectGuid::LowType lowGuid, CreatureData const* cData)
|
||||
{
|
||||
CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(cData->id1);
|
||||
if (!cInfo)
|
||||
{
|
||||
handler->SendErrorMessage(LANG_COMMAND_CREATGUIDNOTFOUND, lowGuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
handler->PSendSysMessage("(Not in world - showing DB data)");
|
||||
uint32 scriptId = cData->ScriptId ? cData->ScriptId : cInfo->ScriptID;
|
||||
handler->PSendSysMessage(LANG_NPCINFO_CHAR, lowGuid, ObjectGuid::Create<HighGuid::Unit>(cData->id1, lowGuid).ToString(), cData->id1,
|
||||
cData->id2, cData->id3, cData->displayid, cData->displayid, cInfo->faction,
|
||||
cData->npcflag);
|
||||
handler->PSendSysMessage(LANG_NPCINFO_PHASEMASK, cData->phaseMask);
|
||||
handler->PSendSysMessage(LANG_NPCINFO_POSITION, cData->posX, cData->posY, cData->posZ);
|
||||
handler->PSendSysMessage("Map: {} Spawn time: {}s", cData->mapid,
|
||||
cData->spawntimesecs);
|
||||
handler->PSendSysMessage(LANG_NPCINFO_EQUIPMENT, cData->equipmentId, cData->equipmentId);
|
||||
handler->PSendSysMessage(LANG_NPCINFO_AIINFO, cInfo->AIName,
|
||||
sObjectMgr->GetScriptName(scriptId));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleNpcGuidCommand(ChatHandler* handler)
|
||||
{
|
||||
Creature* target = handler->getSelectedCreature();
|
||||
|
||||
@@ -40,6 +40,8 @@ enum GameObjects
|
||||
GO_DOOR_LEVER_2 = 101833,
|
||||
GO_DOOR_LEVER_3 = 101834,
|
||||
GO_CANNON = 16398,
|
||||
GO_FOUNDRY_DOOR = 16399,
|
||||
GO_MAST_ROOM_DOOR = 16400,
|
||||
};
|
||||
|
||||
template <class AI, class T>
|
||||
|
||||
@@ -46,6 +46,8 @@ public:
|
||||
case GO_DOOR_LEVER_2:
|
||||
case GO_DOOR_LEVER_3:
|
||||
case GO_CANNON:
|
||||
case GO_FOUNDRY_DOOR:
|
||||
case GO_MAST_ROOM_DOOR:
|
||||
gameobject->AllowSaveToDB(true);
|
||||
break;
|
||||
case GO_FACTORY_DOOR:
|
||||
@@ -57,9 +59,8 @@ public:
|
||||
case GO_IRON_CLAD_DOOR:
|
||||
gameobject->AllowSaveToDB(true);
|
||||
if (GetStoredGameObjectState(gameobject->GetSpawnId()) == GO_STATE_ACTIVE)
|
||||
{
|
||||
gameobject->DespawnOrUnsummon();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ void AddSC_elwynn_forest();
|
||||
void AddSC_eversong_woods();
|
||||
void AddSC_ghostlands();
|
||||
void AddSC_hinterlands();
|
||||
//void AddSC_ironforge();
|
||||
void AddSC_ironforge();
|
||||
void AddSC_isle_of_queldanas();
|
||||
void AddSC_redridge_mountains();
|
||||
void AddSC_silverpine_forest();
|
||||
@@ -305,7 +305,7 @@ void AddEasternKingdomsScripts()
|
||||
AddSC_eversong_woods();
|
||||
AddSC_ghostlands();
|
||||
AddSC_hinterlands();
|
||||
// AddSC_ironforge();
|
||||
AddSC_ironforge();
|
||||
AddSC_isle_of_queldanas();
|
||||
AddSC_redridge_mountains();
|
||||
AddSC_silverpine_forest();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "CreatureScript.h"
|
||||
#include "ScriptedCreature.h"
|
||||
|
||||
@@ -215,8 +216,29 @@ private:
|
||||
float _facing;
|
||||
};
|
||||
|
||||
// 16802 - Lor'themar Theron
|
||||
/// @todo add abilities/timers
|
||||
struct npc_lor_themar_theron : public ScriptedAI
|
||||
{
|
||||
npc_lor_themar_theron(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
DoRewardPlayersInArea();
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 /*diff*/) override
|
||||
{
|
||||
if (!UpdateVictim())
|
||||
return;
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
};
|
||||
|
||||
void AddSC_eversong_woods()
|
||||
{
|
||||
RegisterCreatureAI(npc_partygoer_pather);
|
||||
RegisterCreatureAI(npc_partygoer);
|
||||
RegisterCreatureAI(npc_lor_themar_theron);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore 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 Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 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 Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "CreatureScript.h"
|
||||
#include "ScriptedCreature.h"
|
||||
|
||||
// 2784 - King Magni Bronzebeard
|
||||
enum KingMagniBronzebeard
|
||||
{
|
||||
SOUND_AGGRO = 5896,
|
||||
SPELL_AVATAR = 19135,
|
||||
SPELL_KNOCK_AWAY = 20686,
|
||||
SPELL_STORM_BOLT = 20685
|
||||
};
|
||||
|
||||
struct npc_king_magni_bronzebeard : public ScriptedAI
|
||||
{
|
||||
npc_king_magni_bronzebeard(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
void JustEngagedWith(Unit* /*who*/) override
|
||||
{
|
||||
me->PlayDirectSound(SOUND_AGGRO);
|
||||
|
||||
ScheduleTimedEvent(5s, 7s, [&]
|
||||
{
|
||||
DoCastSelf(SPELL_AVATAR);
|
||||
}, 25s, 30s);
|
||||
|
||||
ScheduleTimedEvent(8s, 10s, [&]
|
||||
{
|
||||
DoCastVictim(SPELL_KNOCK_AWAY);
|
||||
}, 20s, 30s);
|
||||
|
||||
ScheduleTimedEvent(12s, 15s, [&]
|
||||
{
|
||||
DoCastRandomTarget(SPELL_STORM_BOLT);
|
||||
}, 15s, 20s);
|
||||
}
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
DoRewardPlayersInArea();
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
if (!UpdateVictim())
|
||||
return;
|
||||
|
||||
scheduler.Update(diff,
|
||||
std::bind(&ScriptedAI::DoMeleeAttackIfReady, this));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
void AddSC_ironforge()
|
||||
{
|
||||
RegisterCreatureAI(npc_king_magni_bronzebeard);
|
||||
}
|
||||
@@ -464,13 +464,26 @@ enum KingVarianWrynn : uint32
|
||||
// Deathknight Starting Zone End
|
||||
QUEST_WHERE_KINGS_WALK = 13188,
|
||||
};
|
||||
|
||||
class npc_king_varian_wrynn : public CreatureScript
|
||||
// 29611 - King Varian Wryn
|
||||
/// @todo add abilities/timers
|
||||
struct npc_king_varian_wrynn : public ScriptedAI
|
||||
{
|
||||
public:
|
||||
npc_king_varian_wrynn() : CreatureScript("npc_king_varian_wrynn") { }
|
||||
npc_king_varian_wrynn(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
bool OnQuestReward(Player* player, Creature* /*creature*/, Quest const* quest, uint32 /*item*/) override
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
DoRewardPlayersInArea();
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 /*diff*/) override
|
||||
{
|
||||
if (!UpdateVictim())
|
||||
return;
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
|
||||
bool OnQuestReward(Player* player, Creature* /*creature*/, Quest const* quest, uint32 /*item*/)
|
||||
{
|
||||
|
||||
if (quest->GetQuestId() == QUEST_WHERE_KINGS_WALK)
|
||||
@@ -486,5 +499,6 @@ void AddSC_stormwind_city()
|
||||
new npc_tyrion_spybot();
|
||||
new npc_lord_gregor_lescovar();
|
||||
new npc_marzon_silent_blade();
|
||||
new npc_king_varian_wrynn();
|
||||
RegisterCreatureAI(npc_king_varian_wrynn);
|
||||
|
||||
}
|
||||
|
||||
@@ -118,6 +118,11 @@ public:
|
||||
_events.ScheduleEvent(EVENT_MULTI_SHOT, 10s);
|
||||
}
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
DoRewardPlayersInArea();
|
||||
}
|
||||
|
||||
void SetGUID(ObjectGuid const& guid, int32 type) override
|
||||
{
|
||||
if (type == GUID_EVENT_INVOKER)
|
||||
|
||||
+36
-33
@@ -1402,7 +1402,10 @@ enum chromie
|
||||
ITEM_ARCANE_DISRUPTOR = 37888,
|
||||
QUEST_DISPELLING_ILLUSIONS = 13149,
|
||||
QUEST_A_ROYAL_ESCORT = 13151,
|
||||
SPELL_SUMMON_ARCANE_DISRUPTOR = 49591
|
||||
SPELL_SUMMON_ARCANE_DISRUPTOR = 49591,
|
||||
GOSSIP_MENU_START = 9586,
|
||||
GOSSIP_MENU_ACTION_MENU_SKIP = 11277,
|
||||
GOSSIP_MENU_ACTION_INTERFERE = 9595
|
||||
};
|
||||
|
||||
class npc_cos_chromie_start : public CreatureScript
|
||||
@@ -1410,55 +1413,55 @@ class npc_cos_chromie_start : public CreatureScript
|
||||
public:
|
||||
npc_cos_chromie_start() : CreatureScript("npc_cos_chromie_start") { }
|
||||
|
||||
bool OnQuestAccept(Player*, Creature* creature, const Quest* pQuest)
|
||||
bool OnQuestAccept(Player* /*player*/, Creature* creature, const Quest* quest) override
|
||||
{
|
||||
if (pQuest->GetQuestId() == QUEST_DISPELLING_ILLUSIONS)
|
||||
{
|
||||
if (InstanceScript* pInstance = creature->GetInstanceScript())
|
||||
{
|
||||
pInstance->SetData(DATA_SHOW_CRATES, 1);
|
||||
}
|
||||
}
|
||||
if (quest->GetQuestId() == QUEST_DISPELLING_ILLUSIONS)
|
||||
if (InstanceScript* instance = creature->GetInstanceScript())
|
||||
instance->SetData(DATA_SHOW_CRATES, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OnGossipSelect(Player* player, Creature* creature, uint32 /*sender*/, uint32 /*action*/)
|
||||
bool OnGossipSelect(Player* player, Creature* creature, uint32 /*sender*/, uint32 /*action*/) override
|
||||
{
|
||||
// final menu id, show crates if hidden and add item if missing
|
||||
if (player->PlayerTalkClass->GetGossipMenu().GetMenuId() == 9595)
|
||||
switch (player->PlayerTalkClass->GetGossipMenu().GetMenuId())
|
||||
{
|
||||
if (InstanceScript* pInstance = creature->GetInstanceScript())
|
||||
case GOSSIP_MENU_START:
|
||||
{
|
||||
if (pInstance->GetData(DATA_ARTHAS_EVENT) == COS_PROGRESS_NOT_STARTED)
|
||||
{
|
||||
pInstance->SetData(DATA_SHOW_CRATES, 1);
|
||||
}
|
||||
}
|
||||
if (InstanceScript* instance = creature->GetInstanceScript())
|
||||
if (instance->GetData(DATA_ARTHAS_EVENT) == COS_PROGRESS_NOT_STARTED)
|
||||
instance->SetData(DATA_SHOW_CRATES, 1);
|
||||
|
||||
if (!player->HasItemCount(ITEM_ARCANE_DISRUPTOR))
|
||||
{
|
||||
creature->CastSpell(player, SPELL_SUMMON_ARCANE_DISRUPTOR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Skip Event
|
||||
else if (player->PlayerTalkClass->GetGossipMenu().GetMenuId() == 11277)
|
||||
{
|
||||
if (InstanceScript* pInstance = creature->GetInstanceScript())
|
||||
case GOSSIP_MENU_ACTION_INTERFERE:
|
||||
{
|
||||
if (pInstance->GetData(DATA_ARTHAS_EVENT) == COS_PROGRESS_NOT_STARTED)
|
||||
if (!player->HasItemCount(ITEM_ARCANE_DISRUPTOR))
|
||||
creature->CastSpell(player, SPELL_SUMMON_ARCANE_DISRUPTOR);
|
||||
|
||||
break;
|
||||
}
|
||||
// Since 3.3.3: "Players may now skip the initial introduction dialog to this dungeon once they have completed it at least once."
|
||||
case GOSSIP_MENU_ACTION_MENU_SKIP:
|
||||
{
|
||||
if (InstanceScript* instance = creature->GetInstanceScript())
|
||||
{
|
||||
pInstance->SetData(DATA_ARTHAS_EVENT, COS_PROGRESS_FINISHED_INTRO);
|
||||
if (Creature* arthas = ObjectAccessor::GetCreature(*creature, pInstance->GetGuidData(DATA_ARTHAS)))
|
||||
if (instance->GetData(DATA_ARTHAS_EVENT) == COS_PROGRESS_NOT_STARTED)
|
||||
{
|
||||
arthas->AI()->Reset();
|
||||
instance->SetData(DATA_ARTHAS_EVENT, COS_PROGRESS_FINISHED_INTRO);
|
||||
|
||||
if (Creature* arthas = ObjectAccessor::GetCreature(*creature, instance->GetGuidData(DATA_ARTHAS)))
|
||||
arthas->AI()->Reset();
|
||||
}
|
||||
|
||||
player->NearTeleportTo(LeaderIntroPos2.GetPositionX(), LeaderIntroPos2.GetPositionY(), LeaderIntroPos2.GetPositionZ(), LeaderIntroPos2.GetOrientation());
|
||||
}
|
||||
player->NearTeleportTo(LeaderIntroPos2.GetPositionX(), LeaderIntroPos2.GetPositionY(), LeaderIntroPos2.GetPositionZ(), LeaderIntroPos2.GetOrientation());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// return false to display last windows
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1483,7 +1486,7 @@ public:
|
||||
if (!creature->GetInstanceScript() || creature->GetInstanceScript()->GetData(DATA_ARTHAS_EVENT) != COS_PROGRESS_CRATES_FOUND)
|
||||
return true;
|
||||
|
||||
// We can start event:)
|
||||
// "Well, you're not going to sign recruitment papers or anything, but you are going to fight alongside him. ..."
|
||||
if (player->PlayerTalkClass->GetGossipMenu().GetMenuId() == 9612)
|
||||
creature->GetInstanceScript()->SetData(DATA_ARTHAS_EVENT, COS_PROGRESS_START_INTRO);
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ enum Data
|
||||
DATA_START_WAVES,
|
||||
DATA_SHOW_INFINITE_TIMER,
|
||||
DATA_ARTHAS_REPOSITION,
|
||||
|
||||
DATA_INTRO_EVENT_FINISHED,
|
||||
};
|
||||
|
||||
enum Data64
|
||||
|
||||
+2
@@ -188,6 +188,8 @@ public:
|
||||
return _encounterState;
|
||||
case DATA_GUARDIANTIME_EVENT:
|
||||
return _guardianTimer;
|
||||
case DATA_INTRO_EVENT_FINISHED:
|
||||
return _encounterState >= COS_PROGRESS_FINISHED_INTRO ? 1 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ void AddSC_orgrimmar();
|
||||
void AddSC_silithus();
|
||||
void AddSC_stonetalon_mountains();
|
||||
void AddSC_tanaris();
|
||||
void AddSC_teldrassil();
|
||||
void AddSC_the_barrens();
|
||||
void AddSC_thousand_needles();
|
||||
void AddSC_thunder_bluff();
|
||||
@@ -170,6 +171,7 @@ void AddKalimdorScripts()
|
||||
AddSC_silithus();
|
||||
AddSC_stonetalon_mountains();
|
||||
AddSC_tanaris();
|
||||
AddSC_teldrassil();
|
||||
AddSC_the_barrens();
|
||||
AddSC_thousand_needles();
|
||||
AddSC_thunder_bluff();
|
||||
|
||||
@@ -559,6 +559,31 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// 7999 - Tyrande Whisperwind
|
||||
/// @todo add abilities/timers
|
||||
struct npc_prophet_velen : public ScriptedAI
|
||||
{
|
||||
npc_prophet_velen(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
me->setActive(true);
|
||||
}
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
DoRewardPlayersInArea();
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 /*diff*/) override
|
||||
{
|
||||
if (!UpdateVictim())
|
||||
return;
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
};
|
||||
|
||||
void AddSC_azuremyst_isle()
|
||||
{
|
||||
new npc_draenei_survivor();
|
||||
@@ -569,4 +594,5 @@ void AddSC_azuremyst_isle()
|
||||
new npc_stillpine_capitive();
|
||||
new go_bristlelimb_cage();
|
||||
RegisterSpellScript(spell_inoculate_nestlewood_owlkin);
|
||||
RegisterCreatureAI(npc_prophet_velen);
|
||||
}
|
||||
|
||||
@@ -234,6 +234,11 @@ public:
|
||||
|
||||
void JustEngagedWith(Unit* /*who*/) override { }
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
DoRewardPlayersInArea();
|
||||
}
|
||||
|
||||
void DoAction(int32 action) override
|
||||
{
|
||||
if (action == ACTION_START_TALKING)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore 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 Affero General Public License as published by the
|
||||
* Free Software Foundation; either version 3 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 Affero General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "CreatureScript.h"
|
||||
#include "ScriptedCreature.h"
|
||||
|
||||
// 7999 - Tyrande Whisperwind
|
||||
/// @todo add abilities/timers
|
||||
struct npc_tyrande_whisperwind : public ScriptedAI
|
||||
{
|
||||
npc_tyrande_whisperwind(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
DoRewardPlayersInArea();
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 /*diff*/) override
|
||||
{
|
||||
if (!UpdateVictim())
|
||||
return;
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
}
|
||||
};
|
||||
|
||||
void AddSC_teldrassil()
|
||||
{
|
||||
RegisterCreatureAI(npc_tyrande_whisperwind);
|
||||
}
|
||||
@@ -19,10 +19,6 @@
|
||||
#include "Player.h"
|
||||
#include "ScriptedCreature.h"
|
||||
|
||||
/*#####
|
||||
# Support for Quest 925: Cairne's Hoofprint
|
||||
######*/
|
||||
|
||||
// NPC 3057: Cairne Bloodhoof <High Chieftain>
|
||||
enum CairneBloodhoof
|
||||
{
|
||||
@@ -34,7 +30,7 @@ enum CairneBloodhoof
|
||||
SPELL_CAIRNES_HOOFPRINT = 23123
|
||||
};
|
||||
|
||||
// @todo verify abilities/timers
|
||||
/// @todo verify abilities/timers
|
||||
class npc_cairne_bloodhoof : public CreatureScript
|
||||
{
|
||||
public:
|
||||
@@ -61,12 +57,15 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
DoRewardPlayersInArea();
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
if (!UpdateVictim())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_berserkerChargeTimer <= diff)
|
||||
{
|
||||
|
||||
@@ -121,23 +121,30 @@ enum Misc
|
||||
POINT_CHANNEL_STEELBREAKER = 1
|
||||
};
|
||||
|
||||
static uint8 CountAliveBosses(InstanceScript* instance)
|
||||
{
|
||||
uint8 count = 0;
|
||||
if (!instance)
|
||||
return count;
|
||||
|
||||
for (uint8 i = 0; i < 3; ++i)
|
||||
if (Creature* boss = instance->GetCreature(DATA_STEELBREAKER + i))
|
||||
if (boss->IsAlive())
|
||||
++count;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
bool IsEncounterComplete(InstanceScript* pInstance, Creature* me)
|
||||
{
|
||||
if (!pInstance || !me)
|
||||
return false;
|
||||
|
||||
for (uint8 i = 0; i < 3; ++i)
|
||||
{
|
||||
if (Creature* boss = pInstance->GetCreature(DATA_STEELBREAKER + i))
|
||||
{
|
||||
if (boss->IsAlive())
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
if (!pInstance->GetCreature(DATA_STEELBREAKER + i))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
return CountAliveBosses(pInstance) == 0;
|
||||
}
|
||||
|
||||
void RespawnAssemblyOfIron(InstanceScript* pInstance, Creature* me)
|
||||
@@ -550,6 +557,7 @@ struct boss_stormcaller_brundir : public ScriptedAI
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
SetInvincibility(false);
|
||||
me->SetLootMode(0);
|
||||
RespawnAssemblyOfIron(pInstance, me);
|
||||
|
||||
@@ -667,6 +675,26 @@ struct boss_stormcaller_brundir : public ScriptedAI
|
||||
me->CastSpell(me, SPELL_LIGHTNING_CHANNEL_PRE, true);
|
||||
}
|
||||
|
||||
void OnSpellCast(SpellInfo const* spellInfo) override
|
||||
{
|
||||
// When Overload begins, prevent Brundir from dying unless he is the last boss alive
|
||||
if (spellInfo->Id == SPELL_OVERLOAD && CountAliveBosses(pInstance) > 1)
|
||||
SetInvincibility(true);
|
||||
}
|
||||
|
||||
void OnChannelFinished(SpellInfo const* spellInfo) override
|
||||
{
|
||||
if (spellInfo->Id == SPELL_OVERLOAD)
|
||||
SetInvincibility(false);
|
||||
}
|
||||
|
||||
void OnSpellFailed(SpellInfo const* spellInfo) override
|
||||
{
|
||||
// Also lift invincibility if the channel is somehow interrupted
|
||||
if (spellInfo->Id == SPELL_OVERLOAD)
|
||||
SetInvincibility(false);
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
if (!me->IsInCombat() && me->GetReactState() == REACT_AGGRESSIVE)
|
||||
|
||||
@@ -198,8 +198,8 @@ struct boss_high_astromancer_solarian : public BossAI
|
||||
});
|
||||
}).Schedule(23s, [this](TaskContext)
|
||||
{
|
||||
me->GetThreatMgr().ClearAllThreat();
|
||||
me->SetReactState(REACT_AGGRESSIVE);
|
||||
DoResetThreatList();
|
||||
summons.DoForAllSummons([&](WorldObject* summon)
|
||||
{
|
||||
if (Creature* light = summon->ToCreature())
|
||||
|
||||
@@ -171,7 +171,7 @@ struct npc_pet_mage_mirror_image : CasterAI
|
||||
{
|
||||
Unit* selection = owner->ToPlayer()->GetSelectedUnit();
|
||||
|
||||
if (selection)
|
||||
if (selection && me->CanSeeOrDetect(selection))
|
||||
{
|
||||
me->GetThreatMgr().ResetAllThreat();
|
||||
me->AddThreat(selection, 1000000.0f);
|
||||
@@ -208,10 +208,10 @@ struct npc_pet_mage_mirror_image : CasterAI
|
||||
|
||||
if (checktarget >= 1000)
|
||||
{
|
||||
if (me->GetVictim()->HasBreakableByDamageCrowdControlAura() || !me->GetVictim()->IsAlive())
|
||||
if (!me->GetVictim()->IsAlive() || me->GetVictim()->HasBreakableByDamageCrowdControlAura() || !me->CanSeeOrDetect(me->GetVictim()))
|
||||
{
|
||||
MySelectNextTarget();
|
||||
me->InterruptNonMeleeSpells(true); // Stop casting if target is CC or not Alive.
|
||||
me->InterruptNonMeleeSpells(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +492,7 @@ class spell_hun_chimera_shot : public SpellScript
|
||||
|
||||
// Amount of one aura tick
|
||||
basePoint = int32(CalculatePct(unitTarget->GetMaxPower(POWER_MANA), aurEff->GetAmount()));
|
||||
int32 casterBasePoint = aurEff->GetAmount() * unitTarget->GetMaxPower(POWER_MANA) / 50; /// @todo: Caster uses unitTarget?
|
||||
int32 casterBasePoint = aurEff->GetAmount() * caster->GetMaxPower(POWER_MANA) / 50;
|
||||
if (basePoint > casterBasePoint)
|
||||
basePoint = casterBasePoint;
|
||||
ApplyPct(basePoint, TickCount * 60);
|
||||
|
||||
@@ -1219,7 +1219,7 @@ class spell_mage_glyph_of_polymorph : public AuraScript
|
||||
return;
|
||||
|
||||
// Remove DoTs from target
|
||||
target->RemoveAurasByType(SPELL_AURA_PERIODIC_DAMAGE, ObjectGuid::Empty, nullptr, true);
|
||||
target->RemoveAurasByType(SPELL_AURA_PERIODIC_DAMAGE, ObjectGuid::Empty, target->GetAura(32409), true); // SW:D shall not be removed.
|
||||
target->RemoveAurasByType(SPELL_AURA_PERIODIC_DAMAGE_PERCENT, ObjectGuid::Empty, nullptr, true);
|
||||
target->RemoveAurasByType(SPELL_AURA_PERIODIC_LEECH, ObjectGuid::Empty, nullptr, true);
|
||||
}
|
||||
@@ -1556,6 +1556,10 @@ class spell_mage_missile_barrage_proc : public AuraScript
|
||||
{
|
||||
Unit* caster = eventInfo.GetActor();
|
||||
|
||||
// Prevent double proc for Arcane Missiles
|
||||
if (caster == eventInfo.GetActionTarget())
|
||||
return false;
|
||||
|
||||
// T8 4P bonus: chance to not consume the proc
|
||||
if (AuraEffect const* aurEff = caster->GetAuraEffect(SPELL_MAGE_T8_4P_BONUS, EFFECT_0))
|
||||
if (roll_chance_i(aurEff->GetAmount()))
|
||||
@@ -1610,6 +1614,7 @@ void AddSC_mage_spell_scripts()
|
||||
RegisterSpellScript(spell_mage_glyph_of_polymorph);
|
||||
RegisterSpellScript(spell_mage_hot_streak);
|
||||
RegisterSpellScript(spell_mage_ice_barrier);
|
||||
RegisterSpellScript(spell_mage_ice_barrier_aura);
|
||||
RegisterSpellScript(spell_mage_ice_block);
|
||||
RegisterSpellScript(spell_mage_imp_blizzard);
|
||||
RegisterSpellScript(spell_mage_imp_mana_gems);
|
||||
|
||||
@@ -130,9 +130,11 @@ enum PaladinProcSpells
|
||||
SPELL_PALADIN_SPIRITUAL_ATTUNEMENT_MANA = 31786,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_AURA = 53563,
|
||||
SPELL_PALADIN_LIGHTS_BEACON = 53651,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_FLASH = 53652,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HOLY = 53654,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HL = 53652,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_FOL = 53653,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HS = 53654,
|
||||
SPELL_PALADIN_HOLY_LIGHT_R1 = 635,
|
||||
SPELL_PALADIN_FLASH_OF_LIGHT_R1 = 19750,
|
||||
SPELL_PALADIN_GLYPH_OF_HOLY_LIGHT_HEAL = 54968,
|
||||
SPELL_PALADIN_SACRED_SHIELD = 53601,
|
||||
SPELL_PALADIN_T9_HOLY_4P_BONUS = 67191,
|
||||
@@ -1374,7 +1376,8 @@ class spell_pal_judgement_of_wisdom_mana : public AuraScript
|
||||
if (!attacker)
|
||||
return;
|
||||
|
||||
int32 bp = int32(CalculatePct(attacker->GetCreateMana(), aurEff->GetAmount()));
|
||||
SpellInfo const* spellInfo = sSpellMgr->AssertSpellInfo(SPELL_PALADIN_JUDGEMENT_OF_WISDOM_MANA);
|
||||
int32 bp = int32(CalculatePct(attacker->GetCreateMana(), spellInfo->Effects[EFFECT_0].CalcValue()));
|
||||
attacker->CastCustomSpell(attacker, SPELL_PALADIN_JUDGEMENT_OF_WISDOM_MANA, &bp, nullptr, nullptr, true, nullptr, aurEff, GetCasterGUID());
|
||||
}
|
||||
|
||||
@@ -2131,6 +2134,9 @@ private:
|
||||
};
|
||||
|
||||
// 53651 - Light's Beacon - Beacon of Light
|
||||
// Each source heal has a dedicated beacon copy spell:
|
||||
// 53652 - Holy Light, 53653 - Flash of Light, 53654 - Holy Shock
|
||||
// Reference: https://kurn.info/blog/holy-how-to-5-to-beacon-or-not-to-beacon/
|
||||
class spell_pal_light_s_beacon : public AuraScript
|
||||
{
|
||||
PrepareAuraScript(spell_pal_light_s_beacon);
|
||||
@@ -2140,9 +2146,11 @@ class spell_pal_light_s_beacon : public AuraScript
|
||||
return ValidateSpellInfo(
|
||||
{
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_AURA,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_FLASH,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HOLY,
|
||||
SPELL_PALADIN_HOLY_LIGHT_R1
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HL,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_FOL,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HS,
|
||||
SPELL_PALADIN_HOLY_LIGHT_R1,
|
||||
SPELL_PALADIN_FLASH_OF_LIGHT_R1
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2166,9 +2174,13 @@ class spell_pal_light_s_beacon : public AuraScript
|
||||
if (!healInfo || !healInfo->GetHeal())
|
||||
return;
|
||||
|
||||
// Holy Light heals for 100%, Flash of Light heals for 50%
|
||||
uint32 healSpellId = procSpell->IsRankOf(sSpellMgr->AssertSpellInfo(SPELL_PALADIN_HOLY_LIGHT_R1)) ?
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_FLASH : SPELL_PALADIN_BEACON_OF_LIGHT_HOLY;
|
||||
uint32 healSpellId;
|
||||
if (procSpell->IsRankOf(sSpellMgr->AssertSpellInfo(SPELL_PALADIN_HOLY_LIGHT_R1)))
|
||||
healSpellId = SPELL_PALADIN_BEACON_OF_LIGHT_HL;
|
||||
else if (procSpell->IsRankOf(sSpellMgr->AssertSpellInfo(SPELL_PALADIN_FLASH_OF_LIGHT_R1)))
|
||||
healSpellId = SPELL_PALADIN_BEACON_OF_LIGHT_FOL;
|
||||
else
|
||||
healSpellId = SPELL_PALADIN_BEACON_OF_LIGHT_HS;
|
||||
|
||||
// Use heal amount before target-specific modifiers to avoid copying them
|
||||
uint32 healAmount = healInfo->GetHealBeforeTakenMods();
|
||||
|
||||
@@ -884,6 +884,10 @@ class spell_warr_retaliation : public AuraScript
|
||||
|
||||
bool CheckProc(ProcEventInfo& eventInfo)
|
||||
{
|
||||
// Prevent counterattacking yourself on activation
|
||||
if (eventInfo.GetActor() == eventInfo.GetActionTarget())
|
||||
return false;
|
||||
|
||||
// check attack comes not from behind and warrior is not stunned
|
||||
return eventInfo.GetActionTarget()->isInFront(eventInfo.GetActor(), float(M_PI)) && !GetTarget()->HasUnitState(UNIT_STATE_STUNNED);
|
||||
}
|
||||
|
||||
@@ -529,7 +529,7 @@ enum SpellAttr4 : uint32
|
||||
SPELL_ATTR4_NO_PARTIAL_IMMUNITY = 0x00000800, // TITLE Unknown attribute 11@Attr4
|
||||
SPELL_ATTR4_AURA_IS_BUFF = 0x00001000, // TITLE Unknown attribute 12@Attr4
|
||||
SPELL_ATTR4_DO_NOT_LOG_CASTER = 0x00002000, // TITLE Unknown attribute 13@Attr4
|
||||
SPELL_ATTR4_REACTIVE_DAMAGE_PROC = 0x00004000, // TITLE Damage does not break auras
|
||||
SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS = 0x00004000, // TITLE Damage does not break auras
|
||||
SPELL_ATTR4_NOT_IN_SPELLBOOK = 0x00008000, // TITLE Unknown attribute 15@Attr4
|
||||
SPELL_ATTR4_NOT_IN_ARENA_OR_RATED_BATTLEGROUND = 0x00010000, // TITLE Not usable in arena DESCRIPTION Makes spell unusable despite CD <= 10min
|
||||
SPELL_ATTR4_IGNORE_DEFAULT_ARENA_RESTRICTIONS = 0x00020000, // TITLE Usable in arena DESCRIPTION Makes spell usable despite CD > 10min
|
||||
|
||||
@@ -688,7 +688,7 @@ AC_API_EXPORT EnumText EnumUtils<SpellAttr4>::ToString(SpellAttr4 value)
|
||||
case SPELL_ATTR4_NO_PARTIAL_IMMUNITY: return { "SPELL_ATTR4_NO_PARTIAL_IMMUNITY", "Unknown attribute 11@Attr4", "" };
|
||||
case SPELL_ATTR4_AURA_IS_BUFF: return { "SPELL_ATTR4_AURA_IS_BUFF", "Unknown attribute 12@Attr4", "" };
|
||||
case SPELL_ATTR4_DO_NOT_LOG_CASTER: return { "SPELL_ATTR4_DO_NOT_LOG_CASTER", "Unknown attribute 13@Attr4", "" };
|
||||
case SPELL_ATTR4_REACTIVE_DAMAGE_PROC: return { "SPELL_ATTR4_REACTIVE_DAMAGE_PROC", "Damage does not break auras", "" };
|
||||
case SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS: return { "SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS", "Damage does not break auras", "" };
|
||||
case SPELL_ATTR4_NOT_IN_SPELLBOOK: return { "SPELL_ATTR4_NOT_IN_SPELLBOOK", "Unknown attribute 15@Attr4", "" };
|
||||
case SPELL_ATTR4_NOT_IN_ARENA_OR_RATED_BATTLEGROUND: return { "SPELL_ATTR4_NOT_IN_ARENA_OR_RATED_BATTLEGROUND", "Not usable in arena", "Makes spell unusable despite CD <= 10min" };
|
||||
case SPELL_ATTR4_IGNORE_DEFAULT_ARENA_RESTRICTIONS: return { "SPELL_ATTR4_IGNORE_DEFAULT_ARENA_RESTRICTIONS", "Usable in arena", "Makes spell usable despite CD > 10min" };
|
||||
@@ -732,7 +732,7 @@ AC_API_EXPORT SpellAttr4 EnumUtils<SpellAttr4>::FromIndex(std::size_t index)
|
||||
case 11: return SPELL_ATTR4_NO_PARTIAL_IMMUNITY;
|
||||
case 12: return SPELL_ATTR4_AURA_IS_BUFF;
|
||||
case 13: return SPELL_ATTR4_DO_NOT_LOG_CASTER;
|
||||
case 14: return SPELL_ATTR4_REACTIVE_DAMAGE_PROC;
|
||||
case 14: return SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS;
|
||||
case 15: return SPELL_ATTR4_NOT_IN_SPELLBOOK;
|
||||
case 16: return SPELL_ATTR4_NOT_IN_ARENA_OR_RATED_BATTLEGROUND;
|
||||
case 17: return SPELL_ATTR4_IGNORE_DEFAULT_ARENA_RESTRICTIONS;
|
||||
@@ -773,7 +773,7 @@ AC_API_EXPORT std::size_t EnumUtils<SpellAttr4>::ToIndex(SpellAttr4 value)
|
||||
case SPELL_ATTR4_NO_PARTIAL_IMMUNITY: return 11;
|
||||
case SPELL_ATTR4_AURA_IS_BUFF: return 12;
|
||||
case SPELL_ATTR4_DO_NOT_LOG_CASTER: return 13;
|
||||
case SPELL_ATTR4_REACTIVE_DAMAGE_PROC: return 14;
|
||||
case SPELL_ATTR4_DAMAGE_DOESNT_BREAK_AURAS: return 14;
|
||||
case SPELL_ATTR4_NOT_IN_SPELLBOOK: return 15;
|
||||
case SPELL_ATTR4_NOT_IN_ARENA_OR_RATED_BATTLEGROUND: return 16;
|
||||
case SPELL_ATTR4_IGNORE_DEFAULT_ARENA_RESTRICTIONS: return 17;
|
||||
|
||||
@@ -121,6 +121,26 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithEffectBasePoints(uint8 effIndex, int32 basePoints)
|
||||
{
|
||||
if (effIndex < MAX_SPELL_EFFECTS)
|
||||
_entry.EffectBasePoints[effIndex] = basePoints;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithEffectDieSides(uint8 effIndex, int32 dieSides)
|
||||
{
|
||||
if (effIndex < MAX_SPELL_EFFECTS)
|
||||
_entry.EffectDieSides[effIndex] = dieSides;
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestSpellEntryHelper& WithAttributes(uint32 attr)
|
||||
{
|
||||
_entry.Attributes = attr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellEntry const* Get() const
|
||||
{
|
||||
return &_entry;
|
||||
@@ -207,6 +227,24 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithEffectBasePoints(uint8 effIndex, int32 basePoints)
|
||||
{
|
||||
_entryHelper.WithEffectBasePoints(effIndex, basePoints);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithEffectDieSides(uint8 effIndex, int32 dieSides)
|
||||
{
|
||||
_entryHelper.WithEffectDieSides(effIndex, dieSides);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SpellInfoBuilder& WithAttributes(uint32 attr)
|
||||
{
|
||||
_entryHelper.WithAttributes(attr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Builds and returns a SpellInfo pointer
|
||||
// Note: Caller is responsible for lifetime management
|
||||
SpellInfo* Build()
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Tests for Unit::resetAttackTimer arithmetic.
|
||||
*
|
||||
* Unit::resetAttackTimer resets the swing timer after an attack lands.
|
||||
* The timer value determines when the next attack can occur.
|
||||
*
|
||||
* Old (buggy) formula:
|
||||
* int32 time = int32(GetAttackTime(type) * m_modAttackSpeedPct[type]);
|
||||
* m_attackTimer[type] = std::min(m_attackTimer[type] + time, time);
|
||||
*
|
||||
* New (fixed) formula:
|
||||
* m_attackTimer[type] = int32(GetAttackTime(type) * m_modAttackSpeedPct[type]);
|
||||
*
|
||||
* The old formula carried forward negative timer debt, allowing burst
|
||||
* attacks after lag spikes or parry-haste timer reductions.
|
||||
*/
|
||||
|
||||
namespace
|
||||
{
|
||||
// Simulates the old (buggy) resetAttackTimer formula
|
||||
int32_t OldResetFormula(int32_t currentTimer, int32_t fullTime)
|
||||
{
|
||||
return std::min(currentTimer + fullTime, fullTime);
|
||||
}
|
||||
|
||||
// Simulates the new (fixed) resetAttackTimer formula
|
||||
int32_t NewResetFormula(int32_t /*currentTimer*/, int32_t fullTime)
|
||||
{
|
||||
return fullTime;
|
||||
}
|
||||
|
||||
// Calculate effective attack time: GetAttackTime(type) * m_modAttackSpeedPct[type]
|
||||
int32_t CalcFullTime(uint32_t baseAttackTime, float modSpeedPct)
|
||||
{
|
||||
return static_cast<int32_t>(baseAttackTime * modSpeedPct);
|
||||
}
|
||||
}
|
||||
|
||||
// Normal case: timer at 0 (attack just became ready)
|
||||
TEST(ResetAttackTimerTest, NormalReset_TimerAtZero)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f); // 2.0s base, no haste
|
||||
int32_t currentTimer = 0;
|
||||
|
||||
// Both formulas produce the same result when timer is exactly 0
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 2000);
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Normal case: timer slightly negative (typical single-tick overshoot)
|
||||
TEST(ResetAttackTimerTest, NormalReset_SmallNegativeTimer)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f);
|
||||
int32_t currentTimer = -50; // 50ms overshoot
|
||||
|
||||
// Old formula: min(-50 + 2000, 2000) = min(1950, 2000) = 1950
|
||||
// Carries 50ms debt — next attack 50ms sooner
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 1950);
|
||||
|
||||
// New formula: always 2000 — clean reset
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Bug scenario: large negative timer from lag spike
|
||||
TEST(ResetAttackTimerTest, LagSpike_LargeNegativeTimer)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f);
|
||||
int32_t currentTimer = -3000; // 3 second lag spike
|
||||
|
||||
// Old formula: min(-3000 + 2000, 2000) = min(-1000, 2000) = -1000
|
||||
// Timer is STILL negative — attack fires immediately again!
|
||||
EXPECT_LT(OldResetFormula(currentTimer, fullTime), 0);
|
||||
|
||||
// New formula: always 2000 — no burst
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Gluth scenario: parry-haste with enrage
|
||||
TEST(ResetAttackTimerTest, GluthParryHaste_BurstAttack)
|
||||
{
|
||||
// Gluth: 1600ms base, 25% enrage haste
|
||||
// GetAttackTime returns base (1600), modSpeedPct = 0.8 (25% haste)
|
||||
int32_t fullTime = CalcFullTime(1600, 0.8f); // = 1280ms
|
||||
EXPECT_EQ(fullTime, 1280);
|
||||
|
||||
// After parry-haste reduces timer to near-zero and a lag spike
|
||||
// causes 2+ seconds of unprocessed time
|
||||
int32_t currentTimer = -2000;
|
||||
|
||||
// Old formula: min(-2000 + 1280, 1280) = min(-720, 1280) = -720
|
||||
// Timer deeply negative — immediate burst attack
|
||||
EXPECT_LT(OldResetFormula(currentTimer, fullTime), 0);
|
||||
|
||||
// New formula: 1280ms — proper cooldown before next swing
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 1280);
|
||||
}
|
||||
|
||||
// Parry-haste floor scenario: timer at minimum (20% of base)
|
||||
TEST(ResetAttackTimerTest, ParryHasteFloor_MultipleParries)
|
||||
{
|
||||
// Creature with 1600ms base, no haste
|
||||
int32_t fullTime = CalcFullTime(1600, 1.0f);
|
||||
|
||||
// Parry-haste can reduce timer to 20% of base = 320ms
|
||||
// If server tick is 200ms and timer was at 320ms, after tick: 120ms
|
||||
// Attack fires, resetAttackTimer called with timer = 120 - 200 = -80
|
||||
// (timer went from 120 to -80 during the next tick)
|
||||
int32_t currentTimer = -80;
|
||||
|
||||
// Old formula: min(-80 + 1600, 1600) = min(1520, 1600) = 1520
|
||||
// Small debt is carried — minor issue
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 1520);
|
||||
|
||||
// Now consider two rapid parries reducing timer to 320ms,
|
||||
// followed by a 500ms server hiccup
|
||||
currentTimer = 320 - 500; // = -180
|
||||
// Old formula: min(-180 + 1600, 1600) = min(1420, 1600) = 1420
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 1420);
|
||||
|
||||
// New formula: always full time
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 1600);
|
||||
}
|
||||
|
||||
// Multiple consecutive burst attacks (worst case)
|
||||
TEST(ResetAttackTimerTest, ConsecutiveBursts_OldFormulaChainAttacks)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(1600, 0.8f); // Gluth enraged: 1280ms
|
||||
int32_t timer = -2500; // Large lag spike
|
||||
|
||||
int attackCount = 0;
|
||||
// Simulate old formula: how many attacks fire before timer > 0?
|
||||
while (timer <= 0)
|
||||
{
|
||||
attackCount++;
|
||||
timer = OldResetFormula(timer, fullTime);
|
||||
}
|
||||
|
||||
// Old formula allows multiple attacks in burst
|
||||
EXPECT_GT(attackCount, 1);
|
||||
|
||||
// New formula: always exactly 1 attack then timer is positive
|
||||
timer = -2500;
|
||||
int newAttackCount = 0;
|
||||
// Only one attack fires (the one that triggered the reset)
|
||||
timer = NewResetFormula(timer, fullTime);
|
||||
if (timer > 0)
|
||||
newAttackCount = 1;
|
||||
|
||||
EXPECT_EQ(newAttackCount, 1);
|
||||
EXPECT_GT(timer, 0);
|
||||
}
|
||||
|
||||
// Haste modifier correctness
|
||||
TEST(ResetAttackTimerTest, HasteModifier_CorrectCalculation)
|
||||
{
|
||||
// No haste: 2000ms base
|
||||
EXPECT_EQ(CalcFullTime(2000, 1.0f), 2000);
|
||||
|
||||
// 25% haste (modSpeedPct = 0.8)
|
||||
EXPECT_EQ(CalcFullTime(2000, 0.8f), 1600);
|
||||
|
||||
// 50% slow (modSpeedPct = 1.5)
|
||||
EXPECT_EQ(CalcFullTime(2000, 1.5f), 3000);
|
||||
|
||||
// Enrage (25% haste on 1600ms base)
|
||||
EXPECT_EQ(CalcFullTime(1600, 0.8f), 1280);
|
||||
}
|
||||
|
||||
// Edge case: timer exactly equals negative of full time
|
||||
TEST(ResetAttackTimerTest, EdgeCase_TimerEqualsNegativeFullTime)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f);
|
||||
int32_t currentTimer = -2000;
|
||||
|
||||
// Old formula: min(-2000 + 2000, 2000) = min(0, 2000) = 0
|
||||
// Timer exactly 0 — attack is immediately ready again
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 0);
|
||||
|
||||
// New formula: 2000 — proper delay
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Edge case: positive timer (reset called before attack was ready)
|
||||
TEST(ResetAttackTimerTest, EdgeCase_PositiveTimer)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(2000, 1.0f);
|
||||
int32_t currentTimer = 500; // 500ms remaining
|
||||
|
||||
// Old formula: min(500 + 2000, 2000) = min(2500, 2000) = 2000
|
||||
// Capped at full time — correct behavior
|
||||
EXPECT_EQ(OldResetFormula(currentTimer, fullTime), 2000);
|
||||
|
||||
// New formula: same result
|
||||
EXPECT_EQ(NewResetFormula(currentTimer, fullTime), 2000);
|
||||
}
|
||||
|
||||
// Invariant: new formula always returns exactly fullTime
|
||||
TEST(ResetAttackTimerTest, Invariant_AlwaysReturnsFullTime)
|
||||
{
|
||||
int32_t fullTime = CalcFullTime(1600, 1.0f);
|
||||
|
||||
// Test a wide range of current timer values
|
||||
for (int32_t timer = -10000; timer <= 10000; timer += 100)
|
||||
EXPECT_EQ(NewResetFormula(timer, fullTime), fullTime);
|
||||
}
|
||||
@@ -1236,3 +1236,156 @@ TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_InHolidayRules)
|
||||
EXPECT_TRUE(foundMulgore) << "Darkmoon Faire Mulgore (375) should be in HolidayRules";
|
||||
EXPECT_TRUE(foundTerokkar) << "Darkmoon Faire Terokkar (376) should be in HolidayRules";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FindStartTimeForStage tests
|
||||
// ============================================================================
|
||||
|
||||
class FindStartTimeForStageTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
// Helper to create a time_t from year/month/day/hour
|
||||
static time_t MakeTime(int year, int month, int day, int hour = 0)
|
||||
{
|
||||
std::tm t = {};
|
||||
t.tm_year = year - 1900;
|
||||
t.tm_mon = month - 1;
|
||||
t.tm_mday = day;
|
||||
t.tm_hour = hour;
|
||||
t.tm_isdst = -1;
|
||||
return mktime(&t);
|
||||
}
|
||||
|
||||
// Pack two dates into an array (rest zeroed)
|
||||
void PackTwoDates(uint32_t* dates, int y1, int m1, int d1, int y2, int m2, int d2)
|
||||
{
|
||||
std::tm t1 = {};
|
||||
t1.tm_year = y1 - 1900;
|
||||
t1.tm_mon = m1 - 1;
|
||||
t1.tm_mday = d1;
|
||||
t1.tm_isdst = -1;
|
||||
mktime(&t1);
|
||||
dates[0] = HolidayDateCalculator::PackDate(t1);
|
||||
|
||||
std::tm t2 = {};
|
||||
t2.tm_year = y2 - 1900;
|
||||
t2.tm_mon = m2 - 1;
|
||||
t2.tm_mday = d2;
|
||||
t2.tm_isdst = -1;
|
||||
mktime(&t2);
|
||||
dates[1] = HolidayDateCalculator::PackDate(t2);
|
||||
|
||||
for (int i = 2; i < 26; ++i)
|
||||
dates[i] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Stage 1 (no offset): curTime before event starts -> selects first date
|
||||
TEST_F(FindStartTimeForStageTest, Stage1_BeforeStart_SelectsFirstDate)
|
||||
{
|
||||
uint32_t dates[26] = {};
|
||||
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
|
||||
|
||||
time_t curTime = MakeTime(2026, 3, 1);
|
||||
time_t stageOffset = 0;
|
||||
uint32_t stageLengthMin = 72 * 60; // 72 hours = 3 days
|
||||
|
||||
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
|
||||
EXPECT_EQ(result, MakeTime(2026, 3, 6));
|
||||
}
|
||||
|
||||
// Stage 1 (no offset): curTime during event -> selects current date
|
||||
TEST_F(FindStartTimeForStageTest, Stage1_DuringEvent_SelectsCurrentDate)
|
||||
{
|
||||
uint32_t dates[26] = {};
|
||||
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
|
||||
|
||||
time_t curTime = MakeTime(2026, 3, 7, 12); // mid-event
|
||||
time_t stageOffset = 0;
|
||||
uint32_t stageLengthMin = 72 * 60; // 3 days
|
||||
|
||||
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
|
||||
EXPECT_EQ(result, MakeTime(2026, 3, 6));
|
||||
}
|
||||
|
||||
// Stage 1 (no offset): curTime after event ends -> selects next date
|
||||
TEST_F(FindStartTimeForStageTest, Stage1_AfterEnd_SelectsNextDate)
|
||||
{
|
||||
uint32_t dates[26] = {};
|
||||
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
|
||||
|
||||
time_t curTime = MakeTime(2026, 3, 20); // well past first event
|
||||
time_t stageOffset = 0;
|
||||
uint32_t stageLengthMin = 72 * 60; // 3 days
|
||||
|
||||
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
|
||||
EXPECT_EQ(result, MakeTime(2026, 4, 3));
|
||||
}
|
||||
|
||||
// THE BUG: Stage 2 with stageOffset > 0, curTime in the window that the old
|
||||
// code would incorrectly skip (between startTime + stageLength and
|
||||
// startTime + stageOffset + stageLength). Without the fix, this would
|
||||
// return the NEXT occurrence's start instead of the current one.
|
||||
TEST_F(FindStartTimeForStageTest, Stage2_DuringLateWindow_SelectsCurrentDate)
|
||||
{
|
||||
// Simulate Darkmoon Faire:
|
||||
// Holiday starts Mar 6 (Friday, building phase)
|
||||
// Stage 1 (building): 72 hours = 3 days (Mar 6-9)
|
||||
// Stage 2 (active): 168 hours = 7 days (Mar 9-16)
|
||||
// Total holiday: Mar 6 - Mar 16 (10 days)
|
||||
// Next occurrence: Apr 3
|
||||
uint32_t dates[26] = {};
|
||||
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
|
||||
|
||||
time_t stageOffset = 72 * 3600; // Stage 1 = 72 hours in seconds
|
||||
uint32_t stageLengthMin = 168 * 60; // Stage 2 = 168 hours in minutes
|
||||
|
||||
// curTime = Mar 14 (day 8 of holiday, day 5 of stage 2)
|
||||
// Old bug: startTime(Mar 6) + 168h = Mar 13, so curTime > that -> SKIP to Apr 3!
|
||||
// Fixed: startTime(Mar 6) + 72h + 168h = Mar 16, so curTime < that -> correct
|
||||
time_t curTime = MakeTime(2026, 3, 14, 12);
|
||||
|
||||
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
|
||||
// Should return Mar 6 + stageOffset = Mar 9 (stage 2 start)
|
||||
EXPECT_EQ(result, MakeTime(2026, 3, 6) + stageOffset);
|
||||
}
|
||||
|
||||
// Stage 2: curTime after entire holiday ends -> selects next occurrence
|
||||
TEST_F(FindStartTimeForStageTest, Stage2_AfterHolidayEnds_SelectsNextDate)
|
||||
{
|
||||
uint32_t dates[26] = {};
|
||||
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
|
||||
|
||||
time_t stageOffset = 72 * 3600;
|
||||
uint32_t stageLengthMin = 168 * 60;
|
||||
|
||||
// curTime = Mar 20 (well past the entire holiday)
|
||||
time_t curTime = MakeTime(2026, 3, 20);
|
||||
|
||||
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
|
||||
EXPECT_EQ(result, MakeTime(2026, 4, 3) + stageOffset);
|
||||
}
|
||||
|
||||
// No valid dates -> returns 0
|
||||
TEST_F(FindStartTimeForStageTest, NoDates_ReturnsZero)
|
||||
{
|
||||
uint32_t dates[26] = {};
|
||||
time_t curTime = MakeTime(2026, 6, 1);
|
||||
|
||||
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, 0, 168 * 60, curTime);
|
||||
EXPECT_EQ(result, 0);
|
||||
}
|
||||
|
||||
// All dates in the past -> returns 0
|
||||
TEST_F(FindStartTimeForStageTest, AllDatesPast_ReturnsZero)
|
||||
{
|
||||
uint32_t dates[26] = {};
|
||||
PackTwoDates(dates, 2026, 1, 5, 2026, 2, 6);
|
||||
|
||||
time_t stageOffset = 0;
|
||||
uint32_t stageLengthMin = 72 * 60;
|
||||
|
||||
time_t curTime = MakeTime(2026, 6, 1); // way after both dates
|
||||
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
|
||||
EXPECT_EQ(result, 0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SpellInfoTestHelper.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SharedDefines.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
/**
|
||||
* @brief Tests for the binary spell detection condition used in
|
||||
* SpellMgr::LoadSpellInfoCustomAttributes.
|
||||
*
|
||||
* The condition determines whether a non-damage aura effect should mark
|
||||
* a spell as binary (fully resistable via spell resistance).
|
||||
*
|
||||
* Correct logic: CalcValue() || ((INTERRUPT_CAST || DONT_BREAK_STEALTH) && !NO_IMMUNITIES)
|
||||
*
|
||||
* A spell should be marked binary if:
|
||||
* - Its effect has a non-zero CalcValue (e.g. Fear, Polymorph, Frost Nova), OR
|
||||
* - It is an interrupt/stealth spell without the NO_IMMUNITIES attribute
|
||||
*/
|
||||
|
||||
namespace
|
||||
{
|
||||
// Replicates the binary detection condition from SpellMgr.cpp
|
||||
// Returns true if the effect should mark the spell as binary
|
||||
bool ShouldMarkBinary(SpellInfo const* spellInfo, uint8 effIndex)
|
||||
{
|
||||
return spellInfo->Effects[effIndex].CalcValue() ||
|
||||
((spellInfo->Effects[effIndex].Effect == SPELL_EFFECT_INTERRUPT_CAST ||
|
||||
spellInfo->HasAttribute(SPELL_ATTR0_CU_DONT_BREAK_STEALTH)) &&
|
||||
!spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES));
|
||||
}
|
||||
}
|
||||
|
||||
class BinarySpellDetectionTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override {}
|
||||
};
|
||||
|
||||
// CC aura with non-zero CalcValue should be binary (e.g. Fear, Polymorph)
|
||||
TEST_F(BinarySpellDetectionTest, AuraWithCalcValue_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_FEAR)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 1) // CalcValue = 0 + 1 = 1
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// Aura with zero CalcValue and no special attributes should NOT be binary
|
||||
TEST_F(BinarySpellDetectionTest, AuraWithZeroCalcValue_NotBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_FEAR)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 0) // CalcValue = 0
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_FALSE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// INTERRUPT_CAST effect without NO_IMMUNITIES should be binary
|
||||
TEST_F(BinarySpellDetectionTest, InterruptCast_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_INTERRUPT_CAST)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 0) // CalcValue = 0
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// INTERRUPT_CAST with NO_IMMUNITIES should NOT be binary
|
||||
TEST_F(BinarySpellDetectionTest, InterruptCastWithNoImmunities_NotBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_INTERRUPT_CAST)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 0)
|
||||
.WithAttributes(SPELL_ATTR0_NO_IMMUNITIES)
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_FALSE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// Fear-like spell: APPLY_AURA MOD_FEAR with BasePoints=-1, DieSides=1
|
||||
// CalcValue = -1 + 1 = 0, but second effect has value
|
||||
TEST_F(BinarySpellDetectionTest, FearLikeSpell_SecondEffectHasValue_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_FEAR)
|
||||
.WithEffectBasePoints(0, -1)
|
||||
.WithEffectDieSides(0, 1) // CalcValue = -1 + 1 = 0
|
||||
.WithEffect(1, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_INCREASE_SPEED)
|
||||
.WithEffectBasePoints(1, 24)
|
||||
.WithEffectDieSides(1, 1) // CalcValue = 24 + 1 = 25
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_SHADOW)
|
||||
.BuildUnique();
|
||||
|
||||
// Effect 0 has CalcValue 0, should not mark binary
|
||||
EXPECT_FALSE(ShouldMarkBinary(spell.get(), 0));
|
||||
// Effect 1 has CalcValue 25, should mark binary
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 1));
|
||||
}
|
||||
|
||||
// Polymorph-like: APPLY_AURA MOD_CONFUSE with positive BasePoints
|
||||
TEST_F(BinarySpellDetectionTest, PolymorphLikeSpell_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_CONFUSE)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 1) // CalcValue = 1
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_ARCANE)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
|
||||
// Frost Nova-like: APPLY_AURA MOD_ROOT with positive BasePoints
|
||||
TEST_F(BinarySpellDetectionTest, FrostNovaLikeSpell_IsBinary)
|
||||
{
|
||||
auto spell = SpellInfoBuilder()
|
||||
.WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_ROOT)
|
||||
.WithEffectBasePoints(0, 0)
|
||||
.WithEffectDieSides(0, 1) // CalcValue = 1
|
||||
.WithSchoolMask(SPELL_SCHOOL_MASK_FROST)
|
||||
.BuildUnique();
|
||||
|
||||
EXPECT_TRUE(ShouldMarkBinary(spell.get(), 0));
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file SpellProcArcanePotencyTest.cpp
|
||||
* @brief Unit tests for Arcane Potency proc behavior
|
||||
*
|
||||
* Arcane Potency (57529/57531) buffs should only be consumed by the spell
|
||||
* that was actually affected by the crit bonus, not by the same cast that
|
||||
* triggered them. This is achieved via PROC_ATTR_REQ_SPELLMOD (0x08) which
|
||||
* requires the proccing aura to be in the triggering spell's m_appliedMods.
|
||||
*
|
||||
* The crit aura registration in SpellDoneCritChance ensures that only spells
|
||||
* whose crit chance was actually modified by Arcane Potency will have it in
|
||||
* their m_appliedMods set.
|
||||
*
|
||||
* References:
|
||||
* - TrinityCore commit 81f16b201b
|
||||
* - ChromieCraft issue #9092
|
||||
*/
|
||||
|
||||
#include "AuraScriptTestFramework.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace testing;
|
||||
|
||||
namespace
|
||||
{
|
||||
// Arcane Potency buff spell IDs
|
||||
constexpr uint32 ARCANE_POTENCY_R1 = 57529;
|
||||
constexpr uint32 ARCANE_POTENCY_R2 = 57531;
|
||||
|
||||
// SpellFamilyMask from spell_proc for Arcane Potency
|
||||
constexpr uint32 AP_FAMILY_MASK0 = 0x61401035;
|
||||
constexpr uint32 AP_FAMILY_MASK1 = 0x00001000;
|
||||
constexpr uint32 AP_FAMILY_MASK2 = 0;
|
||||
|
||||
// Mage spell flags that SHOULD match Arcane Potency's mask
|
||||
// Fireball: bit 0
|
||||
constexpr uint32 FIREBALL_FLAG0 = 0x00000001;
|
||||
// Frostfire Bolt: bit 2 + Mask1 bit 12
|
||||
constexpr uint32 FROSTFIRE_BOLT_FLAG0 = 0x00000004;
|
||||
constexpr uint32 FROSTFIRE_BOLT_FLAG1 = 0x00001000;
|
||||
// Fire Blast: bit 4
|
||||
constexpr uint32 FIRE_BLAST_FLAG0 = 0x00000010;
|
||||
// Frostbolt: bit 5
|
||||
constexpr uint32 FROSTBOLT_FLAG0 = 0x00000020;
|
||||
// Arcane Explosion: bit 12
|
||||
constexpr uint32 ARCANE_EXPLOSION_FLAG0 = 0x00001000;
|
||||
// Scorch: bit 22
|
||||
constexpr uint32 SCORCH_FLAG0 = 0x00400000;
|
||||
// Arcane Blast: bit 29
|
||||
constexpr uint32 ARCANE_BLAST_FLAG0 = 0x20000000;
|
||||
|
||||
// Mage spell flags that should NOT match
|
||||
// Arcane Missiles: bit 11
|
||||
constexpr uint32 ARCANE_MISSILES_FLAG0 = 0x00000800;
|
||||
// Ice Lance: bit 17
|
||||
constexpr uint32 ICE_LANCE_FLAG0 = 0x00020000;
|
||||
// Arcane Barrage: Mask1 bit 15
|
||||
constexpr uint32 ARCANE_BARRAGE_FLAG1 = 0x00008000;
|
||||
|
||||
/**
|
||||
* Build the Arcane Potency proc entry matching our spell_proc SQL.
|
||||
*/
|
||||
SpellProcEntry BuildArcanePotencyProcEntry()
|
||||
{
|
||||
return SpellProcEntryBuilder()
|
||||
.WithSpellFamilyName(SPELLFAMILY_MAGE)
|
||||
.WithSpellFamilyMask(flag96(AP_FAMILY_MASK0, AP_FAMILY_MASK1, AP_FAMILY_MASK2))
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Proc Entry Configuration Tests
|
||||
// =============================================================================
|
||||
|
||||
class ArcanePotencyProcTest : public AuraScriptProcTestFixture
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
AuraScriptProcTestFixture::SetUp();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ProcEntry_HasReqSpellmodAttribute)
|
||||
{
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD)
|
||||
<< "Arcane Potency must have PROC_ATTR_REQ_SPELLMOD to prevent "
|
||||
"same-cast consumption";
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ProcEntry_CastPhaseOnly)
|
||||
{
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_EQ(procEntry.SpellPhaseMask, PROC_SPELL_PHASE_CAST)
|
||||
<< "Arcane Potency should only proc on CAST phase";
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ProcEntry_MageFamily)
|
||||
{
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_EQ(procEntry.SpellFamilyName, SPELLFAMILY_MAGE);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ProcEntry_BothRanksIdentical)
|
||||
{
|
||||
// Both ranks should use the same proc configuration
|
||||
auto r1 = BuildArcanePotencyProcEntry();
|
||||
auto r2 = BuildArcanePotencyProcEntry();
|
||||
|
||||
EXPECT_EQ(r1.SpellFamilyName, r2.SpellFamilyName);
|
||||
EXPECT_TRUE(r1.SpellFamilyMask == r2.SpellFamilyMask);
|
||||
EXPECT_EQ(r1.SpellPhaseMask, r2.SpellPhaseMask);
|
||||
EXPECT_EQ(r1.AttributesMask, r2.AttributesMask);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellFamilyMask Matching Tests - Spells That SHOULD Match
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_Fireball_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(133, SPELLFAMILY_MAGE, FIREBALL_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_Frostbolt_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(116, SPELLFAMILY_MAGE, FROSTBOLT_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneBlast_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(30451, SPELLFAMILY_MAGE, ARCANE_BLAST_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneExplosion_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(1449, SPELLFAMILY_MAGE, ARCANE_EXPLOSION_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_FrostfireBolt_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(44614, SPELLFAMILY_MAGE,
|
||||
FROSTFIRE_BOLT_FLAG0, FROSTFIRE_BOLT_FLAG1);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_FireBlast_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(2136, SPELLFAMILY_MAGE, FIRE_BLAST_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_Scorch_Matches)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(2948, SPELLFAMILY_MAGE, SCORCH_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_TRUE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellFamilyMask Matching Tests - Spells That Should NOT Match
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneMissiles_DoesNotMatch)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(5143, SPELLFAMILY_MAGE, ARCANE_MISSILES_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_IceLance_DoesNotMatch)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(30455, SPELLFAMILY_MAGE, ICE_LANCE_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_ArcaneBarrage_DoesNotMatch)
|
||||
{
|
||||
auto* spell = CreateSpellInfo(44425, SPELLFAMILY_MAGE, 0, ARCANE_BARRAGE_FLAG1);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, FamilyMask_NonMageSpell_DoesNotMatch)
|
||||
{
|
||||
// Warrior spell with matching flags should NOT match due to family check
|
||||
auto* spell = CreateSpellInfo(6343, /*SPELLFAMILY_WARRIOR*/4, FIREBALL_FLAG0);
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
EXPECT_FALSE(TestSpellFamilyMatch(procEntry.SpellFamilyName, procEntry.SpellFamilyMask, spell));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Proc Phase Tests - CAST phase filtering
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, Phase_TriggersOnCast)
|
||||
{
|
||||
// Test phase filtering in isolation (no family filter)
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnCast();
|
||||
EXPECT_PROC_TRIGGERS(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, Phase_DoesNotTriggerOnHit)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnHit()
|
||||
.WithNormalHit();
|
||||
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, scenario);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, Phase_DoesNotTriggerOnFinish)
|
||||
{
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_CAST)
|
||||
.WithAttributesMask(PROC_ATTR_REQ_SPELLMOD)
|
||||
.Build();
|
||||
|
||||
auto scenario = ProcScenarioBuilder()
|
||||
.OnSpellDamage()
|
||||
.OnFinish();
|
||||
EXPECT_PROC_DOES_NOT_TRIGGER(procEntry, scenario);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROC_ATTR_REQ_SPELLMOD Charge Gating Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ReqSpellmod_AuraWithCharges_BlocksWithoutAppliedMod)
|
||||
{
|
||||
// When PROC_ATTR_REQ_SPELLMOD is set and aura uses charges,
|
||||
// GetProcEffectMask should return 0 if aura is not in m_appliedMods.
|
||||
// We test the prerequisite: that the proc entry is configured correctly
|
||||
// so the charge-gating code path in GetProcEffectMask is reachable.
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(ARCANE_POTENCY_R1)
|
||||
.WithCharges(1)
|
||||
.Build();
|
||||
|
||||
// The aura should be using charges (required for REQ_SPELLMOD gating)
|
||||
EXPECT_TRUE(aura->IsUsingCharges());
|
||||
EXPECT_TRUE(procEntry.AttributesMask & PROC_ATTR_REQ_SPELLMOD);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, ReqSpellmod_AuraWithoutCharges_SkipsCheck)
|
||||
{
|
||||
// If aura has no charges, PROC_ATTR_REQ_SPELLMOD check is skipped
|
||||
// (no charges to consume = no need for spellmod gating)
|
||||
auto procEntry = BuildArcanePotencyProcEntry();
|
||||
|
||||
auto aura = AuraStubBuilder()
|
||||
.WithId(ARCANE_POTENCY_R1)
|
||||
.WithCharges(0)
|
||||
.Build();
|
||||
|
||||
EXPECT_FALSE(aura->IsUsingCharges());
|
||||
// Even though PROC_ATTR_REQ_SPELLMOD is set, the condition
|
||||
// (IsUsingCharges() || USE_STACKS_FOR_CHARGES) would be false
|
||||
// so the spellmod check would be skipped
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SpellFamilyMask Bitmask Validation
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, MaskBits_CorrectBitsSet)
|
||||
{
|
||||
// Verify the exact bits in our SpellFamilyMask0
|
||||
// Bit 0: Fireball
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000001);
|
||||
// Bit 2: Frostfire Bolt
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000004);
|
||||
// Bit 4: Fire Blast
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000010);
|
||||
// Bit 5: Frostbolt
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00000020);
|
||||
// Bit 12: Arcane Explosion
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00001000);
|
||||
// Bit 22: Scorch
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x00400000);
|
||||
// Bit 29: Arcane Blast
|
||||
EXPECT_TRUE(AP_FAMILY_MASK0 & 0x20000000);
|
||||
|
||||
// Mask1: Bit 12 (Frostfire Bolt secondary)
|
||||
EXPECT_TRUE(AP_FAMILY_MASK1 & 0x00001000);
|
||||
}
|
||||
|
||||
TEST_F(ArcanePotencyProcTest, MaskBits_CorrectBitsNotSet)
|
||||
{
|
||||
// Bit 11: Arcane Missiles - should NOT be set
|
||||
EXPECT_FALSE(AP_FAMILY_MASK0 & 0x00000800);
|
||||
// Bit 17: Ice Lance - should NOT be set
|
||||
EXPECT_FALSE(AP_FAMILY_MASK0 & 0x00020000);
|
||||
// Mask1 bit 15: Arcane Barrage - should NOT be set
|
||||
EXPECT_FALSE(AP_FAMILY_MASK1 & 0x00008000);
|
||||
}
|
||||
@@ -790,6 +790,52 @@ TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_SpellFamilyFlagsZeroAcceptsAll)
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo));
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTest, CanSpellTriggerProcOnEvent_GenericFamilyIgnoresMask)
|
||||
{
|
||||
// Generic family (0) should bypass family mask checks entirely
|
||||
auto* spellInfo = SpellInfoBuilder()
|
||||
.WithId(101)
|
||||
.WithSpellFamilyName(SPELLFAMILY_MAGE)
|
||||
.WithSpellFamilyFlags(0x1, 0, 0) // some mage flag
|
||||
.Build();
|
||||
_spellInfos.push_back(spellInfo);
|
||||
|
||||
DamageInfo damageInfo(nullptr, nullptr, 100, spellInfo, SPELL_SCHOOL_MASK_FIRE, SPELL_DIRECT_DAMAGE);
|
||||
|
||||
auto procEntry = SpellProcEntryBuilder()
|
||||
.WithProcFlags(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellFamilyName(0) // generic family
|
||||
.WithSpellFamilyMask(flag96(0x2, 0, 0)) // does NOT match the spell's flags
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.Build();
|
||||
|
||||
auto eventInfo = ProcEventInfoBuilder()
|
||||
.WithTypeMask(PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG)
|
||||
.WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
|
||||
.WithHitMask(PROC_HIT_NORMAL)
|
||||
.WithDamageInfo(&damageInfo)
|
||||
.Build();
|
||||
|
||||
EXPECT_TRUE(sSpellMgr->CanSpellTriggerProcOnEvent(procEntry, eventInfo))
|
||||
<< "Generic family entries should ignore mask restrictions";
|
||||
}
|
||||
|
||||
TEST_F(SpellProcTest, SpellInfo_IsAffected_GenericBehavior)
|
||||
{
|
||||
auto* spellInfo = SpellInfoBuilder()
|
||||
.WithId(102)
|
||||
.WithSpellFamilyName(SPELLFAMILY_WARLOCK)
|
||||
.WithSpellFamilyFlags(0x4, 0, 0)
|
||||
.Build();
|
||||
_spellInfos.push_back(spellInfo);
|
||||
|
||||
// generic family should return true regardless of mask
|
||||
EXPECT_TRUE(spellInfo->IsAffected(0, flag96(0x4, 0, 0)));
|
||||
EXPECT_TRUE(spellInfo->IsAffected(0, flag96(0x8, 0, 0)));
|
||||
// a non-generic family still respects mask
|
||||
EXPECT_FALSE(spellInfo->IsAffected(SPELLFAMILY_PALADIN, flag96(0x4, 0, 0)));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real-world Spell Proc Examples
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user