Merge remote-tracking branch 'master' into test-staging

This commit is contained in:
Keleborn
2026-05-30 07:11:06 -07:00
64 changed files with 33685 additions and 412 deletions
+6 -4
View File
@@ -1,9 +1,11 @@
### TITLE ### TITLE
## Type(Scope/Subscope): Commit ultra short explanation ## Type(Scope/Subscope): Commit ultra short explanation
## |---- Write below the examples with a maximum of 50 characters ----| ## |---- Write below the examples with a maximum of 72 characters ----|
## Example 1: fix(DB/SAI): Missing spell to NPC Hogger ## Example 1: fix(DB/SAI): Missing spell to NPC Hogger
## Example 2: fix(CORE/Raid): Phase 2 of Ragnaros ## Example 2: feat(Scripts/Commands): Add .mail list and .mail return commands
## Example 3: feat(CORE/Commands): New GM command to do something ## Example 3: refactor(Scripts/Ulduar): Leverage DoorData and persistent data
## Example 4: fix(Scripts/SlavePens): Despawn Frozen Core after Ahune emerges
## Example 5: fix(Scripts/VioletHold): Prevent Sinclari from despawning
### DESCRIPTION ### DESCRIPTION
@@ -46,7 +48,7 @@
## More info here https://www.conventionalcommits.org/en/v1.0.0-beta.2/ ## More info here https://www.conventionalcommits.org/en/v1.0.0-beta.2/
## ======================================================= ## =======================================================
## "Scope" can be: ## "Scope" can be:
## CORE (core related, c++) ## Core (core related, c++)
## DB (database related, sql) ## DB (database related, sql)
## ======================================================= ## =======================================================
## "Subscope" is optional and depends on the nature of the commit. ## "Subscope" is optional and depends on the nature of the commit.
+5
View File
@@ -1,4 +1,9 @@
{ {
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"file": ".git_commit_template.txt"
},
],
"files.associations": { "files.associations": {
"*.dist": "properties", "*.dist": "properties",
"*.crash": "properties", "*.crash": "properties",
+74 -96
View File
@@ -1,126 +1,104 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. AzerothCore is a C++ MMORPG server emulator for World of Warcraft 3.3.5a (WotLK), built with CMake, backed by MySQL.
## Project Overview ## Agent rules
AzerothCore is an open-source MMORPG server emulator for World of Warcraft patch 3.3.5a (Wrath of the Lich King). It's a C++ project built with CMake, using MySQL for data storage. Licensed under GNU GPL v2. - **Do not configure or build unless explicitly asked.** Builds are slow (CMake + compile of a large C++ codebase) and rarely needed to make code changes.
- **Never edit SQL files outside `data/sql/updates/pending_db_*/`.** `data/sql/base/`, `data/sql/archive/`, and `data/sql/updates/db_*/` are immutable (do not modify).
- **Do not run git commands that modify repo state** (commit, branch, merge, rebase, reset, push, …) unless explicitly requested, and do not include them in plans. Read-only git (status, diff, log) is fine.
## Build Commands ## Build
### Configure and build (out-of-source build required) Out-of-source build is required (in-source is blocked by CMake).
- Skip building unless explicitly requested.
```bash ```bash
# Create build directory and configure
mkdir -p build && cd build mkdir -p build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server -DCMAKE_BUILD_TYPE=RelWithDebInfo \ cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DSCRIPTS=static -DMODULES=static -DSCRIPTS=static -DMODULES=static
make -j$(nproc) && make install
# Build (use appropriate core count)
make -j$(nproc)
make install
``` ```
### Key CMake options Compiler: **C++20** required (`CMAKE_CXX_STANDARD 20`). Useful CMake flags: `BUILD_TESTING=ON` (Google Test), `NOPCH=1` (disable precompiled headers). Full flag set in `conf/dist/config.cmake`. `compile_commands.json` is exported automatically.
- `SCRIPTS`: none, static, dynamic, minimal-static, minimal-dynamic (default: static) Tests (Google Test, in `src/test/`): configure with `-DBUILD_TESTING=ON`, then `ctest` or `./src/test/unit_tests` from the build dir.
- `MODULES`: none, static, dynamic (default: static)
- `APPS_BUILD`: none, all, auth-only, world-only (default: all)
- `TOOLS_BUILD`: none, all, db-only, maps-only (default: none)
- `BUILD_TESTING`: Enable unit tests (default: OFF)
- `USE_COREPCH` / `USE_SCRIPTPCH`: Precompiled headers (default: ON)
### Unit tests ## Repository layout
- `src/common/` — networking (Asio), crypto, config, logging, shared utilities.
- `src/server/game/` — core gameplay; compiled into worldserver.
- `src/server/scripts/` — content scripts grouped by region (`EasternKingdoms/`, `Northrend/`, …), class (`Spells/spell_mage.cpp`, …), and domain (`Commands/`, `Pet/`, `OutdoorPvP/`, `World/`).
- `src/server/database/` — DB abstraction and schema updater.
- `src/server/shared/` — code shared by auth and world servers.
- `src/server/apps/{authserver,worldserver}/` — entry points (ports 3724 and 8085).
- `src/test/` — Google Test unit tests + mocks.
- `data/sql/` — `base/` (historical schema), `updates/db_*/` (merged), `updates/pending_db_*/` (in-flight, **edit here**), `custom/` (gitignored).
- `modules/` — external modules (each a subdir with its own `CMakeLists.txt`). Disable with `-DDISABLED_AC_MODULES="mod1;mod2"`. See `modules/how_to_make_a_module.md`.
- `apps/` — helper scripts; `apps/codestyle/` holds the lint scripts (see below).
- `conf/dist/` — distributed config templates; `conf/*.conf` is gitignored.
- `deps/` — vendored third-party dependencies.
## Adding SQL updates
1. `cd data/sql/updates/pending_db_world/` (or `pending_db_auth` / `pending_db_characters`).
2. `./create_sql.sh` generates an empty `rev_<timestamp>.sql` you write into.
3. Required SQL conventions (enforced by `apps/codestyle/codestyle-sql.py`):
- Every `INSERT` must be preceded by a matching `DELETE` (idempotency).
- 4-space indent (no tabs), trailing newline, no double semicolons, no multiple blank lines.
- Tables must use the InnoDB engine.
The three databases:
- `acore_auth` — accounts, realm list, IP/account bans, session keys. Shared across all realms.
- `acore_characters` — per-character state: characters, inventory, in-progress quests, mail, guilds, arena teams, achievements. One per realm.
- `acore_world` — static game content: creature/gameobject/item/quest templates, spawn lists, loot tables, SmartAI scripts, gossip, conditions. Read-mostly; rebuilt from SQL.
## Code style
Run the linters before claiming a change is done:
```bash ```bash
# Configure with testing enabled python apps/codestyle/codestyle-cpp.py # C++
cmake .. -DBUILD_TESTING=ON python apps/codestyle/codestyle-sql.py # SQL (compares to origin/master)
make -j$(nproc)
# Run tests
./src/test/unit_tests
# or
ctest
``` ```
Tests use Google Test and live in `src/test/`. The test binary links against the `game` library. Hard rules (also enforced by CI with `-Werror`):
## Architecture - 4-space indent for C++ (tabs forbidden); 2-space for JSON/YAML/sh/ts/js. UTF-8, LF, max 80 cols, trailing newline.
- Allman braces. No braces around single-line statements. `if (x)` — never `if(x)` or `if ( x )`.
- `auto const&` (not `const auto&`); `Type const*` (not `const Type*`).
- Use `{}` format specifiers (`fmt`-style), not `%u`/`%s`.
- Use the typed helpers, not raw flag access:
- `IsPlayer()`, `IsCreature()`, `IsItem()`, … instead of `GetTypeId() == TYPEID_*`.
- `GetNpcFlags()`, `HasNpcFlag()`, `SetNpcFlag()`, `RemoveNpcFlag()`, `ReplaceAllNpcFlags()` instead of `*Flag(UNIT_NPC_FLAGS, …)`.
- `IsRefundable()`, `IsBOPTradable()`, `IsWrapped()` instead of `HasFlag(ITEM_FIELD_FLAGS, …)`.
- `HasFlag(ItemFlag)` / `HasFlag2(ItemFlag2)` / `HasFlagCu(ItemFlagsCustom)` instead of bitwise `Flags & ITEM_FLAG…`.
- `ObjectGuid::ToString().c_str()` instead of `ObjectGuid::GetCounter()`.
### Two server executables CI also runs `cppcheck`.
- **authserver** (`src/server/apps/authserver/`): Handles authentication and realm selection (port 3724)
- **worldserver** (`src/server/apps/worldserver/`): Main game server handling all gameplay (port 8085)
### Source layout (`src/`) ## Project conventions
- **`src/common/`** - Shared libraries: networking (Asio), cryptography, configuration, logging, threading, collision detection, utilities - **Logging**: `LOG_INFO("category.sub", "msg with {}", arg)` (also `LOG_WARN`, `LOG_ERROR`, `LOG_DEBUG`, `LOG_TRACE`). Categories are hierarchical, dot-separated (e.g. `server.loading`, `entities.player`, `sql.dev`). No `printf`-style; no `sLog->`; no `TC_LOG_*`. Macro in `src/common/Logging/Log.h`.
- **`src/server/game/`** - Core game logic (~52 subsystems), the heart of the worldserver - **Random**: use project helpers from `src/common/Utilities/Random.h` — `urand`, `irand`, `frand`, `rand32`, `rand_chance`, `roll_chance_f`, `roll_chance_i`. Do not use `std::rand` or `<random>` directly.
- **`src/server/scripts/`** - Content scripts (bosses, spells, commands, instances) - **Strings**: `Acore::StringFormat(fmt, args...)` (wraps `fmt::format`, `{}` placeholders) — `src/common/Utilities/StringFormat.h`.
- **`src/server/database/`** - Database abstraction layer and schema updater - **Config**: read options with `sConfigMgr->GetOption<T>("Name", default)`.
- **`src/server/shared/`** - Code shared between auth and world servers (packets, network, realm definitions) - **Namespace**: project-wide is `Acore::` (no `Trinity::` remnants — agents porting from upstream forks must rename).
- **`src/test/`** - Unit tests (Google Test) - **Long-lived references**: do not store a raw `Player*` / `Creature*` / `Unit*` past the current call/tick — the object can be removed (logout, despawn, instance unload) and the pointer dangles. Store the `ObjectGuid` and resolve at use time via `ObjectAccessor::FindPlayer(guid)`, `ObjectAccessor::GetCreature(*from, guid)`, `Map::GetCreature(guid)`, etc.
- **DB queries**: use `PreparedStatement` (via `WorldDatabase` / `CharacterDatabase` / `LoginDatabase` and the prepared-statement enums) rather than raw query strings. Reads that don't need to block the world tick go through the async path: `_queryProcessor.AddCallback(db.AsyncQuery(stmt).WithPreparedCallback(...))` (or `WithCallback` for non-prepared). Multi-statement writes wrap in `SQLTransaction` + `Execute` / `AppendPreparedStatement`.
- **Timed actions in AI**: use `EventMap` (event id → delay; simple) or `TaskScheduler` (lambdas, repeats, cancellation). Both are members of `CreatureAI`; see any boss script under `src/server/scripts/` for examples — don't roll your own tick counters.
### Key game subsystems (`src/server/game/`) ## Scripting registration
- **Entities/** - Core game objects: `Player`, `Creature`, `Unit`, `Item`, `GameObject` Scripts inherit from a `ScriptObject` subclass (`SpellScript`, `AuraScript`, `CreatureScript`, `InstanceMapScript`, `GameObjectScript`, `CommandScript`, …). Two registration styles coexist:
- **Spells/** - Spell mechanics, aura system, spell effects
- **Maps/** - Map management, grid system, instancing
- **Handlers/** - Client packet handlers (one file per system: `MovementHandler.cpp`, `SpellHandler.cpp`, etc.). These are methods on `WorldSession`
- **AI/** - Creature AI framework
- **Scripting/** - Script system with typed base classes (`ScriptObject` subclasses: `CreatureScript`, `SpellScript`, `InstanceMapScript`, `GameObjectScript`, `CommandScript`, etc.)
- **Server/** - `WorldSession` (per-player connection), `World` (global state), opcode definitions
### Scripting system - **Spell / aura scripts**: use the `RegisterSpellScript(ClassName)` (or `RegisterSpellAndAuraScriptPair(...)`) macro inside `AddSC_<name>()`.
- **Creature scripts**: prefer `RegisterCreatureAI(ClassName)` for new code; legacy zones still use `new ClassName();`. Match the surrounding pattern.
Scripts follow a registration pattern: Then declare and call `AddSC_<name>()` from the regional loader: `Spells/spells_script_loader.cpp`, `EasternKingdoms/eastern_kingdoms_script_loader.cpp`, etc.
1. Define a class inheriting from `SpellScript`, `CreatureScript`, etc.
2. Implement an `AddSC_*()` function that calls `RegisterSpellScript(ClassName)` (or similar)
3. The `AddSC_*()` is declared and called from the regional `*_script_loader.cpp`
4. Script loaders per region: `spells_script_loader.cpp`, `eastern_kingdoms_script_loader.cpp`, `northrend_script_loader.cpp`, etc.
5. Spell script files are organized by class: `spell_dk.cpp`, `spell_mage.cpp`, `spell_generic.cpp`, etc.
### Three databases **SmartAI** (data-driven creature behaviour) lives in the world DB's `smart_scripts` table — not in C++. Engine: `src/server/game/AI/SmartScripts/`. For new creature behaviour prefer SmartAI (added via the SQL update workflow); reach for `CreatureScript` only when SmartAI's event/action vocabulary isn't enough.
- **acore_auth** - Accounts, realm list, bans (`data/sql/base/db_auth/`)
- **acore_characters** - Character data, inventories, progress (`data/sql/base/db_characters/`)
- **acore_world** - Game content: creatures, items, quests, spells, loot (`data/sql/base/db_world/`)
- SQL updates go in `data/sql/updates/pending_*` with separate subdirectories per database until pull request is merged. Pending SQL files are assigned random names. **Module hooks** (e.g. `OnPlayerLogin`, `OnWorldUpdate`, `OnSpellCast`) are declared in `src/server/game/Scripting/ScriptDefines/*.h`. Implement by inheriting the matching base (`PlayerScript`, `WorldScript`, …) and registering with `new MyClass();` (or its `RegisterXxxScript` macro where one exists) inside `AddSC_<name>()`. Full hook list: https://www.azerothcore.org/wiki/hooks-script.
- SQL updates go in `data/sql/updates/` with separate subdirectories per database after their pull request is merged.
- SQL files outside the `data/sql/updates/pending_*` folders should never be updated.
### Module system Custom (non-upstream) scripts go in `src/server/scripts/Custom/` (gitignored).
External modules are loaded from the `modules/` directory. Each module is a subdirectory with its own `CMakeLists.txt`. Disable specific modules with `-DDISABLED_AC_MODULES="mod1;mod2"`. Module skeleton: https://github.com/azerothcore/skeleton-module/
### Dependencies
Bundled in `deps/`: boost, MySQL client, OpenSSL, zlib, recastnavigation (pathfinding), g3dlite (geometry), fmt, argon2, jemalloc, and others.
## Commit Message Format
Uses Conventional Commits:
```
Type(Scope/Subscope): Short description (max 50 chars)
```
- **Types**: feat, fix, refactor, style, docs, test, chore
- **Scopes**: Core (C++ changes), DB (SQL changes)
- **Examples**: `fix(Core/Spells): Fix damage calculation for Fireball`, `fix(DB/SAI): Missing spell to NPC Hogger`
## Code Style
- 4-space indentation for C++ (no tabs)
- 2-space indentation for JSON, YAML, shell scripts
- UTF-8 encoding, LF line endings
- Max 80 character line length
- No braces around single-line statements
- Use {} to parse variables into output instead of %u etc.
- CI enforces code style checks and compiles with `-Werror`
## PR Requirements
- AI tool usage must be disclosed in PRs
- In-game testing expected
- Changes to generic code require regression testing of related systems
@@ -0,0 +1,9 @@
-- DB update 2026_05_17_00 -> 2026_05_24_00
-- RBAC permission for .pet rename (sec 3+, Admin role 196).
DELETE FROM `rbac_permissions` WHERE `id` = 922;
INSERT INTO `rbac_permissions` (`id`, `name`) VALUES
(922, 'Command: pet rename');
DELETE FROM `rbac_linked_permissions` WHERE `linkedId` = 922;
INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES
(196, 922);
@@ -0,0 +1,3 @@
-- DB update 2026_05_22_00 -> 2026_05_23_00
--
UPDATE `creature_template_addon` SET `auras` = '' WHERE (`entry` IN (31841, 31842));
@@ -0,0 +1,7 @@
-- DB update 2026_05_23_00 -> 2026_05_24_00
--
DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 22) AND (`SourceGroup` = 4) AND (`SourceEntry` = 30886);
INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES
(22, 4, 30886, 0, 0, 23, 1, 4526, 0, 0, 1, 0, 0, '', 'Despawn Subjugated Iskalder when outside quest areas'),
(22, 4, 30886, 0, 0, 23, 1, 4498, 0, 0, 1, 0, 0, '', 'Despawn Subjugated Iskalder when outside quest areas'),
(22, 4, 30886, 0, 0, 23, 1, 4496, 0, 0, 1, 0, 0, '', 'Despawn Subjugated Iskalder when outside quest areas');
@@ -0,0 +1,3 @@
-- DB update 2026_05_24_00 -> 2026_05_24_01
--
UPDATE `creature_template` SET `CreatureImmunitiesId` = -375 WHERE `entry` IN (30183, 16486);
@@ -0,0 +1,6 @@
-- DB update 2026_05_24_01 -> 2026_05_24_02
-- acore_string entries for .pet rename command.
DELETE FROM `acore_string` WHERE `entry` IN (35453, 35454);
INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES
(35453, 'Invalid pet name: {}.', '잘못된 펫 이름입니다: {}.', 'Nom de familier invalide : {}.', 'Ungültiger Begleitername: {}.', '无效的宠物名称: {}。', '無效的寵物名稱:{}。', 'Nombre de mascota no válido: {}.', 'Nombre de mascota no válido: {}.', 'Недопустимое имя питомца: {}.'),
(35454, 'Renamed pet {0} ({1} -> {2}) for player {3} ({4}).', '플레이어 {3} ({4})의 펫 {0}의 이름을 {1}에서 {2}(으)로 변경했습니다.', 'Familier {0} renommé ({1} -> {2}) pour le joueur {3} ({4}).', 'Begleiter {0} umbenannt ({1} -> {2}) für Spieler {3} ({4}).', '已将玩家 {3} ({4}) 的宠物 {0} 重命名 ({1} -> {2})。', '已將玩家 {3} ({4}) 的寵物 {0} 重新命名({1} -> {2})。', 'Mascota {0} renombrada ({1} -> {2}) para el jugador {3} ({4}).', 'Mascota {0} renombrada ({1} -> {2}) para el jugador {3} ({4}).', 'Питомец {0} переименован ({1} -> {2}) у игрока {3} ({4}).');
@@ -0,0 +1,8 @@
-- DB update 2026_05_24_02 -> 2026_05_25_00
-- Hide Whispering Wind (30848) during Wintergrasp battle.
-- Aura 52107 phases the creature out while war is active; the other five
-- small WG elementals (30842, 30845, 30846, 30847, 30849) already have it
-- in creature_template_addon — 30848 was missed.
DELETE FROM `creature_template_addon` WHERE `entry` = 30848;
INSERT INTO `creature_template_addon` (`entry`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES
(30848, 0, 0, 0, 1, 0, 0, '52107');
@@ -0,0 +1,7 @@
-- DB update 2026_05_25_00 -> 2026_05_26_00
--
-- Change Target CREATURE_DISTANCE to CLOSEST_FRIENDLY
DELETE FROM `smart_scripts` WHERE (`entryorguid` = 23960) AND (`source_type` = 0) AND (`id` IN (1, 2));
INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES
(23960, 0, 1, 0, 0, 0, 100, 2, 5000, 7000, 14000, 17000, 0, 0, 11, 42740, 0, 0, 0, 0, 0, 26, 30, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Runecaster - In Combat - Cast \'Njord`s Rune of Protection\' (Normal Dungeon)'),
(23960, 0, 2, 0, 0, 0, 100, 4, 5000, 7000, 14000, 17000, 0, 0, 11, 59616, 0, 0, 0, 0, 0, 26, 30, 0, 0, 0, 0, 0, 0, 0, 'Dragonflayer Runecaster - In Combat - Cast \'Njord`s Rune of Protection\' (Heroic Dungeon)');
@@ -0,0 +1,8 @@
-- DB update 2026_05_26_00 -> 2026_05_28_00
--
DELETE FROM `spawn_group` WHERE `spawnId` IN (153154, 153155, 153156, 153157);
INSERT INTO `spawn_group` (`groupId`, `spawnType`, `spawnId`) VALUES
(1, 0, 153154),
(1, 0, 153155),
(1, 0, 153156),
(1, 0, 153157);
@@ -0,0 +1,14 @@
-- DB update 2026_05_28_00 -> 2026_05_30_00
-- Hellfire Channelers formation: groupAI = 3 (MEMBER_ASSIST_LEADER | LEADER_ASSIST_MEMBER), pack-aggro only. Wipe handling lives in the instance script's hard reset.
DELETE FROM `creature_formations` WHERE `leaderGUID`=90978;
INSERT INTO `creature_formations`
(`leaderGUID`,`memberGUID`,`dist`,`angle`,`groupAI`,`point_1`,`point_2`)
VALUES
(90978, 90978, 0, 0, 3, 0, 0),
(90978, 90979, 0, 0, 3, 0, 0),
(90978, 90980, 0, 0, 3, 0, 0),
(90978, 90981, 0, 0, 3, 0, 0),
(90978, 90982, 0, 0, 3, 0, 0);
-- Hellfire Channeler SAI: drop id=6. Death tracking moved to the instance script (OnUnitDeath).
DELETE FROM `smart_scripts` WHERE `entryorguid`=17256 AND `source_type`=0 AND `id`=6;
@@ -0,0 +1,8 @@
-- DB update 2026_05_30_00 -> 2026_05_30_01
--
DROP TABLE IF EXISTS `spell_cone`;
CREATE TABLE `spell_cone` (
`ID` INT UNSIGNED NOT NULL COMMENT 'Spell ID',
`ConeDegrees` SMALLINT NOT NULL DEFAULT '60' COMMENT 'Cone angle in degrees',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+107
View File
@@ -0,0 +1,107 @@
-- DB update 2026_05_30_01 -> 2026_05_30_02
--
DELETE FROM `spell_cone` WHERE `ID` > 0;
INSERT INTO `spell_cone` VALUES
(5708, 90),
(8374, 90),
-- (9457, 90), -- Tharnariun's Heal
(11019, 90),
(12882, 90),
(12887, 90),
-- (15529, 359), -- Gout of Flame
(15847, -120),
(16094, 90),
(16099, 90),
(16169, 90),
(16340, 90),
(16350, 90),
(16359, 90),
(16390, 90),
(16396, 120),
(18144, 90),
(18435, 120),
(18500, 120),
(19630, 120),
(19641, 120),
(19813, 180),
(20667, 90),
(20712, 120),
(20716, 90),
(20717, 90),
(21071, 120),
(21099, 120),
(21189, 90),
(21333, 180),
(22479, 90),
(22539, 120),
(22558, 90),
(22559, 90),
(22560, 90),
(22561, 90),
-- (22600, 120), -- Force Reactive Disk
(22642, 90),
-- (22682, 120), -- Shadow Flame
(22833, 120),
(22945, 120),
-- (22993, 120), -- Shadow Flame
-- (23187, 120), -- Frost Burn
-- (23189, 120), -- Frost Burn
-- (23308, 120), -- Incinerate
-- (23309, 120), -- Incinerate
-- (23310, 120), -- Time Lapse
-- (23311, 120), -- Time Lapse
-- (23312, 120), -- Time Lapse
-- (23313, 120), -- Corrosive Acid
-- (23314, 120), -- Corrosive Acid
-- (23315, 120), -- Ignite Flesh
-- (23316, 120), -- Ignite Flesh
(23339, 120),
(23364, -120),
(23461, 120),
(23618, 90),
(23919, 120),
(24192, 120),
(24682, 120),
(24818, 120),
(24821, 45),
(24822, 135),
(24823, 90),
(24835, 180),
(24836, 225),
(24837, 270),
(24838, 315),
(24839, 120),
(24933, 7),
-- (25024, 45), -- Drop Mine
-- (25026, 45), Activate MG Turret
-- (25027, 30), -- Flamethrower
(25029, 30), -- Flamethrower
(25030, 90), -- Shoot Rocket
-- (25031, 45), -- Shoot Missile
-- (25032, 10), -- Machine Gun
-- (25060, 90), -- Updraft
(25145, 90),
(25149, 90),
(25150, 90),
(25322, 90),
(25653, -120),
(25993, 120),
(26025, 180), -- Impale used by 15233 Vekniss Guardian; Knockback AoE, Bleed Cone
-- (26027, 180), -- Knockback
(26029, 5), -- Dark Glare
-- (27766, 90), -- Cannon
(27807, 90),
(28127, 90),
(28157, 45),
(28168, 120),
(28405, 180),
(28438, 180),
(28459, 90),
-- (28460, 90), -- Wail of Souls
-- (28469, 360), -- Ghost Vision
(28741, 120),
(28783, 30),
-- (29998, 180), -- Decrepit Fever
(30043, 90),
(30056, 180),
(30732, 180);
@@ -0,0 +1,8 @@
-- DB update 2026_05_30_02 -> 2026_05_30_03
--
-- 25029 Flamethrower TARGET_UNIT_CONE_ENTRY
-- 25030 Shoot Rocket TARGET_UNIT_CONE_ENTRY
DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceEntry` IN (25029, 25030)) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 15328) AND (`ConditionValue3` = 0);
INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES
(13, 5, 25029, 0, 0, 31, 0, 3, 15328, 0, 0, 0, 0, '', 'target must be \'Steam Stonk\''),
(13, 1, 25030, 0, 0, 31, 0, 3, 15328, 0, 0, 0, 0, '', 'target must be \'Steam Stonk\'');
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -17,6 +17,10 @@ if( UNIX )
# READLINE_LIBRARY - full path to the READLINE library # READLINE_LIBRARY - full path to the READLINE library
find_path(READLINE_INCLUDE_DIR readline/readline.h) find_path(READLINE_INCLUDE_DIR readline/readline.h)
find_library(READLINE_LIBRARY NAMES readline) find_library(READLINE_LIBRARY NAMES readline)
# macOS: Homebrew readline is keg-only, so this finds the SDK's libedit stub
# and worldserver fails to link. After `brew install readline`, configure with
# -DREADLINE_LIBRARY=/opt/homebrew/opt/readline/lib/libreadline.dylib and
# -DREADLINE_INCLUDE_DIR=/opt/homebrew/opt/readline/include
message(STATUS "Found Readline library: ${READLINE_LIBRARY}") message(STATUS "Found Readline library: ${READLINE_LIBRARY}")
message(STATUS "Include dir is: ${READLINE_INCLUDE_DIR}") message(STATUS "Include dir is: ${READLINE_INCLUDE_DIR}")
@@ -3677,6 +3677,31 @@ Wintergrasp.CrashRestartTimer = 10
Wintergrasp.SkipBattleSessionCount = 3500 Wintergrasp.SkipBattleSessionCount = 3500
#
# Wintergrasp.KickVoAPlayers
# Description: Kick players from Vault of Archavon and freeze its bosses
# in the minutes leading up to a Wintergrasp battle, and
# block new entries during wartime / the 10-minute warmup.
# Requires a server restart to take effect (not reloadable).
# Default: 1 - (Enabled, players are warned then teleported to their
# homebind before the battle starts)
# 0 - (Disabled, players stay inside and bosses behave
# normally during Wintergrasp)
Wintergrasp.KickVoAPlayers = 1
#
# Wintergrasp.EssenceBothFactions
# Description: Grant the "Essence of Wintergrasp" buff to both factions
# during peacetime, regardless of who controls the keep.
# The wartime suppression still applies; this only affects
# who receives the buff once the battle is over.
# Default: 0 - (Disabled, only the defending faction gets the buff)
# 1 - (Enabled, attackers and defenders both get the buff,
# so both factions can access Vault of Archavon)
Wintergrasp.EssenceBothFactions = 0
# #
################################################################################################### ###################################################################################################
+1
View File
@@ -683,6 +683,7 @@ enum RBACPermissions
RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_ENTRY = 919, RBAC_PERM_COMMAND_RESPAWN_GAMEOBJECT_ENTRY = 919,
RBAC_PERM_COMMAND_DEBUG_INFO = 920, RBAC_PERM_COMMAND_DEBUG_INFO = 920,
RBAC_PERM_COMMAND_DEBUG_COSMETIC = 921, RBAC_PERM_COMMAND_DEBUG_COSMETIC = 921,
RBAC_PERM_COMMAND_PET_RENAME = 922,
// custom permissions 1000+ // custom permissions 1000+
RBAC_PERM_MAX RBAC_PERM_MAX
}; };
+115 -105
View File
@@ -74,12 +74,27 @@ Battlefield::~Battlefield()
CapturePoints.clear(); CapturePoints.clear();
} }
void Battlefield::RemovePlayerFromTracking(ObjectGuid playerGuid)
{
for (uint8 i = 0; i < PVP_TEAMS_COUNT; ++i)
{
InvitedPlayers[i].erase(playerGuid);
PlayersInQueue[i].erase(playerGuid);
PlayersWillBeKick[i].erase(playerGuid);
Players[i].erase(playerGuid);
}
}
void Battlefield::HandlePlayerEnterZone(Player* player, uint32 /*zone*/) void Battlefield::HandlePlayerEnterZone(Player* player, uint32 /*zone*/)
{ {
RemovePlayerFromTracking(player->GetGUID());
// Allow scripts to adjust the player's effective team or appearance before // Allow scripts to adjust the player's effective team or appearance before
// any team-based battlefield containers (such as player lists or queues) are updated. // any team-based battlefield containers (such as player lists or queues) are updated.
sScriptMgr->OnBattlefieldPlayerEnterZone(this, player); sScriptMgr->OnBattlefieldPlayerEnterZone(this, player);
TryRejoinAfterLogout(player); // relog: auto-rejoin, skip invite below
// Xinef: do not invite players on taxi // Xinef: do not invite players on taxi
if (!player->IsInFlight()) if (!player->IsInFlight())
{ {
@@ -111,12 +126,20 @@ void Battlefield::HandlePlayerEnterZone(Player* player, uint32 /*zone*/)
void Battlefield::HandlePlayerLeaveZone(Player* player, uint32 /*zone*/) void Battlefield::HandlePlayerLeaveZone(Player* player, uint32 /*zone*/)
{ {
// Logout still runs full leave-war cleanup, but marks the player for grace-window auto-rejoin.
bool const isLogout = player->GetSession() && player->GetSession()->PlayerLogout();
if (IsWarTime()) if (IsWarTime())
{ {
// If the player is participating to the battle // If the player is participating to the battle
if (PlayersInWar[player->GetTeamId()].erase(player->GetGUID())) if (PlayersInWar[player->GetTeamId()].erase(player->GetGUID()))
{ {
player->GetSession()->SendBfLeaveMessage(BattleId); if (isLogout)
LogoutGracePlayers[player->GetTeamId()][player->GetGUID()] =
GameTime::GetGameTime().count() + LOGOUT_GRACE_SECONDS;
else
player->GetSession()->SendBfLeaveMessage(BattleId);
if (Group* group = player->GetGroup()) // Remove the player from the raid group if (Group* group = player->GetGroup()) // Remove the player from the raid group
if (group->isBFGroup()) if (group->isBFGroup())
group->RemoveMember(player->GetGUID()); group->RemoveMember(player->GetGUID());
@@ -129,13 +152,7 @@ void Battlefield::HandlePlayerLeaveZone(Player* player, uint32 /*zone*/)
for (BfCapturePoint* cp : CapturePoints) for (BfCapturePoint* cp : CapturePoints)
cp->HandlePlayerLeave(player); cp->HandlePlayerLeave(player);
for (uint8 i = 0; i < PVP_TEAMS_COUNT; ++i) RemovePlayerFromTracking(player->GetGUID());
{
InvitedPlayers[i].erase(player->GetGUID());
PlayersInQueue[i].erase(player->GetGUID());
PlayersWillBeKick[i].erase(player->GetGUID());
Players[i].erase(player->GetGUID());
}
SendRemoveWorldStates(player); SendRemoveWorldStates(player);
RemovePlayerFromResurrectQueue(player->GetGUID()); RemovePlayerFromResurrectQueue(player->GetGUID());
OnPlayerLeaveZone(player); OnPlayerLeaveZone(player);
@@ -235,6 +252,9 @@ void Battlefield::InvitePlayerToWar(Player* player)
if (!player) if (!player)
return; return;
if (player->IsGameMaster())
return;
/// @todo : needed ? /// @todo : needed ?
if (player->IsInFlight()) if (player->IsInFlight())
return; return;
@@ -292,7 +312,14 @@ void Battlefield::KickPlayerFromBattlefield(ObjectGuid guid)
if (Player* player = ObjectAccessor::FindPlayer(guid)) if (Player* player = ObjectAccessor::FindPlayer(guid))
if (player->GetZoneId() == GetZoneId() && !player->IsGameMaster() if (player->GetZoneId() == GetZoneId() && !player->IsGameMaster()
&& !PlayersInWar[player->GetTeamId()].count(guid)) && !PlayersInWar[player->GetTeamId()].count(guid))
{
player->TeleportTo(KickPosition); player->TeleportTo(KickPosition);
// Eagerly drop zone tracking: the teleport's zone change does not
// propagate until the next Player::Update, so callers iterating
// Players[team] in the same tick would otherwise still see them.
for (uint8 i = 0; i < PVP_TEAMS_COUNT; ++i)
Players[i].erase(guid);
}
} }
void Battlefield::StartBattle() void Battlefield::StartBattle()
@@ -304,6 +331,7 @@ void Battlefield::StartBattle()
{ {
PlayersInWar[team].clear(); PlayersInWar[team].clear();
Groups[team].clear(); Groups[team].clear();
LogoutGracePlayers[team].clear();
} }
Timer = BattleTime; Timer = BattleTime;
@@ -318,11 +346,25 @@ void Battlefield::StartBattle()
_scheduler.Schedule(1s, BATTLEFIELD_TIMER_GROUP_WAR, [this](TaskContext context) _scheduler.Schedule(1s, BATTLEFIELD_TIMER_GROUP_WAR, [this](TaskContext context)
{ {
time_t now = GameTime::GetGameTime().count(); time_t const now = GameTime::GetGameTime().count();
// Send eject so the 3.3.5 client closes its popup (it does not on its
// own when the timer hits zero), then drop the entry and teleport.
for (uint8 team = 0; team < PVP_TEAMS_COUNT; ++team) for (uint8 team = 0; team < PVP_TEAMS_COUNT; ++team)
for (PlayerTimerMap::value_type const& pair : InvitedPlayers[team]) {
if (pair.second <= now) std::vector<ObjectGuid> expired;
KickPlayerFromBattlefield(pair.first); for (auto const& [guid, expireAt] : InvitedPlayers[team])
if (expireAt <= now)
expired.push_back(guid);
for (ObjectGuid const& guid : expired)
{
if (Player* player = ObjectAccessor::FindPlayer(guid))
player->GetSession()->SendBfLeaveMessage(BattleId, BF_LEAVE_REASON_EXITED);
InvitedPlayers[team].erase(guid);
KickPlayerFromBattlefield(guid);
}
}
InvitePlayersInZoneToWar(); InvitePlayersInZoneToWar();
for (uint8 team = 0; team < PVP_TEAMS_COUNT; ++team) for (uint8 team = 0; team < PVP_TEAMS_COUNT; ++team)
@@ -365,6 +407,14 @@ void Battlefield::EndBattle(bool endByTimer)
OnBattleEnd(endByTimer); OnBattleEnd(endByTimer);
sScriptMgr->OnBattlefieldWarEnd(this, endByTimer); sScriptMgr->OnBattlefieldWarEnd(this, endByTimer);
for (uint8 team = 0; team < PVP_TEAMS_COUNT; ++team)
{
for (ObjectGuid const& guid : Groups[team])
if (Group* group = sGroupMgr->GetGroupByGUID(guid.GetCounter()))
group->Disband();
Groups[team].clear();
}
// Reset battlefield timer // Reset battlefield timer
Timer = NoWarBattleTime; Timer = NoWarBattleTime;
SendInitWorldStatesToAll(); SendInitWorldStatesToAll();
@@ -415,13 +465,21 @@ void Battlefield::PlayerAcceptInviteToWar(Player* player)
if (!IsWarTime()) if (!IsWarTime())
return; return;
// Reject unknown / expired invites; the kick task only sweeps every 5s.
TeamId const invitedTeam = player->GetTeamId();
auto itr = InvitedPlayers[invitedTeam].find(player->GetGUID());
if (itr == InvitedPlayers[invitedTeam].end()
|| itr->second <= GameTime::GetGameTime().count())
return;
sScriptMgr->OnBattlefieldPlayerJoinWar(this, player); sScriptMgr->OnBattlefieldPlayerJoinWar(this, player);
if (AddOrSetPlayerToCorrectBfGroup(player)) if (AddOrSetPlayerToCorrectBfGroup(player))
{ {
player->GetSession()->SendBfEntered(BattleId); player->GetSession()->SendBfEntered(BattleId);
PlayersInWar[player->GetTeamId()].insert(player->GetGUID()); PlayersInWar[player->GetTeamId()].insert(player->GetGUID());
InvitedPlayers[player->GetTeamId()].erase(player->GetGUID()); // Use pre-hook team: JoinWar may have just reassigned GetTeamId().
InvitedPlayers[invitedTeam].erase(player->GetGUID());
if (player->isAFK()) if (player->isAFK())
player->ToggleAFK(); player->ToggleAFK();
@@ -549,6 +607,41 @@ bool Battlefield::AddOrSetPlayerToCorrectBfGroup(Player* player)
return true; return true;
} }
void Battlefield::TryRejoinAfterLogout(Player* player)
{
ObjectGuid const guid = player->GetGUID();
time_t const now = GameTime::GetGameTime().count();
// Consume the marker and honor its grace window (check both teams; team may have changed).
bool pending = false;
for (uint8 team = 0; team < PVP_TEAMS_COUNT; ++team)
if (auto itr = LogoutGracePlayers[team].find(guid); itr != LogoutGracePlayers[team].end())
{
pending = itr->second > now;
LogoutGracePlayers[team].erase(itr);
}
// Vacancy gate mirrors HandlePlayerEnterZone (full team -> queue path). Pre-hook:
// we can't abort after JoinWar, which may already have mutated module state.
if (!pending || !IsWarTime() || !HasWarVacancy(player->GetTeamId()))
return;
if (Group* current = player->GetGroup())
if (current->isBGGroup() || current->isBFGroup())
return;
// Rejoin via the normal join path: firing JoinWar lets modules rebuild
// per-session (Player*-keyed) state and pick the team before the raid bind.
sScriptMgr->OnBattlefieldPlayerJoinWar(this, player);
if (AddOrSetPlayerToCorrectBfGroup(player))
{
player->GetSession()->SendBfEntered(BattleId);
PlayersInWar[player->GetTeamId()].insert(guid);
OnPlayerJoinWar(player);
}
}
BfGraveyard* Battlefield::GetGraveyardById(uint32 id) const BfGraveyard* Battlefield::GetGraveyardById(uint32 id) const
{ {
if (id < GraveyardList.size()) if (id < GraveyardList.size())
@@ -590,34 +683,22 @@ GraveyardStruct const* Battlefield::GetClosestGraveyard(Player* player)
return nullptr; return nullptr;
} }
void Battlefield::AddPlayerToResurrectQueue(ObjectGuid npcGuid, ObjectGuid playerGuid) void Battlefield::AddPlayerToResurrectQueue(ObjectGuid /*npcGuid*/, ObjectGuid playerGuid)
{ {
for (BfGraveyard* gy : GraveyardList) Player* player = ObjectAccessor::FindPlayer(playerGuid);
{ if (!player)
if (!gy) return;
continue;
if (gy->HasNpc(npcGuid)) player->CastSpell(player, SPELL_WAITING_FOR_RESURRECT, true);
{
gy->AddPlayer(playerGuid);
break;
}
}
} }
void Battlefield::RemovePlayerFromResurrectQueue(ObjectGuid playerGuid) void Battlefield::RemovePlayerFromResurrectQueue(ObjectGuid playerGuid)
{ {
for (BfGraveyard* gy : GraveyardList) Player* player = ObjectAccessor::FindPlayer(playerGuid);
{ if (!player)
if (!gy) return;
continue;
if (gy->HasPlayer(playerGuid)) player->RemoveAurasDueToSpell(SPELL_WAITING_FOR_RESURRECT);
{
gy->RemovePlayer(playerGuid);
break;
}
}
} }
void Battlefield::SendAreaSpiritHealerQueryOpcode(Player* player, ObjectGuid const& guid) void Battlefield::SendAreaSpiritHealerQueryOpcode(Player* player, ObjectGuid const& guid)
@@ -663,80 +744,9 @@ float BfGraveyard::GetDistance(Player* player)
return player->GetDistance2d(safeLoc->x, safeLoc->y); return player->GetDistance2d(safeLoc->x, safeLoc->y);
} }
void BfGraveyard::AddPlayer(ObjectGuid playerGuid)
{
if (!ResurrectQueue.count(playerGuid))
{
ResurrectQueue.insert(playerGuid);
if (Player* player = ObjectAccessor::FindPlayer(playerGuid))
player->CastSpell(player, SPELL_WAITING_FOR_RESURRECT, true);
}
}
void BfGraveyard::RemovePlayer(ObjectGuid playerGuid)
{
ResurrectQueue.erase(ResurrectQueue.find(playerGuid));
if (Player* player = ObjectAccessor::FindPlayer(playerGuid))
player->RemoveAurasDueToSpell(SPELL_WAITING_FOR_RESURRECT);
}
void BfGraveyard::Resurrect()
{
if (ResurrectQueue.empty())
return;
for (ObjectGuid const& guid : ResurrectQueue)
{
// Get player object from his guid
Player* player = ObjectAccessor::FindPlayer(guid);
if (!player)
continue;
// Check if the player is in world and on the good graveyard
if (player->IsInWorld())
if (Unit* spirit = ObjectAccessor::GetCreature(*player, SpiritGuide[ControlTeam]))
spirit->CastSpell(spirit, SPELL_SPIRIT_HEAL, true);
// Resurrect player
player->CastSpell(player, SPELL_RESURRECTION_VISUAL, true);
player->ResurrectPlayer(1.0f);
player->CastSpell(player, 6962, true);
player->CastSpell(player, SPELL_SPIRIT_HEAL_MANA, true);
player->SpawnCorpseBones(false);
}
ResurrectQueue.clear();
}
// For changing graveyard control
void BfGraveyard::GiveControlTo(TeamId team) void BfGraveyard::GiveControlTo(TeamId team)
{ {
ControlTeam = team; ControlTeam = team;
// Teleport to other graveyard, players which were on this graveyard
RelocateDeadPlayers();
}
void BfGraveyard::RelocateDeadPlayers()
{
GraveyardStruct const* closestGrave = nullptr;
for (ObjectGuid const& guid : ResurrectQueue)
{
Player* player = ObjectAccessor::FindPlayer(guid);
if (!player)
continue;
if (closestGrave)
player->TeleportTo(player->GetMapId(), closestGrave->x, closestGrave->y, closestGrave->z, player->GetOrientation());
else
{
closestGrave = Bf->GetClosestGraveyard(player);
if (closestGrave)
player->TeleportTo(player->GetMapId(), closestGrave->x, closestGrave->y, closestGrave->z, player->GetOrientation());
}
}
} }
Creature* Battlefield::SpawnCreature(uint32 entry, Position pos, TeamId teamId) Creature* Battlefield::SpawnCreature(uint32 entry, Position pos, TeamId teamId)
+12 -15
View File
@@ -169,18 +169,6 @@ public:
// Set spirit service for the graveyard // Set spirit service for the graveyard
void SetSpirit(Creature* spirit, TeamId team); void SetSpirit(Creature* spirit, TeamId team);
// Add a player to the graveyard
void AddPlayer(ObjectGuid playerGuid);
// Remove a player from the graveyard
void RemovePlayer(ObjectGuid playerGuid);
// Resurrect players
void Resurrect();
// Move players waiting to that graveyard on the nearest one
void RelocateDeadPlayers();
// Check if this graveyard has a spirit guide // Check if this graveyard has a spirit guide
bool HasNpc(ObjectGuid guid) bool HasNpc(ObjectGuid guid)
{ {
@@ -190,8 +178,7 @@ public:
return (SpiritGuide[0] == guid || SpiritGuide[1] == guid); return (SpiritGuide[0] == guid || SpiritGuide[1] == guid);
} }
// Check if a player is in this graveyard's resurrect queue ObjectGuid GetSpiritGuide(TeamId team) const { return SpiritGuide[team]; }
bool HasPlayer(ObjectGuid guid) const { return ResurrectQueue.find(guid) != ResurrectQueue.end(); }
// Get the graveyard's ID. // Get the graveyard's ID.
uint32 GetGraveyardId() const { return GraveyardId; } uint32 GetGraveyardId() const { return GraveyardId; }
@@ -200,7 +187,6 @@ protected:
TeamId ControlTeam; TeamId ControlTeam;
uint32 GraveyardId; uint32 GraveyardId;
ObjectGuid SpiritGuide[2]; ObjectGuid SpiritGuide[2];
GuidUnorderedSet ResurrectQueue;
Battlefield* Bf; Battlefield* Bf;
}; };
@@ -292,6 +278,11 @@ public:
/// Force player to join a battlefield group /// Force player to join a battlefield group
bool AddOrSetPlayerToCorrectBfGroup(Player* player); bool AddOrSetPlayerToCorrectBfGroup(Player* player);
/// Auto-rejoin a player who relogged within the grace window after a mid-war
/// logout, via the normal join hooks. No-op without a pending logout marker.
/// Called from HandlePlayerEnterZone (which fires on the post-login zone set).
void TryRejoinAfterLogout(Player* player);
// Graveyard methods // Graveyard methods
// Find which graveyard the player must be teleported to to be resurrected by spiritguide // Find which graveyard the player must be teleported to to be resurrected by spiritguide
GraveyardStruct const* GetClosestGraveyard(Player* player); GraveyardStruct const* GetClosestGraveyard(Player* player);
@@ -395,6 +386,10 @@ protected:
GuidUnorderedSet PlayersInWar[PVP_TEAMS_COUNT]; // Players in WG combat GuidUnorderedSet PlayersInWar[PVP_TEAMS_COUNT]; // Players in WG combat
PlayerTimerMap InvitedPlayers[PVP_TEAMS_COUNT]; PlayerTimerMap InvitedPlayers[PVP_TEAMS_COUNT];
PlayerTimerMap PlayersWillBeKick[PVP_TEAMS_COUNT]; PlayerTimerMap PlayersWillBeKick[PVP_TEAMS_COUNT];
// Mid-war logouts: GUID -> timestamp until which a relog auto-rejoins the war.
PlayerTimerMap LogoutGracePlayers[PVP_TEAMS_COUNT];
static constexpr uint32 LOGOUT_GRACE_SECONDS = 120; // relog auto-rejoin window
// Variables that must exist for each battlefield // Variables that must exist for each battlefield
uint32 TypeId; // See enum BattlefieldTypes uint32 TypeId; // See enum BattlefieldTypes
@@ -444,6 +439,8 @@ protected:
/// Returns true if the player is already tracked as actively in the war or invited to join it. /// Returns true if the player is already tracked as actively in the war or invited to join it.
bool IsPlayerInWarOrInvited(Player* player) const; bool IsPlayerInWarOrInvited(Player* player) const;
void RemovePlayerFromTracking(ObjectGuid playerGuid);
// Player-iteration helpers: resolve each GUID to a live Player* and call fn(player). // Player-iteration helpers: resolve each GUID to a live Player* and call fn(player).
// Using templates avoids std::function overhead and works naturally with lambdas. // Using templates avoids std::function overhead and works naturally with lambdas.
template<typename Func> template<typename Func>
@@ -112,6 +112,13 @@ Battlefield* BattlefieldMgr::GetBattlefieldByBattleId(uint32 battleId)
return nullptr; return nullptr;
} }
bool BattlefieldMgr::IsWintergraspAttackerVictory()
{
if (Battlefield* bf = GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG))
return static_cast<BattlefieldWG*>(bf)->IsLastBattleAttackerVictory();
return false;
}
void BattlefieldMgr::Update(uint32 diff) void BattlefieldMgr::Update(uint32 diff)
{ {
_updateTimer += diff; _updateTimer += diff;
@@ -47,6 +47,9 @@ public:
Battlefield* GetBattlefieldToZoneId(uint32 zoneId); Battlefield* GetBattlefieldToZoneId(uint32 zoneId);
Battlefield* GetBattlefieldByBattleId(uint32 battleId); Battlefield* GetBattlefieldByBattleId(uint32 battleId);
// True iff the most recent Wintergrasp battle ended with the keep captured.
[[nodiscard]] bool IsWintergraspAttackerVictory();
ZoneScript* GetZoneScript(uint32 zoneId); ZoneScript* GetZoneScript(uint32 zoneId);
void AddZone(uint32 zoneId, Battlefield* handle); void AddZone(uint32 zoneId, Battlefield* handle);
@@ -193,9 +193,39 @@ bool BattlefieldWG::SetupBattlefield()
_scheduler.Schedule(Milliseconds(RESURRECTION_INTERVAL), _scheduler.Schedule(Milliseconds(RESURRECTION_INTERVAL),
BATTLEFIELD_TIMER_GROUP_RESURRECT, [this](TaskContext context) BATTLEFIELD_TIMER_GROUP_RESURRECT, [this](TaskContext context)
{ {
for (BfGraveyard* gy : GraveyardList) ForEachPlayerInZone([this](Player* player)
if (gy) {
gy->Resurrect(); if (!player->HasAura(SPELL_WAITING_FOR_RESURRECT))
return;
TeamId team = player->GetTeamId();
Unit* closestSpirit = nullptr;
float closestDist = -1.0f;
for (BfGraveyard* gy : GraveyardList)
{
if (!gy)
continue;
Unit* spirit = ObjectAccessor::GetCreature(*player, gy->GetSpiritGuide(team));
if (!spirit)
continue;
float dist = player->GetDistance(spirit);
if (closestDist < 0.0f || dist < closestDist)
{
closestDist = dist;
closestSpirit = spirit;
}
}
if (closestSpirit)
closestSpirit->CastSpell(closestSpirit, SPELL_SPIRIT_HEAL, true);
player->CastSpell(player, SPELL_RESURRECTION_VISUAL, true);
player->ResurrectPlayer(1.0f);
player->CastSpell(player, 6962, true);
player->CastSpell(player, SPELL_SPIRIT_HEAL_MANA, true);
player->SpawnCorpseBones(false);
player->RemoveAurasDueToSpell(SPELL_WAITING_FOR_RESURRECT);
});
context.Repeat(); context.Repeat();
}); });
@@ -266,19 +296,7 @@ void BattlefieldWG::OnBattleStart()
capturePoint->SetCapturePointData(capturePoint->GetCapturePointGo(), capturePoint->SetCapturePointData(capturePoint->GetCapturePointGo(),
capturePoint->GetCapturePointGo()->GetEntry() == GO_WINTERGRASP_FACTORY_BANNER_SE || capturePoint->GetCapturePointGo()->GetEntry() == GO_WINTERGRASP_FACTORY_BANNER_SW ? GetAttackerTeam() : GetDefenderTeam()); capturePoint->GetCapturePointGo()->GetEntry() == GO_WINTERGRASP_FACTORY_BANNER_SE || capturePoint->GetCapturePointGo()->GetEntry() == GO_WINTERGRASP_FACTORY_BANNER_SW ? GetAttackerTeam() : GetDefenderTeam());
for (uint8 team = 0; team < 2; ++team) SendInitWorldStatesToAll();
for (ObjectGuid const& guid : Players[team])
{
// Kick player in orb room, TODO: offline player ?
if (Player* player = ObjectAccessor::FindPlayer(guid))
{
float x, y, z;
player->GetPosition(x, y, z);
if (5500 > x && x > 5392 && y < 2880 && y > 2800 && z < 480)
player->TeleportTo(MAP_NORTHREND, 5349.8686f, 2838.481f, 409.240f, 0.046328f);
SendInitWorldStatesTo(player);
}
}
// Initialize vehicle counter // Initialize vehicle counter
UpdateCounterVehicle(true); UpdateCounterVehicle(true);
// Send start warning to all players // Send start warning to all players
@@ -348,6 +366,9 @@ void BattlefieldWG::CapturePointTaken(uint32 areaId)
void BattlefieldWG::OnBattleEnd(bool endByTimer) void BattlefieldWG::OnBattleEnd(bool endByTimer)
{ {
// Must be set before SPELL_VICTORY_REWARD so the 1755 criterion can gate on it.
LastBattleAttackerVictory = !endByTimer;
// Remove relic // Remove relic
if (GameObject* go = GetRelic()) if (GameObject* go = GetRelic())
go->RemoveFromWorld(); go->RemoveFromWorld();
@@ -458,12 +479,17 @@ void BattlefieldWG::OnBattleEnd(bool endByTimer)
} }
} }
bool const grantEssenceToAttackers = sWorld->getBoolConfig(CONFIG_WINTERGRASP_ESSENCE_BOTH_FACTIONS);
for (ObjectGuid const& guid : PlayersInWar[GetAttackerTeam()]) for (ObjectGuid const& guid : PlayersInWar[GetAttackerTeam()])
if (Player* player = ObjectAccessor::FindPlayer(guid)) if (Player* player = ObjectAccessor::FindPlayer(guid))
{ {
player->CastSpell(player, SPELL_DEFEAT_REWARD, true); player->CastSpell(player, SPELL_DEFEAT_REWARD, true);
RemoveAurasFromPlayer(player); RemoveAurasFromPlayer(player);
if (grantEssenceToAttackers)
player->CastSpell(player, SPELL_ESSENCE_OF_WINTERGRASP, true);
for (uint8 i = 0; i < damagedTowersAtt; ++i) for (uint8 i = 0; i < damagedTowersAtt; ++i)
player->CastSpell(player, spellDamagedAtt, true); player->CastSpell(player, spellDamagedAtt, true);
for (uint8 i = 0; i < brokenTowersAtt; ++i) for (uint8 i = 0; i < brokenTowersAtt; ++i)
@@ -908,8 +934,8 @@ void BattlefieldWG::OnPlayerEnterZone(Player* player)
// Send worldstate to player // Send worldstate to player
SendInitWorldStatesTo(player); SendInitWorldStatesTo(player);
// xinef: Attacker, if hidden in relic room kick him out // xinef: Attacker, if hidden in relic room kick him out (only during wartime)
if (player->GetTeamId() == GetAttackerTeam()) if (IsWarTime() && player->GetTeamId() == GetAttackerTeam())
if (player->GetPositionX() > 5400.0f && player->GetPositionX() < 5490.0f && player->GetPositionY() > 2803.0f && player->GetPositionY() < 2878.0f) if (player->GetPositionX() > 5400.0f && player->GetPositionX() < 5490.0f && player->GetPositionY() > 2803.0f && player->GetPositionY() < 2878.0f)
KickPlayerFromBattlefield(player->GetGUID()); KickPlayerFromBattlefield(player->GetGUID());
} }
@@ -403,6 +403,9 @@ public:
uint32 GetData(uint32 data) const override; uint32 GetData(uint32 data) const override;
// True iff the most recent battle ended with the keep captured (attacker win).
[[nodiscard]] bool IsLastBattleAttackerVictory() const { return LastBattleAttackerVictory; }
bool IsKeepNpc(uint32 entry) bool IsKeepNpc(uint32 entry)
{ {
switch (entry) switch (entry)
@@ -451,6 +454,8 @@ protected:
int32 TenacityStack; int32 TenacityStack;
ObjectGuid TitansRelic; ObjectGuid TitansRelic;
bool LastBattleAttackerVictory{false};
}; };
uint8 const WG_MAX_OBJ = 32; uint8 const WG_MAX_OBJ = 32;
@@ -675,7 +675,7 @@ inline void Battleground::_ProcessJoin(uint32 diff)
// Announce BG starting // Announce BG starting
if (sWorld->getBoolConfig(CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_ENABLE)) if (sWorld->getBoolConfig(CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_ENABLE))
ChatHandler(nullptr).SendWorldText(LANG_BG_STARTED_ANNOUNCE_WORLD, GetName(), std::min(GetMinLevel(), (uint32)80), std::min(GetMaxLevel(), (uint32)80)); ChatHandler(nullptr).SendWorldTextOptional(LANG_BG_STARTED_ANNOUNCE_WORLD, ANNOUNCER_FLAG_DISABLE_PVP_START, GetName(), std::min(GetMinLevel(), (uint32)80), std::min(GetMaxLevel(), (uint32)80));
sScriptMgr->OnBattlegroundStart(this); sScriptMgr->OnBattlegroundStart(this);
} }
@@ -1583,7 +1583,10 @@ uint8 BattlegroundAV::GetAttackString(BG_AV_Nodes node, TeamId teamId)
strId = AV_TEXT_H_HERALD_SNOWFALL_GRAVEYARD_ATTACK; strId = AV_TEXT_H_HERALD_SNOWFALL_GRAVEYARD_ATTACK;
break; break;
case BG_AV_NODES_ICEBLOOD_TOWER: case BG_AV_NODES_ICEBLOOD_TOWER:
strId = AV_TEXT_A_HERALD_ICEBLOOD_TOWER_ATTACK; if (teamId == TEAM_ALLIANCE)
strId = AV_TEXT_A_HERALD_ICEBLOOD_TOWER_ATTACK;
else
strId = AV_TEXT_H_HERALD_ICEBLOOD_TOWER_ATTACK;
break; break;
case BG_AV_NODES_ICEBLOOD_GRAVE: case BG_AV_NODES_ICEBLOOD_GRAVE:
if (teamId == TEAM_ALLIANCE) if (teamId == TEAM_ALLIANCE)
@@ -1682,7 +1685,10 @@ uint8 BattlegroundAV::GetDefendString(BG_AV_Nodes node, TeamId teamId)
strId = AV_TEXT_H_HERALD_SNOWFALL_GRAVEYARD_TAKEN; strId = AV_TEXT_H_HERALD_SNOWFALL_GRAVEYARD_TAKEN;
break; break;
case BG_AV_NODES_ICEBLOOD_TOWER: case BG_AV_NODES_ICEBLOOD_TOWER:
strId = AV_TEXT_A_HERALD_ICEBLOOD_TOWER_TAKEN; if (teamId == TEAM_ALLIANCE)
strId = AV_TEXT_A_HERALD_ICEBLOOD_TOWER_TAKEN;
else
strId = AV_TEXT_H_HERALD_ICEBLOOD_TOWER_TAKEN;
break; break;
case BG_AV_NODES_ICEBLOOD_GRAVE: case BG_AV_NODES_ICEBLOOD_GRAVE:
if (teamId == TEAM_ALLIANCE) if (teamId == TEAM_ALLIANCE)
@@ -1622,50 +1622,50 @@ enum BG_AV_Objectives
enum BG_AV_HERALD enum BG_AV_HERALD
{ {
AV_TEXT_A_HERALD_STORMPIKE_GRAVEYARD_TAKEN = 1, AV_TEXT_A_HERALD_STORMPIKE_GRAVEYARD_TAKEN = 1,
AV_TEXT_A_HERALD_STORMPIKE_GRAVEYARD_ATTACK = 2, AV_TEXT_A_HERALD_STORMPIKE_GRAVEYARD_ATTACK = 2,
AV_TEXT_H_HERALD_STORMPIKE_GRAVEYARD_TAKEN = 3, AV_TEXT_H_HERALD_STORMPIKE_GRAVEYARD_TAKEN = 3,
AV_TEXT_H_HERALD_STORMPIKE_GRAVEYARD_ATTACK = 4, AV_TEXT_H_HERALD_STORMPIKE_GRAVEYARD_ATTACK = 4,
AV_TEXT_A_HERALD_SNOWFALL_GRAVEYARD_TAKEN = 5, AV_TEXT_A_HERALD_SNOWFALL_GRAVEYARD_TAKEN = 5,
AV_TEXT_A_HERALD_SNOWFALL_GRAVEYARD_ATTACK = 6, AV_TEXT_A_HERALD_SNOWFALL_GRAVEYARD_ATTACK = 6,
AV_TEXT_H_HERALD_SNOWFALL_GRAVEYARD_TAKEN = 7, AV_TEXT_H_HERALD_SNOWFALL_GRAVEYARD_TAKEN = 7,
AV_TEXT_H_HERALD_SNOWFALL_GRAVEYARD_ATTACK = 8, AV_TEXT_H_HERALD_SNOWFALL_GRAVEYARD_ATTACK = 8,
AV_TEXT_A_HERALD_FROSTWOLF_GRAVEYARD_TAKEN = 9, AV_TEXT_A_HERALD_FROSTWOLF_GRAVEYARD_TAKEN = 9,
AV_TEXT_A_HERALD_FROSTWOLF_GRAVEYARD_ATTACK = 10, AV_TEXT_A_HERALD_FROSTWOLF_GRAVEYARD_ATTACK = 10,
AV_TEXT_H_HERALD_FROSTWOLF_GRAVEYARD_TAKEN = 11, AV_TEXT_H_HERALD_FROSTWOLF_GRAVEYARD_TAKEN = 11,
AV_TEXT_H_HERALD_FROSTWOLF_GRAVEYARD_ATTACK = 12, AV_TEXT_H_HERALD_FROSTWOLF_GRAVEYARD_ATTACK = 12,
AV_TEXT_H_HERALD_DUN_BALDAR_SOUTH_ATTACK = 13, AV_TEXT_H_HERALD_DUN_BALDAR_SOUTH_ATTACK = 13,
AV_TEXT_H_HERALD_DUN_BALDAR_SOUTH_TAKEN = 14, AV_TEXT_H_HERALD_DUN_BALDAR_SOUTH_TAKEN = 14,
AV_TEXT_A_HERALD_DUN_BALDAR_SOUTH_ATTACK = 15, AV_TEXT_A_HERALD_DUN_BALDAR_SOUTH_ATTACK = 15,
AV_TEXT_A_HERALD_DUN_BALDAR_SOUTH_TAKEN = 16, AV_TEXT_A_HERALD_DUN_BALDAR_SOUTH_TAKEN = 16,
AV_TEXT_H_HERALD_DUN_BALDAR_NORTH_ATTACK = 17, AV_TEXT_H_HERALD_DUN_BALDAR_NORTH_ATTACK = 17,
AV_TEXT_H_HERALD_DUN_BALDAR_NORTH_TAKEN = 18, AV_TEXT_H_HERALD_DUN_BALDAR_NORTH_TAKEN = 18,
AV_TEXT_A_HERALD_DUN_BALDAR_NORTH_ATTACK = 19, AV_TEXT_A_HERALD_DUN_BALDAR_NORTH_ATTACK = 19,
AV_TEXT_A_HERALD_DUN_BALDAR_NORTH_TAKEN = 20, AV_TEXT_A_HERALD_DUN_BALDAR_NORTH_TAKEN = 20,
AV_TEXT_H_HERALD_ICEWING_BUNKER_ATTACK = 21, AV_TEXT_H_HERALD_ICEWING_BUNKER_ATTACK = 21,
AV_TEXT_H_HERALD_ICEWING_BUNKER_TAKEN = 22, AV_TEXT_H_HERALD_ICEWING_BUNKER_TAKEN = 22,
AV_TEXT_A_HERALD_ICEWING_BUNKER_ATTACK = 23, AV_TEXT_A_HERALD_ICEWING_BUNKER_ATTACK = 23,
AV_TEXT_A_HERALD_ICEWING_BUNKER_TAKEN = 24, AV_TEXT_A_HERALD_ICEWING_BUNKER_TAKEN = 24,
AV_TEXT_H_HERALD_STONEHEARTH_BUNKER_ATTACK = 25, AV_TEXT_H_HERALD_STONEHEARTH_BUNKER_ATTACK = 25,
AV_TEXT_H_HERALD_STONEHEARTH_BUNKER_TAKEN = 26, AV_TEXT_H_HERALD_STONEHEARTH_BUNKER_TAKEN = 26,
AV_TEXT_A_HERALD_STONEHEARTH_BUNKER_ATTACK = 27, AV_TEXT_A_HERALD_STONEHEARTH_BUNKER_ATTACK = 27,
AV_TEXT_A_HERALD_STONEHEARTH_BUNKER_TAKEN = 28, AV_TEXT_A_HERALD_STONEHEARTH_BUNKER_TAKEN = 28,
AV_TEXT_H_HERALD_WEST_FROSTWOLF_TOWER_ATTACK = 29, AV_TEXT_H_HERALD_WEST_FROSTWOLF_TOWER_ATTACK = 29,
AV_TEXT_H_HERALD_WEST_FROSTWOLF_TOWER_TAKEN = 30, AV_TEXT_H_HERALD_WEST_FROSTWOLF_TOWER_TAKEN = 30,
AV_TEXT_A_HERALD_WEST_FROSTWOLF_TOWER_ATTACK = 31, AV_TEXT_A_HERALD_WEST_FROSTWOLF_TOWER_ATTACK = 31,
AV_TEXT_A_HERALD_WEST_FROSTWOLF_TOWER_TAKEN = 32, AV_TEXT_A_HERALD_WEST_FROSTWOLF_TOWER_TAKEN = 32,
AV_TEXT_H_HERALD_EAST_FROSTWOLF_TOWER_ATTACK = 33, AV_TEXT_H_HERALD_EAST_FROSTWOLF_TOWER_ATTACK = 33,
AV_TEXT_H_HERALD_EAST_FROSTWOLF_TOWER_TAKEN = 34, AV_TEXT_H_HERALD_EAST_FROSTWOLF_TOWER_TAKEN = 34,
AV_TEXT_A_HERALD_EAST_FROSTWOLF_TOWER_ATTACK = 35, AV_TEXT_A_HERALD_EAST_FROSTWOLF_TOWER_ATTACK = 35,
AV_TEXT_A_HERALD_EAST_FROSTWOLF_TOWER_TAKEN = 36, AV_TEXT_A_HERALD_EAST_FROSTWOLF_TOWER_TAKEN = 36,
AV_TEXT_H_HERALD_TOWER_POINT_ATTACK = 37, AV_TEXT_H_HERALD_TOWER_POINT_ATTACK = 37,
AV_TEXT_H_HERALD_TOWER_POINT_TAKEN = 38, AV_TEXT_H_HERALD_TOWER_POINT_TAKEN = 38,
AV_TEXT_A_HERALD_TOWER_POINT_ATTACK = 39, AV_TEXT_A_HERALD_TOWER_POINT_ATTACK = 39,
AV_TEXT_A_HERALD_TOWER_POINT_TAKEN = 40, AV_TEXT_A_HERALD_TOWER_POINT_TAKEN = 40,
AV_TEXT_H_HERALD_ICEBLOOD_BUNKER_ATTACK = 41, AV_TEXT_H_HERALD_ICEBLOOD_TOWER_ATTACK = 41,
AV_TEXT_H_HERALD_ICEBLOOD_BUNKER_TAKEN = 42, AV_TEXT_H_HERALD_ICEBLOOD_TOWER_TAKEN = 42,
AV_TEXT_A_HERALD_ICEBLOOD_TOWER_ATTACK = 43, AV_TEXT_A_HERALD_ICEBLOOD_TOWER_ATTACK = 43,
AV_TEXT_A_HERALD_ICEBLOOD_TOWER_TAKEN = 44, AV_TEXT_A_HERALD_ICEBLOOD_TOWER_TAKEN = 44,
AV_TEXT_H_HERALD_STORMPIKE_AID_STATION_TAKEN = 45, AV_TEXT_H_HERALD_STORMPIKE_AID_STATION_TAKEN = 45,
AV_TEXT_H_HERALD_STORMPIKE_AID_STATION_ATTACK = 46, AV_TEXT_H_HERALD_STORMPIKE_AID_STATION_ATTACK = 46,
AV_TEXT_A_HERALD_STORMPIKE_AID_STATION_TAKEN = 47, AV_TEXT_A_HERALD_STORMPIKE_AID_STATION_TAKEN = 47,
+21 -7
View File
@@ -1692,18 +1692,32 @@ namespace lfg
LfgGuidList players; LfgGuidList players;
GuidUnorderedSet playersToTeleport; GuidUnorderedSet playersToTeleport;
for (LfgProposalPlayerContainer::const_iterator it = proposal.players.begin(); it != proposal.players.end(); ++it) // Sort players by role, leader first, then tank, healer and dps
{ std::vector<ObjectGuid> tanks, healers, dps;
ObjectGuid guid = it->first;
if (guid == proposal.leader)
players.push_front(guid);
else
players.push_back(guid);
if (proposal.leader && proposal.players.contains(proposal.leader))
players.push_back(proposal.leader);
for (auto const& [guid, player] : proposal.players)
{
if (proposal.isNew || GetGroup(guid) != proposal.group) if (proposal.isNew || GetGroup(guid) != proposal.group)
playersToTeleport.insert(guid); playersToTeleport.insert(guid);
if (guid == proposal.leader)
continue;
if (player.role & lfg::PLAYER_ROLE_TANK)
tanks.push_back(guid);
else if (player.role & lfg::PLAYER_ROLE_HEALER)
healers.push_back(guid);
else
dps.push_back(guid);
} }
players.insert(players.end(), tanks.begin(), tanks.end());
players.insert(players.end(), healers.begin(), healers.end());
players.insert(players.end(), dps.begin(), dps.end());
// Set the dungeon difficulty // Set the dungeon difficulty
LFGDungeonData const* dungeon = GetLFGDungeon(proposal.dungeonId); LFGDungeonData const* dungeon = GetLFGDungeon(proposal.dungeonId);
ASSERT(dungeon); ASSERT(dungeon);
@@ -5384,6 +5384,7 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
RemoveAurasDueToSpell(pAbility->Spell); RemoveAurasDueToSpell(pAbility->Spell);
} }
} }
sScriptMgr->OnPlayerSetSkill(this, id, currVal, maxVal, step, newVal);
} }
else if (newVal) //add else if (newVal) //add
{ {
@@ -5430,6 +5431,7 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
learnSkillRewardedSpells(id, newVal); learnSkillRewardedSpells(id, newVal);
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id); UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id);
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id); UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id);
sScriptMgr->OnPlayerSetSkill(this, id, currVal, maxVal, step, newVal);
return; return;
} }
} }
@@ -13115,6 +13117,11 @@ PartyResult Player::CanUninviteFromGroup(ObjectGuid targetPlayerGUID) const
if (InBattleground()) if (InBattleground())
return ERR_INVITE_RESTRICTED; return ERR_INVITE_RESTRICTED;
// BF raids are owned by the Battlefield system; leaders/assistants must not
// be able to kick members (would drop their team assignment mid-battle).
if (grp->isBFGroup())
return ERR_NOT_LEADER;
} }
return ERR_PARTY_RESULT_OK; return ERR_PARTY_RESULT_OK;
@@ -33,7 +33,11 @@ enum AnnouncerFlags : uint8
{ {
ANNOUNCER_FLAG_DISABLE_BG_QUEUE = 1, ANNOUNCER_FLAG_DISABLE_BG_QUEUE = 1,
ANNOUNCER_FLAG_DISABLE_ARENA_QUEUE = 2, ANNOUNCER_FLAG_DISABLE_ARENA_QUEUE = 2,
ANNOUNCER_FLAG_DISABLE_AUTOBROADCAST = 4 ANNOUNCER_FLAG_DISABLE_AUTOBROADCAST = 4,
ANNOUNCER_FLAG_DISABLE_PVP_START = 8,
ANNOUNCER_FLAG_DISABLE_PVP_ALL = ANNOUNCER_FLAG_DISABLE_BG_QUEUE
| ANNOUNCER_FLAG_DISABLE_ARENA_QUEUE
| ANNOUNCER_FLAG_DISABLE_PVP_START
}; };
struct PlayerSetting struct PlayerSetting
@@ -6234,6 +6234,32 @@ Item* Player::_LoadMailedItem(ObjectGuid const& playerGuid, Player* player, uint
return nullptr; return nullptr;
} }
// Rehydrate looters for BoP-tradeable mail items; only the LFG mail path writes this flag, gated on the same config.
if (item->IsBOPTradable() && sWorld->getBoolConfig(CONFIG_SET_BOP_ITEM_TRADEABLE))
{
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_ITEM_BOP_TRADE);
stmt->SetData(0, item->GetGUID().GetCounter());
if (PreparedQueryResult result = CharacterDatabase.Query(stmt))
{
AllowedLooterSet looters;
for (std::string_view guidStr : Acore::Tokenize((*result)[0].Get<std::string_view>(), ' ', false))
{
if (Optional<ObjectGuid::LowType> guid = Acore::StringTo<ObjectGuid::LowType>(guidStr))
looters.insert(ObjectGuid::Create<HighGuid::Player>(*guid));
else
LOG_WARN("entities.player.loading", "Player::_LoadMailedItem: invalid item_soulbound_trade_data GUID '{}' for item {}. Skipped.", guidStr, item->GetGUID().ToString());
}
if (looters.size() > 1 && proto->GetMaxStackSize() == 1 && item->IsSoulBound())
item->SetSoulboundTradeable(looters);
else
item->RemoveFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE);
}
else
item->RemoveFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE);
}
if (mail) if (mail)
{ {
mail->AddItem(itemGuid, itemEntry); mail->AddItem(itemGuid, itemEntry);
+8 -20
View File
@@ -954,6 +954,9 @@ bool Unit::IsImmunedToSpell(SpellInfo const* spellInfo, uint32 effectMask, Unit
for (auto const& [immunitySchoolMask, immunityAuraId] : schoolList) for (auto const& [immunitySchoolMask, immunityAuraId] : schoolList)
{ {
SpellInfo const* immuneSpellInfo = sSpellMgr->GetSpellInfo(immunityAuraId); SpellInfo const* immuneSpellInfo = sSpellMgr->GetSpellInfo(immunityAuraId);
if (immunityAuraId == spellInfo->Id)
continue;
if ((immunitySchoolMask & schoolMask) != schoolMask) if ((immunitySchoolMask & schoolMask) != schoolMask)
continue; continue;
@@ -10093,6 +10096,9 @@ bool Unit::IsImmunedToSpell(SpellInfo const* spellInfo, Spell const* spell)
SpellImmuneContainer const& schoolList = m_spellImmune[IMMUNITY_SCHOOL]; SpellImmuneContainer const& schoolList = m_spellImmune[IMMUNITY_SCHOOL];
for (auto itr = schoolList.begin(); itr != schoolList.end(); ++itr) for (auto itr = schoolList.begin(); itr != schoolList.end(); ++itr)
{ {
if (itr->second == spellInfo->Id)
continue;
SpellInfo const* immuneSpellInfo = sSpellMgr->GetSpellInfo(itr->second); SpellInfo const* immuneSpellInfo = sSpellMgr->GetSpellInfo(itr->second);
if (!(itr->first & spellSchoolMask)) if (!(itr->first & spellSchoolMask))
continue; continue;
@@ -10624,6 +10630,8 @@ void Unit::Dismount()
if (charm->IsCreature() && !charm->HasUnitState(UNIT_STATE_STUNNED)) if (charm->IsCreature() && !charm->HasUnitState(UNIT_STATE_STUNNED))
charm->RemoveUnitFlag(UNIT_FLAG_STUNNED); charm->RemoveUnitFlag(UNIT_FLAG_STUNNED);
} }
UpdateSpeed(MOVE_RUN, true);
} }
void Unit::SetImmuneToPC(bool apply, bool keepCombat) void Unit::SetImmuneToPC(bool apply, bool keepCombat)
@@ -15338,30 +15346,10 @@ void Unit::SetPhaseMask(uint32 newPhaseMask, bool update)
if (!sScriptMgr->CanSetPhaseMask(this, newPhaseMask, update)) if (!sScriptMgr->CanSetPhaseMask(this, newPhaseMask, update))
return; return;
// Phase-related threat updates are done AFTER the phase change below
} }
WorldObject::SetPhaseMask(newPhaseMask, false); WorldObject::SetPhaseMask(newPhaseMask, false);
// Now update threat online states with the new phase mask applied
if (IsCreature() || (IsPlayer() && !ToPlayer()->IsGameMaster() && !ToPlayer()->GetSession()->PlayerLogout()))
{
// Update online state for units that have me on their threat list
for (auto const& pair : GetThreatMgr().GetThreatenedByMeList())
{
if (ThreatReference* ref = pair.second)
ref->UpdateOffline();
}
// Update online state for units on my threat list
if (!IsPlayer())
{
for (ThreatReference* ref : GetThreatMgr().GetModifiableThreatList())
ref->UpdateOffline();
}
}
if (!IsInWorld()) if (!IsInWorld())
{ {
return; return;
+36 -4
View File
@@ -63,6 +63,40 @@ Loot* Roll::getLoot()
return getTarget(); return getTarget();
} }
static void SendRollWonItemViaMail(Player* player, LootItem const* lootItem, uint32 itemId)
{
Item* mailItem = Item::CreateItem(itemId, lootItem->count, player, false, lootItem->randomPropertyId);
if (!mailItem)
return;
AllowedLooterSet looters = lootItem->GetAllowedLooters();
ItemTemplate const* proto = mailItem->GetTemplate();
// Preserve the 2-hour group trade window the item would have had if stored directly.
if (looters.size() > 1 && proto->GetMaxStackSize() == 1 &&
(proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_QUEST_ITEM) &&
sWorld->getBoolConfig(CONFIG_SET_BOP_ITEM_TRADEABLE))
{
mailItem->SetBinding(true);
mailItem->SetSoulboundTradeable(looters);
mailItem->SetUInt32Value(ITEM_FIELD_CREATE_PLAYED_TIME, player->GetTotalPlayedTime());
std::string lootersStr;
for (ObjectGuid const& guid : looters)
{
if (!lootersStr.empty())
lootersStr += ' ';
lootersStr += std::to_string(guid.GetCounter());
}
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_ITEM_BOP_TRADE);
stmt->SetData(0, mailItem->GetGUID().GetCounter());
stmt->SetData(1, lootersStr);
CharacterDatabase.Execute(stmt);
}
player->SendItemRetrievalMail(mailItem);
}
Group::Group() : m_leaderName(""), m_groupType(GROUPTYPE_NORMAL), Group::Group() : m_leaderName(""), m_groupType(GROUPTYPE_NORMAL),
m_dungeonDifficulty(DUNGEON_DIFFICULTY_NORMAL), m_raidDifficulty(RAID_DIFFICULTY_10MAN_NORMAL), m_dungeonDifficulty(DUNGEON_DIFFICULTY_NORMAL), m_raidDifficulty(RAID_DIFFICULTY_10MAN_NORMAL),
m_bfGroup(nullptr), m_bgGroup(nullptr), m_lootMethod(FREE_FOR_ALL), m_lootThreshold(ITEM_QUALITY_UNCOMMON), m_bfGroup(nullptr), m_bgGroup(nullptr), m_lootMethod(FREE_FOR_ALL), m_lootThreshold(ITEM_QUALITY_UNCOMMON),
@@ -1504,8 +1538,7 @@ void Group::CountTheRoll(Rolls::iterator rollI, Map* allowedMap)
roll->getLoot()->NotifyItemRemoved(roll->itemSlot); roll->getLoot()->NotifyItemRemoved(roll->itemSlot);
roll->getLoot()->unlootedCount--; roll->getLoot()->unlootedCount--;
player->SendEquipError(msg, nullptr, nullptr, roll->itemid); player->SendEquipError(msg, nullptr, nullptr, roll->itemid);
if (Item* mailItem = Item::CreateItem(roll->itemid, item->count, player, false, item->randomPropertyId)) SendRollWonItemViaMail(player, item, roll->itemid);
player->SendItemRetrievalMail(mailItem);
} }
else else
{ {
@@ -1587,8 +1620,7 @@ void Group::CountTheRoll(Rolls::iterator rollI, Map* allowedMap)
roll->getLoot()->NotifyItemRemoved(roll->itemSlot); roll->getLoot()->NotifyItemRemoved(roll->itemSlot);
roll->getLoot()->unlootedCount--; roll->getLoot()->unlootedCount--;
player->SendEquipError(msg, nullptr, nullptr, roll->itemid); player->SendEquipError(msg, nullptr, nullptr, roll->itemid);
if (Item* mailItem = Item::CreateItem(roll->itemid, item->count, player, false, item->randomPropertyId)) SendRollWonItemViaMail(player, item, roll->itemid);
player->SendItemRetrievalMail(mailItem);
} }
else else
{ {
+10
View File
@@ -126,6 +126,16 @@ void WorldSession::HandleGroupInviteOpcode(WorldPacket& recvData)
return; return;
} }
// Battlefield raids (e.g. Wintergrasp) have their composition managed by the BF
// system based on queue and team balance. Letting raid members recruit outsiders
// bypasses Battlefield::AddOrSetPlayerToCorrectBfGroup, which on WG entry then
// refuses to add the invitee because they are already in a BF group.
if (Group* invitingGroup = invitingPlayer->GetGroup(); invitingGroup && invitingGroup->isBFGroup())
{
SendPartyResult(PARTY_OP_INVITE, membername, ERR_NOT_LEADER);
return;
}
Group* group = invitingPlayer->GetGroup(); Group* group = invitingPlayer->GetGroup();
if (group && group->isBGGroup()) if (group && group->isBGGroup())
group = invitingPlayer->GetOriginalGroup(); group = invitingPlayer->GetOriginalGroup();
+24 -4
View File
@@ -569,12 +569,32 @@ void WorldSession::SendLfgUpdateProposal(lfg::LfgProposal const& proposal)
data << uint8(silent); // Show proposal window data << uint8(silent); // Show proposal window
data << uint8(proposal.players.size()); // Group size data << uint8(proposal.players.size()); // Group size
for (lfg::LfgProposalPlayerContainer::const_iterator it = proposal.players.begin(); it != proposal.players.end(); ++it) // Sort by roles: tank, healer, dps
std::vector<ObjectGuid> ordered;
ordered.reserve(proposal.players.size());
std::vector<ObjectGuid> tanks, healers, dps;
for (auto const& [pguid, player] : proposal.players)
{ {
lfg::LfgProposalPlayer const& player = it->second; if (player.role & lfg::PLAYER_ROLE_TANK)
tanks.push_back(pguid);
else if (player.role & lfg::PLAYER_ROLE_HEALER)
healers.push_back(pguid);
else
dps.push_back(pguid);
}
ordered.insert(ordered.end(), tanks.begin(), tanks.end());
ordered.insert(ordered.end(), healers.begin(), healers.end());
ordered.insert(ordered.end(), dps.begin(), dps.end());
for (auto const& pguid : ordered)
{
lfg::LfgProposalPlayer const& player = proposal.players.find(pguid)->second;
data << uint32(player.role); // Role data << uint32(player.role); // Role
data << uint8(it->first == guid); // Self player data << uint8(pguid == guid); // Self player
if (!player.group) // Player not it a group if (!player.group) // Player not in a group
{ {
data << uint8(0); // Not in dungeon data << uint8(0); // Not in dungeon
data << uint8(0); // Not same group data << uint8(0); // Not same group
+5 -1
View File
@@ -1508,6 +1508,10 @@ enum AcoreStrings
LANG_RESPAWN_ENTRY_GAMEOBJECT_QUEUED = 35452, LANG_RESPAWN_ENTRY_GAMEOBJECT_QUEUED = 35452,
// List respawns console support // List respawns console support
LANG_LIST_RESPAWNS_NO_MAP = 35447 LANG_LIST_RESPAWNS_NO_MAP = 35447,
// Pet rename command
LANG_PET_RENAME_INVALID = 35453,
LANG_PET_RENAME_SUCCESS = 35454
}; };
#endif #endif
@@ -914,6 +914,11 @@ void ScriptMgr::OnPlayerUpdateSkill(Player* player, uint32 skillId, uint32 value
CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_UPDATE_SKILL, script->OnPlayerUpdateSkill(player, skillId, value, max, step, newValue)); CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_UPDATE_SKILL, script->OnPlayerUpdateSkill(player, skillId, value, max, step, newValue));
} }
void ScriptMgr::OnPlayerSetSkill(Player* player, uint32 skillId, uint32 value, uint32 max, uint32 step, uint32 newValue)
{
CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_SET_SKILL, script->OnPlayerSetSkill(player, skillId, value, max, step, newValue));
}
bool ScriptMgr::OnPlayerCanResurrect(Player* player) bool ScriptMgr::OnPlayerCanResurrect(Player* player)
{ {
CALL_ENABLED_BOOLEAN_HOOKS(PlayerScript, PLAYERHOOK_CAN_RESURRECT, !script->OnPlayerCanResurrect(player)); CALL_ENABLED_BOOLEAN_HOOKS(PlayerScript, PLAYERHOOK_CAN_RESURRECT, !script->OnPlayerCanResurrect(player));
@@ -212,6 +212,7 @@ enum PlayerHook
PLAYERHOOK_ON_CAN_UPDATE_SKILL, PLAYERHOOK_ON_CAN_UPDATE_SKILL,
PLAYERHOOK_ON_BEFORE_UPDATE_SKILL, PLAYERHOOK_ON_BEFORE_UPDATE_SKILL,
PLAYERHOOK_ON_UPDATE_SKILL, PLAYERHOOK_ON_UPDATE_SKILL,
PLAYERHOOK_ON_SET_SKILL,
PLAYERHOOK_CAN_RESURRECT, PLAYERHOOK_CAN_RESURRECT,
PLAYERHOOK_ON_CAN_GIVE_LEVEL, PLAYERHOOK_ON_CAN_GIVE_LEVEL,
PLAYERHOOK_ON_SEND_LIST_INVENTORY, PLAYERHOOK_ON_SEND_LIST_INVENTORY,
@@ -792,6 +793,7 @@ public:
virtual bool OnPlayerCanUpdateSkill(Player* /*player*/, uint32 /*skillId*/) { return true; } virtual bool OnPlayerCanUpdateSkill(Player* /*player*/, uint32 /*skillId*/) { return true; }
virtual void OnPlayerBeforeUpdateSkill(Player* /*player*/, uint32 /*skillId*/, uint32& /*value*/, uint32 /*max*/, uint32 /*step*/) { } virtual void OnPlayerBeforeUpdateSkill(Player* /*player*/, uint32 /*skillId*/, uint32& /*value*/, uint32 /*max*/, uint32 /*step*/) { }
virtual void OnPlayerUpdateSkill(Player* /*player*/, uint32 /*skillId*/, uint32 /*value*/, uint32 /*max*/, uint32 /*step*/, uint32 /*newValue*/) { } virtual void OnPlayerUpdateSkill(Player* /*player*/, uint32 /*skillId*/, uint32 /*value*/, uint32 /*max*/, uint32 /*step*/, uint32 /*newValue*/) { }
virtual void OnPlayerSetSkill(Player* /*player*/, uint32 /*skillId*/, uint32 /*value*/, uint32 /*max*/, uint32 /*step*/, uint32 /*newValue*/) { }
/** /**
* @brief This hook is called, to avoid player resurrect * @brief This hook is called, to avoid player resurrect
+1
View File
@@ -483,6 +483,7 @@ public: /* PlayerScript */
bool OnPlayerCanUpdateSkill(Player* player, uint32 skillId); bool OnPlayerCanUpdateSkill(Player* player, uint32 skillId);
void OnPlayerBeforeUpdateSkill(Player* player, uint32 skill_id, uint32& value, uint32 max, uint32 step); void OnPlayerBeforeUpdateSkill(Player* player, uint32 skill_id, uint32& value, uint32 max, uint32 step);
void OnPlayerUpdateSkill(Player* player, uint32 skillId, uint32 value, uint32 max, uint32 step, uint32 newValue); void OnPlayerUpdateSkill(Player* player, uint32 skillId, uint32 value, uint32 max, uint32 step, uint32 newValue);
void OnPlayerSetSkill(Player* player, uint32 skillId, uint32 value, uint32 max, uint32 step, uint32 newValue);
bool OnPlayerCanResurrect(Player* player); bool OnPlayerCanResurrect(Player* player);
bool OnPlayerCanGiveLevel(Player* player, uint8 newLevel); bool OnPlayerCanGiveLevel(Player* player, uint8 newLevel);
void OnPlayerSendListInventory(Player* player, ObjectGuid vendorGuid, uint32& vendorEntry); void OnPlayerSendListInventory(Player* player, ObjectGuid vendorGuid, uint32& vendorEntry);
@@ -5344,13 +5344,10 @@ void AuraEffect::HandleAuraDummy(AuraApplication const* aurApp, uint8 mode, bool
{ {
case 2584: // Waiting to Resurrect case 2584: // Waiting to Resurrect
// Waiting to resurrect spell cancel, we must remove player from resurrect queue // Waiting to resurrect spell cancel, we must remove player from resurrect queue
// bf branch omitted: it would cascade back into this handler.
if (target->IsPlayer()) if (target->IsPlayer())
{
if (Battleground* bg = target->ToPlayer()->GetBattleground()) if (Battleground* bg = target->ToPlayer()->GetBattleground())
bg->RemovePlayerFromResurrectQueue(target->ToPlayer()); bg->RemovePlayerFromResurrectQueue(target->ToPlayer());
if (Battlefield* bf = sBattlefieldMgr->GetBattlefieldToZoneId(target->GetZoneId()))
bf->RemovePlayerFromResurrectQueue(target->GetGUID());
}
break; break;
case 43681: // Inactive case 43681: // Inactive
{ {
+22 -2
View File
@@ -52,6 +52,7 @@
#include "World.h" #include "World.h"
#include "WorldPacket.h" #include "WorldPacket.h"
#include <cmath> #include <cmath>
#include <G3D/g3dmath.h>
/// @todo: this import is not necessary for compilation and marked as unused by the IDE /// @todo: this import is not necessary for compilation and marked as unused by the IDE
// however, for some reasons removing it would cause a damn linking issue // however, for some reasons removing it would cause a damn linking issue
@@ -1225,9 +1226,28 @@ void Spell::SelectImplicitConeTargets(SpellEffIndex effIndex, SpellImplicitTarge
SpellTargetObjectTypes objectType = targetType.GetObjectType(); SpellTargetObjectTypes objectType = targetType.GetObjectType();
SpellTargetCheckTypes selectionType = targetType.GetCheckType(); SpellTargetCheckTypes selectionType = targetType.GetCheckType();
ConditionList* condList = m_spellInfo->Effects[effIndex].ImplicitTargetConditions; ConditionList* condList = m_spellInfo->Effects[effIndex].ImplicitTargetConditions;
float coneAngle = M_PI / 2; float coneAngle = G3D::toRadians(60.0f);
float radius = m_spellInfo->Effects[effIndex].CalcRadius(m_caster) * m_spellValue->RadiusMod; if (SpellCone const* sc = sSpellMgr->GetSpellCone(m_spellInfo->Id))
coneAngle = G3D::toRadians(static_cast<float>(sc->cone_degrees));
else
{
switch (targetType.GetTarget())
{
case TARGET_UNIT_CONE_ENEMY_24:
coneAngle = G3D::toRadians(24.0f);
break;
case TARGET_UNIT_CONE_ENEMY_54:
coneAngle = G3D::toRadians(54.0f);
break;
case TARGET_UNIT_CONE_ENEMY_104:
coneAngle = G3D::toRadians(104.0f);
break;
default:
break;
}
}
float radius = m_spellInfo->Effects[effIndex].CalcRadius(m_caster) * m_spellValue->RadiusMod;
if (uint32 containerTypeMask = GetSearcherTypeMask(objectType, condList)) if (uint32 containerTypeMask = GetSearcherTypeMask(objectType, condList))
{ {
Acore::WorldObjectSpellConeTargetCheck check(coneAngle, radius, m_caster, m_spellInfo, selectionType, condList); Acore::WorldObjectSpellConeTargetCheck check(coneAngle, radius, m_caster, m_spellInfo, selectionType, condList);
@@ -1527,7 +1527,7 @@ void SpellMgr::LoadSpellInfoCorrections()
spellInfo->Speed = 8.0f; spellInfo->Speed = 8.0f;
}); });
// Spell Absorption // Shadowmoon Reaver - Spell Absorption
ApplySpellFix({ 41034 }, [](SpellInfo* spellInfo) ApplySpellFix({ 41034 }, [](SpellInfo* spellInfo)
{ {
spellInfo->Effects[EFFECT_2].Effect = SPELL_EFFECT_APPLY_AURA; spellInfo->Effects[EFFECT_2].Effect = SPELL_EFFECT_APPLY_AURA;
@@ -1536,12 +1536,13 @@ void SpellMgr::LoadSpellInfoCorrections()
spellInfo->Effects[EFFECT_2].MiscValue = SPELL_SCHOOL_MASK_MAGIC; spellInfo->Effects[EFFECT_2].MiscValue = SPELL_SCHOOL_MASK_MAGIC;
}); });
// Shared Bonds // Priestess of Delight and Priestess of Torment - Shared Bonds
ApplySpellFix({ 41363 }, [](SpellInfo* spellInfo) ApplySpellFix({ 41363 }, [](SpellInfo* spellInfo)
{ {
spellInfo->AttributesEx &= ~SPELL_ATTR1_IS_CHANNELED; spellInfo->AttributesEx &= ~SPELL_ATTR1_IS_CHANNELED;
}); });
// Illidari Council - Veras Darkshadow
ApplySpellFix({ ApplySpellFix({
41485, // Deadly Poison 41485, // Deadly Poison
41487 // Envenom 41487 // Envenom
@@ -1570,13 +1571,13 @@ void SpellMgr::LoadSpellInfoCorrections()
spellInfo->RangeEntry = sSpellRangeStore.LookupEntry(13); // 0-50000yd spellInfo->RangeEntry = sSpellRangeStore.LookupEntry(13); // 0-50000yd
}); });
// Watery Grave Explosion // Morogrim Tidewalker - Watery Grave Explosion
ApplySpellFix({ 37852 }, [](SpellInfo* spellInfo) ApplySpellFix({ 37852 }, [](SpellInfo* spellInfo)
{ {
spellInfo->AttributesEx5 |= SPELL_ATTR5_ALLOW_WHILE_STUNNED; spellInfo->AttributesEx5 |= SPELL_ATTR5_ALLOW_WHILE_STUNNED;
}); });
// Amplify Damage // Prince Malchezaar - Amplify Damage
ApplySpellFix({ 39095 }, [](SpellInfo* spellInfo) ApplySpellFix({ 39095 }, [](SpellInfo* spellInfo)
{ {
spellInfo->MaxAffectedTargets = 1; spellInfo->MaxAffectedTargets = 1;
@@ -1589,6 +1590,7 @@ void SpellMgr::LoadSpellInfoCorrections()
spellInfo->AttributesCu |= SPELL_ATTR0_CU_SINGLE_AURA_STACK; spellInfo->AttributesCu |= SPELL_ATTR0_CU_SINGLE_AURA_STACK;
}); });
// Archimonde
ApplySpellFix({ ApplySpellFix({
31984, // Finger of Death 31984, // Finger of Death
35354 // Hand of Death 35354 // Hand of Death
@@ -1598,12 +1600,18 @@ void SpellMgr::LoadSpellInfoCorrections()
spellInfo->Attributes = SPELL_ATTR0_IS_ABILITY; spellInfo->Attributes = SPELL_ATTR0_IS_ABILITY;
}); });
// Finger of Death // Archimonde - Finger of Death
ApplySpellFix({ 32111 }, [](SpellInfo* spellInfo) ApplySpellFix({ 32111 }, [](SpellInfo* spellInfo)
{ {
spellInfo->CastTimeEntry = sSpellCastTimesStore.LookupEntry(0); // We only need the animation, no damage spellInfo->CastTimeEntry = sSpellCastTimesStore.LookupEntry(0); // We only need the animation, no damage
}); });
// Archimonde - Doomfire
ApplySpellFix({ 31944, 31969 }, [](SpellInfo* spellInfo)
{
spellInfo->AttributesEx4 |= SPELL_ATTR4_NO_CAST_LOG;
});
// Flame Breath, catapult spell // Flame Breath, catapult spell
ApplySpellFix({ 50989 }, [](SpellInfo* spellInfo) ApplySpellFix({ 50989 }, [](SpellInfo* spellInfo)
{ {
@@ -5199,6 +5207,13 @@ void SpellMgr::LoadSpellInfoCorrections()
spellInfo->Effects[EFFECT_0].BasePoints = 1; spellInfo->Effects[EFFECT_0].BasePoints = 1;
}); });
// 26025 Impale
ApplySpellFix({ 26025 }, [](SpellInfo* spellInfo)
{
spellInfo->Effects[EFFECT_1].TargetA = SpellImplicitTargetInfo(TARGET_UNIT_CONE_ENEMY_54);
spellInfo->Effects[EFFECT_1].TargetB = SpellImplicitTargetInfo(0);
});
for (uint32 i = 0; i < GetSpellInfoStoreSize(); ++i) for (uint32 i = 0; i < GetSpellInfoStoreSize(); ++i)
{ {
SpellInfo* spellInfo = mSpellInfoMap[i]; SpellInfo* spellInfo = mSpellInfoMap[i];
+100 -1
View File
@@ -695,6 +695,16 @@ SpellTargetPosition const* SpellMgr::GetSpellTargetPosition(uint32 spell_id, Spe
return nullptr; return nullptr;
} }
SpellCone const* SpellMgr::GetSpellCone(uint32 spell_id) const
{
spell_id = GetFirstSpellInChain(spell_id);
auto itr = mSpellCones.find(spell_id);
if (itr != mSpellCones.end())
return &itr->second;
return nullptr;
}
SpellSpellGroupMapBounds SpellMgr::GetSpellSpellGroupMapBounds(uint32 spell_id) const SpellSpellGroupMapBounds SpellMgr::GetSpellSpellGroupMapBounds(uint32 spell_id) const
{ {
spell_id = GetFirstSpellInChain(spell_id); spell_id = GetFirstSpellInChain(spell_id);
@@ -1101,7 +1111,9 @@ bool SpellArea::IsFitToRequirements(Player const* player, uint32 newZone, uint32
return false; return false;
Battlefield* Bf = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG); Battlefield* Bf = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG);
if (!Bf || player->GetTeamId() != Bf->GetDefenderTeam() || Bf->IsWarTime()) if (!Bf || Bf->IsWarTime())
return false;
if (!sWorld->getBoolConfig(CONFIG_WINTERGRASP_ESSENCE_BOTH_FACTIONS) && player->GetTeamId() != Bf->GetDefenderTeam())
return false; return false;
break; break;
} }
@@ -1578,6 +1590,93 @@ void SpellMgr::LoadSpellTargetPositions()
LOG_INFO("server.loading", " "); LOG_INFO("server.loading", " ");
} }
void SpellMgr::LoadSpellCones()
{
uint32 oldMSTime = getMSTime();
mSpellCones.clear(); // need for reload case
// 0 1
QueryResult result = WorldDatabase.Query("SELECT ID, ConeDegrees FROM spell_cone");
if (!result)
{
LOG_WARN("server.loading", ">> Loaded 0 spell cone definitions. DB table `spell_cone` is empty.");
LOG_INFO("server.loading", " ");
return;
}
uint32 count = 0;
do
{
Field* fields = result->Fetch();
uint32 spellId = fields[0].Get<uint32>();
int16 coneDeg = fields[1].Get<int16>();
SpellInfo const* spellInfo = GetSpellInfo(spellId);
if (!spellInfo)
{
LOG_ERROR("sql.sql", "Spell (ID:{}) listed in `spell_cone` does not exist.", spellId);
continue;
}
if (coneDeg < -360 || coneDeg > 360)
{
LOG_ERROR("sql.sql", "Spell (Id: {}) cone degrees {} out of range (-360..360).", spellId, coneDeg);
continue;
}
uint32 firstRankId = GetFirstSpellInChain(spellId);
SpellInfo const* firstSpellInfo = GetSpellInfo(firstRankId);
SpellInfo const* checkInfo = firstSpellInfo ? firstSpellInfo : spellInfo;
bool hasCone = false;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{
if (checkInfo->Effects[i].TargetA.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CONE ||
checkInfo->Effects[i].TargetB.GetSelectionCategory() == TARGET_SELECT_CATEGORY_CONE)
{
hasCone = true;
break;
}
}
if (!hasCone)
{
LOG_ERROR("sql.sql", "Spell (ID:{}) listed in `spell_cone` does not have a cone implicit target.", spellId);
continue;
}
if (firstRankId != spellId)
LOG_INFO("server.loading", "Spell (ID:{}) listed in `spell_cone` is not first rank; mapping to first rank {}.", spellId, firstRankId);
SpellCone sc{};
sc.cone_degrees = coneDeg;
// Avoid overwriting an existing first-rank entry with conflicting values
auto itr = mSpellCones.find(firstRankId);
if (itr != mSpellCones.end())
{
if (itr->second.cone_degrees != sc.cone_degrees)
LOG_ERROR("sql.sql",
"Conflicting `spell_cone` entries for first-rank spell ID {}: {} vs {}. Keeping first value.",
firstRankId,
itr->second.cone_degrees,
sc.cone_degrees);
}
else
{
mSpellCones[firstRankId] = sc;
++count;
}
} while (result->NextRow());
LOG_INFO("server.loading", ">> Loaded {} Spell Cone definitions in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
}
void SpellMgr::LoadSpellGroups() void SpellMgr::LoadSpellGroups()
{ {
uint32 oldMSTime = getMSTime(); uint32 oldMSTime = getMSTime();
+10
View File
@@ -393,6 +393,13 @@ struct SpellTargetPosition
typedef std::map<std::pair<uint32 /*spell_id*/, SpellEffIndex /*effIndex*/>, SpellTargetPosition> SpellTargetPositionMap; typedef std::map<std::pair<uint32 /*spell_id*/, SpellEffIndex /*effIndex*/>, SpellTargetPosition> SpellTargetPositionMap;
struct SpellCone
{
int16_t cone_degrees;
};
typedef std::map<uint32 /*spell_id*/, SpellCone> SpellConeMap;
// Enum with EffectRadiusIndex and their actual radius // Enum with EffectRadiusIndex and their actual radius
enum EffectRadiusIndex enum EffectRadiusIndex
{ {
@@ -683,6 +690,7 @@ public:
// Spell target coordinates // Spell target coordinates
[[nodiscard]] SpellTargetPosition const* GetSpellTargetPosition(uint32 spell_id, SpellEffIndex effIndex) const; [[nodiscard]] SpellTargetPosition const* GetSpellTargetPosition(uint32 spell_id, SpellEffIndex effIndex) const;
[[nodiscard]] SpellCone const* GetSpellCone(uint32 spell_id) const;
// Spell Groups table // Spell Groups table
SpellSpellGroupMapBounds GetSpellSpellGroupMapBounds(uint32 spell_id) const; SpellSpellGroupMapBounds GetSpellSpellGroupMapBounds(uint32 spell_id) const;
@@ -775,6 +783,7 @@ public:
void LoadSpellRequired(); void LoadSpellRequired();
void LoadSpellLearnSkills(); void LoadSpellLearnSkills();
void LoadSpellTargetPositions(); void LoadSpellTargetPositions();
void LoadSpellCones();
void LoadSpellGroups(); void LoadSpellGroups();
void LoadSpellGroupStackRules(); void LoadSpellGroupStackRules();
void LoadSpellProcs(); void LoadSpellProcs();
@@ -807,6 +816,7 @@ private:
SpellRequiredMap mSpellReq; SpellRequiredMap mSpellReq;
SpellLearnSkillMap mSpellLearnSkills; SpellLearnSkillMap mSpellLearnSkills;
SpellTargetPositionMap mSpellTargetPositions; SpellTargetPositionMap mSpellTargetPositions;
SpellConeMap mSpellCones;
SpellSpellGroupMap mSpellSpellGroup; SpellSpellGroupMap mSpellSpellGroup;
SpellGroupSpellMap mSpellGroupSpell; SpellGroupSpellMap mSpellGroupSpell;
SpellGroupStackMap mSpellGroupStack; SpellGroupStackMap mSpellGroupStack;
+3
View File
@@ -683,6 +683,9 @@ void World::SetInitialWorldSettings()
LOG_INFO("server.loading", "Loading Spell Target Coordinates..."); LOG_INFO("server.loading", "Loading Spell Target Coordinates...");
sSpellMgr->LoadSpellTargetPositions(); sSpellMgr->LoadSpellTargetPositions();
LOG_INFO("server.loading", "Loading Spell Cone definitions...");
sSpellMgr->LoadSpellCones();
LOG_INFO("server.loading", "Loading Enchant Custom Attributes..."); LOG_INFO("server.loading", "Loading Enchant Custom Attributes...");
sSpellMgr->LoadEnchantCustomAttr(); sSpellMgr->LoadEnchantCustomAttr();
+2
View File
@@ -595,6 +595,8 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<uint32>(CONFIG_WINTERGRASP_RESTART_AFTER_CRASH, "Wintergrasp.CrashRestartTimer", 10); SetConfigValue<uint32>(CONFIG_WINTERGRASP_RESTART_AFTER_CRASH, "Wintergrasp.CrashRestartTimer", 10);
SetConfigValue<uint32>(CONFIG_WINTERGRASP_SKIP_BATTLE_SESSION_COUNT, "Wintergrasp.SkipBattleSessionCount", 3500); SetConfigValue<uint32>(CONFIG_WINTERGRASP_SKIP_BATTLE_SESSION_COUNT, "Wintergrasp.SkipBattleSessionCount", 3500);
SetConfigValue<bool>(CONFIG_WINTERGRASP_KICK_VOA_PLAYERS, "Wintergrasp.KickVoAPlayers", true, ConfigValueCache::Reloadable::No);
SetConfigValue<bool>(CONFIG_WINTERGRASP_ESSENCE_BOTH_FACTIONS, "Wintergrasp.EssenceBothFactions", false);
SetConfigValue<uint32>(CONFIG_BIRTHDAY_TIME, "BirthdayTime", 1222964635); SetConfigValue<uint32>(CONFIG_BIRTHDAY_TIME, "BirthdayTime", 1222964635);
SetConfigValue<bool>(CONFIG_MINIGOB_MANABONK, "Minigob.Manabonk.Enable", true); SetConfigValue<bool>(CONFIG_MINIGOB_MANABONK, "Minigob.Manabonk.Enable", true);
+2
View File
@@ -326,6 +326,8 @@ enum ServerConfigs
CONFIG_WINTERGRASP_NOBATTLETIME, CONFIG_WINTERGRASP_NOBATTLETIME,
CONFIG_WINTERGRASP_RESTART_AFTER_CRASH, CONFIG_WINTERGRASP_RESTART_AFTER_CRASH,
CONFIG_WINTERGRASP_SKIP_BATTLE_SESSION_COUNT, CONFIG_WINTERGRASP_SKIP_BATTLE_SESSION_COUNT,
CONFIG_WINTERGRASP_KICK_VOA_PLAYERS,
CONFIG_WINTERGRASP_ESSENCE_BOTH_FACTIONS,
CONFIG_PACKET_SPOOF_BANMODE, CONFIG_PACKET_SPOOF_BANMODE,
CONFIG_PACKET_SPOOF_BANDURATION, CONFIG_PACKET_SPOOF_BANDURATION,
CONFIG_WARDEN_CLIENT_RESPONSE_DELAY, CONFIG_WARDEN_CLIENT_RESPONSE_DELAY,
+76
View File
@@ -19,6 +19,8 @@
#include "CommandScript.h" #include "CommandScript.h"
#include "DatabaseEnv.h" #include "DatabaseEnv.h"
#include <algorithm> #include <algorithm>
#include "GameTime.h"
#include "Group.h"
#include "Language.h" #include "Language.h"
#include "Log.h" #include "Log.h"
#include "ObjectMgr.h" #include "ObjectMgr.h"
@@ -43,6 +45,7 @@ public:
{ "delete", HandlePetDeleteCommand, rbac::RBAC_PERM_COMMAND_PET_DELETE, Console::Yes }, { "delete", HandlePetDeleteCommand, rbac::RBAC_PERM_COMMAND_PET_DELETE, Console::Yes },
{ "learn", HandlePetLearnCommand, rbac::RBAC_PERM_COMMAND_PET_LEARN, Console::No }, { "learn", HandlePetLearnCommand, rbac::RBAC_PERM_COMMAND_PET_LEARN, Console::No },
{ "list", HandlePetListCommand, rbac::RBAC_PERM_COMMAND_PET_LIST, Console::Yes }, { "list", HandlePetListCommand, rbac::RBAC_PERM_COMMAND_PET_LIST, Console::Yes },
{ "rename", HandlePetRenameCommand, rbac::RBAC_PERM_COMMAND_PET_RENAME, Console::Yes },
{ "unlearn", HandlePetUnlearnCommand, rbac::RBAC_PERM_COMMAND_PET_UNLEARN, Console::No } { "unlearn", HandlePetUnlearnCommand, rbac::RBAC_PERM_COMMAND_PET_UNLEARN, Console::No }
}; };
@@ -227,6 +230,79 @@ public:
return true; return true;
} }
static bool HandlePetRenameCommand(ChatHandler* handler, PlayerIdentifier owner, uint32 petNumber, std::string newName)
{
if (ObjectMgr::CheckPetName(newName) != PET_NAME_SUCCESS)
{
handler->SendErrorMessage(LANG_PET_RENAME_INVALID, newName);
return false;
}
ObjectGuid ownerGuid = owner.GetGUID();
QueryResult result = CharacterDatabase.Query("SELECT name FROM character_pet WHERE id = {} AND owner = {}", petNumber, ownerGuid.GetCounter());
if (!result)
{
handler->SendErrorMessage(LANG_PET_DELETE_NOT_FOUND, petNumber, owner.GetName(), ownerGuid.GetCounter());
return false;
}
std::string oldName = result->Fetch()[0].Get<std::string>();
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_PET_NAME);
stmt->SetData(0, newName);
stmt->SetData(1, ownerGuid.GetCounter());
stmt->SetData(2, petNumber);
CharacterDatabase.Execute(stmt);
if (Player* online = owner.GetConnectedPlayer())
{
if (Pet* activePet = online->GetPet())
{
if (activePet->GetCharmInfo() && activePet->GetCharmInfo()->GetPetNumber() == petNumber)
{
activePet->SetName(newName);
// Bump the name timestamp so nearby clients re-query and drop their cached pet name.
activePet->SetUInt32Value(UNIT_FIELD_PET_NAME_TIMESTAMP, uint32(GameTime::GetGameTime().count()));
activePet->RemoveByteFlag(UNIT_FIELD_BYTES_2, 2, UNIT_CAN_BE_RENAMED);
if (online->GetGroup())
online->SetGroupUpdateFlag(GROUP_UPDATE_FLAG_PET_NAME);
}
}
if (PetStable* stable = online->GetPetStable())
{
if (stable->CurrentPet && stable->CurrentPet->PetNumber == petNumber)
{
stable->CurrentPet->Name = newName;
stable->CurrentPet->WasRenamed = true;
}
for (Optional<PetStable::PetInfo>& stabled : stable->StabledPets)
{
if (stabled && stabled->PetNumber == petNumber)
{
stabled->Name = newName;
stabled->WasRenamed = true;
}
}
for (PetStable::PetInfo& unslotted : stable->UnslottedPets)
{
if (unslotted.PetNumber == petNumber)
{
unslotted.Name = newName;
unslotted.WasRenamed = true;
}
}
}
}
handler->PSendSysMessage(LANG_PET_RENAME_SUCCESS, petNumber, oldName, newName, owner.GetName(), ownerGuid.GetCounter());
return true;
}
static bool HandlePetUnlearnCommand(ChatHandler* handler, SpellInfo const* spell) static bool HandlePetUnlearnCommand(ChatHandler* handler, SpellInfo const* spell)
{ {
if (!spell) if (!spell)
@@ -48,15 +48,31 @@ public:
PlayerSetting setting; PlayerSetting setting;
setting = player->GetPlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS); setting = player->GetPlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS);
char const* label = nullptr;
if (type == "bg") if (type == "bg")
{ {
on ? setting.RemoveFlag(ANNOUNCER_FLAG_DISABLE_BG_QUEUE) : setting.AddFlag(ANNOUNCER_FLAG_DISABLE_BG_QUEUE); on ? setting.RemoveFlag(ANNOUNCER_FLAG_DISABLE_BG_QUEUE) : setting.AddFlag(ANNOUNCER_FLAG_DISABLE_BG_QUEUE);
player->UpdatePlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS, setting.value); player->UpdatePlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS, setting.value);
label = "battleground queue";
} }
else if (type == "arena") else if (type == "arena")
{ {
on ? setting.RemoveFlag(ANNOUNCER_FLAG_DISABLE_ARENA_QUEUE) : setting.AddFlag(ANNOUNCER_FLAG_DISABLE_ARENA_QUEUE); on ? setting.RemoveFlag(ANNOUNCER_FLAG_DISABLE_ARENA_QUEUE) : setting.AddFlag(ANNOUNCER_FLAG_DISABLE_ARENA_QUEUE);
player->UpdatePlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS, setting.value); player->UpdatePlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS, setting.value);
label = "arena queue";
}
else if (type == "pvpstart")
{
on ? setting.RemoveFlag(ANNOUNCER_FLAG_DISABLE_PVP_START) : setting.AddFlag(ANNOUNCER_FLAG_DISABLE_PVP_START);
player->UpdatePlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS, setting.value);
label = "PvP start";
}
else if (type == "pvpall")
{
on ? setting.RemoveFlag(ANNOUNCER_FLAG_DISABLE_PVP_ALL) : setting.AddFlag(ANNOUNCER_FLAG_DISABLE_PVP_ALL);
player->UpdatePlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS, setting.value);
label = "PvP";
} }
else if (type == "autobroadcast") else if (type == "autobroadcast")
{ {
@@ -68,10 +84,11 @@ public:
on ? setting.RemoveFlag(ANNOUNCER_FLAG_DISABLE_AUTOBROADCAST) : setting.AddFlag(ANNOUNCER_FLAG_DISABLE_AUTOBROADCAST); on ? setting.RemoveFlag(ANNOUNCER_FLAG_DISABLE_AUTOBROADCAST) : setting.AddFlag(ANNOUNCER_FLAG_DISABLE_AUTOBROADCAST);
player->UpdatePlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS, setting.value); player->UpdatePlayerSetting(AzerothcorePSSource, SETTING_ANNOUNCER_FLAGS, setting.value);
label = "autobroadcast";
} }
handler->SetSentErrorMessage(false); handler->SetSentErrorMessage(false);
handler->PSendSysMessage(on ? LANG_CMD_SETTINGS_ANNOUNCER_ON : LANG_CMD_SETTINGS_ANNOUNCER_OFF, type); handler->PSendSysMessage(on ? LANG_CMD_SETTINGS_ANNOUNCER_ON : LANG_CMD_SETTINGS_ANNOUNCER_OFF, label ? label : type.c_str());
return true; return true;
} }
}; };
@@ -164,6 +164,7 @@ public:
{ "spell_proc", HandleReloadSpellProcsCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_PROC, Console::Yes }, { "spell_proc", HandleReloadSpellProcsCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_PROC, Console::Yes },
{ "spell_scripts", HandleReloadSpellScriptsCommand, rbac::RBAC_PERM_COMMAND_RELOAD, Console::Yes }, { "spell_scripts", HandleReloadSpellScriptsCommand, rbac::RBAC_PERM_COMMAND_RELOAD, Console::Yes },
{ "spell_target_position", HandleReloadSpellTargetPositionCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_TARGET_POSITION, Console::Yes }, { "spell_target_position", HandleReloadSpellTargetPositionCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_TARGET_POSITION, Console::Yes },
{ "spell_cone", HandleReloadSpellConeCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_TARGET_POSITION, Console::Yes },
{ "spell_threats", HandleReloadSpellThreatsCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_THREATS, Console::Yes }, { "spell_threats", HandleReloadSpellThreatsCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_THREATS, Console::Yes },
{ "spell_group_stack_rules", HandleReloadSpellGroupStackRulesCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_GROUP_STACK_RULES, Console::Yes }, { "spell_group_stack_rules", HandleReloadSpellGroupStackRulesCommand, rbac::RBAC_PERM_COMMAND_RELOAD_SPELL_GROUP_STACK_RULES, Console::Yes },
{ "player_loot_template", HandleReloadLootTemplatesPlayerCommand, rbac::RBAC_PERM_COMMAND_RELOAD, Console::Yes }, { "player_loot_template", HandleReloadLootTemplatesPlayerCommand, rbac::RBAC_PERM_COMMAND_RELOAD, Console::Yes },
@@ -936,6 +937,14 @@ public:
return true; return true;
} }
static bool HandleReloadSpellConeCommand(ChatHandler* handler)
{
LOG_INFO("server.loading", "Reloading Spell cone definitions...");
sSpellMgr->LoadSpellCones();
handler->SendGlobalGMSysMessage("DB table `spell_cone` reloaded.");
return true;
}
static bool HandleReloadSpellThreatsCommand(ChatHandler* handler) static bool HandleReloadSpellThreatsCommand(ChatHandler* handler)
{ {
LOG_INFO("server.loading", "Reloading Aggro Spells Definitions..."); LOG_INFO("server.loading", "Reloading Aggro Spells Definitions...");
+2 -2
View File
@@ -340,7 +340,7 @@ public:
uint32 a2, a3, a4, a5, a6; uint32 a2, a3, a4, a5, a6;
float a8, a9, a10, a11; float a8, a9, a10, a11;
char const* a7; std::string a7;
WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_WAYPOINT_SCRIPT_BY_ID); WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_WAYPOINT_SCRIPT_BY_ID);
stmt->SetData(0, id); stmt->SetData(0, id);
@@ -362,7 +362,7 @@ public:
a4 = fields[2].Get<uint32>(); a4 = fields[2].Get<uint32>();
a5 = fields[3].Get<uint32>(); a5 = fields[3].Get<uint32>();
a6 = fields[4].Get<uint32>(); a6 = fields[4].Get<uint32>();
a7 = fields[5].Get<std::string>().c_str(); a7 = fields[5].Get<std::string>();
a8 = fields[6].Get<float>(); a8 = fields[6].Get<float>();
a9 = fields[7].Get<float>(); a9 = fields[7].Get<float>();
a10 = fields[8].Get<float>(); a10 = fields[8].Get<float>();
@@ -119,6 +119,8 @@ struct boss_moroes : public BossAI
DoCastSelf(SPELL_DUAL_WIELD, true); DoCastSelf(SPELL_DUAL_WIELD, true);
_recentlySpoken = false; _recentlySpoken = false;
_vanished = false; _vanished = false;
me->SetImmuneToAll(false);
me->SetReactState(REACT_AGGRESSIVE);
InitializeGuests(); InitializeGuests();
@@ -142,9 +144,11 @@ struct boss_moroes : public BossAI
Talk(SAY_SPECIAL); Talk(SAY_SPECIAL);
DoCastSelf(SPELL_VANISH); DoCastSelf(SPELL_VANISH);
me->SetImmuneToAll(true, true); me->SetImmuneToAll(true, true);
me->SetReactState(REACT_PASSIVE);
scheduler.Schedule(5s, 7s, [this](TaskContext) scheduler.Schedule(5s, 7s, [this](TaskContext)
{ {
me->SetImmuneToAll(false); me->SetImmuneToAll(false);
me->SetReactState(REACT_AGGRESSIVE);
DoCastSelf(SPELL_VANISH_TELEPORT); DoCastSelf(SPELL_VANISH_TELEPORT);
_vanished = false; _vanished = false;
}); });
@@ -395,9 +395,12 @@ public:
active->RemoveNpcFlag(UNIT_NPC_FLAG_QUESTGIVER); active->RemoveNpcFlag(UNIT_NPC_FLAG_QUESTGIVER);
break; break;
case EVENT_RESTART_ESCORT: case EVENT_RESTART_ESCORT:
CheckCaravan(); if (!IsEscorted())
SetDespawnAtEnd(false); {
Start(true, ObjectGuid::Empty, 0, false, false, true); CheckCaravan();
SetDespawnAtEnd(false);
Start(true, ObjectGuid::Empty, 0, false, false, true);
}
break; break;
} }
@@ -217,16 +217,21 @@ public:
break; break;
} }
events.Repeat(5s); events.Repeat(5s);
Unit* unit = SelectTarget(SelectTargetMethod::Random, 0, BoneStormMoveTargetSelector(me)); std::list<Unit*> targets;
if (!unit) SelectTargetList(targets, Is25ManRaid() ? 2 : 1, SelectTargetMethod::MaxDistance, 0, BoneStormMoveTargetSelector(me));
Unit* unit = nullptr;
if (!targets.empty())
unit = targets.size() == 1 ? targets.front() : Acore::Containers::SelectRandomContainerElement(targets);
else if ((unit = SelectTarget(SelectTargetMethod::MaxThreat, 0, 175.0f, true)))
{ {
if ((unit = SelectTarget(SelectTargetMethod::MaxThreat, 0, 175.0f, true))) if (unit->GetPositionX() > -337.0f)
if (unit->GetPositionX() > -337.0f) {
{ EnterEvadeMode();
EnterEvadeMode(); return;
return; }
}
} }
if (unit) if (unit)
me->GetMotionMaster()->MoveCharge(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ(), 25.0f, 1337); me->GetMotionMaster()->MoveCharge(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ(), 25.0f, 1337);
break; break;
@@ -22,6 +22,7 @@
#include "Player.h" #include "Player.h"
#include "ScriptedCreature.h" #include "ScriptedCreature.h"
#include "SpellAuras.h" #include "SpellAuras.h"
#include "World.h"
#include "vault_of_archavon.h" #include "vault_of_archavon.h"
/* Vault of Archavon encounters: /* Vault of Archavon encounters:
@@ -73,6 +74,8 @@ public:
if (checkTimer >= 60000) if (checkTimer >= 60000)
{ {
checkTimer -= 60000; // one minute checkTimer -= 60000; // one minute
if (!sWorld->getBoolConfig(CONFIG_WINTERGRASP_KICK_VOA_PLAYERS))
return;
if (Battlefield* bf = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG)) if (Battlefield* bf = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG))
{ {
if (!bf->IsWarTime()) if (!bf->IsWarTime())
@@ -133,6 +136,9 @@ public:
if (m_auiEncounter[i] == IN_PROGRESS) if (m_auiEncounter[i] == IN_PROGRESS)
return true; return true;
if (!sWorld->getBoolConfig(CONFIG_WINTERGRASP_KICK_VOA_PLAYERS))
return false;
Battlefield* bf = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG); Battlefield* bf = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG);
if (!bf || bf->IsWarTime() || bf->GetTimer() <= 10 * MINUTE * IN_MILLISECONDS) if (!bf || bf->IsWarTime() || bf->GetTimer() <= 10 * MINUTE * IN_MILLISECONDS)
return true; return true;
@@ -28,7 +28,6 @@
#include "ObjectMgr.h" #include "ObjectMgr.h"
#include "Player.h" #include "Player.h"
#include "PoolMgr.h" #include "PoolMgr.h"
#include "RaceMgr.h"
#include "ScriptedCreature.h" #include "ScriptedCreature.h"
#include "ScriptedGossip.h" #include "ScriptedGossip.h"
#include "SpellScript.h" #include "SpellScript.h"
@@ -216,13 +215,19 @@ public:
bool OnGossipHello(Player* player, Creature* creature) override bool OnGossipHello(Player* player, Creature* creature) override
{ {
if (creature->IsQuestGiver())
player->PrepareQuestMenu(creature->GetGUID());
Battlefield* wintergrasp = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG); Battlefield* wintergrasp = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG);
if (!wintergrasp) if (!wintergrasp)
return true; return true;
if (!player->IsAlive())
{
wintergrasp->SendAreaSpiritHealerQueryOpcode(player, creature->GetGUID());
player->CastSpell(player, SPELL_WAITING_FOR_RESURRECT, true);
}
if (creature->IsQuestGiver())
player->PrepareQuestMenu(creature->GetGUID());
GraveyardVect graveyard = wintergrasp->GetGraveyardVector(); GraveyardVect graveyard = wintergrasp->GetGraveyardVector();
for (uint8 i = 0; i < graveyard.size(); i++) for (uint8 i = 0; i < graveyard.size(); i++)
if (graveyard[i]->GetControlTeamId() == player->GetTeamId()) if (graveyard[i]->GetControlTeamId() == player->GetTeamId())
@@ -860,8 +865,12 @@ public:
bool IsFriendly(Unit* passenger) bool IsFriendly(Unit* passenger)
{ {
return ((me->GetUInt32Value(GAMEOBJECT_FACTION) == WintergraspFaction[TEAM_HORDE] && passenger->getRaceMask() & sRaceMgr->GetHordeRaceMask()) || Player* player = passenger->ToPlayer();
(me->GetUInt32Value(GAMEOBJECT_FACTION) == WintergraspFaction[TEAM_ALLIANCE] && passenger->getRaceMask() & sRaceMgr->GetAllianceRaceMask())); if (!player)
return false;
TeamId goTeam = me->GetUInt32Value(GAMEOBJECT_FACTION) == WintergraspFaction[TEAM_HORDE] ? TEAM_HORDE : TEAM_ALLIANCE;
return player->GetTeamId() == goTeam;
} }
Creature* IsValidVehicle(Creature* cVeh) Creature* IsValidVehicle(Creature* cVeh)
@@ -1160,6 +1169,10 @@ public:
if (!wintergrasp) if (!wintergrasp)
return false; return false;
// Attacker-only achievement -- defenders winning fast must not qualify.
if (!sBattlefieldMgr->IsWintergraspAttackerVictory())
return false;
return wintergrasp->GetTimer() >= (20 * MINUTE * IN_MILLISECONDS); return wintergrasp->GetTimer() >= (20 * MINUTE * IN_MILLISECONDS);
} }
}; };
@@ -70,12 +70,6 @@ enum Groups
GROUP_EARLY_RELEASE_CHECK = 0 GROUP_EARLY_RELEASE_CHECK = 0
}; };
enum Actions
{
ACTION_INCREASE_HELLFIRE_CHANNELER_DEATH_COUNT = 1,
ACTION_BANISH_SELF = 2
};
struct boss_magtheridon : public BossAI struct boss_magtheridon : public BossAI
{ {
boss_magtheridon(Creature* creature) : BossAI(creature, DATA_MAGTHERIDON) boss_magtheridon(Creature* creature) : BossAI(creature, DATA_MAGTHERIDON)
@@ -84,7 +78,6 @@ struct boss_magtheridon : public BossAI
void Reset() override void Reset() override
{ {
BossAI::Reset(); BossAI::Reset();
_channelersKilled = 0;
_currentPhase = 0; _currentPhase = 0;
_castingQuake = false; _castingQuake = false;
_recentlySpoken = false; _recentlySpoken = false;
@@ -194,21 +187,19 @@ struct boss_magtheridon : public BossAI
void DoAction(int32 action) override void DoAction(int32 action) override
{ {
if (action == ACTION_INCREASE_HELLFIRE_CHANNELER_DEATH_COUNT) if (action == ACTION_RELEASE_MAGTHERIDON)
{ {
_channelersKilled++; if (_magReleased)
return;
if (_channelersKilled >= 5 && !_magReleased) Talk(SAY_EMOTE_FREE);
Talk(SAY_FREE);
scheduler.CancelGroup(GROUP_EARLY_RELEASE_CHECK); //cancel regular countdown
_magReleased = true;
scheduler.Schedule(3s, [this](TaskContext)
{ {
Talk(SAY_EMOTE_FREE); ScheduleCombatEvents();
Talk(SAY_FREE); });
scheduler.CancelGroup(GROUP_EARLY_RELEASE_CHECK); //cancel regular countdown
_magReleased = true;
scheduler.Schedule(3s, [this](TaskContext)
{
ScheduleCombatEvents();
});
}
} }
else if (action == ACTION_BANISH_SELF) else if (action == ACTION_BANISH_SELF)
{ {
@@ -222,10 +213,6 @@ struct boss_magtheridon : public BossAI
BossAI::JustEngagedWith(who); BossAI::JustEngagedWith(who);
Talk(SAY_EMOTE_BEGIN); Talk(SAY_EMOTE_BEGIN);
instance->DoForAllMinions(DATA_MAGTHERIDON, [&](Creature* creature) {
creature->SetInCombatWithZone();
});
scheduler.Schedule(60s, GROUP_EARLY_RELEASE_CHECK, [this](TaskContext /*context*/) scheduler.Schedule(60s, GROUP_EARLY_RELEASE_CHECK, [this](TaskContext /*context*/)
{ {
Talk(SAY_EMOTE_NEARLY); Talk(SAY_EMOTE_NEARLY);
@@ -260,7 +247,6 @@ private:
bool _recentlySpoken; bool _recentlySpoken;
bool _magReleased; bool _magReleased;
uint8 _currentPhase; uint8 _currentPhase;
uint8 _channelersKilled;
TaskScheduler _interruptScheduler; TaskScheduler _interruptScheduler;
}; };
@@ -15,6 +15,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "CreatureAI.h"
#include "InstanceMapScript.h" #include "InstanceMapScript.h"
#include "InstanceScript.h" #include "InstanceScript.h"
#include "magtheridons_lair.h" #include "magtheridons_lair.h"
@@ -30,12 +31,6 @@ DoorData const doorData[] =
{ 0, 0, DOOR_TYPE_ROOM } // END { 0, 0, DOOR_TYPE_ROOM } // END
}; };
MinionData const minionData[] =
{
{ NPC_HELLFIRE_CHANNELER, DATA_MAGTHERIDON },
{ 0, 0 } // END
};
class instance_magtheridons_lair : public InstanceMapScript class instance_magtheridons_lair : public InstanceMapScript
{ {
public: public:
@@ -48,17 +43,28 @@ public:
SetHeaders(DataHeader); SetHeaders(DataHeader);
SetBossNumber(MAX_ENCOUNTER); SetBossNumber(MAX_ENCOUNTER);
LoadDoorData(doorData); LoadDoorData(doorData);
LoadMinionData(minionData);
LoadBossBoundaries(boundaries); LoadBossBoundaries(boundaries);
} }
void Initialize() override void Initialize() override
{ {
_channelersSet.clear();
_wardersSet.clear(); _wardersSet.clear();
_burningAbyssalsSet.clear();
_cubesSet.clear(); _cubesSet.clear();
_columnSet.clear(); _columnSet.clear();
} }
bool IsAnyChannelerAlive()
{
return std::ranges::any_of(_channelersSet, [&](ObjectGuid const& guid)
{
if (Creature* channeler = instance->GetCreature(guid))
return channeler->IsAlive();
return false;
});
}
void OnCreatureCreate(Creature* creature) override void OnCreatureCreate(Creature* creature) override
{ {
switch (creature->GetEntry()) switch (creature->GetEntry())
@@ -67,22 +73,42 @@ public:
_magtheridonGUID = creature->GetGUID(); _magtheridonGUID = creature->GetGUID();
break; break;
case NPC_HELLFIRE_CHANNELER: case NPC_HELLFIRE_CHANNELER:
AddMinion(creature); _channelersSet.insert(creature->GetGUID());
break; break;
case NPC_HELLFIRE_WARDER: case NPC_HELLFIRE_WARDER:
_wardersSet.insert(creature->GetGUID()); _wardersSet.insert(creature->GetGUID());
break; break;
case NPC_BURNING_ABYSSAL:
_burningAbyssalsSet.insert(creature->GetGUID());
break;
} }
} }
void OnCreatureRemove(Creature* creature) override void OnCreatureRemove(Creature* creature) override
{ {
switch (creature->GetEntry()) if (creature->GetEntry() == NPC_BURNING_ABYSSAL)
{ _burningAbyssalsSet.erase(creature->GetGUID());
case NPC_HELLFIRE_CHANNELER: }
RemoveMinion(creature);
break; void OnUnitDeath(Unit* unit) override
} {
Creature* creature = unit ? unit->ToCreature() : nullptr;
if (!creature || creature->GetEntry() != NPC_HELLFIRE_CHANNELER)
return;
// IN_PROGRESS guard: stays inert during the hard-reset Respawn(true) cycle.
if (GetBossState(DATA_MAGTHERIDON) != IN_PROGRESS || IsAnyChannelerAlive())
return;
if (Creature* magtheridon = instance->GetCreature(_magtheridonGUID))
magtheridon->AI()->DoAction(ACTION_RELEASE_MAGTHERIDON);
}
void OnCreatureEvade(Creature* creature) override
{
// Phase-1 wipe signal: Mag is ImmuneToPC so BossAI evade does not fire; a Channeler evade is the trigger.
if (creature->GetEntry() == NPC_HELLFIRE_CHANNELER && GetBossState(DATA_MAGTHERIDON) == IN_PROGRESS)
SetBossState(DATA_MAGTHERIDON, NOT_STARTED);
} }
void OnGameObjectCreate(GameObject* go) override void OnGameObjectCreate(GameObject* go) override
@@ -154,6 +180,18 @@ public:
if (state == NOT_STARTED) if (state == NOT_STARTED)
SetData(DATA_COLLAPSE, GO_READY); SetData(DATA_COLLAPSE, GO_READY);
// Hard reset: vanish Channelers and their lingering Burning Abyssal summons.
if (state == NOT_STARTED || state == FAIL)
{
for (ObjectGuid const& guid : _channelersSet)
if (Creature* channeler = instance->GetCreature(guid))
channeler->Respawn(true);
for (ObjectGuid const& guid : _burningAbyssalsSet)
if (Creature* abyssal = instance->GetCreature(guid))
abyssal->DespawnOrUnsummon();
}
} }
} }
return true; return true;
@@ -164,9 +202,13 @@ public:
switch (type) switch (type)
{ {
case DATA_CHANNELER_COMBAT: case DATA_CHANNELER_COMBAT:
// Force the encounter start: Mag is ImmuneToPC so SetInCombatWithZone alone may miss JustEngagedWith.
if (GetBossState(DATA_MAGTHERIDON) != IN_PROGRESS) if (GetBossState(DATA_MAGTHERIDON) != IN_PROGRESS)
{
SetBossState(DATA_MAGTHERIDON, IN_PROGRESS);
if (Creature* magtheridon = instance->GetCreature(_magtheridonGUID)) if (Creature* magtheridon = instance->GetCreature(_magtheridonGUID))
magtheridon->SetInCombatWithZone(); magtheridon->SetInCombatWithZone();
}
break; break;
case DATA_ACTIVATE_CUBES: case DATA_ACTIVATE_CUBES:
for (ObjectGuid const& guid : _cubesSet) for (ObjectGuid const& guid : _cubesSet)
@@ -178,12 +220,16 @@ public:
if (GameObject* column = instance->GetGameObject(guid)) if (GameObject* column = instance->GetGameObject(guid))
column->SetGoState(GOState(data)); column->SetGoState(GOState(data));
break; break;
default:
break;
} }
} }
private: private:
ObjectGuid _magtheridonGUID; ObjectGuid _magtheridonGUID;
GuidSet _channelersSet;
GuidSet _wardersSet; GuidSet _wardersSet;
GuidSet _burningAbyssalsSet;
GuidSet _cubesSet; GuidSet _cubesSet;
GuidSet _columnSet; GuidSet _columnSet;
}; };
@@ -39,7 +39,14 @@ enum NpcIds
NPC_HELLFIRE_CHANNELER = 17256, NPC_HELLFIRE_CHANNELER = 17256,
NPC_HELLFIRE_WARDER = 18829, NPC_HELLFIRE_WARDER = 18829,
NPC_HELLFIRE_RAID_TRIGGER = 17376, NPC_HELLFIRE_RAID_TRIGGER = 17376,
NPC_TARGET_TRIGGER = 17474 NPC_TARGET_TRIGGER = 17474,
NPC_BURNING_ABYSSAL = 17454
};
enum MagtheridonActions
{
ACTION_RELEASE_MAGTHERIDON = 1,
ACTION_BANISH_SELF = 2
}; };
enum GoIds enum GoIds
+8 -10
View File
@@ -787,27 +787,25 @@ class spell_item_feast : public SpellScript
Unit* caster = GetCaster(); Unit* caster = GetCaster();
if (Player* player = caster->ToPlayer()) if (Player* player = caster->ToPlayer())
{ {
LocaleConstant loc_idx = player->GetSession()->GetSessionDbLocaleIndex(); uint32 textId = 0;
switch (GetSpellInfo()->Id) switch (GetSpellInfo()->Id)
{ {
case SPELL_GREAT_FEAST: case SPELL_GREAT_FEAST:
if (BroadcastText const* bct = sObjectMgr->GetBroadcastText(GREAT_FEAST_BROADCAST_TEXT_ID_PREPARE)) textId = GREAT_FEAST_BROADCAST_TEXT_ID_PREPARE;
player->TextEmote(bct->GetText(loc_idx, player->getGender()), player);
break; break;
case SPELL_FISH_FEAST: case SPELL_FISH_FEAST:
if (BroadcastText const* bct = sObjectMgr->GetBroadcastText(FISH_FEAST_BROADCAST_TEXT_ID_PREPARE)) textId = FISH_FEAST_BROADCAST_TEXT_ID_PREPARE;
player->TextEmote(bct->GetText(loc_idx, player->getGender()), player);
break; break;
case SPELL_SMALL_FEAST: case SPELL_SMALL_FEAST:
if (BroadcastText const* bct = sObjectMgr->GetBroadcastText(SMALL_FEAST_BROADCAST_TEXT_ID_PREPARE)) textId = SMALL_FEAST_BROADCAST_TEXT_ID_PREPARE;
player->TextEmote(bct->GetText(loc_idx, player->getGender()), player);
break; break;
case SPELL_GIGANTIC_FEAST: case SPELL_GIGANTIC_FEAST:
if (BroadcastText const* bct = sObjectMgr->GetBroadcastText(GIGANTIC_FEAST_BROADCAST_TEXT_ID_PREPARE)) textId = GIGANTIC_FEAST_BROADCAST_TEXT_ID_PREPARE;
player->TextEmote(bct->GetText(loc_idx, player->getGender()), player); break;
default:
break; break;
} }
player->Unit::TextEmote(textId, player);
} }
} }
+15 -8
View File
@@ -52,18 +52,25 @@ public:
if (player->GetZoneId() != AREA_ISLE_OF_QUEL_DANAS) if (player->GetZoneId() != AREA_ISLE_OF_QUEL_DANAS)
disabled = true; disabled = true;
break; break;
case 34475:
if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(SPELL_ARCANE_CHARGES))
Spell::SendCastResult(player, spellInfo, 1, SPELL_FAILED_NOT_ON_GROUND);
break;
} }
// allow use in flight only // allow use in flight only
if (player->IsInFlight() && !disabled) if (player->IsInFlight())
return false; {
if (!disabled)
return false;
}
else // error
{
if (itemId == 34475)
{
if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(SPELL_ARCANE_CHARGES))
Spell::SendCastResult(player, spellInfo, 1, SPELL_FAILED_NOT_ON_GROUND);
}
else
player->SendEquipError(EQUIP_ERR_CANT_DO_RIGHT_NOW, item, nullptr);
}
// error
player->SendEquipError(EQUIP_ERR_CANT_DO_RIGHT_NOW, item, nullptr);
return true; return true;
} }
}; };
@@ -734,7 +734,7 @@ TEST_F(ThreatManagerIntegrationTest,
} }
// ============================================================================ // ============================================================================
// Phase Change + Threat Offline State Tests (SetPhaseMask order fix) // Phase Change + Threat Offline State Tests
// ============================================================================ // ============================================================================
TEST_F(ThreatManagerIntegrationTest, TEST_F(ThreatManagerIntegrationTest,
@@ -748,6 +748,10 @@ TEST_F(ThreatManagerIntegrationTest,
// Move B to a different phase // Move B to a different phase
_creatureB->SetPhase(2); _creatureB->SetPhase(2);
// Online state is refreshed lazily on the next threat update tick (matches
// TrinityCore - SetPhaseMask itself does not touch threat)
_creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL);
// B should now be offline on A's threat list (different phases) // B should now be offline on A's threat list (different phases)
// The list is not empty if we include offline, but empty if we don't // The list is not empty if we include offline, but empty if we don't
EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB, true)); // includeOffline EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB, true)); // includeOffline