Merge pull request #226 from mod-playerbots/test-staging

Test staging
This commit is contained in:
Keleborn
2026-07-17 15:06:47 -07:00
committed by GitHub
56 changed files with 1992 additions and 1153 deletions
+4
View File
@@ -104,6 +104,10 @@ local.properties
/vcpkg/
/vcpkg-ports/
# Python bytecode cache
__pycache__/
*.py[cod]
# ==================
#
+54 -5
View File
@@ -210,6 +210,36 @@ def insert_delete_safety_check(file: io, file_path: str) -> None:
error_handler = True
results["INSERT & DELETE safety usage check"] = "Failed"
# Strip a trailing "-- ..." line comment while ignoring any "--" that appears
# inside a single- or double-quoted string literal (e.g. descriptions).
def strip_inline_comment(text: str) -> str:
in_single_quote = False
in_double_quote = False
index = 0
while index < len(text):
char = text[index]
# Skip backslash-escaped characters inside string literals (e.g. \')
if char == '\\' and (in_single_quote or in_double_quote):
index += 2
continue
if char == "'" and not in_double_quote:
in_single_quote = not in_single_quote
elif char == '"' and not in_single_quote:
in_double_quote = not in_double_quote
elif (char == '-' and index + 1 < len(text) and text[index + 1] == '-'
and not in_single_quote and not in_double_quote):
return text[:index].strip()
index += 1
return text.strip()
# Count how many parentheses are still open on a line, ignoring any that appear
# inside string literals. A positive result means a value tuple continues on the
# following line(s).
def open_paren_balance(text: str) -> int:
without_strings = re.sub(r"'(?:\\.|[^'])*'", "", text)
without_strings = re.sub(r'"(?:\\.|[^"])*"', "", without_strings)
return without_strings.count('(') - without_strings.count(')')
def semicolon_check(file: io, file_path: str) -> None:
global error_handler, results
@@ -255,8 +285,8 @@ def semicolon_check(file: io, file_path: str) -> None:
if not stripped_line and not inside_values_block:
continue
# Remove inline comments after SQL
stripped_line = stripped_line.split('--', 1)[0].strip()
# Remove inline comments after SQL (ignoring "--" inside string literals)
stripped_line = strip_inline_comment(stripped_line)
if stripped_line.upper().startswith("SET") and not stripped_line.endswith(";"):
print(f"❌ Missing semicolon in {file_path} at line {line_number}")
@@ -266,10 +296,29 @@ def semicolon_check(file: io, file_path: str) -> None:
if not query_open and any(keyword in stripped_line.upper() for keyword in ["SELECT", "INSERT", "UPDATE", "DELETE", "REPLACE"]):
query_open = True
# Detect start of multi-line VALUES block
if any(kw in stripped_line.upper() for kw in ["INSERT", "REPLACE"]) and "VALUES" in stripped_line.upper():
inside_values_block = True
# Detect start of a VALUES block
upper_line = stripped_line.upper()
if any(kw in upper_line for kw in ["INSERT", "REPLACE"]) and "VALUES" in upper_line:
query_open = True # Ensure query is marked open too
# Look at whatever follows the VALUES keyword on this same line
tail = stripped_line[upper_line.rfind("VALUES") + len("VALUES"):].strip()
if not tail or tail.endswith(','):
# Multi-line VALUES block: value rows follow on subsequent lines
inside_values_block = True
elif open_paren_balance(stripped_line) > 0:
# A value tuple is still open (row split across lines, or the line
# ends with '('); leave the statement open so the terminator is
# validated once the tuple closes on a following line.
pass
elif tail.endswith(';'):
# Complete single-line insert
query_open = False
else:
# Inline insert whose value tuple(s) are complete on this same line
# but the statement is not terminated with a semicolon
print(f"❌ Missing semicolon in {file_path} at line {line_number}")
check_failed = True
query_open = False
if inside_values_block:
if not stripped_line:
@@ -0,0 +1,6 @@
-- DB update 2026_07_03_03 -> 2026_07_04_00
-- Honor Among Thieves triggered combo point (51699) is beneficial for the rogue:
-- mark it positive so it never pulls the rogue into combat with the targeted enemy
-- (SPELL_ATTR0_CU_POSITIVE_EFF0 | SPELL_ATTR0_CU_POSITIVE_EFF1)
DELETE FROM `spell_custom_attr` WHERE `spell_id` = 51699;
INSERT INTO `spell_custom_attr` (`spell_id`, `attributes`) VALUES (51699, 0x6000000);
@@ -0,0 +1,5 @@
-- DB update 2026_07_04_00 -> 2026_07_04_01
--
DELETE FROM `acore_string` WHERE `entry` = 35455;
INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES
(35455, 'A Wintergrasp battle is in progress. The scheduled server maintenance has been postponed.', '겨울손아귀 전투가 진행 중입니다. 예정된 서버 점검이 연기되었습니다.', 'Une bataille du Joug-d''hiver est en cours. La maintenance programmée du serveur a été reportée.', 'Eine Schlacht um Tausendwinter ist im Gange. Die geplante Serverwartung wurde verschoben.', '冬拥湖战斗正在进行中。计划中的服务器维护已被推迟。', '冬擁湖戰鬥正在進行中。排定的伺服器維護已延後。', 'Hay una batalla de Templo Helado en curso. El mantenimiento programado del servidor se ha pospuesto.', 'Hay una batalla de Templo Helado en curso. El mantenimiento programado del servidor se ha pospuesto.', 'Идёт битва за Ледяную Грудь. Плановое обслуживание сервера отложено.');
@@ -0,0 +1,66 @@
-- DB update 2026_07_04_01 -> 2026_07_04_02
-- Register Rain of Darkness Spell Script.
DELETE FROM `spell_script_names` WHERE `spell_id` = 51761;
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(51761, 'spell_q12641_rain_of_darkness');
-- Delete Personal SAI guid row (Pause Movement and Movement Resume).
DELETE FROM `smart_scripts` WHERE (`source_type` = 0) AND (`id` IN (9, 10)) AND (`entryorguid` IN (-128958, -128959, -128960, -128961, -128962, -128963, -128964, -128965, -128966, -128967, -128968, -128970, -128973, -128976, -128978, -128979, -128980, -128981, -128986, -128991, -128992, -128993, -128910, -128911, -128912, -128913, -128914, -128915, -128916, -128917, -128918, -128919, -128920, -128922, -128924, -128926, -128927, -128928, -128929, -128930, -128934, -128935, -128948, -128954));
-- Remove Rain of Darkness Dummies from map.
DELETE FROM `creature` WHERE `id` = 28643;
-- Set Rain of Darkness Dummy Movement Flags.
DELETE FROM `creature_template_movement` WHERE (`CreatureId` = 28643);
INSERT INTO `creature_template_movement` (`CreatureId`, `Ground`, `Swim`, `Flight`, `Rooted`, `Chase`, `Random`, `InteractionPauseTimer`) VALUES
(28643, 0, 0, 1, 1, 0, 0, 0);
-- Set Rain of Darkness Dummy SAI.
UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE `entry` = 28643;
DELETE FROM `smart_scripts` WHERE (`source_type` = 0 AND `entryorguid` = 28643);
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
(28643, 0, 0, 0, 54, 0, 100, 0, 0, 0, 0, 0, 0, 0, 11, 52149, 2, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 'Rain of Darkness Dummy - On Just Summoned - Cast \'Rain of Darkness\'');
-- Update Citizen of Havenshire Action Lists.
DELETE FROM `smart_scripts` WHERE (`source_type` = 9) AND (`entryorguid` IN (2857600, 2857700));
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
(2857600, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 11, 51604, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Cast \'Serverside - Stun Self\''),
(2857600, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Play Emote 5'),
(2857600, 9, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 4, 14561, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Play Sound 14561'),
(2857600, 9, 3, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Say Line 1'),
(2857600, 9, 4, 0, 0, 0, 100, 0, 2000, 2000, 0, 0, 0, 0, 17, 431, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Set Emote State 431'),
(2857700, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 11, 51604, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Cast \'Serverside - Stun Self\''),
(2857700, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Play Emote 5'),
(2857700, 9, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 4, 14564, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Play Sound 14564'),
(2857700, 9, 3, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Say Line 1'),
(2857700, 9, 4, 0, 0, 0, 100, 0, 2000, 2000, 0, 0, 0, 0, 17, 431, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Actionlist - Set Emote State 431');
-- Update Citizen of Havenshire SAI.
UPDATE `creature_template` SET `AIName` = 'SmartAI' WHERE (`entry` IN (28576, 28577));
DELETE FROM `smart_scripts` WHERE (`entryorguid` IN (28576, 28577)) AND (`source_type` = 0) AND (`id` IN (5, 9));
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
(28576, 0, 5, 0, 1, 0, 30, 0, 5000, 20000, 5000, 20000, 0, 0, 11, 51761, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Out of Combat - Cast \'Rain of Darkness\''),
(28576, 0, 9, 0, 4, 0, 100, 0, 0, 0, 0, 0, 0, 0, 8, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - On Aggro - Set Reactstate Aggressive'),
(28577, 0, 5, 0, 1, 0, 30, 0, 5000, 20000, 5000, 20000, 0, 0, 11, 51761, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - Out of Combat - Cast \'Rain of Darkness\''),
(28577, 0, 9, 0, 4, 0, 100, 0, 0, 0, 0, 0, 0, 0, 8, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Citizen of Havenshire - On Aggro - Set Reactstate Aggressive');
-- Remove old condition (it wasn't used in any case).
DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 22) AND (`SourceGroup` = 9) AND (`SourceEntry` = 28576) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 14) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 12678) AND (`ConditionValue2` = 0) AND (`ConditionValue3` = 0);
-- Add Conditions to trigger Citizen of Havenshire Smart Events.
DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 22) AND (`SourceGroup` = 3) AND (`SourceEntry` IN (28576, 28577)) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 47) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 12678) AND (`ConditionValue2` = 10) AND (`ConditionValue3` = 0);
INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES
(22, 3, 28576, 0, 0, 47, 0, 12678, 10, 0, 0, 0, 0, '', 'Smart Event only occurs if player has quest 12678 in progress or completed.'),
(22, 3, 28577, 0, 0, 47, 0, 12678, 10, 0, 0, 0, 0, '', 'Smart Event only occurs if player has quest 12678 in progress or completed.');
DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 22) AND (`SourceGroup` = 10) AND (`SourceEntry` IN (28576, 28577)) AND (`SourceId` = 0);
INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES
(22, 10, 28576, 0, 1, 32, 0, 16, 0, 0, 0, 0, 0, '', 'Smart Event starts it Invoker is a Player.'),
(22, 10, 28576, 0, 1, 47, 0, 12678, 65, 0, 0, 0, 0, '', 'Smart Event starts if Player doesn\'t have quest 12678 in its quest log.'),
(22, 10, 28577, 0, 1, 32, 0, 16, 0, 0, 0, 0, 0, '', 'Smart Event starts it Invoker is a Player.'),
(22, 10, 28577, 0, 1, 47, 0, 12678, 65, 0, 0, 0, 0, '', 'Smart Event starts if Player doesn\'t have quest 12678 in its quest log.'),
(22, 10, 28576, 0, 2, 32, 0, 16, 0, 0, 1, 0, 0, '', 'Smart Event starts if Invoker is NOT a Player.'),
(22, 10, 28577, 0, 2, 32, 0, 16, 0, 0, 1, 0, 0, '', 'Smart Event starts if Invoker is NOT a Player.');
+110
View File
@@ -0,0 +1,110 @@
-- DB update 2026_07_04_02 -> 2026_07_04_03
--
-- XT-002 Deconstructor: rework
-- Vehicle accessory: auto-spawn Heart of the Deconstructor when XT-002 spawns
DELETE FROM `vehicle_template_accessory` WHERE `entry` = 33293 AND `accessory_entry` = 33329;
INSERT INTO `vehicle_template_accessory` (`entry`, `accessory_entry`, `seat_id`, `minion`, `description`, `summontype`, `summontimer`) VALUES
(33293, 33329, 0, 0, 'XT-002 Deconstructor - Heart', 6, 30000);
-- Spell script names for exposed heart and energy orb
DELETE FROM `spell_script_names` WHERE `ScriptName` IN ('spell_xt002_exposed_heart', 'spell_xt002_energy_orb');
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(63849, 'spell_xt002_exposed_heart'),
(62826, 'spell_xt002_energy_orb');
-- Spell script name for Heart Overload periodic trigger (server-side spell 62791)
DELETE FROM `spell_script_names` WHERE `spell_id`=62791 AND `ScriptName`='spell_xt002_heart_overload_periodic';
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(62791, 'spell_xt002_heart_overload_periodic');
-- Update Gravity Bomb ScriptNames
UPDATE `spell_script_names` SET `ScriptName`='spell_xt002_gravity_bomb_aura' WHERE `spell_id` IN (63024, 64234) AND `ScriptName`='spell_xt002_gravity_bomb';
-- Spell script name for generic Submerge spell
DELETE FROM `spell_script_names` WHERE `ScriptName`='spell_xt002_submerged' AND `spell_id`=37751;
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(37751, 'spell_xt002_submerged');
-- Spell script name for generic Stand spell
DELETE FROM `spell_script_names` WHERE `ScriptName`='spell_xt002_stand' AND `spell_id`=37752;
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(37752, 'spell_xt002_stand');
-- Disable spell proc on Exposed Heart effect_1 (handled by script)
DELETE FROM `spell_proc` WHERE `SpellId`=63849;
INSERT INTO `spell_proc` (`SpellId`, `SchoolMask`, `SpellFamilyName`, `SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`, `ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`, `AttributesMask`, `ProcsPerMinute`, `Chance`, `Cooldown`, `Charges`) VALUES
(63849, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0);
-- Replace waypoint_data for XT-002 pre-combat patrol path
DELETE FROM `waypoint_data` WHERE `id`=1360540;
INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `delay`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES
(1360540, 1, 862.5053, 7.207682, 409.8612, 0, 0, 0, 0, 100, 0),
(1360540, 2, 863.4635, 25.65489, 409.8612, 0, 0, 0, 0, 100, 0),
(1360540, 3, 872.6903, 39.44819, 409.8355, 0, 11000, 0, 0, 100, 0), -- MovementInform: emote 468
(1360540, 4, 869.8901, 32.82189, 409.8509, 0, 0, 0, 0, 100, 0),
(1360540, 5, 854.8365, 9.631022, 409.8612, 0, 0, 0, 0, 100, 0),
(1360540, 6, 855.48, -19.90913, 409.8787, 0, 0, 0, 0, 100, 0),
(1360540, 7, 852.1145, -44.87088, 409.8869, 0, 0, 0, 0, 100, 0),
(1360540, 8, 864.5853, -62.56917, 409.6369, 0, 0, 0, 0, 100, 0),
(1360540, 9, 876.4196, -77.23026, 409.9155, 0, 11000, 0, 0, 100, 0), -- MovementInform: emote 468
(1360540, 10, 878.3914, -79.4183, 409.9155, 0, 0, 0, 0, 100, 0),
(1360540, 11, 870.9197, -65.75331, 409.9155, 0, 0, 0, 0, 100, 0),
(1360540, 12, 880.2524, -33.8488, 409.9155, 0, 0, 0, 0, 100, 0),
(1360540, 13, 883.6281, -12.63596, 409.799, 3.159046, 30000, 0, 0, 100, 0); -- MovementInform: emote 10
DELETE FROM `waypoint_scripts` WHERE `guid`=5 and `id` = 1360540;
DELETE FROM `waypoint_scripts` WHERE `guid`=6 and `id` = 1360541;
-- Update script names for adds
UPDATE `creature_template` SET `ScriptName`='npc_scrapbot' WHERE `entry`=33343;
UPDATE `creature_template` SET `ScriptName`='npc_pummeller' WHERE `entry`=33344;
UPDATE `creature_template` SET `ScriptName`='npc_boombot' WHERE `entry`=33346;
UPDATE `creature_template` SET `ScriptName`='npc_life_spark' WHERE `entry`=34004;
UPDATE `creature_template` SET `ScriptName`='npc_xt_void_zone', `AIName` = '' WHERE `entry`=34001;
-- Base attack time for XT-002 (both 10 and 25) from 1800
UPDATE `creature_template` SET `BaseAttackTime`=2000 WHERE `entry` IN (33293, 33885);
-- Achievement script names
UPDATE `achievement_criteria_data` SET `ScriptName`='achievement_nerf_engineering' WHERE `ScriptName`='achievement_xt002_nerf_engineering';
UPDATE `achievement_criteria_data` SET `ScriptName`='achievement_nerf_gravity_bombs' WHERE `ScriptName`='achievement_xt002_nerf_gravity_bombs';
-- Achievement heartbreaker: type=11 script entries for hard mode (criteria 10072=25M, 10073=10M)
DELETE FROM `achievement_criteria_data` WHERE `criteria_id` IN (10072,10073) AND `type`=11;
INSERT INTO `achievement_criteria_data` (`criteria_id`, `type`, `value1`, `value2`, `ScriptName`) VALUES
(10072, 11, 0, 0, 'achievement_heartbreaker'),
(10073, 11, 0, 0, 'achievement_heartbreaker');
-- Mechanical immunities for Heart of the Deconstructor and XT-002 (both 10 and 25)
SET @ID := -427;
DELETE FROM `creature_immunities` WHERE `ID` = @ID;
INSERT INTO `creature_immunities` (`ID`, `SchoolMask`, `DispelTypeMask`, `MechanicsMask`, `Effects`, `Auras`, `ImmuneAoE`, `ImmuneChain`, `Comment`) VALUES
(@ID, 0, 0, 617299839, '98,124,144,145', 0, 0, 0, 'mech=0x26CB3F7F(CHARM|DISORIENTED|DISARM|DISTRACT|FEAR|GRIP|ROOT|SILENCE|SLEEP|SNARE|STUN|FREEZE|KNOCKOUT|POLYMORPH|BANISH|SHACKLE|TURN|HORROR|DAZE|SAPPED), flags=IMMUNITY_KNOCKBACK, effects=98(KNOCK_BACK),124(PULL_TOWARDS),144(KNOCK_BACK_DEST),145(PULL_TOWARDS_DEST)');
UPDATE `creature_template` SET `CreatureImmunitiesId` = @ID WHERE (`entry` IN (33329, 33995, 33293, 33885));
-- Cannot turn
UPDATE `creature_template` SET `unit_flags2` = `unit_flags2` | 32768 WHERE (`entry` IN (33329, 33995));
-- only hits XT-002
DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 1) AND (`SourceEntry` = 64799) AND (`SourceId` = 0) AND (`ElseGroup` IN (0, 1)) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` IN (33293, 33885)) AND (`ConditionValue3` = 0);
INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES
(13, 1, 64799, 0, 0, 31, 0, 3, 33293, 0, 0, 0, 0, '', 'target must be \'XT-002 Deconstructor\''),
(13, 1, 64799, 0, 1, 31, 0, 3, 33885, 0, 0, 0, 0, '', 'target must be \'XT-002 Deconstructor (1)\'');
UPDATE `spell_proc` SET `AttributesMask`=0, `DisableEffectsMask`=2 WHERE `SpellId`=63849;
-- Toy Piles: spawn at 4 corners of the room (positions from TC, map 603 Ulduar)
SET @CGUID := 12777;
SET @BUILD := 0;
DELETE FROM `creature` WHERE (`id` = 33337) AND (`guid` BETWEEN @CGUID+0 AND @CGUID+3);
INSERT INTO `creature` (`guid`, `id`, `map`, `zoneId`, `areaId`, `spawnMask`, `phaseMask`, `equipment_id`, `position_x`, `position_y`, `position_z`, `orientation`, `spawntimesecs`, `wander_distance`, `currentwaypoint`, `curhealth`, `curmana`, `MovementType`, `npcflag`, `unit_flags`, `dynamicflags`, `ScriptName`, `Comment`, `VerifiedBuild`) VALUES
(@CGUID+0, 33337, 603, 0, 0, 3, 1, 0, 897.908, 67.0764, 412.129, 3.92699, 180, 0, 0, 12600, 0, 0, 0, 0, 0, '', '', @BUILD),
(@CGUID+1, 33337, 603, 0, 0, 3, 1, 0, 898.099, -88.9115, 409.887, 2.23402, 180, 0, 0, 12600, 0, 0, 0, 0, 0, '', '', @BUILD),
(@CGUID+2, 33337, 603, 0, 0, 3, 1, 0, 793.096, -95.158, 409.887, 0.855211, 180, 0, 0, 12600, 0, 0, 0, 0, 0, '', '', @BUILD),
(@CGUID+3, 33337, 603, 0, 0, 3, 1, 0, 792.646, 65.3854, 414.147, 5.20108, 180, 0, 0, 12600, 0, 0, 0, 0, 0, '', '', @BUILD);
DELETE FROM `spell_script_names` WHERE `spell_id`=62826 AND `ScriptName`='spell_xt002_energy_orb';
UPDATE `creature_template` SET `AIName` = '', `ScriptName` = 'npc_xt_toy_pile' WHERE (`entry` = 33337);
@@ -0,0 +1,30 @@
-- DB update 2026_07_04_03 -> 2026_07_04_04
--
-- Flame Leviathan: Remove spell_linked_spell entries for SPELL_SYSTEMS_SHUTDOWN (62475).
-- These effects are now handled explicitly via script
DELETE FROM `spell_linked_spell` WHERE `spell_trigger` = -62475;
-- 62399 Overload Circuit: Entry 33139 (cannon), Entry 33113 (boss)
UPDATE `conditions` SET `SourceGroup` = 1, `Comment` = 'target must be Flame Leviathan' WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 3) AND (`SourceEntry` = 62399) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 33113) AND (`ConditionValue3` = 0);
-- change turret (33142) summontype from CORPSE_TIMED_DESPAWN (6) to CORPSE_DESPAWN (5) so InstallAccessory skips SetDeathState
UPDATE `vehicle_template_accessory` SET `summontype` = 5 WHERE `entry` = 33114 AND `accessory_entry` = 33142;
DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 2) AND (`SourceEntry` = 62399) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 33139) AND (`ConditionValue3` = 0);
INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES
(13, 2, 62399, 0, 0, 31, 0, 3, 33139, 0, 0, 0, 0, '', 'target must be Flame Leviathan');
-- ID - 62323 Hookshot
DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 1) AND (`SourceEntry` = 62323) AND (`SourceId` = 0) AND (`ElseGroup` = 0) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 33114) AND (`ConditionValue3` = 0);
INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES
(13, 1, 62323, 0, 0, 31, 0, 3, 33114, 0, 0, 0, 0, '', 'must be Flame Leviathan Seat');
-- Add missing text
DELETE FROM `creature_text` WHERE (`CreatureID` = 33113) AND (`GroupID` IN (20));
INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES
(33113, 20, 0, 'The Flame Leviathan begins to overload!', 41, 0, 100, 0, 0, 0, 33276, 0, 'Flame Leviathan EMOTE_OVERLOAD_START');
DELETE FROM `spell_script_names` WHERE `spell_id` = 62336;
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(62336, 'spell_hookshot_aura');
DELETE FROM `spell_script_names` WHERE `ScriptName` = 'spell_vehicle_circuit_overload_aura';
@@ -0,0 +1,3 @@
-- DB update 2026_07_04_04 -> 2026_07_04_05
-- Update TotemCategories for Savage and Hateful totems
UPDATE `item_template` SET `TotemCategory` = 21 WHERE (`entry` IN (42593, 42594, 42595, 42596, 42601, 42606));
@@ -0,0 +1,15 @@
-- DB update 2026_07_04_05 -> 2026_07_04_06
--
-- Issue #18038: add .npc showloot command entry and matching acore_string rows.
DELETE FROM `command` WHERE `name` = 'npc showloot';
INSERT INTO `command` (`name`, `security`, `help`) VALUES
('npc showloot', 2, 'Syntax: .npc showloot\nShows the loot generated on the selected creature''s corpse.');
DELETE FROM `acore_string` WHERE `entry` IN (35456, 35457, 35458, 35459, 35460, 35461);
INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES
(35456, '{} is not dead, or its corpse contains no loot.', '{}이(가) 죽지 않았거나 시체에 전리품이 없습니다.', '{} n''est pas mort, ou son cadavre ne contient aucun butin.', '{} ist nicht tot, oder sein Leichnam enthält keine Beute.', '{} 未死亡,或其尸体上没有战利品。', '{} 未死亡,或其屍體上沒有戰利品。', '{} no está muerto, o su cadáver no contiene botín.', '{} no está muerto, o su cadáver no contiene botín.', '{} не убит или его труп не содержит добычи.'),
(35457, 'Loot for {} (Entry: {}):', '{} (ID: {})의 전리품:', 'Butin de {} (ID : {}) :', 'Beute von {} (ID: {}):', '{} (ID: {}) 的战利品:', '{} (ID: {}) 的戰利品:', 'Botín de {} (ID: {}):', 'Botín de {} (ID: {}):', 'Добыча с {} (ID: {}):'),
(35458, 'Money: {}g {}s {}c', '돈: {}금 {}은 {}동', 'Argent : {}po {}pa {}pc', 'Geld: {}G {}S {}K', '金币: {}金 {}银 {}铜', '金幣:{}金 {}銀 {}銅', 'Dinero: {}o {}p {}c', 'Dinero: {}o {}p {}c', 'Деньги: {}зол {}сер {}мед'),
(35459, 'Items ({}):', '아이템 ({}):', 'Objets ({}) :', 'Gegenstände ({}):', '物品 ({})', '物品 ({})', 'Objetos ({}):', 'Objetos ({}):', 'Предметы ({}):'),
(35460, ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (Entry: {})', ' {}개 |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID : {})', ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}个 |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}個 |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}x |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})', ' {}шт. |c{:08x}|Hitem:{}:0:0:0:0:0:0:0:0|h[{}]|h|r (ID: {})'),
(35461, 'Quest items ({}):', '퀘스트 아이템 ({}):', 'Objets de quête ({}) :', 'Questgegenstände ({}):', '任务物品 ({})', '任務物品 ({})', 'Objetos de misión ({}):', 'Objetos de misión ({}):', 'Предметы заданий ({}):');
@@ -0,0 +1,12 @@
-- DB update 2026_07_04_06 -> 2026_07_05_00
--
-- General Vezax (33271) must resist spell-haste debuffs (Curse of Tongues, Mind-numbing Poison,
-- Slow, core hound Lava Breath). They inflate his Shadow Crash / Searing Flames cast times and
-- trivialize the encounter. He already uses shared CC set -287; a single creature can reference only
-- one immunity set, so give him a dedicated superset that keeps -287's immunities and adds
-- aura 216 (HASTE_SPELLS). Other -287 users are left untouched.
DELETE FROM `creature_immunities` WHERE `ID`=-427;
INSERT INTO `creature_immunities` (`ID`, `SchoolMask`, `DispelTypeMask`, `MechanicsMask`, `Effects`, `Auras`, `ImmuneAoE`, `ImmuneChain`, `Comment`) VALUES
(-427, 0, 0, 1234599678, '98,114,124,144,145', '11,216', 0, 0, 'General Vezax: -287 (CC/knockback/taunt, auras=11(MOD_TAUNT)) + auras=216(HASTE_SPELLS) so cast-time slows do not trivialize the fight');
UPDATE `creature_template` SET `CreatureImmunitiesId`=-427 WHERE `entry`=33271;
@@ -0,0 +1,4 @@
-- DB update 2026_07_05_00 -> 2026_07_05_01
-- Set Correct GroupAI (it was 5).
UPDATE `creature_formations` SET `groupAI` = 3 WHERE `leaderGUID` = 136057;
@@ -0,0 +1,4 @@
-- DB update 2026_07_05_01 -> 2026_07_05_02
-- Set Cast Flag for Net Spell.
UPDATE `smart_scripts` SET `action_param2` = 12 WHERE (`entryorguid` = 6230) AND (`source_type` = 0) AND (`id` IN (0));
@@ -0,0 +1,11 @@
-- DB update 2026_07_05_02 -> 2026_07_06_00
-- Move Marksman Bova to the correct position and model.
UPDATE `creature` SET `position_x` = -1721.4673, `position_y` = 5637.9883, `position_z` = 128.10652, `orientation` = 2.321287870407104492 WHERE `guid` = 85406 AND `id` = 25195;
DELETE FROM `creature_template_model` WHERE `CreatureID` = 25195;
INSERT INTO `creature_template_model` (`CreatureID`, `Idx`, `CreatureDisplayID`, `DisplayScale`, `Probability`, `VerifiedBuild`) VALUES
(25195, 0, 18743, 1, 1, 51831),
(25195, 1, 18742, 1, 0, 51831),
(25195, 2, 18741, 1, 0, 51831),
(25195, 3, 18740, 1, 0, 51831);
+102
View File
@@ -0,0 +1,102 @@
-- DB update 2026_07_06_00 -> 2026_07_11_00
-- ---------------------------------------------------------------------------
-- Warden of the Chamber (30058)
-- ---------------------------------------------------------------------------
-- populate warden transform spells
-- Effect_1 = 6 (SPELL_EFFECT_APPLY_AURA), EffectAura_1 = 56
-- 55831 Red - 30072 (display 26747)
-- 55830 Bronze - 30059 (display 26741)
-- 55828 Blue - 30076 (display 26749)
-- 55829 Green - 30073 (display 26748)
-- 55827 Black - 30077 (display 14308)
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30072 WHERE `ID` = 55831;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30059 WHERE `ID` = 55830;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30076 WHERE `ID` = 55828;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30073 WHERE `ID` = 55829;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 30077 WHERE `ID` = 55827;
-- creature_addon apply each warden transform as a passive spawn aura
-- per-GUID flight assignment
-- Blue 105487, 105495 - 55828
-- Bronze 105488, 105489 - 55830
-- Green 131055, 131058 - 55829
-- Red 131056, 131059 - 55831 + 29266 (Permanent Feign Death, 3.3.5a accurate)
-- Black 131063, 131064 - 55827
DELETE FROM `creature_addon` WHERE `guid` IN (105487, 105488, 105489, 105495, 131055, 131056, 131058, 131059, 131063, 131064);
INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES
(105487, 0, 0, 0, 1, 0, 0, '55828'),
(105488, 0, 0, 0, 1, 0, 0, '55830'),
(105489, 0, 0, 0, 1, 0, 0, '55830'),
(105495, 0, 0, 0, 1, 0, 0, '55828'),
(131055, 0, 0, 0, 1, 0, 0, '55829'),
(131056, 0, 0, 0, 1, 0, 0, '55831 29266'),
(131058, 0, 0, 0, 1, 0, 0, '55829'),
(131059, 0, 0, 0, 1, 0, 0, '55831 29266'),
(131063, 0, 0, 0, 1, 0, 0, '55827'),
(131064, 0, 0, 0, 1, 0, 0, '55827');
-- Red fire wall that blocks Ruby Sanctum (Invisible Stalker (23155) GUID 131066) stop spawn (intentional)
UPDATE `creature` SET `spawnMask` = 0 WHERE `guid` = 131066 AND `id` = 23155;
-- Correct blue warden 105487 orientation
UPDATE `creature` SET `orientation` = 2.990109920501708984 WHERE `guid` = 105487 AND `id` = 30058;
-- ---------------------------------------------------------------------------
-- Wyrmrest Protector (27953)
-- ---------------------------------------------------------------------------
-- populate protector transform spells
-- 50158 Red - 27952 (display 14357)
-- 51118 Blue - 28251 (display 14356)
-- 50160 Bronze - 27955 (display 14358)
-- 50159 Green - 27954 (display 14359)
-- 51117 Black - 28250 (display 14355)
-- 51119 Nether - 28252 (display 25257)
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 27952 WHERE `ID` = 50158;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 28251 WHERE `ID` = 51118;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 27955 WHERE `ID` = 50160;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 27954 WHERE `ID` = 50159;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 28250 WHERE `ID` = 51117;
UPDATE `spell_dbc` SET `Effect_1` = 6, `EffectAura_1` = 56, `ImplicitTargetA_1` = 1, `EffectMiscValue_1` = 28252 WHERE `ID` = 51119;
-- ---------------------------------------------------------------------------
-- SAI on respawn, roll 1 of 6 spells and apply spell transform
-- matching polearm is equipped depending what spell is selected
-- 2795301 Red - aura 50158, polearm 38488
-- 2795302 Blue - aura 51118, polearm 32729
-- 2795303 Bronze - aura 50160, polearm 38491
-- 2795304 Green - aura 50159, polearm 38209
-- 2795305 Black - aura 51117, polearm 38487
-- 2795306 Nether - aura 51119, polearm 38490
DELETE FROM `smart_scripts` WHERE `entryorguid` = 27953 AND `source_type` = 0 AND `id` = 4;
DELETE FROM `smart_scripts` WHERE `entryorguid` IN (2795301, 2795302, 2795303, 2795304, 2795305, 2795306) AND `source_type` = 9;
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
(27953, 0, 4, 0, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 87, 2795301, 2795302, 2795303, 2795304, 2795305, 2795306, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector - On Respawn - Random flight transform + polearm'),
(2795301, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 50158, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Red) - Transform aura'),
(2795301, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38488, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Red) - Equip polearm'),
(2795302, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 51118, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Blue) - Transform aura'),
(2795302, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 32729, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Blue) - Equip polearm'),
(2795303, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 50160, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Bronze) - Transform aura'),
(2795303, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38491, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Bronze) - Equip polearm'),
(2795304, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 50159, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Green) - Transform aura'),
(2795304, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38209, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Green) - Equip polearm'),
(2795305, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 51117, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Black) - Transform aura'),
(2795305, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38487, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Black) - Equip polearm'),
(2795306, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 75, 51119, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Nether) - Transform aura'),
(2795306, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 71, 0, 0, 38490, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Wyrmrest Protector (Nether) - Equip polearm');
-- 10 GUIDs are given emote state 173
DELETE FROM `creature_addon` WHERE `guid` IN (131012, 131014, 131015, 131016, 131020, 131021, 131022, 131023, 131026, 131027);
INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES
(131012, 0, 0, 0, 1, 173, 0, NULL),
(131014, 0, 0, 0, 1, 173, 0, NULL),
(131015, 0, 0, 0, 1, 173, 0, NULL),
(131016, 0, 0, 0, 1, 173, 0, NULL),
(131020, 0, 0, 0, 1, 173, 0, NULL),
(131021, 0, 0, 0, 1, 173, 0, NULL),
(131022, 0, 0, 0, 1, 173, 0, NULL),
(131023, 0, 0, 0, 1, 173, 0, NULL),
(131026, 0, 0, 0, 1, 173, 0, NULL),
(131027, 0, 0, 0, 1, 173, 0, NULL);
-- Reposition Protector spawn 131030 to its sniffed location
UPDATE `creature` SET `position_x` = 3452.6472, `position_y` = 250.009, `position_z` = 52.378803, `orientation` = 3.298672199249267578 WHERE `guid` = 131030 AND `id` = 27953;
@@ -3177,13 +3177,6 @@ Creature.MovingStopTimeForPlayer = 180000
WaypointMovementStopTimeForPlayer = 120
# NpcEvadeIfTargetIsUnreachable
# Description: Specifies the time (in seconds) that a creature whom target
# is unreachable to end up in evade mode.
# Default: 5
NpcEvadeIfTargetIsUnreachable = 5
# NpcRegenHPIfTargetIsUnreachable
# Description: Regenerates HP for Creatures in Raids if they cannot reach the target.
# Keep disabled if you are experiencing mmaps/pathing issues.
@@ -3193,12 +3186,15 @@ NpcEvadeIfTargetIsUnreachable = 5
NpcRegenHPIfTargetIsUnreachable = 1
# NpcRegenHPTimeIfTargetIsUnreachable
# Description: Specifies the time (in seconds) that a creature whom target
# is unreachable in raid to end up regenerate health.
# Default: 10
# Creature.Instance.TeleportToUnreachableTarget
# Description: In dungeons and raids, non-boss creatures that cannot path to their
# target teleport to it instead of evading or regenerating in place
# (retail-like behaviour). Bosses and player-controlled creatures are
# not affected.
# Default: 0 - (Disabled)
# 1 - (Enabled)
NpcRegenHPTimeIfTargetIsUnreachable = 10
Creature.Instance.TeleportToUnreachableTarget = 0
# Creatures.CustomIDs
# Description: The list of custom creatures with gossip dialogues hardcoded in core,
@@ -3750,6 +3746,17 @@ Wintergrasp.KickVoAPlayers = 1
Wintergrasp.EssenceBothFactions = 0
#
# Wintergrasp.DeferShutdownTimer
# Description: Defer a scheduled server shutdown/restart if a Wintergrasp battle
# is still in progress when the countdown elapses. It is rescheduled
# to the battle's remaining time plus this many minutes of buffer.
# Idle shutdowns are not affected.
# Default: 0 - (Disabled)
# N - (Enabled, defer to remaining battle time + N minutes)
Wintergrasp.DeferShutdownTimer = 0
#
###################################################################################################
@@ -424,7 +424,7 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_SEL_CHAR_SOCIAL, "SELECT DISTINCT guid FROM character_social WHERE friend = ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHAR_OLD_CHARS, "SELECT guid, deleteInfos_Account FROM characters WHERE deleteDate IS NOT NULL AND deleteDate < ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_ARENA_TEAM_ID_BY_PLAYER_GUID, "SELECT arena_team_member.arenateamid FROM arena_team_member JOIN arena_team ON arena_team_member.arenateamid = arena_team.arenateamid WHERE guid = ? AND type = ? LIMIT 1", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_MAIL, "SELECT id, messageType, sender, receiver, subject, body, expire_time, deliver_time, money, cod, checked, stationery, mailTemplateId FROM mail WHERE receiver = ? AND deliver_time <= ? ORDER BY id DESC", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_MAIL, "SELECT id, messageType, sender, receiver, subject, body, expire_time, deliver_time, money, cod, checked, stationery, mailTemplateId FROM mail WHERE receiver = ? ORDER BY id DESC", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_NEXT_MAIL_DELIVERYTIME, "SELECT MIN(deliver_time) FROM mail WHERE receiver = ? AND deliver_time > ? AND (checked & 1) = 0 LIMIT 1", CONNECTION_SYNCH);
PrepareStatement(CHAR_DEL_CHAR_AURA_FROZEN, "DELETE FROM character_aura WHERE spell = 9454 AND guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHAR_INVENTORY_COUNT_ITEM, "SELECT COUNT(itemEntry) FROM character_inventory ci INNER JOIN item_instance ii ON ii.guid = ci.item WHERE itemEntry = ?", CONNECTION_SYNCH);
+21
View File
@@ -24,6 +24,7 @@
#include "SpellAuraEffects.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "World.h"
void UnitAI::AttackStart(Unit* victim)
{
@@ -414,6 +415,26 @@ void UnitAI::EvadeTimerExpired()
}
}
// Retail-like: instance trash teleports to its unreachable target instead of evading
if (sWorld->getBoolConfig(CONFIG_CREATURE_INSTANCE_TELEPORT_TO_UNREACHABLE_TARGET)
&& creature->GetMap()->IsDungeon()
&& !creature->IsDungeonBoss() && !creature->isWorldBoss()
&& !creature->IsControlledByPlayer())
{
if (ObjectGuid targetGuid = creature->GetCannotReachTarget())
{
if (Unit* target = ObjectAccessor::GetUnit(*creature, targetGuid))
{
if (target->IsAlive() && creature->IsEngagedBy(target))
{
creature->NearTeleportTo(target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(), target->GetOrientation());
creature->SetCannotReachTarget();
return;
}
}
}
}
if (creature->GetMap()->IsRaid())
{
creature->GetCombatManager().ContinueEvadeRegen();
+1 -1
View File
@@ -1239,7 +1239,7 @@ void SmartAI::SetFollow(Unit* target, float dist, float angle, uint32 credit, ui
mFollowArrivedEntry = end;
mFollowArrivedAlive = !aliveState; // negate - 0 is alive
mFollowCreditType = creditType;
me->GetMotionMaster()->MoveFollow(target, mFollowDist, mFollowAngle);
me->GetMotionMaster()->MoveFollow(target, mFollowDist, mFollowAngle, MOTION_SLOT_ACTIVE, true, false);
}
void SmartAI::StopFollow(bool complete)
@@ -568,7 +568,7 @@ enum SMART_ACTION
SMART_ACTION_CALL_GROUPEVENTHAPPENS = 26, // QuestID
SMART_ACTION_COMBAT_STOP = 27, // No Params
SMART_ACTION_REMOVEAURASFROMSPELL = 28, // Spellid (0 removes all auras), charges (0 removes aura)
SMART_ACTION_FOLLOW = 29, // Distance (0 = default), Angle (0 = default), EndCreatureEntry, credit, creditType (0monsterkill, 1event)
SMART_ACTION_FOLLOW = 29, // Distance (0 = default), Angle (0 = default), EndCreatureEntry, credit, creditType (0monsterkill, 1event), aliveState (0 = creature must be alive, 1 = dead creature can trigger arrival)
SMART_ACTION_RANDOM_PHASE = 30, // PhaseId1, PhaseId2, PhaseId3...
SMART_ACTION_RANDOM_PHASE_RANGE = 31, // PhaseMin, PhaseMax
SMART_ACTION_RESET_GOBJECT = 32, //
@@ -647,7 +647,7 @@ void BattlegroundMgr::BuildBattlegroundListPacket(WorldPacket* data, ObjectGuid
}
}
void BattlegroundMgr::SendToBattleground(Player* player, uint32 instanceId, BattlegroundTypeId bgTypeId)
bool BattlegroundMgr::SendToBattleground(Player* player, uint32 instanceId, BattlegroundTypeId bgTypeId)
{
if (Battleground* bg = GetBattleground(instanceId, bgTypeId))
{
@@ -655,12 +655,11 @@ void BattlegroundMgr::SendToBattleground(Player* player, uint32 instanceId, Batt
Position const* pos = bg->GetTeamStartPosition(player->GetBgTeamId());
LOG_DEBUG("bg.battleground", "BattlegroundMgr::SendToBattleground: Sending {} to map {}, {} (bgType {})", player->GetName(), mapid, pos->ToString(), bgTypeId);
player->TeleportTo(mapid, pos->GetPositionX(), pos->GetPositionY(), pos->GetPositionZ(), pos->GetOrientation());
}
else
{
LOG_ERROR("bg.battleground", "BattlegroundMgr::SendToBattleground: Instance {} (bgType {}) not found while trying to teleport player {}", instanceId, bgTypeId, player->GetName());
return player->TeleportTo(mapid, pos->GetPositionX(), pos->GetPositionY(), pos->GetPositionZ(), pos->GetOrientation());
}
LOG_ERROR("bg.battleground", "BattlegroundMgr::SendToBattleground: Instance {} (bgType {}) not found while trying to teleport player {}", instanceId, bgTypeId, player->GetName());
return false;
}
void BattlegroundMgr::SendAreaSpiritHealerQueryOpcode(Player* player, Battleground* bg, ObjectGuid guid)
@@ -93,7 +93,10 @@ public:
void LoadBattlegroundTemplates();
void DeleteAllBattlegrounds();
void SendToBattleground(Player* player, uint32 InstanceID, BattlegroundTypeId bgTypeId);
// Returns false when the teleport could not start (instance gone, or a
// synchronous TeleportTo failure) so the accept path can release the
// otherwise-orphaned invited reservation.
bool SendToBattleground(Player* player, uint32 InstanceID, BattlegroundTypeId bgTypeId);
/* Battleground queues */
BattlegroundQueue& GetBattlegroundQueue(BattlegroundQueueTypeId bgQueueTypeId) { return m_BattlegroundQueues[bgQueueTypeId]; }
@@ -1302,13 +1302,16 @@ int32 BattlegroundQueue::GetQueueAnnouncementTimer(uint32 bracketId) const
void BattlegroundQueue::InviteGroupToBG(GroupQueueInfo* ginfo, Battleground* bg, TeamId teamId)
{
// An already-invited group keeps the side it was invited under: writing
// teamId here would split a future re-invite's IncreaseInvitedCount from the
// original side's DecreaseInvitedCount at leave, desyncing the ledger.
if (ginfo->IsInvitedToBGInstanceGUID)
return;
// set side if needed
if (teamId != TEAM_NEUTRAL)
ginfo->teamId = teamId;
if (ginfo->IsInvitedToBGInstanceGUID)
return;
// set invitation
ginfo->IsInvitedToBGInstanceGUID = bg->GetInstanceID();
+45 -58
View File
@@ -826,7 +826,6 @@ namespace lfg
return;
}
std::string debugNames = "";
if (grp) // Begin rolecheck
{
// Create new rolecheck
@@ -857,9 +856,6 @@ namespace lfg
if (!isContinue)
SetSelectedDungeons(pguid, dungeons);
roleCheck.roles[pguid] = 0;
if (!debugNames.empty())
debugNames.append(", ");
debugNames.append(plrg->GetName());
}
}
// Update leader role
@@ -886,16 +882,7 @@ namespace lfg
player->GetSession()->SendLfgUpdatePlayer(LfgUpdateData(LFG_UPDATETYPE_JOIN_QUEUE, dungeons, comment));
SetState(guid, LFG_STATE_QUEUED);
SetRoles(guid, roles);
debugNames.append(player->GetName());
}
/*if (sLog->ShouldLog(LOG_FILTER_LFG, LOG_LEVEL_DEBUG))
{
std::ostringstream o;
o << "LFGMgr::Join: [" << guid << "] joined (" << (grp ? "group" : "player") << ") Members: " << debugNames.c_str()
<< ". Dungeons (" << uint32(dungeons.size()) << "): " << ConcatenateDungeons(dungeons);
LOG_DEBUG("lfg", "{}", o.str());
}*/
}
void LFGMgr::ToggleTesting()
@@ -1099,10 +1086,7 @@ namespace lfg
int32 spellDamage, spellHeal;
uint32 dungeonId, encounterMask, maxPower;
uint32 deletedCounter, groupCounter, playerCounter;
ByteBuffer buffer_deleted, buffer_groups, buffer_players;
std::string emptyComment;
GuidSet deletedGroups, deletedGroupsToErase;
RBInternalInfoMap copy;
for (uint8 team = 0; team < 2; ++team)
{
@@ -1207,18 +1191,18 @@ namespace lfg
}
}
copy.clear();
copy = currInternalInfoMap; // will be saved as prev at the end
_rbCopy.clear();
_rbCopy = currInternalInfoMap; // will be saved as prev at the end
// compare prev with curr to build difference packet
deletedCounter = 0;
groupCounter = 0;
playerCounter = 0;
buffer_deleted.clear();
buffer_groups.clear();
buffer_players.clear();
deletedGroups.clear();
deletedGroupsToErase.clear();
_rbBufferDeleted.clear();
_rbBufferGroups.clear();
_rbBufferPlayers.clear();
_rbDeletedGroups.clear();
_rbDeletedGroupsToErase.clear();
RBInternalInfoMap& prevInternalInfoMap = RBInternalInfoStorePrev[team][dungeonId];
RBInternalInfoMap::iterator iter, iterTmp;
@@ -1228,50 +1212,51 @@ namespace lfg
if (iter == currInternalInfoMap.end()) // was -> isn't
{
if (sitr->second.isGroupLeader)
deletedGroups.insert(sitr->second.groupGuid);
_rbDeletedGroups.insert(sitr->second.groupGuid);
++deletedCounter;
buffer_deleted << sitr->second.guid;
_rbBufferDeleted << sitr->second.guid;
}
else // was -> is
{
if (sitr->second.isGroupLeader) // was a leader
{
if (!iter->second.isGroupLeader) // leader -> no longer a leader
deletedGroups.insert(sitr->second.groupGuid);
_rbDeletedGroups.insert(sitr->second.groupGuid);
else if (sitr->second.groupGuid != iter->second.groupGuid) // leader -> leader of another group
{
deletedGroups.insert(sitr->second.groupGuid);
deletedGroupsToErase.insert(iter->second.groupGuid);
_rbDeletedGroups.insert(sitr->second.groupGuid);
_rbDeletedGroupsToErase.insert(iter->second.groupGuid);
++groupCounter;
RBPacketAppendGroup(iter->second, buffer_groups);
RBPacketAppendGroup(iter->second, _rbBufferGroups);
}
else if (sitr->second.comment != iter->second.comment || sitr->second.encounterMask != iter->second.encounterMask || sitr->second.instanceGuid != iter->second.instanceGuid) // leader -> nothing changed
{
++groupCounter;
RBPacketAppendGroup(iter->second, buffer_groups);
RBPacketAppendGroup(iter->second, _rbBufferGroups);
}
}
else if (iter->second.isGroupLeader) // wasn't a leader -> is a leader
{
deletedGroupsToErase.insert(iter->second.groupGuid);
_rbDeletedGroupsToErase.insert(iter->second.groupGuid);
++groupCounter;
RBPacketAppendGroup(iter->second, buffer_groups);
RBPacketAppendGroup(iter->second, _rbBufferGroups);
}
if (!iter->second._online) // if offline, copy previous stats (itemLevel, talents, area, etc.)
{
iterTmp = copy.find(sitr->first); // copied container is for building a full packet, so modify it there (currInternalInfoMap is erased)
iterTmp = _rbCopy.find(sitr->first); // copied container is for building a full packet, so modify it there (currInternalInfoMap is erased)
iterTmp->second.CopyStats(sitr->second);
if (!sitr->second.PlayerSameAs(iterTmp->second)) // player info changed
{
++playerCounter;
RBPacketAppendPlayer(iterTmp->second, buffer_players);
RBPacketAppendPlayer(iterTmp->second, _rbBufferPlayers);
}
}
else if (!sitr->second.PlayerSameAs(iter->second)) // player info changed
{
++playerCounter;
RBPacketAppendPlayer(iter->second, buffer_players);
RBPacketAppendPlayer(iter->second, _rbBufferPlayers);
}
currInternalInfoMap.erase(iter);
}
@@ -1281,38 +1266,38 @@ namespace lfg
{
if (sitr->second.isGroupLeader)
{
deletedGroupsToErase.insert(sitr->second.groupGuid);
_rbDeletedGroupsToErase.insert(sitr->second.groupGuid);
++groupCounter;
RBPacketAppendGroup(sitr->second, buffer_groups);
RBPacketAppendGroup(sitr->second, _rbBufferGroups);
}
++playerCounter;
RBPacketAppendPlayer(sitr->second, buffer_players);
RBPacketAppendPlayer(sitr->second, _rbBufferPlayers);
}
if (!deletedGroupsToErase.empty())
if (!_rbDeletedGroupsToErase.empty())
{
for (ObjectGuid const& toErase : deletedGroupsToErase)
for (ObjectGuid const& toErase : _rbDeletedGroupsToErase)
{
deletedGroups.erase(toErase);
_rbDeletedGroups.erase(toErase);
}
}
if (!deletedGroups.empty())
if (!_rbDeletedGroups.empty())
{
for (ObjectGuid const& deletedGroup : deletedGroups)
for (ObjectGuid const& deletedGroup : _rbDeletedGroups)
{
++deletedCounter;
buffer_deleted << deletedGroup;
_rbBufferDeleted << deletedGroup;
}
}
WorldPacket differencePacket(SMSG_UPDATE_LFG_LIST, 1000);
RBPacketBuildDifference(differencePacket, dungeonId, deletedCounter, buffer_deleted, groupCounter, buffer_groups, playerCounter, buffer_players);
RBPacketBuildDifference(differencePacket, dungeonId, deletedCounter, _rbBufferDeleted, groupCounter, _rbBufferGroups, playerCounter, _rbBufferPlayers);
WorldPacket fullPacket(SMSG_UPDATE_LFG_LIST, 1000);
RBPacketBuildFull(fullPacket, dungeonId, copy);
RBPacketBuildFull(fullPacket, dungeonId, _rbCopy);
RBCacheStore[team][dungeonId] = fullPacket;
prevInternalInfoMap = copy;
prevInternalInfoMap = _rbCopy;
currInternalInfoMap.clear();
if (entryInfoMap.empty())
@@ -1403,44 +1388,44 @@ namespace lfg
buffer << (uint32)info.encounterMask;
}
void LFGMgr::RBPacketBuildDifference(WorldPacket& differencePacket, uint32 dungeonId, uint32 deletedCounter, ByteBuffer& buffer_deleted, uint32 groupCounter, ByteBuffer& buffer_groups, uint32 playerCounter, ByteBuffer& buffer_players)
void LFGMgr::RBPacketBuildDifference(WorldPacket& differencePacket, uint32 dungeonId, uint32 deletedCounter, ByteBuffer const& bufferDeleted, uint32 groupCounter, ByteBuffer const& bufferGroups, uint32 playerCounter, ByteBuffer const& bufferPlayers)
{
differencePacket << (uint32)LFG_TYPE_RAID;
differencePacket << (uint32)dungeonId;
differencePacket << (uint8)1;
differencePacket << (uint32)deletedCounter;
differencePacket.append(buffer_deleted);
differencePacket.append(bufferDeleted);
differencePacket << (uint32)groupCounter;
differencePacket << (uint32)0;
differencePacket.append(buffer_groups);
differencePacket.append(bufferGroups);
differencePacket << (uint32)playerCounter;
differencePacket << (uint32)0;
differencePacket.append(buffer_players);
differencePacket.append(bufferPlayers);
}
void LFGMgr::RBPacketBuildFull(WorldPacket& fullPacket, uint32 dungeonId, RBInternalInfoMap& infoMap)
void LFGMgr::RBPacketBuildFull(WorldPacket& fullPacket, uint32 dungeonId, RBInternalInfoMap const& infoMap)
{
fullPacket << (uint32)LFG_TYPE_RAID;
fullPacket << (uint32)dungeonId;
fullPacket << (uint8)0;
uint32 groupCounter = 0, playerCounter = 0;
ByteBuffer buffer_groups, buffer_players;
ByteBuffer bufferGroups, bufferPlayers;
for (RBInternalInfoMap::const_iterator itr = infoMap.begin(); itr != infoMap.end(); ++itr)
{
if (itr->second.isGroupLeader)
{
++groupCounter;
RBPacketAppendGroup(itr->second, buffer_groups);
RBPacketAppendGroup(itr->second, bufferGroups);
}
++playerCounter;
RBPacketAppendPlayer(itr->second, buffer_players);
RBPacketAppendPlayer(itr->second, bufferPlayers);
}
fullPacket << (uint32)groupCounter;
fullPacket << (uint32)0;
fullPacket.append(buffer_groups);
fullPacket.append(bufferGroups);
fullPacket << (uint32)playerCounter;
fullPacket << (uint32)0;
fullPacket.append(buffer_players);
fullPacket.append(bufferPlayers);
}
// pussywizard:
@@ -2272,7 +2257,9 @@ namespace lfg
{
error = LFG_TELEPORTERROR_IN_VEHICLE;
}
else if (player->GetCharmGUID() || player->IsInCombat())
// GetCharm() validates the charmed unit still exists and clears a stale reference,
// unlike GetCharmGUID(); a despawned vehicle must not permanently block the teleport.
else if (player->GetCharm() || player->IsInCombat())
{
error = LFG_TELEPORTERROR_COMBAT;
}
+9 -2
View File
@@ -20,6 +20,7 @@
#include <utility>
#include "ByteBuffer.h"
#include "DBCStructure.h"
#include "Field.h"
#include "LFG.h"
@@ -562,8 +563,8 @@ namespace lfg
void SendRaidBrowserJoinedPacket(Player* p, LfgDungeonSet& dungeons, std::string comment);
void RBPacketAppendGroup(const RBInternalInfo& info, ByteBuffer& buffer);
void RBPacketAppendPlayer(const RBInternalInfo& info, ByteBuffer& buffer);
void RBPacketBuildDifference(WorldPacket& differencePacket, uint32 dungeonId, uint32 deletedCounter, ByteBuffer& buffer_deleted, uint32 groupCounter, ByteBuffer& buffer_groups, uint32 playerCounter, ByteBuffer& buffer_players);
void RBPacketBuildFull(WorldPacket& fullPacket, uint32 dungeonId, RBInternalInfoMap& infoMap);
void RBPacketBuildDifference(WorldPacket& differencePacket, uint32 dungeonId, uint32 deletedCounter, ByteBuffer const& bufferDeleted, uint32 groupCounter, ByteBuffer const& bufferGroups, uint32 playerCounter, ByteBuffer const& bufferPlayers);
void RBPacketBuildFull(WorldPacket& fullPacket, uint32 dungeonId, RBInternalInfoMap const& infoMap);
// LfgQueue
/// Get last lfg state (NONE, DUNGEON or FINISHED_DUNGEON)
@@ -627,6 +628,12 @@ namespace lfg
uint32 lastProposalId; ///< pussywizard, store it here because of splitting LFGMgr update into tasks
uint32 m_raidBrowserUpdateTimer[2]; ///< pussywizard
uint32 m_raidBrowserLastUpdatedDungeonId[2]; ///< pussywizard: for 2 factions
ByteBuffer _rbBufferDeleted;
ByteBuffer _rbBufferGroups;
ByteBuffer _rbBufferPlayers;
GuidSet _rbDeletedGroups;
GuidSet _rbDeletedGroupsToErase;
RBInternalInfoMap _rbCopy;
LfgQueueContainer QueuesStore; ///< Queues
LfgCachedDungeonContainer CachedDungeonMapStore; ///< Stores all dungeons by groupType
+1 -1
View File
@@ -375,7 +375,7 @@ namespace lfg
LfgDungeonSet temporal;
LfgDungeonSet& dungeons = QueueDataStore[check.guids[i]].dungeons;
std::set_intersection(proposalDungeons.begin(), proposalDungeons.end(), dungeons.begin(), dungeons.end(), std::inserter(temporal, temporal.begin()));
proposalDungeons = temporal;
std::swap(proposalDungeons, temporal);
}
if (proposalDungeons.empty())
@@ -1925,6 +1925,11 @@ bool Creature::CanStartAttack(Unit const* who, bool force) const
if (!_IsTargetAcceptable(who))
return false;
// Totems never pull proximity aggro; they are only attacked in response
// to threat they generate themselves (e.g. Searing Totem)
if (who->IsTotem())
return false;
if (IsNeutralToAll() || !IsWithinDistInMap(who, GetAggroRange(who) + m_CombatDistance, true, false, false))
return false;
}
+3 -2
View File
@@ -2337,8 +2337,9 @@ TempSummon* Map::SummonCreature(uint32 entry, Position const& pos, SummonPropert
summon->InitSummon();
// call MoveInLineOfSight for nearby creatures
Acore::AIRelocationNotifier notifier(*summon);
// call MoveInLineOfSight for nearby players and creatures; players are visited
// first (grid typelist order) so aggressive summons prefer them over pets/totems
Acore::AIRelocationNotifier notifier(*summon, true);
Cell::VisitObjects(summon, notifier, GetVisibilityRange());
return summon;
+15 -15
View File
@@ -7431,22 +7431,21 @@ bool Unit::Attack(Unit* victim, bool meleeAttack)
// set position before any AI calls/assistance
//if (IsCreature())
// ToCreature()->SetCombatStartPosition(GetPositionX(), GetPositionY(), GetPositionZ());
if (creature)
// player-controlled creatures (pets, charms) enter combat on contact instead
// (melee swing execution or spell launch/hit, see Unit::AtTargetAttacked)
if (creature && !IsControlledByPlayer())
{
EngageWithTarget(victim);
if (!IsControlledByPlayer())
{
creature->SendAIReaction(AI_REACTION_HOSTILE);
creature->SendAIReaction(AI_REACTION_HOSTILE);
/// @todo: Implement aggro range, detection range and assistance range templates
if (!(creature->HasFlagsExtra(CREATURE_FLAG_EXTRA_DONT_CALL_ASSISTANCE)))
creature->CallAssistance();
/// @todo: Implement aggro range, detection range and assistance range templates
if (!(creature->HasFlagsExtra(CREATURE_FLAG_EXTRA_DONT_CALL_ASSISTANCE)))
creature->CallAssistance();
creature->SetAssistanceTimer(sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_ASSISTANCE_PERIOD));
creature->SetAssistanceTimer(sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_ASSISTANCE_PERIOD));
SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE);
}
SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE);
}
// delay offhand weapon attack by 50% of the base attack time
@@ -10841,7 +10840,7 @@ bool Unit::_IsValidAttackTarget(Unit const* target, SpellInfo const* bySpell, Wo
if (Player const* player = target->GetCharmerOrOwnerPlayerOrPlayerItself())
isContestedPvp = player->HasPlayerFlag(PLAYER_FLAGS_CONTESTED_PVP);
if (!isContestedGuard && !isContestedPvp)
if (!isContestedGuard || !isContestedPvp)
return false;
}
@@ -15442,10 +15441,12 @@ void Unit::KnockbackFrom(float x, float y, float speedXY, float speedZ)
}
}
if (!player)
{
// While feared/confused the client has no control over the unit,
// so a SMSG_MOVE_KNOCK_BACK would be ignored or immediately overridden by the server
// side fleeing/confused splines. Perform the knockback server side instead; the
// fleeing/confused movement generator resumes once this spline is finalized
if (!player || !IsClientControlled())
GetMotionMaster()->MoveKnockbackFrom(x, y, speedXY, speedZ);
}
else
{
float vcos, vsin;
@@ -15874,7 +15875,6 @@ void Unit::_ExitVehicle(Position const* exitPosition)
init.Launch();
DisableSpline();
KnockbackFrom(pos.GetPositionX(), pos.GetPositionY(), 10.0f, 20.0f);
CastSpell(this, VEHICLE_SPELL_PARACHUTE, true);
}
// xinef: move fall, should we support all creatures that exited vehicle in air? Currently Quest Drag and Drop only, Air Assault quest
@@ -186,6 +186,19 @@ void AIRelocationNotifier::Visit(CreatureMapType& m)
}
}
void AIRelocationNotifier::Visit(PlayerMapType& m)
{
if (!includePlayers)
return;
Creature* creature = i_unit.ToCreature();
if (!creature || creature->IsMoveInLineOfSightStrictlyDisabled())
return;
for (PlayerMapType::iterator iter = m.begin(); iter != m.end(); ++iter)
CreatureUnitRelocationWorker(creature, iter->GetSource());
}
// Uses visibility map
void MessageDistDeliverer::Visit(VisiblePlayersMap const& m)
{
@@ -91,9 +91,11 @@ namespace Acore
{
Unit& i_unit;
bool isCreature;
explicit AIRelocationNotifier(Unit& unit) : i_unit(unit), isCreature(unit.IsCreature()) {}
bool includePlayers;
explicit AIRelocationNotifier(Unit& unit, bool includePlayers = false) : i_unit(unit), isCreature(unit.IsCreature()), includePlayers(includePlayers) {}
template<class T> void Visit(GridRefMgr<T>&) {}
void Visit(CreatureMapType&);
void Visit(PlayerMapType&);
};
enum class TeamFilter
@@ -412,7 +412,9 @@ void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData)
return;
}
if (_player->GetCharmGUID() || _player->IsInCombat())
// GetCharm() validates the charmed unit still exists and clears a stale reference,
// unlike GetCharmGUID(); a despawned vehicle must not permanently block BG entry.
if (_player->GetCharm() || _player->IsInCombat())
{
ChatHandler(_player->GetSession()).SendNotification(LANG_YOU_IN_COMBAT);
return;
@@ -542,7 +544,31 @@ void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData)
sLFGMgr->LeaveAllLfgQueues(_player->GetGUID(), false);
_player->SetBattlegroundId(bg->GetInstanceID(), bg->GetBgTypeID(), queueSlot, true, bgTypeId == BATTLEGROUND_RB, teamId);
sBattlegroundMgr->SendToBattleground(_player, ginfo.IsInvitedToBGInstanceGUID, bgTypeId);
if (!sBattlegroundMgr->SendToBattleground(_player, ginfo.IsInvitedToBGInstanceGUID, bgTypeId))
{
// The teleport never started (instance gone, or a synchronous
// TeleportTo veto such as a DK still locked to Ebon Hold). The accept
// already pulled the player out of the queue and he can't decline
// now, so undo the accept here -- otherwise the invited reservation
// leaks forever, permanently skewing team selection and blocking the
// empty instance's cleanup.
bg->DecreaseInvitedCount(teamId);
_player->RemoveBattlegroundQueueId(bgQueueTypeId);
_player->SetBattlegroundId(0, BATTLEGROUND_TYPE_NONE, PLAYER_MAX_BATTLEGROUND_QUEUES, false, false, TEAM_NEUTRAL);
sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_NONE, 0, 0, 0, TEAM_NEUTRAL);
SendPacket(&data);
// Free slot -> let the queue refill it. BG only, like the sibling
// leave-queue path: an arena update needs its own type/rating.
if (!ginfo.ArenaType)
sBattlegroundMgr->ScheduleQueueUpdate(0, 0, bgQueueTypeId, bgTypeId, bracketEntry->GetBracketId());
LOG_ERROR("bg.battleground", "Battleground: player {} {} failed to teleport into bg {}, bgtype {}; released the invited reservation.",
_player->GetName(), _player->GetGUID().ToString(), bg->GetInstanceID(), bg->GetBgTypeID());
return;
}
LOG_DEBUG("bg.battleground", "Battleground: player {} {} joined battle for bg {}, bgtype {}, queue type {}.", _player->GetName(), _player->GetGUID().ToString(), bg->GetInstanceID(), bg->GetBgTypeID(), bgQueueTypeId);
}
@@ -119,7 +119,6 @@ bool LoginQueryHolder::Initialize()
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAIL);
stmt->SetData(0, lowGuid);
stmt->SetData(1, uint32(GameTime::GetGameTime().count()));
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MAILS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAILITEMS);
+9 -3
View File
@@ -840,14 +840,20 @@ void WorldSession::HandleMoveKnockBackAck(WorldPacket& recvData)
movementInfo.guid = guid;
ReadMovementInfo(recvData, &movementInfo);
mover->m_movementInfo = movementInfo;
// Relocate the mover to the acknowledged position. Otherwise the server (and the
// MSG_MOVE_KNOCK_BACK broadcast below) keeps using the pre-knockback position until
// the next regular movement packet arrives, desyncing the unit for nearby clients
if (!ProcessMovementInfo(movementInfo, mover, mover->ToPlayer(), recvData))
{
recvData.rfinish(); // prevent warnings spam
return;
}
if (mover->IsPlayer() && static_cast<Player*>(mover)->IsFreeFlying())
mover->SetCanFly(true);
WorldPacket data(MSG_MOVE_KNOCK_BACK, 66);
data << guid.WriteAsPacked();
_player->m_mover->BuildMovementPacket(&data);
WriteMovementInfo(&data, &movementInfo);
_player->SetCanTeleport(true);
// knockback specific info
data << movementInfo.jump.sinAngle;
+13 -1
View File
@@ -1517,6 +1517,18 @@ enum AcoreStrings
// Pet rename command
LANG_PET_RENAME_INVALID = 35453,
LANG_PET_RENAME_SUCCESS = 35454
LANG_PET_RENAME_SUCCESS = 35454,
// Wintergrasp shutdown deferral
LANG_WG_SHUTDOWN_DEFERRED = 35455,
// npc showloot command
LANG_COMMAND_NOT_DEAD_OR_NO_LOOT = 35456,
LANG_COMMAND_NPC_SHOWLOOT_HEADER = 35457,
LANG_COMMAND_NPC_SHOWLOOT_MONEY = 35458,
LANG_COMMAND_NPC_SHOWLOOT_ITEMS = 35459,
LANG_COMMAND_NPC_SHOWLOOT_ENTRY = 35460,
LANG_COMMAND_NPC_SHOWLOOT_QUEST = 35461
};
#endif
+22 -2
View File
@@ -28,6 +28,7 @@
#include "Log.h"
#include "MoveSpline.h"
#include "MoveSplineInit.h"
#include "Player.h"
#include "PointMovementGenerator.h"
#include "RandomMovementGenerator.h"
#include "TargetedMovementGenerator.h"
@@ -619,7 +620,7 @@ void MotionMaster::MoveTakeoff(uint32 id, float x, float y, float z, float speed
void MotionMaster::MoveKnockbackFrom(float srcX, float srcY, float speedXY, float speedZ)
{
//this function may make players fall below map
if (_owner->IsPlayer())
if (_owner->IsPlayer() && _owner->IsClientControlled())
return;
if (speedXY <= 0.1f)
@@ -639,6 +640,15 @@ void MotionMaster::MoveKnockbackFrom(float srcX, float srcY, float speedXY, floa
init.SetOrientationFixed(true);
init.SetVelocity(speedXY);
// Do not mutate an active fleeing/confused movement generator,
// doing so breaks the movement upon landing from the knockback
MovementGeneratorType slotType = GetMotionSlotType(MOTION_SLOT_CONTROLLED);
if (slotType == FLEEING_MOTION_TYPE || slotType == CONFUSED_MOTION_TYPE)
{
init.Launch();
return;
}
Mutate(new EffectMovementGenerator(init, 0), MOTION_SLOT_CONTROLLED);
}
@@ -837,7 +847,17 @@ void MotionMaster::MoveTaxiFlight(uint32 path, uint32 pathnode)
{
LOG_DEBUG("movement.motionmaster", "{} taxi to (Path {} node {})", _owner->GetName(), path, pathnode);
FlightPathMovementGenerator* mgen = new FlightPathMovementGenerator(pathnode);
mgen->LoadPath(_owner->ToPlayer());
Player* player = _owner->ToPlayer();
if (!mgen->LoadPath(player))
{
LOG_ERROR("movement.motionmaster", "{} failed to build taxi path (Path {} node {}), clearing taxi destinations",
_owner->GetName(), path, pathnode);
player->m_taxi.ClearTaxiDestinations();
player->Dismount();
delete mgen;
return;
}
Mutate(mgen, MOTION_SLOT_CONTROLLED);
}
else
@@ -568,45 +568,56 @@ bool IsNodeIncludedInShortenedPath(TaxiPathNodeEntry const* p1, TaxiPathNodeEntr
return p1->mapid != p2->mapid || std::pow(p1->x - p2->x, 2) + std::pow(p1->y - p2->y, 2) > SKIP_SPLINE_POINT_DISTANCE_SQ;
}
void FlightPathMovementGenerator::LoadPath(Player* player)
bool FlightPathMovementGenerator::LoadPath(Player* player)
{
i_path.clear();
_pointsForPathSwitch.clear();
auto fail = [&]()
{
i_path.clear();
_pointsForPathSwitch.clear();
return false;
};
std::deque<uint32> const& taxi = player->m_taxi.GetPath();
float discount = player->GetReputationPriceDiscount(player->m_taxi.GetFlightMasterFactionTemplate());
for (uint32 src = 0, dst = 1; dst < taxi.size(); src = dst++)
{
uint32 path, cost;
sObjectMgr->GetTaxiPath(taxi[src], taxi[dst], path, cost);
if (path > sTaxiPathNodesByPath.size())
{
return;
}
if (path >= sTaxiPathNodesByPath.size())
return fail();
TaxiPathNodeList const& nodes = sTaxiPathNodesByPath[path];
if (!nodes.empty())
if (nodes.empty())
return fail();
TaxiPathNodeEntry const* start = nodes[0];
TaxiPathNodeEntry const* end = nodes[nodes.size() - 1];
bool passedPreviousSegmentProximityCheck = false;
bool addedPathNode = false;
for (uint32 i = 0; i < nodes.size(); ++i)
{
TaxiPathNodeEntry const* start = nodes[0];
TaxiPathNodeEntry const* end = nodes[nodes.size() - 1];
bool passedPreviousSegmentProximityCheck = false;
for (uint32 i = 0; i < nodes.size(); ++i)
if (passedPreviousSegmentProximityCheck || !src || i_path.empty() || IsNodeIncludedInShortenedPath(i_path[i_path.size() - 1], nodes[i]))
{
if (passedPreviousSegmentProximityCheck || !src || i_path.empty() || IsNodeIncludedInShortenedPath(i_path[i_path.size() - 1], nodes[i]))
if ((!src || (IsNodeIncludedInShortenedPath(start, nodes[i]) && i >= 2)) &&
(dst == taxi.size() - 1 || (IsNodeIncludedInShortenedPath(end, nodes[i]) && i < nodes.size() - 1)))
{
if ((!src || (IsNodeIncludedInShortenedPath(start, nodes[i]) && i >= 2)) &&
(dst == taxi.size() - 1 || (IsNodeIncludedInShortenedPath(end, nodes[i]) && i < nodes.size() - 1)))
{
passedPreviousSegmentProximityCheck = true;
i_path.push_back(nodes[i]);
}
}
else
{
i_path.pop_back();
--_pointsForPathSwitch.back().PathIndex;
passedPreviousSegmentProximityCheck = true;
i_path.push_back(nodes[i]);
addedPathNode = true;
}
}
else
{
i_path.pop_back();
--_pointsForPathSwitch.back().PathIndex;
}
}
if (!addedPathNode || i_path.empty())
return fail();
_pointsForPathSwitch.push_back({ uint32(i_path.size() - 1), int32(ceil(cost * discount)) });
}
@@ -630,6 +641,11 @@ void FlightPathMovementGenerator::LoadPath(Player* player)
else
i_currentNode = 0;
}
if (i_path.empty())
return fail();
return true;
}
void FlightPathMovementGenerator::DoInitialize(Player* player)
@@ -108,7 +108,7 @@ class FlightPathMovementGenerator : public MovementGeneratorMedium< Player, Flig
_endMapId = 0;
_preloadTargetNode = 0;
}
void LoadPath(Player* player);
bool LoadPath(Player* player);
void DoInitialize(Player*);
void DoReset(Player*);
void DoFinalize(Player*);
@@ -2016,7 +2016,6 @@ void SpellMgr::LoadSpellInfoCorrections()
// Potent Pheromones
ApplySpellFix({ 64321 }, [](SpellInfo* spellInfo)
{
spellInfo->AttributesEx3 |= SPELL_ATTR3_ONLY_ON_PLAYER;
spellInfo->AttributesEx |= SPELL_ATTR1_IMMUNITY_PURGES_EFFECT;
});
+42 -4
View File
@@ -56,6 +56,7 @@
#include "InstanceSaveMgr.h"
#include "ItemEnchantmentMgr.h"
#include "LFGMgr.h"
#include "Language.h"
#include "Log.h"
#include "LootItemStorage.h"
#include "LootMgr.h"
@@ -1454,10 +1455,15 @@ void World::_UpdateGameTime()
///- ... and it is overdue, stop the world (set m_stopEvent)
if (_shutdownTimer <= elapsed.count())
{
if (!(_shutdownMask & SHUTDOWN_MASK_IDLE) || sWorldSessionMgr->GetActiveAndQueuedSessionCount() == 0)
_stopEvent = true; // exist code already set
else
_shutdownTimer = 1; // minimum timer value to wait idle state
///- ... unless a Wintergrasp battle is running and deferral is enabled, in which case the
/// shutdown/restart is pushed past the end of the current battle and the world keeps running
if (!RescheduleShutdownForWintergrasp())
{
if (!(_shutdownMask & SHUTDOWN_MASK_IDLE) || sWorldSessionMgr->GetActiveAndQueuedSessionCount() == 0)
_stopEvent = true; // exist code already set
else
_shutdownTimer = 1; // minimum timer value to wait idle state
}
}
///- ... else decrease it and if necessary display a shutdown countdown to the users
else
@@ -1469,6 +1475,38 @@ void World::_UpdateGameTime()
}
}
/// Defer a pending shutdown/restart if a Wintergrasp battle is currently running.
/// Returns true when the shutdown timer was extended (world should keep running).
bool World::RescheduleShutdownForWintergrasp()
{
uint32 const bufferMinutes = getIntConfig(CONFIG_WINTERGRASP_DEFER_SHUTDOWN);
if (!bufferMinutes)
return false;
// Idle shutdowns wait for an empty server anyway; don't interfere with them
if (_shutdownMask & SHUTDOWN_MASK_IDLE)
return false;
Battlefield* wg = sBattlefieldMgr->GetBattlefieldByBattleId(BATTLEFIELD_BATTLEID_WG);
if (!wg || !wg->IsEnabled() || !wg->IsWarTime())
return false;
// GetTimer() is the battle time remaining in milliseconds
_shutdownTimer = wg->GetTimer() / IN_MILLISECONDS + bufferMinutes * MINUTE;
LOG_INFO("server.worldserver", "Server {} deferred: Wintergrasp battle in progress, rescheduled in {}",
(_shutdownMask & SHUTDOWN_MASK_RESTART ? "restart" : "shutdown"), secsToTimeString(_shutdownTimer));
sWorldSessionMgr->DoForAllOnlinePlayers([](Player* player)
{
LocaleConstant locale = player->GetSession()->GetSessionDbLocaleIndex();
sWorldSessionMgr->SendServerMessage(SERVER_MSG_STRING, sObjectMgr->GetAcoreString(LANG_WG_SHUTDOWN_DEFERRED, locale), player);
});
ShutdownMsg(true);
return true;
}
/// Shutdown the server
void World::ShutdownServ(uint32 time, uint32 options, uint8 exitcode, std::string const& reason)
{
+1
View File
@@ -245,6 +245,7 @@ public:
protected:
void _UpdateGameTime();
bool RescheduleShutdownForWintergrasp();
// callback for UpdateRealmCharacters
void _UpdateRealmCharCount(PreparedQueryResult resultCharCount,uint32 accountId);
+2 -2
View File
@@ -477,6 +477,7 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<bool>(CONFIG_OFFHAND_CHECK_AT_SPELL_UNLEARN, "OffhandCheckAtSpellUnlearn", true);
SetConfigValue<bool>(CONFIG_CREATURE_REPOSITION_AGAINST_NPCS, "Creature.RepositionAgainstNpcs", true);
SetConfigValue<bool>(CONFIG_CREATURE_INSTANCE_TELEPORT_TO_UNREACHABLE_TARGET, "Creature.Instance.TeleportToUnreachableTarget", false);
SetConfigValue<uint32>(CONFIG_CREATURE_STOP_FOR_PLAYER, "Creature.MovingStopTimeForPlayer", 180000);
SetConfigValue<uint32>(CONFIG_WATER_BREATH_TIMER, "WaterBreath.Timer", 180000, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value > 0; }, "> 0");
@@ -613,6 +614,7 @@ void WorldConfig::BuildConfigCache()
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_WINTERGRASP_DEFER_SHUTDOWN, "Wintergrasp.DeferShutdownTimer", 0);
SetConfigValue<uint32>(CONFIG_BIRTHDAY_TIME, "BirthdayTime", 1222964635);
SetConfigValue<bool>(CONFIG_MINIGOB_MANABONK, "Minigob.Manabonk.Enable", true);
@@ -663,8 +665,6 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<bool>(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_PORTAL_CHECK_ILVL, "DungeonAccessRequirements.PortalAvgIlevelCheck", false);
SetConfigValue<bool>(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_LFG_DBC_LEVEL_OVERRIDE, "DungeonAccessRequirements.LFGLevelDBCOverride", false);
SetConfigValue<uint32>(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_OPTIONAL_STRING_ID, "DungeonAccessRequirements.OptionalStringID", 0);
SetConfigValue<uint32>(CONFIG_NPC_EVADE_IF_NOT_REACHABLE, "NpcEvadeIfTargetIsUnreachable", 5);
SetConfigValue<uint32>(CONFIG_NPC_REGEN_TIME_IF_NOT_REACHABLE_IN_RAID, "NpcRegenHPTimeIfTargetIsUnreachable", 10);
SetConfigValue<bool>(CONFIG_REGEN_HP_CANNOT_REACH_TARGET_IN_RAID, "NpcRegenHPIfTargetIsUnreachable", true);
//Debug
+2 -2
View File
@@ -75,6 +75,7 @@ enum ServerConfigs
CONFIG_ARENA_QUEUE_ANNOUNCER_PLAYERONLY,
CONFIG_OFFHAND_CHECK_AT_SPELL_UNLEARN,
CONFIG_CREATURE_REPOSITION_AGAINST_NPCS,
CONFIG_CREATURE_INSTANCE_TELEPORT_TO_UNREACHABLE_TARGET,
CONFIG_VMAP_INDOOR_CHECK,
CONFIG_VMAP_ENABLE_LOS,
CONFIG_VMAP_ENABLE_HEIGHT,
@@ -332,6 +333,7 @@ enum ServerConfigs
CONFIG_WINTERGRASP_SKIP_BATTLE_SESSION_COUNT,
CONFIG_WINTERGRASP_KICK_VOA_PLAYERS,
CONFIG_WINTERGRASP_ESSENCE_BOTH_FACTIONS,
CONFIG_WINTERGRASP_DEFER_SHUTDOWN,
CONFIG_PACKET_SPOOF_BANMODE,
CONFIG_PACKET_SPOOF_BANDURATION,
CONFIG_WARDEN_CLIENT_RESPONSE_DELAY,
@@ -374,8 +376,6 @@ enum ServerConfigs
CONFIG_GUILD_MEMBER_LIMIT,
CONFIG_GM_LEVEL_CHANNEL_MODERATION,
CONFIG_TOGGLE_XP_COST,
CONFIG_NPC_EVADE_IF_NOT_REACHABLE,
CONFIG_NPC_REGEN_TIME_IF_NOT_REACHABLE_IN_RAID,
CONFIG_FFA_PVP_TIMER,
CONFIG_OUTDOOR_PVP_CAPTURE_RATE,
CONFIG_LOOT_NEED_BEFORE_GREED_ILVL_RESTRICTION,
+113 -99
View File
@@ -243,37 +243,119 @@ public:
Player* player = target.GetConnectedPlayer();
// Run the same script hook as the client return handler, failing early before any deletions
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
// Collect attachments before the script-hook check and any deletion, so a hook
// veto can still abort the command with nothing changed.
// mail_items is flushed to the DB on every item take, so it is authoritative even
// for online players; mails the session never loaded (expired at login, delivered
// mid-login, inserted externally) are returned with their items too. Same query
// shape as CHAR_SEL_MAILITEMS (LEFT JOIN to handle dangling mail_items) and same
// logic as Player::_LoadMailedItem. Items the session already has loaded are
// reused and stay owned by the session; the bool marks objects loaded (and owned)
// here.
std::vector<std::pair<Item*, bool /*loadedFromDb*/>> attachments;
QueryResult itemResult = CharacterDatabase.Query(
"SELECT creatorGuid, giftCreatorGuid, count, duration, charges, flags, enchantments,"
" randomPropertyId, durability, playedTime, text, mi.item_guid, itemEntry, ii.owner_guid"
" FROM mail_items mi LEFT JOIN item_instance ii ON mi.item_guid = ii.guid"
" WHERE mi.mail_id = {}", mailId);
if (itemResult)
{
do
{
Field* itemFields = itemResult->Fetch();
uint32 itemGuid = itemFields[11].Get<uint32>();
uint32 itemEntry = itemFields[12].Get<uint32>();
// Prefer the item object the session already has loaded over creating a duplicate
if (Item* item = player ? player->GetMItem(itemGuid) : nullptr)
{
attachments.emplace_back(item, false);
continue;
}
// Handle dangling mail_items (missing item_instance)
if (!itemEntry)
{
LOG_ERROR("misc", "cs_mail: Mail #{} has dangling mail_items row for item_guid {}. Cleaning up.", mailId, itemGuid);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
continue;
}
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemEntry);
if (!proto)
{
LOG_ERROR("misc", "cs_mail: Mail #{} has unknown item (entry: {}, guid: {}). Cleaning up.", mailId, itemEntry, itemGuid);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
// The mail is going away, so drop the unloadable item_instance row too
Item::DeleteFromDB(trans, itemGuid);
continue;
}
Item* item = NewItemOrBag(proto);
ObjectGuid ownerGuid = itemFields[13].Get<uint32>()
? ObjectGuid::Create<HighGuid::Player>(itemFields[13].Get<uint32>())
: ObjectGuid::Empty;
if (!item->LoadFromDB(itemGuid, ownerGuid, itemFields, itemEntry))
{
LOG_ERROR("misc", "cs_mail: Item (GUID: {}) in mail #{} failed to load. Cleaning up.", itemGuid, mailId);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
// Queue the row cleanup on the shared transaction instead of executing it
// immediately; SaveToDB in ITEM_REMOVED state also frees the object
item->FSetState(ITEM_REMOVED);
item->SaveToDB(trans);
continue;
}
attachments.emplace_back(item, true);
} while (itemResult->NextRow());
}
// Run the same script hook as the client return handler for online targets,
// covering DB-loaded attachments as well, before any deletion
if (player)
{
Mail* m = player->GetMail(mailId);
if (m)
{
ObjectGuid senderGuid = ObjectGuid(HighGuid::Player, sender);
ObjectGuid senderGuid = ObjectGuid(HighGuid::Player, sender);
bool blocked = false;
if (m->HasItems())
{
for (auto const& itemInfo : m->items)
{
Item* item = player->GetMItem(itemInfo.item_guid);
if (item && !sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, item))
{
handler->SendErrorMessage(LANG_MAIL_RETURN_HOOK_BLOCKED);
return true;
}
}
}
else if (!sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, nullptr))
{
handler->SendErrorMessage(LANG_MAIL_RETURN_HOOK_BLOCKED);
return true;
}
if (attachments.empty())
blocked = !sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, nullptr);
for (auto const& [item, loadedFromDb] : attachments)
{
if (blocked)
break;
blocked = !sScriptMgr->OnPlayerCanSendMail(player, senderGuid, ObjectGuid::Empty, subject, body, money, 0, item);
}
if (blocked)
{
for (auto const& [item, loadedFromDb] : attachments)
if (loadedFromDb)
delete item;
handler->SendErrorMessage(LANG_MAIL_RETURN_HOOK_BLOCKED);
return true;
}
}
// Same logic as WorldSession::HandleReturnToSender
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_MAIL_BY_ID);
stmt->SetData(0, mailId);
trans->Append(stmt);
@@ -288,87 +370,19 @@ public:
if (player)
{
// Online: same logic as WorldSession::HandleReturnToSender
// Get pointer before RemoveMail (which removes from deque but does not delete the object)
// Drop the mail from the session's loaded state if present.
// RemoveMail only erases from the deque, it does not delete the object.
Mail* m = player->GetMail(mailId);
player->RemoveMail(mailId);
if (m && m->HasItems())
{
for (auto const& itemInfo : m->items)
{
if (Item* item = player->GetMItem(itemInfo.item_guid))
draft.AddItem(item);
player->RemoveMItem(itemInfo.item_guid);
}
}
delete m;
}
else
for (auto const& [item, loadedFromDb] : attachments)
{
// Offline: load Item* objects from DB using same query shape as CHAR_SEL_MAILITEMS
// (LEFT JOIN to handle dangling mail_items) and same logic as Player::_LoadMailedItem
QueryResult itemResult = CharacterDatabase.Query(
"SELECT creatorGuid, giftCreatorGuid, count, duration, charges, flags, enchantments,"
" randomPropertyId, durability, playedTime, text, mi.item_guid, itemEntry, ii.owner_guid"
" FROM mail_items mi LEFT JOIN item_instance ii ON mi.item_guid = ii.guid"
" WHERE mi.mail_id = {}", mailId);
if (!loadedFromDb)
player->RemoveMItem(item->GetGUID().GetCounter());
if (itemResult)
{
do
{
Field* itemFields = itemResult->Fetch();
uint32 itemGuid = itemFields[11].Get<uint32>();
uint32 itemEntry = itemFields[12].Get<uint32>();
// Handle dangling mail_items (missing item_instance)
if (!itemEntry)
{
LOG_ERROR("misc", "cs_mail: Mail #{} has dangling mail_items row for item_guid {}. Cleaning up.", mailId, itemGuid);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
continue;
}
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemEntry);
if (!proto)
{
LOG_ERROR("misc", "cs_mail: Mail #{} has unknown item (entry: {}, guid: {}). Cleaning up.", mailId, itemEntry, itemGuid);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
continue;
}
Item* item = NewItemOrBag(proto);
ObjectGuid ownerGuid = itemFields[13].Get<uint32>()
? ObjectGuid::Create<HighGuid::Player>(itemFields[13].Get<uint32>())
: ObjectGuid::Empty;
if (!item->LoadFromDB(itemGuid, ownerGuid, itemFields, itemEntry))
{
LOG_ERROR("misc", "cs_mail: Item (GUID: {}) in mail #{} failed to load. Cleaning up.", itemGuid, mailId);
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_INVALID_MAIL_ITEM);
delStmt->SetData(0, itemGuid);
trans->Append(delStmt);
item->FSetState(ITEM_REMOVED);
CharacterDatabaseTransaction nullTrans = CharacterDatabaseTransaction(nullptr);
item->SaveToDB(nullTrans);
return true;
}
draft.AddItem(item);
} while (itemResult->NextRow());
}
draft.AddItem(item);
}
uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(ObjectGuid(HighGuid::Player, receiver));
+14 -1
View File
@@ -499,8 +499,21 @@ public:
// Remove from LFG queues
sLFGMgr->LeaveAllLfgQueues(player->GetGUID(), false);
// Book the reservation like the queue path does, so it is symmetric
// with RemovePlayerAtLeave's decrement and the 0-players/0-invited
// state can't let Battleground::Update delete the arena while players
// are still on the loading screen.
bg->IncreaseInvitedCount(teamId);
player->SetBattlegroundId(bg->GetInstanceID(), bgTypeId, queueSlot, true, false, teamId);
sBattlegroundMgr->SendToBattleground(player, bg->GetInstanceID(), bgTypeId);
// A synchronous teleport failure would strand that reservation (the
// player never enters and never reaches RemovePlayerAtLeave), leaving
// the arena undeletable; release it and reset his bg data.
if (!sBattlegroundMgr->SendToBattleground(player, bg->GetInstanceID(), bgTypeId))
{
bg->DecreaseInvitedCount(teamId);
player->SetBattlegroundId(0, BATTLEGROUND_TYPE_NONE, PLAYER_MAX_BATTLEGROUND_QUEUES, false, false, TEAM_NEUTRAL);
}
}
handler->PSendSysMessage("Success! Players are now being teleported to the arena.");
+46
View File
@@ -24,6 +24,7 @@
#include "GridNotifiers.h"
#include "GridNotifiersImpl.h"
#include "Language.h"
#include "LootMgr.h"
#include "MapMgr.h"
#include "ObjectMgr.h"
#include "Pet.h"
@@ -201,6 +202,7 @@ public:
{ "follow", npcFollowCommandTable },
{ "load", HandleNpcLoadCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "set", npcSetCommandTable },
{ "showloot", HandleNpcShowLootCommand, rbac::RBAC_PERM_COMMAND_NPC_SHOWLOOT, Console::No },
{ "spawngroup", HandleNpcSpawnGroupCommand, SEC_ADMINISTRATOR, Console::No },
{ "despawngroup", HandleNpcDespawnGroupCommand, SEC_ADMINISTRATOR, Console::No }
};
@@ -1490,6 +1492,50 @@ public:
return true;
}
static void ShowLootEntry(ChatHandler* handler, LootItem const& item)
{
ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(item.itemid);
std::string name = itemTemplate ? itemTemplate->Name1 : "Unknown item";
if (itemTemplate)
if (ItemLocale const* il = sObjectMgr->GetItemLocale(item.itemid))
ObjectMgr::GetLocaleString(il->Name, handler->GetSessionDbLocaleIndex(), name);
uint32 color = ItemQualityColors[itemTemplate ? itemTemplate->Quality : uint32(ITEM_QUALITY_POOR)];
handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_ENTRY, item.count, color, item.itemid, name, item.itemid);
}
static bool HandleNpcShowLootCommand(ChatHandler* handler)
{
Creature* creatureTarget = handler->getSelectedCreature();
if (!creatureTarget || creatureTarget->IsPet())
{
handler->SendErrorMessage(LANG_SELECT_CREATURE);
return false;
}
Loot const& loot = creatureTarget->loot;
if (!creatureTarget->isDead() || (loot.empty() && loot.quest_items.empty()))
{
handler->SendErrorMessage(LANG_COMMAND_NOT_DEAD_OR_NO_LOOT, creatureTarget->GetName());
return false;
}
handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_HEADER, creatureTarget->GetName(), creatureTarget->GetEntry());
handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_MONEY, loot.gold / GOLD, (loot.gold % GOLD) / SILVER, loot.gold % SILVER);
handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_ITEMS, loot.items.size());
for (LootItem const& item : loot.items)
if (!item.is_looted)
ShowLootEntry(handler, item);
handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_QUEST, loot.quest_items.size());
for (LootItem const& item : loot.quest_items)
if (!item.is_looted)
ShowLootEntry(handler, item);
return true;
}
};
void AddSC_npc_commandscript()
@@ -101,6 +101,23 @@ class spell_q12641_death_comes_from_on_high_recall_eye : public SpellScript
}
};
// 51761 - Rain of Darkness
class spell_q12641_rain_of_darkness : public SpellScript
{
PrepareSpellScript(spell_q12641_rain_of_darkness);
void ModDestHeight(SpellDestination& dest)
{
Position const offset = { 0.0f, 0.0f, 15.0f, 0.0f };
dest.RelocateOffset(offset);
}
void Register() override
{
OnDestinationTargetSelect += SpellDestinationTargetSelectFn(spell_q12641_rain_of_darkness::ModDestHeight, EFFECT_0, TARGET_DEST_CASTER_BACK);
}
};
enum GiftOfTheHarvester
{
NPC_GHOUL = 28845,
@@ -449,6 +466,7 @@ void AddSC_the_scarlet_enclave_c1()
{
RegisterSpellScript(spell_q12641_death_comes_from_on_high_summon_ghouls);
RegisterSpellScript(spell_q12641_death_comes_from_on_high_recall_eye);
RegisterSpellScript(spell_q12641_rain_of_darkness);
RegisterSpellScript(spell_item_gift_of_the_harvester);
RegisterSpellScript(spell_q12698_the_gift_that_keeps_on_giving);
new npc_scarlet_ghoul();
@@ -679,25 +679,29 @@ struct boss_malygos : public BossAI
{
for (uint8 i = 0; i < NUM_MAX_SURGE_TARGETS; ++i)
_surgeTargetGUID[i].Clear();
me->CastSpell((Unit*)nullptr, SPELL_SURGE_OF_POWER_WARN_SELECTOR_25, true);
DoCastAOE(SPELL_SURGE_OF_POWER_WARN_SELECTOR_25, true);
me->m_Events.AddEventAtOffset([this]
{
me->CastSpell((Unit*)nullptr, SPELL_PH3_SURGE_OF_POWER_25, true);
DoCastAOE(SPELL_PH3_SURGE_OF_POWER_25, true);
}, 3s);
}
else
{
for (uint8 i = 0; i < NUM_MAX_SURGE_TARGETS; ++i)
_surgeTargetGUID[i].Clear();
if (Unit* target = SelectTarget(SelectTargetMethod::Random, 0, 0.0f, false, true, SPELL_RIDE_RED_DRAGON_BUDDY))
{
if (Vehicle* vehicle = target->GetVehicleKit())
if (Unit* passenger = vehicle->GetPassenger(0))
if (Player* player = passenger->ToPlayer())
Talk(EMOTE_SURGE_OF_POWER_WARNING_P3, player);
ObjectGuid targetGuid = target->GetGUID();
me->m_Events.AddEventAtOffset([this, targetGuid]
SetGUID(target->GetGUID(), DATA_FIRST_SURGE_TARGET_GUID);
me->m_Events.AddEventAtOffset([this]
{
if (Unit* delayedTarget = ObjectAccessor::GetUnit(*me, targetGuid))
me->CastSpell(delayedTarget, SPELL_PH3_SURGE_OF_POWER, true);
DoCastAOE(SPELL_PH3_SURGE_OF_POWER, true);
}, 3s);
}
}
@@ -1277,15 +1281,22 @@ class spell_eoe_ph3_surge_of_power : public SpellScript
{
PrepareSpellScript(spell_eoe_ph3_surge_of_power);
bool Load() override
{
return GetCaster()->IsCreature();
}
void FilterTargets(std::list<WorldObject*>& targets)
{
// Target selection and warning are handled in boss AI.
// Here we just restrict area targets to the explicit cast target.
if (Unit* explTarget = GetExplTargetUnit())
// The spell targets an area, but only the drake that received the fixate warning
// should be hit. The boss AI stores that drake's GUID; keep only that target.
Creature* caster = GetCaster()->ToCreature();
ObjectGuid targetGuid = caster->AI()->GetGUID(DATA_FIRST_SURGE_TARGET_GUID);
targets.remove_if([targetGuid](WorldObject* target)
{
targets.clear();
targets.push_back(explTarget);
}
return target->GetGUID() != targetGuid;
});
}
void Register() override
@@ -516,9 +516,7 @@ struct boss_runemaster_molgeim : public ScriptedAI
events.RescheduleEvent(EVENT_SHIELD_OF_RUNES, 27s, 34s);
break;
case EVENT_RUNE_OF_DEATH:
if (Unit* target = SelectTarget(SelectTargetMethod::Random))
me->CastSpell(target, SPELL_RUNE_OF_DEATH, true);
DoCastRandomTarget(SPELL_RUNE_OF_DEATH, 0, 0.0f, true, true);
Talk(SAY_MOLGEIM_RUNE_DEATH);
events.Repeat(30s, 40s);
break;
@@ -783,11 +781,13 @@ struct boss_stormcaller_brundir : public ScriptedAI
me->SetDisableGravity(true);
me->SetHover(true);
me->CombatStop();
// AttackStop (not CombatStop) so he stays in combat while REACT_PASSIVE;
// otherwise UpdateVictim() sees engaged+passive+out-of-combat and evades
// him mid-flight, resetting the whole encounter.
me->AttackStop();
me->StopMoving();
me->SetReactState(REACT_PASSIVE);
me->SetGuidValue(UNIT_FIELD_TARGET, ObjectGuid::Empty);
me->SetUnitFlag(UNIT_FLAG_STUNNED);
me->CastSpell(me, SPELL_LIGHTNING_TENDRILS, true);
me->CastSpell(me, SPELL_LIGHTNING_TENDRILS_2, true);
@@ -807,16 +807,17 @@ struct boss_stormcaller_brundir : public ScriptedAI
me->SetHover(false);
me->SetReactState(REACT_AGGRESSIVE);
me->SetDisableGravity(false);
if (Unit* flyTarget = ObjectAccessor::GetUnit(*me, _flyTargetGUID))
{
me->Attack(flyTarget, false);
}
me->SetRegeneratingHealth(true);
_flyTargetGUID.Clear();
me->RemoveAura(sSpellMgr->GetSpellIdForDifficulty(SPELL_LIGHTNING_TENDRILS, me));
me->RemoveAura(SPELL_LIGHTNING_TENDRILS_2);
DoResetThreatList();
// AttackStart (not Attack) so MoveChase is re-issued; Attack() alone only
// sets the victim, leaving him landed but standing still.
if (Unit* flyTarget = ObjectAccessor::GetUnit(*me, _flyTargetGUID))
AttackStart(flyTarget);
_flyTargetGUID.Clear();
events.CancelEvent(EVENT_LIGHTNING_FLIGHT);
break;
case EVENT_ENRAGE:
@@ -56,6 +56,7 @@ enum LeviathanSpells
// Shutdown spells
SPELL_SYSTEMS_SHUTDOWN = 62475,
SPELL_OVERLOAD_CIRCUIT = 62399,
SPELL_START_THE_ENGINE = 62472,
// hard mode
SPELL_TOWER_OF_STORMS = 65076,
@@ -84,6 +85,10 @@ enum LeviathanSpells
SPELL_LIQUID_PYRITE = 62494,
SPELL_DUSTY_EXPLOSION = 63360,
SPELL_DUST_CLOUD_IMPACT = 54740,
// Hookshot
SPELL_HOOKSHOT_AURA = 62336,
SPELL_HOOKSHOT = 62323,
};
enum GosNpcs
@@ -115,12 +120,12 @@ enum Events
EVENT_MISSILE = 2,
EVENT_VENT = 3,
EVENT_SPEED = 4,
EVENT_REINSTALL = 5,
EVENT_HODIRS_FURY = 6,
EVENT_FREYA = 7,
EVENT_MIMIRONS_INFERNO = 8,
EVENT_THORIMS_HAMMER = 9,
EVENT_SOUND_BEGINNING = 10,
EVENT_EJECT_PLAYERS = 11,
};
enum Texts
@@ -145,6 +150,7 @@ enum Texts
FLAME_LEVIATHAN_EMOTE_NATURE = 17,
FLAME_LEVIATHAN_EMOTE_STORM = 18,
FLAME_LEVIATHAN_EMOTE_REACTIVATE = 19,
FLAME_LEVIATHAN_EMOTE_OVERLOAD_START = 20,
// NPC_BRANN_RADIO
BRANN_RADIO_SAY_FL_START_0 = 0,
@@ -179,7 +185,6 @@ enum Misc
ACTION_START_BRANN_EVENT = 3,
ACTION_DESPAWN_ADDS = 4,
ACTION_DELAY_CANNON = 5,
ACTION_DESTROYED_TURRET = 6,
};
const Position homePos = {322.39f, -14.5f, 409.8f, 3.14f};
@@ -197,7 +202,7 @@ struct boss_flame_leviathan : public BossAI
uint32 _speakTimer;
uint8 _towersCount;
bool _shutdown;
uint32 _destroyedTurretCount;
uint8 _overloadCircuitCount;
// Custom
void BindPlayers();
@@ -284,7 +289,7 @@ struct boss_flame_leviathan : public BossAI
_startTimer = 1;
_speakTimer = 0;
_towersCount = 0;
_destroyedTurretCount = 0;
_overloadCircuitCount = 0;
if (instance->GetBossState(BOSS_LEVIATHAN) != SPECIAL)
{
@@ -402,13 +407,6 @@ struct boss_flame_leviathan : public BossAI
else
Talk(FLAME_LEVIATHAN_SAY_TOWER_NONE);
return;
case EVENT_REINSTALL:
for (uint8 i = RAID_MODE(0, 2); i < 4; ++i)
if (Unit* seat = vehicle->GetPassenger(i))
if (seat->IsCreature())
seat->ToCreature()->AI()->EnterEvadeMode();
Talk(FLAME_LEVIATHAN_EMOTE_REACTIVATE);
return;
case EVENT_THORIMS_HAMMER:
SummonTowerHelpers(TOWER_OF_STORMS);
events.Repeat(1min, 2min);
@@ -430,6 +428,13 @@ struct boss_flame_leviathan : public BossAI
Talk(FLAME_LEVIATHAN_EMOTE_FROST);
Talk(FLAME_LEVIATHAN_SAY_TOWER_FROST);
return;
case EVENT_EJECT_PLAYERS:
for (int8 i = 0; i < 4; ++i)
if (Unit* seat = vehicle->GetPassenger(i))
if (Vehicle* seatVehicle = seat->GetVehicleKit())
if (Unit* player = seatVehicle->GetPassenger(SEAT_PLAYER))
player->ExitVehicle();
return;
}
if (me->isAttackReady() && !me->HasUnitState(UNIT_STATE_STUNNED))
@@ -441,20 +446,6 @@ struct boss_flame_leviathan : public BossAI
}
}
}
void DoAction(int32 action) override
{
if (action == ACTION_DESTROYED_TURRET)
{
++_destroyedTurretCount;
if (_destroyedTurretCount == RAID_MODE<uint32>(2, 4))
{
_destroyedTurretCount = 0;
me->CastSpell(me, SPELL_SYSTEMS_SHUTDOWN, true);
}
}
}
};
void boss_flame_leviathan::BindPlayers()
@@ -574,16 +565,37 @@ void boss_flame_leviathan::ScheduleEvents()
void boss_flame_leviathan::SpellHit(Unit* /*caster*/, SpellInfo const* spellInfo)
{
if (spellInfo->Id == SPELL_SYSTEMS_SHUTDOWN)
if (spellInfo->Id == SPELL_OVERLOAD_CIRCUIT)
{
++_overloadCircuitCount;
if (_overloadCircuitCount == 1)
Talk(FLAME_LEVIATHAN_EMOTE_OVERLOAD_START);
uint8 const threshold = me->GetMap()->Is25ManRaid() ? 4 : 2;
if (_overloadCircuitCount >= threshold)
{
_overloadCircuitCount = 0;
me->CastSpell(me, SPELL_SYSTEMS_SHUTDOWN, true);
}
}
else if (spellInfo->Id == SPELL_SYSTEMS_SHUTDOWN)
{
_shutdown = true; // ACHIEVEMENT
Talk(FLAME_LEVIATHAN_EMOTE_OVERLOAD);
Talk(FLAME_LEVIATHAN_EMOTE_REPAIR);
Talk(FLAME_LEVIATHAN_SAY_OVERLOAD);
Talk(FLAME_LEVIATHAN_EMOTE_OVERLOAD);
events.DelayEvents(21ms);
events.ScheduleEvent(EVENT_REINSTALL, 20ms);
events.ScheduleEvent(EVENT_EJECT_PLAYERS, 3s);
}
else if (spellInfo->Id == SPELL_START_THE_ENGINE)
{
// Respawn turrets
for (uint8 i = 0; i < 4; ++i)
if (Unit* seat = vehicle->GetPassenger(i))
if (seat->IsCreature())
seat->ToCreature()->AI()->EnterEvadeMode();
Talk(FLAME_LEVIATHAN_EMOTE_REACTIVATE);
}
else if (spellInfo->Id == 62522 /*SPELL_ELECTROSHOCK*/)
me->InterruptNonMeleeSpells(false);
@@ -667,7 +679,11 @@ struct boss_flame_leviathan_seat : public VehicleAI
}
Vehicle* vehicle;
uint32 _despawnTimer;
// Despawn 2 seats in 10-man.
static constexpr uint32 DESPAWN_DELAY_10MAN = 2000;
bool _pending10ManDespawn;
uint32 _despawnCheckTimer;
void EnterEvadeMode(EvadeReason /*why*/) override
{
@@ -676,19 +692,20 @@ struct boss_flame_leviathan_seat : public VehicleAI
void Reset() override
{
_despawnTimer = !me->GetMap()->Is25ManRaid();
_pending10ManDespawn = !me->GetMap()->Is25ManRaid();
_despawnCheckTimer = 0;
}
void UpdateAI(uint32 diff) override
{
if (_despawnTimer)
if (_pending10ManDespawn)
{
_despawnTimer += diff;
if (_despawnTimer >= 2000)
_despawnCheckTimer += diff;
if (_despawnCheckTimer >= DESPAWN_DELAY_10MAN)
{
_despawnTimer = 0;
if (Vehicle* veh = me->GetVehicle())
if (veh->GetPassenger(0) == me || veh->GetPassenger(1) == me)
_pending10ManDespawn = false;
if (Vehicle* parentVehicle = me->GetVehicle())
if (parentVehicle->GetPassenger(0) == me || parentVehicle->GetPassenger(1) == me)
me->DespawnOrUnsummon(1ms);
}
}
@@ -711,6 +728,14 @@ struct boss_flame_leviathan_seat : public VehicleAI
{
if (Unit* turret = me->GetVehicleKit()->GetPassenger(SEAT_TURRET))
{
if (apply)
{
who->RemoveAurasDueToSpell(SPELL_HOOKSHOT);
who->RemoveAurasDueToSpell(SPELL_HOOKSHOT_AURA);
}
else
who->CastSpell(who, SPELL_SMOKE_TRAIL, true);
if (apply)
{
turret->ReplaceAllUnitFlags(UNIT_FLAG_NONE);
@@ -726,6 +751,14 @@ struct boss_flame_leviathan_seat : public VehicleAI
turret->ToCreature()->AI()->EnterEvadeMode();
}
}
if (Unit* device = me->GetVehicleKit()->GetPassenger(SEAT_DEVICE))
{
if (apply)
device->SetNpcFlag(UNIT_NPC_FLAG_SPELLCLICK);
else
device->RemoveNpcFlag(UNIT_NPC_FLAG_SPELLCLICK);
}
}
}
};
@@ -755,12 +788,9 @@ struct boss_flame_leviathan_defense_turret : public TurretAI
if (Player* player = killer->ToPlayer())
player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_GET_KILLING_BLOWS, 1, 0, me);
if (Vehicle* vehicle = me->GetVehicle())
if (Unit* device = vehicle->GetPassenger(SEAT_DEVICE))
device->ReplaceAllUnitFlags(UNIT_FLAG_NONE); // unselectable
if (Creature* leviathan = _instance->GetCreature(BOSS_LEVIATHAN))
leviathan->AI()->DoAction(ACTION_DESTROYED_TURRET);
if (Vehicle* seatVehicle = me->GetVehicle())
if (Unit* device = seatVehicle->GetPassenger(SEAT_DEVICE))
device->CastSpell(device, SPELL_OVERLOAD_CIRCUIT, true);
}
bool CanAIAttack(Unit const* who) const override
@@ -806,10 +836,7 @@ struct boss_flame_leviathan_overload_device : public NullCreatureAI
me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE);
if (Unit* player = me->GetVehicle()->GetPassenger(SEAT_PLAYER))
{
me->GetVehicleBase()->CastSpell(player, SPELL_SMOKE_TRAIL, true);
player->ExitVehicle();
}
}
}
};
@@ -1245,7 +1272,7 @@ class spell_systems_shutdown_aura : public AuraScript
bool Validate(SpellInfo const* /*spellInfo*/) override
{
return ValidateSpellInfo({ SPELL_GATHERING_SPEED });
return ValidateSpellInfo({ SPELL_GATHERING_SPEED, SPELL_OVERLOAD_CIRCUIT, SPELL_START_THE_ENGINE });
}
void OnApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
@@ -1256,6 +1283,7 @@ class spell_systems_shutdown_aura : public AuraScript
owner->SetControlled(true, UNIT_STATE_STUNNED);
owner->RemoveAurasDueToSpell(SPELL_GATHERING_SPEED);
owner->RemoveAurasDueToSpell(SPELL_OVERLOAD_CIRCUIT);
if (Vehicle* vehicle = owner->GetVehicleKit())
if (Unit* cannon = vehicle->GetPassenger(SEAT_CANNON))
cannon->GetAI()->DoAction(ACTION_DELAY_CANNON);
@@ -1268,6 +1296,7 @@ class spell_systems_shutdown_aura : public AuraScript
return;
owner->SetControlled(false, UNIT_STATE_STUNNED);
owner->CastSpell(owner, SPELL_START_THE_ENGINE, true);
}
void Register() override
@@ -1352,50 +1381,58 @@ class spell_vehicle_throw_passenger : public SpellScript
{
PrepareSpellScript(spell_vehicle_throw_passenger);
bool Validate(SpellInfo const* /*spellInfo*/) override
{
return ValidateSpellInfo({ SPELL_HOOKSHOT_AURA });
}
void HandleScript()
{
Spell* baseSpell = GetSpell();
SpellCastTargets targets = baseSpell->m_targets;
if (Vehicle* vehicle = GetCaster()->GetVehicleKit())
if (Unit* passenger = vehicle->GetPassenger(3))
Vehicle* vehicle = GetCaster()->GetVehicleKit();
Unit* passenger = vehicle ? vehicle->GetPassenger(3) : nullptr;
if (!vehicle || !passenger)
return;
// Find nearest unoccupied Flame Leviathan seat near trajectory destination
Position const* dst = GetExplTargetDest();
if (!dst)
return;
constexpr float SEARCH_RADIUS = 99.0f;
std::list<WorldObject*> targetList;
Acore::WorldObjectSpellAreaTargetCheck check(SEARCH_RADIUS, dst, GetCaster(), GetCaster(), GetSpellInfo(), TARGET_CHECK_DEFAULT, nullptr);
Acore::WorldObjectListSearcher searcher(GetCaster(), targetList, check);
Cell::VisitObjects(GetCaster(), searcher, SEARCH_RADIUS);
Unit* seatTarget = nullptr;
float minDist = SEARCH_RADIUS * SEARCH_RADIUS;
for (WorldObject* obj : targetList)
{
Unit* unit = obj->ToUnit();
if (!unit || unit->GetEntry() != NPC_SEAT) continue;
Vehicle* seat = unit->GetVehicleKit();
Unit* device = seat ? seat->GetPassenger(SEAT_DEVICE) : nullptr;
if (!seat || seat->GetPassenger(0) || !device || device->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) continue;
float dist = unit->GetExactDistSq(dst);
if (dist < minDist)
{
// use 99 because it is 3d search
std::list<WorldObject*> targetList;
Acore::WorldObjectSpellAreaTargetCheck check(99, GetExplTargetDest(), GetCaster(), GetCaster(), GetSpellInfo(), TARGET_CHECK_DEFAULT, nullptr);
Acore::WorldObjectListSearcher<Acore::WorldObjectSpellAreaTargetCheck> searcher(GetCaster(), targetList, check);
Cell::VisitObjects(GetCaster(), searcher, 99.0f);
float minDist = 99 * 99;
Unit* target = nullptr;
for (std::list<WorldObject*>::iterator itr = targetList.begin(); itr != targetList.end(); ++itr)
{
if (Unit* unit = (*itr)->ToUnit())
if (unit->GetEntry() == NPC_SEAT)
if (Vehicle* seat = unit->GetVehicleKit())
if (!seat->GetPassenger(0))
if (Unit* device = seat->GetPassenger(2))
if (!device->GetCurrentSpell(CURRENT_CHANNELED_SPELL))
{
float dist = unit->GetExactDistSq(targets.GetDstPos());
if (dist < minDist)
{
minDist = dist;
target = unit;
}
}
}
if (target && target->IsWithinDist2d(targets.GetDstPos(), GetSpellInfo()->Effects[EFFECT_0].CalcRadius() * 2)) // now we use *2 because the location of the seat is not correct
{
passenger->ExitVehicle();
passenger->EnterVehicle(target, 0);
}
else
{
passenger->ExitVehicle();
float x, y, z;
targets.GetDstPos()->GetPosition(x, y, z);
passenger->GetMotionMaster()->MoveJump(x, y, z, targets.GetSpeedXY(), targets.GetSpeedZ());
}
minDist = dist;
seatTarget = unit;
}
}
// Launch passenger toward destination
float x, y, z;
dst->GetPosition(x, y, z);
passenger->ExitVehicle();
passenger->GetMotionMaster()->MoveJump(x, y, z, GetSpell()->m_targets.GetSpeedXY(), GetSpell()->m_targets.GetSpeedZ());
if (seatTarget && seatTarget->IsWithinDist2d(dst, GetSpellInfo()->Effects[EFFECT_0].CalcRadius() * 2)) // now we use *2 because the location of the seat is not correct
passenger->CastCustomSpell(SPELL_HOOKSHOT_AURA, SPELLVALUE_AURA_DURATION, 5000, passenger, true);
}
void Register() override
@@ -1404,6 +1441,28 @@ class spell_vehicle_throw_passenger : public SpellScript
}
};
// 62336 Hookshot Aura
class spell_hookshot_aura : public AuraScript
{
PrepareAuraScript(spell_hookshot_aura);
bool Validate(SpellInfo const* /*spellInfo*/) override
{
return ValidateSpellInfo({ SPELL_HOOKSHOT });
}
void OnPeriodic(AuraEffect const* aurEff)
{
PreventDefaultAction();
GetUnitOwner()->CastSpell(GetUnitOwner(), GetSpellInfo()->Effects[aurEff->GetEffIndex()].TriggerSpell, true);
}
void Register() override
{
OnEffectPeriodic += AuraEffectPeriodicFn(spell_hookshot_aura::OnPeriodic, EFFECT_0, SPELL_AURA_PERIODIC_DUMMY);
}
};
class spell_tar_blaze_aura : public AuraScript
{
PrepareAuraScript(spell_tar_blaze_aura);
@@ -1476,31 +1535,6 @@ class spell_vehicle_grab_pyrite : public SpellScript
}
};
class spell_vehicle_circuit_overload_aura : public AuraScript
{
PrepareAuraScript(spell_vehicle_circuit_overload_aura);
bool Validate(SpellInfo const* /*spellInfo*/) override
{
return ValidateSpellInfo({ SPELL_SYSTEMS_SHUTDOWN });
}
void OnPeriodic(AuraEffect const* /*aurEff*/)
{
if (Unit* target = GetTarget())
if (int(target->GetAppliedAuras().count(SPELL_OVERLOAD_CIRCUIT)) >= (target->GetMap()->Is25ManRaid() ? 4 : 2))
{
target->CastSpell(target, SPELL_SYSTEMS_SHUTDOWN, true);
target->RemoveAurasDueToSpell(SPELL_OVERLOAD_CIRCUIT);
}
}
void Register() override
{
OnEffectPeriodic += AuraEffectPeriodicFn(spell_vehicle_circuit_overload_aura::OnPeriodic, EFFECT_1, SPELL_AURA_PERIODIC_DUMMY);
}
};
class spell_orbital_supports_aura : public AuraScript
{
PrepareAuraScript(spell_orbital_supports_aura);
@@ -1757,9 +1791,9 @@ void AddSC_boss_flame_leviathan()
RegisterSpellScript(spell_systems_shutdown_aura);
RegisterSpellScript(spell_pursue);
RegisterSpellScript(spell_vehicle_throw_passenger);
RegisterSpellScript(spell_hookshot_aura);
RegisterSpellScript(spell_tar_blaze_aura);
RegisterSpellScript(spell_vehicle_grab_pyrite);
RegisterSpellScript(spell_vehicle_circuit_overload_aura);
RegisterSpellScript(spell_orbital_supports_aura);
RegisterSpellScript(spell_thorims_hammer);
RegisterSpellScript(spell_transitus_shield_beam_aura);
@@ -293,14 +293,18 @@ struct boss_freya : public BossAI
++_elderCount;
}
uint32 chestId = RAID_MODE(GO_FREYA_CHEST, GO_FREYA_CHEST_HERO);
chestId -= 2 * _elderCount; // offset
if (GameObject* go = me->SummonGameObject(chestId, 2345.61f, -71.20f, 425.104f, 3.0f, 0, 0, 0, 0, 0))
// Summon the chest via spell so it is a wild object not owned by Freya,
// otherwise it despawns with her when she teleports out. The spell is
// chosen by raid size and how many Elders empowered her.
// Order intentionally differs from TC/cMaNGOS to match AC's even/odd chest-loot grouping.
static constexpr uint32 summonChestSpell[2][4] =
{
go->ReplaceAllGameObjectFlags((GameObjectFlags)0);
go->SetLootRecipient(me->GetMap());
}
// 0 Elder, 1 Elder, 2 Elder, 3 Elder
{ 62957, 62955, 62953, 62950 }, // 10-man
{ 62958, 62956, 62954, 62952 } // 25-man
};
me->CastSpell(me, summonChestSpell[me->GetMap()->Is25ManRaid() ? 1 : 0][_elderCount], true);
// Defeat credit
me->CastSpell(me, 65074, true); // credit
@@ -65,7 +65,6 @@ enum VezaxNpcs
{
// NPC_VEZAX = 33271,
// NPC_VEZAX_BUNNY = 33500,
NPC_SARONITE_ANIMUS = 33524,
};
enum VezaxGOs
@@ -368,11 +367,7 @@ struct npc_ulduar_saronite_animus : public ScriptedAI
npc_ulduar_saronite_animus(Creature* creature) : ScriptedAI(creature)
{
_instance = creature->GetInstanceScript();
if (_instance)
if (Creature* vezax = _instance->GetCreature(BOSS_VEZAX))
vezax->AI()->JustSummoned(me);
timer = 0;
me->SetInCombatWithZone();
}
InstanceScript* _instance;
@@ -389,7 +384,8 @@ struct npc_ulduar_saronite_animus : public ScriptedAI
void UpdateAI(uint32 diff) override
{
UpdateVictim();
if (!UpdateVictim())
return;
timer += diff;
if (timer >= 2000)
@@ -47,6 +47,9 @@ enum SpellData
SPELL_MINE_EXPLOSION = 66351,
SPELL_SUMMON_PROXIMITY_MINE = 65347,
// PHASE 1 -> 2 TRANSITION:
SPELL_ELEVATOR_KNOCKBACK = 65096, // Self-cast by the world trigger; sweeps players off the elevator as it rises
// PHASE 2:
SPELL_HEAT_WAVE = 64533,
@@ -98,6 +101,7 @@ enum NPCs
NPC_ASSAULT_BOT = 34057,
NPC_JUNK_BOT = 33855,
NPC_MAGNETIC_CORE = 34068,
NPC_WORLD_TRIGGER = 21252,
};
enum GOs
@@ -488,6 +492,8 @@ struct boss_mimiron : public BossAI
elevator->SetLootState(GO_READY);
elevator->UseDoorOrButton(0, false);
elevator->EnableCollision(false);
if (Creature* trigger = me->SummonCreature(NPC_WORLD_TRIGGER, elevator->GetPositionX(), elevator->GetPositionY(), elevator->GetPositionZ(), 0.0f, TEMPSUMMON_TIMED_DESPAWN, 5000))
trigger->CastSpell(trigger, SPELL_ELEVATOR_KNOCKBACK);
}
events.ScheduleEvent(EVENT_ELEVATOR_INTERVAL_1, 6s);
break;
@@ -752,10 +758,12 @@ struct boss_mimiron : public BossAI
// spawn chest
if (uint32 chestId = (_hardmode ? RAID_MODE(GO_MIMIRON_CHEST_HARD, GO_MIMIRON_CHEST_HERO_HARD) : RAID_MODE(GO_MIMIRON_CHEST, GO_MIMIRON_CHEST_HERO)))
{
if (GameObject* go = me->SummonGameObject(chestId, 2744.65f, 2569.46f, 364.397f, 0, 0, 0, 0, 0, 0))
// Summoned by the map, not Mimiron, so the chest survives his despawn during the outro.
if (GameObject* go = me->GetMap()->SummonGameObject(chestId, 2744.65f, 2569.46f, 364.397f, 0, 0, 0, 0, 0, 0))
{
go->ReplaceAllGameObjectFlags((GameObjectFlags)0);
go->SetLootRecipient(me->GetMap());
go->SetRespawnTime(7 * DAY);
}
}
events.ScheduleEvent(EVENT_DISAPPEAR, 9s);
@@ -780,6 +788,9 @@ struct boss_mimiron : public BossAI
void EnterEvadeMode(EvadeReason why) override
{
// Once Mimiron turns friendly for the defeat RP, don't reset the encounter.
if (me->GetFaction() == FACTION_FRIENDLY)
return;
if (_isEvading)
return;
_isEvading = true;
File diff suppressed because it is too large Load Diff
@@ -88,6 +88,7 @@ ObjectData const creatureData[] =
{ NPC_IGNIS, BOSS_IGNIS },
{ NPC_RAZORSCALE, BOSS_RAZORSCALE },
{ NPC_XT002, BOSS_XT002 },
{ NPC_HEART_OF_DECONSTRUCTOR,DATA_XT002_HEART },
{ NPC_KOLOGARN, BOSS_KOLOGARN },
{ NPC_AURIAYA, BOSS_AURIAYA },
{ NPC_MIMIRON, BOSS_MIMIRON },
@@ -164,6 +165,12 @@ ObjectData const gameobjectData[] =
{ 0, 0 }
};
ObjectData const summonData[] =
{
{ NPC_SARONITE_ANIMUS, BOSS_VEZAX }, // summoned by a Saronite Vapor, not Vezax
{ 0, 0 }
};
BossBoundaryData const boundaries =
{
{ BOSS_LEVIATHAN, new RectangleBoundary(130.0f, 450.0f, -170.0f, 110.0f) },
@@ -188,6 +195,7 @@ public:
SetPersistentDataCount(MAX_PERSISTENT_DATA);
LoadDoorData(doorData);
LoadObjectData(creatureData, gameobjectData);
LoadSummonData(summonData);
LoadBossBoundaries(boundaries);
Initialize();
};
@@ -87,6 +87,7 @@ enum UlduarData
// XT-002
DATA_XT002_DOORS = 400,
DATA_XT002_HEART = 401,
// Kologarn
DATA_KOLOGARN_DOORS = 410,
@@ -158,6 +159,9 @@ enum UlduarNPCs
NPC_IGNIS = 33118,
NPC_RAZORSCALE = 33186,
NPC_XT002 = 33293,
NPC_XT_TOY_PILE = 33337,
NPC_XS013_SCRAPBOT = 33343,
NPC_HEART_OF_DECONSTRUCTOR = 33329,
NPC_STEELBREAKER = 32867,
NPC_MOLGEIM = 32927,
NPC_BRUNDIR = 32857,
@@ -168,6 +172,7 @@ enum UlduarNPCs
NPC_THORIM = 32865,
NPC_FREYA = 32906,
NPC_VEZAX = 33271,
NPC_SARONITE_ANIMUS = 33524,
NPC_SARA = 33134,
NPC_YOGGSARON = 33288,
NPC_BRAIN_OF_YOGG_SARON = 33890,
@@ -229,8 +234,6 @@ enum UlduarGameObjects
GO_HODIR_CHEST_NORMAL_HERO = 194308,
GO_HODIR_CHEST_HARD = 194200,
GO_HODIR_CHEST_HARD_HERO = 194201,
GO_FREYA_CHEST = 194330, // Normal, -2 - elder offset
GO_FREYA_CHEST_HERO = 194331, // Hero, -2 - elder offset
GO_MIMIRON_CHEST = 194789,
GO_MIMIRON_CHEST_HARD = 194957,
GO_MIMIRON_CHEST_HERO = 194956,
+32 -9
View File
@@ -36,7 +36,9 @@ enum MageSpells
SPELL_SUMMON_MIRROR_IMAGE1 = 58831,
SPELL_SUMMON_MIRROR_IMAGE2 = 58833,
SPELL_SUMMON_MIRROR_IMAGE3 = 58834,
SPELL_SUMMON_MIRROR_IMAGE_GLYPH = 65047
SPELL_SUMMON_MIRROR_IMAGE_GLYPH = 65047,
SPELL_MAGE_MIRROR_IMAGE_FROSTBOLT = 59638,
SPELL_MAGE_MIRROR_IMAGE_FIRE_BLAST = 59637
};
class DeathEvent : public BasicEvent
@@ -202,24 +204,45 @@ struct npc_pet_mage_mirror_image : CasterAI
return;
}
checktarget += diff;
// A dead target, or one we lost sight of, is invalid: drop it and reselect.
// CanSeeOrDetect is comparatively expensive, so throttle the sight check to ~1s.
bool lostTarget = !me->GetVictim()->IsAlive();
checktarget += diff;
if (checktarget >= 1000)
{
if (!me->GetVictim()->IsAlive() || me->GetVictim()->HasBreakableByDamageCrowdControlAura() || !me->CanSeeOrDetect(me->GetVictim()))
{
MySelectNextTarget();
me->InterruptNonMeleeSpells(true);
return;
}
checktarget = 0;
if (!me->CanSeeOrDetect(me->GetVictim()))
lostTarget = true;
}
if (lostTarget)
{
MySelectNextTarget();
me->InterruptNonMeleeSpells(false);
return;
}
// A cast already in progress when the crowd control lands is allowed to finish (3.1.2),
// except a Frostbolt on a target that is now Polymorphed: 3.2.0 cancels that cast so it
// cannot break the Polymorph. Other breakable CC keeps the "let it finish" behaviour.
if (me->HasUnitState(UNIT_STATE_CASTING))
{
if (me->GetVictim()->HasAuraWithMechanic(1ULL << MECHANIC_POLYMORPH)
&& me->FindCurrentSpellBySpellId(SPELL_MAGE_MIRROR_IMAGE_FROSTBOLT))
me->InterruptNonMeleeSpells(false, SPELL_MAGE_MIRROR_IMAGE_FROSTBOLT);
return;
}
// Never start a new cast on a target under a breakable-by-damage CC aura (Polymorph,
// Dragon's Breath, ...) - that is what would break the crowd control.
if (me->GetVictim()->HasBreakableByDamageCrowdControlAura(me))
return;
if (uint32 spellId = events.ExecuteEvent())
{
events.RescheduleEvent(spellId, spellId == 59637 ? 6500ms : 2500ms);
events.RescheduleEvent(spellId, spellId == SPELL_MAGE_MIRROR_IMAGE_FIRE_BLAST ? 6500ms : 2500ms);
me->CastSpell(me->GetVictim(), spellId, false);
}
}