diff --git a/.gitignore b/.gitignore index 05c48bbf1..c4686369e 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,10 @@ local.properties /vcpkg/ /vcpkg-ports/ +# Python bytecode cache +__pycache__/ +*.py[cod] + # ================== # diff --git a/apps/codestyle/codestyle-sql.py b/apps/codestyle/codestyle-sql.py index eb36033a1..c9c9556d6 100644 --- a/apps/codestyle/codestyle-sql.py +++ b/apps/codestyle/codestyle-sql.py @@ -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: diff --git a/data/sql/updates/db_world/2026_07_04_00.sql b/data/sql/updates/db_world/2026_07_04_00.sql new file mode 100644 index 000000000..fbfe949c3 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_04_00.sql @@ -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); diff --git a/data/sql/updates/db_world/2026_07_04_01.sql b/data/sql/updates/db_world/2026_07_04_01.sql new file mode 100644 index 000000000..2c5aa87bd --- /dev/null +++ b/data/sql/updates/db_world/2026_07_04_01.sql @@ -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.', 'Идёт битва за Ледяную Грудь. Плановое обслуживание сервера отложено.'); diff --git a/data/sql/updates/db_world/2026_07_04_02.sql b/data/sql/updates/db_world/2026_07_04_02.sql new file mode 100644 index 000000000..05555ba75 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_04_02.sql @@ -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.'); diff --git a/data/sql/updates/db_world/2026_07_04_03.sql b/data/sql/updates/db_world/2026_07_04_03.sql new file mode 100644 index 000000000..20c5be2a3 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_04_03.sql @@ -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); diff --git a/data/sql/updates/db_world/2026_07_04_04.sql b/data/sql/updates/db_world/2026_07_04_04.sql new file mode 100644 index 000000000..cc033eb52 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_04_04.sql @@ -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'; diff --git a/data/sql/updates/db_world/2026_07_04_05.sql b/data/sql/updates/db_world/2026_07_04_05.sql new file mode 100644 index 000000000..688481cbf --- /dev/null +++ b/data/sql/updates/db_world/2026_07_04_05.sql @@ -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)); diff --git a/data/sql/updates/db_world/2026_07_04_06.sql b/data/sql/updates/db_world/2026_07_04_06.sql new file mode 100644 index 000000000..ea5663e28 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_04_06.sql @@ -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 ({}):', 'Предметы заданий ({}):'); diff --git a/data/sql/updates/db_world/2026_07_05_00.sql b/data/sql/updates/db_world/2026_07_05_00.sql new file mode 100644 index 000000000..afbb48d66 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_05_00.sql @@ -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; diff --git a/data/sql/updates/db_world/2026_07_05_01.sql b/data/sql/updates/db_world/2026_07_05_01.sql new file mode 100644 index 000000000..4658d5005 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_05_01.sql @@ -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; diff --git a/data/sql/updates/db_world/2026_07_05_02.sql b/data/sql/updates/db_world/2026_07_05_02.sql new file mode 100644 index 000000000..a771ac5e7 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_05_02.sql @@ -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)); diff --git a/data/sql/updates/db_world/2026_07_06_00.sql b/data/sql/updates/db_world/2026_07_06_00.sql new file mode 100644 index 000000000..33bcc0bf0 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_06_00.sql @@ -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); diff --git a/data/sql/updates/db_world/2026_07_11_00.sql b/data/sql/updates/db_world/2026_07_11_00.sql new file mode 100644 index 000000000..0ecf9828d --- /dev/null +++ b/data/sql/updates/db_world/2026_07_11_00.sql @@ -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; diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 6f0e0a9f8..2dbe0e423 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -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 + # ################################################################################################### diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index 1b205efb7..db6677223 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -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); diff --git a/src/server/game/AI/CoreAI/UnitAI.cpp b/src/server/game/AI/CoreAI/UnitAI.cpp index dad8adebe..d08958a94 100644 --- a/src/server/game/AI/CoreAI/UnitAI.cpp +++ b/src/server/game/AI/CoreAI/UnitAI.cpp @@ -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(); diff --git a/src/server/game/AI/SmartScripts/SmartAI.cpp b/src/server/game/AI/SmartScripts/SmartAI.cpp index eec86a422..23d7466f5 100644 --- a/src/server/game/AI/SmartScripts/SmartAI.cpp +++ b/src/server/game/AI/SmartScripts/SmartAI.cpp @@ -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) diff --git a/src/server/game/AI/SmartScripts/SmartScriptMgr.h b/src/server/game/AI/SmartScripts/SmartScriptMgr.h index a8ae1cc9f..04804584c 100644 --- a/src/server/game/AI/SmartScripts/SmartScriptMgr.h +++ b/src/server/game/AI/SmartScripts/SmartScriptMgr.h @@ -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, // diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.cpp b/src/server/game/Battlegrounds/BattlegroundMgr.cpp index 3c2411f7f..c03323e4b 100644 --- a/src/server/game/Battlegrounds/BattlegroundMgr.cpp +++ b/src/server/game/Battlegrounds/BattlegroundMgr.cpp @@ -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) diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.h b/src/server/game/Battlegrounds/BattlegroundMgr.h index 313bee8f0..50d5cc27a 100644 --- a/src/server/game/Battlegrounds/BattlegroundMgr.h +++ b/src/server/game/Battlegrounds/BattlegroundMgr.h @@ -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]; } diff --git a/src/server/game/Battlegrounds/BattlegroundQueue.cpp b/src/server/game/Battlegrounds/BattlegroundQueue.cpp index 169f642ec..1fac5ad54 100644 --- a/src/server/game/Battlegrounds/BattlegroundQueue.cpp +++ b/src/server/game/Battlegrounds/BattlegroundQueue.cpp @@ -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(); diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp index 647284322..6651c7a01 100644 --- a/src/server/game/DungeonFinding/LFGMgr.cpp +++ b/src/server/game/DungeonFinding/LFGMgr.cpp @@ -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; } diff --git a/src/server/game/DungeonFinding/LFGMgr.h b/src/server/game/DungeonFinding/LFGMgr.h index fe1a7852a..74d5af06f 100644 --- a/src/server/game/DungeonFinding/LFGMgr.h +++ b/src/server/game/DungeonFinding/LFGMgr.h @@ -20,6 +20,7 @@ #include +#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 diff --git a/src/server/game/DungeonFinding/LFGQueue.cpp b/src/server/game/DungeonFinding/LFGQueue.cpp index 3935bc43f..7cb286d39 100644 --- a/src/server/game/DungeonFinding/LFGQueue.cpp +++ b/src/server/game/DungeonFinding/LFGQueue.cpp @@ -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()) diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 0c1e8b75f..df4785545 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -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; } diff --git a/src/server/game/Entities/Object/Object.cpp b/src/server/game/Entities/Object/Object.cpp index 8a0eebffc..7ff045e0b 100644 --- a/src/server/game/Entities/Object/Object.cpp +++ b/src/server/game/Entities/Object/Object.cpp @@ -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; diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 9d30451a8..f53e3b05d 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -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 diff --git a/src/server/game/Grids/Notifiers/GridNotifiers.cpp b/src/server/game/Grids/Notifiers/GridNotifiers.cpp index 28f87f286..e38c8030c 100644 --- a/src/server/game/Grids/Notifiers/GridNotifiers.cpp +++ b/src/server/game/Grids/Notifiers/GridNotifiers.cpp @@ -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) { diff --git a/src/server/game/Grids/Notifiers/GridNotifiers.h b/src/server/game/Grids/Notifiers/GridNotifiers.h index 8484eb3b5..1f078197f 100644 --- a/src/server/game/Grids/Notifiers/GridNotifiers.h +++ b/src/server/game/Grids/Notifiers/GridNotifiers.h @@ -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 void Visit(GridRefMgr&) {} void Visit(CreatureMapType&); + void Visit(PlayerMapType&); }; enum class TeamFilter diff --git a/src/server/game/Handlers/BattleGroundHandler.cpp b/src/server/game/Handlers/BattleGroundHandler.cpp index 414be5d64..9051801e5 100644 --- a/src/server/game/Handlers/BattleGroundHandler.cpp +++ b/src/server/game/Handlers/BattleGroundHandler.cpp @@ -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); } diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index 2f34b1c55..d7dca9bb4 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -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); diff --git a/src/server/game/Handlers/MovementHandler.cpp b/src/server/game/Handlers/MovementHandler.cpp index 3bca8bbfc..46140f5a4 100644 --- a/src/server/game/Handlers/MovementHandler.cpp +++ b/src/server/game/Handlers/MovementHandler.cpp @@ -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(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; diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 20bd52792..ef158770b 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -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 diff --git a/src/server/game/Movement/MotionMaster.cpp b/src/server/game/Movement/MotionMaster.cpp index aacb78458..f704f23a7 100644 --- a/src/server/game/Movement/MotionMaster.cpp +++ b/src/server/game/Movement/MotionMaster.cpp @@ -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 diff --git a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp index c40d5a0d0..27c2cc45b 100644 --- a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp +++ b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp @@ -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 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) diff --git a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.h b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.h index 12479820a..66fd14162 100644 --- a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.h +++ b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.h @@ -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*); diff --git a/src/server/game/Spells/SpellInfoCorrections.cpp b/src/server/game/Spells/SpellInfoCorrections.cpp index ea20e8202..f5e22058c 100644 --- a/src/server/game/Spells/SpellInfoCorrections.cpp +++ b/src/server/game/Spells/SpellInfoCorrections.cpp @@ -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; }); diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 33f596d7a..7d22bee97 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -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) { diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index 24eb42afa..736fb8f38 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -245,6 +245,7 @@ public: protected: void _UpdateGameTime(); + bool RescheduleShutdownForWintergrasp(); // callback for UpdateRealmCharacters void _UpdateRealmCharCount(PreparedQueryResult resultCharCount,uint32 accountId); diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 7f6b51e98..5233d6e48 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -477,6 +477,7 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_OFFHAND_CHECK_AT_SPELL_UNLEARN, "OffhandCheckAtSpellUnlearn", true); SetConfigValue(CONFIG_CREATURE_REPOSITION_AGAINST_NPCS, "Creature.RepositionAgainstNpcs", true); + SetConfigValue(CONFIG_CREATURE_INSTANCE_TELEPORT_TO_UNREACHABLE_TARGET, "Creature.Instance.TeleportToUnreachableTarget", false); SetConfigValue(CONFIG_CREATURE_STOP_FOR_PLAYER, "Creature.MovingStopTimeForPlayer", 180000); SetConfigValue(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(CONFIG_WINTERGRASP_SKIP_BATTLE_SESSION_COUNT, "Wintergrasp.SkipBattleSessionCount", 3500); SetConfigValue(CONFIG_WINTERGRASP_KICK_VOA_PLAYERS, "Wintergrasp.KickVoAPlayers", true, ConfigValueCache::Reloadable::No); SetConfigValue(CONFIG_WINTERGRASP_ESSENCE_BOTH_FACTIONS, "Wintergrasp.EssenceBothFactions", false); + SetConfigValue(CONFIG_WINTERGRASP_DEFER_SHUTDOWN, "Wintergrasp.DeferShutdownTimer", 0); SetConfigValue(CONFIG_BIRTHDAY_TIME, "BirthdayTime", 1222964635); SetConfigValue(CONFIG_MINIGOB_MANABONK, "Minigob.Manabonk.Enable", true); @@ -663,8 +665,6 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_PORTAL_CHECK_ILVL, "DungeonAccessRequirements.PortalAvgIlevelCheck", false); SetConfigValue(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_LFG_DBC_LEVEL_OVERRIDE, "DungeonAccessRequirements.LFGLevelDBCOverride", false); SetConfigValue(CONFIG_DUNGEON_ACCESS_REQUIREMENTS_OPTIONAL_STRING_ID, "DungeonAccessRequirements.OptionalStringID", 0); - SetConfigValue(CONFIG_NPC_EVADE_IF_NOT_REACHABLE, "NpcEvadeIfTargetIsUnreachable", 5); - SetConfigValue(CONFIG_NPC_REGEN_TIME_IF_NOT_REACHABLE_IN_RAID, "NpcRegenHPTimeIfTargetIsUnreachable", 10); SetConfigValue(CONFIG_REGEN_HP_CANNOT_REACH_TARGET_IN_RAID, "NpcRegenHPIfTargetIsUnreachable", true); //Debug diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index bdeddd3a1..7e3ef1319 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -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, diff --git a/src/server/scripts/Commands/cs_mail.cpp b/src/server/scripts/Commands/cs_mail.cpp index 6f622e2b3..3fcf3a2c8 100644 --- a/src/server/scripts/Commands/cs_mail.cpp +++ b/src/server/scripts/Commands/cs_mail.cpp @@ -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> 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 itemEntry = itemFields[12].Get(); + + // 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() + ? ObjectGuid::Create(itemFields[13].Get()) + : 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 itemEntry = itemFields[12].Get(); - - // 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() - ? ObjectGuid::Create(itemFields[13].Get()) - : 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)); diff --git a/src/server/scripts/Commands/cs_misc.cpp b/src/server/scripts/Commands/cs_misc.cpp index 6b66f6f27..456628d03 100644 --- a/src/server/scripts/Commands/cs_misc.cpp +++ b/src/server/scripts/Commands/cs_misc.cpp @@ -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."); diff --git a/src/server/scripts/Commands/cs_npc.cpp b/src/server/scripts/Commands/cs_npc.cpp index 0616f12b7..28da07703 100644 --- a/src/server/scripts/Commands/cs_npc.cpp +++ b/src/server/scripts/Commands/cs_npc.cpp @@ -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() diff --git a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp b/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp index bcc48a812..d22350896 100644 --- a/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp +++ b/src/server/scripts/EasternKingdoms/ScarletEnclave/chapter1.cpp @@ -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(); diff --git a/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp b/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp index 38afc8509..ce575923c 100644 --- a/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp +++ b/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp @@ -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& 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 diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp index 7e2eb3f7a..c0a63ed30 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_assembly_of_iron.cpp @@ -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: diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp index 7193e43e1..6ed905267 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp @@ -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(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 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 targetList; - Acore::WorldObjectSpellAreaTargetCheck check(99, GetExplTargetDest(), GetCaster(), GetCaster(), GetSpellInfo(), TARGET_CHECK_DEFAULT, nullptr); - Acore::WorldObjectListSearcher searcher(GetCaster(), targetList, check); - Cell::VisitObjects(GetCaster(), searcher, 99.0f); - float minDist = 99 * 99; - Unit* target = nullptr; - for (std::list::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); diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp index ab460a785..216aa247b 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp @@ -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 diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp index 849576a15..de0677dd4 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp @@ -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) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp index 0eba106d7..c0f979d77 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp @@ -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; diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_xt002.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_xt002.cpp index d6260a114..75820d461 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_xt002.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_xt002.cpp @@ -16,259 +16,337 @@ */ #include "AchievementCriteriaScript.h" +#include "Containers.h" #include "CreatureScript.h" +#include "InstanceScript.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" #include "Opcodes.h" #include "PassiveAI.h" #include "Player.h" #include "ScriptedCreature.h" #include "SpellAuraEffects.h" +#include "SpellMgr.h" #include "SpellScript.h" #include "SpellScriptLoader.h" -#include "Vehicle.h" #include "ulduar.h" +#include "Vehicle.h" +#include "WorldPacket.h" -enum XT002Spells +enum Spells { - // BASIC - SPELL_GRAVITY_BOMB = 63024, - SPELL_SEARING_LIGHT = 63018, - SPELL_TYMPANIC_TANTARUM = 62776, - SPELL_XT002_ENRAGE = 26662, + SPELL_TYMPANIC_TANTRUM = 62776, + SPELL_SEARING_LIGHT = 63018, + SPELL_SUMMON_LIFE_SPARK = 64210, + SPELL_SUMMON_VOID_ZONE = 64203, + SPELL_GRAVITY_BOMB = 63024, + SPELL_HEARTBREAK = 65737, + SPELL_STAND = 37752, + SPELL_SUBMERGE = 37751, + SPELL_ENRAGE = 26662, + SPELL_COOLDOWN_CREATURE_SPECIAL_2 = 64404, + SPELL_SCRAP_REPAIR = 62832, - // HELPERS - SPELL_ARCING_SMASH = 8374, - SPELL_TRAMPLE = 5568, - SPELL_UPPERCUT = 10966, - SPELL_BOOM = 62834, + // XT-Toy Pile + SPELL_RECHARGE_PUMMELER = 62831, + SPELL_RECHARGE_SCRAPBOT = 62828, + SPELL_RECHARGE_BOOMBOT = 62835, - // HEARTBREAK - SPELL_HEART_OVERLOAD = 62789, - SPELL_EXPOSED_HEART = 63849, - SPELL_ENERGY_ORB = 62790, - SPELL_ENERGY_ORB_TRIGGER = 62826, - SPELL_HEARTBREAK = 65737, + // Heart of the Deconstructor + SPELL_ENERGY_ORB = 62790, + SPELL_ENERGY_ORB_TRIGGERED = 62826, + SPELL_RIDE_VEHICLE_EXPOSED = 63313, + SPELL_EXPOSED_HEART = 63849, + SPELL_HEART_RIDE_VEHICLE = 63852, + SPELL_SCRAPBOT_RIDE_VEHICLE = 47020, + SPELL_FULL_HEAL = 17683, + SPELL_HEART_OVERLOAD = 62789, + SPELL_HEART_OVERLOAD_TRIGGER = 62791, + SPELL_HEART_LIGHTNING_TETHER = 64799, - // VOID ZONE - SPELL_VOID_ZONE_SUMMON = 64203, - SPELL_VOID_ZONE_DAMAGE = 64208, + // Void Zone + SPELL_CONSUMPTION = 64209, - // SPARK - SPELL_SPARK_SUMMON = 64210, - SPELL_SPARK_DAMAGE = 64227, - SPELL_SPARK_MELEE = 64230, + // Life Spark + SPELL_ARCANE_POWER_STATE = 49411, + SPELL_STATIC_CHARGED = 64227, + SPELL_SHOCK = 64230, - // ACHIEVEMENT - SPELL_ACHIEVEMENT_CREDIT_NERF_SCRAPBOTS = 65037, + // XM-024 Pummeller + SPELL_ARCING_SMASH = 8374, + SPELL_TRAMPLE = 5568, + SPELL_UPPERCUT = 10966, + + //Boombot + SPELL_321_BOOMBOT_AURA = 65032, + SPELL_BOOM = 62834, + + // Achievement-related spells + SPELL_ACHIEVEMENT_CREDIT_NERF_SCRAPBOTS = 65037 }; -enum XT002Events +enum Events { - EVENT_HEALTH_CHECK = 1, - EVENT_GRAVITY_BOMB = 2, - EVENT_SEARING_LIGHT = 3, - EVENT_ENRAGE = 4, - EVENT_TYMPANIC_TANTARUM = 5, - EVENT_RESTORE = 6, - EVENT_START_SECOND_PHASE = 7, - EVENT_REMOVE_EMOTE = 8, - EVENT_CHECK_ROOM = 9, + EVENT_TYMPANIC_TANTRUM = 1, + EVENT_PHASE_CHECK, + EVENT_SEARING_LIGHT, + EVENT_GRAVITY_BOMB, + EVENT_SUBMERGE, + EVENT_DISPOSE_HEART, + EVENT_ENRAGE, + EVENT_ENTER_HARD_MODE, + EVENT_RESUME_ATTACK }; -enum NPCs +enum XT002Phases { - NPC_VOID_ZONE = 34001, - NPC_LIFE_SPARK = 34004, - NPC_XT002_HEART = 33329, - NPC_XS013_SCRAPBOT = 33343, - NPC_XM024_PUMMELLER = 33344, - NPC_XE321_BOOMBOT = 33346, - NPC_PILE_TRIGGER = 33337, + PHASE_1 = 1, + PHASE_HEART }; -enum Texts +enum Actions { - SAY_AGGRO = 0, - SAY_HEART_OPENED = 1, - SAY_HEART_CLOSED = 2, - SAY_TYMPANIC_TANTRUM = 3, - SAY_SLAY = 4, - SAY_BERSERK = 5, - SAY_DEATH = 6, - SAY_SUMMON = 7, - EMOTE_HEART_OPENED = 8, - EMOTE_HEART_CLOSED = 9, - EMOTE_TYMPANIC_TANTRUM = 10, - EMOTE_SCRAPBOT = 11, + ACTION_ENTER_HARD_MODE, + ACTION_START_PHASE_HEART, + ACTION_DISPOSE_HEART +}; + +enum XT002Data +{ + DATA_TRANSFERED_HEALTH, + DATA_HARD_MODE, + DATA_HEALTH_RECOVERED, + DATA_GRAVITY_BOMB_CASUALTY +}; + +enum Yells +{ + SAY_AGGRO = 0, + SAY_HEART_OPENED = 1, + SAY_HEART_CLOSED = 2, + SAY_TYMPANIC_TANTRUM = 3, + SAY_SLAY = 4, + SAY_BERSERK = 5, + SAY_DEATH = 6, + SAY_SUMMON = 7, + EMOTE_HEART_OPENED = 8, + EMOTE_HEART_CLOSED = 9, + EMOTE_TYMPANIC_TANTRUM = 10, + EMOTE_SCRAPBOT = 11 }; enum Misc { - HEART_VEHICLE_SEAT = 0, - - ACTION_AWAKEN_HEART = -5, - ACTION_HIDE_HEART = -4, - ACTION_HEART_BROKEN = -3, - - ACHIEVEMENT_MUST_DECONSTRUCT_FASTER = 21027, - - DATA_XT002_NERF_ENGINEERING = 50, - DATA_XT002_GRAVITY_ACHIEV = 51, + ACHIEV_MUST_DECONSTRUCT_FASTER = 21027, + HEART_VEHICLE_SEAT_EXPOSED = 1, + GROUP_SEARING_GRAVITY = 1 }; struct boss_xt002 : public BossAI { - boss_xt002(Creature* pCreature) : BossAI(pCreature, BOSS_XT002) { } - - uint8 _healthCheck; - bool _hardMode; - bool _nerfAchievement; - bool _gravityAchievement; - - void RescheduleEvents() + boss_xt002(Creature* creature) : BossAI(creature, BOSS_XT002) { - events.RescheduleEvent(EVENT_GRAVITY_BOMB, 1s, 1); - events.RescheduleEvent(EVENT_TYMPANIC_TANTARUM, 1min, 1); - if (!_hardMode) - events.RescheduleEvent(EVENT_HEALTH_CHECK, 2s, 1); + Initialize(); + } + + void Initialize() + { + _healthRecovered = false; + _gravityBombCasualty = false; + _hardMode = false; + _exposeHeartPercent = 75; + } + + void ChangeNextExpose() + { + switch (_exposeHeartPercent) + { + case 75: + _exposeHeartPercent = 50; + break; + case 50: + _exposeHeartPercent = 25; + break; + default: + _exposeHeartPercent = 0; + break; + } } void Reset() override { _Reset(); - - me->ResetLootMode(); - me->RemoveAllAuras(); - - // first heart expose - _healthCheck = 75; - _hardMode = false; - _nerfAchievement = true; - _gravityAchievement = true; - - me->SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, UNIT_STAND_STATE_STAND); // emerge - me->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - me->SetControlled(false, UNIT_STATE_STUNNED); - - if (instance) - { - instance->DoStopTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEVEMENT_MUST_DECONSTRUCT_FASTER); - if (GameObject* pGo = instance->GetGameObject(DATA_XT002_DOORS)) - pGo->SetGoState(GO_STATE_ACTIVE); - } + DoCastSelf(SPELL_STAND); + me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + events.SetPhase(PHASE_1); + me->SetReactState(REACT_AGGRESSIVE); + me->SetNoCallForHelp(true); // Skip pulling nearby + Initialize(); + instance->DoStopTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEV_MUST_DECONSTRUCT_FASTER); } - void AttachHeart() + void EnterEvadeMode(EvadeReason /*why*/) override { - if (Unit* heart = me->GetVehicleKit() ? me->GetVehicleKit()->GetPassenger(HEART_VEHICLE_SEAT) : nullptr) - heart->SetHealth(heart->GetMaxHealth()); - else if (Creature* accessory = me->SummonCreature(NPC_XT002_HEART, *me, TEMPSUMMON_MANUAL_DESPAWN)) - { - accessory->AddUnitTypeMask(UNIT_MASK_ACCESSORY); - if (!me->HandleSpellClick(accessory, 0)) - accessory->DespawnOrUnsummon(); - } + summons.DespawnAll(); + BossAI::EnterEvadeMode(); } - void JustReachedHome() override + void JustEngagedWith(Unit* who) override { - _JustReachedHome(); - me->setActive(false); - } - - void JustEngagedWith(Unit*) override - { - me->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); - events.ScheduleEvent(EVENT_ENRAGE, 10min, 0, 0); - events.ScheduleEvent(EVENT_CHECK_ROOM, 5s, 0, 0); - RescheduleEvents(); // Other events are scheduled here - - me->setActive(true); Talk(SAY_AGGRO); + BossAI::JustEngagedWith(who); + events.ScheduleEvent(EVENT_SEARING_LIGHT, Is25ManRaid() ? 9s : 11s, GROUP_SEARING_GRAVITY, PHASE_1); + events.ScheduleEvent(EVENT_GRAVITY_BOMB, Is25ManRaid() ? 18s : 21s, GROUP_SEARING_GRAVITY, PHASE_1); + events.ScheduleEvent(EVENT_ENRAGE, 10min); + events.ScheduleEvent(EVENT_TYMPANIC_TANTRUM, 60s, 0, PHASE_1); + events.ScheduleEvent(EVENT_PHASE_CHECK, 1s, 0, PHASE_1); + instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEV_MUST_DECONSTRUCT_FASTER); - if (instance) - { - instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEVEMENT_MUST_DECONSTRUCT_FASTER); - instance->SetBossState(BOSS_XT002, IN_PROGRESS); - if (GameObject* pGo = instance->GetGameObject(DATA_XT002_DOORS)) - pGo->SetGoState(GO_STATE_READY); - } - - me->CallForHelp(175); - me->SetInCombatWithZone(); - AttachHeart(); + // AC core difference: Vehicle::InstallAccessory despawns the heart if HandleSpellClick + // returns false (TC uses async VehicleJoinEvent and does not check return value). + // Ensure the heart exists as a fallback. + if (!instance->GetCreature(DATA_XT002_HEART)) + if (Creature* heart = me->SummonCreature(NPC_HEART_OF_DECONSTRUCTOR, *me, TEMPSUMMON_MANUAL_DESPAWN)) + me->HandleSpellClick(heart, 0); } - void KilledUnit(Unit* victim) override + void DoAction(int32 action) override { - if (victim->IsPlayer() && !urand(0, 2)) - { + if (action == ACTION_ENTER_HARD_MODE) + events.ScheduleEvent(EVENT_ENTER_HARD_MODE, 1ms); + } + + void KilledUnit(Unit* who) override + { + if (who->IsPlayer()) Talk(SAY_SLAY); - } } void JustDied(Unit* /*killer*/) override { Talk(SAY_DEATH); _JustDied(); - - if (instance) - { - if (GameObject* pGo = instance->GetGameObject(DATA_XT002_DOORS)) - pGo->SetGoState(GO_STATE_ACTIVE); - } + me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); } - void DoAction(int32 param) override + void ExposeHeart() { - if (param == DATA_XT002_NERF_ENGINEERING) + events.SetPhase(PHASE_HEART); + me->SetReactState(REACT_PASSIVE); + me->AttackStop(); + Talk(SAY_HEART_OPENED); + events.CancelEvent(EVENT_TYMPANIC_TANTRUM); + events.ScheduleEvent(EVENT_SUBMERGE, 6s, 0, PHASE_HEART); + ChangeNextExpose(); + } + + void DisposeHeart(bool isHardMode = false) + { + Talk(SAY_HEART_CLOSED); + + if (isHardMode) { - _nerfAchievement = false; - return; + me->SetReactState(REACT_AGGRESSIVE); + RescheduleEvents(); } - if (param == DATA_XT002_GRAVITY_ACHIEV) + else { - _gravityAchievement = false; - return; - } - - if (!me->IsAlive() || _hardMode) - return; - - // heart destory - if (param == ACTION_HEART_BROKEN) - { - _hardMode = true; - me->SetLootMode(3); // hard mode + normal loot - me->SetMaxHealth(me->GetMaxHealth()); - me->SetHealth(me->GetMaxHealth()); - me->SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, UNIT_STAND_STATE_STAND); // emerge - - me->CastSpell(me, SPELL_HEARTBREAK, true); - Talk(EMOTE_HEART_CLOSED); - events.ScheduleEvent(EVENT_REMOVE_EMOTE, 4s); - return; + events.ScheduleEvent(EVENT_RESUME_ATTACK, 1s, 0, PHASE_HEART); } - // damage from heart - if (param > 0) + DoCastSelf(SPELL_STAND); + DoCastSelf(SPELL_COOLDOWN_CREATURE_SPECIAL_2); + me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + if (Creature* heart = instance->GetCreature(DATA_XT002_HEART)) { - // avoid reducing health under 1 - int32 _final = std::min(param, int32(me->GetHealth() - 1)); + if (heart->IsAlive()) + heart->AI()->DoAction(ACTION_DISPOSE_HEART); + else + heart->DespawnOrUnsummon(); + } - me->ModifyHealth(-_final); - me->LowerPlayerDamageReq(_final); + } + + void RescheduleEvents() + { + events.SetPhase(PHASE_1); + events.ScheduleEvent(EVENT_SEARING_LIGHT, 25s, GROUP_SEARING_GRAVITY, PHASE_1); + events.ScheduleEvent(EVENT_GRAVITY_BOMB, Is25ManRaid() ? 33s : 15s, GROUP_SEARING_GRAVITY, PHASE_1); + events.ScheduleEvent(EVENT_TYMPANIC_TANTRUM, 25s, 0, PHASE_1); + if (!_hardMode) + events.ScheduleEvent(EVENT_PHASE_CHECK, 1s, 0, PHASE_1); + } + + void PassengerBoarded(Unit* who, int8 seatId, bool apply) override + { + if (!apply) + return; + + if (who->GetEntry() == NPC_XS013_SCRAPBOT) + { + Talk(EMOTE_SCRAPBOT); + _healthRecovered = true; + } + else if (seatId == HEART_VEHICLE_SEAT_EXPOSED) + who->CastSpell(who, SPELL_EXPOSED_HEART); // Channeled + } + + void MovementInform(uint32 type, uint32 point) override + { + if (type != WAYPOINT_MOTION_TYPE) + return; + + switch (point) + { + case 3: + case 9: + me->HandleEmoteCommand(EMOTE_STATE_SPELL_CHANNEL_OMNI); + break; + case 13: + me->HandleEmoteCommand(EMOTE_STATE_WORK); + break; + default: + break; } } - uint32 GetData(uint32 param) const override + uint32 GetData(uint32 type) const override { - if (param == DATA_XT002_NERF_ENGINEERING) - return _nerfAchievement; - else if (param == DATA_XT002_GRAVITY_ACHIEV) - return _gravityAchievement; + switch (type) + { + case DATA_HARD_MODE: + return _hardMode ? 1 : 0; + case DATA_HEALTH_RECOVERED: + return _healthRecovered ? 1 : 0; + case DATA_GRAVITY_BOMB_CASUALTY: + return _gravityBombCasualty ? 1 : 0; + default: + return 0; + } + } - return 0; + void SetData(uint32 type, uint32 data) override + { + switch (type) + { + case DATA_TRANSFERED_HEALTH: + if (!_hardMode) + { + uint32 transferHealth = data; + if (transferHealth >= me->GetHealth()) + transferHealth = me->GetHealth() - 1; + + me->ModifyHealth(-static_cast(transferHealth)); + me->LowerPlayerDamageReq(transferHealth); + } + break; + case DATA_GRAVITY_BOMB_CASUALTY: + _gravityBombCasualty = (data > 0) ? true : false; + break; + default: + break; + } } void UpdateAI(uint32 diff) override @@ -277,553 +355,368 @@ struct boss_xt002 : public BossAI return; events.Update(diff); + if (me->HasUnitState(UNIT_STATE_CASTING)) return; - switch (events.ExecuteEvent()) + while (uint32 eventId = events.ExecuteEvent()) { - // Control events - case EVENT_HEALTH_CHECK: - if (_hardMode) - { - return; - } + switch (eventId) + { + case EVENT_SEARING_LIGHT: + DoCastSelf(SPELL_SEARING_LIGHT); + events.Repeat(Is25ManRaid() ? 16s : 20s); + break; + case EVENT_GRAVITY_BOMB: + DoCastSelf(SPELL_GRAVITY_BOMB); + events.Repeat(Is25ManRaid() ? 16s : 20s); + break; + case EVENT_TYMPANIC_TANTRUM: + Talk(SAY_TYMPANIC_TANTRUM); + Talk(EMOTE_TYMPANIC_TANTRUM); + events.DelayEvents(10s, GROUP_SEARING_GRAVITY); + DoCastSelf(SPELL_TYMPANIC_TANTRUM); + events.Repeat(1min); + break; + case EVENT_PHASE_CHECK: + if (me->HealthBelowPct(_exposeHeartPercent)) + ExposeHeart(); + events.Repeat(1s); + break; + case EVENT_SUBMERGE: + DoCastSelf(SPELL_SUBMERGE); + me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + Talk(EMOTE_HEART_OPENED); + if (Creature* heart = instance->GetCreature(DATA_XT002_HEART)) + heart->AI()->DoAction(ACTION_START_PHASE_HEART); + events.ScheduleEvent(EVENT_DISPOSE_HEART, 30s, PHASE_HEART); + break; + case EVENT_DISPOSE_HEART: + DisposeHeart(); + break; + case EVENT_ENRAGE: + Talk(SAY_BERSERK); + DoCastSelf(SPELL_ENRAGE); + break; + case EVENT_ENTER_HARD_MODE: + me->SetFullHealth(); + DoCastSelf(SPELL_HEARTBREAK, true); + me->AddLootMode(LOOT_MODE_HARD_MODE_1); + _hardMode = true; + DisposeHeart(_hardMode); + break; + case EVENT_RESUME_ATTACK: + me->SetReactState(REACT_AGGRESSIVE); + RescheduleEvents(); + break; + default: + break; + } - if (me->HealthBelowPct(_healthCheck)) - { - _healthCheck -= 25; - me->SetControlled(true, UNIT_STATE_STUNNED); - me->SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, UNIT_STAND_STATE_SUBMERGED); // submerge with animation - - Talk(SAY_HEART_OPENED); - - events.CancelEventGroup(1); - events.ScheduleEvent(EVENT_START_SECOND_PHASE, 5s); - return; - } - events.Repeat(1s); - break; - case EVENT_CHECK_ROOM: - events.Repeat(5s); - if (me->GetPositionX() < 722 || me->GetPositionX() > 987 || me->GetPositionY() < -139 || me->GetPositionY() > 124) - EnterEvadeMode(); - - return; - - // Abilities events - case EVENT_GRAVITY_BOMB: - me->CastCustomSpell(SPELL_GRAVITY_BOMB, SPELLVALUE_MAX_TARGETS, 1, me, true); - events.ScheduleEvent(EVENT_SEARING_LIGHT, 10s, 1); - break; - case EVENT_SEARING_LIGHT: - me->CastCustomSpell(SPELL_SEARING_LIGHT, SPELLVALUE_MAX_TARGETS, 1, me, true); - events.ScheduleEvent(EVENT_GRAVITY_BOMB, 10s, 1); - break; - case EVENT_TYMPANIC_TANTARUM: - Talk(EMOTE_TYMPANIC_TANTRUM); - Talk(SAY_TYMPANIC_TANTRUM); - me->CastSpell(me, SPELL_TYMPANIC_TANTARUM, true); - events.Repeat(1min); - return; - case EVENT_ENRAGE: - Talk(SAY_BERSERK); - me->CastSpell(me, SPELL_XT002_ENRAGE, true); - break; - - // Animation events - case EVENT_START_SECOND_PHASE: - Talk(EMOTE_HEART_OPENED); - me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - if (Unit* heart = me->GetVehicleKit() ? me->GetVehicleKit()->GetPassenger(HEART_VEHICLE_SEAT) : nullptr) - heart->GetAI()->DoAction(ACTION_AWAKEN_HEART); - - events.ScheduleEvent(EVENT_RESTORE, 30s); - return; - // Restore from heartbreak - case EVENT_RESTORE: - if (_hardMode) - { - return; - } - - Talk(SAY_HEART_CLOSED); - - me->SetByteValue(UNIT_FIELD_BYTES_1, UNIT_BYTES_1_OFFSET_STAND_STATE, UNIT_STAND_STATE_STAND); // emerge - // Hide heart - if (Unit* heart = me->GetVehicleKit() ? me->GetVehicleKit()->GetPassenger(HEART_VEHICLE_SEAT) : nullptr) - heart->GetAI()->DoAction(ACTION_HIDE_HEART); - - events.ScheduleEvent(EVENT_REMOVE_EMOTE, 4s); - return; - case EVENT_REMOVE_EMOTE: - me->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - me->SetControlled(false, UNIT_STATE_STUNNED); - - RescheduleEvents(); + if (me->HasUnitState(UNIT_STATE_CASTING)) return; } - // Disabled by stunned state - DoMeleeAttackIfReady(); - } -}; - -struct npc_xt002_heart : public PassiveAI -{ - npc_xt002_heart(Creature* pCreature) : PassiveAI(pCreature), summons(me) - { - me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - } - - SummonList summons; - uint32 _damageDone; - uint32 _timerSpawn; - - uint8 _spawnSelection; - uint8 _pummelerCount; - - void MoveInLineOfSight(Unit*) override { } - void AttackStart(Unit*) override { } - void JustSummoned(Creature* cr) override - { - summons.Summon(cr); - if (Unit* owner = me->GetVehicleBase()) - if (owner->IsCreature()) - owner->ToCreature()->AI()->JustSummoned(cr); - } - void DamageTaken(Unit*, uint32& damage, DamageEffectType, SpellSchoolMask) override - { - _damageDone += damage; - } - - void SummonPiles() - { - me->SummonCreature(NPC_PILE_TRIGGER, 893.290f, 66.820f, 409.81f, 4.2f); - me->SummonCreature(NPC_PILE_TRIGGER, 898.099f, -88.9115f, 409.887f, 2.23402f); - me->SummonCreature(NPC_PILE_TRIGGER, 793.096f, -95.158f, 409.887f, 0.855211f); - me->SummonCreature(NPC_PILE_TRIGGER, 794.600f, 59.660f, 409.82f, 5.34f); - } - - void DoAction(int32 param) override - { - if (param == ACTION_AWAKEN_HEART) - { - _pummelerCount = 0; - _spawnSelection = 0; - _damageDone = 0; - _timerSpawn = 0; - me->SetHealth(me->GetMaxHealth()); - me->CastSpell(me, SPELL_HEART_OVERLOAD, true); - me->CastSpell(me, SPELL_EXPOSED_HEART, false); // Channeled - me->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - - if (!summons.HasEntry(NPC_PILE_TRIGGER)) - SummonPiles(); - } - else if (param == ACTION_HIDE_HEART) - { - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - if (pXT002->AI()) - { - pXT002->AI()->DoAction(_damageDone); - _damageDone = 0; - } - me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - } - } - - void SendEnergyToCorner() - { - Unit* pile = nullptr; - uint8 num = urand(1, 4); - for (SummonList::const_iterator itr = summons.begin(); itr != summons.end(); ++itr) - if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr)) - if (summon->GetEntry() == NPC_PILE_TRIGGER) - { - pile = summon; - if ((--num) == 0) - break; - } - - if (pile) - me->CastSpell(pile, SPELL_ENERGY_ORB, true); - } - - void SpellHitTarget(Unit* target, SpellInfo const* spellInfo) override - { - // spawn not-so-random robots - if (spellInfo->Id == SPELL_ENERGY_ORB_TRIGGER && target->GetEntry() == NPC_PILE_TRIGGER) - switch (_spawnSelection) - { - case 0: - for (uint8 i = 0; i < 5; ++i) - me->SummonCreature(NPC_XS013_SCRAPBOT, target->GetPositionX() + irand(-3, 3), target->GetPositionY() + irand(-3, 3), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1000); - _spawnSelection++; - break; - case 1: - me->SummonCreature(NPC_XE321_BOOMBOT, target->GetPositionX(), target->GetPositionY(), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 5000); - _spawnSelection++; - break; - case 2: - for (uint8 i = 0; i < 5; ++i) - me->SummonCreature(NPC_XS013_SCRAPBOT, target->GetPositionX() + irand(-3, 3), target->GetPositionY() + irand(-3, 3), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1000); - _spawnSelection++; - break; - case 3: - if (_pummelerCount < 2) - me->SummonCreature(NPC_XM024_PUMMELLER, target->GetPositionX(), target->GetPositionY(), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 5000); - - _pummelerCount++; - _spawnSelection++; - break; - case 4: - for (uint8 i = 0; i < 5; ++i) - me->SummonCreature(NPC_XS013_SCRAPBOT, target->GetPositionX() + irand(-3, 3), target->GetPositionY() + irand(-3, 3), target->GetPositionZ() + 2, 0, TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1000); - _spawnSelection = 0; - break; - } - } - - void JustDied(Unit* /*killer*/) override - { - me->SetVisible(false); - if (me->GetInstanceScript()) - if (Creature* XT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - if (XT002->AI()) - XT002->AI()->DoAction(ACTION_HEART_BROKEN); - } - - void UpdateAI(uint32 diff) override - { - if (!me->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE)) - { - _timerSpawn += diff; - if (_timerSpawn >= 1900) - { - SendEnergyToCorner(); - _timerSpawn -= 1900; - } - } - } -}; - -struct npc_xt002_scrapbot : public PassiveAI -{ - npc_xt002_scrapbot(Creature* pCreature) : PassiveAI(pCreature) { } - - bool _locked; - void Reset() override - { - me->StopMoving(); - _locked = true; - me->SetWalk(true); - - if (me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - if (pXT002->GetPositionZ() > 411.0f) // he is on stairs... idiot cryness protection - me->GetMotionMaster()->MovePoint(0, 884.028931f, -14.593809f, 409.786987f); - else - _locked = false; - } - } - - void JustDied(Unit* killer) override - { - // Nerf Scrapbots achievement - if (killer && killer->GetEntry() == NPC_XE321_BOOMBOT) - if (me->GetInstanceScript()) - { - me->GetInstanceScript()->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_SPELL_TARGET, 65037); - me->GetInstanceScript()->DoUpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET, 65037); - } - } - - // tc use updateAI, while we have movementinform - void MovementInform(uint32 type, uint32 /*param*/) override - { - if (type == POINT_MOTION_TYPE) - { - _locked = false; - return; - } - - // we reached the target :) - if (type == FOLLOW_MOTION_TYPE && me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - if (pXT002->IsAlive()) - { - pXT002->AI()->DoAction(DATA_XT002_NERF_ENGINEERING); - pXT002->ModifyHealth(pXT002->GetMaxHealth() * 0.01f); - } - - if (!urand(0, 2)) - pXT002->AI()->Talk(EMOTE_SCRAPBOT); - - me->DespawnOrUnsummon(1ms); - } - } - - void UpdateAI(uint32 /*diff*/) override - { - if (!_locked) - { - if (me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - me->GetMotionMaster()->MoveFollow(pXT002, 0.0f, 0.0f); - _locked = true; - } - } - } -}; - -struct npc_xt002_pummeller : public ScriptedAI -{ - npc_xt002_pummeller(Creature* pCreature) : ScriptedAI(pCreature) { } - - int32 _arcingSmashTimer; - int32 _trampleTimer; - int32 _uppercutTimer; - - void Reset() override - { - _arcingSmashTimer = 0; - _trampleTimer = 0; - _uppercutTimer = 0; - - if (Unit* target = SelectTargetFromPlayerList(200)) - AttackStart(target); - else - me->DespawnOrUnsummon(500ms); - } - - void UpdateAI(uint32 diff) override - { - if (!UpdateVictim()) - return; - - _arcingSmashTimer += diff; - _trampleTimer += diff; - _uppercutTimer += diff; - - if (_arcingSmashTimer >= 8000) - { - me->CastSpell(me->GetVictim(), SPELL_ARCING_SMASH, false); - _arcingSmashTimer = 0; - return; - } - if (_trampleTimer >= 11000) - { - me->CastSpell(me->GetVictim(), SPELL_TRAMPLE, false); - _trampleTimer = 0; - return; - } - if (_uppercutTimer >= 14000) - { - me->CastSpell(me->GetVictim(), SPELL_UPPERCUT, false); - _uppercutTimer = 0; - return; - } - - DoMeleeAttackIfReady(); - } -}; - -class BoomEvent : public BasicEvent -{ -public: - BoomEvent(Creature* me) : _me(me) - { - } - - bool Execute(uint64 /*time*/, uint32 /*diff*/) override - { - // This hack is here because we suspect our implementation of spell effect execution on targets - // is done in the wrong order. We suspect that EFFECT_0 needs to be applied on all targets, - // then EFFECT_1, etc - instead of applying each effect on target1, then target2, etc. - // The above situation causes the visual for this spell to be bugged, so we remove the instakill - // effect and implement a script hack for that. - - _me->CastSpell(_me, SPELL_BOOM, false); - return true; + if (events.IsInPhase(PHASE_1)) + DoMeleeAttackIfReady(); } private: - Creature* _me; + bool _healthRecovered; // Did a scrapbot recover XT-002's health during the encounter? + bool _hardMode; // Are we in hard mode? Or: was the heart killed during phase 2? + bool _gravityBombCasualty; // Did someone die because of Gravity Bomb damage? + uint8 _exposeHeartPercent; }; -struct npc_xt002_boombot : public PassiveAI +struct npc_xt002_heart : public NullCreatureAI { - npc_xt002_boombot(Creature* pCreature) : PassiveAI(pCreature) { } + explicit npc_xt002_heart(Creature* creature) : NullCreatureAI(creature), _instance(creature->GetInstanceScript()) { } - bool _locked; - bool _boomed; void Reset() override { - me->StopMoving(); - _locked = true; - _boomed = false; - me->SetUnitMovementFlags(MOVEMENTFLAG_WALKING); - - if (me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - if (pXT002->GetPositionZ() > 411.0f) // he is on stairs... idiot cryness protection - me->GetMotionMaster()->MovePoint(0, 884.028931f, -14.593809f, 409.786987f); - else - _locked = false; - } + me->SetRegeneratingHealth(false); } - void Explode() + void DoAction(int32 action) override { - if (_boomed) + Creature* xt002 = _instance->GetCreature(BOSS_XT002); + if (!xt002) return; - _boomed = true; // Prevent recursive calls - - WorldPacket data(SMSG_SPELLINSTAKILLLOG, 8 + 8 + 4); - data << me->GetGUID(); - data << me->GetGUID(); - data << uint32(SPELL_BOOM); - me->SendMessageToSet(&data, false); - - me->KillSelf(); - - // Visual only seems to work if the instant kill event is delayed or the spell itself is delayed - // Casting done from player and caster source has the same targetinfo flags, - // so that can't be the issue - // See BoomEvent class - // Schedule 1s delayed - me->m_Events.AddEventAtOffset(new BoomEvent(me), 1s); + if (action == ACTION_START_PHASE_HEART) + { + DoCastSelf(SPELL_FULL_HEAL); + DoCast(xt002, SPELL_RIDE_VEHICLE_EXPOSED, true); + DoCastSelf(SPELL_HEART_OVERLOAD, true); + me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + me->SetUnitFlag(UNIT_FLAG_PREVENT_EMOTES_FROM_CHAT_TEXT); + } + else if (action == ACTION_DISPOSE_HEART) + { + DoCast(xt002, SPELL_HEART_RIDE_VEHICLE, true); + me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + me->RemoveUnitFlag(UNIT_FLAG_PREVENT_EMOTES_FROM_CHAT_TEXT); + } } void JustDied(Unit* /*killer*/) override { - me->m_Events.AddEventAtOffset(new BoomEvent(me), 1s); + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->AI()->DoAction(ACTION_ENTER_HARD_MODE); } - void DamageTaken(Unit*, uint32& damage, DamageEffectType, SpellSchoolMask) override - { - if (_boomed) - damage = 0; - - if (me->HealthBelowPctDamaged(50, damage) && !_boomed) - { - damage = 0; - Explode(); - } - } - - // tc they use updateAI, while we have movementinform - void MovementInform(uint32 type, uint32 /*param*/) override - { - if (type == POINT_MOTION_TYPE) - { - _locked = false; - return; - } - // we reached the target :) - //if (type == FOLLOW_MOTION_TYPE) - // _kill = true; - } - - void UpdateAI(uint32 /*diff*/) override - { - if (!_locked) - { - if (me->GetInstanceScript()) - if (Creature* pXT002 = me->GetInstanceScript()->GetCreature(BOSS_XT002)) - { - me->GetMotionMaster()->MoveFollow(pXT002, 0.0f, 0.0f); - _locked = true; - } - } - } +private: + InstanceScript * _instance; }; -struct npc_xt002_life_spark : public ScriptedAI +struct npc_scrapbot : public ScriptedAI { - npc_xt002_life_spark(Creature* pCreature) : ScriptedAI(pCreature) - { - me->SetMaxHealth(RAID_MODE(54000, 172000)); - me->SetHealth(me->GetMaxHealth()); - me->CastSpell(me, SPELL_SPARK_DAMAGE, true); - } + npc_scrapbot(Creature* creature) : ScriptedAI(creature), _instance(creature->GetInstanceScript()) { } - uint32 _attackTimer; void Reset() override { - if (Unit* target = SelectTargetFromPlayerList(200)) - AttackStart(target); - else + me->SetReactState(REACT_PASSIVE); + _scheduler.CancelAll(); + + if (_instance->GetBossState(BOSS_XT002) != IN_PROGRESS) + { me->DespawnOrUnsummon(); + return; + } + + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->AI()->JustSummoned(me); + + _scheduler. + Schedule(2s, [this](TaskContext /*StartMove*/) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + me->GetMotionMaster()->MoveFollow(xt002, 0.0f, 0.0f); + }) + .Schedule(1s, [this](TaskContext checkXt002) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + { + if (me->IsWithinMeleeRange(xt002)) + { + DoCast(xt002, SPELL_SCRAPBOT_RIDE_VEHICLE); + _scheduler.Schedule(1s, [this](TaskContext /*ScrapRepair*/) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->CastSpell(me, SPELL_SCRAP_REPAIR, true); + me->DespawnOrUnsummon(1s); + }); + } + else + checkXt002.Repeat(); + } + else + me->DespawnOrUnsummon(); + }); } - void UpdateAI(uint32 /*diff*/) override + void UpdateAI(uint32 diff) override + { + _scheduler.Update(diff); + } + +private: + InstanceScript* _instance; + TaskScheduler _scheduler; +}; + +struct npc_pummeller : public ScriptedAI +{ + npc_pummeller(Creature* creature) : ScriptedAI(creature), _instance(creature->GetInstanceScript()) { } + + void Reset() override + { + me->SetReactState(REACT_PASSIVE); + _scheduler.CancelAll(); + + if (_instance->GetBossState(BOSS_XT002) != IN_PROGRESS) + { + me->DespawnOrUnsummon(); + return; + } + + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->AI()->JustSummoned(me); + + _scheduler. + Schedule(1s, [this](TaskContext /*StartMove*/) + { + me->SetReactState(REACT_AGGRESSIVE); + DoZoneInCombat(); + }) + .Schedule(17s, [this](TaskContext trample) + { + DoCastSelf(SPELL_TRAMPLE); + trample.Repeat(11s); + }) + .Schedule(19s, [this](TaskContext arcingSmash) + { + DoCastSelf(SPELL_ARCING_SMASH); + arcingSmash.Repeat(8s); + }) + .Schedule(19s, [this](TaskContext upperCut) + { + DoCastVictim(SPELL_UPPERCUT); + upperCut.Repeat(14s); + }); + + } + + void UpdateAI(uint32 diff) override { if (!UpdateVictim()) return; - me->CastSpell(me->GetVictim(), SPELL_SPARK_MELEE, false); - DoMeleeAttackIfReady(); + _scheduler.Update(diff, [this] + { + DoMeleeAttackIfReady(); + }); } + +private: + InstanceScript* _instance; + TaskScheduler _scheduler; }; -// 62775 - Tympanic Tantrum -class spell_xt002_tympanic_tantrum : public SpellScript +struct npc_boombot : public ScriptedAI { - PrepareSpellScript(spell_xt002_tympanic_tantrum); + npc_boombot(Creature* creature) : ScriptedAI(creature), _instance(creature->GetInstanceScript()), _boomed(false) { } - void FilterTargets(std::list& targets) + void Reset() override { - targets.remove_if(PlayerOrPetCheck()); + DoCastSelf(SPELL_321_BOOMBOT_AURA); + me->SetReactState(REACT_PASSIVE); + _scheduler.CancelAll(); + + if (_instance->GetBossState(BOSS_XT002) != IN_PROGRESS) + { + me->DespawnOrUnsummon(); + return; + } + + // HACK/workaround: + // these values aren't confirmed - lack of data - and the values in DB are incorrect + // these values are needed for correct damage of Boom spell + me->SetFloatValue(UNIT_FIELD_MINDAMAGE, 15000.0f); + me->SetFloatValue(UNIT_FIELD_MAXDAMAGE, 18000.0f); + + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + xt002->AI()->JustSummoned(me); + + _scheduler. + Schedule(4s, [this](TaskContext /*StartMove*/) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + me->GetMotionMaster()->MoveFollow(xt002, 0.0f, 0.0f); + + }) + .Schedule(1s, [this](TaskContext checkXt002) + { + if (Creature* xt002 = _instance->GetCreature(BOSS_XT002)) + { + if (me->IsWithinMeleeRange(xt002)) + DoCastAOE(SPELL_BOOM); + else + checkXt002.Repeat(); + } + else + me->DespawnOrUnsummon(); + }); } - void RecalculateDamage() + void DamageTaken(Unit* /*attacker*/, uint32& damage, DamageEffectType /*dmgType*/, SpellSchoolMask /*school*/) override { - if (GetHitUnit()) - SetHitDamage(GetHitUnit()->CountPctFromMaxHealth(GetHitDamage())); + if (damage >= (me->GetHealth() - me->GetMaxHealth() * 0.5f) && !_boomed) + { + _boomed = true; // Prevent recursive call + damage = 0; + DoCastAOE(SPELL_BOOM); + } } - void Register() override + void UpdateAI(uint32 diff) override { - OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_tympanic_tantrum::FilterTargets, EFFECT_ALL, TARGET_UNIT_SRC_AREA_ENEMY); - OnHit += SpellHitFn(spell_xt002_tympanic_tantrum::RecalculateDamage); + _scheduler.Update(diff); } + +private: + InstanceScript* _instance; + bool _boomed; + TaskScheduler _scheduler; }; -// 64234, 63024 - Gravity Bomb -enum GravityBomb +struct npc_life_spark : public ScriptedAI { - SPELL_GRAVITY_BOMB_TRIGGER_10 = 63025 -}; + npc_life_spark(Creature* creature) : ScriptedAI(creature) { } -class spell_xt002_gravity_bomb : public SpellScript -{ - PrepareSpellScript(spell_xt002_gravity_bomb); - - void SelectTarget(std::list& targets) + void Reset() override { - if (Unit* victim = GetCaster()->GetVictim()) - targets.remove_if(Acore::ObjectGUIDCheck(victim->GetGUID(), true)); + DoCastSelf(SPELL_ARCANE_POWER_STATE); + _scheduler.CancelAll(); } - void Register() override + void JustEngagedWith(Unit* /*who*/) override { - OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_gravity_bomb::SelectTarget, EFFECT_ALL, TARGET_UNIT_DEST_AREA_ENEMY); + DoCastSelf(SPELL_STATIC_CHARGED); + _scheduler.Schedule(12s, [this](TaskContext spellShock) + { + DoCastVictim(SPELL_SHOCK); + spellShock.Repeat(); + }); } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim()) + return; + + if (me->HasUnitState(UNIT_STATE_CASTING)) + return; + + _scheduler.Update(diff, [this] + { + DoMeleeAttackIfReady(); + }); + } + +private: + TaskScheduler _scheduler; }; -class spell_xt002_gravity_bomb_aura : public AuraScript +struct npc_xt_void_zone : public PassiveAI { - PrepareAuraScript(spell_xt002_gravity_bomb_aura); + npc_xt_void_zone(Creature* creature) : PassiveAI(creature) { } - bool Validate(SpellInfo const* /*spellInfo*/) override + void Reset() override { - return ValidateSpellInfo({ SPELL_VOID_ZONE_DAMAGE }); + _scheduler.Schedule(2500ms, [this](TaskContext /*task*/) + { + DoCastSelf(SPELL_CONSUMPTION); + }); + } + + void UpdateAI(uint32 diff) override + { + _scheduler.Update(diff); + } + +private: + TaskScheduler _scheduler; +}; + +// 63018, 65121 - Searing Light +class spell_xt002_searing_light_spawn_life_spark : public AuraScript +{ + PrepareAuraScript(spell_xt002_searing_light_spawn_life_spark); + + bool Validate(SpellInfo const* /*spell*/) override + { + return ValidateSpellInfo({ SPELL_SUMMON_LIFE_SPARK }); } void OnRemove(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) @@ -831,26 +724,42 @@ class spell_xt002_gravity_bomb_aura : public AuraScript if (Player* player = GetOwner()->ToPlayer()) if (Unit* xt002 = GetCaster()) if (xt002->HasAura(aurEff->GetAmount())) // Heartbreak aura indicating hard mode - if (Creature* creature = xt002->SummonCreature(NPC_VOID_ZONE, player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(), 0, TEMPSUMMON_TIMED_DESPAWN, 180000)) - { - int32 damage = GetSpellInfo()->Id == SPELL_GRAVITY_BOMB_TRIGGER_10 ? 5000 : 7500; - creature->CastCustomSpell(creature, SPELL_VOID_ZONE_DAMAGE, &damage, 0, 0, true); - } + xt002->CastSpell(player, SPELL_SUMMON_LIFE_SPARK, true); + } + + void Register() override + { + AfterEffectRemove += AuraEffectRemoveFn(spell_xt002_searing_light_spawn_life_spark::OnRemove, EFFECT_0, SPELL_AURA_PERIODIC_TRIGGER_SPELL, AURA_EFFECT_HANDLE_REAL); + } +}; + +// 63024, 64234 - Gravity Bomb +class spell_xt002_gravity_bomb_aura : public AuraScript +{ + PrepareAuraScript(spell_xt002_gravity_bomb_aura); + + bool Validate(SpellInfo const* /*spell*/) override + { + return ValidateSpellInfo({ SPELL_SUMMON_VOID_ZONE }); + } + + void OnRemove(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) + { + if (Player* player = GetOwner()->ToPlayer()) + if (Unit* xt002 = GetCaster()) + if (xt002->HasAura(aurEff->GetAmount())) // Heartbreak aura indicating hard mode + xt002->CastSpell(player, SPELL_SUMMON_VOID_ZONE, true); } void OnPeriodic(AuraEffect const* aurEff) { Unit* xt002 = GetCaster(); + Unit* owner = GetTarget(); if (!xt002) return; - Unit* owner = GetOwner()->ToUnit(); - if (!owner) - return; - if (aurEff->GetAmount() >= int32(owner->GetHealth())) - if (xt002->GetAI()) - xt002->GetAI()->DoAction(DATA_XT002_GRAVITY_ACHIEV); + xt002->GetAI()->SetData(DATA_GRAVITY_BOMB_CASUALTY, 1); } void Register() override @@ -860,20 +769,17 @@ class spell_xt002_gravity_bomb_aura : public AuraScript } }; -// 64233, 63025 - Gravity Bomb +// 63025, 64233 - Gravity Bomb (Damage) class spell_xt002_gravity_bomb_damage : public SpellScript { PrepareSpellScript(spell_xt002_gravity_bomb_damage); - void HandleScript(SpellEffIndex /*eff*/) + void HandleScript(SpellEffIndex /*effIndex*/) { - Unit* caster = GetCaster(); - if (!caster) - return; - if (GetHitDamage() >= int32(GetHitUnit()->GetHealth())) - if (caster->GetAI()) - caster->GetAI()->DoAction(DATA_XT002_GRAVITY_ACHIEV); + if (InstanceScript* instance = GetCaster()->GetInstanceScript()) + if (Creature* xt002 = instance->GetCreature(BOSS_XT002)) + xt002->AI()->SetData(DATA_GRAVITY_BOMB_CASUALTY, 1); } void Register() override @@ -882,74 +788,119 @@ class spell_xt002_gravity_bomb_damage : public SpellScript } }; -// 63018, 65121 - Searing Light -class spell_xt002_searing_light_spawn_life_spark : public SpellScript +// 62791 - XT-002 Heart Overload Trigger Spell (server-side) +// Fires an Energy Orb (62790) at a random XT-Toy Pile. +// The distance check for whether to summon adds happens in spell_xt002_energy_orb (62826). +class spell_xt002_heart_overload_periodic : public SpellScript { - PrepareSpellScript(spell_xt002_searing_light_spawn_life_spark); + PrepareSpellScript(spell_xt002_heart_overload_periodic); - void SelectTarget(std::list& targets) + bool Validate(SpellInfo const* /*spell*/) override { - if (Unit* victim = GetCaster()->GetVictim()) - targets.remove_if(Acore::ObjectGUIDCheck(victim->GetGUID(), true)); + return ValidateSpellInfo({SPELL_ENERGY_ORB, SPELL_HEART_LIGHTNING_TETHER}); + } + + static constexpr float ToyPileSearchDistance = 250.0f; + + static Creature* GetRandomToyPile(Unit const* caster) + { + std::list targets; + caster->GetCreatureListWithEntryInGrid(targets, NPC_XT_TOY_PILE, ToyPileSearchDistance); + + if (targets.empty()) + return nullptr; + + return Acore::Containers::SelectRandomContainerElement(targets); + } + + void HandleScript(SpellEffIndex /*effIndex*/) + { + Unit* caster = GetCaster(); + caster->CastSpell((Unit*)nullptr, SPELL_HEART_LIGHTNING_TETHER, true); + if (Creature* toyPile = GetRandomToyPile(caster)) + caster->CastSpell(toyPile, SPELL_ENERGY_ORB, true); } void Register() override { - OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_searing_light_spawn_life_spark::SelectTarget, EFFECT_ALL, TARGET_UNIT_DEST_AREA_ENEMY); + OnEffectHit += SpellEffectFn(spell_xt002_heart_overload_periodic::HandleScript, EFFECT_0, SPELL_EFFECT_DUMMY); } }; -class spell_xt002_searing_light_spawn_life_spark_aura : public AuraScript +struct npc_xt_toy_pile : public ScriptedAI { - PrepareAuraScript(spell_xt002_searing_light_spawn_life_spark_aura); - - void OnRemove(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/) + explicit npc_xt_toy_pile(Creature* creature) : ScriptedAI(creature) { - if (Player* player = GetOwner()->ToPlayer()) - if (Unit* xt002 = GetCaster()) - if (xt002->HasAura(aurEff->GetAmount())) // Heartbreak aura indicating hard mode - xt002->SummonCreature(NPC_LIFE_SPARK, player->GetPositionX(), player->GetPositionY(), player->GetPositionZ(), 0, TEMPSUMMON_TIMED_DESPAWN, 180000); + _lastSummonTime = 0; + } + + static constexpr float SummonDistance = 90.0f; // large enough so XT can be tanked at the door + static constexpr uint32 SummonCooldown = 12 * IN_MILLISECONDS; // verified with sniff + + void SpellHit(Unit* caster, SpellInfo const* spell) override + { + if (spell->Id != SPELL_ENERGY_ORB_TRIGGERED) + return; + + Creature* xt002 = caster ? caster->GetVehicleCreatureBase() : nullptr; + if (!xt002 || xt002->IsWithinDist(me, SummonDistance)) + return; + + uint32 now = getMSTime(); + if (now - _lastSummonTime < SummonCooldown) + return; + + _lastSummonTime = now; + + DoCastSelf(SPELL_RECHARGE_BOOMBOT, true); + + if (roll_chance_i(30)) + DoCastSelf(SPELL_RECHARGE_PUMMELER, true); + + uint8 const summonCount = urand(5, 7); + for (uint8 i = 0; i < summonCount; ++i) + DoCastSelf(SPELL_RECHARGE_SCRAPBOT, true); + + xt002->AI()->Talk(SAY_SUMMON); + } + +private: + uint32 _lastSummonTime{}; +}; + +// 62775 - Tympanic Tantrum +class spell_xt002_tympanic_tantrum : public SpellScript +{ + PrepareSpellScript(spell_xt002_tympanic_tantrum); + + void FilterTargets(std::list& targets) + { + targets.remove_if([](WorldObject* object) -> bool + { + if (object->IsPlayer()) + return false; + + if (Creature* creature = object->ToCreature()) + return !creature->IsPet(); + + return true; + }); + } + + void RecalculateDamage() + { + SetHitDamage(GetHitUnit()->CountPctFromMaxHealth(GetHitDamage())); } void Register() override { - OnEffectRemove += AuraEffectRemoveFn(spell_xt002_searing_light_spawn_life_spark_aura::OnRemove, EFFECT_0, SPELL_AURA_PERIODIC_TRIGGER_SPELL, AURA_EFFECT_HANDLE_REAL); + OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_tympanic_tantrum::FilterTargets, EFFECT_0, TARGET_UNIT_SRC_AREA_ENEMY); + OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_xt002_tympanic_tantrum::FilterTargets, EFFECT_1, TARGET_UNIT_SRC_AREA_ENEMY); + OnHit += SpellHitFn(spell_xt002_tympanic_tantrum::RecalculateDamage); } }; -class achievement_xt002_nerf_engineering : public AchievementCriteriaScript -{ -public: - achievement_xt002_nerf_engineering() : AchievementCriteriaScript("achievement_xt002_nerf_engineering") {} - - bool OnCheck(Player* /*player*/, Unit* target, uint32 /*criteria_id*/) override - { - if (target) - if (InstanceScript* instance = target->GetInstanceScript()) - if (Creature* cr = instance->GetCreature(BOSS_XT002)) - return cr->AI()->GetData(DATA_XT002_NERF_ENGINEERING); - - return false; - } -}; - -class achievement_xt002_nerf_gravity_bombs : public AchievementCriteriaScript -{ -public: - achievement_xt002_nerf_gravity_bombs() : AchievementCriteriaScript("achievement_xt002_nerf_gravity_bombs") {} - - bool OnCheck(Player* /*player*/, Unit* target, uint32 /*criteria_id*/) override - { - if (target) - if (InstanceScript* instance = target->GetInstanceScript()) - if (Creature* cr = instance->GetCreature(BOSS_XT002)) - return cr->AI()->GetData(DATA_XT002_GRAVITY_ACHIEV); - - return false; - } -}; - -// 65032 - 321 Boombot Aura +// 65032 - 321-Boombot Aura class spell_xt002_321_boombot_aura : public AuraScript { PrepareAuraScript(spell_xt002_321_boombot_aura); @@ -979,24 +930,144 @@ class spell_xt002_321_boombot_aura : public AuraScript } }; +// 63849 - Exposed Heart +// Transfers damage dealt to the Heart to XT-002 on every hit. +// Also fires Energy Orb missiles at Toy Piles on damage taken, with a 1s cooldown. +class spell_xt002_exposed_heart : public AuraScript +{ + PrepareAuraScript(spell_xt002_exposed_heart); + + bool Validate(SpellInfo const* /*spell*/) override + { + return ValidateSpellInfo({ SPELL_HEART_OVERLOAD_TRIGGER }); + } + + static constexpr uint32 OrbCooldown = 1500; + + void OnProc(AuraEffect const* /*aurEff*/, ProcEventInfo& eventInfo) + { + PreventDefaultAction(); + DamageInfo* damageInfo = eventInfo.GetDamageInfo(); + if (!damageInfo || !damageInfo->GetDamage()) + return; + + if (Creature* xt002 = GetTarget()->GetVehicleCreatureBase()) + xt002->AI()->SetData(DATA_TRANSFERED_HEALTH, damageInfo->GetDamage()); + + uint32 now = getMSTime(); + if (now - _lastOrbTime >= OrbCooldown) + { + _lastOrbTime = now; + if (Unit* caster = GetCaster()) + caster->CastSpell(caster, SPELL_HEART_OVERLOAD_TRIGGER, true); + } + } + + void Register() override + { + OnEffectProc += AuraEffectProcFn(spell_xt002_exposed_heart::OnProc, EFFECT_0, SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN); + } + +private: + uint32 _lastOrbTime{}; +}; + +// 37751 - Submerged +class spell_xt002_submerged : public SpellScript +{ + PrepareSpellScript(spell_xt002_submerged); + + void HandleScript(SpellEffIndex /*eff*/) + { + if (Creature* target = GetHitCreature()) + target->SetStandState(UNIT_STAND_STATE_SUBMERGED); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_xt002_submerged::HandleScript, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +// 37752 - Stand +class spell_xt002_stand : public SpellScript +{ + PrepareSpellScript(spell_xt002_stand); + + void HandleScript(SpellEffIndex /*eff*/) + { + if (Creature* target = GetHitCreature()) + target->SetStandState(UNIT_STAND_STATE_STAND); + } + + void Register() override + { + OnEffectHitTarget += SpellEffectFn(spell_xt002_stand::HandleScript, EFFECT_0, SPELL_EFFECT_SCRIPT_EFFECT); + } +}; + +class achievement_nerf_engineering : public AchievementCriteriaScript +{ + public: + achievement_nerf_engineering() : AchievementCriteriaScript("achievement_nerf_engineering") { } + + bool OnCheck(Player* /*source*/, Unit* target, uint32 /*criteria_id*/) override + { + if (!target || !target->GetAI()) + return false; + + return !(target->GetAI()->GetData(DATA_HEALTH_RECOVERED)); + } +}; + +class achievement_heartbreaker : public AchievementCriteriaScript +{ + public: + achievement_heartbreaker() : AchievementCriteriaScript("achievement_heartbreaker") { } + + bool OnCheck(Player* /*source*/, Unit* target, uint32 /*criteria_id*/) override + { + if (!target || !target->GetAI()) + return false; + + return target->GetAI()->GetData(DATA_HARD_MODE) != 0; + } +}; + +class achievement_nerf_gravity_bombs : public AchievementCriteriaScript +{ + public: + achievement_nerf_gravity_bombs() : AchievementCriteriaScript("achievement_nerf_gravity_bombs") { } + + bool OnCheck(Player* /*source*/, Unit* target, uint32 /*criteria_id*/) override + { + if (!target || !target->GetAI()) + return false; + + return !(target->GetAI()->GetData(DATA_GRAVITY_BOMB_CASUALTY)); + } +}; + void AddSC_boss_xt002() { - // Npcs RegisterUlduarCreatureAI(boss_xt002); RegisterUlduarCreatureAI(npc_xt002_heart); - RegisterUlduarCreatureAI(npc_xt002_scrapbot); - RegisterUlduarCreatureAI(npc_xt002_pummeller); - RegisterUlduarCreatureAI(npc_xt002_boombot); - RegisterUlduarCreatureAI(npc_xt002_life_spark); - - // Spells - RegisterSpellScript(spell_xt002_tympanic_tantrum); - RegisterSpellAndAuraScriptPair(spell_xt002_gravity_bomb, spell_xt002_gravity_bomb_aura); + RegisterUlduarCreatureAI(npc_scrapbot); + RegisterUlduarCreatureAI(npc_pummeller); + RegisterUlduarCreatureAI(npc_boombot); + RegisterUlduarCreatureAI(npc_life_spark); + RegisterUlduarCreatureAI(npc_xt_void_zone); + RegisterSpellScript(spell_xt002_searing_light_spawn_life_spark); + RegisterSpellScript(spell_xt002_gravity_bomb_aura); RegisterSpellScript(spell_xt002_gravity_bomb_damage); - RegisterSpellAndAuraScriptPair(spell_xt002_searing_light_spawn_life_spark, spell_xt002_searing_light_spawn_life_spark_aura); + RegisterSpellScript(spell_xt002_heart_overload_periodic); + RegisterUlduarCreatureAI(npc_xt_toy_pile); + RegisterSpellScript(spell_xt002_tympanic_tantrum); RegisterSpellScript(spell_xt002_321_boombot_aura); - - // Achievements - new achievement_xt002_nerf_engineering(); - new achievement_xt002_nerf_gravity_bombs(); + RegisterSpellScript(spell_xt002_exposed_heart); + RegisterSpellScript(spell_xt002_submerged); + RegisterSpellScript(spell_xt002_stand); + new achievement_nerf_engineering(); + new achievement_heartbreaker(); + new achievement_nerf_gravity_bombs(); } diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp index 5712fe72f..9daa556ff 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp @@ -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(); }; diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h index eb50851c8..faaf83d21 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h @@ -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, diff --git a/src/server/scripts/Pet/pet_mage.cpp b/src/server/scripts/Pet/pet_mage.cpp index 6ccf7673f..e3852eeb3 100644 --- a/src/server/scripts/Pet/pet_mage.cpp +++ b/src/server/scripts/Pet/pet_mage.cpp @@ -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); } }