diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index c8105eb83a..3610c759e9 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -89,6 +89,7 @@ SET(common_sources skills.cpp skill_caps.cpp spdat.cpp + spdat_bot.cpp strings.cpp struct_strategy.cpp textures.cpp diff --git a/common/classes.h b/common/classes.h index 96e5cad263..dc586ceebf 100644 --- a/common/classes.h +++ b/common/classes.h @@ -131,6 +131,8 @@ static std::map class_names = { #define ARMOR_TYPE_LAST ARMOR_TYPE_PLATE #define ARMOR_TYPE_COUNT 5 +#define BOT_CLASS_BASE_ID_PREFIX 3000 + const char* GetClassIDName(uint8 class_id, uint8 level = 0); diff --git a/common/database/database_update_manifest_bots.cpp b/common/database/database_update_manifest_bots.cpp index 48c9aa7f85..db1e269cf3 100644 --- a/common/database/database_update_manifest_bots.cpp +++ b/common/database/database_update_manifest_bots.cpp @@ -161,6 +161,1951 @@ ADD COLUMN `extra_haste` mediumint(8) NOT NULL DEFAULT 0 AFTER `wis`; .sql = R"( ALTER TABLE `bot_spells_entries` CHANGE COLUMN `spellid` `spell_id` smallint(5) UNSIGNED NOT NULL DEFAULT 0 AFTER `npc_spells_id`; +)" + }, + ManifestEntry{ + .version = 9046, + .description = "2024_05_18_bot_settings.sql", + .check = "SHOW TABLES LIKE 'bot_settings'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE `bot_settings` ( + `character_id` INT(10) UNSIGNED NOT NULL, + `bot_id` INT(10) UNSIGNED NOT NULL, + `stance` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', + `setting_id` SMALLINT(5) UNSIGNED NOT NULL DEFAULT '0', + `setting_type` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', + `value` INT(10) UNSIGNED NOT NULL, + `category_name` VARCHAR(64) NULL DEFAULT '' COLLATE 'utf8mb4_general_ci', + `setting_name` VARCHAR(64) NULL DEFAULT '' COLLATE 'utf8mb4_general_ci', + PRIMARY KEY (`character_id`, `bot_id`, `stance`, `setting_id`, `setting_type`) USING BTREE +) +COLLATE='utf8mb4_general_ci' +ENGINE=InnoDB +; + +INSERT INTO bot_settings SELECT 0, bd.`bot_id`, (SELECT bs.`stance_id` FROM bot_stances bs WHERE bs.`bot_id` = bd.`bot_id`) AS stance_id, 0, 0, bd.`expansion_bitmask`, 'BaseSetting', 'ExpansionBitmask' FROM bot_data bd +JOIN rule_values rv +WHERE rv.rule_name LIKE 'Bots:BotExpansionSettings' +AND bd.expansion_bitmask != rv.rule_value; + +INSERT INTO bot_settings SELECT 0, `bot_id`, (SELECT bs.`stance_id` FROM bot_stances bs WHERE bs.`bot_id` = `bot_id`) AS stance_id, 1, 0, `show_helm`, 'BaseSetting', 'ShowHelm' FROM bot_data WHERE `show_helm` != 1; +INSERT INTO bot_settings SELECT 0, `bot_id`, (SELECT bs.`stance_id` FROM bot_stances bs WHERE bs.`bot_id` = `bot_id`) AS stance_id, 2, 0, sqrt(`follow_distance`), 'BaseSetting', 'FollowDistance' FROM bot_data WHERE `follow_distance` != 184; + +INSERT INTO bot_settings +SELECT 0, `bot_id`, (SELECT bs.`stance_id` FROM bot_stances bs WHERE bs.`bot_id` = `bot_id`) AS stance_id, 3, 0, `stop_melee_level`, 'BaseSetting', 'StopMeleeLevel' +FROM ( + SELECT `bot_id`, + (CASE + WHEN (`class` IN (2, 6, 10, 11, 12, 13, 14)) THEN 13 + ELSE 255 + END) AS `sml`, + `stop_melee_level` + FROM bot_data +) AS `subquery` +WHERE `sml` != `stop_melee_level`; + +INSERT INTO bot_settings SELECT 0, `bot_id`, (SELECT bs.`stance_id` FROM bot_stances bs WHERE bs.`bot_id` = `bot_id`) AS stance_id, 4, 0, `enforce_spell_settings`, 'BaseSetting', 'EnforceSpellSettings' FROM bot_data WHERE `enforce_spell_settings` != 0; +INSERT INTO bot_settings SELECT 0, `bot_id`, (SELECT bs.`stance_id` FROM bot_stances bs WHERE bs.`bot_id` = `bot_id`) AS stance_id, 5, 0, `archery_setting`, 'BaseSetting', 'RangedSetting' FROM bot_data WHERE `archery_setting` != 0; + +INSERT INTO bot_settings +SELECT 0, `bot_id`, (SELECT bs.`stance_id` FROM bot_stances bs WHERE bs.`bot_id` = `bot_id`) AS stance_id, 8, 0, `caster_range`, 'BaseSetting', 'DistanceRanged' +FROM ( + SELECT `bot_id`, + (CASE + WHEN (`class` IN (1, 7, 19, 16)) THEN 0 + WHEN `class` = 8 THEN 0 + ELSE 90 + END) AS `DistanceRanged`, + `caster_range` + FROM bot_data +) AS `subquery` +WHERE `DistanceRanged` != `caster_range`; + +ALTER TABLE `bot_data` + DROP COLUMN `show_helm`; +ALTER TABLE `bot_data` + DROP COLUMN `follow_distance`; +ALTER TABLE `bot_data` + DROP COLUMN `stop_melee_level`; +ALTER TABLE `bot_data` + DROP COLUMN `expansion_bitmask`; +ALTER TABLE `bot_data` + DROP COLUMN `enforce_spell_settings`; +ALTER TABLE `bot_data` + DROP COLUMN `archery_setting`; +ALTER TABLE `bot_data` + DROP COLUMN `caster_range`; + +UPDATE `bot_command_settings` SET `aliases`= 'bh' WHERE `bot_command`='behindmob'; +UPDATE `bot_command_settings` SET `aliases`= 'bs|settings' WHERE `bot_command`='botsettings'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|followdistance') ELSE 'followd||followdistance' END WHERE `bot_command`='botfollowdistance' AND `aliases` NOT LIKE '%followdistance%'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|ranged|toggleranged|btr') ELSE 'ranged|toggleranged|btr' END WHERE `bot_command`='bottoggleranged' AND `aliases` NOT LIKE '%ranged|toggleranged|btr%'; +UPDATE `bot_command_settings` SET `aliases`= 'distranged|dr' WHERE `bot_command`='distanceranged'; +UPDATE `bot_command_settings` SET `aliases`= 'copy' WHERE `bot_command`='copysettings'; +UPDATE `bot_command_settings` SET `aliases`= 'default' WHERE `bot_command`='defaultsettings'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|enforce') ELSE 'enforce' END WHERE `bot_command`='enforcespellsettings' AND `aliases` NOT LIKE '%enforce%'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|ib') ELSE 'ib' END WHERE `bot_command`='illusionblock' AND `aliases` NOT LIKE '%ib%'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|ig') ELSE 'invgive|ig' END WHERE `bot_command`='inventorygive' AND `aliases` NOT LIKE '%ig%'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|il') ELSE 'invlist|il' END WHERE `bot_command`='inventorylist' AND `aliases` NOT LIKE '%il%'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|ir') ELSE 'invremove|ir' END WHERE `bot_command`='inventoryremove' AND `aliases` NOT LIKE '%ir%'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|iw') ELSE 'invwindow|iw' END WHERE `bot_command`='inventorywindow' AND `aliases` NOT LIKE '%iw%'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|iu') ELSE 'iu' END WHERE `bot_command`='itemuse' AND `aliases` NOT LIKE '%iu%'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|pst') ELSE 'pset||pst' END WHERE `bot_command`='petsettype' AND `aliases` NOT LIKE '%pst%'; +UPDATE `bot_command_settings` SET `aliases`= 'mmr' WHERE `bot_command`='maxmeleerange'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|pp') ELSE 'pp' END WHERE `bot_command`='pickpocket' AND `aliases` NOT LIKE '%pp%'; +UPDATE `bot_command_settings` SET `aliases`= 'sithp' WHERE `bot_command`='sithppercent'; +UPDATE `bot_command_settings` SET `aliases`= 'sitcombat' WHERE `bot_command`='sitincombat'; +UPDATE `bot_command_settings` SET `aliases`= 'sitmana' WHERE `bot_command`='sitmanapercent'; +UPDATE `bot_command_settings` SET `aliases`= 'aggrochecks' WHERE `bot_command`='spellaggrochecks'; +UPDATE `bot_command_settings` SET `aliases`= 'delays' WHERE `bot_command`='spelldelays'; +UPDATE `bot_command_settings` SET `aliases`= 'engagedpriority' WHERE `bot_command`='spellengagedpriority'; +UPDATE `bot_command_settings` SET `aliases`= 'holds' WHERE `bot_command`='spellholds'; +UPDATE `bot_command_settings` SET `aliases`= 'idlepriority' WHERE `bot_command`='spellidlepriority'; +UPDATE `bot_command_settings` SET `aliases`= 'maxhp' WHERE `bot_command`='spellmaxhppct'; +UPDATE `bot_command_settings` SET `aliases`= 'maxmana' WHERE `bot_command`='spellmaxmanapct'; +UPDATE `bot_command_settings` SET `aliases`= 'maxthresholds' WHERE `bot_command`='spellmaxthresholds'; +UPDATE `bot_command_settings` SET `aliases`= 'minhp' WHERE `bot_command`='spellminhppct'; +UPDATE `bot_command_settings` SET `aliases`= 'minmana' WHERE `bot_command`='spellminmanapct'; +UPDATE `bot_command_settings` SET `aliases`= 'minthresholds' WHERE `bot_command`='spellminthresholds'; +UPDATE `bot_command_settings` SET `aliases`= 'pursuepriority' WHERE `bot_command`='spellpursuepriority'; +UPDATE `bot_command_settings` SET `aliases`= 'resistlimits' WHERE `bot_command`='spellresistlimits'; +UPDATE `bot_command_settings` SET `aliases`= 'targetcount' WHERE `bot_command`='spelltargetcount'; +UPDATE `bot_command_settings` SET `aliases`= 'disc' WHERE `bot_command`='discipline'; +UPDATE `bot_command_settings` SET `aliases`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|vc') ELSE 'vc' END WHERE `bot_command`='viewcombos' AND `aliases` NOT LIKE '%vc%'; +UPDATE `bot_command_settings` SET `aliases`= 'announcecasts' WHERE `bot_command`='spellannouncecasts'; +)" + }, + ManifestEntry{ + .version = 9047, + .description = "2024_05_18_bot_update_spell_types.sql", + .check = "SELECT * FROM `bot_spells_entries` WHERE `type` > 21", + .condition = "not_empty", + .match = "", + .sql = R"( +UPDATE `bot_spells_entries` SET `type` = 0 WHERE `type` = 1; +UPDATE `bot_spells_entries` SET `type` = 1 WHERE `type` = 2; +UPDATE `bot_spells_entries` SET `type` = 2 WHERE `type` = 4; +UPDATE `bot_spells_entries` SET `type` = 3 WHERE `type` = 8; +UPDATE `bot_spells_entries` SET `type` = 4 WHERE `type` = 16; +UPDATE `bot_spells_entries` SET `type` = 5 WHERE `type` = 32; +UPDATE `bot_spells_entries` SET `type` = 6 WHERE `type` = 64; +UPDATE `bot_spells_entries` SET `type` = 7 WHERE `type` = 128; +UPDATE `bot_spells_entries` SET `type` = 8 WHERE `type` = 256; +UPDATE `bot_spells_entries` SET `type` = 9 WHERE `type` = 512; +UPDATE `bot_spells_entries` SET `type` = 10 WHERE `type` = 1024; +UPDATE `bot_spells_entries` SET `type` = 11 WHERE `type` = 2048; +UPDATE `bot_spells_entries` SET `type` = 12 WHERE `type` = 4096; +UPDATE `bot_spells_entries` SET `type` = 13 WHERE `type` = 8192; +UPDATE `bot_spells_entries` SET `type` = 14 WHERE `type` = 16384; +UPDATE `bot_spells_entries` SET `type` = 15 WHERE `type` = 32768; +UPDATE `bot_spells_entries` SET `type` = 16 WHERE `type` = 65536; +UPDATE `bot_spells_entries` SET `type` = 17 WHERE `type` = 131072; +UPDATE `bot_spells_entries` SET `type` = 18 WHERE `type` = 262144; +UPDATE `bot_spells_entries` SET `type` = 19 WHERE `type` = 524288; +UPDATE `bot_spells_entries` SET `type` = 20 WHERE `type` = 1048576; +UPDATE `bot_spells_entries` SET `type` = 21 WHERE `type` = 2097152; +)" + }, + ManifestEntry{ + .version = 9048, + .description = "2024_05_18_bot_fear_spell_type.sql", + .check = "SELECT * FROM `bot_spells_entries` where `type` = 22", + .condition = "empty", + .match = "", + .sql = R"( +UPDATE bot_spells_entries b, spells_new s +SET b.`type` = 22 +WHERE b.spell_id = s.id +AND ( + s.`effectid1` = 23 OR + s.`effectid2` = 23 OR + s.`effectid3` = 23 OR + s.`effectid4` = 23 OR + s.`effectid5` = 23 OR + s.`effectid6` = 23 OR + s.`effectid7` = 23 OR + s.`effectid8` = 23 OR + s.`effectid9` = 23 OR + s.`effectid10` = 23 OR + s.`effectid11` = 23 OR + s.`effectid12` = 23 + ); +)" + }, + ManifestEntry{ + .version = 9049, + .description = "2024_05_18_correct_bot_spell_entries_types.sql", + .check = "SELECT * FROM `bot_spells_entries` where `npc_spells_id` = 3002 AND `spell_id` = 14312", + .condition = "empty", + .match = "", + .sql = R"( +-- Class fixes +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3002 WHERE b.`spell_id` = 14312; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3002 WHERE b.`spell_id` = 14313; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3002 WHERE b.`spell_id` = 14314; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3005 WHERE b.`spell_id` = 15186; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3005 WHERE b.`spell_id` = 15187; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3005 WHERE b.`spell_id` = 15188; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spell_id` = 14446; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spell_id` = 14447; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spell_id` = 14467; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spell_id` = 14468; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spell_id` = 14469; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3003 WHERE b.`spell_id` = 14955; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3003 WHERE b.`spell_id` = 14956; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spell_id` = 14387; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spell_id` = 14388; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spell_id` = 14389; + +-- Minlevel fixes +UPDATE bot_spells_entries SET `minlevel` = 34 WHERE `spell_id` = 1445 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 2 WHERE `spell_id` = 229 AND `npc_spells_id` = 3011; +UPDATE bot_spells_entries SET `minlevel` = 13 WHERE `spell_id` = 333 AND `npc_spells_id` = 3013; +UPDATE bot_spells_entries SET `minlevel` = 29 WHERE `spell_id` = 106 AND `npc_spells_id` = 3013; +UPDATE bot_spells_entries SET `minlevel` = 38 WHERE `spell_id` = 754 AND `npc_spells_id` = 3010; +UPDATE bot_spells_entries SET `minlevel` = 58 WHERE `spell_id` = 2589 AND `npc_spells_id` = 3003; +UPDATE bot_spells_entries SET `minlevel` = 67 WHERE `spell_id` = 5305 AND `npc_spells_id` = 3004; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 14267 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 14268 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 14269 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 23 WHERE `spell_id` = 738 AND `npc_spells_id` = 3008; +UPDATE bot_spells_entries SET `minlevel` = 51 WHERE `spell_id` = 1751 AND `npc_spells_id` = 3008; +UPDATE bot_spells_entries SET `minlevel` = 7 WHERE `spell_id` = 734 AND `npc_spells_id` = 3008; +UPDATE bot_spells_entries SET `minlevel` = 5 WHERE `spell_id` = 717 AND `npc_spells_id` = 3008; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 15186 AND `npc_spells_id` = 3005; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 15187 AND `npc_spells_id` = 3005; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 15188 AND `npc_spells_id` = 3005; +UPDATE bot_spells_entries SET `minlevel` = 80 WHERE `spell_id` = 14446 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 80 WHERE `spell_id` = 14447 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 14467 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 14468 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spell_id` = 14469 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spell_id` = 14955 AND `npc_spells_id` = 3003; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spell_id` = 14956 AND `npc_spells_id` = 3003; +UPDATE bot_spells_entries SET `minlevel` = 78 WHERE `spell_id` = 14387 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spell_id` = 14388 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spell_id` = 14389 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spell_id` = 14312 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spell_id` = 14313 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spell_id` = 14314 AND `npc_spells_id` = 3002; + +-- Maxlevel fixes +UPDATE bot_spells_entries SET `maxlevel` = 83 WHERE `spell_id` = 14267 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 83 WHERE `spell_id` = 14268 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 83 WHERE `spell_id` = 14269 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spell_id` = 14446 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spell_id` = 14447 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spell_id` = 14467 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spell_id` = 14468 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spell_id` = 14469 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 81 WHERE `spell_id` = 14312 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 81 WHERE `spell_id` = 14313 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 81 WHERE `spell_id` = 14314 AND `npc_spells_id` = 3002; + +-- Type fixes +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 201; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spell_id` = 752; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spell_id` = 2117; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 2542; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 2544; +UPDATE bot_spells_entries SET `type` = 6 WHERE `spell_id` = 2115; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 1403; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 1405; +UPDATE bot_spells_entries SET `type` = 9 WHERE `spell_id` = 289; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 294; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 302; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 521; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 185; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 450; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 186; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 4074; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 195; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 1712; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 1703; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spell_id` = 3229; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 3345; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 5509; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 6826; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 270; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 281; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 505; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 526; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 110; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 506; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 162; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 111; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 507; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 527; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 163; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 112; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 1588; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 1573; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 1592; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 1577; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 1578; +UPDATE bot_spells_entries SET `type` = 1 WHERE `spell_id` = 1576; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 3386; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 3387; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 4900; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 3395; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 5394; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 5392; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 6827; +UPDATE bot_spells_entries SET `type` = 1 WHERE `spell_id` = 5416; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 1437; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 1436; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 5348; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 8008; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 2571; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 370; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spell_id` = 1741; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spell_id` = 1296; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 270; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 2634; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 2942; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 3462; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spell_id` = 6828; +UPDATE bot_spells_entries SET `type` = 4 WHERE `spell_id` = 14312; +UPDATE bot_spells_entries SET `type` = 4 WHERE `spell_id` = 14313; +UPDATE bot_spells_entries SET `type` = 4 WHERE `spell_id` = 14314; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 18392; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 18393; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 18394; +UPDATE bot_spells_entries SET `type` = 10 WHERE `spell_id` = 15186; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 15187; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 15188; +UPDATE bot_spells_entries SET `type` = 1 WHERE `spell_id` = 14446; +UPDATE bot_spells_entries SET `type` = 1 WHERE `spell_id` = 14447; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 14467; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 14468; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 14469; +UPDATE bot_spells_entries SET `type` = 0 WHERE `spell_id` = 14267; +UPDATE bot_spells_entries SET `type` = 0 WHERE `spell_id` = 14268; +UPDATE bot_spells_entries SET `type` = 0 WHERE `spell_id` = 14269; +UPDATE bot_spells_entries SET `type` = 10 WHERE `spell_id` = 14955; +UPDATE bot_spells_entries SET `type` = 10 WHERE `spell_id` = 14956; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 14387; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 14388; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spell_id` = 14389; +UPDATE bot_spells_entries SET `type` = 4 WHERE `spell_id` = 10436; + +-- UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 3440; -- Ro's Illumination [#3440] from DoT [#8] to Debuff [#14] [Should be 0] +-- UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 303; -- Whirl till you hurl [#303] from Nuke [#0] to Debuff [#14] [Should be 0] +-- UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 619; -- Dyn's Dizzying Draught [#619] from Nuke [#0] to Debuff [#14] [Should be 0] + +-- UPDATE bot_spells_entries SET `type` = 14 WHERE `spell_id` = 74; -- Mana Sieve [#74] from Nuke [#0] to Debuff [#14] +-- UPDATE bot_spells_entries SET `type` = 6 WHERE `spell_id` = 1686; -- Theft of Thought [#1686] from Nuke [#0] to Lifetap [#6] + +-- UPDATE bot_spells_entries SET `type` = 1 WHERE `spell_id` = 3694; -- Stoicism [#3694] from In-Combat Buff [#10] to Regular Heal [#1] +-- UPDATE bot_spells_entries SET `type` = 1 WHERE `spell_id` = 4899; -- Breath of Trushar [#4899] from In-Combat Buff [#10] to Regular Heal [#1] + +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spell_id` = 738; -- Selo's Consonant Chain [#738] from Slow [#13] to Snare [#7] +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spell_id` = 1751; -- Largo's Assonant Binding [#1751] from Slow [#13] to Snare [#7] +-- UPDATE bot_spells_entries SET `type` = 8 WHERE `spell_id` = 1748; -- Angstlich's Assonance [#1748] from Slow [#13] to DoT [#8] +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spell_id` = 738; -- Selo's Consonant Chain [#738] from Slow [#13] to Snare [#7] +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spell_id` = 1751; -- Largo's Assonant Binding [#1751] from Slow [#13] to Snare [#7] +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spell_id` = 738; -- Selo's Consonant Chain [#738] from Slow [#13] to Snare [#7] + +)" + }, + ManifestEntry{ + .version = 9050, + .description = "2024_11_26_add_commanded_spelltypes.sql", + .check = "SELECT * FROM `bot_spells_entries` where `type` = 100", + .condition = "empty", + .match = "", + .sql = R"( +INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`) +VALUES +(3006, 9957, 100, 20, 254), +(3006, 9956, 100, 20, 254), +(3006, 552, 100, 25, 254), +(3006, 550, 100, 25, 254), +(3006, 553, 100, 25, 254), +(3006, 2432, 100, 26, 254), +(3006, 2020, 100, 26, 254), +(3006, 551, 100, 27, 254), +(3006, 3792, 100, 28, 254), +(3006, 2419, 100, 29, 254), +(3006, 554, 100, 30, 254), +(3006, 557, 100, 31, 254), +(3006, 1434, 100, 32, 254), +(3006, 555, 100, 32, 254), +(3006, 25898, 100, 32, 254), +(3006, 25904, 100, 32, 254), +(3006, 556, 100, 32, 254), +(3006, 25698, 100, 33, 254), +(3006, 1517, 100, 33, 254), +(3006, 2424, 100, 33, 254), +(3006, 25689, 100, 33, 254), +(3006, 25899, 100, 34, 254), +(3006, 25690, 100, 35, 254), +(3006, 25903, 100, 35, 254), +(3006, 25900, 100, 35, 254), +(3006, 558, 100, 36, 254), +(3006, 2429, 100, 37, 254), +(3006, 1438, 100, 38, 254), +(3006, 3184, 100, 38, 254), +(3006, 25697, 100, 38, 254), +(3006, 25902, 100, 39, 254), +(3006, 25695, 100, 39, 254), +(3006, 25901, 100, 40, 254), +(3006, 25694, 100, 40, 254), +(3006, 1398, 100, 40, 254), +(3006, 25905, 100, 41, 254), +(3006, 25696, 100, 42, 254), +(3006, 1440, 100, 42, 254), +(3006, 25906, 100, 43, 254), +(3006, 25693, 100, 44, 254), +(3006, 25699, 100, 45, 254), +(3006, 24773, 100, 46, 254), +(3006, 8965, 100, 52, 254), +(3006, 24771, 100, 52, 254), +(3006, 8235, 100, 52, 254), +(3006, 24775, 100, 52, 254), +(3006, 4966, 100, 54, 254), +(3006, 6184, 100, 55, 254), +(3006, 5731, 100, 55, 254), +(3006, 24776, 100, 56, 254), +(3006, 25700, 100, 56, 254), +(3006, 25691, 100, 57, 254), +(3006, 24772, 100, 57, 254), +(3006, 25692, 100, 57, 254), +(3006, 11981, 100, 59, 254), +(3006, 9953, 100, 60, 254), +(3006, 9954, 100, 60, 254), +(3006, 11980, 100, 64, 254), +(3006, 6179, 100, 64, 254), +(3006, 24774, 100, 67, 254), +(3006, 9950, 100, 70, 254), +(3006, 9951, 100, 70, 254), +(3006, 15886, 100, 75, 254), +(3006, 15887, 100, 75, 254), +(3006, 21989, 100, 80, 254), +(3006, 20539, 100, 80, 254), +(3006, 21984, 100, 80, 254), +(3006, 20538, 100, 80, 254), +(3006, 17883, 100, 85, 254), +(3006, 17884, 100, 85, 254), +(3006, 28997, 100, 90, 254), +(3006, 28998, 100, 90, 254), +(3006, 29000, 100, 92, 254), +(3006, 29001, 100, 92, 254), +(3006, 34832, 100, 95, 254), +(3006, 40217, 100, 95, 254), +(3006, 34833, 100, 95, 254), +(3006, 40216, 100, 95, 254), +(3012, 10881, 100, 20, 254), +(3012, 10880, 100, 20, 254), +(3012, 562, 100, 25, 254), +(3012, 563, 100, 27, 254), +(3012, 3793, 100, 27, 254), +(3012, 561, 100, 28, 254), +(3012, 2420, 100, 29, 254), +(3012, 2944, 100, 29, 254), +(3012, 564, 100, 32, 254), +(3012, 565, 100, 33, 254), +(3012, 1418, 100, 33, 254), +(3012, 2425, 100, 33, 254), +(3012, 1516, 100, 34, 254), +(3012, 1338, 100, 35, 254), +(3012, 3833, 100, 35, 254), +(3012, 566, 100, 35, 254), +(3012, 1336, 100, 36, 254), +(3012, 2943, 100, 36, 254), +(3012, 1423, 100, 36, 254), +(3012, 567, 100, 36, 254), +(3012, 568, 100, 37, 254), +(3012, 1337, 100, 37, 254), +(3012, 3180, 100, 38, 254), +(3012, 1339, 100, 38, 254), +(3012, 2421, 100, 39, 254), +(3012, 2430, 100, 39, 254), +(3012, 1372, 100, 40, 254), +(3012, 2426, 100, 41, 254), +(3012, 1371, 100, 41, 254), +(3012, 1399, 100, 42, 254), +(3012, 1374, 100, 42, 254), +(3012, 1373, 100, 43, 254), +(3012, 1425, 100, 43, 254), +(3012, 1375, 100, 44, 254), +(3012, 3181, 100, 45, 254), +(3012, 2022, 100, 45, 254), +(3012, 666, 100, 46, 254), +(3012, 3849, 100, 46, 254), +(3012, 674, 100, 46, 254), +(3012, 2023, 100, 46, 254), +(3012, 2024, 100, 47, 254), +(3012, 2025, 100, 48, 254), +(3012, 2431, 100, 49, 254), +(3012, 8966, 100, 51, 254), +(3012, 8236, 100, 51, 254), +(3012, 4965, 100, 54, 254), +(3012, 8969, 100, 55, 254), +(3012, 8239, 100, 55, 254), +(3012, 6183, 100, 55, 254), +(3012, 5732, 100, 55, 254), +(3012, 4964, 100, 57, 254), +(3012, 6182, 100, 58, 254), +(3012, 5735, 100, 60, 254), +(3012, 10877, 100, 60, 254), +(3012, 10878, 100, 60, 254), +(3012, 6178, 100, 64, 254), +(3012, 6177, 100, 67, 254), +(3012, 11984, 100, 69, 254), +(3012, 10874, 100, 70, 254), +(3012, 10875, 100, 70, 254), +(3012, 11983, 100, 74, 254), +(3012, 15889, 100, 75, 254), +(3012, 15890, 100, 75, 254), +(3012, 21988, 100, 80, 254), +(3012, 20542, 100, 80, 254), +(3012, 21985, 100, 80, 254), +(3012, 20541, 100, 80, 254), +(3012, 17886, 100, 85, 254), +(3012, 17887, 100, 85, 254), +(3012, 29840, 100, 90, 254), +(3012, 29841, 100, 90, 254), +(3012, 29843, 100, 92, 254), +(3012, 29844, 100, 92, 254), +(3012, 40443, 100, 95, 254), +(3012, 35715, 100, 95, 254), +(3012, 40442, 100, 95, 254), +(3012, 35714, 100, 95, 254), +(3002, 208, 101, 1, 4), +(3002, 501, 101, 5, 14), +(3002, 47, 101, 15, 35), +(3002, 45, 101, 36, 64), +(3002, 1541, 101, 55, 254), +(3002, 3197, 101, 65, 254), +(3002, 5274, 101, 70, 70), +(3002, 9798, 101, 71, 75), +(3002, 9799, 101, 71, 75), +(3002, 9797, 101, 71, 75), +(3002, 14288, 101, 76, 80), +(3002, 14289, 101, 76, 80), +(3002, 14290, 101, 76, 80), +(3002, 18309, 101, 81, 85), +(3002, 18310, 101, 81, 85), +(3002, 18311, 101, 81, 85), +(3002, 25103, 101, 86, 90), +(3002, 25101, 101, 86, 90), +(3002, 25102, 101, 86, 90), +(3002, 28102, 101, 91, 95), +(3002, 28100, 101, 91, 95), +(3002, 28101, 101, 91, 95), +(3002, 34096, 101, 96, 254), +(3002, 34094, 101, 96, 254), +(3002, 34095, 101, 96, 254), +(3003, 208, 101, 10, 24), +(3003, 501, 101, 25, 42), +(3003, 47, 101, 43, 48), +(3003, 45, 101, 49, 254), +(3003, 25294, 101, 86, 90), +(3003, 25295, 101, 86, 90), +(3003, 25296, 101, 86, 90), +(3003, 28340, 101, 91, 95), +(3003, 28338, 101, 91, 95), +(3003, 28339, 101, 91, 95), +(3003, 34346, 101, 96, 254), +(3003, 34344, 101, 96, 254), +(3003, 34345, 101, 96, 254), +(3004, 240, 101, 4, 30), +(3004, 250, 101, 22, 254), +(3004, 513, 101, 31, 254), +(3004, 3601, 101, 39, 254), +(3004, 5316, 101, 68, 70), +(3004, 10112, 101, 71, 75), +(3004, 10110, 101, 71, 75), +(3004, 10111, 101, 71, 75), +(3004, 15037, 101, 76, 80), +(3004, 15035, 101, 76, 80), +(3004, 15036, 101, 76, 80), +(3004, 19168, 101, 81, 85), +(3004, 19169, 101, 81, 85), +(3004, 19167, 101, 81, 85), +(3004, 25417, 101, 86, 87), +(3004, 25418, 101, 86, 90), +(3004, 25419, 101, 86, 90), +(3004, 25466, 101, 88, 90), +(3004, 25467, 101, 88, 90), +(3004, 25465, 101, 88, 90), +(3004, 28479, 101, 91, 92), +(3004, 28480, 101, 91, 95), +(3004, 28481, 101, 91, 95), +(3004, 28542, 101, 93, 95), +(3004, 28543, 101, 93, 95), +(3004, 28544, 101, 93, 95), +(3004, 34500, 101, 96, 97), +(3004, 34502, 101, 96, 254), +(3004, 34501, 101, 96, 254), +(3004, 34565, 101, 98, 254), +(3004, 34563, 101, 98, 254), +(3004, 34564, 101, 98, 254), +(3005, 347, 101, 9, 51), +(3005, 448, 101, 52, 254), +(3006, 240, 101, 1, 14), +(3006, 250, 101, 5, 254), +(3006, 513, 101, 15, 254), +(3006, 3601, 101, 29, 254), +(3006, 5347, 101, 67, 70), +(3006, 9851, 101, 71, 75), +(3006, 9852, 101, 71, 75), +(3006, 9853, 101, 71, 75), +(3006, 14369, 101, 76, 80), +(3006, 14367, 101, 76, 80), +(3006, 14368, 101, 76, 80), +(3006, 18409, 101, 81, 85), +(3006, 18407, 101, 81, 85), +(3006, 18408, 101, 81, 85), +(3006, 25736, 101, 86, 90), +(3006, 25734, 101, 86, 90), +(3006, 25735, 101, 86, 90), +(3006, 28831, 101, 91, 95), +(3006, 28832, 101, 91, 95), +(3006, 28830, 101, 91, 95), +(3006, 34863, 101, 96, 254), +(3006, 34864, 101, 96, 254), +(3006, 34862, 101, 96, 254), +(3008, 728, 101, 8, 60), +(3008, 3361, 101, 61, 254), +(3008, 5370, 101, 66, 70), +(3008, 10403, 101, 71, 75), +(3008, 10401, 101, 71, 75), +(3008, 10402, 101, 71, 75), +(3008, 14002, 101, 76, 80), +(3008, 14000, 101, 76, 80), +(3008, 14001, 101, 76, 80), +(3008, 18001, 101, 81, 85), +(3008, 18002, 101, 81, 85), +(3008, 18000, 101, 81, 85), +(3008, 25978, 101, 86, 90), +(3008, 25979, 101, 86, 90), +(3008, 25977, 101, 86, 90), +(3008, 29079, 101, 91, 95), +(3008, 29080, 101, 91, 95), +(3008, 29078, 101, 91, 95), +(3008, 35131, 101, 96, 254), +(3008, 35132, 101, 96, 254), +(3008, 35133, 101, 96, 254), +(3011, 347, 101, 2, 22), +(3011, 448, 101, 23, 254), +(3014, 208, 101, 1, 5), +(3014, 501, 101, 6, 17), +(3014, 47, 101, 18, 34), +(3014, 45, 101, 35, 61), +(3014, 1541, 101, 51, 254), +(3014, 3197, 101, 62, 254), +(3014, 5506, 101, 67, 71), +(3014, 10601, 101, 72, 76), +(3014, 10599, 101, 72, 76), +(3014, 10600, 101, 72, 76), +(3014, 14510, 101, 77, 81), +(3014, 14511, 101, 77, 81), +(3014, 14509, 101, 77, 81), +(3014, 18568, 101, 82, 86), +(3014, 18569, 101, 82, 86), +(3014, 18567, 101, 82, 86), +(3014, 26921, 101, 87, 91), +(3014, 26922, 101, 87, 91), +(3014, 26920, 101, 87, 91), +(3014, 30054, 101, 92, 96), +(3014, 30055, 101, 92, 96), +(3014, 30056, 101, 92, 96), +(3014, 36116, 101, 97, 254), +(3014, 36117, 101, 97, 254), +(3014, 36118, 101, 97, 254), +(3006, 2183, 102, 18, 56), +(3006, 1567, 102, 57, 254), +(3012, 2184, 102, 18, 56), +(3012, 2558, 102, 56, 64), +(3012, 1628, 102, 57, 254), +(3012, 3244, 102, 65, 254), +(3002, 35, 103, 10, 254), +(3006, 35, 103, 12, 254), +(3010, 35, 103, 14, 254), +(3011, 35, 103, 12, 254), +(3012, 35, 103, 12, 254), +(3013, 35, 103, 12, 254), +(3014, 35, 103, 12, 254), +(3008, 737, 104, 14, 254), +(3011, 305, 104, 17, 254), +(3012, 305, 104, 14, 254), +(3013, 305, 104, 13, 254), +(3014, 305, 104, 15, 254), +(3004, 261, 105, 35, 64), +(3004, 2517, 105, 65, 254), +(3006, 261, 105, 14, 49), +(3006, 2894, 105, 50, 53), +(3006, 2517, 105, 54, 254), +(3006, 3185, 105, 62, 254), +(3008, 718, 105, 31, 50), +(3008, 1750, 105, 51, 254), +(3010, 261, 105, 10, 50), +(3010, 2894, 105, 51, 254), +(3011, 457, 105, 41, 254), +(3011, 1391, 105, 45, 254), +(3012, 261, 105, 22, 49), +(3012, 2894, 105, 50, 254), +(3014, 261, 105, 15, 50), +(3014, 2894, 105, 51, 254), +(3015, 261, 105, 32, 254), +(3003, 1743, 106, 55, 254), +(3008, 714, 106, 41, 254), +(3008, 748, 106, 47, 57), +(3008, 1450, 106, 49, 254), +(3008, 1752, 106, 52, 254), +(3008, 1763, 106, 58, 72), +(3008, 11881, 106, 73, 77), +(3008, 11879, 106, 73, 77), +(3008, 11880, 106, 73, 77), +(3008, 14055, 106, 78, 82), +(3008, 14056, 106, 78, 82), +(3008, 14054, 106, 78, 82), +(3008, 18040, 106, 83, 87), +(3008, 18041, 106, 83, 87), +(3008, 18039, 106, 83, 87), +(3008, 26026, 106, 88, 92), +(3008, 26027, 106, 88, 92), +(3008, 26025, 106, 88, 92), +(3008, 29120, 106, 93, 97), +(3008, 29121, 106, 93, 97), +(3008, 29122, 106, 93, 97), +(3008, 35170, 106, 98, 254), +(3008, 35171, 106, 98, 254), +(3008, 35172, 106, 98, 254), +(3012, 2559, 106, 58, 254), +(3014, 481, 106, 13, 21), +(3014, 21, 106, 19, 21), +(3014, 482, 106, 22, 32), +(3014, 483, 106, 33, 39), +(3014, 648, 106, 38, 39), +(3014, 484, 106, 40, 57), +(3014, 176, 106, 47, 57), +(3014, 1689, 106, 52, 57), +(3014, 1713, 106, 58, 60), +(3014, 3343, 106, 61, 66), +(3014, 6739, 106, 61, 68), +(3014, 3351, 106, 63, 66), +(3014, 5504, 106, 67, 70), +(3014, 5514, 106, 69, 70), +(3014, 6671, 106, 69, 254), +(3014, 10598, 106, 71, 75), +(3014, 10596, 106, 71, 75), +(3014, 10597, 106, 71, 75), +(3014, 10643, 106, 74, 75), +(3014, 10641, 106, 74, 75), +(3014, 10642, 106, 74, 75), +(3014, 11887, 106, 74, 78), +(3014, 11885, 106, 74, 78), +(3014, 11886, 106, 74, 78), +(3014, 14507, 106, 76, 80), +(3014, 14508, 106, 76, 80), +(3014, 14506, 106, 76, 80), +(3014, 14543, 106, 79, 80), +(3014, 14544, 106, 79, 80), +(3014, 14542, 106, 79, 80), +(3014, 14582, 106, 79, 83), +(3014, 14583, 106, 79, 83), +(3014, 14581, 106, 79, 83), +(3014, 18564, 106, 81, 85), +(3014, 18565, 106, 81, 85), +(3014, 18566, 106, 81, 85), +(3014, 18600, 106, 84, 85), +(3014, 18601, 106, 84, 85), +(3014, 18602, 106, 84, 85), +(3014, 18641, 106, 84, 88), +(3014, 18639, 106, 84, 88), +(3014, 18640, 106, 84, 88), +(3014, 26901, 106, 86, 90), +(3014, 26899, 106, 86, 90), +(3014, 26900, 106, 86, 90), +(3014, 26999, 106, 89, 90), +(3014, 26997, 106, 89, 90), +(3014, 26998, 106, 89, 90), +(3014, 27025, 106, 89, 93), +(3014, 27026, 106, 89, 93), +(3014, 27024, 106, 89, 93), +(3014, 30028, 106, 91, 95), +(3014, 30029, 106, 91, 95), +(3014, 30027, 106, 91, 95), +(3014, 30132, 106, 93, 95), +(3014, 30133, 106, 93, 95), +(3014, 30131, 106, 93, 95), +(3014, 30140, 106, 94, 95), +(3014, 30141, 106, 94, 95), +(3014, 30142, 106, 94, 95), +(3014, 30167, 106, 94, 98), +(3014, 30168, 106, 94, 98), +(3014, 30169, 106, 94, 98), +(3014, 36091, 106, 96, 254), +(3014, 36089, 106, 96, 254), +(3014, 36090, 106, 96, 254), +(3014, 36189, 106, 98, 254), +(3014, 36187, 106, 98, 254), +(3014, 36188, 106, 98, 254), +(3014, 36216, 106, 99, 254), +(3014, 36196, 106, 99, 254), +(3014, 36217, 106, 99, 254), +(3014, 36194, 106, 99, 254), +(3014, 36215, 106, 99, 254), +(3014, 36195, 106, 99, 254), +(3004, 86, 107, 20, 254), +(3006, 86, 107, 6, 49), +(3006, 2881, 107, 50, 254), +(3008, 729, 107, 16, 254), +(3010, 86, 107, 12, 50), +(3010, 2881, 107, 51, 254), +(3011, 457, 107, 41, 254), +(3011, 1391, 107, 45, 254), +(3014, 86, 107, 12, 43), +(3014, 3696, 107, 44, 50), +(3014, 2881, 107, 51, 254), +(3015, 86, 107, 25, 254), +(3010, 345, 108, 15, 254), +(3010, 2522, 108, 16, 254), +(3015, 345, 108, 23, 254), +(3002, 235, 109, 11, 254), +(3002, 1726, 109, 51, 254), +(3002, 6125, 109, 66, 254), +(3002, 14348, 109, 77, 81), +(3002, 14346, 109, 77, 81), +(3002, 14347, 109, 77, 81), +(3002, 18369, 109, 82, 254), +(3002, 18367, 109, 82, 254), +(3002, 18368, 109, 82, 254), +(3003, 235, 109, 17, 254), +(3004, 247, 109, 14, 46), +(3004, 80, 109, 32, 64), +(3004, 34, 109, 47, 254), +(3004, 2517, 109, 65, 254), +(3005, 235, 109, 4, 254), +(3006, 247, 109, 4, 17), +(3006, 255, 109, 8, 254), +(3006, 80, 109, 13, 53), +(3006, 34, 109, 18, 254), +(3006, 2516, 109, 52, 254), +(3006, 4058, 109, 52, 254), +(3006, 2517, 109, 54, 254), +(3006, 3185, 109, 62, 254), +(3006, 6123, 109, 68, 254), +(3008, 719, 109, 19, 50), +(3008, 735, 109, 24, 254), +(3008, 1750, 109, 51, 254), +(3010, 79, 109, 7, 55), +(3010, 255, 109, 10, 254), +(3010, 42, 109, 27, 254), +(3010, 1575, 109, 56, 254), +(3010, 2886, 109, 58, 254), +(3011, 235, 109, 1, 254), +(3011, 457, 109, 41, 254), +(3011, 1391, 109, 45, 254), +(3011, 6124, 109, 68, 254), +(3012, 80, 109, 4, 39), +(3012, 42, 109, 16, 254), +(3012, 3811, 109, 40, 254), +(3012, 6120, 109, 67, 254), +(3012, 15513, 109, 76, 80), +(3012, 15511, 109, 76, 80), +(3012, 15512, 109, 76, 80), +(3012, 19701, 109, 81, 254), +(3012, 19699, 109, 81, 254), +(3012, 19700, 109, 81, 254), +(3013, 42, 109, 8, 254), +(3013, 80, 109, 16, 254), +(3014, 42, 109, 4, 254), +(3014, 80, 109, 6, 43), +(3014, 235, 109, 14, 254), +(3014, 3696, 109, 44, 254), +(3014, 6122, 109, 66, 254), +(3015, 79, 109, 29, 64), +(3015, 42, 109, 43, 254), +(3015, 1575, 109, 65, 254), +(3004, 278, 110, 28, 64), +(3004, 4054, 110, 41, 64), +(3004, 4055, 110, 49, 254), +(3004, 2517, 110, 65, 254), +(3006, 278, 110, 10, 53), +(3006, 424, 110, 26, 254), +(3006, 4054, 110, 30, 53), +(3006, 169, 110, 35, 61), +(3006, 4055, 110, 35, 254), +(3006, 3579, 110, 45, 254), +(3006, 4058, 110, 52, 254), +(3006, 1554, 110, 53, 254), +(3006, 2517, 110, 54, 254), +(3006, 3185, 110, 62, 254), +(3008, 717, 110, 5, 48), +(3008, 4395, 110, 25, 48), +(3008, 2605, 110, 49, 50), +(3008, 1750, 110, 51, 254), +(3010, 278, 110, 9, 254), +(3010, 424, 110, 22, 254), +(3010, 4054, 110, 29, 254), +(3010, 4055, 110, 34, 254), +(3010, 2524, 110, 36, 254), +(3010, 1554, 110, 52, 254), +(3015, 278, 110, 24, 254), +(3015, 4054, 110, 39, 254), +(3015, 4055, 110, 44, 254), +(3012, 1422, 111, 50, 254), +(3012, 1334, 111, 52, 254), +(3005, 2213, 112, 12, 34), +(3005, 3, 112, 35, 56), +(3005, 1773, 112, 57, 70), +(3005, 10042, 112, 71, 75), +(3005, 14823, 112, 76, 80), +(3005, 18928, 112, 81, 85), +(3005, 25555, 112, 86, 90), +(3005, 28632, 112, 91, 95), +(3005, 34662, 112, 96, 254), +(3011, 2213, 112, 12, 34), +(3011, 3, 112, 35, 56), +(3011, 1773, 112, 57, 70), +(3011, 10042, 112, 71, 75), +(3011, 14823, 112, 76, 80), +(3011, 18928, 112, 81, 85), +(3011, 25555, 112, 86, 90), +(3011, 28632, 112, 91, 95), +(3011, 34662, 112, 96, 254); + +DELETE +FROM bot_spells_entries +WHERE NOT EXISTS +(SELECT * +FROM spells_new +WHERE bot_spells_entries.spell_id = spells_new.id); +)" + }, + ManifestEntry{ + .version = 9051, + .description = "2024_11_26_remove_sk_icb.sql", + .check = "SELECT * FROM `bot_spells_entries` where `type` = 24", + .condition = "empty", + .match = "", + .sql = R"( +DELETE +FROM bot_spells_entries +WHERE `npc_spells_id` = 3005 +AND `type` = 10; + +INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`, `priority`) +VALUES +(3003, 10173, 24, 72, 76, 3), +(3003, 10174, 24, 72, 76, 2), +(3003, 10175, 24, 72, 76, 1), +(3003, 14954, 24, 77, 81, 3), +(3003, 14955, 24, 77, 81, 2), +(3003, 14956, 24, 77, 81, 1), +(3003, 19068, 24, 82, 86, 3), +(3003, 19069, 24, 82, 86, 2), +(3003, 19070, 24, 82, 86, 1), +(3003, 25297, 24, 87, 91, 3), +(3003, 25298, 24, 87, 91, 2), +(3003, 25299, 24, 87, 91, 1), +(3003, 28347, 24, 92, 96, 3), +(3003, 28348, 24, 92, 96, 2), +(3003, 28349, 24, 92, 96, 1), +(3003, 34350, 24, 97, 254, 3), +(3003, 34351, 24, 97, 254, 2), +(3003, 34352, 24, 97, 254, 1), +(3003, 40078, 24, 98, 254, 3), +(3003, 40079, 24, 98, 254, 2), +(3003, 40080, 24, 98, 254, 1), +(3005, 1221, 24, 33, 41, 1), +(3005, 1222, 24, 42, 52, 1), +(3005, 1223, 24, 53, 58, 1), +(3005, 1224, 24, 59, 62, 1), +(3005, 3405, 24, 63, 66, 1), +(3005, 5329, 24, 67, 70, 5), +(3005, 5336, 24, 69, 73, 4), +(3005, 10257, 24, 71, 71, 3), +(3005, 10258, 24, 71, 71, 2), +(3005, 10259, 24, 71, 71, 1), +(3005, 10260, 24, 72, 76, 3), +(3005, 10261, 24, 72, 76, 2), +(3005, 10262, 24, 72, 76, 1), +(3005, 10291, 24, 74, 78, 3), +(3005, 10292, 24, 74, 78, 2), +(3005, 10293, 24, 74, 78, 1), +(3005, 15160, 24, 76, 76, 3), +(3005, 15161, 24, 76, 76, 2), +(3005, 15162, 24, 76, 76, 1), +(3005, 15163, 24, 77, 81, 3), +(3005, 15164, 24, 77, 81, 2), +(3005, 15165, 24, 77, 81, 1), +(3005, 15184, 24, 79, 83, 3), +(3005, 15185, 24, 79, 83, 2), +(3005, 15186, 24, 79, 83, 1), +(3005, 19313, 24, 81, 81, 3), +(3005, 19314, 24, 81, 81, 2), +(3005, 19315, 24, 81, 81, 1), +(3005, 19316, 24, 82, 86, 3), +(3005, 19317, 24, 82, 86, 2), +(3005, 19318, 24, 82, 86, 1), +(3005, 19337, 24, 84, 88, 3), +(3005, 19338, 24, 84, 88, 2), +(3005, 19339, 24, 84, 88, 1), +(3005, 25580, 24, 86, 86, 3), +(3005, 25581, 24, 86, 86, 2), +(3005, 25582, 24, 86, 86, 1), +(3005, 25586, 24, 87, 91, 3), +(3005, 25587, 24, 87, 91, 2), +(3005, 25588, 24, 87, 91, 1), +(3005, 25641, 24, 89, 93, 3), +(3005, 25642, 24, 89, 93, 2), +(3005, 25643, 24, 89, 93, 1), +(3005, 28657, 24, 91, 91, 3), +(3005, 28658, 24, 91, 91, 2), +(3005, 28659, 24, 91, 91, 1), +(3005, 28663, 24, 92, 96, 3), +(3005, 28664, 24, 92, 96, 2), +(3005, 28665, 24, 92, 96, 1), +(3005, 28733, 24, 94, 98, 3), +(3005, 28734, 24, 94, 98, 2), +(3005, 28735, 24, 94, 98, 1), +(3005, 34687, 24, 96, 96, 3), +(3005, 34688, 24, 96, 96, 2), +(3005, 34689, 24, 96, 96, 1), +(3005, 34693, 24, 97, 254, 3), +(3005, 34694, 24, 97, 254, 2), +(3005, 34695, 24, 97, 254, 1), +(3005, 34751, 24, 99, 254, 3), +(3005, 34752, 24, 99, 254, 2), +(3005, 34753, 24, 99, 254, 1); + +DELETE +FROM bot_spells_entries +WHERE NOT EXISTS +(SELECT * +FROM spells_new +WHERE bot_spells_entries.spell_id = spells_new.id); +)" + }, +ManifestEntry{ + .version = 9052, + .description = "2024_12_15_bot_blocked_buffs.sql", + .check = "SHOW TABLES LIKE 'bot_blocked_buffs'", + .condition = "empty", + .match = "", + .sql = R"( +CREATE TABLE `bot_blocked_buffs` ( + `bot_id` INT(11) UNSIGNED NOT NULL, + `spell_id` INT(11) UNSIGNED NOT NULL, + `blocked` TINYINT(4) UNSIGNED NULL DEFAULT '0', + `blocked_pet` TINYINT(4) UNSIGNED NULL DEFAULT '0', + PRIMARY KEY (`bot_id`, `spell_id`) USING BTREE +) +COLLATE='latin1_swedish_ci' +ENGINE=InnoDB +; +)" + }, + ManifestEntry{ + .version = 9053, + .description = "2024_12_26_discipline_inserts.sql", + .check = "SELECT * FROM `bot_spells_entries` where `type` = 200", + .condition = "empty", + .match = "", + .sql = R"( +INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`) +VALUES +(3001, 5225, 200, 1, 254), +(3001, 25060, 200, 5, 254), +(3001, 4721, 200, 10, 62), +(3001, 4608, 200, 20, 51), +(3001, 4585, 200, 30, 254), +(3001, 4587, 200, 40, 254), +(3001, 4681, 200, 52, 55), +(3001, 4503, 200, 52, 254), +(3001, 4672, 200, 53, 254), +(3001, 4514, 200, 54, 254), +(3001, 4499, 200, 55, 64), +(3001, 8921, 200, 55, 69), +(3001, 4682, 200, 56, 62), +(3001, 4674, 200, 56, 254), +(3001, 4501, 200, 57, 254), +(3001, 4675, 200, 58, 59), +(3001, 4670, 200, 59, 254), +(3001, 4498, 200, 60, 254), +(3001, 6750, 200, 61, 68), +(3001, 4689, 200, 61, 254), +(3001, 4697, 200, 63, 64), +(3001, 4687, 200, 63, 254), +(3001, 4688, 200, 65, 71), +(3001, 5015, 200, 65, 254), +(3001, 5016, 200, 65, 254), +(3001, 6191, 200, 66, 70), +(3001, 6192, 200, 68, 73), +(3001, 8000, 200, 68, 254), +(3001, 6725, 200, 69, 73), +(3001, 6173, 200, 69, 80), +(3001, 8467, 200, 70, 74), +(3001, 8468, 200, 70, 254), +(3001, 6190, 200, 70, 254), +(3001, 10959, 200, 71, 75), +(3001, 10960, 200, 71, 75), +(3001, 10961, 200, 71, 75), +(3001, 11913, 200, 72, 76), +(3001, 11914, 200, 72, 76), +(3001, 11915, 200, 72, 76), +(3001, 10965, 200, 72, 254), +(3001, 10966, 200, 72, 254), +(3001, 10967, 200, 72, 254), +(3001, 10970, 200, 73, 254), +(3001, 10968, 200, 73, 254), +(3001, 10969, 200, 73, 254), +(3001, 10971, 200, 74, 78), +(3001, 10972, 200, 74, 78), +(3001, 10973, 200, 74, 78), +(3001, 11917, 200, 74, 254), +(3001, 11918, 200, 74, 254), +(3001, 11916, 200, 74, 254), +(3001, 10974, 200, 75, 79), +(3001, 10975, 200, 75, 79), +(3001, 10976, 200, 75, 79), +(3001, 15345, 200, 76, 80), +(3001, 15346, 200, 76, 80), +(3001, 15347, 200, 76, 80), +(3001, 14192, 200, 77, 81), +(3001, 14193, 200, 77, 81), +(3001, 14194, 200, 77, 81), +(3001, 15369, 200, 77, 254), +(3001, 15370, 200, 77, 254), +(3001, 15371, 200, 77, 254), +(3001, 15375, 200, 78, 82), +(3001, 15376, 200, 78, 82), +(3001, 15377, 200, 78, 82), +(3001, 15359, 200, 79, 83), +(3001, 15357, 200, 79, 83), +(3001, 15358, 200, 79, 83), +(3001, 15379, 200, 80, 84), +(3001, 15360, 200, 80, 84), +(3001, 15380, 200, 80, 84), +(3001, 15361, 200, 80, 84), +(3001, 15362, 200, 80, 84), +(3001, 15378, 200, 80, 84), +(3001, 19537, 200, 81, 254), +(3001, 19538, 200, 81, 254), +(3001, 19516, 200, 81, 254), +(3001, 19539, 200, 81, 254), +(3001, 19517, 200, 81, 254), +(3001, 19518, 200, 81, 254), +(3001, 18213, 200, 82, 254), +(3001, 18214, 200, 82, 254), +(3001, 18215, 200, 82, 254), +(3001, 19555, 200, 83, 254), +(3001, 19556, 200, 83, 254), +(3001, 19557, 200, 83, 254), +(3001, 19914, 200, 83, 254), +(3001, 19915, 200, 83, 254), +(3001, 19916, 200, 83, 254), +(3001, 19553, 200, 84, 254), +(3001, 19554, 200, 84, 254), +(3001, 19528, 200, 84, 254), +(3001, 19529, 200, 84, 254), +(3001, 19552, 200, 84, 254), +(3001, 19530, 200, 84, 254), +(3001, 22556, 200, 85, 254), +(3001, 19531, 200, 85, 254), +(3001, 22557, 200, 85, 254), +(3001, 19532, 200, 85, 254), +(3001, 22558, 200, 85, 254), +(3001, 19533, 200, 85, 254), +(3001, 19549, 200, 85, 254), +(3001, 19550, 200, 85, 254), +(3001, 19917, 200, 85, 254), +(3001, 19551, 200, 85, 254), +(3001, 19918, 200, 85, 254), +(3001, 19919, 200, 85, 254), +(3002, 33000, 200, 1, 254), +(3003, 4585, 200, 51, 254), +(3003, 4587, 200, 54, 254), +(3003, 4500, 200, 55, 254), +(3003, 7004, 200, 56, 60), +(3003, 4590, 200, 59, 254), +(3003, 4518, 200, 60, 254), +(3003, 6731, 200, 61, 68), +(3003, 6663, 200, 69, 72), +(3003, 11854, 200, 73, 77), +(3003, 11855, 200, 73, 77), +(3003, 11856, 200, 73, 77), +(3003, 14987, 200, 78, 82), +(3003, 14988, 200, 78, 82), +(3003, 14989, 200, 78, 82), +(3003, 19103, 200, 83, 254), +(3003, 19131, 200, 83, 254), +(3003, 19132, 200, 83, 254), +(3003, 19133, 200, 83, 254), +(3003, 22665, 200, 83, 254), +(3003, 22666, 200, 83, 254), +(3003, 22667, 200, 83, 254), +(3003, 19101, 200, 83, 254), +(3003, 19102, 200, 83, 254), +(3004, 33000, 200, 1, 254), +(3004, 4585, 200, 51, 254), +(3004, 4587, 200, 54, 254), +(3004, 4506, 200, 55, 79), +(3004, 4519, 200, 60, 254), +(3004, 8019, 200, 69, 254), +(3004, 10086, 200, 72, 76), +(3004, 10087, 200, 72, 76), +(3004, 10088, 200, 72, 76), +(3004, 15020, 200, 77, 81), +(3004, 15021, 200, 77, 81), +(3004, 15022, 200, 77, 81), +(3004, 15091, 200, 80, 84), +(3004, 15092, 200, 80, 84), +(3004, 15093, 200, 80, 84), +(3004, 19153, 200, 82, 254), +(3004, 19154, 200, 82, 254), +(3004, 19152, 200, 82, 254), +(3004, 19223, 200, 85, 254), +(3004, 19224, 200, 85, 254), +(3004, 19225, 200, 85, 254), +(3005, 4585, 200, 51, 254), +(3005, 4587, 200, 54, 254), +(3005, 4520, 200, 55, 254), +(3005, 7005, 200, 56, 60), +(3005, 4590, 200, 59, 254), +(3005, 4504, 200, 60, 254), +(3005, 6741, 200, 61, 68), +(3005, 6673, 200, 69, 72), +(3005, 11866, 200, 73, 77), +(3005, 11867, 200, 73, 77), +(3005, 11868, 200, 73, 77), +(3005, 10306, 200, 75, 77), +(3005, 10307, 200, 75, 77), +(3005, 10308, 200, 75, 77), +(3005, 15223, 200, 78, 79), +(3005, 15224, 200, 78, 79), +(3005, 15225, 200, 78, 79), +(3005, 15211, 200, 78, 82), +(3005, 15212, 200, 78, 82), +(3005, 15213, 200, 78, 82), +(3005, 15191, 200, 80, 84), +(3005, 15192, 200, 80, 84), +(3005, 15190, 200, 80, 84), +(3005, 19364, 200, 83, 254), +(3005, 19365, 200, 83, 254), +(3005, 19366, 200, 83, 254), +(3005, 22662, 200, 83, 254), +(3005, 22663, 200, 83, 254), +(3005, 19131, 200, 83, 254), +(3005, 22664, 200, 83, 254), +(3005, 19132, 200, 83, 254), +(3005, 19133, 200, 83, 254), +(3005, 19343, 200, 85, 254), +(3005, 19344, 200, 85, 254), +(3005, 19345, 200, 85, 254), +(3007, 5225, 200, 1, 254), +(3007, 25060, 200, 5, 60), +(3007, 4721, 200, 10, 62), +(3007, 4585, 200, 30, 254), +(3007, 4614, 200, 35, 49), +(3007, 4587, 200, 40, 254), +(3007, 4683, 200, 50, 56), +(3007, 4510, 200, 51, 64), +(3007, 4511, 200, 52, 59), +(3007, 4509, 200, 53, 254), +(3007, 4502, 200, 54, 254), +(3007, 8923, 200, 55, 69), +(3007, 4512, 200, 56, 78), +(3007, 4684, 200, 57, 63), +(3007, 4513, 200, 57, 254), +(3007, 4507, 200, 59, 254), +(3007, 4508, 200, 60, 73), +(3007, 4692, 200, 61, 65), +(3007, 6752, 200, 61, 68), +(3007, 4687, 200, 63, 254), +(3007, 4691, 200, 63, 254), +(3007, 4698, 200, 64, 64), +(3007, 4690, 200, 65, 71), +(3007, 5019, 200, 65, 254), +(3007, 5020, 200, 65, 254), +(3007, 6193, 200, 66, 254), +(3007, 6195, 200, 68, 254), +(3007, 8002, 200, 68, 254), +(3007, 6175, 200, 69, 70), +(3007, 6727, 200, 69, 254), +(3007, 6194, 200, 70, 254), +(3007, 8473, 200, 70, 254), +(3007, 8474, 200, 70, 254), +(3007, 10947, 200, 71, 75), +(3007, 10948, 200, 71, 75), +(3007, 10949, 200, 71, 75), +(3007, 11913, 200, 72, 76), +(3007, 11914, 200, 72, 76), +(3007, 11915, 200, 72, 76), +(3007, 10938, 200, 72, 254), +(3007, 10939, 200, 72, 254), +(3007, 10940, 200, 72, 254), +(3007, 10933, 200, 73, 77), +(3007, 10934, 200, 73, 77), +(3007, 10932, 200, 73, 77), +(3007, 10944, 200, 74, 78), +(3007, 10945, 200, 74, 78), +(3007, 10946, 200, 74, 78), +(3007, 11922, 200, 74, 254), +(3007, 11923, 200, 74, 254), +(3007, 11924, 200, 74, 254), +(3007, 10950, 200, 75, 79), +(3007, 10951, 200, 75, 79), +(3007, 10952, 200, 75, 79), +(3007, 14799, 200, 76, 80), +(3007, 14800, 200, 76, 80), +(3007, 14801, 200, 76, 80), +(3007, 14194, 200, 77, 81), +(3007, 14811, 200, 77, 81), +(3007, 14812, 200, 77, 81), +(3007, 14813, 200, 77, 81), +(3007, 14192, 200, 77, 81), +(3007, 14193, 200, 77, 81), +(3007, 14784, 200, 78, 82), +(3007, 14785, 200, 78, 82), +(3007, 14786, 200, 78, 82), +(3007, 14796, 200, 79, 80), +(3007, 14797, 200, 79, 80), +(3007, 14798, 200, 79, 80), +(3007, 14822, 200, 79, 254), +(3007, 14820, 200, 79, 254), +(3007, 14821, 200, 79, 254), +(3007, 14814, 200, 80, 84), +(3007, 14815, 200, 80, 84), +(3007, 14816, 200, 80, 84), +(3007, 14802, 200, 80, 254), +(3007, 14803, 200, 80, 254), +(3007, 14804, 200, 80, 254), +(3007, 18895, 200, 81, 83), +(3007, 18896, 200, 81, 83), +(3007, 18897, 200, 81, 83), +(3007, 18904, 200, 81, 254), +(3007, 18905, 200, 81, 254), +(3007, 18906, 200, 81, 254), +(3007, 18918, 200, 82, 254), +(3007, 18213, 200, 82, 254), +(3007, 18214, 200, 82, 254), +(3007, 18215, 200, 82, 254), +(3007, 18916, 200, 82, 254), +(3007, 18917, 200, 82, 254), +(3007, 18889, 200, 83, 254), +(3007, 18890, 200, 83, 254), +(3007, 18891, 200, 83, 254), +(3007, 19826, 200, 83, 254), +(3007, 19827, 200, 83, 254), +(3007, 19828, 200, 83, 254), +(3007, 18902, 200, 84, 254), +(3007, 18903, 200, 84, 254), +(3007, 19552, 200, 84, 254), +(3007, 19553, 200, 84, 254), +(3007, 19554, 200, 84, 254), +(3007, 18901, 200, 84, 254), +(3007, 19830, 200, 85, 254), +(3007, 18919, 200, 85, 254), +(3007, 19831, 200, 85, 254), +(3007, 18920, 200, 85, 254), +(3007, 22525, 200, 85, 254), +(3007, 18921, 200, 85, 254), +(3007, 22526, 200, 85, 254), +(3007, 22527, 200, 85, 254), +(3007, 18907, 200, 85, 254), +(3007, 18908, 200, 85, 254), +(3007, 18909, 200, 85, 254), +(3007, 19829, 200, 85, 254), +(3008, 4585, 200, 51, 254), +(3008, 4587, 200, 54, 254), +(3008, 4516, 200, 55, 254), +(3008, 4586, 200, 60, 254), +(3008, 8030, 200, 69, 254), +(3009, 5225, 200, 1, 254), +(3009, 25060, 200, 5, 82), +(3009, 4721, 200, 10, 62), +(3009, 4659, 200, 20, 51), +(3009, 4585, 200, 30, 254), +(3009, 4587, 200, 40, 254), +(3009, 4685, 200, 52, 62), +(3009, 4673, 200, 53, 254), +(3009, 4505, 200, 54, 62), +(3009, 8922, 200, 55, 69), +(3009, 4515, 200, 55, 254), +(3009, 4517, 200, 57, 254), +(3009, 4677, 200, 58, 254), +(3009, 4676, 200, 59, 74), +(3009, 6751, 200, 61, 68), +(3009, 4696, 200, 61, 254), +(3009, 4686, 200, 63, 64), +(3009, 4687, 200, 63, 254), +(3009, 4694, 200, 63, 254), +(3009, 5017, 200, 65, 68), +(3009, 5018, 200, 65, 68), +(3009, 4695, 200, 65, 254), +(3009, 6198, 200, 66, 254), +(3009, 8001, 200, 68, 254), +(3009, 6196, 200, 68, 254), +(3009, 6174, 200, 69, 69), +(3009, 6726, 200, 69, 70), +(3009, 8470, 200, 70, 79), +(3009, 6197, 200, 70, 254), +(3009, 8471, 200, 70, 254), +(3009, 10883, 200, 71, 75), +(3009, 10884, 200, 71, 75), +(3009, 10885, 200, 71, 75), +(3009, 11913, 200, 72, 76), +(3009, 11914, 200, 72, 76), +(3009, 11915, 200, 72, 76), +(3009, 10889, 200, 72, 254), +(3009, 10890, 200, 72, 254), +(3009, 10891, 200, 72, 254), +(3009, 10892, 200, 73, 77), +(3009, 10893, 200, 73, 77), +(3009, 10894, 200, 73, 77), +(3009, 11925, 200, 74, 78), +(3009, 11926, 200, 74, 78), +(3009, 11927, 200, 74, 78), +(3009, 10895, 200, 74, 254), +(3009, 10896, 200, 74, 254), +(3009, 10897, 200, 74, 254), +(3009, 10899, 200, 75, 254), +(3009, 10900, 200, 75, 254), +(3009, 10898, 200, 75, 254), +(3009, 15097, 200, 76, 80), +(3009, 15120, 200, 76, 80), +(3009, 15098, 200, 76, 80), +(3009, 15099, 200, 76, 80), +(3009, 15118, 200, 76, 80), +(3009, 15119, 200, 76, 80), +(3009, 15121, 200, 77, 81), +(3009, 15122, 200, 77, 81), +(3009, 15123, 200, 77, 81), +(3009, 14192, 200, 77, 81), +(3009, 14193, 200, 77, 81), +(3009, 14194, 200, 77, 81), +(3009, 15103, 200, 78, 82), +(3009, 15104, 200, 78, 82), +(3009, 15105, 200, 78, 82), +(3009, 15115, 200, 79, 83), +(3009, 15116, 200, 79, 83), +(3009, 15117, 200, 79, 83), +(3009, 15127, 200, 80, 84), +(3009, 15128, 200, 80, 84), +(3009, 15129, 200, 80, 84), +(3009, 15133, 200, 80, 84), +(3009, 15134, 200, 80, 84), +(3009, 15135, 200, 80, 84), +(3009, 19244, 200, 81, 254), +(3009, 19245, 200, 81, 254), +(3009, 19265, 200, 81, 254), +(3009, 19246, 200, 81, 254), +(3009, 19266, 200, 81, 254), +(3009, 19267, 200, 81, 254), +(3009, 18213, 200, 82, 254), +(3009, 18214, 200, 82, 254), +(3009, 18215, 200, 82, 254), +(3009, 19268, 200, 82, 254), +(3009, 19269, 200, 82, 254), +(3009, 19270, 200, 82, 254), +(3009, 19873, 200, 83, 84), +(3009, 19871, 200, 83, 84), +(3009, 19872, 200, 83, 84), +(3009, 19252, 200, 83, 254), +(3009, 19247, 200, 83, 254), +(3009, 19248, 200, 83, 254), +(3009, 19249, 200, 83, 254), +(3009, 19250, 200, 83, 254), +(3009, 19251, 200, 83, 254), +(3009, 19262, 200, 84, 254), +(3009, 19263, 200, 84, 254), +(3009, 19264, 200, 84, 254), +(3009, 19283, 200, 84, 254), +(3009, 19284, 200, 84, 254), +(3009, 19285, 200, 84, 254), +(3009, 19275, 200, 85, 254), +(3009, 19874, 200, 85, 254), +(3009, 19276, 200, 85, 254), +(3009, 19875, 200, 85, 254), +(3009, 19280, 200, 85, 254), +(3009, 19876, 200, 85, 254), +(3009, 19281, 200, 85, 254), +(3009, 22540, 200, 85, 254), +(3009, 19282, 200, 85, 254), +(3009, 22541, 200, 85, 254), +(3009, 22542, 200, 85, 254), +(3009, 19274, 200, 85, 254), +(3015, 4585, 200, 51, 254), +(3015, 4587, 200, 54, 254), +(3015, 4671, 200, 55, 254), +(3015, 4678, 200, 60, 68), +(3015, 8233, 200, 69, 254), +(3015, 8782, 200, 70, 79), +(3015, 14158, 200, 80, 84), +(3015, 14159, 200, 80, 84), +(3015, 14160, 200, 80, 84), +(3015, 18170, 200, 85, 254), +(3015, 18171, 200, 85, 254), +(3015, 18172, 200, 85, 254), +(3016, 5225, 200, 1, 254), +(3016, 4937, 200, 1, 4), +(3016, 4938, 200, 5, 9), +(3016, 25060, 200, 5, 254), +(3016, 4928, 200, 8, 31), +(3016, 4939, 200, 10, 14), +(3016, 4721, 200, 10, 62), +(3016, 4940, 200, 15, 19), +(3016, 4931, 200, 16, 39), +(3016, 4941, 200, 20, 24), +(3016, 4934, 200, 24, 47), +(3016, 4942, 200, 25, 29), +(3016, 4943, 200, 30, 34), +(3016, 5027, 200, 30, 49), +(3016, 4585, 200, 30, 254), +(3016, 4929, 200, 32, 53), +(3016, 4944, 200, 35, 39), +(3016, 4945, 200, 40, 44), +(3016, 4932, 200, 40, 59), +(3016, 4587, 200, 40, 254), +(3016, 4946, 200, 45, 49), +(3016, 4935, 200, 48, 64), +(3016, 4947, 200, 50, 54), +(3016, 5028, 200, 50, 56), +(3016, 5039, 200, 53, 254), +(3016, 4930, 200, 54, 66), +(3016, 5037, 200, 54, 254), +(3016, 4948, 200, 55, 59), +(3016, 8924, 200, 55, 69), +(3016, 5040, 200, 56, 254), +(3016, 5029, 200, 57, 63), +(3016, 5035, 200, 57, 254), +(3016, 5041, 200, 58, 59), +(3016, 5038, 200, 59, 254), +(3016, 4949, 200, 60, 64), +(3016, 4933, 200, 60, 67), +(3016, 5034, 200, 60, 74), +(3016, 6754, 200, 61, 68), +(3016, 5044, 200, 61, 254), +(3016, 4687, 200, 63, 254), +(3016, 5042, 200, 63, 254), +(3016, 5030, 200, 64, 64), +(3016, 4950, 200, 65, 65), +(3016, 5107, 200, 65, 65), +(3016, 4936, 200, 65, 68), +(3016, 5043, 200, 65, 254), +(3016, 5031, 200, 65, 254), +(3016, 5032, 200, 65, 254), +(3016, 6172, 200, 66, 70), +(3016, 6200, 200, 66, 254), +(3016, 6169, 200, 67, 70), +(3016, 6170, 200, 68, 72), +(3016, 6201, 200, 68, 254), +(3016, 8003, 200, 68, 254), +(3016, 6171, 200, 69, 73), +(3016, 6729, 200, 69, 73), +(3016, 8476, 200, 70, 254), +(3016, 8477, 200, 70, 254), +(3016, 6199, 200, 70, 254), +(3016, 10907, 200, 71, 75), +(3016, 10908, 200, 71, 75), +(3016, 10909, 200, 71, 75), +(3016, 10910, 200, 71, 75), +(3016, 11915, 200, 72, 76), +(3016, 10914, 200, 72, 76), +(3016, 10915, 200, 72, 76), +(3016, 11913, 200, 72, 76), +(3016, 10916, 200, 72, 76), +(3016, 11914, 200, 72, 76), +(3016, 10918, 200, 73, 77), +(3016, 10919, 200, 73, 77), +(3016, 10917, 200, 73, 77), +(3016, 11928, 200, 74, 78), +(3016, 11929, 200, 74, 78), +(3016, 10920, 200, 74, 78), +(3016, 11930, 200, 74, 78), +(3016, 10921, 200, 74, 78), +(3016, 10922, 200, 74, 78), +(3016, 10923, 200, 75, 254), +(3016, 10924, 200, 75, 254), +(3016, 10925, 200, 75, 254), +(3016, 14176, 200, 76, 80), +(3016, 14177, 200, 76, 80), +(3016, 14178, 200, 76, 80), +(3016, 14179, 200, 76, 80), +(3016, 14192, 200, 77, 81), +(3016, 14193, 200, 77, 81), +(3016, 14180, 200, 77, 81), +(3016, 14194, 200, 77, 81), +(3016, 14181, 200, 77, 81), +(3016, 14182, 200, 77, 81), +(3016, 14183, 200, 78, 82), +(3016, 14184, 200, 78, 82), +(3016, 14185, 200, 78, 82), +(3016, 14197, 200, 79, 83), +(3016, 14186, 200, 79, 83), +(3016, 14187, 200, 79, 83), +(3016, 14188, 200, 79, 83), +(3016, 14195, 200, 79, 83), +(3016, 14196, 200, 79, 83), +(3016, 14198, 200, 80, 84), +(3016, 14199, 200, 80, 84), +(3016, 14200, 200, 80, 84), +(3016, 18200, 200, 81, 254), +(3016, 16918, 200, 81, 254), +(3016, 16919, 200, 81, 254), +(3016, 16920, 200, 81, 254), +(3016, 18197, 200, 81, 254), +(3016, 18198, 200, 81, 254), +(3016, 18199, 200, 81, 254), +(3016, 18201, 200, 82, 254), +(3016, 18202, 200, 82, 254), +(3016, 18213, 200, 82, 254), +(3016, 18203, 200, 82, 254), +(3016, 18214, 200, 82, 254), +(3016, 18215, 200, 82, 254), +(3016, 18211, 200, 83, 254), +(3016, 18212, 200, 83, 254), +(3016, 19753, 200, 83, 254), +(3016, 18204, 200, 83, 254), +(3016, 19754, 200, 83, 254), +(3016, 18205, 200, 83, 254), +(3016, 19755, 200, 83, 254), +(3016, 18206, 200, 83, 254), +(3016, 18210, 200, 83, 254), +(3016, 18216, 200, 84, 254), +(3016, 18217, 200, 84, 254), +(3016, 18207, 200, 84, 254), +(3016, 18218, 200, 84, 254), +(3016, 18208, 200, 84, 254), +(3016, 18209, 200, 84, 254), +(3016, 19741, 200, 85, 254), +(3016, 19742, 200, 85, 254), +(3016, 19743, 200, 85, 254), +(3016, 22506, 200, 85, 254), +(3016, 22507, 200, 85, 254), +(3016, 18219, 200, 85, 254), +(3016, 22508, 200, 85, 254), +(3016, 18220, 200, 85, 254), +(3016, 18221, 200, 85, 254); + +DELETE +FROM bot_spells_entries +WHERE NOT EXISTS +(SELECT * +FROM spells_new +WHERE bot_spells_entries.spell_id = spells_new.id); +)" + }, + ManifestEntry{ + .version = 9054, + .description = "2024_12_29_discipline__subtypes_inserts.sql", + .check = "SELECT * FROM `bot_spells_entries` where `type` = 201", + .condition = "empty", + .match = "", + .sql = R"( +INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`, `priority`) +VALUES +(3001, 4672, 201, 53, 254, 3), +(3001, 4514, 201, 54, 254, 3), +(3001, 4501, 201, 57, 254, 3), +(3001, 4675, 201, 58, 59, 3), +(3001, 4498, 201, 60, 254, 3), +(3001, 6750, 201, 61, 68, 3), +(3001, 6192, 201, 68, 73, 3), +(3001, 6725, 201, 69, 73, 3), +(3001, 10970, 201, 73, 254, 1), +(3001, 10968, 201, 73, 254, 3), +(3001, 10969, 201, 73, 254, 2), +(3001, 10971, 201, 74, 78, 3), +(3001, 10972, 201, 74, 78, 2), +(3001, 10973, 201, 74, 78, 1), +(3001, 11917, 201, 74, 254, 2), +(3001, 11918, 201, 74, 254, 1), +(3001, 11916, 201, 74, 254, 3), +(3001, 15369, 201, 77, 254, 3), +(3001, 15370, 201, 77, 254, 2), +(3001, 15371, 201, 77, 254, 1), +(3001, 15375, 201, 78, 82, 3), +(3001, 15376, 201, 78, 82, 2), +(3001, 15377, 201, 78, 82, 1), +(3001, 15359, 201, 79, 83, 1), +(3001, 15357, 201, 79, 83, 3), +(3001, 15358, 201, 79, 83, 2), +(3001, 15379, 201, 80, 84, 2), +(3001, 15380, 201, 80, 84, 1), +(3001, 15378, 201, 80, 84, 3), +(3001, 19555, 201, 83, 254, 3), +(3001, 19556, 201, 83, 254, 2), +(3001, 19557, 201, 83, 254, 1), +(3001, 19553, 201, 84, 254, 2), +(3001, 19554, 201, 84, 254, 1), +(3001, 19528, 201, 84, 254, 3), +(3001, 19529, 201, 84, 254, 2), +(3001, 19552, 201, 84, 254, 3), +(3001, 19530, 201, 84, 254, 1), +(3001, 19549, 201, 85, 254, 3), +(3001, 19550, 201, 85, 254, 2), +(3001, 19551, 201, 85, 254, 1), +(3003, 4500, 201, 55, 254, 3), +(3003, 4590, 201, 59, 254, 3), +(3004, 4506, 201, 55, 79, 3), +(3004, 8019, 201, 69, 254, 3), +(3004, 10086, 201, 72, 76, 3), +(3004, 10087, 201, 72, 76, 2), +(3004, 10088, 201, 72, 76, 1), +(3004, 15020, 201, 77, 81, 3), +(3004, 15021, 201, 77, 81, 2), +(3004, 15022, 201, 77, 81, 1), +(3004, 15091, 201, 80, 84, 3), +(3004, 15092, 201, 80, 84, 2), +(3004, 15093, 201, 80, 84, 1), +(3004, 19153, 201, 82, 254, 2), +(3004, 19154, 201, 82, 254, 1), +(3004, 19152, 201, 82, 254, 3), +(3004, 19223, 201, 85, 254, 3), +(3004, 19224, 201, 85, 254, 2), +(3004, 19225, 201, 85, 254, 1), +(3005, 4520, 201, 55, 254, 3), +(3005, 4590, 201, 59, 254, 3), +(3007, 4511, 201, 52, 59, 3), +(3007, 4512, 201, 56, 78, 3), +(3007, 4513, 201, 57, 254, 3), +(3007, 4507, 201, 59, 254, 3), +(3007, 4508, 201, 60, 73, 3), +(3007, 4691, 201, 63, 254, 3), +(3007, 6194, 201, 70, 254, 3), +(3007, 8473, 201, 70, 254, 3), +(3007, 10944, 201, 74, 78, 3), +(3007, 10945, 201, 74, 78, 2), +(3007, 10946, 201, 74, 78, 1), +(3007, 11922, 201, 74, 254, 3), +(3007, 11923, 201, 74, 254, 2), +(3007, 11924, 201, 74, 254, 1), +(3007, 14796, 201, 79, 80, 3), +(3007, 14797, 201, 79, 80, 2), +(3007, 14798, 201, 79, 80, 1), +(3007, 14822, 201, 79, 254, 1), +(3007, 14820, 201, 79, 254, 3), +(3007, 14821, 201, 79, 254, 2), +(3007, 14814, 201, 80, 84, 3), +(3007, 14815, 201, 80, 84, 2), +(3007, 14816, 201, 80, 84, 1), +(3007, 18895, 201, 81, 83, 3), +(3007, 18896, 201, 81, 83, 2), +(3007, 18897, 201, 81, 83, 1), +(3007, 18902, 201, 84, 254, 2), +(3007, 18903, 201, 84, 254, 1), +(3007, 19552, 201, 84, 254, 3), +(3007, 19553, 201, 84, 254, 2), +(3007, 19554, 201, 84, 254, 1), +(3007, 18901, 201, 84, 254, 3), +(3007, 18919, 201, 85, 254, 3), +(3007, 18920, 201, 85, 254, 2), +(3007, 22525, 201, 85, 254, 3), +(3007, 18921, 201, 85, 254, 1), +(3007, 22526, 201, 85, 254, 2), +(3007, 22527, 201, 85, 254, 1), +(3008, 4586, 201, 60, 254, 3), +(3008, 8030, 201, 69, 254, 3), +(3009, 4659, 201, 20, 51, 3), +(3009, 4685, 201, 52, 62, 3), +(3009, 4505, 201, 54, 62, 3), +(3009, 4517, 201, 57, 254, 3), +(3009, 4677, 201, 58, 254, 3), +(3009, 4676, 201, 59, 74, 3), +(3009, 4696, 201, 61, 254, 3), +(3009, 4686, 201, 63, 64, 3), +(3009, 4694, 201, 63, 254, 3), +(3009, 5017, 201, 65, 68, 3), +(3009, 5018, 201, 65, 68, 3), +(3009, 4695, 201, 65, 254, 3), +(3009, 8001, 201, 68, 254, 3), +(3009, 6196, 201, 68, 254, 3), +(3009, 6174, 201, 69, 69, 3), +(3009, 8470, 201, 70, 79, 3), +(3009, 6197, 201, 70, 254, 3), +(3009, 10892, 201, 73, 77, 3), +(3009, 10893, 201, 73, 77, 2), +(3009, 10894, 201, 73, 77, 1), +(3009, 11925, 201, 74, 78, 3), +(3009, 11926, 201, 74, 78, 2), +(3009, 11927, 201, 74, 78, 1), +(3009, 10899, 201, 75, 254, 2), +(3009, 10900, 201, 75, 254, 1), +(3009, 10898, 201, 75, 254, 3), +(3009, 15103, 201, 78, 82, 3), +(3009, 15104, 201, 78, 82, 2), +(3009, 15105, 201, 78, 82, 1), +(3009, 15115, 201, 79, 83, 3), +(3009, 15116, 201, 79, 83, 2), +(3009, 15117, 201, 79, 83, 1), +(3009, 15133, 201, 80, 84, 3), +(3009, 15134, 201, 80, 84, 2), +(3009, 15135, 201, 80, 84, 1), +(3009, 19252, 201, 83, 254, 1), +(3009, 19250, 201, 83, 254, 3), +(3009, 19251, 201, 83, 254, 2), +(3009, 19262, 201, 84, 254, 3), +(3009, 19263, 201, 84, 254, 2), +(3009, 19264, 201, 84, 254, 1), +(3009, 19280, 201, 85, 254, 3), +(3009, 19281, 201, 85, 254, 2), +(3009, 19282, 201, 85, 254, 1), +(3015, 4678, 201, 60, 68, 3), +(3015, 8233, 201, 69, 254, 3), +(3015, 8782, 201, 70, 79, 3), +(3015, 14158, 201, 80, 84, 3), +(3015, 14159, 201, 80, 84, 2), +(3015, 14160, 201, 80, 84, 1), +(3015, 18170, 201, 85, 254, 3), +(3015, 18171, 201, 85, 254, 2), +(3015, 18172, 201, 85, 254, 1), +(3016, 5027, 201, 30, 49, 3), +(3016, 5028, 201, 50, 56, 3), +(3016, 5039, 201, 53, 254, 3), +(3016, 5037, 201, 54, 254, 3), +(3016, 5029, 201, 57, 63, 3), +(3016, 5035, 201, 57, 254, 3), +(3016, 5041, 201, 58, 59, 3), +(3016, 5034, 201, 60, 74, 3), +(3016, 6754, 201, 61, 68, 3), +(3016, 5030, 201, 64, 64, 3), +(3016, 5043, 201, 65, 254, 3), +(3016, 5031, 201, 65, 254, 3), +(3016, 5032, 201, 65, 254, 3), +(3016, 8003, 201, 68, 254, 3), +(3016, 6729, 201, 69, 73, 3), +(3016, 6199, 201, 70, 254, 3), +(3016, 10914, 201, 72, 76, 3), +(3016, 10915, 201, 72, 76, 2), +(3016, 10916, 201, 72, 76, 1), +(3016, 11928, 201, 74, 78, 3), +(3016, 11929, 201, 74, 78, 2), +(3016, 11930, 201, 74, 78, 1), +(3016, 10923, 201, 75, 254, 3), +(3016, 10924, 201, 75, 254, 2), +(3016, 10925, 201, 75, 254, 1), +(3016, 14180, 201, 77, 81, 3), +(3016, 14181, 201, 77, 81, 2), +(3016, 14182, 201, 77, 81, 1), +(3016, 14197, 201, 79, 83, 1), +(3016, 14195, 201, 79, 83, 3), +(3016, 14196, 201, 79, 83, 2), +(3016, 14198, 201, 80, 84, 3), +(3016, 14199, 201, 80, 84, 2), +(3016, 14200, 201, 80, 84, 1), +(3016, 16918, 201, 81, 254, 3), +(3016, 16919, 201, 81, 254, 2), +(3016, 16920, 201, 81, 254, 1), +(3016, 18201, 201, 82, 254, 3), +(3016, 18202, 201, 82, 254, 2), +(3016, 18203, 201, 82, 254, 1), +(3016, 18216, 201, 84, 254, 3), +(3016, 18217, 201, 84, 254, 2), +(3016, 18218, 201, 84, 254, 1), +(3016, 19741, 201, 85, 254, 3), +(3016, 19742, 201, 85, 254, 2), +(3016, 19743, 201, 85, 254, 1), +(3016, 22506, 201, 85, 254, 3), +(3016, 22507, 201, 85, 254, 2), +(3016, 18219, 201, 85, 254, 3), +(3016, 22508, 201, 85, 254, 1), +(3016, 18220, 201, 85, 254, 2), +(3016, 18221, 201, 85, 254, 1); + +INSERT INTO `bot_spells_entries` (`npc_spells_id`, `spell_id`, `type`, `minlevel`, `maxlevel`, `priority`) +VALUES +(3001, 4587, 202, 40, 254, 3), +(3001, 4503, 202, 52, 254, 3), +(3001, 4499, 202, 55, 64, 3), +(3001, 4674, 202, 56, 254, 3), +(3001, 4670, 202, 59, 254, 3), +(3001, 4688, 202, 65, 71, 3), +(3001, 8000, 202, 68, 254, 3), +(3001, 6190, 202, 70, 254, 3), +(3001, 10965, 202, 72, 254, 3), +(3001, 10966, 202, 72, 254, 2), +(3001, 10967, 202, 72, 254, 1), +(3003, 4587, 202, 54, 254, 3), +(3003, 7004, 202, 56, 60, 3), +(3003, 4518, 202, 60, 254, 3), +(3003, 6731, 202, 61, 68, 3), +(3003, 6663, 202, 69, 72, 3), +(3003, 11854, 202, 73, 77, 3), +(3003, 11855, 202, 73, 77, 2), +(3003, 11856, 202, 73, 77, 1), +(3003, 14987, 202, 78, 82, 3), +(3003, 14988, 202, 78, 82, 2), +(3003, 14989, 202, 78, 82, 1), +(3003, 19103, 202, 83, 254, 1), +(3003, 19131, 202, 83, 254, 3), +(3003, 19132, 202, 83, 254, 2), +(3003, 19133, 202, 83, 254, 1), +(3003, 22665, 202, 83, 254, 3), +(3003, 22666, 202, 83, 254, 2), +(3003, 22667, 202, 83, 254, 1), +(3003, 19101, 202, 83, 254, 3), +(3003, 19102, 202, 83, 254, 2), +(3004, 4587, 202, 54, 254, 3), +(3004, 4519, 202, 60, 254, 3), +(3005, 4587, 202, 54, 254, 3), +(3005, 7005, 202, 56, 60, 3), +(3005, 4504, 202, 60, 254, 3), +(3005, 6741, 202, 61, 68, 3), +(3005, 6673, 202, 69, 72, 3), +(3005, 11866, 202, 73, 77, 3), +(3005, 11867, 202, 73, 77, 2), +(3005, 11868, 202, 73, 77, 1), +(3005, 15211, 202, 78, 82, 3), +(3005, 15212, 202, 78, 82, 2), +(3005, 15213, 202, 78, 82, 1), +(3005, 19364, 202, 83, 254, 3), +(3005, 19365, 202, 83, 254, 2), +(3005, 19366, 202, 83, 254, 1), +(3005, 22662, 202, 83, 254, 3), +(3005, 22663, 202, 83, 254, 2), +(3005, 19131, 202, 83, 254, 3), +(3005, 22664, 202, 83, 254, 1), +(3005, 19132, 202, 83, 254, 2), +(3005, 19133, 202, 83, 254, 1), +(3007, 4587, 202, 40, 254, 3), +(3007, 4510, 202, 51, 64, 3), +(3007, 4509, 202, 53, 254, 3), +(3007, 4502, 202, 54, 254, 3), +(3007, 4690, 202, 65, 71, 3), +(3007, 6195, 202, 68, 254, 3), +(3007, 10938, 202, 72, 254, 3), +(3007, 10939, 202, 72, 254, 2), +(3007, 10940, 202, 72, 254, 1), +(3007, 19830, 202, 85, 254, 2), +(3007, 19831, 202, 85, 254, 1), +(3007, 19829, 202, 85, 254, 3), +(3008, 4587, 202, 54, 254, 3), +(3008, 4516, 202, 55, 254, 3), +(3009, 4587, 202, 40, 254, 3), +(3009, 4673, 202, 53, 254, 3), +(3009, 4515, 202, 55, 254, 3), +(3009, 10889, 202, 72, 254, 3), +(3009, 10890, 202, 72, 254, 2), +(3009, 10891, 202, 72, 254, 1), +(3009, 10895, 202, 74, 254, 3), +(3009, 10896, 202, 74, 254, 2), +(3009, 10897, 202, 74, 254, 1), +(3015, 4587, 202, 54, 254, 3), +(3015, 4671, 202, 55, 254, 3), +(3016, 4587, 202, 40, 254, 3), +(3016, 5040, 202, 56, 254, 3), +(3016, 5038, 202, 59, 254, 3), +(3016, 5043, 202, 65, 254, 3), +(3016, 6200, 202, 66, 254, 3), +(3016, 6201, 202, 68, 254, 3), +(3016, 8003, 202, 68, 254, 3), +(3016, 8476, 202, 70, 254, 3); + +DELETE +FROM bot_spells_entries +WHERE NOT EXISTS +(SELECT * +FROM spells_new +WHERE bot_spells_entries.spell_id = spells_new.id); )" } // -- template; copy/paste this when you need to create a new entry diff --git a/common/database_schema.h b/common/database_schema.h index 9f23001b18..6c0aac9c96 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -406,6 +406,7 @@ namespace DatabaseSchema { static std::vector GetBotTables() { return { + "bot_blocked_buffs", "bot_buffs", "bot_command_settings", "bot_create_combinations", @@ -419,6 +420,7 @@ namespace DatabaseSchema { "bot_pet_buffs", "bot_pet_inventories", "bot_pets", + "bot_settings", "bot_spell_casting_chances", "bot_spell_settings", "bot_spells_entries", diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index 0697739599..b03050a7be 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -145,6 +145,9 @@ namespace Logs { EvolveItem, PositionUpdate, KSM, + BotSettings, + BotSpellChecks, + BotSpellTypeChecks, MaxCategoryID /* Don't Remove this */ }; @@ -248,7 +251,10 @@ namespace Logs { "XTargets", "EvolveItem", "PositionUpdate", - "KSM" // Kernel Samepage Merging + "KSM", // Kernel Samepage Merging + "Bot Settings", + "Bot Spell Checks", + "Bot Spell Type Checks" }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index 4f1eadde7f..331d22a6c7 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -874,6 +874,36 @@ OutF(LogSys, Logs::Detail, Logs::KSM, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogBotSettings(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::BotSettings))\ + OutF(LogSys, Logs::General, Logs::BotSettings, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotSettingsDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::BotSettings))\ + OutF(LogSys, Logs::Detail, Logs::BotSettings, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotSpellChecks(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::BotSpellChecks))\ + OutF(LogSys, Logs::General, Logs::BotSpellChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotSpellChecksDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::BotSpellChecks))\ + OutF(LogSys, Logs::Detail, Logs::BotSpellChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotSpellTypeChecks(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::BotSpellTypeChecks))\ + OutF(LogSys, Logs::General, Logs::BotSpellTypeChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotSpellTypeChecksDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::BotSpellTypeChecks))\ + OutF(LogSys, Logs::Detail, Logs::BotSpellTypeChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.IsLogEnabled(debug_level, log_category))\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ diff --git a/common/repositories/base/base_bot_blocked_buffs_repository.h b/common/repositories/base/base_bot_blocked_buffs_repository.h new file mode 100644 index 0000000000..77b0ee2cdf --- /dev/null +++ b/common/repositories/base/base_bot_blocked_buffs_repository.h @@ -0,0 +1,416 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_BOT_BLOCKED_BUFFS_REPOSITORY_H +#define EQEMU_BASE_BOT_BLOCKED_BUFFS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseBotBlockedBuffsRepository { +public: + struct BotBlockedBuffs { + uint32_t bot_id; + uint32_t spell_id; + uint8_t blocked; + uint8_t blocked_pet; + }; + + static std::string PrimaryKey() + { + return std::string("bot_id"); + } + + static std::vector Columns() + { + return { + "bot_id", + "spell_id", + "blocked", + "blocked_pet", + }; + } + + static std::vector SelectColumns() + { + return { + "bot_id", + "spell_id", + "blocked", + "blocked_pet", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("bot_blocked_buffs"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static BotBlockedBuffs NewEntity() + { + BotBlockedBuffs e{}; + + e.bot_id = 0; + e.spell_id = 0; + e.blocked = 0; + e.blocked_pet = 0; + + return e; + } + + static BotBlockedBuffs GetBotBlockedBuffs( + const std::vector &bot_blocked_buffss, + int bot_blocked_buffs_id + ) + { + for (auto &bot_blocked_buffs : bot_blocked_buffss) { + if (bot_blocked_buffs.bot_id == bot_blocked_buffs_id) { + return bot_blocked_buffs; + } + } + + return NewEntity(); + } + + static BotBlockedBuffs FindOne( + Database& db, + int bot_blocked_buffs_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + bot_blocked_buffs_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + BotBlockedBuffs e{}; + + e.bot_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.spell_id = row[1] ? static_cast(atoi(row[1])) : 0; + e.blocked = row[2] ? static_cast(atoi(row[2])) : 0; + e.blocked_pet = row[3] ? static_cast(atoi(row[3])) : 0; + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int bot_blocked_buffs_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + bot_blocked_buffs_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const BotBlockedBuffs &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[0] + " = " + std::to_string(e.bot_id)); + v.push_back(columns[1] + " = " + std::to_string(e.spell_id)); + v.push_back(columns[2] + " = " + std::to_string(e.blocked)); + v.push_back(columns[3] + " = " + std::to_string(e.blocked_pet)); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.bot_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static BotBlockedBuffs InsertOne( + Database& db, + BotBlockedBuffs e + ) + { + std::vector v; + + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.spell_id)); + v.push_back(std::to_string(e.blocked)); + v.push_back(std::to_string(e.blocked_pet)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.bot_id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.spell_id)); + v.push_back(std::to_string(e.blocked)); + v.push_back(std::to_string(e.blocked_pet)); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BotBlockedBuffs e{}; + + e.bot_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.spell_id = row[1] ? static_cast(atoi(row[1])) : 0; + e.blocked = row[2] ? static_cast(atoi(row[2])) : 0; + e.blocked_pet = row[3] ? static_cast(atoi(row[3])) : 0; + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BotBlockedBuffs e{}; + + e.bot_id = row[0] ? static_cast(atoi(row[0])) : 0; + e.spell_id = row[1] ? static_cast(atoi(row[1])) : 0; + e.blocked = row[2] ? static_cast(atoi(row[2])) : 0; + e.blocked_pet = row[3] ? static_cast(atoi(row[3])) : 0; + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const BotBlockedBuffs &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.spell_id)); + v.push_back(std::to_string(e.blocked)); + v.push_back(std::to_string(e.blocked_pet)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.spell_id)); + v.push_back(std::to_string(e.blocked)); + v.push_back(std::to_string(e.blocked_pet)); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_BOT_BLOCKED_BUFFS_REPOSITORY_H diff --git a/common/repositories/base/base_bot_data_repository.h b/common/repositories/base/base_bot_data_repository.h index 96622c2db8..2cd4b1d041 100644 --- a/common/repositories/base/base_bot_data_repository.h +++ b/common/repositories/base/base_bot_data_repository.h @@ -64,13 +64,6 @@ class BaseBotDataRepository { int16_t poison; int16_t disease; int16_t corruption; - uint32_t show_helm; - uint32_t follow_distance; - uint8_t stop_melee_level; - int32_t expansion_bitmask; - uint8_t enforce_spell_settings; - uint8_t archery_setting; - uint32_t caster_range; }; static std::string PrimaryKey() @@ -126,13 +119,6 @@ class BaseBotDataRepository { "poison", "disease", "corruption", - "show_helm", - "follow_distance", - "stop_melee_level", - "expansion_bitmask", - "enforce_spell_settings", - "archery_setting", - "caster_range", }; } @@ -184,13 +170,6 @@ class BaseBotDataRepository { "poison", "disease", "corruption", - "show_helm", - "follow_distance", - "stop_melee_level", - "expansion_bitmask", - "enforce_spell_settings", - "archery_setting", - "caster_range", }; } @@ -276,13 +255,7 @@ class BaseBotDataRepository { e.poison = 0; e.disease = 0; e.corruption = 0; - e.show_helm = 0; - e.follow_distance = 200; - e.stop_melee_level = 255; - e.expansion_bitmask = -1; - e.enforce_spell_settings = 0; - e.archery_setting = 0; - e.caster_range = 300; + return e; } @@ -364,13 +337,6 @@ class BaseBotDataRepository { e.poison = row[42] ? static_cast(atoi(row[42])) : 0; e.disease = row[43] ? static_cast(atoi(row[43])) : 0; e.corruption = row[44] ? static_cast(atoi(row[44])) : 0; - e.show_helm = row[45] ? static_cast(strtoul(row[45], nullptr, 10)) : 0; - e.follow_distance = row[46] ? static_cast(strtoul(row[46], nullptr, 10)) : 200; - e.stop_melee_level = row[47] ? static_cast(strtoul(row[47], nullptr, 10)) : 255; - e.expansion_bitmask = row[48] ? static_cast(atoi(row[48])) : -1; - e.enforce_spell_settings = row[49] ? static_cast(strtoul(row[49], nullptr, 10)) : 0; - e.archery_setting = row[50] ? static_cast(strtoul(row[50], nullptr, 10)) : 0; - e.caster_range = row[51] ? static_cast(strtoul(row[51], nullptr, 10)) : 300; return e; } @@ -383,14 +349,25 @@ class BaseBotDataRepository { int bot_data_id ) { - auto results = db.QueryDatabase( - fmt::format( + std::string query; + + if (RuleB(Bots, BotSoftDeletes)) { + query = fmt::format( + "UPDATE {} SET name = SUBSTRING(CONCAT(name, '-deleted-', UNIX_TIMESTAMP()), 1, 64) WHERE {} = {}", + TableName(), + PrimaryKey(), + bot_data_id + ); + } + else { + query = fmt::format( "DELETE FROM {} WHERE {} = {}", TableName(), PrimaryKey(), bot_data_id - ) - ); + ); + } + auto results = db.QueryDatabase(query); return (results.Success() ? results.RowsAffected() : 0); } @@ -448,13 +425,6 @@ class BaseBotDataRepository { v.push_back(columns[42] + " = " + std::to_string(e.poison)); v.push_back(columns[43] + " = " + std::to_string(e.disease)); v.push_back(columns[44] + " = " + std::to_string(e.corruption)); - v.push_back(columns[45] + " = " + std::to_string(e.show_helm)); - v.push_back(columns[46] + " = " + std::to_string(e.follow_distance)); - v.push_back(columns[47] + " = " + std::to_string(e.stop_melee_level)); - v.push_back(columns[48] + " = " + std::to_string(e.expansion_bitmask)); - v.push_back(columns[49] + " = " + std::to_string(e.enforce_spell_settings)); - v.push_back(columns[50] + " = " + std::to_string(e.archery_setting)); - v.push_back(columns[51] + " = " + std::to_string(e.caster_range)); auto results = db.QueryDatabase( fmt::format( @@ -521,13 +491,6 @@ class BaseBotDataRepository { v.push_back(std::to_string(e.poison)); v.push_back(std::to_string(e.disease)); v.push_back(std::to_string(e.corruption)); - v.push_back(std::to_string(e.show_helm)); - v.push_back(std::to_string(e.follow_distance)); - v.push_back(std::to_string(e.stop_melee_level)); - v.push_back(std::to_string(e.expansion_bitmask)); - v.push_back(std::to_string(e.enforce_spell_settings)); - v.push_back(std::to_string(e.archery_setting)); - v.push_back(std::to_string(e.caster_range)); auto results = db.QueryDatabase( fmt::format( @@ -602,13 +565,6 @@ class BaseBotDataRepository { v.push_back(std::to_string(e.poison)); v.push_back(std::to_string(e.disease)); v.push_back(std::to_string(e.corruption)); - v.push_back(std::to_string(e.show_helm)); - v.push_back(std::to_string(e.follow_distance)); - v.push_back(std::to_string(e.stop_melee_level)); - v.push_back(std::to_string(e.expansion_bitmask)); - v.push_back(std::to_string(e.enforce_spell_settings)); - v.push_back(std::to_string(e.archery_setting)); - v.push_back(std::to_string(e.caster_range)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -687,13 +643,6 @@ class BaseBotDataRepository { e.poison = row[42] ? static_cast(atoi(row[42])) : 0; e.disease = row[43] ? static_cast(atoi(row[43])) : 0; e.corruption = row[44] ? static_cast(atoi(row[44])) : 0; - e.show_helm = row[45] ? static_cast(strtoul(row[45], nullptr, 10)) : 0; - e.follow_distance = row[46] ? static_cast(strtoul(row[46], nullptr, 10)) : 200; - e.stop_melee_level = row[47] ? static_cast(strtoul(row[47], nullptr, 10)) : 255; - e.expansion_bitmask = row[48] ? static_cast(atoi(row[48])) : -1; - e.enforce_spell_settings = row[49] ? static_cast(strtoul(row[49], nullptr, 10)) : 0; - e.archery_setting = row[50] ? static_cast(strtoul(row[50], nullptr, 10)) : 0; - e.caster_range = row[51] ? static_cast(strtoul(row[51], nullptr, 10)) : 300; all_entries.push_back(e); } @@ -763,13 +712,6 @@ class BaseBotDataRepository { e.poison = row[42] ? static_cast(atoi(row[42])) : 0; e.disease = row[43] ? static_cast(atoi(row[43])) : 0; e.corruption = row[44] ? static_cast(atoi(row[44])) : 0; - e.show_helm = row[45] ? static_cast(strtoul(row[45], nullptr, 10)) : 0; - e.follow_distance = row[46] ? static_cast(strtoul(row[46], nullptr, 10)) : 200; - e.stop_melee_level = row[47] ? static_cast(strtoul(row[47], nullptr, 10)) : 255; - e.expansion_bitmask = row[48] ? static_cast(atoi(row[48])) : -1; - e.enforce_spell_settings = row[49] ? static_cast(strtoul(row[49], nullptr, 10)) : 0; - e.archery_setting = row[50] ? static_cast(strtoul(row[50], nullptr, 10)) : 0; - e.caster_range = row[51] ? static_cast(strtoul(row[51], nullptr, 10)) : 300; all_entries.push_back(e); } @@ -889,13 +831,6 @@ class BaseBotDataRepository { v.push_back(std::to_string(e.poison)); v.push_back(std::to_string(e.disease)); v.push_back(std::to_string(e.corruption)); - v.push_back(std::to_string(e.show_helm)); - v.push_back(std::to_string(e.follow_distance)); - v.push_back(std::to_string(e.stop_melee_level)); - v.push_back(std::to_string(e.expansion_bitmask)); - v.push_back(std::to_string(e.enforce_spell_settings)); - v.push_back(std::to_string(e.archery_setting)); - v.push_back(std::to_string(e.caster_range)); auto results = db.QueryDatabase( fmt::format( @@ -963,13 +898,6 @@ class BaseBotDataRepository { v.push_back(std::to_string(e.poison)); v.push_back(std::to_string(e.disease)); v.push_back(std::to_string(e.corruption)); - v.push_back(std::to_string(e.show_helm)); - v.push_back(std::to_string(e.follow_distance)); - v.push_back(std::to_string(e.stop_melee_level)); - v.push_back(std::to_string(e.expansion_bitmask)); - v.push_back(std::to_string(e.enforce_spell_settings)); - v.push_back(std::to_string(e.archery_setting)); - v.push_back(std::to_string(e.caster_range)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/repositories/base/base_bot_settings_repository.h b/common/repositories/base/base_bot_settings_repository.h new file mode 100644 index 0000000000..8fbaff17d0 --- /dev/null +++ b/common/repositories/base/base_bot_settings_repository.h @@ -0,0 +1,464 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://docs.eqemu.io/developer/repositories + */ + +#ifndef EQEMU_BASE_BOT_SETTINGS_REPOSITORY_H +#define EQEMU_BASE_BOT_SETTINGS_REPOSITORY_H + +#include "../../database.h" +#include "../../strings.h" +#include + +class BaseBotSettingsRepository { +public: + struct BotSettings { + uint32_t character_id; + uint32_t bot_id; + uint8_t stance; + uint16_t setting_id; + uint8_t setting_type; + int32_t value; + std::string category_name; + std::string setting_name; + }; + + static std::string PrimaryKey() + { + return std::string("character_id"); + } + + static std::vector Columns() + { + return { + "character_id", + "bot_id", + "stance", + "setting_id", + "setting_type", + "value", + "category_name", + "setting_name", + }; + } + + static std::vector SelectColumns() + { + return { + "character_id", + "bot_id", + "stance", + "setting_id", + "setting_type", + "value", + "category_name", + "setting_name", + }; + } + + static std::string ColumnsRaw() + { + return std::string(Strings::Implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(Strings::Implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("bot_settings"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static BotSettings NewEntity() + { + BotSettings e{}; + + e.character_id = 0; + e.bot_id = 0; + e.stance = 0; + e.setting_id = 0; + e.setting_type = 0; + e.value = 0; + e.category_name = ""; + e.setting_name = ""; + + return e; + } + + static BotSettings GetBotSettings( + const std::vector &bot_settingss, + int bot_settings_id + ) + { + for (auto &bot_settings : bot_settingss) { + if (bot_settings.character_id == bot_settings_id) { + return bot_settings; + } + } + + return NewEntity(); + } + + static BotSettings FindOne( + Database& db, + int bot_settings_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {} = {} LIMIT 1", + BaseSelect(), + PrimaryKey(), + bot_settings_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + BotSettings e{}; + + e.character_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.bot_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stance = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.setting_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.setting_type = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.value = row[5] ? static_cast(atoi(row[5])) : 0; + e.category_name = row[6] ? row[6] : ""; + e.setting_name = row[7] ? row[7] : ""; + + return e; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int bot_settings_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + bot_settings_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + const BotSettings &e + ) + { + std::vector v; + + auto columns = Columns(); + + v.push_back(columns[0] + " = " + std::to_string(e.character_id)); + v.push_back(columns[1] + " = " + std::to_string(e.bot_id)); + v.push_back(columns[2] + " = " + std::to_string(e.stance)); + v.push_back(columns[3] + " = " + std::to_string(e.setting_id)); + v.push_back(columns[4] + " = " + std::to_string(e.setting_type)); + v.push_back(columns[5] + " = " + std::to_string(e.value)); + v.push_back(columns[6] + " = '" + Strings::Escape(e.category_name) + "'"); + v.push_back(columns[7] + " = '" + Strings::Escape(e.setting_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + Strings::Implode(", ", v), + PrimaryKey(), + e.character_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static BotSettings InsertOne( + Database& db, + BotSettings e + ) + { + std::vector v; + + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.stance)); + v.push_back(std::to_string(e.setting_id)); + v.push_back(std::to_string(e.setting_type)); + v.push_back(std::to_string(e.value)); + v.push_back("'" + Strings::Escape(e.category_name) + "'"); + v.push_back("'" + Strings::Escape(e.setting_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + Strings::Implode(",", v) + ) + ); + + if (results.Success()) { + e.character_id = results.LastInsertedID(); + return e; + } + + e = NewEntity(); + + return e; + } + + static int InsertMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.stance)); + v.push_back(std::to_string(e.setting_id)); + v.push_back(std::to_string(e.setting_type)); + v.push_back(std::to_string(e.value)); + v.push_back("'" + Strings::Escape(e.category_name) + "'"); + v.push_back("'" + Strings::Escape(e.setting_name) + "'"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BotSettings e{}; + + e.character_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.bot_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stance = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.setting_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.setting_type = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.value = row[5] ? static_cast(atoi(row[5])) : 0; + e.category_name = row[6] ? row[6] : ""; + e.setting_name = row[7] ? row[7] : ""; + + all_entries.push_back(e); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, const std::string &where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + BotSettings e{}; + + e.character_id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.bot_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.stance = row[2] ? static_cast(strtoul(row[2], nullptr, 10)) : 0; + e.setting_id = row[3] ? static_cast(strtoul(row[3], nullptr, 10)) : 0; + e.setting_type = row[4] ? static_cast(strtoul(row[4], nullptr, 10)) : 0; + e.value = row[5] ? static_cast(atoi(row[5])) : 0; + e.category_name = row[6] ? row[6] : ""; + e.setting_name = row[7] ? row[7] : ""; + + all_entries.push_back(e); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, const std::string &where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int64 GetMaxId(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COALESCE(MAX({}), 0) FROM {}", + PrimaryKey(), + TableName() + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static int64 Count(Database& db, const std::string &where_filter = "") + { + auto results = db.QueryDatabase( + fmt::format( + "SELECT COUNT(*) FROM {} {}", + TableName(), + (where_filter.empty() ? "" : "WHERE " + where_filter) + ) + ); + + return (results.Success() && results.begin()[0] ? strtoll(results.begin()[0], nullptr, 10) : 0); + } + + static std::string BaseReplace() + { + return fmt::format( + "REPLACE INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static int ReplaceOne( + Database& db, + const BotSettings &e + ) + { + std::vector v; + + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.stance)); + v.push_back(std::to_string(e.setting_id)); + v.push_back(std::to_string(e.setting_type)); + v.push_back(std::to_string(e.value)); + v.push_back("'" + Strings::Escape(e.category_name) + "'"); + v.push_back("'" + Strings::Escape(e.setting_name) + "'"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseReplace(), + Strings::Implode(",", v) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int ReplaceMany( + Database& db, + const std::vector &entries + ) + { + std::vector insert_chunks; + + for (auto &e: entries) { + std::vector v; + + v.push_back(std::to_string(e.character_id)); + v.push_back(std::to_string(e.bot_id)); + v.push_back(std::to_string(e.stance)); + v.push_back(std::to_string(e.setting_id)); + v.push_back(std::to_string(e.setting_type)); + v.push_back(std::to_string(e.value)); + v.push_back("'" + Strings::Escape(e.category_name) + "'"); + v.push_back("'" + Strings::Escape(e.setting_name) + "'"); + + insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); + } + + std::vector v; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseReplace(), + Strings::Implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } +}; + +#endif //EQEMU_BASE_BOT_SETTINGS_REPOSITORY_H diff --git a/common/repositories/bot_blocked_buffs_repository.h b/common/repositories/bot_blocked_buffs_repository.h new file mode 100644 index 0000000000..be6466cbbe --- /dev/null +++ b/common/repositories/bot_blocked_buffs_repository.h @@ -0,0 +1,50 @@ +#ifndef EQEMU_BOT_BLOCKED_BUFFS_REPOSITORY_H +#define EQEMU_BOT_BLOCKED_BUFFS_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_bot_blocked_buffs_repository.h" + +class BotBlockedBuffsRepository: public BaseBotBlockedBuffsRepository { +public: + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * BotBlockedBuffsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * BotBlockedBuffsRepository::GetWhereNeverExpires() + * BotBlockedBuffsRepository::GetWhereXAndY() + * BotBlockedBuffsRepository::DeleteWhereXAndY() + * + * Most of the above could be covered by base methods, but if you as a developer + * find yourself re-using logic for other parts of the code, its best to just make a + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_BOT_BLOCKED_BUFFS_REPOSITORY_H diff --git a/common/repositories/bot_data_repository.h b/common/repositories/bot_data_repository.h index f6508479f1..e8d0038a44 100644 --- a/common/repositories/bot_data_repository.h +++ b/common/repositories/bot_data_repository.h @@ -44,46 +44,6 @@ class BotDataRepository: public BaseBotDataRepository { */ // Custom extended repository methods here - static bool SaveAllHelmAppearances(Database& db, const uint32 owner_id, const bool show_flag) - { - auto results = db.QueryDatabase( - fmt::format( - "UPDATE `{}` SET `show_helm` = {} WHERE `owner_id` = {}", - TableName(), - show_flag ? 1 : 0, - owner_id - ) - ); - - return results.Success(); - } - - static bool ToggleAllHelmAppearances(Database& db, const uint32 owner_id) - { - auto results = db.QueryDatabase( - fmt::format( - "UPDATE `{}` SET `show_helm` = (`show_helm` XOR '1') WHERE `owner_id` = {}", - TableName(), - owner_id - ) - ); - - return results.Success(); - } - - static bool SaveAllFollowDistances(Database& db, const uint32 owner_id, const uint32 follow_distance) - { - auto results = db.QueryDatabase( - fmt::format( - "UPDATE `{}` SET `follow_distance` = {} WHERE `owner_id` = {}", - TableName(), - follow_distance, - owner_id - ) - ); - - return results.Success(); - } }; #endif //EQEMU_BOT_DATA_REPOSITORY_H diff --git a/common/repositories/bot_settings_repository.h b/common/repositories/bot_settings_repository.h new file mode 100644 index 0000000000..7cf1d0dbd1 --- /dev/null +++ b/common/repositories/bot_settings_repository.h @@ -0,0 +1,50 @@ +#ifndef EQEMU_BOT_SETTINGS_REPOSITORY_H +#define EQEMU_BOT_SETTINGS_REPOSITORY_H + +#include "../database.h" +#include "../strings.h" +#include "base/base_bot_settings_repository.h" + +class BotSettingsRepository: public BaseBotSettingsRepository { +public: + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * BotSettingsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * BotSettingsRepository::GetWhereNeverExpires() + * BotSettingsRepository::GetWhereXAndY() + * BotSettingsRepository::DeleteWhereXAndY() + * + * Most of the above could be covered by base methods, but if you as a developer + * find yourself re-using logic for other parts of the code, its best to just make a + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_BOT_SETTINGS_REPOSITORY_H diff --git a/common/ruletypes.h b/common/ruletypes.h index 2b52e081d1..71b5c4ebdc 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -382,6 +382,9 @@ RULE_BOOL(Map, MobZVisualDebug, false, "Displays spell effects determining wheth RULE_BOOL(Map, MobPathingVisualDebug, false, "Displays nodes in pathing points in realtime to help with visual debugging") RULE_REAL(Map, FixPathingZMaxDeltaSendTo, 20, "At runtime in SendTo: maximum change in Z to allow the BestZ code to apply") RULE_INT(Map, FindBestZHeightAdjust, 1, "Adds this to the current Z before seeking the best Z position") +RULE_BOOL(Map, CheckForLoSCheat, false, "Runs predefined zone checks to check for LoS cheating through doors and such.") +RULE_BOOL(Map, EnableLoSCheatExemptions, false, "Enables exemptions for the LoS Cheat check.") +RULE_REAL(Map, RangeCheckForLoSCheat, 20.0, "Default 20.0. Range to check if one is within range of a door.") RULE_CATEGORY_END() RULE_CATEGORY(Pathing) @@ -754,13 +757,13 @@ RULE_INT(Bots, CommandSpellRank, 1, "Filters bot command spells by rank. 1, 2 an RULE_INT(Bots, CreationLimit, 150, "Number of bots that each account can create") RULE_BOOL(Bots, FinishBuffing, false, "Allow for buffs to complete even if the bot caster is out of mana. Only affects buffing out of combat") RULE_BOOL(Bots, GroupBuffing, false, "Bots will cast single target buffs as group buffs, default is false for single. Does not make single target buffs work for MGB") +RULE_BOOL(Bots, RaidBuffing, false, "Bots will cast single target buffs as raid buffs, default is false for single. Does not make single target buffs work for MGB") RULE_INT(Bots, HealRotationMaxMembers, 24, "Maximum number of heal rotation members") RULE_INT(Bots, HealRotationMaxTargets, 12, "Maximum number of heal rotation targets") RULE_REAL(Bots, ManaRegen, 2.0, "Adjust mana regen. Acts as a final multiplier, stacks with Rule Character:ManaRegenMultiplier.") RULE_BOOL(Bots, PreferNoManaCommandSpells, true, "Give sorting priority to newer no-mana spells (i.e., 'Bind Affinity')") RULE_BOOL(Bots, QuestableSpawnLimit, false, "Optional quest method to manage bot spawn limits using the quest_globals name bot_spawn_limit, see: /bazaar/Aediles_Thrall.pl") RULE_INT(Bots, SpawnLimit, 71, "Number of bots a character can have spawned at one time, You + 71 bots is a 12 group pseudo-raid") -RULE_BOOL(Bots, BotGroupXP, false, "Determines whether client gets experience for bots outside their group") RULE_BOOL(Bots, BotLevelsWithOwner, false, "Auto-updates spawned bots as owner levels/de-levels (false is original behavior)") RULE_INT(Bots, BotCharacterLevel, 0, "If level is greater that value player can spawn bots if BotCharacterLevelEnabled is true") RULE_INT(Bots, CasterStopMeleeLevel, 13, "Level at which caster bots stop melee attacks") @@ -783,6 +786,119 @@ RULE_BOOL(Bots, CanClickMageEpicV1, true, "Whether or not bots are allowed to cl RULE_BOOL(Bots, BotsIgnoreLevelBasedHasteCaps, false, "Ignores hard coded level based haste caps.") RULE_INT(Bots, BotsHasteCap, 100, "Haste cap for non-v3(over haste) haste") RULE_INT(Bots, BotsHastev3Cap, 25, "Haste cap for v3(over haste) haste") +RULE_BOOL(Bots, CrossRaidBuffingAndHealing, true, "If True, bots will be able to cast on all raid members rather than just their raid group members. Default true.") +RULE_BOOL(Bots, CanCastIllusionsOnPets, false, "If True, bots will be able to cast spells that have an illusion effect on pets. Default false.") +RULE_BOOL(Bots, CanCastPetOnlyOnOthersPets, false, "If True, bots will be able to cast pet only spells on other's pets. Default false.") +RULE_BOOL(Bots, RequirePetAffinity, true, "If True, bots will be need to have the Pet Affinity AA to allow their pets to be hit with group spells.") +RULE_INT(Bots, SpellResistLimit, 150, "150 Default. This is the resist cap where bots will refuse to cast spells on enemies due to a high resist chance.") +RULE_INT(Bots, StunCastChanceIfCasting, 50, "50 Default. Chance for non-Paladins to cast a stun spell if the target is casting.") +RULE_INT(Bots, StunCastChanceNormal, 15, "15 Default. Chance for non-Paladins to cast a stun spell on the target.") +RULE_INT(Bots, StunCastChancePaladins, 75, "75 Default. Chance for Paladins to cast a stun spell if the target is casting.") +RULE_INT(Bots, PercentChanceToCastAEs, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastNuke, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastGroupHeal, 90, "The chance for a bot to attempt to cast the given spell type in combat. Default 90%.") +RULE_INT(Bots, PercentChanceToCastHeal, 90, "The chance for a bot to attempt to cast the given spell type in combat. Default 90%.") +RULE_INT(Bots, PercentChanceToCastRoot, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastBuff, 90, "The chance for a bot to attempt to cast the given spell type in combat. Default 90%.") +RULE_INT(Bots, PercentChanceToCastEscape, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastLifetap, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastSnare, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastDOT, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastDispel, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastInCombatBuff, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastHateLine, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastMez, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastSlow, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastDebuff, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastCure, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastGroupCure, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastHateRedux, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastFear, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +RULE_INT(Bots, PercentChanceToCastOtherType, 90, "The chance for a bot to attempt to cast the remaining spell types in combat. Default 0-%.") +RULE_INT(Bots, MinDelayBetweenInCombatCastAttempts, 500, "The minimum delay in milliseconds between cast attempts while in-combat. This is rolled between the min and max. Default 500ms.") +RULE_INT(Bots, MaxDelayBetweenInCombatCastAttempts, 2000, "The maximum delay in milliseconds between cast attempts while in-combat. This is rolled between the min and max. Default 2500ms.") +RULE_INT(Bots, MinDelayBetweenOutCombatCastAttempts, 1000, "The minimum delay in milliseconds between cast attempts while out-of-combat. This is rolled between the min and max. Default 1000ms.") +RULE_INT(Bots, MaxDelayBetweenOutCombatCastAttempts, 2500, "The maximum delay in milliseconds between cast attempts while out-of-combat. This is rolled between the min and max. Default 2500ms.") +RULE_INT(Bots, MezChance, 35, "35 Default. Chance for a bot to attempt to Mez a target after validating it is eligible.") +RULE_INT(Bots, AEMezChance, 35, "35 Default. Chance for a bot to attempt to AE Mez targets after validating they are eligible.") +RULE_INT(Bots, MezSuccessDelay, 3500, "3500 (3.5 sec) Default. Delay between successful Mez attempts.") +RULE_INT(Bots, AEMezSuccessDelay, 5000, "5000 (5 sec) Default. Delay between successful AEMez attempts.") +RULE_INT(Bots, MezFailDelay, 2000, "2000 (2 sec) Default. Delay between failed Mez attempts.") +RULE_INT(Bots, MezAEFailDelay, 4000, "4000 (4 sec) Default. Delay between failed AEMez attempts.") +RULE_INT(Bots, MinGroupHealTargets, 3, "Minimum number of targets in valid range that are required for a group heal to cast. Default 3.") +RULE_INT(Bots, MinGroupCureTargets, 3, "Minimum number of targets in valid range that are required for a cure heal to cast. Default 3.") +RULE_INT(Bots, MinTargetsForAESpell, 3, "Minimum number of targets in valid range that are required for an AE spell to cast. Default 3.") +RULE_INT(Bots, MinTargetsForGroupSpell, 3, "Minimum number of targets in valid range that are required for an group spell to cast. Default 3.") +RULE_BOOL(Bots, AllowBuffingHealingFamiliars, false, "Determines if bots are allowed to buff and heal familiars. Default false.") +RULE_BOOL(Bots, RunSpellTypeChecksOnSpawn, false, "This will run a serious of checks on spell types and output errors to LogBotSpellTypeChecks") +RULE_BOOL(Bots, UseParentSpellTypeForChecks, true, "This will check only the parent instead of AE/Group/Pet types (ex: AENukes/AERains/PBAENukes fall under Nukes or PetBuffs fall under buffs) when RunSpellTypeChecksOnSpawn fires") +RULE_BOOL(Bots, AllowForcedCastsBySpellID, true, "If enabled, players can use ^cast spellid # to cast a specific spell by ID that is in their spell list") +RULE_BOOL(Bots, AllowCastAAs, true, "If enabled, players can use ^cast aa to cast a clickable AA") +RULE_BOOL(Bots, AllowMagicianEpicPet, false, "If enabled, magician bots can summon their epic pets following the rules AllowMagicianEpicPetLevel") +RULE_INT(Bots, AllowMagicianEpicPetLevel, 50, "If AllowMagicianEpicPet is enabled, bots can start using their epic pets at this level") +RULE_INT(Bots, RequiredMagicianEpicPetItemID, 28034, "If AllowMagicianEpicPet is enabled and this is set, bots will be required to have this item ID equipped to cast their epic. Takes in to account AllowMagicianEpicPetLevel as well. Set to 0 to disable requirement") +RULE_STRING(Bots, EpicPetSpellName, "", "'teleport_zone' in the spell to be cast for epic pets. This must be in their spell list to cast.") +RULE_INT(Bots, ReclaimEnergySpellID, 331, "Spell ID for reclaim energy when using ^petsettype. Default 331") +RULE_BOOL(Bots, UseSpellPulling, true, "If enabled bots will use a spell to pull when within range. Uses PullSpellID.") +RULE_INT(Bots, PullSpellID, 5225, "Default 5225 - Throw Stone. Spell that will be cast to pull by bots") +RULE_BOOL(Bots, AllowRangedPulling, true, "If enabled bots will pull with their ranged items if set to ranged.") +RULE_BOOL(Bots, AllowAISpellPulling, true, "If enabled bots will rely on their detrimental AI to pull when within range.") +RULE_BOOL(Bots, AllowBotEquipAnyClassGear, false, "Allows Bots to wear Equipment even if their class is not valid") +RULE_BOOL(Bots, BotArcheryConsumesAmmo, true, "Set to false to disable Archery Ammo Consumption") +RULE_BOOL(Bots, BotThrowingConsumesAmmo, true, "Set to false to disable Throwing Ammo Consumption") +RULE_INT(Bots, StackSizeMin, 20, "20 Default. -1 to disable and use default max stack size. Minimum stack size to give a bot (Arrows/Throwing).") +RULE_INT(Bots, HasOrMayGetAggroThreshold, 90, "90 Default. Percent threshold of total hate where bots will stop casting spells that generate hate if they are set to try to not pull aggro via spells.") +RULE_BOOL(Bots, UseFlatNormalMeleeRange, false, "False Default. If true, bots melee distance will be a flat distance set by Bots:NormalMeleeRangeDistance.") +RULE_REAL(Bots, NormalMeleeRangeDistance, 0.75, "If UseFlatNormalMeleeRange is enabled, multiplier of the max melee range at which a bot will stand in melee combat. 0.75 Recommended, max melee for all abilities to land.") +RULE_REAL(Bots, PercentMinMeleeDistance, 0.75, "Multiplier of the their melee range - Minimum distance from target a bot will stand while in melee combat before trying to adjust. 0.60 Recommended.") +RULE_REAL(Bots, MaxDistanceForMelee, 20, "Maximum distance bots will stand for melee. Default 20 to allow all special attacks to land.") +RULE_REAL(Bots, TauntNormalMeleeRangeDistance, 0.50, "Multiplier of the max melee range at which a taunting bot will stand in melee combat. 0.50 Recommended, closer than others .") +RULE_REAL(Bots, PercentTauntMinMeleeDistance, 0.40, "Multiplier of their melee range - Minimum distance from target a taunting bot will stand while in melee combat before trying to adjust. 0.25 Recommended.") +RULE_REAL(Bots, PercentMaxMeleeRangeDistance, 0.95, "Multiplier of the max melee range at which a bot will stand in melee combat. 0.95 Recommended, max melee while disabling special attacks/taunt.") +RULE_REAL(Bots, PercentMinMaxMeleeRangeDistance, 0.75, "Multiplier of the closest max melee range at which a bot will stand in melee combat before trying to adjust. 0.75 Recommended, max melee while disabling special attacks/taunt.") +RULE_BOOL(Bots, TauntingBotsFollowTopHate, true, "True Default. If true, bots that are taunting will attempt to stick with whoever currently is top hate.") +RULE_INT(Bots, DistanceTauntingBotsStickMainHate, 10, "If TauntingBotsFollowTopHate is enabled, this is the distance bots will try to stick to whoever currently is Top Hate.") +RULE_BOOL(Bots, DisableSpecialAbilitiesAtMaxMelee, true, "True Default. If true, when bots are at max melee distance, special abilities including taunt will be disabled.") +RULE_INT(Bots, MinJitterTimer, 500, "Minimum ms between bot movement jitter checks.") +RULE_INT(Bots, MaxJitterTimer, 2500, "Maximum ms between bot movement jitter checks. Set to 0 to disable timer checks.") +RULE_BOOL(Bots, PreventBotCampOnFD, true, "True Default. If true, players will not be able to camp bots while feign death.") +RULE_BOOL(Bots, PreventBotSpawnOnFD, true, "True Default. If true, players will not be able to spawn bots while feign death.") +RULE_BOOL(Bots, PreventBotSpawnOnEngaged, true, "True Default. If true, players will not be able to spawn bots while you, your group or raid are engaged.") +RULE_BOOL(Bots, PreventBotCampOnEngaged, true, "True Default. If true, players will not be able to camp bots while you, your group or raid are engaged.") +RULE_BOOL(Bots, CopySettingsOwnBotsOnly, true, "Determines whether a bot you are copying settings from must be a bot you own or not, default true.") +RULE_BOOL(Bots, AllowCopySettingsAnon, false, "If player's are allowed to copy settings of bots owned by anonymous players.") +RULE_BOOL(Bots, AllowCharmedPetBuffs, true, "Whether or not bots are allowed to cast buff charmed pets, default true.") +RULE_BOOL(Bots, AllowCharmedPetHeals, true, "Whether or not bots are allowed to cast heal charmed pets, default true.") +RULE_BOOL(Bots, AllowCharmedPetCures, true, "Whether or not bots are allowed to cast cure charmed pets, default true.") +RULE_BOOL(Bots, ShowResistMessagesToOwner, true, "Default True. If enabled, when a bot's spell is resisted it will send a spell failure to their owner.") +RULE_BOOL(Bots, BotBuffLevelRestrictions, true, "Buffs will not land on low level bots like live players") +RULE_BOOL(Bots, BotsUseLiveBlockedMessage, true, "Setting whether detailed spell block messages should be used for bots as players do on the live servers") +RULE_BOOL(Bots, BotSoftDeletes, true, "When bots are deleted, they are only soft deleted") +RULE_INT(Bots, MinStatusToBypassSpawnLimit, 100, "Minimum status to bypass spawn limit. Default 100.") +RULE_INT(Bots, MinStatusBypassSpawnLimit, 120, "Spawn limit with status bypass. Default 120.") +RULE_INT(Bots, MinStatusToBypassCreateLimit, 100, "Minimum status to bypass create limit. Default 100.") +RULE_INT(Bots, MinStatusBypassCreateLimit, 120, "Create limit with status bypass. Default 120.") +RULE_BOOL(Bots, EnableBotTGB, true, "If enabled bots will cast group buffs as TGB.") +RULE_BOOL(Bots, DoResponseAnimations, true, "If enabled bots will do animations to certain responses or commands.") +RULE_INT(Bots, DefaultFollowDistance, 20, "Default 20. Distance a bot will follow behind.") +RULE_INT(Bots, MaxFollowDistance, 300, "Default 300. Max distance a bot can be set to follow behind.") +RULE_INT(Bots, MaxDistanceRanged, 300, "Default 300. Max distance a bot can be set to ranged.") +RULE_BOOL(Bots, AllowAIMez, true, "If enabled bots will automatically mez/AE mez eligible targets.") +RULE_BOOL(Bots, AllowCommandedCharm, true, "If enabled bots can be commanded to charm NPCs.") +RULE_BOOL(Bots, AllowCommandedMez, true, "If enabled bots can be commanded to mez NPCs.") +RULE_BOOL(Bots, AllowCommandedResurrect, true, "If enabled bots can be commanded to resurrect players.") +RULE_BOOL(Bots, AllowCommandedSummonCorpse, true, "If enabled bots can be commanded to summon other's corpses.") +RULE_BOOL(Bots, AllowCommandedLull, true, "If enabled bots can be commanded to lull targets.") +RULE_INT(Bots, CampTimer, 25, "Number of seconds after /camp has begun before bots camp out.") +RULE_BOOL(Bots, SendClassRaceOnHelp, true, "If enabled a reminder of how to check class/race IDs will be sent when using compatible commands.") +RULE_BOOL(Bots, AllowCrossGroupRaidAssist, true, "If enabled bots will autodefend group or raid members set as main assist.") +RULE_BOOL(Bots, AllowBotBlockedBuffs, true, "If enabled, you can create blocked buffs for each bot and for their pets.") +RULE_STRING(Bots, ZonesWithSpawnLimits, "", "Comma-delimited list of zones where different bot spawn limits apply. This is the max a zone allows.") +RULE_STRING(Bots, ZoneSpawnLimits, "", "Comma-delimited list of spawn limits for zones.") +RULE_STRING(Bots, ZonesWithForcedSpawnLimits, "", "Comma-delimited list of zones where bot spawn limits are forced. This will take priority over any other type of spawn limits.") +RULE_STRING(Bots, ZoneForcedSpawnLimits, "", "Comma-delimited list of forced spawn limits for zones.") +RULE_INT(Bots, AICastSpellTypeDelay, 100, "Delay in milliseconds between AI cast attempts for each spell type. Default 100ms") +RULE_INT(Bots, AICastSpellTypeHeldDelay, 2500, "Delay in milliseconds between AI cast attempts for each spell type that is held or disabled. Default 2500ms (2.5s)") RULE_CATEGORY_END() RULE_CATEGORY(Chat) @@ -1009,6 +1125,7 @@ RULE_CATEGORY_END() RULE_CATEGORY(Command) RULE_BOOL(Command, DyeCommandRequiresDyes, false, "Enable this to require a Prismatic Dye (32557) each time someone uses #dye.") RULE_BOOL(Command, HideMeCommandDisablesTells, true, "Disable this to allow tells to be received when using #hideme.") +RULE_INT(Command, MaxHelpLineLength, 53, "Maximum length of a line before splitting it in to new lines for DiaWind. Default 53.") RULE_CATEGORY_END() RULE_CATEGORY(Doors) diff --git a/common/spdat.cpp b/common/spdat.cpp index 3acac1c8b9..831c7f35ec 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -125,9 +125,14 @@ bool IsMesmerizeSpell(uint16 spell_id) return IsEffectInSpell(spell_id, SE_Mez); } +bool SpellBreaksMez(uint16 spell_id) +{ + return (IsValidSpell(spell_id) && IsDetrimentalSpell(spell_id) && IsAnyDamageSpell(spell_id)); +} + bool IsStunSpell(uint16 spell_id) { - return IsEffectInSpell(spell_id, SE_Stun); + return (IsValidSpell(spell_id) && IsEffectInSpell(spell_id, SE_Stun) || IsEffectInSpell(spell_id, SE_SpinTarget)); } bool IsSummonSpell(uint16 spell_id) @@ -154,6 +159,35 @@ bool IsSummonSpell(uint16 spell_id) bool IsDamageSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if (IsLifetapSpell(spell_id)) { + return false; + } + + const auto& spell = spells[spell_id]; + + for (int i = 0; i < EFFECT_COUNT; i++) { + const auto effect_id = spell.effect_id[i]; + if ( + spell.base_value[i] < 0 && + (effect_id == SE_CurrentHPOnce || effect_id == SE_CurrentHP) + ) { + return true; + } + } + + return false; +} + +bool IsAnyDamageSpell(uint16 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + if (IsLifetapSpell(spell_id)) { return false; } @@ -162,6 +196,7 @@ bool IsDamageSpell(uint16 spell_id) for (int i = 0; i < EFFECT_COUNT; i++) { const auto effect_id = spell.effect_id[i]; + if ( spell.base_value[i] < 0 && ( @@ -179,6 +214,35 @@ bool IsDamageSpell(uint16 spell_id) return false; } +bool IsDamageOverTimeSpell(uint16 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + + if (IsLifetapSpell(spell_id)) { + return false; + } + + const auto& spell = spells[spell_id]; + + if (spell.good_effect || !spell.buff_duration_formula) { + return false; + } + + for (int i = 0; i < EFFECT_COUNT; i++) { + const auto effect_id = spell.effect_id[i]; + if ( + spell.base_value[i] < 0 && + effect_id == SE_CurrentHP && + spell.buff_duration > 1 + ) { + return true; + } + } + + return false; +} bool IsFearSpell(uint16 spell_id) { @@ -409,7 +473,8 @@ bool IsSummonPetSpell(uint16 spell_id) return ( IsEffectInSpell(spell_id, SE_SummonPet) || IsEffectInSpell(spell_id, SE_SummonBSTPet) || - IsEffectInSpell(spell_id, SE_Familiar) + IsEffectInSpell(spell_id, SE_Familiar) || + IsEffectInSpell(spell_id, SE_NecPet) ); } @@ -560,12 +625,11 @@ bool IsPBAENukeSpell(uint16 spell_id) if ( IsPureNukeSpell(spell_id) && - spell.aoe_range > 0 && - spell.target_type == ST_AECaster + !IsTargetRequiredForSpell(spell_id) ) { return true; } - + return false; } @@ -588,6 +652,120 @@ bool IsAERainNukeSpell(uint16 spell_id) return false; } +bool IsAnyNukeOrStunSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if (IsSelfConversionSpell(spell_id) || IsEscapeSpell(spell_id)) { + return false; + } + + if ( + IsPBAENukeSpell(spell_id) || + IsAERainNukeSpell(spell_id) || + IsPureNukeSpell(spell_id) || + IsStunSpell(spell_id) || + (IsDamageSpell(spell_id) && !IsDamageOverTimeSpell(spell_id)) + ) { + return true; + } + + return false; +} + +bool IsAnyAESpell(uint16 spell_id) { + return ( + IsValidSpell(spell_id) && + ( + IsAEDurationSpell(spell_id) || + IsAESpell(spell_id) || + IsAERainNukeSpell(spell_id) || + IsAERainSpell(spell_id) || + IsPBAESpell(spell_id) || + IsPBAENukeSpell(spell_id) + ) + ); +} + +bool IsAESpell(uint16 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + + switch (spells[spell_id].target_type) { + case ST_TargetOptional: + case ST_GroupTeleport : + case ST_Target: + case ST_Self: + case ST_Animal: + case ST_Undead: + case ST_Summoned: + case ST_Tap: + case ST_Pet: + case ST_Corpse: + case ST_Plant: + case ST_Giant: + case ST_Dragon: + case ST_LDoNChest_Cursed: + case ST_Muramite: + case ST_SummonedPet: + case ST_GroupNoPets: + case ST_Group: + case ST_GroupClientAndPet: + case ST_TargetsTarget: + case ST_PetMaster: + return false; + default: + break; + } + + if ( + spells[spell_id].aoe_range > 0 + ) { + return true; + } + + return false; +} + +bool IsPBAESpell(uint16 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + + const auto& spell = spells[spell_id]; + + if ( + spell.aoe_range > 0 && + spell.target_type == ST_AECaster + ) { + return true; + } + + return false; +} + +bool IsAERainSpell(uint16 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + + const auto& spell = spells[spell_id]; + + if ( + spell.aoe_range > 0 && + spell.aoe_duration > 1000 + ) { + return true; + } + + return false; +} + bool IsPartialResistableSpell(uint16 spell_id) { if (!IsValidSpell(spell_id)) { @@ -644,7 +822,9 @@ bool IsGroupSpell(uint16 spell_id) return ( spell.target_type == ST_AEBard || spell.target_type == ST_Group || - spell.target_type == ST_GroupTeleport + spell.target_type == ST_GroupTeleport || + spell.target_type == ST_GroupNoPets || + spell.target_type == ST_GroupClientAndPet ); } @@ -1265,9 +1445,48 @@ bool IsCompleteHealSpell(uint16 spell_id) } return false; + } -bool IsFastHealSpell(uint16 spell_id) +bool IsFastHealSpell(uint16 spell_id) { + spell_id = ( + IsEffectInSpell(spell_id, SE_CurrentHP) ? + spell_id : + GetSpellTriggerSpellID(spell_id, SE_CurrentHP) + ); + + if (!spell_id) { + spell_id = ( + IsEffectInSpell(spell_id, SE_CurrentHPOnce) ? + spell_id : + GetSpellTriggerSpellID(spell_id, SE_CurrentHPOnce) + ); + } + + if (spell_id && IsValidSpell(spell_id)) { + if ( + spells[spell_id].cast_time <= MAX_FAST_HEAL_CASTING_TIME && + spells[spell_id].good_effect && + !IsGroupSpell(spell_id) + ) { + for (int i = 0; i < EFFECT_COUNT; i++) { + if ( + spells[spell_id].base_value[i] > 0 && + ( + spells[spell_id].effect_id[i] == SE_CurrentHP || + spells[spell_id].effect_id[i] == SE_CurrentHPOnce + ) + ) { + return true; + } + } + } + } + + return false; +} + +bool IsVeryFastHealSpell(uint16 spell_id) { spell_id = ( IsEffectInSpell(spell_id, SE_CurrentHP) ? @@ -1285,7 +1504,7 @@ bool IsFastHealSpell(uint16 spell_id) if (spell_id) { if ( - spells[spell_id].cast_time <= MAX_FAST_HEAL_CASTING_TIME && + spells[spell_id].cast_time <= MAX_VERY_FAST_HEAL_CASTING_TIME && spells[spell_id].good_effect && !IsGroupSpell(spell_id) ) { @@ -1306,7 +1525,7 @@ bool IsFastHealSpell(uint16 spell_id) return false; } -bool IsVeryFastHealSpell(uint16 spell_id) +bool IsRegularSingleTargetHealSpell(uint16 spell_id) { spell_id = ( IsEffectInSpell(spell_id, SE_CurrentHP) ? @@ -1324,13 +1543,15 @@ bool IsVeryFastHealSpell(uint16 spell_id) if (spell_id) { if ( - spells[spell_id].cast_time <= MAX_VERY_FAST_HEAL_CASTING_TIME && - spells[spell_id].good_effect && + spells[spell_id].target_type == ST_Target && + !IsCompleteHealSpell(spell_id) && + !IsHealOverTimeSpell(spell_id) && !IsGroupSpell(spell_id) ) { for (int i = 0; i < EFFECT_COUNT; i++) { if ( spells[spell_id].base_value[i] > 0 && + spells[spell_id].buff_duration == 0 && ( spells[spell_id].effect_id[i] == SE_CurrentHP || spells[spell_id].effect_id[i] == SE_CurrentHPOnce @@ -1345,7 +1566,7 @@ bool IsVeryFastHealSpell(uint16 spell_id) return false; } -bool IsRegularSingleTargetHealSpell(uint16 spell_id) +bool IsRegularPetHealSpell(uint16 spell_id) { spell_id = ( IsEffectInSpell(spell_id, SE_CurrentHP) ? @@ -1361,9 +1582,9 @@ bool IsRegularSingleTargetHealSpell(uint16 spell_id) ); } - if (spell_id) { + if (spell_id && IsValidSpell(spell_id)) { if ( - spells[spell_id].target_type == ST_Target && + (spells[spell_id].target_type == ST_Pet || spells[spell_id].target_type == ST_Undead) && !IsCompleteHealSpell(spell_id) && !IsHealOverTimeSpell(spell_id) && !IsGroupSpell(spell_id) @@ -1426,11 +1647,60 @@ bool IsRegularGroupHealSpell(uint16 spell_id) return false; } -bool IsGroupCompleteHealSpell(uint16 spell_id) -{ +bool IsGroupCompleteHealSpell(uint16 spell_id) { + if ( + IsValidSpell(spell_id) && + ( + spell_id == SPELL_COMPLETE_HEAL || + IsEffectInSpell(spell_id, SE_CompleteHeal) || + IsPercentalHealSpell(spell_id) || + GetSpellTriggerSpellID(spell_id, SE_CompleteHeal) + ) && + IsGroupSpell(spell_id) + ) { + return true; + } + + return false; +} + +bool IsGroupHealOverTimeSpell(uint16 spell_id) { + if ( + IsValidSpell(spell_id) && + ( + IsEffectInSpell(spell_id, SE_HealOverTime) || + GetSpellTriggerSpellID(spell_id, SE_HealOverTime) + ) && + IsGroupSpell(spell_id) + ) { + return true; + } + + return false; +} + +bool IsAnyHealSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if (spell_id == SPELL_NATURES_RECOVERY) { + return false; + } + + //spell_id != SPELL_ADRENALINE_SWELL && + //spell_id != SPELL_ADRENALINE_SWELL_RK2 && + //spell_id != SPELL_ADRENALINE_SWELL_RK3 && if ( - IsGroupSpell(spell_id) && - IsCompleteHealSpell(spell_id) + IsHealOverTimeSpell(spell_id) || + IsGroupHealOverTimeSpell(spell_id) || + IsFastHealSpell(spell_id) || + IsVeryFastHealSpell(spell_id) || + IsRegularSingleTargetHealSpell(spell_id) || + IsRegularGroupHealSpell(spell_id) || + IsCompleteHealSpell(spell_id) || + IsGroupCompleteHealSpell(spell_id) || + IsRegularPetHealSpell(spell_id) ) { return true; } @@ -1438,12 +1708,33 @@ bool IsGroupCompleteHealSpell(uint16 spell_id) return false; } -bool IsGroupHealOverTimeSpell(uint16 spell_id) -{ +bool IsAnyBuffSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + if ( - IsGroupSpell(spell_id) && - IsHealOverTimeSpell(spell_id) && - spells[spell_id].buff_duration < 10 + spell_id == SPELL_NATURES_RECOVERY || + IsBuffSpell(spell_id) && + IsBeneficialSpell(spell_id) && + !IsBardSong(spell_id) && + !IsEscapeSpell(spell_id) && + (!IsSummonPetSpell(spell_id) && !IsEffectInSpell(spell_id, SE_TemporaryPets)) + ) { + return true; + } + + return false; +} +bool IsDispelSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if ( + IsEffectInSpell(spell_id, SE_CancelMagic) || + IsEffectInSpell(spell_id, SE_DispelBeneficial) || + IsEffectInSpell(spell_id, SE_DispelBeneficial) ) { return true; } @@ -1451,9 +1742,30 @@ bool IsGroupHealOverTimeSpell(uint16 spell_id) return false; } +bool IsEscapeSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + return ( + IsInvulnerabilitySpell(spell_id) || + IsEffectInSpell(spell_id, SE_FeignDeath) || + IsEffectInSpell(spell_id, SE_DeathSave) || + IsEffectInSpell(spell_id, SE_Destroy) || + ( + IsEffectInSpell(spell_id, SE_WipeHateList) && + spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_WipeHateList)] > 0 + ) + ); +} + bool IsDebuffSpell(uint16 spell_id) { - if ( + if (!IsValidSpell(spell_id)) { + return false; + } + + return !( IsBeneficialSpell(spell_id) || IsHealthSpell(spell_id) || IsStunSpell(spell_id) || @@ -1464,17 +1776,39 @@ bool IsDebuffSpell(uint16 spell_id) IsEffectInSpell(spell_id, SE_CancelMagic) || IsEffectInSpell(spell_id, SE_MovementSpeed) || IsFearSpell(spell_id) || - IsEffectInSpell(spell_id, SE_InstantHate) - ) { + IsEffectInSpell(spell_id, SE_InstantHate) || + IsEffectInSpell(spell_id, SE_TossUp) + ); +} + +bool IsHateReduxSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { return false; } - return true; + return ( + ( + IsEffectInSpell(spell_id, SE_InstantHate) && + spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_InstantHate)] < 0 + ) || + ( + IsEffectInSpell(spell_id, SE_Hate) && + spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_Hate)] < 0 + ) || + ( + IsEffectInSpell(spell_id, SE_ReduceHate) && + spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_ReduceHate)] < 0 + ) + ); } bool IsResistDebuffSpell(uint16 spell_id) { - if ( + if (!IsValidSpell(spell_id)) { + return false; + } + + return ( !IsBeneficialSpell(spell_id) && ( IsEffectInSpell(spell_id, SE_ResistFire) || @@ -1485,42 +1819,34 @@ bool IsResistDebuffSpell(uint16 spell_id) IsEffectInSpell(spell_id, SE_ResistAll) || IsEffectInSpell(spell_id, SE_ResistCorruption) ) - ) { - return true; - } - - return false; + ); } bool IsSelfConversionSpell(uint16 spell_id) { - if ( + if (!IsValidSpell(spell_id)) { + return false; + } + + return ( GetSpellTargetType(spell_id) == ST_Self && IsEffectInSpell(spell_id, SE_CurrentMana) && IsEffectInSpell(spell_id, SE_CurrentHP) && spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_CurrentMana)] > 0 && spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_CurrentHP)] < 0 - ) { - return true; - } - - return false; + ); } // returns true for both detrimental and beneficial buffs bool IsBuffSpell(uint16 spell_id) { - if ( + return ( IsValidSpell(spell_id) && ( spells[spell_id].buff_duration || spells[spell_id].buff_duration_formula ) - ) { - return true; - } - - return false; + ); } bool IsPersistDeathSpell(uint16 spell_id) @@ -2383,7 +2709,7 @@ bool AegolismStackingIsSymbolSpell(uint16 spell_id) { if ((i < 2 && spells[spell_id].effect_id[i] != SE_CHA) || i > 3 && spells[spell_id].effect_id[i] != SE_Blank) { - return 0;; + return 0; } if (i == 2 && spells[spell_id].effect_id[i] == SE_TotalHP) { @@ -2430,3 +2756,196 @@ bool AegolismStackingIsArmorClassSpell(uint16 spell_id) { return 0; } + +int8 SpellEffectsCount(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + int8 x = 0; + + for (int i = 0; i < EFFECT_COUNT; i++) { + if (!IsBlankSpellEffect(spell_id, i)) { + ++x; + } + } + + return x; +} + +bool IsLichSpell(uint16 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + + return ( + GetSpellTargetType(spell_id) == ST_Self && + IsEffectInSpell(spell_id, SE_CurrentMana) && + IsEffectInSpell(spell_id, SE_CurrentHP) && + spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_CurrentMana)] > 0 && + spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_CurrentHP)] < 0 && + spells[spell_id].buff_duration > 0 + ); +} + +bool IsValidSpellAndLoS(uint32 spell_id, bool has_los) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if (!has_los && IsTargetRequiredForSpell(spell_id)) { + return false; + } + + return true; +} + +bool IsInstantHealSpell(uint32 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + return ( + IsRegularSingleTargetHealSpell(spell_id) || + IsRegularGroupHealSpell(spell_id) || + IsRegularPetHealSpell(spell_id) || + IsRegularGroupHealSpell(spell_id) || + spell_id == SPELL_COMPLETE_HEAL + ); +} + +bool IsResurrectSpell(uint16 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + + return IsEffectInSpell(spell_id, SE_Revive); +} + +bool IsResistanceBuffSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + const auto& spell = spells[spell_id]; + + for (int i = 0; i < EFFECT_COUNT; i++) { + if (IsBlankSpellEffect(spell_id, i)) { + continue; + } + + if ( + spell.effect_id[i] == SE_ResistFire || + spell.effect_id[i] == SE_ResistCold || + spell.effect_id[i] == SE_ResistPoison || + spell.effect_id[i] == SE_ResistDisease || + spell.effect_id[i] == SE_ResistMagic || + spell.effect_id[i] == SE_ResistCorruption || + spell.effect_id[i] == SE_ResistAll + ) { + return true; + } + } + + return false; +} + +bool IsResistanceOnlySpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + const auto& spell = spells[spell_id]; + + for (int i = 0; i < EFFECT_COUNT; i++) { + if (IsBlankSpellEffect(spell_id, i)) { + continue; + } + + if ( + spell.effect_id[i] == SE_ResistFire || + spell.effect_id[i] == SE_ResistCold || + spell.effect_id[i] == SE_ResistPoison || + spell.effect_id[i] == SE_ResistDisease || + spell.effect_id[i] == SE_ResistMagic || + spell.effect_id[i] == SE_ResistCorruption || + spell.effect_id[i] == SE_ResistAll + ) { + continue; + } + + return false; + } + + return true; +} + +bool IsDamageShieldOnlySpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + const auto& spell = spells[spell_id]; + + for (int i = 0; i < EFFECT_COUNT; i++) { + if (IsBlankSpellEffect(spell_id, i)) { + continue; + } + + if ( + spell.effect_id[i] != SE_DamageShield + ) { + return false; + } + } + + return true; +} + +bool IsDamageShieldAndResistSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + const auto& spell = spells[spell_id]; + + for (int i = 0; i < EFFECT_COUNT; i++) { + if (IsBlankSpellEffect(spell_id, i)) { + continue; + } + + if ( + spell.effect_id[i] != SE_DamageShield && + spell.effect_id[i] != SE_ResistFire && + spell.effect_id[i] != SE_ResistCold && + spell.effect_id[i] != SE_ResistPoison && + spell.effect_id[i] != SE_ResistDisease && + spell.effect_id[i] != SE_ResistMagic && + spell.effect_id[i] != SE_ResistCorruption && + spell.effect_id[i] != SE_ResistAll + ) { + return false; + } + } + + return true; +} + +bool IsHateSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + return ( + ( + IsEffectInSpell(spell_id, SE_Hate) && + spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_Hate)] > 0 + ) || + ( + IsEffectInSpell(spell_id, SE_InstantHate) && + spells[spell_id].base_value[GetSpellEffectIndex(spell_id, SE_InstantHate)] > 0 + ) + ); +} diff --git a/common/spdat.h b/common/spdat.h index d597c79faf..437910fa4e 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -213,6 +213,10 @@ #define SPELL_BLOODTHIRST 8476 #define SPELL_AMPLIFICATION 2603 #define SPELL_DIVINE_REZ 2738 +#define SPELL_NATURES_RECOVERY 2520 +#define SPELL_ADRENALINE_SWELL 14445 +#define SPELL_ADRENALINE_SWELL_RK2 14446 +#define SPELL_ADRENALINE_SWELL_RK3 14447 // discipline IDs. #define DISC_UNHOLY_AURA 4520 @@ -623,38 +627,294 @@ enum ProcType enum SpellTypes : uint32 { - SpellType_Nuke = (1 << 0), - SpellType_Heal = (1 << 1), - SpellType_Root = (1 << 2), - SpellType_Buff = (1 << 3), - SpellType_Escape = (1 << 4), - SpellType_Pet = (1 << 5), - SpellType_Lifetap = (1 << 6), - SpellType_Snare = (1 << 7), - SpellType_DOT = (1 << 8), - SpellType_Dispel = (1 << 9), - SpellType_InCombatBuff = (1 << 10), - SpellType_Mez = (1 << 11), - SpellType_Charm = (1 << 12), - SpellType_Slow = (1 << 13), - SpellType_Debuff = (1 << 14), - SpellType_Cure = (1 << 15), - SpellType_Resurrect = (1 << 16), - SpellType_HateRedux = (1 << 17), - SpellType_InCombatBuffSong = (1 << 18), // bard in-combat group/ae buffs - SpellType_OutOfCombatBuffSong = (1 << 19), // bard out-of-combat group/ae buffs - SpellType_PreCombatBuff = (1 << 20), - SpellType_PreCombatBuffSong = (1 << 21) + SpellType_Nuke = (1 << 0), + SpellType_Heal = (1 << 1), + SpellType_Root = (1 << 2), + SpellType_Buff = (1 << 3), + SpellType_Escape = (1 << 4), + SpellType_Pet = (1 << 5), + SpellType_Lifetap = (1 << 6), + SpellType_Snare = (1 << 7), + SpellType_DOT = (1 << 8), + SpellType_Dispel = (1 << 9), + SpellType_InCombatBuff = (1 << 10), + SpellType_Mez = (1 << 11), + SpellType_Charm = (1 << 12), + SpellType_Slow = (1 << 13), + SpellType_Debuff = (1 << 14), + SpellType_Cure = (1 << 15), + SpellType_Resurrect = (1 << 16), + SpellType_HateRedux = (1 << 17), + SpellType_InCombatBuffSong = (1 << 18), // bard in-combat group/ae buffs + SpellType_OutOfCombatBuffSong = (1 << 19), // bard out-of-combat group/ae buffs + SpellType_PreCombatBuff = (1 << 20), + SpellType_PreCombatBuffSong = (1 << 21) }; -const uint32 SPELL_TYPE_MIN = (SpellType_Nuke << 1) - 1; -const uint32 SPELL_TYPE_MAX = (SpellType_PreCombatBuffSong << 1) - 1; -const uint32 SPELL_TYPE_ANY = 0xFFFFFFFF; +namespace BotSpellTypes +{ + constexpr uint16 Nuke = 0; + constexpr uint16 RegularHeal = 1; + constexpr uint16 Root = 2; + constexpr uint16 Buff = 3; + constexpr uint16 Escape = 4; + constexpr uint16 Pet = 5; + constexpr uint16 Lifetap = 6; + constexpr uint16 Snare = 7; + constexpr uint16 DOT = 8; + constexpr uint16 Dispel = 9; + constexpr uint16 InCombatBuff = 10; + constexpr uint16 Mez = 11; + constexpr uint16 Charm = 12; + constexpr uint16 Slow = 13; + constexpr uint16 Debuff = 14; + constexpr uint16 Cure = 15; + constexpr uint16 Resurrect = 16; + constexpr uint16 HateRedux = 17; + constexpr uint16 InCombatBuffSong = 18; + constexpr uint16 OutOfCombatBuffSong = 19; + constexpr uint16 PreCombatBuff = 20; + constexpr uint16 PreCombatBuffSong = 21; + constexpr uint16 Fear = 22; + constexpr uint16 Stun = 23; + constexpr uint16 HateLine = 24; + constexpr uint16 GroupCures = 25; + constexpr uint16 CompleteHeal = 26; + constexpr uint16 FastHeals = 27; + constexpr uint16 VeryFastHeals = 28; + constexpr uint16 GroupHeals = 29; + constexpr uint16 GroupCompleteHeals = 30; + constexpr uint16 GroupHoTHeals = 31; + constexpr uint16 HoTHeals = 32; + constexpr uint16 AENukes = 33; + constexpr uint16 AERains = 34; + constexpr uint16 AEMez = 35; + constexpr uint16 AEStun = 36; + constexpr uint16 AEDebuff = 37; + constexpr uint16 AESlow = 38; + constexpr uint16 AESnare = 39; + constexpr uint16 AEFear = 40; + constexpr uint16 AEDispel = 41; + constexpr uint16 AERoot = 42; + constexpr uint16 AEDoT = 43; + constexpr uint16 AELifetap = 44; + constexpr uint16 AEHateLine = 45; + constexpr uint16 PBAENuke = 46; + constexpr uint16 PetBuffs = 47; + constexpr uint16 PetRegularHeals = 48; + constexpr uint16 PetCompleteHeals = 49; + constexpr uint16 PetFastHeals = 50; + constexpr uint16 PetVeryFastHeals = 51; + constexpr uint16 PetHoTHeals = 52; + constexpr uint16 PetCures = 53; + constexpr uint16 DamageShields = 54; + constexpr uint16 ResistBuffs = 55; + constexpr uint16 PetDamageShields = 56; + constexpr uint16 PetResistBuffs = 57; + + // Command Spell Types + constexpr uint16 Teleport = 100; // this is handled by ^depart so uses other logic + constexpr uint16 Lull = 101; + constexpr uint16 Succor = 102; + constexpr uint16 BindAffinity = 103; + constexpr uint16 Identify = 104; + constexpr uint16 Levitate = 105; + constexpr uint16 Rune = 106; + constexpr uint16 WaterBreathing = 107; + constexpr uint16 Size = 108; + constexpr uint16 Invisibility = 109; + constexpr uint16 MovementSpeed = 110; + constexpr uint16 SendHome = 111; + constexpr uint16 SummonCorpse = 112; + constexpr uint16 AELull = 113; + + // Discipline Types + constexpr uint16 Discipline = 200; + constexpr uint16 DiscAggressive = 201; + constexpr uint16 DiscDefensive = 202; + constexpr uint16 DiscUtility = 203; + + constexpr uint16 START = BotSpellTypes::Nuke; // Do not remove or change this + constexpr uint16 END = BotSpellTypes::PetResistBuffs; // Do not remove this, increment as needed + constexpr uint16 COMMANDED_START = BotSpellTypes::Lull; // Do not remove or change this + constexpr uint16 COMMANDED_END = BotSpellTypes::AELull; // Do not remove this, increment as needed + constexpr uint16 DISCIPLINE_START = BotSpellTypes::Discipline; // Do not remove or change this + constexpr uint16 DISCIPLINE_END = BotSpellTypes::DiscUtility; // Do not remove this, increment as needed +} + +static std::map spellType_names = { + { BotSpellTypes::Nuke, "Nuke" }, + { BotSpellTypes::RegularHeal, "Regular Heal" }, + { BotSpellTypes::Root, "Root" }, + { BotSpellTypes::Buff, "Buff" }, + { BotSpellTypes::Escape, "Escape" }, + { BotSpellTypes::Pet, "Pet" }, + { BotSpellTypes::Lifetap, "Lifetap" }, + { BotSpellTypes::Snare, "Snare" }, + { BotSpellTypes::DOT, "DoT" }, + { BotSpellTypes::Dispel, "Dispel" }, + { BotSpellTypes::InCombatBuff, "In-Combat Buff" }, + { BotSpellTypes::Mez, "Mez" }, + { BotSpellTypes::Charm, "Charm" }, + { BotSpellTypes::Slow, "Slow" }, + { BotSpellTypes::Debuff, "Debuff" }, + { BotSpellTypes::Cure, "Cure" }, + { BotSpellTypes::GroupCures, "Group Cure" }, + { BotSpellTypes::PetCures, "Pet Cure" }, + { BotSpellTypes::Resurrect, "Resurrect" }, + { BotSpellTypes::HateRedux, "Hate Reduction" }, + { BotSpellTypes::InCombatBuffSong, "In-Combat Buff Song" }, + { BotSpellTypes::OutOfCombatBuffSong, "Out-of-Combat Buff Song" }, + { BotSpellTypes::PreCombatBuff, "Pre-Combat Buff" }, + { BotSpellTypes::PreCombatBuffSong, "Pre-Combat Buff Song" }, + { BotSpellTypes::Fear, "Fear" }, + { BotSpellTypes::Stun, "Stun" }, + { BotSpellTypes::CompleteHeal, "Complete Heal" }, + { BotSpellTypes::FastHeals, "Fast Heal" }, + { BotSpellTypes::VeryFastHeals, "Very Fast Heal" }, + { BotSpellTypes::GroupHeals, "Group Heal" }, + { BotSpellTypes::GroupCompleteHeals, "Group Complete Heal" }, + { BotSpellTypes::GroupHoTHeals, "Group HoT Heal" }, + { BotSpellTypes::HoTHeals, "HoT Heal" }, + { BotSpellTypes::AENukes, "AE Nuke" }, + { BotSpellTypes::AERains, "AE Rain" }, + { BotSpellTypes::AEMez, "AE Mez" }, + { BotSpellTypes::AEStun, "AE Stun" }, + { BotSpellTypes::AEDebuff, "AE Debuff" }, + { BotSpellTypes::AESlow, "AE Slow" }, + { BotSpellTypes::AESnare, "AE Snare" }, + { BotSpellTypes::AEFear, "AE Fear" }, + { BotSpellTypes::AEDispel, "AE Dispel" }, + { BotSpellTypes::AERoot, "AE Root" }, + { BotSpellTypes::AEDoT, "AE DoT" }, + { BotSpellTypes::AELifetap, "AE Lifetap" }, + { BotSpellTypes::PBAENuke, "PBAE Nuke" }, + { BotSpellTypes::PetBuffs, "Pet Buff" }, + { BotSpellTypes::PetRegularHeals, "Pet Regular Heal" }, + { BotSpellTypes::PetCompleteHeals, "Pet Complete Heal" }, + { BotSpellTypes::PetFastHeals, "Pet Fast Heal" }, + { BotSpellTypes::PetVeryFastHeals, "Pet Very Fast Heal" }, + { BotSpellTypes::PetHoTHeals, "Pet HoT Heal" }, + { BotSpellTypes::DamageShields, "Damage Shield" }, + { BotSpellTypes::ResistBuffs, "Resist Buff" }, + { BotSpellTypes::PetDamageShields, "Pet Damage Shield" }, + { BotSpellTypes::PetResistBuffs, "Pet Resist Buff" }, + { BotSpellTypes::HateLine, "Hate Line" }, + { BotSpellTypes::AEHateLine, "AE Hate Line" }, + { BotSpellTypes::Lull, "Lull" }, + { BotSpellTypes::Teleport, "Teleport" }, + { BotSpellTypes::Succor, "Succor" }, + { BotSpellTypes::BindAffinity, "Bind Affinity" }, + { BotSpellTypes::Identify, "Identify" }, + { BotSpellTypes::Levitate, "Levitate" }, + { BotSpellTypes::Rune, "Rune" }, + { BotSpellTypes::WaterBreathing, "Water Breathing" }, + { BotSpellTypes::Size, "Size" }, + { BotSpellTypes::Invisibility, "Invisibility" }, + { BotSpellTypes::MovementSpeed, "Movement Speed" }, + { BotSpellTypes::SendHome, "Send Home" }, + { BotSpellTypes::SummonCorpse, "Summon Corpse" }, + { BotSpellTypes::AELull, "AE Lull" } +}; + +static std::map spellType_shortNames = { + { BotSpellTypes::Nuke, "nukes" }, + { BotSpellTypes::RegularHeal, "regularheals" }, + { BotSpellTypes::Root, "roots" }, + { BotSpellTypes::Buff, "buffs" }, + { BotSpellTypes::Escape, "escapes" }, + { BotSpellTypes::Pet, "pets" }, + { BotSpellTypes::Lifetap, "lifetaps" }, + { BotSpellTypes::Snare, "snares" }, + { BotSpellTypes::DOT, "dots" }, + { BotSpellTypes::Dispel, "dispels" }, + { BotSpellTypes::InCombatBuff, "incombatbuffs" }, + { BotSpellTypes::Mez, "mez" }, + { BotSpellTypes::Charm, "charms" }, + { BotSpellTypes::Slow, "slows" }, + { BotSpellTypes::Debuff, "debuffs" }, + { BotSpellTypes::Cure, "cures" }, + { BotSpellTypes::GroupCures, "groupcures" }, + { BotSpellTypes::PetCures, "petcures" }, + { BotSpellTypes::Resurrect, "resurrects" }, + { BotSpellTypes::HateRedux, "hateredux" }, + { BotSpellTypes::InCombatBuffSong, "incombatbuffsongs" }, + { BotSpellTypes::OutOfCombatBuffSong, "outofcombatbuffsongs" }, + { BotSpellTypes::PreCombatBuff, "precombatbuffs" }, + { BotSpellTypes::PreCombatBuffSong, "precombatbuffsongs" }, + { BotSpellTypes::Fear, "fears" }, + { BotSpellTypes::Stun, "stuns" }, + { BotSpellTypes::CompleteHeal, "completeheals" }, + { BotSpellTypes::FastHeals, "fastheals" }, + { BotSpellTypes::VeryFastHeals, "veryfastheals" }, + { BotSpellTypes::GroupHeals, "groupheals" }, + { BotSpellTypes::GroupCompleteHeals, "groupcompleteheals" }, + { BotSpellTypes::GroupHoTHeals, "grouphotheals" }, + { BotSpellTypes::HoTHeals, "hotheals" }, + { BotSpellTypes::AENukes, "aenukes" }, + { BotSpellTypes::AERains, "aerains" }, + { BotSpellTypes::AEMez, "aemez" }, + { BotSpellTypes::AEStun, "aestuns" }, + { BotSpellTypes::AEDebuff, "aedebuffs" }, + { BotSpellTypes::AESlow, "aeslows" }, + { BotSpellTypes::AESnare, "aesnares" }, + { BotSpellTypes::AEFear, "aefears" }, + { BotSpellTypes::AEDispel, "aedispels" }, + { BotSpellTypes::AERoot, "aeroots" }, + { BotSpellTypes::AEDoT, "aedots" }, + { BotSpellTypes::AELifetap, "aelifetaps" }, + { BotSpellTypes::PBAENuke, "pbaenukes" }, + { BotSpellTypes::PetBuffs, "petbuffs" }, + { BotSpellTypes::PetRegularHeals, "petregularheals" }, + { BotSpellTypes::PetCompleteHeals, "petcompleteheals" }, + { BotSpellTypes::PetFastHeals, "petfastheals" }, + { BotSpellTypes::PetVeryFastHeals, "petveryfastheals" }, + { BotSpellTypes::PetHoTHeals, "pethotheals" }, + { BotSpellTypes::DamageShields, "damageshields" }, + { BotSpellTypes::ResistBuffs, "resistbuffs" }, + { BotSpellTypes::PetDamageShields, "petdamageshields" }, + { BotSpellTypes::PetResistBuffs, "petresistbuffs" }, + { BotSpellTypes::HateLine, "hatelines" }, + { BotSpellTypes::AEHateLine, "aehatelines" }, + { BotSpellTypes::Lull, "lull" }, + { BotSpellTypes::Teleport, "teleport" }, + { BotSpellTypes::Succor, "succor" }, + { BotSpellTypes::BindAffinity, "bindaffinity" }, + { BotSpellTypes::Identify, "identify" }, + { BotSpellTypes::Levitate, "levitate" }, + { BotSpellTypes::Rune, "rune" }, + { BotSpellTypes::WaterBreathing, "waterbreathing" }, + { BotSpellTypes::Size, "size" }, + { BotSpellTypes::Invisibility, "invisibility" }, + { BotSpellTypes::MovementSpeed, "movementspeed" }, + { BotSpellTypes::SendHome, "sendhome" }, + { BotSpellTypes::SummonCorpse, "summoncorpse" }, + { BotSpellTypes::AELull, "aelull" } +}; const uint32 SPELL_TYPES_DETRIMENTAL = (SpellType_Nuke | SpellType_Root | SpellType_Lifetap | SpellType_Snare | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Charm | SpellType_Debuff | SpellType_Slow); const uint32 SPELL_TYPES_BENEFICIAL = (SpellType_Heal | SpellType_Buff | SpellType_Escape | SpellType_Pet | SpellType_InCombatBuff | SpellType_Cure | SpellType_HateRedux | SpellType_InCombatBuffSong | SpellType_OutOfCombatBuffSong | SpellType_PreCombatBuff | SpellType_PreCombatBuffSong); const uint32 SPELL_TYPES_INNATE = (SpellType_Nuke | SpellType_Lifetap | SpellType_DOT | SpellType_Dispel | SpellType_Mez | SpellType_Slow | SpellType_Debuff | SpellType_Charm | SpellType_Root); +// Bot related functions +bool IsBotSpellTypeDetrimental (uint16 spell_type); +bool IsBotSpellTypeBeneficial (uint16 spell_type); +bool IsBotSpellTypeOtherBeneficial(uint16 spell_type); +bool IsBotSpellTypeInnate (uint16 spell_type); +bool IsAEBotSpellType(uint16 spell_type); +bool IsGroupBotSpellType(uint16 spell_type); +bool IsGroupTargetOnlyBotSpellType(uint16 spell_type); +bool IsPetBotSpellType(uint16 spell_type); +bool IsClientBotSpellType(uint16 spell_type); +bool IsHealBotSpellType(uint16 spell_type); +bool BotSpellTypeRequiresLoS(uint16 spell_type); +bool BotSpellTypeRequiresTarget(uint16 spell_type); +bool BotSpellTypeRequiresAEChecks(uint16 spell_type); +bool IsCommandedBotSpellType(uint16 spell_type); +bool IsPullingBotSpellType(uint16 spell_type); +uint16 GetCorrectBotSpellType(uint16 spell_type, uint16 spell_id); +uint16 GetPetBotSpellType(uint16 spell_type); + // These should not be used to determine spell category.. // They are a graphical affects (effects?) index only // TODO: import sai list @@ -1503,6 +1763,7 @@ bool IsTargetableAESpell(uint16 spell_id); bool IsSacrificeSpell(uint16 spell_id); bool IsLifetapSpell(uint16 spell_id); bool IsMesmerizeSpell(uint16 spell_id); +bool SpellBreaksMez(uint16 spell_id); bool IsStunSpell(uint16 spell_id); bool IsSlowSpell(uint16 spell_id); bool IsHasteSpell(uint16 spell_id); @@ -1536,6 +1797,11 @@ bool IsPureNukeSpell(uint16 spell_id); bool IsAENukeSpell(uint16 spell_id); bool IsPBAENukeSpell(uint16 spell_id); bool IsAERainNukeSpell(uint16 spell_id); +bool IsAnyNukeOrStunSpell(uint16 spell_id); +bool IsAnyAESpell(uint16 spell_id); +bool IsAESpell(uint16 spell_id); +bool IsPBAESpell(uint16 spell_id); +bool IsAERainSpell(uint16 spell_id); bool IsPartialResistableSpell(uint16 spell_id); bool IsResistableSpell(uint16 spell_id); bool IsGroupSpell(uint16 spell_id); @@ -1545,8 +1811,11 @@ bool IsEffectInSpell(uint16 spell_id, int effect_id); uint16 GetSpellTriggerSpellID(uint16 spell_id, int effect_id); bool IsBlankSpellEffect(uint16 spell_id, int effect_index); bool IsValidSpell(uint32 spell_id); +bool IsValidSpellAndLoS(uint32 spell_id, bool has_los = true); bool IsSummonSpell(uint16 spell_id); bool IsDamageSpell(uint16 spell_id); +bool IsAnyDamageSpell(uint16 spell_id); +bool IsDamageOverTimeSpell(uint16 spell_i); bool IsFearSpell(uint16 spell_id); bool IsCureSpell(uint16 spell_id); bool IsHarmTouchSpell(uint16 spell_id); @@ -1585,10 +1854,16 @@ bool IsCompleteHealSpell(uint16 spell_id); bool IsFastHealSpell(uint16 spell_id); bool IsVeryFastHealSpell(uint16 spell_id); bool IsRegularSingleTargetHealSpell(uint16 spell_id); +bool IsRegularPetHealSpell(uint16 spell_id); bool IsRegularGroupHealSpell(uint16 spell_id); bool IsGroupCompleteHealSpell(uint16 spell_id); bool IsGroupHealOverTimeSpell(uint16 spell_id); +bool IsAnyHealSpell(uint16 spell_id); +bool IsAnyBuffSpell(uint16 spell_id); +bool IsDispelSpell(uint16 spell_id); +bool IsEscapeSpell(uint16 spell_id); bool IsDebuffSpell(uint16 spell_id); +bool IsHateReduxSpell(uint16 spell_id); bool IsResistDebuffSpell(uint16 spell_id); bool IsSelfConversionSpell(uint16 spell_id); bool IsBuffSpell(uint16 spell_id); @@ -1628,5 +1903,15 @@ bool IsCastRestrictedSpell(uint16 spell_id); bool IsAegolismSpell(uint16 spell_id); bool AegolismStackingIsSymbolSpell(uint16 spell_id); bool AegolismStackingIsArmorClassSpell(uint16 spell_id); +int8 SpellEffectsCount(uint16 spell_id); +bool IsLichSpell(uint16 spell_id); +bool IsInstantHealSpell(uint32 spell_id); +bool IsResurrectSpell(uint16 spell_id); +bool RequiresStackCheck(uint16 spell_type); +bool IsResistanceBuffSpell(uint16 spell_id); +bool IsResistanceOnlySpell(uint16 spell_id); +bool IsDamageShieldOnlySpell(uint16 spell_id); +bool IsDamageShieldAndResistSpell(uint16 spell_id); +bool IsHateSpell(uint16 spell_id); #endif diff --git a/common/spdat_bot.cpp b/common/spdat_bot.cpp new file mode 100644 index 0000000000..e6e6a68c2f --- /dev/null +++ b/common/spdat_bot.cpp @@ -0,0 +1,710 @@ +#include "spdat.h" + +bool IsBotSpellTypeDetrimental(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::Nuke: + case BotSpellTypes::Root: + case BotSpellTypes::Lifetap: + case BotSpellTypes::Snare: + case BotSpellTypes::DOT: + case BotSpellTypes::Dispel: + case BotSpellTypes::Mez: + case BotSpellTypes::Charm: + case BotSpellTypes::Slow: + case BotSpellTypes::Debuff: + case BotSpellTypes::HateRedux: + case BotSpellTypes::Fear: + case BotSpellTypes::Stun: + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::AEMez: + case BotSpellTypes::AEStun: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::AESlow: + case BotSpellTypes::AESnare: + case BotSpellTypes::AEFear: + case BotSpellTypes::AEDispel: + case BotSpellTypes::AERoot: + case BotSpellTypes::AEDoT: + case BotSpellTypes::AELifetap: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Lull: + case BotSpellTypes::AELull: + case BotSpellTypes::HateLine: + case BotSpellTypes::AEHateLine: + return true; + default: + return false; + } + + return false; +} + +bool IsBotSpellTypeBeneficial(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::RegularHeal: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + case BotSpellTypes::Buff: + case BotSpellTypes::Cure: + case BotSpellTypes::GroupCures: + case BotSpellTypes::PetCures: + case BotSpellTypes::DamageShields: + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + case BotSpellTypes::Pet: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::Resurrect: + case BotSpellTypes::Teleport: + case BotSpellTypes::Succor: + case BotSpellTypes::BindAffinity: + case BotSpellTypes::Identify: + case BotSpellTypes::Levitate: + case BotSpellTypes::Rune: + case BotSpellTypes::WaterBreathing: + case BotSpellTypes::Size: + case BotSpellTypes::Invisibility: + case BotSpellTypes::MovementSpeed: + case BotSpellTypes::SendHome: + case BotSpellTypes::SummonCorpse: + return true; + default: + return false; + } + + return false; +} + +bool IsBotSpellTypeOtherBeneficial(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::RegularHeal: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + case BotSpellTypes::Buff: + case BotSpellTypes::Cure: + case BotSpellTypes::GroupCures: + case BotSpellTypes::PetCures: + case BotSpellTypes::DamageShields: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::Teleport: + case BotSpellTypes::Succor: + case BotSpellTypes::BindAffinity: + case BotSpellTypes::Identify: + case BotSpellTypes::Levitate: + case BotSpellTypes::Rune: + case BotSpellTypes::WaterBreathing: + case BotSpellTypes::Size: + case BotSpellTypes::Invisibility: + case BotSpellTypes::MovementSpeed: + case BotSpellTypes::SendHome: + case BotSpellTypes::SummonCorpse: + return true; + default: + return false; + } + + return false; +} + +bool IsBotSpellTypeInnate(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Nuke: + case BotSpellTypes::AEDispel: + case BotSpellTypes::Dispel: + case BotSpellTypes::AERoot: + case BotSpellTypes::Root: + case BotSpellTypes::AESlow: + case BotSpellTypes::Slow: + case BotSpellTypes::Charm: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + case BotSpellTypes::AEDoT: + case BotSpellTypes::DOT: + case BotSpellTypes::AELifetap: + case BotSpellTypes::Lifetap: + case BotSpellTypes::AEStun: + case BotSpellTypes::Stun: + case BotSpellTypes::AEMez: + case BotSpellTypes::Mez: + case BotSpellTypes::Lull: + case BotSpellTypes::AELull: + case BotSpellTypes::HateLine: + case BotSpellTypes::AEHateLine: + return true; + default: + return false; + } + + return false; +} + +bool IsAEBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::AEDebuff: + case BotSpellTypes::AEFear: + case BotSpellTypes::AEMez: + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::AESlow: + case BotSpellTypes::AESnare: + case BotSpellTypes::AEStun: + case BotSpellTypes::AEDispel: + case BotSpellTypes::AEDoT: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::AELifetap: + case BotSpellTypes::AERoot: + case BotSpellTypes::AEHateLine: + case BotSpellTypes::AELull: + return true; + default: + return false; + } + + return false; +} + +bool IsGroupBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::GroupCures: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + return true; + default: + return false; + } + + return false; +} + +bool IsGroupTargetOnlyBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::GroupCures: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::GroupHeals: + return true; + default: + return false; + } + + return false; +} + +bool IsPetBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::PetCures: + return true; + default: + return false; + } + + return false; +} + +bool IsClientBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::RegularHeal: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + case BotSpellTypes::Buff: + case BotSpellTypes::Cure: + case BotSpellTypes::GroupCures: + case BotSpellTypes::PetCures: + case BotSpellTypes::DamageShields: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::PetResistBuffs: + return true; + default: + return false; + } + + return false; +} + +bool IsHealBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::FastHeals: + case BotSpellTypes::RegularHeal: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + return true; + default: + return false; + } + + return false; +} + +bool BotSpellTypeRequiresLoS(uint16 spell_type) { + if (IsAEBotSpellType(spell_type)) { // These gather their own targets later + return false; + } + + switch (spell_type) { + case BotSpellTypes::RegularHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + case BotSpellTypes::InCombatBuff: + return false; + default: + return true; + } + + return true; +} + +bool BotSpellTypeRequiresTarget(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::Pet: + case BotSpellTypes::Succor: + return false; + default: + return true; + } + + return true; +} + +bool BotSpellTypeRequiresAEChecks(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::AEMez: + return false; + default: + return true; + } + + return true; +} + +bool RequiresStackCheck(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::FastHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::RegularHeal: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::GroupCompleteHeals: + return false; + default: + return true; + } + + return true; +} + +bool IsCommandedBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::Charm: + case BotSpellTypes::AEFear: + case BotSpellTypes::Fear: + case BotSpellTypes::Resurrect: + case BotSpellTypes::AELull: + case BotSpellTypes::Lull: + case BotSpellTypes::Teleport: + case BotSpellTypes::Succor: + case BotSpellTypes::BindAffinity: + case BotSpellTypes::Identify: + case BotSpellTypes::Levitate: + case BotSpellTypes::Rune: + case BotSpellTypes::WaterBreathing: + case BotSpellTypes::Size: + case BotSpellTypes::Invisibility: + case BotSpellTypes::MovementSpeed: + case BotSpellTypes::SendHome: + case BotSpellTypes::SummonCorpse: + return true; + default: + return false; + } + + return false; +} + +bool IsPullingBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::Nuke: + case BotSpellTypes::Lifetap: + case BotSpellTypes::Snare: + case BotSpellTypes::DOT: + case BotSpellTypes::Dispel: + case BotSpellTypes::Slow: + case BotSpellTypes::Debuff: + case BotSpellTypes::Stun: + case BotSpellTypes::HateLine: + return true; + default: + return false; + } + + return false; +} + +uint16 GetCorrectBotSpellType(uint16 spell_type, uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return UINT16_MAX; + } + + uint16 correct_type = UINT16_MAX; + SPDat_Spell_Struct spell = spells[spell_id]; + std::string teleport_zone = spell.teleport_zone; + + if (IsCharmSpell(spell_id)) { + correct_type = BotSpellTypes::Charm; + } + else if (IsFearSpell(spell_id)) { + correct_type = BotSpellTypes::Fear; + } + else if (IsEffectInSpell(spell_id, SE_Revive)) { + correct_type = BotSpellTypes::Resurrect; + } + else if (IsHarmonySpell(spell_id)) { + correct_type = BotSpellTypes::Lull; + } + else if ( + teleport_zone.compare("") && + !IsEffectInSpell(spell_id, SE_GateToHomeCity) && + IsBeneficialSpell(spell_id) && + (IsEffectInSpell(spell_id, SE_Teleport) || IsEffectInSpell(spell_id, SE_Translocate)) + ) { + correct_type = BotSpellTypes::Teleport; + } + else if ( + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_Succor) + ) { + correct_type = BotSpellTypes::Succor; + } + else if (IsEffectInSpell(spell_id, SE_BindAffinity)) { + correct_type = BotSpellTypes::BindAffinity; + } + else if (IsEffectInSpell(spell_id, SE_Identify)) { + correct_type = BotSpellTypes::Identify; + } + else if ( + spell_type == BotSpellTypes::Levitate && + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_Levitate) + ) { + correct_type = BotSpellTypes::Levitate; + } + else if ( + spell_type == BotSpellTypes::Rune && + IsBeneficialSpell(spell_id) && + (IsEffectInSpell(spell_id, SE_AbsorbMagicAtt) || IsEffectInSpell(spell_id, SE_Rune)) + ) { + correct_type = BotSpellTypes::Rune; + } + else if ( + spell_type == BotSpellTypes::WaterBreathing && + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_WaterBreathing) + ) { + correct_type = BotSpellTypes::WaterBreathing; + } + else if ( + spell_type == BotSpellTypes::Size && + IsBeneficialSpell(spell_id) && + (IsEffectInSpell(spell_id, SE_ModelSize) || IsEffectInSpell(spell_id, SE_ChangeHeight)) + ) { + correct_type = BotSpellTypes::Size; + } + else if ( + spell_type == BotSpellTypes::Invisibility && + IsBeneficialSpell(spell_id) && + (IsEffectInSpell(spell_id, SE_SeeInvis) || IsInvisibleSpell(spell_id)) + ) { + correct_type = BotSpellTypes::Invisibility; + } + else if ( + spell_type == BotSpellTypes::MovementSpeed && + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_MovementSpeed) + ) { + correct_type = BotSpellTypes::MovementSpeed; + } + else if ( + !teleport_zone.compare("") && + IsBeneficialSpell(spell_id) && + (IsEffectInSpell(spell_id, SE_Translocate) || IsEffectInSpell(spell_id, SE_GateToHomeCity)) + ) { + correct_type = BotSpellTypes::SendHome; + } + else if (IsEffectInSpell(spell_id, SE_SummonCorpse)) { + correct_type = BotSpellTypes::SummonCorpse; + } + + if (correct_type == UINT16_MAX) { + if ( + IsSummonPetSpell(spell_id) || + IsEffectInSpell(spell_id, SE_TemporaryPets) + ) { + correct_type = BotSpellTypes::Pet; + } + else if (IsMesmerizeSpell(spell_id)) { + correct_type = BotSpellTypes::Mez; + } + else if (IsEscapeSpell(spell_id)) { + correct_type = BotSpellTypes::Escape; + } + else if ( + IsDetrimentalSpell(spell_id) && + IsEffectInSpell(spell_id, SE_Root) + ) { + if (IsAnyAESpell(spell_id)) { + correct_type = BotSpellTypes::AERoot; + } + else { + correct_type = BotSpellTypes::Root; + } + } + else if ( + IsDetrimentalSpell(spell_id) && + IsLifetapSpell(spell_id) + ) { + correct_type = BotSpellTypes::Lifetap; + } + else if ( + IsDetrimentalSpell(spell_id) && + IsEffectInSpell(spell_id, SE_MovementSpeed) + ) { + correct_type = BotSpellTypes::Snare; + } + else if ( + IsDetrimentalSpell(spell_id) && + (IsStackableDOT(spell_id) || IsDamageOverTimeSpell(spell_id)) + ) { + correct_type = BotSpellTypes::DOT; + } + else if (IsDispelSpell(spell_id)) { + correct_type = BotSpellTypes::Dispel; + } + else if ( + IsDetrimentalSpell(spell_id) && + IsSlowSpell(spell_id) + ) { + correct_type = BotSpellTypes::Slow; + } + else if ( + IsDebuffSpell(spell_id) && + !IsHateReduxSpell(spell_id) && + !IsHateSpell(spell_id) + ) { + correct_type = BotSpellTypes::Debuff; + } + else if (IsHateReduxSpell(spell_id)) { + correct_type = BotSpellTypes::HateRedux; + } + else if ( + IsDetrimentalSpell(spell_id) && + IsHateSpell(spell_id) + ) { + correct_type = BotSpellTypes::HateLine; + } + else if ( + IsBuffSpell(spell_id) && + IsBeneficialSpell(spell_id) && + IsBardSong(spell_id) + ) { + if ( + spell_type == BotSpellTypes::InCombatBuffSong || + spell_type == BotSpellTypes::OutOfCombatBuffSong || + spell_type == BotSpellTypes::PreCombatBuffSong + ) { + correct_type = spell_type; + } + else { + correct_type = BotSpellTypes::OutOfCombatBuffSong; + } + } + else if ( + !IsBardSong(spell_id) && + ( + (IsSelfConversionSpell(spell_id) && spell.buff_duration < 1) || + (spell_type == BotSpellTypes::InCombatBuff && IsAnyBuffSpell(spell_id)) + ) + ) { + correct_type = BotSpellTypes::InCombatBuff; + } + else if ( + spell_type == BotSpellTypes::PreCombatBuff && + IsAnyBuffSpell(spell_id) && + !IsBardSong(spell_id) + ) { + correct_type = BotSpellTypes::PreCombatBuff; + } + else if ( + (IsCureSpell(spell_id) && spell_type == BotSpellTypes::Cure) || + (IsCureSpell(spell_id) && !IsAnyHealSpell(spell_id)) + ) { + correct_type = BotSpellTypes::Cure; + } + else if (IsAnyNukeOrStunSpell(spell_id)) { + if (IsAnyAESpell(spell_id)) { + if (IsAERainSpell(spell_id)) { + correct_type = BotSpellTypes::AERains; + } + else if (IsPBAENukeSpell(spell_id)) { + correct_type = BotSpellTypes::PBAENuke; + } + else if (IsStunSpell(spell_id)) { + correct_type = BotSpellTypes::AEStun; + } + else { + correct_type = BotSpellTypes::AENukes; + } + } + else if (IsStunSpell(spell_id)) { + correct_type = BotSpellTypes::Stun; + } + else { + correct_type = BotSpellTypes::Nuke; + } + } + else if (IsAnyHealSpell(spell_id)) { + if (IsGroupSpell(spell_id)) { + if (IsGroupCompleteHealSpell(spell_id)) { + correct_type = BotSpellTypes::GroupCompleteHeals; + } + else if (IsGroupHealOverTimeSpell(spell_id)) { + correct_type = BotSpellTypes::GroupHoTHeals; + } + else if (IsRegularGroupHealSpell(spell_id)) { + correct_type = BotSpellTypes::GroupHeals; + } + + return correct_type; + } + + if (IsVeryFastHealSpell(spell_id)) { + correct_type = BotSpellTypes::VeryFastHeals; + } + else if (IsFastHealSpell(spell_id)) { + correct_type = BotSpellTypes::FastHeals; + } + else if (IsCompleteHealSpell(spell_id)) { + correct_type = BotSpellTypes::CompleteHeal; + } + else if (IsHealOverTimeSpell(spell_id)) { + correct_type = BotSpellTypes::HoTHeals; + } + else if (IsRegularSingleTargetHealSpell(spell_id)) { + correct_type = BotSpellTypes::RegularHeal; + } + else if (IsRegularPetHealSpell(spell_id)) { + correct_type = BotSpellTypes::RegularHeal; + } + } + else if (IsAnyBuffSpell(spell_id)) { + correct_type = BotSpellTypes::Buff; + + if (IsResistanceOnlySpell(spell_id)) { + correct_type = BotSpellTypes::ResistBuffs; + } + else if (IsDamageShieldOnlySpell(spell_id)) { + correct_type = BotSpellTypes::DamageShields; + } + } + } + + return correct_type; +} + +uint16 GetPetBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::Buff: + return BotSpellTypes::PetBuffs; + case BotSpellTypes::RegularHeal: + return BotSpellTypes::PetRegularHeals; + case BotSpellTypes::CompleteHeal: + return BotSpellTypes::PetCompleteHeals; + case BotSpellTypes::FastHeals: + return BotSpellTypes::PetFastHeals; + case BotSpellTypes::VeryFastHeals: + return BotSpellTypes::PetVeryFastHeals; + case BotSpellTypes::HoTHeals: + return BotSpellTypes::PetHoTHeals; + case BotSpellTypes::Cure: + return BotSpellTypes::PetCures; + case BotSpellTypes::DamageShields: + return BotSpellTypes::PetDamageShields; + case BotSpellTypes::ResistBuffs: + return BotSpellTypes::PetResistBuffs; + default: + return spell_type; + } + + return spell_type; +} diff --git a/common/version.h b/common/version.h index 56e9d11cb6..e2e19372b8 100644 --- a/common/version.h +++ b/common/version.h @@ -43,7 +43,7 @@ */ #define CURRENT_BINARY_DATABASE_VERSION 9296 -#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9045 +#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 #endif diff --git a/zone/aggro.cpp b/zone/aggro.cpp index 1bc7efa768..4990f88bb9 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -743,12 +743,30 @@ bool Mob::IsAttackAllowed(Mob *target, bool isSpellAttack) } // can't damage own pet (applies to everthing) - Mob *target_owner = target->GetOwner(); - Mob *our_owner = GetOwner(); - if(target_owner && target_owner == this) - return false; - else if(our_owner && our_owner == target) + Mob* target_owner = target->GetOwner(); + Mob* our_owner = GetOwner(); + + // Self-owner check + if (target_owner == this || our_owner == target) { return false; + } + + // Bot-specific logic + if (IsBot()) { + Mob* target_ultimate_owner = target->IsBot() ? target->CastToBot()->GetBotOwner() : target->GetUltimateOwner(); + Mob* our_ultimate_owner = CastToBot()->GetBotOwner(); + + if (target_ultimate_owner) { + if (target_ultimate_owner == our_ultimate_owner || target_ultimate_owner->IsOfClientBot()) { + return false; + } + } + + // Bots should not attack their ultimate owner + if (our_ultimate_owner == target) { + return false; + } + } // invalidate for swarm pets for later on if their owner is a corpse if (IsNPC() && CastToNPC()->GetSwarmInfo() && our_owner && @@ -1278,6 +1296,39 @@ bool Mob::CheckLosFN(glm::vec3 posWatcher, float sizeWatcher, glm::vec3 posTarge return zone->zonemap->CheckLoS(posWatcher, posTarget); } +bool Mob::CheckPositioningLosFN(Mob* other, float x, float y, float z) { + if (!zone->zonemap) { + //not sure what the best return is on error + //should make this a database variable, but im lazy today +#ifdef LOS_DEFAULT_CAN_SEE + return(true); +#else + return(false); +#endif + } + + if (!other) { + return(true); + } + glm::vec3 myloc; + glm::vec3 oloc; + +#define LOS_DEFAULT_HEIGHT 6.0f + + oloc.x = other->GetX(); + oloc.y = other->GetY(); + oloc.z = other->GetZ() + (other->GetSize() == 0.0 ? LOS_DEFAULT_HEIGHT : other->GetSize()) / 2 * SEE_POSITION; + + myloc.x = x; + myloc.y = y; + myloc.z = z + (GetSize() == 0.0 ? LOS_DEFAULT_HEIGHT : GetSize()) / 2 * HEAD_POSITION; + +#if LOSDEBUG>=5 + LogDebug("LOS from ([{}], [{}], [{}]) to ([{}], [{}], [{}]) sizes: ([{}], [{}])", myloc.x, myloc.y, myloc.z, oloc.x, oloc.y, oloc.z, GetSize(), mobSize); +#endif + return zone->zonemap->CheckLoS(myloc, oloc); +} + //offensive spell aggro int32 Mob::CheckAggroAmount(uint16 spell_id, Mob *target, bool is_proc) { @@ -1658,4 +1709,3 @@ void Mob::RogueEvade(Mob *other) return; } - diff --git a/zone/attack.cpp b/zone/attack.cpp index 77bfeab51d..dd70222cea 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -1246,7 +1246,12 @@ int64 Mob::GetWeaponDamage(Mob *against, const EQ::ItemInstance *weapon_item, in return 0; } - if (!weapon_item->IsClassEquipable(GetClass())) { + if (!weapon_item->IsClassEquipable(GetClass()) && + ( + !IsBot() || + (IsBot() && !RuleB(Bots, AllowBotEquipAnyClassGear)) + ) + ) { return 0; } @@ -2393,7 +2398,7 @@ bool NPC::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, bool LogCombat("Final damage against [{}]: [{}]", other->GetName(), my_hit.damage_done); - if (other->IsClient() && IsPet() && GetOwner()->IsClient()) { + if (other->IsClient() && IsPet() && GetOwner()->IsOfClientBot()) { //pets do half damage to clients in pvp my_hit.damage_done /= 2; if (my_hit.damage_done < 1) { @@ -2612,35 +2617,28 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy } if (give_exp && give_exp->HasOwner()) { - bool owner_in_group = false; - - if ( - ( - give_exp->HasGroup() && - give_exp->GetGroup()->IsGroupMember(give_exp->GetUltimateOwner()) - ) || - ( - give_exp->IsPet() && - ( - give_exp->GetOwner()->IsClient() || - ( - give_exp->GetOwner()->HasGroup() && - give_exp->GetOwner()->GetGroup()->IsGroupMember(give_exp->GetOwner()->GetUltimateOwner()) - ) - ) - ) - ) { - owner_in_group = true; - } + auto owner = give_exp->GetOwner(); - give_exp = give_exp->GetUltimateOwner(); + if (owner) { + Mob* ulimate_owner = give_exp->GetUltimateOwner(); + bool pet_owner_is_client = give_exp->IsPet() && owner->IsClient(); + bool pet_owner_is_bot = give_exp->IsPet() && owner->IsBot(); + bool owner_is_client = owner->IsClient(); + + bool is_in_same_group_or_raid = ( + pet_owner_is_client || + (pet_owner_is_bot && owner->IsInGroupOrRaid(ulimate_owner)) || + (owner_is_client && give_exp->IsInGroupOrRaid(ulimate_owner)) + ); - if (!RuleB(Bots, BotGroupXP) && !owner_in_group) { + give_exp = (is_in_same_group_or_raid ? give_exp->GetUltimateOwner() : nullptr); + } + else { give_exp = nullptr; } } - if (give_exp && give_exp->IsTempPet() && give_exp->IsPetOwnerClient()) { + if (give_exp && give_exp->IsTempPet() && give_exp->IsPetOwnerOfClientBot()) { if (give_exp->IsNPC() && give_exp->CastToNPC()->GetSwarmOwner()) { Mob* temp_owner = entity_list.GetMobID(give_exp->CastToNPC()->GetSwarmOwner()); if (temp_owner) { @@ -2810,7 +2808,7 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy const uint32 con_level = give_exp->GetLevelCon(GetLevel()); if (con_level != ConsiderColor::Gray) { - if (!GetOwner() || (GetOwner() && !GetOwner()->IsClient())) { + if (!GetOwner() || (GetOwner() && !GetOwner()->IsOfClientBot())) { give_exp_client->AddEXP(ExpSource::Kill, final_exp, con_level, false, this); if ( @@ -6435,8 +6433,10 @@ void Mob::CommonOutgoingHitSuccess(Mob* defender, DamageHitInfo &hit, ExtraAttac } else { int ass = TryAssassinate(defender, hit.skill); - if (ass > 0) + + if (ass > 0) { hit.damage_done = ass; + } } } else if (hit.skill == EQ::skills::SkillFrenzy && GetClass() == Class::Berserker && GetLevel() > 50) { @@ -6481,7 +6481,7 @@ void Mob::CommonOutgoingHitSuccess(Mob* defender, DamageHitInfo &hit, ExtraAttac int mod = GetSpecialAbilityParam(SpecialAbility::Rampage, 2); if (mod > 0) spec_mod = mod; - if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) { //SE_PC_Pet_Rampage SPA 464 on pet, damage modifier int spell_mod = spellbonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD] + itembonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD] + aabonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD]; if (spell_mod > spec_mod) @@ -6492,7 +6492,7 @@ void Mob::CommonOutgoingHitSuccess(Mob* defender, DamageHitInfo &hit, ExtraAttac int mod = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 2); if (mod > 0) spec_mod = mod; - if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) { //SE_PC_Pet_AE_Rampage SPA 465 on pet, damage modifier int spell_mod = spellbonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD] + itembonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD] + aabonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD]; if (spell_mod > spec_mod) @@ -6924,7 +6924,7 @@ void Mob::DoMainHandAttackRounds(Mob *target, ExtraAttackOptions *opts, bool ram Attack(target, EQ::invslot::slotPrimary, false, false, false, opts); if (CanThisClassDoubleAttack() && CheckDoubleAttack()) { Attack(target, EQ::invslot::slotPrimary, false, false, false, opts); - if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) { int chance = spellbonuses.PC_Pet_Flurry + itembonuses.PC_Pet_Flurry + aabonuses.PC_Pet_Flurry; if (chance && zone->random.Roll(chance)) { Flurry(nullptr); @@ -6985,7 +6985,7 @@ void Mob::DoOffHandAttackRounds(Mob *target, ExtraAttackOptions *opts, bool ramp if (CanThisClassDoubleAttack() && GetLevel() > 35 && CheckDoubleAttack() && !rampage) { Attack(target, EQ::invslot::slotSecondary, false, false, false, opts); - if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) { int chance = spellbonuses.PC_Pet_Flurry + itembonuses.PC_Pet_Flurry + aabonuses.PC_Pet_Flurry; if (chance && zone->random.Roll(chance)) { Flurry(nullptr); diff --git a/zone/aura.cpp b/zone/aura.cpp index 692dc9f0f9..17c12abdda 100644 --- a/zone/aura.cpp +++ b/zone/aura.cpp @@ -81,7 +81,7 @@ void Aura::ProcessOnAllFriendlies(Mob *owner) if (!mob) { continue; } - if (mob->IsClient() || mob->IsPetOwnerClient() || mob->IsMerc() || mob->IsBot()) { + if (mob->IsOfClientBotMerc() || mob->IsPetOwnerOfClientBot()) { auto it = casted_on.find(mob->GetID()); if (it != casted_on.end()) { // we are already on the list, let's check for removal @@ -131,7 +131,7 @@ void Aura::ProcessOnAllGroupMembers(Mob *owner) std::set delayed_remove; bool is_buff = IsBuffSpell(spell_id); // non-buff spells don't cast on enter - if (owner->IsRaidGrouped() && owner->IsClient()) { // currently raids are just client, but safety check + if (owner->IsRaidGrouped() && owner->IsOfClientBot()) { // currently raids are just client, but safety check auto raid = owner->GetRaid(); if (raid == nullptr) { // well shit owner->RemoveAura(GetID(), false, true); @@ -198,17 +198,17 @@ void Aura::ProcessOnAllGroupMembers(Mob *owner) auto it = casted_on.find(mob->GetID()); if (it != casted_on.end()) { // verify still good! - if (mob->IsClient()) { + if (mob->IsOfClientBot()) { if (!verify_raid_client(mob->CastToClient())) { delayed_remove.insert(mob->GetID()); } } - else if (mob->IsPet() && mob->IsPetOwnerClient() && mob->GetOwner()) { + else if (mob->IsPet() && mob->IsPetOwnerOfClientBot() && mob->GetOwner()) { if (!verify_raid_client_pet(mob)) { delayed_remove.insert(mob->GetID()); } } - else if (mob->IsNPC() && mob->IsPetOwnerClient()) { + else if (mob->IsNPC() && mob->IsPetOwnerOfClientBot()) { auto npc = mob->CastToNPC(); if (!verify_raid_client_swarm(npc)) { delayed_remove.insert(mob->GetID()); @@ -216,19 +216,19 @@ void Aura::ProcessOnAllGroupMembers(Mob *owner) } } else { // we're not on it! - if (mob->IsClient() && verify_raid_client(mob->CastToClient())) { + if (mob->IsOfClientBot() && verify_raid_client(mob->CastToClient())) { casted_on.insert(mob->GetID()); if (is_buff) { SpellFinished(spell_id, mob); } } - else if (mob->IsPet() && mob->IsPetOwnerClient() && mob->GetOwner() && verify_raid_client_pet(mob)) { + else if (mob->IsPet() && mob->IsPetOwnerOfClientBot() && mob->GetOwner() && verify_raid_client_pet(mob)) { casted_on.insert(mob->GetID()); if (is_buff) { SpellFinished(spell_id, mob); } } - else if (mob->IsNPC() && mob->IsPetOwnerClient()) { + else if (mob->IsNPC() && mob->IsPetOwnerOfClientBot()) { auto npc = mob->CastToNPC(); if (verify_raid_client_swarm(npc)) { casted_on.insert(mob->GetID()); @@ -376,7 +376,7 @@ void Aura::ProcessOnGroupMembersPets(Mob *owner) auto group_member = owner->GetOwnerOrSelf(); if (group_member->IsRaidGrouped() && - group_member->IsClient()) { // currently raids are just client, but safety check + group_member->IsOfClientBot()) { // currently raids are just client, but safety check auto raid = group_member->GetRaid(); if (raid == nullptr) { // well shit owner->RemoveAura(GetID(), false, true); @@ -428,12 +428,12 @@ void Aura::ProcessOnGroupMembersPets(Mob *owner) auto it = casted_on.find(mob->GetID()); if (it != casted_on.end()) { // verify still good! - if (mob->IsPet() && mob->IsPetOwnerClient() && mob->GetOwner()) { + if (mob->IsPet() && mob->IsPetOwnerOfClientBot() && mob->GetOwner()) { if (!verify_raid_client_pet(mob)) { delayed_remove.insert(mob->GetID()); } } - else if (mob->IsNPC() && mob->IsPetOwnerClient()) { + else if (mob->IsNPC() && mob->IsPetOwnerOfClientBot()) { auto npc = mob->CastToNPC(); if (!verify_raid_client_swarm(npc)) { delayed_remove.insert(mob->GetID()); @@ -441,16 +441,16 @@ void Aura::ProcessOnGroupMembersPets(Mob *owner) } } else { // we're not on it! - if (mob->IsClient()) { + if (mob->IsOfClientBot()) { continue; // never hit client } - else if (mob->IsPet() && mob->IsPetOwnerClient() && mob->GetOwner() && verify_raid_client_pet(mob)) { + else if (mob->IsPet() && mob->IsPetOwnerOfClientBot() && mob->GetOwner() && verify_raid_client_pet(mob)) { casted_on.insert(mob->GetID()); if (is_buff) { SpellFinished(spell_id, mob); } } - else if (mob->IsNPC() && mob->IsPetOwnerClient()) { + else if (mob->IsNPC() && mob->IsPetOwnerOfClientBot()) { auto npc = mob->CastToNPC(); if (verify_raid_client_swarm(npc)) { casted_on.insert(mob->GetID()); @@ -499,7 +499,7 @@ void Aura::ProcessOnGroupMembersPets(Mob *owner) } } else { // not on, check if we should be! - if (mob->IsClient()) { + if (mob->IsOfClientBot()) { continue; } else if (mob->IsPet() && verify_group_pet(mob)) { @@ -690,7 +690,7 @@ void Aura::ProcessSpawns() continue; } - if (!e.second->IsClient()) { + if (!e.second->IsOfClientBot()) { continue; } @@ -801,7 +801,7 @@ bool Aura::ShouldISpawnFor(Client *c) return true; } - if (owner->IsRaidGrouped() && owner->IsClient()) { + if (owner->IsRaidGrouped() && owner->IsOfClientBot()) { auto raid = owner->GetRaid(); if (raid == nullptr) { return false; diff --git a/zone/bot.cpp b/zone/bot.cpp index b0f32b406f..7d1a8277d6 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -29,8 +29,14 @@ #include "../common/repositories/criteria/content_filter_criteria.h" #include "../common/skill_caps.h" +/* +TODO bot rewrite: +--command cleanup remaining commands (move to new help window, make more descriptive) +--Add quest methods for functions +*/ + // This constructor is used during the bot create command -Bot::Bot(NPCType *npcTypeData, Client* botOwner) : NPC(npcTypeData, nullptr, glm::vec4(), Ground, false), rest_timer(1), ping_timer(1) { +Bot::Bot(NPCType *npcTypeData, Client* botOwner) : NPC(npcTypeData, nullptr, glm::vec4(), Ground, false), rest_timer(1), m_ping_timer(1) { GiveNPCTypeData(npcTypeData); if (botOwner) { @@ -69,27 +75,25 @@ Bot::Bot(NPCType *npcTypeData, Client* botOwner) : NPC(npcTypeData, nullptr, glm RestRegenHP = 0; RestRegenMana = 0; RestRegenEndurance = 0; - m_enforce_spell_settings = false; - m_bot_archery_setting = false; - m_expansion_bitmask = -1; - m_bot_caster_range = 0; + SetBotID(0); SetBotSpellID(0); SetSpawnStatus(false); - SetBotCharmer(false); - SetPetChooser(false); - SetRangerAutoWeaponSelect(false); - SetTaunting(GetClass() == Class::Warrior || GetClass() == Class::Paladin || GetClass() == Class::ShadowKnight); + SetBotCharmer(false); SetDefaultBotStance(); + SetTaunting((GetClass() == Class::Warrior || GetClass() == Class::Paladin || GetClass() == Class::ShadowKnight) && (GetBotStance() == Stance::Aggressive)); - SetAltOutOfCombatBehavior(GetClass() == Class::Bard); // will need to be updated if more classes make use of this flag - SetShowHelm(true); SetPauseAI(false); + m_combat_jitter_timer.Disable(); + m_auto_save_timer.Disable(); + m_rogue_evade_timer.Disable(); + m_monk_evade_timer.Disable(); m_auto_defend_timer.Disable(); SetGuardFlag(false); SetHoldFlag(false); SetAttackFlag(false); + SetCombatRoundForAlerts(false); SetAttackingFlag(false); SetPullFlag(false); SetPullingFlag(false); @@ -98,17 +102,23 @@ Bot::Bot(NPCType *npcTypeData, Client* botOwner) : NPC(npcTypeData, nullptr, glm m_previous_pet_order = SPO_Guard; rest_timer.Disable(); - ping_timer.Disable(); - SetFollowDistance(BOT_FOLLOW_DISTANCE_DEFAULT); - if (IsCasterClass(GetClass())) - SetStopMeleeLevel((uint8)RuleI(Bots, CasterStopMeleeLevel)); - else - SetStopMeleeLevel(255); + m_ping_timer.Disable(); + + LoadDefaultBotSettings(); + SetCastedSpellType(UINT16_MAX); + SetCommandedSpell(false); + SetPullingSpell(false); // Do this once and only in this constructor GenerateAppearance(); GenerateBaseStats(); bot_timers.clear(); + bot_blocked_buffs.clear(); + _spell_target_list.clear(); + _group_spell_target_list.clear(); + SetStoredRaid(nullptr); + SetVerifiedRaid(false); + p_raid_instance = nullptr; // Calculate HitPoints Last As It Uses Base Stats current_hp = GenerateBaseHitPoints(); @@ -131,10 +141,9 @@ Bot::Bot( uint32 botSpellsID, double totalPlayTime, uint32 lastZoneId, - NPCType *npcTypeData, - int32 expansion_bitmask + NPCType *npcTypeData ) - : NPC(npcTypeData, nullptr, glm::vec4(), Ground, false), rest_timer(1), ping_timer(1) + : NPC(npcTypeData, nullptr, glm::vec4(), Ground, false), rest_timer(1), m_ping_timer(1) { GiveNPCTypeData(npcTypeData); @@ -175,13 +184,13 @@ Bot::Bot( RestRegenHP = 0; RestRegenMana = 0; RestRegenEndurance = 0; - m_expansion_bitmask = expansion_bitmask; SetBotID(botID); SetBotSpellID(botSpellsID); SetSpawnStatus(false); SetBotCharmer(false); - SetPetChooser(false); - SetRangerAutoWeaponSelect(false); + SetCastedSpellType(UINT16_MAX); + SetCommandedSpell(false); + SetPullingSpell(false); bool stance_flag = false; if (!database.botdb.LoadStance(this, stance_flag) && bot_owner) { @@ -207,10 +216,15 @@ Bot::Bot( SetTaunting((GetClass() == Class::Warrior || GetClass() == Class::Paladin || GetClass() == Class::ShadowKnight) && (GetBotStance() == Stance::Aggressive)); SetPauseAI(false); + m_combat_jitter_timer.Disable(); + m_auto_save_timer.Disable(); + m_rogue_evade_timer.Disable(); + m_monk_evade_timer.Disable(); m_auto_defend_timer.Disable(); SetGuardFlag(false); SetHoldFlag(false); SetAttackFlag(false); + SetCombatRoundForAlerts(false); SetAttackingFlag(false); SetPullFlag(false); SetPullingFlag(false); @@ -219,12 +233,7 @@ Bot::Bot( m_previous_pet_order = SPO_Guard; rest_timer.Disable(); - ping_timer.Disable(); - SetFollowDistance(BOT_FOLLOW_DISTANCE_DEFAULT); - if (IsCasterClass(GetClass())) - SetStopMeleeLevel((uint8)RuleI(Bots, CasterStopMeleeLevel)); - else - SetStopMeleeLevel(255); + m_ping_timer.Disable(); strcpy(name, GetCleanName()); @@ -235,7 +244,11 @@ Bot::Bot( EquipBot(); if (GetClass() == Class::Rogue) { - m_evade_timer.Start(); + m_rogue_evade_timer.Start(); + } + + if (GetClass() == Class::Monk) { + m_monk_evade_timer.Start(); } m_CastingRoles.GroupHealer = false; @@ -246,9 +259,21 @@ Bot::Bot( GenerateBaseStats(); bot_timers.clear(); - database.botdb.LoadTimers(this); + LoadDefaultBotSettings(); + database.botdb.LoadBotSettings(this); + + if (RuleB(Bots, AllowBotBlockedBuffs)) { + bot_blocked_buffs.clear(); + database.botdb.LoadBotBlockedBuffs(this); + } + + _spell_target_list.clear(); + _group_spell_target_list.clear(); + SetStoredRaid(nullptr); + SetVerifiedRaid(false); + p_raid_instance = nullptr; LoadAAs(); if (database.botdb.LoadBuffs(this)) { @@ -267,142 +292,146 @@ Bot::Bot( for (int x1 = 0; x1 < EFFECT_COUNT; x1++) { switch (spell.effect_id[x1]) { - case SE_IllusionCopy: - case SE_Illusion: { - if (spell.base_value[x1] == -1) { - if (gender == Gender::Female) { - gender = Gender::Male; - } else if (gender == Gender::Male) { - gender = Gender::Female; + case SE_IllusionCopy: + case SE_Illusion: { + if (GetIllusionBlock()) { + break; } - SendIllusionPacket( - AppearanceStruct{ - .gender_id = gender, - .race_id = GetRace(), + if (spell.base_value[x1] == -1) { + if (gender == Gender::Female) { + gender = Gender::Male; + } else if (gender == Gender::Male) { + gender = Gender::Female; } - ); - } else if (spell.base_value[x1] == -2) // WTF IS THIS - { - if (GetRace() == IKSAR || GetRace() == VAHSHIR || GetRace() <= GNOME) { + SendIllusionPacket( AppearanceStruct{ - .gender_id = GetGender(), - .helmet_texture = static_cast(spell.max_value[x1]), + .gender_id = gender, .race_id = GetRace(), + } + ); + } else if (spell.base_value[x1] == -2) // WTF IS THIS + { + if (GetRace() == IKSAR || GetRace() == VAHSHIR || GetRace() <= GNOME) { + SendIllusionPacket( + AppearanceStruct{ + .gender_id = GetGender(), + .helmet_texture = static_cast(spell.max_value[x1]), + .race_id = GetRace(), + .texture = static_cast(spell.limit_value[x1]), + } + ); + } + } else if (spell.max_value[x1] > 0) { + SendIllusionPacket( + AppearanceStruct{ + .helmet_texture = static_cast(spell.max_value[x1]), + .race_id = static_cast(spell.base_value[x1]), + .texture = static_cast(spell.limit_value[x1]), + } + ); + } else { + SendIllusionPacket( + AppearanceStruct{ + .helmet_texture = static_cast(spell.max_value[x1]), + .race_id = static_cast(spell.base_value[x1]), .texture = static_cast(spell.limit_value[x1]), } ); } - } else if (spell.max_value[x1] > 0) { - SendIllusionPacket( - AppearanceStruct{ - .helmet_texture = static_cast(spell.max_value[x1]), - .race_id = static_cast(spell.base_value[x1]), - .texture = static_cast(spell.limit_value[x1]), - } - ); - } else { - SendIllusionPacket( - AppearanceStruct{ - .helmet_texture = static_cast(spell.max_value[x1]), - .race_id = static_cast(spell.base_value[x1]), - .texture = static_cast(spell.limit_value[x1]), - } - ); - } - switch (spell.base_value[x1]) { - case OGRE: - SendAppearancePacket(AppearanceType::Size, 9); + switch (spell.base_value[x1]) { + case OGRE: + SendAppearancePacket(AppearanceType::Size, 9); + break; + case TROLL: + SendAppearancePacket(AppearanceType::Size, 8); + break; + case VAHSHIR: + case BARBARIAN: + SendAppearancePacket(AppearanceType::Size, 7); + break; + case HALF_ELF: + case WOOD_ELF: + case DARK_ELF: + case FROGLOK: + SendAppearancePacket(AppearanceType::Size, 5); + break; + case DWARF: + SendAppearancePacket(AppearanceType::Size, 4); + break; + case HALFLING: + case GNOME: + SendAppearancePacket(AppearanceType::Size, 3); + break; + default: + SendAppearancePacket(AppearanceType::Size, 6); + break; + } break; - case TROLL: - SendAppearancePacket(AppearanceType::Size, 8); + } + case SE_Silence: + { + Silence(true); break; - case VAHSHIR: - case BARBARIAN: - SendAppearancePacket(AppearanceType::Size, 7); + } + case SE_Amnesia: + { + Amnesia(true); break; - case HALF_ELF: - case WOOD_ELF: - case DARK_ELF: - case FROGLOK: - SendAppearancePacket(AppearanceType::Size, 5); + } + case SE_DivineAura: + { + invulnerable = true; break; - case DWARF: - SendAppearancePacket(AppearanceType::Size, 4); + } + case SE_Invisibility2: + case SE_Invisibility: + { + invisible = true; + SendAppearancePacket(AppearanceType::Invisibility, 1); break; - case HALFLING: - case GNOME: - SendAppearancePacket(AppearanceType::Size, 3); + } + case SE_Levitate: + { + if (!zone->CanLevitate()) + { + SendAppearancePacket(AppearanceType::FlyMode, 0); + BuffFadeByEffect(SE_Levitate); + } + else { + SendAppearancePacket(AppearanceType::FlyMode, 2); + } break; - default: - SendAppearancePacket(AppearanceType::Size, 6); + } + case SE_InvisVsUndead2: + case SE_InvisVsUndead: + { + invisible_undead = true; break; } - break; - } - case SE_Silence: - { - Silence(true); - break; - } - case SE_Amnesia: - { - Amnesia(true); - break; - } - case SE_DivineAura: - { - invulnerable = true; - break; - } - case SE_Invisibility2: - case SE_Invisibility: - { - invisible = true; - SendAppearancePacket(AppearanceType::Invisibility, 1); - break; - } - case SE_Levitate: - { - if (!zone->CanLevitate()) + case SE_InvisVsAnimals: { - SendAppearancePacket(AppearanceType::FlyMode, 0); - BuffFadeByEffect(SE_Levitate); + invisible_animals = true; + break; } - else { - SendAppearancePacket(AppearanceType::FlyMode, 2); + case SE_AddMeleeProc: + case SE_WeaponProc: + { + AddProcToWeapon(GetProcID(buffs[j1].spellid, x1), false, 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid, buffs[j1].casterlevel); + break; + } + case SE_DefensiveProc: + { + AddDefensiveProc(GetProcID(buffs[j1].spellid, x1), 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid); + break; + } + case SE_RangedProc: + { + AddRangedProc(GetProcID(buffs[j1].spellid, x1), 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid); + break; } - break; - } - case SE_InvisVsUndead2: - case SE_InvisVsUndead: - { - invisible_undead = true; - break; - } - case SE_InvisVsAnimals: - { - invisible_animals = true; - break; - } - case SE_AddMeleeProc: - case SE_WeaponProc: - { - AddProcToWeapon(GetProcID(buffs[j1].spellid, x1), false, 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid, buffs[j1].casterlevel); - break; - } - case SE_DefensiveProc: - { - AddDefensiveProc(GetProcID(buffs[j1].spellid, x1), 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid); - break; - } - case SE_RangedProc: - { - AddRangedProc(GetProcID(buffs[j1].spellid, x1), 100 + spells[buffs[j1].spellid].limit_value[x1], buffs[j1].spellid); - break; - } } } } @@ -538,39 +567,72 @@ void Bot::SetSuffix(std::string_view bot_suffix) { } } -uint32 Bot::GetBotArcheryRange() { +uint32 Bot::GetBotRangedValue() { const EQ::ItemInstance *range_inst = GetBotItem(EQ::invslot::slotRange); const EQ::ItemInstance *ammo_inst = GetBotItem(EQ::invslot::slotAmmo); - if (!range_inst || !ammo_inst) + + if (!range_inst) { return 0; + } const EQ::ItemData *range_item = range_inst->GetItem(); - const EQ::ItemData *ammo_item = ammo_inst->GetItem(); - if (!range_item || !ammo_item || range_item->ItemType != EQ::item::ItemTypeBow || ammo_item->ItemType != EQ::item::ItemTypeArrow) + const EQ::ItemData *ammo_item = nullptr; + + if (ammo_inst) { + ammo_item = ammo_inst->GetItem(); + } + + bool has_ammo = ammo_item; + + if (!range_item || !has_ammo) { return 0; + } - // everything is good! - return (range_item->Range + ammo_item->Range); -} + // Bow requires arrows + if ( + range_item->ItemType == EQ::item::ItemTypeBow && + ammo_item->ItemType != EQ::item::ItemTypeArrow + ) { + return 0; + } -void Bot::ChangeBotArcherWeapons(bool isArcher) { - if ((GetClass()==Class::Warrior) || (GetClass()==Class::Paladin) || (GetClass()==Class::Ranger) || (GetClass()==Class::ShadowKnight) || (GetClass()==Class::Rogue)) { - if (!isArcher) { - BotAddEquipItem(EQ::invslot::slotPrimary, GetBotItemBySlot(EQ::invslot::slotPrimary)); - BotAddEquipItem(EQ::invslot::slotSecondary, GetBotItemBySlot(EQ::invslot::slotSecondary)); - SetAttackTimer(); - BotGroupSay(this, "My blade is ready"); - } else { - BotRemoveEquipItem(EQ::invslot::slotPrimary); - BotRemoveEquipItem(EQ::invslot::slotSecondary); - BotAddEquipItem(EQ::invslot::slotAmmo, GetBotItemBySlot(EQ::invslot::slotAmmo)); - BotAddEquipItem(EQ::invslot::slotSecondary, GetBotItemBySlot(EQ::invslot::slotRange)); - SetAttackTimer(); - BotGroupSay(this, "My bow is true and ready"); + // Throwing items + if ( + range_item->ItemType == EQ::item::ItemTypeSmallThrowing || + range_item->ItemType == EQ::item::ItemTypeLargeThrowing + ) { + if (range_item->ID == ammo_item->ID) { + return range_item->Range; } + + return 0; // mismatched throwing + } + + // Bows and arrows + if ( + range_item->ItemType == EQ::item::ItemTypeBow && + ammo_item->ItemType == EQ::item::ItemTypeArrow + ) { + return (range_item->Range + ammo_item->Range); + } + + return 0; +} + +void Bot::ChangeBotRangedWeapons(bool isRanged) { + if (!isRanged) { + BotAddEquipItem(EQ::invslot::slotPrimary, GetBotItemBySlot(EQ::invslot::slotPrimary)); + BotAddEquipItem(EQ::invslot::slotSecondary, GetBotItemBySlot(EQ::invslot::slotSecondary)); + SetAttackTimer(); + RaidGroupSay(this, "My blade is ready"); + } else { + BotRemoveEquipItem(EQ::invslot::slotPrimary); + BotRemoveEquipItem(EQ::invslot::slotSecondary); + BotAddEquipItem(EQ::invslot::slotAmmo, GetBotItemBySlot(EQ::invslot::slotAmmo)); + BotAddEquipItem(EQ::invslot::slotSecondary, GetBotItemBySlot(EQ::invslot::slotRange)); + SetAttackTimer(); + RaidGroupSay(this, "My blades are sheathed"); } - else - BotGroupSay(this, "I don't know how to use a bow"); } void Bot::Sit() { @@ -1239,7 +1301,6 @@ int32 Bot::GenerateBaseHitPoints() { } void Bot::LoadAAs() { - aa_ranks.clear(); int id = 0; @@ -1304,17 +1365,27 @@ bool Bot::IsValidName() bool Bot::IsValidName(std::string& name) { - if (name.length() < 4) + if (name.empty()) { // can't be empty return false; - if (!isupper(name[0])) + } + + if (islower(name[0])) { // capitalize first letter if not + name[0] = toupper(name[0]); + } + + if (!EQ::ValueWithin(name.size(), 4, 15)) { // must be between 4 and 15 characters return false; + } - for (char c : name.substr(1)) { - if (!RuleB(Bots, AllowCamelCaseNames) && !islower(c)) { - return false; - } - if (isdigit(c) || ispunct(c)) { - return false; + if (std::any_of(name.begin(), name.end(), [](char c) { return c == ' ' || c == '_'; })) { // cannot contain spaces or _ + return false; + } + + if (!RuleB(Bots, AllowCamelCaseNames)) { + for (int i = 1; i < name.size(); ++i) { + if (isupper(name[i])) { + return false; + } } } @@ -1344,6 +1415,11 @@ bool Bot::Save() database.botdb.SaveBuffs(this); database.botdb.SaveTimers(this); database.botdb.SaveStance(this); + database.botdb.SaveBotSettings(this); + + if (RuleB(Bots, AllowBotBlockedBuffs)) { + database.botdb.SaveBotBlockedBuffs(this); + } if (!SavePet()) bot_owner->Message(Chat::White, "Failed to save pet for '%s'", GetCleanName()); @@ -1389,20 +1465,30 @@ bool Bot::DeleteBot() RemoveBotFromRaid(this); } - if (!database.botdb.DeleteItems(GetBotID())) { - return false; - } + if (!RuleB(Bots, BotSoftDeletes)) { + if (!database.botdb.DeleteItems(GetBotID())) { + return false; + } - if (!database.botdb.DeleteTimers(GetBotID())) { - return false; - } + if (!database.botdb.DeleteTimers(GetBotID())) { + return false; + } - if (!database.botdb.DeleteBuffs(GetBotID())) { - return false; - } + if (!database.botdb.DeleteBuffs(GetBotID())) { + return false; + } - if (!database.botdb.DeleteStance(GetBotID())) { - return false; + if (!database.botdb.DeleteStance(GetBotID())) { + return false; + } + + if (!database.botdb.DeleteBotSettings(GetBotID())) { + return false; + } + + if (!database.botdb.DeleteBotBlockedBuffs(GetBotID())) { + return false; + } } if (!database.botdb.DeleteBot(GetBotID())) { @@ -1564,6 +1650,7 @@ bool Bot::Process() { if (IsStunned() && stunned_timer.Check()) { Mob::UnStun(); + spun_timer.Disable(); } if (!GetBotOwner()) { @@ -1571,7 +1658,6 @@ bool Bot::Process() } if (GetDepop()) { - _botOwner = nullptr; _botOwnerCharacterID = 0; @@ -1594,7 +1680,7 @@ bool Bot::Process() BuffProcess(); CalcRestState(); - if (currently_fleeing) { + if (currently_fleeing || IsFeared()) { ProcessFlee(); } @@ -1629,25 +1715,34 @@ bool Bot::Process() } } + if (viral_timer.Check()) { + VirusEffectProcess(); + } + + if (spellbonuses.GravityEffect == 1) { + if (gravity_timer.Check()) { + DoGravityEffect(); + } + } + if (GetAppearance() == eaDead && GetHP() > 0) { SetAppearance(eaStanding); } if (IsMoving()) { - ping_timer.Disable(); + m_ping_timer.Disable(); } else { - - if (!ping_timer.Enabled()) { - ping_timer.Start(BOT_KEEP_ALIVE_INTERVAL); + if (!m_ping_timer.Enabled()) { + m_ping_timer.Start(BOT_KEEP_ALIVE_INTERVAL); } - if (ping_timer.Check()) { + if (m_ping_timer.Check()) { SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0); } } - if (auto_save_timer.Check()) { + if (m_auto_save_timer.Check()) { clock_t t = std::clock(); /* Function timer start */ Save(); LogDebug( @@ -1655,10 +1750,22 @@ bool Bot::Process() GetBotID(), ((float)(std::clock() - t)) / CLOCKS_PER_SEC ); - auto_save_timer.Start(RuleI(Bots, AutosaveIntervalSeconds) * 1000); + m_auto_save_timer.Start(RuleI(Bots, AutosaveIntervalSeconds) * 1000); + } + + if (ForcedMovement) { + ProcessForcedMovement(); + } + + if (IsMezzed()) { + return true; } - if (IsStunned() || IsMezzed()) { + if (IsStunned()) { + if (spun_timer.Check()) { + Spin(); + } + return true; } @@ -1708,183 +1815,303 @@ void Bot::AI_Bot_Init() } void Bot::SpellProcess() { - if (spellend_timer.Check(false)) { + if (spellend_timer.Check(false)) { NPC::SpellProcess(); if (GetClass() == Class::Bard && casting_spell_id != 0) casting_spell_id = 0; } } -void Bot::BotMeditate(bool isSitting) { - if (isSitting) { - if (GetManaRatio() < 99.0f || GetHPRatio() < 99.0f) { - if (!IsEngaged() && !IsSitting()) { - Sit(); - } - } else { - if (IsSitting()) { - Stand(); - } - } - } else { - if (IsSitting()) { - Stand(); - } +void Bot::BotMeditate(bool is_sitting) { + bool needs_to_med = + (!IsEngaged() && (GetManaRatio() < 100 || GetHPRatio() < 100)) || + (GetMedInCombat() && !HasTargetReflection() && + ( + GetManaRatio() < GetSitManaPct() || + (GetHPRatio() < GetSitHPPct() && GetLevel() >= GetStopMeleeLevel()) + ) + ); + + if (needs_to_med && !is_sitting) { + Sit(); + } + else if (!needs_to_med && is_sitting) { + Stand(); } } -void Bot::BotRangedAttack(Mob* other) { - //make sure the attack and ranged timers are up - //if the ranged timer is disabled, then they have no ranged weapon and shouldent be attacking anyhow - if ((attack_timer.Enabled() && !attack_timer.Check(false)) || (ranged_timer.Enabled() && !ranged_timer.Check())) { - LogCombatDetail("Bot Archery attack canceled. Timer not up. Attack [{}] ranged [{}]", attack_timer.GetRemainingTime(), ranged_timer.GetRemainingTime()); - Message(0, "Error: Timer not up. Attack %d, ranged %d", attack_timer.GetRemainingTime(), ranged_timer.GetRemainingTime()); - return; +bool Bot::BotRangedAttack(Mob* other, bool can_double_attack) { + if ( + !other || + !IsAttackAllowed(other) || + IsCasting() || + DivineAura() || + IsStunned() || + IsMezzed() || + (GetAppearance() == eaDead) + ) { + return false; } - const auto rangedItem = GetBotItem(EQ::invslot::slotRange); - const EQ::ItemData* RangeWeapon = nullptr; - if (rangedItem) - RangeWeapon = rangedItem->GetItem(); - - const auto ammoItem = GetBotItem(EQ::invslot::slotAmmo); - const EQ::ItemData* Ammo = nullptr; - if (ammoItem) - Ammo = ammoItem->GetItem(); - - if (!RangeWeapon || !Ammo) - return; - - LogCombatDetail("Shooting [{}] with bow [{}] ([{}]) and arrow [{}] ([{}])", other->GetCleanName(), RangeWeapon->Name, RangeWeapon->ID, Ammo->Name, Ammo->ID); - if (!IsAttackAllowed(other) || IsCasting() || DivineAura() || IsStunned() || IsMezzed() || (GetAppearance() == eaDead)) - return; - - SendItemAnimation(other, Ammo, EQ::skills::SkillArchery); - DoArcheryAttackDmg(other, rangedItem, ammoItem); // watch + if ( + !GetPullingFlag() && + ( + ( + GetBotStance() != Stance::Aggressive && + GetBotStance() != Stance::Burn && + GetBotStance() != Stance::AEBurn + ) && + other->GetHPRatio() > 99.0f + ) + ) { + return false; + } - //break invis when you attack - if (invisible) { - LogCombatDetail("Removing invisibility due to melee attack"); - BuffFadeByEffect(SE_Invisibility); - BuffFadeByEffect(SE_Invisibility2); - invisible = false; + if ( + !can_double_attack && + ( + ( + attack_timer.Enabled() && + !attack_timer.Check(false) + ) || + ( + ranged_timer.Enabled() && + !ranged_timer.Check() + ) + ) + ) { + LogCombatDetail("Bot ranged attack canceled. Timer not up. Attack [{}] ranged [{}]", attack_timer.GetRemainingTime(), ranged_timer.GetRemainingTime()); + return false; } - if (invisible_undead) { - LogCombatDetail("Removing invisibility vs. undead due to melee attack"); - BuffFadeByEffect(SE_InvisVsUndead); - BuffFadeByEffect(SE_InvisVsUndead2); - invisible_undead = false; + const auto ranged_item = GetBotItem(EQ::invslot::slotRange); + const EQ::ItemData* ranged_weapon = nullptr; + + if (ranged_item) { + ranged_weapon = ranged_item->GetItem(); } - if (invisible_animals) { - LogCombatDetail("Removing invisibility vs. animals due to melee attack"); - BuffFadeByEffect(SE_InvisVsAnimals); - invisible_animals = false; + if (!ranged_weapon) { + return false; } - if (spellbonuses.NegateIfCombat) - BuffFadeByEffect(SE_NegateIfCombat); + const auto ammo_item = GetBotItem(EQ::invslot::slotAmmo); + const EQ::ItemData* ammo = nullptr; - if (hidden || improved_hidden) { - hidden = false; - improved_hidden = false; - auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - auto sa_out = (SpawnAppearance_Struct*)outapp->pBuffer; - sa_out->spawn_id = GetID(); - sa_out->type = 0x03; - sa_out->parameter = 0; - entity_list.QueueClients(this, outapp, true); - safe_delete(outapp); + if (ammo_item) { + ammo = ammo_item->GetItem(); } -} -bool Bot::CheckBotDoubleAttack(bool tripleAttack) { - //Check for bonuses that give you a double attack chance regardless of skill (ie Bestial Frenzy/Harmonious Attack AA) - uint32 bonusGiveDA = (aabonuses.GiveDoubleAttack + spellbonuses.GiveDoubleAttack + itembonuses.GiveDoubleAttack); - // If you don't have the double attack skill, return - if (!GetSkill(EQ::skills::SkillDoubleAttack) && !(GetClass() == Class::Bard || GetClass() == Class::Beastlord)) - return false; + // Bow requires arrows + // Check if ammo is invalid + bool is_invalid_ammo = !ammo; - // You start with no chance of double attacking - float chance = 0.0f; - uint16 skill = GetSkill(EQ::skills::SkillDoubleAttack); - int32 bonusDA = (aabonuses.DoubleAttackChance + spellbonuses.DoubleAttackChance + itembonuses.DoubleAttackChance); - //Use skill calculations otherwise, if you only have AA applied GiveDoubleAttack chance then use that value as the base. - if (skill) - chance = ((float(skill + GetLevel()) * (float(100.0f + bonusDA + bonusGiveDA) / 100.0f)) / 500.0f); - else - chance = ((float(bonusGiveDA) * (float(100.0f + bonusDA) / 100.0f)) / 100.0f); + // Check if ranged weapon type is invalid + bool is_invalid_ranged_weapon_type = ranged_weapon && + (ranged_weapon->ItemType != EQ::item::ItemTypeBow && + ranged_weapon->ItemType != EQ::item::ItemTypeSmallThrowing && + ranged_weapon->ItemType != EQ::item::ItemTypeLargeThrowing); - //Live now uses a static Triple Attack skill (lv 46 = 2% lv 60 = 20%) - We do not have this skill on EMU ATM. - //A reasonable forumla would then be TA = 20% * chance - //AA's can also give triple attack skill over cap. (ie Burst of Power) NOTE: Skill ID in spell data is 76 (Triple Attack) - //Kayen: Need to decide if we can implement triple attack skill before working in over the cap effect. - if (tripleAttack) { - // Only some Double Attack classes get Triple Attack [This is already checked in client_processes.cpp] - int32 triple_bonus = (spellbonuses.TripleAttackChance + itembonuses.TripleAttackChance); - chance *= 0.2f; //Baseline chance is 20% of your double attack chance. - chance *= (float(100.0f + triple_bonus) / 100.0f); //Apply modifiers. - } + // Check if bow has the wrong ammo + bool is_bow_with_invalid_ammo = ranged_weapon && + (ranged_weapon->ItemType == EQ::item::ItemTypeBow && + (!ammo || ammo->ItemType != EQ::item::ItemTypeArrow)); - if (zone->random.Real(0, 1) < chance) - return true; + // Check if throwing weapon has insufficient charges + bool is_throwing_weapon_with_insufficient_ammo = ranged_weapon && + (ranged_weapon->ItemType == EQ::item::ItemTypeSmallThrowing || ranged_weapon->ItemType == EQ::item::ItemTypeLargeThrowing) && + ( + !ammo_item || ammo_item->GetCharges() < 1 || // Not enough ammo + ( + (RuleI(Bots, StackSizeMin) != -1 && ranged_item->GetCharges() != ranged_weapon->StackSize) || // Invalid stack size + ranged_item->GetCharges() < RuleI(Bots, StackSizeMin) // Charges below minimum + ) + ); - return false; -} + // Final ranged weapon validity check + bool is_invalid_ranged_weapon = is_invalid_ranged_weapon_type || is_bow_with_invalid_ammo || is_throwing_weapon_with_insufficient_ammo; -void Bot::SetTarget(Mob *mob) -{ - if (mob != this) { - NPC::SetTarget(mob); + // Final condition + if (is_invalid_ammo || is_invalid_ranged_weapon) { + if (!ammo || ammo_item->GetCharges() < 1) { + if (!GetCombatRoundForAlerts()) { + SetCombatRoundForAlerts(); + RaidGroupSay(this, "I do not have any ammo!"); + } + } + return false; } -} -void Bot::SetStopMeleeLevel(uint8 level) { - if (IsCasterClass(GetClass()) || IsHybridClass(GetClass())) - _stopMeleeLevel = level; - else - _stopMeleeLevel = 255; -} + LogCombatDetail("Ranged attacking [{}] with {} [{}] ([{}]){}{}{}", + other->GetCleanName(), + (ranged_weapon->ItemType == EQ::item::ItemTypeBow ? "bow" : "throwing"), + ranged_weapon->Name, + ranged_weapon->ID, + (ammo && ammo->ItemType == EQ::item::ItemTypeArrow ? " and arrow " : ""), + (ammo && ammo->ItemType == EQ::item::ItemTypeArrow ? ammo->Name : ""), + (ammo && ammo->ItemType == EQ::item::ItemTypeArrow ? std::to_string(ammo->ID) : "") + ); -void Bot::SetGuardMode() { + SendItemAnimation(other, ammo, (ranged_weapon->ItemType == EQ::item::ItemTypeBow ? EQ::skills::SkillArchery : EQ::skills::SkillThrowing)); + if (ranged_weapon->ItemType == EQ::item::ItemTypeBow) { + DoArcheryAttackDmg(other, ranged_item, ammo_item); - StopMoving(); - m_GuardPoint = GetPosition(); - SetGuardFlag(); + int chance_avoid_consume = aabonuses.ConsumeProjectile + itembonuses.ConsumeProjectile + spellbonuses.ConsumeProjectile; + bool consumes_ammo = RuleB(Bots, BotArcheryConsumesAmmo); + bool is_expendable_arrow = ranged_weapon->ExpendableArrow; + bool no_chance_to_avoid = chance_avoid_consume == 0; + bool failed_avoid_check = chance_avoid_consume < 100 && zone->random.Int(0, 99) > chance_avoid_consume; + bool should_consume_ammo = consumes_ammo && (is_expendable_arrow || no_chance_to_avoid || failed_avoid_check); - if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { - GetPet()->StopMoving(); + if (should_consume_ammo) { + ammo_item->SetCharges(ammo_item->GetCharges() - 1); + LogCombatDetail("Consumed Archery Ammo from slot {}.", EQ::invslot::slotAmmo); + + if (ammo_item->GetCharges() < 1) { + RemoveBotItemBySlot(EQ::invslot::slotAmmo); + BotRemoveEquipItem(EQ::invslot::slotAmmo); + } + } + else if (!consumes_ammo) { + LogCombatDetail("Archery Ammo Consumption is disabled."); + } + else { + LogCombatDetail("Endless Quiver prevented Ammo Consumption."); + } } -} + else { + DoThrowingAttackDmg(other, ranged_item); // watch + // Consume Ammo, unless Ammo Consumption is disabled + if (RuleB(Bots, BotThrowingConsumesAmmo)) { + ammo_item->SetCharges((ammo_item->GetCharges() - 1)); + LogCombatDetail("Consumed Throwing Ammo from slot {}.", EQ::invslot::slotAmmo); -void Bot::SetHoldMode() { + if (ammo_item->GetCharges() < 1) { + RemoveBotItemBySlot(EQ::invslot::slotAmmo); + BotRemoveEquipItem(EQ::invslot::slotAmmo); + } + } + else { + LogCombatDetail("Throwing Ammo Consumption is disabled."); + } + } - SetHoldFlag(); -} + CommonBreakInvisibleFromCombat(); -// AI Processing for the Bot object + return true; +} -constexpr float MAX_CASTER_DISTANCE[Class::PLAYER_CLASS_COUNT] = { - 0, - (34 * 34), - (24 * 24), - (28 * 28), - (26 * 26), - (42 * 42), - 0, - (30 * 30), - 0, - (38 * 38), - (54 * 54), - (48 * 48), - (52 * 52), - (50 * 50), - (32 * 32), - 0 -}; +bool Bot::CheckBotDoubleAttack(bool triple_attack) { + //Check for bonuses that give you a double attack chance regardless of skill (ie Bestial Frenzy/Harmonious Attack AA) + uint32 bonus_give_double_attack = aabonuses.GiveDoubleAttack + spellbonuses.GiveDoubleAttack + itembonuses.GiveDoubleAttack; -void Bot::AI_Process() -{ + if (!GetSkill(EQ::skills::SkillDoubleAttack) && !bonus_give_double_attack) { + return false; + } + + float chance = 0.0f; + uint16 skill = GetSkill(EQ::skills::SkillDoubleAttack); + + int32 bonus_double_attack = 0; + if ((GetClass() == Class::Paladin || GetClass() == Class::ShadowKnight) && (!HasTwoHanderEquipped())) { + LogCombatDetail("Knight class without a 2 hand weapon equipped = No DA Bonus!"); + } + else { + bonus_double_attack = aabonuses.DoubleAttackChance + spellbonuses.DoubleAttackChance + itembonuses.DoubleAttackChance; + } + + //Use skill calculations otherwise, if you only have AA applied GiveDoubleAttack chance then use that value as the base. + if (skill) { + chance = (float(skill + GetLevel()) * (float(100.0f + bonus_double_attack + bonus_give_double_attack) / 100.0f)) / 500.0f; + } + else { + chance = (float(bonus_give_double_attack + bonus_double_attack) / 100.0f); + } + + LogCombatDetail( + "skill [{}] bonus_give_double_attack [{}] bonus_double_attack [{}] chance [{}]", + skill, + bonus_give_double_attack, + bonus_double_attack, + chance + ); + + return zone->random.Roll(chance); +} + +bool Bot::CheckTripleAttack() +{ + int chance; + + if (RuleB(Combat, ClassicTripleAttack)) { + if ( + GetLevel() >= 60 && + ( + GetClass() == Class::Warrior || + GetClass() == Class::Ranger || + GetClass() == Class::Monk || + GetClass() == Class::Berserker + ) + ) { + switch (GetClass()) { + case Class::Warrior: + chance = RuleI(Combat, ClassicTripleAttackChanceWarrior); + break; + case Class::Ranger: + chance = RuleI(Combat, ClassicTripleAttackChanceRanger); + break; + case Class::Monk: + chance = RuleI(Combat, ClassicTripleAttackChanceMonk); + break; + case Class::Berserker: + chance = RuleI(Combat, ClassicTripleAttackChanceBerserker); + break; + default: + break; + } + } + } + else { + chance = GetSkill(EQ::skills::SkillTripleAttack); + } + + if (chance < 1) { + return false; + } + + int inc = aabonuses.TripleAttackChance + spellbonuses.TripleAttackChance + itembonuses.TripleAttackChance; + chance = static_cast(chance * (1 + inc / 100.0f)); + chance = (chance * 100) / (chance + 800); + + return zone->random.Int(1, 100) <= chance; +} + +void Bot::SetTarget(Mob *mob) +{ + if (mob != this) { + NPC::SetTarget(mob); + } +} + +void Bot::SetGuardMode() { + + StopMoving(); + m_GuardPoint = GetPosition(); + SetGuardFlag(); + + if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + GetPet()->StopMoving(); + } +} + +void Bot::SetHoldMode() { + + SetHoldFlag(); +} + +// AI Processing for the Bot object + +void Bot::AI_Process() +{ #define PULLING_BOT (GetPullingFlag() || GetReturningFlag()) #define NOT_PULLING_BOT (!GetPullingFlag() && !GetReturningFlag()) @@ -1901,19 +2128,21 @@ void Bot::AI_Process() return; } - auto raid = entity_list.GetRaidByBotName(GetName()); + Raid* raid = entity_list.GetRaidByBot(this); + SetStoredRaid(raid); uint32 r_group = RAID_GROUPLESS; - if (raid) { - raid->VerifyRaid(); - r_group = raid->GetGroup(GetName()); - if (mana_timer.Check(false)) { - raid->SendHPManaEndPacketsFrom(this); + if (raid) { + if (!GetVerifiedRaid()) { + raid->VerifyRaid(); + SetVerifiedRaid(true); } - if (send_hp_update_timer.Check(false)) { + if (mana_timer.Check() || send_hp_update_timer.Check()) { raid->SendHPManaEndPacketsFrom(this); } + + r_group = raid->GetGroup(GetName()); } auto bot_group = GetGroup(); @@ -1923,15 +2152,26 @@ void Bot::AI_Process() return; } - auto leash_owner = SetLeashOwner(bot_owner, bot_group, raid, r_group); + Client* leash_owner = bot_owner; if (!leash_owner) { return; } - SetFollowID(leash_owner->GetID()); + Mob* follow_mob = nullptr; + + if (!GetFollowID()) { + follow_mob = leash_owner; + } + else { + follow_mob = entity_list.GetMob(GetFollowID()); + + if (!follow_mob || !IsInGroupOrRaid(follow_mob)) { + follow_mob = leash_owner; + } + } - auto follow_mob = SetFollowMob(leash_owner); + SetFollowID(follow_mob->GetID()); SetBerserkState(); @@ -1940,6 +2180,13 @@ void Bot::AI_Process() return; } + if (HOLDING || (raid && r_group == RAID_GROUPLESS)) { + glm::vec3 Goal(0, 0, 0); + TryNonCombatMovementChecks(bot_owner, follow_mob, Goal); + + return; + } + float fm_distance = DistanceSquared(m_Position, follow_mob->GetPosition()); float lo_distance = DistanceSquared(m_Position, leash_owner->GetPosition()); float leash_distance = RuleR(Bots, LeashDistance); @@ -1950,8 +2197,11 @@ void Bot::AI_Process() return; } -// HEAL ROTATION CASTING CHECKS + _spell_target_list.clear(); + _group_spell_target_list.clear(); + SetTempSpellType(UINT16_MAX); +// HEAL ROTATION CASTING CHECKS HealRotationChecks(); if (GetAttackFlag()) { // Push owner's target onto our hate list @@ -1962,12 +2212,10 @@ void Bot::AI_Process() } //ALT COMBAT (ACQUIRE HATE) - glm::vec3 Goal(0, 0, 0); // We have aggro to choose from if (IsEngaged()) { - if (rest_timer.Enabled()) { rest_timer.Disable(); } @@ -1982,17 +2230,17 @@ void Bot::AI_Process() // RETURNING FLAG - else if (GetReturningFlag()) { - if (!ReturningFlagChecks(bot_owner, fm_distance)) { - return; - } + if (GetReturningFlag()) { + ReturningFlagChecks(bot_owner, leash_owner, fm_distance); + + return; } // DEFAULT (ACQUIRE TARGET) // VERIFY TARGET AND STANCE - auto tar = GetBotTarget(bot_owner); + if (!tar) { return; } @@ -2006,14 +2254,19 @@ void Bot::AI_Process() float tar_distance = DistanceSquared(m_Position, tar->GetPosition()); // TARGET VALIDATION - if (!IsValidTarget(bot_owner, leash_owner, lo_distance, leash_distance, tar, tar_distance)) { + if (HasPet()) { + if (tar && GetPet()->GetTarget() && GetPet()->GetTarget() == tar) { + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + } + } + return; } // This causes conflicts with default pet handler (bounces between targets) if (NOT_PULLING_BOT && HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { - // We don't add to hate list here because it's assumed to already be on the list GetPet()->SetTarget(tar); } @@ -2026,76 +2279,179 @@ void Bot::AI_Process() SendAddPlayerState(PlayerState::Aggressive); } +// COMBAT RANGE CALCS + bool front_mob = InFrontMob(tar, GetX(), GetY()); + bool behind_mob = BehindMob(tar, GetX(), GetY()); + uint8 stop_melee_level = GetStopMeleeLevel(); + tar_distance = sqrt(tar_distance); // sqrt this for future calculations + // Item variables + const EQ::ItemInstance* p_item = GetBotItem(EQ::invslot::slotPrimary); + const EQ::ItemInstance* s_item = GetBotItem(EQ::invslot::slotSecondary); + + CombatRangeInput input = { + .target = tar, + .target_distance = tar_distance, + .behind_mob = behind_mob, + .stop_melee_level = stop_melee_level, + .p_item = p_item, + .s_item = s_item + }; + + CombatRangeOutput o = EvaluateCombatRange(input); + + // Combat range variables + bool at_combat_range = o.at_combat_range; + float melee_distance_min = o.melee_distance_min; + float melee_distance = o.melee_distance; + float melee_distance_max = o.melee_distance_max; + // PULLING FLAG (ACTIONABLE RANGE) if (GetPullingFlag()) { + if (!TargetValidation(tar)) { return; } + + if (!DoLosChecks(tar)) { + return; + } + + if (at_combat_range) { + if ( + !tar->GetSpecialAbility(SpecialAbility::RangedAttackImmunity) && + RuleB(Bots, AllowRangedPulling) && + IsBotRanged() && + ranged_timer.Check(false) + ) { + StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); - constexpr size_t PULL_AGGRO = 5225; // spells[5225]: 'Throw Stone' - 0 cast time + if (BotRangedAttack(tar) && CheckDoubleRangedAttack()) { + BotRangedAttack(tar, true); + } - if (tar_distance <= (spells[PULL_AGGRO].range * spells[PULL_AGGRO].range)) { + ranged_timer.Start(); - StopMoving(); - CastSpell(PULL_AGGRO, tar->GetID()); - return; + return; + } + + if ( + RuleB(Bots, AllowAISpellPulling) && + !IsBotNonSpellFighter() && + AI_HasSpells() + ) { + SetPullingSpell(true); + AI_EngagedCastCheck(); + SetPullingSpell(false); + + return; + } } - } -// COMBAT RANGE CALCS + if (RuleB(Bots, UseSpellPulling)) { + uint16 spell_id = RuleI(Bots, PullSpellID); + + if (tar_distance <= spells[spell_id].range) { + StopMoving(); + SetPullingSpell(true); + CastSpell(spell_id, tar->GetID()); + SetPullingSpell(false); + + return; + } + } - bool atCombatRange; - const EQ::ItemInstance* p_item; - const EQ::ItemInstance* s_item; - CheckCombatRange(tar, tar_distance, atCombatRange, p_item, s_item); + TryPursueTarget(leash_distance, Goal); + + return; + } // ENGAGED AT COMBAT RANGE // We can fight - if (atCombatRange) { + if (at_combat_range) { + bool jitter_cooldown = false; - if (IsMoving()) { - StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); - return; + if (m_combat_jitter_timer.GetRemainingTime() > 1 && m_combat_jitter_timer.Enabled()) { + jitter_cooldown = true; } - if (AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == Class::Bard)) { - - if (TryEvade(tar)) { - return; + if ( + IsMoving() || + GetCombatJitterFlag() || + GetCombatOutOfRangeJitterFlag() + ) { + if ( + !GetCombatJitterFlag() || + !IsMoving() || + GetCombatOutOfRangeJitterFlag() + ) { + StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); } - if (TryFacingTarget(tar)) { + return; + } + + if ( + !jitter_cooldown && + AI_movement_timer->Check() && + (!spellend_timer.Enabled() || GetClass() == Class::Bard) + ) { + DoCombatPositioning(tar, Goal, stop_melee_level, tar_distance, melee_distance_min, melee_distance, melee_distance_max, behind_mob, front_mob); + return; + } + else { + if (!IsSitting() && !IsFacingMob(tar)) { + FaceTarget(tar); return; } } - if (!IsBotNonSpellFighter() && AI_EngagedCastCheck()) { + if (!IsBotNonSpellFighter() && AI_HasSpells() && AI_EngagedCastCheck()) { return; } - // Up to this point, GetTarget() has been safe to dereference since the initial - // if (!GetTarget() || GetAppearance() == eaDead) { return false; } call. Due to the chance of the target dying and our pointer - // being nullified, we need to test it before dereferencing to avoid crashes - - if (IsBotArcher() && TryRangedAttack(tar)) { + if (IsMoving()) { + StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); return; } - if (!IsBotArcher() && GetLevel() < GetStopMeleeLevel()) { - if (!TryClassAttacks(tar)) { - return; + if ( + !tar->GetSpecialAbility(SpecialAbility::RangedAttackImmunity) && + IsBotRanged() && + ranged_timer.Check(false) + ) { + if (BotRangedAttack(tar) && CheckDoubleRangedAttack()) { + BotRangedAttack(tar, true); } - if (!TryPrimaryWeaponAttacks(tar, p_item)) { - return; + ranged_timer.Start(); + } + else if (!IsBotRanged() && GetLevel() < stop_melee_level) { + if (!GetMaxMeleeRange() || !RuleB(Bots, DisableSpecialAbilitiesAtMaxMelee)) { + DoClassAttacks(tar); } - if (!TrySecondaryWeaponAttacks(tar, s_item)) { - return; + if (!TargetValidation(tar)) { return; } + + if (attack_timer.Check()) { + TryCombatProcs(p_item, tar, EQ::invslot::slotPrimary); + TriggerDefensiveProcs(tar, EQ::invslot::slotPrimary, false); + DoAttackRounds(tar, EQ::invslot::slotPrimary); + + if (TryDoubleMeleeRoundEffect()) { + DoAttackRounds(tar, EQ::invslot::slotPrimary); + } } - } - if (GetAppearance() == eaDead) { - return; + if (!TargetValidation(tar)) { return; } + + if (attack_dw_timer.Check()) { + if (CanThisClassDualWield() && CastToClient()->CheckDualWield()) { + TryCombatProcs(s_item, tar, EQ::invslot::slotSecondary); + DoAttackRounds(tar, EQ::invslot::slotSecondary); + } + } + + if (!TargetValidation(tar)) { return; } + } } @@ -2107,16 +2463,14 @@ void Bot::AI_Process() // End not in combat range - if (TryMeditate()) { - return; - } + TryMeditate(); } else { // Out-of-combat behavior - SetAttackFlag(false); + SetCombatRoundForAlerts(false); SetAttackingFlag(false); - if (!bot_owner->GetBotPulling()) { + if (!bot_owner->GetBotPulling()) { SetPullingFlag(false); SetReturningFlag(false); } @@ -2129,8 +2483,14 @@ void Bot::AI_Process() SetTarget(nullptr); - if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { - + if ( + HasPet() && + ( + GetClass() != Class::Enchanter || + GetPet()->GetPetType() != petAnimation || + GetAA(aaAnimationEmpathy) >= 1 + ) + ) { GetPet()->WipeHateList(); GetPet()->SetTarget(nullptr); } @@ -2145,16 +2505,19 @@ void Bot::AI_Process() if (TryNonCombatMovementChecks(bot_owner, follow_mob, Goal)) { return; } - if (TryIdleChecks(fm_distance)) { + if (!IsBotNonSpellFighter() && AI_HasSpells() && TryIdleChecks(fm_distance)) { return; } - if (TryBardMovementCasts()) { + if (!IsBotNonSpellFighter() && AI_HasSpells() && TryBardMovementCasts()) { return; } } } bool Bot::TryBardMovementCasts() {// Basically, bard bots get a chance to cast idle spells while moving + if (HOLDING) { + return false; + } if (GetClass() == Class::Bard && IsMoving() && NOT_PASSIVE && !spellend_timer.Enabled() && AI_think_timer->Check()) { @@ -2165,7 +2528,6 @@ bool Bot::TryBardMovementCasts() {// Basically, bard bots get a chance to cast i } bool Bot::TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob, glm::vec3& Goal) {// Non-engaged movement checks - if (AI_movement_timer->Check() && (!IsCasting() || GetClass() == Class::Bard)) { if (GUARDING) { Goal = GetGuardPoint(); @@ -2173,10 +2535,10 @@ bool Bot::TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob, g else { Goal = follow_mob->GetPosition(); } + float destination_distance = DistanceSquared(GetPosition(), Goal); if ((!bot_owner->GetBotPulling() || PULLING_BOT) && (destination_distance > GetFollowDistance())) { - if (!IsRooted()) { if (rest_timer.Enabled()) { rest_timer.Disable(); @@ -2190,12 +2552,12 @@ bool Bot::TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob, g else { if (IsMoving()) { - StopMoving(); return true; } } } + return false; } @@ -2210,18 +2572,10 @@ bool Bot::TryIdleChecks(float fm_distance) { !spellend_timer.Enabled() ) { - if (NOT_PASSIVE) { - - if (!AI_IdleCastCheck() && !IsCasting() && GetClass() != Class::Bard) { - BotMeditate(true); - } - - } else { - if (GetClass() != Class::Bard) { - BotMeditate(true); - } - + if (!AI_IdleCastCheck() && !IsCasting()) { + BotMeditate(IsSitting()); } + return true; } return false; @@ -2241,83 +2595,201 @@ bool Bot::TryAutoDefend(Client* bot_owner, float leash_distance) { if ( m_auto_defend_timer.Check() && - bot_owner->GetAggroCount() && NOT_HOLDING && NOT_PASSIVE ) { - auto xhaters = bot_owner->GetXTargetAutoMgr(); + XTargetAutoHaters* temp_haters; + std::vector assistee_haters; + std::vector assistee_members; + bool found = false; - if (xhaters && !xhaters->empty()) { - for (auto hater_iter : xhaters->get_list()) { - if (!hater_iter.spawn_id) { - continue; + if (bot_owner->GetAggroCount()) { + temp_haters = bot_owner->GetXTargetAutoMgr(); + + if (temp_haters && !temp_haters->empty()) { + assistee_haters.emplace_back(temp_haters); + assistee_members.emplace_back(bot_owner); + } + } + + if ( + (!bot_owner->GetAssistee() || !entity_list.GetClientByCharID(bot_owner->GetAssistee())) && + RuleB(Bots, AllowCrossGroupRaidAssist) + ) { + XTargetAutoHaters* temp_xhaters = bot_owner->GetXTargetAutoMgr(); + bool assistee_found = false; + + if (IsRaidGrouped()) { + Raid* raid = GetStoredRaid(); + if (raid) { + for (const auto& m : raid->members) { + if ( + m.member && + m.member->IsClient() && + m.member->GetAggroCount() && + raid->IsAssister(m.member_name) + ) { + temp_xhaters = m.member->GetXTargetAutoMgr(); + + if (!temp_xhaters || temp_xhaters->empty()) { + continue; + } + + assistee_haters.emplace_back(temp_xhaters); + assistee_members.emplace_back(m.member); + } + } } + } + else if (HasGroup()) { + Group* g = GetGroup(); + if (g) { + for (auto& m : g->members) { + if ( + m && + m->IsClient() && + m->CastToClient()->GetAggroCount() && + g->AmIMainAssist(m->GetName()) + ) { + temp_xhaters = m->CastToClient()->GetXTargetAutoMgr(); + + if (!temp_xhaters || temp_xhaters->empty()) { + continue; + } - if (bot_owner->GetBotPulling() && bot_owner->GetTarget() && hater_iter.spawn_id == bot_owner->GetTarget()->GetID()) { - continue; + assistee_haters.emplace_back(temp_xhaters); + assistee_members.emplace_back(m->CastToClient()); + } + } } + } + else { + return false; + } + } + else { + if (bot_owner->GetAssistee()) { + Client* c = entity_list.GetClientByCharID(bot_owner->GetAssistee()); - auto hater = entity_list.GetMob(hater_iter.spawn_id); - if (hater && hater->CastToNPC()->IsOnHatelist(bot_owner) && !hater->IsMezzed() && DistanceSquared(hater->GetPosition(), bot_owner->GetPosition()) <= leash_distance) { - // This is roughly equivilent to npc attacking a client pet owner - AddToHateList(hater, 1); - SetTarget(hater); - SetAttackingFlag(); + if (bot_owner->IsInGroupOrRaid(c) && c->GetAggroCount()) { + temp_haters = bot_owner->GetXTargetAutoMgr(); - if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { - GetPet()->AddToHateList(hater, 1); - GetPet()->SetTarget(hater); + if (temp_haters && !temp_haters->empty()) { + assistee_haters.emplace_back(temp_haters); + assistee_members.emplace_back(c); } + } + } + } + + if (!assistee_haters.empty()) { + for (XTargetAutoHaters* x_haters : assistee_haters) { + if (!x_haters->empty()) { + for (auto hater_iter : x_haters->get_list()) { + if (!hater_iter.spawn_id) { + continue; + } + + Mob* hater = nullptr; + + for (Client* x_member : assistee_members) { + if ( + x_member && + x_member->GetBotPulling() && + x_member->GetTarget() && + (hater_iter.spawn_id == x_member->GetTarget()->GetID()) + ) { + continue; + } - m_auto_defend_timer.Disable(); + hater = entity_list.GetMob(hater_iter.spawn_id); - return true; + if ( + hater && + !hater->IsMezzed() && + (DistanceSquared(hater->GetPosition(), bot_owner->GetPosition()) <= leash_distance) && + hater->CastToNPC()->IsOnHatelist(x_member) + ) { + break; + } + + hater = nullptr; + } + + if (hater) { + AddToHateList(hater, 1); + SetTarget(hater); + SetAttackingFlag(); + + if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + GetPet()->AddToHateList(hater, 1); + GetPet()->SetTarget(hater); + } + + m_auto_defend_timer.Disable(); + + return true; + } + } } } } + + return false; } } + return false; } bool Bot::TryMeditate() { - if (!IsMoving() && !spellend_timer.Enabled()) { - if (GetTarget() && AI_EngagedCastCheck()) { - BotMeditate(false); - } else if (GetArchetype() == Archetype::Caster) { - BotMeditate(true); + if (IsEngaged() && HasOrMayGetAggro(IsSitting())) { + if (IsSitting()) { + Stand(); + return false; + } } + BotMeditate(IsSitting()); + if (!(GetPlayerState() & static_cast(PlayerState::Aggressive))) { SendAddPlayerState(PlayerState::Aggressive); } return true; } + return false; } // This code actually gets processed when we are too far away from target and have not engaged yet bool Bot::TryPursueTarget(float leash_distance, glm::vec3& Goal) { - if (AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == Class::Bard)) { if (GetTarget() && !IsRooted()) { LogAIDetail("Pursuing [{}] while engaged", GetTarget()->GetCleanName()); Goal = GetTarget()->GetPosition(); + if (DistanceSquared(m_Position, Goal) <= leash_distance) { RunTo(Goal.x, Goal.y, Goal.z); - + SetCombatOutOfRangeJitter(); } else { WipeHateList(); SetTarget(nullptr); - if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + if ( + HasPet() && + ( + GetClass() != Class::Enchanter || + GetPet()->GetPetType() != petAnimation || + GetAA(aaAnimationEmpathy) >= 2 + ) + ) { GetPet()->WipeHateList(); GetPet()->SetTarget(nullptr); } } - return true; + return true; } else { if (IsMoving()) { StopMoving(); @@ -2333,156 +2805,116 @@ bool Bot::TryPursueTarget(float leash_distance, glm::vec3& Goal) { } // This is a mob that is fleeing either because it has been feared or is low on hitpoints - AI_PursueCastCheck(); + if (!HOLDING && !IsBotNonSpellFighter() && AI_HasSpells()) { + AI_PursueCastCheck(); + } return true; } + return false; } -bool Bot::TrySecondaryWeaponAttacks(Mob* tar, const EQ::ItemInstance* s_item) { +void Bot::DoAttackRounds(Mob* target, int hand) { + if (!target || (target && target->IsCorpse())) { + return; + } - if (!GetTarget() || GetAppearance() == eaDead) { return false; } - if (attack_dw_timer.Check() && CanThisClassDualWield()) { - const EQ::ItemData* s_itemdata = nullptr; + Attack(target, hand, false, false); - // Can only dual wield without a weapon if you're a monk - if (s_item || (GetClass() == Class::Monk)) { + bool candouble = CanThisClassDoubleAttack(); + // extra off hand non-sense, can only double with skill of 150 or above + // or you have any amount of GiveDoubleAttack + if (candouble && hand == EQ::invslot::slotSecondary) + candouble = + GetSkill(EQ::skills::SkillDoubleAttack) > 149 || + (aabonuses.GiveDoubleAttack + spellbonuses.GiveDoubleAttack + itembonuses.GiveDoubleAttack) > 0; - if (s_item) { - s_itemdata = s_item->GetItem(); - } + if (candouble) { + if (CheckBotDoubleAttack()) { + Attack(target, hand, false, false); - if (!s_itemdata) { - return false; - } + if (hand == EQ::invslot::slotPrimary) { + bool is_two_hander = HasTwoHanderEquipped(); - bool use_fist = true; - if (s_itemdata) { - use_fist = false; - } - - if (use_fist || !s_itemdata->IsType2HWeapon()) { + auto extra_attack_chance = is_two_hander + ? aabonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE] + + spellbonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE] + + itembonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE] + : aabonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_CHANCE] + + spellbonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_CHANCE] + + itembonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_CHANCE]; - float DualWieldProbability = 0.0f; + int extra_attack_amt_aas = is_two_hander + ? aabonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_NUM_ATKS] + : aabonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_NUM_ATKS]; - int32 Ambidexterity = (aabonuses.Ambidexterity + spellbonuses.Ambidexterity + itembonuses.Ambidexterity); - DualWieldProbability = ((GetSkill(EQ::skills::SkillDualWield) + GetLevel() + Ambidexterity) / 400.0f); // 78.0 max chance + int extra_attack_amt_spells = is_two_hander + ? spellbonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_NUM_ATKS] + : spellbonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_NUM_ATKS]; - int32 DWBonus = (spellbonuses.DualWieldChance + itembonuses.DualWieldChance); - DualWieldProbability += (DualWieldProbability * float(DWBonus) / 100.0f); + int extra_attack_amt_items = is_two_hander + ? itembonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_NUM_ATKS] + : itembonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_NUM_ATKS]; - float random = zone->random.Real(0, 1); - if (random < DualWieldProbability) { // Max 78% for DW chance - Attack(tar, EQ::invslot::slotSecondary); // Single attack with offhand + int extra_attack_amt = std::max({ extra_attack_amt_aas, extra_attack_amt_spells, extra_attack_amt_items }); - if (GetAppearance() == eaDead) { return false; } - TryCombatProcs(s_item, tar, EQ::invslot::slotSecondary); - - if (GetAppearance() == eaDead) { return false; } - if (CanThisClassDoubleAttack() && CheckBotDoubleAttack() && tar->GetHP() > -10) { - Attack(tar, EQ::invslot::slotSecondary); // Single attack with offhand + if (extra_attack_chance && zone->random.Roll(extra_attack_chance)) { + for (int i = 0; i < extra_attack_amt; i++) { + Attack(target, hand, false, false); } } } - } - } - return true; -} - -bool Bot::TryPrimaryWeaponAttacks(Mob* tar, const EQ::ItemInstance* p_item) { - - if (!GetTarget() || GetAppearance() == eaDead) { return false; } - if (attack_timer.Check()) { // Process primary weapon attacks - - Attack(tar, EQ::invslot::slotPrimary); - - if (GetAppearance() == eaDead) { return false; } - TriggerDefensiveProcs(tar, EQ::invslot::slotPrimary, false); - - if (GetAppearance() == eaDead) { return false; } - TryCombatProcs(p_item, tar, EQ::invslot::slotPrimary); - - if (GetAppearance() == eaDead) { return false; } - if (CanThisClassDoubleAttack()) { - - if (CheckBotDoubleAttack()) { - Attack(tar, EQ::invslot::slotPrimary, true); - } - if (GetAppearance() == eaDead) { return false; } - if (GetSpecialAbility(SpecialAbility::TripleAttack) && CheckBotDoubleAttack(true)) { + if (hand == EQ::invslot::slotSecondary) { + auto extra_attack_chance_secondary = + aabonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_CHANCE] + + spellbonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_CHANCE] + + itembonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_CHANCE]; - Attack(tar, EQ::invslot::slotPrimary, true); - } + if (extra_attack_chance_secondary && zone->random.Roll(extra_attack_chance_secondary)) { + auto extra_attack_amt_secondary = std::max({ + aabonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_NUM_ATKS], + spellbonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_NUM_ATKS], + itembonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_NUM_ATKS] + }); - if (GetAppearance() == eaDead) { return false; } - // quad attack, does this belong here?? - if (GetSpecialAbility(SpecialAbility::QuadrupleAttack) && CheckBotDoubleAttack(true)) { - Attack(tar, EQ::invslot::slotPrimary, true); + for (int i = 0; i < extra_attack_amt_secondary; i++) { + Attack(target, hand, false, false); + } + } } - } - if (GetAppearance() == eaDead) { return false; } - // Live AA - Flurry, Rapid Strikes ect (Flurry does not require Triple Attack). - if (int32 flurrychance = (aabonuses.FlurryChance + spellbonuses.FlurryChance + itembonuses.FlurryChance)) { + // you can only triple from the main hand + if (hand == EQ::invslot::slotPrimary && CanThisClassTripleAttack()) { + if (CheckTripleAttack()) { + Attack(target, hand, false, false); - if (zone->random.Int(0, 100) < flurrychance) { + int flurry_chance = aabonuses.FlurryChance + spellbonuses.FlurryChance + itembonuses.FlurryChance; - MessageString(Chat::NPCFlurry, YOU_FLURRY); - Attack(tar, EQ::invslot::slotPrimary, false); + if (flurry_chance && zone->random.Roll(flurry_chance)) { + Attack(target, hand, false, false); - if (GetAppearance() == eaDead) { return false; } - Attack(tar, EQ::invslot::slotPrimary, false); + if (zone->random.Roll(flurry_chance)) { + Attack(target, hand, false, false); + } + + if (GetOwner()) { + GetOwner()->MessageString( + Chat::NPCFlurry, + NPC_FLURRY, + GetCleanName(), + target->GetCleanName() + ); + } + } + } } } - - if (GetAppearance() == eaDead) { return false; } - auto ExtraAttackChanceBonus = - (spellbonuses.ExtraAttackChance[0] + itembonuses.ExtraAttackChance[0] + - aabonuses.ExtraAttackChance[0]); - - if ( - ExtraAttackChanceBonus && - p_item && - p_item->GetItem()->IsType2HWeapon() && - zone->random.Int(0, 100) < ExtraAttackChanceBonus - ) { - Attack(tar, EQ::invslot::slotPrimary, false); - } - } - return true; -} - -// We can't fight if we don't have a target, are stun/mezzed or dead.. -bool Bot::TryClassAttacks(Mob* tar) { - -// Stop attacking if the target is enraged - if (!GetTarget() || GetAppearance() == eaDead) { return false; } - if (tar->IsEnraged() && !BehindMob(tar, GetX(), GetY())) { - return false; - } - - // First, special attack per class (kick, backstab etc..) - DoClassAttacks(tar); - return true; -} - -bool Bot::TryRangedAttack(Mob* tar) { - - if (IsBotArcher() && ranged_timer.Check(false)) { - - if (!GetTarget() || GetAppearance() == eaDead) { return false; } - if (GetTarget()->GetHPRatio() <= 99.0f) { - BotRangedAttack(tar); - } - return true; } - return false; } bool Bot::TryFacingTarget(Mob* tar) { - if (!IsSitting() && !IsFacingMob(tar)) { FaceTarget(tar); return true; @@ -2492,108 +2924,125 @@ bool Bot::TryFacingTarget(Mob* tar) { bool Bot::TryEvade(Mob* tar) { + if (GetSpellTypeHold(BotSpellTypes::Escape)) { + return false; + } - if ( - !IsRooted() && - HasTargetReflection() && - !tar->IsFeared() && - !tar->IsStunned() && - GetClass() == Class::Rogue && - m_evade_timer.Check(false) - ) { - int timer_duration = (HideReuseTime - GetSkillReuseTime(EQ::skills::SkillHide)) * 1000; + switch (GetClass()) { + case Class::Rogue: { + if (GetSkill(EQ::skills::SkillHide) == 0) { + return false; + } - if (timer_duration < 0) { - timer_duration = 0; - } + if (m_rogue_evade_timer.Check(false)) { + int timer_duration = (HideReuseTime - GetSkillReuseTime(EQ::skills::SkillHide)) * 1000; - m_evade_timer.Start(timer_duration); - if (zone->random.Int(0, 260) < (int) GetSkill(EQ::skills::SkillHide)) { - RogueEvade(tar); - } - return true; - } + if (timer_duration < 0) { + timer_duration = 0; + } - return false; -} + m_rogue_evade_timer.Start(timer_duration); -void Bot::CheckCombatRange(Mob* tar, float tar_distance, bool& atCombatRange, const EQ::ItemInstance*& p_item, const EQ::ItemInstance*& s_item) { + if (zone->random.Int(0, 260) < (int)GetSkill(EQ::skills::SkillHide)) { + + } - atCombatRange= false; - p_item= GetBotItem(EQ::invslot::slotPrimary); - s_item= GetBotItem(EQ::invslot::slotSecondary); - bool behind_mob = false; - bool backstab_weapon = false; - if (GetClass() == Class::Rogue) { + float hidechance = ((GetSkill(EQ::skills::SkillHide) / 250.0f) + .25) * 100; + float random = zone->random.Real(0, 100); - behind_mob = BehindMob(tar, GetX(), GetY()); // Can be separated for other future use - backstab_weapon = p_item && p_item->GetItemBackstabDamage(); - } + if (random < hidechance) { + //SendAppearancePacket(AT_Invis, Invisibility::Invisible); + + if (spellbonuses.ShroudofStealth || aabonuses.ShroudofStealth || itembonuses.ShroudofStealth) { + improved_hidden = true; + hidden = true; + } + else { + hidden = true; + } + } - // Calculate melee distances - float melee_distance_max = 0.0f; - float melee_distance = 0.0f; + if (zone->random.Int(0, 260) < (int)GetSkill(EQ::skills::SkillHide)) { + RaidGroupSay(this, "I have momentarily ducked away from the main combat."); + RogueEvade(tar); + } + else { + RaidGroupSay(this, "My attempts at ducking clear of combat fail."); + } - CalcMeleeDistances(tar, p_item, s_item, behind_mob, backstab_weapon, melee_distance_max, melee_distance); + //SendAppearancePacket(AT_Invis, Invisibility::Visible); + hidden = false; - float caster_distance_max = GetBotCasterMaxRange(melee_distance_max); + return true; + } - bool atArcheryRange = IsArcheryRange(tar); + break; + } + case Class::Monk: { + if (GetSkill(EQ::skills::SkillFeignDeath) == 0) { + return false; + } - SetRangerCombatWeapon(atArcheryRange); + if (m_monk_evade_timer.Check(false)) { + int timer_duration = (FeignDeathReuseTime - GetSkillReuseTime(EQ::skills::SkillFeignDeath)) * 1000; - bool stop_melee_level = GetLevel() >= GetStopMeleeLevel(); - if (IsBotArcher() && atArcheryRange) { - atCombatRange = true; - } - else if (caster_distance_max && tar_distance <= caster_distance_max && stop_melee_level) { - atCombatRange = true; - } - else if (tar_distance <= melee_distance) { - atCombatRange = true; - } -} + if (timer_duration < 0) { + timer_duration = 0; + } -void Bot::SetRangerCombatWeapon(bool atArcheryRange) { + m_monk_evade_timer.Start(timer_duration); - if (GetRangerAutoWeaponSelect()) { - bool changeWeapons = false; + uint16 primfeign = GetSkill(EQ::skills::SkillFeignDeath); + uint16 secfeign = GetSkill(EQ::skills::SkillFeignDeath); + if (primfeign > 100) { + primfeign = 100; + secfeign = secfeign - 100; + secfeign = secfeign / 2; + } + else + secfeign = 0; - if (atArcheryRange && !IsBotArcher()) { - SetBotArcherySetting(true); - changeWeapons = true; - } + uint16 totalfeign = primfeign + secfeign; - else if (!atArcheryRange && IsBotArcher()) { - SetBotArcherySetting(false); - changeWeapons = true; - } + if (zone->random.Real(0, 160) > totalfeign) { + //SendAppearancePacket(AT_Anim, ANIM_DEATH); + RaidGroupSay(this, "I have fallen to the ground."); + SetFeigned(false); + } + else { + RaidGroupSay(this, "I have successfully feigned my death."); + SetFeigned(true); + //SendAppearancePacket(AT_Anim, ANIM_DEATH); + } - if (changeWeapons) { - ChangeBotArcherWeapons(IsBotArcher()); + //SendAppearancePacket(AT_Anim, ANIM_STAND); + SetFeigned(false); + return true; + } } } + + return false; } -void Bot::CalcMeleeDistances(const Mob* tar, const EQ::ItemInstance* const& p_item, const EQ::ItemInstance* const& s_item, bool behind_mob, bool backstab_weapon, float& melee_distance_max, float& melee_distance) const { +CombatRangeOutput Bot::EvaluateCombatRange(const CombatRangeInput& input) { + CombatRangeOutput o; float size_mod = GetSize(); - float other_size_mod = tar->GetSize(); + float other_size_mod = input.target->GetSize(); // For races with a fixed size if (GetRace() == Race::LavaDragon || GetRace() == Race::Wurm || GetRace() == Race::GhostDragon) { // size_mod = 60.0f; } - else if (size_mod < 6.0f) { size_mod = 8.0f; } // For races with a fixed size - if (tar->GetRace() == Race::LavaDragon || tar->GetRace() == Race::Wurm || tar->GetRace() == Race::GhostDragon) { + if (input.target->GetRace() == Race::LavaDragon || input.target->GetRace() == Race::Wurm || input.target->GetRace() == Race::GhostDragon) { other_size_mod = 60.0f; } - else if (other_size_mod < 6.0f) { other_size_mod = 8.0f; } @@ -2605,68 +3054,120 @@ void Bot::CalcMeleeDistances(const Mob* tar, const EQ::ItemInstance* const& p_it if (size_mod > 29.0f) { size_mod *= size_mod; } - else if (size_mod > 19.0f) { size_mod *= (size_mod * 2.0f); } - else { size_mod *= (size_mod * 4.0f); } + if (input.target->GetRace() == Race::VeliousDragon) // Lord Vyemm and other velious dragons + { + size_mod *= 1.75; + } + + if (input.target->GetRace() == Race::DragonSkeleton) // Dracoliche in Fear. Skeletal Dragon + { + size_mod *= 2.25; + } + + size_mod *= RuleR(Combat, HitBoxMod); // used for testing sizemods on different races. + // Prevention of ridiculously sized hit boxes if (size_mod > 10000.0f) { size_mod = (size_mod / 7.0f); } - melee_distance_max = size_mod; + o.melee_distance_max = size_mod; - switch (GetClass()) { - case Class::Warrior: - case Class::Paladin: - case Class::ShadowKnight: - if (p_item && p_item->GetItem()->IsType2HWeapon()) { - melee_distance = melee_distance_max * 0.45f; - } - else if ((s_item && s_item->GetItem()->IsTypeShield()) || (!p_item && !s_item)) { - melee_distance = melee_distance_max * 0.35f; - } - else { - melee_distance = melee_distance_max * 0.40f; - } - break; - case Class::Necromancer: - case Class::Wizard: - case Class::Magician: - case Class::Enchanter: - if (p_item && p_item->GetItem()->IsType2HWeapon()) { - melee_distance = melee_distance_max * 0.95f; - } - else { - melee_distance = melee_distance_max * 0.75f; - } - break; - case Class::Rogue: - if (behind_mob && backstab_weapon) { - if (p_item->GetItem()->IsType2HWeapon()) { - melee_distance = melee_distance_max * 0.30f; - } - else { - melee_distance = melee_distance_max * 0.25f; - } - break; - } - // Fall-through - default: - if (p_item && p_item->GetItem()->IsType2HWeapon()) { - melee_distance = melee_distance_max * 0.70f; - } - else { - melee_distance = melee_distance_max * 0.50f; + if (!RuleB(Bots, UseFlatNormalMeleeRange)) { + + bool is_two_hander = input.p_item && input.p_item->GetItem()->IsType2HWeapon(); + bool is_shield = input.s_item && input.s_item->GetItem()->IsTypeShield(); + bool is_backstab_weapon = input.p_item && input.p_item->GetItemBackstabDamage(); + + switch (GetClass()) { + case Class::Warrior: + case Class::Paladin: + case Class::ShadowKnight: + o.melee_distance = ( + is_two_hander ? o.melee_distance_max * 0.45f + : is_shield ? o.melee_distance_max * 0.35f + : o.melee_distance_max * 0.40f + ); + + break; + case Class::Necromancer: + case Class::Wizard: + case Class::Magician: + case Class::Enchanter: + o.melee_distance = ( + is_two_hander ? o.melee_distance_max * 0.95f + : o.melee_distance_max * 0.75f + ); + + break; + case Class::Rogue: + o.melee_distance = ( + input.behind_mob && is_backstab_weapon + ? o.melee_distance_max * 0.35f + : o.melee_distance_max * 0.50f + ); + + break; + default: + o.melee_distance = ( + is_two_hander ? o.melee_distance_max * 0.70f + : o.melee_distance_max * 0.50f + ); + + break; } - break; + o.melee_distance = sqrt(o.melee_distance); + o.melee_distance_max = sqrt(o.melee_distance_max); + } + else { + o.melee_distance_max = sqrt(o.melee_distance_max); + o.melee_distance = o.melee_distance_max * RuleR(Bots, NormalMeleeRangeDistance); + } + + if (o.melee_distance > RuleR(Bots, MaxDistanceForMelee)) { + o.melee_distance = RuleR(Bots, MaxDistanceForMelee); + } + + o.melee_distance_min = o.melee_distance * RuleR(Bots, PercentMinMeleeDistance); + + if (IsTaunting()) { + o.melee_distance_min = o.melee_distance * RuleR(Bots, PercentTauntMinMeleeDistance); + o.melee_distance = o.melee_distance * RuleR(Bots, TauntNormalMeleeRangeDistance); + } + + bool is_stop_melee_level = GetLevel() >= input.stop_melee_level; + + if (!IsTaunting() && !IsBotRanged() && !is_stop_melee_level && GetMaxMeleeRange()) { + o.melee_distance_min = o.melee_distance_max * RuleR(Bots, PercentMinMaxMeleeRangeDistance); + o.melee_distance = o.melee_distance_max * RuleR(Bots, PercentMaxMeleeRangeDistance); + } + + if (is_stop_melee_level && !IsBotRanged()) { + float desired_range = GetBotDistanceRanged(); + o.melee_distance_min = std::max(o.melee_distance, (desired_range / 2)); + o.melee_distance = std::max((o.melee_distance + 1), desired_range); } + + if (IsBotRanged()) { + float min_distance = RuleI(Combat, MinRangedAttackDist); + float max_distance = GetBotRangedValue(); + float desired_range = GetBotDistanceRanged(); + max_distance = (max_distance == 0 ? desired_range : max_distance); // stay ranged if set to ranged even if items/ammo aren't correct + o.melee_distance_min = std::max(min_distance, (desired_range / 2)); + o.melee_distance = std::min(max_distance, desired_range); + } + + o.at_combat_range = (input.target_distance <= o.melee_distance); + + return o; } bool Bot::IsValidTarget( @@ -2685,10 +3186,11 @@ bool Bot::IsValidTarget( bool invalid_target_state = false; if (HOLDING || !tar->IsNPC() || - tar->IsMezzed() || + (tar->IsMezzed() && !HasBotAttackFlag(tar)) || + (!Charmed() && tar->GetUltimateOwner()->IsOfClientBotMerc()) || lo_distance > leash_distance || tar_distance > leash_distance || - (!GetAttackingFlag() && !CheckLosFN(tar) && !leash_owner->CheckLosFN(tar)) || + (!GetAttackingFlag() && !CheckLosCheat(tar) && !leash_owner->CheckLosCheat(tar)) || !IsAttackAllowed(tar) ) { invalid_target_state = true; @@ -2707,6 +3209,7 @@ bool Bot::IsValidTarget( SetTarget(nullptr); SetAttackFlag(false); + SetCombatRoundForAlerts(false); SetAttackingFlag(false); if (PULLING_BOT) { @@ -2753,41 +3256,63 @@ Mob* Bot::GetBotTarget(Client* bot_owner) } } - if (GetArchetype() == Archetype::Caster) { - BotMeditate(true); - } + BotMeditate(IsSitting()); } return t; } -bool Bot::ReturningFlagChecks(Client* bot_owner, float fm_distance) {// Need to make it back to group before clearing return flag +bool Bot::TargetValidation(Mob* other) { + if (!other || GetAppearance() == eaDead) { + return false; + } - if (fm_distance <= GetFollowDistance()) { + return true; +} - // Once we're back, clear blocking flags so everyone else can join in +bool Bot::ReturningFlagChecks(Client* bot_owner, Mob* leash_owner, float fm_distance) { + if ( + (NOT_GUARDING && fm_distance <= GetFollowDistance()) || + (GUARDING && DistanceSquared(GetPosition(), GetGuardPoint()) <= GetFollowDistance()) + ) { // Once we're back, clear blocking flags so everyone else can join in SetReturningFlag(false); bot_owner->SetBotPulling(false); if (GetPet()) { GetPet()->SetPetOrder(m_previous_pet_order); } + + return false; } // Need to keep puller out of combat until they reach their 'return to' destination - if (HasTargetReflection()) { + WipeHateList(); - SetTarget(nullptr); - WipeHateList(); - return false; + if (!IsMoving()) { + glm::vec3 Goal(0, 0, 0); + + if (GUARDING) { + Goal = GetGuardPoint(); + } + else { + Mob* follow_mob = entity_list.GetMob(GetFollowID()); + + if (!follow_mob) { + follow_mob = leash_owner; + SetFollowID(leash_owner->GetID()); + } + + Goal = follow_mob->GetPosition(); + } + + RunTo(Goal.x, Goal.y, Goal.z); } + return true; } bool Bot::PullingFlagChecks(Client* bot_owner) { - if (!GetTarget()) { - WipeHateList(); SetTarget(nullptr); SetPullingFlag(false); @@ -2801,7 +3326,6 @@ bool Bot::PullingFlagChecks(Client* bot_owner) { return false; } else if (GetTarget()->GetHateList().size()) { - WipeHateList(); SetTarget(nullptr); SetPullingFlag(false); @@ -2823,7 +3347,6 @@ bool Bot::PullingFlagChecks(Client* bot_owner) { } void Bot::HealRotationChecks() { - if (IsMyHealRotationSet()) { if (AIHealRotation(HealRotationTarget(), UseHealRotationFastHeals())) { m_member_of_heal_rotation->SetMemberIsCasting(this); @@ -2838,7 +3361,6 @@ void Bot::HealRotationChecks() { } bool Bot::IsAIProcessValid(const Client* bot_owner, const Group* bot_group, const Raid* raid) { - if (!bot_owner || (!bot_group && !raid) || !IsAIControlled()) { return false; } @@ -2848,11 +3370,11 @@ bool Bot::IsAIProcessValid(const Client* bot_owner, const Group* bot_group, cons SetBotOwner(nullptr); return false; } + return true; } bool Bot::CheckIfCasting(float fm_distance) { - if (IsCasting()) { if (IsHealRotationMember() && m_member_of_heal_rotation->CastingOverride() && @@ -2864,12 +3386,10 @@ bool Bot::CheckIfCasting(float fm_distance) { InterruptSpell(); } else if (AmICastingForHealRotation() && m_member_of_heal_rotation->CastingMember() == this) { - AdvanceHealRotation(false); return true; } else if (GetClass() != Class::Bard) { - if (IsEngaged()) { return true; } @@ -2887,13 +3407,12 @@ bool Bot::CheckIfCasting(float fm_distance) { else if (IsHealRotationMember()) { m_member_of_heal_rotation->SetMemberIsCasting(this, false); } + return false; } bool Bot::CheckIfIncapacitated() { - if (GetPauseAI() || IsStunned() || IsMezzed() || (GetAppearance() == eaDead)) { - if (IsCasting()) { InterruptSpell(); } @@ -2902,6 +3421,7 @@ bool Bot::CheckIfIncapacitated() { AdvanceHealRotation(false); m_member_of_heal_rotation->SetMemberIsCasting(this, false); } + return true; } @@ -2910,18 +3430,35 @@ bool Bot::CheckIfIncapacitated() { return true; } + if (currently_fleeing) { + if (RuleB(Combat, EnableFearPathing) && AI_movement_timer->Check()) { + // Check if we have reached the last fear point + if (DistanceNoZ(glm::vec3(GetX(), GetY(), GetZ()), m_FearWalkTarget) <= 5.0f) { + // Calculate a new point to run to + StopNavigation(); + CalculateNewFearpoint(); + } + + RunTo( + m_FearWalkTarget.x, + m_FearWalkTarget.y, + m_FearWalkTarget.z + ); + } + + return true; + } + return false; } void Bot::SetBerserkState() {// Berserk updates should occur if primary AI criteria are met if (GetClass() == Class::Warrior || GetClass() == Class::Berserker) { - - if (!berserk && GetHP() > 0 && GetHPRatio() < 30.0f) { + if (!berserk && GetHPRatio() < RuleI(Combat, BerserkerFrenzyStart)) { entity_list.MessageCloseString(this, false, 200, 0, BERSERK_START, GetName()); berserk = true; } - - if (berserk && GetHPRatio() >= 30.0f) { + if (berserk && GetHPRatio() > RuleI(Combat, BerserkerFrenzyEnd)) { entity_list.MessageCloseString(this, false, 200, 0, BERSERK_END, GetName()); berserk = false; } @@ -2930,29 +3467,29 @@ void Bot::SetBerserkState() {// Berserk updates should occur if primary AI crite Mob* Bot::SetFollowMob(Client* leash_owner) { Mob* follow_mob = entity_list.GetMob(GetFollowID()); - if (!follow_mob) { + if (!follow_mob) { follow_mob = leash_owner; SetFollowID(leash_owner->GetID()); } + return follow_mob; } Client* Bot::SetLeashOwner(Client* bot_owner, Group* bot_group, Raid* raid, uint32 r_group) const { - Client* leash_owner = nullptr; + if (raid && r_group < MAX_RAID_GROUPS && raid->GetGroupLeader(r_group)) { leash_owner = raid->GetGroupLeader(r_group) && raid->GetGroupLeader(r_group)->IsClient() ? raid->GetGroupLeader(r_group)->CastToClient() : bot_owner; - } else if (bot_group) { leash_owner = (bot_group->GetLeader() && bot_group->GetLeader()->IsClient() ? bot_group->GetLeader()->CastToClient() : bot_owner); - } else { leash_owner = bot_owner; } + return leash_owner; } @@ -2969,15 +3506,15 @@ void Bot::SetOwnerTarget(Client* bot_owner) { bot_owner->SetBotPulling(false); if (NOT_HOLDING && NOT_PASSIVE) { - auto attack_target = bot_owner->GetTarget(); - if (attack_target && HasBotAttackFlag(attack_target)) { + if (attack_target && HasBotAttackFlag(attack_target)) { InterruptSpell(); WipeHateList(); AddToHateList(attack_target, 1); SetTarget(attack_target); SetAttackingFlag(); + if (GetPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { GetPet()->WipeHateList(); GetPet()->AddToHateList(attack_target, 1); @@ -2989,6 +3526,7 @@ void Bot::SetOwnerTarget(Client* bot_owner) { void Bot::BotPullerProcess(Client* bot_owner, Raid* raid) { SetAttackFlag(false); + SetCombatRoundForAlerts(false); SetAttackingFlag(false); SetPullFlag(false); SetPullingFlag(false); @@ -2999,10 +3537,10 @@ void Bot::BotPullerProcess(Client* bot_owner, Raid* raid) { auto pull_target = bot_owner->GetTarget(); if (pull_target) { if (raid) { - const auto msg = fmt::format("Pulling {} to the group..", pull_target->GetCleanName()); + const auto msg = fmt::format("Pulling {}.", pull_target->GetCleanName()); raid->RaidSay(msg.c_str(), GetCleanName(), 0, 100); } else { - BotGroupSay( + RaidGroupSay( this, fmt::format( "Pulling {}.", @@ -3017,11 +3555,12 @@ void Bot::BotPullerProcess(Client* bot_owner, Raid* raid) { SetTarget(pull_target); SetPullingFlag(); bot_owner->SetBotPulling(); - if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { + if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { GetPet()->WipeHateList(); - GetPet()->SetTarget(nullptr); + GetPet()->SetTarget(nullptr); m_previous_pet_order = GetPet()->GetPetOrder(); + GetPet()->CastToNPC()->SaveGuardSpot(GetPosition()); GetPet()->SetPetOrder(SPO_Guard); } } @@ -3106,8 +3645,17 @@ bool Bot::Spawn(Client* botCharacterOwner) { // Load pet LoadPet(); SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0); - ping_timer.Start(8000); - auto_save_timer.Start(RuleI(Bots, AutosaveIntervalSeconds) * 1000); + m_ping_timer.Start(8000); + m_auto_save_timer.Start(RuleI(Bots, AutosaveIntervalSeconds) * 1000); + + m_dont_heal_me_before = 0; + m_dont_buff_me_before = Timer::GetCurrentTime() + 400; + m_dont_dot_me_before = 0; + m_dont_root_me_before = 0; + m_dont_snare_me_before = 0; + m_dont_cure_me_before = 0; + + // there is something askew with spawn struct appearance fields... // I re-enabled this until I can sort it out const auto& m = GetBotItemSlots(); @@ -3121,6 +3669,10 @@ bool Bot::Spawn(Client* botCharacterOwner) { } } + if (IsBotRanged()) { + ChangeBotRangedWeapons(true); + } + if (auto raid = entity_list.GetRaidByBotName(GetName())) { // Safety Check to confirm we have a valid raid auto owner = GetBotOwner(); @@ -3130,9 +3682,12 @@ bool Bot::Spawn(Client* botCharacterOwner) { SetRaidGrouped(true); raid->LearnMembers(); raid->VerifyRaid(); + SetStoredRaid(raid); + p_raid_instance = raid; + SetVerifiedRaid(true); } } - else if (auto group = entity_list.GetGroupByMobName(GetName())) { + else if (auto group = entity_list.GetGroupByMob(this)) { // Safety Check to confirm we have a valid group auto owner = GetBotOwner(); if (owner && !group->IsGroupMember(owner->GetCleanName())) { @@ -3144,6 +3699,13 @@ bool Bot::Spawn(Client* botCharacterOwner) { } } + MapSpellTypeLevels(); + + if (RuleB(Bots, RunSpellTypeChecksOnSpawn)) { + OwnerMessage("Running SpellType checks. There may be some spells that are mislabeled as incorrect. Use this as a loose guideline."); + CheckBotSpells(); //This runs through a serious of checks and outputs any spells that are set to the wrong spell type in the database + } + return true; } @@ -3427,7 +3989,6 @@ void Bot::LevelBotWithClient(Client* c, uint8 new_level, bool send_appearance) { parse->EventBot(EVENT_LEVEL_UP, e, nullptr, std::to_string(levels_change), 0); } - e->SetPetChooser(false); // not sure what this does, but was in bot 'update' code e->CalcBotStats(c->GetBotOption(Client::booStatsUpdate)); if (send_appearance) { @@ -3660,7 +4221,6 @@ bool Bot::AddBotToGroup(Bot* bot, Group* group) { if (bot && group->AddMember(bot)) { if (group->GetLeader()) { - bot->SetFollowID(group->GetLeader()->GetID()); // Need to send this only once when a group is formed with a bot so the client knows it is also the group leader if (group->GroupCount() == 2 && group->GetLeader()->IsClient()) { group->UpdateGroupAAs(); @@ -3676,7 +4236,7 @@ bool Bot::AddBotToGroup(Bot* bot, Group* group) { } // Completes a trade with a client bot owner -void Bot::FinishTrade(Client* client, BotTradeType trade_type) +void Bot::FinishTrade(Client* client, BotTradeType trade_type, int16 chosen_slot) { if ( !client || @@ -3700,7 +4260,7 @@ void Bot::FinishTrade(Client* client, BotTradeType trade_type) else if (trade_type == BotTradeClientNoDropNoTrade) { // Items being traded are found on the Client's cursor slot, slot id 30. This item can be either a single item or it can be a bag. // If it is a bag, then we have to search for items in slots 331 thru 340 - PerformTradeWithClient(EQ::invslot::slotCursor, EQ::invslot::slotCursor, client); + PerformTradeWithClient(EQ::invslot::slotCursor, EQ::invslot::slotCursor, client, chosen_slot); // TODO: Add logic here to test if the item in SLOT_CURSOR is a container type, if it is then we need to call the following: // PerformTradeWithClient(331, 340, client); @@ -3708,7 +4268,7 @@ void Bot::FinishTrade(Client* client, BotTradeType trade_type) } // Perfoms the actual trade action with a client bot owner -void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* client) +void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* client, int16 chosen_slot) { using namespace EQ; @@ -3728,14 +4288,16 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* ClientReturn(ItemInstance* item, int16 from) : return_item_instance(item), from_bot_slot(from) { } }; - static const int16 bot_equip_order[invslot::EQUIPMENT_COUNT] = { - invslot::slotCharm, invslot::slotEar1, invslot::slotHead, invslot::slotFace, - invslot::slotEar2, invslot::slotNeck, invslot::slotShoulders, invslot::slotArms, - invslot::slotBack, invslot::slotWrist1, invslot::slotWrist2, invslot::slotRange, - invslot::slotHands, invslot::slotPrimary, invslot::slotSecondary, invslot::slotFinger1, - invslot::slotFinger2, invslot::slotChest, invslot::slotLegs, invslot::slotFeet, - invslot::slotWaist, invslot::slotPowerSource, invslot::slotAmmo - }; + std::vector bot_equip_order; + + if (chosen_slot != INVALID_INDEX) { + bot_equip_order.emplace_back(chosen_slot); + } + else { + for (int16 i = 0; i < invslot::EQUIPMENT_COUNT; ++i) { + bot_equip_order.push_back(i); + } + } enum { stageStackable = 0, stageEmpty, stageReplaceable }; @@ -3824,23 +4386,44 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* return; } - if (trade_instance->IsStackable() && trade_instance->GetCharges() < trade_instance->GetItem()->StackSize) { // temp until partial stacks are implemented - if (trade_event_exists) { - event_trade.push_back(ClientTrade(trade_instance, trade_index)); - continue; - } - else { + if (RuleI(Bots, StackSizeMin) != -1) { + if ( + trade_instance->IsStackable() && + trade_instance->GetCharges() < RuleI(Bots, StackSizeMin) + ) { // temp until partial stacks are implemented + if (trade_event_exists) { + event_trade.push_back(ClientTrade(trade_instance, trade_index)); + continue; + } + client->Message( Chat::Yellow, fmt::format( - "{} is only a partially stacked item, the trade has been cancelled!", - item_link + "{} is too small of a stack, you need atleast {}, the trade has been cancelled!", + item_link, + RuleI(Bots, StackSizeMin) ).c_str() ); client->ResetTrade(); + return; + } } + else if ( + trade_instance->IsStackable() && + trade_instance->GetCharges() < trade_instance->GetItem()->StackSize + ) { + client->Message( + Chat::Yellow, + fmt::format( + "{} is only a partially stacked item, the trade has been cancelled!", + item_link + ).c_str() + ); + client->ResetTrade(); + return; + } for (int m = EQ::invaug::SOCKET_BEGIN; m <= EQ::invaug::SOCKET_END; ++m) { const auto augment = trade_instance->GetAugment(m); @@ -3899,9 +4482,15 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* } if ( - !trade_instance->IsClassEquipable(GetClass()) || + ( + !trade_instance->IsClassEquipable(GetClass()) && + !RuleB(Bots, AllowBotEquipAnyClassGear) + ) || GetLevel() < trade_instance->GetItem()->ReqLevel || - (!trade_instance->IsRaceEquipable(GetBaseRace()) && !RuleB(Bots, AllowBotEquipAnyRaceGear)) + ( + !trade_instance->IsRaceEquipable(GetBaseRace()) && + !RuleB(Bots, AllowBotEquipAnyRaceGear) + ) ) { if (trade_event_exists) { event_trade.push_back(ClientTrade(trade_instance, trade_index)); @@ -4429,15 +5018,15 @@ float Bot::GetProcChances(float ProcBonus, uint16 hand) { float ProcChance = 0.0f; uint32 weapon_speed = 0; switch (hand) { - case EQ::invslot::slotPrimary: - weapon_speed = attack_timer.GetDuration(); - break; - case EQ::invslot::slotSecondary: - weapon_speed = attack_dw_timer.GetDuration(); - break; - case EQ::invslot::slotRange: - weapon_speed = ranged_timer.GetDuration(); - break; + case EQ::invslot::slotPrimary: + weapon_speed = attack_timer.GetDuration(); + break; + case EQ::invslot::slotSecondary: + weapon_speed = attack_dw_timer.GetDuration(); + break; + case EQ::invslot::slotRange: + weapon_speed = ranged_timer.GetDuration(); + break; } if (weapon_speed < RuleI(Combat, MinHastedDelay)) @@ -4546,84 +5135,93 @@ int Bot::GetBaseSkillDamage(EQ::skills::SkillType skill, Mob *target) int base = EQ::skills::GetBaseDamage(skill); auto skill_level = GetSkill(skill); switch (skill) { - case EQ::skills::SkillDragonPunch: - case EQ::skills::SkillEagleStrike: - case EQ::skills::SkillTigerClaw: - if (skill_level >= 25) - base++; - if (skill_level >= 75) - base++; - if (skill_level >= 125) - base++; - if (skill_level >= 175) - base++; - return base; - case EQ::skills::SkillFrenzy: - if (GetBotItem(EQ::invslot::slotPrimary)) { - if (GetLevel() > 15) - base += GetLevel() - 15; - if (base > 23) - base = 23; - if (GetLevel() > 50) - base += 2; - if (GetLevel() > 54) + case EQ::skills::SkillDragonPunch: + case EQ::skills::SkillEagleStrike: + case EQ::skills::SkillTigerClaw: + if (skill_level >= 25) base++; - if (GetLevel() > 59) + if (skill_level >= 75) base++; - } - return base; - case EQ::skills::SkillFlyingKick: { - float skill_bonus = skill_level / 9.0f; - float ac_bonus = 0.0f; - auto inst = GetBotItem(EQ::invslot::slotFeet); - if (inst) - ac_bonus = inst->GetItemArmorClass(true) / 25.0f; - if (ac_bonus > skill_bonus) - ac_bonus = skill_bonus; - return static_cast(ac_bonus + skill_bonus); - } - case EQ::skills::SkillKick: { - float skill_bonus = skill_level / 10.0f; - float ac_bonus = 0.0f; - auto inst = GetBotItem(EQ::invslot::slotFeet); - if (inst) - ac_bonus = inst->GetItemArmorClass(true) / 25.0f; - if (ac_bonus > skill_bonus) - ac_bonus = skill_bonus; - return static_cast(ac_bonus + skill_bonus); - } - case EQ::skills::SkillBash: { - float skill_bonus = skill_level / 10.0f; - float ac_bonus = 0.0f; - const EQ::ItemInstance *inst = nullptr; - if (HasShieldEquipped()) - inst = GetBotItem(EQ::invslot::slotSecondary); - else if (HasTwoHanderEquipped()) - inst = GetBotItem(EQ::invslot::slotPrimary); - if (inst) - ac_bonus = inst->GetItemArmorClass(true) / 25.0f; - if (ac_bonus > skill_bonus) - ac_bonus = skill_bonus; - return static_cast(ac_bonus + skill_bonus); - } - case EQ::skills::SkillBackstab: { - float skill_bonus = static_cast(skill_level) * 0.02f; - auto inst = GetBotItem(EQ::invslot::slotPrimary); - if (inst && inst->GetItem() && inst->GetItem()->ItemType == EQ::item::ItemType1HPiercing) { - base = inst->GetItemBackstabDamage(true); - if (!inst->GetItemBackstabDamage()) - base += inst->GetItemWeaponDamage(true); - if (target) { - if (inst->GetItemElementalFlag(true) && inst->GetItemElementalDamage(true)) - base += target->ResistElementalWeaponDmg(inst); - if (inst->GetItemBaneDamageBody(true) || inst->GetItemBaneDamageRace(true)) - base += target->CheckBaneDamage(inst); - } - } - return static_cast(static_cast(base) * (skill_bonus + 2.0f)); - } - default: - return 0; + if (skill_level >= 125) + base++; + if (skill_level >= 175) + base++; + return base; + case EQ::skills::SkillFrenzy: + if (GetBotItem(EQ::invslot::slotPrimary)) { + if (GetLevel() > 15) + base += GetLevel() - 15; + if (base > 23) + base = 23; + if (GetLevel() > 50) + base += 2; + if (GetLevel() > 54) + base++; + if (GetLevel() > 59) + base++; + } + return base; + case EQ::skills::SkillFlyingKick: { + float skill_bonus = skill_level / 9.0f; + float ac_bonus = 0.0f; + auto inst = GetBotItem(EQ::invslot::slotFeet); + if (inst) + ac_bonus = inst->GetItemArmorClass(true) / 25.0f; + if (ac_bonus > skill_bonus) + ac_bonus = skill_bonus; + return static_cast(ac_bonus + skill_bonus); + } + case EQ::skills::SkillKick: { + float skill_bonus = skill_level / 10.0f; + float ac_bonus = 0.0f; + auto inst = GetBotItem(EQ::invslot::slotFeet); + if (inst) + ac_bonus = inst->GetItemArmorClass(true) / 25.0f; + if (ac_bonus > skill_bonus) + ac_bonus = skill_bonus; + return static_cast(ac_bonus + skill_bonus); + } + case EQ::skills::SkillBash: { + float skill_bonus = skill_level / 10.0f; + float ac_bonus = 0.0f; + const EQ::ItemInstance *inst = nullptr; + if (HasShieldEquipped()) + inst = GetBotItem(EQ::invslot::slotSecondary); + else if (HasTwoHanderEquipped()) + inst = GetBotItem(EQ::invslot::slotPrimary); + if (inst) + ac_bonus = inst->GetItemArmorClass(true) / 25.0f; + if (ac_bonus > skill_bonus) + ac_bonus = skill_bonus; + return static_cast(ac_bonus + skill_bonus); + } + case EQ::skills::SkillBackstab: { + float skill_bonus = static_cast(skill_level) * 0.02f; + auto inst = GetBotItem(EQ::invslot::slotPrimary); + if (inst && inst->GetItem() && inst->GetItem()->ItemType == EQ::item::ItemType1HPiercing) { + base = inst->GetItemBackstabDamage(true); + if (!inst->GetItemBackstabDamage()) + base += inst->GetItemWeaponDamage(true); + if (target) { + if ( + inst->GetItemElementalFlag(true) && + inst->GetItemElementalDamage(true) + ) { + base += target->ResistElementalWeaponDmg(inst); + } + + if ( + inst->GetItemBaneDamageBody(true) || + inst->GetItemBaneDamageRace(true) + ) { + base += target->CheckBaneDamage(inst); + } + } + } + return static_cast(static_cast(base) * (skill_bonus + 2.0f)); + } + default: + return 0; } } @@ -4678,107 +5276,30 @@ void Bot::DoSpecialAttackDamage(Mob *who, EQ::skills::SkillType skill, int32 max TrySkillProc(who, skill, (ReuseTime * 1000), true); } -void Bot::TryBackstab(Mob *other, int ReuseTime) { - if (!other) - return; - - bool bIsBehind = false; - bool bCanFrontalBS = false; - const EQ::ItemInstance* inst = GetBotItem(EQ::invslot::slotPrimary); - const EQ::ItemData* botpiercer = nullptr; - if (inst) - botpiercer = inst->GetItem(); - - if (!botpiercer || (botpiercer->ItemType != EQ::item::ItemType1HPiercing)) { - BotGroupSay(this, "I can't backstab with this weapon!"); - return; - } - - int tripleChance = (itembonuses.TripleBackstab + spellbonuses.TripleBackstab + aabonuses.TripleBackstab); - if (BehindMob(other, GetX(), GetY())) - bIsBehind = true; - else { - int FrontalBSChance = (itembonuses.FrontalBackstabChance + spellbonuses.FrontalBackstabChance + aabonuses.FrontalBackstabChance); - if (FrontalBSChance && (FrontalBSChance > zone->random.Int(0, 100))) - bCanFrontalBS = true; - } - - if (bIsBehind || bCanFrontalBS) { - int chance = (10 + (GetDEX() / 10) + (itembonuses.HeroicDEX / 10)); - if (level >= 60 && other->GetLevel() <= 45 && !other->CastToNPC()->IsEngaged() && other->GetHP()<= 32000 && other->IsNPC() && zone->random.Real(0, 99) < chance) { - entity_list.MessageCloseString(this, false, 200, Chat::MeleeCrit, ASSASSINATES, GetName()); - RogueAssassinate(other); - } else { - RogueBackstab(other); - if (level > 54) { - float DoubleAttackProbability = ((GetSkill(EQ::skills::SkillDoubleAttack) + GetLevel()) / 500.0f); - if (zone->random.Real(0, 1) < DoubleAttackProbability) { - if (other->GetHP() > 0) - RogueBackstab(other,false,ReuseTime); - - if (tripleChance && other->GetHP() > 0 && tripleChance > zone->random.Int(0, 100)) - RogueBackstab(other,false,ReuseTime); - } - } - } - } else if (aabonuses.FrontalBackstabMinDmg || itembonuses.FrontalBackstabMinDmg || spellbonuses.FrontalBackstabMinDmg) { - m_specialattacks = eSpecialAttacks::ChaoticStab; - RogueBackstab(other, true); - m_specialattacks = eSpecialAttacks::None; - } - else - Attack(other, EQ::invslot::slotPrimary); -} - -void Bot::RogueBackstab(Mob *other, bool min_damage, int ReuseTime) -{ - if (!other) - return; - - EQ::ItemInstance *botweaponInst = GetBotItem(EQ::invslot::slotPrimary); - if (botweaponInst) { - if (!GetWeaponDamage(other, botweaponInst)) - return; - } else if (!GetWeaponDamage(other, (const EQ::ItemData *)nullptr)) { - return; - } - - int64 hate = 0; - - int base_damage = GetBaseSkillDamage(EQ::skills::SkillBackstab, other); - hate = base_damage; - - DoSpecialAttackDamage(other, EQ::skills::SkillBackstab, base_damage, 0, hate, ReuseTime); - DoAnim(anim1HPiercing); -} - -void Bot::RogueAssassinate(Mob* other) { - EQ::ItemInstance* botweaponInst = GetBotItem(EQ::invslot::slotPrimary); - if (botweaponInst) { - if (GetWeaponDamage(other, botweaponInst)) - other->Damage(this, 32000, SPELL_UNKNOWN, EQ::skills::SkillBackstab); - else - other->Damage(this, -5, SPELL_UNKNOWN, EQ::skills::SkillBackstab); - } - - DoAnim(anim1HPiercing); -} - void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { - if (!target || spellend_timer.Enabled() || IsFeared() || IsStunned() || IsMezzed() || DivineAura() || GetHP() < 0 || !IsAttackAllowed(target)) + if ( + !target || + GetAppearance() == eaDead || + spellend_timer.Enabled() || + IsFeared() || + IsStunned() || + IsMezzed() || + DivineAura() || + GetHP() < 0 || + !IsAttackAllowed(target) + ) { return; + } bool taunt_time = taunt_timer.Check(); bool ca_time = classattack_timer.Check(false); bool ma_time = monkattack_timer.Check(false); - bool ka_time = knightattack_timer.Check(false); if (taunt_time) { // Bots without this skill shouldn't be 'checking' on this timer..let's just disable it and avoid the extra IsAttackAllowed() checks // Note: this is done here instead of NPC::ctor() because taunt skill can be acquired during level ups (the timer is re-enabled in CalcBotStats()) if (!GetSkill(EQ::skills::SkillTaunt)) { - taunt_timer.Disable(); return; } @@ -4788,39 +5309,13 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { } } - if ((ca_time || ma_time || ka_time) && !IsAttackAllowed(target)) { + if ((ca_time || ma_time) && !IsAttackAllowed(target)) { return; } - if (ka_time) { - - switch (GetClass()) { - case Class::ShadowKnight: { - CastSpell(SPELL_NPC_HARM_TOUCH, target->GetID()); - knightattack_timer.Start(HarmTouchReuseTime * 1000); - - break; - } - case Class::Paladin: { - if (GetHPRatio() < 20) { - CastSpell(SPELL_LAY_ON_HANDS, GetID()); - knightattack_timer.Start(LayOnHandsReuseTime * 1000); - } - else { - knightattack_timer.Start(2000); - } - - break; - } - default: { - break; - } - } - } - - if (taunting && target->IsNPC() && taunt_time) { + if (IsTaunting() && target->IsNPC() && taunt_time) { if (GetTarget() && GetTarget()->GetHateTop() && GetTarget()->GetHateTop() != this) { - BotGroupSay( + RaidGroupSay( this, fmt::format( "Taunting {}.", @@ -4834,58 +5329,58 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { if (ma_time) { switch (GetClass()) { - case Class::Monk: { - int reuse = (MonkSpecialAttack(target, EQ::skills::SkillTigerClaw) - 1); - - // Live AA - Technique of Master Wu - int wuchance = itembonuses.DoubleSpecialAttack + spellbonuses.DoubleSpecialAttack + aabonuses.DoubleSpecialAttack; - - if (wuchance) { - const int MonkSPA[5] = { - EQ::skills::SkillFlyingKick, - EQ::skills::SkillDragonPunch, - EQ::skills::SkillEagleStrike, - EQ::skills::SkillTigerClaw, - EQ::skills::SkillRoundKick - }; - int extra = 0; - // always 1/4 of the double attack chance, 25% at rank 5 (100/4) - while (wuchance > 0) { - if (zone->random.Roll(wuchance)) { - ++extra; - } - else { - break; + case Class::Monk: { + int reuse = (MonkSpecialAttack(target, EQ::skills::SkillTigerClaw) - 1); + + // Live AA - Technique of Master Wu + int wuchance = itembonuses.DoubleSpecialAttack + spellbonuses.DoubleSpecialAttack + aabonuses.DoubleSpecialAttack; + + if (wuchance) { + const int MonkSPA[5] = { + EQ::skills::SkillFlyingKick, + EQ::skills::SkillDragonPunch, + EQ::skills::SkillEagleStrike, + EQ::skills::SkillTigerClaw, + EQ::skills::SkillRoundKick + }; + int extra = 0; + // always 1/4 of the double attack chance, 25% at rank 5 (100/4) + while (wuchance > 0) { + if (zone->random.Roll(wuchance)) { + ++extra; + } + else { + break; + } + wuchance /= 4; } - wuchance /= 4; - } - Mob* bo = GetBotOwner(); - if (bo && bo->IsClient() && bo->CastToClient()->GetBotOption(Client::booMonkWuMessage)) { + Mob* bo = GetBotOwner(); + if (bo && bo->IsClient() && bo->CastToClient()->GetBotOption(Client::booMonkWuMessage)) { - bo->Message( - GENERIC_EMOTE, - "The spirit of Master Wu fills %s! %s gains %d additional attack(s).", - GetCleanName(), - GetCleanName(), - extra - ); - } + bo->Message( + GENERIC_EMOTE, + "The spirit of Master Wu fills %s! %s gains %d additional attack(s).", + GetCleanName(), + GetCleanName(), + extra + ); + } - auto classic = RuleB(Combat, ClassicMasterWu); - while (extra) { - MonkSpecialAttack(GetTarget(), (classic ? MonkSPA[zone->random.Int(0, 4)] : EQ::skills::SkillTigerClaw)); - --extra; + auto classic = RuleB(Combat, ClassicMasterWu); + while (extra && TargetValidation(GetTarget())) { + MonkSpecialAttack(GetTarget(), (classic ? MonkSPA[zone->random.Int(0, 4)] : EQ::skills::SkillTigerClaw)); + --extra; + } } - } - float HasteModifier = (GetHaste() * 0.01f); - monkattack_timer.Start((reuse * 1000) / HasteModifier); + float HasteModifier = (GetHaste() * 0.01f); + monkattack_timer.Start((reuse * 1000) / HasteModifier); - break; - } - default: - break; + break; + } + default: + break; } } @@ -4898,53 +5393,77 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { int bot_level = GetLevel(); int reuse = (TauntReuseTime * 1000); // Same as Bash and Kick bool did_attack = false; + switch (GetClass()) { case Class::Warrior: - if (bot_level >= RuleI(Combat, NPCBashKickLevel)) { - bool canBash = false; - if ((GetRace() == OGRE || GetRace() == TROLL || GetRace() == BARBARIAN) || (m_inv.GetItem(EQ::invslot::slotSecondary) && m_inv.GetItem(EQ::invslot::slotSecondary)->GetItem()->ItemType == EQ::item::ItemTypeShield) || (m_inv.GetItem(EQ::invslot::slotPrimary) && m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->IsType2HWeapon() && GetAA(aa2HandBash) >= 1)) - canBash = true; + case Class::Cleric: + case Class::ShadowKnight: + case Class::Paladin: + { + bool is_large_race = ( + GetBaseRace() == OGRE || + GetBaseRace() == TROLL || + GetBaseRace() == BARBARIAN + ); + bool has_bash_skill = GetSkill(EQ::skills::SkillBash) > 0; + bool has_shield_in_secondary = + m_inv.GetItem(EQ::invslot::slotSecondary) && + m_inv.GetItem(EQ::invslot::slotSecondary)->GetItem()->ItemType == EQ::item::ItemTypeShield; + bool has_two_hander_with_aa = + m_inv.GetItem(EQ::invslot::slotPrimary) && + m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->IsType2HWeapon() && + GetAA(aa2HandBash) >= 1; + bool can_bash = + is_large_race || + ( + has_bash_skill && + ( + has_shield_in_secondary || has_two_hander_with_aa + ) + ); - if (!canBash || zone->random.Int(0, 100) > 25) - skill_to_use = EQ::skills::SkillKick; - else + if (can_bash) { skill_to_use = EQ::skills::SkillBash; + } + + break; } case Class::Ranger: case Class::Beastlord: - skill_to_use = EQ::skills::SkillKick; + if (GetSkill(EQ::skills::SkillKick)) { + skill_to_use = EQ::skills::SkillKick; + } + break; case Class::Berserker: - skill_to_use = EQ::skills::SkillFrenzy; - break; - case Class::Cleric: - case Class::ShadowKnight: - case Class::Paladin: - if (bot_level >= RuleI(Combat, NPCBashKickLevel)) { - if ((GetRace() == OGRE || GetRace() == TROLL || GetRace() == BARBARIAN) || (m_inv.GetItem(EQ::invslot::slotSecondary) && m_inv.GetItem(EQ::invslot::slotSecondary)->GetItem()->ItemType == EQ::item::ItemTypeShield) || (m_inv.GetItem(EQ::invslot::slotPrimary) && m_inv.GetItem(EQ::invslot::slotPrimary)->GetItem()->IsType2HWeapon() && GetAA(aa2HandBash) >= 1)) - skill_to_use = EQ::skills::SkillBash; + if (GetSkill(EQ::skills::SkillFrenzy)) { + skill_to_use = EQ::skills::SkillFrenzy; } + break; case Class::Monk: - if (GetLevel() >= 30) { + if (GetSkill(EQ::skills::SkillFlyingKick)) { skill_to_use = EQ::skills::SkillFlyingKick; } - else if (GetLevel() >= 25) { + else if (GetSkill(EQ::skills::SkillDragonPunch)) { skill_to_use = EQ::skills::SkillDragonPunch; } - else if (GetLevel() >= 20) { + else if (GetSkill(EQ::skills::SkillEagleStrike)) { skill_to_use = EQ::skills::SkillEagleStrike; } - else if (GetLevel() >= 5) { + else if (GetSkill(EQ::skills::SkillRoundKick)) { skill_to_use = EQ::skills::SkillRoundKick; } - else { + else if (GetSkill(EQ::skills::SkillKick)) { skill_to_use = EQ::skills::SkillKick; } break; case Class::Rogue: - skill_to_use = EQ::skills::SkillBackstab; + if (GetSkill(EQ::skills::SkillBackstab)) { + skill_to_use = EQ::skills::SkillBackstab; + } + break; } @@ -4962,18 +5481,37 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { } if (skill_to_use == EQ::skills::SkillFrenzy) { - int AtkRounds = 3; + int AtkRounds = 1; + float HasteMod = (FrenzyReuseTime - 1) / (GetHaste() * 0.01f); + reuse = (FrenzyReuseTime * 1000); DoAnim(anim2HSlashing); - reuse = (FrenzyReuseTime * 1000); - did_attack = true; - while(AtkRounds > 0) { - if (GetTarget() && (AtkRounds == 1 || zone->random.Int(0, 100) < 75)) { - DoSpecialAttackDamage(GetTarget(), EQ::skills::SkillFrenzy, dmg, 0, dmg, reuse, true); + // bards can do riposte frenzy for some reason + if (!IsRiposte && GetClass() == Class::Berserker) { + int chance = GetLevel() * 2 + GetSkill(EQ::skills::SkillFrenzy); + + if (zone->random.Roll0(450) < chance) { + AtkRounds++; + } + + if (zone->random.Roll0(450) < chance) { + AtkRounds++; + } + } + + while (AtkRounds > 0) { + if (GetTarget() != this && TargetValidation(GetTarget())) { + DoSpecialAttackDamage(GetTarget(), EQ::skills::SkillFrenzy, dmg, 0, dmg, HasteMod); } AtkRounds--; } + + if (reuse > 0 && IsRiposte) { + reuse = 0; + } + + did_attack = true; } if (skill_to_use == EQ::skills::SkillKick) { @@ -5031,7 +5569,7 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { } auto classic = RuleB(Combat, ClassicMasterWu); - while (extra) { + while (extra && TargetValidation(GetTarget())) { MonkSpecialAttack(GetTarget(), (classic ? MonkSPA[zone->random.Int(0, 4)] : skill_to_use)); --extra; } @@ -5044,11 +5582,14 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { if (skill_to_use == EQ::skills::SkillBackstab) { reuse = (BackstabReuseTime * 1000); did_attack = true; - if (IsRiposte) + + if (IsRiposte) { reuse = 0; + } - TryBackstab(target,reuse); + Mob::TryBackstab(target, reuse); } + classattack_timer.Start(reuse / HasteModifier); } @@ -5160,9 +5701,9 @@ void Bot::BotOrderCampAll(Client* c, uint8 class_id) { void Bot::ProcessBotOwnerRefDelete(Mob* botOwner) { if (botOwner && botOwner->IsClient()) { - std::list BotList = entity_list.GetBotsByBotOwnerCharacterID(botOwner->CastToClient()->CharacterID()); + std::vector BotList = entity_list.GetBotsByBotOwnerCharacterID(botOwner->CastToClient()->CharacterID()); if (!BotList.empty()) { - for (std::list::iterator botListItr = BotList.begin(); botListItr != BotList.end(); ++botListItr) { + for (std::vector::iterator botListItr = BotList.begin(); botListItr != BotList.end(); ++botListItr) { Bot* tempBot = *botListItr; if (tempBot) { tempBot->SetTarget(nullptr); @@ -5337,7 +5878,7 @@ bool Bot::CastSpell( ) { bool Result = false; if (zone && !zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))) { - // LogSpells("CastSpell called for spell [{}] ([{}]) on entity [{}], slot [{}], time [{}], mana [{}], from item slot [{}]", spells[spell_id].name, spell_id, target_id, slot, cast_time, mana_cost, (item_slot==0xFFFFFFFF)?999:item_slot); + // LogSpells("CastSpell called for spell [{}] ([{}]) on entity [{}], slot [{}], time [{}], mana [{}], from item slot [{}]", GetSpellName(spell_id), spell_id, target_id, slot, cast_time, mana_cost, (item_slot==0xFFFFFFFF)?999:item_slot); if (casting_spell_id == spell_id) { ZeroCastingVars(); @@ -5349,22 +5890,13 @@ bool Bot::CastSpell( casting_spell_id || delaytimer || spellend_timer.Enabled() || - IsStunned() || - IsFeared() || - IsMezzed() || + ((IsStunned() || IsMezzed() || DivineAura()) && !IsCastNotStandingSpell(spell_id)) || (IsSilenced() && !IsDiscipline(spell_id)) || (IsAmnesiad() && IsDiscipline(spell_id)) ) { LogSpellsDetail("Spell casting canceled: not able to cast now. Valid? [{}] casting [{}] waiting? [{}] spellend? [{}] stunned? [{}] feared? [{}] mezed? [{}] silenced? [{}]", IsValidSpell(spell_id), casting_spell_id, delaytimer, spellend_timer.Enabled(), IsStunned(), IsFeared(), IsMezzed(), IsSilenced() ); - if (IsSilenced() && !IsDiscipline(spell_id)) { - MessageString(Chat::White, SILENCED_STRING); - } - - if (IsAmnesiad() && IsDiscipline(spell_id)) { - MessageString(Chat::White, MELEE_SILENCE); - } if (casting_spell_id) { AI_Bot_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); @@ -5375,7 +5907,7 @@ bool Bot::CastSpell( } if (IsDetrimentalSpell(spell_id) && !zone->CanDoCombat()) { - MessageString(Chat::White, SPELL_WOULDNT_HOLD); + //MessageString(Chat::White, SPELL_WOULDNT_HOLD); if (casting_spell_id) { AI_Bot_Event_SpellCastFinished(false, static_cast(casting_spell_slot)); } @@ -5385,7 +5917,7 @@ bool Bot::CastSpell( if (DivineAura()) { LogSpellsDetail("Spell casting canceled: cannot cast while Divine Aura is in effect"); - InterruptSpell(173, 0x121, false); + InterruptSpell(SPELL_FIZZLE, 0x121, false); return false; } @@ -5483,14 +6015,17 @@ bool Bot::IsImmuneToSpell(uint16 spell_id, Mob *caster) { bool Bot::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_center, CastAction_type &CastAction, EQ::spells::CastingSlot slot) { bool Result = false; - SpellTargetType targetType = spells[spell_id].target_type; - if (targetType == ST_GroupClientAndPet) { + SpellTargetType target_type = spells[spell_id].target_type; + + if (target_type == ST_GroupClientAndPet) { if ((spell_id == 1768 && zone->GetZoneID() == 202) || (!IsDetrimentalSpell(spell_id))) { CastAction = SingleTarget; return true; } } + Result = Mob::DetermineSpellTargets(spell_id, spell_target, ae_center, CastAction, slot); + return Result; } @@ -5622,27 +6157,47 @@ bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, EQ::spe if ( spellTarget && - IsGrouped() && - ( - spellTarget->IsBot() || - spellTarget->IsClient() - ) && - RuleB(Bots, GroupBuffing) + GetClass() != Class::Bard && + (IsGrouped() || (IsRaidGrouped() && GetStoredRaid()->GetGroup(GetCleanName()) != RAID_GROUPLESS)) && + (spellTarget->IsBot() || spellTarget->IsClient()) && + (RuleB(Bots, GroupBuffing) || RuleB(Bots, RaidBuffing)) ) { bool noGroupSpell = false; uint16 thespell = spell_id; - for (int i = 0; i < AIBot_spells.size(); i++) { + for (int i = 0; i < AIBot_spells.size(); i++) { // TODO bot rewrite - fix this to reduce loop with AIBot_spells_by_type? int j = BotGetSpells(i); int spelltype = BotGetSpellType(i); bool spellequal = (j == thespell); - bool spelltypeequal = ((spelltype == 2) || (spelltype == 16) || (spelltype == 32)); - bool spelltypetargetequal = ((spelltype == 8) && (spells[thespell].target_type == ST_Self)); - bool spelltypeclassequal = ((spelltype == 1024) && (GetClass() == Class::Shaman)); + bool spelltypeequal = ( + (spelltype == BotSpellTypes::RegularHeal) || + (spelltype == BotSpellTypes::Escape) || + (spelltype == BotSpellTypes::Pet) + ); + bool spelltypetargetequal = ( + (spelltype == BotSpellTypes::Buff) && + (spells[thespell].target_type == ST_Self) + ); + bool spelltypeclassequal = ( + (spelltype == BotSpellTypes::InCombatBuff) && + (GetClass() == Class::Shaman) + ); bool slotequal = (slot == EQ::spells::CastingSlot::Item); + if (spellequal || slotequal) { - if ((spelltypeequal || spelltypetargetequal) || spelltypeclassequal || slotequal) { - if (((spells[thespell].effect_id[0] == 0) && (spells[thespell].base_value[0] < 0)) && - (spellTarget->GetHP() < ((spells[thespell].base_value[0] * (-1)) + 100))) { + if ( + ( + spelltypeequal || spelltypetargetequal + ) || + spelltypeclassequal || + slotequal + ) { + if ( + ( + (spells[thespell].effect_id[0] == 0) && + (spells[thespell].base_value[0] < 0) + ) && + (spellTarget->GetHP() < ((spells[thespell].base_value[0] * (-1)) + 100)) + ) { LogSpells("GroupBuffing failure"); return false; } @@ -5655,33 +6210,48 @@ bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, EQ::spe } if (!noGroupSpell) { - Group *g = GetGroup(); - if (g) { - for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if (g->members[i]) { - if ((g->members[i]->GetClass() == Class::Necromancer) && (IsEffectInSpell(thespell, SE_AbsorbMagicAtt) || IsEffectInSpell(thespell, SE_Rune))) { + for (Mob* m : GetBuffTargets(spellTarget)) { + if (IsEffectInSpell(thespell, SE_AbsorbMagicAtt) || IsEffectInSpell(thespell, SE_Rune)) { + for (int i = 0; i < m->GetMaxTotalSlots(); i++) { + uint32 buff_count = m->GetMaxTotalSlots(); + + for (unsigned int j = 0; j < buff_count; j++) { + if (IsValidSpell(m->GetBuffs()[j].spellid)) { + if (IsLichSpell(m->GetBuffs()[j].spellid)) { + continue; + } + } } - else - SpellOnTarget(thespell, g->members[i]); - - if (g->members[i] && g->members[i]->GetPetID()) - SpellOnTarget(thespell, g->members[i]->GetPet()); } } - SetMana(GetMana() - (GetActSpellCost(thespell, spells[thespell].mana) * (g->GroupCount() - 1))); + + SpellOnTarget(thespell, m); + + if ( + m->GetPetID() && + ( + !RuleB(Bots, RequirePetAffinity) || + m->HasPetAffinity() + ) + ) { + SpellOnTarget(thespell, m->GetPet()); + } + + SetMana(GetMana() - (GetActSpellCost(thespell, spells[thespell].mana) * (GetBuffTargets(spellTarget).size() - 1))); } } + stopLogic = true; } + return true; } bool Bot::DoFinishedSpellGroupTarget(uint16 spell_id, Mob* spellTarget, EQ::spells::CastingSlot slot, bool& stopLogic) { bool isMainGroupMGB = false; - Raid* raid = entity_list.GetRaidByBotName(GetName()); if (isMainGroupMGB && (GetClass() != Class::Bard)) { - BotGroupSay( + RaidGroupSay( this, fmt::format( "Casting {} as a Mass Group Buff.", @@ -5691,30 +6261,29 @@ bool Bot::DoFinishedSpellGroupTarget(uint16 spell_id, Mob* spellTarget, EQ::spel SpellOnTarget(spell_id, this); entity_list.AESpell(this, this, spell_id, true); } - else if (raid) - { - std::vector raid_group_members = raid->GetRaidGroupMembers(raid->GetGroup(GetName())); - for (auto iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { - if (iter->member) { - SpellOnTarget(spell_id, iter->member); - if (iter->member && iter->member->GetPetID()) - SpellOnTarget(spell_id, iter->member ->GetPet()); - } + else { + if (spellTarget != this) { + SpellOnTarget(spell_id, this); } - } - else - { - Group *g = GetGroup(); - if (g) { - for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) { - if (g->members[i]) { - SpellOnTarget(spell_id, g->members[i]); - if (g->members[i] && g->members[i]->GetPetID()) - SpellOnTarget(spell_id, g->members[i]->GetPet()); + + if (spellTarget->IsOfClientBotMerc()) { + for (Mob* m : GetBuffTargets(spellTarget)) { + if (m == this && spellTarget != this) { + continue; + } + + SpellOnTarget(spell_id, m); + + if (m->GetPetID() && (!RuleB(Bots, RequirePetAffinity) || m->HasPetAffinity())) { + SpellOnTarget(spell_id, m->GetPet()); } } } + else if (spellTarget->IsPet() && !spellTarget->IsFamiliar() && spellTarget->GetOwner() && (!RuleB(Bots, RequirePetAffinity) || spellTarget->GetOwner()->HasPetAffinity())) { + SpellOnTarget(spell_id, spellTarget); + } } + stopLogic = true; return true; } @@ -5783,9 +6352,9 @@ int32 Bot::GetMaxStat() { int32 Bot::GetMaxResist() { int level = GetLevel(); int32 base = 500; - if (level > 60) - base += ((level - 60) * 5); - + if (level > 65) { + base += ((level - 65) * 5); + } return base; } @@ -6159,28 +6728,45 @@ int64 Bot::CalcHPRegen() { } int64 Bot::CalcManaRegen() { - uint8 level = GetLevel(); - uint8 botclass = GetClass(); - int32 regen = 0; - if (IsSitting()) { - BuffFadeBySitModifier(); - if (botclass != Class::Warrior && botclass != Class::Monk && botclass != Class::Rogue && botclass != Class::Berserker) { - regen = ((((GetSkill(EQ::skills::SkillMeditate) / 10) + (level - (level / 4))) / 4) + 4); - regen += (spellbonuses.ManaRegen + itembonuses.ManaRegen); - } else - regen = (2 + spellbonuses.ManaRegen + itembonuses.ManaRegen); - } else - regen = (2 + spellbonuses.ManaRegen + itembonuses.ManaRegen); + uint8 level = GetLevel(); + uint8 bot_class = GetClass(); - regen += aabonuses.ManaRegen + itembonuses.heroic_mana_regen; + // Default values + int32 regen = 2; // Default regen for non-sitting state + float mana_regen_rate = std::max(0.0f, RuleR(Bots, ManaRegen)); - regen = ((regen * RuleI(Character, ManaRegenMultiplier)) / 100); - float mana_regen_rate = RuleR(Bots, ManaRegen); - if (mana_regen_rate < 0.0f) - mana_regen_rate = 0.0f; + if (bot_class == Class::Bard) { + regen = IsSitting() ? 2 : 1; - regen = (regen * mana_regen_rate); - return regen; + if (IsSitting()) { + BuffFadeBySitModifier(); + } + + regen += itembonuses.ManaRegen + aabonuses.ManaRegen; + + return regen; + } + + if (IsSitting()) { + BuffFadeBySitModifier(); + + if (GetArchetype() != Archetype::Melee) { + regen = (((GetSkill(EQ::skills::SkillMeditate) / 10) + (level - (level / 4))) / 4) + 4; + } + } + + regen += spellbonuses.ManaRegen + itembonuses.ManaRegen; + + if (IsHeroicINTCasterClass(bot_class)) { + regen += itembonuses.HeroicINT * RuleR(Character, HeroicIntelligenceMultiplier) / 25; + } else if (IsHeroicWISCasterClass(bot_class)) { + regen += itembonuses.HeroicWIS * RuleR(Character, HeroicWisdomMultiplier) / 25; + } + + regen += aabonuses.ManaRegen; + regen = (regen * RuleI(Character, ManaRegenMultiplier)) / 100; + + return static_cast(regen * mana_regen_rate); } uint64 Bot::GetClassHPFactor() { @@ -6362,17 +6948,6 @@ void Bot::DoEnduranceUpkeep() { } void Bot::Camp(bool save_to_database) { - - if (IsEngaged() || GetBotOwner()->IsEngaged()) { - GetBotOwner()->Message( - Chat::White, - fmt::format( - "You cannot camp your bots while in combat" - ).c_str() - ); - return; - } - Sit(); LeaveHealRotationMemberPool(); @@ -6389,7 +6964,7 @@ void Bot::Camp(bool save_to_database) { } void Bot::Zone() { - if (auto raid = entity_list.GetRaidByBotName(GetName())) { + if (auto raid = entity_list.GetRaidByBot(this)) { raid->MemberZoned(CastToClient()); } else if (HasGroup()) { @@ -6400,17 +6975,21 @@ void Bot::Zone() { Depop(); } -bool Bot::IsArcheryRange(Mob *target) { +bool Bot::IsAtRange(Mob *target) { bool result = false; + if (target) { - float range = (GetBotArcheryRange() + 5.0); + float range = (GetBotRangedValue() + 5.0); range *= range; float targetDistance = DistanceSquaredNoZ(m_Position, target->GetPosition()); float minRuleDistance = (RuleI(Combat, MinRangedAttackDist) * RuleI(Combat, MinRangedAttackDist)); - if ((targetDistance > range) || (targetDistance < minRuleDistance)) + + if ((targetDistance > range) || (targetDistance < minRuleDistance)) { result = false; - else + } + else { result = true; + } } return result; } @@ -6444,107 +7023,107 @@ void Bot::UpdateGroupCastingRoles(const Group* group, bool disband) // GroupHealer switch (iter->GetClass()) { - case Class::Cleric: - if (!healer) - healer = iter; - else - switch (healer->GetClass()) { - case Class::Cleric: - break; - default: + case Class::Cleric: + if (!healer) healer = iter; - } + else + switch (healer->GetClass()) { + case Class::Cleric: + break; + default: + healer = iter; + } - break; - case Class::Druid: - if (!healer) - healer = iter; - else - switch (healer->GetClass()) { - case Class::Cleric: - case Class::Druid: - break; - default: + break; + case Class::Druid: + if (!healer) healer = iter; - } - break; - case Class::Shaman: - if (!healer) - healer = iter; - else - switch (healer->GetClass()) { - case Class::Cleric: - case Class::Druid: - case Class::Shaman: - break; - default: + else + switch (healer->GetClass()) { + case Class::Cleric: + case Class::Druid: + break; + default: + healer = iter; + } + break; + case Class::Shaman: + if (!healer) healer = iter; - } - break; - case Class::Paladin: - case Class::Ranger: - case Class::Beastlord: - if (!healer) - healer = iter; - break; - default: - break; - } + else + switch (healer->GetClass()) { + case Class::Cleric: + case Class::Druid: + case Class::Shaman: + break; + default: + healer = iter; + } + break; + case Class::Paladin: + case Class::Ranger: + case Class::Beastlord: + if (!healer) + healer = iter; + break; + default: + break; + } - // GroupSlower - switch (iter->GetClass()) { - case Class::Shaman: - if (!slower) - slower = iter; - else - switch (slower->GetClass()) { + // GroupSlower + switch (iter->GetClass()) { case Class::Shaman: + if (!slower) + slower = iter; + else + switch (slower->GetClass()) { + case Class::Shaman: + break; + default: + slower = iter; + } break; - default: - slower = iter; - } - break; - case Class::Enchanter: - if (!slower) - slower = iter; - else - switch (slower->GetClass()) { - case Class::Shaman: case Class::Enchanter: + if (!slower) + slower = iter; + else + switch (slower->GetClass()) { + case Class::Shaman: + case Class::Enchanter: + break; + default: + slower = iter; + } + break; + case Class::Beastlord: + if (!slower) + slower = iter; break; default: - slower = iter; - } - break; - case Class::Beastlord: - if (!slower) - slower = iter; - break; - default: - break; - } + break; + } - // GroupNuker - switch (iter->GetClass()) { - // wizard - // magician - // necromancer - // enchanter - // druid - // cleric - // shaman - // shadowknight - // paladin - // ranger - // beastlord - default: - break; - } + // GroupNuker + switch (iter->GetClass()) { + // wizard + // magician + // necromancer + // enchanter + // druid + // cleric + // shaman + // shadowknight + // paladin + // ranger + // beastlord + default: + break; + } - // GroupDoter - switch (iter->GetClass()) { - default: - break; + // GroupDoter + switch (iter->GetClass()) { + default: + break; } } @@ -6557,7 +7136,7 @@ void Bot::UpdateGroupCastingRoles(const Group* group, bool disband) Bot* Bot::GetBotByBotClientOwnerAndBotName(Client* c, const std::string& botName) { Bot* Result = nullptr; if (c) { - std::list BotList = entity_list.GetBotsByBotOwnerCharacterID(c->CharacterID()); + std::vector BotList = entity_list.GetBotsByBotOwnerCharacterID(c->CharacterID()); if (!BotList.empty()) { for (auto botListItr = BotList.begin(); botListItr != BotList.end(); ++botListItr) { if (std::string((*botListItr)->GetCleanName()) == botName) { @@ -6572,11 +7151,26 @@ Bot* Bot::GetBotByBotClientOwnerAndBotName(Client* c, const std::string& botName void Bot::ProcessBotGroupInvite(Client* c, std::string const& botName) { if (c && !c->HasRaid()) { - Bot* invitedBot = GetBotByBotClientOwnerAndBotName(c, botName); + Bot* invitedBot = entity_list.GetBotByBotName(botName); + if (!invitedBot) { return; } + if ( + invitedBot->GetBotOwnerCharacterID() != c->CharacterID() && + !( + c->GetGroup() && + !invitedBot->HasGroup() && + invitedBot->GetOwner()->HasGroup() && + c->GetGroup() == invitedBot->GetOwner()->GetGroup() + ) + ) { + c->Message(Chat::Red, "%s's owner needs to be in your group to be able to invite them.", invitedBot->GetCleanName()); + + return; + } + if (!invitedBot->HasGroup() && !invitedBot->HasRaid()) { if (!c->IsGrouped()) { auto g = new Group(c); @@ -6617,7 +7211,7 @@ void Bot::ProcessBotGroupDisband(Client* c, const std::string& botName) { // Processes a raid disband request from a Client for a Bot. void Bot::RemoveBotFromRaid(Bot* bot) { - Raid* bot_raid = entity_list.GetRaidByBotName(bot->GetName()); + Raid* bot_raid = entity_list.GetRaidByBot(bot); if (bot_raid) { uint32 gid = bot_raid->GetGroup(bot->GetName()); bot_raid->SendRaidGroupRemove(bot->GetName(), gid); @@ -6627,18 +7221,22 @@ void Bot::RemoveBotFromRaid(Bot* bot) { bot_raid->DisbandRaid(); } } + + bot->SetStoredRaid(nullptr); + bot->p_raid_instance = nullptr; + bot->SetVerifiedRaid(false); } // Handles all client zone change event void Bot::ProcessClientZoneChange(Client* botOwner) { if (botOwner) { - std::list BotList = entity_list.GetBotsByBotOwnerCharacterID(botOwner->CharacterID()); + std::vector BotList = entity_list.GetBotsByBotOwnerCharacterID(botOwner->CharacterID()); - for (std::list::iterator itr = BotList.begin(); itr != BotList.end(); ++itr) { + for (std::vector::iterator itr = BotList.begin(); itr != BotList.end(); ++itr) { Bot* tempBot = *itr; if (tempBot) { - Raid* raid = entity_list.GetRaidByBotName(tempBot->GetName()); + Raid* raid = entity_list.GetRaidByBot(tempBot); if (raid) { tempBot->Zone(); } @@ -6789,8 +7387,9 @@ void Bot::CalcBotStats(bool showtext) { } bool Bot::CheckLoreConflict(const EQ::ItemData* item) { - if (!item || !(item->LoreFlag)) + if (!item || !(item->LoreFlag) || (item->LoreGroup == 0)) { return false; + } if (item->LoreGroup == -1) // Standard lore items; look everywhere except the shared bank, return the result return (m_inv.HasItem(item->ID, 0, invWhereWorn) != INVALID_INDEX); @@ -6799,354 +7398,59 @@ bool Bot::CheckLoreConflict(const EQ::ItemData* item) { return (m_inv.HasItemByLoreGroup(item->LoreGroup, invWhereWorn) != INVALID_INDEX); } -bool EntityList::Bot_AICheckCloseBeneficialSpells(Bot* caster, uint8 iChance, float iRange, uint32 iSpellTypes) { - - if ((iSpellTypes & SPELL_TYPES_DETRIMENTAL) != 0) { - LogError("[EntityList::Bot_AICheckCloseBeneficialSpells] detrimental spells requested"); - return false; - } - - if (!caster || !caster->AI_HasSpells()) { - return false; - } - - if (iChance < 100) { - uint8 tmp = zone->random.Int(1, 100); - if (tmp > iChance) - return false; - } - - uint8 botCasterClass = caster->GetClass(); - - if (iSpellTypes == SpellType_Heal) { - if (botCasterClass == Class::Cleric || botCasterClass == Class::Druid || botCasterClass == Class::Shaman) { - if (caster->IsRaidGrouped()) { - Raid* raid = entity_list.GetRaidByBotName(caster->GetName()); - uint32 gid = raid->GetGroup(caster->GetName()); - if (gid < MAX_RAID_GROUPS) { - std::vector raid_group_members = raid->GetRaidGroupMembers(gid); - for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { - if (iter->member && !iter->member->qglobal) { - if (iter->member->IsClient() && iter->member->GetHPRatio() < 90) { - if (caster->AICastSpell(iter->member, 100, SpellType_Heal)) - return true; - } - else if ((iter->member->GetClass() == Class::Warrior || iter->member->GetClass() == Class::Paladin || iter->member->GetClass() == Class::ShadowKnight) && iter->member->GetHPRatio() < 95) { - if (caster->AICastSpell(iter->member, 100, SpellType_Heal)) - return true; - } - else if (iter->member->GetClass() == Class::Enchanter && iter->member->GetHPRatio() < 80) { - if (caster->AICastSpell(iter->member, 100, SpellType_Heal)) - return true; - } - else if (iter->member->GetHPRatio() < 70) { - if (caster->AICastSpell(iter->member, 100, SpellType_Heal)) - return true; - } - - } - - if (iter->member && !iter->member->qglobal && iter->member->HasPet() && iter->member->GetPet()->GetHPRatio() < 50) { - if (iter->member->GetPet()->GetOwner() != caster && caster->IsEngaged() && iter->member->IsCasting() && iter->member->GetClass() != Class::Enchanter) - continue; - - if (caster->AICastSpell(iter->member->GetPet(), 100, SpellType_Heal)) - return true; - } - } - } - } - - else if (caster->HasGroup()) { - Group *g = caster->GetGroup(); - if (g) { - for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if (g->members[i] && !g->members[i]->qglobal) { - if (g->members[i]->IsClient() && g->members[i]->GetHPRatio() < 90) { - if (caster->AICastSpell(g->members[i], 100, SpellType_Heal)) - return true; - } else if ((g->members[i]->GetClass() == Class::Warrior || g->members[i]->GetClass() == Class::Paladin || g->members[i]->GetClass() == Class::ShadowKnight) && g->members[i]->GetHPRatio() < 95) { - if (caster->AICastSpell(g->members[i], 100, SpellType_Heal)) - return true; - } else if (g->members[i]->GetClass() == Class::Enchanter && g->members[i]->GetHPRatio() < 80) { - if (caster->AICastSpell(g->members[i], 100, SpellType_Heal)) - return true; - } else if (g->members[i]->GetHPRatio() < 70) { - if (caster->AICastSpell(g->members[i], 100, SpellType_Heal)) - return true; - } - } - - if (g->members[i] && !g->members[i]->qglobal && g->members[i]->HasPet() && g->members[i]->GetPet()->GetHPRatio() < 50) { - if (g->members[i]->GetPet()->GetOwner() != caster && caster->IsEngaged() && g->members[i]->IsCasting() && g->members[i]->GetClass() != Class::Enchanter ) - continue; - - if (caster->AICastSpell(g->members[i]->GetPet(), 100, SpellType_Heal)) - return true; - } - } - } - } - } - - if ((botCasterClass == Class::Paladin || botCasterClass == Class::Beastlord || botCasterClass == Class::Ranger) && (caster->HasGroup() || caster->IsRaidGrouped())) { - float hpRatioToHeal = 25.0f; - switch(caster->GetBotStance()) { - case Stance::Reactive: - case Stance::Balanced: - hpRatioToHeal = 50.0f; - break; - case Stance::Burn: - case Stance::AEBurn: - hpRatioToHeal = 20.0f; - break; - case Stance::Aggressive: - case Stance::Efficient: - default: - hpRatioToHeal = 25.0f; - break; - } - if (caster->IsRaidGrouped()) { - if (auto raid = entity_list.GetRaidByBotName(caster->GetName())) { - uint32 gid = raid->GetGroup(caster->GetName()); - if (gid < MAX_RAID_GROUPS) { - std::vector raid_group_members = raid->GetRaidGroupMembers(gid); - for (std::vector::iterator iter = raid_group_members.begin(); - iter != raid_group_members.end(); ++iter) { - if (iter->member && !iter->member->qglobal) { - if (iter->member->IsClient() && iter->member->GetHPRatio() < hpRatioToHeal) { - if (caster->AICastSpell(iter->member, 100, SpellType_Heal)) - return true; - } else if ( - (iter->member->GetClass() == Class::Warrior || iter->member->GetClass() == Class::Paladin || - iter->member->GetClass() == Class::ShadowKnight) && - iter->member->GetHPRatio() < hpRatioToHeal) { - if (caster->AICastSpell(iter->member, 100, SpellType_Heal)) - return true; - } else if (iter->member->GetClass() == Class::Enchanter && - iter->member->GetHPRatio() < hpRatioToHeal) { - if (caster->AICastSpell(iter->member, 100, SpellType_Heal)) - return true; - } else if (iter->member->GetHPRatio() < hpRatioToHeal / 2) { - if (caster->AICastSpell(iter->member, 100, SpellType_Heal)) - return true; - } - } - - if (iter->member && !iter->member->qglobal && iter->member->HasPet() && - iter->member->GetPet()->GetHPRatio() < 25) { - if (iter->member->GetPet()->GetOwner() != caster && caster->IsEngaged() && - iter->member->IsCasting() && iter->member->GetClass() != Class::Enchanter) - continue; - - if (caster->AICastSpell(iter->member->GetPet(), 100, SpellType_Heal)) - return true; - } - } - } - } - } - else if (caster->HasGroup()) { - if (auto g = caster->GetGroup()) { - for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if (g->members[i] && !g->members[i]->qglobal) { - if (g->members[i]->IsClient() && g->members[i]->GetHPRatio() < hpRatioToHeal) { - if (caster->AICastSpell(g->members[i], 100, SpellType_Heal)) - return true; - } else if ((g->members[i]->GetClass() == Class::Warrior || g->members[i]->GetClass() == Class::Paladin || - g->members[i]->GetClass() == Class::ShadowKnight) && - g->members[i]->GetHPRatio() < hpRatioToHeal) { - if (caster->AICastSpell(g->members[i], 100, SpellType_Heal)) - return true; - } else if (g->members[i]->GetClass() == Class::Enchanter && - g->members[i]->GetHPRatio() < hpRatioToHeal) { - if (caster->AICastSpell(g->members[i], 100, SpellType_Heal)) - return true; - } else if (g->members[i]->GetHPRatio() < hpRatioToHeal / 2) { - if (caster->AICastSpell(g->members[i], 100, SpellType_Heal)) - return true; - } - } +bool Bot::AttemptCloseBeneficialSpells(uint16 spell_type) { + bool result = false; + Mob* tar = nullptr; - if (g->members[i] && !g->members[i]->qglobal && g->members[i]->HasPet() && - g->members[i]->GetPet()->GetHPRatio() < 25) { - if (g->members[i]->GetPet()->GetOwner() != caster && caster->IsEngaged() && - g->members[i]->IsCasting() && g->members[i]->GetClass() != Class::Enchanter) - continue; + for (Mob* m : GetSpellTargetList(RuleB(Bots, CrossRaidBuffingAndHealing))) { + tar = m; - if (caster->AICastSpell(g->members[i]->GetPet(), 100, SpellType_Heal)) - return true; - } - } - } - } + if (!tar) { + continue; } - } - if (iSpellTypes == SpellType_Buff) { - uint8 chanceToCast = caster->IsEngaged() ? caster->GetChanceToCastBySpellType(SpellType_Buff) : 100; - if (botCasterClass == Class::Bard) { - if (caster->AICastSpell(caster, chanceToCast, SpellType_Buff)) { - return true; - } else - return false; - } + if (RuleB(Bots, CrossRaidBuffingAndHealing) && IsGroupTargetOnlyBotSpellType(spell_type)) { + Raid* raid = GetStoredRaid(); - if (caster->IsRaidGrouped()) { - Raid* raid = entity_list.GetRaidByBotName(caster->GetName()); - uint32 g = raid->GetGroup(caster->GetName()); - if (g < MAX_RAID_GROUPS) { - std::vector raid_group_members = raid->GetRaidGroupMembers(g); - for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { - if (iter->member) { - if (caster->AICastSpell(iter->member, chanceToCast, SpellType_Buff) || caster->AICastSpell(iter->member->GetPet(), chanceToCast, SpellType_Buff)) - return true; - } - } - } - } - if (caster->HasGroup()) { - Group *g = caster->GetGroup(); - if (g) { - for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if (g->members[i]) { - if (caster->AICastSpell(g->members[i], chanceToCast, SpellType_Buff) || caster->AICastSpell(g->members[i]->GetPet(), chanceToCast, SpellType_Buff)) - return true; - } - } + if (raid && + (raid->GetGroup(GetName()) == raid->GetGroup(tar->GetName())) + ) { + continue; } } - } - - if (iSpellTypes == SpellType_Cure) { - if (caster->IsRaidGrouped()) { - Raid* raid = entity_list.GetRaidByBotName(caster->GetName()); - uint32 gid = raid->GetGroup(caster->GetName()); - if (gid < MAX_RAID_GROUPS) { - std::vector raid_group_members = raid->GetRaidGroupMembers(gid); - for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { - if (iter->member && caster->GetNeedsCured(iter->member)) { - if (caster->AICastSpell(iter->member, caster->GetChanceToCastBySpellType(SpellType_Cure), SpellType_Cure)) - return true; - else if (botCasterClass == Class::Bard) { - return false; - } - } - if (iter->member && iter->member->GetPet() && caster->GetNeedsCured(iter->member->GetPet())) { - if (caster->AICastSpell(iter->member->GetPet(), (int)caster->GetChanceToCastBySpellType(SpellType_Cure) / 4, SpellType_Cure)) - return true; - } - } - } - } - else if (caster->HasGroup()) { - Group *g = caster->GetGroup(); - if (g) { - for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if (g->members[i] && caster->GetNeedsCured(g->members[i])) { - if (caster->AICastSpell(g->members[i], caster->GetChanceToCastBySpellType(SpellType_Cure), SpellType_Cure)) - return true; - else if (botCasterClass == Class::Bard) - return false; - } + result = AttemptAICastSpell(spell_type, tar); - if (g->members[i] && g->members[i]->GetPet() && caster->GetNeedsCured(g->members[i]->GetPet())) { - if (caster->AICastSpell(g->members[i]->GetPet(), (int)caster->GetChanceToCastBySpellType(SpellType_Cure)/4, SpellType_Cure)) - return true; - } - } - } - } - } + if (!result) { + if (tar->HasPet()) { + Mob* pet = m->GetPet(); - if (iSpellTypes == SpellType_HateRedux) { - if (!caster->IsEngaged()) - return false; + if (!pet->IsFamiliar() || RuleB(Bots, AllowBuffingHealingFamiliars)) { + tar = pet; - if (caster->IsRaidGrouped()) { - Raid* raid = entity_list.GetRaidByBotName(caster->GetName()); - uint32 gid = raid->GetGroup(caster->GetName()); - if (gid < MAX_RAID_GROUPS) { - std::vector raid_group_members = raid->GetRaidGroupMembers(gid); - for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { - if (iter->member && caster->GetNeedsHateRedux(iter->member)) { - if (caster->AICastSpell(iter->member, caster->GetChanceToCastBySpellType(SpellType_HateRedux), SpellType_HateRedux)) - return true; - } - } - } - } - else if (caster->HasGroup()) { - Group *g = caster->GetGroup(); - if (g) { - for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if (g->members[i] && caster->GetNeedsHateRedux(g->members[i])) { - if (caster->AICastSpell(g->members[i], caster->GetChanceToCastBySpellType(SpellType_HateRedux), SpellType_HateRedux)) - return true; + if (!tar) { + continue; } - } - } - } - } - if (iSpellTypes == SpellType_PreCombatBuff) { - if (botCasterClass == Class::Bard || caster->IsEngaged()) - return false; - - //added raid check - if (caster->IsRaidGrouped()) { - Raid* raid = entity_list.GetRaidByBotName(caster->GetName()); - uint32 g = raid->GetGroup(caster->GetName()); - if (g < MAX_RAID_GROUPS) { - std::vector raid_group_members = raid->GetRaidGroupMembers(g); - for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { - if (iter->member && - (caster->AICastSpell(iter->member, iChance, SpellType_PreCombatBuff) || - caster->AICastSpell(iter->member->GetPet(), iChance, SpellType_PreCombatBuff)) + if (tar->IsOfClientBot() || + ( + tar->IsPet() && + tar->GetOwner() && + tar->GetOwner()->IsOfClientBot() + ) ) { - return true; + result = AttemptAICastSpell(spell_type, tar); } } } - } else if (caster->HasGroup()) { - const auto g = caster->GetGroup(); - if (g) { - for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if (g->members[i]) { - if (caster->AICastSpell(g->members[i], iChance, SpellType_PreCombatBuff) || caster->AICastSpell(g->members[i]->GetPet(), iChance, SpellType_PreCombatBuff)) - return true; - } - } - } - } - } - - if (iSpellTypes == SpellType_InCombatBuff) { - if (botCasterClass == Class::Bard) { - if (caster->AICastSpell(caster, iChance, SpellType_InCombatBuff)) { - return true; - } - else { - return false; - } } - - if (caster->HasGroup()) { - Group* g = caster->GetGroup(); - if (g) { - for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { - if (g->members[i]) { - if (caster->AICastSpell(g->members[i], iChance, SpellType_InCombatBuff) || caster->AICastSpell(g->members[i]->GetPet(), iChance, SpellType_InCombatBuff)) { - return true; - } - } - } - } + + if (result) { + break; } } - return false; + return result; } Mob* EntityList::GetMobByBotID(uint32 botID) { @@ -7166,65 +7470,54 @@ Mob* EntityList::GetMobByBotID(uint32 botID) { } Bot* EntityList::GetBotByBotID(uint32 botID) { - Bot* Result = nullptr; - if (botID > 0) { - for (std::list::iterator botListItr = bot_list.begin(); botListItr != bot_list.end(); ++botListItr) { - Bot* tempBot = *botListItr; - if (tempBot && tempBot->GetBotID() == botID) { - Result = tempBot; - break; - } - } + auto it = bot_list.begin(); + while (it != bot_list.end()) { + if (it->second->GetBotID() == botID) + return it->second; + ++it; } - return Result; + return nullptr; } -Bot* EntityList::GetBotByBotName(std::string_view botName) { - Bot* Result = nullptr; - if (!botName.empty()) { - for (const auto b : bot_list) { - if (b && std::string_view(b->GetName()) == botName) { - Result = b; - break; - } +Bot* EntityList::GetBotByBotName(std::string botName) { + for (const auto& e : bot_list) { + if (e.second && Strings::EqualFold(e.second->GetName(), botName)) { + return e.second; } } - return Result; + + return nullptr; } Client* EntityList::GetBotOwnerByBotEntityID(uint32 entity_id) { - Client* c = nullptr; - if (entity_id) { - for (const auto& b : bot_list) { - if (b && b->GetID() == entity_id) { - c = b->GetBotOwner()->CastToClient(); - break; - } + auto it = bot_list.begin(); + while (it != bot_list.end()) { + if (it->second->GetID() == entity_id) + return it->second->GetBotOwner()->CastToClient(); + ++it; } } - - return c; + return nullptr; } Client* EntityList::GetBotOwnerByBotID(const uint32 bot_id) { - Client* c = nullptr; - if (bot_id) { - const auto owner_id = database.botdb.GetOwnerID(bot_id); - if (owner_id) { - c = GetClientByCharID(owner_id); + auto it = bot_list.begin(); + while (it != bot_list.end()) { + if (it->second->GetBotID() == bot_id) + return it->second->GetBotOwner()->CastToClient(); + ++it; } } - - return c; + return nullptr; } void EntityList::AddBot(Bot *new_bot, bool send_spawn_packet, bool dont_queue) { if (new_bot) { new_bot->SetID(GetFreeID()); - bot_list.push_back(new_bot); - mob_list.insert(std::pair(new_bot->GetID(), new_bot)); + bot_list.emplace(std::pair(new_bot->GetID(), new_bot)); + mob_list.emplace(std::pair(new_bot->GetID(), new_bot)); if (parse->BotHasQuestSub(EVENT_SPAWN)) { parse->EventBot(EVENT_SPAWN, new_bot, nullptr, "", 0); @@ -7253,31 +7546,32 @@ void EntityList::AddBot(Bot *new_bot, bool send_spawn_packet, bool dont_queue) { } } -std::list EntityList::GetBotsByBotOwnerCharacterID(uint32 botOwnerCharacterID) { - std::list Result; - if (botOwnerCharacterID > 0) { - for (std::list::iterator botListItr = bot_list.begin(); botListItr != bot_list.end(); ++botListItr) { - Bot* tempBot = *botListItr; - if (tempBot && tempBot->GetBotOwnerCharacterID() == botOwnerCharacterID) - Result.push_back(tempBot); +std::vector EntityList::GetBotsByBotOwnerCharacterID(uint32 bot_owner_character_id) { + std::vector client_bot_list; + + if (bot_owner_character_id <= 0) { + return client_bot_list; + } + + auto it = bot_list.begin(); + + while (it != bot_list.end()) { + if (it->second->GetOwner() && it->second->GetBotOwnerCharacterID() == bot_owner_character_id) { + client_bot_list.push_back(it->second); } + ++it; } - return Result; + + return client_bot_list; } bool EntityList::RemoveBot(uint16 entityID) { - bool Result = false; - if (entityID > 0) { - for (std::list::iterator botListItr = bot_list.begin(); botListItr != bot_list.end(); ++botListItr) { - Bot* tempBot = *botListItr; - if (tempBot && tempBot->GetID() == entityID) { - bot_list.erase(botListItr); - Result = true; - break; - } - } + auto it = bot_list.find(entityID); + if (it != bot_list.end()) { + bot_list.erase(it); // Already deleted + return true; } - return Result; + return false; } void EntityList::ShowSpawnWindow(Client* client, int Distance, bool NamedOnly) { @@ -7390,29 +7684,21 @@ void EntityList::ShowSpawnWindow(Client* client, int Distance, bool NamedOnly) { return; } -uint8 Bot::GetNumberNeedingHealedInGroup(uint8 hpr, bool includePets, Raid* raid) { - - uint8 need_healed = 0; - if (HasGroup()) { - - auto group_members = GetGroup(); - if (group_members) { - - for (auto member : group_members->members) { - if (member && !member->qglobal) { +uint8 Bot::GetNumberNeedingHealedInGroup(Mob* tar, uint16 spell_type, uint16 spell_id, float range) { + if (!tar->IsBot()) { + return 0; + } - if (member->GetHPRatio() <= hpr) { - need_healed++; - } + uint8 count = 0; + auto target_list = tar->IsClient() ? tar->CastToBot()->GatherSpellTargets() : tar->CastToBot()->GetSpellTargetList(); - if (includePets && member->GetPet() && member->GetPet()->GetHPRatio() <= hpr) { - need_healed++; - } - } - } + for (Mob* m : target_list) { + if (tar->CalculateDistance(m) < range && CastChecks(spell_id, m, spell_type, true, IsGroupBotSpellType(spell_type))) { + ++count; } } - return GetNumberNeedingHealedInRaidGroup(need_healed, hpr, includePets, raid); + + return count; } int Bot::GetRawACNoShield(int &shield_ac) { @@ -7534,78 +7820,97 @@ int Bot::GroupLeadershipAAOffenseEnhancement() { } bool Bot::GetNeedsCured(Mob *tar) { - bool needCured = false; + bool need_cured = false; + if (tar) { if (tar->FindType(SE_PoisonCounter) || tar->FindType(SE_DiseaseCounter) || tar->FindType(SE_CurseCounter) || tar->FindType(SE_CorruptionCounter)) { uint32 buff_count = tar->GetMaxTotalSlots(); - int buffsWithCounters = 0; - needCured = true; + for (unsigned int j = 0; j < buff_count; j++) { if (IsValidSpell(tar->GetBuffs()[j].spellid)) { if (CalculateCounters(tar->GetBuffs()[j].spellid) > 0) { - buffsWithCounters++; - if (buffsWithCounters == 1 && (tar->GetBuffs()[j].ticsremaining < 2 || (int32)((tar->GetBuffs()[j].ticsremaining * 6) / tar->GetBuffs()[j].counters) < 2)) { - needCured = false; - break; + if (tar->GetBuffs()[j].ticsremaining < 1) { + continue; } + + need_cured = true; } } } } } - return needCured; + + return need_cured; } bool Bot::GetNeedsHateRedux(Mob *tar) { - // This really should be a scalar function based in class Mob that returns 'this' state..but, is inline with current Bot coding... - // TODO: Good starting point..but, can be refined.. - // TODO: Still awaiting bot spell rework.. - if (!tar || !tar->IsEngaged() || !tar->HasTargetReflection() || !tar->GetTarget()->IsNPC()) + if (!tar || !tar->IsEngaged() || !tar->HasTargetReflection() || !tar->GetTarget()->IsNPC() || (tar->IsBot() && tar->CastToBot()->IsTaunting())) { return false; + } if (tar->IsBot()) { - switch (tar->GetClass()) { - case Class::Rogue: - if (tar->CanFacestab() || tar->CastToBot()->m_evade_timer.Check(false)) - return false; - case Class::Cleric: - case Class::Druid: - case Class::Shaman: - case Class::Necromancer: - case Class::Wizard: - case Class::Magician: - case Class::Enchanter: + if (tar->GetHPRatio() > GetUltimateSpellTypeMinThreshold(BotSpellTypes::HateRedux, tar) && tar->GetHPRatio() < + GetUltimateSpellTypeMaxThreshold( + BotSpellTypes::HateRedux, + tar + )) { return true; - default: - return false; } } return false; } -bool Bot::HasOrMayGetAggro() { - bool mayGetAggro = false; - if (GetTarget() && GetTarget()->GetHateTop()) { - Mob *topHate = GetTarget()->GetHateTop(); - if (topHate == this) - mayGetAggro = true; - else { - uint32 myHateAmt = GetTarget()->GetHateAmount(this); - uint32 topHateAmt = GetTarget()->GetHateAmount(topHate); +bool Bot::HasOrMayGetAggro(bool sit_aggro, uint32 spell_id) { + if ( + !GetTarget() || + !GetTarget()->GetHateTop() + ) { + return false; + } - if (myHateAmt > 0 && topHateAmt > 0 && (uint8)((myHateAmt / topHateAmt) * 100) > 90) - mayGetAggro = true; - } - } - return mayGetAggro; + Mob* top_hate = GetTarget()->GetHateTop(); + + if (top_hate == this) { + return true; + } + + uint32 my_hate = GetTarget()->GetHateAmount(this); + uint32 top_hate_amt = GetTarget()->GetHateAmount(top_hate); + + if (sit_aggro && !spell_id) { + my_hate *= (1 + (RuleI(Aggro, SittingAggroMod) / 100)); + } + + if (spell_id && IsValidSpell(spell_id)) { + my_hate += CheckAggroAmount(spell_id, GetTarget()); + } + + if ( + top_hate_amt < 1 || + ( + my_hate > 0 && + (my_hate * 100 / top_hate_amt) > RuleI(Bots, HasOrMayGetAggroThreshold) + ) + ) { + return true; + } + + return false; } void Bot::SetDefaultBotStance() { _botStance = GetClass() == Class::Warrior ? Stance::Aggressive : Stance::Balanced; } -void Bot::BotGroupSay(Mob* speaker, const char* msg, ...) { +void Bot::RaidGroupSay(Mob* speaker, const char* msg, ...) { + if ( + speaker->CastToBot()->GetTempSpellType() != UINT16_MAX && + !speaker->CastToBot()->GetSpellTypeAnnounceCast(speaker->CastToBot()->GetTempSpellType()) + ) { + return; + } + char buf[1000]; va_list ap; va_start(ap, msg); @@ -7613,7 +7918,8 @@ void Bot::BotGroupSay(Mob* speaker, const char* msg, ...) { va_end(ap); if (speaker->IsRaidGrouped()) { - Raid* r = entity_list.GetRaidByBotName(speaker->GetName()); + Raid* r = speaker->CastToBot()->GetStoredRaid(); + if (r) { for (const auto& m : r->members) { if (m.member && !m.is_bot) { @@ -7631,6 +7937,7 @@ void Bot::BotGroupSay(Mob* speaker, const char* msg, ...) { } else if (speaker->HasGroup()) { Group* g = speaker->GetGroup(); + if (g) { for (auto& m : g->members) { if (m && !m->IsBot()) { @@ -7647,7 +7954,6 @@ void Bot::BotGroupSay(Mob* speaker, const char* msg, ...) { } } else { - //speaker->Say("%s", buf); speaker->GetOwner()->FilteredMessageString( speaker, Chat::PetResponse, @@ -7661,7 +7967,7 @@ void Bot::BotGroupSay(Mob* speaker, const char* msg, ...) { bool Bot::UseDiscipline(uint32 spell_id, uint32 target) { if (!IsValidSpell(spell_id)) { - BotGroupSay(this, "Not a valid spell."); + RaidGroupSay(this, "Not a valid spell."); return false; } @@ -7957,51 +8263,24 @@ bool Bot::CheckDataBucket(std::string bucket_name, const std::string& bucket_val int Bot::GetExpansionBitmask() { - if (m_expansion_bitmask >= 0) { - return m_expansion_bitmask; + if (_expansionBitmask >= 0) { + return _expansionBitmask; } return RuleI(Bots, BotExpansionSettings); } -void Bot::SetExpansionBitmask(int expansion_bitmask, bool save) +void Bot::SetExpansionBitmask(int expansionBitmask) { - m_expansion_bitmask = expansion_bitmask; - - if (save) { - if (!database.botdb.SaveExpansionBitmask(GetBotID(), expansion_bitmask)) { - if (GetBotOwner() && GetBotOwner()->IsClient()) { - GetBotOwner()->CastToClient()->Message( - Chat::White, - fmt::format( - "Failed to save expansion bitmask for {}.", - GetCleanName() - ).c_str() - ); - } - } - } + _expansionBitmask = expansionBitmask; LoadAAs(); } -void Bot::SetBotEnforceSpellSetting(bool enforce_spell_settings, bool save) +void Bot::SetBotEnforceSpellSetting(bool enforceSpellSettings) { - m_enforce_spell_settings = enforce_spell_settings; + _enforceSpellSettings = enforceSpellSettings; - if (save) { - if (!database.botdb.SaveEnforceSpellSetting(GetBotID(), enforce_spell_settings)) { - if (GetBotOwner() && GetBotOwner()->IsClient()) { - GetBotOwner()->CastToClient()->Message( - Chat::White, - fmt::format( - "Failed to save enforce spell settings for {}.", - GetCleanName() - ).c_str() - ); - } - } - } LoadBotSpellSettings(); AI_AddBotSpells(GetBotSpellID()); } @@ -8251,24 +8530,6 @@ std::string Bot::GetHPString(int8 min_hp, int8 max_hp) return hp_string; } -void Bot::SetBotArcherySetting(bool bot_archer_setting, bool save) -{ - m_bot_archery_setting = bot_archer_setting; - if (save) { - if (!database.botdb.SaveBotArcherSetting(GetBotID(), bot_archer_setting)) { - if (GetBotOwner() && GetBotOwner()->IsClient()) { - GetBotOwner()->CastToClient()->Message( - Chat::White, - fmt::format( - "Failed to save archery settings for {}.", - GetCleanName() - ).c_str() - ); - } - } - } -} - std::vector Bot::GetApplySpellList( ApplySpellType apply_type, bool allow_pets, @@ -8441,59 +8702,45 @@ void Bot::SendSpellAnim(uint16 target_id, uint16 spell_id) entity_list.QueueCloseClients(this, &app, false, RuleI(Range, SpellParticles)); } -float Bot::GetBotCasterMaxRange(float melee_distance_max) {// Calculate caster distances - float caster_distance_max = 0.0f; - float caster_distance_min = 0.0f; - float caster_distance = 0.0f; - - caster_distance_max = GetBotCasterRange() * GetBotCasterRange(); - if (!GetBotCasterRange() && GetLevel() >= GetStopMeleeLevel() && GetClass() >= Class::Warrior && GetClass() <= Class::Berserker) { - caster_distance_max = MAX_CASTER_DISTANCE[GetClass() - 1]; - } - if (caster_distance_max) { - caster_distance_min = melee_distance_max; - if (caster_distance_max <= caster_distance_min) { - caster_distance_max = caster_distance_min * 1.25f; - } - } - return caster_distance_max; -} - - int32 Bot::CalcItemATKCap() { return RuleI(Character, ItemATKCap) + itembonuses.ItemATKCap + spellbonuses.ItemATKCap + aabonuses.ItemATKCap; } -bool Bot::CheckSpawnConditions(Client* c) { +bool Bot::CheckCampSpawnConditions(Client* c) { - if (c->GetFeigned()) { - c->Message(Chat::White, "You cannot spawn a bot-group while feigned."); - return false; - } + if (RuleB(Bots, PreventBotSpawnOnFD) && c->GetFeigned()) { + c->Message(Chat::White, "You cannot camp or spawn bots while feigned."); - Raid* raid = entity_list.GetRaidByClient(c); - if (raid && raid->IsEngaged()) { - c->Message(Chat::White, "You cannot spawn bots while your raid is engaged."); return false; } - auto* owner_group = c->GetGroup(); - if (owner_group) { - std::list member_list; - owner_group->GetClientList(member_list); - member_list.remove(nullptr); + if (RuleB(Bots, PreventBotSpawnOnEngaged)) { + Raid* raid = entity_list.GetRaidByClient(c); + if (raid && raid->IsEngaged()) { + c->Message(Chat::White, "You cannot camp or spawn bots while your raid is engaged."); - for (auto member_iter : member_list) { - if (member_iter->IsEngaged() || member_iter->GetAggroCount() > 0) { - c->Message(Chat::White, "You cannot spawn bots while your group is engaged,"); - return false; + return false; + } + + auto* owner_group = c->GetGroup(); + if (owner_group) { + std::list member_list; + owner_group->GetClientList(member_list); + member_list.remove(nullptr); + + for (auto member_iter : member_list) { + if (member_iter->IsEngaged() || member_iter->GetAggroCount() > 0) { + c->Message(Chat::White, "You cannot camp or spawn bots while your group is engaged,"); + return false; + } } } - } else { - if (c->GetAggroCount() > 0) { - c->Message(Chat::White, "You cannot spawn bots while you are engaged,"); - return false; + else { + if (c->GetAggroCount() > 0) { + c->Message(Chat::White, "You cannot camp or spawn bots while you are engaged,"); + return false; + } } } @@ -8564,14 +8811,14 @@ void Bot::SetSpellRecastTimer(uint16 spell_id, int32 recast_delay) { } if (CheckSpellRecastTimer(spell_id)) { - BotTimer_Struct t; + BotTimer t; - t.timer_id = spells[ spell_id ].timer_id; + t.timer_id = spells[spell_id].timer_id; t.timer_value = (Timer::GetCurrentTime() + recast_delay); t.recast_time = recast_delay; t.is_spell = true; t.is_disc = false; - t.spell_id = spells[ spell_id ].id; + t.spell_id = spells[spell_id].id; t.is_item = false; t.item_id = 0; @@ -8584,7 +8831,7 @@ void Bot::SetSpellRecastTimer(uint16 spell_id, int32 recast_delay) { ( ( spells[spell_id].timer_id != 0 && - spells[spell_id].timer_id == bot_timers[ i ].timer_id + spells[spell_id].timer_id == bot_timers[i].timer_id ) || bot_timers[i].spell_id == spell_id ) @@ -8670,14 +8917,14 @@ void Bot::SetDisciplineReuseTimer(uint16 spell_id, int32 reuse_timer) } if (CheckDisciplineReuseTimer(spell_id)) { - BotTimer_Struct t; + BotTimer t; - t.timer_id = spells[ spell_id ].timer_id; + t.timer_id = spells[spell_id].timer_id; t.timer_value = (Timer::GetCurrentTime() + reuse_timer); t.recast_time = reuse_timer; t.is_spell = false; t.is_disc = true; - t.spell_id = spells[ spell_id ].id; + t.spell_id = spells[spell_id].id; t.is_item = false; t.item_id = 0; @@ -8767,7 +9014,7 @@ void Bot::SetItemReuseTimer(uint32 item_id, uint32 reuse_timer) } if (CheckItemReuseTimer(item_id)) { - BotTimer_Struct t; + BotTimer t; t.timer_id = (item->RecastType == NegativeItemReuse ? item->ID : item->RecastType); t.timer_value = ( @@ -9138,13 +9385,17 @@ void Bot::DoItemClick(const EQ::ItemData *item, uint16 slot_id) bool is_casting_bard_song = false; Mob* tar = (GetOwner()->GetTarget() ? GetOwner()->GetTarget() : this); + if (!DoLosChecks(tar)) { + return; + } + if (IsCasting()) { InterruptSpell(); } SetIsUsingItemClick(true); - BotGroupSay( + RaidGroupSay( this, fmt::format( "Attempting to cast [{}] on {}.", @@ -9177,3 +9428,3889 @@ void Bot::DoItemClick(const EQ::ItemData *item, uint16 slot_id) } uint8 Bot::spell_casting_chances[SPELL_TYPE_COUNT][Class::PLAYER_CLASS_COUNT][Stance::AEBurn][cntHSND] = { 0 }; + +bool Bot::PrecastChecks(Mob* tar, uint16 spell_type) { + if (!tar) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast due to PrecastChecks !tar.'", GetCleanName()); + return false; + } + + LogBotSpellChecksDetail("{} says, 'Running [{}] PreChecks on [{}].'", GetCleanName(), GetSpellTypeNameByID(spell_type), tar->GetCleanName()); + + if (GetUltimateSpellTypeHold(spell_type, tar)) { + if (!IsCommandedSpell()) { + SetSpellTypeAITimer(spell_type, RuleI(Bots, AICastSpellTypeHeldDelay)); + } + LogBotSpellChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetUltimateSpellTypeHold.'", GetCleanName(), GetSpellTypeNameByID(spell_type), tar->GetCleanName()); + return false; + } + + if ( + IsPullingSpell() && + IsPullingBotSpellType(spell_type) + ) { //Skip remaining checks for commanded + return true; + } + + if ( + GetManaRatio() < GetSpellTypeMinManaLimit(spell_type) || + GetManaRatio() > GetSpellTypeMaxManaLimit(spell_type) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetSpellTypeMinManaLimit or GetSpellTypeMaxManaLimit.'", GetCleanName(), GetSpellTypeNameByID(spell_type), tar->GetCleanName()); + return false; + } + + if ( + GetHPRatio() < GetSpellTypeMinHPLimit(spell_type) || + GetHPRatio() > GetSpellTypeMaxHPLimit(spell_type) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetSpellTypeMinHPLimit or GetSpellTypeMaxHPLimit.'", GetCleanName(), GetSpellTypeNameByID(spell_type), tar->GetCleanName()); + return false; + } + + if (!GetUltimateSpellTypeDelayCheck(spell_type, tar)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetUltimateSpellTypeDelayCheck.'", GetCleanName(), GetSpellTypeNameByID(spell_type), tar->GetCleanName()); + return false; + } + + switch (spell_type) { //This will skip Threshold Checks during Precast for specific SpellTypes that are checked when acquiring new targets + case BotSpellTypes::Mez: + case BotSpellTypes::AEMez: + return true; + default: + if ( + GetHPRatioForSpellType(spell_type, tar) < GetUltimateSpellTypeMinThreshold(spell_type, tar) || + GetHPRatioForSpellType(spell_type, tar) > GetUltimateSpellTypeMaxThreshold(spell_type, tar) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetUltimateSpellTypeMinThreshold or GetUltimateSpellTypeMaxThreshold.'", GetCleanName(), GetSpellTypeNameByID(spell_type), tar->GetCleanName()); + return false; + } + } + + return true; +} + +bool Bot::CastChecks(uint16 spell_id, Mob* tar, uint16 spell_type, bool prechecks, bool ae_check) { + if (prechecks) { + if (!tar) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast due to CastChecks !tar.'", GetCleanName()); + return false; + } + + if (spells[spell_id].target_type == ST_Self && tar != this) { + if ( + !IsEffectInSpell(spell_id, SE_SummonCorpse) || + ( + IsEffectInSpell(spell_id, SE_SummonCorpse) && + !RuleB(Bots, AllowCommandedSummonCorpse) + ) + ) { + tar = this; + } + } + + if (!PrecastChecks(tar, spell_type)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast due to !PrecastChecks.'", GetCleanName()); + return false; + } + } + + LogBotSpellChecksDetail("{} says, 'Running [{}] CastChecks on [{}].'", GetCleanName(), GetSpellTypeNameByID(spell_type), (tar ? tar->GetCleanName() : "nobody")); + + if (!IsValidSpell(spell_id)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast due to !IsValidSpell.'", GetCleanName()); + return false; + } + + if ( + IsFeared() || + IsSilenced() || + IsAmnesiad() + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to Incapacitated.'", GetCleanName(), GetSpellName(spell_id), (tar ? tar->GetCleanName() : "nobody")); + return false; + } + + if ( + ( + IsStunned() || + IsMezzed() || + DivineAura() + ) && + !IsCastNotStandingSpell(spell_id) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to !IsCastNotStandingSpell.'", GetCleanName(), GetSpellName(spell_id), (tar ? tar->GetCleanName() : "nobody")); + return false; + } + + if ( + IsDetrimentalSpell(spell_id) && + !zone->CanDoCombat() + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to !CanDoCombat.'", GetCleanName(), GetSpellName(spell_id), (tar ? tar->GetCleanName() : "nobody")); + return false; + } + + if (!CheckSpellRecastTimer(spell_id)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !CheckSpellRecastTimer.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if (!BotHasEnoughMana(spell_id)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !BotHasEnoughMana.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if (zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to IsSpellBlocked.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if ( + spells[spell_id].caster_requirement_id && + !PassCastRestriction(spells[spell_id].caster_requirement_id) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !PassCastRestriction.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if ( + !spells[spell_id].can_cast_in_combat && + spells[spell_id].can_cast_out_of_combat + ) { + if (IsBeneficialSpell(spell_id)) { + if (IsEngaged()) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !can_cast_in_combat.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + } + } + else if ( + spells[spell_id].can_cast_in_combat && + !spells[spell_id].can_cast_out_of_combat + ) { + if (IsBeneficialSpell(spell_id)) { + if (!IsEngaged()) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !can_cast_out_of_combat.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + } + } + + if (!IsDiscipline(spell_id)) { + int chance = GetFocusEffect(focusFcMute, spell_id); + + if ( + chance && + zone->random.Roll(chance) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to focusFcMute.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + } + + if ( + !zone->CanLevitate() && + IsEffectInSpell(spell_id, SE_Levitate) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !CanLevitate.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if ( + spells[spell_id].time_of_day == SpellTimeRestrictions::Day && + !zone->zone_time.IsDayTime() + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !IsDayTime.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if ( + spells[spell_id].time_of_day == SpellTimeRestrictions::Night && + !zone->zone_time.IsNightTime() + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !IsNightTime.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if ( + spells[spell_id].zone_type == 1 && + !zone->CanCastOutdoor() + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to !CanCastOutdoor.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if ( + BotSpellTypeRequiresTarget(spell_type) && + !tar + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast due to CastChecks !tar.'", GetCleanName()); + return false; + } + + if (spells[spell_id].target_type == ST_Self && tar != this) { + if (spell_type == BotSpellTypes::SummonCorpse && RuleB(Bots, AllowCommandedSummonCorpse)) { + // Don't cancel (Summon Corpse is allowed) + } + else { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to ST_Self.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + + return false; + } + } + + if ( + this == tar && + IsSacrificeSpell(spell_id) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} due to IsSacrificeSpell.'", GetCleanName(), GetSpellName(spell_id)); + return false; + } + + if (tar->GetSpecialAbility(SpecialAbility::MagicImmunity)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to MagicImmunity.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if ( + tar->GetSpecialAbility(SpecialAbility::CastingFromRangeImmunity) && + !CombatRange(tar) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to CastingFromRangeImmunity.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if (tar->CastToBot()->IsImmuneToBotSpell(spell_id, this)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsImmuneToBotSpell.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if (!tar->CheckSpellLevelRestriction(this, spell_id)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to CheckSpellLevelRestriction.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if ( + IsBeneficialSpell(spell_id) && + ( + ( + RuleB(Spells, EnableBlockedBuffs) && + tar->IsClient() + ) || + ( + RuleB(Bots, AllowBotBlockedBuffs) && + tar->IsBot() + ) + ) + ) { + if (tar->IsBlockedBuff(spell_id)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsBlockedPetBuff.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + } + + if ( + IsBeneficialSpell(spell_id) && + tar->IsPet() && + ( + ( + RuleB(Spells, EnableBlockedBuffs) && + tar->GetOwner() && + tar->GetOwner()->IsClient() + ) || + ( + RuleB(Bots, AllowBotBlockedBuffs) && + tar->GetOwner() && + tar->GetOwner()->IsBot() + ) + ) + ) { + if (tar->GetOwner()->IsBlockedPetBuff(spell_id)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsBlockedPetBuff.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + } + //LogBotSpellChecksDetail("{} says, 'Doing CanCastSpellType checks of {} on {}.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + if (!CanCastSpellType(spell_type, spell_id, tar)) { + return false; + } + + if (IsCommandedSpell()) { //stop checks here for commanded spells + return true; + } + + if (!IsValidTargetType(spell_id, GetSpellTargetType(spell_id), tar->GetBodyType())) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsValidTargetType.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if ( + ( + RequiresStackCheck(spell_type) || + ( + !RequiresStackCheck(spell_type) && + CalcBuffDuration(this, tar, spell_id) != 0 + ) + ) + && + tar->CanBuffStack(spell_id, GetLevel(), true) < 0 + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to !CanBuffStack.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if ( + IsBeneficialSpell(spell_id) && + tar->BuffCount() >= tar->GetCurrentBuffSlots() && + CalcBuffDuration(this, tar, spell_id) != 0 + ) { + return false; + } + + if ( + !ae_check && + !IsValidSpellRange(spell_id, tar) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsValidSpellRange.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if (spell_type == UINT16_MAX) { //AA/Forced cast checks, return here + return true; + } + + if ( + !IsTaunting() && + GetSpellTypeAggroCheck(spell_type) && + HasOrMayGetAggro(IsSitting(), spell_id) && + !tar->IsFleeing() + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to HasOrMayGetAggro.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if (!DoResistCheckBySpellType(tar, spell_id, spell_type)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to DoResistCheckBySpellType.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if ( + spells[spell_id].target_type != ST_Self && + IsBeneficialSpell(spell_id) && + !IsAnyHealSpell(spell_id) && + !IsCureSpell(spell_id) && + !IsHealOverTimeSpell(spell_id) && + !IsGroupHealOverTimeSpell(spell_id) && + IsTargetAlreadyReceivingSpell(tar, spell_id) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsTargetAlreadyReceivingSpell.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + return true; +} + +bool Bot::CanCastSpellType(uint16 spell_type, uint16 spell_id, Mob* tar) { + if (!spell_id || !tar) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to failsafe checks.'", GetCleanName(), (spell_id ? GetSpellName(spell_id) : (spell_type ? GetSpellTypeNameByID(spell_type) : "Unknown")), (tar ? tar->GetCleanName() : "Unknown")); + return false; + } + + uint8 bot_class = GetClass(); + + switch (spell_type) { + case BotSpellTypes::Buff: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::InCombatBuff: + case BotSpellTypes::DamageShields: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::Teleport: + case BotSpellTypes::Succor: + case BotSpellTypes::BindAffinity: + case BotSpellTypes::Identify: + case BotSpellTypes::Levitate: + case BotSpellTypes::Rune: + case BotSpellTypes::WaterBreathing: + case BotSpellTypes::Size: + case BotSpellTypes::Invisibility: + case BotSpellTypes::MovementSpeed: + case BotSpellTypes::SendHome: + if ( + tar == this && + spells[spell_id].target_type == ST_TargetsTarget + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to target_type checks. Using {}'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName(), GetSpellTargetType(spell_id)); + return false; + } + + if ( + ( + spell_type != BotSpellTypes::Teleport && + spell_type != BotSpellTypes::Succor + ) && + ( + IsEffectInSpell(spell_id, SE_Teleport) || + IsEffectInSpell(spell_id, SE_Succor) + ) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to Teleport.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if ( + tar->IsPet() && + !RuleB(Bots, CanCastIllusionsOnPets) && + IsEffectInSpell(spell_id, SE_Illusion) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to PetSE_Illusion.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if ( + spells[spell_id].target_type == ST_Pet && + ( + !tar->IsPet() || + ( + tar->GetOwner() != this && + !RuleB(Bots, CanCastPetOnlyOnOthersPets) + ) + ) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to PetOnly.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if ( + ( + IsGroupSpell(spell_id) && + tar->IsPet() + ) && + ( + !tar->GetOwner() || + ( + RuleB(Bots, RequirePetAffinity) && + !tar->GetOwner()->HasPetAffinity() + ) + ) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to PetGroupSpellTarget.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + if (!IsCommandedSpell()) { + switch (tar->GetArchetype()) { + case Archetype::Caster: + if ( + tar->IsBot() && + (tar->GetLevel() >= tar->CastToBot()->GetStopMeleeLevel()) && + ( + IsEffectInSpell(spell_id, SE_AttackSpeed) || + IsEffectInSpell(spell_id, SE_ReverseDS) + ) || + ( + SpellEffectsCount(spell_id) == 1 && + ( + IsEffectInSpell(spell_id, SE_ATK) || IsEffectInSpell(spell_id, SE_STR) + ) + ) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to Archetype::Caster.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + break; + case Archetype::Melee: + if ( + IsEffectInSpell(spell_id, SE_IncreaseSpellHaste) || + IsEffectInSpell(spell_id, SE_ManaPool) || + IsEffectInSpell(spell_id, SE_CastingLevel) || + IsEffectInSpell(spell_id, SE_ManaRegen_v2) || + IsEffectInSpell(spell_id, SE_CurrentMana) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to Archetype::Melee.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + break; + case Archetype::Hybrid: //Hybrids get all buffs + default: + break; + } + } + + // Differences for each type + if (spell_type != BotSpellTypes::InCombatBuff) { + if ( + IsEffectInSpell(spell_id, SE_AbsorbMagicAtt) || + IsEffectInSpell(spell_id, SE_Rune) + ) { + for (int i = 0; i < tar->GetMaxTotalSlots(); i++) { + uint32 buff_count = tar->GetMaxTotalSlots(); + + for (unsigned int j = 0; j < buff_count; j++) { + if (IsValidSpell(tar->GetBuffs()[j].spellid)) { + if (IsLichSpell(tar->GetBuffs()[j].spellid)) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsLichSpell.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + } + } + } + } + } + + break; + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + if (!IsCommandedSpell()) { + switch (tar->GetArchetype()) { + case Archetype::Caster: + if ( + tar->IsBot() && + (tar->GetLevel() >= tar->CastToBot()->GetStopMeleeLevel()) && + ( + IsEffectInSpell(spell_id, SE_AttackSpeed) || + IsEffectInSpell(spell_id, SE_ReverseDS) + ) || + ( + SpellEffectsCount(spell_id) == 1 && + ( + IsEffectInSpell(spell_id, SE_ATK) || + IsEffectInSpell(spell_id, SE_STR) + ) + ) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to Archetype::Caster.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + break; + case Archetype::Melee: + if ( + IsEffectInSpell(spell_id, SE_IncreaseSpellHaste) || + IsEffectInSpell(spell_id, SE_ManaPool) || + IsEffectInSpell(spell_id, SE_CastingLevel) || + IsEffectInSpell(spell_id, SE_ManaRegen_v2) || + IsEffectInSpell(spell_id, SE_CurrentMana) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to Archetype::Melee.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + break; + case Archetype::Hybrid: //Hybrids get all buffs + default: + break; + } + } + + break; + case BotSpellTypes::AELull: + case BotSpellTypes::Lull: + if ( + IsHarmonySpell(spell_id) && + !HarmonySpellLevelCheck(spell_id, tar) + ) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to HarmonySpellLevelCheck.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return false; + } + + break; + default: + break; + } + + //LogBotSpellChecksDetail("{} says, {} on {} passed CanCastSpellType.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); + return true; +} + +bool Bot::BotHasEnoughMana(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + int32 mana_cost = spells[spell_id].mana; + + if (GetMana() < mana_cost) { + return false; + } + + return true; +} + +bool Bot::IsTargetAlreadyReceivingSpell(Mob* tar, uint16 spell_id) { + if (!tar || !spell_id) { + return true; + } + + uint16 target_id = tar->GetID(); + + for (Mob* m : GetSpellTargetList(RuleB(Bots, CrossRaidBuffingAndHealing))) { + if ( + m->IsBot() && + m->IsCasting() && + m->CastToBot()->casting_spell_targetid && + m->CastingSpellID() == spell_id + ) { + if (m->CastToBot()->casting_spell_targetid == target_id) { + return true; + } + + if (!RuleB(Bots, CrossRaidBuffingAndHealing) && IsGroupSpell(spell_id)) { + for (Mob* x : GatherSpellTargets(false, tar)) { + if (x->GetID() == m->CastToBot()->casting_spell_targetid) { + return true; + } + } + } + } + } + + return false; +} + +bool Bot::DoResistCheck(Mob* tar, uint16 spell_id, int32 resist_limit) { + + if (!tar || spell_id == 0) { + return false; + } + + int32 resist_difficulty = -spells[spell_id].resist_difficulty; + int32 level_mod = (tar->GetLevel() - GetLevel()) * (tar->GetLevel() - GetLevel()) / 2; + + if (tar->GetLevel() - GetLevel() < 0) { + level_mod = -level_mod; + } + + int32 target_resist = 0; + + switch (GetSpellResistType(spell_id)) { + case RESIST_NONE: + return true; + case RESIST_MAGIC: + target_resist = tar->GetMR(); + break; + case RESIST_COLD: + target_resist = tar->GetCR(); + break; + case RESIST_FIRE: + target_resist = tar->GetFR(); + break; + case RESIST_POISON: + target_resist = tar->GetPR(); + break; + case RESIST_DISEASE: + target_resist = tar->GetDR(); + break; + case RESIST_CORRUPTION: + target_resist = tar->GetCorrup(); + break; + default: + return true; + } + + if ((target_resist + level_mod - resist_difficulty) > resist_limit) { + return false; + } + + return true; +} + +bool Bot::DoResistCheckBySpellType(Mob* tar, uint16 spell_id, uint16 spell_type) { + if (!tar || !IsValidSpell(spell_id)) { + return false; + } + + if (GetSpellTypeResistLimit(spell_type) == 0) { + return true; + } + + return DoResistCheck(tar, spell_id, GetSpellTypeResistLimit(spell_type)); +} + +bool Bot::IsValidTargetType(uint16 spell_id, int target_type, uint8 body_type) { + if (!spell_id) { + return false; + } + + switch (target_type) { + case ST_Undead: + if ( + body_type == BodyType::Undead || + body_type == BodyType::SummonedUndead || + body_type == BodyType::Vampire + ) { + return true; + } + + break; + case ST_Summoned: + if ( + body_type == BodyType::Summoned || + body_type == BodyType::Summoned2 || + body_type == BodyType::Summoned3 + ) { + return true; + } + + break; + case ST_Animal: + if (body_type == BodyType::Animal) { + return true; + } + + break; + case ST_Plant: + if (body_type == BodyType::Plant) { + return true; + } + + break; + case ST_Giant: + if ( + body_type == BodyType::Giant || + body_type == BodyType::RaidGiant + ) { + return true; + } + + break; + case ST_Dragon: + if ( + body_type == BodyType::Dragon || + body_type == BodyType::VeliousDragon + ) { + return true; + } + + break; + default: + return true; + } + + return false; +} + +bool Bot::IsMobEngagedByAnyone(Mob* tar) { + if (!tar) { + return false; + } + + for (Mob* m : GetSpellTargetList(true)) { + if (m->GetTarget() != tar) { + continue; + } + + bool bot_is_engaged = m->IsBot() && m->IsEngaged(); + bool bot_melee_or_casting = + bot_is_engaged && + ( + !m->CastToBot()->IsBotNonSpellFighter() || + ( + m->GetLevel() >= m->CastToBot()->GetStopMeleeLevel() && + !m->IsCasting() + ) + ); + + if (bot_melee_or_casting) { + return true; + } + + if ( + m->IsCasting() && + SpellBreaksMez(m->CastingSpellID()) + ) { + return true; + } + + if ( + m->IsClient() && + ( + m->CastToClient()->AutoAttackEnabled() || + m->CastToClient()->AutoFireEnabled() + ) + ) { + return true; + } + } + + return false; +} + +bool Bot::IsValidMezTarget(Mob* owner, Mob* npc, uint16 spell_id) { + if (npc->GetSpecialAbility(SpecialAbility::MesmerizeImmunity)) { + return false; + } + + if (!npc->CastToNPC()->IsOnHatelist(owner)) { + return false; + } + + if (npc->IsMezzed() || HasBotAttackFlag(npc)) { + return false; + } + + if (npc->HasOwner() && npc->GetOwner() && npc->GetOwner()->IsOfClientBotMerc()) { + return false; + } + + if (!IsValidTargetType(spell_id, GetSpellTargetType(spell_id), npc->GetBodyType())) { + return false; + } + + if (!IsAttackAllowed(GetTarget())) { + return false; + } + + if (!DoLosChecks(npc)) { + return false; + } + + if (IsMobEngagedByAnyone(npc)) { + return false; + } + + int buff_count = npc->GetMaxTotalSlots(); + auto npc_buffs = npc->GetBuffs(); + + for (int i = 0; i < buff_count; i++) { + if (IsDetrimentalSpell(npc_buffs[i].spellid) && IsEffectInSpell(npc_buffs[i].spellid, SE_CurrentHP)) { + return false; + } + } + + return true; +} + +void Bot::SetBotSetting(uint8 setting_type, uint16 bot_setting, int setting_value) { + switch (setting_type) { + case BotSettingCategories::BaseSetting: + SetBotBaseSetting(bot_setting, setting_value); + break; + case BotSettingCategories::SpellHold: + SetSpellTypeHold(bot_setting, setting_value); + break; + case BotSettingCategories::SpellDelay: + SetSpellTypeDelay(bot_setting, setting_value); + break; + case BotSettingCategories::SpellMinThreshold: + SetSpellTypeMinThreshold(bot_setting, setting_value); + break; + case BotSettingCategories::SpellMaxThreshold: + SetSpellTypeMaxThreshold(bot_setting, setting_value); + break; + case BotSettingCategories::SpellTypeResistLimit: + SetSpellTypeResistLimit(bot_setting, setting_value); + break; + case BotSettingCategories::SpellTypeAggroCheck: + SetSpellTypeAggroCheck(bot_setting, setting_value); + break; + case BotSettingCategories::SpellTypeMinManaPct: + SetSpellTypeMinManaLimit(bot_setting, setting_value); + break; + case BotSettingCategories::SpellTypeMaxManaPct: + SetSpellTypeMaxManaLimit(bot_setting, setting_value); + break; + case BotSettingCategories::SpellTypeMinHPPct: + SetSpellTypeMinHPLimit(bot_setting, setting_value); + break; + case BotSettingCategories::SpellTypeMaxHPPct: + SetSpellTypeMaxHPLimit(bot_setting, setting_value); + break; + case BotSettingCategories::SpellTypeIdlePriority: + SetSpellTypePriority(bot_setting, BotPriorityCategories::Idle, setting_value); + break; + case BotSettingCategories::SpellTypeEngagedPriority: + SetSpellTypePriority(bot_setting, BotPriorityCategories::Engaged, setting_value); + break; + case BotSettingCategories::SpellTypePursuePriority: + SetSpellTypePriority(bot_setting, BotPriorityCategories::Pursue, setting_value); + break; + case BotSettingCategories::SpellTypeAEOrGroupTargetCount: + SetSpellTypeAEOrGroupTargetCount(bot_setting, setting_value); + break; + case BotSettingCategories::SpellTypeAnnounceCast: + SetSpellTypeAnnounceCast(bot_setting, setting_value); + break; + } +} + +void Bot::SetBotBaseSetting(uint16 bot_setting, int setting_value) { + switch (bot_setting) { + case BotBaseSettings::ExpansionBitmask: + SetExpansionBitmask(setting_value); + break; + case BotBaseSettings::ShowHelm: + SetShowHelm(setting_value); + break; + case BotBaseSettings::FollowDistance: + SetFollowDistance(EQ::Clamp(static_cast(setting_value * setting_value), static_cast(1), static_cast((RuleI(Bots, MaxFollowDistance) * RuleI(Bots, MaxFollowDistance))))); + break; + case BotBaseSettings::StopMeleeLevel: + SetStopMeleeLevel(setting_value); + break; + case BotBaseSettings::EnforceSpellSettings: + SetBotEnforceSpellSetting(setting_value); + break; + case BotBaseSettings::RangedSetting: + SetBotRangedSetting(setting_value); + break; + case BotBaseSettings::PetSetTypeSetting: + SetPetChooserID(setting_value); + break; + case BotBaseSettings::BehindMob: + SetBehindMob(setting_value); + break; + case BotBaseSettings::DistanceRanged: + SetBotDistanceRanged(setting_value); + break; + case BotBaseSettings::IllusionBlock: + SetIllusionBlock(setting_value); + break; + case BotBaseSettings::MaxMeleeRange: + SetMaxMeleeRange(setting_value); + break; + case BotBaseSettings::MedInCombat: + SetMedInCombat(setting_value); + break; + case BotBaseSettings::SitHPPct: + SetSitHPPct(setting_value); + break; + case BotBaseSettings::SitManaPct: + SetSitManaPct(setting_value); + break; + default: + break; + } +} + +int Bot::GetBotBaseSetting(uint16 bot_setting) { + switch (bot_setting) { + case BotBaseSettings::ExpansionBitmask: + return GetExpansionBitmask(); + case BotBaseSettings::ShowHelm: + return GetShowHelm(); + case BotBaseSettings::FollowDistance: + return sqrt(GetFollowDistance()); + case BotBaseSettings::StopMeleeLevel: + return GetStopMeleeLevel(); + case BotBaseSettings::EnforceSpellSettings: + return GetBotEnforceSpellSetting(); + case BotBaseSettings::RangedSetting: + return IsBotRanged(); + case BotBaseSettings::PetSetTypeSetting: + return GetPetChooserID(); + case BotBaseSettings::BehindMob: + return GetBehindMob(); + case BotBaseSettings::DistanceRanged: + return GetBotDistanceRanged(); + case BotBaseSettings::IllusionBlock: + return GetIllusionBlock(); + case BotBaseSettings::MaxMeleeRange: + return GetMaxMeleeRange(); + case BotBaseSettings::MedInCombat: + return GetMedInCombat(); + case BotBaseSettings::SitHPPct: + return GetSitHPPct(); + case BotBaseSettings::SitManaPct: + return GetSitManaPct(); + default: + return true; + } + + return true; +} + +int Bot::GetDefaultBotBaseSetting(uint16 bot_setting, uint8 stance) { + switch (bot_setting) { + case BotBaseSettings::ExpansionBitmask: + return RuleI(Bots, BotExpansionSettings); + case BotBaseSettings::ShowHelm: + return true; + case BotBaseSettings::FollowDistance: + return RuleI(Bots, DefaultFollowDistance); + case BotBaseSettings::StopMeleeLevel: + if (IsCasterClass(GetClass())) { + return RuleI(Bots, CasterStopMeleeLevel); + } + else { + return 255; + } + case BotBaseSettings::PetSetTypeSetting: + return 0; + case BotBaseSettings::BehindMob: + if (GetClass() == Class::Rogue || (IsPureMeleeClass() && GetClass() != Class::Warrior)) { + return true; + } + else { + return false; + } + case BotBaseSettings::DistanceRanged: + switch (GetClass()) { + case Class::Warrior: + case Class::Monk: + case Class::Rogue: + case Class::Berserker: + return 0; + case Class::Bard: + return 30; + default: + return 90; + } + case BotBaseSettings::MedInCombat: + if (IsCasterClass(GetClass())) { + return true; + } + + return false; + case BotBaseSettings::SitHPPct: + case BotBaseSettings::SitManaPct: + return 80; + case BotBaseSettings::EnforceSpellSettings: + case BotBaseSettings::RangedSetting: + case BotBaseSettings::IllusionBlock: + case BotBaseSettings::MaxMeleeRange: + default: + return false; + } + + return true; +} + + +void Bot::LoadDefaultBotSettings() { + m_bot_spell_settings.clear(); + + uint8 bot_stance = GetBotStance(); + + for (uint16 i = BotBaseSettings::START_ALL; i <= BotBaseSettings::END; ++i) { + SetBotBaseSetting(i, GetDefaultSetting(BotSettingCategories::BaseSetting, i, bot_stance)); + LogBotSettingsDetail("{} says, 'Setting default {} [{}] to [{}]'", GetCleanName(), GetBotSettingCategoryName(i), i, GetDefaultBotBaseSetting(i, bot_stance)); + } + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + BotSpellSettings t; + + t.spell_type = i; + t.short_name = GetSpellTypeShortNameByID(i); + t.name = GetSpellTypeNameByID(i); + t.hold = GetDefaultSpellTypeHold(i, bot_stance); + t.delay = GetDefaultSpellTypeDelay(i, bot_stance); + t.min_threshold = GetDefaultSpellTypeMinThreshold(i, bot_stance); + t.max_threshold = GetDefaultSpellTypeMaxThreshold(i, bot_stance); + t.resist_limit = GetDefaultSpellTypeResistLimit(i, bot_stance); + t.aggro_check = GetDefaultSpellTypeAggroCheck(i, bot_stance); + t.min_mana_pct = GetDefaultSpellTypeMinManaLimit(i, bot_stance); + t.max_mana_pct = GetDefaultSpellTypeMaxManaLimit(i, bot_stance); + t.min_hp_pct = GetDefaultSpellTypeMinHPLimit(i, bot_stance); + t.max_hp_pct = GetDefaultSpellTypeMaxHPLimit(i, bot_stance); + t.idle_priority = GetDefaultSpellTypePriority( + i, + BotPriorityCategories::Idle, + GetClass(), + bot_stance + ); + t.engaged_priority = GetDefaultSpellTypePriority( + i, + BotPriorityCategories::Engaged, + GetClass(), + bot_stance + ); + t.pursue_priority = GetDefaultSpellTypePriority( + i, + BotPriorityCategories::Pursue, + GetClass(), + bot_stance + ); + t.ae_or_group_target_count = GetDefaultSpellTypeAEOrGroupTargetCount(i, bot_stance); + t.announce_cast = GetDefaultSpellTypeAnnounceCast(i, bot_stance); + t.recast_timer.Start(); + t.ai_delay.Start(); + + m_bot_spell_settings.push_back(t); + + LogBotSettingsDetail("{} says, 'Setting defaults for {} ({}) [#{}] - [{} [#{}] stance]'", GetCleanName(), t.name, t.short_name, t.spell_type, Stance::GetName(bot_stance), bot_stance); + LogBotSettingsDetail("{} says, 'Hold = [{}] | Delay = [{}ms] | MinThreshold = [{}\%] | MaxThreshold = [{}\%]'", GetCleanName(), + GetDefaultSpellTypeHold(i, bot_stance), + GetDefaultSpellTypeDelay(i, bot_stance), + GetDefaultSpellTypeMinThreshold(i, bot_stance), + GetDefaultSpellTypeMaxThreshold(i, bot_stance)); + LogBotSettingsDetail("{} says, 'AggroCheck = [{}] | MinManaPCT = [{}\%] | MaxManaPCT = [{}\%] | MinHPPCT = [{}\% | MaxHPPCT = [{}\%]'", GetCleanName(), GetDefaultSpellTypeAggroCheck(i, bot_stance), GetDefaultSpellTypeMinManaLimit(i, bot_stance), GetDefaultSpellTypeMaxManaLimit(i, bot_stance), GetDefaultSpellTypeMinHPLimit(i, bot_stance), GetDefaultSpellTypeMaxHPLimit(i, bot_stance)); + LogBotSettingsDetail("{} says, 'IdlePriority = [{}] | EngagedPriority = [{}] | PursuePriority = [{}]'", GetCleanName(), GetDefaultSpellTypeIdlePriority(i, GetClass(), bot_stance), GetDefaultSpellTypeEngagedPriority(i, GetClass(), bot_stance), GetDefaultSpellTypePursuePriority(i, GetClass(), bot_stance)); + LogBotSettingsDetail("{} says, 'TargetCount = [{}] | AnnounceCast = [{}]'", GetCleanName(), GetDefaultSpellTypeAEOrGroupTargetCount(i, bot_stance), GetDefaultSpellTypeAnnounceCast(i, bot_stance)); + } +} + +void Bot::SetBotSpellRecastTimer(uint16 spell_type, Mob* tar, bool precast) { + if (!tar) { + return; + } + + if (!precast && IsBotSpellTypeOtherBeneficial(spell_type)) { + return; + } + + uint32 added_delay = 0; + + switch (spell_type) { //Additional delays + case BotSpellTypes::Mez: + added_delay = RuleI(Bots, MezSuccessDelay); + break; + case BotSpellTypes::AEMez: + added_delay = RuleI(Bots, AEMezSuccessDelay); + break; + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + Mob* owner = tar->GetOwner(); + + if (owner->IsClient()) { + owner->CastToClient()->SetSpellTypeRecastTimer(spell_type, (GetUltimateSpellTypeDelay(spell_type, tar) + added_delay)); + } + else { + owner->CastToBot()->SetSpellTypeRecastTimer(spell_type, (GetUltimateSpellTypeDelay(spell_type, tar) + added_delay)); + } + } + else if (IsBotSpellTypeOtherBeneficial(spell_type)) { + if (tar->IsClient()) { + tar->CastToClient()->SetSpellTypeRecastTimer(spell_type, (GetUltimateSpellTypeDelay(spell_type, tar) + added_delay)); + } + else { + tar->CastToBot()->SetSpellTypeRecastTimer(spell_type, (GetUltimateSpellTypeDelay(spell_type, tar) + added_delay)); + } + } + else { + SetSpellTypeRecastTimer(spell_type, (GetUltimateSpellTypeDelay(spell_type, tar) + added_delay)); + } +} + +BotSpell Bot::GetSpellByHealType(uint16 spell_type, Mob* tar) { + switch (spell_type) { + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + return GetBestBotSpellForVeryFastHeal(this, tar, spell_type); + case BotSpellTypes::FastHeals: + case BotSpellTypes::PetFastHeals: + return GetBestBotSpellForFastHeal(this, tar, spell_type); + case BotSpellTypes::RegularHeal: + case BotSpellTypes::PetRegularHeals: + return GetBestBotSpellForRegularSingleTargetHeal(this, tar, spell_type); + case BotSpellTypes::GroupHeals: + return GetBestBotSpellForGroupHeal(this, tar, spell_type); + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::PetCompleteHeals: + return GetBestBotSpellForPercentageHeal(this, tar, spell_type); + case BotSpellTypes::GroupCompleteHeals: + return GetBestBotSpellForGroupCompleteHeal(this, tar, spell_type); + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetHoTHeals: + return GetBestBotSpellForHealOverTime(this, tar, spell_type); + case BotSpellTypes::GroupHoTHeals: + return GetBestBotSpellForGroupHealOverTime(this, tar, spell_type); + } +} + +uint16 Bot::GetSpellTypePriority(uint16 spell_type, uint8 priority_type) { + switch (priority_type) { + case BotPriorityCategories::Idle: + return m_bot_spell_settings[spell_type].idle_priority; + case BotPriorityCategories::Engaged: + return m_bot_spell_settings[spell_type].engaged_priority; + case BotPriorityCategories::Pursue: + return m_bot_spell_settings[spell_type].pursue_priority; + default: + return 0; + } +} + +int Bot::GetDefaultSetting(uint16 setting_category, uint16 setting_type, uint8 stance) { + switch (setting_category) { + case BotSettingCategories::BaseSetting: + return GetDefaultBotBaseSetting(setting_type, stance); + case BotSettingCategories::SpellHold: + return GetDefaultSpellTypeHold(setting_type, stance); + case BotSettingCategories::SpellDelay: + return GetDefaultSpellTypeDelay(setting_type, stance); + case BotSettingCategories::SpellMinThreshold: + return GetDefaultSpellTypeMinThreshold(setting_type, stance); + case BotSettingCategories::SpellMaxThreshold: + return GetDefaultSpellTypeMaxThreshold(setting_type, stance); + case BotSettingCategories::SpellTypeResistLimit: + return GetDefaultSpellTypeResistLimit(setting_type, stance); + case BotSettingCategories::SpellTypeAggroCheck: + return GetDefaultSpellTypeAggroCheck(setting_type, stance); + case BotSettingCategories::SpellTypeMinManaPct: + return GetDefaultSpellTypeMinManaLimit(setting_type, stance); + case BotSettingCategories::SpellTypeMaxManaPct: + return GetDefaultSpellTypeMaxManaLimit(setting_type, stance); + case BotSettingCategories::SpellTypeMinHPPct: + return GetDefaultSpellTypeMinHPLimit(setting_type, stance); + case BotSettingCategories::SpellTypeMaxHPPct: + return GetDefaultSpellTypeMaxHPLimit(setting_type, stance); + case BotSettingCategories::SpellTypeIdlePriority: + return GetDefaultSpellTypePriority(setting_type, BotPriorityCategories::Idle, GetClass(), stance); + case BotSettingCategories::SpellTypeEngagedPriority: + return GetDefaultSpellTypePriority(setting_type, BotPriorityCategories::Engaged, GetClass(), stance); + case BotSettingCategories::SpellTypePursuePriority: + return GetDefaultSpellTypePriority(setting_type, BotPriorityCategories::Pursue, GetClass(), stance); + case BotSettingCategories::SpellTypeAEOrGroupTargetCount: + return GetDefaultSpellTypeAEOrGroupTargetCount(setting_type, stance); + case BotSettingCategories::SpellTypeAnnounceCast: + return GetDefaultSpellTypeAnnounceCast(setting_type, stance); + default: + break; + } +} + +int Bot::GetSetting(uint16 setting_category, uint16 setting_type) { + switch (setting_category) { + case BotSettingCategories::BaseSetting: + return GetBotBaseSetting(setting_type); + case BotSettingCategories::SpellHold: + return GetSpellTypeHold(setting_type); + case BotSettingCategories::SpellDelay: + return GetSpellTypeDelay(setting_type); + case BotSettingCategories::SpellMinThreshold: + return GetSpellTypeMinThreshold(setting_type); + case BotSettingCategories::SpellMaxThreshold: + return GetSpellTypeMaxThreshold(setting_type); + case BotSettingCategories::SpellTypeResistLimit: + return GetSpellTypeResistLimit(setting_type); + case BotSettingCategories::SpellTypeAggroCheck: + return GetSpellTypeAggroCheck(setting_type); + case BotSettingCategories::SpellTypeMinManaPct: + return GetSpellTypeMinManaLimit(setting_type); + case BotSettingCategories::SpellTypeMaxManaPct: + return GetSpellTypeMaxManaLimit(setting_type); + case BotSettingCategories::SpellTypeMinHPPct: + return GetSpellTypeMinHPLimit(setting_type); + case BotSettingCategories::SpellTypeMaxHPPct: + return GetSpellTypeMaxHPLimit(setting_type); + case BotSettingCategories::SpellTypeIdlePriority: + return GetSpellTypePriority(setting_type, BotPriorityCategories::Idle); + case BotSettingCategories::SpellTypeEngagedPriority: + return GetSpellTypePriority(setting_type, BotPriorityCategories::Engaged); + case BotSettingCategories::SpellTypePursuePriority: + return GetSpellTypePriority(setting_type, BotPriorityCategories::Pursue); + case BotSettingCategories::SpellTypeAEOrGroupTargetCount: + return GetSpellTypeAEOrGroupTargetCount(setting_type); + case BotSettingCategories::SpellTypeAnnounceCast: + return GetSpellTypeAnnounceCast(setting_type); + default: + break; + } +} + +bool Bot::GetDefaultSpellTypeHold(uint16 spell_type, uint8 stance) { + uint8 bot_class = GetClass(); + + switch (spell_type) { + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::Pet: + case BotSpellTypes::Escape: + case BotSpellTypes::Lifetap: + case BotSpellTypes::Buff: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::InCombatBuff: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::DamageShields: + case BotSpellTypes::ResistBuffs: + return false; + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::RegularHeal: + switch (stance) { + case Stance::Aggressive: + case Stance::AEBurn: + case Stance::Burn: + return true; + default: + return false; + } + case BotSpellTypes::Cure: + case BotSpellTypes::GroupCures: + switch (stance) { + case Stance::Aggressive: + case Stance::AEBurn: + case Stance::Burn: + return true; + default: + return false; + } + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + case BotSpellTypes::PreCombatBuffSong: + if (bot_class == Class::Bard) { + return false; + } + else { + return true; + } + case BotSpellTypes::Nuke: + case BotSpellTypes::DOT: + case BotSpellTypes::Stun: + switch (stance) { + case Stance::Assist: + return true; + default: + return false; + } + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::AEStun: + case BotSpellTypes::AEDoT: + case BotSpellTypes::AELifetap: + case BotSpellTypes::PBAENuke: + switch (stance) { + case Stance::AEBurn: + return false; + default: + return true; + } + case BotSpellTypes::Mez: + case BotSpellTypes::AEMez: + case BotSpellTypes::Debuff: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Slow: + case BotSpellTypes::AESlow: + case BotSpellTypes::HateRedux: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + return true; + default: + return false; + } + case BotSpellTypes::Snare: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Assist: + return true; + default: + return false; + } + case BotSpellTypes::HateLine: + if (bot_class == Class::ShadowKnight || bot_class == Class::Paladin) { + switch (stance) { + case Stance::Aggressive: + return false; + default: + return true; + } + } + else { + return true; + } + case BotSpellTypes::Charm: + case BotSpellTypes::Resurrect: + case BotSpellTypes::AESnare: + case BotSpellTypes::AERoot: + case BotSpellTypes::Root: + case BotSpellTypes::AEDispel: + case BotSpellTypes::Dispel: + case BotSpellTypes::AEFear: + case BotSpellTypes::Fear: + case BotSpellTypes::AEHateLine: + case BotSpellTypes::PetCures: + case BotSpellTypes::PetHoTHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::PetResistBuffs: + default: + return true; + } +} + +uint16 Bot::GetDefaultSpellTypePriority(uint16 spell_type, uint8 priority_type, uint8 bot_class, uint8 stance) { + switch (priority_type) { + case BotPriorityCategories::Idle: + return GetDefaultSpellTypeIdlePriority(spell_type, bot_class, stance); + case BotPriorityCategories::Engaged: + return GetDefaultSpellTypeEngagedPriority(spell_type, bot_class, stance); + case BotPriorityCategories::Pursue: + return GetDefaultSpellTypePursuePriority(spell_type, bot_class, stance); + default: + return 0; + } +} + +uint16 Bot::GetDefaultSpellTypeIdlePriority(uint16 spell_type, uint8 bot_class, uint8 stance) { + if (!IsBotSpellTypeBeneficial(spell_type)) { + return 0; + } + + switch (spell_type) { + case BotSpellTypes::VeryFastHeals: + return 1; + case BotSpellTypes::FastHeals: + return 2; + case BotSpellTypes::GroupHeals: + return 3; + case BotSpellTypes::RegularHeal: + return 4; + case BotSpellTypes::GroupCompleteHeals: + return 5; + case BotSpellTypes::CompleteHeal: + return 6; + case BotSpellTypes::GroupHoTHeals: + return 7; + case BotSpellTypes::HoTHeals: + return 8; + case BotSpellTypes::GroupCures: + return 9; + case BotSpellTypes::Cure: + return 10; + case BotSpellTypes::PetVeryFastHeals: + return 11; + case BotSpellTypes::PetFastHeals: + return 12; + case BotSpellTypes::PetRegularHeals: + return 13; + case BotSpellTypes::PetCompleteHeals: + return 14; + case BotSpellTypes::PetHoTHeals: + return 15; + case BotSpellTypes::PetCures: + return 16; + case BotSpellTypes::Pet: + return 17; + case BotSpellTypes::Buff: + return 18; + case BotSpellTypes::OutOfCombatBuffSong: + return 19; + case BotSpellTypes::ResistBuffs: + return 20; + case BotSpellTypes::DamageShields: + return 21; + case BotSpellTypes::PetBuffs: + return 22; + case BotSpellTypes::PreCombatBuff: + return 23; + case BotSpellTypes::PreCombatBuffSong: + return 24; + case BotSpellTypes::PetResistBuffs: + return 25; + case BotSpellTypes::PetDamageShields: + return 26; + default: + return 0; //unused + } + + return 0; +} + +uint16 Bot::GetDefaultSpellTypeEngagedPriority(uint16 spell_type, uint8 bot_class, uint8 stance) { + switch (spell_type) { + case BotSpellTypes::Escape: + return 1; + case BotSpellTypes::VeryFastHeals: + return 2; + case BotSpellTypes::FastHeals: + return 3; + case BotSpellTypes::GroupHeals: + return 4; + case BotSpellTypes::RegularHeal: + return 5; + case BotSpellTypes::GroupCompleteHeals: + return 6; + case BotSpellTypes::CompleteHeal: + return 7; + case BotSpellTypes::GroupHoTHeals: + return 8; + case BotSpellTypes::HoTHeals: + return 9; + case BotSpellTypes::GroupCures: + return 10; + case BotSpellTypes::Cure: + return 11; + case BotSpellTypes::PetVeryFastHeals: + return 12; + case BotSpellTypes::PetFastHeals: + return 13; + case BotSpellTypes::PetRegularHeals: + return 14; + case BotSpellTypes::PetCompleteHeals: + return 15; + case BotSpellTypes::PetHoTHeals: + return 16; + case BotSpellTypes::PetCures: + return 17; + case BotSpellTypes::AELifetap: + return 18; + case BotSpellTypes::Lifetap: + return 19; + case BotSpellTypes::HateRedux: + return 20; + case BotSpellTypes::AEMez: + return 21; + case BotSpellTypes::Mez: + return 22; + case BotSpellTypes::AEHateLine: + return 23; + case BotSpellTypes::HateLine: + return 24; + case BotSpellTypes::AEDispel: + return 25; + case BotSpellTypes::Dispel: + return 26; + case BotSpellTypes::AEDebuff: + return 27; + case BotSpellTypes::Debuff: + return 28; + case BotSpellTypes::AESnare: + return 29; + case BotSpellTypes::Snare: + return 30; + case BotSpellTypes::AESlow: + return 31; + case BotSpellTypes::Slow: + return 32; + case BotSpellTypes::AERoot: + return 33; + case BotSpellTypes::Root: + return 34; + case BotSpellTypes::AEDoT: + return 35; + case BotSpellTypes::DOT: + return 36; + case BotSpellTypes::AEStun: + return 37; + case BotSpellTypes::PBAENuke: + return 38; + case BotSpellTypes::AENukes: + return 39; + case BotSpellTypes::AERains: + return 40; + case BotSpellTypes::Stun: + return 41; + case BotSpellTypes::Nuke: + return 42; + case BotSpellTypes::InCombatBuff: + return 43; + case BotSpellTypes::InCombatBuffSong: + return 44; + default: + return 0; //unused + } +} + +uint16 Bot::GetDefaultSpellTypePursuePriority(uint16 spell_type, uint8 bot_class, uint8 stance) { + switch (spell_type) { + case BotSpellTypes::Escape: + return 1; + case BotSpellTypes::VeryFastHeals: + return 2; + case BotSpellTypes::FastHeals: + return 3; + case BotSpellTypes::GroupHeals: + return 4; + case BotSpellTypes::RegularHeal: + return 5; + case BotSpellTypes::GroupCompleteHeals: + return 6; + case BotSpellTypes::CompleteHeal: + return 7; + case BotSpellTypes::GroupHoTHeals: + return 8; + case BotSpellTypes::HoTHeals: + return 9; + case BotSpellTypes::GroupCures: + return 10; + case BotSpellTypes::Cure: + return 11; + case BotSpellTypes::Snare: + return 12; + case BotSpellTypes::Lifetap: + return 13; + case BotSpellTypes::Dispel: + return 14; + case BotSpellTypes::Stun: + return 15; + case BotSpellTypes::Nuke: + return 16; + case BotSpellTypes::DOT: + return 17; + case BotSpellTypes::PetVeryFastHeals: + return 18; + case BotSpellTypes::PetFastHeals: + return 19; + case BotSpellTypes::PetRegularHeals: + return 20; + case BotSpellTypes::PetCompleteHeals: + return 21; + case BotSpellTypes::PetHoTHeals: + return 22; + case BotSpellTypes::PetCures: + return 23; + default: + return 0; //unused + } +} + +uint16 Bot::GetDefaultSpellTypeResistLimit(uint16 spell_type, uint8 stance) { + return IsBotSpellTypeBeneficial(spell_type) ? 0 : RuleI(Bots, SpellResistLimit); +} + +bool Bot::GetDefaultSpellTypeAggroCheck(uint16 spell_type, uint8 stance) { + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + return false; + default: + break; + } + + switch (spell_type) { + case BotSpellTypes::Nuke: + case BotSpellTypes::Root: + case BotSpellTypes::Snare: + case BotSpellTypes::DOT: + case BotSpellTypes::Slow: + case BotSpellTypes::Debuff: + case BotSpellTypes::Fear: + case BotSpellTypes::Stun: + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::AEStun: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::AESlow: + case BotSpellTypes::AESnare: + case BotSpellTypes::AEFear: + case BotSpellTypes::AEDispel: + case BotSpellTypes::AERoot: + case BotSpellTypes::AEDoT: + case BotSpellTypes::PBAENuke: + return true; + default: + return false; + } +} + +uint8 Bot::GetDefaultSpellTypeMinManaLimit(uint16 spell_type, uint8 stance) { + return 0; +} + +uint8 Bot::GetDefaultSpellTypeMaxManaLimit(uint16 spell_type, uint8 stance) { + switch (spell_type) { + case BotSpellTypes::InCombatBuff: + if (GetClass() == Class::Shaman) { + return 75; + } + + break; + default: + break; + } + + return 100; +} + +uint8 Bot::GetDefaultSpellTypeMinHPLimit(uint16 spell_type, uint8 stance) { + switch (spell_type) { + case BotSpellTypes::InCombatBuff: + if (GetClass() == Class::Shaman) { + return 40; + } + + break; + default: + break; + } + + return 0; +} + +uint8 Bot::GetDefaultSpellTypeMaxHPLimit(uint16 spell_type, uint8 stance) { + return 100; +} + +uint16 Bot::GetDefaultSpellTypeAEOrGroupTargetCount(uint16 spell_type, uint8 stance) { + if (IsAEBotSpellType(spell_type)) { + return RuleI(Bots, MinTargetsForAESpell); + } + + if (IsGroupBotSpellType(spell_type)) { + return RuleI(Bots, MinTargetsForGroupSpell); + } + + return 1; +} + +uint16 Bot::GetDefaultSpellTypeAnnounceCast(uint16 spell_type, uint8 stance) { + switch (GetClass()) { + case Class::Bard: + switch (spell_type) { + case BotSpellTypes::Mez: + case BotSpellTypes::AEMez: + case BotSpellTypes::Cure: + case BotSpellTypes::GroupCures: + case BotSpellTypes::PetCures: + case BotSpellTypes::Charm: + return 1; + default: + return 0; + } + + return 1; + default: + return 1; + } + + return 1; +} + +bool Bot::GetUltimateSpellTypeHold(uint16 spell_type, Mob* tar) { + if (!tar) { + return GetSpellTypeHold(spell_type); + } + + if (tar->IsPet() && tar->GetOwner() && tar->IsPetOwnerBot()) { + return tar->GetOwner()->CastToBot()->GetSpellTypeHold(GetPetBotSpellType(spell_type)); + } + + return GetSpellTypeHold(spell_type); +} + +void Bot::SetSpellTypePriority(uint16 spell_type, uint8 priority_type, uint16 priority) { + switch (priority_type) { + case BotPriorityCategories::Idle: + m_bot_spell_settings[spell_type].idle_priority = priority; + break; + case BotPriorityCategories::Engaged: + m_bot_spell_settings[spell_type].engaged_priority = priority; + break; + case BotPriorityCategories::Pursue: + m_bot_spell_settings[spell_type].pursue_priority = priority; + break; + default: + return; + } +} + +std::list Bot::GetSpellTypesPrioritized(uint8 priority_type) { + std::list cast_order; + std::list temp_cast_order; + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; i++) { + BotSpellTypeOrder typeSettings = { + .spellType = i, + .priority = GetSpellTypePriority(i, priority_type) + }; + + cast_order.emplace_back(typeSettings); + } + + for (auto& currentType : cast_order) { + if (currentType.priority != 0) { + temp_cast_order.emplace_back(currentType); + } + } + + cast_order = temp_cast_order; + + if (cast_order.size() > 1) { + cast_order.sort( + [](BotSpellTypeOrder const& l, BotSpellTypeOrder const& r) { + return l.priority < r.priority; + } + ); + } + + return cast_order; +} + +bool Bot::AttemptAICastSpell(uint16 spell_type, Mob* tar) { + bool result = false; + + if (!tar) { + if (GetTarget()) { + tar = GetTarget(); + } + else { + if (!IsBotSpellTypeBeneficial(spell_type)) { + return result; + } + } + } + + if (!IsTaunting() && !IsCommandedSpell() && GetSpellTypeAggroCheck(spell_type) && HasOrMayGetAggro(IsSitting())) { + LogBotSpellChecksDetail("{} says, 'Cancelling cast of [{}] due to GetSpellTypeAggroCheck and HasOrMayGetAggro.'", GetCleanName(), GetSpellTypeNameByID(spell_type)); + return result; + } + + if (IsBotSpellTypeBeneficial(spell_type)) { + if (GetClass() == Class::Bard) { + tar = this; + } + + if (!PrecastChecks(tar, spell_type) || !AICastSpell(tar, GetChanceToCastBySpellType(spell_type), spell_type)) { + return result; + } + } + else { + if (!PrecastChecks(tar, spell_type) || !AICastSpell(tar, GetChanceToCastBySpellType(spell_type), spell_type)) { + return result; + } + } + + result = true; + + return result; +} + +bool Bot::AttemptAACastSpell(Mob* tar, uint16 spell_id, AA::Rank* rank) { + if (!IsValidSpell(spell_id)) { + return false; + } + + SPDat_Spell_Struct spell = spells[spell_id]; + + if (!tar || spell.target_type == ST_Self) { + tar = this; + } + + if (!DoLosChecks(tar)) { + return false; + } + + if (CheckSpellRecastTimer(spell_id)) { + if (IsBeneficialSpell(spell_id)) { + if ( + (tar->IsNPC() && !tar->GetOwner()) || + (tar->GetOwner() && tar->GetOwner()->IsOfClientBot() && !GetOwner()->IsInGroupOrRaid(tar->GetOwner())) || + (tar->IsOfClientBot() && !GetOwner()->IsInGroupOrRaid(tar)) + ) { + GetBotOwner()->Message(Chat::Yellow, "[%s] is an invalid target. Only players or their pet in your group or raid are eligible targets.", tar->GetCleanName()); + + return false; + } + } + + if (IsDetrimentalSpell(spell_id) && !IsAttackAllowed(tar)) { + GetBotOwner()->Message(Chat::Yellow, "%s says, 'I cannot attack [%s]'.", GetCleanName(), tar->GetCleanName()); + + return false; + } + + if (!CastChecks(spell_id, tar, UINT16_MAX)) { + GetBotOwner()->Message(Chat::Red, "%s says, 'Ability failed to cast. This could be due to this to any number of things: range, mana, immune, target type, etc.'", GetCleanName()); + + return false; + } + + + if (IsCasting()) { + RaidGroupSay( + this, + fmt::format( + "Interrupting {}. I have been commanded to try to cast an AA - {} on {}.", + CastingSpellID() ? spells[CastingSpellID()].name : "my spell", + spell.name, + tar->GetCleanName() + ).c_str() + ); + + InterruptSpell(); + } + + if (CastSpell(spell_id, tar->GetID())) { + RaidGroupSay( + this, + fmt::format( + "Casting an AA - {} on {}.", + GetSpellName(spell_id), + (tar == this ? "myself" : tar->GetCleanName()) + ).c_str() + ); + + int timer_duration = (rank->recast_time - GetAlternateAdvancementCooldownReduction(rank)) * 1000; + + if (timer_duration < 0) { + timer_duration = 0; + } + + SetSpellRecastTimer(spell_id, timer_duration); + } + else { + GetBotOwner()->Message(Chat::Red, "%s says, 'Ability failed to cast. This could be due to this to any number of things: range, mana, immune, target type, etc.'", GetCleanName()); + + return false; + } + } + else { + GetBotOwner()->Message( + Chat::Yellow, + fmt::format( + "{} says, 'Ability recovery time not yet met. {} remaining.'", + GetCleanName(), + Strings::SecondsToTime(GetSpellRecastRemainingTime(spell_id), true) + ).c_str() + ); + + return false; + } + + return true; +} + +bool Bot::AttemptForcedCastSpell(Mob* tar, uint16 spell_id, bool is_disc) { + if (!IsValidSpell(spell_id)) { + return false; + } + + SPDat_Spell_Struct spell = spells[spell_id]; + + if (!tar || (spell.target_type == ST_Self && tar != this)) { + tar = this; + } + + if ((IsCharmSpell(spell_id) || (IsPetSpell(spell_id) && HasPet()))) { + return false; + } + + if (IsResurrectSpell(spell_id) && (!tar->IsCorpse() || !tar->CastToCorpse()->IsPlayerCorpse())) { + return false; + } + + if (IsBeneficialSpell(spell_id)) { + bool invalid_beneficial_target = + (tar->IsNPC() && !tar->GetOwner()) || + (tar->GetOwner() && tar->GetOwner()->IsOfClientBot() && !GetBotOwner()->IsInGroupOrRaid(tar->GetOwner())) || + (tar->IsOfClientBot() && !GetBotOwner()->IsInGroupOrRaid(tar)); + + if (invalid_beneficial_target) { + GetBotOwner()->Message( + Chat::Yellow, + fmt::format("[{}] is an invalid target. Only players or their pet in your group or raid are eligible targets.", tar->GetCleanName()).c_str() + ); + return false; + } + } + + if (IsDetrimentalSpell(spell_id) && (!GetBotOwner()->IsAttackAllowed(tar) || !IsAttackAllowed(tar))) { + GetBotOwner()->Message( + Chat::Yellow, + fmt::format("{} says, 'I cannot attack [{}]'.", GetCleanName(), tar->GetCleanName()).c_str() + ); + return false; + } + + if (!is_disc && !CheckSpellRecastTimer(spell_id)) { + return false; + } + + if (!IsInGroupOrRaid(tar, true) && + (!RuleB(Bots, EnableBotTGB) || (IsGroupSpell(spell_id) && !IsTGBCompatibleSpell(spell_id)))) { + return false; + } + + if (!DoLosChecks(tar)) { + return false; + } + + if (!CastChecks(spell_id, tar, UINT16_MAX)) { + GetBotOwner()->Message( + Chat::Red, + fmt::format("{} says, 'Ability failed to cast. This could be due to range, mana, immune, target type, etc.'", GetBotOwner()->GetCleanName()).c_str() + ); + return false; + } + + if (IsCasting()) { + RaidGroupSay( + this, + fmt::format( + "Interrupting {}. I have been commanded to try to cast {} on {}.", + CastingSpellID() ? spells[CastingSpellID()].name : "my spell", + spell.name, + tar->GetCleanName() + ).c_str() + ); + InterruptSpell(); + } + + if (!CastSpell(spell_id, tar->GetID())) { + return false; + } + + RaidGroupSay( + this, + fmt::format( + "Casting {} on {}.", + GetSpellName(spell_id), + (tar == this ? "myself" : tar->GetCleanName()) + ).c_str() + ); + + int timer_duration = 0; + + if (!is_disc) { + timer_duration = CalcBuffDuration(tar, this, spell_id); + timer_duration = std::max(0, GetActSpellDuration(spell_id, timer_duration)); + SetSpellRecastTimer(spell_id, timer_duration); + } + else if (spell.recast_time > 0) { + timer_duration = std::max(0, int(spell.recast_time / 1000 - GetFocusEffect(focusReduceRecastTime, spell_id))); + SetDisciplineReuseTimer(spell_id, timer_duration * 1000); + } + + return true; +} + +uint16 Bot::GetParentSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::AEStun: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Nuke: + case BotSpellTypes::Stun: + return BotSpellTypes::Nuke; + case BotSpellTypes::RegularHeal: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + return BotSpellTypes::RegularHeal; + case BotSpellTypes::Buff: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::DamageShields: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::BindAffinity: + case BotSpellTypes::Identify: + case BotSpellTypes::Levitate: + case BotSpellTypes::Rune: + case BotSpellTypes::WaterBreathing: + case BotSpellTypes::Size: + case BotSpellTypes::Invisibility: + case BotSpellTypes::MovementSpeed: + return BotSpellTypes::Buff; + case BotSpellTypes::AEMez: + case BotSpellTypes::Mez: + return BotSpellTypes::Mez; + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + return BotSpellTypes::Debuff; + case BotSpellTypes::AESlow: + case BotSpellTypes::Slow: + return BotSpellTypes::Slow; + case BotSpellTypes::AESnare: + case BotSpellTypes::Snare: + return BotSpellTypes::Snare; + case BotSpellTypes::AEFear: + case BotSpellTypes::Fear: + return BotSpellTypes::Fear; + case BotSpellTypes::GroupCures: + case BotSpellTypes::Cure: + case BotSpellTypes::PetCures: + return BotSpellTypes::Cure; + case BotSpellTypes::AERoot: + case BotSpellTypes::Root: + return BotSpellTypes::Root; + case BotSpellTypes::AEDispel: + case BotSpellTypes::Dispel: + return BotSpellTypes::Dispel; + case BotSpellTypes::AEDoT: + case BotSpellTypes::DOT: + return BotSpellTypes::DOT; + case BotSpellTypes::AELifetap: + case BotSpellTypes::Lifetap: + return BotSpellTypes::Lifetap; + case BotSpellTypes::AELull: + case BotSpellTypes::Lull: + return BotSpellTypes::Lull; + case BotSpellTypes::Charm: + case BotSpellTypes::Escape: + case BotSpellTypes::HateRedux: + case BotSpellTypes::HateLine: + case BotSpellTypes::AEHateLine: + case BotSpellTypes::InCombatBuff: + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + case BotSpellTypes::Pet: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::Resurrect: + default: + return spell_type; + } + + return spell_type; +} + +bool Bot::IsValidBotSpellType(uint16 spell_type) { + return ( + EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END) || + EQ::ValueWithin(spell_type, BotSpellTypes::COMMANDED_START, BotSpellTypes::COMMANDED_END) + ); +} + +bool Bot::IsValidSpellTypeBySpellID(uint16 spell_type, uint16 spell_id) { + if (IsAEBotSpellType(spell_type) && !IsAnyAESpell(spell_id)) { + return false; + } + + if (IsGroupBotSpellType(spell_type) && !IsGroupSpell(spell_id)) { + return false; + } + + switch (spell_type) { + case BotSpellTypes::Buff: + case BotSpellTypes::PetBuffs: + if ( + IsResistanceOnlySpell(spell_id) || + IsDamageShieldOnlySpell(spell_id) || + IsDamageShieldAndResistSpell(spell_id) + ) { + return false; + } + + return true; + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::PetResistBuffs: + if (IsResistanceBuffSpell(spell_id)) { + return true; + } + + return false; + case BotSpellTypes::DamageShields: + case BotSpellTypes::PetDamageShields: + if (IsEffectInSpell(spell_id, SE_DamageShield)) { + return true; + } + + return false; + case BotSpellTypes::PBAENuke: + if ( + IsPBAENukeSpell(spell_id) && + !IsStunSpell(spell_id) + ) { + return true; + } + + return false; + case BotSpellTypes::AERains: + if ( + IsAERainNukeSpell(spell_id) && + !IsStunSpell(spell_id) + ) { + return true; + } + return false; + case BotSpellTypes::AEStun: + case BotSpellTypes::Stun: + if (IsStunSpell(spell_id)) { + return true; + } + + return false; + case BotSpellTypes::AENukes: + case BotSpellTypes::Nuke: + if (!IsStunSpell(spell_id)) { + return true; + } + + return false; + case BotSpellTypes::Lull: + if (IsHarmonySpell(spell_id)) { + return true; + } + + return false; + case BotSpellTypes::Teleport: + if ( + IsBeneficialSpell(spell_id) && + ( + IsEffectInSpell(spell_id, SE_Teleport) || IsEffectInSpell(spell_id, SE_Translocate) + ) + ) { + return true; + } + + return false; + case BotSpellTypes::Succor: + if ( + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_Succor) + ) { + return true; + } + + return false; + case BotSpellTypes::BindAffinity: + if (IsEffectInSpell(spell_id, SE_BindAffinity)) { + return true; + } + + return false; + case BotSpellTypes::Identify: + if (IsEffectInSpell(spell_id, SE_Identify)) { + return true; + } + + return false; + case BotSpellTypes::Levitate: + if ( + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_Levitate) + ) { + return true; + } + + return false; + case BotSpellTypes::Rune: + if ( + IsEffectInSpell(spell_id, SE_AbsorbMagicAtt) || + IsEffectInSpell(spell_id, SE_Rune) + ) { + return true; + } + + return false; + case BotSpellTypes::WaterBreathing: + if (IsEffectInSpell(spell_id, SE_WaterBreathing)) { + return true; + } + + return false; + case BotSpellTypes::Size: + if ( + IsBeneficialSpell(spell_id) && + ( + IsEffectInSpell(spell_id, SE_ModelSize) || + IsEffectInSpell(spell_id, SE_ChangeHeight) + ) + ) { + return true; + } + + return false; + case BotSpellTypes::Invisibility: + if ( + IsEffectInSpell(spell_id, SE_SeeInvis) || + IsInvisibleSpell(spell_id) + ) { + return true; + } + + return false; + case BotSpellTypes::MovementSpeed: + if ( + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_MovementSpeed) + ) { + return true; + } + + return false; + case BotSpellTypes::SendHome: + if ( + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_GateToHomeCity) + ) { + return true; + } + + return false; + case BotSpellTypes::SummonCorpse: + if (IsEffectInSpell(spell_id, SE_SummonCorpse)) { + return true; + } + + return false; + default: + return true; + } + + return true; +} + +void Bot::SetCastedSpellType(uint16 spell_type) { + _castedSpellType = spell_type; +} + +void Bot::DoFaceCheckWithJitter(Mob* tar) { + if (!tar) { + return; + } + + if (IsMoving()) { + return; + } + + SetCombatJitter(); + if (!IsFacingMob(tar)) { + FaceTarget(tar); + return; + } + return; +} + +void Bot::DoFaceCheckNoJitter(Mob* tar) { + if (!tar) { + return; + } + + if (IsMoving()) { + return; + } + + if (!IsFacingMob(tar)) { + FaceTarget(tar); + return; + } + return; +} + +void Bot::RunToGoalWithJitter(glm::vec3 Goal) { + RunTo(Goal.x, Goal.y, Goal.z); + SetCombatJitter(); +} + +void Bot::SetCombatOutOfRangeJitter() { + SetCombatOutOfRangeJitterFlag(); + + if (RuleI(Bots, MaxJitterTimer) > 0) { + m_combat_jitter_timer.Start(zone->random.Int(RuleI(Bots, MinJitterTimer), RuleI(Bots, MaxJitterTimer)), true); + } +} + +void Bot::SetCombatJitter() { + SetCombatJitterFlag(); + + if (RuleI(Bots, MaxJitterTimer) > 0) { + m_combat_jitter_timer.Start(zone->random.Int(RuleI(Bots, MinJitterTimer), RuleI(Bots, MaxJitterTimer)), true); + } +} + +void Bot::DoCombatPositioning( + Mob* tar, + glm::vec3 Goal, + bool stop_melee_level, + float tar_distance, + float melee_distance_min, + float melee_distance, + float melee_distance_max, + bool behind_mob, + bool front_mob +) { + if (HasTargetReflection()) { + if (!IsTaunting() && !tar->IsFeared() && !tar->IsStunned()) { + if (TryEvade(tar)) { + return; + } + } + else if (tar->IsRooted() && !IsTaunting()) { // Move non-taunters out of range - Above already checks if bot is targeted, otherwise they would stay + if (tar_distance <= melee_distance_max) { + if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, (melee_distance_max + 1), (melee_distance_max * 2), false, IsTaunting())) { + RunToGoalWithJitter(Goal); + + return; + } + } + } + else if (IsTaunting() && ((tar_distance < melee_distance_min) || !front_mob)) { // Back up any bots that are too close + if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, false, IsTaunting())) { + RunToGoalWithJitter(Goal); + + return; + } + } + } + else { + if (!tar->IsFeared()) { + if (IsTaunting()) { // Taunting adjustments + Mob* mob_tar = tar->GetTarget(); + + if (!mob_tar) { + DoFaceCheckNoJitter(tar); + + return; + } + + if (RuleB(Bots, TauntingBotsFollowTopHate)) { // If enabled, taunting bots will stick to top hate + if (Distance(m_Position, mob_tar->GetPosition()) > RuleI(Bots, DistanceTauntingBotsStickMainHate)) { + Goal = mob_tar->GetPosition(); + RunToGoalWithJitter(Goal); + + return; + } + } + else { // Otherwise, stick to any other bots that are taunting + if (mob_tar->IsBot() && mob_tar->CastToBot()->IsTaunting() && (Distance(m_Position, mob_tar->GetPosition()) > RuleI(Bots, DistanceTauntingBotsStickMainHate))) { + Goal = mob_tar->GetPosition(); + RunToGoalWithJitter(Goal); + + return; + } + } + } + else if (tar_distance < melee_distance_min || (GetBehindMob() && !behind_mob) || (IsTaunting() && !front_mob)|| !HasRequiredLoSForPositioning(tar)) { // Regular adjustment + if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, GetBehindMob(), IsTaunting())) { + RunToGoalWithJitter(Goal); + + return; + } + } + else if (tar->IsEnraged() && !IsTaunting() && !stop_melee_level && !behind_mob) { // Move non-taunting melee bots behind target during enrage + if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, true)) { + RunToGoalWithJitter(Goal); + + return; + } + } + } + } + + DoFaceCheckNoJitter(tar); + + return; +} + +bool Bot::PlotBotPositionAroundTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float min_distance, float max_distance, bool behind_only, bool front_only, bool bypass_los) { + bool Result = false; + + if (target) { + float look_heading = 0; + + min_distance = min_distance; + max_distance = max_distance; + float temp_x = 0; + float temp_y = 0; + float temp_z = target->GetZ(); + float best_z = 0; + auto offset = GetZOffset(); + const float tar_x = target->GetX(); + const float tar_y = target->GetY(); + float tar_distance = 0; + + glm::vec3 temp_z_Position; + glm::vec4 temp_m_Position; + + const uint16 max_iterations_allowed = 50; + uint16 counter = 0; + + while (counter < max_iterations_allowed) { + temp_x = tar_x + zone->random.Real(-max_distance, max_distance); + temp_y = tar_y + zone->random.Real(-max_distance, max_distance); + + temp_z_Position.x = temp_z; + temp_z_Position.y = temp_y; + temp_z_Position.z = temp_z; + best_z = GetFixedZ(temp_z_Position); + + if (best_z != BEST_Z_INVALID) { + temp_z = best_z; + } + else { + counter++; + continue; + } + + temp_m_Position.x = temp_x; + temp_m_Position.y = temp_y; + temp_m_Position.z = temp_z; + + tar_distance = Distance(target->GetPosition(), temp_m_Position); + + if (tar_distance > max_distance || tar_distance < min_distance) { + counter++; + continue; + } + + if (front_only && !InFrontMob(target, temp_x, temp_y)) { + counter++; + continue; + } + else if (behind_only && !BehindMob(target, temp_x, temp_y)) { + counter++; + continue; + } + + if (!bypass_los && CastToBot()->RequiresLoSForPositioning() && !CheckPositioningLosFN(target, temp_x, temp_y, temp_z)) { + counter++; + continue; + } + + Result = true; + break; + } + + if (Result) { + x_dest = temp_x; + y_dest = temp_y; + z_dest = temp_z; + } + } + + return Result; +} + +bool Bot::CheckDoubleRangedAttack() { + int32 chance = spellbonuses.DoubleRangedAttack + itembonuses.DoubleRangedAttack + aabonuses.DoubleRangedAttack; + + return ( + chance && + zone->random.Roll(chance) + ); +} + +bool Bot::RequiresLoSForPositioning() { + if (GetLevel() < GetStopMeleeLevel()) { + return true; + } + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + if (IsBotSpellTypeDetrimental(i) && !GetSpellTypeHold(i)) { + return true; + } + } + + return false; +} + +bool Bot::HasRequiredLoSForPositioning(Mob* tar) { + if (!tar) { + return true; + } + + if (RequiresLoSForPositioning() && !DoLosChecks(tar)) { + return false; + } + + return true; +} + +bool Bot::HasValidAETarget(Bot* caster, uint16 spell_id, uint16 spell_type, Mob* tar) { + int spell_range = caster->GetActSpellRange(spell_id, spells[spell_id].range); + int spell_ae_range = caster->GetActSpellRange(spell_id, spells[spell_id].aoe_range); + int target_count = 0; + + for (auto& close_mob : caster->m_close_mobs) { + Mob* m = close_mob.second; + + if (tar == m) { + continue; + } + + switch (spell_type) { + case BotSpellTypes::AEDispel: + if (m->GetSpecialAbility(SpecialAbility::DispellImmunity)) { + continue; + } + + break; + case BotSpellTypes::AEFear: + if (m->GetSpecialAbility(SpecialAbility::FearImmunity)) { + continue; + } + + break; + case BotSpellTypes::AESnare: + if (m->GetSpecialAbility(SpecialAbility::SnareImmunity)) { + continue; + } + + break; + case BotSpellTypes::AESlow: + if (m->GetSpecialAbility(SpecialAbility::SlowImmunity)) { + continue; + } + + break; + case BotSpellTypes::AEHateLine: + if (!m->IsNPC()) { + continue; + } + + break; + default: + break; + } + + if ( + !m->IsNPC() || + ( + !IsCommandedSpell() && + !m->CastToNPC()->IsOnHatelist(caster->GetOwner()) + ) + ) { + continue; + } + + if (SpellBreaksMez(spell_id) && m->IsMezzed()) { + continue; + } + + if (IsPBAESpell(spell_id)) { + if ( + spell_ae_range >= Distance(caster->GetPosition(), m->GetPosition()) && + caster->CastChecks(spell_id, m, spell_type, true, true) + ) { + ++target_count; + } + } + else { + if ( + !tar || + spell_range < Distance(caster->GetPosition(), tar->GetPosition()) || + !DoLosChecks(m) + ) { + continue; + } + + if ( + spell_ae_range >= Distance(tar->GetPosition(), m->GetPosition()) && + caster->CastChecks(spell_id, m, spell_type, true, true) + ) { + ++target_count; + } + } + } + + if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { + return false; + } + + SetHasLoS(true); + + return true; +} + +void Bot::CopySettings(Bot* to, uint8 setting_type, uint16 spell_type) { + switch (setting_type) { + case BotSettingCategories::BaseSetting: + for (uint16 i = BotBaseSettings::START; i <= BotBaseSettings::END; ++i) { + to->SetBotBaseSetting(i, GetBotBaseSetting(i)); + } + + break; + case BotSettingCategories::SpellHold: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeHold(spell_type, GetSpellTypeHold(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeHold(i, GetSpellTypeHold(i)); + } + } + + break; + case BotSettingCategories::SpellDelay: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeDelay(spell_type, GetSpellTypeDelay(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeDelay(i, GetSpellTypeDelay(i)); + } + } + + break; + case BotSettingCategories::SpellMinThreshold: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeMinThreshold(spell_type, GetSpellTypeMinThreshold(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMinThreshold(i, GetSpellTypeMinThreshold(i)); + } + } + + break; + case BotSettingCategories::SpellMaxThreshold: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeMaxThreshold(spell_type, GetSpellTypeMaxThreshold(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMaxThreshold(i, GetSpellTypeMaxThreshold(i)); + } + } + + break; + case BotSettingCategories::SpellTypeAggroCheck: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeAggroCheck(spell_type, GetSpellTypeAggroCheck(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeAggroCheck(i, GetSpellTypeAggroCheck(i)); + } + } + + break; + case BotSettingCategories::SpellTypeResistLimit: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeResistLimit(spell_type, GetSpellTypeResistLimit(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeResistLimit(i, GetSpellTypeResistLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeMinManaPct: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeMinManaLimit(spell_type, GetSpellTypeMinManaLimit(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMinManaLimit(i, GetSpellTypeMinManaLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeMaxManaPct: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeMaxManaLimit(spell_type, GetSpellTypeMaxManaLimit(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMaxManaLimit(i, GetSpellTypeMaxManaLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeMinHPPct: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeMinHPLimit(spell_type, GetSpellTypeMinHPLimit(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMinHPLimit(i, GetSpellTypeMinHPLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeMaxHPPct: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeMaxHPLimit(spell_type, GetSpellTypeMaxHPLimit(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMaxHPLimit(i, GetSpellTypeMaxHPLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeIdlePriority: + if (spell_type != UINT16_MAX) { + to->SetSpellTypePriority(spell_type, BotPriorityCategories::Idle, GetSpellTypePriority(spell_type, BotPriorityCategories::Idle)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypePriority(i, BotPriorityCategories::Idle, GetSpellTypePriority(i, BotPriorityCategories::Idle)); + } + } + + break; + case BotSettingCategories::SpellTypeEngagedPriority: + if (spell_type != UINT16_MAX) { + to->SetSpellTypePriority(spell_type, BotPriorityCategories::Engaged, GetSpellTypePriority(spell_type, BotPriorityCategories::Engaged)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypePriority(i, BotPriorityCategories::Engaged, GetSpellTypePriority(i, BotPriorityCategories::Engaged)); + } + } + + break; + case BotSettingCategories::SpellTypePursuePriority: + if (spell_type != UINT16_MAX) { + to->SetSpellTypePriority(spell_type, BotPriorityCategories::Pursue, GetSpellTypePriority(spell_type, BotPriorityCategories::Pursue)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypePriority(i, BotPriorityCategories::Pursue, GetSpellTypePriority(i, BotPriorityCategories::Pursue)); + } + } + + break; + case BotSettingCategories::SpellTypeAEOrGroupTargetCount: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeAEOrGroupTargetCount(spell_type, GetSpellTypeAEOrGroupTargetCount(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeAEOrGroupTargetCount(i, GetSpellTypeAEOrGroupTargetCount(i)); + } + } + + break; + case BotSettingCategories::SpellTypeAnnounceCast: + if (spell_type != UINT16_MAX) { + to->SetSpellTypeAnnounceCast(spell_type, GetSpellTypeAnnounceCast(spell_type)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeAnnounceCast(i, GetSpellTypeAnnounceCast(i)); + } + } + + break; + } +} + +void Bot::CopyBotSpellSettings(Bot* to) +{ + if (!to) { + return; + } + + to->ResetBotSpellSettings(); + to->bot_spell_settings.clear(); + + auto s = BotSpellSettingsRepository::GetWhere(content_db, fmt::format("bot_id = {}", GetBotID())); + if (s.empty()) { + return; + } + + auto* spell_list = content_db.GetBotSpells(to->GetBotSpellID()); + + for (const auto& e : s) { + BotSpellSetting b; + + b.priority = e.priority; + b.min_hp = e.min_hp; + b.max_hp = e.max_hp; + b.is_enabled = e.is_enabled; + + if (IsSpellInBotList(spell_list, e.spell_id)) { + for (auto& se : spell_list->entries) { + if (se.spellid == e.spell_id) { + if (EQ::ValueWithin(to->GetLevel(), se.minlevel, se.maxlevel) && se.spellid) { + to->AddBotSpellSetting(e.spell_id, &b); + } + } + } + } + } + + to->LoadBotSpellSettings(); + to->AI_AddBotSpells(to->GetBotSpellID()); + to->SetBotEnforceSpellSetting(GetBotEnforceSpellSetting()); +} + +void Bot::ResetBotSpellSettings() +{ + auto s = BotSpellSettingsRepository::GetWhere(content_db, fmt::format("bot_id = {}", GetBotID())); + if (s.empty()) { + return; + } + + for (const auto& e : s) { + DeleteBotSpellSetting(e.spell_id); + } + + LoadBotSpellSettings(); + AI_AddBotSpells(GetBotSpellID()); + SetBotEnforceSpellSetting(false); +} + +void Bot::CopyBotBlockedBuffs(Bot* to) { + if (!to) { + return; + } + + to->ClearBotBlockedBuffs(); + + std::vector blocked_buffs = GetBotBlockedBuffs(); + + if (!blocked_buffs.empty()) { + for (auto& blocked_buff : blocked_buffs) { + to->SetBotBlockedBuff(blocked_buff.spell_id, blocked_buff.blocked); + } + } +} + +void Bot::CopyBotBlockedPetBuffs(Bot* to) { + if (!to) { + return; + } + + to->ClearBotBlockedBuffs(); + + std::vector blocked_buffs = GetBotBlockedBuffs(); + + if (!blocked_buffs.empty()) { + for (auto& blocked_buff : blocked_buffs) { + to->SetBotBlockedPetBuff(blocked_buff.spell_id, blocked_buff.blocked_pet); + } + } +} + +bool Bot::BotPassiveCheck() { + if (GetBotStance() == Stance::Passive) { + GetOwner()->Message( + Chat::Yellow, + fmt::format( + "{} says, 'I am currently set to stance {} [#{}]. I cannot do commands and my settings cannot be modified.'", + GetCleanName(), + Stance::GetName(Stance::Passive), + Stance::Passive + ).c_str() + ); + + return true; + } + + return false; +} + +bool Bot::IsValidSpellTypeSubType(uint16 spell_type, uint16 sub_type, uint16 spell_id) { + if (sub_type == UINT16_MAX) { + return true; + } + + switch (sub_type) { + case CommandedSubTypes::SingleTarget: + if ( + !IsAnyAESpell(spell_id) && + !IsGroupSpell(spell_id) + ) { + return true; + } + + break; + case CommandedSubTypes::GroupTarget: + if (IsGroupSpell(spell_id)) { + return true; + } + + break; + case CommandedSubTypes::AETarget: + if ( + IsAnyAESpell(spell_id) && + !IsGroupSpell(spell_id) + ) { + return true; + } + + break; + case CommandedSubTypes::SeeInvis: + if (IsEffectInSpell(spell_id, SE_SeeInvis)) { + return true; + } + + break; + case CommandedSubTypes::Invis: + if ( + IsEffectInSpell(spell_id, SE_Invisibility) || + IsEffectInSpell(spell_id, SE_Invisibility2) + ) { + return true; + } + + break; + case CommandedSubTypes::InvisUndead: + if ( + IsEffectInSpell(spell_id, SE_InvisVsUndead) || + IsEffectInSpell(spell_id, SE_InvisVsUndead2) + ) { + return true; + } + + break; + case CommandedSubTypes::InvisAnimals: + if ( + IsEffectInSpell(spell_id, SE_InvisVsAnimals) || + IsEffectInSpell(spell_id, SE_ImprovedInvisAnimals) + ) { + return true; + } + + break; + case CommandedSubTypes::Shrink: + if ( + ( + IsEffectInSpell(spell_id, SE_ModelSize) && + CalcSpellEffectValue(spell_id, GetSpellEffectIndex(spell_id, SE_ModelSize), GetLevel()) < 100 + ) + || + ( + IsEffectInSpell(spell_id, SE_ChangeHeight) && + CalcSpellEffectValue(spell_id, GetSpellEffectIndex(spell_id, SE_ChangeHeight), GetLevel()) < 100 + ) + ) { + return true; + } + + break; + case CommandedSubTypes::Grow: + if ( + ( + IsEffectInSpell(spell_id, SE_ModelSize) && + CalcSpellEffectValue(spell_id, GetSpellEffectIndex(spell_id, SE_ModelSize), GetLevel()) > 100 + ) + || + ( + IsEffectInSpell(spell_id, SE_ChangeHeight) && + CalcSpellEffectValue(spell_id, GetSpellEffectIndex(spell_id, SE_ChangeHeight), GetLevel()) > 100 + ) + ) { + return true; + } + + break; + case CommandedSubTypes::Selo: + if ( + IsBeneficialSpell(spell_id) && + IsEffectInSpell(spell_id, SE_MovementSpeed) && + IsBardSong(spell_id) + ) { + return true; + } + + break; + default: + break; + } + + return false; +} + +uint16 Bot::GetSpellByAA(int id, AA::Rank*& rank) { + uint16 spell_id = 0; + std::pair aa_ability = std::make_pair(nullptr, nullptr); + AA::Ability* ability = zone->GetAlternateAdvancementAbility(id); + + if (!ability || !ability->first_rank_id) { + return spell_id; + } + + uint32 points = GetAA(ability->first_rank_id); + + if (points > 0) { + aa_ability = zone->GetAlternateAdvancementAbilityAndRank(ability->id, points); + } + + rank = aa_ability.second; + + if (!points || !rank) { + return spell_id; + } + + spell_id = rank->spell; + + return spell_id; +} + +void Bot::SetBotBlockedBuff(uint16 spell_id, bool block) +{ + if (!IsValidSpell(spell_id)) { + OwnerMessage("Failed to set blocked buff."); + return; + } + + auto it = std::find_if( + bot_blocked_buffs.begin(), bot_blocked_buffs.end(), + [spell_id](const BotBlockedBuffs& buff) { return buff.spell_id == spell_id; } + ); + + if (it != bot_blocked_buffs.end()) { + it->blocked = block; + } + else { + bot_blocked_buffs.push_back({ GetBotID(), spell_id, block }); + } + + CleanBotBlockedBuffs(); +} + +bool Bot::IsBlockedBuff(int32 spell_id) +{ + bool result = false; + + if (!IsValidSpell(spell_id)) { + OwnerMessage("Failed to get blocked buff."); + + return result; + } + + CleanBotBlockedBuffs(); + + if (!bot_blocked_buffs.empty()) { + for (int i = 0; i < bot_blocked_buffs.size(); i++) { + if (bot_blocked_buffs[i].spell_id != spell_id) { + continue; + } + + return bot_blocked_buffs[i].blocked; + } + } + + return result; +} + +void Bot::SetBotBlockedPetBuff(uint16 spell_id, bool block) +{ + if (!IsValidSpell(spell_id)) { + OwnerMessage("Failed to set blocked pet buff."); + return; + } + + auto it = std::find_if( + bot_blocked_buffs.begin(), bot_blocked_buffs.end(), + [spell_id](const BotBlockedBuffs& buff) { return buff.spell_id == spell_id; } + ); + + if (it != bot_blocked_buffs.end()) { + it->blocked_pet = block; + } + else { + bot_blocked_buffs.push_back({ GetBotID(), spell_id, false, block }); + } + + CleanBotBlockedBuffs(); +} + +bool Bot::IsBlockedPetBuff(int32 spell_id) +{ + bool result = false; + + if (!IsValidSpell(spell_id)) { + OwnerMessage("Failed to get blocked pet buff."); + + return result; + } + + CleanBotBlockedBuffs(); + + if (!bot_blocked_buffs.empty()) { + for (int i = 0; i < bot_blocked_buffs.size(); i++) { + if (bot_blocked_buffs[i].spell_id != spell_id) { + continue; + } + + return bot_blocked_buffs[i].blocked_pet; + } + } + + return result; +} + +void Bot::CleanBotBlockedBuffs() +{ + if (!bot_blocked_buffs.empty()) { + int current = 0; + int end = bot_blocked_buffs.size(); + + while (current < end) { + if (!IsValidSpell(bot_blocked_buffs[current].spell_id) || (bot_blocked_buffs[current].blocked == 0 && bot_blocked_buffs[current].blocked_pet == 0)) { + bot_blocked_buffs.erase(bot_blocked_buffs.begin() + current); + } + else { + current++; + } + + end = bot_blocked_buffs.size(); + } + } +} + +std::vector Bot::BotGetSpellsByType(uint16 spell_type) { + if (AIBot_spells_by_type[spell_type].empty()) { + spell_type = GetParentSpellType(spell_type); + } + + return AIBot_spells_by_type[spell_type]; +} + +void Bot::AssignBotSpellsToTypes(std::vector& AIBot_spells, std::unordered_map>& AIBot_spells_by_type) { + AIBot_spells_by_type.clear(); + + for (size_t i = 0; i < AIBot_spells.size(); ++i) { + const auto& spell = AIBot_spells[i]; + + if (spell.spellid <= 0) { + continue; + } + + BotSpells_wIndex spell_with_index{ + static_cast(i), + spell.type, + spell.spellid, + spell.manacost, + spell.time_cancast, + spell.recast_delay, + spell.priority, + spell.resist_adjust, + spell.minlevel, + spell.maxlevel, + spell.min_hp, + spell.max_hp, + spell.bucket_name, + spell.bucket_value, + spell.bucket_comparison + }; + + AIBot_spells_by_type[spell.type].emplace_back(spell_with_index); + } +} + +std::vector Bot::GatherSpellTargets(bool entire_raid, Mob* target, bool no_clients, bool no_bots) { + std::vector valid_spell_targets; + + auto is_valid_target = [no_clients, no_bots](Mob* member) { + return member && + ((member->IsClient() && !no_clients) || (member->IsBot() && !no_bots)); + }; + + if (IsRaidGrouped()) { + Raid* raid = IsBot() ? CastToBot()->GetStoredRaid() : GetRaid(); + + if (raid) { + if (entire_raid) { + for (const auto& m : raid->members) { + if (is_valid_target(m.member) && m.group_number != RAID_GROUPLESS) { + valid_spell_targets.emplace_back(m.member); + } + } + } + else { + auto group_name = target ? raid->GetGroup(target->GetName()) : raid->GetGroup(GetName()); + auto raid_group = raid->GetRaidGroupMembers(group_name); + + for (const auto& m : raid_group) { + if (is_valid_target(m.member) && m.group_number != RAID_GROUPLESS) { + valid_spell_targets.emplace_back(m.member); + } + } + } + } + } + else if (IsGrouped()) { + Group* group = GetGroup(); + + if (group) { + for (const auto& m : group->members) { + if (is_valid_target(m)) { + valid_spell_targets.emplace_back(m); + } + } + } + } + else { + valid_spell_targets.emplace_back(this); + } + + return valid_spell_targets; +} + +std::vector Bot::GetBuffTargets(Mob* spellTarget) { + if (RuleB(Bots, RaidBuffing)) { + return GetSpellTargetList(true); + } + + return GatherSpellTargets(false, spellTarget); +} + +uint8 Bot::GetHPRatioForSpellType(uint16 spell_type, Mob* tar) { + switch (spell_type) { + case BotSpellTypes::Escape: + case BotSpellTypes::HateRedux: + case BotSpellTypes::InCombatBuff: + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::AELifetap: + case BotSpellTypes::Lifetap: + case BotSpellTypes::OutOfCombatBuffSong: + case BotSpellTypes::Pet: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::PreCombatBuffSong: + return GetHPRatio(); + default: + return tar->GetHPRatio(); + } + + return tar->GetHPRatio(); +} + +uint16 Bot::GetPetBotSpellType(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::VeryFastHeals: + return BotSpellTypes::PetVeryFastHeals; + case BotSpellTypes::FastHeals: + return BotSpellTypes::PetFastHeals; + case BotSpellTypes::RegularHeal: + return BotSpellTypes::PetRegularHeals; + case BotSpellTypes::CompleteHeal: + return BotSpellTypes::PetCompleteHeals; + case BotSpellTypes::HoTHeals: + return BotSpellTypes::PetHoTHeals; + case BotSpellTypes::Buff: + return BotSpellTypes::PetBuffs; + case BotSpellTypes::Cure: + return BotSpellTypes::PetCures; + case BotSpellTypes::DamageShields: + return BotSpellTypes::PetDamageShields; + case BotSpellTypes::ResistBuffs: + return BotSpellTypes::PetResistBuffs; + default: + break; + } + + return spell_type; +} + +uint16 Bot::GetSpellTypeIDByShortName(std::string spell_type_string) { + + for (int i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + if (!Strings::ToLower(spell_type_string).compare(GetSpellTypeShortNameByID(i))) { + return i; + } + } + + for (int i = BotSpellTypes::COMMANDED_START; i <= BotSpellTypes::COMMANDED_END; ++i) { + if (!Strings::ToLower(spell_type_string).compare(GetSpellTypeShortNameByID(i))) { + return i; + } + } + + return UINT16_MAX; +} + +bool Bot::IsValidBotSpellCategory(uint8 setting_type) { + return EQ::ValueWithin(setting_type, BotSettingCategories::START, BotSettingCategories::END); +} + +std::string Bot::GetBotSpellCategoryName(uint8 setting_type) { + return Bot::IsValidBotSpellCategory(setting_type) ? botSpellCategory_names[setting_type] : "UNKNOWN CATEGORY"; +} + +uint16 Bot::GetBotSpellCategoryIDByShortName(std::string setting_string) { + for (int i = BotSettingCategories::START; i <= BotSettingCategories::END; ++i) { + if (!Strings::ToLower(setting_string).compare(Strings::ToLower(GetBotSpellCategoryName(i)))) { + return i; + } + } + + return UINT16_MAX; +} + +bool Bot::IsValidBotBaseSetting(uint16 setting_type) { + return EQ::ValueWithin(setting_type, BotBaseSettings::START_ALL, BotBaseSettings::END); +} + +std::string Bot::GetBotSettingCategoryName(uint16 setting_type) { + return Bot::IsValidBotBaseSetting(setting_type) ? botBaseSettings_names[setting_type] : "UNKNOWN SETTING"; +} + +uint16 Bot::GetBaseSettingIDByShortName(std::string setting_string) { + for (int i = BotSettingCategories::START; i <= BotSettingCategories::END; ++i) { + if (!Strings::ToLower(setting_string).compare(Strings::ToLower(GetBotSettingCategoryName(i)))) { + return i; + } + } + + return UINT16_MAX; +} + +std::string Bot::GetSpellTypeShortNameByID(uint16 spell_type) { + return IsValidBotSpellType(spell_type) ? spellType_shortNames[spell_type] : "UNKNOWN SPELLTYPE"; +} + +std::string Bot::GetSpellTypeNameByID(uint16 spell_type) { + return IsValidBotSpellType(spell_type) ? spellType_names[spell_type] : "UNKNOWN SPELLTYPE"; +} + +bool Bot::IsValidSubType(uint16 sub_type) { + return EQ::ValueWithin(sub_type, CommandedSubTypes::START, CommandedSubTypes::END); +} + +std::string Bot::GetSubTypeNameByID(uint16 sub_type) { + return IsValidBotSpellType(sub_type) ? botSubType_names[sub_type] : "UNKNOWN SUBTYPE"; +} + +uint16 Bot::GetDefaultSpellTypeDelay(uint16 spell_type, uint8 stance) { + switch (spell_type) { + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + return 1500; + case BotSpellTypes::FastHeals: + case BotSpellTypes::PetFastHeals: + return 2500; + case BotSpellTypes::GroupHeals: + case BotSpellTypes::RegularHeal: + case BotSpellTypes::PetRegularHeals: + return 4000; + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::PetCompleteHeals: + return 8000; + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetHoTHeals: + return 22000; + case BotSpellTypes::Cure: + return 2000; + case BotSpellTypes::GroupCures: + return 3000; + case BotSpellTypes::PetCures: + return 5000; + case BotSpellTypes::AEDoT: + case BotSpellTypes::DOT: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + return 100; + case Stance::Aggressive: + return 2000; + case Stance::Efficient: + return 8000; + default: + return 4000; + } + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Nuke: + case BotSpellTypes::AESnare: + case BotSpellTypes::Snare: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + case BotSpellTypes::AESlow: + case BotSpellTypes::Slow: + case BotSpellTypes::AEStun: + case BotSpellTypes::Stun: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + return 100; + case Stance::Aggressive: + return 3000; + case Stance::Efficient: + return 10000; + default: + return 6000; + } + case BotSpellTypes::AERoot: + case BotSpellTypes::Root: + return 8000; + case BotSpellTypes::Fear: + case BotSpellTypes::AEFear: + return 15000; + default: + return 100; + } +} + +uint8 Bot::GetDefaultSpellTypeMinThreshold(uint16 spell_type, uint8 stance) { + switch (spell_type) { + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + case BotSpellTypes::AEDispel: + case BotSpellTypes::Dispel: + case BotSpellTypes::AESlow: + case BotSpellTypes::Slow: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 0; + default: + return 20; + } + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Nuke: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 0; + default: + return 5; + } + case BotSpellTypes::AEDoT: + case BotSpellTypes::DOT: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 0; + case Stance::Efficient: + return 40; + default: + return 25; + } + case BotSpellTypes::Mez: + case BotSpellTypes::AEMez: + return 85; + default: + return 0; + } +} + +uint8 Bot::GetDefaultSpellTypeMaxThreshold(uint16 spell_type, uint8 stance) { + uint8 bot_class = GetClass(); + + switch (spell_type) { + case BotSpellTypes::Escape: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 40; + case Stance::Efficient: + default: + return 25; + } + case BotSpellTypes::AELifetap: + case BotSpellTypes::Lifetap: + case BotSpellTypes::FastHeals: + case BotSpellTypes::PetFastHeals: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 55; + case Stance::Efficient: + return 35; + default: + return 40; + } + case BotSpellTypes::GroupHeals: + case BotSpellTypes::RegularHeal: + if (bot_class == Class::Necromancer || (bot_class == Class::Shaman && !GetSpellTypeHold(BotSpellTypes::InCombatBuff))) { + return 60; + } + + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 70; + case Stance::Efficient: + return 50; + default: + return 60; + } + case BotSpellTypes::PetRegularHeals: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 70; + case Stance::Efficient: + return 50; + default: + return 60; + } + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + if (bot_class == Class::Necromancer || (bot_class == Class::Shaman && !GetSpellTypeHold(BotSpellTypes::InCombatBuff))) { + return 55; + } + + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 90; + case Stance::Efficient: + return 65; + default: + return 80; + } + case BotSpellTypes::PetCompleteHeals: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 90; + case Stance::Efficient: + return 65; + default: + return 80; + } + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::AEStun: + case BotSpellTypes::Nuke: + case BotSpellTypes::AEDoT: + case BotSpellTypes::DOT: + case BotSpellTypes::AERoot: + case BotSpellTypes::Root: + case BotSpellTypes::AESlow: + case BotSpellTypes::Slow: + case BotSpellTypes::AESnare: + case BotSpellTypes::Snare: + case BotSpellTypes::AEFear: + case BotSpellTypes::Fear: + case BotSpellTypes::AEDispel: + case BotSpellTypes::Dispel: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + case BotSpellTypes::Stun: + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + return 100; + case Stance::Aggressive: + return 100; + case Stance::Efficient: + return 90; + default: + return 99; + } + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetHoTHeals: + if (bot_class == Class::Necromancer || (bot_class == Class::Shaman && !GetSpellTypeHold(BotSpellTypes::InCombatBuff))) { + return 70; + } + else { + switch (stance) { + case Stance::AEBurn: + case Stance::Burn: + case Stance::Aggressive: + return 95; + case Stance::Efficient: + return 80; + default: + return 90; + } + } + case BotSpellTypes::Buff: + case BotSpellTypes::Charm: + case BotSpellTypes::Cure: + case BotSpellTypes::GroupCures: + case BotSpellTypes::PetCures: + case BotSpellTypes::DamageShields: + case BotSpellTypes::HateRedux: + case BotSpellTypes::InCombatBuff: + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::Mez: + case BotSpellTypes::AEMez: + case BotSpellTypes::OutOfCombatBuffSong: + case BotSpellTypes::Pet: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::Resurrect: + case BotSpellTypes::HateLine: + case BotSpellTypes::AEHateLine: + default: + return 100; + } +} + +uint16 Bot::GetUltimateSpellTypeDelay(uint16 spell_type, Mob* tar) { + if (!tar) { + return GetSpellTypeDelay(spell_type); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + Mob* owner = tar->GetOwner(); + + return owner->IsClient() ? owner->CastToClient()->GetSpellTypeDelay(GetPetBotSpellType(spell_type)) : owner->CastToBot()->GetSpellTypeDelay( + GetPetBotSpellType(spell_type)); + } + + if (IsBotSpellTypeOtherBeneficial(spell_type) && tar->IsOfClientBot()) { + return tar->IsClient() ? tar->CastToClient()->GetSpellTypeDelay(spell_type) : tar->CastToBot()->GetSpellTypeDelay( + spell_type + ); + } + + return GetSpellTypeDelay(spell_type); +} + +bool Bot::GetUltimateSpellTypeDelayCheck(uint16 spell_type, Mob* tar) { + if (!tar) { + return SpellTypeRecastCheck(spell_type); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + Mob* owner = tar->GetOwner(); + + return owner->IsClient() ? owner->CastToClient()->SpellTypeRecastCheck(GetPetBotSpellType(spell_type)) : owner->CastToBot()->SpellTypeRecastCheck(GetPetBotSpellType(spell_type)); + } + + if (IsBotSpellTypeOtherBeneficial(spell_type) && tar->IsOfClientBot()) { + return tar->IsClient() ? tar->CastToClient()->SpellTypeRecastCheck(spell_type) : tar->CastToBot()->SpellTypeRecastCheck(spell_type); + } + + return SpellTypeRecastCheck(spell_type); +} + +uint8 Bot::GetUltimateSpellTypeMinThreshold(uint16 spell_type, Mob* tar) { + if (!tar) { + return GetSpellTypeMinThreshold(spell_type); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + Mob* owner = tar->GetOwner(); + + return owner->IsClient() ? owner->CastToClient()->GetSpellTypeMinThreshold(GetPetBotSpellType(spell_type)) : owner->CastToBot()->GetSpellTypeMinThreshold( + GetPetBotSpellType(spell_type)); + } + + if (IsBotSpellTypeOtherBeneficial(spell_type) && tar->IsOfClientBot()) { + return tar->IsClient() ? tar->CastToClient()->GetSpellTypeMinThreshold(spell_type) : tar->CastToBot()->GetSpellTypeMinThreshold( + spell_type + ); + } + + return GetSpellTypeMinThreshold(spell_type); +} + +uint8 Bot::GetUltimateSpellTypeMaxThreshold(uint16 spell_type, Mob* tar) { + if (!tar) { + return GetSpellTypeMaxThreshold(spell_type); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + Mob* owner = tar->GetOwner(); + + return owner->IsClient() ? owner->CastToClient()->GetSpellTypeMaxThreshold(GetPetBotSpellType(spell_type)) : owner->CastToBot()->GetSpellTypeMaxThreshold( + GetPetBotSpellType(spell_type)); + } + + if (IsBotSpellTypeOtherBeneficial(spell_type) && tar->IsOfClientBot()) { + return tar->IsClient() ? tar->CastToClient()->GetSpellTypeMaxThreshold(spell_type) : tar->CastToBot()->GetSpellTypeMaxThreshold( + spell_type + ); + } + + return GetSpellTypeMaxThreshold(spell_type); +} + +bool Bot::IsImmuneToBotSpell(uint16 spell_id, Mob* caster) { + int effect_index; + + if (!caster) { + return false; + } + + LogSpells("Checking to see if we are immune to spell [{}] cast by [{}]", spell_id, caster->GetName()); + + if (!IsValidSpell(spell_id)) { + return true; + } + + if (GetSpecialAbility(SpecialAbility::DispellImmunity) && IsDispelSpell(spell_id)) { + return true; + } + + if (GetSpecialAbility(SpecialAbility::PacifyImmunity) && IsHarmonySpell(spell_id)) { + return true; + } + + if (!GetSpecialAbility(SpecialAbility::MesmerizeImmunity) && IsMesmerizeSpell(spell_id)) { + // check max level for spell + effect_index = GetSpellEffectIndex(spell_id, SE_Mez); + assert(effect_index >= 0); + // NPCs get to ignore the max level + if ( + (GetLevel() > spells[spell_id].max_value[effect_index]) && + (!caster->IsNPC() || (caster->IsNPC() && !RuleB(Spells, NPCIgnoreBaseImmunity))) + ) { + return true; + } + } + + // slow and haste spells + if (GetSpecialAbility(SpecialAbility::SlowImmunity) && IsEffectInSpell(spell_id, SE_AttackSpeed)) { + return true; + } + + // client vs client fear + if (!GetSpecialAbility(SpecialAbility::FearImmunity) && IsEffectInSpell(spell_id, SE_Fear)) { + effect_index = GetSpellEffectIndex(spell_id, SE_Fear); + + if (IsClient() && caster->IsClient() && (caster->CastToClient()->GetGM() == false)) { + LogSpells("Clients cannot fear eachother!"); + caster->MessageString(Chat::Red, IMMUNE_FEAR); // need to verify message type, not in MQ2Cast for easy look up + return true; + } + else if (GetLevel() > spells[spell_id].max_value[effect_index] && spells[spell_id].max_value[effect_index] != 0) { + return true; + } + else if (CheckAATimer(aaTimerWarcry)) { + return true; + } + } + + if (!GetSpecialAbility(SpecialAbility::CharmImmunity) && IsCharmSpell(spell_id)) { + + if (this == caster) { + return true; + } + + //let npcs cast whatever charm on anyone + if (!caster->IsNPC()) { + // check level limit of charm spell + effect_index = GetSpellEffectIndex(spell_id, SE_Charm); + assert(effect_index >= 0); + if (GetLevel() > spells[spell_id].max_value[effect_index] && spells[spell_id].max_value[effect_index] != 0) { + return true; + } + } + } + + if ( + GetSpecialAbility(SpecialAbility::SnareImmunity) && + ( + IsEffectInSpell(spell_id, SE_Root) || + IsEffectInSpell(spell_id, SE_MovementSpeed) + ) + ) { + if (GetSpecialAbility(SpecialAbility::SnareImmunity)) { + return true; + } + } + + if (IsLifetapSpell(spell_id)) { + if (this == caster) { + return true; + } + } + + if (IsSacrificeSpell(spell_id)) { + if (this == caster) { + return true; + } + } + + return false; +} + +std::vector Bot::GetSpellTargetList(bool entire_raid) { + if (entire_raid && _spell_target_list.empty()) { + _spell_target_list = GatherSpellTargets(RuleB(Bots, CrossRaidBuffingAndHealing)); + } + else if (!entire_raid && _group_spell_target_list.empty()) { + _group_spell_target_list = GatherSpellTargets(); + } + + return entire_raid ? _spell_target_list : _group_spell_target_list; +} diff --git a/zone/bot.h b/zone/bot.h index 68b027f052..d9efbefb90 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -37,18 +37,24 @@ #include -constexpr uint32 BOT_FOLLOW_DISTANCE_DEFAULT = 184; // as DSq value (~13.565 units) -constexpr uint32 BOT_FOLLOW_DISTANCE_DEFAULT_MAX = 2500; // as DSq value (50 units) - constexpr uint32 BOT_KEEP_ALIVE_INTERVAL = 5000; // 5 seconds +constexpr uint32 BOT_COMBAT_JITTER_INTERVAL_MIN = 1500; // 1.5 seconds +constexpr uint32 BOT_COMBAT_JITTER_INTERVAL_MAX = 3000; // 3 seconds + constexpr uint32 MAG_EPIC_1_0 = 28034; extern WorldServer worldserver; -constexpr int BotAISpellRange = 100; // TODO: Write a method that calcs what the bot's spell range is based on spell, equipment, AA, whatever and replace this constexpr int NegativeItemReuse = -1; // Unlinked timer for items +constexpr uint8 SumWater = 1; +constexpr uint8 SumFire = 2; +constexpr uint8 SumAir = 3; +constexpr uint8 SumEarth = 4; +constexpr uint8 MonsterSum = 5; +constexpr uint8 SumMageMultiElement = 6; + // nHSND negative Healer/Slower/Nuker/Doter // pH positive Healer // pS positive Slower @@ -87,6 +93,141 @@ enum BotCastingChanceConditional : uint8 cntHSND = 16 }; +namespace BotSettingCategories { // Update GetBotSpellCategoryName as needed + constexpr uint8 BaseSetting = 0; + constexpr uint8 SpellHold = 1; + constexpr uint8 SpellDelay = 2; + constexpr uint8 SpellMinThreshold = 3; + constexpr uint8 SpellMaxThreshold = 4; + constexpr uint8 SpellTypeResistLimit = 5; + constexpr uint8 SpellTypeAggroCheck = 6; + constexpr uint8 SpellTypeMinManaPct = 7; + constexpr uint8 SpellTypeMaxManaPct = 8; + constexpr uint8 SpellTypeMinHPPct = 9; + constexpr uint8 SpellTypeMaxHPPct = 10; + constexpr uint8 SpellTypeIdlePriority = 11; + constexpr uint8 SpellTypeEngagedPriority = 12; + constexpr uint8 SpellTypePursuePriority = 13; + constexpr uint8 SpellTypeAEOrGroupTargetCount = 14; + constexpr uint8 SpellTypeAnnounceCast = 15; + + constexpr uint16 START = BotSettingCategories::BaseSetting; + constexpr uint16 START_NO_BASE = BotSettingCategories::SpellHold; + constexpr uint16 START_CLIENT = BotSettingCategories::SpellDelay; + constexpr uint16 END_CLIENT = BotSettingCategories::SpellMaxThreshold; + constexpr uint16 END = BotSettingCategories::SpellTypeAnnounceCast; +}; + +static std::map botSpellCategory_names = { + { BotSettingCategories::BaseSetting, "BaseSetting" }, + { BotSettingCategories::SpellHold, "SpellHolds" }, + { BotSettingCategories::SpellDelay, "SpellDelays" }, + { BotSettingCategories::SpellMinThreshold, "SpellMinThresholds" }, + { BotSettingCategories::SpellMaxThreshold, "SpellMaxThresholds" }, + { BotSettingCategories::SpellTypeResistLimit, "SpellResistLimit" }, + { BotSettingCategories::SpellTypeAggroCheck, "SpellAggroChecks" }, + { BotSettingCategories::SpellTypeMinManaPct, "SpellMinManaPct" }, + { BotSettingCategories::SpellTypeMaxManaPct, "SpellMaxManaPct" }, + { BotSettingCategories::SpellTypeMinHPPct, "SpellMinHPPct" }, + { BotSettingCategories::SpellTypeMaxHPPct, "SpellMaxHPPct" }, + { BotSettingCategories::SpellTypeIdlePriority, "SpellIdlePriority" }, + { BotSettingCategories::SpellTypeEngagedPriority, "SpellEngagedPriority" }, + { BotSettingCategories::SpellTypePursuePriority, "SpellPursuePriority" }, + { BotSettingCategories::SpellTypeAEOrGroupTargetCount, "SpellTargetCounts" }, + { BotSettingCategories::SpellTypeAnnounceCast, "SpellAnnounceCasts" } +}; + +namespace BotPriorityCategories { // Update GetBotSpellCategoryName as needed + constexpr uint8 Idle = 0; + constexpr uint8 Engaged = 1; + constexpr uint8 Pursue = 2; + + constexpr uint16 START = BotPriorityCategories::Idle; + constexpr uint16 END = BotPriorityCategories::Pursue; // Increment as needed +}; + +namespace BotBaseSettings { + constexpr uint16 ExpansionBitmask = 0; + constexpr uint16 ShowHelm = 1; + constexpr uint16 FollowDistance = 2; + constexpr uint16 StopMeleeLevel = 3; + constexpr uint16 EnforceSpellSettings = 4; + constexpr uint16 RangedSetting = 5; + constexpr uint16 PetSetTypeSetting = 6; + constexpr uint16 BehindMob = 7; + constexpr uint16 DistanceRanged = 8; + constexpr uint16 IllusionBlock = 9; + constexpr uint16 MaxMeleeRange = 10; + constexpr uint16 MedInCombat = 11; + constexpr uint16 SitHPPct = 12; + constexpr uint16 SitManaPct = 13; + + constexpr uint16 START_ALL = ExpansionBitmask; + constexpr uint16 START = BotBaseSettings::ShowHelm; // Everything above this cannot be copied, changed or viewed by players + constexpr uint16 END = BotBaseSettings::SitManaPct; // Increment as needed +}; + +static std::map botBaseSettings_names = { + { BotBaseSettings::ExpansionBitmask, "ExpansionBitmask" }, + { BotBaseSettings::ShowHelm, "ShowHelm" }, + { BotBaseSettings::FollowDistance, "FollowDistance" }, + { BotBaseSettings::StopMeleeLevel, "StopMeleeLevel" }, + { BotBaseSettings::EnforceSpellSettings, "EnforceSpellSettings" }, + { BotBaseSettings::RangedSetting, "RangedSetting" }, + { BotBaseSettings::PetSetTypeSetting, "PetSetTypeSetting" }, + { BotBaseSettings::BehindMob, "BehindMob" }, + { BotBaseSettings::DistanceRanged, "DistanceRanged" }, + { BotBaseSettings::IllusionBlock, "IllusionBlock" }, + { BotBaseSettings::MaxMeleeRange, "MaxMeleeRange" }, + { BotBaseSettings::MedInCombat, "MedInCombat" }, + { BotBaseSettings::SitHPPct, "SitHPPct" }, + { BotBaseSettings::SitManaPct, "SitManaPct" } +}; + +namespace CommandedSubTypes { + constexpr uint16 SingleTarget = 1; + constexpr uint16 GroupTarget = 2; + constexpr uint16 AETarget = 3; + constexpr uint16 SeeInvis = 4; + constexpr uint16 Invis = 5; + constexpr uint16 InvisUndead = 6; + constexpr uint16 InvisAnimals = 7; + constexpr uint16 Shrink = 8; + constexpr uint16 Grow = 9; + constexpr uint16 Selo = 10; + + constexpr uint16 START = CommandedSubTypes::SingleTarget; + constexpr uint16 END = CommandedSubTypes::Selo; +}; + +static std::map botSubType_names = { + { CommandedSubTypes::SingleTarget, "SingleTarget" }, + { CommandedSubTypes::GroupTarget, "GroupTarget" }, + { CommandedSubTypes::AETarget, "AETarget" }, + { CommandedSubTypes::SeeInvis, "SeeInvis" }, + { CommandedSubTypes::Invis, "Invis" }, + { CommandedSubTypes::InvisUndead, "InvisUndead" }, + { CommandedSubTypes::InvisAnimals, "InvisAnimals" }, + { CommandedSubTypes::Shrink, "Shrink" }, + { CommandedSubTypes::Grow, "Grow" }, + { CommandedSubTypes::Selo, "Selo" } +}; + +struct CombatRangeInput { + Mob* target; + float target_distance; + bool behind_mob; + uint8 stop_melee_level; + const EQ::ItemInstance* p_item; + const EQ::ItemInstance* s_item; +}; + +struct CombatRangeOutput { + bool at_combat_range = false; + float melee_distance_min = 0.0f; + float melee_distance = 0.0f; + float melee_distance_max = 0.0f; +}; class Bot : public NPC { friend class Mob; @@ -128,7 +269,7 @@ class Bot : public NPC { // Class Constructors Bot(NPCType *npcTypeData, Client* botOwner); - Bot(uint32 botID, uint32 botOwnerCharacterID, uint32 botSpellsID, double totalPlayTime, uint32 lastZoneId, NPCType *npcTypeData, int32 expansion_bitmask); + Bot(uint32 botID, uint32 botOwnerCharacterID, uint32 botSpellsID, double totalPlayTime, uint32 lastZoneId, NPCType *npcTypeData); //abstract virtual override function implementations requird by base abstract class bool Death(Mob* killer_mob, int64 damage, uint16 spell_id, EQ::skills::SkillType attack_skill, KilledByTypes killed_by = KilledByTypes::Killed_NPC, bool is_buff_tic = false) override; @@ -157,13 +298,14 @@ class Bot : public NPC { void SetLevel(uint8 in_level, bool command = false) override; void FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) override; bool Process() override; - void FinishTrade(Client* client, BotTradeType trade_type); + void FinishTrade(Client* client, BotTradeType trade_type, int16 chosen_slot = INVALID_INDEX); bool Save() override; void Depop(); void CalcBotStats(bool showtext = true); uint16 BotGetSpells(int spellslot) { return AIBot_spells[spellslot].spellid; } uint32 BotGetSpellType(int spellslot) { return AIBot_spells[spellslot].type; } uint16 BotGetSpellPriority(int spellslot) { return AIBot_spells[spellslot].priority; } + std::vector BotGetSpellsByType(uint16 spell_type); float GetProcChances(float ProcBonus, uint16 hand) override; int GetHandToHandDamage(void) override; bool TryFinishingBlow(Mob *defender, int64 &damage) override; @@ -177,9 +319,6 @@ class Bot : public NPC { inline uint16 MaxSkill(EQ::skills::SkillType skillid) { return MaxSkill(skillid, GetClass(), GetLevel()); } int GetBaseSkillDamage(EQ::skills::SkillType skill, Mob *target = nullptr) override; void DoSpecialAttackDamage(Mob *who, EQ::skills::SkillType skill, int32 max_damage, int32 min_damage = 1, int32 hate_override = -1, int ReuseTime = 10, bool HitChance = false); - void TryBackstab(Mob *other,int ReuseTime = 10) override; - void RogueBackstab(Mob* other, bool min_damage = false, int ReuseTime = 10) override; - void RogueAssassinate(Mob* other) override; void DoClassAttacks(Mob *target, bool IsRiposte=false); void CalcBonuses() override; @@ -199,8 +338,8 @@ class Bot : public NPC { void Camp(bool save_to_database = true); void SetTarget(Mob* mob) override; void Zone(); - bool IsArcheryRange(Mob* target); - void ChangeBotArcherWeapons(bool isArcher); + bool IsAtRange(Mob* target); + void ChangeBotRangedWeapons(bool is_ranged); void Sit(); void Stand(); bool IsSitting() const override; @@ -216,6 +355,7 @@ class Bot : public NPC { void SetHoldFlag(bool flag = true) { m_hold_flag = flag; } bool GetAttackFlag() const { return m_attack_flag; } void SetAttackFlag(bool flag = true) { m_attack_flag = flag; } + bool GetCombatRoundForAlerts() const { return m_combat_round_alert_flag; } bool GetAttackingFlag() const { return m_attacking_flag; } bool GetPullFlag() const { return m_pull_flag; } void SetPullFlag(bool flag = true) { m_pull_flag = flag; } @@ -224,11 +364,10 @@ class Bot : public NPC { bool GetIsUsingItemClick() { return is_using_item_click; } void SetIsUsingItemClick(bool flag = true) { is_using_item_click = flag; } bool UseDiscipline(uint32 spell_id, uint32 target); - uint8 GetNumberNeedingHealedInGroup(uint8 hpr, bool includePets, Raid* raid); - uint8 GetNumberNeedingHealedInRaidGroup(uint8& need_healed, uint8 hpr, bool includePets, Raid* raid); + uint8 GetNumberNeedingHealedInGroup(Mob* tar, uint16 spell_type, uint16 spell_id, float range); bool GetNeedsCured(Mob *tar); bool GetNeedsHateRedux(Mob *tar); - bool HasOrMayGetAggro(); + bool HasOrMayGetAggro(bool SitAggro, uint32 spell_id = 0); void SetDefaultBotStance(); void SetSurname(std::string_view bot_surname); void SetTitle(std::string_view bot_title); @@ -327,19 +466,26 @@ class Bot : public NPC { void AI_Bot_Event_SpellCastFinished(bool iCastSucceeded, uint16 slot); // AI Methods - bool AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes); + bool AICastSpell(Mob* tar, uint8 chance, uint16 spell_type, uint16 sub_target_type = UINT16_MAX, uint16 sub_type = UINT16_MAX); + bool AttemptAICastSpell(uint16 spell_type, Mob* tar = nullptr); + bool AttemptAACastSpell(Mob* tar, uint16 spell_id, AA::Rank* rank); + bool AttemptForcedCastSpell(Mob* tar, uint16 spell_id, bool is_disc = false); + bool AttemptCloseBeneficialSpells(uint16 spell_type); bool AI_EngagedCastCheck() override; bool AI_PursueCastCheck() override; bool AI_IdleCastCheck() override; bool AIHealRotation(Mob* tar, bool useFastHeals); bool GetPauseAI() const { return _pauseAI; } void SetPauseAI(bool pause_flag) { _pauseAI = pause_flag; } - uint8 GetStopMeleeLevel() const { return _stopMeleeLevel; } - void SetStopMeleeLevel(uint8 level); + bool IsCommandedSpell() const { return _commandedSpell; } + void SetCommandedSpell(bool value) { _commandedSpell = value; } + bool IsPullingSpell() const { return _pullingSpell; } + void SetPullingSpell(bool value) { _pullingSpell = value; } + void SetGuardMode(); void SetHoldMode(); - uint32 GetBotCasterRange() const { return m_bot_caster_range; } - bool IsValidSpellRange(uint16 spell_id, Mob const* tar); + + bool IsValidSpellRange(uint16 spell_id, Mob* tar); // Bot AI Methods void AI_Bot_Init(); @@ -377,6 +523,169 @@ class Bot : public NPC { inline bool Attack(Mob* other, int Hand = EQ::invslot::slotPrimary, bool FromRiposte = false, bool IsStrikethrough = false, bool IsFromSpell = false, ExtraAttackOptions *opts = nullptr) override { return Mob::Attack(other, Hand, FromRiposte, IsStrikethrough, IsFromSpell, opts); } + void DoAttackRounds(Mob* target, int hand); + + bool BotPassiveCheck(); + Raid* GetStoredRaid() { return _storedRaid; } + void SetStoredRaid(Raid* stored_raid) { _storedRaid = stored_raid; } + bool GetVerifiedRaid() { return _verifiedRaid; } + void SetVerifiedRaid(bool status) { _verifiedRaid = status; } + uint16 GetTempSpellType() { return _tempSpellType; } + void SetTempSpellType(uint16 spell_type) { _tempSpellType = spell_type; } + bool IsMobEngagedByAnyone(Mob* tar); + void SetBotTimers(std::vector timers) { bot_timers = timers; } + + // Targeting + std::vector GatherSpellTargets(bool entireRaid = false, Mob* target = nullptr, bool no_clients = false, bool no_bots = false); + bool HasValidAETarget(Bot* caster, uint16 spell_id, uint16 spell_type, Mob* tar); + void SetHasLoS(bool has_los) { _hasLoS = has_los; } + bool HasLoS() const { return _hasLoS; } + bool IsValidMezTarget(Mob* owner, Mob* npc, uint16 spell_id); + + // Cast checks + bool PrecastChecks(Mob* tar, uint16 spell_type); + bool CastChecks(uint16 spell_id, Mob* tar, uint16 spell_type, bool prechecks = false, bool ae_check = false); + bool IsImmuneToBotSpell(uint16 spell_id, Mob* caster); + bool CanCastSpellType(uint16 spell_type, uint16 spell_id, Mob* tar); + bool BotHasEnoughMana(uint16 spell_id); + bool IsTargetAlreadyReceivingSpell(Mob* tar, uint16 spell_id); + bool DoResistCheck(Mob* target, uint16 spell_id, int32 resist_limit); + bool DoResistCheckBySpellType(Mob* tar, uint16 spell_id, uint16 spell_type); + bool IsValidTargetType(uint16 spell_id, int target_type, uint8 body_type); + + // Spell checks + static bool IsValidBotSpellType(uint16 spell_type); + uint16 GetPetBotSpellType(uint16 spell_type); + + // Movement checks + bool PlotBotPositionAroundTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float min_distance, float max_distance, bool behind_only = false, bool front_only = false, bool bypass_los = false); + std::vector GetSpellTargetList(bool entire_raid = false); + void SetSpellTargetList(std::vector spell_target_list) { _spell_target_list = spell_target_list; } + std::vector GetGroupSpellTargetList() { return _group_spell_target_list; } + void SetGroupSpellTargetList(std::vector spell_target_list) { _group_spell_target_list = spell_target_list; } + std::vector GetBuffTargets(Mob* spellTarget); + + // Bot settings + void LoadDefaultBotSettings(); + int GetDefaultSetting(uint16 setting_category, uint16 setting_type, uint8 stance = Stance::Balanced); + int GetDefaultBotBaseSetting(uint16 bot_setting, uint8 stance = Stance::Balanced); + bool GetDefaultSpellTypeHold(uint16 spell_type, uint8 stance = Stance::Balanced); + uint16 GetDefaultSpellTypePriority(uint16 spell_type, uint8 priority_type, uint8 bot_class, uint8 stance = Stance::Balanced); + uint16 GetDefaultSpellTypeIdlePriority(uint16 spell_type, uint8 bot_class, uint8 stance = Stance::Balanced); + uint16 GetDefaultSpellTypeEngagedPriority(uint16 spell_type, uint8 bot_class, uint8 stance = Stance::Balanced); + uint16 GetDefaultSpellTypePursuePriority(uint16 spell_type, uint8 bot_class, uint8 stance = Stance::Balanced); + uint16 GetDefaultSpellTypeResistLimit(uint16 spell_type, uint8 stance = Stance::Balanced); + bool GetDefaultSpellTypeAggroCheck(uint16 spell_type, uint8 stance = Stance::Balanced); + uint8 GetDefaultSpellTypeMinManaLimit(uint16 spell_type, uint8 stance = Stance::Balanced); + uint8 GetDefaultSpellTypeMaxManaLimit(uint16 spell_type, uint8 stance = Stance::Balanced); + uint8 GetDefaultSpellTypeMinHPLimit(uint16 spell_type, uint8 stance = Stance::Balanced); + uint8 GetDefaultSpellTypeMaxHPLimit(uint16 spell_type, uint8 stance = Stance::Balanced); + uint16 GetDefaultSpellTypeAnnounceCast(uint16 spell_type, uint8 stance = Stance::Balanced); + uint16 GetDefaultSpellTypeAEOrGroupTargetCount(uint16 spell_type, uint8 stance = Stance::Balanced); + + static bool IsValidBotBaseSetting(uint16 setting_type); + static std::string GetBotSettingCategoryName(uint16 setting_type); + uint16 GetBaseSettingIDByShortName(std::string setting_string); + int GetBotBaseSetting(uint16 bot_setting); + void SetBotBaseSetting(uint16 bot_setting, int setting_value); + int GetSetting(uint16 setting_category, uint16 setting_type); + void SetBotSetting(uint8 setting_type, uint16 bot_setting, int setting_value); + void CopySettings(Bot* to, uint8 setting_type, uint16 spell_type = UINT16_MAX); + void CopyBotSpellSettings(Bot* to); + void ResetBotSpellSettings(); + + void CopyBotBlockedBuffs(Bot* to); + void CopyBotBlockedPetBuffs(Bot* to); + void CleanBotBlockedBuffs(); + void ClearBotBlockedBuffs() { bot_blocked_buffs.clear(); } + bool IsBlockedBuff(int32 spell_id) override; + bool IsBlockedPetBuff(int32 spell_id) override; + void SetBotBlockedBuff(uint16 spell_id, bool block); + void SetBotBlockedPetBuff(uint16 spell_id, bool block); + std::vector GetBotBlockedBuffs() { return bot_blocked_buffs; } + void SetBotBlockedBuffs(std::vector blocked_buffs) { bot_blocked_buffs = blocked_buffs; } + + void SetBotSpellRecastTimer(uint16 spell_type, Mob* spelltar, bool pre_cast = false); + uint16 GetSpellTypePriority(uint16 spell_type, uint8 priority_type); + void SetSpellTypePriority(uint16 spell_type, uint8 priority_type, uint16 priority); + inline uint16 GetSpellTypeResistLimit(uint16 spell_type) const { return m_bot_spell_settings[spell_type].resist_limit; } + inline void SetSpellTypeResistLimit(uint16 spell_type, uint16 value) { m_bot_spell_settings[spell_type].resist_limit = value; } + inline bool GetSpellTypeAggroCheck(uint16 spell_type) const { return m_bot_spell_settings[spell_type].aggro_check; } + inline void SetSpellTypeAggroCheck(uint16 spell_type, bool value) { m_bot_spell_settings[spell_type].aggro_check = value; } + uint8 GetHPRatioForSpellType(uint16 spell_type, Mob* tar); + inline uint8 GetSpellTypeMinManaLimit(uint16 spell_type) const { return m_bot_spell_settings[spell_type].min_mana_pct; } + inline uint8 GetSpellTypeMaxManaLimit(uint16 spell_type) const { return m_bot_spell_settings[spell_type].max_mana_pct; } + inline void SetSpellTypeMinManaLimit(uint16 spell_type, uint8 value) { m_bot_spell_settings[spell_type].min_mana_pct = value; } + inline void SetSpellTypeMaxManaLimit(uint16 spell_type, uint8 value) { m_bot_spell_settings[spell_type].max_mana_pct = value; } + inline uint8 GetSpellTypeMinHPLimit(uint16 spell_type) const { return m_bot_spell_settings[spell_type].min_hp_pct; } + inline uint8 GetSpellTypeMaxHPLimit(uint16 spell_type) const { return m_bot_spell_settings[spell_type].max_hp_pct; } + inline void SetSpellTypeMinHPLimit(uint16 spell_type, uint8 value) { m_bot_spell_settings[spell_type].min_hp_pct = value; } + inline void SetSpellTypeMaxHPLimit(uint16 spell_type, uint8 value) { m_bot_spell_settings[spell_type].max_hp_pct = value; } + inline uint16 GetSpellTypeAEOrGroupTargetCount(uint16 spell_type) const { return m_bot_spell_settings[spell_type].ae_or_group_target_count; } + inline void SetSpellTypeAEOrGroupTargetCount(uint16 spell_type, uint16 value) { m_bot_spell_settings[spell_type].ae_or_group_target_count = value; } + inline uint16 GetSpellTypeAnnounceCast(uint16 spell_type) const { return m_bot_spell_settings[spell_type].announce_cast; } + inline void SetSpellTypeAnnounceCast(uint16 spell_type, uint16 value) { m_bot_spell_settings[spell_type].announce_cast = value; } + inline bool GetSpellTypeHold(uint16 spell_type) const { return m_bot_spell_settings[spell_type].hold; } + inline void SetSpellTypeHold(uint16 spell_type, bool value) { m_bot_spell_settings[spell_type].hold = value; } + inline uint16 GetSpellTypeDelay(uint16 spell_type) const { return m_bot_spell_settings[spell_type].delay; } + inline void SetSpellTypeDelay(uint16 spell_type, uint16 delay_value) { m_bot_spell_settings[spell_type].delay = delay_value; } + inline uint8 GetSpellTypeMinThreshold(uint16 spell_type) const { return m_bot_spell_settings[spell_type].min_threshold; } + inline void SetSpellTypeMinThreshold(uint16 spell_type, uint8 threshold_value) { m_bot_spell_settings[spell_type].min_threshold = threshold_value; } + inline uint8 GetSpellTypeMaxThreshold(uint16 spell_type) const { return m_bot_spell_settings[spell_type].max_threshold; } + inline void SetSpellTypeMaxThreshold(uint16 spell_type, uint8 threshold_value) { m_bot_spell_settings[spell_type].max_threshold = threshold_value; } + inline bool SpellTypeRecastCheck(uint16 spellType) { return !m_bot_spell_settings[spellType].recast_timer.GetRemainingTime(); } + void SetSpellTypeRecastTimer(uint16 spell_type, uint32 recast_time) { m_bot_spell_settings[spell_type].recast_timer.Start(recast_time); } + inline bool SpellTypeAIDelayCheck(uint16 spellType) { return !m_bot_spell_settings[spellType].ai_delay.GetRemainingTime(); } + void SetSpellTypeAITimer(uint16 spell_type, uint32 recast_time) { m_bot_spell_settings[spell_type].ai_delay.Start(recast_time); } + bool GetUltimateSpellTypeHold(uint16 spell_type, Mob* tar); + uint16 GetDefaultSpellTypeDelay(uint16 spell_type, uint8 stance = Stance::Balanced); + uint8 GetDefaultSpellTypeMinThreshold(uint16 spell_type, uint8 stance = Stance::Balanced); + uint8 GetDefaultSpellTypeMaxThreshold(uint16 spell_type, uint8 stance = Stance::Balanced); + uint16 GetUltimateSpellTypeDelay(uint16 spell_type, Mob* tar); + bool GetUltimateSpellTypeDelayCheck(uint16 spell_type, Mob* tar); + uint8 GetUltimateSpellTypeMinThreshold(uint16 spell_type, Mob* tar); + uint8 GetUltimateSpellTypeMaxThreshold(uint16 spell_type, Mob* tar); + void SetIllusionBlock(bool value) { _illusionBlock = value; } + bool GetIllusionBlock() const override { return _illusionBlock; } + bool GetShowHelm() const { return _showHelm; } + void SetShowHelm(bool show_helm) { _showHelm = show_helm; } + bool GetBehindMob() const { return _behindMobStatus; } + void SetBehindMob(bool value) { _behindMobStatus = value; } + bool GetMaxMeleeRange() const { return _maxMeleeRangeStatus; } + void SetMaxMeleeRange(bool value) { _maxMeleeRangeStatus = value; } + uint8 GetStopMeleeLevel() const { return _stopMeleeLevel; } + void SetStopMeleeLevel(uint8 level) { _stopMeleeLevel = level; } + uint32 GetBotDistanceRanged() const { return _distanceRanged; } + void SetBotDistanceRanged(uint32 distance) { _distanceRanged = distance; } + bool GetMedInCombat() const { return _medInCombat; } + void SetMedInCombat(bool value) { _medInCombat = value; } + uint8 GetSitHPPct() const { return _SitHPPct; } + void SetSitHPPct(uint8 value) { _SitHPPct = value; } + uint8 GetSitManaPct() const { return _SitManaPct; } + void SetSitManaPct(uint8 value) { _SitManaPct = value; } + + // Spell lists + void CheckBotSpells(); + void MapSpellTypeLevels(); + const std::map>& GetCommandedSpellTypesMinLevels() { return commanded_spells_min_level; } + std::list GetSpellTypesPrioritized(uint8 priority_type); + uint16 GetParentSpellType(uint16 spell_type); + bool IsValidSpellTypeBySpellID(uint16 spell_type, uint16 spell_id); + inline uint16 GetCastedSpellType() const { return _castedSpellType; } + void SetCastedSpellType(uint16 spell_type); + bool IsValidSpellTypeSubType(uint16 spell_type, uint16 sub_type, uint16 spell_id); + static bool IsValidBotSpellCategory(uint8 setting_type); + static std::string GetBotSpellCategoryName(uint8 setting_type); + static uint16 GetBotSpellCategoryIDByShortName(std::string setting_string); + void AssignBotSpellsToTypes(std::vector& AIBot_spells, std::unordered_map>& AIBot_spells_by_type); + uint16 GetSpellByAA(int id, AA::Rank*& rank); + + // Spell Type + static uint16 GetSpellTypeIDByShortName(std::string spellType_string); + static std::string GetSpellTypeNameByID(uint16 spell_type); + static std::string GetSpellTypeShortNameByID(uint16 spell_type); + bool IsValidSubType(uint16 sub_type); + static std::string GetSubTypeNameByID(uint16 sub_type); [[nodiscard]] int GetMaxBuffSlots() const final { return EQ::spells::LONG_BUFFS; } [[nodiscard]] int GetMaxSongSlots() const final { return EQ::spells::SHORT_BUFFS; } @@ -423,33 +732,36 @@ class Bot : public NPC { ProcessBotGroupAdd(Group* group, Raid* raid, Client* client = nullptr, bool new_raid = false, bool initial = false); - static std::list GetBotSpellsForSpellEffect(Bot* botCaster, int spellEffect); - static std::list GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, int spellEffect, SpellTargetType targetType); - static std::list GetBotSpellsBySpellType(Bot* botCaster, uint32 spellType); - static std::list GetPrioritizedBotSpellsBySpellType(Bot* botCaster, uint32 spellType); - - static BotSpell GetFirstBotSpellBySpellType(Bot* botCaster, uint32 spellType); - static BotSpell GetBestBotSpellForFastHeal(Bot* botCaster); - static BotSpell GetBestBotSpellForHealOverTime(Bot* botCaster); - static BotSpell GetBestBotSpellForPercentageHeal(Bot* botCaster); - static BotSpell GetBestBotSpellForRegularSingleTargetHeal(Bot* botCaster); - static BotSpell GetFirstBotSpellForSingleTargetHeal(Bot* botCaster); - static BotSpell GetBestBotSpellForGroupHealOverTime(Bot* botCaster); - static BotSpell GetBestBotSpellForGroupCompleteHeal(Bot* botCaster); - static BotSpell GetBestBotSpellForGroupHeal(Bot* botCaster); - static BotSpell GetBestBotSpellForMagicBasedSlow(Bot* botCaster); - static BotSpell GetBestBotSpellForDiseaseBasedSlow(Bot* botCaster); - - static Mob* GetFirstIncomingMobToMez(Bot* botCaster, BotSpell botSpell); - static BotSpell GetBestBotSpellForMez(Bot* botCaster); - static BotSpell GetBestBotMagicianPetSpell(Bot* botCaster); - static std::string GetBotMagicianPetType(Bot* botCaster); - static BotSpell GetBestBotSpellForNukeByTargetType(Bot* botCaster, SpellTargetType targetType); - static BotSpell GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType targetType); - static BotSpell GetBestBotWizardNukeSpellByTargetResists(Bot* botCaster, Mob* target); - static BotSpell GetDebuffBotSpell(Bot* botCaster, Mob* target); - static BotSpell GetBestBotSpellForCure(Bot* botCaster, Mob* target); - static BotSpell GetBestBotSpellForResistDebuff(Bot* botCaster, Mob* target); + static std::list GetBotSpellsForSpellEffect(Bot* caster, uint16 spell_type, int spell_effect); + static std::list GetBotSpellsForSpellEffectAndTargetType(Bot* caster, uint16 spell_type, int spell_effect, SpellTargetType target_type); + static std::list GetBotSpellsBySpellType(Bot* caster, uint16 spell_type); + static std::vector GetPrioritizedBotSpellsBySpellType(Bot* caster, uint16 spell_type, Mob* tar, bool AE = false, uint16 sub_target_type = UINT16_MAX, uint16 sub_type = UINT16_MAX); + + static BotSpell GetFirstBotSpellBySpellType(Bot* caster, uint16 spell_type); + BotSpell GetSpellByHealType(uint16 spell_type, Mob* tar); + static BotSpell GetBestBotSpellForVeryFastHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForFastHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForHealOverTime(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForPercentageHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForRegularSingleTargetHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + static BotSpell GetFirstBotSpellForSingleTargetHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForGroupHealOverTime(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForGroupCompleteHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForGroupHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); + + static Mob* GetFirstIncomingMobToMez(Bot* caster, int16 spell_id, uint16 spell_type, bool AE); + static BotSpell GetBestBotSpellForMez(Bot* caster, uint16 spell_type = BotSpellTypes::Mez); + static BotSpell GetBestBotMagicianPetSpell(Bot* caster, uint16 spell_type = BotSpellTypes::Pet); + static std::string GetBotMagicianPetType(Bot* caster); + static BotSpell GetBestBotSpellForNukeByTargetType(Bot* caster, SpellTargetType target_type, uint16 spell_type, bool AE = false, Mob* tar = nullptr); + static BotSpell GetBestBotSpellForStunByTargetType(Bot* caster, SpellTargetType target_type, uint16 spell_type, bool AE = false, Mob* tar = nullptr); + static BotSpell GetBestBotWizardNukeSpellByTargetResists(Bot* caster, Mob* target, uint16 spell_type); + static BotSpell GetDebuffBotSpell(Bot* caster, Mob* target, uint16 spell_type); + static BotSpell GetBestBotSpellForCure(Bot* caster, Mob* target, uint16 spell_type); + static BotSpell GetBestBotSpellForResistDebuff(Bot* caster, Mob* target, uint16 spell_type); + static BotSpell GetBestBotSpellForNukeByBodyType(Bot* caster, uint8 body_type, uint16 spell_type, bool AE = false, Mob* tar = nullptr); + static BotSpell GetBestBotSpellForRez(Bot* caster, Mob* target, uint16 spell_type); + static BotSpell GetBestBotSpellForCharm(Bot* caster, Mob* target, uint16 spell_type); static NPCType *CreateDefaultNPCTypeStructForBot( const std::string& botName, @@ -465,19 +777,18 @@ class Bot : public NPC { // Static Bot Group Methods static bool AddBotToGroup(Bot* bot, Group* group); static bool RemoveBotFromGroup(Bot* bot, Group* group); - static void BotGroupSay(Mob *speaker, const char *msg, ...); + static void RaidGroupSay(Mob *speaker, const char *msg, ...); // "GET" Class Methods uint32 GetBotID() const { return _botID; } uint32 GetBotOwnerCharacterID() const { return _botOwnerCharacterID; } uint32 GetBotSpellID() const { return npc_spells_id; } Mob* GetBotOwner() { return this->_botOwner; } - uint32 GetBotArcheryRange(); + uint32 GetBotRangedValue(); EQ::ItemInstance* GetBotItem(uint16 slot_id); bool GetSpawnStatus() { return _spawnStatus; } uint8 GetPetChooserID() { return _petChooserID; } - bool IsPetChooser() { return _petChooser; } - bool IsBotArcher() { return m_bot_archery_setting; } + bool IsBotRanged() { return _botRangedSetting; } bool IsBotCharmer() { return _botCharmer; } bool IsBot() const override { return true; } bool IsOfClientBot() const override { return true; } @@ -485,9 +796,7 @@ class Bot : public NPC { bool GetRangerAutoWeaponSelect() { return _rangerAutoWeaponSelect; } uint8 GetBotStance() { return _botStance; } - uint8 GetChanceToCastBySpellType(uint32 spellType); - bool GetBotEnforceSpellSetting() { return m_enforce_spell_settings; } - float GetBotCasterMaxRange(float melee_distance_max); + uint8 GetChanceToCastBySpellType(uint16 spell_type); bool IsGroupHealer() const { return m_CastingRoles.GroupHealer; } bool IsGroupSlower() const { return m_CastingRoles.GroupSlower; } bool IsGroupNuker() const { return m_CastingRoles.GroupNuker; } @@ -527,8 +836,6 @@ class Bot : public NPC { std::shared_ptr* MemberOfHealRotation() { return &m_member_of_heal_rotation; } - bool GetAltOutOfCombatBehavior() const { return _altoutofcombatbehavior;} - bool GetShowHelm() const { return _showhelm; } inline int32 GetSTR() const override { return STR; } inline int32 GetSTA() const override { return STA; } inline int32 GetDEX() const override { return DEX; } @@ -584,7 +891,8 @@ class Bot : public NPC { inline const InspectMessage_Struct& GetInspectMessage() const { return _botInspectMessage; } // "Quest API" Methods - bool HasBotSpellEntry(uint16 spellid); + bool HasBotSpellEntry(uint16 spell_id); + bool CanUseBotSpell(uint16 spell_id); void ApplySpell(int spell_id, int duration = 0, int level = -1, ApplySpellType apply_type = ApplySpellType::Solo, bool allow_pets = false, bool is_raid_group_only = true); void BreakInvis(); void Escape(); @@ -600,13 +908,11 @@ class Bot : public NPC { void SetBotSpellID(uint32 newSpellID); void SetSpawnStatus(bool spawnStatus) { _spawnStatus = spawnStatus; } void SetPetChooserID(uint8 id) { _petChooserID = id; } - void SetBotArcherySetting(bool bot_archer_setting, bool save = false); + void SetBotRangedSetting(bool value) { _botRangedSetting = value; } void SetBotCharmer(bool c) { _botCharmer = c; } - void SetPetChooser(bool p) { _petChooser = p; } void SetBotOwner(Mob* botOwner) { this->_botOwner = botOwner; } void SetRangerAutoWeaponSelect(bool enable) { GetClass() == Class::Ranger ? _rangerAutoWeaponSelect = enable : _rangerAutoWeaponSelect = false; } void SetBotStance(uint8 stance_id) { _botStance = Stance::IsValid(stance_id) ? stance_id : Stance::Passive; } - void SetBotCasterRange(uint32 bot_caster_range) { m_bot_caster_range = bot_caster_range; } uint32 GetSpellRecastTimer(uint16 spell_id = 0); bool CheckSpellRecastTimer(uint16 spell_id = 0); uint32 GetSpellRecastRemainingTime(uint16 spell_id = 0); @@ -624,8 +930,6 @@ class Bot : public NPC { void ClearSpellRecastTimer(uint16 spell_id = 0); uint32 GetItemReuseRemainingTime(uint32 item_id = 0); void ClearExpiredTimers(); - void SetAltOutOfCombatBehavior(bool behavior_flag) { _altoutofcombatbehavior = behavior_flag;} - void SetShowHelm(bool showhelm) { _showhelm = showhelm; } void SetBeardColor(uint8 value) { beardcolor = value; } void SetBeard(uint8 value) { beard = value; } void SetEyeColor1(uint8 value) { eyecolor1 = value; } @@ -639,7 +943,7 @@ class Bot : public NPC { bool DyeArmor(int16 slot_id, uint32 rgb, bool all_flag = false, bool save_flag = true); int GetExpansionBitmask(); - void SetExpansionBitmask(int expansion_bitmask, bool save = true); + void SetExpansionBitmask(int expansionBitmask); void ListBotSpells(uint8 min_level); @@ -651,15 +955,12 @@ class Bot : public NPC { void ListBotSpellSettings(); void LoadBotSpellSettings(); bool UpdateBotSpellSetting(uint16 spell_id, BotSpellSetting* bs); - void SetBotEnforceSpellSetting(bool enforcespellsettings, bool save = false); - bool GetBotEnforceSpellSetting() const { return m_enforce_spell_settings; } + void SetBotEnforceSpellSetting(bool enforceSpellSettings); + bool GetBotEnforceSpellSetting() { return _enforceSpellSettings; } // Class Destructors ~Bot() override; - // Publicized protected functions - void BotRangedAttack(Mob* other); - // Publicized private functions static NPCType *FillNPCTypeStruct( uint32 botSpellsID, @@ -722,8 +1023,7 @@ class Bot : public NPC { // New accessors for BotDatabase access bool DeleteBot(); - std::vector GetBotTimers() { return bot_timers; } - void SetBotTimers(std::vector timers) { bot_timers = timers; } + std::vector GetBotTimers() { return bot_timers; } uint32 GetLastZoneID() const { return _lastZoneId; } int32 GetBaseAC() const { return _baseAC; } int32 GetBaseATK() const { return _baseATK; } @@ -750,24 +1050,11 @@ class Bot : public NPC { static uint8 spell_casting_chances[SPELL_TYPE_COUNT][Class::PLAYER_CLASS_COUNT][Stance::AEBurn][cntHSND]; - bool BotCastMez(Mob* tar, uint8 botLevel, bool checked_los, BotSpell& botSpell, Raid* raid); - bool BotCastHeal(Mob* tar, uint8 botLevel, uint8 botClass, BotSpell& botSpell, Raid* raid); - bool BotCastRoot(Mob* tar, uint8 botLevel, uint32 iSpellTypes, BotSpell& botSpell, const bool& checked_los); - bool BotCastBuff(Mob* tar, uint8 botLevel, uint8 botClass); - bool BotCastEscape(Mob*& tar, uint8 botClass, BotSpell& botSpell, uint32 iSpellTypes); - bool BotCastNuke(Mob* tar, uint8 botLevel, uint8 botClass, BotSpell& botSpell, const bool& checked_los); - bool BotCastDispel(Mob* tar, BotSpell& botSpell, uint32 iSpellTypes, const bool& checked_los); - bool BotCastPet(Mob* tar, uint8 botClass, BotSpell& botSpell); - bool BotCastCombatBuff(Mob* tar, uint8 botLevel, uint8 botClass); - bool BotCastLifetap(Mob* tar, uint8 botLevel, BotSpell& botSpell, const bool& checked_los, uint32 iSpellTypes); - bool BotCastSnare(Mob* tar, uint8 botLevel, BotSpell& botSpell, const bool& checked_los, uint32 iSpellTypes); - bool BotCastDOT(Mob* tar, uint8 botLevel, const BotSpell& botSpell, const bool& checked_los); - bool BotCastSlow(Mob* tar, uint8 botLevel, uint8 botClass, BotSpell& botSpell, const bool& checked_los, Raid* raid); - bool BotCastDebuff(Mob* tar, uint8 botLevel, BotSpell& botSpell, bool checked_los); - bool BotCastCure(Mob* tar, uint8 botClass, BotSpell& botSpell, Raid* raid); - bool BotCastHateReduction(Mob* tar, uint8 botLevel, const BotSpell& botSpell); - bool BotCastCombatSong(Mob* tar, uint8 botLevel); - bool BotCastSong(Mob* tar, uint8 botLevel); + bool BotCastMez(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type); + bool BotCastHeal(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type); + bool BotCastNuke(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type); + bool BotCastPet(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type); + bool BotCastCure(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type); bool CheckIfIncapacitated(); bool IsAIProcessValid(const Client* bot_owner, const Group* bot_group, const Raid* raid); @@ -785,51 +1072,52 @@ class Bot : public NPC { Mob* tar, float tar_distance ); + bool TargetValidation(Mob* other); bool PullingFlagChecks(Client* bot_owner); - bool ReturningFlagChecks(Client* bot_owner, float fm_distance); + bool ReturningFlagChecks(Client* bot_owner, Mob* leash_owner, float fm_distance); void BotPullerProcess(Client* bot_owner, Raid* raid); - - // Movement Methods - void CalcMeleeDistances( - const Mob* tar, - const EQ::ItemInstance* const& p_item, - const EQ::ItemInstance* const& s_item, - bool behind_mob, - bool backstab_weapon, - float& melee_distance_max, - float& melee_distance - ) const; - // Combat Checks + CombatRangeOutput EvaluateCombatRange(const CombatRangeInput& input); + void SetBerserkState(); bool CheckIfCasting(float fm_distance); void HealRotationChecks(); - void CheckCombatRange(Mob* tar, float tar_distance, bool& atCombatRange, const EQ::ItemInstance*& p_item, const EQ::ItemInstance*& s_item); + + bool GetCombatJitterFlag() { return m_combat_jitter_flag; } + void SetCombatJitterFlag(bool flag = true) { m_combat_jitter_flag = flag; } + bool GetCombatOutOfRangeJitterFlag() { return m_combat_out_of_range_jitter_flag; } + void SetCombatOutOfRangeJitterFlag(bool flag = true) { m_combat_out_of_range_jitter_flag = flag; } + void SetCombatJitter(); + void SetCombatOutOfRangeJitter(); + void DoCombatPositioning(Mob* tar, glm::vec3 Goal, bool stop_melee_level, float tar_distance, float melee_distance_min, float melee_distance, float melee_distance_max, bool behind_mob, bool front_mob); + void DoFaceCheckWithJitter(Mob* tar); + void DoFaceCheckNoJitter(Mob* tar); + void RunToGoalWithJitter(glm::vec3 Goal); + bool RequiresLoSForPositioning(); + bool HasRequiredLoSForPositioning(Mob* tar); // Try Combat Methods bool TryEvade(Mob* tar); bool TryFacingTarget(Mob* tar); - bool TryRangedAttack(Mob* tar); - bool TryClassAttacks(Mob* tar); - bool TryPrimaryWeaponAttacks(Mob* tar, const EQ::ItemInstance* p_item); - bool TrySecondaryWeaponAttacks(Mob* tar, const EQ::ItemInstance* s_item); bool TryPursueTarget(float leash_distance, glm::vec3& Goal); bool TryMeditate(); bool TryAutoDefend(Client* bot_owner, float leash_distance); bool TryIdleChecks(float fm_distance); bool TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob, glm::vec3& Goal); bool TryBardMovementCasts(); - void SetRangerCombatWeapon(bool atArcheryRange); + bool BotRangedAttack(Mob* other, bool can_double_attack = false); + bool CheckDoubleRangedAttack(); // Public "Refactor" Methods - static bool CheckSpawnConditions(Client* c); + static bool CheckCampSpawnConditions(Client* c); protected: - void BotMeditate(bool isSitting); - bool CheckBotDoubleAttack(bool Triple = false); - void PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* client); + void BotMeditate(bool is_sitting); + bool CheckBotDoubleAttack(bool triple_attack = false); + bool CheckTripleAttack(); + void PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* client, int16 chosen_slot = INVALID_INDEX); bool AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore = nullptr) override; BotCastingRoles& GetCastingRoles() { return m_CastingRoles; } @@ -839,9 +1127,14 @@ class Bot : public NPC { void SetGroupDoter(bool flag = true) { m_CastingRoles.GroupDoter = flag; } std::deque bot_signal_q; - std::vector AIBot_spells; - std::vector AIBot_spells_enforced; - std::vector bot_timers; + std::vector AIBot_spells; + std::vector AIBot_spells_enforced; + std::unordered_map> AIBot_spells_by_type; + + std::map> commanded_spells_min_level; + + std::vector bot_timers; + std::vector bot_blocked_buffs; private: // Class Members @@ -849,9 +1142,7 @@ class Bot : public NPC { uint32 _botOwnerCharacterID; bool _spawnStatus; Mob* _botOwner; - bool m_bot_archery_setting; bool _botCharmer; - bool _petChooser; uint8 _petChooserID; bool berserk; EQ::InventoryProfile m_inv; @@ -870,25 +1161,32 @@ class Bot : public NPC { unsigned int RestRegenMana; unsigned int RestRegenEndurance; Timer rest_timer; - Timer ping_timer; + Timer m_ping_timer; int32 base_end; int32 cur_end; int32 max_end; int32 end_regen; - Timer m_evade_timer; // can be moved to pTimers at some point + Timer m_rogue_evade_timer; // Rogue evade timer + Timer m_monk_evade_timer; // Monk evade FD timer Timer m_auto_defend_timer; - Timer auto_save_timer; + Timer m_auto_save_timer; + + Timer m_combat_jitter_timer; + bool m_combat_jitter_flag; + bool m_combat_out_of_range_jitter_flag; + bool m_dirtyautohaters; bool m_guard_flag; bool m_hold_flag; bool m_attack_flag; + bool m_combat_round_alert_flag; bool m_attacking_flag; bool m_pull_flag; bool m_pulling_flag; bool m_returning_flag; bool is_using_item_click; - uint32 m_bot_caster_range; + BotCastingRoles m_CastingRoles; std::map bot_spell_settings; @@ -896,12 +1194,31 @@ class Bot : public NPC { std::shared_ptr m_member_of_heal_rotation; InspectMessage_Struct _botInspectMessage; - bool _altoutofcombatbehavior; - bool _showhelm; bool _pauseAI; + + int _expansionBitmask; + bool _enforceSpellSettings; + bool _showHelm; + bool _botRangedSetting; uint8 _stopMeleeLevel; - int m_expansion_bitmask; - bool m_enforce_spell_settings; + uint32 _distanceRanged; + bool _behindMobStatus; + bool _maxMeleeRangeStatus; + bool _medInCombat; + uint8 _SitHPPct; + uint8 _SitManaPct; + uint16 _castedSpellType; + bool _hasLoS; + bool _commandedSpell; + bool _pullingSpell; + + bool _illusionBlock; + std::vector m_bot_spell_settings; + std::vector _spell_target_list; + std::vector _group_spell_target_list; + Raid* _storedRaid; + bool _verifiedRaid; + uint16 _tempSpellType; // Private "base stats" Members int32 _baseMR; @@ -930,6 +1247,7 @@ class Bot : public NPC { int32 GenerateBaseManaPoints(); void GenerateSpecialAttacks(); void SetBotID(uint32 botID); + void SetCombatRoundForAlerts(bool flag = true) { m_combat_round_alert_flag = flag; } void SetAttackingFlag(bool flag = true) { m_attacking_flag = flag; } void SetPullingFlag(bool flag = true) { m_pulling_flag = flag; } void SetReturningFlag(bool flag = true) { m_returning_flag = flag; } @@ -948,6 +1266,6 @@ class Bot : public NPC { int32 CalcItemATKCap() final; }; -bool IsSpellInBotList(DBbotspells_Struct* spell_list, uint16 iSpellID); +bool IsSpellInBotList(DBbotspells_Struct* spell_list, uint16 spell_id); #endif // BOT_H diff --git a/zone/bot_command.cpp b/zone/bot_command.cpp index b15531dd75..cd82dba6f0 100644 --- a/zone/bot_command.cpp +++ b/zone/bot_command.cpp @@ -64,1168 +64,6 @@ extern QueryServ* QServ; extern WorldServer worldserver; extern TaskManager *task_manager; -bcst_map bot_command_spells; -bcst_required_bot_classes_map required_bots_map; -bcst_required_bot_classes_map_by_class required_bots_map_by_class; - -class BCSpells -{ -public: - static void Load() { - bot_command_spells.clear(); - bcst_levels_map bot_levels_map; - - for (int i = BCEnum::SpellTypeFirst; i <= BCEnum::SpellTypeLast; ++i) { - bot_command_spells[static_cast(i)]; - bot_levels_map[static_cast(i)]; - } - - for (int spell_id = 2; spell_id < SPDAT_RECORDS; ++spell_id) { - if (!IsValidSpell(spell_id)) { - continue; - } - - if (spells[spell_id].player_1[0] == '\0') { - continue; - } - - if ( - spells[spell_id].target_type != ST_Target && - spells[spell_id].cast_restriction != 0 - ) { - continue; - } - - auto target_type = BCEnum::TT_None; - switch (spells[spell_id].target_type) { - case ST_GroupTeleport: - target_type = BCEnum::TT_GroupV1; - break; - case ST_AECaster: - // Disabled until bot code works correctly - //target_type = BCEnum::TT_AECaster; - break; - case ST_AEBard: - // Disabled until bot code works correctly - //target_type = BCEnum::TT_AEBard; - break; - case ST_Target: - switch (spells[spell_id].cast_restriction) { - case 0: - target_type = BCEnum::TT_Single; - break; - case 104: - target_type = BCEnum::TT_Animal; - break; - case 105: - target_type = BCEnum::TT_Plant; - break; - case 118: - target_type = BCEnum::TT_Summoned; - break; - case 120: - target_type = BCEnum::TT_Undead; - break; - default: - break; - } - break; - case ST_Self: - target_type = BCEnum::TT_Self; - break; - case ST_AETarget: - // Disabled until bot code works correctly - //target_type = BCEnum::TT_AETarget; - break; - case ST_Animal: - target_type = BCEnum::TT_Animal; - break; - case ST_Undead: - target_type = BCEnum::TT_Undead; - break; - case ST_Summoned: - target_type = BCEnum::TT_Summoned; - break; - case ST_Corpse: - target_type = BCEnum::TT_Corpse; - break; - case ST_Plant: - target_type = BCEnum::TT_Plant; - break; - case ST_Group: - target_type = BCEnum::TT_GroupV2; - break; - default: - break; - } - if (target_type == BCEnum::TT_None) - continue; - - uint8 class_levels[16] = {0}; - bool player_spell = false; - for (int class_type = Class::Warrior; class_type <= Class::Berserker; ++class_type) { - int class_index = CLASSIDTOINDEX(class_type); - if (spells[spell_id].classes[class_index] == 0 || - spells[spell_id].classes[class_index] > HARD_LEVEL_CAP) { - continue; - } - - class_levels[class_index] = spells[spell_id].classes[class_index]; - player_spell = true; - } - if (!player_spell) - continue; - - STBaseEntry* entry_prototype = nullptr; - while (true) { - switch (spells[spell_id].effect_id[EFFECTIDTOINDEX(1)]) { - case SE_BindAffinity: - entry_prototype = new STBaseEntry(BCEnum::SpT_BindAffinity); - break; - case SE_Charm: - if (spells[spell_id].spell_affect_index != 12) - break; - entry_prototype = new STCharmEntry(); - if (spells[spell_id].resist_difficulty <= -1000) - entry_prototype->SafeCastToCharm()->dire = true; - break; - case SE_Teleport: - entry_prototype = new STDepartEntry; - entry_prototype->SafeCastToDepart()->single = !BCSpells::IsGroupType(target_type); - break; - case SE_Succor: - if (!strcmp(spells[spell_id].teleport_zone, "same")) { - entry_prototype = new STEscapeEntry; - } else { - entry_prototype = new STDepartEntry; - entry_prototype->SafeCastToDepart()->single = !BCSpells::IsGroupType(target_type); - } - break; - case SE_Translocate: - if (spells[spell_id].teleport_zone[0] == '\0') { - entry_prototype = new STSendHomeEntry(); - entry_prototype->SafeCastToSendHome()->group = BCSpells::IsGroupType(target_type); - } else { - entry_prototype = new STDepartEntry; - entry_prototype->SafeCastToDepart()->single = !BCSpells::IsGroupType(target_type); - } - break; - case SE_ModelSize: - if (spells[spell_id].base_value[EFFECTIDTOINDEX(1)] > 100) { - entry_prototype = new STSizeEntry; - entry_prototype->SafeCastToSize()->size_type = BCEnum::SzT_Enlarge; - } else if (spells[spell_id].base_value[EFFECTIDTOINDEX(1)] > 0 && - spells[spell_id].base_value[EFFECTIDTOINDEX(1)] < 100) { - entry_prototype = new STSizeEntry; - entry_prototype->SafeCastToSize()->size_type = BCEnum::SzT_Reduce; - } - break; - case SE_Identify: - entry_prototype = new STBaseEntry(BCEnum::SpT_Identify); - break; - case SE_Invisibility: - if (spells[spell_id].spell_affect_index != 9) - break; - entry_prototype = new STInvisibilityEntry; - entry_prototype->SafeCastToInvisibility()->invis_type = BCEnum::IT_Living; - break; - case SE_SeeInvis: - if (spells[spell_id].spell_affect_index != 5) - break; - entry_prototype = new STInvisibilityEntry; - entry_prototype->SafeCastToInvisibility()->invis_type = BCEnum::IT_See; - break; - case SE_InvisVsUndead: - if (spells[spell_id].spell_affect_index != 9) - break; - entry_prototype = new STInvisibilityEntry; - entry_prototype->SafeCastToInvisibility()->invis_type = BCEnum::IT_Undead; - break; - case SE_InvisVsAnimals: - if (spells[spell_id].spell_affect_index != 9) - break; - entry_prototype = new STInvisibilityEntry; - entry_prototype->SafeCastToInvisibility()->invis_type = BCEnum::IT_Animal; - break; - case SE_Mez: - if (spells[spell_id].effect_id[EFFECTIDTOINDEX(1)] != 31) - break; - entry_prototype = new STBaseEntry(BCEnum::SpT_Mesmerize); - break; - case SE_Revive: - if (spells[spell_id].spell_affect_index != 1) - break; - entry_prototype = new STResurrectEntry(); - entry_prototype->SafeCastToResurrect()->aoe = BCSpells::IsCasterCentered(target_type); - break; - case SE_Rune: - if (spells[spell_id].spell_affect_index != 2) - break; - entry_prototype = new STBaseEntry(BCEnum::SpT_Rune); - break; - case SE_SummonCorpse: - entry_prototype = new STBaseEntry(BCEnum::SpT_SummonCorpse); - break; - case SE_WaterBreathing: - entry_prototype = new STBaseEntry(BCEnum::SpT_WaterBreathing); - break; - default: - break; - } - if (entry_prototype) - break; - - switch (spells[spell_id].effect_id[EFFECTIDTOINDEX(2)]) { - case SE_Succor: - entry_prototype = new STEscapeEntry; - std::string is_lesser = spells[spell_id].name; - if (is_lesser.find("Lesser") != std::string::npos) - entry_prototype->SafeCastToEscape()->lesser = true; - break; - } - if (entry_prototype) - break; - - switch (spells[spell_id].effect_id[EFFECTIDTOINDEX(3)]) { - case SE_Lull: - entry_prototype = new STBaseEntry(BCEnum::SpT_Lull); - break; - case SE_Levitate: // needs more criteria - entry_prototype = new STBaseEntry(BCEnum::SpT_Levitation); - break; - default: - break; - } - if (entry_prototype) - break; - - while (spells[spell_id].type_description_id == 27) { - if (!spells[spell_id].good_effect) - break; - if (spells[spell_id].skill != EQ::skills::SkillOffense && - spells[spell_id].skill != EQ::skills::SkillDefense) - break; - - entry_prototype = new STStanceEntry(); - if (spells[spell_id].skill == EQ::skills::SkillOffense) - entry_prototype->SafeCastToStance()->stance_type = BCEnum::StT_Aggressive; - else - entry_prototype->SafeCastToStance()->stance_type = BCEnum::StT_Defensive; - - break; - } - if (entry_prototype) - break; - - switch (spells[spell_id].spell_affect_index) { - case 1: { - bool valid_spell = false; - entry_prototype = new STCureEntry; - - for (int i = EffectIDFirst; i <= EffectIDLast; ++i) { - int effect_index = EFFECTIDTOINDEX(i); - if (spells[spell_id].effect_id[effect_index] != SE_Blind && - spells[spell_id].base_value[effect_index] >= 0) - continue; - else if (spells[spell_id].effect_id[effect_index] == SE_Blind && - !spells[spell_id].good_effect) - continue; - - switch (spells[spell_id].effect_id[effect_index]) { - case SE_Blind: - entry_prototype->SafeCastToCure()->cure_value[AILMENTIDTOINDEX( - BCEnum::AT_Blindness)] += spells[spell_id].base_value[effect_index]; - break; - case SE_DiseaseCounter: - entry_prototype->SafeCastToCure()->cure_value[AILMENTIDTOINDEX( - BCEnum::AT_Disease)] += spells[spell_id].base_value[effect_index]; - break; - case SE_PoisonCounter: - entry_prototype->SafeCastToCure()->cure_value[AILMENTIDTOINDEX( - BCEnum::AT_Poison)] += spells[spell_id].base_value[effect_index]; - break; - case SE_CurseCounter: - entry_prototype->SafeCastToCure()->cure_value[AILMENTIDTOINDEX( - BCEnum::AT_Curse)] += spells[spell_id].base_value[effect_index]; - break; - case SE_CorruptionCounter: - entry_prototype->SafeCastToCure()->cure_value[AILMENTIDTOINDEX( - BCEnum::AT_Corruption)] += spells[spell_id].base_value[effect_index]; - break; - default: - continue; - } - entry_prototype->SafeCastToCure()->cure_total += spells[spell_id].base_value[effect_index]; - valid_spell = true; - } - if (!valid_spell) { - safe_delete(entry_prototype); - entry_prototype = nullptr; - } - - break; - } - case 2: { - bool valid_spell = false; - entry_prototype = new STResistanceEntry; - - for (int i = EffectIDFirst; i <= EffectIDLast; ++i) { - int effect_index = EFFECTIDTOINDEX(i); - if (spells[spell_id].max_value[effect_index] <= 0) - continue; - - switch (spells[spell_id].effect_id[effect_index]) { - case SE_ResistFire: - entry_prototype->SafeCastToResistance()->resist_value[RESISTANCEIDTOINDEX( - BCEnum::RT_Fire)] += spells[spell_id].max_value[effect_index]; - break; - case SE_ResistCold: - entry_prototype->SafeCastToResistance()->resist_value[RESISTANCEIDTOINDEX( - BCEnum::RT_Cold)] += spells[spell_id].max_value[effect_index]; - break; - case SE_ResistPoison: - entry_prototype->SafeCastToResistance()->resist_value[RESISTANCEIDTOINDEX( - BCEnum::RT_Poison)] += spells[spell_id].max_value[effect_index]; - break; - case SE_ResistDisease: - entry_prototype->SafeCastToResistance()->resist_value[RESISTANCEIDTOINDEX( - BCEnum::RT_Disease)] += spells[spell_id].max_value[effect_index]; - break; - case SE_ResistMagic: - entry_prototype->SafeCastToResistance()->resist_value[RESISTANCEIDTOINDEX( - BCEnum::RT_Magic)] += spells[spell_id].max_value[effect_index]; - break; - case SE_ResistCorruption: - entry_prototype->SafeCastToResistance()->resist_value[RESISTANCEIDTOINDEX( - BCEnum::RT_Corruption)] += spells[spell_id].max_value[effect_index]; - break; - default: - continue; - } - entry_prototype->SafeCastToResistance()->resist_total += spells[spell_id].max_value[effect_index]; - valid_spell = true; - } - if (!valid_spell) { - safe_delete(entry_prototype); - entry_prototype = nullptr; - } - - break; - } - case 7: - case 10: - if (spells[spell_id].effect_description_id != 65) - break; - if (IsEffectInSpell(spell_id, SE_NegateIfCombat)) - break; - entry_prototype = new STMovementSpeedEntry(); - entry_prototype->SafeCastToMovementSpeed()->group = BCSpells::IsGroupType(target_type); - break; - default: - break; - } - if (entry_prototype) - break; - - break; - } - if (!entry_prototype) - continue; - - if (target_type == BCEnum::TT_Self && (entry_prototype->BCST() != BCEnum::SpT_Stance && - entry_prototype->BCST() != BCEnum::SpT_SummonCorpse)) { -#ifdef BCSTSPELLDUMP - LogError("DELETING entry_prototype (primary clause) - name: [{}], target_type: [{}], BCST: [{}]", - spells[spell_id].name, BCEnum::TargetTypeEnumToString(target_type).c_str(), BCEnum::SpellTypeEnumToString(entry_prototype->BCST()).c_str()); -#endif - safe_delete(entry_prototype); - continue; - } - if (entry_prototype->BCST() == BCEnum::SpT_Stance && target_type != BCEnum::TT_Self) { -#ifdef BCSTSPELLDUMP - LogError("DELETING entry_prototype (secondary clause) - name: [{}], BCST: [{}], target_type: [{}]", - spells[spell_id].name, BCEnum::SpellTypeEnumToString(entry_prototype->BCST()).c_str(), BCEnum::TargetTypeEnumToString(target_type).c_str()); -#endif - safe_delete(entry_prototype); - continue; - } - - assert(entry_prototype->BCST() != BCEnum::SpT_None); - - entry_prototype->spell_id = spell_id; - entry_prototype->target_type = target_type; - - bcst_levels& bot_levels = bot_levels_map[entry_prototype->BCST()]; - for (int class_type = Class::Warrior; class_type <= Class::Berserker; ++class_type) { - int class_index = CLASSIDTOINDEX(class_type); - if (!class_levels[class_index]) - continue; - - STBaseEntry* spell_entry = nullptr; - switch (entry_prototype->BCST()) { - case BCEnum::SpT_Charm: - if (entry_prototype->IsCharm()) - spell_entry = new STCharmEntry(entry_prototype->SafeCastToCharm()); - break; - case BCEnum::SpT_Cure: - if (entry_prototype->IsCure()) - spell_entry = new STCureEntry(entry_prototype->SafeCastToCure()); - break; - case BCEnum::SpT_Depart: - if (entry_prototype->IsDepart()) - spell_entry = new STDepartEntry(entry_prototype->SafeCastToDepart()); - break; - case BCEnum::SpT_Escape: - if (entry_prototype->IsEscape()) - spell_entry = new STEscapeEntry(entry_prototype->SafeCastToEscape()); - break; - case BCEnum::SpT_Invisibility: - if (entry_prototype->IsInvisibility()) - spell_entry = new STInvisibilityEntry(entry_prototype->SafeCastToInvisibility()); - break; - case BCEnum::SpT_MovementSpeed: - if (entry_prototype->IsMovementSpeed()) - spell_entry = new STMovementSpeedEntry(entry_prototype->SafeCastToMovementSpeed()); - break; - case BCEnum::SpT_Resistance: - if (entry_prototype->IsResistance()) - spell_entry = new STResistanceEntry(entry_prototype->SafeCastToResistance()); - break; - case BCEnum::SpT_Resurrect: - if (entry_prototype->IsResurrect()) - spell_entry = new STResurrectEntry(entry_prototype->SafeCastToResurrect()); - break; - case BCEnum::SpT_SendHome: - if (entry_prototype->IsSendHome()) - spell_entry = new STSendHomeEntry(entry_prototype->SafeCastToSendHome()); - break; - case BCEnum::SpT_Size: - if (entry_prototype->IsSize()) - spell_entry = new STSizeEntry(entry_prototype->SafeCastToSize()); - break; - case BCEnum::SpT_Stance: - if (entry_prototype->IsStance()) - spell_entry = new STStanceEntry(entry_prototype->SafeCastToStance()); - break; - default: - spell_entry = new STBaseEntry(entry_prototype); - break; - } - - assert(spell_entry); - - spell_entry->caster_class = class_type; - spell_entry->spell_level = class_levels[class_index]; - - bot_command_spells[spell_entry->BCST()].push_back(spell_entry); - - if (bot_levels.find(class_type) == bot_levels.end() || - bot_levels[class_type] > class_levels[class_index]) - bot_levels[class_type] = class_levels[class_index]; - } - - delete(entry_prototype); - } - - remove_inactive(); - order_all(); - load_teleport_zone_names(); - build_strings(bot_levels_map); - status_report(); - -#ifdef BCSTSPELLDUMP - spell_dump(); -#endif - } - - static void Unload() { - for (auto map_iter : bot_command_spells) { - if (map_iter.second.empty()) - continue; - for (auto list_iter: map_iter.second) { - safe_delete(list_iter); - } - map_iter.second.clear(); - } - bot_command_spells.clear(); - required_bots_map.clear(); - required_bots_map_by_class.clear(); - } - - static bool IsCasterCentered(BCEnum::TType target_type) { - switch (target_type) { - case BCEnum::TT_AECaster: - case BCEnum::TT_AEBard: - return true; - default: - return false; - } - } - - static bool IsGroupType(BCEnum::TType target_type) { - switch (target_type) { - case BCEnum::TT_GroupV1: - case BCEnum::TT_GroupV2: - return true; - default: - return false; - } - } -private: - static void remove_inactive() { - if (bot_command_spells.empty()) - return; - - for (auto map_iter = bot_command_spells.begin(); map_iter != bot_command_spells.end(); ++map_iter) { - if (map_iter->second.empty()) - continue; - - bcst_list* spells_list = &map_iter->second; - bcst_list* removed_spells_list = new bcst_list; - - spells_list->remove(nullptr); - spells_list->remove_if([removed_spells_list](STBaseEntry* l) { - if (l->spell_id < 2 || l->spell_id >= SPDAT_RECORDS || strlen(spells[l->spell_id].name) < 3) { - removed_spells_list->push_back(l); - return true; - } - else { - return false; - } - }); - - for (auto del_iter: *removed_spells_list) - { - safe_delete(del_iter); - } - removed_spells_list->clear(); - - if (RuleI(Bots, CommandSpellRank) == 1) { - spells_list->sort([](STBaseEntry* l, STBaseEntry* r) { - if (spells[l->spell_id].spell_group < spells[r->spell_id].spell_group) - return true; - if (spells[l->spell_id].spell_group == spells[r->spell_id].spell_group && l->caster_class < r->caster_class) - return true; - if (spells[l->spell_id].spell_group == spells[r->spell_id].spell_group && l->caster_class == r->caster_class && spells[l->spell_id].rank < spells[r->spell_id].rank) - return true; - - return false; - }); - spells_list->unique([removed_spells_list](STBaseEntry* l, STBaseEntry* r) { - std::string r_name = spells[r->spell_id].name; - if (spells[l->spell_id].spell_group == spells[r->spell_id].spell_group && l->caster_class == r->caster_class && spells[l->spell_id].rank < spells[r->spell_id].rank) { - removed_spells_list->push_back(r); - return true; - } - - return false; - }); - - for (auto del_iter: *removed_spells_list) { - safe_delete(del_iter); - } - removed_spells_list->clear(); - } - - if (RuleI(Bots, CommandSpellRank) == 2) { - spells_list->remove_if([removed_spells_list](STBaseEntry* l) { - std::string l_name = spells[l->spell_id].name; - if (spells[l->spell_id].rank == 10) { - removed_spells_list->push_back(l); - return true; - } - if (l_name.find("III") == (l_name.size() - 3)) { - removed_spells_list->push_back(l); - return true; - } - if (l_name.find("III ") == (l_name.size() - 4)) { - removed_spells_list->push_back(l); - return true; - } - - return false; - }); - - for (auto del_iter: *removed_spells_list) { - safe_delete(del_iter); - } - removed_spells_list->clear(); - } - - // needs rework - if (RuleI(Bots, CommandSpellRank) == 2 || RuleI(Bots, CommandSpellRank) == 3) { - spells_list->sort([](STBaseEntry* l, STBaseEntry* r) { - if (spells[l->spell_id].spell_group < spells[r->spell_id].spell_group) - return true; - if (spells[l->spell_id].spell_group == spells[r->spell_id].spell_group && l->caster_class < r->caster_class) - return true; - if (spells[l->spell_id].spell_group == spells[r->spell_id].spell_group && l->caster_class == r->caster_class && spells[l->spell_id].rank > spells[r->spell_id].rank) - return true; - - return false; - }); - spells_list->unique([removed_spells_list](STBaseEntry* l, STBaseEntry* r) { - std::string l_name = spells[l->spell_id].name; - if (spells[l->spell_id].spell_group == spells[r->spell_id].spell_group && l->caster_class == r->caster_class && spells[l->spell_id].rank > spells[r->spell_id].rank) { - removed_spells_list->push_back(r); - return true; - } - - return false; - }); - - for (auto del_iter: *removed_spells_list) { - safe_delete(del_iter); - } - removed_spells_list->clear(); - } - - safe_delete(removed_spells_list); - } - } - - static void order_all() { - // Example of a macro'd lambda using anonymous property dereference: - // #define XXX(p) ([](const <_Ty>* l, const <_Ty>* r) { return (l->p < r->p); }) - - -#define LT_STBASE(l, r, p) (l->p < r->p) -#define LT_STCHARM(l, r, p) (l->SafeCastToCharm()->p < r->SafeCastToCharm()->p) -#define LT_STCURE(l, r, p) (l->SafeCastToCure()->p < r->SafeCastToCure()->p) -#define LT_STCURE_VAL_ID(l, r, p, ctid) (l->SafeCastToCure()->p[AILMENTIDTOINDEX(ctid)] < r->SafeCastToCure()->p[AILMENTIDTOINDEX(ctid)]) -#define LT_STDEPART(l, r, p) (l->SafeCastToDepart()->p < r->SafeCastToDepart()->p) -#define LT_STESCAPE(l, r, p) (l->SafeCastToEscape()->p < r->SafeCastToEscape()->p) -#define LT_STINVISIBILITY(l, r, p) (l->SafeCastToInvisibility()->p < r->SafeCastToInvisibility()->p) -#define LT_STRESISTANCE(l, r, p) (l->SafeCastToResistance()->p < r->SafeCastToResistance()->p) -#define LT_STRESISTANCE_VAL_ID(l, r, p, rtid) (l->SafeCastToResistance()->p[RESISTANCEIDTOINDEX(rtid)] < r->SafeCastToResistance()->p[RESISTANCEIDTOINDEX(rtid)) -#define LT_STSTANCE(l, r, p) (l->SafeCastToStance()->p < r->SafeCastToStance()->p) -#define LT_SPELLS(l, r, p) (spells[l->spell_id].p < spells[r->spell_id].p) -#define LT_SPELLS_EFFECT_ID(l, r, p, eid) (spells[l->spell_id].p[EFFECTIDTOINDEX(eid)] < spells[r->spell_id].p[EFFECTIDTOINDEX(eid)]) -#define LT_SPELLS_STR(l, r, s) (strcasecmp(spells[l->spell_id].s, spells[r->spell_id].s) < 0) - -#define EQ_STBASE(l, r, p) (l->p == r->p) -#define EQ_STCHARM(l, r, p) (l->SafeCastToCharm()->p == r->SafeCastToCharm()->p) -#define EQ_STCURE(l, r, p) (l->SafeCastToCure()->p == r->SafeCastToCure()->p) -#define EQ_STCURE_VAL_ID(l, r, p, ctid) (l->SafeCastToCure()->p[AILMENTIDTOINDEX(ctid)] == r->SafeCastToCure()->p[AILMENTIDTOINDEX(ctid)]) -#define EQ_STDEPART(l, r, p) (l->SafeCastToDepart()->p == r->SafeCastToDepart()->p) -#define EQ_STESCAPE(l, r, p) (l->SafeCastToEscape()->p == r->SafeCastToEscape()->p) -#define EQ_STINVISIBILITY(l, r, p) (l->SafeCastToInvisibility()->p == r->SafeCastToInvisibility()->p) -#define EQ_STRESISTANCE(l, r, p) (l->SafeCastToResistance()->p == r->SafeCastToResistance()->p) -#define EQ_STRESISTANCE_VAL_ID(l, r, p, rtid) (l->SafeCastToResistance()->p[RESISTANCEIDTOINDEX(rtid)] == r->SafeCastToResistance()->p[RESISTANCEIDTOINDEX(rtid)) -#define EQ_STSTANCE(l, r, p) (l->SafeCastToStance()->p == r->SafeCastToStance()->p) -#define EQ_SPELLS(l, r, p) (spells[l->spell_id].p == spells[r->spell_id].p) -#define EQ_SPELLS_EFFECT_ID(l, r, p, eid) (spells[l->spell_id].p[EFFECTIDTOINDEX(eid)] == spells[r->spell_id].p[EFFECTIDTOINDEX(eid)]) -#define EQ_SPELLS_STR(l, r, s) (strcasecmp(spells[l->spell_id].s, spells[r->spell_id].s) == 0) - -#define GT_STBASE(l, r, p) (l->p > r->p) -#define GT_STCHARM(l, r, p) (l->SafeCastToCharm()->p > r->SafeCastToCharm()->p) -#define GT_STCURE(l, r, p) (l->SafeCastToCure()->p > r->SafeCastToCure()->p) -#define GT_STCURE_VAL_ID(l, r, p, ctid) (l->SafeCastToCure()->p[AILMENTIDTOINDEX(ctid)] > r->SafeCastToCure()->p[AILMENTIDTOINDEX(ctid)]) -#define GT_STDEPART(l, r, p) (l->SafeCastToDepart()->p > r->SafeCastToDepart()->p) -#define GT_STESCAPE(l, r, p) (l->SafeCastToEscape()->p > r->SafeCastToEscape()->p) -#define GT_STINVISIBILITY(l, r, p) (l->SafeCastToInvisibility()->p > r->SafeCastToInvisibility()->p) -#define GT_STRESISTANCE(l, r, p) (l->SafeCastToResistance()->p > r->SafeCastToResistance()->p) -#define GT_STRESISTANCE_VAL_ID(l, r, p, rtid) (l->SafeCastToResistance()->p[RESISTANCEIDTOINDEX(rtid)] > r->SafeCastToResistance()->p[RESISTANCEIDTOINDEX(rtid)) -#define GT_STSTANCE(l, r, p) (l->SafeCastToStance()->p > r->SafeCastToStance()->p) -#define GT_SPELLS(l, r, p) (spells[l->spell_id].p > spells[r->spell_id].p) -#define GT_SPELLS_EFFECT_ID(l, r, p, eid) (spells[l->spell_id].p[EFFECTIDTOINDEX(eid)] > spells[r->spell_id].p[EFFECTIDTOINDEX(eid)]) -#define GT_SPELLS_STR(l, r, s) (strcasecmp(spells[l->spell_id].s, spells[r->spell_id].s) > 0) - - - for (auto map_iter = bot_command_spells.begin(); map_iter != bot_command_spells.end(); ++map_iter) { - if (map_iter->second.size() < 2) - continue; - - auto spell_type = map_iter->first; - bcst_list* spell_list = &map_iter->second; - switch (spell_type) { - case BCEnum::SpT_BindAffinity: - if (RuleB(Bots, PreferNoManaCommandSpells)) { - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_SPELLS(l, r, mana)) - return true; - if (EQ_SPELLS(l, r, mana) && LT_STBASE(l, r, target_type)) - return true; - if (EQ_SPELLS(l, r, mana) && EQ_STBASE(l, r, target_type) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_SPELLS(l, r, mana) && EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - } - else { - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - if (EQ_STBASE(l, r, target_type) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - } - continue; - case BCEnum::SpT_Charm: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_SPELLS(l, r, resist_difficulty)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && LT_STBASE(l, r, target_type)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && GT_SPELLS_EFFECT_ID(l, r, max_value, 1)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, max_value, 1) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, max_value, 1) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Cure: // per-use sorting in command handler - spell_list->sort([](STBaseEntry* l, STBaseEntry* r) { - if (l->spell_id < r->spell_id) - return true; - if (l->spell_id == r->spell_id && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Depart: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - if (EQ_STBASE(l, r, target_type) && LT_STBASE(l, r, caster_class)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, caster_class) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, caster_class) && EQ_STBASE(l, r, spell_level) && LT_SPELLS_STR(l, r, name)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Escape: - spell_list->sort([](STBaseEntry* l, STBaseEntry* r) { - if (LT_STESCAPE(l, r, lesser)) - return true; - if (EQ_STESCAPE(l, r, lesser) && LT_STBASE(l, r, target_type)) - return true; - if (EQ_STESCAPE(l, r, lesser) && EQ_STBASE(l, r, target_type) && GT_STBASE(l, r, spell_level)) - return true; - if (EQ_STESCAPE(l, r, lesser) && EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Identify: - if (RuleB(Bots, PreferNoManaCommandSpells)) { - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_SPELLS(l, r, mana)) - return true; - if (EQ_SPELLS(l, r, mana) && LT_STBASE(l, r, target_type)) - return true; - if (EQ_SPELLS(l, r, mana) && EQ_STBASE(l, r, target_type) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_SPELLS(l, r, mana) && EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - } - else { - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - if (EQ_STBASE(l, r, target_type) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - } - continue; - case BCEnum::SpT_Invisibility: - spell_list->sort([](STBaseEntry* l, STBaseEntry* r) { - if (LT_STINVISIBILITY(l, r, invis_type)) - return true; - if (EQ_STINVISIBILITY(l, r, invis_type) && LT_STBASE(l, r, target_type)) - return true; - if (EQ_STINVISIBILITY(l, r, invis_type) && EQ_STBASE(l, r, target_type) && GT_STBASE(l, r, spell_level)) - return true; - if (EQ_STINVISIBILITY(l, r, invis_type) && EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - return false; - }); - continue; - case BCEnum::SpT_Levitation: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - if (EQ_STBASE(l, r, target_type) && LT_SPELLS(l, r, zone_type)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS(l, r, zone_type) && GT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS(l, r, zone_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Lull: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_SPELLS(l, r, resist_difficulty)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && LT_STBASE(l, r, target_type)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && GT_SPELLS_EFFECT_ID(l, r, max_value, 3)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, max_value, 3) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, max_value, 3) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Mesmerize: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (GT_SPELLS(l, r, resist_difficulty)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && LT_STBASE(l, r, target_type)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && GT_SPELLS_EFFECT_ID(l, r, max_value, 1)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, max_value, 1) && GT_STBASE(l, r, spell_level)) - return true; - if (EQ_SPELLS(l, r, resist_difficulty) && EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, max_value, 1) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_MovementSpeed: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - if (EQ_STBASE(l, r, target_type) && GT_SPELLS_EFFECT_ID(l, r, base_value, 2)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, base_value, 2) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, base_value, 2) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Resistance: // per-use sorting in command handler - spell_list->sort([](STBaseEntry* l, STBaseEntry* r) { - if (l->spell_id < r->spell_id) - return true; - if (l->spell_id == r->spell_id && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Resurrect: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (GT_SPELLS_EFFECT_ID(l, r, base_value, 1)) - return true; - if (EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && LT_STBASE(l, r, target_type)) - return true; - if (EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && EQ_STBASE(l, r, target_type) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Rune: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - if (EQ_STBASE(l, r, target_type) && GT_SPELLS_EFFECT_ID(l, r, max_value, 1)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, max_value, 1) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, max_value, 1) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_SendHome: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - if (EQ_STBASE(l, r, target_type) && GT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_Size: - spell_list->sort([](STBaseEntry* l, STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - - auto l_size_type = l->SafeCastToSize()->size_type; - auto r_size_type = r->SafeCastToSize()->size_type; - if (l_size_type < r_size_type) - return true; - if (l_size_type == BCEnum::SzT_Enlarge && r_size_type == BCEnum::SzT_Enlarge) { - if (EQ_STBASE(l, r, target_type) && GT_SPELLS_EFFECT_ID(l, r, base_value, 1)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && GT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - } - if (l_size_type == BCEnum::SzT_Reduce && r_size_type == BCEnum::SzT_Reduce) { - if (EQ_STBASE(l, r, target_type) && LT_SPELLS_EFFECT_ID(l, r, base_value, 1)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && GT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - } - - return false; - }); - continue; - case BCEnum::SpT_Stance: - spell_list->sort([](STBaseEntry* l, STBaseEntry* r) { - if (LT_STSTANCE(l, r, stance_type)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_SummonCorpse: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (GT_SPELLS_EFFECT_ID(l, r, base_value, 1)) - return true; - if (EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && LT_STBASE(l, r, spell_level)) - return true; - if (EQ_SPELLS_EFFECT_ID(l, r, base_value, 1) && EQ_STBASE(l, r, spell_level) && EQ_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - case BCEnum::SpT_WaterBreathing: - spell_list->sort([](const STBaseEntry* l, const STBaseEntry* r) { - if (LT_STBASE(l, r, target_type)) - return true; - if (EQ_STBASE(l, r, target_type) && GT_STBASE(l, r, spell_level)) - return true; - if (EQ_STBASE(l, r, target_type) && EQ_STBASE(l, r, spell_level) && LT_STBASE(l, r, caster_class)) - return true; - - return false; - }); - continue; - default: - continue; - } - } - } - - static void load_teleport_zone_names() { - auto depart_list = &bot_command_spells[BCEnum::SpT_Depart]; - if (depart_list->empty()) - return; - - std::string query = "SELECT `short_name`, `long_name` FROM `zone` WHERE '' NOT IN (`short_name`, `long_name`)"; - auto results = content_db.QueryDatabase(query); - if (!results.Success()) { - LogError("load_teleport_zone_names() - Error in zone names query: [{}]", results.ErrorMessage().c_str()); - return; - } - - std::map zone_names; - for (auto row = results.begin(); row != results.end(); ++row) - zone_names[row[0]] = row[1]; - - for (auto list_iter = depart_list->begin(); list_iter != depart_list->end();) { - auto test_iter = zone_names.find(spells[(*list_iter)->spell_id].teleport_zone); - if (test_iter == zone_names.end()) { - list_iter = depart_list->erase(list_iter); - continue; - } - - (*list_iter)->SafeCastToDepart()->long_name = test_iter->second; - ++list_iter; - } - - } - - static void build_strings(bcst_levels_map& bot_levels_map) { - for (int i = BCEnum::SpellTypeFirst; i <= BCEnum::SpellTypeLast; ++i) - helper_bots_string(static_cast(i), bot_levels_map[static_cast(i)]); - } - - static void status_report() { - LogCommands("load_bot_command_spells(): - 'RuleI(Bots, CommandSpellRank)' set to [{}]", RuleI(Bots, CommandSpellRank)); - if (bot_command_spells.empty()) { - LogError("load_bot_command_spells() - 'bot_command_spells' is empty"); - return; - } - - for (int i = BCEnum::SpellTypeFirst; i <= BCEnum::SpellTypeLast; ++i) - LogCommands("load_bot_command_spells(): - [{}] returned [{}] spell entries", - BCEnum::SpellTypeEnumToString(static_cast(i)).c_str(), bot_command_spells[static_cast(i)].size()); - } - - static void helper_bots_string(BCEnum::SpType type_index, bcst_levels& bot_levels) { - for (int i = Class::Warrior; i <= Class::Berserker; ++i) - required_bots_map_by_class[type_index][i] = "Unavailable..."; - - if (bot_levels.empty()) { - required_bots_map[type_index] = "This command is currently unavailable..."; - return; - } - - required_bots_map[type_index] = ""; - - auto map_size = bot_levels.size(); - while (bot_levels.size()) { - bcst_levels::iterator test_iter = bot_levels.begin(); - for (bcst_levels::iterator levels_iter = bot_levels.begin(); levels_iter != bot_levels.end(); ++levels_iter) { - if (levels_iter->second < test_iter->second) - test_iter = levels_iter; - if (strcasecmp(GetClassIDName(levels_iter->first), GetClassIDName(test_iter->first)) < 0 && levels_iter->second <= test_iter->second) - test_iter = levels_iter; - } - - std::string bot_segment; - if (bot_levels.size() == map_size) - bot_segment = "%s(%u)"; - else if (bot_levels.size() > 1) - bot_segment = ", %s(%u)"; - else - bot_segment = " or %s(%u)"; - - required_bots_map[type_index].append(StringFormat(bot_segment.c_str(), GetClassIDName(test_iter->first), test_iter->second)); - required_bots_map_by_class[type_index][test_iter->first] = StringFormat("%s(%u)", GetClassIDName(test_iter->first), test_iter->second); - bot_levels.erase(test_iter); - } - } - -#ifdef BCSTSPELLDUMP - static void spell_dump() { - std::ofstream spell_dump; - spell_dump.open(StringFormat("bcs_dump/spell_dump_%i.txt", getpid()), std::ios_base::app | std::ios_base::out); - - if (bot_command_spells.empty()) { - spell_dump << "BCSpells::spell_dump() - 'bot_command_spells' map is empty.\n"; - spell_dump.close(); - return; - } - - int entry_count = 0; - for (int i = BCEnum::SpellTypeFirst; i <= BCEnum::SpellTypeLast; ++i) { - auto bcst_id = static_cast(i); - spell_dump << StringFormat("BCSpells::spell_dump(): - '%s' returned %u spells:\n", - BCEnum::SpellTypeEnumToString(bcst_id).c_str(), bot_command_spells[bcst_id].size()); - - bcst_list& map_entry = bot_command_spells[bcst_id]; - for (auto list_iter = map_entry.begin(); list_iter != map_entry.end(); ++list_iter) { - STBaseEntry* list_entry = *list_iter; - int spell_id = list_entry->spell_id; - spell_dump << StringFormat("\"%20s\" tt:%02u/cc:%02u/cl:%03u", - ((strlen(spells[spell_id].name) > 20) ? (std::string(spells[spell_id].name).substr(0, 20).c_str()) : (spells[spell_id].name)), - list_entry->target_type, - list_entry->caster_class, - list_entry->spell_level - ); - - spell_dump << StringFormat(" /mn:%05u/RD:%06i/zt:%02i/d#:%06i/td#:%05i/ed#:%05i/SAI:%03u", - spells[spell_id].mana, - spells[spell_id].resist_difficulty, - spells[spell_id].zone_type, - spells[spell_id].description_id, - spells[spell_id].type_description_id, - spells[spell_id].effect_description_id, - spells[spell_id].spell_affect_index - ); - - for (int i = EffectIDFirst; i <= 3/*EffectIDLast*/; ++i) { - int effect_index = EFFECTIDTOINDEX(i); - spell_dump << StringFormat(" /e%02i:%04i/b%02i:%06i/m%02i:%06i", - i, spells[spell_id].effect_id[effect_index], i, spells[spell_id].base_value[effect_index], i, spells[spell_id].max_value[effect_index]); - } - - switch (list_entry->BCST()) { - case BCEnum::SpT_Charm: - spell_dump << StringFormat(" /d:%c", ((list_entry->SafeCastToCharm()->dire) ? ('T') : ('F'))); - break; - case BCEnum::SpT_Cure: - spell_dump << ' '; - for (int i = 0; i < BCEnum::AilmentTypeCount; ++i) { - spell_dump << StringFormat("/cv%02i:%03i", i, list_entry->SafeCastToCure()->cure_value[i]); - } - break; - case BCEnum::SpT_Depart: { - std::string long_name = list_entry->SafeCastToDepart()->long_name.c_str(); - spell_dump << StringFormat(" /ln:%20s", ((long_name.size() > 20) ? (long_name.substr(0, 20).c_str()) : (long_name.c_str()))); - break; - } - case BCEnum::SpT_Escape: - spell_dump << StringFormat(" /l:%c", ((list_entry->SafeCastToEscape()->lesser) ? ('T') : ('F'))); - break; - case BCEnum::SpT_Invisibility: - spell_dump << StringFormat(" /it:%02i", list_entry->SafeCastToInvisibility()->invis_type); - break; - case BCEnum::SpT_MovementSpeed: - spell_dump << StringFormat(" /g:%c", ((list_entry->SafeCastToMovementSpeed()->group) ? ('T') : ('F'))); - break; - case BCEnum::SpT_Resistance: - spell_dump << ' '; - for (int i = 0; i < BCEnum::ResistanceTypeCount; ++i) { - spell_dump << StringFormat("/rv%02i:%03i", i, list_entry->SafeCastToResistance()->resist_value[i]); - } - break; - case BCEnum::SpT_Resurrect: - spell_dump << StringFormat(" /aoe:%c", ((list_entry->SafeCastToResurrect()->aoe) ? ('T') : ('F'))); - break; - case BCEnum::SpT_SendHome: - spell_dump << StringFormat(" /g:%c", ((list_entry->SafeCastToSendHome()->group) ? ('T') : ('F'))); - break; - case BCEnum::SpT_Size: - spell_dump << StringFormat(" /st:%02i", list_entry->SafeCastToSize()->size_type); - break; - case BCEnum::SpT_Stance: - spell_dump << StringFormat(" /st:%02i", list_entry->SafeCastToStance()->stance_type); - break; - default: - break; - } - - spell_dump << "\n"; - ++entry_count; - } - - spell_dump << StringFormat("required_bots_map[%s] = \"%s\"\n", - BCEnum::SpellTypeEnumToString(static_cast(i)).c_str(), required_bots_map[static_cast(i)].c_str()); - - spell_dump << "\n"; - } - - spell_dump << StringFormat("Total bcs entry count: %i\n", entry_count); - spell_dump.close(); - } -#endif -}; - int bot_command_count; int (*bot_command_dispatch)(Client *,char const *) = bot_command_not_avail; @@ -1237,7 +75,7 @@ LinkedList cleanup_bot_command_list; int bot_command_not_avail(Client *c, const char *message) { - c->Message(Chat::White, "Bot commands not available."); + c->Message(Chat::Yellow, "Bot commands not available."); return -1; } @@ -1247,11 +85,11 @@ int bot_command_init(void) if ( bot_command_add("actionable", "Lists actionable command arguments and use descriptions", AccountStatus::Player, bot_command_actionable) || - bot_command_add("aggressive", "Orders a bot to use a aggressive discipline", AccountStatus::Player, bot_command_aggressive) || bot_command_add("applypoison", "Applies cursor-held poison to a rogue bot's weapon", AccountStatus::Player, bot_command_apply_poison) || - bot_command_add("applypotion", "Applies cursor-held potion to a bot's effects", AccountStatus::Player, bot_command_apply_potion) || bot_command_add("attack", "Orders bots to attack a designated target", AccountStatus::Player, bot_command_attack) || - bot_command_add("bindaffinity", "Orders a bot to attempt an affinity binding", AccountStatus::Player, bot_command_bind_affinity) || + bot_command_add("behindmob", "Toggles whether or not your bot tries to stay behind a mob", AccountStatus::Player, bot_command_behind_mob) || + bot_command_add("blockedbuffs", "Set, view and clear blocked buffs for the selected bot(s)", AccountStatus::Player, bot_command_blocked_buffs) || + bot_command_add("blockedpetbuffs", "Set, view and clear blocked pet buffs for the selected bot(s)", AccountStatus::Player, bot_command_blocked_pet_buffs) || bot_command_add("bot", "Lists the available bot management [subcommands]", AccountStatus::Player, bot_command_bot) || bot_command_add("botappearance", "Lists the available bot appearance [subcommands]", AccountStatus::Player, bot_command_appearance) || bot_command_add("botbeardcolor", "Changes the beard color of a bot", AccountStatus::Player, bot_command_beard_color) || @@ -1270,8 +108,8 @@ int bot_command_init(void) bot_command_add("botheritage", "Changes the Drakkin heritage of a bot", AccountStatus::Player, bot_command_heritage) || bot_command_add("botinspectmessage", "Changes the inspect message of a bot", AccountStatus::Player, bot_command_inspect_message) || bot_command_add("botlist", "Lists the bots that you own", AccountStatus::Player, bot_command_list_bots) || - bot_command_add("botoutofcombat", "Toggles your bot between standard and out-of-combat spell/skill use - if any specialized behaviors exist", AccountStatus::Player, bot_command_out_of_combat) || bot_command_add("botreport", "Orders a bot to report its readiness", AccountStatus::Player, bot_command_report) || + bot_command_add("botsettings", "Lists settings related to spell types and bot combat", AccountStatus::Player, bot_command_bot_settings) || bot_command_add("botspawn", "Spawns a created bot", AccountStatus::Player, bot_command_spawn) || bot_command_add("botstance", "Changes the stance of a bot", AccountStatus::Player, bot_command_stance) || bot_command_add("botstopmeleelevel", "Sets the level a caster or spell-casting fighter bot will stop melee combat", AccountStatus::Player, bot_command_stop_melee_level) || @@ -1279,20 +117,20 @@ int bot_command_init(void) bot_command_add("botsummon", "Summons bot(s) to your location", AccountStatus::Player, bot_command_summon) || bot_command_add("botsurname", "Sets a bots surname (last name)", AccountStatus::Player, bot_command_surname) || bot_command_add("bottattoo", "Changes the Drakkin tattoo of a bot", AccountStatus::Player, bot_command_tattoo) || - bot_command_add("bottogglearcher", "Toggles a archer bot between melee and ranged weapon use", AccountStatus::Player, bot_command_toggle_archer) || bot_command_add("bottogglehelm", "Toggles the helm visibility of a bot between shown and hidden", AccountStatus::Player, bot_command_toggle_helm) || + bot_command_add("bottoggleranged", "Toggles a ranged bot between melee and ranged weapon use", AccountStatus::Player, bot_command_toggle_ranged) || bot_command_add("bottitle", "Sets a bots title", AccountStatus::Player, bot_command_title) || bot_command_add("botupdate", "Updates a bot to reflect any level changes that you have experienced", AccountStatus::Player, bot_command_update) || bot_command_add("botwoad", "Changes the Barbarian woad of a bot", AccountStatus::Player, bot_command_woad) || - bot_command_add("casterrange", "Controls the range casters will try to stay away from a mob (if too far, they will skip spells that are out-of-range)", AccountStatus::Player, bot_command_caster_range) || - bot_command_add("charm", "Attempts to have a bot charm your target", AccountStatus::Player, bot_command_charm) || - bot_command_add("circle", "Orders a Druid bot to open a magical doorway to a specified destination", AccountStatus::Player, bot_command_circle) || + bot_command_add("cast", "Tells the first found specified bot to cast the given spell type", AccountStatus::Player, bot_command_cast) || + bot_command_add("discipline", "Uses aggressive/defensive disciplines or can specify spell ID", AccountStatus::Player, bot_command_discipline) || + bot_command_add("distanceranged", "Controls the range casters and ranged will try to stay away from a mob", AccountStatus::Player, bot_command_distance_ranged) || + bot_command_add("classracelist", "Lists the classes and races and their appropriate IDs", AccountStatus::Player, bot_command_class_race_list) || bot_command_add("clickitem", "Orders your targeted bot to click the item in the provided inventory slot.", AccountStatus::Player, bot_command_click_item) || - bot_command_add("cure", "Orders a bot to remove any ailments", AccountStatus::Player, bot_command_cure) || - bot_command_add("defensive", "Orders a bot to use a defensive discipline", AccountStatus::Player, bot_command_defensive) || + bot_command_add("copysettings", "Copies settings from one bot to another", AccountStatus::Player, bot_command_copy_settings) || + bot_command_add("defaultsettings", "Restores a bot back to default settings", AccountStatus::Player, bot_command_default_settings) || bot_command_add("depart", "Orders a bot to open a magical doorway to a specified destination", AccountStatus::Player, bot_command_depart) || bot_command_add("enforcespellsettings", "Toggles your Bot to cast only spells in their spell settings list.", AccountStatus::Player, bot_command_enforce_spell_list) || - bot_command_add("escape", "Orders a bot to send a target group to a safe location within the zone", AccountStatus::Player, bot_command_escape) || bot_command_add("findaliases", "Find available aliases for a bot command", AccountStatus::Player, bot_command_find_aliases) || bot_command_add("follow", "Orders bots to follow a designated target (option 'chain' auto-links eligible spawned bots)", AccountStatus::Player, bot_command_follow) || bot_command_add("guard", "Orders bots to guard their current positions", AccountStatus::Player, bot_command_guard) || @@ -1319,18 +157,14 @@ int bot_command_init(void) bot_command_add("healrotationstop", "Stops a heal rotation", AccountStatus::Player, bot_command_heal_rotation_stop) || bot_command_add("help", "List available commands and their description - specify partial command as argument to search", AccountStatus::Player, bot_command_help) || bot_command_add("hold", "Prevents a bot from attacking until released", AccountStatus::Player, bot_command_hold) || - bot_command_add("identify", "Orders a bot to cast an item identification spell", AccountStatus::Player, bot_command_identify) || + bot_command_add("illusionblock", "Control whether or not illusion effects will land on the bot if casted by another player or bot", AccountStatus::Player, bot_command_illusion_block) || bot_command_add("inventory", "Lists the available bot inventory [subcommands]", AccountStatus::Player, bot_command_inventory) || bot_command_add("inventorygive", "Gives the item on your cursor to a bot", AccountStatus::Player, bot_command_inventory_give) || bot_command_add("inventorylist", "Lists all items in a bot's inventory", AccountStatus::Player, bot_command_inventory_list) || bot_command_add("inventoryremove", "Removes an item from a bot's inventory", AccountStatus::Player, bot_command_inventory_remove) || bot_command_add("inventorywindow", "Displays all items in a bot's inventory in a pop-up window", AccountStatus::Player, bot_command_inventory_window) || - bot_command_add("invisibility", "Orders a bot to cast a cloak of invisibility, or allow them to be seen", AccountStatus::Player, bot_command_invisibility) || bot_command_add("itemuse", "Elicits a report from spawned bots that can use the item on your cursor (option 'empty' yields only empty slots)", AccountStatus::Player, bot_command_item_use) || - bot_command_add("levitation", "Orders a bot to cast a levitation spell", AccountStatus::Player, bot_command_levitation) || - bot_command_add("lull", "Orders a bot to cast a pacification spell", AccountStatus::Player, bot_command_lull) || - bot_command_add("mesmerize", "Orders a bot to cast a mesmerization spell", AccountStatus::Player, bot_command_mesmerize) || - bot_command_add("movementspeed", "Orders a bot to cast a movement speed enhancement spell", AccountStatus::Player, bot_command_movement_speed) || + bot_command_add("maxmeleerange", "Toggles whether your bot is at max melee range or not. This will disable all special abilities, including taunt.", AccountStatus::Player, bot_command_max_melee_range) || bot_command_add("owneroption", "Sets options available to bot owners", AccountStatus::Player, bot_command_owner_option) || bot_command_add("pet", "Lists the available bot pet [subcommands]", AccountStatus::Player, bot_command_pet) || bot_command_add("petgetlost", "Orders a bot to remove its summoned pet", AccountStatus::Player, bot_command_pet_get_lost) || @@ -1339,14 +173,27 @@ int bot_command_init(void) bot_command_add("picklock", "Orders a capable bot to pick the lock of the closest door", AccountStatus::Player, bot_command_pick_lock) || bot_command_add("pickpocket", "Orders a capable bot to pickpocket a NPC", AccountStatus::Player, bot_command_pickpocket) || bot_command_add("precombat", "Sets flag used to determine pre-combat behavior", AccountStatus::Player, bot_command_precombat) || - bot_command_add("portal", "Orders a Wizard bot to open a magical doorway to a specified destination", AccountStatus::Player, bot_command_portal) || bot_command_add("pull", "Orders a designated bot to 'pull' an enemy", AccountStatus::Player, bot_command_pull) || bot_command_add("release", "Releases a suspended bot's AI processing (with hate list wipe)", AccountStatus::Player, bot_command_release) || - bot_command_add("resistance", "Orders a bot to cast a specified resistance buff", AccountStatus::Player, bot_command_resistance) || - bot_command_add("resurrect", "Orders a bot to resurrect a player's (players') corpse(s)", AccountStatus::Player, bot_command_resurrect) || - bot_command_add("rune", "Orders a bot to cast a rune of protection", AccountStatus::Player, bot_command_rune) || - bot_command_add("sendhome", "Orders a bot to open a magical doorway home", AccountStatus::Player, bot_command_send_home) || - bot_command_add("size", "Orders a bot to change a player's size", AccountStatus::Player, bot_command_size) || + bot_command_add("setassistee", "Sets your target to be the person your bots assist. Bots will always assist you before others", AccountStatus::Player, bot_command_set_assistee) || + bot_command_add("sithppercent", "HP threshold for a bot to start sitting in combat if allowed", AccountStatus::Player, bot_command_sit_hp_percent) || + bot_command_add("sitincombat", "Toggles whether or a not a bot will attempt to med or sit to heal in combat", AccountStatus::Player, bot_command_sit_in_combat) || + bot_command_add("sitmanapercent", "Mana threshold for a bot to start sitting in combat if allowed", AccountStatus::Player, bot_command_sit_mana_percent) || + bot_command_add("spellaggrochecks", "Toggles whether or not bots will cast a spell type if they think it will get them aggro", AccountStatus::Player, bot_command_spell_aggro_checks) || + bot_command_add("spellannouncecasts", "Turn on or off cast announcements by spell type", AccountStatus::Player, bot_command_spell_announce_cast) || + bot_command_add("spellengagedpriority", "Controls the order of casts by spell type when engaged in combat", AccountStatus::Player, bot_command_spell_engaged_priority) || + bot_command_add("spelldelays", "Controls the delay between casts for a specific spell type", AccountStatus::Player, bot_command_spell_delays) || + bot_command_add("spellholds", "Controls whether a bot holds the specified spell type or not", AccountStatus::Player, bot_command_spell_holds) || + bot_command_add("spellidlepriority", "Controls the order of casts by spell type when out of combat", AccountStatus::Player, bot_command_spell_idle_priority) || + bot_command_add("spellmaxhppct", "Controls at what HP percent a bot will stop casting different spell types", AccountStatus::Player, bot_command_spell_max_hp_pct) || + bot_command_add("spellmaxmanapct", "Controls at what mana percent a bot will stop casting different spell types", AccountStatus::Player, bot_command_spell_max_mana_pct) || + bot_command_add("spellmaxthresholds", "Controls the minimum target HP threshold for a spell to be cast for a specific type", AccountStatus::Player, bot_command_spell_max_thresholds) || + bot_command_add("spellminhppct", "Controls at what HP percent a bot will start casting different spell types", AccountStatus::Player, bot_command_spell_min_hp_pct) || + bot_command_add("spellminmanapct", "Controls at what mana percent a bot will start casting different spell types", AccountStatus::Player, bot_command_spell_min_mana_pct) || + bot_command_add("spellminthresholds", "Controls the maximum target HP threshold for a spell to be cast for a specific type", AccountStatus::Player, bot_command_spell_min_thresholds) || + bot_command_add("spellresistlimits", "Controls the resist limits for bots to cast spells on their target", AccountStatus::Player, bot_command_spell_resist_limits) || + bot_command_add("spellpursuepriority", "Controls the order of casts by spell type when pursuing in combat", AccountStatus::Player, bot_command_spell_pursue_priority) || + bot_command_add("spelltargetcount", "Sets the required target amount for group/AE spells by spell type", AccountStatus::Player, bot_command_spell_target_count) || bot_command_add("spellinfo", "Opens a dialogue window with spell info", AccountStatus::Player, bot_spell_info_dialogue_window) || bot_command_add("spells", "Lists all Spells learned by the Bot.", AccountStatus::Player, bot_command_spell_list) || bot_command_add("spellsettings", "Lists a bot's spell setting entries", AccountStatus::Player, bot_command_spell_settings_list) || @@ -1354,13 +201,13 @@ int bot_command_init(void) bot_command_add("spellsettingsdelete", "Delete a bot spell setting entry", AccountStatus::Player, bot_command_spell_settings_delete) || bot_command_add("spellsettingstoggle", "Toggle a bot spell use", AccountStatus::Player, bot_command_spell_settings_toggle) || bot_command_add("spellsettingsupdate", "Update a bot spell setting entry", AccountStatus::Player, bot_command_spell_settings_update) || - bot_command_add("summoncorpse", "Orders a bot to summon a corpse to its feet", AccountStatus::Player, bot_command_summon_corpse) || + bot_command_add("spelltypeids", "Lists spelltypes by ID", AccountStatus::Player, bot_command_spelltype_ids) || + bot_command_add("spelltypenames", "Lists spelltypes by shortname", AccountStatus::Player, bot_command_spelltype_names) || bot_command_add("suspend", "Suspends a bot's AI processing until released", AccountStatus::Player, bot_command_suspend) || bot_command_add("taunt", "Toggles taunt use by a bot", AccountStatus::Player, bot_command_taunt) || bot_command_add("timer", "Checks or clears timers of the chosen type.", AccountStatus::GMMgmt, bot_command_timer) || bot_command_add("track", "Orders a capable bot to track enemies", AccountStatus::Player, bot_command_track) || - bot_command_add("viewcombos", "Views bot race class combinations", AccountStatus::Player, bot_command_view_combos) || - bot_command_add("waterbreathing", "Orders a bot to cast a water breathing spell", AccountStatus::Player, bot_command_water_breathing) + bot_command_add("viewcombos", "Views bot race class combinations", AccountStatus::Player, bot_command_view_combos) ) { bot_command_deinit(); return -1; @@ -1458,8 +305,6 @@ int bot_command_init(void) bot_command_dispatch = bot_command_real_dispatch; - BCSpells::Load(); - return bot_command_count; } @@ -1470,8 +315,6 @@ void bot_command_deinit(void) bot_command_dispatch = bot_command_not_avail; bot_command_count = 0; - - BCSpells::Unload(); } int bot_command_add(std::string bot_command_name, const char *desc, int access, BotCmdFuncPtr function) @@ -1520,7 +363,7 @@ int bot_command_real_dispatch(Client *c, const char *message) BotCommandRecord *cur = bot_command_list[cstr]; if(c->Admin() < cur->access){ - c->Message(Chat::White, "Your access level is not high enough to use this bot command."); + c->Message(Chat::Yellow, "Your access level is not high enough to use this bot command."); return(-1); } @@ -1545,18 +388,18 @@ int bot_command_real_dispatch(Client *c, const char *message) } -bool helper_bot_appearance_fail(Client *bot_owner, Bot *my_bot, BCEnum::AFType fail_type, const char* type_desc) +bool helper_bot_appearance_fail(Client *bot_owner, Bot *my_bot, uint8 fail_type, const char* type_desc) { switch (fail_type) { - case BCEnum::AFT_Value: - bot_owner->Message(Chat::White, "Failed to change '%s' for %s due to invalid value for this command", type_desc, my_bot->GetCleanName()); - return true; - case BCEnum::AFT_GenderRace: - bot_owner->Message(Chat::White, "Failed to change '%s' for %s due to invalid bot gender and/or race for this command", type_desc, my_bot->GetCleanName()); - return true; - case BCEnum::AFT_Race: - bot_owner->Message(Chat::White, "Failed to change '%s' for %s due to invalid bot race for this command", type_desc, my_bot->GetCleanName()); - return true; + case AFT_Value: + bot_owner->Message(Chat::Yellow, "Failed to change '%s' for %s due to invalid value for this command", type_desc, my_bot->GetCleanName()); + return true; + case AFT_GenderRace: + bot_owner->Message(Chat::Yellow, "Failed to change '%s' for %s due to invalid bot gender and/or race for this command", type_desc, my_bot->GetCleanName()); + return true; + case AFT_Race: + bot_owner->Message(Chat::Yellow, "Failed to change '%s' for %s due to invalid bot race for this command", type_desc, my_bot->GetCleanName()); + return true; default: return false; } @@ -1567,7 +410,7 @@ void helper_bot_appearance_form_final(Client *bot_owner, Bot *my_bot) if (!MyBots::IsMyBot(bot_owner, my_bot)) return; if (!my_bot->Save()) { - bot_owner->Message(Chat::White, "Failed to save appearance change for %s due to unknown cause...", my_bot->GetCleanName()); + bot_owner->Message(Chat::Yellow, "Failed to save appearance change for %s due to unknown cause...", my_bot->GetCleanName()); return; } @@ -1611,35 +454,29 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas if (!Bot::IsValidName(bot_name)) { bot_owner->Message( - Chat::White, + Chat::Yellow, fmt::format( - "'{}' is an invalid name. You may only use characters 'A-Z' or 'a-z'. Mixed case {} allowed.", + "'{}' is an invalid name. You may only use characters 'A-Z' or 'a-z' and it must be between 4 and 15 characters with no spaces. Mixed case {} allowed.", bot_name, RuleB(Bots, AllowCamelCaseNames) ? "is" : "is not" ).c_str() ); + return bot_id; } bool available_flag = false; - if (!database.botdb.QueryNameAvailablity(bot_name, available_flag)) { - bot_owner->Message( - Chat::White, - fmt::format( - "Failed to query name availability for '{}'.", - bot_name - ).c_str() - ); - return bot_id; - } + + !database.botdb.QueryNameAvailablity(bot_name, available_flag); if (!available_flag) { bot_owner->Message( - Chat::White, + Chat::Yellow, fmt::format( - "The name '{}' is already being used. Please choose a different name", + "The name '{}' is already being used or prohibited. Please choose a different name", bot_name ).c_str() ); + return bot_id; } @@ -1648,7 +485,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas const std::string bot_class_name = GetClassIDName(bot_class); bot_owner->Message( - Chat::White, + Chat::Yellow, fmt::format( "{} {} is an invalid race-class combination, would you like to {} proper combinations for {}?", bot_race_name, @@ -1684,7 +521,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas uint32 bot_count = 0; uint32 bot_class_count = 0; if (!database.botdb.QueryBotCount(bot_owner->CharacterID(), bot_class, bot_count, bot_class_count)) { - bot_owner->Message(Chat::White, "Failed to query bot count."); + bot_owner->Message(Chat::Yellow, "Failed to query bot count."); return bot_id; } @@ -1701,7 +538,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas message = "You cannot create any bots."; } - bot_owner->Message(Chat::White, message.c_str()); + bot_owner->Message(Chat::Yellow, message.c_str()); return bot_id; } @@ -1722,7 +559,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas ); } - bot_owner->Message(Chat::White, message.c_str()); + bot_owner->Message(Chat::Yellow, message.c_str()); return bot_id; } @@ -1733,7 +570,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas bot_owner->GetLevel() < bot_character_level ) { bot_owner->Message( - Chat::White, + Chat::Yellow, fmt::format( "You must be level {} to use bots.", bot_character_level @@ -1749,7 +586,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas bot_owner->GetLevel() < bot_character_level_class ) { bot_owner->Message( - Chat::White, + Chat::Yellow, fmt::format( "You must be level {} to use {} bots.", bot_character_level_class, @@ -1764,7 +601,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas if (!my_bot->Save()) { bot_owner->Message( - Chat::White, + Chat::Yellow, fmt::format( "Failed to create '{}' due to unknown cause.", my_bot->GetCleanName() @@ -1806,47 +643,13 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas return bot_id; } -void helper_bot_out_of_combat(Client *bot_owner, Bot *my_bot) -{ - if (!bot_owner || !my_bot) - return; - - switch (my_bot->GetClass()) { - case Class::Warrior: - case Class::Cleric: - case Class::Paladin: - case Class::Ranger: - case Class::ShadowKnight: - case Class::Druid: - case Class::Monk: - bot_owner->Message(Chat::White, "%s has no out-of-combat behavior defined", my_bot->GetCleanName()); - break; - case Class::Bard: - bot_owner->Message(Chat::White, "%s will %s use out-of-combat behavior for bard songs", my_bot->GetCleanName(), ((my_bot->GetAltOutOfCombatBehavior()) ? ("now") : ("no longer"))); - break; - case Class::Rogue: - case Class::Shaman: - case Class::Necromancer: - case Class::Wizard: - case Class::Magician: - case Class::Enchanter: - case Class::Beastlord: - case Class::Berserker: - bot_owner->Message(Chat::White, "%s has no out-of-combat behavior defined", my_bot->GetCleanName()); - break; - default: - break; - bot_owner->Message(Chat::White, "Undefined bot class for %s", my_bot->GetCleanName()); - } -} - int helper_bot_follow_option_chain(Client* bot_owner) { if (!bot_owner) { return 0; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_BySpawnedBots(bot_owner, sbl); if (sbl.empty()) { return 0; @@ -1909,30 +712,10 @@ int helper_bot_follow_option_chain(Client* bot_owner) return chain_follow_count; } -bool helper_cast_standard_spell(Bot* casting_bot, Mob* target_mob, int spell_id, bool annouce_cast, uint32* dont_root_before) -{ - if (!casting_bot || !target_mob) - return false; - - casting_bot->InterruptSpell(); - if (annouce_cast) { - Bot::BotGroupSay( - casting_bot, - fmt::format( - "Attempting to cast {} on {}.", - spells[spell_id].name, - target_mob->GetCleanName() - ).c_str() - ); - } - - return casting_bot->CastSpell(spell_id, target_mob->GetID(), EQ::spells::CastingSlot::Gem2, -1, -1, dont_root_before); -} - bool helper_command_disabled(Client* bot_owner, bool rule_value, const char* command) { if (!rule_value) { - bot_owner->Message(Chat::White, "Bot command %s is not enabled on this server.", command); + bot_owner->Message(Chat::Yellow, "Bot command %s is not enabled on this server.", command); return true; } @@ -1943,111 +726,13 @@ bool helper_command_alias_fail(Client *bot_owner, const char* command_handler, c { auto alias_iter = bot_command_aliases.find(&alias[1]); if (alias_iter == bot_command_aliases.end() || alias_iter->second.compare(command)) { - bot_owner->Message(Chat::White, "Undefined linker usage in %s (%s)", command_handler, &alias[1]); + bot_owner->Message(Chat::Yellow, "Undefined linker usage in %s (%s)", command_handler, &alias[1]); return true; } return false; } -void helper_command_depart_list(Client* bot_owner, Bot* druid_bot, Bot* wizard_bot, bcst_list* local_list, bool single_flag) -{ - if (!bot_owner) { - return; - } - - if (!MyBots::IsMyBot(bot_owner, druid_bot)) { - druid_bot = nullptr; - } - - if (!MyBots::IsMyBot(bot_owner, wizard_bot)) { - wizard_bot = nullptr; - } - - if (!druid_bot && !wizard_bot) { - bot_owner->Message(Chat::White, "No bots are capable of performing this action"); - return; - } - - if (!local_list) { - bot_owner->Message(Chat::White, "There are no destinations you can be taken to."); - return; - } - - auto destination_count = 0; - auto destination_number = 1; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToDepart(); - if (!local_entry) { - continue; - } - - if ( - druid_bot && - druid_bot->GetClass() == local_entry->caster_class && - druid_bot->GetLevel() >= local_entry->spell_level - ) { - if (local_entry->single != single_flag) { - continue; - } - - druid_bot->OwnerMessage( - fmt::format( - "Destination {} | {} | {}", - destination_number, - local_entry->long_name, - Saylink::Silent( - fmt::format( - "^circle {}{}", - spells[local_entry->spell_id].teleport_zone, - single_flag ? " single" : "" - ), - "Goto" - ) - ) - ); - - destination_count++; - destination_number++; - continue; - } - - if ( - wizard_bot && - wizard_bot->GetClass() == local_entry->caster_class && - wizard_bot->GetLevel() >= local_entry->spell_level - ) { - if (local_entry->single != single_flag) { - continue; - } - - wizard_bot->OwnerMessage( - fmt::format( - "Destination {} | {} | {}", - destination_number, - local_entry->long_name, - Saylink::Silent( - fmt::format( - "^portal {}{}", - spells[local_entry->spell_id].teleport_zone, - single_flag ? " single" : "" - ), - "Goto" - ) - ) - ); - - destination_count++; - destination_number++; - continue; - } - } - - if (!destination_count) { - bot_owner->Message(Chat::White, "There are no destinations you can be taken to."); - } -} - bool helper_is_help_or_usage(const char* arg) { if (!arg) @@ -2063,14 +748,14 @@ bool helper_no_available_bots(Client *bot_owner, Bot *my_bot) if (!bot_owner) return true; if (!my_bot) { - bot_owner->Message(Chat::White, "No bots are capable of performing this action"); + bot_owner->Message(Chat::Yellow, "No bots are capable of performing this action"); return true; } return false; } -void helper_send_available_subcommands(Client *bot_owner, const char* command_simile, const std::list& subcommand_list) +void helper_send_available_subcommands(Client* bot_owner, const char* command_simile, std::vector subcommand_list) { bot_owner->Message(Chat::White, "Available %s management subcommands:", command_simile); @@ -2085,8 +770,11 @@ void helper_send_available_subcommands(Client *bot_owner, const char* command_si bot_owner->Message( Chat::White, fmt::format( - "^{} - {}", - subcommand_iter, + "{} - {}", + Saylink::Silent( + fmt::format("^{} help", subcommand_iter), + fmt::format("^{}", subcommand_iter) + ), find_iter != bot_command_list.end() ? find_iter->second->desc : "No Description" ).c_str() ); @@ -2097,64 +785,167 @@ void helper_send_available_subcommands(Client *bot_owner, const char* command_si bot_owner->Message(Chat::White, "%d bot subcommand%s listed.", bot_subcommands_shown, bot_subcommands_shown != 1 ? "s" : ""); } -void helper_send_usage_required_bots(Client *bot_owner, BCEnum::SpType spell_type, uint8 bot_class) +void helper_send_usage_required_bots(Client *bot_owner, uint16 spell_type) { - bot_owner->Message(Chat::White, "requires one of the following bot classes:"); - if (bot_class) - bot_owner->Message(Chat::White, "%s", required_bots_map_by_class[spell_type][bot_class].c_str()); - else - bot_owner->Message(Chat::White, "%s", required_bots_map[spell_type].c_str()); -} + if (!bot_owner) { + return; + } -bool helper_spell_check_fail(STBaseEntry* local_entry) -{ - if (!local_entry) - return true; - if (spells[local_entry->spell_id].zone_type && zone->GetZoneType() && !(spells[local_entry->spell_id].zone_type & zone->GetZoneType())) - return true; + auto sbl = entity_list.GetBotListByCharacterID(bot_owner->CharacterID()); + Bot* bot = nullptr; - return false; + for (const auto& b : sbl) { + if (b) { + bot = b; + + break; + } + } + + auto& spell_map = bot->GetCommandedSpellTypesMinLevels(); + + if (spell_map.empty()) { + bot_owner->Message(Chat::Yellow, "No bots are capable of casting this spell type"); + return; + } + + bool found = false; + std::string description; + + for (int i = Class::Warrior; i <= Class::Berserker; ++i) { + auto spell_type_itr = spell_map.find(spell_type); + auto class_itr = spell_type_itr->second.find(i); + const auto& spell_info = class_itr->second; + + if (spell_info.min_level < UINT8_MAX) { + found = true; + + if (!description.empty()) { + description.append(", "); + } + else { + bot_owner->Message(Chat::Yellow, "Required bots to cast: Class [Class ID]: [Level]"); + } + + description.append(spell_info.description); + } + } + + if (!found || description.empty()) { + bot_owner->Message(Chat::Yellow, "No bots are capable of casting this spell type"); + + return; + } + + bot_owner->Message(Chat::Green, "%s", description.c_str()); } -bool helper_spell_list_fail(Client *bot_owner, bcst_list* spell_list, BCEnum::SpType spell_type) -{ - if (!spell_list || spell_list->empty()) { - bot_owner->Message(Chat::White, "%s", required_bots_map[spell_type].c_str()); - return true; +void SendSpellTypeWindow(Client* c, const Seperator* sep) { + std::string arg0 = sep->arg[0]; + std::string arg1 = sep->arg[1]; + + uint8 min_count = 0; + uint8 max_count = 0; + bool client_only = false; + + if (BotSpellTypes::END <= 19) { + min_count = BotSpellTypes::START; + max_count = BotSpellTypes::END; + } + else if (!arg1.compare("0-19")) { + min_count = BotSpellTypes::START; + max_count = 19; + } + else if (!arg1.compare("20-39")) { + min_count = std::min(static_cast(20), static_cast(BotSpellTypes::END)); + max_count = std::min(static_cast(39), static_cast(BotSpellTypes::END)); } + else if (!arg1.compare("40+")) { + min_count = std::min(static_cast(40), static_cast(BotSpellTypes::END)); + max_count = BotSpellTypes::END; + } + else if (!arg1.compare("commanded")) { + min_count = BotSpellTypes::COMMANDED_START; + max_count = BotSpellTypes::COMMANDED_END; + } + else if (!arg1.compare("client")) { + min_count = BotSpellTypes::START; + max_count = BotSpellTypes::END; + client_only = true; + } + else { + c->Message(Chat::Yellow, "You must choose a valid range option"); - return false; + return; + } + + const std::string& indian_red = "indian_red"; + const std::string& gold = "gold"; + const std::string& slate_blue = "slate_blue"; + const std::string& forest_green = "forest_green"; + const std::string& goldenrod = "goldenrod"; + + std::string filler_line = "-----------"; + std::string spell_type_field = "Spell Type"; + std::string id_field = "ID"; + std::string shortname_field = "Short Name"; + + std::string popup_text = DialogueWindow::TableRow( + DialogueWindow::TableCell(DialogueWindow::ColorMessage(goldenrod, spell_type_field)) + + + DialogueWindow::TableCell((!arg0.compare("^spelltypeids") ? DialogueWindow::ColorMessage(goldenrod, id_field) : DialogueWindow::ColorMessage(goldenrod, shortname_field))) + ); + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell(DialogueWindow::ColorMessage(gold, filler_line)) + + + DialogueWindow::TableCell(DialogueWindow::ColorMessage(gold, filler_line)) + ); + + for (int i = min_count; i <= max_count; ++i) { + if (client_only && !IsClientBotSpellType(i)) { + continue; + } + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell(DialogueWindow::ColorMessage(forest_green, Bot::GetSpellTypeNameByID(i))) + + + DialogueWindow::TableCell((!arg0.compare("^spelltypeids") ? DialogueWindow::ColorMessage(slate_blue, std::to_string(i)) : DialogueWindow::ColorMessage(slate_blue, Bot::GetSpellTypeShortNameByID(i)))) + ); + } + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient("Spell Types", popup_text.c_str()); } #include "bot_commands/actionable.cpp" -#include "bot_commands/aggressive.cpp" #include "bot_commands/appearance.cpp" #include "bot_commands/apply_poison.cpp" #include "bot_commands/apply_potion.cpp" #include "bot_commands/attack.cpp" -#include "bot_commands/bind_affinity.cpp" +#include "bot_commands/behind_mob.cpp" +#include "bot_commands/blocked_buffs.cpp" #include "bot_commands/bot.cpp" -#include "bot_commands/caster_range.cpp" -#include "bot_commands/charm.cpp" +#include "bot_commands/bot_settings.cpp" +#include "bot_commands/cast.cpp" +#include "bot_commands/class_race_list.cpp" #include "bot_commands/click_item.cpp" -#include "bot_commands/cure.cpp" -#include "bot_commands/defensive.cpp" +#include "bot_commands/copy_settings.cpp" +#include "bot_commands/default_settings.cpp" #include "bot_commands/depart.cpp" -#include "bot_commands/escape.cpp" +#include "bot_commands/discipline.cpp" +#include "bot_commands/distance_ranged.cpp" #include "bot_commands/find_aliases.cpp" #include "bot_commands/follow.cpp" #include "bot_commands/guard.cpp" #include "bot_commands/heal_rotation.cpp" #include "bot_commands/help.cpp" #include "bot_commands/hold.cpp" -#include "bot_commands/identify.cpp" +#include "bot_commands/illusion_block.cpp" #include "bot_commands/inventory.cpp" -#include "bot_commands/invisibility.cpp" #include "bot_commands/item_use.cpp" -#include "bot_commands/levitation.cpp" -#include "bot_commands/lull.cpp" -#include "bot_commands/mesmerize.cpp" -#include "bot_commands/movement_speed.cpp" +#include "bot_commands/max_melee_range.cpp" #include "bot_commands/name.cpp" #include "bot_commands/owner_option.cpp" #include "bot_commands/pet.cpp" @@ -2163,18 +954,30 @@ bool helper_spell_list_fail(Client *bot_owner, bcst_list* spell_list, BCEnum::Sp #include "bot_commands/precombat.cpp" #include "bot_commands/pull.cpp" #include "bot_commands/release.cpp" -#include "bot_commands/resistance.cpp" -#include "bot_commands/resurrect.cpp" -#include "bot_commands/rune.cpp" -#include "bot_commands/send_home.cpp" -#include "bot_commands/size.cpp" +#include "bot_commands/set_assistee.cpp" +#include "bot_commands/sit_hp_percent.cpp" +#include "bot_commands/sit_in_combat.cpp" +#include "bot_commands/sit_mana_percent.cpp" #include "bot_commands/spell.cpp" +#include "bot_commands/spell_aggro_checks.cpp" +#include "bot_commands/spell_announce_cast.cpp" +#include "bot_commands/spell_delays.cpp" +#include "bot_commands/spell_engaged_priority.cpp" +#include "bot_commands/spell_holds.cpp" +#include "bot_commands/spell_idle_priority.cpp" +#include "bot_commands/spell_max_hp_pct.cpp" +#include "bot_commands/spell_max_mana_pct.cpp" +#include "bot_commands/spell_max_thresholds.cpp" +#include "bot_commands/spell_min_hp_pct.cpp" +#include "bot_commands/spell_min_mana_pct.cpp" +#include "bot_commands/spell_min_thresholds.cpp" +#include "bot_commands/spell_pursue_priority.cpp" +#include "bot_commands/spell_resist_limits.cpp" +#include "bot_commands/spell_target_count.cpp" +#include "bot_commands/spelltypes.cpp" #include "bot_commands/summon.cpp" -#include "bot_commands/summon_corpse.cpp" #include "bot_commands/suspend.cpp" #include "bot_commands/taunt.cpp" -#include "bot_commands/teleport.cpp" #include "bot_commands/timer.cpp" #include "bot_commands/track.cpp" #include "bot_commands/view_combos.cpp" -#include "bot_commands/water_breathing.cpp" diff --git a/zone/bot_command.h b/zone/bot_command.h index 74dfa14017..77d449c44d 100644 --- a/zone/bot_command.h +++ b/zone/bot_command.h @@ -27,194 +27,6 @@ class Seperator; #include "bot.h" #include "dialogue_window.h" -class BCEnum -{ -public: - typedef enum SpellType { - SpT_None = 0, - SpT_BindAffinity, - SpT_Charm, - SpT_Cure, - SpT_Depart, - SpT_Escape, - SpT_Identify, - SpT_Invisibility, - SpT_Levitation, - SpT_Lull, - SpT_Mesmerize, - SpT_MovementSpeed, - SpT_Resistance, - SpT_Resurrect, - SpT_Rune, - SpT_SendHome, - SpT_Size, - SpT_Stance, - SpT_SummonCorpse, - SpT_WaterBreathing - } SpType; - static const int SpellTypeFirst = SpT_BindAffinity; - static const int SpellTypeLast = SpT_WaterBreathing; - - typedef enum TargetType { - TT_None = 0, - TT_Corpse, - TT_Self, - TT_Animal, - TT_Undead, - TT_Summoned, - TT_Plant, - TT_Single, - TT_GroupV1, - TT_GroupV2, - TT_AECaster, - TT_AEBard, - TT_AETarget - } TType; - static const int TargetTypeFirst = TT_Corpse; - static const int TargetTypeLast = TT_AETarget; - static const int TargetTypeCount = 13; - - typedef enum TargetMask { - TM_None = 0, - TM_Corpse = 1, - TM_Self = 2, - TM_Animal = 4, - TM_Undead = 8, - TM_Summoned = 16, - TM_Plant = 32, - TM_Single = 124, // currently, 2^6 + 2^{2..5}) -or- (64+32+16+8+4) - TM_GroupV1 = 128, - TM_GroupV2 = 256, - TM_AECaster = 512, - TM_AEBard = 1024, - TM_AETarget = 2048 - } TMask; - - typedef enum AppearanceFailType { - AFT_None = 0, - AFT_Value, - AFT_GenderRace, - AFT_Race - } AFType; - - typedef enum AilmentType { - AT_None = 0, - AT_Blindness, // SE: 20 - AT_Disease, // SE: 35 - AT_Poison, // SE: 36 - AT_Curse, // SE: 116 - AT_Corruption // SE: 369 - } AType; - static const int AilmentTypeCount = 5; - - typedef enum InvisType { - IT_None = 0, - IT_Animal, - IT_Undead, - IT_Living, - IT_See - } IType; - - typedef enum ResistanceType { - RT_None = 0, - RT_Fire, // SE: 46 - RT_Cold, // SE: 47 - RT_Poison, // SE: 48 - RT_Disease, // SE: 49 - RT_Magic, // SE: 50 - RT_Corruption // SE: 370 - } RType; - static const int ResistanceTypeCount = 6; - - typedef enum SizeType { - SzT_None = 0, - SzT_Enlarge, - SzT_Reduce - } SzType; - - typedef enum StanceType { - StT_None = 0, - StT_Aggressive, - StT_Defensive - } StType; - - static std::string SpellTypeEnumToString(BCEnum::SpType spell_type) { - switch (spell_type) { - case SpT_BindAffinity: - return "SpT_BindAffinity"; - case SpT_Charm: - return "SpT_Charm"; - case SpT_Cure: - return "SpT_Cure"; - case SpT_Depart: - return "SpT_Depart"; - case SpT_Escape: - return "SpT_Escape"; - case SpT_Identify: - return "SpT_Identify"; - case SpT_Invisibility: - return "SpT_Invisibility"; - case SpT_Levitation: - return "SpT_Levitation"; - case SpT_Lull: - return "SpT_Lull"; - case SpT_Mesmerize: - return "SpT_Mesmerize"; - case SpT_MovementSpeed: - return "SpT_MovementSpeed"; - case SpT_Resistance: - return "SpT_Resistance"; - case SpT_Resurrect: - return "SpT_Resurrect"; - case SpT_Rune: - return "SpT_Rune"; - case SpT_SendHome: - return "SpT_SendHome"; - case SpT_Size: - return "SpT_Size"; - case SpT_Stance: - return "SpT_Stance"; - case SpT_SummonCorpse: - return "SpT_SummonCorpse"; - case SpT_WaterBreathing: - return "SpT_WaterBreathing"; - default: - return "SpT_None"; - } - } - - static std::string TargetTypeEnumToString(BCEnum::TType target_type) { - switch (target_type) { - case TT_Self: - return "TT_Self"; - case TT_Animal: - return "TT_Animal"; - case TT_Undead: - return "TT_Undead"; - case TT_Summoned: - return "TT_Summoned"; - case TT_Plant: - return "TT_Plant"; - case TT_Single: - return "TT_Single"; - case TT_GroupV1: - return "TT_GroupV1"; - case TT_GroupV2: - return "TT_GroupV2"; - case TT_AECaster: - return "TT_AECaster"; - case TT_AEBard: - return "TT_AEBard"; - case TT_AETarget: - return "TT_AETarget"; - case TT_Corpse: - return "TT_Corpse"; - default: - return "TT_None"; - } - } -}; - namespace { #define HP_RATIO_DELTA 5.0f @@ -222,14 +34,17 @@ namespace enum { EffectIDFirst = 1, EffectIDLast = 12 }; #define VALIDATECLASSID(x) ((x >= Class::Warrior && x <= Class::Berserker) ? (x) : (0)) -#define CLASSIDTOINDEX(x) ((x >= Class::Warrior && x <= Class::Berserker) ? (x - 1) : (0)) -#define EFFECTIDTOINDEX(x) ((x >= EffectIDFirst && x <= EffectIDLast) ? (x - 1) : (0)) -#define AILMENTIDTOINDEX(x) ((x >= BCEnum::AT_Blindness && x <= BCEnum::AT_Corruption) ? (x - 1) : (0)) -#define RESISTANCEIDTOINDEX(x) ((x >= BCEnum::RT_Fire && x <= BCEnum::RT_Corruption) ? (x - 1) : (0)) // ActionableTarget action_type #define FRIENDLY true #define ENEMY false + + enum { + AFT_None = 0, + AFT_Value, + AFT_GenderRace, + AFT_Race + }; } namespace MyBots @@ -272,13 +87,13 @@ namespace MyBots return (grouped_player->GetGroup() == grouped_bot->GetGroup()); } - static void UniquifySBL(std::list &sbl) { - sbl.remove(nullptr); - sbl.sort(); - sbl.unique(); + static void UniquifySBL(std::vector &sbl) { + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + std::sort(sbl.begin(), sbl.end()); + sbl.erase(std::unique(sbl.begin(), sbl.end()), sbl.end()); } - static void PopulateSBL_ByTargetedBot(Client *bot_owner, std::list &sbl, bool clear_list = true) { + static void PopulateSBL_ByTargetedBot(Client *bot_owner, std::vector &sbl, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -292,7 +107,7 @@ namespace MyBots } } - static void PopulateSBL_ByNamedBot(Client *bot_owner, std::list &sbl, const char* name, bool clear_list = true) { + static void PopulateSBL_ByNamedBot(Client *bot_owner, std::vector &sbl, const char* name, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -314,7 +129,7 @@ namespace MyBots } } - static void PopulateSBL_ByMyGroupedBots(Client *bot_owner, std::list &sbl, bool clear_list = true) { + static void PopulateSBL_ByMyGroupedBots(Client *bot_owner, std::vector &sbl, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -363,7 +178,7 @@ namespace MyBots } } - static void PopulateSBL_ByMyRaidBots(Client* bot_owner, std::list& sbl, bool clear_list = true) { + static void PopulateSBL_ByMyRaidBots(Client* bot_owner, std::vector& sbl, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -396,7 +211,7 @@ namespace MyBots } } - static void PopulateSBL_ByTargetsGroupedBots(Client *bot_owner, std::list &sbl, bool clear_list = true) { + static void PopulateSBL_ByTargetsGroupedBots(Client *bot_owner, std::vector &sbl, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -448,7 +263,7 @@ namespace MyBots } } - static void PopulateSBL_ByNamesGroupedBots(Client *bot_owner, std::list &sbl, const char* name, bool clear_list = true) { + static void PopulateSBL_ByNamesGroupedBots(Client *bot_owner, std::vector &sbl, const char* name, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -507,7 +322,7 @@ namespace MyBots } } - static void PopulateSBL_ByHealRotation(Client *bot_owner, std::list &sbl, const char* name, bool clear_list = true) { + static void PopulateSBL_ByHealRotation(Client *bot_owner, std::vector &sbl, const char* name, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -516,7 +331,7 @@ namespace MyBots return; } - std::list selectable_bot_list; + std::vector selectable_bot_list; if (name) { PopulateSBL_ByNamedBot(bot_owner, selectable_bot_list, name); } @@ -545,7 +360,7 @@ namespace MyBots UniquifySBL(sbl); } - static void PopulateSBL_ByHealRotationMembers(Client *bot_owner, std::list &sbl, const char* name, bool clear_list = true) { + static void PopulateSBL_ByHealRotationMembers(Client *bot_owner, std::vector &sbl, const char* name, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -554,7 +369,7 @@ namespace MyBots return; } - std::list selectable_bot_list; + std::vector selectable_bot_list; if (name) { PopulateSBL_ByNamedBot(bot_owner, selectable_bot_list, name); } @@ -578,7 +393,7 @@ namespace MyBots } } - static void PopulateSBL_ByHealRotationTargets(Client *bot_owner, std::list &sbl, const char* name, bool clear_list = true) { + static void PopulateSBL_ByHealRotationTargets(Client *bot_owner, std::vector &sbl, const char* name, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -587,7 +402,7 @@ namespace MyBots return; } - std::list selectable_bot_list; + std::vector selectable_bot_list; if (name) { PopulateSBL_ByNamedBot(bot_owner, selectable_bot_list, name); } @@ -611,17 +426,17 @@ namespace MyBots } } - static void PopulateSBL_BySpawnedBots(Client *bot_owner, std::list &sbl) { // should be used for most spell casting commands + static void PopulateSBL_BySpawnedBots(Client *bot_owner, std::vector &sbl) { // should be used for most spell casting commands sbl.clear(); if (!bot_owner) { return; } sbl = entity_list.GetBotsByBotOwnerCharacterID(bot_owner->CharacterID()); - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); } - static void PopulateSBL_BySpawnedBotsClass(Client * bot_owner, std::list &sbl, uint16 cls, bool clear_list = true) { + static void PopulateSBL_BySpawnedBotsClass(Client * bot_owner, std::vector &sbl, uint16 cls, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -644,7 +459,7 @@ namespace MyBots } } - static void PopulateSBL_BySpawnedBotsRace(Client* bot_owner, std::list& sbl, uint16 race, bool clear_list = true) { + static void PopulateSBL_BySpawnedBotsRace(Client* bot_owner, std::vector& sbl, uint16 race, bool clear_list = true) { if (clear_list) { sbl.clear(); } @@ -666,6 +481,27 @@ namespace MyBots UniquifySBL(sbl); } } + + static void PopulateSBL_ByAtMMR(Client* bot_owner, std::vector& sbl, bool clear_list = true) { + if (clear_list) { + sbl.clear(); + } + + if (!bot_owner) { + return; + } + + auto selectable_bot_list = entity_list.GetBotsByBotOwnerCharacterID(bot_owner->CharacterID()); + for (auto bot_iter : selectable_bot_list) { + if (!bot_iter->GetMaxMeleeRange()) { + continue; + } + sbl.push_back(bot_iter); + } + if (!clear_list) { + UniquifySBL(sbl); + } + } }; namespace ActionableTarget @@ -738,128 +574,6 @@ namespace ActionableTarget return false; } - - static Mob* VerifyFriendly(Client* bot_owner, BCEnum::TType target_type, bool return_me_on_null_target = true) { - if (IsAttackable(bot_owner, bot_owner->GetTarget()) || target_type == BCEnum::TT_None) - return nullptr; - - auto target_mob = bot_owner->GetTarget(); - Mob* verified_friendly = nullptr; - switch (target_type) { - case BCEnum::TT_Single: - case BCEnum::TT_GroupV1: - case BCEnum::TT_GroupV2: - case BCEnum::TT_AETarget: - verified_friendly = target_mob; - break; - case BCEnum::TT_Animal: - if (target_mob && target_mob->GetBodyType() == BodyType::Animal) - verified_friendly = target_mob; - break; - case BCEnum::TT_Undead: - if (target_mob && target_mob->GetBodyType() == BodyType::Undead) - verified_friendly = target_mob; - break; - case BCEnum::TT_Summoned: - if (target_mob && target_mob->GetBodyType() == BodyType::Summoned) - verified_friendly = target_mob; - break; - case BCEnum::TT_Plant: - if (target_mob && target_mob->GetBodyType() == BodyType::Plant) - verified_friendly = target_mob; - break; - case BCEnum::TT_Corpse: - if (target_mob && target_mob->IsCorpse()) - verified_friendly = target_mob; - break; - default: - return nullptr; - } - - if (return_me_on_null_target && !target_mob && !verified_friendly) { - switch (target_type) { - case BCEnum::TT_Single: - case BCEnum::TT_GroupV1: - case BCEnum::TT_GroupV2: - case BCEnum::TT_AETarget: - verified_friendly = bot_owner; - break; - default: - break; - } - } - - return verified_friendly; - } - - static Mob* VerifyEnemy(Client* bot_owner, BCEnum::TType target_type) { - if (!IsAttackable(bot_owner, bot_owner->GetTarget()) || target_type == BCEnum::TT_None) - return nullptr; - - auto target_mob = bot_owner->GetTarget(); - Mob* verified_enemy = nullptr; - switch (target_type) { - case BCEnum::TT_Animal: - if (target_mob->GetBodyType() == BodyType::Animal) - verified_enemy = target_mob; - break; - case BCEnum::TT_Undead: - if (target_mob->GetBodyType() == BodyType::Undead) - verified_enemy = target_mob; - break; - case BCEnum::TT_Summoned: - if (target_mob->GetBodyType() == BodyType::Summoned) - verified_enemy = target_mob; - break; - case BCEnum::TT_Plant: - if (target_mob->GetBodyType() == BodyType::Plant) - verified_enemy = target_mob; - break; - case BCEnum::TT_Single: - case BCEnum::TT_GroupV1: - case BCEnum::TT_GroupV2: - case BCEnum::TT_AETarget: - verified_enemy = target_mob; - break; - case BCEnum::TT_Corpse: - if (target_mob->IsCorpse()) - verified_enemy = target_mob; - break; - default: - return nullptr; - } - - return verified_enemy; - } - - class Types { - Mob* target[BCEnum::TargetTypeCount]; - bool target_set[BCEnum::TargetTypeCount]; - - public: - Types() { Clear(); } - - void Clear() { - for (int i = BCEnum::TT_None; i <= BCEnum::TargetTypeLast; ++i) { - target[i] = nullptr; - target_set[i] = false; - } - target_set[BCEnum::TT_None] = true; - } - - Mob* Select(Client* bot_owner, BCEnum::TType target_type, bool action_type, bool return_me_on_null_target = true) { - if (target_set[target_type]) - return target[target_type]; - - if (action_type == FRIENDLY) - target[target_type] = VerifyFriendly(bot_owner, target_type, return_me_on_null_target); - else - target[target_type] = VerifyEnemy(bot_owner, target_type); - target_set[target_type] = true; - - return target[target_type]; - } - }; } namespace ActionableBots @@ -875,6 +589,7 @@ namespace ActionableBots ABT_HealRotation, ABT_HealRotationMembers, ABT_HealRotationTargets, + ABT_MMR, ABT_Class, ABT_Race, ABT_Spawned, @@ -892,6 +607,7 @@ namespace ActionableBots ABM_HealRotation = (1 << (ABT_HealRotation - 1)), ABM_HealRotationMembers = (1 << (ABT_HealRotationMembers - 1)), ABM_HealRotationTargets = (1 << (ABT_HealRotationTargets - 1)), + ABM_MMR = (1 << (ABT_MMR - 1)), ABM_Class = (1 << (ABT_Class - 1)), ABM_Race = (1 << (ABT_Race - 1)), ABM_Spawned = (1 << (ABT_Spawned - 1)), @@ -899,12 +615,12 @@ namespace ActionableBots ABM_Spawned_All = (3 << (ABT_Spawned - 1)), ABM_NoFilter = ~0, // grouped values - ABM_Type1 = (ABM_Target | ABM_ByName | ABM_OwnerGroup | ABM_OwnerRaid | ABM_TargetGroup | ABM_NamesGroup | ABM_HealRotationTargets | ABM_Spawned | ABM_Class | ABM_Race), - ABM_Type2 = (ABM_ByName | ABM_OwnerGroup | ABM_OwnerRaid | ABM_NamesGroup | ABM_HealRotation | ABM_Spawned | ABM_Class | ABM_Race) + ABM_Type1 = (ABM_Target | ABM_ByName | ABM_OwnerGroup | ABM_OwnerRaid | ABM_TargetGroup | ABM_NamesGroup | ABM_HealRotationTargets | ABM_Spawned | ABM_MMR | ABM_Class | ABM_Race), + ABM_Type2 = (ABM_ByName | ABM_OwnerGroup | ABM_OwnerRaid | ABM_NamesGroup | ABM_HealRotation | ABM_Spawned | ABM_MMR | ABM_Class | ABM_Race) }; // Populates 'sbl' - static ABType PopulateSBL(Client* bot_owner, std::string ab_type_arg, std::list &sbl, int ab_mask, const char* name = nullptr, uint16 classrace = 0, bool clear_list = true, bool suppress_message = false) { + static ABType PopulateSBL(Client* bot_owner, std::string ab_type_arg, std::vector &sbl, int ab_mask, const char* name = nullptr, uint16 classrace = 0, bool clear_list = true, bool suppress_message = false) { if (clear_list) { sbl.clear(); } @@ -941,6 +657,9 @@ namespace ActionableBots else if (!ab_type_arg.compare("healrotationtargets")) { ab_type = ABT_HealRotationTargets; } + else if (!ab_type_arg.compare("mmr")) { + ab_type = ABT_MMR; + } else if (!ab_type_arg.compare("byclass")) { ab_type = ABT_Class; } @@ -1004,6 +723,11 @@ namespace ActionableBots MyBots::PopulateSBL_ByHealRotationTargets(bot_owner, sbl, name, clear_list); } break; + case ABT_MMR: + if (ab_mask & ABM_MMR) { + MyBots::PopulateSBL_ByAtMMR(bot_owner, sbl, clear_list); + } + break; case ABT_Class: if (ab_mask & ABM_Class) { MyBots::PopulateSBL_BySpawnedBotsClass(bot_owner, sbl, classrace, clear_list); @@ -1133,7 +857,7 @@ namespace ActionableBots return nullptr; } - static Bot* AsSpawned_ByClass(Client *bot_owner, std::list &sbl, uint8 cls, bool petless = false) { + static Bot* AsSpawned_ByClass(Client *bot_owner, std::vector &sbl, uint8 cls, bool petless = false) { if (!bot_owner) { return nullptr; } @@ -1157,7 +881,7 @@ namespace ActionableBots return nullptr; } - static Bot* AsSpawned_ByMinLevelAndClass(Client *bot_owner, std::list &sbl, uint8 minlvl, uint8 cls, bool petless = false) { + static Bot* AsSpawned_ByMinLevelAndClass(Client *bot_owner, std::vector &sbl, uint8 minlvl, uint8 cls, bool petless = false) { // This function can be nixed if we can enforce bot level as owner level..and the level check can then be moved to the spell loop in the command function if (!bot_owner) return nullptr; @@ -1187,7 +911,7 @@ namespace ActionableBots if (!bot_owner || bot_name.empty()) return nullptr; - std::list selectable_bot_list; + std::vector selectable_bot_list; MyBots::PopulateSBL_BySpawnedBots(bot_owner, selectable_bot_list); for (auto bot_iter : selectable_bot_list) { if (!bot_name.compare(bot_iter->GetCleanName())) @@ -1197,7 +921,7 @@ namespace ActionableBots return nullptr; } - static Bot* Select_ByClass(Client* bot_owner, BCEnum::TType target_type, std::list& sbl, uint8 cls, Mob* target_mob = nullptr, bool petless = false) { + static Bot* Select_ByClass(Client* bot_owner, int target_type, std::vector& sbl, uint8 cls, Mob* target_mob = nullptr, bool petless = false) { if (!bot_owner || sbl.empty()) return nullptr; @@ -1208,7 +932,7 @@ namespace ActionableBots continue; if (petless && bot_iter->GetPet()) continue; - if (target_type == BCEnum::TT_GroupV1) { + if (target_type == ST_GroupTeleport) { if (!target_mob) return nullptr; else if (bot_iter->GetGroup() != target_mob->GetGroup()) @@ -1221,7 +945,7 @@ namespace ActionableBots return nullptr; } - static Bot* Select_ByMinLevelAndClass(Client* bot_owner, BCEnum::TType target_type, std::list& sbl, uint8 minlvl, uint8 cls, Mob* target_mob = nullptr, bool petless = false) { + static Bot* Select_ByMinLevelAndClass(Client* bot_owner, int target_type, std::vector& sbl, uint8 minlvl, uint8 cls, Mob* target_mob = nullptr, bool petless = false) { if (!bot_owner || sbl.empty()) return nullptr; @@ -1232,7 +956,7 @@ namespace ActionableBots continue; if (petless && bot_iter->GetPet()) continue; - if (target_type == BCEnum::TT_GroupV1) { + if (target_type == ST_GroupTeleport) { if (!target_mob) return nullptr; else if (bot_iter->GetGroup() != target_mob->GetGroup()) @@ -1246,23 +970,23 @@ namespace ActionableBots } // Filters actual 'sbl' list - static void Filter_ByClasses(Client* bot_owner, std::list& sbl, uint16 class_mask) { - sbl.remove_if([bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); - sbl.remove_if([class_mask](const Bot* l) { return (GetPlayerClassBit(l->GetClass()) & (~class_mask)); }); + static void Filter_ByClasses(Client* bot_owner, std::vector& sbl, uint16 class_mask) { + std::erase_if(sbl, [bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); + std::erase_if(sbl, [class_mask](Bot* l) { return (GetPlayerClassBit(l->GetClass()) & (~class_mask)); }); } - static void Filter_ByMinLevel(Client* bot_owner, std::list& sbl, uint8 min_level) { - sbl.remove_if([bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); - sbl.remove_if([min_level](const Bot* l) { return (l->GetLevel() < min_level); }); + static void Filter_ByMinLevel(Client* bot_owner, std::vector& sbl, uint8 min_level) { + std::erase_if(sbl, [bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); + std::erase_if(sbl, [min_level](Bot* l) { return (l->GetLevel() < min_level); }); } - static void Filter_ByArcher(Client* bot_owner, std::list& sbl) { - sbl.remove_if([bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); - sbl.remove_if([bot_owner](Bot* l) { return (!l->IsBotArcher()); }); + static void Filter_ByRanged(Client* bot_owner, std::vector& sbl) { + std::erase_if(sbl, [bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); + std::erase_if(sbl, [bot_owner](Bot* l) { return (l->IsBotRanged()); }); } - static void Filter_ByHighestSkill(Client* bot_owner, std::list& sbl, EQ::skills::SkillType skill_type, float& skill_value) { - sbl.remove_if([bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); + static void Filter_ByHighestSkill(Client* bot_owner, std::vector& sbl, EQ::skills::SkillType skill_type, float& skill_value) { + std::erase_if(sbl, [bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); skill_value = 0.0f; float mod_skill_value = 0.0f; @@ -1288,329 +1012,19 @@ namespace ActionableBots skilled_bot = bot_iter; } } - - sbl.remove_if([skilled_bot](const Bot* l) { return (l != skilled_bot); }); + std::erase_if(sbl, [skilled_bot](Bot* l) { return (l != skilled_bot); }); } - static void Filter_ByHighestPickLock(Client* bot_owner, std::list& sbl, float& pick_lock_value) { - sbl.remove_if([bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); - sbl.remove_if([bot_owner](const Bot* l) { return (l->GetClass() != Class::Rogue && l->GetClass() != Class::Bard); }); - sbl.remove_if([bot_owner](const Bot* l) { return (l->GetClass() == Class::Rogue && l->GetLevel() < 5); }); - sbl.remove_if([bot_owner](const Bot* l) { return (l->GetClass() == Class::Bard && l->GetLevel() < 40); }); + static void Filter_ByHighestPickLock(Client* bot_owner, std::vector& sbl, float& pick_lock_value) { + std::erase_if(sbl, [bot_owner](Bot* l) { return (!MyBots::IsMyBot(bot_owner, l)); }); + std::erase_if(sbl, [bot_owner](Bot* l) { return (l->GetClass() != Class::Rogue && l->GetClass() != Class::Bard); }); + std::erase_if(sbl, [bot_owner](Bot* l) { return (l->GetClass() == Class::Rogue && l->GetLevel() < 5); }); + std::erase_if(sbl, [bot_owner](Bot* l) { return (l->GetClass() == Class::Bard && l->GetLevel() < 40); }); ActionableBots::Filter_ByHighestSkill(bot_owner, sbl, EQ::skills::SkillPickLock, pick_lock_value); } } - -class STBaseEntry; -class STCharmEntry; -class STCureEntry; -class STDepartEntry; -class STEscapeEntry; -class STInvisibilityEntry; -class STMovementSpeedEntry; -class STResistanceEntry; -class STResurrectEntry; -class STSendHomeEntry; -class STSizeEntry; -class STStanceEntry; - -class STBaseEntry -{ -protected: - BCEnum::SpType m_bcst; - -public: - int spell_id; - uint8 spell_level; - uint8 caster_class; - BCEnum::TType target_type; - - // A non-polymorphic constructor requires an appropriate, non-'ST_None' BCEnum::SType - STBaseEntry(BCEnum::SpType init_bcst = BCEnum::SpT_None) { - spell_id = 0; - spell_level = 255; - caster_class = 255; - target_type = BCEnum::TT_None; - m_bcst = init_bcst; - } - STBaseEntry(STBaseEntry* prototype) { - spell_id = prototype->spell_id; - spell_level = 255; - caster_class = 255; - target_type = prototype->target_type; - m_bcst = prototype->BCST(); - } - virtual ~STBaseEntry() { return; }; - - BCEnum::SpType BCST() { return m_bcst; } - - virtual bool IsDerived() { return false; } - - bool IsCharm() const { return (m_bcst == BCEnum::SpT_Charm); } - bool IsCure() const { return (m_bcst == BCEnum::SpT_Cure); } - bool IsDepart() const { return (m_bcst == BCEnum::SpT_Depart); } - bool IsEscape() const { return (m_bcst == BCEnum::SpT_Escape); } - bool IsInvisibility() const { return (m_bcst == BCEnum::SpT_Invisibility); } - bool IsMovementSpeed() const { return (m_bcst == BCEnum::SpT_MovementSpeed); } - bool IsResistance() const { return (m_bcst == BCEnum::SpT_Resistance); } - bool IsResurrect() const { return (m_bcst == BCEnum::SpT_Resurrect); } - bool IsSendHome() const { return (m_bcst == BCEnum::SpT_SendHome); } - bool IsSize() const { return (m_bcst == BCEnum::SpT_Size); } - bool IsStance() const { return (m_bcst == BCEnum::SpT_Stance); } - - virtual STCharmEntry* SafeCastToCharm() { return nullptr; } - virtual STCureEntry* SafeCastToCure() { return nullptr; } - virtual STDepartEntry* SafeCastToDepart() { return nullptr; } - virtual STEscapeEntry* SafeCastToEscape() { return nullptr; } - virtual STInvisibilityEntry* SafeCastToInvisibility() { return nullptr; } - virtual STMovementSpeedEntry* SafeCastToMovementSpeed() { return nullptr; } - virtual STResistanceEntry* SafeCastToResistance() { return nullptr; } - virtual STResurrectEntry* SafeCastToResurrect() { return nullptr; } - virtual STSendHomeEntry* SafeCastToSendHome() { return nullptr; } - virtual STSizeEntry* SafeCastToSize() { return nullptr; } - virtual STStanceEntry* SafeCastToStance() { return nullptr; } -}; - -class STCharmEntry : public STBaseEntry -{ -public: - bool dire; - - STCharmEntry() { - m_bcst = BCEnum::SpT_Charm; - dire = false; - } - STCharmEntry(STCharmEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Charm; - dire = prototype->dire; - } - virtual ~STCharmEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STCharmEntry* SafeCastToCharm() { return ((m_bcst == BCEnum::SpT_Charm) ? (static_cast(this)) : (nullptr)); } -}; - -class STCureEntry : public STBaseEntry -{ -public: - int cure_value[BCEnum::AilmentTypeCount]; - int cure_total; - - STCureEntry() { - m_bcst = BCEnum::SpT_Cure; - memset(&cure_value, 0, (sizeof(int) * BCEnum::AilmentTypeCount)); - cure_total = 0; - } - STCureEntry(STCureEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Cure; - memcpy(&cure_value, prototype->cure_value, (sizeof(int) * BCEnum::AilmentTypeCount)); - cure_total = prototype->cure_total; - } - virtual ~STCureEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STCureEntry* SafeCastToCure() { return ((m_bcst == BCEnum::SpT_Cure) ? (static_cast(this)) : (nullptr)); } -}; - -class STDepartEntry : public STBaseEntry -{ -public: - bool single; - std::string long_name; - - STDepartEntry() { - m_bcst = BCEnum::SpT_Depart; - single = false; - long_name.clear(); - } - STDepartEntry(STDepartEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Depart; - single = prototype->single; - long_name = prototype->long_name; - } - virtual ~STDepartEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STDepartEntry* SafeCastToDepart() { return ((m_bcst == BCEnum::SpT_Depart) ? (static_cast(this)) : (nullptr)); } -}; - -class STEscapeEntry : public STBaseEntry -{ -public: - bool lesser; - - STEscapeEntry() { - m_bcst = BCEnum::SpT_Escape; - lesser = false; - } - STEscapeEntry(STEscapeEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Escape; - lesser = prototype->lesser; - } - virtual ~STEscapeEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STEscapeEntry* SafeCastToEscape() { return ((m_bcst == BCEnum::SpT_Escape) ? (static_cast(this)) : (nullptr)); } -}; - -class STInvisibilityEntry : public STBaseEntry -{ -public: - BCEnum::IType invis_type; - - STInvisibilityEntry() { - m_bcst = BCEnum::SpT_Invisibility; - invis_type = BCEnum::IT_None; - } - STInvisibilityEntry(STInvisibilityEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Invisibility; - invis_type = prototype->invis_type; - } - virtual ~STInvisibilityEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STInvisibilityEntry* SafeCastToInvisibility() { return ((m_bcst == BCEnum::SpT_Invisibility) ? (static_cast(this)) : (nullptr)); } -}; - -class STMovementSpeedEntry : public STBaseEntry -{ -public: - bool group; - - STMovementSpeedEntry() { - m_bcst = BCEnum::SpT_MovementSpeed; - group = false; - } - STMovementSpeedEntry(STMovementSpeedEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_MovementSpeed; - group = prototype->group; - } - virtual ~STMovementSpeedEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STMovementSpeedEntry* SafeCastToMovementSpeed() { return ((m_bcst == BCEnum::SpT_MovementSpeed) ? (static_cast(this)) : (nullptr)); } -}; - -class STResistanceEntry : public STBaseEntry -{ -public: - int resist_value[BCEnum::ResistanceTypeCount]; - int resist_total; - - STResistanceEntry() { - m_bcst = BCEnum::SpT_Resistance; - memset(&resist_value, 0, (sizeof(int) * BCEnum::ResistanceTypeCount)); - resist_total = 0; - } - STResistanceEntry(STResistanceEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Resistance; - memcpy(&resist_value, prototype->resist_value, (sizeof(int) * BCEnum::ResistanceTypeCount)); - resist_total = prototype->resist_total; - } - virtual ~STResistanceEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STResistanceEntry* SafeCastToResistance() { return ((m_bcst == BCEnum::SpT_Resistance) ? (static_cast(this)) : (nullptr)); } -}; - -class STResurrectEntry : public STBaseEntry -{ -public: - bool aoe; - - STResurrectEntry() { - m_bcst = BCEnum::SpT_Resurrect; - aoe = false; - } - STResurrectEntry(STResurrectEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Resurrect; - aoe = prototype->aoe; - } - virtual ~STResurrectEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STResurrectEntry* SafeCastToResurrect() { return ((m_bcst == BCEnum::SpT_Resurrect) ? (static_cast(this)) : (nullptr)); } -}; - -class STSendHomeEntry : public STBaseEntry -{ -public: - bool group; - - STSendHomeEntry() { - m_bcst = BCEnum::SpT_SendHome; - group = false; - } - STSendHomeEntry(STSendHomeEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_SendHome; - group = prototype->group; - } - virtual ~STSendHomeEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STSendHomeEntry* SafeCastToSendHome() { return ((m_bcst == BCEnum::SpT_SendHome) ? (static_cast(this)) : (nullptr)); } -}; - -class STSizeEntry : public STBaseEntry -{ -public: - BCEnum::SzType size_type; - - STSizeEntry() { - m_bcst = BCEnum::SpT_Size; - size_type = BCEnum::SzT_None; - } - STSizeEntry(STSizeEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Size; - size_type = prototype->size_type; - } - virtual ~STSizeEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STSizeEntry* SafeCastToSize() { return ((m_bcst == BCEnum::SpT_Size) ? (static_cast(this)) : (nullptr)); } -}; - -class STStanceEntry : public STBaseEntry { -public: - BCEnum::StType stance_type; - - STStanceEntry() { - m_bcst = BCEnum::SpT_Stance; - stance_type = BCEnum::StT_None; - } - STStanceEntry(STStanceEntry* prototype) : STBaseEntry(prototype) { - m_bcst = BCEnum::SpT_Stance; - stance_type = prototype->stance_type; - } - virtual ~STStanceEntry() { return; }; - - virtual bool IsDerived() { return true; } - - virtual STStanceEntry* SafeCastToStance() { return ((m_bcst == BCEnum::SpT_Stance) ? (static_cast(this)) : (nullptr)); } -}; - - -typedef std::list bcst_list; -typedef std::map bcst_map; - -typedef std::map bcst_required_bot_classes_map; -typedef std::map> bcst_required_bot_classes_map_by_class; - -typedef std::map bcst_levels; -typedef std::map bcst_levels_map; - #define BOT_COMMAND_CHAR '^' typedef void (*BotCmdFuncPtr)(Client *,const Seperator *); @@ -1633,33 +1047,32 @@ int bot_command_real_dispatch(Client *c, char const *message); // Bot Commands void bot_command_actionable(Client *c, const Seperator *sep); -void bot_command_aggressive(Client *c, const Seperator *sep); void bot_command_apply_poison(Client *c, const Seperator *sep); void bot_command_apply_potion(Client* c, const Seperator* sep); void bot_command_attack(Client *c, const Seperator *sep); -void bot_command_bind_affinity(Client *c, const Seperator *sep); +void bot_command_behind_mob(Client* c, const Seperator* sep); +void bot_command_blocked_buffs(Client* c, const Seperator* sep); +void bot_command_blocked_pet_buffs(Client* c, const Seperator* sep); void bot_command_bot(Client *c, const Seperator *sep); -void bot_command_caster_range(Client* c, const Seperator* sep); -void bot_command_charm(Client *c, const Seperator *sep); +void bot_command_bot_settings(Client* c, const Seperator* sep); +void bot_command_cast(Client* c, const Seperator* sep); +void bot_command_discipline(Client* c, const Seperator* sep); +void bot_command_distance_ranged(Client* c, const Seperator* sep); +void bot_command_class_race_list(Client* c, const Seperator* sep); void bot_command_click_item(Client* c, const Seperator* sep); -void bot_command_cure(Client *c, const Seperator *sep); -void bot_command_defensive(Client *c, const Seperator *sep); +void bot_command_copy_settings(Client* c, const Seperator* sep); +void bot_command_default_settings(Client* c, const Seperator* sep); void bot_command_depart(Client *c, const Seperator *sep); -void bot_command_escape(Client *c, const Seperator *sep); void bot_command_find_aliases(Client *c, const Seperator *sep); void bot_command_follow(Client *c, const Seperator *sep); void bot_command_guard(Client *c, const Seperator *sep); void bot_command_heal_rotation(Client *c, const Seperator *sep); void bot_command_help(Client *c, const Seperator *sep); void bot_command_hold(Client *c, const Seperator *sep); -void bot_command_identify(Client *c, const Seperator *sep); +void bot_command_illusion_block(Client* c, const Seperator* sep); void bot_command_inventory(Client *c, const Seperator *sep); -void bot_command_invisibility(Client *c, const Seperator *sep); void bot_command_item_use(Client *c, const Seperator *sep); -void bot_command_levitation(Client *c, const Seperator *sep); -void bot_command_lull(Client *c, const Seperator *sep); -void bot_command_mesmerize(Client *c, const Seperator *sep); -void bot_command_movement_speed(Client *c, const Seperator *sep); +void bot_command_max_melee_range(Client* c, const Seperator* sep); void bot_command_owner_option(Client *c, const Seperator *sep); void bot_command_pet(Client *c, const Seperator *sep); void bot_command_pick_lock(Client *c, const Seperator *sep); @@ -1667,26 +1080,39 @@ void bot_command_pickpocket(Client* c, const Seperator* sep); void bot_command_precombat(Client* c, const Seperator* sep); void bot_command_pull(Client *c, const Seperator *sep); void bot_command_release(Client *c, const Seperator *sep); -void bot_command_resistance(Client *c, const Seperator *sep); -void bot_command_resurrect(Client *c, const Seperator *sep); -void bot_command_rune(Client *c, const Seperator *sep); -void bot_command_send_home(Client *c, const Seperator *sep); -void bot_command_size(Client *c, const Seperator *sep); +void bot_command_sit_hp_percent(Client* c, const Seperator* sep); +void bot_command_sit_in_combat(Client* c, const Seperator* sep); +void bot_command_sit_mana_percent(Client* c, const Seperator* sep); +void bot_command_spell_aggro_checks(Client* c, const Seperator* sep); +void bot_command_spell_announce_cast(Client* c, const Seperator* sep); +void bot_command_spell_delays(Client* c, const Seperator* sep); +void bot_command_spell_engaged_priority(Client* c, const Seperator* sep); +void bot_command_spell_holds(Client* c, const Seperator* sep); +void bot_command_spell_idle_priority(Client* c, const Seperator* sep); +void bot_command_spell_max_hp_pct(Client* c, const Seperator* sep); +void bot_command_spell_max_mana_pct(Client* c, const Seperator* sep); +void bot_command_spell_max_thresholds(Client* c, const Seperator* sep); +void bot_command_spell_min_hp_pct(Client* c, const Seperator* sep); +void bot_command_spell_min_mana_pct(Client* c, const Seperator* sep); +void bot_command_spell_min_thresholds(Client* c, const Seperator* sep); +void bot_command_spell_pursue_priority(Client* c, const Seperator* sep); +void bot_command_spell_resist_limits(Client* c, const Seperator* sep); +void bot_command_spell_target_count(Client* c, const Seperator* sep); void bot_command_spell_list(Client* c, const Seperator *sep); void bot_command_spell_settings_add(Client* c, const Seperator *sep); void bot_command_spell_settings_delete(Client* c, const Seperator *sep); void bot_command_spell_settings_list(Client* c, const Seperator *sep); void bot_command_spell_settings_toggle(Client* c, const Seperator *sep); void bot_command_spell_settings_update(Client* c, const Seperator *sep); +void bot_command_spelltype_ids(Client* c, const Seperator* sep); +void bot_command_spelltype_names(Client* c, const Seperator* sep); void bot_spell_info_dialogue_window(Client* c, const Seperator *sep); void bot_command_enforce_spell_list(Client* c, const Seperator* sep); -void bot_command_summon_corpse(Client *c, const Seperator *sep); void bot_command_suspend(Client *c, const Seperator *sep); void bot_command_taunt(Client *c, const Seperator *sep); void bot_command_timer(Client* c, const Seperator* sep); void bot_command_track(Client *c, const Seperator *sep); void bot_command_view_combos(Client *c, const Seperator *sep); -void bot_command_water_breathing(Client *c, const Seperator *sep); // Bot Subcommands void bot_command_appearance(Client *c, const Seperator *sep); @@ -1706,8 +1132,8 @@ void bot_command_hairstyle(Client *c, const Seperator *sep); void bot_command_heritage(Client *c, const Seperator *sep); void bot_command_inspect_message(Client *c, const Seperator *sep); void bot_command_list_bots(Client *c, const Seperator *sep); -void bot_command_out_of_combat(Client *c, const Seperator *sep); void bot_command_report(Client *c, const Seperator *sep); +void bot_command_set_assistee(Client* c, const Seperator* sep); void bot_command_spawn(Client *c, const Seperator *sep); void bot_command_stance(Client *c, const Seperator *sep); void bot_command_stop_melee_level(Client *c, const Seperator *sep); @@ -1716,12 +1142,11 @@ void bot_command_summon(Client *c, const Seperator *sep); void bot_command_surname(Client *c, const Seperator *sep); void bot_command_tattoo(Client *c, const Seperator *sep); void bot_command_title(Client *c, const Seperator *sep); -void bot_command_toggle_archer(Client *c, const Seperator *sep); void bot_command_toggle_helm(Client *c, const Seperator *sep); +void bot_command_toggle_ranged(Client* c, const Seperator* sep); void bot_command_update(Client *c, const Seperator *sep); void bot_command_woad(Client *c, const Seperator *sep); -void bot_command_circle(Client *c, const Seperator *sep); void bot_command_heal_rotation_adaptive_targeting(Client *c, const Seperator *sep); void bot_command_heal_rotation_add_member(Client *c, const Seperator *sep); void bot_command_heal_rotation_add_target(Client *c, const Seperator *sep); @@ -1749,24 +1174,20 @@ void bot_command_inventory_window(Client *c, const Seperator *sep); void bot_command_pet_get_lost(Client *c, const Seperator *sep); void bot_command_pet_remove(Client *c, const Seperator *sep); void bot_command_pet_set_type(Client *c, const Seperator *sep); -void bot_command_portal(Client *c, const Seperator *sep); // bot command helpers -bool helper_bot_appearance_fail(Client *bot_owner, Bot *my_bot, BCEnum::AFType fail_type, const char* type_desc); +bool helper_bot_appearance_fail(Client *bot_owner, Bot *my_bot, uint8 fail_type, const char* type_desc); void helper_bot_appearance_form_final(Client *bot_owner, Bot *my_bot); void helper_bot_appearance_form_update(Bot *my_bot); uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_class, uint16 bot_race, uint8 bot_gender); -void helper_bot_out_of_combat(Client *bot_owner, Bot *my_bot); int helper_bot_follow_option_chain(Client *bot_owner); -bool helper_cast_standard_spell(Bot* casting_bot, Mob* target_mob, int spell_id, bool annouce_cast = true, uint32* dont_root_before = nullptr); bool helper_command_disabled(Client *bot_owner, bool rule_value, const char *command); bool helper_command_alias_fail(Client *bot_owner, const char* command_handler, const char *alias, const char *command); -void helper_command_depart_list(Client* bot_owner, Bot* druid_bot, Bot* wizard_bot, bcst_list* local_list, bool single_flag = false); bool helper_is_help_or_usage(const char* arg); bool helper_no_available_bots(Client *bot_owner, Bot *my_bot = nullptr); -void helper_send_available_subcommands(Client *bot_owner, const char* command_simile, const std::list& subcommand_list); -void helper_send_usage_required_bots(Client *bot_owner, BCEnum::SpType spell_type, uint8 bot_class = Class::None); -bool helper_spell_check_fail(STBaseEntry* local_entry); -bool helper_spell_list_fail(Client *bot_owner, bcst_list* spell_list, BCEnum::SpType spell_type); +void helper_send_available_subcommands(Client *bot_owner, const char* command_simile, std::vector subcommand_list); +void helper_send_usage_required_bots(Client *bot_owner, uint16 spell_type); +void SendSpellTypeWindow(Client* c, const Seperator* sep); + #endif diff --git a/zone/bot_commands/actionable.cpp b/zone/bot_commands/actionable.cpp index 84965b57d1..94ed0b0c3b 100644 --- a/zone/bot_commands/actionable.cpp +++ b/zone/bot_commands/actionable.cpp @@ -3,22 +3,37 @@ void bot_command_actionable(Client* c, const Seperator* sep) { if (helper_command_alias_fail(c, "bot_command_actionable", sep->arg[0], "actionable")) { + c->Message(Chat::White, "note: Lists actionable command arguments and use descriptions"); + return; } - c->Message(Chat::White, "Actionable command arguments:"); - c->Message(Chat::White, "target - selects target as single bot .. use ^command [target] or imply by empty actionable argument"); - c->Message(Chat::White, "byname [name] - selects single bot by name"); - c->Message(Chat::White, "ownergroup - selects all bots in the owner's group"); - c->Message(Chat::White, "ownerraid - selects all bots in the owner's raid"); - c->Message(Chat::White, "targetgroup - selects all bots in target's group"); - c->Message(Chat::White, "namesgroup [name] - selects all bots in name's group"); - c->Message(Chat::White, "healrotation [name] - selects all member and target bots of a heal rotation where name is a member"); - c->Message(Chat::White, "healrotationmembers [name] - selects all member bots of a heal rotation where name is a member"); - c->Message(Chat::White, "healrotationtargets [name] - selects all target bots of a heal rotation where name is a member"); - c->Message(Chat::White, "byclass - selects all bots of the chosen class"); - c->Message(Chat::White, "byrace - selects all bots of the chosen rsce"); - c->Message(Chat::White, "spawned - selects all spawned bots"); - c->Message(Chat::White, "all - selects all spawned bots .. argument use indicates en masse database updating"); - c->Message(Chat::White, "You may only select your bots as actionable"); + BotCommandHelpParams p; + + p.description = { "Lists actionable command arguments and use descriptions." }; + p.notes = { + "[target] - uses the command on the target. Some commands will default to target if no actionable is selected.", + "[byname] [name] - selects a bot by name their name.", + "[ownergroup] - selects all bots in the owner's group.", + "[ownerraid] - selects all bots in the owner's raid.", + "[targetgroup] - selects all bots in the target's group.", + "[namesgroup] [name] - selects all bots in [name]'s group.", + "[healrotation] [name] - selects all member and target bots of a heal rotation where [name] is a member.", + "[healrotationmembers] [name] - selects all member bots of a heal rotation where [name] is a member.", + "[healrotationtargets] [name] - selects all target bots of a heal rotation where [name] is a member.", + "[mmr] - selects all bots that are currently at max melee range.", + "[byclass] - selects all bots of the chosen class.", + "[byrace] - selects all bots of the chosen race.", + "[spawned] - selects all spawned bots.", + "[all] - selects all spawned bots.", + "
", + "You may only select your own bots." + }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + return; } diff --git a/zone/bot_commands/aggressive.cpp b/zone/bot_commands/aggressive.cpp deleted file mode 100644 index 6a3881d98c..0000000000 --- a/zone/bot_commands/aggressive.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "../bot_command.h" - -void bot_command_aggressive(Client* c, const Seperator* sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Stance]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Stance) || - helper_command_alias_fail(c, "bot_command_aggressive", sep->arg[0], "aggressive")) { - return; - } - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message( - Chat::White, - "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | byclass | byrace | spawned] ([actionable_name]))", - sep->arg[0] - ); - helper_send_usage_required_bots(c, BCEnum::SpT_Stance); - return; - } - const int ab_mask = ActionableBots::ABM_Type1; - - std::string class_race_arg = sep->arg[1]; - bool class_race_check = false; - if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { - class_race_check = true; - } - - std::list sbl; - if (ActionableBots::PopulateSBL( - c, - sep->arg[1], - sbl, - ab_mask, - !class_race_check ? sep->arg[2] : nullptr, - class_race_check ? atoi(sep->arg[2]) : 0 - ) == ActionableBots::ABT_None) { - return; - } - - sbl.remove(nullptr); - - int success_count = 0; - int candidate_count = sbl.size(); - for (auto list_iter: *local_list) { - if (sbl.empty()) { - break; - } - - auto local_entry = list_iter->SafeCastToStance(); - if (helper_spell_check_fail(local_entry)) { - continue; - } - if (local_entry->stance_type != BCEnum::StT_Aggressive) { - continue; - } - - for (auto bot_iter = sbl.begin(); bot_iter != sbl.end();) { - Bot* my_bot = *bot_iter; - if (local_entry->caster_class != my_bot->GetClass()) { - ++bot_iter; - continue; - } - if (local_entry->spell_level > my_bot->GetLevel()) { - ++bot_iter; - continue; - } - - my_bot->InterruptSpell(); - if (candidate_count == 1) { - Bot::BotGroupSay( - my_bot, - fmt::format( - "Using {}.", - spells[local_entry->spell_id].name - ).c_str() - ); - } - - my_bot->UseDiscipline(local_entry->spell_id, my_bot->GetID()); - ++success_count; - - bot_iter = sbl.erase(bot_iter); - } - } - - c->Message( - Chat::White, - "%i of %i bots have attempted to use aggressive disciplines", - success_count, - candidate_count - ); -} diff --git a/zone/bot_commands/appearance.cpp b/zone/bot_commands/appearance.cpp index fcaec025cd..5758846c9e 100644 --- a/zone/bot_commands/appearance.cpp +++ b/zone/bot_commands/appearance.cpp @@ -2,19 +2,19 @@ void bot_command_appearance(Client *c, const Seperator *sep) { - - std::list subcommand_list; - subcommand_list.push_back("botbeardcolor"); - subcommand_list.push_back("botbeardstyle"); - subcommand_list.push_back("botdetails"); - subcommand_list.push_back("botdyearmor"); - subcommand_list.push_back("boteyes"); - subcommand_list.push_back("botface"); - subcommand_list.push_back("bothaircolor"); - subcommand_list.push_back("bothairstyle"); - subcommand_list.push_back("botheritage"); - subcommand_list.push_back("bottattoo"); - subcommand_list.push_back("botwoad"); + std::vector subcommand_list = { + "botbeardcolor", + "botbeardstyle", + "botdetails", + "botdyearmor", + "boteyes", + "botface", + "bothaircolor", + "bothairstyle", + "botheritage", + "bottattoo", + "botwoad" + }; if (helper_command_alias_fail(c, "bot_command_appearance", sep->arg[0], "botappearance")) return; @@ -45,11 +45,11 @@ void bot_command_beard_color(Client *c, const Seperator *sep) uint8 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (my_bot->GetGender() != Gender::Male && my_bot->GetRace() != DWARF) - fail_type = BCEnum::AFT_GenderRace; + fail_type = AFT_GenderRace; else if (!PlayerAppearance::IsValidBeardColor(my_bot->GetRace(), my_bot->GetGender(), uvalue)) - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; else my_bot->SetBeardColor(uvalue); @@ -82,11 +82,11 @@ void bot_command_beard_style(Client *c, const Seperator *sep) uint8 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (my_bot->GetGender() != Gender::Male && my_bot->GetRace() != DWARF) - fail_type = BCEnum::AFT_GenderRace; + fail_type = AFT_GenderRace; else if (!PlayerAppearance::IsValidBeard(my_bot->GetRace(), my_bot->GetGender(), uvalue)) - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; else my_bot->SetBeard(uvalue); @@ -121,11 +121,11 @@ void bot_command_details(Client *c, const Seperator *sep) uint32 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (my_bot->GetRace() != DRAKKIN) - fail_type = BCEnum::AFT_Race; + fail_type = AFT_Race; else if (!PlayerAppearance::IsValidDetail(my_bot->GetRace(), my_bot->GetGender(), uvalue)) - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; else my_bot->SetDrakkinDetails(uvalue); @@ -158,7 +158,7 @@ void bot_command_dye_armor(Client *c, const Seperator *sep) c->Message( Chat::White, fmt::format( - "Usage: {} [Material Slot] [Red: 0-255] [Green: 0-255] [Blue: 0-255] ([actionable: target | byname | ownergroup | targetgroup | namesgroup | healrotation | spawned] ([actionable_name]))", + "Usage: {} [Material Slot] [Red: 0-255] [Green: 0-255] [Blue: 0-255] ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationmembers | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0] ).c_str() ); @@ -214,7 +214,7 @@ void bot_command_dye_armor(Client *c, const Seperator *sep) uint32 rgb_value = ((uint32)red_value << 16) | ((uint32)green_value << 8) | ((uint32)blue_value); - std::list sbl; + std::vector sbl; auto ab_type = ActionableBots::PopulateSBL(c, sep->arg[5], sbl, ab_mask); if (ab_type == ActionableBots::ABT_None) { return; @@ -280,9 +280,9 @@ void bot_command_eyes(Client *c, const Seperator *sep) //else if (!arg2.compare("right")) // eye_bias = 2; - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (!PlayerAppearance::IsValidEyeColor(my_bot->GetRace(), my_bot->GetGender(), uvalue)) { - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; } else { //if (eye_bias == 1) { @@ -327,9 +327,9 @@ void bot_command_face(Client *c, const Seperator *sep) uint8 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (!PlayerAppearance::IsValidFace(my_bot->GetRace(), my_bot->GetGender(), uvalue)) { - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; } else { uint8 old_woad = 0; @@ -367,9 +367,9 @@ void bot_command_hair_color(Client *c, const Seperator *sep) uint8 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (!PlayerAppearance::IsValidHairColor(my_bot->GetRace(), my_bot->GetGender(), uvalue)) - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; else my_bot->SetHairColor(uvalue); @@ -402,9 +402,9 @@ void bot_command_hairstyle(Client *c, const Seperator *sep) uint8 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (!PlayerAppearance::IsValidHair(my_bot->GetRace(), my_bot->GetGender(), uvalue)) - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; else my_bot->SetHairStyle(uvalue); @@ -439,11 +439,11 @@ void bot_command_heritage(Client *c, const Seperator *sep) uint32 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (my_bot->GetRace() != DRAKKIN) - fail_type = BCEnum::AFT_Race; + fail_type = AFT_Race; else if (!PlayerAppearance::IsValidHeritage(my_bot->GetRace(), my_bot->GetGender(), uvalue)) - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; else my_bot->SetDrakkinHeritage(uvalue); @@ -478,11 +478,11 @@ void bot_command_tattoo(Client *c, const Seperator *sep) uint32 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (my_bot->GetRace() != DRAKKIN) - fail_type = BCEnum::AFT_Race; + fail_type = AFT_Race; else if (!PlayerAppearance::IsValidTattoo(my_bot->GetRace(), my_bot->GetGender(), uvalue)) - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; else my_bot->SetDrakkinTattoo(uvalue); @@ -515,12 +515,12 @@ void bot_command_woad(Client *c, const Seperator *sep) uint8 uvalue = Strings::ToInt(sep->arg[1]); - auto fail_type = BCEnum::AFT_None; + auto fail_type = AFT_None; if (my_bot->GetRace() != BARBARIAN) { - fail_type = BCEnum::AFT_Race; + fail_type = AFT_Race; } else if (!PlayerAppearance::IsValidWoad(my_bot->GetRace(), my_bot->GetGender(), uvalue)) { - fail_type = BCEnum::AFT_Value; + fail_type = AFT_Value; } else { uint8 old_face = (my_bot->GetLuclinFace() % 10); diff --git a/zone/bot_commands/apply_poison.cpp b/zone/bot_commands/apply_poison.cpp index cfffff2cb6..444e370ed9 100644 --- a/zone/bot_commands/apply_poison.cpp +++ b/zone/bot_commands/apply_poison.cpp @@ -11,6 +11,7 @@ void bot_command_apply_poison(Client* c, const Seperator* sep) if (helper_is_help_or_usage(sep->arg[1])) { c->Message(Chat::White, "usage: %s", sep->arg[0]); + c->Message(Chat::White, "note: Applies cursor-held poison to a rogue bot's weapon"); return; } diff --git a/zone/bot_commands/attack.cpp b/zone/bot_commands/attack.cpp index 60cad8cd8b..05344da125 100644 --- a/zone/bot_commands/attack.cpp +++ b/zone/bot_commands/attack.cpp @@ -5,17 +5,25 @@ void bot_command_attack(Client *c, const Seperator *sep) if (helper_command_alias_fail(c, "bot_command_attack", sep->arg[0], "attack")) { return; } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [actionable: byname | ownergroup | ownerraid | namesgroup | healrotation | byclass | byrace | default: spawned] ([actionable_name])", sep->arg[0]); + c->Message(Chat::White, "usage: %s [actionable: byname | ownergroup | ownerraid | namesgroup | healrotation | mmr | byclass | byrace | default: spawned] ([actionable_name])", sep->arg[0]); + c->Message(Chat::White, "note: Orders bots to attack a designated target"); return; } - const int ab_mask = ActionableBots::ABM_Type2; + const int ab_mask = ActionableBots::ABM_Type2; Mob* target_mob = ActionableTarget::AsSingle_ByAttackable(c); + if (!target_mob) { - c->Message(Chat::White, "You must an enemy to use this command"); + c->Message(Chat::Yellow, "You must an enemy to use this command"); + return; + } + + if (!c->DoLosChecks(target_mob)) { + c->Message(Chat::Red, "You must have Line of Sight to use this command."); return; } @@ -26,11 +34,13 @@ void bot_command_attack(Client *c, const Seperator *sep) std::string class_race_arg(sep->arg[1]); bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { class_race_check = true; } - std::list sbl; + std::vector sbl; + if (ActionableBots::PopulateSBL(c, ab_arg.c_str(), sbl, ab_mask, !class_race_check ? sep->arg[2] : nullptr, class_race_check ? atoi(sep->arg[2]) : 0) == ActionableBots::ABT_None) { return; } @@ -42,7 +52,7 @@ void bot_command_attack(Client *c, const Seperator *sep) size_t attacker_count = 0; Bot *first_attacker = nullptr; - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); for (auto bot_iter : sbl) { if (bot_iter->GetAppearance() != eaDead && bot_iter->GetBotStance() != Stance::Passive) { @@ -57,8 +67,8 @@ void bot_command_attack(Client *c, const Seperator *sep) } if (attacker_count == 1 && first_attacker) { - Bot::BotGroupSay( - first_attacker, + c->Message( + Chat::Green, fmt::format( "Attacking {}.", target_mob->GetCleanName() @@ -66,7 +76,7 @@ void bot_command_attack(Client *c, const Seperator *sep) ); } else { c->Message( - Chat::White, + Chat::Green, fmt::format( "{} of your bots are attacking {}.", sbl.size(), diff --git a/zone/bot_commands/behind_mob.cpp b/zone/bot_commands/behind_mob.cpp new file mode 100644 index 0000000000..be7e417df1 --- /dev/null +++ b/zone/bot_commands/behind_mob.cpp @@ -0,0 +1,146 @@ +#include "../bot_command.h" + +void bot_command_behind_mob(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_behind_mob", sep->arg[0], "behindmob")) { + c->Message(Chat::White, "note: Toggles whether or not bots will stay behind the mob during combat."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Toggles whether or not bots will stay behind the mob during combat." }; + p.example_format = { fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set Monks to stay behind the mob:", + fmt::format("{} 1 byclass {}", sep->arg[0], Class::Monk) + }; + p.examples_two = + { + "To force all bots to stay behind mobs:", + fmt::format("{} 1 spawned", sep->arg[0]) + }; + p.examples_three = + { + "To check the behind mob status of all bots:", + fmt::format("{} current spawned", sep->arg[0]) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + + int ab_arg = 1; + bool current_check = false; + uint32 type_value = 0; + + if (sep->IsNumber(1)) { + type_value = atoi(sep->arg[1]); + ++ab_arg; + if (type_value < 0 || type_value > 1) { + c->Message(Chat::Yellow, "You must enter either 0 for disabled or 1 for enabled."); + + return; + } + } + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} stay behind mobs.'", + my_bot->GetCleanName(), + my_bot->GetBehindMob() ? "will" : "will not" + ).c_str() + ); + } + else { + my_bot->SetBehindMob(type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} stay behind mobs.'", + first_found->GetCleanName(), + first_found->GetBehindMob() ? "will now" : "will no longer" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots {} stay behind mobs.", + success_count, + type_value ? "will now" : "will no longer" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/bind_affinity.cpp b/zone/bot_commands/bind_affinity.cpp deleted file mode 100644 index e6d01fe4c3..0000000000 --- a/zone/bot_commands/bind_affinity.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "../bot_command.h" - -void bot_command_bind_affinity(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_BindAffinity]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_BindAffinity) || helper_command_alias_fail(c, "bot_command_bind_affinity", sep->arg[0], "bindaffinity")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_BindAffinity); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - for (auto list_iter : *local_list) { - auto local_entry = list_iter; - if (helper_spell_check_fail(local_entry)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - // Cast effect message is not being generated - if (helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id)) - c->Message(Chat::White, "Successfully bound %s to this location", target_mob->GetCleanName()); - else - c->Message(Chat::White, "Failed to bind %s to this location", target_mob->GetCleanName()); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/blocked_buffs.cpp b/zone/bot_commands/blocked_buffs.cpp new file mode 100644 index 0000000000..a8f346a5de --- /dev/null +++ b/zone/bot_commands/blocked_buffs.cpp @@ -0,0 +1,460 @@ +#include "../bot_command.h" + +void bot_command_blocked_buffs(Client* c, const Seperator* sep) +{ + if (!RuleB(Bots, AllowBotBlockedBuffs)) { + c->Message(Chat::Yellow, "This command is disabled."); + + return; + } + + if (helper_command_alias_fail(c, "bot_command_blocked_buffs", sep->arg[0], "blockedbuffs")) { + c->Message(Chat::White, "note: Allows you to set, view and wipe blocked buffs for the selected bots."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Allows you to set, view and wipe blocked buffs for the selected bots." }; + p.notes = { "- You can 'set' spells to be blocked, 'remove' spells from the blocked list, 'list' the current blocked spells or 'wipe' the entire list." }; + p.example_format = { fmt::format("{} [add [ID] | remove [ID] | list | wipe] [actionable, default: target]", sep->arg[0]) }; + p.examples_one = + { + "To add Courage(Spell ID #202) to the targeted bot's blocked list:", + fmt::format("{} add 202", sep->arg[0]) + }; + p.examples_two = + { + "To view the targeted bot's blocked buff list:", + fmt::format("{} list", sep->arg[0]) + }; + p.examples_three = + { + "To wipe all Warriors bots' blocked buff list:", + fmt::format( "{} wipe byclass {}", sep->arg[0], Class::Warrior) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + c->Message( + Chat::Yellow, + fmt::format( + "You can also control bot buffs ({}).", + Saylink::Silent("^blockedbuffs help", "^blockedbuffs") + ).c_str() + ); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool add = false; + bool remove = false; + bool list = false; + bool wipe = false; + uint16 spell_id; + + if (!arg1.compare("add")) { + if (!sep->IsNumber(2) || !IsValidSpell(atoi(sep->arg[2])) || !IsBeneficialSpell(atoi(sep->arg[2]))) { + c->Message(Chat::Yellow, "You must enter a valid spell ID."); + return; + } + + add = true; + spell_id = atoi(sep->arg[2]); + ++ab_arg; + } + else if (!arg1.compare("remove")) { + if (!sep->IsNumber(2) || !IsValidSpell(atoi(sep->arg[2]))) { + c->Message(Chat::Yellow, "You must enter a valid spell ID."); + return; + } + + remove = true; + spell_id = atoi(sep->arg[2]); + ++ab_arg; + } + else if (!arg1.compare("list")) { + list = true; + } + else if (!arg1.compare("wipe")) { + wipe = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "target"; + } + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + bool is_success = false; + uint16 success_count = 0; + Bot* first_found = nullptr; + + for (auto bot_iter : sbl) { + if (!bot_iter->IsInGroupOrRaid(c)) { + continue; + } + + if (!first_found) { + first_found = bot_iter; + } + + if (add) { + bot_iter->SetBotBlockedBuff(spell_id, true); + } + else if (remove) { + bot_iter->SetBotBlockedBuff(spell_id, false); + } + else if (list) { + std::vector blocked_buffs = bot_iter->GetBotBlockedBuffs(); + bool found = false; + + if (!blocked_buffs.empty()) { + for (auto& blocked_buff : blocked_buffs) { + if (blocked_buff.blocked == 1 && IsValidSpell(blocked_buff.spell_id)) { + found = true; + c->Message( + Chat::Yellow, + fmt::format( + "{} says, '{} [#{}] is currently blocked. [{}]'", + bot_iter->GetCleanName(), + spells[blocked_buff.spell_id].name, + blocked_buff.spell_id, + Saylink::Silent(fmt::format("^blockedbuffs remove {}", blocked_buff.spell_id),"Remove") + ).c_str() + ); + } + } + } + + if (!found) { + c->Message( + Chat::Yellow, + fmt::format( + "{} says, 'I am not currently blocking any buffs.'", + bot_iter->GetCleanName() + ).c_str() + ); + } + } + else if (wipe) { + bot_iter->ClearBotBlockedBuffs(); + + c->Message( + Chat::Yellow, + fmt::format( + "{} says, I have wiped my blocked buffs list.'", + bot_iter->GetCleanName() + ).c_str() + ); + } + + is_success = true; + ++success_count; + } + + if (!is_success) { + c->Message(Chat::Yellow, "No bots were selected."); + } + else { + if (add || remove) { + c->Message( + Chat::Yellow, + fmt::format( + "{} {} {} blocking {} [#{}]", + ((success_count == 1 && first_found) ? first_found->GetCleanName() : (fmt::format("{}", success_count).c_str())), + ((success_count == 1 && first_found) ? "is" : "of your bots"), + (add ? "now" : "no longer"), + spells[spell_id].name, + spell_id + ).c_str() + ); + } + } +} + + +void bot_command_blocked_pet_buffs(Client* c, const Seperator* sep) +{ + if (!RuleB(Bots, AllowBotBlockedBuffs)) { + c->Message(Chat::Yellow, "This command is disabled."); + + return; + } + + if (helper_command_alias_fail(c, "bot_command_blocked_pet_buffs", sep->arg[0], "blockedpetbuffs")) { + c->Message(Chat::White, "note: Allows you to set, view and wipe blocked pet buffs for the selected bots."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Allows you to set, view and wipe blocked pet buffs for the selected bots." }; + p.notes = + { + "- You can 'set' spells to be blocked, 'remove' spells from the blocked list, 'list' the current blocked spells or 'wipe' the entire list.", + "- This controls whether or not any pet the selected bot(s) own will prevent certain beneficial buffs from landing on them." + }; + p.example_format = { fmt::format("{} [add [ID] | remove [ID] | list | wipe] [actionable, default: target]", sep->arg[0]) }; + p.examples_one = + { + "To add Courage (Spell ID #202) to the targeted bot's blocked list:", + fmt::format( + "{} add 202", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ) + }; + p.examples_two = + { + "To view the targeted bot's blocked buff list:", + fmt::format( + "{} list", + sep->arg[0] + ) + }; + p.examples_three = + { + "To wipe all Warriors bots' blocked buff list:", + fmt::format( + "{} wipe byclass {}", + sep->arg[0], + Class::Warrior + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + c->Message( + Chat::Yellow, + fmt::format( + "You can also control pet buffs ({}).", + Saylink::Silent("^blockedpetbuffs help", "^blockedpetbuffs") + ).c_str() + ); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool add = false; + bool remove = false; + bool list = false; + bool wipe = false; + uint16 spell_id; + + if (!arg1.compare("add")) { + if (!sep->IsNumber(2) || !IsValidSpell(atoi(sep->arg[2])) || !IsBeneficialSpell(atoi(sep->arg[2]))) { + c->Message(Chat::Yellow, "You must enter a valid spell ID."); + return; + } + + add = true; + spell_id = atoi(sep->arg[2]); + ++ab_arg; + } + else if (!arg1.compare("remove")) { + if (!sep->IsNumber(2) || !IsValidSpell(atoi(sep->arg[2]))) { + c->Message(Chat::Yellow, "You must enter a valid spell ID."); + return; + } + + remove = true; + spell_id = atoi(sep->arg[2]); + ++ab_arg; + } + else if (!arg1.compare("list")) { + list = true; + } + else if (!arg1.compare("wipe")) { + wipe = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "target"; + } + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + bool is_success = false; + uint16 success_count = 0; + Bot* first_found = nullptr; + + for (auto bot_iter : sbl) { + if (!bot_iter->IsInGroupOrRaid(c)) { + continue; + } + + if (!first_found) { + first_found = bot_iter; + } + + if (add) { + bot_iter->SetBotBlockedPetBuff(spell_id, true); + } + else if (remove) { + bot_iter->SetBotBlockedPetBuff(spell_id, false); + } + else if (list) { + std::vector blocked_buffs = bot_iter->GetBotBlockedBuffs(); + bool found = false; + + if (!blocked_buffs.empty()) { + for (auto& blocked_buff : blocked_buffs) { + if (blocked_buff.blocked_pet == 1 && IsValidSpell(blocked_buff.spell_id)) { + found = true; + c->Message( + Chat::Yellow, + fmt::format( + "{} says, '{} [#{}] is currently blocked for my pet. [{}]'", + bot_iter->GetCleanName(), + spells[blocked_buff.spell_id].name, + blocked_buff.spell_id, + Saylink::Silent(fmt::format("^blockedpetbuffs remove {}", blocked_buff.spell_id), "Remove") + ).c_str() + ); + } + } + } + + if (!found) { + c->Message( + Chat::Yellow, + fmt::format( + "{} says, 'I am not currently blocking any pet buffs.'", + bot_iter->GetCleanName() + ).c_str() + ); + } + } + else if (wipe) { + bot_iter->ClearBotBlockedBuffs(); + + c->Message( + Chat::Yellow, + fmt::format( + "{} says, I have wiped my blocked buffs list.'", + bot_iter->GetCleanName() + ).c_str() + ); + } + + is_success = true; + ++success_count; + } + + if (!is_success) { + c->Message(Chat::Yellow, "No bots were selected."); + } + else { + if (add || remove) { + c->Message( + Chat::Yellow, + fmt::format( + "{} {} {} blocking {} [#{}] on their pet.", + ((success_count == 1 && first_found) ? first_found->GetCleanName() : (fmt::format("{}", success_count).c_str())), + ((success_count == 1 && first_found) ? "is" : "of your bots"), + (add ? "now" : "no longer"), + spells[spell_id].name, + spell_id + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/bot.cpp b/zone/bot_commands/bot.cpp index 5d2bfe18e5..4c22dd379a 100644 --- a/zone/bot_commands/bot.cpp +++ b/zone/bot_commands/bot.cpp @@ -2,25 +2,25 @@ void bot_command_bot(Client *c, const Seperator *sep) { - - std::list subcommand_list; - subcommand_list.push_back("botappearance"); - subcommand_list.push_back("botcamp"); - subcommand_list.push_back("botclone"); - subcommand_list.push_back("botcreate"); - subcommand_list.push_back("botdelete"); - subcommand_list.push_back("botfollowdistance"); - subcommand_list.push_back("botinspectmessage"); - subcommand_list.push_back("botlist"); - subcommand_list.push_back("botoutofcombat"); - subcommand_list.push_back("botreport"); - subcommand_list.push_back("botspawn"); - subcommand_list.push_back("botstance"); - subcommand_list.push_back("botstopmeleelevel"); - subcommand_list.push_back("botsummon"); - subcommand_list.push_back("bottogglearcher"); - subcommand_list.push_back("bottogglehelm"); - subcommand_list.push_back("botupdate"); + std::vector subcommand_list = { + "botappearance", + "botcamp", + "botclone", + "botcreate", + "botdelete", + "botfollowdistance", + "botinspectmessage", + "botlist", + "botoutofcombat", + "botreport", + "botspawn", + "botstance", + "botstopmeleelevel", + "botsummon", + "bottoggleranged", + "bottogglehelm", + "botupdate" + }; if (helper_command_alias_fail(c, "bot_command_bot", sep->arg[0], "bot")) return; @@ -30,12 +30,19 @@ void bot_command_bot(Client *c, const Seperator *sep) void bot_command_camp(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_camp", sep->arg[0], "botcamp")) + if (helper_command_alias_fail(c, "bot_command_camp", sep->arg[0], "botcamp")) { return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); + return; + } + + if (!Bot::CheckCampSpawnConditions(c)) { return; } + const int ab_mask = ActionableBots::ABM_Type1; std::string class_race_arg = sep->arg[1]; @@ -44,13 +51,21 @@ void bot_command_camp(Client *c, const Seperator *sep) class_race_check = true; } - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, !class_race_check ? sep->arg[2] : nullptr, class_race_check ? atoi(sep->arg[2]) : 0) == ActionableBots::ABT_None) { return; } - for (auto bot_iter : sbl) + uint16 camp_count; + + for (auto bot_iter : sbl) { bot_iter->Camp(); + ++camp_count; + } + + if (camp_count) { + c->Message(Chat::White, "%i of your bots have been camped.", camp_count); + } } void bot_command_clone(Client *c, const Seperator *sep) @@ -106,35 +121,29 @@ void bot_command_clone(Client *c, const Seperator *sep) if (!Bot::IsValidName(bot_name)) { c->Message( - Chat::White, + Chat::Yellow, fmt::format( - "'{}' is an invalid name. You may only use characters 'A-Z', 'a-z' and '_'.", - bot_name + "'{}' is an invalid name. You may only use characters 'A-Z' or 'a-z' and it must be between 4 and 15 characters. Mixed case {} allowed.", + bot_name, RuleB(Bots, AllowCamelCaseNames) ? "is" : "is not" ).c_str() ); + return; } bool available_flag = false; - if (!database.botdb.QueryNameAvailablity(bot_name, available_flag)) { - c->Message( - Chat::White, - fmt::format( - "Failed to query name availability for '{}'.", - bot_name - ).c_str() - ); - return; - } + + !database.botdb.QueryNameAvailablity(bot_name, available_flag); if (!available_flag) { c->Message( - Chat::White, + Chat::Yellow, fmt::format( - "The name '{}' is already being used. Please choose a different name.", + "The name '{}' is already being used or prohibited. Please choose a different name", bot_name ).c_str() ); + return; } @@ -428,12 +437,25 @@ void bot_command_delete(Client *c, const Seperator *sep) return; } + if (!Bot::CheckCampSpawnConditions(c)) { + return; + } + auto my_bot = ActionableBots::AsTarget_ByBot(c); if (!my_bot) { c->Message(Chat::White, "You must a bot that you own to use this command"); return; } + std::string delete_confirm = sep->arg[1]; + + std::string deleted_check = "confirm"; + + if (!(delete_confirm.find(deleted_check) != std::string::npos)) { + c->Message(Chat::White, "You must type %s confirm to confirm the deletion of %s.", sep->arg[0], my_bot->GetCleanName()); + return; + } + if (!my_bot->DeleteBot()) { c->Message(Chat::White, "Failed to delete '%s' due to database error", my_bot->GetCleanName()); return; @@ -449,65 +471,164 @@ void bot_command_delete(Client *c, const Seperator *sep) void bot_command_follow_distance(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_follow_distance", sep->arg[0], "botfollowdistance")) + if (helper_command_alias_fail(c, "bot_command_follow_distance", sep->arg[0], "botfollowdistance")) { + c->Message(Chat::White, "note: Sets or resets the follow distance of the selected bots."); + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [set] [distance] ([actionable: target | byname | ownergroup | targetgroup | namesgroup | healrotation | spawned] ([actionable_name]))", sep->arg[0]); - c->Message(Chat::White, "usage: %s [clear] ([actionable: target | byname | ownergroup | targetgroup | namesgroup | healrotation | spawned] ([actionable_name]))", sep->arg[0]); + BotCommandHelpParams p; + + p.description = { "Sets or resets the follow distance of the selected bots." }; + p.notes = + { + fmt::format("[Default]: {}", RuleI(Bots, MaxFollowDistance)), + fmt::format("- You must use a value between 1 and {}.", RuleI(Bots, MaxFollowDistance)) + }; + p.example_format = { fmt::format("{} [reset]/[set [value]] [actionable]", sep->arg[0]) }; + p.examples_one = { + "To set all bots to follow at a distance of 25:", + fmt::format("{} set 25 spawned", sep->arg[0]) + }; + p.examples_two = { + "To check the curret following distance of all bots:", + fmt::format("{} current spawned", sep->arg[0]) + }; + p.examples_three = + { + "To reset the following distance of all Wizards:", + fmt::format( + "{} reset byclass {}", + sep->arg[0], + Class::Wizard + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + return; } - const int ab_mask = ActionableBots::ABM_NoFilter; - uint32 bfd = BOT_FOLLOW_DISTANCE_DEFAULT; + const int ab_mask = ActionableBots::ABM_Type2; + + uint32 bfd = RuleI(Bots, DefaultFollowDistance); bool set_flag = false; + bool current_check = false; int ab_arg = 2; - if (!strcasecmp(sep->arg[1], "set")) { + std::string arg1 = sep->arg[1]; + + if (!arg1.compare("set")) { if (!sep->IsNumber(2)) { - c->Message(Chat::White, "A numeric [distance] is required to use this command"); + c->Message(Chat::Yellow, "You must enter a value between 1 and %i.", RuleI(Bots, MaxFollowDistance)); + return; } bfd = Strings::ToInt(sep->arg[2]); - if (bfd < 1) - bfd = 1; - if (bfd > BOT_FOLLOW_DISTANCE_DEFAULT_MAX) - bfd = BOT_FOLLOW_DISTANCE_DEFAULT_MAX; + + if (bfd < 1) { + c->Message(Chat::Yellow, "You must enter a value between 1 and %i.", RuleI(Bots, MaxFollowDistance)); + + return; + } + + if (bfd > RuleI(Bots, MaxFollowDistance)) { + c->Message(Chat::Yellow, "You must enter a value between 1 and %i.", RuleI(Bots, MaxFollowDistance)); + + return; + } + set_flag = true; - ab_arg = 3; + ++ab_arg; } - else if (strcasecmp(sep->arg[1], "clear")) { - c->Message(Chat::White, "This command requires a [set | clear] argument"); + else if (!arg1.compare("current")) { + current_check = true; + } + else if (arg1.compare("reset")) { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + return; } - std::list sbl; - auto ab_type = ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, sep->arg[(ab_arg + 1)]); - if (ab_type == ActionableBots::ABT_None) + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); int bot_count = 0; for (auto bot_iter : sbl) { - if (!bot_iter) + if (!bot_iter) { continue; + } - bot_iter->SetFollowDistance(bfd); - if (ab_type != ActionableBots::ABT_All && !database.botdb.SaveFollowDistance(bot_iter->GetBotID(), bfd)) { - return; + if (current_check) { + Mob* follow_mob = entity_list.GetMob(bot_iter->GetFollowID()); + + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I am currently following {} at a distance of {}.'", + bot_iter->GetCleanName(), + follow_mob ? follow_mob->GetCleanName() : "no one", + sqrt(bot_iter->GetFollowDistance()) + ).c_str() + ); + } + else { + bot_iter->SetFollowDistance(bfd * bfd); + ++bot_count; } + } - ++bot_count; + if (current_check) { + return; } - if (ab_type == ActionableBots::ABT_All) { - if (!database.botdb.SaveAllFollowDistances(c->CharacterID(), bfd)) { - return; - } + if (bot_count == 1) { + Mob* follow_mob = entity_list.GetMob(sbl.front()->GetFollowID()); - c->Message(Chat::White, "%s all of your bot follow distances", set_flag ? "Set" : "Cleared"); + c->Message( + Chat::Green, + fmt::format( + "{} says, 'Following {} at a distance of {}.'", + sbl.front()->GetCleanName(), + follow_mob ? follow_mob->GetCleanName() : "you", + bfd + ).c_str() + ); } else { - c->Message(Chat::White, "%s %i of your spawned bot follow distances", (set_flag ? "Set" : "Cleared"), bot_count); + c->Message( + Chat::Green, + fmt::format( + "{} of your bots are now following at a distance of {}.", + bot_count, + bfd + ).c_str() + ); } } @@ -516,7 +637,7 @@ void bot_command_inspect_message(Client *c, const Seperator *sep) if (helper_command_alias_fail(c, "bot_command_inspect_message", sep->arg[0], "botinspectmessage")) return; if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [set | clear] ([actionable: target | byname | ownergroup | targetgroup | namesgroup | healrotation | spawned] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "usage: %s [set | clear] ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationmembers | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); c->Message(Chat::White, "Notes:"); if (c->ClientVersion() >= EQ::versions::ClientVersion::SoF) { c->Message(Chat::White, "- Self-inspect and type your bot's inspect message"); @@ -542,7 +663,7 @@ void bot_command_inspect_message(Client *c, const Seperator *sep) return; } - std::list sbl; + std::vector sbl; auto ab_type = ActionableBots::PopulateSBL(c, sep->arg[2], sbl, ab_mask, sep->arg[3]); if (ab_type == ActionableBots::ABT_None) return; @@ -764,75 +885,31 @@ void bot_command_list_bots(Client *c, const Seperator *sep) } } -void bot_command_out_of_combat(Client *c, const Seperator *sep) +void bot_command_report(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_out_of_combat", sep->arg[0], "botoutofcombat")) + if (helper_command_alias_fail(c, "bot_command_report", sep->arg[0], "botreport")) return; if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([option: on | off]) ([actionable: target | byname] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationmembers | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); return; } - const int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_ByName); + + const int ab_mask = ActionableBots::ABM_Type1; std::string arg1 = sep->arg[1]; - - bool behavior_state = false; - bool toggle_behavior = true; int ab_arg = 1; - if (!arg1.compare("on")) { - behavior_state = true; - toggle_behavior = false; - ab_arg = 2; - } - else if (!arg1.compare("off")) { - toggle_behavior = false; - ab_arg = 2; - } - - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, sep->arg[(ab_arg + 1)]) == ActionableBots::ABT_None) - return; - - for (auto bot_iter : sbl) { - if (!bot_iter) - continue; - if (toggle_behavior) - bot_iter->SetAltOutOfCombatBehavior(!bot_iter->GetAltOutOfCombatBehavior()); - else - bot_iter->SetAltOutOfCombatBehavior(behavior_state); + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; - helper_bot_out_of_combat(c, bot_iter); + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; } -} -void bot_command_report(Client *c, const Seperator *sep) -{ - if (helper_command_alias_fail(c, "bot_command_report", sep->arg[0], "botreport")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | targetgroup | namesgroup | healrotation | spawned] ([actionable_name]))", sep->arg[0]); + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - const int ab_mask = ActionableBots::ABM_NoFilter; - - std::string ab_type_arg = sep->arg[1]; - if (ab_type_arg.empty()) { - auto t = c->GetTarget(); - if (t && t->IsClient()) { - if (t->CastToClient() == c) { - ab_type_arg = "ownergroup"; - } else { - ab_type_arg = "targetgroup"; - } - } else { - ab_type_arg = "spawned"; - } - } - - std::list sbl; - if (ActionableBots::PopulateSBL(c, ab_type_arg, sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) - return; for (auto bot_iter : sbl) { if (!bot_iter) @@ -865,6 +942,7 @@ void bot_command_spawn(Client *c, const Seperator *sep) } auto bot_character_level = c->GetBotRequiredLevel(); + if ( bot_character_level >= 0 && c->GetLevel() < bot_character_level && @@ -880,7 +958,7 @@ void bot_command_spawn(Client *c, const Seperator *sep) return; } - if (!Bot::CheckSpawnConditions(c)) { + if (!Bot::CheckCampSpawnConditions(c)) { return; } @@ -891,8 +969,9 @@ void bot_command_spawn(Client *c, const Seperator *sep) bot_spawn_limit >= 0 && spawned_bot_count >= bot_spawn_limit && !c->GetGM() - ) { + ) { std::string message; + if (bot_spawn_limit) { message = fmt::format( "You cannot have more than {} spawned bot{}.", @@ -916,6 +995,7 @@ void bot_command_spawn(Client *c, const Seperator *sep) uint32 bot_id = 0; uint8 bot_class = Class::None; + if (!database.botdb.LoadBotID(bot_name, bot_id, bot_class)) { c->Message( Chat::White, @@ -934,7 +1014,7 @@ void bot_command_spawn(Client *c, const Seperator *sep) bot_spawn_limit_class >= 0 && spawned_bot_count_class >= bot_spawn_limit_class && !c->GetGM() - ) { + ) { std::string message; if (bot_spawn_limit_class) { @@ -956,11 +1036,12 @@ void bot_command_spawn(Client *c, const Seperator *sep) } auto bot_character_level_class = c->GetBotRequiredLevel(bot_class); + if ( bot_character_level_class >= 0 && c->GetLevel() < bot_character_level_class && !c->GetGM() - ) { + ) { c->Message( Chat::White, fmt::format( @@ -995,6 +1076,7 @@ void bot_command_spawn(Client *c, const Seperator *sep) } auto my_bot = Bot::LoadBot(bot_id); + if (!my_bot) { c->Message( Chat::White, @@ -1016,6 +1098,7 @@ void bot_command_spawn(Client *c, const Seperator *sep) bot_id ).c_str() ); + safe_delete(my_bot); return; } @@ -1046,139 +1129,399 @@ void bot_command_spawn(Client *c, const Seperator *sep) } std::string silent_confirm = sep->arg[2]; - bool silentTell = false; + bool silent_tell = false; if (!silent_confirm.compare("silent")) { - silentTell = true; + silent_tell = true; } - if (!silentTell && c->GetBotOption(Client::booSpawnMessageSay)) { - Bot::BotGroupSay(my_bot, bot_spawn_message[message_index].c_str()); + if (!silent_tell && c->GetBotOption(Client::booSpawnMessageSay)) { + Bot::RaidGroupSay(my_bot, bot_spawn_message[message_index].c_str()); } - else if (!silentTell && c->GetBotOption(Client::booSpawnMessageTell)) { + else if (!silent_tell && c->GetBotOption(Client::booSpawnMessageTell)) { my_bot->OwnerMessage(bot_spawn_message[message_index]); } } void bot_command_stance(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_stance", sep->arg[0], "botstance")) + if (helper_command_alias_fail(c, "bot_command_stance", sep->arg[0], "botstance")) { + c->Message(Chat::White, "note: Change a bot's stance to control the way it behaves."); + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [current | value: 1-9] ([actionable: target | byname] ([actionable_name]))", sep->arg[0]); - c->Message( - Chat::White, + BotCommandHelpParams p; + + p.description = { "Change a bot's stance to control the way it behaves." }; + p.notes = + { + "- Changing a stance will reset all settings to match that stance type.", + "- Any changes made will only save to that stance for future use.", fmt::format( - "Value: {} ({}), {} ({}), {} ({}), {} ({}), {} ({}), {} ({}), {} ({}), {} ({}), {} ({})", - Stance::Passive, + "- {} (#{}) will tell Non-Warrior classes to taunt automatically.", + Stance::GetName(Stance::Aggressive), + Stance::Aggressive + ), + "
", + "Available stances:", + fmt::format( + "{} (#{}), {} (#{}), {} (#{}), {} (#{}), {} (#{}), {} (#{}), {} (#{})", Stance::GetName(Stance::Passive), - Stance::Balanced, + Stance::Passive, Stance::GetName(Stance::Balanced), - Stance::Efficient, + Stance::Balanced, Stance::GetName(Stance::Efficient), - Stance::Reactive, - Stance::GetName(Stance::Reactive), - Stance::Aggressive, + Stance::Efficient, Stance::GetName(Stance::Aggressive), - Stance::Assist, + Stance::Aggressive, Stance::GetName(Stance::Assist), + Stance::Assist, + Stance::GetName(Stance::Burn), Stance::Burn, + Stance::GetName(Stance::AEBurn), + Stance::AEBurn + ), + "
", + fmt::format( + "- {} (#{}) [Default] - Overall balance and casts most spell types by default.", + Stance::GetName(Stance::Balanced), + Stance::Balanced + ), + fmt::format( + "- {} (#{}) - Idle. Does not cast or engage in combat.", + Stance::GetName(Stance::Passive), + Stance::Passive + ), + fmt::format( + "- {} (#{}) - More mana and aggro efficient (SKs will still cast hate line). Longer delays between detrimental spells, thresholds adjusted to cast less often.", + Stance::GetName(Stance::Efficient), + Stance::Efficient + ), + fmt::format( + "- {} (#{}) - Much more aggressive in their cast times and thresholds. More DPS, debuffs and slow but a higher risk of snagging aggro.", + Stance::GetName(Stance::Aggressive), + Stance::Aggressive + ), + fmt::format( + "- {} (#{}) - Support role. Most offensive spell types are disabled. Focused on heals, cures, CC, debuffs and slows.", + Stance::GetName(Stance::Assist), + Stance::Assist + ), + fmt::format( + "- {} (#{}) - Murder. Doesn't care about aggro, just wants to kill. DPS Machine.", Stance::GetName(Stance::Burn), - Stance::Efficient2, - Stance::GetName(Stance::Efficient2), - Stance::AEBurn, - Stance::GetName(Stance::AEBurn) - ).c_str() - ); + Stance::Burn + ), + fmt::format( + "- {} (#{}) - Murder EVERYTHING. Doesn't care about aggro, casts AEs. Everything must die ASAP.", + Stance::GetName(Stance::AEBurn), + Stance::AEBurn + ) + }; + p.example_format = + { fmt::format( "{} [current | value]", sep->arg[0]) }; + p.examples_one = + { + "To set all bots to BurnAE:", + fmt::format("{} {} spawned {}", + sep->arg[0], + Stance::Aggressive, + Class::ShadowKnight + ) + }; + p.examples_two = + { + "To set all Shadowknights to Aggressive:", + fmt::format("{} {} byclass {}", + sep->arg[0], + Stance::Aggressive, + Class::ShadowKnight + ) + }; + p.examples_three = { + "To check the current stances of all bots:", + fmt::format("{} current spawned", sep->arg[0]) + }; + + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + return; } - int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_ByName); - bool current_flag = false; - uint8 bst = Stance::Unknown; + const int ab_mask = ActionableBots::ABM_Type1; - if (!strcasecmp(sep->arg[1], "current")) - current_flag = true; - else if (sep->IsNumber(1)) { - bst = static_cast(Strings::ToUnsignedInt(sep->arg[1])); - if (!Stance::IsValid(bst)) { - bst = Stance::Unknown; + bool current_check = false; + int ab_arg = 1; + uint32 value = 0; + + std::string arg1 = sep->arg[1]; + + if (sep->IsNumber(1)) { + ++ab_arg; + value = atoi(sep->arg[1]); + if ( + value < Stance::Passive || + value > Stance::AEBurn || + value == Stance::Reactive || + value == Stance::Assist + ) { + c->Message( + Chat::Yellow, + fmt::format( + "You must choose a valid stance ID, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; } } + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); - if (!current_flag && bst == Stance::Unknown) { - c->Message(Chat::White, "A [current] argument or valid numeric [value] is required to use this command"); return; } - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[2], sbl, ab_mask, sep->arg[3]) == ActionableBots::ABT_None) + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; + } + Bot* first_found = nullptr; + int success_count = 0; for (auto bot_iter : sbl) { - if (!bot_iter) + if (!first_found) { + first_found = bot_iter; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My current stance is {} ({}).'", + bot_iter->GetCleanName(), + Stance::GetName(bot_iter->GetBotStance()), + bot_iter->GetBotStance() + ).c_str() + ); + continue; + } + + bot_iter->Save(); + bot_iter->SetBotStance(value); + bot_iter->LoadDefaultBotSettings(); + database.botdb.LoadBotSettings(bot_iter); - if (!current_flag) { - bot_iter->SetBotStance(bst); - bot_iter->Save(); + if ( + (bot_iter->GetClass() == Class::Warrior || bot_iter->GetClass() == Class::Paladin || bot_iter->GetClass() == Class::ShadowKnight) && + (bot_iter->GetBotStance() == Stance::Aggressive) + ) { + bot_iter->SetTaunting(true); + + if (bot_iter->HasPet() && bot_iter->GetPet()->GetSkill(EQ::skills::SkillTaunt)) { + bot_iter->GetPet()->CastToNPC()->SetTaunting(true); + } } + else { + bot_iter->SetTaunting(false); + + if (bot_iter->HasPet() && bot_iter->GetPet()->GetSkill(EQ::skills::SkillTaunt)) { + bot_iter->GetPet()->CastToNPC()->SetTaunting(false); + } + } + + bot_iter->Save(); + ++success_count; + } + + if (current_check) { + return; + } - Bot::BotGroupSay( - bot_iter, + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, fmt::format( - "My current stance is {} ({}).", - Stance::GetName(bot_iter->GetBotStance()), - bot_iter->GetBotStance() + "{} says, 'I am now set to the stance [{}].'", + first_found->GetCleanName(), + Stance::GetName(value) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots are now set to the stance [{}].", + success_count, + Stance::GetName(value) ).c_str() ); } } -void bot_command_stop_melee_level(Client *c, const Seperator *sep) +void bot_command_stop_melee_level(Client* c, const Seperator* sep) { - if (helper_command_alias_fail(c, "bot_command_stop_melee_level", sep->arg[0], "botstopmeleelevel")) + if (helper_command_alias_fail(c, "bot_command_stop_melee_level", sep->arg[0], "botstopmeleelevel")) { return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [current | reset | sync | value: 0-255]", sep->arg[0]); - c->Message(Chat::White, "note: Only caster or hybrid class bots may be modified"); + c->Message(Chat::White, "usage: %s [current | reset | sync | value: 0-255] ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); c->Message(Chat::White, "note: Use [reset] to set stop melee level to server rule"); c->Message(Chat::White, "note: Use [sync] to set stop melee level to current bot level"); return; } - auto my_bot = ActionableBots::AsTarget_ByBot(c); - if (!my_bot) { - c->Message(Chat::White, "You must a bot that you own to use this command"); - return; - } - if (!IsCasterClass(my_bot->GetClass()) && !IsHybridClass(my_bot->GetClass())) { - c->Message(Chat::White, "You must a caster or hybrid class bot to use this command"); - return; - } + const int ab_mask = ActionableBots::ABM_Type1; + std::string arg1 = sep->arg[1]; + int ab_arg = 1; uint8 sml = RuleI(Bots, CasterStopMeleeLevel); + bool sync_sml = false; + bool reset_sml = false; + bool current_check = false; if (sep->IsNumber(1)) { + ab_arg = 2; sml = Strings::ToInt(sep->arg[1]); + if (sml <= 0 || sml > 255) { + c->Message(Chat::White, "You must provide a value between 0-255."); + return; + } } else if (!strcasecmp(sep->arg[1], "sync")) { - sml = my_bot->GetLevel(); + ab_arg = 2; + sync_sml = true; } - else if (!strcasecmp(sep->arg[1], "current")) { - c->Message(Chat::White, "My current melee stop level is %u", my_bot->GetStopMeleeLevel()); + else if (!arg1.compare("current")) { + ab_arg = 2; + current_check = true; + } + else if (!strcasecmp(sep->arg[1], "reset")) { + ab_arg = 2; + reset_sml = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + return; } - else if (strcasecmp(sep->arg[1], "reset")) { - c->Message(Chat::White, "A [current] or [reset] argument, or numeric [value] is required to use this command"); + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - // [reset] falls through with initialization value - my_bot->SetStopMeleeLevel(sml); - database.botdb.SaveStopMeleeLevel(my_bot->GetBotID(), sml); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (sync_sml) { + sml = my_bot->GetLevel(); + } + + if (reset_sml) { + sml = my_bot->GetDefaultBotBaseSetting(BotBaseSettings::StopMeleeLevel); + } - c->Message(Chat::White, "Successfully set stop melee level for %s to %u", my_bot->GetCleanName(), sml); + if (current_check) { + c->Message( + Chat::White, + fmt::format( + "{} says, 'My current stop melee level is {}.'", + my_bot->GetCleanName(), + my_bot->GetStopMeleeLevel() + ).c_str() + ); + continue; + } + else { + my_bot->SetStopMeleeLevel(sml); + ++success_count; + } + } + + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::White, + fmt::format( + "{} says, 'My stop melee level was {} to {}.'", + first_found->GetCleanName(), + reset_sml ? "reset" : "set", + sml + ).c_str() + ); + } + else { + c->Message( + Chat::White, + fmt::format( + "{} of your bots {} their stop melee{}'", // level to {}. + success_count, + reset_sml ? "reset" : "set", + fmt::format("{}", reset_sml ? "." : fmt::format(" level to {}.", sml).c_str()).c_str(), + sml + ).c_str() + ); + } + } } void bot_command_summon(Client *c, const Seperator *sep) @@ -1188,23 +1531,28 @@ void bot_command_summon(Client *c, const Seperator *sep) } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); return; } + const int ab_mask = ActionableBots::ABM_Type1; - std::string class_race_arg = sep->arg[1]; + std::string arg1 = sep->arg[1]; + int ab_arg = 1; + + std::string class_race_arg = sep->arg[ab_arg]; bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { class_race_check = true; } - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, !class_race_check ? sep->arg[2] : nullptr, class_race_check ? atoi(sep->arg[2]) : 0) == ActionableBots::ABT_None) { + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); for (auto bot_iter : sbl) { if (!bot_iter) { @@ -1245,127 +1593,331 @@ void bot_command_summon(Client *c, const Seperator *sep) } } -void bot_command_toggle_archer(Client *c, const Seperator *sep) +void bot_command_toggle_ranged(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_toggle_archer", sep->arg[0], "bottogglearcher")) { + if (helper_command_alias_fail(c, "bot_command_toggle_ranged", sep->arg[0], "bottoggleranged")) { + c->Message(Chat::White, "note: Toggles a ranged bot between melee and ranged weapon use."); + return; } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([option: on | off]) ([actionable: target | byname] ([actionable_name]))", sep->arg[0]); + BotCommandHelpParams p; + + p.description = { "Toggles a ranged bot between melee and ranged weapon use." }; + p.example_format = { fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set BotA to use their ranged:", + fmt::format( + "{} 1 byname BotA", + sep->arg[0] + ) + }; + p.examples_two = + { + "To set all ranger bots to ranged:", + fmt::format( + "{} 1 byclass {}", + sep->arg[0], + Class::Ranger + ) + }; + p.examples_two = + { + "To check the ranged status of all bots:", + fmt::format( + "{} current spawned", + sep->arg[0], + Class::Ranger + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + return; } - const int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_ByName); - + std::string arg1 = sep->arg[1]; - bool archer_state = false; - bool toggle_archer = true; int ab_arg = 1; - if (!arg1.compare("on")) { - archer_state = true; - toggle_archer = false; - ab_arg = 2; + bool current_check = false; + uint32 type_value = 0; + + if (sep->IsNumber(1)) { + type_value = atoi(sep->arg[1]); + ++ab_arg; + if (type_value < 0 || type_value > 1) { + c->Message(Chat::Yellow, "You must enter either 0 for disabled or 1 for enabled."); + + return; + } } - else if (!arg1.compare("off")) { - toggle_archer = false; - ab_arg = 2; + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; } - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, sep->arg[(ab_arg + 1)]) == ActionableBots::ABT_None) { + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - for (auto bot_iter : sbl) { - if (!bot_iter) { + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { continue; } - if (toggle_archer) { - bot_iter->SetBotArcherySetting(!bot_iter->IsBotArcher(), true); + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} currently ranged.'", + my_bot->GetCleanName(), + my_bot->IsBotRanged() ? "am" : "am no longer" + ).c_str() + ); } else { - bot_iter->SetBotArcherySetting(archer_state, true); + if (my_bot->GetBotRangedValue() < RuleI(Combat, MinRangedAttackDist)) { + c->Message(Chat::Yellow, "%s does not have proper weapons or ammo to be at range.", my_bot->GetCleanName()); + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + my_bot->SetBotRangedSetting(type_value); + my_bot->ChangeBotRangedWeapons(my_bot->IsBotRanged()); + ++success_count; } - bot_iter->ChangeBotArcherWeapons(bot_iter->IsBotArcher()); + } - if (bot_iter->GetClass() == Class::Ranger && bot_iter->GetLevel() >= 61) { - bot_iter->SetRangerAutoWeaponSelect(bot_iter->IsBotArcher()); + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} ranged.'", + first_found->GetCleanName(), + first_found->IsBotRanged() ? "am now" : "am no longer" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots {} ranged.", + success_count, + type_value ? "are now" : "are no longer" + ).c_str() + ); } } } void bot_command_toggle_helm(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_toggle_helm", sep->arg[0], "bottogglehelm")) + if (helper_command_alias_fail(c, "bot_command_toggle_helm", sep->arg[0], "bottogglehelm")) { + c->Message(Chat::White, "note: Toggles whether or not bots will show their helm."); + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([option: on | off]) ([actionable: target | byname | ownergroup | targetgroup | namesgroup | healrotation | spawned] ([actionable_name]))", sep->arg[0]); + BotCommandHelpParams p; + + p.description = { "Toggles whether or not bots will show their helm." }; + p.example_format = { fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set BotA to show their helm:", + fmt::format( + "{} 1 byname BotA", + sep->arg[0] + ) + }; + p.examples_two = + { + "To set all bots to show their helm:", + fmt::format( + "{} 1 spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To check the toggle helm status of all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + return; } - const int ab_mask = ActionableBots::ABM_NoFilter; - + std::string arg1 = sep->arg[1]; - bool helm_state = false; - bool toggle_helm = true; int ab_arg = 1; - if (!arg1.compare("on")) { - helm_state = true; - toggle_helm = false; - ab_arg = 2; + bool current_check = false; + uint32 type_value = 0; + + if (sep->IsNumber(1)) { + type_value = atoi(sep->arg[1]); + ++ab_arg; + if (type_value < 0 || type_value > 1) { + c->Message(Chat::Yellow, "You must enter either 0 for disabled or 1 for enabled."); + + return; + } } - else if (!arg1.compare("off")) { - toggle_helm = false; - ab_arg = 2; + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); - std::list sbl; - auto ab_type = ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, sep->arg[(ab_arg + 1)]); - if (ab_type == ActionableBots::ABT_None) return; + } - int bot_count = 0; - for (auto bot_iter : sbl) { - if (!bot_iter) + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { continue; + } - if (toggle_helm) - bot_iter->SetShowHelm(!bot_iter->GetShowHelm()); - else - bot_iter->SetShowHelm(helm_state); + if (!first_found) { + first_found = my_bot; + } - if (ab_type != ActionableBots::ABT_All) { - if (!database.botdb.SaveHelmAppearance(bot_iter->GetBotID(), bot_iter->GetShowHelm())) { - return; - } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} show my helm.'", + my_bot->GetCleanName(), + my_bot->GetShowHelm() ? "will" : "will not" + ).c_str() + ); + } + else { + my_bot->SetShowHelm(type_value); - EQApplicationPacket* outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - SpawnAppearance_Struct* saptr = (SpawnAppearance_Struct*)outapp->pBuffer; - saptr->spawn_id = bot_iter->GetID(); + auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); + SpawnAppearance_Struct* saptr = (SpawnAppearance_Struct*) outapp->pBuffer; + saptr->spawn_id = my_bot->GetID(); saptr->type = AppearanceType::ShowHelm; - saptr->parameter = bot_iter->GetShowHelm(); + saptr->parameter = my_bot->GetShowHelm(); - entity_list.QueueClients(bot_iter, outapp); + entity_list.QueueClients(my_bot, outapp, true); safe_delete(outapp); - //helper_bot_appearance_form_update(bot_iter); + ++success_count; } - ++bot_count; } - - if (ab_type == ActionableBots::ABT_All) { - if (toggle_helm) { - database.botdb.ToggleAllHelmAppearances(c->CharacterID()); + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} show my helm.'", + first_found->GetCleanName(), + first_found->GetShowHelm() ? "will now" : "will no longer" + ).c_str() + ); } else { - database.botdb.SaveAllHelmAppearances(c->CharacterID(), helm_state); + c->Message( + Chat::Green, + fmt::format( + "{} of your bots {} show their helm.", + success_count, + type_value ? "will now" : "will no longer" + ).c_str() + ); } - - c->Message(Chat::White, "%s all of your bot show helm flags", toggle_helm ? "Toggled" : (helm_state ? "Set" : "Cleared")); - } - else { - c->Message(Chat::White, "%s %i of your spawned bot show helm flags", toggle_helm ? "Toggled" : (helm_state ? "Set" : "Cleared"), bot_count); } // Notes: @@ -1427,7 +1979,7 @@ void bot_command_update(Client *c, const Seperator *sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_BySpawnedBots(c, sbl); if (sbl.empty()) { c->Message(Chat::White, "You currently have no spawned bots"); @@ -1439,7 +1991,6 @@ void bot_command_update(Client *c, const Seperator *sep) if (!bot_iter || bot_iter->IsEngaged() || bot_iter->GetLevel() == c->GetLevel()) continue; - bot_iter->SetPetChooser(false); bot_iter->CalcBotStats(c->GetBotOption(Client::booStatsUpdate)); bot_iter->SendAppearancePacket(AppearanceType::WhoLevel, bot_iter->GetLevel(), true, true); ++bot_count; diff --git a/zone/bot_commands/bot_settings.cpp b/zone/bot_commands/bot_settings.cpp new file mode 100644 index 0000000000..98c4dccd75 --- /dev/null +++ b/zone/bot_commands/bot_settings.cpp @@ -0,0 +1,47 @@ +#include "../bot_command.h" + +void bot_command_bot_settings(Client* c, const Seperator* sep) +{ + std::vector subcommand_list = { + "behindmob", + "blockedbuffs", + "blockedpetbuffs", + "distanceranged", + "copysettings", + "defaultsettings", + "enforcespelllist", + "follow", + "followdistance", + "illusionblock", + "maxmeleerange", + "owneroption", + "petsettype", + "sithppercent", + "sitincombat", + "sitmanapercent", + "spellaggrochecks", + "spellannouncecasts", + "spelldelays", + "spellengagedpriority", + "spellholds", + "spellidlepriority", + "spellmaxhppct", + "spellmaxmanapct", + "spellmaxthresholds", + "spellminhppct", + "spellminmanapct", + "spellminthresholds", + "spellpursuepriority", + "spellresistlimits", + "spelltargetcount", + "spelllist", + "stance", + "togglehelm", + "bottoggleranged" + }; + + if (helper_command_alias_fail(c, "bot_command_bot_settings", sep->arg[0], "botsettings")) + return; + + helper_send_available_subcommands(c, "botsettings", subcommand_list); +} diff --git a/zone/bot_commands/cast.cpp b/zone/bot_commands/cast.cpp new file mode 100644 index 0000000000..5ac4d65c32 --- /dev/null +++ b/zone/bot_commands/cast.cpp @@ -0,0 +1,625 @@ +#include "../bot_command.h" + +void bot_command_cast(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_cast", sep->arg[0], "cast")) { + c->Message(Chat::White, "note: Commands bots to force cast a specific spell type, ignoring all settings (holds, delays, thresholds, etc)."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Commands bots to force cast a specific spell type, ignoring all settings (holds, delays, thresholds, etc)." }; + p.notes = + { + "- This will interrupt any spell currently being cast by bots told to use the command", + "- Bots will still check to see if they have the spell in their spell list, whether the target is immune, spell is allowed and all other sanity checks for spells", + fmt::format( + "- You can use {} aa # to cast any clickable AA or specifically {} harmtouch / {} layonhands" + , sep->arg[0] + , sep->arg[0] + , sep->arg[0] + ) + }; + p.example_format = + { + fmt::format("{} [Type Shortname] [actionable, default: spawned]", sep->arg[0]), + fmt::format("{} [Type ID] [actionable, default: spawned]", sep->arg[0]) + }; + p.examples_one = + { + "To tell everyone to Nuke the target:", + fmt::format( + "{} {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {}", + sep->arg[0], + BotSpellTypes::Nuke + ) + }; + p.examples_two = + { + "To tell Skbot to Harm Touch the target:", + fmt::format( + "{} aa 6000 byname Skbot", + sep->arg[0] + ), + fmt::format( + "{} harmtouch byname Skbot", + sep->arg[0] + ) + }; + p.examples_three = + { + "To tell all bots to try to cast spell #93 (Burst of Flame)", + fmt::format( + "{} spellid 93", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(true); + + c->Message( + Chat::Yellow, + fmt::format( + "Use help after any command type for more subtypes to use, for example: {}.", + Saylink::Silent("^cast invisibility help") + ).c_str() + ); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + + //AA help + if (!arg1.compare("aa") && !arg2.compare("help")) { + c->Message(Chat::Yellow, "Enter the ID of an AA to attempt to cast.", sep->arg[0]); + } + + //Commanded type help prompts + if (!arg2.compare("help")) { + c->Message(Chat::Yellow, "You can also use [single], [group], [ae]. Ex: ^cast movementspeed group.", sep->arg[0]); + } + + if (!arg1.compare("invisibility") && !arg2.compare("help")) { + c->Message( + Chat::Yellow, + fmt::format( + "Available options for {} are: {}, {}, {}, {}.", + sep->arg[0], + Saylink::Silent("^cast invisibility see", "see"), + Saylink::Silent("^cast invisibility invis", "invis"), + Saylink::Silent("^cast invisibility undead", "undead"), + Saylink::Silent("^cast invisibility animals", "animals") + ).c_str() + ); + + return; + } + + if (!arg1.compare("size") && !arg2.compare("help")) { + c->Message( + Chat::Yellow, + fmt::format( + "Available options for {} are: {}, {}.", + sep->arg[0], + Saylink::Silent("^cast size grow", "grow"), + Saylink::Silent("^cast size shrink", "shrink") + ).c_str() + ); + + return; + } + + if (!arg1.compare("movementspeed") && !arg2.compare("help")) { + c->Message( + Chat::Yellow, + fmt::format( + "Available options for {} are: {}, {}.", + sep->arg[0], + Saylink::Silent("^cast movementspeed selo"), "selo" + ).c_str() + ); + + return; + } + + if (!arg2.compare("help")) { + c->Message(Chat::Yellow, "There are no additional options for {}.", sep->arg[0]); + return; + } + + int ab_arg = 2; + uint16 spell_type = UINT16_MAX; + uint16 sub_type = UINT16_MAX; + uint16 sub_target_type = UINT16_MAX; + bool aa_type = false; + int aa_id = 0; + bool by_spell_id = false; + uint16 chosen_spell_id = UINT16_MAX; + + if (!arg1.compare("aa") || !arg1.compare("harmtouch") || !arg1.compare("layonhands")) { + if (!RuleB(Bots, AllowCastAAs)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + + if (!arg1.compare("harmtouch")) { + aa_id = zone->GetAlternateAdvancementAbilityByRank(aaHarmTouch)->id; + } + else if (!arg1.compare("layonhands")) { + aa_id = zone->GetAlternateAdvancementAbilityByRank(aaLayonHands)->id; + } + else if (!sep->IsNumber(2) || !zone->GetAlternateAdvancementAbility(Strings::ToInt(arg2))) { + c->Message(Chat::Yellow, "You must enter an AA ID."); + return; + } + else { + ++ab_arg; + aa_id = Strings::ToInt(arg2); + } + + aa_type = true; + } + + if (!arg1.compare("spellid")) { + if (!RuleB(Bots, AllowForcedCastsBySpellID)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + + if (sep->IsNumber(2) && IsValidSpell(atoi(sep->arg[2]))) { + ++ab_arg; + chosen_spell_id = atoi(sep->arg[2]); + by_spell_id = true; + } + else { + c->Message(Chat::Yellow, "You must enter a valid spell ID."); + + return; + } + } + + if (!aa_type && !by_spell_id) { + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!Bot::IsValidBotSpellType(spell_type)) { + c->Message( + Chat::Yellow, + fmt::format( + "You must choose a valid spell type. Use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + switch (spell_type) { //Allowed command checks + case BotSpellTypes::Charm: + if (!RuleB(Bots, AllowCommandedCharm)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + + break; + case BotSpellTypes::AEMez: + case BotSpellTypes::Mez: + if (!RuleB(Bots, AllowCommandedMez)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + + break; + case BotSpellTypes::Resurrect: + if (!RuleB(Bots, AllowCommandedResurrect)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + + break; + case BotSpellTypes::AELull: + case BotSpellTypes::Lull: + if (!RuleB(Bots, AllowCommandedLull)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + + break; + case BotSpellTypes::SummonCorpse: + if (!RuleB(Bots, AllowCommandedSummonCorpse)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + + break; + default: + break; + } + + std::string arg_string = sep->arg[ab_arg]; + + + if (!arg_string.compare("shrink")) { + sub_type = CommandedSubTypes::Shrink; + ++ab_arg; + } + else if (!arg_string.compare("grow")) { + sub_type = CommandedSubTypes::Grow; + ++ab_arg; + } + else if (!arg_string.compare("see")) { + sub_type = CommandedSubTypes::SeeInvis; + ++ab_arg; + } + else if (!arg_string.compare("invis")) { + sub_type = CommandedSubTypes::Invis; + ++ab_arg; + } + else if (!arg_string.compare("undead")) { + sub_type = CommandedSubTypes::InvisUndead; + ++ab_arg; + } + else if (!arg_string.compare("animals")) { + sub_type = CommandedSubTypes::InvisAnimals; + ++ab_arg; + } + else if (!arg_string.compare("selo")) { + sub_type = CommandedSubTypes::Selo; + ++ab_arg; + } + + arg_string = sep->arg[ab_arg]; + + if (!arg_string.compare("single")) { + sub_target_type = CommandedSubTypes::SingleTarget; + ++ab_arg; + } + else if (!arg_string.compare("group")) { + sub_target_type = CommandedSubTypes::GroupTarget; + ++ab_arg; + } + else if (!arg_string.compare("ae")) { + sub_target_type = CommandedSubTypes::AETarget; + ++ab_arg; + } + } + + Mob* tar = c->GetTarget(); + + if (!tar) { + if ((!aa_type && !by_spell_id) && spell_type != BotSpellTypes::Escape && spell_type != BotSpellTypes::Pet) { + c->Message(Chat::Yellow, "You need a target for that."); + return; + } + } + + if (!aa_type && !by_spell_id) { + if (IsPetBotSpellType(spell_type) && !tar->IsPet()) { + c->Message( + Chat::Yellow, + fmt::format( + "[{}] is an invalid target. {} requires a pet to be targeted.", + tar->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type) + ).c_str() + ); + + return; + } + + switch (spell_type) { //Target Checks + case BotSpellTypes::Resurrect: + if (!tar->IsCorpse() || !tar->CastToCorpse()->IsPlayerCorpse()) { + c->Message( + Chat::Yellow, + fmt::format( + "[{}] is not a player's corpse.", + tar->GetCleanName() + ).c_str() + ); + + return; + } + + break; + case BotSpellTypes::Identify: + case BotSpellTypes::SendHome: + case BotSpellTypes::BindAffinity: + case BotSpellTypes::SummonCorpse: + if (!tar->IsClient() || !c->IsInGroupOrRaid(tar)) { + c->Message( + Chat::Yellow, + fmt::format( + "[{}] is an invalid target. Only players in your group or raid are eligible targets.", + tar->GetCleanName() + ).c_str() + ); + + return; + } + + break; + default: + if ( + (IsBotSpellTypeDetrimental(spell_type) && !c->IsAttackAllowed(tar)) || + ( + spell_type == BotSpellTypes::Charm && + ( + tar->IsClient() || + tar->IsCorpse() || + tar->GetOwner() + ) + ) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "You cannot attack [{}].", + tar->GetCleanName() + ).c_str() + ); + + return; + } + + if (IsBotSpellTypeBeneficial(spell_type)) { + if ( + (tar->IsNPC() && !tar->GetOwner()) || + (tar->GetOwner() && tar->GetOwner()->IsOfClientBot() && !c->IsInGroupOrRaid(tar->GetOwner())) || + (tar->IsOfClientBot() && !c->IsInGroupOrRaid(tar)) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "[{}] is an invalid target. Only players or their pet in your group or raid are eligible targets.", + tar->GetCleanName() + ).c_str() + ); + + return; + } + } + + break; + } + } + + if ( + (spell_type == BotSpellTypes::Cure || spell_type == BotSpellTypes::GroupCures || spell_type == BotSpellTypes::PetCures) && + !c->CastToBot()->GetNeedsCured(tar) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "[{}] doesn't have anything that needs to be cured.", + tar->GetCleanName() + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "spawned"; + } + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + BotSpell bot_spell; + bot_spell.SpellId = 0; + bot_spell.SpellIndex = 0; + bot_spell.ManaCost = 0; + bool is_success = false; + uint16 success_count = 0; + Bot* first_found = nullptr; + + for (auto bot_iter : sbl) { + if (!bot_iter->IsInGroupOrRaid(c)) { + continue; + } + + if (bot_iter->GetBotStance() == Stance::Passive || bot_iter->GetHoldFlag() || bot_iter->GetAppearance() == eaDead || bot_iter->IsFeared() || bot_iter->IsSilenced() || bot_iter->IsAmnesiad() || bot_iter->GetHP() < 0) { + continue; + } + + Mob* new_tar = tar; + + if (!aa_type && !by_spell_id) { + if (!BotSpellTypeRequiresTarget(spell_type)) { + new_tar = bot_iter; + } + + if (!new_tar) { + continue; + } + + if ( + IsBotSpellTypeBeneficial(spell_type) && + !RuleB(Bots, CrossRaidBuffingAndHealing) && + !bot_iter->IsInGroupOrRaid(new_tar, true) + ) { + continue; + } + + if (IsBotSpellTypeDetrimental(spell_type) && !bot_iter->IsAttackAllowed(new_tar)) { + bot_iter->RaidGroupSay( + bot_iter, + fmt::format( + "I cannot attack [{}].", + new_tar->GetCleanName() + ).c_str() + ); + + continue; + } + } + + if (aa_type) { + if (!bot_iter->GetAA(zone->GetAlternateAdvancementAbility(aa_id)->first_rank_id)) { + continue; + } + + AA::Rank* temp_rank = nullptr; + AA::Rank*& rank = temp_rank; + uint16 spell_id = bot_iter->GetSpellByAA(aa_id, rank); + + if (!IsValidSpell(spell_id)) { + continue; + } + + if (!bot_iter->AttemptAACastSpell(tar, spell_id, rank)) { + continue; + } + + is_success = true; + ++success_count; + + continue; + } + else if (by_spell_id) { + if (!bot_iter->CanUseBotSpell(chosen_spell_id)) { + continue; + } + + if (!tar || (spells[chosen_spell_id].target_type == ST_Self && tar != bot_iter)) { + tar = bot_iter; + } + + if (bot_iter->AttemptForcedCastSpell(tar, chosen_spell_id)) { + if (!first_found) { + first_found = bot_iter; + } + + is_success = true; + ++success_count; + } + + continue; + } + else { + bot_iter->SetCommandedSpell(true); + + if (bot_iter->AICastSpell(new_tar, 100, spell_type, sub_target_type, sub_type)) { + if (!first_found) { + first_found = bot_iter; + } + + is_success = true; + ++success_count; + } + + bot_iter->SetCommandedSpell(false); + + continue; + } + + continue; + } + + std::string type = ""; + + if (aa_type) { + type = zone->GetAAName(zone->GetAlternateAdvancementAbility(aa_id)->first_rank_id); + } + else if (by_spell_id) { + type = "Forced"; + } + else { + if (sub_type == UINT16_MAX) { + type = Bot::GetSpellTypeNameByID(spell_type); + } + else { + type = Bot::GetSubTypeNameByID(sub_type); + } + } + + if (!is_success) { + c->Message( + Chat::Yellow, + fmt::format( + "No bots are capable of casting [{}] on {}. This could be due to this to any number of things: range, mana, immune, target type, etc.", + (by_spell_id ? spells[chosen_spell_id].name : type), + tar ? tar->GetCleanName() : "your target" + ).c_str() + ); + + if (!aa_type && !by_spell_id) { + helper_send_usage_required_bots(c, spell_type); + } + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "{} {} [{}]{}", + ((success_count == 1 && first_found) ? first_found->GetCleanName() : (fmt::format("{}", success_count).c_str())), + ((success_count == 1 && first_found) ? "casted" : "of your bots casted"), + (by_spell_id ? spells[chosen_spell_id].name : type), + tar ? (fmt::format(" on {}.", tar->GetCleanName()).c_str()) : "." + ).c_str() + ); + } +} diff --git a/zone/bot_commands/caster_range.cpp b/zone/bot_commands/caster_range.cpp deleted file mode 100644 index 2c1faf639a..0000000000 --- a/zone/bot_commands/caster_range.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "../bot_command.h" - -void bot_command_caster_range(Client* c, const Seperator* sep) -{ - if (helper_command_alias_fail(c, "bot_command_caster_range", sep->arg[0], "casterrange")) { - return; - } - - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [current | value: 0 - 300] ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); - c->Message(Chat::White, "note: Can only be used for Casters or Hybrids."); - c->Message(Chat::White, "note: Use [current] to check the current setting."); - c->Message(Chat::White, "note: Set the value to the minimum distance you want your bot to try to remain from its target."); - c->Message(Chat::White, "note: If they are too far for a spell, it will be skipped."); - c->Message(Chat::White, "note: This is set to (90) units by default."); - return; - } - const int ab_mask = ActionableBots::ABM_Type1; - - std::string arg1 = sep->arg[1]; - int ab_arg = 1; - bool current_check = false; - uint32 crange = 0; - - if (sep->IsNumber(1)) { - ab_arg = 2; - crange = atoi(sep->arg[1]); - if (crange < 0 || crange > 300) { - c->Message(Chat::White, "You must enter a value within the range of 0 - 300."); - return; - } - } - else if (!arg1.compare("current")) { - ab_arg = 2; - current_check = true; - } - else { - c->Message(Chat::White, "Incorrect argument, use %s help for a list of options.", sep->arg[0]); - return; - } - - std::string class_race_arg = sep->arg[ab_arg]; - bool class_race_check = false; - - if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { - class_race_check = true; - } - - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { - return; - } - - sbl.remove(nullptr); - - Bot* first_found = nullptr; - int success_count = 0; - - for (auto my_bot : sbl) { - if (!IsCasterClass(my_bot->GetClass()) && !IsHybridClass(my_bot->GetClass())) { - continue; - } - - if (!first_found) { - first_found = my_bot; - } - - if (current_check) { - c->Message( - Chat::White, - fmt::format( - "{} says, 'My current Caster Range is {}.'", - my_bot->GetCleanName(), - my_bot->GetBotCasterRange() - ).c_str() - ); - } - else { - my_bot->SetBotCasterRange(crange); - ++success_count; - - database.botdb.SaveBotCasterRange(my_bot->GetBotID(), crange); - } - } - if (!current_check) { - if (success_count == 1 && first_found) { - c->Message( - Chat::White, - fmt::format( - "{} says, 'My Caster Range was set to {}.'", - first_found->GetCleanName(), - crange - ).c_str() - ); - } - else { - c->Message( - Chat::White, - fmt::format( - "{} of your bots set their Caster Range to {}.", - success_count, - crange - ).c_str() - ); - } - } -} diff --git a/zone/bot_commands/charm.cpp b/zone/bot_commands/charm.cpp deleted file mode 100644 index 2df4694b66..0000000000 --- a/zone/bot_commands/charm.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "../bot_command.h" - -void bot_command_charm(Client *c, const Seperator *sep) -{ - auto local_list = &bot_command_spells[BCEnum::SpT_Charm]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Charm) || helper_command_alias_fail(c, "bot_command_charm", sep->arg[0], "charm")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([option: dire])", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Charm); - return; - } - - bool dire = false; - std::string dire_arg = sep->arg[1]; - if (!dire_arg.compare("dire")) - dire = true; - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToCharm(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->dire != dire) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, ENEMY); - if (!target_mob) - continue; - if (target_mob->IsCharmed()) { - c->Message(Chat::White, "Your is already charmed"); - return; - } - - if (spells[local_entry->spell_id].max_value[EFFECTIDTOINDEX(1)] < target_mob->GetLevel()) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob, true); - if (!my_bot) - continue; - - uint32 dont_root_before = 0; - if (helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id, true, &dont_root_before)) - target_mob->SetDontRootMeBefore(dont_root_before); - - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/class_race_list.cpp b/zone/bot_commands/class_race_list.cpp new file mode 100644 index 0000000000..84c611ab38 --- /dev/null +++ b/zone/bot_commands/class_race_list.cpp @@ -0,0 +1,113 @@ +#include "../bot_command.h" + +void bot_command_class_race_list(Client* c, const Seperator* sep) +{ + const std::string class_substrs[17] = { + "", + "WAR", "CLR", "PAL", "RNG", + "SHD", "DRU", "MNK", "BRD", + "ROG", "SHM", "NEC", "WIZ", + "MAG", "ENC", "BST", "BER" + }; + + const std::string race_substrs[17] = { + "", + "HUM", "BAR", "ERU", "ELF", + "HIE", "DEF", "HEF", "DWF", + "TRL", "OGR", "HFL", "GNM", + "IKS", "VAH", "FRG", "DRK" + }; + + const uint16 race_values[17] = { + Race::Doug, + Race::Human, Race::Barbarian, Race::Erudite, Race::WoodElf, + Race::HighElf, Race::DarkElf, Race::HalfElf, Race::Dwarf, + Race::Troll, Race::Ogre, Race::Halfling, Race::Gnome, + Race::Iksar, Race::VahShir, Race::Froglok2, Race::Drakkin + }; + + std::string window_text; + std::string message_separator; + int object_count = 0; + const int object_max = 4; + + window_text.append( + fmt::format( + "Classes{}", + DialogueWindow::Break() + ) + ); + + window_text.append( + fmt::format( + "--------------------------------------------------------------------", + DialogueWindow::Break() + ) + ); + + window_text.append(DialogueWindow::Break()); + + message_separator = " "; + object_count = 0; + for (int i = 0; i <= 15; ++i) { + window_text.append(message_separator); + + if (object_count >= object_max) { + window_text.append(DialogueWindow::Break()); + object_count = 0; + } + + window_text.append( + fmt::format("{} ({})", + class_substrs[i + 1], + (i + 1) + ) + ); + + ++object_count; + message_separator = ", "; + } + + window_text.append(DialogueWindow::Break(2)); + + window_text.append( + fmt::format( + "Races{}", + DialogueWindow::Break() + ) + ); + + window_text.append( + fmt::format( + "--------------------------------------------------------------------", + DialogueWindow::Break() + ) + ); + + window_text.append(DialogueWindow::Break()); + + message_separator = " "; + object_count = 0; + for (int i = 0; i <= 15; ++i) { + window_text.append(message_separator); + + if (object_count >= object_max) { + window_text.append(DialogueWindow::Break()); + object_count = 0; + } + + window_text.append( + fmt::format("{} ({})", + race_substrs[i + 1], + race_values[i + 1] + ) + ); + + ++object_count; + message_separator = ", "; + } + + c->SendPopupToClient("Bot Creation Options", window_text.c_str()); + + return; +} diff --git a/zone/bot_commands/click_item.cpp b/zone/bot_commands/click_item.cpp index b236815d8a..d0ba47b697 100644 --- a/zone/bot_commands/click_item.cpp +++ b/zone/bot_commands/click_item.cpp @@ -8,7 +8,7 @@ void bot_command_click_item(Client* c, const Seperator* sep) } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); c->Message(Chat::White, "This will cause the selected bots to click the item in the given slot ID."); c->Message(Chat::White, "Use ^invlist to see their items along with slot IDs."); return; @@ -25,7 +25,7 @@ void bot_command_click_item(Client* c, const Seperator* sep) uint32 slot_id = 0; if (sep->IsNumber(1)) { - ab_arg = 2; + ++ab_arg; slot_id = atoi(sep->arg[1]); if (slot_id < EQ::invslot::EQUIPMENT_BEGIN || slot_id > EQ::invslot::EQUIPMENT_END) { c->Message(Chat::Yellow, "You must specify a valid inventory slot from 0 to 22. Use %s help for more information", sep->arg[0]); @@ -39,13 +39,30 @@ void bot_command_click_item(Client* c, const Seperator* sep) class_race_check = true; } - std::list sbl; + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Mob* tar = c->GetTarget(); for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if ( + tar && + tar != c && + tar->GetOwner() != c && + !c->DoLosChecks(tar) + ) { + continue; + } + if (RuleI(Bots, BotsClickItemsMinLvl) > my_bot->GetLevel()) { c->Message(Chat::White, "%s must be level %i to use clickable items.", my_bot->GetCleanName(), RuleI(Bots, BotsClickItemsMinLvl)); continue; diff --git a/zone/bot_commands/copy_settings.cpp b/zone/bot_commands/copy_settings.cpp new file mode 100644 index 0000000000..49b2c6d2e7 --- /dev/null +++ b/zone/bot_commands/copy_settings.cpp @@ -0,0 +1,376 @@ +#include "../bot_command.h" + +void bot_command_copy_settings(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_copy_settings", sep->arg[0], "copysettings")) { + c->Message(Chat::White, "note: Copies settings from one bot to another."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = + { + "Copies settings from one bot to another." + }; + p.notes = + { + "- You can put a spell type ID or shortname after any option except [all], [misc] and [spellsettings] to restore that specifc spell type only" + }; + p.example_format = { fmt::format("{} [from] [to] [option] [optional: spelltype id/short name]", sep->arg[0]) }; + p.examples_one = + { + "To copy all settings from BotA to BotB:", + fmt::format("{} BotA BotB all", sep->arg[0]) + }; + p.examples_two = + { + "To copy only Nuke spelltypesettings from BotA to BotB:", + fmt::format( + "{} BotA BotB spelltypesettings {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} BotA BotB spelltypesettings {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + }; + p.examples_three = + { + "To copy only spellsettings from BotA to BotB:", + fmt::format( + "{} BotA BotB spellsettings", + sep->arg[0] + ), + fmt::format( + "{} BotA BotB spellsettings", + sep->arg[0] + ), + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + p.options = { "all, misc, spellsettings, spelltypesettings, spellholds, spelldelays, spellminthresholds, spellmaxthresholds, spellminmanapct, spellmaxmanapct, spellminhppct, spellmaxhppct, spellidlepriority, spellengagedpriority, spellpursuepriority, spellaggrochecks, spelltargetcounts, spellresistlimits, spellannouncecasts, blockedbuffs, blockedpetbuffs" }; + p.options_one = + { + "[spellsettings] will copy ^spellsettings options", + "[spelltypesettings] copies all spell type settings", + "[all] copies all settings" + }; + p.options_two = + { + "[misc] copies all miscellaneous options such as:", + "- ^showhelm, ^followd, ^stopmeleelevel, ^enforcespellsettings, ^bottoggleranged, ^petsettype, ^behindmob, ^distanceranged, ^illusionblock, ^sitincombat, ^sithppercent, ^sitmanapercent, ^blockedbuffs, ^blockedpetbuffs" + }; + p.options_three = { "The remaining options copy that specific type" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + int spell_type_arg_int = 4; + std::string spell_type_arg = sep->arg[spell_type_arg_int]; + int ab_arg = 2; + bool valid_option = false; + uint16 spell_type = UINT16_MAX; + uint16 setting_type = UINT16_MAX; + std::vector options = + { + "all", + "misc", + "spellsettings", + "spelltypesettings", + "spellholds", + "spelldelays", + "spellminthresholds", + "spellmaxthresholds", + "spellminmanapct", + "spellmaxmanapct", + "spellminhppct", + "spellmaxhppct", + "spellidlepriority", + "spellengagedpriority", + "spellpursuepriority", + "spellaggrochecks", + "spelltargetcounts", + "spellresistlimits", + "spellannouncecasts", + "blockedbuffs", + "blockedpetbuffs" + }; + + if (sep->IsNumber(spell_type_arg_int)) { + spell_type = atoi(sep->arg[spell_type_arg_int]); + + if (!Bot::IsValidBotSpellType(spell_type)) { + c->Message( + Chat::Yellow, + fmt::format( + "You must choose a valid spell type. Use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + else if (!spell_type_arg.empty()) { + if (Bot::GetSpellTypeIDByShortName(spell_type_arg) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(spell_type_arg); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "You must choose a valid spell type. Use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + for (int i = 0; i < options.size(); i++) { + if (sep->arg[3] == options[i]) { + setting_type = Bot::GetBotSpellCategoryIDByShortName(sep->arg[3]); + valid_option = true; + break; + } + } + + if (!valid_option) { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + auto from = entity_list.GetBotByBotName(sep->arg[1]); + + if (!from) { + c->Message(Chat::Yellow, "Could not find %s.", sep->arg[1]); + return; + } + + if (!from->IsBot()) { + c->Message(Chat::Yellow, "%s is not a bot.", from->GetCleanName()); + return; + } + + if (!from->GetOwner()) { + c->Message(Chat::Yellow, "Could not find %s's owner.", from->GetCleanName()); + } + + if (RuleB(Bots, CopySettingsOwnBotsOnly) && from->GetOwner() != c) { + c->Message(Chat::Yellow, "You name a bot you own to use this command."); + return; + } + + if (!RuleB(Bots, AllowCopySettingsAnon) && from->GetOwner() != c && from->GetOwner()->CastToClient()->GetAnon()) { + c->Message(Chat::Yellow, "You name a bot you own to use this command."); + return; + } + + auto to = ActionableBots::AsNamed_ByBot(c, sep->arg[2]); + + if (!to) { + c->Message(Chat::Yellow, "Could not find %s.", sep->arg[1]); + return; + } + + if (!to->IsBot()) { + c->Message(Chat::Yellow, "%s is not a bot.", to->GetCleanName()); + return; + } + + if (!to->GetOwner()) { + c->Message(Chat::Yellow, "Could not find %s's owner.", to->GetCleanName()); + } + + if (to->GetOwner() != c) { + c->Message(Chat::Yellow, "You must name a spawned bot that you own to use this command."); + return; + } + + if (to == from) { + c->Message(Chat::Yellow, "You cannot copy to the same bot that you're copying from."); + return; + } + + std::string output = ""; + + if (setting_type != UINT16_MAX) { + if (spell_type != UINT16_MAX) { + from->CopySettings(to, setting_type, spell_type); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + from->CopySettings(to, setting_type, i); + } + } + + output = Bot::GetBotSpellCategoryName(setting_type); + } + else { + if (!strcasecmp(sep->arg[3], "misc")) { + from->CopySettings(to, BotSettingCategories::BaseSetting); + output = "Miscellaneous"; + } + else if (!strcasecmp(sep->arg[3], "spellsettings")) { + from->CopyBotSpellSettings(to); + output = "^spellsettings"; + } + else if (!strcasecmp(sep->arg[3], "spelltypesettings")) { + if (spell_type != UINT16_MAX) { + from->CopySettings(to, BotSettingCategories::SpellHold, spell_type); + from->CopySettings(to, BotSettingCategories::SpellDelay, spell_type); + from->CopySettings(to, BotSettingCategories::SpellMinThreshold, spell_type); + from->CopySettings(to, BotSettingCategories::SpellMaxThreshold, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeAggroCheck, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeResistLimit, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeMinManaPct, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxManaPct, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeMinHPPct, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxHPPct, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeIdlePriority, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeEngagedPriority, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypePursuePriority, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeAEOrGroupTargetCount, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeAnnounceCast, spell_type); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + from->CopySettings(to, BotSettingCategories::SpellHold, i); + from->CopySettings(to, BotSettingCategories::SpellDelay, i); + from->CopySettings(to, BotSettingCategories::SpellMinThreshold, i); + from->CopySettings(to, BotSettingCategories::SpellMaxThreshold, i); + from->CopySettings(to, BotSettingCategories::SpellTypeAggroCheck, i); + from->CopySettings(to, BotSettingCategories::SpellTypeResistLimit, i); + from->CopySettings(to, BotSettingCategories::SpellTypeMinManaPct, i); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxManaPct, i); + from->CopySettings(to, BotSettingCategories::SpellTypeMinHPPct, i); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxHPPct, i); + from->CopySettings(to, BotSettingCategories::SpellTypeIdlePriority, i); + from->CopySettings(to, BotSettingCategories::SpellTypeEngagedPriority, i); + from->CopySettings(to, BotSettingCategories::SpellTypePursuePriority, i); + from->CopySettings(to, BotSettingCategories::SpellTypeAEOrGroupTargetCount, i); + from->CopySettings(to, BotSettingCategories::SpellTypeAnnounceCast, i); + } + } + + output = "spell type"; + } + else if (!strcasecmp(sep->arg[3], "blockedbuffs")) { + from->CopyBotBlockedBuffs(to); + } + else if (!strcasecmp(sep->arg[3], "blockedpetbuffs")) { + from->CopyBotBlockedPetBuffs(to); + } + else if (!strcasecmp(sep->arg[3], "all")) { + from->CopySettings(to, BotSettingCategories::BaseSetting); + + if (spell_type != UINT16_MAX) { + from->CopySettings(to, BotSettingCategories::SpellHold, spell_type); + from->CopySettings(to, BotSettingCategories::SpellDelay, spell_type); + from->CopySettings(to, BotSettingCategories::SpellMinThreshold, spell_type); + from->CopySettings(to, BotSettingCategories::SpellMaxThreshold, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeAggroCheck, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeResistLimit, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeMinManaPct, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxManaPct, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeMinHPPct, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxHPPct, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeIdlePriority, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeEngagedPriority, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypePursuePriority, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeAEOrGroupTargetCount, spell_type); + from->CopySettings(to, BotSettingCategories::SpellTypeAnnounceCast, spell_type); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + from->CopySettings(to, BotSettingCategories::SpellHold, i); + from->CopySettings(to, BotSettingCategories::SpellDelay, i); + from->CopySettings(to, BotSettingCategories::SpellMinThreshold, i); + from->CopySettings(to, BotSettingCategories::SpellMaxThreshold, i); + from->CopySettings(to, BotSettingCategories::SpellTypeAggroCheck, i); + from->CopySettings(to, BotSettingCategories::SpellTypeResistLimit, i); + from->CopySettings(to, BotSettingCategories::SpellTypeMinManaPct, i); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxManaPct, i); + from->CopySettings(to, BotSettingCategories::SpellTypeMinHPPct, i); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxHPPct, i); + from->CopySettings(to, BotSettingCategories::SpellTypeIdlePriority, i); + from->CopySettings(to, BotSettingCategories::SpellTypeEngagedPriority, i); + from->CopySettings(to, BotSettingCategories::SpellTypePursuePriority, i); + from->CopySettings(to, BotSettingCategories::SpellTypeAEOrGroupTargetCount, i); + from->CopySettings(to, BotSettingCategories::SpellTypeAnnounceCast, i); + } + } + + from->CopyBotSpellSettings(to); + from->CopyBotBlockedBuffs(to); + from->CopyBotBlockedPetBuffs(to); + output = "spell type"; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + to->Save(); + + c->Message( + Chat::Green, + fmt::format( + "{}'s{}{} settings were copied to {}.", + from->GetCleanName(), + ( + spell_type != UINT16_MAX ? + fmt::format(" [{}] ", + Bot::GetSpellTypeNameByID(spell_type) + ) + : " " + ), + output, + to->GetCleanName() + ).c_str() + ); +} diff --git a/zone/bot_commands/cure.cpp b/zone/bot_commands/cure.cpp deleted file mode 100644 index 4452f8088d..0000000000 --- a/zone/bot_commands/cure.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "../bot_command.h" - -void bot_command_cure(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Cure]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Cure) || helper_command_alias_fail(c, "bot_command_cure", sep->arg[0], "cure")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s [ailment: blindness | disease | poison | curse | corruption]", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Cure); - return; - } - - std::string ailment_arg = sep->arg[1]; - - auto ailment_type = BCEnum::AT_None; - if (!ailment_arg.compare("blindness")) - ailment_type = BCEnum::AT_Blindness; - else if (!ailment_arg.compare("disease")) - ailment_type = BCEnum::AT_Disease; - else if (!ailment_arg.compare("poison")) - ailment_type = BCEnum::AT_Poison; - else if (!ailment_arg.compare("curse")) - ailment_type = BCEnum::AT_Curse; - else if (!ailment_arg.compare("corruption")) - ailment_type = BCEnum::AT_Corruption; - - if (ailment_type == BCEnum::AT_None) { - c->Message(Chat::White, "You must specify a cure [ailment] to use this command"); - return; - } - - local_list->sort([ailment_type](STBaseEntry* l, STBaseEntry* r) { - auto _l = l->SafeCastToCure(), _r = r->SafeCastToCure(); - if (_l->cure_value[AILMENTIDTOINDEX(ailment_type)] < _r->cure_value[AILMENTIDTOINDEX(ailment_type)]) - return true; - if (_l->cure_value[AILMENTIDTOINDEX(ailment_type)] == _r->cure_value[AILMENTIDTOINDEX(ailment_type)] && spells[_l->spell_id].mana < spells[_r->spell_id].mana) - return true; - if (_l->cure_value[AILMENTIDTOINDEX(ailment_type)] == _r->cure_value[AILMENTIDTOINDEX(ailment_type)] && spells[_l->spell_id].mana == spells[_r->spell_id].mana && _l->cure_total < _r->cure_total) - return true; - - return false; - }); - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToCure(); - if (helper_spell_check_fail(local_entry)) - continue; - if (!local_entry->cure_value[AILMENTIDTOINDEX(ailment_type)]) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/default_settings.cpp b/zone/bot_commands/default_settings.cpp new file mode 100644 index 0000000000..40df680797 --- /dev/null +++ b/zone/bot_commands/default_settings.cpp @@ -0,0 +1,516 @@ +#include "../bot_command.h" + +void bot_command_default_settings(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_default_settings", sep->arg[0], "defaultsettings")) { + c->Message(Chat::White, "note: Restores a bot's setting(s) to defaults."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Restores a bot's setting(s) to defaults" }; + p.notes = { "- You can put a spell type ID or shortname after any option except [all], [misc] and [spellsettings] to restore that specifc spell type only"}; + p.example_format = { fmt::format("{} [option] [optional: spelltype id/short name] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To restore delays for Clerics:", + fmt::format( + "{} delays byclass {}", + sep->arg[0], + Class::Cleric + ) + }; + p.examples_two = + { + "To restore only Snare delays for BotA:", + fmt::format( + "{} delays {} byname BotA", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} delays {} byname BotA", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + p.options = { "all, misc, spellsettings, spelltypesettings, spellholds, spelldelays, spellminthresholds, spellmaxthresholds, spellminmanapct, spellmaxmanapct, spellminhppct, spellmaxhppct, spellidlepriority, spellengagedpriority, spellpursuepriority, spellaggrocheck, spelltargetcounts, spellresistlimits, spellannouncecasts" }; + p.options_one = + { + "[spellsettings] will restore ^spellsettings options", + "[spelltypesettings] restores all spell type settings", + "[all] restores all settings" + }; + p.options_two = + { + "[misc] restores all miscellaneous options such as:", + "- ^showhelm, ^followd, ^stopmeleelevel, ^enforcespellsettings, ^bottoggleranged, ^petsettype, ^behindmob, ^distanceranged, ^illusionblock, ^sitincombat, ^sithppercent and ^sitmanapercent", + }; + p.options_three = + { + "
", + "**The remaining options restore that specific type**" + }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + int spell_type_arg_int = 2; + std::string spell_type_arg = sep->arg[spell_type_arg_int]; + int ab_arg = 2; + bool valid_option = false; + uint16 spell_type = UINT16_MAX; + uint16 setting_type = UINT16_MAX; + std::vector options = + { + "all", + "misc", + "spellsettings", + "spelltypesettings", + "spellholds", + "spelldelays", + "spellminthresholds", + "spellmaxthresholds", + "spellminmanapct", + "spellmaxmanapct", + "spellminhppct", + "spellmaxhppct", + "spellidlepriority", + "spellengagedpriority", + "spellpursuepriority", + "spellaggrochecks", + "spelltargetcounts", + "spellresistlimits", + "spellannouncecasts" + }; + + if (sep->IsNumber(spell_type_arg_int)) { + spell_type = atoi(sep->arg[spell_type_arg_int]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message( + Chat::Yellow, + fmt::format( + "You must choose a valid spell type. Use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + ++ab_arg; + } + else if (!spell_type_arg.empty()) { + if (Bot::GetSpellTypeIDByShortName(spell_type_arg) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(spell_type_arg); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "You must choose a valid spell type. Use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + ++ab_arg; + } + + for (int i = 0; i < options.size(); i++) { + if (sep->arg[1] == options[i]) { + setting_type = Bot::GetBotSpellCategoryIDByShortName(sep->arg[1]); + valid_option = true; + + break; + } + } + + if (!valid_option) { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + std::string output = ""; + uint8 bot_stance = 2; + + for (auto my_bot : sbl) { + if (!first_found) { + first_found = my_bot; + } + + bot_stance = my_bot->GetBotStance(); + + if (setting_type != UINT16_MAX) { + if (spell_type != UINT16_MAX) { + my_bot->SetBotSetting(setting_type, spell_type, my_bot->GetDefaultSetting(setting_type, spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetBotSetting(setting_type, i, my_bot->GetDefaultSetting(setting_type, i, bot_stance)); + } + } + + output = (spell_type != UINT16_MAX ? Bot::GetSpellTypeNameByID(spell_type) : ""); + output += sep->arg[3]; + } + else if (!strcasecmp(sep->arg[1], "misc")) { + for (uint16 i = BotBaseSettings::START; i <= BotBaseSettings::END; ++i) { + my_bot->SetBotBaseSetting(i, my_bot->GetDefaultBotBaseSetting(i, bot_stance)); + output = "miscellanous settings"; + } + } + else if (!strcasecmp(sep->arg[1], "spellsettings")) { + my_bot->ResetBotSpellSettings(); + output = "^spellsettings"; + } + else if (!strcasecmp(sep->arg[1], "spelltypesettings")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeHold(spell_type, my_bot->GetDefaultSpellTypeHold(spell_type, bot_stance)); + my_bot->SetSpellTypeDelay(spell_type, my_bot->GetDefaultSpellTypeDelay(spell_type, bot_stance)); + my_bot->SetSpellTypeMinThreshold( + spell_type, + my_bot->GetDefaultSpellTypeMinThreshold(spell_type, bot_stance)); + my_bot->SetSpellTypeMaxThreshold( + spell_type, + my_bot->GetDefaultSpellTypeMaxThreshold(spell_type, bot_stance)); + my_bot->SetSpellTypeAggroCheck(spell_type, my_bot->GetDefaultSpellTypeAggroCheck(spell_type, bot_stance)); + my_bot->SetSpellTypeResistLimit(spell_type, my_bot->GetDefaultSpellTypeResistLimit(spell_type, bot_stance)); + my_bot->SetSpellTypeMinManaLimit(spell_type, my_bot->GetDefaultSpellTypeMinManaLimit(spell_type, bot_stance)); + my_bot->SetSpellTypeMaxManaLimit(spell_type, my_bot->GetDefaultSpellTypeMaxManaLimit(spell_type, bot_stance)); + my_bot->SetSpellTypeMinHPLimit(spell_type, my_bot->GetDefaultSpellTypeMinHPLimit(spell_type, bot_stance)); + my_bot->SetSpellTypeMaxHPLimit(spell_type, my_bot->GetDefaultSpellTypeMaxHPLimit(spell_type, bot_stance)); + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(spell_type, BotPriorityCategories::Idle, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(spell_type, BotPriorityCategories::Engaged, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(spell_type, BotPriorityCategories::Pursue, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypeAEOrGroupTargetCount(spell_type, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(spell_type, bot_stance)); + my_bot->SetSpellTypeAnnounceCast(spell_type, my_bot->GetDefaultSpellTypeAnnounceCast(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeHold(i, my_bot->GetDefaultSpellTypeHold(i, bot_stance)); + my_bot->SetSpellTypeDelay(i, my_bot->GetDefaultSpellTypeDelay(i, bot_stance)); + my_bot->SetSpellTypeMinThreshold(i, my_bot->GetDefaultSpellTypeMinThreshold(i, bot_stance)); + my_bot->SetSpellTypeMaxThreshold(i, my_bot->GetDefaultSpellTypeMaxThreshold(i, bot_stance)); + my_bot->SetSpellTypeAggroCheck(i, my_bot->GetDefaultSpellTypeAggroCheck(i, bot_stance)); + my_bot->SetSpellTypeResistLimit(i, my_bot->GetDefaultSpellTypeResistLimit(i, bot_stance)); + my_bot->SetSpellTypeMinManaLimit(i, my_bot->GetDefaultSpellTypeMinManaLimit(i, bot_stance)); + my_bot->SetSpellTypeMaxManaLimit(i, my_bot->GetDefaultSpellTypeMaxManaLimit(i, bot_stance)); + my_bot->SetSpellTypeMinHPLimit(i, my_bot->GetDefaultSpellTypeMinHPLimit(i, bot_stance)); + my_bot->SetSpellTypeMaxHPLimit(i, my_bot->GetDefaultSpellTypeMaxHPLimit(i, bot_stance)); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypeAEOrGroupTargetCount(i, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(i, bot_stance)); + my_bot->SetSpellTypeAnnounceCast(i, my_bot->GetDefaultSpellTypeAnnounceCast(i, bot_stance)); + } + } + + output = "spell type settings"; + } + else if (!strcasecmp(sep->arg[1], "all")) { + for (uint16 i = BotBaseSettings::START; i <= BotBaseSettings::END; ++i) { + my_bot->SetBotBaseSetting(i, my_bot->GetDefaultBotBaseSetting(i, bot_stance)); + } + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeHold(i, my_bot->GetDefaultSpellTypeHold(i, bot_stance)); + my_bot->SetSpellTypeDelay(i, my_bot->GetDefaultSpellTypeDelay(i, bot_stance)); + my_bot->SetSpellTypeMinThreshold(i, my_bot->GetDefaultSpellTypeMinThreshold(i, bot_stance)); + my_bot->SetSpellTypeMaxThreshold(i, my_bot->GetDefaultSpellTypeMaxThreshold(i, bot_stance)); + my_bot->SetSpellTypeAggroCheck(i, my_bot->GetDefaultSpellTypeAggroCheck(i, bot_stance)); + my_bot->SetSpellTypeResistLimit(i, my_bot->GetDefaultSpellTypeResistLimit(i, bot_stance)); + my_bot->SetSpellTypeMinManaLimit(i, my_bot->GetDefaultSpellTypeMinManaLimit(i, bot_stance)); + my_bot->SetSpellTypeMaxManaLimit(i, my_bot->GetDefaultSpellTypeMaxManaLimit(i, bot_stance)); + my_bot->SetSpellTypeMinHPLimit(i, my_bot->GetDefaultSpellTypeMinHPLimit(i, bot_stance)); + my_bot->SetSpellTypeMaxHPLimit(i, my_bot->GetDefaultSpellTypeMaxHPLimit(i, bot_stance)); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetClass(), bot_stance)); + my_bot->SetSpellTypeAEOrGroupTargetCount(i, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(i, bot_stance)); + my_bot->SetSpellTypeAnnounceCast(i, my_bot->GetDefaultSpellTypeAnnounceCast(i, bot_stance)); + }; + + my_bot->ResetBotSpellSettings(); + my_bot->ClearBotBlockedBuffs(); + + output = "settings"; + + } + else if (!strcasecmp(sep->arg[1], "holds")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeHold(spell_type, my_bot->GetDefaultSpellTypeHold(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeHold(i, my_bot->GetDefaultSpellTypeHold(i, bot_stance)); + } + } + + output = "hold settings"; + } + else if (!strcasecmp(sep->arg[1], "delays")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeDelay(spell_type, my_bot->GetDefaultSpellTypeDelay(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeDelay(i, my_bot->GetDefaultSpellTypeDelay(i, bot_stance)); + } + } + + output = "delay settings"; + } + else if (!strcasecmp(sep->arg[1], "minthresholds")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeMinThreshold( + spell_type, + my_bot->GetDefaultSpellTypeMinThreshold(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMinThreshold(i, my_bot->GetDefaultSpellTypeMinThreshold(i, bot_stance)); + } + } + + output = "minimum threshold settings"; + } + else if (!strcasecmp(sep->arg[1], "maxthresholds")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeMaxThreshold( + spell_type, + my_bot->GetDefaultSpellTypeMaxThreshold(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMaxThreshold(i, my_bot->GetDefaultSpellTypeMaxThreshold(i, bot_stance)); + } + } + + output = "maximum threshold settings"; + } + else if (!strcasecmp(sep->arg[1], "aggrochecks")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeAggroCheck(spell_type, my_bot->GetDefaultSpellTypeAggroCheck(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeAggroCheck(i, my_bot->GetDefaultSpellTypeAggroCheck(i, bot_stance)); + } + } + + output = "aggro check settings"; + } + else if (!strcasecmp(sep->arg[1], "resist limit")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeAEOrGroupTargetCount(spell_type, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeAEOrGroupTargetCount(i, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(i, bot_stance)); + } + } + + output = "resist limit settings"; + } + else if (!strcasecmp(sep->arg[1], "minmanapct")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeMinManaLimit(spell_type, my_bot->GetDefaultSpellTypeMinManaLimit(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMinManaLimit(i, my_bot->GetDefaultSpellTypeMinManaLimit(i, bot_stance)); + } + } + + output = "min mana settings"; + } + else if (!strcasecmp(sep->arg[1], "maxmanapct")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeMaxManaLimit(spell_type, my_bot->GetDefaultSpellTypeMaxManaLimit(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMaxManaLimit(i, my_bot->GetDefaultSpellTypeMaxManaLimit(i, bot_stance)); + } + } + + output = "max mana settings"; + } + else if (!strcasecmp(sep->arg[1], "minhppct")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeMinHPLimit(spell_type, my_bot->GetDefaultSpellTypeMinHPLimit(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMinHPLimit(i, my_bot->GetDefaultSpellTypeMinHPLimit(i, bot_stance)); + } + } + + output = "min hp settings"; + } + else if (!strcasecmp(sep->arg[1], "maxhppct")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeMaxHPLimit(spell_type, my_bot->GetDefaultSpellTypeMaxHPLimit(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMaxHPLimit(i, my_bot->GetDefaultSpellTypeMaxHPLimit(i, bot_stance)); + } + } + + output = "max hp settings"; + } + else if (!strcasecmp(sep->arg[1], "idlepriority")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(spell_type, BotPriorityCategories::Idle, my_bot->GetClass(), bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetClass(), bot_stance)); + } + } + + output = "idle priority settings"; + } + else if (!strcasecmp(sep->arg[1], "engagedpriority")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(spell_type, BotPriorityCategories::Engaged, my_bot->GetClass(), bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetClass(), bot_stance)); + } + } + + output = "engaged priority settings"; + } + else if (!strcasecmp(sep->arg[1], "pursuepriority")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(spell_type, BotPriorityCategories::Pursue, my_bot->GetClass(), bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetClass(), bot_stance)); + } + } + + output = "pursue priority settings"; + } + else if (!strcasecmp(sep->arg[1], "targetcounts")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeAEOrGroupTargetCount(spell_type, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeAEOrGroupTargetCount(i, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(i, bot_stance)); + } + } + + output = "target count settings"; + } + else if (!strcasecmp(sep->arg[1], "announcecast")) { + if (spell_type != UINT16_MAX) { + my_bot->SetSpellTypeAEOrGroupTargetCount(spell_type, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(spell_type, bot_stance)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeAEOrGroupTargetCount(i, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(i, bot_stance)); + } + } + + output = "announce cast settings"; + } + + my_bot->Save(); + ++success_count; + } + + if (success_count == 1) { + c->Message( + Chat::Green, + fmt::format( + "{} says, '{}{} were restored.'", + first_found->GetCleanName(), + ( + spell_type != UINT16_MAX ? + fmt::format("My [{}] ", + Bot::GetSpellTypeNameByID(spell_type) + ) + : "My " + ), + output + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bot's{}{} were restored.", + success_count, + ( + spell_type != UINT16_MAX ? + fmt::format(" [{}] ", + Bot::GetSpellTypeNameByID(spell_type) + ) + : " " + ), + output + ).c_str() + ); + } +} diff --git a/zone/bot_commands/defensive.cpp b/zone/bot_commands/defensive.cpp deleted file mode 100644 index a2edc3e6e9..0000000000 --- a/zone/bot_commands/defensive.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "../bot_command.h" - -void bot_command_defensive(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Stance]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Stance) || helper_command_alias_fail(c, "bot_command_defensive", sep->arg[0], "defensive")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Stance); - return; - } - const int ab_mask = ActionableBots::ABM_Type1; - - std::string class_race_arg = sep->arg[1]; - bool class_race_check = false; - if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { - class_race_check = true; - } - - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, !class_race_check ? sep->arg[2] : nullptr, class_race_check ? atoi(sep->arg[2]) : 0) == ActionableBots::ABT_None) { - return; - } - sbl.remove(nullptr); - - int success_count = 0; - int candidate_count = sbl.size(); - for (auto list_iter : *local_list) { - if (sbl.empty()) - break; - - auto local_entry = list_iter->SafeCastToStance(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->stance_type != BCEnum::StT_Defensive) - continue; - - for (auto bot_iter = sbl.begin(); bot_iter != sbl.end(); ) { - Bot* my_bot = *bot_iter; - if (local_entry->caster_class != my_bot->GetClass()) { - ++bot_iter; - continue; - } - if (local_entry->spell_level > my_bot->GetLevel()) { - ++bot_iter; - continue; - } - - my_bot->InterruptSpell(); - if (candidate_count == 1) { - Bot::BotGroupSay( - my_bot, - fmt::format( - "Using {}.", - spells[local_entry->spell_id].name - ).c_str() - ); - } - - my_bot->UseDiscipline(local_entry->spell_id, my_bot->GetID()); - ++success_count; - - bot_iter = sbl.erase(bot_iter); - } - } - - c->Message(Chat::White, "%i of %i bots have attempted to use defensive disciplines", success_count, candidate_count); -} diff --git a/zone/bot_commands/depart.cpp b/zone/bot_commands/depart.cpp index b337ef4042..82c7fee505 100644 --- a/zone/bot_commands/depart.cpp +++ b/zone/bot_commands/depart.cpp @@ -1,59 +1,272 @@ #include "../bot_command.h" -void bot_command_depart(Client *c, const Seperator *sep) +void bot_command_depart(Client* c, const Seperator* sep) { - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Depart]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Depart) || helper_command_alias_fail(c, "bot_command_depart", sep->arg[0], "depart")) + if (helper_command_alias_fail(c, "bot_command_depart", sep->arg[0], "depart")) { + c->Message(Chat::White, "note: Tells bots to list their port locations or port to a specific location."); + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [list | destination] ([option: single])", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Depart); + BotCommandHelpParams p; + + p.description = { "Tells bots to list their port locations or port to a specific location." }; + p.notes = + { + "- This will interrupt any spell currently being cast by bots told to use the command.", + "- Bots will still check to see if they have the spell in their spell list, whether the target is immune, spell is allowed and all other sanity checks for spells" + }; + p.example_format = { fmt::format("{} [list | zone shortname] [optional: single | group | ae] [actionable, default: spawned]", sep->arg[0]) }; + p.examples_one = + { + "To tell everyone to list their portable locations:", + fmt::format("{} list spawned", + sep->arg[0] + ) + }; + p.examples_two = + { + "To tell all bots to port to Nexus:", + fmt::format( + "{} nexus spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To tell Druidbot to single target port to Butcher:", + fmt::format( + "{} butcher single byname Druidbot", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + return; } bool single = false; + bool group = true; + bool ae = false; std::string single_arg = sep->arg[2]; - if (!single_arg.compare("single")) + bool list = false; + std::string destination = sep->arg[1]; + int ab_arg = 2; + + if (!single_arg.compare("single")) { + ++ab_arg; single = true; + group = false; + } + else if (!single_arg.compare("group")) { + ++ab_arg; + single = false; + group = true; + } + else if (!single_arg.compare("ae")) { + ++ab_arg; + single = false; + group = false; + ae = true; + } - std::string destination = sep->arg[1]; - if (!destination.compare("list")) { - Bot* my_druid_bot = ActionableBots::AsGroupMember_ByClass(c, c, Class::Druid); - Bot* my_wizard_bot = ActionableBots::AsGroupMember_ByClass(c, c, Class::Wizard); - helper_command_depart_list(c, my_druid_bot, my_wizard_bot, local_list, single); - return; + if (!destination.compare("list") || destination.empty()) { + list = true; + + if (destination.empty()) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + } } - else if (destination.empty()) { - c->Message(Chat::White, "A [destination] or [list] argument is required to use this command"); + + Mob* tar = c->GetTarget(); + + if (!tar) { + tar = c; + } + + std::string arg_string = sep->arg[ab_arg]; + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "spawned"; + } + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToDepart(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->single != single) - continue; - if (destination.compare(spells[local_entry->spell_id].teleport_zone)) - continue; + BotSpell bot_spell; + bot_spell.SpellId = 0; + bot_spell.SpellIndex = 0; + bot_spell.ManaCost = 0; - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) + bool is_success = false; + std::map> list_zones; + + for (auto bot_iter : sbl) { + if (!bot_iter->IsInGroupOrRaid(tar, !single)) { continue; + } - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) + if (bot_iter->GetBotStance() == Stance::Passive || bot_iter->GetHoldFlag() || bot_iter->GetAppearance() == eaDead || bot_iter->IsFeared() || bot_iter->IsSilenced() || bot_iter->IsAmnesiad() || bot_iter->GetHP() < 0) { continue; + } + + std::vector bot_spell_list_itr = bot_iter->GetPrioritizedBotSpellsBySpellType(bot_iter, BotSpellTypes::Teleport, tar); + + for (std::vector::iterator itr = bot_spell_list_itr.begin(); itr != bot_spell_list_itr.end(); ++itr) { + if (!IsValidSpell(itr->SpellId)) { + continue; + } + + if ( + (single && spells[itr->SpellId].target_type != ST_Target) || + (group && !IsGroupSpell(itr->SpellId)) || + (ae && !IsAnyAESpell(itr->SpellId)) + ) { + continue; + } + + if (list) { + auto it = list_zones.find(spells[itr->SpellId].teleport_zone); + + if (it != list_zones.end()) { + const auto& [val1, val2] = it->second; + + if (val1 == spells[itr->SpellId].target_type && val2 == bot_iter->GetClass()) { + continue; + } + } + + Bot::RaidGroupSay( + bot_iter, + fmt::format( + "I can port you to {}.", + Saylink::Silent( + fmt::format( + "{} {} {} byname {}", + sep->arg[0], + spells[itr->SpellId].teleport_zone, + (spells[itr->SpellId].target_type == ST_Target ? "single" : (IsGroupSpell(itr->SpellId) ? "group" : "ae")), + bot_iter->GetCleanName() + ).c_str() + , ZoneLongName(ZoneID(spells[itr->SpellId].teleport_zone))) + ).c_str() + ); + + list_zones.insert({ spells[itr->SpellId].teleport_zone, {spells[itr->SpellId].target_type, bot_iter->GetClass()} }); - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; + continue; + } + + if (destination.compare(spells[itr->SpellId].teleport_zone)) { + continue; + } + + bot_iter->SetCommandedSpell(true); + + if (!IsValidSpellAndLoS(itr->SpellId, bot_iter->HasLoS())) { + continue; + } + + if (IsInvulnerabilitySpell(itr->SpellId)) { + tar = bot_iter; //target self for invul type spells + } + + if (bot_iter->IsCommandedSpell() && bot_iter->IsCasting()) { + Bot::RaidGroupSay( + bot_iter, + fmt::format( + "Interrupting {}. I have been commanded to try to cast a [{}] spell, {} on {}.", + bot_iter->CastingSpellID() ? spells[bot_iter->CastingSpellID()].name : "my spell", + bot_iter->GetSpellTypeNameByID(BotSpellTypes::Teleport), + spells[itr->SpellId].name, + tar->GetCleanName() + ).c_str() + ); + + bot_iter->InterruptSpell(); + } + + if (bot_iter->CastSpell(itr->SpellId, tar->GetID(), EQ::spells::CastingSlot::Gem2, -1, -1)) { + if (IsBotSpellTypeOtherBeneficial(BotSpellTypes::Teleport)) { + bot_iter->SetCastedSpellType(UINT16_MAX); + } + else { + bot_iter->SetCastedSpellType(BotSpellTypes::Teleport); + } + + Bot::RaidGroupSay( + bot_iter, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(itr->SpellId), + bot_iter->GetSpellTypeNameByID(BotSpellTypes::Teleport), + (tar == bot_iter ? "myself" : tar->GetCleanName()) + ).c_str() + ); + + is_success = true; + } + + bot_iter->SetCommandedSpell(false); + + if (is_success) { + return; + } + else { + continue; + } + } } - helper_no_available_bots(c, my_bot); + if ( + (list && list_zones.empty()) || + (!list && !is_success) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "No bots are capable of that on {}. Be sure they are in the same group, raid or raid group if necessary.", + tar ? tar->GetCleanName() : "you" + ).c_str() + ); + } } diff --git a/zone/bot_commands/discipline.cpp b/zone/bot_commands/discipline.cpp new file mode 100644 index 0000000000..146ed60906 --- /dev/null +++ b/zone/bot_commands/discipline.cpp @@ -0,0 +1,273 @@ +#include "../bot_command.h" + +void bot_command_discipline(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_discipline", sep->arg[0], "discipline")) { + c->Message(Chat::White, "note: Tells applicable bots to use the specified disciplines."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = + { + "Tells applicable bots to use the specified disciplines." + }; + p.notes = { "Aside from Lay On Hands and Harm Touch, you will need to know the spell ID of the discipline to tell a bot to attempt to use it." }; + p.example_format = { fmt::format("{} [aggressive | defensive | spell ID] [actionable, default: spawned]", sep->arg[0]) }; + p.examples_one = + { + "To tell all bots to use an aggressive discipline:", + fmt::format( + "{} aggressive spawned", + sep->arg[0] + ) + }; + p.examples_two = + { + "To tell Warrior bots to use a defensive discipline:", + fmt::format( + "{} defensive byclass {}", + sep->arg[0], + Class::Warrior + ) + }; + p.examples_three = + { + "To tell all bots to use their Fearless discipline:", + fmt::format( + "{} 4587 spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool aggressive = false; + bool defensive = false; + Mob* tar = c->GetTarget(); + uint16 spell_id = UINT16_MAX; + + if (!arg1.compare("aggressive")) { + aggressive = true; + } + else if (!arg1.compare("defensive")) { + defensive = true; + } + else if (sep->IsNumber(1)) { + if (!IsValidSpell(atoi(sep->arg[1]))) { + c->Message(Chat::Yellow, "You must enter a valid spell ID."); + return; + } + + spell_id = atoi(sep->arg[1]); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "spawned"; + } + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + bool is_success = false; + uint16 success_count = 0; + Bot* first_found = nullptr; + + for (auto bot_iter : sbl) { + if (!bot_iter->IsInGroupOrRaid(c)) { + continue; + } + + if (bot_iter->GetBotStance() == Stance::Passive || bot_iter->GetHoldFlag() || bot_iter->GetAppearance() == eaDead || bot_iter->IsFeared() || bot_iter->IsSilenced() || bot_iter->IsAmnesiad() || bot_iter->GetHP() < 0) { + continue; + } + + if (spell_id == UINT16_MAX) { // Aggressive/Defensive type + std::vector bot_spell_list; + + if (aggressive) { + bot_spell_list = bot_iter->BotGetSpellsByType(BotSpellTypes::DiscAggressive); + } + else if (defensive) { + bot_spell_list = bot_iter->BotGetSpellsByType(BotSpellTypes::DiscDefensive); + } + + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpell(bot_spell_list[i].spellid)) { + continue; + } + + if (!bot_iter->CheckDisciplineReuseTimer(bot_spell_list[i].spellid)) { + uint32 remaining_time = (bot_iter->GetDisciplineReuseRemainingTime(bot_spell_list[i].spellid) / 1000); + + bot_iter->OwnerMessage( + fmt::format( + "I can use this discipline in {}.", + Strings::SecondsToTime(remaining_time) + ) + ); + + continue; + } + + if (bot_iter->GetEndurance() < spells[bot_spell_list[i].spellid].endurance_cost) { + continue; + } + + if (bot_iter->DivineAura() && !IsCastNotStandingSpell(bot_spell_list[i].spellid)) { + continue; + } + + if (spells[bot_spell_list[i].spellid].buff_duration_formula != 0 && spells[bot_spell_list[i].spellid].target_type == ST_Self && bot_iter->HasDiscBuff()) { + continue; + } + + if (!tar || (spells[bot_spell_list[i].spellid].target_type == ST_Self && tar != bot_iter)) { + tar = bot_iter; + } + + if (bot_iter->AttemptForcedCastSpell(tar, bot_spell_list[i].spellid, true)) { + if (!first_found) { + first_found = bot_iter; + } + + is_success = true; + ++success_count; + spell_id = bot_spell_list[i].spellid; + } + } + } + else { // Direct spell ID + if (!IsValidSpell(spell_id)) { + continue; + } + + SPDat_Spell_Struct spell = spells[spell_id]; + + if (!bot_iter->CanUseBotSpell(spell_id)) { + continue; + } + + if (!bot_iter->CheckDisciplineReuseTimer(spell_id)) { + uint32 remaining_time = (bot_iter->GetDisciplineReuseRemainingTime(spell_id) / 1000); + + bot_iter->OwnerMessage( + fmt::format( + "I can use this item in {}.", + Strings::SecondsToTime(remaining_time) + ) + ); + + continue; + } + + if (bot_iter->GetEndurance() < spell.endurance_cost) { + continue; + } + + if (bot_iter->DivineAura() && !IsCastNotStandingSpell(spell_id)) { + continue; + } + + if (spell.buff_duration_formula != 0 && spell.target_type == ST_Self && bot_iter->HasDiscBuff()) { + continue; + } + + if (!tar || (spell.target_type == ST_Self && tar != bot_iter)) { + tar = bot_iter; + } + + if (bot_iter->AttemptForcedCastSpell(tar, spell_id, true)) { + if (!first_found) { + first_found = bot_iter; + } + + is_success = true; + ++success_count; + } + } + + continue; + } + + if (!is_success) { + c->Message(Chat::Yellow, "No bots were selected."); + } + else { + if (aggressive || defensive) { + c->Message( + Chat::Yellow, + fmt::format( + "{} {} {} {} discipline.", + ((success_count == 1 && first_found) ? first_found->GetCleanName() : (fmt::format("{}", success_count).c_str())), + ((success_count == 1 && first_found) ? "used" : "of your bots used"), + (aggressive ? "an" : "a"), + (aggressive ? "aggressive" : "defensive") + ).c_str() + ); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "{} {} their {} [#{}] discipline.", + ((success_count == 1 && first_found) ? first_found->GetCleanName() : (fmt::format("{}", success_count).c_str())), + ((success_count == 1 && first_found) ? "used" : "of your bots used"), + spells[spell_id].name, + spell_id + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/distance_ranged.cpp b/zone/bot_commands/distance_ranged.cpp new file mode 100644 index 0000000000..1c7857daae --- /dev/null +++ b/zone/bot_commands/distance_ranged.cpp @@ -0,0 +1,157 @@ +#include "../bot_command.h" + +void bot_command_distance_ranged(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_distance_ranged", sep->arg[0], "distanceranged")) { + c->Message(Chat::White, "note: Sets the distance bots will attempt to stay away from their target to cast or use ranged items."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Sets the distance bots will attempt to stay away from their target to cast or use ranged items." }; + p.notes = + { + "- Bots will stay between half the value of the setting and the current value. IE, if set to 60, bots will stay between 30 and 60.", + "- Casters will never go closer than their maximum melee range.", + "- Throwing bots will never get closer than the minimum value for ranged to work, or beyond the range of their items." + }; + p.example_format = { fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = { + "To set Wizards to a range of 100:", + fmt::format( + "{} 100 byclass {}", + sep->arg[0], + Class::Wizard + ) + }; + p.examples_two = { + "To set Rangers to a range of 175:", + fmt::format( + "{} 175 byclass {}", + sep->arg[0], + Class::Ranger + ) + }; + p.examples_three = { + "To view the current setting of all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + c->Message(Chat::White, "usage: %s [current | value: 0 - 300] ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "note: Use [current] to check the current setting."); + c->Message(Chat::White, "note: Set the value to the minimum distance you want your bot to try to remain from its target."); + c->Message(Chat::White, "note: If they are too far for a spell, it will be skipped."); + c->Message(Chat::White, "note: This is set to (90) units by default."); + } + + const int ab_mask = ActionableBots::ABM_Type1; + + std::string arg1 = sep->arg[1]; + int ab_arg = 1; + bool current_check = false; + uint32 value = 0; + + if (sep->IsNumber(1)) { + ++ab_arg; + value = atoi(sep->arg[1]); + if (value < 0 || value > RuleI(Bots, MaxDistanceRanged)) { + c->Message(Chat::Yellow, "You must enter a value within the range of 0 - 300."); + return; + } + } + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message(Chat::Yellow, "Incorrect argument, use %s help for a list of options.", sep->arg[0]); + return; + } + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My current Distance Ranged is {}.'", + my_bot->GetCleanName(), + my_bot->GetBotDistanceRanged() + ).c_str() + ); + } + else { + my_bot->SetBotDistanceRanged(value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My Distance Ranged was set to {}.'", + first_found->GetCleanName(), + value + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their Distance Ranged to {}.", + success_count, + value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/escape.cpp b/zone/bot_commands/escape.cpp deleted file mode 100644 index 31fde82a6b..0000000000 --- a/zone/bot_commands/escape.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "../bot_command.h" - -void bot_command_escape(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Escape]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Escape) || helper_command_alias_fail(c, "bot_command_escape", sep->arg[0], "escape")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s ([option: lesser])", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Escape); - return; - } - - bool use_lesser = false; - if (!strcasecmp(sep->arg[1], "lesser")) - use_lesser = true; - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToEscape(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->lesser != use_lesser) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/follow.cpp b/zone/bot_commands/follow.cpp index 65ec73f8b0..fe780d32eb 100644 --- a/zone/bot_commands/follow.cpp +++ b/zone/bot_commands/follow.cpp @@ -1,40 +1,85 @@ #include "../bot_command.h" -void bot_command_follow(Client *c, const Seperator *sep) +void bot_command_follow(Client* c, const Seperator* sep) { - if (helper_command_alias_fail(c, "bot_command_follow", sep->arg[0], "follow")) + if (helper_command_alias_fail(c, "bot_command_follow", sep->arg[0], "follow")) { + c->Message(Chat::White, "note: Sets bots of your choosing to follow your target, view their current following state or reset their following state."); + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s ([option: reset]) [actionable: byname | ownergroup | ownerraid | namesgroup | healrotation | byclass | byrace | spawned]] ([actionable_name])", sep->arg[0]); - c->Message(Chat::White, "usage: %s chain", sep->arg[0]); + BotCommandHelpParams p; + + p.description = { "Sets bots of your choosing to follow your target, view their current following state or reset their following state." }; + p.notes = { "- You can only follow players, bots or mercenaries belonging to your group or raid." }; + p.example_format = { fmt::format("{} [optional] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set all Clerics to follow your target:", + fmt::format( + "{} byclass {}", + sep->arg[0], + Class::Cleric + ) + }; + p.examples_two = + { + "To check the current state of all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To reset all bots:", + fmt::format( + "{} reset spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + return; } + const int ab_mask = ActionableBots::ABM_Type2; + bool chain = false; bool reset = false; + bool current_check = false; int ab_arg = 1; - int name_arg = 2; Mob* target_mob = nullptr; std::string optional_arg = sep->arg[1]; - if (!optional_arg.compare("chain")) { - - auto chain_count = helper_bot_follow_option_chain(c); - c->Message(Chat::White, "%i of your bots %s now chain following you", chain_count, (chain_count == 1 ? "is" : "are")); - - return; - } - else if (!optional_arg.compare("reset")) { + + if (!optional_arg.compare("reset")) { + target_mob = c; reset = true; - ab_arg = 2; - name_arg = 3; + ++ab_arg; + } + else if (!optional_arg.compare("current")) { + current_check = true; + ++ab_arg; } else { - target_mob = ActionableTarget::VerifyFriendly(c, BCEnum::TT_Single); - if (!target_mob) { - c->Message(Chat::White, "You must a friendly mob to use this command"); + target_mob = c->GetTarget(); + + if (!target_mob || !target_mob->IsOfClientBotMerc() || !c->IsInGroupOrRaid(target_mob)) { + c->Message(Chat::Yellow, "You must a friendly player, bot or merc within your group or raid to use this command"); return; } + + if (!optional_arg.compare("chain")) { + chain = true; + ++ab_arg; + } } std::string class_race_arg = sep->arg[ab_arg]; @@ -43,69 +88,147 @@ void bot_command_follow(Client *c, const Seperator *sep) class_race_check = true; } - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[name_arg] : nullptr, class_race_check ? atoi(sep->arg[name_arg]) : 0) == ActionableBots::ABT_None) { + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + auto bot_count = sbl.size(); + Mob* follow_mob = nullptr; + std::list chain_list; + std::list::const_iterator it = chain_list.begin(); + uint16 count = 0; for (auto bot_iter : sbl) { + if (current_check) { + follow_mob = entity_list.GetMob(bot_iter->GetFollowID()); + + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I am currently following {}.'", + bot_iter->GetCleanName(), + follow_mob ? follow_mob->GetCleanName() : "no one" + ).c_str() + ); + + if (!follow_mob && RuleB(Bots, DoResponseAnimations)) { + bot_iter->DoAnim(28); + } + + continue; + } + + if (bot_iter == target_mob) { + if (bot_count == 1) { + c->Message( + Chat::Yellow, + fmt::format( + "{} says, 'I cannot follow myself, you want me to run circles?", + bot_iter->GetCleanName() + ).c_str() + ); + + if (RuleB(Bots, DoResponseAnimations)) { + bot_iter->DoAnim(60); + } + + return; + } + + bot_iter->WipeHateList(); + --bot_count; + + continue; + } + + if (!bot_iter->IsInGroupOrRaid(target_mob)) { + --bot_count; + + continue; + } + bot_iter->WipeHateList(); - auto my_group = bot_iter->GetGroup(); - if (my_group) { - if (reset) { - if (!my_group->GetLeader() || my_group->GetLeader() == bot_iter) - bot_iter->SetFollowID(c->GetID()); - else - bot_iter->SetFollowID(my_group->GetLeader()->GetID()); + if (!bot_iter->GetGroup() && !bot_iter->GetRaid()) { + bot_iter->SetFollowID(0); + bot_iter->SetManualFollow(false); + } + else { + if (reset) { + bot_iter->SetFollowID(c->GetID()); bot_iter->SetManualFollow(false); } else { - if (bot_iter == target_mob) - bot_iter->SetFollowID(c->GetID()); - else + if (chain) { + Mob* next_tar = target_mob; + + if (count > 0) { + next_tar = *it; + + if (!next_tar) { + next_tar = target_mob; + } + } + + chain_list.push_back(bot_iter); + ++it; + ++count; + bot_iter->SetFollowID(next_tar->GetID()); + } + else { bot_iter->SetFollowID(target_mob->GetID()); + } bot_iter->SetManualFollow(true); } } - else { - bot_iter->SetFollowID(0); - bot_iter->SetManualFollow(false); - } - if (!bot_iter->GetPet()) + + if (!bot_iter->GetPet()) { continue; + } bot_iter->GetPet()->WipeHateList(); bot_iter->GetPet()->SetFollowID(bot_iter->GetID()); } - if (sbl.size() == 1) { - Mob* follow_mob = entity_list.GetMob(sbl.front()->GetFollowID()); - Bot::BotGroupSay( - sbl.front(), + if (current_check || !bot_count) { + return; + } + + follow_mob = target_mob; + + if (bot_count == 1) { + follow_mob = entity_list.GetMob(sbl.front()->GetFollowID()); + + c->Message( + Chat::Green, fmt::format( - "Following {}.", - follow_mob ? follow_mob->GetCleanName() : "no one" + "{} says, 'Following {}.'", + sbl.front()->GetCleanName(), + follow_mob ? follow_mob->GetCleanName() : "you" ).c_str() ); - } else { + } + else { if (reset) { c->Message( - Chat::White, + Chat::Green, fmt::format( - "{} of your bots are following their default assignments.", - sbl.size() + "{} of your bots are following you.", + bot_count ).c_str() ); - } else { + } + else { c->Message( - Chat::White, + Chat::Green, fmt::format( - "{} of your bots are following {}.", - sbl.size(), - target_mob->GetCleanName() + "{} of your bots are {} {}.", + bot_count, + chain ? "chain following" : "following", + follow_mob ? follow_mob->GetCleanName() : "you" ).c_str() ); } diff --git a/zone/bot_commands/guard.cpp b/zone/bot_commands/guard.cpp index 27a9ed3430..808de155d5 100644 --- a/zone/bot_commands/guard.cpp +++ b/zone/bot_commands/guard.cpp @@ -7,7 +7,7 @@ void bot_command_guard(Client *c, const Seperator *sep) } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([option: clear]) [actionable: byname | ownergroup | ownerraid | namesgroup | healrotation | byclass | byrace | spawned]] ([actionable_name])", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([option: clear]) [actionable: byname | ownergroup | ownerraid | namesgroup | healrotation | mmr | byclass | byrace | default: spawned] ([actionable_name])", sep->arg[0]); return; } const int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_Type2); @@ -20,8 +20,8 @@ void bot_command_guard(Client *c, const Seperator *sep) if (!clear_arg.compare("clear")) { clear = true; - ab_arg = 2; - name_arg = 3; + ++ab_arg; + ++name_arg; } std::string class_race_arg = sep->arg[ab_arg]; @@ -30,12 +30,12 @@ void bot_command_guard(Client *c, const Seperator *sep) class_race_check = true; } - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[name_arg] : nullptr, class_race_check ? atoi(sep->arg[name_arg]) : 0) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); for (auto bot_iter : sbl) { if (clear) { @@ -47,7 +47,7 @@ void bot_command_guard(Client *c, const Seperator *sep) } if (sbl.size() == 1) { - Bot::BotGroupSay( + Bot::RaidGroupSay( sbl.front(), fmt::format( "{}uarding this position.", diff --git a/zone/bot_commands/heal_rotation.cpp b/zone/bot_commands/heal_rotation.cpp index b755016fcc..2ffd6df23b 100644 --- a/zone/bot_commands/heal_rotation.cpp +++ b/zone/bot_commands/heal_rotation.cpp @@ -2,28 +2,28 @@ void bot_command_heal_rotation(Client *c, const Seperator *sep) { - - std::list subcommand_list; - subcommand_list.push_back("healrotationadaptivetargeting"); - subcommand_list.push_back("healrotationaddmember"); - subcommand_list.push_back("healrotationaddtarget"); - subcommand_list.push_back("healrotationadjustcritical"); - subcommand_list.push_back("healrotationadjustsafe"); - subcommand_list.push_back("healrotationcastoverride"); - subcommand_list.push_back("healrotationchangeinterval"); - subcommand_list.push_back("healrotationclearhot"); - subcommand_list.push_back("healrotationcleartargets"); - subcommand_list.push_back("healrotationcreate"); - subcommand_list.push_back("healrotationdelete"); - subcommand_list.push_back("healrotationfastheals"); - subcommand_list.push_back("healrotationlist"); - subcommand_list.push_back("healrotationremovemember"); - subcommand_list.push_back("healrotationremovetarget"); - subcommand_list.push_back("healrotationresetlimits"); - subcommand_list.push_back("healrotationsave"); - subcommand_list.push_back("healrotationsethot"); - subcommand_list.push_back("healrotationstart"); - subcommand_list.push_back("healrotationstop"); + std::vector subcommand_list = { + "healrotationadaptivetargeting", + "healrotationaddmember", + "healrotationaddtarget", + "healrotationadjustcritical", + "healrotationadjustsafe", + "healrotationcastoverride", + "healrotationchangeinterval", + "healrotationclearhot", + "healrotationcleartargets", + "healrotationcreate", + "healrotationdelete", + "healrotationfastheals", + "healrotationlist", + "healrotationremovemember", + "healrotationremovetarget", + "healrotationresetlimits", + "healrotationsave", + "healrotationsethot", + "healrotationstart", + "healrotationstop" + }; if (helper_command_alias_fail(c, "bot_command_heal_rotation", sep->arg[0], "healrotation")) return; @@ -63,7 +63,7 @@ void bot_command_heal_rotation_adaptive_targeting(Client* c, const Seperator* se std::string adaptive_targeting_arg; - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (!sbl.empty()) { adaptive_targeting_arg = sep->arg[2]; @@ -123,7 +123,7 @@ void bot_command_heal_rotation_add_member(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { c->Message(Chat::White, "You must [name] a new member as a bot that you own to use this command"); @@ -191,7 +191,7 @@ void bot_command_heal_rotation_add_target(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[2]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -283,7 +283,7 @@ void bot_command_heal_rotation_adjust_critical(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[3]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -383,7 +383,7 @@ void bot_command_heal_rotation_adjust_safe(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[3]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -460,7 +460,7 @@ void bot_command_heal_rotation_casting_override(Client* c, const Seperator* sep) std::string casting_override_arg; - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (!sbl.empty()) { casting_override_arg = sep->arg[2]; @@ -534,7 +534,7 @@ void bot_command_heal_rotation_change_interval(Client* c, const Seperator* sep) std::string change_interval_arg; - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (!sbl.empty()) { change_interval_arg = sep->arg[2]; @@ -603,7 +603,7 @@ void bot_command_heal_rotation_clear_hot(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -649,7 +649,7 @@ void bot_command_heal_rotation_clear_targets(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -703,7 +703,7 @@ void bot_command_heal_rotation_create(Client* c, const Seperator* sep) std::string adaptive_targeting_arg; std::string casting_override_arg; - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (!sbl.empty()) { interval_arg = sep->arg[2]; @@ -858,7 +858,7 @@ void bot_command_heal_rotation_delete(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[name_arg]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -896,7 +896,7 @@ void bot_command_heal_rotation_fast_heals(Client* c, const Seperator* sep) std::string fast_heals_arg; - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (!sbl.empty()) { fast_heals_arg = sep->arg[2]; @@ -956,7 +956,7 @@ void bot_command_heal_rotation_list(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -1076,7 +1076,7 @@ void bot_command_heal_rotation_remove_member(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -1123,7 +1123,7 @@ void bot_command_heal_rotation_remove_target(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[2]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -1181,7 +1181,7 @@ void bot_command_heal_rotation_reset_limits(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -1223,7 +1223,7 @@ void bot_command_heal_rotation_save(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -1272,7 +1272,7 @@ void bot_command_heal_rotation_set_hot(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[2]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -1337,7 +1337,7 @@ void bot_command_heal_rotation_start(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); @@ -1384,7 +1384,7 @@ void bot_command_heal_rotation_stop(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_ByNamedBot(c, sbl, sep->arg[1]); if (sbl.empty()) { MyBots::PopulateSBL_ByTargetedBot(c, sbl); diff --git a/zone/bot_commands/hold.cpp b/zone/bot_commands/hold.cpp index 49f0f516fb..56ca1f5a8d 100644 --- a/zone/bot_commands/hold.cpp +++ b/zone/bot_commands/hold.cpp @@ -7,7 +7,7 @@ void bot_command_hold(Client *c, const Seperator *sep) } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([option: clear]) [actionable: byname | ownergroup | ownerraid | namesgroup | healrotation | byclass | byrace | spawned]] ([actionable_name])", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([option: clear]) [actionable: byname | ownergroup | ownerraid | namesgroup | healrotation | mmr | byclass | byrace | default: spawned] ([actionable_name])", sep->arg[0]); return; } const int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_Type2); @@ -20,8 +20,8 @@ void bot_command_hold(Client *c, const Seperator *sep) if (!clear_arg.compare("clear")) { clear = true; - ab_arg = 2; - name_arg = 3; + ++ab_arg; + ++name_arg; } std::string class_race_arg = sep->arg[ab_arg]; @@ -30,12 +30,12 @@ void bot_command_hold(Client *c, const Seperator *sep) class_race_check = true; } - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[name_arg] : nullptr, class_race_check ? atoi(sep->arg[name_arg]) : 0) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); for (auto bot_iter : sbl) { if (clear) { @@ -47,7 +47,7 @@ void bot_command_hold(Client *c, const Seperator *sep) } if (sbl.size() == 1) { - Bot::BotGroupSay( + Bot::RaidGroupSay( sbl.front(), fmt::format( "{}olding my attacks.", diff --git a/zone/bot_commands/identify.cpp b/zone/bot_commands/identify.cpp deleted file mode 100644 index 7ac90a4e4e..0000000000 --- a/zone/bot_commands/identify.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "../bot_command.h" - -void bot_command_identify(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Identify]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Identify) || helper_command_alias_fail(c, "bot_command_identify", sep->arg[0], "identify")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Identify); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter; - if (helper_spell_check_fail(local_entry)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/illusion_block.cpp b/zone/bot_commands/illusion_block.cpp new file mode 100644 index 0000000000..01c9eafa46 --- /dev/null +++ b/zone/bot_commands/illusion_block.cpp @@ -0,0 +1,157 @@ +#include "../bot_command.h" + +void bot_command_illusion_block(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_illusion_block", sep->arg[0], "illusionblock")) { + c->Message(Chat::White, "note: Toggles whether or not bots will block the illusion effects of spells cast by players or bots."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Toggles whether or not bots will block the illusion effects of spells cast by players or bots." }; + p.example_format = { fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set BotA to block illusions:", + fmt::format( + "{} 1 byname BotA", + sep->arg[0] + ) + }; + p.examples_two = + { + "To set all bots to block illusions:", + fmt::format( + "{} 1 spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To check the illusion block status for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + + int ab_arg = 1; + bool current_check = false; + uint32 type_value = 0; + + if (sep->IsNumber(1)) { + type_value = atoi(sep->arg[1]); + ++ab_arg; + if (type_value < 0 || type_value > 1) { + c->Message(Chat::Yellow, "You must enter either 0 for disabled or 1 for enabled."); + + return; + } + } + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} block illusions.'", + my_bot->GetCleanName(), + my_bot->GetIllusionBlock() ? "will" : "will not" + ).c_str() + ); + } + else { + my_bot->SetIllusionBlock(type_value); + ++success_count; + } + } + + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} block illusions.'", + first_found->GetCleanName(), + first_found->GetIllusionBlock() ? "will now" : "will no longer" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots {} block illusions.", + success_count, + type_value ? "will now" : "will no longer" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/inventory.cpp b/zone/bot_commands/inventory.cpp index b934f96a25..9d8511cf9d 100644 --- a/zone/bot_commands/inventory.cpp +++ b/zone/bot_commands/inventory.cpp @@ -2,12 +2,12 @@ void bot_command_inventory(Client *c, const Seperator *sep) { - - std::list subcommand_list; - subcommand_list.push_back("inventorygive"); - subcommand_list.push_back("inventorylist"); - subcommand_list.push_back("inventoryremove"); - subcommand_list.push_back("inventorywindow"); + std::vector subcommand_list = { + "inventorygive", + "inventorylist", + "inventoryremove", + "inventorywindow" + }; if (helper_command_alias_fail(c, "bot_command_inventory", sep->arg[0], "inventory")) return; @@ -25,7 +25,7 @@ void bot_command_inventory_give(Client* c, const Seperator* sep) c->Message( Chat::White, fmt::format( - "Usage: {} ([actionable: target | byname] ([actionable_name]))", + "Usage: {} ([actionable: target | byname] ([actionable_name])) [optional: slot ID]", sep->arg[0] ).c_str() ); @@ -33,19 +33,45 @@ void bot_command_inventory_give(Client* c, const Seperator* sep) } int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_ByName); + int ab_arg = 1; + int slot_arg = 1; + int16 chosen_slot = INVALID_INDEX; + bool byname = false; - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) { + std::string byname_arg = sep->arg[ab_arg]; + + if (!byname_arg.compare("byname")) { + byname = true; + slot_arg = ab_arg + 2; + } + + if (sep->IsNumber(slot_arg)) { + chosen_slot = atoi(sep->arg[slot_arg]); + + if (!EQ::ValueWithin(chosen_slot, EQ::invslot::EQUIPMENT_BEGIN, EQ::invslot::EQUIPMENT_END)) { + c->Message(Chat::Yellow, "Please enter a valid inventory slot."); + + return; + } + + if (!byname) { + ++ab_arg; + } + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, sep->arg[ab_arg + 1]) == ActionableBots::ABT_None) { return; } auto my_bot = sbl.front(); if (!my_bot) { - c->Message(Chat::White, "ActionableBots returned 'nullptr'"); + c->Message(Chat::Yellow, "ActionableBots returned 'nullptr'"); + return; } - my_bot->FinishTrade(c, Bot::BotTradeClientNoDropNoTrade); + my_bot->FinishTrade(c, Bot::BotTradeClientNoDropNoTrade, chosen_slot); } void bot_command_inventory_list(Client* c, const Seperator* sep) @@ -67,7 +93,7 @@ void bot_command_inventory_list(Client* c, const Seperator* sep) int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_ByName); - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) { return; } @@ -168,7 +194,7 @@ void bot_command_inventory_remove(Client* c, const Seperator* sep) return; } - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[2], sbl, ab_mask, sep->arg[3]) == ActionableBots::ABT_None) { return; } @@ -217,9 +243,18 @@ void bot_command_inventory_remove(Client* c, const Seperator* sep) } const auto* itm = inst->GetItem(); + EQ::SayLinkEngine linker; + linker.SetLinkType(EQ::saylink::SayLinkItemInst); + linker.SetItemInst(inst); if (inst && itm && c->CheckLoreConflict(itm)) { - c->MessageString(Chat::White, PICK_LORE); + c->Message( + Chat::White, + fmt::format( + "You cannot pick up {} because it is a lore item you already possess.", + linker.GenerateLink() + ).c_str() + ); return; } @@ -233,7 +268,13 @@ void bot_command_inventory_remove(Client* c, const Seperator* sep) continue; } - c->MessageString(Chat::White, PICK_LORE); + c->Message( + Chat::White, + fmt::format( + "You cannot pick up {} because it is a lore item you already possess.", + linker.GenerateLink() + ).c_str() + ); return; } @@ -247,7 +288,7 @@ void bot_command_inventory_remove(Client* c, const Seperator* sep) slot_id == EQ::invslot::slotRange || slot_id == EQ::invslot::slotAmmo ) { - my_bot->SetBotArcherySetting(false, true); + my_bot->SetBotRangedSetting(false); } my_bot->RemoveBotItemBySlot(slot_id); @@ -296,7 +337,7 @@ void bot_command_inventory_window(Client* c, const Seperator* sep) int ab_mask = ActionableBots::ABM_Target; - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) { return; } diff --git a/zone/bot_commands/invisibility.cpp b/zone/bot_commands/invisibility.cpp deleted file mode 100644 index dd826ebc8a..0000000000 --- a/zone/bot_commands/invisibility.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "../bot_command.h" - -void bot_command_invisibility(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Invisibility]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Invisibility) || helper_command_alias_fail(c, "bot_command_invisibility", sep->arg[0], "invisibility")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s [invisibility: living | undead | animal | see]", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Invisibility); - return; - } - - std::string invisibility = sep->arg[1]; - - BCEnum::IType invisibility_type = BCEnum::IT_None; - if (!invisibility.compare("living")) - invisibility_type = BCEnum::IT_Living; - else if (!invisibility.compare("undead")) - invisibility_type = BCEnum::IT_Undead; - else if (!invisibility.compare("animal")) - invisibility_type = BCEnum::IT_Animal; - else if (!invisibility.compare("see")) - invisibility_type = BCEnum::IT_See; - - if (invisibility_type == BCEnum::IT_None) { - c->Message(Chat::White, "You must specify an [invisibility]"); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToInvisibility(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->invis_type != invisibility_type) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/item_use.cpp b/zone/bot_commands/item_use.cpp index 1640ff0e33..b9cf6a803a 100644 --- a/zone/bot_commands/item_use.cpp +++ b/zone/bot_commands/item_use.cpp @@ -4,16 +4,17 @@ void bot_command_item_use(Client* c, const Seperator* sep) { if (helper_is_help_or_usage(sep->arg[1])) { c->Message(Chat::White, "usage: [%s empty] will display only bots that can use the item in an empty slot.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s byclass classID] - Example: [%s byclass 7] will display only bots that match the class that can use the item. Example is a Monk, use [^create help] for a list of class IDs.", sep->arg[0], sep->arg[0]); - c->Message(Chat::White, "usage: [%s casteronly] will display only caster bots that can use the item.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s hybridonly] will display only hybrid bots that can use the item.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s meleeonly] will display only melee bots that can use the item.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s wiscasteronly] will display only Wisdom-based Caster bots that can use the item.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s intcasteronly] will display only Intelligence-based Caster bots that can use the item.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s plateonly] will display only Plate-wearing bots that can use the item.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s chainonly] will display only Chain-wearing bots that can use the item.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s leatheronly] will display only Leather-wearing bots that can use the item.", sep->arg[0]); - c->Message(Chat::White, "usage: [%s clothonly] will display only Cloth-wearing bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s caster] will display only caster bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s hybrid] will display only hybrid bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s melee] will display only melee bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s wiscaster] will display only Wisdom-based Caster bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s intcaster] will display only Intelligence-based Caster bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s plate] will display only Plate-wearing bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s chain] will display only Chain-wearing bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s leather] will display only Leather-wearing bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s cloth] will display only Cloth-wearing bots that can use the item.", sep->arg[0]); + c->Message(Chat::White, "usage: [%s haste] will display bots that have no or lesser haste than the item.", sep->arg[0]); + c->Message(Chat::White, "usage: You can also use empty or haste as an argument to narrow down further, for example [%s caster empty], [%s plate haste] or even [%s empty haste]", sep->arg[0], sep->arg[0], sep->arg[0]); return; } @@ -28,136 +29,252 @@ void bot_command_item_use(Client* c, const Seperator* sep) bool chain_only = false; bool leather_only = false; bool cloth_only = false; + bool haste_only = false; + int haste_value = 0; + int ab_arg = 2; std::string arg1 = sep->arg[1]; std::string arg2 = sep->arg[2]; - if (arg1.compare("empty") == 0) { + + if (arg1.compare("empty") == 0 || arg2.compare("empty") == 0) { empty_only = true; + + if (arg2.compare("empty") == 0) { + ++ab_arg; + } } - else if (arg1.compare("byclass") == 0) { - if (Strings::IsNumber(sep->arg[2])) { - class_mask = Strings::ToUnsignedInt(sep->arg[2]); - if (!(class_mask >= Class::Warrior && class_mask <= Class::Berserker)) { - c->Message(Chat::White, "Invalid class range, you must choose between 1 (Warrior) and 15 (Beastlord)"); - return; - } + + if (arg1.compare("haste") == 0 || arg2.compare("haste") == 0) { + haste_only = true; + + if (arg2.compare("haste") == 0) { + ++ab_arg; } } - else if (arg1.compare("casteronly") == 0) { + + if (arg1.compare("caster") == 0) { caster_only = true; } - else if (arg1.compare("hybridonly") == 0) { + else if (arg1.compare("hybrid") == 0) { hybrid_only = true; } - else if (arg1.compare("meleeonly") == 0) { + else if (arg1.compare("melee") == 0) { melee_only = true; } - else if (arg1.compare("wiscasteronly") == 0) { + else if (arg1.compare("wiscaster") == 0) { wis_caster_only = true; } - else if (arg1.compare("intcasteronly") == 0) { + else if (arg1.compare("intcaster") == 0) { int_caster_only = true; } - else if (arg1.compare("plateonly") == 0) { + else if (arg1.compare("plate") == 0) { plate_only = true; } - else if (arg1.compare("chainonly") == 0) { + else if (arg1.compare("chain") == 0) { chain_only = true; } - else if (arg1.compare("leatheronly") == 0) { + else if (arg1.compare("leather") == 0) { leather_only = true; } - else if (arg1.compare("clothonly") == 0) { + else if (arg1.compare("cloth") == 0) { cloth_only = true; } - else if (!arg1.empty()) { - c->Message(Chat::White, "Please choose the correct subtype. For help use %s help.", sep->arg[0]); - return; + else { + if (arg1.empty()) { + --ab_arg; + } + else { + if (!(arg1.compare("empty") == 0) && !(arg1.compare("haste") == 0)) { + c->Message(Chat::White, "Please choose the correct subtype. For help use %s help.", sep->arg[0]); + + return; + } + } } + const auto item_instance = c->GetInv().GetItem(EQ::invslot::slotCursor); + if (!item_instance) { - c->Message(Chat::White, "No item found on cursor!"); + c->Message(Chat::Yellow, "No item found on cursor! For help use %s help.", sep->arg[0]); + return; } auto item_data = item_instance->GetItem(); + if (!item_data) { - c->Message(Chat::White, "No data found for cursor item!"); + c->Message(Chat::Yellow, "No data found for cursor item!"); + return; } if (item_data->ItemClass != EQ::item::ItemClassCommon || item_data->Slots == 0) { - c->Message(Chat::White, "'%s' is not an equipable item!", item_data->Name); + c->Message(Chat::Yellow, "'%s' is not an equipable item!", item_data->Name); + return; } std::vector equipable_slot_list; + for (int16 equipable_slot = EQ::invslot::EQUIPMENT_BEGIN; equipable_slot <= EQ::invslot::EQUIPMENT_END; ++equipable_slot) { if (item_data->Slots & (1 << equipable_slot)) { equipable_slot_list.emplace_back(equipable_slot); } } - EQ::SayLinkEngine linker; - linker.SetLinkType(EQ::saylink::SayLinkItemInst); + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "spawned"; + } - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; - if (class_mask) { - ActionableBots::Filter_ByClasses(c, sbl, GetPlayerClassBit(class_mask)); + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; } + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + EQ::SayLinkEngine linker; + linker.SetLinkType(EQ::saylink::SayLinkItemData); + for (const auto& bot_iter : sbl) { if (!bot_iter) { continue; } + if (caster_only && !IsCasterClass(bot_iter->GetClass())) { continue; } + if (hybrid_only && !IsSpellFighterClass(bot_iter->GetClass())) { continue; } + if (melee_only && !IsNonSpellFighterClass(bot_iter->GetClass())) { continue; } + if (wis_caster_only && !IsWISCasterClass(bot_iter->GetClass())) { continue; } + if (int_caster_only && !IsINTCasterClass(bot_iter->GetClass())) { continue; } + if (plate_only && !IsPlateClass(bot_iter->GetClass())) { continue; } + if (chain_only && !IsChainClass(bot_iter->GetClass())) { continue; } + if (leather_only && !IsLeatherClass(bot_iter->GetClass())) { continue; } + if (cloth_only && !IsClothClass(bot_iter->GetClass())) { continue; } - if (((~item_data->Races) & GetPlayerRaceBit(bot_iter->GetRace())) || ((~item_data->Classes) & GetPlayerClassBit(bot_iter->GetClass()))) { + + if ( + (!RuleB(Bots, AllowBotEquipAnyRaceGear) && ((~item_data->Races) & GetPlayerRaceBit(bot_iter->GetRace()))) || + (!RuleB(Bots, AllowBotEquipAnyClassGear) && ((~item_data->Classes) & GetPlayerClassBit(bot_iter->GetClass()))) + ) { continue; } + std::list refined_equipable_slot_list; + bool skip_bot = false; + const EQ::ItemData* equipped_item = nullptr; + const EQ::ItemInstance* equipped_inst = nullptr; + for (const auto& slot_iter : equipable_slot_list) { // needs more failure criteria - this should cover the bulk for now if (slot_iter == EQ::invslot::slotSecondary && item_data->Damage && !bot_iter->CanThisClassDualWield()) { continue; } - auto equipped_item = bot_iter->GetInv()[slot_iter]; + if (item_data->ReqLevel > bot_iter->GetLevel()) { + continue; + } + + haste_value = 0; + equipped_item = nullptr; + equipped_inst = nullptr; + + + for (int16 equipable_slot = EQ::invslot::EQUIPMENT_BEGIN; equipable_slot <= EQ::invslot::EQUIPMENT_END; ++equipable_slot) { + equipped_inst = bot_iter->GetInv()[equipable_slot]; + if (equipped_inst && equipped_inst->GetItem()) { + equipped_item = equipped_inst->GetItem(); - if (equipped_item && !empty_only) { - linker.SetItemInst(equipped_item); + if (item_data->CheckLoreConflict(equipped_item)) { + skip_bot = true; + break; + } + + if (haste_only) { + if (equipped_item->Haste > haste_value) { + haste_value = equipped_item->Haste; + } + } + } + } + + if (skip_bot) { + break; + } + + if (haste_only && item_data->Haste < haste_value) { + continue; + } + + equipped_inst = bot_iter->GetInv()[slot_iter]; + + if (equipped_inst && empty_only) { + continue; + } + + refined_equipable_slot_list.push_back(slot_iter); + } + + if (skip_bot) { + continue; + } + + if (refined_equipable_slot_list.empty()) { + continue; + } + + for (auto slot_iter : refined_equipable_slot_list) { + equipped_item = nullptr; + equipped_inst = nullptr; + + equipped_inst = bot_iter->GetInv()[slot_iter]; + + if (equipped_inst && equipped_inst->GetItem()) { + equipped_item = equipped_inst->GetItem(); + } + + if (equipped_item) { + linker.SetItemData(equipped_item); c->Message( Chat::Say, fmt::format( - "{} says, 'I can use that for my {} instead of my {}! Would you like to {} my {}?'", + "{} says, 'I can use that for my {} instead of my {}! Would you like to {}?'", Saylink::Silent( fmt::format( "^inventorygive byname {}", @@ -173,19 +290,16 @@ void bot_command_item_use(Client* c, const Seperator* sep) slot_iter, bot_iter->GetCleanName() ), - "remove" - ), - linker.GenerateLink() + "remove my item" + ) ).c_str() ); - - bot_iter->DoAnim(29); } - else if (!equipped_item) { + else { c->Message( Chat::Say, fmt::format( - "{} says, 'I can use that for my {}! Would you like to {} it to me?'", + "{} says, 'I can use that for my {}! Would you like to {}?'", Saylink::Silent( fmt::format( "^inventorygive byname {}", @@ -199,11 +313,13 @@ void bot_command_item_use(Client* c, const Seperator* sep) "^inventorygive byname {}", bot_iter->GetCleanName() ), - "give" + "give it to me" ) ).c_str() ); + } + if (RuleB(Bots, DoResponseAnimations)) { bot_iter->DoAnim(29); } } diff --git a/zone/bot_commands/levitation.cpp b/zone/bot_commands/levitation.cpp deleted file mode 100644 index d4b5cee28e..0000000000 --- a/zone/bot_commands/levitation.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "../bot_command.h" - -void bot_command_levitation(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Levitation]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Levitation) || helper_command_alias_fail(c, "bot_command_levitation", sep->arg[0], "levitation")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Levitation); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter; - if (helper_spell_check_fail(local_entry)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/lull.cpp b/zone/bot_commands/lull.cpp deleted file mode 100644 index d39f534d16..0000000000 --- a/zone/bot_commands/lull.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "../bot_command.h" - -void bot_command_lull(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Lull]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Lull) || helper_command_alias_fail(c, "bot_command_lull", sep->arg[0], "lull")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Lull); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - for (auto list_iter : *local_list) { - auto local_entry = list_iter; - if (helper_spell_check_fail(local_entry)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, ENEMY); - if (!target_mob) - continue; - - //if (spells[local_entry->spell_id].max[EFFECTIDTOINDEX(3)] && spells[local_entry->spell_id].max[EFFECTIDTOINDEX(3)] < target_mob->GetLevel()) - // continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - uint32 dont_root_before = 0; - if (helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id, true, &dont_root_before)) - target_mob->SetDontRootMeBefore(dont_root_before); - - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/max_melee_range.cpp b/zone/bot_commands/max_melee_range.cpp new file mode 100644 index 0000000000..54749c47ca --- /dev/null +++ b/zone/bot_commands/max_melee_range.cpp @@ -0,0 +1,155 @@ +#include "../bot_command.h" + +void bot_command_max_melee_range(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_max_melee_range", sep->arg[0], "maxmeleerange")) { + c->Message(Chat::White, "note: Toggles whether or not bots will stay at max melee range during combat."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Toggles whether or not bots will stay at max melee range during combat."}; + p.example_format ={ fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set BotA to stay at max melee range:", + fmt::format( + "{} 1 byname BotA", + sep->arg[0] + ) + }; + p.examples_two = + { + "To set all bots to stay at max melee range:", + fmt::format( + "{} 1 spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To check the max melee range status for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + + int ab_arg = 1; + bool current_check = false; + uint32 type_value = 0; + + if (sep->IsNumber(1)) { + type_value = atoi(sep->arg[1]); + ++ab_arg; + if (type_value < 0 || type_value > 1) { + c->Message(Chat::Yellow, "You must enter either 0 for disabled or 1 for enabled."); + + return; + } + } + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} stay at max melee range.'", + my_bot->GetCleanName(), + my_bot->GetMaxMeleeRange() ? "will" : "will not" + ).c_str() + ); + } + else { + my_bot->SetMaxMeleeRange(type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} stay at max melee range.'", + first_found->GetCleanName(), + first_found->GetMaxMeleeRange() ? "will now" : "will no longer" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots {} stay at max melee range.", + success_count, + type_value ? "will now" : "will no longer" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/mesmerize.cpp b/zone/bot_commands/mesmerize.cpp deleted file mode 100644 index 532a3c4049..0000000000 --- a/zone/bot_commands/mesmerize.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "../bot_command.h" - -void bot_command_mesmerize(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Mesmerize]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Mesmerize) || helper_command_alias_fail(c, "bot_command_mesmerize", sep->arg[0], "mesmerize")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Mesmerize); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - for (auto list_iter : *local_list) { - auto local_entry = list_iter; - if (helper_spell_check_fail(local_entry)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, ENEMY); - if (!target_mob) - continue; - - if (spells[local_entry->spell_id].max_value[EFFECTIDTOINDEX(1)] < target_mob->GetLevel()) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - uint32 dont_root_before = 0; - if (helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id, true, &dont_root_before)) - target_mob->SetDontRootMeBefore(dont_root_before); - - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/movement_speed.cpp b/zone/bot_commands/movement_speed.cpp deleted file mode 100644 index d1b8841546..0000000000 --- a/zone/bot_commands/movement_speed.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "../bot_command.h" - -void bot_command_movement_speed(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_MovementSpeed]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_MovementSpeed) || helper_command_alias_fail(c, "bot_command_movement_speed", sep->arg[0], "movementspeed")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s ([group | sow])", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_MovementSpeed); - return; - } - - bool group = false; - bool sow = false; - std::string arg1 = sep->arg[1]; - if (!arg1.compare("group")) - group = true; - else if (!arg1.compare("sow")) - sow = true; - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToMovementSpeed(); - if (helper_spell_check_fail(local_entry)) - continue; - if (!sow && (local_entry->group != group)) - continue; - if (sow && (local_entry->spell_id != 278)) // '278' = single-target "Spirit of Wolf" - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/pet.cpp b/zone/bot_commands/pet.cpp index f6c6ae21dd..712b551180 100644 --- a/zone/bot_commands/pet.cpp +++ b/zone/bot_commands/pet.cpp @@ -1,12 +1,13 @@ #include "../bot_command.h" +#include "../bot.h" void bot_command_pet(Client *c, const Seperator *sep) { - - std::list subcommand_list; - subcommand_list.push_back("petgetlost"); - subcommand_list.push_back("petremove"); - subcommand_list.push_back("petsettype"); + std::vector subcommand_list = { + "petgetlost", + "petremove", + "petsettype" + }; if (helper_command_alias_fail(c, "bot_command_pet", sep->arg[0], "pet")) return; @@ -19,12 +20,12 @@ void bot_command_pet_get_lost(Client *c, const Seperator *sep) if (helper_command_alias_fail(c, "bot_command_pet_get_lost", sep->arg[0], "petgetlost")) return; if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | targetgroup | namesgroup | healrotation | spawned] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationmembers | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); return; } int ab_mask = ActionableBots::ABM_NoFilter; - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) return; @@ -55,7 +56,7 @@ void bot_command_pet_remove(Client *c, const Seperator *sep) } int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_ByName); - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) return; @@ -65,7 +66,7 @@ void bot_command_pet_remove(Client *c, const Seperator *sep) c->Message(Chat::White, "You have no spawned bots capable of charming"); return; } - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); int charmed_pet = 0; int summoned_pet = 0; @@ -73,7 +74,7 @@ void bot_command_pet_remove(Client *c, const Seperator *sep) if (bot_iter->IsBotCharmer()) { bot_iter->SetBotCharmer(false); if (sbl.size() == 1) - Bot::BotGroupSay(bot_iter, "Using a summoned pet"); + Bot::RaidGroupSay(bot_iter, "Using a summoned pet"); ++summoned_pet; continue; } @@ -85,7 +86,7 @@ void bot_command_pet_remove(Client *c, const Seperator *sep) } bot_iter->SetBotCharmer(true); if (sbl.size() == 1) - Bot::BotGroupSay(bot_iter, "Available for Charming"); + Bot::RaidGroupSay(bot_iter, "Available for Charming"); ++charmed_pet; } @@ -95,74 +96,324 @@ void bot_command_pet_remove(Client *c, const Seperator *sep) void bot_command_pet_set_type(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_pet_set_type", sep->arg[0], "petsettype")) + if (helper_command_alias_fail(c, "bot_command_pet_set_type", sep->arg[0], "petsettype")) { + c->Message(Chat::White, "note: Allows you to change the type of pet Magician bots will cast."); + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [type: water | fire | air | earth | monster] ([actionable: target | byname] ([actionable_name]))", sep->arg[0]); - c->Message(Chat::White, "requires one of the following bot classes:"); - c->Message(Chat::White, "Magician(1)"); + BotCommandHelpParams p; + + p.description = { "Allows you to change the type of pet Magician bots will cast." }; + p.example_format = { fmt::format("{} [current | water | fire | air | earth | monster | epic] [actionable, default: target]", sep->arg[0]) }; + p.examples_one = + { + "To set all spawned bots to use Water pets:", + fmt::format( + "{} fire spawned", + sep->arg[0] + ) + }; + p.examples_two = + { + "To set Magelulz to use Fire pets:", + fmt::format( + "{} fire byname Magelulz", + sep->arg[0] + ) + }; + p.examples_three = + { + "To check the current pet type for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + return; } - int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_ByName); // this can be expanded without code modification - - std::string pet_arg = sep->arg[1]; + std::string arg1 = sep->arg[1]; + int ab_arg = 2; + bool current_check = false; uint8 pet_type = 255; uint8 level_req = 255; - if (!pet_arg.compare("water")) { + + if (!arg1.compare("auto")) { pet_type = 0; - level_req = 1; } - else if (!pet_arg.compare("fire")) { + else if (!arg1.compare("water")) { pet_type = 1; - level_req = 3; } - else if (!pet_arg.compare("air")) { + else if (!arg1.compare("fire")) { pet_type = 2; - level_req = 4; } - else if (!pet_arg.compare("earth")) { + else if (!arg1.compare("air")) { pet_type = 3; - level_req = 5; } - else if (!pet_arg.compare("monster")) { + else if (!arg1.compare("earth")) { pet_type = 4; - level_req = 30; } + else if (!arg1.compare("monster")) { + pet_type = 5; + } + else if (!arg1.compare("epic")) { + if (!RuleB(Bots, AllowMagicianEpicPet)) { + c->Message(Chat::Yellow, "Epic pets are currently disabled for bots."); + return; + } - if (pet_type == 255) { - c->Message(Chat::White, "You must specify a pet [type: water | fire | air | earth | monster]"); - return; + pet_type = 6; + } + else if (!arg1.compare("current")) { + current_check = true; } - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[2], sbl, ab_mask, sep->arg[3]) == ActionableBots::ABT_None) + if (!current_check && pet_type == 255) { + c->Message( + Chat::Yellow, + fmt::format( + "You must specify a pet [type: auto | water | fire | air | earth | monster | epic]. Use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); return; + } - uint16 class_mask = player_class_bitmasks[Class::Magician]; - ActionableBots::Filter_ByClasses(c, sbl, class_mask); - if (sbl.empty()) { - c->Message(Chat::White, "You have no spawned Magician bots"); - return; + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "target"; } - ActionableBots::Filter_ByMinLevel(c, sbl, level_req); - if (sbl.empty()) { - c->Message(Chat::White, "You have no spawned Magician bots capable of using this pet type: '%s'", pet_arg.c_str()); + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - uint16 reclaim_energy_id = 331; + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + std::string current_type; + uint16 reclaim_energy_id = RuleI(Bots, ReclaimEnergySpellID); + bool is_success = false; + uint16 success_count = 0; + Bot* first_found = nullptr; + for (auto bot_iter : sbl) { - if (!bot_iter) + if (bot_iter->GetClass() != Class::Magician) { + continue; + } + + if (!bot_iter->IsInGroupOrRaid(c)) { + continue; + } + + if (pet_type == 6 && RuleI(Bots, RequiredMagicianEpicPetItemID) > 0) { + bool has_item = bot_iter->HasBotItem(RuleI(Bots, RequiredMagicianEpicPetItemID)) != INVALID_INDEX; + + if (!has_item) { + c->Message( + Chat::Say, + fmt::format( + "{} says, 'I require {} to cast an epic pet which I do not currently possess.'", + bot_iter->GetCleanName(), + (database.GetItem(RuleI(Bots, RequiredMagicianEpicPetItemID)) ? database.CreateItemLink(RuleI(Bots, RequiredMagicianEpicPetItemID)) : "an item") + ).c_str() + ); + + continue; + } + } + + if (current_check) { + switch (bot_iter->GetPetChooserID()) { + case 0: + current_type = "auto"; + break; + case SumWater: + current_type = "water"; + break; + case SumFire: + current_type = "fire"; + break; + case SumAir: + current_type = "air"; + break; + case SumEarth: + current_type = "earth"; + break; + case MonsterSum: + current_type = "monster"; + break; + case SumMageMultiElement: + current_type = "epic"; + break; + default: + current_type = "null"; + break; + } + + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I'm currently summoning {} pets.'", + bot_iter->GetCleanName(), + current_type + ).c_str() + ); + continue; + } + + uint8 air_min_level = 255; + uint8 fire_min_level = 255; + uint8 water_min_level = 255; + uint8 earth_min_level = 255; + uint8 monster_min_level = 255; + uint8 epic_min_level = 255; + std::list bot_spell_list = bot_iter->GetBotSpellsBySpellType(bot_iter, BotSpellTypes::Pet); + + for (const auto& s : bot_spell_list) { + if (!IsValidSpell(s.SpellId)) { + continue; + } + + if (!IsEffectInSpell(s.SpellId, SE_SummonPet)) { + continue; + } + + auto spell = spells[s.SpellId]; + + if (!strncmp(spell.teleport_zone, "SumWater", 8) && spell.classes[Class::Magician - 1] < water_min_level) { + water_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, "SumFire", 7) && spell.classes[Class::Magician - 1] < fire_min_level) { + fire_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, "SumAir", 6) && spell.classes[Class::Magician - 1] < air_min_level) { + air_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, "SumEarth", 8) && spell.classes[Class::Magician - 1] < earth_min_level) { + earth_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, "MonsterSum", 10) && spell.classes[Class::Magician - 1] < monster_min_level) { + monster_min_level = spell.classes[Class::Magician - 1]; + } + } + + uint8 min_level = std::min({ + water_min_level, + fire_min_level, + air_min_level, + earth_min_level, + monster_min_level + }); + + epic_min_level = RuleI(Bots, AllowMagicianEpicPetLevel); + + switch (pet_type) { + case 0: + level_req = min_level; + break; + case SumWater: + level_req = water_min_level; + break; + case SumFire: + level_req = fire_min_level; + break; + case SumAir: + level_req = air_min_level; + break; + case SumEarth: + level_req = earth_min_level; + break; + case MonsterSum: + level_req = monster_min_level; + break; + case SumMageMultiElement: + level_req = epic_min_level; + break; + default: + break; + } + + if (bot_iter->GetLevel() < level_req) { + continue; + } - bot_iter->SetPetChooser(true); bot_iter->SetPetChooserID(pet_type); + if (bot_iter->GetPet()) { auto pet_id = bot_iter->GetPetID(); bot_iter->SetPetID(0); bot_iter->CastSpell(reclaim_energy_id, pet_id); } + + if (!first_found) { + first_found = bot_iter; + } + + is_success = true; + ++success_count; + } + + if (current_check) { + return; + } + + if (!is_success) { + c->Message(Chat::Yellow, "No bots were selected."); + + return; + } + + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I will now summon {} pets.'", + first_found->GetCleanName(), + current_type + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots will now summon {} pets.", + success_count, + arg1 + ).c_str() + ); } } diff --git a/zone/bot_commands/pick_lock.cpp b/zone/bot_commands/pick_lock.cpp index 7c4379458e..f368087e2f 100644 --- a/zone/bot_commands/pick_lock.cpp +++ b/zone/bot_commands/pick_lock.cpp @@ -11,7 +11,7 @@ void bot_command_pick_lock(Client *c, const Seperator *sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_BySpawnedBots(c, sbl); float pick_lock_value = 0.0f; @@ -24,7 +24,7 @@ void bot_command_pick_lock(Client *c, const Seperator *sep) Bot* my_bot = sbl.front(); my_bot->InterruptSpell(); - Bot::BotGroupSay(my_bot, "Attempting to pick the lock."); + Bot::RaidGroupSay(my_bot, "Attempting to pick the lock."); std::list door_list; entity_list.GetDoorsList(door_list); @@ -51,7 +51,7 @@ void bot_command_pick_lock(Client *c, const Seperator *sep) ++open_count; } else { - Bot::BotGroupSay(my_bot, "I am not skilled enough for this lock."); + Bot::RaidGroupSay(my_bot, "I am not skilled enough for this lock."); } } c->Message(Chat::White, "%i door%s attempted - %i door%s successful", door_count, ((door_count != 1) ? ("s") : ("")), open_count, ((open_count != 1) ? ("s") : (""))); diff --git a/zone/bot_commands/pickpocket.cpp b/zone/bot_commands/pickpocket.cpp index 5aec323832..8b5339cca1 100644 --- a/zone/bot_commands/pickpocket.cpp +++ b/zone/bot_commands/pickpocket.cpp @@ -15,7 +15,7 @@ void bot_command_pickpocket(Client *c, const Seperator *sep) return; } - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_BySpawnedBots(c, sbl); // Check for capable rogue diff --git a/zone/bot_commands/precombat.cpp b/zone/bot_commands/precombat.cpp index 86ac7569a4..073dccf722 100644 --- a/zone/bot_commands/precombat.cpp +++ b/zone/bot_commands/precombat.cpp @@ -5,15 +5,22 @@ void bot_command_precombat(Client* c, const Seperator* sep) if (helper_command_alias_fail(c, "bot_command_precombat", sep->arg[0], "precombat")) { return; } - if (helper_is_help_or_usage(sep->arg[1])) { + if (helper_is_help_or_usage(sep->arg[1])) { c->Message(Chat::White, "usage: %s ([set | clear])", sep->arg[0]); + return; } if (!c->GetTarget() || !c->IsAttackAllowed(c->GetTarget())) { - c->Message(Chat::White, "This command requires an attackable target."); + + return; + } + + if (!c->DoLosChecks(c->GetTarget())) { + c->Message(Chat::Red, "You must have Line of Sight to use this command."); + return; } diff --git a/zone/bot_commands/pull.cpp b/zone/bot_commands/pull.cpp index 6265d141e7..22c27ee251 100644 --- a/zone/bot_commands/pull.cpp +++ b/zone/bot_commands/pull.cpp @@ -7,21 +7,50 @@ void bot_command_pull(Client *c, const Seperator *sep) } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); return; } - int ab_mask = ActionableBots::ABM_OwnerGroup; // existing behavior - need to add c->IsGrouped() check and modify code if different behavior is desired - std::list sbl; - if (ActionableBots::PopulateSBL(c, "ownergroup", sbl, ab_mask) == ActionableBots::ABT_None) { + const int ab_mask = ActionableBots::ABM_Type1; + + std::string arg1 = sep->arg[1]; + int ab_arg = 1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "spawned"; + } + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); - auto target_mob = ActionableTarget::VerifyEnemy(c, BCEnum::TT_Single); - if (!target_mob) { + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + auto target_mob = c->GetTarget(); + + if ( + !target_mob || + target_mob == c || + !c->IsAttackAllowed(target_mob) + ) { c->Message(Chat::White, "Your current target is not attackable!"); + + return; + } + + if (!c->DoLosChecks(target_mob)) { + c->Message(Chat::Red, "You must have Line of Sight to use this command."); + return; } @@ -32,8 +61,8 @@ void bot_command_pull(Client *c, const Seperator *sep) } Bot* bot_puller = nullptr; - for (auto bot_iter : sbl) { + for (auto bot_iter : sbl) { if (bot_iter->GetAppearance() == eaDead || bot_iter->GetBotStance() == Stance::Passive) { continue; } diff --git a/zone/bot_commands/release.cpp b/zone/bot_commands/release.cpp index 1872d43d61..9bd9d979c2 100644 --- a/zone/bot_commands/release.cpp +++ b/zone/bot_commands/release.cpp @@ -5,16 +5,16 @@ void bot_command_release(Client *c, const Seperator *sep) if (helper_command_alias_fail(c, "bot_command_release", sep->arg[0], "release")) return; if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([actionable: ] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationmembers | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); return; } const int ab_mask = ActionableBots::ABM_NoFilter; - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) return; - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); for (auto bot_iter : sbl) { bot_iter->WipeHateList(); bot_iter->SetPauseAI(false); diff --git a/zone/bot_commands/resistance.cpp b/zone/bot_commands/resistance.cpp deleted file mode 100644 index 0b84b4b88a..0000000000 --- a/zone/bot_commands/resistance.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "../bot_command.h" - -void bot_command_resistance(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Resistance]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Resistance) || helper_command_alias_fail(c, "bot_command_resistance", sep->arg[0], "resistance")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s [resistance: fire | cold | poison | disease | magic | corruption]", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Resistance); - return; - } - - std::string resistance_arg = sep->arg[1]; - - auto resistance_type = BCEnum::RT_None; - if (!resistance_arg.compare("fire")) - resistance_type = BCEnum::RT_Fire; - else if (!resistance_arg.compare("cold")) - resistance_type = BCEnum::RT_Cold; - else if (!resistance_arg.compare("poison")) - resistance_type = BCEnum::RT_Poison; - else if (!resistance_arg.compare("disease")) - resistance_type = BCEnum::RT_Disease; - else if (!resistance_arg.compare("magic")) - resistance_type = BCEnum::RT_Magic; - else if (!resistance_arg.compare("corruption")) - resistance_type = BCEnum::RT_Corruption; - - if (resistance_type == BCEnum::RT_None) { - c->Message(Chat::White, "You must specify a [resistance]"); - return; - } - - local_list->sort([resistance_type](STBaseEntry* l, STBaseEntry* r) { - auto _l = l->SafeCastToResistance(), _r = r->SafeCastToResistance(); - if (_l->resist_value[RESISTANCEIDTOINDEX(resistance_type)] > _r->resist_value[RESISTANCEIDTOINDEX(resistance_type)]) - return true; - if (_l->resist_value[RESISTANCEIDTOINDEX(resistance_type)] == _r->resist_value[RESISTANCEIDTOINDEX(resistance_type)] && spells[_l->spell_id].mana < spells[_r->spell_id].mana) - return true; - if (_l->resist_value[RESISTANCEIDTOINDEX(resistance_type)] == _r->resist_value[RESISTANCEIDTOINDEX(resistance_type)] && spells[_l->spell_id].mana == spells[_r->spell_id].mana && _l->resist_total > _r->resist_total) - return true; - - return false; - }); - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToResistance(); - if (helper_spell_check_fail(local_entry)) - continue; - if (!local_entry->resist_value[RESISTANCEIDTOINDEX(resistance_type)]) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/resurrect.cpp b/zone/bot_commands/resurrect.cpp deleted file mode 100644 index 0b15ef3e11..0000000000 --- a/zone/bot_commands/resurrect.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "../bot_command.h" - -void bot_command_resurrect(Client *c, const Seperator *sep) -{ - // Obscure bot spell code prohibits the aoe portion from working correctly... - - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Resurrect]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Resurrect) || helper_command_alias_fail(c, "bot_command_resurrect", sep->arg[0], "resurrect")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - //c->Message(Chat::White, "usage: %s ([option: aoe])", sep->arg[0]); - c->Message(Chat::White, "usage: %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Resurrect); - return; - } - - bool aoe = false; - //std::string aoe_arg = sep->arg[1]; - //if (!aoe_arg.compare("aoe")) - // aoe = true; - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToResurrect(); - if (helper_spell_check_fail(local_entry)) - continue; - //if (local_entry->aoe != aoe) - // continue; - if (local_entry->aoe) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - //if (!target_mob && !local_entry->aoe) - // continue; - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - //if (local_entry->aoe) - // target_mob = my_bot; - - uint32 dont_root_before = 0; - if (helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id, true, &dont_root_before)) - target_mob->SetDontRootMeBefore(dont_root_before); - - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/rune.cpp b/zone/bot_commands/rune.cpp deleted file mode 100644 index 71b1cf572a..0000000000 --- a/zone/bot_commands/rune.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "../bot_command.h" - -void bot_command_rune(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Rune]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Rune) || helper_command_alias_fail(c, "bot_command_rune", sep->arg[0], "rune")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Rune); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter; - if (helper_spell_check_fail(local_entry)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/send_home.cpp b/zone/bot_commands/send_home.cpp deleted file mode 100644 index 6950e2bee9..0000000000 --- a/zone/bot_commands/send_home.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "../bot_command.h" - -void bot_command_send_home(Client *c, const Seperator *sep) -{ - // Obscure bot spell code prohibits the aoe portion from working correctly... - - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_SendHome]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_SendHome) || helper_command_alias_fail(c, "bot_command_send_home", sep->arg[0], "sendhome")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s ([option: group])", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_SendHome); - return; - } - - bool group = false; - std::string group_arg = sep->arg[1]; - if (!group_arg.compare("group")) - group = true; - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToSendHome(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->group != group) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/set_assistee.cpp b/zone/bot_commands/set_assistee.cpp new file mode 100644 index 0000000000..f5a1845fea --- /dev/null +++ b/zone/bot_commands/set_assistee.cpp @@ -0,0 +1,42 @@ +#include "../bot_command.h" + +void bot_command_set_assistee(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_set_assistee", sep->arg[0], "setassistee")) { + c->Message(Chat::White, "note: Sets your bots to assist your target in addition to yourself."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Sets your bots to assist your target in addition to yourself." }; + p.notes = + { + "- Your target must be another player in your group or raid.", + "- This needs to be set on every zone/camp you do.", + "- If a Raid or Group assist is set and you do not want your bots to auto assist that person, set yourself as the assistee." + }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + return; + } + + Mob* assistee = c->GetTarget(); + + if (assistee && assistee->IsClient() && c->IsInGroupOrRaid(assistee)) { + c->SetAssistee(assistee->CastToClient()->CharacterID()); + c->Message(Chat::Green, "Your bots will now assist %s.", assistee->GetCleanName()); + + return; + } + + c->Message(Chat::Yellow, "You can only set your bots to assist clients that are in your group or raid."); + + return; +} diff --git a/zone/bot_commands/sit_hp_percent.cpp b/zone/bot_commands/sit_hp_percent.cpp new file mode 100644 index 0000000000..0843aa5b92 --- /dev/null +++ b/zone/bot_commands/sit_hp_percent.cpp @@ -0,0 +1,156 @@ +#include "../bot_command.h" + +void bot_command_sit_hp_percent(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_sit_hp_percent", sep->arg[0], "sithppercent")) { + c->Message(Chat::White, "note: HP % threshold when bots will sit in combat if allowed."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "HP % threshold when bots will sit in combat if allowed." }; + p.example_format = { fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set Clerics to sit at 45% HP:", + fmt::format( + "{} 45 byclass {}", + sep->arg[0], + Class::Cleric + ) + }; + p.examples_two = + { + "To set all bots to sit at 50% HP:", + fmt::format( + "{} 50 spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To check the HP threshold for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + + int ab_arg = 1; + bool current_check = false; + uint32 type_value = 0; + + if (sep->IsNumber(1)) { + type_value = atoi(sep->arg[1]); + ++ab_arg; + if (type_value < 0 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-100 (0%% to 100%% of health)."); + + return; + } + } + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I sit in combat whem at or below [{}%%] HP.'", + my_bot->GetCleanName(), + my_bot->GetSitHPPct() + ).c_str() + ); + } + else { + my_bot->SetSitHPPct(type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I will now sit in combat whem at or below [{}%%] HP.'", + first_found->GetCleanName(), + first_found->GetSitHPPct() + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots will now sit in combat whem at or below [{}%%] HP.'", + success_count, + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/sit_in_combat.cpp b/zone/bot_commands/sit_in_combat.cpp new file mode 100644 index 0000000000..07a2f917e4 --- /dev/null +++ b/zone/bot_commands/sit_in_combat.cpp @@ -0,0 +1,156 @@ +#include "../bot_command.h" + +void bot_command_sit_in_combat(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_sit_in_combat", sep->arg[0], "sitincombat")) { + c->Message(Chat::White, "note: Toggles whether or not bots will sit in combat to heal or med."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Toggles whether or not bots will sit in combat to heal or med." }; + p.example_format = { fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set Clerics to sit in combat:", + fmt::format( + "{} 1 byclass {}", + sep->arg[0], + Class::Cleric + ) + }; + p.examples_two = + { + "To set all bots to sit/med in combat:", + fmt::format( + "{} 1 spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To check the sit in combat state for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + + int ab_arg = 1; + bool current_check = false; + uint32 type_value = 0; + + if (sep->IsNumber(1)) { + type_value = atoi(sep->arg[1]); + ++ab_arg; + if (type_value < 0 || type_value > 1) { + c->Message(Chat::Yellow, "You must enter either 0 for disabled or 1 for enabled."); + + return; + } + } + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} sit in combat.'", + my_bot->GetCleanName(), + my_bot->GetMedInCombat() ? "will" : "will not" + ).c_str() + ); + } + else { + my_bot->SetMedInCombat(type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I {} sit in combat.'", + first_found->GetCleanName(), + first_found->GetMedInCombat() ? "will now" : "will no longer" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots {} sit in combat.", + success_count, + type_value ? "will now" : "will no longer" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/sit_mana_percent.cpp b/zone/bot_commands/sit_mana_percent.cpp new file mode 100644 index 0000000000..e85d976c89 --- /dev/null +++ b/zone/bot_commands/sit_mana_percent.cpp @@ -0,0 +1,156 @@ +#include "../bot_command.h" + +void bot_command_sit_mana_percent(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_sit_mana_percent", sep->arg[0], "sitmanapercent")) { + c->Message(Chat::White, "note: Mana % threshold when bots will sit in combat if allowed."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Mana % threshold when bots will sit in combat if allowed." }; + p.example_format = { fmt::format("{} [value] [actionable]", sep->arg[0]) }; + p.examples_one = + { + "To set Clerics to sit at 45% Mana:", + fmt::format( + "{} 45 byclass {}", + sep->arg[0], + Class::Cleric + ) + }; + p.examples_two = + { + "To set all bots to sit at 50% Mana:", + fmt::format( + "{} 50 spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To check the Mana threshold for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + + int ab_arg = 1; + bool current_check = false; + uint32 type_value = 0; + + if (sep->IsNumber(1)) { + type_value = atoi(sep->arg[1]); + ++ab_arg; + if (type_value < 0 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-100 (0%% to 100%% of health)."); + + return; + } + } + else if (!arg1.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I sit in combat whem at or below [{}%%] mana.'", + my_bot->GetCleanName(), + my_bot->GetSitManaPct() + ).c_str() + ); + } + else { + my_bot->SetSitManaPct(type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I will now sit in combat whem at or below [{}%%] mana.'", + first_found->GetCleanName(), + first_found->GetSitManaPct() + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots will now sit in combat whem at or below [{}%%] mana.'", + success_count, + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/size.cpp b/zone/bot_commands/size.cpp deleted file mode 100644 index 69e2fd1a2f..0000000000 --- a/zone/bot_commands/size.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "../bot_command.h" - -void bot_command_size(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Size]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Size) || helper_command_alias_fail(c, "bot_command_size", sep->arg[0], "size")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s [grow | shrink]", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Size); - return; - } - - std::string size_arg = sep->arg[1]; - auto size_type = BCEnum::SzT_Reduce; - if (!size_arg.compare("grow")) { - size_type = BCEnum::SzT_Enlarge; - } - else if (size_arg.compare("shrink")) { - c->Message(Chat::White, "This command requires a [grow | shrink] argument"); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToSize(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->size_type != size_type) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/spell.cpp b/zone/bot_commands/spell.cpp index 45d9ec6ebc..833c8ef7ab 100644 --- a/zone/bot_commands/spell.cpp +++ b/zone/bot_commands/spell.cpp @@ -503,7 +503,7 @@ void bot_spell_info_dialogue_window(Client* c, const Seperator *sep) auto results = database.QueryDatabase( fmt::format( "SELECT value FROM db_str WHERE id = {} and type = 6 LIMIT 1", - spells[spell_id].effect_description_id + spells[spell_id].description_id ) ); @@ -557,7 +557,7 @@ void bot_command_enforce_spell_list(Client* c, const Seperator *sep) } bool enforce_state = (sep->argnum > 0) ? Strings::ToBool(sep->arg[1]) : !my_bot->GetBotEnforceSpellSetting(); - my_bot->SetBotEnforceSpellSetting(enforce_state, true); + my_bot->SetBotEnforceSpellSetting(enforce_state); c->Message( Chat::White, diff --git a/zone/bot_commands/spell_aggro_checks.cpp b/zone/bot_commands/spell_aggro_checks.cpp new file mode 100644 index 0000000000..41ae404447 --- /dev/null +++ b/zone/bot_commands/spell_aggro_checks.cpp @@ -0,0 +1,222 @@ +#include "../bot_command.h" + +void bot_command_spell_aggro_checks(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_aggro_checks", sep->arg[0], "spellaggrochecks")) { + c->Message(Chat::White, "note: Toggles whether or not bots will cast a spell type if they think it will get them aggro."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Toggles whether or not bots will cast a spell type if they think it will get them aggro." }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to check aggro on nukes:", + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + BotSpellTypes::Nuke + ) + }; + p.examples_two = + { + "To set Shadowknights to ignore aggro checks on snares:", + fmt::format( + "{} {} 0 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Snare), + Class::ShadowKnight + ), + fmt::format( + "{} {} 0 byclass {}", + sep->arg[0], + BotSpellTypes::Snare, + Class::ShadowKnight + ) + }; + p.examples_three = + { + "To check the current DoT aggro check on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::DOT) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::DOT + ) + }; + + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message( + Chat::Yellow, + fmt::format( + "You must choose a valid spell type. Use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 0 || type_value > 1) { + c->Message(Chat::Yellow, "You must enter either 0 for disabled or 1 for enabled."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] aggro check is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeAggroCheck(spell_type) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + my_bot->SetSpellTypeAggroCheck(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] aggro check was [{}].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeAggroCheck(spell_type) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots [{}] their [{}] aggro check.", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value ? "enabled" : "disabled" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_announce_cast.cpp b/zone/bot_commands/spell_announce_cast.cpp new file mode 100644 index 0000000000..4825997249 --- /dev/null +++ b/zone/bot_commands/spell_announce_cast.cpp @@ -0,0 +1,214 @@ +#include "../bot_command.h" + +void bot_command_spell_announce_cast(Client* c, const Seperator* sep) { + if (helper_command_alias_fail(c, "bot_command_spell_announce_cast", sep->arg[0], "spellannouncecasts")) { + c->Message(Chat::White, "note: Allows you to enable or disable cast announcements for bots by spell type."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Allows you to enable or disable cast announcements for bots by spell type." }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to stop announcing dispels:", + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Dispel) + ), + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + BotSpellTypes::Dispel + ) + }; + p.examples_two = + { + "To set Wizards to not announce nukes:", + fmt::format( + "{} {} 0 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke), + Class::Wizard + ), + fmt::format( + "{} {} 0 byclass {}", + sep->arg[0], + BotSpellTypes::Nuke, + Class::Wizard + + ) + }; + p.examples_three = + { + "To check the current announcement setting for debuffs:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Debuff) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Debuff + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + + if (type_value != 0 && type_value != 1) { + c->Message(Chat::Yellow, "You must enter either 0 for [Disabled] or 1 for [Enabled]."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I currently {} announce [{}] casts.'", + my_bot->GetCleanName(), + (my_bot->GetSpellTypeAnnounceCast(spell_type) ? "do" : "do not"), + Bot::GetSpellTypeNameByID(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeAnnounceCast(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'I will {} announce [{}] casts.'", + first_found->GetCleanName(), + (first_found->GetSpellTypeAnnounceCast(spell_type) ? "now" : "no longer"), + Bot::GetSpellTypeNameByID(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots will {} announce [{}] casts.", + success_count, + (type_value ? "now" : "no longer"), + Bot::GetSpellTypeNameByID(spell_type) + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_delays.cpp b/zone/bot_commands/spell_delays.cpp new file mode 100644 index 0000000000..98efb3f225 --- /dev/null +++ b/zone/bot_commands/spell_delays.cpp @@ -0,0 +1,282 @@ +#include "../bot_command.h" + +void bot_command_spell_delays(Client* c, const Seperator* sep) { + if (helper_command_alias_fail(c, "bot_command_spell_delays", sep->arg[0], "spelldelays")) { + c->Message(Chat::White, "note: Controls how long a bot will wait between casts of different spell types."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Controls how long a bot will wait between casts of different spell types." }; + p.notes = + { + "- Targeting yourself for this command will allow you to control your own settings for how bots cast on you", + "- All pet types are based off the pet's owner's setting", + "- Any remaining types use the owner's setting when a pet is the target", + "- All Heals, Cures, Buffs (DS and resists included) are based off the target's setting, not the caster", + "- e.g., BotA is healing BotB using BotB's settings" + }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable, default: target]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable, default: target]", sep->arg[0]) + }; + p.examples_one = + { + "To set all Necromancers to an 8s DoT delay:", + fmt::format( + "{} {} 8000 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::DOT), + Class::Necromancer + ), + fmt::format( + "{} {} 8000 byclass {}", + sep->arg[0], + BotSpellTypes::DOT, + Class::Necromancer + ) + }; + p.examples_two = + { + "To set all Warriors to receive Fast Heals every 2.5s:", + fmt::format( + "{} {} 2500 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::FastHeals), + Class::Warrior + ), + fmt::format( + "{} {} 2500 byclass {}", + sep->arg[0], + BotSpellTypes::FastHeals, + Class::Warrior + ) + }; + p.examples_three = + { + "To check the current Nuke delay on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Nuke + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + c->SendSpellTypePrompts(false, true); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + Mob* target = c->GetTarget(); + bool clientSetting = (target && target == c); + + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if ( + (clientSetting && !IsClientBotSpellType(spell_type)) || + (!clientSetting && !EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "{}. Use {} for information regarding this command.", + (!clientSetting ? "You must choose a valid spell type" : "You must choose a valid client spell type"), + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + if (clientSetting) { + c->SendSpellTypePrompts(false, true); + } + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + + if (clientSetting && !IsClientBotSpellType(spell_type)) { + c->Message(Chat::Yellow, "Invalid spell type for clients."); + c->SendSpellTypePrompts(false, true); + } + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 100 || type_value > 60000) { + c->Message(Chat::Yellow, "You must enter a value between 100-60000 (100ms to 60s)."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "target"; + } + + if (!clientSetting) { + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] spell delay is currently [{}] seconds.'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeDelay(spell_type) / 1000.00 + ).c_str() + ); + } + else { + my_bot->SetSpellTypeDelay(spell_type, type_value); + ++success_count; + } + } + + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] spell delay was set to [{}] seconds.'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeDelay(spell_type) / 1000.00 + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] spell delay to [{}] seconds.", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value / 1000.00 + ).c_str() + ); + } + } + } + else { + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "Your [{}] spell delay is currently [{}] seconds.", + Bot::GetSpellTypeNameByID(spell_type), + c->GetSpellTypeDelay(spell_type) / 1000.00 + ).c_str() + ); + } + else { + c->SetSpellTypeDelay(spell_type, type_value); + + c->Message( + Chat::Green, + fmt::format( + "Your [{}] spell delay was set to [{}] seconds.", + Bot::GetSpellTypeNameByID(spell_type), + c->GetSpellTypeDelay(spell_type) / 1000.00 + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_engaged_priority.cpp b/zone/bot_commands/spell_engaged_priority.cpp new file mode 100644 index 0000000000..2d6d0b17b0 --- /dev/null +++ b/zone/bot_commands/spell_engaged_priority.cpp @@ -0,0 +1,248 @@ +#include "../bot_command.h" + +void bot_command_spell_engaged_priority(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_engaged_priority", sep->arg[0], "spellengagedpriority")) { + c->Message(Chat::White, "note: Sets the order of spell casts when engaged in combat by spell type."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Sets the order of spell casts when engaged in combat by spell type." }; + p.notes = + { + "- Setting a spell type to 0 will prevent that type from being cast.", + "- If 2 or more are set to the same priority they will sort by spell type ID." + }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all Shaman to cast slows first:", + fmt::format( + "{} {} 1 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Slow), + Class::Shaman + ), + fmt::format( + "{} {} 1 byclass {}", + sep->arg[0], + BotSpellTypes::Slow, + Class::Shaman + ) + }; + p.examples_two = + { + "To set all bots to not cast snares:", + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + p.examples_three = + { + "To check the current engaged priority of dispels on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Dispel) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Dispel + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + bool list_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else if (!arg1.compare("list")) { + ++ab_arg; + list_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (EQ::ValueWithin(type_value, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must enter a value between {} and {}.", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else if (!list_check) { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] engaged cast priority is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypePriority(spell_type, BotPriorityCategories::Engaged) + ).c_str() + ); + } + else if (list_check) { + auto cast_order = my_bot->GetSpellTypesPrioritized(BotPriorityCategories::Engaged); + + for (auto& current_cast : cast_order) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] engaged cast priority for is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(current_cast.spellType), + (current_cast.priority == 0 ? "disabled (0)" : std::to_string(current_cast.priority)) + ).c_str() + ); + } + + c->Message( + Chat::Green, + fmt::format( + "{} says, 'Anything not listed is currently disabled (0).'", + my_bot->GetCleanName() + ).c_str() + ); + + return; + } + else { + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Engaged, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] engaged cast priority was set to [{}].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypePriority(spell_type, BotPriorityCategories::Engaged) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] engaged cast priority to [{}].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_holds.cpp b/zone/bot_commands/spell_holds.cpp new file mode 100644 index 0000000000..26d42ad823 --- /dev/null +++ b/zone/bot_commands/spell_holds.cpp @@ -0,0 +1,193 @@ +#include "../bot_command.h" + +void bot_command_spell_holds(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_holds", sep->arg[0], "spellholds")) { + c->Message(Chat::White, "note: Toggles whether or not bots can cast certain spell types."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Toggles whether or not bots can cast certain spell types." }; + p.notes = { "- All pet types are based off the pet owner's setting when a pet is the target" }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to hold DoTs:", + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::DOT) + ), + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + BotSpellTypes::DOT + ) + }; + p.examples_two = + { + "To check the current DoT settings on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::DOT) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::DOT + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 0 || type_value > 1) { + c->Message(Chat::Yellow, "You must enter either 0 for disabled or 1 for enabled."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] spell hold is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeHold(spell_type) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + my_bot->SetSpellTypeHold(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] spell hold was [{}].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeHold(spell_type) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots [{}] their [{}] spell hold.", + success_count, + type_value ? "enabled" : "disabled", + Bot::GetSpellTypeNameByID(spell_type) + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_idle_priority.cpp b/zone/bot_commands/spell_idle_priority.cpp new file mode 100644 index 0000000000..a37e035df7 --- /dev/null +++ b/zone/bot_commands/spell_idle_priority.cpp @@ -0,0 +1,248 @@ +#include "../bot_command.h" + +void bot_command_spell_idle_priority(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_idle_priority", sep->arg[0], "spellidlepriority")) { + c->Message(Chat::White, "note: Sets the order of spell casts when not in combat by spell type."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Sets the order of spell casts when not in combat by spell type." }; + p.notes = + { + "- Setting a spell type to 0 will prevent that type from being cast.", + "- If 2 or more are set to the same priority they will sort by spell type ID." + }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all Clerics to cast fast heals third:", + fmt::format( + "{} {} 3 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::FastHeals), + Class::Cleric + ), + fmt::format( + "{} {} 3 byclass {}", + sep->arg[0], + BotSpellTypes::FastHeals, + Class::Cleric + ) + }; + p.examples_two = + { + "To set all bots to not cast cures:", + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Cure) + ), + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + BotSpellTypes::Cure + ) + }; + p.examples_three = + { + "To check the current idle priority of buffs on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Buff) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Buff + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + bool list_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else if (!arg1.compare("list")) { + ++ab_arg; + list_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (EQ::ValueWithin(type_value, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must enter a value between {} and {}.", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else if (!list_check) { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] idle cast priority is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypePriority(spell_type, BotPriorityCategories::Idle) + ).c_str() + ); + } + else if (list_check) { + auto cast_order = my_bot->GetSpellTypesPrioritized(BotPriorityCategories::Idle); + + for (auto& current_cast : cast_order) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] idle cast priority for is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(current_cast.spellType), + (current_cast.priority == 0 ? "disabled (0)" : std::to_string(current_cast.priority)) + ).c_str() + ); + } + + c->Message( + Chat::Green, + fmt::format( + "{} says, 'Anything not listed is currently disabled (0).'", + my_bot->GetCleanName() + ).c_str() + ); + + return; + } + else { + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Idle, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] idle cast priority was set to [{}].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypePriority(spell_type, BotPriorityCategories::Idle) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] idle cast priority to [{}].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_max_hp_pct.cpp b/zone/bot_commands/spell_max_hp_pct.cpp new file mode 100644 index 0000000000..249e9179d8 --- /dev/null +++ b/zone/bot_commands/spell_max_hp_pct.cpp @@ -0,0 +1,211 @@ +#include "../bot_command.h" + +void bot_command_spell_max_hp_pct(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_max_hp_pct", sep->arg[0], "spellmaxhppct")) { + c->Message(Chat::White, "note: Controls at what health percentage a bot will start casting different spell types."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Controls at what health percentage a bot will start casting different spell types." }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to allow snaring when their health is at or below 100% HP:", + fmt::format( + "{} {} 100 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + p.examples_two = + { + "To set BotA to allow casting of fast heals at 30% HP:", + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + p.examples_three = + { + "To check the current Stun max HP percent on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Stun) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Stun + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 0 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-100 (0%% to 100%% of health)."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum HP is currently [{}%%].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeMaxHPLimit(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMaxHPLimit(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum HP was set to [{}%%].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeMaxHPLimit(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] maximum HP to [{}%%].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_max_mana_pct.cpp b/zone/bot_commands/spell_max_mana_pct.cpp new file mode 100644 index 0000000000..ab42ce8a5e --- /dev/null +++ b/zone/bot_commands/spell_max_mana_pct.cpp @@ -0,0 +1,211 @@ +#include "../bot_command.h" + +void bot_command_spell_max_mana_pct(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_max_mana_pct", sep->arg[0], "spellmaxmanapct")) { + c->Message(Chat::White, "note: Controls at what mana percentage a bot will stop casting different spell types."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Controls at what mana percentage a bot will stop casting different spell types." }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to allow snaring when their mana is at or below 100% HP:", + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + p.examples_two = + { + "To set BotA to allow casting of fast heals at 90% mana:", + fmt::format( + "{} {} 90 byname BotA", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 90 byname BotA", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + p.examples_three = + { + "To check the current Stun max mana percent on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Stun) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Stun + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 0 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-100 (0%% to 100%% of mana)."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum mana is currently [{}%%].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeMaxManaLimit(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMaxManaLimit(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum mana was set to [{}%%].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeMaxManaLimit(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] maximum mana to [{}%%].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_max_thresholds.cpp b/zone/bot_commands/spell_max_thresholds.cpp new file mode 100644 index 0000000000..ca7f78aa32 --- /dev/null +++ b/zone/bot_commands/spell_max_thresholds.cpp @@ -0,0 +1,278 @@ +#include "../bot_command.h" + +void bot_command_spell_max_thresholds(Client* c, const Seperator* sep) { + if (helper_command_alias_fail(c, "bot_command_spell_max_thresholds", sep->arg[0], "spellmaxthresholds")) { + c->Message(Chat::White, "note: Controls at what target HP % the bot will start casting different spell types."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Controls at what target HP % the bot will start casting different spell types." }; + p.notes = + { + "- Targeting yourself for this command will allow you to control your own settings for how bots cast on you", + "- All pet types are based off the pet's owner's setting", + "- Any remaining types use the owner's setting when a pet is the target", + "- All Heals, Cures, Buffs (DS and resists included) are based off the target's setting, not the caster", + "- e.g., BotA is healing BotB using BotB's settings", + }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable, default: target]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable, default: target]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to start snaring at 99%:", + fmt::format( + "{} {} 99 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 99 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + p.examples_two = + { + "To set bot Enchbot to start casting Debuffs at 99%:", + fmt::format( + "{} {} 99 byname Enchbot", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Debuff) + ), + fmt::format( + "{} {} 99 byname Enchbot", + sep->arg[0], + BotSpellTypes::Debuff + ) + }; + p.examples_three = + { + "To check the current Nuke max threshold on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Nuke + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + c->SendSpellTypePrompts(false, true); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + Mob* target = c->GetTarget(); + bool clientSetting = (target && target == c); + + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if ( + (clientSetting && !IsClientBotSpellType(spell_type)) || + (!clientSetting && !EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "{}. Use {} for information regarding this command.", + (!clientSetting ? "You must choose a valid spell type" : "You must choose a valid client spell type"), + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + if (clientSetting) { + c->SendSpellTypePrompts(false, true); + } + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + + if (clientSetting && !IsClientBotSpellType(spell_type)) { + c->Message(Chat::Yellow, "Invalid spell type for clients."); + c->SendSpellTypePrompts(false, true); + } + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 0 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-100 (0%% to 100%% of health)."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "target"; + } + + if (!clientSetting) { + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum threshold is currently [{}%%].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeMaxThreshold(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMaxThreshold(spell_type, type_value); + ++success_count; + } + } + + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum threshold was set to [{}%%].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeMaxThreshold(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] maximum threshold to [{}%%].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } + } + else { + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "Your [{}] maximum threshold is currently [{}%%].", + Bot::GetSpellTypeNameByID(spell_type), + c->GetSpellTypeMaxThreshold(spell_type) + ).c_str() + ); + } + else { + c->SetSpellTypeMaxThreshold(spell_type, type_value); + + c->Message( + Chat::Green, + fmt::format( + "Your [{}] maximum threshold was set to [{}%%].", + Bot::GetSpellTypeNameByID(spell_type), + c->GetSpellTypeMaxThreshold(spell_type) + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_min_hp_pct.cpp b/zone/bot_commands/spell_min_hp_pct.cpp new file mode 100644 index 0000000000..61d6313e37 --- /dev/null +++ b/zone/bot_commands/spell_min_hp_pct.cpp @@ -0,0 +1,211 @@ +#include "../bot_command.h" + +void bot_command_spell_min_hp_pct(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_min_hp_pct", sep->arg[0], "spellminhppct")) { + c->Message(Chat::White, "note: Controls at what health percentage a bot will stop casting different spell types."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Controls at what health percentage a bot will stop casting different spell types." }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to stop snaring when their health is below 10% HP:", + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + p.examples_two = + { + "To set BotA to stop casting fast heals at 30% HP:", + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + p.examples_three = + { + "To check the current Stun min HP percent on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Stun) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Stun + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 0 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-100 (0%% to 100%% of mana)."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum HP is currently [{}%%].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeMinHPLimit(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMinHPLimit(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum HP was set to [{}%%].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeMinHPLimit(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] minimum HP to [{}%%].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_min_mana_pct.cpp b/zone/bot_commands/spell_min_mana_pct.cpp new file mode 100644 index 0000000000..71672cee9e --- /dev/null +++ b/zone/bot_commands/spell_min_mana_pct.cpp @@ -0,0 +1,211 @@ +#include "../bot_command.h" + +void bot_command_spell_min_mana_pct(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_min_mana_pct", sep->arg[0], "spellminmanapct")) { + c->Message(Chat::White, "note: Controls at what mana percentage a bot will stop casting different spell types."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Controls at what mana percentage a bot will stop casting different spell types." }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to stop snaring when their mana is below 10% HP:", + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + p.examples_two = + { + "To set BotA to stop casting fast heals at 30% mana:", + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + p.examples_three = + { + "To check the current Stun min mana percent on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Stun) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Stun + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 0 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-100 (0%% to 100%% of mana)."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum mana is currently [{}%%].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeMinManaLimit(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMinManaLimit(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum mana was set to [{}%%].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeMinManaLimit(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] minimum mana to [{}%%].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_min_thresholds.cpp b/zone/bot_commands/spell_min_thresholds.cpp new file mode 100644 index 0000000000..ba614530c1 --- /dev/null +++ b/zone/bot_commands/spell_min_thresholds.cpp @@ -0,0 +1,280 @@ +#include "../bot_command.h" + +void bot_command_spell_min_thresholds(Client* c, const Seperator* sep) { + if (helper_command_alias_fail(c, "bot_command_spell_min_thresholds", sep->arg[0], "spellminthresholds")) { + c->Message(Chat::White, "note: Controls at what target HP % the bot will stop casting different spell types."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Controls at what target HP % the bot will stop casting different spell types." }; + p.notes = + { + "- Targeting yourself for this command will allow you to control your own settings for how bots cast on you", + "- All pet types are based off the pet's owner's setting", + "- Any remaining types use the owner's setting when a pet is the target", + "- All Heals, Cures, Buffs (DS and resists included) are based off the target's setting, not the caster", + "- e.g., BotA is healing BotB using BotB's settings", + }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable, default: target]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable, default: target]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to stop debuffing at 10%:", + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Debuff) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Debuff + ) + }; + p.examples_two = + { + "To set all Druids to stop casting DoTs at 15%:", + fmt::format( + "{} {} 15 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::DOT), + Class::Druid + ), + fmt::format( + "{} {} 15 byclass {}", + sep->arg[0], + BotSpellTypes::DOT, + Class::Druid + ) + }; + p.examples_three = + { + "To check the current Fast Heal min threshold on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + c->SendSpellTypePrompts(false, true); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + Mob* target = c->GetTarget(); + bool clientSetting = (target && target == c); + + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if ( + (clientSetting && !IsClientBotSpellType(spell_type)) || + (!clientSetting && !EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "{}. Use {} for information regarding this command.", + (!clientSetting ? "You must choose a valid spell type" : "You must choose a valid client spell type"), + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + if (clientSetting) { + c->SendSpellTypePrompts(false, true); + } + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + + if (clientSetting && !IsClientBotSpellType(spell_type)) { + c->Message(Chat::Yellow, "Invalid spell type for clients."); + c->SendSpellTypePrompts(false, true); + } + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (type_value < 0 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-100 (0%% to 100%% of health)."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "target"; + } + + if (!clientSetting) { + + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum threshold is currently [{}%%].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeMinThreshold(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMinThreshold(spell_type, type_value); + ++success_count; + } + } + + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum threshold was set to [{}%%].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeMinThreshold(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] minimum threshold to [{}%%].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } + } + else { + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "Your [{}] minimum threshold is currently [{}%%].", + Bot::GetSpellTypeNameByID(spell_type), + c->GetSpellTypeMinThreshold(spell_type) + ).c_str() + ); + } + else { + c->SetSpellTypeMinThreshold(spell_type, type_value); + + c->Message( + Chat::Green, + fmt::format( + "Your [{}] minimum threshold was set to [{}%%].", + Bot::GetSpellTypeNameByID(spell_type), + c->GetSpellTypeMinThreshold(spell_type) + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_pursue_priority.cpp b/zone/bot_commands/spell_pursue_priority.cpp new file mode 100644 index 0000000000..5267012141 --- /dev/null +++ b/zone/bot_commands/spell_pursue_priority.cpp @@ -0,0 +1,248 @@ +#include "../bot_command.h" + +void bot_command_spell_pursue_priority(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_pursue_priority", sep->arg[0], "spellpursuepriority")) { + c->Message(Chat::White, "note: Sets the order of spell casts when the mob is fleeing in combat by spell type."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Sets the order of spell casts when the mob is fleeing in combat by spell type." }; + p.notes = + { + "- Setting a spell type to 0 will prevent that type from being cast.", + "- If 2 or more are set to the same priority they will sort by spell type ID." + }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to cast nukes first:", + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + p.examples_two = + { + "To set all Shaman to not cast cures:", + fmt::format( + "{} {} 0 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Cure), + Class::Shaman + ), + fmt::format( + "{} {} 0 byclass {}", + sep->arg[0], + BotSpellTypes::Cure, + Class::Shaman + ) + }; + p.examples_three = + { + "To check the current pursue priority of buffs on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Buff) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Buff + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + bool list_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else if (!arg1.compare("list")) { + ++ab_arg; + list_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + if (EQ::ValueWithin(type_value, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must enter a value between {} and {}.", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else if (!list_check) { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] pursue cast priority is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypePriority(spell_type, BotPriorityCategories::Pursue) + ).c_str() + ); + } + else if (list_check) { + auto cast_order = my_bot->GetSpellTypesPrioritized(BotPriorityCategories::Pursue); + + for (auto& current_cast : cast_order) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] pursue cast priority for is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(current_cast.spellType), + (current_cast.priority == 0 ? "disabled (0)" : std::to_string(current_cast.priority)) + ).c_str() + ); + } + + c->Message( + Chat::Green, + fmt::format( + "{} says, 'Anything not listed is currently disabled (0).'", + my_bot->GetCleanName() + ).c_str() + ); + + return; + } + else { + my_bot->SetSpellTypePriority(spell_type, BotPriorityCategories::Pursue, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] pursue cast priority was set to [{}].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypePriority(spell_type, BotPriorityCategories::Pursue) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] pursue cast priority to [{}].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_resist_limits.cpp b/zone/bot_commands/spell_resist_limits.cpp new file mode 100644 index 0000000000..e7c928c338 --- /dev/null +++ b/zone/bot_commands/spell_resist_limits.cpp @@ -0,0 +1,214 @@ +#include "../bot_command.h" + +void bot_command_spell_resist_limits(Client* c, const Seperator* sep) { + if (helper_command_alias_fail(c, "bot_command_spell_resist_limits", sep->arg[0], "spellresistlimits")) { + c->Message(Chat::White, "note: Sets the limit of a target's resists to where a bot won't attempt to cast due to resist chances."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Sets the limit of a target's resists to where a bot won't attempt to cast due to resist chances." }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots' slow resist limit to 250:", + fmt::format( + "{} {} 250 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Slow) + ), + fmt::format( + "{} {} 250 spawned", + sep->arg[0], + BotSpellTypes::Slow + ) + }; + p.examples_two = + { + "To set Magicians to limit their resist to 175 for nukes:", + fmt::format( + "{} {} 175 byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Nuke), + Class::Magician + ), + fmt::format( + "{} {} 175 byclass {}", + sep->arg[0], + BotSpellTypes::Nuke, + Class::Magician + + ) + }; + p.examples_three = + { + "To check the current debuff resist limit on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::Debuff) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Debuff + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + + if (type_value < 0 || type_value > 1000) { + c->Message(Chat::Yellow, "You must enter a value between 1-1000."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] resist limit is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeResistLimit(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeResistLimit(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] resist limit was set to [{}].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeResistLimit(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] resist limit to [{}].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_target_count.cpp b/zone/bot_commands/spell_target_count.cpp new file mode 100644 index 0000000000..ad938d9b52 --- /dev/null +++ b/zone/bot_commands/spell_target_count.cpp @@ -0,0 +1,214 @@ +#include "../bot_command.h" + +void bot_command_spell_target_count(Client* c, const Seperator* sep) +{ + if (helper_command_alias_fail(c, "bot_command_spell_target_count", sep->arg[0], "spelltargetcount")) { + c->Message(Chat::White, "note: Decides how many eligible targets are required for an AE or group spell to cast by spell type."); + + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + BotCommandHelpParams p; + + p.description = { "Decides how many eligible targets are required for an AE or group spell to cast by spell type." }; + p.example_format = + { + fmt::format("{} [Type Shortname] [value] [actionable]", sep->arg[0]), + fmt::format("{} [Type ID] [value] [actionable]", sep->arg[0]) + }; + p.examples_one = + { + "To set all bots to AEMez with 5 or more targets:", + fmt::format( + "{} {} 5 spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::AEMez) + ), + fmt::format( + "{} {} 5 spawned", + sep->arg[0], + BotSpellTypes::AEMez + ) + }; + p.examples_two = + { + "To set Wizards to require 5 targets for AENukes:", + fmt::format( + "{} {} byclass {}", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::AENukes), + Class::Wizard + ), + fmt::format( + "{} {} byclass {}", + sep->arg[0], + BotSpellTypes::AENukes, + Class::Wizard + ) + }; + p.examples_three = + { + "To check the current AESlow count on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + Bot::GetSpellTypeShortNameByID(BotSpellTypes::AESlow) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::AESlow + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->SendSpellTypePrompts(); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + + return; + } + + std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spell_type = 0; + uint32 type_value = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spell_type = atoi(sep->arg[1]); + + if (!EQ::ValueWithin(spell_type, BotSpellTypes::START, BotSpellTypes::END)) { + c->Message(Chat::Yellow, "You must choose a valid spell type. Spell types range from %i to %i", BotSpellTypes::START, BotSpellTypes::END); + + return; + } + } + else { + if (Bot::GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spell_type = Bot::GetSpellTypeIDByShortName(arg1); + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + if (sep->IsNumber(2)) { + type_value = atoi(sep->arg[2]); + ++ab_arg; + + if (type_value < 1 || type_value > 100) { + c->Message(Chat::Yellow, "You must enter a value between 1-100."); + + return; + } + } + else if (!arg2.compare("current")) { + ++ab_arg; + current_check = true; + } + else { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string class_race_arg = sep->arg[ab_arg]; + bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { + class_race_check = true; + } + + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { + return; + } + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + Bot* first_found = nullptr; + int success_count = 0; + for (auto my_bot : sbl) { + if (my_bot->BotPassiveCheck()) { + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] target count is currently [{}].'", + my_bot->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + my_bot->GetSpellTypeAEOrGroupTargetCount(spell_type) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeAEOrGroupTargetCount(spell_type, type_value); + ++success_count; + } + } + if (!current_check) { + if (success_count == 1 && first_found) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] target count was set to [{}].'", + first_found->GetCleanName(), + Bot::GetSpellTypeNameByID(spell_type), + first_found->GetSpellTypeAEOrGroupTargetCount(spell_type) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] target count to [{}].", + success_count, + Bot::GetSpellTypeNameByID(spell_type), + type_value + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spelltypes.cpp b/zone/bot_commands/spelltypes.cpp new file mode 100644 index 0000000000..f91b3ed7e1 --- /dev/null +++ b/zone/bot_commands/spelltypes.cpp @@ -0,0 +1,9 @@ +void bot_command_spelltype_ids(Client* c, const Seperator* sep) +{ + SendSpellTypeWindow(c, sep); +} + +void bot_command_spelltype_names(Client* c, const Seperator* sep) +{ + SendSpellTypeWindow(c, sep); +} diff --git a/zone/bot_commands/summon_corpse.cpp b/zone/bot_commands/summon_corpse.cpp deleted file mode 100644 index 2a7a5b80f5..0000000000 --- a/zone/bot_commands/summon_corpse.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "../bot_command.h" - -void bot_command_summon_corpse(Client *c, const Seperator *sep) -{ - // Same methodology as old command..but, does not appear to work... (note: didn't work there, either...) - - // temp - c->Message(Chat::White, "This command is currently unavailable..."); - return; - - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_SummonCorpse]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_SummonCorpse) || helper_command_alias_fail(c, "bot_command_summon_corpse", sep->arg[0], "summoncorpse")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_SummonCorpse); - return; - } - - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter; - if (helper_spell_check_fail(local_entry)) - continue; - - auto target_mob = ActionableTarget::AsSingle_ByPlayer(c); - if (!target_mob) - continue; - - if (spells[local_entry->spell_id].base_value[EFFECTIDTOINDEX(1)] < target_mob->GetLevel()) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/suspend.cpp b/zone/bot_commands/suspend.cpp index e3198e79e9..63c728fd3d 100644 --- a/zone/bot_commands/suspend.cpp +++ b/zone/bot_commands/suspend.cpp @@ -6,17 +6,17 @@ void bot_command_suspend(Client *c, const Seperator *sep) return; } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([actionable: ] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationmembers | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); return; } const int ab_mask = ActionableBots::ABM_NoFilter; - std::list sbl; + std::vector sbl; if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); for (auto bot_iter : sbl) { bot_iter->SetPauseAI(true); } diff --git a/zone/bot_commands/taunt.cpp b/zone/bot_commands/taunt.cpp index a15d29a5bd..1496646908 100644 --- a/zone/bot_commands/taunt.cpp +++ b/zone/bot_commands/taunt.cpp @@ -1,118 +1,189 @@ #include "../bot_command.h" -void bot_command_taunt(Client *c, const Seperator *sep) +void bot_command_taunt(Client* c, const Seperator* sep) { - if (helper_command_alias_fail(c, "bot_command_taunt", sep->arg[0], "taunt")) + if (helper_command_alias_fail(c, "bot_command_taunt", sep->arg[0], "taunt")) { + c->Message(Chat::White, "note: TAllows you to turn on/off the taunting state of your bots and/or their pets."); + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s ([option: on | off]) ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); + BotCommandHelpParams p; + + p.description = { "Allows you to turn on/off the taunting state of your bots and/or their pets." }; + p.example_format = { fmt::format("{} [on / off / pet] [optional: pet] [actionable, default: target]", sep->arg[0]) }; + p.examples_one = + { + "To turn off taunt on all bots:", + fmt::format( + "{} off spawned", + sep->arg[0] + ) + }; + p.examples_two = + { + "To turn on taunt on all bots' pets:", + fmt::format( + "{} on pet spawned", + sep->arg[0] + ) + }; + p.examples_three = + { + "To turn on taunt for all ShadowKnights:", + fmt::format( + "{} on byclass {}", + sep->arg[0], + Class::ShadowKnight + ) + }; + p.actionables = { "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + if (RuleB(Bots, SendClassRaceOnHelp)) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {} for information about race/class IDs.", + Saylink::Silent("^classracelist") + ).c_str() + ); + } + return; } - const int ab_mask = ActionableBots::ABM_Type1; std::string arg1 = sep->arg[1]; + std::string arg2 = sep->arg[2]; bool taunt_state = false; - bool toggle_taunt = true; + bool pet_taunt = false; + bool valid_option = false; + int ab_arg = 1; + if (!arg1.compare("on")) { taunt_state = true; - toggle_taunt = false; - ab_arg = 2; + valid_option = true; + ++ab_arg; } else if (!arg1.compare("off")) { - toggle_taunt = false; - ab_arg = 2; + valid_option = true; + ++ab_arg; + } + + if (!arg2.compare("pet")) { + pet_taunt = true; + valid_option = true; + ++ab_arg; + } + + if (!valid_option) { + c->Message( + Chat::Yellow, + fmt::format( + "Incorrect argument, use {} for information regarding this command.", + Saylink::Silent( + fmt::format("{} help", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + + const int ab_mask = ActionableBots::ABM_Type1; + std::string actionable_arg = sep->arg[ab_arg]; + + if (actionable_arg.empty()) { + actionable_arg = "target"; } std::string class_race_arg = sep->arg[ab_arg]; bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { class_race_check = true; } - std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[(ab_arg + 1)] : nullptr, class_race_check ? atoi(sep->arg[(ab_arg + 1)]) : 0) == ActionableBots::ABT_None) { + std::vector sbl; + + if (ActionableBots::PopulateSBL(c, actionable_arg, sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); - int taunting_count = 0; - for (auto bot_iter : sbl) { - if (!bot_iter->GetSkill(EQ::skills::SkillTaunt)) { - continue; - } + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); + + int bot_taunting_count = 0; + int pet_taunting_count = 0; + + if (!pet_taunt) { + for (auto bot_iter : sbl) { + if (!bot_iter->GetSkill(EQ::skills::SkillTaunt)) { + continue; + } - if (toggle_taunt) { - bot_iter->SetTaunting(!bot_iter->IsTaunting()); - } else { bot_iter->SetTaunting(taunt_state); - } - if (sbl.size() == 1) { - Bot::BotGroupSay( + Bot::RaidGroupSay( bot_iter, fmt::format( "I am {} taunting.", bot_iter->IsTaunting() ? "now" : "no longer" ).c_str() ); - } - ++taunting_count; + ++bot_taunting_count; + } } - for (auto bot_iter : sbl) { - if (!bot_iter->HasPet()) { - continue; - } + if (pet_taunt) { + for (auto bot_iter : sbl) { + if (!bot_iter->HasPet()) { + continue; + } - if (!bot_iter->GetPet()->GetSkill(EQ::skills::SkillTaunt)) { - continue; - } + if (!bot_iter->GetPet()->GetSkill(EQ::skills::SkillTaunt)) { + continue; + } - if (toggle_taunt) { - bot_iter->GetPet()->CastToNPC()->SetTaunting(!bot_iter->GetPet()->CastToNPC()->IsTaunting()); - } else { bot_iter->GetPet()->CastToNPC()->SetTaunting(taunt_state); - } - if (sbl.size() == 1) { - Bot::BotGroupSay( + Bot::RaidGroupSay( bot_iter, fmt::format( "My Pet is {} taunting.", bot_iter->GetPet()->CastToNPC()->IsTaunting() ? "now" : "no longer" ).c_str() ); - } - ++taunting_count; + ++pet_taunting_count; + } } - if (taunting_count) { - if (toggle_taunt) { - c->Message( - Chat::White, - fmt::format( - "{} of your bots and their pets {} toggled their taunting state", - taunting_count, - taunting_count != 1 ? "have" : "has" - ).c_str() - ); - } else { - c->Message( - Chat::White, - fmt::format( - "{} of your bots and their pets {} {} taunting.", - taunting_count, - taunting_count != 1 ? "have" : "has", - taunt_state ? "started" : "stopped" - ).c_str() - ); - } + if (bot_taunting_count || pet_taunting_count) { + c->Message( + Chat::Green, + fmt::format( + "{} of your {} are {} taunting.", + (bot_taunting_count ? bot_taunting_count : pet_taunting_count), + (bot_taunting_count ? "bots" : "bots' pets"), + taunt_state ? "now" : "no longer" + ).c_str() + ); } else { - c->Message(Chat::White, "None of your bots are capable of taunting"); + c->Message( + Chat::Yellow, + fmt::format( + "None of your {} are capable of taunting.", + !pet_taunt ? "bots" : "bots' pets" + ).c_str() + ); } } diff --git a/zone/bot_commands/teleport.cpp b/zone/bot_commands/teleport.cpp deleted file mode 100644 index 94e8ee2b05..0000000000 --- a/zone/bot_commands/teleport.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "../bot_command.h" - -void bot_command_circle(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Depart]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Depart) || helper_command_alias_fail(c, "bot_command_circle", sep->arg[0], "circle")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [list | destination] ([option: single])", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Depart, Class::Druid); - return; - } - - bool single = false; - std::string single_arg = sep->arg[2]; - if (!single_arg.compare("single")) - single = true; - - std::string destination = sep->arg[1]; - if (!destination.compare("list")) { - auto my_druid_bot = ActionableBots::AsGroupMember_ByClass(c, c, Class::Druid); - helper_command_depart_list(c, my_druid_bot, nullptr, local_list, single); - return; - } - else if (destination.empty()) { - c->Message(Chat::White, "A [destination] or [list] argument is required to use this command"); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToDepart(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->caster_class != Class::Druid) - continue; - if (local_entry->single != single) - continue; - if (destination.compare(spells[local_entry->spell_id].teleport_zone)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} - -void bot_command_portal(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_Depart]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_Depart) || helper_command_alias_fail(c, "bot_command_portal", sep->arg[0], "portal")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [list | destination] ([option: single])", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_Depart, Class::Wizard); - return; - } - - bool single = false; - std::string single_arg = sep->arg[2]; - if (!single_arg.compare("single")) - single = true; - - std::string destination = sep->arg[1]; - if (!destination.compare("list")) { - auto my_wizard_bot = ActionableBots::AsGroupMember_ByClass(c, c, Class::Wizard); - helper_command_depart_list(c, nullptr, my_wizard_bot, local_list, single); - return; - } - else if (destination.empty()) { - c->Message(Chat::White, "A [destination] or [list] argument is required to use this command"); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter->SafeCastToDepart(); - if (helper_spell_check_fail(local_entry)) - continue; - if (local_entry->caster_class != Class::Wizard) - continue; - if (local_entry->single != single) - continue; - if (destination.compare(spells[local_entry->spell_id].teleport_zone)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_commands/timer.cpp b/zone/bot_commands/timer.cpp index 93eab687d6..99ad38001b 100644 --- a/zone/bot_commands/timer.cpp +++ b/zone/bot_commands/timer.cpp @@ -2,10 +2,12 @@ void bot_command_timer(Client* c, const Seperator* sep) { - if (helper_command_alias_fail(c, "bot_command_timer", sep->arg[0], "timer")) + if (helper_command_alias_fail(c, "bot_command_timer", sep->arg[0], "timer")) { return; + } + if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: %s [clear | has | set] [disc | item | spell] [timer ID | item ID | spell ID | all] [optional ms for set] [actionable].", sep->arg[0]); + c->Message(Chat::White, "usage: %s [clear | has | set] [disc | item | spell] [timer ID | item ID | spell ID | all] [optional ms for set] ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name])).", sep->arg[0]); c->Message(Chat::White, "When setting, you can leave the value blank to use the default for the item or specify a value in ms to set the timer to."); c->Message(Chat::White, "Returns or sets the provided timer(s) for the selected bot(s) or clears the selected timer(s) for the selected bot(s)."); return; @@ -88,15 +90,18 @@ void bot_command_timer(Client* c, const Seperator* sep) std::string class_race_arg = sep->arg[ab_arg]; bool class_race_check = false; + if (!class_race_arg.compare("byclass") || !class_race_arg.compare("byrace")) { class_race_check = true; } - std::list sbl; + std::vector sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, !class_race_check ? sep->arg[ab_arg + 1] : nullptr, class_race_check ? atoi(sep->arg[ab_arg + 1]) : 0) == ActionableBots::ABT_None) { return; } - sbl.remove(nullptr); + + sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); for (auto my_bot : sbl) { bool found = false; diff --git a/zone/bot_commands/track.cpp b/zone/bot_commands/track.cpp index 9ac0904a2a..5c62f3a1de 100644 --- a/zone/bot_commands/track.cpp +++ b/zone/bot_commands/track.cpp @@ -13,7 +13,7 @@ void bot_command_track(Client *c, const Seperator *sep) std::string tracking_scope = sep->arg[1]; - std::list sbl; + std::vector sbl; MyBots::PopulateSBL_BySpawnedBots(c, sbl); uint16 class_mask = (player_class_bitmasks[Class::Ranger] | player_class_bitmasks[Class::Druid] | player_class_bitmasks[Class::Bard]); @@ -67,6 +67,6 @@ void bot_command_track(Client *c, const Seperator *sep) } my_bot->InterruptSpell(); - Bot::BotGroupSay(my_bot, tracking_msg.c_str()); + Bot::RaidGroupSay(my_bot, tracking_msg.c_str()); entity_list.ShowSpawnWindow(c, (c->GetLevel() * base_distance), track_named); } diff --git a/zone/bot_commands/water_breathing.cpp b/zone/bot_commands/water_breathing.cpp deleted file mode 100644 index cb9b792c47..0000000000 --- a/zone/bot_commands/water_breathing.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "../bot_command.h" - -void bot_command_water_breathing(Client *c, const Seperator *sep) -{ - bcst_list* local_list = &bot_command_spells[BCEnum::SpT_WaterBreathing]; - if (helper_spell_list_fail(c, local_list, BCEnum::SpT_WaterBreathing) || helper_command_alias_fail(c, "bot_command_water_breathing", sep->arg[0], "waterbreathing")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(Chat::White, "usage: () %s", sep->arg[0]); - helper_send_usage_required_bots(c, BCEnum::SpT_WaterBreathing); - return; - } - - ActionableTarget::Types actionable_targets; - Bot* my_bot = nullptr; - std::list sbl; - MyBots::PopulateSBL_BySpawnedBots(c, sbl); - - bool cast_success = false; - for (auto list_iter : *local_list) { - auto local_entry = list_iter; - if (helper_spell_check_fail(local_entry)) - continue; - - auto target_mob = actionable_targets.Select(c, local_entry->target_type, FRIENDLY); - if (!target_mob) - continue; - - my_bot = ActionableBots::Select_ByMinLevelAndClass(c, local_entry->target_type, sbl, local_entry->spell_level, local_entry->caster_class, target_mob); - if (!my_bot) - continue; - - cast_success = helper_cast_standard_spell(my_bot, target_mob, local_entry->spell_id); - break; - } - - helper_no_available_bots(c, my_bot); -} diff --git a/zone/bot_database.cpp b/zone/bot_database.cpp index a24cd0c308..d4131a3493 100644 --- a/zone/bot_database.cpp +++ b/zone/bot_database.cpp @@ -22,6 +22,7 @@ #include "../common/strings.h" #include "../common/eqemu_logsys.h" +#include "../common/repositories/bot_blocked_buffs_repository.h" #include "../common/repositories/bot_buffs_repository.h" #include "../common/repositories/bot_create_combinations_repository.h" #include "../common/repositories/bot_data_repository.h" @@ -35,6 +36,7 @@ #include "../common/repositories/bot_pet_buffs_repository.h" #include "../common/repositories/bot_pet_inventories_repository.h" #include "../common/repositories/bot_spell_casting_chances_repository.h" +#include "../common/repositories/bot_settings_repository.h" #include "../common/repositories/bot_stances_repository.h" #include "../common/repositories/bot_timers_repository.h" #include "../common/repositories/character_data_repository.h" @@ -184,6 +186,7 @@ bool BotDatabase::QueryNameAvailablity(const std::string& bot_name, bool& availa if ( bot_name.empty() || bot_name.size() > 60 || + !database.CheckNameFilter(bot_name) || database.IsNameUsed(bot_name) ) { return false; @@ -244,6 +247,8 @@ bool BotDatabase::LoadBotsList(const uint32 owner_id, std::listSetSurname(e.last_name); loaded_bot->SetTitle(e.title); loaded_bot->SetSuffix(e.suffix); - - loaded_bot->SetShowHelm(e.show_helm); - - auto bfd = EQ::Clamp(e.follow_distance, static_cast(1), BOT_FOLLOW_DISTANCE_DEFAULT_MAX); - - loaded_bot->SetFollowDistance(bfd); - - loaded_bot->SetStopMeleeLevel(e.stop_melee_level); - - loaded_bot->SetBotEnforceSpellSetting(e.enforce_spell_settings); - - loaded_bot->SetBotArcherySetting(e.archery_setting); - - loaded_bot->SetBotCasterRange(e.caster_range); } return true; @@ -476,13 +466,6 @@ bool BotDatabase::SaveNewBot(Bot* b, uint32& bot_id) e.poison = b->GetBasePR(); e.disease = b->GetBaseDR(); e.corruption = b->GetBaseCorrup(); - e.show_helm = b->GetShowHelm() ? 1 : 0; - e.follow_distance = b->GetFollowDistance(); - e.stop_melee_level = b->GetStopMeleeLevel(); - e.expansion_bitmask = b->GetExpansionBitmask(); - e.enforce_spell_settings = b->GetBotEnforceSpellSetting(); - e.archery_setting = b->IsBotArcher() ? 1 : 0; - e.caster_range = b->GetBotCasterRange(); e = BotDataRepository::InsertOne(database, e); @@ -547,12 +530,6 @@ bool BotDatabase::SaveBot(Bot* b) e.poison = b->GetBasePR(); e.disease = b->GetBaseDR(); e.corruption = b->GetBaseCorrup(); - e.show_helm = b->GetShowHelm() ? 1 : 0; - e.follow_distance = b->GetFollowDistance(); - e.stop_melee_level = b->GetStopMeleeLevel(); - e.expansion_bitmask = b->GetExpansionBitmask(); - e.enforce_spell_settings = b->GetBotEnforceSpellSetting(); - e.archery_setting = b->IsBotArcher() ? 1 : 0; return BotDataRepository::UpdateOne(database, e); } @@ -823,9 +800,9 @@ bool BotDatabase::LoadTimers(Bot* b) ) ); - std::vector v; + std::vector v; - BotTimer_Struct t{ }; + BotTimer t{ }; for (const auto& e : l) { if (e.timer_value < (Timer::GetCurrentTime() + e.recast_time)) { @@ -859,7 +836,7 @@ bool BotDatabase::SaveTimers(Bot* b) return false; } - std::vector v = b->GetBotTimers(); + std::vector v = b->GetBotTimers(); if (v.empty()) { return true; @@ -868,7 +845,7 @@ bool BotDatabase::SaveTimers(Bot* b) std::vector l; if (!v.empty()) { - for (auto & bot_timer : v) { + for (auto& bot_timer : v) { if (bot_timer.timer_value <= Timer::GetCurrentTime()) { continue; } @@ -1670,59 +1647,6 @@ bool BotDatabase::SaveAllArmorColors(const uint32 owner_id, const uint32 rgb_val return BotInventoriesRepository::SaveAllArmorColors(database, owner_id, rgb_value); } -bool BotDatabase::SaveHelmAppearance(const uint32 bot_id, const bool show_flag) -{ - if (!bot_id) { - return false; - } - - auto e = BotDataRepository::FindOne(database, bot_id); - - e.show_helm = show_flag ? 1 : 0; - - return BotDataRepository::UpdateOne(database, e); -} - -bool BotDatabase::SaveAllHelmAppearances(const uint32 owner_id, const bool show_flag) -{ - if (!owner_id) { - return false; - } - - return BotDataRepository::SaveAllHelmAppearances(database, owner_id, show_flag); -} - -bool BotDatabase::ToggleAllHelmAppearances(const uint32 owner_id) -{ - if (!owner_id) { - return false; - } - - return BotDataRepository::ToggleAllHelmAppearances(database, owner_id); -} - -bool BotDatabase::SaveFollowDistance(const uint32 bot_id, const uint32 follow_distance) -{ - if (!bot_id || !follow_distance) { - return false; - } - - auto e = BotDataRepository::FindOne(database, bot_id); - - e.follow_distance = follow_distance; - - return BotDataRepository::UpdateOne(database, e); -} - -bool BotDatabase::SaveAllFollowDistances(const uint32 owner_id, const uint32 follow_distance) -{ - if (!owner_id || !follow_distance) { - return false; - } - - return BotDataRepository::SaveAllFollowDistances(database, owner_id, follow_distance); -} - bool BotDatabase::CreateCloneBot(const uint32 bot_id, const std::string& clone_name, uint32& clone_id) { if (!bot_id || clone_name.empty()) { @@ -1771,19 +1695,6 @@ bool BotDatabase::CreateCloneBotInventory(const uint32 bot_id, const uint32 clon return BotInventoriesRepository::InsertMany(database, l); } -bool BotDatabase::SaveStopMeleeLevel(const uint32 bot_id, const uint8 sml_value) -{ - if (!bot_id) { - return false; - } - - auto e = BotDataRepository::FindOne(database, bot_id); - - e.stop_melee_level = sml_value; - - return BotDataRepository::UpdateOne(database, e); -} - bool BotDatabase::LoadOwnerOptions(Client* c) { if (!c || !c->CharacterID()) { @@ -2224,74 +2135,6 @@ uint32 BotDatabase::GetRaceClassBitmask(uint32 bot_race) return e.race ? e.classes : 0; } -bool BotDatabase::SaveExpansionBitmask(const uint32 bot_id, const int expansion_bitmask) -{ - if (!bot_id) { - return false; - } - - auto e = BotDataRepository::FindOne(database, bot_id); - - if (!e.bot_id) { - return false; - } - - e.expansion_bitmask = expansion_bitmask; - - return BotDataRepository::UpdateOne(database, e); -} - -bool BotDatabase::SaveEnforceSpellSetting(const uint32 bot_id, const bool enforce_spell_setting) -{ - if (!bot_id) { - return false; - } - - auto e = BotDataRepository::FindOne(database, bot_id); - - if (!e.bot_id) { - return false; - } - - e.enforce_spell_settings = enforce_spell_setting ? 1 : 0; - - return BotDataRepository::UpdateOne(database, e); -} - -bool BotDatabase::SaveBotArcherSetting(const uint32 bot_id, const bool bot_archer_setting) -{ - if (!bot_id) { - return false; - } - - auto e = BotDataRepository::FindOne(database, bot_id); - - if (!e.bot_id) { - return false; - } - - e.archery_setting = bot_archer_setting ? 1 : 0; - - return BotDataRepository::UpdateOne(database, e); -} - -bool BotDatabase::SaveBotCasterRange(const uint32 bot_id, const uint32 bot_caster_range_value) -{ - if (!bot_id) { - return false; - } - - auto e = BotDataRepository::FindOne(database, bot_id); - - if (!e.bot_id) { - return false; - } - - e.caster_range = bot_caster_range_value; - - return BotDataRepository::UpdateOne(database, e); -} - const uint8 BotDatabase::GetBotClassByID(const uint32 bot_id) { const auto& e = BotDataRepository::FindOne(database, bot_id); @@ -2322,7 +2165,7 @@ std::vector BotDatabase::GetBotIDsByCharacterID(const uint32 character_i class_id ) : "" - ) + ) ) ); @@ -2360,3 +2203,319 @@ const int BotDatabase::GetBotExtraHasteByID(const uint32 bot_id) return e.bot_id ? e.extra_haste : 0; } + +bool BotDatabase::LoadBotSettings(Mob* m) +{ + if (!m) { + return false; + } + + if (!m->IsOfClientBot()) { + return false; + } + + uint32 mob_id = (m->IsClient() ? m->CastToClient()->CharacterID() : m->CastToBot()->GetBotID()); + uint8 stance_id = (m->IsBot() ? m->CastToBot()->GetBotStance() : 0); + + std::string query = ""; + + if (m->IsClient()) { + query = fmt::format("`character_id` = {} AND `stance` = {}", mob_id, stance_id); + } + else { + query = fmt::format("`bot_id` = {} AND `stance` = {}", mob_id, stance_id); + } + + if (stance_id == Stance::Passive) { + LogBotSettings("{} is currently set to {} [#{}]. No saving or loading required.", m->GetCleanName(), Stance::GetName(Stance::Passive), Stance::Passive); + return true; + } + + const auto& l = BotSettingsRepository::GetWhere(database, query); + + if (l.empty()) { + return true; + } + + for (const auto& e : l) { + if (e.setting_type == BotSettingCategories::BaseSetting) { + LogBotSettings("[{}] says, 'Loading {} [{}] - setting to [{}].", + m->GetCleanName(), + Bot::GetBotSettingCategoryName(e.setting_type), + e.setting_type, + e.value + ); + } + else { + LogBotSettings("[{}] says, 'Loading {} [{}], {} [{}] - setting to [{}].", + m->GetCleanName(), + Bot::GetBotSpellCategoryName(e.setting_type), + e.setting_type, + Bot::GetSpellTypeNameByID(e.setting_id), + e.setting_id, + e.value + ); + } + + if (m->IsClient()) { + m->CastToClient()->SetBotSetting(e.setting_type, e.setting_id, e.value); + } + else { + m->CastToBot()->SetBotSetting(e.setting_type, e.setting_id, e.value); + } + } + + return true; +} + +bool BotDatabase::SaveBotSettings(Mob* m) +{ + if (!m) { + return false; + } + + if (!m->IsOfClientBot()) { + return false; + } + + uint32 bot_id = (m->IsBot() ? m->CastToBot()->GetBotID() : 0); + uint32 character_id = (m->IsClient() ? m->CastToClient()->CharacterID() : 0); + uint8 stance_id = (m->IsBot() ? m->CastToBot()->GetBotStance() : 0); + + if (stance_id == Stance::Passive) { + LogBotSettings("{} is currently set to {} [#{}]. No saving or loading required.", m->GetCleanName(), Stance::GetName(Stance::Passive), Stance::Passive); + return true; + } + + std::string query = ""; + + if (m->IsClient()) { + query = fmt::format("`character_id` = {} AND `stance` = {}", character_id, stance_id); + } + else { + query = fmt::format("`bot_id` = {} AND `stance` = {}", bot_id, stance_id); + } + + BotSettingsRepository::DeleteWhere(database, query); + + std::vector v; + + if (m->IsBot()) { + uint8 bot_stance = m->CastToBot()->GetBotStance(); + + for (uint16 i = BotBaseSettings::START_ALL; i <= BotBaseSettings::END; ++i) { + if (m->CastToBot()->GetBotBaseSetting(i) != m->CastToBot()->GetDefaultBotBaseSetting(i, bot_stance)) { + auto e = BotSettingsRepository::BotSettings{ + .character_id = character_id, + .bot_id = bot_id, + .stance = stance_id, + .setting_id = static_cast(i), + .setting_type = static_cast(BotSettingCategories::BaseSetting), + .value = static_cast(m->CastToBot()->GetBotBaseSetting(i)), + .category_name = Bot::GetBotSpellCategoryName(BotSettingCategories::BaseSetting), + .setting_name = Bot::GetBotSettingCategoryName(i) + }; + + v.emplace_back(e); + + LogBotSettings("{} says, 'Saving {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSettingCategoryName(i), i, e.value, m->CastToBot()->GetDefaultBotBaseSetting(i)); + } + } + + for (uint16 i = BotSettingCategories::START_NO_BASE; i <= BotSettingCategories::END; ++i) { + for (uint16 x = BotSpellTypes::START; x <= BotSpellTypes::END; ++x) { + if (m->CastToBot()->GetSetting(i, x) != m->CastToBot()->GetDefaultSetting(i, x, bot_stance)) { + auto e = BotSettingsRepository::BotSettings{ + .character_id = character_id, + .bot_id = bot_id, + .stance = stance_id, + .setting_id = static_cast(x), + .setting_type = static_cast(i), + .value = m->CastToBot()->GetSetting(i, x), + .category_name = Bot::GetBotSpellCategoryName(i), + .setting_name = Bot::GetSpellTypeNameByID(x) + }; + + v.emplace_back(e); + + LogBotSettings("{} says, 'Saving {} {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSpellCategoryName(i), Bot::GetSpellTypeNameByID(x), x, e.value, m->CastToBot()->GetDefaultSetting(i, x, bot_stance)); + } + } + } + } + + if (m->IsClient()) { + if (m->CastToClient()->GetDefaultBotSettings(BotSettingCategories::BaseSetting, BotBaseSettings::IllusionBlock) != m->CastToClient()->GetIllusionBlock()) { // Only illusion block supported + auto e = BotSettingsRepository::BotSettings{ + .character_id = character_id, + .bot_id = bot_id, + .stance = stance_id, + .setting_id = static_cast(BotBaseSettings::IllusionBlock), + .setting_type = static_cast(BotSettingCategories::BaseSetting), + .value = m->CastToClient()->GetIllusionBlock(), + .category_name = Bot::GetBotSpellCategoryName(BotSettingCategories::BaseSetting), + .setting_name = Bot::GetBotSettingCategoryName(BotBaseSettings::IllusionBlock) + }; + + v.emplace_back(e); + + LogBotSettings("{} says, 'Saving {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSettingCategoryName(BotBaseSettings::IllusionBlock), BotBaseSettings::IllusionBlock, e.value, m->CastToClient()->GetIllusionBlock()); + } + + for (uint16 i = BotSettingCategories::START_CLIENT; i <= BotSettingCategories::END_CLIENT; ++i) { + for (uint16 x = BotSpellTypes::START; x <= BotSpellTypes::END; ++x) { + LogBotSettings("{} says, 'Checking {} {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSpellCategoryName(i), Bot::GetSpellTypeNameByID(x), x, m->CastToClient()->GetBotSetting(i, x), m->CastToClient()->GetDefaultBotSettings(i, x)); + if (m->CastToClient()->GetBotSetting(i, x) != m->CastToClient()->GetDefaultBotSettings(i, x)) { + auto e = BotSettingsRepository::BotSettings{ + .character_id = character_id, + .bot_id = bot_id, + .stance = stance_id, + .setting_id = static_cast(x), + .setting_type = static_cast(i), + .value = m->CastToClient()->GetBotSetting(i, x), + .category_name = Bot::GetBotSpellCategoryName(i), + .setting_name = Bot::GetSpellTypeNameByID(x) + }; + + v.emplace_back(e); + + LogBotSettings("{} says, 'Saving {} {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSpellCategoryName(i), Bot::GetSpellTypeNameByID(x), x, e.value, m->CastToClient()->GetDefaultBotSettings(i, x)); + } + } + } + } + + if (!v.empty()) { + const int inserted = BotSettingsRepository::ReplaceMany(database, v); + + if (!inserted) { + return false; + } + } + + return true; +} + +bool BotDatabase::DeleteBotSettings(const uint32 bot_id) +{ + if (!bot_id) { + return false; + } + + BotSettingsRepository::DeleteWhere( + database, + fmt::format( + "`bot_id` = {}", + bot_id + ) + ); + + return true; +} + +bool BotDatabase::LoadBotBlockedBuffs(Bot* b) +{ + if (!b) { + return false; + } + + const auto& l = BotBlockedBuffsRepository::GetWhere( + database, + fmt::format( + "`bot_id` = {}", + b->GetBotID() + ) + ); + + std::vector v; + + BotBlockedBuffs t{ }; + + for (const auto& e : l) { + t.spell_id = e.spell_id; + t.blocked = e.blocked; + t.blocked_pet = e.blocked_pet; + + v.push_back(t); + } + + if (!v.empty()) { + b->SetBotBlockedBuffs(v); + } + + return true; +} + +bool BotDatabase::SaveBotBlockedBuffs(Bot* b) +{ + if (!b) { + return false; + } + + if (!DeleteBotBlockedBuffs(b->GetBotID())) { + return false; + } + + std::vector v = b->GetBotBlockedBuffs(); + + if (v.empty()) { + return true; + } + + std::vector l; + + if (!v.empty()) { + for (auto& blocked_buff : v) { + if (blocked_buff.blocked == 0 && blocked_buff.blocked_pet == 0) { + continue; + } + + auto e = BotBlockedBuffsRepository::BotBlockedBuffs{ + .bot_id = b->GetBotID(), + .spell_id = blocked_buff.spell_id, + .blocked = blocked_buff.blocked, + .blocked_pet = blocked_buff.blocked_pet + }; + + l.push_back(e); + } + + if (l.empty()) { + return true; + } + + BotBlockedBuffsRepository::DeleteWhere( + database, + fmt::format( + "`bot_id` = {}", + b->GetBotID() + ) + ); + + const int inserted = BotBlockedBuffsRepository::InsertMany(database, l); + + if (!inserted) { + DeleteBotBlockedBuffs(b->GetBotID()); + return false; + } + } + + return true; +} + +bool BotDatabase::DeleteBotBlockedBuffs(const uint32 bot_id) +{ + if (!bot_id) { + return false; + } + + BotBlockedBuffsRepository::DeleteWhere( + database, + fmt::format( + "`bot_id` = {}", + bot_id + ) + ); + + return true; +} diff --git a/zone/bot_database.h b/zone/bot_database.h index 139ebc643f..61fdc63016 100644 --- a/zone/bot_database.h +++ b/zone/bot_database.h @@ -74,6 +74,9 @@ class BotDatabase bool SaveTimers(Bot* b); bool DeleteTimers(const uint32 bot_id); + bool LoadBotBlockedBuffs(Bot* b); + bool SaveBotBlockedBuffs(Bot* b); + bool DeleteBotBlockedBuffs(const uint32 bot_id); /* Bot inventory functions */ bool QueryInventoryCount(const uint32 bot_id, uint32& item_count); @@ -88,10 +91,6 @@ class BotDatabase bool SaveEquipmentColor(const uint32 bot_id, const int16 slot_id, const uint32 color); - bool SaveExpansionBitmask(const uint32 bot_id, const int expansion_bitmask); - bool SaveEnforceSpellSetting(const uint32 bot_id, const bool enforce_spell_setting); - - /* Bot pet functions */ bool LoadPetIndex(const uint32 bot_id, uint32& pet_index); bool LoadPetSpellID(const uint32 bot_id, uint32& pet_spell_id); @@ -120,26 +119,16 @@ class BotDatabase bool SaveAllArmorColorBySlot(const uint32 owner_id, const int16 slot_id, const uint32 rgb_value); bool SaveAllArmorColors(const uint32 owner_id, const uint32 rgb_value); - bool SaveHelmAppearance(const uint32 bot_id, const bool show_flag = true); - bool SaveAllHelmAppearances(const uint32 owner_id, const bool show_flag = true); - - bool ToggleAllHelmAppearances(const uint32 owner_id); - - bool SaveFollowDistance(const uint32 bot_id, const uint32 follow_distance); - bool SaveAllFollowDistances(const uint32 owner_id, const uint32 follow_distance); - bool CreateCloneBot(const uint32 bot_id, const std::string& clone_name, uint32& clone_id); bool CreateCloneBotInventory(const uint32 bot_id, const uint32 clone_id); - bool SaveStopMeleeLevel(const uint32 bot_id, const uint8 sml_value); - - bool SaveBotArcherSetting(const uint32 bot_id, const bool bot_archer_setting); - bool LoadOwnerOptions(Client *owner); bool SaveOwnerOption(const uint32 owner_id, size_t type, const bool flag); bool SaveOwnerOption(const uint32 owner_id, const std::pair type, const std::pair flag); - bool SaveBotCasterRange(const uint32 bot_id, const uint32 bot_caster_range_value); + bool LoadBotSettings(Mob* m); + bool SaveBotSettings(Mob* m); + bool DeleteBotSettings(const uint32 bot_id); /* Bot group functions */ bool LoadGroupedBotsByGroupID(const uint32 owner_id, const uint32 group_id, std::list& group_list); @@ -210,12 +199,6 @@ class BotDatabase static const char* SaveAllInspectMessages(); static const char* SaveAllArmorColorBySlot(); static const char* SaveAllArmorColors(); - static const char* SaveAllHelmAppearances(); - static const char* ToggleAllHelmAppearances(); - static const char* SaveFollowDistance(); - static const char* SaveAllFollowDistances(); - static const char* SaveStopMeleeLevel(); - static const char* SaveBotCasterRange(); /* fail::Bot heal rotation functions */ static const char* LoadHealRotation(); diff --git a/zone/bot_raid.cpp b/zone/bot_raid.cpp index 25d98ef867..e1eb607484 100644 --- a/zone/bot_raid.cpp +++ b/zone/bot_raid.cpp @@ -96,19 +96,10 @@ void Raid::HandleBotGroupDisband(uint32 owner, uint32 gid) // Remove the entire BOT group in this case if (b && gid != RAID_GROUPLESS && IsRaidMember(b->GetName()) && IsGroupLeader(b->GetName())) { auto r_group_members = GetRaidGroupMembers(GetGroup(b->GetName())); - auto g = new Group(b); - entity_list.AddGroup(g); - g->AddToGroup(b); - database.SetGroupLeaderName(g->GetID(), b->GetName()); for (auto m: r_group_members) { if (m.member->IsBot()) { auto b_member = m.member->CastToBot(); - if (strcmp(b_member->GetName(), b->GetName()) == 0) { - b->SetFollowID(owner); - } else { - Bot::AddBotToGroup(b_member, g); - } Bot::RemoveBotFromRaid(b_member); } } @@ -142,26 +133,6 @@ void Raid::HandleOfflineBots(uint32 owner) { } } -uint8 Bot::GetNumberNeedingHealedInRaidGroup(uint8& need_healed, uint8 hpr, bool includePets, Raid* raid) { - - if (raid) { - uint32 r_group = raid->GetGroup(GetName()); - - for (auto& m: raid->GetRaidGroupMembers(r_group)) { - if (m.member && !m.member->qglobal) { - if (m.member->GetHPRatio() <= hpr) { - need_healed++; - } - - if (includePets && m.member->GetPet() && m.member->GetPet()->GetHPRatio() <= hpr) { - need_healed++; - } - } - } - } - return need_healed; -} - void Bot::ProcessRaidInvite(Mob* invitee, Client* invitor, bool group_invite) { if (!invitee || !invitor) { @@ -176,13 +147,7 @@ void Bot::ProcessRaidInvite(Mob* invitee, Client* invitor, bool group_invite) { // If the Bot Owner is in our raid we need to be able to invite their Bots } else if (invitee->IsBot() && (invitee->CastToBot()->GetBotOwnerCharacterID() != invitor->CharacterID())) { - invitor->Message( - Chat::Red, - fmt::format( - "{} is not your Bot. You can only invite your own Bots, or Bots that belong to a Raid member.", - invitee->GetCleanName() - ).c_str() - ); + invitor->Message(Chat::Red, "%s's owner needs to be in your raid to be able to invite them.", invitee->GetCleanName()); return; } @@ -257,10 +222,6 @@ void Bot::CreateBotRaid(Mob* invitee, Client* invitor, bool group_invite, Raid* } else { raid->AddBot(b); } - - if (new_raid) { - invitee->SetFollowID(invitor->GetID()); - } } } @@ -323,7 +284,9 @@ void Client::SpawnRaidBotsOnConnect(Raid* raid) { if (bot) { bot->SetRaidGrouped(true); + bot->SetStoredRaid(raid); bot->p_raid_instance = raid; + bot->SetVerifiedRaid(false); } } } diff --git a/zone/bot_structs.h b/zone/bot_structs.h index 50e319e9e9..baa1aa73c3 100644 --- a/zone/bot_structs.h +++ b/zone/bot_structs.h @@ -20,6 +20,7 @@ #define BOT_STRUCTS #include "../common/types.h" +#include "../common/timer.h" #include @@ -64,7 +65,7 @@ struct BotSpellSetting { bool is_enabled = true; }; -struct BotSpells_Struct { +struct BotSpells { uint32 type; // 0 = never, must be one (and only one) of the defined values int16 spellid; // <= 0 = no spell int16 manacost; // -1 = use spdat, -2 = no cast time @@ -81,7 +82,25 @@ struct BotSpells_Struct { uint8 bucket_comparison; }; -struct BotTimer_Struct { +struct BotSpells_wIndex { + uint32 index; //index of AIBot_spells + uint32 type; // 0 = never, must be one (and only one) of the defined values + int16 spellid; // <= 0 = no spell + int16 manacost; // -1 = use spdat, -2 = no cast time + uint32 time_cancast; // when we can cast this spell next + int32 recast_delay; + int16 priority; + int16 resist_adjust; + uint8 minlevel; + uint8 maxlevel; + int16 min_hp; // >0 won't cast if HP is below + int16 max_hp; // >0 won't cast if HP is above + std::string bucket_name; + std::string bucket_value; + uint8 bucket_comparison; +}; + +struct BotTimer { uint32 timer_id; uint32 timer_value; uint32 recast_time; @@ -92,4 +111,44 @@ struct BotTimer_Struct { uint32 item_id; }; +struct BotSpellSettings { + uint16 spell_type; // type ID of bot category + std::string short_name; // type short name of bot category + std::string name; // type name of bot category + bool hold; // 0 = allow spell type, 1 = hold spell type + uint16 delay; // delay between casts of spell type, 1ms-60,000ms + uint8 min_threshold; // minimum target health threshold to allow casting of spell type + uint8 max_threshold; // maximum target health threshold to allow casting of spell type + uint16 resist_limit; // resist limit to skip spell type + bool aggro_check; // whether or not to check for possible aggro before casting + uint8 min_mana_pct; // lower mana percentage limit to allow spell cast + uint8 max_mana_pct; // upper mana percentage limit to allow spell cast + uint8 min_hp_pct; // lower HP percentage limit to allow spell cast + uint8 max_hp_pct; // upper HP percentage limit to allow spell cast + uint16 idle_priority; // idle priority of the spell type + uint16 engaged_priority; // engaged priority of the spell type + uint16 pursue_priority; // pursue priority of the spell type + uint16 ae_or_group_target_count; // require target count to cast an AE or Group spell type + uint16 announce_cast; // announce when casting a certain spell type + Timer recast_timer; // recast timer based off delay + Timer ai_delay; // spell timer based off delay +}; + +struct BotSpellTypeOrder { + uint16 spellType; + uint16 priority; +}; + +struct BotBlockedBuffs { + uint32_t bot_id; + uint32_t spell_id; + uint8_t blocked; + uint8_t blocked_pet; +}; + +struct BotSpellTypesByClass { + uint8_t min_level = 255; + std::string description; +}; + #endif // BOT_STRUCTS diff --git a/zone/botspellsai.cpp b/zone/botspellsai.cpp index 0050a58b20..35ca8dedc1 100644 --- a/zone/botspellsai.cpp +++ b/zone/botspellsai.cpp @@ -21,1226 +21,534 @@ #include "../common/repositories/bot_spells_entries_repository.h" #include "../common/repositories/npc_spells_repository.h" -bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { - - // Bot AI - Raid* raid = entity_list.GetRaidByBotName(GetName()); - +bool Bot::AICastSpell(Mob* tar, uint8 chance, uint16 spell_type, uint16 sub_target_type, uint16 sub_type) { if (!tar) { return false; } - if (!AI_HasSpells()) { - return false; - } + LogBotSpellChecksDetail("{} says, 'Attempting {} AICastSpell on {}.'", GetCleanName(), GetSpellTypeNameByID(spell_type), tar->GetCleanName()); - if (iChance < 100 && zone->random.Int(0, 100) > iChance) { + if ( + !AI_HasSpells() || + (spell_type == BotSpellTypes::Pet && tar != this) || + (IsPetBotSpellType(spell_type) && !tar->IsPet()) || + (!IsPetBotSpellType(spell_type) && tar->IsPet()) || + (!RuleB(Bots, AllowBuffingHealingFamiliars) && tar->IsFamiliar()) || + (tar->IsPet() && tar->IsCharmed() && spell_type == BotSpellTypes::PetBuffs && !RuleB(Bots, AllowCharmedPetBuffs)) || + (tar->IsPet() && tar->IsCharmed() && spell_type == BotSpellTypes::PetCures && !RuleB(Bots, AllowCharmedPetCures)) || + (tar->IsPet() && tar->IsCharmed() && IsHealBotSpellType(spell_type) && !RuleB(Bots, AllowCharmedPetHeals)) + ) { return false; } - - if (tar->GetAppearance() == eaDead && ((tar->IsClient() && !tar->CastToClient()->GetFeigned()) || tar->IsBot())) { + + if (chance < 100 && zone->random.Int(0, 100) > chance) { return false; } - uint8 botClass = GetClass(); - uint8 botLevel = GetLevel(); - - bool checked_los = false; //we do not check LOS until we are absolutely sure we need to, and we only do it once. - - BotSpell botSpell; - botSpell.SpellId = 0; - botSpell.SpellIndex = 0; - botSpell.ManaCost = 0; - - switch (iSpellTypes) { - case SpellType_Mez: - return BotCastMez(tar, botLevel, checked_los, botSpell, raid); - case SpellType_Heal: - return BotCastHeal(tar, botLevel, botClass, botSpell, raid); - case SpellType_Root: - return BotCastRoot(tar, botLevel, iSpellTypes, botSpell, checked_los); - case SpellType_Buff: - return BotCastBuff(tar, botLevel, botClass); - case SpellType_Escape: - return BotCastEscape(tar, botClass, botSpell, iSpellTypes); - case SpellType_Nuke: - return BotCastNuke(tar, botLevel, botClass, botSpell, checked_los); - case SpellType_Dispel: - return BotCastDispel(tar, botSpell, iSpellTypes, checked_los); - case SpellType_Pet: - return BotCastPet(tar, botClass, botSpell); - case SpellType_InCombatBuff: - return BotCastCombatBuff(tar, botLevel, botClass); - case SpellType_Lifetap: - return BotCastLifetap(tar, botLevel, botSpell, checked_los, iSpellTypes); - case SpellType_Snare: - return BotCastSnare(tar, botLevel, botSpell, checked_los, iSpellTypes); - case SpellType_DOT: - return BotCastDOT(tar, botLevel, botSpell, checked_los); - case SpellType_Slow: - return BotCastSlow(tar, botLevel, botClass, botSpell, checked_los, raid); - case SpellType_Debuff: - return BotCastDebuff(tar, botLevel, botSpell, checked_los); - case SpellType_Cure: - return BotCastCure(tar, botClass, botSpell, raid); - case SpellType_HateRedux: - return BotCastHateReduction(tar, botLevel, botSpell); - case SpellType_InCombatBuffSong: - return BotCastCombatSong(tar, botLevel); - case SpellType_OutOfCombatBuffSong: - return BotCastSong(tar, botLevel); - case SpellType_Resurrect: - case SpellType_PreCombatBuff: - case SpellType_PreCombatBuffSong: - default: + if ((spell_type != BotSpellTypes::Resurrect && spell_type != BotSpellTypes::SummonCorpse) && tar->GetAppearance() == eaDead) { + if (!((tar->IsClient() && tar->CastToClient()->GetFeigned()) || tar->IsBot())) { return false; + } } - return false; -} + uint8 bot_class = GetClass(); -bool Bot::BotCastSong(Mob* tar, uint8 botLevel) { - bool casted_spell = false; - if (GetClass() != Class::Bard || tar != this || IsEngaged()) // Out-of-Combat songs can not be cast in combat - return casted_spell; + SetCastedSpellType(UINT16_MAX); // this is for recast timers + SetTempSpellType(spell_type); // this is for spell checks - for (auto botSongList = GetPrioritizedBotSpellsBySpellType(this, SpellType_OutOfCombatBuffSong); - auto iter : botSongList) { - if (!iter.SpellId) - continue; - if (!CheckSpellRecastTimer(iter.SpellId)) - continue; - if (!IsSpellUsableInThisZoneType(iter.SpellId, zone->GetZoneType())) - continue; - switch (spells[iter.SpellId].target_type) { - case ST_AEBard: - case ST_AECaster: - case ST_GroupTeleport: - case ST_Group: - case ST_Self: - break; - default: - continue; - } - if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) - continue; + BotSpell bot_spell; + bot_spell.SpellId = 0; + bot_spell.SpellIndex = 0; + bot_spell.ManaCost = 0; - casted_spell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); - if (casted_spell) - break; + if (BotSpellTypeRequiresLoS(spell_type) && tar != this) { + SetHasLoS(DoLosChecks(tar)); + } + else { + SetHasLoS(true); } - return casted_spell; -} + switch (spell_type) { + case BotSpellTypes::Slow: + if (tar->GetSpecialAbility(SpecialAbility::SlowImmunity)) { + return false; + } -bool Bot::BotCastCombatSong(Mob* tar, uint8 botLevel) { - bool casted_spell = false; - if (tar != this) { // In-Combat songs can be cast Out-of-Combat in preparation for battle - return casted_spell; - } - for (auto botSongList = GetPrioritizedBotSpellsBySpellType(this, SpellType_InCombatBuffSong); - auto iter : botSongList) { - if (!iter.SpellId) - continue; - if (!CheckSpellRecastTimer(iter.SpellId)) - continue; - if (!IsSpellUsableInThisZoneType(iter.SpellId, zone->GetZoneType())) - continue; - switch (spells[iter.SpellId].target_type) { - case ST_AEBard: - case ST_AECaster: - case ST_GroupTeleport: - case ST_Group: - case ST_Self: break; - default: - continue; - } - if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) - continue; + case BotSpellTypes::Snare: + if (tar->GetSpecialAbility(SpecialAbility::SnareImmunity)) { + return false; + } - casted_spell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); - if (casted_spell) break; - } - - return casted_spell; -} - -bool Bot::BotCastHateReduction(Mob* tar, uint8 botLevel, const BotSpell& botSpell) { - bool casted_spell = false; - if (GetClass() == Class::Bard) { - std::list botSongList = GetPrioritizedBotSpellsBySpellType(this, SpellType_HateRedux); - for (auto iter : botSongList) { - if (!iter.SpellId) - continue; - if (!CheckSpellRecastTimer(iter.SpellId)) - continue; - if (!IsSpellUsableInThisZoneType(iter.SpellId, zone->GetZoneType())) - continue; - if (spells[iter.SpellId].target_type != ST_Target) - continue; - if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) - continue; - - if (IsValidSpellRange(iter.SpellId, tar)) { - casted_spell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); - } - if (casted_spell) { - BotGroupSay( - this, - fmt::format( - "Attempting to reduce hate on {} with {}.", - tar->GetCleanName(), - spells[iter.SpellId].name - ).c_str() - ); + case BotSpellTypes::AELull: + case BotSpellTypes::Lull: + if (tar->GetSpecialAbility(SpecialAbility::PacifyImmunity)) { + return false; } - } - } - return casted_spell; -} - -bool Bot::BotCastCure(Mob* tar, uint8 botClass, BotSpell& botSpell, Raid* raid) { - bool casted_spell = false; - if ( - GetNeedsCured(tar) && - (tar->DontCureMeBefore() < Timer::GetCurrentTime()) && - GetNumberNeedingHealedInGroup(25, false, raid) <= 0 && - GetNumberNeedingHealedInGroup(40, false, raid) <= 2 - ) { - botSpell = GetBestBotSpellForCure(this, tar); - - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - - uint32 TempDontCureMeBeforeTime = tar->DontCureMeBefore(); - - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontCureMeBeforeTime); - - if (casted_spell && botClass != Class::Bard) { - if (IsGroupSpell(botSpell.SpellId)) { - if (HasGroup()) { - Group const* group = GetGroup(); - if (group) { - for (auto member : group->members) { - if ( - member && - !member->qglobal && - TempDontCureMeBeforeTime != tar->DontCureMeBefore() - ) { - member->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000); - } - } - } - } else if (IsRaidGrouped()) { - uint32 r_group = raid->GetGroup(GetName()); - if (r_group) { - std::vector raid_group_members = raid->GetRaidGroupMembers(r_group); - for (auto& iter: raid_group_members) { - if ( - iter.member && - !iter.member->qglobal && - TempDontCureMeBeforeTime != tar->DontCureMeBefore() - ) { - iter.member->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000); - } - } - } - } - } else if (TempDontCureMeBeforeTime != tar->DontCureMeBefore()) { - tar->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000); + break; + case BotSpellTypes::Fear: + if (tar->GetSpecialAbility(SpecialAbility::FearImmunity)) { + return false; } - } - } - return casted_spell; -} - -bool Bot::BotCastDebuff(Mob* tar, uint8 botLevel, BotSpell& botSpell, bool checked_los) { - bool casted_spell = false; - if ((tar->GetHPRatio() <= 99.0f) && (tar->GetHPRatio() > 20.0f)) - { - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; - } - - botSpell = GetBestBotSpellForResistDebuff(this, tar); - - if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetDebuffBotSpell(this, tar); - } - - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - - if (! - ( - !tar->IsImmuneToSpell(botSpell.SpellId, this) && - (tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0) - ) - ) { - return casted_spell; - } - - if (IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); - } - } - return casted_spell; -} - -bool Bot::BotCastSlow(Mob* tar, uint8 botLevel, uint8 botClass, BotSpell& botSpell, const bool& checked_los, Raid* raid) { - bool casted_spell = false; - if (tar->GetHPRatio() <= 99.0f) { - - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; - } - - switch (botClass) { - case Class::Bard: { - // probably needs attackable check - for ( - auto botSongList = GetPrioritizedBotSpellsBySpellType(this, SpellType_Slow); - auto iter : botSongList - ) { - - if (!iter.SpellId) { - continue; - } - - if (!CheckSpellRecastTimer(iter.SpellId)) { - continue; - } - - if (!IsSpellUsableInThisZoneType(iter.SpellId, zone->GetZoneType())) { - continue; - } - - if (spells[iter.SpellId].target_type != ST_Target) { - continue; - } - - if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) { - continue; - } - - if (IsValidSpellRange(iter.SpellId, tar)) { - casted_spell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); - } - - if (casted_spell) { - return casted_spell; - } - } - break; - } - case Class::Enchanter: { - botSpell = GetBestBotSpellForMagicBasedSlow(this); - break; + if (!IsCommandedSpell() && (tar->IsRooted() || tar->GetSnaredAmount() == -1)) { + return false; } - case Class::Shaman: - case Class::Beastlord: { - botSpell = GetBestBotSpellForDiseaseBasedSlow(this); - if (botSpell.SpellId == 0 || ((tar->GetMR() - 50) < (tar->GetDR() + spells[botSpell.SpellId].resist_difficulty))) - botSpell = GetBestBotSpellForMagicBasedSlow(this); - break; + break; + case BotSpellTypes::Dispel: + if (tar->GetSpecialAbility(SpecialAbility::DispellImmunity)) { + return false; } - } - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - - if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) { - return casted_spell; - } - if (IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); - } - - if (casted_spell && GetClass() != Class::Bard) { - if (raid) { - const auto msg = fmt::format("Attempting to slow {}.", tar->GetCleanName()); - raid->RaidSay(msg.c_str(), GetCleanName(), Language::CommonTongue, Language::MaxValue); - } else { - BotGroupSay( - this, - fmt::format( - "Attempting to slow {} with {}.", - tar->GetCleanName(), - spells[botSpell.SpellId].name - ).c_str() - ); + if (!IsCommandedSpell() && tar->CountDispellableBuffs() <= 0) { + return false; } - } - } - return casted_spell; -} - -bool Bot::BotCastDOT(Mob* tar, uint8 botLevel, const BotSpell& botSpell, const bool& checked_los) { - bool casted_spell = false; - if ((tar->GetHPRatio() <= 98.0f) && (tar->DontDotMeBefore() < Timer::GetCurrentTime()) && (tar->GetHPRatio() > 15.0f)) { - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; - } - - if (GetClass() == Class::Bard) { - std::list dotList = GetPrioritizedBotSpellsBySpellType(this, SpellType_DOT); - - const int maxDotSelect = 5; - int dotSelectCounter = 0; - - for (const auto& s : dotList) { - - if (!IsValidSpell(s.SpellId)) { - continue; - } - - if (CheckSpellRecastTimer(s.SpellId)) - { - - if (!(!tar->IsImmuneToSpell(s.SpellId, this) && tar->CanBuffStack(s.SpellId, botLevel, true) >= 0)) { - continue; - } - - uint32 TempDontDotMeBefore = tar->DontDotMeBefore(); - - casted_spell = AIDoSpellCast(s.SpellIndex, tar, s.ManaCost, &TempDontDotMeBefore); - - if (TempDontDotMeBefore != tar->DontDotMeBefore()) { - tar->SetDontDotMeBefore(TempDontDotMeBefore); - } - } - - dotSelectCounter++; - - if ((dotSelectCounter == maxDotSelect) || casted_spell) { - break; - } + break; + case BotSpellTypes::HateRedux: + if (!IsCommandedSpell() && !GetNeedsHateRedux(tar)) { + return false; } - } - else { - std::list dotList = GetBotSpellsBySpellType(this, SpellType_DOT); - - const int maxDotSelect = 5; - int dotSelectCounter = 0; - - for (const auto& s : dotList) { - - if (!IsValidSpell(s.SpellId)) { - continue; - } - if (CheckSpellRecastTimer(s.SpellId)) { - - if (!(!tar->IsImmuneToSpell(s.SpellId, this) && - tar->CanBuffStack(s.SpellId, botLevel, true) >= 0)) { - continue; - } - - uint32 TempDontDotMeBefore = tar->DontDotMeBefore(); - - if (IsValidSpellRange(s.SpellId, tar)) { - casted_spell = AIDoSpellCast(s.SpellIndex, tar, s.ManaCost, &TempDontDotMeBefore); - } - - if (TempDontDotMeBefore != tar->DontDotMeBefore()) { - tar->SetDontDotMeBefore(TempDontDotMeBefore); - } - } - - dotSelectCounter++; - - if ((dotSelectCounter == maxDotSelect) || casted_spell) { - return casted_spell; - } + break; + case BotSpellTypes::InCombatBuff: + if (!IsCommandedSpell() && GetClass() != Class::Shaman && spell_type == BotSpellTypes::InCombatBuff && IsCasterClass(GetClass()) && GetLevel() >= GetStopMeleeLevel()) { + return false; } - } - } - return casted_spell; -} - -bool Bot::BotCastSnare(Mob* tar, uint8 botLevel, BotSpell& botSpell, const bool& checked_los, uint32 iSpellTypes) { - bool casted_spell = false; - if (tar->DontSnareMeBefore() < Timer::GetCurrentTime()) { - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; - } - - - botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); - - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - - if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) { - return casted_spell; - } - - uint32 TempDontSnareMeBefore = tar->DontSnareMeBefore(); - - if (IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontSnareMeBefore); - } - - if (TempDontSnareMeBefore != tar->DontSnareMeBefore()) { - tar->SetDontSnareMeBefore(TempDontSnareMeBefore); - } - } - return casted_spell; -} - -bool Bot::BotCastLifetap(Mob* tar, uint8 botLevel, BotSpell& botSpell, const bool& checked_los, uint32 iSpellTypes) { - bool casted_spell = false; - if (GetHPRatio() < 90.0f) { - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; - } - - - botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); - - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - - if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && (tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0))) { - return casted_spell; - } - - if (IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); - } - } - return casted_spell; -} - -bool Bot::BotCastCombatBuff(Mob* tar, uint8 botLevel, uint8 botClass) { - - bool casted_spell = false; - if (tar->DontBuffMeBefore() < Timer::GetCurrentTime()) { - std::list buffSpellList = GetBotSpellsBySpellType(this, SpellType_InCombatBuff); - - for (const auto& s : buffSpellList) { - if (!IsValidSpell(s.SpellId)) { - continue; - } - // no buffs with illusions.. use #bot command to cast illusions - if (IsEffectInSpell(s.SpellId, SE_Illusion) && tar != this) { - continue; - } - //no teleport spells use #bot command to cast teleports - if (IsEffectInSpell(s.SpellId, SE_Teleport) || IsEffectInSpell(s.SpellId, SE_Succor)) { - continue; - } - // can not cast buffs for your own pet only on another pet that isn't yours - if ((spells[s.SpellId].target_type == ST_Pet) && (tar != GetPet())) { - continue; + break; + case BotSpellTypes::HateLine: + if (!tar->IsNPC()) { + return false; } - //Conversion Spells - if ( - IsSelfConversionSpell(s.SpellId) && - ( - GetManaRatio() > 90.0f || - GetHPRatio() < 50.0f || - GetHPRatio() < (GetManaRatio() + 10.0f) - ) - ) { - break; //don't cast if low hp, lots of mana, or if mana is higher than hps + break; + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::Buff: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::DamageShields: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::Teleport: + case BotSpellTypes::Succor: + case BotSpellTypes::BindAffinity: + case BotSpellTypes::Identify: + case BotSpellTypes::Levitate: + case BotSpellTypes::Rune: + case BotSpellTypes::WaterBreathing: + case BotSpellTypes::Size: + case BotSpellTypes::Invisibility: + case BotSpellTypes::MovementSpeed: + case BotSpellTypes::SendHome: + if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { + return false; } - // Validate target - // TODO: Add ST_TargetsTarget support for Buffing. - if ( - !( - ( - spells[s.SpellId].target_type == ST_Target || - spells[s.SpellId].target_type == ST_Pet || - (tar == this && spells[s.SpellId].target_type != ST_TargetsTarget) || - spells[s.SpellId].target_type == ST_Group || - spells[s.SpellId].target_type == ST_GroupTeleport || - (botClass == Class::Bard && spells[s.SpellId].target_type == ST_AEBard) - ) && - !tar->IsImmuneToSpell(s.SpellId, this) && - tar->CanBuffStack(s.SpellId, botLevel, true) >= 0 - ) - ) { - continue; + break; + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + if (!IsCommandedSpell() && (IsEngaged() || tar->IsEngaged())) { // Out-of-Combat songs can not be cast in combat + return false; } - // Put the zone levitate and movement check here since bots are able to bypass the client casting check - if ( - ((IsEffectInSpell(s.SpellId, SE_Levitate) && !zone->CanLevitate()) || - (IsEffectInSpell(s.SpellId, SE_MovementSpeed) && !zone->CanCastOutdoor())) && - (botClass != Class::Bard || !IsSpellUsableInThisZoneType(s.SpellId, zone->GetZoneType())) - ) { - continue; + break; + case BotSpellTypes::AEMez: + case BotSpellTypes::Mez: + return BotCastMez(tar, bot_class, bot_spell, spell_type); + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Nuke: + case BotSpellTypes::AEStun: + case BotSpellTypes::Stun: + return BotCastNuke(tar, bot_class, bot_spell, spell_type); + case BotSpellTypes::RegularHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { + return false; } - if (!IsGroupSpell(s.SpellId)) { - //Only check archetype if spell is not a group spell - //Hybrids get all buffs - switch (tar->GetArchetype()) { - case Archetype::Caster: - //TODO: probably more caster specific spell effects in here - if ( - ( - IsEffectInSpell(s.SpellId, SE_AttackSpeed) || - IsEffectInSpell(s.SpellId, SE_ATK) || - IsEffectInSpell(s.SpellId, SE_STR) || - IsEffectInSpell(s.SpellId, SE_ReverseDS) || - IsEffectInSpell(s.SpellId, SE_DamageShield) - ) && - spells[s.SpellId].target_type != ST_Self - ) { - continue; - } - break; - case Archetype::Melee: - if ( - ( - IsEffectInSpell(s.SpellId, SE_IncreaseSpellHaste) || - IsEffectInSpell(s.SpellId, SE_ManaPool) || - IsEffectInSpell(s.SpellId, SE_CastingLevel) || - IsEffectInSpell(s.SpellId, SE_ManaRegen_v2) || - IsEffectInSpell(s.SpellId, SE_CurrentMana) - ) && - spells[s.SpellId].target_type != ST_Self - ) { - continue; - } - break; - default: - break; - } + return BotCastHeal(tar, bot_class, bot_spell, spell_type); + case BotSpellTypes::GroupCures: + case BotSpellTypes::Cure: + case BotSpellTypes::PetCures: + if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { + return false; } - // TODO: Add TriggerSpell Support for Exchanter Runes - if (botClass == Class::Enchanter && IsEffectInSpell(s.SpellId, SE_Rune)) { - float manaRatioToCast = 75.0f; - switch(GetBotStance()) { - case Stance::Efficient: - manaRatioToCast = 90.0f; - break; - case Stance::Balanced: - case Stance::Aggressive: - manaRatioToCast = 75.0f; - break; - case Stance::Reactive: - case Stance::Burn: - case Stance::AEBurn: - manaRatioToCast = 50.0f; - break; - default: - manaRatioToCast = 75.0f; - break; - } - - //If we're at specified mana % or below, don't rune as enchanter - if (GetManaRatio() <= manaRatioToCast) { - break; - } + return BotCastCure(tar, bot_class, bot_spell, spell_type); + case BotSpellTypes::Pet: + if (HasPet() || IsBotCharmer()) { + return false; } - if (CheckSpellRecastTimer(s.SpellId)) { - uint32 TempDontBuffMeBefore = tar->DontBuffMeBefore(); - casted_spell = AIDoSpellCast(s.SpellIndex, tar, s.ManaCost, &TempDontBuffMeBefore); - if (TempDontBuffMeBefore != tar->DontBuffMeBefore()) { - tar->SetDontBuffMeBefore(TempDontBuffMeBefore); - } + return BotCastPet(tar, bot_class, bot_spell, spell_type); + case BotSpellTypes::Resurrect: + if (!tar->IsCorpse() || !tar->CastToCorpse()->IsPlayerCorpse()) { + return false; } - if (casted_spell) { - return casted_spell; + break; + case BotSpellTypes::Charm: + if (HasPet() || tar->IsCharmed() || !tar->IsNPC() || tar->GetSpecialAbility(SpecialAbility::CharmImmunity)) { + return false; } - } + + break; + default: + break; } - return casted_spell; -} -bool Bot::BotCastPet(Mob* tar, uint8 botClass, BotSpell& botSpell) { - bool casted_spell = false; - if (!IsPet() && !GetPetID() && !IsBotCharmer()) { - if (botClass == Class::Wizard) { - auto buffs_max = GetMaxBuffSlots(); - auto my_buffs = GetBuffs(); - int familiar_buff_slot = -1; - if (buffs_max && my_buffs) { - for (int index = 0; index < buffs_max; ++index) { - if (IsEffectInSpell(my_buffs[index].spellid, SE_Familiar)) { - MakePet(my_buffs[index].spellid, spells[my_buffs[index].spellid].teleport_zone); - familiar_buff_slot = index; - break; - } - } - } - if (GetPetID()) { - return casted_spell; - } - if (familiar_buff_slot >= 0) { - BuffFadeBySlot(familiar_buff_slot); - return casted_spell; - } + std::vector bot_spell_list = GetPrioritizedBotSpellsBySpellType(this, spell_type, tar, (IsAEBotSpellType(spell_type) || sub_target_type == CommandedSubTypes::AETarget), sub_target_type, sub_type); - botSpell = GetFirstBotSpellBySpellType(this, SpellType_Pet); - } - else if (botClass == Class::Magician) { - botSpell = GetBestBotMagicianPetSpell(this); - } - else { - botSpell = GetFirstBotSpellBySpellType(this, SpellType_Pet); - } + for (const auto& s : bot_spell_list) { - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; + if (!IsValidSpellAndLoS(s.SpellId, HasLoS())) { + continue; } - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); - } - return casted_spell; -} - -bool Bot::BotCastDispel(Mob* tar, BotSpell& botSpell, uint32 iSpellTypes, const bool& checked_los) { + if (IsInvulnerabilitySpell(s.SpellId)) { + tar = this; //target self for invul type spells + } - bool casted_spell = false; - if (tar->GetHPRatio() > 95.0f) { - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; + if (IsCommandedSpell() && IsCasting()) { + RaidGroupSay( + this, + fmt::format( + "Interrupting {}. I have been commanded to try to cast a [{}] spell, {} on {}.", + CastingSpellID() ? spells[CastingSpellID()].name : "my spell", + GetSpellTypeNameByID(spell_type), + spells[s.SpellId].name, + tar->GetCleanName() + ).c_str() + ); + + InterruptSpell(); } - botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); + if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { + if (IsBotSpellTypeOtherBeneficial(spell_type)) { + SetCastedSpellType(UINT16_MAX); - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - // TODO: Check target to see if there is anything to dispel + if (!IsCommandedSpell()) { + SetBotSpellRecastTimer(spell_type, tar, true); + } + } + else { + SetCastedSpellType(spell_type); + } - if (tar->CountDispellableBuffs() > 0 && IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + RaidGroupSay( + this, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(s.SpellId), + GetSpellTypeNameByID(spell_type), + (tar == this ? "myself" : tar->GetCleanName()) + ).c_str() + ); + + return true; } } - return casted_spell; + + return false; } -bool Bot::BotCastNuke(Mob* tar, uint8 botLevel, uint8 botClass, BotSpell& botSpell, const bool& checked_los) { +bool Bot::BotCastMez(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { + std::vector bot_spell_list = GetPrioritizedBotSpellsBySpellType(this, spell_type, tar, IsAEBotSpellType(spell_type)); - bool casted_spell = false; - if ((tar->GetHPRatio() <= 95.0f) || ((botClass == Class::Bard) || (botClass == Class::Shaman) || (botClass == Class::Enchanter) || (botClass == Class::Paladin) || (botClass == Class::ShadowKnight) || (botClass == Class::Warrior))) - { - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; + for (const auto& s : bot_spell_list) { + if (!IsValidSpellAndLoS(s.SpellId, HasLoS())) { + continue; } - if (botClass == Class::Cleric || botClass == Class::Enchanter) - { - float manaRatioToCast = 75.0f; + if (!IsCommandedSpell()) { + Mob* add_mob = GetFirstIncomingMobToMez(this, s.SpellId, spell_type, IsAEBotSpellType(spell_type)); - switch(GetBotStance()) { - case Stance::Efficient: - manaRatioToCast = 90.0f; - break; - case Stance::Balanced: - manaRatioToCast = 75.0f; - break; - case Stance::Reactive: - case Stance::Aggressive: - manaRatioToCast = 50.0f; - break; - case Stance::Burn: - case Stance::AEBurn: - manaRatioToCast = 25.0f; - break; - default: - manaRatioToCast = 50.0f; - break; + if (!add_mob) { + return false; } - //If we're at specified mana % or below, don't nuke as cleric or enchanter - if (GetManaRatio() <= manaRatioToCast) - return casted_spell; + tar = add_mob; } - if (botClass == Class::Magician || botClass == Class::ShadowKnight || botClass == Class::Necromancer || botClass == Class::Paladin || botClass == Class::Ranger || botClass == Class::Druid || botClass == Class::Cleric) { - if (tar->GetBodyType() == BodyType::Undead || tar->GetBodyType() == BodyType::SummonedUndead || tar->GetBodyType() == BodyType::Vampire) - botSpell = GetBestBotSpellForNukeByTargetType(this, ST_Undead); - else if (tar->GetBodyType() == BodyType::Summoned || tar->GetBodyType() == BodyType::Summoned2 || tar->GetBodyType() == BodyType::Summoned3) - botSpell = GetBestBotSpellForNukeByTargetType(this, ST_Summoned); - } - - if ((botClass == Class::Paladin || botClass == Class::Druid || botClass == Class::Cleric || botClass == Class::Enchanter || botClass == Class::Wizard) && !IsValidSpell(botSpell.SpellId)) { - uint8 stunChance = (tar->IsCasting() ? 30: 15); - - if (botClass == Class::Paladin) { - stunChance = 50; + if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { + if (IsBotSpellTypeOtherBeneficial(spell_type)) { + SetCastedSpellType(UINT16_MAX); + + if (!IsCommandedSpell()) { + SetBotSpellRecastTimer(spell_type, tar, true); + } } - - if (!tar->GetSpecialAbility(SpecialAbility::StunImmunity) && !tar->IsStunned() && (zone->random.Int(1, 100) <= stunChance)) { - botSpell = GetBestBotSpellForStunByTargetType(this, ST_Target); + else { + SetCastedSpellType(spell_type); } - } - - if (botClass == Class::Wizard && botSpell.SpellId == 0) { - botSpell = GetBestBotWizardNukeSpellByTargetResists(this, tar); - } - if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForNukeByTargetType(this, ST_Target); - } - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && (tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0))) { - return casted_spell; - } - if (IsFearSpell(botSpell.SpellId) && tar->GetSnaredAmount() == -1 && !tar->IsRooted()) { - return casted_spell; - } + RaidGroupSay( + this, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(s.SpellId), + GetSpellTypeNameByID(spell_type), + (tar == this ? "myself" : tar->GetCleanName()) + ).c_str() + ); - if (IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + return true; } } - return casted_spell; -} - -bool Bot::BotCastEscape(Mob*& tar, uint8 botClass, BotSpell& botSpell, uint32 iSpellTypes) { - - bool casted_spell = false; - auto hpr = (uint8) GetHPRatio(); - bool mayGetAggro = false; - - if (hpr > 15 && ((botClass == Class::Wizard) || (botClass == Class::Enchanter) || (botClass == Class::Ranger))) { - mayGetAggro = HasOrMayGetAggro(); - } - - if (hpr <= 15 || mayGetAggro) - { - botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } + return false; +} - if (IsInvulnerabilitySpell(botSpell.SpellId)) { - tar = this; //target self for invul type spells - } +bool Bot::BotCastCure(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { + uint32 current_time = Timer::GetCurrentTime(); + uint32 next_cure_time = tar->DontCureMeBefore(); - if (IsValidSpellRange(botSpell.SpellId, tar) || botClass == Class::Bard) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + if (!IsCommandedSpell()) { + if ((next_cure_time > current_time) || !GetNeedsCured(tar)) { + return false; } } - return casted_spell; -} -bool Bot::BotCastBuff(Mob* tar, uint8 botLevel, uint8 botClass) { - bool casted_spell = false; - if (tar->DontBuffMeBefore() < Timer::GetCurrentTime()) { - std::list buffSpellList = GetBotSpellsBySpellType(this, SpellType_Buff); + bot_spell = GetBestBotSpellForCure(this, tar, spell_type); - for(const auto& s : buffSpellList) { + if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS())) { + return false; + } - if (!IsValidSpell(s.SpellId)) { - continue; + if (AIDoSpellCast(bot_spell.SpellIndex, tar, bot_spell.ManaCost)) { + if (IsGroupSpell(bot_spell.SpellId)) { + if (!IsCommandedSpell()) { + for (Mob* m : GatherSpellTargets(false, tar)) { + SetBotSpellRecastTimer(spell_type, m, true); + } } - // no buffs with illusions.. use #bot command to cast illusions - if (IsEffectInSpell(s.SpellId, SE_Illusion) && tar != this) { - continue; + RaidGroupSay( + this, + fmt::format( + "Curing the group with {}.", + GetSpellName(bot_spell.SpellId) + ).c_str() + ); + } + else { + if (!IsCommandedSpell()) { + SetBotSpellRecastTimer(spell_type, tar, true); } - //no teleport spells use #bot command to cast teleports - if (IsEffectInSpell(s.SpellId, SE_Teleport) || IsEffectInSpell(s.SpellId, SE_Succor)) { - continue; - } - // can not cast buffs for your own pet only on another pet that isn't yours - if ((spells[s.SpellId].target_type == ST_Pet) && (tar != GetPet())) { - continue; - } + RaidGroupSay( + this, + fmt::format( + "Curing {} with {}.", + (tar == this ? "myself" : tar->GetCleanName()), + GetSpellName(bot_spell.SpellId) + ).c_str() + ); + } - // Validate target - // TODO: Add ST_TargetsTarget support for Buffing. - if ( - !( - ( - spells[s.SpellId].target_type == ST_Target || - spells[s.SpellId].target_type == ST_Pet || - (tar == this && spells[s.SpellId].target_type != ST_TargetsTarget) || - spells[s.SpellId].target_type == ST_Group || - spells[s.SpellId].target_type == ST_GroupTeleport || - (botClass == Class::Bard && spells[s.SpellId].target_type == ST_AEBard) - ) && - !tar->IsImmuneToSpell(s.SpellId, this) && - tar->CanBuffStack(s.SpellId, botLevel, true) >= 0 - ) - ) { - continue; - } + return true; + } - // Put the zone levitate and movement check here since bots are able to bypass the client casting check - if ( - ( - (IsEffectInSpell(s.SpellId, SE_Levitate) && !zone->CanLevitate()) || - (IsEffectInSpell(s.SpellId, SE_MovementSpeed) && !zone->CanCastOutdoor()) - ) && - (botClass != Class::Bard || !IsSpellUsableInThisZoneType(s.SpellId, zone->GetZoneType())) - ) { - continue; - } + return false; +} - switch (tar->GetArchetype()) - { - case Archetype::Caster: - //TODO: probably more caster specific spell effects in here - if (IsEffectInSpell(s.SpellId, SE_AttackSpeed) || IsEffectInSpell(s.SpellId, SE_ATK) || - IsEffectInSpell(s.SpellId, SE_STR) || IsEffectInSpell(s.SpellId, SE_ReverseDS)) - { - continue; - } +bool Bot::BotCastPet(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { + if (bot_class == Class::Wizard) { + auto buffs_max = GetMaxBuffSlots(); + auto my_buffs = GetBuffs(); + int familiar_buff_slot = -1; + if (buffs_max && my_buffs) { + for (int index = 0; index < buffs_max; ++index) { + if (IsEffectInSpell(my_buffs[index].spellid, SE_Familiar)) { + MakePet(my_buffs[index].spellid, spells[my_buffs[index].spellid].teleport_zone); + familiar_buff_slot = index; break; - case Archetype::Melee: - if (IsEffectInSpell(s.SpellId, SE_IncreaseSpellHaste) || IsEffectInSpell(s.SpellId, SE_ManaPool) || - IsEffectInSpell(s.SpellId, SE_CastingLevel) || IsEffectInSpell(s.SpellId, SE_ManaRegen_v2) || - IsEffectInSpell(s.SpellId, SE_CurrentMana)) - { - continue; - } - break; - default: //Hybrids get all buffs - break; - } - - if (botClass == Class::Enchanter && IsEffectInSpell(s.SpellId, SE_Rune)) - { - float manaRatioToCast = 75.0f; - - switch (GetBotStance()) { - case Stance::Efficient: - manaRatioToCast = 90.0f; - break; - case Stance::Balanced: - case Stance::Aggressive: - manaRatioToCast = 75.0f; - break; - case Stance::Reactive: - case Stance::Burn: - case Stance::AEBurn: - manaRatioToCast = 50.0f; - break; - default: - manaRatioToCast = 75.0f; - break; - } - - //If we're at specified mana % or below, don't rune as enchanter - if (GetManaRatio() <= manaRatioToCast) { - return casted_spell; } } - - if (CheckSpellRecastTimer(s.SpellId)) - { - uint32 TempDontBuffMeBefore = tar->DontBuffMeBefore(); - - casted_spell = AIDoSpellCast(s.SpellIndex, tar, s.ManaCost, &TempDontBuffMeBefore); - - if (TempDontBuffMeBefore != tar->DontBuffMeBefore()) { - tar->SetDontBuffMeBefore(TempDontBuffMeBefore); - } - } - } - } - return casted_spell; -} - -bool Bot::BotCastRoot(Mob* tar, uint8 botLevel, uint32 iSpellTypes, BotSpell& botSpell, const bool& checked_los) { - bool casted_spell = false; - if (!tar->IsRooted() && tar->DontRootMeBefore() < Timer::GetCurrentTime()) { - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; - } - - // TODO: If there is a ranger in the group then don't allow root spells - - botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); - - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; } - if (tar->CanBuffStack(botSpell.SpellId, botLevel, true) == 0) { - return casted_spell; + if (GetPetID()) { + return false; } - uint32 TempDontRootMeBefore = tar->DontRootMeBefore(); - - if (IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontRootMeBefore); + if (familiar_buff_slot >= 0) { + BuffFadeBySlot(familiar_buff_slot); + return false; } - if (TempDontRootMeBefore != tar->DontRootMeBefore()) { - tar->SetDontRootMeBefore(TempDontRootMeBefore); - } + bot_spell = GetFirstBotSpellBySpellType(this, spell_type); + } + else if (bot_class == Class::Magician) { + bot_spell = GetBestBotMagicianPetSpell(this, spell_type); + } + else { + bot_spell = GetFirstBotSpellBySpellType(this, spell_type); } - return casted_spell; -} - -bool Bot::BotCastHeal(Mob* tar, uint8 botLevel, uint8 botClass, BotSpell& botSpell, Raid* raid) { - bool casted_spell = false; - if (tar->DontHealMeBefore() < Timer::GetCurrentTime()) { - auto hpr = (uint8)tar->GetHPRatio(); - bool hasAggro = false; - bool isPrimaryHealer = false; - - if (HasGroup() || IsRaidGrouped()) { - isPrimaryHealer = IsGroupHealer(); - } - - if (hpr < 95 || tar->IsClient() || botClass == Class::Bard) { - if (tar->GetClass() == Class::Necromancer && hpr >= 40) { - return false; - } - - if (tar->GetClass() == Class::Shaman && hpr >= 80) { - return false; - } - - // Evaluate the situation - if ((IsEngaged()) && ((botClass == Class::Cleric) || (botClass == Class::Druid) || (botClass == Class::Shaman) || (botClass == Class::Paladin))) { - if (tar->GetTarget() && tar->GetTarget()->GetHateTop() && tar->GetTarget()->GetHateTop() == tar) { - hasAggro = true; - } - - if (hpr < 35) { - botSpell = GetBestBotSpellForFastHeal(this); - } - else if (hpr < 70) { - if (GetNumberNeedingHealedInGroup(60, false, raid) >= 3) { - botSpell = GetBestBotSpellForGroupHeal(this); - } - - if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForPercentageHeal(this); - } - } - else if (hpr < 95) { - if (GetNumberNeedingHealedInGroup(80, false, raid) >= 3) { - botSpell = GetBestBotSpellForGroupHealOverTime(this); - } - - if (hasAggro) { - botSpell = GetBestBotSpellForPercentageHeal(this); - } - } - else { - if (!tar->FindType(SE_HealOverTime)) { - botSpell = GetBestBotSpellForHealOverTime(this); - } - } - } - else if ((botClass == Class::Cleric) || (botClass == Class::Druid) || (botClass == Class::Shaman) || (botClass == Class::Paladin)) { - if (GetNumberNeedingHealedInGroup(40, true, raid) >= 2) { - botSpell = GetBestBotSpellForGroupCompleteHeal(this); - - if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForGroupHeal(this); - } - - if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForGroupHealOverTime(this); - } - - if (hpr < 40 && !IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForPercentageHeal(this); - } - } - else if (GetNumberNeedingHealedInGroup(60, true, raid) >= 2) { - botSpell = GetBestBotSpellForGroupHeal(this); - - if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForGroupHealOverTime(this); - } - - if (hpr < 40 && !IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForPercentageHeal(this); - } - } - else if (hpr < 40) { - botSpell = GetBestBotSpellForPercentageHeal(this); - } - else if (hpr < 75) { - botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); - } - else { - if (hpr < 90 && !tar->FindType(SE_HealOverTime)) { - botSpell = GetBestBotSpellForHealOverTime(this); - } - } - } - else { - float hpRatioToCast = 0.0f; - - switch (GetBotStance()) { - case Stance::Efficient: - case Stance::Aggressive: - hpRatioToCast = isPrimaryHealer ? 90.0f : 50.0f; - break; - case Stance::Balanced: - hpRatioToCast = isPrimaryHealer ? 95.0f : 75.0f; - break; - case Stance::Reactive: - hpRatioToCast = isPrimaryHealer ? 100.0f : 90.0f; - break; - case Stance::Burn: - case Stance::AEBurn: - hpRatioToCast = isPrimaryHealer ? 75.0f : 25.0f; - break; - default: - hpRatioToCast = isPrimaryHealer ? 100.0f : 0.0f; - break; - } - //If we're at specified mana % or below, don't heal as hybrid - if (tar->GetHPRatio() <= hpRatioToCast) { - botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); - } - } + if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS())) { + return false; + } - if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); - } - if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetFirstBotSpellForSingleTargetHeal(this); - } - if (botSpell.SpellId == 0 && botClass == Class::Bard) { - botSpell = GetFirstBotSpellBySpellType(this, SpellType_Heal); - } + if (AIDoSpellCast(bot_spell.SpellIndex, tar, bot_spell.ManaCost)) { + SetCastedSpellType(spell_type); - if (!IsValidSpell(botSpell.SpellId)) { - return false; - } - // Can we cast this spell on this target? - if (!(spells[botSpell.SpellId].target_type==ST_GroupTeleport || spells[botSpell.SpellId].target_type == ST_Target || tar == this) - && tar->CanBuffStack(botSpell.SpellId, botLevel, true) < 0) { - return false; - } + RaidGroupSay( + this, + fmt::format( + "Summoning a pet [{}].", + GetSpellName(bot_spell.SpellId) + ).c_str() + ); - uint32 TempDontHealMeBeforeTime = tar->DontHealMeBefore(); + return true; + } - if (IsValidSpellRange(botSpell.SpellId, tar) || botClass == Class::Bard) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontHealMeBeforeTime); - } + return false; +} - if (casted_spell && botClass != Class::Bard) { - if (IsGroupSpell(botSpell.SpellId)) { - if (HasGroup()) { - Group *group = GetGroup(); - if (group) { - BotGroupSay( - this, - fmt::format( - "Casting {}.", - spells[botSpell.SpellId].name - ).c_str() - ); +bool Bot::BotCastNuke(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { + if (spell_type == BotSpellTypes::Stun || spell_type == BotSpellTypes::AEStun) { + uint8 stun_chance = (tar->IsCasting() ? RuleI(Bots, StunCastChanceIfCasting) : RuleI(Bots, StunCastChanceNormal)); - for (const auto& member : group->members) { - if (member && !member->qglobal) { - member->SetDontHealMeBefore(Timer::GetCurrentTime() + 1000); - } - } - } else if (IsRaidGrouped()) { - uint32 r_group = raid->GetGroup(GetName()); - const auto msg = fmt::format("Casting {}.", spells[botSpell.SpellId].name); - raid->RaidGroupSay(msg.c_str(), GetCleanName(), Language::CommonTongue, Language::MaxValue); - std::vector raid_group_members = raid->GetRaidGroupMembers(r_group); - for (const auto& rgm : raid_group_members) { - if (rgm.member && !rgm.member->qglobal) { - rgm.member->SetDontHealMeBefore(Timer::GetCurrentTime() + 1000); - } - } - } - } else { - if (tar != this) { //we don't need spam of bots healing themselves - BotGroupSay( - this, - fmt::format( - "Casting {} on {}.", - spells[botSpell.SpellId].name, - tar->GetCleanName() - ).c_str() - ); - } - } - } - tar->SetDontHealMeBefore(Timer::GetCurrentTime() + 2000); - } + if (bot_class == Class::Paladin) { + stun_chance = RuleI(Bots, StunCastChancePaladins); + } + + if ( + !tar->GetSpecialAbility(SpecialAbility::StunImmunity) && + ( + IsCommandedSpell() || + (!tar->IsStunned() && (zone->random.Int(1, 100) <= stun_chance)) + ) + ) { + bot_spell = GetBestBotSpellForStunByTargetType(this, ST_TargetOptional, spell_type, IsAEBotSpellType(spell_type), tar); + } + + if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS())) { + return false; } } - return casted_spell; -} -bool Bot::BotCastMez(Mob* tar, uint8 botLevel, bool checked_los, BotSpell& botSpell, Raid* raid) { - bool casted_spell = false; - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return false; + if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS())) { + bot_spell = GetBestBotSpellForNukeByBodyType(this, tar->GetBodyType(), spell_type, IsAEBotSpellType(spell_type), tar); } - //TODO - //Check if single target or AoE mez is best - //if (TARGETS ON MT IS => 3 THEN botSpell = AoEMez) - //if (TARGETS ON MT IS <= 2 THEN botSpell = BestMez) + if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS()) && spell_type == BotSpellTypes::Nuke && bot_class == Class::Wizard) { + bot_spell = GetBestBotWizardNukeSpellByTargetResists(this, tar, spell_type); + } - botSpell = GetBestBotSpellForMez(this); + if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS())) { + std::vector bot_spell_list = GetPrioritizedBotSpellsBySpellType(this, spell_type, tar, IsAEBotSpellType(spell_type)); - if (!IsValidSpell(botSpell.SpellId)) { - return false; + for (const auto& s : bot_spell_list) { + if (!IsValidSpellAndLoS(s.SpellId, HasLoS())) { + continue; + } + + if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { + SetCastedSpellType(spell_type); + + RaidGroupSay( + this, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(s.SpellId), + GetSpellTypeNameByID(spell_type), + tar->GetCleanName() + ).c_str() + ); + + return true; + } + } } + else { + if (AIDoSpellCast(bot_spell.SpellIndex, tar, bot_spell.ManaCost)) { + SetCastedSpellType(spell_type); - Mob* addMob = GetFirstIncomingMobToMez(this, botSpell); + RaidGroupSay( + this, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(bot_spell.SpellId), + GetSpellTypeNameByID(spell_type), + tar->GetCleanName() + ).c_str() + ); - if (!addMob) { - return false; + return true; + } } - if (!(!addMob->IsImmuneToSpell(botSpell.SpellId, this) && addMob->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) { + return false; +} + +bool Bot::BotCastHeal(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { + bot_spell = GetSpellByHealType(spell_type, tar); + + if (!IsValidSpell(bot_spell.SpellId)) { return false; } - if (IsValidSpellRange(botSpell.SpellId, addMob)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, addMob, botSpell.ManaCost); - } - if (casted_spell) { - if (raid) { - raid->RaidSay( - GetCleanName(), + if (AIDoSpellCast(bot_spell.SpellIndex, tar, bot_spell.ManaCost)) { + if (IsGroupSpell(bot_spell.SpellId)) { + if (bot_class != Class::Bard) { + if (!IsCommandedSpell()) { + for (Mob* m : GatherSpellTargets(false, tar)) { + SetBotSpellRecastTimer(spell_type, m, true); + } + } + } + + RaidGroupSay( + this, fmt::format( - "Attempting to mesmerize {} with {}.", - addMob->GetCleanName(), - spells[botSpell.SpellId].name - ).c_str(), - 0, - 100 + "Healing the group with {} [{}].", + GetSpellName(bot_spell.SpellId), + GetSpellTypeNameByID(spell_type) + ).c_str() ); - } else { - BotGroupSay( + + } + else { + if (bot_class != Class::Bard) { + if (!IsCommandedSpell()) { + SetBotSpellRecastTimer(spell_type, tar, true); + } + } + + RaidGroupSay( this, fmt::format( - "Attempting to mesmerize {} with {}.", - addMob->GetCleanName(), - spells[botSpell.SpellId].name + "Healing {} with {} [{}].", + (tar == this ? "myself" : tar->GetCleanName()), + GetSpellName(bot_spell.SpellId), + GetSpellTypeNameByID(spell_type) ).c_str() ); } + + return true; } - return casted_spell; + + return false; } bool Bot::AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore) { @@ -1260,39 +568,19 @@ bool Bot::AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgain // Allow bots to cast buff spells even if they are out of mana if ( RuleB(Bots, FinishBuffing) && - manaCost > hasMana && AIBot_spells[i].type & SpellType_Buff + manaCost > hasMana && AIBot_spells[i].type == BotSpellTypes::Buff ) { SetMana(manaCost); } float dist2 = 0; - if (AIBot_spells[i].type & SpellType_Escape) { + if (AIBot_spells[i].type == BotSpellTypes::Escape) { dist2 = 0; } else dist2 = DistanceSquared(m_Position, tar->GetPosition()); - if ( - ( - ( - ( - (spells[AIBot_spells[i].spellid].target_type==ST_GroupTeleport && AIBot_spells[i].type == SpellType_Heal) || - spells[AIBot_spells[i].spellid].target_type ==ST_AECaster || - spells[AIBot_spells[i].spellid].target_type ==ST_Group || - spells[AIBot_spells[i].spellid].target_type ==ST_AEBard || - ( - tar == this && spells[AIBot_spells[i].spellid].target_type != ST_TargetsTarget - ) - ) && - dist2 <= spells[AIBot_spells[i].spellid].aoe_range*spells[AIBot_spells[i].spellid].aoe_range - ) || - dist2 <= GetActSpellRange(AIBot_spells[i].spellid, spells[AIBot_spells[i].spellid].range)*GetActSpellRange(AIBot_spells[i].spellid, spells[AIBot_spells[i].spellid].range) - ) && - ( - mana_cost <= GetMana() || - IsBotNonSpellFighter() - ) - ) { + if (IsValidSpellRange(AIBot_spells[i].spellid, tar) && (mana_cost <= GetMana() || IsBotNonSpellFighter())) { casting_spell_AIindex = i; LogAI("spellid [{}] tar [{}] mana [{}] Name [{}]", AIBot_spells[i].spellid, tar->GetName(), mana_cost, spells[AIBot_spells[i].spellid].name); result = Mob::CastSpell(AIBot_spells[i].spellid, tar->GetID(), EQ::spells::CastingSlot::Gem2, spells[AIBot_spells[i].spellid].cast_time, AIBot_spells[i].manacost == -2 ? 0 : mana_cost, oDontDoAgainBefore, -1, -1, 0, &(AIBot_spells[i].resist_adjust)); @@ -1305,32 +593,69 @@ bool Bot::AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgain if (!result) { SetMana(hasMana); } - else { - if (CalcSpellRecastTimer(AIBot_spells[i].spellid) > 0) { - SetSpellRecastTimer(AIBot_spells[i].spellid); - } - } return result; } bool Bot::AI_PursueCastCheck() { + if (GetAppearance() == eaDead || delaytimer || spellend_timer.Enabled() || IsFeared() || IsSilenced() || IsAmnesiad() || GetHP() < 0) { + return false; + } + bool result = false; - if (AIautocastspell_timer->Check(false)) { + if (GetTarget() && AIautocastspell_timer->Check(false)) { + + LogAIDetail("Bot Pursue autocast check triggered: [{}]", GetCleanName()); + LogBotSpellChecksDetail("{} says, 'AI_PursueCastCheck started.'", GetCleanName()); AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. - LogAIDetail("Bot Engaged (pursuing) autocast check triggered. Trying to cast offensive spells"); + if (!IsAttackAllowed(GetTarget())) { + return false; + } + + auto cast_order = GetSpellTypesPrioritized(BotPriorityCategories::Pursue); + Mob* tar = nullptr; + + for (auto& current_cast : cast_order) { + if (current_cast.priority == 0) { + SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeHeldDelay)); + LogBotSpellChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(current_cast.spellType)); + continue; + } + + if (!RuleB(Bots, AllowAIMez) && (current_cast.spellType == BotSpellTypes::AEMez || current_cast.spellType == BotSpellTypes::Mez)) { + continue; + } + + if (IsCommandedBotSpellType(current_cast.spellType)) { // Unsupported by AI currently. + continue; + } + + if (AIBot_spells_by_type[current_cast.spellType].empty() && AIBot_spells_by_type[GetParentSpellType(current_cast.spellType)].empty()) { + continue; + } + + if (!SpellTypeAIDelayCheck(current_cast.spellType)) { + continue; + } + + result = AttemptAICastSpell(current_cast.spellType, nullptr); + + if (!result && IsBotSpellTypeBeneficial(current_cast.spellType)) { + result = AttemptCloseBeneficialSpells(current_cast.spellType); + } - if (!AICastSpell(GetTarget(), 100, SpellType_Snare)) { - if (!AICastSpell(GetTarget(), 100, SpellType_Lifetap) && !AICastSpell(GetTarget(), 100, SpellType_Nuke)) { + SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeDelay)); + + if (result) { + break; } - result = true; } if (!AIautocastspell_timer->Enabled()) { - AIautocastspell_timer->Start(RandomTimer(100, 250), false); + AIautocastspell_timer->Start(RandomTimer(RuleI(Bots, MinDelayBetweenInCombatCastAttempts), RuleI(Bots, MaxDelayBetweenInCombatCastAttempts)), false); } } @@ -1338,10 +663,17 @@ bool Bot::AI_PursueCastCheck() { } bool Bot::AI_IdleCastCheck() { + if (GetAppearance() == eaDead || delaytimer || spellend_timer.Enabled() || IsFeared() || IsSilenced() || IsAmnesiad() || GetHP() < 0) { + return false; + } + bool result = false; if (AIautocastspell_timer->Check(false)) { + LogAIDetail("Bot Non-Engaged autocast check triggered: [{}]", GetCleanName()); + LogBotSpellChecksDetail("{} says, 'AI_IdleCastCheck started.'", GetCleanName()); + AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. bool pre_combat = false; @@ -1349,7 +681,8 @@ bool Bot::AI_IdleCastCheck() { if (HasGroup() && GetGroup()->GetLeader() && GetGroup()->GetLeader()->IsClient()) { test_against = GetGroup()->GetLeader()->CastToClient(); - } else if (GetOwner() && GetOwner()->IsClient()) { + } + else if (GetOwner() && GetOwner()->IsClient()) { test_against = GetOwner()->CastToClient(); } @@ -1357,434 +690,122 @@ bool Bot::AI_IdleCastCheck() { pre_combat = test_against->GetBotPrecombat(); } - //Ok, IdleCastCheck depends of class. - switch (GetClass()) { - // Healers WITHOUT pets will check if a heal is needed before buffing. - case Class::Cleric: - case Class::Paladin: - case Class::Ranger: { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Cure)) { - if (!AICastSpell(this, 100, SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Heal)) { - if (!AICastSpell(this, 100, SpellType_Buff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Buff)) { - } - } - } - } + auto cast_order = GetSpellTypesPrioritized(BotPriorityCategories::Idle); + Mob* tar = nullptr; + + for (auto& current_cast : cast_order) { + if (current_cast.priority == 0) { + SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeHeldDelay)); + LogBotSpellChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(current_cast.spellType)); + continue; } - result = true; - break; - } - case Class::Monk: - case Class::Rogue: - case Class::Warrior: - case Class::Berserker: { - if (!AICastSpell(this, 100, SpellType_Cure)) { - if (!AICastSpell(this, 100, SpellType_Heal)) { - if (!AICastSpell(this, 100, SpellType_Buff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Buff)) { - } - } - } + if (!pre_combat && (current_cast.spellType == BotSpellTypes::PreCombatBuff || current_cast.spellType == BotSpellTypes::PreCombatBuffSong)) { + continue; } - result = true; - break; - } - // Pets class will first cast their pet, then buffs - - case Class::Magician: - case Class::ShadowKnight: - case Class::Necromancer: - case Class::Enchanter: { - if (!AICastSpell(this, 100, SpellType_Pet)) { - if (!AICastSpell(this, 100, SpellType_Cure)) { - if (!AICastSpell(GetPet(), 100, SpellType_Cure)) { - if (!AICastSpell(this, 100, SpellType_Buff)) { - if (!AICastSpell(GetPet(), 100, SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Buff)) { - } - } - } - } - } + if (!RuleB(Bots, AllowAIMez) && (current_cast.spellType == BotSpellTypes::AEMez || current_cast.spellType == BotSpellTypes::Mez)) { + continue; } - result = true; - break; - } - case Class::Druid: - case Class::Shaman: - case Class::Beastlord: { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Cure)) { - if (!AICastSpell(this, 100, SpellType_Pet)) { - if (!AICastSpell(this, 100, SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Heal)) { - if (!AICastSpell(this, 100, SpellType_Buff)) { - if (!AICastSpell(GetPet(), 100, SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Buff)) { - } - } - } - } - } - } + if (IsCommandedBotSpellType(current_cast.spellType)) { // Unsupported by AI currently. + continue; } - result = true; - break; - } - case Class::Wizard: { // This can eventually be move into the Class::Beastlord case handler once pre-combat is fully implemented - if (pre_combat) { - if (!AICastSpell(this, 100, SpellType_Pet)) { - if (!AICastSpell(this, 100, SpellType_Cure)) { - if (!AICastSpell(this, 100, SpellType_Heal)) { - if (!AICastSpell(this, 100, SpellType_Buff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_PreCombatBuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Buff)) { - } - } - } - } - } - } + if (!IsBotSpellTypeBeneficial(current_cast.spellType)) { + continue; } - else { - if (!AICastSpell(this, 100, SpellType_Cure)) { - if (!AICastSpell(this, 100, SpellType_Pet)) { - if (!AICastSpell(this, 100, SpellType_Heal)) { - if (!AICastSpell(this, 100, SpellType_Buff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Buff)) { - } - } - } - } - } + + if (AIBot_spells_by_type[current_cast.spellType].empty() && AIBot_spells_by_type[GetParentSpellType(current_cast.spellType)].empty()) { + continue; } - result = true; - break; - } - case Class::Bard: { - if (pre_combat) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Cure)) { - if (!AICastSpell(this, 100, SpellType_Buff)) { - if (!AICastSpell(this, 100, SpellType_PreCombatBuffSong)) { - if (!AICastSpell(this, 100, SpellType_InCombatBuffSong)) { - } - } - } - } + if (!SpellTypeAIDelayCheck(current_cast.spellType)) { + continue; } - else { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, 100, BotAISpellRange, SpellType_Cure)) { - if (!AICastSpell(this, 100, SpellType_Buff)) { - if (!AICastSpell(this, 100, SpellType_OutOfCombatBuffSong)) { - if (!AICastSpell(this, 100, SpellType_InCombatBuffSong)) { - } - } - } - } + + result = AttemptAICastSpell(current_cast.spellType, nullptr); + + if (result) { + break; } - result = true; - break; - } - default: - break; + result = AttemptCloseBeneficialSpells(current_cast.spellType); + + SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeDelay)); + + if (result) { + break; + } } - if (!AIautocastspell_timer->Enabled()) - AIautocastspell_timer->Start(RandomTimer(500, 2000), false); // avg human response is much less than 5 seconds..even for non-combat situations... + if (!AIautocastspell_timer->Enabled()) { + AIautocastspell_timer->Start(RandomTimer(RuleI(Bots, MinDelayBetweenOutCombatCastAttempts), RuleI(Bots, MaxDelayBetweenOutCombatCastAttempts)), false); + } } return result; } bool Bot::AI_EngagedCastCheck() { + if (GetAppearance() == eaDead || delaytimer || spellend_timer.Enabled() || IsFeared() || IsSilenced() || IsAmnesiad() || GetHP() < 0) { + return false; + } + bool result = false; - bool failedToCast = false; if (GetTarget() && AIautocastspell_timer->Check(false)) { + LogAIDetail("Bot Engaged autocast check triggered: [{}]", GetCleanName()); + LogBotSpellChecksDetail("{} says, 'AI_EngagedCastCheck started.'", GetCleanName()); + AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. - uint8 botClass = GetClass(); - bool mayGetAggro = HasOrMayGetAggro(); - - LogAIDetail("Engaged autocast check triggered (BOTS). Trying to cast healing spells then maybe offensive spells"); - - if (botClass == Class::Cleric) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_Heal), BotAISpellRange, SpellType_Heal)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - //AIautocastspell_timer->Start(RandomTimer(100, 250), false); // Do not give healer classes a lot of time off or your tank's die - failedToCast = true; - } - } - } - } - } - } - } - else if (botClass == Class::Druid) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_Heal), BotAISpellRange, SpellType_Heal)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - //AIautocastspell_timer->Start(RandomTimer(100, 250), false); // Do not give healer classes a lot of time off or your tank's die - failedToCast = true; - } - } - } - } - } - } - } - } - else if (botClass == Class::Shaman) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Slow), SpellType_Slow)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_Heal), BotAISpellRange, SpellType_Heal)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - if (!AICastSpell(GetPet(), GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - failedToCast = true; - } - } - } - } - } - } - } - } - } - } - else if (botClass == Class::Ranger) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_Heal), BotAISpellRange, SpellType_Heal)) { - failedToCast = true; - } - } - } - } - } - } - } - else if (botClass == Class::Beastlord) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Slow), SpellType_Slow)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Pet), SpellType_Pet)) { - if (!AICastSpell(GetPet(), GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_Heal), BotAISpellRange, SpellType_Heal)) { - failedToCast = true; - } - } - } - } - } - } - } - } - } - } - else if (botClass == Class::Wizard) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - else if (botClass == Class::Paladin) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_Heal), BotAISpellRange, SpellType_Heal)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - } - else if (botClass == Class::ShadowKnight) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Lifetap), SpellType_Lifetap)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - } - else if (botClass == Class::Magician) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Pet), SpellType_Pet)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetPet(), GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - } - else if (botClass == Class::Necromancer) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Pet), SpellType_Pet)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Lifetap), SpellType_Lifetap)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetPet(), GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - } - } - } + if (!IsAttackAllowed(GetTarget())) { + return false; } - else if (botClass == Class::Enchanter) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Mez), SpellType_Mez)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Slow), SpellType_Slow)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), mayGetAggro?0:GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - } + + auto cast_order = GetSpellTypesPrioritized(BotPriorityCategories::Engaged); + Mob* tar = nullptr; + + for (auto& current_cast : cast_order) { + if (current_cast.priority == 0) { + SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeHeldDelay)); + LogBotSpellChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(current_cast.spellType)); + continue; } - } - else if (botClass == Class::Bard) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) {// Bards will use their escape songs - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_HateRedux), BotAISpellRange, SpellType_HateRedux)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Slow), SpellType_Slow)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_InCombatBuffSong), SpellType_InCombatBuffSong)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), mayGetAggro ? 0 : GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) {// Bards will use their dot songs - if (!AICastSpell(GetTarget(), mayGetAggro ? 0 : GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) {// Bards will use their nuke songs - failedToCast = true; - } - } - } - } - } - } - } + + if (!RuleB(Bots, AllowAIMez) && (current_cast.spellType == BotSpellTypes::AEMez || current_cast.spellType == BotSpellTypes::Mez)) { + continue; } - } - else if (botClass == Class::Berserker) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_InCombatBuffSong), SpellType_InCombatBuffSong)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - } + + if (IsCommandedBotSpellType(current_cast.spellType)) { // Unsupported by AI currently. + continue; } - } - else if (botClass == Class::Monk) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_InCombatBuffSong), SpellType_InCombatBuffSong)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - } + + if (AIBot_spells_by_type[current_cast.spellType].empty() && AIBot_spells_by_type[GetParentSpellType(current_cast.spellType)].empty()) { + continue; } - } - else if (botClass == Class::Rogue) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_InCombatBuffSong), SpellType_InCombatBuffSong)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } - } + + if (!SpellTypeAIDelayCheck(current_cast.spellType)) { + continue; } - } - else if (botClass == Class::Warrior) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_Heal), SpellType_Heal)) { - if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_InCombatBuff), BotAISpellRange, SpellType_InCombatBuff)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Debuff), SpellType_Debuff)) { - if (!AICastSpell(this, GetChanceToCastBySpellType(SpellType_InCombatBuffSong), SpellType_InCombatBuffSong)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_DOT), SpellType_DOT)) { - if (!AICastSpell(GetTarget(), GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) { - failedToCast = true; - } - } - } - } - } + + result = AttemptAICastSpell(current_cast.spellType, nullptr); + + SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeDelay)); + + if (!result && IsBotSpellTypeBeneficial(current_cast.spellType)) { + result = AttemptCloseBeneficialSpells(current_cast.spellType); } - } - if (!AIautocastspell_timer->Enabled()) { - AIautocastspell_timer->Start(RandomTimer(150, 300), false); + if (result) { + break; + } } - if (!failedToCast) { - result = true; + if (!AIautocastspell_timer->Enabled()) { + AIautocastspell_timer->Start(RandomTimer(RuleI(Bots, MinDelayBetweenInCombatCastAttempts), RuleI(Bots, MaxDelayBetweenInCombatCastAttempts)), false); } } @@ -1819,22 +840,22 @@ bool Bot::AIHealRotation(Mob* tar, bool useFastHeals) { botSpell.ManaCost = 0; if (useFastHeals) { - botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + botSpell = GetBestBotSpellForRegularSingleTargetHeal(this, tar); if (!IsValidSpell(botSpell.SpellId)) - botSpell = GetBestBotSpellForFastHeal(this); + botSpell = GetBestBotSpellForFastHeal(this, tar); } else { - botSpell = GetBestBotSpellForPercentageHeal(this); + botSpell = GetBestBotSpellForPercentageHeal(this, tar); if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + botSpell = GetBestBotSpellForRegularSingleTargetHeal(this, tar); } if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetFirstBotSpellForSingleTargetHeal(this); + botSpell = GetFirstBotSpellForSingleTargetHeal(this, tar); } if (!IsValidSpell(botSpell.SpellId)) { - botSpell = GetFirstBotSpellBySpellType(this, SpellType_Heal); + botSpell = GetFirstBotSpellBySpellType(this, BotSpellTypes::RegularHeal); } } @@ -1866,7 +887,7 @@ bool Bot::AIHealRotation(Mob* tar, bool useFastHeals) { } if (castedSpell) { - BotGroupSay( + RaidGroupSay( this, fmt::format( "Casting {} on {}, please stay in range!", @@ -1879,34 +900,37 @@ bool Bot::AIHealRotation(Mob* tar, bool useFastHeals) { return castedSpell; } -std::list Bot::GetBotSpellsForSpellEffect(Bot* botCaster, int spellEffect) { +std::list Bot::GetBotSpellsForSpellEffect(Bot* caster, uint16 spell_type, int spell_effect) { std::list result; - if (!botCaster) { + if (!caster) { return result; } - if (auto bot_owner = botCaster->GetBotOwner(); !bot_owner) { + if (auto bot_owner = caster->GetBotOwner(); !bot_owner) { return result; } - if (botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->AIBot_spells; + if (caster->AI_HasSpells()) { + std::vector bot_spell_list = caster->BotGetSpellsByType(spell_type); - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpellAndLoS(bot_spell_list[i].spellid, caster->HasLoS())) { continue; } - if (IsEffectInSpell(botSpellList[i].spellid, spellEffect) || GetSpellTriggerSpellID(botSpellList[i].spellid, spellEffect)) { - BotSpell botSpell; - botSpell.SpellId = botSpellList[i].spellid; - botSpell.SpellIndex = i; - botSpell.ManaCost = botSpellList[i].manacost; + if ( + caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && + (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == caster->GetParentSpellType(spell_type)) && + caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) && + (IsEffectInSpell(bot_spell_list[i].spellid, spell_effect) || GetSpellTriggerSpellID(bot_spell_list[i].spellid, spell_effect)) + ) { + BotSpell bot_spell; + bot_spell.SpellId = bot_spell_list[i].spellid; + bot_spell.SpellIndex = bot_spell_list[i].index; + bot_spell.ManaCost = bot_spell_list[i].manacost; - result.push_back(botSpell); + result.push_back(bot_spell); } } } @@ -1914,39 +938,40 @@ std::list Bot::GetBotSpellsForSpellEffect(Bot* botCaster, int spellEff return result; } -std::list Bot::GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, int spellEffect, SpellTargetType targetType) { +std::list Bot::GetBotSpellsForSpellEffectAndTargetType(Bot* caster, uint16 spell_type, int spell_effect, SpellTargetType target_type) { std::list result; - if (!botCaster) { + if (!caster) { return result; } - if (auto bot_owner = botCaster->GetBotOwner(); !bot_owner) { + if (auto bot_owner = caster->GetBotOwner(); !bot_owner) { return result; } - if (botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->AIBot_spells; + if (caster->AI_HasSpells()) { + std::vector bot_spell_list = caster->BotGetSpellsByType(spell_type); - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpellAndLoS(bot_spell_list[i].spellid, caster->HasLoS())) { continue; } if ( + caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && + (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == caster->GetParentSpellType(spell_type)) && + caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) && ( - IsEffectInSpell(botSpellList[i].spellid, spellEffect) || - GetSpellTriggerSpellID(botSpellList[i].spellid, spellEffect) + IsEffectInSpell(bot_spell_list[i].spellid, spell_effect) || + GetSpellTriggerSpellID(bot_spell_list[i].spellid, spell_effect) ) && - spells[botSpellList[i].spellid].target_type == targetType + (target_type == ST_TargetOptional || spells[bot_spell_list[i].spellid].target_type == target_type) ) { - BotSpell botSpell; - botSpell.SpellId = botSpellList[i].spellid; - botSpell.SpellIndex = i; - botSpell.ManaCost = botSpellList[i].manacost; - result.push_back(botSpell); + BotSpell bot_spell; + bot_spell.SpellId = bot_spell_list[i].spellid; + bot_spell.SpellIndex = bot_spell_list[i].index; + bot_spell.ManaCost = bot_spell_list[i].manacost; + result.push_back(bot_spell); } } } @@ -1954,34 +979,36 @@ std::list Bot::GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, return result; } -std::list Bot::GetBotSpellsBySpellType(Bot* botCaster, uint32 spellType) { +std::list Bot::GetBotSpellsBySpellType(Bot* caster, uint16 spell_type) { std::list result; - if (!botCaster) { + if (!caster) { return result; } - if (auto bot_owner = botCaster->GetBotOwner(); !bot_owner) { + if (auto bot_owner = caster->GetBotOwner(); !bot_owner) { return result; } - if (botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->AIBot_spells; + if (caster->AI_HasSpells()) { + std::vector bot_spell_list = caster->BotGetSpellsByType(spell_type); - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpellAndLoS(bot_spell_list[i].spellid, caster->HasLoS())) { continue; } - if (botSpellList[i].type & spellType) { - BotSpell botSpell; - botSpell.SpellId = botSpellList[i].spellid; - botSpell.SpellIndex = i; - botSpell.ManaCost = botSpellList[i].manacost; + if ( + caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && + (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == caster->GetParentSpellType(spell_type)) && + caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) + ) { + BotSpell bot_spell; + bot_spell.SpellId = bot_spell_list[i].spellid; + bot_spell.SpellIndex = bot_spell_list[i].index; + bot_spell.ManaCost = bot_spell_list[i].manacost; - result.push_back(botSpell); + result.push_back(bot_spell); } } } @@ -1989,63 +1016,114 @@ std::list Bot::GetBotSpellsBySpellType(Bot* botCaster, uint32 spellTyp return result; } -std::list Bot::GetPrioritizedBotSpellsBySpellType(Bot* botCaster, uint32 spellType) { - std::list result; +std::vector Bot::GetPrioritizedBotSpellsBySpellType(Bot* caster, uint16 spell_type, Mob* tar, bool AE, uint16 sub_target_type, uint16 sub_type) { + std::vector result; - if (botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->AIBot_spells; + if (caster && caster->AI_HasSpells()) { + std::vector bot_spell_list = caster->BotGetSpellsByType(spell_type); - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpellAndLoS(bot_spell_list[i].spellid, caster->HasLoS())) { continue; } - if (botSpellList[i].type & spellType) { - BotSpell_wPriority botSpell; - botSpell.SpellId = botSpellList[i].spellid; - botSpell.SpellIndex = i; - botSpell.ManaCost = botSpellList[i].manacost; - botSpell.Priority = botSpellList[i].priority; + if (spell_type == BotSpellTypes::HateRedux && caster->GetClass() == Class::Bard) { + if (spells[bot_spell_list[i].spellid].target_type != ST_Target) { + continue; + } + } + + if ( + caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && + (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == caster->GetParentSpellType(spell_type)) && + caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) + ) { + if ( + caster->IsCommandedSpell() && + ( + !caster->IsValidSpellTypeSubType(spell_type, sub_target_type, bot_spell_list[i].spellid) || + !caster->IsValidSpellTypeSubType(spell_type, sub_type, bot_spell_list[i].spellid) + ) + ) { + continue; + } + + if (!AE && IsAnyAESpell(bot_spell_list[i].spellid) && !IsGroupSpell(bot_spell_list[i].spellid)) { + continue; + } + else if (AE && !IsAnyAESpell(bot_spell_list[i].spellid)) { + continue; + } + + if ( + !caster->IsInGroupOrRaid(tar, true) && + ( + !RuleB(Bots, EnableBotTGB) || + ( + IsGroupSpell(bot_spell_list[i].spellid) && + !IsTGBCompatibleSpell(bot_spell_list[i].spellid) + ) + ) + ) { + continue; + } + + if (!IsPBAESpell(bot_spell_list[i].spellid) && !caster->CastChecks(bot_spell_list[i].spellid, tar, spell_type, false, IsAEBotSpellType(spell_type))) { + continue; + } + + if ( + caster->IsCommandedSpell() || + !AE || + ( + BotSpellTypeRequiresAEChecks(spell_type) && + caster->HasValidAETarget(caster, bot_spell_list[i].spellid, spell_type, tar) + ) + ) { + BotSpell_wPriority bot_spell; + bot_spell.SpellId = bot_spell_list[i].spellid; + bot_spell.SpellIndex = bot_spell_list[i].index; + bot_spell.ManaCost = bot_spell_list[i].manacost; + bot_spell.Priority = bot_spell_list[i].priority; - result.push_back(botSpell); + result.emplace_back(bot_spell); + } } } if (result.size() > 1) { - result.sort( - [](BotSpell_wPriority const& l, BotSpell_wPriority const& r) { - return l.Priority < r.Priority; - } - ); + std::sort(result.begin(), result.end(), [](BotSpell_wPriority const& l, BotSpell_wPriority const& r) { + return l.Priority < r.Priority; + }); } } return result; } -BotSpell Bot::GetFirstBotSpellBySpellType(Bot* botCaster, uint32 spellType) { +BotSpell Bot::GetFirstBotSpellBySpellType(Bot* caster, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->AIBot_spells; + if (caster && caster->AI_HasSpells()) { + std::vector bot_spell_list = caster->BotGetSpellsByType(spell_type); - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpellAndLoS(bot_spell_list[i].spellid, caster->HasLoS())) { continue; } - if ((botSpellList[i].type & spellType) && botCaster->CheckSpellRecastTimer(botSpellList[i].spellid)) { - result.SpellId = botSpellList[i].spellid; - result.SpellIndex = i; - result.ManaCost = botSpellList[i].manacost; + if ( + caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && + (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == caster->GetParentSpellType(spell_type)) && + caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) + ) { + result.SpellId = bot_spell_list[i].spellid; + result.SpellIndex = bot_spell_list[i].index; + result.ManaCost = bot_spell_list[i].manacost; break; } @@ -2055,22 +1133,23 @@ BotSpell Bot::GetFirstBotSpellBySpellType(Bot* botCaster, uint32 spellType) { return result; } -BotSpell Bot::GetBestBotSpellForFastHeal(Bot *botCaster) { +BotSpell Bot::GetBestBotSpellForVeryFastHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CurrentHP); + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP); - for (auto botSpellListItr : botSpellList) { + for (auto bot_spell_list_itr : bot_spell_list) { // Assuming all the spells have been loaded into this list by level and in descending order - if (IsFastHealSpell(botSpellListItr.SpellId) && botCaster->CheckSpellRecastTimer(botSpellListItr.SpellId)) { - result.SpellId = botSpellListItr.SpellId; - result.SpellIndex = botSpellListItr.SpellIndex; - result.ManaCost = botSpellListItr.ManaCost; + if ( + IsVeryFastHealSpell(bot_spell_list_itr.SpellId) && caster->CastChecks(bot_spell_list_itr.SpellId, tar, spell_type)) { + result.SpellId = bot_spell_list_itr.SpellId; + result.SpellIndex = bot_spell_list_itr.SpellIndex; + result.ManaCost = bot_spell_list_itr.ManaCost; break; } @@ -2080,37 +1159,22 @@ BotSpell Bot::GetBestBotSpellForFastHeal(Bot *botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForFastHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botHoTSpellList = GetBotSpellsForSpellEffect(botCaster, SE_HealOverTime); - std::vector botSpellList = botCaster->AIBot_spells; + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP); - for (auto botSpellListItr : botHoTSpellList) { + for (auto bot_spell_list_itr : bot_spell_list) { // Assuming all the spells have been loaded into this list by level and in descending order - if (IsHealOverTimeSpell(botSpellListItr.SpellId)) { - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here - continue; - } - - if ( - botSpellList[i].spellid == botSpellListItr.SpellId && - (botSpellList[i].type & SpellType_Heal) && - botCaster->CheckSpellRecastTimer(botSpellListItr.SpellId) - ) { - result.SpellId = botSpellListItr.SpellId; - result.SpellIndex = botSpellListItr.SpellIndex; - result.ManaCost = botSpellListItr.ManaCost; - } - } + if (IsFastHealSpell(bot_spell_list_itr.SpellId) && caster->CastChecks(bot_spell_list_itr.SpellId, tar, spell_type)) { + result.SpellId = bot_spell_list_itr.SpellId; + result.SpellIndex = bot_spell_list_itr.SpellIndex; + result.ManaCost = bot_spell_list_itr.ManaCost; break; } @@ -2120,27 +1184,23 @@ BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot *botCaster) { +BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster && botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->AIBot_spells; + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_HealOverTime); - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here - continue; - } + for (auto bot_spell_list_itr : bot_spell_list) { + // Assuming all the spells have been loaded into this list by level and in descending order + if (IsHealOverTimeSpell(bot_spell_list_itr.SpellId) && caster->CastChecks(bot_spell_list_itr.SpellId, tar, spell_type)) { + result.SpellId = bot_spell_list_itr.SpellId; + result.SpellIndex = bot_spell_list_itr.SpellIndex; + result.ManaCost = bot_spell_list_itr.ManaCost; - if (IsCompleteHealSpell(botSpellList[i].spellid) && botCaster->CheckSpellRecastTimer(botSpellList[i].spellid)) { - result.SpellId = botSpellList[i].spellid; - result.SpellIndex = i; - result.ManaCost = botSpellList[i].manacost; break; } } @@ -2149,22 +1209,30 @@ BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot *botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForRegularSingleTargetHeal(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CurrentHP); + if (caster && caster->AI_HasSpells()) { + std::vector bot_spell_list = caster->BotGetSpellsByType(spell_type); + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpell(bot_spell_list[i].spellid)) { + continue; + } + + if ( + (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == caster->GetParentSpellType(spell_type)) && + caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) && + IsCompleteHealSpell(bot_spell_list[i].spellid) && + caster->CastChecks(bot_spell_list[i].spellid, tar, spell_type) + ) { + result.SpellId = bot_spell_list[i].spellid; + result.SpellIndex = bot_spell_list[i].index; + result.ManaCost = bot_spell_list[i].manacost; - for (std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { - // Assuming all the spells have been loaded into this list by level and in descending order - if (IsRegularSingleTargetHealSpell(botSpellListItr->SpellId) && botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; break; } } @@ -2173,28 +1241,23 @@ BotSpell Bot::GetBestBotSpellForRegularSingleTargetHeal(Bot* botCaster) { return result; } -BotSpell Bot::GetFirstBotSpellForSingleTargetHeal(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForRegularSingleTargetHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CurrentHP); + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP); - for (std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order - if ( - ( - IsRegularSingleTargetHealSpell(botSpellListItr->SpellId) || - IsFastHealSpell(botSpellListItr->SpellId) - ) && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if (IsRegularSingleTargetHealSpell(bot_spell_list_itr->SpellId) && caster->CastChecks(bot_spell_list_itr->SpellId, tar, spell_type)) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + break; } } @@ -2203,25 +1266,23 @@ BotSpell Bot::GetFirstBotSpellForSingleTargetHeal(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForGroupHeal(Bot* botCaster) { +BotSpell Bot::GetFirstBotSpellForSingleTargetHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CurrentHP); + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP); - for (std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order - if ( - IsRegularGroupHealSpell(botSpellListItr->SpellId) && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if (IsRegularSingleTargetHealSpell(bot_spell_list_itr->SpellId) && caster->CastChecks(bot_spell_list_itr->SpellId, tar, spell_type)) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + break; } } @@ -2230,39 +1291,35 @@ BotSpell Bot::GetBestBotSpellForGroupHeal(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForGroupHealOverTime(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForGroupHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botHoTSpellList = GetBotSpellsForSpellEffect(botCaster, SE_HealOverTime); - std::vector botSpellList = botCaster->AIBot_spells; + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP); + + int target_count = 0; - for (std::list::iterator botSpellListItr = botHoTSpellList.begin(); botSpellListItr != botHoTSpellList.end(); ++botSpellListItr) { + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order - if (IsGroupHealOverTimeSpell(botSpellListItr->SpellId)) { + if (IsGroupHealOverTimeSpell(bot_spell_list_itr->SpellId)) { + uint16 spell_id = bot_spell_list_itr->SpellId; - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here - continue; - } + if (!caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { + target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); - if ( - botSpellList[i].spellid == botSpellListItr->SpellId && - (botSpellList[i].type & SpellType_Heal) && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { + continue; } } + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + break; } } @@ -2271,25 +1328,35 @@ BotSpell Bot::GetBestBotSpellForGroupHealOverTime(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForGroupCompleteHeal(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForGroupHealOverTime(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CompleteHeal); + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_HealOverTime); + + int target_count = 0; - for(std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order - if ( - IsGroupCompleteHealSpell(botSpellListItr->SpellId) && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if (IsGroupHealOverTimeSpell(bot_spell_list_itr->SpellId)) { + uint16 spell_id = bot_spell_list_itr->SpellId; + + if (!caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { + target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); + + if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { + continue; + } + } + + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + break; } } @@ -2298,25 +1365,34 @@ BotSpell Bot::GetBestBotSpellForGroupCompleteHeal(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForMez(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForGroupCompleteHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_Mez); + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CompleteHeal); + + int target_count = 0; - for (std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order - if ( - IsMesmerizeSpell(botSpellListItr->SpellId) && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if (IsGroupHealOverTimeSpell(bot_spell_list_itr->SpellId)) { + uint16 spell_id = bot_spell_list_itr->SpellId; + + if (!caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { + target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); + + if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { + continue; + } + } + + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; break; } @@ -2326,26 +1402,26 @@ BotSpell Bot::GetBestBotSpellForMez(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForMagicBasedSlow(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForMez(Bot* caster, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_AttackSpeed); + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_Mez); - for (std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order if ( - IsSlowSpell(botSpellListItr->SpellId) && - spells[botSpellListItr->SpellId].resist_type == RESIST_MAGIC && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) + IsMesmerizeSpell(bot_spell_list_itr->SpellId) && + caster->CheckSpellRecastTimer(bot_spell_list_itr->SpellId) ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + break; } } @@ -2354,95 +1430,123 @@ BotSpell Bot::GetBestBotSpellForMagicBasedSlow(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForDiseaseBasedSlow(Bot* botCaster) { - BotSpell result; +Mob* Bot::GetFirstIncomingMobToMez(Bot* caster, int16 spell_id, uint16 spell_type, bool AE) { + Mob* result = nullptr; - result.SpellId = 0; - result.SpellIndex = 0; - result.ManaCost = 0; + if (caster && caster->GetOwner()) { + int spell_range = (!AE ? caster->GetActSpellRange(spell_id, spells[spell_id].range) : caster->GetActSpellRange(spell_id, spells[spell_id].aoe_range)); + int buff_count = 0; + NPC* npc = nullptr; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_AttackSpeed); + for (auto& close_mob : caster->m_close_mobs) { + buff_count = 0; + npc = close_mob.second->CastToNPC(); - for (std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { - // Assuming all the spells have been loaded into this list by level and in descending order - if ( - IsSlowSpell(botSpellListItr->SpellId) && - spells[botSpellListItr->SpellId].resist_type == RESIST_DISEASE && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if (!npc) { + continue; + } - break; + if (!caster->IsValidMezTarget(caster->GetOwner(), npc, spell_id)) { + continue; } - } - } - return result; -} + if (AE) { + int target_count = 0; -Mob* Bot::GetFirstIncomingMobToMez(Bot* botCaster, BotSpell botSpell) { - Mob* result = 0; + for (auto& close_mob : caster->m_close_mobs) { + Mob* m = close_mob.second; - if (botCaster && IsMesmerizeSpell(botSpell.SpellId)) { + if (npc == m) { + continue; + } - std::list npc_list; - entity_list.GetNPCList(npc_list); + if (!caster->IsValidMezTarget(caster->GetOwner(), m, spell_id)) { + continue; + } - for(std::list::iterator itr = npc_list.begin(); itr != npc_list.end(); ++itr) { - NPC* npc = *itr; + if (IsPBAESpell(spell_id)) { + if (spell_range < Distance(caster->GetPosition(), m->GetPosition())) { + continue; + } + } + else { + if (spell_range < Distance(m->GetPosition(), npc->GetPosition())) { + continue; + } + } - if (DistanceSquaredNoZ(npc->GetPosition(), botCaster->GetPosition()) <= botCaster->GetActSpellRange(botSpell.SpellId, spells[botSpell.SpellId].range)) { - if (!npc->IsMezzed()) { - if (botCaster->HasGroup()) { - Group* g = botCaster->GetGroup(); + if (caster->CastChecks(spell_id, m, spell_type, true, true)) { + ++target_count; + } - if (g) { - for (int counter = 0; counter < g->GroupCount(); counter++) { - if ( - npc->IsOnHatelist(g->members[counter]) && - g->members[counter]->GetTarget() != npc && g->members[counter]->IsEngaged()) { - result = npc; - break; - } - } - } + if (target_count >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { + break; } } + + if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { + continue; + } + + if (zone->random.Int(1, 100) < RuleI(Bots, AEMezChance)) { + caster->SetSpellTypeRecastTimer(spell_type, RuleI(Bots, MezFailDelay)); + return result; + } + + result = npc; } + else { + if (spell_range < Distance(caster->GetPosition(), npc->GetPosition())) { + continue; + } - if (result) - break; + if (!caster->CastChecks(spell_id, npc, spell_type, true)) { + continue; + } + + if (zone->random.Int(1, 100) < RuleI(Bots, MezChance)) { + caster->SetSpellTypeRecastTimer(spell_type, RuleI(Bots, MezAEFailDelay)); + + return result; + } + + result = npc; + } + + if (result) { + caster->SetHasLoS(true); + + return result; + } } } return result; } -BotSpell Bot::GetBestBotMagicianPetSpell(Bot *botCaster) { +BotSpell Bot::GetBestBotMagicianPetSpell(Bot* caster, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_SummonPet); + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_SummonPet); + std::string pet_type = GetBotMagicianPetType(caster); - std::string petType = GetBotMagicianPetType(botCaster); - - for(std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { + for(std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order - if (IsSummonPetSpell(botSpellListItr->SpellId) && botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) { - if (!strncmp(spells[botSpellListItr->SpellId].teleport_zone, petType.c_str(), petType.length())) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if ( + IsSummonPetSpell(bot_spell_list_itr->SpellId) && + caster->CheckSpellRecastTimer(bot_spell_list_itr->SpellId) && + !strncmp(spells[bot_spell_list_itr->SpellId].teleport_zone, pet_type.c_str(), pet_type.length()) + ) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; break; - } } } } @@ -2450,80 +1554,147 @@ BotSpell Bot::GetBestBotMagicianPetSpell(Bot *botCaster) { return result; } -std::string Bot::GetBotMagicianPetType(Bot* botCaster) { +std::string Bot::GetBotMagicianPetType(Bot* caster) { std::string result; - if (botCaster) { - if (botCaster->IsPetChooser()) { - switch(botCaster->GetPetChooserID()) { - case 0: + if (caster) { + uint8 pet_type = caster->GetPetChooserID(); + uint8 bot_level = caster->GetLevel(); + bool epic_allowed = false; + std::string epic_spell_name = RuleS(Bots, EpicPetSpellName); + + if (epic_spell_name.empty()) { + epic_spell_name = "SumMageMultiElement"; + } + + if (RuleB(Bots, AllowMagicianEpicPet)) { + if (bot_level >= RuleI(Bots, AllowMagicianEpicPetLevel)) { + if (!RuleI(Bots, RequiredMagicianEpicPetItemID)) { + epic_allowed = true; + } + else { + bool has_item = caster->HasBotItem(RuleI(Bots, RequiredMagicianEpicPetItemID)) != INVALID_INDEX; + + if (has_item) { + epic_allowed = true; + } + } + } + } + + if (pet_type > 0) { + switch (pet_type) { + case SumWater: result = std::string("SumWater"); break; - case 1: + case SumFire: result = std::string("SumFire"); break; - case 2: + case SumAir: result = std::string("SumAir"); break; - case 3: + case SumEarth: result = std::string("SumEarth"); break; - default: + case MonsterSum: result = std::string("MonsterSum"); break; + case SumMageMultiElement: + if (epic_allowed) { + result = epic_spell_name; + } + else { + caster->SetPetChooserID(0); + } + break; } } else { - if (botCaster->GetLevel() == 2) - result = std::string("SumWater"); - else if (botCaster->GetLevel() == 3) - result = std::string("SumFire"); - else if (botCaster->GetLevel() == 4) - result = std::string("SumAir"); - else if (botCaster->GetLevel() == 5) - result = std::string("SumEarth"); - else if (botCaster->GetLevel() < 30) { - // Under level 30 - int counter = zone->random.Int(0, 3); - - switch(counter) { - case 0: - result = std::string("SumWater"); - break; - case 1: - result = std::string("SumFire"); - break; - case 2: - result = std::string("SumAir"); - break; - case 3: - result = std::string("SumEarth"); - break; - default: - result = std::string("MonsterSum"); - break; + uint8 air_min_level = 255; + uint8 fire_min_level = 255; + uint8 water_min_level = 255; + uint8 earth_min_level = 255; + uint8 monster_min_level = 255; + uint8 epic_min_level = 255; + std::list bot_spell_list = caster->GetBotSpellsBySpellType(caster, BotSpellTypes::Pet); + + for (const auto& s : bot_spell_list) { + if (!IsValidSpell(s.SpellId)) { + continue; + } + + if (!IsEffectInSpell(s.SpellId, SE_SummonPet)) { + continue; + } + + auto spell = spells[s.SpellId]; + + if (!strncmp(spell.teleport_zone, "SumWater", 8) && spell.classes[Class::Magician - 1] < water_min_level) { + water_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, "SumFire", 7) && spell.classes[Class::Magician - 1] < fire_min_level) { + fire_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, "SumAir", 6) && spell.classes[Class::Magician - 1] < air_min_level) { + air_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, "SumEarth", 8) && spell.classes[Class::Magician - 1] < earth_min_level) { + earth_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, "MonsterSum", 10) && spell.classes[Class::Magician - 1] < monster_min_level) { + monster_min_level = spell.classes[Class::Magician - 1]; + } + else if (!strncmp(spell.teleport_zone, epic_spell_name.c_str(), epic_spell_name.length()) && spell.classes[Class::Magician - 1] < epic_min_level) { + epic_min_level = spell.classes[Class::Magician - 1]; + } + } + + if (epic_allowed) { + epic_min_level = std::max(int(epic_min_level), RuleI(Bots, AllowMagicianEpicPetLevel)); + + if (bot_level >= epic_min_level) { + result = epic_spell_name; } } else { - // Over level 30 - int counter = zone->random.Int(0, 4); + bool found = false; + uint8 count = 0; - switch(counter) { - case 0: - result = std::string("SumWater"); - break; - case 1: - result = std::string("SumFire"); - break; - case 2: - result = std::string("SumAir"); - break; - case 3: - result = std::string("SumEarth"); - break; - default: - result = std::string("MonsterSum"); - break; + while (count <= 4 && !found) { + int counter = zone->random.Int(1, 4); + + switch (counter) { + case SumWater: + if (bot_level >= water_min_level) { + result = std::string("SumWater"); + } + + found = true; + break; + case SumFire: + if (bot_level >= fire_min_level) { + result = std::string("SumFire"); + } + + found = true; + break; + case SumAir: + if (bot_level >= air_min_level) { + result = std::string("SumAir"); + } + + found = true; + break; + case SumEarth: + if (bot_level >= earth_min_level) { + result = std::string("SumEarth"); + } + + found = true; + break; + } + + ++count; } } } @@ -2532,24 +1703,46 @@ std::string Bot::GetBotMagicianPetType(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForNukeByTargetType(Bot* botCaster, SpellTargetType targetType) { +BotSpell Bot::GetBestBotSpellForNukeByTargetType(Bot* caster, SpellTargetType target_type, uint16 spell_type, bool AE, Mob* tar) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, SE_CurrentHP, targetType); + if (tar == nullptr) { + tar = caster->GetTarget(); + } + + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffectAndTargetType(caster, spell_type, SE_CurrentHP, target_type); + + for(std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { + // Assuming all the spells have been loaded into this list by level and in descending order + if (IsPureNukeSpell(bot_spell_list_itr->SpellId) || IsDamageSpell(bot_spell_list_itr->SpellId)) { + if (!AE && IsAnyAESpell(bot_spell_list_itr->SpellId) && !IsGroupSpell(bot_spell_list_itr->SpellId)) { + continue; + } + else if (AE && !IsAnyAESpell(bot_spell_list_itr->SpellId)) { + continue; + } + + if (!IsPBAESpell(bot_spell_list_itr->SpellId) && !caster->CastChecks(bot_spell_list_itr->SpellId, tar, spell_type, false, IsAEBotSpellType(spell_type))) { + continue; + } + - for(std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { - // Assuming all the spells have been loaded into this list by level and in descending order - if ((IsPureNukeSpell(botSpellListItr->SpellId) || IsDamageSpell(botSpellListItr->SpellId)) && botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if ( + caster->IsCommandedSpell() || + !AE || + (AE && caster->HasValidAETarget(caster, bot_spell_list_itr->SpellId, spell_type, tar)) + ) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; - break; + break; + } } } } @@ -2557,7 +1750,7 @@ BotSpell Bot::GetBestBotSpellForNukeByTargetType(Bot* botCaster, SpellTargetType return result; } -BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType targetType) +BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* caster, SpellTargetType target_type, uint16 spell_type, bool AE, Mob* tar) { BotSpell result; @@ -2565,19 +1758,40 @@ BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster) + if (tar == nullptr) { + tar = caster->GetTarget(); + } + + if (caster) { - std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, SE_Stun, targetType); + std::list bot_spell_list = GetBotSpellsForSpellEffectAndTargetType(caster, spell_type, SE_Stun, target_type); - for(std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) + for(std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order - if (IsStunSpell(botSpellListItr->SpellId) && botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) - { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; - break; + if (IsStunSpell(bot_spell_list_itr->SpellId)) { + if (!AE && IsAnyAESpell(bot_spell_list_itr->SpellId) && !IsGroupSpell(bot_spell_list_itr->SpellId)) { + continue; + } + else if (AE && !IsAnyAESpell(bot_spell_list_itr->SpellId)) { + continue; + } + + if (!IsPBAESpell(bot_spell_list_itr->SpellId) && !caster->CastChecks(bot_spell_list_itr->SpellId, tar, spell_type, false, IsAEBotSpellType(spell_type))) { + continue; + } + + if ( + caster->IsCommandedSpell() || + !AE || + (AE && caster->HasValidAETarget(caster, bot_spell_list_itr->SpellId, spell_type, tar)) + ) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + + break; + } } } } @@ -2585,105 +1799,146 @@ BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType return result; } -BotSpell Bot::GetBestBotWizardNukeSpellByTargetResists(Bot* botCaster, Mob* target) { +BotSpell Bot::GetBestBotWizardNukeSpellByTargetResists(Bot* caster, Mob* target, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (botCaster && target) { - const int lureResisValue = -100; - const int maxTargetResistValue = 300; - bool selectLureNuke = false; + if (caster && target) { + + const int lure_resis_value = -100; + + int32 level_mod = (target->GetLevel() - caster->GetLevel()) * (target->GetLevel() - caster->GetLevel()) / 2; + + if (target->GetLevel() - caster->GetLevel() < 0) { + level_mod = -level_mod; + } + const int max_target_resist_value = caster->GetSpellTypeResistLimit(spell_type); + bool select_lure_nuke = false; - if ((target->GetMR() > maxTargetResistValue) && (target->GetCR() > maxTargetResistValue) && (target->GetFR() > maxTargetResistValue)) - selectLureNuke = true; + if (((target->GetMR() + level_mod) > max_target_resist_value) && ((target->GetCR() + level_mod) > max_target_resist_value) && ((target->GetFR() + level_mod) > max_target_resist_value)) { + select_lure_nuke = true; + } - std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, SE_CurrentHP, ST_Target); + std::list bot_spell_list = GetBotSpellsForSpellEffectAndTargetType(caster, spell_type, SE_CurrentHP, ST_Target); - BotSpell firstWizardMagicNukeSpellFound; - firstWizardMagicNukeSpellFound.SpellId = 0; - firstWizardMagicNukeSpellFound.SpellIndex = 0; - firstWizardMagicNukeSpellFound.ManaCost = 0; + BotSpell first_wizard_magic_nuke_spell_found; + first_wizard_magic_nuke_spell_found.SpellId = 0; + first_wizard_magic_nuke_spell_found.SpellIndex = 0; + first_wizard_magic_nuke_spell_found.ManaCost = 0; + bool spell_selected = false; - for(std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order - bool spellSelected = false; + if (!caster->IsValidSpellRange(bot_spell_list_itr->SpellId, target)) { + continue; + } - if (botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) { - if (selectLureNuke && (spells[botSpellListItr->SpellId].resist_difficulty < lureResisValue)) { - spellSelected = true; + if (select_lure_nuke && (spells[bot_spell_list_itr->SpellId].resist_difficulty < lure_resis_value)) { + if (caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type)) { + spell_selected = true; } - else if (IsPureNukeSpell(botSpellListItr->SpellId)) { - if (((target->GetMR() < target->GetCR()) || (target->GetMR() < target->GetFR())) && (GetSpellResistType(botSpellListItr->SpellId) == RESIST_MAGIC) - && (spells[botSpellListItr->SpellId].resist_difficulty > lureResisValue)) - { - spellSelected = true; - } - else if (((target->GetCR() < target->GetMR()) || (target->GetCR() < target->GetFR())) && (GetSpellResistType(botSpellListItr->SpellId) == RESIST_COLD) - && (spells[botSpellListItr->SpellId].resist_difficulty > lureResisValue)) - { - spellSelected = true; - } - else if (((target->GetFR() < target->GetCR()) || (target->GetFR() < target->GetMR())) && (GetSpellResistType(botSpellListItr->SpellId) == RESIST_FIRE) - && (spells[botSpellListItr->SpellId].resist_difficulty > lureResisValue)) - { - spellSelected = true; - } - else if ((GetSpellResistType(botSpellListItr->SpellId) == RESIST_MAGIC) && (spells[botSpellListItr->SpellId].resist_difficulty > lureResisValue) && !IsStunSpell(botSpellListItr->SpellId)) { - firstWizardMagicNukeSpellFound.SpellId = botSpellListItr->SpellId; - firstWizardMagicNukeSpellFound.SpellIndex = botSpellListItr->SpellIndex; - firstWizardMagicNukeSpellFound.ManaCost = botSpellListItr->ManaCost; - } + } + else if (!select_lure_nuke && IsPureNukeSpell(bot_spell_list_itr->SpellId)) { + if ( + ((target->GetMR() < target->GetCR()) || (target->GetMR() < target->GetFR())) && + (GetSpellResistType(bot_spell_list_itr->SpellId) == RESIST_MAGIC) && + (spells[bot_spell_list_itr->SpellId].resist_difficulty > lure_resis_value) && + caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) + ) { + spell_selected = true; + } + else if ( + ((target->GetCR() < target->GetMR()) || (target->GetCR() < target->GetFR())) && + (GetSpellResistType(bot_spell_list_itr->SpellId) == RESIST_COLD) && + (spells[bot_spell_list_itr->SpellId].resist_difficulty > lure_resis_value) && + caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) + ) { + spell_selected = true; + } + else if ( + ((target->GetFR() < target->GetCR()) || (target->GetFR() < target->GetMR())) && + (GetSpellResistType(bot_spell_list_itr->SpellId) == RESIST_FIRE) && + (spells[bot_spell_list_itr->SpellId].resist_difficulty > lure_resis_value) && + caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) + ) { + spell_selected = true; + } + else if ( + (GetSpellResistType(bot_spell_list_itr->SpellId) == RESIST_MAGIC) && + (spells[bot_spell_list_itr->SpellId].resist_difficulty > lure_resis_value) && + caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) + ) { + first_wizard_magic_nuke_spell_found.SpellId = bot_spell_list_itr->SpellId; + first_wizard_magic_nuke_spell_found.SpellIndex = bot_spell_list_itr->SpellIndex; + first_wizard_magic_nuke_spell_found.ManaCost = bot_spell_list_itr->ManaCost; } } - if (spellSelected) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; + if (spell_selected) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; break; } } + if (!spell_selected) { + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { + // Assuming all the spells have been loaded into this list by level and in descending order + + if (caster->CheckSpellRecastTimer(bot_spell_list_itr->SpellId)) { + if (caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type)) { + spell_selected = true; + } + } + if (spell_selected) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + + break; + } + } + } + if (result.SpellId == 0) { - result = firstWizardMagicNukeSpellFound; + result = first_wizard_magic_nuke_spell_found; } } return result; } -BotSpell Bot::GetDebuffBotSpell(Bot* botCaster, Mob *tar) { +BotSpell Bot::GetDebuffBotSpell(Bot* caster, Mob *tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (!tar || !botCaster) + if (!tar || !caster) return result; - if (botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->AIBot_spells; + if (caster->AI_HasSpells()) { + std::vector bot_spell_list = caster->BotGetSpellsByType(spell_type); - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpellAndLoS(bot_spell_list[i].spellid, caster->HasLoS())) { continue; } - if (((botSpellList[i].type & SpellType_Debuff) || IsDebuffSpell(botSpellList[i].spellid)) - && (!tar->IsImmuneToSpell(botSpellList[i].spellid, botCaster) - && tar->CanBuffStack(botSpellList[i].spellid, botCaster->GetLevel(), true) >= 0) - && botCaster->CheckSpellRecastTimer(botSpellList[i].spellid)) { - result.SpellId = botSpellList[i].spellid; - result.SpellIndex = i; - result.ManaCost = botSpellList[i].manacost; + if (((bot_spell_list[i].type == BotSpellTypes::Debuff) || IsDebuffSpell(bot_spell_list[i].spellid)) + && (!tar->IsImmuneToSpell(bot_spell_list[i].spellid, caster) + && tar->CanBuffStack(bot_spell_list[i].spellid, caster->GetLevel(), true) >= 0) + && caster->CheckSpellRecastTimer(bot_spell_list[i].spellid)) { + result.SpellId = bot_spell_list[i].spellid; + result.SpellIndex = bot_spell_list[i].index; + result.ManaCost = bot_spell_list[i].manacost; break; } @@ -2693,50 +1948,52 @@ BotSpell Bot::GetDebuffBotSpell(Bot* botCaster, Mob *tar) { return result; } -BotSpell Bot::GetBestBotSpellForResistDebuff(Bot* botCaster, Mob *tar) { +BotSpell Bot::GetBestBotSpellForResistDebuff(Bot* caster, Mob *tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (!tar || !botCaster) { + if (!tar || !caster) { return result; } - int level_mod = (tar->GetLevel() - botCaster->GetLevel())* (tar->GetLevel() - botCaster->GetLevel()) / 2; - if (tar->GetLevel() - botCaster->GetLevel() < 0) { + int level_mod = (tar->GetLevel() - caster->GetLevel())* (tar->GetLevel() - caster->GetLevel()) / 2; + if (tar->GetLevel() - caster->GetLevel() < 0) { level_mod = -level_mod; } - bool needsMagicResistDebuff = (tar->GetMR() + level_mod) > 100; - bool needsColdResistDebuff = (tar->GetCR() + level_mod) > 100; - bool needsFireResistDebuff = (tar->GetFR() + level_mod) > 100; - bool needsPoisonResistDebuff = (tar->GetPR() + level_mod) > 100; - bool needsDiseaseResistDebuff = (tar->GetDR() + level_mod) > 100; + bool needs_magic_resist_debuff = (tar->GetMR() + level_mod) > 100; + bool needs_cold_resist_debuff = (tar->GetCR() + level_mod) > 100; + bool needs_fire_resist_debuff = (tar->GetFR() + level_mod) > 100; + bool needs_poison_resist_debuff = (tar->GetPR() + level_mod) > 100; + bool needs_disease_resist_debuff = (tar->GetDR() + level_mod) > 100; - if (botCaster->AI_HasSpells()) { - std::vector botSpellList = botCaster->AIBot_spells; + if (caster->AI_HasSpells()) { + std::vector bot_spell_list = caster->BotGetSpellsByType(spell_type); - for (int i = botSpellList.size() - 1; i >= 0; i--) { - if (!IsValidSpell(botSpellList[i].spellid)) { - // this is both to quit early to save cpu and to avoid casting bad spells - // Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here + for (int i = bot_spell_list.size() - 1; i >= 0; i--) { + if (!IsValidSpellAndLoS(bot_spell_list[i].spellid, caster->HasLoS())) { continue; } - if (((botSpellList[i].type & SpellType_Debuff) || IsResistDebuffSpell(botSpellList[i].spellid)) - && ((needsMagicResistDebuff && (IsEffectInSpell(botSpellList[i].spellid, SE_ResistMagic)) || IsEffectInSpell(botSpellList[i].spellid, SE_ResistAll)) - || (needsColdResistDebuff && (IsEffectInSpell(botSpellList[i].spellid, SE_ResistCold)) || IsEffectInSpell(botSpellList[i].spellid, SE_ResistAll)) - || (needsFireResistDebuff && (IsEffectInSpell(botSpellList[i].spellid, SE_ResistFire)) || IsEffectInSpell(botSpellList[i].spellid, SE_ResistAll)) - || (needsPoisonResistDebuff && (IsEffectInSpell(botSpellList[i].spellid, SE_ResistPoison)) || IsEffectInSpell(botSpellList[i].spellid, SE_ResistAll)) - || (needsDiseaseResistDebuff && (IsEffectInSpell(botSpellList[i].spellid, SE_ResistDisease)) || IsEffectInSpell(botSpellList[i].spellid, SE_ResistAll))) - && (!tar->IsImmuneToSpell(botSpellList[i].spellid, botCaster) - && tar->CanBuffStack(botSpellList[i].spellid, botCaster->GetLevel(), true) >= 0) - && botCaster->CheckSpellRecastTimer(botSpellList[i].spellid)) { - result.SpellId = botSpellList[i].spellid; - result.SpellIndex = i; - result.ManaCost = botSpellList[i].manacost; + if ( + (bot_spell_list[i].type == BotSpellTypes::Debuff || IsResistDebuffSpell(bot_spell_list[i].spellid)) && + ( + (needs_magic_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistMagic) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) || + (needs_cold_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistCold) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) || + (needs_fire_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistFire) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) || + (needs_poison_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistPoison) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) || + (needs_disease_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistDisease) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) + ) && + !tar->IsImmuneToSpell(bot_spell_list[i].spellid, caster) && + tar->CanBuffStack(bot_spell_list[i].spellid, caster->GetLevel(), true) >= 0 && + caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) + ) { + result.SpellId = bot_spell_list[i].spellid; + result.SpellIndex = bot_spell_list[i].index; + result.ManaCost = bot_spell_list[i].manacost; break; } @@ -2746,109 +2003,81 @@ BotSpell Bot::GetBestBotSpellForResistDebuff(Bot* botCaster, Mob *tar) { return result; } -BotSpell Bot::GetBestBotSpellForCure(Bot* botCaster, Mob *tar) { +BotSpell Bot::GetBestBotSpellForCure(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell_wPriority result; - bool spellSelected = false; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; - if (!tar) + if (!tar) { return result; + } - int countNeedsCured = 0; - bool isPoisoned = tar->FindType(SE_PoisonCounter); - bool isDiseased = tar->FindType(SE_DiseaseCounter); - bool isCursed = tar->FindType(SE_CurseCounter); - bool isCorrupted = tar->FindType(SE_CorruptionCounter); - - if (botCaster && botCaster->AI_HasSpells()) { - std::list cureList = GetPrioritizedBotSpellsBySpellType(botCaster, SpellType_Cure); - - if (tar->HasGroup()) { - Group *g = tar->GetGroup(); + if (caster) { + std::vector bot_spell_list_itr = GetPrioritizedBotSpellsBySpellType(caster, spell_type, tar); + + if (IsGroupBotSpellType(spell_type)) { + int count_needs_cured = 0; + uint16 count_poisoned = 0; + uint16 count_diseased = 0; + uint16 count_cursed = 0; + uint16 count_corrupted = 0; - if (g) { - for( int i = 0; imembers[i] && !g->members[i]->qglobal) { - if (botCaster->GetNeedsCured(g->members[i])) - countNeedsCured++; - } + for (std::vector::iterator itr = bot_spell_list_itr.begin(); itr != bot_spell_list_itr.end(); ++itr) { + if (!IsValidSpell(itr->SpellId) || !IsGroupSpell(itr->SpellId)) { + continue; } - } - } - - //Check for group cure first - if (countNeedsCured > 2) { - for (std::list::iterator itr = cureList.begin(); itr != cureList.end(); ++itr) { - BotSpell selectedBotSpell = *itr; - - if (IsGroupSpell(itr->SpellId) && botCaster->CheckSpellRecastTimer(selectedBotSpell.SpellId)) { - if (selectedBotSpell.SpellId == 0) - continue; - if (isPoisoned && IsEffectInSpell(selectedBotSpell.SpellId, SE_PoisonCounter)) { - spellSelected = true; - } - else if (isDiseased && IsEffectInSpell(selectedBotSpell.SpellId, SE_DiseaseCounter)) { - spellSelected = true; - } - else if (isCursed && IsEffectInSpell(selectedBotSpell.SpellId, SE_CurseCounter)) { - spellSelected = true; - } - else if (isCorrupted && IsEffectInSpell(selectedBotSpell.SpellId, SE_CorruptionCounter)) { - spellSelected = true; - } - else if (IsEffectInSpell(selectedBotSpell.SpellId, SE_DispelDetrimental)) { - spellSelected = true; + for (Mob* m : (IsGroupBotSpellType(spell_type) ? caster->GetSpellTargetList() : caster->GetSpellTargetList(true))) { + if (caster->GetNeedsCured(m)) { + if (caster->CastChecks(itr->SpellId, m, spell_type, true, IsGroupBotSpellType(spell_type))) { + if (m->FindType(SE_PoisonCounter)) { + ++count_poisoned; + } + if (m->FindType(SE_DiseaseCounter)) { + ++count_diseased; + } + if (m->FindType(SE_CurseCounter)) { + ++count_cursed; + } + if (m->FindType(SE_CorruptionCounter)) { + ++count_corrupted; + } + } } + } - if (spellSelected) - { - result.SpellId = selectedBotSpell.SpellId; - result.SpellIndex = selectedBotSpell.SpellIndex; - result.ManaCost = selectedBotSpell.ManaCost; + if ( + (count_poisoned >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SE_PoisonCounter)) || + (count_diseased >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SE_DiseaseCounter)) || + (count_cursed >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SE_CurseCounter)) || + (count_corrupted >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SE_CorruptionCounter)) + ) { + result.SpellId = itr->SpellId; + result.SpellIndex = itr->SpellIndex; + result.ManaCost = itr->ManaCost; - break; - } + break; } } } + else { + for (std::vector::iterator itr = bot_spell_list_itr.begin(); itr != bot_spell_list_itr.end(); ++itr) { + if (!IsValidSpell(itr->SpellId) || IsGroupSpell(itr->SpellId)) { + continue; + } - //no group cure for target- try to find single target spell - if (!spellSelected) { - for(std::list::iterator itr = cureList.begin(); itr != cureList.end(); ++itr) { - BotSpell selectedBotSpell = *itr; - - if (botCaster->CheckSpellRecastTimer(selectedBotSpell.SpellId)) { - if (selectedBotSpell.SpellId == 0) - continue; - - if (isPoisoned && IsEffectInSpell(selectedBotSpell.SpellId, SE_PoisonCounter)) { - spellSelected = true; - } - else if (isDiseased && IsEffectInSpell(selectedBotSpell.SpellId, SE_DiseaseCounter)) { - spellSelected = true; - } - else if (isCursed && IsEffectInSpell(selectedBotSpell.SpellId, SE_CurseCounter)) { - spellSelected = true; - } - else if (isCorrupted && IsEffectInSpell(selectedBotSpell.SpellId, SE_CorruptionCounter)) { - spellSelected = true; - } - else if (IsEffectInSpell(selectedBotSpell.SpellId, SE_DispelDetrimental)) { - spellSelected = true; - } - - if (spellSelected) - { - result.SpellId = selectedBotSpell.SpellId; - result.SpellIndex = selectedBotSpell.SpellIndex; - result.ManaCost = selectedBotSpell.ManaCost; - - break; - } + if ( + tar->FindType(SE_PoisonCounter) && IsEffectInSpell(itr->SpellId, SE_PoisonCounter) || + tar->FindType(SE_DiseaseCounter) && IsEffectInSpell(itr->SpellId, SE_DiseaseCounter) || + tar->FindType(SE_CurseCounter) && IsEffectInSpell(itr->SpellId, SE_CurseCounter) || + tar->FindType(SE_CorruptionCounter) && IsEffectInSpell(itr->SpellId, SE_CorruptionCounter) + ) { + result.SpellId = itr->SpellId; + result.SpellIndex = itr->SpellIndex; + result.ManaCost = itr->ManaCost; + break; } } } @@ -2857,108 +2086,84 @@ BotSpell Bot::GetBestBotSpellForCure(Bot* botCaster, Mob *tar) { return result; } -uint8 Bot::GetChanceToCastBySpellType(uint32 spellType) +uint8 Bot::GetChanceToCastBySpellType(uint16 spell_type) { - uint8 spell_type_index = SPELL_TYPE_COUNT; - switch (spellType) { - case SpellType_Nuke: - spell_type_index = spellTypeIndexNuke; - break; - case SpellType_Heal: - spell_type_index = spellTypeIndexHeal; - break; - case SpellType_Root: - spell_type_index = spellTypeIndexRoot; - break; - case SpellType_Buff: - spell_type_index = spellTypeIndexBuff; - break; - case SpellType_Escape: - spell_type_index = spellTypeIndexEscape; - break; - case SpellType_Pet: - spell_type_index = spellTypeIndexPet; - break; - case SpellType_Lifetap: - spell_type_index = spellTypeIndexLifetap; - break; - case SpellType_Snare: - spell_type_index = spellTypeIndexSnare; - break; - case SpellType_DOT: - spell_type_index = spellTypeIndexDot; - break; - case SpellType_Dispel: - spell_type_index = spellTypeIndexDispel; - break; - case SpellType_InCombatBuff: - spell_type_index = spellTypeIndexInCombatBuff; - break; - case SpellType_Mez: - spell_type_index = spellTypeIndexMez; - break; - case SpellType_Charm: - spell_type_index = spellTypeIndexCharm; - break; - case SpellType_Slow: - spell_type_index = spellTypeIndexSlow; - break; - case SpellType_Debuff: - spell_type_index = spellTypeIndexDebuff; - break; - case SpellType_Cure: - spell_type_index = spellTypeIndexCure; - break; - case SpellType_Resurrect: - spell_type_index = spellTypeIndexResurrect; - break; - case SpellType_HateRedux: - spell_type_index = spellTypeIndexHateRedux; - break; - case SpellType_InCombatBuffSong: - spell_type_index = spellTypeIndexInCombatBuffSong; - break; - case SpellType_OutOfCombatBuffSong: - spell_type_index = spellTypeIndexOutOfCombatBuffSong; - break; - case SpellType_PreCombatBuff: - spell_type_index = spellTypeIndexPreCombatBuff; - break; - case SpellType_PreCombatBuffSong: - spell_type_index = spellTypeIndexPreCombatBuffSong; - break; - default: - break; - } - - if (spell_type_index >= SPELL_TYPE_COUNT) - return 0; - - uint8 class_index = GetClass(); - if (class_index > Class::Berserker || class_index < Class::Warrior) - return 0; - --class_index; - - uint32 stance_id = GetBotStance(); - if (!Stance::IsValid(stance_id)) { - return 0; - } - - uint8 stance_index = Stance::GetIndex(stance_id); - uint8 type_index = nHSND; - - if (HasGroup()) { - if (IsGroupHealer()/* || IsRaidHealer()*/) - type_index |= pH; - if (IsGroupSlower()/* || IsRaidSlower()*/) - type_index |= pS; - if (IsGroupNuker()/* || IsRaidNuker()*/) - type_index |= pN; - if (IsGroupDoter()/* || IsRaidDoter()*/) - type_index |= pD; - } - - return database.botdb.GetSpellCastingChance(spell_type_index, class_index, stance_index, type_index); + switch (spell_type) { + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::AEStun: + case BotSpellTypes::AESnare: + case BotSpellTypes::AEMez: + case BotSpellTypes::AESlow: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::AEFear: + case BotSpellTypes::AEDispel: + case BotSpellTypes::AEDoT: + case BotSpellTypes::AELifetap: + case BotSpellTypes::AERoot: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::AEHateLine: + return RuleI(Bots, PercentChanceToCastAEs); + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::GroupHoTHeals: + return RuleI(Bots, PercentChanceToCastGroupHeal); + case BotSpellTypes::Nuke: + return RuleI(Bots, PercentChanceToCastNuke); + case BotSpellTypes::Root: + return RuleI(Bots, PercentChanceToCastRoot); + case BotSpellTypes::Buff: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::DamageShields: + case BotSpellTypes::PetDamageShields: + return RuleI(Bots, PercentChanceToCastBuff); + case BotSpellTypes::Escape: + return RuleI(Bots, PercentChanceToCastEscape); + case BotSpellTypes::Lifetap: + return RuleI(Bots, PercentChanceToCastLifetap); + case BotSpellTypes::Snare: + return RuleI(Bots, PercentChanceToCastSnare); + case BotSpellTypes::DOT: + return RuleI(Bots, PercentChanceToCastDOT); + case BotSpellTypes::Dispel: + return RuleI(Bots, PercentChanceToCastDispel); + case BotSpellTypes::InCombatBuff: + return RuleI(Bots, PercentChanceToCastInCombatBuff); + case BotSpellTypes::HateLine: + return RuleI(Bots, PercentChanceToCastHateLine); + case BotSpellTypes::Mez: + return RuleI(Bots, PercentChanceToCastMez); + case BotSpellTypes::Slow: + return RuleI(Bots, PercentChanceToCastSlow); + case BotSpellTypes::Debuff: + return RuleI(Bots, PercentChanceToCastDebuff); + case BotSpellTypes::Cure: + case BotSpellTypes::PetCures: + return RuleI(Bots, PercentChanceToCastCure); + case BotSpellTypes::GroupCures: + return RuleI(Bots, PercentChanceToCastGroupCure); + case BotSpellTypes::HateRedux: + return RuleI(Bots, PercentChanceToCastHateRedux); + case BotSpellTypes::Fear: + return RuleI(Bots, PercentChanceToCastFear); + case BotSpellTypes::RegularHeal: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + return RuleI(Bots, PercentChanceToCastHeal); + default: + return RuleI(Bots, PercentChanceToCastOtherType); + } + + return RuleI(Bots, PercentChanceToCastOtherType); } bool Bot::AI_AddBotSpells(uint32 bot_spell_id) { @@ -2966,6 +2171,8 @@ bool Bot::AI_AddBotSpells(uint32 bot_spell_id) { npc_spells_id = bot_spell_id; AIBot_spells.clear(); AIBot_spells_enforced.clear(); + AIBot_spells_by_type.clear(); + if (!bot_spell_id) { AIautocastspell_timer->Disable(); return false; @@ -3217,7 +2424,7 @@ bool Bot::AI_AddBotSpells(uint32 bot_spell_id) { } } - std::sort(AIBot_spells.begin(), AIBot_spells.end(), [](const BotSpells_Struct& a, const BotSpells_Struct& b) { + std::sort(AIBot_spells.begin(), AIBot_spells.end(), [](const BotSpells& a, const BotSpells& b) { return a.priority > b.priority; }); @@ -3256,6 +2463,7 @@ bool Bot::AI_AddBotSpells(uint32 bot_spell_id) { AIautocastspell_timer->Disable(); } else { AIautocastspell_timer->Trigger(); + AssignBotSpellsToTypes(AIBot_spells, AIBot_spells_by_type); // Assign AIBot_spells to AIBot_spells_by_type with an index } return true; } @@ -3340,7 +2548,7 @@ DBbotspells_Struct* ZoneDatabase::GetBotSpells(uint32 bot_spell_id) entry.bucket_comparison = e.bucket_comparison; // some spell types don't make much since to be priority 0, so fix that - if (!(entry.type & SPELL_TYPES_INNATE) && entry.priority == 0) { + if (!IsBotSpellTypeInnate(entry.type) && entry.priority == 0) { entry.priority = 1; } @@ -3383,7 +2591,7 @@ void Bot::AddSpellToBotList( } HasAISpell = true; - BotSpells_Struct t; + BotSpells t; t.priority = in_priority; t.spellid = in_spell_id; @@ -3429,22 +2637,22 @@ void Bot::AddSpellToBotEnforceList( } HasAISpell = true; - BotSpells_Struct t; - - t.priority = iPriority; - t.spellid = iSpellID; - t.type = iType; - t.manacost = iManaCost; - t.recast_delay = iRecastDelay; - t.time_cancast = 0; - t.resist_adjust = iResistAdjust; - t.minlevel = min_level; - t.maxlevel = maxlevel; - t.min_hp = min_hp; - t.max_hp = max_hp; - t.bucket_name = bucket_name; - t.bucket_value = bucket_value; - t.bucket_comparison = bucket_comparison; + BotSpells t; + + t.priority = iPriority; + t.spellid = iSpellID; + t.type = iType; + t.manacost = iManaCost; + t.recast_delay = iRecastDelay; + t.time_cancast = 0; + t.resist_adjust = iResistAdjust; + t.minlevel = min_level; + t.maxlevel = maxlevel; + t.min_hp = min_hp; + t.max_hp = max_hp; + t.bucket_name = bucket_name; + t.bucket_value = bucket_value; + t.bucket_comparison = bucket_comparison; AIBot_spells_enforced.push_back(t); } @@ -3477,7 +2685,7 @@ void Bot::AI_Bot_Event_SpellCastFinished(bool iCastSucceeded, uint16 slot) { } } -bool Bot::HasBotSpellEntry(uint16 spellid) { +bool Bot::HasBotSpellEntry(uint16 spell_id) { auto* spell_list = content_db.GetBotSpells(GetBotSpellID()); if (!spell_list) { @@ -3486,7 +2694,7 @@ bool Bot::HasBotSpellEntry(uint16 spellid) { // Check if Spell ID is found in Bot Spell Entries for (auto& e : spell_list->entries) { - if (spellid == e.spellid) { + if (spell_id == e.spellid) { return true; } } @@ -3494,16 +2702,342 @@ bool Bot::HasBotSpellEntry(uint16 spellid) { return false; } -bool Bot::IsValidSpellRange(uint16 spell_id, Mob const* tar) { - if (!IsValidSpell(spell_id)) { +bool Bot::CanUseBotSpell(uint16 spell_id) { + if (AIBot_spells.empty()) { return false; } - if (tar) { - int spellrange = (GetActSpellRange(spell_id, spells[spell_id].range) * GetActSpellRange(spell_id, spells[spell_id].range)); - if (spellrange >= DistanceSquared(m_Position, tar->GetPosition())) { - return true; + for (const auto& s : AIBot_spells) { + if (!IsValidSpell(s.spellid)) { + return false; + } + + if (s.spellid != spell_id) { + continue; + } + + if (s.minlevel > GetLevel()) { + return false; } + + return true; } + return false; } + +bool Bot::IsValidSpellRange(uint16 spell_id, Mob* tar) { + if (!IsValidSpell(spell_id) || !tar) { + return false; + } + + float range = spells[spell_id].range + GetRangeDistTargetSizeMod(tar); + + if (IsAnyAESpell(spell_id)) { + range = GetAOERange(spell_id); + } + + if (RuleB(Bots, EnableBotTGB) && IsTGBCompatibleSpell(spell_id) && IsGroupSpell(spell_id)) { + range = spells[spell_id].aoe_range; + } + + range = GetActSpellRange(spell_id, range); + + if (HasProjectIllusion() && IsIllusionSpell(spell_id)) { + range = 100; + } + + float dist2 = DistanceSquared(m_Position, tar->GetPosition()); + float range2 = range * range; + float min_range2 = spells[spell_id].min_range * spells[spell_id].min_range; + + if (dist2 > range2) { + //target is out of range. + return false; + } + else if (dist2 < min_range2) { + //target is too close range. + return false; + } + + return true; +} + +BotSpell Bot::GetBestBotSpellForNukeByBodyType(Bot* caster, uint8 body_type, uint16 spell_type, bool AE, Mob* tar) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (!caster || !body_type) { + return result; + } + + if (tar == nullptr) { + tar = caster->GetTarget(); + } + + switch (body_type) { + case BodyType::Undead: + case BodyType::SummonedUndead: + case BodyType::Vampire: + result = GetBestBotSpellForNukeByTargetType(caster, (!AE ? ST_Undead : ST_UndeadAE), spell_type, AE, tar); + break; + case BodyType::Summoned: + case BodyType::Summoned2: + case BodyType::Summoned3: + result = GetBestBotSpellForNukeByTargetType(caster, (!AE ? ST_Summoned : ST_SummonedAE), spell_type, AE, tar); + break; + case BodyType::Animal: + result = GetBestBotSpellForNukeByTargetType(caster, ST_Animal, spell_type, AE, tar); + break; + case BodyType::Plant: + result = GetBestBotSpellForNukeByTargetType(caster, ST_Plant, spell_type, AE, tar); + break; + case BodyType::Giant: + result = GetBestBotSpellForNukeByTargetType(caster, ST_Giant, spell_type, AE, tar); + break; + case BodyType::Dragon: + result = GetBestBotSpellForNukeByTargetType(caster, ST_Dragon, spell_type, AE, tar); + break; + default: + break; + } + + return result; +} + +BotSpell Bot::GetBestBotSpellForRez(Bot* caster, Mob* target, uint16 spell_type) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_Revive); + + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { + // Assuming all the spells have been loaded into this list by level and in descending order + if ( + IsResurrectSpell(bot_spell_list_itr->SpellId) && + caster->CheckSpellRecastTimer(bot_spell_list_itr->SpellId) + ) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + + break; + } + } + } + + return result; +} + +BotSpell Bot::GetBestBotSpellForCharm(Bot* caster, Mob* target, uint16 spell_type) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (caster) { + std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_Charm); + + for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { + // Assuming all the spells have been loaded into this list by level and in descending order + if ( + IsCharmSpell(bot_spell_list_itr->SpellId) && + caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) + ) { + result.SpellId = bot_spell_list_itr->SpellId; + result.SpellIndex = bot_spell_list_itr->SpellIndex; + result.ManaCost = bot_spell_list_itr->ManaCost; + + break; + } + } + } + + return result; +} + + +void Bot::CheckBotSpells() { + auto spell_list = BotSpellsEntriesRepository::All(content_db); + uint16 spell_id; + SPDat_Spell_Struct spell; + uint16 correct_type; + uint16 parent_type; + + for (const auto& s : spell_list) { + if (!IsValidSpell(s.spell_id)) { + LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id); + continue; + } + + spell = spells[s.spell_id]; + spell_id = spell.id; + + if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] >= 255) { + LogBotSpellTypeChecks("{} [#{}] is not usable by a {} [#{}].", GetSpellName(spell_id), spell_id, GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX), s.npc_spells_id); + } + else { + if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] > s.minlevel) { + LogBotSpellTypeChecks("{} [#{}] is not usable until level {} for a {} [#{}] and the min level is currently set to {}." + , GetSpellName(spell_id) + , spell_id + , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + , s.minlevel + ); + + LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `minlevel` = {} WHERE `spellid` = {} AND `npc_spells_id` = {}; -- {} [#{}] from minlevel {} to {} for {} [#{}]" + , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] + , spell_id + , s.npc_spells_id + , GetSpellName(spell_id) + , spell_id + , s.minlevel + , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + ); + } + + if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] < s.minlevel) { + LogBotSpellTypeChecks("{} [#{}] could be used starting at level {} for a {} [#{}] instead of the current min level of {}." + , GetSpellName(spell_id) + , spell_id + , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + , s.minlevel + ); + + LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `minlevel` = {} WHERE `spellid` = {} AND `npc_spells_id` = {}; -- {} [#{}] from minlevel {} to {} for {} [#{}]" + , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] + , spell_id + , s.npc_spells_id + , GetSpellName(spell_id) + , spell_id + , s.minlevel + , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + ); + } + + + if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] > s.maxlevel) { + LogBotSpellTypeChecks("{} [#{}] is not usable until level {} for a {} [#{}] and the max level is currently set to {}." + , GetSpellName(spell_id) + , spell_id + , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + , s.maxlevel + ); + } + } + + correct_type = GetCorrectBotSpellType(s.type, spell_id); + parent_type = GetParentSpellType(correct_type); + + if (RuleB(Bots, UseParentSpellTypeForChecks)) { + if (s.type == parent_type || s.type == correct_type) { + continue; + } + } + else { + if (IsPetBotSpellType(s.type)) { + correct_type = GetPetBotSpellType(correct_type); + } + } + + if (correct_type == s.type) { + continue; + } + + if (correct_type == UINT16_MAX) { + LogBotSpellTypeChecks("{} [#{}] is incorrect. It is currently set as {} [#{}] but the correct type is unknown." + , GetSpellName(spell_id) + , spell_id + , GetSpellTypeNameByID(s.type) + , s.type + ); + } + else { + LogBotSpellTypeChecks("{} [#{}] is incorrect. It is currently set as {} [#{}] and should be {} [#{}]" + , GetSpellName(spell_id) + , spell_id + , GetSpellTypeNameByID(s.type) + , s.type + , GetSpellTypeNameByID(correct_type) + , correct_type + ); + LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `type` = {} WHERE `spell_id` = {}; -- {} [#{}] from {} [#{}] to {} [#{}]" + , correct_type + , spell_id + , GetSpellName(spell_id) + , spell_id + , GetSpellTypeNameByID(s.type) + , s.type + , GetSpellTypeNameByID(correct_type) + , correct_type + ); + } + } +} + +void Bot::MapSpellTypeLevels() { + commanded_spells_min_level.clear(); + + auto start = std::min({ BotSpellTypes::START, BotSpellTypes::COMMANDED_START, BotSpellTypes::DISCIPLINE_START }); + auto end = std::max({ BotSpellTypes::END, BotSpellTypes::COMMANDED_END, BotSpellTypes::DISCIPLINE_END }); + + for (int i = start; i <= end; ++i) { + if (!Bot::IsValidBotSpellType(i)) { + continue; + } + + for (int x = Class::Warrior; x <= Class::Berserker; ++x) { + commanded_spells_min_level[i][x] = { UINT8_MAX, "" }; + } + } + + auto spell_list = BotSpellsEntriesRepository::All(content_db); + + for (const auto& s : spell_list) { + if (!IsValidSpell(s.spell_id)) { + LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id); + continue; + } + + uint16_t spell_type = s.type; + int32_t bot_class = s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX; + uint8_t min_level = s.minlevel; + + if ( + !EQ::ValueWithin(bot_class, Class::Warrior, Class::Berserker) || + !Bot::IsValidBotSpellType(spell_type) + ) { + continue; + } + + auto& spell_info = commanded_spells_min_level[spell_type][bot_class]; + + if (min_level < spell_info.min_level) { + spell_info.min_level = min_level; + spell_info.description = StringFormat( + "%s [#%u]: Level %u", + GetClassIDName(bot_class), + bot_class, + min_level + ); + } + } +} diff --git a/zone/client.cpp b/zone/client.cpp index 1c577c08d2..45e7d39ffd 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -51,6 +51,7 @@ extern volatile bool RunLoops; #include "water_map.h" #include "bot_command.h" #include "string_ids.h" +#include "dialogue_window.h" #include "guild_mgr.h" #include "quest_parser_collection.h" @@ -148,6 +149,7 @@ Client::Client(EQStreamInterface *ieqs) : Mob( ), hpupdate_timer(2000), camp_timer(29000), + bot_camp_timer((RuleI(Bots, CampTimer) * 1000)), process_timer(100), consume_food_timer(CONSUMPTION_TIMER), zoneinpacket_timer(1000), @@ -252,6 +254,7 @@ Client::Client(EQStreamInterface *ieqs) : Mob( fishing_timer.Disable(); dead_timer.Disable(); camp_timer.Disable(); + bot_camp_timer.Disable(); autosave_timer.Disable(); GetMercTimer()->Disable(); instalog = false; @@ -777,6 +780,10 @@ bool Client::Save(uint8 iCommitNow) { database.SaveCharacterEXPModifier(this); + if (RuleB(Bots, Enabled)) { + database.botdb.SaveBotSettings(this); + } + return true; } @@ -5950,7 +5957,7 @@ void Client::SuspendMinion(int value) { m_suspendedminion.SpellID = SpellID; - m_suspendedminion.HP = CurrentPet->GetHP();; + m_suspendedminion.HP = CurrentPet->GetHP(); m_suspendedminion.Mana = CurrentPet->GetMana(); m_suspendedminion.petpower = CurrentPet->GetPetPower(); @@ -8802,10 +8809,10 @@ void Client::ProcessAggroMeter() // probably should have PVP rules ... if (cur_tar && cur_tar != this) { - if (cur_tar->IsNPC() && !cur_tar->IsPetOwnerClient() && cur_tar->GetID() != m_aggrometer.get_target_id()) { + if (cur_tar->IsNPC() && !cur_tar->IsPetOwnerOfClientBot() && cur_tar->GetID() != m_aggrometer.get_target_id()) { m_aggrometer.set_target_id(cur_tar->GetID()); send_targetinfo = true; - } else if ((cur_tar->IsPetOwnerClient() || cur_tar->IsClient()) && cur_tar->GetTarget() && cur_tar->GetTarget()->GetID() != m_aggrometer.get_target_id()) { + } else if ((cur_tar->IsPetOwnerOfClientBot() || cur_tar->IsClient()) && cur_tar->GetTarget() && cur_tar->GetTarget()->GetID() != m_aggrometer.get_target_id()) { m_aggrometer.set_target_id(cur_tar->GetTarget()->GetID()); send_targetinfo = true; } @@ -12933,11 +12940,11 @@ void Client::AddMoneyToPPWithOverflow(uint64 copper, bool update_client) SaveCurrency(); LogDebug("Client::AddMoneyToPPWithOverflow() [{}] should have: plat:[{}] gold:[{}] silver:[{}] copper:[{}]", - GetName(), - m_pp.platinum, - m_pp.gold, - m_pp.silver, - m_pp.copper + GetName(), + m_pp.platinum, + m_pp.gold, + m_pp.silver, + m_pp.copper ); } @@ -13120,8 +13127,8 @@ void Client::ClientToNpcAggroProcess() { if (zone->CanDoCombat() && !GetFeigned() && m_client_npc_aggro_scan_timer.Check()) { int npc_scan_count = 0; - for (auto &close_mob: GetCloseMobList()) { - Mob *mob = close_mob.second; + for (auto& close_mob : GetCloseMobList()) { + Mob* mob = close_mob.second; if (!mob) { continue; } @@ -13213,6 +13220,144 @@ void Client::ShowZoneShardMenu() } } +std::string Client::SendBotCommandHelpWindow(const BotCommandHelpParams& params) { + unsigned string_length = 0; + unsigned current_place = 0; + uint16 max_length = RuleI(Command, MaxHelpLineLength); // Line length before splitting + const std::string& header_color = "indian_red"; + const std::string& description_color = "light_grey"; + const std::string& description_color_secondary = "dark_orange"; + const std::string& example_color = "goldenrod"; + const std::string& example_color_secondary = "slate_blue"; + const std::string& option_color = "light_grey"; + const std::string& option_color_secondary = "slate_blue"; + const std::string& filler_line_color = "dark_grey"; + + std::string filler_line = "--------------------------------------------------------------------"; + std::string filler_dia = DialogueWindow::TableRow(DialogueWindow::TableCell(fmt::format("{}", DialogueWindow::ColorMessage(filler_line_color, filler_line)))); + std::string break_line = DialogueWindow::Break(); + std::string popup_text; + + if (!params.description.empty()) { + popup_text += GetCommandHelpHeader("[Description]", header_color); + popup_text += SplitCommandHelpText(params.description, description_color, max_length); + } + + if (!params.notes.empty()) { + popup_text += break_line + break_line; + popup_text += GetCommandHelpHeader("[Notes]", header_color); + popup_text += SplitCommandHelpText(params.notes, description_color_secondary, max_length); + } + + if (!params.example_format.empty()) { + popup_text += filler_dia; + popup_text += GetCommandHelpHeader("[Examples]", header_color); + popup_text += SplitCommandHelpText(params.example_format, example_color, max_length); + } + + if (!params.examples_one.empty()) { + popup_text += break_line + break_line; + popup_text += SplitCommandHelpText(params.examples_one, example_color, max_length, example_color_secondary); + } + + if (!params.examples_two.empty()) { + popup_text += SplitCommandHelpText(params.examples_two, example_color, max_length, example_color_secondary); + } + + if (!params.examples_three.empty()) { + popup_text += SplitCommandHelpText(params.examples_three, example_color, max_length, example_color_secondary); + } + + if (!params.options.empty()) { + popup_text += filler_dia; + popup_text += GetCommandHelpHeader("[Options]", header_color); + popup_text += SplitCommandHelpText(params.options, option_color, max_length); + } + + if (!params.options_one.empty()) { + popup_text += break_line + break_line; + popup_text += SplitCommandHelpText(params.options_one, option_color_secondary, max_length); + } + + if (!params.options_two.empty()) { + popup_text += SplitCommandHelpText(params.options_two, option_color_secondary, max_length); + } + + if (!params.options_three.empty()) { + popup_text += SplitCommandHelpText(params.options_three, option_color_secondary, max_length); + } + + if (!params.actionables.empty()) { + popup_text += filler_dia; + popup_text += GetCommandHelpHeader("[Actionables]", header_color); + popup_text += SplitCommandHelpText(params.actionables, description_color, max_length); + } + + popup_text = DialogueWindow::Table(popup_text); + + return popup_text; +} + +std::string Client::GetCommandHelpHeader(std::string msg, std::string color) { + std::string return_text = DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(color, msg) + ) + ) + ); + + return return_text; +} + +std::string Client::SplitCommandHelpText(std::vector msg, std::string color, uint16 max_length, std::string secondary_color) { + std::string return_text; + + for (int i = 0; i < msg.size(); i++) { + std::vector msg_split; + int string_length = msg[i].length() + 1; + int end_count = 0; + int new_count = 0; + int split_count = 0; + + for (int x = 0; x < string_length; x = end_count) { + end_count = std::min(int(string_length), (int(x) + std::min(int(string_length), int(max_length)))); + + if ((string_length - (x + 1)) > max_length) { + for (int y = end_count; y >= x; --y) { + if (msg[i][y] == ' ') { + split_count = y - x; + msg_split.emplace_back(msg[i].substr(x, split_count)); + end_count = y + 1; + + break; + } + + if (y == x) { + msg_split.emplace_back(msg[i].substr(x, max_length)); + + break; + } + } + } + else { + msg_split.emplace_back(msg[i].substr(x, (string_length - 1) - x)); + + break; + } + } + + for (const auto& s : msg_split) { + return_text += DialogueWindow::TableRow( + DialogueWindow::TableCell(DialogueWindow::ColorMessage(((!secondary_color.empty() && i== 0) ? secondary_color : color), s)) + ); + } + } + + return return_text; +} + void Client::SetAAEXPPercentage(uint8 percentage) { const uint32 before_percentage = m_epp.perAA; diff --git a/zone/client.h b/zone/client.h index 5ce9df4d89..cc1378319c 100644 --- a/zone/client.h +++ b/zone/client.h @@ -74,6 +74,8 @@ namespace EQ #include "../common/repositories/buyer_buy_lines_repository.h" #include "../common/repositories/character_evolving_items_repository.h" +#include "bot_structs.h" + #ifdef _WINDOWS // since windows defines these within windef.h (which windows.h include) // we are required to undefine these to use min and max from @@ -197,6 +199,19 @@ struct RespawnOption float heading; }; +struct BotCommandHelpParams { + std::vector description = {}; + std::vector notes = {}; + std::vector example_format = {}; + std::vector examples_one = {}; + std::vector examples_two = {}; + std::vector examples_three = {}; + std::vector actionables = {}; + std::vector options = {}; + std::vector options_one = {}; + std::vector options_two = {}; + std::vector options_three = {}; +}; // do not ask what all these mean because I have no idea! // named from the client's CEverQuest::GetInnateDesc, they're missing some @@ -1264,6 +1279,12 @@ class Client : public Mob PendingTranslocate_Struct PendingTranslocateData; void SendOPTranslocateConfirm(Mob *Caster, uint16 SpellID); + // Help Window + std::string SendBotCommandHelpWindow(const BotCommandHelpParams& params); + std::string GetCommandHelpHeader(std::string msg, std::string color); + std::string SplitCommandHelpText(std::vector msg, std::string color, uint16 max_length, std::string secondary_color = ""); + void SendSpellTypePrompts(bool commanded_types = false, bool client_only_types = false); + // Task System Methods void LoadClientTaskState(); void RemoveClientTaskState(); @@ -2059,6 +2080,7 @@ class Client : public Mob PTimerList p_timers; //persistent timers Timer hpupdate_timer; Timer camp_timer; + Timer bot_camp_timer; Timer process_timer; Timer consume_food_timer; Timer zoneinpacket_timer; @@ -2243,6 +2265,8 @@ class Client : public Mob bool GetBotPulling() { return m_bot_pulling; } void SetBotPulling(bool flag = true) { m_bot_pulling = flag; } + uint32 GetAssistee() { return bot_assistee; } + void SetAssistee(uint32 id = 0) { bot_assistee = id; } bool GetBotPrecombat() { return m_bot_precombat; } void SetBotPrecombat(bool flag = true) { m_bot_precombat = flag; } @@ -2257,10 +2281,33 @@ class Client : public Mob void CampAllBots(uint8 class_id = Class::None); void SpawnRaidBotsOnConnect(Raid* raid); + void LoadDefaultBotSettings(); + int GetDefaultBotSettings(uint8 setting_type, uint16 bot_setting); + int GetBotSetting(uint8 setting_type, uint16 bot_setting); + void SetBotSetting(uint8 setting_type, uint16 bot_setting, uint32 setting_value); + + uint16 GetDefaultSpellTypeDelay(uint16 spell_type); + uint8 GetDefaultSpellTypeMinThreshold(uint16 spell_type); + uint8 GetDefaultSpellTypeMaxThreshold(uint16 spell_type); + inline uint16 GetSpellTypeDelay(uint16 spell_type) const { return m_bot_spell_settings[spell_type].delay; } + inline void SetSpellTypeDelay(uint16 spell_type, uint16 delay_value) { m_bot_spell_settings[spell_type].delay = delay_value; } + inline uint8 GetSpellTypeMinThreshold(uint16 spell_type) const { return m_bot_spell_settings[spell_type].min_threshold; } + inline void SetSpellTypeMinThreshold(uint16 spell_type, uint8 threshold_value) { m_bot_spell_settings[spell_type].min_threshold = threshold_value; } + inline uint8 GetSpellTypeMaxThreshold(uint16 spell_type) const { return m_bot_spell_settings[spell_type].max_threshold; } + inline void SetSpellTypeMaxThreshold(uint16 spell_type, uint8 threshold_value) { m_bot_spell_settings[spell_type].max_threshold = threshold_value; } + inline bool SpellTypeRecastCheck(uint16 spellType) { return !m_bot_spell_settings[spellType].recast_timer.GetRemainingTime(); } + void SetSpellTypeRecastTimer(uint16 spell_type, uint32 recast_time) { m_bot_spell_settings[spell_type].recast_timer.Start(recast_time); } + + void SetIllusionBlock(bool value) { _illusionBlock = value; } + bool GetIllusionBlock() const override { return _illusionBlock; } + private: bool bot_owner_options[_booCount]; bool m_bot_pulling; bool m_bot_precombat; + uint32 bot_assistee; + std::vector m_bot_spell_settings; + bool _illusionBlock; bool CanTradeFVNoDropItem(); void SendMobPositions(); diff --git a/zone/client_bot.cpp b/zone/client_bot.cpp index 1f3f809dd0..8e1f07c046 100644 --- a/zone/client_bot.cpp +++ b/zone/client_bot.cpp @@ -15,10 +15,13 @@ void Client::SetBotOption(BotOwnerOption boo, bool flag) { } } -uint32 Client::GetBotCreationLimit(uint8 class_id) -{ +uint32 Client::GetBotCreationLimit(uint8 class_id) { uint32 bot_creation_limit = RuleI(Bots, CreationLimit); + if (Admin() >= RuleI(Bots, MinStatusToBypassCreateLimit)) { + return RuleI(Bots, MinStatusBypassCreateLimit); + } + const auto bucket_name = fmt::format( "bot_creation_limit{}", ( @@ -39,8 +42,7 @@ uint32 Client::GetBotCreationLimit(uint8 class_id) return bot_creation_limit; } -int Client::GetBotRequiredLevel(uint8 class_id) -{ +int Client::GetBotRequiredLevel(uint8 class_id) { int bot_character_level = RuleI(Bots, BotCharacterLevel); const auto bucket_name = fmt::format( @@ -63,10 +65,13 @@ int Client::GetBotRequiredLevel(uint8 class_id) return bot_character_level; } -int Client::GetBotSpawnLimit(uint8 class_id) -{ +int Client::GetBotSpawnLimit(uint8 class_id) { int bot_spawn_limit = RuleI(Bots, SpawnLimit); + if (Admin() >= RuleI(Bots, MinStatusToBypassSpawnLimit)) { + return RuleI(Bots, MinStatusBypassSpawnLimit); + } + const auto bucket_name = fmt::format( "bot_spawn_limit{}", ( @@ -80,9 +85,21 @@ int Client::GetBotSpawnLimit(uint8 class_id) ); auto bucket_value = GetBucket(bucket_name); + + if (class_id && !bot_spawn_limit && bucket_value.empty()) { + const auto new_bucket_name = "bot_spawn_limit"; + + bucket_value = GetBucket(new_bucket_name); + + if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) { + bot_spawn_limit = Strings::ToInt(bucket_value); + + return bot_spawn_limit; + } + } + if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) { bot_spawn_limit = Strings::ToInt(bucket_value); - return bot_spawn_limit; } if (RuleB(Bots, QuestableSpawnLimit)) { @@ -93,6 +110,7 @@ int Client::GetBotSpawnLimit(uint8 class_id) ); auto results = database.QueryDatabase(query); // use 'database' for non-bot table calls + if (!results.Success() || !results.RowCount()) { return bot_spawn_limit; } @@ -101,11 +119,56 @@ int Client::GetBotSpawnLimit(uint8 class_id) bot_spawn_limit = Strings::ToInt(row[0]); } + const auto& zones_list = Strings::Split(RuleS(Bots, ZonesWithSpawnLimits), ","); + const auto& zones_limits_list = Strings::Split(RuleS(Bots, ZoneSpawnLimits), ","); + int i = 0; + + for (const auto& result : zones_list) { + try { + if ( + std::stoul(result) == zone->GetZoneID() && + std::stoul(zones_limits_list[i]) < bot_spawn_limit + ) { + bot_spawn_limit = std::stoul(zones_limits_list[i]); + + break; + } + + ++i; + } + + catch (const std::exception& e) { + LogInfo("Invalid entry in Rule VegasScaling:SpecialScalingZones or SpecialScalingZonesVersions: [{}]", e.what()); + } + } + + const auto& zones_forced_list = Strings::Split(RuleS(Bots, ZonesWithForcedSpawnLimits), ","); + const auto& zones_forced_limits_list = Strings::Split(RuleS(Bots, ZoneForcedSpawnLimits), ","); + i = 0; + + for (const auto& result : zones_forced_list) { + try { + if ( + std::stoul(result) == zone->GetZoneID() && + std::stoul(zones_forced_limits_list[i]) != bot_spawn_limit + ) { + bot_spawn_limit = std::stoul(zones_forced_limits_list[i]); + + break; + } + + ++i; + } + + catch (const std::exception& e) { + LogInfo("Invalid entry in Rule VegasScaling:SpecialScalingZones or SpecialScalingZonesVersions: [{}]", e.what()); + } + } + return bot_spawn_limit; } -void Client::SetBotCreationLimit(uint32 new_creation_limit, uint8 class_id) -{ +void Client::SetBotCreationLimit(uint32 new_creation_limit, uint8 class_id) { const auto bucket_name = fmt::format( "bot_creation_limit{}", ( @@ -121,8 +184,7 @@ void Client::SetBotCreationLimit(uint32 new_creation_limit, uint8 class_id) SetBucket(bucket_name, std::to_string(new_creation_limit)); } -void Client::SetBotRequiredLevel(int new_required_level, uint8 class_id) -{ +void Client::SetBotRequiredLevel(int new_required_level, uint8 class_id) { const auto bucket_name = fmt::format( "bot_required_level{}", ( @@ -138,8 +200,7 @@ void Client::SetBotRequiredLevel(int new_required_level, uint8 class_id) SetBucket(bucket_name, std::to_string(new_required_level)); } -void Client::SetBotSpawnLimit(int new_spawn_limit, uint8 class_id) -{ +void Client::SetBotSpawnLimit(int new_spawn_limit, uint8 class_id) { const auto bucket_name = fmt::format( "bot_spawn_limit{}", ( @@ -155,7 +216,219 @@ void Client::SetBotSpawnLimit(int new_spawn_limit, uint8 class_id) SetBucket(bucket_name, std::to_string(new_spawn_limit)); } -void Client::CampAllBots(uint8 class_id) -{ +void Client::CampAllBots(uint8 class_id) { Bot::BotOrderCampAll(this, class_id); } + +void Client::LoadDefaultBotSettings() { + m_bot_spell_settings.clear(); + + // Only illusion block supported currently + SetBotSetting(BotSettingCategories::BaseSetting, BotBaseSettings::IllusionBlock, GetDefaultBotSettings(BotSettingCategories::BaseSetting, BotBaseSettings::IllusionBlock)); + LogBotSettingsDetail("{} says, 'Setting default {} [{}] to [{}]'", GetCleanName(), CastToBot()->GetBotSettingCategoryName(BotBaseSettings::IllusionBlock), BotBaseSettings::IllusionBlock, GetDefaultBotSettings(BotSettingCategories::BaseSetting, BotBaseSettings::IllusionBlock)); + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + BotSpellSettings t; + + t.spell_type = i; + t.short_name = Bot::GetSpellTypeShortNameByID(i); + t.name = Bot::GetSpellTypeNameByID(i); + t.delay = GetDefaultSpellTypeDelay(i); + t.min_threshold = GetDefaultSpellTypeMinThreshold(i); + t.max_threshold = GetDefaultSpellTypeMaxThreshold(i); + t.recast_timer.Start(); + + m_bot_spell_settings.push_back(t); + + LogBotSettingsDetail("{} says, 'Setting defaults for {} ({}) [#{}]'", GetCleanName(), t.name, t.short_name, t.spell_type); + LogBotSettingsDetail("{} says, 'Delay = [{}ms] | MinThreshold = [{}\%] | MaxThreshold = [{}\%]'", GetCleanName(), + GetDefaultSpellTypeDelay(i), + GetDefaultSpellTypeMinThreshold(i), GetDefaultSpellTypeMaxThreshold(i)); + } +} + +int Client::GetDefaultBotSettings(uint8 setting_type, uint16 bot_setting) { + switch (setting_type) { + case BotSettingCategories::BaseSetting: + return false; // only setting supported currently is illusion block + case BotSettingCategories::SpellDelay: + return GetDefaultSpellTypeDelay(bot_setting); + case BotSettingCategories::SpellMinThreshold: + return GetDefaultSpellTypeMinThreshold(bot_setting); + case BotSettingCategories::SpellMaxThreshold: + return GetDefaultSpellTypeMaxThreshold(bot_setting); + } +} + +int Client::GetBotSetting(uint8 setting_type, uint16 bot_setting) { + switch (setting_type) { + case BotSettingCategories::BaseSetting: + return GetIllusionBlock(); // only setting supported currently + case BotSettingCategories::SpellDelay: + return GetSpellTypeDelay(bot_setting); + case BotSettingCategories::SpellMinThreshold: + return GetSpellTypeMinThreshold(bot_setting); + case BotSettingCategories::SpellMaxThreshold: + return GetSpellTypeMaxThreshold(bot_setting); + } +} + +void Client::SetBotSetting(uint8 setting_type, uint16 bot_setting, uint32 setting_value) { + switch (setting_type) { + case BotSettingCategories::BaseSetting: + SetIllusionBlock(setting_value); // only setting supported currently + break; + case BotSettingCategories::SpellDelay: + SetSpellTypeDelay(bot_setting, setting_value); + break; + case BotSettingCategories::SpellMinThreshold: + SetSpellTypeMinThreshold(bot_setting, setting_value); + break; + case BotSettingCategories::SpellMaxThreshold: + SetSpellTypeMaxThreshold(bot_setting, setting_value); + break; + } +} + +void Client::SendSpellTypePrompts(bool commanded_types, bool client_only_types) { + if (client_only_types) { + Message( + Chat::Yellow, + fmt::format( + "You can view client spell types by {} or {}.", + Saylink::Silent( + fmt::format("^spelltypeids client"), "ID" + ), + Saylink::Silent( + fmt::format("^spelltypenames client"), "Shortname" + ) + ).c_str() + ); + } + else { + Message( + Chat::Yellow, + fmt::format( + "You can view spell types by {}, {}, {} or by {}, {}, {}.", + Saylink::Silent( + fmt::format("^spelltypeids 0-19"), "ID 0-19" + ), + Saylink::Silent( + fmt::format("^spelltypeids 20-39"), "20-39" + ), + Saylink::Silent( + fmt::format("^spelltypeids 40+"), "40+" + ), + Saylink::Silent( + fmt::format("^spelltypenames 0-19"), "Shortname 0-19" + ), + Saylink::Silent( + fmt::format("^spelltypenames 20-39"), "20-39" + ), + Saylink::Silent( + fmt::format("^spelltypenames 40+"), "40+" + ) + ).c_str() + ); + } + + if (commanded_types) { + Message( + Chat::Yellow, + fmt::format( + "You can view commanded spell types by {} or {}.", + Saylink::Silent( + fmt::format("^spelltypeids commanded"), "ID" + ), + Saylink::Silent( + fmt::format("^spelltypenames commanded"), "Shortname" + ) + ).c_str() + ); + } + + return; +} + +uint16 Client::GetDefaultSpellTypeDelay(uint16 spell_type) { + switch (spell_type) { + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + return 1500; + case BotSpellTypes::FastHeals: + case BotSpellTypes::PetFastHeals: + return 2500; + case BotSpellTypes::GroupHeals: + case BotSpellTypes::RegularHeal: + case BotSpellTypes::PetRegularHeals: + return 4000; + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::PetCompleteHeals: + return 8000; + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetHoTHeals: + return 22000; + case BotSpellTypes::Cure: + return 2000; + case BotSpellTypes::GroupCures: + return 3000; + case BotSpellTypes::PetCures: + return 5000; + default: + return 100; + } +} + +uint8 Client::GetDefaultSpellTypeMinThreshold(uint16 spell_type) { + switch (spell_type) { + default: + return 0; + } +} + +uint8 Client::GetDefaultSpellTypeMaxThreshold(uint16 spell_type) { + uint8 client_class = GetClass(); + + switch (spell_type) { + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + return 25; + case BotSpellTypes::FastHeals: + case BotSpellTypes::PetFastHeals: + return 40; + case BotSpellTypes::GroupHeals: + case BotSpellTypes::RegularHeal: + case BotSpellTypes::PetRegularHeals: + return 60; + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::PetCompleteHeals: + if (client_class == Class::Necromancer || client_class == Class::Shaman) { + return 55; + } + else { + return 80; + } + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetHoTHeals: + if (client_class == Class::Necromancer || client_class == Class::Shaman) { + return 70; + } + else { + return 90; + } + case BotSpellTypes::Buff: + case BotSpellTypes::Cure: + case BotSpellTypes::GroupCures: + case BotSpellTypes::PetCures: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PetDamageShields: + case BotSpellTypes::PetResistBuffs: + case BotSpellTypes::ResistBuffs: + default: + return 100; + } +} diff --git a/zone/client_mods.cpp b/zone/client_mods.cpp index 0387b795b5..23ef134541 100644 --- a/zone/client_mods.cpp +++ b/zone/client_mods.cpp @@ -812,7 +812,7 @@ int32 Client::CalcSTR() int32 Client::CalcSTA() { - int32 val = m_pp.STA + itembonuses.STA + spellbonuses.STA + CalcAlcoholPhysicalEffect();; + int32 val = m_pp.STA + itembonuses.STA + spellbonuses.STA + CalcAlcoholPhysicalEffect(); int32 mod = aabonuses.STA; STA = val + mod; if (STA < 1) { @@ -827,7 +827,7 @@ int32 Client::CalcSTA() int32 Client::CalcAGI() { - int32 val = m_pp.AGI + itembonuses.AGI + spellbonuses.AGI - CalcAlcoholPhysicalEffect();; + int32 val = m_pp.AGI + itembonuses.AGI + spellbonuses.AGI - CalcAlcoholPhysicalEffect(); int32 mod = aabonuses.AGI; int32 str = GetSTR(); //Encumbered penalty @@ -852,7 +852,7 @@ int32 Client::CalcAGI() int32 Client::CalcDEX() { - int32 val = m_pp.DEX + itembonuses.DEX + spellbonuses.DEX - CalcAlcoholPhysicalEffect();; + int32 val = m_pp.DEX + itembonuses.DEX + spellbonuses.DEX - CalcAlcoholPhysicalEffect(); int32 mod = aabonuses.DEX; DEX = val + mod; if (DEX < 1) { diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 03aa87f100..2955c6b75e 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -416,7 +416,7 @@ void MapOpcodes() ConnectedOpcodes[OP_TributeUpdate] = &Client::Handle_OP_TributeUpdate; ConnectedOpcodes[OP_VetClaimRequest] = &Client::Handle_OP_VetClaimRequest; ConnectedOpcodes[OP_VoiceMacroIn] = &Client::Handle_OP_VoiceMacroIn; - ConnectedOpcodes[OP_UpdateAura] = &Client::Handle_OP_UpdateAura;; + ConnectedOpcodes[OP_UpdateAura] = &Client::Handle_OP_UpdateAura; ConnectedOpcodes[OP_WearChange] = &Client::Handle_OP_WearChange; ConnectedOpcodes[OP_WhoAllRequest] = &Client::Handle_OP_WhoAllRequest; ConnectedOpcodes[OP_WorldUnknown001] = &Client::Handle_OP_Ignore; @@ -670,6 +670,10 @@ void Client::CompleteConnect() for (int x1 = 0; x1 < EFFECT_COUNT; x1++) { switch (spell.effect_id[x1]) { case SE_Illusion: { + if (GetIllusionBlock()) { + break; + } + if (buffs[j1].persistant_buff) { Mob *caster = entity_list.GetMobID(buffs[j1].casterid); ApplySpellEffectIllusion(spell.id, caster, j1, spell.base_value[x1], spell.limit_value[x1], spell.max_value[x1]); @@ -1506,6 +1510,11 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) LogError("Error loading AA points for [{}]", GetName()); } + if (RuleB(Bots, Enabled)) { + LoadDefaultBotSettings(); + database.botdb.LoadBotSettings(this); + } + if (SPDAT_RECORDS > 0) { for (uint32 z = 0; z < EQ::spells::SPELL_GEM_COUNT; z++) { if (m_pp.mem_spells[z] >= (uint32)SPDAT_RECORDS) @@ -1602,7 +1611,6 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) LFG = false; } - /* Load Bots */ if (RuleB(Bots, Enabled)) { database.botdb.LoadOwnerOptions(this); // TODO: mod below function for loading spawned botgroups @@ -4303,7 +4311,13 @@ void Client::Handle_OP_Camp(const EQApplicationPacket *app) OnDisconnect(true); return; } + camp_timer.Start(29000, true); + + if (RuleB(Bots, Enabled)) { + bot_camp_timer.Start((RuleI(Bots, CampTimer) * 1000), true); + } + return; } @@ -11078,10 +11092,17 @@ void Client::Handle_OP_PetCommands(const EQApplicationPacket *app) case PET_ATTACK: { if (!target) break; + + if (RuleB(Map, CheckForLoSCheat) && (!DoLosChecks(target) || !CheckLosCheat(target))) { + mypet->SayString(this, NOT_LEGAL_TARGET); + break; + } + if (target->IsMezzed()) { MessageString(Chat::NPCQuestSay, CANNOT_WAKE, mypet->GetCleanName(), target->GetCleanName()); break; } + if (mypet->IsFeared()) break; //prevent pet from attacking stuff while feared @@ -11136,8 +11157,15 @@ void Client::Handle_OP_PetCommands(const EQApplicationPacket *app) if (mypet->IsFeared()) break; //prevent pet from attacking stuff while feared - if (!GetTarget()) + if (!GetTarget()) { break; + } + + if (RuleB(Map, CheckForLoSCheat) && (!DoLosChecks(GetTarget()) || !CheckLosCheat(GetTarget()))) { + mypet->SayString(this, NOT_LEGAL_TARGET); + break; + } + if (GetTarget()->IsMezzed()) { MessageString(Chat::NPCQuestSay, CANNOT_WAKE, mypet->GetCleanName(), GetTarget()->GetCleanName()); break; @@ -14759,6 +14787,7 @@ void Client::Handle_OP_SpawnAppearance(const EQApplicationPacket *app) SetFeigned(false); BindWound(this, false, true); camp_timer.Disable(); + bot_camp_timer.Disable(); } else if (sa->parameter == Animation::Sitting) { SetAppearance(eaSitting); @@ -15216,7 +15245,7 @@ void Client::Handle_OP_TargetMouse(const EQApplicationPacket *app) GetTarget()->IsTargeted(1); return; } - else if (GetTarget()->IsPetOwnerClient()) + else if (GetTarget()->IsPetOwnerOfClientBot()) { GetTarget()->IsTargeted(1); return; diff --git a/zone/client_process.cpp b/zone/client_process.cpp index 7a61db35ea..07158afa72 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -193,6 +193,12 @@ bool Client::Process() { return false; //delete client } + if (RuleB(Bots, Enabled)) { + if (bot_camp_timer.Check()) { + CampAllBots(); + } + } + if (camp_timer.Check()) { Raid *myraid = entity_list.GetRaidByClient(this); if (myraid) { diff --git a/zone/command.cpp b/zone/command.cpp index 35b920e53e..94a0ef9291 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -148,6 +148,7 @@ int command_init(void) command_add("help", "[Search Criteria] - List available commands and their description, specify partial command as argument to search", AccountStatus::Player, command_help) || command_add("hotfix", "[hotfix_name] - Reloads shared memory into a hotfix, equiv to load_shared_memory followed by apply_shared_memory", AccountStatus::GMImpossible, command_hotfix) || command_add("hp", "Refresh your HP bar from the server.", AccountStatus::Player, command_hp) || + command_add("illusionblock", "Controls whether or not illusion effects will land on you when cast by other players or bots", AccountStatus::Guide, command_illusion_block) || command_add("instance", "Modify Instances", AccountStatus::GMMgmt, command_instance) || command_add("interrogateinv", "use [help] argument for available options", AccountStatus::Player, command_interrogateinv) || command_add("interrupt", "[Message ID] [Color] - Interrupt your casting. Arguments are optional.", AccountStatus::Guide, command_interrupt) || @@ -844,6 +845,7 @@ void command_bot(Client *c, const Seperator *sep) #include "gm_commands/grid.cpp" #include "gm_commands/guild.cpp" #include "gm_commands/hp.cpp" +#include "gm_commands/illusion_block.cpp" #include "gm_commands/instance.cpp" #include "gm_commands/interrogateinv.cpp" #include "gm_commands/interrupt.cpp" diff --git a/zone/command.h b/zone/command.h index 09164e6bf3..4ea728a30b 100644 --- a/zone/command.h +++ b/zone/command.h @@ -102,6 +102,7 @@ void command_guild(Client *c, const Seperator *sep); void command_help(Client *c, const Seperator *sep); void command_hotfix(Client *c, const Seperator *sep); void command_hp(Client *c, const Seperator *sep); +void command_illusion_block(Client* c, const Seperator* sep); void command_instance(Client *c, const Seperator *sep); void command_interrogateinv(Client *c, const Seperator *sep); void command_interrupt(Client *c, const Seperator *sep); diff --git a/zone/effects.cpp b/zone/effects.cpp index 63e349f1b3..2b6472446e 100644 --- a/zone/effects.cpp +++ b/zone/effects.cpp @@ -1142,7 +1142,7 @@ void EntityList::AESpell( continue; } - if (spells[spell_id].target_type == ST_TargetAENoPlayersPets && current_mob->IsPetOwnerClient()) { + if (spells[spell_id].target_type == ST_TargetAENoPlayersPets && current_mob->IsPetOwnerOfClientBot()) { continue; } diff --git a/zone/entity.cpp b/zone/entity.cpp index b40e6d949e..198ddf710a 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -1891,13 +1891,13 @@ Bot* EntityList::GetRandomBot(const glm::vec3& location, float distance, Bot* ex for (const auto& b : bot_list) { if ( - b != exclude_bot && + b.second != exclude_bot && ( is_whole_zone || - DistanceSquared(static_cast(b->GetPosition()), location) <= distance_squared - ) + DistanceSquared(static_cast(b.second->GetPosition()), location) <= distance_squared + ) ) { - bots_in_range.push_back(b); + bots_in_range.push_back(b.second); } } @@ -2224,44 +2224,37 @@ Raid* EntityList::GetRaidByClient(Client* client) } Raid* EntityList::GetRaidByBotName(const char* name) { - std::list rm; - auto GetMembersWithNames = [&rm](Raid const* r) -> std::list { - for (const auto& m : r->members) { - if (strlen(m.member_name) > 0) - rm.push_back(m); - } - return rm; - }; - for (const auto& r : raid_list) { - for (const auto& m : GetMembersWithNames(r)) { - if (strcmp(m.member_name, name) == 0) { + for (const auto& m : r->members) { + if (m.is_bot && strcmp(m.member_name, name) == 0) { return r; } } } + return nullptr; } -Raid* EntityList::GetRaidByBot(const Bot* bot) +Raid* EntityList::GetRaidByBot(Bot* bot) { - std::list rm; - auto GetMembersWhoAreBots = [&rm](Raid* r) -> std::list { - for (auto const& m : r->members) { - if (m.is_bot) { - rm.push_back(m); - } - } - return rm; - }; + if (bot->p_raid_instance) { + return bot->p_raid_instance; + } - for (const auto& r : raid_list) { - for (const auto& m : GetMembersWhoAreBots(r)) { - if (m.member->CastToBot() == bot) { - return r; + std::list::iterator iterator; + iterator = raid_list.begin(); + + while (iterator != raid_list.end()) { + for (const auto& member : (*iterator)->members) { + if (member.member && member.is_bot && member.member->CastToBot() == bot) { + bot->p_raid_instance = *iterator; + return *iterator; } } + + ++iterator; } + return nullptr; } @@ -2951,6 +2944,7 @@ void EntityList::ScanCloseMobs(Mob *scanning_mob) for (auto &e : mob_list) { auto mob = e.second; + if (mob->GetID() <= 0) { continue; } @@ -5136,9 +5130,10 @@ void EntityList::GetClientList(std::list &c_list) void EntityList::GetBotList(std::list &b_list) { b_list.clear(); - - for (const auto& b : bot_list) { - b_list.push_back(b); + auto it = bot_list.begin(); + while (it != bot_list.end()) { + b_list.push_back(it->second); + ++it; } } @@ -5150,14 +5145,13 @@ std::vector EntityList::GetBotListByCharacterID(uint32 character_id, uint return client_bot_list; } - for (const auto& b : bot_list) { - if ( - b->GetOwner() && - b->GetBotOwnerCharacterID() == character_id && - (!class_id || b->GetClass() == class_id) - ) { - client_bot_list.push_back(b); + auto it = bot_list.begin(); + + while (it != bot_list.end()) { + if (it->second->GetOwner() && it->second->GetBotOwnerCharacterID() == character_id && (!class_id || it->second->GetClass() == class_id)) { + client_bot_list.push_back(it->second); } + ++it; } return client_bot_list; @@ -5171,14 +5165,13 @@ std::vector EntityList::GetBotListByClientName(std::string client_name, u return client_bot_list; } - for (const auto& b : bot_list) { - if ( - b->GetOwner() && - Strings::ToLower(b->GetOwner()->GetCleanName()) == Strings::ToLower(client_name) && - (!class_id || b->GetClass() == class_id) - ) { - client_bot_list.push_back(b); + auto it = bot_list.begin(); + + while (it != bot_list.end()) { + if (it->second->GetOwner() && Strings::ToLower(it->second->GetOwner()->GetCleanName()) == Strings::ToLower(client_name) && (!class_id || it->second->GetClass() == class_id)) { + client_bot_list.push_back(it->second); } + ++it; } return client_bot_list; @@ -5550,7 +5543,7 @@ Mob *EntityList::GetClosestMobByBodyType(Mob *sender, uint8 BodyType, bool skip_ continue; // Do not detect client pets - if (skip_client_pets && CurrentMob->IsPet() && CurrentMob->IsPetOwnerClient()) + if (skip_client_pets && CurrentMob->IsPet() && CurrentMob->IsPetOwnerOfClientBot()) continue; CurrentDistance = ((CurrentMob->GetY() - sender->GetY()) * (CurrentMob->GetY() - sender->GetY())) + diff --git a/zone/entity.h b/zone/entity.h index f073a4fb5d..d2bf8eb633 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -200,7 +200,7 @@ class EntityList Raid *GetRaidByClient(Client* client); Raid *GetRaidByID(uint32 id); Raid* GetRaidByBotName(const char* name); - Raid* GetRaidByBot(const Bot* bot); + Raid* GetRaidByBot(Bot* bot); Raid* GetRaidByName(const char* name); Corpse *GetCorpseByOwner(Client* client); @@ -547,7 +547,7 @@ class EntityList inline const std::unordered_map &GetNPCList() { return npc_list; } inline const std::unordered_map &GetMercList() { return merc_list; } inline const std::unordered_map &GetClientList() { return client_list; } - inline const std::list &GetBotList() { return bot_list; } + inline const std::unordered_map &GetBotList() { return bot_list; } std::vector GetBotListByCharacterID(uint32 character_id, uint8 class_id = Class::None); std::vector GetBotListByClientName(std::string client_name, uint8 class_id = Class::None); void SignalAllBotsByOwnerCharacterID(uint32 character_id, int signal_id); @@ -623,17 +623,16 @@ class EntityList bool RemoveBot(uint16 entityID); Mob* GetMobByBotID(uint32 botID); Bot* GetBotByBotID(uint32 botID); - Bot* GetBotByBotName(std::string_view botName); + Bot* GetBotByBotName(std::string botName); Client* GetBotOwnerByBotEntityID(uint32 entity_id); Client* GetBotOwnerByBotID(const uint32 bot_id); - std::list GetBotsByBotOwnerCharacterID(uint32 botOwnerCharacterID); + std::vector GetBotsByBotOwnerCharacterID(uint32 botOwnerCharacterID); - bool Bot_AICheckCloseBeneficialSpells(Bot* caster, uint8 iChance, float iRange, uint32 iSpellTypes); // TODO: Evaluate this closesly in hopes to eliminate void ShowSpawnWindow(Client* client, int Distance, bool NamedOnly); // TODO: Implement ShowSpawnWindow in the bot class but it needs entity list stuff void GetBotList(std::list &b_list); private: - std::list bot_list; + std::unordered_map bot_list; }; class BulkZoneSpawnPacket { diff --git a/zone/gm_commands/illusion_block.cpp b/zone/gm_commands/illusion_block.cpp new file mode 100644 index 0000000000..67c3afa31c --- /dev/null +++ b/zone/gm_commands/illusion_block.cpp @@ -0,0 +1,53 @@ +#include "../client.h" + +void command_illusion_block(Client* c, const Seperator* sep) +{ + int arguments = sep->argnum; + if (!arguments || !strcasecmp(sep->arg[1], "help")) { + BotCommandHelpParams p; + + p.description = { "Toggles whether or not you will block the illusion effects of spells cast by players or bots." }; + p.example_format = { fmt::format("{} [value]", sep->arg[0]) }; + p.examples_one = + { + "To enable illusion block:", + fmt::format( + "{} 1", + sep->arg[0] + ) + }; + p.examples_two = + { + "To disable illusion block:", + fmt::format( + "{} 0", + sep->arg[0] + ) + }; + + std::string popup_text = c->SendBotCommandHelpWindow(p); + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + + return; + } + + if (sep->IsNumber(1)) { + int set_status = atoi(sep->arg[1]); + if (set_status == 0 || set_status == 1) { + c->SetIllusionBlock(set_status); + c->Message(Chat::Green, "Your Illusion Block has been %s.", (set_status ? "enabled" : "disabled")); + } + else { + c->Message(Chat::Yellow, "You must enter 0 for disabled or 1 for enabled."); + return; + } + } + else if (!strcasecmp(sep->arg[1], "current")) { + c->Message(Chat::Green, "You're currently %s illusions.", (c->GetIllusionBlock() ? "blocking" : "allowing")); + } + else { + c->Message(Chat::Yellow , "Incorrect argument, use %s help for a list of options.", sep->arg[0]); + } +} diff --git a/zone/gm_commands/list.cpp b/zone/gm_commands/list.cpp index 5019c09b84..7e05a1d6a2 100755 --- a/zone/gm_commands/list.cpp +++ b/zone/gm_commands/list.cpp @@ -74,20 +74,22 @@ void command_list(Client *c, const Seperator *sep) if (is_bots) { const auto& l = entity_list.GetBotList(); - for (const auto& e: l) { + for (const auto& e : l) { + Bot* entity = e.second; + entity_count++; - const std::string& entity_name = Strings::ToLower(e->GetName()); + const std::string& entity_name = Strings::ToLower(entity->GetName()); if (!search_string.empty() && !Strings::Contains(entity_name, search_string)) { continue; } unique_entities.emplace_back( UniqueEntity{ - .entity_id = e->GetID(), - .entity_name = e->GetName(), - .unique_id = e->GetBotID(), - .position = e->GetPosition() + .entity_id = entity->GetID(), + .entity_name = entity->GetName(), + .unique_id = entity->GetBotID(), + .position = entity->GetPosition() } ); diff --git a/zone/groups.cpp b/zone/groups.cpp index e61bbade79..6e5d2c489a 100644 --- a/zone/groups.cpp +++ b/zone/groups.cpp @@ -1017,6 +1017,27 @@ void Group::GetBotList(std::list& bot_list, bool clear_list) } } +std::list Group::GetRawBotList() +{ + std::list bot_list; + + const auto& l = GroupIdRepository::GetWhere( + database, + fmt::format( + "`group_id` = {}", + GetID() + ) + ); + + for (const auto& e : l) { + if (e.bot_id) { + bot_list.push_back(e.bot_id); + } + } + + return bot_list; +} + bool Group::Process() { if(disbandcheck && !GroupCount()) return false; @@ -1234,30 +1255,48 @@ void Group::GroupMessageString(Mob* sender, uint32 type, uint32 string_id, const void Client::LeaveGroup() { Group *g = GetGroup(); - if(g) - { + if (g) { int32 MemberCount = g->GroupCount(); // Account for both client and merc leaving the group - if (GetMerc() && g == GetMerc()->GetGroup()) - { + if (GetMerc() && g == GetMerc()->GetGroup()) { MemberCount -= 1; } - if(MemberCount < 3) - { + if (RuleB(Bots, Enabled)) { + std::list sbl = g->GetRawBotList(); + + for (auto botID : sbl) { + auto b = entity_list.GetBotByBotID(botID); + + if (b) { + if (b->GetBotOwnerCharacterID() == CharacterID()) { + MemberCount -= 1; + } + } + else { + if (database.botdb.GetOwnerID(botID) == CharacterID()) { + MemberCount -= 1; + } + } + } + } + + if (MemberCount < 3) { g->DisbandGroup(); } - else - { + else { g->DelMember(this); - if (GetMerc() != nullptr && g == GetMerc()->GetGroup() ) - { + + if (GetMerc() != nullptr && g == GetMerc()->GetGroup()) { GetMerc()->RemoveMercFromGroup(GetMerc(), GetMerc()->GetGroup()); } + + if (RuleB(Bots, Enabled)) { + g->RemoveClientsBots(this); + } } } - else - { + else { //force things a little Group::RemoveFromGroup(this); @@ -2222,24 +2261,24 @@ void Group::ClearAllNPCMarks() } -int8 Group::GetNumberNeedingHealedInGroup(int8 hpr, bool includePets) { - int8 needHealed = 0; +int8 Group::GetNumberNeedingHealedInGroup(int8 hpr, bool include_pets) { + int8 need_healed = 0; for( int i = 0; iqglobal) { if(members[i]->GetHPRatio() <= hpr) - needHealed++; + need_healed++; - if(includePets) { + if(include_pets) { if(members[i]->GetPet() && members[i]->GetPet()->GetHPRatio() <= hpr) { - needHealed++; + need_healed++; } } } } - return needHealed; + return need_healed; } void Group::UpdateGroupAAs() @@ -2576,3 +2615,76 @@ void Group::AddToGroup(AddToGroupRequest r) } ); } + +void Group::RemoveClientsBots(Client* c) { + std::list sbl = GetRawBotList(); + + for (auto botID : sbl) { + auto b = entity_list.GetBotByBotID(botID); + + if (b) { + if (b->GetBotOwnerCharacterID() == c->CharacterID()) { + b->RemoveBotFromGroup(b, this); + } + } + else { + if (database.botdb.GetOwnerID(botID) == c->CharacterID()) { + auto botName = database.botdb.GetBotNameByID(botID); + + for (uint32 i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (membername[i] == botName) { + members[i] = nullptr; + membername[i][0] = '\0'; + memset(membername[i], 0, 64); + MemberRoles[i] = 0; + break; + } + } + + auto pack = new ServerPacket(ServerOP_GroupLeave, sizeof(ServerGroupLeave_Struct)); + ServerGroupLeave_Struct* gl = (ServerGroupLeave_Struct*)pack->pBuffer; + gl->gid = GetID(); + gl->zoneid = zone->GetZoneID(); + gl->instance_id = zone->GetInstanceID(); + strcpy(gl->member_name, botName.c_str()); + worldserver.SendPacket(pack); + safe_delete(pack); + + auto outapp = new EQApplicationPacket(OP_GroupUpdate, sizeof(GroupJoin_Struct)); + GroupJoin_Struct* gu = (GroupJoin_Struct*)outapp->pBuffer; + gu->action = groupActLeave; + strcpy(gu->membername, botName.c_str()); + strcpy(gu->yourname, botName.c_str()); + + gu->leader_aas = LeaderAbilities; + + for (uint32 i = 0; i < MAX_GROUP_MEMBERS; i++) { + if (members[i] == nullptr) { + //if (DEBUG>=5) LogFile->write(EQEMuLog::Debug, "Group::DelMember() null member at slot %i", i); + continue; + } + + if (membername[i] != botName.c_str()) { + strcpy(gu->yourname, members[i]->GetCleanName()); + + if (members[i]->IsClient()) { + members[i]->CastToClient()->QueuePacket(outapp); + } + } + } + + safe_delete(outapp); + + DelMemberOOZ(botName.c_str()); + + GroupIdRepository::DeleteWhere( + database, + fmt::format( + "`bot_id` = {}", + botID + ) + ); + } + } + } +} diff --git a/zone/groups.h b/zone/groups.h index 9cd1a594f7..91fcee13e1 100644 --- a/zone/groups.h +++ b/zone/groups.h @@ -69,6 +69,7 @@ class Group : public GroupIDConsumer { void GetMemberList(std::list& member_list, bool clear_list = true); void GetClientList(std::list& client_list, bool clear_list = true); void GetBotList(std::list& bot_list, bool clear_list = true); + std::list GetRawBotList(); bool IsGroupMember(Mob* c); bool IsGroupMember(const char* name); bool Process(); @@ -106,7 +107,7 @@ class Group : public GroupIDConsumer { void UpdateGroupAAs(); void SaveGroupLeaderAA(); void MarkNPC(Mob* Target, int Number); - int8 GetNumberNeedingHealedInGroup(int8 hpr, bool includePets); + int8 GetNumberNeedingHealedInGroup(int8 hpr, bool include_pets); void DelegateMainTank(const char *NewMainAssistName, uint8 toggle = 0); void DelegateMainAssist(const char *NewMainAssistName, uint8 toggle = 0); void DelegatePuller(const char *NewMainAssistName, uint8 toggle = 0); @@ -153,6 +154,7 @@ class Group : public GroupIDConsumer { void AddToGroup(AddToGroupRequest r); void AddToGroup(Mob* m); static void RemoveFromGroup(Mob* m); + void RemoveClientsBots(Client* c); void SetGroupMentor(int percent, char *name); void ClearGroupMentor(); diff --git a/zone/lua_bot.cpp b/zone/lua_bot.cpp index 5b37a1344e..dc1742feea 100644 --- a/zone/lua_bot.cpp +++ b/zone/lua_bot.cpp @@ -105,11 +105,6 @@ void Lua_Bot::SetExpansionBitmask(int expansion_bitmask) { self->SetExpansionBitmask(expansion_bitmask); } -void Lua_Bot::SetExpansionBitmask(int expansion_bitmask, bool save) { - Lua_Safe_Call_Void(); - self->SetExpansionBitmask(expansion_bitmask, save); -} - bool Lua_Bot::ReloadBotDataBuckets() { Lua_Safe_Call_Bool(); return DataBucket::GetDataBuckets(self); @@ -664,6 +659,11 @@ void Lua_Bot::DeleteBot() { self->DeleteBot(); } +void Lua_Bot::RaidGroupSay(const char* message) { + Lua_Safe_Call_Void(); + self->RaidGroupSay(self, message); +} + luabind::scope lua_register_bot() { return luabind::class_("Bot") .def(luabind::constructor<>()) @@ -693,6 +693,7 @@ luabind::scope lua_register_bot() { .def("ApplySpellRaid", (void(Lua_Bot::*)(int,int,int,bool))&Lua_Bot::ApplySpellRaid) .def("ApplySpellRaid", (void(Lua_Bot::*)(int,int,int,bool,bool))&Lua_Bot::ApplySpellRaid) .def("ApplySpellRaid", (void(Lua_Bot::*)(int,int,int,bool,bool))&Lua_Bot::ApplySpellRaid) + .def("RaidGroupSay", (void(Lua_Bot::*)(const char*))&Lua_Bot::RaidGroupSay) .def("Camp", (void(Lua_Bot::*)(void))&Lua_Bot::Camp) .def("Camp", (void(Lua_Bot::*)(bool))&Lua_Bot::Camp) .def("ClearDisciplineReuseTimer", (void(Lua_Bot::*)())&Lua_Bot::ClearDisciplineReuseTimer) @@ -763,7 +764,6 @@ luabind::scope lua_register_bot() { .def("SetBucket", (void(Lua_Bot::*)(std::string,std::string))&Lua_Bot::SetBucket) .def("SetBucket", (void(Lua_Bot::*)(std::string,std::string,std::string))&Lua_Bot::SetBucket) .def("SetExpansionBitmask", (void(Lua_Bot::*)(int))&Lua_Bot::SetExpansionBitmask) - .def("SetExpansionBitmask", (void(Lua_Bot::*)(int,bool))&Lua_Bot::SetExpansionBitmask) .def("SetDisciplineReuseTimer", (void(Lua_Bot::*)(uint16))&Lua_Bot::SetDisciplineReuseTimer) .def("SetDisciplineReuseTimer", (void(Lua_Bot::*)(uint16, uint32))&Lua_Bot::SetDisciplineReuseTimer) .def("SetItemReuseTimer", (void(Lua_Bot::*)(uint32))&Lua_Bot::SetItemReuseTimer) diff --git a/zone/lua_bot.h b/zone/lua_bot.h index 6825d2d295..86e3ca28bf 100644 --- a/zone/lua_bot.h +++ b/zone/lua_bot.h @@ -46,13 +46,13 @@ class Lua_Bot : public Lua_Mob Lua_Mob GetOwner(); int16 HasBotItem(uint32 item_id); void OwnerMessage(std::string message); + void RaidGroupSay(const char* message); bool ReloadBotDataBuckets(); bool ReloadBotOwnerDataBuckets(); bool ReloadBotSpells(); void ReloadBotSpellSettings(); void RemoveBotItem(uint32 item_id); void SetExpansionBitmask(int expansion_bitmask); - void SetExpansionBitmask(int expansion_bitmask, bool save); void Signal(int signal_id); bool HasBotSpellEntry(uint16 spellid); void SendPayload(int payload_id); diff --git a/zone/lua_entity_list.cpp b/zone/lua_entity_list.cpp index 45c0e66210..4b99c869ec 100644 --- a/zone/lua_entity_list.cpp +++ b/zone/lua_entity_list.cpp @@ -382,12 +382,13 @@ Lua_Bot Lua_EntityList::GetBotByName(std::string bot_name) { Lua_Bot_List Lua_EntityList::GetBotList() { Lua_Safe_Call_Class(Lua_Bot_List); Lua_Bot_List ret; - auto &bot_list = self->GetBotList(); - if (bot_list.size()) { - for (auto bot : bot_list) { - ret.entries.emplace_back(Lua_Bot(bot)); - } + auto ¤t_bot_list = self->GetBotList(); + auto it = current_bot_list.begin(); + + while (it != current_bot_list.end()) { + ret.entries.emplace_back(it->second); + ++it; } return ret; diff --git a/zone/lua_mob.cpp b/zone/lua_mob.cpp index 0e644f4f2f..27ab7bbd86 100644 --- a/zone/lua_mob.cpp +++ b/zone/lua_mob.cpp @@ -3268,6 +3268,12 @@ bool Lua_Mob::IsPetOwnerNPC() return self->IsPetOwnerNPC(); } +bool Lua_Mob::IsPetOwnerOfClientBot() +{ + Lua_Safe_Call_Bool(); + return self->IsPetOwnerOfClientBot(); +} + bool Lua_Mob::IsDestructibleObject() { Lua_Safe_Call_Bool(); @@ -3902,8 +3908,9 @@ luabind::scope lua_register_mob() { .def("IsPausedTimer", &Lua_Mob::IsPausedTimer) .def("IsPet", (bool(Lua_Mob::*)(void))&Lua_Mob::IsPet) .def("IsPetOwnerBot", &Lua_Mob::IsPetOwnerBot) - .def("IsPetOwnerClient", &Lua_Mob::IsPetOwnerClient) + .def("IsPetOwnerClient", &Lua_Mob::IsPetOwnerClient) .def("IsPetOwnerNPC", &Lua_Mob::IsPetOwnerNPC) + .def("IsPetOwnerOfClientBot", &Lua_Mob::IsPetOwnerOfClientBot) .def("IsPureMeleeClass", &Lua_Mob::IsPureMeleeClass) .def("IsRoamer", (bool(Lua_Mob::*)(void))&Lua_Mob::IsRoamer) .def("IsRooted", (bool(Lua_Mob::*)(void))&Lua_Mob::IsRooted) diff --git a/zone/lua_mob.h b/zone/lua_mob.h index dfe646cc01..0f3d4f6c4e 100644 --- a/zone/lua_mob.h +++ b/zone/lua_mob.h @@ -574,8 +574,9 @@ class Lua_Mob : public Lua_Entity bool IsFamiliar(); bool IsTargetLockPet(); bool IsPetOwnerBot(); - bool IsPetOwnerClient(); + bool IsPetOwnerClient(); bool IsPetOwnerNPC(); + bool IsPetOwnerOfClientBot(); bool IsDestructibleObject(); bool IsBoat(); bool IsControllableBoat(); diff --git a/zone/merc.cpp b/zone/merc.cpp index 6200e5b895..c54585f9eb 100644 --- a/zone/merc.cpp +++ b/zone/merc.cpp @@ -1500,7 +1500,7 @@ bool Merc::AI_IdleCastCheck() { bool EntityList::Merc_AICheckCloseBeneficialSpells(Merc* caster, uint8 iChance, float iRange, uint32 iSpellTypes) { - if((iSpellTypes & SPELL_TYPES_DETRIMENTAL) != 0) { + if ((iSpellTypes & SPELL_TYPES_DETRIMENTAL) != 0) { //according to live, you can buff and heal through walls... //now with PCs, this only applies if you can TARGET the target, but // according to Rogean, Live NPCs will just cast through walls/floors, no problem.. diff --git a/zone/mob.cpp b/zone/mob.cpp index 788b0b2f52..9d3b3007c9 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -1689,6 +1689,13 @@ void Mob::StopMoving() void Mob::StopMoving(float new_heading) { + if (IsBot()) { + auto bot = CastToBot(); + + bot->SetCombatJitterFlag(false); + bot->SetCombatOutOfRangeJitterFlag(false); + } + StopNavigation(); RotateTo(new_heading); @@ -4656,25 +4663,22 @@ bool Mob::CanThisClassDoubleAttack(void) const } } -bool Mob::CanThisClassTripleAttack() const -{ - if (!IsClient()) { - return false; // When they added the real triple attack skill, mobs lost the ability to triple - } else { - if (RuleB(Combat, ClassicTripleAttack)) { - return ( - GetLevel() >= 60 && - ( - GetClass() == Class::Warrior || - GetClass() == Class::Ranger || - GetClass() == Class::Monk || - GetClass() == Class::Berserker - ) - ); - } else { - return CastToClient()->HasSkill(EQ::skills::SkillTripleAttack); - } +bool Mob::CanThisClassTripleAttack() const { + if (!IsOfClientBot()) { + return false; // Mobs lost the ability to triple attack when the real skill was added } + + if (RuleB(Combat, ClassicTripleAttack)) { + return GetLevel() >= 60 && ( + GetClass() == Class::Warrior || + GetClass() == Class::Ranger || + GetClass() == Class::Monk || + GetClass() == Class::Berserker + ); + } + + return IsClient() ? CastToClient()->HasSkill(EQ::skills::SkillTripleAttack) + : GetSkill(EQ::skills::SkillTripleAttack) > 0; } bool Mob::CanThisClassParry(void) const @@ -4874,7 +4878,7 @@ bool Mob::HateSummon() { } else { bool target_is_client_pet = ( target->IsPet() && - target->IsPetOwnerClient() + target->IsPetOwnerOfClientBot() ); bool set_new_guard_spot = !(IsNPC() && target_is_client_pet); @@ -6014,18 +6018,17 @@ int32 Mob::GetVulnerability(Mob *caster, uint32 spell_id, uint32 ticsremaining, bool Mob::IsTargetedFocusEffect(int focus_type) { switch (focus_type) { - case focusSpellVulnerability: - case focusFcSpellDamagePctIncomingPC: - case focusFcDamageAmtIncoming: - case focusFcSpellDamageAmtIncomingPC: - case focusFcCastSpellOnLand: - case focusFcHealAmtIncoming: - case focusFcHealPctCritIncoming: - case focusFcHealPctIncoming: - return true; - default: - return false; - + case focusSpellVulnerability: + case focusFcSpellDamagePctIncomingPC: + case focusFcDamageAmtIncoming: + case focusFcSpellDamageAmtIncomingPC: + case focusFcCastSpellOnLand: + case focusFcHealAmtIncoming: + case focusFcHealPctCritIncoming: + case focusFcHealPctIncoming: + return true; + default: + return false; } } @@ -7156,56 +7159,56 @@ void Mob::SlowMitigation(Mob* caster) EQ::skills::SkillType Mob::GetSkillByItemType(int ItemType) { switch (ItemType) { - case EQ::item::ItemType1HSlash: - return EQ::skills::Skill1HSlashing; - case EQ::item::ItemType2HSlash: - return EQ::skills::Skill2HSlashing; - case EQ::item::ItemType1HPiercing: - return EQ::skills::Skill1HPiercing; - case EQ::item::ItemType1HBlunt: - return EQ::skills::Skill1HBlunt; - case EQ::item::ItemType2HBlunt: - return EQ::skills::Skill2HBlunt; - case EQ::item::ItemType2HPiercing: - if (IsClient() && CastToClient()->ClientVersion() < EQ::versions::ClientVersion::RoF2) + case EQ::item::ItemType1HSlash: + return EQ::skills::Skill1HSlashing; + case EQ::item::ItemType2HSlash: + return EQ::skills::Skill2HSlashing; + case EQ::item::ItemType1HPiercing: return EQ::skills::Skill1HPiercing; - else - return EQ::skills::Skill2HPiercing; - case EQ::item::ItemTypeBow: - return EQ::skills::SkillArchery; - case EQ::item::ItemTypeLargeThrowing: - case EQ::item::ItemTypeSmallThrowing: - return EQ::skills::SkillThrowing; - case EQ::item::ItemTypeMartial: - return EQ::skills::SkillHandtoHand; - default: - return EQ::skills::SkillHandtoHand; + case EQ::item::ItemType1HBlunt: + return EQ::skills::Skill1HBlunt; + case EQ::item::ItemType2HBlunt: + return EQ::skills::Skill2HBlunt; + case EQ::item::ItemType2HPiercing: + if (IsClient() && CastToClient()->ClientVersion() < EQ::versions::ClientVersion::RoF2) + return EQ::skills::Skill1HPiercing; + else + return EQ::skills::Skill2HPiercing; + case EQ::item::ItemTypeBow: + return EQ::skills::SkillArchery; + case EQ::item::ItemTypeLargeThrowing: + case EQ::item::ItemTypeSmallThrowing: + return EQ::skills::SkillThrowing; + case EQ::item::ItemTypeMartial: + return EQ::skills::SkillHandtoHand; + default: + return EQ::skills::SkillHandtoHand; } } uint8 Mob::GetItemTypeBySkill(EQ::skills::SkillType skill) { switch (skill) { - case EQ::skills::SkillThrowing: - return EQ::item::ItemTypeSmallThrowing; - case EQ::skills::SkillArchery: - return EQ::item::ItemTypeArrow; - case EQ::skills::Skill1HSlashing: - return EQ::item::ItemType1HSlash; - case EQ::skills::Skill2HSlashing: - return EQ::item::ItemType2HSlash; - case EQ::skills::Skill1HPiercing: - return EQ::item::ItemType1HPiercing; - case EQ::skills::Skill2HPiercing: // watch for undesired client behavior - return EQ::item::ItemType2HPiercing; - case EQ::skills::Skill1HBlunt: - return EQ::item::ItemType1HBlunt; - case EQ::skills::Skill2HBlunt: - return EQ::item::ItemType2HBlunt; - case EQ::skills::SkillHandtoHand: - return EQ::item::ItemTypeMartial; - default: - return EQ::item::ItemTypeMartial; + case EQ::skills::SkillThrowing: + return EQ::item::ItemTypeSmallThrowing; + case EQ::skills::SkillArchery: + return EQ::item::ItemTypeArrow; + case EQ::skills::Skill1HSlashing: + return EQ::item::ItemType1HSlash; + case EQ::skills::Skill2HSlashing: + return EQ::item::ItemType2HSlash; + case EQ::skills::Skill1HPiercing: + return EQ::item::ItemType1HPiercing; + case EQ::skills::Skill2HPiercing: // watch for undesired client behavior + return EQ::item::ItemType2HPiercing; + case EQ::skills::Skill1HBlunt: + return EQ::item::ItemType1HBlunt; + case EQ::skills::Skill2HBlunt: + return EQ::item::ItemType2HBlunt; + case EQ::skills::SkillHandtoHand: + return EQ::item::ItemTypeMartial; + default: + return EQ::item::ItemTypeMartial; } } @@ -7213,7 +7216,6 @@ uint16 Mob::GetWeaponSpeedbyHand(uint16 hand) { uint16 weapon_speed = 0; switch (hand) { - case 13: weapon_speed = attack_timer.GetDuration(); break; @@ -8566,8 +8568,9 @@ bool Mob::HasBotAttackFlag(Mob* tar) { return false; } + const uint16 scan_close_mobs_timer_moving = 6000; // 6 seconds -const uint16 scan_close_mobs_timer_idle = 60000; // 60 seconds +const uint16 scan_close_mobs_timer_idle = 60000; // 60 seconds // If the moving timer triggers, lets see if we are moving or idle to restart the appropriate dynamic timer void Mob::CheckScanCloseMobsMovingTimer() @@ -8596,7 +8599,7 @@ void Mob::CheckScanCloseMobsMovingTimer() } } -std::unordered_map &Mob::GetCloseMobList(float distance) +std::unordered_map& Mob::GetCloseMobList(float distance) { return entity_list.GetCloseMobList(this, distance); } @@ -8618,3 +8621,129 @@ void Mob::ClearDataBucketCache() DataBucket::DeleteFromCache(id, t); } } + +bool Mob::IsInGroupOrRaid(Mob* other, bool same_raid_group) { + if (!other || !IsOfClientBotMerc() || !other->IsOfClientBotMerc()) { + return false; + } + + if (this == other) { + return true; + } + + Raid* raid = IsBot() ? CastToBot()->GetStoredRaid() : (IsRaidGrouped() ? GetRaid() : nullptr); + + if (raid) { + if (!other->IsRaidGrouped()) { + return false; + } + + Raid* other_raid = other->IsBot() ? other->CastToBot()->GetStoredRaid() : other->GetRaid(); + + if (!other_raid) { + return false; + } + + auto raid_group = raid->GetGroup(GetCleanName()); + auto other_raid_group = other_raid->GetGroup(other->GetCleanName()); + + if ( + raid_group == RAID_GROUPLESS || + other_raid_group == RAID_GROUPLESS || + (same_raid_group && raid_group != other_raid_group) + ) { + return false; + } + + return true; + } + + Group* group = GetGroup(); + Group* other_group = other->GetGroup(); + + return group && group == other_group; +} + +bool Mob::DoLosChecks(Mob* other) { + if (!CheckLosFN(other) || !CheckWaterLoS(other)) { + if (CheckLosCheatExempt(other)) { + return true; + } + + return false; + } + + if (!CheckLosCheat(other)) { + return false; + } + + return true; +} + +bool Mob::CheckLosCheat(Mob* other) { + if (RuleB(Map, CheckForLoSCheat)) { + for (auto itr : entity_list.GetDoorsList()) { + Doors* d = itr.second; + + if ( + !d->IsDoorOpen() && + ( + d->GetKeyItem() || + d->GetLockpick() || + d->IsDoorOpen() || + d->IsDoorBlacklisted() || + d->GetNoKeyring() != 0 || + d->GetDoorParam() > 0 + ) + ) { + // If the door is a trigger door, check if the trigger door is open + if (d->GetTriggerDoorID() > 0) { + auto td = entity_list.GetDoorsByDoorID(d->GetTriggerDoorID()); + + if (td) { + if (Strings::RemoveNumbers(d->GetDoorName()) != Strings::RemoveNumbers(td->GetDoorName())) { + continue; + } + } + } + + if (DistanceNoZ(GetPosition(), d->GetPosition()) <= 50) { + auto who_to_door = DistanceNoZ(GetPosition(), d->GetPosition()); + auto other_to_door = DistanceNoZ(other->GetPosition(), d->GetPosition()); + auto who_to_other = DistanceNoZ(GetPosition(), other->GetPosition()); + auto distance_difference = who_to_other - (who_to_door + other_to_door); + + if (distance_difference >= (-1 * RuleR(Maps, RangeCheckForLoSCheat)) && distance_difference <= RuleR(Maps, RangeCheckForLoSCheat)) { + return false; + } + } + } + } + } + + return true; +} + +bool Mob::CheckLosCheatExempt(Mob* other) { + if (RuleB(Map, EnableLoSCheatExemptions)) { + /* This is an exmaple of how to configure exemptions for LoS checks. + glm::vec4 exempt_check_who; + glm::vec4 exempt_check_other; + + switch (zone->GetZoneID()) { + case POEARTHB: + exempt_check_who.x = 2051; exempt_check_who.y = 407; exempt_check_who.z = -219; //Middle of councilman spawns + //exempt_check_other.x = 1455; exempt_check_other.y = 415; exempt_check_other.z = -242; + //check to be sure the player and the target are outside of the councilman area + //if the player is inside the cove they cannot be higher than the ceiling (no exploiting from uptop) + if (GetZ() <= -171 && other->GetZ() <= -171 && DistanceNoZ(other->GetPosition(), exempt_check_who) <= 800 && DistanceNoZ(GetPosition(), exempt_check_who) <= 800) { + return true; + } + default: + return false; + } + */ + } + + return false; +} diff --git a/zone/mob.h b/zone/mob.h index e4a495d6b2..6ac286b5e2 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -405,6 +405,7 @@ class Mob : public Entity { virtual bool CheckFizzle(uint16 spell_id); virtual bool CheckSpellLevelRestriction(Mob *caster, uint16 spell_id); virtual bool IsImmuneToSpell(uint16 spell_id, Mob *caster); + virtual float GetAOERange(uint16 spell_id); void InterruptSpell(uint16 spellid = SPELL_UNKNOWN); void InterruptSpell(uint16, uint16, uint16 spellid = SPELL_UNKNOWN); @@ -513,6 +514,7 @@ class Mob : public Entity { void ApplySpellBuff(int spell_id, int duration = 0, int level_override = -1); int GetBuffStatValueBySpell(int32 spell_id, const char* stat_identifier); int GetBuffStatValueBySlot(uint8 slot, const char* stat_identifier); + virtual bool GetIllusionBlock() const { return false; } //Basic Stats/Inventory virtual void SetLevel(uint8 in_level, bool command = false) { level = in_level; } @@ -707,6 +709,7 @@ class Mob : public Entity { virtual bool HasGroup() = 0; virtual Raid* GetRaid() = 0; virtual Group* GetGroup() = 0; + bool IsInGroupOrRaid(Mob* other, bool same_raid_group = false); //Faction virtual inline int32 GetPrimaryFaction() const { return 0; } @@ -793,6 +796,10 @@ class Mob : public Entity { bool CheckLosFN(float posX, float posY, float posZ, float mobSize); static bool CheckLosFN(glm::vec3 posWatcher, float sizeWatcher, glm::vec3 posTarget, float sizeTarget); virtual bool CheckWaterLoS(Mob* m); + bool CheckPositioningLosFN(Mob* other, float posX, float posY, float posZ); + bool CheckLosCheat(Mob* other); //door skipping checks for LoS + bool CheckLosCheatExempt(Mob* other); //exemptions to bypass los + bool DoLosChecks(Mob* other); inline void SetLastLosState(bool value) { last_los_check = value; } inline bool CheckLastLosState() const { return last_los_check; } std::string GetMobDescription(); @@ -856,6 +863,7 @@ class Mob : public Entity { void ShowStats(Client* client); void ShowBuffs(Client* c); bool PlotPositionAroundTarget(Mob* target, float &x_dest, float &y_dest, float &z_dest, bool lookForAftArc = true); + virtual int GetKillExpMod() const { return 100; } // aura functions @@ -1094,6 +1102,7 @@ class Mob : public Entity { inline void SetPetOwnerClient(bool b) { pet_owner_client = b; } inline bool IsPetOwnerNPC() const { return pet_owner_npc; } inline void SetPetOwnerNPC(bool b) { pet_owner_npc = b; } + inline bool IsPetOwnerOfClientBot() const { return pet_owner_bot || pet_owner_client; } inline bool IsTempPet() const { return _IsTempPet; } inline void SetTempPet(bool value) { _IsTempPet = value; } inline bool IsHorse() { return is_horse; } @@ -1252,19 +1261,20 @@ class Mob : public Entity { float GetFixedZ(const glm::vec3 &destination, int32 z_find_offset = 5); virtual int GetStuckBehavior() const { return 0; } - void NPCSpecialAttacks(const char* parse, int permtag, bool reset = true, bool remove = false); - inline uint32 DontHealMeBefore() const { return pDontHealMeBefore; } - inline uint32 DontBuffMeBefore() const { return pDontBuffMeBefore; } - inline uint32 DontDotMeBefore() const { return pDontDotMeBefore; } - inline uint32 DontRootMeBefore() const { return pDontRootMeBefore; } - inline uint32 DontSnareMeBefore() const { return pDontSnareMeBefore; } - inline uint32 DontCureMeBefore() const { return pDontCureMeBefore; } - void SetDontRootMeBefore(uint32 time) { pDontRootMeBefore = time; } - void SetDontHealMeBefore(uint32 time) { pDontHealMeBefore = time; } - void SetDontBuffMeBefore(uint32 time) { pDontBuffMeBefore = time; } - void SetDontDotMeBefore(uint32 time) { pDontDotMeBefore = time; } - void SetDontSnareMeBefore(uint32 time) { pDontSnareMeBefore = time; } - void SetDontCureMeBefore(uint32 time) { pDontCureMeBefore = time; } + void NPCSpecialAttacks(const char *parse, int permtag, bool reset = true, bool remove = false); + inline uint32 DontHealMeBefore() const { return m_dont_heal_me_before; } + inline uint32 DontBuffMeBefore() const { return m_dont_buff_me_before; } + inline uint32 DontDotMeBefore() const { return m_dont_dot_me_before; } + inline uint32 DontRootMeBefore() const { return m_dont_root_me_before; } + inline uint32 DontSnareMeBefore() const { return m_dont_snare_me_before; } + inline uint32 DontCureMeBefore() const { return m_dont_cure_me_before; } + + void SetDontRootMeBefore(uint32 time) { m_dont_root_me_before = time; } + void SetDontHealMeBefore(uint32 time) { m_dont_heal_me_before = time; } + void SetDontBuffMeBefore(uint32 time) { m_dont_buff_me_before = time; } + void SetDontDotMeBefore(uint32 time) { m_dont_dot_me_before = time; } + void SetDontSnareMeBefore(uint32 time) { m_dont_snare_me_before = time; } + void SetDontCureMeBefore(uint32 time) { m_dont_cure_me_before = time; } // calculate interruption of spell via movement of mob void SaveSpellLoc() { m_SpellLocation = glm::vec3(m_Position); } @@ -1859,12 +1869,12 @@ class Mob : public Entity { bool pause_timer_complete; bool DistractedFromGrid; - uint32 pDontHealMeBefore; - uint32 pDontBuffMeBefore; - uint32 pDontDotMeBefore; - uint32 pDontRootMeBefore; - uint32 pDontSnareMeBefore; - uint32 pDontCureMeBefore; + uint32 m_dont_heal_me_before; + uint32 m_dont_buff_me_before; + uint32 m_dont_dot_me_before; + uint32 m_dont_root_me_before; + uint32 m_dont_snare_me_before; + uint32 m_dont_cure_me_before; // hp event int nexthpevent; diff --git a/zone/mob_ai.cpp b/zone/mob_ai.cpp index 69255073b1..f39797c108 100644 --- a/zone/mob_ai.cpp +++ b/zone/mob_ai.cpp @@ -139,7 +139,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes, bool bInnates if ( (spells[AIspells[i].spellid].target_type == ST_Target || tar == this) && tar->DontHealMeBefore() < Timer::GetCurrentTime() - && !(tar->IsPet() && tar->GetOwner()->IsClient()) //no buffing PC's pets + && !(tar->IsPet() && tar->GetOwner()->IsOfClientBot()) //no buffing PC's pets ) { auto hp_ratio = tar->GetIntHPRatio(); @@ -180,7 +180,7 @@ bool NPC::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes, bool bInnates && tar->DontBuffMeBefore() < Timer::GetCurrentTime() && !tar->IsImmuneToSpell(AIspells[i].spellid, this) && tar->CanBuffStack(AIspells[i].spellid, GetLevel(), true) >= 0 - && !(tar->IsPet() && tar->GetOwner()->IsClient() && this != tar) //no buffing PC's pets, but they can buff themself + && !(tar->IsPet() && tar->GetOwner()->IsOfClientBot() && this != tar) //no buffing PC's pets, but they can buff themself ) { if(!checked_los) { @@ -393,12 +393,12 @@ void Mob::AI_Init() minLastFightingDelayMoving = RuleI(NPC, LastFightingDelayMovingMin); maxLastFightingDelayMoving = RuleI(NPC, LastFightingDelayMovingMax); - pDontHealMeBefore = 0; - pDontBuffMeBefore = Timer::GetCurrentTime() + 400; - pDontDotMeBefore = 0; - pDontRootMeBefore = 0; - pDontSnareMeBefore = 0; - pDontCureMeBefore = 0; + m_dont_heal_me_before = 0; + m_dont_buff_me_before = Timer::GetCurrentTime() + 400; + m_dont_dot_me_before = 0; + m_dont_root_me_before = 0; + m_dont_snare_me_before = 0; + m_dont_cure_me_before = 0; } void NPC::AI_Init() @@ -1219,7 +1219,7 @@ void Mob::AI_Process() { //SE_PC_Pet_Rampage SPA 464 on pet, chance modifier - if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) { int chance = spellbonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_CHANCE] + itembonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_CHANCE] + aabonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_CHANCE]; if (chance && zone->random.Roll(chance)) { Rampage(nullptr); @@ -1262,7 +1262,7 @@ void Mob::AI_Process() { } //SE_PC_Pet_Rampage SPA 465 on pet, chance modifier - if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) { int chance = spellbonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_CHANCE] + itembonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_CHANCE] + aabonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_CHANCE]; if (chance && zone->random.Roll(chance)) { Rampage(nullptr); @@ -1945,8 +1945,8 @@ void Mob::StartEnrage() return; } - if(RuleB(NPC, LiveLikeEnrage) && !((IsPet() && !IsCharmed() && GetOwner() && GetOwner()->IsClient()) || - (CastToNPC()->GetSwarmOwner() && entity_list.GetMob(CastToNPC()->GetSwarmOwner())->IsClient()))) { + if(RuleB(NPC, LiveLikeEnrage) && !((IsPet() && !IsCharmed() && GetOwner() && GetOwner()->IsOfClientBot()) || + (CastToNPC()->GetSwarmOwner() && entity_list.GetMob(CastToNPC()->GetSwarmOwner())->IsOfClientBot()))) { return; } diff --git a/zone/npc.cpp b/zone/npc.cpp index 16027b8a6c..0e3572858f 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -3666,7 +3666,7 @@ void NPC::AIYellForHelp(Mob *sender, Mob *attacker) && mob != attacker && mob->GetPrimaryFaction() != 0 && !mob->IsEngaged() - && ((!mob->IsPet()) || (mob->IsPet() && mob->GetOwner() && !mob->GetOwner()->IsClient())) + && ((!mob->IsPet()) || (mob->IsPet() && mob->GetOwner() && !mob->GetOwner()->IsOfClientBot())) ) { /** diff --git a/zone/perl_bot.cpp b/zone/perl_bot.cpp index bd0747e948..b4aaee5454 100644 --- a/zone/perl_bot.cpp +++ b/zone/perl_bot.cpp @@ -425,11 +425,6 @@ void Perl_Bot_SetExpansionBitmask(Bot* self, int expansion_bitmask) self->SetExpansionBitmask(expansion_bitmask); } -void Perl_Bot_SetExpansionBitmask(Bot* self, int expansion_bitmask, bool save) -{ - self->SetExpansionBitmask(expansion_bitmask, save); -} - void Perl_Bot_SetSpellDuration(Bot* self, int spell_id) { self->SetSpellDuration(spell_id); @@ -620,6 +615,11 @@ void Perl_Bot_DeleteBot(Bot* self) // @categories Script Utility self->DeleteBot(); } +void Perl_Bot_RaidGroupSay(Bot* self, const char* message) // @categories Script Utility +{ + self->RaidGroupSay(self, message); +} + void perl_register_bot() { perl::interpreter state(PERL_GET_THX); @@ -649,6 +649,7 @@ void perl_register_bot() package.add("ApplySpellRaid", (void(*)(Bot*, int, int, int))&Perl_Bot_ApplySpellRaid); package.add("ApplySpellRaid", (void(*)(Bot*, int, int, int, bool))&Perl_Bot_ApplySpellRaid); package.add("ApplySpellRaid", (void(*)(Bot*, int, int, int, bool, bool))&Perl_Bot_ApplySpellRaid); + package.add("RaidGroupSay", &Perl_Bot_RaidGroupSay); package.add("Camp", (void(*)(Bot*))&Perl_Bot_Camp); package.add("Camp", (void(*)(Bot*, bool))&Perl_Bot_Camp); package.add("ClearDisciplineReuseTimer", (void(*)(Bot*))&Perl_Bot_ClearDisciplineReuseTimer); @@ -716,7 +717,6 @@ void perl_register_bot() package.add("SendPayload", (void(*)(Bot*, int, std::string))&Perl_Bot_SendPayload); package.add("SendSpellAnim", &Perl_Bot_SendSpellAnim); package.add("SetExpansionBitmask", (void(*)(Bot*, int))&Perl_Bot_SetExpansionBitmask); - package.add("SetExpansionBitmask", (void(*)(Bot*, int, bool))&Perl_Bot_SetExpansionBitmask); package.add("SetDisciplineReuseTimer", (void(*)(Bot*, uint16))&Perl_Bot_SetDisciplineReuseTimer); package.add("SetDisciplineReuseTimer", (void(*)(Bot*, uint16, uint32))&Perl_Bot_SetDisciplineReuseTimer); package.add("SetItemReuseTimer", (void(*)(Bot*, uint32))&Perl_Bot_SetItemReuseTimer); diff --git a/zone/perl_client.cpp b/zone/perl_client.cpp index 3cf1ad0d75..98bbc10c65 100644 --- a/zone/perl_client.cpp +++ b/zone/perl_client.cpp @@ -1144,7 +1144,7 @@ void Perl_Client_SetStartZone(Client* self, uint32 zone_id, float x, float y, fl void Perl_Client_KeyRingAdd(Client* self, uint32 item_id) // @categories Account and Character, Inventory and Items { - self->KeyRingAdd(item_id);; + self->KeyRingAdd(item_id); } bool Perl_Client_KeyRingCheck(Client* self, uint32 item_id) // @categories Account and Character, Inventory and Items @@ -1154,7 +1154,7 @@ bool Perl_Client_KeyRingCheck(Client* self, uint32 item_id) // @categories Accou void Perl_Client_AddPVPPoints(Client* self, uint32 points) // @categories Currency and Points { - self->AddPVPPoints(points);; + self->AddPVPPoints(points); } void Perl_Client_AddCrystals(Client* self, uint32 radiant_count, uint32 ebon_count) // @categories Currency and Points diff --git a/zone/perl_entity.cpp b/zone/perl_entity.cpp index 64860f4a27..eaad24db5e 100644 --- a/zone/perl_entity.cpp +++ b/zone/perl_entity.cpp @@ -419,10 +419,13 @@ perl::array Perl_EntityList_GetBotList(EntityList* self) // @categories Script U { perl::array result; auto current_bot_list = self->GetBotList(); - for (Bot* bot_iterator : current_bot_list) - { - result.push_back(bot_iterator); + auto it = current_bot_list.begin(); + + while (it != current_bot_list.end()) { + result.push_back(it->second); + ++it; } + return result; } diff --git a/zone/perl_mob.cpp b/zone/perl_mob.cpp index a4042dc4b8..af564d3b7b 100644 --- a/zone/perl_mob.cpp +++ b/zone/perl_mob.cpp @@ -3400,6 +3400,11 @@ bool Perl_Mob_IsPetOwnerNPC(Mob* self) return self->IsPetOwnerNPC(); } +bool Perl_Mob_IsPetOwnerOfClientBot(Mob* self) +{ + return self->IsPetOwnerOfClientBot(); +} + bool Perl_Mob_IsDestructibleObject(Mob* self) { return self->IsDestructibleObject(); @@ -3999,6 +4004,7 @@ void perl_register_mob() package.add("IsPetOwnerBot", &Perl_Mob_IsPetOwnerBot); package.add("IsPetOwnerClient", &Perl_Mob_IsPetOwnerClient); package.add("IsPetOwnerNPC", &Perl_Mob_IsPetOwnerNPC); + package.add("IsPetOwnerOfClientBot", &Perl_Mob_IsPetOwnerOfClientBot); package.add("IsPlayerCorpse", &Perl_Mob_IsPlayerCorpse); package.add("IsPureMeleeClass", &Perl_Mob_IsPureMeleeClass); package.add("IsRoamer", &Perl_Mob_IsRoamer); diff --git a/zone/perl_npc.cpp b/zone/perl_npc.cpp index a44bd146ed..ce3d8ba178 100644 --- a/zone/perl_npc.cpp +++ b/zone/perl_npc.cpp @@ -892,7 +892,7 @@ void perl_register_npc() package.add("IsGuarding", &Perl_NPC_IsGuarding); package.add("IsLDoNLocked", &Perl_NPC_IsLDoNLocked); package.add("IsLDoNTrapped", &Perl_NPC_IsLDoNTrapped); - package.add("IsLDoNTrapDetected", &Perl_NPC_IsLDoNTrapDetected);; + package.add("IsLDoNTrapDetected", &Perl_NPC_IsLDoNTrapDetected); package.add("IsOnHatelist", &Perl_NPC_IsOnHatelist); package.add("IsRaidTarget", &Perl_NPC_IsRaidTarget); package.add("IsRareSpawn", &Perl_NPC_IsRareSpawn); diff --git a/zone/questmgr.cpp b/zone/questmgr.cpp index fbeac199d9..257f088e63 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -2857,7 +2857,7 @@ bool QuestManager::createBot(const char *name, const char *lastname, uint8 level initiator->Message( Chat::White, fmt::format( - "{} has invalid characters. You can use only the A-Z, a-z and _ characters in a bot name.", + "{} has invalid characters. You can use only the A-Z, a-z and _ characters in a bot name and it must be between 4 and 15 characters long.", new_bot->GetCleanName() ).c_str() ); diff --git a/zone/raids.cpp b/zone/raids.cpp index 4dd710e3af..7bb6ab4e50 100644 --- a/zone/raids.cpp +++ b/zone/raids.cpp @@ -240,8 +240,6 @@ void Raid::AddBot(Bot* b, uint32 group, bool raid_leader, bool group_leader, boo SendRaidAddAll(b->GetName()); b->SetRaidGrouped(true); - b->p_raid_instance = this; - auto pack = new ServerPacket(ServerOP_RaidAdd, sizeof(ServerRaidGeneralAction_Struct)); auto* rga = (ServerRaidGeneralAction_Struct*) pack->pBuffer; @@ -267,6 +265,9 @@ void Raid::RemoveMember(const char *character_name) b->SetFollowID(b->GetOwner()->CastToClient()->GetID()); b->SetTarget(nullptr); b->SetRaidGrouped(false); + b->p_raid_instance = nullptr; + b->SetStoredRaid(nullptr); + b->SetVerifiedRaid(false); } disbandCheck = true; diff --git a/zone/special_attacks.cpp b/zone/special_attacks.cpp index 5e9c6f7920..71eec72558 100644 --- a/zone/special_attacks.cpp +++ b/zone/special_attacks.cpp @@ -25,6 +25,7 @@ #include "string_ids.h" #include "lua_parser.h" #include "npc.h" +#include "bot.h" #include @@ -709,54 +710,74 @@ int Mob::MonkSpecialAttack(Mob *other, uint8 unchecked_type) } void Mob::TryBackstab(Mob *other, int ReuseTime) { - if(!other) + if (!other) { return; + } bool bIsBehind = false; bool bCanFrontalBS = false; //make sure we have a proper weapon if we are a client. - if(IsClient()) { + if (IsClient()) { const EQ::ItemInstance *wpn = CastToClient()->GetInv().GetItem(EQ::invslot::slotPrimary); + if (!wpn || (wpn->GetItem()->ItemType != EQ::item::ItemType1HPiercing)){ MessageString(Chat::Red, BACKSTAB_WEAPON); return; } } + else if (IsBot()) { + auto bot = CastToBot(); + auto inst = bot->GetBotItem(EQ::invslot::slotPrimary); + auto bot_piercer = inst ? inst->GetItem() : nullptr; + + if (!bot_piercer || bot_piercer->ItemType != EQ::item::ItemType1HPiercing) { + if (!bot->GetCombatRoundForAlerts()) { + bot->SetCombatRoundForAlerts(); + bot->RaidGroupSay(this, "I can't backstab with this weapon!"); + } + return; + } + } //Live AA - Triple Backstab int tripleChance = itembonuses.TripleBackstab + spellbonuses.TripleBackstab + aabonuses.TripleBackstab; - if (BehindMob(other, GetX(), GetY())) + if (BehindMob(other, GetX(), GetY())) { bIsBehind = true; - + } else { //Live AA - Seized Opportunity int FrontalBSChance = itembonuses.FrontalBackstabChance + spellbonuses.FrontalBackstabChance + aabonuses.FrontalBackstabChance; - if (FrontalBSChance && zone->random.Roll(FrontalBSChance)) + if (FrontalBSChance && zone->random.Roll(FrontalBSChance)) { bCanFrontalBS = true; + } } if (bIsBehind || bCanFrontalBS || (IsNPC() && CanFacestab())) { // Player is behind other OR can do Frontal Backstab - if (bCanFrontalBS && IsClient()) // I don't think there is any message ... - CastToClient()->Message(Chat::White,"Your fierce attack is executed with such grace, your target did not see it coming!"); + if (bCanFrontalBS && IsClient()) { // I don't think there is any message ... + CastToClient()->Message(Chat::White, "Your fierce attack is executed with such grace, your target did not see it coming!"); + } RogueBackstab(other,false,ReuseTime); + if (level >= RuleI(Combat, DoubleBackstabLevelRequirement)) { // TODO: 55-59 doesn't appear to match just checking double attack, 60+ does though - if(IsClient() && CastToClient()->CheckDoubleAttack()) - { - if(other->GetHP() > 0) - RogueBackstab(other,false,ReuseTime); + if(IsOfClientBot() && CastToClient()->CheckDoubleAttack()) { + if (other->GetHP() > 0) { + RogueBackstab(other, false, ReuseTime); + } - if (tripleChance && other->GetHP() > 0 && zone->random.Roll(tripleChance)) - RogueBackstab(other,false,ReuseTime); + if (tripleChance && other->GetHP() > 0 && zone->random.Roll(tripleChance)) { + RogueBackstab(other, false, ReuseTime); + } } } - if(IsClient()) + if (IsClient()) { CastToClient()->CheckIncreaseSkill(EQ::skills::SkillBackstab, other, 10); + } } //Live AA - Chaotic Backstab @@ -766,14 +787,20 @@ void Mob::TryBackstab(Mob *other, int ReuseTime) { //we can stab from any angle, we do min damage though. // chaotic backstab can't double etc Seized can, but that's because it's a chance to do normal BS // Live actually added SPA 473 which grants chance to double here when they revamped chaotic/seized + RogueBackstab(other, true, ReuseTime); - if(IsClient()) + + if (IsClient()) { CastToClient()->CheckIncreaseSkill(EQ::skills::SkillBackstab, other, 10); + } + m_specialattacks = eSpecialAttacks::None; int double_bs_front = aabonuses.Double_Backstab_Front + itembonuses.Double_Backstab_Front + spellbonuses.Double_Backstab_Front; - if (double_bs_front && other->GetHP() > 0 && zone->random.Roll(double_bs_front)) + + if (double_bs_front && other->GetHP() > 0 && zone->random.Roll(double_bs_front)) { RogueBackstab(other, false, ReuseTime); + } } else { //We do a single regular attack if we attack from the front without chaotic stab Attack(other, EQ::invslot::slotPrimary); @@ -790,10 +817,25 @@ void Mob::RogueBackstab(Mob* other, bool min_damage, int ReuseTime) // make sure we can hit (bane, magical, etc) if (IsClient()) { - const EQ::ItemInstance *wpn = CastToClient()->GetInv().GetItem(EQ::invslot::slotPrimary); - if (!GetWeaponDamage(other, wpn)) + const EQ::ItemInstance* wpn = CastToClient()->GetInv().GetItem(EQ::invslot::slotPrimary); + + if (!GetWeaponDamage(other, wpn)) { return; - } else if (!GetWeaponDamage(other, (const EQ::ItemData*)nullptr)){ + } + } + else if (IsBot()) { + EQ::ItemInstance* botweaponInst = CastToBot()->GetBotItem(EQ::invslot::slotPrimary); + + if (botweaponInst) { + if (!GetWeaponDamage(other, botweaponInst)) { + return; + } + } + else if (!GetWeaponDamage(other, (const EQ::ItemData*)nullptr)) { + return; + } + } + else if (!GetWeaponDamage(other, (const EQ::ItemData*)nullptr)) { return; } @@ -2408,7 +2450,7 @@ int Mob::TryHeadShot(Mob *defender, EQ::skills::SkillType skillInUse) // Only works on YOUR target. if ( defender && - !defender->IsClient() && + !defender->IsOfClientBot() && skillInUse == EQ::skills::SkillArchery && GetTarget() == defender && (defender->GetBodyType() == BodyType::Humanoid || !RuleB(Combat, HeadshotOnlyHumanoids)) && @@ -2421,7 +2463,7 @@ int Mob::TryHeadShot(Mob *defender, EQ::skills::SkillType skillInUse) if (HeadShot_Dmg && HeadShot_Level && (defender->GetLevel() <= HeadShot_Level)) { int chance = GetDEX(); chance = 100 * chance / (chance + 3500); - if (IsClient() || IsBot()) { + if (IsOfClientBot()) { chance += GetHeroicDEX() / 25; } chance *= 10; @@ -2445,7 +2487,7 @@ int Mob::TryAssassinate(Mob *defender, EQ::skills::SkillType skillInUse) { if ( defender && - !defender->IsClient() && + !defender->IsOfClientBot() && GetLevel() >= RuleI(Combat, AssassinateLevelRequirement) && (skillInUse == EQ::skills::SkillBackstab || skillInUse == EQ::skills::SkillThrowing) && (defender->GetBodyType() == BodyType::Humanoid || !RuleB(Combat, AssassinateOnlyHumanoids)) && @@ -2454,7 +2496,7 @@ int Mob::TryAssassinate(Mob *defender, EQ::skills::SkillType skillInUse) int chance = GetDEX(); if (skillInUse == EQ::skills::SkillBackstab) { chance = 100 * chance / (chance + 3500); - if (IsClient() || IsBot()) { + if (IsOfClientBot()) { chance += GetHeroicDEX(); } chance *= 10; diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 32f6f33d9f..2365fa2fc9 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -1480,6 +1480,14 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove #ifdef SPELL_EFFECT_SPAM snprintf(effect_desc, _EDLEN, "Illusion: race %d", effect_value); #endif + if (caster && caster->IsOfClientBot()) { + Mob* target = IsClient() ? static_cast(CastToClient()) : static_cast(CastToBot()); + + if (target && target->GetIllusionBlock()) { + break; + } + } + ApplySpellEffectIllusion(spell_id, caster, buffslot, spells[spell_id].base_value[i], spells[spell_id].limit_value[i], spells[spell_id].max_value[i]); break; } @@ -1489,6 +1497,14 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove #ifdef SPELL_EFFECT_SPAM snprintf(effect_desc, _EDLEN, "Illusion Copy"); #endif + if (caster && caster->IsOfClientBot()) { + Mob* target = IsClient() ? static_cast(CastToClient()) : static_cast(CastToBot()); + + if (target && target->GetIllusionBlock()) { + break; + } + } + if(caster && caster->GetTarget()){ SendIllusionPacket ( @@ -10657,7 +10673,7 @@ int Mob::GetBuffStatValueBySlot(uint8 slot, const char* stat_identifier) if (id == "caster_level") { return buffs[slot].casterlevel; } else if (id == "spell_id") { return buffs[slot].spellid; } - else if (id == "caster_id") { return buffs[slot].spellid;; } + else if (id == "caster_id") { return buffs[slot].spellid; } else if (id == "ticsremaining") { return buffs[slot].ticsremaining; } else if (id == "counters") { return buffs[slot].counters; } else if (id == "hit_number") { return buffs[slot].hit_number; } diff --git a/zone/spells.cpp b/zone/spells.cpp index 225bf80008..feaf38bcd0 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -182,14 +182,14 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, } //Goal of Spells:UseSpellImpliedTargeting is to replicate the EQ2 feature where spells will 'pass through' invalid targets to target's target to try to find a valid target. - if (RuleB(Spells,UseSpellImpliedTargeting) && IsClient()) { + if (RuleB(Spells,UseSpellImpliedTargeting) && IsOfClientBot()) { Mob* spell_target = entity_list.GetMobID(target_id); if (spell_target) { Mob* targets_target = spell_target->GetTarget(); if (targets_target) { // If either this is beneficial and the target is not a player or player's pet or vis versa - if ((IsBeneficialSpell(spell_id) && (!(spell_target->IsClient() || (spell_target->HasOwner() && spell_target->GetOwner()->IsClient())))) - || (IsDetrimentalSpell(spell_id) && (spell_target->IsClient() || (spell_target->HasOwner() && spell_target->GetOwner()->IsClient())))) { + if ((IsBeneficialSpell(spell_id) && (!(spell_target->IsOfClientBot() || (spell_target->HasOwner() && spell_target->GetOwner()->IsOfClientBot())))) + || (IsDetrimentalSpell(spell_id) && (spell_target->IsOfClientBot() || (spell_target->HasOwner() && spell_target->GetOwner()->IsOfClientBot())))) { //Check if the target's target is a valid target; we can use DoCastingChecksOnTarget() here because we can let it handle the failure as vanilla would if (DoCastingChecksOnTarget(true, spell_id, targets_target)) { target_id = targets_target->GetID(); @@ -466,7 +466,6 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, CastedSpellFinished(spell_id, target_id, slot, mana_cost, item_slot, resist_adjust); // return true; } - // ok we know it has a cast time so we can start the timer now spellend_timer.Start(cast_time); @@ -1256,6 +1255,16 @@ void Mob::InterruptSpell(uint16 message, uint16 color, uint16 spellid) EQApplicationPacket *outapp = nullptr; uint16 message_other; bool bard_song_mode = false; //has the bard song gone to auto repeat mode + + if (IsBot()) { + auto bot = CastToBot(); + bot->SetCastedSpellType(UINT16_MAX); + + if (IsValidSpell(spellid) && bot->CheckSpellRecastTimer(spellid)) { + bot->ClearSpellRecastTimer(spellid); + } + } + if (!IsValidSpell(spellid)) { if (bardsong) { spellid = bardsong; @@ -1892,7 +1901,7 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce if(IsIllusionSpell(spell_id) && spell_target != nullptr // null ptr crash safeguard && !spell_target->IsNPC() // still self only if NPC targetted - && IsClient() + && IsOfClientBot() && (IsGrouped() // still self only if not grouped || IsRaidGrouped()) && (HasProjectIllusion())){ @@ -1910,7 +1919,14 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce // single target spells case ST_Self: { - spell_target = this; + bool bot_can_summon_corpse = IsBot() && + IsEffectInSpell(spell_id, SE_SummonCorpse) && + RuleB(Bots, AllowCommandedSummonCorpse); + + if (!bot_can_summon_corpse) { + spell_target = this; // Summon corpse spells are self-only; bots need a fallthrough + } + CastAction = SingleTarget; break; } @@ -2057,13 +2073,21 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce } case ST_Pet: { - spell_target = GetPet(); - if(!spell_target) - { + bool bot_casting_on_other_pet = IsBot() && + spell_target && + spell_target->GetOwner() != this && + RuleB(Bots, CanCastPetOnlyOnOthersPets); + + if (!bot_casting_on_other_pet) { + spell_target = GetPet(); + } + + if (!spell_target) { LogSpells("Spell [{}] canceled: invalid target (no pet)", spell_id); - MessageString(Chat::Red,NO_PET); - return false; // can't cast these unless we have a target + MessageString(Chat::Red, NO_PET); + return false; // Can't cast these unless we have a target } + CastAction = SingleTarget; break; } @@ -2145,19 +2169,26 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce case ST_Group: case ST_GroupNoPets: { - if(IsClient() && CastToClient()->TGB() && IsTGBCompatibleSpell(spell_id) && (slot != CastingSlot::Item || RuleB(Spells, AllowItemTGB))) { - if( (!target) || - (target->IsNPC() && !(target->GetOwner() && target->GetOwner()->IsClient())) || - (target->IsCorpse()) ) + bool tgb_enabled = (IsClient() && RuleB(Character, EnableTGB) && CastToClient()->TGB()) || + (IsBot() && RuleB(Bots, EnableBotTGB)); + bool tgb_compatible = IsTGBCompatibleSpell(spell_id); + bool item_tgb_allowed = (slot != CastingSlot::Item || RuleB(Spells, AllowItemTGB)); + + if (tgb_enabled && tgb_compatible && item_tgb_allowed) { + bool valid_target = spell_target && + !spell_target->IsCorpse() && + (!spell_target->IsNPC() || + (spell_target->GetOwner() && spell_target->IsPetOwnerOfClientBot())); + + if (!valid_target) { spell_target = this; - else - spell_target = target; + } } else { spell_target = this; } - if (spell_target && spell_target->IsPet() && spells[spell_id].target_type == ST_GroupNoPets){ - MessageString(Chat::Red,NO_CAST_ON_PET); + if (spell_target && spell_target->IsPet() && spells[spell_id].target_type == ST_GroupNoPets) { + MessageString(Chat::Red, NO_CAST_ON_PET); return false; } @@ -2222,14 +2253,15 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce } else if(IsBot()) { - if(IsGrouped()) - { - group_id_caster = GetGroup()->GetID(); - } - else if(IsRaidGrouped()) - { - if(GetOwner()) - group_id_caster = (GetRaid()->GetGroup(GetOwner()->CastToClient()) == 0xFFFF) ? 0 : (GetRaid()->GetGroup(GetOwner()->CastToClient()) + 1); + if (IsGrouped()) { + if (auto group = GetGroup()) { + group_id_caster = group->GetID(); + } + } else if (IsRaidGrouped()) { + if (auto raid = GetRaid()) { + uint32 group_id = raid->GetGroup(GetName()); + group_id_caster = (group_id == 0xFFFFFFFF) ? 0 : (group_id + 1); + } } } @@ -2392,8 +2424,9 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, in } } - if ((RuleB(Bots, CazicTouchBotsOwner) && spell_target && spell_target->IsBot()) && spell_id == (SPELL_CAZIC_TOUCH || spell_id == SPELL_TOUCH_OF_VINITRAS)) { + if ((RuleB(Bots, CazicTouchBotsOwner) && spell_target && spell_target->IsBot()) && (spell_id == SPELL_CAZIC_TOUCH || spell_id == SPELL_TOUCH_OF_VINITRAS)) { auto bot_owner = spell_target->GetOwner(); + if (bot_owner) { spell_target = bot_owner; } @@ -2479,11 +2512,16 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, in //range check our target, if we have one and it is not us float range = spells[spell_id].range + GetRangeDistTargetSizeMod(spell_target); - if(IsClient() && CastToClient()->TGB() && IsTGBCompatibleSpell(spell_id) && IsGroupSpell(spell_id)) + bool can_use_tgb = (IsClient() && CastToClient()->TGB()) || (IsBot() && RuleB(Bots, EnableBotTGB)); + bool is_tgb_compatible = IsTGBCompatibleSpell(spell_id); + bool is_group_spell = IsGroupSpell(spell_id); + + if (can_use_tgb && is_tgb_compatible && is_group_spell) { range = spells[spell_id].aoe_range; + } range = GetActSpellRange(spell_id, range); - if(IsClient() && IsIllusionSpell(spell_id) && (HasProjectIllusion())){ + if(IsOfClientBot() && IsIllusionSpell(spell_id) && (HasProjectIllusion())){ range = 100; } @@ -2568,7 +2606,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, in } if(IsIllusionSpell(spell_id) - && IsClient() + && IsOfClientBot() && (HasProjectIllusion())){ LogAA("Effect Project Illusion for [{}] on spell id: [{}] was ON", GetName(), spell_id); SetProjectIllusion(false); @@ -2629,7 +2667,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, in // it can affect up to 7 people if the targeted group is not our own // Allow pets who cast group spells to affect the group. - if (spell_target->IsPetOwnerClient() && IsPetOwnerClient()) { + if (spell_target->IsPetOwnerOfClientBot() && IsPetOwnerOfClientBot()) { Mob* owner = spell_target->GetOwner(); if (owner) { @@ -2645,8 +2683,8 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, in SpellOnTarget(spell_id, this); } } - } else if (spell_target->IsRaidGrouped() && spell_target->IsClient()) { - Raid *target_raid = entity_list.GetRaidByClient(spell_target->CastToClient()); + } else if (spell_target->IsRaidGrouped() && spell_target->IsOfClientBot()) { + Raid *target_raid = (IsClient() ? entity_list.GetRaidByClient(spell_target->CastToClient()) : entity_list.GetRaidByBot(spell_target->CastToBot())); uint32 gid = 0xFFFFFFFF; if (target_raid) { gid = target_raid->GetGroup(spell_target->GetName()); @@ -2804,6 +2842,13 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, CastingSlot slot, in } } } + + if (IsBot() && !isproc && !IsFromTriggeredSpell(slot, inventory_slot) && IsValidSpell(spell_id)) { + if (spells[spell_id].recast_time > 1000 && !spells[spell_id].is_discipline) { + CastToBot()->SetSpellRecastTimer(spell_id); + } + } + /* Set Recast Timer on item clicks, including augmenets. */ @@ -3395,12 +3440,17 @@ bool Mob::CheckSpellLevelRestriction(Mob *caster, uint16 spell_id) bool can_cast = true; // NON GM clients might be restricted by rule setting - if (caster->IsClient()) { + if (caster->IsOfClientBot()) { if (IsClient()) { // Only restrict client on client for this rule if (RuleB(Spells, BuffLevelRestrictions)) { check_for_restrictions = true; } } + else if (IsBot()) { + if (RuleB(Bots, BotBuffLevelRestrictions)) { + check_for_restrictions = true; + } + } } // NPCS might be restricted by rule setting else if (RuleB(Spells, NPCBuffLevelRestrictions)) { @@ -3547,6 +3597,19 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid ); } + if (caster->IsBot() && RuleB(Bots, BotsUseLiveBlockedMessage) && caster->GetClass() != Class::Bard) { + caster->GetOwner()->Message( + Chat::SpellFailure, + fmt::format( + "{}'s {} did not take hold on {}. (Blocked by {}.)", + caster->GetCleanName(), + spells[spell_id].name, + GetName(), + spells[curbuf.spellid].name + ).c_str() + ); + } + std::function f = [&]() { return fmt::format( "{} {}", @@ -3736,35 +3799,56 @@ int Mob::CanBuffStack(uint16 spellid, uint8 caster_level, bool iFailIfOverwrite) if (!IsValidSpell(curbuf.spellid)) { // if we haven't found a free slot, this is the first one so save it - if(firstfree == -2) + if (firstfree == -2) { firstfree = i; + } + continue; } - if(curbuf.spellid == spellid) - return(-1); //do not recast a buff we already have on, we recast fast enough that we dont need to refresh our buffs + if ( + IsBot() && + GetClass() == Class::Bard && + curbuf.spellid == spellid && + curbuf.ticsremaining == 0 && + curbuf.casterid == GetID() + ) { + LogAI("Bard check for song, spell [{}] has [{}] ticks remaining.", spellid, curbuf.ticsremaining); + firstfree = i; + } + else { + if (curbuf.spellid == spellid) { + return(-1); //do not recast a buff we already have on, we recast fast enough that we dont need to refresh our buffs + } + } // there's a buff in this slot ret = CheckStackConflict(curbuf.spellid, curbuf.casterlevel, spellid, caster_level, nullptr, nullptr, i); - if(ret == 1) { + + if (ret == 1) { // should overwrite current slot - if(iFailIfOverwrite) { + if (iFailIfOverwrite) { LogAIDetail("Buff [{}] would overwrite [{}] in slot [{}], reporting stack failure", spellid, curbuf.spellid, i); return(-1); } - if(firstfree == -2) + + if (firstfree == -2) { firstfree = i; + } } - if(ret == -1) { + if(ret == -1) { LogAIDetail("Buff [{}] would conflict with [{}] in slot [{}], reporting stack failure", spellid, curbuf.spellid, i); return -1; // stop the spell, can't stack it } + if (ret == 2) { //ResurrectionEffectBlock handling to move potential overwrites to a new buff slock while keeping Res Sickness LogAIDetail("Adding buff [{}] will overwrite spell [{}] in slot [{}] with caster level [{}], but ResurrectionEffectBlock is set to 2. Attempting to move [{}] to an empty buff slot.", spellid, curbuf.spellid, i, curbuf.casterlevel, spellid); + for (int x = 0; x < buff_count; x++) { const Buffs_Struct& curbuf = buffs[x]; + if (IsValidSpell(curbuf.spellid)) { continue; } @@ -3952,8 +4036,22 @@ bool Mob::SpellOnTarget( return false; } + bool client_blocked_buffs = + RuleB(Spells, EnableBlockedBuffs) && + ( + spelltar->IsClient() || + (spelltar->IsPet() && spelltar->IsPetOwnerClient()) + ); + + bool bot_blocked_buffs = + RuleB(Bots, AllowBotBlockedBuffs) && + ( + spelltar->IsBot() || + (spelltar->IsPet() && spelltar->IsPetOwnerBot()) + ); + // now check if the spell is allowed to land - if (RuleB(Spells, EnableBlockedBuffs)) { + if (client_blocked_buffs || bot_blocked_buffs) { // We return true here since the caster's client should act like normal if (spelltar->IsBlockedBuff(spell_id)) { LogSpells( @@ -4389,6 +4487,18 @@ bool Mob::SpellOnTarget( } else { MessageString(Chat::SpellFailure, TARGET_RESISTED, spells[spell_id].name); spelltar->MessageString(Chat::SpellFailure, YOU_RESIST, spells[spell_id].name); + + if (IsBot() && RuleB(Bots, ShowResistMessagesToOwner)) { + CastToBot()->GetBotOwner()->Message + (Chat::SpellFailure, + fmt::format( + "{} resisted {}'s spell: {}.", + spelltar->GetCleanName(), + GetCleanName(), + spells[spell_id].name + ).c_str() + ); + } } if (spelltar->IsAIControlled()) { @@ -4606,6 +4716,14 @@ bool Mob::SpellOnTarget( LogSpells("Cast of [{}] by [{}] on [{}] complete successfully", spell_id, GetName(), spelltar->GetName()); + if (IsBot() && (CastToBot()->GetCastedSpellType() != UINT16_MAX)) { + if (!CastToBot()->IsCommandedSpell()) { + CastToBot()->SetBotSpellRecastTimer(CastToBot()->GetCastedSpellType(), spelltar); + } + + CastToBot()->SetCastedSpellType(UINT16_MAX); + } + return true; } @@ -5253,7 +5371,7 @@ float Mob::ResistSpell(uint8 resist_type, uint16 spell_id, Mob *caster, bool use // JULY 24, 2002 changes int level = GetLevel(); - if (RuleB(Spells,July242002PetResists) && IsPetOwnerClient() && caster->IsNPC() && !caster->IsPetOwnerClient()) { + if (RuleB(Spells,July242002PetResists) && IsPetOwnerOfClientBot() && caster->IsNPC() && !caster->IsPetOwnerOfClientBot()) { auto owner = GetOwner(); if (owner != nullptr) { target_resist = std::max(target_resist, owner->GetResist(resist_type)); @@ -6893,7 +7011,7 @@ void Mob::BeamDirectional(uint16 spell_id, int16 resist_adjust) float b = (GetY() * dX - dY * GetX()) / (dX - GetX()); while (iter != targets_in_range.end()) { - if (!(*iter) || (beneficial_targets && ((*iter)->IsNPC() && !(*iter)->IsPetOwnerClient())) || + if (!(*iter) || (beneficial_targets && ((*iter)->IsNPC() && !(*iter)->IsPetOwnerOfClientBot())) || (*iter)->BehindMob(this, (*iter)->GetX(), (*iter)->GetY())) { ++iter; continue; @@ -6977,7 +7095,7 @@ void Mob::ConeDirectional(uint16 spell_id, int16 resist_adjust) auto iter = targets_in_range.begin(); while (iter != targets_in_range.end()) { - if (!(*iter) || (beneficial_targets && ((*iter)->IsNPC() && !(*iter)->IsPetOwnerClient()))) { + if (!(*iter) || (beneficial_targets && ((*iter)->IsNPC() && !(*iter)->IsPetOwnerOfClientBot()))) { ++iter; continue; } diff --git a/zone/tune.cpp b/zone/tune.cpp index e46a8b32f3..39d6e9bdb2 100644 --- a/zone/tune.cpp +++ b/zone/tune.cpp @@ -853,7 +853,7 @@ int64 Mob::TuneNPCAttack(Mob* other, bool no_avoid, bool no_hit_chance, int hit_ TuneDoAttack(other, my_hit, nullptr, no_avoid, no_hit_chance, ac_override, add_ac, avoidance_override, accuracy_override, add_avoidance, add_accuracy); LogCombat("Final damage against [{}]: [{}]", other->GetName(), my_hit.damage_done); - if (other->IsClient() && IsPet() && GetOwner()->IsClient()) { + if (other->IsClient() && IsPet() && GetOwner()->IsOfClientBot()) { //pets do half damage to clients in pvp my_hit.damage_done /= 2; if (my_hit.damage_done < 1) @@ -1556,7 +1556,7 @@ void Mob::TuneCommonOutgoingHitSuccess(Mob* defender, DamageHitInfo &hit, ExtraA int mod = GetSpecialAbilityParam(SpecialAbility::Rampage, 2); if (mod > 0) spec_mod = mod; - if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) { //SE_PC_Pet_Rampage SPA 464 on pet, damage modifier int spell_mod = spellbonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD] + itembonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD] + aabonuses.PC_Pet_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD]; if (spell_mod > spec_mod) @@ -1567,7 +1567,7 @@ void Mob::TuneCommonOutgoingHitSuccess(Mob* defender, DamageHitInfo &hit, ExtraA int mod = GetSpecialAbilityParam(SpecialAbility::AreaRampage, 2); if (mod > 0) spec_mod = mod; - if ((IsPet() || IsTempPet()) && IsPetOwnerClient()) { + if ((IsPet() || IsTempPet()) && IsPetOwnerOfClientBot()) { //SE_PC_Pet_AE_Rampage SPA 465 on pet, damage modifier int spell_mod = spellbonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD] + itembonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD] + aabonuses.PC_Pet_AE_Rampage[SBIndex::PET_RAMPAGE_DMG_MOD]; if (spell_mod > spec_mod)