diff --git a/src/test/mocks/IntegrationTestFixture.h b/src/test/mocks/IntegrationTestFixture.h
new file mode 100644
index 0000000000..494279a880
--- /dev/null
+++ b/src/test/mocks/IntegrationTestFixture.h
@@ -0,0 +1,189 @@
+/*
+ * 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 .
+ */
+
+#ifndef AZEROTHCORE_INTEGRATION_TEST_FIXTURE_H
+#define AZEROTHCORE_INTEGRATION_TEST_FIXTURE_H
+
+#include "TestMap.h"
+#include "TestPlayer.h"
+#include "TestCreature.h"
+#include "WorldMock.h"
+#include "WorldSession.h"
+#include "DBCStores.h"
+#include "SharedDefines.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include
+
+using namespace testing;
+
+// Faction template IDs for test creatures
+static constexpr uint32 TEST_FACTION_HOSTILE_TO_MONSTERS = 90001;
+static constexpr uint32 TEST_FACTION_HOSTILE_TO_ALL = 90002;
+
+class IntegrationTestFixture : public ::testing::Test
+{
+protected:
+ void SetUp() override
+ {
+ TestMap::EnsureDBC();
+ EnsureFactionTemplates();
+
+ _originalWorld = sWorld.release();
+ _worldMock = new NiceMock();
+ sWorld.reset(_worldMock);
+
+ static std::string emptyString;
+ ON_CALL(*_worldMock, GetDataPath()).WillByDefault(ReturnRef(emptyString));
+ ON_CALL(*_worldMock, GetRealmName()).WillByDefault(ReturnRef(emptyString));
+ ON_CALL(*_worldMock, GetDefaultDbcLocale()).WillByDefault(Return(LOCALE_enUS));
+ ON_CALL(*_worldMock, getRate(_)).WillByDefault(Return(1.0f));
+ ON_CALL(*_worldMock, getBoolConfig(_)).WillByDefault(Return(false));
+ ON_CALL(*_worldMock, getIntConfig(_)).WillByDefault(Return(0));
+ ON_CALL(*_worldMock, getFloatConfig(_)).WillByDefault(Return(0.0f));
+ ON_CALL(*_worldMock, GetPlayerSecurityLimit()).WillByDefault(Return(SEC_PLAYER));
+
+ _testMap = new TestMap();
+ }
+
+ void TearDown() override
+ {
+ for (auto* creature : _trackedCreatures)
+ {
+ if (creature->IsInWorld())
+ creature->RemoveFromWorld();
+ delete creature;
+ }
+ _trackedCreatures.clear();
+
+ for (auto* player : _trackedPlayers)
+ {
+ if (player->IsInWorld())
+ player->RemoveFromWorld();
+ }
+ _trackedPlayers.clear();
+
+ for (auto* tmpl : _ownedCreatureTemplates)
+ delete tmpl;
+ _ownedCreatureTemplates.clear();
+
+ delete _testMap;
+ _testMap = nullptr;
+
+ IWorld* currentWorld = sWorld.release();
+ delete currentWorld;
+ _worldMock = nullptr;
+
+ sWorld.reset(_originalWorld);
+ _originalWorld = nullptr;
+
+ // Intentional leaks of session/player to avoid database access in destructors
+ }
+
+ TestPlayer* CreateTestPlayer(ObjectGuid::LowType guidLow = 1,
+ std::string const& name = "TestPlayer",
+ AccountTypes security = SEC_PLAYER)
+ {
+ auto* session = new WorldSession(guidLow, std::string(name), 0, nullptr, security,
+ EXPANSION_WRATH_OF_THE_LICH_KING, 0, LOCALE_enUS, 0, false, false, 0);
+
+ auto* player = new TestPlayer(session);
+ player->ForceInitValues(guidLow);
+ session->SetPlayer(player);
+ player->SetSession(session);
+ player->SetMap(_testMap);
+ player->AddToWorld();
+ _trackedPlayers.push_back(player);
+
+ return player;
+ }
+
+ TestCreature* CreateTestCreature(ObjectGuid::LowType guidLow, uint32 entry, uint32 factionId)
+ {
+ // CreatureTemplate must be heap-allocated AFTER WorldMock is installed
+ // because CreatureMovementData() constructor calls sWorld->getIntConfig()
+ auto* tmpl = new CreatureTemplate();
+ tmpl->Entry = entry;
+ tmpl->faction = factionId;
+ tmpl->minlevel = 80;
+ tmpl->maxlevel = 80;
+ tmpl->unit_class = CLASS_WARRIOR;
+ tmpl->DamageModifier = 1.0f;
+ tmpl->BaseAttackTime = 2000;
+ tmpl->RangeAttackTime = 2000;
+ tmpl->BaseVariance = 1.0f;
+ tmpl->RangeVariance = 1.0f;
+ tmpl->ModHealth = 1.0f;
+ _ownedCreatureTemplates.push_back(tmpl);
+
+ auto* creature = new TestCreature();
+ creature->ForceInitValues(guidLow, entry);
+ creature->SetTestMap(_testMap);
+ creature->SetInWorld(true);
+ creature->SetAlive(true);
+ creature->SetPhase(1);
+ creature->SetFaction(factionId);
+ creature->SetLevel(80);
+ creature->SetMaxHealth(10000);
+ creature->SetHealth(10000);
+ creature->InitializeThreatManager();
+ _trackedCreatures.push_back(creature);
+
+ return creature;
+ }
+
+ NiceMock* GetWorldMock() { return _worldMock; }
+ TestMap* GetTestMap() { return _testMap; }
+
+private:
+ static void EnsureFactionTemplates()
+ {
+ static bool initialized = false;
+ if (initialized)
+ return;
+ initialized = true;
+
+ // Faction 90001: "player-like" — ourMask=1 (FACTION_MASK_PLAYER), hostile to monsters (hostileMask=8)
+ auto* f1 = new FactionTemplateEntry();
+ std::memset(f1, 0, sizeof(FactionTemplateEntry));
+ f1->ID = TEST_FACTION_HOSTILE_TO_MONSTERS;
+ f1->faction = TEST_FACTION_HOSTILE_TO_MONSTERS;
+ f1->ourMask = 0x1; // FACTION_MASK_PLAYER
+ f1->friendlyMask = 0x1; // friendly to players
+ f1->hostileMask = 0x8; // hostile to monsters
+ sFactionTemplateStore.SetEntry(TEST_FACTION_HOSTILE_TO_MONSTERS, f1);
+
+ // Faction 90002: "monster" — ourMask=8 (FACTION_MASK_MONSTER), hostile to players (hostileMask=1)
+ auto* f2 = new FactionTemplateEntry();
+ std::memset(f2, 0, sizeof(FactionTemplateEntry));
+ f2->ID = TEST_FACTION_HOSTILE_TO_ALL;
+ f2->faction = TEST_FACTION_HOSTILE_TO_ALL;
+ f2->ourMask = 0x8; // FACTION_MASK_MONSTER
+ f2->friendlyMask = 0x0; // friendly to none
+ f2->hostileMask = 0x1; // hostile to players
+ sFactionTemplateStore.SetEntry(TEST_FACTION_HOSTILE_TO_ALL, f2);
+ }
+
+ IWorld* _originalWorld = nullptr;
+ NiceMock* _worldMock = nullptr;
+ TestMap* _testMap = nullptr;
+ std::vector _trackedPlayers;
+ std::vector _trackedCreatures;
+ std::vector _ownedCreatureTemplates;
+};
+
+#endif //AZEROTHCORE_INTEGRATION_TEST_FIXTURE_H
diff --git a/src/test/mocks/SpellInfoTestHelper.h b/src/test/mocks/SpellInfoTestHelper.h
index 9828234794..2b5a14eca9 100644
--- a/src/test/mocks/SpellInfoTestHelper.h
+++ b/src/test/mocks/SpellInfoTestHelper.h
@@ -128,6 +128,13 @@ public:
return *this;
}
+ TestSpellEntryHelper& WithEffectMiscValue(uint8 effIndex, int32 miscValue)
+ {
+ if (effIndex < MAX_SPELL_EFFECTS)
+ _entry.EffectMiscValue[effIndex] = miscValue;
+ return *this;
+ }
+
TestSpellEntryHelper& WithEffectDieSides(uint8 effIndex, int32 dieSides)
{
if (effIndex < MAX_SPELL_EFFECTS)
@@ -233,6 +240,12 @@ public:
return *this;
}
+ SpellInfoBuilder& WithEffectMiscValue(uint8 effIndex, int32 miscValue)
+ {
+ _entryHelper.WithEffectMiscValue(effIndex, miscValue);
+ return *this;
+ }
+
SpellInfoBuilder& WithEffectDieSides(uint8 effIndex, int32 dieSides)
{
_entryHelper.WithEffectDieSides(effIndex, dieSides);
diff --git a/src/test/mocks/TestMap.cpp b/src/test/mocks/TestMap.cpp
index 012f6eeeeb..43352c50eb 100644
--- a/src/test/mocks/TestMap.cpp
+++ b/src/test/mocks/TestMap.cpp
@@ -17,10 +17,13 @@
#include "DBCStores.h"
#include "ScriptMgr.h"
#include "ScriptDefines/AllMapScript.h"
+#include "ScriptDefines/AllSpellScript.h"
#include "ScriptDefines/GlobalScript.h"
#include "ScriptDefines/MiscScript.h"
+#include "ScriptDefines/PlayerScript.h"
#include "ScriptDefines/UnitScript.h"
#include "ScriptDefines/WorldObjectScript.h"
+#include "ScriptDefines/CommandScript.h"
TestMap::TestMap()
: Map(0, 0, REGULAR_DIFFICULTY, nullptr)
@@ -55,8 +58,11 @@ TestMap::~TestMap()
// Initialize all script registries so CALL_ENABLED_HOOKS doesn't
// crash on uninitialized vectors during Object/Unit/Map operations
ScriptRegistry::InitEnabledHooksIfNeeded(ALLMAPHOOK_END);
+ ScriptRegistry::InitEnabledHooksIfNeeded(ALLSPELLHOOK_END);
ScriptRegistry::InitEnabledHooksIfNeeded(GLOBALHOOK_END);
ScriptRegistry::InitEnabledHooksIfNeeded(MISCHOOK_END);
+ ScriptRegistry::InitEnabledHooksIfNeeded(PLAYERHOOK_END);
ScriptRegistry::InitEnabledHooksIfNeeded(UNITHOOK_END);
ScriptRegistry::InitEnabledHooksIfNeeded(WORLDOBJECTHOOK_END);
+ ScriptRegistry::InitEnabledHooksIfNeeded(ALLCOMMANDHOOK_END);
}
diff --git a/src/test/mocks/TestPlayer.h b/src/test/mocks/TestPlayer.h
new file mode 100644
index 0000000000..e827ff9ba4
--- /dev/null
+++ b/src/test/mocks/TestPlayer.h
@@ -0,0 +1,42 @@
+/*
+ * 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 .
+ */
+
+#ifndef AZEROTHCORE_TEST_PLAYER_H
+#define AZEROTHCORE_TEST_PLAYER_H
+
+#include "Player.h"
+
+class TestPlayer : public Player
+{
+public:
+ using Player::Player;
+
+ void UpdateObjectVisibility(bool /*forced*/ = true, bool /*fromUpdate*/ = false) override { }
+
+ void AddToWorld() override { Object::AddToWorld(); }
+ void RemoveFromWorld() override { Object::RemoveFromWorld(); }
+
+ void SaveToDB(bool /*create*/, bool /*logout*/) { }
+ void SaveToDB(CharacterDatabaseTransaction /*trans*/, bool /*create*/, bool /*logout*/) { }
+
+ void ForceInitValues(ObjectGuid::LowType guidLow = 1)
+ {
+ Object::_Create(guidLow, uint32(0), HighGuid::Player);
+ }
+};
+
+#endif //AZEROTHCORE_TEST_PLAYER_H
diff --git a/src/test/server/game/Commands/GmVisibleCommandTest.cpp b/src/test/server/game/Commands/GmVisibleCommandTest.cpp
index f916b079e4..4be3b06e9b 100644
--- a/src/test/server/game/Commands/GmVisibleCommandTest.cpp
+++ b/src/test/server/game/Commands/GmVisibleCommandTest.cpp
@@ -15,16 +15,13 @@
* with this program. If not, see .
*/
-#include "Player.h"
+#include "TestPlayer.h"
+#include "TestMap.h"
#include "ScriptMgr.h"
#include "WorldSession.h"
#include "WorldMock.h"
#include "ObjectGuid.h"
-#include "ScriptDefines/MiscScript.h"
#include "ScriptDefines/PlayerScript.h"
-#include "ScriptDefines/WorldObjectScript.h"
-#include "ScriptDefines/UnitScript.h"
-#include "ScriptDefines/CommandScript.h"
#include "SharedDefines.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
@@ -68,25 +65,12 @@ public:
inline static AccountTypes LastSecurity = SEC_PLAYER;
};
-class TestPlayer : public Player
-{
-public:
- using Player::Player;
-
- void UpdateObjectVisibility(bool /*forced*/ = true, bool /*fromUpdate*/ = false) override { }
-
- void ForceInitValues(ObjectGuid::LowType guidLow = 1)
- {
- Object::_Create(guidLow, uint32(0), HighGuid::Player);
- }
-};
-
class GmVisibleCommandTest : public ::testing::Test
{
protected:
void SetUp() override
{
- EnsureScriptRegistriesInitialized();
+ TestMap::EnsureDBC();
TestVisibilityScript::EnsureRegistered();
@@ -139,20 +123,6 @@ protected:
player->SetServerSideVisibility(SERVERSIDE_VISIBILITY_GM, SEC_PLAYER);
}
- static void EnsureScriptRegistriesInitialized()
- {
- static bool initialized = false;
- if (!initialized)
- {
- ScriptRegistry::InitEnabledHooksIfNeeded(MISCHOOK_END);
- ScriptRegistry::InitEnabledHooksIfNeeded(WORLDOBJECTHOOK_END);
- ScriptRegistry::InitEnabledHooksIfNeeded(UNITHOOK_END);
- ScriptRegistry::InitEnabledHooksIfNeeded(PLAYERHOOK_END);
- ScriptRegistry::InitEnabledHooksIfNeeded(ALLCOMMANDHOOK_END);
- initialized = true;
- }
- }
-
IWorld* originalWorld = nullptr;
NiceMock* worldMock = nullptr;
WorldSession* session = nullptr;
diff --git a/src/test/server/game/Modules/OmenOfClarityGlyphLockTest.cpp b/src/test/server/game/Modules/OmenOfClarityGlyphLockTest.cpp
new file mode 100644
index 0000000000..4563a29a5d
--- /dev/null
+++ b/src/test/server/game/Modules/OmenOfClarityGlyphLockTest.cpp
@@ -0,0 +1,300 @@
+/*
+ * Unit test for the mod-omen-of-clarity glyph slot locking behavior.
+ *
+ * Verifies that when the "empowered omen" feature is enabled for a player,
+ * the major glyph slot 5 (bit 0x20 in PLAYER_GLYPHS_ENABLED) stays locked
+ * even after leveling to 80 (which normally enables that slot).
+ *
+ * Because the module's internal cache (s_oocEnabled) and helper functions
+ * are static to its .cpp file, this test uses a test-local PlayerScript
+ * that replicates the same glyph-locking logic the module uses. This
+ * proves the mechanism works end-to-end through the hook system.
+ */
+
+#include "TestPlayer.h"
+#include "TestMap.h"
+#include "ScriptMgr.h"
+#include "WorldSession.h"
+#include "WorldMock.h"
+#include "ObjectGuid.h"
+#include "ScriptDefines/PlayerScript.h"
+#include "SharedDefines.h"
+#include "gtest/gtest.h"
+
+#ifndef TEST_F
+#define TEST_F(fixture, name) void fixture##_##name()
+#endif
+
+#include
+
+using namespace testing;
+
+namespace
+{
+
+// Constants matching the module's values
+static constexpr uint8 OOC_LOCKED_GLYPH_SLOT = 5;
+static constexpr uint32 OOC_LOCKED_SLOT_BIT = 0x20;
+
+// Test-local cache mirroring the module's s_oocEnabled
+static std::unordered_set s_testOocEnabled;
+
+// -------------------------------------------------------
+// Test PlayerScript: replicates the module's level-change
+// and quest-complete hooks for glyph slot locking.
+// -------------------------------------------------------
+class TestOocPlayerScript : public PlayerScript
+{
+public:
+ TestOocPlayerScript()
+ : PlayerScript("TestOocPlayerScript",
+ { PLAYERHOOK_ON_LEVEL_CHANGED,
+ PLAYERHOOK_ON_PLAYER_COMPLETE_QUEST }) { }
+
+ void OnPlayerLevelChanged(Player* player,
+ uint8 /*oldLevel*/) override
+ {
+ if (s_testOocEnabled.count(
+ player->GetGUID().GetCounter()))
+ {
+ // Same logic as EnsureGlyphSlotLocked in the module
+ uint32 bits = player->GetUInt32Value(
+ PLAYER_GLYPHS_ENABLED);
+ if (bits & OOC_LOCKED_SLOT_BIT)
+ {
+ player->SetUInt32Value(PLAYER_GLYPHS_ENABLED,
+ bits & ~OOC_LOCKED_SLOT_BIT);
+ }
+ }
+ }
+
+ void OnPlayerCompleteQuest(Player* player,
+ Quest const* /*quest*/) override
+ {
+ // Simplified: always enable for the completing player
+ s_testOocEnabled.insert(
+ player->GetGUID().GetCounter());
+
+ // Lock the slot (clear bit 0x20)
+ uint32 bits = player->GetUInt32Value(
+ PLAYER_GLYPHS_ENABLED);
+ player->SetUInt32Value(PLAYER_GLYPHS_ENABLED,
+ bits & ~OOC_LOCKED_SLOT_BIT);
+ }
+
+ static void EnsureRegistered()
+ {
+ if (!Instance)
+ Instance = new TestOocPlayerScript();
+ }
+
+ inline static TestOocPlayerScript* Instance = nullptr;
+};
+
+// -------------------------------------------------------
+// Test fixture
+// -------------------------------------------------------
+class OmenOfClarityGlyphLockTest : public ::testing::Test
+{
+protected:
+ void SetUp() override
+ {
+ TestMap::EnsureDBC();
+ TestOocPlayerScript::EnsureRegistered();
+
+ originalWorld = sWorld.release();
+ worldMock = new NiceMock();
+ sWorld.reset(worldMock);
+
+ static std::string emptyString;
+ ON_CALL(*worldMock, GetDataPath())
+ .WillByDefault(ReturnRef(emptyString));
+ ON_CALL(*worldMock, GetRealmName())
+ .WillByDefault(ReturnRef(emptyString));
+ ON_CALL(*worldMock, GetDefaultDbcLocale())
+ .WillByDefault(Return(LOCALE_enUS));
+ ON_CALL(*worldMock, getRate(_))
+ .WillByDefault(Return(1.0f));
+ ON_CALL(*worldMock, getBoolConfig(_))
+ .WillByDefault(Return(false));
+ ON_CALL(*worldMock, getIntConfig(_))
+ .WillByDefault(Return(0));
+ ON_CALL(*worldMock, getFloatConfig(_))
+ .WillByDefault(Return(0.0f));
+ ON_CALL(*worldMock, GetPlayerSecurityLimit())
+ .WillByDefault(Return(SEC_PLAYER));
+
+ session = new WorldSession(
+ 1, "test", 0, nullptr, SEC_PLAYER,
+ EXPANSION_WRATH_OF_THE_LICH_KING,
+ 0, LOCALE_enUS, 0, false, false, 0);
+
+ player = new TestPlayer(session);
+ player->ForceInitValues(42);
+ session->SetPlayer(player);
+ player->SetSession(session);
+
+ s_testOocEnabled.clear();
+ }
+
+ void TearDown() override
+ {
+ s_testOocEnabled.clear();
+
+ // Intentional leaks of session/player to avoid
+ // database access in destructors.
+ IWorld* currentWorld = sWorld.release();
+ delete currentWorld;
+ worldMock = nullptr;
+
+ sWorld.reset(originalWorld);
+ originalWorld = nullptr;
+ session = nullptr;
+ player = nullptr;
+ }
+
+ /// Simulate what InitGlyphsForLevel() does for a given level.
+ void SimulateGlyphsForLevel(uint8 level)
+ {
+ uint32 value = 0;
+ if (level >= 15)
+ value |= (0x01 | 0x02);
+ if (level >= 30)
+ value |= 0x08;
+ if (level >= 50)
+ value |= 0x04;
+ if (level >= 70)
+ value |= 0x10;
+ if (level >= 80)
+ value |= 0x20;
+ player->SetUInt32Value(PLAYER_GLYPHS_ENABLED, value);
+ }
+
+ IWorld* originalWorld = nullptr;
+ NiceMock* worldMock = nullptr;
+ WorldSession* session = nullptr;
+ TestPlayer* player = nullptr;
+};
+
+// ----------------------------------------------------------
+// TEST: Feature enabled → level to 80 → slot stays locked
+// ----------------------------------------------------------
+TEST_F(OmenOfClarityGlyphLockTest,
+ GlyphSlotStaysLockedAfterLevelingTo80)
+{
+ // 1. Enable the empowered omen feature for this player
+ s_testOocEnabled.insert(
+ player->GetGUID().GetCounter());
+
+ // 2. Lock glyph slot 5 (clear bit 0x20) as the module
+ // does on quest completion
+ SimulateGlyphsForLevel(79); // slots for level 79 (no 0x20)
+ uint32 bitsAtLevel79 =
+ player->GetUInt32Value(PLAYER_GLYPHS_ENABLED);
+ EXPECT_EQ(bitsAtLevel79 & OOC_LOCKED_SLOT_BIT, 0u)
+ << "Slot 5 should not be enabled before level 80";
+
+ // 3. Simulate hitting level 80:
+ // The game calls InitGlyphsForLevel() which sets 0x20,
+ // THEN fires OnPlayerLevelChanged.
+ SimulateGlyphsForLevel(80);
+ uint32 bitsAfterInit =
+ player->GetUInt32Value(PLAYER_GLYPHS_ENABLED);
+ EXPECT_NE(bitsAfterInit & OOC_LOCKED_SLOT_BIT, 0u)
+ << "InitGlyphsForLevel should enable slot 5 at 80";
+
+ // 4. Fire the level-change hook (same as GiveLevel does)
+ sScriptMgr->OnPlayerLevelChanged(player, 79);
+
+ // 5. Verify the hook cleared the bit
+ uint32 bitsAfterHook =
+ player->GetUInt32Value(PLAYER_GLYPHS_ENABLED);
+ EXPECT_EQ(bitsAfterHook & OOC_LOCKED_SLOT_BIT, 0u)
+ << "Module hook should re-lock slot 5 after level 80";
+
+ // Other glyph slots should be unaffected
+ EXPECT_NE(bitsAfterHook & 0x01, 0u); // slot 0 (level 15)
+ EXPECT_NE(bitsAfterHook & 0x02, 0u); // slot 1 (level 15)
+ EXPECT_NE(bitsAfterHook & 0x04, 0u); // slot 2 (level 50)
+ EXPECT_NE(bitsAfterHook & 0x08, 0u); // slot 3 (level 30)
+ EXPECT_NE(bitsAfterHook & 0x10, 0u); // slot 4 (level 70)
+}
+
+// ----------------------------------------------------------
+// TEST: Feature NOT enabled → level to 80 → slot stays open
+// ----------------------------------------------------------
+TEST_F(OmenOfClarityGlyphLockTest,
+ GlyphSlotOpensNormallyWhenFeatureDisabled)
+{
+ // Feature is NOT enabled (cache is empty)
+
+ SimulateGlyphsForLevel(80);
+ sScriptMgr->OnPlayerLevelChanged(player, 79);
+
+ uint32 bits =
+ player->GetUInt32Value(PLAYER_GLYPHS_ENABLED);
+ EXPECT_NE(bits & OOC_LOCKED_SLOT_BIT, 0u)
+ << "Without the feature, slot 5 should remain enabled";
+}
+
+// ----------------------------------------------------------
+// TEST: Enable at sub-80, level through multiple levels,
+// slot stays locked throughout
+// ----------------------------------------------------------
+TEST_F(OmenOfClarityGlyphLockTest,
+ GlyphSlotStaysLockedThroughMultipleLevelUps)
+{
+ s_testOocEnabled.insert(
+ player->GetGUID().GetCounter());
+
+ // Level from 78 → 79 → 80 → 81 (if somehow possible)
+ for (uint8 newLevel = 78; newLevel <= 80; ++newLevel)
+ {
+ SCOPED_TRACE("Level: " + std::to_string(newLevel));
+ SimulateGlyphsForLevel(newLevel);
+ sScriptMgr->OnPlayerLevelChanged(
+ player, newLevel - 1);
+
+ uint32 bits =
+ player->GetUInt32Value(PLAYER_GLYPHS_ENABLED);
+ EXPECT_EQ(bits & OOC_LOCKED_SLOT_BIT, 0u)
+ << "Slot 5 should remain locked at level "
+ << static_cast(newLevel);
+ }
+}
+
+// ----------------------------------------------------------
+// TEST: Feature enabled mid-session then level up
+// ----------------------------------------------------------
+TEST_F(OmenOfClarityGlyphLockTest,
+ QuestCompletionLocksSlotThenLevelUpKeepsItLocked)
+{
+ // Player is level 79 with no feature
+ SimulateGlyphsForLevel(79);
+ EXPECT_EQ(
+ player->GetUInt32Value(PLAYER_GLYPHS_ENABLED) &
+ OOC_LOCKED_SLOT_BIT, 0u);
+
+ // Complete the quest → hook enables feature + locks slot
+ sScriptMgr->OnPlayerCompleteQuest(player, nullptr);
+ EXPECT_TRUE(s_testOocEnabled.count(
+ player->GetGUID().GetCounter()) > 0)
+ << "Feature should be enabled after quest completion";
+
+ // Now level to 80 → game re-enables all slots
+ SimulateGlyphsForLevel(80);
+ uint32 bitsPreHook =
+ player->GetUInt32Value(PLAYER_GLYPHS_ENABLED);
+ EXPECT_NE(bitsPreHook & OOC_LOCKED_SLOT_BIT, 0u)
+ << "InitGlyphsForLevel should have set 0x20";
+
+ // Level-change hook fires
+ sScriptMgr->OnPlayerLevelChanged(player, 79);
+
+ uint32 bitsPostHook =
+ player->GetUInt32Value(PLAYER_GLYPHS_ENABLED);
+ EXPECT_EQ(bitsPostHook & OOC_LOCKED_SLOT_BIT, 0u)
+ << "Slot 5 should be re-locked after leveling to 80";
+}
+
+} // namespace
diff --git a/src/test/server/game/Spells/FrostboltPvPTest.cpp b/src/test/server/game/Spells/FrostboltPvPTest.cpp
new file mode 100644
index 0000000000..abb7828ac8
--- /dev/null
+++ b/src/test/server/game/Spells/FrostboltPvPTest.cpp
@@ -0,0 +1,254 @@
+/*
+ * 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 .
+ */
+
+#include "IntegrationTestFixture.h"
+#include "SpellInfoTestHelper.h"
+#include "SpellAuras.h"
+#include "SpellAuraEffects.h"
+#include "SpellMgr.h"
+#include "gtest/gtest.h"
+
+#ifndef TEST_F
+#define TEST_F(fixture, name) void fixture##_##name()
+#endif
+
+/*
+ * Integration test: Mage casts Frostbolt at a Warrior in PvP.
+ *
+ * What we test:
+ * 1. The Frostbolt slow aura (SPELL_AURA_MOD_DECREASE_SPEED) is applied
+ * 2. The warrior's run speed is reduced by the correct percentage
+ * 3. The spell damage matches expected base points
+ *
+ * What we simulate vs. what's real:
+ * ┌──────────────────────────────────────────────────────┐
+ * │ REAL (actual game code) │
+ * │ - SpellInfo built from SpellEntry (same as DBC) │
+ * │ - Aura::TryRefreshStackOrCreate │
+ * │ - AuraEffect with SPELL_AURA_MOD_DECREASE_SPEED │
+ * │ - Unit::UpdateSpeed applies the slow modifier │
+ * │ - SpellNonMeleeDamage struct for damage bookkeeping │
+ * │ - Unit::GetSpeedRate / GetSpeed for verification │
+ * ├──────────────────────────────────────────────────────┤
+ * │ SIMULATED (bypassed for test isolation) │
+ * │ - No full Spell::prepare → cast pipeline │
+ * │ - No target selection / LoS / range checks │
+ * │ - Aura applied directly via Unit::AddAura │
+ * │ - Damage dealt directly, no resist/absorb rolls │
+ * └──────────────────────────────────────────────────────┘
+ */
+
+namespace
+{
+
+// Frostbolt Rank 14 (level 79) reference values
+static constexpr uint32 FROSTBOLT_SPELL_ID = 42842;
+static constexpr int32 FROSTBOLT_BASE_DAMAGE = 799; // base points for effect 0
+static constexpr int32 FROSTBOLT_SLOW_PCT = -40; // 40% speed reduction
+static constexpr int32 FROSTBOLT_DURATION_MS = 9000; // 9 seconds
+
+class FrostboltPvPTest : public IntegrationTestFixture
+{
+protected:
+ void SetUp() override
+ {
+ IntegrationTestFixture::SetUp();
+
+ // Build a SpellInfo mimicking Frostbolt Rank 14:
+ // Effect[0]: SPELL_EFFECT_SCHOOL_DAMAGE — base 799 frost damage
+ // Effect[1]: SPELL_EFFECT_APPLY_AURA / SPELL_AURA_MOD_DECREASE_SPEED — -40%
+ _frostboltInfo = SpellInfoBuilder()
+ .WithId(FROSTBOLT_SPELL_ID)
+ .WithSpellFamilyName(SPELLFAMILY_MAGE)
+ .WithSpellFamilyFlags(0x20) // Frostbolt family flag
+ .WithSchoolMask(SPELL_SCHOOL_MASK_FROST)
+ .WithDmgClass(SPELL_DAMAGE_CLASS_MAGIC)
+ .WithEffect(0, SPELL_EFFECT_SCHOOL_DAMAGE)
+ .WithEffectBasePoints(0, FROSTBOLT_BASE_DAMAGE)
+ .WithEffect(1, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_DECREASE_SPEED)
+ .WithEffectBasePoints(1, FROSTBOLT_SLOW_PCT)
+ .BuildUnique();
+
+ // Create a SpellDurationEntry so the aura has a real duration
+ _durationEntry = {};
+ _durationEntry.ID = 1;
+ _durationEntry.Duration[0] = FROSTBOLT_DURATION_MS;
+ _durationEntry.Duration[1] = FROSTBOLT_DURATION_MS;
+ _durationEntry.Duration[2] = FROSTBOLT_DURATION_MS;
+ // Patch the SpellInfo's DurationEntry pointer
+ const_cast(_frostboltInfo.get())->DurationEntry = &_durationEntry;
+ }
+
+ std::unique_ptr _frostboltInfo;
+ SpellDurationEntry _durationEntry;
+};
+
+// ─────────────────────────────────────────────────────────
+// TEST: Frostbolt slow aura is applied and reduces speed
+// ─────────────────────────────────────────────────────────
+//
+// ┌───────────┐ Frostbolt ┌───────────┐
+// │ Mage │ ────────────► │ Warrior │
+// │ (player) │ │ (player) │
+// └───────────┘ └───────────┘
+// │
+// ┌─────▼─────┐
+// │ Slow Aura │
+// │ -40% spd │
+// └───────────┘
+//
+TEST_F(FrostboltPvPTest, SlowAuraAppliedAndReducesSpeed)
+{
+ auto* mage = CreateTestPlayer(1, "Frostmage");
+ auto* warrior = CreateTestPlayer(2, "Berserker");
+
+ // Record the warrior's baseline run speed rate (should be 1.0)
+ float baseSpeedRate = warrior->GetSpeedRate(MOVE_RUN);
+ ASSERT_FLOAT_EQ(baseSpeedRate, 1.0f)
+ << "Warrior should start at 100% run speed";
+
+ // Mage applies the Frostbolt aura (effect index 1 = slow) on the warrior.
+ // effMask 0x2 = only effect[1] (the APPLY_AURA slow), skipping effect[0] (damage).
+ Aura* slowAura = mage->AddAura(_frostboltInfo.get(), 0x2, warrior);
+ ASSERT_NE(slowAura, nullptr) << "Frostbolt slow aura should be created";
+
+ // Verify the aura is on the warrior
+ EXPECT_TRUE(warrior->HasAura(FROSTBOLT_SPELL_ID))
+ << "Warrior should have the Frostbolt aura";
+
+ // Verify aura duration
+ EXPECT_EQ(slowAura->GetMaxDuration(), FROSTBOLT_DURATION_MS);
+
+ // Verify the slow amount on the aura effect
+ AuraEffect const* slowEffect = slowAura->GetEffect(1);
+ ASSERT_NE(slowEffect, nullptr);
+ EXPECT_EQ(slowEffect->GetAmount(), FROSTBOLT_SLOW_PCT)
+ << "Slow effect should be -40%";
+
+ // Verify the warrior's run speed is actually reduced.
+ // UpdateSpeed was called by HandleAuraModDecreaseSpeed.
+ // Formula: speed = max(non_stack_bonus, stack_bonus) → 1.0
+ // then AddPct(speed, -40) → 1.0 * (1 + (-40/100)) = 0.60
+ float expectedSpeedRate = 0.60f;
+ EXPECT_NEAR(warrior->GetSpeedRate(MOVE_RUN), expectedSpeedRate, 0.01f)
+ << "Warrior run speed should be 60% (slowed by 40%)";
+
+ // Walk speed should also be reduced
+ EXPECT_NEAR(warrior->GetSpeedRate(MOVE_WALK), expectedSpeedRate, 0.01f)
+ << "Warrior walk speed should also be reduced by 40%";
+}
+
+// ─────────────────────────────────────────────────────────
+// TEST: Frostbolt damage matches expected base points
+// ─────────────────────────────────────────────────────────
+TEST_F(FrostboltPvPTest, DamageMatchesBasePoints)
+{
+ auto* mage = CreateTestPlayer(1, "Frostmage");
+ auto* warrior = CreateTestPlayer(2, "Berserker");
+
+ // Set warrior health high enough to survive
+ warrior->SetMaxHealth(50000);
+ warrior->SetHealth(50000);
+
+ // The SpellInfo's effect[0] base damage
+ int32 baseDamage = _frostboltInfo->Effects[0].CalcValue(mage);
+ EXPECT_EQ(baseDamage, FROSTBOLT_BASE_DAMAGE)
+ << "Base damage should match Frostbolt Rank 14";
+
+ // Simulate the damage using SpellNonMeleeDamage (same struct the real
+ // spell pipeline uses to track damage/absorb/resist per hit).
+ SpellNonMeleeDamage dmgInfo(mage, warrior, _frostboltInfo.get(),
+ SPELL_SCHOOL_MASK_FROST);
+ dmgInfo.damage = baseDamage;
+
+ // Record health before
+ uint32 healthBefore = warrior->GetHealth();
+
+ // Deal the damage through the real DealDamage path
+ Unit::DealDamage(mage, warrior, dmgInfo.damage, nullptr,
+ SPELL_DIRECT_DAMAGE, SPELL_SCHOOL_MASK_FROST, _frostboltInfo.get());
+
+ uint32 healthAfter = warrior->GetHealth();
+ uint32 actualDamage = healthBefore - healthAfter;
+
+ // With no spell power, no resilience, no armor (frost is magic),
+ // and no absorb shields, the damage dealt equals base damage.
+ EXPECT_EQ(actualDamage, uint32(FROSTBOLT_BASE_DAMAGE))
+ << "Damage dealt should equal base points with no modifiers";
+}
+
+// ─────────────────────────────────────────────────────────
+// TEST: Removing the aura restores full speed
+// ─────────────────────────────────────────────────────────
+TEST_F(FrostboltPvPTest, RemovingSlowRestoresSpeed)
+{
+ auto* mage = CreateTestPlayer(1, "Frostmage");
+ auto* warrior = CreateTestPlayer(2, "Berserker");
+
+ // Apply slow
+ mage->AddAura(_frostboltInfo.get(), 0x2, warrior);
+ ASSERT_NEAR(warrior->GetSpeedRate(MOVE_RUN), 0.60f, 0.01f);
+
+ // Remove the aura (dispel / expiry)
+ warrior->RemoveAurasDueToSpell(FROSTBOLT_SPELL_ID);
+
+ // Speed should be back to normal
+ EXPECT_FLOAT_EQ(warrior->GetSpeedRate(MOVE_RUN), 1.0f)
+ << "Speed should return to 100% after slow is removed";
+}
+
+// ─────────────────────────────────────────────────────────
+// TEST: Full scenario — slow + damage in sequence
+// ─────────────────────────────────────────────────────────
+//
+// Timeline:
+// ┌─────────┬──────────────┬──────────────┬────────────┐
+// │ Start │ Apply slow │ Deal damage │ Verify │
+// │ 100%spd │ → 60% speed │ → HP reduced │ slow+dmg │
+// └─────────┴──────────────┴──────────────┴────────────┘
+//
+TEST_F(FrostboltPvPTest, FullFrostboltScenario)
+{
+ auto* mage = CreateTestPlayer(1, "Frostmage");
+ auto* warrior = CreateTestPlayer(2, "Berserker");
+
+ warrior->SetMaxHealth(50000);
+ warrior->SetHealth(50000);
+
+ // Verify pre-conditions
+ ASSERT_FLOAT_EQ(warrior->GetSpeedRate(MOVE_RUN), 1.0f);
+ ASSERT_EQ(warrior->GetHealth(), 50000u);
+
+ // Step 1: Apply the slow aura (effect[1] only)
+ Aura* aura = mage->AddAura(_frostboltInfo.get(), 0x2, warrior);
+ ASSERT_NE(aura, nullptr);
+
+ // Step 2: Deal the damage (effect[0] simulated)
+ int32 damage = _frostboltInfo->Effects[0].CalcValue(mage);
+ Unit::DealDamage(mage, warrior, damage, nullptr,
+ SPELL_DIRECT_DAMAGE, SPELL_SCHOOL_MASK_FROST, _frostboltInfo.get());
+
+ // Verify both effects landed
+ EXPECT_NEAR(warrior->GetSpeedRate(MOVE_RUN), 0.60f, 0.01f)
+ << "Warrior should be slowed to 60%";
+ EXPECT_EQ(warrior->GetHealth(), 50000u - FROSTBOLT_BASE_DAMAGE)
+ << "Warrior HP should be reduced by Frostbolt base damage";
+ EXPECT_TRUE(warrior->HasAura(FROSTBOLT_SPELL_ID))
+ << "Warrior should still have the Frostbolt debuff";
+}
+
+} // namespace
diff --git a/src/test/server/game/Spells/SpellProcUnitIntegrationTest.cpp b/src/test/server/game/Spells/SpellProcUnitIntegrationTest.cpp
new file mode 100644
index 0000000000..403e0507ee
--- /dev/null
+++ b/src/test/server/game/Spells/SpellProcUnitIntegrationTest.cpp
@@ -0,0 +1,157 @@
+/*
+ * 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 .
+ */
+
+#include "IntegrationTestFixture.h"
+#include "ProcEventInfoHelper.h"
+#include "SpellInfoTestHelper.h"
+#include "SpellMgr.h"
+#include "gtest/gtest.h"
+
+#ifndef TEST_F
+#define TEST_F(fixture, name) void fixture##_##name()
+#endif
+
+namespace
+{
+
+class ProcUnitIntegrationTest : public IntegrationTestFixture
+{
+};
+
+TEST_F(ProcUnitIntegrationTest, CreatureAndPlayerAreHostile)
+{
+ auto* player = CreateTestPlayer(1, "Hostile", SEC_PLAYER);
+ // Player faction defaults to 0, set it to our test faction
+ player->SetUInt32Value(UNIT_FIELD_FACTIONTEMPLATE, TEST_FACTION_HOSTILE_TO_MONSTERS);
+
+ auto* creature = CreateTestCreature(100, 99001, TEST_FACTION_HOSTILE_TO_ALL);
+
+ ReputationRank reaction = player->GetReactionTo(creature);
+ EXPECT_EQ(reaction, REP_HOSTILE);
+
+ ReputationRank reverseReaction = creature->GetReactionTo(player);
+ EXPECT_EQ(reverseReaction, REP_HOSTILE);
+}
+
+TEST_F(ProcUnitIntegrationTest, ProcEventInfoWithRealUnits)
+{
+ auto* attacker = CreateTestPlayer(1, "Attacker", SEC_PLAYER);
+ auto* victim = CreateTestCreature(100, 99001, TEST_FACTION_HOSTILE_TO_ALL);
+
+ auto procInfo = ProcEventInfoBuilder()
+ .WithActor(attacker)
+ .WithActionTarget(victim)
+ .WithProcTarget(victim)
+ .WithTypeMask(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
+ .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
+ .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
+ .WithHitMask(PROC_HIT_NORMAL)
+ .Build();
+
+ EXPECT_EQ(procInfo.GetActor(), attacker);
+ EXPECT_EQ(procInfo.GetActionTarget(), victim);
+ EXPECT_EQ(procInfo.GetProcTarget(), victim);
+ EXPECT_EQ(procInfo.GetTypeMask(), uint32(PROC_FLAG_DONE_MELEE_AUTO_ATTACK));
+}
+
+TEST_F(ProcUnitIntegrationTest, ProcEntryMatchesSpellInfo)
+{
+ auto spellInfo = SpellInfoBuilder()
+ .WithId(99999)
+ .WithSpellFamilyName(SPELLFAMILY_GENERIC)
+ .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
+ .WithProcChance(100)
+ .WithEffect(0, SPELL_EFFECT_APPLY_AURA, SPELL_AURA_PROC_TRIGGER_SPELL)
+ .BuildUnique();
+
+ auto procEntry = SpellProcEntryBuilder()
+ .WithProcFlags(PROC_FLAG_DONE_MELEE_AUTO_ATTACK)
+ .WithSpellTypeMask(PROC_SPELL_TYPE_DAMAGE)
+ .WithSpellPhaseMask(PROC_SPELL_PHASE_HIT)
+ .WithHitMask(PROC_HIT_NORMAL)
+ .WithChance(100.0f)
+ .Build();
+
+ EXPECT_EQ(spellInfo->Id, 99999u);
+ EXPECT_EQ(spellInfo->ProcFlags, uint32(PROC_FLAG_DONE_MELEE_AUTO_ATTACK));
+ EXPECT_EQ(procEntry.ProcFlags, uint32(PROC_FLAG_DONE_MELEE_AUTO_ATTACK));
+ EXPECT_FLOAT_EQ(procEntry.Chance, 100.0f);
+}
+
+TEST_F(ProcUnitIntegrationTest, MultipleCreaturesCanBeCreated)
+{
+ auto* player = CreateTestPlayer(1, "Attacker", SEC_PLAYER);
+ auto* creature1 = CreateTestCreature(100, 99001, TEST_FACTION_HOSTILE_TO_ALL);
+ auto* creature2 = CreateTestCreature(101, 99002, TEST_FACTION_HOSTILE_TO_ALL);
+
+ EXPECT_NE(creature1, creature2);
+ EXPECT_NE(creature1->GetGUID(), creature2->GetGUID());
+ EXPECT_EQ(creature1->GetHealth(), 10000u);
+ EXPECT_EQ(creature2->GetHealth(), 10000u);
+ EXPECT_NE(player->GetGUID(), creature1->GetGUID());
+}
+
+TEST_F(ProcUnitIntegrationTest, ProcOnKillWithRealUnits)
+{
+ auto* attacker = CreateTestPlayer(1, "Killer", SEC_PLAYER);
+ auto* victim = CreateTestCreature(100, 99001, TEST_FACTION_HOSTILE_TO_ALL);
+
+ // Build a kill-proc event
+ auto procInfo = ProcEventInfoBuilder()
+ .WithActor(attacker)
+ .WithActionTarget(victim)
+ .WithProcTarget(victim)
+ .WithTypeMask(PROC_FLAG_KILL)
+ .WithSpellTypeMask(PROC_SPELL_TYPE_NONE)
+ .WithSpellPhaseMask(PROC_SPELL_PHASE_NONE)
+ .WithHitMask(PROC_HIT_NORMAL)
+ .Build();
+
+ EXPECT_EQ(procInfo.GetTypeMask(), uint32(PROC_FLAG_KILL));
+ EXPECT_EQ(procInfo.GetActor(), attacker);
+ EXPECT_EQ(procInfo.GetActionTarget(), victim);
+}
+
+TEST_F(ProcUnitIntegrationTest, CreatureHealthCanBeManipulated)
+{
+ auto* creature = CreateTestCreature(100, 99001, TEST_FACTION_HOSTILE_TO_ALL);
+
+ EXPECT_EQ(creature->GetHealth(), 10000u);
+ EXPECT_EQ(creature->GetMaxHealth(), 10000u);
+
+ creature->SetHealth(1);
+ EXPECT_EQ(creature->GetHealth(), 1u);
+
+ creature->SetHealth(0);
+ EXPECT_EQ(creature->GetHealth(), 0u);
+}
+
+TEST_F(ProcUnitIntegrationTest, FactionHostilityIsSymmetric)
+{
+ auto* c1 = CreateTestCreature(100, 99001, TEST_FACTION_HOSTILE_TO_MONSTERS);
+ auto* c2 = CreateTestCreature(101, 99002, TEST_FACTION_HOSTILE_TO_ALL);
+
+ // c2 (monster mask=8) hostile to c1's mask (player mask=1): yes
+ // c1 (player mask=1) hostile to c2's mask (monster mask=8): yes
+ ReputationRank r1 = c1->GetReactionTo(c2);
+ ReputationRank r2 = c2->GetReactionTo(c1);
+
+ EXPECT_EQ(r1, REP_HOSTILE);
+ EXPECT_EQ(r2, REP_HOSTILE);
+}
+
+} // namespace