- Merge fix

This commit is contained in:
kadeshar
2026-03-13 22:41:25 +01:00
114 changed files with 73317 additions and 385 deletions
+51
View File
@@ -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
#
+1 -1
View File
@@ -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
+11 -4
View File
@@ -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;
+3 -3
View File
@@ -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);
}
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+14 -2
View File
@@ -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);
+36 -12
View File
@@ -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
+6 -6
View File
@@ -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;
+15 -18
View File
@@ -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);
+18 -1
View File
@@ -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;
+6
View File
@@ -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;
+9 -7
View File
@@ -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);
+9 -1
View File
@@ -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,
@@ -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);
}
@@ -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();
}
+12
View File
@@ -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
*
+3 -1
View File
@@ -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*/) { }
};
+1
View File
@@ -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.
+3 -3
View File
@@ -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; }
+23 -14
View File
@@ -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;
-7
View File
@@ -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();
+16 -12
View File
@@ -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
+3
View File
@@ -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
+40 -10
View File
@@ -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;
+1
View File
@@ -778,6 +778,7 @@ public:
void LoadSpellInfoCustomAttributes();
void LoadSpellInfoCorrections();
void LoadSpellSpecificAndAuraState();
void LoadSpellJumpDistances();
private:
SpellDifficultySearcherMap mSpellDifficultySearcherMap;
+3
View File
@@ -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();
+3
View File
@@ -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);
}
+1
View File
@@ -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
};
+62 -1
View File
@@ -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()
+49 -2
View File
@@ -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)
@@ -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
@@ -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())
+3 -3
View File
@@ -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;
}
}
+1 -1
View File
@@ -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);
+6 -1
View File
@@ -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);
+21 -9
View File
@@ -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);
}
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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;
+38
View File
@@ -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
// =============================================================================